
### Basic Chart

```svelte
<script lang="ts">
	import { EChartsBarChart } from '$lib/components/evilcharts/charts/echarts-bar-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>

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

## Installation


  
  
    ### npm

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

### yarn

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

### bun

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

### pnpm

```bash
pnpm dlx shadcn-svelte@latest add @evilcharts/echarts-bar-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 the following code into your project.
        

In your `components` directory, create the nested folders `evilcharts` → `charts`, then paste the bar-chart code into a new `echarts-bar-chart` file inside.


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

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

```ts
import { getContext, setContext } from 'svelte';
import { RegistrationSet } from '$lib/components/evilcharts/ui/echarts-chart/index.js';
import type { AxisRegistration, BarRegistration } from './types.js';
const CONTEXT = Symbol('echarts-bar-chart');
export class EChartsBarChartContext {
	bars = new RegistrationSet<BarRegistration>();
	xAxes = new RegistrationSet<AxisRegistration>();
	yAxes = new RegistrationSet<AxisRegistration>();
	grids = new RegistrationSet<Record<string, never>>();
}
export function setEChartsBarChartContext() {
	const value = new EChartsBarChartContext();
	setContext(CONTEXT, value);
	return value;
}
export function useEChartsBarChart() {
	const value = getContext<EChartsBarChartContext | undefined>(CONTEXT);
	if (!value)
		throw new Error('[EvilCharts] ECharts bar parts must be children of EChartsBarChart.');
	return value;
}
```

