## Demo: charts ### Charts.svelte ```svelte

Minimal setup: install it, import from layerchart, and Mochi bundles it into whichever island imports it. Full component reference at layerchart.com.

Pure SSR — SVG

LayerChart can render to pure SVG or HTML by omitting Mochi's hydrate directives. That needs two opt-ins: ssr (LayerChart skips server rendering otherwise) and explicit width/height to set the dimensions, since nothing resizes the container client side.

SVG is recommended for pure SSR graphs: the <Svg> gets a matching viewBox, so the fixed 600×220 geometry rescales to any column width with CSS alone.

It's composed from <Chart> primitives rather than the <BarChart> shortcut: that shortcut's marks snippet shadows its own marks prop, which the Svelte server compiler turns into unbounded recursion once ssr forces a server render.

LayerChart's SSR pitfall. LayerChart doesn't officially support SSR for its SVG and HTML renderers, and its all-in-one chart components (<BarChart>, <AreaChart>, <LineChart>, <PieChart>, <ScatterChart>, <ArcChart>) can't be server-rendered — turning on ssr crashes the whole page. For a no-JavaScript chart, build it from the smaller pieces like this one does.

Theming — light/dark

LayerChart ships its tooltip, axis, and other default styles pre-coloured with CSS light-dark() so they read correctly in both modes out of the box — you lean on that any time you use a LayerChart component without hand-styling its colours, the tooltip most of all. Bun downlevels light-dark() to a --buncss-light/--buncss-dark toggle but only defines the pair beside a color-scheme rule in the same stylesheet, which LayerChart doesn't ship — so define it once on :root in your shell:

Interactive — SVG

The same library imported from layerchart/svg, hydrated via mochi:hydrate — hover the plot for the crosshair and tooltip. Leaving ssr off ships an empty frame; omitting width (which would override the measured container) is what makes it responsive. The tooltip sets portal: false because LayerChart otherwise portals it to <body>, escaping the themed frame and rendering unstyled.

Helpers are fine when hydrating. With ssr off, the marks never render on the server, so the all-in-one components like <AreaChart> work here — no need to compose from primitives the way the pure-SSR chart does.

Interactive — HTML

Imported from layerchart/html, so the bars are real DOM boxes instead of SVG. This one uses the <BarChart> shortcut — fine here because ssr is off, so the marks never render on the server. Hydration measures the container, so it reflows fluidly with no viewBox trick needed.

Donut — SVG

A PieChart with a negative innerRadius and cRange colours; click a legend swatch to toggle a slice. Client-only is usually the better fit for a chart: since LayerChart draws nothing server-side anyway, hydrating would server-render an empty container and reconcile it, whereas a client-only island skips the server pass entirely and lets you ship real fallback markup — the skeleton below, wiped the moment the component mounts.

Loading chart…

Server-rendered image chart

You can server-render charts as an image (JPEG/PNG) using layerchart/server. This is possible today in Mochi but requires a bit of glue code code, see serverChart.ts in the tab view at the bottom of the demo for an example. The general recommendation if you want to go SSR-only is to use the SVG approach in the first example.

