
### Basic Chart

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

	const data = [
		{ month: 'January', desktop: 342, mobile: 245 },
		{ month: 'February', desktop: 876, mobile: 654 },
		{ month: 'March', desktop: 512, mobile: 387 },
		{ month: 'April', desktop: 629, mobile: 521 },
		{ month: 'May', desktop: 458, mobile: 412 },
		{ month: 'June', desktop: 781, mobile: 598 },
		{ month: 'July', desktop: 394, mobile: 312 },
		{ month: 'August', desktop: 925, mobile: 743 },
		{ month: 'September', desktop: 647, mobile: 489 },
		{ month: 'October', desktop: 532, mobile: 476 },
		{ month: 'November', desktop: 803, mobile: 687 },
		{ month: 'December', desktop: 271, mobile: 198 }
	];

	const chartConfig = {
		desktop: {
			label: 'Desktop',
			colors: {
				light: ['#047857'],
				dark: ['#10b981']
			}
		},
		mobile: {
			label: 'Mobile',
			colors: {
				light: ['#be123c'],
				dark: ['#f43f5e']
			}
		}
	} satisfies ChartConfig;
</script>

<EChartsAreaChart
	{data}
	config={chartConfig}
	accessibility={{
		label: 'Monthly desktop and mobile usage area chart',
		description: 'Desktop and mobile values from January through December, with a range brush.'
	}}
	class="h-full w-full p-4"
	stackType="stacked"
	xDataKey="month"
>
	<EChartsAreaChart.Grid />
	<EChartsAreaChart.XAxis
		dataKey="month"
		tickFormatter={(value) => String(value).substring(0, 3)}
	/>
	<EChartsAreaChart.Brush formatLabel={(value) => String(value).substring(0, 3)} />
	<EChartsAreaChart.Legend isClickable />
	<EChartsAreaChart.Tooltip />
	<EChartsAreaChart.Area dataKey="desktop" variant="gradient" isClickable>
		<EChartsAreaChart.Dot variant="border" />
		<EChartsAreaChart.ActiveDot variant="colored-border" />
	</EChartsAreaChart.Area>
	<EChartsAreaChart.Area dataKey="mobile" variant="gradient" isClickable>
		<EChartsAreaChart.Dot variant="border" />
		<EChartsAreaChart.ActiveDot variant="colored-border" />
	</EChartsAreaChart.Area>
</EChartsAreaChart>
```

## Installation


  
  
    ### npm

```bash
npx shadcn-svelte@latest add @evilcharts/echarts-area-chart
```

### yarn

```bash
yarn dlx shadcn-svelte@latest add @evilcharts/echarts-area-chart
```

### bun

```bash
bunx --bun shadcn-svelte@latest add @evilcharts/echarts-area-chart
```

### pnpm

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

```bash
npm install echarts
```

### yarn

```bash
yarn add echarts
```

### bun

```bash
bun add echarts
```

### pnpm

```bash
pnpm add echarts
```
        
      
      
        ### Copy and paste the following code snippets into your project.
        

In your `components` directory, create `evilcharts`, then `charts` nested inside it, and paste the area-chart code into a new `echarts-area-chart` file there.


        
          ### $lib/components/evilcharts/charts/echarts-area-chart

`$lib/components/evilcharts/charts/echarts-area-chart/active-dot.svelte`

```svelte
<script lang="ts">
	import type { DotVariant } from '$lib/components/evilcharts/ui/echarts-dot/index.js';
	import { useEChartsAreaSlots } from './area-slots.svelte.js';
	let { variant = 'default' }: { variant?: DotVariant } = $props();
	const token = $props.id();
	const slots = useEChartsAreaSlots();
	$effect(() => slots.activeDots.register(token, () => ({ variant })));
</script>
```

`$lib/components/evilcharts/charts/echarts-area-chart/area-chart-context.svelte.ts`

```ts
import { getContext, setContext } from 'svelte';
import { RegistrationSet } from '$lib/components/evilcharts/ui/echarts-chart/index.js';
import type { AreaRegistration, AxisRegistration } from './types.js';

const CONTEXT = Symbol('evilcharts.echarts-area-chart');

export class EChartsAreaChartContext {
	areas = new RegistrationSet<AreaRegistration>();
	xAxes = new RegistrationSet<AxisRegistration>();
	yAxes = new RegistrationSet<AxisRegistration>();
	grids = new RegistrationSet<Record<string, never>>();
}

export function setEChartsAreaChartContext(): EChartsAreaChartContext {
	const context = new EChartsAreaChartContext();
	setContext(CONTEXT, context);
	return context;
}

export function useEChartsAreaChart(): EChartsAreaChartContext {
	const context = getContext<EChartsAreaChartContext | undefined>(CONTEXT);
	if (!context)
		throw new Error('[EvilCharts] ECharts area parts must be children of EChartsAreaChart.');
	return context;
}
```

`$lib/components/evilcharts/charts/echarts-area-chart/area-chart.svelte`

```svelte
<script lang="ts">
	import { prefersReducedMotion } from 'svelte/motion';
	import { untrack, type Snippet } from 'svelte';
	import type { EChartsCoreOption, EChartsType } from 'echarts/core';
	import {
		AriaComponent,
		DataZoomComponent,
		GridComponent,
		TooltipComponent
	} from 'echarts/components';
	import { LineChart } from 'echarts/charts';
	import * as echarts from 'echarts/core';
	import {
		ChartContainer,
		DEFAULT_ECHARTS_RENDERER,
		EChartsHost,
		LoadingIndicator,
		mergeLifecycleOptions,
		RegistrationSet,
		SelectableSeriesControls,
		resolveColors,
		setEChartsSharedSlotContext,
		withAlpha,
		type ChartAccessibility,
		type ChartConfig,
		type EChartsRenderer,
		type EChartsRenderStyle,
		type ResolvedColors
	} from '$lib/components/evilcharts/ui/echarts-chart/index.js';
	import type { DitherBloom, DitherVariant } from '$lib/components/evilcharts/ui/echarts-dither/index.js';
	import { LegendOverlay } from '$lib/components/evilcharts/ui/echarts-legend/index.js';
	import {
		BrushControls,
		syncBrushOverlay,
		type BrushOverlayElements
	} from '$lib/components/evilcharts/ui/echarts-brush/index.js';
	import { setEChartsAreaChartContext } from './area-chart-context.svelte.js';
	import {
		buildAreaOption,
		computeAreaPlottedTops,
		createAreaLoadingData,
		resolveAreaAtPixel,
		type AreaOptionContext
	} from './option.js';
	import type {
		BrushRegistration,
		CurveType,
		LegendRegistration,
		AreaAnimationType,
		StackType,
		TooltipRegistration
	} from './types.js';

	echarts.use([LineChart, GridComponent, TooltipComponent, DataZoomComponent, AriaComponent]);

	let {
		data,
		config,
		renderer = DEFAULT_ECHARTS_RENDERER,
		renderStyle = 'native',
		ditherVariant = 'gradient',
		ditherCellSize = 2,
		bloom = 'off',
		xDataKey,
		class: className,
		curveType = 'linear',
		stackType = 'default',
		animation = true,
		animationType = 'left-to-right',
		enableHoverHighlight = false,
		enableHoverReveal = false,
		defaultSelectedDataKey = null,
		selectedDataKey: selectedDataKeyProp,
		onSelectionChange,
		isLoading = false,
		loadingPoints = 14,
		chartOptions,
		accessibility,
		children
	}: {
		data: Record<string, unknown>[];
		config: ChartConfig;
		renderer?: EChartsRenderer;
		renderStyle?: EChartsRenderStyle;
		ditherVariant?: DitherVariant;
		ditherCellSize?: number;
		bloom?: DitherBloom;
		xDataKey?: string;
		class?: string;
		curveType?: CurveType;
		stackType?: StackType;
		animation?: boolean;
		animationType?: AreaAnimationType;
		enableHoverHighlight?: boolean;
		enableHoverReveal?: boolean;
		defaultSelectedDataKey?: string | null;
		selectedDataKey?: string | null;
		onSelectionChange?: (key: string | null) => void;
		isLoading?: boolean;
		loadingPoints?: number;
		chartOptions?: Record<string, unknown>;
		accessibility?: ChartAccessibility;
		children?: Snippet;
	} = $props();

	let container = $state<HTMLDivElement>();
	let dimension = $state({ width: 320, height: 200 });
	let themeRevision = $state(0);
	let instance = $state.raw<EChartsType>();
	let internalSelectedDataKey = $state<string | null>(untrack(() => defaultSelectedDataKey));
	const selectedDataKey = $derived(
		selectedDataKeyProp === undefined ? internalSelectedDataKey : selectedDataKeyProp
	);
	let hoveredDataKey = $state<string | null>(null);
	let introComplete = $state(false);
	const revealState = { index: null as number | null };
	let brushRange = $state({ start: 0, end: 100 });
	let brushHover = $state({ inside: false, left: false, right: false });
	let loadingData = $state.raw<number[]>(untrack(() => createAreaLoadingData(loadingPoints)));
	const brushOverlayStore: { brushOverlay: BrushOverlayElements | null } = { brushOverlay: null };
	let resolved = $state.raw<ResolvedColors>({
		series: {},
		tokens: {
			mutedForeground: 'rgba(120, 120, 120, 1)',
			border: 'rgba(120, 120, 120, 0.35)',
			foreground: 'rgba(120, 120, 120, 1)',
			background: 'rgba(0, 0, 0, 1)'
		}
	});

	const chart = setEChartsAreaChartContext();
	const tooltipSlots = new RegistrationSet<TooltipRegistration>();
	const legendSlots = new RegistrationSet<LegendRegistration>();
	const brushSlots = new RegistrationSet<BrushRegistration>();
	setEChartsSharedSlotContext({
		register(slot, token, getter) {
			if (slot === 'tooltip')
				return tooltipSlots.register(token, getter as () => TooltipRegistration);
			if (slot === 'legend') return legendSlots.register(token, getter as () => LegendRegistration);
			return brushSlots.register(token, getter as () => BrushRegistration);
		}
	});

	const areas = $derived(chart.areas.values);
	const selectableSeries = $derived(
		areas
			.filter((area) => area.isClickable)
			.filter(
				(area, index, all) => all.findIndex((item) => item.dataKey === area.dataKey) === index
			)
			.map((area) => ({
				key: area.dataKey,
				label:
					typeof config[area.dataKey]?.label === 'string'
						? (config[area.dataKey].label as string)
						: area.dataKey
			}))
	);
	const effectiveAnimation = $derived(areas[0]?.animationType ?? animationType);
	const seriesKeys = $derived(areas.map((area) => area.dataKey));
	const xAxis = $derived(chart.xAxes.first);
	const yAxis = $derived(chart.yAxes.first);
	const tooltip = $derived(tooltipSlots.first);
	const legend = $derived(legendSlots.first);
	const brush = $derived(brushSlots.first);

	$effect(() => {
		loadingData = createAreaLoadingData(loadingPoints);
	});
	const categoryValues = $derived.by(() => {
		const series = new Set(areas.map((area) => area.dataKey));
		const key =
			xDataKey ?? xAxis?.dataKey ?? Object.keys(data[0] ?? {}).find((item) => !series.has(item));
		return data.map((row, index) => String((key ? row[key] : undefined) ?? index));
	});

	$effect(() => {
		void themeRevision;
		const host = container;
		const keys = seriesKeys;
		if (!host) return;
		resolved = resolveColors(host, config, keys);
	});

	function areaOptionContext(): AreaOptionContext {
		return {
			data,
			config,
			areas,
			xDataKey,
			curveType,
			stackType,
			selectedDataKey,
			enableHoverHighlight,
			enableHoverReveal,
			hoverRevealIndex: revealState.index,
			xAxis,
			yAxis,
			showGrid: chart.grids.size > 0,
			tooltip,
			legend,
			brush,
			brushRange,
			isLoading,
			loadingData,
			resolved,
			animation: animation && !introComplete && effectiveAnimation !== 'none',
			animationType,
			reducedMotion: prefersReducedMotion.current,
			rendererSize: dimension,
			renderStyle,
			ditherVariant,
			ditherCellSize,
			bloom
		};
	}

	const option = $derived.by(() => {
		const built = buildAreaOption(areaOptionContext());
		return mergeLifecycleOptions(built, chartOptions) as EChartsCoreOption;
	});

	$effect(() => {
		if (isLoading) {
			introComplete = false;
			return;
		}
		if (introComplete || !instance || areas.length === 0) return;
		if (!animation || effectiveAnimation === 'none' || prefersReducedMotion.current) {
			introComplete = true;
			return;
		}
		const timer = window.setTimeout(() => (introComplete = true), 1000);
		return () => window.clearTimeout(timer);
	});

	$effect(() => {
		const chartInstance = instance;
		if (!chartInstance) return;
		if (!brush || isLoading) {
			syncBrushOverlay(chartInstance, brushOverlayStore, null);
			return;
		}
		const last = Math.max(0, categoryValues.length - 1);
		const startIndex = Math.round((brushRange.start / 100) * last);
		const endIndex = Math.round((brushRange.end / 100) * last);
		const format = brush.formatLabel;
		syncBrushOverlay(chartInstance, brushOverlayStore, {
			range: brushRange,
			geom: { bottom: legend?.verticalAlign === 'bottom' ? 34 : 6, height: brush.height ?? 56 },
			size: dimension,
			tokens: resolved.tokens,
			labels: format
				? {
						start: format(categoryValues[startIndex] ?? '', startIndex),
						end: format(categoryValues[endIndex] ?? '', endIndex)
					}
				: null,
			showLabels: brushHover.inside,
			hover: brushHover
		});
	});

	$effect(() => {
		const chartInstance = instance;
		const activeBrush = brush;
		const currentLegend = legend;
		const keys = seriesKeys;
		const revealEnabled = enableHoverReveal;
		const highlightEnabled = enableHoverHighlight;
		if (!chartInstance) return;
		const renderer = chartInstance.getZr();
		const context = areaOptionContext();
		const tops = computeAreaPlottedTops(context);
		const base = buildAreaOption({ ...context, hoverRevealIndex: null });
		const baseSeries = Array.isArray(base.series) ? base.series : base.series ? [base.series] : [];
		const revealValues = new Map(
			keys.map((key) => {
				const entry = baseSeries.find(
					(series) => String((series as { id?: unknown }).id ?? '') === key
				);
				const values = Array.isArray((entry as { data?: unknown[] } | undefined)?.data)
					? ((entry as { data: unknown[] }).data ?? [])
					: [];
				return [key, values] as const;
			})
		);

		const applyHoverKey = (key: string | null) => {
			if (hoveredDataKey === key) return;
			const previous = hoveredDataKey;
			hoveredDataKey = key;
			if (previous) {
				chartInstance.dispatchAction({ type: 'downplay', seriesId: previous });
				chartInstance.dispatchAction({ type: 'downplay', seriesId: `__buffer-${previous}` });
			}
			if (key) {
				chartInstance.dispatchAction({ type: 'highlight', seriesId: key });
				chartInstance.dispatchAction({ type: 'highlight', seriesId: `__buffer-${key}` });
			}
		};

		const pushReveal = (index: number | null) => {
			const updates = keys.flatMap((key) => {
				const values = revealValues.get(key) ?? [];
				return [
					{
						id: key,
						data:
							index === null
								? values
								: values.map((value, valueIndex) => (valueIndex > index ? null : value))
					},
					{
						id: `__reveal-${key}`,
						data:
							index === null
								? values
								: values.map((value, valueIndex) => (valueIndex < index ? null : value)),
						lineStyle: { opacity: index === null ? 0 : 0.3 }
					}
				];
			});
			chartInstance.setOption({ series: updates }, { silent: true });
			for (const key of keys) {
				chartInstance.dispatchAction(
					index === null
						? { type: 'downplay', seriesId: key }
						: { type: 'highlight', seriesId: key, dataIndex: index }
				);
			}
		};

		const clearReveal = () => {
			if (revealState.index === null) return;
			revealState.index = null;
			pushReveal(null);
		};

		const move = (event: { offsetX?: number; offsetY?: number }) => {
			const x = event.offsetX ?? -1;
			const y = event.offsetY ?? -1;
			if (activeBrush && !isLoading) {
				const height = activeBrush.height ?? 56;
				const bottom = currentLegend?.verticalAlign === 'bottom' ? 34 : 6;
				const top = chartInstance.getHeight() - bottom - height;
				const inside = y >= top - 4 && y <= top + height + 4;
				const trackWidth = Math.max(chartInstance.getWidth() - 16, 1);
				const leftX = 8 + (trackWidth * brushRange.start) / 100;
				const rightX = 8 + (trackWidth * brushRange.end) / 100;
				brushHover = {
					inside,
					left: inside && Math.abs(x - leftX) <= 8,
					right: inside && Math.abs(x - rightX) <= 8
				};
			}

			if (revealEnabled) {
				if (!chartInstance.containPixel({ gridIndex: 0 }, [x, y])) {
					clearReveal();
					return;
				}
				const converted = chartInstance.convertFromPixel({ gridIndex: 0 }, [x, y]);
				const raw = Array.isArray(converted) ? converted[0] : converted;
				if (typeof raw !== 'number') return;
				const index = Math.max(0, Math.min(data.length - 1, Math.round(raw)));
				if (index === revealState.index) return;
				revealState.index = index;
				pushReveal(index);
				return;
			}

			if (highlightEnabled && selectedDataKey === null) {
				applyHoverKey(resolveAreaAtPixel(chartInstance, tops, keys, x, y));
			}
		};

		const out = () => {
			brushHover = { inside: false, left: false, right: false };
			if (revealEnabled) clearReveal();
			else if (highlightEnabled) applyHoverKey(null);
		};

		renderer.on('mousemove', move);
		renderer.on('globalout', out);
		return () => {
			renderer.off('mousemove', move);
			renderer.off('globalout', out);
		};
	});

	function toggleSelection(key: string) {
		if (!areas.some((area) => area.dataKey === key && area.isClickable) && !legend?.isClickable) {
			return;
		}
		const next = selectedDataKey === key ? null : key;
		internalSelectedDataKey = next;
		onSelectionChange?.(next);
	}

	function eventSeriesKey(params: unknown): string | null {
		if (!params || typeof params !== 'object') return null;
		const id = (params as { seriesId?: unknown }).seriesId;
		if (typeof id !== 'string' || id.startsWith('__')) return null;
		return id;
	}

	const events = $derived({
		click: (params: unknown) => {
			let key = eventSeriesKey(params);
			if (instance && params && typeof params === 'object') {
				const event = (params as { event?: { offsetX?: unknown; offsetY?: unknown } }).event;
				if (typeof event?.offsetX === 'number' && typeof event.offsetY === 'number') {
					key =
						resolveAreaAtPixel(
							instance,
							computeAreaPlottedTops(areaOptionContext()),
							seriesKeys,
							event.offsetX,
							event.offsetY
						) ?? key;
				}
			}
			if (key) toggleSelection(key);
		},
		mouseover: (params: unknown) => {
			if (!enableHoverHighlight || enableHoverReveal || selectedDataKey !== null || !instance)
				return;
			const nativeKey = eventSeriesKey(params);
			if (nativeKey && nativeKey !== hoveredDataKey) {
				instance.dispatchAction({ type: 'downplay', seriesId: nativeKey });
				if (hoveredDataKey)
					instance.dispatchAction({ type: 'highlight', seriesId: hoveredDataKey });
			}
		},
		datazoom: () => {
			const zoom = (instance?.getOption() as { dataZoom?: { start?: number; end?: number }[] })
				?.dataZoom?.[0];
			if (!zoom) return;
			brushRange = { start: zoom.start ?? 0, end: zoom.end ?? 100 };
			if (!brush?.onChange) return;
			const last = Math.max(0, data.length - 1);
			brush.onChange({
				startIndex: Math.round((brushRange.start / 100) * last),
				endIndex: Math.round((brushRange.end / 100) * last)
			});
		}
	});

	$effect(() => {
		const chartInstance = instance;
		const animatedKeys = areas
			.filter((area) => area.strokeVariant === 'animated-dashed' && !area.enableBufferLine)
			.map((area) => area.dataKey);
		if (
			!chartInstance ||
			isLoading ||
			selectedDataKey !== null ||
			animatedKeys.length === 0 ||
			prefersReducedMotion.current ||
			!introComplete
		)
			return;
		let frame = 0;
		const start = performance.now();
		const tick = (now: number) => {
			const dashOffset = -(((now - start) / 1000) % 1) * 6;
			chartInstance.setOption(
				{ series: animatedKeys.map((id) => ({ id, lineStyle: { dashOffset } })) },
				{ silent: true, lazyUpdate: true }
			);
			frame = requestAnimationFrame(tick);
		};
		frame = requestAnimationFrame(tick);
		return () => cancelAnimationFrame(frame);
	});

	$effect(() => {
		const chartInstance = instance;
		if (!chartInstance || !isLoading) return;
		if (prefersReducedMotion.current) {
			chartInstance.setOption(
				{
					series: [
						{
							id: '__loading',
							data: loadingData,
							lineStyle: { color: withAlpha(resolved.tokens.foreground, 0.5), width: 1 },
							areaStyle: { color: withAlpha(resolved.tokens.foreground, 0.03) }
						}
					]
				},
				{ silent: true, lazyUpdate: true }
			);
			return;
		}
		let frame = 0;
		let lastPhase = 0;
		const start = performance.now();
		const tick = (now: number) => {
			const phase = ((now - start) / 2000) % 1;
			// Change the wave only after the reveal has left the plot. Its geometry
			// therefore stays fixed for the complete visible sweep, like Line loading.
			if (phase < lastPhase) loadingData = createAreaLoadingData(loadingPoints);
			lastPhase = phase;
			const width = chartInstance.getWidth();
			const height = chartInstance.getHeight();
			if (!width || !height) {
				frame = requestAnimationFrame(tick);
				return;
			}
			const maxT = (width + height) / (2 * width);
			const center = phase * (maxT + 0.4) - 0.2;
			const color = resolved.tokens.foreground;
			const stops = (peak: number) =>
				[0, center - 0.2, center, center + 0.2, 1]
					.filter((offset) => offset >= 0 && offset <= 1)
					.sort((left, right) => left - right)
					.filter(
						(offset, index, values) =>
							index === 0 || offset - (values[index - 1] ?? Number.NEGATIVE_INFINITY) > 0.0001
					)
					.map((offset) => {
						const distance = Math.abs(offset - center);
						const alpha =
							distance >= 0.2 ? 0 : peak * Math.sin(((1 - distance / 0.2) * Math.PI) / 2);
						return { offset, color: withAlpha(color, alpha) };
					});
			const clip = (peak: number) =>
				new echarts.graphic.LinearGradient(0, 0, width, width, stops(peak), true);
			chartInstance.setOption(
				{
					series: [
						{
							id: '__loading',
							data: loadingData,
							lineStyle: { color: clip(0.5), width: 1 },
							areaStyle: { color: clip(0.03) }
						}
					]
				},
				{ silent: true, lazyUpdate: true }
			);
			frame = requestAnimationFrame(tick);
		};
		frame = requestAnimationFrame(tick);
		return () => cancelAnimationFrame(frame);
	});

	const legendStyle = $derived(
		`position:absolute;left:16px;right:16px;${
			legend?.verticalAlign === 'bottom'
				? `bottom:${brush ? (brush.height ?? 56) + 16 : 12}px;`
				: legend?.verticalAlign === 'middle'
					? 'top:50%;transform:translateY(-50%);'
					: 'top:12px;'
		}`
	);
