
### Basic Chart

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

	const data = [
		{ browser: 'chrome', visitors: 275 },
		{ browser: 'safari', visitors: 200 },
		{ browser: 'firefox', visitors: 187 },
		{ browser: 'edge', visitors: 173 },
		{ browser: 'other', visitors: 90 }
	];

	const chartConfig = {
		chrome: {
			label: 'Chrome',
			colors: {
				light: ['#3b82f6'],
				dark: ['#60a5fa']
			}
		},
		safari: {
			label: 'Safari',
			colors: {
				light: ['#10b981'],
				dark: ['#34d399']
			}
		},
		firefox: {
			label: 'Firefox',
			colors: {
				light: ['#f59e0b'],
				dark: ['#fbbf24']
			}
		},
		edge: {
			label: 'Edge',
			colors: {
				light: ['#8b5cf6'],
				dark: ['#a78bfa']
			}
		},
		other: {
			label: 'Other',
			colors: {
				light: ['#6b7280'],
				dark: ['#9ca3af']
			}
		}
	} satisfies ChartConfig;
</script>

<EChartsPieChart
	class="h-full w-full p-4"
	{data}
	dataKey="visitors"
	nameKey="browser"
	config={chartConfig}
	accessibility={{
		label: 'Browser visitor share pie chart',
		description: 'Visitor totals for Chrome, Safari, Firefox, Edge, and other browsers.'
	}}
>
	<EChartsPieChart.Legend isClickable />
	<EChartsPieChart.Tooltip />
	<EChartsPieChart.Pie isClickable />
</EChartsPieChart>
```

## Installation


  
  
    ### npm

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

### yarn

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

### bun

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

### pnpm

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

```bash
npm install echarts
```

### yarn

```bash
yarn add echarts
```

### bun

```bash
bun add echarts
```

### pnpm

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

In your `components` directory, create an `evilcharts` folder with a `charts` subfolder inside. Paste the code below into a new `echarts-pie-chart` file there.


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

`$lib/components/evilcharts/charts/echarts-pie-chart/background-overlay.svelte`

```svelte
<script lang="ts">
	import type { BackgroundVariant } from './types.js';
	let { variant }: { variant: BackgroundVariant } = $props();
	const id = $props.id();
	const patternId = $derived(`${id}-${variant}`);
	const maskId = $derived(`${id}-mask`);
	const filterId = $derived(`${id}-blur`);
</script>

<svg
	class="pointer-events-none absolute inset-0 h-full w-full"
	aria-hidden="true"
	preserveAspectRatio="none"
