From 7848b5e70ff78ec63b1fcc5cf89be0903e68386f Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Fri, 13 Dec 2024 09:27:07 -0500 Subject: [PATCH 01/55] feat: Add new `renderPathData()` canvas util to simplify rendering SVG path data onto canvas context with CSS class support --- .changeset/dry-masks-suffer.md | 5 ++ .changeset/shaggy-rocks-tan.md | 5 ++ packages/layerchart/src/lib/utils/canvas.ts | 56 +++++++++++++++++++++ packages/layerchart/src/lib/utils/index.ts | 2 + 4 files changed, 68 insertions(+) create mode 100644 .changeset/dry-masks-suffer.md create mode 100644 .changeset/shaggy-rocks-tan.md create mode 100644 packages/layerchart/src/lib/utils/canvas.ts diff --git a/.changeset/dry-masks-suffer.md b/.changeset/dry-masks-suffer.md new file mode 100644 index 000000000..b26cad3b8 --- /dev/null +++ b/.changeset/dry-masks-suffer.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +feat: Add new `renderPathData()` canvas util to simplify rendering SVG path data onto canvas context with CSS class support diff --git a/.changeset/shaggy-rocks-tan.md b/.changeset/shaggy-rocks-tan.md new file mode 100644 index 000000000..891b504d9 --- /dev/null +++ b/.changeset/shaggy-rocks-tan.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +Add `clearCanvasContext()` util diff --git a/packages/layerchart/src/lib/utils/canvas.ts b/packages/layerchart/src/lib/utils/canvas.ts new file mode 100644 index 000000000..b20e54aa8 --- /dev/null +++ b/packages/layerchart/src/lib/utils/canvas.ts @@ -0,0 +1,56 @@ +const DEFAULT_FILL = 'rgb(0, 0, 0)'; + +/** Render SVG path data onto canvas context. Supports CSS classes by tranferring to `` element for retrieval) */ +export function renderPathData( + canvasCtx: CanvasRenderingContext2D, + pathData: string | null, + props: { fill?: string; stroke?: string; strokeWidth?: number; class?: string } = {} +) { + let computedStyles: Partial = {}; + + // Transfer classes defined on //etc to to enable window.getComputedStyle() retrieval (Tailwind classes, etc) + if (props.class) { + canvasCtx.canvas.classList.add(...props.class.split(' ')); + computedStyles = window.getComputedStyle(canvasCtx.canvas); + } + + const path = new Path2D(pathData ?? ''); + + const fill = props.fill ?? (computedStyles.fill !== DEFAULT_FILL ? computedStyles.fill : null); + if (fill) { + canvasCtx.fillStyle = fill; + canvasCtx.fill(path); + } + + const stroke = + props.stroke ?? (computedStyles.stroke === 'none' ? null : (computedStyles.stroke ?? null)); + if (stroke) { + canvasCtx.lineWidth = + props.strokeWidth ?? Number(computedStyles.strokeWidth?.replace('px', '')); + canvasCtx.strokeStyle = stroke; + canvasCtx.stroke(path); + } +} + +/** Clear canvas accounting for Canvas `context.translate(...)` */ +export function clearCanvasContext( + canvasCtx: CanvasRenderingContext2D, + options: { + containerWidth: number; + containerHeight: number; + padding: { + top: number; + bottom: number; + left: number; + right: number; + }; + } +) { + // Clear with negative offset due to Canvas `context.translate(...)` + canvasCtx.clearRect( + -options.padding.left, + -options.padding.top, + options.containerWidth, + options.containerHeight + ); +} diff --git a/packages/layerchart/src/lib/utils/index.ts b/packages/layerchart/src/lib/utils/index.ts index 749c156b1..817db2b70 100644 --- a/packages/layerchart/src/lib/utils/index.ts +++ b/packages/layerchart/src/lib/utils/index.ts @@ -1,8 +1,10 @@ +export * from './canvas.js'; export * from './common.js'; export * from './geo.js'; export * from './graph.js'; export * from './hierarchy.js'; export * from './math.js'; +export * from './path.js'; export * from './pivot.js'; export * from './stack.js'; export * from './ticks.js'; From 403a1fea65d131de3c17160d65da5eaf1937949e Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Fri, 13 Dec 2024 09:27:18 -0500 Subject: [PATCH 02/55] fix(circlePath): Correctly handle sweep argument --- .changeset/chilly-moles-wave.md | 5 +++++ packages/layerchart/src/lib/utils/path.ts | 9 ++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 .changeset/chilly-moles-wave.md diff --git a/.changeset/chilly-moles-wave.md b/.changeset/chilly-moles-wave.md new file mode 100644 index 000000000..284fdec8d --- /dev/null +++ b/.changeset/chilly-moles-wave.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +fix(circlePath): Correctly handle sweep argument diff --git a/packages/layerchart/src/lib/utils/path.ts b/packages/layerchart/src/lib/utils/path.ts index d0b77e5d6..204adaed5 100644 --- a/packages/layerchart/src/lib/utils/path.ts +++ b/packages/layerchart/src/lib/utils/path.ts @@ -21,12 +21,15 @@ export function circlePath(dimensions: { r: number; sweep?: 'inside' | 'outside'; }) { - // sweep: 0 (inside), 1 (outside) + // https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths#arcs const { cx, cy, r, sweep = 'outside' } = dimensions; + // sweep: 0 (inside), 1 (outside) + const _sweep = sweep === 'outside' ? 1 : 0; + return ` M ${cx - r} ${cy} - a ${r},${r} 0 1,${sweep} ${r * 2},0 - a ${r},${r} 0 1,${sweep} -${r * 2},0 + a ${r},${r} 0 1,${_sweep} ${r * 2},0 + a ${r},${r} 0 1,${_sweep} -${r * 2},0 `; } From fc84ef81893f16ca981def6818e228d3f3a684e6 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Fri, 13 Dec 2024 09:29:20 -0500 Subject: [PATCH 03/55] feat(Spline): Support Canvas context --- .changeset/moody-deers-wonder.md | 5 + .../src/lib/components/Spline.svelte | 146 +++++++++++------- .../routes/docs/examples/Line/+page.svelte | 29 ++++ 3 files changed, 122 insertions(+), 58 deletions(-) create mode 100644 .changeset/moody-deers-wonder.md diff --git a/.changeset/moody-deers-wonder.md b/.changeset/moody-deers-wonder.md new file mode 100644 index 000000000..6dede60c2 --- /dev/null +++ b/.changeset/moody-deers-wonder.md @@ -0,0 +1,5 @@ +--- +'layerchart': minor +--- + +feat(Spline): Support Canvas context diff --git a/packages/layerchart/src/lib/components/Spline.svelte b/packages/layerchart/src/lib/components/Spline.svelte index bd719d5c9..5072e4f08 100644 --- a/packages/layerchart/src/lib/components/Spline.svelte +++ b/packages/layerchart/src/lib/components/Spline.svelte @@ -1,6 +1,6 @@ -{#key key} - - - - - {#if markerStart} +{#if renderContext === 'svg'} + {#key key} + + + + + {#if markerStart} + + {/if} + + + - {/if} - + - - - + + + - - - - - {#if $$slots.start && $startPoint} - - - - {/if} - - {#if $$slots.end && $endPoint} - - - - {/if} -{/key} + {#if $$slots.start && $startPoint} + + + + {/if} + + {#if $$slots.end && $endPoint} + + + + {/if} + {/key} +{/if} diff --git a/packages/layerchart/src/routes/docs/examples/Line/+page.svelte b/packages/layerchart/src/routes/docs/examples/Line/+page.svelte index 4610d7c52..a0872ba32 100644 --- a/packages/layerchart/src/routes/docs/examples/Line/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/Line/+page.svelte @@ -7,6 +7,7 @@ import { Axis, + Canvas, Chart, Highlight, Labels, @@ -84,6 +85,34 @@ +

Canvas

+ + +
+ + + + formatDate(d, PeriodType.Day, { variant: 'short' })} + rule + /> + + + + + +
+
+

With Tooltip and Highlight

