Skip to content

Commit

Permalink
Overhaul Canvas support (#295)
Browse files Browse the repository at this point in the history
* feat: Add new `renderPathData()` canvas util to simplify rendering SVG path data onto canvas context with CSS class support

* fix(circlePath): Correctly handle sweep argument

* feat(Spline): Support Canvas context

* breaking(GeoPath): Simplify render prop use case by leveraging renderPathData() (ex. HitCanvas)

* fix(GeoPath): Always apply classes defined on GeoPath onto <canvas> parent (useful for rendering text, etc)

* Update more GeoPath render usage

* docs(Spline): Resolve check errors

* docs(Spline): Resolve additional check error (not sure why not failing locally)

* fix(Spline): Remove transfering classes to <canvas> (after removing from `renderPathData()`)

* docs(Spline): Add canvas explicit example

* feat(Area): Support Canvas context

* fix(Canvas): Support multiple children (fix infinite loops, coordinating redraws, etc). Resolves issue #158

* Cleanup unused imports

* Add changeset (ComputedStyles)

* fix(Canvas/GeoPath): Fix tooltip ghosting (recreate geoPath() when `geojson` data changes).  Fix tooltip path performance by rendering to separate <Canvas>

* Fix `pnpm check` errors/warnings

* docs(ZoomableMap): Hide tooltip when selecting state on canvas examples

* Add and use new `getComputedStyles()` to create/reuse single `svg` when calculating styles for canvas (resolving CSS variables or classes).

* fix(Circle): Redraw on position changes (fix tooltip highlight)

* fix(renderPathData()): Adhere to CSS paint order: https://developer.mozilla.org/en-US/docs/Web/CSS/paint-order

* fix(Rule): Remove unnecessary classes

* feat(Line): Support Canvas context

* fix(Line): Use tweened coords when rendering via canvas

* fix(renderPathData()): Respect `opacity` CSS style via `globalAlpha` canvas attribute

* feat: Add new `renderText()` canvas util to simplify rendering SVG path data onto canvas context with CSS variable and class support

* feat(Text): Support Canvas context

* docs: Use new `renderText()` to simplify manual rendering on geo example

* feat: Add new `renderRect()` canvas util to simplify rendering rectangles onto canvas context with CSS variable and class support

* feat(Rect): Support Canvas context

* fix(render()): Support `strokeDasharray` style via `ctx.setLineDash()`

* fix(Points): Render primative components (Circle /Link) instead of using `renderPathData` directly to fix tweening and support links

* fix(Spline): Fix opacity for svg context

* Add `spikePath()` util

* docs: Simplify SpikeMap canvas example using  `renderPathData()` and new `spikePath` util

* feat: Support `renderContext` prop to switch between Svg (default) and Canvas for all simplified charts (AreaChart, BarChart, LineChart, PieChart, and ScatterChart)

* feat(Canvas): Support `center` prop (similar to `Svg`) to translate children to center (useful for radial layouts)

* fix(PieChart): Use `center` prop (broke after recent refactor)

* Fix CI failures (`pnpm check`)

* feat(render): Support `fill-opacity`

* feat(Arc): Support Canvas context

* feat(Group): Support Canvas context

* fix(Canvas): Change registration API.  Pass config instead of simple function to enable `retainState` use case (fix `Group` translate() use case).  Return `unregister` function

* fix: Reduce likihood of clipping for Canvas-rendered simplified charts by increasing default padding (and add top))

* fix: Support `fillOpacity` with more primatives

* fix(render): Restore `globalAlpha` after using for `fillOpacity`

* fix(Text): Fix font color class override on canvas

* fix(render): Do not apply fill when set to `none`

* fix(Bar): Fix fully rounded when using canvas renderContext by always using `Spline` (path) instead of `Rect`

* fix(render): Workaround when using `tabular-nums` causing `computedStyles.font` to return empty

* feat(LinearGradient): Support Canvas context (WIP)

* Add parsePercent util and use for gradient color stops

* breaking(LinearGradient|RadialGradient): Rename `url` slot prop to `gradient`. Improves name, especially within canvas context

* chore(RadialGradient): Stub out canvas support

* Simplify changesets

* Refine changeset
  • Loading branch information
techniq authored Jan 12, 2025
1 parent 93793d9 commit ecf1e67
Show file tree
Hide file tree
Showing 82 changed files with 2,236 additions and 897 deletions.
5 changes: 5 additions & 0 deletions .changeset/bright-buses-buy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'layerchart': minor
---

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)
5 changes: 5 additions & 0 deletions .changeset/chilly-jeans-film.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'layerchart': patch
---

