@amcharts/amcharts5
Version:
amCharts 5
1,436 lines • 66.5 kB
JavaScript
import { Entity } from "../../core/util/Entity";
import { Container } from "../../core/render/Container";
import { DataItem } from "../../core/render/Component";
import { Color } from "../../core/util/Color";
import { ListTemplate } from "../../core/util/List";
import { Serializer } from "./Serializer";
import * as $object from "../../core/util/Object";
import * as $type from "../../core/util/Type";
import * as $array from "../../core/util/Array";
/**
* Serializes whole charts into simple objects or JSON.
*
* @see {@link https://www.amcharts.com/docs/v5/concepts/serializing/chart-serializer/} for more info
* @since 5.15.0
*/
export class ChartSerializer extends Serializer {
constructor() {
super(...arguments);
this._disableDataSerialization = false;
/**
* Settings that hold another series (a map line series' `pointSeries`, say).
* They are written out as a reference, but the series they point at may not
* have been given one yet, so they are collected and filled in at the end.
*/
this._pendingSeriesRefs = [];
/**
* Work that cannot be done until every series has been given an id - a legend
* that sits above its chart is reached before the series it lists.
*/
this._deferred = [];
this._globalRefs = [];
this._internalRefs = {};
this._counters = {};
this._lists = {};
this._mainRefs = {};
}
_afterNew() {
super._afterNew();
this._setSoft("maxDepth", 10);
this._setSoft("removeEmptyObjects", true);
this._setSoft("includeRoot", false);
this._setSoft("includeEvents", true);
this._setSoft("includeAdapters", true);
this._setSoft("includeStates", true);
this._setSoft("functionsAs", "function");
//this._setSoft("includeProperties", ["label"]);
this._setSoft("excludeSettings", [
"chart",
"draw",
"curveFactory",
"tooltipDataItem",
"legendDataItem",
"vcx",
"vcy",
"bounds",
"pointTo",
"tooltipTarget",
"translateX",
"translateY",
// Handled directly
"heatRules"
]); // "x", "y"
this.adapters.add("excludeProperties", (exclude) => {
if (this._disableDataSerialization) {
return (exclude || []).concat(["data"]);
}
return exclude;
});
this.reset();
}
reset() {
this._disableDataSerialization = false;
this._pendingSeriesRefs = [];
this._deferred = [];
this._globalRefs = [];
this._internalRefs = {};
this._counters = {};
this._lists = {
data: {},
axes: {},
series: {},
sprites: {},
elements: {}
};
this._mainRefs = {
functions: [],
data: [],
templates: [],
axes: [],
series: [],
axisRanges: [],
seriesAxisRanges: []
};
}
/**
* Serializes target object into a simple object.
*
* @param source Target object
* @return Serialized data
*/
serializeAll(source) {
// This will go as deep in the structure to find either chart or a series,
// then will pass on to specialized serializers
// Map out the structure
this._examineStructure(source);
// Kick off serializing
const result = this._serializeEntity(source);
// Now that every series has been given an id, the settings that point at
// one can say so
const referenced = {};
$array.each(this._pendingSeriesRefs, (pending) => {
const id = this._getInternalRef(pending.series.uid);
if (id) {
pending.node[pending.key] = "#" + id;
const holder = this._getInternalRef(pending.holder.uid);
if (holder) {
if (!referenced[holder]) {
referenced[holder] = [];
}
referenced[holder].push(id);
}
}
else {
// Nothing to point at - the series is not part of what was
// serialized. Better an absent setting than a second, empty copy
// of a series that already exists somewhere else.
delete pending.node[pending.key];
}
});
this._orderSeriesRefs(referenced);
// Anything that had to wait for every series to have an id
$array.each(this._deferred, (run) => {
run();
});
// Finally, add in global references
result.refs = [];
$object.each(this._mainRefs, (_key, value) => {
if (value.length) {
value.forEach((item) => {
result.refs.push(item);
});
}
});
// Root settings/properties
if (this.get("includeRoot")) {
result.root = this._serializeRoot();
}
// Cleanup
if (this.get("removeEmptyObjects")) {
this._pruneEmptyObjects(result);
}
return result;
}
_serializeRoot() {
const root = this.root;
const result = { properties: {}, settings: {} };
// Properties. Entity-based properties (formatters) are serialized into a
// `settings` block (no `type`) so they merge into the existing instance on
// parse; primitives are included as-is.
// What a `Root` starts out with, so that only a changed one is written out
const rootDefaults = { utc: false, tabindex: 0 };
$array.each(["utc", "fps", "numberFormatter", "dateFormatter", "durationFormatter", "tabindex", "interfaceColors"], (key) => {
const value = root[key];
if (value === undefined || value === rootDefaults[key]) {
return;
}
if (value instanceof Entity) {
const serialized = this.serialize(value, 0, false);
this._stripType(serialized);
result.properties[key] = serialized;
}
else {
result.properties[key] = value;
}
});
// A locale is a pack loaded as a global (`am5locales_de_DE`), so it goes
// out as that name - the same as a geodata pack - rather than as a copy of
// every translation in it
if (root.locale && typeof window !== "undefined") {
const global = window;
$array.eachContinue($object.keys(global), (key) => {
if (key.indexOf("am5locales_") === 0 && global[key] === root.locale) {
result.properties.locale = key;
return false;
}
return true;
});
}
// Settings from `Root._settings`, minus the ones the Root filled in for
// itself
const settings = {};
$object.each(root.settings, (key, value) => {
if (!root._computedSettings[key]) {
settings[key] = value;
}
});
result.settings = this.serialize(settings, 0, true);
return result;
}
_serializeEntity(source) {
// Initialize the result object
let result;
// Process series differently
if (source.isType("Series") && !this._isLegend(source)) {
this._disableDataSerialization = true;
result = this.serialize(source, 0, true);
this._disableDataSerialization = false;
this._processSeries(source, result);
// const seriesId = this._processSeries(source, result);
// this._lists.series[seriesId];
}
else {
result = this.serialize(source, 0, true);
}
// Only go deeper if it's a plain Container
if (source.className === "Container") {
result.children = [];
source.children.each((child) => {
// A legend, a breadcrumb bar or anything tagged `serialize` is written
// out by `_processCustomChildren`, which knows how to do it - writing
// it here as well would put it in the config twice
const claimed = this._lists.sprites[child.uid] !== undefined;
if (!this._isLegend(child) && !claimed) {
const serializedChild = this._serializeEntity(child);
if (child.isType("Series")) {
result.children.push("#" + this._getInternalRef(child.uid));
}
else {
result.children.push(serializedChild);
}
}
});
}
// Specialized serializers
if (source.isType("SerialChart")) {
this._processSerialChart(source, result);
}
if (source.isType("SerialChartContainer")) {
this._processSerialChartContainer(source, result);
}
if (source.isType("XYChart")) {
this._processXYChart(source, result);
}
if (source.isType("MapChart")) {
this._processMapChart(source, result);
}
if (source.isType("CurveChart")) {
this._processCurveChart(source, result);
}
if (source.isType("StockChart")) {
this._processStockChart(source, result);
}
if (source.isType("Gantt")) {
this._processGantt(source, result);
}
if (source.isType("Hierarchy")) {
this._processHierarchy(source, result);
}
// Process custom children
if (source.isType("Container")) {
this._processCustomChildren(source, result);
// Check for custom children in property-linked internal containers
$object.each(source, (key, child) => {
if (child instanceof Container && !key.match(/^\_/)) {
this._maybeInit(result, "properties", {});
this._maybeInit(result.properties, key, {});
this._processCustomChildren(child, result.properties[key]);
}
});
}
return result;
}
_examineStructure(source) {
// A plain container's children are serialized as children already
const plain = source.className === "Container";
// Collect serializable custom children
source.children.each((child) => {
this._lists.elements[child.uid] = child;
// A sprite made for a data item is the data showing, not configuration
const dataDriven = child.dataItem !== undefined;
if (child.hasTag("serialize") || this._isLegend(child) || child.isType("BreadcrumbBar")
|| (!plain && !dataDriven && !this._isOwnElement(source, child))) {
this._lists.sprites[child.uid] = child;
}
if (child.isType("Container")) {
this._examineStructure(child);
}
});
}
_processCustomChildren(source, result) {
source.children.each((child) => {
if (this._lists.sprites[child.uid] !== undefined) {
delete this._lists.sprites[child.uid];
this._maybeInit(result, "children", []);
let childItem;
// A series among the children still goes through the series path -
// data, templates and bullets - and is referenced from here
if (child.isType("Series") && !this._isLegend(child)) {
this._serializeEntity(child);
result.children.push("#" + this._getInternalRef(child.uid));
return;
}
// Additional processing for Legend
if (child.isType("Legend")) {
// Do not let serializer serialize legend's data
// It can contain big objects like Series, we'll build the data array
// ourselves
this._disableDataSerialization = true;
childItem = this.serialize(child, 0, false);
this._maybeInit(childItem, "properties", {});
childItem.properties.data = [];
this._disableDataSerialization = false;
// Which series a legend lists is settled at the end: a legend put
// above its chart - in a band of its own, say - is reached before
// the series it refers to, and those have no ids yet
const legendItem = childItem;
const legendChild = child;
this._deferred.push(() => {
legendChild.data.values.forEach((item, index) => {
if (item instanceof DataItem) {
legendItem.properties.data[index] = this._dataItemRef(item.component, item);
}
else if (item instanceof Entity && item.isType("Series")) {
legendItem.properties.data[index] = "#" + this._getInternalRef(item.uid);
}
else {
legendItem.properties.data[index] = item;
}
});
this._collapseLegendData(legendChild, legendItem);
});
this._populateTemplates(child, childItem.properties, [
"labels", "valueLabels", "markers", "markerRectangles", "itemContainers"
]);
}
else if (child.isType("HeatLegend")) {
childItem = this.serialize(child, 0, false);
this._maybeInit(childItem, "properties", {});
this._populateTemplates(child, childItem.properties, ["markers"]);
["startLabel", "endLabel"].forEach((key) => {
const serializedPropValue = this.serialize(child[key], 0, true);
if (serializedPropValue) {
this._stripType(serializedPropValue);
childItem.properties[key] = serializedPropValue;
}
});
}
else if (child.isType("BreadcrumbBar")) {
childItem = this.serialize(child, 0, false);
// `series` is the chart's own series, so it goes out as a reference
const series = child.get("series");
if (series) {
this._maybeInit(childItem, "settings", {});
// Filled in at the end: a bar sitting above its chart is reached
// before the series is, and the series has no id yet
this._pendingSeriesRefs.push({ node: childItem.settings, key: "series", series: series, holder: child });
}
}
else {
// A container someone added to a chart - a title block, say - is
// only itself worth writing out if what is inside it comes too
childItem = this._serializeEntity(child);
}
// Only worth writing out if the child sits somewhere other than the
// end, where parsing puts it anyway
const children = child.parent.children;
const index = children.indexOf(child);
if (index < children.length - 1) {
childItem.index = index;
}
result.children.push(childItem);
}
});
}
/**
* A map's `geoJSON` is normally one of the geodata packs, loaded as a global.
* Written out by the name it is loaded under, so a config stays small and the
* pack is not duplicated into it.
*/
_referenceGeodata(source, result) {
// The live setting, not the serialized copy - the copy has lost the
// identity that says which pack it came from
const geoJSON = source.get ? source.get("geoJSON") : undefined;
if (!geoJSON || !result || !result.settings || !result.settings.geoJSON || typeof window === "undefined") {
return;
}
const global = window;
$array.eachContinue($object.keys(global), (key) => {
if (key.indexOf("am5geodata_") === 0 && global[key] === geoJSON) {
result.settings.geoJSON = key;
return false;
}
return true;
});
}
_processSeries(series, result) {
this._referenceGeodata(series, result);
const datahash = this._hashObject(series.data.values);
let dataId = this._getInternalRef(datahash);
const seriesId = this._getId("series");
this._lists.series[seriesId] = result;
// A word cloud extracts its words from `text`, so its data is its own
// doing and comes back on its own
const generated = series.isType("WordCloud") && series.get("text") != null;
this._saveInternalRef(series.uid, seriesId);
if (dataId === undefined && !generated) {
dataId = this._getId("data");
this._lists.data[dataId] = this.serializeData(series.data.values);
this._saveInternalRef(datahash, dataId);
//_datas[dataId] = this._lists.data[dataId];
this._saveRefId("data", dataId, this._lists.data[dataId]);
}
this._maybeInit(this._lists.series[seriesId], "properties", {});
if (!generated) {
this._lists.series[seriesId].properties.data = "#" + dataId;
}
// Populate templates
this._populateTemplates(series, this._lists.series[seriesId].properties, [
// XY
"strokes", "fills", "columns",
// Percent
"slices", "labels", "ticks", "links",
// Hierarchy
"nodes", "circles", "outerCircles",
// Map
"mapPolygons", "mapLines"
]);
// The nodes of a flow - or of a map sankey - are a series in their own
// right, with their own templates. Serialized without a type, so that they
// configure the nodes the series already made
const nodes = series.nodes;
if (nodes && nodes.isType && nodes.isType("Series")) {
// The nodes are built from the links, so their data is only worth
// writing out when it was set by hand
this._disableDataSerialization = !nodes._userDataSet;
const serializedNodes = this.serialize(nodes, 0, false);
this._disableDataSerialization = false;
this._stripType(serializedNodes);
this._maybeInit(serializedNodes, "properties", {});
this._populateTemplates(nodes, serializedNodes.properties, [
"rectangles", "slices", "circles", "labels", "mapPolygons"
]);
this._processBullets(nodes, serializedNodes.properties);
this._lists.series[seriesId].properties.nodes = serializedNodes;
}
// A word cloud's silhouette is styleable, so it goes out as settings for
// the shape the series already made
if (series.isType("WordCloud")) {
const shape = series.shape;
if (shape) {
const serializedShape = this.serialize(shape, 0, false);
this._stripType(serializedShape);
if (serializedShape.settings) {
this._lists.series[seriesId].properties.shape = serializedShape;
}
}
}
// A setting that holds another series goes out as a reference to it, not as
// a second copy of that series
const serializedSettings = this._lists.series[seriesId].settings;
if (serializedSettings) {
$object.each(serializedSettings, (key) => {
const value = series.get(key);
if (value && value.isType && value.isType("Series")) {
this._pendingSeriesRefs.push({ node: serializedSettings, key: key, series: value, holder: series });
}
});
}
// Handle bullets
this._processBullets(series, this._lists.series[seriesId].properties);
// Handle heat rules
const heatRules = series.get("heatRules");
if (heatRules) {
this._lists.series[seriesId].settings.heatRules = [];
heatRules.forEach((rule) => {
// Figure out target
let targetId = this._findInternalRef(rule.target);
if (targetId) {
targetId = "#" + targetId;
}
else {
// Let's search in series template lists
$object.each(series, (key, value) => {
if (value instanceof ListTemplate && value.template === rule.target) {
const templateId = this._getId("template");
const serializedTemplate = this.serialize(value.template, 0, true);
targetId = "#" + templateId;
result.properties[key].properties.template = "#" + templateId;
this._saveRefId("templates", templateId, serializedTemplate);
this._saveInternalRef(templateId, value.template);
}
});
}
if (targetId) {
const serializedRule = this.serialize(rule, 0, true);
serializedRule.target = targetId;
serializedRule.__parse = true;
this._lists.series[seriesId].settings.heatRules.push(serializedRule);
}
});
}
// Handle Hierarchy
if (series.isType("Hierarchy")) {
this._processHierarchy(series, result);
}
this._saveRefId("series", seriesId, this._lists.series[seriesId]);
this._processCustomChildren(series, this._lists.series[seriesId]);
return seriesId;
}
_referenceSeriesColors(sprite, series, serializedSprite) {
const settings = serializedSprite && serializedSprite.settings;
if (!settings) {
return;
}
$array.each(["fill", "stroke"], (key) => {
// Only touch colors that were actually serialized (own settings).
if (settings[key] === undefined) {
return;
}
// Raw: the comparison is against what was configured on the sprite, and
// a colour adapter reads a data item the sample sprite does not have
const spriteColor = sprite.getRaw(key);
const seriesColor = series.get(key);
if (spriteColor instanceof Color && seriesColor instanceof Color && spriteColor.hex === seriesColor.hex) {
settings[key] = "@series.get('" + key + "')";
}
});
}
_processTemplate(source, result) {
if (source.template) {
const template = source.template;
const templateId = this._getId("template");
const serializedTemplate = this.serialize(template, 0, true);
this._maybeInit(result, "properties", {});
result.properties.template = "#" + templateId;
this._saveRefId("templates", templateId, serializedTemplate);
this._saveInternalRef(templateId, template);
}
if (source.isType("Container")) {
source.children.each((child, index) => {
if (result.children && (result.children.length > index)) {
this._processTemplate(child, result.children[index]);
}
});
}
}
_processSerialChart(source, result) {
// A background behind the series is styling; the container's own geometry is
// the chart's business and is left alone. Shaped like the plot container's:
// the background is a setting, so it rides in `settings` where merging
// reaches it
const seriesBackground = source.seriesContainer.get("background");
if (seriesBackground) {
// The type stays: unlike a plot container, a series container has no
// background of its own, so there is nothing to merge into and one has
// to be built
const serializedBackground = this.serialize(seriesBackground, 0, false);
if (serializedBackground.settings || serializedBackground.states) {
this._maybeInit(result, "properties", {});
result.properties.seriesContainer = { settings: { background: serializedBackground } };
}
}
// Process series
const _series = [];
source.series.each((series, _index) => {
this._disableDataSerialization = true;
const serializedSeries = this.serialize(series, 0, true);
this._disableDataSerialization = false;
const seriesId = this._processSeries(series, serializedSeries);
this._lists.series[seriesId];
_series.push("#" + seriesId);
});
// Add series to chart
if (_series.length) {
this._maybeInit(result, "properties", {});
result.properties.series = _series;
}
// Custom children
this._processCustomChildren(source, result);
}
_processSerialChartContainer(source, result) {
// Init properites
this._maybeInit(result, "properties", {});
// Serialize zoomableContainer
result.properties.zoomableContainer = this.serialize(source.zoomableContainer, 0, false);
this._stripType(result.properties.zoomableContainer);
// Process ZoomTools
if (result.settings && result.settings.zoomTools && result.settings.zoomTools.settings) {
// `target` is re-pointed at the chart's `seriesContainer` on parse.
delete result.settings.zoomTools.settings.target;
// A parsed `ZoomTools` creates its own buttons, so we only export
// their settings (no `type`) to configure them in place. `background`
// and `icon` are theme-created sub-elements and are stripped, same as
// XYChart's `zoomOutButton`.
const zoomTools = source.get("zoomTools");
this._maybeInit(result.settings.zoomTools, "properties", {});
["homeButton", "minusButton", "plusButton"].forEach((button) => {
const serializedButton = this.serialize(zoomTools[button], 0, false);
this._stripType(serializedButton);
if (serializedButton.settings) {
this._stripType(serializedButton.settings.background);
this._stripType(serializedButton.settings.icon);
//delete serializedButton.settings.icon;
}
result.settings.zoomTools.properties[button] = serializedButton;
});
}
}
_processXYChart(source, result) {
this._maybeInit(result, "properties", {});
// Add axes
["x", "y"].forEach((axisType) => {
const ids = [];
source[axisType + "Axes"].each((axis, _index) => {
const axisId = this._processAxis(axis, null);
ids.push("#" + axisId);
});
if (ids.length) {
result.properties[axisType + "Axes"] = ids;
}
});
// Populate series with axe references
source.series.each((series, _index) => {
const id = this._getInternalRef(series.uid);
this._lists.series[id].settings.xAxis = "#" + this._getInternalRef(series.get("xAxis").uid);
this._lists.series[id].settings.yAxis = "#" + this._getInternalRef(series.get("yAxis").uid);
// only when it was asked for - the chart picks one otherwise
if (series.isUserSetting("baseAxis") && !series.isComputedSetting("baseAxis")) {
this._lists.series[id].settings.baseAxis = "#" + this._getInternalRef(series.get("baseAxis").uid);
}
});
// Process series axis ranges
source.series.each((series, _index) => {
const axisRanges = series.axisRanges.values;
if (axisRanges.length) {
for (let i = 0; i < axisRanges.length; i++) {
const rangeId = this._getId("seriesAxisRange");
const range = axisRanges[i];
const axis = range.axisDataItem.component;
const axisId = this._getInternalRef(axis.uid);
const seriesId = this._getInternalRef(series.uid);
const rangeSettings = this.serialize(range.axisDataItem._settings, 0, true);
this._stripRangeInternals(rangeSettings);
const serializedRange = {
axis: "#" + axisId,
series: "#" + seriesId,
settings: rangeSettings,
__parseLogic: "axisRange"
};
["fills", "strokes", "graphics"].forEach((key) => {
const value = range[key];
if (value && value instanceof ListTemplate) {
// Raw settings, so the computed ones have to be left out here
const templateSettings = {};
$object.each(value.template._settings, (settingKey, settingValue) => {
if (!value.template._computedSettings[settingKey]) {
templateSettings[settingKey] = settingValue;
}
});
serializedRange[key] = this.serialize(templateSettings, 0, true);
}
});
this._saveRefId("seriesAxisRanges", rangeId, serializedRange);
}
}
});
// Handle layout for axis containers
["leftAxesContainer", "rightAxesContainer"].forEach((key) => {
const serializedContainer = this.serialize(source[key], 0, true);
if (serializedContainer) {
this._stripType(serializedContainer);
this._maybeInit(result, "properties", {});
result.properties[key] = serializedContainer;
}
});
// Process cursor
const cursor = source.get("cursor");
if (cursor && result.settings && result.settings.cursor) {
["x", "y"].forEach((axisType) => {
const axis = cursor.get(axisType + "Axis");
if (axis) {
result.settings.cursor.settings[axisType + "Axis"] = "#" + this._getInternalRef(axis.uid);
}
this._maybeInit(result.settings.cursor, "properties", {});
});
result.settings.cursor.properties.lineX = this.serialize(cursor.lineX);
result.settings.cursor.properties.lineY = this.serialize(cursor.lineY);
this._stripType(result.settings.cursor.properties.lineX);
this._stripType(result.settings.cursor.properties.lineY);
cursor.get("snapToSeries", []).forEach((series, index) => {
result.settings.cursor.settings.snapToSeries[index] = "#" + this._getInternalRef(series.uid);
});
}
// Process the rest of the XY stuff
// The chart makes this button itself, so it goes out type-less - and so do
// the background and icon it carries, which are styleable and used to be
// dropped outright
result.properties.zoomOutButton = this.serialize(source.zoomOutButton, 0, false);
this._stripType(result.properties.zoomOutButton);
if (result.properties.zoomOutButton.settings) {
this._stripType(result.properties.zoomOutButton.settings.background);
this._stripType(result.properties.zoomOutButton.settings.icon);
}
// The plot container is a readonly property, so it is serialized type-less
// and merged into the existing one on parse. Its background gets the same
// treatment, so it keeps the theme tags the chart gave it.
const serializedPlotContainer = this.serialize(source.plotContainer, 0, false);
this._stripType(serializedPlotContainer);
// Its background gets the same treatment - unless someone chose it, in
// which case the class is part of the choice
if (serializedPlotContainer.settings && serializedPlotContainer.settings.background) {
const plotBackground = source.plotContainer.get("background");
const chosen = source.plotContainer.isUserSetting("background") && !source.plotContainer.isComputedSetting("background");
if (!chosen || !plotBackground) {
this._stripType(serializedPlotContainer.settings.background);
}
}
result.properties.plotContainer = serializedPlotContainer;
// Process the scrollbar
const scrollbarX = source.get("scrollbarX");
if (scrollbarX && scrollbarX.isType("XYChartScrollbar")) {
const serializedScrollbarChart = this._serializeEntity(scrollbarX.chart);
this._stripType(serializedScrollbarChart);
this._maybeInit(result.settings.scrollbarX, "properties", {});
result.settings.scrollbarX.properties.chart = serializedScrollbarChart;
}
}
_processAxis(axis, _result, stripType) {
const axisId = this._getId("axis");
const serializedAxis = this.serialize(axis, 0, true);
this._saveRefId("axes", axisId, serializedAxis);
// Renderer
this._maybeInit(serializedAxis.settings, "renderer", {});
this._maybeInit(serializedAxis.settings.renderer, "properties", {});
this._populateTemplates(axis.get("renderer"), serializedAxis.settings.renderer.properties, [
"labels", "grid", "minorGrid", "ticks"
]);
// Axis syncing
const syncedAxis = axis.get("syncWithAxis");
if (syncedAxis) {
serializedAxis.settings.syncWithAxis = "#" + this._getInternalRef(syncedAxis.uid);
}
// Axis header
if (axis.axisHeader && axis.axisHeader.children.length) {
this._maybeInit(serializedAxis, "properties", {});
serializedAxis.properties.axisHeader = this.serialize(axis.axisHeader, 0, true);
this._processCustomChildren(axis.axisHeader, serializedAxis.properties.axisHeader);
}
// Axis bullet
const bulletFunction = axis.get("bullet");
if (bulletFunction) {
const sampleDataItem = axis.dataItems.length ? axis.dataItems[0] : new DataItem(axis, {}, {});
const bullet = bulletFunction(axis.root, axis, sampleDataItem);
const serializedBullet = this.serialize(bullet, 0, true);
serializedAxis.properties.bullet = serializedBullet;
}
// Axis ranges
const axisRanges = axis.axisRanges.values;
if (axisRanges.length) {
for (let i = 0; i < axisRanges.length; i++) {
const rangeId = this._getId("axisRange");
const range = axisRanges[i];
const rangeSettings = this.serialize(range._settings, 0, true);
this._stripRangeInternals(rangeSettings);
const serializedRange = {
axis: "#" + axisId,
settings: rangeSettings,
__parseLogic: "axisRange"
};
this._saveRefId("axisRanges", rangeId, serializedRange);
}
}
// Process CategoryAxis data
if (axis.isType("CategoryAxis")) {
let dataid = this._getInternalRef(this._hashObject(axis.data.values));
if (dataid === undefined) {
dataid = this._getId("data");
this._lists.data[dataid] = this.serializeData(axis.data.values);
this._saveInternalRef(this._hashObject(axis.data.values), dataid);
this._saveRefId("data", dataid, this._lists.data[dataid]);
}
this._maybeInit(serializedAxis, "properties", {});
serializedAxis.properties.data = "#" + dataid;
}
this._saveInternalRef(axis.uid, axisId);
if (stripType) {
this._stripType(serializedAxis);
this._stripType(serializedAxis.settings.renderer);
}
const axisItem = {};
axisItem[axisId] = serializedAxis;
this._processCustomChildren(axis, axisItem[axisId]);
this._globalRefs.push(axisItem);
return axisId;
}
/**
* Some elements are built out of smaller ones they make for themselves - a
* scrollbar's thumb and grips, a button's background and icon, a clock hand's
* hand and pin. Styling those is styling the chart, so they go out as settings
* for the ones already there, wherever the element was reached from.
*/
_afterEntity(source, res) {
if (!source || !source.isType) {
return;
}
if (source.isType("ClockHand")) {
this._processParts(source, res, ["hand", "pin"]);
}
if (source.isType("Scrollbar")) {
// `overlay` is an XY chart scrollbar's - absent ones are skipped
this._processParts(source, res, ["thumb", "startGrip", "endGrip", "background", "overlay"]);
}
if (source.isType("XYCursor")) {
this._processParts(source, res, ["selection"]);
}
if (source.isType("AxisRenderer")) {
this._processParts(source, res, ["thumb"]);
}
if (source.isType("PictorialStackedSeries")) {
this._processParts(source, res, ["seriesGraphics"]);
}
// A palette usually arrives from a theme and is then edited in place rather
// than replaced, so what was changed on it has to be written out. It keeps
// its type and goes in `settings`, the same shape an assigned palette
// already takes - a colour set is cheap to build, and a config that names
// its colours has no need of the themed one. An assigned palette is
// already there and is left alone.
const colors = source.get ? source.get("colors") : undefined;
if (colors && colors.isType && colors.isType("ColorSet") && !(res.settings && res.settings.colors)) {
// A palette arriving from a theme is not a setting anyone made, so the
// generic pass writes what was edited on it in place. It is written
// whole here instead, and both would be one palette too many.
if (res.properties) {
delete res.properties.colors;
}
const serializedColors = this.serialize(colors, 0, false);
if (serializedColors.settings) {
// It stands in for the themed palette rather than merging into it, so
// it has to carry what the theme built that one with - a hierarchy's
// `step: 2`, the scrollbar preview's `saturation: 0` - or those are
// lost and the colours come back different
$object.each(colors._settings, (key, value) => {
if (serializedColors.settings[key] === undefined) {
serializedColors.settings[key] = this.serialize(value, 0, true);
}
});
this._maybeInit(res, "settings", {});
res.settings.colors = serializedColors;
}
}
if (source.isType("Button")) {
this._processParts(source, res, ["background", "icon"]);
}
}
/**
* Writes out the named parts of an element, type-less so that they configure
* the ones it has already made rather than standing in for them. A part with
* nothing configured on it is left out, and one the element carries as a
* setting is left to that setting.
*/
_processParts(source, res, keys) {
$array.each(keys, (key) => {
if (res.settings && res.settings[key] !== undefined) {
return;
}
const part = source[key] !== undefined ? source[key] : (source.get ? source.get(key) : undefined);
if (!part || !part.isType) {
return;
}
const serializedPart = this.serialize(part, 0, false);
this._stripType(serializedPart);
// States and adapters count as configuration too - a hover on a
// scrollbar's thumb may be the only thing anyone set on it
if (serializedPart.settings || serializedPart.properties || serializedPart.states || serializedPart.adapters) {
this._maybeInit(res, "properties", {});
res.properties[key] = serializedPart;
}
});
}
_processCurveChart(source, _result) {
// Curve Chart needs special treament, as it's X axis references Y axis
// via `yAxis` of its renderer
source.xAxes.each((axis) => {
const xRenderer = axis.get("renderer");
const yRenderer = xRenderer.get("yRenderer");
if (yRenderer) {
// Add reference
const xAxisId = this._getInternalRef(axis.uid);
const yAxisId = this._getInternalRef(yRenderer.axis.uid);
const serializedXAxis = this._getRef("axes", xAxisId);
serializedXAxis.settings.renderer.settings.yRenderer = "#" + yAxisId + ".get('renderer')";
const serializedYAxis = this._getRef("axes", yAxisId);
delete serializedYAxis.settings.renderer.settings.xRenderer;
// Swap positions of the axes
const xIndex = this._getRefIndex("axes", xAxisId);
const yIndex = this._getRefIndex("axes", yAxisId);
if (xIndex < yIndex) {
[this._mainRefs.axes[xIndex], this._mainRefs.axes[yIndex]] = [this._mainRefs.axes[yIndex], this._mainRefs.axes[xIndex]];
}
}
});
}
_processMapChart(source, result) {
// Clean up auto-populated data
source.series.each((series, _index) => {
const seriesId = this._getInternalRef(series.uid);
const serializedSeries = this._lists.series[seriesId];
let serializedData = this._getRef("data", serializedSeries.properties.data.substr(1));
$array.keepIf(serializedData, (item) => !item.madeFromGeoData);
// Collect ids present in the series' `geoJSON`, so we can strip the
// geometry off the user data that duplicates a GeoJSON feature.
const idField = series.get("idField", "id");
const geoJSON = series.get("geoJSON");
const geoJSONIds = {};
if (geoJSON) {
let features = [];
if (geoJSON.type == "FeatureCollection") {
features = geoJSON.features;
}
else if (geoJSON.type == "Feature") {
features = [geoJSON];
}
$array.each(features, (feature) => {
const id = feature[idField];
if (id != null) {
geoJSONIds[id] = feature.properties || {};
}
});
}
$array.each(serializedData, (item) => {
const id = item[idField];
// If the same id exists in `geoJSON`, the geometry will be
// re-populated from there, so we don't need to serialize it.
if (id != null && geoJSONIds[id]) {
delete item.geometry;
delete item.geometryType;
// Feature properties (`name`, ...) are copied onto the row on
// load, so one still equal to the feature's is not the user's
$object.each(geoJSONIds[id], (key, value) => {
if (key !== idField && item[key] === value) {
delete item[key];
}
});
}
});
// what is left of a row that only restated its feature is its id
if (geoJSON) {
$array.keepIf(serializedData, (item) => {
return item[idField] == null || !geoJSONIds[item[idField]] || $object.keys(item).some((key) => key !== idField && key !== "__parse");
});
}
if (serializedData.length === 0) {
delete serializedSeries.properties.data;
}
// Clustered bullet
if (series.get("clusteredBullet")) {
const bulletFunction = series.get("clusteredBullet");
const sampleDataItem = series.clusteredDataItems.length ? series.clusteredDataItems[0] : new DataItem(series, {}, {});
const bullet = bulletFunction(series.root, series, sampleDataItem);
const serializedBullet = this.serialize(bullet, 0, true);
if (bullet.get("sprite")) {
serializedBullet.settings.sprite = this._serializeEntity(bullet.get("sprite"));
this._lists.series[seriesId].settings.clusteredBullet = serializedBullet;
this._processTemplate(bullet.get("sprite"), serializedBullet.settings.sprite);
}
}
});
// Remove target from ZoomControl
if (result.settings && result.settings.zoomControl && result.settings.zoomControl.settings && result.settings.zoomControl.settings.target) {
delete result.settings.zoomControl.settings.target;
this._maybeInit(result.settings.zoomControl, "properties", {});
// Same as `ZoomTools`: the control builds its own buttons, so export
// what was asked of them and nothing else.
const zoomControl = source.get("zoomControl");
["homeButton", "minusButton", "plusButton"].forEach((button) => {
const serializedButton = this.serialize(zoomControl[button], 0, false);
this._stripType(serializedButton);
if (serializedButton.settings) {
this._stripType(serializedButton.settings.background);
this._stripType(serializedButton.settings.icon);
}
if (serializedButton.settings || serializedButton.properties || serializedButton.states) {
result.settings.zoomControl.properties[button] = serializedButton;
}
});
}
// Handle projection setting
const projection = source.get("projection");
// If the projection carries a registered name (e.g. set via
// `am5map.geoMercator()`) and no explicit `projectionName` was set,
// emit `projectionName` so the chart round-trips even when built in code.
if (projection && !source.get("projectionName") && projection.__projectionName) {
this._maybeInit(result, "settings", {});
result.settings.projectionName = projection.__projectionName;
}
}
/**
* A Gantt is its own settings, the rows it lays out and the tasks on them.
* The chart, both date axes, the category axis, the series and the
* scrollbars are the Gantt's own doing, so they are written out only where
* something was configured on them - as settings on the ones it rebuilds,
* never as replacements.
*/
_processGantt(source, result) {
this._maybeInit(result, "properties", {});
const part = (element, data, templates) => {
// No type: the Gantt makes these itself, so this configures them in
// place rather than standing in for them
this._disableDataSerialization = true;
const serialized = this.serialize(element, 0, false);
this._disableDataSerialization = false;
this._stripType(serialized);
if (data && data.length) {
this._maybeInit(serialized, "properties", {});
serialized.properties.data = "#" + this._dataRef(data);
}
if (templates.length) {
this._maybeInit(serialized, "properties", {});
this._populateTemplates(element, serialized.properties, templates);
}
return serialized;
};
const yAxis = part(source.yAxis, source.yAxis.data.values, []);
// Labels, grid and ticks belong to the axis' renderer, and the Gantt makes
// that renderer itself - so it goes out type-less too, to style the one
// that is already there
const renderer = source.yAxis.get("renderer");
if (renderer) {
const serializedRenderer = part(renderer, undefined, ["labels", "grid", "ticks"]);
this._maybeInit(yAxis, "properties", {});
yAxis.properties.renderer = serializedRenderer;
}
result.properties.yAxis = yAxis;
const series = part(source.series, source.series.data.values, [
"links", "columns", "startGrips", "endGrips", "startBullets", "endBullets",
"zeroRectangles", "progressRectangles", "progressGrips"
]);
// The line and the arrow shown while a link is being drawn are sprites
// rather than templates, so they are styled in place
this._maybeInit(series, "properties", {});
["connectorLine", "connectorArrow"].forEach((key) => {
const sprite = source.series[key];
if (sprite) {
const serializedSprite = this.serialize(sprite, 0, false);
this._stripType(serializedSprite);
if (serializedSprite.settings) {
series.properties[key] = serializedSprite;
}
}
});
result.properties.series = series;
}
/**
* The ref a data array is written out as, reusing the one already made if
* the same data is on more than one element.
*/
_dataRef(values) {
const hash = this._hashObject(values);
let dataId = this._getInternalRef(hash);
if (dataId === undefined) {
dataId = this._getId("data");
this._lists.data[dataId] = this.serializeData(values);
this._saveInternalRef(hash, dataId);
this._saveRefId("data", dataId, this._lists.data[dataId]);
}
return dataId;
}
_processStockChart(source, result) {
this._notSupported(source, result);
}
_processHierarchy(source, result) {
const selectedDataItem = source.get("selectedDataItem");
if (selectedDataItem) {
const index = source.dataItems.indexOf(selectedDataItem);
result.settings.selectedDataItem = "@self.dataItems." + index;
}
}
_processBullets(series, properties) {
series.bullets.each((bulletFunction) => {
const sampleDataItem = series.dataItems.length ? series.dataItems[0] : new DataItem(series, {}, {});
const bullet = bulletFunction(series.root, series, sampleDataItem);
// A bullet function may decide there is no bullet for this data item
if (!bullet) {
return;
}
// only now, or a series whose bullets all opt out writes out an empty list
this._maybeInit(properties, "bullets", []);
const serializedBullet = this.serialize(bullet, 0, true);
const bulletSprite = bullet.get("sprite");
if (bulletSprite) {
// Serialize the sprite (including children)
serializedBullet.settings.sprite = this._serializeEntity(bulletSprite);
// If the bullet's `fill`/`stroke` is the exact same color as the
// related series', replace the serialized color with a reference to
// the series, so the two stay in sync when parsed back.
this._referenceSeriesColors(bulletSprite, series, serializedBullet.settings.sprite);
properties.bullets.push(serializedBullet);
// Look for templates
this._processTemplate(bulletSprite, serializedBullet.settings.sprite);
}
// The sample was built only to be read. Left alive it still draws on
// the next frame, and a bullet's own adapter - which expects the data
// item this sample never had - throws there instead.
if (bulletSprite) {
bulletSprite.dispose();
}
bullet.dispose();
});
}
_populateTemplates(source, result, templateKeys) {
templateKeys.forEach((key) => {
if (source[key] && source[key].template) {
this._maybeInit(result, key, {});
result[key] = {
properties: {
template: this.serialize(source[key].template, 0, true)
}
};
this._stripType(result[key].properties.template);
}
});
}
/**
* A reference to a data item, by the path it sits at. Only rows of a series'
* data end up in `dataItems`; anything below that (a hierarchy's second level,
* say) is reached through its parent's `children`.
*/
_dataItemRef(series, item) {
const path = [];
let current = item;
while (current) {
const parent = current.get("parent");
if (parent) {
const children = parent.get("children", []);
const index = $array.indexOf(children, current);
if (index === -1) {
return undefined;
}
path.unshift("get('children')." + index);
current = parent;
}
else {
const index = $array.indexOf(series.dataItems, current);
if (index === -1) {
return undefined;
}
path.unshift("dataItems." + index);
current = undefined;
}
}
return "#" + this._getInternalRef(series.uid) + "." + path.join(".");
}
_stripKeys(source, keys) {
if (source == null) {
return;
}
keys.forEach((key) => {
if (source[key] !== undefined) {
delete source[key];
}
});
}
_stripType(source) {
this._stripKeys(source, ["type"]);
}
_saveInternalRef(ref, value) {
return this._internalRefs[ref] = value;
}
_getInternalRef(ref) {
return this._internalRefs[ref];
}
_findInternalRef(ref) {
let res;
$object.eachContinue(this._internalRefs, (key, value) => {
if (value === ref) {
res = key;
return false;
}
return true;
});
return res;
}
_getRef(key, id) {
let found;
this._mainRefs[key].forEach((value) => {
if (value[id] !== undefined) {
found = value[id];
return;
}
});
return found;
}
_getRefIndex(key, id) {
let found = -1;
this._mainRefs[key].forEach((value, index) => {
if (value[id] !== undefined) {
found = index;
return;
}
});
return found;
}
_saveRef(key, value) {
this._mainRefs[key].push(value);
}
/**
* Puts a series that another series points at before the one pointing at it.
*
* Refs are resolved in the order they are written, so a reference to a series
* further down the list is not there yet when it is read. Series are usually
* added to a chart in drawing order - lines under points, say - which is
* exactly the order that breaks.
*/
_orderSeriesRefs(referenced) {
if ($object.keys(referenced).length === 0) {
return;
}
const entries = this._mainRefs.series;
const byId = {};
$array.each(entries, (entry) => {
byId[$object.keys(entry)[0]] = entry;
});
const ordered = [];
const placed = {};
const visiting = {};
const place = (id) => {
// A cycle cannot be satisfied by ordering; leave those where they were
if (placed[id] || visiting[id] || !byId[id]) {
return;
}
visiting[id] = true;
$array.each(referenced[id] || [], (target) => {
place(target);
});
delete visiting[id];
placed[id] = true;
ordered.push(byId[id]);
};
$array.each(entries, (entry) => {
place($object.keys(entry)[0]);
});
this._mainRefs.series = ordered;
}
_saveRefId(key, id, value) {
const ref = {};
ref[id] = value;
this._saveRef(key, ref);
}
// private _saveFunction(callback: any): string {
// const id = this._getId("function");
// this._saveRefId("functions", id, this.get("functionsAs") == "string" ? callback.toString() : callback);
// return id;
// }
_hashObject(obj) {
const json = JSON.stringify(obj, (_key, value) => {
if ($type.isObject(value) && value instanceof Entity) {
return JSON.stringify(Object.keys(value));
}
return value;
});
let hash = 0;
for (let i = 0; i < json.length; i++) {
const chr = json.charCodeAt(i);
hash = (hash << 5) - hash + chr;
hash |= 0; // convert to 32-bit int
}
return "h" + (hash >>> 0).toString(16); // unsigned hex
}
_maybeInit(source, key, value) {
if (!source[key]) {
source[key] = value;
}
}
_counter(id) {
if (this._counters[id] === undefined) {
this._counters[id] = 0;
}
else {
this._counters[id]++;
}
return this._counters[id];
}
_getId(id) {
return id + "-" + this._counter(id);
}
/**
* Leaves out what a range sets up for itself when parsed back: `isRange`,
* which `__parseLogic` already implies, and any asset that came out carrying
* nothing but a type - saying only that the object exists.
*/
_stripRangeInternals(settings) {
delete settings.isRange;
// A range starts out shown, so only a hidden one is worth writing out
if (settings.visible === true) {
delete settings.visible;
}
$array.each(["grid", "tick", "label", "axisFill", "bullet"], (key) => {
const value = settings[key];
if (value && $type.isObject(value) && value.type !== undefined) {
// empty its settings first - a bag that only held values the axis
// worked out is empty by the time anyone reads it
this._pruneEmptyObjects(value);
if ($object.keys(value).length === 1) {
delete settings[key];
}
}
});
}
_pruneEmptyObjects(value) {
// Non-object primitives → never empty
if (value === null || !$type.isObject(value)) {
return false;
}
// Process arrays
if ($type.isArray(value)) {
for (let i = value.length - 1; i >= 0; i--) {
const el = value[i];
if (el && $type.isObject(el) && !$type.isArray(el)) {
const empty = this._pruneEmptyObjects(el);
if (empty) {
value.splice(i, 1);
}
}
}
return false;
}
// Regular object
for (const key of $object.keys(value)) {
const child = value[key];
if (key === "states" && $type.isArray(value)) {
delete value[key];
}
else if (child && $type.isObject(child)) {
const childEmpty = this._pruneEmptyObjects(child);
if (childEmpty) {
delete value[key];
}
}
}
return Object.keys(value).length === 0;
}
/**
* A legend holding a whole list of data items, in order, is written out as the
* list itself - the way it is usually configured - rather than as one
* reference per item, so it still matches the series after its data changes.
*/
_collapseLegendData(legend, item) {
const values = legend.data.values;
const first = values[0];
if (!values.length || !(first instanceof DataItem)) {
return;
}
const series = first.component;
const parent = first.get("parent");
const siblings = parent ? parent.get("children", []) : series.dataItems;
let whole = siblings.length === values.length;
if (whole) {
$array.eachContinue(values, (value, index) => {
whole = value === siblings[index];
return whole;
});
}
if (!whole) {
return;
}
if (parent) {
const parentRef = this._dataItemRef(series, parent);
if (parentRef) {
item.properties.data = parentRef + ".get('children')";
}
}
else {
item.properties.data = "#" + this._getInternalRef(series.uid) + ".dataItems";
}
}
/**
* Whether the element is one the parent keeps for itself - held in a property
* or in a setting. Anything else among its children was put there by whoever
* built the chart.
*/
_isOwnElement(source, child) {
// The holder is not always the parent: a Gantt keeps its toolbar containers
// and puts them inside the chart it builds, so ask every ancestor too
let holder = source;
while (holder) {
if (this._holds(holder, child)) {
return true;
}
holder = holder.parent;
}
return false;
}
_holds(source, child) {
let own = false;
$object.each(source, (_key, value) => {
if (value === child) {
own = true;
}
});
if (!own) {
$object.each(source._settings, (_key, value) => {
if (value === child) {
own = true;
}
});
}
// A list the parent keeps - a series' axis ranges, say - can hold the
// element itself or a container the library made to put it in. `children`
// is the list being decided about, so it holds everything and answers
// nothing. Data is never one of those, and walking it would mean a pass
// over every row.
if (!own) {
$object.each(source, (key, value) => {
if (own || key === "children" || key === "_data" || key === "_dataItems" || !value || typeof value.each !== "function") {
return;
}
$array.eachContinue(value.values || [], (item) => {
if (item === child || (item && item.container === child)) {
own = true;
}
return !own;
});
});
}
return own;
}
_isLegend(source) {
return source.isType("Legend") || source.isType("HeatLegend");
}
_notSupported(_source, result) {
throw new Error("Unsupported type for serialization: " + result.type);
}
}
ChartSerializer.className = "ChartSerializer";
ChartSerializer.classNames = Serializer.classNames.concat([ChartSerializer.className]);
//# sourceMappingURL=ChartSerializer.js.map