From eab12cf5cbdd2446e51389cc7dfed50011bd3e22 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Fri, 13 Dec 2024 09:31:16 -0500 Subject: [PATCH 04/55] breaking(GeoPath): Simplify render prop use case by leveraging renderPathData() (ex. HitCanvas) --- .changeset/wicked-mirrors-sleep.md | 5 ++ .../src/lib/components/GeoPath.svelte | 53 ++++++------------- .../docs/components/HitCanvas/+page.svelte | 13 ++--- .../docs/components/Spline/+page.svelte | 27 +++++++--- .../docs/examples/AnimatedGlobe/+page.svelte | 12 ++--- .../docs/examples/BubbleMap/+page.svelte | 43 +++++++++------ .../docs/examples/Choropleth/+page.svelte | 30 +++++++---- .../routes/docs/examples/GeoPath/+page.svelte | 13 ++--- .../docs/examples/ZoomableMap/+page.svelte | 31 +++++------ .../docs/tools/GeojsonPreview/+page.svelte | 46 +++++++++------- 10 files changed, 143 insertions(+), 130 deletions(-) create mode 100644 .changeset/wicked-mirrors-sleep.md diff --git a/.changeset/wicked-mirrors-sleep.md b/.changeset/wicked-mirrors-sleep.md new file mode 100644 index 000000000..7c0f699a3 --- /dev/null +++ b/.changeset/wicked-mirrors-sleep.md @@ -0,0 +1,5 @@ +--- +'layerchart': minor +--- + +breaking(GeoPath): Simplify render prop use case by leveraging renderPathData() (ex. HitCanvas) diff --git a/packages/layerchart/src/lib/components/GeoPath.svelte b/packages/layerchart/src/lib/components/GeoPath.svelte index 487d3f246..a8679a13f 100644 --- a/packages/layerchart/src/lib/components/GeoPath.svelte +++ b/packages/layerchart/src/lib/components/GeoPath.svelte @@ -15,6 +15,7 @@ import type { TooltipContextValue } from './tooltip/TooltipContext.svelte'; import { curveLinearClosed, type CurveFactory, type CurveFactoryLineOnly } from 'd3-shape'; import { geoCurvePath } from '$lib/utils/geo.js'; + import { clearCanvasContext, renderPathData } from '$lib/utils/canvas.js'; export let geojson: GeoPermissibleObjects | null | undefined = undefined; @@ -22,13 +23,13 @@ export let render: | (( ctx: CanvasRenderingContext2D, - options: { geoPath: ReturnType } + options: { newGeoPath: () => ReturnType } ) => any) | undefined = undefined; export let fill: string | undefined = undefined; export let stroke: string | undefined = undefined; - export let strokeWidth: number | string | undefined = undefined; + export let strokeWidth: number | undefined = undefined; /** * Tooltip context to setup mouse events to show tooltip for related data @@ -69,48 +70,28 @@ $: geoPath = geoCurvePath(_projection, curve); - const DEFAULT_FILL = 'rgb(0, 0, 0)'; - $: renderContext = canvas ? 'canvas' : 'svg'; + $: canvasCtx = canvas?.ctx; - $: ctx = canvas?.ctx; - $: if (renderContext === 'canvas' && $ctx) { - let computedStyles: Partial = {}; - - // Transfer classes defined on to to enable window.getComputedStyle() retrieval (Tailwind classes, etc) - if (className) { - $ctx.canvas.classList.add(...className.split(' ')); - computedStyles = window.getComputedStyle($ctx.canvas); - } - - // console.count('render'); - - // Clear with negative offset due to Canvas `context.translate(...)` - $ctx.clearRect(-$padding.left, -$padding.top, $containerWidth, $containerHeight); + $: if (renderContext === 'canvas' && $canvasCtx) { + clearCanvasContext($canvasCtx, { + padding: $padding, + containerWidth: $containerWidth, + containerHeight: $containerHeight, + }); if (render) { - geoPath = geoCurvePath(_projection, curve, $ctx); - render($ctx, { geoPath }); + // geoPath = geoCurvePath(_projection, curve, $canvasCtx); + geoPath = geoCurvePath(_projection, curve); + render($canvasCtx, { newGeoPath: () => geoCurvePath(_projection, curve) }); } else { - $ctx.beginPath(); // Set the context here since setting it in `$: geoPath` is a circular reference - geoPath = geoCurvePath(_projection, curve, $ctx); + geoPath = geoCurvePath(_projection, curve); + if (geojson) { - geoPath(geojson); + const pathData = geoPath(geojson); + renderPathData($canvasCtx, pathData, { fill, stroke, strokeWidth, class: $$props.class }); } - - $ctx.fillStyle = - fill ?? - (computedStyles.fill !== DEFAULT_FILL ? computedStyles.fill : undefined) ?? - 'transparent'; - $ctx.fill(); - - $ctx.lineWidth = Number(strokeWidth ?? 0); - $ctx.strokeStyle = - (stroke ?? computedStyles.stroke === 'none') - ? 'transparent' - : (computedStyles.stroke ?? ''); - $ctx.stroke(); } } diff --git a/packages/layerchart/src/routes/docs/components/HitCanvas/+page.svelte b/packages/layerchart/src/routes/docs/components/HitCanvas/+page.svelte index 751e252de..8e99d46c3 100644 --- a/packages/layerchart/src/routes/docs/components/HitCanvas/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/HitCanvas/+page.svelte @@ -2,7 +2,7 @@ import { geoIdentity, type GeoProjection } from 'd3-geo'; import { feature } from 'topojson-client'; - import { Chart, Canvas, GeoPath, HitCanvas, Tooltip } from 'layerchart'; + import { Chart, Canvas, GeoPath, HitCanvas, Tooltip, renderPathData } from 'layerchart'; import { Field, Switch } from 'svelte-ux'; import Preview from '$lib/docs/Preview.svelte'; @@ -57,18 +57,13 @@ {debug} > { + render={(ctx, { newGeoPath }) => { for (var feature of counties.features) { const color = nextColor(); - ctx.beginPath(); - geoPath(feature); - ctx.fillStyle = color; - ctx.fill(); - + const geoPath = newGeoPath(); // Stroking shape seems to help with dark border, but there is still antialising and thus gaps - ctx.strokeStyle = color; - ctx.stroke(); + renderPathData(ctx, geoPath(feature), { fill: color, stroke: color }); setColorData(color, feature); } diff --git a/packages/layerchart/src/routes/docs/components/Spline/+page.svelte b/packages/layerchart/src/routes/docs/components/Spline/+page.svelte index b0e1efdcb..9603286ee 100644 --- a/packages/layerchart/src/routes/docs/components/Spline/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Spline/+page.svelte @@ -1,7 +1,7 @@ + {#if line} {/if} - - +{#if renderContext === 'svg'} + + +{/if} diff --git a/packages/layerchart/src/routes/docs/components/Area/+page.svelte b/packages/layerchart/src/routes/docs/components/Area/+page.svelte index 5f3da31c9..e9ba6698b 100644 --- a/packages/layerchart/src/routes/docs/components/Area/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Area/+page.svelte @@ -1,8 +1,8 @@ @@ -187,3 +178,11 @@ on:pointerleave /> {/if} + + +{#if renderContext === 'canvas'} +
(_styles = styles)} + >
+{/if} diff --git a/packages/layerchart/src/lib/components/Circle.svelte b/packages/layerchart/src/lib/components/Circle.svelte index 03f21befc..3fe82e4f5 100644 --- a/packages/layerchart/src/lib/components/Circle.svelte +++ b/packages/layerchart/src/lib/components/Circle.svelte @@ -1,9 +1,13 @@ - - +{#if renderContext === 'svg'} + + +{/if} + + +{#if renderContext === 'canvas'} +
(_styles = styles)} + >
+{/if} diff --git a/packages/layerchart/src/lib/components/ComputedStyles.svelte b/packages/layerchart/src/lib/components/ComputedStyles.svelte new file mode 100644 index 000000000..2c8fb4324 --- /dev/null +++ b/packages/layerchart/src/lib/components/ComputedStyles.svelte @@ -0,0 +1,16 @@ + + +
(styles = _styles)} +>
+ + diff --git a/packages/layerchart/src/lib/components/GeoPath.svelte b/packages/layerchart/src/lib/components/GeoPath.svelte index c023ca38e..843ff0766 100644 --- a/packages/layerchart/src/lib/components/GeoPath.svelte +++ b/packages/layerchart/src/lib/components/GeoPath.svelte @@ -1,6 +1,5 @@ @@ -121,3 +125,11 @@ /> {/if} + + +{#if renderContext === 'canvas'} +
(_styles = styles)} + >
+{/if} diff --git a/packages/layerchart/src/lib/components/GeoPoint.svelte b/packages/layerchart/src/lib/components/GeoPoint.svelte index ed44c1190..099b468da 100644 --- a/packages/layerchart/src/lib/components/GeoPoint.svelte +++ b/packages/layerchart/src/lib/components/GeoPoint.svelte @@ -1,10 +1,12 @@ {#if renderContext === 'svg'} @@ -46,3 +51,19 @@ {/if} {/if} + + +{#if renderContext === 'canvas'} + {#if $$slots.default} + + + + {:else} + + {/if} + +
(_styles = styles)} + >
+{/if} diff --git a/packages/layerchart/src/lib/components/GeoTile.svelte b/packages/layerchart/src/lib/components/GeoTile.svelte index 5657557a9..b365eec72 100644 --- a/packages/layerchart/src/lib/components/GeoTile.svelte +++ b/packages/layerchart/src/lib/components/GeoTile.svelte @@ -1,6 +1,5 @@ {#if renderContext === 'svg' && url} diff --git a/packages/layerchart/src/lib/components/HitCanvas.svelte b/packages/layerchart/src/lib/components/HitCanvas.svelte index 5352fd59a..4a7145f4c 100644 --- a/packages/layerchart/src/lib/components/HitCanvas.svelte +++ b/packages/layerchart/src/lib/components/HitCanvas.svelte @@ -1,7 +1,5 @@ @@ -233,3 +219,11 @@ {/if} + + +{#if renderContext === 'canvas'} +
(_styles = styles)} + >
+{/if} diff --git a/packages/layerchart/src/lib/components/Spline.svelte b/packages/layerchart/src/lib/components/Spline.svelte index d61624703..f8239d54e 100644 --- a/packages/layerchart/src/lib/components/Spline.svelte +++ b/packages/layerchart/src/lib/components/Spline.svelte @@ -1,6 +1,6 @@ + ` element for retrieval) */ export function renderPathData( canvasCtx: CanvasRenderingContext2D, pathData: string | null | undefined, - props: { fill?: string; stroke?: string; strokeWidth?: number; class?: string } = {} + styles: Partial = {} ) { - // Get classes from nearest `` element. Useful if classes are moved up from underlying component (ex. GeoPath) - const computedStyles: Partial = window.getComputedStyle(canvasCtx.canvas); - const path = new Path2D(pathData ?? ''); - const fill = props.fill ?? (computedStyles.fill !== DEFAULT_FILL ? computedStyles.fill : null); + const fill = styles.fill === DEFAULT_FILL ? null : styles.fill; if (fill) { canvasCtx.fillStyle = fill; canvasCtx.fill(path); } - const stroke = - props.stroke ?? (computedStyles.stroke === 'none' ? null : (computedStyles.stroke ?? null)); + const stroke = styles.stroke === 'none' ? null : styles.stroke; if (stroke) { - canvasCtx.lineWidth = - props.strokeWidth ?? Number(computedStyles.strokeWidth?.replace('px', '')); + canvasCtx.lineWidth = Number(styles.strokeWidth?.replace('px', '')); canvasCtx.strokeStyle = stroke; canvasCtx.stroke(path); } @@ -49,3 +44,21 @@ export function clearCanvasContext( options.containerHeight ); } + +/** + Scales a canvas for high DPI / retina displays. + @see: https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio#examples + @see: https://web.dev/articles/canvas-hidipi +*/ +export function scaleCanvas(ctx: CanvasRenderingContext2D, width: number, height: number) { + const devicePixelRatio = window.devicePixelRatio || 1; + + ctx.canvas.width = width * devicePixelRatio; + ctx.canvas.height = height * devicePixelRatio; + + ctx.canvas.style.width = `${width}px`; + ctx.canvas.style.height = `${height}px`; + + ctx.scale(devicePixelRatio, devicePixelRatio); + return { width: ctx.canvas.width, height: ctx.canvas.height }; +} diff --git a/packages/layerchart/src/routes/docs/components/Area/+page.svelte b/packages/layerchart/src/routes/docs/components/Area/+page.svelte index e9ba6698b..08adf8b80 100644 --- a/packages/layerchart/src/routes/docs/components/Area/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Area/+page.svelte @@ -75,13 +75,10 @@ {tweened} class="fill-primary/10" /> - {/if} - - - - {#if show && showPoints} - + {#if showPoints} + + {/if} {/if} @@ -122,22 +119,17 @@ - {#if show} - {/if} - - - {#if show && showLine} - - {/if} - + {#if showLine} + + {/if} - - {#if show && showPoints} - + {#if showPoints} + + {/if} {/if} diff --git a/packages/layerchart/src/routes/docs/components/HitCanvas/+page.svelte b/packages/layerchart/src/routes/docs/components/HitCanvas/+page.svelte index 8e99d46c3..8047eb1b3 100644 --- a/packages/layerchart/src/routes/docs/components/HitCanvas/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/HitCanvas/+page.svelte @@ -38,16 +38,12 @@ > - - - - {#if tooltip.data} - + {#if tooltip.data} - - {/if} + {/if} + - - - - - - - - - - - - {#if tooltip.data} - + {#if tooltip.data} - - {/if} + {/if} + -
- { for (var feature of enrichedCountiesFeatures) { diff --git a/packages/layerchart/src/routes/docs/examples/Choropleth/+page.svelte b/packages/layerchart/src/routes/docs/examples/Choropleth/+page.svelte index b058a9fa6..97452ee50 100644 --- a/packages/layerchart/src/routes/docs/examples/Choropleth/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/Choropleth/+page.svelte @@ -168,8 +168,6 @@ } }} /> - - diff --git a/packages/layerchart/src/routes/docs/examples/CountryMap/+page.svelte b/packages/layerchart/src/routes/docs/examples/CountryMap/+page.svelte index e9810b82b..6a7408162 100644 --- a/packages/layerchart/src/routes/docs/examples/CountryMap/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/CountryMap/+page.svelte @@ -4,6 +4,7 @@ import { Canvas, Chart, GeoPath, Svg, Text } from 'layerchart'; import Preview from '$lib/docs/Preview.svelte'; + import ComputedStyles from 'layerchart/components/ComputedStyles.svelte'; export let data; const states = feature(data.geojson, data.geojson.objects.states); @@ -62,29 +63,27 @@ > - - {#each states.features as feature} - - { - const geoPath = newGeoPath(); - const [x, y] = geoPath.centroid(feature); - const computedStyle = window.getComputedStyle(ctx.canvas); - ctx.font = '8px sans-serif'; - ctx.textAlign = 'center'; + + {#each states.features as feature} + { + const geoPath = newGeoPath(); + const [x, y] = geoPath.centroid(feature); + ctx.font = '8px sans-serif'; + ctx.textAlign = 'center'; - ctx.lineWidth = 2; - ctx.strokeStyle = computedStyle.stroke; - ctx.strokeText(feature.properties.name, x, y); + ctx.lineWidth = 2; + ctx.strokeStyle = styles.stroke; + ctx.strokeText(feature.properties.name, x, y); - ctx.fillStyle = computedStyle.fill; - ctx.fillText(feature.properties.name, x, y); - }} - /> - - {/each} + ctx.fillStyle = styles.fill; + ctx.fillText(feature.properties.name, x, y); + }} + /> + {/each} + +
diff --git a/packages/layerchart/src/routes/docs/examples/GeoPath/+page.svelte b/packages/layerchart/src/routes/docs/examples/GeoPath/+page.svelte index 32b1d819c..b6be831cd 100644 --- a/packages/layerchart/src/routes/docs/examples/GeoPath/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/GeoPath/+page.svelte @@ -65,16 +65,11 @@ > - - - - - {#if tooltip.data} - + {#if tooltip.data} - - {/if} + {/if} +
- - {#each data.us.captitals as capital} - - - { - const computedStyle = window.getComputedStyle(ctx.canvas); - const radius = 2; - ctx.strokeStyle = computedStyle.stroke; - ctx.fillStyle = computedStyle.fill; - ctx.beginPath(); - ctx.arc(x, y, radius, 0, 2 * Math.PI, false); - ctx.fill(); - ctx.stroke(); - }} - /> - + + + {#each data.us.captitals as capital} + + { + const pathData = circlePath({ cx: x, cy: y, r: 2 }); + renderPathData(ctx, pathData, pointStyles); + }} + /> - - - - { - const computedStyle = window.getComputedStyle(ctx.canvas); - ctx.font = '8px sans-serif'; - ctx.textAlign = 'center'; - ctx.lineWidth = 2; - ctx.strokeStyle = computedStyle.stroke; - ctx.fillStyle = computedStyle.fill; - ctx.strokeText(capital.description, x, y - 6); - ctx.fillText(capital.description, x, y - 6); - }} - /> - - {/each} + + { + ctx.font = '8px sans-serif'; + ctx.textAlign = 'center'; + ctx.lineWidth = 2; + ctx.strokeStyle = labelStyles.stroke; + ctx.fillStyle = labelStyles.fill; + ctx.strokeText(capital.description, x, y - 6); + ctx.fillText(capital.description, x, y - 6); + }} + /> + {/each} + + + diff --git a/packages/layerchart/src/routes/docs/examples/SpikeMap/+page.svelte b/packages/layerchart/src/routes/docs/examples/SpikeMap/+page.svelte index 32283e229..54984dd29 100644 --- a/packages/layerchart/src/routes/docs/examples/SpikeMap/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/SpikeMap/+page.svelte @@ -17,6 +17,7 @@ import TransformControls from 'layerchart/components/TransformControls.svelte'; import Preview from '$lib/docs/Preview.svelte'; + import ComputedStyles from 'layerchart/components/ComputedStyles.svelte'; export let data; @@ -167,37 +168,34 @@ class="fill-surface-content/10 stroke-surface-100" {strokeWidth} /> - - - - { - const computedStyle = window.getComputedStyle(ctx.canvas); - for (var feature of enrichedCountiesFeatures) { - const geoPath = newGeoPath(); - const [x, y] = geoPath.centroid(feature); - const d = feature.properties.data; - const height = heightScale(d?.population ?? 0); + + { + for (var feature of enrichedCountiesFeatures) { + const geoPath = newGeoPath(); + const [x, y] = geoPath.centroid(feature); + const d = feature.properties.data; + const height = heightScale(d?.population ?? 0); - ctx.lineWidth = strokeWidth; - ctx.strokeStyle = computedStyle.stroke; - ctx.fillStyle = computedStyle.fill; + ctx.lineWidth = strokeWidth; + ctx.strokeStyle = styles.stroke; + ctx.fillStyle = styles.fill; - const startPoint = [x - width / 2, y]; - const midPoint = [x, y - height]; - const endPoint = [x + width / 2, y]; + const startPoint = [x - width / 2, y]; + const midPoint = [x, y - height]; + const endPoint = [x + width / 2, y]; - ctx.beginPath(); - ctx.moveTo(x - width / 2, y); // startPoint - ctx.lineTo(x, y - height); // midPoint - ctx.lineTo(x + width / 2, y); // endPoint - ctx.fill(); - ctx.stroke(); - } - }} - /> + ctx.beginPath(); + ctx.moveTo(x - width / 2, y); // startPoint + ctx.lineTo(x, y - height); // midPoint + ctx.lineTo(x + width / 2, y); // endPoint + ctx.fill(); + ctx.stroke(); + } + }} + /> + {#if tooltip.data} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2a06c150e..8b21048ee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,7 +19,7 @@ importers: version: 6.0.1 wrangler: specifier: ^3.95.0 - version: 3.95.0(@cloudflare/workers-types@4.20241218.0) + version: 3.95.0(@cloudflare/workers-types@4.20241230.0) packages/layerchart: dependencies: @@ -27,8 +27,8 @@ importers: specifier: ^1.1.4 version: 1.1.4 '@layerstack/svelte-actions': - specifier: ^0.0.9 - version: 0.0.9 + specifier: ^0.0.11 + version: 0.0.11 '@layerstack/svelte-stores': specifier: ^0.0.9 version: 0.0.9 @@ -122,16 +122,16 @@ importers: version: 3.0.5(rollup@2.79.2) '@sveltejs/adapter-cloudflare': specifier: ^4.8.0 - version: 4.8.0(@sveltejs/kit@2.11.1(@sveltejs/vite-plugin-svelte@5.0.2(svelte@5.13.0)(vite@6.0.3(@types/node@22.10.2)(jiti@2.4.2)(yaml@2.6.1)))(svelte@5.13.0)(vite@6.0.3(@types/node@22.10.2)(jiti@2.4.2)(yaml@2.6.1)))(wrangler@3.99.0(@cloudflare/workers-types@4.20241218.0)) + version: 4.8.0(@sveltejs/kit@2.11.1(@sveltejs/vite-plugin-svelte@5.0.2(svelte@5.13.0)(vite@6.0.3(@types/node@22.10.4)(jiti@2.4.2)(yaml@2.7.0)))(svelte@5.13.0)(vite@6.0.3(@types/node@22.10.4)(jiti@2.4.2)(yaml@2.7.0)))(wrangler@3.99.0(@cloudflare/workers-types@4.20241230.0)) '@sveltejs/kit': specifier: ^2.11.1 - version: 2.11.1(@sveltejs/vite-plugin-svelte@5.0.2(svelte@5.13.0)(vite@6.0.3(@types/node@22.10.2)(jiti@2.4.2)(yaml@2.6.1)))(svelte@5.13.0)(vite@6.0.3(@types/node@22.10.2)(jiti@2.4.2)(yaml@2.6.1)) + version: 2.11.1(@sveltejs/vite-plugin-svelte@5.0.2(svelte@5.13.0)(vite@6.0.3(@types/node@22.10.4)(jiti@2.4.2)(yaml@2.7.0)))(svelte@5.13.0)(vite@6.0.3(@types/node@22.10.4)(jiti@2.4.2)(yaml@2.7.0)) '@sveltejs/package': specifier: ^2.3.7 version: 2.3.7(svelte@5.13.0)(typescript@5.7.2) '@sveltejs/vite-plugin-svelte': specifier: ^5.0.2 - version: 5.0.2(svelte@5.13.0)(vite@6.0.3(@types/node@22.10.2)(jiti@2.4.2)(yaml@2.6.1)) + version: 5.0.2(svelte@5.13.0)(vite@6.0.3(@types/node@22.10.4)(jiti@2.4.2)(yaml@2.7.0)) '@svitejs/changesets-changelog-github-compact': specifier: ^1.2.0 version: 1.2.0 @@ -251,7 +251,7 @@ importers: version: 2.2.0(svelte@5.13.0) svelte-ux: specifier: ^0.76.0 - version: 0.76.0(postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.4.49)(yaml@2.6.1))(postcss@8.4.49)(svelte@5.13.0) + version: 0.76.0(postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.4.49)(yaml@2.7.0))(postcss@8.4.49)(svelte@5.13.0) svelte2tsx: specifier: ^0.7.30 version: 0.7.30(svelte@5.13.0)(typescript@5.7.2) @@ -275,10 +275,10 @@ importers: version: 3.0.1 vite: specifier: ^6.0.3 - version: 6.0.3(@types/node@22.10.2)(jiti@2.4.2)(yaml@2.6.1) + version: 6.0.3(@types/node@22.10.4)(jiti@2.4.2)(yaml@2.7.0) vitest: specifier: ^2.1.8 - version: 2.1.8(@types/node@22.10.2) + version: 2.1.8(@types/node@22.10.4) packages: @@ -423,8 +423,8 @@ packages: '@cloudflare/workers-types@4.20241205.0': resolution: {integrity: sha512-pj1VKRHT/ScQbHOIMFODZaNAlJHQHdBSZXNIdr9ebJzwBff9Qz8VdqhbhggV7f+aUEh8WSbrsPIo4a+WtgjUvw==} - '@cloudflare/workers-types@4.20241218.0': - resolution: {integrity: sha512-Y0brjmJHcAZBXOPI7lU5hbiXglQWniA1kQjot2ata+HFimyjPPcz+4QWBRrmWcMPo0OadR2Vmac7WStDLpvz0w==} + '@cloudflare/workers-types@4.20241230.0': + resolution: {integrity: sha512-dtLD4jY35Lb750cCVyO1i/eIfdZJg2Z0i+B1RYX6BVeRPlgaHx/H18ImKAkYmy0g09Ow8R2jZy3hIxMgXun0WQ==} '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} @@ -903,8 +903,8 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@layerstack/svelte-actions@0.0.9': - resolution: {integrity: sha512-1mDXM7R0pT3pvQvXS479hN9tkn6M812Iip9M7BSorDTQcccGNVGU67aqLO5qded7CuzRnyeN2id5KcVU5zfY8A==} + '@layerstack/svelte-actions@0.0.11': + resolution: {integrity: sha512-sfplFX8rOBW74xjDCAY7QzHilvUGjFQTkfQXxUO6e68cKCHmxsSuTCtFbHRCfIUJCQ31gAbeY1xpWKnlVfgZ9g==} '@layerstack/svelte-stores@0.0.9': resolution: {integrity: sha512-5yyi7eV/2hOraw7wQgEObVR4s2/4T3G/GjGaZLR9BQH8mnsLuXw1n3k6rFT8NxiRvpEjWJ9l9ILOGc/XzFJb3g==} @@ -1207,6 +1207,9 @@ packages: '@types/node@22.10.2': resolution: {integrity: sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==} + '@types/node@22.10.4': + resolution: {integrity: sha512-99l6wv4HEzBQhvaU/UGoeBoCK61SCROQaCCGyQSgX2tEQ3rKkNZ2S7CEWnS/4s1LV+8ODdK21UeyR1fHP2mXug==} + '@types/prismjs@1.26.5': resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==} @@ -2894,6 +2897,11 @@ packages: engines: {node: '>= 14'} hasBin: true + yaml@2.7.0: + resolution: {integrity: sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==} + engines: {node: '>= 14'} + hasBin: true + youch@3.3.4: resolution: {integrity: sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==} @@ -3109,7 +3117,7 @@ snapshots: '@cloudflare/workers-types@4.20241205.0': {} - '@cloudflare/workers-types@4.20241218.0': + '@cloudflare/workers-types@4.20241230.0': optional: true '@cspotcode/source-map-support@0.8.1': @@ -3385,7 +3393,7 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 - '@layerstack/svelte-actions@0.0.9': + '@layerstack/svelte-actions@0.0.11': dependencies: '@floating-ui/dom': 1.6.12 '@layerstack/utils': 0.0.7 @@ -3554,17 +3562,17 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.28.1': optional: true - '@sveltejs/adapter-cloudflare@4.8.0(@sveltejs/kit@2.11.1(@sveltejs/vite-plugin-svelte@5.0.2(svelte@5.13.0)(vite@6.0.3(@types/node@22.10.2)(jiti@2.4.2)(yaml@2.6.1)))(svelte@5.13.0)(vite@6.0.3(@types/node@22.10.2)(jiti@2.4.2)(yaml@2.6.1)))(wrangler@3.99.0(@cloudflare/workers-types@4.20241218.0))': + '@sveltejs/adapter-cloudflare@4.8.0(@sveltejs/kit@2.11.1(@sveltejs/vite-plugin-svelte@5.0.2(svelte@5.13.0)(vite@6.0.3(@types/node@22.10.4)(jiti@2.4.2)(yaml@2.7.0)))(svelte@5.13.0)(vite@6.0.3(@types/node@22.10.4)(jiti@2.4.2)(yaml@2.7.0)))(wrangler@3.99.0(@cloudflare/workers-types@4.20241230.0))': dependencies: '@cloudflare/workers-types': 4.20241205.0 - '@sveltejs/kit': 2.11.1(@sveltejs/vite-plugin-svelte@5.0.2(svelte@5.13.0)(vite@6.0.3(@types/node@22.10.2)(jiti@2.4.2)(yaml@2.6.1)))(svelte@5.13.0)(vite@6.0.3(@types/node@22.10.2)(jiti@2.4.2)(yaml@2.6.1)) + '@sveltejs/kit': 2.11.1(@sveltejs/vite-plugin-svelte@5.0.2(svelte@5.13.0)(vite@6.0.3(@types/node@22.10.4)(jiti@2.4.2)(yaml@2.7.0)))(svelte@5.13.0)(vite@6.0.3(@types/node@22.10.4)(jiti@2.4.2)(yaml@2.7.0)) esbuild: 0.24.0 worktop: 0.8.0-next.18 - wrangler: 3.99.0(@cloudflare/workers-types@4.20241218.0) + wrangler: 3.99.0(@cloudflare/workers-types@4.20241230.0) - '@sveltejs/kit@2.11.1(@sveltejs/vite-plugin-svelte@5.0.2(svelte@5.13.0)(vite@6.0.3(@types/node@22.10.2)(jiti@2.4.2)(yaml@2.6.1)))(svelte@5.13.0)(vite@6.0.3(@types/node@22.10.2)(jiti@2.4.2)(yaml@2.6.1))': + '@sveltejs/kit@2.11.1(@sveltejs/vite-plugin-svelte@5.0.2(svelte@5.13.0)(vite@6.0.3(@types/node@22.10.4)(jiti@2.4.2)(yaml@2.7.0)))(svelte@5.13.0)(vite@6.0.3(@types/node@22.10.4)(jiti@2.4.2)(yaml@2.7.0))': dependencies: - '@sveltejs/vite-plugin-svelte': 5.0.2(svelte@5.13.0)(vite@6.0.3(@types/node@22.10.2)(jiti@2.4.2)(yaml@2.6.1)) + '@sveltejs/vite-plugin-svelte': 5.0.2(svelte@5.13.0)(vite@6.0.3(@types/node@22.10.4)(jiti@2.4.2)(yaml@2.7.0)) '@types/cookie': 0.6.0 cookie: 0.6.0 devalue: 5.1.1 @@ -3578,7 +3586,7 @@ snapshots: sirv: 3.0.0 svelte: 5.13.0 tiny-glob: 0.2.9 - vite: 6.0.3(@types/node@22.10.2)(jiti@2.4.2)(yaml@2.6.1) + vite: 6.0.3(@types/node@22.10.4)(jiti@2.4.2)(yaml@2.7.0) '@sveltejs/package@2.3.7(svelte@5.13.0)(typescript@5.7.2)': dependencies: @@ -3591,25 +3599,25 @@ snapshots: transitivePeerDependencies: - typescript - '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.0.2(svelte@5.13.0)(vite@6.0.3(@types/node@22.10.2)(jiti@2.4.2)(yaml@2.6.1)))(svelte@5.13.0)(vite@6.0.3(@types/node@22.10.2)(jiti@2.4.2)(yaml@2.6.1))': + '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.0.2(svelte@5.13.0)(vite@6.0.3(@types/node@22.10.4)(jiti@2.4.2)(yaml@2.7.0)))(svelte@5.13.0)(vite@6.0.3(@types/node@22.10.4)(jiti@2.4.2)(yaml@2.7.0))': dependencies: - '@sveltejs/vite-plugin-svelte': 5.0.2(svelte@5.13.0)(vite@6.0.3(@types/node@22.10.2)(jiti@2.4.2)(yaml@2.6.1)) + '@sveltejs/vite-plugin-svelte': 5.0.2(svelte@5.13.0)(vite@6.0.3(@types/node@22.10.4)(jiti@2.4.2)(yaml@2.7.0)) debug: 4.4.0 svelte: 5.13.0 - vite: 6.0.3(@types/node@22.10.2)(jiti@2.4.2)(yaml@2.6.1) + vite: 6.0.3(@types/node@22.10.4)(jiti@2.4.2)(yaml@2.7.0) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@5.0.2(svelte@5.13.0)(vite@6.0.3(@types/node@22.10.2)(jiti@2.4.2)(yaml@2.6.1))': + '@sveltejs/vite-plugin-svelte@5.0.2(svelte@5.13.0)(vite@6.0.3(@types/node@22.10.4)(jiti@2.4.2)(yaml@2.7.0))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.0.2(svelte@5.13.0)(vite@6.0.3(@types/node@22.10.2)(jiti@2.4.2)(yaml@2.6.1)))(svelte@5.13.0)(vite@6.0.3(@types/node@22.10.2)(jiti@2.4.2)(yaml@2.6.1)) + '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.0.2(svelte@5.13.0)(vite@6.0.3(@types/node@22.10.4)(jiti@2.4.2)(yaml@2.7.0)))(svelte@5.13.0)(vite@6.0.3(@types/node@22.10.4)(jiti@2.4.2)(yaml@2.7.0)) debug: 4.4.0 deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.15 svelte: 5.13.0 - vite: 6.0.3(@types/node@22.10.2)(jiti@2.4.2)(yaml@2.6.1) - vitefu: 1.0.4(vite@6.0.3(@types/node@22.10.2)(jiti@2.4.2)(yaml@2.6.1)) + vite: 6.0.3(@types/node@22.10.4)(jiti@2.4.2)(yaml@2.7.0) + vitefu: 1.0.4(vite@6.0.3(@types/node@22.10.4)(jiti@2.4.2)(yaml@2.7.0)) transitivePeerDependencies: - supports-color @@ -3710,6 +3718,11 @@ snapshots: dependencies: undici-types: 6.20.0 + '@types/node@22.10.4': + dependencies: + undici-types: 6.20.0 + optional: true + '@types/prismjs@1.26.5': {} '@types/resolve@1.17.1': @@ -3746,13 +3759,13 @@ snapshots: chai: 5.1.2 tinyrainbow: 1.2.0 - '@vitest/mocker@2.1.8(vite@5.4.11(@types/node@22.10.2))': + '@vitest/mocker@2.1.8(vite@5.4.11(@types/node@22.10.4))': dependencies: '@vitest/spy': 2.1.8 estree-walker: 3.0.3 magic-string: 0.30.15 optionalDependencies: - vite: 5.4.11(@types/node@22.10.2) + vite: 5.4.11(@types/node@22.10.4) '@vitest/pretty-format@2.1.8': dependencies: @@ -4694,13 +4707,13 @@ snapshots: optionalDependencies: postcss: 8.4.49 - postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.4.49)(yaml@2.6.1): + postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.4.49)(yaml@2.7.0): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 2.4.2 postcss: 8.4.49 - yaml: 2.6.1 + yaml: 2.7.0 optional: true postcss-nested@6.2.0(postcss@8.4.49): @@ -4974,7 +4987,7 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - sveld@0.22.0(postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.4.49)(yaml@2.6.1))(postcss@8.4.49): + sveld@0.22.0(postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.4.49)(yaml@2.7.0))(postcss@8.4.49): dependencies: '@rollup/plugin-node-resolve': 13.3.0(rollup@2.79.2) acorn: 8.14.0 @@ -4983,7 +4996,7 @@ snapshots: rollup: 2.79.2 rollup-plugin-svelte: 7.2.2(rollup@2.79.2)(svelte@4.2.19) svelte: 4.2.19 - svelte-preprocess: 6.0.3(postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.4.49)(yaml@2.6.1))(postcss@8.4.49)(svelte@4.2.19)(typescript@5.7.2) + svelte-preprocess: 6.0.3(postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.4.49)(yaml@2.7.0))(postcss@8.4.49)(svelte@4.2.19)(typescript@5.7.2) tinyglobby: 0.2.10 typescript: 5.7.2 transitivePeerDependencies: @@ -5013,15 +5026,15 @@ snapshots: dependencies: svelte: 5.13.0 - svelte-preprocess@6.0.3(postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.4.49)(yaml@2.6.1))(postcss@8.4.49)(svelte@4.2.19)(typescript@5.7.2): + svelte-preprocess@6.0.3(postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.4.49)(yaml@2.7.0))(postcss@8.4.49)(svelte@4.2.19)(typescript@5.7.2): dependencies: svelte: 4.2.19 optionalDependencies: postcss: 8.4.49 - postcss-load-config: 6.0.1(jiti@2.4.2)(postcss@8.4.49)(yaml@2.6.1) + postcss-load-config: 6.0.1(jiti@2.4.2)(postcss@8.4.49)(yaml@2.7.0) typescript: 5.7.2 - svelte-ux@0.76.0(postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.4.49)(yaml@2.6.1))(postcss@8.4.49)(svelte@5.13.0): + svelte-ux@0.76.0(postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.4.49)(yaml@2.7.0))(postcss@8.4.49)(svelte@5.13.0): dependencies: '@floating-ui/dom': 1.6.12 '@fortawesome/fontawesome-common-types': 6.7.1 @@ -5037,7 +5050,7 @@ snapshots: prism-svelte: 0.5.0 prism-themes: 1.9.0 prismjs: 1.29.0 - sveld: 0.22.0(postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.4.49)(yaml@2.6.1))(postcss@8.4.49) + sveld: 0.22.0(postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.4.49)(yaml@2.7.0))(postcss@8.4.49) svelte: 5.13.0 tailwind-merge: 2.5.5 zod: 3.24.1 @@ -5237,13 +5250,13 @@ snapshots: '@types/unist': 2.0.11 unist-util-stringify-position: 2.0.3 - vite-node@2.1.8(@types/node@22.10.2): + vite-node@2.1.8(@types/node@22.10.4): dependencies: cac: 6.7.14 debug: 4.4.0 es-module-lexer: 1.5.4 pathe: 1.1.2 - vite: 5.4.11(@types/node@22.10.2) + vite: 5.4.11(@types/node@22.10.4) transitivePeerDependencies: - '@types/node' - less @@ -5255,34 +5268,34 @@ snapshots: - supports-color - terser - vite@5.4.11(@types/node@22.10.2): + vite@5.4.11(@types/node@22.10.4): dependencies: esbuild: 0.21.5 postcss: 8.4.49 rollup: 4.28.1 optionalDependencies: - '@types/node': 22.10.2 + '@types/node': 22.10.4 fsevents: 2.3.3 - vite@6.0.3(@types/node@22.10.2)(jiti@2.4.2)(yaml@2.6.1): + vite@6.0.3(@types/node@22.10.4)(jiti@2.4.2)(yaml@2.7.0): dependencies: esbuild: 0.24.0 postcss: 8.4.49 rollup: 4.28.1 optionalDependencies: - '@types/node': 22.10.2 + '@types/node': 22.10.4 fsevents: 2.3.3 jiti: 2.4.2 - yaml: 2.6.1 + yaml: 2.7.0 - vitefu@1.0.4(vite@6.0.3(@types/node@22.10.2)(jiti@2.4.2)(yaml@2.6.1)): + vitefu@1.0.4(vite@6.0.3(@types/node@22.10.4)(jiti@2.4.2)(yaml@2.7.0)): optionalDependencies: - vite: 6.0.3(@types/node@22.10.2)(jiti@2.4.2)(yaml@2.6.1) + vite: 6.0.3(@types/node@22.10.4)(jiti@2.4.2)(yaml@2.7.0) - vitest@2.1.8(@types/node@22.10.2): + vitest@2.1.8(@types/node@22.10.4): dependencies: '@vitest/expect': 2.1.8 - '@vitest/mocker': 2.1.8(vite@5.4.11(@types/node@22.10.2)) + '@vitest/mocker': 2.1.8(vite@5.4.11(@types/node@22.10.4)) '@vitest/pretty-format': 2.1.8 '@vitest/runner': 2.1.8 '@vitest/snapshot': 2.1.8 @@ -5298,11 +5311,11 @@ snapshots: tinyexec: 0.3.1 tinypool: 1.0.2 tinyrainbow: 1.2.0 - vite: 5.4.11(@types/node@22.10.2) - vite-node: 2.1.8(@types/node@22.10.2) + vite: 5.4.11(@types/node@22.10.4) + vite-node: 2.1.8(@types/node@22.10.4) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 22.10.2 + '@types/node': 22.10.4 transitivePeerDependencies: - less - lightningcss @@ -5353,7 +5366,7 @@ snapshots: mrmime: 2.0.0 regexparam: 3.0.0 - wrangler@3.95.0(@cloudflare/workers-types@4.20241218.0): + wrangler@3.95.0(@cloudflare/workers-types@4.20241230.0): dependencies: '@cloudflare/kv-asset-handler': 0.3.4 '@cloudflare/workers-shared': 0.11.0 @@ -5374,14 +5387,14 @@ snapshots: workerd: 1.20241205.0 xxhash-wasm: 1.1.0 optionalDependencies: - '@cloudflare/workers-types': 4.20241218.0 + '@cloudflare/workers-types': 4.20241230.0 fsevents: 2.3.3 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - wrangler@3.99.0(@cloudflare/workers-types@4.20241218.0): + wrangler@3.99.0(@cloudflare/workers-types@4.20241230.0): dependencies: '@cloudflare/kv-asset-handler': 0.3.4 '@esbuild-plugins/node-globals-polyfill': 0.2.3(esbuild@0.17.19) @@ -5401,7 +5414,7 @@ snapshots: workerd: 1.20241218.0 xxhash-wasm: 1.1.0 optionalDependencies: - '@cloudflare/workers-types': 4.20241218.0 + '@cloudflare/workers-types': 4.20241230.0 fsevents: 2.3.3 transitivePeerDependencies: - bufferutil @@ -5426,6 +5439,9 @@ snapshots: yaml@2.6.1: {} + yaml@2.7.0: + optional: true + youch@3.3.4: dependencies: cookie: 0.7.2 From 52de26e7d0b4defc89b889c25b41206a1783feea Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Mon, 6 Jan 2025 08:50:39 -0500 Subject: [PATCH 13/55] Cleanup unused imports --- packages/layerchart/src/lib/components/GeoPath.svelte | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/layerchart/src/lib/components/GeoPath.svelte b/packages/layerchart/src/lib/components/GeoPath.svelte index 843ff0766..0b38185ad 100644 --- a/packages/layerchart/src/lib/components/GeoPath.svelte +++ b/packages/layerchart/src/lib/components/GeoPath.svelte @@ -10,12 +10,11 @@ import { cls } from '@layerstack/tailwind'; import { computedStyles } from '@layerstack/svelte-actions'; - import { chartContext } from './ChartContext.svelte'; import { geoContext } from './GeoContext.svelte'; import type { TooltipContextValue } from './tooltip/TooltipContext.svelte'; import { curveLinearClosed, type CurveFactory, type CurveFactoryLineOnly } from 'd3-shape'; import { geoCurvePath } from '$lib/utils/geo.js'; - import { clearCanvasContext, renderPathData } from '$lib/utils/canvas.js'; + import { renderPathData } from '$lib/utils/canvas.js'; import { getCanvasContext } from './layout/Canvas.svelte'; export let geojson: GeoPermissibleObjects | null | undefined = undefined; @@ -55,7 +54,6 @@ click: { geoPath: ReturnType; event: MouseEvent }; }>(); - const { containerWidth, containerHeight, padding } = chartContext(); const geo = geoContext(); /** @@ -79,7 +77,7 @@ render(ctx, { newGeoPath: () => geoCurvePath(_projection, curve) }); } else { if (geojson) { - console.log('rendering', _styles.fill); + // console.log('rendering', _styles.fill); const pathData = geoPath(geojson); // renderPathData(ctx, pathData, { ..._styles, fill, stroke, strokeWidth }); renderPathData(ctx, pathData, { ..._styles }); From 4738e042da821d93edc70c960ecd95296a40e19e Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Tue, 7 Jan 2025 12:56:15 -0500 Subject: [PATCH 14/55] Add changeset (ComputedStyles) --- .changeset/warm-cars-attack.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/warm-cars-attack.md diff --git a/.changeset/warm-cars-attack.md b/.changeset/warm-cars-attack.md new file mode 100644 index 000000000..0d16563c4 --- /dev/null +++ b/.changeset/warm-cars-attack.md @@ -0,0 +1,5 @@ +--- +'layerchart': minor +--- + +feat: Add `ComputedStyles` component to easily resolve classes / CSS variable values (useful when working with ) From 0f531c6e12a7ec8a954949bb4b65fb6e5ce6fcf0 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Tue, 7 Jan 2025 13:12:21 -0500 Subject: [PATCH 15/55] fix(Canvas/GeoPath): Fix tooltip ghosting (recreate geoPath() when `geojson` data changes). Fix tooltip path performance by rendering to separate --- .../src/lib/components/GeoPath.svelte | 11 ++++++++--- .../src/lib/components/layout/Canvas.svelte | 17 +++++++++++++---- packages/layerchart/src/lib/utils/canvas.ts | 5 ++++- .../docs/components/HitCanvas/+page.svelte | 3 +++ .../docs/examples/AnimatedGlobe/+page.svelte | 4 ++++ .../routes/docs/examples/GeoPath/+page.svelte | 4 ++++ .../docs/examples/ZoomableMap/+page.svelte | 3 +-- 7 files changed, 37 insertions(+), 10 deletions(-) diff --git a/packages/layerchart/src/lib/components/GeoPath.svelte b/packages/layerchart/src/lib/components/GeoPath.svelte index 0b38185ad..cda13f2f2 100644 --- a/packages/layerchart/src/lib/components/GeoPath.svelte +++ b/packages/layerchart/src/lib/components/GeoPath.svelte @@ -9,6 +9,7 @@ } from 'd3-geo'; import { cls } from '@layerstack/tailwind'; import { computedStyles } from '@layerstack/svelte-actions'; + import { merge } from 'lodash-es'; import { geoContext } from './GeoContext.svelte'; import type { TooltipContextValue } from './tooltip/TooltipContext.svelte'; @@ -67,6 +68,11 @@ $: _projection = geoTransform ? d3geoTransform(geoTransform($geo)) : $geo; $: geoPath = geoCurvePath(_projection, curve); + $: { + // Recreate `geoPath()` if `geojson` data changes (fixes ghosting issue when rendering to canvas) + geojson; + geoPath = geoCurvePath(_projection, curve); + } const canvasContext = getCanvasContext(); const renderContext = canvasContext ? 'canvas' : 'svg'; @@ -79,8 +85,7 @@ if (geojson) { // console.log('rendering', _styles.fill); const pathData = geoPath(geojson); - // renderPathData(ctx, pathData, { ..._styles, fill, stroke, strokeWidth }); - renderPathData(ctx, pathData, { ..._styles }); + renderPathData(ctx, pathData, merge({}, _styles, { fill, stroke, strokeWidth })); } } } @@ -91,7 +96,7 @@ $: if (renderContext === 'canvas') { // Redraw when geojson, projection, or class change - geojson && _projection && className; + geojson && _projection && className && fill && stroke && strokeWidth; canvasContext.invalidate(); } diff --git a/packages/layerchart/src/lib/components/layout/Canvas.svelte b/packages/layerchart/src/lib/components/layout/Canvas.svelte index c4bbbdeec..03e7dfcd4 100644 --- a/packages/layerchart/src/lib/components/layout/Canvas.svelte +++ b/packages/layerchart/src/lib/components/layout/Canvas.svelte @@ -77,11 +77,13 @@ function update() { if (!context) return; + // TODO: only `scaleCanvas()` when containerWidth/Height change (not all invalidations) + // scaleCanvas in `update()` to fix `requestAnimationFrame()` timing causing flash of blank canvas scaleCanvas(context, $containerWidth, $containerHeight); + context.clearRect(0, 0, $containerWidth, $containerHeight); context.translate($padding.left ?? 0, $padding.top ?? 0); - if (mode === 'canvas') { const center = { x: $width / 2, y: $height / 2 }; const newTranslate = { @@ -92,7 +94,6 @@ context.scale($scale, $scale); } - // console.log({ drawFunctions }); drawFunctions.forEach((fn) => { context.save(); fn(context); @@ -102,7 +103,7 @@ pendingInvalidation = false; } - $: setCanvasContext({ + const canvasContext: CanvasContext = { register(fn) { drawFunctions.push(fn); this.invalidate(); @@ -116,7 +117,15 @@ pendingInvalidation = true; frameId = requestAnimationFrame(update); }, - }); + }; + + $: { + // Redraw when resized + $containerWidth, $containerHeight; + canvasContext.invalidate(); + } + + setCanvasContext(canvasContext); + + + {#if tooltip.data} {/if} diff --git a/packages/layerchart/src/routes/docs/examples/AnimatedGlobe/+page.svelte b/packages/layerchart/src/routes/docs/examples/AnimatedGlobe/+page.svelte index e56ee8f2b..4868913f3 100644 --- a/packages/layerchart/src/routes/docs/examples/AnimatedGlobe/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/AnimatedGlobe/+page.svelte @@ -228,6 +228,10 @@ + + + + {#if tooltip.data} {/if} diff --git a/packages/layerchart/src/routes/docs/examples/GeoPath/+page.svelte b/packages/layerchart/src/routes/docs/examples/GeoPath/+page.svelte index b6be831cd..d9f85518d 100644 --- a/packages/layerchart/src/routes/docs/examples/GeoPath/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/GeoPath/+page.svelte @@ -66,6 +66,10 @@ + + + + {#if tooltip.data} {/if} diff --git a/packages/layerchart/src/routes/docs/examples/ZoomableMap/+page.svelte b/packages/layerchart/src/routes/docs/examples/ZoomableMap/+page.svelte index 78e4761cc..b631789d6 100644 --- a/packages/layerchart/src/routes/docs/examples/ZoomableMap/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/ZoomableMap/+page.svelte @@ -353,9 +353,7 @@ class="stroke-surface-content fill-surface-100 hover:fill-surface-content/10" strokeWidth={1 / transform.scale} /> - - --> + {#if tooltip.data} Date: Tue, 7 Jan 2025 13:23:42 -0500 Subject: [PATCH 16/55] Fix `pnpm check` errors/warnings --- packages/layerchart/src/lib/components/GeoTile.svelte | 2 +- packages/layerchart/src/lib/components/Points.svelte | 3 +-- packages/layerchart/src/lib/utils/canvas.ts | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/layerchart/src/lib/components/GeoTile.svelte b/packages/layerchart/src/lib/components/GeoTile.svelte index b365eec72..5aab2e67c 100644 --- a/packages/layerchart/src/lib/components/GeoTile.svelte +++ b/packages/layerchart/src/lib/components/GeoTile.svelte @@ -46,7 +46,7 @@ }); } - $: if (renderContext === 'canvas' && url) { + $: if (renderContext === 'canvas') { canvasContext.register(render); } diff --git a/packages/layerchart/src/lib/components/Points.svelte b/packages/layerchart/src/lib/components/Points.svelte index 6bb9efd2d..6b6ee8bf0 100644 --- a/packages/layerchart/src/lib/components/Points.svelte +++ b/packages/layerchart/src/lib/components/Points.svelte @@ -15,7 +15,7 @@ import Link from './Link.svelte'; import { isScaleBand, type AnyScale } from '../utils/scales.js'; import { getCanvasContext } from './layout/Canvas.svelte'; - import { DEFAULT_FILL, renderPathData } from '../utils/canvas.js'; + import { renderPathData } from '../utils/canvas.js'; import { circlePath } from 'layerchart/utils/path.js'; const context = chartContext() as any; @@ -47,7 +47,6 @@ export let fill: string | undefined = undefined; export let stroke: string | undefined = undefined; - export let strokeWidth: number | string | undefined = undefined; /** Render to canvas */ export let render: ((ctx: CanvasRenderingContext2D, points: Point[]) => any) | undefined = diff --git a/packages/layerchart/src/lib/utils/canvas.ts b/packages/layerchart/src/lib/utils/canvas.ts index 621a0201a..acf0ea38b 100644 --- a/packages/layerchart/src/lib/utils/canvas.ts +++ b/packages/layerchart/src/lib/utils/canvas.ts @@ -4,7 +4,7 @@ export const DEFAULT_FILL = 'rgb(0, 0, 0)'; export function renderPathData( canvasCtx: CanvasRenderingContext2D, pathData: string | null | undefined, - styles: Partial = {} + styles: Partial & { strokeWidth?: number | string }> = {} ) { const path = new Path2D(pathData ?? ''); From f91c40cb85e43b2ad792009ffda91532c1ea590e Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Tue, 7 Jan 2025 17:42:17 -0500 Subject: [PATCH 17/55] docs(ZoomableMap): Hide tooltip when selecting state on canvas examples --- .../src/routes/docs/examples/ZoomableMap/+page.svelte | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/layerchart/src/routes/docs/examples/ZoomableMap/+page.svelte b/packages/layerchart/src/routes/docs/examples/ZoomableMap/+page.svelte index b631789d6..92ac902a2 100644 --- a/packages/layerchart/src/routes/docs/examples/ZoomableMap/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/ZoomableMap/+page.svelte @@ -283,6 +283,7 @@ transform.reset(); } else { selectedStateId = feature.id; + tooltip.hide(); const featureTransform = geoFitObjectTransform(projection, [width, height], feature); transform.setTranslate(featureTransform.translate); transform.setScale(featureTransform.scale); @@ -397,6 +398,7 @@ transform.reset(); } else { selectedStateId = feature.id; + tooltip.hide(); let [[left, top], [right, bottom]] = geoPath.bounds(feature); let width = right - left; let height = bottom - top; From 71f2f1963caed5a03dbc93945dca75b5fa99199f Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Thu, 9 Jan 2025 14:14:58 -0500 Subject: [PATCH 18/55] Add and use new `getComputedStyles()` to create/reuse single `svg` when calculating styles for canvas (resolving CSS variables or classes). --- .../layerchart/src/lib/components/Area.svelte | 20 +++--- .../src/lib/components/Circle.svelte | 22 +++---- .../src/lib/components/GeoPath.svelte | 17 ++--- .../src/lib/components/GeoPoint.svelte | 12 +--- .../src/lib/components/Points.svelte | 13 +--- .../src/lib/components/Spline.svelte | 22 +++---- packages/layerchart/src/lib/utils/canvas.ts | 66 +++++++++++++++++-- .../docs/components/HitCanvas/+page.svelte | 2 +- .../docs/examples/AnimatedGlobe/+page.svelte | 2 +- .../docs/examples/BubbleMap/+page.svelte | 10 +-- .../docs/examples/Choropleth/+page.svelte | 4 +- .../routes/docs/examples/GeoPath/+page.svelte | 2 +- .../docs/examples/GeoPoint/+page.svelte | 54 ++++++++------- .../routes/docs/examples/Pack/+page.svelte | 4 +- .../docs/examples/SpikeMap/+page.svelte | 2 +- .../docs/examples/ZoomableMap/+page.svelte | 8 +-- .../docs/tools/GeojsonPreview/+page.svelte | 18 +++-- .../docs/tools/TopojsonPreview/+page.svelte | 18 +++-- 18 files changed, 165 insertions(+), 131 deletions(-) diff --git a/packages/layerchart/src/lib/components/Area.svelte b/packages/layerchart/src/lib/components/Area.svelte index 2f245484f..fbf19f42f 100644 --- a/packages/layerchart/src/lib/components/Area.svelte +++ b/packages/layerchart/src/lib/components/Area.svelte @@ -6,7 +6,6 @@ import { max, min } from 'd3-array'; import { interpolatePath } from 'd3-interpolate-path'; - import { computedStyles } from '@layerstack/svelte-actions'; import { cls } from '@layerstack/tailwind'; import { motionStore } from '$lib/stores/motionStore.js'; @@ -55,6 +54,10 @@ /** Enable showing line */ export let line: boolean | Partial> = false; + export let fill: string | undefined = undefined; + export let stroke: string | undefined = undefined; + export let strokeWidth: number | undefined = undefined; + const xAccessor = x ? accessor(x) : $contextX; const y0Accessor = y0 ? accessor(y0) : (d: any) => min($yDomain); const y1Accessor = y1 ? accessor(y1) : $y; @@ -131,11 +134,12 @@ const canvasContext = getCanvasContext(); const renderContext = canvasContext ? 'canvas' : 'svg'; - let _styles: CSSStyleDeclaration; function render(ctx: CanvasRenderingContext2D) { - // TODO: Only apply `stroke-` to `Spline` - renderPathData(ctx, $tweened_d, _styles); + renderPathData(ctx, $tweened_d, { + styles: { fill, stroke, strokeWidth }, + classes: $$props.class, + }); } $: if (renderContext === 'canvas') { @@ -178,11 +182,3 @@ on:pointerleave /> {/if} - - -{#if renderContext === 'canvas'} -
(_styles = styles)} - >
-{/if} diff --git a/packages/layerchart/src/lib/components/Circle.svelte b/packages/layerchart/src/lib/components/Circle.svelte index 3fe82e4f5..85d309627 100644 --- a/packages/layerchart/src/lib/components/Circle.svelte +++ b/packages/layerchart/src/lib/components/Circle.svelte @@ -7,7 +7,6 @@ import { getCanvasContext } from './layout/Canvas.svelte'; import { circlePath } from '../utils/path.js'; import { renderPathData } from '../utils/canvas.js'; - import { computedStyles } from '@layerstack/svelte-actions'; export let cx: number = 0; export let initialCx = cx; @@ -21,6 +20,10 @@ export let spring: boolean | Parameters[1] = undefined; export let tweened: boolean | Parameters[1] = undefined; + export let fill: string | undefined = undefined; + export let stroke: string | undefined = undefined; + export let strokeWidth: number | undefined = undefined; + let tweened_cx = motionStore(initialCx, { spring, tweened }); let tweened_cy = motionStore(initialCy, { spring, tweened }); let tweened_r = motionStore(initialR, { spring, tweened }); @@ -33,11 +36,13 @@ const canvasContext = getCanvasContext(); const renderContext = canvasContext ? 'canvas' : 'svg'; - let _styles: CSSStyleDeclaration; function render(ctx: CanvasRenderingContext2D) { const pathData = circlePath({ cx: $tweened_cx, cy: $tweened_cy, r: $tweened_r }); - renderPathData(ctx, pathData, _styles); + renderPathData(ctx, pathData, { + styles: { fill, stroke, strokeWidth }, + classes: $$props.class, + }); } $: if (renderContext === 'canvas') { @@ -57,6 +62,9 @@ cx={$tweened_cx} cy={$tweened_cy} r={$tweened_r} + {fill} + {stroke} + stroke-width={strokeWidth} class={cls($$props.fill == null && 'fill-surface-content')} {...$$restProps} on:click @@ -64,11 +72,3 @@ on:pointerleave /> {/if} - - -{#if renderContext === 'canvas'} -
(_styles = styles)} - >
-{/if} diff --git a/packages/layerchart/src/lib/components/GeoPath.svelte b/packages/layerchart/src/lib/components/GeoPath.svelte index cda13f2f2..598f1e7c2 100644 --- a/packages/layerchart/src/lib/components/GeoPath.svelte +++ b/packages/layerchart/src/lib/components/GeoPath.svelte @@ -8,8 +8,6 @@ type GeoTransformPrototype, } from 'd3-geo'; import { cls } from '@layerstack/tailwind'; - import { computedStyles } from '@layerstack/svelte-actions'; - import { merge } from 'lodash-es'; import { geoContext } from './GeoContext.svelte'; import type { TooltipContextValue } from './tooltip/TooltipContext.svelte'; @@ -76,16 +74,17 @@ const canvasContext = getCanvasContext(); const renderContext = canvasContext ? 'canvas' : 'svg'; - let _styles: CSSStyleDeclaration; function _render(ctx: CanvasRenderingContext2D) { if (render) { render(ctx, { newGeoPath: () => geoCurvePath(_projection, curve) }); } else { if (geojson) { - // console.log('rendering', _styles.fill); const pathData = geoPath(geojson); - renderPathData(ctx, pathData, merge({}, _styles, { fill, stroke, strokeWidth })); + renderPathData(ctx, pathData, { + styles: { fill, stroke, strokeWidth }, + classes: className, + }); } } } @@ -128,11 +127,3 @@ /> {/if} - - -{#if renderContext === 'canvas'} -
(_styles = styles)} - >
-{/if} diff --git a/packages/layerchart/src/lib/components/GeoPoint.svelte b/packages/layerchart/src/lib/components/GeoPoint.svelte index 099b468da..b88dcd0f7 100644 --- a/packages/layerchart/src/lib/components/GeoPoint.svelte +++ b/packages/layerchart/src/lib/components/GeoPoint.svelte @@ -5,8 +5,6 @@ import Circle from './Circle.svelte'; import Group from './Group.svelte'; import { getCanvasContext } from './layout/Canvas.svelte'; - import { cls } from '@layerstack/tailwind'; - import { computedStyles } from '@layerstack/svelte-actions'; /** Latitude */ export let lat: number; @@ -25,7 +23,6 @@ const canvasContext = getCanvasContext(); const renderContext = canvasContext ? 'canvas' : 'svg'; - let _styles: CSSStyleDeclaration; function _render(ctx: CanvasRenderingContext2D) { render(ctx, { x, y }); @@ -52,18 +49,13 @@ {/if} {/if} - {#if renderContext === 'canvas'} {#if $$slots.default} + {:else} - + {/if} - -
(_styles = styles)} - >
{/if} diff --git a/packages/layerchart/src/lib/components/Points.svelte b/packages/layerchart/src/lib/components/Points.svelte index 6b6ee8bf0..9706ba4c7 100644 --- a/packages/layerchart/src/lib/components/Points.svelte +++ b/packages/layerchart/src/lib/components/Points.svelte @@ -7,8 +7,6 @@ import { extent } from 'd3-array'; import { pointRadial } from 'd3-shape'; import { notNull } from '@layerstack/utils'; - import { cls } from '@layerstack/tailwind'; - import { computedStyles } from '@layerstack/svelte-actions'; import { chartContext } from './ChartContext.svelte'; import Circle from './Circle.svelte'; @@ -164,7 +162,6 @@ const canvasContext = getCanvasContext(); const renderContext = canvasContext ? 'canvas' : 'svg'; - let _styles: CSSStyleDeclaration; function _render(ctx: CanvasRenderingContext2D) { if (render) { @@ -172,7 +169,7 @@ } else { points.forEach((point) => { const pathData = circlePath({ cx: point.x, cy: point.y, r: point.r }); - renderPathData(ctx, pathData, _styles); + renderPathData(ctx, pathData, { styles: { fill, stroke }, classes: className }); }); } } @@ -218,11 +215,3 @@ {/if}
- - -{#if renderContext === 'canvas'} -
(_styles = styles)} - >
-{/if} diff --git a/packages/layerchart/src/lib/components/Spline.svelte b/packages/layerchart/src/lib/components/Spline.svelte index f8239d54e..1b39cdfb8 100644 --- a/packages/layerchart/src/lib/components/Spline.svelte +++ b/packages/layerchart/src/lib/components/Spline.svelte @@ -12,7 +12,6 @@ import { max } from 'd3-array'; import { cls } from '@layerstack/tailwind'; import { uniqueId } from '@layerstack/utils'; - import { computedStyles } from '@layerstack/svelte-actions'; import { chartContext } from './ChartContext.svelte'; import Group from './Group.svelte'; @@ -64,6 +63,10 @@ export let curve: CurveFactory | CurveFactoryLineOnly | undefined = undefined; export let defined: Parameters['defined']>[0] | undefined = undefined; + export let fill: string | undefined = undefined; + export let stroke: string | undefined = undefined; + export let strokeWidth: number | undefined = undefined; + /** Marker to attach to start, mid, and end points of path */ export let marker: ComponentProps['type'] | ComponentProps | undefined = undefined; @@ -158,10 +161,12 @@ const canvasContext = getCanvasContext(); const renderContext = canvasContext ? 'canvas' : 'svg'; - let _styles: CSSStyleDeclaration; function render(ctx: CanvasRenderingContext2D) { - renderPathData(ctx, $tweened_d, _styles); + renderPathData(ctx, $tweened_d, { + styles: { stroke, fill, strokeWidth }, + classes: $$props.class, + }); } $: if (renderContext === 'canvas') { @@ -224,6 +229,9 @@ !$$props.stroke && 'stroke-surface-content', $$props.class )} + {fill} + {stroke} + stroke-width={strokeWidth} marker-start={markerStartId ? `url(#${markerStartId})` : undefined} marker-mid={markerMidId ? `url(#${markerMidId})` : undefined} marker-end={markerEndId ? `url(#${markerEndId})` : undefined} @@ -274,11 +282,3 @@ {/if} {/key} {/if} - - -{#if renderContext === 'canvas'} -
(_styles = styles)} - >
-{/if} diff --git a/packages/layerchart/src/lib/utils/canvas.ts b/packages/layerchart/src/lib/utils/canvas.ts index acf0ea38b..f74036683 100644 --- a/packages/layerchart/src/lib/utils/canvas.ts +++ b/packages/layerchart/src/lib/utils/canvas.ts @@ -1,25 +1,77 @@ export const DEFAULT_FILL = 'rgb(0, 0, 0)'; -/** Render SVG path data onto canvas context. Supports CSS classes tranferring to `` element for retrieval) */ +const CANVAS_STYLES_ELEMENT_ID = '__layerchart_canvas_styles_id'; + +type ComputedStylesOptions = { + styles?: Partial & { strokeWidth?: number | string }>; + classes?: string; +}; + +/** + * Appends or reuses `` element below `` to resolve CSS variables and classes (ex. `stroke: hsl(var(--color-primary))` => `stroke: rgb(...)` ) + */ +function getComputedStyles( + canvas: HTMLCanvasElement, + { styles, classes }: ComputedStylesOptions = {} +) { + try { + // Get or create `` below `` + let svg = document.getElementById(CANVAS_STYLES_ELEMENT_ID) as SVGElement | null; + + if (!svg) { + svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.setAttribute('id', CANVAS_STYLES_ELEMENT_ID); + svg.style.display = 'none'; + // Add `` next to `` to allow same scope resolution for CSS variables + canvas.after(svg); + } + svg = svg!; // guarantee SVG is set + + // Remove any previously set styles or classes. Not able to do as part of cleanup below as `window.getComputedStyles()` appearing to be lazily read and removing `style` results in incorrect values, and copying result is very slow + svg.removeAttribute('style'); + svg.removeAttribute('class'); + + // Add styles and class to svg element + if (styles) { + Object.assign(svg.style, styles); + } + + if (classes) { + svg.setAttribute('class', classes); + } + + const computedStyles = window.getComputedStyle(svg); + return computedStyles; + } catch (e) { + console.error('Unable to get computed styles', e); + return null; + } +} + +/** Render SVG path data onto canvas context. Supports CSS variables and classes by tranferring to hidden `` element before retrieval) */ export function renderPathData( canvasCtx: CanvasRenderingContext2D, pathData: string | null | undefined, - styles: Partial & { strokeWidth?: number | string }> = {} + styleOptions: ComputedStylesOptions = {} ) { const path = new Path2D(pathData ?? ''); - const fill = styles.fill === DEFAULT_FILL ? null : styles.fill; + // TODO: Consider memoizing? How about reactiving to CSS variable changes (light/dark mode toggle) + const computedStyles = getComputedStyles(canvasCtx.canvas, styleOptions); + + const fill = computedStyles?.fill === DEFAULT_FILL ? null : computedStyles?.fill; if (fill) { canvasCtx.fillStyle = fill; canvasCtx.fill(path); } - const stroke = styles.stroke === 'none' ? null : styles.stroke; + const stroke = computedStyles?.stroke === 'none' ? null : computedStyles?.stroke; if (stroke) { canvasCtx.lineWidth = - typeof styles.strokeWidth === 'string' - ? Number(styles.strokeWidth?.replace('px', '')) - : (styles.strokeWidth ?? 0); + typeof computedStyles?.strokeWidth === 'string' + ? Number(computedStyles?.strokeWidth?.replace('px', '')) + : (computedStyles?.strokeWidth ?? 1); + canvasCtx.strokeStyle = stroke; canvasCtx.stroke(path); } diff --git a/packages/layerchart/src/routes/docs/components/HitCanvas/+page.svelte b/packages/layerchart/src/routes/docs/components/HitCanvas/+page.svelte index f1bfb2588..13d439205 100644 --- a/packages/layerchart/src/routes/docs/components/HitCanvas/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/HitCanvas/+page.svelte @@ -62,7 +62,7 @@ const geoPath = newGeoPath(); // Stroking shape seems to help with dark border, but there is still antialising and thus gaps - renderPathData(ctx, geoPath(feature), { fill: color, stroke: color }); + renderPathData(ctx, geoPath(feature), { styles: { fill: color, stroke: color } }); setColorData(color, feature); } diff --git a/packages/layerchart/src/routes/docs/examples/AnimatedGlobe/+page.svelte b/packages/layerchart/src/routes/docs/examples/AnimatedGlobe/+page.svelte index 4868913f3..8ab98f938 100644 --- a/packages/layerchart/src/routes/docs/examples/AnimatedGlobe/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/AnimatedGlobe/+page.svelte @@ -253,7 +253,7 @@ const geoPath = newGeoPath(); // Stroking shape seems to help with dark border, but there is still antialising and thus gaps - renderPathData(ctx, geoPath(feature), { fill: color, stroke: color }); + renderPathData(ctx, geoPath(feature), { styles: { fill: color, stroke: color } }); setColorData(color, feature); } diff --git a/packages/layerchart/src/routes/docs/examples/BubbleMap/+page.svelte b/packages/layerchart/src/routes/docs/examples/BubbleMap/+page.svelte index 22fdf0d85..c1e83aa0c 100644 --- a/packages/layerchart/src/routes/docs/examples/BubbleMap/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/BubbleMap/+page.svelte @@ -199,9 +199,11 @@ const pathData = circlePath({ cx, cy, r }); renderPathData(ctx, pathData, { - fill: color + (256 * 0.5).toString(16), - stroke: color, - strokeWidth, + styles: { + fill: color + (256 * 0.5).toString(16), + stroke: color, + strokeWidth, + }, }); } }} @@ -231,7 +233,7 @@ const geoPath = newGeoPath(); // Stroking shape seems to help with dark border, but there is still antialising and thus gaps - renderPathData(ctx, geoPath(feature), { fill: color, stroke: color }); + renderPathData(ctx, geoPath(feature), { styles: { fill: color, stroke: color } }); setColorData(color, feature); } diff --git a/packages/layerchart/src/routes/docs/examples/Choropleth/+page.svelte b/packages/layerchart/src/routes/docs/examples/Choropleth/+page.svelte index 97452ee50..034037da8 100644 --- a/packages/layerchart/src/routes/docs/examples/Choropleth/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/Choropleth/+page.svelte @@ -163,7 +163,7 @@ for (var feature of enrichedCountiesFeatures) { const geoPath = newGeoPath(); renderPathData(ctx, geoPath(feature), { - fill: colorScale(feature.properties.data?.population ?? 0), + styles: { fill: colorScale(feature.properties.data?.population ?? 0) }, }); } }} @@ -190,7 +190,7 @@ const geoPath = newGeoPath(); // Stroking shape seems to help with dark border, but there is still antialising and thus gaps - renderPathData(ctx, geoPath(feature), { fill: color, stroke: color }); + renderPathData(ctx, geoPath(feature), { styles: { fill: color, stroke: color } }); setColorData(color, feature); } diff --git a/packages/layerchart/src/routes/docs/examples/GeoPath/+page.svelte b/packages/layerchart/src/routes/docs/examples/GeoPath/+page.svelte index d9f85518d..50b13a8bf 100644 --- a/packages/layerchart/src/routes/docs/examples/GeoPath/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/GeoPath/+page.svelte @@ -87,7 +87,7 @@ const color = nextColor(); const geoPath = newGeoPath(); // Stroking shape seems to help with dark border, but there is still antialising and thus gaps - renderPathData(ctx, geoPath(feature), { fill: color, stroke: color }); + renderPathData(ctx, geoPath(feature), { styles: { fill: color, stroke: color } }); setColorData(color, feature); } }} diff --git a/packages/layerchart/src/routes/docs/examples/GeoPoint/+page.svelte b/packages/layerchart/src/routes/docs/examples/GeoPoint/+page.svelte index 2df8ac9e5..7db336986 100644 --- a/packages/layerchart/src/routes/docs/examples/GeoPoint/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/GeoPoint/+page.svelte @@ -269,35 +269,33 @@ - - - {#each data.us.captitals as capital} - - { - const pathData = circlePath({ cx: x, cy: y, r: 2 }); - renderPathData(ctx, pathData, pointStyles); - }} - /> + + {#each data.us.captitals as capital} + + { + const pathData = circlePath({ cx: x, cy: y, r: 2 }); + renderPathData(ctx, pathData, { classes: 'fill-white stroke-danger' }); + }} + /> - - { - ctx.font = '8px sans-serif'; - ctx.textAlign = 'center'; - ctx.lineWidth = 2; - ctx.strokeStyle = labelStyles.stroke; - ctx.fillStyle = labelStyles.fill; - ctx.strokeText(capital.description, x, y - 6); - ctx.fillText(capital.description, x, y - 6); - }} - /> - {/each} - + + { + ctx.font = '8px sans-serif'; + ctx.textAlign = 'center'; + ctx.lineWidth = 2; + ctx.strokeStyle = labelStyles.stroke; + ctx.fillStyle = labelStyles.fill; + ctx.strokeText(capital.description, x, y - 6); + ctx.fillText(capital.description, x, y - 6); + }} + /> + {/each} diff --git a/packages/layerchart/src/routes/docs/examples/Pack/+page.svelte b/packages/layerchart/src/routes/docs/examples/Pack/+page.svelte index a686b0e2b..d416082d2 100644 --- a/packages/layerchart/src/routes/docs/examples/Pack/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/Pack/+page.svelte @@ -114,7 +114,9 @@ {@const nodeColor = getNodeColor(node, colorBy)} diff --git a/packages/layerchart/src/routes/docs/examples/SpikeMap/+page.svelte b/packages/layerchart/src/routes/docs/examples/SpikeMap/+page.svelte index 54984dd29..f1f3ab409 100644 --- a/packages/layerchart/src/routes/docs/examples/SpikeMap/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/SpikeMap/+page.svelte @@ -220,7 +220,7 @@ const color = nextColor(); const geoPath = newGeoPath(); - renderPathData(ctx, geoPath(feature), { fill: color, stroke: color }); + renderPathData(ctx, geoPath(feature), { styles: { fill: color, stroke: color } }); setColorData(color, feature); } diff --git a/packages/layerchart/src/routes/docs/examples/ZoomableMap/+page.svelte b/packages/layerchart/src/routes/docs/examples/ZoomableMap/+page.svelte index 92ac902a2..67455a8d9 100644 --- a/packages/layerchart/src/routes/docs/examples/ZoomableMap/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/ZoomableMap/+page.svelte @@ -297,7 +297,7 @@ const geoPath = newGeoPath(); // Stroking shape seems to help with dark border, but there is still antialising and thus gaps - renderPathData(ctx, geoPath(feature), { fill: color, stroke: color }); + renderPathData(ctx, geoPath(feature), { styles: { fill: color, stroke: color } }); setColorData(color, feature); } @@ -308,7 +308,7 @@ const geoPath = newGeoPath(); // Stroking shape seems to help with dark border, but there is still antialising and thus gaps - renderPathData(ctx, geoPath(feature), { fill: color, stroke: color }); + renderPathData(ctx, geoPath(feature), { styles: { fill: color, stroke: color } }); setColorData(color, feature); } @@ -416,7 +416,7 @@ const color = nextColor(); const geoPath = newGeoPath(); - renderPathData(ctx, geoPath(feature), { fill: color, stroke: color }); + renderPathData(ctx, geoPath(feature), { styles: { fill: color, stroke: color } }); setColorData(color, feature); } @@ -426,7 +426,7 @@ const color = nextColor(); const geoPath = newGeoPath(); - renderPathData(ctx, geoPath(feature), { fill: color, stroke: color }); + renderPathData(ctx, geoPath(feature), { styles: { fill: color, stroke: color } }); setColorData(color, feature); } diff --git a/packages/layerchart/src/routes/docs/tools/GeojsonPreview/+page.svelte b/packages/layerchart/src/routes/docs/tools/GeojsonPreview/+page.svelte index cc151e338..f73aa8cdd 100644 --- a/packages/layerchart/src/routes/docs/tools/GeojsonPreview/+page.svelte +++ b/packages/layerchart/src/routes/docs/tools/GeojsonPreview/+page.svelte @@ -128,8 +128,10 @@ for (var feature of features) { const geoPath = newGeoPath(); renderPathData(ctx, geoPath(feature), { - fill: colorScale(String(feature.id)), - stroke: 'black', + styles: { + fill: colorScale(String(feature.id)), + stroke: 'black', + }, }); } }} @@ -141,8 +143,10 @@ for (var feature of features) { const geoPath = newGeoPath(); renderPathData(ctx, geoPath(feature), { - fill: colorScale(String(feature.id)), - stroke: 'black', + styles: { + fill: colorScale(String(feature.id)), + stroke: 'black', + }, }); } }} @@ -163,8 +167,10 @@ const geoPath = newGeoPath(); renderPathData(ctx, geoPath(feature), { - fill: color, - stroke: color, + styles: { + fill: color, + stroke: color, + }, }); setColorData(color, feature); diff --git a/packages/layerchart/src/routes/docs/tools/TopojsonPreview/+page.svelte b/packages/layerchart/src/routes/docs/tools/TopojsonPreview/+page.svelte index 431ece5cf..7f746bcb4 100644 --- a/packages/layerchart/src/routes/docs/tools/TopojsonPreview/+page.svelte +++ b/packages/layerchart/src/routes/docs/tools/TopojsonPreview/+page.svelte @@ -135,8 +135,10 @@ for (var feature of features) { const geoPath = newGeoPath(); renderPathData(ctx, geoPath(feature), { - fill: colorScale(String(feature.id)), - stroke: 'black', + styles: { + fill: colorScale(String(feature.id)), + stroke: 'black', + }, }); } }} @@ -148,8 +150,10 @@ for (var feature of features) { const geoPath = newGeoPath(); renderPathData(ctx, geoPath(feature), { - fill: colorScale(String(feature.id)), - stroke: 'black', + styles: { + fill: colorScale(String(feature.id)), + stroke: 'black', + }, }); } }} @@ -170,8 +174,10 @@ const geoPath = newGeoPath(); renderPathData(ctx, geoPath(feature), { - fill: color, - stroke: color, + styles: { + fill: color, + stroke: color, + }, }); setColorData(color, feature); From df87438a4ec1010d11219ece5aec71773cb5e86c Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Thu, 9 Jan 2025 14:25:51 -0500 Subject: [PATCH 19/55] fix(Circle): Redraw on position changes (fix tooltip highlight) --- packages/layerchart/src/lib/components/Circle.svelte | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/layerchart/src/lib/components/Circle.svelte b/packages/layerchart/src/lib/components/Circle.svelte index 85d309627..1f9e46de0 100644 --- a/packages/layerchart/src/lib/components/Circle.svelte +++ b/packages/layerchart/src/lib/components/Circle.svelte @@ -49,6 +49,12 @@ canvasContext.register(render); } + $: if (renderContext === 'canvas') { + // Redraw when props changes (TODO: styles, class, etc) + $tweened_cx && $tweened_cy && $tweened_r; + canvasContext.invalidate(); + } + onDestroy(() => { if (renderContext === 'canvas') { canvasContext.deregister(render); From 50681a8528833a9120e034e96b07b93f096dba85 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Thu, 9 Jan 2025 14:59:20 -0500 Subject: [PATCH 20/55] fix(renderPathData()): Adhere to CSS paint order: https://developer.mozilla.org/en-US/docs/Web/CSS/paint-order --- packages/layerchart/src/lib/utils/canvas.ts | 40 ++++++++++++--------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/packages/layerchart/src/lib/utils/canvas.ts b/packages/layerchart/src/lib/utils/canvas.ts index f74036683..51973a090 100644 --- a/packages/layerchart/src/lib/utils/canvas.ts +++ b/packages/layerchart/src/lib/utils/canvas.ts @@ -59,22 +59,30 @@ export function renderPathData( // TODO: Consider memoizing? How about reactiving to CSS variable changes (light/dark mode toggle) const computedStyles = getComputedStyles(canvasCtx.canvas, styleOptions); - const fill = computedStyles?.fill === DEFAULT_FILL ? null : computedStyles?.fill; - if (fill) { - canvasCtx.fillStyle = fill; - canvasCtx.fill(path); - } - - const stroke = computedStyles?.stroke === 'none' ? null : computedStyles?.stroke; - if (stroke) { - canvasCtx.lineWidth = - typeof computedStyles?.strokeWidth === 'string' - ? Number(computedStyles?.strokeWidth?.replace('px', '')) - : (computedStyles?.strokeWidth ?? 1); - - canvasCtx.strokeStyle = stroke; - canvasCtx.stroke(path); - } + // Adhere to CSS paint order: https://developer.mozilla.org/en-US/docs/Web/CSS/paint-order + const paintOrder = + computedStyles?.paintOrder === 'stroke' ? ['stroke', 'fill'] : ['fill', 'stroke']; + + paintOrder.forEach((attr) => { + if (attr === 'fill') { + const fill = computedStyles?.fill === DEFAULT_FILL ? null : computedStyles?.fill; + if (fill) { + canvasCtx.fillStyle = fill; + canvasCtx.fill(path); + } + } else if (attr === 'stroke') { + const stroke = computedStyles?.stroke === 'none' ? null : computedStyles?.stroke; + if (stroke) { + canvasCtx.lineWidth = + typeof computedStyles?.strokeWidth === 'string' + ? Number(computedStyles?.strokeWidth?.replace('px', '')) + : (computedStyles?.strokeWidth ?? 1); + + canvasCtx.strokeStyle = stroke; + canvasCtx.stroke(path); + } + } + }); } /** Clear canvas accounting for Canvas `context.translate(...)` */ From 2e5db72c01c39381a785fea676955ab95792bd01 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Thu, 9 Jan 2025 15:24:11 -0500 Subject: [PATCH 21/55] fix(Rule): Remove unnecessary classes --- packages/layerchart/src/lib/components/Rule.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/layerchart/src/lib/components/Rule.svelte b/packages/layerchart/src/lib/components/Rule.svelte index 704e87f53..3c5ad1edc 100644 --- a/packages/layerchart/src/lib/components/Rule.svelte +++ b/packages/layerchart/src/lib/components/Rule.svelte @@ -67,7 +67,7 @@ {x2} {y2} {...$$restProps} - class={cls('test grid stroke-surface-content/10', $$props.class)} + class={cls('stroke-surface-content/10', $$props.class)} /> {:else} Date: Thu, 9 Jan 2025 15:54:13 -0500 Subject: [PATCH 22/55] feat(Line): Support Canvas context --- .changeset/calm-bikes-walk.md | 5 + .changeset/flat-pants-run.md | 4 +- .../layerchart/src/lib/components/Line.svelte | 100 ++++++++++++------ 3 files changed, 76 insertions(+), 33 deletions(-) create mode 100644 .changeset/calm-bikes-walk.md diff --git a/.changeset/calm-bikes-walk.md b/.changeset/calm-bikes-walk.md new file mode 100644 index 000000000..d0e16c7ec --- /dev/null +++ b/.changeset/calm-bikes-walk.md @@ -0,0 +1,5 @@ +--- +'layerchart': minor +--- + +feat(Line): Support Canvas context diff --git a/.changeset/flat-pants-run.md b/.changeset/flat-pants-run.md index b858f8314..666718668 100644 --- a/.changeset/flat-pants-run.md +++ b/.changeset/flat-pants-run.md @@ -1,5 +1,5 @@ --- -'layerchart': patch +'layerchart': minor --- -feat(Circle): Support Canvas render context +feat(Circle): Support Canvas context diff --git a/packages/layerchart/src/lib/components/Line.svelte b/packages/layerchart/src/lib/components/Line.svelte index 957a3e627..0c5867601 100644 --- a/packages/layerchart/src/lib/components/Line.svelte +++ b/packages/layerchart/src/lib/components/Line.svelte @@ -1,5 +1,5 @@ - - - - - {#if markerStart} +{#if renderContext === 'svg'} + + + + + {#if markerStart} + + {/if} + + + - {/if} - - - - - + +{/if} From 59807f80941041dfa3417ce12f720542b5c9a303 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Thu, 9 Jan 2025 16:02:49 -0500 Subject: [PATCH 23/55] fix(Line): Use tweened coords when rendering via canvas --- packages/layerchart/src/lib/components/Line.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/layerchart/src/lib/components/Line.svelte b/packages/layerchart/src/lib/components/Line.svelte index 0c5867601..a4b069544 100644 --- a/packages/layerchart/src/lib/components/Line.svelte +++ b/packages/layerchart/src/lib/components/Line.svelte @@ -58,7 +58,7 @@ const renderContext = canvasContext ? 'canvas' : 'svg'; function render(ctx: CanvasRenderingContext2D) { - const pathData = `M ${x1},${y1} L ${x2},${y2}`; + const pathData = `M ${$tweened_x1},${$tweened_y1} L ${$tweened_x2},${$tweened_y2}`; renderPathData(ctx, pathData, { styles: { fill, stroke, strokeWidth }, classes: $$props.class, From f3ebda06223c129906d2174f4fe9c37b2acb0f42 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Thu, 9 Jan 2025 16:18:00 -0500 Subject: [PATCH 24/55] fix(renderPathData()): Respect `opacity` CSS style via `globalAlpha` canvas attribute --- packages/layerchart/src/lib/components/Spline.svelte | 3 ++- packages/layerchart/src/lib/utils/canvas.ts | 11 ++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/layerchart/src/lib/components/Spline.svelte b/packages/layerchart/src/lib/components/Spline.svelte index 1b39cdfb8..ac9028e47 100644 --- a/packages/layerchart/src/lib/components/Spline.svelte +++ b/packages/layerchart/src/lib/components/Spline.svelte @@ -66,6 +66,7 @@ export let fill: string | undefined = undefined; export let stroke: string | undefined = undefined; export let strokeWidth: number | undefined = undefined; + export let opacity: number | undefined = undefined; /** Marker to attach to start, mid, and end points of path */ export let marker: ComponentProps['type'] | ComponentProps | undefined = @@ -164,7 +165,7 @@ function render(ctx: CanvasRenderingContext2D) { renderPathData(ctx, $tweened_d, { - styles: { stroke, fill, strokeWidth }, + styles: { stroke, fill, strokeWidth, opacity }, classes: $$props.class, }); } diff --git a/packages/layerchart/src/lib/utils/canvas.ts b/packages/layerchart/src/lib/utils/canvas.ts index 51973a090..2c06bfe7c 100644 --- a/packages/layerchart/src/lib/utils/canvas.ts +++ b/packages/layerchart/src/lib/utils/canvas.ts @@ -3,7 +3,12 @@ export const DEFAULT_FILL = 'rgb(0, 0, 0)'; const CANVAS_STYLES_ELEMENT_ID = '__layerchart_canvas_styles_id'; type ComputedStylesOptions = { - styles?: Partial & { strokeWidth?: number | string }>; + styles?: Partial< + Omit & { + strokeWidth?: number | string; + opacity?: number | string; + } + >; classes?: string; }; @@ -63,6 +68,10 @@ export function renderPathData( const paintOrder = computedStyles?.paintOrder === 'stroke' ? ['stroke', 'fill'] : ['fill', 'stroke']; + if (computedStyles?.opacity) { + canvasCtx.globalAlpha = Number(computedStyles?.opacity); + } + paintOrder.forEach((attr) => { if (attr === 'fill') { const fill = computedStyles?.fill === DEFAULT_FILL ? null : computedStyles?.fill; From 03466777da12e895af713c997c698ccb8bff4938 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Thu, 9 Jan 2025 17:54:32 -0500 Subject: [PATCH 25/55] feat: Add new `renderText()` canvas util to simplify rendering SVG path data onto canvas context with CSS variable and class support --- .changeset/dry-masks-suffer.md | 2 +- .changeset/orange-parrots-arrive.md | 5 ++ packages/layerchart/src/lib/utils/canvas.ts | 73 ++++++++++++++++++--- 3 files changed, 71 insertions(+), 9 deletions(-) create mode 100644 .changeset/orange-parrots-arrive.md diff --git a/.changeset/dry-masks-suffer.md b/.changeset/dry-masks-suffer.md index b26cad3b8..9cca5abd3 100644 --- a/.changeset/dry-masks-suffer.md +++ b/.changeset/dry-masks-suffer.md @@ -2,4 +2,4 @@ 'layerchart': patch --- -feat: Add new `renderPathData()` canvas util to simplify rendering SVG path data onto canvas context with CSS class support +feat: Add new `renderPathData()` canvas util to simplify rendering SVG path data onto canvas context with CSS variable and class support diff --git a/.changeset/orange-parrots-arrive.md b/.changeset/orange-parrots-arrive.md new file mode 100644 index 000000000..ddb8d29d4 --- /dev/null +++ b/.changeset/orange-parrots-arrive.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +feat: Add new `renderText()` canvas util to simplify rendering SVG path data onto canvas context with CSS variable and class support diff --git a/packages/layerchart/src/lib/utils/canvas.ts b/packages/layerchart/src/lib/utils/canvas.ts index 2c06bfe7c..cd4eba895 100644 --- a/packages/layerchart/src/lib/utils/canvas.ts +++ b/packages/layerchart/src/lib/utils/canvas.ts @@ -49,18 +49,19 @@ function getComputedStyles( return computedStyles; } catch (e) { console.error('Unable to get computed styles', e); - return null; + return {} as CSSStyleDeclaration; } } -/** Render SVG path data onto canvas context. Supports CSS variables and classes by tranferring to hidden `` element before retrieval) */ -export function renderPathData( +/** Render onto canvas context. Supports CSS variables and classes by tranferring to hidden `` element before retrieval) */ +function render( canvasCtx: CanvasRenderingContext2D, - pathData: string | null | undefined, + render: { + stroke: (canvasCtx: CanvasRenderingContext2D) => void; + fill: (canvasCtx: CanvasRenderingContext2D) => void; + }, styleOptions: ComputedStylesOptions = {} ) { - const path = new Path2D(pathData ?? ''); - // TODO: Consider memoizing? How about reactiving to CSS variable changes (light/dark mode toggle) const computedStyles = getComputedStyles(canvasCtx.canvas, styleOptions); @@ -72,12 +73,32 @@ export function renderPathData( canvasCtx.globalAlpha = Number(computedStyles?.opacity); } + // Text properties + canvasCtx.font = computedStyles.font; + + // TODO: Hack to handle `textAnchor` with canvas. Try to find a better approach + if (computedStyles.textAnchor === 'middle') { + canvasCtx.textAlign = 'center'; + } else if (computedStyles.textAnchor === 'end') { + canvasCtx.textAlign = 'right'; + } else { + canvasCtx.textAlign = computedStyles.textAlign as CanvasTextAlign; // TODO: Handle `justify` and `match-parent`? + } + + // TODO: Handle `textBaseline` / `verticalAnchor` (Text) + // canvasCtx.textBaseline = 'top'; + // canvasCtx.textBaseline = 'middle'; + // canvasCtx.textBaseline = 'bottom'; + // canvasCtx.textBaseline = 'alphabetic'; + // canvasCtx.textBaseline = 'hanging'; + // canvasCtx.textBaseline = 'ideographic'; + paintOrder.forEach((attr) => { if (attr === 'fill') { const fill = computedStyles?.fill === DEFAULT_FILL ? null : computedStyles?.fill; if (fill) { canvasCtx.fillStyle = fill; - canvasCtx.fill(path); + render.fill(canvasCtx); } } else if (attr === 'stroke') { const stroke = computedStyles?.stroke === 'none' ? null : computedStyles?.stroke; @@ -88,12 +109,48 @@ export function renderPathData( : (computedStyles?.strokeWidth ?? 1); canvasCtx.strokeStyle = stroke; - canvasCtx.stroke(path); + render.stroke(canvasCtx); } } }); } +/** Render SVG path data onto canvas context. Supports CSS variables and classes by tranferring to hidden `` element before retrieval) */ +export function renderPathData( + canvasCtx: CanvasRenderingContext2D, + pathData: string | null | undefined, + styleOptions: ComputedStylesOptions = {} +) { + const path = new Path2D(pathData ?? ''); + + render( + canvasCtx, + { + fill: (ctx) => ctx.fill(path), + stroke: (ctx) => ctx.stroke(path), + }, + styleOptions + ); +} + +export function renderText( + canvasCtx: CanvasRenderingContext2D, + text: string | number | null | undefined, + coords: { x: number; y: number }, + styleOptions: ComputedStylesOptions = {} +) { + if (text) { + render( + canvasCtx, + { + fill: (ctx) => ctx.fillText(text.toString(), coords.x, coords.y), + stroke: (ctx) => ctx.strokeText(text.toString(), coords.x, coords.y), + }, + styleOptions + ); + } +} + /** Clear canvas accounting for Canvas `context.translate(...)` */ export function clearCanvasContext( canvasCtx: CanvasRenderingContext2D, From 425957228034bfd1d9d1a5c73df6bc950035e658 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Thu, 9 Jan 2025 17:54:41 -0500 Subject: [PATCH 26/55] feat(Text): Support Canvas context --- .changeset/fluffy-beds-love.md | 5 + .../layerchart/src/lib/components/Text.svelte | 92 ++++++++++++++----- 2 files changed, 75 insertions(+), 22 deletions(-) create mode 100644 .changeset/fluffy-beds-love.md diff --git a/.changeset/fluffy-beds-love.md b/.changeset/fluffy-beds-love.md new file mode 100644 index 000000000..3492e3084 --- /dev/null +++ b/.changeset/fluffy-beds-love.md @@ -0,0 +1,5 @@ +--- +'layerchart': minor +--- + +feat(Text): Support Canvas context diff --git a/packages/layerchart/src/lib/components/Text.svelte b/packages/layerchart/src/lib/components/Text.svelte index 1efcd441c..996b18924 100644 --- a/packages/layerchart/src/lib/components/Text.svelte +++ b/packages/layerchart/src/lib/components/Text.svelte @@ -1,10 +1,12 @@ - - - - {#if isValidXOrY(x) && isValidXOrY(y)} - - {#each wordsByLines as line, index} - - {line.words.join(' ')} - - {/each} - - {/if} - +{#if renderContext === 'svg'} + + + + {#if isValidXOrY(x) && isValidXOrY(y)} + + {#each wordsByLines as line, index} + + {line.words.join(' ')} + + {/each} + + {/if} + +{/if} From a147c1c4c8b9e925043bd0f783415e44c5e5c5be Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Thu, 9 Jan 2025 18:09:43 -0500 Subject: [PATCH 27/55] docs: Use new `renderText()` to simplify manual rendering on geo example --- .../docs/examples/CountryMap/+page.svelte | 40 +++++++------ .../docs/examples/GeoPoint/+page.svelte | 56 +++++++++---------- 2 files changed, 47 insertions(+), 49 deletions(-) diff --git a/packages/layerchart/src/routes/docs/examples/CountryMap/+page.svelte b/packages/layerchart/src/routes/docs/examples/CountryMap/+page.svelte index 6a7408162..6e646cdeb 100644 --- a/packages/layerchart/src/routes/docs/examples/CountryMap/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/CountryMap/+page.svelte @@ -2,9 +2,8 @@ import { geoAlbersUsa } from 'd3-geo'; import { feature } from 'topojson-client'; - import { Canvas, Chart, GeoPath, Svg, Text } from 'layerchart'; + import { Canvas, Chart, GeoPath, renderText, Svg, Text } from 'layerchart'; import Preview from '$lib/docs/Preview.svelte'; - import ComputedStyles from 'layerchart/components/ComputedStyles.svelte'; export let data; const states = feature(data.geojson, data.geojson.objects.states); @@ -63,26 +62,25 @@ > - - {#each states.features as feature} - { - const geoPath = newGeoPath(); - const [x, y] = geoPath.centroid(feature); - ctx.font = '8px sans-serif'; - ctx.textAlign = 'center'; - ctx.lineWidth = 2; - ctx.strokeStyle = styles.stroke; - ctx.strokeText(feature.properties.name, x, y); - - ctx.fillStyle = styles.fill; - ctx.fillText(feature.properties.name, x, y); - }} - /> - {/each} - + {#each states.features as feature} + { + const geoPath = newGeoPath(); + const [x, y] = geoPath.centroid(feature); + renderText( + ctx, + feature.properties.name, + { x, y }, + { + classes: 'text-[8px] text-center fill-surface-content stroke-surface-100', + styles: { paintOrder: 'stroke' }, + } + ); + }} + /> + {/each} diff --git a/packages/layerchart/src/routes/docs/examples/GeoPoint/+page.svelte b/packages/layerchart/src/routes/docs/examples/GeoPoint/+page.svelte index 7db336986..58bb950cb 100644 --- a/packages/layerchart/src/routes/docs/examples/GeoPoint/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/GeoPoint/+page.svelte @@ -10,12 +10,12 @@ GeoPath, GeoPoint, renderPathData, + renderText, Svg, Text, Tooltip, } from 'layerchart'; import Preview from '$lib/docs/Preview.svelte'; - import ComputedStyles from '$lib/components/ComputedStyles.svelte'; export let data; @@ -269,34 +269,34 @@ - - {#each data.us.captitals as capital} - - { - const pathData = circlePath({ cx: x, cy: y, r: 2 }); - renderPathData(ctx, pathData, { classes: 'fill-white stroke-danger' }); - }} - /> + {#each data.us.captitals as capital} + + { + const pathData = circlePath({ cx: x, cy: y, r: 2 }); + renderPathData(ctx, pathData, { classes: 'fill-white stroke-danger' }); + }} + /> - - { - ctx.font = '8px sans-serif'; - ctx.textAlign = 'center'; - ctx.lineWidth = 2; - ctx.strokeStyle = labelStyles.stroke; - ctx.fillStyle = labelStyles.fill; - ctx.strokeText(capital.description, x, y - 6); - ctx.fillText(capital.description, x, y - 6); - }} - /> - {/each} - + + { + renderText( + ctx, + capital.description, + { x, y: y - 6 }, + { + classes: 'text-[8px] text-center fill-surface-content stroke-surface-100', + styles: { paintOrder: 'stroke' }, + } + ); + }} + /> + {/each} From bf3b41d25e9d93238cf7e69494199c0081d20344 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Thu, 9 Jan 2025 18:20:50 -0500 Subject: [PATCH 28/55] feat: Add new `renderRect()` canvas util to simplify rendering rectangles onto canvas context with CSS variable and class support --- .changeset/dry-singers-travel.md | 5 +++++ .changeset/orange-parrots-arrive.md | 2 +- packages/layerchart/src/lib/utils/canvas.ts | 17 +++++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 .changeset/dry-singers-travel.md diff --git a/.changeset/dry-singers-travel.md b/.changeset/dry-singers-travel.md new file mode 100644 index 000000000..7ac5505b7 --- /dev/null +++ b/.changeset/dry-singers-travel.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +feat: Add new `renderRect()` canvas util to simplify rendering rectangles onto canvas context with CSS variable and class support diff --git a/.changeset/orange-parrots-arrive.md b/.changeset/orange-parrots-arrive.md index ddb8d29d4..085733e0c 100644 --- a/.changeset/orange-parrots-arrive.md +++ b/.changeset/orange-parrots-arrive.md @@ -2,4 +2,4 @@ 'layerchart': patch --- -feat: Add new `renderText()` canvas util to simplify rendering SVG path data onto canvas context with CSS variable and class support +feat: Add new `renderText()` canvas util to simplify rendering text onto canvas context with CSS variable and class support diff --git a/packages/layerchart/src/lib/utils/canvas.ts b/packages/layerchart/src/lib/utils/canvas.ts index cd4eba895..c15802019 100644 --- a/packages/layerchart/src/lib/utils/canvas.ts +++ b/packages/layerchart/src/lib/utils/canvas.ts @@ -93,6 +93,8 @@ function render( // canvasCtx.textBaseline = 'hanging'; // canvasCtx.textBaseline = 'ideographic'; + // TODO: Support dashed lines + paintOrder.forEach((attr) => { if (attr === 'fill') { const fill = computedStyles?.fill === DEFAULT_FILL ? null : computedStyles?.fill; @@ -151,6 +153,21 @@ export function renderText( } } +export function renderRect( + canvasCtx: CanvasRenderingContext2D, + coords: { x: number; y: number; width: number; height: number }, + styleOptions: ComputedStylesOptions = {} +) { + render( + canvasCtx, + { + fill: (ctx) => ctx.fillRect(coords.x, coords.y, coords.width, coords.height), + stroke: (ctx) => ctx.strokeRect(coords.x, coords.y, coords.width, coords.height), + }, + styleOptions + ); +} + /** Clear canvas accounting for Canvas `context.translate(...)` */ export function clearCanvasContext( canvasCtx: CanvasRenderingContext2D, From bc83c5ca9a3a81b65745a4ebc7632358d9710695 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Thu, 9 Jan 2025 18:28:24 -0500 Subject: [PATCH 29/55] feat(Rect): Support Canvas context --- .changeset/healthy-kiwis-begin.md | 5 ++ .../layerchart/src/lib/components/Rect.svelte | 77 ++++++++++++++----- 2 files changed, 64 insertions(+), 18 deletions(-) create mode 100644 .changeset/healthy-kiwis-begin.md diff --git a/.changeset/healthy-kiwis-begin.md b/.changeset/healthy-kiwis-begin.md new file mode 100644 index 000000000..a9344a85c --- /dev/null +++ b/.changeset/healthy-kiwis-begin.md @@ -0,0 +1,5 @@ +--- +'layerchart': minor +--- + +feat(Rect): Support Canvas context diff --git a/packages/layerchart/src/lib/components/Rect.svelte b/packages/layerchart/src/lib/components/Rect.svelte index 08600db1c..0b1c80e3d 100644 --- a/packages/layerchart/src/lib/components/Rect.svelte +++ b/packages/layerchart/src/lib/components/Rect.svelte @@ -1,5 +1,5 @@ - - - +{#if renderContext === 'svg'} + + + +{/if} From 39e855fa1fcbaeedbabb063a1423c4151b4f6cea Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Thu, 9 Jan 2025 18:29:19 -0500 Subject: [PATCH 30/55] fix(render()): Support `strokeDasharray` style via `ctx.setLineDash()` --- packages/layerchart/src/lib/utils/canvas.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/layerchart/src/lib/utils/canvas.ts b/packages/layerchart/src/lib/utils/canvas.ts index c15802019..5b367fa98 100644 --- a/packages/layerchart/src/lib/utils/canvas.ts +++ b/packages/layerchart/src/lib/utils/canvas.ts @@ -93,7 +93,13 @@ function render( // canvasCtx.textBaseline = 'hanging'; // canvasCtx.textBaseline = 'ideographic'; - // TODO: Support dashed lines + // Dashed lines + if (computedStyles.strokeDasharray.includes(',')) { + const dashArray = computedStyles.strokeDasharray + .split(',') + .map((s) => Number(s.replace('px', ''))); + canvasCtx.setLineDash(dashArray); + } paintOrder.forEach((attr) => { if (attr === 'fill') { From 4bcd2397263cc321a9358dfdfe0dd8c969a60a04 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Thu, 9 Jan 2025 21:07:42 -0500 Subject: [PATCH 31/55] fix(Points): Render primative components (Circle /Link) instead of using `renderPathData` directly to fix tweening and support links --- .../layerchart/src/lib/components/Points.svelte | 9 ++------- .../routes/docs/components/Spline/+page.svelte | 15 +++++---------- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/packages/layerchart/src/lib/components/Points.svelte b/packages/layerchart/src/lib/components/Points.svelte index 9706ba4c7..afedc3d9b 100644 --- a/packages/layerchart/src/lib/components/Points.svelte +++ b/packages/layerchart/src/lib/components/Points.svelte @@ -13,8 +13,6 @@ import Link from './Link.svelte'; import { isScaleBand, type AnyScale } from '../utils/scales.js'; import { getCanvasContext } from './layout/Canvas.svelte'; - import { renderPathData } from '../utils/canvas.js'; - import { circlePath } from 'layerchart/utils/path.js'; const context = chartContext() as any; const { @@ -167,10 +165,7 @@ if (render) { render(ctx, points); } else { - points.forEach((point) => { - const pathData = circlePath({ cx: point.x, cy: point.y, r: point.r }); - renderPathData(ctx, pathData, { styles: { fill, stroke }, classes: className }); - }); + // Rendered below } } @@ -186,7 +181,7 @@ - {#if renderContext === 'svg'} + {#if renderContext === 'svg' || (renderContext === 'canvas' && !render)} {#if links} {#each _links as link} diff --git a/packages/layerchart/src/routes/docs/components/Spline/+page.svelte b/packages/layerchart/src/routes/docs/components/Spline/+page.svelte index 47693f496..41ed18c5e 100644 --- a/packages/layerchart/src/routes/docs/components/Spline/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Spline/+page.svelte @@ -66,13 +66,11 @@
- - + + - - {#if show} - {/if} - - - - {#if show && showPoints} - + {#if showPoints} + + {/if} {/if} From ba6da85160fa99cd75840f0e395596ae64b3aaf2 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Thu, 9 Jan 2025 21:08:07 -0500 Subject: [PATCH 32/55] fix(Spline): Fix opacity for svg context --- packages/layerchart/src/lib/components/Spline.svelte | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/layerchart/src/lib/components/Spline.svelte b/packages/layerchart/src/lib/components/Spline.svelte index ac9028e47..10d8d8918 100644 --- a/packages/layerchart/src/lib/components/Spline.svelte +++ b/packages/layerchart/src/lib/components/Spline.svelte @@ -233,6 +233,7 @@ {fill} {stroke} stroke-width={strokeWidth} + {opacity} marker-start={markerStartId ? `url(#${markerStartId})` : undefined} marker-mid={markerMidId ? `url(#${markerMidId})` : undefined} marker-end={markerEndId ? `url(#${markerEndId})` : undefined} From 30e14c7958d42e0196c26f5fef67420aaeb3c61c Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Thu, 9 Jan 2025 22:18:35 -0500 Subject: [PATCH 33/55] Add `spikePath()` util --- .changeset/chilly-jeans-film.md | 5 +++++ packages/layerchart/src/lib/utils/path.ts | 25 +++++++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 .changeset/chilly-jeans-film.md diff --git a/.changeset/chilly-jeans-film.md b/.changeset/chilly-jeans-film.md new file mode 100644 index 000000000..e23109128 --- /dev/null +++ b/.changeset/chilly-jeans-film.md @@ -0,0 +1,5 @@ +--- +'layerchart': patch +--- + +Add `spikePath()` util diff --git a/packages/layerchart/src/lib/utils/path.ts b/packages/layerchart/src/lib/utils/path.ts index 204adaed5..98305a127 100644 --- a/packages/layerchart/src/lib/utils/path.ts +++ b/packages/layerchart/src/lib/utils/path.ts @@ -33,6 +33,31 @@ export function circlePath(dimensions: { `; } +/** Create spike (triangle) using path data */ +export function spikePath({ + x, + y, + width, + height, +}: { + x: number; + y: number; + width: number; + height: number; +}) { + const startPoint = { x: x - width / 2, y }; + const midPoint = { x, y: y - height }; + const endPoint = { x: x + width / 2, y }; + + const pathData = ` + M ${startPoint.x},${startPoint.y} + L ${midPoint.x},${midPoint.y} + L ${endPoint.x},${endPoint.y} + `; + + return pathData; +} + /** Flatten all `y` coordinates to `0` */ export function flattenPathData(pathData: string, yOverride = 0) { let result = pathData; From 2d1f4d8a959210bb6975a21c5418c6ec7aea862b Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Thu, 9 Jan 2025 22:20:09 -0500 Subject: [PATCH 34/55] docs: Simplify SpikeMap canvas example using `renderPathData()` and new `spikePath` util --- .../docs/examples/SpikeMap/+page.svelte | 45 +++++++------------ 1 file changed, 17 insertions(+), 28 deletions(-) diff --git a/packages/layerchart/src/routes/docs/examples/SpikeMap/+page.svelte b/packages/layerchart/src/routes/docs/examples/SpikeMap/+page.svelte index f1f3ab409..06f9cf2d7 100644 --- a/packages/layerchart/src/routes/docs/examples/SpikeMap/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/SpikeMap/+page.svelte @@ -11,13 +11,13 @@ Group, HitCanvas, renderPathData, + spikePath, Svg, Tooltip, } from 'layerchart'; import TransformControls from 'layerchart/components/TransformControls.svelte'; import Preview from '$lib/docs/Preview.svelte'; - import ComputedStyles from 'layerchart/components/ComputedStyles.svelte'; export let data; @@ -169,33 +169,22 @@ {strokeWidth} /> - - { - for (var feature of enrichedCountiesFeatures) { - const geoPath = newGeoPath(); - const [x, y] = geoPath.centroid(feature); - const d = feature.properties.data; - const height = heightScale(d?.population ?? 0); - - ctx.lineWidth = strokeWidth; - ctx.strokeStyle = styles.stroke; - ctx.fillStyle = styles.fill; - - const startPoint = [x - width / 2, y]; - const midPoint = [x, y - height]; - const endPoint = [x + width / 2, y]; - - ctx.beginPath(); - ctx.moveTo(x - width / 2, y); // startPoint - ctx.lineTo(x, y - height); // midPoint - ctx.lineTo(x + width / 2, y); // endPoint - ctx.fill(); - ctx.stroke(); - } - }} - /> - + { + for (var feature of enrichedCountiesFeatures) { + const geoPath = newGeoPath(); + const [x, y] = geoPath.centroid(feature); + const d = feature.properties.data; + const height = heightScale(d?.population ?? 0); + + const pathData = spikePath({ x, y, width, height }); + renderPathData(ctx, pathData, { + classes: 'stroke-danger fill-danger/25', + styles: { strokeWidth }, + }); + } + }} + /> {#if tooltip.data} From dc5b22e97fe0c13ca97e451234630f5291a5ee1d Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Fri, 10 Jan 2025 10:52:38 -0500 Subject: [PATCH 35/55] feat: Support `renderContext` prop to switch between Svg (default) and Canvas for all simplified charts (AreaChart, BarChart, LineChart, PieChart, and ScatterChart) --- .changeset/cold-penguins-jump.md | 5 + .../lib/components/charts/AreaChart.svelte | 8 +- .../src/lib/components/charts/BarChart.svelte | 8 +- .../lib/components/charts/LineChart.svelte | 8 +- .../src/lib/components/charts/PieChart.svelte | 8 +- .../lib/components/charts/ScatterChart.svelte | 8 +- .../docs/components/AreaChart/+page.svelte | 43 +++++-- .../docs/components/BarChart/+page.svelte | 105 +++++++++++++++--- .../docs/components/LineChart/+page.svelte | 60 +++++++--- .../docs/components/PieChart/+page.svelte | 67 +++++++++-- .../docs/components/ScatterChart/+page.svelte | 56 ++++++++-- 11 files changed, 303 insertions(+), 73 deletions(-) create mode 100644 .changeset/cold-penguins-jump.md diff --git a/.changeset/cold-penguins-jump.md b/.changeset/cold-penguins-jump.md new file mode 100644 index 000000000..8d3d515d5 --- /dev/null +++ b/.changeset/cold-penguins-jump.md @@ -0,0 +1,5 @@ +--- +'layerchart': minor +--- + +feat: Support `renderContext` prop to switch between Svg (default) and Canvas for all simplified charts (AreaChart, BarChart, LineChart, PieChart, and ScatterChart) diff --git a/packages/layerchart/src/lib/components/charts/AreaChart.svelte b/packages/layerchart/src/lib/components/charts/AreaChart.svelte index 99c9bf0d8..a4d399790 100644 --- a/packages/layerchart/src/lib/components/charts/AreaChart.svelte +++ b/packages/layerchart/src/lib/components/charts/AreaChart.svelte @@ -7,6 +7,7 @@ import Area from '../Area.svelte'; import Axis from '../Axis.svelte'; + import Canvas from '../layout/Canvas.svelte'; import Chart from '../Chart.svelte'; import Grid from '../Grid.svelte'; import Highlight from '../Highlight.svelte'; @@ -30,6 +31,7 @@ rule?: typeof rule; series?: typeof series; seriesLayout?: typeof seriesLayout; + renderContext?: typeof renderContext; } export let data: $$Props['data'] = []; @@ -74,6 +76,8 @@ labels?: Partial>; } = {}; + export let renderContext: 'svg' | 'canvas' = 'svg'; + $: allSeriesData = series .flatMap((s) => s.data?.map((d) => ({ seriesKey: s.key, ...d }))) .filter((d) => d) as Array; @@ -186,7 +190,7 @@ }} - + {#if grid} @@ -261,7 +265,7 @@ {#if labels} {/if} - + {#if legend} diff --git a/packages/layerchart/src/lib/components/charts/BarChart.svelte b/packages/layerchart/src/lib/components/charts/BarChart.svelte index 8dca8ff04..eacabf51b 100644 --- a/packages/layerchart/src/lib/components/charts/BarChart.svelte +++ b/packages/layerchart/src/lib/components/charts/BarChart.svelte @@ -7,6 +7,7 @@ import Axis from '../Axis.svelte'; import Bars from '../Bars.svelte'; + import Canvas from '../layout/Canvas.svelte'; import Chart from '../Chart.svelte'; import Grid from '../Grid.svelte'; import Highlight from '../Highlight.svelte'; @@ -32,6 +33,7 @@ rule?: typeof rule; series?: typeof series; seriesLayout?: typeof seriesLayout; + renderContext?: typeof renderContext; } export let data: $$Props['data'] = []; @@ -111,6 +113,8 @@ labels?: Partial>; } = {}; + export let renderContext: 'svg' | 'canvas' = 'svg'; + $: allSeriesData = series .flatMap((s) => s.data?.map((d) => { @@ -229,7 +233,7 @@ getBarsProps, }} - + {#if grid} {/if} - + {#if legend} diff --git a/packages/layerchart/src/lib/components/charts/LineChart.svelte b/packages/layerchart/src/lib/components/charts/LineChart.svelte index fd8b76698..3a011db2d 100644 --- a/packages/layerchart/src/lib/components/charts/LineChart.svelte +++ b/packages/layerchart/src/lib/components/charts/LineChart.svelte @@ -4,6 +4,7 @@ import { format } from '@layerstack/utils'; import Axis from '../Axis.svelte'; + import Canvas from '../layout/Canvas.svelte'; import Chart from '../Chart.svelte'; import Grid from '../Grid.svelte'; import Highlight from '../Highlight.svelte'; @@ -26,6 +27,7 @@ props?: typeof props; rule?: typeof rule; series?: typeof series; + renderContext?: typeof renderContext; } export let data: $$Props['data'] = []; @@ -65,6 +67,8 @@ points?: Partial>; } = {}; + export let renderContext: 'svg' | 'canvas' = 'svg'; + $: allSeriesData = series .flatMap((s) => s.data?.map((d) => ({ seriesKey: s.key, ...d }))) .filter((d) => d) as Array; @@ -133,7 +137,7 @@ getSplineProps, }} - + {#if grid} @@ -203,7 +207,7 @@ /> {/each} - + {#if legend} diff --git a/packages/layerchart/src/lib/components/charts/PieChart.svelte b/packages/layerchart/src/lib/components/charts/PieChart.svelte index 6c1f8d638..5bac9e76b 100644 --- a/packages/layerchart/src/lib/components/charts/PieChart.svelte +++ b/packages/layerchart/src/lib/components/charts/PieChart.svelte @@ -4,6 +4,7 @@ import { format } from '@layerstack/utils'; import Arc from '../Arc.svelte'; + import Canvas from '../layout/Canvas.svelte'; import Chart from '../Chart.svelte'; import Group from '../Group.svelte'; import Legend from '../Legend.svelte'; @@ -30,6 +31,7 @@ range?: typeof range; series?: typeof series; value?: typeof label; + renderContext?: typeof renderContext; } export let data: ChartProps['data'] = []; @@ -98,6 +100,8 @@ legend?: Partial>; } = {}; + export let renderContext: 'svg' | 'canvas' = 'svg'; + $: allSeriesData = series .flatMap((s) => s.data?.map((d) => ({ seriesKey: s.key, ...d }))) .filter((d) => d) as Array; @@ -151,7 +155,7 @@ tooltip, }} - + @@ -216,7 +220,7 @@ - + {#if legend} diff --git a/packages/layerchart/src/lib/components/charts/ScatterChart.svelte b/packages/layerchart/src/lib/components/charts/ScatterChart.svelte index d29f630bd..511b78984 100644 --- a/packages/layerchart/src/lib/components/charts/ScatterChart.svelte +++ b/packages/layerchart/src/lib/components/charts/ScatterChart.svelte @@ -4,6 +4,7 @@ import { format } from '@layerstack/utils'; import Axis from '../Axis.svelte'; + import Canvas from '../layout/Canvas.svelte'; import Chart from '../Chart.svelte'; import Grid from '../Grid.svelte'; import Highlight from '../Highlight.svelte'; @@ -23,6 +24,7 @@ legend?: typeof legend; props?: typeof props; series?: typeof series; + renderContext?: typeof renderContext; } export let data: $$Props['data'] = []; @@ -55,6 +57,8 @@ rule?: Partial>; } = {}; + export let renderContext: 'svg' | 'canvas' = 'svg'; + // Default xScale based on first data's `x` value $: xScale = $$props.xScale ?? @@ -117,7 +121,7 @@ : null} - + {#if grid} @@ -171,7 +175,7 @@ {...typeof labels === 'object' ? labels : null} /> {/if} - + {#if legend} diff --git a/packages/layerchart/src/routes/docs/components/AreaChart/+page.svelte b/packages/layerchart/src/routes/docs/components/AreaChart/+page.svelte index e934a947f..40dc71655 100644 --- a/packages/layerchart/src/routes/docs/components/AreaChart/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/AreaChart/+page.svelte @@ -10,7 +10,7 @@ LinearGradient, Spline, } from 'layerchart'; - import { PeriodType } from 'svelte-ux'; + import { Field, PeriodType, ToggleGroup, ToggleOption } from 'svelte-ux'; import { format } from '@layerstack/utils'; import Preview from '$lib/docs/Preview.svelte'; @@ -45,22 +45,31 @@ }); const multiSeriesFlatData = pivotLonger(multiSeriesData, keys, 'fruit', 'value'); const multiSeriesDataByFruit = group(multiSeriesFlatData, (d) => d.fruit); + + let renderContext: 'svg' | 'canvas' = 'svg';

Examples

+ + + Svg + Canvas + + +

Basic

- +

Gradient

- + @@ -78,7 +87,7 @@ }}
- + {@const thresholdValue = 0} {@const thresholdOffset = yScale(thresholdValue) / (height + padding.bottom)} @@ -131,6 +140,7 @@ x="date" y="value" props={{ area: { curve: curveCatmullRom } }} + {renderContext} />
@@ -153,6 +163,7 @@ color: 'hsl(var(--color-warning))', }, ]} + {renderContext} />
@@ -182,6 +193,7 @@ }, ]} tooltip={{ mode: 'voronoi' }} + {renderContext} > {#each series as s} @@ -238,6 +250,7 @@ }, ]} seriesLayout="stack" + {renderContext} />
@@ -261,6 +274,7 @@ }, ]} seriesLayout="stackExpand" + {renderContext} /> @@ -284,6 +298,7 @@ }, ]} seriesLayout="stackDiverging" + {renderContext} /> @@ -307,6 +322,7 @@ }, ]} seriesLayout="stack" + {renderContext} > {#each series as s, i (s.key)} @@ -328,7 +344,7 @@
- +
@@ -336,7 +352,7 @@
- +
@@ -371,6 +387,7 @@ color: 'hsl(var(--color-primary) / 20%)', }, ]} + {renderContext} > @@ -383,7 +400,7 @@
- +
@@ -400,6 +417,7 @@ axis={false} grid={false} props={{ highlight: { points: { r: 3, class: 'stroke-2 stroke-surface-100' } } }} + {renderContext} /> @@ -408,7 +426,7 @@
- +
@@ -416,7 +434,7 @@
- +
@@ -440,6 +458,7 @@ ]} seriesLayout="stack" legend + {renderContext} /> @@ -464,6 +483,7 @@ ]} seriesLayout="stack" legend={{ placement: 'top-right' }} + {renderContext} /> @@ -490,6 +510,7 @@ ]} seriesLayout="stack" legend + {renderContext} /> @@ -498,7 +519,7 @@
- +
- +

