# Pivograph documentation > Map an organization's projects and their connections as a graph, from one JSON document. > Source: https://github.com/ecrou-exact/project-graph — App: https://ecrou-exact.github.io/project-graph/ — Guide: https://ecrou-exact.github.io/project-graph/docs/guide.html ## Overview Pivograph draws an organization as a graph: its projects, platforms, data sources and partners are **nodes**, and the ways they depend on, feed or maintain each other are **edges**. The whole map is one JSON document. You can write it by hand, generate it (an AI agent is a good fit), import it from an organization's published [Open Contributions Descriptor](https://ecrou-exact.github.io/project-graph/docs/guide.html#open-contributions-descriptor), then refine it in the browser. Rendering and interaction come from [Pivotick](https://pivotick.github.io/Pivotick/), a graph library by CIRCL. Pivograph adds a document format, forms to edit every node and edge, types, tags, GitHub details, filters, and an embeddable read-only viewer. - **App**: [ecrou-exact.github.io/project-graph](https://ecrou-exact.github.io/project-graph/) — open the bundled Rulezet example, or your own file. - **Source**: [github.com/ecrou-exact/project-graph](https://github.com/ecrou-exact/project-graph) (MIT). - **For AI agents**: [`llms.txt`](https://ecrou-exact.github.io/project-graph/llms.txt) and the full text of this page as [`llms-full.txt`](https://ecrou-exact.github.io/project-graph/llms-full.txt); the [JSON schema](https://ecrou-exact.github.io/project-graph/docs/pivograph.schema.json); [examples](https://ecrou-exact.github.io/project-graph/docs/guide.html#examples). What a map contains: | Part | What it holds | |---|---| | `nodes` | The things being mapped. Each has an id, a label, and optionally a type, a description, links, a GitHub repository, tags, extra details and its own look. | | `edges` | Directed connections between two nodes: a label that says how they are related, an arrow direction, a type and a look. | | `nodeTypes`, `edgeTypes` | Shared defaults. A node of type `project` takes the type's colour, shape, image and label style unless it sets its own. | | `tags` | How each `#tag` is drawn: colour and icon. Tags appear as small pills under nodes and can filter the graph. | | `meta` | Title, description and a few document settings. | ## Quick start **In the browser.** Open the [app](https://ecrou-exact.github.io/project-graph/). It starts with the Rulezet example, locked (it can be explored and exported, not edited). The *Graph* menu opens a map (or drop a `.json` file on the page), imports an organization's Open Contributions Descriptor, or starts a new graph from ready-made types. *Add* creates nodes and edges. *Export* saves the map as JSON, or as a PDF or Markdown report with a picture of the graph. Changes are also kept in the browser between visits. **From the command line.** ```bash git clone https://github.com/ecrou-exact/project-graph.git cd project-graph npm install npm run dev # http://localhost:5173 npm test # tests for the document format npm run build # static site in dist/ ``` **The smallest valid document** has one node: ```json { "nodes": [{ "id": "rulezet" }] } ``` Everything else is optional. A missing label defaults to the id; missing types, tags and styles fall back to built-in defaults. ## Mapping an organization This section is a procedure. It is written for an AI agent (or a person) asked to *"map organization X with Pivograph"*, and it ends with a document that loads without errors and reads well. Follow the steps in order. ### 1. Collect the sources Gather facts before writing anything, and keep the URL of every fact you use. 1. **The organization's Open Contributions Descriptor**, if it publishes one: `https:///.well-known/open-contributions.json`. It lists projects, licenses, repositories, open data, standards and relationships in a structured form. If it exists, load it in the app with *Graph → Import an organization…* first: it gives you a correct skeleton in one step (see [Open Contributions Descriptor](https://ecrou-exact.github.io/project-graph/docs/guide.html#open-contributions-descriptor)). 2. **The code hosting organization**: `https://github.com/` (or GitLab). List the repositories that are real projects: skip forks, archived experiments and tooling repositories unless they matter to the map. For each project, note the repository description, license, main language and topics. The GitHub API gives them without authentication (`https://api.github.com/repos//`), at up to 60 requests per hour. 3. **The website and documentation** of the organization and of each project: what the project is for, who uses it, which other projects it talks to. 4. **The code itself**, for relationships: API clients, configuration files, import/export modules, webhooks, submodules, dependency manifests. A relationship you can point to in code is worth more than one inferred from a website. ### 2. Decide what is a node A node is anything with its own identity that a reader would want to locate on the map: - the organization itself and the teams or partner organizations that matter; - software projects, platforms and services (usually the bulk of the map); - datasets, feeds and published standards; - important external things the projects depend on or serve (another organization's platform, a data source, a standards body). Keep the map about **one level of granularity**: if one node is "MISP", its sub-modules are not separate nodes unless the map is about MISP's internals. When in doubt, a detail belongs in a node's `description`, `details` or `tags`, not in a new node. ### 3. Decide what is an edge An edge says **how two nodes are related**, and its arrow says **which way the relationship goes**. Use one convention for the whole map and write it in `meta.description`. The most readable convention is *"the arrow goes from the one who acts to the one acted upon"*: A → B when A calls, uses, imports from, feeds or maintains B. - Write the relationship as a short verb phrase in `label`: `uses`, `pulls rules from`, `exports events to`, `queries CVE data from`, `maintains`. - Use `"direction": "both"` only for genuinely symmetric relationships (two-way sync), and `"none"` for associations without a direction (`affiliated with`). - Explain the mechanism in the edge's `description` (which API, which job, which format), and put checkable facts in `details` (for example `{ "API": "/api/rule/search", "Auth": "API key" }`). - An edge from a node to itself is allowed (for example two instances of the same platform federating). ### 4. Design the types and tags Types carry the look, so the map stays consistent and the JSON stays short: - 3 to 8 **node types** (`project`, `platform`, `dataset`, `organization`, `standard`…), each with a colour, a shape and optionally an image or a bundled icon; - a few **edge types** for the relationships that repeat (`uses`, `feeds`, `develops`), each with a colour and a default label and direction; - **tags** for cross-cutting facts that should be filterable: technology (`python`, `yara`), domain (`threat-intelligence`), status (`archived`). GitHub topics make good tags. Keep them lowercase with hyphens. ### 5. Write the document Follow the [document format](https://ecrou-exact.github.io/project-graph/docs/guide.html#document-format) exactly. Rules that matter: - **Ids** are short, stable and unique: lowercase with hyphens (`misp`, `vulnerability-lookup`). Edges refer to nodes by id. - Every node gets a `label` and, whenever possible, a one-sentence `description` written for someone who does not know the project. - Put the project's website in `url`, the repository in `github` (`owner/repo`), and other useful pages in `links`. - Leave out anything you would only be guessing. A smaller correct map is better than a complete-looking wrong one. - Put the sources you used in each node's `details` (for example `"Source": "https://…"`) when a fact is not obvious. - Set `meta.title`, a `meta.description` that states the arrow convention and the date the facts were checked, and `meta.linkDistance` (200–350) if nodes are big or edge labels long. ### 6. Validate Load the file in the app (*Graph → Open a file…*, or paste it in the JSON tab and press *Apply*). The app reports **errors** (the file is refused: duplicate ids, edges pointing to unknown nodes, wrong top-level shapes) and **warnings** (the file loads, but something was ignored: an unknown shape or direction, a type that is not declared). Fix every error and every warning. You can also check the file against the [JSON schema](https://ecrou-exact.github.io/project-graph/docs/pivograph.schema.json). Then read the map as a newcomer would: every node label is understandable, every arrow reads as a sentence (`Flowintel` → *uses* → `MISP`), nothing important floats unconnected. ### Checklist - [ ] Every node has `id`, `label`, `description`; projects have `github` and/or `url`. - [ ] Every edge has a `label` that is a verb phrase, and the arrow follows the convention in `meta.description`. - [ ] Types are declared in `nodeTypes` / `edgeTypes` before being used. - [ ] Tags are lowercase-with-hyphens and meaningful as filters. - [ ] No duplicate ids; no edge to a missing node; the app shows no warning. - [ ] Facts that are not obvious cite a source in `details`. ### Prompt template Give an AI agent this prompt, with the organization filled in: ```text Map the organization (, ) with Pivograph. Read https://ecrou-exact.github.io/project-graph/llms-full.txt first and follow its "Mapping an organization" procedure and "Document format" exactly. Check https:///.well-known/open-contributions.json and use it if present. Map . Arrow convention: from the one who acts to the one acted upon. Only include relationships you can support with code or documentation, and cite the source in each edge's details. Output one JSON document, nothing else. ``` ## Document format A Pivograph document is a JSON object. All top-level keys are optional; unknown keys are ignored. The [JSON schema](https://ecrou-exact.github.io/project-graph/docs/pivograph.schema.json) describes the same format for validators. ```json { "version": 1, "meta": { "title": "…", "description": "…" }, "nodeTypes": { "project": { … } }, "edgeTypes": { "uses": { … } }, "tags": { "open-source": { … } }, "sections": [ { "title": "…", "x": 0, "y": 0, … } ], "arrows": [ { "from": { "node": "…" }, "to": { "section": "…" }, … } ], "nodes": [ { "id": "…", … } ], "edges": [ { "from": "…", "to": "…", … } ] } ``` **Inheritance.** For every visual field, the value on the node or edge wins; otherwise its type's value is used; otherwise the built-in default. Leaving a field out is how you say "inherit". ### meta | Field | Type | Description | |---|---|---| | `title` | string | Shown in the app's header and the browser tab. | | `description` | string | What the map shows, the arrow convention, when facts were checked. | | `linkDistance` | number | Edge length in the layout. Default `150`; raise it (200–350) for big nodes or long edge labels. | | `fixedLayout` | boolean | Nodes stay exactly at their `x` / `y`: no force layout, as in a drawn diagram. *Add → Fixed layout* toggles it. | | `readOnly` | boolean | Opens the map locked: it can be explored and exported, not edited. Set automatically on imported descriptors and on the bundled example. To change a locked map, edit its JSON file. | | `source` | object | Where the map came from, e.g. `{ "format": "ocd", "domain": "misp-project.org" }`. Set by imports. | ### nodes Each node is an object. Only `id` is required. | Field | Type | Description | |---|---|---| | `id` | string or number | **Required.** Unique. Edges refer to it. Use lowercase-with-hyphens. | | `label` | string | Displayed name. Defaults to the id. | | `type` | string | A key of `nodeTypes`. Undeclared types load with a warning. | | `description` | string | One or two sentences, shown in the details panel, the tooltip and the node list. | | `url` | string | The node's website. | | `graph` | string | Another graph that details this node, e.g. `examples/circl/misp.json` for an organisation and its projects: a Pivograph graph or an OCD file, as a URL or a path relative to the app. The details panel and the right-click menu offer *Open its graph*; a *←* button in the header leads back. | | `github` | string | GitHub repository: `owner/repo`, or any github.com URL of the repository (normalized to `owner/repo`). | | `githubInfo` | object | Repository summary saved by the app when a repository is fetched in the node form: `fullName`, `url`, `description`, `homepage`, `stars`, `forks`, `issues`, `language`, `license`, `topics`, `archived`, `pushedAt`, `fetchedAt`. Kept only together with `github`. You can fill it yourself from the GitHub API. | | `links` | array | Other links: `[{ "label": "Documentation", "url": "https://…" }]`. `label` is optional. | | `tags` | array or string | `["threat-intelligence", "python"]`, or `"#threat-intelligence #python"`. Normalized to lowercase, spaces become hyphens, duplicates removed. | | `details` | object | Extra facts shown in the details panel: strings, numbers, booleans, lists, or nested objects of those. Nested objects are shown as sub-sections. Empty values are dropped. | | `x`, `y` | number | Saved position. Both are needed. Export keeps positions unless you untick *positions*. | **Look** (each can also be set on a node type): | Field | Type | Default | Description | |---|---|---|---| | `color` | CSS colour | `#4f7cff` | Fill colour. `transparent` works, e.g. for logos. | | `shape` | string | `circle` | `circle`, `square`, `triangle`, `hexagon`, or `card`: a rounded box with the image and the label **inside** (see below). | | `size` | number | `14` | Radius in pixels. | | `image` | string | | URL, path relative to the app (`icons/tool.svg`, `logos/misp.png`), or `data:` URL. | | `imageFit` | string | `cover` | `cover` (fills the shape), `contain` (whole image inside), `icon` (a glyph on the node's colour), `frame` (square shape framing the image). | | `borderColor` | CSS colour | `#ffffff` | Outline colour. | | `borderWidth` | number | `2` | Outline width; `0` removes it. | | `hideLabel` | boolean | `false` | Hide the label, e.g. when the image already shows the name. | | `labelColor` | CSS colour | | Label text colour. | | `labelBackground` | CSS colour or `"none"` | | Label background; `"none"` for no background. | | `labelSize` | number | auto | Label font size in pixels. | | `labelFont` | string | | `sans`, `serif`, `mono`, `rounded`, `condensed`, or any CSS `font-family`. | | `hideBadges` | boolean | `false` | Hide the tag pills under the node. | **Card nodes** (`shape: "card"`) draw the box of a diagram: the image (if any), then the label and an optional `subtitle`, inside the box; edges and arrows end on its border. `color` is the background (default white), `borderColor` / `borderWidth` the border, `labelColor` / `labelSize` / `labelFont` the text (`\n` starts a new line). `size` and `imageFit` don't apply. | Field | Type | Default | Description | |---|---|---|---| | `subtitle` | string | | Second line, under the image and label. | | `width`, `height` | number | its content | Box size in pixels. | | `padding` | number | `16` | Space between the border and the content: raise it to give logos more room. | | `imageSize` | number | `56` | Image height in pixels. | | `imagePosition` | string | `top` | `top` (image above the label) or `left` (beside it). | `labelFont` also accepts `hand`: Excalidraw's hand-drawn font (Virgil, bundled). Bundled images: `icons/project.svg`, `platform`, `tool`, `data`, `organization`, `format`, `rule`, `code` (white glyphs for `imageFit: "icon"`). ### edges Each edge is an object with `from` and `to`. | Field | Type | Description | |---|---|---| | `from`, `to` | string or number | **Required.** Node ids. Both nodes must exist. | | `id` | string | Unique. Defaults to `from-to` (with a suffix if needed). | | `label` | string | How the two are related: a short verb phrase shown on the edge. | | `type` | string | A key of `edgeTypes`. | | `description` | string | The mechanism behind the relationship. | | `details` | object | Extra facts, as for nodes (API endpoints, authentication, sources…). | **Look** (each can also be set on an edge type): | Field | Type | Default | Description | |---|---|---|---| | `direction` | string | `forward` | `forward` (arrow at `to`), `backward` (arrow at `from`), `both`, `none`. | | `color` | CSS colour | `#8a94a6` | Line and arrow colour. | | `width` | number | `2` | Line width. | | `dashed` | boolean | `false` | Dashed line. | | `curve` | string | `auto` | `auto` (straight, curved only between nodes with several edges), `straight`, `curved`. | | `animated` | boolean | `false` | Moving dashes along the edge, showing the flow. | | `hideLabel` | boolean | `false` | Hide the label. | | `labelColor`, `labelBackground`, `labelSize`, `labelFont` | | | As for nodes. | ### nodeTypes and edgeTypes Objects keyed by type name. A node type accepts `label` (display name) and every node **look** field; an edge type accepts `label` (the default edge label) and every edge **look** field. ```json "nodeTypes": { "project": { "label": "Project", "color": "#3b63f3", "shape": "hexagon", "size": 24, "image": "icons/project.svg", "imageFit": "icon" }, "dataset": { "label": "Open data", "color": "#c2860b", "image": "icons/data.svg", "imageFit": "icon" } }, "edgeTypes": { "uses": { "label": "uses", "color": "#0f9d8a" }, "feeds": { "label": "feeds", "color": "#c2860b", "animated": true }, "sync": { "label": "syncs with", "direction": "both", "dashed": true } } ``` ### tags Tags are declared on nodes. The top-level `tags` object only sets how a tag is drawn; tags without an entry get a colour derived from their name. | Field | Type | Description | |---|---|---| | `color` | CSS colour | Pill colour; the text turns black or white for contrast. | | `icon` | string | A bundled Font Awesome Free icon name. | ```json "tags": { "open-source": { "color": "#2f9e44", "icon": "github" }, "threat-intelligence": { "color": "#d6384b", "icon": "shield-halved" } } ``` Available icons: `shield-halved`, `bug`, `virus`, `skull`, `lock`, `key`, `fingerprint`, `triangle-exclamation`, `circle-info`, `circle-question`, `check`, `xmark`, `star`, `flag`, `tag`, `heart`, `fire`, `bolt`, `bell`, `lightbulb`, `eye`, `magnifying-glass`, `database`, `server`, `cloud`, `network-wired`, `globe`, `link`, `code`, `terminal`, `gear`, `wrench`, `cube`, `robot`, `rocket`, `chart-line`, `clock`, `book`, `file-lines`, `graduation-cap`, `user`, `users`, `building`, `handshake`, `scale-balanced`, `money-bill`, and the brands `github`, `gitlab`, `python`, `js`, `rust`, `docker`, `linux`, `windows`, `apple`, `android`, `aws`, `google`, `slack`, `discord`, `mastodon`. ### sections Titled frames drawn behind the graph ("Incident response", "Sensors"…), for the picture only: they hold no nodes and edges don't point to them. Add one with *Add → Section*; drag its title to move it, its bottom-right corner to resize it, double-click its title to edit it (or use the Sections tab). Pair them with `meta.fixedLayout` so the nodes stay inside. They are part of the PNG export. | Field | Type | Default | Description | |---|---|---|---| | `x`, `y` | number | | **Required.** Top-left corner, in graph coordinates (the same as the nodes' `x` / `y`). | | `width`, `height` | number | `400`, `240` | Size. | | `title` | string | | Shown in the top-left corner. | | `color` | CSS colour | grey | Title colour, and the line under it. | | `underline` | boolean | when `color` is set | Line under the title. | | `titleSize` | number | `22` | Title font size in pixels. | | `titleFont` | string | | As `labelFont`. | | `fill` | CSS colour or `"none"` | light grey | Background; `"none"` for a bare title (e.g. the diagram's heading). | | `borderColor` | CSS colour or `"none"` | grey | Border. | | `id` | string | from the title | Unique. | ```json "meta": { "title": "SOC stack", "fixedLayout": true }, "sections": [ { "title": "Incident response", "x": 20, "y": 115, "width": 750, "height": 420, "color": "#1971c2" }, { "title": "Sensors", "x": 20, "y": 880, "width": 1660, "height": 250 } ] ``` ### arrows Arrows drawn by Pivograph, like sections, for diagrams: unlike an edge, an arrow can start or end on a **section** or on **a free point**, and each end can sit **anywhere** on its target. Add one with *Add → Arrow*; click it on the graph to select it, then drag its ends (onto a node, a section or empty space) and its label; Delete removes it, a double-click edits it. Arrows are part of the PNG export, not of the report. | Field | Type | Default | Description | |---|---|---|---| | `from`, `to` | object | | **Required.** `{ "node": "id" }`, `{ "section": "id" }` or `{ "x": 0, "y": 0 }`. Add `"at": [u, v]` to place the end on the target's box: `[0, 0]` top-left, `[1, 1]` bottom-right, `[0.5, 1]` the middle of the bottom side. Without `at`, the end sits on the side facing the other end (straight up or down from a section). | | `label` | string | | `\n` starts a new line. | | `description` | string | | Shown when the pointer is over the arrow. | | `direction` | string | `forward` | As for edges: `forward`, `backward`, `both`, `none`. | | `route` | string | `straight` | `straight` or `elbow` (right angles). | | `color`, `width`, `dashed` | | `#343a40`, `2`, `false` | Line. | | `labelColor`, `labelSize`, `labelFont`, `labelBackground` | | line colour, `14` | Label; the background defaults to the page colour, `"none"` for none. | | `labelOffset` | [dx, dy] | `[0, 0]` | Shift of the label from the middle of the arrow (set by dragging it). | ```json "arrows": [ { "from": { "node": "misp", "at": [0.5, 1] }, "to": { "section": "pipeline" }, "direction": "both", "label": "4) Feed detections\n(IoCs) & sightings", "color": "#1c5cff" } ] ``` ### Validation Loading a document reports: - **Errors** — the document is refused: the root is not an object; `nodes`, `edges`, `nodeTypes`, `edgeTypes` or `tags` has the wrong shape; a node has no id; two nodes share an id; an edge lacks `from` or `to`, or points to a node that does not exist. - **Warnings** — the document loads, the listed value is ignored: an unknown `shape` or `direction`; a `type` not declared in `nodeTypes` / `edgeTypes`; a `github` value that is not a repository; a duplicate edge id (renamed). ## Examples - [`examples/rulezet.json`](https://ecrou-exact.github.io/project-graph/docs/examples/rulezet.json): how Rulezet connects to ten other projects, with logos, detailed relationship descriptions, GitHub details and tags. It is the example the app opens with. - [`examples/circl.json`](https://ecrou-exact.github.io/project-graph/docs/examples/circl.json): the GitHub organisations managed or co-managed by CIRCL, from [new.circl.lu](https://new.circl.lu/projects/github-organisations/). CIRCL in the centre and 21 organisations around it, each with its logo, description, website, most starred repository (`githubInfo`) and most used topics as tags. Generated by `node scripts/circl-example.mjs` from the GitHub API. - [`examples/ngsoti-soc-stack.json`](https://ecrou-exact.github.io/project-graph/docs/examples/ngsoti-soc-stack.json): a drawn diagram — the NGSOTI SOC stack (sensors, Tenzir pipeline, MISP, flowintel, AIL) with Rulezet: card nodes, sections, arrows, a fixed layout and the hand-drawn font. It opens read-only (*Graph → NGSOTI SOC stack example*). - [`examples/minimal.json`](https://ecrou-exact.github.io/project-graph/docs/examples/minimal.json): the smallest useful map — two types, three nodes, two edges. A compact example with the main features: ```json { "version": 1, "meta": { "title": "Projects linked to Rulezet", "description": "Arrow = who uses whom. Checked against each project's code, September 2026.", "linkDistance": 260 }, "nodeTypes": { "project": { "label": "Project", "color": "#3b63f3", "shape": "hexagon", "size": 24, "image": "icons/project.svg", "imageFit": "icon" } }, "edgeTypes": { "uses": { "label": "uses", "color": "#0f9d8a", "direction": "forward" } }, "tags": { "threat-intelligence": { "color": "#d6384b", "icon": "shield-halved" } }, "nodes": [ { "id": "rulezet", "label": "Rulezet", "type": "project", "description": "Community platform for sharing and managing detection rules.", "url": "https://rulezet.org", "github": "rulezet/rulezet-core", "tags": ["threat-intelligence", "yara"] }, { "id": "vulnerability-lookup", "label": "Vulnerability-Lookup", "type": "project", "description": "Vulnerability lookup and correlation.", "github": "vulnerability-lookup/vulnerability-lookup", "links": [{ "label": "Public instance", "url": "https://vulnerability.circl.lu" }] } ], "edges": [ { "from": "rulezet", "to": "vulnerability-lookup", "type": "uses", "label": "fetches CVE & EPSS data", "description": "Rulezet uses Vulnerability-Lookup as its CVE reference: every CVE shown on a rule links to its vulnerability.circl.lu page.", "details": { "Source": "https://github.com/rulezet/rulezet-core" } } ] } ``` ## Open Contributions Descriptor The [Open Contributions Descriptor](https://github.com/ossbase-org/Open-Contributions-Descriptor) (OCD) is a JSON file in which an organization describes its open source projects, open data, participation in open standards and relationships with other organizations. It is published at `https:///.well-known/open-contributions.json`. [OCD Viewer](https://github.com/ossbase-org/ocd-viewer) shows it as cards and, through Pivograph, as a graph. Pivograph recognizes an OCD file wherever a JSON file is accepted (*Graph → Open a file…*, drag and drop, the JSON tab, *Graph → Import an organization…*, `?src=`, `postMessage`) and turns it into a map: | OCD | Map | |---|---| | `organization` | The central *Organization* node. Its fields (`domain`, `country`, `links`…), the document's `contacts`, `policies`, `spec_version`, `generated_at`, `extensions` and any unknown top-level section go to the node's `details`. | | `projects[]` | One *Project* node each, linked from the organization (edge type `maintains`, no label). `name` → `label`, `description` → `description`, `tags` → `tags`, `repository.url` → `github` when it is a GitHub repository. Everything else (`status`, `repository`, `links`, `participate`, `governance`, `release`, custom fields) is kept as-is in `details`, in its original order. `status: archived` / `disabled` also becomes a tag. | | `open_data[]` | *Open data* nodes (edge `publishes`); fields in `details`. | | `open_standards[]` | *Open standard* nodes, one per `body` (edge `participates in`, labelled with the contribution types); fields in `details`. | | `relationships[]` | *External organization* or *External project* nodes, one per `target`, linked from the organization with the relationship type as label: `maintains`, `co-maintains` (both ways), `supports`, `contributes to`, `sponsors`, `upstream` (arrow towards the organization), `downstream`, `member of`, `affiliated with`. `since`, `until`, `evidence`, `contacts` and `tags` go to the edge's `details`. | Because each item keeps its OCD structure in `details`, the details panel reads section by section like OCD Viewer, and nothing is lost — unknown or future fields included. An imported descriptor opens **read-only**: no creation, editing or deletion (Pivotick's *Create* tools, context-menu entries, double-click, the app's buttons and the JSON editor are off). Browsing, filtering and exporting still work. The lock can't be lifted in the app: the graph stays a faithful view of the descriptor. To build your own map around it, start a *New graph*, or export the JSON and edit it. *Graph → Import an organization…* accepts a local file, a domain (`misp-project.org` becomes `https://misp-project.org/.well-known/open-contributions.json`), or a URL, and offers the official samples (MISP, AIL, flowintel). A remote file must be served with CORS headers; otherwise download it and open it. To go further than the descriptor, enable editing and add what it does not say: relationships between the organization's own projects, logos, tags with colours and icons. ## Embedding Pivograph can be shown inside another site with an ` ```html ``` The host page can also send the data itself, which works for files that have no URL (for example a file the visitor opened): ```js const frame = document.querySelector('iframe') window.addEventListener('message', (event) => { if (event.source !== frame.contentWindow) return if (event.data?.type === 'pivograph:ready') { frame.contentWindow.postMessage( { type: 'pivograph:load', data: documentOrOcd, name: 'open-contributions.json' }, 'https://ecrou-exact.github.io', ) } }) ``` | Message | Direction | Meaning | |---|---|---| | `{ type: 'pivograph:ready' }` | app → host | The app is ready to receive data. Sent once at start. | | `{ type: 'pivograph:load', data, name }` | host → app | Load `data` (a Pivograph document or an OCD file). Only the embedding page can send it. | | `{ type: 'pivograph:loaded', nodes, edges }` | app → host | Loaded, with the number of nodes and edges. | | `{ type: 'pivograph:error', message }` | app → host | The data was refused; `message` says why. | | `{ type: 'pivograph:changed', data }` | app → host | An editable map (loaded with `meta.readOnly: false`) was changed; `data` is the whole document, positions included. Never sent for read-only maps. | | `{ type: 'pivograph:get' }` | host → app | Ask for the current document. | | `{ type: 'pivograph:snapshot' }` | host → app | Ask for a PNG of the graph as drawn. | | `{ type: 'pivograph:snapshot', image }` | app → host | The answer: a `data:image/png` URL, or `null` when the picture could not be made. | | `{ type: 'pivograph:export', format }` | host → app | Run one of the app's exports in the frame: `json`, `pivotick`, `png`, `md` or `pdf` (the report). | | `{ type: 'pivograph:theme', scheme, background }` | host → app | Switch to `light` / `dark` and/or a background colour, live (when the host page changes theme). | | `{ type: 'pivograph:document', data }` | app → host | The answer: the whole document, positions included. | [OCD Viewer](https://github.com/ossbase-org/ocd-viewer) uses this for its *Graph* view. ## Hugo A site built with [Hugo](https://gohugo.io) can show a graph with the Pivograph component (`hugo/pivograph` in the repository). **Hugo builds the graph's JSON itself**, from the site's pages or from a data file, and every graph works with and without JavaScript: | | Without JavaScript | With JavaScript | |---|---|---| | Picture | The graph **drawn by Hugo as SVG** at build time: nodes with their shape, colour, logo and label; edges with their colour, dashes, arrows and labels. A node is a link to the page of its own graph (`graph.graph`), else to its entry in the text, and shows its name on hover. | The **interactive graph** (the Pivograph app in an iframe) replaces the picture: zoom, drag, search, filters by tag, type and relationship, details panel. If the app can't load, the picture comes back. | | Text | The graph written out: every node with its links, GitHub facts, tags and details, and every relationship as a sentence ([report protocol](https://ecrou-exact.github.io/project-graph/docs/guide.html#the-report-protocol)). Read by search engines and screen readers. | The same text, folded under *Read the graph as text*. | | Data | `/pivograph/.json`, linked under the graph. | The same file, handed to the app. | The same page of the [example site](https://ecrou-exact.github.io/project-graph/hugo-example/projects/), both ways:
The Rulezet graph drawn by Hugo as SVG: logos, labels, tag pills and coloured arrows, without JavaScript
Without JavaScript: the picture Hugo draws at build time, followed by the text. Open it
The same graph in the interactive Pivograph app, with its search, filter and view tools
With JavaScript: the interactive graph, with search, filters and zoom. Open it
The example's source: [hugo/example](https://github.com/ecrou-exact/project-graph/tree/main/hugo/example). ```text hugo/example/ hugo.toml theme = "pivograph" (a real site: ["my-theme", "pivograph"]) content/ projects/ _index.md graph: title, nodeTypes, edgeTypes · {{< pivograph >}} rulezet/ index.md graph: type, url, github, image, relations logo.png misp/ index.md logo.png … one bundle per project circl/ _index.md the CIRCL GitHub organisations · {{< pivograph >}} circl/ misp/ … one bundle per organisation, with its logo and GitHub facts ``` `hugo` then writes `public/pivograph/projects.json` and `public/pivograph/circl.json`, the documents the pages show. Both sections are generated from the app's examples by `node scripts/hugo-example.mjs`. ### Add it to a Hugo site 1. **Copy the component** into the site's `themes/` folder: ```bash git clone --depth 1 https://github.com/ecrou-exact/project-graph /tmp/project-graph cp -r /tmp/project-graph/hugo/pivograph themes/pivograph ``` Or as a Hugo Module: `[[module.imports]] path = "github.com/ecrou-exact/project-graph/hugo/pivograph"` (needs Go). 2. **Enable it** in `hugo.toml`, **after** the site's theme. A single `theme = "x"` becomes a list: ```toml theme = ["my-theme", "pivograph"] ``` The component adds a shortcode and partials under `pivograph/`; it doesn't change any existing page, layout or style. 3. **Give it data**: the site's pages ([option A](https://ecrou-exact.github.io/project-graph/docs/guide.html#a-graph-from-the-sites-pages)) or a data file ([option B](https://ecrou-exact.github.io/project-graph/docs/guide.html#a-graph-from-a-data-file)). 4. **Place the graph** with the shortcode in a Markdown page, or with the partial in a layout: ```text {{< pivograph >}} ``` ```go-html-template {{ partial "pivograph/embed.html" (dict "page" . "section" "/projects") }} ``` 5. **Check**: run `hugo`. It must print no `WARN` from pivograph. Then open the page: with JavaScript you get the interactive graph; click *Without JavaScript* in the switch above the graph (it adds `?pivograph=static` to the address), or turn JavaScript off, to see the version without JavaScript, the picture drawn by Hugo and the text; *Interactive* brings the graph back. That choice holds for the browser tab, and the site's links keep it, until `?pivograph=interactive`. Requirements: Hugo 0.130 or later, standard or extended edition. Nothing to install with npm, no build step besides Hugo. ### A graph from the site's pages Use this when the site already has one page per project, team, product… Every page below the section whose front matter has a `graph` map becomes a node; its `relations` are its outgoing edges. ```yaml # content/projects/rulezet/index.md (a page bundle: logo.png sits next to it) --- title: Rulezet # the node's label description: Community platform for sharing and managing detection rules. tags: [cti, yara] # the node's tags (the site's own taxonomy) graph: type: project url: https://rulezet.org github: rulezet/rulezet-core image: logo.png relations: - to: misp # id of another node: its file or bundle name label: pushes rules as MISP events type: uses description: Admins register MISP servers in Rulezet… - to: vulnerability-lookup label: fetches CVE data from --- ``` The section's `_index.md` holds the graph's title and its types, and places it: ```yaml # content/projects/_index.md --- title: Projects linked to Rulezet description: How Rulezet connects to the other projects of its ecosystem. graph: nodeTypes: project: { label: Project, shape: circle, size: 44, imageFit: contain } edgeTypes: uses: { label: uses, color: "#0f9d8a" } --- {{< pivograph >}} ``` **Front matter reference** | Where | Key | Becomes | Default | |---|---|---|---| | node page | `title` | node `label` | | | node page | `description` | node `description` | | | node page | `tags` | node `tags` | | | node page | `graph.id` | node `id` | the file name (`misp.md`) or bundle name (`misp/index.md`) | | node page | `graph.label`, `graph.description`, `graph.tags` | override the three above | | | node page | `graph.image` | node `image`: a page resource (`logo.png`), a site path (`/images/x.png`), a URL, or an icon of the app (`icons/tool.svg`) | | | node page | `graph.graph` | node `graph`: the path of the page that shows this node's own graph (`/circl/misp`), written as that page's address. Without JavaScript the node links to that page; with JavaScript, *Open its graph* in the app goes there. The CIRCL example uses it: each organisation's page shows its projects with `{{< pivograph data="circl-" >}}`. | | | node page | `graph.x`, `graph.y` | the node's position, used by the app and by the picture | computed | | node page | `graph.` | any other [node field](https://ecrou-exact.github.io/project-graph/docs/guide.html#nodes): `type`, `url`, `github`, `links`, `details`, `color`, `shape`, `size`, `imageFit`, `githubInfo`… | | | node page | `graph.relations[]` | edges from this node: `to` (required) and any [edge field](https://ecrou-exact.github.io/project-graph/docs/guide.html#edges): `label`, `type`, `direction`, `description`, `details`, `color`, `dashed`… | | | section `_index.md` | `title`, `description` | `meta.title`, `meta.description` | | | section `_index.md` | `graph.nodeTypes`, `graph.edgeTypes`, `graph.tags`, `graph.linkDistance`, `graph.meta` | the same document keys | | Rules the component applies: - Every node gets a *Site page* link back to its page. - A relation to an id that no page has is dropped, with a build warning naming the page (`pivograph: content/projects/x.md: relation to "y" ignored`), so the app never refuses the file. - Graphs shown on a site open read-only. - Hugo lowercases front-matter keys; the component restores Pivograph's names (`imagefit` → `imageFit`). Hugo also sorts map keys, so `details` come out in alphabetical order. - Node order is the section's page order (weight, date, then title). ### A graph from a data file Use this for a graph made in the app. *Export → Graph data (JSON)* in Pivograph, save the file as `data/pivograph/.json` (or write it as `.yaml` / `.toml`), and show it anywhere: ```text {{< pivograph data="ecosystem" >}} ``` Export with *positions* ticked (JSON tab) and the picture drawn without JavaScript keeps the app's layout. Without positions, Hugo lays the graph out itself: the most connected node in the centre when it links to at least half of the others, the rest on rings around it. ### Parameters The shortcode and the partial take the same parameters. | Parameter | Default | Effect | |---|---|---| | `section` | the current section | Build the graph from another section: `section="/projects"`. | | `data` | | Use `data/pivograph/.*` instead of pages. | | `app` | `params.pivograph.app`, else `https://ecrou-exact.github.io/project-graph/` | The Pivograph app that draws the interactive graph. | | `height` | `85vh` | Height of the interactive graph. The graph takes the width of its container: give it the whole width of the page for big graphs (the example site keeps only its text in a narrow column). | | `sidebar` | `false` | Show the app's side panel (node list, tags, types). | | `tags` | `false` | `true` shows the tag pills, on the interactive graph and in the picture drawn by Hugo. | | `relations` | `false` | `true` writes the relationships as sentences in the text, under each node and in a *Relationships* section. By default the text only counts them. | | `picture` | `true` | `false` leaves out the picture drawn by Hugo. | | `image` | | A picture of your own (e.g. *Export → Picture* in the app) shown instead of Hugo's. | | `level` | `3` | Heading level of the text's sections. | | `text` | `true` | `false` leaves the text out. Not recommended: without JavaScript, only the picture would remain. | Site-wide setting, in `hugo.toml`: ```toml [params.pivograph] app = "https://ecrou-exact.github.io/project-graph/" ``` To host everything yourself, copy the app's build (`npm run build`, the `dist/` folder) into `static/pivograph/` and set `app = "/pivograph/"`. To use the data in your own templates: `{{ $doc := partial "pivograph/document.html" (dict "page" . "section" "/projects") }}`, then `$doc.nodes`, `$doc.edges`… The picture alone: `{{ partial "pivograph/svg.html" (dict "doc" $doc) | safeHTML }}`. ### How it works - **Build time (Hugo, no JavaScript).** `document.html` reads the pages or the data file and returns the document; `embed.html` publishes it as `/pivograph/.json` with `resources.FromString`, then writes the picture (`svg.html`, laid out by `layout.html`), the text (`text.html`, sentences by `sentence.html`) and a caption with a link to the data. - **In the browser (JavaScript).** Unless the address has `?pivograph=static`, the script (`assets/pivograph/pivograph.js`) hides the picture, folds the text, opens the app in an iframe, fetches the JSON from the site itself, embeds the site's images in it as data URLs, and hands it over with the [`pivograph:load` message](https://ecrou-exact.github.io/project-graph/docs/guide.html#embedding). The app never loads anything from the site, so no CORS setup is needed, an `https` app works with an `http` site, and the app's picture and PDF exports keep the logos. ### Checklist - [ ] `hugo` builds with no `WARN` or `ERROR` from pivograph. - [ ] `/pivograph/.json` loads in the Pivograph app (*Graph → Open a file…*) with no error or warning. - [ ] Without JavaScript (open the page with `?pivograph=static`, or turn JavaScript off: in Firefox, F12 → F1 → *Disable JavaScript*; in Chrome, F12 → Ctrl+Shift+P → *Disable JavaScript*): the picture shows every node and arrow, and the text lists every node and relationship. - [ ] With JavaScript on: the interactive graph shows, with its logos, and *Read the graph as text* opens the text. - [ ] Every relationship reads well as a sentence in the text (see [how a relationship becomes a sentence](https://ecrou-exact.github.io/project-graph/docs/guide.html#the-report-protocol)); reword labels that don't. ### Prompt for an AI agent ```text Add a Pivograph graph to this Hugo site, following https://ecrou-exact.github.io/project-graph/docs/guide.html#hugo exactly. 1. Copy hugo/pivograph from https://github.com/ecrou-exact/project-graph into themes/pivograph and add "pivograph" after the current theme in hugo.toml. 2. For every page of
that describes , add a `graph` map to its front matter: type, url, github, image when known, and `relations` to the other pages (to: , label: a lowercase verb phrase from this page's point of view). 3. In
/_index.md, declare nodeTypes and edgeTypes under `graph` and put {{< pivograph >}} where the graph should appear. 4. Run `hugo`: fix every "pivograph:" warning. Check that the page works with JavaScript on (interactive graph) and off (SVG picture and text). Do not change the site's theme, layouts or other pages. ``` ### Troubleshooting | Symptom | Cause and fix | |---|---| | I can't see the picture drawn by Hugo | With JavaScript on, the interactive graph replaces it. Open the page with `?pivograph=static`, or turn JavaScript off, to see it. | | `can't evaluate field Pi`, `math.Sin` not defined | Hugo is older than 0.130. Update Hugo (distribution packages are often older; use a [release](https://github.com/gohugoio/hugo/releases)). | | `pivograph: … relation to "x" ignored` | No page has the graph id `x`. Use the target's file or bundle name, or set `graph.id` on it. | | The interactive graph stays empty | The page's Content Security Policy blocks the iframe or the fetch: allow the app's origin in `frame-src`, and `connect-src 'self'`, `img-src 'self' data:`. | | Logos missing in the picture | `graph.image` must be a page resource (next to `index.md`), a path under `static/` starting with `/`, or a full URL. | | The picture is crowded | Lay the graph out in the app, export it with positions, and use it as a data file (or copy the `x`, `y` into `graph`). | ## Reports *Export* offers five formats: | Menu entry | File | Content | |---|---|---| | Graph data (JSON) | `.json` | The document, to open again or edit later. | | Pivotick data (JSON) | `<title>.pivotick.json` | The graph for Pivotick alone, without Pivograph: nodes and edges with their computed styles, positions and embedded images, plus the options they need. | | Report (PDF) | via the print dialog | A picture of the graph, then a text written from every field. Choose *Save as PDF*. | | Report (Markdown) | `<title>.md` | The same report as Markdown, with the picture embedded. | | Picture (PNG) | `<title>.png` | The whole graph as drawn, whatever the zoom. | **Pivotick data.** The file is self-contained and loads in any page that has Pivotick: ```js const file = await (await fetch('map.pivotick.json')).json() new Pivotick(container, file, file.options) ``` - `nodes` and `edges` are Pivotick's raw nodes and edges (`{ id, data, style }`, `{ id, from, to, data, style }`), with `x` and `y` when positions are kept. - Logos and icons are embedded as data URLs, so the file shows them anywhere. An image from another site that doesn't allow cross-origin requests stays a link, and the app says so. - `options` holds the arrow markers (`pg-arrow`, `pg-arrow-start`) and the link distance. Without them, Pivotick draws the edges without arrow heads. - An edge label inherited from the edge type is copied into `data.label`, the field Pivotick's default label renderer reads. The file keeps everything Pivotick draws: colours, shapes, sizes, images, borders, line styles, arrows, edge labels. It does not keep what Pivograph draws on top of Pivotick: the tag pills, and the colour, background and font of node and edge labels. Pivotick shows those labels in its default style. ### The picture The graph is drawn as it looks in the app, with images, labels and tag pills included. The picture covers the whole graph, not just the visible part, at twice the screen resolution, and is at most 4000 pixels on its longest side. Images from another site are left out unless that site allows cross-origin requests. ### The report protocol The content is decided by `reportModel()` in `src/report.js`, and laid out by `buildReport()` (Markdown) and `reportHtml()` in `src/reportPrint.js` (PDF). The rules are fixed, so any graph gives a complete report and the same document always gives the same text: 1. **Title** from `meta.title` (default *Untitled graph*), then the picture, then `meta.description`. 2. **Overview.** The number of nodes, by type; the number of relationships, by edge type when there are several; the three most connected nodes (two relationships or more); the nodes without any relationship; the most used tags. 3. **Nodes**, grouped by type in their order of first appearance, with untyped nodes last under *Other*. For each node: - "*Label* is a *type*." followed by its description; - website, GitHub repository (with the saved `githubInfo`: description, stars, forks, open issues, language, licence, last update, topics not already in the tags, and the date of the data), other links, tags; - `details` as a nested list, with keys shown with `_` replaced by spaces and URLs as links; - every relationship of the node, as a sentence. 4. **Relationships**, grouped by edge type. A relationship that has a description or details gets its own entry with them. The others are merged when only their object differs: "**MISP Project** maintains **MISP**, **PyMISP** and **misp-modules**." 5. **Tags**: every tag, with its nodes. 6. A footer with the date and the counts. **How a relationship becomes a sentence.** The subject is the node the arrow starts from. With `direction` set to `forward` (the default) or `none`, that is the source (`from`); with `backward`, it is the target. The label is the edge's own `label`, else its type's label, else its type id with `_` and `-` turned into spaces. A final parenthesis in the label is moved to the end of the sentence. The label is then read by its words: | Label | Kind | Sentence | |---|---|---| | `uses`, `depends on`, `draws graphs with` | verb, alone or ending with a preposition | **A** uses **B**. | | `pushes rules as MISP events` | verb with its own object | **A** pushes rules as MISP events (→ **B**). | | `member of`, `affiliated with`, `used by` | state (ends with a preposition, or starts with a participle) | **A** is member of **B**. | | `API`, `upstream` | noun | **A** is linked to **B** (API). | | none | — | **A** is linked to **B**. | A word counts as a verb when it ends in *s* (*uses*, *pushes*), or when it is one of *is*, *are*, *was*, *has*, *have*, *can*, *will*, *may*, *must*, *does*, *use*. Verbs joined by *or* / *and* read as one verb (`manages or co-manages`: **A** manages or co-manages **B**). A label that starts with a capital letter is read as a noun. With `direction: "both"`, the sentence ends with "in both directions". An edge from a node to itself reads "… itself". **Writing labels that read well.** Use a lowercase verb phrase from the source's point of view: `uses`, `depends on`, `fetches data from`, `is funded by`. The same label then reads well on the graph and in the report. ### The PDF layout The PDF and the Markdown file say the same thing; the PDF lays it out with the graph's own colours: - the first page: title, description, a legend of the node types (colour and count) and relationship types (a sample of their line), the picture, and the overview; - each node with a bar in its colour, its logo or icon, its type, its links, its GitHub statistics, its tags as coloured pills with their icons, its details as a table, and its relationships; - each relationship type with a sample of its line, colour and dashes included; - the tag index as pills, and a running header with the title and the date on every page. The page has no margin of its own, so the browser prints no header or footer (address, date) over the report. Use *Save as PDF* with the default options; keep *Background graphics* on if the browser offers it. ### From the command line ```bash npm run report -- examples/rulezet.json report.md npm run report -- open-contributions.json report.md --image graph.png ``` The command runs the same protocol on a Pivograph document or an Open Contributions Descriptor. Making the picture needs a browser, so the command can only link one you pass with `--image`. ## Using the app **Top bar.** *Search the docs* finds a section of this guide as you type (↑ ↓ and Enter to open it). *Add* creates a node or an edge. *Graph* starts a new graph, opens a file (a Pivograph document or an Open Contributions Descriptor), imports an organization from its domain, or loads the Rulezet example. *Export* saves the graph (see [Reports](https://ecrou-exact.github.io/project-graph/docs/guide.html#reports)). **Side panel** (left, resizable by dragging its edge): - *Nodes* and *Edges* list everything with its type and description. Click to select on the graph, double-click to edit. On a read-only map, double-clicking a node (in the list or on the graph) opens its website in a new tab instead. The filter box searches labels, types, descriptions and tags. - *Tags* and *Types* list tags and types. Click one to **filter** the graph by it (click again to remove; several combine). Double-click to edit its colour, icon or style. - *JSON* shows the document; edit it and press *Apply*. **Editing a node** (double-click, right-click → *Edit node*, or the *Create* tools on the canvas) opens a form in tabs: | Tab | Fields | |---|---| | Content | Label, id, type, description | | Links | Website, GitHub repository, other links | | Details | Extra fields, edited by path (`repository.url`) | | Appearance | Colour, shape, size, image, image fit | | Border | Width (0 = none), colour | | Label | Shown or hidden, size, font, text colour, background | | Badges | Tags (existing tags are suggested), show or hide the tag pills | **Editing an edge** (double-click, or right-click → *Edit Edge*) opens a form with a live preview: *Content* (source, target, swap, how they are related, type, arrow direction), *Line* (colour, width, solid or dashed, shape, moving dashes), *Label* and *Details*. Drawing a new edge on the canvas (*Create* → *Add edge*, then click the source and the target) opens the same form. **GitHub details.** Entering a repository in a node's *Links* tab fetches its description, stars, forks, open issues, language, licence, topics and last update from the GitHub API, and saves them with the node (`githubInfo`). *Refresh from GitHub* fetches them again; *Also add the repository topics as tags* copies the topics into the node's tags. Nothing else calls the API: showing or importing a map never does, which keeps within the limit of 60 unauthenticated requests per hour. **Filtering.** Pivotick's *Filter Graph* panel filters by tags (every tag is offered), type, label, description and, for OCD projects, status and license; relationship types can be switched on and off. Filters set from the side panel and from *Filter Graph* are the same; active filters are listed at the top of the side panel with *Clear*. **View.** *Tags on graph* shows or hides every tag pill. Pivotick's *View* and *Physics* tools change the layout; the minimap and zoom controls sit on the right. **Saving.** A map you create is kept automatically **in your browser only** (local storage): nothing is sent to GitHub or to any server, and other visitors never see it. The Rulezet example and imported descriptors are locked: they can be explored and exported, not edited. *Export → Graph data (JSON)* downloads it (with positions, unless *positions* is unticked in the JSON tab). The other *Export* entries are described in [Reports](https://ecrou-exact.github.io/project-graph/docs/guide.html#reports). ## Architecture Pivograph is a static web app built with [Vite](https://vite.dev), in plain JavaScript modules, with no server. ```text src/ main.js app shell: state, header menus, side panel, import/export, embedding, autosave graph.js GraphView: the Pivotick instance, styles, forms wired to Pivotick's tools, filters, tag pills model.js the document format: parsing and validation, type inheritance, Pivotick styles ocd.js Open Contributions Descriptor → document icons.js node icons drawn on the node's colour badgeIcons.js the bundled Font Awesome Free icons github.js GitHub API client (cached, only used by the node form) report.js the report protocol: document → English Markdown (pure, also used by scripts/report.mjs) reportPrint.js the report laid out for print (PDF) snapshot.js the graph as a PNG picture ui/ forms, modals, tag pills, GitHub card, details rendering, DOM helper examples/ example maps hugo/ the Hugo component (pivograph/: shortcode, partials that build the JSON, draw the SVG and write the text; script) and its example site (example/) schema/ JSON schema of the format docs/ this documentation (Markdown, built into docs/index.html and llms-full.txt) tests/ Vitest tests ``` **Data flow.** A JSON file goes through `parseDocument()` (model.js), which validates it and returns a normalized document with errors and warnings. `GraphView.load()` turns its nodes and edges into Pivotick elements (`toRawNode`, `toRawEdge`); from then on **Pivotick holds the nodes and edges**, and the app holds the document-level state (`meta`, `nodeTypes`, `edgeTypes`, `tags`). `view.toDocument()` reads them back for export and autosave. **Styles are derived, never stored.** Every visual field lives in a node's or edge's data. `nodeStyle()` and `edgeStyle()` compute the Pivotick style from the data and the types, so an edit made anywhere — a form, Pivotick's own tools, a type change — restyles the element. **Edits** from the app's forms go through `GraphView.updateNode()` / `updateEdge()` (Pivotick's `updateData()`). Pivotick's own create and edit tools are routed to the same forms through its callbacks (`onBeforeNodeCreate`, `onBeforeEdgeCreate`, `onNodeEdit`, `onEdgeEdit`). **Things drawn outside Pivotick's styles:** - Node label looks (colour, background, size, font) go into a generated stylesheet keyed by each node's DOM id. - Tag pills are SVG groups added inside each node's group, redrawn by a `MutationObserver` whenever Pivotick redraws the node. - Icons with `imageFit: "icon"` are redrawn as data URLs on the node's colour, so they stay visible in Pivotick's tooltip and panels. **Pivotick 2.0.1 notes**, worked around in the code: - The constructor is `new Pivotick(container, data, options)`. - `getNodes()` returns copies with fresh DOM ids; `getMutableNodes()` returns the live nodes. - `setStyle()` does not redraw a node's content; `updateData()` does. - With a custom `onNodeEdit` body, the *Edit Node* button of Pivotick's modal fails; the app closes that modal and opens its own form. - Custom edge labels are measured in screen pixels; CSS re-centres them. ## Development ```bash npm run dev # dev server with hot reload: the app, the docs at /docs/, the Hugo example at /hugo-example/ (needs hugo) npm test # Vitest: document format, OCD import, icons, reports, and the Hugo component when hugo is installed (or HUGO=/path/to/hugo) npm run report -- map.json report.md # the text report from the command line npm run hugo:example # the Hugo example site (needs hugo) npm run build # static site in dist/: the app, docs/, llms.txt, llms-full.txt, hugo-example/ (needs hugo) npm run update:pivotick # install the latest Pivotick ``` Every push to `main` installs Hugo, runs the tests, builds the app, the documentation and the Hugo example site (published at `/hugo-example/`), and deploys to GitHub Pages. A weekly workflow installs the latest Pivotick, runs the tests and the build, and opens a pull request if the version changed. This documentation is written in `docs/content/*.md`. The build also writes `docs/search.json`, one entry per section, which the search box (`docs/search.js`, on the docs pages and in the app's top bar; `/` focuses it in the docs) searches in the browser. The build turns the Markdown into `docs/index.html` (static HTML, readable without JavaScript) and `llms-full.txt`, and publishes the schema and the examples next to it. ## For AI agents | File | Content | |---|---| | [`/llms.txt`](https://ecrou-exact.github.io/project-graph/llms.txt) | Short index of this documentation, in the llms.txt format. | | [`/llms-full.txt`](https://ecrou-exact.github.io/project-graph/llms-full.txt) | This whole page as Markdown. Read it before generating a map. | | [`pivograph.schema.json`](https://ecrou-exact.github.io/project-graph/docs/pivograph.schema.json) | JSON schema of the document format. | | [`examples/rulezet.json`](https://ecrou-exact.github.io/project-graph/docs/examples/rulezet.json) | A complete, checked map. | | [`examples/circl.json`](https://ecrou-exact.github.io/project-graph/docs/examples/circl.json) | An organisation and the 21 GitHub organisations it manages, with logos and GitHub facts. | | [`examples/ngsoti-soc-stack.json`](https://ecrou-exact.github.io/project-graph/docs/examples/ngsoti-soc-stack.json) | A drawn diagram: the NGSOTI SOC stack with Rulezet, in card nodes, sections and arrows, with a fixed layout. | | [`examples/minimal.json`](https://ecrou-exact.github.io/project-graph/docs/examples/minimal.json) | The smallest useful map. | | [Hugo](https://ecrou-exact.github.io/project-graph/docs/guide.html#hugo) | To add a graph to a Hugo site: procedure, front-matter reference, checklist and prompt. | To produce a map, follow [Mapping an organization](https://ecrou-exact.github.io/project-graph/docs/guide.html#mapping-an-organization), write the document per [Document format](https://ecrou-exact.github.io/project-graph/docs/guide.html#document-format), and check it against the [validation](https://ecrou-exact.github.io/project-graph/docs/guide.html#validation) rules. Output only the JSON document.