`$lib/components/evilcharts/charts/echarts-bar-chart/bar-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,
		MarkLineComponent,
		TooltipComponent
	} from 'echarts/components';
	import { BarChart, CustomChart } 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 { setEChartsBarChartContext } from './bar-chart-context.svelte.js';
	import {
		buildBarOption,
		createBarLoadingData,
		measureBarValueScale,
		measureBarWidth,
		type BarOptionContext
	} from './option.js';
	import type {
		BrushRegistration,
		BarLayout,
		LegendRegistration,
		BarAnimationType,
		BarHoverDatum,
		StackType,
		TooltipRegistration
	} from './types.js';

	echarts.use([
		BarChart,
		CustomChart,
		GridComponent,
		TooltipComponent,
		DataZoomComponent,
		MarkLineComponent,
		AriaComponent
	]);

	let {
		data,
		config,
		renderer = DEFAULT_ECHARTS_RENDERER,
		renderStyle = 'native',
		ditherVariant = 'gradient',
		ditherCellSize = 2,
		bloom = 'off',
		xDataKey,
		class: className,
		stackType = 'default',
		layout = 'vertical',
		barRadius = 2,
		barGap,
		barCategoryGap,
		animation = true,
		animationType = 'left-to-right',
		enableMaxValueHighlight = false,
		referenceLine = null,
		referenceLineFormatter,
		onDataHover,
		defaultSelectedDataKey = null,
		onSelectionChange,
		isLoading = false,
		loadingBars = 12,
		chartOptions,
		accessibility,
		children
	}: {
		data: Record<string, unknown>[];
		config: ChartConfig;
		renderer?: EChartsRenderer;
		renderStyle?: EChartsRenderStyle;
		ditherVariant?: DitherVariant;
		ditherCellSize?: number;
		bloom?: DitherBloom;
		xDataKey?: string;
		class?: string;
		stackType?: StackType;
		layout?: BarLayout;
		barRadius?: number;
		barGap?: number;
		barCategoryGap?: number;
		animation?: boolean;
		animationType?: BarAnimationType;
		enableMaxValueHighlight?: boolean;
		referenceLine?: number | null;
		referenceLineFormatter?: (value: number) => string;
		onDataHover?: (datum: BarHoverDatum | null) => void;
		defaultSelectedDataKey?: string | null;
		onSelectionChange?: (key: string | null) => void;
		isLoading?: boolean;
		loadingBars?: 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 selectedDataKey = $state<string | null>(untrack(() => defaultSelectedDataKey));
	let hoveredDataKey = $state<string | null>(null);
	let introComplete = $state(false);
	let brushRange = $state({ start: 0, end: 100 });
	let brushHover = $state({ inside: false, left: false, right: false });
	let layoutMetrics = $state.raw({
		valuePxPerUnit: null as number | null,
		barWidthPx: null as number | null
	});
	const expand = {
		key: null as string | null,
		hovered: null as number | null,
		progress: new Map<number, number>()
	};
	let expandFrame = 0;
	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 = setEChartsBarChartContext();
	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 bars = $derived(chart.bars.values);
	const selectableSeries = $derived(
		bars
			.filter((bar) => bar.isClickable)
			.filter((bar, index, all) => all.findIndex((item) => item.dataKey === bar.dataKey) === index)
			.map((bar) => ({
				key: bar.dataKey,
				label:
					typeof config[bar.dataKey]?.label === 'string'
						? (config[bar.dataKey].label as string)
						: bar.dataKey
			}))
	);
	const effectiveAnimation = $derived(bars[0]?.animationType ?? animationType);
	const seriesKeys = $derived(bars.map((bar) => bar.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);
	let loadingData = $state.raw<number[]>(untrack(() => createBarLoadingData(loadingBars)));
	$effect(() => {
		loadingData = createBarLoadingData(loadingBars);
	});
	const categoryValues = $derived.by(() => {
		const series = new Set(bars.map((bar) => bar.dataKey));
		const axis = layout === 'vertical' ? xAxis : yAxis;
		const key =
			xDataKey ?? axis?.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 barOptionContext(): BarOptionContext {
		return {
			data,
			config,
			bars,
			xDataKey,
			stackType,
			layout,
			barRadius,
			barGap,
			barCategoryGap,
			selectedDataKey,
			enableMaxValueHighlight,
			referenceLine,
			referenceLineFormatter,
			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,
			valuePxPerUnit: layoutMetrics.valuePxPerUnit,
			barWidthPx: layoutMetrics.barWidthPx,
			expand
		};
	}

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

	$effect(() => {
		if (isLoading) {
			introComplete = false;
			return;
		}
		if (introComplete || !instance || bars.length === 0) return;
		if (!animation || effectiveAnimation === 'none' || prefersReducedMotion.current) {
			introComplete = true;
			return;
		}
		const timer = window.setTimeout(
			() => (introComplete = true),
			500 + Math.max(0, data.length - 1) * 50
		);
		return () => window.clearTimeout(timer);
	});

	$effect(() => {
		const chartInstance = instance;
		if (!chartInstance) return;
		if (!brush || isLoading || layout === 'horizontal') {
			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 ?? ((value: string) => value);
		syncBrushOverlay(chartInstance, brushOverlayStore, {
			range: brushRange,
			geom: { bottom: legend?.verticalAlign === 'bottom' ? 34 : 6, height: brush.height ?? 56 },
			size: dimension,
			tokens: resolved.tokens,
			labels: {
				start: format(categoryValues[startIndex] ?? '', startIndex),
				end: format(categoryValues[endIndex] ?? '', endIndex)
			},
			showLabels: brushHover.inside && Boolean(brush.formatLabel),
			hover: brushHover
		});
	});

	$effect(() => {
		const chartInstance = instance;
		const activeBrush = brush;
		const currentLegend = legend;
		const currentLayout = layout;
		const expandable = bars.find((bar) => bar.variant === 'expandable');
		const hasBlocks = bars.some((bar) => bar.variant === 'blocks');
		const hasStripped = bars.some((bar) => bar.variant === 'stripped');
		const needsValueScale = hasStripped || (stackType !== 'default' && bars.length > 1);
		if (!chartInstance) return;
		const renderer = chartInstance.getZr();

		const syncHover = (x: number, y: number) => {
			if (activeBrush && !isLoading && currentLayout === 'vertical') {
				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 (!expandable) return;
			const point = [x, y];
			let next: number | null = null;
			if (chartInstance.containPixel({ gridIndex: 0 }, point)) {
				const converted = chartInstance.convertFromPixel({ gridIndex: 0 }, point);
				const raw = Array.isArray(converted) ? converted[0] : converted;
				if (typeof raw === 'number') next = Math.round(raw);
			}
			animateExpandable(expandable.dataKey, next);
		};

		const move = (event: { offsetX?: number; offsetY?: number }) =>
			syncHover(event.offsetX ?? -1, event.offsetY ?? -1);
		const out = () => {
			brushHover = { inside: false, left: false, right: false };
			if (expandable) animateExpandable(expandable.dataKey, null);
		};
		const finished = () => {
			let nextValue = layoutMetrics.valuePxPerUnit;
			let nextWidth = layoutMetrics.barWidthPx;
			if (needsValueScale && (introComplete || !animation || isLoading)) {
				nextValue = measureBarValueScale(chartInstance, currentLayout === 'horizontal');
			}
			if (hasBlocks) {
				nextWidth = measureBarWidth(chartInstance, currentLayout === 'horizontal', barCategoryGap);
			}
			if (
				(nextValue !== null &&
					(layoutMetrics.valuePxPerUnit === null ||
						Math.abs(nextValue - layoutMetrics.valuePxPerUnit) > 0.5)) ||
				(nextWidth !== null &&
					(layoutMetrics.barWidthPx === null ||
						Math.abs(nextWidth - layoutMetrics.barWidthPx) > 0.5))
			) {
				layoutMetrics = { valuePxPerUnit: nextValue, barWidthPx: nextWidth };
			}
		};

		renderer.on('mousemove', move);
		renderer.on('globalout', out);
		chartInstance.on('finished', finished);
		return () => {
			renderer.off('mousemove', move);
			renderer.off('globalout', out);
			if (!chartInstance.isDisposed()) chartInstance.off('finished', finished);
			if (expandFrame) {
				cancelAnimationFrame(expandFrame);
				expandFrame = 0;
			}
		};
	});

	function renderExpandableSeries(key: string): boolean {
		const chartInstance = instance;
		if (!chartInstance || chartInstance.isDisposed()) return false;
		const built = buildBarOption(barOptionContext());
		const all = Array.isArray(built.series) ? built.series : built.series ? [built.series] : [];
		const series = all.find((entry) => String((entry as { id?: unknown }).id ?? '') === key);
		if (!series) return false;
		chartInstance.setOption({ series: [series] }, { silent: true, lazyUpdate: true });
		return true;
	}

	function snapExpandableToReducedMotion(key: string) {
		expand.progress.clear();
		if (expand.hovered !== null) expand.progress.set(expand.hovered, 1);
		renderExpandableSeries(key);
	}

	function animateExpandable(key: string, index: number | null) {
		if (expand.hovered === index && expand.key === key) return;
		expand.key = key;
		expand.hovered = index;
		if (prefersReducedMotion.current) {
			snapExpandableToReducedMotion(key);
			return;
		}
		if (index !== null && !expand.progress.has(index)) expand.progress.set(index, 0.12);
		if (expandFrame) return;
		let previous = performance.now();
		const tick = (now: number) => {
			const chartInstance = instance;
			if (!chartInstance || chartInstance.isDisposed()) {
				expandFrame = 0;
				return;
			}
			if (prefersReducedMotion.current) {
				snapExpandableToReducedMotion(key);
				expandFrame = 0;
				return;
			}
			const factor = 1 - Math.exp(-Math.min(64, now - previous) / 70);
			previous = now;
			let moving = false;
			for (const [datumIndex, current] of expand.progress) {
				const target = datumIndex === expand.hovered ? 1 : 0.12;
				const next = current + (target - current) * factor;
				if (Math.abs(target - next) < 0.004) {
					if (target === 0.12) expand.progress.delete(datumIndex);
					else expand.progress.set(datumIndex, target);
				} else {
					expand.progress.set(datumIndex, next);
					moving = true;
				}
			}
			if (!renderExpandableSeries(key)) {
				expandFrame = 0;
				return;
			}
			expandFrame = moving ? requestAnimationFrame(tick) : 0;
		};
		expandFrame = requestAnimationFrame(tick);
	}

	$effect(() => {
		const chartInstance = instance;
		const index = tooltip?.defaultIndex;
		if (!chartInstance || isLoading || index === undefined) return;
		const timer = window.setTimeout(() => {
			if (!chartInstance.isDisposed()) {
				chartInstance.dispatchAction({ type: 'showTip', seriesIndex: 0, dataIndex: index });
			}
		}, 300);
		return () => window.clearTimeout(timer);
	});

	function toggleSelection(key: string) {
		if (!bars.some((bar) => bar.dataKey === key && bar.isClickable) && !legend?.isClickable) {
			return;
		}
		selectedDataKey = selectedDataKey === key ? null : key;
		onSelectionChange?.(selectedDataKey);
	}

	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;
	}

	function eventDataIndex(params: unknown): number | null {
		if (!params || typeof params !== 'object') return null;
		const index = (params as { dataIndex?: unknown }).dataIndex;
		return typeof index === 'number' && Number.isInteger(index) ? index : null;
	}

	const events = $derived({
		click: (params: unknown) => {
			const key = eventSeriesKey(params);
			if (key) toggleSelection(key);
		},
		mouseover: (params: unknown) => {
			const index = eventDataIndex(params);
			onDataHover?.(index === null || !data[index] ? null : { index, row: data[index] });
			if (selectedDataKey !== null) return;
			hoveredDataKey = eventSeriesKey(params);
		},
		mouseout: () => {
			hoveredDataKey = null;
			onDataHover?.(null);
		},
		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;
		if (!chartInstance || !isLoading) return;
		if (prefersReducedMotion.current) {
			chartInstance.setOption(
				{
					series: [
						{
							id: '__loading',
							data: loadingData,
							itemStyle: { color: withAlpha(resolved.tokens.foreground, 0.22) }
						}
					]
				},
				{ 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;
			if (phase < lastPhase) loadingData = createBarLoadingData(loadingBars);
			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 = [0, center - 0.2, center, center + 0.2, 1]
				.filter((offset) => offset >= 0 && offset <= 1)
				.sort((left, right) => left - right)
				.filter((offset, index, all) => index === 0 || offset - (all[index - 1] ?? -1) > 0.0001)
				.map((offset) => {
					const distance = Math.abs(offset - center);
					const alpha = distance >= 0.2 ? 0 : 0.22 * Math.sin(((1 - distance / 0.2) * Math.PI) / 2);
					return { offset, color: withAlpha(color, alpha) };
				});
			chartInstance.setOption(
				{
					series: [
						{
							id: '__loading',
							itemStyle: {
								color: new echarts.graphic.LinearGradient(0, 0, width, width, stops, true)
							}
						}
					]
				},
				{ 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?.()}
	{#if bars.length > 0}
		<EChartsHost {option} {renderer} {events} bind:instance />
	{/if}
	{#if !legend?.isClickable}
		<SelectableSeriesControls
			items={selectableSeries}
			selectedKey={selectedDataKey}
			onToggle={toggleSelection}
		/>
	{/if}
	{#if brush && !isLoading && data.length > 0 && layout === 'vertical'}
		<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-bar-chart/bar.svelte`

```svelte
<script lang="ts">
	import { useEChartsBarChart } from './bar-chart-context.svelte.js';
	import type { DitherVariant } from '$lib/components/evilcharts/ui/echarts-dither/index.js';
	import { type BarAnimationType, type BarVariant } from './types.js';
	let {
		dataKey,
		variant = 'default',
		radius,
		animationType,
		isClickable = false,
		enableHoverHighlight = false,
		glowing = false,
		bufferBar = false,
		ditherVariant
	}: {
		dataKey: string;
		variant?: BarVariant;
		radius?: number;
		animationType?: BarAnimationType;
		isClickable?: boolean;
		enableHoverHighlight?: boolean;
		glowing?: boolean;
		bufferBar?: boolean;
		ditherVariant?: DitherVariant;
	} = $props();
	const token = $props.id();
	const chart = useEChartsBarChart();
	$effect(() =>
		chart.bars.register(token, () => ({
			dataKey,
			variant,
			radius,
			animationType,
			isClickable,
			enableHoverHighlight,
			glowing,
			bufferBar,
			ditherVariant
		}))
	);
</script>
```

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

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

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

