ZoeDuan's picture
Upload 1382 files
4bb817b verified
raw
history blame
50.7 kB
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
// FIXME step not support polar
import * as zrUtil from 'zrender/src/core/util';
import SymbolDraw from '../helper/SymbolDraw';
import SymbolClz from '../helper/Symbol';
import lineAnimationDiff from './lineAnimationDiff';
import * as graphic from '../../util/graphic';
import * as modelUtil from '../../util/model';
import { ECPolyline, ECPolygon } from './poly';
import ChartView from '../../view/Chart';
import { prepareDataCoordInfo, getStackedOnPoint } from './helper';
import { createGridClipPath, createPolarClipPath } from '../helper/createClipPathFromCoordSys';
import LineSeriesModel, { LineSeriesOption } from './LineSeries';
import type GlobalModel from '../../model/Global';
import type ExtensionAPI from '../../core/ExtensionAPI';
// TODO
import Cartesian2D from '../../coord/cartesian/Cartesian2D';
import Polar from '../../coord/polar/Polar';
import type SeriesData from '../../data/SeriesData';
import type {
Payload,
Dictionary,
ColorString,
ECElement,
DisplayState,
LabelOption,
ParsedValue
} from '../../util/types';
import type OrdinalScale from '../../scale/Ordinal';
import type Axis2D from '../../coord/cartesian/Axis2D';
import { CoordinateSystemClipArea, isCoordinateSystemType } from '../../coord/CoordinateSystem';
import { setStatesStylesFromModel, setStatesFlag, toggleHoverEmphasis, SPECIAL_STATES } from '../../util/states';
import Model from '../../model/Model';
import { setLabelStyle, getLabelStatesModels, labelInner } from '../../label/labelStyle';
import { getDefaultLabel, getDefaultInterpolatedLabel } from '../helper/labelHelper';
import { getECData } from '../../util/innerStore';
import { createFloat32Array } from '../../util/vendor';
import { convertToColorString } from '../../util/format';
import { lerp } from 'zrender/src/tool/color';
import Element from 'zrender/src/Element';
type PolarArea = ReturnType<Polar['getArea']>;
type Cartesian2DArea = ReturnType<Cartesian2D['getArea']>;
interface SymbolExtended extends SymbolClz {
__temp: boolean
}
interface ColorStop {
offset: number
coord?: number
color: ColorString
}
function isPointsSame(points1: ArrayLike<number>, points2: ArrayLike<number>) {
if (points1.length !== points2.length) {
return;
}
for (let i = 0; i < points1.length; i++) {
if (points1[i] !== points2[i]) {
return;
}
}
return true;
}
function bboxFromPoints(points: ArrayLike<number>) {
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
for (let i = 0; i < points.length;) {
const x = points[i++];
const y = points[i++];
if (!isNaN(x)) {
minX = Math.min(x, minX);
maxX = Math.max(x, maxX);
}
if (!isNaN(y)) {
minY = Math.min(y, minY);
maxY = Math.max(y, maxY);
}
}
return [
[minX, minY],
[maxX, maxY]
];
}
function getBoundingDiff(points1: ArrayLike<number>, points2: ArrayLike<number>): number {
const [min1, max1] = bboxFromPoints(points1);
const [min2, max2] = bboxFromPoints(points2);
// Get a max value from each corner of two boundings.
return Math.max(
Math.abs(min1[0] - min2[0]),
Math.abs(min1[1] - min2[1]),
Math.abs(max1[0] - max2[0]),
Math.abs(max1[1] - max2[1])
);
}
function getSmooth(smooth: number | boolean) {
return zrUtil.isNumber(smooth) ? smooth : (smooth ? 0.5 : 0);
}
function getStackedOnPoints(
coordSys: Cartesian2D | Polar,
data: SeriesData,
dataCoordInfo: ReturnType<typeof prepareDataCoordInfo>
) {
if (!dataCoordInfo.valueDim) {
return [];
}
const len = data.count();
const points = createFloat32Array(len * 2);
for (let idx = 0; idx < len; idx++) {
const pt = getStackedOnPoint(dataCoordInfo, coordSys, data, idx);
points[idx * 2] = pt[0];
points[idx * 2 + 1] = pt[1];
}
return points;
}
function turnPointsIntoStep(
points: ArrayLike<number>,
coordSys: Cartesian2D | Polar,
stepTurnAt: 'start' | 'end' | 'middle',
connectNulls: boolean
): number[] {
const baseAxis = coordSys.getBaseAxis();
const baseIndex = baseAxis.dim === 'x' || baseAxis.dim === 'radius' ? 0 : 1;
const stepPoints: number[] = [];
let i = 0;
const stepPt: number[] = [];
const pt: number[] = [];
const nextPt: number[] = [];
const filteredPoints = [];
if (connectNulls) {
for (i = 0; i < points.length; i += 2) {
if (!isNaN(points[i]) && !isNaN(points[i + 1])) {
filteredPoints.push(points[i], points[i + 1]);
}
}
points = filteredPoints;
}
for (i = 0; i < points.length - 2; i += 2) {
nextPt[0] = points[i + 2];
nextPt[1] = points[i + 3];
pt[0] = points[i];
pt[1] = points[i + 1];
stepPoints.push(pt[0], pt[1]);
switch (stepTurnAt) {
case 'end':
stepPt[baseIndex] = nextPt[baseIndex];
stepPt[1 - baseIndex] = pt[1 - baseIndex];
stepPoints.push(stepPt[0], stepPt[1]);
break;
case 'middle':
const middle = (pt[baseIndex] + nextPt[baseIndex]) / 2;
const stepPt2 = [];
stepPt[baseIndex] = stepPt2[baseIndex] = middle;
stepPt[1 - baseIndex] = pt[1 - baseIndex];
stepPt2[1 - baseIndex] = nextPt[1 - baseIndex];
stepPoints.push(stepPt[0], stepPt[1]);
stepPoints.push(stepPt2[0], stepPt2[1]);
break;
default:
// default is start
stepPt[baseIndex] = pt[baseIndex];
stepPt[1 - baseIndex] = nextPt[1 - baseIndex];
stepPoints.push(stepPt[0], stepPt[1]);
}
}
// Last points
stepPoints.push(points[i++], points[i++]);
return stepPoints;
}
/**
* Clip color stops to edge. Avoid creating too large gradients.
* Which may lead to blurry when GPU acceleration is enabled. See #15680
*
* The stops has been sorted from small to large.
*/
function clipColorStops(colorStops: ColorStop[], maxSize: number): ColorStop[] {
const newColorStops: ColorStop[] = [];
const len = colorStops.length;
// coord will always < 0 in prevOutOfRangeColorStop.
let prevOutOfRangeColorStop: ColorStop;
let prevInRangeColorStop: ColorStop;
function lerpStop(stop0: ColorStop, stop1: ColorStop, clippedCoord: number) {
const coord0 = stop0.coord;
const p = (clippedCoord - coord0) / (stop1.coord - coord0);
const color = lerp(p, [stop0.color, stop1.color]) as string;
return { coord: clippedCoord, color } as ColorStop;
}
for (let i = 0; i < len; i++) {
const stop = colorStops[i];
const coord = stop.coord;
if (coord < 0) {
prevOutOfRangeColorStop = stop;
}
else if (coord > maxSize) {
if (prevInRangeColorStop) {
newColorStops.push(lerpStop(prevInRangeColorStop, stop, maxSize));
}
else if (prevOutOfRangeColorStop) { // If there are two stops and coord range is between these two stops
newColorStops.push(
lerpStop(prevOutOfRangeColorStop, stop, 0),
lerpStop(prevOutOfRangeColorStop, stop, maxSize)
);
}
// All following stop will be out of range. So just ignore them.
break;
}
else {
if (prevOutOfRangeColorStop) {
newColorStops.push(lerpStop(prevOutOfRangeColorStop, stop, 0));
// Reset
prevOutOfRangeColorStop = null;
}
newColorStops.push(stop);
prevInRangeColorStop = stop;
}
}
return newColorStops;
}
function getVisualGradient(
data: SeriesData,
coordSys: Cartesian2D | Polar,
api: ExtensionAPI
) {
const visualMetaList = data.getVisual('visualMeta');
if (!visualMetaList || !visualMetaList.length || !data.count()) {
// When data.count() is 0, gradient range can not be calculated.
return;
}
if (coordSys.type !== 'cartesian2d') {
if (__DEV__) {
console.warn('Visual map on line style is only supported on cartesian2d.');
}
return;
}
let coordDim: 'x' | 'y';
let visualMeta;
for (let i = visualMetaList.length - 1; i >= 0; i--) {
const dimInfo = data.getDimensionInfo(visualMetaList[i].dimension);
coordDim = (dimInfo && dimInfo.coordDim) as 'x' | 'y';
// Can only be x or y
if (coordDim === 'x' || coordDim === 'y') {
visualMeta = visualMetaList[i];
break;
}
}
if (!visualMeta) {
if (__DEV__) {
console.warn('Visual map on line style only support x or y dimension.');
}
return;
}
// If the area to be rendered is bigger than area defined by LinearGradient,
// the canvas spec prescribes that the color of the first stop and the last
// stop should be used. But if two stops are added at offset 0, in effect
// browsers use the color of the second stop to render area outside
// LinearGradient. So we can only infinitesimally extend area defined in
// LinearGradient to render `outerColors`.
const axis = coordSys.getAxis(coordDim);
// dataToCoord mapping may not be linear, but must be monotonic.
const colorStops: ColorStop[] = zrUtil.map(visualMeta.stops, function (stop) {
// offset will be calculated later.
return {
coord: axis.toGlobalCoord(axis.dataToCoord(stop.value)),
color: stop.color
} as ColorStop;
});
const stopLen = colorStops.length;
const outerColors = visualMeta.outerColors.slice();
if (stopLen && colorStops[0].coord > colorStops[stopLen - 1].coord) {
colorStops.reverse();
outerColors.reverse();
}
const colorStopsInRange = clipColorStops(
colorStops, coordDim === 'x' ? api.getWidth() : api.getHeight()
);
const inRangeStopLen = colorStopsInRange.length;
if (!inRangeStopLen && stopLen) {
// All stops are out of range. All will be the same color.
return colorStops[0].coord < 0
? (outerColors[1] ? outerColors[1] : colorStops[stopLen - 1].color)
: (outerColors[0] ? outerColors[0] : colorStops[0].color);
}
const tinyExtent = 10; // Arbitrary value: 10px
const minCoord = colorStopsInRange[0].coord - tinyExtent;
const maxCoord = colorStopsInRange[inRangeStopLen - 1].coord + tinyExtent;
const coordSpan = maxCoord - minCoord;
if (coordSpan < 1e-3) {
return 'transparent';
}
zrUtil.each(colorStopsInRange, function (stop) {
stop.offset = (stop.coord - minCoord) / coordSpan;
});
colorStopsInRange.push({
// NOTE: inRangeStopLen may still be 0 if stoplen is zero.
offset: inRangeStopLen ? colorStopsInRange[inRangeStopLen - 1].offset : 0.5,
color: outerColors[1] || 'transparent'
});
colorStopsInRange.unshift({ // notice newColorStops.length have been changed.
offset: inRangeStopLen ? colorStopsInRange[0].offset : 0.5,
color: outerColors[0] || 'transparent'
});
const gradient = new graphic.LinearGradient(0, 0, 0, 0, colorStopsInRange, true);
gradient[coordDim] = minCoord;
gradient[coordDim + '2' as 'x2' | 'y2'] = maxCoord;
return gradient;
}
function getIsIgnoreFunc(
seriesModel: LineSeriesModel,
data: SeriesData,
coordSys: Cartesian2D
) {
const showAllSymbol = seriesModel.get('showAllSymbol');
const isAuto = showAllSymbol === 'auto';
if (showAllSymbol && !isAuto) {
return;
}
const categoryAxis = coordSys.getAxesByScale('ordinal')[0];
if (!categoryAxis) {
return;
}
// Note that category label interval strategy might bring some weird effect
// in some scenario: users may wonder why some of the symbols are not
// displayed. So we show all symbols as possible as we can.
if (isAuto
// Simplify the logic, do not determine label overlap here.
&& canShowAllSymbolForCategory(categoryAxis, data)
) {
return;
}
// Otherwise follow the label interval strategy on category axis.
const categoryDataDim = data.mapDimension(categoryAxis.dim);
const labelMap: Dictionary<1> = {};
zrUtil.each(categoryAxis.getViewLabels(), function (labelItem) {
const ordinalNumber = (categoryAxis.scale as OrdinalScale)
.getRawOrdinalNumber(labelItem.tickValue);
labelMap[ordinalNumber] = 1;
});
return function (dataIndex: number) {
return !labelMap.hasOwnProperty(data.get(categoryDataDim, dataIndex));
};
}
function canShowAllSymbolForCategory(
categoryAxis: Axis2D,
data: SeriesData
) {
// In most cases, line is monotonous on category axis, and the label size
// is close with each other. So we check the symbol size and some of the
// label size alone with the category axis to estimate whether all symbol
// can be shown without overlap.
const axisExtent = categoryAxis.getExtent();
let availSize = Math.abs(axisExtent[1] - axisExtent[0]) / (categoryAxis.scale as OrdinalScale).count();
isNaN(availSize) && (availSize = 0); // 0/0 is NaN.
// Sampling some points, max 5.
const dataLen = data.count();
const step = Math.max(1, Math.round(dataLen / 5));
for (let dataIndex = 0; dataIndex < dataLen; dataIndex += step) {
if (SymbolClz.getSymbolSize(
data, dataIndex
// Only for cartesian, where `isHorizontal` exists.
)[categoryAxis.isHorizontal() ? 1 : 0]
// Empirical number
* 1.5 > availSize
) {
return false;
}
}
return true;
}
function isPointNull(x: number, y: number) {
return isNaN(x) || isNaN(y);
}
function getLastIndexNotNull(points: ArrayLike<number>) {
let len = points.length / 2;
for (; len > 0; len--) {
if (!isPointNull(points[len * 2 - 2], points[len * 2 - 1])) {
break;
}
}
return len - 1;
}
function getPointAtIndex(points: ArrayLike<number>, idx: number) {
return [points[idx * 2], points[idx * 2 + 1]];
}
function getIndexRange(points: ArrayLike<number>, xOrY: number, dim: 'x' | 'y') {
const len = points.length / 2;
const dimIdx = dim === 'x' ? 0 : 1;
let a;
let b;
let prevIndex = 0;
let nextIndex = -1;
for (let i = 0; i < len; i++) {
b = points[i * 2 + dimIdx];
if (isNaN(b) || isNaN(points[i * 2 + 1 - dimIdx])) {
continue;
}
if (i === 0) {
a = b;
continue;
}
if (a <= xOrY && b >= xOrY || a >= xOrY && b <= xOrY) {
nextIndex = i;
break;
}
prevIndex = i;
a = b;
}
return {
range: [prevIndex, nextIndex],
t: (xOrY - a) / (b - a)
};
}
function anyStateShowEndLabel(
seriesModel: LineSeriesModel
) {
if (seriesModel.get(['endLabel', 'show'])) {
return true;
}
for (let i = 0; i < SPECIAL_STATES.length; i++) {
if (seriesModel.get([SPECIAL_STATES[i], 'endLabel', 'show'])) {
return true;
}
}
return false;
}
interface EndLabelAnimationRecord {
lastFrameIndex: number
originalX?: number
originalY?: number
}
function createLineClipPath(
lineView: LineView,
coordSys: Cartesian2D | Polar,
hasAnimation: boolean,
seriesModel: LineSeriesModel
) {
if (isCoordinateSystemType<Cartesian2D>(coordSys, 'cartesian2d')) {
const endLabelModel = seriesModel.getModel('endLabel');
const valueAnimation = endLabelModel.get('valueAnimation');
const data = seriesModel.getData();
const labelAnimationRecord: EndLabelAnimationRecord = { lastFrameIndex: 0 };
const during = anyStateShowEndLabel(seriesModel)
? (percent: number, clipRect: graphic.Rect) => {
lineView._endLabelOnDuring(
percent,
clipRect,
data,
labelAnimationRecord,
valueAnimation,
endLabelModel,
coordSys
);
}
: null;
const isHorizontal = coordSys.getBaseAxis().isHorizontal();
const clipPath = createGridClipPath(coordSys, hasAnimation, seriesModel, () => {
const endLabel = lineView._endLabel;
if (endLabel && hasAnimation) {
if (labelAnimationRecord.originalX != null) {
endLabel.attr({
x: labelAnimationRecord.originalX,
y: labelAnimationRecord.originalY
});
}
}
}, during);
// Expand clip shape to avoid clipping when line value exceeds axis
if (!seriesModel.get('clip', true)) {
const rectShape = clipPath.shape;
const expandSize = Math.max(rectShape.width, rectShape.height);
if (isHorizontal) {
rectShape.y -= expandSize;
rectShape.height += expandSize * 2;
}
else {
rectShape.x -= expandSize;
rectShape.width += expandSize * 2;
}
}
// Set to the final frame. To make sure label layout is right.
if (during) {
during(1, clipPath);
}
return clipPath;
}
else {
if (__DEV__) {
if (seriesModel.get(['endLabel', 'show'])) {
console.warn('endLabel is not supported for lines in polar systems.');
}
}
return createPolarClipPath(coordSys, hasAnimation, seriesModel);
}
}
function getEndLabelStateSpecified(endLabelModel: Model, coordSys: Cartesian2D) {
const baseAxis = coordSys.getBaseAxis();
const isHorizontal = baseAxis.isHorizontal();
const isBaseInversed = baseAxis.inverse;
const align = isHorizontal
? (isBaseInversed ? 'right' : 'left')
: 'center';
const verticalAlign = isHorizontal
? 'middle'
: (isBaseInversed ? 'top' : 'bottom');
return {
normal: {
align: endLabelModel.get('align') || align,
verticalAlign: endLabelModel.get('verticalAlign') || verticalAlign
}
};
}
class LineView extends ChartView {
static readonly type = 'line';
_symbolDraw: SymbolDraw;
_lineGroup: graphic.Group;
_coordSys: Cartesian2D | Polar;
_endLabel: graphic.Text;
_polyline: ECPolyline;
_polygon: ECPolygon;
_stackedOnPoints: ArrayLike<number>;
_points: ArrayLike<number>;
_step: LineSeriesOption['step'];
_valueOrigin: LineSeriesOption['areaStyle']['origin'];
_clipShapeForSymbol: CoordinateSystemClipArea;
_data: SeriesData;
init() {
const lineGroup = new graphic.Group();
const symbolDraw = new SymbolDraw();
this.group.add(symbolDraw.group);
this._symbolDraw = symbolDraw;
this._lineGroup = lineGroup;
}
render(seriesModel: LineSeriesModel, ecModel: GlobalModel, api: ExtensionAPI) {
const coordSys = seriesModel.coordinateSystem;
const group = this.group;
const data = seriesModel.getData();
const lineStyleModel = seriesModel.getModel('lineStyle');
const areaStyleModel = seriesModel.getModel('areaStyle');
let points = data.getLayout('points') as number[] || [];
const isCoordSysPolar = coordSys.type === 'polar';
const prevCoordSys = this._coordSys;
const symbolDraw = this._symbolDraw;
let polyline = this._polyline;
let polygon = this._polygon;
const lineGroup = this._lineGroup;
const hasAnimation = !ecModel.ssr && seriesModel.get('animation');
const isAreaChart = !areaStyleModel.isEmpty();
const valueOrigin = areaStyleModel.get('origin');
const dataCoordInfo = prepareDataCoordInfo(coordSys, data, valueOrigin);
let stackedOnPoints = isAreaChart && getStackedOnPoints(coordSys, data, dataCoordInfo);
const showSymbol = seriesModel.get('showSymbol');
const connectNulls = seriesModel.get('connectNulls');
const isIgnoreFunc = showSymbol && !isCoordSysPolar
&& getIsIgnoreFunc(seriesModel, data, coordSys as Cartesian2D);
// Remove temporary symbols
const oldData = this._data;
oldData && oldData.eachItemGraphicEl(function (el: SymbolExtended, idx) {
if (el.__temp) {
group.remove(el);
oldData.setItemGraphicEl(idx, null);
}
});
// Remove previous created symbols if showSymbol changed to false
if (!showSymbol) {
symbolDraw.remove();
}
group.add(lineGroup);
// FIXME step not support polar
const step = !isCoordSysPolar ? seriesModel.get('step') : false;
let clipShapeForSymbol: PolarArea | Cartesian2DArea;
if (coordSys && coordSys.getArea && seriesModel.get('clip', true)) {
clipShapeForSymbol = coordSys.getArea();
// Avoid float number rounding error for symbol on the edge of axis extent.
// See #7913 and `test/dataZoom-clip.html`.
if ((clipShapeForSymbol as Cartesian2DArea).width != null) {
(clipShapeForSymbol as Cartesian2DArea).x -= 0.1;
(clipShapeForSymbol as Cartesian2DArea).y -= 0.1;
(clipShapeForSymbol as Cartesian2DArea).width += 0.2;
(clipShapeForSymbol as Cartesian2DArea).height += 0.2;
}
else if ((clipShapeForSymbol as PolarArea).r0) {
(clipShapeForSymbol as PolarArea).r0 -= 0.5;
(clipShapeForSymbol as PolarArea).r += 0.5;
}
}
this._clipShapeForSymbol = clipShapeForSymbol;
const visualColor = getVisualGradient(data, coordSys, api)
|| data.getVisual('style')[data.getVisual('drawType')];
// Initialization animation or coordinate system changed
if (
!(polyline && prevCoordSys.type === coordSys.type && step === this._step)
) {
showSymbol && symbolDraw.updateData(data, {
isIgnore: isIgnoreFunc,
clipShape: clipShapeForSymbol,
disableAnimation: true,
getSymbolPoint(idx) {
return [points[idx * 2], points[idx * 2 + 1]];
}
});
hasAnimation && this._initSymbolLabelAnimation(
data,
coordSys,
clipShapeForSymbol
);
if (step) {
// TODO If stacked series is not step
points = turnPointsIntoStep(points, coordSys, step, connectNulls);
if (stackedOnPoints) {
stackedOnPoints = turnPointsIntoStep(stackedOnPoints, coordSys, step, connectNulls);
}
}
polyline = this._newPolyline(points);
if (isAreaChart) {
polygon = this._newPolygon(
points, stackedOnPoints
);
}// If areaStyle is removed
else if (polygon) {
lineGroup.remove(polygon);
polygon = this._polygon = null;
}
// NOTE: Must update _endLabel before setClipPath.
if (!isCoordSysPolar) {
this._initOrUpdateEndLabel(seriesModel, coordSys as Cartesian2D, convertToColorString(visualColor));
}
lineGroup.setClipPath(
createLineClipPath(this, coordSys, true, seriesModel)
);
}
else {
if (isAreaChart && !polygon) {
// If areaStyle is added
polygon = this._newPolygon(
points, stackedOnPoints
);
}
else if (polygon && !isAreaChart) {
// If areaStyle is removed
lineGroup.remove(polygon);
polygon = this._polygon = null;
}
// NOTE: Must update _endLabel before setClipPath.
if (!isCoordSysPolar) {
this._initOrUpdateEndLabel(seriesModel, coordSys as Cartesian2D, convertToColorString(visualColor));
}
// Update clipPath
const oldClipPath = lineGroup.getClipPath();
if (oldClipPath) {
const newClipPath = createLineClipPath(this, coordSys, false, seriesModel);
graphic.initProps(oldClipPath, {
shape: newClipPath.shape
}, seriesModel);
}
else {
lineGroup.setClipPath(
createLineClipPath(this, coordSys, true, seriesModel)
);
}
// Always update, or it is wrong in the case turning on legend
// because points are not changed.
showSymbol && symbolDraw.updateData(data, {
isIgnore: isIgnoreFunc,
clipShape: clipShapeForSymbol,
disableAnimation: true,
getSymbolPoint(idx) {
return [points[idx * 2], points[idx * 2 + 1]];
}
});
// In the case data zoom triggered refreshing frequently
// Data may not change if line has a category axis. So it should animate nothing.
if (!isPointsSame(this._stackedOnPoints, stackedOnPoints)
|| !isPointsSame(this._points, points)
) {
if (hasAnimation) {
this._doUpdateAnimation(
data, stackedOnPoints, coordSys, api, step, valueOrigin, connectNulls
);
}
else {
// Not do it in update with animation
if (step) {
// TODO If stacked series is not step
points = turnPointsIntoStep(points, coordSys, step, connectNulls);
if (stackedOnPoints) {
stackedOnPoints = turnPointsIntoStep(stackedOnPoints, coordSys, step, connectNulls);
}
}
polyline.setShape({
points: points
});
polygon && polygon.setShape({
points: points,
stackedOnPoints: stackedOnPoints
});
}
}
}
const emphasisModel = seriesModel.getModel('emphasis');
const focus = emphasisModel.get('focus');
const blurScope = emphasisModel.get('blurScope');
const emphasisDisabled = emphasisModel.get('disabled');
polyline.useStyle(zrUtil.defaults(
// Use color in lineStyle first
lineStyleModel.getLineStyle(),
{
fill: 'none',
stroke: visualColor,
lineJoin: 'bevel' as CanvasLineJoin
}
));
setStatesStylesFromModel(polyline, seriesModel, 'lineStyle');
if (polyline.style.lineWidth > 0 && seriesModel.get(['emphasis', 'lineStyle', 'width']) === 'bolder') {
const emphasisLineStyle = polyline.getState('emphasis').style;
emphasisLineStyle.lineWidth = +polyline.style.lineWidth + 1;
}
// Needs seriesIndex for focus
getECData(polyline).seriesIndex = seriesModel.seriesIndex;
toggleHoverEmphasis(polyline, focus, blurScope, emphasisDisabled);
const smooth = getSmooth(seriesModel.get('smooth'));
const smoothMonotone = seriesModel.get('smoothMonotone');
polyline.setShape({
smooth,
smoothMonotone,
connectNulls
});
if (polygon) {
const stackedOnSeries = data.getCalculationInfo('stackedOnSeries');
let stackedOnSmooth = 0;
polygon.useStyle(zrUtil.defaults(
areaStyleModel.getAreaStyle(),
{
fill: visualColor,
opacity: 0.7,
lineJoin: 'bevel' as CanvasLineJoin,
decal: data.getVisual('style').decal
}
));
if (stackedOnSeries) {
stackedOnSmooth = getSmooth(stackedOnSeries.get('smooth'));
}
polygon.setShape({
smooth,
stackedOnSmooth,
smoothMonotone,
connectNulls
});
setStatesStylesFromModel(polygon, seriesModel, 'areaStyle');
// Needs seriesIndex for focus
getECData(polygon).seriesIndex = seriesModel.seriesIndex;
toggleHoverEmphasis(polygon, focus, blurScope, emphasisDisabled);
}
const changePolyState = (toState: DisplayState) => {
this._changePolyState(toState);
};
data.eachItemGraphicEl(function (el) {
// Switch polyline / polygon state if element changed its state.
el && ((el as ECElement).onHoverStateChange = changePolyState);
});
(this._polyline as ECElement).onHoverStateChange = changePolyState;
this._data = data;
// Save the coordinate system for transition animation when data changed
this._coordSys = coordSys;
this._stackedOnPoints = stackedOnPoints;
this._points = points;
this._step = step;
this._valueOrigin = valueOrigin;
if (seriesModel.get('triggerLineEvent')) {
this.packEventData(seriesModel, polyline);
polygon && this.packEventData(seriesModel, polygon);
}
}
private packEventData(seriesModel: LineSeriesModel, el: Element) {
getECData(el).eventData = {
componentType: 'series',
componentSubType: 'line',
componentIndex: seriesModel.componentIndex,
seriesIndex: seriesModel.seriesIndex,
seriesName: seriesModel.name,
seriesType: 'line'
};
}
highlight(
seriesModel: LineSeriesModel,
ecModel: GlobalModel,
api: ExtensionAPI,
payload: Payload
) {
const data = seriesModel.getData();
const dataIndex = modelUtil.queryDataIndex(data, payload);
this._changePolyState('emphasis');
if (!(dataIndex instanceof Array) && dataIndex != null && dataIndex >= 0) {
const points = data.getLayout('points');
let symbol = data.getItemGraphicEl(dataIndex) as SymbolClz;
if (!symbol) {
// Create a temporary symbol if it is not exists
const x = points[dataIndex * 2];
const y = points[dataIndex * 2 + 1];
if (isNaN(x) || isNaN(y)) {
// Null data
return;
}
// fix #11360: shouldn't draw symbol outside clipShapeForSymbol
if (this._clipShapeForSymbol && !this._clipShapeForSymbol.contain(x, y)) {
return;
}
const zlevel = seriesModel.get('zlevel') || 0;
const z = seriesModel.get('z') || 0;
symbol = new SymbolClz(data, dataIndex);
symbol.x = x;
symbol.y = y;
symbol.setZ(zlevel, z);
// ensure label text of the temporary symbol is in front of line and area polygon
const symbolLabel = symbol.getSymbolPath().getTextContent();
if (symbolLabel) {
symbolLabel.zlevel = zlevel;
symbolLabel.z = z;
symbolLabel.z2 = this._polyline.z2 + 1;
}
(symbol as SymbolExtended).__temp = true;
data.setItemGraphicEl(dataIndex, symbol);
// Stop scale animation
symbol.stopSymbolAnimation(true);
this.group.add(symbol);
}
symbol.highlight();
}
else {
// Highlight whole series
ChartView.prototype.highlight.call(
this, seriesModel, ecModel, api, payload
);
}
}
downplay(
seriesModel: LineSeriesModel,
ecModel: GlobalModel,
api: ExtensionAPI,
payload: Payload
) {
const data = seriesModel.getData();
const dataIndex = modelUtil.queryDataIndex(data, payload) as number;
this._changePolyState('normal');
if (dataIndex != null && dataIndex >= 0) {
const symbol = data.getItemGraphicEl(dataIndex) as SymbolExtended;
if (symbol) {
if (symbol.__temp) {
data.setItemGraphicEl(dataIndex, null);
this.group.remove(symbol);
}
else {
symbol.downplay();
}
}
}
else {
// FIXME
// can not downplay completely.
// Downplay whole series
ChartView.prototype.downplay.call(
this, seriesModel, ecModel, api, payload
);
}
}
_changePolyState(toState: DisplayState) {
const polygon = this._polygon;
setStatesFlag(this._polyline, toState);
polygon && setStatesFlag(polygon, toState);
}
_newPolyline(points: ArrayLike<number>) {
let polyline = this._polyline;
// Remove previous created polyline
if (polyline) {
this._lineGroup.remove(polyline);
}
polyline = new ECPolyline({
shape: {
points
},
segmentIgnoreThreshold: 2,
z2: 10
});
this._lineGroup.add(polyline);
this._polyline = polyline;
return polyline;
}
_newPolygon(points: ArrayLike<number>, stackedOnPoints: ArrayLike<number>) {
let polygon = this._polygon;
// Remove previous created polygon
if (polygon) {
this._lineGroup.remove(polygon);
}
polygon = new ECPolygon({
shape: {
points,
stackedOnPoints: stackedOnPoints
},
segmentIgnoreThreshold: 2
});
this._lineGroup.add(polygon);
this._polygon = polygon;
return polygon;
}
_initSymbolLabelAnimation(
data: SeriesData,
coordSys: Polar | Cartesian2D,
clipShape: PolarArea | Cartesian2DArea
) {
let isHorizontalOrRadial: boolean;
let isCoordSysPolar: boolean;
const baseAxis = coordSys.getBaseAxis();
const isAxisInverse = baseAxis.inverse;
if (coordSys.type === 'cartesian2d') {
isHorizontalOrRadial = (baseAxis as Axis2D).isHorizontal();
isCoordSysPolar = false;
}
else if (coordSys.type === 'polar') {
isHorizontalOrRadial = baseAxis.dim === 'angle';
isCoordSysPolar = true;
}
const seriesModel = data.hostModel;
let seriesDuration = seriesModel.get('animationDuration');
if (zrUtil.isFunction(seriesDuration)) {
seriesDuration = seriesDuration(null);
}
const seriesDelay = seriesModel.get('animationDelay') || 0;
const seriesDelayValue = zrUtil.isFunction(seriesDelay)
? seriesDelay(null)
: seriesDelay;
data.eachItemGraphicEl(function (symbol: SymbolExtended, idx) {
const el = symbol;
if (el) {
const point = [symbol.x, symbol.y];
let start;
let end;
let current;
if (clipShape) {
if (isCoordSysPolar) {
const polarClip = clipShape as PolarArea;
const coord = (coordSys as Polar).pointToCoord(point);
if (isHorizontalOrRadial) {
start = polarClip.startAngle;
end = polarClip.endAngle;
current = -coord[1] / 180 * Math.PI;
}
else {
start = polarClip.r0;
end = polarClip.r;
current = coord[0];
}
}
else {
const gridClip = clipShape as Cartesian2DArea;
if (isHorizontalOrRadial) {
start = gridClip.x;
end = gridClip.x + gridClip.width;
current = symbol.x;
}
else {
start = gridClip.y + gridClip.height;
end = gridClip.y;
current = symbol.y;
}
}
}
let ratio = end === start ? 0 : (current - start) / (end - start);
if (isAxisInverse) {
ratio = 1 - ratio;
}
const delay = zrUtil.isFunction(seriesDelay) ? seriesDelay(idx)
: (seriesDuration * ratio) + seriesDelayValue;
const symbolPath = el.getSymbolPath();
const text = symbolPath.getTextContent();
el.attr({ scaleX: 0, scaleY: 0 });
el.animateTo({
scaleX: 1,
scaleY: 1
}, {
duration: 200,
setToFinal: true,
delay: delay
});
if (text) {
text.animateFrom({
style: {
opacity: 0
}
}, {
duration: 300,
delay: delay
});
}
(symbolPath as ECElement).disableLabelAnimation = true;
}
});
}
_initOrUpdateEndLabel(
seriesModel: LineSeriesModel,
coordSys: Cartesian2D,
inheritColor: string
) {
const endLabelModel = seriesModel.getModel('endLabel');
if (anyStateShowEndLabel(seriesModel)) {
const data = seriesModel.getData();
const polyline = this._polyline;
// series may be filtered.
const points = data.getLayout('points');
if (!points) {
polyline.removeTextContent();
this._endLabel = null;
return;
}
let endLabel = this._endLabel;
if (!endLabel) {
endLabel = this._endLabel = new graphic.Text({
z2: 200 // should be higher than item symbol
});
endLabel.ignoreClip = true;
polyline.setTextContent(this._endLabel);
(polyline as ECElement).disableLabelAnimation = true;
}
// Find last non-NaN data to display data
const dataIndex = getLastIndexNotNull(points);
if (dataIndex >= 0) {
setLabelStyle(
polyline,
getLabelStatesModels(seriesModel, 'endLabel'),
{
inheritColor,
labelFetcher: seriesModel,
labelDataIndex: dataIndex,
defaultText(dataIndex, opt, interpolatedValue) {
return interpolatedValue != null
? getDefaultInterpolatedLabel(data, interpolatedValue)
: getDefaultLabel(data, dataIndex);
},
enableTextSetter: true
},
getEndLabelStateSpecified(endLabelModel, coordSys)
);
polyline.textConfig.position = null;
}
}
else if (this._endLabel) {
this._polyline.removeTextContent();
this._endLabel = null;
}
}
_endLabelOnDuring(
percent: number,
clipRect: graphic.Rect,
data: SeriesData,
animationRecord: EndLabelAnimationRecord,
valueAnimation: boolean,
endLabelModel: Model<LabelOption>,
coordSys: Cartesian2D
) {
const endLabel = this._endLabel;
const polyline = this._polyline;
if (endLabel) {
// NOTE: Don't remove percent < 1. percent === 1 means the first frame during render.
// The label is not prepared at this time.
if (percent < 1 && animationRecord.originalX == null) {
animationRecord.originalX = endLabel.x;
animationRecord.originalY = endLabel.y;
}
const points = data.getLayout('points');
const seriesModel = data.hostModel as LineSeriesModel;
const connectNulls = seriesModel.get('connectNulls');
const precision = endLabelModel.get('precision');
const distance = endLabelModel.get('distance') || 0;
const baseAxis = coordSys.getBaseAxis();
const isHorizontal = baseAxis.isHorizontal();
const isBaseInversed = baseAxis.inverse;
const clipShape = clipRect.shape;
const xOrY = isBaseInversed
? isHorizontal ? clipShape.x : (clipShape.y + clipShape.height)
: isHorizontal ? (clipShape.x + clipShape.width) : clipShape.y;
const distanceX = (isHorizontal ? distance : 0) * (isBaseInversed ? -1 : 1);
const distanceY = (isHorizontal ? 0 : -distance) * (isBaseInversed ? -1 : 1);
const dim = isHorizontal ? 'x' : 'y';
const dataIndexRange = getIndexRange(points, xOrY, dim);
const indices = dataIndexRange.range;
const diff = indices[1] - indices[0];
let value: ParsedValue;
if (diff >= 1) {
// diff > 1 && connectNulls, which is on the null data.
if (diff > 1 && !connectNulls) {
const pt = getPointAtIndex(points, indices[0]);
endLabel.attr({
x: pt[0] + distanceX,
y: pt[1] + distanceY
});
valueAnimation && (value = seriesModel.getRawValue(indices[0]) as ParsedValue);
}
else {
const pt = polyline.getPointOn(xOrY, dim);
pt && endLabel.attr({
x: pt[0] + distanceX,
y: pt[1] + distanceY
});
const startValue = seriesModel.getRawValue(indices[0]) as ParsedValue;
const endValue = seriesModel.getRawValue(indices[1]) as ParsedValue;
valueAnimation && (value = modelUtil.interpolateRawValues(
data, precision, startValue, endValue, dataIndexRange.t
) as ParsedValue);
}
animationRecord.lastFrameIndex = indices[0];
}
else {
// If diff <= 0, which is the range is not found(Include NaN)
// Choose the first point or last point.
const idx = (percent === 1 || animationRecord.lastFrameIndex > 0) ? indices[0] : 0;
const pt = getPointAtIndex(points, idx);
valueAnimation && (value = seriesModel.getRawValue(idx) as ParsedValue);
endLabel.attr({
x: pt[0] + distanceX,
y: pt[1] + distanceY
});
}
if (valueAnimation) {
const inner = labelInner(endLabel);
if (typeof inner.setLabelText === 'function') {
inner.setLabelText(value);
}
}
}
}
/**
* @private
*/
// FIXME Two value axis
_doUpdateAnimation(
data: SeriesData,
stackedOnPoints: ArrayLike<number>,
coordSys: Cartesian2D | Polar,
api: ExtensionAPI,
step: LineSeriesOption['step'],
valueOrigin: LineSeriesOption['areaStyle']['origin'],
connectNulls: boolean
) {
const polyline = this._polyline;
const polygon = this._polygon;
const seriesModel = data.hostModel;
const diff = lineAnimationDiff(
this._data, data,
this._stackedOnPoints, stackedOnPoints,
this._coordSys, coordSys,
this._valueOrigin, valueOrigin
);
let current = diff.current;
let stackedOnCurrent = diff.stackedOnCurrent;
let next = diff.next;
let stackedOnNext = diff.stackedOnNext;
if (step) {
// TODO If stacked series is not step
current = turnPointsIntoStep(diff.current, coordSys, step, connectNulls);
stackedOnCurrent = turnPointsIntoStep(diff.stackedOnCurrent, coordSys, step, connectNulls);
next = turnPointsIntoStep(diff.next, coordSys, step, connectNulls);
stackedOnNext = turnPointsIntoStep(diff.stackedOnNext, coordSys, step, connectNulls);
}
// Don't apply animation if diff is large.
// For better result and avoid memory explosion problems like
// https://github.com/apache/incubator-echarts/issues/12229
if (getBoundingDiff(current, next) > 3000
|| (polygon && getBoundingDiff(stackedOnCurrent, stackedOnNext) > 3000)
) {
polyline.stopAnimation();
polyline.setShape({
points: next
});
if (polygon) {
polygon.stopAnimation();
polygon.setShape({
points: next,
stackedOnPoints: stackedOnNext
});
}
return;
}
(polyline.shape as any).__points = diff.current;
polyline.shape.points = current;
const target = {
shape: {
points: next
}
};
// Also animate the original points.
// If points reference is changed when turning into step line.
if (diff.current !== current) {
(target.shape as any).__points = diff.next;
}
// Stop previous animation.
polyline.stopAnimation();
graphic.updateProps(polyline, target, seriesModel);
if (polygon) {
polygon.setShape({
// Reuse the points with polyline.
points: current,
stackedOnPoints: stackedOnCurrent
});
polygon.stopAnimation();
graphic.updateProps(polygon, {
shape: {
stackedOnPoints: stackedOnNext
}
}, seriesModel);
// If use attr directly in updateProps.
if (polyline.shape.points !== polygon.shape.points) {
polygon.shape.points = polyline.shape.points;
}
}
const updatedDataInfo: {
el: SymbolExtended,
ptIdx: number
}[] = [];
const diffStatus = diff.status;
for (let i = 0; i < diffStatus.length; i++) {
const cmd = diffStatus[i].cmd;
if (cmd === '=') {
const el = data.getItemGraphicEl(diffStatus[i].idx1) as SymbolExtended;
if (el) {
updatedDataInfo.push({
el: el,
ptIdx: i // Index of points
});
}
}
}
if (polyline.animators && polyline.animators.length) {
polyline.animators[0].during(function () {
polygon && polygon.dirtyShape();
const points = (polyline.shape as any).__points;
for (let i = 0; i < updatedDataInfo.length; i++) {
const el = updatedDataInfo[i].el;
const offset = updatedDataInfo[i].ptIdx * 2;
el.x = points[offset];
el.y = points[offset + 1];
el.markRedraw();
}
});
}
}
remove(ecModel: GlobalModel) {
const group = this.group;
const oldData = this._data;
this._lineGroup.removeAll();
this._symbolDraw.remove(true);
// Remove temporary created elements when highlighting
oldData && oldData.eachItemGraphicEl(function (el: SymbolExtended, idx) {
if (el.__temp) {
group.remove(el);
oldData.setItemGraphicEl(idx, null);
}
});
this._polyline =
this._polygon =
this._coordSys =
this._points =
this._stackedOnPoints =
this._endLabel =
this._data = null;
}
}
export default LineView;