>
	<defs>
		<pattern
			id={patternId}
			width={variant === 'diagonal-lines'
				? 6
				: variant === 'plus' || variant === '4-pointed-star'
					? 16
					: variant === 'falling-triangles'
						? 18
						: variant === 'tiny-checkers'
							? 8
							: variant === 'overlapping-circles'
								? 40
								: variant === 'wiggle-lines'
									? 52
									: variant === 'bubbles'
										? 100
										: 20}
			height={variant === 'diagonal-lines'
				? 6
				: variant === 'plus' || variant === '4-pointed-star'
					? 16
					: variant === 'falling-triangles'
						? 36
						: variant === 'tiny-checkers'
							? 8
							: variant === 'overlapping-circles'
								? 40
								: variant === 'wiggle-lines'
									? 26
									: variant === 'bubbles'
										? 100
										: 20}
			patternUnits="userSpaceOnUse"
			patternTransform={variant === 'diagonal-lines'
				? 'rotate(45)'
				: variant === 'wiggle-lines'
					? 'scale(0.6)'
					: variant === 'bubbles'
						? 'scale(0.6667)'
						: undefined}
		>
			{#if variant === 'dots'}
				<circle class="text-border" cx="2" cy="2" r="1" fill="currentColor" />
			{:else if variant === 'grid'}
				<path
					class="text-border"
					d="M20 0H0V20"
					fill="none"
					stroke="currentColor"
					stroke-width="0.5"
				/>
			{:else if variant === 'cross-hatch'}
				<path
					class="text-border/60 dark:text-border/50"
					d="M0 0L20 20M20 0L0 20"
					fill="none"
					stroke="currentColor"
					stroke-width="0.5"
				/>
			{:else if variant === 'diagonal-lines'}
				<line
					class="text-border"
					x1="0"
					y1="0"
					x2="0"
					y2="6"
					stroke="currentColor"
					stroke-width="0.5"
				/>
			{:else if variant === 'plus'}
				<path
					class="text-border"
					d="M8 4V12M4 8H12"
					fill="none"
					stroke="currentColor"
					stroke-width="0.5"
					stroke-linecap="round"
				/>
			{:else if variant === 'falling-triangles'}
				<path
					class="text-border"
					d="M2 6h12L8 18 2 6zm18 36h12l-6 12-6-12z"
					transform="scale(0.5)"
					fill="currentColor"
					fill-opacity="0.4"
				/>
			{:else if variant === '4-pointed-star'}
				<polygon
					class="text-border"
					fill-rule="evenodd"
					points="5 3 8 4 5 5 4 8 3 5 0 4 3 3 4 0 5 3"
					fill="currentColor"
					fill-opacity="0.4"
				/>
			{:else if variant === 'tiny-checkers'}
				<path
					class="text-border"
					fill-rule="evenodd"
					d="M0 0h4v4H0V0zm4 4h4v4H4V4z"
					fill="currentColor"
					fill-opacity="0.2"
				/>
			{:else if variant === 'overlapping-circles'}
				<path
					class="text-border"
					fill-rule="evenodd"
					d="M25 25c0-2.762 2.238-5 5-5s5 2.238 5 5-2.238 5-5 5c0 2.762-2.238 5-5 5s-5-2.238-5-5 2.238-5 5-5zM5 5c0-2.762 2.238-5 5-5s5 2.238 5 5-2.238 5-5 5c0 2.762-2.238 5-5 5S0 12.762 0 10s2.238-5 5-5zm5 4c2.209 0 4-1.791 4-4s-1.791-4-4-4-4 1.791-4 4 1.791 4 4 4zm20 20c2.209 0 4-1.791 4-4s-1.791-4-4-4-4 1.791-4 4 1.791 4 4 4z"
					fill="currentColor"
					fill-opacity="0.4"
				/>
			{:else if variant === 'wiggle-lines'}
				<path
					class="text-border"
					d="M10 10c0-2.21-1.79-4-4-4-3.314 0-6-2.686-6-6h2c0 2.21 1.79 4 4 4 3.314 0 6 2.686 6 6 0 2.21 1.79 4 4 4 3.314 0 6 2.686 6 6 0 2.21 1.79 4 4 4 3.314 0 6 2.686 6 6 0 2.21 1.79 4 4 4v2c-3.314 0-6-2.686-6-6 0-2.21-1.79-4-4-4-3.314 0-6-2.686-6-6zm25.464-1.95l8.486 8.486-1.414 1.414-8.486-8.486 1.414-1.414z"
					fill="currentColor"
					fill-opacity="0.4"
				/>
			{:else}
				<path
					class="text-border"
					d="M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z"
					fill="currentColor"
					fill-opacity="0.4"
					fill-rule="evenodd"
				/>
			{/if}
		</pattern>
		<filter id={filterId}><feGaussianBlur stdDeviation="25" /></filter>
		<mask id={maskId} maskUnits="userSpaceOnUse">
			<rect x="8%" y="20%" width="85%" height="60%" fill="white" filter={`url(#${filterId})`} />
		</mask>
	</defs>
	<rect width="100%" height="100%" fill={`url(#${patternId})`} mask={`url(#${maskId})`} />
</svg>
```

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

```svelte
<script lang="ts">
	import { useEChartsPieChart } from './pie-chart-context.svelte.js';
	import type { BackgroundVariant } from './types.js';

	let { variant = 'dots' }: { variant?: BackgroundVariant } = $props();
	const token = $props.id();
	const chart = useEChartsPieChart();
	$effect(() => chart.backgrounds.register(token, () => ({ variant })));
</script>
```

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

```ts
import Root from './pie-chart.svelte';
import Pie from './pie.svelte';
import Label from './label.svelte';
import Background from './background.svelte';
import Legend from './legend.svelte';
import Tooltip from './tooltip.svelte';

type RootComponent = typeof Root;

export const EChartsPieChart: RootComponent & {
	Pie: typeof Pie;
	Label: typeof Label;
	Tooltip: typeof Tooltip;
	Legend: typeof Legend;
	Background: typeof Background;
} = Object.assign(Root, { Pie, Label, Tooltip, Legend, Background });

export type {
	ChartAccessibility,
	ChartConfig,
	EChartsRenderer
} from '$lib/components/evilcharts/ui/echarts-chart/index.js';
export type { LegendVariant } from '$lib/components/evilcharts/ui/echarts-legend/index.js';
export type { DitherVariant, RenderStyle } from '$lib/components/evilcharts/ui/echarts-dither/index.js';
export type {
	TooltipPosition,
	TooltipRoundness,
	TooltipVariant
} from '$lib/components/evilcharts/ui/echarts-tooltip/index.js';
export type { BackgroundVariant, DitherBloom, LabelPosition, PieVariant } from './types.js';
```

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

```svelte
<script lang="ts">
	import { useEChartsPieSlots } from './pie-slots.svelte.js';
	import type { LabelPosition } from './types.js';

	let { dataKey, position = 'inside' }: { dataKey?: string; position?: LabelPosition } = $props();
	const token = $props.id();
	const slots = useEChartsPieSlots();
	$effect(() => slots.labels.register(token, () => ({ dataKey, position })));
</script>
```

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

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

	let {
		variant = 'rounded-square',
		align = 'center',
		verticalAlign = 'bottom',
		isClickable = false
	}: {
		variant?: LegendVariant;
		align?: 'left' | 'center' | 'right';
		verticalAlign?: 'top' | 'middle' | 'bottom';
		isClickable?: boolean;
	} = $props();

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

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

```ts
import type { PieSeriesOption } from 'echarts/charts';
import type { TooltipComponentOption } from 'echarts/components';
import type { ComposeOption } from 'echarts/core';
import * as echarts from 'echarts/core';
import {
	getColorsCount,
	withAlpha,
	type ChartConfig,
	type ResolvedColors
} from '$lib/components/evilcharts/ui/echarts-chart/index.js';
import {
	createDitherPattern,
	type DitherVariant,
	type RenderStyle
} from '$lib/components/evilcharts/ui/echarts-dither/index.js';
import {
	resolveTooltipPosition,
	roundnessClass,
	tooltipIndicatorHtml,
	tooltipRow,
	tooltipVariantClass
} from '$lib/components/evilcharts/ui/echarts-tooltip/index.js';
import {
	DEFAULT_CORNER_RADIUS,
	DEFAULT_END_ANGLE,
	DEFAULT_INNER_RADIUS,
	DEFAULT_OUTER_RADIUS,
	DEFAULT_PADDING_ANGLE,
	DEFAULT_START_ANGLE,
	LOADING_SECTORS,
	REVEAL_DURATION,
	type DitherBloom,
	type LegendRegistration,
	type PieRegistration,
	type TooltipRegistration
} from './types.js';

export type EChartsPieOption = ComposeOption<PieSeriesOption | TooltipComponentOption>;

export type PieOptionContext = {
	data: Record<string, unknown>[];
	config: ChartConfig;
	dataKey: string;
	nameKey: string;
	pie?: PieRegistration;
	selectedSector: string | null;
	tooltip?: TooltipRegistration;
	legend?: LegendRegistration;
	isLoading: boolean;
	resolved: ResolvedColors;
	animation: boolean;
	reducedMotion: boolean;
	renderStyle?: RenderStyle;
	ditherVariant?: DitherVariant;
	ditherCellSize?: number;
	bloom?: DitherBloom;
	rendererSize?: { width: number; height: number };
};

const FALLBACK_COLOR = 'rgba(120, 120, 120, 1)';
const OVERLAP_BORDER_WIDTH = 5;
const SELECTED_OFFSET = 12;
const DIMMED_OPACITY = 0.15;
const LOADING_BASE_OPACITY = 0.15;
const LOADING_PEAK_OPACITY = 0.5;
const LOADING_SHIMMER_BAND = 0.28;
const LOADING_SHIMMER_FEATHER = 0.22;

function percent(value: string): number | null {
	const match = /^(-?(?:\d+\.?\d*|\.\d+))%$/.exec(value.trim());
	return match ? Number(match[1]) / 100 : null;
}

function pieDitherBounds(context: PieOptionContext) {
	const size = context.rendererSize;
	if (!size || size.width <= 0 || size.height <= 0) return undefined;
	const outer = context.pie?.outerRadius ?? DEFAULT_OUTER_RADIUS;
	const radius =
		typeof outer === 'number'
			? outer
			: (percent(outer) ?? 0.8) * (Math.min(size.width, size.height) / 2);
	const center = Number.parseFloat(centerY(context.legend)) / 100;
	return { height: radius * 2, offsetY: size.height * center - radius };
}

function rendererDitherPattern(
	slots: string[],
	variant: DitherVariant,
	cellSize: number,
	context: PieOptionContext
) {
	return createDitherPattern(slots, variant, cellSize, 1, pieDitherBounds(context));
}

function sectorPaint(
	slots: string[],
	renderStyle: RenderStyle,
	ditherVariant: DitherVariant,
	ditherCellSize: number,
	context: PieOptionContext
): string | echarts.graphic.LinearGradient | ReturnType<typeof createDitherPattern> {
	if (renderStyle === 'dither') {
		return rendererDitherPattern(slots, ditherVariant, ditherCellSize, context);
	}
	if (slots.length <= 1) return slots[0] ?? FALLBACK_COLOR;
	return new echarts.graphic.LinearGradient(
		0,
		0,
		1,
		1,
		slots.map((color, index) => ({ offset: index / (slots.length - 1), color }))
	);
}

function bloomPixels(bloom: DitherBloom | undefined): number {
	if (bloom === 'aura') return 14;
	if (bloom === 'high') return 8;
	if (bloom === 'low') return 4;
	return 0;
}

function sectorBorder(paddingAngle: number, background: string) {
	if (paddingAngle < 0) return { borderColor: background, borderWidth: OVERLAP_BORDER_WIDTH };
	if (paddingAngle > 0) return { borderColor: background, borderWidth: paddingAngle };
	return {};
}

function loadingSectorAlpha(position: number, center: number): number {
	const rawDistance = Math.abs(position - center);
	const distance = Math.min(rawDistance, 1 - rawDistance);
	if (distance >= LOADING_SHIMMER_BAND) return LOADING_BASE_OPACITY;
	if (distance <= LOADING_SHIMMER_BAND - LOADING_SHIMMER_FEATHER) {
		return LOADING_PEAK_OPACITY;
	}
	const progress =
		1 - (distance - (LOADING_SHIMMER_BAND - LOADING_SHIMMER_FEATHER)) / LOADING_SHIMMER_FEATHER;
	const eased = Math.sin((progress * Math.PI) / 2);
	return LOADING_BASE_OPACITY + (LOADING_PEAK_OPACITY - LOADING_BASE_OPACITY) * eased;
}

export function createPieLoadingFrame({
	center,
	foreground,
	background,
	cornerRadius,
	paddingAngle
}: {
	center?: number;
	foreground: string;
	background: string;
	cornerRadius: number;
	paddingAngle: number;
}) {
	return Array.from({ length: LOADING_SECTORS }, (_, index) => ({
		name: `__loading-${index}`,
		value: 1,
		itemStyle: {
			color: withAlpha(
				foreground,
				center === undefined
					? LOADING_BASE_OPACITY
					: loadingSectorAlpha((index + 0.5) / LOADING_SECTORS, center)
			),
			opacity: 1,
			borderRadius: cornerRadius,
			...sectorBorder(paddingAngle, background)
		}
	}));
}

function centerY(legend?: LegendRegistration): string {
	if (!legend) return '50%';
	if (legend.verticalAlign === 'bottom') return '45%';
	if (legend.verticalAlign === 'top') return '55%';
	return '50%';
}

function tooltipOption(context: PieOptionContext): TooltipComponentOption {
	const slot = context.tooltip;
	return {
		show: Boolean(slot) && !context.isLoading,
		trigger: 'item',
		confine: true,
		backgroundColor: 'transparent',
		borderWidth: 0,
		padding: 0,
		extraCssText: 'box-shadow:none;',
		displayTransition: false,
		position: resolveTooltipPosition(slot?.position ?? 'variable'),
		formatter: (params: unknown) => {
			const item = (Array.isArray(params) ? params[0] : params) as {
				name?: unknown;
				value?: unknown;
				seriesId?: unknown;
			} | null;
			if (!item || String(item.seriesId ?? '').startsWith('__')) return '';
			const key = String(item.name ?? '');
			const configItem = context.config[key];
			const label = typeof configItem?.label === 'string' ? configItem.label : key;
			const value =
				typeof item.value === 'number' ? item.value.toLocaleString() : String(item.value ?? '');
			const row = tooltipRow({
				indicatorHtml: tooltipIndicatorHtml(key, getColorsCount(configItem ?? {})),
				labelText: label,
				valueText: value,
				dimmed:
					context.selectedSector !== null && context.selectedSector !== key ? ' opacity-30' : ''
			});
			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[slot?.roundness ?? 'lg']} ${tooltipVariantClass[slot?.variant ?? 'default']}"><div class="grid gap-1.5">${row}</div></div>`;
		}
	};
}

