Skip to content

Commit e90aa8b

Browse files
committed
fix: moasic chart's crosshair can not render rect shape
1 parent b466bd3 commit e90aa8b

3 files changed

Lines changed: 172 additions & 6 deletions

File tree

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { getDatumByValue } from '../../../../src/component/crosshair/utils/common';
2+
import { layoutCrosshair } from '../../../../src/component/crosshair/utils/cartesian';
3+
import type { CrossHairStateItem } from '../../../../src/component/crosshair/interface';
4+
5+
describe('crosshair utils', () => {
6+
describe('getDatumByValue', () => {
7+
const data = [
8+
{ start: 0, end: 0.2, name: 'A' },
9+
{ start: 0.2, end: 0.6, name: 'B' },
10+
{ start: 0.6, end: 1, name: 'C' }
11+
];
12+
13+
test('should find datum when startField < endField (histogram-like)', () => {
14+
const result = getDatumByValue(data, 0.1, 'start', 'end');
15+
expect(result).toEqual(data[0]);
16+
17+
const result2 = getDatumByValue(data, 0.4, 'start', 'end');
18+
expect(result2).toEqual(data[1]);
19+
20+
const result3 = getDatumByValue(data, 0.8, 'start', 'end');
21+
expect(result3).toEqual(data[2]);
22+
});
23+
24+
test('should find datum when startField > endField (mosaic-like reversed fields)', () => {
25+
const reversedData = [
26+
{ catEnd: 0.2, catStart: 0, name: 'A' },
27+
{ catEnd: 0.6, catStart: 0.2, name: 'B' },
28+
{ catEnd: 1, catStart: 0.6, name: 'C' }
29+
];
30+
// In mosaic, fieldX[0] = catEnd (larger), fieldX2 = catStart (smaller)
31+
// so startField=catEnd, endField=catStart → startValue > endValue
32+
const result = getDatumByValue(reversedData, 0.1, 'catEnd', 'catStart');
33+
expect(result).toEqual(reversedData[0]);
34+
35+
const result2 = getDatumByValue(reversedData, 0.4, 'catEnd', 'catStart');
36+
expect(result2).toEqual(reversedData[1]);
37+
38+
const result3 = getDatumByValue(reversedData, 0.8, 'catEnd', 'catStart');
39+
expect(result3).toEqual(reversedData[2]);
40+
});
41+
42+
test('should return null when value is out of range', () => {
43+
const result = getDatumByValue(data, 1.5, 'start', 'end');
44+
expect(result).toBeNull();
45+
46+
const result2 = getDatumByValue(data, -0.1, 'start', 'end');
47+
expect(result2).toBeNull();
48+
});
49+
50+
test('should return null for empty data', () => {
51+
const result = getDatumByValue([], 0.5, 'start', 'end');
52+
expect(result).toBeNull();
53+
});
54+
55+
test('should handle single field (no endField)', () => {
56+
const pointData = [{ x: 5 }, { x: 10 }, { x: 15 }];
57+
const result = getDatumByValue(pointData, 10, 'x');
58+
expect(result).toEqual(pointData[1]);
59+
});
60+
61+
test('should match boundary values', () => {
62+
const result = getDatumByValue(data, 0, 'start', 'end');
63+
expect(result).toEqual(data[0]);
64+
65+
const result2 = getDatumByValue(data, 0.2, 'start', 'end');
66+
// 0.2 matches both data[0] (end) and data[1] (start), returns first match
67+
expect(result2).toEqual(data[0]);
68+
});
69+
});
70+
71+
describe('layoutCrosshair for rect type', () => {
72+
test('should compute correct rect position when coord is at left edge (normal order)', () => {
73+
const stateItem: CrossHairStateItem = {
74+
coordKey: 'x',
75+
anotherAxisKey: 'y',
76+
currentValue: new Map(),
77+
bandSize: 100,
78+
offsetSize: 0,
79+
cacheInfo: {
80+
coord: 50,
81+
coordRange: [0, 500],
82+
sizeRange: [0, 300],
83+
visible: true,
84+
labels: {},
85+
labelsTextStyle: {},
86+
axis: { getLayoutRect: () => ({ width: 500, height: 300 }) } as any
87+
},
88+
attributes: {
89+
visible: true,
90+
type: 'rect'
91+
}
92+
};
93+
94+
const result = layoutCrosshair(stateItem);
95+
expect(result).toBeDefined();
96+
expect(result.visible).toBe(true);
97+
// bandSize=100, getRectSize returns [0, 100] for bandSize > 0
98+
// start.x = max(50 + 0, 0) = 50, end.x = min(50 + 100, 500) = 150
99+
expect(result.start.x).toBe(50);
100+
expect(result.end.x).toBe(150);
101+
expect(result.start.y).toBe(0);
102+
expect(result.end.y).toBe(300);
103+
});
104+
105+
test('should compute correct rect position for mosaic-like scenario', () => {
106+
// In mosaic after the fix, coord = Math.min(posStart, posEnd)
107+
// so coord is always at the left edge of the band
108+
const stateItem: CrossHairStateItem = {
109+
coordKey: 'x',
110+
anotherAxisKey: 'y',
111+
currentValue: new Map(),
112+
bandSize: 200,
113+
offsetSize: 0,
114+
cacheInfo: {
115+
coord: 100,
116+
coordRange: [0, 500],
117+
sizeRange: [0, 300],
118+
visible: true,
119+
labels: {},
120+
labelsTextStyle: {},
121+
axis: { getLayoutRect: () => ({ width: 500, height: 300 }) } as any
122+
},
123+
attributes: {
124+
visible: true,
125+
type: 'rect'
126+
}
127+
};
128+
129+
const result = layoutCrosshair(stateItem);
130+
expect(result).toBeDefined();
131+
// bandSize=200, getRectSize returns [0, 200]
132+
// start.x = max(100 + 0, 0) = 100, end.x = min(100 + 200, 500) = 300
133+
expect(result.start.x).toBe(100);
134+
expect(result.end.x).toBe(300);
135+
});
136+
137+
test('should clamp rect to coordRange', () => {
138+
const stateItem: CrossHairStateItem = {
139+
coordKey: 'x',
140+
anotherAxisKey: 'y',
141+
currentValue: new Map(),
142+
bandSize: 100,
143+
offsetSize: 0,
144+
cacheInfo: {
145+
coord: 450,
146+
coordRange: [0, 500],
147+
sizeRange: [0, 300],
148+
visible: true,
149+
labels: {},
150+
labelsTextStyle: {},
151+
axis: { getLayoutRect: () => ({ width: 500, height: 300 }) } as any
152+
},
153+
attributes: {
154+
visible: true,
155+
type: 'rect'
156+
}
157+
};
158+
159+
const result = layoutCrosshair(stateItem);
160+
// end.x = min(450 + 100, 500) = 500, clamped
161+
expect(result.end.x).toBe(500);
162+
});
163+
});
164+
});

