Zum Hauptinhalt springen
Rezept Nummer 162

Rezepte · Kapitel 1 · Seite und Raster

Ein Organigramm und ein Fotoraster aus Modulen

Ein Bürgerwegweiser aus Modulen: ein Organigramm, dessen Linien die Vorlage zieht, und Fotoraster, die je Gebäude eine Karte wiederholen.

Auf dieser Seite
Gattung
Berichte · Kataloge
Ausgabe
Canvas · PDF
Postext
Getestet mit Postext 1.48.1
Erfordert ≥ 1.48.1 · postext-pdf ≥ 1.48.1
Lizenz
Aktualisiert am 20. Okt. 2026
Code MIT · Text CC BY 4.0

S. 1–2 von 2

  • Englisches Beispiel: noch keine deutsche Ausgabe
  • Endformat 210 × 297 mm
  • 1 Spalte
  • Source Serif 4 10,5/15
  • Public Sans
  • 2 Seiten
  • Stufe
  • Postext 1.48.1
  • In 88 ms gesetzt
  • 174 Zeilen Code

Kurz gesagt

Zwei Seiten aus dem Wegweiser einer Gemeinde: wer wofür zuständig ist, als Schaubild aus Kästen mit Linien, und wo jede Stelle zu finden ist, in Bildreihen. Die Seiten bestehen aus wenigen Bausteinen; das Markdown sagt, wie viele.

Was Sie setzen

Eine Doppelseite aus dem Bürgerwegweiser von Valdeloma, einer erfundenen Gemeinde mit 8.400 Einwohnern. Die linke Seite ist das Organigramm der Gemeinde: Bürgermeisteramt und Gemeinderat in einem roten Kasten, vier Ämter in Grün und unter jedem Amt seine Stellen in hellen Kästen, verbunden durch grüne Linien mit einem Punkt oder Pfeil am Ende. Die rechte Seite zeigt, wo die Stellen sind: zwei Tafeln mit je drei Aquarellen, unter jedem Bild Name, Adresse und Öffnungszeiten, dazu ein Kasten für das Bürgerbüro. Nichts ist von Hand gezeichnet. Die Vorlage enthält drei Module (ein Amt, eine Fototafel und einen Hinweis), und jede Seite stapelt so viele davon, wie ihr Markdown enthält; die Linien folgen den Kästen, wohin der Text sie auch schiebt.

Dieses Rezept beantwortet

  • Wie baue ich ein Organigramm und ein Fotoraster aus wiederverwendbaren Modulen, deren Linien die Vorlage zieht?

Die kurze Antwort

script.js · Zeilen 40–72im vollständigen Code
// The page template draws the head of the chart and a group that takes the page's modules;
// the module draws one area: its box, a grid of its services and the lines between them.
const area = {
  id: 'area', role: 'module',
  frames: [
    { name: 'concejalia', style: 'area', width: mm(84), placement: at('container', 'top-left') },
    { name: 'servicio', style: 'servicio' }, // repeated: one per :::frame{slot="servicio"}
  ],
  groups: [{ name: 'servicios', kind: 'grid', columns: 3, columnGap: mm(4), rowGap: mm(4),
    repeat: { frames: ['servicio'] }, // rowAlign 'stretch': a row's boxes are one height
    placement: { ...at('#concejalia', 'below', 0, 7), size: { width: 'fill', height: 'fill' } } }],
  decoration: { elements: [
    // From the area down to each of its services: a tree with a bar over the row.
    line({ id: 'rama', routing: 'tree', from: { to: '#concejalia', side: 'bottom' },
      to: { to: '#servicio[*]', side: 'top' }, endCap: 'dot', capSize: mm(1.8) }),
    // From the mayor's box, a frame of the page, to this area: the spine and its branch.
    line({ id: 'tronco', routing: 'elbow', width: pt(1.8),
      from: { to: '#alcaldia', side: 'bottom', at: 0.06 }, to: { to: '#concejalia', side: 'left' },
      endCap: 'arrow', capSize: mm(2.6) }),
  ] },
};
const chart = {
  id: 'organigrama',
  frames: [
    { name: 'titulo', takes: 'h1', width: mm(W), placement: at('container', 'top-left') },
    { name: 'entradilla', flow: true, width: mm(W) },
    { name: 'alcaldia', style: 'alcaldia', width: mm(96) },
  ],
  groups: [{ name: 'cabeza', kind: 'stack', gap: mm(6),
    frames: ['titulo', 'entradilla', 'alcaldia'] },
    { name: 'areas', modules: true, gap: mm(6), placement: { ...at('#alcaldia', 'below', SPINE, 9),
      size: { width: mm(W - SPINE), height: 'fill' } } }],
};