```ts
import Root from './bar-chart.svelte';
import Bar from './bar.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 EChartsBarChart: RootComponent & {
	Bar: typeof Bar;
	XAxis: typeof XAxis;
	YAxis: typeof YAxis;
	Grid: typeof Grid;
	Tooltip: typeof Tooltip;
	Legend: typeof Legend;
	Brush: typeof Brush;
} = Object.assign(Root, { Bar, 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 { 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 { BarAnimationType, BarHoverDatum, BarLayout, BarVariant, StackType } from './types.js';
```

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

```ts
import type { BarSeriesOption, CustomSeriesOption } from 'echarts/charts';
import type {
	DataZoomComponentOption,
	GridComponentOption,
	MarkLineComponentOption,
	TooltipComponentOption
} from 'echarts/components';
import type { ComposeOption, EChartsType } from 'echarts/core';
import * as echarts from 'echarts/core';
import {
	flattenColor,
	getColorsCount,
	withAlpha,
	type ChartConfig,
	type ResolvedColors
} from '$lib/components/evilcharts/ui/echarts-chart/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 { sampleGradient } from '$lib/components/evilcharts/ui/echarts-dot/index.js';
import {
	tooltipBaseOption,
	tooltipIndicatorHtml,
	tooltipRow,
	tooltipShell
} from '$lib/components/evilcharts/ui/echarts-tooltip/index.js';
import type {
	AxisRegistration,
	BarLayout,
	BarRegistration,
	BrushRegistration,
	LegendRegistration,
	StackType,
	TooltipRegistration
} from './types.js';

export type EChartsBarOption = ComposeOption<
	| BarSeriesOption
	| CustomSeriesOption
	| GridComponentOption
	| TooltipComponentOption
	| DataZoomComponentOption
	| MarkLineComponentOption
>;
type ArrayItem<T> = T extends readonly (infer Item)[] ? Item : T;
type XAxisOption = ArrayItem<NonNullable<EChartsBarOption['xAxis']>>;
type YAxisOption = ArrayItem<NonNullable<EChartsBarOption['yAxis']>>;
export type BarOptionContext = {
	data: Record<string, unknown>[];
	config: ChartConfig;
	bars: BarRegistration[];
	xDataKey?: string;
	stackType: StackType;
	layout: BarLayout;
	barRadius: number;
	barGap?: number;
	barCategoryGap?: number;
	selectedDataKey: string | null;
	enableMaxValueHighlight: boolean;
	referenceLine?: number | null;
	referenceLineFormatter?: (value: number) => string;
	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;
	valuePxPerUnit?: number | null;
	barWidthPx?: number | null;
	expand?: { key: string | null; hovered: number | null; progress: ReadonlyMap<number, number> };
};
const ditherBloomBlur = (style: RenderStyle, bloom?: DitherBloom) =>
	style !== 'dither' || bloom === 'off' || bloom === undefined
		? 0
		: bloom === 'aura'
			? 14
			: bloom === 'high'
				? 8
				: 4;

const GRAY = 'rgba(120, 120, 120, 1)';
const BLOCK_SIZE = 8;
const BLOCK_GAP = 4;
const BLOCK_TRACK_OPACITY = 0.22;
const STACK_SEGMENT_GAP = 4;
const STRIPPED_CAP_HEIGHT = 4;
const STRIPPED_BODY_ALPHA = 0.2;
const STRIPPED_CAP_MAX_FRACTION = 0.85;
const STRIPPED_FALLBACK_FRACTION = 0.12;
const EXPAND_COLLAPSED = 0.12;
const MAX_HIGHLIGHT_DIM = 0.16;
const labelFor = (config: ChartConfig, key: string) =>
	typeof config[key]?.label === 'string' ? (config[key].label as string) : key;
function ditherPlotBounds(c: BarOptionContext) {
	const top = c.legend?.verticalAlign === 'top' ? 42 : 16;
	const showBrush = c.layout === 'vertical' && 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
	};
}
function categoryKey(c: BarOptionContext) {
	if (c.xDataKey) return c.xDataKey;
	const axis = c.layout === 'vertical' ? c.xAxis : c.yAxis;
	if (axis?.dataKey) return axis.dataKey;
	const keys = new Set(c.bars.map((bar) => bar.dataKey));
	return Object.keys(c.data[0] ?? {}).find((key) => !keys.has(key));
}
function categories(c: BarOptionContext) {
	const key = categoryKey(c);
	return c.data.map((row, i) => String((key ? row[key] : undefined) ?? i));
}
function rawValues(c: BarOptionContext, key: string): number[] {
	return c.data.map((row) =>
		typeof row[key] === 'number' && Number.isFinite(row[key]) ? (row[key] as number) : 0
	);
}
function values(c: BarOptionContext, key: string) {
	const raw = rawValues(c, key);
	if (c.stackType !== 'percent') return raw;
	return raw.map((value, i) => {
		const total = c.bars.reduce((sum, bar) => sum + rawValues(c, bar.dataKey)[i], 0);
		return total ? value / total : 0;
	});
}
function axisBase(c: BarOptionContext, axis: AxisRegistration | undefined, isCategory: boolean) {
	const { mutedForeground, border, background } = c.resolved.tokens;
	return {
		type: isCategory ? ('category' as const) : ('value' as const),
		data: isCategory ? (c.isLoading ? c.loadingData.map((_, i) => i) : categories(c)) : undefined,
		axisLine: { show: false },
		axisTick: {
			show: !c.isLoading && Boolean(axis) && !axis?.hideDots,
			length: 0.5,
			lineStyle: { color: flattenColor(border, background), width: 3, cap: 'round' }
		},
		axisLabel: {
			show: !c.isLoading && Boolean(axis),
			color: mutedForeground,
			fontSize: 10,
			margin: 8,
			formatter:
				!isCategory && c.stackType === 'percent'
					? (value: number) => `${Math.round(value * 100)}%`
					: axis?.tickFormatter
		},
		splitLine: {
			show: !isCategory && c.showGrid && !c.isLoading,
			lineStyle: { color: border, type: [3, 3], width: 1 }
		},
		name: c.isLoading ? undefined : axis?.label,
		nameLocation: 'middle' as const,
		nameGap: isCategory
			? c.layout === 'horizontal'
				? 38
				: 30
			: c.layout === 'horizontal'
				? 30
				: 38,
		nameTextStyle: { color: mutedForeground, fontSize: 10 },
		max: !isCategory && c.stackType === 'percent' ? 1 : undefined
	};
}
function axes(c: BarOptionContext): { xAxis: XAxisOption; yAxis: YAxisOption } {
	if (c.layout === 'horizontal')
		return {
			xAxis: axisBase(c, c.xAxis, false) as XAxisOption,
			yAxis: { ...axisBase(c, c.yAxis, true), inverse: true } as YAxisOption
		};
	return {
		xAxis: axisBase(c, c.xAxis, true) as XAxisOption,
		yAxis: axisBase(c, c.yAxis, false) as YAxisOption
	};
}
type ImagePattern = {
	image: HTMLCanvasElement;
	repeat: 'repeat';
	rotation?: number;
	scaleX?: number;
	scaleY?: number;
};

function solidVerticalPaint(slots: string[], alpha = 1) {
	if (slots.length <= 1) {
		const base = slots[0] ?? GRAY;
		return alpha === 1 ? base : withAlpha(base, alpha);
	}
	return new echarts.graphic.LinearGradient(
		0,
		0,
		0,
		1,
		slots.map((color, index) => ({
			offset: index / (slots.length - 1),
			color: withAlpha(color, alpha)
		}))
	);
}

function verticalFadePaint(slots: string[]) {
	const offsets = [0, 0.2, 0.45, 0.7, 0.9, 1];
	return new echarts.graphic.LinearGradient(
		0,
		0,
		0,
		1,
		offsets.map((offset) => ({
			offset,
			color: withAlpha(
				sampleGradient(slots, offset),
				offset <= 0.2 ? 1 : offset >= 0.9 ? 0 : 1 - (offset - 0.2) / 0.7
			)
		}))
	);
}

function duotoneSplitPaint(
	base: string,
	firstAlpha: number,
	secondAlpha: number,
	horizontal: boolean
) {
	const stops = [
		{ offset: 0, color: withAlpha(base, firstAlpha) },
		{ offset: 0.5, color: withAlpha(base, firstAlpha) },
		{ offset: 0.5, color: withAlpha(base, secondAlpha) },
		{ offset: 1, color: withAlpha(base, secondAlpha) }
	];
	return horizontal
		? new echarts.graphic.LinearGradient(0, 0, 0, 1, stops)
		: new echarts.graphic.LinearGradient(1, 0, 0, 0, stops);
}

function strippedDatumPaint(slots: string[], horizontal: boolean, capFraction: number) {
	const fraction = Math.min(Math.max(capFraction, 0), 1);
	const cap = withAlpha(sampleGradient(slots, 0), 1);
	const stops = [
		{ offset: 0, color: cap },
		{ offset: fraction, color: cap },
		{ offset: fraction, color: withAlpha(sampleGradient(slots, fraction), STRIPPED_BODY_ALPHA) },
		{ offset: 1, color: withAlpha(sampleGradient(slots, 1), STRIPPED_BODY_ALPHA) }
	];
	return horizontal
		? new echarts.graphic.LinearGradient(1, 0, 0, 0, stops)
		: new echarts.graphic.LinearGradient(0, 0, 0, 1, stops);
}

export function strippedCapFraction(value: number, valuePxPerUnit: number | null): number {
	if (valuePxPerUnit === null) return STRIPPED_FALLBACK_FRACTION;
	const barPixels = Math.abs(value) * valuePxPerUnit;
	if (!(barPixels > 0)) return STRIPPED_FALLBACK_FRACTION;
	return Math.min(STRIPPED_CAP_HEIGHT / barPixels, STRIPPED_CAP_MAX_FRACTION);
}

export function measureBarValueScale(chart: EChartsType, horizontal: boolean): number | null {
	try {
		const finder = horizontal ? { xAxisIndex: 0 } : { yAxisIndex: 0 };
		const start = chart.convertToPixel(finder, 0);
		const end = chart.convertToPixel(finder, 1);
		if (typeof start !== 'number' || typeof end !== 'number') return null;
		const delta = Math.abs(end - start);
		return Number.isFinite(delta) && delta > 0 ? delta : null;
	} catch {
		return null;
	}
}

export function measureBarWidth(
	chart: EChartsType,
	horizontal: boolean,
	barCategoryGap?: number
): number | null {
	try {
		const finder = horizontal ? { yAxisIndex: 0 } : { xAxisIndex: 0 };
		const start = chart.convertToPixel(finder, 0);
		const end = chart.convertToPixel(finder, 1);
		if (typeof start !== 'number' || typeof end !== 'number') return null;
		const pitch = Math.abs(end - start);
		if (!Number.isFinite(pitch) || pitch <= 0) return null;
		const width = barCategoryGap === undefined ? pitch * 0.8 : pitch - barCategoryGap;
		return width > 1 ? width : null;
	} catch {
		return null;
	}
}

function patternFill(
	kind: 'hatched' | 'buffer' | 'blocks',
	color: string,
	blockSize = BLOCK_SIZE
): 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 === 'blocks') {
		size(1, blockSize + BLOCK_GAP);
		context.fillStyle = color;
		context.fillRect(0, 0, 1, blockSize);
		return pattern();
	}
	if (kind === 'hatched') {
		size(5, 5);
		context.fillStyle = withAlpha(color, 0.3);
		context.fillRect(0, 0, 5, 5);
		context.fillStyle = color;
		context.fillRect(0, 0, 1.5, 5);
		return pattern(-Math.PI / 4);
	}
	size(5, 5);
	context.fillStyle = color;
	context.fillRect(0, 0, 1, 5);
	return pattern(-Math.PI / 4);
}

export function createExpandablePaint(slots: string[], openness: number) {
	const base = slots[0] ?? GRAY;
	const half = Math.max(0, Math.min(1, openness)) / 2;
	const left = 0.5 - half;
	const right = 0.5 + half;
	const clear = withAlpha(base, 0);
	return new echarts.graphic.LinearGradient(0, 0, 1, 0, [
		{ offset: 0, color: clear },
		{ offset: left, color: clear },
		{ offset: left, color: base },
		{ offset: right, color: base },
		{ offset: right, color: clear },
		{ offset: 1, color: clear }
	]);
}
function barBorderRadius(radius: number, variant: BarRegistration['variant'], horizontal: boolean) {
	if (variant === 'blocks' || variant === 'expandable') return 0;
	if (variant !== 'stripped') return radius;
	return horizontal ? [0, radius, radius, 0] : [radius, radius, 0, 0];
}
function fill(c: BarOptionContext, bar: BarRegistration) {
	const slots = c.resolved.series[bar.dataKey] ?? [c.resolved.tokens.foreground];
	const color = slots[0] ?? c.resolved.tokens.foreground;
	if (c.renderStyle === 'dither')
		return createDitherPattern(
			slots,
			bar.ditherVariant ?? c.ditherVariant,
			c.ditherCellSize,
			1,
			ditherPlotBounds(c)
		);
	if (bar.variant === 'gradient') return verticalFadePaint(slots);
	if (bar.variant === 'duotone') return duotoneSplitPaint(color, 0.4, 1, c.layout === 'horizontal');
	if (bar.variant === 'duotone-reverse')
		return duotoneSplitPaint(color, 1, 0.4, c.layout === 'horizontal');
	if (bar.variant === 'hatched') return patternFill('hatched', color) ?? solidVerticalPaint(slots);
	if (bar.variant === 'blocks')
		return patternFill('blocks', color, c.barWidthPx ?? BLOCK_SIZE) ?? solidVerticalPaint(slots);
	if (bar.variant === 'stripped')
		return strippedDatumPaint(slots, c.layout === 'horizontal', STRIPPED_FALLBACK_FRACTION);
	if (bar.variant === 'expandable') return createExpandablePaint(slots, EXPAND_COLLAPSED);
	return solidVerticalPaint(slots);
}
function tooltip(c: BarOptionContext): TooltipComponentOption {
	const slot = c.tooltip;
	return {
		...tooltipBaseOption({
			present: Boolean(slot) && !c.isLoading,
			cursor: false,
			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;
				axisValueLabel?: string;
				value?: unknown;
				data?: unknown;
			}>;
			const body = params
				.filter((p) => p.seriesId && !p.seriesId.startsWith('__'))
				.map((p) => {
					const key = p.seriesId as string;
					const rawValue =
						typeof p.data === 'object' && p.data && 'value' in p.data
							? (p.data as { value: unknown }).value
							: p.value;
					const value = Array.isArray(rawValue) ? rawValue.at(-1) : rawValue;
					return tooltipRow({
						indicatorHtml: tooltipIndicatorHtml(key, getColorsCount(c.config[key] ?? {})),
						labelText: labelFor(c.config, key),
						valueText: Number(value).toLocaleString(),
						dimmed: c.selectedDataKey && c.selectedDataKey !== key ? ' opacity-30' : ''
					});
				})
				.join('');
			return tooltipShell({
				label: params[0]?.axisValueLabel ?? '',
				body,
				roundness: slot?.roundness ?? 'lg',
				variant: slot?.variant ?? 'default'
			});
		}
	};
}

function isDarkBackground(color: string): boolean {
	const channels = color
		.match(/[\d.]+/g)
		?.slice(0, 3)
		.map(Number);
	if (!channels || channels.length < 3) return false;
	return channels[0] * 0.2126 + channels[1] * 0.7152 + channels[2] * 0.0722 < 128;
}

function isometricSeries(
	c: BarOptionContext,
	bar: BarRegistration,
	maxIndex: number | null
): CustomSeriesOption {
	const slots = c.resolved.series[bar.dataKey] ?? [c.resolved.tokens.foreground];
	const base = slots[0] ?? c.resolved.tokens.foreground;
	const dark = isDarkBackground(c.resolved.tokens.background);
	const accent = dark ? '#15803d' : '#22c55e';
	const valuesForBar = values(c, bar.dataKey);

	return {
		id: bar.dataKey,
		name: labelFor(c.config, bar.dataKey),
		type: 'custom',
		coordinateSystem: 'cartesian2d',
		encode: { x: 0, y: 1 },
		data: valuesForBar.map((value, index) => [index, value]),
		cursor: bar.isClickable ? 'pointer' : 'default',
		renderItem(params, api) {
			const index = params.dataIndex;
			const value = Number(api.value(1));
			const top = api.coord([api.value(0), value]);
			const baseline = api.coord([api.value(0), 0]);
			const categorySize = api.size?.([1, 0]) ?? [0, 0];
			const width = Math.max(
				4,
				Number(Array.isArray(categorySize) ? categorySize[0] : categorySize) * 0.55
			);
			const depth = Math.min(10, Math.max(5, width * 0.24));
			const left = Number(top[0]) - width / 2;
			const right = Number(top[0]) + width / 2;
			const topY = Number(top[1]);
			const baseY = Number(baseline[1]);
			const height = Math.max(0, baseY - topY);
			const highlight = index === maxIndex;
			const color = highlight ? accent : base;
			const front = patternFill('hatched', color) ?? color;

			return {
				type: 'group',
				children: [
					{
						type: 'polygon',
						shape: {
							points: [
								[right, topY],
								[right + depth, topY - depth],
								[right + depth, baseY - depth],
								[right, baseY]
							]
						},
						style: { fill: withAlpha(color, 0.55) }
					},
					{
						type: 'polygon',
						shape: {
							points: [
								[left, topY],
								[right, topY],
								[right + depth, topY - depth],
								[left + depth, topY - depth]
							]
						},
						style: { fill: withAlpha(color, 0.78) }
					},
					{
						type: 'rect',
						shape: { x: left, y: topY, width, height },
						style: { fill: front }
					}
				]
			};
		},
		animation: c.animation && !c.reducedMotion,
		animationDuration: 700,
		animationDelay: (index) => index * 80,
		animationEasing: 'cubicOut'
	};
}

function referenceMarkLine(c: BarOptionContext) {
	if (c.referenceLine === null || c.referenceLine === undefined) return undefined;
	return {
		symbol: 'none',
		silent: true,
		lineStyle: {
			color: withAlpha(c.resolved.tokens.mutedForeground, 0.6),
			type: 'dashed' as const,
			width: 1
		},
		label: {
			show: true,
			position: c.layout === 'horizontal' ? ('end' as const) : ('insideEndTop' as const),
			formatter: c.referenceLineFormatter?.(c.referenceLine) ?? String(c.referenceLine),
			color: c.resolved.tokens.foreground,
			backgroundColor: c.resolved.tokens.background,
			borderColor: c.resolved.tokens.border,
			borderWidth: 1,
			borderRadius: 4,
			padding: [2, 5],
			fontFamily: 'var(--font-mono, monospace)',
			fontSize: 10
		},
		data: [c.layout === 'horizontal' ? { xAxis: c.referenceLine } : { yAxis: c.referenceLine }]
	};
}

function series(c: BarOptionContext): Array<BarSeriesOption | CustomSeriesOption> {
	if (c.isLoading) {
		return [
			{
				id: '__loading',
				type: 'bar',
				data: c.loadingData,
				barCategoryGap: '30%',
				silent: true,
				itemStyle: {
					color: withAlpha(c.resolved.tokens.foreground, 0),
					borderRadius: barBorderRadius(c.barRadius, 'default', c.layout === 'horizontal')
				},
				z: 1,
				animation: false
			}
		];
	}

	const totals = c.data.map((_, index) =>
		c.bars.reduce((sum, bar) => sum + rawValues(c, bar.dataKey)[index], 0)
	);
	const maxIndex = c.enableMaxValueHighlight
		? totals.reduce((best, total, index) => (total > (totals[best] ?? -Infinity) ? index : best), 0)
		: null;
	const horizontal = c.layout === 'horizontal';
	const built = c.bars.map((bar, seriesIndex): BarSeriesOption | CustomSeriesOption => {
		if (bar.variant === 'isometric') return isometricSeries(c, bar, maxIndex);
		const slots = c.resolved.series[bar.dataKey] ?? [GRAY];
		const baseColor = slots[0] ?? GRAY;
		const opacity = c.selectedDataKey === null || c.selectedDataKey === bar.dataKey ? 1 : 0.3;
		const paint = fill(c, bar);
		const bloomBlur = ditherBloomBlur(c.renderStyle, c.bloom);
		const bloomColor = bloomBlur ? withAlpha(baseColor, 0.55) : undefined;
		const vals = values(c, bar.dataKey);
		const radius = barBorderRadius(bar.radius ?? c.barRadius, bar.variant, horizontal);
		const expandedIndex = c.expand?.key === bar.dataKey ? c.expand.hovered : null;
		const openness = (index: number) =>
			c.expand?.key === bar.dataKey
				? (c.expand.progress.get(index) ?? EXPAND_COLLAPSED)
				: EXPAND_COLLAPSED;
		const datumStyle =
			bar.variant === 'stripped' ||
			bar.variant === 'expandable' ||
			bar.bufferBar ||
			bar.glowing ||
			maxIndex !== null ||
			opacity < 1;
		const dataPoints = datumStyle
			? vals.map((value, index) => {
					const buffer = bar.bufferBar && index === vals.length - 1;
					const muted = maxIndex !== null && index !== maxIndex;
					let color = paint;
					if (bar.variant === 'stripped') {
						color = strippedDatumPaint(
							slots,
							horizontal,
							strippedCapFraction(value, c.valuePxPerUnit ?? null)
						);
					}
					if (bar.variant === 'expandable') color = createExpandablePaint(slots, openness(index));
					if (buffer) color = patternFill('buffer', baseColor) ?? 'transparent';
					if (muted) color = withAlpha(c.resolved.tokens.mutedForeground, MAX_HIGHLIGHT_DIM);
					const glow = bar.glowing || index === maxIndex;
					return {
						value,
						...(bar.variant === 'expandable' ? { label: { show: index === expandedIndex } } : {}),
						itemStyle: {
							color,
							borderColor: buffer ? baseColor : undefined,
							borderWidth: buffer ? 1 : 0,
							borderRadius: radius,
							opacity,
							shadowBlur: glow ? 18 : bloomBlur,
							shadowColor: glow
								? withAlpha(
										sampleGradient(slots, vals.length > 1 ? index / (vals.length - 1) : 0),
										0.65
									)
								: bloomColor
						}
					};
				})
			: vals;
		return {
			id: bar.dataKey,
			name: labelFor(c.config, bar.dataKey),
			type: 'bar',
			data: dataPoints,
			stack: c.stackType === 'default' ? undefined : '__bar-stack',
			barGap: c.barGap,
			barCategoryGap: c.barCategoryGap,
			cursor: bar.isClickable ? 'pointer' : 'default',
			z: c.selectedDataKey === bar.dataKey ? 3 : c.selectedDataKey === null ? 2 : 1,
			label:
				bar.variant === 'expandable'
					? {
							show: false,
							position: 'top',
							color: c.resolved.tokens.foreground,
							fontFamily: 'var(--font-mono, monospace)',
							fontSize: 11
						}
					: undefined,
			showBackground: bar.variant === 'blocks',
			backgroundStyle:
				bar.variant === 'blocks'
					? {
							color:
								patternFill(
									'blocks',
									withAlpha(c.resolved.tokens.mutedForeground, BLOCK_TRACK_OPACITY),
									c.barWidthPx ?? BLOCK_SIZE
								) ?? withAlpha(c.resolved.tokens.mutedForeground, BLOCK_TRACK_OPACITY),
							borderRadius: radius
						}
					: undefined,
			itemStyle: {
				color: paint,
				borderRadius: radius,
				opacity,
				shadowBlur: bloomBlur,
				shadowColor: bloomColor
			},
			emphasis:
				bar.enableHoverHighlight && c.selectedDataKey === null
					? { focus: 'self', blurScope: 'coordinateSystem' }
					: { disabled: true },
			blur:
				bar.enableHoverHighlight && c.selectedDataKey === null
					? { itemStyle: { opacity: 0.3 } }
					: undefined,
			animation:
				c.animation && (bar.animationType ?? c.animationType) !== 'none' && !c.reducedMotion,
			animationDuration: 500,
			animationEasing: 'cubicOut',
			markLine: seriesIndex === 0 ? referenceMarkLine(c) : undefined,
			animationDelay: (index) => {
				const type = bar.animationType ?? c.animationType;
				const last = Math.max(0, c.data.length - 1);
				const center = last / 2;
				const rank =
					type === 'right-to-left'
						? last - index
						: type === 'center-out'
							? Math.abs(index - center)
							: type === 'edges-in'
								? center - Math.abs(index - center)
								: index;
				return rank * 50;
			}
		};
	});
	if (built.some((entry) => entry.type === 'custom')) return built;

	const gapUnits =
		c.stackType !== 'default' && built.length > 1 && c.valuePxPerUnit
			? STACK_SEGMENT_GAP / c.valuePxPerUnit
			: 0;
	if (!gapUnits) return built;
	return (built as BarSeriesOption[]).flatMap((entry, index) =>
		index === built.length - 1
			? [entry]
			: [
					entry,
					{
						id: `__stackgap-${index}`,
						type: 'bar' as const,
						stack: '__bar-stack',
						data: c.data.map(() => gapUnits),
						itemStyle: { color: 'transparent' },
						silent: true,
						tooltip: { show: false },
						legendHoverLink: false,
						emphasis: { disabled: true },
						animation: false,
						z: 1
					}
				]
	);
}
export function buildBarOption(c: BarOptionContext): EChartsBarOption {
	const allowBrush = c.layout === 'vertical' && 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 +
			(allowBrush ? height + 30 + (c.xAxis?.label ? 22 : 0) : 0) +
			(c.legend?.verticalAlign === 'bottom' ? 34 : 0)
	};
	const builtAxes = axes(c);
	const mainSeries = series(c);
	if (!allowBrush)
		return {
			animation: c.animation && !c.reducedMotion,
			animationDuration: 500,
			animationDurationUpdate: 0,
			aria: { enabled: true },
			grid: main,
			xAxis: builtAxes.xAxis,
			yAxis: builtAxes.yAxis,
			tooltip: tooltip(c),
			series: mainSeries
		};
	const mini = c.bars.map((bar) => ({
		id: `__mini-${bar.dataKey}`,
		type: 'bar' as const,
		xAxisIndex: 1,
		yAxisIndex: 1,
		data: rawValues(c, bar.dataKey),
		stack: c.stackType === 'default' ? undefined : '__mini-total',
		silent: true,
		barCategoryGap: '20%',
		emphasis: { disabled: true },
		tooltip: { show: false },
		itemStyle: {
			color: c.resolved.series[bar.dataKey]?.[0] ?? c.resolved.tokens.foreground,
			opacity: 0.5 * (c.selectedDataKey !== null && c.selectedDataKey !== bar.dataKey ? 0.3 : 1),
			borderRadius: 1
		},
		z: 0,
		animation: false
	}));
	return {
		animation: c.animation && !c.reducedMotion,
		animationDuration: 500,
		animationDurationUpdate: 0,
		aria: { enabled: true },
		grid: [main, { left: 8, right: 8, bottom, height, outerBoundsMode: 'none' }],
		xAxis: [builtAxes.xAxis, { type: 'category', gridIndex: 1, data: categories(c), 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 createBarLoadingData(count: number): number[] {
	const rows: number[] = [];
	let value = 40 + Math.random() * 25;
	for (let index = 0; index < Math.max(0, count); index += 1) {
		value = Math.min(85, Math.max(20, value + (Math.random() - 0.5) * 30));
		rows.push(Math.round(value));
	}
	return rows;
}
```

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

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

