Chart.js advanced patterns unlock custom plugins and mixed chart types — Chart.register(CategoryScale, LinearScale, BarElement, LineElement, PointElement, Tooltip, Legend) tree-shakes bundle. type: "bar" with a dataset of type: "line" creates mixed charts. Custom plugin: { id: "myPlugin", beforeDraw(chart, args, options) { const ctx = chart.ctx; ctx.save(); ... ctx.restore() } }. ScriptableContext: backgroundColor: (ctx) => ctx.raw > 0 ? "green" : "red" computes colors per-point. ChartDataLabels plugin adds value labels with datalabels.anchor/align/formatter. chartjs-plugin-zoom with zoom.wheel.enabled and pan.enabled adds pan/zoom. chartjs-plugin-streaming enables real-time live data via streaming.onRefresh. Annotations: chartjs-plugin-annotation draws threshold lines and boxes. ctx.createLinearGradient(0, 0, 0, height) creates gradient fills. chartRef.current.getDatasetAtEvent(e) gets clicked datasets. chart.update("none") updates without animation. Claude Code generates Chart.js dashboards, mixed charts, plugins, and real-time feeds.
CLAUDE.md for Chart.js Advanced
## Chart.js Advanced Stack
- Version: chart.js >= 4.4, react-chartjs-2 >= 5.3
- Register: import { Chart, CategoryScale, LinearScale, BarElement, LineElement, PointElement, ArcElement, Title, Tooltip, Legend } from "chart.js"; Chart.register(...)
- Ref: const chartRef = useRef<ChartJS<"bar">>(null); <Bar ref={chartRef} ...>
- Mixed: datasets: [{ type: "bar", data: [] }, { type: "line", data: [] }]
- Gradient: (ctx) => { const grad = ctx.chart.ctx.createLinearGradient(0,0,0,300); grad.addColorStop(0,"#6366f1"); grad.addColorStop(1,"rgba(99,102,241,0)"); return grad }
- Plugin: Chart.register({ id: "myPlugin", beforeDraw(chart) { ... } })
- Zoom: import zoomPlugin from "chartjs-plugin-zoom"; Chart.register(zoomPlugin)
Mixed Chart Component
// components/charts/MixedChart.tsx — bar + line combined with gradient
"use client"
import { useRef, useEffect } from "react"
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
BarElement,
LineElement,
PointElement,
Title,
Tooltip,
Legend,
Filler,
type ChartData,
type ChartOptions,
type ScriptableContext,
} from "chart.js"
import { Chart } from "react-chartjs-2"
ChartJS.register(
CategoryScale, LinearScale, BarElement, LineElement,
PointElement, Title, Tooltip, Legend, Filler,
)
type MixedDataPoint = { month: string; revenue: number; units: number; target: number }
interface MixedChartProps {
data: MixedDataPoint[]
title?: string
}
export function MixedChart({ data, title }: MixedChartProps) {
const chartRef = useRef<ChartJS<"bar">>(null)
const labels = data.map((d) => d.month)
function buildGradient(ctx: CanvasRenderingContext2D, color: string, alpha = 0.3): CanvasGradient {
const gradient = ctx.createLinearGradient(0, 0, 0, 300)
gradient.addColorStop(0, color.replace(")", `, ${alpha})`).replace("rgb(", "rgba("))
gradient.addColorStop(1, color.replace(")", ", 0)").replace("rgb(", "rgba("))
return gradient
}
const chartData: ChartData<"bar"> = {
labels,
datasets: [
{
type: "bar",
label: "Revenue",
data: data.map((d) => d.revenue),
backgroundColor: (ctx: ScriptableContext<"bar">) => {
const chart = ctx.chart
const { canvas } = chart
const context = canvas.getContext("2d")
if (!context) return "#6366f1"
return buildGradient(context, "rgb(99, 102, 241)")
},
borderColor: "#6366f1",
borderWidth: 0,
borderRadius: 4,
borderSkipped: false,
yAxisID: "y",
order: 2,
},
{
type: "line",
label: "Units Sold",
data: data.map((d) => d.units),
borderColor: "#22c55e",
pointBackgroundColor: "#22c55e",
pointBorderColor: "#fff",
pointBorderWidth: 2,
pointRadius: 5,
borderWidth: 2,
tension: 0.4,
yAxisID: "y1",
order: 1,
},
{
type: "line",
label: "Target",
data: data.map((d) => d.target),
borderColor: "#f59e0b",
borderDash: [8, 4],
borderWidth: 1.5,
pointRadius: 0,
tension: 0,
yAxisID: "y",
order: 0,
},
],
}
const options: ChartOptions<"bar"> = {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: "index", intersect: false },
plugins: {
legend: {
position: "top" as const,
labels: { boxWidth: 12, padding: 16, font: { size: 11 }, color: "#6b7280" },
},
title: title ? { display: true, text: title, font: { size: 13, weight: "bold" }, color: "#111" } : { display: false },
tooltip: {
backgroundColor: "#fff",
titleColor: "#111",
bodyColor: "#6b7280",
borderColor: "#e5e7eb",
borderWidth: 1,
padding: 10,
boxPadding: 4,
callbacks: {
label: (ctx) => ` ${ctx.dataset.label}: ${Number(ctx.raw).toLocaleString()}`,
},
},
},
scales: {
x: {
grid: { display: false },
ticks: { color: "#9ca3af", font: { size: 11 } },
},
y: {
position: "left" as const,
grid: { color: "#f3f4f6" },
ticks: { color: "#9ca3af", font: { size: 11 }, callback: (v) => `$${Number(v) / 1000}k` },
},
y1: {
position: "right" as const,
grid: { display: false },
ticks: { color: "#9ca3af", font: { size: 11 } },
},
},
}
return (
<div className="rounded-2xl border bg-card p-5">
<div style={{ height: 320 }}>
<Chart ref={chartRef} type="bar" data={chartData} options={options} />
</div>
</div>
)
}
Custom Plugin: Watermark + Threshold Line
// lib/charts/plugins.ts — custom Chart.js plugins
import { type Plugin } from "chart.js"
/** Draws a horizontal threshold line with label */
export function thresholdPlugin(threshold: number, label: string, color = "#ef4444"): Plugin {
return {
id: `threshold-${threshold}`,
afterDraw(chart) {
const { ctx, scales } = chart
const yAxis = scales["y"]
if (!yAxis) return
const y = yAxis.getPixelForValue(threshold)
const { left, right } = chart.chartArea
ctx.save()
ctx.beginPath()
ctx.setLineDash([6, 4])
ctx.moveTo(left, y)
ctx.lineTo(right, y)
ctx.strokeStyle = color
ctx.lineWidth = 1.5
ctx.stroke()
ctx.setLineDash([])
ctx.fillStyle = color
ctx.font = "11px Inter, system-ui, sans-serif"
ctx.textAlign = "right"
ctx.fillText(label, right - 4, y - 5)
ctx.restore()
},
}
}
/** Watermark text in the center of the chart */
export const watermarkPlugin: Plugin = {
id: "watermark",
beforeDraw(chart) {
const { ctx, chartArea } = chart
const { left, right, top, bottom } = chartArea
const cx = (left + right) / 2
const cy = (top + bottom) / 2
ctx.save()
ctx.globalAlpha = 0.04
ctx.font = "bold 28px Inter, sans-serif"
ctx.fillStyle = "#000"
ctx.textAlign = "center"
ctx.textBaseline = "middle"
ctx.translate(cx, cy)
ctx.rotate(-Math.PI / 8)
ctx.fillText("INTERNAL USE ONLY", 0, 0)
ctx.restore()
},
}
Real-Time Streaming Chart
// components/charts/StreamingChart.tsx — live data with ring buffer
"use client"
import { useRef, useEffect, useState, useCallback } from "react"
import { Chart as ChartJS, CategoryScale, LinearScale, LineElement, PointElement, Tooltip, Filler } from "chart.js"
import { Line } from "react-chartjs-2"
import type { ChartData, ChartOptions } from "chart.js"
ChartJS.register(CategoryScale, LinearScale, LineElement, PointElement, Tooltip, Filler)
const BUFFER = 60
interface StreamingChartProps {
source: () => number
label?: string
color?: string
warningThreshold?: number
}
export function StreamingChart({ source, label = "Metric", color = "#6366f1", warningThreshold }: StreamingChartProps) {
const [points, setPoints] = useState<{ t: string; v: number }[]>(() =>
Array.from({ length: 20 }, () => ({ t: new Date().toLocaleTimeString(), v: source() })),
)
useEffect(() => {
const id = setInterval(() => {
setPoints((prev) => [
...prev.slice(-(BUFFER - 1)),
{ t: new Date().toLocaleTimeString("en", { hour12: false }), v: source() },
])
}, 1000)
return () => clearInterval(id)
}, [source])
const isWarning = warningThreshold !== undefined && (points[points.length - 1]?.v ?? 0) > warningThreshold
const chartData: ChartData<"line"> = {
labels: points.map((p) => p.t),
datasets: [{
label,
data: points.map((p) => p.v),
borderColor: isWarning ? "#f59e0b" : color,
backgroundColor: (ctx) => {
const c = ctx.chart.ctx
const g = c.createLinearGradient(0, 0, 0, 120)
g.addColorStop(0, isWarning ? "rgba(245,158,11,0.3)" : `${color}33`)
g.addColorStop(1, "rgba(255,255,255,0)")
return g
},
fill: true,
tension: 0.4,
pointRadius: 0,
borderWidth: 2,
}],
}
const options: ChartOptions<"line"> = {
responsive: true,
maintainAspectRatio: false,
animation: false,
plugins: { legend: { display: false }, tooltip: { mode: "index", intersect: false } },
scales: {
x: { display: false },
y: {
grid: { color: "#f3f4f6" },
ticks: { color: "#9ca3af", font: { size: 10 } },
...(warningThreshold ? { suggestedMax: warningThreshold * 1.3 } : {}),
},
},
}
return (
<div className={`rounded-2xl border p-4 ${isWarning ? "border-amber-400/50" : "bg-card"}`}>
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium">{label}</span>
<span className={`text-lg font-bold tabular-nums ${isWarning ? "text-amber-600" : ""}`}>
{points[points.length - 1]?.v.toFixed(1)}
</span>
</div>
<div style={{ height: 100 }}>
<Line data={chartData} options={options} />
</div>
</div>
)
}
For the Recharts alternative when a more React-idiomatic component API (each chart element as JSX children rather than config objects), TypeScript-first design, and a larger React ecosystem of tutorials is preferred — Recharts is built natively for React while Chart.js originated as a vanilla JavaScript library with react-chartjs-2 as a wrapper, making Chart.js stronger for canvas-based custom plugins and real-time streaming, see the Recharts guide. For the ApexCharts alternative when built-in pan/zoom, interactive drill-down, brushing, and extensive chart type variety (candlestick, range bar, polar area, funnel) are needed without third-party plugins — ApexCharts has more built-in features while Chart.js has a more minimal, plugin-extensible core, see the ApexCharts guide. The Claude Skills 360 bundle includes Chart.js skill sets covering mixed charts, custom plugins, and real-time streaming. Start with the free tier to try advanced charting generation.