From 090006a34c674118e88e6d910103f1d2988436a5 Mon Sep 17 00:00:00 2001 From: Wojciech Maj Date: Thu, 27 Aug 2026 23:48:23 +0200 Subject: [PATCH] Allow line marks to configure caps and joins --- .changeset/line-cap-options.md | 6 ++ API-FRICTION.md | 18 ++++ .../bundle-size/universal-baseline.json | 32 +++---- benchmarks/comparison/bundle-baseline.json | 36 ++++---- .../cases/70-composed-chart/case.json | 1 + .../cases/70-composed-chart/example.tsx | 1 + benchmarks/conformance/catalog-index.json | 1 + .../previews/70-composed-chart.svg | 2 +- benchmarks/conformance/previews/manifest.json | 6 +- docs/comparison.md | 4 +- docs/examples/lines-and-areas.md | 5 ++ docs/reference/marks/line-and-area.md | 2 + packages/charts-core/docs/comparison.md | 4 +- .../docs/examples/lines-and-areas.md | 5 ++ .../docs/reference/marks/line-and-area.md | 2 + packages/charts-core/src/canvas.test.ts | 73 +++++++++++++++ packages/charts-core/src/export.test.ts | 4 + packages/charts-core/src/line.test.ts | 89 +++++++++++++++++++ packages/charts-core/src/line.ts | 9 +- packages/charts-core/src/motion.test.ts | 46 ++++++++++ .../react-native-charts/src/SvgScene.test.tsx | 9 +- 21 files changed, 309 insertions(+), 46 deletions(-) create mode 100644 .changeset/line-cap-options.md diff --git a/.changeset/line-cap-options.md b/.changeset/line-cap-options.md new file mode 100644 index 00000000..2ea20a1a --- /dev/null +++ b/.changeset/line-cap-options.md @@ -0,0 +1,6 @@ +--- +'@tanstack/charts': minor +--- + +Add `lineCap` and `lineJoin` options to `lineY` and `lineX`. Both options use +the renderer-supported `SceneStyle` values and continue to default to `round`. diff --git a/API-FRICTION.md b/API-FRICTION.md index 5204435b..14c78c25 100644 --- a/API-FRICTION.md +++ b/API-FRICTION.md @@ -331,6 +331,7 @@ Each entry records: | F-292 | Fixed preview paints ignored the selected site theme | Tooling | resolved | | F-293 | Root scale slots blocked named axes | API | resolved | | F-294 | Automatic mark renderers imposed shared host plumbing | API | resolved | +| F-295 | Line marks forced round endpoints | API | resolved | ## Findings @@ -8505,3 +8506,20 @@ Each entry records: React Native Metro gates, and framework package checks pass. Bundle boundary checks keep SVG-only entries free of Canvas and measure the opt-in mixed representative and React consumers at 35.68 KiB and 41.63 KiB gzip. + +### F-295 - Line marks forced round endpoints + +- Status: resolved +- Severity: medium +- Owner: API +- Observed in: migrating Rewardo's composed statistics chart from Recharts +- Friction: `lineY` and `lineX` exposed stroke width, opacity, dash pattern, + and curve options, but hard-coded round caps and joins. Reproducing the + previous Recharts line, which inherited SVG's butt cap, required replacing + or patching rendered output. +- Decision: expose `lineCap` and `lineJoin` on the shared line options using + the values from `SceneStyle`. Preserve round as the default for both options. +- Verification: public type tests bind both line APIs to the scene renderer's + supported values. Scene, SVG, Canvas, motion, export, and React Native tests + preserve configured values. The Rewardo chart can use `lineCap: 'butt'` on + its existing pink `lineY` mark. diff --git a/benchmarks/bundle-size/universal-baseline.json b/benchmarks/bundle-size/universal-baseline.json index 486ae8f9..8f88756c 100644 --- a/benchmarks/bundle-size/universal-baseline.json +++ b/benchmarks/bundle-size/universal-baseline.json @@ -3,16 +3,16 @@ "policy": "Exact minified and gzip output for entries that optional features must not affect. Review every change before updating.", "bundles": { "D3-scale line scene": { - "bytes": 50813, - "gzip": 19070 + "bytes": 50836, + "gzip": 19079 }, "D3-scale line + static SVG": { - "bytes": 55544, - "gzip": 20776 + "bytes": 55567, + "gzip": 20785 }, "Representative marks": { - "bytes": 75617, - "gzip": 27582 + "bytes": 75640, + "gzip": 27591 }, "TanStack DOM host": { "bytes": 74947, @@ -23,24 +23,24 @@ "gzip": 26831 }, "React line consumer": { - "bytes": 100766, - "gzip": 36222 + "bytes": 100789, + "gzip": 36231 }, "Compact-scale line scene": { - "bytes": 33265, - "gzip": 11935 + "bytes": 33288, + "gzip": 11944 }, "React compact-scale line consumer": { - "bytes": 83266, - "gzip": 29144 + "bytes": 83289, + "gzip": 29153 }, "Custom-scale line scene": { - "bytes": 31447, - "gzip": 11193 + "bytes": 31470, + "gzip": 11201 }, "D3 linear-scale line scene": { - "bytes": 50745, - "gzip": 19032 + "bytes": 50768, + "gzip": 19041 } } } diff --git a/benchmarks/comparison/bundle-baseline.json b/benchmarks/comparison/bundle-baseline.json index a22a54c1..2087d60d 100644 --- a/benchmarks/comparison/bundle-baseline.json +++ b/benchmarks/comparison/bundle-baseline.json @@ -1,8 +1,8 @@ { "schemaVersion": 4, - "generatedAt": "2026-08-26T21:51:12.832Z", + "generatedAt": "2026-08-27T21:38:32.371Z", "packageVersions": { - "tanstack": "0.15.0", + "tanstack": "0.16.0", "chartjs": "4.5.1", "echarts": "6.1.0", "recharts": "3.10.1", @@ -12,7 +12,7 @@ "tanstack": { "kind": "workspace", "revision": "3df87d71f0305e5a450c10b66940f76f3e14259a", - "inputDigest": "sha256:b1016e599e63d05e1647b9d27adabaaed7a38bb44dac8d6ba0fef06b8b8bd38e" + "inputDigest": "sha256:a5332d205ee6b678314f0a444dc2198cb002c6b55b49c7bf1c5acd0ea7ed1783" }, "chartjs": { "kind": "package", @@ -45,25 +45,25 @@ }, "bundles": { "tanstack-line-basic": { - "minifiedBytes": 110431, - "gzipBytes": 39891, - "brotliBytes": 35325, - "incrementalGzipBytes": 39891, - "incrementalBrotliBytes": 35325 + "minifiedBytes": 110454, + "gzipBytes": 39901, + "brotliBytes": 35367, + "incrementalGzipBytes": 39901, + "incrementalBrotliBytes": 35367 }, "tanstack-line-interactive": { - "minifiedBytes": 115872, - "gzipBytes": 41648, - "brotliBytes": 36746, - "incrementalGzipBytes": 41648, - "incrementalBrotliBytes": 36746 + "minifiedBytes": 115895, + "gzipBytes": 41661, + "brotliBytes": 36828, + "incrementalGzipBytes": 41661, + "incrementalBrotliBytes": 36828 }, "tanstack-line-advanced": { - "minifiedBytes": 123062, - "gzipBytes": 43974, - "brotliBytes": 38764, - "incrementalGzipBytes": 43974, - "incrementalBrotliBytes": 38764 + "minifiedBytes": 123085, + "gzipBytes": 43988, + "brotliBytes": 38803, + "incrementalGzipBytes": 43988, + "incrementalBrotliBytes": 38803 }, "tanstack-bar-basic": { "minifiedBytes": 119225, diff --git a/benchmarks/conformance/cases/70-composed-chart/case.json b/benchmarks/conformance/cases/70-composed-chart/case.json index 62dac152..b80c5320 100644 --- a/benchmarks/conformance/cases/70-composed-chart/case.json +++ b/benchmarks/conformance/cases/70-composed-chart/case.json @@ -13,6 +13,7 @@ "left axis and stacked right axes", "daily observations", "monotone curves", + "butt line cap", "resolved maximum bar thickness" ], "geometry": [ diff --git a/benchmarks/conformance/cases/70-composed-chart/example.tsx b/benchmarks/conformance/cases/70-composed-chart/example.tsx index 02187acb..c92d3bbc 100644 --- a/benchmarks/conformance/cases/70-composed-chart/example.tsx +++ b/benchmarks/conformance/cases/70-composed-chart/example.tsx @@ -41,6 +41,7 @@ export const createExampleChart = (input: ChartOptions) => { y: 'temp_min', stroke: '#ff7300', strokeWidth: 2, + lineCap: 'butt', curve: monotone, }), dot(rows, { diff --git a/benchmarks/conformance/catalog-index.json b/benchmarks/conformance/catalog-index.json index 9d3c84f0..54a371a0 100644 --- a/benchmarks/conformance/catalog-index.json +++ b/benchmarks/conformance/catalog-index.json @@ -2394,6 +2394,7 @@ "left axis and stacked right axes", "daily observations", "monotone curves", + "butt line cap", "resolved maximum bar thickness" ], "geometry": [ diff --git a/benchmarks/conformance/previews/70-composed-chart.svg b/benchmarks/conformance/previews/70-composed-chart.svg index 5de7df71..091ffce8 100644 --- a/benchmarks/conformance/previews/70-composed-chart.svg +++ b/benchmarks/conformance/previews/70-composed-chart.svg @@ -1 +1 @@ - + diff --git a/benchmarks/conformance/previews/manifest.json b/benchmarks/conformance/previews/manifest.json index e1176205..8f98afe7 100644 --- a/benchmarks/conformance/previews/manifest.json +++ b/benchmarks/conformance/previews/manifest.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "width": 288, "height": 192, - "sourceHash": "d3177faf65fca2414b431c195a003cf9afdceb8ae2f1b37e4f8cb81578dc8293", + "sourceHash": "99895d05b94419426e1065524564e39ba82fc1cdeae9c5875b4718ba6aeae5ee", "assets": [ { "id": "01-line-gaps", @@ -306,8 +306,8 @@ }, { "id": "70-composed-chart", - "sha256": "642139631cae72ef76ff81e148e6d40e2ff83225a11b582a32878d0f166beae7", - "bytes": 14436 + "sha256": "db93c404c4e097cad237dca330dab72a1080f1e38e873755ef7ca86a81bbeb30", + "bytes": 14435 }, { "id": "71-recharts-population-pyramid", diff --git a/docs/comparison.md b/docs/comparison.md index 912adf20..83f66b70 100644 --- a/docs/comparison.md +++ b/docs/comparison.md @@ -90,7 +90,7 @@ output model. ## Bundle snapshot -Baseline date: `2026-08-26`. +Baseline date: `2026-08-27`. Controlled ranges cover 12 independently built, minified browser consumers: line, bar, area, and scatter at basic, interactive, and advanced tiers. Only @@ -106,7 +106,7 @@ Vega-Lite, AG Charts, and uPlot main exports were read from Bundlephobia on July | Library | Bundle size | React externalized | Evidence | | ------------------ | -------------------------------------- | -----------------: | ---------------------------------------------------------- | -| TanStack Charts | 38.96–44.98 KiB | Not applicable | Controlled suite | +| TanStack Charts | 38.97–44.98 KiB | Not applicable | Controlled suite | | D3 | 90 KB gzip | — | External main export | | Chart.js | 44.70–58.21 KiB | — | Controlled suite | | Apache ECharts | 153.10–173.18 KiB | — | Controlled suite | diff --git a/docs/examples/lines-and-areas.md b/docs/examples/lines-and-areas.md index 03e8bd35..d72d0323 100644 --- a/docs/examples/lines-and-areas.md +++ b/docs/examples/lines-and-areas.md @@ -48,6 +48,7 @@ const chart = defineChart({ y: 'downloads', points: true, stroke: '#2563eb', + lineCap: 'butt', }), ], scales: { @@ -64,6 +65,10 @@ const chart = defineChart({ export default chart ``` +`lineCap: 'butt'` ends the stroke at each path endpoint. Omit it to retain the +default round endpoints. `lineX` accepts the same `lineCap` and `lineJoin` +options. + ## Compare several series from a common baseline Indexing each series to its first observation compares relative change when the diff --git a/docs/reference/marks/line-and-area.md b/docs/reference/marks/line-and-area.md index 2f776f67..7a3c4978 100644 --- a/docs/reference/marks/line-and-area.md +++ b/docs/reference/marks/line-and-area.md @@ -55,6 +55,8 @@ function lineY( | `strokeOpacity` | `number` | SVG default | Stroke opacity | | `strokeWidth` | `number` | `2.25` | Stroke width | | `strokeDasharray` | `string` | None | SVG dash array | +| `lineCap` | `SceneStyle["lineCap"]` | `"round"` | Stroke endpoint shape | +| `lineJoin` | `SceneStyle["lineJoin"]` | `"round"` | Stroke corner shape | | `points` | `boolean` | `false` | Draws a radius-`2.5` dot at each valid point | | `curve` | `ChartCurve` | Straight segments | Optional path generator | | `states` | `readonly ChartMarkState[]` | None | Focus-driven presentation overrides | diff --git a/packages/charts-core/docs/comparison.md b/packages/charts-core/docs/comparison.md index 912adf20..83f66b70 100644 --- a/packages/charts-core/docs/comparison.md +++ b/packages/charts-core/docs/comparison.md @@ -90,7 +90,7 @@ output model. ## Bundle snapshot -Baseline date: `2026-08-26`. +Baseline date: `2026-08-27`. Controlled ranges cover 12 independently built, minified browser consumers: line, bar, area, and scatter at basic, interactive, and advanced tiers. Only @@ -106,7 +106,7 @@ Vega-Lite, AG Charts, and uPlot main exports were read from Bundlephobia on July | Library | Bundle size | React externalized | Evidence | | ------------------ | -------------------------------------- | -----------------: | ---------------------------------------------------------- | -| TanStack Charts | 38.96–44.98 KiB | Not applicable | Controlled suite | +| TanStack Charts | 38.97–44.98 KiB | Not applicable | Controlled suite | | D3 | 90 KB gzip | — | External main export | | Chart.js | 44.70–58.21 KiB | — | Controlled suite | | Apache ECharts | 153.10–173.18 KiB | — | Controlled suite | diff --git a/packages/charts-core/docs/examples/lines-and-areas.md b/packages/charts-core/docs/examples/lines-and-areas.md index 03e8bd35..d72d0323 100644 --- a/packages/charts-core/docs/examples/lines-and-areas.md +++ b/packages/charts-core/docs/examples/lines-and-areas.md @@ -48,6 +48,7 @@ const chart = defineChart({ y: 'downloads', points: true, stroke: '#2563eb', + lineCap: 'butt', }), ], scales: { @@ -64,6 +65,10 @@ const chart = defineChart({ export default chart ``` +`lineCap: 'butt'` ends the stroke at each path endpoint. Omit it to retain the +default round endpoints. `lineX` accepts the same `lineCap` and `lineJoin` +options. + ## Compare several series from a common baseline Indexing each series to its first observation compares relative change when the diff --git a/packages/charts-core/docs/reference/marks/line-and-area.md b/packages/charts-core/docs/reference/marks/line-and-area.md index 2f776f67..7a3c4978 100644 --- a/packages/charts-core/docs/reference/marks/line-and-area.md +++ b/packages/charts-core/docs/reference/marks/line-and-area.md @@ -55,6 +55,8 @@ function lineY( | `strokeOpacity` | `number` | SVG default | Stroke opacity | | `strokeWidth` | `number` | `2.25` | Stroke width | | `strokeDasharray` | `string` | None | SVG dash array | +| `lineCap` | `SceneStyle["lineCap"]` | `"round"` | Stroke endpoint shape | +| `lineJoin` | `SceneStyle["lineJoin"]` | `"round"` | Stroke corner shape | | `points` | `boolean` | `false` | Draws a radius-`2.5` dot at each valid point | | `curve` | `ChartCurve` | Straight segments | Optional path generator | | `states` | `readonly ChartMarkState[]` | None | Focus-driven presentation overrides | diff --git a/packages/charts-core/src/canvas.test.ts b/packages/charts-core/src/canvas.test.ts index 5ce7d73c..99609d49 100644 --- a/packages/charts-core/src/canvas.test.ts +++ b/packages/charts-core/src/canvas.test.ts @@ -77,6 +77,79 @@ afterEach(() => { }) describe('Canvas renderer', () => { + it('paints authored caps and joins for both line directions', () => { + const scene = createChartScene( + defineChart({ + marks: [ + lineY([4, 9], { lineCap: 'butt', lineJoin: 'bevel' }), + lineX([4, 9], { lineCap: 'square', lineJoin: 'miter' }), + ], + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + }, + guides: false, + }), + { width: 300, height: 180 }, + ) + const container = document.createElement('div') + const surface = createCanvasChartRenderer().mount(container, () => {}) + + surface.render(scene, renderOptions()) + + const canvas = container.querySelector( + '.ts-chart-canvas__scene', + ) + const painted = canvas ? contexts.get(canvas) : undefined + if (!painted) throw new Error('Expected a painted scene canvas') + expect(painted.operations).toEqual( + expect.arrayContaining([ + 'lineCap:butt', + 'lineJoin:bevel', + 'lineCap:square', + 'lineJoin:miter', + ]), + ) + + surface.destroy() + }) + + it('paints round caps and joins by default for both line directions', () => { + const scene = createChartScene( + defineChart({ + marks: [lineY([4, 9]), lineX([4, 9])], + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + }, + guides: false, + }), + { width: 300, height: 180 }, + ) + const container = document.createElement('div') + const surface = createCanvasChartRenderer().mount(container, () => {}) + + surface.render(scene, renderOptions()) + + const canvas = container.querySelector( + '.ts-chart-canvas__scene', + ) + const painted = canvas ? contexts.get(canvas) : undefined + if (!painted) throw new Error('Expected a painted scene canvas') + expect( + painted.operations.filter((operation) => + operation.startsWith('lineCap:'), + ), + ).toEqual(['lineCap:round', 'lineCap:round']) + expect( + painted.operations.filter((operation) => + operation.startsWith('lineJoin:'), + ), + ).toEqual(['lineJoin:round', 'lineJoin:round']) + + surface.destroy() + }) + it('composes renderer-tagged marks in source order', () => { const data = [ { id: 'a', category: 'A', value: 3 }, diff --git a/packages/charts-core/src/export.test.ts b/packages/charts-core/src/export.test.ts index 0e64ed83..2218614b 100644 --- a/packages/charts-core/src/export.test.ts +++ b/packages/charts-core/src/export.test.ts @@ -18,6 +18,8 @@ describe('optional export', () => { defineChart({ marks: [ lineY([1, 3, 2], { + lineCap: 'butt', + lineJoin: 'bevel', stroke: 'currentColor', }), ], @@ -34,6 +36,8 @@ describe('optional export', () => { expect(result).toContain('width="480"') expect(result).toContain('height="260"') expect(result).toContain('aria-label="Exported chart"') + expect(result).toContain('stroke-linecap="butt"') + expect(result).toContain('stroke-linejoin="bevel"') expect(result).not.toContain('data-ts-focus-layer') }) diff --git a/packages/charts-core/src/line.test.ts b/packages/charts-core/src/line.test.ts index 7d897ad3..3b67d052 100644 --- a/packages/charts-core/src/line.test.ts +++ b/packages/charts-core/src/line.test.ts @@ -5,11 +5,13 @@ import { d3Curve } from './d3-shape' import { lineX, lineY } from './line' import { createChartScene, defineChart } from './scene' import { renderChartSvg } from './svg' +import type { LineXOptions, LineYOptions } from './line' import type { ChartDefinition, ChartMark, SceneNode, ScenePolyline, + SceneStyle, } from './types' interface Row { @@ -40,6 +42,75 @@ const rows: readonly Row[] = [ }, ] +describe('line presentation', () => { + it('applies authored caps and joins to vertical and horizontal lines', () => { + const scene = createChartScene( + defineChart({ + marks: [ + lineY([4, 9], { + id: 'vertical', + lineCap: 'butt', + lineJoin: 'bevel', + }), + lineX([4, 9], { + id: 'horizontal', + lineCap: 'square', + lineJoin: 'miter-clip', + }), + ], + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + }, + }), + { width: 480, height: 260 }, + ) + const lines = flatten(scene.nodes).filter( + (node): node is ScenePolyline => node.kind === 'polyline', + ) + const svg = renderChartSvg(scene, { ariaLabel: 'Line presentation' }) + + expect(lines.map(({ style }) => style)).toEqual([ + expect.objectContaining({ lineCap: 'butt', lineJoin: 'bevel' }), + expect.objectContaining({ lineCap: 'square', lineJoin: 'miter-clip' }), + ]) + expect(svg).toContain('stroke-linecap="butt" stroke-linejoin="bevel"') + expect(svg).toContain( + 'stroke-linecap="square" stroke-linejoin="miter-clip"', + ) + }) + + it('keeps round caps and joins as the default', () => { + const scene = createChartScene( + defineChart({ + marks: [ + lineY([4, 9], { id: 'vertical' }), + lineX([4, 9], { id: 'horizontal' }), + ], + scales: { + x: { scale: scaleLinear }, + y: { scale: scaleLinear }, + }, + }), + { width: 480, height: 260 }, + ) + const lines = flatten(scene.nodes).filter( + (node): node is ScenePolyline => node.kind === 'polyline', + ) + const svg = renderChartSvg(scene, { + ariaLabel: 'Default line presentation', + }) + + expect(lines.map(({ style }) => style)).toEqual([ + expect.objectContaining({ lineCap: 'round', lineJoin: 'round' }), + expect.objectContaining({ lineCap: 'round', lineJoin: 'round' }), + ]) + expect( + svg.match(/stroke-linecap="round" stroke-linejoin="round"/g), + ).toHaveLength(2) + }) +}) + describe('lineX mark', () => { it('transposes the implicit value and index channels from lineY', () => { const values = [4, 9, 7] @@ -232,6 +303,24 @@ if (false) { expectTypeOf(numeric).toEqualTypeOf>() expectTypeOf(temporal).toEqualTypeOf>() + expectTypeOf['lineCap']>().toEqualTypeOf< + SceneStyle['lineCap'] + >() + expectTypeOf['lineJoin']>().toEqualTypeOf< + SceneStyle['lineJoin'] + >() + expectTypeOf['lineCap']>().toEqualTypeOf< + SceneStyle['lineCap'] + >() + expectTypeOf['lineJoin']>().toEqualTypeOf< + SceneStyle['lineJoin'] + >() + + // @ts-expect-error Line caps must use a renderer-supported scene value. + lineY(rows, { x: 'at', y: 'value', lineCap: 'flat' }) + + // @ts-expect-error Line joins must use a renderer-supported scene value. + lineX(rows, { x: 'value', y: 'at', lineJoin: 'sharp' }) // @ts-expect-error Horizontal line values must be numeric. lineX(rows, { x: 'category', y: 'at' }) diff --git a/packages/charts-core/src/line.ts b/packages/charts-core/src/line.ts index 1734f929..937d7e52 100644 --- a/packages/charts-core/src/line.ts +++ b/packages/charts-core/src/line.ts @@ -13,6 +13,7 @@ import type { Channel, CartesianChartMark, CartesianScaleBindings, + ChartCurve, ChartKey, ChartMark, ChartMarkMotionOptions, @@ -24,7 +25,7 @@ import type { MarkChannelOutput, MarkScaleBindings, SceneNode, - ChartCurve, + SceneStyle, VisualChannel, } from './types' @@ -38,6 +39,8 @@ interface LineOptions strokeOpacity?: number strokeWidth?: number strokeDasharray?: string + lineCap?: SceneStyle['lineCap'] + lineJoin?: SceneStyle['lineJoin'] points?: boolean curve?: ChartCurve states?: readonly ChartMarkState>[] @@ -299,8 +302,8 @@ function createLineMark( strokeOpacity: options.strokeOpacity, strokeWidth: options.strokeWidth ?? 2.25, strokeDasharray: options.strokeDasharray, - lineCap: 'round', - lineJoin: 'round', + lineCap: options.lineCap || 'round', + lineJoin: options.lineJoin || 'round', }, }) segment = [] diff --git a/packages/charts-core/src/motion.test.ts b/packages/charts-core/src/motion.test.ts index a8f24f91..f8e3058a 100644 --- a/packages/charts-core/src/motion.test.ts +++ b/packages/charts-core/src/motion.test.ts @@ -28,6 +28,52 @@ const rows = [ ] describe('SVG motion', () => { + it('applies discrete line cap and join revisions synchronously', () => { + const definition = ( + lineCap: 'butt' | 'round', + lineJoin: 'bevel' | 'round', + ) => + defineChart({ + motion: { + transition: { type: 'tween', duration: 100, easing: 'linear' }, + }, + marks: [ + lineY(rows, { + id: 'series', + x: 'category', + y: 'value', + key: 'id', + lineCap, + lineJoin, + }), + ], + scales: { + x: { scale: scaleBand().domain(['A', 'B']) }, + y: { scale: scaleLinear().domain([0, 100]) }, + }, + guides: false, + }) + const first = createChartScene(definition('round', 'round'), { + width: 300, + height: 200, + }) + const next = createChartScene(definition('butt', 'bevel'), { + width: 300, + height: 200, + }) + const container = document.createElement('div') + const surface = motion({ initial: false }).mount(container, () => {}) + surface.render(first, { ariaLabel: 'Line style motion' }) + const line = container.querySelector('g.ts-chart__line > path') + + surface.render(next, { ariaLabel: 'Line style motion' }) + + expect(container.querySelector('g.ts-chart__line > path')).toBe(line) + expect(line?.getAttribute('stroke-linecap')).toBe('butt') + expect(line?.getAttribute('stroke-linejoin')).toBe('bevel') + surface.destroy() + }) + it('maps client coordinates through the rendered SVG transform', () => { const scene = createChartScene( defineChart({ diff --git a/packages/react-native-charts/src/SvgScene.test.tsx b/packages/react-native-charts/src/SvgScene.test.tsx index 272eab7a..5e6924db 100644 --- a/packages/react-native-charts/src/SvgScene.test.tsx +++ b/packages/react-native-charts/src/SvgScene.test.tsx @@ -37,6 +37,8 @@ describe('React Native SVG scene renderer', () => { expect(markup).toContain('