nyuchimzizi
Mzizi — an open-architecture project of the Bundu Foundation, operated and developed by Nyuchi. Built on the Five African Minerals palette.
Built by Nyuchi Africav4.0.39
Reusable Canvas-based time series chart with multi-series support, dual Y-axes, mixed line/bar types, custom tooltip and tick formatters. Built on canvas-chart. Draws lines, areas, and bars on a single canvas element. Production-proven in mukoko-weather for temperature trends, precipitation, humidity, UV index, and wind speed charts. Use this instead of composing individual Recharts chart blocks when you need a data-driven chart with dynamic series.
View the full component source code below.
"use client"
import * as React from "react"
import { CanvasChart, resolveColor, type CanvasChartConfig } from "@/components/ui/canvas-chart"
import { hexWithAlpha } from "@/lib/tokens"
import type { ChartData, ChartOptions } from "chart.js"
/* ═══════════════════════════════════════════════════════════════
TIME SERIES CHART — Layer 2 Primitive
Multi-series line/bar/area on a single canvas.
Mineral colors auto-assigned by index.
✅ TOKENS ✅ ARIA ✅ LOADING ✅ MOBILE ✅ MEMORY
═══════════════════════════════════════════════════════════════ */
export interface SeriesConfig {
key: string
label: string
color: string
type?: "line" | "bar"
fill?: boolean
dashed?: boolean
yAxisID?: string
order?: number
opacity?: number
}
export interface TimeSeriesChartProps {
data: Record<string, any>[]
labelKey: string
series: SeriesConfig[]
yAxes?: Record<string, {
min?: number; max?: number
position?: "left" | "right"
display?: boolean
format?: (v: number) => string
}>
tooltipLabel?: (label: string, value: number) => string
tooltipTitle?: (label: string) => string
xTickFormat?: (label: string, index: number) => string
maxTicksLimit?: number
aspect?: string
loading?: boolean
ariaLabel?: string
className?: string
}
export function TimeSeriesChart({
data, labelKey, series, yAxes,
tooltipLabel, tooltipTitle, xTickFormat,
maxTicksLimit = 8, aspect = "aspect-[2/1] sm:aspect-[16/5]",
loading = false, ariaLabel = "Time series chart", className,
}: TimeSeriesChartProps) {
const gridColor = resolveColor("var(--muted-foreground)")
const resolvedColors = React.useMemo(
() => series.map((s) => resolveColor(s.color)),
[series, gridColor]
)
const config = React.useMemo(() => {
const cfg: CanvasChartConfig = {}
for (const s of series) cfg[s.key] = { label: s.label, color: s.color }
return cfg
}, [series])
const chartData: ChartData<any> = React.useMemo(() => {
const labels = data.map((d) => d[labelKey])
const datasets = series.map((s, idx) => {
const color = resolvedColors[idx]
if (s.type === "bar") {
return {
type: "bar" as const, label: s.label,
data: data.map((d) => d[s.key]),
backgroundColor: hexWithAlpha(color, s.opacity ?? 0.6),
borderRadius: 2, yAxisID: s.yAxisID ?? "y", order: s.order ?? 2,
}
}
return {
type: "line" as const, label: s.label,
data: data.map((d) => d[s.key]),
borderColor: color,
backgroundColor: s.fill ? hexWithAlpha(color, s.opacity ?? 0.12) : undefined,
borderWidth: s.dashed ? 1.5 : 2,
borderDash: s.dashed ? [4, 3] : undefined,
fill: s.fill ?? false, tension: 0.4,
pointRadius: s.dashed ? 0 : 0, pointHitRadius: 8,
yAxisID: s.yAxisID ?? "y", order: s.order ?? 1,
}
})
return { labels, datasets }
}, [data, labelKey, series, resolvedColors])
const chartOptions: ChartOptions<any> = React.useMemo(() => {
const scales: Record<string, unknown> = {
x: {
grid: { display: false },
ticks: { color: gridColor, font: { size: 11 }, maxRotation: 0, autoSkip: true, maxTicksLimit,
...(xTickFormat ? { callback: (_v: any, i: number) => xTickFormat(data[i]?.[labelKey] ?? "", i) } : {}),
},
border: { display: false },
},
}
if (yAxes) {
for (const [id, cfg] of Object.entries(yAxes)) {
scales[id] = {
position: cfg.position ?? "left", display: cfg.display ?? true,
min: cfg.min, max: cfg.max,
grid: cfg.position === "right" ? { display: false } : { color: hexWithAlpha(gridColor, 0.15), drawTicks: false },
ticks: { color: gridColor, font: { size: 11 }, ...(cfg.format ? { callback: (v: number) => cfg.format!(v) } : {}) },
border: { display: false },
}
}
} else {
scales.y = { grid: { color: hexWithAlpha(gridColor, 0.15), drawTicks: false }, ticks: { color: gridColor, font: { size: 11 } }, border: { display: false } }
}
return {
scales,
plugins: {
tooltip: {
callbacks: {
title: tooltipTitle ? (items: any[]) => tooltipTitle(data[items[0]?.dataIndex ?? 0]?.[labelKey] ?? "") : undefined,
label: tooltipLabel ? (ctx: any) => tooltipLabel(ctx.dataset.label ?? "", ctx.parsed.y) : undefined,
},
},
},
}
}, [data, labelKey, gridColor, yAxes, tooltipLabel, tooltipTitle, xTickFormat, maxTicksLimit])
const chartType = series.some((s) => s.type === "bar") && series.some((s) => !s.type || s.type === "line") ? "bar" : (series[0]?.type ?? "line")
return (
<CanvasChart
type={chartType}
data={chartData}
options={chartOptions}
config={config}
loading={loading}
ariaLabel={ariaLabel}
className={`${aspect} w-full ${className || ""}`}
/>
)
}
npx shadcn@latest add https://mzizi.dev/api/v1/ui/time-series-chartFetch this component's metadata and source code from the registry API.
/api/v1/ui/time-series-chart