Server-rendered traffic chart, January through December
``` ### ChartFrame.svelte ```svelte
{@render children()}
``` ### StaticTrafficChart.svelte ```svelte
`${v / 1000}k`} />
``` ### TrafficChart.svelte ```svelte `${v / 1000}k` }, // LayerChart portals the tooltip to by default, which would move it out of the // frame that maps its theme custom properties and leave it unstyled. tooltip: { root: { portal: false } }, }} /> ``` ### HtmlBars.svelte ```svelte `${v / 1000}k` }, tooltip: { root: { portal: false } }, }} /> ``` ### RuntimeDonut.svelte ```svelte ``` ### ServerTrafficChart.svelte ```svelte `${v / 1000}k`} stroke="rgba(100,116,139,0.4)" fill="rgba(71,85,105,0.9)" /> ``` ### serverChart.ts ```ts import path from 'node:path'; import { compile, compileModule } from 'svelte/compiler'; import { createCanvas, Path2D } from '@napi-rs/canvas'; import { traffic } from './data.ts'; // renderChart draws the marks with Path2D and expects it on globalThis. if (typeof globalThis.Path2D === 'undefined') { (globalThis as { Path2D?: unknown }).Path2D = Path2D; } export type ChartFormat = 'png' | 'jpeg'; type ChartBundle = { renderChart: ( component: unknown, options: { width: number; height: number; format: ChartFormat; background: string; props: object; createCanvas: (w: number, h: number) => unknown }, ) => Uint8Array; ServerTrafficChart: unknown; }; // Mochi can't import `.svelte` in server code, so compile the ServerChart tree to a JS bundle here. // `.svelte.js` rune modules (layerchart's chart state) need compileModule, not compile. const svelteServerPlugin: import('bun').BunPlugin = { name: 'svelte-server', setup(build) { build.onLoad({ filter: /\.svelte$/ }, async ({ path: f }) => ({ contents: compile(await Bun.file(f).text(), { generate: 'server', filename: f }).js.code, loader: 'js', })); build.onLoad({ filter: /\.svelte\.[jt]s$/ }, async ({ path: f }) => ({ contents: compileModule(await Bun.file(f).text(), { generate: 'server', filename: f }).js.code, loader: 'js', })); }, }; let bundle: Promise | undefined; async function loadBundle(): Promise { const result = await Bun.build({ entrypoints: [path.join(import.meta.dir, 'serverChartBundle.ts')], plugins: [svelteServerPlugin], target: 'bun', conditions: ['svelte'], external: ['svelte', 'svelte/*'], outdir: path.join(import.meta.dir, '..', '..', '..', '.mochi', 'serverchart'), throw: true, }); return import(result.outputs[0]!.path) as Promise; } export async function renderTrafficChart(opts: { width: number; height: number; format: ChartFormat }): Promise> { const { renderChart, ServerTrafficChart } = await (bundle ??= loadBundle()); const bytes = renderChart(ServerTrafficChart, { width: opts.width, height: opts.height, format: opts.format, background: 'white', props: { data: traffic }, createCanvas, }); // Copy into an ArrayBuffer-backed view so it satisfies Response's BodyInit. return new Uint8Array(bytes); } ``` ### serverChartBundle.ts ```ts // Bun.build entrypoint only — never imported at runtime. Bundling renderChart together with the // chart means the compiled output carries no `.svelte` import, which Mochi's server runtime can't // load. serverChart.ts compiles this with a Svelte plugin and imports the pure-JS result. export { renderChart } from 'layerchart/server'; export { default as ServerTrafficChart } from './ServerTrafficChart.svelte'; ``` ### data.ts ```ts export type TrafficPoint = { month: string; requests: number; cached: number }; export const traffic: TrafficPoint[] = [ { month: 'Jan', requests: 1240, cached: 820 }, { month: 'Feb', requests: 1380, cached: 940 }, { month: 'Mar', requests: 1610, cached: 1170 }, { month: 'Apr', requests: 1520, cached: 1090 }, { month: 'May', requests: 1840, cached: 1390 }, { month: 'Jun', requests: 2110, cached: 1660 }, { month: 'Jul', requests: 2340, cached: 1880 }, { month: 'Aug', requests: 2280, cached: 1810 }, { month: 'Sep', requests: 2560, cached: 2040 }, { month: 'Oct', requests: 2890, cached: 2350 }, { month: 'Nov', requests: 3120, cached: 2580 }, { month: 'Dec', requests: 3410, cached: 2870 }, ]; export type RuntimeSlice = { stage: string; ms: number }; export const runtimes: RuntimeSlice[] = [ { stage: 'Server render', ms: 42 }, { stage: 'Island hydration', ms: 27 }, { stage: 'Asset transfer', ms: 19 }, { stage: 'Idle', ms: 12 }, ]; // Passed straight into LayerChart's `color` / `cRange` props, which write them into `fill` // and `stroke` attributes. Custom properties inherit into SVG, so they resolve against // `.chart-frame` and follow the site's theme toggle without any JavaScript. export const seriesColors = ['var(--chart-1)', 'var(--chart-2)', 'var(--chart-3)', 'var(--chart-4)']; ``` ### routes.ts ```ts import { Mochi } from 'mochi-framework'; import type { MochiRouteValue } from 'mochi-framework'; export const routes: Record = { '/demos/charts': Mochi.page('./src/demos/charts/Charts.svelte'), '/demos/charts/traffic.png': Mochi.api(async ({ url }) => { const { renderTrafficChart } = await import('./serverChart'); const width = Number(url.searchParams.get('width') ?? 640); const height = Number(url.searchParams.get('height') ?? 240); const format = url.searchParams.get('format') === 'jpeg' ? 'jpeg' : 'png'; const image = await renderTrafficChart({ width, height, format }); return new Response(image, { headers: { 'Content-Type': `image/${format}`, 'Cache-Control': 'public, max-age=3600' }, }); }), }; ``` ### index.ts ```ts import { Mochi, logger } from 'mochi-framework'; import { routes } from './routes'; await Mochi.serve({ port: 3333, development: process.env.MODE === 'development', routes, }); logger.info('Server running at http://localhost:3333'); ```