Porphyry.js v1.6.2

GitHub
LAYOUT
CENTER EDGE
SPACING
0.6×
THEME
× px

Porphyry.js

A lightweight, zero-dependency JavaScript library for rendering interactive mind maps as SVG. Named after Porphyry of Tyre, the ancient philosopher who introduced the hierarchical tree of categories.

v1.6.2 Zero dependencies SVG-based MIT license

Quick Start

Want to see it in action first? Check out the live demo →

Via CDN (recommended)

The fastest way to get started — no download or build step needed:

<!-- Latest release via jsDelivr -->
<script src="https://cdn.jsdelivr.net/gh/antoniu86/porphyry.js@latest/porphyry.min.js"></script>

<!-- Pin to a specific version -->
<script src="https://cdn.jsdelivr.net/gh/antoniu86/porphyry.js@v1.6.2/porphyry.min.js"></script>

Self-hosted

Download porphyry.min.js (or porphyry.js for the commented source) and include it directly:

<script src="porphyry.min.js"></script>

Example

<!-- Include from CDN -->
<script src="https://cdn.jsdelivr.net/gh/antoniu86/porphyry.js@v1.6.2/porphyry.min.js"></script>

<!-- Give it a container -->
<div id="map" style="width:100%;height:500px"></div>

<script>
  const map = new Porphyry('#map');
  map.render({
    topic: 'My Topic',
    children: [
      { topic: 'Branch A' },
      { topic: 'Branch B', children: [
        { topic: 'Sub-node' }
      ]}
    ]
  });
</script>
The container must have an explicit width and height — Porphyry fills 100% of it. Set these via CSS or inline styles.

Data Format

Porphyry accepts a plain JSON object. Every node has a topic and an optional children array. Nesting can go as deep as needed.

{
  topic: "Root Subject",           // required — label text
  url:   "https://example.com",   // optional — makes node clickable (opens URL)
  onclick: function(node) { },   // optional — custom click handler (url takes priority)
  direction: "left",              // optional — "left"|"right", auto otherwise
  children: [
    {
      topic: "First branch",
      children: [
        { topic: "Leaf node" },
        { topic: "Linked leaf", url: "https://..." }
      ]
    },
    { topic: "Second branch" }
  ]
}

Node fields

FieldTypeDescription
topicstringNode label. Long text wraps automatically within the configured max width.
urlstring?If present, the node becomes clickable and opens this URL in a new tab. A small ↗ icon appears inside the node. Takes priority over onclick.
onclickfunction?A JS function called when the node is clicked. Receives the node object as its argument. Ignored if url is also set. A ▶ icon appears inside the node.
direction"left"|"right"?Pin a root-level child to a specific side. Only respected on direct children of the root in horizontal layouts. Ignored in vertical layouts.
childrenNode[]?Child nodes. Omit or leave empty for leaf nodes.

Constructor

new Porphyry(selector, options)
ParameterTypeDescription
selectorstring | ElementA CSS selector string or a DOM element to render into.
optionsobject?Optional configuration. See Options reference below.

Options Reference

Layout

OptionDefaultDescription
layout"auto"Direction mode. One of "auto", "left", "right", "down", "up". See Layout Modes below.
centerEdge"side"Where edges connect on the center node in horizontal layouts. "side" exits the left/right walls. "vertical" fans edges out from the top or bottom center — top vs bottom is chosen automatically per branch based on vertical position. Nodes whose vertical overlap with the root falls within the fanAlignThreshold band automatically draw from the side instead, keeping the fan shape clean. With "vertical" the center node's width no longer affects branch placement, so it can grow wide freely. Has no effect on "up"/"down" layouts.
fanAlignThreshold10Extra pixel buffer (px) used when centerEdge: "vertical" to decide whether a first-level node is close enough to the root's horizontal center to draw its edge from the side instead of the top/bottom fan. The detection zone is (root.height / 2) + (node.height / 2) + fanAlignThreshold. Increase to widen the side-exit band; set to 0 for exact edge-to-edge overlap only.
theme"classic"Visual theme controlling node shapes, fills, borders and edge anchors. See Themes below.
fitPadding20Pixels of padding around the graph when auto-fitting to the container.
lineHeight1.45Line height multiplier for wrapped text.
spacing1Spacing multiplier applied to all node distances before depth-adaptive scaling. Any value from 0.1 (extremely compact) to 2.0 (very spread out). 1 is the default.

