Charts: bar, line, pie and scatter over a query
The add-on @skaile/forge-editor-charts draws charts from the Blocks of the workspace. A chart is a Typed block of the Block type chart. It stores its configuration only (meta.chart, a Block property that has no form field); the numbers are the answer to a query whenever the chart is drawn, so a change of a task changes the chart.
| Piece | What it does | Where |
|---|---|---|
chartBlockTypes(locale) | the Block type chart | editor and server |
toQuery(config) | the DataQuery a chart asks: its own query plus group and aggregates | anywhere |
toVegaLite(config, result) | the pure Vega-Lite specification of the chart | anywhere |
chartNodeViews | the node view that draws the chart and carries the chart editor | editor (/vue, /react) |
chartTargets({ answer }) | the render target chart-svg | server |
The configuration
interface ChartConfig {
type: 'bar' | 'line' | 'pie' | 'scatter'
/** Which Blocks the chart is drawn from (Block types, filter). */
query: DataQuery
/** The x axis (bar, line, scatter) or the slices (pie): a property key or a pseudo-field such as `$type`. */
x: string
/** What is measured: an aggregate of a field, named `as`. */
series: { field?: string, fn: AggregateFn, as: string }[]
color?: string
stack?: boolean
}
toQuery(config) copies config.query and adds group: { field: config.x, hideEmpty: true }, one aggregate per series and a row limit, and selects only the fields the chart reads. The Block index answers the grouping (backend.index.query), so a backend that groups in the database sends only the rows the chart needs. A scatter chart has no grouping: it reads the rows. Each group has one point per series: count counts the rows, sum, avg, min and max read the numbers of field. Blocks without a value in the x field are left out of the chart.
The Block's own chart in the kitchen sink counts tasks per status:
{ "type": "bar", "query": { "v": 1, "source": { "kind": "blocks", "blockType": "task" } }, "x": "status", "series": [{ "fn": "count", "as": "Anzahl" }] }
The Vega-Lite specification
toVegaLite(config, result) returns a Vega-Lite 6 specification as a plain object. It is pure: the same configuration and answer give the same specification, and it carries no size. The data are the points in long form (x, series, value), so any number of series share one encoding. bar stacks several series (or puts them side by side with stack: false), line takes a time axis when every x value is a date and a number axis for numbers, pie shows the first series, scatter plots two numeric fields. A single series takes color; several take the palette and a legend. Both the node view (vega-embed) and the server target (vega) draw this one specification.
Install and wire
import { defaultBlockTypes } from '@skaile/forge-editor/composites'
import { chartBlockTypes } from '@skaile/forge-editor-charts/client'
import { chartNodeViews } from '@skaile/forge-editor-charts/vue' // or '/react'
const editor = composeEditor({
content: 'rich',
blockTypes: [...defaultBlockTypes(t), ...chartBlockTypes('de')],
// The node views by Block type id: a `chart` block draws its chart, every other type keeps its own view.
blockNodeViews: chartNodeViews,
})
blockNodeViews is an option of the composites feature (composeEditor, AdvancedEditor, ComposedSurface): a node view per Block type id that replaces the standard Typed block view for that type. The title, the chrome and the content of the block stay; the view adds the chart.
import { createRenderer, markdownTarget } from '@skaile/forge-editor/adapters/render'
import { answerFromDocument, chartBlockTypes, chartTargets } from '@skaile/forge-editor-charts/server'
// installEditorServer({ ports: { render }, blockTypes })
const answer = answerFromDocument({ load: async id => (await getDocument(id))?.content ?? null, blockTypes: [...serverBlockTypes] })
export const render = createRenderer([markdownTarget(), ...chartTargets({ answer })])
The entry @skaile/forge-editor-charts/client reaches neither vega nor the render target; vega-embed loads when the first chart draws. The /server entry reaches no editor code and needs no DOM.
In the editor
The node view asks backend.index.query (the Blocks of every Document the user may read) and, for a backend without it, answers the same query over the Blocks of the open Document. It asks again while the chart is visible: after a change of the block or of the Blocks (debounced), and every few seconds, because Blocks of other Documents change without the editor hearing of it. A query the Block index refuses shows its message instead of a chart.
An editable chart has a Diagramm einstellen button that opens the chart editor under the chart: the chart type, the x field, the series (aggregate, field, name), the colour, stacking, and the library's query builder for the Blocks the chart is drawn from. Every change writes meta.chart through setBlockProperty, one key, so collaborators' other properties survive.
Render target
chartTargets({ answer }) gives chart-svg for a Block of the type chart: the query is answered by answer (answerFromDocument reads the Blocks of the Document the render is for), the specification is compiled with vega-lite, and vega renders the SVG without a DOM or a canvas. The file is named after the chart's title. A render of any other block fails with a message the client shows.
Strings
The add-on keeps its own small catalog in German and English (CHARTS_MESSAGES, createChartsTranslator(locale, overrides)), because the library's message keys are closed over the library's own strings. chartBlockTypes(locale) takes the language; the node view reads it from the editor context.
In the demo app
The charts always draw on /kitchen-sink/databases: the page imports the add-on's /client (chartBlockTypes) and /vue (chartNodeViews) entries itself, whatever NUXT_PUBLIC_ADDONS_CHARTS says, and gives its editor a backend without a Block index, so the charts and the data views beside them answer from the open document. Its „Task tracker“ sample has eight tasks, two data views and two charts over them. NUXT_PUBLIC_ADDONS_CHARTS=1 (see .env.example; a runtime flag, read by the server and the browser alike) adds the server's target chart-svg, the SVG export; without it the page shows a hint card. The documents under /documents load the Block type and the node view only with the flag on. See Kitchen sink: learning path.
Index on Postgres: typos, meaning and SQL pushdown
The @skaile/forge-editor-index-pg add-on: trigram title search, a vector table kept current on replace, nearest and related Blocks, query pushdown, and the Embedder port with its OpenAI-compatible adapter.
Collaboration on the client
Collaboration is a feature: mount a surface with a Yjs session, per-client undo, authors and comments, positions that survive concurrent edits, and matching schemas.