diff --git a/README.md b/README.md index 2ce7287..b5148ee 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ function App() { | `range` | `GraphRange` | `undefined`. Both axes are inferred from `points`. | Always | Overrides all or part of the visible x-axis and y-axis ranges. | | `gradientFillColors` | `Color[]` | `undefined`. No area fill is drawn. | `animated={true}` | Colors for the vertical gradient below the graph line. | | `lineThickness` | `number` | `3` | Always | The graph line width in points. | +| `curve` | `'bezier' \| 'linear'` | `'bezier'` | `animated={false}` | Uses smooth Bézier interpolation or straight point-to-point segments. | | `enableFadeInMask` | `boolean` | `false` | Always | Fades in the start of the graph line. | | `enablePanGesture` | `boolean` | `false` | `animated={true}` | Lets the user press and scrub through graph points. | | `panGestureDelay` | `number` | `300` | `animated={true}` | Time in milliseconds that a press must be held before scrubbing starts. Set it to `0` to start immediately. | diff --git a/example/src/screens/GraphPage.tsx b/example/src/screens/GraphPage.tsx index 6d8af23..ef6bab8 100644 --- a/example/src/screens/GraphPage.tsx +++ b/example/src/screens/GraphPage.tsx @@ -80,6 +80,7 @@ export function GraphPage() { diff --git a/src/CreateGraphPath.ts b/src/CreateGraphPath.ts index 0c7f1c0..166f7fd 100644 --- a/src/CreateGraphPath.ts +++ b/src/CreateGraphPath.ts @@ -44,6 +44,10 @@ type GraphPathConfig = { * Range of the graph's x and y-axis */ range: GraphPathRange; + /** + * Interpolation used between graph points. + */ + curve?: 'bezier' | 'linear'; }; type GraphPathConfigWithGradient = GraphPathConfig & { @@ -140,6 +144,7 @@ function createGraphPathBase({ verticalPadding, canvasHeight: height, canvasWidth: width, + curve = 'bezier', shouldFillGradient, }: GraphPathConfigWithGradient | GraphPathConfigWithoutGradient): | SkPath @@ -214,16 +219,18 @@ function createGraphPathBase({ for (let i = 0; i < points.length; i++) { const point = points[i]!; - // first point needs to start the path - if (i === 0) path.moveTo(point.x, point.y); - - const prev = points[i - 1]; - const prevPrev = points[i - 2]; + if (i === 0) { + path.moveTo(point.x, point.y); + continue; + } - if (prev == null) continue; + if (curve === 'linear') { + path.lineTo(point.x, point.y); + continue; + } - const p0 = prevPrev ?? prev; - const p1 = prev; + const p1 = points[i - 1]!; + const p0 = points[i - 2] ?? p1; const cp1x = (2 * p0.x + p1.x) / 3; const cp1y = (2 * p0.y + p1.y) / 3; const cp2x = (p0.x + 2 * p1.x) / 3; diff --git a/src/LineGraphProps.ts b/src/LineGraphProps.ts index 4c73f1e..aae9c78 100644 --- a/src/LineGraphProps.ts +++ b/src/LineGraphProps.ts @@ -50,7 +50,12 @@ interface BaseLineGraphProps extends ViewProps { } export type StaticLineGraphProps = BaseLineGraphProps & { - /* any static-only line graph props? */ + /** + * Interpolation used between graph points. + * + * @default 'bezier' + */ + curve?: 'bezier' | 'linear'; }; export type AnimatedLineGraphProps = BaseLineGraphProps & { /** diff --git a/src/StaticLineGraph.tsx b/src/StaticLineGraph.tsx index 8a00fff..dcab7ba 100644 --- a/src/StaticLineGraph.tsx +++ b/src/StaticLineGraph.tsx @@ -17,6 +17,7 @@ export function StaticLineGraph({ color, lineThickness = 3, enableFadeInMask, + curve = 'bezier', style, ...props }: StaticLineGraphProps): React.ReactElement { @@ -50,8 +51,9 @@ export function StaticLineGraph({ canvasWidth: width, horizontalPadding: lineThickness, verticalPadding: lineThickness, + curve, }), - [height, lineThickness, pathRange, pointsInRange, width] + [curve, height, lineThickness, pathRange, pointsInRange, width] ); const gradientColors = useMemo( diff --git a/src/__tests__/CreateGraphPath.test.ts b/src/__tests__/CreateGraphPath.test.ts index dc015dd..1c14d79 100644 --- a/src/__tests__/CreateGraphPath.test.ts +++ b/src/__tests__/CreateGraphPath.test.ts @@ -43,3 +43,51 @@ it('creates a finite path when every graph point maps to the same pixel', () => expect(mockPath.moveTo).toHaveBeenCalledTimes(1); expect(mockPath.moveTo.mock.calls[0]?.every(Number.isFinite)).toBe(true); }); + +it('keeps bezier interpolation as the default', () => { + const points = [ + { date: new Date(2024, 0, 1), value: 10 }, + { date: new Date(2024, 0, 2), value: 50 }, + { date: new Date(2024, 0, 3), value: 90 }, + ]; + + createGraphPath({ + pointsInRange: points, + range: { + x: { min: points[0]!.date, max: points[2]!.date }, + y: { min: 0, max: 100 }, + }, + horizontalPadding: 0, + verticalPadding: 0, + canvasHeight: 100, + canvasWidth: 4, + }); + + expect(mockPath.cubicTo).toHaveBeenCalled(); + expect(mockPath.lineTo).not.toHaveBeenCalled(); +}); + +it('connects static graph points with straight segments when curve is linear', () => { + const points = [ + { date: new Date(2024, 0, 1), value: 10 }, + { date: new Date(2024, 0, 2), value: 50 }, + { date: new Date(2024, 0, 3), value: 90 }, + ]; + + createGraphPath({ + pointsInRange: points, + range: { + x: { min: points[0]!.date, max: points[2]!.date }, + y: { min: 0, max: 100 }, + }, + horizontalPadding: 0, + verticalPadding: 0, + canvasHeight: 100, + canvasWidth: 4, + curve: 'linear', + }); + + expect(mockPath.moveTo).toHaveBeenCalledTimes(1); + expect(mockPath.lineTo).toHaveBeenCalledTimes(2); + expect(mockPath.cubicTo).not.toHaveBeenCalled(); +}); diff --git a/src/__tests__/LineGraphProps.test.ts b/src/__tests__/LineGraphProps.test.ts index c2d4e1a..beeb8e7 100644 --- a/src/__tests__/LineGraphProps.test.ts +++ b/src/__tests__/LineGraphProps.test.ts @@ -26,4 +26,17 @@ describe('LineGraphProps', () => { expect(props.animated).toBe(true); }); + + it('accepts linear interpolation for the static renderer', () => { + const props = acceptLineGraphProps({ + points, + color: '#4484B2', + curve: 'linear', + }); + + if (props.animated === true) { + throw new Error('Expected the static graph variant'); + } + expect(props.curve).toBe('linear'); + }); });