Skip to content

Commit

Permalink
feat(Area): Support Canvas context
Browse files Browse the repository at this point in the history
  • Loading branch information
techniq committed Dec 15, 2024
1 parent 3d9f74c commit 9d119cc
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 19 deletions.
5 changes: 5 additions & 0 deletions .changeset/weak-terms-rhyme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'layerchart': minor
---

feat(Area): Support Canvas context
62 changes: 50 additions & 12 deletions packages/layerchart/src/lib/components/Area.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script lang="ts">
import { type ComponentProps } from 'svelte';
import { getContext, onDestroy, type ComponentProps } from 'svelte';
import type { Readable } from 'svelte/store';
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 +15,7 @@
import Spline from './Spline.svelte';
import { accessor, type Accessor } from '../utils/common.js';
import { isScaleBand } from '../utils/scales.js';
import { clearCanvasContext, renderPathData } from '../utils/canvas.js';
const {
data: contextData,
Expand All @@ -23,8 +25,11 @@
y,
yDomain,
yRange,
config,
radial,
padding,
containerWidth,
containerHeight,
config,
} = chartContext();
/** Override data instead of using context */
Expand Down Expand Up @@ -59,6 +64,10 @@
$: xOffset = isScaleBand($xScale) ? $xScale.bandwidth() / 2 : 0;
$: yOffset = isScaleBand($yScale) ? $yScale.bandwidth() / 2 : 0;
const canvas = getContext<{ ctx: Readable<CanvasRenderingContext2D> }>('canvas');
$: renderContext = canvas ? 'canvas' : 'svg';
$: canvasCtx = canvas?.ctx;
/** Provide initial `0` horizontal baseline and initially hide/untrack scale changes so not reactive (only set on initial mount) */
function defaultPathData() {
const path = $radial
Expand Down Expand Up @@ -125,8 +134,35 @@
const d = pathData ?? path(data ?? $contextData);
tweened_d.set(d ?? '');
}
$: if (renderContext === 'canvas' && $canvasCtx) {
clearCanvasContext($canvasCtx, {
padding: $padding,
containerWidth: $containerWidth,
containerHeight: $containerHeight,
});
// Transfer classes defined on <Spline> to <canvas> to enable window.getComputedStyle() retrieval (Tailwind classes, etc)
if ($$props.class) {
$canvasCtx.canvas.classList.add(...$$props.class.split(' '));
}
// TODO: Only apply `stroke-` to `Spline`
renderPathData($canvasCtx, $tweened_d, { class: $$props.class });
}
onDestroy(() => {
if (renderContext === 'canvas' && $canvasCtx) {
clearCanvasContext($canvasCtx, {
padding: $padding,
containerWidth: $containerWidth,
containerHeight: $containerHeight,
});
}
});
</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 +175,15 @@
/>
{/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}
{...$$restProps}
class={cls('path-area', $$props.class)}
on:click
on:pointermove
on:pointerleave
/>
{/if}
85 changes: 78 additions & 7 deletions packages/layerchart/src/routes/docs/components/Area/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<script lang="ts">
import type { ComponentProps } from 'svelte';
import type { Component, ComponentProps } from 'svelte';
import { Field, RangeField, Switch } from 'svelte-ux';
import { Area, Axis, Chart, Svg, Points } from 'layerchart';
import { Field, RangeField, Switch, ToggleGroup, ToggleOption } from 'svelte-ux';
import { Area, Axis, Chart, Svg, Points, Canvas, Spline } from 'layerchart';
import Preview from '$lib/docs/Preview.svelte';
import PathDataMenuField from '$lib/docs/PathDataMenuField.svelte';
Expand All @@ -13,6 +13,7 @@
let showLine = true;
let show = true;
let tweened = true;
let Context: Component = Svg;
let pathGenerator = (x: number) => x;
let curve: ComponentProps<CurveMenuField>['value'] = undefined;
Expand Down Expand Up @@ -40,11 +41,18 @@
</Field>
</div>

<div class="grid grid-cols-[100px,auto,1fr] gap-2">
<div class="grid grid-cols-[100px,auto,auto,1fr] gap-2">
<Field label="Show" let:id>
<Switch bind:checked={show} {id} size="md" />
</Field>

<Field label="Context" classes={{ input: 'mt-1 mb-[6px]' }}>
<ToggleGroup bind:value={Context} variant="outline" size="sm">
<ToggleOption value={Svg}>Svg</ToggleOption>
<ToggleOption value={Canvas}>Canvas</ToggleOption>
</ToggleGroup>
</Field>

<Field label="Tweened" let:id>
<Switch bind:checked={tweened} {id} size="md" />
</Field>
Expand All @@ -57,18 +65,81 @@
<Svg>
<Axis placement="left" grid rule />
<Axis placement="bottom" rule />
</Svg>

<svelte:component this={Context}>
{#if show}
<Area
{curve}
line={showLine && { class: 'stroke-primary stroke-2' }}
{tweened}
class="fill-primary/10"
/>
{#if showPoints}
<Points {tweened} r={3} class="fill-surface-100 stroke-primary" />
{/if}
{/if}
</svelte:component>

<!-- Render separate context for Points to play nice with Canvas (clear, etc) -->
<svelte:component this={Context}>
{#if show && showPoints}
<Points {tweened} r={3} class="fill-surface-100 stroke-primary" />
{/if}
</svelte:component>
</Chart>
</div>
</Preview>

<h1>Canvas</h1>

<div class="grid gap-2 mb-2">
<div class="grid grid-cols-[1fr,1fr,1fr,auto,auto] gap-2">
<PathDataMenuField bind:value={pathGenerator} />
<CurveMenuField bind:value={curve} />
<RangeField label="Points" bind:value={pointCount} min={2} />
<Field label="Show points" let:id>
<Switch bind:checked={showPoints} {id} size="md" />
</Field>
<Field label="Show Line" let:id>
<Switch bind:checked={showLine} {id} size="md" />
</Field>
</div>

<div class="grid grid-cols-[100px,auto,auto,1fr] gap-2">
<Field label="Show" let:id>
<Switch bind:checked={show} {id} size="md" />
</Field>

<Field label="Tweened" let:id>
<Switch bind:checked={tweened} {id} size="md" />
</Field>
</div>
</div>

<Preview {data}>
<div class="h-[300px] p-4 border rounded">
<Chart {data} x="x" y="y" yNice padding={{ left: 16, bottom: 24 }}>
<Svg>
<Axis placement="left" grid rule />
<Axis placement="bottom" rule />
</Svg>

<!-- Render separate Canvas context for each component to play nice (clear, etc). See: https://github.com/techniq/layerchart/issues/158#issuecomment-2543416108 -->
<Canvas>
{#if show}
<Area {curve} {tweened} class="fill-primary/10" />
{/if}
</Canvas>

<Canvas>
{#if show && showLine}
<Spline {curve} {tweened} class="stroke-primary stroke-2" />
{/if}
</Canvas>

<Canvas>
{#if show && showPoints}
<Points {tweened} r={3} class="fill-surface-100 stroke-primary" />
{/if}
</Canvas>
</Chart>
</div>
</Preview>
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
<Axis placement="left" grid rule />
<Axis placement="bottom" rule />
</Svg>

<svelte:component this={Context}>
{#if show}
<Spline
Expand Down

0 comments on commit 9d119cc

Please sign in to comment.