Zaidan

Command Palette

Search for a command to run...

GitHub69

A collection of chart components built on Apache ECharts with SVG rendering for crisp graphics at any resolution.

Overview

Unlike shadcn's Recharts-based implementation, this chart component uses Apache ECharts with its powerful configuration-based API. The key benefits include:

  • SVG Rendering: Crisp graphics that scale perfectly on any display
  • Rich Chart Types: Bar, line, area, pie, radar, scatter, and more
  • Theme Integration: Uses CSS variables for automatic light/dark mode support
  • Responsive by Default: Charts automatically resize with their container
  • Familiar Patterns: Similar ChartConfig and ChartContainer patterns from shadcn

Installation

Terminal window
bunx shadcn@latest add @zaidan/chart

You'll also need to install the ECharts dependency:

Terminal window
bun add echarts

Chart Configuration

The ChartConfig type allows you to define labels, colors, and icons for each data series:

import type { ChartConfig } from "@/registry/kobalte/ui/chart"
const chartConfig: ChartConfig = {
desktop: {
label: "Desktop",
color: "var(--chart-1)",
},
mobile: {
label: "Mobile",
color: "var(--chart-2)",
},
}

Theme-aware Colors

You can define different colors for light and dark mode:

const chartConfig: ChartConfig = {
revenue: {
label: "Revenue",
theme: {
light: "oklch(0.646 0.222 41.116)",
dark: "oklch(0.488 0.243 264.376)",
},
},
}

Default Presets

The component exports several preset configurations to quickly style your charts:

Colors

import { chartColors } from "@/registry/kobalte/ui/chart"
// Uses --chart-1 through --chart-5 CSS variables
const option = {
color: chartColors,
// ...
}

Tooltip

import { chartTooltipDefaults } from "@/registry/kobalte/ui/chart"
const option = {
tooltip: {
...chartTooltipDefaults,
trigger: "axis",
},
// ...
}

Axes

import { chartXAxisDefaults, chartYAxisDefaults } from "@/registry/kobalte/ui/chart"
const option = {
xAxis: {
...chartXAxisDefaults,
type: "category",
data: ["Jan", "Feb", "Mar"],
},
yAxis: {
...chartYAxisDefaults,
type: "value",
},
// ...
}

Grid

import { chartGridDefaults } from "@/registry/kobalte/ui/chart"
const option = {
grid: chartGridDefaults,
// ...
}

Legend

import { chartLegendDefaults } from "@/registry/kobalte/ui/chart"
const option = {
legend: {
...chartLegendDefaults,
data: ["Series A", "Series B"],
},
// ...
}

Event Handling

Handle chart interactions using the eventHandlers prop:

<ChartContainer
option={option}
eventHandlers={{
click: (params) => {
console.log("Clicked:", params)
},
}}
/>

Loading State

Show a loading animation while fetching data:

<ChartContainer
option={option}
loading={isLoading()}
loadingOptions={{
text: "Loading...",
}}
/>

Accessing the Chart Instance

Use the onInit callback to access the ECharts instance directly:

<ChartContainer
option={option}
onInit={(chart) => {
// Access the ECharts instance
chart.on("click", handleClick)
}}
/>

Chart Types

Bar Chart

const option = {
color: chartColors,
xAxis: { type: "category", data: categories },
yAxis: { type: "value" },
series: [{
type: "bar",
data: values,
itemStyle: { borderRadius: [4, 4, 0, 0] },
}],
}

Line Chart

const option = {
color: chartColors,
xAxis: { type: "category", data: categories, boundaryGap: false },
yAxis: { type: "value" },
series: [{
type: "line",
data: values,
smooth: true,
}],
}

Area Chart

const option = {
series: [{
type: "line",
data: values,
areaStyle: { opacity: 0.3 },
}],
}

Pie Chart

const option = {
color: chartColors,
series: [{
type: "pie",
radius: ["40%", "70%"],
data: pieData,
itemStyle: { borderRadius: 8 },
}],
}

Radar Chart

const option = {
color: chartColors,
radar: {
indicator: [
{ name: "Sales", max: 100 },
{ name: "Admin", max: 100 },
// ...
],
},
series: [{
type: "radar",
data: radarData,
}],
}

Theming

Charts automatically adapt to light/dark mode using CSS variables. The default chart colors are defined as:

:root {
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
}
[data-kb-theme="dark"] {
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
}

API Reference

ChartContainer Props

PropTypeDefaultDescription
optionEChartsOptionRequiredECharts configuration object
configChartConfig{}Chart series configuration
loadingbooleanfalseShow loading animation
loadingOptionsobject{}ECharts loading options
setOptionOptsSetOptionOpts{ notMerge: true }Options for setOption
onInit(chart: ECharts) => void-Callback with chart instance
eventHandlersRecord<string, Function>-Chart event handlers
classstring-Additional CSS classes