Add `spikePath()` util
5 changes: 5 additions & 0 deletions .changeset/chilly-moles-wave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'layerchart': patch
---

fix(circlePath): Correctly handle sweep argument
5 changes: 5 additions & 0 deletions .changeset/cold-penguins-jump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'layerchart': minor
---

feat: Update all simplified charts to support `renderContext` prop to switch between Svg (default) and Canvas (AreaChart, BarChart, LineChart, PieChart, and ScatterChart)
5 changes: 5 additions & 0 deletions .changeset/dry-dodos-build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'layerchart': minor
---

feat(Canvas): Support `center` prop (similar to `Svg`) to translate children to center (useful for radial layouts)
5 changes: 5 additions & 0 deletions .changeset/dry-masks-suffer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'layerchart': patch
---

feat: Add new `renderPathData()` canvas util to simplify rendering SVG path data onto canvas context with CSS variable and class support
5 changes: 5 additions & 0 deletions .changeset/dry-singers-travel.md
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions .changeset/early-keys-think.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'layerchart': minor
---

breaking(LinearGradient|RadialGradient): Rename `url` slot prop to `gradient`. Improves name, especially within canvas context
5 changes: 5 additions & 0 deletions .changeset/gentle-months-rest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'layerchart': patch
---

fix: Reduce likihood of clipping for Canvas-rendered simplified charts by increasing default padding (and add top))
5 changes: 5 additions & 0 deletions .changeset/ninety-numbers-fail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'layerchart': patch
---

fix(Canvas): Support multiple children (fix infinite loops, coordinating redraws, etc). Resolves issue #158
5 changes: 5 additions & 0 deletions .changeset/orange-parrots-arrive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'layerchart': patch
---

feat: Add new `renderText()` canvas util to simplify rendering text onto canvas context with CSS variable and class support
5 changes: 5 additions & 0 deletions .changeset/popular-stingrays-change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'layerchart': minor
---

feat: Improve Canvas implementation with registering render functions and common invalidation to synchronize redrawing
5 changes: 5 additions & 0 deletions .changeset/pretty-bears-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'layerchart': patch
---

feat: Add `scaleCanvas` util
5 changes: 5 additions & 0 deletions .changeset/shaggy-rocks-tan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'layerchart': patch
---

Add `clearCanvasContext()` util
5 changes: 5 additions & 0 deletions .changeset/warm-cars-attack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'layerchart': minor
---

feat: Add `ComputedStyles` component to easily resolve classes / CSS variable values (useful when working with <canvas>)
5 changes: 5 additions & 0 deletions .changeset/wicked-mirrors-sleep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'layerchart': minor
---

breaking(GeoPath): Simplify render prop use case by leveraging renderPathData() (ex. HitCanvas)
2 changes: 1 addition & 1 deletion packages/layerchart/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
"type": "module",
"dependencies": {
"@dagrejs/dagre": "^1.1.4",
"@layerstack/svelte-actions": "^0.0.9",
"@layerstack/svelte-actions": "^0.0.11",
"@layerstack/svelte-stores": "^0.0.9",
"@layerstack/tailwind": "^0.0.11",
"@layerstack/utils": "^0.0.7",
Expand Down
110 changes: 83 additions & 27 deletions packages/layerchart/src/lib/components/Arc.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<typeof springStore>[1] = undefined;
export let tweened: boolean | Parameters<typeof tweenedStore>[1] = undefined;
Expand Down Expand Up @@ -80,6 +82,11 @@
export let padAngle = 0;
// export let padRadius = 0;
export let fill: string | undefined = undefined;
export let fillOpacity: number | undefined = undefined;
export let stroke: string | undefined = undefined;
export let strokeWidth: number | undefined = undefined;
export let track: boolean | SVGAttributes<SVGPathElement> = false;
const { yRange } = chartContext();
Expand Down Expand Up @@ -183,36 +190,85 @@
* 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: { fill, fillOpacity, stroke, strokeWidth },
classes: $$props.class,
});
}
let canvasUnregister: ReturnType<typeof canvasContext.register>;
$: if (renderContext === 'canvas') {
canvasUnregister = canvasContext.register({ name: 'Arc', render });
}
$: if (renderContext === 'canvas') {
// Redraw when props changes (TODO: styles, class, etc)
arc && trackArc;
canvasContext.invalidate();
}
onDestroy(() => {
if (renderContext === 'canvas') {
canvasUnregister();
}
});
</script>

