
### Basic Chart

```svelte
<script lang="ts">
	import { EvilComposedChart } from '$lib/components/evilcharts/charts/layerchart-composed-chart/index.js';
	import { type ChartConfig } from '$lib/components/evilcharts/ui/layerchart-chart/index.js';

	const data = [
		{ month: 'January', revenue: 4200, profit: 1800 },
		{ month: 'February', revenue: 5800, profit: 2400 },
		{ month: 'March', revenue: 4100, profit: 1600 },
		{ month: 'April', revenue: 6200, profit: 2800 },
		{ month: 'May', revenue: 5400, profit: 2200 },
		{ month: 'June', revenue: 7800, profit: 3400 },
		{ month: 'July', revenue: 6100, profit: 2600 },
		{ month: 'August', revenue: 8200, profit: 3800 },
		{ month: 'September', revenue: 5900, profit: 2500 },
		{ month: 'October', revenue: 6800, profit: 3000 },
		{ month: 'November', revenue: 7200, profit: 3200 },
		{ month: 'December', revenue: 9100, profit: 4200 }
	];

	const chartConfig = {
		revenue: {
			label: 'Revenue',
			colors: {
				light: ['#3b82f6'],
				dark: ['#6A5ACD']
			}
		},
		profit: {
			label: 'Profit',
			colors: {
				light: ['#10b981'],
				dark: ['#34d399']
			}
		}
	} satisfies ChartConfig;
</script>

<EvilComposedChart class="h-full w-full p-4" xDataKey="month" {data} config={chartConfig}>
	<EvilComposedChart.Grid />
	<EvilComposedChart.XAxis
		dataKey="month"
		tickFormatter={(value) => String(value).substring(0, 3)}
	/>
	<EvilComposedChart.Brush formatLabel={(value) => String(value).substring(0, 3)} />
	<EvilComposedChart.Legend isClickable />
	<EvilComposedChart.Tooltip />
	<EvilComposedChart.Bar dataKey="revenue" isClickable />
	<EvilComposedChart.Line dataKey="profit" isClickable>
		<EvilComposedChart.ActiveDot variant="colored-border" />
		<EvilComposedChart.Dot variant="default" />
	</EvilComposedChart.Line>
</EvilComposedChart>
```

## Installation


  
  
    ### npm

```bash
npx shadcn-svelte@latest add @evilcharts/layerchart-composed-chart
```

### yarn

```bash
yarn dlx shadcn-svelte@latest add @evilcharts/layerchart-composed-chart
```

### bun

```bash
bunx --bun shadcn-svelte@latest add @evilcharts/layerchart-composed-chart
```

### pnpm

```bash
pnpm dlx shadcn-svelte@latest add @evilcharts/layerchart-composed-chart
```
  
  
    
      
        ### Install the following dependencies:
        
          ### npm

```bash
npm install layerchart @humanspeak/svelte-motion
```

### yarn

```bash
yarn add layerchart @humanspeak/svelte-motion
```

### bun

```bash
bun add layerchart @humanspeak/svelte-motion
```

### pnpm

```bash
pnpm add layerchart @humanspeak/svelte-motion
```
        
      
      
        ### Copy and paste the following code snippets into your project.
         

Create an `evilcharts` folder with a `charts` subfolder inside `components`, then paste the base layerchart-composed-chart code into a new file there.


        
          ### $lib/components/evilcharts/charts/layerchart-composed-chart

`$lib/components/evilcharts/charts/layerchart-composed-chart/active-dot.svelte`

```svelte
<script lang="ts">
	/**
	 * Declares the hovered/active point marker for the <Line /> it is composed
	 * inside. Like <Dot />, it is a configuration slot and renders nothing itself.
	 */
	import { useLineSlots } from './line-slots.svelte.js';
	import type { DotVariant } from '../../ui/layerchart-dot/types.js';

	let { variant }: { variant?: DotVariant } = $props();

	const slots = useLineSlots();
	const token = $props.id();

	$effect.pre(() => {
		slots.registerActiveDot(token, variant);
		return () => slots.unregisterActiveDot(token);
	});
</script>
```

`$lib/components/evilcharts/charts/layerchart-composed-chart/bar.svelte`

