Zum Inhalt springen
Docxcelerate

Nodes

Graph

Echte Word-Diagramme — Balken, Linie, Fläche, Kreis, Ring oder Punkt — als Daten deklariert statt als Bild gezeichnet.

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.

Helper
Graph
Node-Art
graph
Kategorie
Daten
Wird aufgelöst
Both
Kinder
None.
Option Typ Was sie bewirkt
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 erforderlich 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 erforderlich 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.

Deklariert, nicht gezeichnet

Ein Graph-Node enthält Zahlen und eine Form, nie ein Bild:

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"
    />
  );
};

Was in der .docx landet, ist ein echtes Word-Diagramm — ein DrawingML-Chart-Part mit allen Werten darin, samt der Arbeitsmappe, die „Daten bearbeiten“ öffnet. Der Leser kann es auswählen, umgestalten, seinen Typ ändern und seine Zahlen öffnen, und Word zeichnet es als Vektoren in jeder Größe. Nichts davon wird gerastert.

Die Form der Daten

data ist typisiert: Kategorien und Serien, jede Serie mit einem Label, ihren Werten und optional einer Farbe.

interface GraphData {
  categories?: string[];
  series: Array<{ label?: string; values: (number | null)[]; color?: string }>;
}

null ist eine Lücke, keine Null. Ein Monat, der noch nicht abgelesen wurde, ist kein Monat, in dem nichts gemessen wurde — Word lässt den Punkt aus, statt die Linie abstürzen zu lassen.

Farben kommen aus dem Theme (palette.series, eine feste, auf Farbfehlsichtigkeit geprüfte Reihe), damit ein Dokument neu gestaltet werden kann, ohne dass seine Diagramme dazu nicht mehr passen.

Eines geschieht mit den Daten doch: String-Werte laufen durch den Template-Renderer, genau wie Prosa. Ein Label mit {{derived.total}} wird aufgelöst.

Ableiten im State

Laufende Summen, Prozentwerte und Umbasierungen gehören in die data-Funktion, nicht weiter stromaufwärts. Diagramm und der Text daneben werden dann aus einer Quelle berechnet und können sich nicht widersprechen.

Varianten

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`}
    />
  );
};
graph · bar Open ↗
Wozu es aufgelöst wird

Der Node, wie er im DocumentModel erscheint: das JSON, das ein Renderer bekommt. Kein Styling, kein Layout.

{
  "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"
    />
  );
};
graph · line Open ↗
Wozu es aufgelöst wird

Der Node, wie er im DocumentModel erscheint: das JSON, das ein Renderer bekommt. Kein Styling, kein Layout.

{
  "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"
    />
  );
};
graph · pie Open ↗
Wozu es aufgelöst wird

Der Node, wie er im DocumentModel erscheint: das JSON, das ein Renderer bekommt. Kein Styling, kein Layout.

{
  "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"
    />
  );
};
graph · stacked to a share Open ↗
Wozu es aufgelöst wird

Der Node, wie er im DocumentModel erscheint: das JSON, das ein Renderer bekommt. Kein Styling, kein Layout.

{
  "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"
    />
  );
};
graph · radar Open ↗
Wozu es aufgelöst wird

Der Node, wie er im DocumentModel erscheint: das JSON, das ein Renderer bekommt. Kein Styling, kein Layout.

{
  "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" />;
};
graph · dynamic Open ↗
Wozu es aufgelöst wird

Der Node, wie er im DocumentModel erscheint: das JSON, das ein Renderer bekommt. Kein Styling, kein Layout.

{
  "id": "peak-times",
  "kind": "graph",
  "mode": "dynamic",
  "graphType": "bar",
  "placeholder": "Your busiest hours, Monday to Sunday"
}
Was der Endpoint gefragt wird

Gegen dieselben Beispieldaten aufgelöst. Ein Vorschau-Build hält beim Platzhalter an; ein Build zur Anfragezeit schickt diese mit.

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.

Anmerkungen

  • graphType steht bei beiden Helpern standardmäßig auf bar.
  • Bei einem dynamischen Graph liegt die Form weiterhin lokal fest. Der Endpoint liefert die Zahlen und darf graphType und caption in seiner Antwort überschreiben.

Diese Seite auf GitHub bearbeiten ↗ Diese Seite als Markdown lesen