@amcharts/amcharts5
Version:
amCharts 5
377 lines • 16.1 kB
JavaScript
import { Entity } from "../../core/util/Entity";
import { Component } from "../../core/render/Component";
import { Color } from "../../core/util/Color";
import { Gradient } from "../../core/render/gradients/Gradient";
import { Percent } from "../../core/util/Percent";
import { Template } from "../../core/util/Template";
import { ListData } from "../../core/util/Data";
import * as $type from "../../core/util/Type";
import * as $array from "../../core/util/Array";
import * as $object from "../../core/util/Object";
/**
* Provides functionality to serialize charts or individual elements into simple
* objects or JSON.
*
* @see {@link https://www.amcharts.com/docs/v5/concepts/serializing/} for more info
* @since 5.3.0
*/
export class Serializer extends Entity {
constructor() {
super(...arguments);
this._refs = {};
/**
* Set while serializing data. A `#` or `@` at the start of a string means a
* reference in a config, so strings are escaped - but most of a data row is
* handed back untouched when parsed, and escaping it there would corrupt it
* a little more on every round trip. `_escapeParsed` puts the escaping back
* in the few places the parser does read values.
*
* @ignore
*/
this._serializingData = false;
}
/**
* Serializes data, which is values rather than configuration.
*
* @ignore
*/
serializeData(values) {
const previous = this._serializingData;
this._serializingData = true;
const result = this.serialize(values, 0, true, true);
this._serializingData = previous;
this._escapeParsed(result);
return result;
}
/**
* Escapes the strings the parser is going to read as references, and only
* those. It walks data the way the parser does: through every array, and
* into an object only when that object is marked `__parse`. Anywhere else a
* string comes back as it was written, so escaping it would leave a stray
* `##` in the data.
*/
_escapeParsed(value) {
if ($type.isArray(value)) {
$array.each(value, (item, index) => {
if ($type.isString(item)) {
value[index] = this._escapeText(item);
}
else {
this._escapeParsed(item);
}
});
}
else if ($type.isObject(value) && value.__parse === true) {
$object.each(value, (key, item) => {
if ($type.isString(item)) {
value[key] = this._escapeText(item);
}
else {
this._escapeParsed(item);
}
});
}
}
/**
* Serializes target object into a simple object or JSON string.
*
* @param source Target object
* @param depth Current depth
* @param full Serialize object in full (ignoring maxDepth)
* @param forceParse If true, will add __parse to serialized objects
* @return Serialized data
*/
serialize(source, depth = 0, full = false, forceParse = false) {
if (depth > this.get("maxDepth", 2)) {
return undefined;
}
if (source === false || source === true || source === null) {
return source;
}
// A geometry built by one of the map's shape functions goes out as that
// call - a handful of numbers rather than every coordinate it expands to
const described = source && source.__geometry;
if (described) {
const call = { type: described.name };
$object.each(described.args, (key, value) => {
call[key] = value;
});
return call;
}
if ($type.isArray(source)) {
const res = [];
$array.each(source, (arrval) => {
res.push(this.serialize(arrval, depth, full, forceParse));
});
return res;
}
else if (source instanceof ListData) {
const res = [];
$array.each(source.values, (arrval) => {
res.push(this.serialize(arrval, depth, full, forceParse));
});
return res;
}
const res = {};
const am5object = source instanceof Entity || source instanceof Template || source instanceof Color || source instanceof Percent ? true : false;
// Process settings
const fullSettings = this.get("fullSettings", []);
if (source instanceof Entity) {
res.type = source.className;
let settings = this._filterSettings($object.keys(source._settings));
// Include only what was asked for - not what the chart worked out for
// itself
settings = settings.filter((value) => {
return source.isUserSetting(value) && !source.isComputedSetting(value);
});
if (settings.length) {
res.settings = {};
$array.each(settings, (setting) => {
// Raw, not `get()`: an adapter's job is to work out a value at
// draw time from a data item, and the sample sprite serialized
// here has none - running one throws. What was configured is
// the raw value anyway; the adapted one is not configuration.
let settingValue = source.getRaw(setting);
// Only the tags that were given, not the ones the library added
if (setting === "themeTags") {
settingValue = source._getUserThemeTags();
if (!settingValue.length) {
return;
}
}
if (settingValue !== undefined) {
res.settings[setting] = this.serialize(settingValue, depth + 1, full || fullSettings.indexOf(setting) !== -1);
if (source instanceof Gradient && setting === "stops") {
res.settings[setting] = this.serialize(settingValue, 0, true);
}
}
});
}
this._processComputedEntities(source, res, settings, depth);
this._processAdapters(source, res);
this._processStates(source, res);
}
else if (source instanceof Template) {
res.type = "Template";
let settings = this._filterSettings($object.keys(source._settings));
settings = settings.filter((value) => {
return value !== "themeTags" && !source._computedSettings[value];
});
if (settings.length) {
res.settings = {};
$array.each(settings, (setting) => {
if (source._settings[setting] !== undefined) {
res.settings[setting] = this.serialize(source.get(setting), depth + 1, fullSettings.indexOf(setting) !== -1);
}
});
}
this._processAdapters(source, res);
this._processStates(source, res);
this._afterEntity(source, res);
return res;
}
// Data
if (source instanceof Component) {
if (source.data.length && (this.get("excludeProperties") || []).indexOf("data") === -1) {
res.properties = {
data: this.serializeData(source.data.values)
};
}
}
// Process the rest
if (source instanceof Color) {
return {
type: "Color",
value: source.toCSSHex()
};
}
else if (source instanceof Percent) {
return {
type: "Percent",
value: source.percent
};
}
else if ($type.isString(source)) {
return this._escapeRefs(source);
}
else if ($type.isNumber(source)) {
return source;
}
else if ($type.isObject(source)) {
// TODO
if (full && !am5object) {
const excludeProperties = this.get("excludeProperties", []);
$object.each(source, (key, value) => {
if (excludeProperties.indexOf(key) === -1 && value !== undefined) {
res[key] = this.serialize(value, depth + 1, full, forceParse);
}
});
if (forceParse) {
// Add only if there are objects
$object.each(res, (_key, value) => {
if ($type.isObject(value)) {
res.__parse = true;
}
});
// A data row with its own `type` field reads as an entity to the
// parser, which then goes looking for a class by that name. Saying
// so outright keeps a column called `type` a column.
if (res.type !== undefined && res.__parse !== true) {
res.__parse = false;
}
}
}
else if (am5object) {
const includeProperties = this.get("includeProperties", []);
$array.each(includeProperties, (key) => {
const value = source[key];
if (value !== undefined) {
if (res.properties === undefined) {
res.properties = {};
}
res.properties[key] = this.serialize(value, depth + 1, full, forceParse);
}
});
}
}
if (depth == 0 && $object.keys(this._refs).length) {
res.refs = this._refs;
}
this._afterEntity(source, res);
return res;
}
/**
* A place for a subclass to add to what an entity serialized to, wherever it
* was reached from.
*
* @ignore
*/
_afterEntity(_source, _res) { }
/**
* Writes out what was styled on an object the library built for itself.
*
* A background an element seeds, a part it configures - the object itself is
* not configuration, so it is not written out on its own account. What
* someone then styled on it is, and would otherwise be lost. It goes out
* without a `type`, so parsing configures the object already there instead of
* replacing it with a new one.
*
* @param source Source object
* @param res Result
* @param written Setting keys already written out in full
* @param depth Current depth
*/
_processComputedEntities(source, res, written, depth) {
$array.each(this._filterSettings($object.keys(source._settings)), (key) => {
if (written.indexOf(key) !== -1) {
return;
}
const value = source._settings[key];
if (!(value instanceof Entity)) {
return;
}
// An element the chart holds in its own right - an axis, a series,
// another chart - is written out where it belongs and referenced from
// here. Only the parts an element owns are configured in place.
const part = value;
if (part.isType && (part.isType("Axis") || part.isType("Series") || part.isType("Chart"))) {
return;
}
// past `maxDepth` there is nothing to write
const serialized = this.serialize(part, depth + 1, false);
if (!serialized || typeof serialized !== "object") {
return;
}
delete serialized.type;
// The part already has its tags, and writing them would replace them
if (serialized.settings) {
delete serialized.settings.themeTags;
if (!Object.keys(serialized.settings).length) {
delete serialized.settings;
}
}
if (serialized.settings || serialized.properties || serialized.states || serialized.adapters) {
if (!res.properties) {
res.properties = {};
}
if (res.properties[key] === undefined) {
res.properties[key] = serialized;
}
}
});
}
_processAdapters(source, res) {
if (this.get("includeAdapters", false)) {
const asString = this.get("functionsAs", "string") == "string";
const callbacks = source.adapters._callbacks;
if (Object.keys(callbacks).length > 0) {
res.adapters = [];
$object.each(callbacks, (key, callbackList) => {
callbackList.forEach((callback) => {
res.adapters.push({
key: key,
callback: asString ? callback.toString() : callback
});
});
});
}
}
}
_processStates(source, res) {
if (this.get("includeStates", false)) {
const states = source.states._states;
if (Object.keys(states).length > 0) {
const serialized = [];
$object.each(states, (key, state) => {
// Same rule as for settings - leave out what the chart worked out
// for itself. A template's states have no computed settings of
// their own: a template is configuration, so its states are too.
const settings = {};
const computed = state._computedSettings || {};
$object.each(state._settings, (settingKey, value) => {
if (!computed[settingKey]) {
settings[settingKey] = value;
}
});
if (Object.keys(settings).length > 0) {
const serializedSettings = this.serialize(settings, 0, true);
if (serializedSettings && Object.keys(serializedSettings).length > 0) {
serialized.push({
key: key,
settings: serializedSettings
});
}
}
});
if (serialized.length > 0) {
res.states = serialized;
}
}
}
}
_filterSettings(settings) {
const includeSettings = this.get("includeSettings", []);
const excludeSettings = this.get("excludeSettings", []);
if (includeSettings.length) {
settings = includeSettings;
}
else if (excludeSettings.length) {
settings = settings.filter((value) => {
return excludeSettings.indexOf(value) === -1;
});
}
return settings;
}
_escapeRefs(text) {
if (this._serializingData) {
return text;
}
return this._escapeText(text);
}
_escapeText(text) {
return text.replace(/^#/, '##').replace(/^@/, '@@');
}
}
Serializer.className = "Serializer";
Serializer.classNames = Entity.classNames.concat([Serializer.className]);
//# sourceMappingURL=Serializer.js.map