export const DEFAULT_BAR_RADIUS = 2;
export type BarVariant =
	| 'default'
	| 'hatched'
	| 'duotone'
	| 'duotone-reverse'
	| 'gradient'
	| 'stripped'
	| 'blocks'
	| 'expandable'
	| 'isometric';
export type StackType = 'default' | 'stacked' | 'percent';
export type BarLayout = 'vertical' | 'horizontal';
export type BarAnimationType =
	'none' | 'left-to-right' | 'right-to-left' | 'center-out' | 'edges-in';
export type BarHoverDatum = {
	index: number;
	row: Record<string, unknown>;
};
export type BarRegistration = {
	dataKey: string;
	variant: BarVariant;
	radius?: number;
	animationType?: BarAnimationType;
	isClickable: boolean;
	enableHoverHighlight: boolean;
	glowing: boolean;
	bufferBar: boolean;
	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-bar-chart/x-axis.svelte`

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

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

```svelte
<script lang="ts">
	import { useEChartsBarChart } from './bar-chart-context.svelte.js';
	let {
		dataKey,
		tickFormatter,
		label,
		hideDots = false
	}: {
		dataKey?: string;
		tickFormatter?: (value: string | number, index: number) => string;
		label?: string;
		hideDots?: boolean;
	} = $props();
	const token = $props.id();
	const chart = useEChartsBarChart();
	$effect(() => chart.yAxes.register(token, () => ({ dataKey, tickFormatter, 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 bar chart is composable, sharing the LayerChart sibling's API shape. `<EChartsBarChart>` is the container, and every part hangs off it as a compound member — `<EChartsBarChart.Grid>`, `<EChartsBarChart.XAxis>`, `<EChartsBarChart.YAxis>`, `<EChartsBarChart.Legend>`, `<EChartsBarChart.Tooltip>`, and one or more `<EChartsBarChart.Bar>` — so a single import gives you the whole chart. Each `<Bar>` carries its own `variant`, `radius`, `glowing`, `bufferBar`, and `isClickable`, so one chart can mix fill styles and make only some series interactive.

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

```svelte
<EChartsBarChart {data} config={chartConfig} stackType="default">
	<EChartsBarChart.Grid />
	<EChartsBarChart.XAxis dataKey="month" />
	<EChartsBarChart.Legend isClickable />
	<EChartsBarChart.Tooltip />
	<EChartsBarChart.Bar dataKey="desktop" variant="default" isClickable />
	<EChartsBarChart.Bar dataKey="mobile" variant="hatched" isClickable />
</EChartsBarChart>
```

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 a few small departures from the LayerChart sibling: the `duotone` split is a single ECharts gradient, the `stripped` cap is a fixed-height band derived from the measured axis scale, and the zoom brush is a themed mini chart driven by ECharts' native `dataZoom`. The `hatched`, `blocks`, and buffer fills use offscreen canvas tiles; with `renderer="svg"`, ECharts may embed those tiles as raster images inside the SVG.




### 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 { EChartsBarChart } from '$lib/components/evilcharts/charts/echarts-bar-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>

<EChartsBarChart
	renderer="svg"
	{data}
	config={chartConfig}
	class="h-full w-full p-4"
	xDataKey="month"
>
	<EChartsBarChart.Grid />
	<EChartsBarChart.XAxis dataKey="month" tickFormatter={(value) => String(value).substring(0, 3)} />
	<EChartsBarChart.Brush formatLabel={(value) => String(value).substring(0, 3)} />
	<EChartsBarChart.Legend isClickable />
	<EChartsBarChart.Tooltip />
	<EChartsBarChart.Bar dataKey="desktop" variant="default" isClickable />
	<EChartsBarChart.Bar dataKey="mobile" variant="default" isClickable />
</EChartsBarChart>
```

### Interactive Selection

Add `isClickable` to any `<Bar>` (and to `<Legend>`) to make those series selectable. Use the `onSelectionChange` callback on `<EChartsBarChart>` to handle selection events:

```svelte
<EChartsBarChart
	{data}
	config={chartConfig}
	onSelectionChange={(selectedDataKey) => {
		if (selectedDataKey) {
			console.log('Selected:', selectedDataKey);
		} else {
			console.log('Deselected');
		}
	}}
>
	<EChartsBarChart.XAxis dataKey="month" />
	<EChartsBarChart.Legend isClickable />
	<EChartsBarChart.Tooltip />
	<EChartsBarChart.Bar dataKey="desktop" variant="default" isClickable />
	<EChartsBarChart.Bar dataKey="mobile" variant="default" isClickable />
</EChartsBarChart>
```

### Loading State

### isLoading='true'

```svelte
<script lang="ts">
	import { EChartsBarChart } from '$lib/components/evilcharts/charts/echarts-bar-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:6] -->
<EChartsBarChart data={[]} config={chartConfig} class="h-full w-full p-4" isLoading={true}>
	<EChartsBarChart.Grid />
	<EChartsBarChart.XAxis dataKey="month" tickFormatter={(value) => String(value).substring(0, 3)} />
	<EChartsBarChart.Legend />
	<EChartsBarChart.Tooltip />
	<EChartsBarChart.Bar dataKey="desktop" variant="default" />
	<EChartsBarChart.Bar dataKey="mobile" variant="default" />
</EChartsBarChart>
```
> 
  

Pass `isLoading` to show an animated skeleton of gray bars with a shimmer while data loads, and `loadingBars` to set how many bars the skeleton draws.




### Buffer Bar

### <Bar bufferBar />

```svelte
<script lang="ts">
	import { EChartsBarChart } from '$lib/components/evilcharts/charts/echarts-bar-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: 503, mobile: 215 },
		{ month: 'December', desktop: 971, mobile: 749 }
	];

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

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

With `bufferBar` set, a <code>&lt;Bar&gt;</code>'s last data point renders with a hatched (diagonal lines) pattern and a series-colored outline while the rest stay solid — handy for flagging projected, estimated, or incomplete data at the end of a series.




## Examples

Examples of the bar chart with different `variants`. Each `<Bar>` sets its own `variant`; the chart-wide `stackType` and `layout` shape the rest.

### Hover Highlight

### <Bar enableHoverHighlight />

```svelte
<script lang="ts">
	import { EChartsBarChart } from '$lib/components/evilcharts/charts/echarts-bar-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>

<EChartsBarChart {data} config={chartConfig} class="h-full w-full p-4">
	<EChartsBarChart.Grid />
	<EChartsBarChart.XAxis dataKey="month" tickFormatter={(value) => String(value).substring(0, 3)} />
	<EChartsBarChart.Legend />
	<EChartsBarChart.Tooltip />
	<!-- [!code highlight] -->
	<EChartsBarChart.Bar dataKey="desktop" variant="default" enableHoverHighlight />
	<!-- [!code highlight] -->
	<EChartsBarChart.Bar dataKey="mobile" variant="default" enableHoverHighlight />
</EChartsBarChart>
```
> 
  

Set `enableHoverHighlight` on a <code>&lt;Bar&gt;</code> to dim every other bar on hover, keeping focus on one series. It uses ECharts' native emphasis/blur, so nothing re-renders mid-hover.




### Max Value Highlight

### <EChartsBarChart enableMaxValueHighlight />

```svelte
<script lang="ts">
	import { EChartsBarChart } from '$lib/components/evilcharts/charts/echarts-bar-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>

<EChartsBarChart {data} config={chartConfig} class="h-full w-full p-4" enableMaxValueHighlight>
	<EChartsBarChart.XAxis dataKey="month" tickFormatter={(value) => String(value).substring(0, 3)} />
	<EChartsBarChart.Legend />
	<EChartsBarChart.Tooltip />
	<EChartsBarChart.Bar dataKey="desktop" />
	<EChartsBarChart.Bar dataKey="mobile" />
</EChartsBarChart>
```
> 
  

Set `enableMaxValueHighlight` on the chart to color only its tallest column and mute the rest. With several series the comparison is per column — the totals across every series at that category — so a whole stack or group lights up together rather than one bar inside it.




### Gradient Colors

### gradient colors

```svelte
<script lang="ts">
	import { EChartsBarChart } from '$lib/components/evilcharts/charts/echarts-bar-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: ['#a855f7', '#6366f1', '#3b82f6'], // [!code highlight]
				dark: ['#f43f5e', '#ec4899', '#a855f7', '#6366f1', '#3b82f6'] // [!code highlight]
			}
		},
		mobile: {
			label: 'Mobile',
			colors: {
				light: ['#10b981', '#34d399', '#6ee7b7'], // [!code highlight]
				dark: ['#10b981', '#14b8a6', '#06b6d4'] // [!code highlight]
			}
		}
	} satisfies ChartConfig;
</script>

<EChartsBarChart {data} config={chartConfig} class="h-full w-full p-4">
	<EChartsBarChart.Grid />
	<EChartsBarChart.XAxis dataKey="month" tickFormatter={(value) => String(value).substring(0, 3)} />
	<EChartsBarChart.Legend isClickable />
	<EChartsBarChart.Tooltip />
	<EChartsBarChart.Bar dataKey="desktop" variant="default" isClickable />
	<EChartsBarChart.Bar dataKey="mobile" variant="default" isClickable />
</EChartsBarChart>
```

### Bar Variants

### variant='default'

```svelte
<script lang="ts">
	import { EChartsBarChart } from '$lib/components/evilcharts/charts/echarts-bar-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>

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

```svelte
<script lang="ts">
	import { EChartsBarChart } from '$lib/components/evilcharts/charts/echarts-bar-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>

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

```svelte
<script lang="ts">
	import { EChartsBarChart } from '$lib/components/evilcharts/charts/echarts-bar-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>

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

```svelte
<script lang="ts">
	import { EChartsBarChart } from '$lib/components/evilcharts/charts/echarts-bar-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>

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

```svelte
<script lang="ts">
	import { EChartsBarChart } from '$lib/components/evilcharts/charts/echarts-bar-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>

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

```svelte
<script lang="ts">
	import { EChartsBarChart } from '$lib/components/evilcharts/charts/echarts-bar-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>

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

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

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

	const chartConfig = {
		desktop: {
			label: 'Desktop',
			colors: {
				light: ['#0a0a0a'],
				dark: ['#fafafa']
			}
		}
	} satisfies ChartConfig;
</script>

<EChartsBarChart {data} config={chartConfig} class="h-full w-full p-4" barCategoryGap={32}>
	<EChartsBarChart.XAxis dataKey="month" tickFormatter={(value) => String(value).substring(0, 3)} />
	<EChartsBarChart.Tooltip />
	<EChartsBarChart.Bar dataKey="desktop" variant="blocks" />
</EChartsBarChart>
```
### variant='expandable'

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

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

	const chartConfig = {
		desktop: {
			label: 'Desktop',
			colors: {
				light: ['#0a0a0a'],
				dark: ['#fafafa']
			}
		}
	} satisfies ChartConfig;
</script>

<EChartsBarChart {data} config={chartConfig} class="h-full w-full p-4">
	<EChartsBarChart.XAxis dataKey="month" tickFormatter={(value) => String(value).substring(0, 3)} />
	<EChartsBarChart.Tooltip />
	<EChartsBarChart.Bar dataKey="desktop" variant="expandable" />
</EChartsBarChart>
```
> 
  

`variant="blocks"` renders each bar as a stack of segments rather than a solid column, and fills the rest of the column with the same segments in a muted tone — so every bar reads against a dim grid of its own blocks. `variant="expandable"` rests as a thin line and grows out from its own middle to the full bar width on hover, naming its value above itself.




### Stack Types

### stackType='stacked'

```svelte
<script lang="ts">
	import { EChartsBarChart } from '$lib/components/evilcharts/charts/echarts-bar-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>

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

```svelte
<script lang="ts">
	import { EChartsBarChart } from '$lib/components/evilcharts/charts/echarts-bar-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>

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

### Horizontal Layout

### layout='horizontal'

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

	const data = [
		{ month: 'January', desktop: 186 },
		{ month: 'February', desktop: 305 },
		{ month: 'March', desktop: 237 },
		{ month: 'April', desktop: 173 },
		{ month: 'May', desktop: 209 },
		{ month: 'June', desktop: 214 }
	];

	const chartConfig = {
		desktop: {
			label: 'Desktop',
			colors: {
				light: ['#2563eb'],
				dark: ['#3b82f6']
			}
		}
	} satisfies ChartConfig;
</script>

<!-- [!code highlight:6] -->
<EChartsBarChart {data} config={chartConfig} class="h-full w-full p-4" layout="horizontal">
	<EChartsBarChart.Grid />
	<!-- [!code highlight] -->
	<EChartsBarChart.YAxis dataKey="month" tickFormatter={(value) => String(value).substring(0, 3)} />
	<EChartsBarChart.Legend />
	<EChartsBarChart.Tooltip />
	<EChartsBarChart.Bar dataKey="desktop" variant="default" />
</EChartsBarChart>
```
> 
  

Set `layout="horizontal"` on <code>&lt;EChartsBarChart&gt;</code> to render bars horizontally. The <code>&lt;YAxis&gt;</code> then shows categories and the <code>&lt;XAxis&gt;</code> shows values — pass a `tickFormatter` to <code>&lt;YAxis&gt;</code> for category formatting.




### Glowing Bars

### <Bar glowing /> - desktop

```svelte
<script lang="ts">
	import { EChartsBarChart } from '$lib/components/evilcharts/charts/echarts-bar-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>

<EChartsBarChart {data} config={chartConfig} class="h-full w-full p-4">
	<EChartsBarChart.Grid />
	<EChartsBarChart.XAxis dataKey="month" tickFormatter={(value) => String(value).substring(0, 3)} />
	<EChartsBarChart.Legend isClickable />
	<EChartsBarChart.Tooltip />
	<!-- [!code highlight] -->
	<EChartsBarChart.Bar dataKey="desktop" variant="default" glowing isClickable />
	<EChartsBarChart.Bar dataKey="mobile" variant="default" isClickable />
</EChartsBarChart>
```
### <Bar glowing /> - mobile

```svelte
<script lang="ts">
	import { EChartsBarChart } from '$lib/components/evilcharts/charts/echarts-bar-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>

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

### 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 {
		EChartsBarChart,
		type ChartConfig
	} from '$lib/components/evilcharts/charts/echarts-bar-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'], dark: ['#f43f5e'] } }
	} satisfies ChartConfig;
