UNPKG

@amcharts/amcharts5

Version:
377 lines 16.1 kB
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