Zopra/Charts
Guide
Getting startedAll chartsWhich chart to use?Domain specific
Base charts7
XY ChartTrendCandlestickHeatmapState TimelineFlame GraphBar Gauge
Derived charts73
Time SeriesHistogramStatus HistoryStatGaugeHoneyCombTracesNode GraphHeikin-AshiRenkoKagiPoint & FigureParetoIshikawaControl ChartBland-AltmanLollipopDumbbellSlopeBumpBack-to-BackWaterfallBulletFunnelMekkoForestBarcodeBeeswarmRidgelineHorizonSpectrogramAttention MapWaffleVennUpSetWarped WaffleWord CloudGanttSankeySankey MatrixTreemap ForceChord DiagramCircosArc DiagramAlluvialSankey FunnelDendrogramIcicleSunburstSpiderNightingale RoseParallel CoordinatesOscilloscopeEfficient FrontierPhase PortraitPoincaré MapBifurcationH-R DiagramRamachandranHarkerRoot LocusRadiation PatternQuiverHodographSmith ChartEye DiagramSequence LogoTernaryTernary ContourPiperStüveTephigramChoropleth

Charts/Core charts

Node Graph

<NodeGraph /> · 13 examples

Services as circles, calls as arrows, and the health of each one drawn around its edge.

Overview

Every architecture diagram begins life as a tidy drawing on a whiteboard and ends as a lie. Somebody adds a queue, somebody else adds a cache, a third person adds a service called "misc", and nobody updates the drawing. A node graph is the architecture diagram that cannot lie, because it is built from what the system actually did.

Each node is a service, a stage or a dependency. Each edge is a call between two of them. The twist, from Grafana's Node graph panel which this chart ports, is that nodes carry numbers. Two stats sit inside the circle, a title below, and the outline is split into coloured arcs, so a node can say "98% success, 2% errors" without being clicked.

That is the whole appeal. A table of services answers "what is the error rate of checkout?". A node graph answers "which of the services that checkout depends on is the one on fire?", and gives you the other thirty at the same time.

Props

API

<NodeGraph> draws a directed graph: services and the calls between them, pipeline stages, dependencies. Nodes are circles with two stats inside, a title below and coloured arc sections around them; edges are arrows that can carry stats of their own. It is a port of Grafana's Node graph panel; see Coming from Grafana.

use zopra_charts::*;
  • <NodeGraph>
  • GraphNode
  • GraphEdge
  • Arcs and colours
  • Layouts
  • Interaction
  • Enums and types
  • Coming from Grafana

Not sure which option fits? See the guide.


<NodeGraph>

let nodes = vec![
    GraphNode::new("api").sub_title("gateway").main_stat("120 req/s").arcs([0.98, 0.02]),
    GraphNode::new("db").sub_title("postgres").main_stat("40 ms"),
];
let edges = vec![GraphEdge::new("api", "db").main_stat("12 ms")];
let health = vec![
    ArcSection::new("success", rgb(0x40a02b)),
    ArcSection::new("errors", rgb(0xd20f39)),
];

<NodeGraph nodes={nodes} edges={edges} arcs={health} />
PropTypeDefault
nodesVec<GraphNode>, Signal<Vec<GraphNode>> or your own Dataset<GraphNode>required
edgesVec<GraphEdge>, Signal<Vec<GraphEdge>> or your own Dataset<GraphEdge>required
layoutNodeLayoutLayered
zoom_modeZoomModeCooperative
arcsVec<ArcSection>: the name and colour of each arc sectionnone
node_limitusize: nodes shown at once200
formatsStatFormats: how numeric stats are writtennumbers as they are
legendbool: the arc legend below the graphtrue
on_node_clickFn(&GraphNode, &mut Window, &mut App)a details popover
on_edge_clickFn(&GraphEdge, &mut Window, &mut App)a details popover
class, key

Pass Signals when the graph changes over time: layouts are computed once per change and cached.

GraphNode

MethodEffectDefault
GraphNode::new(id)a node with this unique idtitle = id
title(text), sub_title(text)two lines under the circle
main_stat(value), secondary_stat(value)two lines inside the circle: numbers, text, bool or a StateValueempty
arcs([..])one fraction from 0 to 1 per ArcSection, in the same ordernone
color(colour)the outline colour when there are no arcsgrey
radius(px)the circle's radius40.
highlighted(bool)fills the circle in redfalse
detail(name, value)an extra row in the details popover; call it once per rownone

GraphEdge

MethodEffectDefault
GraphEdge::new(source, target)an arrow from the node with id source to the node with id target
id(text)a unique id"source→target"
main_stat(value), secondary_stat(value)a label shown when you hover the edge or one of its nodesempty
thickness(px)line width1.
color(colour)line colourgrey
dashed(bool)a dashed linefalse
highlighted(bool)red and one pixel thickerfalse
detail(name, value)an extra row in the details popovernone

Edges whose source or target id is not among the nodes are left out.

Arcs and colours

