Vancetope — Document Kind chart
Specifies the
chartpayload for documents that carry one or more data series with chart-rendering metadata. One kind for all chart types — the variant (line,bar,candlestick, …) is a discriminator inside the document, not a separate kind. Only JSON and YAML; Markdown is intentionally not supported. See also: doc-kind-graph | doc-kind-records | doc-kind-sheet | web-ui
1. Purpose
Use cases: Time series charts (Line/Area), comparison charts (Bar), distributions (Pie/Donut), correlations (Scatter), financial/OHLC data (Candlestick), density visualizations (Heatmap). Embedded in Reports, delivered as Worker output, manually maintained in a Project doc pool.
Distinctions:
- records: Tabular without visualization. If the table is to receive a chart, the chart lives as its own
kind: chartdocument, which references the records or duplicates the values. - sheet: 2D sheet with formulas, not intended for visualization.
- graph: Nodes + edges, not a numerical data diagram.
- data: Unstructured storage. To get a chart, use this Kind and not
datawith an ad-hoc structure.
Design Principle — One Kind, Many Chart Types. All data diagrams share the same Document Shape (axes, series, title, legend). The specific type is a field (chart.chartType), not a separate Kind. Rationale: the data model is almost identical across types; a user who switches a Line chart to Bar changes one field — no Kind change, no file change. Established libraries (ECharts, Plotly, Vega) do the same.
Design Principle — Lean custom schema, ECharts option as escape hatch. We define a custom mini-schema (10-12 fields) in vance-api that the Vancetope Face renderer maps to an ECharts option. Rationale:
- Grafana spec is coupled to panel data sources — we have static Document content, not a pull query.
- Vega-Lite is academically clean but too cumbersome for LLM generation (Grammar-of-Graphics concepts complicate clean YAML generation).
- Raw ECharts option is too library-specific and verbose (~50 fields); we don’t want to rewrite all Docs if the library changes.
- Custom schema covers 80% of use cases and is AI-friendly; power users get
echartsOptionOverrideas a deep-merge slot.
What this spec defines:
- Top-level block
chartwithchartTypeand display metadata. - Axis model (
xAxis,yAxis). - Series model with
chartTypediscriminator (Document-level; v2 allows per-series override). - Data point shapes per chart type — a closed table.
- Format mapping JSON and YAML — no Markdown.
- Web UI activation with Apache ECharts as renderer.
- Escape hatch
echartsOptionOverridefor power users.
What it does not define:
- Markdown form. Deliberately excluded — chart data as CSV-light would be readable, but axis/series configuration would not. Documents with
kind: chart+ Markdown only get the raw editor. - Data source bindings (live-pull from Records, SQL queries, external APIs). v1 is static; Documents carry their data inline. Live bindings are §6.
- Interactive chart features (cross-filter, brush selection, drilldown). v1 is display-only with tooltip + dataZoom for time series.
- Per-document theming beyond colors per series. Theme comes from the DaisyUI layer.
- Chart composition (multiple charts in one Document). One Document = one chart. Multiple charts = multiple Documents.
2. Data Model
2.1 Top-Level
| Field | Type | Required | Meaning |
|---|---|---|---|
kind |
string = "chart" |
yes | For dispatcher recognition. |
chart |
ChartHeader |
yes | Chart metadata (type, title, legend, theme hints). |
xAxis |
Axis |
no | X-axis. Default { type: 'category' }. Ignored for pie/donut. |
yAxis |
Axis |
no | Y-axis. Default { type: 'value' }. Ignored for pie/donut. |
series |
Series[] |
yes | At least one series. Datapoint shape depends on chartType. |
echartsOptionOverride |
object |
no | Raw ECharts option, deep-merged onto the generated option (escape hatch). |
Unknown top-level keys remain in doc.extra and are re-emitted verbatim when written (analogous to all other Kinds).
2.2 ChartHeader
| Field | Type | Required | Meaning |
|---|---|---|---|
chartType |
enum |
yes | One of line, bar, area, scatter, pie, donut, candlestick, heatmap. |
title |
string |
no | Display title above the chart. |
subtitle |
string |
no | Second line below the title, rendered smaller. |
legend |
boolean |
no | Default true. Toggles the legend on/off. |
stacked |
boolean |
no | Default false. Only relevant for bar/area/line — series are stacked. |
smooth |
boolean |
no | Default false. Only for line/area — cubic spline interpolation instead of polyline. |
2.3 Axis
| Field | Type | Required | Meaning |
|---|---|---|---|
type |
enum category | value | time | log |
no | Default category for xAxis, value for yAxis. |
label |
string |
no | Axis label. |
min |
number |
no | Forces the lower limit (otherwise auto). |
max |
number |
no | Forces the upper limit (otherwise auto). |
categories |
string[] |
(see below) | Only for type: category — explicit category order. If not specified, the renderer derives the tick list from the data points of the first series. |
The Axis field list is closed: the codec reads only type, label, min, max, categories. Unknown axis keys do not end up in extra, but are discarded — an xAxis.name or axisLabel written out of ECharts habit will thus silently disappear (no codec error, no render). Raw ECharts axis options belong in echartsOptionOverride.
categories is required as soon as multiple series on a Category-Axis do not share the same x set: otherwise, the tick list only comes from the first series, and points of later series with a missing x there have no slot on the axis and do not render — without a codec error. In that case, list all x values of all series in character order and set stacked: true so that for each category, a bar spans the full slot width instead of being distributed across all series.
type: time interprets the X values of the data points as ISO-8601 strings or Unix milliseconds. type: log requires positive values; for negative values, a codec warning + fallback to value.
2.4 Series
| Field | Type | Required | Meaning |
|---|---|---|---|
name |
string |
yes | Display name in the legend and tooltip. Unique per Document. |
color |
string (HTML-Hex) |
no | Color of the series. Missing → ECharts Theme palette. |
data |
DataPoint[] |
yes | At least one data point. Shape depends on chart.chartType — see §2.5. |
Per-series override of chartType (mixed types like Line-on-Bar) is v2 — see §6.
2.5 Data Point Shapes per Chart Type
chartType |
Object Form | Tuple Form (alternative) |
|---|---|---|
line / bar / area |
{ x: string\|number, y: number } |
[x, y] |
scatter |
{ x: number, y: number, size?: number } |
[x, y] or [x, y, size] |
pie / donut |
{ name: string, value: number, color?: string } |
— |
candlestick |
{ t: string\|number, o: number, h: number, l: number, c: number, v?: number } |
[t, o, h, l, c] or [t, o, h, l, c, v] |
heatmap |
{ x: string\|number, y: string\|number, v: number } |
[x, y, v] |
Rules:
- Both forms are equivalent in the codec. When reading, tuple-form points are internally normalized to object form; when writing, the form in which the data point was received wins (round-trip stable per point). Mixing within a series is allowed.
chartTypeanddatashape must match. Mismatch (e.g.,chartType: candlestickwith{x, y}points) →ChartCodecError("Data shape does not match chartType: <type>")on parse.pie/donuthave no axis semantics;xAxis/yAxisare ignored (codec drops warnings).- For
timeaxes:x/tis an ISO-8601 string (2024-01-02or2024-01-02T15:30:00Z) or Unix milliseconds (1704153600000). Inconsistent types within a series → codec warning, fallback to string sort.
2.6 Canonical Form (JSON)
{
"$meta": { "kind": "chart" },
"chart": {
"chartType": "line",
"title": "Daily Active Users",
"legend": true,
"smooth": true
},
"xAxis": { "type": "time", "label": "Date" },
"yAxis": { "type": "value", "label": "DAU" },
"series": [
{
"name": "Web",
"color": "#3b82f6",
"data": [
{ "x": "2024-01-01", "y": 1200 },
{ "x": "2024-01-02", "y": 1340 },
{ "x": "2024-01-03", "y": 1280 }
]
},
{
"name": "Mobile",
"color": "#10b981",
"data": [
["2024-01-01", 800],
["2024-01-02", 920],
["2024-01-03", 1010]
]
}
]
}
Header convention per format: identical to doc-kind-graph — both JSON and YAML carry kind in a $meta mapping at the top level. Structured top-level objects (chart, xAxis, yAxis, series) remain outside of $meta.
3. On-Disk Formats
3.1 JSON
See §2.6 for the canonical form.
Reading Rules:
kindfrom$meta.kind(with top-level fallback for legacy Documents).- Top-level keys
chart,xAxis?,yAxis?,series,echartsOptionOverride?. Other keys (except$meta) →doc.extra. chart.chartTypeis required and must be from the enum; otherwise, a codec error.seriesmust be an array of objects with at least one entry. For each series,nameanddataare required; entries without them are dropped.- Data points: for each series, the shape is validated against
chartType(see §2.5). Individual malformed points are dropped with a codec warning; the series survives if at least one point remains valid. echartsOptionOverrideis not parsed-validated; whatever is in it is fed directly into ECharts during rendering (mergestrategy). The Document owner is responsible for valid ECharts options.
Writing Rules:
- 2-space indent.
- Top-level order:
$meta,chart,xAxis,yAxis,series,echartsOptionOverride, thenextrapass-through. - Series key order:
name,color?,data, then unknown pass-through keys. - Data points: for each point, the input form (object or tuple) wins. For code modification (e.g., a new point inserted from the UI), object form is used.
- Fields that are not set are omitted (no
nullwriting).
3.2 YAML
$meta:
kind: chart
chart:
chartType: candlestick
title: AAPL 2024
xAxis:
type: time
label: Date
yAxis:
type: value
label: USD
series:
- name: AAPL
data:
- { t: 2024-01-02, o: 187.15, h: 188.44, l: 183.89, c: 185.64, v: 82488700 }
- { t: 2024-01-03, o: 184.22, h: 185.88, l: 183.43, c: 184.25, v: 58414500 }
- [2024-01-04, 182.15, 183.09, 180.88, 181.91, 71983600]
Single-Document: Top-level mapping with $meta: { kind: chart } as the first key, followed by chart, xAxis?, yAxis?, series, echartsOptionOverride? at the same level. Block style for top-level structures, flow style allowed for compact data points (see example above).
3.3 Markdown
Deliberately not supported. Markdown bodies with kind: chart are rejected by the codec; the Web UI offers only the raw editor and no chart tab. Rationale in §1 under “What it does not define”.
4. Server Path
Like graph/data/sheet: no dedicated endpoint, no server-side chart parser/renderer. Editor loads via GET /documents/{id}, parses in the browser, writes back via PUT /documents/{id}. HeaderStrategy automatically mirrors kind: chart to DocumentDocument.kind.
Server-side rendering (e.g., PNG export for Reports) is not v1 — see §6.
chart/xAxis/yAxis/series blocks are transparent to the server; it sees them like all other top-level JSON/YAML keys.
5. Web UI
5.1 Editor Activation
kind === 'chart'+ Format ∈ {json, yaml} → TabsChart(Default) /Raw.kind === 'chart'+ Markdown → onlyRaweditor, no Chart tab.- Otherwise: only
Raweditor.
When switching Chart → Raw, the parsed model is serialized back; when switching Raw → Chart, the codec reparses the current body. Round-trip is idempotent. In case of a parse error, the Chart tab shows a focused error card (same style as doc-kind-data §5.5).
5.2 Library
Apache ECharts (echarts, MIT, ~1 MB minified with tree-shakable v5 import):
- Native Candlestick (
series.type: 'candlestick') including OHLC tooltip; volume overlay as a second series is possible. - All v1 chart types out-of-the-box: line, bar, area (
line+areaStyle), scatter, pie, candlestick, heatmap. - Declarative option object — clean mapping from the Vancetope chart schema.
- Pan/Zoom (
dataZoomcomponent), tooltip, legend built-in. - DaisyUI theme integration via CSS variables (background/text colors from
--b1/--bc).
No Vue wrapper (vue-echarts): direct echarts.init() call on a DOM ref is sufficient for v1 and avoids an additional library with its own release cadence. In <script setup>, ECharts is initialized on onMounted, disposed on onBeforeUnmount.
Alternatives considered and rejected:
- Chart.js: Candlestick only via external plugin (
chartjs-chart-financial), insufficiently maintained. - Plotly.js: ~3 MB bundle, too much overhead for our use cases.
- Vega-Lite: nice standard, but Candlestick requires manual layer construction — bad for LLM-generated charts.
- Lightweight Charts (TradingView): great for OHLC, but only financial charts.
5.3 Feature Set v1
| Feature | v1 | Note |
|---|---|---|
| Render all v1 Chart Types | ✓ | line, bar, area, scatter, pie, donut, candlestick, heatmap |
| Tooltip on Hover | ✓ | ECharts standard, formatted per chart type |
| Toggle Legend | ✓ | Click on legend entry hides series |
| Pan/Zoom on Time Axis | ✓ | dataZoom slider below the chart for xAxis.type === 'time' |
| Theme Hook on DaisyUI | ✓ | Background/text from CSS variables |
| Side Panel: Change ChartType | ✓ | Dropdown in toolbar (line/bar/area/…) — writes chart.chartType |
| Side Panel: Title, Legend, Stacked | ✓ | Inputs in toolbar |
| Side Panel: Change Axis Type | ✓ | xAxis/yAxis type dropdown |
| Per-Series Color Picker | ✓ | HTML5 <input type="color"> |
echartsOptionOverride Editor |
✓ | Mini JSON textarea in toolbar (power users) |
| Edit Datapoints in UI | ◯ | best-effort: for pie/bar add-row dialog; for time-series editing via Raw tab |
| Live Data Binding (Records / SQL) | ✗ | v2, see §6 |
| Server-Side PNG Export | ✗ | v2 |
| Cross-Chart Brush / Drilldown | ✗ | v3 |
(◯) = best-effort, not fully as spec’d
5.4 Components
<ChartView>— Top-level. Receives:doc: ChartDocument, emitsupdate:doc.- Holds local, mutable copy of
chart/xAxis/yAxis/series. - Maps the Vancetope schema → ECharts option via
chartSchemaToEChartsOption(doc)(pure function in@vance/shared/chart). - Initializes ECharts in
onMounted, callschart.setOption(option, true)on schema change, disposes inonBeforeUnmount. - Merges
echartsOptionOverridevialodash.mergeover the generated option (Document override wins). - Side panel on the right: global chart properties (Type, Title, Legend, Axes); when a series is selected (click on legend), Color + Name are editable.
- Toolbar at the top:
+ Datapoint(for pie/bar),+ Series,Reset Zoom, hint to Raw tab for complex edits.
- Holds local, mutable copy of
<ChartTypePicker>— Dropdown with icon buttons per chart type, switcheschart.chartType. On change, data points are validated against the new shape; if it doesn’t fit, a warning dialog shows the incompatibility and offers “delete data, change type” vs. “cancel”.
5.5 Visual Conventions
- Chart container fills editor content to 65 vh / min. 420 px height (analogous to Mindmap/Graph).
- Default theme: ECharts with dynamically calculated colors from DaisyUI CSS variables — background
hsl(var(--b1)), texthsl(var(--bc)), grid lineshsl(var(--bc) / 0.1). - Series default palette: ECharts standard, overridden per series via
colorfield. - Side panel on the right (or bottom on narrow viewports): Properties, structured into sections “Chart”, “X-Axis”, “Y-Axis”, “Series”.
- Toolbar at the top: ChartType picker, “Reset Zoom”, hint text with cheat sheet for operation.
5.6 Changing Chart Type
Datapoint shapes differ per chart type (§2.5). When changing via the side panel, <ChartView> checks:
- Are the existing data points compatible with the new type?
line↔bar↔area↔scatter→ yes, all share{x, y}.pie↔donut→ yes, both use{name, value}.- Switching between groups → no (e.g., line → pie requires different data points).
- In case of incompatibility: Modal with two options — “Discard data and change type” or “Cancel”. No auto-mapping (e.g., line → pie would guess
{name=x, value=y}— error-prone and degrades LLM training signal).
6. Future (not v1)
6.1 Live Data Binding
series.data becomes series.dataRef: { document: <docId>, query: <path> } — the renderer loads another Document at render time (e.g., a kind: records) and projects it onto the data points. This allows the chart to live on the source data and not require manual updates when data changes. A separate spec point, as query language (JSONPath? jq subset? Records column selector?) and caching/refresh semantics need to be clarified.
6.2 Per-Series-ChartType (Mixed Charts)
series[i].chartType overrides the Document-level chart.chartType for that one series. Use case: volume bar below candlestick, line trend over bar comparison. v2 extension; v1 only requires the Document-level field because 80% of charts are single-type.
6.3 Server-Side Rendering (PNG/SVG Export)
For Reports, email embedding, external linking. ECharts can be rendered on the server via Node-Canvas (echarts-server-renderer or similar). A separate spec point, as the server pipeline (vance-brain) currently has no browser libraries, and either a Node sidecar or a JVM-based renderer (JFreeChart?) must be added.
6.4 Additional Chart Types
- Radar — same shape change effort as pie/heatmap; comes when a use case arises.
- Boxplot — 5-number summary per data point, ECharts supports it natively.
- Treemap / Sankey — tree or node-link structure, different data point shape (recursive / nodes+edges). Worth considering: whether Sankey should rather live in
kind: graphwith a Sankey render variant. - Gauge / Funnel — rare, later.
6.5 Annotations and Markers
markPoint/markLine on Series — e.g., “release date as vertical line”, “min/max markers”. ECharts supports this natively; only schema extension in the Vancetope schema is needed.
6.6 Recipe Tool: chart_create
A chart_create(documentName, chartType, series) tool for Workers that creates a chart as a Document from the Recipe. Today, Workers can do this via doc_write(kind="chart", …) with a YAML body, but a typed tool would be more LLM-friendly. Follows the pattern of existing doc_write routine.
7. Open Points
- Default format on creation:
.jsonor.yaml? Suggest YAML — consistent withdata/graph, more compact, reliably generated by LLMs. - Tuple form vs. Object form as default for UI insert: If the user adds a data point via UI, the codec writes object form (more readable). For large datasets (e.g., > 200 points), tuple form would be more compact — UI could automatically switch above a threshold. v1: always object form for UI insert; LLM output with tuple form is preserved.
echartsOptionOverridevalidation: Raw ECharts options are fed at render time; typos lead to silent render defects, not codec errors. Acceptable as an escape hatch — those who use it know what they are doing. Optional lint warning in the side panel is polish.- Bundle size: ECharts tree-shakable with v5 modular import (~600 KB for our chart set) — vs. full build ~1.1 MB. We import modularly (
echarts/core+ specific charts and components). Lazy loading of<ChartView>analogous to<MindmapView>/<GraphView>is v1. - Time format ambiguity:
2024-01-02(date-only) vs.2024-01-02T15:30:00Z(datetime). ECharts handles both; codec does not normalize. If LLMs mix, the tooltip format string can appear inconsistent — polish point. - Incompatible ChartType change: Current solution requires “discard data or cancel”. A smarter approach would be a “transform data if possible” mode (e.g., line → pie aggregates over X-axis), but this opens up aggregation logic complexity that is not needed in v1.