Zutaten

Schrift
Public Sans, Source Serif 4 (SIL OFL 1.1)
Dateien
  • biblioteca-900.jpg
  • centro-mayores-900.jpg
  • mercado-900.jpg
  • musica-900.jpg
  • piscina-900.jpg
  • polideportivo-900.jpg
  • The reading room of a town library, in watercolour. (Generated With Diffusion Models, eigener Text)
  • Children with violins and a piano at a music school, in watercolour. (Generated With Diffusion Models, eigener Text)
  • Older people playing cards and painting at a day centre, in watercolour. (Generated With Diffusion Models, eigener Text)
  • A covered pool under a timber roof, in watercolour. (Generated With Diffusion Models, eigener Text)
  • Children playing basketball in a sports hall, in watercolour. (Generated With Diffusion Models, eigener Text)
  • A Saturday market under the arcades of a town square, in watercolour. (Generated With Diffusion Models, eigener Text)

Zubereitung

#1 · Ein Modul je Amt

script.js · Zeilen 40–72im vollständigen Code
// The page template draws the head of the chart and a group that takes the page's modules;
// the module draws one area: its box, a grid of its services and the lines between them.
const area = {
  id: 'area', role: 'module',
  frames: [
    { name: 'concejalia', style: 'area', width: mm(84), placement: at('container', 'top-left') },
    { name: 'servicio', style: 'servicio' }, // repeated: one per :::frame{slot="servicio"}
  ],
  groups: [{ name: 'servicios', kind: 'grid', columns: 3, columnGap: mm(4), rowGap: mm(4),
    repeat: { frames: ['servicio'] }, // rowAlign 'stretch': a row's boxes are one height
    placement: { ...at('#concejalia', 'below', 0, 7), size: { width: 'fill', height: 'fill' } } }],
  decoration: { elements: [
    // From the area down to each of its services: a tree with a bar over the row.
    line({ id: 'rama', routing: 'tree', from: { to: '#concejalia', side: 'bottom' },
      to: { to: '#servicio[*]', side: 'top' }, endCap: 'dot', capSize: mm(1.8) }),
    // From the mayor's box, a frame of the page, to this area: the spine and its branch.
    line({ id: 'tronco', routing: 'elbow', width: pt(1.8),
      from: { to: '#alcaldia', side: 'bottom', at: 0.06 }, to: { to: '#concejalia', side: 'left' },
      endCap: 'arrow', capSize: mm(2.6) }),
  ] },
};
const chart = {
  id: 'organigrama',
  frames: [
    { name: 'titulo', takes: 'h1', width: mm(W), placement: at('container', 'top-left') },
    { name: 'entradilla', flow: true, width: mm(W) },
    { name: 'alcaldia', style: 'alcaldia', width: mm(96) },
  ],
  groups: [{ name: 'cabeza', kind: 'stack', gap: mm(6),
    frames: ['titulo', 'entradilla', 'alcaldia'] },
    { name: 'areas', modules: true, gap: mm(6), placement: { ...at('#alcaldia', 'below', SPINE, 9),
      size: { width: mm(W - SPINE), height: 'fill' } } }],
};