function realSeries(context: PieOptionContext): PieSeriesOption[] {
	const pie = context.pie;
	if (!pie) return [];
	const selected = context.selectedSector;
	const outside = pie.labelPosition === 'outside';
	const explicitLabelKey = pie.labelDataKey || null;
	const shadowBlur = context.renderStyle === 'dither' ? bloomPixels(context.bloom) : 0;
	const data = context.data.map((row) => {
		const name = String(row[context.nameKey]);
		const isSelected = pie.isClickable && selected === name;
		const dimmed = pie.isClickable && selected !== null && !isSelected;
		return {
			name,
			value: Number(row[context.dataKey]) || 0,
			selected: isSelected,
			itemStyle: {
				color: sectorPaint(
					context.resolved.series[name] ?? [FALLBACK_COLOR],
					context.renderStyle ?? 'native',
					pie.ditherVariant ?? context.ditherVariant ?? 'gradient',
					context.ditherCellSize ?? 2,
					context
				),
				opacity: dimmed ? DIMMED_OPACITY : 1,
				borderRadius: pie.cornerRadius,
				shadowBlur,
				shadowColor:
					shadowBlur > 0
						? (context.resolved.series[name]?.[0] ?? context.resolved.tokens.foreground)
						: undefined,
				...sectorBorder(pie.paddingAngle, context.resolved.tokens.background)
			}
		};
	});

	return [
		{
			id: 'pie',
			type: 'pie',
			center: ['50%', centerY(context.legend)],
			radius: [pie.innerRadius, pie.outerRadius],
			startAngle: pie.startAngle,
			endAngle: pie.endAngle,
			clockwise: false,
			padAngle: Math.min(pie.paddingAngle, 0),
			cursor: pie.isClickable ? 'pointer' : 'default',
			emphasis: { scale: false },
			selectedMode: pie.isClickable ? 'single' : false,
			selectedOffset: SELECTED_OFFSET,
			select: { itemStyle: {} },
			label: {
				show: pie.labelDataKey !== null,
				position: outside ? 'outside' : 'inner',
				color: outside
					? context.resolved.tokens.mutedForeground
					: context.resolved.tokens.background,
				fontSize: 12,
				fontWeight: 500,
				formatter: (params: { dataIndex: number; name?: string; value?: unknown }) => {
					const row = context.data[params.dataIndex];
					if (explicitLabelKey) return String(row?.[explicitLabelKey] ?? '');
					if (outside) {
						const label = context.config[String(params.name ?? '')]?.label;
						return typeof label === 'string' ? label : String(params.name ?? '');
					}
					return String(row?.[context.dataKey] ?? params.value ?? '');
				}
			},
			labelLine: outside
				? {
						show: true,
						length: 14,
						length2: 14,
						smooth: false,
						lineStyle: { color: withAlpha(context.resolved.tokens.mutedForeground, 0.45), width: 1 }
					}
				: { show: false },
			data,
			animation: context.animation && !context.reducedMotion,
			animationType: 'expansion',
			animationDuration: REVEAL_DURATION,
			animationDurationUpdate: 0
		}
	];
}

function loadingSeries(context: PieOptionContext): PieSeriesOption[] {
	const pie = context.pie;
	const innerRadius = pie?.innerRadius ?? DEFAULT_INNER_RADIUS;
	const outerRadius = pie?.outerRadius ?? DEFAULT_OUTER_RADIUS;
	const cornerRadius = pie?.cornerRadius ?? DEFAULT_CORNER_RADIUS;
	const paddingAngle = pie?.paddingAngle ?? DEFAULT_PADDING_ANGLE;
	const startAngle = pie?.startAngle ?? DEFAULT_START_ANGLE;
	const endAngle = pie?.endAngle ?? DEFAULT_END_ANGLE;
	return [
		{
			id: '__loading',
			type: 'pie',
			center: ['50%', centerY(context.legend)],
			radius: [innerRadius, outerRadius],
			startAngle,
			endAngle,
			clockwise: false,
			padAngle: Math.min(paddingAngle, 0),
			silent: true,
			emphasis: { scale: false },
			label: { show: false },
			labelLine: { show: false },
			animation: false,
			data: createPieLoadingFrame({
				foreground: context.resolved.tokens.foreground,
				background: context.resolved.tokens.background,
				cornerRadius,
				paddingAngle
			})
		}
	];
}