</script>

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

## API Reference

The props below 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.

### EChartsBarChart

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


  ### `data` (required)

type: `TData[]`

The 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 `<Bar />`.
  ### `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.
  ### `stackType`

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

How multiple bars combine. `"default"` renders them side by side, `"stacked"` stacks them, and `"percent"` normalizes them to a percentage distribution.
  ### `layout`

type: `"vertical" | "horizontal"` · default: `"vertical"`

Bar orientation. With `"horizontal"`, bars lay sideways and the axes swap — the `<YAxis />` shows categories and the `<XAxis />` shows values.
  ### `barRadius`

type: `number` · default: `2`

Default corner radius for every `<Bar />`, in pixels. Each `<Bar />` can override it with its own `radius` prop.
  ### `animation`

type: `boolean` · default: `true`

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

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

Order in which bars grow into view, inherited by every `<Bar />`. Bars rise from their baseline with a per-datum stagger. `"none"` disables it — devices set to OS reduce-motion fall back to `"none"` automatically.
  ### `barGap`

type: `number`

Gap between bars in the same category (with multiple series), in pixels.
  ### `barCategoryGap`

type: `number`

Gap between bar categories, in pixels.
  ### `defaultSelectedDataKey`

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

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

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

Fires when a series is selected or deselected via a clickable `<Bar />` or `<Legend />`. Receives the selected data key, or `null` when deselected.
  ### `enableMaxValueHighlight`

