
## Monospace Bar Chart

### Monospace Bar Chart

`$lib/components/evilcharts/blocks/monospace-bar-chart.svelte`

```svelte
<script lang="ts">
	/**
	 * Monospace bar chart block.
	 *
	 * Built on `ChartContainer` plus LayerChart primitives rather than `EvilBarChart`, exactly as
	 * the reference builds it on `ChartContainer` plus raw Recharts — a block is a self-contained
	 * composition, not a configuration of the chart component.
	 */
	import { Axis, Chart, Svg, type ChartState } from 'layerchart';
	import {
		ChartContainer,
		thinAxisTicks,
		type ChartConfig
	} from '$lib/components/evilcharts/ui/layerchart-chart/index.js';
	import MonospaceBar from './b-monospace-bar-chart-bar.svelte';

	const chartData = [
		{ month: 'January', desktop: 342 },
		{ month: 'February', desktop: 876 },
		{ month: 'March', desktop: 512 },
		{ month: 'April', desktop: 629 },
		{ month: 'May', desktop: 458 },
		{ month: 'June', desktop: 781 },
		{ month: 'July', desktop: 394 },
		{ month: 'August', desktop: 925 },
		{ month: 'September', desktop: 647 },
		{ month: 'October', desktop: 532 },
		{ month: 'November', desktop: 803 },
		{ month: 'December', desktop: 271 },
		{ month: 'January', desktop: 342 },
		{ month: 'February', desktop: 876 },
		{ month: 'March', desktop: 512 },
		{ month: 'April', desktop: 629 },
		{ month: 'May', desktop: 458 },
		{ month: 'June', desktop: 781 },
		{ month: 'July', desktop: 394 },
		{ month: 'August', desktop: 925 },
		{ month: 'September', desktop: 647 },
		{ month: 'October', desktop: 532 },
		{ month: 'November', desktop: 803 },
		{ month: 'December', desktop: 271 }
	];

	const chartConfig = {
		desktop: {
			label: 'Desktop',
			colors: {
				light: ['#18181b'],
				dark: ['#fafafa']
			}
		}
	} satisfies ChartConfig;

	const seriesKeys = Object.keys(chartConfig);

	/**
	 * The reference's data repeats the twelve months twice, so a category *name* is not unique —
	 * the band scale is keyed by row index and the axis formats that index back to a month.
	 */
	const INDEX_KEY = '__monospaceIndex';
	const rows = chartData.map((row, index) => ({ ...row, [INDEX_KEY]: index }));

	/** Twenty-four categories do not all fit, so the axis thins them as Recharts does. */
	const formatMonth = (value: unknown) => String(rows[Number(value)]?.month ?? '').slice(0, 3);

	/** LayerChart's chart state, read for the row the pointer is over. */
	let layerContext = $state<ChartState<Record<string, unknown>> | undefined>(undefined);
	const activeRow = $derived(layerContext?.tooltip?.data as Record<string, unknown> | undefined);
</script>

<div class="flex h-full flex-col p-4">
	<div class="flex flex-row justify-between">
		<div class="flex flex-row">
			<div class="flex flex-col gap-2">
				<span class="font-mono text-xs text-muted-foreground">[$] Total Sales</span>
				<span class="font-mono text-3xl text-primary">
					<span class="text-xl font-normal text-muted-foreground">$</span>
					<span class="tracking-tighter">14,340</span>
				</span>
			</div>
			<hr class="mx-4 h-full border-l border-dashed" />
			<div class="flex flex-col gap-2">
				<span class="font-mono text-xs text-muted-foreground">[⬆] Top Month</span>
				<span class="font-mono text-3xl text-primary">
					<span class="tracking-tighter">June</span>
				</span>
			</div>
		</div>
		<div class="flex flex-col justify-end gap-1">
			<span class="font-mono text-[10px] text-muted-foreground">
				// X-AXIS: <span class="text-primary">MONTHS</span>
			</span>
			<span class="font-mono text-[10px] text-muted-foreground">
				// Y-AXIS: <span class="text-primary">SALES</span>
			</span>
		</div>
	</div>
	<hr class="my-4 border-t border-dashed" />
	<ChartContainer config={chartConfig}>
		<Chart
			bind:context={layerContext}
			data={rows}
			x={INDEX_KEY}
			series={seriesKeys.map((key) => ({ key, value: key }))}
			seriesLayout="overlap"
			bandPadding={0}
			yBaseline={0}
			yNice
			padding={{ top: 5, right: 5, bottom: 35, left: 5 }}
			tooltipContext={{ mode: 'band' }}
			class="h-full w-full"
		>
			<Svg>
				{#each seriesKeys as key (key)}
					<MonospaceBar dataKey={key} {rows} {activeRow} fill={`var(--color-${key}-0)`} />
				{/each}
				<Axis
					placement="bottom"
					rule={false}
					tickMarks={false}
					tickLength={10}
					ticks={thinAxisTicks({ format: formatMonth })}
					format={formatMonth}
				/>
			</Svg>
		</Chart>
	</ChartContainer>
</div>
```

`$lib/components/evilcharts/blocks/monospace-bar-chart-bar.svelte`