</script>

{#snippet overlay()}
	{#if legend && !isLoading}
		<LegendOverlay
			{seriesKeys}
			{config}
			variant={legend.variant}
			align={legend.align}
			selectedKey={selectedDataKey}
			hoveredKey={hoveredDataKey}
			isClickable={legend.isClickable}
			onToggle={toggleSelection}
			style={legendStyle}
		/>
	{/if}
	<LoadingIndicator {isLoading} />
{/snippet}

<ChartContainer
	{config}
	{accessibility}
	{overlay}
	bind:element={container}
	bind:dimension
	bind:themeRevision
	aria-busy={isLoading}
	class={className}
>
	{@render children?.()}
	<EChartsHost {option} {renderer} {events} bind:instance />
	{#if !legend?.isClickable}
		<SelectableSeriesControls
			items={selectableSeries}
			selectedKey={selectedDataKey}
			onToggle={toggleSelection}
		/>
	{/if}
	{#if brush && !isLoading && data.length > 0}
		<BrushControls
			startIndex={Math.round((brushRange.start / 100) * Math.max(0, data.length - 1))}
			endIndex={Math.round((brushRange.end / 100) * Math.max(0, data.length - 1))}
			totalPoints={data.length}
			formatLabel={(index) =>
				brush.formatLabel?.(categoryValues[index] ?? '', index) ??
				String(categoryValues[index] ?? index)}
			onChange={(range) => {
				const last = Math.max(0, data.length - 1);
				brushRange = {
					start: last === 0 ? 0 : (range.startIndex / last) * 100,
					end: last === 0 ? 100 : (range.endIndex / last) * 100
				};
				instance?.dispatchAction(
					{ type: 'dataZoom', start: brushRange.start, end: brushRange.end },
					{ silent: true }
				);
				brush.onChange?.(range);
			}}
		/>
	{/if}
</ChartContainer>
```

`$lib/components/evilcharts/charts/echarts-area-chart/area-slots.svelte.ts`

```ts
import { getContext, setContext } from 'svelte';
import { RegistrationSet } from '$lib/components/evilcharts/ui/echarts-chart/index.js';
import type { DotVariant } from '$lib/components/evilcharts/ui/echarts-dot/index.js';

type DotRegistration = { variant: DotVariant };
const CONTEXT = Symbol('evilcharts.echarts-area-slots');

export class EChartsAreaSlots {
	dots = new RegistrationSet<DotRegistration>();
	activeDots = new RegistrationSet<DotRegistration>();
}

export function setEChartsAreaSlots(): EChartsAreaSlots {
	const context = new EChartsAreaSlots();
	setContext(CONTEXT, context);
	return context;
}

export function useEChartsAreaSlots(): EChartsAreaSlots {
	const context = getContext<EChartsAreaSlots | undefined>(CONTEXT);
	if (!context) throw new Error('[EvilCharts] ECharts Dot must be nested inside Area.');
	return context;
}
```

`$lib/components/evilcharts/charts/echarts-area-chart/area.svelte`

```svelte
<script lang="ts">
	import type { Snippet } from 'svelte';
	import { useEChartsAreaChart } from './area-chart-context.svelte.js';
	import type { DitherVariant } from '$lib/components/evilcharts/ui/echarts-dither/index.js';
	import { setEChartsAreaSlots } from './area-slots.svelte.js';
	import {
		STROKE_WIDTH,
		type AreaAnimationType,
		type AreaVariant,
		type CurveType,
		type StrokeVariant
	} from './types.js';

	let {
		dataKey,
		variant = 'gradient',
		strokeVariant = 'dashed',
		strokeWidth = STROKE_WIDTH,
		curveType,
		animationType,
		connectNulls = false,
		isClickable = false,
		enableBufferLine = false,
		ditherVariant,
		children
	}: {
		dataKey: string;
		variant?: AreaVariant;
		strokeVariant?: StrokeVariant;
		strokeWidth?: number;
		curveType?: CurveType;
		animationType?: AreaAnimationType;
		connectNulls?: boolean;
		isClickable?: boolean;
		enableBufferLine?: boolean;
		ditherVariant?: DitherVariant;
		children?: Snippet;
	} = $props();

	const token = $props.id();
	const chart = useEChartsAreaChart();
	const slots = setEChartsAreaSlots();
	$effect(() =>
		chart.areas.register(token, () => ({
			dataKey,
			variant,
			strokeVariant,
			strokeWidth,
			curveType,
			animationType,
			connectNulls,
			isClickable,
			enableBufferLine,
			ditherVariant,
			dotVariant: slots.dots.first?.variant ?? 'none',
			activeDotVariant: slots.activeDots.first?.variant ?? 'none'
		}))
	);
</script>

{@render children?.()}
```

`$lib/components/evilcharts/charts/echarts-area-chart/dot.svelte`

```svelte
<script lang="ts">
	import type { DotVariant } from '$lib/components/evilcharts/ui/echarts-dot/index.js';
	import { useEChartsAreaSlots } from './area-slots.svelte.js';
	let { variant = 'default' }: { variant?: DotVariant } = $props();
	const token = $props.id();
	const slots = useEChartsAreaSlots();
	$effect(() => slots.dots.register(token, () => ({ variant })));
</script>
```

`$lib/components/evilcharts/charts/echarts-area-chart/grid.svelte`

```svelte
<script lang="ts">
	import { useEChartsAreaChart } from './area-chart-context.svelte.js';
	const token = $props.id();
	const chart = useEChartsAreaChart();
	$effect(() => chart.grids.register(token, () => ({})));
</script>
```

`$lib/components/evilcharts/charts/echarts-area-chart/index.ts`

```ts
import Root from './area-chart.svelte';
import Area from './area.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 '$lib/components/evilcharts/ui/echarts-tooltip/index.js';
import { Legend } from '$lib/components/evilcharts/ui/echarts-legend/index.js';
import { Brush } from '$lib/components/evilcharts/ui/echarts-brush/index.js';

type RootComponent = typeof Root;
export const EChartsAreaChart: RootComponent & {
	Area: typeof Area;
	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, { Area, Dot, ActiveDot, XAxis, YAxis, Grid, Tooltip, Legend, Brush });

export type {
	ChartAccessibility,
	ChartConfig,
	EChartsRenderer,
	EChartsRenderStyle
} from '$lib/components/evilcharts/ui/echarts-chart/index.js';
export type { DitherBloom, DitherVariant, RenderStyle } from '$lib/components/evilcharts/ui/echarts-dither/index.js';
export type { DotVariant } from '$lib/components/evilcharts/ui/echarts-dot/index.js';
export type { LegendVariant } from '$lib/components/evilcharts/ui/echarts-legend/index.js';
export type {
	TooltipPosition,
	TooltipRoundness,
	TooltipVariant
} from '$lib/components/evilcharts/ui/echarts-tooltip/index.js';
export type {
	AreaAnimationType,
	AreaVariant,
	CurveType,
	StackType,
	StrokeVariant
} from './types.js';
```

`$lib/components/evilcharts/charts/echarts-area-chart/option.ts`

```ts
import type { LineSeriesOption } from 'echarts/charts';
import type {
	DataZoomComponentOption,
	GridComponentOption,
	TooltipComponentOption
} from 'echarts/components';
import type { ComposeOption, EChartsType } from 'echarts/core';
import * as echarts from 'echarts/core';
import {
	flattenColor,
	getColorsCount,
	seriesPaint,
	withAlpha,
	type ChartConfig,
	type ResolvedColors
} from '$lib/components/evilcharts/ui/echarts-chart/index.js';
import { dotItemStyle, dotStyle, sampleGradient } from '$lib/components/evilcharts/ui/echarts-dot/index.js';
import { buildBrushDataZoom, type BrushRange } from '$lib/components/evilcharts/ui/echarts-brush/index.js';
import {
	createDitherPattern,
	type DitherBloom,
	type DitherVariant,
	type RenderStyle
} from '$lib/components/evilcharts/ui/echarts-dither/index.js';
import {
	tooltipBaseOption,
	tooltipIndicatorHtml,
	tooltipRow,
	tooltipShell
} from '$lib/components/evilcharts/ui/echarts-tooltip/index.js';
import {
	BUFFER_DASH,
	type AreaRegistration,
	type AxisRegistration,
	type BrushRegistration,
	type CurveType,
	type LegendRegistration,
	type StackType,
	type TooltipRegistration
} from './types.js';

export type EChartsAreaOption = ComposeOption<
	LineSeriesOption | GridComponentOption | TooltipComponentOption | DataZoomComponentOption
>;
type ArrayItem<T> = T extends readonly (infer Item)[] ? Item : T;
type XAxisOption = ArrayItem<NonNullable<EChartsAreaOption['xAxis']>>;
type YAxisOption = ArrayItem<NonNullable<EChartsAreaOption['yAxis']>>;

export type AreaOptionContext = {
	data: Record<string, unknown>[];
	config: ChartConfig;
	areas: AreaRegistration[];
	xDataKey?: string;
	curveType: CurveType;
	stackType: StackType;
	selectedDataKey: string | null;
	enableHoverHighlight: boolean;
	enableHoverReveal: boolean;
	hoverRevealIndex: number | null;
	xAxis?: AxisRegistration;
	yAxis?: AxisRegistration;
	showGrid: boolean;
	tooltip?: TooltipRegistration;
	legend?: LegendRegistration;
	brush?: BrushRegistration;
	brushRange: BrushRange;
	isLoading: boolean;
	loadingData: number[];
	resolved: ResolvedColors;
	animation: boolean;
	animationType: string;
	reducedMotion: boolean;
	rendererSize: { width: number; height: number };
	renderStyle: RenderStyle;
	ditherVariant: DitherVariant;
	ditherCellSize: number;
	bloom?: DitherBloom;
};

const ditherBloomBlur = (style: RenderStyle, bloom?: DitherBloom) =>
	style !== 'dither' || bloom === 'off' || bloom === undefined
		? 0
		: bloom === 'aura'
			? 14
			: bloom === 'high'
				? 8
				: 4;

const labelFor = (config: ChartConfig, key: string) =>
	typeof config[key]?.label === 'string' ? (config[key].label as string) : key;
const opacityFor = (selected: string | null, key: string) =>
	selected === null || selected === key ? 1 : 0.3;
function curveConfig(curve: CurveType) {
	return curve === 'step'
		? { smooth: false, step: 'middle' as const }
		: { smooth: curve !== 'linear', step: false as const };
}
function categoryKey(c: AreaOptionContext) {
	if (c.xDataKey) return c.xDataKey;
	if (c.xAxis?.dataKey) return c.xAxis.dataKey;
	const keys = new Set(c.areas.map((area) => area.dataKey));
	return Object.keys(c.data[0] ?? {}).find((key) => !keys.has(key));
}
function categories(c: AreaOptionContext) {
	const key = categoryKey(c);
	return c.data.map((row, i) => String((key ? row[key] : undefined) ?? i));
}
function finiteNumber(value: unknown): number | null {
	return typeof value === 'number' && Number.isFinite(value) ? value : null;
}
function rawValues(c: AreaOptionContext, key: string): (number | null)[] {
	return c.data.map((row) => finiteNumber(row[key]));
}

function ditherPlotBounds(c: AreaOptionContext, reverse = false) {
	const top = c.legend?.verticalAlign === 'top' ? 42 : 16;
	const showBrush = Boolean(c.brush) && !c.isLoading;
	const bottom =
		8 +
		(showBrush ? (c.brush?.height ?? 56) + 30 + (c.xAxis?.label ? 22 : 0) : 0) +
		(c.legend?.verticalAlign === 'bottom' ? 34 : 0);
	return {
		height: Math.max(c.ditherCellSize, c.rendererSize.height - top - bottom),
		offsetY: top,
		reverse
	};
}
function values(c: AreaOptionContext, key: string) {
	const raw = rawValues(c, key);
	if (c.stackType !== 'expanded') return raw;
	return raw.map((value, index) => {
		if (value === null) return null;
		const total = c.areas.reduce((sum, area) => sum + (rawValues(c, area.dataKey)[index] ?? 0), 0);
		return total ? value / total : 0;
	});
}
function axes(c: AreaOptionContext): { xAxis: XAxisOption; yAxis: YAxisOption } {
	const { mutedForeground, border, background } = c.resolved.tokens;
	const dotColor = flattenColor(border, background);
	return {
		xAxis: {
			type: 'category',
			boundaryGap: false,
			data: c.isLoading ? c.loadingData.map((_, i) => i) : categories(c),
			name: c.isLoading ? undefined : c.xAxis?.label,
			nameLocation: 'middle',
			nameGap: 30,
			nameTextStyle: { color: mutedForeground, fontSize: 10 },
			axisLine: { show: false },
			axisTick: {
				show: !c.isLoading && Boolean(c.xAxis) && !c.xAxis?.hideDots,
				alignWithLabel: true,
				length: 0.5,
				lineStyle: { color: dotColor, width: 3, cap: 'round' }
			},
			axisLabel: {
				show: !c.isLoading && Boolean(c.xAxis),
				color: mutedForeground,
				fontSize: 10,
				margin: 8,
				formatter: c.xAxis?.tickFormatter
			},
			splitLine: { show: false }
		},
		yAxis: {
			type: 'value',
			max: c.stackType === 'expanded' ? 1 : undefined,
			name: c.isLoading ? undefined : c.yAxis?.label,
			nameLocation: 'middle',
			nameGap: 38,
			nameTextStyle: { color: mutedForeground, fontSize: 10 },
			axisLine: { show: false },
			axisTick: {
				show: !c.isLoading && Boolean(c.yAxis) && !c.yAxis?.hideDots,
				length: 0.5,
				lineStyle: { color: dotColor, width: 3, cap: 'round' }
			},
			axisLabel: {
				show: !c.isLoading && Boolean(c.yAxis),
				color: mutedForeground,
				fontSize: 10,
				margin: 8,
				formatter:
					c.stackType === 'expanded'
						? (value: number) => `${Math.round(value * 100)}%`
						: c.yAxis?.tickFormatter
			},
			splitLine: {
				show: c.showGrid && !c.isLoading,
				lineStyle: { color: border, type: [3, 3], width: 1 }
			}
		}
	};
}
type ImagePattern = {
	image: HTMLCanvasElement;
	repeat: 'repeat' | 'no-repeat';
	rotation?: number;
	scaleX?: number;
	scaleY?: number;
};

function nativePatternFill(
	kind: 'dotted' | 'lines' | 'hatched' | 'stripe',
	color: string
): ImagePattern | null {
	if (typeof document === 'undefined') return null;
	const dpr = Math.max(window.devicePixelRatio || 1, 1);
	const canvas = document.createElement('canvas');
	const context = canvas.getContext('2d');
	if (!context) return null;
	const size = (width: number, height: number) => {
		canvas.width = width * dpr;
		canvas.height = height * dpr;
		context.scale(dpr, dpr);
	};
	const pattern = (rotation = 0): ImagePattern => ({
		image: canvas,
		repeat: 'repeat',
		rotation,
		scaleX: 1 / dpr,
		scaleY: 1 / dpr
	});
	if (kind === 'dotted') {
		size(6, 6);
		context.fillStyle = withAlpha(color, 0.7);
		context.beginPath();
		context.arc(3, 3, 0.85, 0, Math.PI * 2);
		context.fill();
		return pattern();
	}
	if (kind === 'lines' || kind === 'stripe') {
		size(5, 5);
		context.strokeStyle = withAlpha(color, 0.3);
		context.lineWidth = 1;
		context.beginPath();
		context.moveTo(2.5, -1);
		context.lineTo(2.5, 6);
		context.stroke();
		return pattern(-Math.PI / 4);
	}
	size(20, 20);
	context.fillStyle = withAlpha(color, 0.06);
	context.fillRect(0, 0, 10, 20);
	context.fillStyle = withAlpha(color, 0.22);
	context.fillRect(10, 0, 10, 20);
	return pattern((20 * Math.PI) / 180);
}

function gradientFillTexture(slots: string[], width: number, height: number, reverse: boolean) {
	if (typeof document === 'undefined' || width < 1 || height < 1) return null;
	const canvas = document.createElement('canvas');
	canvas.width = Math.ceil(width);
	canvas.height = Math.ceil(height);
	const context = canvas.getContext('2d');
	if (!context) return null;
	const colors = context.createLinearGradient(0, 0, canvas.width, 0);
	for (const [index, color] of slots.entries()) {
		colors.addColorStop(index / (slots.length - 1), color);
	}
	context.fillStyle = colors;
	context.fillRect(0, 0, canvas.width, canvas.height);
	const fade = context.createLinearGradient(0, 0, 0, canvas.height);
	fade.addColorStop(0, `rgba(0, 0, 0, ${reverse ? 0 : 0.1})`);
	fade.addColorStop(1, `rgba(0, 0, 0, ${reverse ? 0.1 : 0})`);
	context.globalCompositeOperation = 'destination-in';
	context.fillStyle = fade;
	context.fillRect(0, 0, canvas.width, canvas.height);
	return canvas;
}

function patternFadeTexture(
	kind: 'dotted' | 'lines' | 'hatched',
	color: string,
	width: number,
	height: number
) {
	const source = nativePatternFill(kind, color);
	if (!source || typeof document === 'undefined' || width < 1 || height < 1) return null;
	const tile = source.image;
	const canvas = document.createElement('canvas');
	canvas.width = Math.ceil(width);
	canvas.height = Math.ceil(height);
	const context = canvas.getContext('2d');
	if (!context) return null;
	const pattern = context.createPattern(tile, 'repeat');
	if (!pattern) return null;
	if (typeof pattern.setTransform === 'function') {
		const transform = new DOMMatrix();
		transform.rotateSelf(((source.rotation ?? 0) * 180) / Math.PI);
		transform.scaleSelf(source.scaleX ?? 1, source.scaleY ?? 1);
		pattern.setTransform(transform);
	}
	context.fillStyle = pattern;
	context.fillRect(0, 0, canvas.width, canvas.height);
	const fade = context.createLinearGradient(0, 0, 0, canvas.height);
	fade.addColorStop(0, 'rgba(0, 0, 0, 1)');
	fade.addColorStop(1, 'rgba(0, 0, 0, 0)');
	context.globalCompositeOperation = 'destination-in';
	context.fillStyle = fade;
	context.fillRect(0, 0, canvas.width, canvas.height);
	return canvas;
}

function areaPaint(
	c: AreaOptionContext,
	area: AreaRegistration,
	slots: string[],
	showUnselected = false
) {
	const color = slots[0] ?? c.resolved.tokens.foreground;
	if (area.variant === 'none') return 'transparent';
	if (c.renderStyle === 'dither') {
		return createDitherPattern(
			slots,
			area.ditherVariant ?? c.ditherVariant,
			c.ditherCellSize,
			0.8,
			ditherPlotBounds(c, area.variant === 'gradient-reverse')
		);
	}
	if (showUnselected) return nativePatternFill('stripe', color) ?? withAlpha(color, 0.1);
	if (area.variant === 'solid') {
		if (slots.length > 1)
			return new echarts.graphic.LinearGradient(
				0,
				0,
				1,
				0,
				slots.map((slot, index) => ({
					offset: index / (slots.length - 1),
					color: withAlpha(slot, 0.1)
				}))
			);
		return withAlpha(color, 0.1);
	}
	if (area.variant === 'dotted' || area.variant === 'hatched' || area.variant === 'lines') {
		const texture = patternFadeTexture(
			area.variant,
			color,
			c.rendererSize.width,
			c.rendererSize.height
		);
		return texture
			? ({ image: texture, repeat: 'no-repeat' } as ImagePattern)
			: (nativePatternFill(area.variant, color) ?? withAlpha(color, 0.1));
	}
	const reverse = area.variant === 'gradient-reverse';
	if (slots.length > 1) {
		const texture = gradientFillTexture(
			slots,
			c.rendererSize.width,
			c.rendererSize.height,
			reverse
		);
		if (texture) return { image: texture, repeat: 'no-repeat' } as ImagePattern;
	}
	const transparent = withAlpha(color, 0);
	return new echarts.graphic.LinearGradient(0, reverse ? 1 : 0, 0, reverse ? 0 : 1, [
		{ offset: 0, color: withAlpha(color, 0.1) },
		{ offset: 1, color: transparent }
	]);
}
function tooltip(c: AreaOptionContext): TooltipComponentOption {
	const slot = c.tooltip;
	return {
		...tooltipBaseOption({
			present: Boolean(slot) && !c.isLoading,
			cursor: slot?.cursor ?? true,
			position: slot?.position ?? 'variable',
			axisPointerColor: c.resolved.tokens.border,
			strokeWidth: 0.8
		}),
		formatter: (raw) => {
			const params = (Array.isArray(raw) ? raw : [raw]) as Array<{
				seriesId?: string;
				seriesName?: string;
				axisValueLabel?: string;
				value?: unknown;
				data?: unknown;
			}>;
			const seen = new Set<string>();
			const body = params
				.map((p) => {
					const rawId = p.seriesId ?? '';
					const key = rawId.startsWith('__buffer-')
						? rawId.slice('__buffer-'.length)
						: rawId.startsWith('__')
							? ''
							: rawId || p.seriesName || '';
					if (!key || seen.has(key)) return '';
					const value =
						typeof p.data === 'object' && p.data && 'value' in p.data
							? (p.data as { value: unknown }).value
							: p.value;
					const numericValue = finiteNumber(value);
					if (numericValue === null) return '';
					seen.add(key);
					return tooltipRow({
						indicatorHtml: tooltipIndicatorHtml(key, getColorsCount(c.config[key] ?? {})),
						labelText: labelFor(c.config, key),
						valueText:
							c.stackType === 'expanded'
								? `${Math.round(numericValue * 100)}%`
								: numericValue.toLocaleString(),
						dimmed: opacityFor(c.selectedDataKey, key) < 1 ? ' opacity-30' : ''
					});
				})
				.join('');
			return tooltipShell({
				label: params[0]?.axisValueLabel ?? '',
				body,
				roundness: slot?.roundness ?? 'lg',
				variant: slot?.variant ?? 'default'
			});
		}
	};
}
function series(c: AreaOptionContext): LineSeriesOption[] {
	if (c.isLoading)
		return [
			{
				id: '__loading',
				type: 'line',
				data: c.loadingData,
				...curveConfig(c.curveType),
				showSymbol: false,
				silent: true,
				// The rAF shimmer reveals the fixed stroke and fill together. Keeping the
				// resting paint transparent prevents a solid line between sweeps.
				lineStyle: { color: withAlpha(c.resolved.tokens.foreground, 0), width: 1 },
				areaStyle: { color: withAlpha(c.resolved.tokens.foreground, 0) },
				z: 1,
				animation: false
			}
		];
	return c.areas.flatMap((area, index) => {
		const vals = values(c, area.dataKey);
		const slots = c.resolved.series[area.dataKey] ?? [c.resolved.tokens.foreground];
		const showUnselected = c.selectedDataKey !== null && c.selectedDataKey !== area.dataKey;
		const bloomBlur = ditherBloomBlur(c.renderStyle, c.bloom);
		const bloomColor =
			bloomBlur > 0 ? withAlpha(slots[0] ?? c.resolved.tokens.foreground, 0.55) : undefined;
		const ditherStroke = c.renderStyle === 'dither' && area.strokeVariant !== 'animated-dashed';
		const paint = ditherStroke
			? createDitherPattern(
					slots,
					area.ditherVariant ?? c.ditherVariant,
					c.ditherCellSize,
					1,
					ditherPlotBounds(c)
				)
			: seriesPaint(slots);
		const strokePaint =
			c.enableHoverReveal && slots.length > 1 && c.renderStyle === 'native'
				? new echarts.graphic.LinearGradient(
						8,
						0,
						Math.max(c.rendererSize.width - 8, 9),
						0,
						slots.map((color, slotIndex) => ({
							offset: slotIndex / (slots.length - 1),
							color
						})),
						true
					)
				: paint;
		const dotPaint = seriesPaint(slots);
		const curve = curveConfig(area.curveType ?? c.curveType);
		const opacity = opacityFor(c.selectedDataKey, area.dataKey);
		const dot = dotStyle(area.dotVariant, dotPaint, c.resolved.tokens.background);
		const activeDot = dotStyle(area.activeDotVariant, dotPaint, c.resolved.tokens.background);
		const hasBuffer = !c.enableHoverReveal && area.enableBufferLine && vals.length > 1;
		const revealActive =
			c.enableHoverReveal && c.hoverRevealIndex !== null && c.selectedDataKey === null;
		const visibleValues = revealActive
			? vals.map((value, valueIndex) => (valueIndex <= c.hoverRevealIndex! ? value : null))
			: hasBuffer
				? vals.map((v, i) => (i === vals.length - 1 ? null : v))
				: vals;
		const data = visibleValues.map((v, i) =>
			v === null || (area.dotVariant === 'none' && area.activeDotVariant === 'none')
				? v
				: {
						value: v,
						itemStyle: dotItemStyle(
							area.dotVariant,
							sampleGradient(slots, vals.length > 1 ? i / (vals.length - 1) : 0),
							c.resolved.tokens.background
						)
					}
		);
		const bufferData = vals.map((v, i) => {
			if (i < vals.length - 2 || v === null) return null;
			if (area.dotVariant === 'none' && area.activeDotVariant === 'none') return v;
			return {
				value: v,
				itemStyle: dotItemStyle(
					area.dotVariant,
					sampleGradient(slots, vals.length > 1 ? i / (vals.length - 1) : 0),
					c.resolved.tokens.background
				)
			};
		});
		const result: LineSeriesOption[] = [];
		if (c.enableHoverReveal)
			result.push({
				id: `__reveal-${area.dataKey}`,
				type: 'line',
				data: revealActive
					? vals.map((value, valueIndex) => (valueIndex < c.hoverRevealIndex! ? null : value))
					: vals,
				stack: c.stackType === 'default' ? undefined : '__area-reveal-stack',
				smooth: curve.smooth,
				step: curve.step,
				connectNulls: area.connectNulls,
				showSymbol: false,
				silent: true,
				z: index,
				lineStyle: {
					color: c.resolved.tokens.mutedForeground,
					width: area.strokeWidth,
					type: hasBuffer || area.strokeVariant === 'solid' ? 'solid' : [3, 3],
					opacity: revealActive ? 0.3 : 0
				},
				emphasis: { disabled: true },
				blur: { lineStyle: { opacity: revealActive ? 0.3 : 0 } },
				tooltip: { show: false },
				animation: false
			});
		result.push({
			id: area.dataKey,
			name: labelFor(c.config, area.dataKey),
			type: 'line',
			data,
			stack: c.stackType === 'default' ? undefined : '__area-stack',
			smooth: curve.smooth,
			step: curve.step,
			connectNulls: area.connectNulls,
			showSymbol: dot.size > 0,
			symbol: 'circle',
			symbolSize: dot.size > 0 ? dot.size : activeDot.size,
			cursor: area.isClickable ? 'pointer' : 'default',
			triggerEvent: area.isClickable,
			z: c.selectedDataKey === area.dataKey ? 3 : c.selectedDataKey === null ? 2 : 1,
			lineStyle: {
				color: strokePaint,
				width: ditherStroke ? Math.max(area.strokeWidth, c.ditherCellSize) : area.strokeWidth,
				type: ditherStroke
					? [c.ditherCellSize, c.ditherCellSize]
					: hasBuffer || area.strokeVariant === 'solid'
						? 'solid'
						: [3, 3],
				opacity,
				shadowBlur: bloomBlur,
				shadowColor: bloomColor
			},
			itemStyle: { ...dot.itemStyle, opacity },
			areaStyle: {
				color: areaPaint(c, area, slots, showUnselected),
				opacity: c.selectedDataKey === null || c.selectedDataKey === area.dataKey ? 0.8 : 0.1,
				shadowBlur: bloomBlur,
				shadowColor: bloomColor
			},
			emphasis: {
				focus:
					c.enableHoverHighlight && !c.enableHoverReveal && c.selectedDataKey === null
						? 'series'
						: 'none',
				scale: dot.size > 0 ? activeDot.size / Math.max(dot.size, 1) : 1,
				itemStyle: { ...activeDot.itemStyle, opacity: 1 }
			},
			blur: {
				lineStyle: { opacity: 0.3 },
				areaStyle: { opacity: 0.1 },
				itemStyle: { opacity: 0.3 }
			},
			animation:
				c.animation && (area.animationType ?? c.animationType) !== 'none' && !c.reducedMotion,
			animationDuration: 1000,
			animationDurationUpdate: 0
		});
		if (hasBuffer)
			result.push({
				id: `__buffer-${area.dataKey}`,
				type: 'line',
				data: bufferData,
				stack: c.stackType === 'default' ? undefined : '__area-buffer-stack',
				smooth: curve.smooth,
				step: curve.step,
				connectNulls: true,
				showSymbol: dot.size > 0,
				symbol: 'circle',
				symbolSize: dot.size > 0 ? dot.size : activeDot.size,
				silent: true,
				z: c.selectedDataKey === area.dataKey ? 3 : c.selectedDataKey === null ? 2 : 1,
				lineStyle: {
					color: paint,
					width: area.strokeWidth,
					type: BUFFER_DASH,
					opacity,
					shadowBlur: bloomBlur,
					shadowColor: bloomColor
				},
				itemStyle: { ...dot.itemStyle, opacity },
				emphasis: {
					disabled: false,
					scale: dot.size > 0 ? activeDot.size / Math.max(dot.size, 1) : 1,
					itemStyle: { ...activeDot.itemStyle, opacity: 1 }
				},
				blur: { lineStyle: { opacity: 0.3 }, itemStyle: { opacity: 0.3 } },
				areaStyle: { opacity: 0 },
				animation: false
			});
		if (hasBuffer)
			result.push({
				id: `__bufferfill-${area.dataKey}`,
				type: 'line',
				data: vals.map((v, i) => (i >= vals.length - 2 ? v : null)),
				stack: c.stackType === 'default' ? undefined : '__area-buffer-fill-stack',
				smooth: curve.smooth,
				step: curve.step,
				connectNulls: true,
				showSymbol: false,
				silent: true,
				z: index,
				lineStyle: { opacity: 0 },
				areaStyle: {
					color: areaPaint(c, area, slots, showUnselected),
					opacity: c.selectedDataKey === null || c.selectedDataKey === area.dataKey ? 0.8 : 0.1,
					shadowBlur: bloomBlur,
					shadowColor: bloomColor
				},
				animation: false,
				tooltip: { show: false }
			});
		return result;
	});
}
export function buildAreaOption(c: AreaOptionContext): EChartsAreaOption {
	const showBrush = Boolean(c.brush) && !c.isLoading;
	const height = c.brush?.height ?? 56;
	const bottom = c.legend?.verticalAlign === 'bottom' ? 34 : 6;
	const main: GridComponentOption = {
		left: 8,
		right: 8,
		top: c.legend?.verticalAlign === 'top' ? 42 : 16,
		bottom:
			8 +
			(showBrush ? height + 30 + (c.xAxis?.label ? 22 : 0) : 0) +
			(c.legend?.verticalAlign === 'bottom' ? 34 : 0)
	};
	const builtAxes = axes(c);
	const mainSeries = series(c);
	if (!showBrush)
		return {
			animation: false,
			aria: { enabled: true },
			grid: main,
			xAxis: builtAxes.xAxis,
			yAxis: builtAxes.yAxis,
			tooltip: tooltip(c),
			series: mainSeries
		};
	const mini = c.areas.map((area) => {
		const key = area.dataKey;
		const base = c.resolved.series[key]?.[0] ?? c.resolved.tokens.foreground;
		const curve = curveConfig(area.curveType ?? c.curveType);
		const selected = c.selectedDataKey === null || c.selectedDataKey === key;
		return {
			id: `__mini-${key}`,
			type: 'line' as const,
			xAxisIndex: 1,
			yAxisIndex: 1,
			data: rawValues(c, key),
			stack: c.stackType === 'default' ? undefined : '__mini-total',
			smooth: curve.smooth,
			step: curve.step,
			connectNulls: area.connectNulls,
			showSymbol: false,
			silent: true,
			emphasis: { disabled: true },
			tooltip: { show: false },
			lineStyle: {
				color: base,
				width: 1,
				opacity: 0.5 * (selected ? 1 : 0.3)
			},
			areaStyle: {
				color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
					{ offset: 0, color: withAlpha(base, 0.15 * (selected ? 1 : 0.125)) },
					{ offset: 1, color: withAlpha(base, 0) }
				])
			},
			z: 0,
			animation: false
		};
	});
	return {
		animation: false,
		aria: { enabled: true },
		grid: [main, { left: 8, right: 8, bottom, height, outerBoundsMode: 'none' }],
		xAxis: [
			builtAxes.xAxis,
			{
				type: 'category',
				gridIndex: 1,
				boundaryGap: false,
				data: categories(c),
				show: false,
				axisPointer: { show: false }
			}
		],
		yAxis: [builtAxes.yAxis, { type: 'value', gridIndex: 1, show: false }],
		tooltip: tooltip(c),
		dataZoom: buildBrushDataZoom({
			brushBottom: bottom,
			brushHeight: height,
			brushRange: c.brushRange,
			fillerColor: 'transparent'
		}),
		series: [...mainSeries, ...mini]
	};
}
export function createAreaLoadingData(points: number): number[] {
	const rows: number[] = [];
	let value = 30 + Math.random() * 20;
	for (let index = 0; index < Math.max(0, points); index += 1) {
		value = Math.min(58, Math.max(16, value + (Math.random() - 0.5) * 16));
		rows.push(Math.round(value));
	}
	return rows;
}

export function computeAreaPlottedTops(c: AreaOptionContext): Record<string, (number | null)[]> {
	const running = new Array(c.data.length).fill(0);
	const tops: Record<string, (number | null)[]> = {};
	for (const area of c.areas) {
		const plotted = values(c, area.dataKey);
		tops[area.dataKey] = plotted.map((value, index) => {
			if (value === null) return null;
			return c.stackType === 'default' ? value : (running[index] += value);
		});
	}
	return tops;
}

export function resolveAreaAtPixel(
	chart: EChartsType,
	tops: Record<string, (number | null)[]>,
	keys: string[],
	x: number,
	y: number
): string | null {
	if (keys.length < 2 || !chart.containPixel({ gridIndex: 0 }, [x, y])) return null;
	const converted = chart.convertFromPixel({ gridIndex: 0 }, [x, y]);
	const rawIndex = Array.isArray(converted) ? converted[0] : converted;
	if (typeof rawIndex !== 'number') return null;
	const index = Math.round(rawIndex);
	let nearest: string | null = null;
	let nearestDistance = Number.POSITIVE_INFINITY;
	let above: string | null = null;
	let abovePixelY = Number.NEGATIVE_INFINITY;
	for (const key of keys) {
		const value = tops[key]?.[index];
		if (value === null || value === undefined) continue;
		const point = chart.convertToPixel({ gridIndex: 0 }, [index, value]);
		const pixelY = Array.isArray(point) ? point[1] : undefined;
		if (typeof pixelY !== 'number') continue;
		const distance = Math.abs(pixelY - y);
		if (distance < nearestDistance) {
			nearestDistance = distance;
			nearest = key;
		}
		if (pixelY <= y && pixelY > abovePixelY) {
			abovePixelY = pixelY;
			above = key;
		}
	}
	return nearestDistance <= 10 ? nearest : above;
}
```

`$lib/components/evilcharts/charts/echarts-area-chart/types.ts`

```ts
import type { DotVariant } from '$lib/components/evilcharts/ui/echarts-dot/index.js';
import type { DitherVariant } from '$lib/components/evilcharts/ui/echarts-dither/index.js';

export const STROKE_WIDTH = 0.8;
export const BUFFER_DASH: [number, number] = [4, 3];

export type AreaVariant =
	'gradient' | 'gradient-reverse' | 'solid' | 'dotted' | 'lines' | 'hatched' | 'none';
export type StrokeVariant = 'solid' | 'dashed' | 'animated-dashed';
export type StackType = 'default' | 'stacked' | 'expanded';
export type AreaAnimationType =
	'none' | 'left-to-right' | 'right-to-left' | 'center-out' | 'edges-in';
export type CurveType =
	'linear' | 'smooth' | 'bump' | 'monotone' | 'monotoneX' | 'monotoneY' | 'natural' | 'step';

export type AreaRegistration = {
	dataKey: string;
	variant: AreaVariant;
	strokeVariant: StrokeVariant;
	strokeWidth: number;
	curveType?: CurveType;
	animationType?: AreaAnimationType;
	connectNulls: boolean;
	isClickable: boolean;
	enableBufferLine: boolean;
	dotVariant: DotVariant;
	activeDotVariant: DotVariant;
	ditherVariant?: DitherVariant;
};

export type AxisRegistration = {
	dataKey?: string;
	tickFormatter?: (value: string | number, index: number) => string;
	label?: string;
	hideDots: boolean;
};

export type TooltipRegistration = {
	variant: import('$lib/components/evilcharts/ui/echarts-tooltip/index.js').TooltipVariant;
	roundness: import('$lib/components/evilcharts/ui/echarts-tooltip/index.js').TooltipRoundness;
	cursor?: boolean;
	defaultIndex?: number;
	position: import('$lib/components/evilcharts/ui/echarts-tooltip/index.js').TooltipPosition;
};

export type LegendRegistration = {
	variant: import('$lib/components/evilcharts/ui/echarts-legend/index.js').LegendVariant;
	align: 'left' | 'center' | 'right';
	verticalAlign: 'top' | 'middle' | 'bottom';
	isClickable: boolean;
};

export type BrushRegistration = {
	height?: number;
	formatLabel?: (value: string, index: number) => string;
	onChange?: (range: { startIndex: number; endIndex: number }) => void;
};
```

`$lib/components/evilcharts/charts/echarts-area-chart/x-axis.svelte`

```svelte
<script lang="ts">
	import { useEChartsAreaChart } from './area-chart-context.svelte.js';
	let {
		dataKey,
		tickFormatter,
		label,
		hideDots = false
	}: {
		dataKey?: string;
		tickFormatter?: (value: string, index: number) => string;
		label?: string;
		hideDots?: boolean;
	} = $props();
	const token = $props.id();
	const chart = useEChartsAreaChart();
	$effect(() =>
		chart.xAxes.register(token, () => ({
			dataKey,
			tickFormatter: tickFormatter as
				((value: string | number, index: number) => string) | undefined,
			label,
			hideDots
		}))
	);
</script>
```

`$lib/components/evilcharts/charts/echarts-area-chart/y-axis.svelte`

```svelte
<script lang="ts">
	import { useEChartsAreaChart } from './area-chart-context.svelte.js';
	let {
		dataKey,
		tickFormatter,
		label,
		hideDots = false
	}: {
		dataKey?: string;
		tickFormatter?: (value: number, index: number) => string;
		label?: string;
		hideDots?: boolean;
	} = $props();
	const token = $props.id();
	const chart = useEChartsAreaChart();
	$effect(() =>
		chart.yAxes.register(token, () => ({
			dataKey,
			tickFormatter: tickFormatter as
				((value: string | number, index: number) => string) | undefined,
			label,
			hideDots
		}))
	);
</script>
```
        
      
      
        ### Add the shared chart module.
        

Create a `ui` folder inside `evilcharts` and paste this one in first — it resolves your config's colors from the page's CSS variables, and every sub-component below imports from it.


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

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

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

	type Props = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
		config: ChartConfig;
		children?: Snippet;
		overlay?: Snippet;
		footer?: Snippet;
		initialDimension?: { width: number; height: number };
		dimension?: { width: number; height: number };
		element?: HTMLDivElement;
		themeRevision?: number;
		accessibility?: ChartAccessibility;
	};

	let {
		id,
		config,
		children,
		overlay,
		footer,
		initialDimension = { width: 320, height: 200 },
		dimension = $bindable(),
		element = $bindable(),
		themeRevision = $bindable(0),
		accessibility,
		class: className,
		...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;
	});

	$effect.pre(() => validateChartConfigColors(config));

	function observeTheme(node: HTMLElement) {
		const observer = new MutationObserver(() => {
			themeRevision += 1;
		});
		const options = {
			attributes: true,
			attributeFilter: ['class', 'style']
		};
		observer.observe(document.documentElement, options);
		if (node !== document.documentElement) observer.observe(node, options);
		return () => observer.disconnect();
	}
</script>

<div
	{@attach observeTheme}
	bind:this={element}
	data-slot="chart"
	data-chart={chartId}
	role={accessibility ? 'group' : undefined}
	aria-label={accessibility?.label}
	aria-labelledby={accessibility?.labelledBy}
	aria-describedby={describedBy}
	class={cn(
		'relative flex min-h-0 w-full flex-1 flex-col justify-center text-xs',
		!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 overlay?.()}
	{@render footer?.()}
</div>
```

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

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

	let { id, config }: { id: string; config: ChartConfig } = $props();
	const css = $derived(buildChartCss(id, config));
</script>

{#if css}
	<svelte:element this={"style"}>{css}</svelte:element>
{/if}
```

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

```ts
import * as echarts from 'echarts/core';
import { THEMES, THEME_KEYS, type ChartConfig, type ThemeKey } from './types.js';

const ENCODED_TOKEN = /^u-(?:[0-9a-f]{6})+$/;

export function chartColorToken(key: string): string {
	if (/^[A-Za-z0-9_-]+$/.test(key) && !ENCODED_TOKEN.test(key)) return key;
	return `u-${Array.from(key, (character) =>
		(character.codePointAt(0) ?? 0).toString(16).padStart(6, '0')
	).join('')}`;
}

export function chartColorVariableName(key: string, index: number): string {
	return `--color-${chartColorToken(key)}-${index}`;
}

export function chartColorVariable(key: string, index: number, fallbackIndex?: number): string {
	const name = chartColorVariableName(key, index);
	return fallbackIndex === undefined
		? `var(${name})`
		: `var(${name}, var(${chartColorVariableName(key, fallbackIndex)}))`;
}

export function quoteCssString(value: string): string {
	return `"${Array.from(value, (character) => {
		const codePoint = character.codePointAt(0) ?? 0;
		if (character === '"' || character === '\\') return `\\${character}`;
		if (codePoint === 0) return '\uFFFD';
		if (codePoint < 0x20 || codePoint === 0x7f) return `\\${codePoint.toString(16)} `;
		return character;
	}).join('')}"`;
}

export function getColorsCount(item: ChartConfig[string]): number {
	if (!item.colors) return 1;
	return Math.max(...THEME_KEYS.map((theme) => item.colors?.[theme]?.length ?? 0), 1);
}

export function distributeColors(colors: string[], maxCount: number): string[] {
	if (colors.length === 0) return [];
	if (colors.length >= maxCount) return colors.slice(0, maxCount);

	const result: string[] = [];
	const baseSlots = Math.floor(maxCount / colors.length);
	const extraSlots = maxCount % colors.length;
	for (let index = 0; index < colors.length; index += 1) {
		const slots = baseSlots + (index >= colors.length - extraSlots ? 1 : 0);
		for (let slot = 0; slot < slots; slot += 1) result.push(colors[index]);
	}
	return result;
}

export function buildChartCss(id: string, config: ChartConfig): string {
	const colorConfig = Object.entries(config).filter(([, item]) => item.colors);
	if (colorConfig.length === 0) return '';

	const variablesFor = (theme: ThemeKey) =>
		colorConfig
			.flatMap(([key, item]) => {
				const authored = item.colors?.[theme];
				if (!authored?.length) return [];
				return distributeColors(authored, getColorsCount(item)).map(
					(color, index) => `  ${chartColorVariableName(key, index)}: ${color};`
				);
			})
			.join('\n');

	return Object.entries(THEMES)
		.map(
			([theme, prefix]) =>
				`${prefix} [data-chart=${quoteCssString(id)}] {\n${variablesFor(theme as ThemeKey)}\n}`
		)
		.join('\n');
}

let normalizerContext: CanvasRenderingContext2D | null = null;

export function normalizeColor(value: string): string {
	const raw = value.trim();
	if (!raw || typeof document === 'undefined') return raw;

	if (!normalizerContext) {
		const canvas = document.createElement('canvas');
		canvas.width = 1;
		canvas.height = 1;
		normalizerContext = canvas.getContext('2d', { willReadFrequently: true });
	}
	if (!normalizerContext) return raw;

	normalizerContext.clearRect(0, 0, 1, 1);
	normalizerContext.fillStyle = '#000';
	normalizerContext.fillStyle = raw;
	normalizerContext.fillRect(0, 0, 1, 1);
	const [red, green, blue, alpha] = normalizerContext.getImageData(0, 0, 1, 1).data;
	return `rgba(${red}, ${green}, ${blue}, ${(alpha / 255).toFixed(3)})`;
}

export function withAlpha(color: string, alpha: number): string {
	const match = color.match(/rgba?\(([^)]+)\)/);
	if (!match) return color;
	const [red, green, blue, sourceAlpha] = match[1].split(',').map((part) => part.trim());
	const baseAlpha = sourceAlpha === undefined ? 1 : Number.parseFloat(sourceAlpha) || 0;
	return `rgba(${red}, ${green}, ${blue}, ${(baseAlpha * alpha).toFixed(3)})`;
}

export type ResolvedColors = {
	series: Record<string, string[]>;
	tokens: {
		mutedForeground: string;
		border: string;
		foreground: string;
		background: string;
	};
};

export function resolveColors(
	container: HTMLElement,
	config: ChartConfig,
	seriesKeys: string[]
): ResolvedColors {
	const computed = getComputedStyle(container);
	const series: Record<string, string[]> = {};

	for (const key of seriesKeys) {
		const count = getColorsCount(config[key] ?? {});
		series[key] = Array.from({ length: count }, (_, index) => {
			const raw = computed.getPropertyValue(chartColorVariableName(key, index)).trim();
			return raw ? normalizeColor(raw) : 'rgba(120, 120, 120, 1)';
		});
	}

	const probe = document.createElement('span');
	probe.style.cssText = 'position:absolute;width:0;height:0;visibility:hidden;pointer-events:none;';
	container.appendChild(probe);
	const readToken = (className: string) => {
		probe.className = className;
		return normalizeColor(getComputedStyle(probe).color);
	};
	const tokens = {
		mutedForeground: readToken('text-muted-foreground'),
		border: readToken('text-border'),
		foreground: readToken('text-foreground'),
		background: readToken('text-background')
	};
	probe.remove();

	return { series, tokens };
}

export function seriesPaint(slots: string[]): string | echarts.graphic.LinearGradient {
	if (slots.length <= 1) return slots[0] ?? 'rgba(120, 120, 120, 1)';
	return new echarts.graphic.LinearGradient(
		0,
		0,
		1,
		0,
		slots.map((color, index) => ({ offset: index / (slots.length - 1), color }))
	);
}

export function indicatorBackground(key: string, colorsCount: number): string {
	if (colorsCount <= 1) return chartColorVariable(key, 0);
	const stops = Array.from({ length: colorsCount }, (_, index) => {
		const offset = (index / (colorsCount - 1)) * 100;
		return `${chartColorVariable(key, index)} ${offset}%`;
	}).join(', ');
	return `linear-gradient(to right, ${stops})`;
}

export function flattenColor(color: string, base: string): string {
	const parse = (value: string) =>
		value
			.match(/rgba?\(([^)]+)\)/)?.[1]
			.split(',')
			.map((part) => Number.parseFloat(part)) ?? [0, 0, 0, 1];
	const [red, green, blue, alpha = 1] = parse(color);
	const [baseRed, baseGreen, baseBlue] = parse(base);
	const mix = (channel: number, baseChannel: number) =>
		Math.round(channel * alpha + baseChannel * (1 - alpha));
	return `rgb(${mix(red, baseRed)}, ${mix(green, baseGreen)}, ${mix(blue, baseBlue)})`;
}
```

`$lib/components/evilcharts/ui/echarts-chart/echarts-host.svelte`

```svelte
<script lang="ts">
	import type { HTMLAttributes } from 'svelte/elements';
	import type { EChartsCoreOption, EChartsType, SetOptionOpts } from 'echarts/core';
	import { CanvasRenderer, SVGRenderer } from 'echarts/renderers';
	import * as echarts from 'echarts/core';
	import { cn } from '$lib/utils.js';
	import type { EChartsRenderer } from './types.js';

	// Register renderers in the module that calls `echarts.init`. Keeping this beside the runtime
	// use prevents production tree-shaking from dropping a side-effect-only barrel registration.
	echarts.use([CanvasRenderer, SVGRenderer]);

	export type EChartsEventHandler = (params: unknown) => void;

	type Props = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
		option: EChartsCoreOption;
		renderer: EChartsRenderer;
		instance?: EChartsType;
		events?: Record<string, EChartsEventHandler>;
		setOptionOptions?: SetOptionOpts;
		hideSource?: boolean;
	};

	let {
		option,
		renderer,
		instance = $bindable(),
		events = {},
		setOptionOptions = { notMerge: true, lazyUpdate: false },
		hideSource = false,
		class: className,
		...restProps
	}: Props = $props();

	function createChartAttachment(activeRenderer: EChartsRenderer) {
		return (node: HTMLDivElement) => {
			const chart = echarts.init(node, undefined, { renderer: activeRenderer });
			instance = chart;
			const resizeObserver = new ResizeObserver(() => {
				if (chart.isDisposed()) return;
				if (node.clientWidth === chart.getWidth() && node.clientHeight === chart.getHeight())
					return;
				chart.resize();
			});
			resizeObserver.observe(node);

			return () => {
				resizeObserver.disconnect();
				if (!chart.isDisposed()) chart.dispose();
				if (instance === chart) instance = undefined;
			};
		};
	}

	$effect(() => {
		const chart = instance;
		const nextOption = option;
		const options = setOptionOptions;
		if (!chart || chart.isDisposed()) return;
		chart.setOption(nextOption, options);
	});

	$effect(() => {
		const chart = instance;
		const bindings = Object.entries(events);
		if (!chart || chart.isDisposed()) return;
		for (const [event, handler] of bindings) chart.on(event, handler);
		return () => {
			if (chart.isDisposed()) return;
			for (const [event, handler] of bindings) chart.off(event, handler);
		};
	});