type: `boolean` · default: `false`

Colors only the tallest column and mutes every other. With several series the comparison is per column (totals across all series), so a whole stack or group highlights together.
  ### `referenceLine`

type: `number | null` · default: `null`

Draws a dashed reference line across the value axis. Useful for medians, targets, and thresholds.
  ### `referenceLineFormatter`

type: `(value: number) => string`

Formats the label attached to `referenceLine`.
  ### `onDataHover`

type: `(datum: BarHoverDatum | null) => void`

Reports the category row under the pointer, and `null` when the pointer leaves the plot. This powers block-level readouts without coupling the chart to their layout.
  ### `isLoading`

type: `boolean` · default: `false`

Shows the animated shimmer skeleton while data loads.
  ### `loadingBars`

type: `number` · default: `12`

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

type: `keyof TData & string`

The data key used for the category axis. Falls back to the axis `dataKey`; also read by the brush footer.
  ### `chartOptions`

type: `Record<string, unknown>`

Escape hatch merged over 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.


### Bar

A single bar series. Each `<Bar />` carries its own fill variant, radius, glow, buffer, and clickability, so a chart can hold any number of bars with mixed styles.


  ### `dataKey` (required)

type: `string`

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

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

The bar's fill style, applied to this bar only. The `default` variant renders the full vertical color gradient for multi-color configs; `blocks` renders the bar as a stack of segments over a muted grid of the same segments; `isometric` draws dimensional front, top, and side faces.
  ### `radius`

