| /* | |
| * 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 } 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; | |
| const hasWindow = typeof window !== 'undefined'; | |
| export const version = '5.4.0'; | |
| export const dependencies = { | |
| zrender: '5.4.0' | |
| }; | |
| 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 */ | |
| hasWindow ? 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); | |
| // 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); | |
| 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 */ | |
| || (hasWindow && 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__) { | |
| console.warn(key + ': ' + (view | |
| ? 'The found component do not support containPoint.' | |
| : 'No view mapping to the found component.' | |
| )); | |
| } | |
| } | |
| } | |
| else { | |
| if (__DEV__) { | |
| console.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) { | |
| console.warn('There is no specified seires 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)) { | |
| console.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__) { | |
| console.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 reused if possible for the case that `setOption` with "notMerge" | |
| // mode and need to enable transition animation. (Usually, when they have the same id, or | |
| // especially no id but have the same type & name & index. See the `model.id` generation | |
| // rule in `makeIdAndName` and `viewId` generation rule here). | |
| // But in `replaceMerge` mode, this feature should be able to disabled when it is clear that | |
| // the new model has nothing to do with the old model. | |
| const requireNewView = model.__requireNewView; | |
| // This command should not work twice. | |
| model.__requireNewView = false; | |
| // Consider: id same and type changed. | |
| const viewId = '_ec_' + model.id + '_' + model.type; | |
| let view = !requireNewView && viewMap[viewId]; | |
| if (!view) { | |
| const classType = parseClassType(model.type); | |
| const Clazz = isComponent | |
| ? (ComponentView as ComponentViewConstructor).getClass(classType.main, classType.sub) | |
| : ( | |
| // FIXME:TS | |
| // (ChartView as ChartViewConstructor).getClass('series', classType.sub) | |
| // For backward compat, still support a chart type declared as only subType | |
| // like "liquidfill", but recommend "series.liquidfill" | |
| // But need a base class to make a type series. | |
| (ChartView as ChartViewConstructor).getClass(classType.sub) | |
| ); | |
| if (__DEV__) { | |
| assert(Clazz, classType.sub + ' does not exist.'); | |
| } | |
| view = new Clazz(); | |
| view.init(ecModel, api); | |
| viewMap[viewId] = view; | |
| viewList.push(view as any); | |
| zr.add(view.group); | |
| } | |
| model.__viewId = view.__id = viewId; | |
| view.__alive = true; | |
| view.__model = model; | |
| view.group.__ecComponentInfo = { | |
| mainType: model.mainType, | |
| index: model.componentIndex | |
| }; | |
| !isComponent && scheduler.prepareView( | |
| view as ChartView, model as SeriesModel, ecModel, api | |
| ); | |
| } | |
| for (let i = 0; i < viewList.length;) { | |
| const view = viewList[i]; | |
| if (!view.__alive) { | |
| !isComponent && (view as ChartView).renderTask.dispose(); | |
| zr.remove(view.group); | |
| view.dispose(ecModel, api); | |
| viewList.splice(i, 1); | |
| if (viewMap[view.__id] === view) { | |
| delete viewMap[view.__id]; | |
| } | |
| view.__id = view.group.__ecComponentInfo = null; | |
| } | |
| else { | |
| i++; | |
| } | |
| } | |
| }; | |
| updateDirectly = function ( | |
| ecIns: ECharts, | |
| method: string, | |
| payload: Payload, | |
| mainType: ComponentMainType, | |
| subType?: ComponentSubType | |
| ): void { | |
| const ecModel = ecIns._model; | |
| ecModel.setUpdatePayload(payload); | |
| // broadcast | |
| if (!mainType) { | |
| // FIXME | |
| // Chart will not be update directly here, except set dirty. | |
| // But there is no such scenario now. | |
| each([].concat(ecIns._componentsViews).concat(ecIns._chartsViews), callView); | |
| return; | |
| } | |
| const query: QueryConditionKindA['query'] = {}; | |
| query[mainType + 'Id'] = payload[mainType + 'Id']; | |
| query[mainType + 'Index'] = payload[mainType + 'Index']; | |
| query[mainType + 'Name'] = payload[mainType + 'Name']; | |
| const condition = {mainType: mainType, query: query} as QueryConditionKindA; | |
| subType && (condition.subType = subType); // subType may be '' by parseClassType; | |
| const excludeSeriesId = payload.excludeSeriesId; | |
| let excludeSeriesIdMap: HashMap<true, string>; | |
| if (excludeSeriesId != null) { | |
| excludeSeriesIdMap = createHashMap(); | |
| each(modelUtil.normalizeToArray(excludeSeriesId), id => { | |
| const modelId = modelUtil.convertOptionIdName(id, null); | |
| if (modelId != null) { | |
| excludeSeriesIdMap.set(modelId, true); | |
| } | |
| }); | |
| } | |
| // If dispatchAction before setOption, do nothing. | |
| ecModel && ecModel.eachComponent(condition, function (model) { | |
| const isExcluded = excludeSeriesIdMap && excludeSeriesIdMap.get(model.id) !== null; | |
| if (isExcluded) { | |
| return; | |
| }; | |
| if (isHighDownPayload(payload)) { | |
| if (model instanceof SeriesModel) { | |
| if ( | |
| payload.type === HIGHLIGHT_ACTION_TYPE | |
| && !payload.notBlur && !model.get(['emphasis', 'disabled']) | |
| ) { | |
| blurSeriesFromHighlightPayload(model, payload, ecIns._api); | |
| } | |
| } | |
| else { | |
| const { focusSelf, dispatchers } = findComponentHighDownDispatchers( | |
| model.mainType, model.componentIndex, payload.name, ecIns._api | |
| ); | |
| if (payload.type === HIGHLIGHT_ACTION_TYPE && focusSelf && !payload.notBlur) { | |
| blurComponent(model.mainType, model.componentIndex, ecIns._api); | |
| } | |
| // PENDING: | |
| // Whether to put this "enter emphasis" code in `ComponentView`, | |
| // which will be the same as `ChartView` but might be not necessary | |
| // and will be far from this logic. | |
| if (dispatchers) { | |
| each(dispatchers, dispatcher => { | |
| payload.type === HIGHLIGHT_ACTION_TYPE | |
| ? enterEmphasis(dispatcher) | |
| : leaveEmphasis(dispatcher); | |
| }); | |
| } | |
| } | |
| } | |
| else if (isSelectChangePayload(payload)) { | |
| // TODO geo | |
| if (model instanceof SeriesModel) { | |
| toggleSelectionFromPayload(model, payload, ecIns._api); | |
| updateSeriesElementSelection(model); | |
| markStatusToUpdate(ecIns); | |
| } | |
| } | |
| }, ecIns); | |
| ecModel && ecModel.eachComponent(condition, function (model) { | |
| const isExcluded = excludeSeriesIdMap && excludeSeriesIdMap.get(model.id) !== null; | |
| if (isExcluded) { | |
| return; | |
| }; | |
| callView(ecIns[ | |
| mainType === 'series' ? '_chartsMap' : '_componentsMap' | |
| ][model.__viewId]); | |
| }, ecIns); | |
| function callView(view: ComponentView | ChartView) { | |
| view && view.__alive && (view as any)[method] && (view as any)[method]( | |
| view.__model, ecModel, ecIns._api, payload | |
| ); | |
| } | |
| }; | |
| updateMethods = { | |
| prepareAndUpdate(this: ECharts, payload: Payload): void { | |
| prepare(this); | |
| updateMethods.update.call(this, payload, { | |
| // Needs to mark option changed if newOption is given. | |
| // It's from MagicType. | |
| // TODO If use a separate flag optionChanged in payload? | |
| optionChanged: payload.newOption != null | |
| }); | |
| }, | |
| update(this: ECharts, payload: Payload, updateParams: UpdateLifecycleParams): void { | |
| const ecModel = this._model; | |
| const api = this._api; | |
| const zr = this._zr; | |
| const coordSysMgr = this._coordSysMgr; | |
| const scheduler = this._scheduler; | |
| // update before setOption | |
| if (!ecModel) { | |
| return; | |
| } | |
| ecModel.setUpdatePayload(payload); | |
| scheduler.restoreData(ecModel, payload); | |
| scheduler.performSeriesTasks(ecModel); | |
| // TODO | |
| // Save total ecModel here for undo/redo (after restoring data and before processing data). | |
| // Undo (restoration of total ecModel) can be carried out in 'action' or outside API call. | |
| // Create new coordinate system each update | |
| // In LineView may save the old coordinate system and use it to get the original point. | |
| coordSysMgr.create(ecModel, api); | |
| scheduler.performDataProcessorTasks(ecModel, payload); | |
| // Current stream render is not supported in data process. So we can update | |
| // stream modes after data processing, where the filtered data is used to | |
| // determine whether to use progressive rendering. | |
| updateStreamModes(this, ecModel); | |
| // We update stream modes before coordinate system updated, then the modes info | |
| // can be fetched when coord sys updating (consider the barGrid extent fix). But | |
| // the drawback is the full coord info can not be fetched. Fortunately this full | |
| // coord is not required in stream mode updater currently. | |
| coordSysMgr.update(ecModel, api); | |
| clearColorPalette(ecModel); | |
| scheduler.performVisualTasks(ecModel, payload); | |
| render(this, ecModel, api, payload, updateParams); | |
| // Set background | |
| const backgroundColor = ecModel.get('backgroundColor') || 'transparent'; | |
| const darkMode = ecModel.get('darkMode'); | |
| zr.setBackgroundColor(backgroundColor); | |
| // Force set dark mode. | |
| if (darkMode != null && darkMode !== 'auto') { | |
| zr.setDarkMode(darkMode); | |
| } | |
| lifecycle.trigger('afterupdate', ecModel, api); | |
| }, | |
| updateTransform(this: ECharts, payload: Payload): void { | |
| const ecModel = this._model; | |
| const api = this._api; | |
| // update before setOption | |
| if (!ecModel) { | |
| return; | |
| } | |
| ecModel.setUpdatePayload(payload); | |
| // ChartView.markUpdateMethod(payload, 'updateTransform'); | |
| const componentDirtyList = []; | |
| ecModel.eachComponent((componentType, componentModel) => { | |
| if (componentType === 'series') { | |
| return; | |
| } | |
| const componentView = this.getViewOfComponentModel(componentModel); | |
| if (componentView && componentView.__alive) { | |
| if (componentView.updateTransform) { | |
| const result = componentView.updateTransform(componentModel, ecModel, api, payload); | |
| result && result.update && componentDirtyList.push(componentView); | |
| } | |
| else { | |
| componentDirtyList.push(componentView); | |
| } | |
| } | |
| }); | |
| const seriesDirtyMap = createHashMap(); | |
| ecModel.eachSeries((seriesModel) => { | |
| const chartView = this._chartsMap[seriesModel.__viewId]; | |
| if (chartView.updateTransform) { | |
| const result = chartView.updateTransform(seriesModel, ecModel, api, payload); | |
| result && result.update && seriesDirtyMap.set(seriesModel.uid, 1); | |
| } | |
| else { | |
| seriesDirtyMap.set(seriesModel.uid, 1); | |
| } | |
| }); | |
| clearColorPalette(ecModel); | |
| // Keep pipe to the exist pipeline because it depends on the render task of the full pipeline. | |
| // this._scheduler.performVisualTasks(ecModel, payload, 'layout', true); | |
| this._scheduler.performVisualTasks( | |
| ecModel, payload, {setDirty: true, dirtyMap: seriesDirtyMap} | |
| ); | |
| // Currently, not call render of components. Geo render cost a lot. | |
| // renderComponents(ecIns, ecModel, api, payload, componentDirtyList); | |
| renderSeries(this, ecModel, api, payload, {}, seriesDirtyMap); | |
| lifecycle.trigger('afterupdate', ecModel, api); | |
| }, | |
| updateView(this: ECharts, payload: Payload): void { | |
| const ecModel = this._model; | |
| // update before setOption | |
| if (!ecModel) { | |
| return; | |
| } | |
| ecModel.setUpdatePayload(payload); | |
| ChartView.markUpdateMethod(payload, 'updateView'); | |
| clearColorPalette(ecModel); | |
| // Keep pipe to the exist pipeline because it depends on the render task of the full pipeline. | |
| this._scheduler.performVisualTasks(ecModel, payload, {setDirty: true}); | |
| render(this, ecModel, this._api, payload, {}); | |
| lifecycle.trigger('afterupdate', ecModel, this._api); | |
| }, | |
| updateVisual(this: ECharts, payload: Payload): void { | |
| // updateMethods.update.call(this, payload); | |
| const ecModel = this._model; | |
| // update before setOption | |
| if (!ecModel) { | |
| return; | |
| } | |
| ecModel.setUpdatePayload(payload); | |
| // clear all visual | |
| ecModel.eachSeries(function (seriesModel) { | |
| seriesModel.getData().clearAllVisual(); | |
| }); | |
| // Perform visual | |
| ChartView.markUpdateMethod(payload, 'updateVisual'); | |
| clearColorPalette(ecModel); | |
| // Keep pipe to the exist pipeline because it depends on the render task of the full pipeline. | |
| this._scheduler.performVisualTasks(ecModel, payload, {visualType: 'visual', setDirty: true}); | |
| ecModel.eachComponent((componentType, componentModel) => { // TODO componentType may be series. | |
| if (componentType !== 'series') { | |
| const componentView = this.getViewOfComponentModel(componentModel); | |
| componentView && componentView.__alive | |
| && componentView.updateVisual(componentModel, ecModel, this._api, payload); | |
| } | |
| }); | |
| ecModel.eachSeries((seriesModel) => { | |
| const chartView = this._chartsMap[seriesModel.__viewId]; | |
| chartView.updateVisual(seriesModel, ecModel, this._api, payload); | |
| }); | |
| lifecycle.trigger('afterupdate', ecModel, this._api); | |
| }, | |
| updateLayout(this: ECharts, payload: Payload): void { | |
| updateMethods.update.call(this, payload); | |
| } | |
| }; | |
| doConvertPixel = function ( | |
| ecIns: ECharts, | |
| methodName: 'convertFromPixel' | 'convertToPixel', | |
| finder: ModelFinder, | |
| value: (number | number[]) | (ScaleDataValue | ScaleDataValue[]) | |
| ): (number | number[]) { | |
| if (ecIns._disposed) { | |
| disposedWarning(ecIns.id); | |
| return; | |
| } | |
| const ecModel = ecIns._model; | |
| const coordSysList = ecIns._coordSysMgr.getCoordinateSystems(); | |
| let result; | |
| const parsedFinder = modelUtil.parseFinder(ecModel, finder); | |
| for (let i = 0; i < coordSysList.length; i++) { | |
| const coordSys = coordSysList[i]; | |
| if (coordSys[methodName] | |
| && (result = coordSys[methodName](ecModel, parsedFinder, value as any)) != null | |
| ) { | |
| return result; | |
| } | |
| } | |
| if (__DEV__) { | |
| console.warn( | |
| 'No coordinate system that supports ' + methodName + ' found by the given finder.' | |
| ); | |
| } | |
| }; | |
| updateStreamModes = function (ecIns: ECharts, ecModel: GlobalModel): void { | |
| const chartsMap = ecIns._chartsMap; | |
| const scheduler = ecIns._scheduler; | |
| ecModel.eachSeries(function (seriesModel) { | |
| scheduler.updateStreamModes(seriesModel, chartsMap[seriesModel.__viewId]); | |
| }); | |
| }; | |
| doDispatchAction = function (this: ECharts, payload: Payload, silent: boolean): void { | |
| const ecModel = this.getModel(); | |
| const payloadType = payload.type; | |
| const escapeConnect = payload.escapeConnect; | |
| const actionWrap = actions[payloadType]; | |
| const actionInfo = actionWrap.actionInfo; | |
| const cptTypeTmp = (actionInfo.update || 'update').split(':'); | |
| const updateMethod = cptTypeTmp.pop(); | |
| const cptType = cptTypeTmp[0] != null && parseClassType(cptTypeTmp[0]); | |
| this[IN_MAIN_PROCESS_KEY] = true; | |
| let payloads: Payload[] = [payload]; | |
| let batched = false; | |
| // Batch action | |
| if (payload.batch) { | |
| batched = true; | |
| payloads = map<Payload['batch'][0], Payload, unknown>(payload.batch, function (item) { | |
| item = defaults(extend({}, item), payload); | |
| item.batch = null; | |
| return item as Payload; | |
| }); | |
| } | |
| const eventObjBatch: ECEventData[] = []; | |
| let eventObj: ECActionEvent; | |
| const isSelectChange = isSelectChangePayload(payload); | |
| const isHighDown = isHighDownPayload(payload); | |
| // Only leave blur once if there are multiple batches. | |
| if (isHighDown) { | |
| allLeaveBlur(this._api); | |
| } | |
| each(payloads, (batchItem) => { | |
| // Action can specify the event by return it. | |
| eventObj = actionWrap.action(batchItem, this._model, this._api) as ECActionEvent; | |
| // Emit event outside | |
| eventObj = eventObj || extend({} as ECActionEvent, batchItem); | |
| // Convert type to eventType | |
| eventObj.type = actionInfo.event || eventObj.type; | |
| eventObjBatch.push(eventObj); | |
| // light update does not perform data process, layout and visual. | |
| if (isHighDown) { | |
| const { queryOptionMap, mainTypeSpecified } = modelUtil.preParseFinder(payload as ModelFinder); | |
| const componentMainType = mainTypeSpecified ? queryOptionMap.keys()[0] : 'series'; | |
| updateDirectly(this, updateMethod, batchItem as Payload, componentMainType); | |
| markStatusToUpdate(this); | |
| } | |
| else if (isSelectChange) { | |
| // At present `dispatchAction({ type: 'select', ... })` is not supported on components. | |
| // geo still use 'geoselect'. | |
| updateDirectly(this, updateMethod, batchItem as Payload, 'series'); | |
| markStatusToUpdate(this); | |
| } | |
| else if (cptType) { | |
| updateDirectly(this, updateMethod, batchItem as Payload, cptType.main, cptType.sub); | |
| } | |
| }); | |
| if (updateMethod !== 'none' && !isHighDown && !isSelectChange && !cptType) { | |
| try { | |
| // Still dirty | |
| if (this[PENDING_UPDATE]) { | |
| prepare(this); | |
| updateMethods.update.call(this, payload); | |
| this[PENDING_UPDATE] = null; | |
| } | |
| else { | |
| updateMethods[updateMethod as keyof typeof updateMethods].call(this, payload); | |
| } | |
| } | |
| catch (e) { | |
| this[IN_MAIN_PROCESS_KEY] = false; | |
| throw e; | |
| } | |
| } | |
| // Follow the rule of action batch | |
| if (batched) { | |
| eventObj = { | |
| type: actionInfo.event || payloadType, | |
| escapeConnect: escapeConnect, | |
| batch: eventObjBatch | |
| }; | |
| } | |
| else { | |
| eventObj = eventObjBatch[0] as ECActionEvent; | |
| } | |
| this[IN_MAIN_PROCESS_KEY] = false; | |
| if (!silent) { | |
| const messageCenter = this._messageCenter; | |
| messageCenter.trigger(eventObj.type, eventObj); | |
| // Extra triggered 'selectchanged' event | |
| if (isSelectChange) { | |
| const newObj: SelectChangedPayload = { | |
| type: 'selectchanged', | |
| escapeConnect: escapeConnect, | |
| selected: getAllSelectedIndices(ecModel), | |
| isFromClick: payload.isFromClick || false, | |
| fromAction: payload.type as 'select' | 'unselect' | 'toggleSelected', | |
| fromActionPayload: payload | |
| }; | |
| messageCenter.trigger(newObj.type, newObj); | |
| } | |
| } | |
| }; | |
| flushPendingActions = function (this: ECharts, silent: boolean): void { | |
| const pendingActions = this._pendingActions; | |
| while (pendingActions.length) { | |
| const payload = pendingActions.shift(); | |
| doDispatchAction.call(this, payload, silent); | |
| } | |
| }; | |
| triggerUpdatedEvent = function (this: ECharts, silent): void { | |
| !silent && this.trigger('updated'); | |
| }; | |
| /** | |
| * Event `rendered` is triggered when zr | |
| * rendered. It is useful for realtime | |
| * snapshot (reflect animation). | |
| * | |
| * Event `finished` is triggered when: | |
| * (1) zrender rendering finished. | |
| * (2) initial animation finished. | |
| * (3) progressive rendering finished. | |
| * (4) no pending action. | |
| * (5) no delayed setOption needs to be processed. | |
| */ | |
| bindRenderedEvent = function (zr: zrender.ZRenderType, ecIns: ECharts): void { | |
| zr.on('rendered', function (params: RenderedEventParam) { | |
| ecIns.trigger('rendered', params); | |
| // The `finished` event should not be triggered repeatedly, | |
| // so it should only be triggered when rendering indeed happens | |
| // in zrender. (Consider the case that dipatchAction is keep | |
| // triggering when mouse move). | |
| if ( | |
| // Although zr is dirty if initial animation is not finished | |
| // and this checking is called on frame, we also check | |
| // animation finished for robustness. | |
| zr.animation.isFinished() | |
| && !ecIns[PENDING_UPDATE] | |
| && !ecIns._scheduler.unfinished | |
| && !ecIns._pendingActions.length | |
| ) { | |
| ecIns.trigger('finished'); | |
| } | |
| }); | |
| }; | |
| bindMouseEvent = function (zr: zrender.ZRenderType, ecIns: ECharts): void { | |
| zr.on('mouseover', function (e) { | |
| const el = e.target; | |
| const dispatcher = findEventDispatcher(el, isHighDownDispatcher); | |
| if (dispatcher) { | |
| handleGlobalMouseOverForHighDown(dispatcher, e, ecIns._api); | |
| markStatusToUpdate(ecIns); | |
| } | |
| }).on('mouseout', function (e) { | |
| const el = e.target; | |
| const dispatcher = findEventDispatcher(el, isHighDownDispatcher); | |
| if (dispatcher) { | |
| handleGlobalMouseOutForHighDown(dispatcher, e, ecIns._api); | |
| markStatusToUpdate(ecIns); | |
| } | |
| }).on('click', function (e) { | |
| const el = e.target; | |
| const dispatcher = findEventDispatcher( | |
| el, (target) => getECData(target).dataIndex != null, true | |
| ); | |
| if (dispatcher) { | |
| const actionType = (dispatcher as ECElement).selected ? 'unselect' : 'select'; | |
| const ecData = getECData(dispatcher); | |
| ecIns._api.dispatchAction({ | |
| type: actionType, | |
| dataType: ecData.dataType, | |
| dataIndexInside: ecData.dataIndex, | |
| seriesIndex: ecData.seriesIndex, | |
| isFromClick: true | |
| }); | |
| } | |
| }); | |
| }; | |
| function clearColorPalette(ecModel: GlobalModel): void { | |
| ecModel.clearColorPalette(); | |
| ecModel.eachSeries(function (seriesModel) { | |
| seriesModel.clearColorPalette(); | |
| }); | |
| }; | |
| // Allocate zlevels for series and components | |
| function allocateZlevels(ecModel: GlobalModel) { | |
| interface ZLevelItem { | |
| z: number, | |
| zlevel: number, | |
| idx: number, | |
| type: string, | |
| key: string | |
| }; | |
| const componentZLevels: ZLevelItem[] = []; | |
| const seriesZLevels: ZLevelItem[] = []; | |
| let hasSeperateZLevel = false; | |
| ecModel.eachComponent(function (componentType, componentModel) { | |
| const zlevel = componentModel.get('zlevel') || 0; | |
| const z = componentModel.get('z') || 0; | |
| const zlevelKey = componentModel.getZLevelKey(); | |
| hasSeperateZLevel = hasSeperateZLevel || !!zlevelKey; | |
| (componentType === 'series' ? seriesZLevels : componentZLevels).push({ | |
| zlevel, | |
| z, | |
| idx: componentModel.componentIndex, | |
| type: componentType, | |
| key: zlevelKey | |
| }); | |
| }); | |
| if (hasSeperateZLevel) { | |
| // Series after component | |
| const zLevels: ZLevelItem[] = componentZLevels.concat(seriesZLevels); | |
| let lastSeriesZLevel: number; | |
| let lastSeriesKey: string; | |
| timsort(zLevels, (a, b) => { | |
| if (a.zlevel === b.zlevel) { | |
| return a.z - b.z; | |
| } | |
| return a.zlevel - b.zlevel; | |
| }); | |
| each(zLevels, item => { | |
| const componentModel = ecModel.getComponent(item.type, item.idx); | |
| let zlevel = item.zlevel; | |
| const key = item.key; | |
| if (lastSeriesZLevel != null) { | |
| zlevel = Math.max(lastSeriesZLevel, zlevel); | |
| } | |
| if (key) { | |
| if (zlevel === lastSeriesZLevel && key !== lastSeriesKey) { | |
| zlevel++; | |
| } | |
| lastSeriesKey = key; | |
| } | |
| else if (lastSeriesKey) { | |
| if (zlevel === lastSeriesZLevel) { | |
| zlevel++; | |
| } | |
| lastSeriesKey = ''; | |
| } | |
| lastSeriesZLevel = zlevel; | |
| componentModel.setZLevel(zlevel); | |
| }); | |
| } | |
| } | |
| render = ( | |
| ecIns: ECharts, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload, | |
| updateParams: UpdateLifecycleParams | |
| ) => { | |
| allocateZlevels(ecModel); | |
| renderComponents(ecIns, ecModel, api, payload, updateParams); | |
| each(ecIns._chartsViews, function (chart: ChartView) { | |
| chart.__alive = false; | |
| }); | |
| renderSeries(ecIns, ecModel, api, payload, updateParams); | |
| // Remove groups of unrendered charts | |
| each(ecIns._chartsViews, function (chart: ChartView) { | |
| if (!chart.__alive) { | |
| chart.remove(ecModel, api); | |
| } | |
| }); | |
| }; | |
| renderComponents = ( | |
| ecIns: ECharts, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload, | |
| updateParams: UpdateLifecycleParams, dirtyList?: ComponentView[] | |
| ) => { | |
| each(dirtyList || ecIns._componentsViews, function (componentView: ComponentView) { | |
| const componentModel = componentView.__model; | |
| clearStates(componentModel, componentView); | |
| componentView.render(componentModel, ecModel, api, payload); | |
| updateZ(componentModel, componentView); | |
| updateStates(componentModel, componentView); | |
| }); | |
| }; | |
| /** | |
| * Render each chart and component | |
| */ | |
| renderSeries = ( | |
| ecIns: ECharts, | |
| ecModel: GlobalModel, | |
| api: ExtensionAPI, | |
| payload: Payload | 'remain', | |
| updateParams: UpdateLifecycleParams, | |
| dirtyMap?: {[uid: string]: any} | |
| ) => { | |
| // Render all charts | |
| const scheduler = ecIns._scheduler; | |
| updateParams = extend(updateParams || {}, { | |
| updatedSeries: ecModel.getSeries() | |
| }); | |
| // TODO progressive? | |
| lifecycle.trigger('series:beforeupdate', ecModel, api, updateParams); | |
| let unfinished: boolean = false; | |
| ecModel.eachSeries(function (seriesModel) { | |
| const chartView = ecIns._chartsMap[seriesModel.__viewId]; | |
| chartView.__alive = true; | |
| const renderTask = chartView.renderTask; | |
| scheduler.updatePayload(renderTask, payload); | |
| // TODO states on marker. | |
| clearStates(seriesModel, chartView); | |
| if (dirtyMap && dirtyMap.get(seriesModel.uid)) { | |
| renderTask.dirty(); | |
| } | |
| if (renderTask.perform(scheduler.getPerformArgs(renderTask))) { | |
| unfinished = true; | |
| } | |
| chartView.group.silent = !!seriesModel.get('silent'); | |
| // Should not call markRedraw on group, because it will disable zrender | |
| // incremental render (always render from the __startIndex each frame) | |
| // chartView.group.markRedraw(); | |
| updateBlend(seriesModel, chartView); | |
| updateSeriesElementSelection(seriesModel); | |
| }); | |
| scheduler.unfinished = unfinished || scheduler.unfinished; | |
| lifecycle.trigger('series:layoutlabels', ecModel, api, updateParams); | |
| // transition after label is layouted. | |
| lifecycle.trigger('series:transition', ecModel, api, updateParams); | |
| ecModel.eachSeries(function (seriesModel) { | |
| const chartView = ecIns._chartsMap[seriesModel.__viewId]; | |
| // Update Z after labels updated. Before applying states. | |
| updateZ(seriesModel, chartView); | |
| // NOTE: Update states after label is updated. | |
| // label should be in normal status when layouting. | |
| updateStates(seriesModel, chartView); | |
| }); | |
| // If use hover layer | |
| updateHoverLayerStatus(ecIns, ecModel); | |
| lifecycle.trigger('series:afterupdate', ecModel, api, updateParams); | |
| }; | |
| markStatusToUpdate = function (ecIns: ECharts): void { | |
| ecIns[STATUS_NEEDS_UPDATE_KEY] = true; | |
| // Wake up zrender if it's sleep. Let it update states in the next frame. | |
| ecIns.getZr().wakeUp(); | |
| }; | |
| applyChangedStates = function (ecIns: ECharts): void { | |
| if (!ecIns[STATUS_NEEDS_UPDATE_KEY]) { | |
| return; | |
| } | |
| ecIns.getZr().storage.traverse(function (el: ECElement) { | |
| // Not applied on removed elements, it may still in fading. | |
| if (graphic.isElementRemoved(el)) { | |
| return; | |
| } | |
| applyElementStates(el); | |
| }); | |
| ecIns[STATUS_NEEDS_UPDATE_KEY] = false; | |
| }; | |
| function applyElementStates(el: ECElement) { | |
| const newStates = []; | |
| const oldStates = el.currentStates; | |
| // Keep other states. | |
| for (let i = 0; i < oldStates.length; i++) { | |
| const stateName = oldStates[i]; | |
| if (!(stateName === 'emphasis' || stateName === 'blur' || stateName === 'select')) { | |
| newStates.push(stateName); | |
| } | |
| } | |
| // Only use states when it's exists. | |
| if (el.selected && el.states.select) { | |
| newStates.push('select'); | |
| } | |
| if (el.hoverState === HOVER_STATE_EMPHASIS && el.states.emphasis) { | |
| newStates.push('emphasis'); | |
| } | |
| else if (el.hoverState === HOVER_STATE_BLUR && el.states.blur) { | |
| newStates.push('blur'); | |
| } | |
| el.useStates(newStates); | |
| } | |
| function updateHoverLayerStatus(ecIns: ECharts, ecModel: GlobalModel): void { | |
| const zr = ecIns._zr; | |
| const storage = zr.storage; | |
| let elCount = 0; | |
| storage.traverse(function (el) { | |
| if (!el.isGroup) { | |
| elCount++; | |
| } | |
| }); | |
| if (elCount > ecModel.get('hoverLayerThreshold') && !env.node && !env.worker) { | |
| ecModel.eachSeries(function (seriesModel) { | |
| if (seriesModel.preventUsingHoverLayer) { | |
| return; | |
| } | |
| const chartView = ecIns._chartsMap[seriesModel.__viewId]; | |
| if (chartView.__alive) { | |
| chartView.eachRendered((el: ECElement) => { | |
| if (el.states.emphasis) { | |
| el.states.emphasis.hoverLayer = true; | |
| } | |
| }); | |
| } | |
| }); | |
| } | |
| }; | |
| /** | |
| * Update chart and blend. | |
| */ | |
| function updateBlend(seriesModel: SeriesModel, chartView: ChartView): void { | |
| const blendMode = seriesModel.get('blendMode') || null; | |
| chartView.eachRendered((el: Displayable) => { | |
| // FIXME marker and other components | |
| if (!el.isGroup) { | |
| // DON'T mark the element dirty. In case element is incremental and don't want to rerender. | |
| el.style.blend = blendMode; | |
| } | |
| }); | |
| }; | |
| function updateZ(model: ComponentModel, view: ComponentView | ChartView): void { | |
| if (model.preventAutoZ) { | |
| return; | |
| } | |
| const z = model.get('z') || 0; | |
| const zlevel = model.get('zlevel') || 0; | |
| // Set z and zlevel | |
| view.eachRendered((el) => { | |
| doUpdateZ(el, z, zlevel, -Infinity); | |
| // Don't traverse the children because it has been traversed in _updateZ. | |
| return true; | |
| }); | |
| }; | |
| function doUpdateZ(el: Element, z: number, zlevel: number, maxZ2: number): number { | |
| // Group may also have textContent | |
| const label = el.getTextContent(); | |
| const labelLine = el.getTextGuideLine(); | |
| const isGroup = el.isGroup; | |
| if (isGroup) { | |
| // set z & zlevel of children elements of Group | |
| const children = (el as graphic.Group).childrenRef(); | |
| for (let i = 0; i < children.length; i++) { | |
| maxZ2 = Math.max(doUpdateZ(children[i], z, zlevel, maxZ2), maxZ2); | |
| } | |
| } | |
| else { | |
| // not Group | |
| (el as Displayable).z = z; | |
| (el as Displayable).zlevel = zlevel; | |
| maxZ2 = Math.max((el as Displayable).z2, maxZ2); | |
| } | |
| // always set z and zlevel if label/labelLine exists | |
| if (label) { | |
| label.z = z; | |
| label.zlevel = zlevel; | |
| // lift z2 of text content | |
| // TODO if el.emphasis.z2 is spcefied, what about textContent. | |
| isFinite(maxZ2) && (label.z2 = maxZ2 + 2); | |
| } | |
| if (labelLine) { | |
| const textGuideLineConfig = el.textGuideLineConfig; | |
| labelLine.z = z; | |
| labelLine.zlevel = zlevel; | |
| isFinite(maxZ2) | |
| && (labelLine.z2 = maxZ2 + (textGuideLineConfig && textGuideLineConfig.showAbove ? 1 : -1)); | |
| } | |
| return maxZ2; | |
| } | |
| // Clear states without animation. | |
| // TODO States on component. | |
| function clearStates(model: ComponentModel, view: ComponentView | ChartView): void { | |
| view.eachRendered(function (el: Displayable) { | |
| // Not applied on removed elements, it may still in fading. | |
| if (graphic.isElementRemoved(el)) { | |
| return; | |
| } | |
| const textContent = el.getTextContent(); | |
| const textGuide = el.getTextGuideLine(); | |
| if (el.stateTransition) { | |
| el.stateTransition = null; | |
| } | |
| if (textContent && textContent.stateTransition) { | |
| textContent.stateTransition = null; | |
| } | |
| if (textGuide && textGuide.stateTransition) { | |
| textGuide.stateTransition = null; | |
| } | |
| // TODO If el is incremental. | |
| if (el.hasState()) { | |
| el.prevStates = el.currentStates; | |
| el.clearStates(); | |
| } | |
| else if (el.prevStates) { | |
| el.prevStates = null; | |
| } | |
| }); | |
| } | |
| function updateStates(model: ComponentModel, view: ComponentView | ChartView): void { | |
| const stateAnimationModel = (model as SeriesModel).getModel('stateAnimation'); | |
| const enableAnimation = model.isAnimationEnabled(); | |
| const duration = stateAnimationModel.get('duration'); | |
| const stateTransition = duration > 0 ? { | |
| duration, | |
| delay: stateAnimationModel.get('delay'), | |
| easing: stateAnimationModel.get('easing') | |
| // additive: stateAnimationModel.get('additive') | |
| } : null; | |
| view.eachRendered(function (el: Displayable) { | |
| if (el.states && el.states.emphasis) { | |
| // Not applied on removed elements, it may still in fading. | |
| if (graphic.isElementRemoved(el)) { | |
| return; | |
| } | |
| if (el instanceof graphic.Path) { | |
| savePathStates(el); | |
| } | |
| // Only updated on changed element. In case element is incremental and don't want to rerender. | |
| // TODO, a more proper way? | |
| if (el.__dirty) { | |
| const prevStates = el.prevStates; | |
| // Restore states without animation | |
| if (prevStates) { | |
| el.useStates(prevStates); | |
| } | |
| } | |
| // Update state transition and enable animation again. | |
| if (enableAnimation) { | |
| el.stateTransition = stateTransition; | |
| const textContent = el.getTextContent(); | |
| const textGuide = el.getTextGuideLine(); | |
| // TODO Is it necessary to animate label? | |
| if (textContent) { | |
| textContent.stateTransition = stateTransition; | |
| } | |
| if (textGuide) { | |
| textGuide.stateTransition = stateTransition; | |
| } | |
| } | |
| // Use highlighted and selected flag to toggle states. | |
| if (el.__dirty) { | |
| applyElementStates(el); | |
| } | |
| } | |
| }); | |
| }; | |
| createExtensionAPI = function (ecIns: ECharts): ExtensionAPI { | |
| return new (class extends ExtensionAPI { | |
| getCoordinateSystems(): CoordinateSystemMaster[] { | |
| return ecIns._coordSysMgr.getCoordinateSystems(); | |
| } | |
| getComponentByElement(el: Element) { | |
| while (el) { | |
| const modelInfo = (el as ViewRootGroup).__ecComponentInfo; | |
| if (modelInfo != null) { | |
| return ecIns._model.getComponent(modelInfo.mainType, modelInfo.index); | |
| } | |
| el = el.parent; | |
| } | |
| } | |
| enterEmphasis(el: Element, highlightDigit?: number) { | |
| enterEmphasis(el, highlightDigit); | |
| markStatusToUpdate(ecIns); | |
| } | |
| leaveEmphasis(el: Element, highlightDigit?: number) { | |
| leaveEmphasis(el, highlightDigit); | |
| markStatusToUpdate(ecIns); | |
| } | |
| enterBlur(el: Element) { | |
| enterBlur(el); | |
| markStatusToUpdate(ecIns); | |
| } | |
| leaveBlur(el: Element) { | |
| leaveBlur(el); | |
| markStatusToUpdate(ecIns); | |
| } | |
| enterSelect(el: Element) { | |
| enterSelect(el); | |
| markStatusToUpdate(ecIns); | |
| } | |
| leaveSelect(el: Element) { | |
| leaveSelect(el); | |
| markStatusToUpdate(ecIns); | |
| } | |
| getModel(): GlobalModel { | |
| return ecIns.getModel(); | |
| } | |
| getViewOfComponentModel(componentModel: ComponentModel): ComponentView { | |
| return ecIns.getViewOfComponentModel(componentModel); | |
| } | |
| getViewOfSeriesModel(seriesModel: SeriesModel): ChartView { | |
| return ecIns.getViewOfSeriesModel(seriesModel); | |
| } | |
| })(ecIns); | |
| }; | |
| enableConnect = function (chart: ECharts): void { | |
| function updateConnectedChartsStatus(charts: ECharts[], status: ConnectStatus) { | |
| for (let i = 0; i < charts.length; i++) { | |
| const otherChart = charts[i]; | |
| otherChart[CONNECT_STATUS_KEY] = status; | |
| } | |
| } | |
| each(eventActionMap, function (actionType, eventType) { | |
| chart._messageCenter.on(eventType, function (event: ECActionEvent) { | |
| if (connectedGroups[chart.group] && chart[CONNECT_STATUS_KEY] !== CONNECT_STATUS_PENDING) { | |
| if (event && event.escapeConnect) { | |
| return; | |
| } | |
| const action = chart.makeActionFromEvent(event); | |
| const otherCharts: ECharts[] = []; | |
| each(instances, function (otherChart) { | |
| if (otherChart !== chart && otherChart.group === chart.group) { | |
| otherCharts.push(otherChart); | |
| } | |
| }); | |
| updateConnectedChartsStatus(otherCharts, CONNECT_STATUS_PENDING); | |
| each(otherCharts, function (otherChart) { | |
| if (otherChart[CONNECT_STATUS_KEY] !== CONNECT_STATUS_UPDATING) { | |
| otherChart.dispatchAction(action); | |
| } | |
| }); | |
| updateConnectedChartsStatus(otherCharts, CONNECT_STATUS_UPDATED); | |
| } | |
| }); | |
| }); | |
| }; | |
| })(); | |
| } | |
| const echartsProto = ECharts.prototype; | |
| echartsProto.on = createRegisterEventWithLowercaseECharts('on'); | |
| echartsProto.off = createRegisterEventWithLowercaseECharts('off'); | |
| /** | |
| * @deprecated | |
| */ | |
| // @ts-ignore | |
| echartsProto.one = function (eventName: string, cb: Function, ctx?: any) { | |
| const self = this; | |
| deprecateLog('ECharts#one is deprecated.'); | |
| function wrapped(this: unknown, ...args2: any) { | |
| cb && cb.apply && cb.apply(this, args2); | |
| // @ts-ignore | |
| self.off(eventName, wrapped); | |
| }; | |
| // @ts-ignore | |
| this.on.call(this, eventName, wrapped, ctx); | |
| }; | |
| const MOUSE_EVENT_NAMES: ZRElementEventName[] = [ | |
| 'click', 'dblclick', 'mouseover', 'mouseout', 'mousemove', | |
| 'mousedown', 'mouseup', 'globalout', 'contextmenu' | |
| ]; | |
| function disposedWarning(id: string): void { | |
| if (__DEV__) { | |
| console.warn('Instance ' + id + ' has been disposed'); | |
| } | |
| } | |
| const actions: { | |
| [actionType: string]: { | |
| action: ActionHandler, | |
| actionInfo: ActionInfo | |
| } | |
| } = {}; | |
| /** | |
| * Map eventType to actionType | |
| */ | |
| const eventActionMap: {[eventType: string]: string} = {}; | |
| const dataProcessorFuncs: StageHandlerInternal[] = []; | |
| const optionPreprocessorFuncs: OptionPreprocessor[] = []; | |
| const visualFuncs: StageHandlerInternal[] = []; | |
| const themeStorage: {[themeName: string]: ThemeOption} = {}; | |
| const loadingEffects: {[effectName: string]: LoadingEffectCreator} = {}; | |
| const instances: {[id: string]: ECharts} = {}; | |
| const connectedGroups: {[groupId: string]: boolean} = {}; | |
| let idBase: number = +(new Date()) - 0; | |
| let groupIdBase: number = +(new Date()) - 0; | |
| const DOM_ATTRIBUTE_KEY = '_echarts_instance_'; | |
| /** | |
| * @param opts.devicePixelRatio Use window.devicePixelRatio by default | |
| * @param opts.renderer Can choose 'canvas' or 'svg' to render the chart. | |
| * @param opts.width Use clientWidth of the input `dom` by default. | |
| * Can be 'auto' (the same as null/undefined) | |
| * @param opts.height Use clientHeight of the input `dom` by default. | |
| * Can be 'auto' (the same as null/undefined) | |
| * @param opts.locale Specify the locale. | |
| * @param opts.useDirtyRect Enable dirty rectangle rendering or not. | |
| */ | |
| export function init( | |
| dom: HTMLElement, | |
| theme?: string | object, | |
| opts?: EChartsInitOpts | |
| ): EChartsType { | |
| const isClient = !(opts && opts.ssr); | |
| if (isClient) { | |
| if (__DEV__) { | |
| if (!dom) { | |
| throw new Error('Initialize failed: invalid dom.'); | |
| } | |
| } | |
| const existInstance = getInstanceByDom(dom); | |
| if (existInstance) { | |
| if (__DEV__) { | |
| console.warn('There is a chart instance already initialized on the dom.'); | |
| } | |
| return existInstance; | |
| } | |
| if (__DEV__) { | |
| if (isDom(dom) | |
| && dom.nodeName.toUpperCase() !== 'CANVAS' | |
| && ( | |
| (!dom.clientWidth && (!opts || opts.width == null)) | |
| || (!dom.clientHeight && (!opts || opts.height == null)) | |
| ) | |
| ) { | |
| console.warn('Can\'t get DOM width or height. Please check ' | |
| + 'dom.clientWidth and dom.clientHeight. They should not be 0.' | |
| + 'For example, you may need to call this in the callback ' | |
| + 'of window.onload.'); | |
| } | |
| } | |
| } | |
| const chart = new ECharts(dom, theme, opts); | |
| chart.id = 'ec_' + idBase++; | |
| instances[chart.id] = chart; | |
| isClient && modelUtil.setAttribute(dom, DOM_ATTRIBUTE_KEY, chart.id); | |
| enableConnect(chart); | |
| lifecycle.trigger('afterinit', chart); | |
| return chart; | |
| } | |
| /** | |
| * @usage | |
| * (A) | |
| * ```js | |
| * let chart1 = echarts.init(dom1); | |
| * let chart2 = echarts.init(dom2); | |
| * chart1.group = 'xxx'; | |
| * chart2.group = 'xxx'; | |
| * echarts.connect('xxx'); | |
| * ``` | |
| * (B) | |
| * ```js | |
| * let chart1 = echarts.init(dom1); | |
| * let chart2 = echarts.init(dom2); | |
| * echarts.connect('xxx', [chart1, chart2]); | |
| * ``` | |
| */ | |
| export function connect(groupId: string | EChartsType[]): string { | |
| // Is array of charts | |
| if (isArray(groupId)) { | |
| const charts = groupId; | |
| groupId = null; | |
| // If any chart has group | |
| each(charts, function (chart) { | |
| if (chart.group != null) { | |
| groupId = chart.group; | |
| } | |
| }); | |
| groupId = groupId || ('g_' + groupIdBase++); | |
| each(charts, function (chart) { | |
| chart.group = groupId as string; | |
| }); | |
| } | |
| connectedGroups[groupId as string] = true; | |
| return groupId as string; | |
| } | |
| /** | |
| * @deprecated | |
| */ | |
| export function disConnect(groupId: string): void { | |
| connectedGroups[groupId] = false; | |
| } | |
| /** | |
| * Alias and backward compatibility | |
| */ | |
| export const disconnect = disConnect; | |
| /** | |
| * Dispose a chart instance | |
| */ | |
| export function dispose(chart: EChartsType | HTMLElement | string): void { | |
| if (isString(chart)) { | |
| chart = instances[chart]; | |
| } | |
| else if (!(chart instanceof ECharts)) { | |
| // Try to treat as dom | |
| chart = getInstanceByDom(chart); | |
| } | |
| if ((chart instanceof ECharts) && !chart.isDisposed()) { | |
| chart.dispose(); | |
| } | |
| } | |
| export function getInstanceByDom(dom: HTMLElement): EChartsType | undefined { | |
| return instances[modelUtil.getAttribute(dom, DOM_ATTRIBUTE_KEY)]; | |
| } | |
| export function getInstanceById(key: string): EChartsType | undefined { | |
| return instances[key]; | |
| } | |
| /** | |
| * Register theme | |
| */ | |
| export function registerTheme(name: string, theme: ThemeOption): void { | |
| themeStorage[name] = theme; | |
| } | |
| /** | |
| * Register option preprocessor | |
| */ | |
| export function registerPreprocessor(preprocessorFunc: OptionPreprocessor): void { | |
| if (indexOf(optionPreprocessorFuncs, preprocessorFunc) < 0) { | |
| optionPreprocessorFuncs.push(preprocessorFunc); | |
| } | |
| } | |
| export function registerProcessor( | |
| priority: number | StageHandler | StageHandlerOverallReset, | |
| processor?: StageHandler | StageHandlerOverallReset | |
| ): void { | |
| normalizeRegister(dataProcessorFuncs, priority, processor, PRIORITY_PROCESSOR_DEFAULT); | |
| } | |
| /** | |
| * Register postIniter | |
| * @param {Function} postInitFunc | |
| */ | |
| export function registerPostInit(postInitFunc: PostIniter): void { | |
| registerUpdateLifecycle('afterinit', postInitFunc); | |
| } | |
| /** | |
| * Register postUpdater | |
| * @param {Function} postUpdateFunc | |
| */ | |
| export function registerPostUpdate(postUpdateFunc: PostUpdater): void { | |
| registerUpdateLifecycle('afterupdate', postUpdateFunc); | |
| } | |
| export function registerUpdateLifecycle<T extends keyof LifecycleEvents>( | |
| name: T, cb: (...args: LifecycleEvents[T]) => void | |
| ): void { | |
| (lifecycle as any).on(name, cb); | |
| } | |
| /** | |
| * @usage | |
| * registerAction('someAction', 'someEvent', function () { ... }); | |
| * registerAction('someAction', function () { ... }); | |
| * registerAction( | |
| * {type: 'someAction', event: 'someEvent', update: 'updateView'}, | |
| * function () { ... } | |
| * ); | |
| * | |
| * @param {(string|Object)} actionInfo | |
| * @param {string} actionInfo.type | |
| * @param {string} [actionInfo.event] | |
| * @param {string} [actionInfo.update] | |
| * @param {string} [eventName] | |
| * @param {Function} action | |
| */ | |
| export function registerAction(type: string, eventName: string, action: ActionHandler): void; | |
| export function registerAction(type: string, action: ActionHandler): void; | |
| export function registerAction(actionInfo: ActionInfo, action: ActionHandler): void; | |
| export function registerAction( | |
| actionInfo: string | ActionInfo, | |
| eventName: string | ActionHandler, | |
| action?: ActionHandler | |
| ): void { | |
| if (isFunction(eventName)) { | |
| action = eventName; | |
| eventName = ''; | |
| } | |
| const actionType = isObject(actionInfo) | |
| ? (actionInfo as ActionInfo).type | |
| : ([actionInfo, actionInfo = { | |
| event: eventName | |
| } as ActionInfo][0]); | |
| // Event name is all lowercase | |
| (actionInfo as ActionInfo).event = ( | |
| (actionInfo as ActionInfo).event || actionType as string | |
| ).toLowerCase(); | |
| eventName = (actionInfo as ActionInfo).event; | |
| if (eventActionMap[eventName as string]) { | |
| // Already registered. | |
| return; | |
| } | |
| // Validate action type and event name. | |
| assert(ACTION_REG.test(actionType as string) && ACTION_REG.test(eventName)); | |
| if (!actions[actionType as string]) { | |
| actions[actionType as string] = {action: action, actionInfo: actionInfo as ActionInfo}; | |
| } | |
| eventActionMap[eventName as string] = actionType as string; | |
| } | |
| export function registerCoordinateSystem( | |
| type: string, | |
| coordSysCreator: CoordinateSystemCreator | |
| ): void { | |
| CoordinateSystemManager.register(type, coordSysCreator); | |
| } | |
| /** | |
| * Get dimensions of specified coordinate system. | |
| * @param {string} type | |
| * @return {Array.<string|Object>} | |
| */ | |
| export function getCoordinateSystemDimensions(type: string): DimensionDefinitionLoose[] { | |
| const coordSysCreator = CoordinateSystemManager.get(type); | |
| if (coordSysCreator) { | |
| return coordSysCreator.getDimensionsInfo | |
| ? coordSysCreator.getDimensionsInfo() | |
| : coordSysCreator.dimensions.slice(); | |
| } | |
| } | |
| export {registerLocale} from './locale'; | |
| /** | |
| * Layout is a special stage of visual encoding | |
| * Most visual encoding like color are common for different chart | |
| * But each chart has it's own layout algorithm | |
| */ | |
| function registerLayout(priority: number, layoutTask: StageHandler | StageHandlerOverallReset): void; | |
| function registerLayout(layoutTask: StageHandler | StageHandlerOverallReset): void; | |
| function registerLayout( | |
| priority: number | StageHandler | StageHandlerOverallReset, | |
| layoutTask?: StageHandler | StageHandlerOverallReset | |
| ): void { | |
| normalizeRegister(visualFuncs, priority, layoutTask, PRIORITY_VISUAL_LAYOUT, 'layout'); | |
| } | |
| function registerVisual(priority: number, layoutTask: StageHandler | StageHandlerOverallReset): void; | |
| function registerVisual(layoutTask: StageHandler | StageHandlerOverallReset): void; | |
| function registerVisual( | |
| priority: number | StageHandler | StageHandlerOverallReset, | |
| visualTask?: StageHandler | StageHandlerOverallReset | |
| ): void { | |
| normalizeRegister(visualFuncs, priority, visualTask, PRIORITY_VISUAL_CHART, 'visual'); | |
| } | |
| export {registerLayout, registerVisual}; | |
| const registeredTasks: (StageHandler | StageHandlerOverallReset)[] = []; | |
| function normalizeRegister( | |
| targetList: StageHandler[], | |
| priority: number | StageHandler | StageHandlerOverallReset, | |
| fn: StageHandler | StageHandlerOverallReset, | |
| defaultPriority: number, | |
| visualType?: StageHandlerInternal['visualType'] | |
| ): void { | |
| if (isFunction(priority) || isObject(priority)) { | |
| fn = priority as (StageHandler | StageHandlerOverallReset); | |
| priority = defaultPriority; | |
| } | |
| if (__DEV__) { | |
| if (isNaN(priority) || priority == null) { | |
| throw new Error('Illegal priority'); | |
| } | |
| // Check duplicate | |
| each(targetList, function (wrap) { | |
| assert((wrap as StageHandlerInternal).__raw !== fn); | |
| }); | |
| } | |
| // Already registered | |
| if (indexOf(registeredTasks, fn) >= 0) { | |
| return; | |
| } | |
| registeredTasks.push(fn); | |
| const stageHandler = Scheduler.wrapStageHandler(fn, visualType); | |
| stageHandler.__prio = priority; | |
| stageHandler.__raw = fn; | |
| targetList.push(stageHandler); | |
| } | |
| export function registerLoading( | |
| name: string, | |
| loadingFx: LoadingEffectCreator | |
| ): void { | |
| loadingEffects[name] = loadingFx; | |
| } | |
| /** | |
| * ZRender need a canvas context to do measureText. | |
| * But in node environment canvas may be created by node-canvas. | |
| * So we need to specify how to create a canvas instead of using document.createElement('canvas') | |
| * | |
| * | |
| * @deprecated use setPlatformAPI({ createCanvas }) instead. | |
| * | |
| * @example | |
| * let Canvas = require('canvas'); | |
| * let echarts = require('echarts'); | |
| * echarts.setCanvasCreator(function () { | |
| * // Small size is enough. | |
| * return new Canvas(32, 32); | |
| * }); | |
| */ | |
| export function setCanvasCreator(creator: () => HTMLCanvasElement): void { | |
| if (__DEV__) { | |
| deprecateLog('setCanvasCreator is deprecated. Use setPlatformAPI({ createCanvas }) instead.'); | |
| } | |
| setPlatformAPI({ | |
| createCanvas: creator | |
| }); | |
| } | |
| type RegisterMapParams = Parameters<typeof geoSourceManager.registerMap>; | |
| /** | |
| * The parameters and usage: see `geoSourceManager.registerMap`. | |
| * Compatible with previous `echarts.registerMap`. | |
| */ | |
| export function registerMap( | |
| mapName: RegisterMapParams[0], | |
| geoJson: RegisterMapParams[1], | |
| specialAreas?: RegisterMapParams[2] | |
| ): void { | |
| const registerMap = getImpl('registerMap'); | |
| registerMap && registerMap(mapName, geoJson, specialAreas); | |
| } | |
| export function getMap(mapName: string) { | |
| const getMap = getImpl('getMap'); | |
| return getMap && getMap(mapName); | |
| } | |
| export const registerTransform = registerExternalTransform; | |
| /** | |
| * Globa dispatchAction to a specified chart instance. | |
| */ | |
| // export function dispatchAction(payload: { chartId: string } & Payload, opt?: Parameters<ECharts['dispatchAction']>[1]) { | |
| // if (!payload || !payload.chartId) { | |
| // // Must have chartId to find chart | |
| // return; | |
| // } | |
| // const chart = instances[payload.chartId]; | |
| // if (chart) { | |
| // chart.dispatchAction(payload, opt); | |
| // } | |
| // } | |
| // Builtin global visual | |
| registerVisual(PRIORITY_VISUAL_GLOBAL, seriesStyleTask); | |
| registerVisual(PRIORITY_VISUAL_CHART_DATA_CUSTOM, dataStyleTask); | |
| registerVisual(PRIORITY_VISUAL_CHART_DATA_CUSTOM, dataColorPaletteTask); | |
| registerVisual(PRIORITY_VISUAL_GLOBAL, seriesSymbolTask); | |
| registerVisual(PRIORITY_VISUAL_CHART_DATA_CUSTOM, dataSymbolTask); | |
| registerVisual(PRIORITY_VISUAL_DECAL, decal); | |
| registerPreprocessor(backwardCompat); | |
| registerProcessor(PRIORITY_PROCESSOR_DATASTACK, dataStack); | |
| registerLoading('default', loadingDefault); | |
| // Default actions | |
| registerAction({ | |
| type: HIGHLIGHT_ACTION_TYPE, | |
| event: HIGHLIGHT_ACTION_TYPE, | |
| update: HIGHLIGHT_ACTION_TYPE | |
| }, noop); | |
| registerAction({ | |
| type: DOWNPLAY_ACTION_TYPE, | |
| event: DOWNPLAY_ACTION_TYPE, | |
| update: DOWNPLAY_ACTION_TYPE | |
| }, noop); | |
| registerAction({ | |
| type: SELECT_ACTION_TYPE, | |
| event: SELECT_ACTION_TYPE, | |
| update: SELECT_ACTION_TYPE | |
| }, noop); | |
| registerAction({ | |
| type: UNSELECT_ACTION_TYPE, | |
| event: UNSELECT_ACTION_TYPE, | |
| update: UNSELECT_ACTION_TYPE | |
| }, noop); | |
| registerAction({ | |
| type: TOGGLE_SELECT_ACTION_TYPE, | |
| event: TOGGLE_SELECT_ACTION_TYPE, | |
| update: TOGGLE_SELECT_ACTION_TYPE | |
| }, noop); | |
| // Default theme | |
| registerTheme('light', lightTheme); | |
| registerTheme('dark', darkTheme); | |
| // For backward compatibility, where the namespace `dataTool` will | |
| // be mounted on `echarts` is the extension `dataTool` is imported. | |
| export const dataTool = {}; | |
| export interface EChartsType extends ECharts {} |