export function buildPieOption(context: PieOptionContext): EChartsPieOption {
	if (context.isLoading) {
		return {
			animation: false,
			aria: { enabled: true },
			tooltip: { show: false },
			series: loadingSeries(context)
		};
	}
	return {
		animation: false,
		aria: { enabled: true },
		tooltip: tooltipOption(context),
		series: realSeries(context)
	};
}
```

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

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

const PIE_CHART_CONTEXT = Symbol('evilcharts.echarts-pie-chart');

export class EChartsPieChartContext {
	pies = new RegistrationSet<PieRegistration>();
	backgrounds = new RegistrationSet<BackgroundRegistration>();
}

export function setEChartsPieChartContext(): EChartsPieChartContext {
	const context = new EChartsPieChartContext();
	setContext(PIE_CHART_CONTEXT, context);
	return context;
}

export function useEChartsPieChart(): EChartsPieChartContext {
	const context = getContext<EChartsPieChartContext | undefined>(PIE_CHART_CONTEXT);
	if (!context) {
		throw new Error('[EvilCharts] ECharts pie parts must be children of EChartsPieChart.');
	}
	return context;
}
```

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

```svelte
<script lang="ts" generics="TData extends Record<string, unknown>">
	import { prefersReducedMotion } from 'svelte/motion';
	import { untrack, type Snippet } from 'svelte';
	import type { EChartsCoreOption, EChartsType } from 'echarts/core';
	import { AriaComponent, TooltipComponent } from 'echarts/components';
	import { PieChart } from 'echarts/charts';
	import * as echarts from 'echarts/core';
	import {
		ChartContainer,
		DEFAULT_ECHARTS_RENDERER,
		EChartsHost,
		LoadingIndicator,
		RegistrationSet,
		SelectableSeriesControls,
		resolveColors,
		setEChartsSharedSlotContext,
		type ChartAccessibility,
		type ChartConfig,
		type EChartsRenderer,
		type ResolvedColors
	} from '$lib/components/evilcharts/ui/echarts-chart/index.js';
	import type { DitherVariant, RenderStyle } from '$lib/components/evilcharts/ui/echarts-dither/index.js';
	import { LegendOverlay } from '$lib/components/evilcharts/ui/echarts-legend/index.js';
	import { setEChartsPieChartContext } from './pie-chart-context.svelte.js';
	import { buildPieOption, createPieLoadingFrame } from './option.js';
	import BackgroundOverlay from './background-overlay.svelte';
	import {
		LOADING_ANIMATION_DURATION,
		REVEAL_DURATION,
		type DitherBloom,
		type LegendRegistration,
		type TooltipRegistration
	} from './types.js';

	echarts.use([PieChart, TooltipComponent, AriaComponent]);

	let {
		data,
		config,
		dataKey,
		nameKey,
		class: className,
		renderer = DEFAULT_ECHARTS_RENDERER,
		animation = true,
		renderStyle = 'native',
		ditherVariant = 'gradient',
		ditherCellSize = 2,
		bloom = 'off',
		defaultSelectedSector = null,
		selectedSector: selectedSectorProp,
		onSelectionChange,
		isLoading = false,
		chartOptions,
		accessibility,
		children
	}: {
		data: TData[];
		config: ChartConfig;
		dataKey: keyof TData & string;
		nameKey: keyof TData & string;
		class?: string;
		renderer?: EChartsRenderer;
		animation?: boolean;
		renderStyle?: RenderStyle;
		ditherVariant?: DitherVariant;
		ditherCellSize?: number;
		bloom?: DitherBloom;
		defaultSelectedSector?: string | null;
		selectedSector?: string | null;
		onSelectionChange?: (selection: { dataKey: string; value: number } | null) => void;
		isLoading?: boolean;
		chartOptions?: Record<string, unknown>;
		accessibility?: ChartAccessibility;
		children?: Snippet;
	} = $props();

	let container = $state<HTMLDivElement>();
	let instance = $state.raw<EChartsType>();
	let themeRevision = $state(0);
	let dimension = $state({ width: 320, height: 200 });
	let internalSelectedSector = $state<string | null>(untrack(() => defaultSelectedSector));
	let introComplete = $state(false);
	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 selectedSector = $derived(
		selectedSectorProp === undefined ? internalSelectedSector : selectedSectorProp
	);
	const chart = setEChartsPieChartContext();
	const tooltipSlots = new RegistrationSet<TooltipRegistration>();
	const legendSlots = new RegistrationSet<LegendRegistration>();
	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 () => {};
		}
	});

	const pie = $derived(chart.pies.first);
	const background = $derived(chart.backgrounds.first);
	const tooltip = $derived(tooltipSlots.first);
	const legend = $derived(legendSlots.first);
	const sectorKeys = $derived(data.map((row) => String(row[nameKey])));
	const selectableSectors = $derived(
		pie?.isClickable
			? sectorKeys.map((key) => ({
					key,
					label: typeof config[key]?.label === 'string' ? config[key].label : key
				}))
			: []
	);

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

	const option = $derived.by(() => {
		const revealEnabled =
			animation && !introComplete && !isLoading && !prefersReducedMotion.current;
		const built = buildPieOption({
			data: data as Record<string, unknown>[],
			config,
			dataKey,
			nameKey,
			pie,
			selectedSector,
			tooltip,
			legend,
			isLoading,
			resolved,
			animation: revealEnabled,
			reducedMotion: prefersReducedMotion.current,
			renderStyle,
			ditherVariant,
			ditherCellSize,
			bloom,
			rendererSize: dimension
		});
		return {
			...(chartOptions ? { ...built, ...chartOptions } : built),
			animation: revealEnabled,
			animationDuration: REVEAL_DURATION,
			animationDurationUpdate: 0
		} as EChartsCoreOption;
	});

	function selectSector(name: string | null) {
		if (selectedSectorProp === undefined) internalSelectedSector = name;
		if (name === null) {
			onSelectionChange?.(null);
			return;
		}
		const row = data.find((item) => String(item[nameKey]) === name);
		onSelectionChange?.(row ? { dataKey: name, value: Number(row[dataKey]) || 0 } : null);
	}

	function toggleSector(name: string) {
		selectSector(selectedSector === name ? null : name);
	}

	const events = $derived({
		click: (params: unknown) => {
			const event = params as { name?: unknown; seriesId?: unknown } | null;
			if (!event || String(event.seriesId ?? '').startsWith('__')) return;
			if (pie?.isClickable && typeof event.name === 'string') toggleSector(event.name);
		}
	});

	$effect(() => {
		const chartInstance = instance;
		const pieSlot = pie;
		if (isLoading) {
			introComplete = false;
			return;
		}
		if (!chartInstance || !pieSlot || introComplete) return;
		if (!animation || prefersReducedMotion.current) {
			introComplete = true;
			return;
		}
		const timer = window.setTimeout(() => (introComplete = true), REVEAL_DURATION);
		return () => window.clearTimeout(timer);
	});

	$effect(() => {
		const chartInstance = instance;
		if (!chartInstance || isLoading || tooltip?.defaultIndex === undefined) return;
		const frame = requestAnimationFrame(() => {
			chartInstance.dispatchAction({
				type: 'showTip',
				seriesIndex: 0,
				dataIndex: tooltip.defaultIndex
			});
		});
		return () => cancelAnimationFrame(frame);
	});

	$effect(() => {
		const chartInstance = instance;
		if (!chartInstance || !isLoading) return;
		const cornerRadius = pie?.cornerRadius ?? 0;
		const paddingAngle = pie?.paddingAngle ?? 0;
		if (prefersReducedMotion.current) {
			chartInstance.setOption(
				{
					series: [
						{
							id: '__loading',
							data: createPieLoadingFrame({
								center: 0.5,
								foreground: resolved.tokens.foreground,
								background: resolved.tokens.background,
								cornerRadius,
								paddingAngle
							})
						}
					]
				},
				{ silent: true, lazyUpdate: true }
			);
			return;
		}
		let frame = 0;
		const startedAt = performance.now();
		const tick = (now: number) => {
			const center = ((((now - startedAt) / LOADING_ANIMATION_DURATION) % 1) + 1) % 1;
			const data = createPieLoadingFrame({
				center,
				foreground: resolved.tokens.foreground,
				background: resolved.tokens.background,
				cornerRadius,
				paddingAngle
			});
			chartInstance.setOption(
				{ series: [{ id: '__loading', data }] },
				{ silent: true, lazyUpdate: true }
			);
			frame = requestAnimationFrame(tick);
		};
		frame = requestAnimationFrame(tick);
		return () => cancelAnimationFrame(frame);
	});

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

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

<ChartContainer
	{config}
	{accessibility}
	{overlay}
	bind:element={container}
	bind:themeRevision
	bind:dimension
	aria-busy={isLoading}
	class={className}
>
	{@render children?.()}
	{#if background && !isLoading}<BackgroundOverlay variant={background.variant} />{/if}
	{#if pie}
		<EChartsHost {option} {renderer} {events} bind:instance />
	{/if}
	{#if !legend?.isClickable}
		<SelectableSeriesControls
			items={selectableSectors}
			selectedKey={selectedSector}
			onToggle={toggleSector}
		/>
	{/if}
</ChartContainer>
```

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

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

