Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Overhaul Canvas support (infinite loop fix / cooperation, additional marks support, etc) #295

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
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/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 class support
5 changes: 5 additions & 0 deletions .changeset/moody-deers-wonder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'layerchart': minor
---

feat(Spline): Support Canvas context
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/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
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)
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}
51 changes: 18 additions & 33 deletions packages/layerchart/src/lib/components/GeoPath.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,21 @@
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;

/** Render to canvas */
export let render:
| ((
ctx: CanvasRenderingContext2D,
options: { geoPath: ReturnType<typeof geoCurvePath> }
options: { newGeoPath: () => ReturnType<typeof geoCurvePath> }
) => 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
Expand Down Expand Up @@ -69,48 +70,32 @@

$: 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<CSSStyleDeclaration> = {};
$: if (renderContext === 'canvas' && $canvasCtx) {
clearCanvasContext($canvasCtx, {
padding: $padding,
containerWidth: $containerWidth,
containerHeight: $containerHeight,
});

// Transfer classes defined on <GeoPath> to <canvas> to enable window.getComputedStyle() retrieval (Tailwind classes, etc)
if (className) {
$ctx.canvas.classList.add(...className.split(' '));
computedStyles = window.getComputedStyle($ctx.canvas);
if ($$props.class) {
$canvasCtx.canvas.classList.add(...$$props.class.split(' '));
}

// console.count('render');

// Clear with negative offset due to Canvas `context.translate(...)`
$ctx.clearRect(-$padding.left, -$padding.top, $containerWidth, $containerHeight);

if (render) {
geoPath = geoCurvePath(_projection, curve, $ctx);
render($ctx, { geoPath });
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();
}
}
</script>
Expand Down
Loading
Loading