Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"comment": "fix: restore point visibility after overlap updates",
"type": "none",
"packageName": "@visactor/vchart"
}
],
"packageName": "@visactor/vchart",
"email": "dingling112@gmail.com"
}
34 changes: 34 additions & 0 deletions packages/vchart/__tests__/unit/mark/symbol-overlap.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { transform } from '../../../src/mark/transform/symbol-overlap';
import type { IMarkGraphic } from '../../../src/mark/interface';

const point = (position: number, group = 'a', forceShow = false) =>
({
context: {
data: [{ group }],
finalAttrs: { x: position, y: position, size: 10, visible: true, forceShow }
}
} as unknown as IMarkGraphic);

test.each([1, 2])('overlap respects the current encoded visibility in direction %s', direction => {
const graphics = [point(0), point(1)];
transform({ direction }, graphics);
expect(graphics[1].context.finalAttrs.visible).toBe(false);

graphics[0].context.finalAttrs = { x: 0, y: 0, size: 10, visible: true };
graphics[1].context.finalAttrs = { x: 100, y: 100, size: 10, visible: false };
transform({ direction }, graphics);
expect(graphics[1].context.finalAttrs.visible).toBe(false);

graphics[1].context.finalAttrs = { x: 100, y: 100, size: 10, visible: true };
transform({ direction }, graphics);
expect(graphics[1].context.finalAttrs.visible).toBe(true);
});

test('overlap preserves grouping, sorting and forceShow', () => {
const graphics = [point(50), point(0), point(1), point(2, 'a', true), point(0, 'b')];
const order = graphics.slice();

expect(transform({ direction: 1, sort: true, groupBy: 'group' }, graphics)).toBe(graphics);
expect(graphics).toEqual(order);
expect(graphics.map(g => g.context.finalAttrs.visible)).toEqual([true, true, false, true, true]);
});
19 changes: 19 additions & 0 deletions packages/vchart/__tests__/unit/mark/symbol.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,25 @@ test('symbol setAttribute support constant value.', () => {
expect(y_hover).toEqual(100);
});

test('symbol reinit restores defaults before applying the next spec', () => {
const symbolMark = new SymbolMark('symbol0', ctx);
symbolMark.created();
symbolMark.initStyleWithSpec({ style: { visible: false, size: 12, lineWidth: 2 } });

symbolMark.clearBeforeReInit();

expect(symbolMark.stateStyle.normal).toBeDefined();
expect(symbolMark.getAttribute('visible', {})).toBe(true);
expect(symbolMark.getAttribute('size', {})).toBe(1);
expect(symbolMark.getAttribute('symbolType', {})).toBe('circle');
expect(symbolMark.getAttribute('lineWidth', {})).toBe(0);

symbolMark.initStyleWithSpec({ style: { visible: false, size: 8 } });

expect(symbolMark.getAttribute('visible', {})).toBe(false);
expect(symbolMark.getAttribute('size', {})).toBe(8);
});