```svelte
<script lang="ts">
	/**
	 * One monospace bar per row.
	 *
	 * Resting, a bar is a thin line (`COLLAPSED_SCALE`); hovered, it springs out to full width and
	 * its value floats in above it. The reference gets `isActive` from Recharts' `activeBar`, which
	 * here is the row LayerChart's tooltip is pointing at.
	 *
	 * `transform-box: fill-box` with motion's default origin collapses each bar onto its **own**
	 * centre. The reference also computes an absolute `transformOrigin`, but motion overrides it —
	 * measured on the reference, every bar resolves to `width/2 height/2`. Passing the absolute
	 * value here instead pushed the origin outside the 19px rect and the bars vanished.
	 *
	 * No `<AnimatePresence>`: this port's version renders a wrapper `<div>`, which is invalid inside
	 * an `<svg>` and left every rect with a 0x0 box. React's renders no DOM at all. It is not needed
	 * anyway — the rect never unmounts, and the value label animates its opacity instead of
	 * mounting and unmounting, which is what the reference's enter/exit pair looks like.
	 */
	import { Bar as LayerBar, getChartContext } from 'layerchart';
	import { animate, useReducedMotion } from '@humanspeak/svelte-motion';
	import { getBarPositions } from '$lib/components/evilcharts/ui/layerchart-chart/index.js';

	let {
		dataKey,
		rows,
		activeRow,
		fill
	}: {
		dataKey: string;
		rows: Record<string, unknown>[];
		activeRow: Record<string, unknown> | undefined;
		fill: string;
	} = $props();

	const layer = getChartContext();
	const shouldReduceMotion = useReducedMotion();

	// Scale factor: collapsed = thin line, expanded = full width
	const COLLAPSED_SCALE = 0.1;

	function animateScaleX(target: number, reduced: boolean) {
		return (node: SVGRectElement) => {
			node.style.transformBox = 'fill-box';
			node.style.transformOrigin = 'center';

			if (reduced) {
				node.style.transform = `scaleX(${target})`;
				return undefined;
			}

			const controls = animate(
				node,
				{ scaleX: target },
				{
					type: 'spring',
					stiffness: 200,
					damping: 25
				}
			);

			return () => controls.stop();
		};
	}

	/**
	 * Geometry per row, read off the chart scales — the reference gets it from Recharts' shape
	 * props. Resolved in one derivation rather than with declaration tags.
	 */
	const bars = $derived.by(() => {
		const band = layer.xScale.bandwidth?.() ?? 0;
		// One bar per category, sized by Recharts' own arithmetic — a 24.5px band yields a 19px bar
		// with `barCategoryGap="10%"`, which is what the reference renders.
		const slot = getBarPositions({ bandSize: band, count: 1 })[0];

		return rows.map((row, index) => {
			const value = Number(row[dataKey] ?? 0);
			const x = (Number(layer.xScale(index as never)) || 0) + (slot?.offset ?? 0);
			const y = Number(layer.yScale(value));
			const height = Math.abs(Number(layer.yScale(0)) - y);

			return {
				row,
				index,
				value,
				x,
				y,
				width: slot?.size ?? band,
				height,
				centerX: x + (slot?.size ?? band) / 2,
				isActive: activeRow === row
			};
		});
	});
</script>

{#each bars as bar (bar.index)}
	<!-- Transparent twin keeps the whole column hoverable while the painted bar is collapsed. -->
	<LayerBar data={bar.row} seriesKey={dataKey} fill="transparent" motion="none" tooltip />

	<rect
		{@attach animateScaleX(bar.isActive ? 1 : COLLAPSED_SCALE, shouldReduceMotion.current)}
		class="origin-center"
		x={bar.x}
		y={bar.y}
		width={bar.width}
		height={bar.height}
		{fill}
	/>
	<text
		class="pointer-events-none font-mono transition-[opacity,transform,filter] duration-200 motion-reduce:transition-none"
		style:opacity={bar.isActive ? 1 : 0}
		style:transform={`translateY(${bar.isActive ? 0 : -10}px)`}
		style:filter={bar.isActive ? 'blur(0px)' : 'blur(3px)'}
		x={bar.centerX}
		y={bar.y - 5}
		text-anchor="middle"
		{fill}
	>
		{bar.value}
	</text>
{/each}
```
### npm

```bash
npx shadcn-svelte@latest add @evilcharts/monospace-bar-chart
```

### yarn

```bash
yarn dlx shadcn-svelte@latest add @evilcharts/monospace-bar-chart
```

### bun

```bash
bunx --bun shadcn-svelte@latest add @evilcharts/monospace-bar-chart
```

### pnpm

```bash
pnpm dlx shadcn-svelte@latest add @evilcharts/monospace-bar-chart
```

## Hover Trace Bar Chart

### Hover Trace Bar Chart

`$lib/components/evilcharts/blocks/hover-trace-bar-chart.svelte`