type: `number`

The corner radius of this bar, in pixels. Falls back to the chart's `barRadius` when omitted.
  ### `animationType`

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

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

type: `boolean` · default: `false`

Lets this bar be selected by clicking it. When any bar is selected, unselected bars become semi-transparent.
  ### `enableHoverHighlight`

type: `boolean` · default: `false`

Hovering over a bar dims every other bar, keeping focus on one series.
  ### `glowing`

type: `boolean` · default: `false`

Applies a soft outer glow to this bar series.
  ### `bufferBar`

type: `boolean` · default: `false`

Renders this series' last data point with a hatched (diagonal lines) pattern and a series-colored outline while the rest stay solid. Useful for flagging projected or incomplete data at the end of a series.
  ### `ditherVariant`

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

Overrides the root ordered-dither pattern for this bar series only.


### XAxis and YAxis

The two axes. In the default (vertical) layout `<XAxis />` is the category axis and `<YAxis />` the value axis; `layout="horizontal"` swaps the roles. Include an axis to show its tick labels, omit it to hide them. Both hide automatically while loading, and the value axis formats ticks as percentages when `stackType="percent"`.


  ### `dataKey`

type: `string`

The category key for the axis. Overrides the root `xDataKey`.
  ### `tickFormatter`

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

Formats the axis tick labels. Category values arrive as strings.
  ### `label`

type: `string`

An axis title centered outside the tick labels — below the `<XAxis />`, alongside the `<YAxis />`. Hidden while loading.
  ### `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 split lines on the value axis; omit it and they don't render. Takes no props.

### Tooltip

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


  ### `variant`

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

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

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

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

type: `number`

Shows the tooltip by default at the given data point index, with no hover.
  ### `position`

type: `"fixed" | "variable"` · default: `"variable"`

How the tooltip is anchored. `"variable"` lets it follow the pointer, and `"fixed"` pins it near the top of the chart while only tracking the pointer's X.


### Legend

The series legend, rendered as HTML above the chart surface. Include it to show the legend; omit it and none shows. With `isClickable`, 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 `<EChartsBarChart.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.