Eine Vorlage mit role: 'module' ist ein Modul: ein paar Rahmen, Gruppen und Dekor, gedacht zum Wiederverwenden. Die Organigrammseite hat eine Gruppe mit modules: true, und jedes :::module{template="area"} ihres Markdowns setzt ein weiteres Amt 6 mm unter das vorige. Die Rahmen des Moduls tragen den Namen der Instanz, area[1].concejalia, area[2].concejalia …, darum stehen seine beiden Verbinder nur einmal da: rama, ein Baum (tree) vom Fuß des Amts zu jeder Stelle seines Rasters (#servicio[*]), und tronco, ein Winkel (elbow) von einem Punkt nahe der linken Kante des Bürgermeisterkastens (at: 0.06) zur linken Seite des Amts. #alcaldia ist ein Rahmen der Seite, nicht des Moduls: Das Modul behält dieses Ende, und das Layout findet es auf der Seite, sodass die vier Winkel ihr erstes Stück teilen und sich wie ein einziger Stamm mit einem Pfeil in jedes Amt lesen. Die Stellen sind ein dreispaltiges Raster, das einen Rahmen servicio wiederholt; ein Amt mit zwei Stellen zeichnet also zwei Kästen, und sein Baum hat zwei Äste.

#2 · Tafeln aus Fotokarten

script.js · Zeilen 76–105im vollständigen Code
const gallery = {
  id: 'galeria', role: 'module',
  frames: [
    { name: 'cabecera', takes: 'h2', placement: at('container', 'top-left', 4, 4) },
    { name: 'foto', style: 'marco' }, // the photo of a card
    { name: 'pie', style: 'ficha', placement: at('#foto', 'below', 0, 2.5) }, // and its words
  ],
  groups: [{ name: 'fotos', kind: 'grid', columns: 3, columnGap: mm(5), rowGap: mm(6),
    rowAlign: 'start', repeat: { frames: ['foto', 'pie'] },
    placement: { ...at('#cabecera', 'below', 0, 3), size: { width: mm(W - 8), height: 'fill' } } }],
  // The module's own ground: a tint as large as the module, whatever its rows.
  decoration: { elements: [{ kind: 'box', id: 'suelo', decorative: true,
    placement: { ...at('container', 'top-left'), size: { width: 'fill', height: 'fill' } },
    style: { backgroundColor: col('tint'), borderRadius: mm(3) } }] },
};
// A notice box as a module of its own: one frame whose tab grows out of the box (joined).
const notice = { id: 'aviso', role: 'module', frames: [{ name: 'texto', style: 'aviso',
  placement: at('container', 'top-left', 0, 7) }] };
const places = {
  id: 'centros',
  frames: [
    { name: 'titulo', takes: 'h1', width: mm(W), placement: at('container', 'top-left') },
    { name: 'entradilla', flow: true, width: mm(W) },
    { name: 'colofon', style: 'colofon', optional: true, width: mm(W),
      placement: at('container', 'bottom-left', 0, 8) },
  ],
  groups: [{ name: 'cabeza', kind: 'stack', gap: mm(5), frames: ['titulo', 'entradilla'] },
    { name: 'galerias', modules: true, gap: mm(7), placement: { ...at('#entradilla', 'below', 0, 7),
      size: { width: mm(W), height: 'fill' } } }],
};

Auch eine Tafel ist ein Modul: ein Kopfrahmen, der die ##-Überschrift des Moduls aufnimmt, und ein Raster, das einen Satz aus zwei Rahmen wiederholt, das Foto und den Text darunter. Jedes :::frame{slot="foto"} ist eine Karte, und das :::frame{slot="pie"} darin geht an den Text dieser Karte. Der getönte Grund ist Dekor des Moduls, ein Kasten so groß wie das Modul, und wächst deshalb mit den Reihen und der längsten Bildunterschrift. Das Bürgerbüro ist ein drittes Modul, ein Kasten, dessen Reiter joined ist: Reiter und Kasten bilden eine Kontur mit einer Füllung.

#3 · Bilder in ihrer Druckgröße

script.js · Zeilen 346–372im vollständigen Code
const ART = {
  biblioteca: t({ es: 'La sala de lectura de la biblioteca, con estanterías de madera y vecinos '
      + 'de todas las edades leyendo en mesas largas.',
    en: 'The library reading room, with wooden shelves and neighbours of every age reading at '
      + 'long tables.' }),
  musica: t({ es: 'Un aula de la escuela de música: niños con violines, una profesora y una niña '
      + 'al piano.',
    en: 'A room of the music school: children with violins, a teacher and a girl at the piano.' }),
  'centro-mayores': t({ es: 'El centro de día: mayores jugando a las cartas y pintando junto a '
      + 'una puerta al jardín.',
    en: 'The day centre: older people playing cards and painting beside a door to the garden.' }),
  piscina: t({ es: 'La piscina cubierta bajo un techo de madera, con nadadores en las calles y un '
      + 'socorrista.',
    en: 'The covered pool under a timber roof, with swimmers in the lanes and a lifeguard.' }),
  polideportivo: t({ es: 'La pista del polideportivo, con niños jugando al baloncesto y gradas a '
      + 'un lado.',
    en: 'The sports hall court, with children playing basketball and stands along one side.' }),
  mercado: t({ es: 'El mercado de los sábados en los soportales de la plaza, con el ayuntamiento '
      + 'al fondo.',
    en: 'The Saturday market under the arcades of the square, with the town hall behind.' }),
};
const resources = await Promise.all(Object.entries(ART).map(async ([id, altText]) => {
  const fileId = `${id}-900.jpg`;
  await loadImage(fileId, asset(fileId));
  return { id, typeId: 'foto', kind: 'bitmap', caption: '', altText, createdAt: 0, updatedAt: 0,
    bitmap: { fileId, format: 'jpeg', width: 900, height: 600, resolution: 400 } };
}));

Die sechs Aquarelle haben 900 × 600 px bei angegebenen 400 ppi, sind also 57 mm breit, etwa so breit wie eine Rasterspalte; das Raster vergrößert sie nie. Ihr Alternativtext steht hier im Skript, weil die Bilder in beiden Ausgaben dieselben sind; eine Bildunterschrift, die sich mit der Sprache ändert, wäre eine ::caption-Zeile im Markdown.

#4 · Ein Grün für Linien und Kästen

script.js · Zeilen 15–28im vollständigen Code
const palette = {
  ink: '#1f2b2a', // text
  green: '#1d6b57', // the lines of the chart, the areas' boxes, the headings
  mayor: '#a8452e', // the box at the head of the chart
  tint: '#e6f1ec', // the services' boxes and the galleries' ground
  rule: '#b9cbc3', // the hairline round a service
  muted: '#5c6b67', // running heads and the colophon
  paper: '#ffffff',
};
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = [
  ...Object.entries(palette).map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })),
  { id: 'main-color', name: 'green (defaults)', value: { hex: palette.green, model: 'hex' } },
];