Spacing (horizontal layouts)

OptionDefaultDescription
branchSpacingX220Horizontal gap (px) between the center node and depth-1 branches. Scales down automatically for deep trees.
subSpacingX170Horizontal gap (px) between sub-levels (depth ≥ 2). Also auto-scales.
verticalSpacing50Minimum vertical gap (px) between sibling nodes.

Spacing (vertical layouts)

OptionDefaultDescription
verticalSpacingY60Vertical gap (px) between depth levels in up/down layouts.
horizontalSpacing30Horizontal gap (px) between sibling subtrees in vertical layouts.

Center node

OptionDefaultDescription
center.fontSize17
center.fontWeight"700"
center.paddingX / paddingY28 / 16
center.maxWidth240Max node width (px) before text wraps to next line.
center.radius12Corner radius. 99 = pill (auto height/2 when single-line).
center.bgColor"#1A1F2E"Node background color.
center.borderfalseBorder style. false = none. true or 'around' = full rect border. 'bottom'/'top'/'left'/'right' = single edge. Space-separated for multiple: 'top bottom'. Overrides the theme default when set.
center.borderColornullBorder color. null = use bgColor.
center.fontColor"#FFFFFF"Text color.
center.shadowColor"rgba(0,0,0,0.35)"Drop shadow color.

Branch nodes (depth 1)

OptionDefaultDescription
branch.fontSize14
branch.fontWeight"600"
branch.paddingX / paddingY18 / 10
branch.maxWidth200Max node width (px) before text wraps.
branch.radius99Corner radius. 99 = pill (auto height/2 when single-line).
branch.bgColornullNode background color. null = use color palette.
branch.borderfalseBorder style. false = none. true or 'around' = full rect border. 'bottom'/'top'/'left'/'right' = single edge. Space-separated for multiple: 'top bottom'. Overrides the theme default when set.
branch.borderColornullBorder color. null = use palette color.
branch.fontColor"#FFFFFF"Text color.
branch.shadowColornullDrop shadow color. null = no shadow.

Leaf nodes (depth ≥ 2)

OptionDefaultDescription
leaf.fontSize13
leaf.fontWeight"500"
leaf.paddingX / paddingY14 / 7
leaf.maxWidth170Max node width (px) before text wraps.
leaf.radius3Corner radius.
leaf.bgColornullNode background color. null = use palette color (tinted at low opacity in classic/underline). Set to 'transparent' or 'none' to remove the background entirely while keeping any border.
leaf.border"bottom"Border style. false = none. true or 'around' = full rect border. 'bottom'/'top'/'left'/'right' = single edge. Space-separated for multiple: 'top bottom'. Overrides the theme default when set.
leaf.borderColornullBorder color. null = use palette color.
leaf.fontColor"#2D3748"Text color.

Colors & edges

OptionDefaultDescription
colors10-color paletteArray of hex color strings. Each root branch is assigned one in order; descendants inherit it.
edgeWidth.root2.5Stroke width of edges leaving the center node.
edgeWidth.branch2Stroke width of depth-1 → depth-2 edges.
edgeWidth.leaf1.5Stroke width of deeper edges.
edgeOpacity0.85Global opacity of all edges.

Interactions

All interactions are off by default for clean embedding. Opt in explicitly to what you need.