Each node's outline is drawn from its arcs values, in the order of the chart's arcs sections, clockwise from the top:

  • A value of 1 or more paints the whole outline in that section's colour.
  • Otherwise each non-zero value takes that share of the circle. Shares past 100% are cut off.
  • A node without non-zero arcs gets its color, or grey.

The legend lists the sections. Clicking a section switches to the grid layout sorted by it; click again to reverse the order.

Layouts

NodeLayoutPlacement
Layered (default)left to right in layers, so every edge points right where it can; crossings are reduced; loops are allowed
Forcea physical simulation that pulls connected nodes together and pushes others apart (Grafana's d3-force settings)
Gridrows of nodes in data order, or sorted by an arc from the legend

The buttons in the top-right corner switch layouts at any time.

Node limit. With more nodes than node_limit, the graph shows the nodes nearest the roots (nodes with no incoming edges) and collapses each unexplored branch into a "N nodes" marker. Click a marker to centre the view on that branch. In the grid layout, the limit shows a window of the sorted nodes.

Interaction

  • Pan by dragging the background. Zoom with the + and − buttons, or the mouse wheel: with ZoomMode::Cooperative, hold Ctrl (⌘ on macOS) so the page can still scroll; with Greedy, the wheel always zooms. The ⛶ button resets the view and fits the graph.
  • Hover a node to highlight it and its neighbours, dim the rest, show its full title and the stats of its edges. Hover an edge to show its stats.
  • Click a node or edge for a popover with its title, stats, arc shares and detail rows, or handle the click yourself with on_node_click / on_edge_click.

The graph starts fitted to the panel, never zoomed in past 100%.

Enums and types

TypeValues
NodeLayoutLayered (default), Force, Grid
ZoomModeCooperative (default), Greedy
ArcSectionArcSection::new(name, colour)
StatFormatsStatFormats::new() with .node_main(f), .node_secondary(f), .edge_main(f), .edge_secondary(f), each fn(f64) -> SharedString for numeric stats

Coming from Grafana

GrafanaHere
Nodes frame: id, title, subtitle, mainstat, secondarystatGraphNode::new(id), title, sub_title, main_stat, secondary_stat
arc__* fields, arc coloursarcs([..]) and the chart's arcs
color, nodeRadius, highlighted, detail__*color, radius, highlighted, detail
Edges frame: id, source, target, mainstat, secondarystatGraphEdge::new(source, target), id, main_stat, secondary_stat
thickness, color, strokeDasharray, highlighted, detail__*thickness, color, dashed, highlighted, detail
Layout algorithmlayout
Zoom modezoom_mode
Nodes / edges main and secondary stat unitformats
Node limitnode_limit
Data links in the context menuon_node_click, on_edge_click

Differences:

  • Grafana lays out every node and then applies the limit; here only the shown nodes are laid out, so a limited graph stays compact.
  • The graph starts fitted to the panel instead of at 100%.
  • Node icons and fixed node positions are not available.

Run them

13 examples

Press Run to load an example into the live render at the top of the page. Open one to read its code.

How to read it

  1. 01Start at the left in the layered layout: roots (nodes nothing calls) are there, and calls go rightward.
  2. 02Scan the outlines for colour. An arc split between success and error colours tells you the share of each before you read a number.
  3. 03Hover a node: it and its neighbours stay bright, the rest dim, and the stats on the connected edges appear. Hover an edge for its own stats.
  4. 04Click a node or edge for a popover with its stats and extra rows. Drag the background to pan, use + and - to zoom (hold Ctrl with the wheel), and the layout buttons to try Force or Grid.

When to use it, and when not to

Reach for it when

  • You want to see how a set of services depend on each other, with health or load on each, in one view.
  • The question is "where is the problem?": a red arc on a node, and the edges that touch it, point at the culprit.
  • The data is a graph: pipeline stages, dependency trees, data lineage, anything with a source and a target.
  • You want a starting map for an incident, where hovering a node dims everything except it and its neighbours.

Skip it when

  • The data is about one request in time. Use Traces, where the x axis is a clock.
  • You need to compare flow volumes accurately. Edge thickness is a hint. A Sankey gives width proportional to flow.
  • The graph has thousands of nodes and no obvious roots. The chart caps what it draws (200 by default) and the rest will be collapsed into markers.
  • The relationships are dense and between a small set of groups. A Chord Diagram shows that better.

The math behind it

Layered layout

The default layout turns the graph into columns, so edges point rightward wherever they can. First, cycles are broken: a depth-first search reverses any edge that points back to a node on the current path, and self-loops are ignored. On the resulting acyclic graph each node gets a layer equal to the length of the longest path leading into it.

Here L(v)L(v)L(v) is the layer of node vvv and pred(v)\mathrm{pred}(v)pred(v) its predecessors. Edges that span more than one layer get invisible dummy nodes in the layers between, so they take part in ordering.

L(v)=max⁡u∈pred(v)(L(u)+1),L(v)=0 if no predecessorL(v) = \max_{u \in \mathrm{pred}(v)} \bigl(L(u) + 1\bigr), \qquad L(v) = 0 \text{ if no predecessor}L(v)=u∈pred(v)max​(L(u)+1),L(v)=0 if no predecessor

Reducing crossings and placing nodes

Within each layer, the order of nodes is chosen by 12 alternating sweeps. On a downward sweep every node is sorted by the mean position of its neighbours in the layer above. On an upward sweep, by those below. This barycentre heuristic is the classic cheap way to cut edge crossings. It does not find the minimum, and does not claim to.

Vertical coordinates start evenly spaced at a step of 140 px (a 100 px circle plus a 40 px gap) and are then pulled toward the mean of their neighbours for 8 passes, with the constraint that nodes in a layer cannot overlap. Columns are 160 px apart. The result is centred on the origin.

xv=160 L(v),key(v)=1∣N(v)∣∑u∈N(v)order(u)x_v = 160\,L(v), \qquad \mathrm{key}(v) = \frac{1}{|N(v)|}\sum_{u \in N(v)} \mathrm{order}(u)xv​=160L(v),key(v)=∣N(v)∣1​u∈N(v)∑​order(u)

Force layout, grid and arcs

Force runs a physical simulation for 300 ticks with Grafana's d3-force settings: links pull their endpoints toward a rest length of 150 px with strength 0.5, a collision radius of 100 px pushes overlapping nodes apart, and a velocity decay of 0.6 damps the motion. Roots and their direct children start on a coarse tree and keep their horizontal position, which is why force layouts here still flow left to right. Grid ignores edges and fills rows in data order.

A node's outline is divided by its arcs values a1,…,aka_1, \dots, a_ka1​,…,ak​, clockwise from the top. If any value is 1 or more, the whole outline takes that colour. Otherwise each non-zero aia_iai​ gets that share of the circle and anything beyond 100% in total is cut off.

sweepi=360∘⋅ai,∑iai≤1\text{sweep}_i = 360^\circ \cdot a_i, \qquad \textstyle\sum_i a_i \le 1sweepi​=360∘⋅ai​,∑i​ai​≤1

Real life usage

Nobody calls the thing that everybody calls

Priya joined a logistics company as its fourth platform engineer, inheriting a system of forty-odd services and a wiki page titled "Architecture (current)" that had last been edited by someone who had since left to open a bakery. Her first week was supposed to be reading. She spent it asking people what depended on what, and getting forty-odd slightly different answers.

The question became urgent in her third week, when the parcel-routing service started returning errors for about three percent of requests. Three percent is the dangerous number: too small to page anyone, large enough that somebody's delivery is a week late.

She did not need another dashboard of per-service charts. She needed the shape of the failure: which of the forty services were affected, in what order, and which one they all seemed to lean on. That is a question about edges, not values.

She exported the service-to-service call counts and error rates from the metrics store and drew them with <NodeGraph> in a small desktop tool: nodes with the request rate inside, arcs for success and error share, edges labelled with latency. The layered layout put the public API on the left and the databases on the right, and made the shape plain at once.

Three services had a faint red arc. Every edge feeding them came from the routing service, and the routing service had one edge, thick and dark, into a node called geocoder-legacy whose outline was almost half red. It was the only service in the graph that the wiki did not mention.

The geocoder was running on a single instance and timing out under load. Adding two replicas dropped routing errors from 3.1% to 0.2% by the afternoon. The wiki page was updated, and the bakery was informed that the old owner was off the hook.

Priya's graph now lives on a side monitor. It has, as far as she can tell, the same effect as a smoke detector: nobody looks at it until it is the only thing that matters.

Pitfalls

  • Layout is not meaning. In the layered layout, position along the x axis is the number of hops, not time or size. In the force layout, distance is only loosely related to anything.
  • Cycles are drawn, but bent. The layered layout reverses back-edges for placement only. A call loop will still be an arrow in the right direction, but it may cross the whole graph to get there.
  • Arcs that sum past 1. Each value is a share, and shares past 100% in total are cut off. If your two arcs are "success" and "errors", make sure they add to at most 1.
  • The node limit. With more than node_limit nodes, only those nearest the roots are laid out and the rest become "N nodes" markers. Check for them before concluding a service is not there.
  • Dangling edges. An edge whose source or target id is not among the nodes is silently left out.

From around the web

Watch and read

YouTubeForce directed network graph - D3.js interactive data visualizationPiotr PaszekShows an interactive force-directed node-link graph. Made with D3 rather than this library.

Keep going

Related charts

TracesWhen you need one request in time rather than the whole map.SankeyWhen edge widths must be proportional to volume.Chord DiagramWhen a few groups are densely connected and you want to compare flows between them.
PreviousTracesNextHeikin-Ashi

On this page

  • Live
  • Overview
  • API
  • Examples
  • How to read it
  • When to use it
  • The math
  • Story
  • Pitfalls
  • Watch and read
  • Related