Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/chart/line/LineSeries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ export interface LineDataItemOption extends SymbolOptionMixin,
name?: string

value?: LineDataValue

/**
* Restart smoothing at this vertex. The stroke stays connected.
* A missing sample (`null`) is still what lifts the pen.
* Ignored while `step` is set. Dataset rows do not carry this flag.
*/
smoothBreak?: boolean
}

export interface LineEndLabelOption extends SeriesLabelOption {
Expand Down
82 changes: 74 additions & 8 deletions src/chart/line/LineView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ 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, isPointIllegal } from './helper';
import { prepareDataCoordInfo, getStackedOnPoint, isPointIllegal, isSmoothBreakDatum } from './helper';
import { createGridClipPath, createPolarClipPath } from '../helper/createClipPathFromCoordSys';
import LineSeriesModel, { LineSeriesOption } from './LineSeries';
import type GlobalModel from '../../model/Global';
Expand Down Expand Up @@ -119,6 +119,44 @@ function getSmooth(smooth: number | boolean) {
return zrUtil.isNumber(smooth) ? smooth : (smooth ? 0.5 : 0);
}

/**
* One flag per layout point. `step` rebuilds the point list, so a seam index
* would no longer name a sample. No flags means the line is one smooth piece.
*/
function readSmoothBreaks(
data: SeriesData,
pointCount: number,
step: LineSeriesOption['step']
): boolean[] | null {
if (step || pointCount <= 0 || data.count() !== pointCount) {
return null;
}
const breaks: boolean[] = [];
let any = false;
for (let i = 0; i < pointCount; i++) {
const on = isSmoothBreakDatum(data, i);
breaks.push(on);
any = any || on;
}
return any ? breaks : null;
}

function breaksOrNull(flags: ArrayLike<boolean> | null | undefined): boolean[] | null {
if (!flags) {
return null;
}
for (let i = 0; i < flags.length; i++) {
if (flags[i]) {
const out: boolean[] = [];
for (let j = 0; j < flags.length; j++) {
out.push(!!flags[j]);
}
return out;
}
}
return null;
}

