Every network chart takes a perspective prop. It draws the finished layout in a parallel projection — isometric, 2:1 pixel, dimetric, military or cabinet — without changing the layout. Every mark becomes a piece with a little thickness, edges ride just above the ground and cast a shadow, and hover, keyboard focus, annotations, the accessible table and server rendering all follow the projected marks. It is a projection, not a 3D engine: there is no WebGL and nothing new to learn about the chart underneath.
JSX
import { ForceDirectedGraph } from "semiotic/network" <ForceDirectedGraph nodes={nodes} edges={edges} perspective="isometric" />
Seven charts, one prop
Switch the preset to redraw all seven charts, or drag the thickness to see how much depth the pieces carry. Presets animate because each config sets transition; the layouts never re-run.
ForceDirectedGraph · elevation by tierSankeyDiagram · slabs and lifted bandsTreemap · extruded leavesTreeDiagram · tokens and ground-aligned labelsCirclePack · stacked levelsChordDiagram · thick arcs and ribbonslineageDagLayout recipe · hulls, arrows and node cards follow Presets
| Value | Ground diamond | Use it for |
|---|
"isometric" | true 30° isometric (0.577) | The classic architecture-diagram look |
"pixel" | 2:1 (0.5) | Pixel-art and game-map conventions |
"dimetric" | flatter (0.342) | More of the plan visible, less foreshortening |
"military" | no foreshortening | Keeping ground shapes and angles true |
"cabinet" | oblique, depth at half scale | Front-facing elevations such as org charts |
"flat" | identity | The default; no projection work at all |
An object form starts from a preset and adjusts it: rotation andtilt in degrees, verticalScale, and fit ("contain" scales the projected scene down, never up, and centers it).
Pieces: thickness, tokens, slabs and prisms
A projected outline with no depth reads as a skewed picture, not as an object. So every piece gets thickness — 6 layout px by default — and shows it as shaded side walls. Circles and symbols become tokens: discs lying on the ground at their pixel size, with a visible rim. Rects (sankey nodes, treemap and partition cells), chord arcs and circle-pack circles become slabs with exact hit areas, and each level of a treemap or circle pack sits on its parent's top surface, so nesting reads as terraces. Edges and bands ride one thickness above the surface their nodes stand on and cast a soft shadow onto it; the gap between line and shadow is what makes them read as lifted.
Set thickness: 0 for flat pieces, edgeShadow: false (or{ color, opacity }) to change the shadow, andmarks: "billboard" to stand circles and symbols upright at their size instead. Pictogram glyphs always stand. marks: "extrude" turns leaf rects into prisms whose height comes from extrude — a constant, a field name, or a function — while containers stay on the ground beneath them. Standing marks are painted back to front.
JSX
<Treemap data={org} perspective={{ type: "isometric", thickness: 10 }} /> <Treemap data={org} perspective={{ type: "isometric", marks: "extrude", extrude: "value" }} /> <ForceDirectedGraph nodes={nodes} edges={edges} perspective={{ type: "isometric", marks: "billboard" }} />
Elevation is the one new encoding
elevation lifts nodes by a constant, a field or a callback. Data values scale so the largest lifts 48px unless you set elevationScale. Lifted marks get a dashed drop line and a soft shadow at their ground point so position stays readable; turn them off with elevationGuides: false. Everything else about perspective is presentation.
Ground, plates and regions
ground.grid draws a projected grid over the plot, and ground.platea slab under the whole scene. regions draw labelled plates under groups of node ids — as thick as the pieces unless you set depth, optionally raised (elevation) — and seat their members on the plate top. Region labels run along the plate's back edge. Edges attach to the surface their nodes stand on, so a link between plates climbs from one to the other.
JSX
perspective={{ type: "isometric", ground: { grid: { step: 32 } }, regions: [ { id: "dmz", label: "INTERNET FACING", nodes: ["web-1", "web-2"], depth: 10 }, { id: "private", label: "PRIVATE", nodes: ["db-1"], elevation: 40, depth: 8 } ], edges: { route: "orthogonal-rounded" }, labels: { mode: "ground" } }}
edges.route replaces straight edges with orthogonal ground routes, so bends follow the two ground axes. labels.mode: "ground" rotates node labels along a ground axis (labels.axis). See both inIsometric Infrastructure.
Pictograms
semiotic/network/perspective ships isometric pictograms — box,server, database, cylinder, tile,cloud and pin — plus builders (isoBox,isoStack, isoCylinder, …) and the accessiblePerspectiveToggle used above. Faces are lit and shaded versions of each node's color, so pictograms follow colorBy, themes and selection dimming. Pass them to any chart with perspective.glyph, or emit glyph nodes from a custom layout.
JSX
import { isometricGlyphs } from "semiotic/network/perspective" <ForceDirectedGraph nodes={services} edges={calls} perspective={{ type: "isometric", glyph: (d) => isometricGlyphs[d.kind] }} />
Custom layouts and overlays
A custom layout keeps returning flat plot coordinates; the frame projects its scene nodes, edges, labels and HTML marks. ctx.perspective holds the active config (ornull) if the layout wants to adapt. SVG backgrounds andoverlays are the layout's own drawing, so they need to say how they follow the projection. Setting perspective: "ground" on the result lays all of them on the ground. For a mix, wrap each piece in a placement component and setperspective: "manual":
<NetworkPerspectiveGround z={0}> lays flat geometry on the ground: hulls, lanes, grids. z="top" puts it at piece height, where edges ride, which suits arrowheads.<NetworkPerspectiveBillboard x y> moves upright content with its anchor point: node cards, badges, edge labels. Add onGround to lay it flat at its pixel size instead, like an icon on a token.- Anything left unwrapped, such as a legend, stays in plot space.
The projection is fitted to the plot from your scene marks. When decorations reach past them, such as hulls padded around their nodes, bands wider than their cells, or headers beside a matrix, list them in perspectiveBounds so the fit keeps them in view. A box { x, y, width, height } lies on the ground. A point withextent: [left, right, top, bottom] is upright content that many pixels around its projected anchor.
On a flat chart, the components render their children unchanged. In development, a chart warns once if a projected layout returns decorations without saying how they follow. Every built-in recipe already does this: lineage DAGs, dagre, Mermaid flowcharts, net ensembles, packed cluster matrices, transit maps, adjacency flows, dependency forests and GoFish display lists all draw their decorations in place under a projection. Mermaid goes further and emits solid pieces, so its nodes get thickness too.
JSX
import { NetworkPerspectiveBillboard, NetworkPerspectiveGround } from "semiotic/network" const zonesLayout = (ctx) => ({ sceneNodes, sceneEdges, backgrounds: ( <NetworkPerspectiveGround> {zones.map((z) => <rect key={z.id} {...z.box} fill={z.color} opacity={0.15} />)} </NetworkPerspectiveGround> ), // Zones reach past the nodes: keep them in the fitted plot. perspectiveBounds: zones.map((z) => z.box), overlays: ( <> {badges.map((b) => ( <NetworkPerspectiveBillboard key={b.id} x={b.x} y={b.y}> <text x={b.x} y={b.y - 14} textAnchor="middle">{b.text}</text> </NetworkPerspectiveBillboard> ))} <Legend /> </> ), perspective: "manual" })
For full control, overlay components read the fitted projection withuseNetworkPerspective() and place content with project(x, y, z).
JSX
import { useNetworkPerspective } from "semiotic/network" function Beacon({ x, y }) { const frame = useNetworkPerspective() const [px, py] = frame.project(x, y, frame.thickness ?? 0) return <circle cx={px} cy={py} r={4} fill="red" /> }
Server rendering and loading
renderToStaticSVG, renderChart and MCP render every perspective feature synchronously from a JSON config — use the string or object form; callbacks are React-only. In the browser, grids, plates, regions, routing and extrusion load in a small chunk the first time a chart asks for them, then repaint. When hydrating server-rendered charts that use them, await preloadNetworkPerspectiveExtras() first so the first client render matches.
JSON
{ "component": "ForceDirectedGraph", "props": { "nodes": [{ "id": "a", "tier": 1 }, { "id": "b", "tier": 0 }], "edges": [{ "source": "a", "target": "b" }], "perspective": { "type": "isometric", "elevation": "tier", "ground": { "grid": true } } } }
When perspective is honest
Projection compresses depth and distorts distance and area. Use it where position is categorical or topological — architecture and infrastructure maps, lineage and DAGs, org charts, matrices — and avoid it where distance or area encodes a measured quantity. Sankey band widths survive projection; the lengths between nodes do not. The prop is calledperspective rather than projection because geo charts already useprojection for map projections.