# core/renderers/walk.renderer.ts

> 137 lines of code and 63 definitions.

Tree: GovLab Patterns
Language: typescript
Layer: product
Canonical: https://banes-lab.com/anatomy/patterns#file-patterns-core-renderers-walk-renderer-ts
Source text: https://banes-lab.com/source/patterns/core/renderers/walk.renderer.ts.txt

Listed in [core/renderers](https://banes-lab.com/api/source/patterns/core/renderers.md), after [core/renderers/report.renderer.ts](https://banes-lab.com/source/patterns/core/renderers/report.renderer.ts.md).

## Definitions

- `round` (lexical_declaration, line 49)
- `svgDoc` (lexical_declaration, line 129)
- `warnMark` (lexical_declaration, line 80)
- `pair` (lexical_declaration, line 53)
- `renderFlow` (lexical_declaration, line 120)
- `renderHexGrid` (lexical_declaration, line 133, exported)
- `flatTopPixel` (lexical_declaration, line 57)
- `hexCorners` (lexical_declaration, line 61)
- `renderCell` (lexical_declaration, line 89)
- `PRECISION` (lexical_declaration, line 21)
- `HEX_SIZE` (lexical_declaration, line 22)
- `HEX_SIDES` (lexical_declaration, line 23)
- `HEX_ANGLE_STEP` (lexical_declaration, line 24)
- `DOUBLE` (lexical_declaration, line 25)
- `HALF_TURN` (lexical_declaration, line 26)
- `DEG_TO_RAD` (lexical_declaration, line 27)
- `COL_RATIO` (lexical_declaration, line 28)
- `SQRT3` (lexical_declaration, line 29)
- `ARROW_EVERY` (lexical_declaration, line 30)
- `ARROW_LEN` (lexical_declaration, line 31)
- `ARROW_WIDE` (lexical_declaration, line 32)
- `MARGIN` (lexical_declaration, line 33)
- `WARN_SCALE` (lexical_declaration, line 34)
- `WARN_WIDE` (lexical_declaration, line 35)
- `EMPTY_W` (lexical_declaration, line 36)
- `EMPTY_H` (lexical_declaration, line 37)
- `EMPTY_CX` (lexical_declaration, line 38)
- `EMPTY_CY` (lexical_declaration, line 39)
- `SVG_NS` (lexical_declaration, line 40)
- `Bounds` (interface_declaration, line 42)
- `angle` (lexical_declaration, line 63)
- `boundsOf` (lexical_declaration, line 68)
- `xs` (lexical_declaration, line 69)
- `ys` (lexical_declaration, line 70)
- `pad` (lexical_declaration, line 71)
- `size` (lexical_declaration, line 81)
- `severity` (lexical_declaration, line 95)
- `classes` (lexical_declaration, line 97)
- `polygon` (lexical_declaration, line 98)
- `arrowAt` (lexical_declaration, line 102)
- `dx` (lexical_declaration, line 103)
- `dy` (lexical_declaration, line 104)
- `len` (lexical_declaration, line 105)
- `[ux, uy]` (lexical_declaration, line 106)
- `mid` (lexical_declaration, line 107)
- `reach` (lexical_declaration, line 108)
- `base` (lexical_declaration, line 109)
- `tip` (lexical_declaration, line 110)
- `left` (lexical_declaration, line 111)
- `right` (lexical_declaration, line 112)
- `pointText` (lexical_declaration, line 116)
- `line` (lexical_declaration, line 121)
- `arrows` (lexical_declaration, line 122)
- `from` (lexical_declaration, line 123)
- `title` (lexical_declaration, line 134, exported)
- `cells` (lexical_declaration, line 135, exported)
- `note` (lexical_declaration, line 137, exported)
- `label` (lexical_declaration, line 138, exported)
- `bounds` (lexical_declaration, line 141, exported)
- `centers` (lexical_declaration, line 142, exported)
- `pixel` (lexical_declaration, line 143, exported)
- `flagged` (lexical_declaration, line 146, exported)
- `body` (lexical_declaration, line 147, exported)

## Contained in

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

## Uses

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

## Linked from

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

## Source

```typescript
import type { Axial, Cell, HexGridOptions, Point, WalkNode } from "#types/walk.types";
import { DEFAULT_WALK_TITLE, EMPTY_WALK_NOTE, WARN_MARK } from "#configuration/strings/walk.strings";
import {
    HALF,
    HEX_ARROW_CLASS,
    HEX_CLASS,
    HEX_EMPTY_CLASS,
    HEX_FLAG_PREFIX,
    HEX_PATH_CLASS,
    HEX_REF_ATTRIBUTE,
    HEX_STATE_PREFIX,
    HEX_WARN_CLASS,
    HEX_WARN_PREFIX,
    HEX_WARN_TEXT_CLASS,
} from "#configuration/constants/walk.constants";
import { refOf, severityOf } from "#core/converters/walk.converter";
import { escapeXml } from "#core/formatters/markup.formatter";
import { roundTo } from "#core/normalizers/math.normalizer";
import { walkGrid } from "#core/resolvers/walk.resolver";

const PRECISION = 2;
const HEX_SIZE = 15;
const HEX_SIDES = 6;
const HEX_ANGLE_STEP = 60;
const DOUBLE = 2;
const HALF_TURN = 180;
const DEG_TO_RAD = Math.PI / HALF_TURN;
const COL_RATIO = 1.5;
const SQRT3 = Math.sqrt(3);
const ARROW_EVERY = 6;
const ARROW_LEN = 6;
const ARROW_WIDE = 3.2;
const MARGIN = 32;
const WARN_SCALE = 0.66;
const WARN_WIDE = 0.86;
const EMPTY_W = 360;
const EMPTY_H = 120;
const EMPTY_CX = 180;
const EMPTY_CY = 64;
const SVG_NS = "http://www.w3.org/2000/svg";

interface Bounds {
    minX: number;
    minY: number;
    width: number;
    height: number;
}

const round = function round(value: number): number {
    return roundTo(value, PRECISION);
};

const pair = function pair(x: number, y: number): string {
    return `${round(x)},${round(y)}`;
};

const flatTopPixel = function flatTopPixel(cell: Axial): Point {
    return { x: round(HEX_SIZE * COL_RATIO * cell.q), y: round(HEX_SIZE * SQRT3 * (cell.r + cell.q * HALF)) };
};

const hexCorners = function hexCorners(center: Point): string {
    return Array.from({ length: HEX_SIDES }, (_, corner) => {
        const angle = DEG_TO_RAD * HEX_ANGLE_STEP * corner;
        return pair(center.x + HEX_SIZE * Math.cos(angle), center.y + HEX_SIZE * Math.sin(angle));
    }).join(" ");
};

const boundsOf = function boundsOf(centers: readonly Point[]): Bounds {
    const xs = centers.map((point) => point.x);
    const ys = centers.map((point) => point.y);
    const pad = HEX_SIZE + MARGIN;
    return {
        height: Math.max(...ys) - Math.min(...ys) + pad * DOUBLE,
        minX: Math.min(...xs) - HEX_SIZE - MARGIN,
        minY: Math.min(...ys) - HEX_SIZE - MARGIN,
        width: Math.max(...xs) - Math.min(...xs) + pad * DOUBLE,
    };
};

const warnMark = function warnMark(center: Point, severity: string): string {
    const size = HEX_SIZE * WARN_SCALE;
    const tip = pair(center.x, center.y - size);
    const left = pair(center.x - size * WARN_WIDE, center.y + size * HALF);
    const right = pair(center.x + size * WARN_WIDE, center.y + size * HALF);
    const polygon = `<polygon class="${HEX_WARN_CLASS} ${HEX_WARN_PREFIX}${escapeXml(severity)}" points="${tip} ${left} ${right}"/>`;
    return `${polygon}<text class="${HEX_WARN_TEXT_CLASS}" x="${round(center.x)}" y="${round(center.y + size * HALF)}" font-size="${round(size)}" text-anchor="middle">${WARN_MARK}</text>`;
};

const renderCell = function renderCell(
    cell: Cell,
    index: number,
    center: Point,
    flagged: ReadonlyMap<string, string>,
): string {
    const severity = severityOf(cell, flagged);
    const base = `${HEX_CLASS} ${HEX_STATE_PREFIX}${cell.state}`;
    const classes = severity === undefined ? base : `${base} ${HEX_FLAG_PREFIX}${severity}`;
    const polygon = `<polygon class="${escapeXml(classes)}" ${HEX_REF_ATTRIBUTE}="${refOf(index)}" points="${hexCorners(center)}"/>`;
    return severity === undefined ? polygon : polygon + warnMark(center, severity);
};

const arrowAt = function arrowAt(from: Point, to: Point): string {
    const dx = to.x - from.x;
    const dy = to.y - from.y;
    const len = Math.hypot(dx, dy) || 1;
    const [ux, uy] = [dx / len, dy / len];
    const mid: Point = { x: (from.x + to.x) * HALF, y: (from.y + to.y) * HALF };
    const reach = ARROW_LEN * HALF;
    const base: Point = { x: mid.x - ux * reach, y: mid.y - uy * reach };
    const tip = pair(mid.x + ux * reach, mid.y + uy * reach);
    const left = pair(base.x - uy * ARROW_WIDE, base.y + ux * ARROW_WIDE);
    const right = pair(base.x + uy * ARROW_WIDE, base.y - ux * ARROW_WIDE);
    return `<polygon class="${HEX_ARROW_CLASS}" points="${tip} ${left} ${right}"/>`;
};

const pointText = function pointText(point: Point): string {
    return `${point.x},${point.y}`;
};

const renderFlow = function renderFlow(centers: readonly Point[]): string {
    const line = `<polyline class="${HEX_PATH_CLASS}" points="${centers.map(pointText).join(" ")}"/>`;
    const arrows = centers.flatMap((to, index) => {
        const from = centers[index - 1];
        return index >= ARROW_EVERY && index % ARROW_EVERY === 0 && from !== undefined ? [arrowAt(from, to)] : [];
    });
    return line + arrows.join("");
};

const svgDoc = function svgDoc(width: number, height: number, body: string): string {
    return `<svg xmlns="${SVG_NS}" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}" role="img">${body}</svg>`;
};

export const renderHexGrid = function renderHexGrid(nodes: readonly WalkNode[], options: HexGridOptions = {}): string {
    const title = options.title ?? DEFAULT_WALK_TITLE;
    const cells = walkGrid(nodes);
    if (cells.length === 0) {
        const note = escapeXml(title + EMPTY_WALK_NOTE);
        const label = `<text class="${HEX_EMPTY_CLASS}" x="${EMPTY_CX}" y="${EMPTY_CY}" text-anchor="middle">${note}</text>`;
        return svgDoc(EMPTY_W, EMPTY_H, label);
    }
    const bounds = boundsOf(cells.map((cell) => flatTopPixel(cell.at)));
    const centers = cells.map((cell) => {
        const pixel = flatTopPixel(cell.at);
        return { x: round(pixel.x - bounds.minX), y: round(pixel.y - bounds.minY) };
    });
    const flagged = options.flagged ?? new Map<string, string>();
    const body = cells
        .map((cell, index) => renderCell(cell, index, centers[index] ?? { x: 0, y: 0 }, flagged))
        .join("");
    return svgDoc(round(bounds.width), round(bounds.height), body + renderFlow(centers));
};
```
