Nodes
Graph
Charts declared as data — bar, line or pie — from your figures or an endpoint's.
Charts are declared, never drawn: `graphType` fixes the form, `data` returns the payload. Holding numbers rather than an image means one declaration serves every renderer and stays diffable in the artifact.
- Helpers
- graph
- Node kind
- graph
- Category
- Data
- Resolves
- Both
- Children
- None.
| Option | Type | What it does |
|---|---|---|
id required | string | Stable address for the node. Generation endpoints target it and build artifacts diff on it, so treat a rename as a breaking change. |
graphType | "bar" | "line" | "pie" | The form of the chart. Defaults to bar. |
data required | JsonObject | (data) => JsonObject | Static only. The plot payload, as plain JSON. Any shape you like — string values in it are run through the template renderer, so {{derived.total}} resolves inside the payload as it would in prose. |
caption | string | (data) => string | Printed beneath the chart, and the words the placeholder frame shows today. Optional on both modes. |
placeholder | (data, availableTokens) => string | Dynamic only. What previews show in place of generated content. Optional, but a letter that reads badly without one cannot be reviewed. |
generalPrompt required | (data, availableTokens) => string | Dynamic only. What this node should say. |
infoPrompt | (data, availableTokens) => string | Dynamic only. Context the model should have but should not restate. |
negativePrompt | (data, availableTokens) => string | Dynamic only. What to avoid — claims, tones, or facts it must not invent. |
systemPrompt | (data, availableTokens) => string | Dynamic only. Role and voice, applied ahead of the other prompts. |
derivers | DeriverInvocation[] | Values computed before the node resolves, written to derived.* and readable from a template token. Built with derive(). |
Declared, not drawn
A graph node holds numbers and a form, never an image:
graph<MemberData>({
id: "visits-by-month",
graphType: "bar",
data: (data) => ({
labels: data.visitsByMonth.map((entry) => entry.month),
series: [{ name: "Visits", values: data.visitsByMonth.map((entry) => entry.visits) }],
}),
caption: () => "Your visits, last six months",
});
Carrying figures rather than a picture means one declaration serves every renderer, diffs legibly between builds, and can be asserted on in a test.
The payload shape is yours
data is a JsonObject and the framework never looks inside it. The
{ labels, series } shape used here is a convention, not a schema — use
whatever your renderer expects, and keep it consistent across a project.
One thing does happen to the payload: string values are run through the template
renderer, the same as prose. A label containing {{derived.total}} resolves.
Deriving in data
Running totals, percentages and rebasing belong in the data function rather
than upstream. The chart and the prose beside it are then computed from one
source, so they cannot disagree.
Variants
Bar
src/nodes/graph/bar.node.ts Discrete values across a handful of buckets.
import { graph } from "docxcelerate";
import type { SampleData } from "../sample-data.ts";
/**
* A chart is declared, not drawn: `graphType` fixes the form, `data` returns
* the payload. One node then serves the browser preview and the packed DOCX.
*/
export const VisitsByMonth = graph<SampleData>({
id: "visits-by-month",
graphType: "bar",
data: (data) => ({
labels: data.visitsByMonth.map((entry) => entry.month),
series: [{ name: "Visits", values: data.visitsByMonth.map((entry) => entry.visits) }],
}),
caption: (data) => `Your visits to ${data.centreName}, last six months`,
}); What it resolves to
The node as it appears in the DocumentModel: the JSON a renderer is handed. No styling, no layout.
{
"id": "visits-by-month",
"kind": "graph",
"mode": "static",
"graphType": "bar",
"data": {
"labels": [
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep"
],
"series": [
{
"name": "Visits",
"values": [
11,
14,
9,
16,
18,
12
]
}
]
},
"caption": "Your visits to Riverside Leisure Centre, last six months"
} Line
src/nodes/graph/line.node.ts The same data as a running total — derived in `data`, not upstream.
import { graph } from "docxcelerate";
import type { SampleData } from "../sample-data.ts";
/**
* Only `graphType` changes between the three static forms. Deriving the
* running total here keeps the chart and the prose from disagreeing.
*/
export const CumulativeVisits = graph<SampleData>({
id: "cumulative-visits",
graphType: "line",
data: (data) => {
let running = 0;
return {
labels: data.visitsByMonth.map((entry) => entry.month),
series: [
{
name: "Visits to date",
values: data.visitsByMonth.map((entry) => (running += entry.visits)),
},
],
};
},
caption: () => "Visits accumulated across the membership year",
}); What it resolves to
The node as it appears in the DocumentModel: the JSON a renderer is handed. No styling, no layout.
{
"id": "cumulative-visits",
"kind": "graph",
"mode": "static",
"graphType": "line",
"data": {
"labels": [
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep"
],
"series": [
{
"name": "Visits to date",
"values": [
11,
25,
34,
50,
68,
80
]
}
]
},
"caption": "Visits accumulated across the membership year"
} Pie
src/nodes/graph/pie.node.ts Shares of a whole.
import { graph } from "docxcelerate";
import type { SampleData } from "../sample-data.ts";
/**
* Shares of a whole. `caption` is optional, but a chart in a letter is read
* once and not returned to.
*/
export const ClassMix = graph<SampleData>({
id: "class-mix",
graphType: "pie",
data: (data) => ({
labels: data.classMix.map((entry) => entry.label),
series: [{ name: "Share of visits", values: data.classMix.map((entry) => entry.share) }],
}),
caption: () => "How you used the centre, by activity",
}); What it resolves to
The node as it appears in the DocumentModel: the JSON a renderer is handed. No styling, no layout.
{
"id": "class-mix",
"kind": "graph",
"mode": "static",
"graphType": "pie",
"data": {
"labels": [
"Swim",
"Strength",
"Classes"
],
"series": [
{
"name": "Share of visits",
"values": [
42,
33,
25
]
}
]
},
"caption": "How you used the centre, by activity"
} Dynamic
src/nodes/graph/dynamic.node.ts Figures the endpoint derives; the form still fixed locally.
import { graph } from "docxcelerate";
import type { SampleData } from "../sample-data.ts";
/**
* For figures that need deriving rather than reading. `graphType` still fixes
* the form locally, so the layout is known before the numbers are.
*/
export const PeakTimes = graph<SampleData>({
id: "peak-times",
graphType: "bar",
placeholder: () => "Your busiest hours, Monday to Sunday",
generalPrompt: (data) =>
`Plot when ${data.memberName} (${data.membershipRef}) visits, bucketed by ` +
`hour of the day across the week.`,
infoPrompt: (data) => `Their plan is ${data.plan}, which allows entry at any hour.`,
}); What it resolves to
The node as it appears in the DocumentModel: the JSON a renderer is handed. No styling, no layout.
{
"id": "peak-times",
"kind": "graph",
"mode": "dynamic",
"graphType": "bar",
"placeholder": "Your busiest hours, Monday to Sunday"
} What the endpoint is asked
Resolved against the same sample data. A preview build stops at the placeholder; a request-time build sends these.
- general
- Plot when Adaeze Nkemelu (RIV-88214) visits, bucketed by hour of the day across the week.
- info
- Their plan is Peak Anytime, which allows entry at any hour.
Notes
graphTypedefaults tobaron both helpers.- On a dynamic graph the form is still fixed locally. The endpoint supplies the
figures, and may override
graphTypeandcaptionin its response.