Examples

+ + + Svg + Canvas + + +

Vertical (default)

- +
@@ -63,7 +75,13 @@
- +
@@ -76,6 +94,7 @@ x="date" y="value" props={{ bars: { class: 'fill-secondary' } }} + {renderContext} />
@@ -84,7 +103,13 @@
- +
@@ -106,6 +131,7 @@ props={{ yAxis: { format: 'metric' }, }} + {renderContext} />
@@ -122,6 +148,7 @@ cScale={scaleThreshold()} cDomain={[0]} cRange={['hsl(var(--color-danger))', 'hsl(var(--color-success))']} + {renderContext} /> @@ -130,7 +157,7 @@
- + {#each series as s, i (s.key)}
- + @@ -174,6 +207,7 @@ props: { inset: 16 }, }, ]} + {renderContext} />
@@ -190,6 +224,7 @@ { key: 'baseline', color: 'hsl(var(--color-surface-content) / 20%)' }, { key: 'value', color: 'hsl(var(--color-primary))', props: { inset: 8 } }, ]} + {renderContext} />
@@ -214,6 +249,7 @@ props: { inset: 16 }, }, ]} + {renderContext} /> @@ -236,6 +272,7 @@ color: 'hsl(var(--color-secondary))', }, ]} + {renderContext} /> @@ -265,6 +302,7 @@ color: 'hsl(var(--color-secondary))', }, ]} + {renderContext} > @@ -319,6 +357,7 @@ color: 'hsl(var(--color-secondary))', }, ]} + {renderContext} > @@ -372,6 +411,7 @@ xAxis: { format: 'none' }, yAxis: { format: 'metric' }, }} + {renderContext} /> @@ -404,6 +444,7 @@ xAxis: { format: 'metric' }, yAxis: { format: 'none' }, }} + {renderContext} /> @@ -438,6 +479,7 @@ xAxis: { format: 'none' }, yAxis: { format: 'metric' }, }} + {renderContext} /> @@ -469,6 +511,7 @@ xAxis: { format: 'none' }, yAxis: { format: 'metric' }, }} + {renderContext} /> @@ -501,6 +544,7 @@ xAxis: { format: 'metric' }, yAxis: { format: 'none' }, }} + {renderContext} /> @@ -531,6 +575,7 @@ props={{ xAxis: { format: 'none' }, }} + {renderContext} /> @@ -567,6 +612,7 @@ xAxis: { format: 'none' }, yAxis: { format: 'metric' }, }} + {renderContext} /> @@ -603,6 +649,7 @@ xAxis: { format: 'none' }, yAxis: { format: 'metric' }, }} + {renderContext} /> --> @@ -635,6 +682,7 @@ yAxis: { format: 'metric' }, }} legend + {renderContext} /> @@ -667,6 +715,7 @@ yAxis: { format: 'metric' }, }} legend={{ placement: 'top-right', classes: { root: 'mt-2' } }} + {renderContext} /> @@ -702,6 +751,7 @@ yAxis: { format: 'metric' }, }} legend + {renderContext} /> @@ -710,7 +760,7 @@
- +
@@ -718,7 +768,13 @@
- +
@@ -745,6 +801,7 @@ }, }} padding={{ left: 0, bottom: 16 }} + {renderContext} /> @@ -753,7 +810,15 @@
- + d.date} class="text-sm fill-surface-300 stroke-none" /> @@ -773,6 +838,7 @@ grid={false} bandPadding={0.1} props={{ bars: { radius: 1, strokeWidth: 0 } }} + {renderContext} />
@@ -801,6 +867,7 @@ xAxis: { ticks: (scale) => scaleTime(scale.domain(), scale.range()).ticks() }, rule: { y: false }, }} + {renderContext} > @@ -818,7 +885,7 @@
- +
@@ -826,7 +893,7 @@
- +
@@ -839,6 +906,7 @@ x="date" y="value" props={{ xAxis: { ticks: (scale) => scaleTime(scale.domain(), scale.range()).ticks() } }} + {renderContext} /> @@ -847,7 +915,7 @@
- +
@@ -855,7 +923,13 @@
- +
@@ -871,6 +945,7 @@ yScale={scaleLog()} yDomain={[1, 100]} props={{ yAxis: { ticks: [1, 2, 3, 4, 5, 10, 20, 30, 40, 50, 100] } }} + {renderContext} /> @@ -879,7 +954,7 @@
- + {format(x(data), PeriodType.DayTime)} @@ -897,7 +972,7 @@
- + - + {format(x(data))} diff --git a/packages/layerchart/src/routes/docs/components/LineChart/+page.svelte b/packages/layerchart/src/routes/docs/components/LineChart/+page.svelte index 230a9392a..e8a0b0c0c 100644 --- a/packages/layerchart/src/routes/docs/components/LineChart/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/LineChart/+page.svelte @@ -1,6 +1,7 @@

