UNPKG

@amcharts/amcharts5

Version:
862 lines 34.1 kB
import { __awaiter } from "tslib"; import { Entity } from "../../core/util/Entity"; import { Sprite } from "../../core/render/Sprite"; import { Container } from "../../core/render/Container"; import { Color } from "../../core/util/Color"; 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 { registry } from "../../core/Registry"; import * as $object from "../../core/util/Object"; import classes from "./Classes"; function isObject(value) { return $type.isObject(value); } function isObjectParsed(value) { return value instanceof Entity || value instanceof Color || value instanceof Percent || value instanceof Template; } // Detects a type-less, entity-shaped config object, e.g. a serialized `background` // whose `type` was stripped so that it configures the existing sub-object in place // instead of constructing a new one. function isMergeDescriptor(value) { return isObject(value) && value.constructor === Object && value.type === undefined && (value.settings !== undefined || value.states !== undefined || value.properties !== undefined || value.adapters !== undefined); } // Detects whether a raw setting value references `@self` (the entity itself), // which means it must be applied after the entity is constructed. // // Only recurses into plain JSON objects/arrays (the shapes a parsed config is made of) so that // live instances passed programmatically (Entity, DataItem, Sprite, ...) are never walked - those // can hold circular references and are never `@self` strings anyway. function usesSelfRef(value) { if ($type.isString(value)) { return value === "@self" || value.indexOf("@self.") === 0; } else if ($type.isArray(value)) { return $array.any(value, usesSelfRef); } else if (isObject(value) && value.constructor === Object) { return $array.any($object.keys(value), (key) => usesSelfRef(value[key])); } else { return false; } } function lookupRef(refs, name) { let i = refs.length; while (i--) { const sub = refs[i]; if (name in sub) { return sub[name]; } } throw new Error("Could not find ref " + name); } const GEODATA_PREFIX = "am5geodata_"; const LOCALE_PREFIX = "am5locales_"; function parseRef(value, refs) { if (value[0] === "#" || value[0] === "@") { if (value[1] === value[0]) { // "##foo" or "@@foo" gets escaped into the literal strings "#foo" or "@foo" return { isValue: true, value: value.slice(1), }; } else { const path = value.split(/\./g); let object = lookupRef(refs, path[0]); for (let i = 1; i < path.length; ++i) { const subpath = path[i]; // Supports `#foo.get("bar")` and `#foo.get('bar')` syntax const parsed = /get\((["'])([^\1]*)\1\)/.exec(subpath); if (parsed) { object = object.get(parsed[2]); } else { object = object[subpath]; } } return { isValue: true, value: object, }; } } else { return { isValue: true, value, }; } } function mergeObject(entity, parsed) { if (parsed.properties) { $array.each(parsed.properties, (fn) => { fn(entity); }); } } function mergeEntity(entity, parsed) { mergeObject(entity, parsed); if (parsed.adapters) { $array.each(parsed.adapters, (adapter) => { // A callback cannot survive a trip through JSON, so a config may carry // an adapter with nothing to call. Skip it rather than break the chart if (typeof adapter.callback === "function") { entity.adapters.add(adapter.key, adapter.callback); } }); } if (parsed.states) { $array.each(parsed.states, (state) => { entity.states.create(state.key, state.settings); }); } if (entity instanceof Container) { if (parsed.children) { parsed.children.forEach((child) => { if (child.index == null || child.index >= entity.children.length) { entity.children.push(child.value); } else { entity.children.insertIndex(child.index, child.value); } }); } } } // Adds parsed tags to the ones an existing element already has, instead of replacing them function mergeThemeTags(entity, settings) { if (settings && $type.isArray(settings.themeTags)) { entity._addUserThemeTags(settings.themeTags); const tags = entity.get("themeTags", []).slice(); $array.each(settings.themeTags, (tag) => { if (tags.indexOf(tag) === -1) { tags.push(tag); } }); settings.themeTags = tags; } } function mergeExisting(entity, parsed) { if (parsed.settings) { mergeThemeTags(entity, parsed.settings); entity.setAll(parsed.settings); } mergeEntity(entity, parsed); } // TODO replace this with `__parseLogic: "merge"` function mergeExistingAxisRange(entity, parsed) { if (parsed.settings) { mergeThemeTags(entity, parsed.settings); $object.each(parsed.settings, (key, value) => { if (value instanceof Entity) { const old = entity.get(key); if (old instanceof Entity) { old.setAll(value._settings); } else { entity.set(key, value); } } else { entity.set(key, value); } }); } mergeEntity(entity, parsed); } function constructEntity(root, parsed) { if (!parsed.construct) { return parsed.value; } const entity = parsed.construct.new(root, parsed.settings || {}); mergeEntity(entity, parsed); return entity; } class ParserState { constructor(updateTargets = "strict") { this._caching = {}; this._cache = {}; this._delayed = []; this._updateTargets = updateTargets; } // When `updateTargets` is "soft" and there is an existing entity target whose // type matches the incoming config's `type`, the config is merged into that // target in place (as if `type` had not been specified) instead of replacing // it with a freshly constructed object. _softMerges(old, type) { return this._updateTargets === "soft" && type !== undefined && old instanceof Entity && old.className === type; } afterParse() { this._delayed.forEach((f) => { f(); }); } getClass(name) { return this._cache[name]; } storeClass(name) { return __awaiter(this, void 0, void 0, function* () { if (name === "Template") { // Do nothing return; } const promise = classes[name]; if (promise == null) { throw new Error("Unknown object type `" + name + "`"); } this._cache[name] = (yield promise()); }); } cacheClass(name) { let promise = this._caching[name]; if (promise == null) { promise = this._caching[name] = this.storeClass(name); } return promise; } parseAsyncArray(array) { return __awaiter(this, void 0, void 0, function* () { yield Promise.all($array.map(array, (x) => this.parseAsync(x))); }); } parseAsyncObjectKey(key, value) { return __awaiter(this, void 0, void 0, function* () { if (key === "type") { if ($type.isString(value)) { yield this.cacheClass(value); } else { yield this.parseAsync(value); } } else if (key === "geoJSON" || key === "geometry") { // Do nothing } else { yield this.parseAsync(value); } }); } parseAsyncObject(object) { return __awaiter(this, void 0, void 0, function* () { switch (object.type) { case "Color": case "Percent": // Do nothing break; default: // `__parse` says the object is data, so a `type` in it is a column // name and not a class to go and load const isData = object.__parse === true; yield Promise.all($array.map($object.keys(object), (key) => { return isData && key === "type" ? Promise.resolve() : this.parseAsyncObjectKey(key, object[key]); })); } }); } parseAsync(value) { return __awaiter(this, void 0, void 0, function* () { if ($type.isArray(value)) { yield this.parseAsyncArray(value); } else if (isObject(value)) { if (!isObjectParsed(value)) { if (value.__parse === true || value.__parse !== false) { yield this.parseAsyncObject(value); } } } }); } parseObject(root, object, refs) { const output = {}; $array.each($object.keys(object), (key) => { // Our own marker, not part of the object if (key === "__parse") { return; } output[key] = this.parse(root, object[key], refs); }); return output; } parseArray(root, value, refs) { return $array.map(value, (value) => this.parse(root, value, refs)); } parseChildren(root, value, refs) { return $array.map(value, (value) => this.parseChild(root, value, refs)); } parseStops(root, value, refs) { return $array.map(value, (value) => this.parseObject(root, value, refs)); } parseSetting(root, key, value, refs) { if (key === "layout") { switch (value) { case "horizontal": return root.horizontalLayout; case "vertical": return root.verticalLayout; case "grid": return root.gridLayout; } } if (key === "clusteredBullet") { return (_, series) => { const newRefs = refs.concat([{ "@series": series }]); return this.parse(root, value, newRefs); }; } if (key === "projection") { // @todo: implement return value; } if (key === "geoJSON") { // A geodata pack is a global (`am5geodata_worldLow`), so a config // refers to it by name rather than carrying a megabyte of coordinates if ($type.isString(value) && value.indexOf(GEODATA_PREFIX) === 0 && typeof window !== "undefined") { const geodata = window[value]; if (geodata) { return geodata; } } return value; } if (key === "stops") { return this.parseStops(root, value, refs); } return this.parse(root, value, refs); } parseSettings(root, object, refs) { const settings = {}; $array.each($object.keys(object), (key) => { settings[key] = this.parseSetting(root, key, object[key], refs); }); return settings; } parseState(root, object, refs) { return { key: object.key, settings: object.settings ? this.parseSettings(root, object.settings, refs) : {}, }; } parseAdapter(root, object, refs) { return { key: object.key, callback: this.parse(root, object.callback, refs), }; } parseProperties(root, object, refs) { // A flow's `nodes` have to exist before the `data` whose links point at // them, so they go first no matter where the config lists them const keys = $object.keys(object).slice(); const nodesAt = keys.indexOf("nodes"); if (nodesAt > 0) { const nodesKey = keys.splice(nodesAt, 1)[0]; keys.unshift(nodesKey); } return $array.map(keys, (key) => { const rawValue = object[key]; return (entity) => { const run = () => { const parsed = this.parseValue(root, rawValue, refs); // Usually a property, but a block may also name a setting that // already holds an entity - an axis' `renderer`, say - and then it // configures the one that is there rather than replacing it const old = (entity[key] === undefined && typeof entity.get === "function" ? entity.get(key) : entity[key]); if (old) { if (parsed.isValue) { // Merge Array into List if ($type.isArray(parsed.value)) { if (old instanceof ListData) { old.setAll(parsed.value); } else { $array.each(parsed.value, (value) => { old.push(value); }); } } else { entity[key] = parsed.value; } // A typed config normally replaces the existing object, but in // "soft" mode a same-type target is merged into instead (falls // through to the merge branch below). } else if (parsed.construct && !this._softMerges(old, parsed.type)) { entity[key] = constructEntity(root, parsed); // Merge into existing Entity } else if (old instanceof Entity) { this.mergeExistingEntity(root, old, parsed, refs); // Merge into existing Template } else if (old instanceof Template) { mergeExisting(old, parsed); // Merge into existing object } else { mergeObject(old, parsed); } } else { if (parsed.isValue) { entity[key] = parsed.value; } else { entity[key] = constructEntity(root, parsed); } } }; if (key === "data") { this._delayed.push(run); } else if (key === "bullets") { const old = entity[key]; $type.assert(old != null); $type.assert($type.isArray(rawValue)); $array.each(rawValue, (value) => { old.push((_, series) => { const newRefs = refs.concat([{ "@series": series }]); return this.parse(root, value, newRefs); }); }); } else if (key === "bullet") { entity.set("bullet", (_, axis) => { const newRefs = refs.concat([{ "@axis": axis }]); return this.parse(root, rawValue, newRefs); }); } else { run(); } }; }); } // Applies a parsed config to an already-existing entity. Unlike `setAll`, a // setting whose existing value is an [[Entity]] and whose incoming value is a // type-less, entity-shaped object (see `isMergeDescriptor`) is merged into that // existing sub-entity instead of replacing it with the raw config object. mergeExistingEntity(root, entity, parsed, refs) { if (parsed.settings) { const rawSettings = parsed.value.settings; mergeThemeTags(entity, parsed.settings); $object.each(parsed.settings, (key, value) => { const existing = entity.get(key); const raw = rawSettings ? rawSettings[key] : undefined; // Merge into the existing sub-entity when the incoming setting is a // type-less merge descriptor, or - in "soft" mode - a same-type typed // object. Otherwise replace it with the parsed value. if (existing instanceof Entity && (isMergeDescriptor(raw) || this._softMerges(existing, isObject(raw) ? raw.type : undefined))) { this.mergeExistingEntity(root, existing, this.parseEntity(root, raw, refs), refs); } else { entity.set(key, value); } }); } mergeEntity(entity, parsed); } // Applies a `root` config section to the [[Root]] object. `properties` are set // directly on the `Root`; [[Entity]]-based properties (e.g. `numberFormatter`, // `dateFormatter`) are parsed and merged into the existing instance the same // way entities are parsed elsewhere (i.e. their settings live in a `settings` // block). `settings` are applied to the root's settings, then re-applied // non-destructively so they take effect. applyRoot(root, config) { const rootAny = root; const properties = config.properties; if (isObject(properties)) { $object.each(properties, (key, value) => { // A locale is named rather than copied out, so the pack it names is // looked up the same way a geodata pack is if (key === "locale" && $type.isString(value) && value.indexOf(LOCALE_PREFIX) === 0 && typeof window !== "undefined") { const pack = window[value]; if (pack) { rootAny.locale = pack; } return; } const existing = rootAny[key]; if (existing instanceof Entity && isObject(value)) { this.mergeExistingEntity(root, existing, this.parseEntity(root, value, []), []); } else { rootAny[key] = this.parse(root, value, []); } }); } const settings = config.settings; if (isObject(settings) && $object.keys(settings).length > 0) { $object.each(settings, (key, value) => { rootAny._settings[key] = this.parse(root, value, []); // Asked for now, whatever the Root filled in for itself earlier delete rootAny._computedSettings[key]; }); // Non-destructive re-apply (does NOT re-run `_init()`, which would // duplicate containers, DOM nodes and event listeners). rootAny._reapplySettings(); } } parseAxisRange(root, value, refs) { const settings = this.parseSettings(root, value.settings, refs); return { isValue: false, type: undefined, construct: undefined, settings, adapters: undefined, states: undefined, children: undefined, properties: undefined, index: undefined, value, }; } parseRefsObject(root, object, refs) { const newRefs = {}; $object.each(object, (key, value) => { if (value.__parseLogic == "axisRange" || value.__parseLogic == "seriesAxisRange") { // Parse axis range const parsed = this.parseAxisRange(root, value, refs); const axis = lookupRef(refs, value.axis); const axisDataItem = axis.makeDataItem({}); let seriesRange; const series = value.series ? lookupRef(refs, value.series) : undefined; if (series) { seriesRange = series.createAxisRange(axisDataItem); } else { axis.createAxisRange(axisDataItem); } // Apply settings to data item mergeExistingAxisRange(axisDataItem, parsed); // Set series range settings if (seriesRange) { ["fills", "strokes", "graphics"].forEach((targetKey) => { if (seriesRange[targetKey] !== undefined && value[targetKey] !== undefined) { value[targetKey].__parse = true; const parsedSetting = this.parse(root, value[targetKey], refs); // The flag is ours, not a setting delete parsedSetting.__parse; seriesRange[targetKey].template.setAll(parsedSetting); } }); } } else { newRefs["#" + key] = this.parse(root, value, refs); } }); return newRefs; } parseRefs(root, object, refs) { if ($type.isArray(object)) { const length = object.length; for (let i = 0; i < length; ++i) { refs = refs.concat([this.parseRefsObject(root, object[i], refs)]); } } else { refs = refs.concat([this.parseRefsObject(root, object, refs)]); } return refs; } parseChild(root, value, refs) { if ($type.isString(value)) { return { index: undefined, value: parseRef(value, refs).value, }; } else if (value.ref != null) { const index = (value.index == null ? undefined : value.index); return { index, value: parseRef(value.ref, refs).value, }; } else { const parsed = this.parseEntity(root, value, refs); return { index: parsed.index, value: constructEntity(root, parsed), }; } } parseEntity(root, value, refs) { if (value.refs) { refs = this.parseRefs(root, value.refs, refs); } const construct = (value.type ? this.getClass(value.type) : undefined); // Settings that reference `@self` (the entity itself) can't be resolved at construction time, // so they're split off and applied after the entity is built and its deferred data is set. let settingsRaw = value.settings; const selfSettings = {}; let hasSelfSettings = false; if (settingsRaw) { $array.each($object.keys(settingsRaw), (key) => { // `geoJSON`/`geometry` are large opaque settings (same ones `parseAsync` skips) - never // scan into them. if (key !== "geoJSON" && key !== "geometry" && usesSelfRef(settingsRaw[key])) { selfSettings[key] = settingsRaw[key]; hasSelfSettings = true; } }); if (hasSelfSettings) { settingsRaw = Object.assign({}, settingsRaw); $array.each($object.keys(selfSettings), (key) => { delete settingsRaw[key]; }); } } const settings = (settingsRaw ? this.parseSettings(root, settingsRaw, refs) : undefined); const properties = (value.properties ? this.parseProperties(root, value.properties, refs) : undefined); const children = (value.children ? this.parseChildren(root, value.children, refs) : undefined); const states = (value.states ? $array.map(value.states, (value) => this.parseState(root, value, refs)) : undefined); const adapters = (value.adapters ? $array.map(value.adapters, (value) => this.parseAdapter(root, value, refs)) : undefined); const index = (value.index == null ? undefined : value.index); let allProperties = properties; if (hasSelfSettings) { const deferredProp = (entity) => { this._delayed.push(() => { const newRefs = refs.concat([{ "@self": entity }]); $array.each($object.keys(selfSettings), (key) => { const parsed = this.parseValue(root, selfSettings[key], newRefs); entity.set(key, parsed.isValue ? parsed.value : constructEntity(root, parsed)); }); }); }; allProperties = allProperties ? allProperties.concat([deferredProp]) : [deferredProp]; } return { isValue: false, type: value.type, construct, settings, adapters, states, children, properties: allProperties, index, value, }; } parseTemplate(root, value, refs) { if (value.refs) { refs = this.parseRefs(root, value.refs, refs); } const settings = (value.settings ? this.parseSettings(root, value.settings, refs) : {}); const template = Template.new(settings); const states = (value.states ? $array.map(value.states, (value) => this.parseState(root, value, refs)) : undefined); const adapters = (value.adapters ? $array.map(value.adapters, (value) => this.parseAdapter(root, value, refs)) : undefined); if (adapters) { $array.each(adapters, (adapter) => { if (typeof adapter.callback === "function") { template.adapters.add(adapter.key, adapter.callback); } }); } if (states) { $array.each(states, (state) => { template.states.create(state.key, state.settings); }); } return { isValue: true, value: template, }; } parseValue(root, value, refs) { if (value instanceof Entity) { return { isValue: true, value: value }; } else if ($type.isArray(value)) { return { isValue: true, value: this.parseArray(root, value, refs), }; } else if (isObject(value)) { if (isObjectParsed(value)) { return { isValue: true, value, }; // Read before any `type`: the marker is what says whether this is an // object to build or data to leave alone } else if (value.__parse === true) { return { isValue: true, value: this.parseObject(root, value, refs), }; } else if (value.__parse === false) { // The marker is ours, not part of the data const copy = {}; $object.each(value, (key, item) => { if (key !== "__parse") { copy[key] = item; } }); return { isValue: true, value: copy, }; } else if (value.type === "Color") { return { isValue: true, value: Color.fromAny(value.value), }; } else if (registry.geometryFunctions[value.type]) { // A shape stored as the call that made it (GeoRectangle, GeoCircle) const args = {}; $object.each(value, (key, arg) => { if (key !== "type") { args[key] = arg; } }); return { isValue: true, value: registry.geometryFunctions[value.type](args), }; } else if (value.type === "Percent") { return { isValue: true, value: new Percent(value.value), }; } else if (value.type === "Template") { return this.parseTemplate(root, value, refs); } else { return this.parseEntity(root, value, refs); } } else if ($type.isString(value)) { return parseRef(value, refs); } else { return { isValue: true, value, }; } } parse(root, value, refs) { const parsed = this.parseValue(root, value, refs); if (parsed.isValue) { return parsed.value; } else { return constructEntity(root, parsed); } } } /** * A parser for JSON based chart configs. * * @see {@link https://www.amcharts.com/docs/v5/concepts/serializing/} for more info * @since 5.3.0 */ export class JsonParser { /** * IMPORTANT! Do not instantiate this class via `new Class()` syntax. * * Use static method `Class.new()` instead. * * @see {@link https://www.amcharts.com/docs/v5/getting-started/#New_element_syntax} for more info * @ignore */ constructor(root, isReal) { if (!isReal) { throw new Error("You cannot use `new Class()`, instead use `Class.new()`"); } this._root = root; } /** * Use this method to create an instance of this class. * * @see {@link https://www.amcharts.com/docs/v5/getting-started/#New_element_syntax} for more info * @param root Root element * @return Instantiated object */ static new(root) { return (new this(root, true)); } /** * Parses and creates chart objects from simple objects. * * @param object Serialized data * @return A promise of a target object */ parse(object, settings = {}) { return __awaiter(this, void 0, void 0, function* () { const state = new ParserState(settings.updateTargets); yield state.parseAsync(object); // Apply the top-level `root` config section to the `Root` object before // the chart is parsed. Only the top-level entry is treated this way // (nested parsing never inspects `root`), and the key is ignored by the // rest of the parser, so it does not affect chart/entity parsing. if (isObject(object) && isObject(object.root)) { state.applyRoot(this._root, object.root); } const output = state.parse(this._root, object, []); if (settings.parent) { if (output instanceof Sprite) { settings.parent.children.push(output); } else { throw new Error("When using the parent setting, the entity must be a Sprite"); } } else if (output instanceof Entity) { output._applyThemes(true); } state.afterParse(); return output; }); } /** * Parses and creates chart objects from JSON string. * * @param string JSON string * @return A promise of a target object */ parseString(string, settings = {}) { return __awaiter(this, void 0, void 0, function* () { return yield this.parse(JSON.parse(string), settings); }); } } /** * Registers a class so that it can be parsed and instantiated bey JSON parser. * * @param name Class name * @param func Class reference * @ignore */ export function registerCustomClass(name, ref) { if (!classes[name]) { classes[name] = () => new Promise((resolve) => { resolve(ref); }); } } //# sourceMappingURL=Json.js.map