Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/add-pattern-rects.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'layerchart': minor
---

feat(Pattern): Add `rects` shape definition for tile patterns for rendering one or more rectangles per pattern tile
5 changes: 5 additions & 0 deletions .changeset/add-waffle-component.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'layerchart': minor
---

feat(Waffle): Add Waffle component for countable-cell visualizations
5 changes: 5 additions & 0 deletions .changeset/fix-canvas-pattern-rendering.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'layerchart': patch
---

fix(Pattern): fix alignment and sharply render on high-DPI displays when using Canvas layers
6 changes: 5 additions & 1 deletion docs/src/content/components/Pattern.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
description: Fill component which provides a line or circle-based fill pattern for chart elements.
description: Fill component which provides a line, circle, or rect-based fill pattern for chart elements.
category: fill
layers: [svg, canvas, html]
related: [LinearGradient, RadialGradient]
Expand All @@ -15,6 +15,10 @@ related: [LinearGradient, RadialGradient]

:example{ name="circles" noResize clip}

### Rects

:example{ name="rects" noResize clip}

### With Fill color

:example{ name="with-fill-color" noResize clip}
Expand Down
99 changes: 99 additions & 0 deletions docs/src/content/components/Waffle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
---
description: Subdivides each bar into countable square cells, ideal for showing exact quantities.
category: marks
layers: [svg, canvas]
related: [Bar, Bars, BarChart]
---

The **Waffle** mark visualizes quantities as a grid of small, countable square cells. Each cell represents a discrete `unit` of value, so a waffle of 24 cells unambiguously says "twenty-four" — much easier to read at a glance than a continuous bar of equivalent height.

Waffles are rendered as a single `<Path>` per datum filled with a tiled `<Pattern>` (one cell per tile), so even a 1,000-cell waffle costs a single path node. Cells aren't individually addressable for hover/click — for that, use a [Cell](/docs/components/Cell) grid instead.

:example{ name="basic" }

## Axis

The waffle mark comes in two orientations. `axis="y"` extends vertically; `axis="x"` extends horizontally. The other axis is the **anchor axis** — typically a band scale of categories. When `axis` is omitted, it falls back to the chart's `valueAxis`.

:example{ name="horizontal" }

## Cells (`unit`, `multiple`, `gap`)

`Waffle` automatically determines the appropriate number of cells along the anchor axis (`multiple`) so that cells stay square, don't overlap, and remain consistent with position scales.

| Prop | Default | Purpose |
| ---------- | ------- | ---------------------------------------------------------------------------------------------------------------- |
| `unit` | `1` | Quantity each cell represents. For large values, increase to keep cell counts manageable. |
| `multiple` | _auto_ | Cells per row (along the anchor axis). Auto-computed from bar width and unit so cells stay approximately square. |
| `gap` | `1` | Pixel separation between adjacent cells. |
| `round` | `false` | Partial-cell handling: `true` for `Math.round`, or pass a custom function (`Math.floor`, `Math.ceil`). |

Drag the slider to see how the layout adapts as the value changes:

:example{ name="auto-multiple" }

:::note
The number of cells along the anchor axis is guaranteed to be an integer, but it might not be a multiple or factor of the value-axis tick interval. For example, the waffle might have 5 rows while the x-axis shows ticks every 20 units.
:::

You can also set `multiple` directly, though note that manually setting it may produce non-square cells if there isn't enough room.

:example{ name="unit-multiple" }

Alternatively, you can bias the automatic `multiple` value while preserving square cells by adjusting the band scale's padding.

:example{ name="band-padding" }

For large values, increasing `unit` keeps cell counts manageable while still showing the discrete nature of the data. Here, each cell represents 5 Olympians born in the same 5-year period:

:example{ name="olympians-by-birth-year" }

## Cell shape (`rx` / `ry`)

`rx` and `ry` round each cell's corners (in pixels, or `"100%"` for circles — a stacked-dots look).

:example{ name="circular-cells" }

## Proportion of a whole

Two waffle marks layered together — a faded one sized to the total and an opaque one sized to the value — turn each band into a "X out of N" graphic. After ["Teens in Syria"](https://www.economist.com/graphic-detail/2015/08/19/teens-in-syria) (_The Economist_, August 2015):

:example{ name="survey" }

## Custom symbol

Pass a `symbol` snippet to render an icon, glyph, or arbitrary SVG in each cell instead of the default rect. The snippet receives the cell's `width` and `height` — pass them straight to an inner `<svg>` with the icon's native `viewBox` to scale the shape to fit. The cell's resolved color (from the chart's `c` scale, the series, or `fill`) is set via CSS `color` on the wrapping element, so any `fill="currentColor"` (or stroke) inside the snippet inherits it.