Examples

+ + + Svg + Canvas + + +

Basic

- +
@@ -82,6 +92,7 @@ data={dateSeriesData} x="date" series={[{ key: 'value', color: 'hsl(var(--color-secondary))' }]} + {renderContext} />
@@ -95,6 +106,7 @@ x="date" y="value" props={{ spline: { curve: curveCatmullRom } }} + {renderContext} />
@@ -111,6 +123,7 @@ { key: 'bananas', color: 'hsl(var(--color-success))' }, { key: 'oranges', color: 'hsl(var(--color-warning))' }, ]} + {renderContext} /> @@ -129,6 +142,7 @@ { key: 'oranges', color: 'hsl(var(--color-warning))' }, ]} tooltip={{ mode: 'voronoi' }} + {renderContext} > {#each series as s} @@ -164,7 +178,7 @@
- +
@@ -172,7 +186,7 @@
- +
@@ -180,7 +194,14 @@
- +
@@ -199,6 +220,7 @@ points: false, }, }} + {renderContext} /> @@ -237,6 +259,7 @@ }, }} tooltip={{ mode: 'voronoi' }} + {renderContext} /> @@ -273,6 +296,7 @@ }, }} tooltip={{ mode: 'voronoi' }} + {renderContext} /> @@ -314,6 +338,7 @@ }, }} tooltip={{ mode: 'voronoi' }} + {renderContext} /> @@ -322,7 +347,7 @@
- +
- + {@const thresholdOffset = (yScale(50) / (height + padding.bottom)) * 100 + '%'}
@@ -440,6 +466,7 @@ }; })} tooltip={{ mode: 'manual' }} + {renderContext} />
@@ -463,6 +490,7 @@ yBaseline={undefined} tooltip={{ mode: 'manual' }} props={{ yAxis: { tweened: true }, grid: { tweened: true } }} + {renderContext} /> @@ -471,7 +499,7 @@
- +
@@ -479,7 +507,7 @@
- + {#each series as s}
@@ -513,7 +542,7 @@
- +
@@ -521,7 +550,7 @@
- +
@@ -538,6 +567,7 @@ { key: 'oranges', color: 'hsl(var(--color-warning))' }, ]} legend + {renderContext} /> @@ -546,7 +576,7 @@
- +
- - + + - + {format(x(data), PeriodType.DayTime)} diff --git a/packages/layerchart/src/routes/docs/components/PieChart/+page.svelte b/packages/layerchart/src/routes/docs/components/PieChart/+page.svelte index dc45b80af..d34846725 100644 --- a/packages/layerchart/src/routes/docs/components/PieChart/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/PieChart/+page.svelte @@ -6,6 +6,7 @@ import Preview from '$lib/docs/Preview.svelte'; import { longData } from '$lib/utils/genData.js'; + import { Field, ToggleGroup, ToggleOption } from 'svelte-ux'; const dataByYear = group(longData, (d) => d.year); const data = dataByYear.get(2019) ?? []; @@ -27,15 +28,24 @@ { key: 'exercise', value: 20, maxValue: 30, color: '#a3e635' }, { key: 'stand', value: 10, maxValue: 12, color: '#22d3ee' }, ]; + + let renderContext: 'svg' | 'canvas' = 'svg';

