From 0ae97ae6273cc4776c50b81b31c4c82031234851 Mon Sep 17 00:00:00 2001 From: Ronan Berder Date: Fri, 19 Jun 2026 16:42:15 +0800 Subject: [PATCH] Expand chart examples and derived surfaces --- docs/src/components/chart.mdx | 38 +++-- docs/src/site/ChartExample.astro | 256 ++++++++++++++++++++++++++---- docs/src/site/ChartExamples.astro | 83 ++++++++++ src/js/chart.js | 121 +++++++++++--- 4 files changed, 429 insertions(+), 69 deletions(-) create mode 100644 docs/src/site/ChartExamples.astro diff --git a/docs/src/components/chart.mdx b/docs/src/components/chart.mdx index fce7371..7484fae 100644 --- a/docs/src/components/chart.mdx +++ b/docs/src/components/chart.mdx @@ -1,7 +1,12 @@ import ChartExample from "../site/ChartExample.astro" +import ChartExamples from "../site/ChartExamples.astro" # Chart + + Chart support is alpha. The JavaScript API, generated markup, and option names may change before the stable 1.0 release. + + Use Chart.js with Basecoat defaults for themed colors, external tooltips, and optional generated legends. @@ -9,7 +14,7 @@ Use Chart.js with Basecoat defaults for themed colors, external tooltips, and op Chart is an optional JavaScript helper. Load Chart.js first, then `basecoat.js`, then `chart.js`. ```html - + ``` @@ -45,22 +50,6 @@ Chart is an optional JavaScript helper. Load Chart.js first, then `basecoat.js`, Charts default to `aspect-video`. Use a height, `min-h-*`, or `aspect-*` utility on a parent element when you need a different measuring box. -## Card Composition - -Chart does not own titles, descriptions, or framing. Compose those with Card or your own markup. - -```html -
-
-

Bar Chart - Multiple

-

Showing total visitors for the last 6 months

-
-
- -
-
-``` - ## Legend Set `legend: true` to generate a Basecoat-styled legend after the canvas. @@ -90,7 +79,9 @@ window.basecoat.chart("#revenue-chart", { recurring: { label: "Recurring", color: "var(--chart-1)", + surface: "gradient", dataset: { + fill: true, tension: 0.35, }, }, @@ -155,6 +146,10 @@ window.basecoat.chart("#custom-chart", { `basecoat.chart()` returns the Chart.js instance for single targets and an array of instances for multi-target selectors. +## Examples + + + ## API | Option | Default | Description | @@ -168,3 +163,12 @@ window.basecoat.chart("#custom-chart", { | `options` | `{}` | Raw Chart.js options. | | `plugins` | `[]` | Raw Chart.js plugins. | | `chartData` | `undefined` | Complete Chart.js data object. Overrides the data mapper. | + +### Series options + +| Option | Description | +| --- | --- | +| `label` | Display label for legends and tooltips. | +| `color` | Base color for the series stroke, fill, legend marker, and tooltip marker. | +| `surface` | Derives a fill from `color`. Use `true` for a translucent fill, `"gradient"` for an upstream-style vertical fade, or `{ from, to }` for custom opacity stops. | +| `dataset` | Raw Chart.js dataset options. | diff --git a/docs/src/site/ChartExample.astro b/docs/src/site/ChartExample.astro index 29152ff..3629ffd 100644 --- a/docs/src/site/ChartExample.astro +++ b/docs/src/site/ChartExample.astro @@ -1,65 +1,255 @@ --- import { Preview } from "reallysimpledocs/components"; -const code = `
-
-

Bar Chart - Multiple

-

Showing total visitors for the last 6 months

-
-
- -
+const code = `
+

Area Chart - Interactive

+

Showing total visitors for the last 3 months

+
+ +
`; --- -
-
-

Bar Chart - Multiple

-

Showing total visitors for the last 6 months

-
-
- -
+
+
+

Area Chart - Interactive

+

Showing total visitors for the last 3 months

+
+
+ +
- + diff --git a/docs/src/site/ChartExamples.astro b/docs/src/site/ChartExamples.astro new file mode 100644 index 0000000..403430b --- /dev/null +++ b/docs/src/site/ChartExamples.astro @@ -0,0 +1,83 @@ +--- +import { Preview } from "reallysimpledocs/components"; + +const barCode = `
+ +
`; + +const lineCode = `
+ +
`; + +const stepCode = `
+ +
`; + +const stackedCode = `
+ +
`; + +const donutCode = `
+ +
`; + +const radarCode = `
+ +
`; +--- + +
+
+

Bar Chart - Multiple

+ +
+ +
+
+
+ +
+

Line Chart - Linear

+ +
+ +
+
+
+ +
+

Line Chart - Step

+ +
+ +
+
+
+ +
+

Bar Chart - Stacked

+ +
+ +
+
+
+ +
+

Pie Chart - Donut

+ +
+ +
+
+
+ +
+

Radar Chart

