# core/formatters/diagram.formatter.ts

> 99 lines of code and 27 definitions.

Tree: GovLab Docs
Language: typescript
Layer: processing
Canonical: https://banes-lab.com/anatomy/docs#file-docs-core-formatters-diagram-formatter-ts
Source text: https://banes-lab.com/source/docs/core/formatters/diagram.formatter.ts.txt

Listed in [core/formatters](https://banes-lab.com/api/source/docs/core/formatters.md), after [core/formatters/coverage.formatter.ts](https://banes-lab.com/source/docs/core/formatters/coverage.formatter.ts.md) and before [core/formatters/diagram.machine.formatter.ts](https://banes-lab.com/source/docs/core/formatters/diagram.machine.formatter.ts.md).

## Definitions

- `accLines` (lexical_declaration, line 23, exported)
- `wrapNode` (lexical_declaration, line 30)
- `subgraphLines` (lexical_declaration, line 59)
- `classMemberLines` (lexical_declaration, line 84)
- `looseNodeLines` (lexical_declaration, line 55)
- `classDefLines` (lexical_declaration, line 78)
- `edgeArrow` (lexical_declaration, line 46)
- `subgraphMembers` (lexical_declaration, line 51)
- `emitGraph` (lexical_declaration, line 99, exported)
- `edgeLine` (lexical_declaration, line 73)
- `INDENT` (lexical_declaration, line 13)
- `NESTED_INDENT` (lexical_declaration, line 14)
- `ELK_HEADER` (lexical_declaration, line 15)
- `DASHED_WEIGHTS` (lexical_declaration, line 16)
- `byId` (lexical_declaration, line 18)
- `byEndpoints` (lexical_declaration, line 19)
- `bySubgraphId` (lexical_declaration, line 20)
- `byClassName` (lexical_declaration, line 21)
- `id` (lexical_declaration, line 31)
- `text` (lexical_declaration, line 32)
- `arrowBody` (lexical_declaration, line 39)
- `body` (lexical_declaration, line 47)
- `caption` (lexical_declaration, line 74)
- `members` (lexical_declaration, line 85)
- `ids` (lexical_declaration, line 94)
- `nodes` (lexical_declaration, line 100, exported)
- `subgraphs` (lexical_declaration, line 101, exported)

## Contained in

- [core/formatters](https://banes-lab.com/anatomy/docs/folder-docs-core-formatters.md)

## Uses

- [core/normalizers/diagram.normalizer.ts](https://banes-lab.com/source/docs/core/normalizers/diagram.normalizer.ts.md)

## Used by

- [core/formatters/diagram.machine.formatter.ts](https://banes-lab.com/source/docs/core/formatters/diagram.machine.formatter.ts.md)
- [core/formatters/diagram.sequence.formatter.ts](https://banes-lab.com/source/docs/core/formatters/diagram.sequence.formatter.ts.md)
- [core/formatters/diagram.structure.formatter.ts](https://banes-lab.com/source/docs/core/formatters/diagram.structure.formatter.ts.md)

## Linked from

- [core/formatters](https://banes-lab.com/anatomy/docs/folder-docs-core-formatters.md)
- [core/normalizers](https://banes-lab.com/anatomy/docs/folder-docs-core-normalizers.md)

## Source

```typescript
import type {
    AccessibleText,
    ClassDef,
    EdgeWeight,
    GraphEdge,
    GraphModel,
    GraphNode,
    GraphSubgraph,
} from "#types/diagram.types";
import { idSanitize, label } from "#core/normalizers/diagram.normalizer";
import { byString } from "#core/selectors/base.selector";

const INDENT = "    ";
const NESTED_INDENT = "        ";
const ELK_HEADER: readonly string[] = ["---", "config:", "  layout: elk", "---"];
const DASHED_WEIGHTS: ReadonlySet<EdgeWeight | undefined> = new Set<EdgeWeight | undefined>(["teardown", "dependency"]);

const byId = byString<GraphNode>((node) => node.id);
const byEndpoints = byString<GraphEdge>((edge) => `${edge.from} ${edge.to} ${edge.weight ?? ""} ${edge.label ?? ""}`);
const bySubgraphId = byString<GraphSubgraph>((subgraph) => subgraph.id);
const byClassName = byString<ClassDef>((def) => def.name);

export const accLines = function accLines(model: AccessibleText): string[] {
    return [
        ...(typeof model.accTitle === "string" ? [`${INDENT}accTitle: ${label(model.accTitle)}`] : []),
        ...(typeof model.accDescr === "string" ? [`${INDENT}accDescr: ${label(model.accDescr)}`] : []),
    ];
};

const wrapNode = function wrapNode(node: GraphNode): string {
    const id = idSanitize(node.id);
    const text = label(node.label);
    if (node.shape === "decision") {
        return `${id}{"${text}"}`;
    }
    return node.shape === "collaborator" ? `${id}[["${text}"]]` : `${id}["${text}"]`;
};

const arrowBody = function arrowBody(weight: EdgeWeight | undefined): string {
    if (weight === "hook") {
        return "==>";
    }
    return DASHED_WEIGHTS.has(weight) ? "-.->" : "-->";
};

const edgeArrow = function edgeArrow(weight: EdgeWeight | undefined, caption: string): string {
    const body = arrowBody(weight);
    return caption.length === 0 ? body : `${body}|${caption}|`;
};

const subgraphMembers = function subgraphMembers(subgraphs: readonly GraphSubgraph[]): Set<string> {
    return new Set(subgraphs.flatMap((subgraph) => subgraph.nodeIds));
};

const looseNodeLines = function looseNodeLines(nodes: readonly GraphNode[], inSubgraph: ReadonlySet<string>): string[] {
    return nodes.filter((node) => !inSubgraph.has(node.id)).map((node) => `${INDENT}${wrapNode(node)}`);
};

const subgraphLines = function subgraphLines(
    subgraphs: readonly GraphSubgraph[],
    nodes: readonly GraphNode[],
): string[] {
    return subgraphs.flatMap((subgraph) => {
        const members = new Set(subgraph.nodeIds);
        return [
            `${INDENT}subgraph ${idSanitize(subgraph.id)}["${label(subgraph.title)}"]`,
            ...nodes.filter((node) => members.has(node.id)).map((node) => `${NESTED_INDENT}${wrapNode(node)}`),
            `${INDENT}end`,
        ];
    });
};

const edgeLine = function edgeLine(edge: GraphEdge): string {
    const caption = typeof edge.label === "string" ? label(edge.label) : "";
    return `${INDENT}${idSanitize(edge.from)} ${edgeArrow(edge.weight, caption)} ${idSanitize(edge.to)}`;
};

const classDefLines = function classDefLines(defs: readonly ClassDef[]): string[] {
    return defs
        .toSorted(byClassName)
        .map((def) => `${INDENT}classDef ${idSanitize(def.name)} stroke:${def.stroke},stroke-width:2px;`);
};

const classMemberLines = function classMemberLines(nodes: readonly GraphNode[]): string[] {
    const members = new Map<string, string[]>();
    for (const node of nodes) {
        if (typeof node.class === "string") {
            members.set(node.class, [...(members.get(node.class) ?? []), idSanitize(node.id)]);
        }
    }
    return [...members.keys()]
        .toSorted((left, right) => left.localeCompare(right))
        .map((name) => {
            const ids = (members.get(name) ?? []).toSorted((left, right) => left.localeCompare(right));
            return `${INDENT}class ${ids.join(",")} ${idSanitize(name)};`;
        });
};

export const emitGraph = function emitGraph(model: GraphModel): string {
    const nodes = model.nodes.toSorted(byId);
    const subgraphs = (model.subgraphs ?? []).toSorted(bySubgraphId);
    return [
        ...ELK_HEADER,
        `${model.kind} ${model.direction}`,
        ...accLines(model),
        ...looseNodeLines(nodes, subgraphMembers(subgraphs)),
        ...subgraphLines(subgraphs, nodes),
        ...model.edges.toSorted(byEndpoints).map(edgeLine),
        ...classDefLines(model.classDefs ?? []),
        ...classMemberLines(nodes),
    ].join("\n");
};
```
