Skip to content

Commit

Permalink
fix(Canvas): Support multiple children (fix infinite loops, coordinat…
Browse files Browse the repository at this point in the history
…ing redraws, etc). Resolves issue #158
  • Loading branch information
techniq committed Jan 7, 2025
1 parent 3f7827d commit 54561c0
Show file tree
Hide file tree
Showing 26 changed files with 523 additions and 386 deletions.
5 changes: 5 additions & 0 deletions .changeset/flat-pants-run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'layerchart': patch
---

feat(Circle): Support Canvas render context
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/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
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
55 changes: 27 additions & 28 deletions packages/layerchart/src/lib/components/Area.svelte
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<script lang="ts">
import { getContext, onDestroy, type ComponentProps } from 'svelte';
import type { Readable } from 'svelte/store';
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';
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';
Expand All @@ -15,7 +15,8 @@
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';
import { renderPathData } from '../utils/canvas.js';
import { getCanvasContext } from './layout/Canvas.svelte';
const {
data: contextData,
Expand All @@ -26,9 +27,6 @@
yDomain,
yRange,
radial,
padding,
containerWidth,
containerHeight,
config,
} = chartContext();
Expand Down Expand Up @@ -64,10 +62,6 @@
$: 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 @@ -135,29 +129,26 @@
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(' '));
}
const canvasContext = getCanvasContext();
const renderContext = canvasContext ? 'canvas' : 'svg';
let _styles: CSSStyleDeclaration;
function render(ctx: CanvasRenderingContext2D) {
// TODO: Only apply `stroke-` to `Spline`
renderPathData($canvasCtx, $tweened_d, { class: $$props.class });
renderPathData(ctx, $tweened_d, _styles);
}
$: if (renderContext === 'canvas') {
canvasContext.register(render);
tweened_d.subscribe(() => {
canvasContext.invalidate();
});
}
onDestroy(() => {
if (renderContext === 'canvas' && $canvasCtx) {
clearCanvasContext($canvasCtx, {
padding: $padding,
containerWidth: $containerWidth,
containerHeight: $containerHeight,
});
if (renderContext === 'canvas') {
canvasContext.deregister(render);
}
});
</script>
Expand Down Expand Up @@ -187,3 +178,11 @@
on:pointerleave
/>
{/if}

<!-- Hidden div to copy computed styles -->
{#if renderContext === 'canvas'}
<div
class={cls('Area-classes hidden', $$props.class)}
use:computedStyles={(styles) => (_styles = styles)}
></div>
{/if}
57 changes: 45 additions & 12 deletions packages/layerchart/src/lib/components/Circle.svelte
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
<script lang="ts">
import { tick } from 'svelte';
import { onDestroy, tick } from 'svelte';
import type { spring as springStore, tweened as tweenedStore } from 'svelte/motion';
import { cls } from '@layerstack/tailwind';
import { motionStore } from '$lib/stores/motionStore.js';
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;
Expand All @@ -26,16 +30,45 @@
tweened_cy.set(cy);
tweened_r.set(r);
});
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);
}
$: if (renderContext === 'canvas') {
canvasContext.register(render);
}
onDestroy(() => {
if (renderContext === 'canvas') {
canvasContext.deregister(render);
}
});
</script>

<!-- svelte-ignore a11y-no-static-element-interactions -->
<circle
cx={$tweened_cx}
cy={$tweened_cy}
r={$tweened_r}
class={cls($$props.fill == null && 'fill-surface-content')}
{...$$restProps}
on:click
on:pointermove
on:pointerleave
/>
{#if renderContext === 'svg'}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<circle
cx={$tweened_cx}
cy={$tweened_cy}
r={$tweened_r}
class={cls($$props.fill == null && 'fill-surface-content')}
{...$$restProps}
on:click
on:pointermove
on:pointerleave
/>
{/if}

<!-- Hidden div to copy computed styles -->
{#if renderContext === 'canvas'}
<div
class={cls('Circle-classes hidden', $$props.class)}
use:computedStyles={(styles) => (_styles = styles)}
></div>
{/if}
16 changes: 16 additions & 0 deletions packages/layerchart/src/lib/components/ComputedStyles.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<script lang="ts">
import { computedStyles } from '@layerstack/svelte-actions';
import { cls } from '@layerstack/tailwind';
let className: string | undefined = undefined;
export { className as class };
let styles: CSSStyleDeclaration;
</script>

<div
class={cls('ComputedStyles hidden', className)}
use:computedStyles={(_styles) => (styles = _styles)}
></div>

<slot {styles} />
58 changes: 35 additions & 23 deletions packages/layerchart/src/lib/components/GeoPath.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<script lang="ts">
import { createEventDispatcher, getContext } from 'svelte';
import type { Readable } from 'svelte/store';
import { createEventDispatcher, onDestroy } from 'svelte';
import {
geoTransform as d3geoTransform,
type GeoIdentityTransform,
Expand All @@ -9,13 +8,15 @@
type GeoTransformPrototype,
} from 'd3-geo';
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 { getCanvasContext } from './layout/Canvas.svelte';
export let geojson: GeoPermissibleObjects | null | undefined = undefined;
Expand Down Expand Up @@ -55,7 +56,6 @@
}>();
const { containerWidth, containerHeight, padding } = chartContext();
const canvas = getContext<{ ctx: Readable<CanvasRenderingContext2D> }>('canvas');
const geo = geoContext();
/**
Expand All @@ -70,34 +70,38 @@
$: geoPath = geoCurvePath(_projection, curve);
$: renderContext = canvas ? 'canvas' : 'svg';
$: canvasCtx = canvas?.ctx;
$: 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 ($$props.class) {
$canvasCtx.canvas.classList.add(...$$props.class.split(' '));
}
const canvasContext = getCanvasContext();
const renderContext = canvasContext ? 'canvas' : 'svg';
let _styles: CSSStyleDeclaration;
function _render(ctx: CanvasRenderingContext2D) {
if (render) {
geoPath = geoCurvePath(_projection, curve);
render($canvasCtx, { newGeoPath: () => geoCurvePath(_projection, curve) });
render(ctx, { newGeoPath: () => geoCurvePath(_projection, curve) });
} else {
// Set the context here since setting it in `$: geoPath` is a circular reference
geoPath = geoCurvePath(_projection, curve);
if (geojson) {
console.log('rendering', _styles.fill);
const pathData = geoPath(geojson);
renderPathData($canvasCtx, pathData, { fill, stroke, strokeWidth, class: $$props.class });
// renderPathData(ctx, pathData, { ..._styles, fill, stroke, strokeWidth });
renderPathData(ctx, pathData, { ..._styles });
}
}
}
$: if (renderContext === 'canvas') {
canvasContext.register(_render);
}
$: if (renderContext === 'canvas') {
// Redraw when geojson, projection, or class change
geojson && _projection && className;
canvasContext.invalidate();
}
onDestroy(() => {
if (renderContext === 'canvas') {
canvasContext.deregister(_render);
}
});
</script>

<!-- svelte-ignore a11y-no-static-element-interactions -->
Expand All @@ -121,3 +125,11 @@
/>
</slot>
{/if}

<!-- Hidden div to copy computed styles -->
{#if renderContext === 'canvas'}
<div
class={cls('GeoPath-classes hidden', className)}
use:computedStyles={(styles) => (_styles = styles)}
></div>
{/if}
Loading

0 comments on commit 54561c0

Please sign in to comment.