+ +
+ +
+
+
+
diff --git a/src/js/chart.js b/src/js/chart.js index aeda8d2..1893cfb 100644 --- a/src/js/chart.js +++ b/src/js/chart.js @@ -68,6 +68,35 @@ return resolved || color; }; + const colorWithAlpha = (color, alpha, element) => { + const canvas = document.createElement('canvas'); + canvas.width = 1; + canvas.height = 1; + const context = canvas.getContext('2d', { willReadFrequently: true }); + if (!context) return color; + + context.clearRect(0, 0, 1, 1); + context.fillStyle = '#000'; + context.fillStyle = resolveColor(color, element); + context.fillRect(0, 0, 1, 1); + + const [red, green, blue, sourceAlpha] = context.getImageData(0, 0, 1, 1).data; + return `rgba(${red}, ${green}, ${blue}, ${(sourceAlpha / 255) * alpha})`; + }; + + const surfaceColor = (color, element, { from = 0.4, to } = {}) => { + if (to === undefined) return colorWithAlpha(color, from, element); + + return ({ chart }) => { + if (!chart.chartArea) return colorWithAlpha(color, from, element); + + const gradient = chart.ctx.createLinearGradient(0, chart.chartArea.top, 0, chart.chartArea.bottom); + gradient.addColorStop(0, colorWithAlpha(color, from, element)); + gradient.addColorStop(1, colorWithAlpha(color, to, element)); + return gradient; + }; + }; + const resolveDatasetColors = (chartData, element) => { chartData.datasets?.forEach((dataset) => { dataset.borderColor = resolveColor(dataset.borderColor, element); @@ -78,10 +107,47 @@ return chartData; }; + const resolveScaleColors = (scales, element) => { + Object.values(scales || {}).forEach((scale) => { + ['border', 'grid', 'ticks', 'angleLines', 'pointLabels'].forEach((key) => { + if (scale[key]?.color !== undefined) { + scale[key].color = resolveColor(scale[key].color, element); + } + }); + }); + + return scales; + }; + const chartColor = (element, token) => resolveColor(`var(${token})`, element); const colorMix = (color, opacity) => `color-mix(in oklab, ${color} ${opacity * 100}%, transparent)`; + const readCssPixels = (element, property, fallback = 0) => { + const value = getComputedStyle(element).getPropertyValue(property).trim(); + if (!value) return fallback; + + const number = Number.parseFloat(value); + return Number.isFinite(number) ? number : fallback; + }; + + const applyDatasetDefaults = (chartData, type, { barRadius, element }) => { + chartData.datasets?.forEach((dataset) => { + if (type === 'bar' || dataset.type === 'bar') { + if (dataset.borderRadius === undefined) dataset.borderRadius = barRadius; + } + + if (dataset._basecoatSurface) { + dataset.backgroundColor = surfaceColor(dataset.borderColor || dataset.backgroundColor, element, dataset._basecoatSurface); + delete dataset._basecoatSurface; + } + }); + + return chartData; + }; + + const usesCartesianScales = (type) => !['pie', 'doughnut', 'polarArea', 'radar'].includes(type); + const ensureContainer = (canvas) => { if (canvas.parentElement?.classList.contains('chart')) { canvas.parentElement.dataset.basecoatChartContainer = 'true'; @@ -128,6 +194,13 @@ labels, datasets: entries.map(([key, item], index) => { const color = item.color || colorForIndex(index); + const dataset = item.dataset || {}; + const surface = item.surface === true + ? { from: 0.4 } + : item.surface === 'gradient' + ? { from: 0.8, to: 0.1 } + : item.surface; + return { label: item.label || key, data: rows.map((row) => valueFor(row, key)), @@ -137,7 +210,10 @@ type: item.type, yAxisID: item.axis, hidden: item.hidden, - ...(item.dataset || {}), + ...dataset, + ...(surface && item.backgroundColor === undefined && dataset.backgroundColor === undefined + ? { _basecoatSurface: surface } + : {}), }; }), }; @@ -276,23 +352,9 @@ const borderColor = chartColor(canvas, '--border'); const gridColor = resolveColor(colorMix('var(--border)', 0.5), canvas); const mutedColor = chartColor(canvas, '--muted-foreground'); - - canvas.classList.remove('chart'); - canvas._basecoatChartContainer = container; - canvas.dataset.chartLegend = String(legend); - - const chart = new Chart(canvas, { - type, - data: resolveDatasetColors(chartData, canvas), - options: { - responsive: true, - maintainAspectRatio: false, - interaction: { - mode: 'index', - intersect: false, - }, - ...userOptions, - scales: { + const barRadius = readCssPixels(canvas, '--chart-bar-radius'); + const defaultScales = usesCartesianScales(type) + ? { x: { border: { display: false, color: borderColor }, grid: { display: false, color: gridColor }, @@ -305,8 +367,29 @@ ticks: { display: false, color: mutedColor }, ...(userOptions.scales?.y || {}), }, - ...(Object.fromEntries(Object.entries(userOptions.scales || {}).filter(([key]) => key !== 'x' && key !== 'y'))), + } + : {}; + const scales = { + ...defaultScales, + ...(Object.fromEntries(Object.entries(userOptions.scales || {}).filter(([key]) => key !== 'x' && key !== 'y'))), + }; + + canvas.classList.remove('chart'); + canvas._basecoatChartContainer = container; + canvas.dataset.chartLegend = String(legend); + + const chart = new Chart(canvas, { + type, + data: resolveDatasetColors(applyDatasetDefaults(chartData, type, { barRadius, element: canvas }), canvas), + options: { + responsive: true, + maintainAspectRatio: false, + interaction: { + mode: 'index', + intersect: false, }, + ...userOptions, + scales: resolveScaleColors(scales, canvas), plugins: { ...userOptions.plugins, legend: {