组件
图表 Chart
基于设计令牌的图表:Chart 组件渲染由 mark 工厂构建的 ChartDefinition,调色板(chartColors、chartSeriesRange)为 mark 提供当前主题的配色。
#基础用法
<script setup lang="ts">
import { areaY, chartColors, defineChart, ruleY } from "@bysages/charts";
import { scaleLinear } from "@bysages/charts/scales/linear";
import { scalePoint } from "@bysages/charts/scales/point";
import { Chart, type ChartDefinition } from "@bysages/charts/vue";
const weeks = [
{ day: "Mon", visits: 120 },
{ day: "Tue", visits: 148 },
{ day: "Wed", visits: 131 },
{ day: "Thu", visits: 165 },
{ day: "Fri", visits: 189 },
{ day: "Sat", visits: 96 },
{ day: "Sun", visits: 74 },
];
const mean = Math.round(weeks.reduce((sum, d) => sum + d.visits, 0) / weeks.length);
const definition = defineChart({
marks: [
areaY(weeks, {
x: "day",
y: "visits",
fill: chartColors.ink,
fillOpacity: 0.12,
strokeWidth: 2,
}),
ruleY([mean], { stroke: chartColors.danger, strokeDasharray: "4 3" }),
],
scales: {
x: { scale: () => scalePoint<string>().padding(0.3) },
y: { scale: scaleLinear, nice: true, grid: true, axis: { label: "Visits" } },
},
}) as ChartDefinition;
</script>
<template>
<div class="max-w-136">
<Chart :definition="definition" aria-label="Visits per day, area with a mean rule" />
</div>
</template>
<script setup lang="ts">
import { barY, chartColors, defineChart } from "@bysages/charts";
import { scaleBand } from "@bysages/charts/scales/band";
import { scaleLinear } from "@bysages/charts/scales/linear";
import { Chart, type ChartDefinition } from "@bysages/charts/vue";
const months = [
{ month: "Jan", orders: 42 },
{ month: "Feb", orders: 58 },
{ month: "Mar", orders: 51 },
{ month: "Apr", orders: 73 },
{ month: "May", orders: 86 },
];
const definition = defineChart({
marks: [barY(months, { x: "month", y: "orders", fill: chartColors.ink })],
scales: {
x: { scale: () => scaleBand<string>().padding(0.25) },
y: { scale: scaleLinear, nice: true, grid: true, axis: { label: "Orders" } },
},
}) as ChartDefinition;
</script>
<template>
<div class="max-w-136">
<Chart :definition="definition" aria-label="Monthly orders, bars" />
</div>
</template>
<script setup lang="ts">
import { boxY, chartColors, defineChart } from "@bysages/charts";
import { scaleBand } from "@bysages/charts/scales/band";
import { scaleLinear } from "@bysages/charts/scales/linear";
import { Chart, type ChartDefinition } from "@bysages/charts/vue";
const weeks = [
{ day: "Mon", visits: 120 },
{ day: "Tue", visits: 148 },
{ day: "Wed", visits: 131 },
{ day: "Thu", visits: 165 },
{ day: "Fri", visits: 189 },
{ day: "Sat", visits: 96 },
{ day: "Sun", visits: 74 },
];
const samples = weeks.flatMap((d) =>
[0.7, 0.9, 1, 1.1, 1.35].map((f) => ({ day: d.day, visits: Math.round(d.visits * f) })),
);
const definition = defineChart({
marks: [boxY(samples, { x: "day", y: "visits", fill: chartColors.ink })],
scales: {
x: { scale: () => scaleBand<string>().padding(0.35) },
y: { scale: scaleLinear, nice: true, grid: true, axis: { label: "Visits" } },
},
}) as ChartDefinition;
</script>
<template>
<div class="max-w-136">
<Chart :definition="definition" aria-label="Visits per day, boxes" />
</div>
</template>
<script setup lang="ts">
import { cell, defineChart } from "@bysages/charts";
import { scaleBand } from "@bysages/charts/scales/band";
import { scaleOrdinal } from "@bysages/charts/scales/ordinal";
import { Chart, type ChartDefinition } from "@bysages/charts/vue";
const slots = ["6a", "8a", "10a", "12p", "2p", "4p", "6p", "8p"];
const days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
// Fixed sample load per cell — the demo is deterministic, so the wall
// reads the same on the server and the client.
const load = [
[1, 2, 3, 2, 2, 1, 0, 0],
[2, 3, 4, 3, 2, 1, 0, 0],
[2, 4, 4, 3, 3, 2, 1, 0],
[1, 3, 4, 4, 2, 2, 1, 0],
[2, 3, 3, 4, 3, 2, 1, 1],
[0, 1, 2, 2, 3, 3, 2, 1],
[0, 0, 1, 2, 2, 2, 1, 0],
];
const rows = days.flatMap((day, d) => slots.map((slot, s) => ({ day, slot, tier: load[d][s] })));
// The ramp is the ink's own density: the busier the bench, the deeper
// the wash of the primary pigment — no second hue enters the drawing.
const inkTiers = [
"var(--bs-color-surface-inset)",
"color-mix(in oklab, var(--bs-color-primary) 25%, var(--bs-color-surface-1))",
"color-mix(in oklab, var(--bs-color-primary) 50%, var(--bs-color-surface-1))",
"color-mix(in oklab, var(--bs-color-primary) 75%, var(--bs-color-surface-1))",
"var(--bs-color-primary)",
];
const tierScale = scaleOrdinal<string, string>().domain(["0", "1", "2", "3", "4"]).range(inkTiers);
const definition = defineChart({
marks: [cell(rows, { x: "slot", y: "day", color: (d) => String(d.tier), inset: 2 })],
scales: {
x: { scale: () => scaleBand<string>().domain(slots).padding(0.05) },
y: { scale: () => scaleBand<string>().domain(days).padding(0.15), axis: {} },
},
color: { scale: tierScale },
}) as ChartDefinition;
</script>
<template>
<figure class="m-0 max-w-136">
<Chart :definition="definition" aria-label="Bench occupancy by day and time slot" />
<figcaption class="mt-3 flex items-center gap-2 text-xs text-tertiary">
<span>Quiet</span>
<span class="flex gap-1">
<span
v-for="t in inkTiers"
:key="t"
class="size-3 rounded-[2px]"
:style="{ background: t }"
/>
</span>
<span>Full</span>
</figcaption>
</figure>
</template>
<script setup lang="ts">
import { chartColors, defineChart, dot, lineY } from "@bysages/charts";
import { scaleLinear } from "@bysages/charts/scales/linear";
import { scalePoint } from "@bysages/charts/scales/point";
import { Chart, type ChartDefinition } from "@bysages/charts/vue";
const months = [
{ month: "Jan", orders: 42 },
{ month: "Feb", orders: 58 },
{ month: "Mar", orders: 51 },
{ month: "Apr", orders: 73 },
{ month: "May", orders: 86 },
];
const definition = defineChart({
marks: [
lineY(months, { x: "month", y: "orders", stroke: chartColors.ink, strokeWidth: 2 }),
dot(months, { x: "month", y: "orders", fill: chartColors.ink, r: 3 }),
],
scales: {
x: { scale: () => scalePoint<string>().padding(0.3) },
y: { scale: scaleLinear, nice: true, grid: true, axis: { label: "Orders" } },
},
}) as ChartDefinition;
</script>
<template>
<div class="max-w-136">
<Chart :definition="definition" aria-label="Monthly orders, line" />
</div>
</template>
<script setup lang="ts">
import { chartSeriesRange, defineChart, lineY } from "@bysages/charts";
import { scaleLinear } from "@bysages/charts/scales/linear";
import { scaleOrdinal } from "@bysages/charts/scales/ordinal";
import { scalePoint } from "@bysages/charts/scales/point";
import { Chart, type ChartDefinition } from "@bysages/charts/vue";
const rows = [
{ month: "Jan", region: "North", orders: 42 },
{ month: "Feb", region: "North", orders: 58 },
{ month: "Mar", region: "North", orders: 51 },
{ month: "Jan", region: "South", orders: 30 },
{ month: "Feb", region: "South", orders: 47 },
{ month: "Mar", region: "South", orders: 64 },
{ month: "Jan", region: "West", orders: 25 },
{ month: "Feb", region: "West", orders: 38 },
{ month: "Mar", region: "West", orders: 45 },
];
const color = scaleOrdinal<string, string>()
.domain(["North", "South", "West"])
.range(chartSeriesRange.slice(0, 3));
const definition = defineChart({
marks: [lineY(rows, { x: "month", y: "orders", z: "region", strokeWidth: 2 })],
scales: {
x: { scale: () => scalePoint<string>().padding(0.3) },
y: { scale: scaleLinear, nice: true, grid: true, axis: { label: "Orders" } },
},
color: { scale: color },
}) as ChartDefinition;
</script>
<template>
<div class="max-w-136">
<Chart :definition="definition" aria-label="Monthly orders by region, lines" />
</div>
</template>
Orders this season
8,214
+12% vs last season
<script setup lang="ts">
import { areaY, chartColors, defineChart, lineY } from "@bysages/charts";
import { scaleLinear } from "@bysages/charts/scales/linear";
import { scalePoint } from "@bysages/charts/scales/point";
import { Chart, type ChartDefinition } from "@bysages/charts/vue";
import { Stat } from "@bysages/vue";
const trend = [
{ m: "Jan", orders: 42 },
{ m: "Feb", orders: 38 },
{ m: "Mar", orders: 51 },
{ m: "Apr", orders: 47 },
{ m: "May", orders: 66 },
{ m: "Jun", orders: 61 },
{ m: "Jul", orders: 78 },
{ m: "Aug", orders: 86 },
];
// A sparkline is a trend reduced to its shape — no axes, no grid, the
// ink line and a breath of the same pigment under it. Numbers live in
// the stat beside it, not on the drawing.
const definition = defineChart({
marks: [
areaY(trend, { x: "m", y: "orders", fill: chartColors.ink, fillOpacity: 0.12 }),
lineY(trend, { x: "m", y: "orders", stroke: chartColors.ink, strokeWidth: 1.5 }),
],
scales: {
x: { scale: () => scalePoint<string>().padding(0.2), axis: false },
y: { scale: scaleLinear, nice: true, axis: false, grid: false },
},
}) as ChartDefinition;
</script>
<template>
<Stat.Root class="max-w-80!">
<Stat.Label>Orders this season</Stat.Label>
<div class="flex items-end justify-between gap-4">
<Stat.Value>8,214</Stat.Value>
<Chart
class="w-36!"
:height="48"
:definition="definition"
aria-label="Orders trend, sparkline"
/>
</div>
<Stat.Description direction="up">+12% vs last season</Stat.Description>
</Stat.Root>
</template>
<script setup lang="ts">
import { defineChart, waffleY } from "@bysages/charts";
import { scaleBand } from "@bysages/charts/scales/band";
import { scaleLinear } from "@bysages/charts/scales/linear";
import { Chart, type ChartDefinition } from "@bysages/charts/vue";
const weeks = [
{ day: "Mon", visits: 120 },
{ day: "Tue", visits: 148 },
{ day: "Wed", visits: 131 },
{ day: "Thu", visits: 165 },
{ day: "Fri", visits: 189 },
{ day: "Sat", visits: 96 },
{ day: "Sun", visits: 74 },
];
const definition = defineChart({
// The waffle's mark type outruns the definition overload here; the
// cast keeps the example honest about the runtime contract.
marks: [waffleY(weeks, { y: "visits", columns: 20, gap: 2 }) as any],
scales: {
x: { scale: () => scaleBand<string>().padding(0.1) },
y: { scale: scaleLinear, grid: false },
},
}) as ChartDefinition;
</script>
<template>
<div class="max-w-136">
<Chart :definition="definition" aria-label="Visits per day, waffle of squares" />
</div>
</template>