diff --git a/src/chart/line/LineView.ts b/src/chart/line/LineView.ts index 3682d96512..0d5f5c00e1 100644 --- a/src/chart/line/LineView.ts +++ b/src/chart/line/LineView.ts @@ -869,7 +869,8 @@ class LineView extends ChartView { polyline.setShape({ smooth, smoothMonotone, - connectNulls + connectNulls, + step: !!step }); if (polygon) { @@ -894,7 +895,8 @@ class LineView extends ChartView { smooth, stackedOnSmooth, smoothMonotone, - connectNulls + connectNulls, + step: !!step }); setStatesStylesFromModel(polygon, seriesModel, 'areaStyle'); diff --git a/src/chart/line/poly.ts b/src/chart/line/poly.ts index aa6826de8d..62dfe3099a 100644 --- a/src/chart/line/poly.ts +++ b/src/chart/line/poly.ts @@ -45,7 +45,8 @@ function drawSegment( dir: number, smooth: number, smoothMonotone: 'x' | 'y' | 'none', - connectNulls: boolean + connectNulls: boolean, + step?: boolean ) { let prevX: number; let prevY: number; @@ -81,7 +82,14 @@ function drawSegment( let dy = y - prevY; // Ignore tiny segment. - if ((dx * dx + dy * dy) < 0.5) { + // In step mode, keep every corner; only drop strict duplicates. See #21614. + if (step) { + if (dx === 0 && dy === 0) { + idx += dir; + continue; + } + } + else if ((dx * dx + dy * dy) < 0.5) { idx += dir; continue; } @@ -218,6 +226,8 @@ class ECPolylineShape { smoothConstraint = true; smoothMonotone: 'x' | 'y' | 'none'; connectNulls: boolean; + // True when `points` are step-expanded; keep every corner. See #21614. + step: boolean; } interface ECPolylineProps extends PathProps { @@ -271,7 +281,8 @@ export class ECPolyline extends Path { ctx, points, i, len, len, 1, shape.smooth, - shape.smoothMonotone, shape.connectNulls + shape.smoothMonotone, shape.connectNulls, + shape.step ) + 1; } } @@ -395,13 +406,15 @@ export class ECPolygon extends Path { ctx, points, i, len, len, 1, shape.smooth, - smoothMonotone, shape.connectNulls + smoothMonotone, shape.connectNulls, + shape.step ); drawSegment( ctx, stackedOnPoints, i + k - 1, k, len, -1, shape.stackedOnSmooth, - smoothMonotone, shape.connectNulls + smoothMonotone, shape.connectNulls, + shape.step ); i += k + 1; diff --git a/test/ut/spec/series/line-step-poly.test.ts b/test/ut/spec/series/line-step-poly.test.ts new file mode 100644 index 0000000000..1356162ae1 --- /dev/null +++ b/test/ut/spec/series/line-step-poly.test.ts @@ -0,0 +1,103 @@ +/* +* 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 { ECPolyline } from '../../../../src/chart/line/poly'; + +type Cmd = ['M' | 'L', number, number] | ['C', number, number, number, number, number, number]; + +function recordCommands(polyline: ECPolyline): Cmd[] { + const cmds: Cmd[] = []; + const mockCtx = { + moveTo(x: number, y: number) { cmds.push(['M', x, y]); }, + lineTo(x: number, y: number) { cmds.push(['L', x, y]); }, + bezierCurveTo(x: number, y: number, x2: number, y2: number, x3: number, y3: number) { + cmds.push(['C', x, y, x2, y2, x3, y3]); + }, + closePath() { /* noop */ } + }; + polyline.buildPath(mockCtx as any, polyline.shape); + return cmds; +} + +describe('chart/line/poly', function () { + + // https://github.com/apache/echarts/issues/21614 + it('step mode keeps every corner even when points are visually close', function () { + // Step-expanded points for data (0,1),(0.3,1),(0.3,0) with step:'end'. + // Adjacent corners fall within the legacy `< 0.5` tiny-segment threshold, + // which previously collapsed the L-shape into a slope. + const points = [ + 0, 1, + 0.3, 1, + 0.3, 1, + 0.3, 1, + 0.3, 0 + ]; + + const polyline = new ECPolyline({ + shape: { points, smooth: 0, connectNulls: false, step: true } + }); + + const cmds = recordCommands(polyline); + const lineTos = cmds.filter(c => c[0] === 'L'); + + expect(cmds[0]).toEqual(['M', 0, 1]); + + const cornerIdx = lineTos.findIndex(c => c[1] === 0.3 && c[2] === 1); + const tailIdx = lineTos.findIndex(c => c[1] === 0.3 && c[2] === 0); + expect(cornerIdx).toBeGreaterThanOrEqual(0); + expect(tailIdx).toBeGreaterThanOrEqual(0); + expect(cornerIdx).toBeLessThan(tailIdx); + }); + + it('step mode still drops strictly duplicated points', function () { + const points = [ + 0, 0, + 0, 0, + 0, 0, + 5, 5 + ]; + + const polyline = new ECPolyline({ + shape: { points, smooth: 0, connectNulls: false, step: true } + }); + + const cmds = recordCommands(polyline); + const lineTos = cmds.filter(c => c[0] === 'L'); + + expect(cmds[0]).toEqual(['M', 0, 0]); + expect(lineTos).toEqual([['L', 5, 5]]); + }); + + it('non-step mode preserves the tiny-segment optimization', function () { + const points = [ + 0, 0, + 0.3, 0, + 10, 0 + ]; + + const polyline = new ECPolyline({ + shape: { points, smooth: 0, connectNulls: false, step: false } + }); + + const lineTos = recordCommands(polyline).filter(c => c[0] === 'L'); + expect(lineTos).toEqual([['L', 10, 0]]); + }); + +});