const PIE_SLOTS_CONTEXT = Symbol('evilcharts.echarts-pie-slots');

export class EChartsPieSlots {
	labels = new RegistrationSet<LabelRegistration>();
}

export function setEChartsPieSlots(): EChartsPieSlots {
	const context = new EChartsPieSlots();
	setContext(PIE_SLOTS_CONTEXT, context);
	return context;
}

export function useEChartsPieSlots(): EChartsPieSlots {
	const context = getContext<EChartsPieSlots | undefined>(PIE_SLOTS_CONTEXT);
	if (!context) throw new Error('[EvilCharts] ECharts Label must be nested inside Pie.');
	return context;
}
```

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

```svelte
<script lang="ts">
	import type { Snippet } from 'svelte';
	import type { DitherVariant } from '$lib/components/evilcharts/ui/echarts-dither/index.js';
	import { useEChartsPieChart } from './pie-chart-context.svelte.js';
	import { setEChartsPieSlots } from './pie-slots.svelte.js';
	import {
		DEFAULT_CORNER_RADIUS,
		DEFAULT_END_ANGLE,
		DEFAULT_INNER_RADIUS,
		DEFAULT_OUTER_RADIUS,
		DEFAULT_PADDING_ANGLE,
		DEFAULT_START_ANGLE,
		type PieVariant
	} from './types.js';

	let {
		variant = 'gradient',
		innerRadius = DEFAULT_INNER_RADIUS,
		outerRadius = DEFAULT_OUTER_RADIUS,
		cornerRadius = DEFAULT_CORNER_RADIUS,
		paddingAngle = DEFAULT_PADDING_ANGLE,
		startAngle = DEFAULT_START_ANGLE,
		endAngle = DEFAULT_END_ANGLE,
		isClickable = false,
		ditherVariant,
		children
	}: {
		variant?: PieVariant;
		innerRadius?: number | string;
		outerRadius?: number | string;
		cornerRadius?: number;
		paddingAngle?: number;
		startAngle?: number;
		endAngle?: number;
		isClickable?: boolean;
		ditherVariant?: DitherVariant;
		children?: Snippet;
	} = $props();

	const token = $props.id();
	const chart = useEChartsPieChart();
	const slots = setEChartsPieSlots();

	$effect(() =>
		chart.pies.register(token, () => {
			const label = slots.labels.first;
			return {
				variant,
				innerRadius,
				outerRadius,
				cornerRadius,
				paddingAngle,
				startAngle,
				endAngle,
				isClickable,
				ditherVariant,
				labelDataKey: label ? (label.dataKey ?? '') : null,
				labelPosition: label?.position ?? 'inside'
			};
		})
	);
</script>

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

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

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

	let {
		variant = 'default',
		roundness = 'lg',
		defaultIndex,
		position = 'variable'
	}: {
		variant?: TooltipVariant;
		roundness?: TooltipRoundness;
		defaultIndex?: number;
		position?: TooltipPosition;
	} = $props();

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

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

```ts
import type { LegendVariant } from '$lib/components/evilcharts/ui/echarts-legend/index.js';
import type { DitherBloom, DitherVariant } from '$lib/components/evilcharts/ui/echarts-dither/index.js';
import type {
	TooltipPosition,
	TooltipRoundness,
	TooltipVariant
} from '$lib/components/evilcharts/ui/echarts-tooltip/index.js';

export const REVEAL_DURATION = 1000;
export const LOADING_ANIMATION_DURATION = 2000;
export const LOADING_SECTORS = 5;
export const DEFAULT_INNER_RADIUS: number | string = 0;
export const DEFAULT_OUTER_RADIUS: number | string = '80%';
export const DEFAULT_CORNER_RADIUS = 0;
export const DEFAULT_PADDING_ANGLE = 0;
export const DEFAULT_START_ANGLE = 0;
export const DEFAULT_END_ANGLE = 360;

export type PieVariant = 'gradient';
export type LabelPosition = 'inside' | 'outside';
export type { DitherBloom };
export type BackgroundVariant =
	| 'dots'
	| 'grid'
	| 'cross-hatch'
	| 'diagonal-lines'
	| 'plus'
	| 'falling-triangles'
	| '4-pointed-star'
	| 'tiny-checkers'
	| 'overlapping-circles'
	| 'wiggle-lines'
	| 'bubbles';

export type LabelRegistration = {
	dataKey?: string;
	position: LabelPosition;
};

export type PieRegistration = {
	variant: PieVariant;
	innerRadius: number | string;
	outerRadius: number | string;
	cornerRadius: number;
	paddingAngle: number;
	startAngle: number;
	endAngle: number;
	isClickable: boolean;
	ditherVariant?: DitherVariant;
	labelDataKey: string | null;
	labelPosition: LabelPosition;
};

export type BackgroundRegistration = { variant: BackgroundVariant };

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

export type LegendRegistration = {
	variant: LegendVariant;
	align: 'left' | 'center' | 'right';
	verticalAlign: 'top' | 'middle' | 'bottom';
	isClickable: boolean;
};
```
        
      
      
        ### 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)
	};
}
```
        
        

Finally, 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>
```
        
      
    
  


## Usage

The ECharts pie chart is composable, sharing the LayerChart sibling's API shape. `<EChartsPieChart>` is the container, and every part hangs off it as a compound member — `<EChartsPieChart.Legend>`, `<EChartsPieChart.Tooltip>`, `<EChartsPieChart.Background>`, and one `<EChartsPieChart.Pie>` — so a single import gives you the whole chart. The `<EChartsPieChart.Pie>` carries its own shape props (`innerRadius`, `paddingAngle`, `cornerRadius`, …) and an `isClickable` flag.

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

```svelte
const data = [
  { browser: "chrome", visitors: 275 },
  { browser: "safari", visitors: 200 },
  { browser: "firefox", visitors: 187 },
];

const chartConfig = {
  chrome: {
    label: "Chrome",
    colors: { light: ["#3b82f6"], dark: ["#60a5fa"] },
  },
  safari: {
    label: "Safari",
    colors: { light: ["#10b981"], dark: ["#34d399"] },
  },
  firefox: {
    label: "Firefox",
    colors: { light: ["#f59e0b"], dark: ["#fbbf24"] },
  },
} satisfies ChartConfig;
```