OptionDefaultDescription
interactions.panfalseEnable drag-to-pan (mouse + touch).
interactions.zoomfalseEnable scroll-wheel zoom and pinch-to-zoom.
interactions.collapsefalseShow +/− toggle buttons on nodes to expand/collapse subtrees.
interactions.hudfalseInject a zoom HUD (−, level %, +, fit) into the bottom-right of the container.
interactions.tipsfalseInject a hint bar at the bottom-center describing active interactions.
interactions.downloadfalseAdd a download-as-SVG button. When hud: true it appears inside the HUD; otherwise a standalone button is injected in the bottom-right corner.
minZoom0.08Minimum zoom scale (when zoom is enabled).
maxZoom4Maximum zoom scale (when zoom is enabled).
zoomSensitivity0.12Scroll-wheel zoom speed per tick.
showLinkIconstrueWhether to show the ↗ icon on url nodes and the ▶ icon on onclick nodes. Set to false to hide the icons while keeping click behaviour active.
fontColornullGlobal text color override. When set, overrides fontColor on all node types. Accepts any CSS color string. null = use per-node-type defaults.
animationDuration350Duration (ms) of the render fade-in animation. Set to 0 to disable.

Themes

The theme option controls node shapes, fills, borders and edge connection points across the whole map. All 9 themes work in every layout; 4 extra themes are exclusive to horizontal layouts (auto / left / right).

// Set at init time
new Porphyry('#map', { theme: 'solid' });

// Change at runtime
map.options.theme = 'baseline';
map._renderInternal(true);

Universal themes

ThemeFillBorderEdge anchor
outlinewhitecolored full border, roundedcenter (default for vertical)
solidsolid color, roundednonecenter
solid-sharpsolid color, sharp cornersnonecenter
outline-sharpwhite, sharp cornerscolored full bordercenter
minimalnonenonecenter

Horizontal-only themes

ThemeFillBorderEdge anchor
classicdark center, colored pills (depth 1), 10% tint (depth 2+)bottom line on leavesbottom for leaves, center otherwise (default for horizontal)
ghost10% tinted on all nodesnonecenter — all nodes
underline10% tinted on all nodesbottom solid linebottom — all nodes
baselinenonebottom solid linebottom — all nodes

When classic is selected with a vertical layout it automatically falls back to outline. When switching to a vertical layout in the demo, the classic, ghost, underline and baseline options are hidden from the dropdown.

Layout Modes

Set via options.layout at init time, or by mutating instance.options.layout before calling render() again.

ValueDescription
"auto"Branches are distributed evenly left and right of the center node. Explicit direction fields in node data are honoured first; the remainder are balanced.
"left"All branches grow to the left. Node direction fields are ignored.
"right"All branches grow to the right. Node direction fields are ignored.
"down"Tree grows downward. Siblings spread horizontally. All nodes use the outlined button style.
"up"Tree grows upward. Siblings spread horizontally. All nodes use the outlined button style.
In "down" and "up" modes all nodes — including the center and branches — use an outlined button style (white fill, colored border) instead of the solid-fill styles used in horizontal layouts.

Collapsible Branches

When interactions.collapse is enabled, a small circular toggle button appears at the child-facing edge of every non-root node that has children.

  • Shows when the subtree is expanded, + when collapsed.
  • Button color matches the node's branch color; hover inverts fill and icon.
  • Collapsed nodes are treated as leaves by the layout engine — the rest of the tree reflows automatically around them.
  • Button position adapts to layout direction: left/right edge in horizontal layouts, top/bottom edge in vertical layouts.
  • Collapse state is preserved across re-renders triggered by layout changes or data edits.
  • Calling render() clears all collapse state and restores the full tree.
const map = new Porphyry('#map', {
  interactions: { collapse: true }
});

map.render(data);

// Programmatically collapse a branch by its internal ID
// (IDs assigned depth-first, root = 0)
map._collapsed.add(3);
map._renderInternal(false); // re-render, no re-fit

Methods