function getStackedOnPoints(
coordSys: Cartesian2D | Polar,
data: SeriesData,
Expand Down Expand Up @@ -684,6 +722,9 @@ class LineView extends ChartView {

// FIXME step not support polar
const step = !isCoordSysPolar ? seriesModel.get('step') : false;
// Taken from the layout points, before `step` rewrites that array.
const layoutBreaks = readSmoothBreaks(data, points.length / 2, step);
let seamFlagsFromAnimation = false;
let clipShapeForSymbol: PolarArea | Cartesian2DArea;
if (coordSys && coordSys.getArea && seriesModel.get('clip', true)) {
clipShapeForSymbol = coordSys.getArea();
Expand Down Expand Up @@ -802,6 +843,7 @@ class LineView extends ChartView {
this._doUpdateAnimation(
data, stackedOnPoints, coordSys, api, step, valueOrigin, connectNulls
);
seamFlagsFromAnimation = true;
}
else {
// Not do it in update with animation
Expand All @@ -814,11 +856,13 @@ class LineView extends ChartView {
}

polyline.setShape({
points: points
points: points,
smoothBreaks: layoutBreaks
});
polygon && polygon.setShape({
points: points,
stackedOnPoints: stackedOnPoints
stackedOnPoints: stackedOnPoints,
smoothBreaks: layoutBreaks
});
}
}
Expand Down Expand Up @@ -853,10 +897,15 @@ class LineView extends ChartView {
const smooth = getSmooth(seriesModel.get('smooth'));
const smoothMonotone = seriesModel.get('smoothMonotone');

polyline.setShape({
polyline.setShape(seamFlagsFromAnimation ? {
smooth,
smoothMonotone,
connectNulls
} : {
smooth,
smoothMonotone,
connectNulls,
smoothBreaks: layoutBreaks
});

if (polygon) {
Expand All @@ -877,11 +926,17 @@ class LineView extends ChartView {
stackedOnSmooth = getSmooth(stackedOnSeries.get('smooth'));
}

polygon.setShape({
polygon.setShape(seamFlagsFromAnimation ? {
smooth,
stackedOnSmooth,
smoothMonotone,
connectNulls
} : {
smooth,
stackedOnSmooth,
smoothMonotone,
connectNulls,
smoothBreaks: layoutBreaks
});

setStatesStylesFromModel(polygon, seriesModel, 'areaStyle');
Expand Down Expand Up @@ -1348,6 +1403,9 @@ class LineView extends ChartView {
let stackedOnCurrent = diff.stackedOnCurrent;
let next = diff.next;
let stackedOnNext = diff.stackedOnNext;
// Step expands the point list, so these flags would name the wrong vertex.
// Otherwise use the target datum for the whole morph: booleans do not interpolate.
const smoothBreaks = step ? null : breaksOrNull(diff.smoothBreaks);
if (step) {
// TODO If stacked series is not step
stackedOnCurrent = turnPointsIntoStep(diff.stackedOnCurrent, diff.current, coordSys, step, connectNulls);
Expand All @@ -1363,20 +1421,27 @@ class LineView extends ChartView {
) {
polyline.stopAnimation();
polyline.setShape({
points: next
points: next,
smoothBreaks: smoothBreaks
});
if (polygon) {
polygon.stopAnimation();
polygon.setShape({
points: next,
stackedOnPoints: stackedOnNext
stackedOnPoints: stackedOnNext,
smoothBreaks: smoothBreaks
});
}
return;
}

(polyline.shape as any).__points = diff.current;
polyline.shape.points = current;
// Not part of the animation target. The flag stays on this vertex
// while `points` is interpolated, and it is already the target value.
polyline.setShape({
smoothBreaks: smoothBreaks
});

const target = {
shape: {
Expand All @@ -1397,7 +1462,8 @@ class LineView extends ChartView {
polygon.setShape({
// Reuse the points with polyline.
points: current,
stackedOnPoints: stackedOnCurrent
stackedOnPoints: stackedOnCurrent,
smoothBreaks: smoothBreaks
});
polygon.stopAnimation();
graphic.updateProps(polygon, {
Expand Down
16 changes: 14 additions & 2 deletions src/chart/line/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@
*/

import {isDimensionStacked} from '../../data/helper/dataStackHelper';
import {isNumber, map} from 'zrender/src/core/util';
import {isArray, isNumber, map} from 'zrender/src/core/util';
import type Polar from '../../coord/polar/Polar';
import type Cartesian2D from '../../coord/cartesian/Cartesian2D';
import SeriesData from '../../data/SeriesData';
import Axis from '../../coord/Axis';
import type { LineSeriesOption } from './LineSeries';
import type { LineDataItemOption, LineSeriesOption } from './LineSeries';

interface CoordInfo {
dataDimsForPoint: string[]
Expand Down Expand Up @@ -133,6 +133,18 @@ export function getStackedOnPoint(
return coordSys.dataToPoint(stackedData);
}

/**
* `smoothBreak` lives on the datum object. Numbers and `[x, y]` tuples cannot carry it.
* `getItemModel` is used for object items so the flag is read the same way as other item options.
*/
export function isSmoothBreakDatum(data: SeriesData, dataIndex: number): boolean {
const raw = data.getRawDataItem(dataIndex);
if (raw == null || typeof raw !== 'object' || isArray(raw)) {
return false;
}
return !!(data.getItemModel<Pick<LineDataItemOption, 'smoothBreak'>>(dataIndex).get('smoothBreak'));
}

export function isPointIllegal(xOrY: number, yOrX: number) {
// NOTE:
// - `NaN` point x/y may be generated by, e.g.,
Expand Down
13 changes: 11 additions & 2 deletions src/chart/line/lineAnimationDiff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* under the License.
*/

import {prepareDataCoordInfo, getStackedOnPoint} from './helper';
import {prepareDataCoordInfo, getStackedOnPoint, isSmoothBreakDatum} from './helper';
import SeriesData from '../../data/SeriesData';
import type Cartesian2D from '../../coord/cartesian/Cartesian2D';
import type Polar from '../../coord/polar/Polar';
Expand Down Expand Up @@ -74,6 +74,9 @@ export default function lineAnimationDiff(
const status = [];
const sortedIndices: number[] = [];
const rawIndices: number[] = [];
// Target seam flags, one per morphed vertex, in the same order as
// currPoints / nextPoints before sorting.
const nextBreaks: boolean[] = [];

const newDataOldCoordInfo = prepareDataCoordInfo(oldCoordSys, newData, oldValueOrigin);
// const oldDataNewCoordInfo = prepareDataCoordInfo(newCoordSys, oldData, newValueOrigin);
Expand Down Expand Up @@ -111,6 +114,7 @@ export default function lineAnimationDiff(
nextStackedPoints.push(newStackedOnPoints[newIdx2], newStackedOnPoints[newIdx2 + 1]);

rawIndices.push(newData.getRawIndex(diffItem.idx1));
nextBreaks.push(isSmoothBreakDatum(newData, diffItem.idx1));
break;
case '+':
const newIdx = diffItem.idx;
Expand All @@ -130,6 +134,7 @@ export default function lineAnimationDiff(
nextStackedPoints.push(newStackedOnPoints[newIdx2], newStackedOnPoints[newIdx2 + 1]);

rawIndices.push(newData.getRawIndex(newIdx));
nextBreaks.push(isSmoothBreakDatum(newData, newIdx));
break;
case '-':
pointAdded = false;
Expand All @@ -156,6 +161,7 @@ export default function lineAnimationDiff(
const sortedNextStackedPoints = createFloat32Array(len);

const sortedStatus = [];
const sortedBreaks: boolean[] = [];
for (let i = 0; i < sortedIndices.length; i++) {
const idx = sortedIndices[i];
const i2 = i * 2;
Expand All @@ -171,6 +177,7 @@ export default function lineAnimationDiff(
sortedNextStackedPoints[i2 + 1] = nextStackedPoints[idx2 + 1];

sortedStatus[i] = status[idx];
sortedBreaks[i] = nextBreaks[idx];
}

return {
Expand All @@ -180,6 +187,8 @@ export default function lineAnimationDiff(
stackedOnCurrent: sortedCurrStackedPoints,
stackedOnNext: sortedNextStackedPoints,

status: sortedStatus
status: sortedStatus,
// Target flags for the whole animation. A boolean does not interpolate.
smoothBreaks: sortedBreaks
};
}
37 changes: 31 additions & 6 deletions src/chart/line/poly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ import { isPointIllegal } from './helper';
const mathMin = Math.min;
const mathMax = Math.max;

function pointIsSmoothBreak(
smoothBreaks: ArrayLike<boolean> | null | undefined,
idx: number
): boolean {
return !!(smoothBreaks && idx >= 0 && idx < smoothBreaks.length && smoothBreaks[idx]);
}

/**
* Draw smoothed line in non-monotone, in may cause undesired curve in extreme
* situations. This should be used when points are non-monotone neither in x or
Expand All @@ -42,7 +49,8 @@ function drawSegment(
dir: number,
smooth: number,
smoothMonotone: 'x' | 'y' | 'none',
connectNulls: boolean
connectNulls: boolean,
smoothBreaks?: ArrayLike<boolean> | null
) {
let prevX: number;
let prevY: number;
Expand Down Expand Up @@ -87,11 +95,13 @@ function drawSegment(
let nextIdx = idx + dir;
let nextX = points[nextIdx * 2];
let nextY = points[nextIdx * 2 + 1];
let seam = pointIsSmoothBreak(smoothBreaks, idx);
// Ignore duplicate point
while (nextX === x && nextY === y && k < segLen) {
k++;
nextIdx += dir;
idx += dir;
seam = seam || pointIsSmoothBreak(smoothBreaks, idx);
nextX = points[nextIdx * 2];
nextY = points[nextIdx * 2 + 1];
x = points[idx * 2];
Expand All @@ -116,10 +126,16 @@ function drawSegment(
let vy: number = 0;
let nextCpx0;
let nextCpy0;
// Is last point
if (tmpK >= segLen || isPointIllegal(nextX, nextY)) {
// Last point, or a seam: incoming control stops on this vertex.
// The outgoing control is this vertex too, so the next piece
// does not inherit a tangent from the previous one.
// A real last point leaves no following piece; setting the
// outgoing control here is unused in that case.
if (tmpK >= segLen || isPointIllegal(nextX, nextY) || seam) {
cpx1 = x;
cpy1 = y;
nextCpx0 = x;
nextCpy0 = y;
}
else {
vx = nextX - prevX;
Expand Down Expand Up @@ -215,6 +231,11 @@ class ECPolylineShape {
smoothConstraint = true;
smoothMonotone: 'x' | 'y' | 'none';
connectNulls: boolean;
/**
* Parallel to `points`: true restarts smoothing at that vertex.
* The baseline of an area does not use this series' flags.
*/
smoothBreaks: ArrayLike<boolean> | null = null;
}

interface ECPolylineProps extends PathProps {
Expand Down Expand Up @@ -268,7 +289,8 @@ export class ECPolyline extends Path<ECPolylineProps> {
ctx, points, i, len, len,
1,
shape.smooth,
shape.smoothMonotone, shape.connectNulls
shape.smoothMonotone, shape.connectNulls,
shape.smoothBreaks
) + 1;
}
}
Expand Down Expand Up @@ -392,13 +414,16 @@ export class ECPolygon extends Path {
ctx, points, i, len, len,
1,
shape.smooth,
smoothMonotone, shape.connectNulls
smoothMonotone, shape.connectNulls,
shape.smoothBreaks
);
// The stacked baseline is another series. Do not copy this series' seams onto it.
drawSegment(
ctx, stackedOnPoints, i + k - 1, k, len,
-1,
shape.stackedOnSmooth,
smoothMonotone, shape.connectNulls
smoothMonotone, shape.connectNulls,
null
);
i += k + 1;

Expand Down
Loading