di-echarts
Version:
Apache ECharts is a powerful, interactive charting and data visualization library for browser
1,479 lines (1,262 loc) • 110 kB
text/typescript
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import * as zrender from 'zrender/src/zrender';
import {
assert,
each,
isFunction,
isObject,
indexOf,
bind,
clone,
setAsPrimitive,
extend,
HashMap,
createHashMap,
map,
defaults,
isDom,
isArray,
noop,
isString,
retrieve2
} from 'zrender/src/core/util';
import env from 'zrender/src/core/env';
import timsort from 'zrender/src/core/timsort';
import Eventful, { EventCallbackSingleParam } from 'zrender/src/core/Eventful';
import Element, { ElementEvent } from 'zrender/src/Element';
import GlobalModel, {QueryConditionKindA, GlobalModelSetOptionOpts} from '../model/Global';
import ExtensionAPI from './ExtensionAPI';
import CoordinateSystemManager from './CoordinateSystem';
import OptionManager from '../model/OptionManager';
import backwardCompat from '../preprocessor/backwardCompat';
import dataStack from '../processor/dataStack';
import ComponentModel from '../model/Component';
import SeriesModel from '../model/Series';
import ComponentView, {ComponentViewConstructor} from '../view/Component';
import ChartView, {ChartViewConstructor} from '../view/Chart';
import * as graphic from '../util/graphic';
import {getECData} from '../util/innerStore';
import {
isHighDownDispatcher,
HOVER_STATE_EMPHASIS,
HOVER_STATE_BLUR,
blurSeriesFromHighlightPayload,
toggleSelectionFromPayload,
updateSeriesElementSelection,
getAllSelectedIndices,
isSelectChangePayload,
isHighDownPayload,
HIGHLIGHT_ACTION_TYPE,
DOWNPLAY_ACTION_TYPE,
SELECT_ACTION_TYPE,
UNSELECT_ACTION_TYPE,
TOGGLE_SELECT_ACTION_TYPE,
savePathStates,
enterEmphasis,
leaveEmphasis,
leaveBlur,
enterSelect,
leaveSelect,
enterBlur,
allLeaveBlur,
findComponentHighDownDispatchers,
blurComponent,
handleGlobalMouseOverForHighDown,
handleGlobalMouseOutForHighDown
} from '../util/states';
import * as modelUtil from '../util/model';
import {throttle} from '../util/throttle';
import {seriesStyleTask, dataStyleTask, dataColorPaletteTask} from '../visual/style';
import loadingDefault from '../loading/default';
import Scheduler from './Scheduler';
import lightTheme from '../theme/light';
import darkTheme from '../theme/dark';
import {CoordinateSystemMaster, CoordinateSystemCreator, CoordinateSystemHostModel} from '../coord/CoordinateSystem';
import { parseClassType } from '../util/clazz';
import {ECEventProcessor} from '../util/ECEventProcessor';
import {
Payload, ECElement, RendererType, ECActionEvent,
ActionHandler, ActionInfo, OptionPreprocessor, PostUpdater,
LoadingEffect, LoadingEffectCreator, StageHandlerInternal,
StageHandlerOverallReset, StageHandler,
ViewRootGroup, DimensionDefinitionLoose, ECEventData, ThemeOption,
ECBasicOption,
ECUnitOption,
ZRColor,
ComponentMainType,
ComponentSubType,
ColorString,
SelectChangedPayload,
ScaleDataValue,
ZRElementEventName,
ECElementEvent,
AnimationOption
} from '../util/types';
import Displayable from 'zrender/src/graphic/Displayable';
import { seriesSymbolTask, dataSymbolTask } from '../visual/symbol';
import { getVisualFromData, getItemVisualFromData } from '../visual/helper';
import { deprecateLog, deprecateReplaceLog, error, warn } from '../util/log';
import { handleLegacySelectEvents } from '../legacy/dataSelectAction';
import { registerExternalTransform } from '../data/helper/transform';
import { createLocaleObject, SYSTEM_LANG, LocaleOption } from './locale';
import type {EChartsOption} from '../export/option';
import { findEventDispatcher } from '../util/event';
import decal from '../visual/decal';
import CanvasPainter from 'zrender/src/canvas/Painter';
import SVGPainter from 'zrender/src/svg/Painter';
import lifecycle, {
LifecycleEvents,
UpdateLifecycleTransitionItem,
UpdateLifecycleParams,
UpdateLifecycleTransitionOpt
} from './lifecycle';
import { platformApi, setPlatformAPI } from 'zrender/src/core/platform';
import { getImpl } from './impl';
import type geoSourceManager from '../coord/geo/geoSourceManager';
declare let global: any;
type ModelFinder = modelUtil.ModelFinder;
export const version = '5.4.1';
export const dependencies = {
zrender: '5.4.1'
};
const TEST_FRAME_REMAIN_TIME = 1;
const PRIORITY_PROCESSOR_SERIES_FILTER = 800;
// Some data processors depends on the stack result dimension (to calculate data extent).
// So data stack stage should be in front of data processing stage.
const PRIORITY_PROCESSOR_DATASTACK = 900;
// "Data filter" will block the stream, so it should be
// put at the beginning of data processing.
const PRIORITY_PROCESSOR_FILTER = 1000;
const PRIORITY_PROCESSOR_DEFAULT = 2000;
const PRIORITY_PROCESSOR_STATISTIC = 5000;
const PRIORITY_VISUAL_LAYOUT = 1000;
const PRIORITY_VISUAL_PROGRESSIVE_LAYOUT = 1100;
const PRIORITY_VISUAL_GLOBAL = 2000;
const PRIORITY_VISUAL_CHART = 3000;
const PRIORITY_VISUAL_COMPONENT = 4000;
// Visual property in data. Greater than `PRIORITY_VISUAL_COMPONENT` to enable to
// overwrite the viusal result of component (like `visualMap`)
// using data item specific setting (like itemStyle.xxx on data item)
const PRIORITY_VISUAL_CHART_DATA_CUSTOM = 4500;
// Greater than `PRIORITY_VISUAL_CHART_DATA_CUSTOM` to enable to layout based on
// visual result like `symbolSize`.
const PRIORITY_VISUAL_POST_CHART_LAYOUT = 4600;
const PRIORITY_VISUAL_BRUSH = 5000;
const PRIORITY_VISUAL_ARIA = 6000;
const PRIORITY_VISUAL_DECAL = 7000;
export const PRIORITY = {
PROCESSOR: {
FILTER: PRIORITY_PROCESSOR_FILTER,
SERIES_FILTER: PRIORITY_PROCESSOR_SERIES_FILTER,
STATISTIC: PRIORITY_PROCESSOR_STATISTIC
},
VISUAL: {
LAYOUT: PRIORITY_VISUAL_LAYOUT,
PROGRESSIVE_LAYOUT: PRIORITY_VISUAL_PROGRESSIVE_LAYOUT,
GLOBAL: PRIORITY_VISUAL_GLOBAL,
CHART: PRIORITY_VISUAL_CHART,
POST_CHART_LAYOUT: PRIORITY_VISUAL_POST_CHART_LAYOUT,
COMPONENT: PRIORITY_VISUAL_COMPONENT,
BRUSH: PRIORITY_VISUAL_BRUSH,
CHART_ITEM: PRIORITY_VISUAL_CHART_DATA_CUSTOM,
ARIA: PRIORITY_VISUAL_ARIA,
DECAL: PRIORITY_VISUAL_DECAL
}
};
// Main process have three entries: `setOption`, `dispatchAction` and `resize`,
// where they must not be invoked nestedly, except the only case: invoke
// dispatchAction with updateMethod "none" in main process.
// This flag is used to carry out this rule.
// All events will be triggered out side main process (i.e. when !this[IN_MAIN_PROCESS]).
const IN_MAIN_PROCESS_KEY = '__flagInMainProcess' as const;
const PENDING_UPDATE = '__pendingUpdate' as const;
const STATUS_NEEDS_UPDATE_KEY = '__needsUpdateStatus' as const;
const ACTION_REG = /^[a-zA-Z0-9_]+$/;
const CONNECT_STATUS_KEY = '__connectUpdateStatus' as const;
const CONNECT_STATUS_PENDING = 0 as const;
const CONNECT_STATUS_UPDATING = 1 as const;
const CONNECT_STATUS_UPDATED = 2 as const;
type ConnectStatus =
typeof CONNECT_STATUS_PENDING
| typeof CONNECT_STATUS_UPDATING
| typeof CONNECT_STATUS_UPDATED;
export type SetOptionTransitionOpt = UpdateLifecycleTransitionOpt;
export type SetOptionTransitionOptItem = UpdateLifecycleTransitionItem;
export interface SetOptionOpts {
notMerge?: boolean;
lazyUpdate?: boolean;
silent?: boolean;
// Rule: only `id` mapped will be merged,
// other components of the certain `mainType` will be removed.
replaceMerge?: GlobalModelSetOptionOpts['replaceMerge'];
transition?: SetOptionTransitionOpt
};
export interface ResizeOpts {
width?: number | 'auto', // Can be 'auto' (the same as null/undefined)
height?: number | 'auto', // Can be 'auto' (the same as null/undefined)
animation?: AnimationOption
silent?: boolean // by default false.
};
interface PostIniter {
(chart: EChartsType): void
}
type EventMethodName = 'on' | 'off';
function createRegisterEventWithLowercaseECharts(method: EventMethodName) {
return function (this: ECharts, ...args: any): ECharts {
if (this.isDisposed()) {
disposedWarning(this.id);
return;
}
return toLowercaseNameAndCallEventful<ECharts>(this, method, args);
};
}
function createRegisterEventWithLowercaseMessageCenter(method: EventMethodName) {
return function (this: MessageCenter, ...args: any): MessageCenter {
return toLowercaseNameAndCallEventful<MessageCenter>(this, method, args);
};
}
function toLowercaseNameAndCallEventful<T>(host: T, method: EventMethodName, args: any): T {
// `args[0]` is event name. Event name is all lowercase.
args[0] = args[0] && args[0].toLowerCase();
return Eventful.prototype[method].apply(host, args) as any;
}
class MessageCenter extends Eventful {}
const messageCenterProto = MessageCenter.prototype;
messageCenterProto.on = createRegisterEventWithLowercaseMessageCenter('on');
messageCenterProto.off = createRegisterEventWithLowercaseMessageCenter('off');
// ---------------------------------------
// Internal method names for class ECharts
// ---------------------------------------
let prepare: (ecIns: ECharts) => void;
let prepareView: (ecIns: ECharts, isComponent: boolean) => void;
let updateDirectly: (
ecIns: ECharts, method: string, payload: Payload, mainType: ComponentMainType, subType?: ComponentSubType
) => void;
type UpdateMethod = (this: ECharts, payload?: Payload, renderParams?: UpdateLifecycleParams) => void;
let updateMethods: {
prepareAndUpdate: UpdateMethod,
update: UpdateMethod,
updateTransform: UpdateMethod,
updateView: UpdateMethod,
updateVisual: UpdateMethod,
updateLayout: UpdateMethod
};
let doConvertPixel: (
ecIns: ECharts,
methodName: string,
finder: ModelFinder,
value: (number | number[]) | (ScaleDataValue | ScaleDataValue[])
) => (number | number[]);
let updateStreamModes: (ecIns: ECharts, ecModel: GlobalModel) => void;
let doDispatchAction: (this: ECharts, payload: Payload, silent: boolean) => void;
let flushPendingActions: (this: ECharts, silent: boolean) => void;
let triggerUpdatedEvent: (this: ECharts, silent: boolean) => void;
let bindRenderedEvent: (zr: zrender.ZRenderType, ecIns: ECharts) => void;
let bindMouseEvent: (zr: zrender.ZRenderType, ecIns: ECharts) => void;
let render: (
ecIns: ECharts, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload, updateParams: UpdateLifecycleParams
) => void;
let renderComponents: (
ecIns: ECharts, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload,
updateParams: UpdateLifecycleParams, dirtyList?: ComponentView[]
) => void;
let renderSeries: (
ecIns: ECharts, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload | 'remain',
updateParams: UpdateLifecycleParams,
dirtyMap?: {[uid: string]: any}
) => void;
let createExtensionAPI: (ecIns: ECharts) => ExtensionAPI;
let enableConnect: (ecIns: ECharts) => void;
let markStatusToUpdate: (ecIns: ECharts) => void;
let applyChangedStates: (ecIns: ECharts) => void;
type RenderedEventParam = { elapsedTime: number };
type ECEventDefinition = {
[key in ZRElementEventName]: EventCallbackSingleParam<ECElementEvent>
} & {
rendered: EventCallbackSingleParam<RenderedEventParam>
finished: () => void | boolean
} & {
// TODO: Use ECActionEvent
[key: string]: (...args: unknown[]) => void | boolean
};
type EChartsInitOpts = {
locale?: string | LocaleOption,
renderer?: RendererType,
devicePixelRatio?: number,
useDirtyRect?: boolean,
useCoarsePointer?: boolean,
pointerSize?: number,
ssr?: boolean,
width?: number | string,
height?: number | string
};
class ECharts extends Eventful<ECEventDefinition> {
/**
* @readonly
*/
id: string;
/**
* Group id
* @readonly
*/
group: string;
private _ssr: boolean;
private _zr: zrender.ZRenderType;
private _dom: HTMLElement;
private _model: GlobalModel;
private _throttledZrFlush: zrender.ZRenderType extends {flush: infer R} ? R : never;
private _theme: ThemeOption;
private _locale: LocaleOption;
private _chartsViews: ChartView[] = [];
private _chartsMap: {[viewId: string]: ChartView} = {};
private _componentsViews: ComponentView[] = [];
private _componentsMap: {[viewId: string]: ComponentView} = {};
private _coordSysMgr: CoordinateSystemManager;
private _api: ExtensionAPI;
private _scheduler: Scheduler;
private _messageCenter: MessageCenter;
// Can't dispatch action during rendering procedure
private _pendingActions: Payload[] = [];
// We use never here so ECEventProcessor will not been exposed.
// which may include many unexpected types won't be exposed in the types to developers.
protected _$eventProcessor: never;
private _disposed: boolean;
private _loadingFX: LoadingEffect;
private [PENDING_UPDATE]: {
silent: boolean
updateParams: UpdateLifecycleParams
};
private [IN_MAIN_PROCESS_KEY]: boolean;
private [CONNECT_STATUS_KEY]: ConnectStatus;
private [STATUS_NEEDS_UPDATE_KEY]: boolean;
constructor(
dom: HTMLElement,
// Theme name or themeOption.
theme?: string | ThemeOption,
opts?: EChartsInitOpts
) {
super(new ECEventProcessor());
opts = opts || {};
// Get theme by name
if (isString(theme)) {
theme = themeStorage[theme] as object;
}
this._dom = dom;
let defaultRenderer = 'canvas';
let defaultCoarsePointer: 'auto' | boolean = 'auto';
let defaultUseDirtyRect = false;
if (__DEV__) {
const root = (
/* eslint-disable-next-line */
env.hasGlobalWindow ? window : global
) as any;
defaultRenderer = root.__ECHARTS__DEFAULT__RENDERER__ || defaultRenderer;
defaultCoarsePointer = retrieve2(root.__ECHARTS__DEFAULT__COARSE_POINTER, defaultCoarsePointer);
const devUseDirtyRect = root.__ECHARTS__DEFAULT__USE_DIRTY_RECT__;
defaultUseDirtyRect = devUseDirtyRect == null
? defaultUseDirtyRect
: devUseDirtyRect;
}
const zr = this._zr = zrender.init(dom, {
renderer: opts.renderer || defaultRenderer,
devicePixelRatio: opts.devicePixelRatio,
width: opts.width,
height: opts.height,
ssr: opts.ssr,
useDirtyRect: retrieve2(opts.useDirtyRect, defaultUseDirtyRect),
useCoarsePointer: retrieve2(opts.useCoarsePointer, defaultCoarsePointer),
pointerSize: opts.pointerSize
});
this._ssr = opts.ssr;
// Expect 60 fps.
this._throttledZrFlush = throttle(bind(zr.flush, zr), 17);
theme = clone(theme);
theme && backwardCompat(theme as ECUnitOption, true);
this._theme = theme;
// 语言相关
this._locale = createLocaleObject(opts.locale || SYSTEM_LANG);
this._coordSysMgr = new CoordinateSystemManager(); // 协调器
const api = this._api = createExtensionAPI(this); //自定义的一些api
// Sort on demand
function prioritySortFunc(a: StageHandlerInternal, b: StageHandlerInternal): number {
return a.__prio - b.__prio;
}
timsort(visualFuncs, prioritySortFunc);
timsort(dataProcessorFuncs, prioritySortFunc);
this._scheduler = new Scheduler(this, api, dataProcessorFuncs, visualFuncs);
this._messageCenter = new MessageCenter();
// Init mouse events
this._initEvents();
// In case some people write `window.onresize = chart.resize`
this.resize = bind(this.resize, this);
zr.animation.on('frame', this._onframe, this);
bindRenderedEvent(zr, this);
bindMouseEvent(zr, this);
// ECharts instance can be used as value.
setAsPrimitive(this);
}
private _onframe(): void {
if (this._disposed) {
return;
}
applyChangedStates(this);
const scheduler = this._scheduler;
// Lazy update
if (this[PENDING_UPDATE]) {
const silent = (this[PENDING_UPDATE] as any).silent;
this[IN_MAIN_PROCESS_KEY] = true;
try {
prepare(this);
updateMethods.update.call(this, null, this[PENDING_UPDATE].updateParams);
}
catch (e) {
this[IN_MAIN_PROCESS_KEY] = false;
this[PENDING_UPDATE] = null;
throw e;
}
// At present, in each frame, zrender performs:
// (1) animation step forward.
// (2) trigger('frame') (where this `_onframe` is called)
// (3) zrender flush (render).
// If we do nothing here, since we use `setToFinal: true`, the step (3) above
// will render the final state of the elements before the real animation started.
this._zr.flush();
this[IN_MAIN_PROCESS_KEY] = false;
this[PENDING_UPDATE] = null;
flushPendingActions.call(this, silent);
triggerUpdatedEvent.call(this, silent);
}
// Avoid do both lazy update and progress in one frame.
else if (scheduler.unfinished) {
// Stream progress.
let remainTime = TEST_FRAME_REMAIN_TIME;
const ecModel = this._model;
const api = this._api;
scheduler.unfinished = false;
do {
const startTime = +new Date();
scheduler.performSeriesTasks(ecModel);
// Currently dataProcessorFuncs do not check threshold.
scheduler.performDataProcessorTasks(ecModel);
updateStreamModes(this, ecModel);
// Do not update coordinate system here. Because that coord system update in
// each frame is not a good user experience. So we follow the rule that
// the extent of the coordinate system is determined in the first frame (the
// frame is executed immediately after task reset.
// this._coordSysMgr.update(ecModel, api);
// console.log('--- ec frame visual ---', remainTime);
scheduler.performVisualTasks(ecModel);
renderSeries(this, this._model, api, 'remain', {});
remainTime -= (+new Date() - startTime);
}
while (remainTime > 0 && scheduler.unfinished);
// Call flush explicitly for trigger finished event.
if (!scheduler.unfinished) {
this._zr.flush();
}
// Else, zr flushing be ensue within the same frame,
// because zr flushing is after onframe event.
}
}
getDom(): HTMLElement {
return this._dom;
}
getId(): string {
return this.id;
}
getZr(): zrender.ZRenderType {
return this._zr;
}
isSSR(): boolean {
return this._ssr;
}
/**
* Usage:
* chart.setOption(option, notMerge, lazyUpdate);
* chart.setOption(option, {
* notMerge: ...,
* lazyUpdate: ...,
* silent: ...
* });
*
* @param opts opts or notMerge.
* @param opts.notMerge Default `false`.
* @param opts.lazyUpdate Default `false`. Useful when setOption frequently.
* @param opts.silent Default `false`.
* @param opts.replaceMerge Default undefined.
*/
// Expose to user full option.
setOption<Opt extends ECBasicOption>(option: Opt, notMerge?: boolean, lazyUpdate?: boolean): void;
setOption<Opt extends ECBasicOption>(option: Opt, opts?: SetOptionOpts): void;
/* eslint-disable-next-line */
setOption<Opt extends ECBasicOption>(option: Opt, notMerge?: boolean | SetOptionOpts, lazyUpdate?: boolean): void {
if (this[IN_MAIN_PROCESS_KEY]) {
if (__DEV__) {
error('`setOption` should not be called during main process.');
}
return;
}
if (this._disposed) {
disposedWarning(this.id);
return;
}
let silent;
let replaceMerge;
let transitionOpt: SetOptionTransitionOpt;
if (isObject(notMerge)) {
lazyUpdate = notMerge.lazyUpdate;
silent = notMerge.silent;
replaceMerge = notMerge.replaceMerge;
transitionOpt = notMerge.transition;
notMerge = notMerge.notMerge;
}
this[IN_MAIN_PROCESS_KEY] = true;
if (!this._model || notMerge) {
const optionManager = new OptionManager(this._api); // ECharts option manager
const theme = this._theme;
const ecModel = this._model = new GlobalModel();
ecModel.scheduler = this._scheduler;
ecModel.ssr = this._ssr;
ecModel.init(null, null, null, theme, this._locale, optionManager);
}
this._model.setOption(option as ECBasicOption, { replaceMerge }, optionPreprocessorFuncs);
const updateParams = {
seriesTransition: transitionOpt,
optionChanged: true
} as UpdateLifecycleParams;
if (lazyUpdate) {
this[PENDING_UPDATE] = {
silent: silent,
updateParams: updateParams
};
this[IN_MAIN_PROCESS_KEY] = false;
// `setOption(option, {lazyMode: true})` may be called when zrender has been slept.
// It should wake it up to make sure zrender start to render at the next frame.
this.getZr().wakeUp();
}
else {
try {
prepare(this);
updateMethods.update.call(this, null, updateParams);
}
catch (e) {
this[PENDING_UPDATE] = null;
this[IN_MAIN_PROCESS_KEY] = false;
throw e;
}
// Ensure zr refresh sychronously, and then pixel in canvas can be
// fetched after `setOption`.
if (!this._ssr) {
// not use flush when using ssr mode.
this._zr.flush();
}
this[PENDING_UPDATE] = null;
this[IN_MAIN_PROCESS_KEY] = false;
flushPendingActions.call(this, silent);
triggerUpdatedEvent.call(this, silent);
}
}
/**
* @deprecated
*/
private setTheme(): void {
deprecateLog('ECharts#setTheme() is DEPRECATED in ECharts 3.0');
}
// We don't want developers to use getModel directly.
private getModel(): GlobalModel {
return this._model;
}
getOption(): ECBasicOption {
return this._model && this._model.getOption() as ECBasicOption;
}
getWidth(): number {
return this._zr.getWidth();
}
getHeight(): number {
return this._zr.getHeight();
}
getDevicePixelRatio(): number {
return (this._zr.painter as CanvasPainter).dpr
/* eslint-disable-next-line */
|| (env.hasGlobalWindow && window.devicePixelRatio) || 1;
}
/**
* Get canvas which has all thing rendered
* @deprecated Use renderToCanvas instead.
*/
getRenderedCanvas(opts?: any): HTMLCanvasElement {
if (__DEV__) {
deprecateReplaceLog('getRenderedCanvas', 'renderToCanvas');
}
return this.renderToCanvas(opts);
}
renderToCanvas(opts?: {
backgroundColor?: ZRColor
pixelRatio?: number
}): HTMLCanvasElement {
opts = opts || {};
const painter = this._zr.painter;
if (__DEV__) {
if (painter.type !== 'canvas') {
throw new Error('renderToCanvas can only be used in the canvas renderer.');
}
}
return (painter as CanvasPainter).getRenderedCanvas({
backgroundColor: (opts.backgroundColor || this._model.get('backgroundColor')) as ColorString,
pixelRatio: opts.pixelRatio || this.getDevicePixelRatio()
});
}
renderToSVGString(opts?: {
useViewBox?: boolean
}): string {
opts = opts || {};
const painter = this._zr.painter;
if (__DEV__) {
if (painter.type !== 'svg') {
throw new Error('renderToSVGString can only be used in the svg renderer.');
}
}
return (painter as SVGPainter).renderToString({
useViewBox: opts.useViewBox
});
}
/**
* Get svg data url
*/
getSvgDataURL(): string {
if (!env.svgSupported) {
return;
}
const zr = this._zr;
const list = zr.storage.getDisplayList();
// Stop animations
each(list, function (el: Element) {
el.stopAnimation(null, true);
});
return (zr.painter as SVGPainter).toDataURL();
}
getDataURL(opts?: {
// file type 'png' by default
type?: 'png' | 'jpeg' | 'svg',
pixelRatio?: number,
backgroundColor?: ZRColor,
// component type array
excludeComponents?: ComponentMainType[]
}): string {
if (this._disposed) {
disposedWarning(this.id);
return;
}
opts = opts || {};
const excludeComponents = opts.excludeComponents;
const ecModel = this._model;
const excludesComponentViews: ComponentView[] = [];
const self = this;
each(excludeComponents, function (componentType) {
ecModel.eachComponent({
mainType: componentType
}, function (component) {
const view = self._componentsMap[component.__viewId];
if (!view.group.ignore) {
excludesComponentViews.push(view);
view.group.ignore = true;
}
});
});
const url = this._zr.painter.getType() === 'svg'
? this.getSvgDataURL()
: this.renderToCanvas(opts).toDataURL(
'image/' + (opts && opts.type || 'png')
);
each(excludesComponentViews, function (view) {
view.group.ignore = false;
});
return url;
}
getConnectedDataURL(opts?: {
// file type 'png' by default
type?: 'png' | 'jpeg' | 'svg',
pixelRatio?: number,
backgroundColor?: ZRColor,
connectedBackgroundColor?: ZRColor
excludeComponents?: string[]
}): string {
if (this._disposed) {
disposedWarning(this.id);
return;
}
const isSvg = opts.type === 'svg';
const groupId = this.group;
const mathMin = Math.min;
const mathMax = Math.max;
const MAX_NUMBER = Infinity;
if (connectedGroups[groupId]) {
let left = MAX_NUMBER;
let top = MAX_NUMBER;
let right = -MAX_NUMBER;
let bottom = -MAX_NUMBER;
const canvasList: {dom: HTMLCanvasElement | string, left: number, top: number}[] = [];
const dpr = (opts && opts.pixelRatio) || this.getDevicePixelRatio();
each(instances, function (chart, id) {
if (chart.group === groupId) {
const canvas = isSvg
? (chart.getZr().painter as SVGPainter).getSvgDom().innerHTML
: chart.renderToCanvas(clone(opts));
const boundingRect = chart.getDom().getBoundingClientRect();
left = mathMin(boundingRect.left, left);
top = mathMin(boundingRect.top, top);
right = mathMax(boundingRect.right, right);
bottom = mathMax(boundingRect.bottom, bottom);
canvasList.push({
dom: canvas,
left: boundingRect.left,
top: boundingRect.top
});
}
});
left *= dpr;
top *= dpr;
right *= dpr;
bottom *= dpr;
const width = right - left;
const height = bottom - top;
const targetCanvas = platformApi.createCanvas();
const zr = zrender.init(targetCanvas, {
renderer: isSvg ? 'svg' : 'canvas'
});
zr.resize({
width: width,
height: height
});
if (isSvg) {
let content = '';
each(canvasList, function (item) {
const x = item.left - left;
const y = item.top - top;
content += '<g transform="translate(' + x + ','
+ y + ')">' + item.dom + '</g>';
});
(zr.painter as SVGPainter).getSvgRoot().innerHTML = content;
if (opts.connectedBackgroundColor) {
(zr.painter as SVGPainter).setBackgroundColor(opts.connectedBackgroundColor as string);
}
zr.refreshImmediately();
return (zr.painter as SVGPainter).toDataURL();
}
else {
// Background between the charts
if (opts.connectedBackgroundColor) {
zr.add(new graphic.Rect({
shape: {
x: 0,
y: 0,
width: width,
height: height
},
style: {
fill: opts.connectedBackgroundColor
}
}));
}
each(canvasList, function (item) {
const img = new graphic.Image({
style: {
x: item.left * dpr - left,
y: item.top * dpr - top,
image: item.dom
}
});
zr.add(img);
});
zr.refreshImmediately();
return targetCanvas.toDataURL('image/' + (opts && opts.type || 'png'));
}
}
else {
return this.getDataURL(opts);
}
}
/**
* Convert from logical coordinate system to pixel coordinate system.
* See CoordinateSystem#convertToPixel.
*/
convertToPixel(finder: ModelFinder, value: ScaleDataValue): number;
convertToPixel(finder: ModelFinder, value: ScaleDataValue[]): number[];
convertToPixel(finder: ModelFinder, value: ScaleDataValue | ScaleDataValue[]): number | number[] {
return doConvertPixel(this, 'convertToPixel', finder, value);
}
/**
* Convert from pixel coordinate system to logical coordinate system.
* See CoordinateSystem#convertFromPixel.
*/
convertFromPixel(finder: ModelFinder, value: number): number;
convertFromPixel(finder: ModelFinder, value: number[]): number[];
convertFromPixel(finder: ModelFinder, value: number | number[]): number | number[] {
return doConvertPixel(this, 'convertFromPixel', finder, value);
}
/**
* Is the specified coordinate systems or components contain the given pixel point.
* @param {Array|number} value
* @return {boolean} result
*/
containPixel(finder: ModelFinder, value: number[]): boolean {
if (this._disposed) {
disposedWarning(this.id);
return;
}
const ecModel = this._model;
let result: boolean;
const findResult = modelUtil.parseFinder(ecModel, finder);
each(findResult, function (models, key) {
key.indexOf('Models') >= 0 && each(models as ComponentModel[], function (model) {
const coordSys = (model as CoordinateSystemHostModel).coordinateSystem;
if (coordSys && coordSys.containPoint) {
result = result || !!coordSys.containPoint(value);
}
else if (key === 'seriesModels') {
const view = this._chartsMap[model.__viewId];
if (view && view.containPoint) {
result = result || view.containPoint(value, model as SeriesModel);
}
else {
if (__DEV__) {
warn(key + ': ' + (view
? 'The found component do not support containPoint.'
: 'No view mapping to the found component.'
));
}
}
}
else {
if (__DEV__) {
warn(key + ': containPoint is not supported');
}
}
}, this);
}, this);
return !!result;
}
/**
* Get visual from series or data.
* @param finder
* If string, e.g., 'series', means {seriesIndex: 0}.
* If Object, could contain some of these properties below:
* {
* seriesIndex / seriesId / seriesName,
* dataIndex / dataIndexInside
* }
* If dataIndex is not specified, series visual will be fetched,
* but not data item visual.
* If all of seriesIndex, seriesId, seriesName are not specified,
* visual will be fetched from first series.
* @param visualType 'color', 'symbol', 'symbolSize'
*/
getVisual(finder: ModelFinder, visualType: string) {
const ecModel = this._model;
const parsedFinder = modelUtil.parseFinder(ecModel, finder, {
defaultMainType: 'series'
}) as modelUtil.ParsedModelFinderKnown;
const seriesModel = parsedFinder.seriesModel;
if (__DEV__) {
if (!seriesModel) {
warn('There is no specified series model');
}
}
const data = seriesModel.getData();
const dataIndexInside = parsedFinder.hasOwnProperty('dataIndexInside')
? parsedFinder.dataIndexInside
: parsedFinder.hasOwnProperty('dataIndex')
? data.indexOfRawIndex(parsedFinder.dataIndex)
: null;
return dataIndexInside != null
? getItemVisualFromData(data, dataIndexInside, visualType)
: getVisualFromData(data, visualType);
}
/**
* Get view of corresponding component model
*/
private getViewOfComponentModel(componentModel: ComponentModel): ComponentView {
return this._componentsMap[componentModel.__viewId];
}
/**
* Get view of corresponding series model
*/
private getViewOfSeriesModel(seriesModel: SeriesModel): ChartView {
return this._chartsMap[seriesModel.__viewId];
}
private _initEvents(): void {
each(MOUSE_EVENT_NAMES, (eveName) => {
const handler = (e: ElementEvent) => {
const ecModel = this.getModel();
const el = e.target;
let params: ECElementEvent;
const isGlobalOut = eveName === 'globalout';
// no e.target when 'globalout'.
if (isGlobalOut) {
params = {} as ECElementEvent;
}
else {
el && findEventDispatcher(el, (parent) => {
const ecData = getECData(parent);
if (ecData && ecData.dataIndex != null) {
const dataModel = ecData.dataModel || ecModel.getSeriesByIndex(ecData.seriesIndex);
params = (
dataModel && dataModel.getDataParams(ecData.dataIndex, ecData.dataType) || {}
) as ECElementEvent;
return true;
}
// If element has custom eventData of components
else if (ecData.eventData) {
params = extend({}, ecData.eventData) as ECElementEvent;
return true;
}
}, true);
}
// Contract: if params prepared in mouse event,
// these properties must be specified:
// {
// componentType: string (component main type)
// componentIndex: number
// }
// Otherwise event query can not work.
if (params) {
let componentType = params.componentType;
let componentIndex = params.componentIndex;
// Special handling for historic reason: when trigger by
// markLine/markPoint/markArea, the componentType is
// 'markLine'/'markPoint'/'markArea', but we should better
// enable them to be queried by seriesIndex, since their
// option is set in each series.
if (componentType === 'markLine'
|| componentType === 'markPoint'
|| componentType === 'markArea'
) {
componentType = 'series';
componentIndex = params.seriesIndex;
}
const model = componentType && componentIndex != null
&& ecModel.getComponent(componentType, componentIndex);
const view = model && this[
model.mainType === 'series' ? '_chartsMap' : '_componentsMap'
][model.__viewId];
if (__DEV__) {
// `event.componentType` and `event[componentTpype + 'Index']` must not
// be missed, otherwise there is no way to distinguish source component.
// See `dataFormat.getDataParams`.
if (!isGlobalOut && !(model && view)) {
warn('model or view can not be found by params');
}
}
params.event = e;
params.type = eveName;
(this._$eventProcessor as ECEventProcessor).eventInfo = {
targetEl: el,
packedEvent: params,
model: model,
view: view
};
this.trigger(eveName, params);
}
};
// Consider that some component (like tooltip, brush, ...)
// register zr event handler, but user event handler might
// do anything, such as call `setOption` or `dispatchAction`,
// which probably update any of the content and probably
// cause problem if it is called previous other inner handlers.
(handler as any).zrEventfulCallAtLast = true;
this._zr.on(eveName, handler, this);
});
each(eventActionMap, (actionType, eventType) => {
this._messageCenter.on(eventType, function (event: Payload) {
(this as any).trigger(eventType, event);
}, this);
});
// Extra events
// TODO register?
each(
['selectchanged'],
(eventType) => {
this._messageCenter.on(eventType, function (event: Payload) {
(this as any).trigger(eventType, event);
}, this);
}
);
handleLegacySelectEvents(this._messageCenter, this, this._api);
}
isDisposed(): boolean {
return this._disposed;
}
clear(): void {
if (this._disposed) {
disposedWarning(this.id);
return;
}
this.setOption({ series: [] } as EChartsOption, true);
}
dispose(): void {
if (this._disposed) {
disposedWarning(this.id);
return;
}
this._disposed = true;
const dom = this.getDom();
if (dom) {
modelUtil.setAttribute(this.getDom(), DOM_ATTRIBUTE_KEY, '');
}
const chart = this;
const api = chart._api;
const ecModel = chart._model;
each(chart._componentsViews, function (component) {
component.dispose(ecModel, api);
});
each(chart._chartsViews, function (chart) {
chart.dispose(ecModel, api);
});
// Dispose after all views disposed
chart._zr.dispose();
// Set properties to null.
// To reduce the memory cost in case the top code still holds this instance unexpectedly.
chart._dom =
chart._model =
chart._chartsMap =
chart._componentsMap =
chart._chartsViews =
chart._componentsViews =
chart._scheduler =
chart._api =
chart._zr =
chart._throttledZrFlush =
chart._theme =
chart._coordSysMgr =
chart._messageCenter = null;
delete instances[chart.id];
}
/**
* Resize the chart
*/
resize(opts?: ResizeOpts): void {
if (this[IN_MAIN_PROCESS_KEY]) {
if (__DEV__) {
error('`resize` should not be called during main process.');
}
return;
}
if (this._disposed) {
disposedWarning(this.id);
return;
}
this._zr.resize(opts);
const ecModel = this._model;
// Resize loading effect
this._loadingFX && this._loadingFX.resize();
if (!ecModel) {
return;
}
let needPrepare = ecModel.resetOption('media');
let silent = opts && opts.silent;
// There is some real cases that:
// chart.setOption(option, { lazyUpdate: true });
// chart.resize();
if (this[PENDING_UPDATE]) {
if (silent == null) {
silent = (this[PENDING_UPDATE] as any).silent;
}
needPrepare = true;
this[PENDING_UPDATE] = null;
}
this[IN_MAIN_PROCESS_KEY] = true;
try {
needPrepare && prepare(this);
updateMethods.update.call(this, {
type: 'resize',
animation: extend({
// Disable animation
duration: 0
}, opts && opts.animation)
});
}
catch (e) {
this[IN_MAIN_PROCESS_KEY] = false;
throw e;
}
this[IN_MAIN_PROCESS_KEY] = false;
flushPendingActions.call(this, silent);
triggerUpdatedEvent.call(this, silent);
}
/**
* Show loading effect
* @param name 'default' by default
* @param cfg cfg of registered loading effect
*/
showLoading(cfg?: object): void;
showLoading(name?: string, cfg?: object): void;
showLoading(name?: string | object, cfg?: object): void {
if (this._disposed) {
disposedWarning(this.id);
return;
}
if (isObject(name)) {
cfg = name as object;
name = '';
}
name = name || 'default';
this.hideLoading();
if (!loadingEffects[name]) {
if (__DEV__) {
warn('Loading effects ' + name + ' not exists.');
}
return;
}
const el = loadingEffects[name](this._api, cfg);
const zr = this._zr;
this._loadingFX = el;
zr.add(el);
}
/**
* Hide loading effect
*/
hideLoading(): void {
if (this._disposed) {
disposedWarning(this.id);
return;
}
this._loadingFX && this._zr.remove(this._loadingFX);
this._loadingFX = null;
}
makeActionFromEvent(eventObj: ECActionEvent): Payload {
const payload = extend({}, eventObj) as Payload;
payload.type = eventActionMap[eventObj.type];
return payload;
}
/**
* @param opt If pass boolean, means opt.silent
* @param opt.silent Default `false`. Whether trigger events.
* @param opt.flush Default `undefined`.
* true: Flush immediately, and then pixel in canvas can be fetched
* immediately. Caution: it might affect performance.
* false: Not flush.
* undefined: Auto decide whether perform flush.
*/
dispatchAction(
payload: Payload,
opt?: boolean | {
silent?: boolean,
flush?: boolean | undefined
}
): void {
if (this._disposed) {
disposedWarning(this.id);
return;
}
if (!isObject(opt)) {
opt = {silent: !!opt};
}
if (!actions[payload.type]) {
return;
}
// Avoid dispatch action before setOption. Especially in `connect`.
if (!this._model) {
return;
}
// May dispatchAction in rendering procedure
if (this[IN_MAIN_PROCESS_KEY]) {
this._pendingActions.push(payload);
return;
}
const silent = opt.silent;
doDispatchAction.call(this, payload, silent);
const flush = opt.flush;
if (flush) {
this._zr.flush();
}
else if (flush !== false && env.browser.weChat) {
// In WeChat embedded browser, `requestAnimationFrame` and `setInterval`
// hang when sliding page (on touch event), which cause that zr does not
// refresh until user interaction finished, which is not expected.
// But `dispatchAction` may be called too frequently when pan on touch
// screen, which impacts performance if do not throttle them.
this._throttledZrFlush();
}
flushPendingActions.call(this, silent);
triggerUpdatedEvent.call(this, silent);
}
updateLabelLayout() {
lifecycle.trigger('series:layoutlabels', this._model, this._api, {
// Not adding series labels.
// TODO
updatedSeries: []
});
}
appendData(params: {
seriesIndex: number,
data: any
}): void {
if (this._disposed) {
disposedWarning(this.id);
return;
}
const seriesIndex = params.seriesIndex;
const ecModel = this.getModel();
const seriesModel = ecModel.getSeriesByIndex(seriesIndex) as SeriesModel;
if (__DEV__) {
assert(params.data && seriesModel);
}
seriesModel.appendData(params);
// Note: `appendData` does not support that update extent of coordinate
// system, util some scenario require that. In the expected usage of
// `appendData`, the initial extent of coordinate system should better
// be fixed by axis `min`/`max` setting or initial data, otherwise if
// the extent changed while `appendData`, the location of the painted
// graphic elements have to be changed, which make the usage of
// `appendData` meaningless.
this._scheduler.unfinished = true;
this.getZr().wakeUp();
}
// A work around for no `internal` modifier in ts yet but
// need to strictly hide private methods to JS users.
private static internalField = (function () {
prepare = function (ecIns: ECharts): void {
const scheduler = ecIns._scheduler;
scheduler.restorePipelines(ecIns._model);
scheduler.prepareStageTasks();
prepareView(ecIns, true);
prepareView(ecIns, false);
scheduler.plan();
};
/**
* Prepare view instances of charts and components
*/
prepareView = function (ecIns: ECharts, isComponent: boolean): void {
const ecModel = ecIns._model;
const scheduler = ecIns._scheduler;
const viewList = isComponent ? ecIns._componentsViews : ecIns._chartsViews;
const viewMap = isComponent ? ecIns._componentsMap : ecIns._chartsMap;
const zr = ecIns._zr;
const api = ecIns._api;
for (let i = 0; i < viewList.length; i++) {
viewList[i].__alive = false;
}
isComponent
? ecModel.eachComponent(function (componentType, model) {
componentType !== 'series' && doPrepare(model);
})
: ecModel.eachSeries(doPrepare);
function doPrepare(model: ComponentModel): void {
// By default view will be reuse