Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ A full Svelte 5 rewrite. Stores are gone: the context is a reactive getter objec
- `debug` no longer prints during server-side rendering; it prints in the browser after hydration.
- Unknown props are reported with a console warning unless `verbose={false}`.
- A range you customized on a passed-in scale is now preserved instead of being overwritten with the dimension's default – so `zScale={scaleOrdinal(schemeCategory10)}` keeps its colors. Layer Cake still manages the range of pristine scales, and an explicit `[name]Range` prop always wins. Per [#364](https://github.com/mhkeller/layercake/issues/364).
- `<Canvas>` now owns the canvas. It fills the whole chart container. Before every repaint it scales the canvas for the screen, clears it and moves the origin to the top-left of the chart area. Components draw by calling `getCanvasContext().draw(ctx => { ... })` once while they set up. Several can share one `<Canvas>`. Drawings can run into the padding like Svg and Html children do. To migrate, move your drawing into a `draw` function and delete the `scaleCanvas`, `clearRect` and `$effect` around it. Pointer coordinates read off the `<canvas>` element (`offsetX`, `getBoundingClientRect()`) are now relative to the container, so subtract `k.padding.left`/`top` if you hit-test that way. See the [Canvas guide](https://layercake.graphics/guide#canvas).

**New features**

Expand All @@ -28,6 +29,9 @@ A full Svelte 5 rewrite. Stores are gone: the context is a reactive getter objec
- New `x2DomainSort`, `y2DomainSort`, `cDomainSort` and `c2DomainSort` props.
- The context exposes `element`, the `.layercake-container` div.
- Dimensions are defined as data in a registry (`settings/dimensions.js`); prop handling, scale creation, context keys and TypeScript definitions are all generated from it.
- `getCanvasContext()` returns the typed canvas context: `draw(fn)` to add a layer, `redraw()` to repaint by hand and `ctx` to read the canvas.
- `<Canvas>` accepts the same `overflow` prop as the other layouts. `overflow="hidden"` clips drawings at the edge of the chart area.
- `k.pointer(event)` returns chart-area `[x, y]` for a pointer event, the same on every layer. Useful for hit-testing on canvas, where the element covers the whole container.

**Performance**

Expand All @@ -47,16 +51,18 @@ A full Svelte 5 rewrite. Stores are gone: the context is a reactive getter objec
- The zero-width/zero-height container warning now always fires when the container is unsized, not only when a child happens to read a size-dependent value.
- Axis components fall back gracefully on charts that don't configure the opposite dimension.
- The declared svelte peer dependency now matches the version the library actually requires.
- The WebGL layout's `<canvas>` no longer spills past the container by the padding. Its CSS was over-constrained, so the element was container-sized but offset by the top and left padding.

## Migrating from 10.x

| 10.x | 11.0 |
| ------------------------------------------------ | ------------------------------------------------------------------ |
| `const { data, xGet } = getContext('LayerCake')` | `const k = getLayerCakeContext()` |
| `$xGet(d)`, `$yScale.ticks()`, `$width` | `k.xGet(d)`, `k.yScale.ticks()`, `k.width` |
| `<LayerCake let:width>` | `{#snippet children(k)}...{/snippet}` or read `k.width` in a child |
| Color via `z` | Still works, but `c` is now the dedicated color dimension |
| `getContext('canvas')` store | `getContext('canvas').ctx` getter object (same for `'gl'`) |
| 10.x | 11.0 |
| ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `const { data, xGet } = getContext('LayerCake')` | `const k = getLayerCakeContext()` |
| `$xGet(d)`, `$yScale.ticks()`, `$width` | `k.xGet(d)`, `k.yScale.ticks()`, `k.width` |
| `<LayerCake let:width>` | `{#snippet children(k)}...{/snippet}` or read `k.width` in a child |
| Color via `z` | Still works, but `c` is now the dedicated color dimension |
| `$ctx` from `getContext('canvas')`, then `scaleCanvas` + `clearRect` + draw in an effect | `getCanvasContext().draw(ctx => { ...draw... })` – no scaling, clearing or effect needed |
| `getContext('gl')` store | `getContext('gl').gl` getter object |

# 10.0.3

Expand Down
30 changes: 11 additions & 19 deletions src/_components/Map.canvas.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@
Generates a canvas map using the `geoPath` function from [d3-geo](https://github.com/d3/d3-geo).
-->
<script>
import { getContext } from 'svelte';
import { scaleCanvas, getLayerCakeContext } from 'layercake';
import { getLayerCakeContext, getCanvasContext } from 'layercake';
import { geoPath } from 'd3-geo';

const k = getLayerCakeContext();

const canvasCtx = getContext('canvas');
const canvas = getCanvasContext();

/**
* @typedef {Object} Props
Expand All @@ -29,27 +27,21 @@

let featuresToDraw = $derived(features || k.data.features);

$effect(() => {
if (!k.width || !k.height || !canvasCtx.ctx) return;

const context = canvasCtx.ctx;

scaleCanvas(context, k.width, k.height);
context.clearRect(0, 0, k.width, k.height);

// Layer Cake runs this on every repaint: resize, new data or a prop change
canvas.draw(ctx => {
featuresToDraw.forEach(
/** @param {any} feature */ feature => {
context.beginPath();
ctx.beginPath();
// Set the context here since setting it in `geoPath` is a circular reference
geoPathFn.context(context);
geoPathFn.context(ctx);
geoPathFn(feature);

context.fillStyle = fill || k.cGet(feature.properties);
context.fill();
ctx.fillStyle = fill || k.cGet(feature.properties);
ctx.fill();

context.lineWidth = strokeWidth;
context.strokeStyle = stroke;
context.stroke();
ctx.lineWidth = strokeWidth;
ctx.strokeStyle = stroke;
ctx.stroke();
}
);
});
Expand Down
30 changes: 11 additions & 19 deletions src/_components/MapPoints.canvas.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@
Generates canvas dots onto a map using [d3-geo](https://github.com/d3/d3-geo).
-->
<script>
import { getContext } from 'svelte';
import { scaleCanvas, getLayerCakeContext } from 'layercake';
import { getLayerCakeContext, getCanvasContext } from 'layercake';

const k = getLayerCakeContext();

const canvasCtx = getContext('canvas');
const canvas = getCanvasContext();

/**
* @typedef {Object} Props
Expand All @@ -34,25 +32,19 @@

let featuresToDraw = $derived(features || k.data.features);

$effect(() => {
if (!k.width || !k.height || !canvasCtx.ctx) return;

const context = canvasCtx.ctx;

scaleCanvas(context, k.width, k.height);
context.clearRect(0, 0, k.width, k.height);

// Layer Cake runs this on every repaint: resize, new data or a prop change
canvas.draw(ctx => {
// To scale the circle by size, set width and height to `k.rGet(d.properties)`
featuresToDraw.forEach(
/** @param {any} d */ d => {
context.beginPath();
ctx.beginPath();
const coordinates = projectionFn(d.geometry.coordinates);
context.arc(coordinates[0], coordinates[1], r, 0, 2 * Math.PI, false);
context.fillStyle = fill;
context.fill();
context.lineWidth = strokeWidth;
context.strokeStyle = stroke;
context.stroke();
ctx.arc(coordinates[0], coordinates[1], r, 0, 2 * Math.PI, false);
ctx.fillStyle = fill;
ctx.fill();
ctx.lineWidth = strokeWidth;
ctx.strokeStyle = stroke;
ctx.stroke();
}
);
});
Expand Down
39 changes: 11 additions & 28 deletions src/_components/Scatter.canvas.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@
Generates a canvas scatter plot.
-->
<script>
import { getContext } from 'svelte';
import { scaleCanvas, getLayerCakeContext } from 'layercake';
import { getLayerCakeContext, getCanvasContext } from 'layercake';