{#if track}
{#if renderContext === 'svg'}
{#if track}
<path
d={trackArc()}
class="track"
bind:this={trackArcEl}
{...typeof track === 'object' ? track : null}
/>
{/if}

<!-- svelte-ignore a11y-no-static-element-interactions -->
<path
d={trackArc()}
class="track"
bind:this={trackArcEl}
{...typeof track === 'object' ? track : null}
d={arc()}
transform="translate({xOffset}, {yOffset})"
{fill}
fill-opacity={fillOpacity}
{stroke}
stroke-width={strokeWidth}
{...$$restProps}
on:pointerenter={(e) => 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}

<!-- svelte-ignore a11y-no-static-element-interactions -->
<path
d={arc()}
transform="translate({xOffset}, {yOffset})"
{...$$restProps}
on:pointerenter={(e) => 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
/>

<slot value={$tweened_value} centroid={trackArcCentroid} {boundingBox} />
63 changes: 51 additions & 12 deletions packages/layerchart/src/lib/components/Area.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { type ComponentProps } from 'svelte';
import { onDestroy, type ComponentProps } from 'svelte';
import type { tweened as tweenedStore } from 'svelte/motion';
import { type Area, area as d3Area, areaRadial } from 'd3-shape';
import type { CurveFactory } from 'd3-shape';
Expand All @@ -14,6 +14,8 @@
import Spline from './Spline.svelte';
import { accessor, type Accessor } from '../utils/common.js';
import { isScaleBand } from '../utils/scales.js';
import { renderPathData } from '../utils/canvas.js';
import { getCanvasContext } from './layout/Canvas.svelte';
const {
data: contextData,
Expand All @@ -23,8 +25,8 @@
y,
yDomain,
yRange,
config,
radial,
config,
} = chartContext();
/** Override data instead of using context */
Expand Down Expand Up @@ -52,6 +54,11 @@
/** Enable showing line */
export let line: boolean | Partial<ComponentProps<Spline>> = false;
export let fill: string | undefined = undefined;
export let fillOpacity: number | 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;
Expand Down Expand Up @@ -125,8 +132,34 @@
const d = pathData ?? path(data ?? $contextData);
tweened_d.set(d ?? '');
}
const canvasContext = getCanvasContext();
const renderContext = canvasContext ? 'canvas' : 'svg';
function render(ctx: CanvasRenderingContext2D) {
renderPathData(ctx, $tweened_d, {
styles: { fill, fillOpacity, stroke, strokeWidth },
classes: $$props.class,
});
}
let canvasUnregister: ReturnType<typeof canvasContext.register>;
$: if (renderContext === 'canvas') {
canvasUnregister = canvasContext.register({ name: 'Area', render });
tweened_d.subscribe(() => {
canvasContext.invalidate();
});
}
onDestroy(() => {
if (renderContext === 'canvas') {
canvasUnregister();
}
});
</script>

<!-- TODO: Find way to not clear <Canvas> when rendering Spline (remove Area rendering). Idea: https://github.com/techniq/layerchart/issues/158#issuecomment-2543416108 -->
{#if line}
<Spline
{data}
Expand All @@ -139,13 +172,19 @@
/>
{/if}

<!-- svelte-ignore a11y-no-static-element-interactions -->
<path
d={$tweened_d}
clip-path={clipPath}
{...$$restProps}
class={cls('path-area', $$props.class)}
on:click
on:pointermove
on:pointerleave
/>
{#if renderContext === 'svg'}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<path
d={$tweened_d}
clip-path={clipPath}
{fill}
fill-opacity={fillOpacity}
{stroke}
stroke-width={strokeWidth}
{...$$restProps}
class={cls('path-area', $$props.class)}
on:click
on:pointermove
on:pointerleave
/>
{/if}
6 changes: 5 additions & 1 deletion packages/layerchart/src/lib/components/Bar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import { isScaleBand } from '../utils/scales.js';
import { accessor, type Accessor } from '../utils/common.js';
import { greatestAbs } from '@layerstack/utils';
import { getCanvasContext } from './layout/Canvas.svelte';
const { x: xContext, y: yContext, xScale } = chartContext();
Expand Down Expand Up @@ -106,9 +107,12 @@
z`
.split('\n')
.join('');
const canvasContext = getCanvasContext();
const renderContext = canvasContext ? 'canvas' : 'svg';
</script>

{#if _rounded === 'all' || radius === 0}
{#if (_rounded === 'all' || radius === 0) && renderContext === 'svg'}
<Rect
{fill}
{spring}
Expand Down
Loading

0 comments on commit ecf1e67

Please sign in to comment.