You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
273 lines
13 KiB
273 lines
13 KiB
<script lang="ts">
|
|
import {
|
|
Chart,
|
|
LineChart,
|
|
AreaChart,
|
|
BarChart,
|
|
ScatterChart,
|
|
Sparkline,
|
|
PieChart,
|
|
type PieDatum
|
|
} from '$uix/eidos/components/chart';
|
|
import { compileMorfo } from '$uix/morfo';
|
|
import { chartMorfo } from '@/uix/morfo/components/chart';
|
|
import type { CurveName } from '$libs/plots';
|
|
|
|
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
|
|
let tab = $state<Tab>('live');
|
|
|
|
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
const revenue = months.map((month, i) => ({
|
|
month,
|
|
revenue: [42, 48, 55, 51, 67, 72, 69, 78, 74, 83, 88, 95][i],
|
|
expenses: [30, 33, 37, 35, 44, 46, 45, 50, 48, 53, 55, 58][i]
|
|
}));
|
|
const sales = [
|
|
{ quarter: 'Q1', productA: 45, productB: 30 },
|
|
{ quarter: 'Q2', productA: 52, productB: 41 },
|
|
{ quarter: 'Q3', productA: 48, productB: 55 },
|
|
{ quarter: 'Q4', productA: 67, productB: 49 }
|
|
];
|
|
const scatter = [5, 12, 18, 25, 31, 38, 44, 52, 60, 68].map((x, i) => ({
|
|
x,
|
|
y: [12, 19, 15, 28, 24, 35, 31, 42, 38, 49][i]
|
|
}));
|
|
const traffic: PieDatum[] = [
|
|
{ label: 'Direct', value: 38 },
|
|
{ label: 'Organic', value: 27 },
|
|
{ label: 'Referral', value: 18 },
|
|
{ label: 'Social', value: 12 },
|
|
{ label: 'Email', value: 5 }
|
|
];
|
|
const spark = [4, 6, 5, 8, 7, 9, 8, 11, 10, 13, 12, 15, 14, 17].map((v, i) => ({ i, v }));
|
|
|
|
let curve = $state<CurveName>('monotone');
|
|
let area = $state(true);
|
|
let multi = $state(true);
|
|
|
|
const curves: CurveName[] = ['monotone', 'linear', 'step'];
|
|
|
|
const compiled = compileMorfo(chartMorfo);
|
|
const partsList = $derived([...compiled.parts.byKebab.values()]);
|
|
const money = (v: number) => `${v}k`;
|
|
|
|
const eidosSnippet = $derived(
|
|
[
|
|
"<script lang='ts'>",
|
|
" import { Chart } from '$uix/eidos/components/chart';",
|
|
'',
|
|
' // rows keyed by x (a month), one column per series',
|
|
' const data = [',
|
|
" { month: 'Jan', revenue: 42, expenses: 30 },",
|
|
" { month: 'Feb', revenue: 48, expenses: 33 }",
|
|
' // …',
|
|
' ];',
|
|
'</' + 'script>',
|
|
'',
|
|
'<Chart',
|
|
' {data}',
|
|
' x="month"',
|
|
' xScale="point"',
|
|
multi ? " y={['revenue', 'expenses']}" : ' y="revenue"',
|
|
'>',
|
|
' <Chart.Grid />',
|
|
' <Chart.YAxis format={(v) => `${v}k`} />',
|
|
' <Chart.XAxis />',
|
|
area && ' <Chart.Area series="revenue" />',
|
|
' <Chart.Line series="revenue" ' + `curve="${curve}"` + ' />',
|
|
multi && ` <Chart.Line series="expenses" curve="${curve}" />`,
|
|
' <Chart.Tooltip />',
|
|
' <Chart.Legend />',
|
|
'</Chart>'
|
|
]
|
|
.filter(Boolean)
|
|
.join('\n')
|
|
);
|
|
</script>
|
|
|
|
<div data-uix-canvas-inner>
|
|
<header>
|
|
<div data-uix-eyebrow>Charts · Chart</div>
|
|
<h1 data-uix-page-title>Chart</h1>
|
|
<p data-uix-page-lede>
|
|
The compositional chart frame and the chart family overview. <code><Chart></code> measures its
|
|
width, computes the scales from <code>data</code> + <code>x</code>/<code>y</code> via the
|
|
zero-dependency <code>$libs/plots</code> engine, and shares them with SVG children
|
|
(<code>Grid</code> / <code>XAxis</code> / <code>YAxis</code> / <code>Line</code> /
|
|
<code>Area</code> / <code>Bar</code> / <code>Scatter</code>). The ergonomic presets —
|
|
<code>LineChart</code>, <code>AreaChart</code>, <code>BarChart</code>, <code>ScatterChart</code>,
|
|
<code>Sparkline</code>, <code>PieChart</code> — compose these for you (one demo each in the
|
|
Charts group).
|
|
</p>
|
|
<div data-uix-page-meta>
|
|
<span data-uix-meta-pill><span data-uix-meta-key>parts</span>{compiled.parts.order.length}</span>
|
|
<span data-uix-meta-pill><span data-uix-meta-key>events</span>0</span>
|
|
<span data-uix-meta-pill><span data-uix-meta-key>render</span>svg · 0-dep</span>
|
|
<span data-uix-meta-pill><span data-uix-meta-key>scope</span>eidos</span>
|
|
</div>
|
|
</header>
|
|
|
|
<div data-uix-stage>
|
|
<div data-uix-stage-area>
|
|
<Chart data={revenue} x="month" xScale="point" y={multi ? ['revenue', 'expenses'] : 'revenue'} height={260} ariaLabel="Revenue overview">
|
|
<Chart.Grid />
|
|
<Chart.YAxis format={money} />
|
|
<Chart.XAxis />
|
|
{#if area}<Chart.Area series="revenue" {curve} />{/if}
|
|
<Chart.Line series="revenue" {curve} />
|
|
{#if multi}<Chart.Line series="expenses" {curve} />{/if}
|
|
<Chart.Tooltip />
|
|
<Chart.Legend />
|
|
</Chart>
|
|
</div>
|
|
<div data-uix-stage-trace>
|
|
<span data-uix-stage-trace-key>renders</span>
|
|
<span><span data-uix-stage-trace-event>compositional</span> · {multi ? 2 : 1} series · {curve}</span>
|
|
<span style="margin-inline-start: auto;"><span data-uix-stage-trace-key>scope</span> eidos · 0 events</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div data-uix-tabs role="tablist">
|
|
<button data-uix-tab data-active={tab === 'live'} onclick={() => (tab = 'live')}>Live</button>
|
|
<button data-uix-tab data-active={tab === 'api'} onclick={() => (tab = 'api')}>API</button>
|
|
<button data-uix-tab data-active={tab === 'morfo'} onclick={() => (tab = 'morfo')}>
|
|
<span data-uix-layer-badge="morfo">morfo</span>
|
|
<span data-uix-tab-count>{partsList.length}p</span>
|
|
</button>
|
|
<button data-uix-tab data-active={tab === 'sema'} onclick={() => (tab = 'sema')}>
|
|
<span data-uix-layer-badge="sema">sema</span>
|
|
<span data-uix-tab-count>0</span>
|
|
</button>
|
|
<button data-uix-tab data-active={tab === 'recipe'} onclick={() => (tab = 'recipe')}>Recipe</button>
|
|
<button data-uix-tab data-active={tab === 'a11y'} onclick={() => (tab = 'a11y')}>A11y</button>
|
|
</div>
|
|
|
|
{#if tab === 'live'}
|
|
<section data-uix-section>
|
|
<h2 data-uix-section-title>Controls</h2>
|
|
<p data-uix-section-desc>Pure <span data-uix-layer-badge="eidos">eidos</span> display — the controls drive the compositional chart above; the gallery shows the presets.</p>
|
|
<div data-uix-controls>
|
|
<label data-uix-control>
|
|
<span data-uix-control-label>curve</span>
|
|
<span data-uix-chips role="radiogroup">
|
|
{#each curves as c}<button data-uix-chip data-active={curve === c} onclick={() => (curve = c)}>{c}</button>{/each}
|
|
</span>
|
|
</label>
|
|
<label data-uix-control>
|
|
<span data-uix-control-label>area fill</span>
|
|
<span data-uix-switch><input type="checkbox" bind:checked={area} /><span data-uix-switch-label>{area ? 'on' : 'off'}</span></span>
|
|
</label>
|
|
<label data-uix-control>
|
|
<span data-uix-control-label>series</span>
|
|
<span data-uix-switch><input type="checkbox" bind:checked={multi} /><span data-uix-switch-label>{multi ? 'revenue + expenses' : 'revenue only'}</span></span>
|
|
</label>
|
|
</div>
|
|
<div data-uix-code>
|
|
<div data-uix-code-head>
|
|
<span data-uix-layer-badge="eidos">eidos</span>
|
|
<span>compositional <Chart></span>
|
|
<span data-uix-code-lang>svelte</span>
|
|
</div>
|
|
<pre><code>{eidosSnippet}</code></pre>
|
|
</div>
|
|
|
|
<h2 data-uix-section-title style="margin-top: 1.5rem;">The family</h2>
|
|
<p data-uix-section-desc>Each preset has its own demo page under Charts.</p>
|
|
<div data-uix-stage-area style="display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1rem;">
|
|
<LineChart data={revenue} x="month" xScale="point" y={['revenue', 'expenses']} title="LineChart" formatY={money} height={190} />
|
|
<AreaChart data={revenue} x="month" xScale="point" y="revenue" title="AreaChart" formatY={money} height={190} />
|
|
<BarChart data={sales} x="quarter" y={['productA', 'productB']} title="BarChart" height={190} />
|
|
<ScatterChart data={scatter} x="x" y="y" xScale="linear" title="ScatterChart" height={190} />
|
|
<PieChart data={traffic} size={170} donut={0.55} title="PieChart" ariaLabel="Traffic" />
|
|
<div><div style="font-size: 0.875rem; font-weight: 500; margin-bottom: 0.5rem; color: var(--color-content-primary);">Sparkline</div><Sparkline data={spark} x="i" y="v" /></div>
|
|
</div>
|
|
</section>
|
|
{/if}
|
|
|
|
{#if tab === 'api'}
|
|
<section data-uix-section>
|
|
<h2 data-uix-section-title>API reference — <Chart> frame</h2>
|
|
<div data-uix-table-wrap>
|
|
<table data-uix-table>
|
|
<thead><tr><th>Prop</th><th>Type</th><th>Default</th><th>Description</th></tr></thead>
|
|
<tbody>
|
|
<tr><td class="name">data</td><td class="type">{`readonly Record<string, unknown>[]`}</td><td class="default empty">—</td><td>Rows of data.</td></tr>
|
|
<tr><td class="name">x</td><td class="type">string</td><td class="default empty">—</td><td>x position key.</td></tr>
|
|
<tr><td class="name">y</td><td class="type">{`string | string[]`}</td><td class="default empty">—</td><td>Series key(s) for the y domain.</td></tr>
|
|
<tr><td class="name">xScale</td><td class="type">'linear' | 'time' | 'band' | 'point'</td><td class="default">'linear'</td><td>x mapping.</td></tr>
|
|
<tr><td class="name">height</td><td class="type">number</td><td class="default">260</td><td>Plot height (width fills container).</td></tr>
|
|
<tr><td class="name">margin / padding / niceY / yDomain</td><td class="type">various</td><td class="default empty">—</td><td>Layout + domain tuning.</td></tr>
|
|
<tr><td class="name">children</td><td class="type">Snippet</td><td class="default empty">—</td><td>SVG parts: Grid / XAxis / YAxis / Line / Area / Bar / Scatter.</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<p data-uix-section-desc>Parts (<code>Chart.Line</code>, <code>Chart.Bar</code>, …) take <code>series</code>, <code>color</code> (ColorRole) and per-mark options (<code>curve</code>, <code>radius</code>, <code>opacity</code>). See each preset's demo for the full surface.</p>
|
|
</section>
|
|
{/if}
|
|
|
|
{#if tab === 'morfo'}
|
|
<section data-uix-section>
|
|
<h2 data-uix-section-title><span data-uix-layer-badge="morfo">morfo</span> · declarative contract</h2>
|
|
<p data-uix-section-desc>Source: <code>src/uix/morfo/components/chart.ts</code>. <code>scope:['eidos']</code> — a display family, 0 events. One contract for the whole family.</p>
|
|
<div data-uix-table-wrap>
|
|
<table data-uix-table>
|
|
<thead><tr><th>Part</th><th>Marker</th><th>Element</th><th>Role</th><th>Optional</th></tr></thead>
|
|
<tbody>
|
|
{#each partsList as part}
|
|
<tr><td class="name">{part.kebab}</td><td><code data-uix-part-marker>[{part.marker}]</code></td><td class="type"><{part.defaultElement}></td><td class="type">{part.role ?? '—'}</td><td class="default">{part.optional ? 'yes' : 'no'}</td></tr>
|
|
{/each}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
{/if}
|
|
|
|
{#if tab === 'sema'}
|
|
<section data-uix-section>
|
|
<h2 data-uix-section-title><span data-uix-layer-badge="sema">sema</span> · perceptual events</h2>
|
|
<p data-uix-section-desc>
|
|
<strong>No events.</strong> A static chart is a derived render (<code>data</code> → scales →
|
|
SVG); <code>scope: ['eidos']</code>, like <code>Avatar</code>, <code>Image</code> and
|
|
<code>QrCode</code>. It emits no perceptual signal, so there is nothing for sema to project.
|
|
Hover tooltips (a future, opt-in interaction) will introduce <code>contact</code> events at
|
|
that point — see the component README · Gaps.
|
|
</p>
|
|
</section>
|
|
{/if}
|
|
|
|
{#if tab === 'recipe'}
|
|
<section data-uix-section>
|
|
<h2 data-uix-section-title>Eidos recipe</h2>
|
|
<p data-uix-section-desc>No <code>--chart-*</code> tokens — the chart styles from global theme tokens directly, so series re-tint with the theme. <code>chart.css</code> is code-split (imported from <code>chart.svelte</code>).</p>
|
|
<div data-uix-table-wrap>
|
|
<table data-uix-table>
|
|
<thead><tr><th>Surface</th><th>Token</th></tr></thead>
|
|
<tbody>
|
|
<tr><td class="name">series</td><td class="type">var(--color-{'{'}role{'}'}-solid) · primary · affirm · risk · …</td></tr>
|
|
<tr><td class="name">axis / grid</td><td class="type">var(--color-border-default / -subtle)</td></tr>
|
|
<tr><td class="name">labels / title</td><td class="type">var(--color-content-secondary / -primary)</td></tr>
|
|
<tr><td class="name">pie separator</td><td class="type">var(--color-surface-default)</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
{/if}
|
|
|
|
{#if tab === 'a11y'}
|
|
<section data-uix-section>
|
|
<h2 data-uix-section-title>Accessibility</h2>
|
|
<div data-uix-table-wrap>
|
|
<table data-uix-table>
|
|
<thead><tr><th>Part</th><th>Attribute</th><th>Value</th></tr></thead>
|
|
<tbody>
|
|
<tr><td class="name">provider (svg)</td><td>role / aria-label</td><td class="type">"img" / ariaLabel</td></tr>
|
|
<tr><td class="name">grid / axis</td><td>aria-hidden</td><td class="type">"true" (decorative)</td></tr>
|
|
<tr><td class="name">marks</td><td>—</td><td class="type">summarised by the provider label</td></tr>
|
|
</tbody>
|
|
</table>
|
|
<p data-uix-section-desc style="margin-top: 0.75rem;">Always pass <code>ariaLabel</code> with a one-line summary of the chart. A keyboard-navigable data-table fallback is a planned P2 enhancement (README · Gaps).</p>
|
|
</div>
|
|
</section>
|
|
{/if}
|
|
</div>
|