Examples

+ + + Svg + Canvas + + +

Basic

- +
@@ -43,7 +53,7 @@
- +
@@ -51,7 +61,7 @@
- +
@@ -59,7 +69,15 @@
- +
@@ -77,6 +95,7 @@ cornerRadius={10} padAngle={0.02} props={{ group: { y: 80 } }} + {renderContext} />
@@ -93,6 +112,7 @@ outerRadius={-25} innerRadius={-20} cornerRadius={10} + {renderContext} />
@@ -101,7 +121,7 @@
- + @@ -148,6 +168,7 @@ props={{ group: { y: 45 }, }} + {renderContext} />
@@ -164,6 +185,7 @@ innerRadius={-20} cornerRadius={5} padAngle={0.01} + {renderContext} /> @@ -179,6 +201,7 @@ outerRadius={-25} innerRadius={-20} cornerRadius={10} + {renderContext} /> @@ -196,6 +219,7 @@ innerRadius={-20} cornerRadius={10} props={{ group: { y: 70 } }} + {renderContext} /> @@ -216,6 +240,7 @@ outerRadius={-25} innerRadius={-20} cornerRadius={10} + {renderContext} /> @@ -238,6 +263,7 @@ outerRadius={-25} innerRadius={-20} cornerRadius={10} + {renderContext} /> @@ -253,6 +279,7 @@ { key: 2019, data: dataByYear.get(2019), props: { innerRadius: -20 } }, { key: 2018, data: dataByYear.get(2018), props: { outerRadius: -30 } }, ]} + {renderContext} /> @@ -261,7 +288,13 @@
- +
@@ -269,7 +302,7 @@
- +
@@ -282,6 +315,7 @@ key="fruit" value="value" legend={{ placement: 'top-left', orientation: 'vertical' }} + {renderContext} /> @@ -307,6 +341,7 @@ }} value="value" legend + {renderContext} /> @@ -325,6 +360,7 @@ 'hsl(var(--color-danger))', 'hsl(var(--color-info))', ]} + {renderContext} /> @@ -333,7 +369,7 @@
- +
@@ -341,7 +377,13 @@
- +
@@ -355,6 +397,7 @@ value="value" c="color" cRange={dataWithColor.map((d) => d.color)} + {renderContext} /> @@ -369,6 +412,7 @@ value="value" padding={{ right: 80 }} legend={{ placement: 'right', orientation: 'vertical' }} + {renderContext} /> @@ -383,6 +427,7 @@ value="value" placement="left" legend={{ placement: 'right', orientation: 'vertical' }} + {renderContext} /> @@ -397,6 +442,7 @@ value="value" placement="right" legend={{ placement: 'left', orientation: 'vertical' }} + {renderContext} /> @@ -412,6 +458,7 @@ center={false} props={{ group: { x: 200, center: 'y' } }} legend={{ placement: 'right', orientation: 'vertical' }} + {renderContext} /> @@ -420,6 +467,6 @@
- +
--> diff --git a/packages/layerchart/src/routes/docs/components/ScatterChart/+page.svelte b/packages/layerchart/src/routes/docs/components/ScatterChart/+page.svelte index efcbd1576..0190dca0c 100644 --- a/packages/layerchart/src/routes/docs/components/ScatterChart/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/ScatterChart/+page.svelte @@ -1,5 +1,5 @@