const k = getLayerCakeContext();

const canvasCtx = getContext('canvas');
const canvas = getCanvasContext();

/**
* @typedef {Object} Props
Expand All @@ -21,31 +19,16 @@
/** @type {Props} */
let { r = 5, fill = '#0cf', stroke = '#000', strokeWidth = 1 } = $props();

$effect(() => {
if (!k.width || !k.height || !canvasCtx.ctx) return;

const context = canvasCtx.ctx;

/**
* If you were to have multiple canvas layers
* maybe for some artistic layering purposes
* put these reset functions in the first layer, not each one
* since they should only run once per update
*/
scaleCanvas(context, k.width, k.height);
context.clearRect(0, 0, k.width, k.height);

/**
* Draw our scatterplot
*/
// Layer Cake runs this on every repaint: resize, new data or a prop change
canvas.draw(ctx => {
k.data.forEach((/** @type {any} d */ d) => {
context.beginPath();
context.arc(k.xGet(d), k.yGet(d), r, 0, 2 * Math.PI, false);
context.lineWidth = strokeWidth;
context.strokeStyle = stroke;
context.stroke();
context.fillStyle = fill;
context.fill();
ctx.beginPath();
ctx.arc(k.xGet(d), k.yGet(d), r, 0, 2 * Math.PI, false);
ctx.lineWidth = strokeWidth;
ctx.strokeStyle = stroke;
ctx.stroke();
ctx.fillStyle = fill;
ctx.fill();
});
});
</script>
2 changes: 1 addition & 1 deletion src/content/examples/MapLayered.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
A canvas layer and an SVG layer. This technique is useful if you have a background layer that would require a large number of DOM nodes. Rendering that layer with canvas will speed up the page. The shapes you actually care about are in SVG to make styling and mouse interaction easier.
A canvas layer and an SVG layer. This technique is useful if you have a background layer that would require a large number of DOM nodes. Rendering that layer with canvas will possibly improve performance. The shapes you actually care about are in SVG to make styling and mouse interaction easier. The canvas layer holds two components, the state shapes and a dot for each state too small to label; one `<Canvas>` paints both.
2 changes: 1 addition & 1 deletion src/content/guide/03-layercake-props.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ A few shapes repeat across the props below. Every accessor – `x`, `y`, `c` and
</script>
```

Two more names come from the context: `LayerCakeContext` for the object you get back from `getLayerCakeContext()`, and `Scale` for the d3 scales hanging off it. See [Typing the context](/guide#typing-the-context).
Two more names come from the context: `LayerCakeContext` for the object you get back from `getLayerCakeContext()`, and `Scale` for the d3 scales hanging off it. Canvas layers have `CanvasContext` and `CanvasDrawFn`. See [Typing the context](/guide#typing-the-context).

The headings below spell out the full shape rather than the alias, so you can see what a prop takes without looking anything up.

Expand Down
13 changes: 13 additions & 0 deletions src/content/guide/04-computed-context-values.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,19 @@ The width of the drawable space for the chart. This is the width of the parent c

The height of the drawable space for the chart. This is the height of the parent container taking into account any padding. It's also on the children snippet, as `k.height`.

### pointer(event: `MouseEvent`)

Chart-area coordinates for a pointer event, as `[x, y]`. Layers cover different boxes – Canvas covers the whole container while Svg and Html cover the chart area – so `offsetX` and friends change meaning depending on where you listen. `k.pointer` measures against the container and subtracts the padding, so it gives the same answer everywhere.

```svelte
<div
onmousemove={e => {
const [x, y] = k.pointer(e);
const nearestYear = k.xScale.invert(x);
}}
></div>
```

### x `Function`

The x accessor. This will always be a function regardless of whether you passed in a string or an array as a prop. If you passed in an array, it will return an array of equal length.
Expand Down
63 changes: 28 additions & 35 deletions src/content/guide/05-layout-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Layer Cake comes with layout components that provide HTML, Svg, ScaledSvg, Canva

You must wrap your chart components in these layout components for them to appear properly scaled. For Html and Svg components, they create a `<div>` and `<svg>`, respectively.

The Canvas and WebGL layout components also create rendering contexts that are made available to your layer components on their own Svelte contexts, under the `'canvas'` and `'gl'` keys, respectively. See the [Canvas](/guide#canvas) and [WebGL](/guide#webgl) sections below for details.
The Canvas and WebGL layout components also create rendering contexts that are made available to your layer components on their own Svelte contexts, under the `'canvas'` and `'gl'` keys, respectively – `getCanvasContext()` returns the canvas one. See the [Canvas](/guide#canvas) and [WebGL](/guide#webgl) sections below for details.

Each of these components also takes props. See the next section [Layout component props](/guide#layout-component-props) for more info.

Expand Down Expand Up @@ -186,23 +186,17 @@ This component also has a named `defs` [snippet](https://svelte.dev/docs/svelte/
</style>
```

In the component, you access the canvas context with `const canvasCtx = getContext('canvas');` and read the 2d context as `canvasCtx.ctx`. This value is on a different context from the LayerCake one because you could have multiple canvas layers and there wouldn't be an easy way to grab the right one. This way, the component always has access to just its parent Canvas component.
The `<canvas>` element covers the whole chart container, padding included. Layer Cake moves its origin to the top-left of the chart area, so you draw in the same coordinates as an Svg or Html child: `k.xGet(d)` lands in the same spot on every layout. Anything you draw past the edges shows up in the padding, the way it does on the other layouts. Pass `overflow="hidden"` to clip at the chart area instead.

> Warning: If you want to draw multiple canvas layers, use one `<Canvas>` tag each. There is a bug in [Svelte's reactivity](https://github.com/mhkeller/layercake/issues/50) that will cause an infinite loop if you add two or more components in a single `<Canvas>` tag.

> Since the `canvasCtx.ctx` value is a normal 2d context, the underlying canvas element is accessible under `canvasCtx.ctx.canvas`.

Here's an example showing a scatter plot.
Components draw by handing Layer Cake a function. Get the canvas context with `getCanvasContext()` and call `canvas.draw(ctx => { ... })`. Here's a scatter plot:

```svelte
<!-- { filename: './components/CanvasLayer.svelte' } -->
<script>
import { getContext } from 'svelte';
import { getLayerCakeContext, scaleCanvas } from 'layercake';
import { getLayerCakeContext, getCanvasContext } from 'layercake';

const k = getLayerCakeContext();

const canvasCtx = getContext('canvas');
const canvas = getCanvasContext();

/**
* @typedef {Object} Props
Expand All @@ -215,36 +209,35 @@ Here's an example showing a scatter plot.
/** @type {Props} */
let { r = 5, fill = '#0cf', stroke = '#000', strokeWidth = 1 } = $props();

$effect(() => {
if (!k.width || !k.height || !canvasCtx.ctx) return;

const context = canvasCtx.ctx;

/**
* If you were to have multiple canvas layers
* maybe for some artistic layering purposes
* put these reset functions in the first layer, not each one
* since they should only run once per update
*/
scaleCanvas(context, k.width, k.height);
context.clearRect(0, 0, k.width, k.height);

/**
* Draw our scatterplot
*/
canvas.draw(ctx => {
k.data.forEach((/** @type {any} d */ d) => {
context.beginPath();
context.arc(k.xGet(d), k.yGet(d), r, 0, 2 * Math.PI, false);
context.lineWidth = strokeWidth;
context.strokeStyle = stroke;
context.stroke();
context.fillStyle = fill;
context.fill();
ctx.beginPath();
ctx.arc(k.xGet(d), k.yGet(d), r, 0, 2 * Math.PI, false);
ctx.lineWidth = strokeWidth;
ctx.strokeStyle = stroke;
ctx.stroke();
ctx.fillStyle = fill;
ctx.fill();
});
});
</script>
```

Call `draw` once while your component is setting up. Layer Cake runs your function every time the chart repaints: on resize, new data or a prop change. Before each repaint it scales the canvas for the screen, clears it and moves the origin, so your function only draws. Everything the function reads (props, `$state`, `k.*`) is tracked, so changing any of it repaints. The function runs inside an effect: it can read reactive values but should not write them.

Several components can draw on one `<Canvas>`. They paint in the order they called `draw`, so the first component ends up at the bottom. A component that is removed and added back by an `{#if}` goes to the top of the stack. Each component's layer is removed when the component is destroyed. `draw` also returns a function that removes it sooner.

```svelte
<Canvas>
<Background />
<Points />
</Canvas>
```

`canvas.ctx` is the canvas's 2d context (`null` until the canvas mounts) for reading – the pixel under the pointer, `canvas.ctx.canvas.toDataURL()` – rather than drawing. If your draw function reads something Svelte can't see change, like an array you mutate in place or an image that just finished loading, call `canvas.redraw()` from wherever that change happens. That runs the whole paint again, the same as after a resize: the canvas is cleared and every draw function is called, not just yours.

The canvas context is separate from the LayerCake one because you could have multiple canvas layers and there wouldn't be an easy way to grab the right one. This way, the component always has access to just its parent Canvas component.

### WebGL

```svelte
Expand Down
6 changes: 3 additions & 3 deletions src/content/guide/06-layout-component-props.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ In addition to the [accessibility props](guide#accessibility) described above, a
- [zIndex](guide#zindex) `number|string`
- [pointerEvents](guide#pointerevents) `boolean`

The Html, Svg and ScaledSvg layout components also accept:
The Html, Svg, ScaledSvg and Canvas layout components also accept:

- [overflow](guide#overflow) `'visible'|'hidden'`

Expand All @@ -28,7 +28,7 @@ Each layout component also export an `element` prop that you can bind to and rep
- In the `Html` component, `element` equals the `<div>` tag.
- In the `Svg` component, `element` equals the `<svg>` tag.
- In the `ScaledSvg` component, `element` equals the `<svg>` tag.
- In the `Canvas` component, `element` equals the `<canvas>` tag.
- In the `Canvas` component, `element` equals the `<canvas>` tag. It covers the whole container, padding included – see the [Canvas](/guide#canvas) section.
- In the `WebGL` component, `element` equals the `<canvas>` tag.

The `Canvas` and the `WebGL` components also export a `context` variable that you can bind to and is also available as a slot prop.
Expand Down Expand Up @@ -81,7 +81,7 @@ Useful for tooltip layers that need to be display above chart elements but not c

### overflow `'visible'|'hidden'`

For Html, Svg and ScaledSvg components, whether or not the CSS `overflow` property is set to `'visible'` or `'hidden'`. Useful if you want to hide overflow during an animation or values that exceed the bounds of your chart. See [PR#311](https://github.com/mhkeller/layercake/pull/311) for some examples.
For Html, Svg and ScaledSvg components, whether the CSS `overflow` property is set to `'visible'` or `'hidden'`. For Canvas, whether drawings are clipped at the edge of the chart area. Useful if you want to hide overflow during an animation or values that exceed the bounds of your chart. See [PR#311](https://github.com/mhkeller/layercake/pull/311) for some examples.

```svelte
<LayerCake ...>
Expand Down
Loading