```svelte
<Waffle>
{#snippet symbol({ width, height })}
<svg {width} {height} viewBox="0 0 64 64">
<path fill="currentColor" d={iconPath} />
</svg>
{/snippet}
</Waffle>
```

:example{ name="custom-symbol" }

:::note
`symbol` is supported on the SVG layer only — canvas falls back to the default rect.
:::

## Stacking

Stacked waffles share a cell grid across the stack so cells line up across series — the cleanest way to compare composition. Configure `series` + `seriesLayout="stack"` and render one `<Waffle seriesKey={...}>` per visible series. Toggling the legend hides a series and restacks the remaining segments to the baseline.

:example{ name="penguins" }

For continuous variables, bin into discrete intervals before stacking — here athletes are grouped into 10-kg weight cohorts and stacked by sex. Setting `unit={10}` (one cell per ten athletes) keeps the cell count readable when each bin holds hundreds of values:

:example{ name="olympians-weight-by-sex" }

If your data is already in wide format (one row per category with one column per series), pass it straight to `<Chart>` — no `pivotWider` step needed:

:example{ name="stacked" }

A stacked horizontal waffle works equally well for showing a single composition broken into named segments — here, the days of each month across a year:

:example{ name="months" }
7 changes: 4 additions & 3 deletions docs/src/content/guides/structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,11 +237,10 @@ A stacked bar chart comparison:
x="category"
xScale={scaleBand()}
xDomain={data.map((d) => d.category)}
y={['q1', 'q2']}
yBaseline={0}
series={[
{ key: 'q1', color: 'steelblue' },
{ key: 'q2', color: 'coral' }
{ key: 'q1', value: 'q1', color: 'steelblue' },
{ key: 'q2', value: 'q2', color: 'coral' }
]}
seriesLayout="stack"
tooltipContext={{ mode: 'band' }}
Expand All @@ -256,6 +255,8 @@ A stacked bar chart comparison:
</Chart>
```

> **Note:** when stacking against shared chart data (no per-series `data`), each series needs an explicit `value` accessor so the stack layout knows which field to read for that series. With `seriesLayout="stack"`, `<Chart>`'s internal stack value function falls through `s.value` → chart-level `y` → `s.key`; setting `y={['q1', 'q2']}` (an array) on `<Chart>` here would shadow the `s.key` fallback and feed the stack the whole `[d.q1, d.q2]` tuple per series, producing zero-height bars. `<BarChart>` doesn't hit this because it doesn't forward an array `y` to `<Chart>` when you pass `series=[…]`.

### Escape hatches

Simplified charts accept the same snippets as `<Chart>`. When you need to customize beyond what props offer, use a snippet to override just that part:
Expand Down
101 changes: 101 additions & 0 deletions docs/src/examples/components/Pattern/rects.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<script lang="ts">
import { Chart, Layer, Pattern, Rect } from 'layerchart';
</script>

<Chart height={300}>
<Layer>
<!-- Solid filled tile (default inset 0) -->
<Pattern size={8} rects>
{#snippet children({ pattern })}
<Rect
x={120 * 0}
y={0}
width={100}
height={300}
rx={8}
fill={pattern}
stroke="var(--color-surface-content)"
/>
{/snippet}
</Pattern>

<!-- Cell grid: rects with inset for gaps -->
<Pattern size={12} rects={{ inset: 1 }}>
{#snippet children({ pattern })}
<Rect
x={120 * 1}
y={0}
width={100}
height={300}
rx={8}
fill={pattern}
stroke="var(--color-surface-content)"
/>
{/snippet}
</Pattern>

<!-- Rounded cells -->
<Pattern size={14} rects={{ inset: 2, rx: 2, ry: 2 }}>
{#snippet children({ pattern })}
<Rect
x={120 * 2}
y={0}
width={100}
height={300}
rx={8}
fill={pattern}
stroke="var(--color-surface-content)"
/>
{/snippet}
</Pattern>

<!-- Circular cells (rx="100%") -->
<Pattern size={14} rects={{ inset: 2, rx: '100%', ry: '100%' }}>
{#snippet children({ pattern })}
<Rect
x={120 * 3}
y={0}
width={100}
height={300}
rx={8}
fill={pattern}
stroke="var(--color-surface-content)"
/>
{/snippet}
</Pattern>

<!-- Custom color + opacity -->
<Pattern size={14} rects={{ inset: 2, rx: 2, color: 'var(--color-primary)', opacity: 0.6 }}>
{#snippet children({ pattern })}
<Rect
x={120 * 4}
y={0}
width={100}
height={300}
rx={8}
fill={pattern}
stroke="var(--color-surface-content)"
/>
{/snippet}
</Pattern>

<!-- Background + foreground rect (checkerboard-ish) -->
<Pattern
size={14}
background="var(--color-surface-200)"
rects={{ inset: 2, rx: 2, color: 'var(--color-info)' }}
>
{#snippet children({ pattern })}
<Rect
x={120 * 5}
y={0}
width={100}
height={300}
rx={8}
fill={pattern}
stroke="var(--color-surface-content)"
/>
{/snippet}
</Pattern>
</Layer>
</Chart>
25 changes: 25 additions & 0 deletions docs/src/examples/components/Waffle/auto-multiple.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<script lang="ts">
import { Chart, Waffle } from 'layerchart';
import { RangeField } from 'svelte-ux';

let apples = $state(500);
const data = $derived([{ label: 'apples', count: apples }]);
</script>

<div class="mb-4 screenshot-hidden">
<RangeField label="Apples" bind:value={apples} min={10} max={1000} dense />
</div>

<Chart
{data}
x="count"
xDomain={[0, null]}
xNice
y="label"
padding={{ left: 48, bottom: 24, right: 8 }}
height={180}
>
{#snippet marks()}
<Waffle axis="x" fill="var(--color-primary)" />
{/snippet}
</Chart>
63 changes: 63 additions & 0 deletions docs/src/examples/components/Waffle/band-padding.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<script lang="ts">
import { Chart, Tooltip, Waffle } from 'layerchart';
import { Field, RangeField, ToggleGroup, ToggleOption } from 'svelte-ux';

let bandPadding = $state(0.2);
let unit = $state(5);

const data = [
{ fruit: 'Apple', count: 212 },
{ fruit: 'Banana', count: 207 },
{ fruit: 'Cherry', count: 315 },
{ fruit: 'Date', count: 11 }
];
export { data };
</script>

<div class="grid grid-cols-[auto_1fr] gap-4 mb-4 screenshot-hidden">
<Field label="Cells per unit" dense>
<ToggleGroup bind:value={unit} variant="outline" size="sm">
{#each [1, 2, 5, 10, 25, 50, 100] as opt (opt)}
<ToggleOption value={opt}>{opt}</ToggleOption>
{/each}
</ToggleGroup>
</Field>

<RangeField
label="Band padding"
bind:value={bandPadding}
min={0}
max={0.8}
step={0.05}
format="decimal"
/>
</div>

<Chart
{data}
x="fruit"
{bandPadding}
y="count"
yDomain={[0, null]}
yNice
padding={{ left: 36, bottom: 24, top: 8, right: 8 }}
height={400}
rule
grid
clip
>
{#snippet marks()}
<Waffle fill="var(--color-info)" {unit} round tooltip />
{/snippet}

{#snippet tooltip()}
<Tooltip.Root>
{#snippet children({ data })}
<Tooltip.Header>{data.fruit}</Tooltip.Header>
<Tooltip.List>
<Tooltip.Item label="Count" value={data.count} format="integer" />
</Tooltip.List>
{/snippet}
</Tooltip.Root>
{/snippet}
</Chart>
39 changes: 39 additions & 0 deletions docs/src/examples/components/Waffle/basic.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<script lang="ts">
import { Chart, Tooltip, Waffle } from 'layerchart';

const data = [
{ fruit: 'Apple', count: 212 },
{ fruit: 'Banana', count: 207 },
{ fruit: 'Cherry', count: 315 },
{ fruit: 'Date', count: 11 }
];
export { data };
</script>

<Chart
{data}
x="fruit"
bandPadding={0.2}
y="count"
yDomain={[0, null]}
yNice
padding={{ left: 36, bottom: 24, top: 8, right: 8 }}
height={400}
rule
grid
>
{#snippet marks()}
<Waffle fill="var(--color-primary)" tooltip />
{/snippet}

{#snippet tooltip()}
<Tooltip.Root>
{#snippet children({ data })}
<Tooltip.Header>{data.fruit}</Tooltip.Header>
<Tooltip.List>
<Tooltip.Item label="Count" value={data.count} format="integer" />
</Tooltip.List>
{/snippet}
</Tooltip.Root>
{/snippet}
</Chart>
Loading
Loading