@amcharts/amcharts5
Version:
amCharts 5
862 lines • 34.1 kB
JavaScript
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