```svelte
<EChartsPieChart {data} dataKey="visitors" nameKey="browser" config={chartConfig}>
	<EChartsPieChart.Legend isClickable />
	<EChartsPieChart.Tooltip />
	<EChartsPieChart.Pie isClickable innerRadius={60} paddingAngle={4} cornerRadius={8}>
		<EChartsPieChart.Label />
	</EChartsPieChart.Pie>
</EChartsPieChart>
```

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 sector name to a `label` and a per-theme `colors` array. See [Chart Config](/docs/chart-config) for the full shape. Colors resolve from your CSS variables at runtime, so dark mode works with no extra wiring.

> 
  

The ECharts implementation brings a few small departures from the LayerChart sibling: per-sector gradients paint across each sector's own bounding box, sector gaps are constant-width background borders (parallel-edged from rim to center, not a wedge-shaped angular pad), and the <code>&lt;Background&gt;</code> pattern is a separate SVG layer behind the transparent ECharts surface.




### 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 { EChartsPieChart } from '$lib/components/evilcharts/charts/echarts-pie-chart/index.js';
	import { type ChartConfig } from '$lib/components/evilcharts/ui/echarts-chart/index.js';

	const data = [
		{ browser: 'chrome', visitors: 275 },
		{ browser: 'safari', visitors: 200 },
		{ browser: 'firefox', visitors: 187 },
		{ browser: 'edge', visitors: 173 },
		{ browser: 'other', visitors: 90 }
	];

	const chartConfig = {
		chrome: {
			label: 'Chrome',
			colors: {
				light: ['#3b82f6'],
				dark: ['#60a5fa']
			}
		},
		safari: {
			label: 'Safari',
			colors: {
				light: ['#10b981'],
				dark: ['#34d399']
			}
		},
		firefox: {
			label: 'Firefox',
			colors: {
				light: ['#f59e0b'],
				dark: ['#fbbf24']
			}
		},
		edge: {
			label: 'Edge',
			colors: {
				light: ['#8b5cf6'],
				dark: ['#a78bfa']
			}
		},
		other: {
			label: 'Other',
			colors: {
				light: ['#6b7280'],
				dark: ['#9ca3af']
			}
		}
	} satisfies ChartConfig;
</script>

<EChartsPieChart
	renderer="svg"
	class="h-full w-full p-4"
	{data}
	dataKey="visitors"
	nameKey="browser"
	config={chartConfig}
>
	<EChartsPieChart.Legend isClickable />
	<EChartsPieChart.Tooltip />
	<EChartsPieChart.Pie isClickable />
</EChartsPieChart>
```

### Interactive Selection

Add `isClickable` to the `<Pie>` (and `<Legend>`) to make sectors selectable. Selecting one pops it radially outward — the offset-slice look — while the others dim; select again to reset. Handle selection events with the `onSelectionChange` callback on `<EChartsPieChart>`:

```svelte
<EChartsPieChart
	{data}
	dataKey="visitors"
	nameKey="browser"
	config={chartConfig}
	onSelectionChange={(selection) => {
		if (selection) {
			console.log('Selected:', selection.dataKey, 'Value:', selection.value);
		} else {
			console.log('Deselected');
		}
	}}
>
	<EChartsPieChart.Legend isClickable />
	<EChartsPieChart.Tooltip />
	<EChartsPieChart.Pie isClickable />
</EChartsPieChart>
```

### Loading State

### isLoading='true'

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

	const data = [
		{ browser: 'chrome', visitors: 275 },
		{ browser: 'safari', visitors: 200 },
		{ browser: 'firefox', visitors: 187 },
		{ browser: 'edge', visitors: 173 },
		{ browser: 'other', visitors: 90 }
	];

	const chartConfig = {
		chrome: {
			label: 'Chrome',
			colors: {
				light: ['#3b82f6'],
				dark: ['#60a5fa']
			}
		},
		safari: {
			label: 'Safari',
			colors: {
				light: ['#10b981'],
				dark: ['#34d399']
			}
		},
		firefox: {
			label: 'Firefox',
			colors: {
				light: ['#f59e0b'],
				dark: ['#fbbf24']
			}
		},
		edge: {
			label: 'Edge',
			colors: {
				light: ['#8b5cf6'],
				dark: ['#a78bfa']
			}
		},
		other: {
			label: 'Other',
			colors: {
				light: ['#6b7280'],
				dark: ['#9ca3af']
			}
		}
	} satisfies ChartConfig;
</script>

<EChartsPieChart
	class="h-full w-full p-4"
	{data}
	dataKey="visitors"
	nameKey="browser"
	config={chartConfig}
	isLoading
>
	<EChartsPieChart.Legend isClickable />
	<EChartsPieChart.Tooltip />
	<EChartsPieChart.Pie isClickable />
</EChartsPieChart>
```
> 
  

Pass the `isLoading` prop to show an animated skeleton ring — a shimmer sweeps around the sectors while your data loads.




## Examples

Examples of the pie chart in different configurations. Customize `innerRadius`, `paddingAngle`, `cornerRadius`, and more.

### Gradient Colors

### gradient colors

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

	const data = [
		{ browser: 'chrome', visitors: 275 },
		{ browser: 'safari', visitors: 200 },
		{ browser: 'firefox', visitors: 187 },
		{ browser: 'edge', visitors: 173 },
		{ browser: 'other', visitors: 90 }
	];

	const chartConfig = {
		chrome: {
			label: 'Chrome',
			colors: {
				// [!code highlight:2]
				light: ['#93c5fd', '#3b82f6', '#2563eb', '#1d4ed8', '#1e40af'],
				dark: ['#bfdbfe', '#60a5fa', '#3b82f6', '#2563eb', '#1d4ed8']
			}
		},
		safari: {
			label: 'Safari',
			colors: {
				// [!code highlight:2]
				light: ['#6ee7b7', '#10b981', '#059669', '#047857', '#065f46'],
				dark: ['#a7f3d0', '#34d399', '#10b981', '#059669', '#047857']
			}
		},
		firefox: {
			label: 'Firefox',
			colors: {
				// [!code highlight:2]
				light: ['#fcd34d', '#f59e0b', '#d97706', '#b45309', '#92400e'],
				dark: ['#fde68a', '#fbbf24', '#f59e0b', '#d97706', '#b45309']
			}
		},
		edge: {
			label: 'Edge',
			colors: {
				// [!code highlight:2]
				light: ['#c4b5fd', '#8b5cf6', '#7c3aed', '#6d28d9', '#5b21b6'],
				dark: ['#ddd6fe', '#a78bfa', '#8b5cf6', '#7c3aed', '#6d28d9']
			}
		},
		other: {
			label: 'Other',
			colors: {
				// [!code highlight:2]
				light: ['#d1d5db', '#9ca3af', '#6b7280', '#4b5563', '#374151'],
				dark: ['#e5e7eb', '#d1d5db', '#9ca3af', '#6b7280', '#4b5563']
			}
		}
	} satisfies ChartConfig;
</script>

<EChartsPieChart
	class="h-full w-full p-4"
	{data}
	dataKey="visitors"
	nameKey="browser"
	config={chartConfig}
>
	<EChartsPieChart.Legend isClickable />
	<EChartsPieChart.Tooltip />
	<EChartsPieChart.Pie isClickable />