```svelte
<script lang="ts">
	/**
	 * A single bar series. Each <Bar /> is fully self-contained: it generates its
	 * own gradient/pattern definitions under a unique id, so any number of bars —
	 * each with its own variant, glow, and clickability — can live in one chart
	 * without style collisions.
	 */
	import { Bar as LayerBar, getChartContext } from 'layerchart';
	import { useReducedMotion } from '@humanspeak/svelte-motion';
	import { useComposedChart } from './composed-chart-context.svelte.js';
	import BarGlowFilter from './defs/bar-glow-filter.svelte';
	import DuotonePattern from './defs/duotone-pattern.svelte';
	import DuotoneReversePattern from './defs/duotone-reverse-pattern.svelte';
	import GradientPattern from './defs/gradient-pattern.svelte';
	import HatchedPattern from './defs/hatched-pattern.svelte';
	import StrippedPattern from './defs/stripped-pattern.svelte';
	import VerticalColorGradient from './defs/vertical-color-gradient.svelte';
	import { getBarPositions, type BarInsets } from '../../ui/layerchart-chart/bar-geometry.js';
	import AnimatedGrow from '../../ui/layerchart-chart/animated-grow.svelte';
	import type { DitherVariant } from '../../ui/layerchart-dither/index.js';
	import { getBarGrowAnimation, getBarOpacity, getVariantFill } from './helpers.js';
	import { DEFAULT_BAR_RADIUS, type BarVariant, type ComposedAnimationType } from './types.js';

	let {
		dataKey,
		variant = 'default',
		radius = DEFAULT_BAR_RADIUS,
		glow = false,
		animationType,
		isClickable = false,
		enableHoverHighlight = false,
		barProps,
		ditherVariant
	}: {
		dataKey: string; // series key — must exist on the data and config
		variant?: BarVariant; // fill style for this bar only
		radius?: number; // corner radius of the bar in pixels
		glow?: boolean; // applies a soft neon glow to this bar
		animationType?: ComposedAnimationType; // grow-in order — falls back to the chart default
		isClickable?: boolean; // lets this bar be selected by clicking it
		enableHoverHighlight?: boolean; // dims this bar when another column is hovered
		barProps?: Record<string, unknown>; // escape hatch for raw LayerChart Bar props
		ditherVariant?: DitherVariant; // ordered-dither texture override
	} = $props();

	const chart = useComposedChart();
	/** LayerChart's own context, for the y scale the stripped variant's cap measures against. */
	const layer = getChartContext();
	const id = $props.id(); // unique id scopes this bar's style defs
	// Devices set to "reduce motion" skip the grow-in animation entirely
	const shouldReduceMotion = useReducedMotion();

	// Announce this bar so the root can divide the category between every bar it finds,
	// the way Recharts does.
	$effect.pre(() => {
		chart.registerBar(id, dataKey);
		chart.registerSeries(id, dataKey);
		return () => {
			chart.registerBar(id, undefined);
			chart.registerSeries(id, undefined);
		};
	});

	const isSelected = $derived(chart.selectedDataKey === null || chart.selectedDataKey === dataKey);
	const filter = $derived(glow ? `url(#${id}-glow)` : undefined);
	const isDither = $derived(chart.renderStyle === 'dither');
	const resolvedDitherVariant = $derived(ditherVariant ?? chart.ditherVariant);

	// The grow-in is a per-frame animation — heavier than a static chart — so
	// `"none"` and the OS reduce-motion preference both opt out of it.
	const revealType = $derived<ComposedAnimationType>(
		shouldReduceMotion.current ? 'none' : (animationType ?? chart.animationType)
	);

	const isStripped = $derived(variant === 'stripped');
	const fill = $derived(getVariantFill(variant, id));
	const cursorClass = $derived(isClickable || enableHoverHighlight ? 'cursor-pointer' : undefined);

	/**
	 * This bar's slice of the category, in pixels.
	 *
	 * The chart runs with `bandPadding={0}`, so the band and the step are the same width — the
	 * shape Recharts' arithmetic assumes. `getBarPositions` then divides it exactly as Recharts
	 * does and the result is applied as insets, rather than letting LayerChart nest a second band
	 * scale. Doing it here rather than on the chart also keeps the lines on the full category
	 * width, as Recharts does.
	 */
	const bandSize = $derived(layer.xScale.bandwidth?.() ?? 0);

	const slot = $derived.by(() => {
		const count = Math.max(1, chart.barKeys.length);
		const index = Math.max(0, chart.barKeys.indexOf(dataKey));

		return getBarPositions({
			bandSize,
			count,
			barGap: chart.barGap,
			barCategoryGap: chart.barCategoryGap
		})[index];
	});

	const bandInsets = $derived(
		slot ? { left: slot.offset, right: Math.max(0, bandSize - slot.offset - slot.size) } : {}
	);

	function select() {
		if (!isClickable) return;
		// Clicking the selected bar clears the selection, otherwise selects it
		chart.selectDataKey(chart.selectedDataKey === dataKey ? null : dataKey);
	}

	/**
	 * Height of the painted body, in pixels.
	 *
	 * The stripped variant's cap is a 2px strip flush with the bar's top edge. LayerChart's
	 * `height` prop re-centres a bar on its value range rather than pinning it to that edge, so
	 * the cap is expressed as a bottom inset measured off the same scale instead.
	 */
	function bodyHeight(row: Record<string, unknown>) {
		const value = Number(row[dataKey]);
		if (!Number.isFinite(value)) return 0;
		return Math.abs(layer.yScale(value) - layer.yScale(0));
	}

	/**
	 * Everything each row needs to paint, resolved in one derivation.
	 *
	 * A `{const}` inside the keyed `{#each}` below does not re-derive when the selection changes,
	 * so the per-row values are computed here where they track it properly.
	 */
	const rows = $derived(
		chart.data.map((row, index) => ({
			row,
			grow: getBarGrowAnimation(revealType, index, chart.dataLength, chart.introStartedAt),
			opacity: getBarOpacity({
				isClickable,
				isSelected,
				selectedDataKey: chart.selectedDataKey,
				enableHoverHighlight,
				hoveredIndex: chart.hoveredIndex,
				index
			}),
			capInset: isStripped ? { ...bandInsets, bottom: Math.max(0, bodyHeight(row) - 2) } : undefined
		}))
	);
</script>

<!-- The root renders the skeleton bar while loading, so real bars step aside -->
{#if !chart.isLoading}
	<!--
		One `<Bar>` per row rather than a single `<Bars>`, so each bar can be wrapped in its own
		staggered grow-in. LayerChart still computes every bar's geometry from the chart scales.
	-->
	{#each rows as { row, grow, opacity, capInset }, index (index)}
		<g class={cursorClass} onclick={select} role="presentation">
			<!--
				Full-height transparent twin, outside the grow wrapper: it keeps the column hoverable
				even mid grow-in, which is what the reference's `hitArea` rect does.
			-->
			<LayerBar
				data={row}
				seriesKey={dataKey}
				fill="transparent"
				insets={bandInsets}
				motion="none"
				tooltip
			/>
			{#if grow}
				<AnimatedGrow animation={grow}>
					{@render painted(row, opacity, capInset)}
				</AnimatedGrow>
			{:else}
				{@render painted(row, opacity, capInset)}
			{/if}
		</g>
	{/each}

	<defs>
		<VerticalColorGradient {id} {dataKey} config={chart.config} />
		{#if variant === 'hatched'}
			<HatchedPattern {id} />
		{/if}
		{#if variant === 'duotone'}
			<DuotonePattern {id} {dataKey} config={chart.config} />
		{/if}
		{#if variant === 'duotone-reverse'}
			<DuotoneReversePattern {id} {dataKey} config={chart.config} />
		{/if}
		{#if variant === 'gradient'}
			<GradientPattern {id} />
		{/if}
		{#if variant === 'stripped'}
			<StrippedPattern {id} />
		{/if}
		{#if glow}
			<BarGlowFilter {id} />
		{/if}
	</defs>
{/if}

{#snippet painted(row: Record<string, unknown>, opacity: number, capInset: BarInsets | undefined)}
	{#if isStripped}
		<!--
			The stripped variant: a square-cornered body plus a solid 2px strip flush with its top
			edge. The glow and the dim both apply to the pair, so they are set on the wrapper.
		-->
		<g {filter} {opacity} class="transition-opacity duration-200">
			<LayerBar
				data={row}
				seriesKey={dataKey}
				radius={0}
				fill={isDither ? 'transparent' : fill}
				opacity={isDither ? opacity : undefined}
				data-evil-dither-mark={isDither ? 'fill' : undefined}
				data-evil-dither-key={isDither ? dataKey : undefined}
				data-evil-dither-variant={isDither ? resolvedDitherVariant : undefined}
				data-evil-dither-glow={isDither && glow ? 'true' : undefined}
				insets={bandInsets}
				motion="none"
			/>
			<LayerBar
				data={row}
				seriesKey={dataKey}
				radius={0}
				insets={capInset}
				fill={isDither ? 'transparent' : `url(#${id}-bar-colors)`}
				opacity={isDither ? opacity : undefined}
				data-evil-dither-mark={isDither ? 'fill' : undefined}
				data-evil-dither-key={isDither ? dataKey : undefined}
				data-evil-dither-variant={isDither ? 'solid' : undefined}
				motion="none"
			/>
		</g>
	{:else}
		<LayerBar
			data={row}
			seriesKey={dataKey}
			{radius}
			rounded="all"
			fill={isDither ? 'transparent' : fill}
			{opacity}
			{filter}
			insets={bandInsets}
			class="transition-opacity duration-200"
			data-evil-dither-mark={isDither ? 'fill' : undefined}
			data-evil-dither-key={isDither ? dataKey : undefined}
			data-evil-dither-variant={isDither ? resolvedDitherVariant : undefined}
			data-evil-dither-glow={isDither && glow ? 'true' : undefined}
			motion="none"
			{...barProps}
		/>
	{/if}
{/snippet}
```

`$lib/components/evilcharts/charts/layerchart-composed-chart/composed-chart-context.svelte.ts`

```ts
import { getContext, setContext } from 'svelte';
import type { ChartConfig } from '../../ui/layerchart-chart/chart-config.js';
import type { DitherVariant, RenderStyle } from '../../ui/layerchart-dither/index.js';
import { ChartSlots } from '../../ui/layerchart-chart/chart-slots.svelte.js';
import type { ComposedAnimationType, CurveType } from './types.js';

const COMPOSED_CHART_KEY = Symbol('evilcharts.composed-chart');

type Options = {
	config: () => ChartConfig;
	/** Rows currently rendered by the chart (brush-filtered, or the loading skeleton). */
	data: () => Record<string, unknown>[];
	/** Resolved category key for the band scale. */
	xKey: () => string | undefined;
	/** Series keys rendered by the chart, in config order. */
	seriesKeys: () => string[];
	/** Data keys of the `<Bar />` children currently rendered, in registration order. */
	barKeys: () => string[];
	curveType: () => CurveType;
	animationType: () => ComposedAnimationType;
	/** Gap between bars sharing a category, in pixels. Recharts' default is 4. */
	barGap: () => number | undefined;
	/** Gap on each side of a category. Recharts' default is `"10%"`. */
	barCategoryGap: () => number | undefined;
	/** Timestamp the chart mounted — anchors the one-shot intro. */
	introStartedAt: () => number;
	renderStyle: () => RenderStyle;
	ditherVariant: () => DitherVariant;
	isLoading: () => boolean;
	/** Data index currently hovered, or null when none. */
	hoveredIndex: () => number | null;
	chartId: () => string;
	selectedDataKey: () => string | null;
	selectDataKey: (dataKey: string | null) => void;
	/**
	 * Called by each `<Bar />` so the root knows how many bars share a category.
	 *
	 * Recharts divides a category between the bars it finds in the tree; LayerChart needs the
	 * sub-band domain up front, so the bars announce themselves and the root derives it.
	 */
	registerBar: (token: string, dataKey: string | undefined) => void;
	/** Registers every rendered graphical series, preserving child order for legend and tooltip. */
	registerSeries: (token: string, dataKey: string | undefined) => void;
	/**
	 * Called by `<XAxis dataKey>` on mount.
	 *
	 * Recharts reads the category key off `<XAxis dataKey>`; LayerChart needs it on the root's
	 * `x` accessor, so the axis pushes it up rather than the root reading down. Keeping the
	 * state on the root avoids a circular dependency between `xKey` and this context.
	 */
	registerXAxisDataKey: (token: string, dataKey: string | undefined) => void;
	/**
	 * Called by `<XAxis />` / `<YAxis />` so the root can reserve plot-area space for them.
	 *
	 * Recharts sizes the plot from the axes it renders (default chart margin 5 on every side, plus
	 * a 30px band for an `<XAxis>` and a 60px gutter for a `<YAxis>`). LayerChart takes `padding`
	 * as a single explicit value, so the axes announce themselves and the root derives it.
	 */
	registerAxis: (token: string, axis: 'x' | 'y', present: boolean) => void;
};

/**
 * Shared state for every part of the chart. Lifted into <EvilComposedChart /> so
 * that <Bar />, <Line />, <XAxis />, <Legend />, and friends can read it without
 * prop drilling. Sub-components are composed freely — the provider is the single
 * source of truth.
 */
export class ComposedChartContext {
	#options: Options;

	/** Parts that render outside `<Svg>` — see `ChartSlots`. */
	slots = new ChartSlots();

	constructor(options: Options) {
		this.#options = options;
	}

	get config() {
		return this.#options.config();
	}
	get data() {
		return this.#options.data();
	}
	get xKey() {
		return this.#options.xKey();
	}
	get seriesKeys() {
		return this.#options.seriesKeys();
	}
	get barKeys() {
		return this.#options.barKeys();
	}
	get curveType() {
		return this.#options.curveType();
	}
	get animationType() {
		return this.#options.animationType();
	}
	get barGap() {
		return this.#options.barGap();
	}
	get barCategoryGap() {
		return this.#options.barCategoryGap();
	}
	get introStartedAt() {
		return this.#options.introStartedAt();
	}
	get renderStyle() {
		return this.#options.renderStyle();
	}
	get ditherVariant() {
		return this.#options.ditherVariant();
	}
	get dataLength() {
		return this.#options.data().length;
	}
	get isLoading() {
		return this.#options.isLoading();
	}
	get hoveredIndex() {
		return this.#options.hoveredIndex();
	}
	get chartId() {
		return this.#options.chartId();
	}
	get selectedDataKey() {
		return this.#options.selectedDataKey();
	}

	selectDataKey = (dataKey: string | null) => {
		this.#options.selectDataKey(dataKey);
	};

	registerBar = (token: string, dataKey: string | undefined) => {
		this.#options.registerBar(token, dataKey);
	};

	registerSeries = (token: string, dataKey: string | undefined) => {
		this.#options.registerSeries(token, dataKey);
	};

	registerXAxisDataKey = (token: string, dataKey: string | undefined) => {
		this.#options.registerXAxisDataKey(token, dataKey);
	};

	registerAxis = (token: string, axis: 'x' | 'y', present: boolean) => {
		this.#options.registerAxis(token, axis, present);
	};
}

export function setComposedChartContext(options: Options) {
	const context = new ComposedChartContext(options);
	setContext(COMPOSED_CHART_KEY, context);
	return context;
}

/** Reads the chart context, throwing a helpful error when used outside <EvilComposedChart /> */
export function useComposedChart(): ComposedChartContext {
	const context = getContext<ComposedChartContext | undefined>(COMPOSED_CHART_KEY);

	if (!context) {
		throw new Error(
			'Composed chart parts (<Bar />, <Line />, <XAxis />, …) must be used within <EvilComposedChart />'
		);
	}

	return context;
}
```

`$lib/components/evilcharts/charts/layerchart-composed-chart/composed-chart.svelte`

```svelte
<script lang="ts" generics="TData extends Record<string, unknown>">
	/**
	 * Root of the composable composed chart. Owns the data, the shared context, the
	 * loading skeleton, and the optional zoom brush. Everything visual — axes, grid,
	 * tooltip, legend, and the bars and lines themselves — is composed as children,
	 * so a consumer renders exactly the parts they need.
	 */
	import { Chart, Html, Svg } from 'layerchart';
	import { untrack, type Snippet } from 'svelte';
	import {
		ChartContainer,
		LOADING_CATEGORY_DATA_KEY,
		LoadingIndicator,
		type ChartAccessibility,
		type ChartConfig
	} from '../../ui/layerchart-chart/index.js';
	import {
		EvilBrush,
		EvilBrushState,
		setBrushSlotContext
	} from '../../ui/layerchart-brush/index.js';
	import { setComposedChartContext } from './composed-chart-context.svelte.js';
	import {
		DitherDomLayer,
		type DitherBloom,
		type DitherVariant,
		type RenderStyle
	} from '../../ui/layerchart-dither/index.js';
	import LegendRender from './legend-render.svelte';
	import LoadingBar from './loading/loading-bar.svelte';
	import { LoadingDataState } from './loading/use-loading-data.svelte.js';
	import TooltipCursor from './tooltip-cursor.svelte';
	import TooltipRender from './tooltip-render.svelte';
	import { SvelteMap, SvelteSet } from 'svelte/reactivity';
	import {
		DEFAULT_BAR_RADIUS,
		LOADING_DATA_KEY,
		type ComposedAnimationType,
		type CurveType
	} from './types.js';

	let {
		config,
		data,
		children,
		class: className,
		chartProps,
		accessibility,
		curveType = 'linear',
		animationType = 'left-to-right',
		barGap,
		barCategoryGap,
		defaultSelectedDataKey = null,
		onSelectionChange,
		isLoading = false,
		loadingBars,
		xDataKey,
		initialDimension = { width: 320, height: 200 },
		renderStyle = 'svg',
		ditherVariant = 'gradient',
		ditherCellSize = 2,
		bloom = 'off'
	}: {
		config: ChartConfig; // series colors + labels for bars and lines
		data: TData[]; // rows rendered by the chart
		children: Snippet; // composed parts — <Bar />, <Line />, <XAxis />, <Legend />, …
		class?: string; // extra classes for the chart container
		chartProps?: Record<string, unknown>; // escape hatch for the raw LayerChart Chart
		accessibility?: ChartAccessibility; // accessible name and description for the chart group
		curveType?: CurveType; // default curve interpolation for every <Line />
		animationType?: ComposedAnimationType; // default intro for every <Bar /> and <Line />
		barGap?: number; // gap between bars sharing a category
		barCategoryGap?: number; // gap between bar categories
		defaultSelectedDataKey?: string | null; // series selected on first render
		onSelectionChange?: (selectedDataKey: string | null) => void; // fires when the selected series changes
		isLoading?: boolean; // shows the animated loading skeleton
		loadingBars?: number; // number of bars in the loading skeleton
		xDataKey?: keyof TData & string; // x-axis key — also used by the <Brush /> footer
		initialDimension?: { width: number; height: number }; // zero-size/first-render fallback
		renderStyle?: RenderStyle;
		ditherVariant?: DitherVariant;
		ditherCellSize?: number;
		bloom?: DitherBloom;
	} = $props();

	const chartId = $props.id(); // selector-safe id keeps CSS/SVG references valid

	/**
	 * Anchors the intro to a fixed moment so it plays exactly once — re-renders read elapsed
	 * time from here instead of replaying.
	 */
	let introStartedAt = $state(Date.now());
	let chartDimension = $state(untrack(() => initialDimension));
	let previousLoading = untrack(() => isLoading);

	$effect(() => {
		const loadingNow = isLoading;
		if (previousLoading && !loadingNow) introStartedAt = Date.now();
		previousLoading = loadingNow;
	});

	// One-time initialisation, mirroring the reference's `useState(defaultSelectedDataKey)`.
	let selectedDataKey = $state<string | null>(untrack(() => defaultSelectedDataKey));
	let hoveredIndex = $state<number | null>(null);

	const loading = new LoadingDataState({
		isLoading: () => isLoading,
		loadingBars: () => loadingBars ?? 12
	});

	const brush = new EvilBrushState({ data: () => data as Record<string, unknown>[] });

	// The <Brush> child is config-only: its presence turns the footer on. The reference pulls it
	// out of `children`; Svelte registers it into this context instead.
	const brushSlot = setBrushSlotContext();
	const showBrush = $derived(brushSlot.present);

	/** Category key pushed up by the rendered `<XAxis dataKey>`, when there is one. */
	let registeredXKey = $state<string | undefined>(undefined);
	let registeredXKeyToken: string | null = null;

	/** Data keys of the rendered `<Bar />` children, so several bars can split a category. */
	// `SvelteMap` for the same reactive mutation behavior as `axesPresent` below.
	const barKeyByToken = new SvelteMap<string, string>();
	const barKeys = $derived([...barKeyByToken.values()]);
	/** Every rendered Bar/Line in actual child order; config entries are presentation metadata. */
	const seriesKeyByToken = new SvelteMap<string, string>();
	const seriesKeys = $derived([...seriesKeyByToken.values()]);

	/** Which axes are rendered, so the plot reserves the space Recharts does. */
	// `SvelteSet`, not a plain `Set` in `$state`: `$state` proxies objects and arrays but not
	// `Map`/`Set`, so `.add()` / `.delete()` would not notify and the padding would never
	// pick up an axis.
	const axesPresent = { x: new SvelteSet<string>(), y: new SvelteSet<string>() };

	const CHART_MARGIN = 5; // Recharts' default <ComposedChart margin>
	const X_AXIS_HEIGHT = 30; // Recharts' default <XAxis height>
	const Y_AXIS_WIDTH = 60; // Recharts' default <YAxis width>
	const EDGE_LEGEND_HEIGHT = 32; // Recharts' absolute edge-legend wrapper height
	let composedContext: ReturnType<typeof setComposedChartContext>;
	const edgeLegendPlacement = $derived.by(() => {
		if (isLoading || !composedContext) return null;
		const align = composedContext.slots.legend?.verticalAlign;
		return align === 'top' || align === 'bottom' ? align : null;
	});

	const padding = $derived({
		top: CHART_MARGIN + (edgeLegendPlacement === 'top' ? EDGE_LEGEND_HEIGHT : 0),
		right: CHART_MARGIN,
		bottom:
			CHART_MARGIN +
			(!isLoading && axesPresent.x.size > 0 ? X_AXIS_HEIGHT : 0) +
			(edgeLegendPlacement === 'bottom' ? EDGE_LEGEND_HEIGHT : 0),
		left: CHART_MARGIN + (!isLoading && axesPresent.y.size > 0 ? Y_AXIS_WIDTH : 0)
	});

	const configuredKeys = $derived(Object.keys(config));
	const displayData = $derived(showBrush && !isLoading ? brush.visibleData : data);
	const chartData = $derived(
		(isLoading ? loading.loadingData : displayData) as Record<string, unknown>[]
	);

	/** Category key for the band scale, resolved from the mounted axis before falling back to data. */
	const fallbackXKey = $derived(
		Object.keys(chartData[0] ?? {}).find(
			(key) => !configuredKeys.includes(key) && key !== LOADING_DATA_KEY
		)
	);
	const xKey = $derived(
		isLoading ? LOADING_CATEGORY_DATA_KEY : (xDataKey ?? registeredXKey ?? fallbackXKey)
	);

	const series = $derived(
		isLoading
			? [{ key: LOADING_DATA_KEY, value: LOADING_DATA_KEY }]
			: seriesKeys.map((key) => ({ key, value: key }))
	);
	const ditherAnimationDuration = $derived(
		Math.max(1000, 500 + Math.max(0, chartData.length - 1) * 50)
	);

	composedContext = setComposedChartContext({
		config: () => config,
		data: () => chartData,
		xKey: () => xKey,
		seriesKeys: () => seriesKeys,
		barKeys: () => barKeys,
		curveType: () => curveType,
		animationType: () => animationType,
		barGap: () => barGap,
		barCategoryGap: () => barCategoryGap,
		introStartedAt: () => introStartedAt,
		renderStyle: () => renderStyle,
		ditherVariant: () => ditherVariant,
		isLoading: () => isLoading,
		hoveredIndex: () => hoveredIndex,
		chartId: () => chartId,
		selectedDataKey: () => selectedDataKey,
		selectDataKey: (next) => {
			selectedDataKey = next;
			onSelectionChange?.(next);
		},
		registerBar: (token, key) => {
			if (key === undefined) barKeyByToken.delete(token);
			else barKeyByToken.set(token, key);
		},
		registerSeries: (token, key) => {
			if (key === undefined) seriesKeyByToken.delete(token);
			else seriesKeyByToken.set(token, key);
		},
		registerXAxisDataKey: (token, key) => {
			// Ignore a stale teardown from LayerChart's mount-time remount.
			if (key === undefined && registeredXKeyToken !== token) return;
			registeredXKeyToken = key === undefined ? null : token;
			registeredXKey = key;
		},
		registerAxis: (token, axis, present) => {
			if (present) axesPresent[axis].add(token);
			else axesPresent[axis].delete(token);
		}
	});
</script>

<ChartContainer
	{config}
	{initialDimension}
	{accessibility}
	bind:dimension={chartDimension}
	class={className}
>
	<LoadingIndicator {isLoading} />
	<LegendRender placement="top" />
	<!-- The reference clears the hovered column when the pointer leaves the chart. -->
	<div
		class="flex min-h-0 w-full flex-1 flex-col"
		onpointerleave={() => (hoveredIndex = null)}
		role="presentation"
	>
		<Chart
			width={chartDimension.width}
			height={chartDimension.height}
			data={chartData}
			x={xKey}
			{series}
			seriesLayout="overlap"
			bandPadding={0}
			yBaseline={0}
			yNice
			{padding}
			tooltipContext={{ mode: 'band' }}
			class="h-full w-full"
			{...chartProps}
		>
			{#if renderStyle === 'dither'}
				<Html pointerEvents={false} clip zIndex={0}>
					<DitherDomLayer
						{ditherVariant}
						cellSize={ditherCellSize}
						{bloom}
						paused={isLoading}
						animationDuration={ditherAnimationDuration}
						animationRevision={introStartedAt}
					/>
				</Html>
			{/if}
			<Svg>
				{@render children()}
				<TooltipCursor />
				{#if isLoading}
					<LoadingBar
						{chartId}
						barRadius={DEFAULT_BAR_RADIUS}
						onShimmerExit={loading.onShimmerExit}
					/>
				{/if}
			</Svg>
			<TooltipRender />
		</Chart>
	</div>
	<LegendRender placement="middle" />
	<LegendRender placement="bottom" />

	{#snippet footer()}
		{#if showBrush && !isLoading}
			<EvilBrush
				data={data as Record<string, unknown>[]}
				chartConfig={config}
				{xDataKey}
				variant="area"
				{curveType}
				height={brushSlot.slot?.height}
				formatLabel={brushSlot.slot?.formatLabel}
				skipStyle
				class="mt-1"
				startIndex={brush.brushProps.startIndex}
				endIndex={brush.brushProps.endIndex}
				onChange={(range) => {
					brush.brushProps.onChange(range);
					brushSlot.slot?.onChange?.(range);
				}}
			/>
		{/if}
	{/snippet}
</ChartContainer>
```

`$lib/components/evilcharts/charts/layerchart-composed-chart/defs/bar-glow-filter.svelte`

```svelte
<script lang="ts">
	/** Soft outer-glow filter applied to a glowing bar. */
	let { id }: { id: string } = $props();
</script>

<filter id={`${id}-glow`} x="-100%" y="-100%" width="300%" height="300%">
	<feGaussianBlur in="SourceGraphic" stdDeviation="8" result="blur" />
	<feColorMatrix
		in="blur"
		type="matrix"
		values="1 0 0 0 0
                0 1 0 0 0
                0 0 1 0 0
                0 0 0 0.5 0"
		result="glow"
	/>
	<feMerge>
		<feMergeNode in="glow" />
		<feMergeNode in="SourceGraphic" />
	</feMerge>
</filter>
```

`$lib/components/evilcharts/charts/layerchart-composed-chart/defs/color-stops.svelte`

```svelte
<script lang="ts">
	/**
	 * The series' colour stops, shared by both gradient orientations.
	 *
	 * A single-colour series still emits two stops (0% and 100%) so the gradient paints a flat
	 * fill rather than a fade, exactly as the reference does.
	 */
	let { dataKey, colorsCount }: { dataKey: string; colorsCount: number } = $props();
</script>

{#if colorsCount === 1}
	<stop offset="0%" stop-color={`var(--color-${dataKey}-0)`} />
	<stop offset="100%" stop-color={`var(--color-${dataKey}-0)`} />
{:else}
	{#each Array.from({ length: colorsCount }, (_, index) => `${(index / (colorsCount - 1)) * 100}%`) as offset, index (offset)}
		<stop {offset} stop-color={`var(--color-${dataKey}-${index}, var(--color-${dataKey}-0))`} />
	{/each}
{/if}
```

`$lib/components/evilcharts/charts/layerchart-composed-chart/defs/duotone-pattern.svelte`

```svelte
<script lang="ts">
	/** Two-tone fill that splits each bar into a light and a full-strength half. */
	import { getColorsCount } from '../../../ui/layerchart-chart/colors.js';
	import type { ChartConfig } from '../../../ui/layerchart-chart/chart-config.js';
	import ColorStops from './color-stops.svelte';

	let { id, dataKey, config }: { id: string; dataKey: string; config: ChartConfig } = $props();

	const colorsCount = $derived(getColorsCount(config[dataKey] ?? {}));
</script>

<linearGradient
	id={`${id}-duotone-mask-gradient`}
	gradientUnits="objectBoundingBox"
	x1="0"
	y1="0"
	x2="1"
	y2="0"
>
	<stop offset="50%" stop-color="white" stop-opacity={0.4} />
	<stop offset="50%" stop-color="white" stop-opacity={1} />
</linearGradient>
<linearGradient
	id={`${id}-duotone-colors`}
	gradientUnits="objectBoundingBox"
	x1="0"
	y1="0"
	x2="0"
	y2="1"
>
	<ColorStops {dataKey} {colorsCount} />
</linearGradient>
<mask id={`${id}-duotone-mask`} maskContentUnits="objectBoundingBox">
	<rect x="0" y="0" width="1" height="1" fill={`url(#${id}-duotone-mask-gradient)`} />
</mask>
<pattern
	id={`${id}-duotone`}
	patternUnits="objectBoundingBox"
	patternContentUnits="objectBoundingBox"
	width="1"
	height="1"
>
	<rect
		x="0"
		y="0"
		width="1"
		height="1"
		fill={`url(#${id}-duotone-colors)`}
		mask={`url(#${id}-duotone-mask)`}
	/>
</pattern>
```

`$lib/components/evilcharts/charts/layerchart-composed-chart/defs/duotone-reverse-pattern.svelte`

```svelte
<script lang="ts">
	/** Two-tone fill mirrored from `duotone` — the full-strength half comes first. */
	import { getColorsCount } from '../../../ui/layerchart-chart/colors.js';
	import type { ChartConfig } from '../../../ui/layerchart-chart/chart-config.js';
	import ColorStops from './color-stops.svelte';

	let { id, dataKey, config }: { id: string; dataKey: string; config: ChartConfig } = $props();

	const colorsCount = $derived(getColorsCount(config[dataKey] ?? {}));
</script>

<linearGradient
	id={`${id}-duotone-reverse-mask-gradient`}
	gradientUnits="objectBoundingBox"
	x1="0"
	y1="0"
	x2="1"
	y2="0"
>
	<stop offset="50%" stop-color="white" stop-opacity={1} />
	<stop offset="50%" stop-color="white" stop-opacity={0.4} />
</linearGradient>
<linearGradient
	id={`${id}-duotone-reverse-colors`}
	gradientUnits="objectBoundingBox"
	x1="0"
	y1="0"
	x2="0"
	y2="1"
>
	<ColorStops {dataKey} {colorsCount} />
</linearGradient>
<mask id={`${id}-duotone-reverse-mask`} maskContentUnits="objectBoundingBox">
	<rect x="0" y="0" width="1" height="1" fill={`url(#${id}-duotone-reverse-mask-gradient)`} />
</mask>
<pattern
	id={`${id}-duotone-reverse`}
	patternUnits="objectBoundingBox"
	patternContentUnits="objectBoundingBox"
	width="1"
	height="1"
>
	<rect
		x="0"
		y="0"
		width="1"
		height="1"
		fill={`url(#${id}-duotone-reverse-colors)`}
		mask={`url(#${id}-duotone-reverse-mask)`}
	/>
</pattern>
```

`$lib/components/evilcharts/charts/layerchart-composed-chart/defs/gradient-pattern.svelte`

```svelte
<script lang="ts">
	/** Gradient fill for a bar that fades from visible at the top toward transparent. */
	let { id }: { id: string } = $props();
</script>

<linearGradient id={`${id}-gradient-mask-gradient`} x1="0" y1="0" x2="0" y2="1">
	<stop offset="20%" stop-color="white" stop-opacity={1} />
	<stop offset="90%" stop-color="white" stop-opacity={0} />
</linearGradient>
<mask id={`${id}-gradient-mask`}>
	<rect width="100%" height="100%" fill={`url(#${id}-gradient-mask-gradient)`} />
</mask>
<pattern id={`${id}-gradient`} patternUnits="userSpaceOnUse" width="100%" height="100%">
	<rect
		width="100%"
		height="100%"
		fill={`url(#${id}-bar-colors)`}
		mask={`url(#${id}-gradient-mask)`}
	/>
</pattern>
```

`$lib/components/evilcharts/charts/layerchart-composed-chart/defs/hatched-pattern.svelte`

```svelte
<script lang="ts">
	/** Hatched diagonal-stripe fill for a bar, masked from the series color gradient. */
	let { id }: { id: string } = $props();
</script>

<pattern
	id={`${id}-hatched-mask-pattern`}
	x="0"
	y="0"
	width="5"
	height="5"
	patternUnits="userSpaceOnUse"
	patternTransform="rotate(-45)"
>
	<rect width="5" height="5" fill="white" fill-opacity={0.3} />
	<rect width="1.5" height="5" fill="white" fill-opacity={1} />
</pattern>
<mask id={`${id}-hatched-mask`}>
	<rect width="100%" height="100%" fill={`url(#${id}-hatched-mask-pattern)`} />
</mask>
<pattern id={`${id}-hatched`} patternUnits="userSpaceOnUse" width="100%" height="100%">
	<rect
		width="100%"
		height="100%"
		fill={`url(#${id}-bar-colors)`}
		mask={`url(#${id}-hatched-mask)`}
	/>
</pattern>
```

`$lib/components/evilcharts/charts/layerchart-composed-chart/defs/horizontal-color-gradient.svelte`

```svelte
<script lang="ts">
	/** Horizontal left-to-right color gradient — the stroke source for a line series. */
	import { getColorsCount } from '../../../ui/layerchart-chart/colors.js';
	import type { ChartConfig } from '../../../ui/layerchart-chart/chart-config.js';
	import ColorStops from './color-stops.svelte';

	let { id, dataKey, config }: { id: string; dataKey: string; config: ChartConfig } = $props();

	const colorsCount = $derived(getColorsCount(config[dataKey] ?? {}));
</script>

<linearGradient id={`${id}-line-colors-${dataKey}`} x1="0" y1="0" x2="1" y2="0">
	<ColorStops {dataKey} {colorsCount} />
</linearGradient>
```

`$lib/components/evilcharts/charts/layerchart-composed-chart/defs/line-glow-filter.svelte`

```svelte
<script lang="ts">
	/** Soft outer-glow filter applied to a glowing line. */
	let { id }: { id: string } = $props();
</script>

<filter id={`${id}-glow`} x="-50%" y="-50%" width="200%" height="200%">
	<feGaussianBlur in="SourceGraphic" stdDeviation="10" result="blur" />
	<feColorMatrix
		in="blur"
		type="matrix"
		values="1 0 0 0 0
                0 1 0 0 0
                0 0 1 0 0
                0 0 0 2 0"
		result="glow"
	/>
	<feMerge>
		<feMergeNode in="glow" />
		<feMergeNode in="SourceGraphic" />
	</feMerge>
</filter>
```

`$lib/components/evilcharts/charts/layerchart-composed-chart/defs/reveal-mask.svelte`

```svelte
<script lang="ts">
	/**
	 * Wipe mask driven by motion.dev, played once when a <Line /> mounts. The same
	 * mask is applied to the line's stroke and its resting dots, so both
	 * reveal in lockstep, replacing the default behaviour where the dots appeared
	 * before the line had finished drawing.
	 *
	 * `maskUnits`/`maskContentUnits` are both userSpaceOnUse so every masked element
	 * shares one coordinate space and the wipe edge lands at the same x on each.
	 *
	 * Each rect animates `scaleX` 0 → 1; `originX` decides which edge it grows from.
	 * "edges-in" needs two rects — each half grows inward from an opposite edge.
	 */
	import { motion } from '@humanspeak/svelte-motion';
	import { getRevealAnimation } from '../../../ui/layerchart-chart/intros.js';
	import { REVEAL_DURATION, REVEAL_EASE, SINGLE_REVEAL_ORIGIN } from '../types.js';
	import type { RevealAnimationType } from '../types.js';

	let {
		id,
		type,
		introStartedAt
	}: { id: string; type: RevealAnimationType; introStartedAt: number } = $props();

	const reveal = $derived(
		getRevealAnimation(REVEAL_DURATION, REVEAL_EASE, introStartedAt) ?? {
			initial: { scaleX: 1 },
			animate: { scaleX: 1 },
			transition: { duration: 0, ease: REVEAL_EASE }
		}
	);
</script>

<mask
	id={`${id}-reveal-mask`}
	maskUnits="userSpaceOnUse"
	maskContentUnits="userSpaceOnUse"
	x="0"
	y="0"
	width="100%"
	height="100%"
>
	{#if type === 'edges-in'}
		<!-- left half wipes inward from the left edge toward the centre -->
		<motion.rect
			{...reveal}
			x="0"
			y="0"
			width="50%"
			height="100%"
			fill="white"
			style={{ originX: 0 }}
		/>
		<!-- right half wipes inward from the right edge toward the centre -->
		<motion.rect
			{...reveal}
			x="50%"
			y="0"
			width="50%"
			height="100%"
			fill="white"
			style={{ originX: 1 }}
		/>
	{:else}
		<motion.rect
			{...reveal}
			x="0"
			y="0"
			width="100%"
			height="100%"
			fill="white"
			style={{ originX: SINGLE_REVEAL_ORIGIN[type] }}
		/>
	{/if}
</mask>
```

`$lib/components/evilcharts/charts/layerchart-composed-chart/defs/stripped-pattern.svelte`

```svelte
<script lang="ts">
	/** Low-opacity gradient fill paired with the solid top strip drawn by the bar itself. */
	let { id }: { id: string } = $props();
</script>

<linearGradient id={`${id}-stripped-mask-gradient`} x1="0" y1="0" x2="0" y2="1">
	<stop offset="0%" stop-color="white" stop-opacity={0.4} />
	<stop offset="100%" stop-color="white" stop-opacity={0.1} />
</linearGradient>
<mask id={`${id}-stripped-mask`}>
	<rect width="100%" height="100%" fill={`url(#${id}-stripped-mask-gradient)`} />
</mask>
<pattern id={`${id}-stripped`} patternUnits="userSpaceOnUse" width="100%" height="100%">
	<rect
		width="100%"
		height="100%"
		fill={`url(#${id}-bar-colors)`}
		mask={`url(#${id}-stripped-mask)`}
	/>
</pattern>
```

`$lib/components/evilcharts/charts/layerchart-composed-chart/defs/vertical-color-gradient.svelte`

```svelte
<script lang="ts">
	/** Vertical top-to-bottom color gradient — the fill source for every bar variant. */
	import { getColorsCount } from '../../../ui/layerchart-chart/colors.js';
	import type { ChartConfig } from '../../../ui/layerchart-chart/chart-config.js';
	import ColorStops from './color-stops.svelte';

	let { id, dataKey, config }: { id: string; dataKey: string; config: ChartConfig } = $props();

	const colorsCount = $derived(getColorsCount(config[dataKey] ?? {}));
</script>

<linearGradient id={`${id}-bar-colors`} x1="0" y1="0" x2="0" y2="1">
	<ColorStops {dataKey} {colorsCount} />
</linearGradient>
```

`$lib/components/evilcharts/charts/layerchart-composed-chart/dot.svelte`

```svelte
<script lang="ts">
	/**
	 * Declares a resting point marker for the <Line /> it is composed inside.
	 * It renders nothing on its own — the parent <Line /> reads its variant and
	 * wires it into the dot slot.
	 */
	import { useLineSlots } from './line-slots.svelte.js';
	import type { DotVariant } from '../../ui/layerchart-dot/types.js';

	let { variant }: { variant?: DotVariant } = $props();

	const slots = useLineSlots();
	const token = $props.id();

	$effect.pre(() => {
		slots.registerDot(token, variant);
		return () => slots.unregisterDot(token);
	});
</script>
```

`$lib/components/evilcharts/charts/layerchart-composed-chart/grid.svelte`

```svelte
<script lang="ts">
	/**
	 * The background grid lines. Defaults to horizontal-only dashed lines and
	 * forwards every LayerChart Grid prop for full control.
	 *
	 * Recharts takes `strokeDasharray` on the grid itself; LayerChart takes the dash pattern on
	 * each axis' line props, so it is threaded into both.
	 */
	import { Grid as LayerGrid } from 'layerchart';
	import { rechartsValueAxisTicks } from '../../ui/layerchart-chart/ticks.js';

	let {
		vertical = false,
		horizontal = true,
		strokeDasharray = '3 3',
		...restProps
	}: {
		vertical?: boolean;
		horizontal?: boolean;
		strokeDasharray?: string;
		[key: string]: unknown;
	} = $props();
</script>

<LayerGrid
	x={vertical ? { dashArray: strokeDasharray } : false}
	y={horizontal ? { dashArray: strokeDasharray } : false}
	yTicks={rechartsValueAxisTicks}
	{...restProps}
/>
```

`$lib/components/evilcharts/charts/layerchart-composed-chart/helpers.ts`

```ts
import {
	BAR_GROW_DURATION,
	BAR_STAGGER,
	REVEAL_EASE,
	type BarVariant,
	type ComposedAnimationType
} from './types.js';

// Returns stroke/dot opacity for a line — dims a series only when another is selected
export const getOpacity = (selectedDataKey: string | null, dataKey: string) => {
	if (selectedDataKey === null) {
		return { stroke: 1, dot: 1 };
	}

	return selectedDataKey === dataKey ? { stroke: 1, dot: 1 } : { stroke: 0.3, dot: 0.3 };
};

// Returns the fill opacity for a bar, accounting for both selection and hover state
export const getBarOpacity = ({
	isClickable,
	isSelected,
	selectedDataKey,
	enableHoverHighlight,
	hoveredIndex,
	index
}: {
	isClickable: boolean;
	isSelected: boolean;
	selectedDataKey: string | null;
	enableHoverHighlight: boolean;
	hoveredIndex: number | null;
	index: number;
}) => {
	const clickOpacity = isClickable && selectedDataKey !== null ? (isSelected ? 1 : 0.15) : 1;

	if (enableHoverHighlight && hoveredIndex !== null) {
		return hoveredIndex === index ? clickOpacity : clickOpacity * 0.3;
	}

	return clickOpacity;
};

// Resolves the SVG paint reference for a bar's fill based on its variant
export const getVariantFill = (variant: BarVariant, id: string): string => {
	switch (variant) {
		case 'hatched':
			return `url(#${id}-hatched)`;
		case 'duotone':
			return `url(#${id}-duotone)`;
		case 'duotone-reverse':
			return `url(#${id}-duotone-reverse)`;
		case 'gradient':
			return `url(#${id}-gradient)`;
		case 'stripped':
			return `url(#${id}-stripped)`;
		default:
			return `url(#${id}-bar-colors)`;
	}
};

/**
 * Builds the motion.dev grow-in animation for a single bar, or returns `null`
 * when it should render statically (`"none"`, an unknown index, or — crucially —
 * once the bar has already finished growing).
 *
 * The intro is anchored to `introStartedAt` (stamped once when the chart mounts)
 * rather than to component mount. A re-render caught mid-grow therefore resumes
 * from the progress it should already be at instead of replaying, which makes
 * the intro a true one-shot.
 */
export const getBarGrowAnimation = (
	animationType: ComposedAnimationType,
	index: number,
	dataLength: number,
	introStartedAt: number
) => {
	if (animationType === 'none' || index < 0 || dataLength <= 0) return null;

	const lastIndex = dataLength - 1;
	const center = lastIndex / 2;

	// How many bars this one waits behind before it starts growing
	let step: number;
	switch (animationType) {
		case 'right-to-left':
			step = lastIndex - index;
			break;
		case 'center-out':
			step = Math.abs(index - center);
			break;
		case 'edges-in':
			step = center - Math.abs(index - center);
			break;
		default: // left-to-right
			step = index;
	}

	const startMs = step * BAR_STAGGER * 1000;
	const durationMs = BAR_GROW_DURATION * 1000;
	const endMs = startMs + durationMs;
	const elapsed = Date.now() - introStartedAt;

	// Already finished — render static so re-renders can't replay it
	if (elapsed >= endMs) return null;

	// Resume from wherever this bar should already be (0 before it starts)
	const from = elapsed <= startMs ? 0 : (elapsed - startMs) / durationMs;

	return {
		initial: { scaleY: from },
		animate: { scaleY: 1 },
		transition: {
			duration: (endMs - Math.max(elapsed, startMs)) / 1000,
			ease: REVEAL_EASE,
			delay: Math.max(0, startMs - elapsed) / 1000
		},
		style: { originY: 1 } // grow upward from the baseline
	};
};
```

`$lib/components/evilcharts/charts/layerchart-composed-chart/index.ts`

```ts
import Root from './composed-chart.svelte';
import Bar from './bar.svelte';
import Line from './line.svelte';
import Dot from './dot.svelte';
import ActiveDot from './active-dot.svelte';
import XAxis from './x-axis.svelte';
import YAxis from './y-axis.svelte';
import Grid from './grid.svelte';
import Tooltip from './tooltip.svelte';
import Legend from './legend.svelte';
import { Brush } from '../../ui/layerchart-brush/index.js';

type RootComponent = typeof Root;

// Compound API: every part hangs off the root as a static member, so a consumer
// writes <EvilComposedChart.Bar/>, <EvilComposedChart.Line/>, … from a single import
// — no colliding named marker exports when several charts share one file.
//
// The explicit annotation is required for `svelte-package` to emit types.
export const EvilComposedChart: RootComponent & {
	Bar: typeof Bar;
	Line: typeof Line;
	Dot: typeof Dot;
	ActiveDot: typeof ActiveDot;
	XAxis: typeof XAxis;
	YAxis: typeof YAxis;
	Grid: typeof Grid;
	Tooltip: typeof Tooltip;
	Legend: typeof Legend;
	Brush: typeof Brush;
} = Object.assign(Root, {
	Bar,
	Line,
	Dot,
	ActiveDot,
	XAxis,
	YAxis,
	Grid,
	Tooltip,
	Legend,
	Brush
});

export type { BarVariant, ComposedAnimationType, CurveType, StrokeVariant } from './types.js';
export type { ChartAccessibility, ChartConfig } from '../../ui/layerchart-chart/index.js';
export type { DitherBloom, DitherVariant, RenderStyle } from '../../ui/layerchart-dither/index.js';
```

`$lib/components/evilcharts/charts/layerchart-composed-chart/legend-render.svelte`

```svelte
<script lang="ts">
	/** Renders the registered `<Legend />` slot as an HTML box outside the plot area. */
	import {
		ChartLegendContent,
		resolveLegendPlacement,
		type LegendPayloadItem,
		type LegendVerticalAlign
	} from '../../ui/layerchart-legend/index.js';
	import { useComposedChart } from './composed-chart-context.svelte.js';

	let { placement }: { placement: LegendVerticalAlign } = $props();

	const chart = useComposedChart();

	const slot = $derived(chart.slots.legend);
	const resolvedPlacement = $derived(resolveLegendPlacement(slot?.verticalAlign, 'top'));

	const payload = $derived<LegendPayloadItem[]>(
		chart.seriesKeys.map((key) => ({ dataKey: key, value: key }))
	);
</script>

{#if slot && !chart.isLoading && resolvedPlacement === placement}
	<ChartLegendContent
		{payload}
		verticalAlign={slot.verticalAlign}
		align={slot.align}
		variant={slot.variant}
		isClickable={slot.isClickable}
		selected={chart.selectedDataKey}
		onSelectChange={chart.selectDataKey}
		class={placement === 'middle'
			? 'pointer-events-auto absolute inset-x-[5px] top-1/2 z-10 -translate-y-1/2'
			: placement === 'top'
				? 'pointer-events-auto absolute inset-x-[5px] top-[5px] z-10'
				: 'pointer-events-auto absolute inset-x-[5px] bottom-[5px] z-10'}
	/>
{/if}
```

`$lib/components/evilcharts/charts/layerchart-composed-chart/legend.svelte`

```svelte
<script lang="ts">
	/**
	 * The series legend. When `isClickable` is set, each entry toggles selection of
	 * its series, driving the shared selection state read by every <Bar /> and <Line />.
	 *
	 * Config-only: the legend is an HTML box outside the SVG, so this registers its props
	 * and the root renders it above or below the plot per `verticalAlign`.
	 */
	import type {
		ChartLegendVariant,
		LegendAlign,
		LegendVerticalAlign
	} from '../../ui/layerchart-legend/index.js';
	import { useComposedChart } from './composed-chart-context.svelte.js';

	let {
		variant,
		align = 'right',
		verticalAlign = 'top',
		isClickable = false
	}: {
		variant?: ChartLegendVariant; // visual style of the legend indicators
		align?: LegendAlign; // horizontal placement
		verticalAlign?: LegendVerticalAlign; // vertical placement
		isClickable?: boolean; // lets each entry toggle selection of its series
	} = $props();

	const chart = useComposedChart();
	const token = $props.id();

	$effect.pre(() => {
		chart.slots.registerLegend(token, { variant, align, verticalAlign, isClickable });
		return () => chart.slots.unregisterLegend(token);
	});
</script>
```

`$lib/components/evilcharts/charts/layerchart-composed-chart/line-slots.svelte.ts`

```ts
import { getContext, setContext } from 'svelte';
import type { DotVariant } from '../../ui/layerchart-dot/types.js';

const LINE_SLOTS_KEY = Symbol('evilcharts.composed-line-slots');

/**
 * Registry for the `<Dot />` / `<ActiveDot />` children of one `<Line />`.
 *
 * The reference resolves them with `React.Children.forEach`; Svelte cannot inspect a
 * snippet, so each slot registers itself here instead.
 */
export class LineSlots {
	#dotToken: string | null = null;
	#activeDotToken: string | null = null;

	dot = $state<{ variant?: DotVariant } | null>(null);
	activeDot = $state<{ variant?: DotVariant } | null>(null);

	/** Token-keyed so a remount's stale teardown cannot clear the live slot. */
	registerDot(token: string, variant: DotVariant | undefined) {
		this.#dotToken = token;
		this.dot = { variant };
	}

	unregisterDot(token: string) {
		if (this.#dotToken !== token) return;
		this.#dotToken = null;
		this.dot = null;
	}

	registerActiveDot(token: string, variant: DotVariant | undefined) {
		this.#activeDotToken = token;
		this.activeDot = { variant };
	}

	unregisterActiveDot(token: string) {
		if (this.#activeDotToken !== token) return;
		this.#activeDotToken = null;
		this.activeDot = null;
	}
}

export function setLineSlotsContext() {
	const slots = new LineSlots();
	setContext(LINE_SLOTS_KEY, slots);
	return slots;
}

export function useLineSlots(): LineSlots {
	const slots = getContext<LineSlots | undefined>(LINE_SLOTS_KEY);

	if (!slots) {
		throw new Error('<Dot /> and <ActiveDot /> must be composed inside a <Line />');
	}

	return slots;
}
```

`$lib/components/evilcharts/charts/layerchart-composed-chart/line.svelte`

```svelte
<script lang="ts">
	/**
	 * A single line series. Each <Line /> is fully self-contained: it generates its
	 * own color gradient and glow filter under a unique id, so any number of lines —
	 * each with its own stroke, curve, glow, and clickability — can live in one chart
	 * without style collisions. Compose <Dot /> and <ActiveDot /> inside it to add
	 * point markers.
	 */
	import { Highlight, Points, Spline } from 'layerchart';
	import { useReducedMotion } from '@humanspeak/svelte-motion';
	import type { Snippet } from 'svelte';
	import { resolveCurve } from '../../ui/layerchart-chart/curves.js';
	import { ChartDot } from '../../ui/layerchart-dot/index.js';
	import type { DitherVariant } from '../../ui/layerchart-dither/index.js';
	import { useComposedChart } from './composed-chart-context.svelte.js';
	import HorizontalColorGradient from './defs/horizontal-color-gradient.svelte';
	import LineGlowFilter from './defs/line-glow-filter.svelte';
	import RevealMask from './defs/reveal-mask.svelte';
	import { getOpacity } from './helpers.js';
	import { setLineSlotsContext } from './line-slots.svelte.js';
	import {
		STROKE_WIDTH,
		type ComposedAnimationType,
		type CurveType,
		type StrokeVariant
	} from './types.js';

	let {
		dataKey,
		strokeVariant = 'solid',
		curveType,
		animationType,
		connectNulls = false,
		glow = false,
		isClickable = false,
		children,
		lineProps,
		ditherVariant
	}: {
		dataKey: string; // series key — must exist on the data and config
		strokeVariant?: StrokeVariant; // stroke style for this line only
		curveType?: CurveType; // curve interpolation — falls back to the chart default
		animationType?: ComposedAnimationType; // intro reveal — falls back to the chart default
		connectNulls?: boolean; // join segments across null/missing values
		glow?: boolean; // applies a soft neon glow to this line
		isClickable?: boolean; // lets this line be selected by clicking it
		children?: Snippet; // optional <Dot /> and <ActiveDot /> composition
		lineProps?: Record<string, unknown>; // escape hatch for raw LayerChart Spline props
		ditherVariant?: DitherVariant; // ordered-dither texture override
	} = $props();

	const chart = useComposedChart();
	const id = $props.id(); // unique id scopes this line's style defs
	// Devices set to "reduce motion" skip the intro reveal entirely
	const shouldReduceMotion = useReducedMotion();

	const slots = setLineSlotsContext();

	$effect.pre(() => {
		chart.registerSeries(id, dataKey);
		return () => chart.registerSeries(id, undefined);
	});

	const resolvedCurve = $derived(curveType ?? chart.curveType);

	// The reveal is an animated SVG mask — heavier than a static chart — so
	// `"none"` and the OS reduce-motion preference both opt out of it.
	const revealType = $derived<ComposedAnimationType>(
		shouldReduceMotion.current ? 'none' : (animationType ?? chart.animationType)
	);
	const maskId = $derived(revealType === 'none' ? undefined : `${id}-reveal-mask`);

	const opacity = $derived(getOpacity(chart.selectedDataKey, dataKey));
	const hasSelection = $derived(chart.selectedDataKey !== null);
	const filter = $derived(glow ? `url(#${id}-glow)` : undefined);

	const dotVariant = $derived(slots.dot?.variant);
	const activeDotVariant = $derived(slots.activeDot?.variant);

	const isAnimatedDashed = $derived(strokeVariant === 'animated-dashed');
	const isDashed = $derived(strokeVariant === 'dashed' || isAnimatedDashed);
	const isDither = $derived(chart.renderStyle === 'dither' && !isAnimatedDashed);
	const resolvedDitherVariant = $derived(ditherVariant ?? chart.ditherVariant);

	const defined = $derived(
		connectNulls
			? undefined
			: (d: Record<string, unknown>) => d[dataKey] !== null && d[dataKey] !== undefined
	);

	function select() {
		if (!isClickable) return;
		// Clicking the selected line clears the selection, otherwise selects it
		chart.selectDataKey(chart.selectedDataKey === dataKey ? null : dataKey);
	}

	/**
	 * Centres a point on the *category* band.
	 *
	 * `<Points seriesKey>` normally centres on the sub-band when one exists, which is right for a
	 * grouped bar but wrong for a line — Recharts always plots a line through the category centre.
	 * Passing the offset explicitly keeps the dots on the stroke once two bars have split the band.
	 */
	const bandOffset = (_value: number, ctx: { xScale: { bandwidth?: () => number } }) =>
		(ctx.xScale.bandwidth?.() ?? 0) / 2;
</script>

<!-- The root renders the skeleton bar while loading, so real lines step aside -->
{#if !chart.isLoading}
	{@render children?.()}

	{#if isClickable}
		<!-- Invisible fat stroke: a 20px hit area so the thin line is easy to click -->
		<Spline
			seriesKey={dataKey}
			curve={resolveCurve(resolvedCurve)}
			stroke="transparent"
			strokeWidth={20}
			{defined}
			motion="none"
			class="cursor-pointer"
			onclick={select}
		/>
	{/if}
	<Spline
		seriesKey={dataKey}
		curve={resolveCurve(resolvedCurve)}
		strokeOpacity={opacity.stroke}
		stroke={isDither ? 'transparent' : `url(#${id}-line-colors-${dataKey})`}
		strokeWidth={STROKE_WIDTH}
		stroke-dasharray={isDashed ? '5 5' : undefined}
		{filter}
		{defined}
		mask={maskId ? `url(#${maskId})` : undefined}
		data-evil-dither-mark={isDither ? 'stroke' : undefined}
		data-evil-dither-key={isDither ? dataKey : undefined}
		data-evil-dither-variant={isDither ? resolvedDitherVariant : undefined}
		data-evil-dither-reveal={isDither ? revealType : undefined}
		data-evil-dither-glow={isDither && glow ? 'true' : undefined}
		class={[
			isClickable && 'pointer-events-none cursor-pointer',
			isAnimatedDashed && !hasSelection && 'evil-composed-animated-dash'
		]
			.filter(Boolean)
			.join(' ') || undefined}
		motion="none"
		{...lineProps}
	/>

	{#if slots.dot}
		<!-- Resting point markers, wired to the intro reveal so they wipe in with the line -->
		<Points seriesKey={dataKey} offsetX={bandOffset} fill="none" stroke="none">
			{#snippet children({ points })}
				{#each points as point, index (index)}
					<ChartDot
						cx={point.x}
						cy={point.y}
						type={dotVariant}
						{dataKey}
						chartId={`${id}-line`}
						fillOpacity={opacity.dot}
						{maskId}
					/>
				{/each}
			{/snippet}
		</Points>
	{/if}

	{#if slots.activeDot}
		<!-- The active dot is left unmasked: it only appears on hover, after the intro -->
		<Highlight axis="none">
			{#snippet points({ points })}
				{#each points.filter((p) => (p as { seriesKey?: string }).seriesKey === dataKey) as point, index (index)}
					<ChartDot
						cx={point.x}
						cy={point.y}
						type={activeDotVariant}
						{dataKey}
						chartId={`${id}-line`}
						fillOpacity={opacity.dot}
					/>
				{/each}
			{/snippet}
		</Highlight>
	{/if}

	<defs>
		{#if revealType !== 'none'}
			<RevealMask {id} type={revealType} introStartedAt={chart.introStartedAt} />
		{/if}
		<HorizontalColorGradient {id} {dataKey} config={chart.config} />
		{#if glow}
			<LineGlowFilter {id} />
		{/if}
	</defs>
{/if}

<style>
	/*
		Move a fixed dash pattern along the stroke. Animating the dash lengths makes the visible dash
		shrink to zero every half-cycle, which reads as a flicker instead of directional motion.
	*/
	@keyframes evil-composed-dash-offset {
		from {
			stroke-dashoffset: 0;
		}
		to {
			stroke-dashoffset: -10;
		}
	}

	:global(.evil-composed-animated-dash) {
		animation: evil-composed-dash-offset 1s linear infinite;
	}

	@media (prefers-reduced-motion: reduce) {
		:global(.evil-composed-animated-dash) {
			animation: none;
		}
	}
</style>
```

`$lib/components/evilcharts/charts/layerchart-composed-chart/loading/gradient-stops.ts`

```ts
// Builds bell-curve eased gradient stops for the loading shimmer
export const generateEasedGradientStops = (
	steps: number = 17,
	minOpacity: number = 0.05,
	maxOpacity: number = 0.9
) => {
	return Array.from({ length: steps }, (_, i) => {
		const t = i / (steps - 1); // 0 to 1
		// Sine-based bell curve easing: peaks at center (t=0.5), smooth falloff at edges
		const eased = Math.sin(t * Math.PI) ** 2;
		const opacity = minOpacity + eased * (maxOpacity - minOpacity);
		return { offset: `${(t * 100).toFixed(0)}%`, opacity: Number(opacity.toFixed(3)) };
	});
};
```

`$lib/components/evilcharts/charts/layerchart-composed-chart/loading/loading-bar.svelte`

```svelte
<script lang="ts">
	/**
	 * The skeleton bar shown while the chart is loading. Rendered by the root in
	 * place of the real bars and lines, paired with its own masked shimmer pattern.
	 */
	import { Bars, getChartContext } from 'layerchart';
	import { getBarPositions } from '../../../ui/layerchart-chart/bar-geometry.js';
	import { LOADING_DATA_KEY } from '../types.js';
	import LoadingPattern from './loading-pattern.svelte';

	let {
		chartId,
		barRadius,
		onShimmerExit
	}: { chartId: string; barRadius: number; onShimmerExit: () => void } = $props();

	const layer = getChartContext();
	const bandSize = $derived((layer.xScale as { bandwidth?: () => number }).bandwidth?.() ?? 0);
	const slot = $derived(getBarPositions({ bandSize, count: 1 })[0]);
	const insets = $derived({
		left: slot?.offset ?? 0,
		right: bandSize - (slot?.offset ?? 0) - (slot?.size ?? 0)
	});
</script>

<Bars
	seriesKey={LOADING_DATA_KEY}
	fill="currentColor"
	fillOpacity={0.15}
	radius={barRadius}
	rounded="all"
	{insets}
	motion="none"
	mask={`url(#${chartId}-loading-mask)`}
/>
<defs>
	<LoadingPattern {chartId} {onShimmerExit} />
</defs>
```

`$lib/components/evilcharts/charts/layerchart-composed-chart/loading/loading-pattern.svelte`

```svelte
<script lang="ts">
	/**
	 * Animated shimmer pattern for the loading skeleton.
	 *
	 * The visible chart area is normalized to 0-1, the shimmer gradient has width 1,
	 * and the pattern is 3x wide so the shimmer has buffer on both sides. The motion
	 * rect travels x from -1 to 2; onShimmerExit fires as it crosses x=1, letting the
	 * data swap happen while the shimmer is off-screen for a seamless loop.
	 */
	import { animate, useReducedMotion } from '@humanspeak/svelte-motion';
	import { LOADING_ANIMATION_DURATION } from '../types.js';
	import { generateEasedGradientStops } from './gradient-stops.js';

	let { chartId, onShimmerExit }: { chartId: string; onShimmerExit: () => void } = $props();

	const gradientStops = generateEasedGradientStops();

	// 1 (left buffer) + 1 (visible) + 1 (right buffer)
	const patternWidth = 3;
	const startX = -1;
	const endX = 2;
	const shouldReduceMotion = useReducedMotion();

	// Tracks the last x value to detect the exit threshold crossing
	let lastX = startX;

	function runShimmer(reduced: boolean) {
		return (node: SVGRectElement) => {
			node.setAttribute('x', reduced ? '0' : String(startX));
			if (reduced) return undefined;

			const controls = animate(startX, endX, {
				duration: LOADING_ANIMATION_DURATION / 1000,
				ease: 'linear',
				repeat: Infinity,
				repeatType: 'loop',
				onUpdate(xValue: number) {
					node.setAttribute('x', String(xValue));
					if (xValue >= 1 && lastX < 1) onShimmerExit();
					lastX = xValue;
				}
			});
			return () => controls.stop();
		};
	}
</script>

<linearGradient id={`${chartId}-loading-gradient`} x1="0" y1="0" x2="1" y2="0">
	{#each gradientStops as { offset, opacity } (offset)}
		<stop {offset} stop-color="white" stop-opacity={opacity} />
	{/each}
</linearGradient>
<pattern
	id={`${chartId}-loading-pattern`}
	patternUnits="objectBoundingBox"
	patternContentUnits="objectBoundingBox"
	patternTransform="rotate(25)"
	width={patternWidth}
	height="1"
	x="0"
	y="0"
>
	<rect
		{@attach runShimmer(shouldReduceMotion.current)}
		y="0"
		width="1"
		height="1"
		fill={`url(#${chartId}-loading-gradient)`}
	/>
</pattern>
<mask id={`${chartId}-loading-mask`} maskUnits="userSpaceOnUse">
	<rect width="100%" height="100%" fill={`url(#${chartId}-loading-pattern)`} />
</mask>
```

`$lib/components/evilcharts/charts/layerchart-composed-chart/loading/use-loading-data.svelte.ts`

```ts
import { getLoadingData } from '../../../ui/layerchart-chart/loading.js';

/**
 * Loading data with pixel-perfect shimmer synchronization.
 *
 * Uses motion.dev's onUpdate callback to ensure chart data is only regenerated
 * when the shimmer has completely exited the visible area. This eliminates
 * timing drift issues from setTimeout/setInterval.
 */
export class LoadingDataState {
	#isLoading: () => boolean;
	#loadingBars: () => number;
	/** Toggled by `onShimmerExit`; regenerates the skeleton data once per shimmer loop. */
	#tick = $state(0);

	constructor(options: { isLoading: () => boolean; loadingBars?: () => number }) {
		this.#isLoading = options.isLoading;
		this.#loadingBars = options.loadingBars ?? (() => 12);
	}

	get loadingData() {
		this.#tick;
		return getLoadingData(this.#loadingBars(), 20, 80);
	}

	/** Fired by motion.dev when the shimmer exits the visible area. */
	onShimmerExit = () => {
		if (this.#isLoading()) {
			this.#tick += 1;
		}
	};
}
```

`$lib/components/evilcharts/charts/layerchart-composed-chart/tooltip-cursor.svelte`

```svelte
<script lang="ts">
	/**
	 * Renders the registered `<Tooltip cursor>` slot: the dashed vertical rule that follows the
	 * pointer. Lives inside `<Svg>`, matching Recharts' tooltip cursor.
	 */
	import { Highlight, getChartContext } from 'layerchart';
	import { useComposedChart } from './composed-chart-context.svelte.js';
	import { STROKE_WIDTH } from './types.js';

	const chart = useComposedChart();
	/** LayerChart's own context, to tell a hovered row from the `defaultIndex` one. */
	const layer = getChartContext();

	const slot = $derived(chart.slots.tooltip);
	const defaultRow = $derived(
		slot?.defaultIndex === undefined ? undefined : chart.data[slot.defaultIndex]
	);
	// The hovered row wins over `defaultIndex`, so pointer movement is not pinned to the initial row.
	const cursorRow = $derived(layer.tooltip.data ?? defaultRow);
</script>

{#if slot?.cursor && !chart.isLoading}
	<Highlight axis="x" lines={{ dashArray: '3 3', strokeWidth: STROKE_WIDTH }} data={cursorRow} />
{/if}
```

`$lib/components/evilcharts/charts/layerchart-composed-chart/tooltip-render.svelte`

```svelte
<script lang="ts">
	/** Renders the registered `<Tooltip />` slot: the floating box, as a sibling of `<Svg>`. */
	import { getChartContext } from 'layerchart';
	import {
		ChartTooltip,
		ChartTooltipContent,
		type TooltipPayloadItem
	} from '../../ui/layerchart-tooltip/index.js';
	import { useComposedChart } from './composed-chart-context.svelte.js';

	const chart = useComposedChart();
	/** LayerChart's own context, to tell a hovered row from the `defaultIndex` one. */
	const layer = getChartContext();

	const slot = $derived(chart.slots.tooltip);

	/**
	 * Row shown when nothing is hovered — the reference's `defaultIndex`.
	 *
	 * LayerChart resolves its tooltip data as `dataProp ?? ctx.tooltip.data`, so passing `data`
	 * unconditionally pins the tooltip to that row forever: with `defaultIndex` set, hovering any
	 * other category still reported the default one. The hovered row therefore takes precedence
	 * here and `defaultRow` only fills in when nothing is hovered.
	 */
	const defaultRow = $derived(
		slot?.defaultIndex === undefined ? undefined : chart.data[slot.defaultIndex]
	);

	function toPayload(row: Record<string, unknown>): TooltipPayloadItem[] {
		return chart.seriesKeys.map((key) => ({
			dataKey: key,
			name: key,
			value: row[key] as number | string | null,
			payload: row
		}));
	}
</script>

{#if slot && !chart.isLoading}
	<ChartTooltip data={layer.tooltip.data ?? defaultRow}>
		{#snippet children({ data })}
			<!-- Read inline rather than through a `{const}`: a declaration tag in a snippet body does
			     not re-derive when the snippet's argument changes, which froze the tooltip on the
			     first row it was shown for. -->
			<ChartTooltipContent
				active
				payload={toPayload(data as Record<string, unknown>)}
				label={chart.xKey ? ((data as Record<string, unknown>)[chart.xKey] as string) : undefined}
				selected={chart.selectedDataKey}
				roundness={slot.roundness}
				variant={slot.variant}
			/>
		{/snippet}
	</ChartTooltip>
{/if}
```

`$lib/components/evilcharts/charts/layerchart-composed-chart/tooltip.svelte`

```svelte
<script lang="ts">
	/**
	 * The hover tooltip. Reads the chart's selection from context so its content
	 * dims unselected series. Hidden automatically while the chart is loading.
	 *
	 * Config-only: the tooltip box and its cursor cannot render inside `<Svg>`, so this
	 * registers its props and the root renders them in the right place.
	 */
	import type { TooltipRoundness, TooltipVariant } from '../../ui/layerchart-tooltip/index.js';
	import { useComposedChart } from './composed-chart-context.svelte.js';

	let {
		variant,
		roundness,
		defaultIndex,
		cursor = true
	}: {
		variant?: TooltipVariant; // visual style of the tooltip surface
		roundness?: TooltipRoundness; // border-radius of the tooltip
		defaultIndex?: number; // data index shown by default with no hover
		cursor?: boolean; // whether the vertical cursor line follows the pointer
	} = $props();

	const chart = useComposedChart();
	const token = $props.id();

	$effect.pre(() => {
		chart.slots.registerTooltip(token, { variant, roundness, defaultIndex, cursor });
		return () => chart.slots.unregisterTooltip(token);
	});
</script>
```

`$lib/components/evilcharts/charts/layerchart-composed-chart/types.ts`

```ts
import type { CurveType } from '../../ui/layerchart-chart/curves.js';

// Constants
export const STROKE_WIDTH = 2; // line stroke — also the tooltip cursor's width
export const DEFAULT_BAR_RADIUS = 4;
export const LOADING_DATA_KEY = 'loading';
export const LOADING_ANIMATION_DURATION = 2000; // in milliseconds
export const REVEAL_DURATION = 1; // line intro wipe length, in seconds
export const REVEAL_EASE: [number, number, number, number] = [0, 0.7, 0.5, 1]; // intro easing
export const BAR_GROW_DURATION = 0.5; // per-bar grow-in length, in seconds
export const BAR_STAGGER = 0.05; // delay between consecutive bars, in seconds

export type StrokeVariant = 'solid' | 'dashed' | 'animated-dashed';
export type BarVariant =
	'default' | 'hatched' | 'duotone' | 'duotone-reverse' | 'gradient' | 'stripped';

/**
 * Direction of the custom motion.dev intro. LayerChart's own animation is
 * permanently disabled — lines wipe in along this direction, while bars grow up
 * from their baseline staggered in this same order.
 *
 * NOTE: the intro is a per-frame animation, heavier than a static chart.
 * `"none"` opts out — as does a device with the OS "reduce motion" preference.
 */
export type ComposedAnimationType =
	'none' | 'left-to-right' | 'right-to-left' | 'center-out' | 'edges-in';
export type RevealAnimationType = Exclude<ComposedAnimationType, 'none'>;

// motion `originX` for each single-rect line reveal — the edge the wipe grows from
export const SINGLE_REVEAL_ORIGIN: Record<Exclude<RevealAnimationType, 'edges-in'>, number> = {
	'left-to-right': 0,
	'right-to-left': 1,
	'center-out': 0.5
};

export type { CurveType };
```

`$lib/components/evilcharts/charts/layerchart-composed-chart/x-axis.svelte`

```svelte
<script lang="ts">
	/**
	 * The horizontal category axis. Ships with the chart's flat default styling and
	 * forwards every LayerChart Axis prop, so `tickFormatter`, `ticks`, etc. are
	 * passed straight through. Hidden automatically while the chart is loading.
	 *
	 * `dataKey` names the category key. Recharts reads it here; LayerChart needs it on the
	 * root's `x` accessor, so it is registered into the chart context on mount.
	 */
	import { Axis } from 'layerchart';
	import {
		layerChartFormatter,
		RECHARTS_X_AXIS_TICK_OFFSET,
		thinAxisTicks
	} from '../../ui/layerchart-chart/ticks.js';
	import type { ComponentProps } from 'svelte';
	import { useComposedChart } from './composed-chart-context.svelte.js';

	type Props = Omit<ComponentProps<typeof Axis>, 'placement' | 'format'> & {
		dataKey?: string;
		tickFormatter?: (value: unknown, index: number) => string;
		tickLine?: boolean;
		axisLine?: boolean;
		tickMargin?: number;
		minTickGap?: number;
	};

	let {
		dataKey,
		tickLine = false,
		axisLine = false,
		tickMargin = 8,
		minTickGap = 8,
		tickFormatter,
		...restProps
	}: Props = $props();

	const chart = useComposedChart();
	const token = $props.id();
	const translatedTickLength = $derived(tickMargin + RECHARTS_X_AXIS_TICK_OFFSET);
	const format = $derived(tickFormatter ? layerChartFormatter(tickFormatter) : undefined);
	const ticks = $derived(
		thinAxisTicks({
			minGap: minTickGap,
			format: (value, index) => tickFormatter?.(value, index) ?? String(value)
		})
	);

	$effect.pre(() => {
		chart.registerXAxisDataKey(token, dataKey);
		chart.registerAxis(token, 'x', true);
		return () => {
			chart.registerXAxisDataKey(token, undefined);
			chart.registerAxis(token, 'x', false);
		};
	});
</script>

{#if !chart.isLoading}
	<Axis
		placement="bottom"
		{ticks}
		rule={axisLine}
		tickMarks={tickLine}
		tickLength={translatedTickLength}
		{format}
		{...restProps}
	/>
{/if}
```

`$lib/components/evilcharts/charts/layerchart-composed-chart/y-axis.svelte`

```svelte
<script lang="ts">
	/**
	 * The vertical value axis. Forwards every LayerChart Axis prop.
	 * Hidden automatically while the chart is loading.
	 */
	import { Axis } from 'layerchart';
	import { layerChartFormatter } from '../../ui/layerchart-chart/ticks.js';
	import type { ComponentProps } from 'svelte';
	import { useComposedChart } from './composed-chart-context.svelte.js';

	type Props = Omit<ComponentProps<typeof Axis>, 'placement' | 'format'> & {
		dataKey?: string;
		tickFormatter?: (value: unknown, index: number) => string;
		tickLine?: boolean;
		axisLine?: boolean;
		tickMargin?: number;
		minTickGap?: number;
	};

	let {
		tickLine = false,
		axisLine = false,
		tickMargin = 8,
		minTickGap: _minTickGap = 8,
		tickFormatter,
		dataKey: _dataKey,
		...restProps
	}: Props = $props();

	const chart = useComposedChart();
	const token = $props.id();

	$effect.pre(() => {
		chart.registerAxis(token, 'y', true);
		return () => chart.registerAxis(token, 'y', false);
	});

	const format = $derived(tickFormatter ? layerChartFormatter(tickFormatter) : undefined);
</script>

{#if !chart.isLoading}
	<Axis
		placement="left"
		rule={axisLine}
		tickMarks={tickLine}
		tickLength={tickMargin}
		{format}
		{...restProps}
	/>
{/if}
```
        
      
       
        ### Add the chart component.
        

The chart needs these components to render. Create a `ui` folder inside `evilcharts` and paste the code there.

Below is the main chart component.


        
          ### $lib/components/evilcharts/ui/layerchart-chart

`$lib/components/evilcharts/ui/layerchart-chart/accessibility.ts`

```ts
/**
 * Accessible name and description for a chart root.
 *
 * A chart may be named directly or by visible text elsewhere on the page. Descriptions can be
 * supplied directly, linked from existing content, or both. The container exposes this as a
 * `group`, rather than an image, so interactive legends and marks remain discoverable.
 */
type ChartAccessibleName =
	{ label: string; labelledBy?: never } | { label?: never; labelledBy: string };

export type ChartAccessibility = ChartAccessibleName & {
	description?: string;
	describedBy?: string;
};
```

`$lib/components/evilcharts/ui/layerchart-chart/animated-grow.svelte`

```svelte
<script lang="ts">
	import type { Snippet } from 'svelte';

	type GrowAnimation = {
		initial: { scaleX: number } | { scaleY: number };
		animate: { scaleX: number } | { scaleY: number };
		transition: {
			duration: number;
			delay: number;
			ease: number[];
		};
		style: { originX: number } | { originY: number };
	};

	let { animation, children }: { animation: GrowAnimation; children: Snippet } = $props();

	function runGrow(value: GrowAnimation) {
		return (node: SVGGElement) => {
			node.style.transformBox = 'fill-box';
			const options: KeyframeAnimationOptions = {
				duration: value.transition.duration * 1000,
				delay: value.transition.delay * 1000,
				easing: `cubic-bezier(${value.transition.ease.join(',')})`,
				fill: 'both'
			};

			if ('scaleX' in value.initial) {
				const to = 'scaleX' in value.animate ? value.animate.scaleX : 1;
				node.style.transformOrigin = '0% 50%';
				const animation = node.animate(
					[{ transform: `scaleX(${value.initial.scaleX})` }, { transform: `scaleX(${to})` }],
					options
				);
				return () => animation.cancel();
			}

			const to = 'scaleY' in value.animate ? value.animate.scaleY : 1;
			node.style.transformOrigin = '50% 100%';
			const animation = node.animate(
				[{ transform: `scaleY(${value.initial.scaleY})` }, { transform: `scaleY(${to})` }],
				options
			);
			return () => animation.cancel();
		};
	}
</script>

<g {@attach runGrow(animation)}>
	{@render children()}
</g>
```

`$lib/components/evilcharts/ui/layerchart-chart/bar-geometry.ts`

```ts
/**
 * Bar sizing and placement within a category, ported from Recharts' `getBarPositions`.
 *
 * LayerChart divides a band with a nested `scaleBand`, which cannot reproduce Recharts' numbers:
 * Recharts subtracts the category gap and the inter-bar gaps from the band, divides what is left
 * between the bars, and **floors the result to a whole pixel** — so a 49px band holding two bars
 * with `barCategoryGap="10%"` and `barGap={4}` yields bars of exactly 17px, not 17.6px. It also
 * supports a fixed `barSize`, which centres a group of that width in the band.
 *
 * The port therefore computes each bar's offset and width here and applies them as `insets` on
 * LayerChart's `<Bar>`, leaving the band itself undivided.
 */

/** Leading/trailing insets along one axis, as LayerChart's `<Bar insets>` takes them. */
export type BarInsets = { left?: number; right?: number; top?: number; bottom?: number };

export type BarSlot = {
	/** Distance from the band's leading edge to this bar's leading edge, in pixels. */
	offset: number;
	/** Width of this bar along the category axis, in pixels. */
	size: number;
};

/**
 * Recharts' `getPercentValue` for a gap: a `"10%"` string resolves against the band, a number is
 * taken as pixels, and the result is clamped into `[0, bandSize]`.
 */
function resolveGap(
	value: number | string | undefined,
	bandSize: number,
	fallback: number | string
) {
	const raw = value ?? fallback;
	const resolved =
		typeof raw === 'string' && raw.trim().endsWith('%')
			? (Number.parseFloat(raw) / 100) * bandSize
			: Number(raw);

	if (!Number.isFinite(resolved)) return 0;
	return Math.max(0, Math.min(resolved, bandSize));
}

export function getBarPositions({
	bandSize,
	count,
	barGap,
	barCategoryGap,
	barSize,
	maxBarSize
}: {
	/** The category band's full size along the category axis. */
	bandSize: number;
	/** How many bars share the category. Stacked series count as one. */
	count: number;
	/** Gap between bars sharing a category. Recharts' default is `4`. */
	barGap?: number | string;
	/** Gap on each side of the category. Recharts' default is `"10%"`. */
	barCategoryGap?: number | string;
	/** Fixed bar width. When set, the group is centred in the band at that width. */
	barSize?: number;
	/** Upper bound on the derived width. */
	maxBarSize?: number;
}): BarSlot[] {
	if (count < 1 || !(bandSize > 0)) return [];

	let realBarGap = resolveGap(barGap, bandSize, 4);

	if (barSize != null && Number.isFinite(barSize)) {
		let useFull = false;
		let fullBarSize = bandSize / count;
		let sum = count * barSize + (count - 1) * realBarGap;

		// Too wide to fit: first drop the gaps, then fall back to 90% of an even share.
		if (sum >= bandSize) {
			sum -= (count - 1) * realBarGap;
			realBarGap = 0;
		}
		if (sum >= bandSize && fullBarSize > 0) {
			useFull = true;
			fullBarSize *= 0.9;
			sum = count * fullBarSize;
		}

		// Recharts truncates the centring offset to a whole pixel (`>> 0`).
		const offset = Math.trunc((bandSize - sum) / 2);
		const size = useFull ? fullBarSize : barSize;

		return Array.from({ length: count }, (_, index) => ({
			offset: offset + (size + realBarGap) * index,
			size
		}));
	}

	const categoryOffset = resolveGap(barCategoryGap, bandSize, '10%');
	// No room left for gaps once the category inset is taken out.
	if (bandSize - 2 * categoryOffset - (count - 1) * realBarGap <= 0) realBarGap = 0;

	let originalSize = (bandSize - 2 * categoryOffset - (count - 1) * realBarGap) / count;
	// Recharts floors anything above a pixel (`>>= 0`), which is why bars land on whole pixels.
	if (originalSize > 1) originalSize = Math.trunc(originalSize);

	const size =
		maxBarSize != null && Number.isFinite(maxBarSize)
			? Math.min(originalSize, maxBarSize)
			: originalSize;

	// The stride uses the unclamped size, so `maxBarSize` narrows a bar in place rather than
	// re-packing the group — again matching Recharts.
	return Array.from({ length: count }, (_, index) => ({
		offset: categoryOffset + (originalSize + realBarGap) * index + (originalSize - size) / 2,
		size
	}));
}
```

`$lib/components/evilcharts/ui/layerchart-chart/chart-config.ts`

```ts
import type { Component, Snippet } from 'svelte';

// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: '', dark: '.dark' } as const;

export type ThemeKey = keyof typeof THEMES;

// All Keys are optional at first
type ThemeColorsBase = {
	[K in ThemeKey]?: string[];
};

// Require at least one theme key
type AtLeastOneThemeColor = {
	[K in ThemeKey]: Required<Pick<ThemeColorsBase, K>> & Partial<Omit<ThemeColorsBase, K>>;
}[ThemeKey];

export const VALID_THEME_KEYS = Object.keys(THEMES) as ThemeKey[];

export { THEMES };

// Validation for chart config colors at runtime
export function validateChartConfigColors(config: ChartConfig): void {
	for (const [key, value] of Object.entries(config)) {
		if (value.colors) {
			const hasValidThemeKey = VALID_THEME_KEYS.some(
				(themeKey) => value.colors?.[themeKey] !== undefined
			);

			if (!hasValidThemeKey) {
				throw new Error(
					`[EvilCharts] Invalid chart config for "${key}": colors object must have at least one theme key (${VALID_THEME_KEYS.join(', ')}). Received empty object or invalid keys.`
				);
			}
		}
	}
}

export type ChartConfig = Record<
	string,
	{
		label?: string | Snippet;
		icon?: Component<Record<string, never>>;
		colors?: AtLeastOneThemeColor;
	}
>;

/** Validates that every config key also exists on the data row type. */
export type ValidateConfigKeys<TData, TConfig> = {
	[K in keyof TConfig]: K extends keyof TData ? ChartConfig[string] : never;
};
```

`$lib/components/evilcharts/ui/layerchart-chart/chart-container.svelte`

```svelte
<script lang="ts">
	import type { HTMLAttributes } from 'svelte/elements';
	import type { Snippet } from 'svelte';
	import { cn } from '$lib/utils.js';
	import { validateChartConfigColors, type ChartConfig } from './chart-config.js';
	import type { ChartAccessibility } from './accessibility.js';
	import { setChartContext } from './chart-context.svelte.js';
	import ChartStyle from './chart-style.svelte';

	type Props = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
		config: ChartConfig;
		children?: Snippet;
		/** Size used before the container has been measured. */
		initialDimension?: { width: number; height: number };
		/** @internal Resolved fallback-or-measured size used by chart roots. */
		dimension?: { width: number; height: number };
		/** Optional content rendered below the chart (e.g. EvilBrush) */
		footer?: Snippet;
		/** Accessible name and optional description for the chart as an interactive group. */
		accessibility?: ChartAccessibility;
	};

	let {
		id,
		config,
		initialDimension = { width: 320, height: 200 },
		dimension = $bindable(),
		class: className,
		children,
		footer,
		accessibility,
		...restProps
	}: Props = $props();

	const uniqueId = $props.id();
	const chartId = $derived(`chart-${id ?? uniqueId}`);
	const descriptionId = $derived(`${chartId}-description`);
	const describedBy = $derived(
		[accessibility?.description ? descriptionId : undefined, accessibility?.describedBy]
			.filter(Boolean)
			.join(' ') || undefined
	);
	let measuredWidth = $state(0);
	let measuredHeight = $state(0);
	const resolvedDimension = $derived(
		measuredWidth > 0 && measuredHeight > 0
			? { width: measuredWidth, height: measuredHeight }
			: initialDimension
	);

	$effect(() => {
		dimension = resolvedDimension;
	});

	// Validate chart config at runtime
	$effect.pre(() => {
		validateChartConfigColors(config);
	});

	setChartContext({
		get config() {
			return config;
		},
		get chartId() {
			return chartId;
		},
		get initialDimension() {
			return resolvedDimension;
		}
	});
</script>

<div
	data-slot="chart"
	data-chart={chartId}
	role={accessibility ? 'group' : undefined}
	aria-label={accessibility?.label}
	aria-labelledby={accessibility?.labelledBy}
	aria-describedby={describedBy}
	class={cn(
		'min-h-0 w-full flex-1',
		// Reference equivalents, retargeted from Recharts' `.recharts-*` hooks onto
		// LayerChart's `.lc-*` hooks.
		/*
			The grid and rule overrides are gated on `:not([stroke])`, mirroring the reference's
			`[&_.recharts-cartesian-grid_line[stroke='#ccc']]` / `[&_.recharts-polar-grid_[stroke='#ccc']]`
			selectors: they restyle only marks still carrying the library's *default* stroke and leave
			an explicitly-set one alone. Without the gate they also repainted the radar's polar grid,
			which sets `stroke="currentColor"` itself, washing the web out to `border/50`.
		*/
		"relative flex flex-col justify-center text-xs [&_.lc-arc-track]:fill-muted [&_.lc-axis-label]:[stroke:none] [&_.lc-axis-label]:text-xs [&_.lc-axis-label]:font-normal [&_.lc-axis-tick-label]:fill-[#666] [&_.lc-axis-tick-label]:[stroke:none] [&_.lc-axis-tick-label]:text-xs [&_.lc-axis-tick-label]:font-normal [&_.lc-axis[data-evil-scale='point']_.lc-axis-tick-group:last-of-type_.lc-axis-tick-label]:translate-x-[5px] [&_.lc-axis[data-evil-scale='point']_.lc-axis-tick-group:last-of-type_.lc-axis-tick-label]:[text-anchor:end] [&_.lc-grid-x-line:not([stroke])]:stroke-border/50 [&_.lc-grid-x-radial-line:not([stroke])]:stroke-border [&_.lc-grid-y-line:not([stroke])]:stroke-border/50 [&_.lc-grid-y-radial-circle:not([stroke])]:stroke-border [&_.lc-highlight-bar]:fill-muted [&_.lc-highlight-line]:stroke-border [&_.lc-highlight-point[stroke='#fff']]:stroke-transparent [&_.lc-layer]:outline-hidden [&_.lc-layout-svg]:outline-hidden [&_.lc-pie-arc]:outline-hidden [&_.lc-pie-arc[stroke='#fff']]:stroke-transparent [&_.lc-rule-x-line:not([stroke])]:stroke-border [&_.lc-rule-y-line:not([stroke])]:stroke-border",
		!footer && 'aspect-video',
		className
	)}
	{...restProps}
>
	{#if accessibility?.description}
		<span id={descriptionId} class="sr-only">{accessibility.description}</span>
	{/if}
	<ChartStyle id={chartId} {config} />
	<div
		class="relative flex min-h-0 w-full flex-1 flex-col"
		bind:clientWidth={measuredWidth}
		bind:clientHeight={measuredHeight}
	>
		{@render children?.()}
	</div>
	{@render footer?.()}
</div>
```

`$lib/components/evilcharts/ui/layerchart-chart/chart-context.svelte.ts`

```ts
import { getContext, setContext } from 'svelte';
import type { ChartConfig } from './chart-config.js';

const CHART_CONTEXT_KEY = Symbol('evilcharts.chart');

export type ChartContextValue = {
	readonly config: ChartConfig;
	readonly chartId: string;
	/**
	 * Size the chart falls back to before its container has been measured — the reference
	 * passes this to Recharts' `<ResponsiveContainer initialDimension>`.
	 */
	readonly initialDimension: { width: number; height: number };
};

export function setChartContext(value: ChartContextValue) {
	setContext(CHART_CONTEXT_KEY, value);
	return value;
}

/**
 * Reads the container context, throwing a helpful error when used outside <ChartContainer />.
 */
export function useChart(): ChartContextValue {
	const context = getContext<ChartContextValue | undefined>(CHART_CONTEXT_KEY);

	if (!context) {
		throw new Error('useChart must be used within a <ChartContainer />');
	}

	return context;
}
```

`$lib/components/evilcharts/ui/layerchart-chart/chart-slots.svelte.ts`

```ts
export type TooltipSlot = {
	variant?: 'default' | 'frosted-glass';
	roundness?: 'sm' | 'md' | 'lg' | 'xl';
	defaultIndex?: number;
	cursor?: boolean;
};

export type LegendSlot = {
	variant?:
		| 'square'
		| 'circle'
		| 'circle-outline'
		| 'rounded-square'
		| 'rounded-square-outline'
		| 'vertical-bar'
		| 'horizontal-bar';
	align?: 'left' | 'center' | 'right';
	verticalAlign?: 'top' | 'middle' | 'bottom';
	isClickable?: boolean;
};

/**
 * Reactive registrations for chart parts that render outside the plot SVG.
 *
 * A chart child records its tooltip or legend props here, and the root renders the matching HTML
 * layer in the correct place. Registrations use per-instance tokens because a chart subtree can
 * remount before the previous instance finishes tearing down. A stale cleanup must not clear the
 * newer live registration.
 */
export class ChartSlots {
	#tooltipToken: string | null = null;
	#legendToken: string | null = null;

	tooltip = $state<TooltipSlot | null>(null);
	legend = $state<LegendSlot | null>(null);

	registerTooltip(token: string, slot: TooltipSlot) {
		this.#tooltipToken = token;
		this.tooltip = slot;
	}

	unregisterTooltip(token: string) {
		if (this.#tooltipToken !== token) return;
		this.#tooltipToken = null;
		this.tooltip = null;
	}

	registerLegend(token: string, slot: LegendSlot) {
		this.#legendToken = token;
		this.legend = slot;
	}

	unregisterLegend(token: string) {
		if (this.#legendToken !== token) return;
		this.#legendToken = null;
		this.legend = null;
	}
}
```

`$lib/components/evilcharts/ui/layerchart-chart/chart-style.svelte`

```svelte
<script lang="ts">
	import { THEMES, type ChartConfig, type ThemeKey } from './chart-config.js';
	import { distributeColors, getColorsCount } from './colors.js';

	let { id, config }: { id: string; config: ChartConfig } = $props();

	const colorConfig = $derived(
		Object.entries(config).filter(([, itemConfig]) => itemConfig.colors)
	);

	function generateCssVars(theme: ThemeKey) {
		return colorConfig
			.flatMap(([key, itemConfig]) => {
				const colorsArray = itemConfig.colors?.[theme];
				if (!colorsArray || !Array.isArray(colorsArray) || colorsArray.length === 0) {
					return [];
				}

				// Get max count across all themes for this key
				const maxCount = getColorsCount(itemConfig);

				// Distribute colors evenly across all required slots
				const distributedColors = distributeColors(colorsArray, maxCount);

				return distributedColors.map((color, index) => `  --color-${key}-${index}: ${color};`);
			})
			.filter(Boolean)
			.join('\n');
	}

	const css = $derived(
		Object.entries(THEMES)
			.map(
				([theme, prefix]) =>
					`${prefix} [data-chart=${id}] {\n${generateCssVars(theme as ThemeKey)}\n}`
			)
			.join('\n')
	);
</script>

{#if colorConfig.length}
	<!-- A plain <style> element in a Svelte template is scoped-compiled, so the tag is built
	     dynamically to emit global CSS — the equivalent of the reference's
	     `<style dangerouslySetInnerHTML>`. Building it as an element (rather than {@html})
	     means the CSS text can never be parsed as markup. -->
	<svelte:element this={"style"}>{css}</svelte:element>
{/if}
```

`$lib/components/evilcharts/ui/layerchart-chart/colors.ts`

```ts
import { VALID_THEME_KEYS, type ChartConfig } from './chart-config.js';

// Distribute colors evenly across slots, extra slots go to last color(s)
// Example: 2 colors for 4 slots → [red, red, pink, pink]
// Example: 3 colors for 4 slots → [red, pink, blue, blue]
export function distributeColors(colorsArray: string[], maxCount: number): string[] {
	const availableCount = colorsArray.length;
	if (availableCount >= maxCount) {
		return colorsArray.slice(0, maxCount);
	}

	const result: string[] = [];
	const baseSlots = Math.floor(maxCount / availableCount);
	const extraSlots = maxCount % availableCount;

	// First (availableCount - extraSlots) colors get baseSlots each
	// Last extraSlots colors get (baseSlots + 1) each
	for (let colorIdx = 0; colorIdx < availableCount; colorIdx++) {
		const isExtraColor = colorIdx >= availableCount - extraSlots;
		const slotsForThisColor = baseSlots + (isExtraColor ? 1 : 0);
		for (let j = 0; j < slotsForThisColor; j++) {
			result.push(colorsArray[colorIdx]);
		}
	}

	return result;
}

// Get max colors count across all themes for a config entry
export function getColorsCount(config: ChartConfig[string]): number {
	if (!config.colors) return 1;
	const counts = VALID_THEME_KEYS.map((theme) => config.colors?.[theme]?.length ?? 0);
	return Math.max(...counts, 1);
}
```

`$lib/components/evilcharts/ui/layerchart-chart/curves.ts`

```ts
import {
	curveBasis,
	curveBasisClosed,
	curveBasisOpen,
	curveBumpX,
	curveBumpY,
	curveLinear,
	curveLinearClosed,
	curveMonotoneX,
	curveMonotoneY,
	curveNatural,
	curveStep,
	curveStepAfter,
	curveStepBefore,
	type CurveFactory
} from 'd3-shape';

/**
 * The curve names Recharts accepts on `<Area type>` / `<Line type>`. Kept as the public
 * `curveType` union so every chart's API reads exactly as it does in the reference.
 */
export type CurveType =
	| 'basis'
	| 'basisClosed'
	| 'basisOpen'
	| 'bumpX'
	| 'bumpY'
	| 'bump'
	| 'linear'
	| 'linearClosed'
	| 'natural'
	| 'monotoneX'
	| 'monotoneY'
	| 'monotone'
	| 'step'
	| 'stepBefore'
	| 'stepAfter';

/**
 * Recharts resolves each `type` to the identically named d3-shape curve — `bump` and
 * `monotone` being the two aliases, which it maps to the X-oriented variants for the
 * default horizontal layout. LayerChart takes the d3 curve factory directly, so the
 * mapping is all that stands between the two APIs.
 */
const CURVES: Record<CurveType, CurveFactory> = {
	basis: curveBasis,
	basisClosed: curveBasisClosed as CurveFactory,
	basisOpen: curveBasisOpen as CurveFactory,
	bumpX: curveBumpX,
	bumpY: curveBumpY,
	bump: curveBumpX,
	linear: curveLinear,
	linearClosed: curveLinearClosed as CurveFactory,
	natural: curveNatural,
	monotoneX: curveMonotoneX,
	monotoneY: curveMonotoneY,
	monotone: curveMonotoneX,
	step: curveStep,
	stepBefore: curveStepBefore,
	stepAfter: curveStepAfter
};

/**
 * Resolve a Recharts `type` string to the d3-shape curve LayerChart marks expect.
 *
 * The three closed/open variants are line-only in d3's typings; they are cast to `CurveFactory`
 * because Recharts accepts them on `<Area type>` as well and behaves the same way — the closing
 * segment simply has no area counterpart.
 */
export function resolveCurve(type: CurveType | undefined): CurveFactory {
	return CURVES[type ?? 'linear'] ?? curveLinear;
}

export const CURVE_TYPES = Object.keys(CURVES) as CurveType[];
```

`$lib/components/evilcharts/ui/layerchart-chart/format.ts`

```ts
// Format values to percent for expanded charts
export function axisValueToPercentFormatter(value: number) {
	return `${Math.round(value * 100).toFixed(0)}%`;
}
```

`$lib/components/evilcharts/ui/layerchart-chart/index.ts`

```ts
export { default as ChartContainer } from './chart-container.svelte';
export { default as ChartStyle } from './chart-style.svelte';
export { default as LoadingIndicator } from './loading-indicator.svelte';
export type { ChartAccessibility } from './accessibility.js';

export {
	THEMES,
	VALID_THEME_KEYS,
	validateChartConfigColors,
	type ChartConfig,
	type ThemeKey,
	type ValidateConfigKeys
} from './chart-config.js';
export { setChartContext, useChart, type ChartContextValue } from './chart-context.svelte.js';
export { distributeColors, getColorsCount } from './colors.js';
export { getPayloadConfigFromPayload } from './payload.js';
export { axisValueToPercentFormatter } from './format.js';
export { getLoadingData, LOADING_CATEGORY_DATA_KEY } from './loading.js';
export { resolveCurve, CURVE_TYPES, type CurveType } from './curves.js';
export { getBarPositions, type BarSlot, type BarInsets } from './bar-geometry.js';
export { dropOverflowingLeadTick, rechartsValueAxisTicks, thinAxisTicks } from './ticks.js';
```

`$lib/components/evilcharts/ui/layerchart-chart/intros.ts`

```ts
export type IntroAction = 'reset' | 'animate' | 'finish' | 'none';

/** Decides how a polar mark responds to the chart loading lifecycle. */
export function polarIntroAction(
	wasLoading: boolean | undefined,
	isLoading: boolean,
	reduceMotion: boolean
): IntroAction {
	if (isLoading) return 'reset';
	if (reduceMotion) return 'finish';
	if (wasLoading === undefined || wasLoading) return 'animate';
	return 'none';
}

/**
 * Builds a one-shot wipe animation anchored to the chart root's mount timestamp.
 * Keyed LayerChart remounts therefore resume at elapsed progress and can never jump backwards.
 */
export function getRevealAnimation(
	durationSeconds: number,
	ease: [number, number, number, number],
	startedAt: number,
	now = Date.now()
) {
	const durationMs = durationSeconds * 1000;
	const elapsed = Math.max(0, now - startedAt);
	if (elapsed >= durationMs) return null;

	const progress = durationMs > 0 ? elapsed / durationMs : 1;
	return {
		initial: { scaleX: progress },
		animate: { scaleX: 1 },
		transition: {
			duration: Math.max(0, durationSeconds - elapsed / 1000),
			ease
		}
	};
}
```

`$lib/components/evilcharts/ui/layerchart-chart/loading-indicator.svelte`

```svelte
<script lang="ts">
	let { isLoading }: { isLoading: boolean } = $props();
</script>

{#if isLoading}
	<div class="pointer-events-none absolute inset-0 z-20 flex items-center justify-center">
		<div
			class="flex items-center justify-center gap-2 rounded-md border bg-background px-2 py-0.5 text-sm text-primary"
		>
			<div
				class="h-3 w-3 animate-spin rounded-full border border-border border-t-primary motion-reduce:animate-none"
			></div>
			<span>Loading</span>
		</div>
	</div>
{/if}
```

`$lib/components/evilcharts/ui/layerchart-chart/loading.ts`

```ts
// Generate random loading data for skeleton/loading state
// min/max represent percentage of the range (0-100), defaults to 20-80 for realistic look
/** Internal ordinal key that lets LayerChart spread generated loading rows across a category scale. */
export const LOADING_CATEGORY_DATA_KEY = '__loadingCategory';

export const getLoadingData = (points: number = 10, min: number = 0, max: number = 70) => {
	const range = max - min;
	return Array.from({ length: points }, (_, index) => ({
		[LOADING_CATEGORY_DATA_KEY]: index,
		loading: Math.floor(Math.random() * range) + min
	}));
};
```

`$lib/components/evilcharts/ui/layerchart-chart/payload.ts`

```ts
import type { ChartConfig } from './chart-config.js';

// Helper to extract item config from a payload.
export function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {
	if (typeof payload !== 'object' || payload === null) {
		return undefined;
	}

	const payloadPayload =
		'payload' in payload && typeof payload.payload === 'object' && payload.payload !== null
			? payload.payload
			: undefined;

	let configLabelKey: string = key;

	if (key in payload && typeof payload[key as keyof typeof payload] === 'string') {
		configLabelKey = payload[key as keyof typeof payload] as string;
	} else if (
		payloadPayload &&
		key in payloadPayload &&
		typeof payloadPayload[key as keyof typeof payloadPayload] === 'string'
	) {
		configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string;
	}

	return configLabelKey in config ? config[configLabelKey] : config[key];
}
```

`$lib/components/evilcharts/ui/layerchart-chart/ticks.ts`

```ts
import type { AnyScale } from 'layerchart';

/** Converts LayerChart's x-axis tick origin to Recharts' `tickMargin` baseline. */
export const RECHARTS_X_AXIS_TICK_OFFSET = 5.5;

/**
 * Axis tick values with a leading tick dropped when its label cannot fit inside the plot.
 *
 * Recharts' `<XAxis>` runs `interval="preserveEnd"`: it keeps the last tick unconditionally and
 * discards any earlier one that will not fit. For a **point** scale with no outer padding that
 * always means the first tick — its label is centred on the plot's very left edge, so half of it
 * would spill outside. A **band** scale centres the label in the band instead, half a step in, so
 * nothing is dropped. LayerChart draws every tick it is given, so the same filter is applied here.
 *
 * Pass it straight to `<Axis ticks={dropOverflowingLeadTick}>`.
 */
export function dropOverflowingLeadTick(scale: AnyScale): unknown[] {
	const values = scale.domain() as unknown[];
	if (values.length < 2) return values;

	const range = scale.range() as number[];
	const start = Math.min(...range);

	// A band scale reports its band's leading edge, but the label sits at the band's centre.
	const bandOffset =
		typeof (scale as { bandwidth?: () => number }).bandwidth === 'function'
			? ((scale as { bandwidth: () => number }).bandwidth() ?? 0) / 2
			: 0;

	const first = (scale as (value: unknown) => number | undefined)(values[0]);
	if (typeof first !== 'number') return values;

	// Less than a pixel of room means the label is centred on the boundary itself.
	return first + bandOffset - start < 1 ? values.slice(1) : values;
}

/**
 * Axis tick values thinned so their labels do not collide, the way Recharts' `interval="preserveEnd"`
 * does: it keeps the last tick and walks backwards, dropping any tick whose label would come within
 * `minTickGap` of the one already kept. LayerChart's `tickSpacing` cannot do this — it only derives a
 * tick *count*, and it is disabled outright for band scales.
 *
 * Recharts measures real text; there is no rendered text to measure before the axis draws, so the
 * width is estimated from the label's length. `charWidth` defaults to a 12px monospace-ish advance,
 * which is what these axes use.
 *
 * Pass it to `<Axis ticks={…}>`; it also applies `dropOverflowingLeadTick`'s boundary rule.
 */
export function thinAxisTicks({
	format,
	minGap = 5,
	charWidth = 6.6,
	leadingInset = 0
}: {
	/** Renders a domain value the way the axis will, so its width can be estimated. */
	format: (value: unknown, index: number) => string;
	/** Recharts' `minTickGap`, which defaults to 5. */
	minGap?: number;
	/** Estimated advance per character, in pixels. */
	charWidth?: number;
	/** Space between the SVG edge and the scale range (for example a rendered Y axis). */
	leadingInset?: number;
}) {
	return (scale: AnyScale): unknown[] => {
		const domain = scale.domain() as unknown[];
		if (domain.length < 2) return domain;

		const bandOffset =
			typeof (scale as { bandwidth?: () => number }).bandwidth === 'function'
				? ((scale as { bandwidth: () => number }).bandwidth() ?? 0) / 2
				: 0;

		const centreOf = (value: unknown) =>
			Number((scale as (v: unknown) => number)(value)) + bandOffset;
		const halfWidthOf = (value: unknown) =>
			(format(
				value,
				domain.findIndex((candidate) => Object.is(candidate, value))
			).length *
				charWidth) /
			2;
		const range = scale.range() as number[];
		const endBoundary = Math.max(...range);

		// Recharts moves the final label just far enough inward for its trailing edge to stay inside
		// the axis view box. That shifted label then owns the collision boundary, which is why a
		// narrow Jan–Dec axis keeps Dec but drops Nov even though the unshifted labels would fit.
		const kept: unknown[] = [];
		let nextHeadEdge = Number.POSITIVE_INFINITY;

		for (let index = domain.length - 1; index >= 0; index -= 1) {
			const value = domain[index];
			const half = halfWidthOf(value);
			const centre = centreOf(value);

			// The first point-scale label may use space before the plot when a Y axis has reserved it.
			// Clip against the SVG's physical leading edge (0), not the scale range's first position.
			if (index === 0 && leadingInset + centre - half < 0) continue;

			const adjustedCentre =
				index === domain.length - 1 ? Math.min(centre, endBoundary - half) : centre;
			const tail = adjustedCentre + half;

			if (tail + minGap <= nextHeadEdge) {
				kept.push(value);
				nextHeadEdge = adjustedCentre - half;
			}
		}

		return kept.reverse();
	};
}

/**
 * Recharts' numeric axes default to five ticks and include both ends of the resolved domain.
 * D3's `scale.ticks(5)` instead chooses a rounded step and can omit the upper endpoint (for
 * example `[0, 500, 1000, 1500]` for a `[0, 1800]` domain), so LayerChart needs explicit values.
 */
export function rechartsValueAxisTicks(scale: AnyScale, count = 5): unknown[] {
	const domain = scale.domain() as unknown[];
	const start = Number(domain[0]);
	const end = Number(domain.at(-1));
	if (!Number.isFinite(start) || !Number.isFinite(end) || count < 2) return domain;

	const step = (end - start) / (count - 1);
	return Array.from({ length: count }, (_, index) =>
		Number((start + step * index).toPrecision(12))
	);
}

/** Recharts' hidden 6px tick length still contributes to an auto-sized Y-axis. */
export const RECHARTS_VALUE_AXIS_TICK_LENGTH = 6;

/**
 * Resolves Recharts' `width="auto"` gutter from already measured tick labels.
 *
 * Recharts rounds the widest label to the nearest pixel, then adds the configured tick margin and
 * the default 6px tick length (even when `tickLine={false}`). Keeping this as a pure helper makes
 * the browser-only canvas measurement easy to test independently.
 */
export function rechartsAutoYAxisWidth(
	labelWidths: number[],
	tickMargin = 8,
	tickLength = RECHARTS_VALUE_AXIS_TICK_LENGTH
): number {
	return Math.round(Math.max(0, ...labelWidths) + tickMargin + tickLength);
}

/** Measures value-axis labels in the same 12px inherited font used by the chart container. */
export function measureRechartsYAxisWidth(labels: string[], tickMargin = 8): number {
	if (typeof document === 'undefined') return 42;

	const canvas = document.createElement('canvas');
	const context = canvas.getContext('2d');
	if (!context) return 42;

	const family = getComputedStyle(document.body).fontFamily;
	context.font = `400 12px ${family}`;
	return rechartsAutoYAxisWidth(
		labels.map((label) => context.measureText(label).width),
		tickMargin
	);
}

/**
 * Keeps the second argument that LayerChart supplies to format functions at runtime.
 *
 * Its public `FormatType` currently describes a single-argument callback even though Axis invokes
 * it as `format(tick, index)`. Recharts exposes that index, so this adapter keeps the runtime value
 * while remaining assignable to LayerChart's narrower callback type.
 */
export function layerChartFormatter(
	formatter: (value: unknown, index: number) => string
): (value: unknown, index?: number) => string {
	return (value, index = 0) => formatter(value, index);
}
```
        
      
       
        ### Add the sub-components.
        

Create `tooltip.svelte` inside `evilcharts/ui` and paste the code there.


        
          ### $lib/components/evilcharts/ui/layerchart-tooltip

`$lib/components/evilcharts/ui/layerchart-tooltip/index.ts`

```ts
export { default as ChartTooltip } from './tooltip.svelte';
export { default as ChartTooltipContent } from './tooltip-content.svelte';
export { getIndicatorColorStyle, roundnessMap, variantMap } from './styles.js';
export type {
	TooltipIndicator,
	TooltipPayloadItem,
	TooltipRoundness,
	TooltipVariant
} from './types.js';
```

`$lib/components/evilcharts/ui/layerchart-tooltip/styles.ts`

```ts
import { getColorsCount } from '../layerchart-chart/colors.js';
import type { ChartConfig } from '../layerchart-chart/chart-config.js';
import type { TooltipRoundness, TooltipVariant } from './types.js';

export const roundnessMap: Record<TooltipRoundness, string> = {
	sm: 'rounded-sm',
	md: 'rounded-md',
	lg: 'rounded-lg',
	xl: 'rounded-xl'
};

export const variantMap: Record<TooltipVariant, string> = {
	default: 'bg-background',
	'frosted-glass': 'bg-background/70 backdrop-blur-sm'
};

export function getIndicatorColorStyle(dataKey: string, colorsCount: number): string {
	if (colorsCount <= 1) {
		return `background: var(--color-${dataKey}-0)`;
	}

	// Multiple colors: create linear gradient with evenly distributed stops
	const stops = Array.from({ length: colorsCount }, (_, index) => {
		const offset = (index / (colorsCount - 1)) * 100;
		return `var(--color-${dataKey}-${index}) ${offset}%`;
	}).join(', ');

	return `background: linear-gradient(to right, ${stops})`;
}

export function colorsCountFor(itemConfig: ChartConfig[string] | undefined): number {
	return itemConfig ? getColorsCount(itemConfig) : 1;
}
```

`$lib/components/evilcharts/ui/layerchart-tooltip/tooltip-content.svelte`

```svelte
<script lang="ts">
	import type { Snippet } from 'svelte';
	import { cn } from '$lib/utils.js';
	import { getPayloadConfigFromPayload } from '../layerchart-chart/payload.js';
	import { useChart } from '../layerchart-chart/chart-context.svelte.js';
	import { colorsCountFor, getIndicatorColorStyle, roundnessMap, variantMap } from './styles.js';
	import type { ChartConfig } from '../layerchart-chart/chart-config.js';
	import type {
		TooltipIndicator,
		TooltipPayloadItem,
		TooltipRoundness,
		TooltipVariant
	} from './types.js';

	type Props = {
		active?: boolean;
		payload?: TooltipPayloadItem[];
		class?: string;
		indicator?: TooltipIndicator;
		hideLabel?: boolean;
		hideIndicator?: boolean;
		label?: unknown;
		labelFormatter?: Snippet<[unknown, TooltipPayloadItem[]]>;
		labelClassName?: string;
		formatter?: Snippet<[TooltipPayloadItem['value'], string, TooltipPayloadItem, number, unknown]>;
		nameKey?: string;
		labelKey?: string;
		selected?: string | null;
		roundness?: TooltipRoundness;
		variant?: TooltipVariant;
	};

	let {
		active,
		payload,
		class: className,
		indicator = 'dot',
		hideLabel = false,
		hideIndicator = false,
		label,
		labelFormatter,
		labelClassName,
		formatter,
		nameKey,
		labelKey,
		selected,
		roundness = 'lg',
		variant = 'default'
	}: Props = $props();

	const { config } = $derived(useChart());

	const labelValue = $derived.by(() => {
		if (hideLabel || !payload?.length) {
			return null;
		}

		const [item] = payload;
		const key = `${labelKey ?? item?.dataKey ?? item?.name ?? 'value'}`;
		const itemConfig = getPayloadConfigFromPayload(config, item, key);

		return !labelKey && typeof label === 'string'
			? (config[label]?.label ?? label)
			: itemConfig?.label;
	});

	const nestLabel = $derived(payload?.length === 1 && indicator !== 'dot');

	type Row = {
		item: TooltipPayloadItem;
		index: number;
		key: string;
		itemConfig: ChartConfig[string] | undefined;
		colorsCount: number;
		isDimmed: boolean;
		/** Resolved before rendering so changes to the row re-derive it. */
		icon: ChartConfig[string]['icon'];
	};

	/**
	 * Fully resolved rows.
	 *
	 * Derived as one list rather than with in-markup declarations so a later `selected` change
	 * re-derives every row — the reference recomputes these inline on each React render.
	 */
	const rows = $derived<Row[]>(
		(payload ?? [])
			.filter((item) => item.type !== 'none')
			.map((item, index) => {
				// For pie charts, item.name contains the sector name (e.g., "chrome")
				// For radial charts, the name is in item.payload[nameKey]
				// For other charts, item.name or item.dataKey contains the series name
				const payloadName =
					nameKey && item.payload ? (item.payload as Record<string, unknown>)[nameKey] : undefined;
				const key = `${payloadName ?? item.name ?? item.dataKey ?? 'value'}`;
				const itemConfig = getPayloadConfigFromPayload(config, item, key);

				return {
					item,
					index,
					key,
					itemConfig,
					// Get colors count for this item to determine gradient vs solid
					colorsCount: colorsCountFor(itemConfig),
					isDimmed: selected != null && selected !== item.dataKey,
					icon: itemConfig?.icon
				};
			})
	);
</script>

{#snippet tooltipLabel()}
	{#if !hideLabel && payload?.length}
		{#if labelFormatter}
			<div class={cn('font-medium', labelClassName)}>
				{@render labelFormatter(labelValue, payload)}
			</div>
		{:else if labelValue}
			<div class={cn('font-medium', labelClassName)}>
				{#if typeof labelValue === 'string'}{labelValue}{:else}{@render labelValue()}{/if}
			</div>
		{/if}
	{/if}
{/snippet}

{#if !active || !payload?.length}
	<!-- Empty tooltip - to prevent position getting 0.0 so it doesnt animate tooltip every time from 0.0 origin -->
	<span class="p-4"></span>
{:else}
	<div
		class={cn(
			'grid min-w-32 items-start gap-1.5 border border-border/50 px-2.5 py-1.5 text-xs shadow-xl',
			roundnessMap[roundness],
			variantMap[variant],
			className
		)}
	>
		{#if !nestLabel}{@render tooltipLabel()}{/if}
		<div class="grid gap-1.5">
			{#each rows as row (row.index)}
				<div
					class={cn(
						'flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground',
						indicator === 'dot' && 'items-center',
						row.isDimmed && 'opacity-30'
					)}
				>
					{#if formatter && row.item?.value !== undefined && row.item.name}
						{@render formatter(
							row.item.value,
							row.item.name,
							row.item,
							row.index,
							row.item.payload
						)}
					{:else}
						{#if row.icon}
							{const Icon = row.icon}
							<Icon />
						{:else if !hideIndicator}
							<div
								class={cn('shrink-0 rounded-[2px]', {
									'h-2.5 w-2.5': indicator === 'dot',
									'w-1': indicator === 'line',
									'w-0 border-[1.5px] border-dashed bg-transparent!': indicator === 'dashed',
									'my-0.5': nestLabel && indicator === 'dashed'
								})}
								style={getIndicatorColorStyle(row.key, row.colorsCount)}
							></div>
						{/if}
						<div
							class={cn(
								'flex flex-1 justify-between gap-4 leading-none',
								nestLabel ? 'items-end' : 'items-center'
							)}
						>
							<div class="grid gap-1.5">
								{#if nestLabel}{@render tooltipLabel()}{/if}
								<span class="text-muted-foreground">
									{#if row.itemConfig?.label}
										{#if typeof row.itemConfig.label === 'string'}
											{row.itemConfig.label}
										{:else}
											{@render row.itemConfig.label()}
										{/if}
									{:else}
										{row.item.name}
									{/if}
								</span>
							</div>
							{#if row.item.value != null}
								<span class="font-mono font-medium text-foreground tabular-nums">
									{typeof row.item.value === 'number'
										? row.item.value.toLocaleString()
										: String(row.item.value)}
								</span>
							{/if}
						</div>
					{/if}
				</div>
			{/each}
		</div>
	</div>
{/if}
```

`$lib/components/evilcharts/ui/layerchart-tooltip/tooltip.svelte`

```svelte
<script lang="ts">
	import { Tooltip } from 'layerchart';
	import type { ComponentProps, Snippet } from 'svelte';
	import { useChart } from '../layerchart-chart/index.js';

	type Props = Omit<ComponentProps<typeof Tooltip.Root>, 'children' | 'variant'> & {
		children: Snippet<[{ data: unknown }]>;
	};

	let { children: content, props, ...restProps }: Props = $props();

	const { chartId } = useChart();
</script>

<!-- `variant="none"` drops LayerChart's own tooltip chrome so <ChartTooltipContent> owns the
     entire look, matching the reference where Recharts' `<Tooltip content>` replaces the
     default box. Position motion is left at LayerChart's default spring.

     LayerChart's default body portal escapes overflow boundaries. Repeating the chart's
     `data-chart` value on the portaled root keeps the scoped `--color-*` variables available. -->
<Tooltip.Root
	variant="none"
	{...restProps}
	props={{ ...props, root: { ...props?.root, 'data-chart': chartId } }}
>
	{#snippet children({ data })}
		{@render content({ data })}
	{/snippet}
</Tooltip.Root>
```

`$lib/components/evilcharts/ui/layerchart-tooltip/types.ts`

```ts
export type TooltipRoundness = 'sm' | 'md' | 'lg' | 'xl';
export type TooltipVariant = 'default' | 'frosted-glass';

export type TooltipIndicator = 'line' | 'dot' | 'dashed';

/**
 * One row of tooltip data.
 *
 * Mirrors the shape Recharts hands `<Tooltip content>` in the reference, so the content
 * component stays renderer-agnostic exactly as the reference's is. Each chart's `Tooltip`
 * part builds this list from LayerChart's `TooltipContext`.
 */
export type TooltipPayloadItem = {
	dataKey?: string;
	name?: string;
	value?: number | string | null;
	payload?: unknown;
	/** Rows with `type: 'none'` are hidden, as in Recharts. */
	type?: string;
	color?: string;
};
```
        
        

Then create `legend.svelte` in the same folder and paste the code there.


        
          ### $lib/components/evilcharts/ui/layerchart-legend

`$lib/components/evilcharts/ui/layerchart-legend/index.ts`

```ts
export { default as ChartLegendContent } from './legend-content.svelte';
export { default as LegendIndicator } from './legend-indicator.svelte';
export { getLegendFillStyle, getLegendOutlineStyle } from './styles.js';
export { resolveLegendPlacement } from './types.js';
export type {
	ChartLegendVariant,
	LegendAlign,
	LegendPayloadItem,
	LegendVerticalAlign
} from './types.js';
```

`$lib/components/evilcharts/ui/layerchart-legend/legend-content.svelte`

```svelte
<script lang="ts">
	import { cn } from '$lib/utils.js';
	import { getColorsCount } from '../layerchart-chart/colors.js';
	import { getPayloadConfigFromPayload } from '../layerchart-chart/payload.js';
	import { useChart } from '../layerchart-chart/chart-context.svelte.js';
	import LegendIndicator from './legend-indicator.svelte';
	import type { ChartConfig } from '../layerchart-chart/chart-config.js';
	import type {
		ChartLegendVariant,
		LegendAlign,
		LegendPayloadItem,
		LegendVerticalAlign
	} from './types.js';

	type Props = {
		class?: string;
		hideIcon?: boolean;
		nameKey?: string;
		payload?: LegendPayloadItem[];
		verticalAlign?: LegendVerticalAlign;
		align?: LegendAlign;
		selected?: string | null;
		isClickable?: boolean;
		onSelectChange?: (selected: string | null) => void;
		variant?: ChartLegendVariant;
	};

	let {
		class: className,
		hideIcon = false,
		nameKey,
		payload,
		verticalAlign,
		align = 'right',
		selected,
		isClickable,
		onSelectChange,
		variant = 'rounded-square'
	}: Props = $props();

	const { config } = $derived(useChart());

	type Entry = {
		key: string;
		itemConfig: ChartConfig[string] | undefined;
		colorsCount: number;
		isSelected: boolean;
		/** Resolved before rendering so changes to the entry re-derive it. */
		icon: ChartConfig[string]['icon'];
	};

	/**
	 * Fully resolved entries.
	 *
	 * Everything each row needs is derived here rather than with in-markup declarations, so a later
	 * `selected` change re-derives the whole list — the reference recomputes these inline on every
	 * React render.
	 */
	/**
	 * Recharts' `<Legend>` defaults to `itemSorter="value"`, so its entries come out ordered by
	 * series name rather than in config order — a composed chart of `revenue` + `profit` lists
	 * "Profit" first, and a pie of browsers lists them alphabetically. The comparison matches
	 * lodash's `compareAscending`, which is what `sortBy` uses: plain `<` on the raw value, with
	 * anything missing sorted last. (Its `<Tooltip itemSorter>` only reaches the *default* tooltip
	 * content, so tooltip rows stay in data order — the reference behaves the same way.)
	 */
	function byValueAscending(a: LegendPayloadItem, b: LegendPayloadItem) {
		const left = a.value;
		const right = b.value;
		if (left === right) return 0;
		if (left === undefined) return 1;
		if (right === undefined) return -1;
		return left < right ? -1 : 1;
	}

	const entries = $derived<Entry[]>(
		(payload ?? [])
			.filter((item) => item.type !== 'none')
			.toSorted(byValueAscending)
			.map((item) => {
				// For pie charts, item.value contains the sector name (e.g., "chrome")
				// For radial charts, the name is in item.payload[nameKey]
				// For other charts, item.dataKey contains the series name (e.g., "desktop")
				const payloadName =
					nameKey && item.payload ? (item.payload as Record<string, unknown>)[nameKey] : undefined;
				const key = `${payloadName ?? item.value ?? item.dataKey ?? 'value'}`;
				const itemConfig = getPayloadConfigFromPayload(config, item, key);

				return {
					key,
					itemConfig,
					// Get colors count for this item to determine gradient vs solid
					colorsCount: itemConfig ? getColorsCount(itemConfig) : 1,
					isSelected: selected === null || selected === undefined || selected === key,
					icon: itemConfig?.icon
				};
			})
	);

	function entryClass(isSelected: boolean) {
		return cn(
			'[&>svg]:text-muted-foreground flex items-center gap-1.5 transition-opacity [&>svg]:h-3 [&>svg]:w-3',
			!isSelected && 'opacity-30',
			isClickable && 'cursor-pointer'
		);
	}

	function select(key: string) {
		if (!isClickable) return;

		onSelectChange?.(selected === key ? null : key);
	}
</script>

{#snippet indicator(entry: Entry)}
	{#if entry.icon && !hideIcon}
		{const Icon = entry.icon}
		<Icon />
	{:else}
		<LegendIndicator {variant} dataKey={entry.key} colorsCount={entry.colorsCount} />
	{/if}
	{#if entry.itemConfig?.label}
		{#if typeof entry.itemConfig.label === 'string'}
			{entry.itemConfig.label}
		{:else}
			{@render entry.itemConfig.label()}
		{/if}
	{/if}
{/snippet}

{#if entries.length}
	<div
		class={cn(
			'relative z-10 flex items-center gap-4 select-none',
			align === 'left' && 'justify-start',
			align === 'center' && 'justify-center',
			align === 'right' && 'justify-end',
			verticalAlign === 'top' && 'pb-4',
			verticalAlign === 'bottom' && 'pt-4',
			className
		)}
	>
		{#each entries as entry (entry.key)}
			{#if isClickable}
				<!-- The reference makes the entry clickable with a bare onClick. Keyboard support is
				     added here without changing the rendered class list. -->
				<button
					type="button"
					class={entryClass(entry.isSelected)}
					aria-pressed={selected === entry.key}
					onclick={() => select(entry.key)}
				>
					{@render indicator(entry)}
				</button>
			{:else}
				<div class={entryClass(entry.isSelected)}>
					{@render indicator(entry)}
				</div>
			{/if}
		{/each}
	</div>
{/if}
```

`$lib/components/evilcharts/ui/layerchart-legend/legend-indicator.svelte`

```svelte
<script lang="ts">
	// ---------------------------------------------------------------------------
	// Legend indicator — each variant gets its own branch so future variants
	// can diverge freely in markup & style.
	// ---------------------------------------------------------------------------
	import { getLegendFillStyle, getLegendOutlineStyle } from './styles.js';
	import type { ChartLegendVariant } from './types.js';

	let {
		variant,
		dataKey,
		colorsCount
	}: { variant: ChartLegendVariant; dataKey: string; colorsCount: number } = $props();

	const fillStyle = $derived(getLegendFillStyle(dataKey, colorsCount));
	const outlineStyle = $derived(getLegendOutlineStyle(dataKey, colorsCount));
</script>

{#if variant === 'square'}
	<div class="h-2 w-2 shrink-0" style={fillStyle}></div>
{:else if variant === 'circle'}
	<div class="h-2 w-2 shrink-0 rounded-full" style={fillStyle}></div>
{:else if variant === 'circle-outline'}
	<div class="h-2.5 w-2.5 shrink-0 rounded-full p-[1.5px]" style={outlineStyle}></div>
{:else if variant === 'vertical-bar'}
	<div class="h-3 w-1 shrink-0 rounded-[2px]" style={fillStyle}></div>
{:else if variant === 'horizontal-bar'}
	<div class="h-1 w-3 shrink-0 rounded-[2px]" style={fillStyle}></div>
{:else if variant === 'rounded-square-outline'}
	<div class="h-2.5 w-2.5 shrink-0 rounded-[3px] p-[1.5px]" style={outlineStyle}></div>
{:else}
	<div class="h-2 w-2 shrink-0 rounded-[2px]" style={fillStyle}></div>
{/if}
```

`$lib/components/evilcharts/ui/layerchart-legend/styles.ts`

```ts
/** Solid fill / gradient background for filled variants. */
export function getLegendFillStyle(dataKey: string, colorsCount: number): string {
	if (colorsCount <= 1) {
		return `background-color: var(--color-${dataKey}-0)`;
	}

	const stops = Array.from({ length: colorsCount }, (_, i) => {
		const offset = (i / (colorsCount - 1)) * 100;
		return `var(--color-${dataKey}-${i}) ${offset}%`;
	}).join(', ');

	return `background: linear-gradient(to right, ${stops})`;
}

/**
 * Outline style for stroke variants.
 * Uses background + mask-composite to punch out the center, leaving only the
 * "border" visible. Works with both solid colors and gradients, and respects
 * border-radius — unlike plain `border-color`.
 */
export function getLegendOutlineStyle(dataKey: string, colorsCount: number): string {
	const maskStyle = [
		'-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)',
		'-webkit-mask-composite: xor',
		'mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)',
		'mask-composite: exclude'
	].join('; ');

	if (colorsCount <= 1) {
		return `background-color: var(--color-${dataKey}-0); ${maskStyle}`;
	}

	const stops = Array.from({ length: colorsCount }, (_, i) => {
		const offset = (i / (colorsCount - 1)) * 100;
		return `var(--color-${dataKey}-${i}) ${offset}%`;
	}).join(', ');

	return `background: linear-gradient(to right, ${stops}); ${maskStyle}`;
}
```

`$lib/components/evilcharts/ui/layerchart-legend/types.ts`

```ts
export type ChartLegendVariant =
	| 'square'
	| 'circle'
	| 'circle-outline'
	| 'rounded-square'
	| 'rounded-square-outline'
	| 'vertical-bar'
	| 'horizontal-bar';

export type LegendAlign = 'left' | 'center' | 'right';
export type LegendVerticalAlign = 'top' | 'middle' | 'bottom';

export function resolveLegendPlacement(
	requested: LegendVerticalAlign | undefined,
	fallback: 'top' | 'bottom'
): LegendVerticalAlign {
	return requested ?? fallback;
}

/**
 * One legend entry.
 *
 * Mirrors the shape Recharts hands `<Legend content>` in the reference: `value` carries the
 * series name for pie charts, `dataKey` for cartesian charts, and `payload` the raw row so
 * `nameKey` can reach into it.
 */
export type LegendPayloadItem = {
	value?: string;
	dataKey?: string;
	payload?: unknown;
	/** Entries typed `'none'` are hidden, as in Recharts. */
	type?: string;
	color?: string;
};
```
        
        

Finally, create `dot.svelte` in the same folder and paste the code there.


        
          ### $lib/components/evilcharts/ui/layerchart-dot

`$lib/components/evilcharts/ui/layerchart-dot/chart-dot.svelte`

```svelte
<script lang="ts">
	// The reference wraps each variant in `React.memo`; Svelte's fine-grained updates make that
	// unnecessary, so the memo boundaries are dropped and nothing else changes.
	import ColoredBorderDot from './colored-border-dot.svelte';
	import DefaultDot from './default-dot.svelte';
	import PrimaryBorderDot from './primary-border-dot.svelte';
	import type { ChartDotProps } from './types.js';

	let {
		cx,
		cy,
		dataKey,
		chartId,
		class: className,
		fillOpacity = 1,
		type = 'default',
		maskId,
		gradientX = 0,
		gradientWidth = '100%'
	}: ChartDotProps = $props();

	const dotId = $props.id();
	const gradientUrl = $derived(`url(#${chartId}-colors-${String(dataKey)})`);
</script>

{#if cx !== undefined && cy !== undefined}
	{#if type === 'border'}
		<PrimaryBorderDot
			{cx}
			{cy}
			{dotId}
			{fillOpacity}
			{gradientUrl}
			class={className}
			{maskId}
			{gradientX}
			{gradientWidth}
		/>
	{:else if type === 'colored-border'}
		<ColoredBorderDot
			{cx}
			{cy}
			{dotId}
			{fillOpacity}
			{gradientUrl}
			class={className}
			{maskId}
			{gradientX}
			{gradientWidth}
		/>
	{:else}
		<DefaultDot
			{cx}
			{cy}
			{dotId}
			{fillOpacity}
			{gradientUrl}
			class={className}
			{maskId}
			{gradientX}
			{gradientWidth}
		/>
	{/if}
{/if}
```

`$lib/components/evilcharts/ui/layerchart-dot/colored-border-dot.svelte`

```svelte
<script lang="ts">
	import { cn } from '$lib/utils.js';
	import type { DotVariantProps } from './types.js';

	let {
		cx,
		cy,
		dotId,
		fillOpacity,
		gradientUrl,
		class: className,
		maskId,
		gradientX,
		gradientWidth
	}: DotVariantProps = $props();

	const r = 3;
	const strokeWidth = 1;
</script>

<g class={cn(className, 'text-background')} mask={maskId ? `url(#${maskId})` : undefined}>
	<defs>
		<clipPath id={`dot-clip-${dotId}`}>
			<circle {cx} {cy} r={r + strokeWidth / 2} />
		</clipPath>
	</defs>
	<!-- Gradient stroke (border) via clipped rect -->
	<rect
		x={gradientX}
		y={cy - r - strokeWidth / 2}
		width={gradientWidth}
		height={(r + strokeWidth / 2) * 2}
		fill={gradientUrl}
		fill-opacity={fillOpacity}
		clip-path={`url(#dot-clip-${dotId})`}
	/>
	<!-- Inner solid fill -->
	<circle {cx} {cy} r={r - strokeWidth / 2} fill="currentColor" />
</g>
```

`$lib/components/evilcharts/ui/layerchart-dot/default-dot.svelte`

```svelte
<script lang="ts">
	import type { DotVariantProps } from './types.js';

	let {
		cx,
		cy,
		dotId,
		fillOpacity,
		gradientUrl,
		class: className,
		maskId,
		gradientX,
		gradientWidth
	}: DotVariantProps = $props();

	const r = 3;
</script>

<g class={className} mask={maskId ? `url(#${maskId})` : undefined}>
	<defs>
		<clipPath id={`dot-clip-${dotId}`}>
			<circle {cx} {cy} {r} />
		</clipPath>
	</defs>
	<!-- Full-width gradient rectangle clipped to dot shape -->
	<rect
		x={gradientX}
		y={cy - r}
		width={gradientWidth}
		height={r * 2}
		fill={gradientUrl}
		fill-opacity={fillOpacity}
		clip-path={`url(#dot-clip-${dotId})`}
	/>
</g>
```

`$lib/components/evilcharts/ui/layerchart-dot/index.ts`

```ts
export { default as ChartDot } from './chart-dot.svelte';
export type { ChartDotProps, DotVariant, DotVariantProps } from './types.js';
```

`$lib/components/evilcharts/ui/layerchart-dot/primary-border-dot.svelte`

```svelte
<script lang="ts">
	import { cn } from '$lib/utils.js';
	import type { DotVariantProps } from './types.js';

	let {
		cx,
		cy,
		dotId,
		fillOpacity,
		gradientUrl,
		class: className,
		maskId,
		gradientX,
		gradientWidth
	}: DotVariantProps = $props();

	const r = 6;
	const strokeWidth = 5;
</script>

<g class={cn(className, 'text-background')} mask={maskId ? `url(#${maskId})` : undefined}>
	<defs>
		<clipPath id={`dot-clip-${dotId}`}>
			<circle {cx} {cy} {r} />
		</clipPath>
	</defs>
	<!-- Background stroke (border) -->
	<circle {cx} {cy} {r} fill="currentColor" />
	<!-- Inner gradient circle clipped -->
	<rect
		x={gradientX}
		y={cy - (r - strokeWidth / 2)}
		width={gradientWidth}
		height={(r - strokeWidth / 2) * 2}
		fill={gradientUrl}
		fill-opacity={fillOpacity}
		clip-path={`url(#dot-clip-inner-${dotId})`}
	/>
	<defs>
		<clipPath id={`dot-clip-inner-${dotId}`}>
			<circle {cx} {cy} r={r - strokeWidth / 2} />
		</clipPath>
	</defs>
</g>
```

`$lib/components/evilcharts/ui/layerchart-dot/types.ts`

```ts
export type DotVariant = 'default' | 'border' | 'colored-border';

export type ChartDotProps = {
	cx?: number;
	cy?: number;
	dataKey: string;
	chartId: string;
	class?: string;
	fillOpacity?: number;
	type?: DotVariant;
	/** Optional SVG <mask> id — lets the dot share an area's intro reveal wipe. */
	maskId?: string;
	/**
	 * Left edge and width of the gradient rect each dot is clipped out of.
	 *
	 * The reference draws a plot-wide rect and clips it to a circle, so a dot samples the series'
	 * horizontal gradient at its own x. That assumes the origin is the plot's left edge, which is
	 * true in a cartesian chart but not inside a centred `<Group>` — there, `x="0"` starts at the
	 * *centre* and every dot left of it disappeared. A radial chart passes its own plot span.
	 */
	gradientX?: number | string;
	gradientWidth?: number | string;
};

export type DotVariantProps = {
	cx: number;
	cy: number;
	dotId: string;
	fillOpacity: number;
	gradientUrl: string;
	class?: string;
	maskId?: string;
	gradientX: number | string;
	gradientWidth: number | string;
};
```
        
      
    
  


## Usage

`<EvilComposedChart>` is the container; compose the parts you need — `<EvilComposedChart.Grid>`, `<EvilComposedChart.XAxis>`, `<EvilComposedChart.YAxis>`, `<EvilComposedChart.Legend>`, `<EvilComposedChart.Tooltip>`, and one or more `<EvilComposedChart.Bar>` and `<EvilComposedChart.Line>` — as children. Each `<Bar>` carries its own `variant`, `glow`, and `isClickable`; each `<Line>` its own `strokeVariant`, `curveType`, `glow`, and `isClickable`, so one chart can mix bar and line styles freely.

```svelte
<script lang="ts">
	import { EvilComposedChart } from '$lib/components/evilcharts/charts/layerchart-composed-chart';
	import { type ChartConfig } from '$lib/components/evilcharts/ui/layerchart-chart';
</script>
```

```svelte
<script lang="ts">
	const chartConfig = {
		revenue: {
			label: 'Revenue',
			colors: { light: ['#3b82f6'], dark: ['#6A5ACD'] }
		},
		profit: {
			label: 'Profit',
			colors: { light: ['#10b981'], dark: ['#34d399'] }
		}
	} satisfies ChartConfig;
</script>

<EvilComposedChart xDataKey="month" {data} config={chartConfig}>
	<EvilComposedChart.Grid />
	<EvilComposedChart.XAxis dataKey="month" />
	<EvilComposedChart.YAxis />
	<EvilComposedChart.Legend isClickable />
	<EvilComposedChart.Tooltip />
	<EvilComposedChart.Bar dataKey="revenue" variant="gradient" isClickable />
	<EvilComposedChart.Line dataKey="profit" strokeVariant="dashed" isClickable>
		<EvilComposedChart.Dot variant="default" />
		<EvilComposedChart.ActiveDot variant="colored-border" />
	</EvilComposedChart.Line>
</EvilComposedChart>
```

### Interactive Selection

Add `isClickable` to any `<Bar>`, `<Line>`, or `<Legend>` to make those series selectable. Handle selection with the `onSelectionChange` callback on `<EvilComposedChart>`:

```svelte
<EvilComposedChart
	{data}
	config={chartConfig}
	onSelectionChange={(selectedDataKey) => {
		if (selectedDataKey) {
			console.log('Selected:', selectedDataKey);
		} else {
			console.log('Deselected');
		}
	}}
>
	<EvilComposedChart.XAxis dataKey="month" />
	<EvilComposedChart.Legend isClickable />
	<EvilComposedChart.Tooltip />
	<EvilComposedChart.Bar dataKey="revenue" isClickable />
	<EvilComposedChart.Line dataKey="profit" isClickable />
</EvilComposedChart>
```

### Loading State

### isLoading={true}

```svelte
<script lang="ts">
	import { EvilComposedChart } from '$lib/components/evilcharts/charts/layerchart-composed-chart/index.js';
	import { type ChartConfig } from '$lib/components/evilcharts/ui/layerchart-chart/index.js';

	const data = [
		{ month: 'January', revenue: 4200, profit: 1800 },
		{ month: 'February', revenue: 5800, profit: 2400 },
		{ month: 'March', revenue: 4100, profit: 1600 },
		{ month: 'April', revenue: 6200, profit: 2800 },
		{ month: 'May', revenue: 5400, profit: 2200 },
		{ month: 'June', revenue: 7800, profit: 3400 }
	];

	const chartConfig = {
		revenue: {
			label: 'Revenue',
			colors: {
				light: ['#3b82f6'],
				dark: ['#6A5ACD']
			}
		},
		profit: {
			label: 'Profit',
			colors: {
				light: ['#10b981'],
				dark: ['#34d399']
			}
		}
	} satisfies ChartConfig;
</script>

<EvilComposedChart isLoading class="h-full w-full p-4" xDataKey="month" {data} config={chartConfig}>
	<EvilComposedChart.Grid />
	<EvilComposedChart.XAxis
		dataKey="month"
		tickFormatter={(value) => String(value).substring(0, 3)}
	/>
	<EvilComposedChart.Legend />
	<EvilComposedChart.Tooltip />
	<EvilComposedChart.Bar dataKey="revenue" />
	<EvilComposedChart.Line dataKey="profit" />
</EvilComposedChart>
```
>  
  

Pass the `isLoading` prop to show a shimmer skeleton while your data is being fetched.




## Examples

Customize each `<Bar>` with a `variant`, and each `<Line>` with a `strokeVariant`, `curveType`, and more.

### Gradient Colors

### gradient colors

```svelte
<script lang="ts">
	import { EvilComposedChart } from '$lib/components/evilcharts/charts/layerchart-composed-chart/index.js';
	import { type ChartConfig } from '$lib/components/evilcharts/ui/layerchart-chart/index.js';

	const data = [
		{ month: 'January', revenue: 4200, profit: 1800 },
		{ month: 'February', revenue: 5800, profit: 2400 },
		{ month: 'March', revenue: 4100, profit: 1600 },
		{ month: 'April', revenue: 6200, profit: 2800 },
		{ month: 'May', revenue: 5400, profit: 2200 },
		{ month: 'June', revenue: 7800, profit: 3400 },
		{ month: 'July', revenue: 6100, profit: 2600 },
		{ month: 'August', revenue: 8200, profit: 3800 },
		{ month: 'September', revenue: 5900, profit: 2500 },
		{ month: 'October', revenue: 6800, profit: 3000 },
		{ month: 'November', revenue: 7200, profit: 3200 },
		{ month: 'December', revenue: 9100, profit: 4200 }
	];

	const chartConfig = {
		revenue: {
			label: 'Revenue',
			colors: {
				// [!code highlight:2]
				light: ['#f43f5e', '#ec4899', '#a855f7', '#6366f1', '#3b82f6'],
				dark: ['#f43f5e', '#ec4899', '#a855f7', '#6366f1', '#3b82f6']
			}
		},
		profit: {
			label: 'Profit',
			colors: {
				// [!code highlight:2]
				light: ['#10b981', '#14b8a6', '#06b6d4'],
				dark: ['#10b981', '#14b8a6', '#06b6d4']
			}
		}
	} satisfies ChartConfig;
</script>

<EvilComposedChart class="h-full w-full p-4" xDataKey="month" {data} config={chartConfig}>
	<EvilComposedChart.Grid />
	<EvilComposedChart.XAxis
		dataKey="month"
		tickFormatter={(value) => String(value).substring(0, 3)}
	/>
	<EvilComposedChart.Legend isClickable />
	<EvilComposedChart.Tooltip />
	<EvilComposedChart.Bar dataKey="revenue" isClickable />
	<EvilComposedChart.Line dataKey="profit" isClickable />
</EvilComposedChart>
```

### Bar Variants

### <Bar variant='hatched' />

```svelte
<script lang="ts">
	import { EvilComposedChart } from '$lib/components/evilcharts/charts/layerchart-composed-chart/index.js';
	import { type ChartConfig } from '$lib/components/evilcharts/ui/layerchart-chart/index.js';

	const data = [
		{ month: 'January', revenue: 4200, profit: 1800 },
		{ month: 'February', revenue: 5800, profit: 2400 },
		{ month: 'March', revenue: 4100, profit: 1600 },
		{ month: 'April', revenue: 6200, profit: 2800 },
		{ month: 'May', revenue: 5400, profit: 2200 },
		{ month: 'June', revenue: 7800, profit: 3400 },
		{ month: 'July', revenue: 6100, profit: 2600 },
		{ month: 'August', revenue: 8200, profit: 3800 },
		{ month: 'September', revenue: 5900, profit: 2500 },
		{ month: 'October', revenue: 6800, profit: 3000 },
		{ month: 'November', revenue: 7200, profit: 3200 },
		{ month: 'December', revenue: 9100, profit: 4200 }
	];

	const chartConfig = {
		revenue: {
			label: 'Revenue',
			colors: {
				light: ['#3b82f6'],
				dark: ['#6A5ACD']
			}
		},
		profit: {
			label: 'Profit',
			colors: {
				light: ['#10b981'],
				dark: ['#34d399']
			}
		}
	} satisfies ChartConfig;
</script>

<EvilComposedChart class="h-full w-full p-4" xDataKey="month" {data} config={chartConfig}>
	<EvilComposedChart.Grid />
	<EvilComposedChart.XAxis
		dataKey="month"
		tickFormatter={(value) => String(value).substring(0, 3)}
	/>
	<EvilComposedChart.Legend isClickable />
	<EvilComposedChart.Tooltip />
	<!-- [!code highlight:3] -->
	<EvilComposedChart.Bar dataKey="revenue" variant="hatched" isClickable />
	<EvilComposedChart.Line dataKey="profit" isClickable />
</EvilComposedChart>
```
### <Bar variant='duotone' />

```svelte
<script lang="ts">
	import { EvilComposedChart } from '$lib/components/evilcharts/charts/layerchart-composed-chart/index.js';
	import { type ChartConfig } from '$lib/components/evilcharts/ui/layerchart-chart/index.js';

	const data = [
		{ month: 'January', revenue: 4200, profit: 1800 },
		{ month: 'February', revenue: 5800, profit: 2400 },
		{ month: 'March', revenue: 4100, profit: 1600 },
		{ month: 'April', revenue: 6200, profit: 2800 },
		{ month: 'May', revenue: 5400, profit: 2200 },
		{ month: 'June', revenue: 7800, profit: 3400 },
		{ month: 'July', revenue: 6100, profit: 2600 },
		{ month: 'August', revenue: 8200, profit: 3800 },
		{ month: 'September', revenue: 5900, profit: 2500 },
		{ month: 'October', revenue: 6800, profit: 3000 },
		{ month: 'November', revenue: 7200, profit: 3200 },
		{ month: 'December', revenue: 9100, profit: 4200 }
	];

	const chartConfig = {
		revenue: {
			label: 'Revenue',
			colors: {
				light: ['#3b82f6'],
				dark: ['#6A5ACD']
			}
		},
		profit: {
			label: 'Profit',
			colors: {
				light: ['#10b981'],
				dark: ['#34d399']
			}
		}
	} satisfies ChartConfig;
</script>

<EvilComposedChart class="h-full w-full p-4" xDataKey="month" {data} config={chartConfig}>
	<EvilComposedChart.Grid />
	<EvilComposedChart.XAxis
		dataKey="month"
		tickFormatter={(value) => String(value).substring(0, 3)}
	/>
	<EvilComposedChart.Legend isClickable />
	<EvilComposedChart.Tooltip />
	<!-- [!code highlight:3] -->
	<EvilComposedChart.Bar dataKey="revenue" variant="duotone" isClickable />
	<EvilComposedChart.Line dataKey="profit" isClickable />
</EvilComposedChart>
```
### <Bar variant='gradient' />

```svelte
<script lang="ts">
	import { EvilComposedChart } from '$lib/components/evilcharts/charts/layerchart-composed-chart/index.js';
	import { type ChartConfig } from '$lib/components/evilcharts/ui/layerchart-chart/index.js';

	const data = [
		{ month: 'January', revenue: 4200, profit: 1800 },
		{ month: 'February', revenue: 5800, profit: 2400 },
		{ month: 'March', revenue: 4100, profit: 1600 },
		{ month: 'April', revenue: 6200, profit: 2800 },
		{ month: 'May', revenue: 5400, profit: 2200 },
		{ month: 'June', revenue: 7800, profit: 3400 },
		{ month: 'July', revenue: 6100, profit: 2600 },
		{ month: 'August', revenue: 8200, profit: 3800 },
		{ month: 'September', revenue: 5900, profit: 2500 },
		{ month: 'October', revenue: 6800, profit: 3000 },
		{ month: 'November', revenue: 7200, profit: 3200 },
		{ month: 'December', revenue: 9100, profit: 4200 }
	];

	const chartConfig = {
		revenue: {
			label: 'Revenue',
			colors: {
				light: ['#3b82f6'],
				dark: ['#6A5ACD']
			}
		},
		profit: {
			label: 'Profit',
			colors: {
				light: ['#10b981'],
				dark: ['#34d399']
			}
		}
	} satisfies ChartConfig;
</script>

<EvilComposedChart class="h-full w-full p-4" xDataKey="month" {data} config={chartConfig}>
	<EvilComposedChart.Grid />
	<EvilComposedChart.XAxis
		dataKey="month"
		tickFormatter={(value) => String(value).substring(0, 3)}
	/>
	<EvilComposedChart.Legend isClickable />
	<EvilComposedChart.Tooltip />
	<!-- [!code highlight:3] -->
	<EvilComposedChart.Bar dataKey="revenue" variant="gradient" isClickable />
	<EvilComposedChart.Line dataKey="profit" isClickable />
</EvilComposedChart>
```
### <Bar variant='stripped' />

```svelte
<script lang="ts">
	import { EvilComposedChart } from '$lib/components/evilcharts/charts/layerchart-composed-chart/index.js';
	import { type ChartConfig } from '$lib/components/evilcharts/ui/layerchart-chart/index.js';

	const data = [
		{ month: 'January', revenue: 4200, profit: 1800 },
		{ month: 'February', revenue: 5800, profit: 2400 },
		{ month: 'March', revenue: 4100, profit: 1600 },
		{ month: 'April', revenue: 6200, profit: 2800 },
		{ month: 'May', revenue: 5400, profit: 2200 },
		{ month: 'June', revenue: 7800, profit: 3400 },
		{ month: 'July', revenue: 6100, profit: 2600 },
		{ month: 'August', revenue: 8200, profit: 3800 },
		{ month: 'September', revenue: 5900, profit: 2500 },
		{ month: 'October', revenue: 6800, profit: 3000 },
		{ month: 'November', revenue: 7200, profit: 3200 },
		{ month: 'December', revenue: 9100, profit: 4200 }
	];

	const chartConfig = {
		revenue: {
			label: 'Revenue',
			colors: {
				light: ['#3b82f6'],
				dark: ['#6A5ACD']
			}
		},
		profit: {
			label: 'Profit',
			colors: {
				light: ['#10b981'],
				dark: ['#34d399']
			}
		}
	} satisfies ChartConfig;
</script>

<EvilComposedChart class="h-full w-full p-4" xDataKey="month" {data} config={chartConfig}>
	<EvilComposedChart.Grid />
	<EvilComposedChart.XAxis
		dataKey="month"
		tickFormatter={(value) => String(value).substring(0, 3)}
	/>
	<EvilComposedChart.Legend isClickable />
	<EvilComposedChart.Tooltip />
	<!-- [!code highlight:3] -->
	<EvilComposedChart.Bar dataKey="revenue" variant="stripped" isClickable />
	<EvilComposedChart.Line dataKey="profit" isClickable />
</EvilComposedChart>
```

### Line Stroke Variants

### <Line strokeVariant='dashed' />

```svelte
<script lang="ts">
	import { EvilComposedChart } from '$lib/components/evilcharts/charts/layerchart-composed-chart/index.js';
	import { type ChartConfig } from '$lib/components/evilcharts/ui/layerchart-chart/index.js';

	const data = [
		{ month: 'January', revenue: 4200, profit: 1800 },
		{ month: 'February', revenue: 5800, profit: 2400 },
		{ month: 'March', revenue: 4100, profit: 1600 },
		{ month: 'April', revenue: 6200, profit: 2800 },
		{ month: 'May', revenue: 5400, profit: 2200 },
		{ month: 'June', revenue: 7800, profit: 3400 },
		{ month: 'July', revenue: 6100, profit: 2600 },
		{ month: 'August', revenue: 8200, profit: 3800 },
		{ month: 'September', revenue: 5900, profit: 2500 },
		{ month: 'October', revenue: 6800, profit: 3000 },
		{ month: 'November', revenue: 7200, profit: 3200 },
		{ month: 'December', revenue: 9100, profit: 4200 }
	];

	const chartConfig = {
		revenue: {
			label: 'Revenue',
			colors: {
				light: ['#3b82f6'],
				dark: ['#6A5ACD']
			}
		},
		profit: {
			label: 'Profit',
			colors: {
				light: ['#10b981'],
				dark: ['#34d399']
			}
		}
	} satisfies ChartConfig;
</script>

<EvilComposedChart class="h-full w-full p-4" xDataKey="month" {data} config={chartConfig}>
	<EvilComposedChart.Grid />
	<EvilComposedChart.XAxis
		dataKey="month"
		tickFormatter={(value) => String(value).substring(0, 3)}
	/>
	<EvilComposedChart.Legend isClickable />
	<EvilComposedChart.Tooltip />
	<EvilComposedChart.Bar dataKey="revenue" isClickable />
	<!-- [!code highlight:3] -->
	<EvilComposedChart.Line dataKey="profit" strokeVariant="dashed" isClickable />
</EvilComposedChart>
```
### <Line strokeVariant='animated-dashed' />

```svelte
<script lang="ts">
	import { EvilComposedChart } from '$lib/components/evilcharts/charts/layerchart-composed-chart/index.js';
	import { type ChartConfig } from '$lib/components/evilcharts/ui/layerchart-chart/index.js';

	const data = [
		{ month: 'January', revenue: 4200, profit: 1800 },
		{ month: 'February', revenue: 5800, profit: 2400 },
		{ month: 'March', revenue: 4100, profit: 1600 },
		{ month: 'April', revenue: 6200, profit: 2800 },
		{ month: 'May', revenue: 5400, profit: 2200 },
		{ month: 'June', revenue: 7800, profit: 3400 },
		{ month: 'July', revenue: 6100, profit: 2600 },
		{ month: 'August', revenue: 8200, profit: 3800 },
		{ month: 'September', revenue: 5900, profit: 2500 },
		{ month: 'October', revenue: 6800, profit: 3000 },
		{ month: 'November', revenue: 7200, profit: 3200 },
		{ month: 'December', revenue: 9100, profit: 4200 }
	];

	const chartConfig = {
		revenue: {
			label: 'Revenue',
			colors: {
				light: ['#3b82f6'],
				dark: ['#6A5ACD']
			}
		},
		profit: {
			label: 'Profit',
			colors: {
				light: ['#10b981'],
				dark: ['#34d399']
			}
		}
	} satisfies ChartConfig;
</script>

<EvilComposedChart class="h-full w-full p-4" xDataKey="month" {data} config={chartConfig}>
	<EvilComposedChart.Grid />
	<EvilComposedChart.XAxis
		dataKey="month"
		tickFormatter={(value) => String(value).substring(0, 3)}
	/>
	<EvilComposedChart.Legend isClickable />
	<EvilComposedChart.Tooltip />
	<EvilComposedChart.Bar dataKey="revenue" isClickable />
	<!-- [!code highlight:3] -->
	<EvilComposedChart.Line dataKey="profit" strokeVariant="animated-dashed" isClickable />
</EvilComposedChart>
```

### Curve Types

### <Line curveType='bump' />

```svelte
<script lang="ts">
	import { EvilComposedChart } from '$lib/components/evilcharts/charts/layerchart-composed-chart/index.js';
	import { type ChartConfig } from '$lib/components/evilcharts/ui/layerchart-chart/index.js';

	const data = [
		{ month: 'January', revenue: 4200, profit: 1800 },
		{ month: 'February', revenue: 5800, profit: 2400 },
		{ month: 'March', revenue: 4100, profit: 1600 },
		{ month: 'April', revenue: 6200, profit: 2800 },
		{ month: 'May', revenue: 5400, profit: 2200 },
		{ month: 'June', revenue: 7800, profit: 3400 },
		{ month: 'July', revenue: 6100, profit: 2600 },
		{ month: 'August', revenue: 8200, profit: 3800 },
		{ month: 'September', revenue: 5900, profit: 2500 },
		{ month: 'October', revenue: 6800, profit: 3000 },
		{ month: 'November', revenue: 7200, profit: 3200 },
		{ month: 'December', revenue: 9100, profit: 4200 }
	];

	const chartConfig = {
		revenue: {
			label: 'Revenue',
			colors: {
				light: ['#3b82f6'],
				dark: ['#6A5ACD']
			}
		},
		profit: {
			label: 'Profit',
			colors: {
				light: ['#10b981'],
				dark: ['#34d399']
			}
		}
	} satisfies ChartConfig;
</script>

<EvilComposedChart class="h-full w-full p-4" xDataKey="month" {data} config={chartConfig}>
	<EvilComposedChart.Grid />
	<EvilComposedChart.XAxis
		dataKey="month"
		tickFormatter={(value) => String(value).substring(0, 3)}
	/>
	<EvilComposedChart.Legend isClickable />
	<EvilComposedChart.Tooltip />
	<EvilComposedChart.Bar dataKey="revenue" isClickable />
	<!-- [!code highlight:3] -->
	<EvilComposedChart.Line dataKey="profit" curveType="bump" isClickable />
</EvilComposedChart>
```

### Line Dots

### <Dot /> and <ActiveDot />

```svelte
<script lang="ts">
	import { EvilComposedChart } from '$lib/components/evilcharts/charts/layerchart-composed-chart/index.js';
	import { type ChartConfig } from '$lib/components/evilcharts/ui/layerchart-chart/index.js';

	const data = [
		{ month: 'January', revenue: 4200, profit: 1800 },
		{ month: 'February', revenue: 5800, profit: 2400 },
		{ month: 'March', revenue: 4100, profit: 1600 },
		{ month: 'April', revenue: 6200, profit: 2800 },
		{ month: 'May', revenue: 5400, profit: 2200 },
		{ month: 'June', revenue: 7800, profit: 3400 },
		{ month: 'July', revenue: 6100, profit: 2600 },
		{ month: 'August', revenue: 8200, profit: 3800 },
		{ month: 'September', revenue: 5900, profit: 2500 },
		{ month: 'October', revenue: 6800, profit: 3000 },
		{ month: 'November', revenue: 7200, profit: 3200 },
		{ month: 'December', revenue: 9100, profit: 4200 }
	];

	const chartConfig = {
		revenue: {
			label: 'Revenue',
			colors: {
				light: ['#3b82f6'],
				dark: ['#6A5ACD']
			}
		},
		profit: {
			label: 'Profit',
			colors: {
				light: ['#10b981'],
				dark: ['#34d399']
			}
		}
	} satisfies ChartConfig;
</script>

<EvilComposedChart class="h-full w-full p-4" xDataKey="month" {data} config={chartConfig}>
	<EvilComposedChart.Grid />
	<EvilComposedChart.XAxis
		dataKey="month"
		tickFormatter={(value) => String(value).substring(0, 3)}
	/>
	<EvilComposedChart.Legend isClickable />
	<EvilComposedChart.Tooltip />
	<EvilComposedChart.Bar dataKey="revenue" isClickable />
	<EvilComposedChart.Line dataKey="profit" isClickable>
		<EvilComposedChart.Dot variant="default" />
		<EvilComposedChart.ActiveDot variant="border" />
	</EvilComposedChart.Line>
</EvilComposedChart>
```
>  
  

Compose a `<Dot>` for the resting marker and an `<ActiveDot>` for the hover marker inside a `<Line>`. Variants: `default`, `border`, `colored-border`.




### Hover Highlight

### <Bar enableHoverHighlight />

```svelte
<script lang="ts">
	import { EvilComposedChart } from '$lib/components/evilcharts/charts/layerchart-composed-chart/index.js';
	import { type ChartConfig } from '$lib/components/evilcharts/ui/layerchart-chart/index.js';

	const data = [
		{ month: 'January', revenue: 4200, profit: 1800 },
		{ month: 'February', revenue: 5800, profit: 2400 },
		{ month: 'March', revenue: 4100, profit: 1600 },
		{ month: 'April', revenue: 6200, profit: 2800 },
		{ month: 'May', revenue: 5400, profit: 2200 },
		{ month: 'June', revenue: 7800, profit: 3400 },
		{ month: 'July', revenue: 6100, profit: 2600 },
		{ month: 'August', revenue: 8200, profit: 3800 },
		{ month: 'September', revenue: 5900, profit: 2500 },
		{ month: 'October', revenue: 6800, profit: 3000 },
		{ month: 'November', revenue: 7200, profit: 3200 },
		{ month: 'December', revenue: 9100, profit: 4200 }
	];

	const chartConfig = {
		revenue: {
			label: 'Revenue',
			colors: {
				light: ['#3b82f6'],
				dark: ['#6A5ACD']
			}
		},
		profit: {
			label: 'Profit',
			colors: {
				light: ['#10b981'],
				dark: ['#34d399']
			}
		}
	} satisfies ChartConfig;
</script>

<EvilComposedChart class="h-full w-full p-4" xDataKey="month" {data} config={chartConfig}>
	<EvilComposedChart.Grid />
	<EvilComposedChart.XAxis
		dataKey="month"
		tickFormatter={(value) => String(value).substring(0, 3)}
	/>
	<EvilComposedChart.Legend isClickable />
	<EvilComposedChart.Tooltip />
	<!-- [!code highlight:3] -->
	<EvilComposedChart.Bar dataKey="revenue" enableHoverHighlight isClickable />
	<EvilComposedChart.Line dataKey="profit" isClickable />
</EvilComposedChart>
```
>  
  

Set `enableHoverHighlight` on a `<Bar>` to dim the other bars on hover, keeping focus on specific data points.




### Glowing Effects

### <Bar glow /> and <Line glow />

```svelte
<script lang="ts">
	import { EvilComposedChart } from '$lib/components/evilcharts/charts/layerchart-composed-chart/index.js';
	import { type ChartConfig } from '$lib/components/evilcharts/ui/layerchart-chart/index.js';

	const data = [
		{ month: 'January', revenue: 4200, profit: 1800 },
		{ month: 'February', revenue: 5800, profit: 2400 },
		{ month: 'March', revenue: 4100, profit: 1600 },
		{ month: 'April', revenue: 6200, profit: 2800 },
		{ month: 'May', revenue: 5400, profit: 2200 },
		{ month: 'June', revenue: 7800, profit: 3400 },
		{ month: 'July', revenue: 6100, profit: 2600 },
		{ month: 'August', revenue: 8200, profit: 3800 },
		{ month: 'September', revenue: 5900, profit: 2500 },
		{ month: 'October', revenue: 6800, profit: 3000 },
		{ month: 'November', revenue: 7200, profit: 3200 },
		{ month: 'December', revenue: 9100, profit: 4200 }
	];

	const chartConfig = {
		revenue: {
			label: 'Revenue',
			colors: {
				light: ['#3b82f6'],
				dark: ['#6A5ACD']
			}
		},
		profit: {
			label: 'Profit',
			colors: {
				light: ['#10b981'],
				dark: ['#34d399']
			}
		}
	} satisfies ChartConfig;
</script>

<EvilComposedChart class="h-full w-full p-4" xDataKey="month" {data} config={chartConfig}>
	<EvilComposedChart.Grid />
	<EvilComposedChart.XAxis
		dataKey="month"
		tickFormatter={(value) => String(value).substring(0, 3)}
	/>
	<EvilComposedChart.Legend isClickable />
	<EvilComposedChart.Tooltip />
	<!-- [!code highlight:3] -->
	<EvilComposedChart.Bar dataKey="revenue" glow isClickable />
	<!-- [!code highlight:3] -->
	<EvilComposedChart.Line dataKey="profit" glow isClickable />
</EvilComposedChart>
```
>  
  

Add the `glow` prop to a `<Bar>` or `<Line>` for a subtle glow. Each glowing series renders its own scoped filter.




### Dither rendering

Set `renderStyle="dither"` on the existing chart root to share one ordered-dither canvas across its bars and lines. SVG marks continue to own hover, selection, tooltips, dots, loading, and brush interaction; each `<Bar />` or `<Line />` can override `ditherVariant`.

### renderStyle='dither'

```svelte
<script lang="ts">
	import {
		EvilComposedChart,
		type ChartConfig
	} from '$lib/components/evilcharts/charts/layerchart-composed-chart/index.js';
	const data = [
		{ month: 'Jan', desktop: 42, mobile: 25 },
		{ month: 'Feb', desktop: 76, mobile: 54 },
		{ month: 'Mar', desktop: 51, mobile: 38 },
		{ month: 'Apr', desktop: 84, mobile: 62 },
		{ month: 'May', desktop: 63, mobile: 47 },
		{ month: 'Jun', desktop: 91, mobile: 70 }
	];
	const config = {
		desktop: { label: 'Desktop', colors: { light: ['#047857'], dark: ['#10b981'] } },
		mobile: { label: 'Mobile', colors: { light: ['#be123c'], dark: ['#f43f5e'] } }
	} satisfies ChartConfig;
</script>

<EvilComposedChart
	{data}
	{config}
	xDataKey="month"
	renderStyle="dither"
	bloom="low"
	class="h-full w-full p-4"
>
	<EvilComposedChart.Grid /><EvilComposedChart.XAxis dataKey="month" /><EvilComposedChart.Legend
		isClickable
	/><EvilComposedChart.Tooltip />
	<EvilComposedChart.Bar dataKey="desktop" ditherVariant="gradient" isClickable />
	<EvilComposedChart.Line dataKey="mobile" ditherVariant="solid" isClickable />
</EvilComposedChart>
```

The renderer is independently implemented for EvilCharts SV and inspired by [Dither Kit](https://github.com/Boring-Software-Inc/dither-kit) by Boring Software.

## API Reference

The props below are grouped by the component they belong to.

### EvilComposedChart

The root container. It owns the data, shared selection state, loading skeleton, and optional brush; everything visual is composed as its children.


  ### `data` (required)

type: `TData[]`

The chart data — an array of objects, one per data point (`TData extends Record<string, unknown>`).
  ### `config` (required)

type: `Record<string, ChartConfig[string]>`

Defines every bar and line series. Each key matches a data key in your data, with a corresponding color or color array.
  ### `children` (required)

type: `Snippet`

The composed chart parts — `<Grid />`, `<XAxis />`, `<YAxis />`, `<Legend />`, `<Tooltip />`, and one or more `<Bar />` and `<Line />`.
  ### `accessibility`

type: `ChartAccessibility`

Names and optionally describes the chart wrapper. It remains a group, so interactive legends and marks stay available to assistive technology.
  ### `className`

type: `string`

Extra CSS classes for the chart container.
  ### `curveType`

type: `"basis" | "bumpX" | "bumpY" | "bump" | "linear" | "natural" | "monotoneX" | "monotoneY" | "monotone" | "step" | …` · default: `"linear"`

Default curve interpolation for every `<Line />`; each can override it locally.
  ### `animationType`

type: `"none" | "left-to-right" | "right-to-left" | "center-out" | "edges-in"` · default: `"left-to-right"`

Default intro for every `<Bar />` and `<Line />` — lines wipe in along this direction, bars grow from their baseline staggered in this order. `"none"` disables it; OS reduce-motion falls back to `"none"` automatically.
  ### `barGap`

type: `number`

Gap between bars in the same category.
  ### `barCategoryGap`

type: `number`

Gap between bar categories.
  ### `defaultSelectedDataKey`

type: `string | null` · default: `null`

The data key selected by default.
  ### `onSelectionChange`

type: `(selectedDataKey: string | null) => void`

Fires when a series is selected or deselected by clicking a clickable `<Bar />`, `<Line />`, or `<Legend />` entry. Receives the selected data key, or `null` when deselected.
  ### `isLoading`

type: `boolean` · default: `false`

Shows a skeleton with a shimmer effect while data is being fetched.
  ### `loadingBars`

type: `number` · default: `12`

Number of bars in the loading skeleton.
  ### `xDataKey`

type: `keyof TData & string`

The x-axis data key. Only needed by the brush footer — the axis reads its own key from `<XAxis dataKey="…" />`.
  ### `renderStyle`

type: `"svg" | "dither"` · default: `"svg"`

Selects the SVG or ordered-dither renderer.
  ### `ditherVariant`

type: `"gradient" | "dotted" | "hatched" | "solid"` · default: `"gradient"`

Default texture for dithered series.
  ### `ditherCellSize`

type: `number` · default: `2`

Dither cell size in CSS pixels.
  ### `bloom`

type: `"off" | "low" | "high" | "aura"` · default: `"off"`

Optional bounded glow around dither pixels.
  ### `chartProps`

type: `ComponentProps<typeof ComposedChart>`

Extra props forwarded to the underlying LayerChart Chart. See the [LayerChart Chart documentation](https://www.layerchart.com/docs/components/Chart).


### Bar

A single bar series. Each `<Bar />` generates its own gradient/pattern definitions, so a chart can hold any number of bars — each with its own variant, glow, and clickability.


  ### `dataKey` (required)

type: `string`

The series key. Must exist on both the data rows and the chart `config`.
  ### `variant`

type: `"default" | "hatched" | "duotone" | "duotone-reverse" | "gradient" | "stripped"` · default: `"default"`

The bar fill's visual style. Applies to this bar only.
  ### `radius`

type: `number` · default: `4`

The bar's corner radius, in pixels.
  ### `animationType`

type: `"none" | "left-to-right" | "right-to-left" | "center-out" | "edges-in"`

The grow-in order for this bar series. Falls back to the chart's `animationType` when omitted.
  ### `glow`

type: `boolean` · default: `false`

Applies a soft outer neon glow to this bar.
  ### `isClickable`

type: `boolean` · default: `false`

Makes this bar selectable on click. When any series is selected, unselected series become semi-transparent.
  ### `enableHoverHighlight`

type: `boolean` · default: `false`

When set, hovering a column dims the other bars, making it easier to focus on specific data points.
  ### `barProps`

type: `ComponentProps<typeof Bar>`

Escape hatch for raw props forwarded to the underlying LayerChart Bar.


### Line

A single line series. Each `<Line />` generates its own color gradient and glow filter, so a chart can hold any number of lines — each with its own stroke, curve, glow, and clickability.


  ### `dataKey` (required)

type: `string`

The series key. Must exist on both the data rows and the chart `config`.
  ### `strokeVariant`

type: `"solid" | "dashed" | "animated-dashed"` · default: `"solid"`

The stroke style for this line.
  ### `curveType`

type: `"basis" | "bump" | "linear" | "natural" | "monotoneX" | "monotoneY" | "monotone" | "step" | "stepBefore" | "stepAfter" | …`

The curve interpolation for this line. Falls back to the chart's `curveType` when omitted.
  ### `animationType`

type: `"none" | "left-to-right" | "right-to-left" | "center-out" | "edges-in"`

The intro reveal direction for this line. Falls back to the chart's `animationType` when omitted.
  ### `connectNulls`

type: `boolean` · default: `false`

Whether to connect line segments across null or missing values.
  ### `glow`

type: `boolean` · default: `false`

Applies a soft outer neon glow to this line.
  ### `isClickable`

type: `boolean` · default: `false`

Makes this line selectable on click. When any series is selected, unselected series become semi-transparent.
  ### `children`

type: `Snippet`

Optional `<Dot />` and `<ActiveDot />` that add point markers to this line.
  ### `lineProps`

type: `ComponentProps<typeof Line>`

Escape hatch for raw props forwarded to the underlying LayerChart Line.


### Dot and ActiveDot

Point markers composed inside a `<Line />`. `<Dot />` is the resting marker; `<ActiveDot />` is the hovered marker. They render nothing on their own — the parent `<Line />` reads their `variant`.


  ### `variant`

type: `"default" | "border" | "colored-border"`

The visual style of the point marker.


### XAxis and YAxis

The category and value axes. Both ship with the chart's flat default styling and forward every LayerChart axis prop — `dataKey`, `tickFormatter`, `tickMargin`, etc. pass straight through. They hide automatically while the chart is loading.


  ### `dataKey`

type: `string`

The data key for the axis values.
  ### `…axisProps`



Every other LayerChart axis prop is forwarded as-is. See the [LayerChart Axis documentation](https://www.layerchart.com/docs/components/Axis).


### Grid

The background grid lines. Defaults to horizontal-only dashed lines and forwards every LayerChart CartesianGrid prop.


  ### `…gridProps`



Every LayerChart grid prop is forwarded as-is. See the [LayerChart Grid documentation](https://www.layerchart.com/docs/components/Grid).


### Tooltip

The hover tooltip. It reads the chart's selection state so its content dims unselected series.


  ### `variant`

type: `"default" | "frosted-glass"` · default: `"default"`

The visual style of the tooltip surface.
  ### `roundness`

type: `"sm" | "md" | "lg" | "xl"` · default: `"lg"`

Controls the border-radius of the tooltip.
  ### `defaultIndex`

type: `number`

When set, the tooltip shows by default at this data point index.
  ### `cursor`

type: `boolean` · default: `true`

Whether the vertical cursor line follows the pointer on hover.


### Legend

The series legend. When `isClickable` is set, each entry toggles selection of its series.


  ### `variant`

type: `"square" | "circle" | "circle-outline" | "rounded-square" | "rounded-square-outline" | "vertical-bar" | "horizontal-bar"`

The visual style of the legend indicators.
  ### `align`

type: `"left" | "center" | "right"` · default: `"right"`

Horizontal placement of the legend.
  ### `verticalAlign`

type: `"top" | "middle" | "bottom"` · default: `"top"`

Vertical placement of the legend.
  ### `isClickable`

type: `boolean` · default: `false`

Lets each legend entry toggle selection of its series.


### Brush

An optional zoom brush below the chart. Include `<EvilComposedChart.Brush />` to render it; dragging the range filters the main chart.


  ### `height`

type: `number`

Height of the brush preview strip in pixels.
  ### `formatLabel`

type: `(value: unknown, index: number) => string`

Formats the range-handle labels below the brush.
  ### `onChange`

type: `(range: { startIndex: number; endIndex: number }) => void`

Fires when the brush selection range changes.

