import type { Component } from "solid-js"; import { createEffect, createSignal, mergeProps, on, onCleanup, onMount } from "solid-js"; import { unwrap } from "solid-js/store"; import type { Ref } from "@solid-primitives/refs"; import { mergeRefs } from "@solid-primitives/refs"; import type { ChartComponent, ChartData, ChartItem, ChartOptions, ChartType, ChartTypeRegistry, Plugin as ChartPlugin, TooltipModel, } from "chart.js"; import { ArcElement, BarController, BarElement, BubbleController, CategoryScale, Chart, Colors, DoughnutController, Filler, Legend, LinearScale, LineController, LineElement, PieController, PointElement, PolarAreaController, RadarController, RadialLinearScale, ScatterController, Tooltip, } from "chart.js"; import { WordCloudController, WordElement } from "chartjs-chart-wordcloud"; import ChartDeferred from "chartjs-plugin-deferred"; import ChartZoom from "chartjs-plugin-zoom"; Chart.register(ChartDeferred, ChartZoom); interface TypedChartProps { data: ChartData; options?: ChartOptions; plugins?: ChartPlugin[]; ref?: Ref; width?: number | undefined; height?: number | undefined; } type ChartProps = TypedChartProps & { type: ChartType; }; interface ChartContext { chart: Chart; tooltip: TooltipModel; } const BaseChart: Component = (rawProps) => { const [canvasRef, setCanvasRef] = createSignal(); const [chart, setChart] = createSignal(); const props = mergeProps( { width: 512, height: 512, options: { responsive: true } as ChartOptions, plugins: [] as ChartPlugin[], }, rawProps, ); const init = () => { const ctx = canvasRef()?.getContext("2d") as ChartItem; const config = unwrap(props); const chart = new Chart(ctx, { type: config.type, data: config.data, options: config.options, plugins: config.plugins, }); setChart(chart); }; onMount(() => { init(); }); createEffect( on( () => props.data, () => { chart()!.data = props.data; chart()!.update(); }, { defer: true }, ), ); createEffect( on( () => props.options, () => { chart()!.options = props.options; chart()!.update(); }, { defer: true }, ), ); createEffect( on( [() => props.width, () => props.height], () => { chart()!.resize(props.width, props.height); }, { defer: true }, ), ); createEffect( on( () => props.type, () => { const dimensions = [chart()!.width, chart()!.height]; chart()!.destroy(); init(); chart()!.resize(...dimensions); }, { defer: true }, ), ); onCleanup(() => { chart()?.destroy(); mergeRefs(props.ref, null); }); Chart.register(Colors, Filler, Legend, Tooltip); return ( setCanvasRef(el))} height={props.height} width={props.width} /> ); }; function showTooltip(context: ChartContext) { let el = document.getElementById("chartjs-tooltip"); if (!el) { el = document.createElement("div"); el.id = "chartjs-tooltip"; document.body.appendChild(el); } const model = context.tooltip; if (model.opacity === 0 || !model.body) { el.style.opacity = "0"; return; } el.className = `p-2 bg-card text-card-foreground rounded-lg border shadow-sm text-sm ${ model.yAlign ?? `no-transform` }`; let content = ""; model.title.forEach((title) => { content += `

${title}

`; }); content += `
`; const body = model.body.flatMap((body) => body.lines); body.forEach((line, i) => { const colors = model.labelColors[i]; content += `
${line}
`; }); content += `
`; el.innerHTML = content; const pos = context.chart.canvas.getBoundingClientRect(); el.style.opacity = "1"; el.style.position = "absolute"; el.style.left = `${(pos.left + window.scrollX + model.caretX).toString()}px`; el.style.top = `${(pos.top + window.scrollY + model.caretY).toString()}px`; el.style.pointerEvents = "none"; } function createTypedChart(type: ChartType, components: ChartComponent[]): Component { const chartsWithScales: ChartType[] = ["bar", "line", "scatter"]; const chartsWithLegends: ChartType[] = ["bar", "line"]; const options: ChartOptions = { responsive: true, maintainAspectRatio: false, scales: chartsWithScales.includes(type) ? { x: { border: { display: false }, grid: { display: false }, }, y: { border: { dash: [3], dashOffset: 3, display: false, }, grid: { color: "hsla(240, 3.8%, 46.1%, 0.4)", }, }, } : {}, plugins: { legend: chartsWithLegends.includes(type) ? { display: true, align: "end", labels: { usePointStyle: true, boxWidth: 6, boxHeight: 6, color: "hsl(240, 3.8%, 46.1%)", font: { size: 14 }, }, } : { display: false }, tooltip: { enabled: false, external: (context) => { showTooltip(context); }, }, }, }; Chart.register(...components); return (props) => ( ); } const BarChart = /* #__PURE__ */ createTypedChart("bar", [BarController, BarElement, CategoryScale, LinearScale]); const BubbleChart = /* #__PURE__ */ createTypedChart("bubble", [BubbleController, PointElement, LinearScale]); const DonutChart = /* #__PURE__ */ createTypedChart("doughnut", [DoughnutController, ArcElement]); const LineChart = /* #__PURE__ */ createTypedChart("line", [ LineController, LineElement, PointElement, CategoryScale, LinearScale, ]); const PieChart = /* #__PURE__ */ createTypedChart("pie", [PieController, ArcElement]); const PolarAreaChart = /* #__PURE__ */ createTypedChart("polarArea", [ PolarAreaController, ArcElement, RadialLinearScale, ]); const RadarChart = /* #__PURE__ */ createTypedChart("radar", [ RadarController, LineElement, PointElement, RadialLinearScale, ]); const ScatterChart = /* #__PURE__ */ createTypedChart("scatter", [ScatterController, PointElement, LinearScale]); const WordCloudChart = /* #__PURE__ */ createTypedChart("wordCloud", [WordCloudController, WordElement]); export { BarChart, BubbleChart, BaseChart as Chart, DonutChart, LineChart, PieChart, PolarAreaChart, RadarChart, ScatterChart, WordCloudChart, };