packages/vchart/src/component/crosshair/utils/cartesian.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,16 +89,16 @@ export const layoutByValue = (
8989
const field2 = field === 'xField' ? series.fieldX2 : series.fieldY2; // todo
9090
const datum = getDatumByValue(series.getViewData().latestData, +value, field1, field2);
9191
if (datum) {
92-
const startX = field === 'xField' ? series.dataToPositionX(datum) : series.dataToPositionY(datum);
92+
const posStart = field === 'xField' ? series.dataToPositionX(datum) : series.dataToPositionY(datum);
9393
if (field2) {
94-
bandSize = Math.abs(
95-
startX - (field === 'xField' ? series.dataToPositionX1(datum) : series.dataToPositionY1(datum))
96-
);
94+
const posEnd = field === 'xField' ? series.dataToPositionX1(datum) : series.dataToPositionY1(datum);
95+
bandSize = Math.abs(posStart - posEnd);
96+
coord = Math.min(posStart, posEnd);
9797
value = `${datum[field1]} ~ ${datum[field2]}`;
9898
} else {
9999
bandSize = 1;
100+
coord = posStart;
100101
}
101-
coord = startX;
102102
}
103103
niceLabelFormatter = (axis as ILinearAxis).niceLabelFormatter;
104104
}

packages/vchart/src/component/crosshair/utils/common.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,10 @@ export function getDatumByValue(data: Datum[], value: number, startField: string
6161
if (record) {
6262
const startValue = record[startField];
6363
const endValue = record[endField || startField];
64+
const min = Math.min(startValue, endValue);
65+
const max = Math.max(startValue, endValue);
6466

65-
if (startValue <= value && endValue >= value) {
67+
if (min <= value && max >= value) {
6668
return record;
6769
}
6870
}

0 commit comments

Comments
 (0)