test('symbol setAttribute support function.', () => {
const symbolMark = new SymbolMark('symbol0', ctx);
symbolMark.created();
Expand Down
149 changes: 149 additions & 0 deletions packages/vchart/__tests__/unit/series/line-overlap-update.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import VChart, { ManualTicker, type ILineChartSpec } from '../../../src';
import type { ISymbolGraphicAttribute } from '@visactor/vrender-core';
import { setGraphicStates } from '../../../src/util/graphic-state';

const makeValues = (count: number) =>
['green', 'orange'].flatMap(group => Array.from({ length: count }, (_, i) => ({ x: `item-${i}`, y: i + 1, group })));

const makeSpec = (count: number, horizontal = false): ILineChartSpec => ({
type: 'line',
width: horizontal ? 160 : 240,
height: horizontal ? 240 : 160,
padding: 0,
animation: false,
direction: horizontal ? 'horizontal' : 'vertical',
data: { id: 'data', values: makeValues(count) },
xField: horizontal ? 'y' : 'x',
yField: horizontal ? 'x' : 'y',
seriesField: 'group',
markOverlap: true,
point: { style: { size: 14 } },
axes: [
{ orient: 'bottom', visible: false },
{ orient: 'left', visible: false }
]
});

const points = (chart: VChart) => chart.getChart().getAllSeries()[0].getMarkInName('point').getGraphics();
const visibleCount = (chart: VChart, group: string) =>
points(chart).filter(g => g.context.data[0].group === group && g.attribute.visible !== false).length;
const snapshot = (chart: VChart) =>
points(chart).map(g => ({
x: g.attribute.x,
y: g.attribute.y,
visible: g.attribute.visible,
size: (g.attribute as ISymbolGraphicAttribute).size,
fill: g.attribute.fill
}));

describe('line overlap updates', () => {
const charts: VChart[] = [];
const containers: HTMLElement[] = [];
const createChart = (spec: ILineChartSpec, ticker?: ManualTicker) => {
const dom = document.createElement('div');
document.body.appendChild(dom);
containers.push(dom);
const chart = new VChart(spec, { dom, ticker, animation: spec.animation !== false });
charts.push(chart);
chart.renderSync();
return chart;
};

afterEach(() => {
charts.splice(0).forEach(chart => chart.release());
containers.splice(0).forEach(dom => dom.remove());
});

test.each([false, true])('dense to sparse update restores reused points, horizontal=%s', horizontal => {
const chart = createChart(makeSpec(12, horizontal));
const original = points(chart).slice();
expect(visibleCount(chart, 'green')).toBeLessThan(12);

chart.updateSpecSync(makeSpec(3, horizontal));

const green = points(chart).filter(g => g.context.data[0].group === 'green');
expect(green.every(g => original.includes(g))).toBe(true);
expect(visibleCount(chart, 'green')).toBe(3);
expect(visibleCount(chart, 'orange')).toBe(3);
expect(snapshot(chart)).toEqual(snapshot(createChart(makeSpec(3, horizontal))));

chart.updateSpecSync(makeSpec(12, horizontal));
expect(visibleCount(chart, 'green')).toBeLessThan(12);
chart.updateSpecSync(makeSpec(3, horizontal));
expect(visibleCount(chart, 'green')).toBe(3);
expect(visibleCount(chart, 'orange')).toBe(3);
});

test('a previous overlap decision does not override the next explicit visibility', () => {
const chart = createChart(makeSpec(12));
// 在真实图元上再次运行防重叠,覆盖旧实现的隐藏标记已存在的路径。
chart.updateSpecSync(makeSpec(12));
const spec = makeSpec(3);
spec.point.style.visible = datum => datum.x !== 'item-1';

chart.updateSpecSync(spec);

expect(visibleCount(chart, 'green')).toBe(2);
expect(visibleCount(chart, 'orange')).toBe(2);
expect(snapshot(chart)).toEqual(snapshot(createChart(spec)));
});

test('updateData restores points that no longer overlap', () => {
const chart = createChart(makeSpec(12));
const original = points(chart)[1];
expect(original.attribute.visible).toBe(false);

chart.updateDataSync('data', makeValues(3));

expect(points(chart)[1]).toBe(original);
expect(snapshot(chart)).toEqual(snapshot(createChart(makeSpec(3))));
});

test('resize recomputes visibility in both directions', () => {
const chart = createChart(makeSpec(12));
const narrow = snapshot(chart);

chart.resize(720, 160);
expect(visibleCount(chart, 'green')).toBe(12);
expect(visibleCount(chart, 'orange')).toBe(12);

chart.resize(240, 160);
expect(snapshot(chart)).toEqual(narrow);
});

test('hover after an update restores the new normal visibility', () => {
const chart = createChart(makeSpec(12));
chart.updateSpecSync(makeSpec(3));
const point = points(chart)[1];

setGraphicStates(point, ['dimension_hover'], false);
setGraphicStates(point, [], false);

expect(point.attribute.visible).toBe(true);
expect(snapshot(chart)).toEqual(snapshot(createChart(makeSpec(3))));
});

test('animated update finishes with the same visible points as a fresh render', () => {
const ticker = new ManualTicker();
ticker.autoStop = false;
const animatedSpec = (count: number): ILineChartSpec => ({
...makeSpec(count),
animation: true,
animationAppear: { duration: 300, easing: 'linear' },
animationUpdate: { duration: 300, easing: 'linear' }
});
try {
const chart = createChart(animatedSpec(12), ticker);
ticker.tickAt(400);
chart.updateSpecSync(animatedSpec(3));
ticker.tickAt(800);

expect(visibleCount(chart, 'green')).toBe(3);
expect(visibleCount(chart, 'orange')).toBe(3);
expect(snapshot(chart)).toEqual(snapshot(createChart(makeSpec(3))));
} finally {
charts.splice(0).forEach(chart => chart.release());
ticker.release();
}
});
});
1 change: 1 addition & 0 deletions packages/vchart/src/mark/base/base-mark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2296,6 +2296,7 @@ export class BaseMark<T extends ICommonSpec> extends GrammarItem implements IMar
this.state.clearAllStateInfo();
this.uncommit();
this.stateStyle = {};
this._initStyle();
this.getGraphics().forEach(g => {
if (g.currentStates?.length) {
(g as ReinitStateGraphic)[statesClearedBeforeReInitKey] = g.currentStates.slice();
Expand Down
37 changes: 4 additions & 33 deletions packages/vchart/src/mark/transform/symbol-overlap.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,8 @@
import type { ISymbolGraphicAttribute } from '@visactor/vrender-core';
import { PREFIX } from '../../constant/base';
import { isNil } from '@visactor/vutils';
import { Factory } from '../../core/factory';
import type { IMarkGraphic } from '../interface';

export const OVERLAP_HIDE_KEY = `${PREFIX}_hide_`;

function setVisible(g: IMarkGraphic, visible: boolean) {
if (g.context.finalAttrs) {
g.context.finalAttrs.visible = visible;
}
}

function reset(graphics: IMarkGraphic[]) {
graphics.forEach(g => {
const hide = (g as any)[OVERLAP_HIDE_KEY];

if (hide) {
setVisible(g, true);

(g as any)[OVERLAP_HIDE_KEY] = false;
}
});
return graphics;
}

function overlapX(graphics: IMarkGraphic[], delta: number, deltaMul: number) {
let lastX = -Infinity;
let lastR = 0;
Expand All @@ -44,9 +22,7 @@ function overlapX(graphics: IMarkGraphic[], delta: number, deltaMul: number) {
}
if (Math.abs(currentX - lastX) < itemDelta + lastR + r) {
if (!(g.context.finalAttrs as any).forceShow) {
(g as any)[OVERLAP_HIDE_KEY] = true;

setVisible(g, false);
g.context.finalAttrs.visible = false;
}
} else {
lastX = currentX;
Expand Down Expand Up @@ -75,9 +51,7 @@ function overlapY(graphics: IMarkGraphic[], delta: number, deltaMul: number) {
}
if (Math.abs(currentY - lastY) < itemDelta + lastR + r) {
if (!(g.context.finalAttrs as any).forceShow) {
(g as any)[OVERLAP_HIDE_KEY] = true;

setVisible(g, false);
g.context.finalAttrs.visible = false;
}
} else {
lastY = currentY;
Expand Down Expand Up @@ -110,9 +84,7 @@ function overlapXY(graphics: IMarkGraphic[], delta: number, deltaMul: number) {
dis = (lastX - currentX) ** 2 + (lastY - currentY) ** 2;
if (dis < (itemDelta + lastR + r) ** 2) {
if (!(g.context.finalAttrs as any).forceShow) {
(g as any)[OVERLAP_HIDE_KEY] = true;

setVisible(g, false);
g.context.finalAttrs.visible = false;
}
} else {
lastY = currentY;
Expand Down Expand Up @@ -148,8 +120,7 @@ export const transform = (
const { direction, delta, deltaMul = 1, groupBy } = options;

const handleOverlap = (graphics: IMarkGraphic[]) => {
reset(graphics);

// afterEncode 消费本轮完整编码结果,旧防重叠结果不能覆盖当前配置的可见性。
const sortedgraphics = options.sort
? graphics.slice().sort((a, b) => {
return a.context.finalAttrs.x - b.context.finalAttrs.x;
Expand Down
Loading