Die Verbinder, die Amtskästen und die Köpfe der Tafeln verwenden green, einen Eintrag der Palette; eine Gemeinde mit einer anderen Farbe ändert also einen einzigen Hexwert. Der Kasten der Gemeindespitze trägt die einzige warme Farbe der Doppelseite, dort, wohin der Blick zuerst fällt.

Das ganze Rezept

Sandbox
// ═══ Postext Cookbook · Nº 162 · An org chart and a photo grid built from modules ═══
// https://postext.dev/en/cookbook/org-chart-from-modules
// Code: MIT · Text: original (CC BY 4.0) · Pictures: generated with diffusion models
// Fonts: Public Sans, Source Serif 4 (SIL OFL 1.1) · Needs postext ≥ 1.48.1
import {
  buildDocument, withLoadedFonts, renderPageToCanvas, registerResourceImage,
} from 'https://esm.sh/postext';
import { renderToPdf, decompressWoff2 } from 'https://esm.sh/postext-pdf';

const LANG = 'en'; // @lang: the language of the sample document ('es' | 'en')
const RECIPE = 'org-chart-from-modules';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: an ink, the town's green for the lines and the areas, a warm mayor's box
const palette = {
  ink: '#1f2b2a', // text
  green: '#1d6b57', // the lines of the chart, the areas' boxes, the headings
  mayor: '#a8452e', // the box at the head of the chart
  tint: '#e6f1ec', // the services' boxes and the galleries' ground
  rule: '#b9cbc3', // the hairline round a service
  muted: '#5c6b67', // running heads and the colophon
  paper: '#ffffff',
};
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = [
  ...Object.entries(palette).map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })),
  { id: 'main-color', name: 'green (defaults)', value: { hex: palette.green, model: 'hex' } },
];
// #endregion