</EChartsPieChart>
```

### Donut Chart

### innerRadius={60}

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

	const data = [
		{ browser: 'chrome', visitors: 275 },
		{ browser: 'safari', visitors: 200 },
		{ browser: 'firefox', visitors: 187 },
		{ browser: 'edge', visitors: 173 },
		{ browser: 'other', visitors: 90 }
	];

	const chartConfig = {
		chrome: {
			label: 'Chrome',
			colors: {
				light: ['#3b82f6'],
				dark: ['#60a5fa']
			}
		},
		safari: {
			label: 'Safari',
			colors: {
				light: ['#10b981'],
				dark: ['#34d399']
			}
		},
		firefox: {
			label: 'Firefox',
			colors: {
				light: ['#f59e0b'],
				dark: ['#fbbf24']
			}
		},
		edge: {
			label: 'Edge',
			colors: {
				light: ['#8b5cf6'],
				dark: ['#a78bfa']
			}
		},
		other: {
			label: 'Other',
			colors: {
				light: ['#6b7280'],
				dark: ['#9ca3af']
			}
		}
	} satisfies ChartConfig;
</script>

<EChartsPieChart
	class="h-full w-full p-4"
	{data}
	dataKey="visitors"
	nameKey="browser"
	config={chartConfig}
>
	<EChartsPieChart.Legend isClickable />
	<EChartsPieChart.Tooltip />
	<EChartsPieChart.Pie isClickable innerRadius={60} />
</EChartsPieChart>
```
> 
  

Set `innerRadius` above 0 to create a donut chart — the inner radius carves the hole in the center.




### Padded Sectors

### paddingAngle={4} cornerRadius={8}

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

	const data = [
		{ browser: 'chrome', visitors: 275 },
		{ browser: 'safari', visitors: 200 },
		{ browser: 'firefox', visitors: 187 },
		{ browser: 'edge', visitors: 173 },
		{ browser: 'other', visitors: 90 }
	];

	const chartConfig = {
		chrome: {
			label: 'Chrome',
			colors: {
				light: ['#3b82f6'],
				dark: ['#60a5fa']
			}
		},
		safari: {
			label: 'Safari',
			colors: {
				light: ['#10b981'],
				dark: ['#34d399']
			}
		},
		firefox: {
			label: 'Firefox',
			colors: {
				light: ['#f59e0b'],
				dark: ['#fbbf24']
			}
		},
		edge: {
			label: 'Edge',
			colors: {
				light: ['#8b5cf6'],
				dark: ['#a78bfa']
			}
		},
		other: {
			label: 'Other',
			colors: {
				light: ['#6b7280'],
				dark: ['#9ca3af']
			}
		}
	} satisfies ChartConfig;
</script>

<EChartsPieChart
	class="h-full w-full p-4"
	{data}
	dataKey="visitors"
	nameKey="browser"
	config={chartConfig}
>
	<EChartsPieChart.Legend isClickable />
	<EChartsPieChart.Tooltip />
	<EChartsPieChart.Pie isClickable innerRadius={30} paddingAngle={4} cornerRadius={8} />
</EChartsPieChart>
```
> 
  

Use `paddingAngle` to space sectors apart and `cornerRadius` to round their corners. Combine with `innerRadius` for a modern donut look.




### innerRadius={60} paddingAngle={-25} cornerRadius={99}

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

	const data = [
		{ browser: 'chrome', visitors: 275 },
		{ browser: 'safari', visitors: 200 },
		{ browser: 'firefox', visitors: 187 },
		{ browser: 'edge', visitors: 173 },
		{ browser: 'other', visitors: 90 }
	];

	const chartConfig = {
		chrome: {
			label: 'Chrome',
			colors: {
				light: ['#3b82f6'],
				dark: ['#60a5fa']
			}
		},
		safari: {
			label: 'Safari',
			colors: {
				light: ['#10b981'],
				dark: ['#34d399']
			}
		},
		firefox: {
			label: 'Firefox',
			colors: {
				light: ['#f59e0b'],
				dark: ['#fbbf24']
			}
		},
		edge: {
			label: 'Edge',
			colors: {
				light: ['#8b5cf6'],
				dark: ['#a78bfa']
			}
		},
		other: {
			label: 'Other',
			colors: {
				light: ['#6b7280'],
				dark: ['#9ca3af']
			}
		}
	} satisfies ChartConfig;
</script>

<EChartsPieChart
	class="h-full w-full p-4"
	{data}
	dataKey="visitors"
	nameKey="browser"
	config={chartConfig}
>
	<EChartsPieChart.Legend isClickable />
	<EChartsPieChart.Tooltip />
	<EChartsPieChart.Pie innerRadius={60} paddingAngle={-25} cornerRadius={99} />
</EChartsPieChart>
```
> 
  

Pair a negative `paddingAngle` with a high `cornerRadius` for overlapping, petal-like sectors. A background-colored border separates the petals into a flower-shaped donut.




### Labels

### <Label />

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

	const data = [
		{ browser: 'chrome', visitors: 275 },
		{ browser: 'safari', visitors: 200 },
		{ browser: 'firefox', visitors: 187 },
		{ browser: 'edge', visitors: 173 },
		{ browser: 'other', visitors: 90 }
	];

	const chartConfig = {
		chrome: {
			label: 'Chrome',
			colors: {
				light: ['#3b82f6'],
				dark: ['#60a5fa']
			}
		},
		safari: {
			label: 'Safari',
			colors: {
				light: ['#10b981'],
				dark: ['#34d399']
			}
		},
		firefox: {
			label: 'Firefox',
			colors: {
				light: ['#f59e0b'],
				dark: ['#fbbf24']
			}
		},
		edge: {
			label: 'Edge',
			colors: {
				light: ['#8b5cf6'],
				dark: ['#a78bfa']
			}
		},
		other: {
			label: 'Other',
			colors: {
				light: ['#6b7280'],
				dark: ['#9ca3af']
			}
		}
	} satisfies ChartConfig;
</script>

<EChartsPieChart
	class="h-full w-full p-4"
	{data}
	dataKey="visitors"
	nameKey="browser"
	config={chartConfig}
>
	<EChartsPieChart.Legend isClickable />
	<EChartsPieChart.Tooltip />
	<EChartsPieChart.Pie isClickable innerRadius={30} paddingAngle={4} cornerRadius={8}>
		<!-- [!code highlight:2] -->
		<EChartsPieChart.Label />
	</EChartsPieChart.Pie>
</EChartsPieChart>
```
> 
  

Compose a <code>&lt;Label /&gt;</code> inside the <code>&lt;Pie /&gt;</code> to draw labels on each sector. It shows the sector's value by default; set the <code>&lt;Label /&gt;</code>'s `dataKey` for a different field.




### Outside Labels

### <Label position="outside" />

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

	const data = [
		{ browser: 'chrome', visitors: 275 },
		{ browser: 'safari', visitors: 200 },
		{ browser: 'firefox', visitors: 187 },
		{ browser: 'edge', visitors: 173 },
		{ browser: 'other', visitors: 90 }
	];

	const chartConfig = {
		chrome: {
			label: 'Chrome',
			colors: {
				light: ['#3b82f6'],
				dark: ['#60a5fa']
			}
		},
		safari: {
			label: 'Safari',
			colors: {
				light: ['#10b981'],
				dark: ['#34d399']
			}
		},
		firefox: {
			label: 'Firefox',
			colors: {
				light: ['#f59e0b'],
				dark: ['#fbbf24']
			}
		},
		edge: {
			label: 'Edge',
			colors: {
				light: ['#8b5cf6'],
				dark: ['#a78bfa']
			}
		},
		other: {
			label: 'Other',
			colors: {
				light: ['#6b7280'],
				dark: ['#9ca3af']
			}
		}
	} satisfies ChartConfig;
</script>

<EChartsPieChart
	class="h-full w-full p-4"
	{data}
	dataKey="visitors"
	nameKey="browser"
	config={chartConfig}
>
	<EChartsPieChart.Tooltip />
	<EChartsPieChart.Pie outerRadius="65%" paddingAngle={2} cornerRadius={4}>
		<!-- [!code highlight] -->
		<EChartsPieChart.Label position="outside" />
	</EChartsPieChart.Pie>
</EChartsPieChart>
```
> 
  

