From d93b78389119ab57542b49718fe9cfd11bf8b3ec Mon Sep 17 00:00:00 2001 From: Harris Robin Kalash Date: Sat, 29 Aug 2026 13:11:31 +0100 Subject: [PATCH] feat: support linear static graph curves --- README.md | 1 + src/CreateGraphPath.ts | 33 ++++++++++++++++++++++++--- src/LineGraphProps.ts | 8 ++++++- src/StaticLineGraph.tsx | 4 +++- src/__tests__/CreateGraphPath.test.ts | 25 ++++++++++++++++++++ src/index.ts | 1 + 6 files changed, 67 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 2ce7287..b2fd611 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ function App() { | Prop | Type | Default | Availability | Description | | ------------------------- | ------------------------------------------ | -------------------------------------------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | `animated` | `boolean` | `false` | Always | Uses the animated renderer when `true` and the lightweight static renderer when `false`. | +| `curve` | `'bezier' \| 'linear'` | `'bezier'` | `animated={false}` | Controls how adjacent points are connected. | | `points` | `GraphPoint[]` | None. Required. | Always | The points to draw. Each point contains a numeric `value` and a `Date`. The graph scales to fit them unless `range` overrides an axis. | | `color` | `string` | None. Required. | Always | The graph line color. | | `range` | `GraphRange` | `undefined`. Both axes are inferred from `points`. | Always | Overrides all or part of the visible x-axis and y-axis ranges. | diff --git a/src/CreateGraphPath.ts b/src/CreateGraphPath.ts index 0c7f1c0..7992975 100644 --- a/src/CreateGraphPath.ts +++ b/src/CreateGraphPath.ts @@ -1,6 +1,6 @@ import { Skia } from '@shopify/react-native-skia'; import type { SkPath, SkPoint } from '@shopify/react-native-skia'; -import type { GraphPoint, GraphRange } from './LineGraphProps'; +import type { GraphCurve, GraphPoint, GraphRange } from './LineGraphProps'; const PIXEL_RATIO = 2; @@ -44,6 +44,10 @@ type GraphPathConfig = { * Range of the graph's x and y-axis */ range: GraphPathRange; + /** + * Path used to connect adjacent graph points. + */ + curve?: GraphCurve; }; type GraphPathConfigWithGradient = GraphPathConfig & { @@ -140,6 +144,7 @@ function createGraphPathBase({ verticalPadding, canvasHeight: height, canvasWidth: width, + curve = 'bezier', shouldFillGradient, }: GraphPathConfigWithGradient | GraphPathConfigWithoutGradient): | SkPath @@ -153,14 +158,36 @@ function createGraphPathBase({ if (graphData[0] == null) return path; - const points: SkPoint[] = []; - const startX = getXInRange(drawingWidth, graphData[0]!.date, range.x) + horizontalPadding; const endX = getXInRange(drawingWidth, graphData[graphData.length - 1]!.date, range.x) + horizontalPadding; + if (curve === 'linear') { + for (let index = 0; index < graphData.length; index++) { + const point = graphData[index]!; + const x = + getXInRange(drawingWidth, point.date, range.x) + horizontalPadding; + const y = + drawingHeight - + getYInRange(drawingHeight, point.value, range.y) + + verticalPadding; + + if (index === 0) path.moveTo(x, y); + else path.lineTo(x, y); + } + + if (!shouldFillGradient) return path; + + const gradientPath = path.copy(); + gradientPath.lineTo(endX, height + verticalPadding); + gradientPath.lineTo(0 + horizontalPadding, height + verticalPadding); + return { path, gradientPath }; + } + + const points: SkPoint[] = []; + const getGraphDataIndex = (pixel: number) => endX === startX ? 0 diff --git a/src/LineGraphProps.ts b/src/LineGraphProps.ts index 4c73f1e..c5c40e4 100644 --- a/src/LineGraphProps.ts +++ b/src/LineGraphProps.ts @@ -10,6 +10,7 @@ export interface GraphPoint { } export type GraphRange = Partial; +export type GraphCurve = 'bezier' | 'linear'; export interface SelectionDotProps { isActive: SharedValue; @@ -50,7 +51,12 @@ interface BaseLineGraphProps extends ViewProps { } export type StaticLineGraphProps = BaseLineGraphProps & { - /* any static-only line graph props? */ + /** + * Path used to connect adjacent graph points. + * + * @default 'bezier' + */ + curve?: GraphCurve; }; export type AnimatedLineGraphProps = BaseLineGraphProps & { /** diff --git a/src/StaticLineGraph.tsx b/src/StaticLineGraph.tsx index 8a00fff..81ea230 100644 --- a/src/StaticLineGraph.tsx +++ b/src/StaticLineGraph.tsx @@ -14,6 +14,7 @@ import type { StaticLineGraphProps } from './LineGraphProps'; export function StaticLineGraph({ points: allPoints, range, + curve = 'bezier', color, lineThickness = 3, enableFadeInMask, @@ -46,12 +47,13 @@ export function StaticLineGraph({ createGraphPath({ pointsInRange: pointsInRange, range: pathRange, + curve, canvasHeight: height, canvasWidth: width, horizontalPadding: lineThickness, verticalPadding: lineThickness, }), - [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..9eb70fc 100644 --- a/src/__tests__/CreateGraphPath.test.ts +++ b/src/__tests__/CreateGraphPath.test.ts @@ -43,3 +43,28 @@ 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('connects measured samples directly when curve is linear', () => { + const result = createGraphPath({ + pointsInRange: [ + { date: new Date(0), value: 10 }, + { date: new Date(1_000), value: 20 }, + { date: new Date(10_000), value: 15 }, + ], + range: { + x: { min: new Date(0), max: new Date(10_000) }, + y: { min: 10, max: 20 }, + }, + curve: 'linear', + horizontalPadding: 0, + verticalPadding: 0, + canvasHeight: 100, + canvasWidth: 100, + }); + + expect(result).toBe(mockPath); + expect(mockPath.moveTo).toHaveBeenCalledWith(0, 100); + expect(mockPath.lineTo).toHaveBeenNthCalledWith(1, 10, 0); + expect(mockPath.lineTo).toHaveBeenNthCalledWith(2, 100, 50); + expect(mockPath.cubicTo).not.toHaveBeenCalled(); +}); diff --git a/src/index.ts b/src/index.ts index 31a54a7..c90cdc0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ export * from './SelectionDot'; export * from './LineGraph'; export type { + GraphCurve, GraphPoint, LineGraphProps, SelectionDotProps,