blob: eb9db4a2d13bff60de858ef0c9f28e647dbb99d6 [file] [log] [blame]
/*
* 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 } 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 List from '../data/List';
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]: Becuase 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<SeriesTaskContext>({
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 {}
/**
* Highlight series or specified data item.
*/
highlight(seriesModel: SeriesModel, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload): void {
toggleHighlight(seriesModel.getData(), payload, 'emphasis');
}
/**
* Downplay series or specified data item.
*/
downplay(seriesModel: SeriesModel, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload): void {
toggleHighlight(seriesModel.getData(), 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);
}
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) {
(state === 'emphasis' ? enterEmphasis : leaveEmphasis)(el, highlightDigit);
}
}
function toggleHighlight(data: List, 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<SeriesTaskContext> {
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<SeriesTaskContext>} = {
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;