Examples

+ + + Svg + Canvas + + +

Basic

- +
@@ -51,7 +61,14 @@
- +
@@ -59,7 +76,16 @@
- +
@@ -67,7 +93,7 @@
- +
@@ -89,6 +115,7 @@ ][i], }; })} + {renderContext} /> @@ -113,6 +140,7 @@ ][i], }; })} + {renderContext} /> @@ -121,7 +149,7 @@
- +
@@ -144,6 +172,7 @@ }; })} legend + {renderContext} /> @@ -168,6 +197,7 @@ }; })} legend + {renderContext} /> @@ -187,6 +217,7 @@ grid: { tweened: { duration: 200 } }, points: { tweened: { duration: 200 } }, }} + {renderContext} >
- +
@@ -229,7 +260,7 @@
- +
@@ -244,6 +275,7 @@ axis={false} grid={false} props={{ highlight: { lines: false } }} + {renderContext} > @@ -258,7 +290,7 @@
- +
- - + + - + {format(x(data), 'integer')} From 2a3f7da4bf9164444ae5e773cacdd70e2cd93e9b Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Fri, 10 Jan 2025 11:01:59 -0500 Subject: [PATCH 36/55] feat(Canvas): Support `center` prop (similar to `Svg`) to translate children to center (useful for radial layouts) --- .changeset/dry-dodos-build.md | 5 +++++ .../src/lib/components/layout/Canvas.svelte | 14 +++++++++++++- .../src/lib/components/layout/Svg.svelte | 2 +- 3 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 .changeset/dry-dodos-build.md diff --git a/.changeset/dry-dodos-build.md b/.changeset/dry-dodos-build.md new file mode 100644 index 000000000..31e5d8f75 --- /dev/null +++ b/.changeset/dry-dodos-build.md @@ -0,0 +1,5 @@ +--- +'layerchart': minor +--- + +feat(Canvas): Support `center` prop (similar to `Svg`) to translate children to center (useful for radial layouts) diff --git a/packages/layerchart/src/lib/components/layout/Canvas.svelte b/packages/layerchart/src/lib/components/layout/Canvas.svelte index 03e7dfcd4..70603ab78 100644 --- a/packages/layerchart/src/lib/components/layout/Canvas.svelte +++ b/packages/layerchart/src/lib/components/layout/Canvas.svelte @@ -59,6 +59,11 @@ /** A string passed to `aria-describedby` property on the `` tag. */ export let describedBy: string | undefined = undefined; + /** + * Translate children to center (useful for radial layouts) + */ + export let center: boolean | 'x' | 'y' = false; + const drawFunctions: DrawFunction[] = []; let pendingInvalidation = false; let frameId: number | undefined; @@ -84,7 +89,14 @@ context.clearRect(0, 0, $containerWidth, $containerHeight); context.translate($padding.left ?? 0, $padding.top ?? 0); - if (mode === 'canvas') { + + if (center) { + const newTranslate = { + x: center === 'x' || center === true ? $width / 2 : 0, + y: center === 'y' || center === true ? $height / 2 : 0, + }; + context.translate(newTranslate.x, newTranslate.y); + } else if (mode === 'canvas') { const center = { x: $width / 2, y: $height / 2 }; const newTranslate = { x: $translate.x * $scale + center.x - center.x * $scale, diff --git a/packages/layerchart/src/lib/components/layout/Svg.svelte b/packages/layerchart/src/lib/components/layout/Svg.svelte index 35f014c57..397b97fa4 100644 --- a/packages/layerchart/src/lib/components/layout/Svg.svelte +++ b/packages/layerchart/src/lib/components/layout/Svg.svelte @@ -32,7 +32,7 @@ export let title: string | undefined = undefined; /** - * Translate children to center, useful for radial layouts + * Translate children to center (useful for radial layouts) */ export let center: boolean | 'x' | 'y' = false; From 3c3c939bbcb9c8cddf93cf24a4e8f53182226492 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Fri, 10 Jan 2025 11:05:06 -0500 Subject: [PATCH 37/55] fix(PieChart): Use `center` prop (broke after recent refactor) --- packages/layerchart/src/lib/components/charts/PieChart.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/layerchart/src/lib/components/charts/PieChart.svelte b/packages/layerchart/src/lib/components/charts/PieChart.svelte index 5bac9e76b..53161f99c 100644 --- a/packages/layerchart/src/lib/components/charts/PieChart.svelte +++ b/packages/layerchart/src/lib/components/charts/PieChart.svelte @@ -155,7 +155,7 @@ tooltip, }} - + From 1130406a82a60933a488dc34921e3371305b4bbf Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Fri, 10 Jan 2025 11:06:42 -0500 Subject: [PATCH 38/55] Fix CI failures (`pnpm check`) --- .../src/routes/docs/components/BarChart/+page.svelte | 1 - .../src/routes/docs/examples/Partition/+page.svelte | 6 +++--- .../src/routes/docs/examples/Treemap/+page.svelte | 6 +++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/layerchart/src/routes/docs/components/BarChart/+page.svelte b/packages/layerchart/src/routes/docs/components/BarChart/+page.svelte index f1a12fe7f..32cd76d44 100644 --- a/packages/layerchart/src/routes/docs/components/BarChart/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/BarChart/+page.svelte @@ -19,7 +19,6 @@ import Blockquote from '$lib/docs/Blockquote.svelte'; import { createDateSeries, wideData, longData } from '$lib/utils/genData.js'; import { Field, ToggleGroup, ToggleOption } from 'svelte-ux'; - import { render } from 'svelte/server'; export let data; diff --git a/packages/layerchart/src/routes/docs/examples/Partition/+page.svelte b/packages/layerchart/src/routes/docs/examples/Partition/+page.svelte index 84b68314e..2bd4ca8dc 100644 --- a/packages/layerchart/src/routes/docs/examples/Partition/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/Partition/+page.svelte @@ -180,7 +180,7 @@ height={nodeHeight} stroke={colorBy === 'children' ? 'hsl(var(--color-primary-content))' - : hsl(nodeColor).darker(1)} + : hsl(nodeColor).darker(1).toString()} stroke-opacity={colorBy === 'children' ? 0.2 : 1} fill={nodeColor} rx={5} @@ -262,7 +262,7 @@ height={nodeHeight} stroke={colorBy === 'children' ? 'hsl(var(--color-primary-content))' - : hsl(nodeColor).darker(1)} + : hsl(nodeColor).darker(1).toString()} stroke-opacity={colorBy === 'children' ? 0.2 : 1} fill={nodeColor} rx={5} @@ -353,7 +353,7 @@ height={nodeHeight} stroke={colorBy === 'children' ? 'hsl(var(--color-primary-content))' - : hsl(nodeColor).darker(1)} + : hsl(nodeColor).darker(1).toString()} stroke-opacity={colorBy === 'children' ? 0.2 : 1} fill={nodeColor} rx={5} diff --git a/packages/layerchart/src/routes/docs/examples/Treemap/+page.svelte b/packages/layerchart/src/routes/docs/examples/Treemap/+page.svelte index f5c6a5cf3..8257bef70 100644 --- a/packages/layerchart/src/routes/docs/examples/Treemap/+page.svelte +++ b/packages/layerchart/src/routes/docs/examples/Treemap/+page.svelte @@ -196,7 +196,7 @@ height={nodeHeight} stroke={colorBy === 'children' ? 'hsl(var(--color-primary-content))' - : hsl(nodeColor).darker(1)} + : hsl(nodeColor).darker(1).toString()} stroke-opacity={colorBy === 'children' ? 0.2 : 1} fill={nodeColor} fill-opacity={node.children ? 0.5 : 1} @@ -337,7 +337,7 @@ height={nodeHeight} stroke={colorBy === 'children' ? 'hsl(var(--color-primary-content))' - : hsl(nodeColor).darker(1)} + : hsl(nodeColor).darker(1).toString()} stroke-opacity={colorBy === 'children' ? 0.2 : 1} fill={nodeColor} fill-opacity={node.children ? 0.5 : 1} @@ -448,7 +448,7 @@ height={nodeHeight} stroke={colorBy === 'children' ? 'hsl(var(--color-primary-content))' - : hsl(nodeColor).darker(1)} + : hsl(nodeColor).darker(1).toString()} stroke-opacity={colorBy === 'children' ? 0.2 : 1} fill={nodeColor} rx={5} From 7f392ad8f99f0679129d67a6e789241b03184b11 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Fri, 10 Jan 2025 12:00:56 -0500 Subject: [PATCH 39/55] feat(render): Support `fill-opacity` --- packages/layerchart/src/lib/utils/canvas.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/layerchart/src/lib/utils/canvas.ts b/packages/layerchart/src/lib/utils/canvas.ts index 5b367fa98..3aa782b58 100644 --- a/packages/layerchart/src/lib/utils/canvas.ts +++ b/packages/layerchart/src/lib/utils/canvas.ts @@ -4,7 +4,8 @@ const CANVAS_STYLES_ELEMENT_ID = '__layerchart_canvas_styles_id'; type ComputedStylesOptions = { styles?: Partial< - Omit & { + Omit & { + fillOpacity?: number | string; strokeWidth?: number | string; opacity?: number | string; } @@ -82,7 +83,7 @@ function render( } else if (computedStyles.textAnchor === 'end') { canvasCtx.textAlign = 'right'; } else { - canvasCtx.textAlign = computedStyles.textAlign as CanvasTextAlign; // TODO: Handle `justify` and `match-parent`? + canvasCtx.textAlign = computedStyles.textAlign as CanvasTextAlign; // TODO: Handle/map `justify` and `match-parent`? } // TODO: Handle `textBaseline` / `verticalAnchor` (Text) @@ -105,6 +106,10 @@ function render( if (attr === 'fill') { const fill = computedStyles?.fill === DEFAULT_FILL ? null : computedStyles?.fill; if (fill) { + if (computedStyles?.fillOpacity) { + canvasCtx.globalAlpha = Number(computedStyles?.fillOpacity); + } + canvasCtx.fillStyle = fill; render.fill(canvasCtx); } From 085fd8c5598078d12c345e31ae7a66fd25f5fe7b Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Fri, 10 Jan 2025 12:06:34 -0500 Subject: [PATCH 40/55] feat(Arc): Support Canvas context --- .changeset/bright-buses-buy.md | 5 + .../layerchart/src/lib/components/Arc.svelte | 107 +++++++++++++----- 2 files changed, 85 insertions(+), 27 deletions(-) create mode 100644 .changeset/bright-buses-buy.md diff --git a/.changeset/bright-buses-buy.md b/.changeset/bright-buses-buy.md new file mode 100644 index 000000000..c9d97a6fb --- /dev/null +++ b/.changeset/bright-buses-buy.md @@ -0,0 +1,5 @@ +--- +'layerchart': minor +--- + +feat(Arc): Support Canvas context diff --git a/packages/layerchart/src/lib/components/Arc.svelte b/packages/layerchart/src/lib/components/Arc.svelte index f23ddd44f..1fec966c9 100644 --- a/packages/layerchart/src/lib/components/Arc.svelte +++ b/packages/layerchart/src/lib/components/Arc.svelte @@ -17,7 +17,7 @@ // https://svelte.dev/repl/09711e43a1264ba18945d7db7cab9335?version=3.38.2 // https://codepen.io/simeydotme/pen/rrOEmO/ - import { tick } from 'svelte'; + import { onDestroy, tick } from 'svelte'; import type { SVGAttributes } from 'svelte/elements'; import type { spring as springStore, tweened as tweenedStore } from 'svelte/motion'; import { arc as d3arc } from 'd3-shape'; @@ -28,6 +28,8 @@ import { motionStore } from '$lib/stores/motionStore.js'; import { degreesToRadians } from '$lib/utils/math.js'; import type { TooltipContextValue } from './tooltip/TooltipContext.svelte'; + import { getCanvasContext } from './layout/Canvas.svelte'; + import { renderPathData } from '../utils/canvas.js'; export let spring: boolean | Parameters[1] = undefined; export let tweened: boolean | Parameters[1] = undefined; @@ -80,6 +82,10 @@ export let padAngle = 0; // export let padRadius = 0; + export let fill: string | undefined = undefined; + export let stroke: string | undefined = undefined; + export let strokeWidth: number | undefined = undefined; + export let track: boolean | SVGAttributes = false; const { yRange } = chartContext(); @@ -183,36 +189,83 @@ * Data to set when showing tooltip */ export let data: any = undefined; + + const canvasContext = getCanvasContext(); + const renderContext = canvasContext ? 'canvas' : 'svg'; + + function render(ctx: CanvasRenderingContext2D) { + ctx.translate(xOffset, yOffset); + + // Track + const trackProps = { ...(typeof track === 'object' ? track : null) }; + renderPathData(ctx, trackArc(), { + styles: { + fill: trackProps['fill'] ?? undefined, + fillOpacity: trackProps['fill-opacity'] ?? undefined, + stroke: trackProps['stroke'] ?? undefined, + strokeWidth: trackProps['stroke-width'] ?? undefined, + opacity: trackProps['opacity'] ?? undefined, + }, + classes: trackProps.class ?? undefined, + }); + + // Arc + renderPathData(ctx, arc(), { + styles: { stroke, fill, strokeWidth }, + classes: $$props.class, + }); + } + + $: if (renderContext === 'canvas') { + canvasContext.register(render); + } + + $: if (renderContext === 'canvas') { + // Redraw when props changes (TODO: styles, class, etc) + arc && trackArc; + canvasContext.invalidate(); + } + + onDestroy(() => { + if (renderContext === 'canvas') { + canvasContext.deregister(render); + } + }); -{#if track} +{#if renderContext === 'svg'} + {#if track} + + {/if} + + tooltip?.show(e, data)} + on:pointermove={(e) => tooltip?.show(e, data)} + on:pointerleave={(e) => tooltip?.hide()} + on:touchmove={(e) => { + if (tooltip) { + // Prevent touch to not interfer with pointer when using tooltip + e.preventDefault(); + } + }} + on:click + on:pointerenter + on:pointermove + on:pointerleave + on:touchmove /> {/if} - - tooltip?.show(e, data)} - on:pointermove={(e) => tooltip?.show(e, data)} - on:pointerleave={(e) => tooltip?.hide()} - on:touchmove={(e) => { - if (tooltip) { - // Prevent touch to not interfer with pointer when using tooltip - e.preventDefault(); - } - }} - on:click - on:pointerenter - on:pointermove - on:pointerleave - on:touchmove -/> - From a9c16ba93ed5ebf64129ac76bf34b50a0c0cd80f Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Fri, 10 Jan 2025 12:16:07 -0500 Subject: [PATCH 41/55] feat(Group): Support Canvas context --- .changeset/lazy-mayflies-push.md | 5 ++ .../src/lib/components/Group.svelte | 63 +++++++++++++------ .../src/lib/components/layout/Canvas.svelte | 5 +- 3 files changed, 53 insertions(+), 20 deletions(-) create mode 100644 .changeset/lazy-mayflies-push.md diff --git a/.changeset/lazy-mayflies-push.md b/.changeset/lazy-mayflies-push.md new file mode 100644 index 000000000..08a67cc85 --- /dev/null +++ b/.changeset/lazy-mayflies-push.md @@ -0,0 +1,5 @@ +--- +'layerchart': minor +--- + +feat(Group): Support Canvas context diff --git a/packages/layerchart/src/lib/components/Group.svelte b/packages/layerchart/src/lib/components/Group.svelte index c23b19339..e8089b25f 100644 --- a/packages/layerchart/src/lib/components/Group.svelte +++ b/packages/layerchart/src/lib/components/Group.svelte @@ -1,9 +1,10 @@ - - { - if (preventTouchMove) { - // Prevent touch to not interfer with pointer - e.preventDefault(); + const canvasContext = getCanvasContext(); + const renderContext = canvasContext ? 'canvas' : 'svg'; + + function render(ctx: CanvasRenderingContext2D) { + ctx.translate($tweened_x ?? 0, $tweened_y ?? 0); + } + + $: if (renderContext === 'canvas') { + canvasContext.register(render); + } + + $: if (renderContext === 'canvas') { + $tweened_x && $tweened_y; + canvasContext.invalidate(); + } + + onDestroy(() => { + if (renderContext === 'canvas') { + canvasContext.deregister(render); } - }} -> + }); + + +{#if renderContext === 'canvas'} - +{:else if renderContext === 'svg'} + + { + if (preventTouchMove) { + // Prevent touch to not interfer with pointer + e.preventDefault(); + } + }} + > + + +{/if} diff --git a/packages/layerchart/src/lib/components/layout/Canvas.svelte b/packages/layerchart/src/lib/components/layout/Canvas.svelte index 70603ab78..a072b308c 100644 --- a/packages/layerchart/src/lib/components/layout/Canvas.svelte +++ b/packages/layerchart/src/lib/components/layout/Canvas.svelte @@ -107,9 +107,10 @@ } drawFunctions.forEach((fn) => { - context.save(); + // TODO: `.save()` / `.restore()` breaks `Group`. Is this needed for resetting styles or other unidentified cases? + // context.save(); fn(context); - context.restore(); + // context.restore(); }); pendingInvalidation = false; From 6e91781b2e5fe5af3225c88aa3a04c095dd9e084 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Fri, 10 Jan 2025 14:57:19 -0500 Subject: [PATCH 42/55] fix(Canvas): Change registration API. Pass config instead of simple function to enable `retainState` use case (fix `Group` translate() use case). Return `unregister` function --- .../layerchart/src/lib/components/Arc.svelte | 5 ++- .../layerchart/src/lib/components/Area.svelte | 5 ++- .../src/lib/components/Circle.svelte | 5 ++- .../src/lib/components/GeoPath.svelte | 5 ++- .../src/lib/components/GeoPoint.svelte | 5 ++- .../src/lib/components/GeoTile.svelte | 5 ++- .../src/lib/components/Group.svelte | 5 ++- .../layerchart/src/lib/components/Line.svelte | 5 ++- .../src/lib/components/Points.svelte | 5 ++- .../layerchart/src/lib/components/Rect.svelte | 5 ++- .../src/lib/components/Spline.svelte | 5 ++- .../layerchart/src/lib/components/Text.svelte | 5 ++- .../src/lib/components/layout/Canvas.svelte | 41 ++++++++++++------- 13 files changed, 62 insertions(+), 39 deletions(-) diff --git a/packages/layerchart/src/lib/components/Arc.svelte b/packages/layerchart/src/lib/components/Arc.svelte index 1fec966c9..936bbc733 100644 --- a/packages/layerchart/src/lib/components/Arc.svelte +++ b/packages/layerchart/src/lib/components/Arc.svelte @@ -216,8 +216,9 @@ }); } + let canvasUnregister: ReturnType; $: if (renderContext === 'canvas') { - canvasContext.register(render); + canvasUnregister = canvasContext.register({ name: 'Arc', render }); } $: if (renderContext === 'canvas') { @@ -228,7 +229,7 @@ onDestroy(() => { if (renderContext === 'canvas') { - canvasContext.deregister(render); + canvasUnregister(); } }); diff --git a/packages/layerchart/src/lib/components/Area.svelte b/packages/layerchart/src/lib/components/Area.svelte index fbf19f42f..97f613582 100644 --- a/packages/layerchart/src/lib/components/Area.svelte +++ b/packages/layerchart/src/lib/components/Area.svelte @@ -142,8 +142,9 @@ }); } + let canvasUnregister: ReturnType; $: if (renderContext === 'canvas') { - canvasContext.register(render); + canvasUnregister = canvasContext.register({ name: 'Area', render }); tweened_d.subscribe(() => { canvasContext.invalidate(); @@ -152,7 +153,7 @@ onDestroy(() => { if (renderContext === 'canvas') { - canvasContext.deregister(render); + canvasUnregister(); } }); diff --git a/packages/layerchart/src/lib/components/Circle.svelte b/packages/layerchart/src/lib/components/Circle.svelte index 1f9e46de0..c5e153710 100644 --- a/packages/layerchart/src/lib/components/Circle.svelte +++ b/packages/layerchart/src/lib/components/Circle.svelte @@ -45,8 +45,9 @@ }); } + let canvasUnregister: ReturnType; $: if (renderContext === 'canvas') { - canvasContext.register(render); + canvasUnregister = canvasContext.register({ name: 'Circle', render }); } $: if (renderContext === 'canvas') { @@ -57,7 +58,7 @@ onDestroy(() => { if (renderContext === 'canvas') { - canvasContext.deregister(render); + canvasUnregister(); } }); diff --git a/packages/layerchart/src/lib/components/GeoPath.svelte b/packages/layerchart/src/lib/components/GeoPath.svelte index 598f1e7c2..34091e478 100644 --- a/packages/layerchart/src/lib/components/GeoPath.svelte +++ b/packages/layerchart/src/lib/components/GeoPath.svelte @@ -89,8 +89,9 @@ } } + let canvasUnregister: ReturnType; $: if (renderContext === 'canvas') { - canvasContext.register(_render); + canvasUnregister = canvasContext.register({ name: 'GeoPath', render: _render }); } $: if (renderContext === 'canvas') { @@ -101,7 +102,7 @@ onDestroy(() => { if (renderContext === 'canvas') { - canvasContext.deregister(_render); + canvasUnregister(); } }); diff --git a/packages/layerchart/src/lib/components/GeoPoint.svelte b/packages/layerchart/src/lib/components/GeoPoint.svelte index b88dcd0f7..c752a1542 100644 --- a/packages/layerchart/src/lib/components/GeoPoint.svelte +++ b/packages/layerchart/src/lib/components/GeoPoint.svelte @@ -28,13 +28,14 @@ render(ctx, { x, y }); } + let canvasUnregister: ReturnType; $: if (renderContext === 'canvas') { - canvasContext.register(_render); + canvasUnregister = canvasContext.register({ name: 'GeoPoint', render: _render }); } onDestroy(() => { if (renderContext === 'canvas') { - canvasContext.deregister(_render); + canvasUnregister(); } }); diff --git a/packages/layerchart/src/lib/components/GeoTile.svelte b/packages/layerchart/src/lib/components/GeoTile.svelte index 5aab2e67c..b5ef6aeea 100644 --- a/packages/layerchart/src/lib/components/GeoTile.svelte +++ b/packages/layerchart/src/lib/components/GeoTile.svelte @@ -46,8 +46,9 @@ }); } + let canvasUnregister: ReturnType; $: if (renderContext === 'canvas') { - canvasContext.register(render); + canvasUnregister = canvasContext.register({ name: 'GeoTile', render }); } $: if (renderContext === 'canvas') { @@ -57,7 +58,7 @@ onDestroy(() => { if (renderContext === 'canvas') { - canvasContext.deregister(render); + canvasUnregister(); } }); diff --git a/packages/layerchart/src/lib/components/Group.svelte b/packages/layerchart/src/lib/components/Group.svelte index e8089b25f..6cf3ca221 100644 --- a/packages/layerchart/src/lib/components/Group.svelte +++ b/packages/layerchart/src/lib/components/Group.svelte @@ -53,8 +53,9 @@ ctx.translate($tweened_x ?? 0, $tweened_y ?? 0); } + let canvasUnregister: ReturnType; $: if (renderContext === 'canvas') { - canvasContext.register(render); + canvasUnregister = canvasContext.register({ name: 'Group', render, retainState: true }); } $: if (renderContext === 'canvas') { @@ -64,7 +65,7 @@ onDestroy(() => { if (renderContext === 'canvas') { - canvasContext.deregister(render); + canvasUnregister(); } }); diff --git a/packages/layerchart/src/lib/components/Line.svelte b/packages/layerchart/src/lib/components/Line.svelte index a4b069544..e870df225 100644 --- a/packages/layerchart/src/lib/components/Line.svelte +++ b/packages/layerchart/src/lib/components/Line.svelte @@ -65,8 +65,9 @@ }); } + let canvasUnregister: ReturnType; $: if (renderContext === 'canvas') { - canvasContext.register(render); + canvasUnregister = canvasContext.register({ name: 'Line', render }); } $: if (renderContext === 'canvas') { @@ -77,7 +78,7 @@ onDestroy(() => { if (renderContext === 'canvas') { - canvasContext.deregister(render); + canvasUnregister(); } }); diff --git a/packages/layerchart/src/lib/components/Points.svelte b/packages/layerchart/src/lib/components/Points.svelte index afedc3d9b..8c28de929 100644 --- a/packages/layerchart/src/lib/components/Points.svelte +++ b/packages/layerchart/src/lib/components/Points.svelte @@ -169,13 +169,14 @@ } } + let canvasUnregister: ReturnType; $: if (renderContext === 'canvas') { - canvasContext.register(_render); + canvasUnregister = canvasContext.register({ name: 'Points', render: _render }); } onDestroy(() => { if (renderContext === 'canvas') { - canvasContext.deregister(_render); + canvasUnregister(); } }); diff --git a/packages/layerchart/src/lib/components/Rect.svelte b/packages/layerchart/src/lib/components/Rect.svelte index 0b1c80e3d..45121f45a 100644 --- a/packages/layerchart/src/lib/components/Rect.svelte +++ b/packages/layerchart/src/lib/components/Rect.svelte @@ -56,8 +56,9 @@ ); } + let canvasUnregister: ReturnType; $: if (renderContext === 'canvas') { - canvasContext.register(render); + canvasUnregister = canvasContext.register({ name: 'Rect', render }); } $: if (renderContext === 'canvas') { @@ -68,7 +69,7 @@ onDestroy(() => { if (renderContext === 'canvas') { - canvasContext.deregister(render); + canvasUnregister(); } }); diff --git a/packages/layerchart/src/lib/components/Spline.svelte b/packages/layerchart/src/lib/components/Spline.svelte index 10d8d8918..6f5fff0b7 100644 --- a/packages/layerchart/src/lib/components/Spline.svelte +++ b/packages/layerchart/src/lib/components/Spline.svelte @@ -170,8 +170,9 @@ }); } + let canvasUnregister: ReturnType; $: if (renderContext === 'canvas') { - canvasContext.register(render); + canvasUnregister = canvasContext.register({ name: 'Spline', render }); tweened_d.subscribe(() => { canvasContext.invalidate(); @@ -180,7 +181,7 @@ onDestroy(() => { if (renderContext === 'canvas') { - canvasContext.deregister(render); + canvasUnregister(); } }); diff --git a/packages/layerchart/src/lib/components/Text.svelte b/packages/layerchart/src/lib/components/Text.svelte index 996b18924..4da7c15f1 100644 --- a/packages/layerchart/src/lib/components/Text.svelte +++ b/packages/layerchart/src/lib/components/Text.svelte @@ -193,8 +193,9 @@ }); } + let canvasUnregister: ReturnType; $: if (renderContext === 'canvas') { - canvasContext.register(render); + canvasUnregister = canvasContext.register({ name: 'Text', render }); } $: if (renderContext === 'canvas') { @@ -205,7 +206,7 @@ onDestroy(() => { if (renderContext === 'canvas') { - canvasContext.deregister(render); + canvasUnregister(); } }); diff --git a/packages/layerchart/src/lib/components/layout/Canvas.svelte b/packages/layerchart/src/lib/components/layout/Canvas.svelte index a072b308c..53b10696e 100644 --- a/packages/layerchart/src/lib/components/layout/Canvas.svelte +++ b/packages/layerchart/src/lib/components/layout/Canvas.svelte @@ -1,11 +1,15 @@ -{#if _rounded === 'all' || radius === 0} +{#if (_rounded === 'all' || radius === 0) && renderContext === 'svg'} Date: Fri, 10 Jan 2025 18:03:06 -0500 Subject: [PATCH 49/55] fix(render): Workaround when using `tabular-nums` causing `computedStyles.font` to return empty --- packages/layerchart/src/lib/utils/canvas.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/layerchart/src/lib/utils/canvas.ts b/packages/layerchart/src/lib/utils/canvas.ts index 471b5a7c7..28a3b8da3 100644 --- a/packages/layerchart/src/lib/utils/canvas.ts +++ b/packages/layerchart/src/lib/utils/canvas.ts @@ -75,7 +75,7 @@ function render( } // Text properties - canvasCtx.font = computedStyles.font; + canvasCtx.font = `${computedStyles.fontSize} ${computedStyles.fontFamily}`; // build string instead of using `computedStyles.font` to fix/workaround `tabular-nums` returning `null` // TODO: Hack to handle `textAnchor` with canvas. Try to find a better approach if (computedStyles.textAnchor === 'middle') { From 3a0d22887051a007ea12682ed907e306f3e38458 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Sun, 12 Jan 2025 01:59:50 -0500 Subject: [PATCH 50/55] feat(LinearGradient): Support Canvas context (WIP) --- .changeset/shiny-lobsters-check.md | 5 + .../src/lib/components/LinearGradient.svelte | 117 ++++++++++++++---- packages/layerchart/src/lib/utils/canvas.ts | 19 ++- .../docs/components/LineChart/+page.svelte | 2 +- 4 files changed, 111 insertions(+), 32 deletions(-) create mode 100644 .changeset/shiny-lobsters-check.md diff --git a/.changeset/shiny-lobsters-check.md b/.changeset/shiny-lobsters-check.md new file mode 100644 index 000000000..8766f59de --- /dev/null +++ b/.changeset/shiny-lobsters-check.md @@ -0,0 +1,5 @@ +--- +'layerchart': minor +--- + +feat(LinearGradient): Support Canvas context diff --git a/packages/layerchart/src/lib/components/LinearGradient.svelte b/packages/layerchart/src/lib/components/LinearGradient.svelte index 44c448d16..86f22292b 100644 --- a/packages/layerchart/src/lib/components/LinearGradient.svelte +++ b/packages/layerchart/src/lib/components/LinearGradient.svelte @@ -1,6 +1,11 @@ - - - - {#if stops} - {#each stops as stop, i} - {#if Array.isArray(stop)} - - {:else} - - {/if} - {/each} - {/if} - - - - - + + +{#if renderContext === 'canvas'} + +{:else if renderContext === 'svg'} + + + + {#if stops} + {#each stops as stop, i} + {#if Array.isArray(stop)} + + {:else} + + {/if} + {/each} + {/if} + + + + + +{/if} diff --git a/packages/layerchart/src/lib/utils/canvas.ts b/packages/layerchart/src/lib/utils/canvas.ts index 28a3b8da3..b00ed1cdc 100644 --- a/packages/layerchart/src/lib/utils/canvas.ts +++ b/packages/layerchart/src/lib/utils/canvas.ts @@ -16,7 +16,7 @@ type ComputedStylesOptions = { /** * Appends or reuses `` element below `` to resolve CSS variables and classes (ex. `stroke: hsl(var(--color-primary))` => `stroke: rgb(...)` ) */ -function getComputedStyles( +export function getComputedStyles( canvas: HTMLCanvasElement, { styles, classes }: ComputedStylesOptions = {} ) { @@ -104,9 +104,13 @@ function render( paintOrder.forEach((attr) => { if (attr === 'fill') { - const fill = ['none', DEFAULT_FILL].includes(computedStyles?.fill) - ? null - : computedStyles?.fill; + const fill = + (styleOptions.styles?.fill as any) instanceof CanvasGradient + ? styleOptions.styles?.fill + : ['none', DEFAULT_FILL].includes(computedStyles?.fill) + ? null + : computedStyles?.fill; + if (fill) { const currentGlobalAlpha = canvasCtx.globalAlpha; if (computedStyles?.fillOpacity) { @@ -120,7 +124,12 @@ function render( canvasCtx.globalAlpha = currentGlobalAlpha; } } else if (attr === 'stroke') { - const stroke = computedStyles?.stroke === 'none' ? null : computedStyles?.stroke; + const stroke = + (styleOptions.styles?.stroke as any) instanceof CanvasGradient + ? styleOptions.styles?.stroke + : computedStyles?.stroke === 'none' + ? null + : computedStyles?.stroke; if (stroke) { canvasCtx.lineWidth = typeof computedStyles?.strokeWidth === 'string' diff --git a/packages/layerchart/src/routes/docs/components/LineChart/+page.svelte b/packages/layerchart/src/routes/docs/components/LineChart/+page.svelte index e8a0b0c0c..7ab43b8d7 100644 --- a/packages/layerchart/src/routes/docs/components/LineChart/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/LineChart/+page.svelte @@ -383,7 +383,7 @@
- {@const thresholdOffset = (yScale(50) / (height + padding.bottom)) * 100 + '%'} + {@const thresholdOffset = yScale(50) / (height + padding.bottom)} Date: Sun, 12 Jan 2025 11:53:09 -0500 Subject: [PATCH 51/55] Add parsePercent util and use for gradient color stops --- .../src/lib/components/LinearGradient.svelte | 4 ++-- packages/layerchart/src/lib/utils/math.ts | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/layerchart/src/lib/components/LinearGradient.svelte b/packages/layerchart/src/lib/components/LinearGradient.svelte index 86f22292b..22ddc6917 100644 --- a/packages/layerchart/src/lib/components/LinearGradient.svelte +++ b/packages/layerchart/src/lib/components/LinearGradient.svelte @@ -5,6 +5,7 @@ import { chartContext } from './ChartContext.svelte'; import { getCanvasContext } from './layout/Canvas.svelte'; import { getComputedStyles } from '../utils/canvas.js'; + import { parsePercent } from 'layerchart/utils/math.js'; /** Unique id for linearGradient */ export let id: string = uniqueId('linearGradient-'); @@ -50,8 +51,7 @@ styles: { fill: stop[1] }, classes: $$props.class, }); - // TODO: Convert percentage stops (strings) - gradient.addColorStop(stop[0], fill); + gradient.addColorStop(parsePercent(stop[0]), fill); } else { const { fill } = getComputedStyles(ctx.canvas, { styles: { fill: stop }, diff --git a/packages/layerchart/src/lib/utils/math.ts b/packages/layerchart/src/lib/utils/math.ts index f968bec88..120207e6d 100644 --- a/packages/layerchart/src/lib/utils/math.ts +++ b/packages/layerchart/src/lib/utils/math.ts @@ -53,3 +53,13 @@ export function celsiusToFahrenheit(temperature: number) { export function fahrenheitToCelsius(temperature: number) { return (temperature - 32) * (5 / 9); } + +/** Parse percent string (`50%`) to decimal (`0.5`) */ +export function parsePercent(percent: string | number) { + if (typeof percent === 'number') { + // Assume already decimal + return percent; + } else { + return Number(percent.replace('%', '')) / 100; + } +} From 8e1854588585941be9692fd3f6760ddb00ac53a8 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Sun, 12 Jan 2025 12:06:35 -0500 Subject: [PATCH 52/55] breaking(LinearGradient|RadialGradient): Rename `url` slot prop to `gradient`. Improves name, especially within canvas context --- .changeset/early-keys-think.md | 5 ++ .../src/lib/components/LinearGradient.svelte | 4 +- .../src/lib/components/RadialGradient.svelte | 2 +- .../routes/docs/components/Arc/+page.svelte | 12 +++-- .../docs/components/AreaChart/+page.svelte | 14 +++--- .../docs/components/BarChart/+page.svelte | 4 +- .../routes/docs/components/Brush/+page.svelte | 35 +++++++------- .../routes/docs/components/Frame/+page.svelte | 4 +- .../docs/components/LineChart/+page.svelte | 8 ++-- .../components/LinearGradient/+page.svelte | 8 ++-- .../docs/components/PieChart/+page.svelte | 4 +- .../components/RadialGradient/+page.svelte | 47 ++++++++++++------- .../src/routes/docs/examples/Arc/+page.svelte | 4 +- .../routes/docs/examples/Area/+page.svelte | 35 ++++++++------ .../routes/docs/examples/Bars/+page.svelte | 4 +- .../routes/docs/examples/Columns/+page.svelte | 9 +++- .../routes/docs/examples/Line/+page.svelte | 8 ++-- .../docs/examples/Oscilloscope/+page.svelte | 4 +- 18 files changed, 124 insertions(+), 87 deletions(-) create mode 100644 .changeset/early-keys-think.md diff --git a/.changeset/early-keys-think.md b/.changeset/early-keys-think.md new file mode 100644 index 000000000..6e0d66ffb --- /dev/null +++ b/.changeset/early-keys-think.md @@ -0,0 +1,5 @@ +--- +'layerchart': minor +--- + +breaking(LinearGradient|RadialGradient): Rename `url` slot prop to `gradient`. Improves name, especially within canvas context diff --git a/packages/layerchart/src/lib/components/LinearGradient.svelte b/packages/layerchart/src/lib/components/LinearGradient.svelte index 22ddc6917..275c85435 100644 --- a/packages/layerchart/src/lib/components/LinearGradient.svelte +++ b/packages/layerchart/src/lib/components/LinearGradient.svelte @@ -85,7 +85,7 @@ {#if renderContext === 'canvas'} - + {:else if renderContext === 'svg'} - + {/if} diff --git a/packages/layerchart/src/lib/components/RadialGradient.svelte b/packages/layerchart/src/lib/components/RadialGradient.svelte index 0b1bf6bd1..aaa0c63ec 100644 --- a/packages/layerchart/src/lib/components/RadialGradient.svelte +++ b/packages/layerchart/src/lib/components/RadialGradient.svelte @@ -54,4 +54,4 @@ - + diff --git a/packages/layerchart/src/routes/docs/components/Arc/+page.svelte b/packages/layerchart/src/routes/docs/components/Arc/+page.svelte index 3ac5928f1..8e48c9ee4 100644 --- a/packages/layerchart/src/routes/docs/components/Arc/+page.svelte +++ b/packages/layerchart/src/routes/docs/components/Arc/+page.svelte @@ -54,7 +54,7 @@ {#key spring} - + - + - {#if renderContext === 'canvas'} {:else if renderContext === 'svg'} diff --git a/packages/layerchart/src/lib/components/RadialGradient.svelte b/packages/layerchart/src/lib/components/RadialGradient.svelte index aaa0c63ec..8a08c2c93 100644 --- a/packages/layerchart/src/lib/components/RadialGradient.svelte +++ b/packages/layerchart/src/lib/components/RadialGradient.svelte @@ -1,6 +1,12 @@ - - - - {#if stops} - {#each stops as stop, i} - {#if Array.isArray(stop)} - - {:else} - - {/if} - {/each} - {/if} - - - - - +{#if renderContext === 'canvas'} + +{:else if renderContext === 'svg'} + + + + {#if stops} + {#each stops as stop, i} + {#if Array.isArray(stop)} + + {:else} + + {/if} + {/each} + {/if} + + + + + +{/if} From 683dc93a140d69cc5d962c66323b011bfa079137 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Sun, 12 Jan 2025 12:31:15 -0500 Subject: [PATCH 54/55] Simplify changesets --- .changeset/bright-buses-buy.md | 2 +- .changeset/calm-bikes-walk.md | 5 ----- .changeset/cold-penguins-jump.md | 2 +- .changeset/flat-pants-run.md | 5 ----- .changeset/fluffy-beds-love.md | 5 ----- .changeset/healthy-kiwis-begin.md | 5 ----- .changeset/lazy-mayflies-push.md | 5 ----- .changeset/moody-deers-wonder.md | 5 ----- .changeset/shiny-lobsters-check.md | 5 ----- .changeset/weak-terms-rhyme.md | 5 ----- 10 files changed, 2 insertions(+), 42 deletions(-) delete mode 100644 .changeset/calm-bikes-walk.md delete mode 100644 .changeset/flat-pants-run.md delete mode 100644 .changeset/fluffy-beds-love.md delete mode 100644 .changeset/healthy-kiwis-begin.md delete mode 100644 .changeset/lazy-mayflies-push.md delete mode 100644 .changeset/moody-deers-wonder.md delete mode 100644 .changeset/shiny-lobsters-check.md delete mode 100644 .changeset/weak-terms-rhyme.md diff --git a/.changeset/bright-buses-buy.md b/.changeset/bright-buses-buy.md index c9d97a6fb..e66b78356 100644 --- a/.changeset/bright-buses-buy.md +++ b/.changeset/bright-buses-buy.md @@ -2,4 +2,4 @@ 'layerchart': minor --- -feat(Arc): Support Canvas context +feat: Support Canvas context for most primatives (Arc, Area, Circle, Group, Line, LinearGradient, Rect, Spline, and Text) diff --git a/.changeset/calm-bikes-walk.md b/.changeset/calm-bikes-walk.md deleted file mode 100644 index d0e16c7ec..000000000 --- a/.changeset/calm-bikes-walk.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'layerchart': minor ---- - -feat(Line): Support Canvas context diff --git a/.changeset/cold-penguins-jump.md b/.changeset/cold-penguins-jump.md index 8d3d515d5..cd60cd2e2 100644 --- a/.changeset/cold-penguins-jump.md +++ b/.changeset/cold-penguins-jump.md @@ -2,4 +2,4 @@ 'layerchart': minor --- -feat: Support `renderContext` prop to switch between Svg (default) and Canvas for all simplified charts (AreaChart, BarChart, LineChart, PieChart, and ScatterChart) +feat: Update all simplified charts to support `renderContext` prop to switch between Svg (default) and Canvas (AreaChart, BarChart, LineChart, PieChart, and ScatterChart) diff --git a/.changeset/flat-pants-run.md b/.changeset/flat-pants-run.md deleted file mode 100644 index 666718668..000000000 --- a/.changeset/flat-pants-run.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'layerchart': minor ---- - -feat(Circle): Support Canvas context diff --git a/.changeset/fluffy-beds-love.md b/.changeset/fluffy-beds-love.md deleted file mode 100644 index 3492e3084..000000000 --- a/.changeset/fluffy-beds-love.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'layerchart': minor ---- - -feat(Text): Support Canvas context diff --git a/.changeset/healthy-kiwis-begin.md b/.changeset/healthy-kiwis-begin.md deleted file mode 100644 index a9344a85c..000000000 --- a/.changeset/healthy-kiwis-begin.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'layerchart': minor ---- - -feat(Rect): Support Canvas context diff --git a/.changeset/lazy-mayflies-push.md b/.changeset/lazy-mayflies-push.md deleted file mode 100644 index 08a67cc85..000000000 --- a/.changeset/lazy-mayflies-push.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'layerchart': minor ---- - -feat(Group): Support Canvas context diff --git a/.changeset/moody-deers-wonder.md b/.changeset/moody-deers-wonder.md deleted file mode 100644 index 6dede60c2..000000000 --- a/.changeset/moody-deers-wonder.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'layerchart': minor ---- - -feat(Spline): Support Canvas context diff --git a/.changeset/shiny-lobsters-check.md b/.changeset/shiny-lobsters-check.md deleted file mode 100644 index 8766f59de..000000000 --- a/.changeset/shiny-lobsters-check.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'layerchart': minor ---- - -feat(LinearGradient): Support Canvas context diff --git a/.changeset/weak-terms-rhyme.md b/.changeset/weak-terms-rhyme.md deleted file mode 100644 index cfede88de..000000000 --- a/.changeset/weak-terms-rhyme.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'layerchart': minor ---- - -feat(Area): Support Canvas context From 3b0415cb5551cc851bb7b7833b074a99f3286ad1 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Sun, 12 Jan 2025 12:41:16 -0500 Subject: [PATCH 55/55] Refine changeset --- .changeset/bright-buses-buy.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/bright-buses-buy.md b/.changeset/bright-buses-buy.md index e66b78356..650013953 100644 --- a/.changeset/bright-buses-buy.md +++ b/.changeset/bright-buses-buy.md @@ -2,4 +2,4 @@ 'layerchart': minor --- -feat: Support Canvas context for most primatives (Arc, Area, Circle, Group, Line, LinearGradient, Rect, Spline, and Text) +feat: Support Canvas context for most primatives (Arc, Area, Circle, Group, Line, LinearGradient, Rect, Spline, and Text). Also updates components using primatives (Axis, Bar, Grid, Rule, and more)