</script>

<div
	{@attach createChartAttachment(renderer)}
	data-slot="echarts-host"
	data-echarts-source-hidden={hideSource || undefined}
	class={cn(
		'absolute inset-0 min-h-0 min-w-0',
		hideSource && '[&_canvas]:opacity-0 [&_svg]:opacity-0',
		className
	)}
	{...restProps}
></div>
```

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

```ts
export { default as ChartContainer } from './chart-container.svelte';
export { default as ChartStyle } from './chart-style.svelte';
export { default as EChartsHost } from './echarts-host.svelte';
export { default as LoadingIndicator } from './loading-indicator.svelte';
export { default as SelectableSeriesControls } from './selectable-series-controls.svelte';
export { mergeLifecycleOptions } from './merge-options.js';
export type { EChartsEventHandler } from './echarts-host.svelte';
export { RegistrationSet, type RegistrationGetter } from './registrations.svelte.js';
export {
	getEChartsSharedSlotContext,
	setEChartsSharedSlotContext,
	type EChartsSharedSlotName
} from './shared-slots.svelte.js';
export {
	buildChartCss,
	chartColorToken,
	chartColorVariable,
	chartColorVariableName,
	distributeColors,
	flattenColor,
	getColorsCount,
	indicatorBackground,
	normalizeColor,
	resolveColors,
	seriesPaint,
	withAlpha,
	quoteCssString,
	type ResolvedColors
} from './colors.js';
export {
	DEFAULT_ECHARTS_RENDERER,
	ECHARTS_RENDERERS,
	THEMES,
	THEME_KEYS,
	validateChartConfigColors,
	type AtLeastOneThemeColor,
	type ChartAccessibility,
	type ChartConfig,
	type EChartsRenderer,
	type EChartsRenderStyle,
	type ThemeKey
} from './types.js';
```

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

```svelte
<script lang="ts">
	import { cubicOut } from 'svelte/easing';
	import { prefersReducedMotion } from 'svelte/motion';
	import { scale } from 'svelte/transition';

	let { isLoading }: { isLoading: boolean } = $props();