```svelte
<script lang="ts">
	/**
	 * Hover-trace bar chart block.
	 *
	 * Hovering a column dims the rest, springs a dashed reference line to that column's value, and
	 * rolls the headline figure to it. Built on `ChartContainer` plus LayerChart primitives, as the
	 * reference builds it on `ChartContainer` plus raw Recharts.
	 */
	import { Axis, Chart, Svg, type ChartState } from 'layerchart';
	import { useMotionValueEvent, useSpring } from '@humanspeak/svelte-motion';
	import NumberFlow from '@number-flow/svelte';
	import {
		ChartContainer,
		thinAxisTicks,
		type ChartConfig
	} from '$lib/components/evilcharts/ui/layerchart-chart/index.js';
	import HoverTraceBar from './b-hover-trace-bar-chart-bar.svelte';
	import HoverTraceTrace from './b-hover-trace-bar-chart-trace.svelte';

	const CHART_MARGIN = 38;

	const chartData = [
		{ month: 'January', desktop: 342 },
		{ month: 'February', desktop: 676 },
		{ month: 'March', desktop: 512 },
		{ month: 'April', desktop: 629 },
		{ month: 'May', desktop: 458 },
		{ month: 'June', desktop: 781 },
		{ month: 'July', desktop: 394 },
		{ month: 'August', desktop: 924 },
		{ month: 'September', desktop: 647 },
		{ month: 'October', desktop: 532 },
		{ month: 'November', desktop: 803 },
		{ month: 'December', desktop: 271 },
		{ month: 'January', desktop: 342 },
		{ month: 'February', desktop: 876 },
		{ month: 'March', desktop: 512 },
		{ month: 'April', desktop: 629 }
	];

	const chartConfig = {
		desktop: {
			label: 'Desktop',
			colors: {
				light: ['#18181b'],
				dark: ['#fafafa']
			}
		}
	} satisfies ChartConfig;

	/** The band is keyed by row index — the months repeat, so a name is not unique. */
	const INDEX_KEY = '__traceIndex';
	const rows = chartData.map((row, index) => ({ ...row, [INDEX_KEY]: index }));
	const formatMonth = (value: unknown) => String(rows[Number(value)]?.month ?? '').slice(0, 3);

	const maxData = chartData.reduce(
		(max, item, index) =>
			item.desktop > max.value ? { index, month: item.month, value: item.desktop } : max,
		{ index: 0, month: chartData[0].month, value: chartData[0].desktop }
	);

	/** LayerChart's chart state, read for the column under the pointer. */
	let layerContext = $state<ChartState<Record<string, unknown>> | undefined>(undefined);
	const activeRow = $derived(layerContext?.tooltip?.data as Record<string, unknown> | undefined);
	const activeIndex = $derived(activeRow ? (activeRow[INDEX_KEY] as number) : null);

	const selectedData = $derived(
		activeIndex != null && chartData[activeIndex]
			? {
					index: activeIndex,
					month: chartData[activeIndex].month,
					value: chartData[activeIndex].desktop
				}
			: maxData
	);

	/** The reference's spring, so the trace line eases between columns. */
	const valueSpring = useSpring(maxData.value, { stiffness: 110, damping: 20 });
	let springValue = $state(maxData.value);

	useMotionValueEvent(valueSpring, 'change', (latest: number) => {
		springValue = Math.round(latest);
	});

	$effect(() => {
		valueSpring.set(selectedData.value);
	});
</script>

<div class="flex h-full flex-col p-4">
	<div class="mb-4 flex items-end justify-between">
		<div class="space-y-1">
			<p class="font-mono text-xs text-muted-foreground">[desktop] Value</p>
			<p class="font-mono text-3xl tracking-tighter text-primary">
				<NumberFlow
					value={selectedData.value}
					format={{ style: 'currency', currency: 'USD', currencyDisplay: 'narrowSymbol' }}
				/>
			</p>
		</div>

		<div class="space-y-1 text-right">
			<p class="font-mono text-[10px] text-muted-foreground">[month]</p>
			<p class="font-mono text-xs text-primary">{selectedData.month}</p>
		</div>
	</div>

	<ChartContainer config={chartConfig}>
		<Chart
			bind:context={layerContext}
			data={rows}
			x={INDEX_KEY}
			series={[{ key: 'desktop', value: 'desktop' }]}
			seriesLayout="overlap"
			bandPadding={0}
			yBaseline={0}
			yNice
			padding={{ top: 5, right: 5, bottom: 35, left: CHART_MARGIN }}
			tooltipContext={{ mode: 'band' }}
			class="h-full w-full"
		>
			<Svg>
				<HoverTraceBar
					dataKey="desktop"
					{rows}
					{activeRow}
					highlightedIndex={selectedData.index}
					fill="var(--color-desktop-0)"
				/>
				<HoverTraceTrace {springValue} labelValue={selectedData.value} chartMargin={CHART_MARGIN} />
				<Axis
					placement="bottom"
					rule={false}
					tickMarks={false}
					tickLength={10}
					ticks={thinAxisTicks({ format: formatMonth })}
					format={formatMonth}
				/>
			</Svg>
		</Chart>
	</ChartContainer>
</div>
```

`$lib/components/evilcharts/blocks/hover-trace-bar-chart-bar.svelte`

