begin new custom chart implementation for js ui

This commit is contained in:
2026-07-16 11:45:17 -04:00
parent 13a60a90a1
commit 550e97aa9b
21 changed files with 1001 additions and 179 deletions

View File

@@ -9,13 +9,54 @@
What is left is only what is genuinely this app's: the brand.
The values below match the Go/WASM section's css/app.css on purpose — same
Lora, same sky accent — so that crossing between /wasm and /js reads as two
parts of ONE site rather than two demos that happen to share a domain. The
Open Sans, same navy accent — so that crossing between /wasm and /js reads as
two parts of ONE site rather than two demos that happen to share a domain. The
two sections are built by completely different pipelines; they should not look
like it.
--------------------------------------------------------------------------- */
/* Open Sans — self-hosted (files in wwwroot/fonts). One variable file per subset
carries weights 400–700 upright and italic. unicode-range keeps the browser to the
one subset a glyph needs; font-display: swap paints the fallback first. The same four
blocks live in the /wasm side's css/app.css so both front-ends share one typeface. */
@font-face {
font-family: "Open Sans";
font-style: normal;
font-weight: 400 700;
font-display: swap;
src: url("/fonts/opensans-latin-normal.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: "Open Sans";
font-style: normal;
font-weight: 400 700;
font-display: swap;
src: url("/fonts/opensans-latin-ext-normal.woff2") format("woff2");
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: "Open Sans";
font-style: italic;
font-weight: 400 700;
font-display: swap;
src: url("/fonts/opensans-latin-italic.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: "Open Sans";
font-style: italic;
font-weight: 400 700;
font-display: swap;
src: url("/fonts/opensans-latin-ext-italic.woff2") format("woff2");
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@theme {
/* The UI typeface — see the @font-face blocks above. This one line points the kit's
font-sans utilities and the html rule below onto Open Sans. */
--font-sans: "Open Sans", ui-sans-serif, system-ui, sans-serif;
/* The brand: navy throughout, a muted flag-navy. These are the SAME values the Go/WASM
section's css/app.css sets, and under the same names — so the Layers menu, the
sidebar highlight and the callouts are the same navy on both sides of the site rather

View File

@@ -83,7 +83,7 @@ import { CrmTabGroup, CrmSubTabGroup } from "@ui/CrmTabs";
import { Accordion, SingleAccordion } from "@ui/Accordion";
import { SidebarNav } from "@ui/Sidebar";
import { FuzzyMatch } from "@ui/FuzzyMatch";
import ReactiveChart from "@ui/Chart";
import { Chart, ChartSeries } from "@ui/Chart";
import { ThemeToggle, useTheme } from "@ui/Theme";
import { Demo } from "../layout/Demo.tsx";
@@ -1328,7 +1328,7 @@ const KIT_NAMES = [
"AutoTable", "Accordion", "Alert", "Badge", "ButtonUI", "Calendar", "CellGrid", "FormCombobox",
"DatePicker", "FormInput", "Menu", "Modal", "FormMultiSelect", "Popover", "PrettyTable",
"SegmentedButtons", "FormSignaturePad", "TabGroup", "ThemeToggle", "ToastProvider", "ToggleSwitch",
"Tooltip", "TutorialProvider", "SidebarNav", "ReactiveChart", "FuzzyMatch",
"Tooltip", "TutorialProvider", "SidebarNav", "Chart", "FuzzyMatch",
];
function Search() {
@@ -1364,59 +1364,48 @@ function Search() {
// ---- charts --------------------------------------------------------------
const CHART_DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
function Charts() {
const [seed, setSeed] = createSignal(0);
const data = () => {
const base = [42, 17, 63, 28, 55, 9, 71];
const values = base.map((v) => (seed() === 0 ? v : Math.max(4, (v * 7 + seed() * 13) % 80)));
return {
labels: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
datasets: [
{
label: "Requests",
data: values,
// A literal, not var(--color-primary): chart.js paints a canvas, and a
// canvas cannot read a CSS variable. So this is the one place in the
// site that names the navy instead of asking for it — and it names the
// DARK theme's lighter navy, which is the only one of the two that
// reads on both a white page and a near-black one.
backgroundColor: "#2b4f80",
borderColor: "#2b4f80",
},
],
};
};
// Two series over the same week. seed() reshuffles them so you can watch the SVG
// move — no teardown, no new instance, just the marks that changed.
const shuffle = (base: number[]) =>
base.map((v) => (seed() === 0 ? v : Math.max(4, (v * 7 + seed() * 13) % 80)));
const requests = (): ChartSeries => ({ name: "Requests", data: shuffle([42, 17, 63, 28, 55, 9, 71]) });
const errors = (): ChartSeries => ({ name: "Errors", data: shuffle([8, 3, 12, 6, 9, 2, 14]) });
const pieData = (): ChartSeries => ({ name: "Traffic", data: shuffle([40, 25, 20, 15, 8]) });
const pieLabels = ["Direct", "Search", "Social", "Email", "Referral"];
return (
<Section id="charts" title="Charts">
<Prose>
<code class="font-mono">@ui/Chart</code> wraps chart.js: it creates a chart against a canvas
on mount and pushes new data through an effect, so changing the data updates the chart rather
than rebuilding the page.
<code class="font-mono">@ui/Chart</code> draws SVG — no charting library, nothing vendored, no{" "}
<code class="font-mono">&lt;canvas&gt;</code>. Because the picture is JSX it is already reactive:
change <code class="font-mono">series</code> and Solid re-renders the marks that moved. There is
no <code class="font-mono">update()</code> to call.
</Prose>
<Prose>
This is the one component in the kit that is genuinely different on the other side. The Go
port keeps the props shape for parity and renders an empty canvas — canvas drawing, a
JavaScript charting library and mount lifecycles do not exist in the neutral Go runtime. Kjol
Wasm Web draws SVG with go-chart instead, and draws it in the browser, in WebAssembly, exactly
as this one draws to a canvas here. Same place, different library. Neither side pretends
otherwise.
Being SVG buys two things a canvas cannot. The marks name the theme tokens
(<code class="font-mono">var(--color-chart-1)</code> …), so the palette inverts for dark mode with
the rest of the site — a canvas paints pixels and cannot read a CSS variable. And every mark is a
real element, so hovering shows a crosshair and one tooltip listing every series at that point.
</Prose>
<Panel title="Bar and doughnut, from the same data">
{/* The wrapper is h-full with maintainAspectRatio:false, so the PARENT must
have an explicit height or the canvas collapses to nothing at all. */}
<div class="grid gap-4 lg:grid-cols-12">
<div class="h-72 lg:col-span-7">
<ReactiveChart
type="bar"
data={data()}
options={{ plugins: { legend: { display: false } } }}
/>
<Panel title="Grouped bars, a smooth area, a two-line series, and a donut — from one data set">
<div class="grid gap-6 lg:grid-cols-12">
<div class="lg:col-span-7">
<Chart kind="bar" labels={CHART_DAYS} series={[requests(), errors()]} height={260} />
</div>
<div class="h-72 lg:col-span-5">
<ReactiveChart type="doughnut" data={data()} />
<div class="lg:col-span-5">
<Chart kind="donut" labels={pieLabels} series={[pieData()]} height={260} />
</div>
<div class="lg:col-span-7">
<Chart kind="area" curve="smooth" labels={CHART_DAYS} series={[requests()]} height={220} />
</div>
<div class="lg:col-span-5">
<Chart kind="line" labels={CHART_DAYS} series={[requests(), errors()]} height={220} />
</div>
</div>
<div class="mt-4 flex items-center gap-3">
@@ -1424,17 +1413,18 @@ function Charts() {
New data
</ButtonUI>
<span class="text-xs text-ink-muted">
The chart updates in place — it is not torn down and rebuilt.
Every chart updates in place — hover any of them for the tooltip.
</span>
</div>
</Panel>
<AlertYellow header="It has to be vendored" class="mt-6">
<code class="font-mono">Chart.tsx</code> imports chart.js at the top level and registers
every controller on module load. An app that uses it must vendor chart.js and{" "}
<code class="font-mono">@kurkle/color</code>, or the bundle does not degrade — it fails to
evaluate, and you get an empty page and one line in the console.
</AlertYellow>
<Prose>
One <code class="font-mono">kind</code> prop picks the form —{" "}
<code class="font-mono">"line" | "area" | "bar" | "pie" | "donut"</code> — and{" "}
<code class="font-mono">stacked</code>, <code class="font-mono">curve</code>,{" "}
<code class="font-mono">palette</code> and <code class="font-mono">valueFormat</code> refine it.
The width is measured from the container, so a chart fills whatever column you give it.
</Prose>
</Section>
);
}

View File

@@ -5,12 +5,8 @@
"//3": "Only the files the bundler actually pins are vendored, not the whole npm packages — 3.5 MB rather than 62 MB of type definitions, CJS builds and documentation. If a subpath import is ever added that reaches outside dist/ or build/, this is the first place it will fail.",
"//4": "chart.js (and its @kurkle/color dependency) are here for the same reason: @ui/Chart imports chart.js at the top level and registers every controller on module load. Its wrapper is h-full with maintainAspectRatio:false, so the PARENT must have an explicit height or the canvas collapses to nothing.",
"entrypoints": {
"pdf-lib": "pdf-lib/dist/pdf-lib.esm.js",
"pdfjs-dist": "pdfjs-dist/build/pdf.mjs",
"chart.js": "chart.js/dist/chart.js",
"@kurkle/color": "@kurkle/color/dist/color.esm.js"
"pdfjs-dist": "pdfjs-dist/build/pdf.mjs"
}
}