@amcharts/amcharts5
Version:
amCharts 5
772 lines • 31.2 kB
JavaScript
import { __awaiter } from "tslib";
import { Component } from "../../core/render/Component";
import { List } from "../../core/util/List";
import { Color } from "../../core/util/Color";
import { percentInterpolate } from "../../core/util/Animation";
import { Percent } from "../../core/util/Percent";
import { p100 } from "../../core/util/Percent";
import { Container } from "../../core/render/Container";
import { Label } from "../../core/render/Label";
//import { Animations } from "../../core/util/Animation";
import * as $array from "../../core/util/Array";
import * as $type from "../../core/util/Type";
import * as $time from "../../core/util/Time";
/**
* A base class for all series.
*/
export class Series extends Component {
constructor() {
super(...arguments);
this._aggregatesCalculated = false;
this._selectionAggregatesCalculated = false;
this._dataProcessed = false;
this._baseSeriesDirty = false;
/**
* List of bullets to use for the series.
*
* @see {@link https://www.amcharts.com/docs/v5/concepts/common-elements/bullets/} for more info
*/
this.bullets = new List();
/**
* A [[Container]] series' bullets are stored in.
*
* @default Container.new()
*/
this.bulletsContainer = Container.new(this._root, { width: p100, height: p100, position: "absolute" });
}
_afterNew() {
this.valueFields.push("value", "customValue");
this.fields.push("url");
super._afterNew();
this.setPrivate("customData", {});
this._disposers.push(this.bullets.events.onAll(() => {
this._handleBullets(this.dataItems);
}));
}
_dispose() {
this.bulletsContainer.dispose(); // can be in a different parent
super._dispose();
}
startIndex() {
let len = this.dataItems.length;
return Math.min(this.getPrivate("startIndex", 0), len);
}
endIndex() {
let len = this.dataItems.length;
return Math.max(0, Math.min(this.getPrivate("endIndex", len), len));
}
_handleBullets(dataItems) {
$array.each(dataItems, (dataItem) => {
const bullets = dataItem.bullets;
if (bullets) {
$array.each(bullets, (bullet) => {
bullet.dispose();
});
dataItem.bullets = undefined;
}
});
this.markDirtyValues();
}
_makeBullets(dataItem) {
if (this._shouldMakeBullet(dataItem)) {
dataItem.bullets = [];
this.bullets.each((bulletFunction) => {
this._makeBullet(dataItem, bulletFunction);
});
}
}
_shouldMakeBullet(_dataItem) {
return true;
}
_makeBullet(dataItem, bulletFunction, index) {
const bullet = bulletFunction(this._root, this, dataItem);
if (bullet) {
bullet._index = index;
this._makeBulletReal(dataItem, bullet);
}
return bullet;
}
/**
* Container a data item's bullets are added to. Series that lay their data
* items out as separate containers can put bullets into the item itself, so
* that they move with it and can be stacked among its other children.
*/
_getBulletContainer(_dataItem) {
return this.bulletsContainer;
}
_makeBulletReal(dataItem, bullet) {
let sprite = bullet.get("sprite");
if (sprite) {
sprite._setDataItem(dataItem);
sprite.setRaw("position", "absolute");
this._applyBulletDefaultPaint(sprite, dataItem);
this._linkSprite(sprite, dataItem);
this._getBulletContainer(dataItem).children.push(sprite);
}
bullet.series = this;
dataItem.bullets.push(bullet);
}
/**
* Makes an element open its data item's URL when clicked.
*
* @ignore
*/
_linkSprite(sprite, dataItem) {
// the url can come or go later (setIndex, urlField set), so the click and pointer follow it
let clicking = false;
const sync = (url) => {
if (url && !clicking) {
clicking = true;
sprite.events.on("click", () => {
this.openUrl(dataItem);
});
}
// a cursor someone chose stays; only the library's own pointer follows the url
if (sprite.isUserSetting("cursorOverStyle") && !sprite.isComputedSetting("cursorOverStyle")) {
return;
}
if (url) {
sprite._setC("cursorOverStyle", "pointer");
}
else if (sprite.get("cursorOverStyle") == "pointer") {
sprite.remove("cursorOverStyle");
}
};
sync(dataItem.get("url"));
sprite.addDisposer(dataItem.on("url", (url) => sync(url)));
}
/**
* Opens a data item's URL in the window set by `linkTarget`.
*
* Override to intercept links, e.g. in an editor.
*
* @param dataItem Data item
* @since 5.20.7
*/
openUrl(dataItem) {
const url = dataItem.get("url");
// a script url would run code in the chart's page, not go anywhere
if (url && !/^(javascript|data|vbscript):/i.test(String(url).replace(/[\s\x00-\x1f]/g, ""))) {
const target = this.get("linkTarget", "_self");
window.open(url, target, target == "_blank" ? "noopener" : undefined);
}
}
/**
* A bullet whose shape defines none of its own paint inherits the full set
* from the series, so bullets match their series without restating the color
* (like columns do). It's all-or-nothing: if the bullet sets even one of
* `fill`/`fillGradient`/`fillPattern`/`stroke`/`strokeGradient`, it is left
* entirely alone. Only `Graphics`-based shapes are touched; container/label/
* picture bullets are left to style their own children.
*
* @ignore
*/
_applyBulletDefaultPaint(sprite, dataItem) {
if (sprite.isType("Graphics")) {
const unstyled = sprite.get("fill") == null
&& sprite.get("fillGradient") == null
&& sprite.get("fillPattern") == null
&& sprite.get("stroke") == null
&& sprite.get("strokeGradient") == null;
if (unstyled) {
// pie slices, flow nodes and hierarchy nodes hold their color on the data item
const itemFill = dataItem ? dataItem.get("fill") : undefined;
sprite.set("fill", itemFill != null ? itemFill : this.get("fill"));
sprite.set("fillGradient", this.get("fillGradient"));
sprite.set("fillPattern", this.get("fillPattern"));
sprite.set("stroke", this.get("stroke"));
sprite.set("strokeGradient", this.get("strokeGradient"));
}
}
}
/**
* Adds bullet directly to a data item.
*
* Please note: method accepts [[Bullet]] instance as a paramter, not a
* reference to a function.
*
* You should add Bullet instance, not a method like you do it on series.
*
* @see {@link https://www.amcharts.com/docs/v5/concepts/common-elements/bullets/#Adding_directly_to_data_item} for more info
* @since 5.6.0
*
* @param dataItem Target data item
* @param bullet Bullet instance
*/
addBullet(dataItem, bullet) {
if (!dataItem.bullets) {
dataItem.bullets = [];
}
if (bullet) {
this._makeBulletReal(dataItem, bullet);
}
}
_clearDirty() {
super._clearDirty();
this._aggregatesCalculated = false;
this._baseSeriesDirty = false;
this._selectionAggregatesCalculated = false;
}
_prepareChildren() {
super._prepareChildren();
const dataItems = this.dataItems;
const count = dataItems.length;
let startIndex = this.startIndex();
let endIndex = this.endIndex();
// links switched on, off or moved to another field after the items exist
if (this.isDirty("urlField")) {
this._updateFields();
const urlField = this.get("urlField");
$array.each(dataItems, (dataItem) => {
const context = dataItem.dataContext;
dataItem.set("url", urlField && context ? context[urlField] : undefined);
});
}
if (this.isDirty("name")) {
this.updateLegendValue();
}
if (this.isDirty("heatRules")) {
this._valuesDirty = true;
}
if (this.isPrivateDirty("baseValueSeries")) {
const baseValueSeries = this.getPrivate("baseValueSeries");
if (baseValueSeries) {
this._disposers.push(baseValueSeries.onPrivate("startIndex", () => {
this._baseSeriesDirty = true;
this.markDirtyValues();
}));
}
}
const calculateAggregates = this.get("calculateAggregates");
if (calculateAggregates) {
if (this._valuesDirty && !this._dataProcessed) {
if (!this._aggregatesCalculated) {
this._calculateAggregates(0, count);
this._aggregatesCalculated = true;
if (startIndex != 0) {
this._psi = undefined;
}
}
}
if ((this._psi != startIndex || this._pei != endIndex || this.isPrivateDirty("adjustedStartIndex")) && !this._selectionAggregatesCalculated) {
if (startIndex === 0 && endIndex === count && this._aggregatesCalculated) {
// void
}
else {
this._calculateAggregates(startIndex, endIndex);
}
this._selectionAggregatesCalculated = true;
}
}
if (this.isDirty("tooltip")) {
let tooltip = this.get("tooltip");
if (tooltip) {
tooltip.hide(0);
tooltip.set("tooltipTarget", this);
}
}
if (this.isDirty("fill") || this.isDirty("stroke")) {
let markerRectangle;
const legendDataItem = this.get("legendDataItem");
if (legendDataItem) {
markerRectangle = legendDataItem.get("markerRectangle");
if (markerRectangle) {
if (this.isVisible()) {
if (this.isDirty("stroke")) {
let stroke = this.get("stroke");
markerRectangle.set("stroke", stroke);
}
if (this.isDirty("fill")) {
let fill = this.get("fill");
markerRectangle.set("fill", fill);
}
}
}
}
this.updateLegendMarker(undefined);
}
if (this.bullets.length > 0) {
let startIndex = this.startIndex();
let endIndex = this.endIndex();
if (endIndex < count) {
endIndex++;
}
for (let i = startIndex; i < endIndex; i++) {
let dataItem = dataItems[i];
if (!dataItem.bullets) {
this._makeBullets(dataItem);
}
}
}
}
_handleRemoved() {
}
/**
* @ignore
*/
_adjustStartIndex(index) {
return index;
}
_calculateAggregates(startIndex, endIndex) {
let fields = this._valueFields;
if (!fields) {
throw new Error("No value fields are set for the series.");
}
const excludeFromAggregate = this.get("excludeFromAggregate");
if (excludeFromAggregate) {
fields = fields.filter((field) => {
return excludeFromAggregate.indexOf(field) == -1;
});
}
const sum = {};
const absSum = {};
const count = {};
const low = {};
const high = {};
const open = {};
const close = {};
const average = {};
const previous = {};
for (let f = 0, flen = fields.length; f < flen; f++) {
let key = fields[f];
sum[key] = 0;
absSum[key] = 0;
count[key] = 0;
}
const dataItems = this.dataItems;
const len = dataItems.length;
for (let f = 0, flen = fields.length; f < flen; f++) {
let key = fields[f];
let change = key + "Change";
let changePercent = key + "ChangePercent";
let changePrevious = key + "ChangePrevious";
let changePreviousPercent = key + "ChangePreviousPercent";
let changeSelection = key + "ChangeSelection";
let changeSelectionPercent = key + "ChangeSelectionPercent";
let openKey = "valueY";
if (key === "valueX" || key.endsWith("ValueX")) {
openKey = "valueX";
}
const baseValueSeries = this.getPrivate("baseValueSeries");
const adjustedStartIndex = this.getPrivate("adjustedStartIndex", startIndex);
const calculateChangesForItem = (dataItem, value, key) => {
if (startIndex === 0) {
const openValue = open[openKey];
const changeValue = value - openValue;
dataItem.setRaw((change), changeValue);
dataItem.setRaw((changePercent), changeValue / openValue * 100);
}
const changePreviousValue = value - previous[openKey];
const changeSelectionValue = value - open[openKey];
dataItem.setRaw((changePrevious), changePreviousValue);
dataItem.setRaw((changePreviousPercent), changePreviousValue / previous[openKey] * 100);
dataItem.setRaw((changeSelection), changeSelectionValue);
dataItem.setRaw((changeSelectionPercent), changeSelectionValue / open[openKey] * 100);
previous[key] = value;
};
// find first non-null value to initialize open, low, high instead of checking in loop
for (let i = adjustedStartIndex; i < endIndex; i++) {
const dataItem = dataItems[i];
if (dataItem) {
let value = dataItem.get(key);
if (value != null) {
if (low[key] == null) {
low[key] = value;
}
if (high[key] == null) {
high[key] = value;
}
if (open[key] == null) {
open[key] = value;
previous[key] = value;
if (baseValueSeries) {
open[openKey] = baseValueSeries._getBase(openKey);
}
}
// stop cycle
break;
}
}
}
for (let i = adjustedStartIndex; i < endIndex; i++) {
const dataItem = dataItems[i];
if (dataItem) {
let value = dataItem._settings[key];
if (value != null) {
count[key]++;
sum[key] += value;
absSum[key] += Math.abs(value);
if (low[key] > value) {
low[key] = value;
}
if (high[key] < value) {
high[key] = value;
}
close[key] = value;
calculateChangesForItem(dataItem, value, key);
}
}
}
average[key] = sum[key] / count[key];
if (endIndex < len) {
const dataItem = dataItems[endIndex];
if (dataItem) {
let value = dataItem.get(key);
if (value != null) {
calculateChangesForItem(dataItem, value, key);
}
}
}
if (endIndex + 1 < len) {
const dataItem = dataItems[endIndex + 1];
if (dataItem) {
let value = dataItem.get(key);
if (value != null) {
calculateChangesForItem(dataItem, value, key);
}
}
}
if (startIndex > 0) {
startIndex--;
}
delete previous[key];
for (let i = startIndex; i < adjustedStartIndex; i++) {
const dataItem = dataItems[i];
if (dataItem) {
let value = dataItem.get(key);
if (previous[key] == null) {
previous[key] = value;
}
if (value != null) {
calculateChangesForItem(dataItem, value, key);
}
}
}
}
for (let f = 0, flen = fields.length; f < flen; f++) {
let key = fields[f];
this.setPrivate((key + "AverageSelection"), average[key]);
this.setPrivate((key + "CountSelection"), count[key]);
this.setPrivate((key + "SumSelection"), sum[key]);
this.setPrivate((key + "AbsoluteSumSelection"), absSum[key]);
this.setPrivate((key + "LowSelection"), low[key]);
this.setPrivate((key + "HighSelection"), high[key]);
this.setPrivate((key + "OpenSelection"), open[key]);
this.setPrivate((key + "CloseSelection"), close[key]);
}
if (startIndex === 0 && endIndex === len) {
for (let f = 0, flen = fields.length; f < flen; f++) {
let key = fields[f];
this.setPrivate((key + "Average"), average[key]);
this.setPrivate((key + "Count"), count[key]);
this.setPrivate((key + "Sum"), sum[key]);
this.setPrivate((key + "AbsoluteSum"), absSum[key]);
this.setPrivate((key + "Low"), low[key]);
this.setPrivate((key + "High"), high[key]);
this.setPrivate((key + "Open"), open[key]);
this.setPrivate((key + "Close"), close[key]);
}
}
}
_updateChildren() {
super._updateChildren();
this._psi = this.startIndex();
this._pei = this.endIndex();
if (this.isDirty("visible")) {
this.bulletsContainer.set("visible", this.get("visible"));
}
// Apply heat rules
const rules = this.get("heatRules");
if (this._valuesDirty && rules && rules.length > 0) {
$array.each(rules, (rule) => {
const minValue = rule.minValue || this.getPrivate((rule.dataField + "Low")) || 0;
const maxValue = rule.maxValue || this.getPrivate((rule.dataField + "High")) || 0;
$array.each(rule.target._entities, (target) => {
const value = target.dataItem.get(rule.dataField);
if (!$type.isNumber(value)) {
if (rule.neutral) {
target._setC(rule.key, rule.neutral);
}
const states = target.states;
if (states) {
const defaultState = states.lookup("default");
if (defaultState && rule.neutral) {
defaultState._setC(rule.key, rule.neutral);
}
}
if (!rule.customFunction) {
return;
}
}
if (rule.customFunction) {
rule.customFunction.call(this, target, minValue, maxValue, value);
}
else {
let percent;
if (rule.logarithmic) {
percent = (Math.log(value) * Math.LOG10E - Math.log(minValue) * Math.LOG10E) / ((Math.log(maxValue) * Math.LOG10E - Math.log(minValue) * Math.LOG10E));
}
else {
percent = (value - minValue) / (maxValue - minValue);
}
if ($type.isNumber(value) && (!$type.isNumber(percent) || Math.abs(percent) == Infinity)) {
percent = 0.5;
}
// fixes problems if all values are the same
let propertyValue;
if ($type.isNumber(rule.min)) {
propertyValue = rule.min + (rule.max - rule.min) * percent;
}
else if (rule.min instanceof Color) {
propertyValue = Color.interpolate(percent, rule.min, rule.max);
}
else if (rule.min instanceof Percent) {
propertyValue = percentInterpolate(percent, rule.min, rule.max);
}
target._setC(rule.key, propertyValue);
const states = target.states;
if (states) {
const defaultState = states.lookup("default");
if (defaultState) {
defaultState._setC(rule.key, propertyValue);
}
}
}
});
});
}
if (this.get("visible")) {
const dataItems = this.dataItems;
let count = dataItems.length;
let startIndex = this.startIndex();
let endIndex = this.endIndex();
if (endIndex < count) {
endIndex++;
}
if (startIndex > 0) {
startIndex--;
}
for (let i = 0; i < startIndex; i++) {
this._hideBullets(dataItems[i]);
}
for (let i = startIndex; i < endIndex; i++) {
this._positionBullets(dataItems[i]);
}
for (let i = endIndex; i < count; i++) {
this._hideBullets(dataItems[i]);
}
}
}
_positionBullets(dataItem) {
if (dataItem.bullets) {
$array.each(dataItem.bullets, (bullet) => {
this._positionBullet(bullet);
const sprite = bullet.get("sprite");
if (bullet.get("dynamic")) {
if (sprite) {
sprite._markDirtyKey("fill");
sprite.markDirtySize();
}
if (sprite instanceof Container) {
sprite.walkChildren((child) => {
child._markDirtyKey("fill");
child.markDirtySize();
if (child instanceof Label) {
child.text.markDirtyText();
}
});
}
}
if (sprite instanceof Label && sprite.get("populateText")) {
sprite.text.markDirtyText();
}
});
}
}
_hideBullets(dataItem) {
if (dataItem.bullets) {
$array.each(dataItem.bullets, (bullet) => {
let sprite = bullet.get("sprite");
if (sprite) {
sprite.setPrivate("visible", false);
}
});
}
}
_positionBullet(_bullet) {
}
_placeBulletsContainer(chart) {
chart.bulletsContainer.children.moveValue(this.bulletsContainer);
}
_removeBulletsContainer() {
const bulletsContainer = this.bulletsContainer;
if (bulletsContainer.parent) {
bulletsContainer.parent.children.removeValue(bulletsContainer);
}
}
/**
* @ignore
*/
disposeDataItem(dataItem) {
//super.disposeDataItem(dataItem); // does nothing
const bullets = dataItem.bullets;
if (bullets) {
$array.each(bullets, (bullet) => {
bullet.dispose();
});
dataItem.bullets = undefined;
}
}
_getItemReaderLabel() {
return "";
}
/**
* Shows series's data item.
*
* @param dataItem Data item
* @param duration Animation duration in milliseconds
* @return Promise
*/
showDataItem(dataItem, duration) {
const _super = Object.create(null, {
showDataItem: { get: () => super.showDataItem }
});
return __awaiter(this, void 0, void 0, function* () {
const promises = [_super.showDataItem.call(this, dataItem, duration)];
const bullets = dataItem.bullets;
if (bullets) {
$array.each(bullets, (bullet) => {
const sprite = bullet.get("sprite");
if (sprite) {
promises.push(sprite.show(duration));
}
});
}
yield Promise.all(promises);
});
}
/**
* Hides series's data item.
*
* @param dataItem Data item
* @param duration Animation duration in milliseconds
* @return Promise
*/
hideDataItem(dataItem, duration) {
const _super = Object.create(null, {
hideDataItem: { get: () => super.hideDataItem }
});
return __awaiter(this, void 0, void 0, function* () {
const promises = [_super.hideDataItem.call(this, dataItem, duration)];
const bullets = dataItem.bullets;
if (bullets) {
$array.each(bullets, (bullet) => {
const sprite = bullet.get("sprite");
if (sprite) {
promises.push(sprite.hide(duration));
}
});
}
yield Promise.all(promises);
});
}
_sequencedShowHide(show, duration) {
return __awaiter(this, void 0, void 0, function* () {
if (this.get("sequencedInterpolation")) {
if (!$type.isNumber(duration)) {
duration = this.get("interpolationDuration", 0);
}
if (duration > 0) {
const startIndex = this.startIndex();
const endIndex = this.endIndex();
yield Promise.all($array.map(this.dataItems, (dataItem, i) => __awaiter(this, void 0, void 0, function* () {
let realDuration = duration || 0;
if (i < startIndex - 10 || i > endIndex + 10) {
realDuration = 0;
}
//let delay = this.get("sequencedDelay", 0) * i + realDuration * (i - startIndex) / (endIndex - startIndex);
let delay = this.get("sequencedDelay", 0) + realDuration / (endIndex - startIndex);
yield $time.sleep(delay * (i - startIndex));
if (show) {
yield this.showDataItem(dataItem, realDuration);
}
else {
yield this.hideDataItem(dataItem, realDuration);
}
})));
}
else {
yield Promise.all($array.map(this.dataItems, (dataItem) => {
if (show) {
return this.showDataItem(dataItem, 0);
}
else {
return this.hideDataItem(dataItem, 0);
}
}));
}
}
});
}
/**
* @ignore
*/
updateLegendValue(dataItem) {
if (dataItem) {
const legendDataItem = dataItem.get("legendDataItem");
if (legendDataItem) {
const valueLabel = legendDataItem.get("valueLabel");
if (valueLabel) {
const text = valueLabel.text;
let txt = "";
valueLabel._setDataItem(dataItem);
txt = this.get("legendValueText", text.get("text", ""));
valueLabel.set("text", txt);
text.markDirtyText();
}
const label = legendDataItem.get("label");
if (label) {
const text = label.text;
let txt = "";
label._setDataItem(dataItem);
txt = this.get("legendLabelText", text.get("text", ""));
label.set("text", txt);
text.markDirtyText();
}
}
}
}
/**
* @ignore
*/
updateLegendMarker(_dataItem) {
}
_onHide() {
super._onHide();
const tooltip = this.getTooltip();
if (tooltip) {
tooltip.hide();
}
}
/**
* @ignore
*/
hoverDataItem(_dataItem) { }
/**
* @ignore
*/
unhoverDataItem(_dataItem) { }
/**
* @ignore
*/
_getBase(key) {
const dataItem = this.dataItems[this.startIndex()];
if (dataItem) {
return dataItem.get(key);
}
return 0;
}
}
Series.className = "Series";
Series.classNames = Component.classNames.concat([Series.className]);
//# sourceMappingURL=Series.js.map