ChartConfig Type

type ChartConfig = {
[key: string]: {
label?: JSX.Element | string
icon?: Component
color?: string
theme?: { light: string; dark: string }
}
}

Examples

Here are the source code of all the examples from the preview page:

Area Chart

import type { ECBasicOption } from "echarts/types/dist/shared";
import {
type ChartConfig,
ChartContainer,
chartColors,
chartGridDefaults,
chartTooltipDefaults,
chartXAxisDefaults,
chartYAxisDefaults,
} from "~/components/ui/chart";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
function ChartAreaExample() {
const option: ECBasicOption = {
color: chartColors,
tooltip: {
...chartTooltipDefaults,
trigger: "axis" as const,
},
grid: {
...chartGridDefaults,
bottom: "15%",
},
xAxis: {
...chartXAxisDefaults,
type: "category" as const,
data: monthlyData.map((d) => d.month.slice(0, 3)),
boundaryGap: false,
},
yAxis: {
...chartYAxisDefaults,
axisLabel: {
show: false,
},
},
series: [
{
name: "Desktop",
type: "line" as const,
data: monthlyData.map((d) => d.desktop),
smooth: true,
areaStyle: {
opacity: 0.3,
},
lineStyle: {
width: 2,
},
emphasis: { disabled: true },
},
],
};
return (
<Example title="Area Chart">
<Card class="w-full">
<CardHeader>
<CardTitle>Area Chart - Stacked</CardTitle>
<CardDescription>Showing total visitors for the last 6 months</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer option={option} config={chartConfig} class="h-[300px] w-full" />
</CardContent>
<CardFooter>
<div class="flex w-full items-start gap-2">
<div class="grid gap-2">
<div class="flex items-center gap-2 font-medium leading-none">
Trending up by 5.2% this month <TrendingUpIcon class="size-4" />
</div>
<div class="flex items-center gap-2 text-muted-foreground leading-none">
January - June 2024
</div>
</div>
</div>
</CardFooter>
</Card>
</Example>
);
}

Bar Chart

import type { ECBasicOption } from "echarts/types/dist/shared";
import {
type ChartConfig,
ChartContainer,
chartColors,
chartGridDefaults,
chartTooltipDefaults,
chartXAxisDefaults,
chartYAxisDefaults,
} from "~/components/ui/chart";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
function ChartBarExample() {
const option = {
color: chartColors,
tooltip: {
...chartTooltipDefaults,
trigger: "axis" as const,
},
grid: {
...chartGridDefaults,
bottom: "15%",
},
xAxis: {
...chartXAxisDefaults,
type: "category" as const,
data: monthlyData.map((d) => d.month.slice(0, 3)),
},
yAxis: {
...chartYAxisDefaults,
axisLabel: {
show: false,
},
},
series: [
{
name: "Desktop",
type: "bar" as const,
data: monthlyData.map((d) => d.desktop),
barWidth: "40%",
itemStyle: {
borderRadius: [4, 4, 0, 0],
},
// Disable emphasis to prevent flickering when using CSS variables
// ECharts can't calculate emphasis colors from CSS variables
emphasis: { disabled: true },
},
{
name: "Mobile",
type: "bar" as const,
data: monthlyData.map((d) => d.mobile),
barWidth: "40%",
itemStyle: {
borderRadius: [4, 4, 0, 0],
},
emphasis: { disabled: true },
},
],
};
return (
<Example title="Bar Chart">
<Card class="w-full">
<CardHeader>
<CardTitle>Bar Chart - Multiple</CardTitle>
<CardDescription>January - June 2024</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer option={option} config={chartConfig} class="h-[300px] w-full" />
</CardContent>
<CardFooter class="flex-col items-start gap-2">
<div class="flex gap-2 font-medium leading-none">
Trending up by 5.2% this month <TrendingUpIcon class="size-4" />
</div>
<div class="text-muted-foreground leading-none">
Showing total visitors for the last 6 months
</div>
</CardFooter>
</Card>
</Example>
);
}

Line Chart

