Nodos
Graph
Gráficos de Word de verdad — barras, líneas, áreas, sectores, anillo o dispersión — declarados como datos en lugar de dibujados como imagen.
Charts are declared, never drawn: `graphType` fixes the form, `data` carries the payload. What lands in the `.docx` is a DrawingML chart part with every value cached in it — the chart Word builds from Insert → Chart — so a reader can select it, restyle it and open its numbers. Nothing is rasterised, which is why charts cost the package no drawing library and no native module.
- Helpers
- Graph
- Clase de nodo
- graph
- Categoría
- Datos
- Se resuelve
- Both
- Hijos
- None.
| Opción | Tipo | Qué hace |
|---|---|---|
id | string | Stable address for the node. Engines target it and build artifacts diff on it, so treat a rename as a breaking change. Optional: a node without one takes an id from where it sits, which is what keeps branches and loops from forcing you to invent names. Two nodes claiming one id is an error rather than a race. |
graphType | "bar" | "barHorizontal" | "line" | "area" | "pie" | "doughnut" | "scatter" | "radar" | "bubble" | The form of the chart. Defaults to bar. radar closes the category axis into a ring, for comparing shapes rather than amounts; bubble is a scatter whose points carry a third figure as their size, from sizes on the series. |
data obligatorio | GraphData | Static only. { categories?, series: { label?, values, color?, sizes? }[] }. A null value is a gap rather than a zero. String values in it are run through the template renderer, so {{derived.total}} resolves inside the payload as it would in prose. |
stacked | boolean | "percent" | Whether the series stack. true stacks them to a total; "percent" stacks them to the same height, so what is read is each series' share of its category — the 100% stacked chart. Bars, lines and areas take it; a pie is already a share of a whole. |
caption | string | Printed beneath the chart, as a paragraph of its own rather than text drawn into the frame. Optional on both modes. |
placeholder | string | Dynamic only. What previews show in place of generated content. Also settable with useSetPlaceholders. Optional, but a document that reads badly without one cannot be reviewed. |
generalPrompt obligatorio | string | Dynamic only. What this node should say. |
infoPrompt | string | Dynamic only. Context the model should have but should not restate. |
negativePrompt | string | Dynamic only. What to avoid — claims, tones, or facts it must not invent. |
systemPrompt | string | Dynamic only. Role and voice, applied ahead of the other prompts. |
examplePrompt | string | Dynamic only. What a good answer looks like, written out as finished text. Shown last, because it is what the answer gets measured against. |
derivers | DeriverInvocation[] | Values the engine computes before the node resolves, written to derived.* and readable from a template token. Built with derive(). These survive publishing and run per document — use them for anything computed from request data. useDeriver runs one during the build instead. |
Declarado, no dibujado
Un nodo de gráfico contiene números y una forma, nunca una imagen:
export const VisitsByMonth: Graph = () => {
const [state] = useState((data: MemberData) => ({
months: data.visitsByMonth.map((entry) => entry.month),
visits: data.visitsByMonth.map((entry) => entry.visits),
}));
return (
<Graph
id="visits-by-month"
title="Visits by month"
graphType="bar"
valueAxisTitle="Visits"
data={{
categories: state.months,
series: [{ label: "Visits", values: state.visits }],
}}
caption="Your visits, last six months"
/>
);
};
Lo que llega al .docx es un gráfico de Word de verdad: una parte DrawingML
con todos los valores dentro, más el libro que abre «Editar datos». Quien lo
recibe puede seleccionarlo, cambiar su estilo, cambiar su tipo y abrir sus
números, y Word lo dibuja como vectores a cualquier tamaño. Nada de esto se
rasteriza.
La forma de los datos
data está tipado: categorías y series, cada serie con una etiqueta, sus
valores y, opcionalmente, un color.
interface GraphData {
categories?: string[];
series: Array<{ label?: string; values: (number | null)[]; color?: string }>;
}
Un null es un hueco, no un cero. Un mes que aún no se ha leído no es un mes
que midió nada, y Word omite el punto en lugar de hacer caer la línea.
Los colores vienen del tema (palette.series, una serie fija y validada para
daltonismo), de modo que un documento se puede volver a tematizar sin que sus
gráficos dejen de encajar.
Una cosa sí le ocurre a los datos: los valores de tipo cadena pasan por el
renderizador de plantillas, igual que la prosa. Una etiqueta que contenga
{{derived.total}} se resuelve.
Derivar en el estado
Los acumulados, los porcentajes y los rebasados van en la función data, no
aguas arriba. Así el gráfico y el texto que lo acompaña se calculan desde una
única fuente y no pueden contradecirse.
Variantes
Bar
src/nodes/graph/bar.node.tsx Discrete values across a handful of buckets.
import { Graph, useState } from "docxcelerate/template";
import type { SampleData } from "../sample-data.ts";
/**
* A chart is declared, not drawn: `graphType` fixes the form, `data` carries
* the payload. One node then serves the browser preview and the packed DOCX.
*/
export const VisitsByMonth: Graph = () => {
const [state] = useState((data: SampleData) => ({
centreName: data.centreName,
months: data.visitsByMonth.map((entry) => entry.month),
visits: data.visitsByMonth.map((entry) => entry.visits),
}));
return (
<Graph
id="visits-by-month"
title={`Visits to ${state.centreName}`}
graphType="bar"
valueAxisTitle="Visits"
data={{
categories: state.months,
series: [{ label: "Visits", values: state.visits }],
}}
caption={`Your visits to ${state.centreName}, last six months`}
/>
);
}; En qué se resuelve
El nodo tal como aparece en el DocumentModel: el JSON que recibe un renderizador. Sin estilos, sin maquetación.
{
"id": "visits-by-month",
"kind": "graph",
"mode": "static",
"graphType": "bar",
"title": "Visits to Riverside Leisure Centre",
"valueAxisTitle": "Visits",
"data": {
"categories": [
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep"
],
"series": [
{
"label": "Visits",
"values": [
11,
14,
9,
16,
18,
12
]
}
]
},
"caption": "Your visits to Riverside Leisure Centre, last six months"
} Line
src/nodes/graph/line.node.tsx The same data as a running total — derived in `data`, not upstream.
import { Graph, useState } from "docxcelerate/template";
import type { SampleData } from "../sample-data.ts";
/**
* Only `graphType` changes between the three static forms. Deriving the
* running total inside the state initializer keeps the chart and the prose
* from disagreeing, and keeps the work out of the returned element.
*/
export const CumulativeVisits: Graph = () => {
const [state] = useState((data: SampleData) => {
let running = 0;
return {
months: data.visitsByMonth.map((entry) => entry.month),
toDate: data.visitsByMonth.map((entry) => (running += entry.visits)),
};
});
return (
<Graph
id="cumulative-visits"
title="Visits to date"
graphType="line"
data={{
categories: state.months,
series: [{ label: "Visits to date", values: state.toDate }],
}}
caption="Visits accumulated across the membership year"
/>
);
}; En qué se resuelve
El nodo tal como aparece en el DocumentModel: el JSON que recibe un renderizador. Sin estilos, sin maquetación.
{
"id": "cumulative-visits",
"kind": "graph",
"mode": "static",
"graphType": "line",
"title": "Visits to date",
"data": {
"categories": [
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep"
],
"series": [
{
"label": "Visits to date",
"values": [
11,
25,
34,
50,
68,
80
]
}
]
},
"caption": "Visits accumulated across the membership year"
} Pie
src/nodes/graph/pie.node.tsx Shares of a whole.
import { Graph, useState } from "docxcelerate/template";
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 = () => {
const [state] = useState((data: SampleData) => ({
activities: data.classMix.map((entry) => entry.label),
shares: data.classMix.map((entry) => entry.share),
}));
return (
<Graph
id="class-mix"
title="Share of visits"
graphType="pie"
dataLabels
data={{
categories: state.activities,
series: [{ label: "Share of visits", values: state.shares }],
}}
caption="How you used the centre, by activity"
/>
);
}; En qué se resuelve
El nodo tal como aparece en el DocumentModel: el JSON que recibe un renderizador. Sin estilos, sin maquetación.
{
"id": "class-mix",
"kind": "graph",
"mode": "static",
"graphType": "pie",
"title": "Share of visits",
"dataLabels": true,
"data": {
"categories": [
"Swim",
"Strength",
"Classes"
],
"series": [
{
"label": "Share of visits",
"values": [
42,
33,
25
]
}
]
},
"caption": "How you used the centre, by activity"
} Stacked to a share
src/nodes/graph/stacked.node.tsx `stacked="percent"` — the mix within each column, not the size of it.
import { Graph, useState } from "docxcelerate/template";
import type { SampleData } from "../sample-data.ts";
/**
* `stacked="percent"` is the 100% stacked chart, and it is a different reading
* from `stacked`: every column is drawn to the same height, so what is
* compared is the mix within each month rather than the size of the months.
*
* The raw counts are what the file carries. Word works the shares out itself
* and labels the axis 0% to 100%, so nothing here divides — pre-dividing would
* hand Word shares of shares.
*/
export const VisitMix: Graph = () => {
const [state] = useState((data: SampleData) => ({
months: data.visitMix.map((entry) => entry.month),
swim: data.visitMix.map((entry) => entry.swim),
strength: data.visitMix.map((entry) => entry.strength),
classes: data.visitMix.map((entry) => entry.classes),
}));
return (
<Graph
id="visit-mix"
title="What you came for, month by month"
graphType="bar"
stacked="percent"
data={{
categories: state.months,
series: [
{ label: "Swim", values: state.swim },
{ label: "Strength", values: state.strength },
{ label: "Classes", values: state.classes },
],
}}
caption="Share of each month's visits, by activity"
/>
);
}; En qué se resuelve
El nodo tal como aparece en el DocumentModel: el JSON que recibe un renderizador. Sin estilos, sin maquetación.
{
"id": "visit-mix",
"kind": "graph",
"mode": "static",
"graphType": "bar",
"title": "What you came for, month by month",
"stacked": "percent",
"data": {
"categories": [
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep"
],
"series": [
{
"label": "Swim",
"values": [
5,
6,
4,
8,
9,
5
]
},
{
"label": "Strength",
"values": [
4,
5,
3,
5,
6,
4
]
},
{
"label": "Classes",
"values": [
2,
3,
2,
3,
3,
3
]
}
]
},
"caption": "Share of each month's visits, by activity"
} Radar
src/nodes/graph/radar.node.tsx Two shapes held against one another, on one shared scale.
import { Graph, useState } from "docxcelerate/template";
import type { SampleData } from "../sample-data.ts";
/**
* Two shapes held against one another. A radar answers "strong where", not
* "how much" — a reader cannot compare the areas of two rings, and is not
* meant to. The axis is scored out of 100 so the spokes share a scale;
* plotting raw counts of different things on one ring compares nothing.
*/
export const FacilityMix: Graph = () => {
const [state] = useState((data: SampleData) => ({
facilities: data.facilityUse.map((entry) => entry.facility),
you: data.facilityUse.map((entry) => entry.you),
average: data.facilityUse.map((entry) => entry.average),
}));
return (
<Graph
id="facility-mix"
title="What you used, against the average"
graphType="radar"
data={{
categories: state.facilities,
series: [
{ label: "You", values: state.you },
{ label: "Centre average", values: state.average },
],
}}
caption="Use of each facility, scored out of 100"
/>
);
}; En qué se resuelve
El nodo tal como aparece en el DocumentModel: el JSON que recibe un renderizador. Sin estilos, sin maquetación.
{
"id": "facility-mix",
"kind": "graph",
"mode": "static",
"graphType": "radar",
"title": "What you used, against the average",
"data": {
"categories": [
"Pool",
"Gym",
"Classes",
"Sauna",
"Courts"
],
"series": [
{
"label": "You",
"values": [
82,
61,
45,
30,
12
]
},
{
"label": "Centre average",
"values": [
54,
70,
63,
22,
38
]
}
]
},
"caption": "Use of each facility, scored out of 100"
} Dynamic
src/nodes/graph/dynamic.node.tsx Figures the endpoint derives; the form still fixed locally.
import { Graph, useSetPlaceholders, useSetPrompts, useState } from "docxcelerate/template";
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 = () => {
const [state] = useState((data: SampleData) => ({
name: data.memberName,
ref: data.membershipRef,
plan: data.plan,
}));
useSetPrompts({
generalPrompt: `Plot when ${state.name} (${state.ref}) visits, bucketed by ` +
`hour of the day across the week.`,
infoPrompt: `Their plan is ${state.plan}, which allows entry at any hour.`,
});
useSetPlaceholders("Your busiest hours, Monday to Sunday");
return <Graph id="peak-times" graphType="bar" />;
}; En qué se resuelve
El nodo tal como aparece en el DocumentModel: el JSON que recibe un renderizador. Sin estilos, sin maquetación.
{
"id": "peak-times",
"kind": "graph",
"mode": "dynamic",
"graphType": "bar",
"placeholder": "Your busiest hours, Monday to Sunday"
} Qué se le pide al endpoint
Resuelto con los mismos datos de ejemplo. Una compilación de vista previa se detiene en el marcador de posición; una compilación en el momento de la petición envía estos.
- 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.
Notas
graphTypevalebarpor defecto en ambos helpers.- En un gráfico dinámico la forma se sigue fijando en local. El endpoint aporta
las cifras y puede sobrescribir
graphTypeycaptionen su respuesta.