const SANS = 'Public Sans', SERIF = 'Source Serif 4';
const PAGE = { w: 210, h: 297, top: 22, bottom: 22, inner: 18, outer: 15 }; // mm, mirrored
const W = PAGE.w - PAGE.inner - PAGE.outer; // the type area's width: 177 mm
const SPINE = 12; // mm: the room on the left of the areas where the chart's spine runs down
const at = (to, edge, x = 0, y = 0) => ({ anchor: { to, edge }, offset: { x: mm(x), y: mm(y) } });
const line = (more) => ({ kind: 'connector', color: col('green'), width: pt(1.2), radius: mm(2.5),
  ...more });

// #region answer: a module per area of the town hall, stacked by the page's modules group
// The page template draws the head of the chart and a group that takes the page's modules;
// the module draws one area: its box, a grid of its services and the lines between them.
const area = {
  id: 'area', role: 'module',
  frames: [
    { name: 'concejalia', style: 'area', width: mm(84), placement: at('container', 'top-left') },
    { name: 'servicio', style: 'servicio' }, // repeated: one per :::frame{slot="servicio"}
  ],
  groups: [{ name: 'servicios', kind: 'grid', columns: 3, columnGap: mm(4), rowGap: mm(4),

Das zusammengesetzte script.js läuft unverändert: Fügen Sie es in das Modulskript einer beliebigen Seite ein, oder öffnen Sie das Rezept auf CodePen. Ordner des Rezepts auf GitHub ↗ (öffnet sich in einem neuen Tab)

Varianten

#Zwei Spalten für die Stellen

Mit columns: 2 stehen die Stellen jedes Amts zu zweit in einer Reihe; der Baum zieht seinen Querbalken nur über die erste Reihe, nehmen Sie das also für Ämter mit zwei oder vier Stellen.

-  groups: [{ name: 'servicios', kind: 'grid', columns: 3, columnGap: mm(4), rowGap: mm(4),
+  groups: [{ name: 'servicios', kind: 'grid', columns: 2, columnGap: mm(4), rowGap: mm(4),

#Die Ecken des Stamms abrunden

radius rundet die Knicke eines Winkels und eines Baums ab: Die Hilfsfunktion line() gibt jedem Verbinder 2,5 mm; mit radius: mm(0) wird das Organigramm eckig.

Häufige Fehler

  • Ein Rasterelement ist ein eigener Bereich: Der Text einer Fotokarte gehört nur dann zu ihr, wenn sein :::frame{slot="pie"} innerhalb des :::frame{slot="foto"} der Karte steht.
  • Ein Verbinder eines Moduls, der einen Rahmen der Seite nennt, wird nur auf Seiten gezeichnet, die diesen Rahmen haben; auf anderen fällt er weg, und das Umbruchprotokoll meldet connectorDropped.
  • Ein verbundener Reiter sitzt auf der Kante des Kastens und übernimmt dessen Füllung: Geben Sie seinem Text eine dunkle color, denn eine weiße Beschriftung auf einem hellen Kasten ist nicht lesbar.

Nachweise

Text
  • A guide to the council services of Valdeloma, an invented town, written for this recipe in Spanish and English · Postext Cookbook · CC BY 4.0
Bilder
  • The reading room of a town library, in watercolour. · Generated With Diffusion Models · eigener Text
  • Children with violins and a piano at a music school, in watercolour. · Generated With Diffusion Models · eigener Text
  • Older people playing cards and painting at a day centre, in watercolour. · Generated With Diffusion Models · eigener Text
  • A covered pool under a timber roof, in watercolour. · Generated With Diffusion Models · eigener Text
  • Children playing basketball in a sports hall, in watercolour. · Generated With Diffusion Models · eigener Text
  • A Saturday market under the arcades of a town square, in watercolour. · Generated With Diffusion Models · eigener Text
Schriften
Public Sans (SIL OFL 1.1) · Source Serif 4 (SIL OFL 1.1)
SandboxPDF