UNPKG

di-echarts

Version:

Apache ECharts is a powerful, interactive charting and data visualization library for browser

1,479 lines (1,262 loc) 110 kB
/* * 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