Set the <code>&lt;Label /&gt;</code>'s `position` to `"outside"` to move each sector's name past the rim with a leader line — the classic ECharts pie layout ([pie-simple](https://echarts.apache.org/examples/en/editor.html?c=pie-simple)). Outside labels show the sector's name (from `config`) by default; inside labels show its value. Give the <code>&lt;Pie /&gt;</code> a smaller `outerRadius` so the labels have room.




### Ordered dither

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

### renderStyle="dither"

```svelte
<script lang="ts">
	import {
		EChartsPieChart,
		type ChartConfig
	} from '$lib/components/evilcharts/charts/echarts-pie-chart/index.js';
	const data = [
		{ browser: 'chrome', visitors: 46 },
		{ browser: 'safari', visitors: 28 },
		{ browser: 'firefox', visitors: 17 },
		{ browser: 'other', visitors: 9 }
	];
	const config = {
		chrome: { label: 'Chrome', colors: { light: ['#047857'], dark: ['#10b981'] } },
		safari: { label: 'Safari', colors: { light: ['#0369a1'], dark: ['#38bdf8'] } },
		firefox: { label: 'Firefox', colors: { light: ['#be123c'], dark: ['#f43f5e'] } },
		other: { label: 'Other', colors: { light: ['#a16207'], dark: ['#facc15'] } }
	} satisfies ChartConfig;
</script>

<EChartsPieChart
	{data}
	{config}
	dataKey="visitors"
	nameKey="browser"
	renderStyle="dither"
	bloom="low"
	class="h-full w-full p-4"
>
	<EChartsPieChart.Legend isClickable /><EChartsPieChart.Tooltip />
	<EChartsPieChart.Pie innerRadius={54} ditherVariant="gradient" isClickable />
</EChartsPieChart>
```

## API Reference

The chart has several parts; the props below are grouped by part. Regardless of renderer, each part is declarative config the root compiles, but the API mirrors the LayerChart sibling one-to-one.

### EChartsPieChart

The root container. It owns the data, shared selection state, loading skeleton, and intro reveal. Everything visual is composed as its children and compiled into the ECharts option.


  ### `data` (required)

type: `TData[]`

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

type: `keyof TData & string`

The data key for sector values — typically the numbers that size each sector.
  ### `nameKey` (required)

type: `keyof TData & string`

The data key for sector names, used in labels and legend. Each name must match a key in `config`.
  ### `config` (required)

type: `ChartConfig`

Defines the chart's sectors. Each key matches a value from your `nameKey` field, 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 — `<Legend />`, `<Tooltip />`, `<Background />`, and one `<Pie />`.
  ### `class`

type: `string`

Extra 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 sectors.
  ### `ditherCellSize`

type: `number` · default: `2`

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

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

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

type: `boolean` · default: `true`

Master switch for the intro draw-in. Pass `false` to render instantly. Not on the LayerChart sibling — it's the ECharts off-switch. The OS reduce-motion preference disables the entrance automatically.
  ### `defaultSelectedSector`

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

The sector selected on first render.
  ### `selectedSector`

type: `string | null`

Controlled selection. Leave it undefined to let the chart manage selection; pass `null` to clear a controlled selection. Pair it with `onSelectionChange` to keep your own UI, such as a custom legend or stat cards, in sync.
  ### `onSelectionChange`



Fires when a sector is selected or deselected via a clickable `<Pie />` sector or `<Legend />` entry. Receives an object with `dataKey` (sector name) and `value` (sector value), or `null` when deselected.
  ### `isLoading`

type: `boolean` · default: `false`

Shows the animated loading skeleton while data loads.
  ### `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.


### Pie

The pie series. Carries its own shape and clickability. When clickable, the selected sector pops radially outward. Compose a `<Label />` inside it to draw labels on each sector.


  ### `variant`

type: `"gradient"` · default: `"gradient"`

The fill style for the sectors. Each paints a diagonal gradient from its `config` colors — solid for a single color, or a multi-stop gradient across the sector.
  ### `innerRadius`

type: `number | string` · default: `0`

The pie's inner radius. Set above 0 for a donut. Accepts a number (pixels) or percentage string.
  ### `outerRadius`

type: `number | string` · default: `"80%"`

The pie's outer radius. Accepts a number (pixels) or percentage string.
  ### `cornerRadius`

type: `number` · default: `0`

The border radius for the corners of each sector in pixels.
  ### `paddingAngle`

type: `number` · default: `0`

The space between sectors. Positive values draw a constant-width, background-colored gap (parallel-edged from rim to center, not a wedge-shaped angular pad). Negative values overlap sectors into petals, kept distinct by a background-colored border.
  ### `startAngle`

type: `number` · default: `0`

The starting angle of the pie in degrees (0 is 3 o'clock, 90 is 12 o'clock). Sectors sweep counterclockwise from here.
  ### `endAngle`

type: `number` · default: `360`

The ending angle of the pie in degrees. Set to less than 360 for a partial pie.
  ### `isClickable`

type: `boolean` · default: `false`

Enables clicking a sector to select/deselect it. The selected sector pops radially outward from the center while the others dim.
  ### `children`

type: `Snippet`

Optional `<Label />` composition that draws labels on each sector.


### Label

Per-sector labels composed inside a `<Pie />`. It renders nothing on its own — the parent `<Pie />` reads its props and draws the labels, either on each sector or outside the rim with a leader line.


  ### `position`

type: `"inside" | "outside"` · default: `"inside"`

Where the labels sit. `"inside"` draws the value on each sector; `"outside"` moves the sector's name past the rim with a leader line (the classic ECharts pie layout). When `"outside"` and no `dataKey` is set, the label shows the sector's name from `config` instead of its value.
  ### `dataKey`

type: `string`

The data key for label text. When omitted, inside labels fall back to the chart's `dataKey` (the sector value), outside labels to the sector's name.


### Tooltip

The hover tooltip. Its presence enables the tooltip; omit it and none shows. Hidden automatically while the chart is loading.


  ### `variant`

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

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

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

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

type: `number`

When set, the tooltip is visible by default at the specified sector index.
  ### `position`

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

How the tooltip is anchored. `"variable"` follows the pointer (the default); `"fixed"` pins the tooltip near the top and only tracks the pointer's X.


### Legend

The sector legend, rendered as HTML over the chart surface. Its presence enables the legend; omit it and none shows. When `isClickable` is set, each entry toggles selection of its sector.


  ### `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: `"center"`

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

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

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

type: `boolean` · default: `false`

Lets each legend entry toggle selection of its sector.


### Background

An optional decorative SVG pattern drawn behind the pie. Its presence renders the pattern; omit it and none shows.


  ### `variant`

type: `BackgroundVariant` · default: `"dots"`

The background pattern style — one of `"dots"`, `"grid"`, `"cross-hatch"`, `"diagonal-lines"`, `"plus"`, `"falling-triangles"`, `"4-pointed-star"`, `"tiny-checkers"`, `"overlapping-circles"`, `"wiggle-lines"`, or `"bubbles"`.

