Skip to content

Commit 64957b2

Browse files
fix: render points sharing one date (#131)
* fix: render points sharing one date * perf: bound same-date graph paths --------- Co-authored-by: Christoph Pader <chris@margelo.com>
1 parent d68f97c commit 64957b2

2 files changed

Lines changed: 170 additions & 31 deletions

File tree

‎src/CreateGraphPath.ts‎

Lines changed: 81 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ export const getXPositionInRange = (
8787
const diff = xRange.max.getTime() - xRange.min.getTime();
8888
const x = date.getTime();
8989

90+
if (diff === 0) {
91+
return x === xRange.min.getTime() ? 0.5 : Number.NaN;
92+
}
93+
9094
return (x - xRange.min.getTime()) / diff;
9195
};
9296

@@ -174,48 +178,95 @@ function createGraphPathBase({
174178
return endX;
175179
};
176180

177-
for (
178-
let pixel = startX;
179-
startX <= pixel && pixel <= endX;
180-
pixel = getNextPixelValue(pixel)
181-
) {
182-
const index = getGraphDataIndex(pixel);
183-
184-
// Draw first point only on the very first pixel
185-
if (index === 0 && pixel !== startX) continue;
186-
// Draw last point only on the very last pixel
187-
188-
if (index === graphData.length - 1 && pixel !== endX) continue;
189-
190-
if (index !== 0 && index !== graphData.length - 1) {
191-
// Only draw point, when the point is exact
192-
const exactPointX =
193-
getXInRange(drawingWidth, graphData[index]!.date, range.x) +
194-
horizontalPadding;
195-
196-
const isExactPointInsidePixelRatio = Array(PIXEL_RATIO)
197-
.fill(0)
198-
.some((_value, additionalPixel) => {
199-
return pixel + additionalPixel === exactPointX;
200-
});
201-
202-
if (!isExactPointInsidePixelRatio) continue;
203-
}
204-
181+
const addPoint = (index: number, x: number) => {
205182
const value = graphData[index]!.value;
206183
const y =
207184
drawingHeight -
208185
getYInRange(drawingHeight, value, range.y) +
209186
verticalPadding;
210187

211-
points.push({ x: pixel, y: y });
188+
points.push({ x, y });
189+
};
190+
191+
let allPointsShareDate = false;
192+
let minValueIndex = 0;
193+
let maxValueIndex = 0;
194+
195+
if (endX === startX) {
196+
const firstPointTime = graphData[0]!.date.getTime();
197+
let minValue = graphData[0]!.value;
198+
let maxValue = minValue;
199+
allPointsShareDate = true;
200+
201+
for (let index = 1; index < graphData.length; index++) {
202+
const point = graphData[index]!;
203+
if (point.date.getTime() !== firstPointTime) {
204+
allPointsShareDate = false;
205+
break;
206+
}
207+
208+
if (point.value < minValue) {
209+
minValue = point.value;
210+
minValueIndex = index;
211+
}
212+
if (point.value > maxValue) {
213+
maxValue = point.value;
214+
maxValueIndex = index;
215+
}
216+
}
217+
}
218+
219+
if (allPointsShareDate) {
220+
const indices = [
221+
...new Set([0, minValueIndex, maxValueIndex, graphData.length - 1]),
222+
].sort((a, b) => a - b);
223+
indices.forEach((index) => addPoint(index, startX));
224+
} else {
225+
for (
226+
let pixel = startX;
227+
startX <= pixel && pixel <= endX;
228+
pixel = getNextPixelValue(pixel)
229+
) {
230+
const index = getGraphDataIndex(pixel);
231+
232+
// Draw first point only on the very first pixel
233+
if (index === 0 && pixel !== startX) continue;
234+
// Draw last point only on the very last pixel
235+
236+
if (index === graphData.length - 1 && pixel !== endX) continue;
237+
238+
if (index !== 0 && index !== graphData.length - 1) {
239+
// Only draw point, when the point is exact
240+
const exactPointX =
241+
getXInRange(drawingWidth, graphData[index]!.date, range.x) +
242+
horizontalPadding;
243+
244+
const isExactPointInsidePixelRatio = Array(PIXEL_RATIO)
245+
.fill(0)
246+
.some((_value, additionalPixel) => {
247+
return pixel + additionalPixel === exactPointX;
248+
});
249+
250+
if (!isExactPointInsidePixelRatio) continue;
251+
}
252+
253+
addPoint(index, pixel);
254+
}
212255
}
213256

214257
for (let i = 0; i < points.length; i++) {
215258
const point = points[i]!;
216259

217260
// first point needs to start the path
218-
if (i === 0) path.moveTo(point.x, point.y);
261+
if (i === 0) {
262+
path.moveTo(point.x, point.y);
263+
continue;
264+
}
265+
266+
if (allPointsShareDate) {
267+
path.cubicTo(point.x, point.y, point.x, point.y, point.x, point.y);
268+
continue;
269+
}
219270

220271
const prev = points[i - 1];
221272
const prevPrev = points[i - 2];

‎src/__tests__/CreateGraphPath.test.ts‎

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ jest.mock('@shopify/react-native-skia', () => ({
1313
},
1414
}));
1515

16-
import { createGraphPath } from '../CreateGraphPath';
16+
import {
17+
createGraphPath,
18+
getGraphPathRange,
19+
getPointsInRange,
20+
} from '../CreateGraphPath';
1721

1822
beforeEach(() => jest.clearAllMocks());
1923

@@ -43,3 +47,87 @@ it('creates a finite path when every graph point maps to the same pixel', () =>
4347
expect(mockPath.moveTo).toHaveBeenCalledTimes(1);
4448
expect(mockPath.moveTo.mock.calls[0]?.every(Number.isFinite)).toBe(true);
4549
});
50+
51+
it('creates a visible path when graph points share the same date', () => {
52+
const date = new Date('2023-01-01');
53+
const points = [
54+
{ date, value: 1 },
55+
{ date, value: 2 },
56+
];
57+
const range = getGraphPathRange(points);
58+
const pointsInRange = getPointsInRange(points, range);
59+
60+
createGraphPath({
61+
pointsInRange,
62+
range,
63+
horizontalPadding: 0,
64+
verticalPadding: 0,
65+
canvasHeight: 200,
66+
canvasWidth: 300,
67+
});
68+
69+
expect(pointsInRange).toEqual(points);
70+
expect(mockPath.moveTo).toHaveBeenCalledTimes(1);
71+
expect(mockPath.cubicTo).toHaveBeenCalled();
72+
expect(
73+
[...mockPath.moveTo.mock.calls, ...mockPath.cubicTo.mock.calls]
74+
.flat()
75+
.every(Number.isFinite)
76+
).toBe(true);
77+
});
78+
79+
it('keeps a large same-date path bounded without losing its value range', () => {
80+
const date = new Date('2023-01-01');
81+
const points = Array.from({ length: 10_000 }, (_, index) => ({
82+
date,
83+
value: index === 2_500 ? 0 : index === 7_500 ? 100 : 50,
84+
}));
85+
const range = getGraphPathRange(points);
86+
87+
createGraphPath({
88+
pointsInRange: getPointsInRange(points, range),
89+
range,
90+
horizontalPadding: 0,
91+
verticalPadding: 0,
92+
canvasHeight: 200,
93+
canvasWidth: 300,
94+
});
95+
96+
expect(mockPath.moveTo).toHaveBeenCalledWith(150, 100);
97+
expect(mockPath.cubicTo).toHaveBeenCalledTimes(3);
98+
expect(mockPath.cubicTo).toHaveBeenNthCalledWith(
99+
1,
100+
150,
101+
200,
102+
150,
103+
200,
104+
150,
105+
200
106+
);
107+
expect(mockPath.cubicTo).toHaveBeenNthCalledWith(2, 150, 0, 150, 0, 150, 0);
108+
expect(mockPath.cubicTo).toHaveBeenNthCalledWith(
109+
3,
110+
150,
111+
100,
112+
150,
113+
100,
114+
150,
115+
100
116+
);
117+
});
118+
119+
it('filters different dates from a zero-duration range', () => {
120+
const date = new Date('2023-01-01');
121+
const points = [
122+
{ date: new Date('2022-12-31'), value: 1 },
123+
{ date, value: 2 },
124+
{ date: new Date('2023-01-02'), value: 3 },
125+
];
126+
127+
expect(
128+
getPointsInRange(points, {
129+
x: { min: date, max: date },
130+
y: { min: 1, max: 3 },
131+
})
132+
).toEqual([points[1]]);
133+
});

0 commit comments

Comments
 (0)