import type { ECBasicOption } from "echarts/types/dist/shared";
import {
type ChartConfig,
ChartContainer,
chartColors,
chartGridDefaults,
chartLegendDefaults,
chartTooltipDefaults,
chartXAxisDefaults,
chartYAxisDefaults,
} from "~/components/ui/chart";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
function ChartLineExample() {
const option = {
color: chartColors,
tooltip: {
...chartTooltipDefaults,
trigger: "axis" as const,
},
legend: {
...chartLegendDefaults,
data: ["Desktop", "Mobile"],
bottom: 0,
},
grid: {
...chartGridDefaults,
bottom: "15%",
},
xAxis: {
...chartXAxisDefaults,
type: "category" as const,
data: monthlyData.map((d) => d.month.slice(0, 3)),
boundaryGap: false,
},
yAxis: {
...chartYAxisDefaults,
type: "value" as const,
},
series: [
{
name: "Desktop",
type: "line" as const,
data: monthlyData.map((d) => d.desktop),
smooth: true,
symbolSize: 8,
lineStyle: {
width: 2,
},
emphasis: { disabled: true },
},
{
name: "Mobile",
type: "line" as const,
data: monthlyData.map((d) => d.mobile),
smooth: true,
symbolSize: 8,
lineStyle: {
width: 2,
},
emphasis: { disabled: true },
},
],
};
return (
<Example title="Line Chart">
<Card class="w-full">
<CardHeader>
<CardTitle>Line Chart</CardTitle>
<CardDescription>Showing total visitors for the last 6 months</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer option={option} config={chartConfig} class="h-[300px] w-full" />
</CardContent>
</Card>
</Example>
);
}

Pie Chart

import type { ECBasicOption } from "echarts/types/dist/shared";
import {
type ChartConfig,
ChartContainer,
chartColors,
chartLegendDefaults,
} from "~/components/ui/chart";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
function ChartPieExample() {
const pieData = [
{ name: "Chrome", value: 275 },
{ name: "Safari", value: 200 },
{ name: "Firefox", value: 287 },
{ name: "Edge", value: 173 },
{ name: "Other", value: 190 },
];
const pieConfig: ChartConfig = {
Chrome: { label: "Chrome", color: "var(--chart-1)" },
Safari: { label: "Safari", color: "var(--chart-2)" },
Firefox: { label: "Firefox", color: "var(--chart-3)" },
Edge: { label: "Edge", color: "var(--chart-4)" },
Other: { label: "Other", color: "var(--chart-5)" },
};
const option = {
color: chartColors,
tooltip: {
trigger: "item" as const,
backgroundColor: "var(--background)",
borderColor: "var(--border)",
borderWidth: 1,
textStyle: {
color: "var(--foreground)",
fontSize: 12,
},
},
legend: {
...chartLegendDefaults,
orient: "horizontal" as const,
bottom: 0,
},
series: [
{
name: "Visitors",
type: "pie" as const,
radius: ["40%", "70%"],
center: ["50%", "45%"],
avoidLabelOverlap: true,
itemStyle: {
borderRadius: 8,
borderColor: "var(--background)",
borderWidth: 2,
},
label: {
show: false,
},
// Disable emphasis to prevent flickering when using CSS variables
emphasis: { disabled: true },
labelLine: {
show: false,
},
data: pieData,
},
],
};
return (
<Example title="Pie Chart">
<Card class="w-full">
<CardHeader>
<CardTitle>Pie Chart - Donut</CardTitle>
<CardDescription>Browser usage for the last month</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer option={option} config={pieConfig} class="h-[300px] w-full" />
</CardContent>
</Card>
</Example>
);
}

Radar Chart

import type { ECBasicOption } from "echarts/types/dist/shared";
import {
ChartContainer,
chartColors,
chartLegendDefaults,
} from "~/components/ui/chart";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
function ChartRadarExample() {
const radarIndicators = [
{ name: "Sales", max: 100 },
{ name: "Admin", max: 100 },
{ name: "Tech", max: 100 },
{ name: "Support", max: 100 },
{ name: "Dev", max: 100 },
{ name: "Marketing", max: 100 },
];
const option = {
color: chartColors,
tooltip: {
trigger: "item" as const,
backgroundColor: "var(--background)",
borderColor: "var(--border)",
borderWidth: 1,
textStyle: {
color: "var(--foreground)",
fontSize: 12,
},
},
legend: {
...chartLegendDefaults,
data: ["Team A", "Team B"],
bottom: 0,
},
radar: {
indicator: radarIndicators,
shape: "polygon" as const,
axisName: {
color: "var(--muted-foreground)",
},
splitLine: {
lineStyle: {
color: "var(--border)",
},
},
splitArea: {
show: false,
},
axisLine: {
lineStyle: {
color: "var(--border)",
},
},
},
series: [
{
type: "radar" as const,
emphasis: { disabled: true },
data: [
{
name: "Team A",
value: [80, 50, 70, 84, 60, 55],
areaStyle: {
opacity: 0.3,
},
},
{
name: "Team B",
value: [60, 75, 90, 40, 85, 70],
areaStyle: {
opacity: 0.3,
},
},
],
},
],
};
return (
<Example title="Radar Chart">
<Card class="w-full">
<CardHeader>
<CardTitle>Radar Chart</CardTitle>
<CardDescription>Team performance comparison</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer option={option} class="h-[300px] w-full" />
</CardContent>
</Card>
</Example>
);
}