```svelte
<script lang="ts">
	/**
	 * One hover-trace column: dimmed unless it is the highlighted one, and outlined while hovered.
	 *
	 * The reference reads `isActive` from Recharts' `activeBar` and `highlightedIndex` from the
	 * chart's own state; both come from the chart context here.
	 */
	import { Bar as LayerBar, getChartContext } from 'layerchart';
	import { getBarPositions } from '$lib/components/evilcharts/ui/layerchart-chart/index.js';

	let {
		dataKey,
		rows,
		activeRow,
		highlightedIndex,
		fill
	}: {
		dataKey: string;
		rows: Record<string, unknown>[];
		activeRow: Record<string, unknown> | undefined;
		highlightedIndex: number;
		fill: string;
	} = $props();

	const layer = getChartContext();

	/**
	 * Column width and offset from Recharts' own arithmetic — its default `barCategoryGap="10%"`
	 * leaves a gap either side, which `bandPadding={0}` alone would not.
	 */
	const insets = $derived.by(() => {
		const band = layer.xScale.bandwidth?.() ?? 0;
		const slot = getBarPositions({ bandSize: band, count: 1 })[0];
		if (!slot) return {};
		return { left: slot.offset, right: Math.max(0, band - slot.offset - slot.size) };
	});

	const bars = $derived(
		rows.map((row, index) => {
			const isActive = activeRow === row;
			return {
				row,
				index,
				isActive,
				// Everything but the highlighted (or hovered) column recedes.
				fillOpacity: isActive || index === highlightedIndex ? 1 : 0.2
			};
		})
	);
</script>

{#each bars as bar (bar.index)}
	<!-- Transparent twin: a full-height hit area, as the reference's first `<Rectangle>` is. -->
	<LayerBar
		data={bar.row}
		seriesKey={dataKey}
		fill="transparent"
		motion="none"
		tooltip
		style="pointer-events: all"
	/>
	<LayerBar
		data={bar.row}
		seriesKey={dataKey}
		radius={4}
		rounded="all"
		{insets}
		{fill}
		fillOpacity={bar.fillOpacity}
		stroke={bar.isActive ? 'var(--foreground)' : undefined}
		strokeOpacity={bar.isActive ? 0.35 : undefined}
		strokeWidth={bar.isActive ? 1 : undefined}
		class="transition-opacity duration-200"
		motion="none"
	/>
{/each}
```

`$lib/components/evilcharts/blocks/hover-trace-bar-chart-trace.svelte`

```svelte
<script lang="ts">
	/**
	 * The dashed reference line at the tracked value, with its pill label and end dot.
	 *
	 * The reference draws it with Recharts' `<ReferenceLine>` plus a custom label; the y comes from
	 * a spring, so the line eases between values as the pointer moves between columns.
	 */
	import { getChartContext } from 'layerchart';

	let {
		springValue,
		labelValue,
		chartMargin
	}: { springValue: number; labelValue: number; chartMargin: number } = $props();

	const layer = getChartContext();

	const y = $derived(Number(layer.yScale(springValue)));
	const formatted = $derived(labelValue.toLocaleString());
	/** The reference sizes the pill from the label's length. */
	const width = $derived(formatted.length * 8 + 12);
	const xEnd = $derived(Math.max(...layer.xRange));
</script>

<line
	x1={0}
	y1={y}
	x2={xEnd}
	y2={y}
	stroke="var(--foreground)"
	stroke-dasharray="3 3"
	class="pointer-events-none"
/>
<rect
	x={-chartMargin}
	y={y - 9}
	{width}
	height={18}
	fill="var(--foreground)"
	rx={4}
	class="pointer-events-none"
/>
<text
	class="pointer-events-none font-mono text-[11px]"
	font-weight={600}
	x={-chartMargin + 7}
	y={y + 4}
	fill="var(--background)"
>
	{formatted}
</text>
<ellipse cx={xEnd} cy={y} rx={3} ry={3} fill="var(--foreground)" class="pointer-events-none" />
```
### npm

```bash
npx shadcn-svelte@latest add @evilcharts/hover-trace-bar-chart
```

### yarn

```bash
yarn dlx shadcn-svelte@latest add @evilcharts/hover-trace-bar-chart
```

### bun

```bash
bunx --bun shadcn-svelte@latest add @evilcharts/hover-trace-bar-chart
```

### pnpm

```bash
pnpm dlx shadcn-svelte@latest add @evilcharts/hover-trace-bar-chart
```

## Grid Bar Chart

### Grid Bar Chart

`$lib/components/evilcharts/blocks/grid-bar-chart.svelte`

