/* * 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 {each} from 'zrender/src/core/util'; import Group from 'zrender/src/graphic/Group'; import * as componentUtil from '../util/component'; import * as clazzUtil from '../util/clazz'; import * as modelUtil from '../util/model'; import { enterEmphasis, leaveEmphasis, getHighlightDigit, isHighDownDispatcher } from '../util/states'; import {createTask, TaskResetCallbackReturn} from '../core/task'; import createRenderPlanner from '../chart/helper/createRenderPlanner'; import SeriesModel from '../model/Series'; import GlobalModel from '../model/Global'; import ExtensionAPI from '../core/ExtensionAPI'; import Element from 'zrender/src/Element'; import { Payload, ViewRootGroup, ECActionEvent, EventQueryItem, StageHandlerPlanReturn, DisplayState, StageHandlerProgressParams, ECElementEvent } from '../util/types'; import { SeriesTaskContext, SeriesTask } from '../core/Scheduler'; import SeriesData from '../data/SeriesData'; import { traverseElements } from '../util/graphic'; import { error } from '../util/log'; const inner = modelUtil.makeInner<{ updateMethod: keyof ChartView }, Payload>(); const renderPlanner = createRenderPlanner(); interface ChartView { /** * Rendering preparation in progressive mode. * Implement it if needed. */ incrementalPrepareRender( seriesModel: SeriesModel, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload ): void; /** * Render in progressive mode. * Implement it if needed. * @param params See taskParams in `stream/task.js` */ incrementalRender( params: StageHandlerProgressParams, seriesModel: SeriesModel, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload ): void; /** * Update transform directly. * Implement it if needed. */ updateTransform( seriesModel: SeriesModel, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload ): void | {update: true}; /** * The view contains the given point. * Implement it if needed. */ containPoint( point: number[], seriesModel: SeriesModel ): boolean; /** * Pass only when return `true`. * Implement it if needed. */ filterForExposedEvent( eventType: string, query: EventQueryItem, targetEl: Element, packedEvent: ECActionEvent | ECElementEvent ): boolean; } class ChartView { // [Caution]: Because this class or desecendants can be used as `XXX.extend(subProto)`, // the class members must not be initialized in constructor or declaration place. // Otherwise there is bad case: // class A {xxx = 1;} // enableClassExtend(A); // class B extends A {} // var C = B.extend({xxx: 5}); // var c = new C(); // console.log(c.xxx); // expect 5 but always 1. // @readonly type: string; readonly group: ViewRootGroup; readonly uid: string; readonly renderTask: SeriesTask; /** * Ignore label line update in global stage. Will handle it in chart itself. * Used in pie / funnel */ ignoreLabelLineUpdate: boolean; // ---------------------- // Injectable properties // ---------------------- __alive: boolean; __model: SeriesModel; __id: string; static protoInitialize = (function () { const proto = ChartView.prototype; proto.type = 'chart'; })(); constructor() { this.group = new Group(); this.uid = componentUtil.getUID('viewChart'); this.renderTask = createTask({ plan: renderTaskPlan, reset: renderTaskReset }); this.renderTask.context = {view: this} as SeriesTaskContext; } init(ecModel: GlobalModel, api: ExtensionAPI): void {} render(seriesModel: SeriesModel, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload): void { if (__DEV__) { throw new Error('render method must been implemented'); } } /** * Highlight series or specified data item. */ highlight(seriesModel: SeriesModel, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload): void { const data = seriesModel.getData(payload && payload.dataType); if (!data) { if (__DEV__) { error(`Unknown dataType ${payload.dataType}`); } return; } toggleHighlight(data, payload, 'emphasis'); } /** * Downplay series or specified data item. */ downplay(seriesModel: SeriesModel, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload): void { const data = seriesModel.getData(payload && payload.dataType); if (!data) { if (__DEV__) { error(`Unknown dataType ${payload.dataType}`); } return; } toggleHighlight(data, payload, 'normal'); } /** * Remove self. */ remove(ecModel: GlobalModel, api: ExtensionAPI): void { this.group.removeAll(); } /** * Dispose self. */ dispose(ecModel: GlobalModel, api: ExtensionAPI): void {} updateView(seriesModel: SeriesModel, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload): void { this.render(seriesModel, ecModel, api, payload); } // FIXME never used? updateLayout(seriesModel: SeriesModel, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload): void { this.render(seriesModel, ecModel, api, payload); } // FIXME never used? updateVisual(seriesModel: SeriesModel, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload): void { this.render(seriesModel, ecModel, api, payload); } /** * Traverse the new rendered elements. * * It will traverse the new added element in progressive rendering. * And traverse all in normal rendering. */ eachRendered(cb: (el: Element) => boolean | void) { traverseElements(this.group, cb); } static markUpdateMethod(payload: Payload, methodName: keyof ChartView): void { inner(payload).updateMethod = methodName; } static registerClass: clazzUtil.ClassManager['registerClass']; }; /** * Set state of single element */ function elSetState(el: Element, state: DisplayState, highlightDigit: number) { if (el && isHighDownDispatcher(el)) { (state === 'emphasis' ? enterEmphasis : leaveEmphasis)(el, highlightDigit); } } function toggleHighlight(data: SeriesData, payload: Payload, state: DisplayState) { const dataIndex = modelUtil.queryDataIndex(data, payload); const highlightDigit = (payload && payload.highlightKey != null) ? getHighlightDigit(payload.highlightKey) : null; if (dataIndex != null) { each(modelUtil.normalizeToArray(dataIndex), function (dataIdx) { elSetState(data.getItemGraphicEl(dataIdx), state, highlightDigit); }); } else { data.eachItemGraphicEl(function (el) { elSetState(el, state, highlightDigit); }); } } export type ChartViewConstructor = typeof ChartView & clazzUtil.ExtendableConstructor & clazzUtil.ClassManager; clazzUtil.enableClassExtend(ChartView as ChartViewConstructor, ['dispose']); clazzUtil.enableClassManagement(ChartView as ChartViewConstructor); function renderTaskPlan(context: SeriesTaskContext): StageHandlerPlanReturn { return renderPlanner(context.model); } function renderTaskReset(context: SeriesTaskContext): TaskResetCallbackReturn { const seriesModel = context.model; const ecModel = context.ecModel; const api = context.api; const payload = context.payload; // FIXME: remove updateView updateVisual const progressiveRender = seriesModel.pipelineContext.progressiveRender; const view = context.view; const updateMethod = payload && inner(payload).updateMethod; const methodName: keyof ChartView = progressiveRender ? 'incrementalPrepareRender' : (updateMethod && view[updateMethod]) ? updateMethod // `appendData` is also supported when data amount // is less than progressive threshold. : 'render'; if (methodName !== 'render') { (view[methodName] as any)(seriesModel, ecModel, api, payload); } return progressMethodMap[methodName]; } const progressMethodMap: {[method: string]: TaskResetCallbackReturn} = { incrementalPrepareRender: { progress: function (params: StageHandlerProgressParams, context: SeriesTaskContext): void { context.view.incrementalRender( params, context.model, context.ecModel, context.api, context.payload ); } }, render: { // Put view.render in `progress` to support appendData. But in this case // view.render should not be called in reset, otherwise it will be called // twise. Use `forceFirstProgress` to make sure that view.render is called // in any cases. forceFirstProgress: true, progress: function (params: StageHandlerProgressParams, context: SeriesTaskContext): void { context.view.render( context.model, context.ecModel, context.api, context.payload ); } } }; export default ChartView;