</script>

{#if isLoading}
	<div class="pointer-events-none absolute inset-0 z-20 flex items-center justify-center">
		<div
			role="status"
			aria-live="polite"
			in:scale={{
				duration: prefersReducedMotion.current ? 0 : 250,
				start: 0.92,
				opacity: 0,
				easing: cubicOut
			}}
			class="flex items-center justify-center gap-2 rounded-md border bg-background px-2 py-0.5 text-sm text-primary"
		>
			<div
				aria-hidden="true"
				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/echarts-chart/merge-options.ts`

```ts
type LifecycleOptions = {
	animation?: unknown;
	animationDuration?: unknown;
	animationDurationUpdate?: unknown;
};

/** Merge consumer options without letting them break chart-owned animation lifecycles. */
export function mergeLifecycleOptions<T extends object>(built: T, overrides?: object): T {
	const lifecycle = built as T & LifecycleOptions;
	return {
		...built,
		...overrides,
		animation: lifecycle.animation,
		animationDuration: lifecycle.animationDuration,
		animationDurationUpdate: lifecycle.animationDurationUpdate
	} as T;
}
```

`$lib/components/evilcharts/ui/echarts-chart/registrations.svelte.ts`

```ts
import { SvelteMap } from 'svelte/reactivity';

export type RegistrationGetter<T> = () => T;

/** Ordered, reactive storage for DOM-free compound-component registrations. */
export class RegistrationSet<T> {
	#entries = new SvelteMap<string, RegistrationGetter<T>>();

	register(token: string, getter: RegistrationGetter<T>): () => void {
		this.#entries.set(token, getter);
		return () => {
			if (this.#entries.get(token) === getter) this.#entries.delete(token);
		};
	}

	get values(): T[] {
		return Array.from(this.#entries.values(), (getter) => getter());
	}

	get first(): T | undefined {
		return this.#entries.values().next().value?.();
	}

	get size(): number {
		return this.#entries.size;
	}
}
```

`$lib/components/evilcharts/ui/echarts-chart/selectable-series-controls.svelte`

```svelte
<script lang="ts">
	let {
		items,
		selectedKey,
		onToggle
	}: {
		items: { key: string; label: string }[];
		selectedKey: string | null;
		onToggle: (key: string) => void;
	} = $props();
</script>

{#if items.length > 0}
	<div
		class="pointer-events-none absolute inset-0 z-50"
		role="group"
		aria-label="Selectable chart series"
	>
		{#each items as item (item.key)}
			<button
				type="button"
				aria-pressed={selectedKey === item.key}
				class="sr-only focus:pointer-events-auto focus:not-sr-only focus:absolute focus:top-2 focus:left-1/2 focus:-translate-x-1/2 focus:rounded-md focus:border focus:bg-background focus:px-3 focus:py-2 focus:text-foreground focus:shadow-md focus:ring-2 focus:ring-ring focus:outline-none"
				onclick={() => onToggle(item.key)}
			>
				{item.label}
			</button>
		{/each}
	</div>
{/if}
```

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

```ts
import { getContext, setContext } from 'svelte';

export type EChartsSharedSlotName = 'tooltip' | 'legend' | 'brush';

type SharedSlotContext = {
	register: (slot: EChartsSharedSlotName, token: string, getter: () => unknown) => () => void;
};

const ECHARTS_SHARED_SLOT_CONTEXT = Symbol('evilcharts-echarts-shared-slots');

export function setEChartsSharedSlotContext(context: SharedSlotContext): SharedSlotContext {
	setContext(ECHARTS_SHARED_SLOT_CONTEXT, context);
	return context;
}

export function getEChartsSharedSlotContext(): SharedSlotContext {
	const context = getContext<SharedSlotContext | undefined>(ECHARTS_SHARED_SLOT_CONTEXT);
	if (!context) {
		throw new Error('[EvilCharts] ECharts compound parts must be children of an ECharts chart.');
	}
	return context;
}
```

`$lib/components/evilcharts/ui/echarts-chart/types.ts`

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

export const ECHARTS_RENDERERS = {
	canvas: 'canvas',
	svg: 'svg'
} as const;

export type EChartsRenderer = (typeof ECHARTS_RENDERERS)[keyof typeof ECHARTS_RENDERERS];
export const DEFAULT_ECHARTS_RENDERER = ECHARTS_RENDERERS.canvas;

export const THEMES = { light: '', dark: '.dark' } as const;
export type ThemeKey = keyof typeof THEMES;
export const THEME_KEYS = Object.keys(THEMES) as ThemeKey[];

type ThemeColorsBase = {
	[K in ThemeKey]?: string[];
};

export type AtLeastOneThemeColor = {
	[K in ThemeKey]: Required<Pick<ThemeColorsBase, K>> & Partial<Omit<ThemeColorsBase, K>>;
}[ThemeKey];

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

export function validateChartConfigColors(config: ChartConfig): void {
	for (const [key, item] of Object.entries(config)) {
		if (!item.colors) continue;
		if (THEME_KEYS.some((theme) => item.colors?.[theme] !== undefined)) continue;

		throw new Error(
			`[EvilCharts] Invalid chart config for "${key}": colors must define light or dark.`
		);
	}
}

export type ChartAccessibility =
	| {
			label: string;
			labelledBy?: never;
			description?: string;
			describedBy?: string;
	  }
	| {
			label?: never;
			labelledBy: string;
			description?: string;
			describedBy?: string;
	  };

export type EChartsRenderStyle = 'native' | 'dither';
```
        
      
      
        ### Add the sub-components.
        

Create `echarts-tooltip` in the same `ui` folder and paste the tooltip surface and its variants there.


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

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

```ts
export { default as Tooltip } from './tooltip.svelte';
export type { TooltipProps } from './tooltip.svelte';
export {
	escapeTooltipHtml,
	resolveTooltipPosition,
	roundnessClass,
	tooltipBaseOption,
	tooltipIndicatorHtml,
	tooltipRow,
	tooltipShell,
	tooltipVariantClass,
	type TooltipPosition,
	type TooltipRoundness,
	type TooltipVariant
} from './tooltip.js';
```

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

```svelte
<script lang="ts">
	import { getEChartsSharedSlotContext } from '../echarts-chart/index.js';
	import type { TooltipPosition, TooltipRoundness, TooltipVariant } from './tooltip.js';

	export type TooltipProps = {
		variant?: TooltipVariant;
		roundness?: TooltipRoundness;
		cursor?: boolean;
		defaultIndex?: number;
		position?: TooltipPosition;
	};

	let {
		variant = 'default',
		roundness = 'lg',
		cursor,
		defaultIndex,
		position = 'variable'
	}: TooltipProps = $props();

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

	$effect(() =>
		slots.register('tooltip', token, () => ({
			variant,
			roundness,
			cursor,
			defaultIndex,
			position
		}))
	);
</script>
```

`$lib/components/evilcharts/ui/echarts-tooltip/tooltip.ts`

```ts
import type { TooltipComponentOption } from 'echarts/components';
import { indicatorBackground } from '../echarts-chart/index.js';

export type TooltipVariant = 'default' | 'frosted-glass';
export type TooltipRoundness = 'sm' | 'md' | 'lg' | 'xl';
export type TooltipPosition = 'fixed' | 'variable';

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

export const tooltipVariantClass: Record<TooltipVariant, string> = {
	default: 'bg-background',
	'frosted-glass': 'bg-background/50 backdrop-blur-md'
};

export function escapeTooltipHtml(value: unknown): string {
	return String(value ?? '')
		.replaceAll('&', '&amp;')
		.replaceAll('<', '&lt;')
		.replaceAll('>', '&gt;')
		.replaceAll('"', '&quot;')
		.replaceAll("'", '&#039;');
}

export function tooltipIndicatorHtml(key: string, colorsCount: number): string {
	return `<div class="h-2.5 w-2.5 shrink-0 rounded-[2px]" style="background:${indicatorBackground(key, colorsCount)}"></div>`;
}

export function tooltipRow({
	indicatorHtml,
	labelText,
	valueText,
	dimmed
}: {
	indicatorHtml: string;
	labelText: string;
	valueText: string;
	dimmed: string;
}): string {
	return `<div class="flex w-full flex-wrap items-center gap-2${dimmed}">
          ${indicatorHtml}
          <div class="flex flex-1 items-center justify-between gap-4 leading-none">
            <span class="text-muted-foreground">${escapeTooltipHtml(labelText)}</span>
            <span class="text-foreground font-mono font-medium tabular-nums">${escapeTooltipHtml(valueText)}</span>
          </div>
        </div>`;
}

export function tooltipShell({
	label,
	body,
	roundness,
	variant
}: {
	label: string;
	body: string;
	roundness: TooltipRoundness;
	variant: TooltipVariant;
}): string {
	return `<div class="grid min-w-32 items-start gap-1.5 border border-border/50 px-2.5 py-1.5 text-xs shadow-xl ${roundnessClass[roundness]} ${tooltipVariantClass[variant]}">
      <div class="font-medium text-primary">${escapeTooltipHtml(label)}</div>
      <div class="grid gap-1.5">${body}</div>
    </div>`;
}

export function resolveTooltipPosition(
	position: TooltipPosition
): TooltipComponentOption['position'] {
	if (position === 'variable') return undefined;
	return (point, _params, _dom, _rect, size) => [point[0] - size.contentSize[0] / 2, 8];
}

export function tooltipBaseOption(params: {
	present: boolean;
	cursor: boolean;
	position: TooltipPosition;
	axisPointerColor: string;
	strokeWidth: number;
}): TooltipComponentOption {
	const { present, cursor, position, axisPointerColor, strokeWidth } = params;
	return {
		show: present,
		trigger: 'axis',
		confine: true,
		displayTransition: false,
		backgroundColor: 'transparent',
		borderWidth: 0,
		padding: 0,
		extraCssText: 'box-shadow:none;',
		axisPointer: cursor
			? {
					type: 'line',
					lineStyle: { color: axisPointerColor, width: strokeWidth, type: [3, 3] }
				}
			: { type: 'none' },
		position: resolveTooltipPosition(position)
	};
}
```
        
        

Next, create `echarts-legend` in the same `ui` folder and paste the legend overlay there.


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

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

```ts
export { default as Legend } from './legend.svelte';
export { default as LegendIndicator } from './legend-indicator.svelte';
export { default as LegendOverlay } from './legend-overlay.svelte';
export type { LegendProps, LegendVariant } from './legend.svelte';
```

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

```svelte
<script lang="ts">
	import { indicatorBackground } from '../echarts-chart/index.js';
	import type { LegendVariant } from './legend.svelte';

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

	const background = $derived(indicatorBackground(dataKey, colorsCount));
	const fillStyle = $derived(`background:${background}`);
	const outlineStyle = $derived(
		`${fillStyle};-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`
	);
</script>

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

`$lib/components/evilcharts/ui/echarts-legend/legend-overlay.svelte`

```svelte
<script lang="ts">
	import type { ChartConfig } from '../echarts-chart/index.js';
	import { getColorsCount } from '../echarts-chart/index.js';
	import LegendIndicator from './legend-indicator.svelte';
	import type { LegendVariant } from './legend.svelte';

	let {
		seriesKeys,
		config,
		variant,
		align,
		selectedKey,
		hoveredKey,
		isClickable,
		onToggle,
		style
	}: {
		seriesKeys: string[];
		config: ChartConfig;
		variant: LegendVariant;
		align: 'left' | 'center' | 'right';
		selectedKey: string | null;
		hoveredKey: string | null;
		isClickable: boolean;
		onToggle: (key: string) => void;
		style?: string;
	} = $props();

	const justify = $derived(
		align === 'left' ? 'justify-start' : align === 'center' ? 'justify-center' : 'justify-end'
	);
	const entries = $derived(
		seriesKeys.map((key) => ({
			key,
			item: config[key],
			colorsCount: getColorsCount(config[key] ?? {}),
			selected:
				(selectedKey === null || selectedKey === key) && (hoveredKey === null || hoveredKey === key)
		}))
	);
</script>

<div {style} class={['pointer-events-auto flex items-center gap-4 select-none', justify]}>
	{#each entries as entry (entry.key)}
		{#if isClickable}
			<button
				type="button"
				aria-pressed={selectedKey === entry.key}
				class={[
					'flex appearance-none items-center gap-1.5 border-0 bg-transparent p-0 text-inherit transition-opacity',
					!entry.selected && 'opacity-30',
					'cursor-pointer'
				]}
				onclick={() => onToggle(entry.key)}
			>
				<LegendIndicator {variant} dataKey={entry.key} colorsCount={entry.colorsCount} />
				{#if typeof entry.item?.label === 'function'}
					{@render entry.item.label()}
				{:else}
					{entry.item?.label ?? entry.key}
				{/if}
			</button>
		{:else}
			<div
				class={['flex items-center gap-1.5 transition-opacity', !entry.selected && 'opacity-30']}
			>
				<LegendIndicator {variant} dataKey={entry.key} colorsCount={entry.colorsCount} />
				{#if typeof entry.item?.label === 'function'}
					{@render entry.item.label()}
				{:else}
					{entry.item?.label ?? entry.key}
				{/if}
			</div>
		{/if}
	{/each}
</div>
```

`$lib/components/evilcharts/ui/echarts-legend/legend.svelte`

```svelte
<script lang="ts">
	import { getEChartsSharedSlotContext } from '../echarts-chart/index.js';

	export type LegendVariant =
		| 'square'
		| 'circle'
		| 'circle-outline'
		| 'rounded-square'
		| 'rounded-square-outline'
		| 'vertical-bar'
		| 'horizontal-bar';

	export type LegendProps = {
		variant?: LegendVariant;
		align?: 'left' | 'center' | 'right';
		verticalAlign?: 'top' | 'middle' | 'bottom';
		isClickable?: boolean;
	};

	let {
		variant = 'rounded-square',
		align = 'right',
		verticalAlign = 'top',
		isClickable = false
	}: LegendProps = $props();

	const token = $props.id();
	const slots = getEChartsSharedSlotContext();
	$effect(() =>
		slots.register('legend', token, () => ({ variant, align, verticalAlign, isClickable }))
	);
</script>
```
        
        

Next, create `echarts-dot` in the same `ui` folder and paste the dot styles the series markers draw with there.


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

`$lib/components/evilcharts/ui/echarts-dot/dot.ts`

```ts
import type * as echarts from 'echarts/core';

export type DotVariant = 'none' | 'default' | 'border' | 'colored-border' | 'ping';

export type DotItemStyleOption = {
	color?: string | echarts.graphic.LinearGradient;
	borderColor?: string | echarts.graphic.LinearGradient;
	borderWidth?: number;
	opacity?: number;
};

export type DotStyle = { size: number; itemStyle: DotItemStyleOption };

function colorWithAlpha(color: string, alpha: number): string {
	if (color.startsWith('#')) {
		let hex = color.slice(1);
		if (hex.length === 3) hex = hex.replace(/./g, (character) => character.repeat(2));
		const numeric = Number.parseInt(hex, 16);
		return `rgba(${(numeric >> 16) & 255}, ${(numeric >> 8) & 255}, ${numeric & 255}, ${alpha})`;
	}
	const match = color.match(/rgba?\(([^)]+)\)/);
	if (!match) return color;
	const [red, green, blue] = match[1].split(',').map((part) => Number.parseFloat(part));
	return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
}

export function dotItemStyle(
	variant: DotVariant,
	paint: string | echarts.graphic.LinearGradient,
	background: string
): DotItemStyleOption {
	switch (variant) {
		case 'border':
			return { color: paint, borderColor: background, borderWidth: 2 };
		case 'colored-border':
			return { color: background, borderColor: paint, borderWidth: 1 };
		case 'ping':
			return {
				color: paint,
				borderColor: typeof paint === 'string' ? colorWithAlpha(paint, 0.28) : paint,
				borderWidth: 10
			};
		case 'default':
			return { color: paint, borderWidth: 0 };
		default:
			return {};
	}
}

export const DOT_SIZES: Record<DotVariant, number> = {
	none: 0,
	default: 6,
	border: 8,
	'colored-border': 6,
	ping: 8
};

export function dotStyle(
	variant: DotVariant,
	paint: string | echarts.graphic.LinearGradient,
	background: string
): DotStyle {
	return { size: DOT_SIZES[variant], itemStyle: dotItemStyle(variant, paint, background) };
}

export function sampleGradient(slots: string[], position: number): string {
	if (slots.length <= 1) return slots[0] ?? 'rgba(120, 120, 120, 1)';
	const parse = (color: string) =>
		color
			.match(/rgba?\(([^)]+)\)/)?.[1]
			.split(',')
			.map(Number) ?? [120, 120, 120, 1];
	const scaled = position * (slots.length - 1);
	const index = Math.min(Math.floor(scaled), slots.length - 2);
	const fraction = scaled - index;
	const [redFrom, greenFrom, blueFrom, alphaFrom = 1] = parse(slots[index]);
	const [redTo, greenTo, blueTo, alphaTo = 1] = parse(slots[index + 1]);
	const lerp = (from: number, to: number) => from + (to - from) * fraction;
	return `rgba(${Math.round(lerp(redFrom, redTo))}, ${Math.round(lerp(greenFrom, greenTo))}, ${Math.round(lerp(blueFrom, blueTo))}, ${lerp(alphaFrom, alphaTo).toFixed(3)})`;
}
```

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

```ts
export {
	DOT_SIZES,
	dotItemStyle,
	dotStyle,
	sampleGradient,
	type DotItemStyleOption,
	type DotStyle,
	type DotVariant
} from './dot.js';
```
        
        

Finally, create `echarts-brush` in the same `ui` folder and paste the zoom brush there.


        
          ### $lib/components/evilcharts/ui/echarts-brush

`$lib/components/evilcharts/ui/echarts-brush/brush-controls.svelte`

```svelte
<script lang="ts">
	type BrushRange = { startIndex: number; endIndex: number };
	type BrushControl = 'range' | 'start' | 'end';

	let {
		startIndex,
		endIndex,
		totalPoints,
		formatLabel = (index: number) => String(index),
		onChange
	}: {
		startIndex: number;
		endIndex: number;
		totalPoints: number;
		formatLabel?: (index: number) => string;
		onChange: (range: BrushRange) => void;
	} = $props();

	const maximum = $derived(Math.max(0, totalPoints - 1));
	const minimumSpan = $derived(totalPoints > 1 ? 1 : 0);
	const windowSize = $derived(Math.max(0, endIndex - startIndex));
	const maximumWindowStart = $derived(Math.max(0, maximum - windowSize));

	function commit(next: BrushRange) {
		const start = Math.max(0, Math.min(next.startIndex, maximum));
		const end = Math.max(start, Math.min(next.endIndex, maximum));
		if (start === startIndex && end === endIndex) return;
		onChange({ startIndex: start, endIndex: end });
	}

	function handleKey(event: KeyboardEvent, control: BrushControl) {
		if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
		event.preventDefault();

		if (control === 'range') {
			const delta =
				event.key === 'ArrowLeft'
					? -1
					: event.key === 'ArrowRight'
						? 1
						: event.key === 'Home'
							? -startIndex
							: maximum - endIndex;
			const nextStart = Math.max(0, Math.min(startIndex + delta, maximumWindowStart));
			commit({ startIndex: nextStart, endIndex: nextStart + windowSize });
			return;
		}

		if (control === 'start') {
			const nextStart =
				event.key === 'Home'
					? 0
					: event.key === 'End'
						? endIndex - minimumSpan
						: startIndex + (event.key === 'ArrowLeft' ? -1 : 1);
			commit({ startIndex: Math.min(nextStart, endIndex - minimumSpan), endIndex });
			return;
		}

		const nextEnd =
			event.key === 'Home'
				? startIndex + minimumSpan
				: event.key === 'End'
					? maximum
					: endIndex + (event.key === 'ArrowLeft' ? -1 : 1);
		commit({ startIndex, endIndex: Math.max(nextEnd, startIndex + minimumSpan) });
	}
</script>

{#if totalPoints > 0}
	<div
		class="pointer-events-none absolute inset-0 z-50"
		role="group"
		aria-label="Chart range controls"
	>
		<div
			role="slider"
			tabindex="0"
			class="sr-only focus:pointer-events-auto focus:not-sr-only focus:absolute focus:bottom-2 focus:left-1/2 focus:-translate-x-1/2 focus:rounded-md focus:border focus:bg-background focus:px-3 focus:py-2 focus:text-foreground focus:shadow-md focus:ring-2 focus:ring-ring focus:outline-none"
			aria-label="Selected chart range"
			aria-orientation="horizontal"
			aria-valuemin="0"
			aria-valuemax={maximumWindowStart}
			aria-valuenow={startIndex}
			aria-valuetext={`${formatLabel(startIndex)} to ${formatLabel(endIndex)}`}
			onkeydown={(event) => handleKey(event, 'range')}
		>
			Selected range: {formatLabel(startIndex)} to {formatLabel(endIndex)}
		</div>
		<div
			role="slider"
			tabindex="0"
			class="sr-only focus:pointer-events-auto focus:not-sr-only focus:absolute focus:bottom-2 focus:left-1/2 focus:-translate-x-1/2 focus:rounded-md focus:border focus:bg-background focus:px-3 focus:py-2 focus:text-foreground focus:shadow-md focus:ring-2 focus:ring-ring focus:outline-none"
			aria-label="Range start"
			aria-orientation="horizontal"
			aria-valuemin="0"
			aria-valuemax={Math.max(0, endIndex - minimumSpan)}
			aria-valuenow={startIndex}
			aria-valuetext={formatLabel(startIndex)}
			onkeydown={(event) => handleKey(event, 'start')}
		>
			Range start: {formatLabel(startIndex)}
		</div>
		<div
			role="slider"
			tabindex="0"
			class="sr-only focus:pointer-events-auto focus:not-sr-only focus:absolute focus:bottom-2 focus:left-1/2 focus:-translate-x-1/2 focus:rounded-md focus:border focus:bg-background focus:px-3 focus:py-2 focus:text-foreground focus:shadow-md focus:ring-2 focus:ring-ring focus:outline-none"
			aria-label="Range end"
			aria-orientation="horizontal"
			aria-valuemin={Math.min(maximum, startIndex + minimumSpan)}
			aria-valuemax={maximum}
			aria-valuenow={endIndex}
			aria-valuetext={formatLabel(endIndex)}
			onkeydown={(event) => handleKey(event, 'end')}
		>
			Range end: {formatLabel(endIndex)}
		</div>
	</div>
{/if}
```

`$lib/components/evilcharts/ui/echarts-brush/brush.svelte`

```svelte
<script lang="ts">
	import { getEChartsSharedSlotContext } from '../echarts-chart/index.js';

	export type BrushProps = {
		height?: number;
		formatLabel?: (value: string, index: number) => string;
		onChange?: (range: { startIndex: number; endIndex: number }) => void;
	};

	let { height, formatLabel, onChange }: BrushProps = $props();
	const token = $props.id();
	const slots = getEChartsSharedSlotContext();
	$effect(() => slots.register('brush', token, () => ({ height, formatLabel, onChange })));
</script>
```

`$lib/components/evilcharts/ui/echarts-brush/brush.ts`

```ts
import type { DataZoomComponentOption } from 'echarts/components';
import type { EChartsType } from 'echarts/core';
import * as echarts from 'echarts/core';
import { withAlpha, type ResolvedColors } from '../echarts-chart/index.js';

export const BRUSH_BORDER_OPACITY = 1;

export type BrushRange = { start: number; end: number };
export type BrushGeometry = { bottom: number; height: number };

export type BrushOverlayParams = {
	range: BrushRange;
	geom: BrushGeometry;
	size: { width: number; height: number };
	tokens: ResolvedColors['tokens'];
	labels: { start: string; end: string } | null;
	showLabels: boolean;
	hover: { left: boolean; right: boolean };
};

type ZrRect = InstanceType<typeof echarts.graphic.Rect>;
type ZrCircle = InstanceType<typeof echarts.graphic.Circle>;
type ZrText = InstanceType<typeof echarts.graphic.Text>;

export type BrushOverlayElements = {
	dimLeft: ZrRect;
	dimRight: ZrRect;
	frame: ZrRect;
	pillLeft: ZrRect;
	pillRight: ZrRect;
	grips: ZrCircle[];
	labelStart: ZrText;
	labelEnd: ZrText;
};

export function syncBrushOverlay(
	chart: EChartsType,
	store: { brushOverlay: BrushOverlayElements | null },
	params: BrushOverlayParams | null
): void {
	const renderer = chart.getZr();
	if (!renderer) return;

	if (!params) {
		if (store.brushOverlay) {
			const { grips, ...rest } = store.brushOverlay;
			for (const element of [...Object.values(rest), ...grips]) renderer.remove(element);
			store.brushOverlay = null;
		}
		return;
	}

	if (!store.brushOverlay) {
		const rect = (z: number) => new echarts.graphic.Rect({ silent: true, z, shape: {} });
		const elements: BrushOverlayElements = {
			dimLeft: rect(100),
			dimRight: rect(100),
			frame: rect(101),
			pillLeft: rect(102),
			pillRight: rect(102),
			grips: Array.from(
				{ length: 6 },
				() => new echarts.graphic.Circle({ silent: true, z: 103, shape: {} })
			),
			labelStart: new echarts.graphic.Text({ silent: true, z: 104 }),
			labelEnd: new echarts.graphic.Text({ silent: true, z: 104 })
		};
		const { grips, ...rest } = elements;
		for (const element of [...Object.values(rest), ...grips]) renderer.add(element);
		store.brushOverlay = elements;
	}

	const elements = store.brushOverlay;
	const { range, geom, size, tokens, labels, showLabels, hover } = params;
	const trackLeft = 8;
	const trackRight = Math.max(size.width - 8, trackLeft);
	const trackWidth = trackRight - trackLeft;
	const top = size.height - geom.bottom - geom.height;
	const centerY = top + geom.height / 2;
	const selectionLeft = trackLeft + (trackWidth * range.start) / 100;
	const selectionRight = trackLeft + (trackWidth * range.end) / 100;

	const dimFill = withAlpha(tokens.background, 0.7);
	elements.dimLeft.setShape({
		x: trackLeft,
		y: top,
		width: Math.max(selectionLeft - trackLeft, 0),
		height: geom.height
	});
	elements.dimLeft.setStyle({ fill: dimFill });
	elements.dimRight.setShape({
		x: selectionRight,
		y: top,
		width: Math.max(trackRight - selectionRight, 0),
		height: geom.height
	});
	elements.dimRight.setStyle({ fill: dimFill });
	elements.frame.setShape({
		x: selectionLeft,
		y: top,
		width: Math.max(selectionRight - selectionLeft, 0),
		height: geom.height,
		r: 6
	});
	elements.frame.setStyle({
		fill: 'none',
		stroke: withAlpha(tokens.border, BRUSH_BORDER_OPACITY),
		lineWidth: 1
	});

	const updatePill = (element: ZrRect, x: number, hovered: boolean) => {
		element.setShape({ x: x - 3, y: centerY - 8, width: 6, height: 16, r: 3 });
		element.setStyle({ fill: hovered ? tokens.foreground : tokens.mutedForeground });
	};
	updatePill(elements.pillLeft, selectionLeft, hover.left);
	updatePill(elements.pillRight, selectionRight, hover.right);

	const gripFill = withAlpha(tokens.background, 0.7);
	for (const [index, offset] of [-4, 0, 4].entries()) {
		elements.grips[index].setShape({ cx: selectionLeft, cy: centerY + offset, r: 1 });
		elements.grips[index].setStyle({ fill: gripFill });
		elements.grips[index + 3].setShape({ cx: selectionRight, cy: centerY + offset, r: 1 });
		elements.grips[index + 3].setStyle({ fill: gripFill });
	}

	const updateLabel = (element: ZrText, text: string, x: number, align: 'left' | 'right') => {
		element.setStyle({
			text,
			x: align === 'left' ? Math.max(x + 6, trackLeft + 2) : Math.min(x - 6, trackRight - 2),
			y: top + geom.height,
			align,
			verticalAlign: 'middle',
			fill: tokens.background,
			backgroundColor: tokens.foreground,
			padding: [2, 5],
			borderRadius: 4,
			font: '500 9px system-ui, sans-serif'
		});
		element.attr('invisible', !showLabels || !text);
	};
	updateLabel(elements.labelStart, labels?.start ?? '', selectionLeft, 'left');
	updateLabel(elements.labelEnd, labels?.end ?? '', selectionRight, 'right');
}

export function buildBrushDataZoom(params: {
	brushBottom: number;
	brushHeight: number;
	brushRange: BrushRange;
	fillerColor: string;
}): DataZoomComponentOption[] {
	const { brushBottom, brushHeight, brushRange, fillerColor } = params;
	return [
		{
			type: 'slider',
			show: true,
			xAxisIndex: [0],
			left: 8,
			right: 8,
			bottom: brushBottom,
			height: brushHeight,
			start: brushRange.start,
			end: brushRange.end,
			brushSelect: false,
			showDetail: false,
			backgroundColor: 'transparent',
			borderColor: 'transparent',
			fillerColor,
			dataBackground: { lineStyle: { opacity: 0 }, areaStyle: { opacity: 0 } },
			selectedDataBackground: { lineStyle: { opacity: 0 }, areaStyle: { opacity: 0 } },
			handleIcon: 'path://M -3 -5 L -3 5 A 3 3 0 0 0 3 5 L 3 -5 A 3 3 0 0 0 -3 -5 Z',
			handleSize: '35%',
			handleStyle: { opacity: 0 },
			moveHandleSize: 0,
			emphasis: { handleStyle: { opacity: 0 } }
		},
		{ type: 'inside', xAxisIndex: [0] }
	];
}
```

`$lib/components/evilcharts/ui/echarts-brush/index.ts`

```ts
export { default as Brush } from './brush.svelte';
export { default as BrushControls } from './brush-controls.svelte';
export type { BrushProps } from './brush.svelte';
export {
	BRUSH_BORDER_OPACITY,
	buildBrushDataZoom,
	syncBrushOverlay,
	type BrushGeometry,
	type BrushOverlayElements,
	type BrushOverlayParams,
	type BrushRange
} from './brush.js';
```
        
      
    
  


## Usage

The ECharts area chart is composable. `<EChartsAreaChart>` is the container, and every part hangs off it as a compound member — `<EChartsAreaChart.Grid>`, `<EChartsAreaChart.XAxis>`, `<EChartsAreaChart.YAxis>`, `<EChartsAreaChart.Legend>`, `<EChartsAreaChart.Tooltip>`, `<EChartsAreaChart.Brush>`, and one or more `<EChartsAreaChart.Area>` — so a single import gives you the whole chart. Each `<Area>` sets its own `variant`, `strokeVariant`, and `isClickable`, so one chart can mix fills, strokes, and selective interactivity.

```svelte
<script lang="ts">
	import {
		EChartsAreaChart,
		type ChartConfig
	} from '$lib/components/evilcharts/charts/echarts-area-chart/index.js';
</script>
```

```svelte
<EChartsAreaChart {data} config={chartConfig} stackType="stacked">
	<EChartsAreaChart.Grid />
	<EChartsAreaChart.XAxis dataKey="month" />
	<EChartsAreaChart.Brush />
	<EChartsAreaChart.Legend isClickable />
	<EChartsAreaChart.Tooltip />
	<EChartsAreaChart.Area dataKey="desktop" variant="gradient" strokeVariant="dashed" isClickable />
	<EChartsAreaChart.Area dataKey="mobile" variant="hatched" strokeVariant="solid" isClickable />
</EChartsAreaChart>
```

The difference is under the hood: these compound children are declarative configuration slots rather than visual DOM nodes. The root reads their props and compiles an ECharts option, which ECharts paints with Canvas by default or SVG when `renderer="svg"`.

The `config` is the same contract as every EvilCharts chart — each key maps a data key to a `label` and a per-theme `colors` array. See [Chart Config](/docs/chart-config) for the full shape. Colors resolve from CSS variables at runtime, so dark mode just works.

> 
  

The ECharts implementation brings two small departures from the LayerChart sibling: multi-color gradient areas and the `dotted`, `lines`, and `hatched` fills use offscreen canvas textures sized to the plot. With `renderer="svg"`, ECharts may embed those texture tiles as raster images inside the SVG. The zoom brush is a themed mini chart driven by ECharts' native `dataZoom` instead of the custom `EvilBrush`.




### Loading State

### isLoading='true'

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

	const chartConfig = {
		desktop: {
			label: 'Desktop',
			colors: {
				light: ['#047857'],
				dark: ['#10b981']
			}
		},
		mobile: {
			label: 'Mobile',
			colors: {
				light: ['#be123c'],
				dark: ['#f43f5e']
			}
		}
	} satisfies ChartConfig;
</script>

<!-- [!code highlight:8] -->
<EChartsAreaChart
	data={[]}
	config={chartConfig}
	class="h-full w-full p-4"
	isLoading={true}
	stackType="stacked"
	curveType="bump"
>
	<EChartsAreaChart.Grid />
	<EChartsAreaChart.XAxis
		dataKey="month"
		tickFormatter={(value) => String(value).substring(0, 3)}
	/>
	<EChartsAreaChart.YAxis dataKey="desktop" />
	<EChartsAreaChart.Legend isClickable />
	<EChartsAreaChart.Tooltip />
	<EChartsAreaChart.Area dataKey="desktop" variant="gradient" isClickable>
		<EChartsAreaChart.ActiveDot variant="default" />
	</EChartsAreaChart.Area>
	<EChartsAreaChart.Area dataKey="mobile" variant="gradient" isClickable>
		<EChartsAreaChart.ActiveDot variant="default" />
	</EChartsAreaChart.Area>
</EChartsAreaChart>
```
> 
  

Pass `isLoading` to show an animated skeleton; use `loadingPoints` to set how many points it draws.




## Examples

Change `variant` and `strokeVariant` on an `<Area>`, or `curveType` and `stackType` on the chart, to restyle it.

### SVG Renderer

Pass `renderer="svg"` to the chart root to opt into ECharts' SVG renderer. Omit it to use the default Canvas renderer.

### renderer="svg"

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

	const data = [
		{ month: 'January', visitors: 342 },
		{ month: 'February', visitors: 876 },
		{ month: 'March', visitors: 512 },
		{ month: 'April', visitors: 629 },
		{ month: 'May', visitors: 458 },
		{ month: 'June', visitors: 781 }
	];

	const chartConfig = {
		visitors: {
			label: 'Visitors',
			colors: {
				light: ['#047857'],
				dark: ['#10b981']
			}
		}
	} satisfies ChartConfig;
</script>

<EChartsAreaChart
	renderer="svg"
	{data}
	config={chartConfig}
	class="h-full w-full p-4"
	xDataKey="month"
>
	<EChartsAreaChart.Grid />
	<EChartsAreaChart.XAxis
		dataKey="month"
		tickFormatter={(value) => String(value).substring(0, 3)}
	/>
	<EChartsAreaChart.Tooltip />
	<EChartsAreaChart.Area dataKey="visitors" variant="gradient">
		<EChartsAreaChart.ActiveDot variant="colored-border" />
	</EChartsAreaChart.Area>
	<EChartsAreaChart.Brush />
</EChartsAreaChart>
```

### Hover Highlight

### enableHoverHighlight='true'

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

	const data = [
		{ month: 'January', desktop: 342, mobile: 245 },
		{ month: 'February', desktop: 876, mobile: 654 },
		{ month: 'March', desktop: 512, mobile: 387 },
		{ month: 'April', desktop: 629, mobile: 521 },
		{ month: 'May', desktop: 458, mobile: 412 },
		{ month: 'June', desktop: 781, mobile: 598 },
		{ month: 'July', desktop: 394, mobile: 312 },
		{ month: 'August', desktop: 925, mobile: 743 },
		{ month: 'September', desktop: 647, mobile: 489 },
		{ month: 'October', desktop: 532, mobile: 476 },
		{ month: 'November', desktop: 803, mobile: 687 },
		{ month: 'December', desktop: 271, mobile: 198 }
	];

	const chartConfig = {
		desktop: {
			label: 'Desktop',
			colors: {
				light: ['#047857'],
				dark: ['#10b981']
			}
		},
		mobile: {
			label: 'Mobile',
			colors: {
				light: ['#be123c'],
				dark: ['#f43f5e']
			}
		}
	} satisfies ChartConfig;
</script>

<EChartsAreaChart
	enableHoverHighlight
	{data}
	config={chartConfig}
	class="h-full w-full p-4"
	stackType="stacked"
>
	<EChartsAreaChart.Grid />
	<EChartsAreaChart.XAxis
		dataKey="month"
		tickFormatter={(value) => String(value).substring(0, 3)}
	/>
	<EChartsAreaChart.YAxis dataKey="desktop" />
	<EChartsAreaChart.Legend isClickable />
	<EChartsAreaChart.Tooltip />
	<EChartsAreaChart.Area dataKey="desktop" variant="gradient" isClickable>
		<EChartsAreaChart.ActiveDot variant="default" />
	</EChartsAreaChart.Area>
	<EChartsAreaChart.Area dataKey="mobile" variant="gradient" isClickable>
		<EChartsAreaChart.ActiveDot variant="default" />
	</EChartsAreaChart.Area>
</EChartsAreaChart>
```

### Buffer Line

### enableBufferLine='true'

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

	const data = [
		{ month: 'January', desktop: 342, mobile: 184 },
		{ month: 'February', desktop: 876, mobile: 491 },
		{ month: 'March', desktop: 512, mobile: 290 },
		{ month: 'April', desktop: 629, mobile: 391 },
		{ month: 'May', desktop: 458, mobile: 309 },
		{ month: 'June', desktop: 781, mobile: 449 },
		{ month: 'July', desktop: 394, mobile: 234 },
		{ month: 'August', desktop: 925, mobile: 557 },
		{ month: 'September', desktop: 647, mobile: 367 },
		{ month: 'October', desktop: 532, mobile: 357 },
		{ month: 'November', desktop: 803, mobile: 515 },
		{ month: 'December', desktop: 271, mobile: 149 }
	];

	const chartConfig = {
		desktop: {
			label: 'Desktop',
			colors: {
				light: ['#047857'],
				dark: ['#10b981']
			}
		},
		mobile: {
			label: 'Mobile',
			colors: {
				light: ['#be123c'],
				dark: ['#f43f5e']
			}
		}
	} satisfies ChartConfig;
</script>

<EChartsAreaChart
	{data}
	config={chartConfig}
	class="h-full w-full p-4"
	stackType="stacked"
	xDataKey="month"
>
	<EChartsAreaChart.XAxis
		dataKey="month"
		tickFormatter={(value) => String(value).substring(0, 3)}
	/>
	<EChartsAreaChart.Brush />
	<EChartsAreaChart.Legend isClickable />
	<EChartsAreaChart.Tooltip />
	<EChartsAreaChart.Area
		enableBufferLine
		dataKey="desktop"
		variant="gradient"
		strokeVariant="solid"
		isClickable
	>
		<EChartsAreaChart.Dot variant="border" />
		<EChartsAreaChart.ActiveDot variant="colored-border" />
	</EChartsAreaChart.Area>
	<EChartsAreaChart.Area
		enableBufferLine
		dataKey="mobile"
		variant="gradient"
		strokeVariant="solid"
		isClickable
	>
		<EChartsAreaChart.Dot variant="border" />
		<EChartsAreaChart.ActiveDot variant="colored-border" />
	</EChartsAreaChart.Area>
</EChartsAreaChart>
```
> 
  

With `enableBufferLine`, each area's last segment renders as a dashed tail while the rest stays solid — useful for marking projected, estimated, or incomplete data at the end of a series, as in financial charts and forecasting dashboards.




### Hover Reveal

### enableHoverReveal='true'

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

	const data = [
		{ month: 'January', desktop: 342, mobile: 245 },
		{ month: 'February', desktop: 876, mobile: 654 },
		{ month: 'March', desktop: 512, mobile: 387 },
		{ month: 'April', desktop: 629, mobile: 521 },
		{ month: 'May', desktop: 458, mobile: 412 },
		{ month: 'June', desktop: 781, mobile: 598 },
		{ month: 'July', desktop: 394, mobile: 312 },
		{ month: 'August', desktop: 925, mobile: 743 },
		{ month: 'September', desktop: 647, mobile: 489 },
		{ month: 'October', desktop: 532, mobile: 476 },
		{ month: 'November', desktop: 803, mobile: 687 },
		{ month: 'December', desktop: 271, mobile: 198 }
	];

	const chartConfig = {
		desktop: {
			label: 'Desktop',
			colors: {
				light: ['#047857'],
				dark: ['#10b981']
			}
		},
		mobile: {
			label: 'Mobile',
			colors: {
				light: ['#be123c'],
				dark: ['#f43f5e']
			}
		}
	} satisfies ChartConfig;
</script>

<EChartsAreaChart
	enableHoverReveal
	{data}
	config={chartConfig}
	class="h-full w-full p-4"
	stackType="stacked"
>
	<EChartsAreaChart.Grid />
	<EChartsAreaChart.XAxis
		dataKey="month"
		tickFormatter={(value) => String(value).substring(0, 3)}
	/>
	<EChartsAreaChart.Legend />
	<EChartsAreaChart.Tooltip />
	<EChartsAreaChart.Area dataKey="desktop" variant="gradient">
		<EChartsAreaChart.ActiveDot variant="default" />
	</EChartsAreaChart.Area>
	<EChartsAreaChart.Area dataKey="mobile" variant="gradient">
		<EChartsAreaChart.ActiveDot variant="default" />
	</EChartsAreaChart.Area>
</EChartsAreaChart>
```
> 
  

With `enableHoverReveal`, hovering colors each area's line and fill only up to the pointer's position and mutes everything past it to a neutral gray, with the active dot riding the cursor — a scrubbing effect for reading a series left-to-right. When not hovering, the chart looks completely normal.




### Gradient Colors

### gradient colors

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

	const data = [
		{ month: 'January', desktop: 342, mobile: 245 },
		{ month: 'February', desktop: 876, mobile: 654 },
		{ month: 'March', desktop: 512, mobile: 387 },
		{ month: 'April', desktop: 629, mobile: 521 },
		{ month: 'May', desktop: 458, mobile: 412 },
		{ month: 'June', desktop: 781, mobile: 598 },
		{ month: 'July', desktop: 394, mobile: 312 },
		{ month: 'August', desktop: 925, mobile: 743 },
		{ month: 'September', desktop: 647, mobile: 489 },
		{ month: 'October', desktop: 532, mobile: 476 },
		{ month: 'November', desktop: 803, mobile: 687 },
		{ month: 'December', desktop: 271, mobile: 198 }
	];

	const chartConfig = {
		desktop: {
			label: 'Desktop',
			colors: {
				light: ['red', 'orange', 'rosybrown', 'purple', 'blue'], // [!code highlight]
				dark: ['red', 'orange', 'rosybrown', 'purple', 'blue'] // [!code highlight]
			}
		},
		mobile: {
			label: 'Mobile',
			colors: {
				light: ['gray'],
				dark: ['gray']
			}
		}
	} satisfies ChartConfig;
</script>

<EChartsAreaChart {data} config={chartConfig} class="h-full w-full p-4" stackType="stacked">
	<EChartsAreaChart.Grid />
	<EChartsAreaChart.XAxis
		dataKey="month"
		tickFormatter={(value) => String(value).substring(0, 3)}
	/>
	<EChartsAreaChart.Legend isClickable />
	<EChartsAreaChart.Tooltip />
	<EChartsAreaChart.Area dataKey="desktop" variant="gradient" isClickable>
		<EChartsAreaChart.Dot variant="colored-border" />
		<EChartsAreaChart.ActiveDot variant="default" />
	</EChartsAreaChart.Area>
	<EChartsAreaChart.Area dataKey="mobile" variant="gradient" isClickable>
		<EChartsAreaChart.Dot variant="colored-border" />
		<EChartsAreaChart.ActiveDot variant="default" />
	</EChartsAreaChart.Area>
</EChartsAreaChart>
```
### gradient colors - bump

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

	const data = [
		{ month: 'January', desktop: 342, mobile: 245 },
		{ month: 'February', desktop: 876, mobile: 654 },
		{ month: 'March', desktop: 512, mobile: 387 },
		{ month: 'April', desktop: 629, mobile: 521 },
		{ month: 'May', desktop: 458, mobile: 412 },
		{ month: 'June', desktop: 781, mobile: 598 },
		{ month: 'July', desktop: 394, mobile: 312 },
		{ month: 'August', desktop: 925, mobile: 743 },
		{ month: 'September', desktop: 647, mobile: 489 },
		{ month: 'October', desktop: 532, mobile: 476 },
		{ month: 'November', desktop: 803, mobile: 687 },
		{ month: 'December', desktop: 271, mobile: 198 }
	];

	const chartConfig = {
		desktop: {
			label: 'Desktop',
			colors: {
				light: ['red', 'orange', 'rosybrown', 'purple', 'blue'], // [!code highlight]
				dark: ['red', 'orange', 'rosybrown', 'purple', 'blue'] // [!code highlight]
			}
		},
		mobile: {
			label: 'Mobile',
			colors: {
				light: ['gray'],
				dark: ['gray']
			}
		}
	} satisfies ChartConfig;
</script>

<EChartsAreaChart
	{data}
	config={chartConfig}
	class="h-full w-full p-4"
	stackType="stacked"
	curveType="bump"
>
	<EChartsAreaChart.Grid />
	<EChartsAreaChart.XAxis
		dataKey="month"
		tickFormatter={(value) => String(value).substring(0, 3)}
	/>
	<EChartsAreaChart.Legend isClickable />
	<EChartsAreaChart.Tooltip />
	<EChartsAreaChart.Area dataKey="desktop" variant="gradient" isClickable>
		<EChartsAreaChart.Dot variant="colored-border" />
		<EChartsAreaChart.ActiveDot variant="default" />
	</EChartsAreaChart.Area>
	<EChartsAreaChart.Area dataKey="mobile" variant="gradient" isClickable>
		<EChartsAreaChart.Dot variant="colored-border" />
		<EChartsAreaChart.ActiveDot variant="default" />
	</EChartsAreaChart.Area>
</EChartsAreaChart>
```

### Curve Types

### curveType='bump'

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

	const data = [
		{ month: 'January', desktop: 342, mobile: 245 },
		{ month: 'February', desktop: 876, mobile: 654 },
		{ month: 'March', desktop: 512, mobile: 387 },
		{ month: 'April', desktop: 629, mobile: 521 },
		{ month: 'May', desktop: 458, mobile: 412 },
		{ month: 'June', desktop: 781, mobile: 598 },
		{ month: 'July', desktop: 394, mobile: 312 },
		{ month: 'August', desktop: 925, mobile: 743 },
		{ month: 'September', desktop: 647, mobile: 489 },
		{ month: 'October', desktop: 532, mobile: 476 },
		{ month: 'November', desktop: 803, mobile: 687 },
		{ month: 'December', desktop: 271, mobile: 198 }
	];

	const chartConfig = {
		desktop: {
			label: 'Desktop',
			colors: {
				light: ['#047857'],
				dark: ['#10b981']
			}
		},
		mobile: {
			label: 'Mobile',
			colors: {
				light: ['#be123c'],
				dark: ['#f43f5e']
			}
		}
	} satisfies ChartConfig;
</script>

<!-- [!code highlight:7] -->
<EChartsAreaChart
	{data}
	config={chartConfig}
	class="h-full w-full p-4"
	stackType="stacked"
	curveType="bump"
>
	<EChartsAreaChart.Grid />
	<EChartsAreaChart.XAxis
		dataKey="month"
		tickFormatter={(value) => String(value).substring(0, 3)}
	/>
	<EChartsAreaChart.YAxis dataKey="desktop" />
	<EChartsAreaChart.Legend isClickable />
	<EChartsAreaChart.Tooltip />
	<EChartsAreaChart.Area dataKey="desktop" variant="gradient" isClickable>
		<EChartsAreaChart.Dot variant="default" />
		<EChartsAreaChart.ActiveDot variant="default" />
	</EChartsAreaChart.Area>
	<EChartsAreaChart.Area dataKey="mobile" variant="gradient" isClickable>
		<EChartsAreaChart.Dot variant="default" />
		<EChartsAreaChart.ActiveDot variant="default" />
	</EChartsAreaChart.Area>
</EChartsAreaChart>
```
### curveType='step'

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

	const data = [
		{ month: 'January', desktop: 342, mobile: 245 },
		{ month: 'February', desktop: 876, mobile: 654 },
		{ month: 'March', desktop: 512, mobile: 387 },
		{ month: 'April', desktop: 629, mobile: 521 },
		{ month: 'May', desktop: 458, mobile: 412 },
		{ month: 'June', desktop: 781, mobile: 598 },
		{ month: 'July', desktop: 394, mobile: 312 },
		{ month: 'August', desktop: 925, mobile: 743 },
		{ month: 'September', desktop: 647, mobile: 489 },
		{ month: 'October', desktop: 532, mobile: 476 },
		{ month: 'November', desktop: 803, mobile: 687 },
		{ month: 'December', desktop: 271, mobile: 198 }
	];

	const chartConfig = {
		desktop: {
			label: 'Desktop',
			colors: {
				light: ['#047857'],
				dark: ['#10b981']
			}
		},
		mobile: {
			label: 'Mobile',
			colors: {
				light: ['#be123c'],
				dark: ['#f43f5e']
			}
		}
	} satisfies ChartConfig;
</script>

<!-- [!code highlight:7] -->
<EChartsAreaChart
	{data}
	config={chartConfig}
	class="h-full w-full p-4"
	stackType="stacked"
	curveType="step"
>
	<EChartsAreaChart.Grid />
	<EChartsAreaChart.XAxis
		dataKey="month"
		tickFormatter={(value) => String(value).substring(0, 3)}
	/>
	<EChartsAreaChart.YAxis dataKey="desktop" />
	<EChartsAreaChart.Legend isClickable />
	<EChartsAreaChart.Tooltip />
	<EChartsAreaChart.Area dataKey="desktop" variant="gradient" isClickable>
		<EChartsAreaChart.Dot variant="default" />
		<EChartsAreaChart.ActiveDot variant="default" />
	</EChartsAreaChart.Area>
	<EChartsAreaChart.Area dataKey="mobile" variant="gradient" isClickable>
		<EChartsAreaChart.Dot variant="default" />
		<EChartsAreaChart.ActiveDot variant="default" />
	</EChartsAreaChart.Area>
</EChartsAreaChart>
```
### curveType='monotoneY'

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

	const data = [
		{ month: 'January', desktop: 342, mobile: 245 },
		{ month: 'February', desktop: 876, mobile: 654 },
		{ month: 'March', desktop: 512, mobile: 387 },
		{ month: 'April', desktop: 629, mobile: 521 },
		{ month: 'May', desktop: 458, mobile: 412 },
		{ month: 'June', desktop: 781, mobile: 598 },
		{ month: 'July', desktop: 394, mobile: 312 },
		{ month: 'August', desktop: 925, mobile: 743 },
		{ month: 'September', desktop: 647, mobile: 489 },
		{ month: 'October', desktop: 532, mobile: 476 },
		{ month: 'November', desktop: 803, mobile: 687 },
		{ month: 'December', desktop: 271, mobile: 198 }
	];

	const chartConfig = {
		desktop: {
			label: 'Desktop',
			colors: {
				light: ['#047857'],
				dark: ['#10b981']
			}
		},
		mobile: {
			label: 'Mobile',
			colors: {
				light: ['#be123c'],
				dark: ['#f43f5e']
			}
		}
	} satisfies ChartConfig;
</script>

<!-- [!code highlight:7] -->
<EChartsAreaChart
	{data}
	config={chartConfig}
	class="h-full w-full p-4"
	stackType="stacked"
	curveType="monotoneY"
>
	<EChartsAreaChart.Grid />
	<EChartsAreaChart.XAxis
		dataKey="month"
		tickFormatter={(value) => String(value).substring(0, 3)}
	/>
	<EChartsAreaChart.YAxis dataKey="desktop" />
	<EChartsAreaChart.Legend isClickable />
	<EChartsAreaChart.Tooltip />
	<EChartsAreaChart.Area dataKey="desktop" variant="gradient" isClickable>
		<EChartsAreaChart.Dot variant="default" />
		<EChartsAreaChart.ActiveDot variant="default" />
	</EChartsAreaChart.Area>
	<EChartsAreaChart.Area dataKey="mobile" variant="gradient" isClickable>
		<EChartsAreaChart.Dot variant="default" />
		<EChartsAreaChart.ActiveDot variant="default" />
	</EChartsAreaChart.Area>
</EChartsAreaChart>
```

### Stack Types

### stackType='default'

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

	const data = [
		{ month: 'January', desktop: 342, mobile: 245 },
		{ month: 'February', desktop: 876, mobile: 654 },
		{ month: 'March', desktop: 512, mobile: 387 },
		{ month: 'April', desktop: 629, mobile: 521 },
		{ month: 'May', desktop: 458, mobile: 412 },
		{ month: 'June', desktop: 781, mobile: 598 },
		{ month: 'July', desktop: 394, mobile: 312 },
		{ month: 'August', desktop: 925, mobile: 743 },
		{ month: 'September', desktop: 647, mobile: 489 },
		{ month: 'October', desktop: 532, mobile: 476 },
		{ month: 'November', desktop: 803, mobile: 687 },
		{ month: 'December', desktop: 271, mobile: 198 }
	];

	const chartConfig = {
		desktop: {
			label: 'Desktop',
			colors: {
				light: ['#047857'],
				dark: ['#10b981']
			}
		},
		mobile: {
			label: 'Mobile',
			colors: {
				light: ['#be123c'],
				dark: ['#f43f5e']
			}
		}
	} satisfies ChartConfig;
</script>

<!-- [!code highlight:6] -->
<EChartsAreaChart {data} config={chartConfig} class="h-full w-full p-4" stackType="default">
	<EChartsAreaChart.Grid />
	<EChartsAreaChart.XAxis
		dataKey="month"
		tickFormatter={(value) => String(value).substring(0, 3)}
	/>
	<EChartsAreaChart.YAxis dataKey="desktop" />
	<EChartsAreaChart.Legend isClickable />
	<EChartsAreaChart.Tooltip />
	<EChartsAreaChart.Area dataKey="desktop" variant="gradient" isClickable>
		<EChartsAreaChart.ActiveDot variant="default" />
	</EChartsAreaChart.Area>
	<EChartsAreaChart.Area dataKey="mobile" variant="gradient" isClickable>
		<EChartsAreaChart.ActiveDot variant="default" />
	</EChartsAreaChart.Area>
</EChartsAreaChart>
```
### stackType='stacked'

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

	const data = [
		{ month: 'January', desktop: 342, mobile: 245 },
		{ month: 'February', desktop: 876, mobile: 654 },
		{ month: 'March', desktop: 512, mobile: 387 },
		{ month: 'April', desktop: 629, mobile: 521 },
		{ month: 'May', desktop: 458, mobile: 412 },
		{ month: 'June', desktop: 781, mobile: 598 },
		{ month: 'July', desktop: 394, mobile: 312 },
		{ month: 'August', desktop: 925, mobile: 743 },
		{ month: 'September', desktop: 647, mobile: 489 },
		{ month: 'October', desktop: 532, mobile: 476 },
		{ month: 'November', desktop: 803, mobile: 687 },
		{ month: 'December', desktop: 271, mobile: 198 }
	];

	const chartConfig = {
		desktop: {
			label: 'Desktop',
			colors: {
				light: ['#047857'],
				dark: ['#10b981']
			}
		},
		mobile: {
			label: 'Mobile',
			colors: {
				light: ['#be123c'],
				dark: ['#f43f5e']
			}
		}
	} satisfies ChartConfig;
</script>

<!-- [!code highlight:6] -->
<EChartsAreaChart {data} config={chartConfig} class="h-full w-full p-4" stackType="stacked">
	<EChartsAreaChart.Grid />
	<EChartsAreaChart.XAxis
		dataKey="month"
		tickFormatter={(value) => String(value).substring(0, 3)}
	/>
	<EChartsAreaChart.YAxis dataKey="desktop" />
	<EChartsAreaChart.Legend isClickable />
	<EChartsAreaChart.Tooltip />
	<EChartsAreaChart.Area dataKey="desktop" variant="gradient" isClickable>
		<EChartsAreaChart.ActiveDot variant="default" />
	</EChartsAreaChart.Area>
	<EChartsAreaChart.Area dataKey="mobile" variant="gradient" isClickable>
		<EChartsAreaChart.ActiveDot variant="default" />
	</EChartsAreaChart.Area>
</EChartsAreaChart>
```
### stackType='expanded'

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

	const data = [
		{ month: 'January', desktop: 342, mobile: 245 },
		{ month: 'February', desktop: 876, mobile: 654 },
		{ month: 'March', desktop: 512, mobile: 387 },
		{ month: 'April', desktop: 629, mobile: 521 },
		{ month: 'May', desktop: 458, mobile: 412 },
		{ month: 'June', desktop: 781, mobile: 598 },
		{ month: 'July', desktop: 394, mobile: 312 },
		{ month: 'August', desktop: 925, mobile: 743 },
		{ month: 'September', desktop: 647, mobile: 489 },
		{ month: 'October', desktop: 532, mobile: 476 },
		{ month: 'November', desktop: 803, mobile: 687 },
		{ month: 'December', desktop: 271, mobile: 198 }
	];

	const chartConfig = {
		desktop: {
			label: 'Desktop',
			colors: {
				light: ['#047857'],
				dark: ['#10b981']
			}
		},
		mobile: {
			label: 'Mobile',
			colors: {
				light: ['#be123c'],
				dark: ['#f43f5e']
			}
		}
	} satisfies ChartConfig;
</script>

<!-- [!code highlight:6] -->
<EChartsAreaChart {data} config={chartConfig} class="h-full w-full p-4" stackType="expanded">
	<EChartsAreaChart.Grid />
	<EChartsAreaChart.XAxis
		dataKey="month"
		tickFormatter={(value) => String(value).substring(0, 3)}
	/>
	<EChartsAreaChart.YAxis dataKey="desktop" />
	<EChartsAreaChart.Legend isClickable />
	<EChartsAreaChart.Tooltip />
	<EChartsAreaChart.Area dataKey="desktop" variant="gradient" isClickable>
		<EChartsAreaChart.ActiveDot variant="default" />
	</EChartsAreaChart.Area>
	<EChartsAreaChart.Area dataKey="mobile" variant="gradient" isClickable>
		<EChartsAreaChart.ActiveDot variant="default" />
	</EChartsAreaChart.Area>
</EChartsAreaChart>
```

### Stroke Variants

### strokeVariant='solid'

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

	const data = [
		{ month: 'January', desktop: 342, mobile: 245 },
		{ month: 'February', desktop: 876, mobile: 654 },
		{ month: 'March', desktop: 512, mobile: 387 },
		{ month: 'April', desktop: 629, mobile: 521 },
		{ month: 'May', desktop: 458, mobile: 412 },
		{ month: 'June', desktop: 781, mobile: 598 },
		{ month: 'July', desktop: 394, mobile: 312 },
		{ month: 'August', desktop: 925, mobile: 743 },
		{ month: 'September', desktop: 647, mobile: 489 },
		{ month: 'October', desktop: 532, mobile: 476 },
		{ month: 'November', desktop: 803, mobile: 687 },
		{ month: 'December', desktop: 271, mobile: 198 }
	];

	const chartConfig = {
		desktop: {
			label: 'Desktop',
			colors: {
				light: ['#047857'],
				dark: ['#10b981']
			}
		},
		mobile: {
			label: 'Mobile',
			colors: {
				light: ['#be123c'],
				dark: ['#f43f5e']
			}
		}
	} satisfies ChartConfig;
</script>

<EChartsAreaChart {data} config={chartConfig} class="h-full w-full p-4" stackType="stacked">
	<EChartsAreaChart.Grid />
	<EChartsAreaChart.XAxis
		dataKey="month"
		tickFormatter={(value) => String(value).substring(0, 3)}
	/>
	<EChartsAreaChart.YAxis dataKey="desktop" />
	<EChartsAreaChart.Legend isClickable />
	<EChartsAreaChart.Tooltip />
	<!-- [!code highlight:3] -->
	<EChartsAreaChart.Area dataKey="desktop" variant="gradient" strokeVariant="solid" isClickable>
		<EChartsAreaChart.ActiveDot variant="default" />
	</EChartsAreaChart.Area>
	<!-- [!code highlight:3] -->
	<EChartsAreaChart.Area dataKey="mobile" variant="gradient" strokeVariant="solid" isClickable>
		<EChartsAreaChart.ActiveDot variant="default" />
	</EChartsAreaChart.Area>
</EChartsAreaChart>
```
### strokeVariant='dashed'

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

	const data = [
		{ month: 'January', desktop: 342, mobile: 245 },
		{ month: 'February', desktop: 876, mobile: 654 },
		{ month: 'March', desktop: 512, mobile: 387 },
		{ month: 'April', desktop: 629, mobile: 521 },
		{ month: 'May', desktop: 458, mobile: 412 },
		{ month: 'June', desktop: 781, mobile: 598 },
		{ month: 'July', desktop: 394, mobile: 312 },
		{ month: 'August', desktop: 925, mobile: 743 },
		{ month: 'September', desktop: 647, mobile: 489 },
		{ month: 'October', desktop: 532, mobile: 476 },
		{ month: 'November', desktop: 803, mobile: 687 },
		{ month: 'December', desktop: 271, mobile: 198 }
	];

	const chartConfig = {
		desktop: {
			label: 'Desktop',
			colors: {
				light: ['#047857'],
				dark: ['#10b981']
			}
		},
		mobile: {
			label: 'Mobile',
			colors: {
				light: ['#be123c'],
				dark: ['#f43f5e']
			}
		}
	} satisfies ChartConfig;
</script>

<EChartsAreaChart {data} config={chartConfig} class="h-full w-full p-4" stackType="stacked">
	<EChartsAreaChart.Grid />
	<EChartsAreaChart.XAxis
		dataKey="month"
		tickFormatter={(value) => String(value).substring(0, 3)}
	/>
	<EChartsAreaChart.YAxis dataKey="desktop" />
	<EChartsAreaChart.Legend isClickable />
	<EChartsAreaChart.Tooltip />
	<!-- [!code highlight:3] -->
	<EChartsAreaChart.Area dataKey="desktop" variant="gradient" strokeVariant="dashed" isClickable>
		<EChartsAreaChart.ActiveDot variant="default" />
	</EChartsAreaChart.Area>
	<!-- [!code highlight:3] -->
	<EChartsAreaChart.Area dataKey="mobile" variant="gradient" strokeVariant="dashed" isClickable>
		<EChartsAreaChart.ActiveDot variant="default" />
	</EChartsAreaChart.Area>
</EChartsAreaChart>
```
### strokeVariant='animated-dashed'

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

	const data = [
		{ month: 'January', desktop: 342, mobile: 245 },
		{ month: 'February', desktop: 876, mobile: 654 },
		{ month: 'March', desktop: 512, mobile: 387 },
		{ month: 'April', desktop: 629, mobile: 521 },
		{ month: 'May', desktop: 458, mobile: 412 },
		{ month: 'June', desktop: 781, mobile: 598 },
		{ month: 'July', desktop: 394, mobile: 312 },
		{ month: 'August', desktop: 925, mobile: 743 },
		{ month: 'September', desktop: 647, mobile: 489 },
		{ month: 'October', desktop: 532, mobile: 476 },
		{ month: 'November', desktop: 803, mobile: 687 },
		{ month: 'December', desktop: 271, mobile: 198 }
	];

	const chartConfig = {
		desktop: {
			label: 'Desktop',
			colors: {
				light: ['#047857'],
				dark: ['#10b981']
			}
		},
		mobile: {
			label: 'Mobile',
			colors: {
				light: ['#be123c'],
				dark: ['#f43f5e']
			}
		}
	} satisfies ChartConfig;
</script>

<EChartsAreaChart {data} config={chartConfig} class="h-full w-full p-4" stackType="stacked">
	<EChartsAreaChart.Grid />
	<EChartsAreaChart.XAxis
		dataKey="month"
		tickFormatter={(value) => String(value).substring(0, 3)}
	/>
	<EChartsAreaChart.YAxis dataKey="desktop" />
	<EChartsAreaChart.Legend isClickable />
	<EChartsAreaChart.Tooltip />
	<!-- [!code highlight:3] -->
	<EChartsAreaChart.Area
		dataKey="desktop"
		variant="gradient"
		strokeVariant="animated-dashed"
		isClickable
	>
		<EChartsAreaChart.ActiveDot variant="default" />
	</EChartsAreaChart.Area>
	<!-- [!code highlight:3] -->
	<EChartsAreaChart.Area
		dataKey="mobile"
		variant="gradient"
		strokeVariant="animated-dashed"
		isClickable
	>
		<EChartsAreaChart.ActiveDot variant="default" />
	</EChartsAreaChart.Area>
</EChartsAreaChart>
```

### Area Variants

### variant='gradient'

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

	const data = [
		{ month: 'January', desktop: 342, mobile: 245 },
		{ month: 'February', desktop: 876, mobile: 654 },
		{ month: 'March', desktop: 512, mobile: 387 },
		{ month: 'April', desktop: 629, mobile: 521 },
		{ month: 'May', desktop: 458, mobile: 412 },
		{ month: 'June', desktop: 781, mobile: 598 },
		{ month: 'July', desktop: 394, mobile: 312 },
		{ month: 'August', desktop: 925, mobile: 743 },
		{ month: 'September', desktop: 647, mobile: 489 },
		{ month: 'October', desktop: 532, mobile: 476 },
		{ month: 'November', desktop: 803, mobile: 687 },
		{ month: 'December', desktop: 271, mobile: 198 }
	];

	const chartConfig = {
		desktop: {
			label: 'Desktop',
			colors: {
				light: ['#047857'],
				dark: ['#10b981']
			}
		},
		mobile: {
			label: 'Mobile',
			colors: {
				light: ['#be123c'],
				dark: ['#f43f5e']
			}
		}
	} satisfies ChartConfig;
</script>

<EChartsAreaChart {data} config={chartConfig} class="h-full w-full p-4" stackType="stacked">
	<EChartsAreaChart.Grid />
	<EChartsAreaChart.XAxis
		dataKey="month"
		tickFormatter={(value) => String(value).substring(0, 3)}
	/>
	<EChartsAreaChart.YAxis dataKey="desktop" />
	<EChartsAreaChart.Legend isClickable />
	<EChartsAreaChart.Tooltip />
	<!-- [!code highlight:3] -->
	<EChartsAreaChart.Area dataKey="desktop" variant="gradient" isClickable>
		<EChartsAreaChart.ActiveDot variant="default" />
	</EChartsAreaChart.Area>
	<!-- [!code highlight:3] -->
	<EChartsAreaChart.Area dataKey="mobile" variant="gradient" isClickable>
		<EChartsAreaChart.ActiveDot variant="default" />
	</EChartsAreaChart.Area>
</EChartsAreaChart>
```
### variant='gradient-reverse'

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

	const data = [
		{ month: 'January', desktop: 342, mobile: 245 },
		{ month: 'February', desktop: 876, mobile: 654 },
		{ month: 'March', desktop: 512, mobile: 387 },
		{ month: 'April', desktop: 629, mobile: 521 },
		{ month: 'May', desktop: 458, mobile: 412 },
		{ month: 'June', desktop: 781, mobile: 598 },
		{ month: 'July', desktop: 394, mobile: 312 },
		{ month: 'August', desktop: 925, mobile: 743 },
		{ month: 'September', desktop: 647, mobile: 489 },
		{ month: 'October', desktop: 532, mobile: 476 },
		{ month: 'November', desktop: 803, mobile: 687 },
		{ month: 'December', desktop: 271, mobile: 198 }
	];

	const chartConfig = {
		desktop: {
			label: 'Desktop',
			colors: {
				light: ['#047857'],
				dark: ['#10b981']
			}
		},
		mobile: {
			label: 'Mobile',
			colors: {
				light: ['#be123c'],
				dark: ['#f43f5e']
			}
		}
	} satisfies ChartConfig;
</script>

<EChartsAreaChart {data} config={chartConfig} class="h-full w-full p-4" stackType="stacked">
	<EChartsAreaChart.Grid />
	<EChartsAreaChart.XAxis
		dataKey="month"
		tickFormatter={(value) => String(value).substring(0, 3)}
	/>
	<EChartsAreaChart.YAxis dataKey="desktop" />
	<EChartsAreaChart.Legend isClickable />
	<EChartsAreaChart.Tooltip />
	<!-- [!code highlight:3] -->
	<EChartsAreaChart.Area dataKey="desktop" variant="gradient-reverse" isClickable>
		<EChartsAreaChart.ActiveDot variant="default" />
	</EChartsAreaChart.Area>
	<!-- [!code highlight:3] -->
	<EChartsAreaChart.Area dataKey="mobile" variant="gradient-reverse" isClickable>
		<EChartsAreaChart.ActiveDot variant="default" />
	</EChartsAreaChart.Area>
</EChartsAreaChart>
```
### variant='solid'

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

	const data = [
		{ month: 'January', desktop: 342, mobile: 245 },
		{ month: 'February', desktop: 876, mobile: 654 },
		{ month: 'March', desktop: 512, mobile: 387 },
		{ month: 'April', desktop: 629, mobile: 521 },
		{ month: 'May', desktop: 458, mobile: 412 },
		{ month: 'June', desktop: 781, mobile: 598 },
		{ month: 'July', desktop: 394, mobile: 312 },
		{ month: 'August', desktop: 925, mobile: 743 },
		{ month: 'September', desktop: 647, mobile: 489 },
		{ month: 'October', desktop: 532, mobile: 476 },
		{ month: 'November', desktop: 803, mobile: 687 },
		{ month: 'December', desktop: 271, mobile: 198 }
	];

	const chartConfig = {
		desktop: {
			label: 'Desktop',
			colors: {
				light: ['#047857'],
				dark: ['#10b981']
			}
		},
		mobile: {
			label: 'Mobile',
			colors: {
				light: ['#be123c'],
				dark: ['#f43f5e']
			}
		}
	} satisfies ChartConfig;
</script>

<EChartsAreaChart {data} config={chartConfig} class="h-full w-full p-4" stackType="stacked">
	<EChartsAreaChart.Grid />
	<EChartsAreaChart.XAxis
		dataKey="month"
		tickFormatter={(value) => String(value).substring(0, 3)}
	/>
	<EChartsAreaChart.YAxis dataKey="desktop" />
	<EChartsAreaChart.Legend isClickable />
	<EChartsAreaChart.Tooltip />
	<!-- [!code highlight:3] -->
	<EChartsAreaChart.Area dataKey="desktop" variant="solid" isClickable>
		<EChartsAreaChart.ActiveDot variant="default" />
	</EChartsAreaChart.Area>
	<!-- [!code highlight:3] -->
	<EChartsAreaChart.Area dataKey="mobile" variant="solid" isClickable>
		<EChartsAreaChart.ActiveDot variant="default" />
	</EChartsAreaChart.Area>
</EChartsAreaChart>
```
### variant='dotted'

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

	const data = [
		{ month: 'January', desktop: 342, mobile: 245 },
		{ month: 'February', desktop: 876, mobile: 654 },
		{ month: 'March', desktop: 512, mobile: 387 },
		{ month: 'April', desktop: 629, mobile: 521 },
		{ month: 'May', desktop: 458, mobile: 412 },
		{ month: 'June', desktop: 781, mobile: 598 },
		{ month: 'July', desktop: 394, mobile: 312 },
		{ month: 'August', desktop: 925, mobile: 743 },
		{ month: 'September', desktop: 647, mobile: 489 },
		{ month: 'October', desktop: 532, mobile: 476 },
		{ month: 'November', desktop: 803, mobile: 687 },
		{ month: 'December', desktop: 271, mobile: 198 }
	];

	const chartConfig = {
		desktop: {
			label: 'Desktop',
			colors: {
				light: ['#047857'],
				dark: ['#10b981']
			}
		},
		mobile: {
			label: 'Mobile',
			colors: {
				light: ['#be123c'],
				dark: ['#f43f5e']
			}
		}
	} satisfies ChartConfig;
</script>

<EChartsAreaChart {data} config={chartConfig} class="h-full w-full p-4" stackType="stacked">
	<EChartsAreaChart.Grid />
	<EChartsAreaChart.XAxis
		dataKey="month"
		tickFormatter={(value) => String(value).substring(0, 3)}
	/>
	<EChartsAreaChart.YAxis dataKey="desktop" />
	<EChartsAreaChart.Legend isClickable />
	<EChartsAreaChart.Tooltip />
	<!-- [!code highlight:3] -->
	<EChartsAreaChart.Area dataKey="desktop" variant="dotted" isClickable>
		<EChartsAreaChart.ActiveDot variant="default" />
	</EChartsAreaChart.Area>
	<!-- [!code highlight:3] -->
	<EChartsAreaChart.Area dataKey="mobile" variant="dotted" isClickable>
		<EChartsAreaChart.ActiveDot variant="default" />
	</EChartsAreaChart.Area>
</EChartsAreaChart>
```
### variant='lines'

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

	const data = [
		{ month: 'January', desktop: 342, mobile: 245 },
		{ month: 'February', desktop: 876, mobile: 654 },
		{ month: 'March', desktop: 512, mobile: 387 },
		{ month: 'April', desktop: 629, mobile: 521 },
		{ month: 'May', desktop: 458, mobile: 412 },
		{ month: 'June', desktop: 781, mobile: 598 },
		{ month: 'July', desktop: 394, mobile: 312 },
		{ month: 'August', desktop: 925, mobile: 743 },
		{ month: 'September', desktop: 647, mobile: 489 },
		{ month: 'October', desktop: 532, mobile: 476 },
		{ month: 'November', desktop: 803, mobile: 687 },
		{ month: 'December', desktop: 271, mobile: 198 }
	];

	const chartConfig = {
		desktop: {
			label: 'Desktop',
			colors: {
				light: ['#047857'],
				dark: ['#10b981']
			}
		},
		mobile: {
			label: 'Mobile',
			colors: {
				light: ['#be123c'],
				dark: ['#f43f5e']
			}
		}
	} satisfies ChartConfig;
</script>

<EChartsAreaChart {data} config={chartConfig} class="h-full w-full p-4" stackType="stacked">
	<EChartsAreaChart.Grid />
	<EChartsAreaChart.XAxis
		dataKey="month"
		tickFormatter={(value) => String(value).substring(0, 3)}
	/>
	<EChartsAreaChart.YAxis dataKey="desktop" />
	<EChartsAreaChart.Legend isClickable />
	<EChartsAreaChart.Tooltip />
	<!-- [!code highlight:3] -->
	<EChartsAreaChart.Area dataKey="desktop" variant="lines" isClickable>
		<EChartsAreaChart.ActiveDot variant="default" />
	</EChartsAreaChart.Area>
	<!-- [!code highlight:3] -->
	<EChartsAreaChart.Area dataKey="mobile" variant="lines" isClickable>
		<EChartsAreaChart.ActiveDot variant="default" />
	</EChartsAreaChart.Area>
</EChartsAreaChart>
```
### variant='hatched'

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

	const data = [
		{ month: 'January', desktop: 342, mobile: 245 },
		{ month: 'February', desktop: 876, mobile: 654 },
		{ month: 'March', desktop: 512, mobile: 387 },
		{ month: 'April', desktop: 629, mobile: 521 },
		{ month: 'May', desktop: 458, mobile: 412 },
		{ month: 'June', desktop: 781, mobile: 598 },
		{ month: 'July', desktop: 394, mobile: 312 },
		{ month: 'August', desktop: 925, mobile: 743 },
		{ month: 'September', desktop: 647, mobile: 489 },
		{ month: 'October', desktop: 532, mobile: 476 },
		{ month: 'November', desktop: 803, mobile: 687 },
		{ month: 'December', desktop: 271, mobile: 198 }
	];

	const chartConfig = {
		desktop: {
			label: 'Desktop',
			colors: {
				light: ['#047857'],
				dark: ['#10b981']
			}
		},
		mobile: {
			label: 'Mobile',
			colors: {
				light: ['#be123c'],
				dark: ['#f43f5e']
			}
		}
	} satisfies ChartConfig;
</script>

<EChartsAreaChart {data} config={chartConfig} class="h-full w-full p-4" stackType="stacked">
	<EChartsAreaChart.Grid />
	<EChartsAreaChart.XAxis
		dataKey="month"
		tickFormatter={(value) => String(value).substring(0, 3)}
	/>
	<EChartsAreaChart.YAxis dataKey="desktop" />
	<EChartsAreaChart.Legend isClickable />
	<EChartsAreaChart.Tooltip />
	<!-- [!code highlight:3] -->
	<EChartsAreaChart.Area dataKey="desktop" variant="hatched" isClickable>
		<EChartsAreaChart.ActiveDot variant="default" />
	</EChartsAreaChart.Area>
	<!-- [!code highlight:3] -->
	<EChartsAreaChart.Area dataKey="mobile" variant="hatched" isClickable>
		<EChartsAreaChart.ActiveDot variant="default" />
	</EChartsAreaChart.Area>
</EChartsAreaChart>
```

### Ordered dither

Set `renderStyle="dither"` to use the independent ordered-dither treatment inspired by [Dither Kit](https://github.com/Boring-Software-Inc/dither-kit). Axes, tooltips, selection, and the ECharts renderer stay intact.

### renderStyle="dither"

```svelte
<script lang="ts">
	import {
		EChartsAreaChart,
		type ChartConfig
	} from '$lib/components/evilcharts/charts/echarts-area-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', '#34d399'], dark: ['#10b981', '#6ee7b7'] }
		},
		mobile: {
			label: 'Mobile',
			colors: { light: ['#be123c', '#fb7185'], dark: ['#f43f5e', '#fda4af'] }
		}
	} satisfies ChartConfig;
</script>

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

## API Reference

Props are grouped by the part they belong to. Regardless of renderer, each part is declarative config the root compiles, but the API mirrors the LayerChart sibling one-to-one.

### EChartsAreaChart

The root container. It owns the data, shared selection state, loading skeleton, and optional native `dataZoom` brush. Everything visual is composed as its children and compiled into the ECharts option.


  ### `data` (required)

type: `TData[]`

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

type: `ChartConfig`

Defines the chart's series. Each key matches a data key, with a `label` and a per-theme `colors` array. Same contract as every EvilCharts chart — see [Chart Config](/docs/chart-config).
  ### `children` (required)

type: `Snippet`

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

type: `string`

Additional CSS classes for the chart container.
  ### `renderer`

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

Rendering engine used by ECharts. Use `"svg"` for an SVG-backed chart surface; omit the prop to keep the Canvas default.
  ### `renderStyle`

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

Selects native ECharts paint or EvilCharts' ordered-dither rendering.
  ### `ditherVariant`

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

Default ordered-dither pattern used by the chart's series.
  ### `ditherCellSize`

type: `number` · default: `2`

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

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

Optional glow applied to dithered marks. It has no effect in native rendering mode.
  ### `xDataKey`

type: `keyof TData & string`

Data key for the x-axis categories.
  ### `curveType`

type: `"linear" | "smooth" | "bump" | "monotone" | "monotoneX" | "monotoneY" | "natural" | "step"` · default: `"linear"`

Default curve interpolation inherited by every `<Area />`.
  ### `animation`

type: `boolean` · default: `true`

Master switch for the intro draw-in. Pass `false` to render instantly, regardless of `animationType`.
  ### `animationType`

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

The intro animation inherited by every `<Area />`. Any value but `"none"` plays ECharts' native progressive draw-in — the line traces in and dots pop as its front passes (direction values exist for parity with the LayerChart sibling). `"none"` disables it; OS reduce-motion falls back to `"none"` automatically.
  ### `enableHoverHighlight`

type: `boolean` · default: `false`

Highlights the hovered series by dimming the others — the hover twin of click selection, using the same dim levels.
  ### `enableHoverReveal`

type: `boolean` · default: `false`

On hover, colors each area up to the pointer's x-position and mutes the rest to a neutral gray, with the active dot at the cursor. A standalone hover mode that takes visual precedence over `enableHoverHighlight`; idle, the chart renders normally.
  ### `stackType`

type: `"default" | "stacked" | "expanded"` · default: `"default"`

How multiple areas combine — independent, stacked, or normalized to 100%.
  ### `defaultSelectedDataKey`

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

The series selected on first render.
  ### `selectedDataKey`

type: `string | null`

Controls the selected series. Leave it undefined to let the chart manage selection; pass `null` to clear a controlled selection.
  ### `onSelectionChange`

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

Fires when a series is selected or deselected via a clickable `<Area />` or `<Legend />`.
  ### `isLoading`

type: `boolean` · default: `false`

Shows the animated loading skeleton.
  ### `loadingPoints`

type: `number` · default: `14`

Number of points in the loading skeleton.
  ### `chartOptions`

type: `Record<string, unknown>`

Escape hatch deep-merged into the underlying ECharts option object. See the [ECharts option documentation](https://echarts.apache.org/en/option.html).
  ### `accessibility`

type: `ChartAccessibility`

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


### Area

A single area series. Each `<Area />` carries its own fill and stroke config, so a chart can hold any number — each with its own variant, stroke, and clickability.


  ### `dataKey` (required)

type: `string`

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

type: `"gradient" | "gradient-reverse" | "solid" | "dotted" | "lines" | "hatched"` · default: `"gradient"`

Visual style of the area fill, for this area only. Multi-color configs render the full horizontal gradient faded vertically, matching the LayerChart sibling.
  ### `strokeVariant`

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

The stroke style for this area.
  ### `strokeWidth`

type: `number` · default: `0.8`

Stroke thickness for this area, in pixels.
  ### `curveType`

type: `"linear" | "smooth" | "bump" | "monotone" | "monotoneX" | "monotoneY" | "natural" | "step"`

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

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

The intro draw-in for this area (the first `<Area />`'s value drives the chart). Falls back to the chart's `animationType` when omitted.
  ### `connectNulls`

type: `boolean` · default: `false`

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

type: `boolean` · default: `false`

Makes this area selectable on click. When any area is selected, the rest turn semi-transparent.
  ### `enableBufferLine`

type: `boolean` · default: `false`

Renders this area's last segment as a dashed buffer tail while the rest stays solid — useful for projected or incomplete data at the end of a series.
  ### `ditherVariant`

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

Overrides the root ordered-dither pattern for this area only.
  ### `children`

type: `Snippet`

Optional `<Dot />` and `<ActiveDot />` config that adds point markers to this area.


### Dot and ActiveDot

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


  ### `variant`

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

The visual style of the point marker.


### XAxis and YAxis

The category and value axes. Include `<XAxis />` or `<YAxis />` to show each; omit either to hide it. Both hide automatically while loading, and `<YAxis />` formats ticks as percentages when `stackType="expanded"`.


  ### `dataKey`

type: `string`

The data key for the axis values.
  ### `tickFormatter`

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

Formats the axis tick labels.
  ### `hideDots`

type: `boolean` · default: `false`

Hides the small tick dots that sit beside this axis's labels.


### Grid

The background grid lines. Include it to draw the dashed horizontal split lines; omit it and they don't. Takes no props.

### Tooltip

The hover tooltip. Include it to enable the tooltip; omit it and none shows. It reads selection state, dimming unselected series in its content.


  ### `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.
  ### `cursor`

type: `boolean` · default: `true`

Whether the vertical cursor line follows the pointer on hover.


### Legend

The series legend, rendered as HTML above the chart surface. Include it to show the legend; omit it and none shows. 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 — a themed mini chart driven by ECharts' native `dataZoom`. Include `<EChartsAreaChart.Brush />` to render it; dragging the range filters the main chart.


  ### `height`

type: `number` · default: `56`

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

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

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



Fires when the brush selection range changes.