```svelte
<script lang="ts">
	/**
	 * Grid bar chart block.
	 *
	 * Each column is a stack of 10x10 squares — ghost squares for the full plot height, solid ones
	 * up to the value. Built on `ChartContainer` plus LayerChart primitives, as the reference builds
	 * it on `ChartContainer` plus raw Recharts.
	 */
	import { Axis, Chart, Svg } from 'layerchart';
	import {
		ChartContainer,
		thinAxisTicks,
		type ChartConfig
	} from '$lib/components/evilcharts/ui/layerchart-chart/index.js';
	import GridBar from './b-grid-bar-chart-bar.svelte';

	const chartData = [
		{ month: 'January', desktop: 186 },
		{ month: 'February', desktop: 305 },
		{ month: 'March', desktop: 237 },
		{ month: 'April', desktop: 273 },
		{ month: 'May', desktop: 209 },
		{ month: 'June', desktop: 346 },
		{ month: 'July', desktop: 181 },
		{ month: 'August', desktop: 392 },
		{ month: 'September', desktop: 298 },
		{ month: 'October', desktop: 215 },
		{ month: 'November', desktop: 327 },
		{ month: 'December', desktop: 162 }
	];

	const chartConfig = {
		desktop: {
			label: 'Desktop',
			colors: {
				light: ['#18181b'],
				dark: ['#fafafa']
			}
		}
	} satisfies ChartConfig;

	const seriesKeys = Object.keys(chartConfig);

	const total = chartData.reduce((sum, item) => sum + item.desktop, 0);
	const maxData = chartData.reduce(
		(max, item, index) =>
			item.desktop > max.value ? { index, month: item.month, value: item.desktop } : max,
		{ index: 0, month: chartData[0].month, value: chartData[0].desktop }
	);

	/** The band is keyed by row index so the axis can format it back to a month. */
	const INDEX_KEY = '__gridIndex';
	const rows = chartData.map((row, index) => ({ ...row, [INDEX_KEY]: index }));
	const formatMonth = (value: unknown) => String(rows[Number(value)]?.month ?? '').slice(0, 3);
</script>

<div class="flex h-full flex-col p-4">
	<div class="flex flex-row justify-between">
		<div class="flex flex-row">
			<div class="flex flex-col gap-2">
				<span class="font-mono text-xs text-muted-foreground">[Σ] Total</span>
				<span class="font-mono text-3xl tracking-tighter text-primary">
					{total.toLocaleString()}
				</span>
			</div>
			<hr class="mx-4 h-full border-l border-dashed" />
			<div class="flex flex-col gap-2">
				<span class="font-mono text-xs text-muted-foreground">[⬆] Peak</span>
				<span class="font-mono text-3xl tracking-tighter text-primary">
					{maxData.month.slice(0, 3)}
				</span>
			</div>
		</div>
		<div class="flex flex-col justify-end gap-1">
			<span class="font-mono text-[10px] text-muted-foreground">
				// CELL: <span class="text-primary">10x10px</span>
			</span>
			<span class="font-mono text-[10px] text-muted-foreground">
				// TYPE: <span class="text-primary">GRID</span>
			</span>
		</div>
	</div>
	<hr class="my-4 border-t border-dashed" />
	<ChartContainer config={chartConfig}>
		<Chart
			data={rows}
			x={INDEX_KEY}
			series={seriesKeys.map((key) => ({ key, value: key }))}
			seriesLayout="overlap"
			bandPadding={0}
			yBaseline={0}
			yNice
			padding={{ top: 5, right: 5, bottom: 35, left: 5 }}
			tooltipContext={{ mode: 'band' }}
			class="h-full w-full"
		>
			<Svg>
				{#each seriesKeys as key (key)}
					<GridBar dataKey={key} {rows} fill={`var(--color-${key}-0)`} />
				{/each}
				<Axis
					placement="bottom"
					rule={false}
					tickMarks={false}
					tickLength={10}
					ticks={thinAxisTicks({ format: formatMonth })}
					format={formatMonth}
				/>
			</Svg>
		</Chart>
	</ChartContainer>
</div>
```

`$lib/components/evilcharts/blocks/grid-bar-chart-bar.svelte`

```svelte
<script lang="ts">
	/**
	 * One grid column: ghost squares filling the plot height, and solid squares stacked up to the
	 * value.
	 *
	 * The reference draws both from Recharts' shape props — the background gets `y` = the plot top
	 * and `height` = the full plot, so `y + height` is the shared baseline the data squares also
	 * stack from. Here both come off the chart scales instead, with the same arithmetic.
	 */
	import { Bar as LayerBar, getChartContext } from 'layerchart';
	import { getBarPositions } from '$lib/components/evilcharts/ui/layerchart-chart/index.js';

	let { dataKey, rows, fill }: { dataKey: string; rows: Record<string, unknown>[]; fill: string } =
		$props();

	const layer = getChartContext();

	/**
	 * Recharts hands a `background` shape its *own* default fill rather than the bar's, which is why
	 * the reference's ghost squares are grey and not the series colour — measured as `#eee` on the
	 * running reference. The `dark:opacity-[0.1]` class then knocks them back on a dark surface.
	 */
	const GHOST_FILL = '#eee';

	const SQUARE_SIZE = 10;
	const GAP = 2;
	const CELL_SIZE = SQUARE_SIZE + GAP;

	/** Squares for one column, stacked upward from `bottomY`. */
	function squaresOf(squareX: number, squareSize: number, bottomY: number, count: number) {
		return Array.from({ length: count }, (_, index) => ({
			index,
			x: squareX,
			y: bottomY - (index + 1) * CELL_SIZE + GAP,
			size: squareSize
		}));
	}

	const columns = $derived.by(() => {
		const band = layer.xScale.bandwidth?.() ?? 0;
		const slot = getBarPositions({ bandSize: band, count: 1 })[0];
		const width = slot?.size ?? band;
		const baseline = Number(layer.yScale(0));
		const plotTop = Math.min(...layer.yRange);
		const plotHeight = Math.abs(baseline - plotTop);

		return rows.map((row, index) => {
			const value = Number(row[dataKey] ?? 0);
			const barTop = Number(layer.yScale(value));
			const barHeight = Math.abs(baseline - barTop);

			const squareSize = Math.min(SQUARE_SIZE, Math.max(2, width - 2));
			const squareX =
				(Number(layer.xScale(index as never)) || 0) +
				(slot?.offset ?? 0) +
				Math.floor((width - squareSize) / 2);

			return {
				row,
				index,
				// The ghost column always fills the plot; the data column stops at the value.
				ghost:
					plotHeight > 0
						? squaresOf(squareX, squareSize, baseline, Math.floor(plotHeight / CELL_SIZE))
						: [],
				solid:
					barHeight > 0
						? squaresOf(
								squareX,
								squareSize,
								baseline,
								Math.max(1, Math.floor(barHeight / CELL_SIZE))
							)
						: []
			};
		});
	});
</script>

