Area Chart
Filled lines for totals over time: a 6-26% wash of the series hue under a 2px line, in natural, linear and step curves, stacked and expanded.
Area charts show totals over time as a filled line. The fill is a gradient wash of the series colour and the 2px line carries the value; the tooltip lists every series at the hovered X. Installation and the shared API live on the Chart page.
Interactive
Section titled “Interactive”import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle,} from "@/components/ui/card";import { type ChartConfig, ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartTooltipContent,} from "@/components/ui/chart";import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from "@/components/ui/select";import * as React from "react";import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";
// 91 days (Apr 1 - Jun 30, 2024) of sample visitors.const data = Array.from({ length: 91 }, (_, i) => { const date = new Date(Date.UTC(2024, 3, 1 + i)).toISOString().slice(0, 10); const desktop = Math.round( 220 + 110 * Math.sin(i / 3.1) + 70 * Math.sin(i / 7.3 + 1) + (i % 5) * 9, ); const mobile = Math.round( 170 + 90 * Math.sin(i / 2.7 + 2) + 60 * Math.cos(i / 6.1) + (i % 4) * 11, ); return { date, desktop, mobile };});
const config = { desktop: { label: "Desktop", color: "var(--chart-1)" }, mobile: { label: "Mobile", color: "var(--chart-2)" },} satisfies ChartConfig;
const ranges = [ { value: "90d", label: "Last 3 months", days: 90 }, { value: "30d", label: "Last 30 days", days: 30 }, { value: "7d", label: "Last 7 days", days: 7 },];
export default function Demo() { const [range, setRange] = React.useState("90d"); const days = ranges.find((r) => r.value === range)?.days ?? 90; const filtered = data.slice(-days);
return ( <Card className="w-full max-w-3xl gap-0 py-0"> <CardHeader className="gap-1 border-b px-6 pt-5.5 pb-5.5"> <CardTitle className="font-semibold text-base tracking-[-0.2px]"> Area Chart - Interactive </CardTitle> <CardDescription className="text-[13.5px]"> Showing total visitors for the last 3 months </CardDescription> <CardAction className="self-center"> <Select value={range} onValueChange={(value) => value && setRange(value)} items={ranges} > <SelectTrigger className="w-40" aria-label="Select a range"> <SelectValue /> </SelectTrigger> <SelectContent alignItemWithTrigger={false}> {ranges.map((r) => ( <SelectItem key={r.value} value={r.value}> {r.label} </SelectItem> ))} </SelectContent> </Select> </CardAction> </CardHeader> <CardContent className="items-center px-5 pt-2.5 pb-4"> <ChartContainer config={config} className="aspect-auto h-64 w-full"> <AreaChart data={filtered} margin={{ left: 12, right: 12 }}> <defs> <linearGradient id="fill-desktop-main" x1="0" y1="0" x2="0" y2="1" > <stop offset="0" stopColor="var(--color-desktop)" stopOpacity={0.26} /> <stop offset="1" stopColor="var(--color-desktop)" stopOpacity={0.03} /> </linearGradient> <linearGradient id="fill-mobile-main" x1="0" y1="0" x2="0" y2="1"> <stop offset="0" stopColor="var(--color-mobile)" stopOpacity={0.26} /> <stop offset="1" stopColor="var(--color-mobile)" stopOpacity={0.03} /> </linearGradient> </defs> <CartesianGrid vertical={false} /> <XAxis dataKey="date" tickLine={false} axisLine={false} tickMargin={8} minTickGap={32} tickFormatter={(value) => new Date(value).toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC", }) } /> <ChartTooltip content={ <ChartTooltipContent indicator="dot" labelFormatter={(value) => new Date(value).toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC", }) } /> } /> <ChartLegend content={<ChartLegendContent />} /> <Area dataKey="desktop" type="natural" fill="url(#fill-desktop-main)" fillOpacity={1} stroke="var(--color-desktop)" strokeWidth={2} /> <Area dataKey="mobile" type="natural" fill="url(#fill-mobile-main)" fillOpacity={1} stroke="var(--color-mobile)" strokeWidth={2} /> </AreaChart> </ChartContainer> </CardContent> </Card> );}<script setup lang="ts">import { CurveType } from "@unovis/ts";import { VisArea, VisAxis, VisLine, VisXYContainer } from "@unovis/vue";import { computed, ref } from "vue";import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle,} from "@/components/ui/card";import { type ChartConfig, ChartContainer, ChartCrosshair, ChartLegendContent, ChartTooltip, ChartTooltipContent, componentToString,} from "@/components/ui/chart";import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from "@/components/ui/select";
type Row = { date: string; desktop: number; mobile: number };
// 91 days (Apr 1 - Jun 30, 2024) of sample visitors.const data: Row[] = Array.from({ length: 91 }, (_, i) => { const date = new Date(Date.UTC(2024, 3, 1 + i)).toISOString().slice(0, 10); const desktop = Math.round( 220 + 110 * Math.sin(i / 3.1) + 70 * Math.sin(i / 7.3 + 1) + (i % 5) * 9, ); const mobile = Math.round( 170 + 90 * Math.sin(i / 2.7 + 2) + 60 * Math.cos(i / 6.1) + (i % 4) * 11, ); return { date, desktop, mobile };});
const config = { desktop: { label: "Desktop", color: "var(--chart-1)" }, mobile: { label: "Mobile", color: "var(--chart-2)" },} satisfies ChartConfig;
const ranges = [ { value: "90d", label: "Last 3 months", days: 90 }, { value: "30d", label: "Last 30 days", days: 30 }, { value: "7d", label: "Last 7 days", days: 7 },];const range = ref("90d");const filtered = computed(() => data.slice(-(ranges.find((r) => r.value === range.value)?.days ?? 90)));
const fills = ["url(#fill-desktop-main)", "url(#fill-mobile-main)"];const svgDefs = '<linearGradient id="fill-desktop-main" x1="0" y1="0" x2="0" y2="1"><stop offset="0" style="stop-color:var(--chart-1);stop-opacity:.26"/><stop offset="1" style="stop-color:var(--chart-1);stop-opacity:.03"/></linearGradient><linearGradient id="fill-mobile-main" x1="0" y1="0" x2="0" y2="1"><stop offset="0" style="stop-color:var(--chart-2);stop-opacity:.26"/><stop offset="1" style="stop-color:var(--chart-2);stop-opacity:.03"/></linearGradient>';
const x = (_: Row, i: number) => i;const label = (i: number) => new Date(`${filtered.value[Math.round(i)]?.date}`).toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC", });const template = componentToString(config, ChartTooltipContent, { indicator: "dot", labelFormatter: (i) => label(Number(i)),});</script>
<template> <Card class="w-full max-w-3xl gap-0 py-0"> <CardHeader class="gap-1 border-b px-6 pt-5.5 pb-5.5"> <CardTitle class="font-semibold text-base tracking-[-0.2px]">Area Chart - Interactive</CardTitle> <CardDescription class="text-[13.5px]">Showing total visitors for the last 3 months</CardDescription> <CardAction class="self-center"> <Select v-model="range"> <SelectTrigger class="w-40" aria-label="Select a range"> <SelectValue /> </SelectTrigger> <SelectContent> <SelectItem v-for="r in ranges" :key="r.value" :value="r.value">{{ r.label }}</SelectItem> </SelectContent> </Select> </CardAction> </CardHeader> <CardContent class="items-center px-5 pt-2.5 pb-4"> <ChartContainer :config="config" class="aspect-auto h-64 w-full" cursor> <VisXYContainer :data="filtered" :margin="{ left: 12, right: 12 }" :y-domain="[0, undefined]" :svg-defs="svgDefs" > <VisArea :x="x" :y="(d: Row) => d.desktop" :color="() => fills[0]" :opacity="1" :curve-type="CurveType.Natural" /> <VisLine :x="x" :y="(d: Row) => d.desktop" :color="config.desktop.color" :curve-type="CurveType.Natural" /> <VisArea :x="x" :y="(d: Row) => d.mobile" :color="() => fills[1]" :opacity="1" :curve-type="CurveType.Natural" /> <VisLine :x="x" :y="(d: Row) => d.mobile" :color="config.mobile.color" :curve-type="CurveType.Natural" /> <VisAxis type="x" :x="x" :num-ticks="range === '7d' ? 7 : 8" :tick-format="(i: number) => label(i)" :tick-line="false" :domain-line="false" :grid-line="false" /> <VisAxis type="y" :num-ticks="4" :tick-format="() => ''" :tick-line="false" :domain-line="false" /> <ChartTooltip /> <ChartCrosshair :x="x" :y="[(d: Row) => d.desktop, (d: Row) => d.mobile]" :template="template" :color="(_: Row, i: number) => [config.desktop.color, config.mobile.color][i]" /> </VisXYContainer> <ChartLegendContent /> </ChartContainer> </CardContent> </Card></template><script lang="ts"> import { Area, AreaChart } from "layerchart"; import * as Card from "$lib/components/ui/card"; import * as Chart from "$lib/components/ui/chart"; import { Select, SelectContent, SelectItem, SelectTrigger } from "$lib/components/ui/select";
// 91 days (Apr 1 - Jun 30, 2024) of sample visitors. const all = Array.from({ length: 91 }, (_, i) => { const date = new Date(Date.UTC(2024, 3, 1 + i)).toISOString().slice(0, 10); const desktop = Math.round( 220 + 110 * Math.sin(i / 3.1) + 70 * Math.sin(i / 7.3 + 1) + (i % 5) * 9, ); const mobile = Math.round( 170 + 90 * Math.sin(i / 2.7 + 2) + 60 * Math.cos(i / 6.1) + (i % 4) * 11, ); return { date, desktop, mobile }; });
const config = { desktop: { label: "Desktop", color: "var(--chart-1)" }, mobile: { label: "Mobile", color: "var(--chart-2)" }, } satisfies Chart.ChartConfig;
const ranges: Record<string, { label: string; days: number }> = { "90d": { label: "Last 3 months", days: 90 }, "30d": { label: "Last 30 days", days: 30 }, "7d": { label: "Last 7 days", days: 7 }, }; let range = $state("90d"); const data = $derived(all.slice(-ranges[range].days).map((d, i) => ({ ...d, i }))); const label = (i: number) => new Date(`${data[Math.round(i)]?.date}`).toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC", });
// d3 `curveNatural` (natural cubic spline); d3-shape is not a dependency of the docs app. function controlPoints(p: number[]): [number[], number[]] { const n = p.length - 1; const a = new Array<number>(n); const b = new Array<number>(n); const r = new Array<number>(n); a[0] = 0; b[0] = 2; r[0] = p[0] + 2 * p[1]; for (let i = 1; i < n - 1; ++i) { a[i] = 1; b[i] = 4; r[i] = 4 * p[i] + 2 * p[i + 1]; } a[n - 1] = 2; b[n - 1] = 7; r[n - 1] = 8 * p[n - 1] + p[n]; for (let i = 1; i < n; ++i) { const m = a[i] / b[i - 1]; b[i] -= m; r[i] -= m * r[i - 1]; } a[n - 1] = r[n - 1] / b[n - 1]; for (let i = n - 2; i >= 0; --i) a[i] = (r[i] - a[i + 1]) / b[i]; b[n - 1] = (p[n] + a[n - 1]) / 2; for (let i = 0; i < n - 1; ++i) b[i] = 2 * p[i + 1] - a[i + 1]; return [a, b]; }
// biome-ignore lint/suspicious/noExplicitAny: d3 curve factory shape const curveNatural = (context: any) => { let line = Number.NaN; let xs: number[] = []; let ys: number[] = []; return { areaStart() { line = 0; }, areaEnd() { line = Number.NaN; }, lineStart() { xs = []; ys = []; }, lineEnd() { const n = xs.length; if (n) { if (line) context.lineTo(xs[0], ys[0]); else context.moveTo(xs[0], ys[0]); if (n === 2) context.lineTo(xs[1], ys[1]); else if (n > 2) { const [px0, px1] = controlPoints(xs); const [py0, py1] = controlPoints(ys); for (let i = 0; i < n - 1; ++i) { context.bezierCurveTo(px0[i], py0[i], px1[i], py1[i], xs[i + 1], ys[i + 1]); } } } if (line || (line !== 0 && n === 1)) context.closePath(); line = 1 - line; }, point(px: number, py: number) { xs.push(+px); ys.push(+py); }, }; };</script>
<Card.Root class="w-full max-w-3xl gap-0 py-0"> <Card.Header class="gap-1 border-b px-6 pt-5.5 pb-5.5"> <Card.Title class="font-semibold text-base tracking-[-0.2px]">Area Chart - Interactive</Card.Title> <Card.Description class="text-[13.5px]">Showing total visitors for the last 3 months</Card.Description> <Card.Action class="self-center"> <Select type="single" bind:value={range}> <SelectTrigger class="w-40" aria-label="Select a range"> {ranges[range].label} </SelectTrigger> <SelectContent> {#each Object.entries(ranges) as [value, r]} <SelectItem {value} label={r.label} /> {/each} </SelectContent> </Select> </Card.Action> </Card.Header> <Card.Content class="items-center px-5 pt-2.5 pb-4"> <Chart.Container {config} class="aspect-auto h-64 w-full"> <AreaChart {data} x="i" seriesLayout="overlap" legend series={[ { key: "desktop", label: config.desktop.label, color: "var(--color-desktop)" }, { key: "mobile", label: config.mobile.label, color: "var(--color-mobile)" }, ]} padding={{ left: 12, right: 12, top: 4, bottom: 20 }} props={{ xAxis: { format: (v: number) => label(v), ticks: range === "7d" ? 7 : 8 }, yAxis: { format: () => "" }, }} > {#snippet marks({ context })} <defs> <linearGradient id="fill-desktop-main" x1="0" y1="0" x2="0" y2="1"> <stop offset="0" style="stop-color: var(--chart-1); stop-opacity: 0.26" /> <stop offset="1" style="stop-color: var(--chart-1); stop-opacity: 0.03" /> </linearGradient> <linearGradient id="fill-mobile-main" x1="0" y1="0" x2="0" y2="1"> <stop offset="0" style="stop-color: var(--chart-2); stop-opacity: 0.26" /> <stop offset="1" style="stop-color: var(--chart-2); stop-opacity: 0.03" /> </linearGradient> </defs> {#each context.series.visibleSeries as s (s.key)} <Area seriesKey={s.key} curve={curveNatural} fill="url(#fill-{s.key}-main)" fillOpacity={1} line={{ class: "stroke-2" }} /> {/each} {/snippet} {#snippet tooltip()} <Chart.Tooltip indicator="dot" labelFormatter={(v: number) => label(v)} /> {/snippet} </AreaChart> </Chart.Container> </Card.Content></Card.Root>import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle,} from "@/components/ui/card";import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent,} from "@/components/ui/chart";import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";import { IconPlaceholder } from "@/edmi/icon-placeholder";
const data = [ { month: "January", desktop: 186, mobile: 80 }, { month: "February", desktop: 305, mobile: 200 }, { month: "March", desktop: 237, mobile: 120 }, { month: "April", desktop: 73, mobile: 190 }, { month: "May", desktop: 209, mobile: 130 }, { month: "June", desktop: 214, mobile: 140 },];
const config = { desktop: { label: "Desktop", color: "var(--chart-1)" },} satisfies ChartConfig;
export default function Demo() { return ( <Card className="w-full max-w-sm gap-0 py-0"> <CardHeader className="gap-1 px-6 pt-5.5 pb-0"> <CardTitle className="font-semibold text-base tracking-[-0.2px]"> Area Chart </CardTitle> <CardDescription className="text-[13.5px]"> Showing total visitors for the last 6 months </CardDescription> </CardHeader> <CardContent className="items-center px-5 pt-2.5 pb-0"> <ChartContainer config={config} className="aspect-auto h-44 w-full"> <AreaChart accessibilityLayer data={data} margin={{ left: 12, right: 12 }} > <defs> <linearGradient id="fill-desktop-default" x1="0" y1="0" x2="0" y2="1" > <stop offset="0" stopColor="var(--color-desktop)" stopOpacity={0.26} /> <stop offset="1" stopColor="var(--color-desktop)" stopOpacity={0.03} /> </linearGradient> </defs> <CartesianGrid vertical={false} /> <XAxis dataKey="month" tickLine={false} axisLine={false} tickMargin={8} tickFormatter={(value) => value.slice(0, 3)} /> <ChartTooltip content={<ChartTooltipContent indicator="line" />} /> <Area dataKey="desktop" type="natural" fill="url(#fill-desktop-default)" fillOpacity={1} stroke="var(--color-desktop)" strokeWidth={2} /> </AreaChart> </ChartContainer> </CardContent> <CardFooter className="flex-col items-stretch gap-1 px-6 pt-3.5 pb-5.5 text-[13.5px]"> <b className="flex items-center gap-2 font-medium"> Trending up by 5.2% this month <IconPlaceholder lucide="TrendingUpIcon" tabler="IconTrendingUp" hugeicons="AnalyticsUpIcon" phosphor="TrendUpIcon" remixicon="RiLineChartLine" className="size-4" /> </b> <span className="text-muted-foreground">January - June 2024</span> </CardFooter> </Card> );}<script setup lang="ts">import { CurveType } from "@unovis/ts";import { VisArea, VisAxis, VisLine, VisXYContainer } from "@unovis/vue";import { TrendingUpIcon } from "@lucide/vue";import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle,} from "@/components/ui/card";import { type ChartConfig, ChartContainer, ChartCrosshair, ChartTooltip, ChartTooltipContent, componentToString,} from "@/components/ui/chart";
type Row = { month: string; desktop: number; mobile: number };
const data: Row[] = [ { month: "January", desktop: 186, mobile: 80 }, { month: "February", desktop: 305, mobile: 200 }, { month: "March", desktop: 237, mobile: 120 }, { month: "April", desktop: 73, mobile: 190 }, { month: "May", desktop: 209, mobile: 130 }, { month: "June", desktop: 214, mobile: 140 },];
const config = { desktop: { label: "Desktop", color: "var(--chart-1)" },} satisfies ChartConfig;
// Gradient washes (6-26%) under the 2px line.const fills = ["url(#fill-desktop-default)"];const svgDefs = '<linearGradient id="fill-desktop-default" x1="0" y1="0" x2="0" y2="1"><stop offset="0" style="stop-color:var(--chart-1);stop-opacity:.26"/><stop offset="1" style="stop-color:var(--chart-1);stop-opacity:.03"/></linearGradient>';
const x = (_: Row, i: number) => i;const tickFormat = (i: number) => data[i]?.month.slice(0, 3) ?? "";const template = componentToString(config, ChartTooltipContent, { indicator: "line", labelFormatter: (i) => data[Number(i)]?.month ?? "",});</script>
<template> <Card class="w-full max-w-sm gap-0 py-0"> <CardHeader class="gap-1 px-6 pt-5.5 pb-0"> <CardTitle class="font-semibold text-base tracking-[-0.2px]">Area Chart</CardTitle> <CardDescription class="text-[13.5px]">Showing total visitors for the last 6 months</CardDescription> </CardHeader> <CardContent class="items-center px-5 pt-2.5 pb-0"> <ChartContainer :config="config" class="aspect-auto h-44 w-full" cursor> <VisXYContainer :data="data" :margin="{ left: 12, right: 12 }" :y-domain="[0, undefined]" :svg-defs="svgDefs" > <VisArea :x="x" :y="(d: Row) => d.desktop" :color="() => fills[0]" :opacity="1" :curve-type="CurveType.Natural" /> <VisLine :x="x" :y="(d: Row) => d.desktop" :color="config.desktop.color" :curve-type="CurveType.Natural" /> <VisAxis type="x" :x="x" :tick-values="[0, 1, 2, 3, 4, 5]" :tick-format="tickFormat" :tick-line="false" :domain-line="false" :grid-line="false" /> <VisAxis type="y" :num-ticks="4" :tick-format="() => ''" :tick-line="false" :domain-line="false" /> <ChartTooltip /> <ChartCrosshair :x="x" :y="[(d: Row) => d.desktop]" :template="template" :color="(_: Row, i: number) => [config.desktop.color][i]" /> </VisXYContainer> </ChartContainer> </CardContent> <CardFooter class="flex-col items-stretch gap-1 px-6 pt-3.5 pb-5.5 text-[13.5px]"> <b class="flex items-center gap-2 font-medium"> Trending up by 5.2% this month <TrendingUpIcon class="size-4" /> </b> <span class="text-muted-foreground">January - June 2024</span> </CardFooter> </Card></template><script lang="ts"> import { Area, AreaChart } from "layerchart"; import IconPlaceholder from "$lib/components/icon-placeholder/icon-placeholder.svelte"; import * as Card from "$lib/components/ui/card"; import * as Chart from "$lib/components/ui/chart";
const data = [ { i: 0, month: "January", desktop: 186, mobile: 80 }, { i: 1, month: "February", desktop: 305, mobile: 200 }, { i: 2, month: "March", desktop: 237, mobile: 120 }, { i: 3, month: "April", desktop: 73, mobile: 190 }, { i: 4, month: "May", desktop: 209, mobile: 130 }, { i: 5, month: "June", desktop: 214, mobile: 140 }, ];
const config = { desktop: { label: "Desktop", color: "var(--chart-1)" }, } satisfies Chart.ChartConfig;
// d3 `curveNatural` (natural cubic spline); d3-shape is not a dependency of the docs app. function controlPoints(p: number[]): [number[], number[]] { const n = p.length - 1; const a = new Array<number>(n); const b = new Array<number>(n); const r = new Array<number>(n); a[0] = 0; b[0] = 2; r[0] = p[0] + 2 * p[1]; for (let i = 1; i < n - 1; ++i) { a[i] = 1; b[i] = 4; r[i] = 4 * p[i] + 2 * p[i + 1]; } a[n - 1] = 2; b[n - 1] = 7; r[n - 1] = 8 * p[n - 1] + p[n]; for (let i = 1; i < n; ++i) { const m = a[i] / b[i - 1]; b[i] -= m; r[i] -= m * r[i - 1]; } a[n - 1] = r[n - 1] / b[n - 1]; for (let i = n - 2; i >= 0; --i) a[i] = (r[i] - a[i + 1]) / b[i]; b[n - 1] = (p[n] + a[n - 1]) / 2; for (let i = 0; i < n - 1; ++i) b[i] = 2 * p[i + 1] - a[i + 1]; return [a, b]; }
// biome-ignore lint/suspicious/noExplicitAny: d3 curve factory shape const curveNatural = (context: any) => { let line = Number.NaN; let xs: number[] = []; let ys: number[] = []; return { areaStart() { line = 0; }, areaEnd() { line = Number.NaN; }, lineStart() { xs = []; ys = []; }, lineEnd() { const n = xs.length; if (n) { if (line) context.lineTo(xs[0], ys[0]); else context.moveTo(xs[0], ys[0]); if (n === 2) context.lineTo(xs[1], ys[1]); else if (n > 2) { const [px0, px1] = controlPoints(xs); const [py0, py1] = controlPoints(ys); for (let i = 0; i < n - 1; ++i) { context.bezierCurveTo(px0[i], py0[i], px1[i], py1[i], xs[i + 1], ys[i + 1]); } } } if (line || (line !== 0 && n === 1)) context.closePath(); line = 1 - line; }, point(px: number, py: number) { xs.push(+px); ys.push(+py); }, }; };
</script>
<Card.Root class="w-full max-w-sm gap-0 py-0"> <Card.Header class="gap-1 px-6 pt-5.5 pb-0"> <Card.Title class="font-semibold text-base tracking-[-0.2px]">Area Chart</Card.Title> <Card.Description class="text-[13.5px]">Showing total visitors for the last 6 months</Card.Description> </Card.Header> <Card.Content class="items-center px-5 pt-2.5 pb-0"> <Chart.Container {config} class="aspect-auto h-44 w-full"> <AreaChart {data} x="i" seriesLayout="overlap" series={[ { key: "desktop", label: config.desktop.label, color: "var(--color-desktop)" }, ]} padding={{ left: 12, right: 12, top: 4, bottom: 20 }} props={{ xAxis: { format: (v: number) => data[v]?.month.slice(0, 3) ?? "", ticks: 6 }, yAxis: { format: () => "" }, }} > {#snippet marks({ context })} <defs> <linearGradient id="fill-desktop-default" x1="0" y1="0" x2="0" y2="1"> <stop offset="0" style="stop-color: var(--chart-1); stop-opacity: 0.26" /> <stop offset="1" style="stop-color: var(--chart-1); stop-opacity: 0.03" /> </linearGradient> </defs> {#each context.series.visibleSeries as s (s.key)} <Area seriesKey={s.key} curve={curveNatural} fill="url(#fill-{s.key}-default)" fillOpacity={1} line={{ class: "stroke-2" }} /> {/each} {/snippet} {#snippet tooltip()} <Chart.Tooltip indicator="line" labelFormatter={(v: number) => data[v]?.month ?? ""} /> {/snippet} </AreaChart> </Chart.Container> </Card.Content> <Card.Footer class="flex-col items-stretch gap-1 px-6 pt-3.5 pb-5.5 text-[13.5px]"> <b class="flex items-center gap-2 font-medium"> Trending up by 5.2% this month <IconPlaceholder lucide="TrendingUpIcon" tabler="IconTrendingUp" hugeicons="AnalyticsUpIcon" phosphor="TrendUpIcon" remixicon="RiLineChartLine" class="size-4" /> </b> <span class="text-muted-foreground">January - June 2024</span> </Card.Footer></Card.Root>Linear
Section titled “Linear”import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle,} from "@/components/ui/card";import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent,} from "@/components/ui/chart";import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";import { IconPlaceholder } from "@/edmi/icon-placeholder";
const data = [ { month: "January", desktop: 186, mobile: 80 }, { month: "February", desktop: 305, mobile: 200 }, { month: "March", desktop: 237, mobile: 120 }, { month: "April", desktop: 73, mobile: 190 }, { month: "May", desktop: 209, mobile: 130 }, { month: "June", desktop: 214, mobile: 140 },];
const config = { desktop: { label: "Desktop", color: "var(--chart-1)" },} satisfies ChartConfig;
export default function Demo() { return ( <Card className="w-full max-w-sm gap-0 py-0"> <CardHeader className="gap-1 px-6 pt-5.5 pb-0"> <CardTitle className="font-semibold text-base tracking-[-0.2px]"> Area Chart - Linear </CardTitle> <CardDescription className="text-[13.5px]"> Showing total visitors for the last 6 months </CardDescription> </CardHeader> <CardContent className="items-center px-5 pt-2.5 pb-0"> <ChartContainer config={config} className="aspect-auto h-44 w-full"> <AreaChart accessibilityLayer data={data} margin={{ left: 12, right: 12 }} > <defs> <linearGradient id="fill-desktop-linear" x1="0" y1="0" x2="0" y2="1" > <stop offset="0" stopColor="var(--color-desktop)" stopOpacity={0.26} /> <stop offset="1" stopColor="var(--color-desktop)" stopOpacity={0.03} /> </linearGradient> </defs> <CartesianGrid vertical={false} /> <XAxis dataKey="month" tickLine={false} axisLine={false} tickMargin={8} tickFormatter={(value) => value.slice(0, 3)} /> <ChartTooltip content={<ChartTooltipContent indicator="line" />} /> <Area dataKey="desktop" type="linear" fill="url(#fill-desktop-linear)" fillOpacity={1} stroke="var(--color-desktop)" strokeWidth={2} /> </AreaChart> </ChartContainer> </CardContent> <CardFooter className="flex-col items-stretch gap-1 px-6 pt-3.5 pb-5.5 text-[13.5px]"> <b className="flex items-center gap-2 font-medium"> Trending up by 5.2% this month <IconPlaceholder lucide="TrendingUpIcon" tabler="IconTrendingUp" hugeicons="AnalyticsUpIcon" phosphor="TrendUpIcon" remixicon="RiLineChartLine" className="size-4" /> </b> <span className="text-muted-foreground">January - June 2024</span> </CardFooter> </Card> );}<script setup lang="ts">import { CurveType } from "@unovis/ts";import { VisArea, VisAxis, VisLine, VisXYContainer } from "@unovis/vue";import { TrendingUpIcon } from "@lucide/vue";import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle,} from "@/components/ui/card";import { type ChartConfig, ChartContainer, ChartCrosshair, ChartTooltip, ChartTooltipContent, componentToString,} from "@/components/ui/chart";
type Row = { month: string; desktop: number; mobile: number };
const data: Row[] = [ { month: "January", desktop: 186, mobile: 80 }, { month: "February", desktop: 305, mobile: 200 }, { month: "March", desktop: 237, mobile: 120 }, { month: "April", desktop: 73, mobile: 190 }, { month: "May", desktop: 209, mobile: 130 }, { month: "June", desktop: 214, mobile: 140 },];
const config = { desktop: { label: "Desktop", color: "var(--chart-1)" },} satisfies ChartConfig;
// Gradient washes (6-26%) under the 2px line.const fills = ["url(#fill-desktop-linear)"];const svgDefs = '<linearGradient id="fill-desktop-linear" x1="0" y1="0" x2="0" y2="1"><stop offset="0" style="stop-color:var(--chart-1);stop-opacity:.26"/><stop offset="1" style="stop-color:var(--chart-1);stop-opacity:.03"/></linearGradient>';
const x = (_: Row, i: number) => i;const tickFormat = (i: number) => data[i]?.month.slice(0, 3) ?? "";const template = componentToString(config, ChartTooltipContent, { indicator: "line", labelFormatter: (i) => data[Number(i)]?.month ?? "",});</script>
<template> <Card class="w-full max-w-sm gap-0 py-0"> <CardHeader class="gap-1 px-6 pt-5.5 pb-0"> <CardTitle class="font-semibold text-base tracking-[-0.2px]">Area Chart - Linear</CardTitle> <CardDescription class="text-[13.5px]">Showing total visitors for the last 6 months</CardDescription> </CardHeader> <CardContent class="items-center px-5 pt-2.5 pb-0"> <ChartContainer :config="config" class="aspect-auto h-44 w-full" cursor> <VisXYContainer :data="data" :margin="{ left: 12, right: 12 }" :y-domain="[0, undefined]" :svg-defs="svgDefs" > <VisArea :x="x" :y="(d: Row) => d.desktop" :color="() => fills[0]" :opacity="1" :curve-type="CurveType.Linear" /> <VisLine :x="x" :y="(d: Row) => d.desktop" :color="config.desktop.color" :curve-type="CurveType.Linear" /> <VisAxis type="x" :x="x" :tick-values="[0, 1, 2, 3, 4, 5]" :tick-format="tickFormat" :tick-line="false" :domain-line="false" :grid-line="false" /> <VisAxis type="y" :num-ticks="4" :tick-format="() => ''" :tick-line="false" :domain-line="false" /> <ChartTooltip /> <ChartCrosshair :x="x" :y="[(d: Row) => d.desktop]" :template="template" :color="(_: Row, i: number) => [config.desktop.color][i]" /> </VisXYContainer> </ChartContainer> </CardContent> <CardFooter class="flex-col items-stretch gap-1 px-6 pt-3.5 pb-5.5 text-[13.5px]"> <b class="flex items-center gap-2 font-medium"> Trending up by 5.2% this month <TrendingUpIcon class="size-4" /> </b> <span class="text-muted-foreground">January - June 2024</span> </CardFooter> </Card></template><script lang="ts"> import { Area, AreaChart } from "layerchart"; import IconPlaceholder from "$lib/components/icon-placeholder/icon-placeholder.svelte"; import * as Card from "$lib/components/ui/card"; import * as Chart from "$lib/components/ui/chart";
const data = [ { i: 0, month: "January", desktop: 186, mobile: 80 }, { i: 1, month: "February", desktop: 305, mobile: 200 }, { i: 2, month: "March", desktop: 237, mobile: 120 }, { i: 3, month: "April", desktop: 73, mobile: 190 }, { i: 4, month: "May", desktop: 209, mobile: 130 }, { i: 5, month: "June", desktop: 214, mobile: 140 }, ];
const config = { desktop: { label: "Desktop", color: "var(--chart-1)" }, } satisfies Chart.ChartConfig;
</script>
<Card.Root class="w-full max-w-sm gap-0 py-0"> <Card.Header class="gap-1 px-6 pt-5.5 pb-0"> <Card.Title class="font-semibold text-base tracking-[-0.2px]">Area Chart - Linear</Card.Title> <Card.Description class="text-[13.5px]">Showing total visitors for the last 6 months</Card.Description> </Card.Header> <Card.Content class="items-center px-5 pt-2.5 pb-0"> <Chart.Container {config} class="aspect-auto h-44 w-full"> <AreaChart {data} x="i" seriesLayout="overlap" series={[ { key: "desktop", label: config.desktop.label, color: "var(--color-desktop)" }, ]} padding={{ left: 12, right: 12, top: 4, bottom: 20 }} props={{ xAxis: { format: (v: number) => data[v]?.month.slice(0, 3) ?? "", ticks: 6 }, yAxis: { format: () => "" }, }} > {#snippet marks({ context })} <defs> <linearGradient id="fill-desktop-linear" x1="0" y1="0" x2="0" y2="1"> <stop offset="0" style="stop-color: var(--chart-1); stop-opacity: 0.26" /> <stop offset="1" style="stop-color: var(--chart-1); stop-opacity: 0.03" /> </linearGradient> </defs> {#each context.series.visibleSeries as s (s.key)} <Area seriesKey={s.key} fill="url(#fill-{s.key}-linear)" fillOpacity={1} line={{ class: "stroke-2" }} /> {/each} {/snippet} {#snippet tooltip()} <Chart.Tooltip indicator="line" labelFormatter={(v: number) => data[v]?.month ?? ""} /> {/snippet} </AreaChart> </Chart.Container> </Card.Content> <Card.Footer class="flex-col items-stretch gap-1 px-6 pt-3.5 pb-5.5 text-[13.5px]"> <b class="flex items-center gap-2 font-medium"> Trending up by 5.2% this month <IconPlaceholder lucide="TrendingUpIcon" tabler="IconTrendingUp" hugeicons="AnalyticsUpIcon" phosphor="TrendUpIcon" remixicon="RiLineChartLine" class="size-4" /> </b> <span class="text-muted-foreground">January - June 2024</span> </Card.Footer></Card.Root>import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle,} from "@/components/ui/card";import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent,} from "@/components/ui/chart";import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";import { IconPlaceholder } from "@/edmi/icon-placeholder";
const data = [ { month: "January", desktop: 186, mobile: 80 }, { month: "February", desktop: 305, mobile: 200 }, { month: "March", desktop: 237, mobile: 120 }, { month: "April", desktop: 73, mobile: 190 }, { month: "May", desktop: 209, mobile: 130 }, { month: "June", desktop: 214, mobile: 140 },];
const config = { desktop: { label: "Desktop", color: "var(--chart-1)" },} satisfies ChartConfig;
export default function Demo() { return ( <Card className="w-full max-w-sm gap-0 py-0"> <CardHeader className="gap-1 px-6 pt-5.5 pb-0"> <CardTitle className="font-semibold text-base tracking-[-0.2px]"> Area Chart - Step </CardTitle> <CardDescription className="text-[13.5px]"> Showing total visitors for the last 6 months </CardDescription> </CardHeader> <CardContent className="items-center px-5 pt-2.5 pb-0"> <ChartContainer config={config} className="aspect-auto h-44 w-full"> <AreaChart accessibilityLayer data={data} margin={{ left: 12, right: 12 }} > <defs> <linearGradient id="fill-desktop-step" x1="0" y1="0" x2="0" y2="1" > <stop offset="0" stopColor="var(--color-desktop)" stopOpacity={0.26} /> <stop offset="1" stopColor="var(--color-desktop)" stopOpacity={0.03} /> </linearGradient> </defs> <CartesianGrid vertical={false} /> <XAxis dataKey="month" tickLine={false} axisLine={false} tickMargin={8} tickFormatter={(value) => value.slice(0, 3)} /> <ChartTooltip content={<ChartTooltipContent indicator="line" />} /> <Area dataKey="desktop" type="step" fill="url(#fill-desktop-step)" fillOpacity={1} stroke="var(--color-desktop)" strokeWidth={2} /> </AreaChart> </ChartContainer> </CardContent> <CardFooter className="flex-col items-stretch gap-1 px-6 pt-3.5 pb-5.5 text-[13.5px]"> <b className="flex items-center gap-2 font-medium"> Trending up by 5.2% this month <IconPlaceholder lucide="TrendingUpIcon" tabler="IconTrendingUp" hugeicons="AnalyticsUpIcon" phosphor="TrendUpIcon" remixicon="RiLineChartLine" className="size-4" /> </b> <span className="text-muted-foreground">January - June 2024</span> </CardFooter> </Card> );}<script setup lang="ts">import { CurveType } from "@unovis/ts";import { VisArea, VisAxis, VisLine, VisXYContainer } from "@unovis/vue";import { TrendingUpIcon } from "@lucide/vue";import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle,} from "@/components/ui/card";import { type ChartConfig, ChartContainer, ChartCrosshair, ChartTooltip, ChartTooltipContent, componentToString,} from "@/components/ui/chart";
type Row = { month: string; desktop: number; mobile: number };
const data: Row[] = [ { month: "January", desktop: 186, mobile: 80 }, { month: "February", desktop: 305, mobile: 200 }, { month: "March", desktop: 237, mobile: 120 }, { month: "April", desktop: 73, mobile: 190 }, { month: "May", desktop: 209, mobile: 130 }, { month: "June", desktop: 214, mobile: 140 },];
const config = { desktop: { label: "Desktop", color: "var(--chart-1)" },} satisfies ChartConfig;
// Gradient washes (6-26%) under the 2px line.const fills = ["url(#fill-desktop-step)"];const svgDefs = '<linearGradient id="fill-desktop-step" x1="0" y1="0" x2="0" y2="1"><stop offset="0" style="stop-color:var(--chart-1);stop-opacity:.26"/><stop offset="1" style="stop-color:var(--chart-1);stop-opacity:.03"/></linearGradient>';
const x = (_: Row, i: number) => i;const tickFormat = (i: number) => data[i]?.month.slice(0, 3) ?? "";const template = componentToString(config, ChartTooltipContent, { indicator: "line", labelFormatter: (i) => data[Number(i)]?.month ?? "",});</script>
<template> <Card class="w-full max-w-sm gap-0 py-0"> <CardHeader class="gap-1 px-6 pt-5.5 pb-0"> <CardTitle class="font-semibold text-base tracking-[-0.2px]">Area Chart - Step</CardTitle> <CardDescription class="text-[13.5px]">Showing total visitors for the last 6 months</CardDescription> </CardHeader> <CardContent class="items-center px-5 pt-2.5 pb-0"> <ChartContainer :config="config" class="aspect-auto h-44 w-full" cursor> <VisXYContainer :data="data" :margin="{ left: 12, right: 12 }" :y-domain="[0, undefined]" :svg-defs="svgDefs" > <VisArea :x="x" :y="(d: Row) => d.desktop" :color="() => fills[0]" :opacity="1" :curve-type="CurveType.Step" /> <VisLine :x="x" :y="(d: Row) => d.desktop" :color="config.desktop.color" :curve-type="CurveType.Step" /> <VisAxis type="x" :x="x" :tick-values="[0, 1, 2, 3, 4, 5]" :tick-format="tickFormat" :tick-line="false" :domain-line="false" :grid-line="false" /> <VisAxis type="y" :num-ticks="4" :tick-format="() => ''" :tick-line="false" :domain-line="false" /> <ChartTooltip /> <ChartCrosshair :x="x" :y="[(d: Row) => d.desktop]" :template="template" :color="(_: Row, i: number) => [config.desktop.color][i]" /> </VisXYContainer> </ChartContainer> </CardContent> <CardFooter class="flex-col items-stretch gap-1 px-6 pt-3.5 pb-5.5 text-[13.5px]"> <b class="flex items-center gap-2 font-medium"> Trending up by 5.2% this month <TrendingUpIcon class="size-4" /> </b> <span class="text-muted-foreground">January - June 2024</span> </CardFooter> </Card></template><script lang="ts"> import { Area, AreaChart } from "layerchart"; import IconPlaceholder from "$lib/components/icon-placeholder/icon-placeholder.svelte"; import * as Card from "$lib/components/ui/card"; import * as Chart from "$lib/components/ui/chart";
const data = [ { i: 0, month: "January", desktop: 186, mobile: 80 }, { i: 1, month: "February", desktop: 305, mobile: 200 }, { i: 2, month: "March", desktop: 237, mobile: 120 }, { i: 3, month: "April", desktop: 73, mobile: 190 }, { i: 4, month: "May", desktop: 209, mobile: 130 }, { i: 5, month: "June", desktop: 214, mobile: 140 }, ];
const config = { desktop: { label: "Desktop", color: "var(--chart-1)" }, } satisfies Chart.ChartConfig;
// d3 `curveStep` (midpoint step); d3-shape is not a dependency of the docs app. // biome-ignore lint/suspicious/noExplicitAny: d3 curve factory shape const curveStep = (context: any) => { let line = Number.NaN; let px = Number.NaN; let py = Number.NaN; let point = 0; let t = 0.5; return { areaStart() { line = 0; }, areaEnd() { line = Number.NaN; }, lineStart() { px = py = Number.NaN; point = 0; }, lineEnd() { if (point === 2) context.lineTo(px, py); if (line || (line !== 0 && point === 1)) context.closePath(); if (line >= 0) { t = 1 - t; line = 1 - line; } }, point(x: number, y: number) { if (point === 0) { point = 1; if (line) context.lineTo(x, y); else context.moveTo(x, y); } else { point = 2; const x1 = px * (1 - t) + x * t; context.lineTo(x1, py); context.lineTo(x1, y); } px = x; py = y; }, }; };
</script>
<Card.Root class="w-full max-w-sm gap-0 py-0"> <Card.Header class="gap-1 px-6 pt-5.5 pb-0"> <Card.Title class="font-semibold text-base tracking-[-0.2px]">Area Chart - Step</Card.Title> <Card.Description class="text-[13.5px]">Showing total visitors for the last 6 months</Card.Description> </Card.Header> <Card.Content class="items-center px-5 pt-2.5 pb-0"> <Chart.Container {config} class="aspect-auto h-44 w-full"> <AreaChart {data} x="i" seriesLayout="overlap" series={[ { key: "desktop", label: config.desktop.label, color: "var(--color-desktop)" }, ]} padding={{ left: 12, right: 12, top: 4, bottom: 20 }} props={{ xAxis: { format: (v: number) => data[v]?.month.slice(0, 3) ?? "", ticks: 6 }, yAxis: { format: () => "" }, }} > {#snippet marks({ context })} <defs> <linearGradient id="fill-desktop-step" x1="0" y1="0" x2="0" y2="1"> <stop offset="0" style="stop-color: var(--chart-1); stop-opacity: 0.26" /> <stop offset="1" style="stop-color: var(--chart-1); stop-opacity: 0.03" /> </linearGradient> </defs> {#each context.series.visibleSeries as s (s.key)} <Area seriesKey={s.key} curve={curveStep} fill="url(#fill-{s.key}-step)" fillOpacity={1} line={{ class: "stroke-2" }} /> {/each} {/snippet} {#snippet tooltip()} <Chart.Tooltip indicator="line" labelFormatter={(v: number) => data[v]?.month ?? ""} /> {/snippet} </AreaChart> </Chart.Container> </Card.Content> <Card.Footer class="flex-col items-stretch gap-1 px-6 pt-3.5 pb-5.5 text-[13.5px]"> <b class="flex items-center gap-2 font-medium"> Trending up by 5.2% this month <IconPlaceholder lucide="TrendingUpIcon" tabler="IconTrendingUp" hugeicons="AnalyticsUpIcon" phosphor="TrendUpIcon" remixicon="RiLineChartLine" class="size-4" /> </b> <span class="text-muted-foreground">January - June 2024</span> </Card.Footer></Card.Root>Stacked
Section titled “Stacked”import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle,} from "@/components/ui/card";import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent,} from "@/components/ui/chart";import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";import { IconPlaceholder } from "@/edmi/icon-placeholder";
const data = [ { month: "January", desktop: 186, mobile: 80 }, { month: "February", desktop: 305, mobile: 200 }, { month: "March", desktop: 237, mobile: 120 }, { month: "April", desktop: 73, mobile: 190 }, { month: "May", desktop: 209, mobile: 130 }, { month: "June", desktop: 214, mobile: 140 },];
const config = { desktop: { label: "Desktop", color: "var(--chart-1)" }, mobile: { label: "Mobile", color: "var(--chart-2)" },} satisfies ChartConfig;
export default function Demo() { return ( <Card className="w-full max-w-sm gap-0 py-0"> <CardHeader className="gap-1 px-6 pt-5.5 pb-0"> <CardTitle className="font-semibold text-base tracking-[-0.2px]"> Area Chart - Stacked </CardTitle> <CardDescription className="text-[13.5px]"> Showing total visitors for the last 6 months </CardDescription> </CardHeader> <CardContent className="items-center px-5 pt-2.5 pb-0"> <ChartContainer config={config} className="aspect-auto h-44 w-full"> <AreaChart accessibilityLayer data={data} margin={{ left: 12, right: 12 }} > <defs> <linearGradient id="fill-desktop-stacked" x1="0" y1="0" x2="0" y2="1" > <stop offset="0" stopColor="var(--color-desktop)" stopOpacity={0.26} /> <stop offset="1" stopColor="var(--color-desktop)" stopOpacity={0.03} /> </linearGradient> <linearGradient id="fill-mobile-stacked" x1="0" y1="0" x2="0" y2="1" > <stop offset="0" stopColor="var(--color-mobile)" stopOpacity={0.26} /> <stop offset="1" stopColor="var(--color-mobile)" stopOpacity={0.03} /> </linearGradient> </defs> <CartesianGrid vertical={false} /> <XAxis dataKey="month" tickLine={false} axisLine={false} tickMargin={8} tickFormatter={(value) => value.slice(0, 3)} /> <ChartTooltip content={<ChartTooltipContent indicator="dot" />} /> <Area dataKey="desktop" type="natural" fill="url(#fill-desktop-stacked)" fillOpacity={1} stroke="var(--color-desktop)" strokeWidth={2} stackId="a" /> <Area dataKey="mobile" type="natural" fill="url(#fill-mobile-stacked)" fillOpacity={1} stroke="var(--color-mobile)" strokeWidth={2} stackId="a" /> </AreaChart> </ChartContainer> </CardContent> <CardFooter className="flex-col items-stretch gap-1 px-6 pt-3.5 pb-5.5 text-[13.5px]"> <b className="flex items-center gap-2 font-medium"> Trending up by 5.2% this month <IconPlaceholder lucide="TrendingUpIcon" tabler="IconTrendingUp" hugeicons="AnalyticsUpIcon" phosphor="TrendUpIcon" remixicon="RiLineChartLine" className="size-4" /> </b> <span className="text-muted-foreground">January - June 2024</span> </CardFooter> </Card> );}<script setup lang="ts">import { CurveType } from "@unovis/ts";import { VisArea, VisAxis, VisLine, VisXYContainer } from "@unovis/vue";import { TrendingUpIcon } from "@lucide/vue";import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle,} from "@/components/ui/card";import { type ChartConfig, ChartContainer, ChartCrosshair, ChartTooltip, ChartTooltipContent, componentToString,} from "@/components/ui/chart";
type Row = { month: string; desktop: number; mobile: number };
const data: Row[] = [ { month: "January", desktop: 186, mobile: 80 }, { month: "February", desktop: 305, mobile: 200 }, { month: "March", desktop: 237, mobile: 120 }, { month: "April", desktop: 73, mobile: 190 }, { month: "May", desktop: 209, mobile: 130 }, { month: "June", desktop: 214, mobile: 140 },];
const config = { desktop: { label: "Desktop", color: "var(--chart-1)" }, mobile: { label: "Mobile", color: "var(--chart-2)" },} satisfies ChartConfig;
// Gradient washes (6-26%) under the 2px line.const fills = ["url(#fill-desktop-stacked)", "url(#fill-mobile-stacked)"];const svgDefs = '<linearGradient id="fill-desktop-stacked" x1="0" y1="0" x2="0" y2="1"><stop offset="0" style="stop-color:var(--chart-1);stop-opacity:.26"/><stop offset="1" style="stop-color:var(--chart-1);stop-opacity:.03"/></linearGradient><linearGradient id="fill-mobile-stacked" x1="0" y1="0" x2="0" y2="1"><stop offset="0" style="stop-color:var(--chart-2);stop-opacity:.26"/><stop offset="1" style="stop-color:var(--chart-2);stop-opacity:.03"/></linearGradient>';
const x = (_: Row, i: number) => i;const tickFormat = (i: number) => data[i]?.month.slice(0, 3) ?? "";const template = componentToString(config, ChartTooltipContent, { indicator: "dot", labelFormatter: (i) => data[Number(i)]?.month ?? "",});</script>
<template> <Card class="w-full max-w-sm gap-0 py-0"> <CardHeader class="gap-1 px-6 pt-5.5 pb-0"> <CardTitle class="font-semibold text-base tracking-[-0.2px]">Area Chart - Stacked</CardTitle> <CardDescription class="text-[13.5px]">Showing total visitors for the last 6 months</CardDescription> </CardHeader> <CardContent class="items-center px-5 pt-2.5 pb-0"> <ChartContainer :config="config" class="aspect-auto h-44 w-full" cursor> <VisXYContainer :data="data" :margin="{ left: 12, right: 12 }" :y-domain="[0, undefined]" :svg-defs="svgDefs" ><VisArea :x="x" :y="[(d: Row) => d.desktop, (d: Row) => d.mobile]" :color="(_: Row, i: number) => fills[i]" :opacity="1" :curve-type="CurveType.Natural" /> <VisLine :x="x" :y="(d: Row) => d.desktop" :color="config.desktop.color" :curve-type="CurveType.Natural" /> <VisLine :x="x" :y="(d: Row) => d.desktop + d.mobile" :color="config.mobile.color" :curve-type="CurveType.Natural" /> <VisAxis type="x" :x="x" :tick-values="[0, 1, 2, 3, 4, 5]" :tick-format="tickFormat" :tick-line="false" :domain-line="false" :grid-line="false" /> <VisAxis type="y" :num-ticks="4" :tick-format="() => ''" :tick-line="false" :domain-line="false" /> <ChartTooltip /> <ChartCrosshair :x="x" :y="[(d: Row) => d.desktop, (d: Row) => d.desktop + d.mobile]" :template="template" :color="(_: Row, i: number) => [config.desktop.color, config.mobile.color][i]" /> </VisXYContainer> </ChartContainer> </CardContent> <CardFooter class="flex-col items-stretch gap-1 px-6 pt-3.5 pb-5.5 text-[13.5px]"> <b class="flex items-center gap-2 font-medium"> Trending up by 5.2% this month <TrendingUpIcon class="size-4" /> </b> <span class="text-muted-foreground">January - June 2024</span> </CardFooter> </Card></template><script lang="ts"> import { Area, AreaChart } from "layerchart"; import IconPlaceholder from "$lib/components/icon-placeholder/icon-placeholder.svelte"; import * as Card from "$lib/components/ui/card"; import * as Chart from "$lib/components/ui/chart";
const data = [ { i: 0, month: "January", desktop: 186, mobile: 80 }, { i: 1, month: "February", desktop: 305, mobile: 200 }, { i: 2, month: "March", desktop: 237, mobile: 120 }, { i: 3, month: "April", desktop: 73, mobile: 190 }, { i: 4, month: "May", desktop: 209, mobile: 130 }, { i: 5, month: "June", desktop: 214, mobile: 140 }, ];
const config = { desktop: { label: "Desktop", color: "var(--chart-1)" }, mobile: { label: "Mobile", color: "var(--chart-2)" }, } satisfies Chart.ChartConfig;
// d3 `curveNatural` (natural cubic spline); d3-shape is not a dependency of the docs app. function controlPoints(p: number[]): [number[], number[]] { const n = p.length - 1; const a = new Array<number>(n); const b = new Array<number>(n); const r = new Array<number>(n); a[0] = 0; b[0] = 2; r[0] = p[0] + 2 * p[1]; for (let i = 1; i < n - 1; ++i) { a[i] = 1; b[i] = 4; r[i] = 4 * p[i] + 2 * p[i + 1]; } a[n - 1] = 2; b[n - 1] = 7; r[n - 1] = 8 * p[n - 1] + p[n]; for (let i = 1; i < n; ++i) { const m = a[i] / b[i - 1]; b[i] -= m; r[i] -= m * r[i - 1]; } a[n - 1] = r[n - 1] / b[n - 1]; for (let i = n - 2; i >= 0; --i) a[i] = (r[i] - a[i + 1]) / b[i]; b[n - 1] = (p[n] + a[n - 1]) / 2; for (let i = 0; i < n - 1; ++i) b[i] = 2 * p[i + 1] - a[i + 1]; return [a, b]; }
// biome-ignore lint/suspicious/noExplicitAny: d3 curve factory shape const curveNatural = (context: any) => { let line = Number.NaN; let xs: number[] = []; let ys: number[] = []; return { areaStart() { line = 0; }, areaEnd() { line = Number.NaN; }, lineStart() { xs = []; ys = []; }, lineEnd() { const n = xs.length; if (n) { if (line) context.lineTo(xs[0], ys[0]); else context.moveTo(xs[0], ys[0]); if (n === 2) context.lineTo(xs[1], ys[1]); else if (n > 2) { const [px0, px1] = controlPoints(xs); const [py0, py1] = controlPoints(ys); for (let i = 0; i < n - 1; ++i) { context.bezierCurveTo(px0[i], py0[i], px1[i], py1[i], xs[i + 1], ys[i + 1]); } } } if (line || (line !== 0 && n === 1)) context.closePath(); line = 1 - line; }, point(px: number, py: number) { xs.push(+px); ys.push(+py); }, }; };
</script>
<Card.Root class="w-full max-w-sm gap-0 py-0"> <Card.Header class="gap-1 px-6 pt-5.5 pb-0"> <Card.Title class="font-semibold text-base tracking-[-0.2px]">Area Chart - Stacked</Card.Title> <Card.Description class="text-[13.5px]">Showing total visitors for the last 6 months</Card.Description> </Card.Header> <Card.Content class="items-center px-5 pt-2.5 pb-0"> <Chart.Container {config} class="aspect-auto h-44 w-full"> <AreaChart {data} x="i" seriesLayout="stack" series={[ { key: "desktop", label: config.desktop.label, color: "var(--color-desktop)" }, { key: "mobile", label: config.mobile.label, color: "var(--color-mobile)" }, ]} padding={{ left: 12, right: 12, top: 4, bottom: 20 }} props={{ xAxis: { format: (v: number) => data[v]?.month.slice(0, 3) ?? "", ticks: 6 }, yAxis: { format: () => "" }, }} > {#snippet marks({ context })} <defs> <linearGradient id="fill-desktop-stacked" x1="0" y1="0" x2="0" y2="1"> <stop offset="0" style="stop-color: var(--chart-1); stop-opacity: 0.26" /> <stop offset="1" style="stop-color: var(--chart-1); stop-opacity: 0.03" /> </linearGradient> <linearGradient id="fill-mobile-stacked" x1="0" y1="0" x2="0" y2="1"> <stop offset="0" style="stop-color: var(--chart-2); stop-opacity: 0.26" /> <stop offset="1" style="stop-color: var(--chart-2); stop-opacity: 0.03" /> </linearGradient> </defs> {#each context.series.visibleSeries as s (s.key)} <Area seriesKey={s.key} curve={curveNatural} fill="url(#fill-{s.key}-stacked)" fillOpacity={1} line={{ class: "stroke-2" }} /> {/each} {/snippet} {#snippet tooltip()} <Chart.Tooltip indicator="dot" labelFormatter={(v: number) => data[v]?.month ?? ""} /> {/snippet} </AreaChart> </Chart.Container> </Card.Content> <Card.Footer class="flex-col items-stretch gap-1 px-6 pt-3.5 pb-5.5 text-[13.5px]"> <b class="flex items-center gap-2 font-medium"> Trending up by 5.2% this month <IconPlaceholder lucide="TrendingUpIcon" tabler="IconTrendingUp" hugeicons="AnalyticsUpIcon" phosphor="TrendUpIcon" remixicon="RiLineChartLine" class="size-4" /> </b> <span class="text-muted-foreground">January - June 2024</span> </Card.Footer></Card.Root>Stacked Expanded
Section titled “Stacked Expanded”import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle,} from "@/components/ui/card";import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent,} from "@/components/ui/chart";import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";import { IconPlaceholder } from "@/edmi/icon-placeholder";
const data = [ { month: "January", desktop: 186, mobile: 80 }, { month: "February", desktop: 305, mobile: 200 }, { month: "March", desktop: 237, mobile: 120 }, { month: "April", desktop: 73, mobile: 190 }, { month: "May", desktop: 209, mobile: 130 }, { month: "June", desktop: 214, mobile: 140 },];
const config = { desktop: { label: "Desktop", color: "var(--chart-1)" }, mobile: { label: "Mobile", color: "var(--chart-2)" },} satisfies ChartConfig;
export default function Demo() { return ( <Card className="w-full max-w-sm gap-0 py-0"> <CardHeader className="gap-1 px-6 pt-5.5 pb-0"> <CardTitle className="font-semibold text-base tracking-[-0.2px]"> Area Chart - Stacked Expanded </CardTitle> <CardDescription className="text-[13.5px]"> Showing total visitors for the last 6 months </CardDescription> </CardHeader> <CardContent className="items-center px-5 pt-2.5 pb-0"> <ChartContainer config={config} className="aspect-auto h-44 w-full"> <AreaChart accessibilityLayer data={data} margin={{ left: 12, right: 12 }} stackOffset="expand" > <defs> <linearGradient id="fill-desktop-stacked-expanded" x1="0" y1="0" x2="0" y2="1" > <stop offset="0" stopColor="var(--color-desktop)" stopOpacity={0.26} /> <stop offset="1" stopColor="var(--color-desktop)" stopOpacity={0.03} /> </linearGradient> <linearGradient id="fill-mobile-stacked-expanded" x1="0" y1="0" x2="0" y2="1" > <stop offset="0" stopColor="var(--color-mobile)" stopOpacity={0.26} /> <stop offset="1" stopColor="var(--color-mobile)" stopOpacity={0.03} /> </linearGradient> </defs> <CartesianGrid vertical={false} /> <XAxis dataKey="month" tickLine={false} axisLine={false} tickMargin={8} tickFormatter={(value) => value.slice(0, 3)} /> <ChartTooltip content={<ChartTooltipContent indicator="line" />} /> <Area dataKey="desktop" type="natural" fill="url(#fill-desktop-stacked-expanded)" fillOpacity={1} stroke="var(--color-desktop)" strokeWidth={2} stackId="a" /> <Area dataKey="mobile" type="natural" fill="url(#fill-mobile-stacked-expanded)" fillOpacity={1} stroke="var(--color-mobile)" strokeWidth={2} stackId="a" /> </AreaChart> </ChartContainer> </CardContent> <CardFooter className="flex-col items-stretch gap-1 px-6 pt-3.5 pb-5.5 text-[13.5px]"> <b className="flex items-center gap-2 font-medium"> Trending up by 5.2% this month <IconPlaceholder lucide="TrendingUpIcon" tabler="IconTrendingUp" hugeicons="AnalyticsUpIcon" phosphor="TrendUpIcon" remixicon="RiLineChartLine" className="size-4" /> </b> <span className="text-muted-foreground">January - June 2024</span> </CardFooter> </Card> );}<script setup lang="ts">import { CurveType } from "@unovis/ts";import { VisArea, VisAxis, VisLine, VisXYContainer } from "@unovis/vue";import { TrendingUpIcon } from "@lucide/vue";import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle,} from "@/components/ui/card";import { type ChartConfig, ChartContainer, ChartCrosshair, ChartTooltip, ChartTooltipContent, componentToString,} from "@/components/ui/chart";
type Row = { month: string; desktop: number; mobile: number };
const data: Row[] = [ { month: "January", desktop: 186, mobile: 80 }, { month: "February", desktop: 305, mobile: 200 }, { month: "March", desktop: 237, mobile: 120 }, { month: "April", desktop: 73, mobile: 190 }, { month: "May", desktop: 209, mobile: 130 }, { month: "June", desktop: 214, mobile: 140 },];
const config = { desktop: { label: "Desktop", color: "var(--chart-1)" }, mobile: { label: "Mobile", color: "var(--chart-2)" },} satisfies ChartConfig;
// Gradient washes (6-26%) under the 2px line.const fills = ["url(#fill-desktop-stacked-expanded)", "url(#fill-mobile-stacked-expanded)"];const svgDefs = '<linearGradient id="fill-desktop-stacked-expanded" x1="0" y1="0" x2="0" y2="1"><stop offset="0" style="stop-color:var(--chart-1);stop-opacity:.26"/><stop offset="1" style="stop-color:var(--chart-1);stop-opacity:.03"/></linearGradient><linearGradient id="fill-mobile-stacked-expanded" x1="0" y1="0" x2="0" y2="1"><stop offset="0" style="stop-color:var(--chart-2);stop-opacity:.26"/><stop offset="1" style="stop-color:var(--chart-2);stop-opacity:.03"/></linearGradient>';
const x = (_: Row, i: number) => i;const tickFormat = (i: number) => data[i]?.month.slice(0, 3) ?? "";const template = componentToString(config, ChartTooltipContent, { indicator: "line", labelFormatter: (i) => data[Number(i)]?.month ?? "",});</script>
<template> <Card class="w-full max-w-sm gap-0 py-0"> <CardHeader class="gap-1 px-6 pt-5.5 pb-0"> <CardTitle class="font-semibold text-base tracking-[-0.2px]">Area Chart - Stacked Expanded</CardTitle> <CardDescription class="text-[13.5px]">Showing total visitors for the last 6 months</CardDescription> </CardHeader> <CardContent class="items-center px-5 pt-2.5 pb-0"> <ChartContainer :config="config" class="aspect-auto h-44 w-full" cursor> <VisXYContainer :data="data" :margin="{ left: 12, right: 12 }" :y-domain="[0, 1]" :svg-defs="svgDefs" ><VisArea :x="x" :y="[(d: Row) => d.desktop / (d.desktop + d.mobile), (d: Row) => d.mobile / (d.desktop + d.mobile)]" :color="(_: Row, i: number) => fills[i]" :opacity="1" :curve-type="CurveType.Natural" /> <VisLine :x="x" :y="(d: Row) => d.desktop / (d.desktop + d.mobile)" :color="config.desktop.color" :curve-type="CurveType.Natural" /> <VisLine :x="x" :y="() => 1" :color="config.mobile.color" :curve-type="CurveType.Natural" /> <VisAxis type="x" :x="x" :tick-values="[0, 1, 2, 3, 4, 5]" :tick-format="tickFormat" :tick-line="false" :domain-line="false" :grid-line="false" /> <VisAxis type="y" :num-ticks="4" :tick-format="() => ''" :tick-line="false" :domain-line="false" /> <ChartTooltip /> <ChartCrosshair :x="x" :y="[(d: Row) => d.desktop / (d.desktop + d.mobile), () => 1]" :template="template" :color="(_: Row, i: number) => [config.desktop.color, config.mobile.color][i]" /> </VisXYContainer> </ChartContainer> </CardContent> <CardFooter class="flex-col items-stretch gap-1 px-6 pt-3.5 pb-5.5 text-[13.5px]"> <b class="flex items-center gap-2 font-medium"> Trending up by 5.2% this month <TrendingUpIcon class="size-4" /> </b> <span class="text-muted-foreground">January - June 2024</span> </CardFooter> </Card></template><script lang="ts"> import { Area, AreaChart } from "layerchart"; import IconPlaceholder from "$lib/components/icon-placeholder/icon-placeholder.svelte"; import * as Card from "$lib/components/ui/card"; import * as Chart from "$lib/components/ui/chart";
const data = [ { i: 0, month: "January", desktop: 186, mobile: 80 }, { i: 1, month: "February", desktop: 305, mobile: 200 }, { i: 2, month: "March", desktop: 237, mobile: 120 }, { i: 3, month: "April", desktop: 73, mobile: 190 }, { i: 4, month: "May", desktop: 209, mobile: 130 }, { i: 5, month: "June", desktop: 214, mobile: 140 }, ];
const config = { desktop: { label: "Desktop", color: "var(--chart-1)" }, mobile: { label: "Mobile", color: "var(--chart-2)" }, } satisfies Chart.ChartConfig;
// d3 `curveNatural` (natural cubic spline); d3-shape is not a dependency of the docs app. function controlPoints(p: number[]): [number[], number[]] { const n = p.length - 1; const a = new Array<number>(n); const b = new Array<number>(n); const r = new Array<number>(n); a[0] = 0; b[0] = 2; r[0] = p[0] + 2 * p[1]; for (let i = 1; i < n - 1; ++i) { a[i] = 1; b[i] = 4; r[i] = 4 * p[i] + 2 * p[i + 1]; } a[n - 1] = 2; b[n - 1] = 7; r[n - 1] = 8 * p[n - 1] + p[n]; for (let i = 1; i < n; ++i) { const m = a[i] / b[i - 1]; b[i] -= m; r[i] -= m * r[i - 1]; } a[n - 1] = r[n - 1] / b[n - 1]; for (let i = n - 2; i >= 0; --i) a[i] = (r[i] - a[i + 1]) / b[i]; b[n - 1] = (p[n] + a[n - 1]) / 2; for (let i = 0; i < n - 1; ++i) b[i] = 2 * p[i + 1] - a[i + 1]; return [a, b]; }
// biome-ignore lint/suspicious/noExplicitAny: d3 curve factory shape const curveNatural = (context: any) => { let line = Number.NaN; let xs: number[] = []; let ys: number[] = []; return { areaStart() { line = 0; }, areaEnd() { line = Number.NaN; }, lineStart() { xs = []; ys = []; }, lineEnd() { const n = xs.length; if (n) { if (line) context.lineTo(xs[0], ys[0]); else context.moveTo(xs[0], ys[0]); if (n === 2) context.lineTo(xs[1], ys[1]); else if (n > 2) { const [px0, px1] = controlPoints(xs); const [py0, py1] = controlPoints(ys); for (let i = 0; i < n - 1; ++i) { context.bezierCurveTo(px0[i], py0[i], px1[i], py1[i], xs[i + 1], ys[i + 1]); } } } if (line || (line !== 0 && n === 1)) context.closePath(); line = 1 - line; }, point(px: number, py: number) { xs.push(+px); ys.push(+py); }, }; };
</script>
<Card.Root class="w-full max-w-sm gap-0 py-0"> <Card.Header class="gap-1 px-6 pt-5.5 pb-0"> <Card.Title class="font-semibold text-base tracking-[-0.2px]">Area Chart - Stacked Expanded</Card.Title> <Card.Description class="text-[13.5px]">Showing total visitors for the last 6 months</Card.Description> </Card.Header> <Card.Content class="items-center px-5 pt-2.5 pb-0"> <Chart.Container {config} class="aspect-auto h-44 w-full"> <AreaChart {data} x="i" seriesLayout="stackExpand" series={[ { key: "desktop", label: config.desktop.label, color: "var(--color-desktop)" }, { key: "mobile", label: config.mobile.label, color: "var(--color-mobile)" }, ]} padding={{ left: 12, right: 12, top: 4, bottom: 20 }} props={{ xAxis: { format: (v: number) => data[v]?.month.slice(0, 3) ?? "", ticks: 6 }, yAxis: { format: () => "" }, }} > {#snippet marks({ context })} <defs> <linearGradient id="fill-desktop-stacked-expanded" x1="0" y1="0" x2="0" y2="1"> <stop offset="0" style="stop-color: var(--chart-1); stop-opacity: 0.26" /> <stop offset="1" style="stop-color: var(--chart-1); stop-opacity: 0.03" /> </linearGradient> <linearGradient id="fill-mobile-stacked-expanded" x1="0" y1="0" x2="0" y2="1"> <stop offset="0" style="stop-color: var(--chart-2); stop-opacity: 0.26" /> <stop offset="1" style="stop-color: var(--chart-2); stop-opacity: 0.03" /> </linearGradient> </defs> {#each context.series.visibleSeries as s (s.key)} <Area seriesKey={s.key} curve={curveNatural} fill="url(#fill-{s.key}-stacked-expanded)" fillOpacity={1} line={{ class: "stroke-2" }} /> {/each} {/snippet} {#snippet tooltip()} <Chart.Tooltip indicator="line" labelFormatter={(v: number) => data[v]?.month ?? ""} /> {/snippet} </AreaChart> </Chart.Container> </Card.Content> <Card.Footer class="flex-col items-stretch gap-1 px-6 pt-3.5 pb-5.5 text-[13.5px]"> <b class="flex items-center gap-2 font-medium"> Trending up by 5.2% this month <IconPlaceholder lucide="TrendingUpIcon" tabler="IconTrendingUp" hugeicons="AnalyticsUpIcon" phosphor="TrendUpIcon" remixicon="RiLineChartLine" class="size-4" /> </b> <span class="text-muted-foreground">January - June 2024</span> </Card.Footer></Card.Root>Legend
Section titled “Legend”import { Card, CardContent, CardDescription, CardHeader, CardTitle,} from "@/components/ui/card";import { type ChartConfig, ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartTooltipContent,} from "@/components/ui/chart";import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";
const data = [ { month: "January", desktop: 186, mobile: 80 }, { month: "February", desktop: 305, mobile: 200 }, { month: "March", desktop: 237, mobile: 120 }, { month: "April", desktop: 73, mobile: 190 }, { month: "May", desktop: 209, mobile: 130 }, { month: "June", desktop: 214, mobile: 140 },];
const config = { desktop: { label: "Desktop", color: "var(--chart-1)" }, mobile: { label: "Mobile", color: "var(--chart-2)" },} satisfies ChartConfig;
export default function Demo() { return ( <Card className="w-full max-w-sm gap-0 py-0"> <CardHeader className="gap-1 px-6 pt-5.5 pb-0"> <CardTitle className="font-semibold text-base tracking-[-0.2px]"> Area Chart - Legend </CardTitle> <CardDescription className="text-[13.5px]"> Showing total visitors for the last 6 months </CardDescription> </CardHeader> <CardContent className="items-center px-5 pt-2.5 pb-4"> <ChartContainer config={config} className="aspect-auto h-44 w-full"> <AreaChart accessibilityLayer data={data} margin={{ left: 12, right: 12 }} > <defs> <linearGradient id="fill-desktop-legend" x1="0" y1="0" x2="0" y2="1" > <stop offset="0" stopColor="var(--color-desktop)" stopOpacity={0.26} /> <stop offset="1" stopColor="var(--color-desktop)" stopOpacity={0.03} /> </linearGradient> <linearGradient id="fill-mobile-legend" x1="0" y1="0" x2="0" y2="1" > <stop offset="0" stopColor="var(--color-mobile)" stopOpacity={0.26} /> <stop offset="1" stopColor="var(--color-mobile)" stopOpacity={0.03} /> </linearGradient> </defs> <CartesianGrid vertical={false} /> <XAxis dataKey="month" tickLine={false} axisLine={false} tickMargin={8} tickFormatter={(value) => value.slice(0, 3)} /> <ChartTooltip content={<ChartTooltipContent indicator="dot" />} /> <ChartLegend content={<ChartLegendContent />} /> <Area dataKey="desktop" type="natural" fill="url(#fill-desktop-legend)" fillOpacity={1} stroke="var(--color-desktop)" strokeWidth={2} stackId="a" /> <Area dataKey="mobile" type="natural" fill="url(#fill-mobile-legend)" fillOpacity={1} stroke="var(--color-mobile)" strokeWidth={2} stackId="a" /> </AreaChart> </ChartContainer> </CardContent> </Card> );}<script setup lang="ts">import { CurveType } from "@unovis/ts";import { VisArea, VisAxis, VisLine, VisXYContainer } from "@unovis/vue";import { Card, CardContent, CardDescription, CardHeader, CardTitle,} from "@/components/ui/card";import { type ChartConfig, ChartContainer, ChartCrosshair, ChartLegendContent, ChartTooltip, ChartTooltipContent, componentToString,} from "@/components/ui/chart";
type Row = { month: string; desktop: number; mobile: number };
const data: Row[] = [ { month: "January", desktop: 186, mobile: 80 }, { month: "February", desktop: 305, mobile: 200 }, { month: "March", desktop: 237, mobile: 120 }, { month: "April", desktop: 73, mobile: 190 }, { month: "May", desktop: 209, mobile: 130 }, { month: "June", desktop: 214, mobile: 140 },];
const config = { desktop: { label: "Desktop", color: "var(--chart-1)" }, mobile: { label: "Mobile", color: "var(--chart-2)" },} satisfies ChartConfig;
// Gradient washes (6-26%) under the 2px line.const fills = ["url(#fill-desktop-legend)", "url(#fill-mobile-legend)"];const svgDefs = '<linearGradient id="fill-desktop-legend" x1="0" y1="0" x2="0" y2="1"><stop offset="0" style="stop-color:var(--chart-1);stop-opacity:.26"/><stop offset="1" style="stop-color:var(--chart-1);stop-opacity:.03"/></linearGradient><linearGradient id="fill-mobile-legend" x1="0" y1="0" x2="0" y2="1"><stop offset="0" style="stop-color:var(--chart-2);stop-opacity:.26"/><stop offset="1" style="stop-color:var(--chart-2);stop-opacity:.03"/></linearGradient>';
const x = (_: Row, i: number) => i;const tickFormat = (i: number) => data[i]?.month.slice(0, 3) ?? "";const template = componentToString(config, ChartTooltipContent, { indicator: "dot", labelFormatter: (i) => data[Number(i)]?.month ?? "",});</script>
<template> <Card class="w-full max-w-sm gap-0 py-0"> <CardHeader class="gap-1 px-6 pt-5.5 pb-0"> <CardTitle class="font-semibold text-base tracking-[-0.2px]">Area Chart - Legend</CardTitle> <CardDescription class="text-[13.5px]">Showing total visitors for the last 6 months</CardDescription> </CardHeader> <CardContent class="items-center px-5 pt-2.5 pb-4"> <ChartContainer :config="config" class="aspect-auto h-[203px] w-full [&_[data-vis-xy-container]]:!h-44" cursor> <VisXYContainer :data="data" :margin="{ left: 12, right: 12 }" :y-domain="[0, undefined]" :svg-defs="svgDefs" ><VisArea :x="x" :y="[(d: Row) => d.desktop, (d: Row) => d.mobile]" :color="(_: Row, i: number) => fills[i]" :opacity="1" :curve-type="CurveType.Natural" /> <VisLine :x="x" :y="(d: Row) => d.desktop" :color="config.desktop.color" :curve-type="CurveType.Natural" /> <VisLine :x="x" :y="(d: Row) => d.desktop + d.mobile" :color="config.mobile.color" :curve-type="CurveType.Natural" /> <VisAxis type="x" :x="x" :tick-values="[0, 1, 2, 3, 4, 5]" :tick-format="tickFormat" :tick-line="false" :domain-line="false" :grid-line="false" /> <VisAxis type="y" :num-ticks="4" :tick-format="() => ''" :tick-line="false" :domain-line="false" /> <ChartTooltip /> <ChartCrosshair :x="x" :y="[(d: Row) => d.desktop, (d: Row) => d.desktop + d.mobile]" :template="template" :color="(_: Row, i: number) => [config.desktop.color, config.mobile.color][i]" /> </VisXYContainer> <ChartLegendContent /> </ChartContainer> </CardContent> </Card></template><script lang="ts"> import { Area, AreaChart } from "layerchart"; import * as Card from "$lib/components/ui/card"; import * as Chart from "$lib/components/ui/chart";
const data = [ { i: 0, month: "January", desktop: 186, mobile: 80 }, { i: 1, month: "February", desktop: 305, mobile: 200 }, { i: 2, month: "March", desktop: 237, mobile: 120 }, { i: 3, month: "April", desktop: 73, mobile: 190 }, { i: 4, month: "May", desktop: 209, mobile: 130 }, { i: 5, month: "June", desktop: 214, mobile: 140 }, ];
const config = { desktop: { label: "Desktop", color: "var(--chart-1)" }, mobile: { label: "Mobile", color: "var(--chart-2)" }, } satisfies Chart.ChartConfig;
// d3 `curveNatural` (natural cubic spline); d3-shape is not a dependency of the docs app. function controlPoints(p: number[]): [number[], number[]] { const n = p.length - 1; const a = new Array<number>(n); const b = new Array<number>(n); const r = new Array<number>(n); a[0] = 0; b[0] = 2; r[0] = p[0] + 2 * p[1]; for (let i = 1; i < n - 1; ++i) { a[i] = 1; b[i] = 4; r[i] = 4 * p[i] + 2 * p[i + 1]; } a[n - 1] = 2; b[n - 1] = 7; r[n - 1] = 8 * p[n - 1] + p[n]; for (let i = 1; i < n; ++i) { const m = a[i] / b[i - 1]; b[i] -= m; r[i] -= m * r[i - 1]; } a[n - 1] = r[n - 1] / b[n - 1]; for (let i = n - 2; i >= 0; --i) a[i] = (r[i] - a[i + 1]) / b[i]; b[n - 1] = (p[n] + a[n - 1]) / 2; for (let i = 0; i < n - 1; ++i) b[i] = 2 * p[i + 1] - a[i + 1]; return [a, b]; }
// biome-ignore lint/suspicious/noExplicitAny: d3 curve factory shape const curveNatural = (context: any) => { let line = Number.NaN; let xs: number[] = []; let ys: number[] = []; return { areaStart() { line = 0; }, areaEnd() { line = Number.NaN; }, lineStart() { xs = []; ys = []; }, lineEnd() { const n = xs.length; if (n) { if (line) context.lineTo(xs[0], ys[0]); else context.moveTo(xs[0], ys[0]); if (n === 2) context.lineTo(xs[1], ys[1]); else if (n > 2) { const [px0, px1] = controlPoints(xs); const [py0, py1] = controlPoints(ys); for (let i = 0; i < n - 1; ++i) { context.bezierCurveTo(px0[i], py0[i], px1[i], py1[i], xs[i + 1], ys[i + 1]); } } } if (line || (line !== 0 && n === 1)) context.closePath(); line = 1 - line; }, point(px: number, py: number) { xs.push(+px); ys.push(+py); }, }; };
</script>
<Card.Root class="w-full max-w-sm gap-0 py-0"> <Card.Header class="gap-1 px-6 pt-5.5 pb-0"> <Card.Title class="font-semibold text-base tracking-[-0.2px]">Area Chart - Legend</Card.Title> <Card.Description class="text-[13.5px]">Showing total visitors for the last 6 months</Card.Description> </Card.Header> <Card.Content class="items-center px-5 pt-2.5 pb-4"> <Chart.Container {config} class="aspect-auto h-44 w-full"> <AreaChart {data} x="i" seriesLayout="stack" legend series={[ { key: "desktop", label: config.desktop.label, color: "var(--color-desktop)" }, { key: "mobile", label: config.mobile.label, color: "var(--color-mobile)" }, ]} padding={{ left: 12, right: 12, top: 4, bottom: 20 }} props={{ xAxis: { format: (v: number) => data[v]?.month.slice(0, 3) ?? "", ticks: 6 }, yAxis: { format: () => "" }, }} > {#snippet marks({ context })} <defs> <linearGradient id="fill-desktop-legend" x1="0" y1="0" x2="0" y2="1"> <stop offset="0" style="stop-color: var(--chart-1); stop-opacity: 0.26" /> <stop offset="1" style="stop-color: var(--chart-1); stop-opacity: 0.03" /> </linearGradient> <linearGradient id="fill-mobile-legend" x1="0" y1="0" x2="0" y2="1"> <stop offset="0" style="stop-color: var(--chart-2); stop-opacity: 0.26" /> <stop offset="1" style="stop-color: var(--chart-2); stop-opacity: 0.03" /> </linearGradient> </defs> {#each context.series.visibleSeries as s (s.key)} <Area seriesKey={s.key} curve={curveNatural} fill="url(#fill-{s.key}-legend)" fillOpacity={1} line={{ class: "stroke-2" }} /> {/each} {/snippet} {#snippet tooltip()} <Chart.Tooltip indicator="dot" labelFormatter={(v: number) => data[v]?.month ?? ""} /> {/snippet} </AreaChart> </Chart.Container> </Card.Content></Card.Root>import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle,} from "@/components/ui/card";import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent,} from "@/components/ui/chart";import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";import { IconPlaceholder } from "@/edmi/icon-placeholder";
const data = [ { month: "January", desktop: 186, mobile: 80 }, { month: "February", desktop: 305, mobile: 200 }, { month: "March", desktop: 237, mobile: 120 }, { month: "April", desktop: 73, mobile: 190 }, { month: "May", desktop: 209, mobile: 130 }, { month: "June", desktop: 214, mobile: 140 },];
const config = { desktop: { label: "Desktop", color: "var(--chart-1)" },} satisfies ChartConfig;
export default function Demo() { return ( <Card className="w-full max-w-sm gap-0 py-0"> <CardHeader className="gap-1 px-6 pt-5.5 pb-0"> <CardTitle className="font-semibold text-base tracking-[-0.2px]"> Area Chart - Axes </CardTitle> <CardDescription className="text-[13.5px]"> Showing total visitors for the last 6 months </CardDescription> </CardHeader> <CardContent className="items-center px-5 pt-2.5 pb-0"> <ChartContainer config={config} className="aspect-auto h-44 w-full"> <AreaChart accessibilityLayer data={data} margin={{ left: 4, right: 12 }} > <defs> <linearGradient id="fill-desktop-axes" x1="0" y1="0" x2="0" y2="1" > <stop offset="0" stopColor="var(--color-desktop)" stopOpacity={0.26} /> <stop offset="1" stopColor="var(--color-desktop)" stopOpacity={0.03} /> </linearGradient> </defs> <CartesianGrid vertical={false} /> <XAxis dataKey="month" tickLine={false} axisLine={false} tickMargin={8} tickFormatter={(value) => value.slice(0, 3)} /> <YAxis tickLine={false} axisLine={false} tickMargin={8} width={40} domain={[0, 400]} ticks={[0, 133, 266, 400]} /> <ChartTooltip content={<ChartTooltipContent indicator="line" />} /> <Area dataKey="desktop" type="natural" fill="url(#fill-desktop-axes)" fillOpacity={1} stroke="var(--color-desktop)" strokeWidth={2} /> </AreaChart> </ChartContainer> </CardContent> <CardFooter className="flex-col items-stretch gap-1 px-6 pt-3.5 pb-5.5 text-[13.5px]"> <b className="flex items-center gap-2 font-medium"> Trending up by 5.2% this month <IconPlaceholder lucide="TrendingUpIcon" tabler="IconTrendingUp" hugeicons="AnalyticsUpIcon" phosphor="TrendUpIcon" remixicon="RiLineChartLine" className="size-4" /> </b> <span className="text-muted-foreground">January - June 2024</span> </CardFooter> </Card> );}<script setup lang="ts">import { CurveType } from "@unovis/ts";import { VisArea, VisAxis, VisLine, VisXYContainer } from "@unovis/vue";import { TrendingUpIcon } from "@lucide/vue";import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle,} from "@/components/ui/card";import { type ChartConfig, ChartContainer, ChartCrosshair, ChartTooltip, ChartTooltipContent, componentToString,} from "@/components/ui/chart";
type Row = { month: string; desktop: number; mobile: number };
const data: Row[] = [ { month: "January", desktop: 186, mobile: 80 }, { month: "February", desktop: 305, mobile: 200 }, { month: "March", desktop: 237, mobile: 120 }, { month: "April", desktop: 73, mobile: 190 }, { month: "May", desktop: 209, mobile: 130 }, { month: "June", desktop: 214, mobile: 140 },];
const config = { desktop: { label: "Desktop", color: "var(--chart-1)" },} satisfies ChartConfig;
// Gradient washes (6-26%) under the 2px line.const fills = ["url(#fill-desktop-axes)"];const svgDefs = '<linearGradient id="fill-desktop-axes" x1="0" y1="0" x2="0" y2="1"><stop offset="0" style="stop-color:var(--chart-1);stop-opacity:.26"/><stop offset="1" style="stop-color:var(--chart-1);stop-opacity:.03"/></linearGradient>';
const x = (_: Row, i: number) => i;const tickFormat = (i: number) => data[i]?.month.slice(0, 3) ?? "";const template = componentToString(config, ChartTooltipContent, { indicator: "line", labelFormatter: (i) => data[Number(i)]?.month ?? "",});</script>
<template> <Card class="w-full max-w-sm gap-0 py-0"> <CardHeader class="gap-1 px-6 pt-5.5 pb-0"> <CardTitle class="font-semibold text-base tracking-[-0.2px]">Area Chart - Axes</CardTitle> <CardDescription class="text-[13.5px]">Showing total visitors for the last 6 months</CardDescription> </CardHeader> <CardContent class="items-center px-5 pt-2.5 pb-0"> <ChartContainer :config="config" class="aspect-auto h-44 w-full" cursor> <VisXYContainer :data="data" :margin="{ left: 0, right: 12 }" :y-domain="[0, 400]" :svg-defs="svgDefs" > <VisArea :x="x" :y="(d: Row) => d.desktop" :color="() => fills[0]" :opacity="1" :curve-type="CurveType.Natural" /> <VisLine :x="x" :y="(d: Row) => d.desktop" :color="config.desktop.color" :curve-type="CurveType.Natural" /> <VisAxis type="x" :x="x" :tick-values="[0, 1, 2, 3, 4, 5]" :tick-format="tickFormat" :tick-line="false" :domain-line="false" :grid-line="false" /> <VisAxis type="y" :tick-values="[0, 133, 266, 400]" :tick-line="false" :domain-line="false" /> <ChartTooltip /> <ChartCrosshair :x="x" :y="[(d: Row) => d.desktop]" :template="template" :color="(_: Row, i: number) => [config.desktop.color][i]" /> </VisXYContainer> </ChartContainer> </CardContent> <CardFooter class="flex-col items-stretch gap-1 px-6 pt-3.5 pb-5.5 text-[13.5px]"> <b class="flex items-center gap-2 font-medium"> Trending up by 5.2% this month <TrendingUpIcon class="size-4" /> </b> <span class="text-muted-foreground">January - June 2024</span> </CardFooter> </Card></template><script lang="ts"> import { Area, AreaChart } from "layerchart"; import IconPlaceholder from "$lib/components/icon-placeholder/icon-placeholder.svelte"; import * as Card from "$lib/components/ui/card"; import * as Chart from "$lib/components/ui/chart";
const data = [ { i: 0, month: "January", desktop: 186, mobile: 80 }, { i: 1, month: "February", desktop: 305, mobile: 200 }, { i: 2, month: "March", desktop: 237, mobile: 120 }, { i: 3, month: "April", desktop: 73, mobile: 190 }, { i: 4, month: "May", desktop: 209, mobile: 130 }, { i: 5, month: "June", desktop: 214, mobile: 140 }, ];
const config = { desktop: { label: "Desktop", color: "var(--chart-1)" }, } satisfies Chart.ChartConfig;
// d3 `curveNatural` (natural cubic spline); d3-shape is not a dependency of the docs app. function controlPoints(p: number[]): [number[], number[]] { const n = p.length - 1; const a = new Array<number>(n); const b = new Array<number>(n); const r = new Array<number>(n); a[0] = 0; b[0] = 2; r[0] = p[0] + 2 * p[1]; for (let i = 1; i < n - 1; ++i) { a[i] = 1; b[i] = 4; r[i] = 4 * p[i] + 2 * p[i + 1]; } a[n - 1] = 2; b[n - 1] = 7; r[n - 1] = 8 * p[n - 1] + p[n]; for (let i = 1; i < n; ++i) { const m = a[i] / b[i - 1]; b[i] -= m; r[i] -= m * r[i - 1]; } a[n - 1] = r[n - 1] / b[n - 1]; for (let i = n - 2; i >= 0; --i) a[i] = (r[i] - a[i + 1]) / b[i]; b[n - 1] = (p[n] + a[n - 1]) / 2; for (let i = 0; i < n - 1; ++i) b[i] = 2 * p[i + 1] - a[i + 1]; return [a, b]; }
// biome-ignore lint/suspicious/noExplicitAny: d3 curve factory shape const curveNatural = (context: any) => { let line = Number.NaN; let xs: number[] = []; let ys: number[] = []; return { areaStart() { line = 0; }, areaEnd() { line = Number.NaN; }, lineStart() { xs = []; ys = []; }, lineEnd() { const n = xs.length; if (n) { if (line) context.lineTo(xs[0], ys[0]); else context.moveTo(xs[0], ys[0]); if (n === 2) context.lineTo(xs[1], ys[1]); else if (n > 2) { const [px0, px1] = controlPoints(xs); const [py0, py1] = controlPoints(ys); for (let i = 0; i < n - 1; ++i) { context.bezierCurveTo(px0[i], py0[i], px1[i], py1[i], xs[i + 1], ys[i + 1]); } } } if (line || (line !== 0 && n === 1)) context.closePath(); line = 1 - line; }, point(px: number, py: number) { xs.push(+px); ys.push(+py); }, }; };
</script>
<Card.Root class="w-full max-w-sm gap-0 py-0"> <Card.Header class="gap-1 px-6 pt-5.5 pb-0"> <Card.Title class="font-semibold text-base tracking-[-0.2px]">Area Chart - Axes</Card.Title> <Card.Description class="text-[13.5px]">Showing total visitors for the last 6 months</Card.Description> </Card.Header> <Card.Content class="items-center px-5 pt-2.5 pb-0"> <Chart.Container {config} class="aspect-auto h-44 w-full"> <AreaChart {data} x="i" seriesLayout="overlap" yDomain={[0, 400]} series={[ { key: "desktop", label: config.desktop.label, color: "var(--color-desktop)" }, ]} padding={{ left: 28, right: 12, top: 4, bottom: 20 }} props={{ xAxis: { format: (v: number) => data[v]?.month.slice(0, 3) ?? "", ticks: 6 }, yAxis: { ticks: [0, 133, 266, 400], format: (v: number) => String(v) }, }} > {#snippet marks({ context })} <defs> <linearGradient id="fill-desktop-axes" x1="0" y1="0" x2="0" y2="1"> <stop offset="0" style="stop-color: var(--chart-1); stop-opacity: 0.26" /> <stop offset="1" style="stop-color: var(--chart-1); stop-opacity: 0.03" /> </linearGradient> </defs> {#each context.series.visibleSeries as s (s.key)} <Area seriesKey={s.key} curve={curveNatural} fill="url(#fill-{s.key}-axes)" fillOpacity={1} line={{ class: "stroke-2" }} /> {/each} {/snippet} {#snippet tooltip()} <Chart.Tooltip indicator="line" labelFormatter={(v: number) => data[v]?.month ?? ""} /> {/snippet} </AreaChart> </Chart.Container> </Card.Content> <Card.Footer class="flex-col items-stretch gap-1 px-6 pt-3.5 pb-5.5 text-[13.5px]"> <b class="flex items-center gap-2 font-medium"> Trending up by 5.2% this month <IconPlaceholder lucide="TrendingUpIcon" tabler="IconTrendingUp" hugeicons="AnalyticsUpIcon" phosphor="TrendUpIcon" remixicon="RiLineChartLine" class="size-4" /> </b> <span class="text-muted-foreground">January - June 2024</span> </Card.Footer></Card.Root>import { Card, CardContent, CardDescription, CardHeader, CardTitle,} from "@/components/ui/card";import { type ChartConfig, ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartTooltipContent,} from "@/components/ui/chart";import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";import { IconPlaceholder } from "@/edmi/icon-placeholder";
const data = [ { month: "January", desktop: 186, mobile: 80 }, { month: "February", desktop: 305, mobile: 200 }, { month: "March", desktop: 237, mobile: 120 }, { month: "April", desktop: 73, mobile: 190 }, { month: "May", desktop: 209, mobile: 130 }, { month: "June", desktop: 214, mobile: 140 },];
const config = { desktop: { label: "Desktop", color: "var(--chart-1)", icon: DesktopIcon }, mobile: { label: "Mobile", color: "var(--chart-2)", icon: MobileIcon },} satisfies ChartConfig;
function DesktopIcon() { return ( <IconPlaceholder lucide="MonitorIcon" tabler="IconDeviceDesktop" hugeicons="ComputerIcon" phosphor="MonitorIcon" remixicon="RiComputerLine" /> );}
function MobileIcon() { return ( <IconPlaceholder lucide="SmartphoneIcon" tabler="IconDeviceMobile" hugeicons="SmartPhone01Icon" phosphor="DeviceMobileIcon" remixicon="RiSmartphoneLine" /> );}
export default function Demo() { return ( <Card className="w-full max-w-sm gap-0 py-0"> <CardHeader className="gap-1 px-6 pt-5.5 pb-0"> <CardTitle className="font-semibold text-base tracking-[-0.2px]"> Area Chart - Icons </CardTitle> <CardDescription className="text-[13.5px]"> Showing total visitors for the last 6 months </CardDescription> </CardHeader> <CardContent className="items-center px-5 pt-2.5 pb-4"> <ChartContainer config={config} className="aspect-auto h-44 w-full"> <AreaChart accessibilityLayer data={data} margin={{ left: 12, right: 12 }} > <defs> <linearGradient id="fill-desktop-icons" x1="0" y1="0" x2="0" y2="1" > <stop offset="0" stopColor="var(--color-desktop)" stopOpacity={0.26} /> <stop offset="1" stopColor="var(--color-desktop)" stopOpacity={0.03} /> </linearGradient> <linearGradient id="fill-mobile-icons" x1="0" y1="0" x2="0" y2="1" > <stop offset="0" stopColor="var(--color-mobile)" stopOpacity={0.26} /> <stop offset="1" stopColor="var(--color-mobile)" stopOpacity={0.03} /> </linearGradient> </defs> <CartesianGrid vertical={false} /> <XAxis dataKey="month" tickLine={false} axisLine={false} tickMargin={8} tickFormatter={(value) => value.slice(0, 3)} /> <ChartTooltip content={<ChartTooltipContent indicator="dot" />} /> <ChartLegend content={<ChartLegendContent />} /> <Area dataKey="desktop" type="natural" fill="url(#fill-desktop-icons)" fillOpacity={1} stroke="var(--color-desktop)" strokeWidth={2} /> <Area dataKey="mobile" type="natural" fill="url(#fill-mobile-icons)" fillOpacity={1} stroke="var(--color-mobile)" strokeWidth={2} /> </AreaChart> </ChartContainer> </CardContent> </Card> );}<script setup lang="ts">import { CurveType } from "@unovis/ts";import { VisArea, VisAxis, VisLine, VisXYContainer } from "@unovis/vue";import { GlobeIcon, MonitorIcon } from "@lucide/vue";import { Card, CardContent, CardDescription, CardHeader, CardTitle,} from "@/components/ui/card";import { type ChartConfig, ChartContainer, ChartCrosshair, ChartLegendContent, ChartTooltip, ChartTooltipContent, componentToString,} from "@/components/ui/chart";
type Row = { month: string; desktop: number; mobile: number };
const data: Row[] = [ { month: "January", desktop: 186, mobile: 80 }, { month: "February", desktop: 305, mobile: 200 }, { month: "March", desktop: 237, mobile: 120 }, { month: "April", desktop: 73, mobile: 190 }, { month: "May", desktop: 209, mobile: 130 }, { month: "June", desktop: 214, mobile: 140 },];
const config = { desktop: { label: "Desktop", color: "var(--chart-1)", icon: MonitorIcon }, mobile: { label: "Mobile", color: "var(--chart-2)", icon: GlobeIcon },} satisfies ChartConfig;
// Gradient washes (6-26%) under the 2px line.const fills = ["url(#fill-desktop-icons)", "url(#fill-mobile-icons)"];const svgDefs = '<linearGradient id="fill-desktop-icons" x1="0" y1="0" x2="0" y2="1"><stop offset="0" style="stop-color:var(--chart-1);stop-opacity:.26"/><stop offset="1" style="stop-color:var(--chart-1);stop-opacity:.03"/></linearGradient><linearGradient id="fill-mobile-icons" x1="0" y1="0" x2="0" y2="1"><stop offset="0" style="stop-color:var(--chart-2);stop-opacity:.26"/><stop offset="1" style="stop-color:var(--chart-2);stop-opacity:.03"/></linearGradient>';
const x = (_: Row, i: number) => i;const tickFormat = (i: number) => data[i]?.month.slice(0, 3) ?? "";const template = componentToString(config, ChartTooltipContent, { indicator: "dot", labelFormatter: (i) => data[Number(i)]?.month ?? "",});</script>
<template> <Card class="w-full max-w-sm gap-0 py-0"> <CardHeader class="gap-1 px-6 pt-5.5 pb-0"> <CardTitle class="font-semibold text-base tracking-[-0.2px]">Area Chart - Icons</CardTitle> <CardDescription class="text-[13.5px]">Showing total visitors for the last 6 months</CardDescription> </CardHeader> <CardContent class="items-center px-5 pt-2.5 pb-4"> <ChartContainer :config="config" class="aspect-auto h-[203px] w-full [&_[data-vis-xy-container]]:!h-44" cursor> <VisXYContainer :data="data" :margin="{ left: 12, right: 12 }" :y-domain="[0, undefined]" :svg-defs="svgDefs" > <VisArea :x="x" :y="(d: Row) => d.desktop" :color="() => fills[0]" :opacity="1" :curve-type="CurveType.Natural" /> <VisLine :x="x" :y="(d: Row) => d.desktop" :color="config.desktop.color" :curve-type="CurveType.Natural" /> <VisArea :x="x" :y="(d: Row) => d.mobile" :color="() => fills[1]" :opacity="1" :curve-type="CurveType.Natural" /> <VisLine :x="x" :y="(d: Row) => d.mobile" :color="config.mobile.color" :curve-type="CurveType.Natural" /> <VisAxis type="x" :x="x" :tick-values="[0, 1, 2, 3, 4, 5]" :tick-format="tickFormat" :tick-line="false" :domain-line="false" :grid-line="false" /> <VisAxis type="y" :num-ticks="4" :tick-format="() => ''" :tick-line="false" :domain-line="false" /> <ChartTooltip /> <ChartCrosshair :x="x" :y="[(d: Row) => d.desktop, (d: Row) => d.mobile]" :template="template" :color="(_: Row, i: number) => [config.desktop.color, config.mobile.color][i]" /> </VisXYContainer> <ChartLegendContent /> </ChartContainer> </CardContent> </Card></template><script lang="ts"> import { Area, AreaChart } from "layerchart"; import DeviceMobileIcon from "phosphor-svelte/lib/DeviceMobileIcon"; import MonitorIcon from "phosphor-svelte/lib/MonitorIcon"; import * as Card from "$lib/components/ui/card"; import * as Chart from "$lib/components/ui/chart";
const data = [ { i: 0, month: "January", desktop: 186, mobile: 80 }, { i: 1, month: "February", desktop: 305, mobile: 200 }, { i: 2, month: "March", desktop: 237, mobile: 120 }, { i: 3, month: "April", desktop: 73, mobile: 190 }, { i: 4, month: "May", desktop: 209, mobile: 130 }, { i: 5, month: "June", desktop: 214, mobile: 140 }, ];
const config = { desktop: { label: "Desktop", color: "var(--chart-1)", icon: MonitorIcon }, mobile: { label: "Mobile", color: "var(--chart-2)", icon: DeviceMobileIcon }, } satisfies Chart.ChartConfig;
// d3 `curveNatural` (natural cubic spline); d3-shape is not a dependency of the docs app. function controlPoints(p: number[]): [number[], number[]] { const n = p.length - 1; const a = new Array<number>(n); const b = new Array<number>(n); const r = new Array<number>(n); a[0] = 0; b[0] = 2; r[0] = p[0] + 2 * p[1]; for (let i = 1; i < n - 1; ++i) { a[i] = 1; b[i] = 4; r[i] = 4 * p[i] + 2 * p[i + 1]; } a[n - 1] = 2; b[n - 1] = 7; r[n - 1] = 8 * p[n - 1] + p[n]; for (let i = 1; i < n; ++i) { const m = a[i] / b[i - 1]; b[i] -= m; r[i] -= m * r[i - 1]; } a[n - 1] = r[n - 1] / b[n - 1]; for (let i = n - 2; i >= 0; --i) a[i] = (r[i] - a[i + 1]) / b[i]; b[n - 1] = (p[n] + a[n - 1]) / 2; for (let i = 0; i < n - 1; ++i) b[i] = 2 * p[i + 1] - a[i + 1]; return [a, b]; }
// biome-ignore lint/suspicious/noExplicitAny: d3 curve factory shape const curveNatural = (context: any) => { let line = Number.NaN; let xs: number[] = []; let ys: number[] = []; return { areaStart() { line = 0; }, areaEnd() { line = Number.NaN; }, lineStart() { xs = []; ys = []; }, lineEnd() { const n = xs.length; if (n) { if (line) context.lineTo(xs[0], ys[0]); else context.moveTo(xs[0], ys[0]); if (n === 2) context.lineTo(xs[1], ys[1]); else if (n > 2) { const [px0, px1] = controlPoints(xs); const [py0, py1] = controlPoints(ys); for (let i = 0; i < n - 1; ++i) { context.bezierCurveTo(px0[i], py0[i], px1[i], py1[i], xs[i + 1], ys[i + 1]); } } } if (line || (line !== 0 && n === 1)) context.closePath(); line = 1 - line; }, point(px: number, py: number) { xs.push(+px); ys.push(+py); }, }; };
</script>
<Card.Root class="w-full max-w-sm gap-0 py-0"> <Card.Header class="gap-1 px-6 pt-5.5 pb-0"> <Card.Title class="font-semibold text-base tracking-[-0.2px]">Area Chart - Icons</Card.Title> <Card.Description class="text-[13.5px]">Showing total visitors for the last 6 months</Card.Description> </Card.Header> <Card.Content class="items-center px-5 pt-2.5 pb-4"> <Chart.Container {config} class="aspect-auto h-auto w-full flex-col"> <div class="h-44 w-full"> <AreaChart {data} x="i" seriesLayout="overlap" series={[ { key: "desktop", label: config.desktop.label, color: "var(--color-desktop)" }, { key: "mobile", label: config.mobile.label, color: "var(--color-mobile)" }, ]} padding={{ left: 12, right: 12, top: 4, bottom: 20 }} props={{ xAxis: { format: (v: number) => data[v]?.month.slice(0, 3) ?? "", ticks: 6 }, yAxis: { format: () => "" }, }} > {#snippet marks({ context })} <defs> <linearGradient id="fill-desktop-icons" x1="0" y1="0" x2="0" y2="1"> <stop offset="0" style="stop-color: var(--chart-1); stop-opacity: 0.26" /> <stop offset="1" style="stop-color: var(--chart-1); stop-opacity: 0.03" /> </linearGradient> <linearGradient id="fill-mobile-icons" x1="0" y1="0" x2="0" y2="1"> <stop offset="0" style="stop-color: var(--chart-2); stop-opacity: 0.26" /> <stop offset="1" style="stop-color: var(--chart-2); stop-opacity: 0.03" /> </linearGradient> </defs> {#each context.series.visibleSeries as s (s.key)} <Area seriesKey={s.key} curve={curveNatural} fill="url(#fill-{s.key}-icons)" fillOpacity={1} line={{ class: "stroke-2" }} /> {/each} {/snippet} {#snippet tooltip()} <Chart.Tooltip indicator="dot" labelFormatter={(v: number) => data[v]?.month ?? ""} /> {/snippet} </AreaChart> </div> <div class="flex flex-wrap items-center justify-center gap-x-4 gap-y-1.5 pt-2.5 text-[12.5px] text-foreground-2"> {#each Object.values(config) as item} <div class="flex items-center gap-1.5 [&>svg]:size-3 [&>svg]:text-muted-foreground"> <item.icon /> {item.label} </div> {/each} </div> </Chart.Container> </Card.Content></Card.Root>Gradient
Section titled “Gradient”import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle,} from "@/components/ui/card";import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent,} from "@/components/ui/chart";import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";import { IconPlaceholder } from "@/edmi/icon-placeholder";
const data = [ { month: "January", desktop: 186, mobile: 80 }, { month: "February", desktop: 305, mobile: 200 }, { month: "March", desktop: 237, mobile: 120 }, { month: "April", desktop: 73, mobile: 190 }, { month: "May", desktop: 209, mobile: 130 }, { month: "June", desktop: 214, mobile: 140 },];
const config = { desktop: { label: "Desktop", color: "var(--chart-1)" }, mobile: { label: "Mobile", color: "var(--chart-2)" },} satisfies ChartConfig;
export default function Demo() { return ( <Card className="w-full max-w-sm gap-0 py-0"> <CardHeader className="gap-1 px-6 pt-5.5 pb-0"> <CardTitle className="font-semibold text-base tracking-[-0.2px]"> Area Chart - Gradient </CardTitle> <CardDescription className="text-[13.5px]"> Showing total visitors for the last 6 months </CardDescription> </CardHeader> <CardContent className="items-center px-5 pt-2.5 pb-0"> <ChartContainer config={config} className="aspect-auto h-44 w-full"> <AreaChart accessibilityLayer data={data} margin={{ left: 12, right: 12 }} > <defs> <linearGradient id="fill-desktop-gradient" x1="0" y1="0" x2="0" y2="1" > <stop offset="0" stopColor="var(--color-desktop)" stopOpacity={0.26} /> <stop offset="1" stopColor="var(--color-desktop)" stopOpacity={0.03} /> </linearGradient> <linearGradient id="fill-mobile-gradient" x1="0" y1="0" x2="0" y2="1" > <stop offset="0" stopColor="var(--color-mobile)" stopOpacity={0.26} /> <stop offset="1" stopColor="var(--color-mobile)" stopOpacity={0.03} /> </linearGradient> </defs> <CartesianGrid vertical={false} /> <XAxis dataKey="month" tickLine={false} axisLine={false} tickMargin={8} tickFormatter={(value) => value.slice(0, 3)} /> <ChartTooltip content={<ChartTooltipContent indicator="dot" />} /> <Area dataKey="desktop" type="natural" fill="url(#fill-desktop-gradient)" fillOpacity={1} stroke="var(--color-desktop)" strokeWidth={2} /> <Area dataKey="mobile" type="natural" fill="url(#fill-mobile-gradient)" fillOpacity={1} stroke="var(--color-mobile)" strokeWidth={2} /> </AreaChart> </ChartContainer> </CardContent> <CardFooter className="flex-col items-stretch gap-1 px-6 pt-3.5 pb-5.5 text-[13.5px]"> <b className="flex items-center gap-2 font-medium"> Trending up by 5.2% this month <IconPlaceholder lucide="TrendingUpIcon" tabler="IconTrendingUp" hugeicons="AnalyticsUpIcon" phosphor="TrendUpIcon" remixicon="RiLineChartLine" className="size-4" /> </b> <span className="text-muted-foreground">January - June 2024</span> </CardFooter> </Card> );}<script setup lang="ts">import { CurveType } from "@unovis/ts";import { VisArea, VisAxis, VisLine, VisXYContainer } from "@unovis/vue";import { TrendingUpIcon } from "@lucide/vue";import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle,} from "@/components/ui/card";import { type ChartConfig, ChartContainer, ChartCrosshair, ChartTooltip, ChartTooltipContent, componentToString,} from "@/components/ui/chart";
type Row = { month: string; desktop: number; mobile: number };
const data: Row[] = [ { month: "January", desktop: 186, mobile: 80 }, { month: "February", desktop: 305, mobile: 200 }, { month: "March", desktop: 237, mobile: 120 }, { month: "April", desktop: 73, mobile: 190 }, { month: "May", desktop: 209, mobile: 130 }, { month: "June", desktop: 214, mobile: 140 },];
const config = { desktop: { label: "Desktop", color: "var(--chart-1)" }, mobile: { label: "Mobile", color: "var(--chart-2)" },} satisfies ChartConfig;
// Gradient washes (6-26%) under the 2px line.const fills = ["url(#fill-desktop-gradient)", "url(#fill-mobile-gradient)"];const svgDefs = '<linearGradient id="fill-desktop-gradient" x1="0" y1="0" x2="0" y2="1"><stop offset="0" style="stop-color:var(--chart-1);stop-opacity:.26"/><stop offset="1" style="stop-color:var(--chart-1);stop-opacity:.03"/></linearGradient><linearGradient id="fill-mobile-gradient" x1="0" y1="0" x2="0" y2="1"><stop offset="0" style="stop-color:var(--chart-2);stop-opacity:.26"/><stop offset="1" style="stop-color:var(--chart-2);stop-opacity:.03"/></linearGradient>';
const x = (_: Row, i: number) => i;const tickFormat = (i: number) => data[i]?.month.slice(0, 3) ?? "";const template = componentToString(config, ChartTooltipContent, { indicator: "dot", labelFormatter: (i) => data[Number(i)]?.month ?? "",});</script>
<template> <Card class="w-full max-w-sm gap-0 py-0"> <CardHeader class="gap-1 px-6 pt-5.5 pb-0"> <CardTitle class="font-semibold text-base tracking-[-0.2px]">Area Chart - Gradient</CardTitle> <CardDescription class="text-[13.5px]">Showing total visitors for the last 6 months</CardDescription> </CardHeader> <CardContent class="items-center px-5 pt-2.5 pb-0"> <ChartContainer :config="config" class="aspect-auto h-44 w-full" cursor> <VisXYContainer :data="data" :margin="{ left: 12, right: 12 }" :y-domain="[0, undefined]" :svg-defs="svgDefs" > <VisArea :x="x" :y="(d: Row) => d.desktop" :color="() => fills[0]" :opacity="1" :curve-type="CurveType.Natural" /> <VisLine :x="x" :y="(d: Row) => d.desktop" :color="config.desktop.color" :curve-type="CurveType.Natural" /> <VisArea :x="x" :y="(d: Row) => d.mobile" :color="() => fills[1]" :opacity="1" :curve-type="CurveType.Natural" /> <VisLine :x="x" :y="(d: Row) => d.mobile" :color="config.mobile.color" :curve-type="CurveType.Natural" /> <VisAxis type="x" :x="x" :tick-values="[0, 1, 2, 3, 4, 5]" :tick-format="tickFormat" :tick-line="false" :domain-line="false" :grid-line="false" /> <VisAxis type="y" :num-ticks="4" :tick-format="() => ''" :tick-line="false" :domain-line="false" /> <ChartTooltip /> <ChartCrosshair :x="x" :y="[(d: Row) => d.desktop, (d: Row) => d.mobile]" :template="template" :color="(_: Row, i: number) => [config.desktop.color, config.mobile.color][i]" /> </VisXYContainer> </ChartContainer> </CardContent> <CardFooter class="flex-col items-stretch gap-1 px-6 pt-3.5 pb-5.5 text-[13.5px]"> <b class="flex items-center gap-2 font-medium"> Trending up by 5.2% this month <TrendingUpIcon class="size-4" /> </b> <span class="text-muted-foreground">January - June 2024</span> </CardFooter> </Card></template><script lang="ts"> import { Area, AreaChart } from "layerchart"; import IconPlaceholder from "$lib/components/icon-placeholder/icon-placeholder.svelte"; import * as Card from "$lib/components/ui/card"; import * as Chart from "$lib/components/ui/chart";
const data = [ { i: 0, month: "January", desktop: 186, mobile: 80 }, { i: 1, month: "February", desktop: 305, mobile: 200 }, { i: 2, month: "March", desktop: 237, mobile: 120 }, { i: 3, month: "April", desktop: 73, mobile: 190 }, { i: 4, month: "May", desktop: 209, mobile: 130 }, { i: 5, month: "June", desktop: 214, mobile: 140 }, ];
const config = { desktop: { label: "Desktop", color: "var(--chart-1)" }, mobile: { label: "Mobile", color: "var(--chart-2)" }, } satisfies Chart.ChartConfig;
// d3 `curveNatural` (natural cubic spline); d3-shape is not a dependency of the docs app. function controlPoints(p: number[]): [number[], number[]] { const n = p.length - 1; const a = new Array<number>(n); const b = new Array<number>(n); const r = new Array<number>(n); a[0] = 0; b[0] = 2; r[0] = p[0] + 2 * p[1]; for (let i = 1; i < n - 1; ++i) { a[i] = 1; b[i] = 4; r[i] = 4 * p[i] + 2 * p[i + 1]; } a[n - 1] = 2; b[n - 1] = 7; r[n - 1] = 8 * p[n - 1] + p[n]; for (let i = 1; i < n; ++i) { const m = a[i] / b[i - 1]; b[i] -= m; r[i] -= m * r[i - 1]; } a[n - 1] = r[n - 1] / b[n - 1]; for (let i = n - 2; i >= 0; --i) a[i] = (r[i] - a[i + 1]) / b[i]; b[n - 1] = (p[n] + a[n - 1]) / 2; for (let i = 0; i < n - 1; ++i) b[i] = 2 * p[i + 1] - a[i + 1]; return [a, b]; }
// biome-ignore lint/suspicious/noExplicitAny: d3 curve factory shape const curveNatural = (context: any) => { let line = Number.NaN; let xs: number[] = []; let ys: number[] = []; return { areaStart() { line = 0; }, areaEnd() { line = Number.NaN; }, lineStart() { xs = []; ys = []; }, lineEnd() { const n = xs.length; if (n) { if (line) context.lineTo(xs[0], ys[0]); else context.moveTo(xs[0], ys[0]); if (n === 2) context.lineTo(xs[1], ys[1]); else if (n > 2) { const [px0, px1] = controlPoints(xs); const [py0, py1] = controlPoints(ys); for (let i = 0; i < n - 1; ++i) { context.bezierCurveTo(px0[i], py0[i], px1[i], py1[i], xs[i + 1], ys[i + 1]); } } } if (line || (line !== 0 && n === 1)) context.closePath(); line = 1 - line; }, point(px: number, py: number) { xs.push(+px); ys.push(+py); }, }; };
</script>
<Card.Root class="w-full max-w-sm gap-0 py-0"> <Card.Header class="gap-1 px-6 pt-5.5 pb-0"> <Card.Title class="font-semibold text-base tracking-[-0.2px]">Area Chart - Gradient</Card.Title> <Card.Description class="text-[13.5px]">Showing total visitors for the last 6 months</Card.Description> </Card.Header> <Card.Content class="items-center px-5 pt-2.5 pb-0"> <Chart.Container {config} class="aspect-auto h-44 w-full"> <AreaChart {data} x="i" seriesLayout="overlap" series={[ { key: "desktop", label: config.desktop.label, color: "var(--color-desktop)" }, { key: "mobile", label: config.mobile.label, color: "var(--color-mobile)" }, ]} padding={{ left: 12, right: 12, top: 4, bottom: 20 }} props={{ xAxis: { format: (v: number) => data[v]?.month.slice(0, 3) ?? "", ticks: 6 }, yAxis: { format: () => "" }, }} > {#snippet marks({ context })} <defs> <linearGradient id="fill-desktop-gradient" x1="0" y1="0" x2="0" y2="1"> <stop offset="0" style="stop-color: var(--chart-1); stop-opacity: 0.26" /> <stop offset="1" style="stop-color: var(--chart-1); stop-opacity: 0.03" /> </linearGradient> <linearGradient id="fill-mobile-gradient" x1="0" y1="0" x2="0" y2="1"> <stop offset="0" style="stop-color: var(--chart-2); stop-opacity: 0.26" /> <stop offset="1" style="stop-color: var(--chart-2); stop-opacity: 0.03" /> </linearGradient> </defs> {#each context.series.visibleSeries as s (s.key)} <Area seriesKey={s.key} curve={curveNatural} fill="url(#fill-{s.key}-gradient)" fillOpacity={1} line={{ class: "stroke-2" }} /> {/each} {/snippet} {#snippet tooltip()} <Chart.Tooltip indicator="dot" labelFormatter={(v: number) => data[v]?.month ?? ""} /> {/snippet} </AreaChart> </Chart.Container> </Card.Content> <Card.Footer class="flex-col items-stretch gap-1 px-6 pt-3.5 pb-5.5 text-[13.5px]"> <b class="flex items-center gap-2 font-medium"> Trending up by 5.2% this month <IconPlaceholder lucide="TrendingUpIcon" tabler="IconTrendingUp" hugeicons="AnalyticsUpIcon" phosphor="TrendUpIcon" remixicon="RiLineChartLine" class="size-4" /> </b> <span class="text-muted-foreground">January - June 2024</span> </Card.Footer></Card.Root>