MethodDescription
render(data)Parse data, lay out and draw the full tree. Clears all collapse state. Automatically calls fit() after the first paint.
fit()Scale and translate the graph so it fits neatly inside the container, respecting fitPadding.
reset()Reset pan and zoom to 1:1, centered.
destroy()Disconnects the internal ResizeObserver. Call when removing the container from the DOM to avoid memory leaks.
downloadSVG(filename?)Download the current mind map as a standalone SVG file. filename defaults to 'mindmap.svg'. The export strips the pan/zoom transform and recalculates a clean viewBox from content bounds.
_renderInternal(autoFit)Re-layout and redraw while preserving collapse state. Pass false to skip re-fitting (e.g. after a collapse toggle or layout switch).
_rebindInteractions()Call after mutating options.interactions at runtime. Strips old event listeners, re-attaches new ones, and updates cursor and tips text.
const map = new Porphyry('#map', {
  layout: 'auto',
  interactions: { pan: true, zoom: true, collapse: true, hud: true, tips: true, download: true },
  colors: ['#E05C5C', '#4A90D9', '#4CAF82'],
  branchSpacingX: 200,
});

map.render(myData);

// Switch to vertical layout and re-render
map.options.layout = 'down';
map.render(myData);

// Toggle pan off at runtime
map.options.interactions.pan = false;
map._rebindInteractions();

SVG Download

The downloadSVG() method exports the current mind map as a clean, self-contained SVG file that opens in browsers, Illustrator, Inkscape, and any SVG editor.

There are three ways to expose it depending on your use case:

// Option 1 — download icon appears inside the HUD alongside zoom controls
new Porphyry('#map', {
  interactions: { hud: true, download: true }
});

// Option 2 — standalone button injected in the bottom-right corner
new Porphyry('#map', {
  interactions: { download: true }
});

// Option 3 — no button, wire your own trigger anywhere on the page
document.getElementById('my-btn').addEventListener('click', () => {
  map.downloadSVG('my-diagram.svg'); // filename is optional
});
System fonts (system-ui, sans-serif) are referenced by name in the export — text renders correctly on any machine that has them, which is effectively everywhere. The pan/zoom transform is stripped so the file always opens at the correct size regardless of the viewer state.

Node Links & Actions

Any node can be made interactive with either a url or an onclick handler. If both are set, url takes priority.

url — external link

  • A small ↗ icon appears inside the node on the right side.
  • The cursor changes to a pointer on hover.
  • Clicking opens the URL in a new tab (noopener noreferrer).
  • A drag that moves more than 5 px never triggers the link, so panning over linked nodes is safe.
{ topic: "React", url: "https://react.dev" }

onclick — custom handler

  • A small ▶ icon appears inside the node on the right side.
  • The cursor changes to a pointer on hover.
  • Clicking calls your function, receiving the node object as its argument.
  • Drag-to-pan is still safe — the handler only fires on non-drag clicks.
{ topic: "Run Report", onclick: function(node) { alert(node.topic); } }

To hide the icons while keeping click behaviour, set showLinkIcons: false.

Text Wrapping

Long node labels wrap automatically. Each depth level has a configurable maxWidth. Text is measured with a hidden Canvas element before layout so node dimensions are always exact — the layout engine never needs to re-run after drawing.

  • Words are greedily packed onto lines.
  • A single word wider than maxWidth gets its own line rather than being clipped.
  • Multi-line branch nodes (depth 1) switch from a pill shape to a rounded rectangle (rx=10) automatically.
  • All vertical spacing accounts for the actual wrapped height, so nodes never overlap.

Adaptive Column Spacing

In horizontal layouts, column gaps scale down automatically as the tree gets deeper, keeping wide maps readable without manual tuning.

Max depthFactorbranchSpacingXsubSpacingX
≤ 21.00220 px170 px
30.83183 px141 px
40.63138 px107 px
50.50110 px85 px
≥ 60.4599 px77 px

The floor is 45 % of the configured defaults. Vertical layouts are unaffected.

Automatic Width Scaling

When a container has significantly more horizontal space than vertical (horizontal-to-vertical ratio > 1.5×), the library automatically scales up node maxWidth values before layout — by up to 2.5× — so text wraps less and the map fills the available width. This happens transparently on every render; no configuration is needed.