You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@echarts.apache.org by sh...@apache.org on 2020/09/30 09:34:47 UTC
[incubator-echarts] 01/01: Merge branch 'master' into
next-merge-master
This is an automated email from the ASF dual-hosted git repository.
shenyi pushed a commit to branch next-merge-master
in repository https://gitbox.apache.org/repos/asf/incubator-echarts.git
commit 2f32ac609d5d29280c22d7abe1786e90bfa7133b
Merge: 3f616bb fd8417b
Author: pissang <bm...@gmail.com>
AuthorDate: Wed Sep 30 17:33:44 2020 +0800
Merge branch 'master' into next-merge-master
# Conflicts:
# dist/echarts-en.common.js
# dist/echarts-en.common.min.js
# dist/echarts-en.js
# dist/echarts-en.js.map
# dist/echarts-en.min.js
# dist/echarts-en.simple.js
# dist/echarts-en.simple.min.js
# dist/echarts.common.js
# dist/echarts.common.min.js
# dist/echarts.js
# dist/echarts.js.map
# dist/echarts.min.js
# dist/echarts.simple.js
# dist/echarts.simple.min.js
# dist/extension/bmap.js
# dist/extension/bmap.js.map
# dist/extension/bmap.min.js
# dist/extension/dataTool.js.map
# extension-src/bmap/BMapCoordSys.ts
# extension-src/bmap/BMapView.ts
# extension/bmap/BMapCoordSys.js
# extension/bmap/BMapModel.js
# extension/bmap/BMapView.js
# package-lock.json
# package.json
# src/chart/bar/BarView.js
# src/chart/bar/PictorialBarSeries.js
# src/chart/funnel/funnelLayout.ts
# src/chart/graph/GraphSeries.js
# src/chart/graph/circularLayoutHelper.ts
# src/chart/graph/forceLayout.js
# src/chart/graph/simpleLayoutHelper.ts
# src/chart/helper/Line.js
# src/chart/helper/Symbol.js
# src/chart/helper/createGraphFromNodeEdge.ts
# src/chart/lines/LinesSeries.ts
# src/chart/map/MapSeries.js
# src/chart/radar/RadarSeries.js
# src/chart/radar/RadarView.js
# src/chart/sunburst/SunburstPiece.js
# src/chart/sunburst/SunburstSeries.ts
# src/chart/themeRiver/ThemeRiverSeries.js
# src/chart/tree/TreeSeries.js
# src/chart/tree/TreeView.js
# src/chart/treemap/TreemapSeries.ts
# src/chart/treemap/treemapLayout.ts
# src/chart/treemap/treemapVisual.js
# src/component/marker/MarkAreaView.js
# src/component/marker/MarkLineView.js
# src/component/marker/MarkPointView.js
# src/component/marker/MarkerModel.js
# src/component/timeline/SliderTimelineView.js
# src/component/title.ts
# src/component/toolbox/feature/DataView.js
# src/component/toolbox/feature/DataZoom.js
# src/component/toolbox/feature/SaveAsImage.js
# src/component/tooltip/TooltipContent.js
# src/component/tooltip/TooltipRichContent.js
# src/component/tooltip/TooltipView.js
# src/coord/axisHelper.js
# src/coord/geo/GeoModel.js
# src/data/Graph.js
# src/data/List.js
# src/data/Tree.js
# src/echarts.js
# src/layout/barPolar.ts
# src/processor/dataSample.ts
# src/stream/Scheduler.js
# src/visual/VisualMapping.js
# test/timeline-dynamic-series.html
.asf.yaml | 13 +
.gitattributes | 3 +
.huskyrc | 5 +
.lgtm.yml | 12 +
README.md | 8 +-
package.json | 1 +
src/chart/bar/BarView.ts | 92 +++--
src/chart/funnel/FunnelSeries.ts | 10 +-
src/chart/funnel/funnelLayout.ts | 233 ++++++++---
src/chart/graph/GraphSeries.ts | 11 +-
src/chart/graph/circularLayoutHelper.ts | 10 +-
src/chart/graph/edgeVisual.ts | 8 -
src/chart/graph/forceLayout.ts | 8 +-
src/chart/graph/simpleLayout.ts | 2 +-
src/chart/graph/simpleLayoutHelper.ts | 17 +-
src/chart/helper/Line.ts | 41 +-
src/chart/helper/multipleGraphEdgeHelper.ts | 230 +++++++++++
src/chart/lines/LinesSeries.ts | 5 +-
src/chart/radar/RadarView.ts | 4 +-
src/chart/sunburst/SunburstSeries.ts | 24 +-
src/chart/themeRiver/ThemeRiverSeries.ts | 67 ++-
src/chart/tree/TreeSeries.ts | 2 +-
src/chart/treemap/TreemapSeries.ts | 16 +-
src/chart/treemap/treemapLayout.ts | 4 +-
src/chart/treemap/treemapVisual.ts | 31 +-
src/component/axisPointer/axisTrigger.ts | 2 +-
src/component/legend/ScrollableLegendView.ts | 2 +-
src/component/marker/MarkAreaView.ts | 73 +++-
src/component/marker/MarkLineView.ts | 3 +
src/component/marker/MarkPointView.ts | 7 +-
src/component/timeline/SliderTimelineView.ts | 17 +-
src/component/toolbox/feature/DataView.ts | 32 +-
src/component/toolbox/feature/DataZoom.ts | 18 +-
src/component/toolbox/feature/SaveAsImage.ts | 3 +-
src/component/visualMap/VisualMapModel.ts | 2 +-
src/coord/axisDefault.ts | 2 +-
src/coord/axisHelper.ts | 6 +-
src/coord/geo/GeoModel.ts | 1 -
src/data/Graph.ts | 4 -
src/data/List.ts | 2 +-
src/data/Tree.ts | 29 +-
src/echarts.ts | 2 +-
src/layout/barPolar.ts | 6 +-
src/stream/Scheduler.ts | 2 +-
src/visual/commonVisualTypes.ts | 2 +
test/axisLabel.html | 11 +-
test/bar-polar-stack.html | 218 ++++++++++
test/bmap-mapOptions.html | 530 ++++++++++++++++++++++++
test/bmap2.html | 592 +++++++++++++++++++++++++++
test/data/aqi/aqi-beijing.json | 1 +
test/dataZoom-toolbox.html | 8 +-
test/dataset-charts.html | 36 +-
test/funnel.html | 11 +-
test/geo-labelFormatter.html | 364 ++++++++++++++++
test/graph-mulitple-edges.html | 357 ++++++++++++++++
test/lines-mergeOption.html | 103 +++++
test/markArea.html | 21 +-
test/markLine-symbolRotate.html | 187 +++++++++
test/markPoint.html | 11 +-
test/pie-dataView.html | 122 ++++++
test/radar.html | 9 +-
test/sunburst-visualMap.html | 24 +-
test/themeRiver3.html | 154 +++++++
test/timeline-dynamic-series.html | 10 +-
test/tooltip-rich.html | 14 +-
test/tooltip-textStyle.html | 455 ++++++++++++++++++++
test/tooltip-windowResize.html | 383 +++++++++++++++++
test/tree-image2.html | 98 +++++
test/treemap-simple2.html | 137 +++++++
69 files changed, 4628 insertions(+), 300 deletions(-)
diff --cc package.json
index 113afe5,8616612..ac87f3e
--- a/package.json
+++ b/package.json
@@@ -40,19 -31,21 +40,20 @@@
},
"devDependencies": {
"@babel/core": "7.3.4",
- "@babel/helper-module-transforms": "7.0.0-beta.31",
- "@babel/helper-simple-access": "7.0.0-beta.31",
- "@babel/template": "7.0.0-beta.31",
- "@babel/types": "7.0.0-beta.31",
- "assert": "1.4.1",
+ "@babel/types": "^7.10.5",
+ "@microsoft/api-extractor": "7.7.2",
+ "@typescript-eslint/eslint-plugin": "^2.15.0",
+ "@typescript-eslint/parser": "^2.18.0",
++ "husky": "^4.2.5",
"canvas": "^2.6.0",
+ "chalk": "^3.0.0",
+ "chokidar": "^3.4.0",
"commander": "2.11.0",
- "coordtransform": "2.0.2",
- "escodegen": "1.8.0",
+ "esbuild": "^0.4.1",
"eslint": "6.3.0",
- "esprima": "2.7.2",
- "estraverse": "4.1.1",
"fs-extra": "0.26.7",
"glob": "7.0.0",
- "husky": "^4.2.5",
+ "globby": "11.0.0",
"jest": "^24.9.0",
"jest-canvas-mock": "^2.2.0",
"jsdom": "^15.2.1",
diff --cc src/chart/bar/BarView.ts
index 2d48aaa,0000000..cd00df6
mode 100644,000000..100644
--- a/src/chart/bar/BarView.ts
+++ b/src/chart/bar/BarView.ts
@@@ -1,1088 -1,0 +1,1118 @@@
+/*
+* 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 Path, {PathProps} from 'zrender/src/graphic/Path';
+import Group from 'zrender/src/graphic/Group';
+import {extend, map, defaults, each} from 'zrender/src/core/util';
+import type {RectLike} from 'zrender/src/core/BoundingRect';
+import {
+ Rect,
+ Sector,
+ updateProps,
+ initProps,
+ updateLabel,
+ initLabel,
+ removeElementWithFadeOut
+} from '../../util/graphic';
+import { getECData } from '../../util/innerStore';
+import { enableHoverEmphasis, setStatesStylesFromModel } from '../../util/states';
+import { setLabelStyle, getLabelStatesModels, labelInner } from '../../label/labelStyle';
+import {throttle} from '../../util/throttle';
+import {createClipPath} from '../helper/createClipPathFromCoordSys';
+import Sausage from '../../util/shape/sausage';
+import ChartView from '../../view/Chart';
+import List, {DefaultDataVisual} from '../../data/List';
+import GlobalModel from '../../model/Global';
+import ExtensionAPI from '../../ExtensionAPI';
+import {
+ StageHandlerProgressParams,
+ ZRElementEvent,
+ ColorString,
+ OrdinalSortInfo,
+ Payload,
+ OrdinalNumber,
+ ParsedValue,
+ ECElement
+} from '../../util/types';
+import BarSeriesModel, {BarSeriesOption, BarDataItemOption} from './BarSeries';
+import type Axis2D from '../../coord/cartesian/Axis2D';
+import type Cartesian2D from '../../coord/cartesian/Cartesian2D';
++import type Polar from '../../coord/polar/Polar';
+import type Model from '../../model/Model';
+import { isCoordinateSystemType } from '../../coord/CoordinateSystem';
+import { getDefaultLabel, getDefaultInterpolatedLabel } from '../helper/labelHelper';
+import OrdinalScale from '../../scale/Ordinal';
+import SeriesModel from '../../model/Series';
+import {AngleAxisModel, RadiusAxisModel} from '../../coord/polar/AxisModel';
+import CartesianAxisModel from '../../coord/cartesian/AxisModel';
+import {LayoutRect} from '../../util/layout';
+import {EventCallback} from 'zrender/src/core/Eventful';
+
+const BAR_BORDER_WIDTH_QUERY = ['itemStyle', 'borderWidth'] as const;
+const BAR_BORDER_RADIUS_QUERY = ['itemStyle', 'borderRadius'] as const;
+const _eventPos = [0, 0];
+
+const mathMax = Math.max;
+const mathMin = Math.min;
+
+type CoordSysOfBar = BarSeriesModel['coordinateSystem'];
+type RectShape = Rect['shape'];
+type SectorShape = Sector['shape'];
+
+type SectorLayout = SectorShape;
+type RectLayout = RectShape;
+
+type BarPossiblePath = Sector | Rect | Sausage;
+
++type CartesianCoordArea = ReturnType<Cartesian2D['getArea']>;
++type PolarCoordArea = ReturnType<Polar['getArea']>;
++
+function getClipArea(coord: CoordSysOfBar, data: List) {
- let coordSysClipArea;
++ let coordSysClipArea = coord.getArea && coord.getArea();
+ if (isCoordinateSystemType<Cartesian2D>(coord, 'cartesian2d')) {
- coordSysClipArea = coord.getArea && coord.getArea();
+ const baseAxis = coord.getBaseAxis();
+ // When boundaryGap is false or using time axis. bar may exceed the grid.
+ // We should not clip this part.
+ // See test/bar2.html
+ if (baseAxis.type !== 'category' || !baseAxis.onBand) {
+ const expandWidth = data.getLayout('bandWidth');
+ if (baseAxis.isHorizontal()) {
- coordSysClipArea.x -= expandWidth;
- coordSysClipArea.width += expandWidth * 2;
++ (coordSysClipArea as CartesianCoordArea).x -= expandWidth;
++ (coordSysClipArea as CartesianCoordArea).width += expandWidth * 2;
+ }
+ else {
- coordSysClipArea.y -= expandWidth;
- coordSysClipArea.height += expandWidth * 2;
++ (coordSysClipArea as CartesianCoordArea).y -= expandWidth;
++ (coordSysClipArea as CartesianCoordArea).height += expandWidth * 2;
+ }
+ }
+ }
+
- return coordSysClipArea;
++ return coordSysClipArea as PolarCoordArea | CartesianCoordArea;
+}
+
+class BarView extends ChartView {
+ static type = 'bar' as const;
+ type = BarView.type;
+
+ private _data: List;
+
+ private _isLargeDraw: boolean;
+
+ private _isFirstFrame: boolean; // First frame after series added
+ private _onRendered: EventCallback<unknown, unknown>;
+
+ private _backgroundGroup: Group;
+
+ private _backgroundEls: (Rect | Sector)[];
+
+ private _model: BarSeriesModel;
+
+ constructor() {
+ super();
+ this._isFirstFrame = true;
+ }
+
+ render(seriesModel: BarSeriesModel, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload) {
+ this._model = seriesModel;
+
+ this.removeOnRenderedListener(api);
+
+ this._updateDrawMode(seriesModel);
+
+ const coordinateSystemType = seriesModel.get('coordinateSystem');
+
+ if (coordinateSystemType === 'cartesian2d'
+ || coordinateSystemType === 'polar'
+ ) {
+ this._isLargeDraw
+ ? this._renderLarge(seriesModel, ecModel, api)
+ : this._renderNormal(seriesModel, ecModel, api, payload);
+ }
+ else if (__DEV__) {
+ console.warn('Only cartesian2d and polar supported for bar.');
+ }
+ }
+
+ incrementalPrepareRender(seriesModel: BarSeriesModel): void {
+ this._clear();
+ this._updateDrawMode(seriesModel);
+ // incremental also need to clip, otherwise might be overlow.
+ // But must not set clip in each frame, otherwise all of the children will be marked redraw.
+ this._updateLargeClip(seriesModel);
+ }
+
+ incrementalRender(params: StageHandlerProgressParams, seriesModel: BarSeriesModel): void {
+ // Do not support progressive in normal mode.
+ this._incrementalRenderLarge(params, seriesModel);
+ }
+
+ private _updateDrawMode(seriesModel: BarSeriesModel): void {
+ const isLargeDraw = seriesModel.pipelineContext.large;
+ if (this._isLargeDraw == null || isLargeDraw !== this._isLargeDraw) {
+ this._isLargeDraw = isLargeDraw;
+ this._clear();
+ }
+ }
+
+ private _renderNormal(
+ seriesModel: BarSeriesModel,
+ ecModel: GlobalModel,
+ api: ExtensionAPI,
+ payload: Payload
+ ): void {
+ const group = this.group;
+ const data = seriesModel.getData();
+ const oldData = this._data;
+
+ const coord = seriesModel.coordinateSystem;
+ const baseAxis = coord.getBaseAxis();
+ let isHorizontalOrRadial: boolean;
+
+ if (coord.type === 'cartesian2d') {
+ isHorizontalOrRadial = (baseAxis as Axis2D).isHorizontal();
+ }
+ else if (coord.type === 'polar') {
+ isHorizontalOrRadial = baseAxis.dim === 'angle';
+ }
+
+ const animationModel = seriesModel.isAnimationEnabled() ? seriesModel : null;
+
+ const axis2DModel = (baseAxis as Axis2D).model;
+ const realtimeSort = seriesModel.get('realtimeSort');
+
+ // If no data in the first frame, wait for data to initSort
+ if (realtimeSort && data.count()) {
+ if (this._isFirstFrame) {
+ this._initSort(data, isHorizontalOrRadial, baseAxis as Axis2D, api);
+ this._isFirstFrame = false;
+ return;
+ }
+ else {
+ this._onRendered = () => {
+ const orderMap = (idx: number) => {
+ const el = (data.getItemGraphicEl(idx) as Rect);
+ if (el) {
+ const shape = el.shape;
+ // If data is NaN, shape.xxx may be NaN, so use || 0 here in case
+ return (isHorizontalOrRadial ? shape.y + shape.height : shape.x + shape.width) || 0;
+ }
+ else {
+ return 0;
+ }
+ };
+ this._updateSort(data, orderMap, baseAxis as Axis2D, api);
+ };
+ api.getZr().on('rendered', this._onRendered as any);
+ }
+ }
+
+ const needsClip = seriesModel.get('clip', true) || realtimeSort;
+ const coordSysClipArea = getClipArea(coord, data);
+ // If there is clipPath created in large mode. Remove it.
+ group.removeClipPath();
+ // We don't use clipPath in normal mode because we needs a perfect animation
+ // And don't want the label are clipped.
+
+ const roundCap = seriesModel.get('roundCap', true);
+
+ const drawBackground = seriesModel.get('showBackground', true);
+ const backgroundModel = seriesModel.getModel('backgroundStyle');
+ const barBorderRadius = backgroundModel.get('borderRadius') || 0;
+
+ const bgEls: BarView['_backgroundEls'] = [];
+ const oldBgEls = this._backgroundEls;
+
+ const isInitSort = payload && payload.isInitSort;
+ const isChangeOrder = payload && payload.type === 'changeAxisOrder';
+
+ const defaultTextGetter = (values: ParsedValue | ParsedValue[]) => {
+ return getDefaultInterpolatedLabel(seriesModel.getData(), values);
+ };
++ function createBackground(dataIndex: number) {
++ const bgLayout = getLayout[coord.type](data, dataIndex);
++ const bgEl = createBackgroundEl(coord, isHorizontalOrRadial, bgLayout);
++ bgEl.useStyle(backgroundModel.getItemStyle());
++ // Only cartesian2d support borderRadius.
++ if (coord.type === 'cartesian2d') {
++ (bgEl as Rect).setShape('r', barBorderRadius);
++ }
++ bgEls[dataIndex] = bgEl;
++ return bgEl;
++ };
+ data.diff(oldData)
+ .add(function (dataIndex) {
+ const itemModel = data.getItemModel<BarDataItemOption>(dataIndex);
+ const layout = getLayout[coord.type](data, dataIndex, itemModel);
+
+ if (drawBackground) {
- const bgLayout = getLayout[coord.type](data, dataIndex);
- const bgEl = createBackgroundEl(coord, isHorizontalOrRadial, bgLayout);
- bgEl.useStyle(backgroundModel.getItemStyle());
- // Only cartesian2d support borderRadius.
- if (coord.type === 'cartesian2d') {
- (bgEl as Rect).setShape('r', barBorderRadius);
- }
- bgEls[dataIndex] = bgEl;
++ createBackground(dataIndex);
+ }
+
+ // If dataZoom in filteMode: 'empty', the baseValue can be set as NaN in "axisProxy".
+ if (!data.hasValue(dataIndex)) {
+ return;
+ }
+
+ let isClipped = false;
+ if (needsClip) {
+ // Clip will modify the layout params.
+ // And return a boolean to determine if the shape are fully clipped.
+ isClipped = clip[coord.type](coordSysClipArea, layout);
+ }
+
+ const el = elementCreator[coord.type](
+ seriesModel,
+ data,
+ dataIndex,
+ layout,
+ isHorizontalOrRadial,
+ animationModel,
+ baseAxis.model,
+ false,
+ roundCap
+ );
+
+ updateStyle(
+ el, data, dataIndex, itemModel, layout,
+ seriesModel, isHorizontalOrRadial, coord.type === 'polar'
+ );
+
+ initLabel(
+ el, data, dataIndex, itemModel.getModel('label'), seriesModel, animationModel, defaultTextGetter
+ );
+ if (isInitSort) {
+ (el as Rect).attr({ shape: layout });
+ }
+ else if (realtimeSort) {
+ (el as unknown as ECElement).disableLabelAnimation = true;
+
+ updateRealtimeAnimation(
+ seriesModel,
+ axis2DModel,
+ animationModel,
+ el as Rect,
+ layout as LayoutRect,
+ dataIndex,
+ isHorizontalOrRadial,
+ false,
+ false
+ );
+ }
+ else {
+ initProps(el, {shape: layout} as any, seriesModel, dataIndex);
+ }
+
+ data.setItemGraphicEl(dataIndex, el);
+
+ group.add(el);
+ el.ignore = isClipped;
+ })
+ .update(function (newIndex, oldIndex) {
+ const itemModel = data.getItemModel<BarDataItemOption>(newIndex);
+ const layout = getLayout[coord.type](data, newIndex, itemModel);
+
+ if (drawBackground) {
- const bgEl = oldBgEls[oldIndex];
- bgEl.useStyle(backgroundModel.getItemStyle());
- // Only cartesian2d support borderRadius.
- if (coord.type === 'cartesian2d') {
- (bgEl as Rect).setShape('r', barBorderRadius);
++ let bgEl: Rect | Sector;
++ if (oldBgEls.length === 0) {
++ bgEl = createBackground(oldIndex);
++ }
++ else {
++ bgEl = oldBgEls[oldIndex];
++ bgEl.useStyle(backgroundModel.getItemStyle());
++ // Only cartesian2d support borderRadius.
++ if (coord.type === 'cartesian2d') {
++ (bgEl as Rect).setShape('r', barBorderRadius);
++ }
++ bgEls[newIndex] = bgEl;
+ }
- bgEls[newIndex] = bgEl;
-
- const bgLayout = getLayout[coord.type](data, newIndex);
- const shape = createBackgroundShape(isHorizontalOrRadial, bgLayout, coord);
- updateProps(
- bgEl as Path, {shape: shape as RectShape}, animationModel, newIndex
- );
+ }
+
+ let el = oldData.getItemGraphicEl(oldIndex) as BarPossiblePath;
+ if (!data.hasValue(newIndex)) {
+ group.remove(el);
+ el = null;
+ }
+
+ let isClipped = false;
+ if (needsClip) {
+ isClipped = clip[coord.type](coordSysClipArea, layout);
+ if (isClipped) {
+ group.remove(el);
+ }
+ }
+
+ if (!el) {
+ el = elementCreator[coord.type](
+ seriesModel,
+ data,
+ newIndex,
+ layout,
+ isHorizontalOrRadial,
+ animationModel,
+ baseAxis.model,
+ !!el,
+ roundCap
+ );
+ }
+
+ // Not change anything if only order changed.
+ // Especially not change label.
+ if (!isChangeOrder) {
+ updateStyle(
+ el, data, newIndex, itemModel, layout,
+ seriesModel, isHorizontalOrRadial, coord.type === 'polar'
+ );
+ updateLabel(
+ el, data, newIndex, itemModel.getModel('label'), seriesModel, animationModel, defaultTextGetter
+ );
+ }
+
+ if (isInitSort) {
+ (el as Rect).attr({ shape: layout });
+ }
+ else if (realtimeSort) {
+ (el as unknown as ECElement).disableLabelAnimation = true;
+
+ updateRealtimeAnimation(
+ seriesModel,
+ axis2DModel,
+ animationModel,
+ el as Rect,
+ layout as LayoutRect,
+ newIndex,
+ isHorizontalOrRadial,
+ true,
+ isChangeOrder
+ );
+ }
+ else {
+ updateProps(el, {
+ shape: layout
+ } as any, seriesModel, newIndex, null);
+ }
+
+ data.setItemGraphicEl(newIndex, el);
+ el.ignore = isClipped;
+ group.add(el);
+ })
+ .remove(function (dataIndex) {
+ const el = oldData.getItemGraphicEl(dataIndex) as Path;
+ el && removeElementWithFadeOut(el, seriesModel, dataIndex);
+ })
+ .execute();
+
+ const bgGroup = this._backgroundGroup || (this._backgroundGroup = new Group());
+ bgGroup.removeAll();
+
+ for (let i = 0; i < bgEls.length; ++i) {
+ bgGroup.add(bgEls[i]);
+ }
+ group.add(bgGroup);
+ this._backgroundEls = bgEls;
+
+ this._data = data;
+ }
+
+ private _renderLarge(seriesModel: BarSeriesModel, ecModel: GlobalModel, api: ExtensionAPI): void {
+ this._clear();
+ createLarge(seriesModel, this.group);
+ this._updateLargeClip(seriesModel);
+ }
+
+ private _incrementalRenderLarge(params: StageHandlerProgressParams, seriesModel: BarSeriesModel): void {
+ this._removeBackground();
+ createLarge(seriesModel, this.group, true);
+ }
+
+ private _updateLargeClip(seriesModel: BarSeriesModel): void {
+ // Use clipPath in large mode.
+ const clipPath = seriesModel.get('clip', true)
+ ? createClipPath(seriesModel.coordinateSystem, false, seriesModel)
+ : null;
+ if (clipPath) {
+ this.group.setClipPath(clipPath);
+ }
+ else {
+ this.group.removeClipPath();
+ }
+ }
+
+ _dataSort(
+ data: List<BarSeriesModel, DefaultDataVisual>,
+ idxMap: ((idx: number) => number)
+ ): OrdinalSortInfo[] {
+ type SortValueInfo = {
+ mappedValue: number,
+ ordinalNumber: OrdinalNumber,
+ beforeSortIndex: number
+ };
+ const info: SortValueInfo[] = [];
+ data.each(idx => {
+ info.push({
+ mappedValue: idxMap(idx),
+ ordinalNumber: idx,
+ beforeSortIndex: null
+ });
+ });
+
+ info.sort((a, b) => {
+ return b.mappedValue - a.mappedValue;
+ });
+
+ // Update beforeSortIndex
+ for (let i = 0; i < info.length; ++i) {
+ info[info[i].ordinalNumber].beforeSortIndex = i;
+ }
+
+ return map(info, item => {
+ return {
+ ordinalNumber: item.ordinalNumber,
+ beforeSortIndex: item.beforeSortIndex
+ };
+ });
+ }
+
+ _isDataOrderChanged(
+ data: List<BarSeriesModel, DefaultDataVisual>,
+ orderMap: ((idx: number) => number),
+ oldOrder: OrdinalSortInfo[]
+ ): boolean {
+ const oldCount = oldOrder ? oldOrder.length : 0;
+ if (oldCount !== data.count()) {
+ return true;
+ }
+
+ let lastValue = Number.MAX_VALUE;
+ for (let i = 0; i < oldOrder.length; ++i) {
+ const value = orderMap(oldOrder[i].ordinalNumber);
+ if (value > lastValue) {
+ return true;
+ }
+ lastValue = value;
+ }
+ return false;
+ }
+
+ _updateSort(
+ data: List<BarSeriesModel, DefaultDataVisual>,
+ orderMap: ((idx: number) => number),
+ baseAxis: Axis2D,
+ api: ExtensionAPI
+ ) {
+ const oldOrder = (baseAxis.scale as OrdinalScale).getCategorySortInfo();
+ const isOrderChanged = this._isDataOrderChanged(data, orderMap, oldOrder);
+ if (isOrderChanged) {
+ const newOrder = this._dataSort(data, orderMap);
+ const extent = baseAxis.scale.getExtent();
+ for (let i = extent[0]; i < extent[1]; ++i) {
+ /**
+ * Consider the case when A and B changed order, whose representing
+ * bars are both out of sight, we don't wish to trigger reorder action
+ * as long as the order in the view doesn't change.
+ */
+ if (!oldOrder[i] || oldOrder[i].ordinalNumber !== newOrder[i].ordinalNumber) {
+ this.removeOnRenderedListener(api);
+
+ const action = {
+ type: 'changeAxisOrder',
+ componentType: baseAxis.dim + 'Axis',
+ axisId: baseAxis.index,
+ sortInfo: newOrder
+ } as Payload;
+ api.dispatchAction(action);
+ break;
+ }
+ }
+ }
+ }
+
+ _initSort(
+ data: List<BarSeriesModel, DefaultDataVisual>,
+ isHorizontal: boolean,
+ baseAxis: Axis2D,
+ api: ExtensionAPI
+ ) {
+ const action = {
+ type: 'changeAxisOrder',
+ componentType: baseAxis.dim + 'Axis',
+ isInitSort: true,
+ axisId: baseAxis.index,
+ sortInfo: this._dataSort(
+ data,
+ idx => parseFloat(data.get(isHorizontal ? 'y' : 'x', idx) as string) || 0
+ )
+ } as Payload;
+ api.dispatchAction(action);
+ }
+
+ remove(ecModel: GlobalModel, api: ExtensionAPI) {
+ this._clear(this._model);
+ this.removeOnRenderedListener(api);
+ }
+
+ dispose(ecModel: GlobalModel, api: ExtensionAPI) {
+ this.removeOnRenderedListener(api);
+ }
+
+ removeOnRenderedListener(api: ExtensionAPI) {
+ if (this._onRendered) {
+ api.getZr().off('rendered', this._onRendered);
+ this._onRendered = null;
+ }
+ }
+
+ private _clear(model?: SeriesModel): void {
+ const group = this.group;
+ const data = this._data;
+ if (model && model.isAnimationEnabled() && data && !this._isLargeDraw) {
+ this._removeBackground();
+ this._backgroundEls = [];
+
+ data.eachItemGraphicEl(function (el: Path) {
+ removeElementWithFadeOut(el, model, getECData(el).dataIndex);
+ });
+ }
+ else {
+ group.removeAll();
+ }
+ this._data = null;
+ this._isFirstFrame = true;
+ }
+
+ private _removeBackground(): void {
+ this.group.remove(this._backgroundGroup);
+ this._backgroundGroup = null;
+ }
+}
+
+interface Clipper {
- (coordSysBoundingRect: RectLike, layout: RectLayout | SectorLayout): boolean
++ (coordSysBoundingRect: PolarCoordArea | CartesianCoordArea, layout: RectLayout | SectorLayout): boolean
+}
+const clip: {
+ [key in 'cartesian2d' | 'polar']: Clipper
+} = {
- cartesian2d(coordSysBoundingRect: RectLike, layout: Rect['shape']) {
++ cartesian2d(coordSysBoundingRect: CartesianCoordArea, layout: Rect['shape']) {
+ const signWidth = layout.width < 0 ? -1 : 1;
+ const signHeight = layout.height < 0 ? -1 : 1;
+ // Needs positive width and height
+ if (signWidth < 0) {
+ layout.x += layout.width;
+ layout.width = -layout.width;
+ }
+ if (signHeight < 0) {
+ layout.y += layout.height;
+ layout.height = -layout.height;
+ }
+
+ const x = mathMax(layout.x, coordSysBoundingRect.x);
+ const x2 = mathMin(layout.x + layout.width, coordSysBoundingRect.x + coordSysBoundingRect.width);
+ const y = mathMax(layout.y, coordSysBoundingRect.y);
+ const y2 = mathMin(layout.y + layout.height, coordSysBoundingRect.y + coordSysBoundingRect.height);
+
+ layout.x = x;
+ layout.y = y;
+ layout.width = x2 - x;
+ layout.height = y2 - y;
+
+ const clipped = layout.width < 0 || layout.height < 0;
+
+ // Reverse back
+ if (signWidth < 0) {
+ layout.x += layout.width;
+ layout.width = -layout.width;
+ }
+ if (signHeight < 0) {
+ layout.y += layout.height;
+ layout.height = -layout.height;
+ }
+
+ return clipped;
+ },
+
- polar() {
- return false;
++ polar(coordSysClipArea: PolarCoordArea, layout: Sector['shape']) {
++ const signR = layout.r0 <= layout.r ? 1 : -1;
++ // Make sure r is larger than r0
++ if (signR < 0) {
++ const tmp = layout.r;
++ layout.r = layout.r0;
++ layout.r0 = tmp;
++ }
++
++ const r = mathMin(layout.r, coordSysClipArea.r);
++ const r0 = mathMax(layout.r0, coordSysClipArea.r0);
++
++ layout.r = r;
++ layout.r0 = r0;
++
++ const clipped = r - r0 < 0;
++
++ // Reverse back
++ if (signR < 0) {
++ const tmp = layout.r;
++ layout.r = layout.r0;
++ layout.r0 = tmp;
++ }
++
++ return clipped;
+ }
+};
+
+interface ElementCreator {
+ (
+ seriesModel: BarSeriesModel, data: List, newIndex: number,
+ layout: RectLayout | SectorLayout, isHorizontalOrRadial: boolean,
+ animationModel: BarSeriesModel,
+ axisModel: CartesianAxisModel | AngleAxisModel | RadiusAxisModel,
+ isUpdate: boolean,
+ roundCap?: boolean
+ ): BarPossiblePath
+}
+
+const elementCreator: {
+ [key in 'polar' | 'cartesian2d']: ElementCreator
+} = {
+
+ cartesian2d(
+ seriesModel, data, newIndex, layout: RectLayout, isHorizontal,
+ animationModel, axisModel, isUpdate, roundCap
+ ) {
+ const rect = new Rect({
+ shape: extend({}, layout),
+ z2: 1
+ });
+ (rect as any).__dataIndex = newIndex;
+
+ rect.name = 'item';
+
+ if (animationModel) {
+ const rectShape = rect.shape;
+ const animateProperty = isHorizontal ? 'height' : 'width' as 'width' | 'height';
+ rectShape[animateProperty] = 0;
+ }
+ return rect;
+ },
+
+ polar(
+ seriesModel, data, newIndex, layout: SectorLayout, isRadial: boolean,
+ animationModel, axisModel, isUpdate, roundCap
+ ) {
+ // Keep the same logic with bar in catesion: use end value to control
+ // direction. Notice that if clockwise is true (by default), the sector
+ // will always draw clockwisely, no matter whether endAngle is greater
+ // or less than startAngle.
+ const clockwise = layout.startAngle < layout.endAngle;
+
+ const ShapeClass = (!isRadial && roundCap) ? Sausage : Sector;
+
+ const sector = new ShapeClass({
+ shape: defaults({clockwise: clockwise}, layout),
+ z2: 1
+ });
+
+ sector.name = 'item';
+
+ // Animation
+ if (animationModel) {
+ const sectorShape = sector.shape;
+ const animateProperty = isRadial ? 'r' : 'endAngle' as 'r' | 'endAngle';
+ const animateTarget = {} as SectorShape;
+ sectorShape[animateProperty] = isRadial ? 0 : layout.startAngle;
+ animateTarget[animateProperty] = layout[animateProperty];
+ (isUpdate ? updateProps : initProps)(sector, {
+ shape: animateTarget
+ // __value: typeof dataValue === 'string' ? parseInt(dataValue, 10) : dataValue
+ }, animationModel);
+ }
+
+ return sector;
+ }
+};
+
+function updateRealtimeAnimation(
+ seriesModel: BarSeriesModel,
+ axisModel: CartesianAxisModel,
+ animationModel: BarSeriesModel,
+ el: Rect,
+ layout: LayoutRect,
+ newIndex: number,
+ isHorizontal: boolean,
+ isUpdate: boolean,
+ isChangeOrder: boolean
+) {
+ // Animation
+ if (animationModel || axisModel) {
+ let seriesTarget;
+ let axisTarget;
+ if (isHorizontal) {
+ axisTarget = {
+ x: layout.x,
+ width: layout.width
+ };
+ seriesTarget = {
+ y: layout.y,
+ height: layout.height
+ };
+ }
+ else {
+ axisTarget = {
+ y: layout.y,
+ height: layout.height
+ };
+ seriesTarget = {
+ x: layout.x,
+ width: layout.width
+ };
+ }
+
+ if (!isChangeOrder) {
+ // Keep the original growth animation if only axis order changed.
+ // Not start a new animation.
+ (isUpdate ? updateProps : initProps)(el, {
+ shape: seriesTarget
+ }, seriesModel, newIndex, null);
+ }
+
+ (isUpdate ? updateProps : initProps)(el, {
+ shape: axisTarget
+ }, axisModel, newIndex);
+ }
+}
+
+interface GetLayout {
+ (data: List, dataIndex: number, itemModel?: Model<BarDataItemOption>): RectLayout | SectorLayout
+}
+const getLayout: {
+ [key in 'cartesian2d' | 'polar']: GetLayout
+} = {
+ // itemModel is only used to get borderWidth, which is not needed
+ // when calculating bar background layout.
+ cartesian2d(data, dataIndex, itemModel?): RectLayout {
+ const layout = data.getItemLayout(dataIndex) as RectLayout;
+ const fixedLineWidth = itemModel ? getLineWidth(itemModel, layout) : 0;
+
+ // fix layout with lineWidth
+ const signX = layout.width > 0 ? 1 : -1;
+ const signY = layout.height > 0 ? 1 : -1;
+ return {
+ x: layout.x + signX * fixedLineWidth / 2,
+ y: layout.y + signY * fixedLineWidth / 2,
+ width: layout.width - signX * fixedLineWidth,
+ height: layout.height - signY * fixedLineWidth
+ };
+ },
+
+ polar(data, dataIndex, itemModel?): SectorLayout {
+ const layout = data.getItemLayout(dataIndex);
+ return {
+ cx: layout.cx,
+ cy: layout.cy,
+ r0: layout.r0,
+ r: layout.r,
+ startAngle: layout.startAngle,
+ endAngle: layout.endAngle
+ } as SectorLayout;
+ }
+};
+
+function isZeroOnPolar(layout: SectorLayout) {
+ return layout.startAngle != null
+ && layout.endAngle != null
+ && layout.startAngle === layout.endAngle;
+}
+
+function updateStyle(
+ el: BarPossiblePath,
+ data: List, dataIndex: number,
+ itemModel: Model<BarDataItemOption>,
+ layout: RectLayout | SectorLayout,
+ seriesModel: BarSeriesModel,
+ isHorizontal: boolean,
+ isPolar: boolean
+) {
+ const style = data.getItemVisual(dataIndex, 'style');
+
+ if (!isPolar) {
+ (el as Rect).setShape('r', itemModel.get(BAR_BORDER_RADIUS_QUERY) || 0);
+ }
+
+ el.useStyle(style);
+
+ el.ignore = isZeroOnPolar(layout as SectorLayout);
+
+ const cursorStyle = itemModel.getShallow('cursor');
+ cursorStyle && (el as Path).attr('cursor', cursorStyle);
+
+ if (!isPolar) {
+ const labelPositionOutside = isHorizontal
+ ? ((layout as RectLayout).height > 0 ? 'bottom' as const : 'top' as const)
+ : ((layout as RectLayout).width > 0 ? 'left' as const : 'right' as const);
+
+ setLabelStyle(
+ el, getLabelStatesModels(itemModel),
+ {
+ labelFetcher: seriesModel,
+ labelDataIndex: dataIndex,
+ defaultText: getDefaultLabel(seriesModel.getData(), dataIndex),
+ inheritColor: style.fill as ColorString,
+ defaultOutsidePosition: labelPositionOutside
+ }
+ );
+
+ const label = el.getTextContent();
+ if (label) {
+ const obj = labelInner(label);
+ obj.prevValue = obj.value;
+ obj.value = seriesModel.getRawValue(dataIndex) as ParsedValue | ParsedValue[];
+ }
+ }
+
+ const emphasisModel = itemModel.getModel(['emphasis']);
+ enableHoverEmphasis(el, emphasisModel.get('focus'), emphasisModel.get('blurScope'));
+ setStatesStylesFromModel(el, itemModel);
+
+ if (isZeroOnPolar(layout as SectorLayout)) {
+ each(el.states, (state) => {
+ if (state.style) {
+ state.style.fill = state.style.stroke = 'none';
+ }
+ });
+ }
+}
+
+// In case width or height are too small.
+function getLineWidth(
+ itemModel: Model<BarDataItemOption>,
+ rawLayout: RectLayout
+) {
+ const lineWidth = itemModel.get(BAR_BORDER_WIDTH_QUERY) || 0;
+ // width or height may be NaN for empty data
+ const width = isNaN(rawLayout.width) ? Number.MAX_VALUE : Math.abs(rawLayout.width);
+ const height = isNaN(rawLayout.height) ? Number.MAX_VALUE : Math.abs(rawLayout.height);
+ return Math.min(lineWidth, width, height);
+}
+
+class LagePathShape {
+ points: ArrayLike<number>;
+}
+interface LargePathProps extends PathProps {
+ shape?: LagePathShape
+}
+class LargePath extends Path<LargePathProps> {
+ type = 'largeBar';
+
+ shape: LagePathShape;
+;
+ __startPoint: number[];
+ __baseDimIdx: number;
+ __largeDataIndices: ArrayLike<number>;
+ __barWidth: number;
+
+ constructor(opts?: LargePathProps) {
+ super(opts);
+ }
+
+ getDefaultShape() {
+ return new LagePathShape();
+ }
+
+ buildPath(ctx: CanvasRenderingContext2D, shape: LagePathShape) {
+ // Drawing lines is more efficient than drawing
+ // a whole line or drawing rects.
+ const points = shape.points;
+ const startPoint = this.__startPoint;
+ const baseDimIdx = this.__baseDimIdx;
+
+ for (let i = 0; i < points.length; i += 2) {
+ startPoint[baseDimIdx] = points[i + baseDimIdx];
+ ctx.moveTo(startPoint[0], startPoint[1]);
+ ctx.lineTo(points[i], points[i + 1]);
+ }
+ }
+}
+
+function createLarge(
+ seriesModel: BarSeriesModel,
+ group: Group,
+ incremental?: boolean
+) {
+ // TODO support polar
+ const data = seriesModel.getData();
+ const startPoint = [];
+ const baseDimIdx = data.getLayout('valueAxisHorizontal') ? 1 : 0;
+ startPoint[1 - baseDimIdx] = data.getLayout('valueAxisStart');
+
+ const largeDataIndices = data.getLayout('largeDataIndices');
+ const barWidth = data.getLayout('barWidth');
+
+ const backgroundModel = seriesModel.getModel('backgroundStyle');
+ const drawBackground = seriesModel.get('showBackground', true);
+
+ if (drawBackground) {
+ const points = data.getLayout('largeBackgroundPoints');
+ const backgroundStartPoint: number[] = [];
+ backgroundStartPoint[1 - baseDimIdx] = data.getLayout('backgroundStart');
+
+ const bgEl = new LargePath({
+ shape: {points: points},
+ incremental: !!incremental,
+ silent: true,
+ z2: 0
+ });
+ bgEl.__startPoint = backgroundStartPoint;
+ bgEl.__baseDimIdx = baseDimIdx;
+ bgEl.__largeDataIndices = largeDataIndices;
+ bgEl.__barWidth = barWidth;
+ setLargeBackgroundStyle(bgEl, backgroundModel, data);
+ group.add(bgEl);
+ }
+
+ const el = new LargePath({
+ shape: {points: data.getLayout('largePoints')},
+ incremental: !!incremental
+ });
+ el.__startPoint = startPoint;
+ el.__baseDimIdx = baseDimIdx;
+ el.__largeDataIndices = largeDataIndices;
+ el.__barWidth = barWidth;
+ group.add(el);
+ setLargeStyle(el, seriesModel, data);
+
+ // Enable tooltip and user mouse/touch event handlers.
+ getECData(el).seriesIndex = seriesModel.seriesIndex;
+
+ if (!seriesModel.get('silent')) {
+ el.on('mousedown', largePathUpdateDataIndex);
+ el.on('mousemove', largePathUpdateDataIndex);
+ }
+}
+
+// Use throttle to avoid frequently traverse to find dataIndex.
+const largePathUpdateDataIndex = throttle(function (this: LargePath, event: ZRElementEvent) {
+ const largePath = this;
+ const dataIndex = largePathFindDataIndex(largePath, event.offsetX, event.offsetY);
+ getECData(largePath).dataIndex = dataIndex >= 0 ? dataIndex : null;
+}, 30, false);
+
+function largePathFindDataIndex(largePath: LargePath, x: number, y: number) {
+ const baseDimIdx = largePath.__baseDimIdx;
+ const valueDimIdx = 1 - baseDimIdx;
+ const points = largePath.shape.points;
+ const largeDataIndices = largePath.__largeDataIndices;
+ const barWidthHalf = Math.abs(largePath.__barWidth / 2);
+ const startValueVal = largePath.__startPoint[valueDimIdx];
+
+ _eventPos[0] = x;
+ _eventPos[1] = y;
+ const pointerBaseVal = _eventPos[baseDimIdx];
+ const pointerValueVal = _eventPos[1 - baseDimIdx];
+ const baseLowerBound = pointerBaseVal - barWidthHalf;
+ const baseUpperBound = pointerBaseVal + barWidthHalf;
+
+ for (let i = 0, len = points.length / 2; i < len; i++) {
+ const ii = i * 2;
+ const barBaseVal = points[ii + baseDimIdx];
+ const barValueVal = points[ii + valueDimIdx];
+ if (
+ barBaseVal >= baseLowerBound && barBaseVal <= baseUpperBound
+ && (
+ startValueVal <= barValueVal
+ ? (pointerValueVal >= startValueVal && pointerValueVal <= barValueVal)
+ : (pointerValueVal >= barValueVal && pointerValueVal <= startValueVal)
+ )
+ ) {
+ return largeDataIndices[i];
+ }
+ }
+
+ return -1;
+}
+
+function setLargeStyle(
+ el: LargePath,
+ seriesModel: BarSeriesModel,
+ data: List
+) {
+ const globalStyle = data.getVisual('style');
+
+ el.useStyle(extend({}, globalStyle));
+ // Use stroke instead of fill.
+ el.style.fill = null;
+ el.style.stroke = globalStyle.fill;
+ el.style.lineWidth = data.getLayout('barWidth');
+}
+
+function setLargeBackgroundStyle(
+ el: LargePath,
+ backgroundModel: Model<BarSeriesOption['backgroundStyle']>,
+ data: List
+) {
+ const borderColor = backgroundModel.get('borderColor') || backgroundModel.get('color');
+ const itemStyle = backgroundModel.getItemStyle();
+
+ el.useStyle(itemStyle);
+ el.style.fill = null;
+ el.style.stroke = borderColor;
+ el.style.lineWidth = data.getLayout('barWidth') as number;
+}
+
+function createBackgroundShape(
+ isHorizontalOrRadial: boolean,
+ layout: SectorLayout | RectLayout,
+ coord: CoordSysOfBar
+): SectorShape | RectShape {
+ if (isCoordinateSystemType<Cartesian2D>(coord, 'cartesian2d')) {
+ const rectShape = layout as RectShape;
+ const coordLayout = coord.getArea();
+ return {
+ x: isHorizontalOrRadial ? rectShape.x : coordLayout.x,
+ y: isHorizontalOrRadial ? coordLayout.y : rectShape.y,
+ width: isHorizontalOrRadial ? rectShape.width : coordLayout.width,
+ height: isHorizontalOrRadial ? coordLayout.height : rectShape.height
+ } as RectShape;
+ }
+ else {
+ const coordLayout = coord.getArea();
+ const sectorShape = layout as SectorShape;
+ return {
+ cx: coordLayout.cx,
+ cy: coordLayout.cy,
+ r0: isHorizontalOrRadial ? coordLayout.r0 : sectorShape.r0,
+ r: isHorizontalOrRadial ? coordLayout.r : sectorShape.r,
+ startAngle: isHorizontalOrRadial ? sectorShape.startAngle : 0,
+ endAngle: isHorizontalOrRadial ? sectorShape.endAngle : Math.PI * 2
+ } as SectorShape;
+ }
+}
+
+function createBackgroundEl(
+ coord: CoordSysOfBar,
+ isHorizontalOrRadial: boolean,
+ layout: SectorLayout | RectLayout
+): Rect | Sector {
+ const ElementClz = coord.type === 'polar' ? Sector : Rect;
+ return new ElementClz({
+ shape: createBackgroundShape(isHorizontalOrRadial, layout, coord) as any,
+ silent: true,
+ z2: 0
+ });
+}
+
+ChartView.registerClass(BarView);
+
+export default BarView;
diff --cc src/chart/funnel/FunnelSeries.ts
index bb358b2,0000000..c1f2f62
mode 100644,000000..100644
--- a/src/chart/funnel/FunnelSeries.ts
+++ b/src/chart/funnel/FunnelSeries.ts
@@@ -1,188 -1,0 +1,194 @@@
+/*
+* 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 zrUtil from 'zrender/src/core/util';
+import createListSimply from '../helper/createListSimply';
+import {defaultEmphasis} from '../../util/model';
+import {makeSeriesEncodeForNameBased} from '../../data/helper/sourceHelper';
+import LegendVisualProvider from '../../visual/LegendVisualProvider';
+import SeriesModel from '../../model/Series';
+import {
+ SeriesOption,
+ BoxLayoutOptionMixin,
+ HorizontalAlign,
+ LabelOption,
+ LabelLineOption,
+ ItemStyleOption,
+ OptionDataValueNumeric,
+ StatesOptionMixin,
- OptionDataItemObject
++ OptionDataItemObject,
++ LayoutOrient,
++ VerticalAlign
+} from '../../util/types';
+import GlobalModel from '../../model/Global';
+import List from '../../data/List';
+import ComponentModel from '../../model/Component';
+
+
+type FunnelLabelOption = Omit<LabelOption, 'position'> & {
+ position?: LabelOption['position']
+ | 'outer' | 'inner' | 'center' | 'rightTop' | 'rightBottom' | 'leftTop' | 'leftBottom'
+};
+
+export interface FunnelStateOption {
+ itemStyle?: ItemStyleOption
+ label?: FunnelLabelOption
+ labelLine?: LabelLineOption
+}
+
+export interface FunnelDataItemOption
+ extends FunnelStateOption, StatesOptionMixin<FunnelStateOption>,
+ OptionDataItemObject<OptionDataValueNumeric> {
+
+ itemStyle?: ItemStyleOption & {
++ width?: number | string
+ height?: number | string
+ }
+}
+
+export interface FunnelSeriesOption extends SeriesOption<FunnelStateOption>, FunnelStateOption,
+ BoxLayoutOptionMixin {
+ type?: 'funnel'
+
+ min?: number
+ max?: number
+
+ /**
+ * Absolute number or percent string
+ */
+ minSize?: number | string
+ maxSize?: number | string
+
+ sort?: 'ascending' | 'descending' | 'none'
+
++ orient?: LayoutOrient
++
+ gap?: number
+
- funnelAlign?: HorizontalAlign
++ funnelAlign?: HorizontalAlign | VerticalAlign
+
+ data?: (OptionDataValueNumeric | OptionDataValueNumeric[] | FunnelDataItemOption)[]
+}
+
+class FunnelSeriesModel extends SeriesModel<FunnelSeriesOption> {
+ static type = 'series.funnel' as const;
+ type = FunnelSeriesModel.type;
+
+ useColorPaletteOnData = true;
+
+ init(option: FunnelSeriesOption) {
+ super.init.apply(this, arguments as any);
+
+ // Enable legend selection for each data item
+ // Use a function instead of direct access because data reference may changed
+ this.legendVisualProvider = new LegendVisualProvider(
+ zrUtil.bind(this.getData, this), zrUtil.bind(this.getRawData, this)
+ );
+ // Extend labelLine emphasis
+ this._defaultLabelLine(option);
+ }
+
+ getInitialData(this: FunnelSeriesModel, option: FunnelSeriesOption, ecModel: GlobalModel): List {
+ return createListSimply(this, {
+ coordDimensions: ['value'],
+ encodeDefaulter: zrUtil.curry(makeSeriesEncodeForNameBased, this)
+ });
+ }
+
+ _defaultLabelLine(option: FunnelSeriesOption) {
+ // Extend labelLine emphasis
+ defaultEmphasis(option, 'labelLine', ['show']);
+
+ const labelLineNormalOpt = option.labelLine;
+ const labelLineEmphasisOpt = option.emphasis.labelLine;
+ // Not show label line if `label.normal.show = false`
+ labelLineNormalOpt.show = labelLineNormalOpt.show
+ && option.label.show;
+ labelLineEmphasisOpt.show = labelLineEmphasisOpt.show
+ && option.emphasis.label.show;
+ }
+
+ // Overwrite
+ getDataParams(dataIndex: number) {
+ const data = this.getData();
+ const params = super.getDataParams(dataIndex);
+ const valueDim = data.mapDimension('value');
+ const sum = data.getSum(valueDim);
+ // Percent is 0 if sum is 0
+ params.percent = !sum ? 0 : +(data.get(valueDim, dataIndex) as number / sum * 100).toFixed(2);
+
+ params.$vars.push('percent');
+ return params;
+ }
+
+ static defaultOption: FunnelSeriesOption = {
+ zlevel: 0, // 一级层叠
+ z: 2, // 二级层叠
+ legendHoverLink: true,
+ left: 80,
+ top: 60,
+ right: 80,
+ bottom: 60,
+ // width: {totalWidth} - left - right,
+ // height: {totalHeight} - top - bottom,
+
+ // 默认取数据最小最大值
+ // min: 0,
+ // max: 100,
+ minSize: '0%',
+ maxSize: '100%',
+ sort: 'descending', // 'ascending', 'descending'
++ orient: 'vertical',
+ gap: 0,
+ funnelAlign: 'center',
+ label: {
+ show: true,
+ position: 'outer'
+ // formatter: 标签文本格式器,同Tooltip.formatter,不支持异步回调
+ },
+ labelLine: {
+ show: true,
+ length: 20,
+ lineStyle: {
+ // color: 各异,
+ width: 1
+ }
+ },
+ itemStyle: {
+ // color: 各异,
+ borderColor: '#fff',
+ borderWidth: 1
+ },
+ emphasis: {
+ label: {
+ show: true
+ }
+ },
+ select: {
+ itemStyle: {
+ borderColor: '#212121'
+ }
+ }
+ };
+
+}
+
+ComponentModel.registerClass(FunnelSeriesModel);
+
+export default FunnelSeriesModel;
diff --cc src/chart/funnel/funnelLayout.ts
index 9a6e533,0000000..e309fb9
mode 100644,000000..100644
--- a/src/chart/funnel/funnelLayout.ts
+++ b/src/chart/funnel/funnelLayout.ts
@@@ -1,265 -1,0 +1,390 @@@
+/*
+* 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 layout from '../../util/layout';
+import {parsePercent, linearMap} from '../../util/number';
+import FunnelSeriesModel, { FunnelSeriesOption, FunnelDataItemOption } from './FunnelSeries';
+import ExtensionAPI from '../../ExtensionAPI';
+import List from '../../data/List';
+import GlobalModel from '../../model/Global';
+
+function getViewRect(seriesModel: FunnelSeriesModel, api: ExtensionAPI) {
+ return layout.getLayoutRect(
+ seriesModel.getBoxLayoutParams(), {
+ width: api.getWidth(),
+ height: api.getHeight()
+ }
+ );
+}
+
+function getSortedIndices(data: List, sort: FunnelSeriesOption['sort']) {
+ const valueDim = data.mapDimension('value');
+ const valueArr = data.mapArray(valueDim, function (val: number) {
+ return val;
+ });
+ const indices: number[] = [];
+ const isAscending = sort === 'ascending';
+ for (let i = 0, len = data.count(); i < len; i++) {
+ indices[i] = i;
+ }
+
+ // Add custom sortable function & none sortable opetion by "options.sort"
+ if (typeof sort === 'function') {
+ indices.sort(sort);
+ }
+ else if (sort !== 'none') {
+ indices.sort(function (a, b) {
+ return isAscending
+ ? valueArr[a] - valueArr[b]
+ : valueArr[b] - valueArr[a];
+ });
+ }
+ return indices;
+}
+
+function labelLayout(data: List) {
++ const seriesModel = data.hostModel;
++ const orient = seriesModel.get('orient');
+ data.each(function (idx) {
+ const itemModel = data.getItemModel<FunnelDataItemOption>(idx);
+ const labelModel = itemModel.getModel('label');
- const labelPosition = labelModel.get('position');
++ let labelPosition = labelModel.get('position');
+
+ const labelLineModel = itemModel.getModel('labelLine');
+
+ const layout = data.getItemLayout(idx);
+ const points = layout.points;
+
+ const isLabelInside = labelPosition === 'inner'
+ || labelPosition === 'inside' || labelPosition === 'center'
+ || labelPosition === 'insideLeft' || labelPosition === 'insideRight';
+
+ let textAlign;
+ let textX;
+ let textY;
+ let linePoints;
+
+ if (isLabelInside) {
+ if (labelPosition === 'insideLeft') {
+ textX = (points[0][0] + points[3][0]) / 2 + 5;
+ textY = (points[0][1] + points[3][1]) / 2;
+ textAlign = 'left';
+ }
+ else if (labelPosition === 'insideRight') {
+ textX = (points[1][0] + points[2][0]) / 2 - 5;
+ textY = (points[1][1] + points[2][1]) / 2;
+ textAlign = 'right';
+ }
+ else {
+ textX = (points[0][0] + points[1][0] + points[2][0] + points[3][0]) / 4;
+ textY = (points[0][1] + points[1][1] + points[2][1] + points[3][1]) / 4;
+ textAlign = 'center';
+ }
+ linePoints = [
+ [textX, textY], [textX, textY]
+ ];
+ }
+ else {
+ let x1;
+ let y1;
+ let x2;
++ let y2;
+ const labelLineLen = labelLineModel.get('length');
++ if (__DEV__) {
++ if (orient === 'vertical' && ['top', 'bottom'].indexOf(labelPosition as string) > -1) {
++ labelPosition = 'left';
++ console.warn('Position error: Funnel chart on vertical orient dose not support top and bottom.');
++ }
++ if (orient === 'horizontal' && ['left', 'right'].indexOf(labelPosition as string) > -1) {
++ labelPosition = 'bottom';
++ console.warn('Position error: Funnel chart on horizontal orient dose not support left and right.');
++ }
++ }
+ if (labelPosition === 'left') {
+ // Left side
+ x1 = (points[3][0] + points[0][0]) / 2;
+ y1 = (points[3][1] + points[0][1]) / 2;
+ x2 = x1 - labelLineLen;
+ textX = x2 - 5;
+ textAlign = 'right';
+ }
+ else if (labelPosition === 'right') {
+ // Right side
+ x1 = (points[1][0] + points[2][0]) / 2;
+ y1 = (points[1][1] + points[2][1]) / 2;
+ x2 = x1 + labelLineLen;
+ textX = x2 + 5;
+ textAlign = 'left';
+ }
++ else if (labelPosition === 'top') {
++ // Top side
++ x1 = (points[3][0] + points[0][0]) / 2;
++ y1 = (points[3][1] + points[0][1]) / 2;
++ y2 = y1 - labelLineLen;
++ textY = y2 - 5;
++ textAlign = 'center';
++ }
++ else if (labelPosition === 'bottom') {
++ // Bottom side
++ x1 = (points[1][0] + points[2][0]) / 2;
++ y1 = (points[1][1] + points[2][1]) / 2;
++ y2 = y1 + labelLineLen;
++ textY = y2 + 5;
++ textAlign = 'center';
++ }
+ else if (labelPosition === 'rightTop') {
+ // RightTop side
- x1 = points[1][0];
- y1 = points[1][1];
- x2 = x1 + labelLineLen;
- textX = x2 + 5;
- textAlign = 'top';
++ x1 = orient === 'horizontal' ? points[3][0] : points[1][0];
++ y1 = orient === 'horizontal' ? points[3][1] : points[1][1];
++ if (orient === 'horizontal') {
++ y2 = y1 - labelLineLen;
++ textY = y2 - 5;
++ textAlign = 'center';
++ }
++ else {
++ x2 = x1 + labelLineLen;
++ textX = x2 + 5;
++ textAlign = 'top';
++ }
+ }
+ else if (labelPosition === 'rightBottom') {
+ // RightBottom side
+ x1 = points[2][0];
+ y1 = points[2][1];
- x2 = x1 + labelLineLen;
- textX = x2 + 5;
- textAlign = 'bottom';
++ if (orient === 'horizontal') {
++ y2 = y1 + labelLineLen;
++ textY = y2 + 5;
++ textAlign = 'center';
++ }
++ else {
++ x2 = x1 + labelLineLen;
++ textX = x2 + 5;
++ textAlign = 'bottom';
++ }
+ }
+ else if (labelPosition === 'leftTop') {
+ // LeftTop side
+ x1 = points[0][0];
- y1 = points[1][1];
- x2 = x1 - labelLineLen;
- textX = x2 - 5;
- textAlign = 'right';
++ y1 = orient === 'horizontal' ? points[0][1] : points[1][1];
++ if (orient === 'horizontal') {
++ y2 = y1 - labelLineLen;
++ textY = y2 - 5;
++ textAlign = 'center';
++ }
++ else {
++ x2 = x1 - labelLineLen;
++ textX = x2 - 5;
++ textAlign = 'right';
++ }
+ }
+ else if (labelPosition === 'leftBottom') {
+ // LeftBottom side
- x1 = points[3][0];
- y1 = points[2][1];
- x2 = x1 - labelLineLen;
- textX = x2 - 5;
- textAlign = 'right';
++ x1 = orient === 'horizontal' ? points[1][0] : points[3][0];
++ y1 = orient === 'horizontal' ? points[1][1] : points[2][1];
++ if (orient === 'horizontal') {
++ y2 = y1 + labelLineLen;
++ textY = y2 + 5;
++ textAlign = 'center';
++ }
++ else {
++ x2 = x1 - labelLineLen;
++ textX = x2 - 5;
++ textAlign = 'right';
++ }
+ }
+ else {
- // Right side
++ // Right side or Bottom side
+ x1 = (points[1][0] + points[2][0]) / 2;
+ y1 = (points[1][1] + points[2][1]) / 2;
- x2 = x1 + labelLineLen;
- textX = x2 + 5;
- textAlign = 'left';
++ if (orient === 'horizontal') {
++ y2 = y1 + labelLineLen;
++ textY = y2 + 5;
++ textAlign = 'center';
++ }
++ else {
++ x2 = x1 + labelLineLen;
++ textX = x2 + 5;
++ textAlign = 'left';
++ }
++ }
++ if (orient === 'horizontal') {
++ x2 = x1;
++ textX = x2;
++ }
++ else {
++ y2 = y1;
++ textY = y2;
+ }
- const y2 = y1;
-
+ linePoints = [[x1, y1], [x2, y2]];
- textY = y2;
+ }
+
+ layout.label = {
+ linePoints: linePoints,
+ x: textX,
+ y: textY,
+ verticalAlign: 'middle',
+ textAlign: textAlign,
+ inside: isLabelInside
+ };
+ });
+}
+
+export default function (ecModel: GlobalModel, api: ExtensionAPI) {
+ ecModel.eachSeriesByType('funnel', function (seriesModel: FunnelSeriesModel) {
+ const data = seriesModel.getData();
+ const valueDim = data.mapDimension('value');
+ const sort = seriesModel.get('sort');
+ const viewRect = getViewRect(seriesModel, api);
++ const orient = seriesModel.get('orient');
++ const viewWidth = viewRect.width;
++ const viewHeight = viewRect.height;
+ let indices = getSortedIndices(data, sort);
++ let x = viewRect.x;
++ let y = viewRect.y;
+
- const sizeExtent = [
- parsePercent(seriesModel.get('minSize'), viewRect.width),
- parsePercent(seriesModel.get('maxSize'), viewRect.width)
- ];
++ const sizeExtent = orient === 'horizontal' ? [
++ parsePercent(seriesModel.get('minSize'), viewHeight),
++ parsePercent(seriesModel.get('maxSize'), viewHeight)
++ ] : [
++ parsePercent(seriesModel.get('minSize'), viewWidth),
++ parsePercent(seriesModel.get('maxSize'), viewWidth)
++ ];
+ const dataExtent = data.getDataExtent(valueDim);
+ let min = seriesModel.get('min');
+ let max = seriesModel.get('max');
+ if (min == null) {
+ min = Math.min(dataExtent[0], 0);
+ }
+ if (max == null) {
+ max = dataExtent[1];
+ }
+
+ const funnelAlign = seriesModel.get('funnelAlign');
+ let gap = seriesModel.get('gap');
- let itemHeight = (viewRect.height - gap * (data.count() - 1)) / data.count();
++ const viewSize = orient === 'horizontal' ? viewWidth : viewHeight;
++ let itemSize = (viewSize - gap * (data.count() - 1)) / data.count();
+
- let y = viewRect.y;
-
- const getLinePoints = function (idx: number, offY: number) {
++ const getLinePoints = function (idx: number, offset: number) {
+ // End point index is data.count() and we assign it 0
++ if (orient === 'horizontal') {
++ const val = data.get(valueDim, idx) as number || 0;
++ const itemHeight = linearMap(val, [min, max], sizeExtent, true);
++ let y0;
++ switch (funnelAlign) {
++ case 'top':
++ y0 = y;
++ break;
++ case 'center':
++ y0 = y + (viewHeight - itemHeight) / 2;
++ break;
++ case 'bottom':
++ y0 = y + (viewHeight - itemHeight);
++ break;
++ }
++
++ return [
++ [offset, y0],
++ [offset, y0 + itemHeight]
++ ];
++ }
+ const val = data.get(valueDim, idx) as number || 0;
+ const itemWidth = linearMap(val, [min, max], sizeExtent, true);
+ let x0;
+ switch (funnelAlign) {
+ case 'left':
- x0 = viewRect.x;
++ x0 = x;
+ break;
+ case 'center':
- x0 = viewRect.x + (viewRect.width - itemWidth) / 2;
++ x0 = x + (viewWidth - itemWidth) / 2;
+ break;
+ case 'right':
- x0 = viewRect.x + viewRect.width - itemWidth;
++ x0 = x + viewWidth - itemWidth;
+ break;
+ }
+ return [
- [x0, offY],
- [x0 + itemWidth, offY]
++ [x0, offset],
++ [x0 + itemWidth, offset]
+ ];
+ };
+
+ if (sort === 'ascending') {
+ // From bottom to top
- itemHeight = -itemHeight;
++ itemSize = -itemSize;
+ gap = -gap;
- y += viewRect.height;
++ if (orient === 'horizontal') {
++ x += viewWidth;
++ }
++ else {
++ y += viewHeight;
++ }
+ indices = indices.reverse();
+ }
+
+ for (let i = 0; i < indices.length; i++) {
+ const idx = indices[i];
+ const nextIdx = indices[i + 1];
-
+ const itemModel = data.getItemModel<FunnelDataItemOption>(idx);
- let height = itemModel.get(['itemStyle', 'height']);
- if (height == null) {
- height = itemHeight;
++
++ if (orient === 'horizontal') {
++ let width = itemModel.get(['itemStyle', 'width']);
++ if (width == null) {
++ width = itemSize;
++ }
++ else {
++ width = parsePercent(width, viewWidth);
++ if (sort === 'ascending') {
++ width = -width;
++ }
++ }
++
++ const start = getLinePoints(idx, x);
++ const end = getLinePoints(nextIdx, x + width);
++
++ x += width + gap;
++
++ data.setItemLayout(idx, {
++ points: start.concat(end.slice().reverse())
++ });
+ }
+ else {
- height = parsePercent(height, viewRect.height);
- if (sort === 'ascending') {
- height = -height;
++ let height = itemModel.get(['itemStyle', 'height']);
++ if (height == null) {
++ height = itemSize;
++ }
++ else {
++ height = parsePercent(height, viewHeight);
++ if (sort === 'ascending') {
++ height = -height;
++ }
+ }
- }
+
- const start = getLinePoints(idx, y);
- const end = getLinePoints(nextIdx, y + height);
++ const start = getLinePoints(idx, y);
++ const end = getLinePoints(nextIdx, y + height);
+
- y += height + gap;
++ y += height + gap;
+
- data.setItemLayout(idx, {
- points: start.concat(end.slice().reverse())
- });
++ data.setItemLayout(idx, {
++ points: start.concat(end.slice().reverse())
++ });
++ }
+ }
+
+ labelLayout(data);
+ });
+}
diff --cc src/chart/graph/GraphSeries.ts
index 1623c9c,0000000..59bd859
mode 100644,000000..100644
--- a/src/chart/graph/GraphSeries.ts
+++ b/src/chart/graph/GraphSeries.ts
@@@ -1,505 -1,0 +1,512 @@@
+/*
+* 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 List from '../../data/List';
+import * as zrUtil from 'zrender/src/core/util';
+import {defaultEmphasis} from '../../util/model';
+import Model from '../../model/Model';
+import createGraphFromNodeEdge from '../helper/createGraphFromNodeEdge';
+import LegendVisualProvider from '../../visual/LegendVisualProvider';
+import {
+ SeriesOption,
+ SeriesOnCartesianOptionMixin,
+ SeriesOnPolarOptionMixin,
+ SeriesOnCalendarOptionMixin,
+ SeriesOnGeoOptionMixin,
+ SeriesOnSingleOptionMixin,
+ OptionDataValue,
+ RoamOptionMixin,
+ LabelOption,
+ ItemStyleOption,
+ LineStyleOption,
+ SymbolOptionMixin,
+ BoxLayoutOptionMixin,
+ LabelFormatterCallback,
+ Dictionary,
+ LineLabelOption,
+ StatesOptionMixin,
+ GraphEdgeItemObject,
+ OptionDataValueNumeric
+} from '../../util/types';
+import SeriesModel from '../../model/Series';
+import Graph from '../../data/Graph';
+import GlobalModel from '../../model/Global';
+import { VectorArray } from 'zrender/src/core/vector';
+import { ForceLayoutInstance } from './forceLayout';
+import { LineDataVisual } from '../../visual/commonVisualTypes';
+import { createTooltipMarkup } from '../../component/tooltip/tooltipMarkup';
+import { defaultSeriesFormatTooltip } from '../../component/tooltip/seriesFormatTooltip';
++import {initCurvenessList, createEdgeMapForCurveness} from '../helper/multipleGraphEdgeHelper';
++
+
+type GraphDataValue = OptionDataValue | OptionDataValue[];
+
+interface GraphEdgeLineStyleOption extends LineStyleOption {
+ curveness?: number
+}
+
+export interface GraphNodeStateOption {
+ itemStyle?: ItemStyleOption
+ label?: LabelOption
+}
+
+interface ExtraNodeStateOption {
+ emphasis?: {
+ focus?: 'adjacency'
+ scale?: boolean
+ }
+}
+
+interface ExtraEdgeStateOption {
+ emphasis?: {
+ focus?: 'adjacency'
+ }
+}
+
+export interface GraphNodeItemOption extends SymbolOptionMixin, GraphNodeStateOption,
+ GraphNodeStateOption, StatesOptionMixin<GraphNodeStateOption, ExtraNodeStateOption> {
+
+ id?: string
+ name?: string
+ value?: GraphDataValue
+
+ /**
+ * Fixed x position
+ */
+ x?: number
+ /**
+ * Fixed y position
+ */
+ y?: number
+
+ /**
+ * If this node is fixed during force layout.
+ */
+ fixed?: boolean
+
+ /**
+ * Index or name of category
+ */
+ category?: number | string
+
+ draggable?: boolean
+}
+
+export interface GraphEdgeStateOption {
+ lineStyle?: GraphEdgeLineStyleOption
+ label?: LineLabelOption
+}
+export interface GraphEdgeItemOption extends
+ GraphEdgeStateOption,
+ StatesOptionMixin<GraphEdgeStateOption, ExtraEdgeStateOption>,
+ GraphEdgeItemObject<OptionDataValueNumeric> {
+
+ value?: number
+
+ /**
+ * Symbol of both line ends
+ */
+ symbol?: string | string[]
+
+ symbolSize?: number | number[]
+
+ ignoreForceLayout?: boolean
+}
+
+export interface GraphCategoryItemOption extends SymbolOptionMixin,
+ GraphNodeStateOption, StatesOptionMixin<GraphNodeStateOption> {
+ name?: string
+
+ value?: OptionDataValue
+}
+
+export interface GraphSeriesOption extends SeriesOption,
+ SeriesOnCartesianOptionMixin, SeriesOnPolarOptionMixin, SeriesOnCalendarOptionMixin,
+ SeriesOnGeoOptionMixin, SeriesOnSingleOptionMixin,
+ SymbolOptionMixin,
+ RoamOptionMixin,
+ BoxLayoutOptionMixin {
+
+ type?: 'graph'
+
+ coordinateSystem?: string
+
+ legendHoverLink?: boolean
+
+ layout?: 'none' | 'force' | 'circular'
+
+ data?: (GraphNodeItemOption | GraphDataValue)[]
+ nodes?: (GraphNodeItemOption | GraphDataValue)[]
+
+ edges?: GraphEdgeItemOption[]
+ links?: GraphEdgeItemOption[]
+
+ categories?: GraphCategoryItemOption[]
+
+ focusNodeAdjacency?: boolean
+
+ /**
+ * Symbol size scale ratio in roam
+ */
+ nodeScaleRatio?: 0.6,
+
+ draggable?: boolean
+
+ edgeSymbol?: string | string[]
+ edgeSymbolSize?: number | number[]
+
+ edgeLabel?: LineLabelOption & {
+ formatter?: LabelFormatterCallback | string
+ }
+ label?: LabelOption & {
+ formatter?: LabelFormatterCallback | string
+ }
+
+ itemStyle?: ItemStyleOption
+ lineStyle?: GraphEdgeLineStyleOption
+
+ emphasis?: {
+ focus?: GraphNodeItemOption['emphasis']['focus']
+ scale?: boolean
+ label?: LabelOption
+ edgeLabel?: LabelOption
+ itemStyle?: ItemStyleOption
+ lineStyle?: LineStyleOption
+ }
+
+ blur?: {
+ label?: LabelOption
+ edgeLabel?: LabelOption
+ itemStyle?: ItemStyleOption
+ lineStyle?: LineStyleOption
+ }
+
+ select?: {
+ label?: LabelOption
+ edgeLabel?: LabelOption
+ itemStyle?: ItemStyleOption
+ lineStyle?: LineStyleOption
+ }
+
+ // Configuration of circular layout
+ circular?: {
+ rotateLabel?: boolean
+ }
+
+ // Configuration of force directed layout
+ force?: {
+ initLayout?: 'circular' | 'none'
+ // Node repulsion. Can be an array to represent range.
+ repulsion?: number | number[]
+ gravity?: number
+ // Initial friction
+ friction?: number
+
+ // Edge length. Can be an array to represent range.
+ edgeLength?: number | number[]
+
+ layoutAnimation?: boolean
+ }
+}
+
+class GraphSeriesModel extends SeriesModel<GraphSeriesOption> {
+ static readonly type = 'series.graph';
+ readonly type = GraphSeriesModel.type;
+
+ private _categoriesData: List;
+ private _categoriesModels: Model<GraphCategoryItemOption>[];
+
+ /**
+ * Preserved points during layouting
+ */
+ preservedPoints?: Dictionary<VectorArray>;
+
+ forceLayout?: ForceLayoutInstance;
+
+ hasSymbolVisual = true;
+
+ init(option: GraphSeriesOption) {
+ super.init.apply(this, arguments as any);
+
+ const self = this;
+ function getCategoriesData() {
+ return self._categoriesData;
+ }
+ // Provide data for legend select
+ this.legendVisualProvider = new LegendVisualProvider(
+ getCategoriesData, getCategoriesData
+ );
+
+ this.fillDataTextStyle(option.edges || option.links);
+
+ this._updateCategoriesData();
+ }
+
+ mergeOption(option: GraphSeriesOption) {
+ super.mergeOption.apply(this, arguments as any);
+
+ this.fillDataTextStyle(option.edges || option.links);
+
+ this._updateCategoriesData();
+ }
+
+ mergeDefaultAndTheme(option: GraphSeriesOption) {
+ super.mergeDefaultAndTheme.apply(this, arguments as any);
+ defaultEmphasis(option, 'edgeLabel', ['show']);
+ }
+
+ getInitialData(option: GraphSeriesOption, ecModel: GlobalModel): List {
+ const edges = option.edges || option.links || [];
+ const nodes = option.data || option.nodes || [];
+ const self = this;
+
+ if (nodes && edges) {
- return createGraphFromNodeEdge(nodes as GraphNodeItemOption[], edges, this, true, beforeLink).data;
++ // auto curveness
++ initCurvenessList(this);
++ const graph = createGraphFromNodeEdge(nodes as GraphNodeItemOption[], edges, this, true, beforeLink);
++ zrUtil.each(graph.edges, function (edge) {
++ createEdgeMapForCurveness(edge.node1, edge.node2, this, edge.dataIndex);
++ }, this);
++ return graph.data;
+ }
+
+ function beforeLink(nodeData: List, edgeData: List) {
+ // Overwrite nodeData.getItemModel to
+ nodeData.wrapMethod('getItemModel', function (model) {
+ const categoriesModels = self._categoriesModels;
+ const categoryIdx = model.getShallow('category');
+ const categoryModel = categoriesModels[categoryIdx];
+ if (categoryModel) {
+ categoryModel.parentModel = model.parentModel;
+ model.parentModel = categoryModel;
+ }
+ return model;
+ });
+
+ // TODO Inherit resolveParentPath by default in Model#getModel?
+ const oldGetModel = Model.prototype.getModel;
+ function newGetModel(this: Model, path: any, parentModel?: Model) {
+ const model = oldGetModel.call(this, path, parentModel);
+ model.resolveParentPath = resolveParentPath;
+ return model;
+ }
+
+ edgeData.wrapMethod('getItemModel', function (model: Model) {
+ model.resolveParentPath = resolveParentPath;
+ model.getModel = newGetModel;
+ return model;
+ });
+
+ function resolveParentPath(this: Model, pathArr: readonly string[]): string[] {
+ if (pathArr && (pathArr[0] === 'label' || pathArr[1] === 'label')) {
+ const newPathArr = pathArr.slice();
+ if (pathArr[0] === 'label') {
+ newPathArr[0] = 'edgeLabel';
+ }
+ else if (pathArr[1] === 'label') {
+ newPathArr[1] = 'edgeLabel';
+ }
+ return newPathArr;
+ }
+ return pathArr as string[];
+ }
+ }
+ }
+
+ getGraph(): Graph {
+ return this.getData().graph;
+ }
+
+ getEdgeData() {
+ return this.getGraph().edgeData as List<GraphSeriesModel, LineDataVisual>;
+ }
+
+ getCategoriesData(): List {
+ return this._categoriesData;
+ }
+
+ formatTooltip(
+ dataIndex: number,
+ multipleSeries: boolean,
+ dataType: string
+ ) {
+ if (dataType === 'edge') {
+ const nodeData = this.getData();
+ const params = this.getDataParams(dataIndex, dataType);
+ const edge = nodeData.graph.getEdgeByIndex(dataIndex);
+ const sourceName = nodeData.getName(edge.node1.dataIndex);
+ const targetName = nodeData.getName(edge.node2.dataIndex);
+
+ const nameArr = [];
+ sourceName != null && nameArr.push(sourceName);
+ targetName != null && nameArr.push(targetName);
+
+ return createTooltipMarkup('nameValue', {
+ name: nameArr.join(' > '),
+ value: params.value,
+ noValue: params.value == null
+ });
+ }
+ // dataType === 'node' or empty
+ const nodeMarkup = defaultSeriesFormatTooltip({
+ series: this,
+ dataIndex: dataIndex,
+ multipleSeries: multipleSeries
+ });
+ return nodeMarkup;
+ }
+
+ _updateCategoriesData() {
+ const categories = zrUtil.map(this.option.categories || [], function (category) {
+ // Data must has value
+ return category.value != null ? category : zrUtil.extend({
+ value: 0
+ }, category);
+ });
+ const categoriesData = new List(['value'], this);
+ categoriesData.initData(categories);
+
+ this._categoriesData = categoriesData;
+
+ this._categoriesModels = categoriesData.mapArray(function (idx) {
+ return categoriesData.getItemModel(idx);
+ });
+ }
+
+ setZoom(zoom: number) {
+ this.option.zoom = zoom;
+ }
+
+ setCenter(center: number[]) {
+ this.option.center = center;
+ }
+
+ isAnimationEnabled() {
+ return super.isAnimationEnabled()
+ // Not enable animation when do force layout
+ && !(this.get('layout') === 'force' && this.get(['force', 'layoutAnimation']));
+ }
+
+ static defaultOption: GraphSeriesOption = {
+ zlevel: 0,
+ z: 2,
+
+ coordinateSystem: 'view',
+
+ // Default option for all coordinate systems
+ // xAxisIndex: 0,
+ // yAxisIndex: 0,
+ // polarIndex: 0,
+ // geoIndex: 0,
+
+ legendHoverLink: true,
+
+ layout: null,
+
+ focusNodeAdjacency: false,
+
+ // Configuration of circular layout
+ circular: {
+ rotateLabel: false
+ },
+ // Configuration of force directed layout
+ force: {
+ initLayout: null,
+ // Node repulsion. Can be an array to represent range.
+ repulsion: [0, 50],
+ gravity: 0.1,
+ // Initial friction
+ friction: 0.6,
+
+ // Edge length. Can be an array to represent range.
+ edgeLength: 30,
+
+ layoutAnimation: true
+ },
+
+ left: 'center',
+ top: 'center',
+ // right: null,
+ // bottom: null,
+ // width: '80%',
+ // height: '80%',
+
+ symbol: 'circle',
+ symbolSize: 10,
+
+ edgeSymbol: ['none', 'none'],
+ edgeSymbolSize: 10,
+ edgeLabel: {
+ position: 'middle',
+ distance: 5
+ },
+
+ draggable: false,
+
+ roam: false,
+
+ // Default on center of graph
+ center: null,
+
+ zoom: 1,
+ // Symbol size scale ratio in roam
+ nodeScaleRatio: 0.6,
+
+ // cursor: null,
+
+ // categories: [],
+
+ // data: []
+ // Or
+ // nodes: []
+ //
+ // links: []
+ // Or
+ // edges: []
+
+ label: {
+ show: false,
+ formatter: '{b}'
+ },
+
+ itemStyle: {},
+
+ lineStyle: {
+ color: '#aaa',
+ width: 1,
- curveness: 0,
+ opacity: 0.5
+ },
+ emphasis: {
+ scale: true,
+ label: {
+ show: true
+ }
+ },
+
+ select: {
+ itemStyle: {
+ borderColor: '#212121'
+ }
+ }
+ };
+}
+
+SeriesModel.registerClass(GraphSeriesModel);
+
+export default GraphSeriesModel;
diff --cc src/chart/graph/circularLayoutHelper.ts
index 4364763,0000000..a81538c
mode 100644,000000..100644
--- a/src/chart/graph/circularLayoutHelper.ts
+++ b/src/chart/graph/circularLayoutHelper.ts
@@@ -1,167 -1,0 +1,173 @@@
+/*
+* 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 vec2 from 'zrender/src/core/vector';
+import {getSymbolSize, getNodeGlobalScale} from './graphHelper';
+import GraphSeriesModel, { GraphEdgeItemOption } from './GraphSeries';
+import Graph from '../../data/Graph';
+import List from '../../data/List';
++import * as zrUtil from 'zrender/src/core/util';
++import {getCurvenessForEdge} from '../helper/multipleGraphEdgeHelper';
+
+const PI = Math.PI;
+
+const _symbolRadiansHalf: number[] = [];
+
+/**
+ * `basedOn` can be:
+ * 'value':
+ * This layout is not accurate and have same bad case. For example,
+ * if the min value is very smaller than the max value, the nodes
+ * with the min value probably overlap even though there is enough
+ * space to layout them. So we only use this approach in the as the
+ * init layout of the force layout.
+ * FIXME
+ * Probably we do not need this method any more but use
+ * `basedOn: 'symbolSize'` in force layout if
+ * delay its init operations to GraphView.
+ * 'symbolSize':
+ * This approach work only if all of the symbol size calculated.
+ * That is, the progressive rendering is not applied to graph.
+ * FIXME
+ * If progressive rendering is applied to graph some day,
+ * probably we have to use `basedOn: 'value'`.
+ */
+export function circularLayout(
+ seriesModel: GraphSeriesModel,
+ basedOn: 'value' | 'symbolSize'
+) {
+ const coordSys = seriesModel.coordinateSystem;
+ if (coordSys && coordSys.type !== 'view') {
+ return;
+ }
+
+ const rect = coordSys.getBoundingRect();
+
+ const nodeData = seriesModel.getData();
+ const graph = nodeData.graph;
+
+ const cx = rect.width / 2 + rect.x;
+ const cy = rect.height / 2 + rect.y;
+ const r = Math.min(rect.width, rect.height) / 2;
+ const count = nodeData.count();
+
+ nodeData.setLayout({
+ cx: cx,
+ cy: cy
+ });
+
+ if (!count) {
+ return;
+ }
+
+ _layoutNodesBasedOn[basedOn](seriesModel, graph, nodeData, r, cx, cy, count);
+
- graph.eachEdge(function (edge) {
- let curveness = edge.getModel<GraphEdgeItemOption>().get(['lineStyle', 'curveness']) || 0;
++ graph.eachEdge(function (edge, index) {
++ let curveness = zrUtil.retrieve3(
++ edge.getModel<GraphEdgeItemOption>().get(['lineStyle', 'curveness']),
++ getCurvenessForEdge(edge, seriesModel, index),
++ 0
++ );
+ const p1 = vec2.clone(edge.node1.getLayout());
+ const p2 = vec2.clone(edge.node2.getLayout());
+ let cp1;
+ const x12 = (p1[0] + p2[0]) / 2;
+ const y12 = (p1[1] + p2[1]) / 2;
+ if (+curveness) {
+ curveness *= 3;
+ cp1 = [
+ cx * curveness + x12 * (1 - curveness),
+ cy * curveness + y12 * (1 - curveness)
+ ];
+ }
+ edge.setLayout([p1, p2, cp1]);
+ });
+}
+
+interface LayoutNode {
+ (
+ seriesModel: GraphSeriesModel,
+ graph: Graph,
+ nodeData: List,
+ r: number,
+ cx: number,
+ cy: number,
+ count: number
+ ): void
+}
+
+const _layoutNodesBasedOn: Record<'value' | 'symbolSize', LayoutNode> = {
+
+ value(seriesModel, graph, nodeData, r, cx, cy, count) {
+ let angle = 0;
+ const sum = nodeData.getSum('value');
+ const unitAngle = Math.PI * 2 / (sum || count);
+
+ graph.eachNode(function (node) {
+ const value = node.getValue('value') as number;
+ const radianHalf = unitAngle * (sum ? value : 1) / 2;
+
+ angle += radianHalf;
+ node.setLayout([
+ r * Math.cos(angle) + cx,
+ r * Math.sin(angle) + cy
+ ]);
+ angle += radianHalf;
+ });
+ },
+
+ symbolSize(seriesModel, graph, nodeData, r, cx, cy, count) {
+ let sumRadian = 0;
+ _symbolRadiansHalf.length = count;
+
+ const nodeScale = getNodeGlobalScale(seriesModel);
+
+ graph.eachNode(function (node) {
+ let symbolSize = getSymbolSize(node);
+
+ // Normally this case will not happen, but we still add
+ // some the defensive code (2px is an arbitrary value).
+ isNaN(symbolSize) && (symbolSize = 2);
+ symbolSize < 0 && (symbolSize = 0);
+
+ symbolSize *= nodeScale;
+
+ let symbolRadianHalf = Math.asin(symbolSize / 2 / r);
+ // when `symbolSize / 2` is bigger than `r`.
+ isNaN(symbolRadianHalf) && (symbolRadianHalf = PI / 2);
+ _symbolRadiansHalf[node.dataIndex] = symbolRadianHalf;
+ sumRadian += symbolRadianHalf * 2;
+ });
+
+ const halfRemainRadian = (2 * PI - sumRadian) / count / 2;
+
+ let angle = 0;
+ graph.eachNode(function (node) {
+ const radianHalf = halfRemainRadian + _symbolRadiansHalf[node.dataIndex];
+
+ angle += radianHalf;
+ node.setLayout([
+ r * Math.cos(angle) + cx,
+ r * Math.sin(angle) + cy
+ ]);
+ angle += radianHalf;
+ });
+ }
+};
diff --cc src/chart/graph/edgeVisual.ts
index ba298b1,0000000..a134794
mode 100644,000000..100644
--- a/src/chart/graph/edgeVisual.ts
+++ b/src/chart/graph/edgeVisual.ts
@@@ -1,88 -1,0 +1,80 @@@
+/*
+* 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 GlobalModel from '../../model/Global';
+import GraphSeriesModel, { GraphEdgeItemOption } from './GraphSeries';
- import { DefaultDataVisual } from '../../data/List';
+import { extend } from 'zrender/src/core/util';
+
+function normalize(a: string | string[]): string[];
+function normalize(a: number | number[]): number[];
+function normalize(a: string | number | (string | number)[]): (string | number)[] {
+ if (!(a instanceof Array)) {
+ a = [a, a];
+ }
+ return a;
+}
+
- interface EdgeLineDataVisual extends DefaultDataVisual {
- fromSymbol: string
- toSymbol: string
- fromSymbolSize: number
- toSymbolSize: number
- }
-
+export default function (ecModel: GlobalModel) {
+ ecModel.eachSeriesByType('graph', function (seriesModel: GraphSeriesModel) {
+ const graph = seriesModel.getGraph();
+ const edgeData = seriesModel.getEdgeData();
+ const symbolType = normalize(seriesModel.get('edgeSymbol'));
+ const symbolSize = normalize(seriesModel.get('edgeSymbolSize'));
+
+ // const colorQuery = ['lineStyle', 'color'] as const;
+ // const opacityQuery = ['lineStyle', 'opacity'] as const;
+
+ edgeData.setVisual('fromSymbol', symbolType && symbolType[0]);
+ edgeData.setVisual('toSymbol', symbolType && symbolType[1]);
+ edgeData.setVisual('fromSymbolSize', symbolSize && symbolSize[0]);
+ edgeData.setVisual('toSymbolSize', symbolSize && symbolSize[1]);
+
+ edgeData.setVisual('style', seriesModel.getModel('lineStyle').getLineStyle());
+
+ edgeData.each(function (idx) {
+ const itemModel = edgeData.getItemModel<GraphEdgeItemOption>(idx);
+ const edge = graph.getEdgeByIndex(idx);
+ const symbolType = normalize(itemModel.getShallow('symbol', true));
+ const symbolSize = normalize(itemModel.getShallow('symbolSize', true));
+ // Edge visual must after node visual
+ const style = itemModel.getModel('lineStyle').getLineStyle();
+
+ const existsStyle = edgeData.ensureUniqueItemVisual(idx, 'style');
+ extend(existsStyle, style);
+
+ switch (existsStyle.stroke) {
+ case 'source': {
+ const nodeStyle = edge.node1.getVisual('style');
+ existsStyle.stroke = nodeStyle && nodeStyle.fill;
+ break;
+ }
+ case 'target': {
+ const nodeStyle = edge.node2.getVisual('style');
+ existsStyle.stroke = nodeStyle && nodeStyle.fill;
+ break;
+ }
+ }
+
+ symbolType[0] && edge.setVisual('fromSymbol', symbolType[0]);
+ symbolType[1] && edge.setVisual('toSymbol', symbolType[1]);
+ symbolSize[0] && edge.setVisual('fromSymbolSize', symbolSize[0]);
+ symbolSize[1] && edge.setVisual('toSymbolSize', symbolSize[1]);
+ });
+ });
+}
diff --cc src/chart/graph/forceLayout.ts
index 1fbbed8,0000000..70a1d1d
mode 100644,000000..100644
--- a/src/chart/graph/forceLayout.ts
+++ b/src/chart/graph/forceLayout.ts
@@@ -1,161 -1,0 +1,167 @@@
+/*
+* 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 {forceLayout} from './forceHelper';
+import {simpleLayout} from './simpleLayoutHelper';
+import {circularLayout} from './circularLayoutHelper';
+import {linearMap} from '../../util/number';
+import * as vec2 from 'zrender/src/core/vector';
+import * as zrUtil from 'zrender/src/core/util';
+import GlobalModel from '../../model/Global';
+import GraphSeriesModel, { GraphNodeItemOption, GraphEdgeItemOption } from './GraphSeries';
++import {getCurvenessForEdge} from '../helper/multipleGraphEdgeHelper';
+
+export interface ForceLayoutInstance {
+ step(cb: (stopped: boolean) => void): void
+ warmUp(): void
+ setFixed(idx: number): void
+ setUnfixed(idx: number): void
+}
+
+
+export default function (ecModel: GlobalModel) {
+ ecModel.eachSeriesByType('graph', function (graphSeries: GraphSeriesModel) {
+ const coordSys = graphSeries.coordinateSystem;
+ if (coordSys && coordSys.type !== 'view') {
+ return;
+ }
+ if (graphSeries.get('layout') === 'force') {
+ const preservedPoints = graphSeries.preservedPoints || {};
+ const graph = graphSeries.getGraph();
+ const nodeData = graph.data;
+ const edgeData = graph.edgeData;
+ const forceModel = graphSeries.getModel('force');
+ const initLayout = forceModel.get('initLayout');
+ if (graphSeries.preservedPoints) {
+ nodeData.each(function (idx) {
+ const id = nodeData.getId(idx);
+ nodeData.setItemLayout(idx, preservedPoints[id] || [NaN, NaN]);
+ });
+ }
+ else if (!initLayout || initLayout === 'none') {
+ simpleLayout(graphSeries);
+ }
+ else if (initLayout === 'circular') {
+ circularLayout(graphSeries, 'value');
+ }
+
+ const nodeDataExtent = nodeData.getDataExtent('value');
+ const edgeDataExtent = edgeData.getDataExtent('value');
+ // let edgeDataExtent = edgeData.getDataExtent('value');
+ const repulsion = forceModel.get('repulsion');
+ const edgeLength = forceModel.get('edgeLength');
+ const repulsionArr = zrUtil.isArray(repulsion)
+ ? repulsion : [repulsion, repulsion];
+ let edgeLengthArr = zrUtil.isArray(edgeLength)
+ ? edgeLength : [edgeLength, edgeLength];
+
+ // Larger value has smaller length
+ edgeLengthArr = [edgeLengthArr[1], edgeLengthArr[0]];
+
+ const nodes = nodeData.mapArray('value', function (value: number, idx) {
+ const point = nodeData.getItemLayout(idx) as number[];
+ let rep = linearMap(value, nodeDataExtent, repulsionArr);
+ if (isNaN(rep)) {
+ rep = (repulsionArr[0] + repulsionArr[1]) / 2;
+ }
+ return {
+ w: rep,
+ rep: rep,
+ fixed: nodeData.getItemModel<GraphNodeItemOption>(idx).get('fixed'),
+ p: (!point || isNaN(point[0]) || isNaN(point[1])) ? null : point
+ };
+ });
+ const edges = edgeData.mapArray('value', function (value: number, idx) {
+ const edge = graph.getEdgeByIndex(idx);
+ let d = linearMap(value, edgeDataExtent, edgeLengthArr);
+ if (isNaN(d)) {
+ d = (edgeLengthArr[0] + edgeLengthArr[1]) / 2;
+ }
+ const edgeModel = edge.getModel<GraphEdgeItemOption>();
++ const curveness = zrUtil.retrieve3(
++ edge.getModel<GraphEdgeItemOption>().get(['lineStyle', 'curveness']),
++ -getCurvenessForEdge(edge, graphSeries, idx, true),
++ 0
++ );
+ return {
+ n1: nodes[edge.node1.dataIndex],
+ n2: nodes[edge.node2.dataIndex],
+ d: d,
- curveness: edgeModel.get(['lineStyle', 'curveness']) || 0,
++ curveness,
+ ignoreForceLayout: edgeModel.get('ignoreForceLayout')
+ };
+ });
+
+ // let coordSys = graphSeries.coordinateSystem;
+ const rect = coordSys.getBoundingRect();
+ const forceInstance = forceLayout(nodes, edges, {
+ rect: rect,
+ gravity: forceModel.get('gravity'),
+ friction: forceModel.get('friction')
+ });
+ forceInstance.beforeStep(function (nodes, edges) {
+ for (let i = 0, l = nodes.length; i < l; i++) {
+ if (nodes[i].fixed) {
+ // Write back to layout instance
+ vec2.copy(
+ nodes[i].p,
+ graph.getNodeByIndex(i).getLayout() as number[]
+ );
+ }
+ }
+ });
+ forceInstance.afterStep(function (nodes, edges, stopped) {
+ for (let i = 0, l = nodes.length; i < l; i++) {
+ if (!nodes[i].fixed) {
+ graph.getNodeByIndex(i).setLayout(nodes[i].p);
+ }
+ preservedPoints[nodeData.getId(i)] = nodes[i].p;
+ }
+ for (let i = 0, l = edges.length; i < l; i++) {
+ const e = edges[i];
+ const edge = graph.getEdgeByIndex(i);
+ const p1 = e.n1.p;
+ const p2 = e.n2.p;
+ let points = edge.getLayout() as number[][];
+ points = points ? points.slice() : [];
+ points[0] = points[0] || [];
+ points[1] = points[1] || [];
+ vec2.copy(points[0], p1);
+ vec2.copy(points[1], p2);
+ if (+e.curveness) {
+ points[2] = [
+ (p1[0] + p2[0]) / 2 - (p1[1] - p2[1]) * e.curveness,
+ (p1[1] + p2[1]) / 2 - (p2[0] - p1[0]) * e.curveness
+ ];
+ }
+ edge.setLayout(points);
+ }
+ });
+ graphSeries.forceLayout = forceInstance;
+ graphSeries.preservedPoints = preservedPoints;
+
+ // Step to get the layout
+ forceInstance.step();
+ }
+ else {
+ // Remove prev injected forceLayout instance
+ graphSeries.forceLayout = null;
+ }
+ });
+}
diff --cc src/chart/graph/simpleLayout.ts
index 945b138,0000000..8b6d785
mode 100644,000000..100644
--- a/src/chart/graph/simpleLayout.ts
+++ b/src/chart/graph/simpleLayout.ts
@@@ -1,63 -1,0 +1,63 @@@
+/*
+* 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 {simpleLayout, simpleLayoutEdge} from './simpleLayoutHelper';
+import GlobalModel from '../../model/Global';
+import ExtensionAPI from '../../ExtensionAPI';
+import GraphSeriesModel from './GraphSeries';
+
+export default function (ecModel: GlobalModel, api: ExtensionAPI) {
+ ecModel.eachSeriesByType('graph', function (seriesModel: GraphSeriesModel) {
+ const layout = seriesModel.get('layout');
+ const coordSys = seriesModel.coordinateSystem;
+ if (coordSys && coordSys.type !== 'view') {
+ const data = seriesModel.getData();
+
+ let dimensions: string[] = [];
+ each(coordSys.dimensions, function (coordDim) {
+ dimensions = dimensions.concat(data.mapDimensionsAll(coordDim));
+ });
+
+ for (let dataIndex = 0; dataIndex < data.count(); dataIndex++) {
+ const value = [];
+ let hasValue = false;
+ for (let i = 0; i < dimensions.length; i++) {
+ const val = data.get(dimensions[i], dataIndex) as number;
+ if (!isNaN(val)) {
+ hasValue = true;
+ }
+ value.push(val);
+ }
+ if (hasValue) {
+ data.setItemLayout(dataIndex, coordSys.dataToPoint(value));
+ }
+ else {
+ // Also {Array.<number>}, not undefined to avoid if...else... statement
+ data.setItemLayout(dataIndex, [NaN, NaN]);
+ }
+ }
+
- simpleLayoutEdge(data.graph);
++ simpleLayoutEdge(data.graph, seriesModel);
+ }
+ else if (!layout || layout === 'none') {
+ simpleLayout(seriesModel);
+ }
+ });
+}
diff --cc src/chart/graph/simpleLayoutHelper.ts
index 723f153,0000000..73644ae
mode 100644,000000..100644
--- a/src/chart/graph/simpleLayoutHelper.ts
+++ b/src/chart/graph/simpleLayoutHelper.ts
@@@ -1,53 -1,0 +1,60 @@@
+/*
+* 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 vec2 from 'zrender/src/core/vector';
+import GraphSeriesModel, { GraphNodeItemOption, GraphEdgeItemOption } from './GraphSeries';
+import Graph from '../../data/Graph';
++import * as zrUtil from 'zrender/src/core/util';
++import {getCurvenessForEdge} from '../helper/multipleGraphEdgeHelper';
++
+
+export function simpleLayout(seriesModel: GraphSeriesModel) {
+ const coordSys = seriesModel.coordinateSystem;
+ if (coordSys && coordSys.type !== 'view') {
+ return;
+ }
+ const graph = seriesModel.getGraph();
+
+ graph.eachNode(function (node) {
+ const model = node.getModel<GraphNodeItemOption>();
+ node.setLayout([+model.get('x'), +model.get('y')]);
+ });
+
- simpleLayoutEdge(graph);
++ simpleLayoutEdge(graph, seriesModel);
+}
+
- export function simpleLayoutEdge(graph: Graph) {
- graph.eachEdge(function (edge, idx) {
- const curveness = edge.getModel<GraphEdgeItemOption>().get(['lineStyle', 'curveness']) || 0;
++export function simpleLayoutEdge(graph: Graph, seriesModel: GraphSeriesModel) {
++ graph.eachEdge(function (edge, index) {
++ const curveness = zrUtil.retrieve3(
++ edge.getModel<GraphEdgeItemOption>().get(['lineStyle', 'curveness']),
++ -getCurvenessForEdge(edge, seriesModel, index, true),
++ 0
++ );
+ const p1 = vec2.clone(edge.node1.getLayout());
+ const p2 = vec2.clone(edge.node2.getLayout());
+ const points = [p1, p2];
+ if (+curveness) {
+ points.push([
+ (p1[0] + p2[0]) / 2 - (p1[1] - p2[1]) * curveness,
+ (p1[1] + p2[1]) / 2 - (p2[0] - p1[0]) * curveness
+ ]);
+ }
+ edge.setLayout(points);
+ });
- }
++}
diff --cc src/chart/helper/Line.ts
index dbc09c0,0000000..138f8c8
mode 100644,000000..100644
--- a/src/chart/helper/Line.ts
+++ b/src/chart/helper/Line.ts
@@@ -1,463 -1,0 +1,490 @@@
+/*
+* 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 zrUtil from 'zrender/src/core/util';
+import * as vector from 'zrender/src/core/vector';
+import * as symbolUtil from '../../util/symbol';
+import ECLinePath from './LinePath';
+import * as graphic from '../../util/graphic';
+import { enableHoverEmphasis, enterEmphasis, leaveEmphasis, SPECIAL_STATES } from '../../util/states';
- import {createTextStyle, getLabelStatesModels, setLabelStyle} from '../../label/labelStyle';
++import {getLabelStatesModels, setLabelStyle} from '../../label/labelStyle';
+import {round} from '../../util/number';
+import List from '../../data/List';
+import { ZRTextAlign, ZRTextVerticalAlign, LineLabelOption, ColorString } from '../../util/types';
+import SeriesModel from '../../model/Series';
+import type { LineDrawSeriesScope, LineDrawModelOption } from './LineDraw';
+
+import { TextStyleProps } from 'zrender/src/graphic/Text';
+import { LineDataVisual } from '../../visual/commonVisualTypes';
- import { setLabelLineStyle } from '../../label/labelGuideHelper';
+import Model from '../../model/Model';
+
+const SYMBOL_CATEGORIES = ['fromSymbol', 'toSymbol'] as const;
+
+type ECSymbol = ReturnType<typeof createSymbol>;
+
++type LineECSymbol = ECSymbol & {
++ __specifiedRotation: number
++}
++
+type LineList = List<SeriesModel, LineDataVisual>;
+
+export interface LineLabel extends graphic.Text {
+ lineLabelOriginalOpacity: number
+}
+
+interface InnerLineLabel extends LineLabel {
+ __align: TextStyleProps['align']
+ __verticalAlign: TextStyleProps['verticalAlign']
+ __position: LineLabelOption['position']
+ __labelDistance: number[]
+}
+
+function makeSymbolTypeKey(symbolCategory: 'fromSymbol' | 'toSymbol') {
+ return '_' + symbolCategory + 'Type' as '_fromSymbolType' | '_toSymbolType';
+}
+
+/**
+ * @inner
+ */
+function createSymbol(name: 'fromSymbol' | 'toSymbol', lineData: LineList, idx: number) {
+ const symbolType = lineData.getItemVisual(idx, name);
- const symbolSize = lineData.getItemVisual(
- idx,
- name + 'Size' as 'fromSymbolSize' | 'toSymbolSize'
- );
-
+ if (!symbolType || symbolType === 'none') {
+ return;
+ }
+
++ const symbolSize = lineData.getItemVisual(idx, name + 'Size' as 'fromSymbolSize' | 'toSymbolSize');
++ const symbolRotate = lineData.getItemVisual(idx, name + 'Rotate' as 'fromSymbolRotate' | 'toSymbolRotate');
++
+ const symbolSizeArr = zrUtil.isArray(symbolSize)
+ ? symbolSize : [symbolSize, symbolSize];
+ const symbolPath = symbolUtil.createSymbol(
+ symbolType, -symbolSizeArr[0] / 2, -symbolSizeArr[1] / 2,
+ symbolSizeArr[0], symbolSizeArr[1]
+ );
+
++ (symbolPath as LineECSymbol).__specifiedRotation = symbolRotate == null || isNaN(symbolRotate)
++ ? void 0
++ : +symbolRotate * Math.PI / 180 || 0;
++
+ symbolPath.name = name;
+
+ return symbolPath;
+}
+
+function createLine(points: number[][]) {
+ const line = new ECLinePath({
+ name: 'line',
+ subPixelOptimize: true
+ });
+ setLinePoints(line.shape, points);
+ return line;
+}
+
+function setLinePoints(targetShape: ECLinePath['shape'], points: number[][]) {
+ type CurveShape = ECLinePath['shape'] & {
+ cpx1: number
+ cpy1: number
+ };
+
+ targetShape.x1 = points[0][0];
+ targetShape.y1 = points[0][1];
+ targetShape.x2 = points[1][0];
+ targetShape.y2 = points[1][1];
+ targetShape.percent = 1;
+
+ const cp1 = points[2];
+ if (cp1) {
+ (targetShape as CurveShape).cpx1 = cp1[0];
+ (targetShape as CurveShape).cpy1 = cp1[1];
+ }
+ else {
+ (targetShape as CurveShape).cpx1 = NaN;
+ (targetShape as CurveShape).cpy1 = NaN;
+ }
+}
+
+class Line extends graphic.Group {
+
+ private _fromSymbolType: string;
+ private _toSymbolType: string;
+
+ constructor(lineData: List, idx: number, seriesScope?: LineDrawSeriesScope) {
+ super();
+ this._createLine(lineData as LineList, idx, seriesScope);
+ }
+
+ _createLine(lineData: LineList, idx: number, seriesScope?: LineDrawSeriesScope) {
+ const seriesModel = lineData.hostModel;
+ const linePoints = lineData.getItemLayout(idx);
+ const line = createLine(linePoints);
+ line.shape.percent = 0;
+ graphic.initProps(line, {
+ shape: {
+ percent: 1
+ }
+ }, seriesModel, idx);
+
+ this.add(line);
+
+ zrUtil.each(SYMBOL_CATEGORIES, function (symbolCategory) {
+ const symbol = createSymbol(symbolCategory, lineData, idx);
+ // symbols must added after line to make sure
+ // it will be updated after line#update.
+ // Or symbol position and rotation update in line#beforeUpdate will be one frame slow
+ this.add(symbol);
+ this[makeSymbolTypeKey(symbolCategory)] = lineData.getItemVisual(idx, symbolCategory);
+ }, this);
+
+ this._updateCommonStl(lineData, idx, seriesScope);
+ }
+
+ // TODO More strict on the List type in parameters?
+ updateData(lineData: List, idx: number, seriesScope: LineDrawSeriesScope) {
+ const seriesModel = lineData.hostModel;
+
+ const line = this.childOfName('line') as ECLinePath;
+ const linePoints = lineData.getItemLayout(idx);
+ const target = {
+ shape: {} as ECLinePath['shape']
+ };
+
+ setLinePoints(target.shape, linePoints);
+ graphic.updateProps(line, target, seriesModel, idx);
+
+ zrUtil.each(SYMBOL_CATEGORIES, function (symbolCategory) {
+ const symbolType = (lineData as LineList).getItemVisual(idx, symbolCategory);
+ const key = makeSymbolTypeKey(symbolCategory);
+ // Symbol changed
+ if (this[key] !== symbolType) {
+ this.remove(this.childOfName(symbolCategory));
+ const symbol = createSymbol(symbolCategory, lineData as LineList, idx);
+ this.add(symbol);
+ }
+ this[key] = symbolType;
+ }, this);
+
+ this._updateCommonStl(lineData, idx, seriesScope);
+ };
+
+ getLinePath() {
+ return this.childAt(0) as graphic.Line;
+ }
+
+ _updateCommonStl(lineData: List, idx: number, seriesScope?: LineDrawSeriesScope) {
+ const seriesModel = lineData.hostModel as SeriesModel;
+
+ const line = this.childOfName('line') as ECLinePath;
+
+ let emphasisLineStyle = seriesScope && seriesScope.emphasisLineStyle;
+ let blurLineStyle = seriesScope && seriesScope.blurLineStyle;
+ let selectLineStyle = seriesScope && seriesScope.selectLineStyle;
+
+ let labelStatesModels = seriesScope && seriesScope.labelStatesModels;
+
+ // Optimization for large dataset
+ if (!seriesScope || lineData.hasItemOption) {
+ const itemModel = lineData.getItemModel<LineDrawModelOption>(idx);
+
+ emphasisLineStyle = itemModel.getModel(['emphasis', 'lineStyle']).getLineStyle();
+ blurLineStyle = itemModel.getModel(['blur', 'lineStyle']).getLineStyle();
+ selectLineStyle = itemModel.getModel(['select', 'lineStyle']).getLineStyle();
+
+ labelStatesModels = getLabelStatesModels(itemModel);
+ }
+
+ const lineStyle = lineData.getItemVisual(idx, 'style');
+ const visualColor = lineStyle.stroke;
+
+ line.useStyle(lineStyle);
+ line.style.fill = null;
+ line.style.strokeNoScale = true;
+
+ line.ensureState('emphasis').style = emphasisLineStyle;
+ line.ensureState('blur').style = blurLineStyle;
+ line.ensureState('select').style = selectLineStyle;
+
+ // Update symbol
+ zrUtil.each(SYMBOL_CATEGORIES, function (symbolCategory) {
+ const symbol = this.childOfName(symbolCategory) as ECSymbol;
+ if (symbol) {
+ // Share opacity and color with line.
+ symbol.setColor(visualColor);
+ symbol.style.opacity = lineStyle.opacity;
+
+ for (let i = 0; i < SPECIAL_STATES.length; i++) {
+ const stateName = SPECIAL_STATES[i];
+ const lineState = line.getState(stateName);
+ if (lineState) {
+ const lineStateStyle = lineState.style || {};
+ const state = symbol.ensureState(stateName);
+ const stateStyle = state.style || (state.style = {});
+ if (lineStateStyle.stroke != null) {
+ stateStyle[symbol.__isEmptyBrush ? 'stroke' : 'fill'] = lineStateStyle.stroke;
+ }
+ if (lineStateStyle.opacity != null) {
+ stateStyle.opacity = lineStateStyle.opacity;
+ }
+ }
+ }
+
+ symbol.markRedraw();
+ }
+ }, this);
+
+ const rawVal = seriesModel.getRawValue(idx) as number;
+ setLabelStyle(this, labelStatesModels, {
+ labelDataIndex: idx,
+ labelFetcher: {
+ getFormattedLabel(dataIndex, stateName) {
+ return seriesModel.getFormattedLabel(dataIndex, stateName, lineData.dataType);
+ }
+ },
+ inheritColor: visualColor as ColorString || '#000',
+ defaultText: (rawVal == null
+ ? lineData.getName(idx)
+ : isFinite(rawVal)
+ ? round(rawVal)
+ : rawVal) + ''
+ });
+ const label = this.getTextContent() as InnerLineLabel;
+
+ // Always set `textStyle` even if `normalStyle.text` is null, because default
+ // values have to be set on `normalStyle`.
+ if (label) {
+ const labelNormalModel = labelStatesModels.normal as unknown as Model<LineLabelOption>;
+ label.__align = label.style.align;
+ label.__verticalAlign = label.style.verticalAlign;
+ // 'start', 'middle', 'end'
+ label.__position = labelNormalModel.get('position') || 'middle';
+
+ let distance = labelNormalModel.get('distance');
+ if (!zrUtil.isArray(distance)) {
+ distance = [distance, distance];
+ }
+ label.__labelDistance = distance;
+ }
+
+ this.setTextConfig({
+ position: null,
+ local: true,
+ inside: false // Can't be inside for stroke element.
+ });
+
+ enableHoverEmphasis(this);
+ }
+
+ highlight() {
+ enterEmphasis(this);
+ }
+
+ downplay() {
+ leaveEmphasis(this);
+ }
+
+ updateLayout(lineData: List, idx: number) {
+ this.setLinePoints(lineData.getItemLayout(idx));
+ }
+
+ setLinePoints(points: number[][]) {
+ const linePath = this.childOfName('line') as ECLinePath;
+ setLinePoints(linePath.shape, points);
+ linePath.dirty();
+ }
+
+ beforeUpdate() {
+ const lineGroup = this;
+ const symbolFrom = lineGroup.childOfName('fromSymbol') as ECSymbol;
+ const symbolTo = lineGroup.childOfName('toSymbol') as ECSymbol;
+ const label = lineGroup.getTextContent() as InnerLineLabel;
+ // Quick reject
+ if (!symbolFrom && !symbolTo && (!label || label.ignore)) {
+ return;
+ }
+
+ let invScale = 1;
+ let parentNode = this.parent;
+ while (parentNode) {
+ if (parentNode.scaleX) {
+ invScale /= parentNode.scaleX;
+ }
+ parentNode = parentNode.parent;
+ }
+
+ const line = lineGroup.childOfName('line') as ECLinePath;
+ // If line not changed
+ // FIXME Parent scale changed
+ if (!this.__dirty && !line.__dirty) {
+ return;
+ }
+
+ const percent = line.shape.percent;
+ const fromPos = line.pointAt(0);
+ const toPos = line.pointAt(percent);
+
+ const d = vector.sub([], toPos, fromPos);
+ vector.normalize(d, d);
+
++ function setSymbolRotation(symbol: ECSymbol, percent: 0 | 1) {
++ // Fix #12388
++ // when symbol is set to be 'arrow' in markLine,
++ // symbolRotate value will be ignored, and compulsively use tangent angle.
++ // rotate by default if symbol rotation is not specified
++ const specifiedRotation = (symbol as LineECSymbol).__specifiedRotation;
++ if (specifiedRotation == null) {
++ const tangent = line.tangentAt(percent);
++ symbol.attr('rotation', (percent === 1 ? -1 : 1) * Math.PI / 2 - Math.atan2(
++ tangent[1], tangent[0]
++ ));
++ }
++ else {
++ symbol.attr('rotation', specifiedRotation);
++ }
++ }
++
+ if (symbolFrom) {
+ symbolFrom.setPosition(fromPos);
++
++ setSymbolRotation(symbolFrom, 0);
++
+ const tangent = line.tangentAt(0);
+ symbolFrom.rotation = Math.PI / 2 - Math.atan2(
+ tangent[1], tangent[0]
+ );
+ symbolFrom.scaleX = symbolFrom.scaleY = invScale * percent;
+ symbolFrom.markRedraw();
+ }
+ if (symbolTo) {
+ symbolTo.setPosition(toPos);
+ const tangent = line.tangentAt(1);
++ setSymbolRotation(symbolFrom, 1);
++
+ symbolTo.rotation = -Math.PI / 2 - Math.atan2(
+ tangent[1], tangent[0]
+ );
+ symbolTo.scaleX = symbolTo.scaleY = invScale * percent;
+ symbolTo.markRedraw();
+ }
+
+ if (label && !label.ignore) {
+ label.x = label.y = 0;
+ label.originX = label.originY = 0;
+
+ let textAlign: ZRTextAlign;
+ let textVerticalAlign: ZRTextVerticalAlign;
+
+ const distance = label.__labelDistance;
+ const distanceX = distance[0] * invScale;
+ const distanceY = distance[1] * invScale;
+ const halfPercent = percent / 2;
+ const tangent = line.tangentAt(halfPercent);
+ const n = [tangent[1], -tangent[0]];
+ const cp = line.pointAt(halfPercent);
+ if (n[1] > 0) {
+ n[0] = -n[0];
+ n[1] = -n[1];
+ }
+ const dir = tangent[0] < 0 ? -1 : 1;
+
+ if (label.__position !== 'start' && label.__position !== 'end') {
+ let rotation = -Math.atan2(tangent[1], tangent[0]);
+ if (toPos[0] < fromPos[0]) {
+ rotation = Math.PI + rotation;
+ }
+ label.rotation = rotation;
+ }
+
+ let dy;
+ switch (label.__position) {
+ case 'insideStartTop':
+ case 'insideMiddleTop':
+ case 'insideEndTop':
+ case 'middle':
+ dy = -distanceY;
+ textVerticalAlign = 'bottom';
+ break;
+
+ case 'insideStartBottom':
+ case 'insideMiddleBottom':
+ case 'insideEndBottom':
+ dy = distanceY;
+ textVerticalAlign = 'top';
+ break;
+
+ default:
+ dy = 0;
+ textVerticalAlign = 'middle';
+ }
+
+ switch (label.__position) {
+ case 'end':
+ label.x = d[0] * distanceX + toPos[0];
+ label.y = d[1] * distanceY + toPos[1];
+ textAlign = d[0] > 0.8 ? 'left' : (d[0] < -0.8 ? 'right' : 'center');
+ textVerticalAlign = d[1] > 0.8 ? 'top' : (d[1] < -0.8 ? 'bottom' : 'middle');
+ break;
+
+ case 'start':
+ label.x = -d[0] * distanceX + fromPos[0];
+ label.y = -d[1] * distanceY + fromPos[1];
+ textAlign = d[0] > 0.8 ? 'right' : (d[0] < -0.8 ? 'left' : 'center');
+ textVerticalAlign = d[1] > 0.8 ? 'bottom' : (d[1] < -0.8 ? 'top' : 'middle');
+ break;
+
+ case 'insideStartTop':
+ case 'insideStart':
+ case 'insideStartBottom':
+ label.x = distanceX * dir + fromPos[0];
+ label.y = fromPos[1] + dy;
+ textAlign = tangent[0] < 0 ? 'right' : 'left';
+ label.originX = -distanceX * dir;
+ label.originY = -dy;
+ break;
+
+ case 'insideMiddleTop':
+ case 'insideMiddle':
+ case 'insideMiddleBottom':
+ case 'middle':
+ label.x = cp[0];
+ label.y = cp[1] + dy;
+ textAlign = 'center';
+ label.originY = -dy;
+ break;
+
+ case 'insideEndTop':
+ case 'insideEnd':
+ case 'insideEndBottom':
+ label.x = -distanceX * dir + toPos[0];
+ label.y = toPos[1] + dy;
+ textAlign = tangent[0] >= 0 ? 'right' : 'left';
+ label.originX = distanceX * dir;
+ label.originY = -dy;
+ break;
+ }
+
+ label.scaleX = label.scaleY = invScale;
+ label.setStyle({
+ // Use the user specified text align and baseline first
+ verticalAlign: label.__verticalAlign || textVerticalAlign,
+ align: label.__align || textAlign
+ });
+ }
+ }
+}
+
+export default Line;
diff --cc src/chart/helper/multipleGraphEdgeHelper.ts
index 0000000,0000000..964c5ae
new file mode 100644
--- /dev/null
+++ b/src/chart/helper/multipleGraphEdgeHelper.ts
@@@ -1,0 -1,0 +1,230 @@@
++/*
++* 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.
++*/
++
++// @ts-nocheck
++import * as zrUtil from 'zrender/src/core/util';
++
++const KEY_DELIMITER = '-->';
++/**
++ * params handler
++ * @param {module:echarts/model/SeriesModel} seriesModel
++ * @returns {*}
++ */
++const getAutoCurvenessParams = function (seriesModel) {
++ return seriesModel.get('autoCurveness') || null;
++};
++
++/**
++ * Generate a list of edge curvatures, 20 is the default
++ * @param {module:echarts/model/SeriesModel} seriesModel
++ * @param {number} appendLength
++ * @return 20 => [0, -0.2, 0.2, -0.4, 0.4, -0.6, 0.6, -0.8, 0.8, -1, 1, -1.2, 1.2, -1.4, 1.4, -1.6, 1.6, -1.8, 1.8, -2]
++ */
++const createCurveness = function (seriesModel, appendLength) {
++ const autoCurvenessParmas = getAutoCurvenessParams(seriesModel);
++ let length = 20;
++ let curvenessList = [];
++
++ // handler the function set
++ if (typeof autoCurvenessParmas === 'number') {
++ length = autoCurvenessParmas;
++ }
++ else if (zrUtil.isArray(autoCurvenessParmas)) {
++ seriesModel.__curvenessList = autoCurvenessParmas;
++ return;
++ }
++
++ // append length
++ if (appendLength > length) {
++ length = appendLength;
++ }
++
++ // make sure the length is even
++ const len = length % 2 ? length + 2 : length + 3;
++ curvenessList = [];
++
++ for (let i = 0; i < len; i++) {
++ curvenessList.push((i % 2 ? i + 1 : i) / 10 * (i % 2 ? -1 : 1));
++ }
++ seriesModel.__curvenessList = curvenessList;
++};
++
++/**
++ * Create different cache key data in the positive and negative directions, in order to set the curvature later
++ * @param {number|string|module:echarts/data/Graph.Node} n1
++ * @param {number|string|module:echarts/data/Graph.Node} n2
++ * @param {module:echarts/model/SeriesModel} seriesModel
++ * @returns {string} key
++ */
++const getKeyOfEdges = function (n1, n2, seriesModel) {
++ const source = [n1.id, n1.dataIndex].join('.');
++ const target = [n2.id, n2.dataIndex].join('.');
++ return [seriesModel.uid, source, target].join(KEY_DELIMITER);
++};
++
++/**
++ * get opposite key
++ * @param {string} key
++ * @returns {string}
++ */
++const getOppositeKey = function (key) {
++ const keys = key.split(KEY_DELIMITER);
++ return [keys[0], keys[2], keys[1]].join(KEY_DELIMITER);
++};
++
++/**
++ * get edgeMap with key
++ * @param edge
++ * @param {module:echarts/model/SeriesModel} seriesModel
++ */
++const getEdgeFromMap = function (edge, seriesModel) {
++ const key = getKeyOfEdges(edge.node1, edge.node2, seriesModel);
++ return seriesModel.__edgeMap[key];
++};
++
++/**
++ * calculate all cases total length
++ * @param edge
++ * @param seriesModel
++ * @returns {number}
++ */
++const getTotalLengthBetweenNodes = function (edge, seriesModel) {
++ const len = getEdgeMapLengthWithKey(getKeyOfEdges(edge.node1, edge.node2, seriesModel), seriesModel);
++ const lenV = getEdgeMapLengthWithKey(getKeyOfEdges(edge.node2, edge.node1, seriesModel), seriesModel);
++
++ return len + lenV;
++};
++
++/**
++ *
++ * @param key
++ */
++const getEdgeMapLengthWithKey = function (key, seriesModel) {
++ const edgeMap = seriesModel.__edgeMap;
++ return edgeMap[key] ? edgeMap[key].length : 0;
++};
++
++/**
++ * Count the number of edges between the same two points, used to obtain the curvature table and the parity of the edge
++ * @see /graph/GraphSeries.js@getInitialData
++ * @param {module:echarts/model/SeriesModel} seriesModel
++ */
++export function initCurvenessList(seriesModel) {
++ if (!getAutoCurvenessParams(seriesModel)) {
++ return;
++ }
++
++ seriesModel.__curvenessList = [];
++ seriesModel.__edgeMap = {};
++ // calc the array of curveness List
++ createCurveness(seriesModel);
++}
++
++/**
++ * set edgeMap with key
++ * @param {number|string|module:echarts/data/Graph.Node} n1
++ * @param {number|string|module:echarts/data/Graph.Node} n2
++ * @param {module:echarts/model/SeriesModel} seriesModel
++ * @param {number} index
++ */
++export function createEdgeMapForCurveness(n1, n2, seriesModel, index) {
++ if (!getAutoCurvenessParams(seriesModel)) {
++ return;
++ }
++
++ const key = getKeyOfEdges(n1, n2, seriesModel);
++ const edgeMap = seriesModel.__edgeMap;
++ const oppositeEdges = edgeMap[getOppositeKey(key)];
++ // set direction
++ if (edgeMap[key] && !oppositeEdges) {
++ edgeMap[key].isForward = true;
++ }
++ else if (oppositeEdges && edgeMap[key]) {
++ oppositeEdges.isForward = true;
++ edgeMap[key].isForward = false;
++ }
++
++ edgeMap[key] = edgeMap[key] || [];
++ edgeMap[key].push(index);
++}
++
++/**
++ * get curvature for edge
++ * @param edge
++ * @param {module:echarts/model/SeriesModel} seriesModel
++ * @param index
++ */
++export function getCurvenessForEdge(edge, seriesModel, index, needReverse?: boolean) {
++ const autoCurvenessParams = getAutoCurvenessParams(seriesModel);
++ const isArrayParam = zrUtil.isArray(autoCurvenessParams);
++ if (!autoCurvenessParams) {
++ return null;
++ }
++
++ const edgeArray = getEdgeFromMap(edge, seriesModel);
++ if (!edgeArray) {
++ return null;
++ }
++
++ let edgeIndex = -1;
++ for (const i = 0; i < edgeArray.length; i++) {
++ if (edgeArray[i] === index) {
++ edgeIndex = i;
++ break;
++ }
++ }
++ // if totalLen is Longer createCurveness
++ const totalLen = getTotalLengthBetweenNodes(edge, seriesModel);
++ createCurveness(seriesModel, totalLen);
++
++ edge.lineStyle = edge.lineStyle || {};
++ // if is opposite edge, must set curvenss to opposite number
++ const curKey = getKeyOfEdges(edge.node1, edge.node2, seriesModel);
++ const curvenessList = seriesModel.__curvenessList;
++ // if pass array no need parity
++ const parityCorrection = isArrayParam ? 0 : totalLen % 2 ? 0 : 1;
++
++ if (!edgeArray.isForward) {
++ // the opposite edge show outside
++ const oppositeKey = getOppositeKey(curKey);
++ const len = getEdgeMapLengthWithKey(oppositeKey, seriesModel);
++ const resValue = curvenessList[edgeIndex + len + parityCorrection];
++ // isNeedReverse, simple, force type need reverse the curveness in the junction of the forword and the opposite
++ if (needReverse) {
++ // set as array may make the parity handle with the len of opposite
++ if (isArrayParam) {
++ if (autoCurvenessParams && autoCurvenessParams[0] === 0) {
++ return (len + parityCorrection) % 2 ? resValue : -resValue;
++ }
++ else {
++ return ((len % 2 ? 0 : 1) + parityCorrection) % 2 ? resValue : -resValue;
++ }
++ }
++ else {
++ return (len + parityCorrection) % 2 ? resValue : -resValue;
++ }
++ }
++ else {
++ return curvenessList[edgeIndex + len + parityCorrection];
++ }
++ }
++ else {
++ return curvenessList[parityCorrection + edgeIndex];
++ }
++}
diff --cc src/chart/lines/LinesSeries.ts
index a75d7e4,0000000..b2da8ab
mode 100644,000000..100644
--- a/src/chart/lines/LinesSeries.ts
+++ b/src/chart/lines/LinesSeries.ts
@@@ -1,413 -1,0 +1,410 @@@
+/*
+* 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.
+*/
+
+/* global Uint32Array, Float64Array, Float32Array */
+
+import SeriesModel from '../../model/Series';
+import List from '../../data/List';
+import { concatArray, mergeAll, map } from 'zrender/src/core/util';
+import CoordinateSystem from '../../CoordinateSystem';
+import {
+ SeriesOption,
+ SeriesOnCartesianOptionMixin,
+ SeriesOnGeoOptionMixin,
+ SeriesOnPolarOptionMixin,
+ SeriesOnCalendarOptionMixin,
+ SeriesLargeOptionMixin,
+ LineStyleOption,
+ OptionDataValue,
+ LineLabelOption,
+ StatesOptionMixin
+} from '../../util/types';
+import GlobalModel from '../../model/Global';
+import type { LineDrawModelOption } from '../helper/LineDraw';
+import { createTooltipMarkup } from '../../component/tooltip/tooltipMarkup';
+
+const Uint32Arr = typeof Uint32Array === 'undefined' ? Array : Uint32Array;
+const Float64Arr = typeof Float64Array === 'undefined' ? Array : Float64Array;
+
+function compatEc2(seriesOpt: LinesSeriesOption) {
+ const data = seriesOpt.data;
+ if (data && data[0] && (data as LegacyDataItemOption[][])[0][0] && (data as LegacyDataItemOption[][])[0][0].coord) {
+ if (__DEV__) {
+ console.warn('Lines data configuration has been changed to'
+ + ' { coords:[[1,2],[2,3]] }');
+ }
+ seriesOpt.data = map(data as LegacyDataItemOption[][], function (itemOpt) {
+ const coords = [
+ itemOpt[0].coord, itemOpt[1].coord
+ ];
+ const target: LinesDataItemOption = {
+ coords: coords
+ };
+ if (itemOpt[0].name) {
+ target.fromName = itemOpt[0].name;
+ }
+ if (itemOpt[1].name) {
+ target.toName = itemOpt[1].name;
+ }
+ return mergeAll([target, itemOpt[0], itemOpt[1]]);
+ });
+ }
+}
+
+type LinesCoords = number[][];
+
+type LinesValue = OptionDataValue | OptionDataValue[];
+
+interface LinesLineStyleOption extends LineStyleOption {
+ curveness?: number
+}
+
+// @deprecated
+interface LegacyDataItemOption {
+ coord: number[]
+ name: string
+}
+
+export interface LinesStateOption {
+ lineStyle?: LinesLineStyleOption
+ label?: LineLabelOption
+}
+
+export interface LinesDataItemOption extends LinesStateOption, StatesOptionMixin<LinesStateOption> {
+ name?: string
+
+ fromName?: string
+ toName?: string
+
+ symbol?: string[] | string
+ symbolSize?: number[] | number
+
+ coords?: LinesCoords
+
+ value?: LinesValue
+}
+
+export interface LinesSeriesOption extends SeriesOption<LinesStateOption>, LinesStateOption,
+ SeriesOnCartesianOptionMixin, SeriesOnGeoOptionMixin, SeriesOnPolarOptionMixin,
+ SeriesOnCalendarOptionMixin, SeriesLargeOptionMixin {
+
+ type?: 'lines'
+
+ coordinateSystem?: string
+
+ symbol?: string[] | string
+ symbolSize?: number[] | number
+
+ effect?: LineDrawModelOption['effect']
+
+ /**
+ * If lines are polyline
+ * polyline not support curveness, label, animation
+ */
+ polyline?: boolean
+ /**
+ * If clip the overflow.
+ * Available when coordinateSystem is cartesian or polar.
+ */
+ clip?: boolean
+
+ data?: LinesDataItemOption[]
+ // Stored as a flat array. In format
+ // Points Count(2) | x | y | x | y | Points Count(3) | x | y | x | y | x | y |
+ | ArrayLike<number>
+}
+
+class LinesSeriesModel extends SeriesModel<LinesSeriesOption> {
+
+ static readonly type = 'series.lines';
+ readonly type = LinesSeriesModel.type;
+
+ static readonly dependencies = ['grid', 'polar', 'geo', 'calendar'];
+
+ visualStyleAccessPath = 'lineStyle';
+ visualDrawType = 'stroke' as const;
+
+ private _flatCoords: ArrayLike<number>;
+ private _flatCoordsOffset: ArrayLike<number>;
+
+ init(option: LinesSeriesOption) {
+ // The input data may be null/undefined.
+ option.data = option.data || [];
+
+ // Not using preprocessor because mergeOption may not have series.type
+ compatEc2(option);
+
+ const result = this._processFlatCoordsArray(option.data);
+ this._flatCoords = result.flatCoords;
+ this._flatCoordsOffset = result.flatCoordsOffset;
+ if (result.flatCoords) {
+ option.data = new Float32Array(result.count);
+ }
+
+ super.init.apply(this, arguments as any);
+ }
+
+ mergeOption(option: LinesSeriesOption) {
- // The input data may be null/undefined.
- option.data = option.data || [];
-
+ compatEc2(option);
+
+ if (option.data) {
+ // Only update when have option data to merge.
+ const result = this._processFlatCoordsArray(option.data);
+ this._flatCoords = result.flatCoords;
+ this._flatCoordsOffset = result.flatCoordsOffset;
+ if (result.flatCoords) {
+ option.data = new Float32Array(result.count);
+ }
+ }
+
+ super.mergeOption.apply(this, arguments as any);
+ }
+
+ appendData(params: Pick<LinesSeriesOption, 'data'>) {
+ const result = this._processFlatCoordsArray(params.data);
+ if (result.flatCoords) {
+ if (!this._flatCoords) {
+ this._flatCoords = result.flatCoords;
+ this._flatCoordsOffset = result.flatCoordsOffset;
+ }
+ else {
+ this._flatCoords = concatArray(this._flatCoords, result.flatCoords);
+ this._flatCoordsOffset = concatArray(this._flatCoordsOffset, result.flatCoordsOffset);
+ }
+ params.data = new Float32Array(result.count);
+ }
+
+ this.getRawData().appendData(params.data);
+ }
+
+ _getCoordsFromItemModel(idx: number) {
+ const itemModel = this.getData().getItemModel<LinesDataItemOption>(idx);
+ const coords = (itemModel.option instanceof Array)
+ ? itemModel.option : itemModel.getShallow('coords');
+
+ if (__DEV__) {
+ if (!(coords instanceof Array && coords.length > 0 && coords[0] instanceof Array)) {
+ throw new Error(
+ 'Invalid coords ' + JSON.stringify(coords) + '. Lines must have 2d coords array in data item.'
+ );
+ }
+ }
+ return coords;
+ }
+
+ getLineCoordsCount(idx: number) {
+ if (this._flatCoordsOffset) {
+ return this._flatCoordsOffset[idx * 2 + 1];
+ }
+ else {
+ return this._getCoordsFromItemModel(idx).length;
+ }
+ }
+
+ getLineCoords(idx: number, out: number[][]) {
+ if (this._flatCoordsOffset) {
+ const offset = this._flatCoordsOffset[idx * 2];
+ const len = this._flatCoordsOffset[idx * 2 + 1];
+ for (let i = 0; i < len; i++) {
+ out[i] = out[i] || [];
+ out[i][0] = this._flatCoords[offset + i * 2];
+ out[i][1] = this._flatCoords[offset + i * 2 + 1];
+ }
+ return len;
+ }
+ else {
+ const coords = this._getCoordsFromItemModel(idx);
+ for (let i = 0; i < coords.length; i++) {
+ out[i] = out[i] || [];
+ out[i][0] = coords[i][0];
+ out[i][1] = coords[i][1];
+ }
+ return coords.length;
+ }
+ }
+
+ _processFlatCoordsArray(data: LinesSeriesOption['data']) {
+ let startOffset = 0;
+ if (this._flatCoords) {
+ startOffset = this._flatCoords.length;
+ }
+ // Stored as a typed array. In format
+ // Points Count(2) | x | y | x | y | Points Count(3) | x | y | x | y | x | y |
+ if (typeof data[0] === 'number') {
+ const len = data.length;
+ // Store offset and len of each segment
+ const coordsOffsetAndLenStorage = new Uint32Arr(len) as Uint32Array;
+ const coordsStorage = new Float64Arr(len) as Float64Array;
+ let coordsCursor = 0;
+ let offsetCursor = 0;
+ let dataCount = 0;
+ for (let i = 0; i < len;) {
+ dataCount++;
+ const count = data[i++] as number;
+ // Offset
+ coordsOffsetAndLenStorage[offsetCursor++] = coordsCursor + startOffset;
+ // Len
+ coordsOffsetAndLenStorage[offsetCursor++] = count;
+ for (let k = 0; k < count; k++) {
+ const x = data[i++] as number;
+ const y = data[i++] as number;
+ coordsStorage[coordsCursor++] = x;
+ coordsStorage[coordsCursor++] = y;
+
+ if (i > len) {
+ if (__DEV__) {
+ throw new Error('Invalid data format.');
+ }
+ }
+ }
+ }
+
+ return {
+ flatCoordsOffset: new Uint32Array(coordsOffsetAndLenStorage.buffer, 0, offsetCursor),
+ flatCoords: coordsStorage,
+ count: dataCount
+ };
+ }
+
+ return {
+ flatCoordsOffset: null,
+ flatCoords: null,
+ count: data.length
+ };
+ }
+
+ getInitialData(option: LinesSeriesOption, ecModel: GlobalModel) {
+ if (__DEV__) {
+ const CoordSys = CoordinateSystem.get(option.coordinateSystem);
+ if (!CoordSys) {
+ throw new Error('Unkown coordinate system ' + option.coordinateSystem);
+ }
+ }
+
+ const lineData = new List(['value'], this);
+ lineData.hasItemOption = false;
+
+ lineData.initData(option.data, [], function (dataItem, dimName, dataIndex, dimIndex) {
+ // dataItem is simply coords
+ if (dataItem instanceof Array) {
+ return NaN;
+ }
+ else {
+ lineData.hasItemOption = true;
+ const value = dataItem.value;
+ if (value != null) {
+ return value instanceof Array ? value[dimIndex] : value;
+ }
+ }
+ });
+
+ return lineData;
+ }
+
+ formatTooltip(
+ dataIndex: number,
+ multipleSeries: boolean,
+ dataType: string
+ ) {
+ const data = this.getData();
+ const itemModel = data.getItemModel<LinesDataItemOption>(dataIndex);
+ const name = itemModel.get('name');
+ if (name) {
+ return name;
+ }
+ const fromName = itemModel.get('fromName');
+ const toName = itemModel.get('toName');
+ const nameArr = [];
+ fromName != null && nameArr.push(fromName);
+ toName != null && nameArr.push(toName);
+
+ return createTooltipMarkup('nameValue', {
+ name: nameArr.join(' > ')
+ });
+ }
+
+ preventIncremental() {
+ return !!this.get(['effect', 'show']);
+ }
+
+ getProgressive() {
+ const progressive = this.option.progressive;
+ if (progressive == null) {
+ return this.option.large ? 1e4 : this.get('progressive');
+ }
+ return progressive;
+ }
+
+ getProgressiveThreshold() {
+ const progressiveThreshold = this.option.progressiveThreshold;
+ if (progressiveThreshold == null) {
+ return this.option.large ? 2e4 : this.get('progressiveThreshold');
+ }
+ return progressiveThreshold;
+ }
+
+ static defaultOption: LinesSeriesOption = {
+ coordinateSystem: 'geo',
+ zlevel: 0,
+ z: 2,
+ legendHoverLink: true,
+
+ // Cartesian coordinate system
+ xAxisIndex: 0,
+ yAxisIndex: 0,
+
+ symbol: ['none', 'none'],
+ symbolSize: [10, 10],
+ // Geo coordinate system
+ geoIndex: 0,
+
+ effect: {
+ show: false,
+ period: 4,
+ constantSpeed: 0,
+ symbol: 'circle',
+ symbolSize: 3,
+ loop: true,
+ trailLength: 0.2
+ },
+
+ large: false,
+ // Available when large is true
+ largeThreshold: 2000,
+
+ polyline: false,
+
+ clip: true,
+
+ label: {
+ show: false,
+ position: 'end'
+ // distance: 5,
+ // formatter: 标签文本格式器,同Tooltip.formatter,不支持异步回调
+ },
+
+ lineStyle: {
+ opacity: 0.5
+ }
+ };
+}
+
+SeriesModel.registerClass(LinesSeriesModel);
+
- export default LinesSeriesModel;
++export default LinesSeriesModel;
diff --cc src/chart/radar/RadarView.ts
index dd266ec,0000000..7842224
mode 100644,000000..100644
--- a/src/chart/radar/RadarView.ts
+++ b/src/chart/radar/RadarView.ts
@@@ -1,272 -1,0 +1,274 @@@
+/*
+* 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 graphic from '../../util/graphic';
+import { setStatesStylesFromModel, enableHoverEmphasis } from '../../util/states';
+import * as zrUtil from 'zrender/src/core/util';
+import * as symbolUtil from '../../util/symbol';
+import ChartView from '../../view/Chart';
+import RadarSeriesModel, { RadarSeriesDataItemOption } from './RadarSeries';
+import ExtensionAPI from '../../ExtensionAPI';
+import List from '../../data/List';
+import { ColorString } from '../../util/types';
+import GlobalModel from '../../model/Global';
+import { VectorArray } from 'zrender/src/core/vector';
+import { setLabelStyle, getLabelStatesModels } from '../../label/labelStyle';
+import ZRImage from 'zrender/src/graphic/Image';
+
+function normalizeSymbolSize(symbolSize: number | number[]) {
+ if (!zrUtil.isArray(symbolSize)) {
+ symbolSize = [+symbolSize, +symbolSize];
+ }
+ return symbolSize;
+}
+
+type RadarSymbol = ReturnType<typeof symbolUtil.createSymbol> & {
+ __dimIdx: number
+};
+
+class RadarView extends ChartView {
+ static type = 'radar';
+ type = RadarView.type;
+
+ private _data: List<RadarSeriesModel>;
+
+ render(seriesModel: RadarSeriesModel, ecModel: GlobalModel, api: ExtensionAPI) {
+ const polar = seriesModel.coordinateSystem;
+ const group = this.group;
+
+ const data = seriesModel.getData();
+ const oldData = this._data;
+
+ function createSymbol(data: List<RadarSeriesModel>, idx: number) {
+ const symbolType = data.getItemVisual(idx, 'symbol') as string || 'circle';
+ if (symbolType === 'none') {
+ return;
+ }
+ const symbolSize = normalizeSymbolSize(
+ data.getItemVisual(idx, 'symbolSize')
+ );
+ const symbolPath = symbolUtil.createSymbol(
+ symbolType, -1, -1, 2, 2
+ );
++ const symbolRotate = data.getItemVisual(idx, 'symbolRotate') || 0;
+ symbolPath.attr({
+ style: {
+ strokeNoScale: true
+ },
+ z2: 100,
+ scaleX: symbolSize[0] / 2,
- scaleY: symbolSize[1] / 2
++ scaleY: symbolSize[1] / 2,
++ rotation: symbolRotate * Math.PI / 180 || 0
+ });
+ return symbolPath as RadarSymbol;
+ }
+
+ function updateSymbols(
+ oldPoints: VectorArray[],
+ newPoints: VectorArray[],
+ symbolGroup: graphic.Group,
+ data: List<RadarSeriesModel>,
+ idx: number,
+ isInit?: boolean
+ ) {
+ // Simply rerender all
+ symbolGroup.removeAll();
+ for (let i = 0; i < newPoints.length - 1; i++) {
+ const symbolPath = createSymbol(data, idx);
+ if (symbolPath) {
+ symbolPath.__dimIdx = i;
+ if (oldPoints[i]) {
+ symbolPath.setPosition(oldPoints[i]);
+ graphic[isInit ? 'initProps' : 'updateProps'](
+ symbolPath, {
+ x: newPoints[i][0],
+ y: newPoints[i][1]
+ }, seriesModel, idx
+ );
+ }
+ else {
+ symbolPath.setPosition(newPoints[i]);
+ }
+ symbolGroup.add(symbolPath);
+ }
+ }
+ }
+
+ function getInitialPoints(points: number[][]) {
+ return zrUtil.map(points, function (pt) {
+ return [polar.cx, polar.cy];
+ });
+ }
+ data.diff(oldData)
+ .add(function (idx) {
+ const points = data.getItemLayout(idx);
+ if (!points) {
+ return;
+ }
+ const polygon = new graphic.Polygon();
+ const polyline = new graphic.Polyline();
+ const target = {
+ shape: {
+ points: points
+ }
+ };
+
+ polygon.shape.points = getInitialPoints(points);
+ polyline.shape.points = getInitialPoints(points);
+ graphic.initProps(polygon, target, seriesModel, idx);
+ graphic.initProps(polyline, target, seriesModel, idx);
+
+ const itemGroup = new graphic.Group();
+ const symbolGroup = new graphic.Group();
+ itemGroup.add(polyline);
+ itemGroup.add(polygon);
+ itemGroup.add(symbolGroup);
+
+ updateSymbols(
+ polyline.shape.points, points, symbolGroup, data, idx, true
+ );
+
+ data.setItemGraphicEl(idx, itemGroup);
+ })
+ .update(function (newIdx, oldIdx) {
+ const itemGroup = oldData.getItemGraphicEl(oldIdx) as graphic.Group;
+
+ const polyline = itemGroup.childAt(0) as graphic.Polyline;
+ const polygon = itemGroup.childAt(1) as graphic.Polygon;
+ const symbolGroup = itemGroup.childAt(2) as graphic.Group;
+ const target = {
+ shape: {
+ points: data.getItemLayout(newIdx)
+ }
+ };
+
+ if (!target.shape.points) {
+ return;
+ }
+ updateSymbols(
+ polyline.shape.points,
+ target.shape.points,
+ symbolGroup,
+ data,
+ newIdx,
+ false
+ );
+
+ graphic.updateProps(polyline, target, seriesModel);
+ graphic.updateProps(polygon, target, seriesModel);
+
+ data.setItemGraphicEl(newIdx, itemGroup);
+ })
+ .remove(function (idx) {
+ group.remove(oldData.getItemGraphicEl(idx));
+ })
+ .execute();
+
+ data.eachItemGraphicEl(function (itemGroup: graphic.Group, idx) {
+ const itemModel = data.getItemModel<RadarSeriesDataItemOption>(idx);
+ const polyline = itemGroup.childAt(0) as graphic.Polyline;
+ const polygon = itemGroup.childAt(1) as graphic.Polygon;
+ const symbolGroup = itemGroup.childAt(2) as graphic.Group;
+ // Radar uses the visual encoded from itemStyle.
+ const itemStyle = data.getItemVisual(idx, 'style');
+ const color = itemStyle.fill;
+
+ group.add(itemGroup);
+
+ polyline.useStyle(
+ zrUtil.defaults(
+ itemModel.getModel('lineStyle').getLineStyle(),
+ {
+ fill: 'none',
+ stroke: color
+ }
+ )
+ );
+
+ setStatesStylesFromModel(polyline, itemModel, 'lineStyle');
+ setStatesStylesFromModel(polygon, itemModel, 'areaStyle');
+
+ const areaStyleModel = itemModel.getModel('areaStyle');
+ const polygonIgnore = areaStyleModel.isEmpty() && areaStyleModel.parentModel.isEmpty();
+
+ polygon.ignore = polygonIgnore;
+
+ zrUtil.each(['emphasis', 'select', 'blur'] as const, function (stateName) {
+ const stateModel = itemModel.getModel([stateName, 'areaStyle']);
+ const stateIgnore = stateModel.isEmpty() && stateModel.parentModel.isEmpty();
+ // Won't be ignore if normal state is not ignore.
+ polygon.ensureState(stateName).ignore = stateIgnore && polygonIgnore;
+ });
+
+ polygon.useStyle(
+ zrUtil.defaults(
+ areaStyleModel.getAreaStyle(),
+ {
+ fill: color,
+ opacity: 0.7
+ }
+ )
+ );
+ const emphasisModel = itemModel.getModel('emphasis');
+ const itemHoverStyle = emphasisModel.getModel('itemStyle').getItemStyle();
+ symbolGroup.eachChild(function (symbolPath: RadarSymbol) {
+ if (symbolPath instanceof ZRImage) {
+ const pathStyle = symbolPath.style;
+ symbolPath.useStyle(zrUtil.extend({
+ // TODO other properties like x, y ?
+ image: pathStyle.image,
+ x: pathStyle.x, y: pathStyle.y,
+ width: pathStyle.width, height: pathStyle.height
+ }, itemStyle));
+ }
+ else {
+ symbolPath.useStyle(itemStyle);
+ symbolPath.setColor(color);
+ }
+
+ const pathEmphasisState = symbolPath.ensureState('emphasis');
+ pathEmphasisState.style = zrUtil.clone(itemHoverStyle);
+ let defaultText = data.get(data.dimensions[symbolPath.__dimIdx], idx);
+ (defaultText == null || isNaN(defaultText as number)) && (defaultText = '');
+
+ setLabelStyle(
+ symbolPath, getLabelStatesModels(itemModel),
+ {
+ labelFetcher: data.hostModel,
+ labelDataIndex: idx,
+ labelDimIndex: symbolPath.__dimIdx,
+ defaultText: defaultText as string,
+ inheritColor: color as ColorString
+ }
+ );
+ });
+
+ enableHoverEmphasis(itemGroup, emphasisModel.get('focus'), emphasisModel.get('blurScope'));
+ });
+
+ this._data = data;
+ }
+
+ remove() {
+ this.group.removeAll();
+ this._data = null;
+ }
+}
+
+ChartView.registerClass(RadarView);
diff --cc src/chart/sunburst/SunburstSeries.ts
index fff56bf,0000000..20cd920
mode 100644,000000..100644
--- a/src/chart/sunburst/SunburstSeries.ts
+++ b/src/chart/sunburst/SunburstSeries.ts
@@@ -1,303 -1,0 +1,311 @@@
+/*
+* 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 zrUtil from 'zrender/src/core/util';
+import SeriesModel from '../../model/Series';
+import Tree, { TreeNode } from '../../data/Tree';
+import {wrapTreePathInfo} from '../helper/treeHelper';
+import {
+ SeriesOption,
+ CircleLayoutOptionMixin,
+ LabelOption,
+ ItemStyleOption,
+ OptionDataValue,
+ CallbackDataParams,
+ StatesOptionMixin,
+ OptionDataItemObject
+} from '../../util/types';
+import GlobalModel from '../../model/Global';
++import List from '../../data/List';
++import Model from '../../model/Model';
+
+interface SunburstLabelOption extends Omit<LabelOption, 'rotate' | 'position'> {
+ rotate?: 'radial' | 'tangential' | number
+ minAngle?: number
+ silent?: boolean
+ position?: LabelOption['position'] | 'outside'
+}
+
+interface SunburstDataParams extends CallbackDataParams {
+ treePathInfo: {
+ name: string,
+ dataIndex: number
+ value: SunburstSeriesNodeItemOption['value']
+ }[]
+}
+
+interface ExtraStateOption {
+ emphasis?: {
+ focus?: 'descendant' | 'ancestor'
+ }
+}
+
+export interface SunburstStateOption {
+ itemStyle?: ItemStyleOption
+ label?: SunburstLabelOption
+}
+
+export interface SunburstSeriesNodeItemOption extends
+ SunburstStateOption, StatesOptionMixin<SunburstStateOption, ExtraStateOption>,
+ OptionDataItemObject<OptionDataValue>
+{
+ nodeClick?: 'rootToNode' | 'link'
+ // Available when nodeClick is link
+ link?: string
+ target?: string
+
+ children?: SunburstSeriesNodeItemOption[]
+
+ collapsed?: boolean
+
+ cursor?: string
+}
+export interface SunburstSeriesLevelOption extends SunburstStateOption, StatesOptionMixin<SunburstStateOption> {
+ highlight?: {
+ itemStyle?: ItemStyleOption
+ label?: SunburstLabelOption
+ }
+}
+export interface SunburstSeriesOption extends
+ SeriesOption<SunburstStateOption, ExtraStateOption>, SunburstStateOption,
+ CircleLayoutOptionMixin {
+
+ type?: 'sunburst'
+
+ clockwise?: boolean
+ startAngle?: number
+ minAngle?: number
+ /**
+ * If still show when all data zero.
+ */
+ stillShowZeroSum?: boolean
+ /**
+ * Policy of highlighting pieces when hover on one
+ * Valid values: 'none' (for not downplay others), 'descendant',
+ * 'ancestor', 'self'
+ */
+ // highlightPolicy?: 'descendant' | 'ancestor' | 'self'
+
+ nodeClick?: 'rootToNode' | 'link'
+
+ renderLabelForZeroData?: boolean
+
+ levels?: SunburstSeriesLevelOption[]
+
+ animationType?: 'expansion' | 'scale'
+
+ sort?: 'desc' | 'asc' | ((a: TreeNode, b: TreeNode) => number)
+}
+
+interface SunburstSeriesModel {
+ getFormattedLabel(
+ dataIndex: number,
+ state?: 'emphasis' | 'normal' | 'highlight' | 'blur' | 'select'
+ ): string
+}
+class SunburstSeriesModel extends SeriesModel<SunburstSeriesOption> {
+
+ static readonly type = 'series.sunburst';
+ readonly type = SunburstSeriesModel.type;
+
+ ignoreStyleOnData = true;
+
+ private _viewRoot: TreeNode;
+
+ getInitialData(option: SunburstSeriesOption, ecModel: GlobalModel) {
+ // Create a virtual root.
+ const root = { name: option.name, children: option.data } as SunburstSeriesNodeItemOption;
+
+ completeTreeValue(root);
+
- const levels = option.levels || [];
-
- // levels = option.levels = setDefault(levels, ecModel);
-
- const treeOption = {
- levels: levels
- };
++ const levelModels = zrUtil.map(option.levels || [], function (levelDefine) {
++ return new Model(levelDefine, this, ecModel);
++ }, this);
+
+ // Make sure always a new tree is created when setOption,
+ // in TreemapView, we check whether oldTree === newTree
+ // to choose mappings approach among old shapes and new shapes.
- return Tree.createTree(root, this, treeOption).data;
++ const tree = Tree.createTree(root, this, beforeLink);
++
++ function beforeLink(nodeData: List) {
++ nodeData.wrapMethod('getItemModel', function (model, idx) {
++ const node = tree.getNodeByDataIndex(idx);
++ const levelModel = levelModels[node.depth];
++ levelModel && (model.parentModel = levelModel);
++ return model;
++ });
++ }
++ return tree.data;
+ }
+
+ optionUpdated() {
+ this.resetViewRoot();
+ }
+
+ /*
+ * @override
+ */
+ getDataParams(dataIndex: number) {
+ const params = super.getDataParams.apply(this, arguments as any) as SunburstDataParams;
+
+ const node = this.getData().tree.getNodeByDataIndex(dataIndex);
+ params.treePathInfo = wrapTreePathInfo<SunburstSeriesNodeItemOption['value']>(node, this);
+
+ return params;
+ }
+
+ static defaultOption: SunburstSeriesOption = {
+ zlevel: 0,
+ z: 2,
+
+ // 默认全局居中
+ center: ['50%', '50%'],
+ radius: [0, '75%'],
+ // 默认顺时针
+ clockwise: true,
+ startAngle: 90,
+ // 最小角度改为0
+ minAngle: 0,
+
+ // If still show when all data zero.
+ stillShowZeroSum: true,
+
+ // 'rootToNode', 'link', or false
+ nodeClick: 'rootToNode',
+
+ renderLabelForZeroData: false,
+
+ label: {
+ // could be: 'radial', 'tangential', or 'none'
+ rotate: 'radial',
+ show: true,
+ opacity: 1,
+ // 'left' is for inner side of inside, and 'right' is for outter
+ // side for inside
+ align: 'center',
+ position: 'inside',
+ distance: 5,
+ silent: true
+ },
+ itemStyle: {
+ borderWidth: 1,
+ borderColor: 'white',
+ borderType: 'solid',
+ shadowBlur: 0,
+ shadowColor: 'rgba(0, 0, 0, 0.2)',
+ shadowOffsetX: 0,
+ shadowOffsetY: 0,
+ opacity: 1
+ },
+
+ emphasis: {
+ focus: 'descendant'
+ },
+
+ blur: {
+ itemStyle: {
+ opacity: 0.2
+ },
+ label: {
+ opacity: 0.1
+ }
+ },
+
+ // Animation type canbe expansion, scale
+ animationType: 'expansion',
+ animationDuration: 1000,
+ animationDurationUpdate: 500,
+
+ data: [],
+
+ levels: [],
+
+ /**
+ * Sort order.
+ *
+ * Valid values: 'desc', 'asc', null, or callback function.
+ * 'desc' and 'asc' for descend and ascendant order;
+ * null for not sorting;
+ * example of callback function:
+ * function(nodeA, nodeB) {
+ * return nodeA.getValue() - nodeB.getValue();
+ * }
+ */
+ sort: 'desc'
+ };
+
+ getViewRoot() {
+ return this._viewRoot;
+ }
+
+ resetViewRoot(viewRoot?: TreeNode) {
+ viewRoot
+ ? (this._viewRoot = viewRoot)
+ : (viewRoot = this._viewRoot);
+
+ const root = this.getRawData().tree.root;
+
+ if (!viewRoot
+ || (viewRoot !== root && !root.contains(viewRoot))
+ ) {
+ this._viewRoot = root;
+ }
+ }
+}
+
+
+
+function completeTreeValue(dataNode: SunburstSeriesNodeItemOption) {
+ // Postorder travel tree.
+ // If value of none-leaf node is not set,
+ // calculate it by suming up the value of all children.
+ let sum = 0;
+
+ zrUtil.each(dataNode.children, function (child) {
+
+ completeTreeValue(child);
+
+ let childValue = child.value;
+ // TODO First value of array must be a number
+ zrUtil.isArray(childValue) && (childValue = childValue[0]);
+ sum += childValue as number;
+ });
+
+ let thisValue = dataNode.value as number;
+ if (zrUtil.isArray(thisValue)) {
+ thisValue = thisValue[0];
+ }
+
+ if (thisValue == null || isNaN(thisValue)) {
+ thisValue = sum;
+ }
+ // Value should not less than 0.
+ if (thisValue < 0) {
+ thisValue = 0;
+ }
+
+ zrUtil.isArray(dataNode.value)
+ ? (dataNode.value[0] = thisValue)
+ : (dataNode.value = thisValue);
+}
+
+
+SeriesModel.registerClass(SunburstSeriesModel);
+
+export default SunburstSeriesModel;
diff --cc src/chart/themeRiver/ThemeRiverSeries.ts
index d1650e5,0000000..2bf5d4f
mode 100644,000000..100644
--- a/src/chart/themeRiver/ThemeRiverSeries.ts
+++ b/src/chart/themeRiver/ThemeRiverSeries.ts
@@@ -1,332 -1,0 +1,325 @@@
+/*
+* 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 SeriesModel from '../../model/Series';
+import createDimensions from '../../data/helper/createDimensions';
+import {getDimensionTypeByAxis} from '../../data/helper/dimensionHelper';
+import List from '../../data/List';
+import * as zrUtil from 'zrender/src/core/util';
+import {groupData, SINGLE_REFERRING} from '../../util/model';
+import LegendVisualProvider from '../../visual/LegendVisualProvider';
+import {
+ SeriesOption,
+ SeriesOnSingleOptionMixin,
+ LabelOption,
+ OptionDataValueDate,
+ OptionDataValueNumeric,
+ ItemStyleOption,
+ BoxLayoutOptionMixin,
- ZRColor
++ ZRColor,
++ Dictionary
+} from '../../util/types';
+import SingleAxis from '../../coord/single/SingleAxis';
+import GlobalModel from '../../model/Global';
+import Single from '../../coord/single/Single';
+import { createTooltipMarkup } from '../../component/tooltip/tooltipMarkup';
+
+const DATA_NAME_INDEX = 2;
+
+interface ThemeRiverSeriesLabelOption extends LabelOption {
+ margin?: number
+}
+
++type ThemerRiverDataItem = [OptionDataValueDate, OptionDataValueNumeric, string];
++
+export interface ThemeRiverStateOption {
+ label?: ThemeRiverSeriesLabelOption
+ itemStyle?: ItemStyleOption
+}
+
+export interface ThemeRiverSeriesOption extends SeriesOption<ThemeRiverStateOption>, ThemeRiverStateOption,
+ SeriesOnSingleOptionMixin, BoxLayoutOptionMixin {
+ type?: 'themeRiver'
+
+ color?: ZRColor[]
+
+ coordinateSystem?: 'singleAxis'
+
+ /**
+ * gap in axis's orthogonal orientation
+ */
+ boundaryGap?: (string | number)[]
+ /**
+ * [date, value, name]
+ */
- data?: [OptionDataValueDate, OptionDataValueNumeric, string][]
++ data?: ThemerRiverDataItem[]
+}
+
+class ThemeRiverSeriesModel extends SeriesModel<ThemeRiverSeriesOption> {
+ static readonly type = 'series.themeRiver';
+ readonly type = ThemeRiverSeriesModel.type;
+
+ static readonly dependencies = ['singleAxis'];
+
+ nameMap: zrUtil.HashMap<number, string>;
+
+ coordinateSystem: Single;
+
+ useColorPaletteOnData = true;
+
+ /**
+ * @override
+ */
+ init(option: ThemeRiverSeriesOption) {
+ // eslint-disable-next-line
+ super.init.apply(this, arguments as any);
+
+ // Put this function here is for the sake of consistency of code style.
+ // Enable legend selection for each data item
+ // Use a function instead of direct access because data reference may changed
+ this.legendVisualProvider = new LegendVisualProvider(
+ zrUtil.bind(this.getData, this), zrUtil.bind(this.getRawData, this)
+ );
+ }
+
+ /**
+ * If there is no value of a certain point in the time for some event,set it value to 0.
+ *
+ * @param {Array} data initial data in the option
+ * @return {Array}
+ */
+ fixData(data: ThemeRiverSeriesOption['data']) {
+ let rawDataLength = data.length;
++ /**
++ * Make sure every layer data get the same keys.
++ * The value index tells which layer has visited.
++ * {
++ * 2014/01/01: -1
++ * }
++ */
++ const timeValueKeys: Dictionary<number> = {};
+
+ // grouped data by name
- const groupResult = groupData(data, function (item) {
++ const groupResult = groupData(data, (item: ThemerRiverDataItem) => {
++ if (!timeValueKeys.hasOwnProperty(item[0] + '')) {
++ timeValueKeys[item[0] + ''] = -1;
++ }
+ return item[2];
+ });
- const layData: {
- name: string,
- dataList: ThemeRiverSeriesOption['data']
- }[] = [];
++ const layerData: {name: string, dataList: ThemerRiverDataItem[]}[] = [];
+ groupResult.buckets.each(function (items, key) {
- layData.push({
- name: key,
- dataList: items
++ layerData.push({
++ name: key, dataList: items
+ });
+ });
-
- const layerNum = layData.length;
- let largestLayer = -1;
- let index = -1;
- for (let i = 0; i < layerNum; ++i) {
- const len = layData[i].dataList.length;
- if (len > largestLayer) {
- largestLayer = len;
- index = i;
- }
- }
++ const layerNum = layerData.length;
+
+ for (let k = 0; k < layerNum; ++k) {
- if (k === index) {
- continue;
++ const name = layerData[k].name;
++ for (let j = 0; j < layerData[k].dataList.length; ++j) {
++ const timeValue = layerData[k].dataList[j][0] = '';
++ timeValueKeys[timeValue] = k;
+ }
- const name = layData[k].name;
- for (let j = 0; j < largestLayer; ++j) {
- const timeValue = layData[index].dataList[j][0];
- const length = layData[k].dataList.length;
- let keyIndex = -1;
- for (let l = 0; l < length; ++l) {
- const value = layData[k].dataList[l][0];
- if (value === timeValue) {
- keyIndex = l;
- break;
- }
- }
- if (keyIndex === -1) {
++
++ for (const timeValue in timeValueKeys) {
++ if (timeValueKeys.hasOwnProperty(timeValue) && timeValueKeys[timeValue] !== k) {
++ timeValueKeys[timeValue] = k;
+ data[rawDataLength] = [timeValue, 0, name];
+ rawDataLength++;
-
+ }
+ }
++
+ }
+ return data;
+ }
+
+ /**
+ * @override
+ * @param option the initial option that user gived
+ * @param ecModel the model object for themeRiver option
+ */
+ getInitialData(option: ThemeRiverSeriesOption, ecModel: GlobalModel): List {
+
+ const singleAxisModel = this.getReferringComponents('singleAxis', SINGLE_REFERRING).models[0];
+
+ const axisType = singleAxisModel.get('type');
+
+ // filter the data item with the value of label is undefined
+ const filterData = zrUtil.filter(option.data, function (dataItem) {
+ return dataItem[2] !== undefined;
+ });
+
+ // ??? TODO design a stage to transfer data for themeRiver and lines?
+ const data = this.fixData(filterData || []);
+ const nameList = [];
+ const nameMap = this.nameMap = zrUtil.createHashMap();
+ let count = 0;
+
+ for (let i = 0; i < data.length; ++i) {
+ nameList.push(data[i][DATA_NAME_INDEX]);
+ if (!nameMap.get(data[i][DATA_NAME_INDEX] as string)) {
+ nameMap.set(data[i][DATA_NAME_INDEX] as string, count);
+ count++;
+ }
+ }
+
+ const dimensionsInfo = createDimensions(data, {
+ coordDimensions: ['single'],
+ dimensionsDefine: [
+ {
+ name: 'time',
+ type: getDimensionTypeByAxis(axisType)
+ },
+ {
+ name: 'value',
+ type: 'float'
+ },
+ {
+ name: 'name',
+ type: 'ordinal'
+ }
+ ],
+ encodeDefine: {
+ single: 0,
+ value: 1,
+ itemName: 2
+ }
+ });
+
+ const list = new List(dimensionsInfo, this);
+ list.initData(data);
+
+ return list;
+ }
+
+ /**
+ * The raw data is divided into multiple layers and each layer
+ * has same name.
+ */
+ getLayerSeries() {
+ const data = this.getData();
+ const lenCount = data.count();
+ const indexArr = [];
+
+ for (let i = 0; i < lenCount; ++i) {
+ indexArr[i] = i;
+ }
+
+ const timeDim = data.mapDimension('single');
+
+ // data group by name
+ const groupResult = groupData(indexArr, function (index) {
+ return data.get('name', index) as string;
+ });
+ const layerSeries: {
+ name: string
+ indices: number[]
+ }[] = [];
+ groupResult.buckets.each(function (items: number[], key: string) {
+ items.sort(function (index1: number, index2: number) {
+ return data.get(timeDim, index1) as number - (data.get(timeDim, index2) as number);
+ });
+ layerSeries.push({
+ name: key,
+ indices: items
+ });
+ });
+
+ return layerSeries;
+ }
+
+ /**
+ * Get data indices for show tooltip content
+ */
+ getAxisTooltipData(dim: string | string[], value: number, baseAxis: SingleAxis) {
+ if (!zrUtil.isArray(dim)) {
+ dim = dim ? [dim] : [];
+ }
+
+ const data = this.getData();
+ const layerSeries = this.getLayerSeries();
+ const indices = [];
+ const layerNum = layerSeries.length;
+ let nestestValue;
+
+ for (let i = 0; i < layerNum; ++i) {
+ let minDist = Number.MAX_VALUE;
+ let nearestIdx = -1;
+ const pointNum = layerSeries[i].indices.length;
+ for (let j = 0; j < pointNum; ++j) {
+ const theValue = data.get(dim[0], layerSeries[i].indices[j]) as number;
+ const dist = Math.abs(theValue - value);
+ if (dist <= minDist) {
+ nestestValue = theValue;
+ minDist = dist;
+ nearestIdx = layerSeries[i].indices[j];
+ }
+ }
+ indices.push(nearestIdx);
+ }
+
+ return {dataIndices: indices, nestestValue: nestestValue};
+ }
+
+ formatTooltip(
+ dataIndex: number,
+ multipleSeries: boolean,
+ dataType: string
+ ) {
+ const data = this.getData();
+ const name = data.getName(dataIndex);
+ const value = data.get(data.mapDimension('value'), dataIndex);
+
+ return createTooltipMarkup('nameValue', { name: name, value: value });
+ }
+
+ static defaultOption: ThemeRiverSeriesOption = {
+ zlevel: 0,
+ z: 2,
+
+ coordinateSystem: 'singleAxis',
+
+ // gap in axis's orthogonal orientation
+ boundaryGap: ['10%', '10%'],
+
+ // legendHoverLink: true,
+
+ singleAxisIndex: 0,
+
+ animationEasing: 'linear',
+
+ label: {
+ margin: 4,
+ show: true,
+ position: 'left',
+ fontSize: 11
+ },
+
+ emphasis: {
+
+ label: {
+ show: true
+ }
+ }
+ };
+}
+
+SeriesModel.registerClass(ThemeRiverSeriesModel);
+
+export default ThemeRiverSeriesModel;
diff --cc src/chart/tree/TreeSeries.ts
index e5a49f6,0000000..912e9a6
mode 100644,000000..100644
--- a/src/chart/tree/TreeSeries.ts
+++ b/src/chart/tree/TreeSeries.ts
@@@ -1,291 -1,0 +1,291 @@@
+/*
+* 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 SeriesModel from '../../model/Series';
+import Tree from '../../data/Tree';
+import {
+ SeriesOption,
+ SymbolOptionMixin,
+ BoxLayoutOptionMixin,
+ RoamOptionMixin,
+ LineStyleOption,
+ ItemStyleOption,
+ LabelOption,
+ OptionDataValue,
+ StatesOptionMixin,
+ OptionDataItemObject
+} from '../../util/types';
+import List from '../../data/List';
+import View from '../../coord/View';
+import { LayoutRect } from '../../util/layout';
+import Model from '../../model/Model';
+import { createTooltipMarkup } from '../../component/tooltip/tooltipMarkup';
+
+interface CurveLineStyleOption extends LineStyleOption{
+ curveness?: number
+}
+
+export interface TreeSeriesStateOption {
+ itemStyle?: ItemStyleOption
+ /**
+ * Line style of the edge between node and it's parent.
+ */
+ lineStyle?: CurveLineStyleOption
+ label?: LabelOption
+}
+
+interface ExtraStateOption {
+ emphasis?: {
+ focus?: 'ancestor' | 'descendant'
+ scale?: boolean
+ }
+}
+
+export interface TreeSeriesNodeItemOption extends SymbolOptionMixin,
+ TreeSeriesStateOption, StatesOptionMixin<TreeSeriesStateOption, ExtraStateOption>,
+ OptionDataItemObject<OptionDataValue> {
+
+ children?: TreeSeriesNodeItemOption[]
+
+ collapsed?: boolean
+
+ link?: string
+ target?: string
+}
+
+/**
+ * Configuration of leaves nodes.
+ */
+export interface TreeSeriesLeavesOption extends TreeSeriesStateOption, StatesOptionMixin<TreeSeriesStateOption> {
+
+}
+
+export interface TreeSeriesOption extends
+ SeriesOption<TreeSeriesStateOption, ExtraStateOption>, TreeSeriesStateOption,
+ SymbolOptionMixin, BoxLayoutOptionMixin, RoamOptionMixin {
+ type?: 'tree'
+
+ layout?: 'orthogonal' | 'radial'
+
+ edgeShape?: 'polyline' | 'curve'
+
+ /**
+ * Available when edgeShape is polyline
+ */
+ edgeForkPosition?: string | number
+
+ nodeScaleRatio?: number
+
+ /**
+ * The orient of orthoginal layout, can be setted to 'LR', 'TB', 'RL', 'BT'.
+ * and the backward compatibility configuration 'horizontal = LR', 'vertical = TB'.
+ */
+ orient?: 'LR' | 'TB' | 'RL' | 'BT' | 'horizontal' | 'vertical'
+
+ expandAndCollapse?: boolean
+
+ /**
+ * The initial expanded depth of tree
+ */
+ initialTreeDepth?: number
+
+ leaves?: TreeSeriesLeavesOption
+
+ data?: TreeSeriesNodeItemOption[]
+}
+
+class TreeSeriesModel extends SeriesModel<TreeSeriesOption> {
+ static readonly type = 'series.tree';
+
+ // can support the position parameters 'left', 'top','right','bottom', 'width',
+ // 'height' in the setOption() with 'merge' mode normal.
+ static readonly layoutMode = 'box';
+
+ coordinateSystem: View;
+
+ layoutInfo: LayoutRect;
+
+ hasSymbolVisual = true;
+
+ // Do it self.
+ ignoreStyleOnData = true;
+
+ /**
+ * Init a tree data structure from data in option series
+ * @param option the object used to config echarts view
+ * @return storage initial data
+ */
+ getInitialData(option: TreeSeriesOption): List {
+
+ //create an virtual root
+ const root: TreeSeriesNodeItemOption = {
+ name: option.name,
+ children: option.data
+ };
+
+ const leaves = option.leaves || {};
+ const leavesModel = new Model(leaves, this, this.ecModel);
+
- const tree = Tree.createTree(root, this, {}, beforeLink);
++ const tree = Tree.createTree(root, this, beforeLink);
+
+ function beforeLink(nodeData: List) {
+ nodeData.wrapMethod('getItemModel', function (model, idx) {
+ const node = tree.getNodeByDataIndex(idx);
+ if (!node.children.length || !node.isExpand) {
+ model.parentModel = leavesModel;
+ }
+ return model;
+ });
+ }
+
+ let treeDepth = 0;
+
+ tree.eachNode('preorder', function (node) {
+ if (node.depth > treeDepth) {
+ treeDepth = node.depth;
+ }
+ });
+
+ const expandAndCollapse = option.expandAndCollapse;
+ const expandTreeDepth = (expandAndCollapse && option.initialTreeDepth >= 0)
+ ? option.initialTreeDepth : treeDepth;
+
+ tree.root.eachNode('preorder', function (node) {
+ const item = node.hostTree.data.getRawDataItem(node.dataIndex) as TreeSeriesNodeItemOption;
+ // Add item.collapsed != null, because users can collapse node original in the series.data.
+ node.isExpand = (item && item.collapsed != null)
+ ? !item.collapsed
+ : node.depth <= expandTreeDepth;
+ });
+
+ return tree.data;
+ }
+
+ /**
+ * Make the configuration 'orient' backward compatibly, with 'horizontal = LR', 'vertical = TB'.
+ * @returns {string} orient
+ */
+ getOrient() {
+ let orient = this.get('orient');
+ if (orient === 'horizontal') {
+ orient = 'LR';
+ }
+ else if (orient === 'vertical') {
+ orient = 'TB';
+ }
+ return orient;
+ }
+
+ setZoom(zoom: number) {
+ this.option.zoom = zoom;
+ }
+
+ setCenter(center: number[]) {
+ this.option.center = center;
+ }
+
+ formatTooltip(
+ dataIndex: number,
+ multipleSeries: boolean,
+ dataType: string
+ ) {
+ const tree = this.getData().tree;
+ const realRoot = tree.root.children[0];
+ let node = tree.getNodeByDataIndex(dataIndex);
+ const value = node.getValue();
+ let name = node.name;
+ while (node && (node !== realRoot)) {
+ name = node.parentNode.name + '.' + name;
+ node = node.parentNode;
+ }
+
+ return createTooltipMarkup('nameValue', {
+ name: name,
+ value: value,
+ noValue: isNaN(value as number) || value == null
+ });
+ }
+
+ static defaultOption: TreeSeriesOption = {
+ zlevel: 0,
+ z: 2,
+ coordinateSystem: 'view',
+
+ // the position of the whole view
+ left: '12%',
+ top: '12%',
+ right: '12%',
+ bottom: '12%',
+
+ // the layout of the tree, two value can be selected, 'orthogonal' or 'radial'
+ layout: 'orthogonal',
+
+ // value can be 'polyline'
+ edgeShape: 'curve',
+
+ edgeForkPosition: '50%',
+
+ // true | false | 'move' | 'scale', see module:component/helper/RoamController.
+ roam: false,
+
+ // Symbol size scale ratio in roam
+ nodeScaleRatio: 0.4,
+
+ // Default on center of graph
+ center: null,
+
+ zoom: 1,
+
+ orient: 'LR',
+
+ symbol: 'emptyCircle',
+
+ symbolSize: 7,
+
+ expandAndCollapse: true,
+
+ initialTreeDepth: 2,
+
+ lineStyle: {
+ color: '#ccc',
+ width: 1.5,
+ curveness: 0.5
+ },
+
+ itemStyle: {
+ color: 'lightsteelblue',
+ borderColor: '#c23531',
+ borderWidth: 1.5
+ },
+
+ label: {
+ show: true
+ },
+
+ animationEasing: 'linear',
+
+ animationDuration: 700,
+
+ animationDurationUpdate: 500
+ };
+}
+
+SeriesModel.registerClass(TreeSeriesModel);
+
+export default TreeSeriesModel;
diff --cc src/chart/treemap/TreemapSeries.ts
index 5444a26,0000000..9bc1852
mode 100644,000000..100644
--- a/src/chart/treemap/TreemapSeries.ts
+++ b/src/chart/treemap/TreemapSeries.ts
@@@ -1,543 -1,0 +1,553 @@@
+/*
+* 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 zrUtil from 'zrender/src/core/util';
+import SeriesModel from '../../model/Series';
+import Tree, { TreeNode } from '../../data/Tree';
+import Model from '../../model/Model';
+import {wrapTreePathInfo} from '../helper/treeHelper';
+import {
+ SeriesOption,
+ BoxLayoutOptionMixin,
+ ItemStyleOption,
+ LabelOption,
+ RoamOptionMixin,
+ CallbackDataParams,
+ ColorString,
+ StatesOptionMixin
+} from '../../util/types';
+import GlobalModel from '../../model/Global';
+import { LayoutRect } from '../../util/layout';
+import List from '../../data/List';
+import { normalizeToArray } from '../../util/model';
+import { createTooltipMarkup } from '../../component/tooltip/tooltipMarkup';
+
+// Only support numberic value.
+type TreemapSeriesDataValue = number | number[];
+
+interface BreadcrumbItemStyleOption extends ItemStyleOption {
+ // TODO: textStyle should be in breadcrumb.label
+ textStyle?: LabelOption
+}
+
+interface TreemapSeriesLabelOption extends LabelOption {
+ ellipsis?: boolean
+ formatter?: string | ((params: CallbackDataParams) => string)
+}
+
+interface TreemapSeriesItemStyleOption extends ItemStyleOption {
+ borderRadius?: number | number[]
+
+ colorAlpha?: number
+ colorSaturation?: number
+
+ borderColorSaturation?: number
+
+ gapWidth?: number
+}
+
+interface TreePathInfo {
+ name: string
+ dataIndex: number
+ value: TreemapSeriesDataValue
+}
+
+interface TreemapSeriesCallbackDataParams extends CallbackDataParams {
+ treePathInfo?: TreePathInfo[]
+}
+
+interface ExtraStateOption {
+ emphasis?: {
+ focus?: 'descendant' | 'ancestor'
+ }
+}
+
+export interface TreemapStateOption {
+ itemStyle?: TreemapSeriesItemStyleOption
+ label?: TreemapSeriesLabelOption
+ upperLabel?: TreemapSeriesLabelOption
+}
+
+export interface TreemapSeriesVisualOption {
+ /**
+ * Which dimension will be applied with the visual properties.
+ */
+ visualDimension?: number | string
+
+ colorMappingBy?: 'value' | 'index' | 'id'
+
+ visualMin?: number
+ visualMax?: number
+
+ colorAlpha?: number[] | 'none'
+ colorSaturation?: number[] | 'none'
+ // A color list for a level. Each node in the level will obtain a color from the color list.
+ // Only suuport ColorString for interpolation
+ // color?: ColorString[]
+
+ /**
+ * A node will not be shown when its area size is smaller than this value (unit: px square).
+ */
+ visibleMin?: number
+ /**
+ * Children will not be shown when area size of a node is smaller than this value (unit: px square).
+ */
+ childrenVisibleMin?: number
+}
+
+export interface TreemapSeriesLevelOption extends TreemapSeriesVisualOption,
+ TreemapStateOption, StatesOptionMixin<TreemapStateOption, ExtraStateOption> {
+
+ color?: ColorString[] | 'none'
+}
+
+export interface TreemapSeriesNodeItemOption extends TreemapSeriesVisualOption,
+ TreemapStateOption, StatesOptionMixin<TreemapStateOption, ExtraStateOption> {
+ id?: string
+ name?: string
+
+ value?: TreemapSeriesDataValue
+
+ children?: TreemapSeriesNodeItemOption[]
+
+ color?: ColorString[] | 'none'
+}
+
+export interface TreemapSeriesOption
+ extends SeriesOption<TreemapStateOption, ExtraStateOption>, TreemapStateOption,
+ BoxLayoutOptionMixin,
+ RoamOptionMixin,
+ TreemapSeriesVisualOption {
+
+ type?: 'treemap'
+
+ /**
+ * configuration in echarts2
+ * @deprecated
+ */
+ size?: (number | string)[]
+
+ /**
+ * If sort in desc order.
+ * Default to be desc. asc has strange effect
+ */
+ sort?: boolean | 'asc' | 'desc'
+
+ /**
+ * Size of clipped window when zooming. 'origin' or 'fullscreen'
+ */
+ clipWindow?: 'origin' | 'fullscreen'
+
+ squareRatio: number
+ /**
+ * Nodes on depth from root are regarded as leaves.
+ * Count from zero (zero represents only view root).
+ */
+ leafDepth: number
+
+ drillDownIcon?: string
+
+ /**
+ * Be effective when using zoomToNode. Specify the proportion of the
+ * target node area in the view area.
+ */
+ zoomToNodeRatio?: number
+ /**
+ * Leaf node click behaviour: 'zoomToNode', 'link', false.
+ * If leafDepth is set and clicking a node which has children but
+ * be on left depth, the behaviour would be changing root. Otherwise
+ * use behavious defined above.
+ */
+ nodeClick?: 'zoomToNode' | 'link'
+
+ breadcrumb?: BoxLayoutOptionMixin & {
+ show?: boolean
+ height?: number
+
+ emptyItemWidth: number // With of empty width
+ itemStyle?: BreadcrumbItemStyleOption
+
+ emphasis?: {
+ itemStyle?: BreadcrumbItemStyleOption
+ }
+ }
+
+ levels?: TreemapSeriesLevelOption[]
+
+ data?: TreemapSeriesNodeItemOption[]
+}
+
+class TreemapSeriesModel extends SeriesModel<TreemapSeriesOption> {
+
+ static type = 'series.treemap';
+ type = TreemapSeriesModel.type;
+
+ static layoutMode = 'box' as const;
+
+ preventUsingHoverLayer = true;
+
+ layoutInfo: LayoutRect;
+
++ designatedVisualItemStyle: TreemapSeriesItemStyleOption;
++
+ private _viewRoot: TreeNode;
+ private _idIndexMap: zrUtil.HashMap<number>;
+ private _idIndexMapCount: number;
+
+ static defaultOption: TreemapSeriesOption = {
+ // Disable progressive rendering
+ progressive: 0,
+ // size: ['80%', '80%'], // deprecated, compatible with ec2.
+ left: 'center',
+ top: 'middle',
+ width: '80%',
+ height: '80%',
+ sort: true,
+
+ clipWindow: 'origin',
+ squareRatio: 0.5 * (1 + Math.sqrt(5)), // golden ratio
+ leafDepth: null,
+
+ drillDownIcon: '▶', // Use html character temporarily because it is complicated
+ // to align specialized icon. ▷▶❒❐▼✚
+
+ zoomToNodeRatio: 0.32 * 0.32,
+
+ roam: true,
+ nodeClick: 'zoomToNode',
+ animation: true,
+ animationDurationUpdate: 900,
+ animationEasing: 'quinticInOut',
+ breadcrumb: {
+ show: true,
+ height: 22,
+ left: 'center',
+ top: 'bottom',
+ // right
+ // bottom
+ emptyItemWidth: 25, // Width of empty node.
+ itemStyle: {
+ color: 'rgba(0,0,0,0.7)', //'#5793f3',
+ textStyle: {
+ color: '#fff'
+ }
+ }
+ },
+ label: {
+ show: true,
+ // Do not use textDistance, for ellipsis rect just the same as treemap node rect.
+ distance: 0,
+ padding: 5,
+ position: 'inside', // Can be [5, '5%'] or position stirng like 'insideTopLeft', ...
+ // formatter: null,
+ color: '#fff',
+ overflow: 'truncate'
+ // align
+ // verticalAlign
+ },
+ upperLabel: { // Label when node is parent.
+ show: false,
+ position: [0, '50%'],
+ height: 20,
+ // formatter: null,
+ // color: '#fff',
+ overflow: 'truncate',
+ // align: null,
+ verticalAlign: 'middle'
+ },
+ itemStyle: {
+ color: null, // Can be 'none' if not necessary.
+ colorAlpha: null, // Can be 'none' if not necessary.
+ colorSaturation: null, // Can be 'none' if not necessary.
+ borderWidth: 0,
+ gapWidth: 0,
+ borderColor: '#fff',
+ borderColorSaturation: null // If specified, borderColor will be ineffective, and the
+ // border color is evaluated by color of current node and
+ // borderColorSaturation.
+ },
+ emphasis: {
+ upperLabel: {
+ show: true,
+ position: [0, '50%'],
+ ellipsis: true,
+ verticalAlign: 'middle'
+ }
+ },
+
+ visualDimension: 0, // Can be 0, 1, 2, 3.
+ visualMin: null,
+ visualMax: null,
+
+ color: [], // + treemapSeries.color should not be modified. Please only modified
+ // level[n].color (if necessary).
+ // + Specify color list of each level. level[0].color would be global
+ // color list if not specified. (see method `setDefault`).
+ // + But set as a empty array to forbid fetch color from global palette
+ // when using nodeModel.get('color'), otherwise nodes on deep level
+ // will always has color palette set and are not able to inherit color
+ // from parent node.
+ // + TreemapSeries.color can not be set as 'none', otherwise effect
+ // legend color fetching (see seriesColor.js).
+ colorAlpha: null, // Array. Specify color alpha range of each level, like [0.2, 0.8]
+ colorSaturation: null, // Array. Specify color saturation of each level, like [0.2, 0.5]
+ colorMappingBy: 'index', // 'value' or 'index' or 'id'.
+ visibleMin: 10, // If area less than this threshold (unit: pixel^2), node will not
+ // be rendered. Only works when sort is 'asc' or 'desc'.
+ childrenVisibleMin: null, // If area of a node less than this threshold (unit: pixel^2),
+ // grandchildren will not show.
+ // Why grandchildren? If not grandchildren but children,
+ // some siblings show children and some not,
+ // the appearance may be mess and not consistent,
+ levels: [] // Each item: {
+ // visibleMin, itemStyle, visualDimension, label
+ // }
+ // data: {
+ // value: [],
+ // children: [],
+ // link: 'http://xxx.xxx.xxx',
+ // target: 'blank' or 'self'
+ // }
+ };
+
+ /**
+ * @override
+ */
+ getInitialData(option: TreemapSeriesOption, ecModel: GlobalModel) {
+ // Create a virtual root.
+ const root: TreemapSeriesNodeItemOption = {
+ name: option.name,
+ children: option.data
+ };
+
+ completeTreeValue(root);
+
+ let levels = option.levels || [];
+
++ // Used in "visual priority" in `treemapVisual.js`.
++ // This way is a little tricky, must satisfy the precondition:
++ // 1. There is no `treeNode.getModel('itemStyle.xxx')` used.
++ // 2. The `Model.prototype.getModel()` will not use any clone-like way.
++ const designatedVisualItemStyle = this.designatedVisualItemStyle = {};
++ const designatedVisualModel = new Model({itemStyle: designatedVisualItemStyle}, this, ecModel);
++
+ levels = option.levels = setDefault(levels, ecModel);
+ const levelModels = zrUtil.map(levels || [], function (levelDefine) {
- return new Model(levelDefine, this, ecModel);
++ return new Model(levelDefine, designatedVisualModel, ecModel);
+ }, this);
+
+ // Make sure always a new tree is created when setOption,
+ // in TreemapView, we check whether oldTree === newTree
+ // to choose mappings approach among old shapes and new shapes.
- const tree = Tree.createTree(root, this, null, beforeLink);
++ const tree = Tree.createTree(root, this, beforeLink);
+
+ function beforeLink(nodeData: List) {
+ nodeData.wrapMethod('getItemModel', function (model, idx) {
+ const node = tree.getNodeByDataIndex(idx);
+ const levelModel = levelModels[node.depth];
- levelModel && (model.parentModel = levelModel);
++ // If no levelModel, we also need `designatedVisualModel`.
++ model.parentModel = levelModel || designatedVisualModel;
+ return model;
+ });
+ }
+
+ return tree.data;
+ }
+
+ optionUpdated() {
+ this.resetViewRoot();
+ }
+
+ /**
+ * @override
+ * @param {number} dataIndex
+ * @param {boolean} [mutipleSeries=false]
+ */
+ formatTooltip(
+ dataIndex: number,
+ multipleSeries: boolean,
+ dataType: string
+ ) {
+ const data = this.getData();
+ const value = this.getRawValue(dataIndex) as TreemapSeriesDataValue;
+ const name = data.getName(dataIndex);
+
+ return createTooltipMarkup('nameValue', { name: name, value: value });
+ }
+
+ /**
+ * Add tree path to tooltip param
+ *
+ * @override
+ * @param {number} dataIndex
+ * @return {Object}
+ */
+ getDataParams(dataIndex: number) {
+ const params = super.getDataParams.apply(this, arguments as any) as TreemapSeriesCallbackDataParams;
+
+ const node = this.getData().tree.getNodeByDataIndex(dataIndex);
+ params.treePathInfo = wrapTreePathInfo(node, this);
+
+ return params;
+ }
+
+ /**
+ * @public
+ * @param {Object} layoutInfo {
+ * x: containerGroup x
+ * y: containerGroup y
+ * width: containerGroup width
+ * height: containerGroup height
+ * }
+ */
+ setLayoutInfo(layoutInfo: LayoutRect) {
+ /**
+ * @readOnly
+ * @type {Object}
+ */
+ this.layoutInfo = this.layoutInfo || {} as LayoutRect;
+ zrUtil.extend(this.layoutInfo, layoutInfo);
+ }
+
+ /**
+ * @param {string} id
+ * @return {number} index
+ */
+ mapIdToIndex(id: string): number {
+ // A feature is implemented:
+ // index is monotone increasing with the sequence of
+ // input id at the first time.
+ // This feature can make sure that each data item and its
+ // mapped color have the same index between data list and
+ // color list at the beginning, which is useful for user
+ // to adjust data-color mapping.
+
+ /**
+ * @private
+ * @type {Object}
+ */
+ let idIndexMap = this._idIndexMap;
+
+ if (!idIndexMap) {
+ idIndexMap = this._idIndexMap = zrUtil.createHashMap();
+ /**
+ * @private
+ * @type {number}
+ */
+ this._idIndexMapCount = 0;
+ }
+
+ let index = idIndexMap.get(id);
+ if (index == null) {
+ idIndexMap.set(id, index = this._idIndexMapCount++);
+ }
+
+ return index;
+ }
+
+ getViewRoot() {
+ return this._viewRoot;
+ }
+
+ resetViewRoot(viewRoot?: TreeNode) {
+ viewRoot
+ ? (this._viewRoot = viewRoot)
+ : (viewRoot = this._viewRoot);
+
+ const root = this.getRawData().tree.root;
+
+ if (!viewRoot
+ || (viewRoot !== root && !root.contains(viewRoot))
+ ) {
+ this._viewRoot = root;
+ }
+ }
+}
+
+/**
+ * @param {Object} dataNode
+ */
+function completeTreeValue(dataNode: TreemapSeriesNodeItemOption) {
+ // Postorder travel tree.
+ // If value of none-leaf node is not set,
+ // calculate it by suming up the value of all children.
+ let sum = 0;
+
+ zrUtil.each(dataNode.children, function (child) {
+
+ completeTreeValue(child);
+
+ let childValue = child.value;
+ zrUtil.isArray(childValue) && (childValue = childValue[0]);
+
+ sum += childValue;
+ });
+
+ let thisValue = dataNode.value;
+ if (zrUtil.isArray(thisValue)) {
+ thisValue = thisValue[0];
+ }
+
+ if (thisValue == null || isNaN(thisValue)) {
+ thisValue = sum;
+ }
+ // Value should not less than 0.
+ if (thisValue < 0) {
+ thisValue = 0;
+ }
+
+ zrUtil.isArray(dataNode.value)
+ ? (dataNode.value[0] = thisValue)
+ : (dataNode.value = thisValue);
+}
+
+/**
+ * set default to level configuration
+ */
+function setDefault(levels: TreemapSeriesLevelOption[], ecModel: GlobalModel) {
+ const globalColorList = normalizeToArray(ecModel.get('color')) as ColorString[];
+
+ if (!globalColorList) {
+ return;
+ }
+
+ levels = levels || [];
+ let hasColorDefine;
+ zrUtil.each(levels, function (levelDefine) {
+ const model = new Model(levelDefine);
+ const modelColor = model.get('color');
+
+ if (model.get(['itemStyle', 'color'])
+ || (modelColor && modelColor !== 'none')
+ ) {
+ hasColorDefine = true;
+ }
+ });
+
+ if (!hasColorDefine) {
+ const level0 = levels[0] || (levels[0] = {});
+ level0.color = globalColorList.slice();
+ }
+
+ return levels;
+}
+
+SeriesModel.registerClass(TreemapSeriesModel);
+
+export default TreemapSeriesModel;
diff --cc src/chart/treemap/treemapLayout.ts
index 7939b9e,0000000..9046d32
mode 100644,000000..100644
--- a/src/chart/treemap/treemapLayout.ts
+++ b/src/chart/treemap/treemapLayout.ts
@@@ -1,705 -1,0 +1,705 @@@
+/*
+* 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.
+*/
+
+/*
+* A third-party license is embeded for some of the code in this file:
+* The treemap layout implementation was originally copied from
+* "d3.js" with some modifications made for this project.
+* (See more details in the comment of the method "squarify" below.)
+* The use of the source code of this file is also subject to the terms
+* and consitions of the license of "d3.js" (BSD-3Clause, see
+* </licenses/LICENSE-d3>).
+*/
+
+import * as zrUtil from 'zrender/src/core/util';
+import BoundingRect, { RectLike } from 'zrender/src/core/BoundingRect';
+import {parsePercent, MAX_SAFE_INTEGER} from '../../util/number';
+import * as layout from '../../util/layout';
+import * as helper from '../helper/treeHelper';
+import TreemapSeriesModel, { TreemapSeriesNodeItemOption } from './TreemapSeries';
+import GlobalModel from '../../model/Global';
+import ExtensionAPI from '../../ExtensionAPI';
+import { TreeNode } from '../../data/Tree';
+import Model from '../../model/Model';
+import { TreemapRenderPayload, TreemapMovePayload, TreemapZoomToNodePayload } from './treemapAction';
+
+const mathMax = Math.max;
+const mathMin = Math.min;
+const retrieveValue = zrUtil.retrieve;
+const each = zrUtil.each;
+
+const PATH_BORDER_WIDTH = ['itemStyle', 'borderWidth'] as const;
+const PATH_GAP_WIDTH = ['itemStyle', 'gapWidth'] as const;
+const PATH_UPPER_LABEL_SHOW = ['upperLabel', 'show'] as const;
+const PATH_UPPER_LABEL_HEIGHT = ['upperLabel', 'height'] as const;
+
+export interface TreemapLayoutNode extends TreeNode {
+ parentNode: TreemapLayoutNode
+ children: TreemapLayoutNode[]
+ viewChildren: TreemapLayoutNode[]
+}
+
+export interface TreemapItemLayout extends RectLike {
+ area: number
+ isLeafRoot: boolean
+ dataExtent: [number, number]
+
+ borderWidth: number
+ upperHeight: number
+ upperLabelHeight: number
+
+ isInView: boolean
+ invisible: boolean
+
+ isAboveViewRoot: boolean
+};
+
+type NodeModel = Model<TreemapSeriesNodeItemOption>;
+
+type OrderBy = 'asc' | 'desc' | boolean;
+
+type LayoutRow = TreemapLayoutNode[] & {
+ area: number
+};
+/**
+ * @public
+ */
+export default {
+ seriesType: 'treemap',
+ reset: function (
+ seriesModel: TreemapSeriesModel,
+ ecModel: GlobalModel,
+ api: ExtensionAPI,
+ payload?: TreemapZoomToNodePayload | TreemapRenderPayload | TreemapMovePayload
+ ) {
+ // Layout result in each node:
+ // {x, y, width, height, area, borderWidth}
+ const ecWidth = api.getWidth();
+ const ecHeight = api.getHeight();
+ const seriesOption = seriesModel.option;
+
+ const layoutInfo = layout.getLayoutRect(
+ seriesModel.getBoxLayoutParams(),
+ {
+ width: api.getWidth(),
+ height: api.getHeight()
+ }
+ );
+
+ const size = seriesOption.size || []; // Compatible with ec2.
+ const containerWidth = parsePercent(
+ retrieveValue(layoutInfo.width, size[0]),
+ ecWidth
+ );
+ const containerHeight = parsePercent(
+ retrieveValue(layoutInfo.height, size[1]),
+ ecHeight
+ );
+
+ // Fetch payload info.
+ const payloadType = payload && payload.type;
+ const types = ['treemapZoomToNode', 'treemapRootToNode'];
+ const targetInfo = helper
+ .retrieveTargetInfo(payload, types, seriesModel);
+ const rootRect = (payloadType === 'treemapRender' || payloadType === 'treemapMove')
+ ? payload.rootRect : null;
+ const viewRoot = seriesModel.getViewRoot();
+ const viewAbovePath = helper.getPathToRoot(viewRoot) as TreemapLayoutNode[];
+
+ if (payloadType !== 'treemapMove') {
+ const rootSize = payloadType === 'treemapZoomToNode'
+ ? estimateRootSize(
+ seriesModel, targetInfo, viewRoot, containerWidth, containerHeight
+ )
+ : rootRect
+ ? [rootRect.width, rootRect.height]
+ : [containerWidth, containerHeight];
+
+ let sort = seriesOption.sort;
+ if (sort && sort !== 'asc' && sort !== 'desc') {
+ // Default to be desc order.
+ sort = 'desc';
+ }
+ const options = {
+ squareRatio: seriesOption.squareRatio,
+ sort: sort,
+ leafDepth: seriesOption.leafDepth
+ };
+
+ // layout should be cleared because using updateView but not update.
+ viewRoot.hostTree.clearLayouts();
+
+ // TODO
+ // optimize: if out of view clip, do not layout.
+ // But take care that if do not render node out of view clip,
+ // how to calculate start po
+
+ let viewRootLayout = {
+ x: 0,
+ y: 0,
+ width: rootSize[0],
+ height: rootSize[1],
+ area: rootSize[0] * rootSize[1]
+ };
+ viewRoot.setLayout(viewRootLayout);
+
+ squarify(viewRoot, options, false, 0);
+ // Supplement layout.
+ viewRootLayout = viewRoot.getLayout();
+ each(viewAbovePath, function (node, index) {
+ const childValue = (viewAbovePath[index + 1] || viewRoot).getValue();
+ node.setLayout(zrUtil.extend(
+ {
+ dataExtent: [childValue, childValue],
+ borderWidth: 0,
+ upperHeight: 0
+ },
+ viewRootLayout
+ ));
+ });
+ }
+
+ const treeRoot = seriesModel.getData().tree.root;
+
+ treeRoot.setLayout(
+ calculateRootPosition(layoutInfo, rootRect, targetInfo),
+ true
+ );
+
+ seriesModel.setLayoutInfo(layoutInfo);
+
+ // FIXME
+ // 现在没有clip功能,暂时取ec高宽。
+ prunning(
+ treeRoot,
+ // Transform to base element coordinate system.
+ new BoundingRect(-layoutInfo.x, -layoutInfo.y, ecWidth, ecHeight),
+ viewAbovePath,
+ viewRoot,
+ 0
+ );
+ }
+};
+
+/**
+ * Layout treemap with squarify algorithm.
+ * The original presentation of this algorithm
+ * was made by Mark Bruls, Kees Huizing, and Jarke J. van Wijk
+ * <https://graphics.ethz.ch/teaching/scivis_common/Literature/squarifiedTreeMaps.pdf>.
+ * The implementation of this algorithm was originally copied from "d3.js"
+ * <https://github.com/d3/d3/blob/9cc9a875e636a1dcf36cc1e07bdf77e1ad6e2c74/src/layout/treemap.js>
+ * with some modifications made for this program.
+ * See the license statement at the head of this file.
+ *
+ * @protected
+ * @param {module:echarts/data/Tree~TreeNode} node
+ * @param {Object} options
+ * @param {string} options.sort 'asc' or 'desc'
+ * @param {number} options.squareRatio
+ * @param {boolean} hideChildren
+ * @param {number} depth
+ */
+function squarify(
+ node: TreemapLayoutNode,
+ options: {
+ sort?: OrderBy
+ squareRatio?: number
+ leafDepth?: number
+ },
+ hideChildren: boolean,
+ depth: number
+) {
+ let width;
+ let height;
+
+ if (node.isRemoved()) {
+ return;
+ }
+
+ const thisLayout = node.getLayout();
+ width = thisLayout.width;
+ height = thisLayout.height;
+
+ // Considering border and gap
+ const nodeModel = node.getModel<TreemapSeriesNodeItemOption>();
+ const borderWidth = nodeModel.get(PATH_BORDER_WIDTH);
+ const halfGapWidth = nodeModel.get(PATH_GAP_WIDTH) / 2;
+ const upperLabelHeight = getUpperLabelHeight(nodeModel);
+ const upperHeight = Math.max(borderWidth, upperLabelHeight);
+ const layoutOffset = borderWidth - halfGapWidth;
+ const layoutOffsetUpper = upperHeight - halfGapWidth;
+
+ node.setLayout({
+ borderWidth: borderWidth,
+ upperHeight: upperHeight,
+ upperLabelHeight: upperLabelHeight
+ }, true);
+
+ width = mathMax(width - 2 * layoutOffset, 0);
+ height = mathMax(height - layoutOffset - layoutOffsetUpper, 0);
+
+ const totalArea = width * height;
+ const viewChildren = initChildren(
+ node, nodeModel, totalArea, options, hideChildren, depth
+ );
+
+ if (!viewChildren.length) {
+ return;
+ }
+
+ const rect = {x: layoutOffset, y: layoutOffsetUpper, width: width, height: height};
+ let rowFixedLength = mathMin(width, height);
+ let best = Infinity; // the best row score so far
+ const row = [] as LayoutRow;
+ row.area = 0;
+
+ for (let i = 0, len = viewChildren.length; i < len;) {
+ const child = viewChildren[i];
+
+ row.push(child);
+ row.area += child.getLayout().area;
+ const score = worst(row, rowFixedLength, options.squareRatio);
+
+ // continue with this orientation
+ if (score <= best) {
+ i++;
+ best = score;
+ }
+ // abort, and try a different orientation
+ else {
+ row.area -= row.pop().getLayout().area;
+ position(row, rowFixedLength, rect, halfGapWidth, false);
+ rowFixedLength = mathMin(rect.width, rect.height);
+ row.length = row.area = 0;
+ best = Infinity;
+ }
+ }
+
+ if (row.length) {
+ position(row, rowFixedLength, rect, halfGapWidth, true);
+ }
+
+ if (!hideChildren) {
+ const childrenVisibleMin = nodeModel.get('childrenVisibleMin');
+ if (childrenVisibleMin != null && totalArea < childrenVisibleMin) {
+ hideChildren = true;
+ }
+ }
+
+ for (let i = 0, len = viewChildren.length; i < len; i++) {
+ squarify(viewChildren[i], options, hideChildren, depth + 1);
+ }
+}
+
+/**
+ * Set area to each child, and calculate data extent for visual coding.
+ */
+function initChildren(
+ node: TreemapLayoutNode,
+ nodeModel: NodeModel,
+ totalArea: number,
+ options: {
+ sort?: OrderBy
+ leafDepth?: number
+ },
+ hideChildren: boolean,
+ depth: number
+) {
+ let viewChildren = node.children || [];
+ let orderBy = options.sort;
+ orderBy !== 'asc' && orderBy !== 'desc' && (orderBy = null);
+
+ const overLeafDepth = options.leafDepth != null && options.leafDepth <= depth;
+
+ // leafDepth has higher priority.
+ if (hideChildren && !overLeafDepth) {
+ return (node.viewChildren = []);
+ }
+
+ // Sort children, order by desc.
+ viewChildren = zrUtil.filter(viewChildren, function (child) {
+ return !child.isRemoved();
+ });
+
+ sort(viewChildren, orderBy);
+
+ const info = statistic(nodeModel, viewChildren, orderBy);
+
+ if (info.sum === 0) {
+ return (node.viewChildren = []);
+ }
+
+ info.sum = filterByThreshold(nodeModel, totalArea, info.sum, orderBy, viewChildren);
+
+ if (info.sum === 0) {
+ return (node.viewChildren = []);
+ }
+
+ // Set area to each child.
+ for (let i = 0, len = viewChildren.length; i < len; i++) {
+ const area = viewChildren[i].getValue() as number / info.sum * totalArea;
+ // Do not use setLayout({...}, true), because it is needed to clear last layout.
+ viewChildren[i].setLayout({
+ area: area
+ });
+ }
+
+ if (overLeafDepth) {
+ viewChildren.length && node.setLayout({
+ isLeafRoot: true
+ }, true);
+ viewChildren.length = 0;
+ }
+
+ node.viewChildren = viewChildren;
+ node.setLayout({
+ dataExtent: info.dataExtent
+ }, true);
+
+ return viewChildren;
+}
+
+/**
+ * Consider 'visibleMin'. Modify viewChildren and get new sum.
+ */
+function filterByThreshold(
+ nodeModel: NodeModel,
+ totalArea: number,
+ sum: number,
+ orderBy: OrderBy,
+ orderedChildren: TreemapLayoutNode[]
+) {
+
+ // visibleMin is not supported yet when no option.sort.
+ if (!orderBy) {
+ return sum;
+ }
+
+ const visibleMin = nodeModel.get('visibleMin');
+ const len = orderedChildren.length;
+ let deletePoint = len;
+
+ // Always travel from little value to big value.
+ for (let i = len - 1; i >= 0; i--) {
+ const value = orderedChildren[
+ orderBy === 'asc' ? len - i - 1 : i
+ ].getValue() as number;
+
+ if (value / sum * totalArea < visibleMin) {
+ deletePoint = i;
+ sum -= value;
+ }
+ }
+
+ orderBy === 'asc'
+ ? orderedChildren.splice(0, len - deletePoint)
+ : orderedChildren.splice(deletePoint, len - deletePoint);
+
+ return sum;
+}
+
+/**
+ * Sort
+ */
+function sort(
+ viewChildren: TreemapLayoutNode[],
+ orderBy: OrderBy
+) {
+ if (orderBy) {
+ viewChildren.sort(function (a, b) {
+ const diff = orderBy === 'asc'
+ ? a.getValue() as number - (b.getValue() as number)
+ : b.getValue() as number - (a.getValue() as number);
+ return diff === 0
+ ? (orderBy === 'asc'
+ ? a.dataIndex - b.dataIndex : b.dataIndex - a.dataIndex
+ )
+ : diff;
+ });
+ }
+ return viewChildren;
+}
+
+/**
+ * Statistic
+ */
+function statistic(
+ nodeModel: NodeModel,
+ children: TreemapLayoutNode[],
+ orderBy: OrderBy
+) {
+ // Calculate sum.
+ let sum = 0;
+ for (let i = 0, len = children.length; i < len; i++) {
+ sum += children[i].getValue() as number;
+ }
+
+ // Statistic data extent for latter visual coding.
+ // Notice: data extent should be calculate based on raw children
+ // but not filtered view children, otherwise visual mapping will not
+ // be stable when zoom (where children is filtered by visibleMin).
+
+ const dimension = nodeModel.get('visualDimension');
+ let dataExtent: number[];
+
+ // The same as area dimension.
+ if (!children || !children.length) {
+ dataExtent = [NaN, NaN];
+ }
+ else if (dimension === 'value' && orderBy) {
+ dataExtent = [
+ children[children.length - 1].getValue() as number,
+ children[0].getValue() as number
+ ];
+ orderBy === 'asc' && dataExtent.reverse();
+ }
+ // Other dimension.
+ else {
+ dataExtent = [Infinity, -Infinity];
+ each(children, function (child) {
+ const value = child.getValue(dimension) as number;
+ value < dataExtent[0] && (dataExtent[0] = value);
+ value > dataExtent[1] && (dataExtent[1] = value);
+ });
+ }
+
+ return {sum: sum, dataExtent: dataExtent};
+}
+
+/**
+ * Computes the score for the specified row,
+ * as the worst aspect ratio.
+ */
+function worst(row: LayoutRow, rowFixedLength: number, ratio: number) {
+ let areaMax = 0;
+ let areaMin = Infinity;
+
+ for (let i = 0, area, len = row.length; i < len; i++) {
+ area = row[i].getLayout().area;
+ if (area) {
+ area < areaMin && (areaMin = area);
+ area > areaMax && (areaMax = area);
+ }
+ }
+
+ const squareArea = row.area * row.area;
+ const f = rowFixedLength * rowFixedLength * ratio;
+
+ return squareArea
+ ? mathMax(
+ (f * areaMax) / squareArea,
+ squareArea / (f * areaMin)
+ )
+ : Infinity;
+}
+
+/**
+ * Positions the specified row of nodes. Modifies `rect`.
+ */
+function position(
+ row: LayoutRow,
+ rowFixedLength: number,
+ rect: RectLike,
+ halfGapWidth: number,
+ flush?: boolean
+) {
+ // When rowFixedLength === rect.width,
+ // it is horizontal subdivision,
+ // rowFixedLength is the width of the subdivision,
+ // rowOtherLength is the height of the subdivision,
+ // and nodes will be positioned from left to right.
+
+ // wh[idx0WhenH] means: when horizontal,
+ // wh[idx0WhenH] => wh[0] => 'width'.
+ // xy[idx1WhenH] => xy[1] => 'y'.
+ const idx0WhenH = rowFixedLength === rect.width ? 0 : 1;
+ const idx1WhenH = 1 - idx0WhenH;
+ const xy = ['x', 'y'] as const;
+ const wh = ['width', 'height'] as const;
+
+ let last = rect[xy[idx0WhenH]];
+ let rowOtherLength = rowFixedLength
+ ? row.area / rowFixedLength : 0;
+
+ if (flush || rowOtherLength > rect[wh[idx1WhenH]]) {
+ rowOtherLength = rect[wh[idx1WhenH]]; // over+underflow
+ }
+ for (let i = 0, rowLen = row.length; i < rowLen; i++) {
+ const node = row[i];
+ const nodeLayout = {} as TreemapItemLayout;
+ const step = rowOtherLength
+ ? node.getLayout().area / rowOtherLength : 0;
+
+ const wh1 = nodeLayout[wh[idx1WhenH]] = mathMax(rowOtherLength - 2 * halfGapWidth, 0);
+
+ // We use Math.max/min to avoid negative width/height when considering gap width.
+ const remain = rect[xy[idx0WhenH]] + rect[wh[idx0WhenH]] - last;
+ const modWH = (i === rowLen - 1 || remain < step) ? remain : step;
+ const wh0 = nodeLayout[wh[idx0WhenH]] = mathMax(modWH - 2 * halfGapWidth, 0);
+
+ nodeLayout[xy[idx1WhenH]] = rect[xy[idx1WhenH]] + mathMin(halfGapWidth, wh1 / 2);
+ nodeLayout[xy[idx0WhenH]] = last + mathMin(halfGapWidth, wh0 / 2);
+
+ last += modWH;
+ node.setLayout(nodeLayout, true);
+ }
+
+ rect[xy[idx1WhenH]] += rowOtherLength;
+ rect[wh[idx1WhenH]] -= rowOtherLength;
+}
+
- // Return [containerWidth, containerHeight] as defualt.
++// Return [containerWidth, containerHeight] as default.
+function estimateRootSize(
+ seriesModel: TreemapSeriesModel,
+ targetInfo: { node: TreemapLayoutNode },
+ viewRoot: TreemapLayoutNode,
+ containerWidth: number,
+ containerHeight: number
+) {
+ // If targetInfo.node exists, we zoom to the node,
+ // so estimate whold width and heigth by target node.
+ let currNode = (targetInfo || {}).node;
+ const defaultSize = [containerWidth, containerHeight];
+
+ if (!currNode || currNode === viewRoot) {
+ return defaultSize;
+ }
+
+ let parent;
+ const viewArea = containerWidth * containerHeight;
+ let area = viewArea * seriesModel.option.zoomToNodeRatio;
+
+ while (parent = currNode.parentNode) { // jshint ignore:line
+ let sum = 0;
+ const siblings = parent.children;
+
+ for (let i = 0, len = siblings.length; i < len; i++) {
+ sum += siblings[i].getValue() as number;
+ }
+ const currNodeValue = currNode.getValue() as number;
+ if (currNodeValue === 0) {
+ return defaultSize;
+ }
+ area *= sum / currNodeValue;
+
+ // Considering border, suppose aspect ratio is 1.
+ const parentModel = parent.getModel<TreemapSeriesNodeItemOption>();
+ const borderWidth = parentModel.get(PATH_BORDER_WIDTH);
+ const upperHeight = Math.max(borderWidth, getUpperLabelHeight(parentModel));
+ area += 4 * borderWidth * borderWidth
+ + (3 * borderWidth + upperHeight) * Math.pow(area, 0.5);
+
+ area > MAX_SAFE_INTEGER && (area = MAX_SAFE_INTEGER);
+
+ currNode = parent;
+ }
+
+ area < viewArea && (area = viewArea);
+ const scale = Math.pow(area / viewArea, 0.5);
+
+ return [containerWidth * scale, containerHeight * scale];
+}
+
+// Root postion base on coord of containerGroup
+function calculateRootPosition(
+ layoutInfo: layout.LayoutRect,
+ rootRect: RectLike,
+ targetInfo: { node: TreemapLayoutNode }
+) {
+ if (rootRect) {
+ return {x: rootRect.x, y: rootRect.y};
+ }
+
+ const defaultPosition = {x: 0, y: 0};
+ if (!targetInfo) {
+ return defaultPosition;
+ }
+
+ // If targetInfo is fetched by 'retrieveTargetInfo',
+ // old tree and new tree are the same tree,
+ // so the node still exists and we can visit it.
+
+ const targetNode = targetInfo.node;
+ const layout = targetNode.getLayout();
+
+ if (!layout) {
+ return defaultPosition;
+ }
+
+ // Transform coord from local to container.
+ const targetCenter = [layout.width / 2, layout.height / 2];
+ let node = targetNode;
+ while (node) {
+ const nodeLayout = node.getLayout();
+ targetCenter[0] += nodeLayout.x;
+ targetCenter[1] += nodeLayout.y;
+ node = node.parentNode;
+ }
+
+ return {
+ x: layoutInfo.width / 2 - targetCenter[0],
+ y: layoutInfo.height / 2 - targetCenter[1]
+ };
+}
+
+// Mark nodes visible for prunning when visual coding and rendering.
+// Prunning depends on layout and root position, so we have to do it after layout.
+function prunning(
+ node: TreemapLayoutNode,
+ clipRect: BoundingRect,
+ viewAbovePath: TreemapLayoutNode[],
+ viewRoot: TreemapLayoutNode,
+ depth: number
+) {
+ const nodeLayout = node.getLayout();
+ const nodeInViewAbovePath = viewAbovePath[depth];
+ const isAboveViewRoot = nodeInViewAbovePath && nodeInViewAbovePath === node;
+
+ if (
+ (nodeInViewAbovePath && !isAboveViewRoot)
+ || (depth === viewAbovePath.length && node !== viewRoot)
+ ) {
+ return;
+ }
+
+ node.setLayout({
+ // isInView means: viewRoot sub tree + viewAbovePath
+ isInView: true,
+ // invisible only means: outside view clip so that the node can not
+ // see but still layout for animation preparation but not render.
+ invisible: !isAboveViewRoot && !clipRect.intersect(nodeLayout),
+ isAboveViewRoot
+ }, true);
+
+ // Transform to child coordinate.
+ const childClipRect = new BoundingRect(
+ clipRect.x - nodeLayout.x,
+ clipRect.y - nodeLayout.y,
+ clipRect.width,
+ clipRect.height
+ );
+
+ each(node.viewChildren || [], function (child) {
+ prunning(child, childClipRect, viewAbovePath, viewRoot, depth + 1);
+ });
+}
+
+function getUpperLabelHeight(model: NodeModel): number {
+ return model.get(PATH_UPPER_LABEL_SHOW) ? model.get(PATH_UPPER_LABEL_HEIGHT) : 0;
- }
++}
diff --cc src/chart/treemap/treemapVisual.ts
index 0ff4b8e,0000000..2796338
mode 100644,000000..100644
--- a/src/chart/treemap/treemapVisual.ts
+++ b/src/chart/treemap/treemapVisual.ts
@@@ -1,287 -1,0 +1,270 @@@
+/*
+* 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 VisualMapping, { VisualMappingOption } from '../../visual/VisualMapping';
+import { map, each, extend, isArray } from 'zrender/src/core/util';
+import TreemapSeriesModel, { TreemapSeriesNodeItemOption, TreemapSeriesOption } from './TreemapSeries';
+import { TreemapLayoutNode, TreemapItemLayout } from './treemapLayout';
+import Model from '../../model/Model';
+import { ColorString, ZRColor } from '../../util/types';
+import { modifyHSL, modifyAlpha } from 'zrender/src/tool/color';
+import { makeInner } from '../../util/model';
+
+type NodeModel = Model<TreemapSeriesNodeItemOption>;
+type NodeItemStyleModel = Model<TreemapSeriesNodeItemOption['itemStyle']>;
+
+const ITEM_STYLE_NORMAL = 'itemStyle';
+
+const inner = makeInner<{
+ drColorMappingBy: TreemapSeriesNodeItemOption['colorMappingBy']
+}, VisualMapping>();
+
+interface TreemapVisual {
+ color?: ZRColor
+ colorAlpha?: number
+ colorSaturation?: number
+}
+
+type TreemapLevelItemStyleOption = TreemapSeriesOption['levels'][number]['itemStyle'];
+
+export default {
+ seriesType: 'treemap',
+ reset(seriesModel: TreemapSeriesModel) {
+ const tree = seriesModel.getData().tree;
+ const root = tree.root;
- const seriesItemStyleModel = seriesModel.getModel(ITEM_STYLE_NORMAL);
+
+ if (root.isRemoved()) {
+ return;
+ }
+
- const levelItemStyles = map(tree.levelModels, function (levelModel) {
- return levelModel ? levelModel.get(ITEM_STYLE_NORMAL) : null;
- });
-
+ travelTree(
+ root, // Visual should calculate from tree root but not view root.
+ {},
- levelItemStyles,
- seriesItemStyleModel,
+ seriesModel.getViewRoot().getAncestors(),
+ seriesModel
+ );
+ }
+};
+
+function travelTree(
+ node: TreemapLayoutNode,
+ designatedVisual: TreemapVisual,
- levelItemStyles: TreemapLevelItemStyleOption[],
- seriesItemStyleModel: Model<TreemapSeriesOption['itemStyle']>,
+ viewRootAncestors: TreemapLayoutNode[],
+ seriesModel: TreemapSeriesModel
+) {
+ const nodeModel = node.getModel<TreemapSeriesNodeItemOption>();
+ const nodeLayout = node.getLayout();
+ const data = node.hostTree.data;
+
+ // Optimize
+ if (!nodeLayout || nodeLayout.invisible || !nodeLayout.isInView) {
+ return;
+ }
-
+ const nodeItemStyleModel = nodeModel.getModel(ITEM_STYLE_NORMAL);
- const levelItemStyle = levelItemStyles[node.depth];
- const visuals = buildVisuals(
- nodeItemStyleModel, designatedVisual, levelItemStyle, seriesItemStyleModel
- );
++ const visuals = buildVisuals(nodeItemStyleModel, designatedVisual, seriesModel);
+
+ const existsStyle = data.ensureUniqueItemVisual(node.dataIndex, 'style');
+ // calculate border color
+ let borderColor = nodeItemStyleModel.get('borderColor');
+ const borderColorSaturation = nodeItemStyleModel.get('borderColorSaturation');
+ let thisNodeColor;
+ if (borderColorSaturation != null) {
+ // For performance, do not always execute 'calculateColor'.
+ thisNodeColor = calculateColor(visuals);
+ borderColor = calculateBorderColor(borderColorSaturation, thisNodeColor);
+ }
+ existsStyle.stroke = borderColor;
+
+ const viewChildren = node.viewChildren;
+ if (!viewChildren || !viewChildren.length) {
+ thisNodeColor = calculateColor(visuals);
+ // Apply visual to this node.
+ existsStyle.fill = thisNodeColor;
+ }
+ else {
+ const mapping = buildVisualMapping(
+ node, nodeModel, nodeLayout, nodeItemStyleModel, visuals, viewChildren
+ );
+
+ // Designate visual to children.
+ each(viewChildren, function (child, index) {
+ // If higher than viewRoot, only ancestors of viewRoot is needed to visit.
+ if (child.depth >= viewRootAncestors.length
+ || child === viewRootAncestors[child.depth]
+ ) {
+ const childVisual = mapVisual(
+ nodeModel, visuals, child, index, mapping, seriesModel
+ );
- travelTree(
- child, childVisual, levelItemStyles, seriesItemStyleModel,
- viewRootAncestors, seriesModel
- );
++ travelTree(child, childVisual, viewRootAncestors, seriesModel);
+ }
+ });
+ }
+}
+
+function buildVisuals(
+ nodeItemStyleModel: Model<TreemapSeriesNodeItemOption['itemStyle']>,
+ designatedVisual: TreemapVisual,
- levelItemStyle: TreemapLevelItemStyleOption,
- seriesItemStyleModel: Model<TreemapSeriesOption['itemStyle']>
++ seriesModel: TreemapSeriesModel
+) {
+ const visuals = extend({}, designatedVisual);
++ const designatedVisualItemStyle = seriesModel.designatedVisualItemStyle;
+
+ each(['color', 'colorAlpha', 'colorSaturation'] as const, function (visualName) {
+ // Priority: thisNode > thisLevel > parentNodeDesignated > seriesModel
- let val = nodeItemStyleModel.get(visualName, true); // Ignore parent
- val == null && levelItemStyle && (val = levelItemStyle[visualName]);
- val == null && (val = designatedVisual[visualName]);
- val == null && (val = seriesItemStyleModel.get(visualName));
++ (designatedVisualItemStyle as any)[visualName] = designatedVisual[visualName];
++ const val = nodeItemStyleModel.get(visualName);
++ designatedVisualItemStyle[visualName] = null;
+
+ val != null && ((visuals as any)[visualName] = val);
+ });
+
+ return visuals;
+}
+
+function calculateColor(visuals: TreemapVisual) {
+ let color = getValueVisualDefine(visuals, 'color') as ColorString;
+
+ if (color) {
+ const colorAlpha = getValueVisualDefine(visuals, 'colorAlpha') as number;
+ const colorSaturation = getValueVisualDefine(visuals, 'colorSaturation') as number;
+ if (colorSaturation) {
+ color = modifyHSL(color, null, null, colorSaturation);
+ }
+ if (colorAlpha) {
+ color = modifyAlpha(color, colorAlpha);
+ }
+
+ return color;
+ }
+}
+
+function calculateBorderColor(
+ borderColorSaturation: number,
+ thisNodeColor: ColorString
+) {
+ return thisNodeColor != null
+ // Can only be string
+ ? modifyHSL(thisNodeColor, null, null, borderColorSaturation)
+ : null;
+}
+
+function getValueVisualDefine(visuals: TreemapVisual, name: keyof TreemapVisual) {
+ const value = visuals[name];
+ if (value != null && value !== 'none') {
+ return value;
+ }
+}
+
+function buildVisualMapping(
+ node: TreemapLayoutNode,
+ nodeModel: NodeModel,
+ nodeLayout: TreemapItemLayout,
+ nodeItemStyleModel: NodeItemStyleModel,
+ visuals: TreemapVisual,
+ viewChildren: TreemapLayoutNode[]
+) {
+ if (!viewChildren || !viewChildren.length) {
+ return;
+ }
+
+ const rangeVisual = getRangeVisual(nodeModel, 'color')
+ || (
+ visuals.color != null
+ && visuals.color !== 'none'
+ && (
+ getRangeVisual(nodeModel, 'colorAlpha')
+ || getRangeVisual(nodeModel, 'colorSaturation')
+ )
+ );
+
+ if (!rangeVisual) {
+ return;
+ }
+
+ const visualMin = nodeModel.get('visualMin');
+ const visualMax = nodeModel.get('visualMax');
+ const dataExtent = nodeLayout.dataExtent.slice() as [number, number];
+ visualMin != null && visualMin < dataExtent[0] && (dataExtent[0] = visualMin);
+ visualMax != null && visualMax > dataExtent[1] && (dataExtent[1] = visualMax);
+
+ const colorMappingBy = nodeModel.get('colorMappingBy');
+ const opt: VisualMappingOption = {
+ type: rangeVisual.name,
+ dataExtent: dataExtent,
+ visual: rangeVisual.range
+ };
+ if (opt.type === 'color'
+ && (colorMappingBy === 'index' || colorMappingBy === 'id')
+ ) {
+ opt.mappingMethod = 'category';
+ opt.loop = true;
+ // categories is ordinal, so do not set opt.categories.
+ }
+ else {
+ opt.mappingMethod = 'linear';
+ }
+
+ const mapping = new VisualMapping(opt);
+ inner(mapping).drColorMappingBy = colorMappingBy;
+
+ return mapping;
+}
+
+// Notice: If we dont have the attribute 'colorRange', but only use
+// attribute 'color' to represent both concepts of 'colorRange' and 'color',
+// (It means 'colorRange' when 'color' is Array, means 'color' when not array),
+// this problem will be encountered:
+// If a level-1 node dont have children, and its siblings has children,
+// and colorRange is set on level-1, then the node can not be colored.
+// So we separate 'colorRange' and 'color' to different attributes.
+function getRangeVisual(nodeModel: NodeModel, name: keyof TreemapVisual) {
+ // 'colorRange', 'colorARange', 'colorSRange'.
+ // If not exsits on this node, fetch from levels and series.
+ const range = nodeModel.get(name);
+ return (isArray(range) && range.length) ? {
+ name: name,
+ range: range
+ } : null;
+}
+
+function mapVisual(
+ nodeModel: NodeModel,
+ visuals: TreemapVisual,
+ child: TreemapLayoutNode,
+ index: number,
+ mapping: VisualMapping,
+ seriesModel: TreemapSeriesModel
+) {
+ const childVisuals = extend({}, visuals);
+
+ if (mapping) {
+ // Only support color, colorAlpha, colorSaturation.
+ const mappingType = mapping.type as keyof TreemapVisual;
+ const colorMappingBy = mappingType === 'color' && inner(mapping).drColorMappingBy;
+ const value = colorMappingBy === 'index'
+ ? index
+ : colorMappingBy === 'id'
+ ? seriesModel.mapIdToIndex(child.getId())
+ : child.getValue(nodeModel.get('visualDimension'));
+
+ (childVisuals as any)[mappingType] = mapping.mapValueToVisual(value);
+ }
+
+ return childVisuals;
+}
diff --cc src/component/axisPointer/axisTrigger.ts
index 0bc672f,0000000..27aa8c5
mode 100644,000000..100644
--- a/src/component/axisPointer/axisTrigger.ts
+++ b/src/component/axisPointer/axisTrigger.ts
@@@ -1,529 -1,0 +1,529 @@@
+/*
+* 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 {makeInner, ModelFinderObject} from '../../util/model';
+import * as modelHelper from './modelHelper';
+import findPointFromSeries from './findPointFromSeries';
+import GlobalModel from '../../model/Global';
+import ExtensionAPI from '../../ExtensionAPI';
+import { Dictionary, Payload, CommonAxisPointerOption, HighlightPayload, DownplayPayload } from '../../util/types';
+import AxisPointerModel, { AxisPointerOption } from './AxisPointerModel';
+import { each, curry, bind, extend, Curry1 } from 'zrender/src/core/util';
+import { ZRenderType } from 'zrender/src/zrender';
+
+const inner = makeInner<{
+ axisPointerLastHighlights: Dictionary<BatchItem>
+}, ZRenderType>();
+
+type AxisValue = CommonAxisPointerOption['value'];
+
+interface DataIndex {
+ seriesIndex: number
+ dataIndex: number
+ dataIndexInside: number
+}
+
+type BatchItem = DataIndex;
+
+export interface DataByAxis {
+ // TODO: TYPE Value type
+ value: string | number
+ axisIndex: number
+ axisDim: string
+ axisType: string
+ axisId: string
+
+ seriesDataIndices: DataIndex[]
+
+ valueLabelOpt: {
+ precision: AxisPointerOption['label']['precision']
+ formatter: AxisPointerOption['label']['formatter']
+ }
+}
+export interface DataByCoordSys {
+ coordSysId: string
+ coordSysIndex: number
+ coordSysType: string
+ coordSysMainType: string
+ dataByAxis: DataByAxis[]
+}
+interface DataByCoordSysCollection {
+ list: DataByCoordSys[]
+ map: Dictionary<DataByCoordSys>
+}
+
+type CollectedCoordInfo = ReturnType<typeof modelHelper['collect']>;
+type CollectedAxisInfo = CollectedCoordInfo['axesInfo'][string];
+
+interface AxisTriggerPayload extends Payload {
+ currTrigger?: 'click' | 'mousemove' | 'leave'
+ /**
+ * x and y, which are mandatory, specify a point to trigger axisPointer and tooltip.
+ */
+ x?: number
+ /**
+ * x and y, which are mandatory, specify a point to trigger axisPointer and tooltip.
+ */
+ y?: number
+ /**
+ * finder, optional, restrict target axes.
+ */
+ seriesIndex?: number
+ dataIndex: number
+
+ axesInfo?: {
+ // 'x'|'y'|'angle'
+ axisDim?: string
+ axisIndex?: number
+ value?: AxisValue
+ }[]
+
+ dispatchAction: ExtensionAPI['dispatchAction']
+}
+
+type ShowValueMap = Dictionary<{
+ value: AxisValue
+ payloadBatch: BatchItem[]
+}>;
+
+/**
+ * Basic logic: check all axis, if they do not demand show/highlight,
+ * then hide/downplay them.
+ *
+ * @return content of event obj for echarts.connect.
+ */
+export default function (
+ payload: AxisTriggerPayload,
+ ecModel: GlobalModel,
+ api: ExtensionAPI
+) {
+ const currTrigger = payload.currTrigger;
+ let point = [payload.x, payload.y];
+ const finder = payload;
+ const dispatchAction = payload.dispatchAction || bind(api.dispatchAction, api);
+ const coordSysAxesInfo = (ecModel.getComponent('axisPointer') as AxisPointerModel)
+ .coordSysAxesInfo as CollectedCoordInfo;
+
+ // Pending
+ // See #6121. But we are not able to reproduce it yet.
+ if (!coordSysAxesInfo) {
+ return;
+ }
+
+ if (illegalPoint(point)) {
+ // Used in the default behavior of `connection`: use the sample seriesIndex
+ // and dataIndex. And also used in the tooltipView trigger.
+ point = findPointFromSeries({
+ seriesIndex: finder.seriesIndex,
+ // Do not use dataIndexInside from other ec instance.
+ // FIXME: auto detect it?
+ dataIndex: finder.dataIndex
+ }, ecModel).point;
+ }
+ const isIllegalPoint = illegalPoint(point);
+
+ // Axis and value can be specified when calling dispatchAction({type: 'updateAxisPointer'}).
+ // Notice: In this case, it is difficult to get the `point` (which is necessary to show
+ // tooltip, so if point is not given, we just use the point found by sample seriesIndex
+ // and dataIndex.
+ const inputAxesInfo = finder.axesInfo;
+
+ const axesInfo = coordSysAxesInfo.axesInfo;
+ const shouldHide = currTrigger === 'leave' || illegalPoint(point);
+ const outputPayload = {} as AxisTriggerPayload;
+
+ const showValueMap: ShowValueMap = {};
+ const dataByCoordSys: DataByCoordSysCollection = {
+ list: [],
+ map: {}
+ };
+ const updaters = {
+ showPointer: curry(showPointer, showValueMap),
+ showTooltip: curry(showTooltip, dataByCoordSys)
+ };
+
+ // Process for triggered axes.
+ each(coordSysAxesInfo.coordSysMap, function (coordSys, coordSysKey) {
+ // If a point given, it must be contained by the coordinate system.
+ const coordSysContainsPoint = isIllegalPoint || coordSys.containPoint(point);
+
+ each(coordSysAxesInfo.coordSysAxesInfo[coordSysKey], function (axisInfo, key) {
+ const axis = axisInfo.axis;
+ const inputAxisInfo = findInputAxisInfo(inputAxesInfo, axisInfo);
+ // If no inputAxesInfo, no axis is restricted.
+ if (!shouldHide && coordSysContainsPoint && (!inputAxesInfo || inputAxisInfo)) {
+ let val = inputAxisInfo && inputAxisInfo.value;
+ if (val == null && !isIllegalPoint) {
+ val = axis.pointToData(point);
+ }
+ val != null && processOnAxis(axisInfo, val, updaters, false, outputPayload);
+ }
+ });
+ });
+
+ // Process for linked axes.
+ const linkTriggers: Dictionary<AxisValue> = {};
+ each(axesInfo, function (tarAxisInfo, tarKey) {
+ const linkGroup = tarAxisInfo.linkGroup;
+
+ // If axis has been triggered in the previous stage, it should not be triggered by link.
+ if (linkGroup && !showValueMap[tarKey]) {
+ each(linkGroup.axesInfo, function (srcAxisInfo, srcKey) {
+ const srcValItem = showValueMap[srcKey];
+ // If srcValItem exist, source axis is triggered, so link to target axis.
+ if (srcAxisInfo !== tarAxisInfo && srcValItem) {
+ let val = srcValItem.value;
+ linkGroup.mapper && (val = tarAxisInfo.axis.scale.parse(linkGroup.mapper(
+ val, makeMapperParam(srcAxisInfo), makeMapperParam(tarAxisInfo)
+ )));
+ linkTriggers[tarAxisInfo.key] = val;
+ }
+ });
+ }
+ });
+ each(linkTriggers, function (val, tarKey) {
+ processOnAxis(axesInfo[tarKey], val, updaters, true, outputPayload);
+ });
+
+ updateModelActually(showValueMap, axesInfo, outputPayload);
+ dispatchTooltipActually(dataByCoordSys, point, payload, dispatchAction);
+ dispatchHighDownActually(axesInfo, dispatchAction, api);
+
+ return outputPayload;
+}
+
+function processOnAxis(
+ axisInfo: CollectedCoordInfo['axesInfo'][string],
+ newValue: AxisValue,
+ updaters: {
+ showPointer: Curry1<typeof showPointer, ShowValueMap>
+ showTooltip: Curry1<typeof showTooltip, DataByCoordSysCollection>
+ },
+ noSnap: boolean,
+ outputFinder: ModelFinderObject
+) {
+ const axis = axisInfo.axis;
+
+ if (axis.scale.isBlank() || !axis.containData(newValue)) {
+ return;
+ }
+
+ if (!axisInfo.involveSeries) {
+ updaters.showPointer(axisInfo, newValue);
+ return;
+ }
+
+ // Heavy calculation. So put it after axis.containData checking.
+ const payloadInfo = buildPayloadsBySeries(newValue, axisInfo);
+ const payloadBatch = payloadInfo.payloadBatch;
+ const snapToValue = payloadInfo.snapToValue;
+
+ // Fill content of event obj for echarts.connect.
- // By defualt use the first involved series data as a sample to connect.
++ // By default use the first involved series data as a sample to connect.
+ if (payloadBatch[0] && outputFinder.seriesIndex == null) {
+ extend(outputFinder, payloadBatch[0]);
+ }
+
+ // If no linkSource input, this process is for collecting link
+ // target, where snap should not be accepted.
+ if (!noSnap && axisInfo.snap) {
+ if (axis.containData(snapToValue) && snapToValue != null) {
+ newValue = snapToValue;
+ }
+ }
+
+ updaters.showPointer(axisInfo, newValue, payloadBatch);
+ // Tooltip should always be snapToValue, otherwise there will be
+ // incorrect "axis value ~ series value" mapping displayed in tooltip.
+ updaters.showTooltip(axisInfo, payloadInfo, snapToValue);
+}
+
+function buildPayloadsBySeries(value: AxisValue, axisInfo: CollectedAxisInfo) {
+ const axis = axisInfo.axis;
+ const dim = axis.dim;
+ let snapToValue = value;
+ const payloadBatch: BatchItem[] = [];
+ let minDist = Number.MAX_VALUE;
+ let minDiff = -1;
+
+ each(axisInfo.seriesModels, function (series, idx) {
+ const dataDim = series.getData().mapDimensionsAll(dim);
+ let seriesNestestValue;
+ let dataIndices;
+
+ if (series.getAxisTooltipData) {
+ const result = series.getAxisTooltipData(dataDim, value, axis);
+ dataIndices = result.dataIndices;
+ seriesNestestValue = result.nestestValue;
+ }
+ else {
+ dataIndices = series.getData().indicesOfNearest(
+ dataDim[0],
+ value as number,
+ // Add a threshold to avoid find the wrong dataIndex
+ // when data length is not same.
+ // false,
+ axis.type === 'category' ? 0.5 : null
+ );
+ if (!dataIndices.length) {
+ return;
+ }
+ seriesNestestValue = series.getData().get(dataDim[0], dataIndices[0]);
+ }
+
+ if (seriesNestestValue == null || !isFinite(seriesNestestValue)) {
+ return;
+ }
+
+ const diff = value as number - seriesNestestValue;
+ const dist = Math.abs(diff);
+ // Consider category case
+ if (dist <= minDist) {
+ if (dist < minDist || (diff >= 0 && minDiff < 0)) {
+ minDist = dist;
+ minDiff = diff;
+ snapToValue = seriesNestestValue;
+ payloadBatch.length = 0;
+ }
+ each(dataIndices, function (dataIndex) {
+ payloadBatch.push({
+ seriesIndex: series.seriesIndex,
+ dataIndexInside: dataIndex,
+ dataIndex: series.getData().getRawIndex(dataIndex)
+ });
+ });
+ }
+ });
+
+ return {
+ payloadBatch: payloadBatch,
+ snapToValue: snapToValue
+ };
+}
+
+function showPointer(
+ showValueMap: ShowValueMap,
+ axisInfo: CollectedAxisInfo,
+ value: AxisValue,
+ payloadBatch?: BatchItem[]
+) {
+ showValueMap[axisInfo.key] = {
+ value: value,
+ payloadBatch: payloadBatch
+ };
+}
+
+function showTooltip(
+ dataByCoordSys: DataByCoordSysCollection,
+ axisInfo: CollectedCoordInfo['axesInfo'][string],
+ payloadInfo: { payloadBatch: BatchItem[] },
+ value: AxisValue
+) {
+ const payloadBatch = payloadInfo.payloadBatch;
+ const axis = axisInfo.axis;
+ const axisModel = axis.model;
+ const axisPointerModel = axisInfo.axisPointerModel;
+
+ // If no data, do not create anything in dataByCoordSys,
+ // whose length will be used to judge whether dispatch action.
+ if (!axisInfo.triggerTooltip || !payloadBatch.length) {
+ return;
+ }
+
+ const coordSysModel = axisInfo.coordSys.model;
+ const coordSysKey = modelHelper.makeKey(coordSysModel);
+ let coordSysItem = dataByCoordSys.map[coordSysKey];
+ if (!coordSysItem) {
+ coordSysItem = dataByCoordSys.map[coordSysKey] = {
+ coordSysId: coordSysModel.id,
+ coordSysIndex: coordSysModel.componentIndex,
+ coordSysType: coordSysModel.type,
+ coordSysMainType: coordSysModel.mainType,
+ dataByAxis: []
+ };
+ dataByCoordSys.list.push(coordSysItem);
+ }
+
+ coordSysItem.dataByAxis.push({
+ axisDim: axis.dim,
+ axisIndex: axisModel.componentIndex,
+ axisType: axisModel.type,
+ axisId: axisModel.id,
+ value: value as number,
+ // Caustion: viewHelper.getValueLabel is actually on "view stage", which
+ // depends that all models have been updated. So it should not be performed
+ // here. Considering axisPointerModel used here is volatile, which is hard
+ // to be retrieve in TooltipView, we prepare parameters here.
+ valueLabelOpt: {
+ precision: axisPointerModel.get(['label', 'precision']),
+ formatter: axisPointerModel.get(['label', 'formatter'])
+ },
+ seriesDataIndices: payloadBatch.slice()
+ });
+}
+
+function updateModelActually(
+ showValueMap: ShowValueMap,
+ axesInfo: Dictionary<CollectedAxisInfo>,
+ outputPayload: AxisTriggerPayload
+) {
+ const outputAxesInfo: AxisTriggerPayload['axesInfo'] = outputPayload.axesInfo = [];
+ // Basic logic: If no 'show' required, 'hide' this axisPointer.
+ each(axesInfo, function (axisInfo, key) {
+ const option = axisInfo.axisPointerModel.option;
+ const valItem = showValueMap[key];
+
+ if (valItem) {
+ !axisInfo.useHandle && (option.status = 'show');
+ option.value = valItem.value;
+ // For label formatter param and highlight.
+ option.seriesDataIndices = (valItem.payloadBatch || []).slice();
+ }
+ // When always show (e.g., handle used), remain
+ // original value and status.
+ else {
+ // If hide, value still need to be set, consider
+ // click legend to toggle axis blank.
+ !axisInfo.useHandle && (option.status = 'hide');
+ }
+
+ // If status is 'hide', should be no info in payload.
+ option.status === 'show' && outputAxesInfo.push({
+ axisDim: axisInfo.axis.dim,
+ axisIndex: axisInfo.axis.model.componentIndex,
+ value: option.value
+ });
+ });
+}
+
+function dispatchTooltipActually(
+ dataByCoordSys: DataByCoordSysCollection,
+ point: number[],
+ payload: AxisTriggerPayload,
+ dispatchAction: ExtensionAPI['dispatchAction']
+) {
+ // Basic logic: If no showTip required, hideTip will be dispatched.
+ if (illegalPoint(point) || !dataByCoordSys.list.length) {
+ dispatchAction({type: 'hideTip'});
+ return;
+ }
+
+ // In most case only one axis (or event one series is used). It is
+ // convinient to fetch payload.seriesIndex and payload.dataIndex
+ // dirtectly. So put the first seriesIndex and dataIndex of the first
+ // axis on the payload.
+ const sampleItem = ((dataByCoordSys.list[0].dataByAxis[0] || {}).seriesDataIndices || [])[0] || {} as DataIndex;
+
+ dispatchAction({
+ type: 'showTip',
+ escapeConnect: true,
+ x: point[0],
+ y: point[1],
+ tooltipOption: payload.tooltipOption,
+ position: payload.position,
+ dataIndexInside: sampleItem.dataIndexInside,
+ dataIndex: sampleItem.dataIndex,
+ seriesIndex: sampleItem.seriesIndex,
+ dataByCoordSys: dataByCoordSys.list
+ });
+}
+
+function dispatchHighDownActually(
+ axesInfo: Dictionary<CollectedAxisInfo>,
+ dispatchAction: ExtensionAPI['dispatchAction'],
+ api: ExtensionAPI
+) {
+ // FIXME
+ // highlight status modification shoule be a stage of main process?
+ // (Consider confilct (e.g., legend and axisPointer) and setOption)
+
+ const zr = api.getZr();
+ const highDownKey = 'axisPointerLastHighlights' as const;
+ const lastHighlights = inner(zr)[highDownKey] || {};
+ const newHighlights: Dictionary<BatchItem> = inner(zr)[highDownKey] = {};
+
+ // Update highlight/downplay status according to axisPointer model.
+ // Build hash map and remove duplicate incidentally.
+ each(axesInfo, function (axisInfo, key) {
+ const option = axisInfo.axisPointerModel.option;
+ option.status === 'show' && each(option.seriesDataIndices, function (batchItem) {
+ const key = batchItem.seriesIndex + ' | ' + batchItem.dataIndex;
+ newHighlights[key] = batchItem;
+ });
+ });
+
+ // Diff.
+ const toHighlight: BatchItem[] = [];
+ const toDownplay: BatchItem[] = [];
+ each(lastHighlights, function (batchItem, key) {
+ !newHighlights[key] && toDownplay.push(batchItem);
+ });
+ each(newHighlights, function (batchItem, key) {
+ !lastHighlights[key] && toHighlight.push(batchItem);
+ });
+
+ toDownplay.length && api.dispatchAction({
+ type: 'downplay',
+ escapeConnect: true,
+ // Not blur others when highlight in axisPointer.
+ notBlur: true,
+ batch: toDownplay
+ } as DownplayPayload);
+ toHighlight.length && api.dispatchAction({
+ type: 'highlight',
+ escapeConnect: true,
+ // Not blur others when highlight in axisPointer.
+ notBlur: true,
+ batch: toHighlight
+ } as HighlightPayload);
+}
+
+function findInputAxisInfo(
+ inputAxesInfo: AxisTriggerPayload['axesInfo'],
+ axisInfo: CollectedAxisInfo
+) {
+ for (let i = 0; i < (inputAxesInfo || []).length; i++) {
+ const inputAxisInfo = inputAxesInfo[i];
+ if (axisInfo.axis.dim === inputAxisInfo.axisDim
+ && axisInfo.axis.model.componentIndex === inputAxisInfo.axisIndex
+ ) {
+ return inputAxisInfo;
+ }
+ }
+}
+
+function makeMapperParam(axisInfo: CollectedAxisInfo) {
+ const axisModel = axisInfo.axis.model;
+ const item = {} as {
+ axisDim: string
+ axisIndex: number
+ axisId: string
+ axisName: string
+ // TODO `dim`AxisIndex, `dim`AxisName, `dim`AxisId?
+ };
+ const dim = item.axisDim = axisInfo.axis.dim;
+ item.axisIndex = (item as any)[dim + 'AxisIndex'] = axisModel.componentIndex;
+ item.axisName = (item as any)[dim + 'AxisName'] = axisModel.name;
+ item.axisId = (item as any)[dim + 'AxisId'] = axisModel.id;
+ return item;
+}
+
+function illegalPoint(point?: number[]) {
+ return !point || point[0] == null || isNaN(point[0]) || point[1] == null || isNaN(point[1]);
+}
diff --cc src/component/legend/ScrollableLegendView.ts
index 3a90998,0000000..0345b87
mode 100644,000000..100644
--- a/src/component/legend/ScrollableLegendView.ts
+++ b/src/component/legend/ScrollableLegendView.ts
@@@ -1,553 -1,0 +1,553 @@@
+/*
+* 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.
+*/
+
+/**
+ * Separate legend and scrollable legend to reduce package size.
+ */
+
+import * as zrUtil from 'zrender/src/core/util';
+import * as graphic from '../../util/graphic';
+import * as layoutUtil from '../../util/layout';
+import LegendView from './LegendView';
+import { LegendSelectorButtonOption } from './LegendModel';
+import ExtensionAPI from '../../ExtensionAPI';
+import GlobalModel from '../../model/Global';
+import ScrollableLegendModel, {ScrollableLegendOption} from './ScrollableLegendModel';
+import Displayable from 'zrender/src/graphic/Displayable';
+import ComponentView from '../../view/Component';
+import Element from 'zrender/src/Element';
+import { ZRRectLike } from '../../util/types';
+
+const Group = graphic.Group;
+
+const WH = ['width', 'height'] as const;
+const XY = ['x', 'y'] as const;
+
+interface PageInfo {
+ contentPosition: number[]
+ pageCount: number
+ pageIndex: number
+ pagePrevDataIndex: number
+ pageNextDataIndex: number
+}
+
+interface ItemInfo {
+ /**
+ * Start
+ */
+ s: number
+ /**
+ * End
+ */
+ e: number
+ /**
+ * Index
+ */
+ i: number
+}
+
+type LegendGroup = graphic.Group & {
+ __rectSize: number
+};
+
+type LegendItemElement = Element & {
+ __legendDataIndex: number
+};
+
+class ScrollableLegendView extends LegendView {
+
+ static type = 'legend.scroll' as const;
+ type = ScrollableLegendView.type;
+
+ newlineDisabled = true;
+
+ private _containerGroup: LegendGroup;
+ private _controllerGroup: graphic.Group;
+
+ private _currentIndex: number = 0;
+
+ private _showController: boolean;
+
+ init() {
+
+ super.init();
+
+ this.group.add(this._containerGroup = new Group() as LegendGroup);
+ this._containerGroup.add(this.getContentGroup());
+
+ this.group.add(this._controllerGroup = new Group());
+ }
+
+ /**
+ * @override
+ */
+ resetInner() {
+ super.resetInner();
+
+ this._controllerGroup.removeAll();
+ this._containerGroup.removeClipPath();
+ this._containerGroup.__rectSize = null;
+ }
+
+ /**
+ * @override
+ */
+ renderInner(
+ itemAlign: ScrollableLegendOption['align'],
+ legendModel: ScrollableLegendModel,
+ ecModel: GlobalModel,
+ api: ExtensionAPI,
+ selector: LegendSelectorButtonOption[],
+ orient: ScrollableLegendOption['orient'],
+ selectorPosition: ScrollableLegendOption['selectorPosition']
+ ) {
+ const self = this;
+
+ // Render content items.
+ super.renderInner(itemAlign, legendModel, ecModel, api, selector, orient, selectorPosition);
+
+ const controllerGroup = this._controllerGroup;
+
+ // FIXME: support be 'auto' adapt to size number text length,
+ // e.g., '3/12345' should not overlap with the control arrow button.
+ const pageIconSize = legendModel.get('pageIconSize', true);
+ const pageIconSizeArr: number[] = zrUtil.isArray(pageIconSize)
+ ? pageIconSize : [pageIconSize, pageIconSize];
+
+ createPageButton('pagePrev', 0);
+
+ const pageTextStyleModel = legendModel.getModel('pageTextStyle');
+ controllerGroup.add(new graphic.Text({
+ name: 'pageText',
+ style: {
+ // Placeholder to calculate a proper layout.
+ text: 'xx/xx',
+ fill: pageTextStyleModel.getTextColor(),
+ font: pageTextStyleModel.getFont(),
+ verticalAlign: 'middle',
+ align: 'center'
+ },
+ silent: true
+ }));
+
+ createPageButton('pageNext', 1);
+
+ function createPageButton(name: string, iconIdx: number) {
+ const pageDataIndexName = (name + 'DataIndex') as 'pagePrevDataIndex' | 'pageNextDataIndex';
+ const icon = graphic.createIcon(
+ legendModel.get('pageIcons', true)[legendModel.getOrient().name][iconIdx],
+ {
+ // Buttons will be created in each render, so we do not need
+ // to worry about avoiding using legendModel kept in scope.
+ onclick: zrUtil.bind(
+ self._pageGo, self, pageDataIndexName, legendModel, api
+ )
+ },
+ {
+ x: -pageIconSizeArr[0] / 2,
+ y: -pageIconSizeArr[1] / 2,
+ width: pageIconSizeArr[0],
+ height: pageIconSizeArr[1]
+ }
+ );
+ icon.name = name;
+ controllerGroup.add(icon);
+ }
+ }
+
+ /**
+ * @override
+ */
+ layoutInner(
+ legendModel: ScrollableLegendModel,
+ itemAlign: ScrollableLegendOption['align'],
+ maxSize: { width: number, height: number },
+ isFirstRender: boolean,
+ selector: LegendSelectorButtonOption[],
+ selectorPosition: ScrollableLegendOption['selectorPosition']
+ ) {
+ const selectorGroup = this.getSelectorGroup();
+
+ const orientIdx = legendModel.getOrient().index;
+ const wh = WH[orientIdx];
+ const xy = XY[orientIdx];
+ const hw = WH[1 - orientIdx];
+ const yx = XY[1 - orientIdx];
+
+ selector && layoutUtil.box(
+ // Buttons in selectorGroup always layout horizontally
+ 'horizontal',
+ selectorGroup,
+ legendModel.get('selectorItemGap', true)
+ );
+
+ const selectorButtonGap = legendModel.get('selectorButtonGap', true);
+ const selectorRect = selectorGroup.getBoundingRect();
+ const selectorPos = [-selectorRect.x, -selectorRect.y];
+
+ const processMaxSize = zrUtil.clone(maxSize);
+ selector && (processMaxSize[wh] = maxSize[wh] - selectorRect[wh] - selectorButtonGap);
+
+ const mainRect = this._layoutContentAndController(legendModel, isFirstRender,
+ processMaxSize, orientIdx, wh, hw, yx
+ );
+
+ if (selector) {
+ if (selectorPosition === 'end') {
+ selectorPos[orientIdx] += mainRect[wh] + selectorButtonGap;
+ }
+ else {
+ const offset = selectorRect[wh] + selectorButtonGap;
+ selectorPos[orientIdx] -= offset;
+ mainRect[xy] -= offset;
+ }
+ mainRect[wh] += selectorRect[wh] + selectorButtonGap;
+
+ selectorPos[1 - orientIdx] += mainRect[yx] + mainRect[hw] / 2 - selectorRect[hw] / 2;
+ mainRect[hw] = Math.max(mainRect[hw], selectorRect[hw]);
+ mainRect[yx] = Math.min(mainRect[yx], selectorRect[yx] + selectorPos[1 - orientIdx]);
+
+ selectorGroup.x = selectorPos[0];
+ selectorGroup.y = selectorPos[1];
+ selectorGroup.markRedraw();
+ }
+
+ return mainRect;
+ }
+
+ _layoutContentAndController(
+ legendModel: ScrollableLegendModel,
+ isFirstRender: boolean,
+ maxSize: { width: number, height: number },
+ orientIdx: 0 | 1,
+ wh: 'width' | 'height',
+ hw: 'width' | 'height',
+ yx: 'x' | 'y'
+ ) {
+ const contentGroup = this.getContentGroup();
+ const containerGroup = this._containerGroup;
+ const controllerGroup = this._controllerGroup;
+
+ // Place items in contentGroup.
+ layoutUtil.box(
+ legendModel.get('orient'),
+ contentGroup,
+ legendModel.get('itemGap'),
+ !orientIdx ? null : maxSize.width,
+ orientIdx ? null : maxSize.height
+ );
+
+ layoutUtil.box(
+ // Buttons in controller are layout always horizontally.
+ 'horizontal',
+ controllerGroup,
+ legendModel.get('pageButtonItemGap', true)
+ );
+
+ const contentRect = contentGroup.getBoundingRect();
+ const controllerRect = controllerGroup.getBoundingRect();
+ const showController = this._showController = contentRect[wh] > maxSize[wh];
+
+ const contentPos = [-contentRect.x, -contentRect.y];
+ // Remain contentPos when scroll animation perfroming.
+ // If first rendering, `contentGroup.position` is [0, 0], which
+ // does not make sense and may cause unexepcted animation if adopted.
+ if (!isFirstRender) {
+ contentPos[orientIdx] = contentGroup[yx];
+ }
+
+ // Layout container group based on 0.
+ const containerPos = [0, 0];
+ const controllerPos = [-controllerRect.x, -controllerRect.y];
+ const pageButtonGap = zrUtil.retrieve2(
+ legendModel.get('pageButtonGap', true), legendModel.get('itemGap', true)
+ );
+
+ // Place containerGroup and controllerGroup and contentGroup.
+ if (showController) {
+ const pageButtonPosition = legendModel.get('pageButtonPosition', true);
+ // controller is on the right / bottom.
+ if (pageButtonPosition === 'end') {
+ controllerPos[orientIdx] += maxSize[wh] - controllerRect[wh];
+ }
+ // controller is on the left / top.
+ else {
+ containerPos[orientIdx] += controllerRect[wh] + pageButtonGap;
+ }
+ }
+
+ // Always align controller to content as 'middle'.
+ controllerPos[1 - orientIdx] += contentRect[hw] / 2 - controllerRect[hw] / 2;
+
+ contentGroup.setPosition(contentPos);
+ containerGroup.setPosition(containerPos);
+ controllerGroup.setPosition(controllerPos);
+
+ // Calculate `mainRect` and set `clipPath`.
+ // mainRect should not be calculated by `this.group.getBoundingRect()`
+ // for sake of the overflow.
+ const mainRect = {x: 0, y: 0} as ZRRectLike;
+
+ // Consider content may be overflow (should be clipped).
+ mainRect[wh] = showController ? maxSize[wh] : contentRect[wh];
+ mainRect[hw] = Math.max(contentRect[hw], controllerRect[hw]);
+
+ // `containerRect[yx] + containerPos[1 - orientIdx]` is 0.
+ mainRect[yx] = Math.min(0, controllerRect[yx] + controllerPos[1 - orientIdx]);
+
+ containerGroup.__rectSize = maxSize[wh];
+ if (showController) {
+ const clipShape = {x: 0, y: 0} as graphic.Rect['shape'];
+ clipShape[wh] = Math.max(maxSize[wh] - controllerRect[wh] - pageButtonGap, 0);
+ clipShape[hw] = mainRect[hw];
+ containerGroup.setClipPath(new graphic.Rect({shape: clipShape}));
+ // Consider content may be larger than container, container rect
+ // can not be obtained from `containerGroup.getBoundingRect()`.
+ containerGroup.__rectSize = clipShape[wh];
+ }
+ else {
+ // Do not remove or ignore controller. Keep them set as placeholders.
+ controllerGroup.eachChild(function (child: Displayable) {
+ child.attr({
+ invisible: true,
+ silent: true
+ });
+ });
+ }
+
+ // Content translate animation.
+ const pageInfo = this._getPageInfo(legendModel);
+ pageInfo.pageIndex != null && graphic.updateProps(
+ contentGroup,
+ { x: pageInfo.contentPosition[0], y: pageInfo.contentPosition[1] },
+ // When switch from "show controller" to "not show controller", view should be
+ // updated immediately without animation, otherwise causes weird effect.
+ showController ? legendModel : null
+ );
+
+ this._updatePageInfoView(legendModel, pageInfo);
+
+ return mainRect;
+ }
+
+ _pageGo(
+ to: 'pagePrevDataIndex' | 'pageNextDataIndex',
+ legendModel: ScrollableLegendModel,
+ api: ExtensionAPI
+ ) {
+ const scrollDataIndex = this._getPageInfo(legendModel)[to];
+
+ scrollDataIndex != null && api.dispatchAction({
+ type: 'legendScroll',
+ scrollDataIndex: scrollDataIndex,
+ legendId: legendModel.id
+ });
+ }
+
+ _updatePageInfoView(
+ legendModel: ScrollableLegendModel,
+ pageInfo: PageInfo
+ ) {
+ const controllerGroup = this._controllerGroup;
+
+ zrUtil.each(['pagePrev', 'pageNext'], function (name) {
+ const key = (name + 'DataIndex') as'pagePrevDataIndex' | 'pageNextDataIndex';
+ const canJump = pageInfo[key] != null;
+ const icon = controllerGroup.childOfName(name) as graphic.Path;
+ if (icon) {
+ icon.setStyle(
+ 'fill',
+ canJump
+ ? legendModel.get('pageIconColor', true)
+ : legendModel.get('pageIconInactiveColor', true)
+ );
+ icon.cursor = canJump ? 'pointer' : 'default';
+ }
+ });
+
+ const pageText = controllerGroup.childOfName('pageText') as graphic.Text;
+ const pageFormatter = legendModel.get('pageFormatter');
+ const pageIndex = pageInfo.pageIndex;
+ const current = pageIndex != null ? pageIndex + 1 : 0;
+ const total = pageInfo.pageCount;
+
+ pageText && pageFormatter && pageText.setStyle(
+ 'text',
+ zrUtil.isString(pageFormatter)
+ ? pageFormatter.replace('{current}', current == null ? '' : current + '')
+ .replace('{total}', total == null ? '' : total + '')
+ : pageFormatter({current: current, total: total})
+ );
+ }
+
+ /**
+ * contentPosition: Array.<number>, null when data item not found.
+ * pageIndex: number, null when data item not found.
+ * pageCount: number, always be a number, can be 0.
+ * pagePrevDataIndex: number, null when no previous page.
+ * pageNextDataIndex: number, null when no next page.
+ * }
+ */
+ _getPageInfo(legendModel: ScrollableLegendModel): PageInfo {
+ const scrollDataIndex = legendModel.get('scrollDataIndex', true);
+ const contentGroup = this.getContentGroup();
+ const containerRectSize = this._containerGroup.__rectSize;
+ const orientIdx = legendModel.getOrient().index;
+ const wh = WH[orientIdx];
+ const xy = XY[orientIdx];
+
+ const targetItemIndex = this._findTargetItemIndex(scrollDataIndex);
+ const children = contentGroup.children();
+ const targetItem = children[targetItemIndex];
+ const itemCount = children.length;
+ const pCount = !itemCount ? 0 : 1;
+
+ const result: PageInfo = {
+ contentPosition: [contentGroup.x, contentGroup.y],
+ pageCount: pCount,
+ pageIndex: pCount - 1,
+ pagePrevDataIndex: null,
+ pageNextDataIndex: null
+ };
+
+ if (!targetItem) {
+ return result;
+ }
+
+ const targetItemInfo = getItemInfo(targetItem);
+ result.contentPosition[orientIdx] = -targetItemInfo.s;
+
+ // Strategy:
+ // (1) Always align based on the left/top most item.
+ // (2) It is user-friendly that the last item shown in the
+ // current window is shown at the begining of next window.
+ // Otherwise if half of the last item is cut by the window,
+ // it will have no chance to display entirely.
+ // (3) Consider that item size probably be different, we
+ // have calculate pageIndex by size rather than item index,
+ // and we can not get page index directly by division.
+ // (4) The window is to narrow to contain more than
+ // one item, we should make sure that the page can be fliped.
+
+ for (let i = targetItemIndex + 1,
+ winStartItemInfo = targetItemInfo,
+ winEndItemInfo = targetItemInfo,
+ currItemInfo = null;
+ i <= itemCount;
+ ++i
+ ) {
+ currItemInfo = getItemInfo(children[i]);
+ if (
+ // Half of the last item is out of the window.
+ (!currItemInfo && winEndItemInfo.e > winStartItemInfo.s + containerRectSize)
+ // If the current item does not intersect with the window, the new page
+ // can be started at the current item or the last item.
+ || (currItemInfo && !intersect(currItemInfo, winStartItemInfo.s))
+ ) {
+ if (winEndItemInfo.i > winStartItemInfo.i) {
+ winStartItemInfo = winEndItemInfo;
+ }
+ else { // e.g., when page size is smaller than item size.
+ winStartItemInfo = currItemInfo;
+ }
+ if (winStartItemInfo) {
+ if (result.pageNextDataIndex == null) {
+ result.pageNextDataIndex = winStartItemInfo.i;
+ }
+ ++result.pageCount;
+ }
+ }
+ winEndItemInfo = currItemInfo;
+ }
+
+ for (let i = targetItemIndex - 1,
+ winStartItemInfo = targetItemInfo,
+ winEndItemInfo = targetItemInfo,
+ currItemInfo = null;
+ i >= -1;
+ --i
+ ) {
+ currItemInfo = getItemInfo(children[i]);
+ if (
+ // If the the end item does not intersect with the window started
+ // from the current item, a page can be settled.
+ (!currItemInfo || !intersect(winEndItemInfo, currItemInfo.s))
+ // e.g., when page size is smaller than item size.
+ && winStartItemInfo.i < winEndItemInfo.i
+ ) {
+ winEndItemInfo = winStartItemInfo;
+ if (result.pagePrevDataIndex == null) {
+ result.pagePrevDataIndex = winStartItemInfo.i;
+ }
+ ++result.pageCount;
+ ++result.pageIndex;
+ }
+ winStartItemInfo = currItemInfo;
+ }
+
+ return result;
+
+ function getItemInfo(el: Element): ItemInfo {
+ if (el) {
+ const itemRect = el.getBoundingRect();
+ const start = itemRect[xy] + el[xy];
+ return {
+ s: start,
+ e: start + itemRect[wh],
+ i: (el as LegendItemElement).__legendDataIndex
+ };
+ }
+ }
+
+ function intersect(itemInfo: ItemInfo, winStart: number) {
+ return itemInfo.e >= winStart && itemInfo.s <= winStart + containerRectSize;
+ }
+ }
+
+ _findTargetItemIndex(targetDataIndex: number) {
+ if (!this._showController) {
+ return 0;
+ }
+
+ let index;
+ const contentGroup = this.getContentGroup();
+ let defaultIndex: number;
+
+ contentGroup.eachChild(function (child, idx) {
+ const legendDataIdx = (child as LegendItemElement).__legendDataIndex;
+ // FIXME
+ // If the given targetDataIndex (from model) is illegal,
- // we use defualtIndex. But the index on the legend model and
++ // we use defaultIndex. But the index on the legend model and
+ // action payload is still illegal. That case will not be
+ // changed until some scenario requires.
+ if (defaultIndex == null && legendDataIdx != null) {
+ defaultIndex = idx;
+ }
+ if (legendDataIdx === targetDataIndex) {
+ index = idx;
+ }
+ });
+
+ return index != null ? index : defaultIndex;
+ }
+}
+
+ComponentView.registerClass(ScrollableLegendView);
+
+export default ScrollableLegendView;
diff --cc src/component/marker/MarkAreaView.ts
index cd992fc,0000000..2b12284
mode 100644,000000..100644
--- a/src/component/marker/MarkAreaView.ts
+++ b/src/component/marker/MarkAreaView.ts
@@@ -1,382 -1,0 +1,421 @@@
+/*
+* 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.
+*/
+
+// TODO Optimize on polar
+
+import * as colorUtil from 'zrender/src/tool/color';
+import List from '../../data/List';
+import * as numberUtil from '../../util/number';
+import * as graphic from '../../util/graphic';
+import { enableHoverEmphasis, setStatesStylesFromModel } from '../../util/states';
+import * as markerHelper from './markerHelper';
+import MarkerView from './MarkerView';
- import { retrieve, mergeAll, map, defaults, curry, filter, HashMap } from 'zrender/src/core/util';
++import { retrieve, mergeAll, map, defaults, curry, filter, HashMap, each } from 'zrender/src/core/util';
+import { ScaleDataValue, ParsedValue, ZRColor } from '../../util/types';
+import { CoordinateSystem, isCoordinateSystemType } from '../../coord/CoordinateSystem';
+import MarkAreaModel, { MarkArea2DDataItemOption } from './MarkAreaModel';
+import SeriesModel from '../../model/Series';
+import Cartesian2D from '../../coord/cartesian/Cartesian2D';
+import DataDimensionInfo from '../../data/DataDimensionInfo';
+import ComponentView from '../../view/Component';
+import GlobalModel from '../../model/Global';
+import ExtensionAPI from '../../ExtensionAPI';
+import MarkerModel from './MarkerModel';
+import { makeInner } from '../../util/model';
+import { getVisualFromData } from '../../visual/helper';
+import { setLabelStyle, getLabelStatesModels } from '../../label/labelStyle';
+import { getECData } from '../../util/innerStore';
+
+interface MarkAreaDrawGroup {
+ group: graphic.Group
+}
+
+const inner = makeInner<{
+ data: List<MarkAreaModel>
+}, MarkAreaDrawGroup>();
+
+// Merge two ends option into one.
+type MarkAreaMergedItemOption = Omit<MarkArea2DDataItemOption[number], 'coord'> & {
+ coord: MarkArea2DDataItemOption[number]['coord'][]
+ x0: number | string
+ y0: number | string
+ x1: number | string
+ y1: number | string
+};
+
+const markAreaTransform = function (
+ seriesModel: SeriesModel,
+ coordSys: CoordinateSystem,
+ maModel: MarkAreaModel,
+ item: MarkArea2DDataItemOption
+): MarkAreaMergedItemOption {
+ const lt = markerHelper.dataTransform(seriesModel, item[0]);
+ const rb = markerHelper.dataTransform(seriesModel, item[1]);
+
+ // FIXME make sure lt is less than rb
+ const ltCoord = lt.coord;
+ const rbCoord = rb.coord;
+ ltCoord[0] = retrieve(ltCoord[0], -Infinity);
+ ltCoord[1] = retrieve(ltCoord[1], -Infinity);
+
+ rbCoord[0] = retrieve(rbCoord[0], Infinity);
+ rbCoord[1] = retrieve(rbCoord[1], Infinity);
+
+ // Merge option into one
+ const result: MarkAreaMergedItemOption = mergeAll([{}, lt, rb]);
+
+ result.coord = [
+ lt.coord, rb.coord
+ ];
+ result.x0 = lt.x;
+ result.y0 = lt.y;
+ result.x1 = rb.x;
+ result.y1 = rb.y;
+ return result;
+};
+
+function isInifinity(val: ScaleDataValue) {
+ return !isNaN(val as number) && !isFinite(val as number);
+}
+
+// If a markArea has one dim
+function ifMarkAreaHasOnlyDim(
+ dimIndex: number,
+ fromCoord: ScaleDataValue[],
+ toCoord: ScaleDataValue[],
+ coordSys: CoordinateSystem
+) {
+ const otherDimIndex = 1 - dimIndex;
+ return isInifinity(fromCoord[otherDimIndex]) && isInifinity(toCoord[otherDimIndex]);
+}
+
+function markAreaFilter(coordSys: CoordinateSystem, item: MarkAreaMergedItemOption) {
+ const fromCoord = item.coord[0];
+ const toCoord = item.coord[1];
+ if (isCoordinateSystemType<Cartesian2D>(coordSys, 'cartesian2d')) {
+ // In case
+ // {
+ // markArea: {
+ // data: [{ yAxis: 2 }]
+ // }
+ // }
+ if (
+ fromCoord && toCoord
+ && (ifMarkAreaHasOnlyDim(1, fromCoord, toCoord, coordSys)
+ || ifMarkAreaHasOnlyDim(0, fromCoord, toCoord, coordSys))
+ ) {
+ return true;
+ }
+ }
+ return markerHelper.dataFilter(coordSys, {
+ coord: fromCoord,
+ x: item.x0,
+ y: item.y0
+ })
+ || markerHelper.dataFilter(coordSys, {
+ coord: toCoord,
+ x: item.x1,
+ y: item.y1
+ });
+}
+
+// dims can be ['x0', 'y0'], ['x1', 'y1'], ['x0', 'y1'], ['x1', 'y0']
+function getSingleMarkerEndPoint(
+ data: List<MarkAreaModel>,
+ idx: number,
+ dims: typeof dimPermutations[number],
+ seriesModel: SeriesModel,
+ api: ExtensionAPI
+) {
+ const coordSys = seriesModel.coordinateSystem;
+ const itemModel = data.getItemModel<MarkAreaMergedItemOption>(idx);
+
+ let point;
+ const xPx = numberUtil.parsePercent(itemModel.get(dims[0]), api.getWidth());
+ const yPx = numberUtil.parsePercent(itemModel.get(dims[1]), api.getHeight());
+ if (!isNaN(xPx) && !isNaN(yPx)) {
+ point = [xPx, yPx];
+ }
+ else {
+ // Chart like bar may have there own marker positioning logic
+ if (seriesModel.getMarkerPosition) {
+ // Use the getMarkerPoisition
+ point = seriesModel.getMarkerPosition(
+ data.getValues(dims, idx)
+ );
+ }
+ else {
+ const x = data.get(dims[0], idx) as number;
+ const y = data.get(dims[1], idx) as number;
+ const pt = [x, y];
+ coordSys.clampData && coordSys.clampData(pt, pt);
+ point = coordSys.dataToPoint(pt, true);
+ }
+ if (isCoordinateSystemType<Cartesian2D>(coordSys, 'cartesian2d')) {
+ const xAxis = coordSys.getAxis('x');
+ const yAxis = coordSys.getAxis('y');
+ const x = data.get(dims[0], idx) as number;
+ const y = data.get(dims[1], idx) as number;
+ if (isInifinity(x)) {
+ point[0] = xAxis.toGlobalCoord(xAxis.getExtent()[dims[0] === 'x0' ? 0 : 1]);
+ }
+ else if (isInifinity(y)) {
+ point[1] = yAxis.toGlobalCoord(yAxis.getExtent()[dims[1] === 'y0' ? 0 : 1]);
+ }
+ }
+
+ // Use x, y if has any
+ if (!isNaN(xPx)) {
+ point[0] = xPx;
+ }
+ if (!isNaN(yPx)) {
+ point[1] = yPx;
+ }
+ }
+
+ return point;
+}
+
+const dimPermutations = [['x0', 'y0'], ['x1', 'y0'], ['x1', 'y1'], ['x0', 'y1']] as const;
+
+class MarkAreaView extends MarkerView {
+
+ static type = 'markArea';
+ type = MarkAreaView.type;
+
+ markerGroupMap: HashMap<MarkAreaDrawGroup>;
+
+ updateTransform(markAreaModel: MarkAreaModel, ecModel: GlobalModel, api: ExtensionAPI) {
+ ecModel.eachSeries(function (seriesModel) {
+ const maModel = MarkerModel.getMarkerModelFromSeries(seriesModel, 'markArea');
+ if (maModel) {
+ const areaData = maModel.getData();
+ areaData.each(function (idx) {
+ const points = map(dimPermutations, function (dim) {
+ return getSingleMarkerEndPoint(areaData, idx, dim, seriesModel, api);
+ });
+ // Layout
+ areaData.setItemLayout(idx, points);
+ const el = areaData.getItemGraphicEl(idx) as graphic.Polygon;
+ el.setShape('points', points);
+ });
+ }
+ }, this);
+ }
+
+ renderSeries(
+ seriesModel: SeriesModel,
+ maModel: MarkAreaModel,
+ ecModel: GlobalModel,
+ api: ExtensionAPI
+ ) {
+ const coordSys = seriesModel.coordinateSystem;
+ const seriesId = seriesModel.id;
+ const seriesData = seriesModel.getData();
+
+ const areaGroupMap = this.markerGroupMap;
+ const polygonGroup = areaGroupMap.get(seriesId)
+ || areaGroupMap.set(seriesId, {group: new graphic.Group()});
+
+ this.group.add(polygonGroup.group);
+ this.markKeep(polygonGroup);
+
+ const areaData = createList(coordSys, seriesModel, maModel);
+
+ // Line data for tooltip and formatter
+ maModel.setData(areaData);
+
+ // Update visual and layout of line
+ areaData.each(function (idx) {
+ // Layout
- areaData.setItemLayout(idx, map(dimPermutations, function (dim) {
++ const points = map(dimPermutations, function (dim) {
+ return getSingleMarkerEndPoint(areaData, idx, dim, seriesModel, api);
- }));
++ });
++ // If none of the area is inside coordSys, allClipped is set to be true
++ // in layout so that label will not be displayed. See #12591
++ let allClipped = true;
++ each(dimPermutations, function (dim) {
++ if (!allClipped) {
++ return;
++ }
++ const xValue = areaData.get(dim[0], idx);
++ const yValue = areaData.get(dim[1], idx);
++ // If is infinity, the axis should be considered not clipped
++ if ((isInifinity(xValue) || coordSys.getAxis('x').containData(xValue))
++ && (isInifinity(yValue) || coordSys.getAxis('y').containData(yValue))
++ ) {
++ allClipped = false;
++ }
++ });
++ areaData.setItemLayout(idx, {
++ points: points,
++ allClipped: allClipped
++ });
++
+
+ const style = areaData.getItemModel<MarkAreaMergedItemOption>(idx).getModel('itemStyle').getItemStyle();
+ const color = getVisualFromData(seriesData, 'color') as ZRColor;
+ if (!style.fill) {
+ style.fill = color;
+ if (typeof style.fill === 'string') {
+ style.fill = colorUtil.modifyAlpha(style.fill, 0.4);
+ }
+ }
+ if (!style.stroke) {
+ style.stroke = color;
+ }
+ // Visual
+ areaData.setItemVisual(idx, 'style', style);
+ });
+
+
+ areaData.diff(inner(polygonGroup).data)
+ .add(function (idx) {
- const polygon = new graphic.Polygon({
- shape: {
- points: areaData.getItemLayout(idx)
- }
- });
- areaData.setItemGraphicEl(idx, polygon);
- polygonGroup.group.add(polygon);
++ const layout = areaData.getItemLayout(idx);
++ if (!layout.allClipped) {
++ const polygon = new graphic.Polygon({
++ shape: {
++ points: layout.points
++ }
++ });
++ areaData.setItemGraphicEl(idx, polygon);
++ polygonGroup.group.add(polygon);
++ }
+ })
+ .update(function (newIdx, oldIdx) {
- const polygon = inner(polygonGroup).data.getItemGraphicEl(oldIdx) as graphic.Polygon;
- graphic.updateProps(polygon, {
- shape: {
- points: areaData.getItemLayout(newIdx)
++ let polygon = inner(polygonGroup).data.getItemGraphicEl(oldIdx) as graphic.Polygon;
++ const layout = areaData.getItemLayout(newIdx);
++ if (!layout.allClipped) {
++ if (polygon) {
++ graphic.updateProps(polygon, {
++ shape: {
++ points: layout.points
++ }
++ }, maModel, newIdx);
++ }
++ else {
++ polygon = new graphic.Polygon({
++ shape: {
++ points: layout.points
++ }
++ });
+ }
- }, maModel, newIdx);
- polygonGroup.group.add(polygon);
- areaData.setItemGraphicEl(newIdx, polygon);
++ areaData.setItemGraphicEl(newIdx, polygon);
++ polygonGroup.group.add(polygon);
++ }
++ else if (polygon) {
++ polygonGroup.group.remove(polygon);
++ }
+ })
+ .remove(function (idx) {
+ const polygon = inner(polygonGroup).data.getItemGraphicEl(idx);
+ polygonGroup.group.remove(polygon);
+ })
+ .execute();
+
+ areaData.eachItemGraphicEl(function (polygon: graphic.Polygon, idx) {
+ const itemModel = areaData.getItemModel<MarkAreaMergedItemOption>(idx);
+ const style = areaData.getItemVisual(idx, 'style');
+ polygon.useStyle(areaData.getItemVisual(idx, 'style'));
+
+ setLabelStyle(
+ polygon, getLabelStatesModels(itemModel),
+ {
+ labelFetcher: maModel,
+ labelDataIndex: idx,
+ defaultText: areaData.getName(idx) || '',
+ inheritColor: typeof style.fill === 'string'
+ ? colorUtil.modifyAlpha(style.fill, 1) : '#000'
+ }
+ );
+
+ setStatesStylesFromModel(polygon, itemModel);
+
+ enableHoverEmphasis(polygon);
+
+ getECData(polygon).dataModel = maModel;
+ });
+
+ inner(polygonGroup).data = areaData;
+
+ polygonGroup.group.silent = maModel.get('silent') || seriesModel.get('silent');
+ }
+}
+
+function createList(
+ coordSys: CoordinateSystem,
+ seriesModel: SeriesModel,
+ maModel: MarkAreaModel
+) {
+
+ let coordDimsInfos: DataDimensionInfo[];
+ let areaData: List<MarkAreaModel>;
+ const dims = ['x0', 'y0', 'x1', 'y1'];
+ if (coordSys) {
+ coordDimsInfos = map(coordSys && coordSys.dimensions, function (coordDim) {
+ const data = seriesModel.getData();
+ const info = data.getDimensionInfo(
+ data.mapDimension(coordDim)
+ ) || {};
+ // In map series data don't have lng and lat dimension. Fallback to same with coordSys
+ return defaults({
+ name: coordDim
+ }, info);
+ });
+ areaData = new List(map(dims, function (dim, idx) {
+ return {
+ name: dim,
+ type: coordDimsInfos[idx % 2].type
+ };
+ }), maModel);
+ }
+ else {
+ coordDimsInfos = [{
+ name: 'value',
+ type: 'float'
+ }];
+ areaData = new List(coordDimsInfos, maModel);
+ }
+
+ let optData = map(maModel.get('data'), curry(
+ markAreaTransform, seriesModel, coordSys, maModel
+ ));
+ if (coordSys) {
+ optData = filter(
+ optData, curry(markAreaFilter, coordSys)
+ );
+ }
+
+ const dimValueGetter = coordSys ? function (
+ item: MarkAreaMergedItemOption,
+ dimName: string,
+ dataIndex: number,
+ dimIndex: number
+ ) {
+ // TODO should convert to ParsedValue?
+ return item.coord[Math.floor(dimIndex / 2)][dimIndex % 2] as ParsedValue;
+ } : function (item: MarkAreaMergedItemOption) {
+ return item.value;
+ };
+ areaData.initData(optData, null, dimValueGetter);
+ areaData.hasItemOption = true;
+ return areaData;
+}
+
+ComponentView.registerClass(MarkAreaView);
diff --cc src/component/marker/MarkLineView.ts
index 6f890ac,0000000..56e6b65
mode 100644,000000..100644
--- a/src/component/marker/MarkLineView.ts
+++ b/src/component/marker/MarkLineView.ts
@@@ -1,461 -1,0 +1,464 @@@
+/*
+* 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 List from '../../data/List';
+import * as numberUtil from '../../util/number';
+import * as markerHelper from './markerHelper';
+import LineDraw from '../../chart/helper/LineDraw';
+import MarkerView from './MarkerView';
+import {getStackedDimension} from '../../data/helper/dataStackHelper';
+import { CoordinateSystem, isCoordinateSystemType } from '../../coord/CoordinateSystem';
+import MarkLineModel, { MarkLine2DDataItemOption, MarkLineOption } from './MarkLineModel';
+import { ScaleDataValue, ColorString } from '../../util/types';
+import SeriesModel from '../../model/Series';
+import { getECData } from '../../util/innerStore';
+import ExtensionAPI from '../../ExtensionAPI';
+import Cartesian2D from '../../coord/cartesian/Cartesian2D';
+import GlobalModel from '../../model/Global';
+import MarkerModel from './MarkerModel';
+import {
+ isArray,
+ retrieve,
+ clone,
+ extend,
+ logError,
+ merge,
+ map,
+ defaults,
+ curry,
+ filter,
+ HashMap
+} from 'zrender/src/core/util';
+import ComponentView from '../../view/Component';
+import { makeInner } from '../../util/model';
+import { LineDataVisual } from '../../visual/commonVisualTypes';
+import { getVisualFromData } from '../../visual/helper';
+
+// Item option for configuring line and each end of symbol.
+// Line option. be merged from configuration of two ends.
+type MarkLineMergedItemOption = MarkLine2DDataItemOption[number];
+
+const inner = makeInner<{
+ // from data
+ from: List<MarkLineModel>
+ // to data
+ to: List<MarkLineModel>
+}, MarkLineModel>();
+
+const markLineTransform = function (
+ seriesModel: SeriesModel,
+ coordSys: CoordinateSystem,
+ mlModel: MarkLineModel,
+ item: MarkLineOption['data'][number]
+) {
+ const data = seriesModel.getData();
+
+ let itemArray: MarkLineMergedItemOption[];
+ if (!isArray(item)) {
+ // Special type markLine like 'min', 'max', 'average', 'median'
+ const mlType = item.type;
+ if (
+ mlType === 'min' || mlType === 'max' || mlType === 'average' || mlType === 'median'
+ // In case
+ // data: [{
+ // yAxis: 10
+ // }]
+ || (item.xAxis != null || item.yAxis != null)
+ ) {
+
+ let valueAxis;
+ let value;
+
+ if (item.yAxis != null || item.xAxis != null) {
+ valueAxis = coordSys.getAxis(item.yAxis != null ? 'y' : 'x');
+ value = retrieve(item.yAxis, item.xAxis);
+ }
+ else {
+ const axisInfo = markerHelper.getAxisInfo(item, data, coordSys, seriesModel);
+ valueAxis = axisInfo.valueAxis;
+ const valueDataDim = getStackedDimension(data, axisInfo.valueDataDim);
+ value = markerHelper.numCalculate(data, valueDataDim, mlType);
+ }
+ const valueIndex = valueAxis.dim === 'x' ? 0 : 1;
+ const baseIndex = 1 - valueIndex;
+
+ // Normized to 2d data with start and end point
+ const mlFrom = clone(item) as MarkLine2DDataItemOption[number];
+ const mlTo = {
+ coord: []
+ } as MarkLine2DDataItemOption[number];
+
+ mlFrom.type = null;
+
+ mlFrom.coord = [];
+ mlFrom.coord[baseIndex] = -Infinity;
+ mlTo.coord[baseIndex] = Infinity;
+
+ const precision = mlModel.get('precision');
+ if (precision >= 0 && typeof value === 'number') {
+ value = +value.toFixed(Math.min(precision, 20));
+ }
+
+ mlFrom.coord[valueIndex] = mlTo.coord[valueIndex] = value;
+
+ itemArray = [mlFrom, mlTo, { // Extra option for tooltip and label
+ type: mlType,
+ valueIndex: item.valueIndex,
+ // Force to use the value of calculated value.
+ value: value
+ }];
+ }
+ else {
+ // Invalid data
+ if (__DEV__) {
+ logError('Invalid markLine data.');
+ }
+ itemArray = [];
+ }
+ }
+ else {
+ itemArray = item;
+ }
+
+ const normalizedItem = [
+ markerHelper.dataTransform(seriesModel, itemArray[0]),
+ markerHelper.dataTransform(seriesModel, itemArray[1]),
+ extend({}, itemArray[2])
+ ];
+
+ // Avoid line data type is extended by from(to) data type
+ normalizedItem[2].type = normalizedItem[2].type || null;
+
+ // Merge from option and to option into line option
+ merge(normalizedItem[2], normalizedItem[0]);
+ merge(normalizedItem[2], normalizedItem[1]);
+
+ return normalizedItem;
+};
+
+function isInifinity(val: ScaleDataValue) {
+ return !isNaN(val as number) && !isFinite(val as number);
+}
+
+// If a markLine has one dim
+function ifMarkLineHasOnlyDim(
+ dimIndex: number,
+ fromCoord: ScaleDataValue[],
+ toCoord: ScaleDataValue[],
+ coordSys: CoordinateSystem
+) {
+ const otherDimIndex = 1 - dimIndex;
+ const dimName = coordSys.dimensions[dimIndex];
+ return isInifinity(fromCoord[otherDimIndex]) && isInifinity(toCoord[otherDimIndex])
+ && fromCoord[dimIndex] === toCoord[dimIndex] && coordSys.getAxis(dimName).containData(fromCoord[dimIndex]);
+}
+
+function markLineFilter(
+ coordSys: CoordinateSystem,
+ item: MarkLine2DDataItemOption
+) {
+ if (coordSys.type === 'cartesian2d') {
+ const fromCoord = item[0].coord;
+ const toCoord = item[1].coord;
+ // In case
+ // {
+ // markLine: {
+ // data: [{ yAxis: 2 }]
+ // }
+ // }
+ if (
+ fromCoord && toCoord
+ && (ifMarkLineHasOnlyDim(1, fromCoord, toCoord, coordSys)
+ || ifMarkLineHasOnlyDim(0, fromCoord, toCoord, coordSys))
+ ) {
+ return true;
+ }
+ }
+ return markerHelper.dataFilter(coordSys, item[0])
+ && markerHelper.dataFilter(coordSys, item[1]);
+}
+
+function updateSingleMarkerEndLayout(
+ data: List<MarkLineModel>,
+ idx: number,
+ isFrom: boolean,
+ seriesModel: SeriesModel,
+ api: ExtensionAPI
+) {
+ const coordSys = seriesModel.coordinateSystem;
+ const itemModel = data.getItemModel<MarkLine2DDataItemOption[number]>(idx);
+
+ let point;
+ const xPx = numberUtil.parsePercent(itemModel.get('x'), api.getWidth());
+ const yPx = numberUtil.parsePercent(itemModel.get('y'), api.getHeight());
+ if (!isNaN(xPx) && !isNaN(yPx)) {
+ point = [xPx, yPx];
+ }
+ else {
+ // Chart like bar may have there own marker positioning logic
+ if (seriesModel.getMarkerPosition) {
+ // Use the getMarkerPoisition
+ point = seriesModel.getMarkerPosition(
+ data.getValues(data.dimensions, idx)
+ );
+ }
+ else {
+ const dims = coordSys.dimensions;
+ const x = data.get(dims[0], idx);
+ const y = data.get(dims[1], idx);
+ point = coordSys.dataToPoint([x, y]);
+ }
+ // Expand line to the edge of grid if value on one axis is Inifnity
+ // In case
+ // markLine: {
+ // data: [{
+ // yAxis: 2
+ // // or
+ // type: 'average'
+ // }]
+ // }
+ if (isCoordinateSystemType<Cartesian2D>(coordSys, 'cartesian2d')) {
+ const xAxis = coordSys.getAxis('x');
+ const yAxis = coordSys.getAxis('y');
+ const dims = coordSys.dimensions;
+ if (isInifinity(data.get(dims[0], idx))) {
+ point[0] = xAxis.toGlobalCoord(xAxis.getExtent()[isFrom ? 0 : 1]);
+ }
+ else if (isInifinity(data.get(dims[1], idx))) {
+ point[1] = yAxis.toGlobalCoord(yAxis.getExtent()[isFrom ? 0 : 1]);
+ }
+ }
+
+ // Use x, y if has any
+ if (!isNaN(xPx)) {
+ point[0] = xPx;
+ }
+ if (!isNaN(yPx)) {
+ point[1] = yPx;
+ }
+ }
+
+ data.setItemLayout(idx, point);
+}
+
+class MarkLineView extends MarkerView {
+
+ static type = 'markLine';
+ type = MarkLineView.type;
+
+ markerGroupMap: HashMap<LineDraw>;
+
+ updateTransform(markLineModel: MarkLineModel, ecModel: GlobalModel, api: ExtensionAPI) {
+ ecModel.eachSeries(function (seriesModel) {
+ const mlModel = MarkerModel.getMarkerModelFromSeries(seriesModel, 'markLine') as MarkLineModel;
+ if (mlModel) {
+ const mlData = mlModel.getData();
+ const fromData = inner(mlModel).from;
+ const toData = inner(mlModel).to;
+ // Update visual and layout of from symbol and to symbol
+ fromData.each(function (idx) {
+ updateSingleMarkerEndLayout(fromData, idx, true, seriesModel, api);
+ updateSingleMarkerEndLayout(toData, idx, false, seriesModel, api);
+ });
+ // Update layout of line
+ mlData.each(function (idx) {
+ mlData.setItemLayout(idx, [
+ fromData.getItemLayout(idx),
+ toData.getItemLayout(idx)
+ ]);
+ });
+
+ this.markerGroupMap.get(seriesModel.id).updateLayout();
+
+ }
+ }, this);
+ }
+
+ renderSeries(
+ seriesModel: SeriesModel,
+ mlModel: MarkLineModel,
+ ecModel: GlobalModel,
+ api: ExtensionAPI
+ ) {
+ const coordSys = seriesModel.coordinateSystem;
+ const seriesId = seriesModel.id;
+ const seriesData = seriesModel.getData();
+
+ const lineDrawMap = this.markerGroupMap;
+ const lineDraw = lineDrawMap.get(seriesId)
+ || lineDrawMap.set(seriesId, new LineDraw());
+ this.group.add(lineDraw.group);
+
+ const mlData = createList(coordSys, seriesModel, mlModel);
+
+ const fromData = mlData.from;
+ const toData = mlData.to;
+ const lineData = mlData.line as List<MarkLineModel, LineDataVisual>;
+
+ inner(mlModel).from = fromData;
+ inner(mlModel).to = toData;
+ // Line data for tooltip and formatter
+ mlModel.setData(lineData);
+
+ let symbolType = mlModel.get('symbol');
+ let symbolSize = mlModel.get('symbolSize');
+ if (!isArray(symbolType)) {
+ symbolType = [symbolType, symbolType];
+ }
+ if (!isArray(symbolSize)) {
+ symbolSize = [symbolSize, symbolSize];
+ }
+
+ // Update visual and layout of from symbol and to symbol
+ mlData.from.each(function (idx) {
+ updateDataVisualAndLayout(fromData, idx, true);
+ updateDataVisualAndLayout(toData, idx, false);
+ });
+
+ // Update visual and layout of line
+ lineData.each(function (idx) {
+ const lineStyle = lineData.getItemModel<MarkLineMergedItemOption>(idx)
+ .getModel('lineStyle').getLineStyle();
+ // lineData.setItemVisual(idx, {
+ // color: lineColor || fromData.getItemVisual(idx, 'color')
+ // });
+ lineData.setItemLayout(idx, [
+ fromData.getItemLayout(idx),
+ toData.getItemLayout(idx)
+ ]);
+
+ if (lineStyle.stroke == null) {
+ lineStyle.stroke = fromData.getItemVisual(idx, 'style').fill;
+ }
+
+ lineData.setItemVisual(idx, {
++ fromSymbolRotate: fromData.getItemVisual(idx, 'symbolRotate'),
+ fromSymbolSize: fromData.getItemVisual(idx, 'symbolSize') as number,
+ fromSymbol: fromData.getItemVisual(idx, 'symbol'),
++ toSymbolRotate: toData.getItemVisual(idx, 'symbolRotate'),
+ toSymbolSize: toData.getItemVisual(idx, 'symbolSize') as number,
+ toSymbol: toData.getItemVisual(idx, 'symbol'),
+ style: lineStyle
+ });
+ });
+
+ lineDraw.updateData(lineData);
+
+ // Set host model for tooltip
+ // FIXME
+ mlData.line.eachItemGraphicEl(function (el, idx) {
+ el.traverse(function (child) {
+ getECData(child).dataModel = mlModel;
+ });
+ });
+
+ function updateDataVisualAndLayout(
+ data: List<MarkLineModel>,
+ idx: number,
+ isFrom: boolean
+ ) {
+ const itemModel = data.getItemModel<MarkLineMergedItemOption>(idx);
+
+ updateSingleMarkerEndLayout(
+ data, idx, isFrom, seriesModel, api
+ );
+
+ const style = itemModel.getModel('itemStyle').getItemStyle();
+ if (style.fill == null) {
+ style.fill = getVisualFromData(seriesData, 'color') as ColorString;
+ }
+
+ data.setItemVisual(idx, {
++ symbolRotate: itemModel.get('symbolRotate'),
+ symbolSize: itemModel.get('symbolSize') || (symbolSize as number[])[isFrom ? 0 : 1],
+ symbol: itemModel.get('symbol', true) || (symbolType as string[])[isFrom ? 0 : 1],
+ style
+ });
+ }
+
+ this.markKeep(lineDraw);
+
+ lineDraw.group.silent = mlModel.get('silent') || seriesModel.get('silent');
+ }
+}
+
+function createList(coordSys: CoordinateSystem, seriesModel: SeriesModel, mlModel: MarkLineModel) {
+
+ let coordDimsInfos;
+ if (coordSys) {
+ coordDimsInfos = map(coordSys && coordSys.dimensions, function (coordDim) {
+ const info = seriesModel.getData().getDimensionInfo(
+ seriesModel.getData().mapDimension(coordDim)
+ ) || {};
+ // In map series data don't have lng and lat dimension. Fallback to same with coordSys
+ return defaults({name: coordDim}, info);
+ });
+ }
+ else {
+ coordDimsInfos = [{
+ name: 'value',
+ type: 'float'
+ }];
+ }
+
+ const fromData = new List(coordDimsInfos, mlModel);
+ const toData = new List(coordDimsInfos, mlModel);
+ // No dimensions
+ const lineData = new List([], mlModel);
+
+ let optData = map(mlModel.get('data'), curry(
+ markLineTransform, seriesModel, coordSys, mlModel
+ ));
+ if (coordSys) {
+ optData = filter(
+ optData, curry(markLineFilter, coordSys)
+ );
+ }
+ const dimValueGetter = coordSys ? markerHelper.dimValueGetter : function (item: MarkLineMergedItemOption) {
+ return item.value;
+ };
+ fromData.initData(
+ map(optData, function (item) {
+ return item[0];
+ }),
+ null,
+ dimValueGetter
+ );
+ toData.initData(
+ map(optData, function (item) {
+ return item[1];
+ }),
+ null,
+ dimValueGetter
+ );
+ lineData.initData(
+ map(optData, function (item) {
+ return item[2];
+ })
+ );
+ lineData.hasItemOption = true;
+
+ return {
+ from: fromData,
+ to: toData,
+ line: lineData
+ };
+}
+
+ComponentView.registerClass(MarkLineView);
diff --cc src/component/marker/MarkPointView.ts
index 503e855,0000000..db5bc14
mode 100644,000000..100644
--- a/src/component/marker/MarkPointView.ts
+++ b/src/component/marker/MarkPointView.ts
@@@ -1,208 -1,0 +1,213 @@@
+/*
+* 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 SymbolDraw from '../../chart/helper/SymbolDraw';
+import * as numberUtil from '../../util/number';
+import List from '../../data/List';
+import * as markerHelper from './markerHelper';
+import MarkerView from './MarkerView';
+import ComponentView from '../../view/Component';
+import { CoordinateSystem } from '../../coord/CoordinateSystem';
+import SeriesModel from '../../model/Series';
+import MarkPointModel, {MarkPointDataItemOption} from './MarkPointModel';
+import GlobalModel from '../../model/Global';
+import MarkerModel from './MarkerModel';
+import ExtensionAPI from '../../ExtensionAPI';
+import { HashMap, isFunction, map, defaults, filter, curry } from 'zrender/src/core/util';
+import { getECData } from '../../util/innerStore';
+import { getVisualFromData } from '../../visual/helper';
+import { ZRColor } from '../../util/types';
+
+function updateMarkerLayout(
+ mpData: List<MarkPointModel>,
+ seriesModel: SeriesModel,
+ api: ExtensionAPI
+) {
+ const coordSys = seriesModel.coordinateSystem;
+ mpData.each(function (idx: number) {
+ const itemModel = mpData.getItemModel<MarkPointDataItemOption>(idx);
+ let point;
+ const xPx = numberUtil.parsePercent(itemModel.get('x'), api.getWidth());
+ const yPx = numberUtil.parsePercent(itemModel.get('y'), api.getHeight());
+ if (!isNaN(xPx) && !isNaN(yPx)) {
+ point = [xPx, yPx];
+ }
+ // Chart like bar may have there own marker positioning logic
+ else if (seriesModel.getMarkerPosition) {
+ // Use the getMarkerPoisition
+ point = seriesModel.getMarkerPosition(
+ mpData.getValues(mpData.dimensions, idx)
+ );
+ }
+ else if (coordSys) {
+ const x = mpData.get(coordSys.dimensions[0], idx);
+ const y = mpData.get(coordSys.dimensions[1], idx);
+ point = coordSys.dataToPoint([x, y]);
+
+ }
+
+ // Use x, y if has any
+ if (!isNaN(xPx)) {
+ point[0] = xPx;
+ }
+ if (!isNaN(yPx)) {
+ point[1] = yPx;
+ }
+
+ mpData.setItemLayout(idx, point);
+ });
+}
+
+class MarkPointView extends MarkerView {
+
+ static type = 'markPoint';
+ type = MarkPointView.type;
+
+ markerGroupMap: HashMap<SymbolDraw>;
+
+ updateTransform(markPointModel: MarkPointModel, ecModel: GlobalModel, api: ExtensionAPI) {
+ ecModel.eachSeries(function (seriesModel) {
+ const mpModel = MarkerModel.getMarkerModelFromSeries(seriesModel, 'markPoint') as MarkPointModel;
+ if (mpModel) {
+ updateMarkerLayout(
+ mpModel.getData(),
+ seriesModel, api
+ );
+ this.markerGroupMap.get(seriesModel.id).updateLayout();
+ }
+ }, this);
+ }
+
+ renderSeries(
+ seriesModel: SeriesModel,
+ mpModel: MarkPointModel,
+ ecModel: GlobalModel,
+ api: ExtensionAPI
+ ) {
+ const coordSys = seriesModel.coordinateSystem;
+ const seriesId = seriesModel.id;
+ const seriesData = seriesModel.getData();
+
+ const symbolDrawMap = this.markerGroupMap;
+ const symbolDraw = symbolDrawMap.get(seriesId)
+ || symbolDrawMap.set(seriesId, new SymbolDraw());
+
+ const mpData = createList(coordSys, seriesModel, mpModel);
+
+ // FIXME
+ mpModel.setData(mpData);
+
+ updateMarkerLayout(mpModel.getData(), seriesModel, api);
+
+ mpData.each(function (idx) {
+ const itemModel = mpData.getItemModel<MarkPointDataItemOption>(idx);
+ let symbol = itemModel.getShallow('symbol');
+ let symbolSize = itemModel.getShallow('symbolSize');
++ let symbolRotate = itemModel.getShallow('symbolRotate');
+
- if (isFunction(symbol) || isFunction(symbolSize)) {
++ if (isFunction(symbol) || isFunction(symbolSize) || isFunction(symbolRotate)) {
+ const rawIdx = mpModel.getRawValue(idx);
+ const dataParams = mpModel.getDataParams(idx);
+ if (isFunction(symbol)) {
+ symbol = symbol(rawIdx, dataParams);
+ }
+ if (isFunction(symbolSize)) {
+ // FIXME 这里不兼容 ECharts 2.x,2.x 貌似参数是整个数据?
+ symbolSize = symbolSize(rawIdx, dataParams);
+ }
++ if (isFunction(symbolRotate)) {
++ symbolRotate = symbolRotate(rawIdx, dataParams);
++ }
+ }
+
+ const style = itemModel.getModel('itemStyle').getItemStyle();
+ const color = getVisualFromData(seriesData, 'color') as ZRColor;
+ if (!style.fill) {
+ style.fill = color;
+ }
+
+ mpData.setItemVisual(idx, {
+ symbol: symbol,
+ symbolSize: symbolSize,
++ symbolRotate: symbolRotate,
+ style
+ });
+ });
+
+ // TODO Text are wrong
+ symbolDraw.updateData(mpData);
+ this.group.add(symbolDraw.group);
+
+ // Set host model for tooltip
+ // FIXME
+ mpData.eachItemGraphicEl(function (el) {
+ el.traverse(function (child) {
+ getECData(child).dataModel = mpModel;
+ });
+ });
+
+ this.markKeep(symbolDraw);
+
+ symbolDraw.group.silent = mpModel.get('silent') || seriesModel.get('silent');
+ }
+}
+
+function createList(
+ coordSys: CoordinateSystem,
+ seriesModel: SeriesModel,
+ mpModel: MarkPointModel
+) {
+ let coordDimsInfos;
+ if (coordSys) {
+ coordDimsInfos = map(coordSys && coordSys.dimensions, function (coordDim) {
+ const info = seriesModel.getData().getDimensionInfo(
+ seriesModel.getData().mapDimension(coordDim)
+ ) || {};
+ // In map series data don't have lng and lat dimension. Fallback to same with coordSys
+ return defaults({name: coordDim}, info);
+ });
+ }
+ else {
+ coordDimsInfos = [{
+ name: 'value',
+ type: 'float'
+ }];
+ }
+
+ const mpData = new List(coordDimsInfos, mpModel);
+ let dataOpt = map(mpModel.get('data'), curry(
+ markerHelper.dataTransform, seriesModel
+ ));
+ if (coordSys) {
+ dataOpt = filter(
+ dataOpt, curry(markerHelper.dataFilter, coordSys)
+ );
+ }
+
+ mpData.initData(dataOpt, null,
+ coordSys ? markerHelper.dimValueGetter : function (item: MarkPointDataItemOption) {
+ return item.value;
+ }
+ );
+
+ return mpData;
+}
+
+ComponentView.registerClass(MarkPointView);
diff --cc src/component/timeline/SliderTimelineView.ts
index b382e5a,0000000..0df2fff
mode 100644,000000..100644
--- a/src/component/timeline/SliderTimelineView.ts
+++ b/src/component/timeline/SliderTimelineView.ts
@@@ -1,893 -1,0 +1,900 @@@
+/*
+* 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 BoundingRect, { RectLike } from 'zrender/src/core/BoundingRect';
+import * as matrix from 'zrender/src/core/matrix';
+import * as graphic from '../../util/graphic';
+import { createTextStyle } from '../../label/labelStyle';
+import * as layout from '../../util/layout';
+import TimelineView from './TimelineView';
+import TimelineAxis from './TimelineAxis';
+import {createSymbol} from '../../util/symbol';
+import * as numberUtil from '../../util/number';
+import GlobalModel from '../../model/Global';
+import ExtensionAPI from '../../ExtensionAPI';
+import { merge, each, extend, clone, isString, bind, defaults, retrieve2 } from 'zrender/src/core/util';
+import SliderTimelineModel from './SliderTimelineModel';
+import ComponentView from '../../view/Component';
+import { LayoutOrient, ZRTextAlign, ZRTextVerticalAlign, ZRElementEvent, ScaleTick } from '../../util/types';
+import TimelineModel, { TimelineDataItemOption, TimelineCheckpointStyle } from './TimelineModel';
+import { TimelineChangePayload, TimelinePlayChangePayload } from './timelineAction';
+import Model from '../../model/Model';
+import { PathProps, PathStyleProps } from 'zrender/src/graphic/Path';
+import Scale from '../../scale/Scale';
+import OrdinalScale from '../../scale/Ordinal';
+import TimeScale from '../../scale/Time';
+import IntervalScale from '../../scale/Interval';
+import { VectorArray } from 'zrender/src/core/vector';
+import { parsePercent } from 'zrender/src/contain/text';
+import { makeInner } from '../../util/model';
+import { getECData } from '../../util/innerStore';
+import { enableHoverEmphasis } from '../../util/states';
+import { createTooltipMarkup } from '../tooltip/tooltipMarkup';
++import Displayable from 'zrender/src/graphic/Displayable';
+
+const PI = Math.PI;
+
+type TimelineSymbol = ReturnType<typeof createSymbol>;
+
+type RenderMethodName = '_renderAxisLine' | '_renderAxisTick' | '_renderControl' | '_renderCurrentPointer';
+
+type ControlName = 'play' | 'stop' | 'next' | 'prev';
+type ControlIconName = 'playIcon' | 'stopIcon' | 'nextIcon' | 'prevIcon';
+
+const labelDataIndexStore = makeInner<{
+ dataIndex: number
+}, graphic.Text>();
+
+interface LayoutInfo {
+ viewRect: BoundingRect
+ mainLength: number
+ orient: LayoutOrient
+
+ rotation: number
+ labelRotation: number
+ labelPosOpt: number | '+' | '-'
+ labelAlign: ZRTextAlign
+ labelBaseline: ZRTextVerticalAlign
+
+ playPosition: number[]
+ prevBtnPosition: number[]
+ nextBtnPosition: number[]
+ axisExtent: number[]
+
+ controlSize: number
+ controlGap: number
+}
+
+class SliderTimelineView extends TimelineView {
+
+ static type = 'timeline.slider';
+ type = SliderTimelineView.type;
+
+ api: ExtensionAPI;
+ model: SliderTimelineModel;
+ ecModel: GlobalModel;
+
+ private _axis: TimelineAxis;
+
+ private _viewRect: BoundingRect;
+
+ private _timer: number;
+
+ private _currentPointer: TimelineSymbol;
+
+ private _progressLine: graphic.Line;
+
+ private _mainGroup: graphic.Group;
+
+ private _labelGroup: graphic.Group;
+
+ private _tickSymbols: graphic.Path[];
+ private _tickLabels: graphic.Text[];
+
+ init(ecModel: GlobalModel, api: ExtensionAPI) {
+ this.api = api;
+ }
+
+ /**
+ * @override
+ */
+ render(timelineModel: SliderTimelineModel, ecModel: GlobalModel, api: ExtensionAPI) {
+ this.model = timelineModel;
+ this.api = api;
+ this.ecModel = ecModel;
+
+ this.group.removeAll();
+
+ if (timelineModel.get('show', true)) {
+
+ const layoutInfo = this._layout(timelineModel, api);
+ const mainGroup = this._createGroup('_mainGroup');
+ const labelGroup = this._createGroup('_labelGroup');
+
+ const axis = this._axis = this._createAxis(layoutInfo, timelineModel);
+
+ timelineModel.formatTooltip = function (dataIndex: number) {
+ const name = axis.scale.getLabel({value: dataIndex});
+ return createTooltipMarkup('nameValue', { noName: true, value: name });
+ };
+
+ each(
+ ['AxisLine', 'AxisTick', 'Control', 'CurrentPointer'] as const,
+ function (name) {
+ this['_render' + name as RenderMethodName](layoutInfo, mainGroup, axis, timelineModel);
+ },
+ this
+ );
+
+ this._renderAxisLabel(layoutInfo, labelGroup, axis, timelineModel);
+ this._position(layoutInfo, timelineModel);
+ }
+
+ this._doPlayStop();
+
+ this._updateTicksStatus();
+ }
+
+ /**
+ * @override
+ */
+ remove() {
+ this._clearTimer();
+ this.group.removeAll();
+ }
+
+ /**
+ * @override
+ */
+ dispose() {
+ this._clearTimer();
+ }
+
+ private _layout(timelineModel: SliderTimelineModel, api: ExtensionAPI): LayoutInfo {
+ const labelPosOpt = timelineModel.get(['label', 'position']);
+ const orient = timelineModel.get('orient');
+ const viewRect = getViewRect(timelineModel, api);
+ let parsedLabelPos: number | '+' | '-';
+ // Auto label offset.
+ if (labelPosOpt == null || labelPosOpt === 'auto') {
+ parsedLabelPos = orient === 'horizontal'
+ ? ((viewRect.y + viewRect.height / 2) < api.getHeight() / 2 ? '-' : '+')
+ : ((viewRect.x + viewRect.width / 2) < api.getWidth() / 2 ? '+' : '-');
+ }
+ else if (isString(labelPosOpt)) {
+ parsedLabelPos = ({
+ horizontal: {top: '-', bottom: '+'},
+ vertical: {left: '-', right: '+'}
+ } as const)[orient][labelPosOpt];
+ }
+ else {
+ // is number
+ parsedLabelPos = labelPosOpt;
+ }
+
+ const labelAlignMap = {
+ horizontal: 'center',
+ vertical: (parsedLabelPos >= 0 || parsedLabelPos === '+') ? 'left' : 'right'
+ };
+
+ const labelBaselineMap = {
+ horizontal: (parsedLabelPos >= 0 || parsedLabelPos === '+') ? 'top' : 'bottom',
+ vertical: 'middle'
+ };
+ const rotationMap = {
+ horizontal: 0,
+ vertical: PI / 2
+ };
+
+ // Position
+ const mainLength = orient === 'vertical' ? viewRect.height : viewRect.width;
+
+ const controlModel = timelineModel.getModel('controlStyle');
+ const showControl = controlModel.get('show', true);
+ const controlSize = showControl ? controlModel.get('itemSize') : 0;
+ const controlGap = showControl ? controlModel.get('itemGap') : 0;
+ const sizePlusGap = controlSize + controlGap;
+
+ // Special label rotate.
+ let labelRotation = timelineModel.get(['label', 'rotate']) || 0;
+ labelRotation = labelRotation * PI / 180; // To radian.
+
+ let playPosition: number[];
+ let prevBtnPosition: number[];
+ let nextBtnPosition: number[];
+ const controlPosition = controlModel.get('position', true);
+ const showPlayBtn = showControl && controlModel.get('showPlayBtn', true);
+ const showPrevBtn = showControl && controlModel.get('showPrevBtn', true);
+ const showNextBtn = showControl && controlModel.get('showNextBtn', true);
+ let xLeft = 0;
+ let xRight = mainLength;
+
+ // position[0] means left, position[1] means middle.
+ if (controlPosition === 'left' || controlPosition === 'bottom') {
+ showPlayBtn && (playPosition = [0, 0], xLeft += sizePlusGap);
+ showPrevBtn && (prevBtnPosition = [xLeft, 0], xLeft += sizePlusGap);
+ showNextBtn && (nextBtnPosition = [xRight - controlSize, 0], xRight -= sizePlusGap);
+ }
+ else { // 'top' 'right'
+ showPlayBtn && (playPosition = [xRight - controlSize, 0], xRight -= sizePlusGap);
+ showPrevBtn && (prevBtnPosition = [0, 0], xLeft += sizePlusGap);
+ showNextBtn && (nextBtnPosition = [xRight - controlSize, 0], xRight -= sizePlusGap);
+ }
+ const axisExtent = [xLeft, xRight];
+
+ if (timelineModel.get('inverse')) {
+ axisExtent.reverse();
+ }
+
+ return {
+ viewRect: viewRect,
+ mainLength: mainLength,
+ orient: orient,
+
+ rotation: rotationMap[orient],
+ labelRotation: labelRotation,
+ labelPosOpt: parsedLabelPos,
+ labelAlign: timelineModel.get(['label', 'align']) || labelAlignMap[orient] as ZRTextAlign,
+ labelBaseline: timelineModel.get(['label', 'verticalAlign'])
+ || timelineModel.get(['label', 'baseline'])
+ || labelBaselineMap[orient] as ZRTextVerticalAlign,
+
+ // Based on mainGroup.
+ playPosition: playPosition,
+ prevBtnPosition: prevBtnPosition,
+ nextBtnPosition: nextBtnPosition,
+ axisExtent: axisExtent,
+
+ controlSize: controlSize,
+ controlGap: controlGap
+ };
+ }
+
+ private _position(layoutInfo: LayoutInfo, timelineModel: SliderTimelineModel) {
+ // Position is be called finally, because bounding rect is needed for
+ // adapt content to fill viewRect (auto adapt offset).
+
+ // Timeline may be not all in the viewRect when 'offset' is specified
+ // as a number, because it is more appropriate that label aligns at
+ // 'offset' but not the other edge defined by viewRect.
+
+ const mainGroup = this._mainGroup;
+ const labelGroup = this._labelGroup;
+
+ let viewRect = layoutInfo.viewRect;
+ if (layoutInfo.orient === 'vertical') {
+ // transform to horizontal, inverse rotate by left-top point.
+ const m = matrix.create();
+ const rotateOriginX = viewRect.x;
+ const rotateOriginY = viewRect.y + viewRect.height;
+ matrix.translate(m, m, [-rotateOriginX, -rotateOriginY]);
+ matrix.rotate(m, m, -PI / 2);
+ matrix.translate(m, m, [rotateOriginX, rotateOriginY]);
+ viewRect = viewRect.clone();
+ viewRect.applyTransform(m);
+ }
+
+ const viewBound = getBound(viewRect);
+ const mainBound = getBound(mainGroup.getBoundingRect());
+ const labelBound = getBound(labelGroup.getBoundingRect());
+
+ const mainPosition = [mainGroup.x, mainGroup.y];
+ const labelsPosition = [labelGroup.x, labelGroup.y];
+
+ labelsPosition[0] = mainPosition[0] = viewBound[0][0];
+
+ const labelPosOpt = layoutInfo.labelPosOpt;
+
+ if (labelPosOpt == null || isString(labelPosOpt)) { // '+' or '-'
+ const mainBoundIdx = labelPosOpt === '+' ? 0 : 1;
+ toBound(mainPosition, mainBound, viewBound, 1, mainBoundIdx);
+ toBound(labelsPosition, labelBound, viewBound, 1, 1 - mainBoundIdx);
+ }
+ else {
+ const mainBoundIdx = labelPosOpt >= 0 ? 0 : 1;
+ toBound(mainPosition, mainBound, viewBound, 1, mainBoundIdx);
+ labelsPosition[1] = mainPosition[1] + labelPosOpt;
+ }
+
+ mainGroup.setPosition(mainPosition);
+ labelGroup.setPosition(labelsPosition);
+ mainGroup.rotation = labelGroup.rotation = layoutInfo.rotation;
+
+ setOrigin(mainGroup);
+ setOrigin(labelGroup);
+
+ function setOrigin(targetGroup: graphic.Group) {
+ targetGroup.originX = viewBound[0][0] - targetGroup.x;
+ targetGroup.originY = viewBound[1][0] - targetGroup.y;
+ }
+
+ function getBound(rect: RectLike) {
+ // [[xmin, xmax], [ymin, ymax]]
+ return [
+ [rect.x, rect.x + rect.width],
+ [rect.y, rect.y + rect.height]
+ ];
+ }
+
+ function toBound(fromPos: VectorArray, from: number[][], to: number[][], dimIdx: number, boundIdx: number) {
+ fromPos[dimIdx] += to[dimIdx][boundIdx] - from[dimIdx][boundIdx];
+ }
+ }
+
+ private _createAxis(layoutInfo: LayoutInfo, timelineModel: SliderTimelineModel) {
+ const data = timelineModel.getData();
+ const axisType = timelineModel.get('axisType');
+
+ const scale = createScaleByModel(timelineModel, axisType);
+
+ // Customize scale. The `tickValue` is `dataIndex`.
+ scale.getTicks = function () {
+ return data.mapArray(['value'], function (value: number) {
+ return {value};
+ });
+ };
+
+ const dataExtent = data.getDataExtent('value');
+ scale.setExtent(dataExtent[0], dataExtent[1]);
+ scale.niceTicks();
+
+ const axis = new TimelineAxis('value', scale, layoutInfo.axisExtent as [number, number], axisType);
+ axis.model = timelineModel;
+
+ return axis;
+ }
+
+ private _createGroup(key: '_mainGroup' | '_labelGroup') {
+ const newGroup = this[key] = new graphic.Group();
+ this.group.add(newGroup);
+ return newGroup;
+ }
+
+ private _renderAxisLine(
+ layoutInfo: LayoutInfo,
+ group: graphic.Group,
+ axis: TimelineAxis,
+ timelineModel: SliderTimelineModel
+ ) {
+ const axisExtent = axis.getExtent();
+
+ if (!timelineModel.get(['lineStyle', 'show'])) {
+ return;
+ }
+
+ const line = new graphic.Line({
+ shape: {
+ x1: axisExtent[0], y1: 0,
+ x2: axisExtent[1], y2: 0
+ },
+ style: extend(
+ {lineCap: 'round'},
+ timelineModel.getModel('lineStyle').getLineStyle()
+ ),
+ silent: true,
+ z2: 1
+ });
+ group.add(line);
+
+ const progressLine = this._progressLine = new graphic.Line({
+ shape: {
+ x1: axisExtent[0],
+ x2: this._currentPointer
+ ? this._currentPointer.x : axisExtent[0],
+ y1: 0, y2: 0
+ },
+ style: defaults(
+ { lineCap: 'round', lineWidth: line.style.lineWidth } as PathStyleProps,
+ timelineModel.getModel(['progress', 'lineStyle']).getLineStyle()
+ ),
+ silent: true,
+ z2: 1
+ });
+ group.add(progressLine);
+ }
+
+ private _renderAxisTick(
+ layoutInfo: LayoutInfo,
+ group: graphic.Group,
+ axis: TimelineAxis,
+ timelineModel: SliderTimelineModel
+ ) {
+ const data = timelineModel.getData();
+ // Show all ticks, despite ignoring strategy.
+ const ticks = axis.scale.getTicks();
+
+ this._tickSymbols = [];
+
+ // The value is dataIndex, see the costomized scale.
+ each(ticks, (tick: ScaleTick) => {
+ const tickCoord = axis.dataToCoord(tick.value);
+ const itemModel = data.getItemModel<TimelineDataItemOption>(tick.value);
+ const itemStyleModel = itemModel.getModel('itemStyle');
+ const hoverStyleModel = itemModel.getModel(['emphasis', 'itemStyle']);
+ const progressStyleModel = itemModel.getModel(['progress', 'itemStyle']);
+
+ const symbolOpt = {
+ position: [tickCoord, 0],
+ onclick: bind(this._changeTimeline, this, tick.value)
+ };
+ const el = giveSymbol(itemModel, itemStyleModel, group, symbolOpt);
+ el.ensureState('emphasis').style = hoverStyleModel.getItemStyle();
+ el.ensureState('progress').style = progressStyleModel.getItemStyle();
+
+ enableHoverEmphasis(el);
+
+ const ecData = getECData(el);
+ if (itemModel.get('tooltip')) {
+ ecData.dataIndex = tick.value;
+ ecData.dataModel = timelineModel;
+ }
+ else {
+ ecData.dataIndex = ecData.dataModel = null;
+ }
+
+ this._tickSymbols.push(el);
+ });
+ }
+
+ private _renderAxisLabel(
+ layoutInfo: LayoutInfo,
+ group: graphic.Group,
+ axis: TimelineAxis,
+ timelineModel: SliderTimelineModel
+ ) {
+ const labelModel = axis.getLabelModel();
+
+ if (!labelModel.get('show')) {
+ return;
+ }
+
+ const data = timelineModel.getData();
+ const labels = axis.getViewLabels();
+
+ this._tickLabels = [];
+
+ each(labels, (labelItem) => {
+ // The tickValue is dataIndex, see the costomized scale.
+ const dataIndex = labelItem.tickValue;
+
+ const itemModel = data.getItemModel<TimelineDataItemOption>(dataIndex);
+ const normalLabelModel = itemModel.getModel('label');
+ const hoverLabelModel = itemModel.getModel(['emphasis', 'label']);
+ const progressLabelModel = itemModel.getModel(['progress', 'label']);
+
+ const tickCoord = axis.dataToCoord(labelItem.tickValue);
+ const textEl = new graphic.Text({
+ x: tickCoord,
+ y: 0,
+ rotation: layoutInfo.labelRotation - layoutInfo.rotation,
+ onclick: bind(this._changeTimeline, this, dataIndex),
+ silent: false,
+ style: createTextStyle(normalLabelModel, {
+ text: labelItem.formattedLabel,
+ align: layoutInfo.labelAlign,
+ verticalAlign: layoutInfo.labelBaseline
+ })
+ });
+
+ textEl.ensureState('emphasis').style = createTextStyle(hoverLabelModel);
+ textEl.ensureState('progress').style = createTextStyle(progressLabelModel);
+
+ group.add(textEl);
+ enableHoverEmphasis(textEl);
+
+ labelDataIndexStore(textEl).dataIndex = dataIndex;
+
+ this._tickLabels.push(textEl);
+
+ });
+ }
+
+ private _renderControl(
+ layoutInfo: LayoutInfo,
+ group: graphic.Group,
+ axis: TimelineAxis,
+ timelineModel: SliderTimelineModel
+ ) {
+ const controlSize = layoutInfo.controlSize;
+ const rotation = layoutInfo.rotation;
+
+ const itemStyle = timelineModel.getModel('controlStyle').getItemStyle();
+ const hoverStyle = timelineModel.getModel(['emphasis', 'controlStyle']).getItemStyle();
+ const playState = timelineModel.getPlayState();
+ const inverse = timelineModel.get('inverse', true);
+
+ makeBtn(
+ layoutInfo.nextBtnPosition,
+ 'next',
+ bind(this._changeTimeline, this, inverse ? '-' : '+')
+ );
+ makeBtn(
+ layoutInfo.prevBtnPosition,
+ 'prev',
+ bind(this._changeTimeline, this, inverse ? '+' : '-')
+ );
+ makeBtn(
+ layoutInfo.playPosition,
+ (playState ? 'stop' : 'play'),
+ bind(this._handlePlayClick, this, !playState),
+ true
+ );
+
+ function makeBtn(
+ position: number[],
+ iconName: ControlName,
+ onclick: () => void,
+ willRotate?: boolean
+ ) {
+ if (!position) {
+ return;
+ }
+ const iconSize = parsePercent(
+ retrieve2(timelineModel.get(['controlStyle', iconName + 'BtnSize' as any]), controlSize),
+ controlSize
+ );
+ const rect = [0, -iconSize / 2, iconSize, iconSize];
+ const opt = {
+ position: position,
+ origin: [controlSize / 2, 0],
+ rotation: willRotate ? -rotation : 0,
+ rectHover: true,
+ style: itemStyle,
+ onclick: onclick
+ };
+ const btn = makeControlIcon(timelineModel, iconName + 'Icon' as ControlIconName, rect, opt);
+ btn.ensureState('emphasis').style = hoverStyle;
+ group.add(btn);
+ enableHoverEmphasis(btn);
+ }
+ }
+
+ private _renderCurrentPointer(
+ layoutInfo: LayoutInfo,
+ group: graphic.Group,
+ axis: TimelineAxis,
+ timelineModel: SliderTimelineModel
+ ) {
+ const data = timelineModel.getData();
+ const currentIndex = timelineModel.getCurrentIndex();
+ const pointerModel = data.getItemModel<TimelineDataItemOption>(currentIndex)
+ .getModel('checkpointStyle');
+ const me = this;
+
+ const callback = {
+ onCreate(pointer: TimelineSymbol) {
+ pointer.draggable = true;
+ pointer.drift = bind(me._handlePointerDrag, me);
+ pointer.ondragend = bind(me._handlePointerDragend, me);
+ pointerMoveTo(pointer, me._progressLine, currentIndex, axis, timelineModel, true);
+ },
+ onUpdate(pointer: TimelineSymbol) {
+ pointerMoveTo(pointer, me._progressLine, currentIndex, axis, timelineModel);
+ }
+ };
+
+ // Reuse when exists, for animation and drag.
+ this._currentPointer = giveSymbol(
+ pointerModel, pointerModel, this._mainGroup, {}, this._currentPointer, callback
+ );
+ }
+
+ private _handlePlayClick(nextState: boolean) {
+ this._clearTimer();
+ this.api.dispatchAction({
+ type: 'timelinePlayChange',
+ playState: nextState,
+ from: this.uid
+ } as TimelinePlayChangePayload);
+ }
+
+ private _handlePointerDrag(dx: number, dy: number, e: ZRElementEvent) {
+ this._clearTimer();
+ this._pointerChangeTimeline([e.offsetX, e.offsetY]);
+ }
+
+ private _handlePointerDragend(e: ZRElementEvent) {
+ this._pointerChangeTimeline([e.offsetX, e.offsetY], true);
+ }
+
+ private _pointerChangeTimeline(mousePos: number[], trigger?: boolean) {
+ let toCoord = this._toAxisCoord(mousePos)[0];
+
+ const axis = this._axis;
+ const axisExtent = numberUtil.asc(axis.getExtent().slice());
+
+ toCoord > axisExtent[1] && (toCoord = axisExtent[1]);
+ toCoord < axisExtent[0] && (toCoord = axisExtent[0]);
+
+ this._currentPointer.x = toCoord;
+ this._currentPointer.markRedraw();
+
+ this._progressLine.shape.x2 = toCoord;
+ this._progressLine.dirty();
+
+ const targetDataIndex = this._findNearestTick(toCoord);
+ const timelineModel = this.model;
+
+ if (trigger || (
+ targetDataIndex !== timelineModel.getCurrentIndex()
+ && timelineModel.get('realtime')
+ )) {
+ this._changeTimeline(targetDataIndex);
+ }
+ }
+
+ private _doPlayStop() {
+ this._clearTimer();
+
+ if (this.model.getPlayState()) {
+ this._timer = setTimeout(
+ () => {
+ // Do not cache
+ const timelineModel = this.model;
+ this._changeTimeline(
+ timelineModel.getCurrentIndex()
+ + (timelineModel.get('rewind', true) ? -1 : 1)
+ );
+ },
+ this.model.get('playInterval')
+ ) as any;
... 10170 lines suppressed ...
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@echarts.apache.org
For additional commands, e-mail: commits-help@echarts.apache.org