{#each columns as column (column.index)}
	<!-- Transparent twin keeps the column hoverable, as Recharts' own bar rect does. -->
	<LayerBar data={column.row} seriesKey={dataKey} fill="transparent" motion="none" tooltip />

	{#each column.ghost as square (square.index)}
		<rect
			class="dark:opacity-[0.1]"
			x={square.x}
			y={square.y}
			width={square.size}
			height={square.size}
			fill={GHOST_FILL}
		/>
	{/each}
	{#each column.solid as square (square.index)}
		<rect x={square.x} y={square.y} width={square.size} height={square.size} {fill} />
	{/each}
{/each}
```
### npm

```bash
npx shadcn-svelte@latest add @evilcharts/grid-bar-chart
```

### yarn

```bash
yarn dlx shadcn-svelte@latest add @evilcharts/grid-bar-chart
```

### bun

```bash
bunx --bun shadcn-svelte@latest add @evilcharts/grid-bar-chart
```

### pnpm

```bash
pnpm dlx shadcn-svelte@latest add @evilcharts/grid-bar-chart
```

## Isometric Bar Chart

### Isometric Bar Chart

`$lib/components/evilcharts/blocks/isometric-bar-chart.svelte`

```svelte
<script lang="ts">
	/**
	 * Isometric bar chart block.
	 *
	 * Each column is drawn as a front face plus two bevels, hatched over, with the tallest picked out
	 * in green. Built on `ChartContainer` plus LayerChart primitives, as the reference builds it on
	 * `ChartContainer` plus raw Recharts.
	 */
	import { Axis, Chart, Svg } from 'layerchart';
	import {
		ChartContainer,
		thinAxisTicks,
		type ChartConfig
	} from '$lib/components/evilcharts/ui/layerchart-chart/index.js';
	import { ChartTooltip, ChartTooltipContent } from '$lib/components/evilcharts/ui/layerchart-tooltip/index.js';
	import IsoBar from './b-isometric-bar-chart-bar.svelte';
	import IsoBarDefs from './b-isometric-bar-chart-defs.svelte';

	const chartData = [
		{ month: 'January', revenue: 28 },
		{ month: 'February', revenue: 34 },
		{ month: 'March', revenue: 22 },
		{ month: 'April', revenue: 41 },
		{ month: 'May', revenue: 47 },
		{ month: 'June', revenue: 31 },
		{ month: 'July', revenue: 38 }
	];

	const chartConfig = {
		revenue: {
			label: 'Revenue',
			colors: {
				light: ['#18181b'],
				dark: ['#fafafa']
			}
		}
	} satisfies ChartConfig;

	const BEVEL_OPACITY = 0.55;
	const HIGHLIGHT_COLOR = '#22c55e';
	const HIGHLIGHT_COLOR_DARK = '#15803d';

	/** Namespaces this instance's `<defs>` ids so several charts can coexist on a page. */
	const idPrefix = $props.id();

	const maxValue = chartData.reduce((m, d) => (d.revenue > m ? d.revenue : m), 0);
	const total = chartData.reduce((sum, d) => sum + d.revenue, 0);
	const peak = chartData.find((d) => d.revenue === maxValue)!;

	/** The band is keyed by row index so the axis can format it back to a month. */
	const INDEX_KEY = '__isoIndex';
	const rows = chartData.map((row, index) => ({ ...row, [INDEX_KEY]: index }));
	const formatMonth = (value: unknown) => String(rows[Number(value)]?.month ?? '').slice(0, 3);
</script>

<div class="flex h-full w-full flex-col p-4">
	<div class="flex flex-row justify-between">
		<div class="flex flex-row">
			<div class="flex flex-col gap-2">
				<span class="font-mono text-xs text-muted-foreground">[$] Total</span>
				<span class="font-mono text-3xl text-primary">
					<span class="text-xl font-normal text-muted-foreground">$</span>
					<span class="tracking-tighter">{total}K</span>
				</span>
			</div>
			<hr class="mx-4 h-full border-l border-dashed" />
			<div class="flex flex-col gap-2">
				<span class="font-mono text-xs text-muted-foreground">[⬆] Peak</span>
				<span class="font-mono text-3xl tracking-tighter text-primary">
					{peak.month.slice(0, 3)}
				</span>
			</div>
		</div>
		<div class="flex flex-col justify-end gap-1">
			<span class="font-mono text-[10px] text-muted-foreground">
				// PROJECTION: <span class="text-primary">ISOMETRIC</span>
			</span>
			<span class="font-mono text-[10px] text-muted-foreground">
				// HIGHLIGHT: <span class="text-primary">MAX</span>
			</span>
		</div>
	</div>
	<hr class="my-4 border-t border-dashed" />
	<ChartContainer config={chartConfig}>
		<!--
			`margin={{ top: 30, right: 30 }}` in the reference leaves room for the bevels, which stick
			out above and to the right of every column. `yNice` is dropped in favour of the reference's
			`domain={[0, 'dataMax + 10']}`.
		-->
		<Chart
			data={rows}
			x={INDEX_KEY}
			series={[{ key: 'revenue', value: 'revenue' }]}
			seriesLayout="overlap"
			bandPadding={0}
			yBaseline={0}
			yDomain={[0, maxValue + 10]}
			padding={{ top: 30, right: 30, bottom: 30, left: 0 }}
			tooltipContext={{ mode: 'band' }}
			class="h-full w-full"
		>
			<Svg>
				<IsoBarDefs
					{idPrefix}
					bevelOpacity={BEVEL_OPACITY}
					highlightColor={HIGHLIGHT_COLOR}
					highlightColorDark={HIGHLIGHT_COLOR_DARK}
				/>
				<IsoBar dataKey="revenue" {rows} {maxValue} {idPrefix} />
				<Axis
					placement="bottom"
					rule={false}
					tickMarks={false}
					tickLength={10}
					ticks={thinAxisTicks({ format: formatMonth })}
					format={formatMonth}
				/>
			</Svg>
			<ChartTooltip>
				{#snippet children({ data })}
					<ChartTooltipContent
						active
						label={(data as Record<string, unknown>)?.month as string}
						payload={[
							{
								dataKey: 'revenue',
								name: 'revenue',
								value: (data as Record<string, unknown>)?.revenue as number,
								payload: data
							}
						]}
					>
						{#snippet formatter(value, name)}
							<div class="flex flex-1 items-center gap-2">
								<div
									class="size-2.5 shrink-0 rounded-[2px]"
									style="background: var(--color-revenue-0)"
								></div>
								<span class="flex-1 text-muted-foreground capitalize">{name}</span>
								<span class="font-mono font-medium text-foreground tabular-nums">
									${value}K
								</span>
							</div>
						{/snippet}
					</ChartTooltipContent>
				{/snippet}
			</ChartTooltip>
		</Chart>
	</ChartContainer>
</div>
```

`$lib/components/evilcharts/blocks/isometric-bar-chart-bar.svelte`

```svelte
<script lang="ts">
	/**
	 * One isometric column: a front face, a top bevel and a side bevel, hatched over, growing up out
	 * of the baseline on a staggered delay. The tallest column is picked out in the accent colour.
	 *
	 * The reference reads the rectangle from Recharts' shape props; here it comes off the chart
	 * scales with the same arithmetic.
	 */
	import { Bar as LayerBar, getChartContext } from 'layerchart';
	import { useReducedMotion } from '@humanspeak/svelte-motion';
	import { getBarPositions } from '$lib/components/evilcharts/ui/layerchart-chart/index.js';

	let {
		dataKey,
		rows,
		maxValue,
		idPrefix
	}: {
		dataKey: string;
		rows: Record<string, unknown>[];
		maxValue: number;
		idPrefix: string;
	} = $props();

	const layer = getChartContext();
	const shouldReduceMotion = useReducedMotion();

	const DX = 10;
	const DY = 10;
	const FILLED: boolean = true;
	const DIRECTION = 'right' as 'left' | 'right';
	const HIGHLIGHT_COLOR_DARK = '#15803d';
	/** The reference's `barCategoryGap="25%"`. */
	const BAR_CATEGORY_GAP = '25%';

	const url = (name: string) => `url(#${idPrefix}-${name})`;

	/**
	 * Hoisted out of the template: a fresh object literal on every re-derive makes svelte-motion
	 * tear down and rebuild its presence child, which restarts the intro and leaves it reading a
	 * destroyed branch's derived (`derived_inert`).
	 */
	const INTRO_INITIAL = { transform: 'scaleY(0)', opacity: 0 };
	const INTRO_ANIMATE = { transform: 'scaleY(1)', opacity: 1 };

	/** Memoised per index so the staggered transition keeps its identity when the scales change. */
	const transitions: Record<number, { duration: number; delay: number; ease: number[] }> = {};
	const transitionFor = (index: number) =>
		(transitions[index] ??= { duration: 0.7, delay: index * 0.08, ease: [0.16, 1, 0.3, 1] });

	function growIn(
		transition: { duration: number; delay: number; ease: number[] },
		reduced: boolean
	) {
		return (node: SVGGElement) => {
			if (reduced) return undefined;

			const animation = node.animate([INTRO_INITIAL, INTRO_ANIMATE], {
				duration: transition.duration * 1000,
				delay: transition.delay * 1000,
				easing: `cubic-bezier(${transition.ease.join(',')})`,
				fill: 'both'
			});
			return () => animation.cancel();
		};
	}

	const bars = $derived.by(() => {
		const band = layer.xScale.bandwidth?.() ?? 0;
		const slot = getBarPositions({ bandSize: band, count: 1, barCategoryGap: BAR_CATEGORY_GAP })[0];
		const width = slot?.size ?? band;
		const baseline = Number(layer.yScale(0));

		return rows.map((row, index) => {
			const value = Number(row[dataKey] ?? 0);
			const bx = (Number(layer.xScale(index as never)) || 0) + (slot?.offset ?? 0);
			const by = Number(layer.yScale(value));
			const bh = Math.abs(baseline - by);
			const highlight = value === maxValue;

			const dx = DIRECTION === 'left' ? -DX : DX;
			const sideX = DIRECTION === 'left' ? bx : bx + width;

			return {
				row,
				index,
				bx,
				by,
				bw: width,
				bh,
				highlight,
				transition: transitionFor(index),
				topPoints: `${bx},${by} ${bx + width},${by} ${bx + width + dx},${by - DY} ${bx + dx},${by - DY}`,
				sidePoints: `${sideX},${by} ${sideX + dx},${by - DY} ${sideX + dx},${by + bh - DY} ${sideX},${by + bh}`,
				strokeColor: highlight ? HIGHLIGHT_COLOR_DARK : 'var(--color-accent)',
				frontFill: FILLED ? url(highlight ? 'iso-front-accent' : 'iso-front-base') : 'none',
				topFill: FILLED ? url(highlight ? 'iso-top-accent' : 'iso-top-base') : 'none',
				rightFill: FILLED ? url(highlight ? 'iso-right-accent' : 'iso-right-base') : 'none',
				hatchFill: url(highlight ? 'iso-hatch-accent' : 'iso-hatch-base')
			};
		});
	});
</script>

{#each bars as bar (bar.index)}
	<!-- Transparent twin keeps the column hoverable for the tooltip. -->
	<LayerBar data={bar.row} seriesKey={dataKey} fill="transparent" motion="none" tooltip />

	<!--
		Rendered unconditionally. The reference bails out with `null` when the height is zero, but
		LayerChart runs one pass before it has measured the container, so an `{#if bar.bh > 0}` gate
		tore the group down and rebuilt it — restarting the intro and leaving svelte-motion reading a
		derived from the destroyed branch (`derived_inert`).
	-->
	<g
		{@attach growIn(bar.transition, shouldReduceMotion.current)}
		style:transform-box="fill-box"
		style:transform-origin="50% 100%"
	>
		<polygon
			points={bar.sidePoints}
			fill={bar.rightFill}
			stroke={bar.strokeColor}
			stroke-width={FILLED ? 0 : 1}
		/>
		<polygon
			points={bar.topPoints}
			fill={bar.topFill}
			stroke={bar.strokeColor}
			stroke-width={FILLED ? 0 : 1}
		/>
		<rect
			x={bar.bx}
			y={bar.by}
			width={bar.bw}
			height={bar.bh}
			fill={bar.frontFill}
			stroke={bar.strokeColor}
			stroke-width={FILLED ? 0 : 1}
		/>
		{#if FILLED}
			<rect x={bar.bx} y={bar.by} width={bar.bw} height={bar.bh} fill={bar.hatchFill} />
		{/if}
		{#if FILLED && bar.highlight}
			<rect x={bar.bx} y={bar.by} width={2} height={bar.bh} fill="rgba(0,0,0,0.15)" />
		{/if}
	</g>
{/each}
```

`$lib/components/evilcharts/blocks/isometric-bar-chart-defs.svelte`

```svelte
<script lang="ts">
	/**
	 * Gradients and hatches for the isometric bars.
	 *
	 * Ids are namespaced per chart instance so several charts on a page do not share — and clobber —
	 * each other's `<defs>`.
	 */
	let {
		idPrefix,
		bevelOpacity,
		highlightColor,
		highlightColorDark
	}: {
		idPrefix: string;
		bevelOpacity: number;
		highlightColor: string;
		highlightColorDark: string;
	} = $props();
</script>

<defs>
	<linearGradient id={`${idPrefix}-iso-front-base`} x1="0" y1="0" x2="0" y2="1">
		<stop offset="0%" stop-color="var(--color-accent)" stop-opacity={1} />
		<stop offset="100%" stop-color="var(--color-accent)" stop-opacity={0.8} />
	</linearGradient>
	<linearGradient id={`${idPrefix}-iso-top-base`} x1="0" y1="0" x2="1" y2="0">
		<stop offset="0%" stop-color="var(--color-accent)" stop-opacity={bevelOpacity} />
		<stop offset="100%" stop-color="var(--color-accent)" stop-opacity={bevelOpacity * 0.9} />
	</linearGradient>
	<linearGradient id={`${idPrefix}-iso-right-base`} x1="0" y1="0" x2="0" y2="1">
		<stop offset="0%" stop-color="var(--color-accent)" stop-opacity={bevelOpacity * 0.7} />
		<stop offset="100%" stop-color="var(--color-accent)" stop-opacity={bevelOpacity * 0.55} />
	</linearGradient>

	<linearGradient id={`${idPrefix}-iso-front-accent`} x1="0" y1="0" x2="0" y2="1">
		<stop offset="0%" stop-color={highlightColor} stop-opacity={1} />
		<stop offset="100%" stop-color={highlightColorDark} stop-opacity={0.95} />
	</linearGradient>
	<linearGradient id={`${idPrefix}-iso-top-accent`} x1="0" y1="0" x2="1" y2="0">
		<stop offset="0%" stop-color={highlightColor} stop-opacity={bevelOpacity + 0.15} />
		<stop offset="100%" stop-color={highlightColor} stop-opacity={bevelOpacity} />
	</linearGradient>
	<linearGradient id={`${idPrefix}-iso-right-accent`} x1="0" y1="0" x2="0" y2="1">
		<stop offset="0%" stop-color={highlightColorDark} stop-opacity={bevelOpacity + 0.05} />
		<stop offset="100%" stop-color={highlightColorDark} stop-opacity={bevelOpacity * 0.7} />
	</linearGradient>

	<pattern
		id={`${idPrefix}-iso-hatch-base`}
		patternUnits="userSpaceOnUse"
		width="6"
		height="6"
		patternTransform="rotate(45)"
	>
		<line
			x1="0"
			y1="0"
			x2="0"
			y2="6"
			stroke="currentColor"
			stroke-width="1"
			stroke-opacity="0.15"
		/>
	</pattern>
	<pattern
		id={`${idPrefix}-iso-hatch-accent`}
		patternUnits="userSpaceOnUse"
		width="6"
		height="6"
		patternTransform="rotate(45)"
	>
		<line
			x1="0"
			y1="0"
			x2="0"
			y2="6"
			stroke={highlightColorDark}
			stroke-width="1"
			stroke-opacity="0.15"
		/>
	</pattern>
</defs>
```
### npm

```bash
npx shadcn-svelte@latest add @evilcharts/isometric-bar-chart
```

### yarn

```bash
yarn dlx shadcn-svelte@latest add @evilcharts/isometric-bar-chart
```

### bun

```bash
bunx --bun shadcn-svelte@latest add @evilcharts/isometric-bar-chart
```

### pnpm

```bash
pnpm dlx shadcn-svelte@latest add @evilcharts/isometric-bar-chart
```
