NetworkGraph
Nodes and links laid out by a force simulation run on the server, coloured by group and sized by their links.
Demo
| Node | Group | Linked to |
|---|---|---|
| Ana | Design | Rui, Eva, Joe, Ola |
| Rui | Design | Ana, Eva, Kim |
| Eva | Design | Ana, Rui |
| Joe | Engineering | Ana, Kim, Lea |
| Kim | Engineering | Rui, Joe, Max, Noa |
| Lea | Engineering | Joe, Max, Noa |
| Max | Engineering | Kim, Lea, Noa |
| Noa | Engineering | Max, Lea, Kim, Uma |
| Ola | Sales | Pia, Quinn, Ana |
| Pia | Sales | Ola, Quinn |
| Quinn | Sales | Pia, Ola, Sam |
| Sam | Support | Quinn, Tia, Uma |
| Tia | Support | Sam, Uma |
| Uma | Support | Tia, Sam, Noa |
| Node | Linked to |
|---|---|
| web | api |
| api | web, auth, db, cache, mail |
| auth | api, db |
| db | api, auth |
| cache | api |
| api |
The second hides its labels until a node is hovered.
Get it
It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.
$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/network_graph.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/network_graph.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/network_graph.qwe
let network_graph_demo_people = [ { id: "ana", label: "Ana", group: "Design" }, { id: "rui", label: "Rui", group: "Design" }, { id: "eva", label: "Eva", group: "Design" }, { id: "joe", label: "Joe", group: "Engineering" }, { id: "kim", label: "Kim", group: "Engineering" }, { id: "lea", label: "Lea", group: "Engineering" }, { id: "max", label: "Max", group: "Engineering" }, { id: "noa", label: "Noa", group: "Engineering" }, { id: "ola", label: "Ola", group: "Sales" }, { id: "pia", label: "Pia", group: "Sales" }, { id: "quinn", label: "Quinn", group: "Sales" }, { id: "sam", label: "Sam", group: "Support" }, { id: "tia", label: "Tia", group: "Support" }, { id: "uma", label: "Uma", group: "Support" },]let network_graph_demo_links = [ { from: "ana", to: "rui", value: 3 }, { from: "ana", to: "eva", value: 2 }, { from: "rui", to: "eva" }, { from: "ana", to: "joe", value: 2 }, { from: "rui", to: "kim" }, { from: "joe", to: "kim", value: 3 }, { from: "joe", to: "lea", value: 2 }, { from: "kim", to: "max" }, { from: "lea", to: "max", value: 3 }, { from: "max", to: "noa", value: 2 }, { from: "noa", to: "lea" }, { from: "kim", to: "noa" }, { from: "ola", to: "pia", value: 3 }, { from: "pia", to: "quinn", value: 2 }, { from: "ola", to: "quinn" }, { from: "ola", to: "ana" }, { from: "quinn", to: "sam" }, { from: "sam", to: "tia", value: 3 }, { from: "tia", to: "uma", value: 2 }, { from: "uma", to: "sam" }, { from: "uma", to: "noa", value: 2 },]view NetworkGraphDemo() = <div.demo-stack> <NetworkGraph label="Who works with whom" show_label={true} nodes={network_graph_demo_people} links={network_graph_demo_links}/> <div style="max-width: 420px"> <NetworkGraph label="Service calls" show_label={true} aspect={1.3} labels={false} nodes={[{ id: "web" }, { id: "api" }, { id: "auth" }, { id: "db" }, { id: "cache" }, { id: "mail" }]} links={[{ from: "web", to: "api", value: 5 }, { from: "api", to: "auth", value: 2 }, { from: "api", to: "db", value: 4 }, { from: "api", to: "cache", value: 3 }, { from: "auth", to: "db" }, { from: "api", to: "mail" }]}/> </div> <p.demo-note>The second hides its labels until a node is hovered.</p> </div>Source
ui/network_graph.qwe
// NetworkGraph: things and the links between them (people and who works with whom,// services and their calls), laid out on the server by a force simulation run a// fixed number of steps: linked nodes pull together, all nodes push apart, so// clusters show. The same data always gives the same picture. CSS only: hovering a// node or link names it; screen readers get each node's links as a table.//// <NetworkGraph label="Team" nodes={[{ id: "ana", label: "Ana", group: "Design" }, { id: "rui", label: "Rui" }]}// links={[{ from: "ana", to: "rui", value: 3 }]}/>//// nodes: a list of { id, label, group } (label defaults to the id); nodes in the// same group share a colour (eight, then they repeat), with a legend. links: a// list of { from, to, value }, naming nodes by id; value (1 by default) makes a// link thicker and pulls harder. A node is as big as its number of links. steps:// how many steps the simulation runs (300; more settles further, and takes longer// on the server). aspect: the drawing's width over its height (1.6). labels: each// node's label under it (true; otherwise on hover only). Dragging nodes is not// offered: the layout is the server's. label: what the chart shows; it names it for// screen readers and captions their table. show_label also writes it above.import ui/theme.qwe use ui_clslet network_graph_css = css`.ui-network { --ui-network-1: light-dark(#2a78d6, #3987e5); --ui-network-2: light-dark(#eb6834, #d95926); --ui-network-3: light-dark(#1baf7a, #199e70); --ui-network-4: light-dark(#eda100, #c98500); --ui-network-5: light-dark(#e87ba4, #d55181); --ui-network-6: light-dark(#008300, #008300); --ui-network-7: light-dark(#6250d6, #9085e9); --ui-network-8: light-dark(#e34948, #e66767); display: grid; gap: 10px; min-width: 0; margin: 0; font: 12px/1.3 var(--ui-font); color: var(--ui-fg); }.ui-network-title { margin: 0; font-size: 14px; font-weight: 600; }.ui-network-s1 { --c: var(--ui-network-1); } .ui-network-s2 { --c: var(--ui-network-2); } .ui-network-s3 { --c: var(--ui-network-3); }.ui-network-s4 { --c: var(--ui-network-4); } .ui-network-s5 { --c: var(--ui-network-5); } .ui-network-s6 { --c: var(--ui-network-6); }.ui-network-s7 { --c: var(--ui-network-7); } .ui-network-s8 { --c: var(--ui-network-8); }.ui-network-legend { display: flex; flex-wrap: wrap; gap: 4px 16px; margin: 0; padding: 0; list-style: none; color: var(--ui-muted); font-size: 13px; }.ui-network-legend li { display: inline-flex; align-items: center; gap: 6px; }.ui-network-key { width: 10px; height: 10px; border-radius: 50%; background: var(--c); flex: none; }.ui-network-svg { display: block; width: 100%; height: auto; overflow: visible; }.ui-network-link { stroke: var(--ui-strong); stroke-linecap: round; stroke-opacity: .8; }.ui-network-link:hover { stroke: var(--ui-fg); stroke-opacity: 1; }.ui-network-node circle { fill: var(--c); stroke: var(--ui-bg); stroke-width: .6; }.ui-network-node text { fill: var(--ui-fg); font-family: var(--ui-font); font-size: 2.3px; text-anchor: middle; paint-order: stroke; stroke: var(--ui-bg); stroke-width: .6px; pointer-events: none; }.ui-network-node:hover circle { stroke: var(--ui-fg); stroke-width: .8; }.ui-network-node:hover text { font-weight: 700; }.ui-network-nolabels .ui-network-node text { visibility: hidden; }.ui-network-nolabels .ui-network-node:hover text { visibility: visible; }.ui-network-plain { --c: var(--ui-network-1); }`// A number for SVG, to two decimals.fn network_graph_num(n: num) -> str { let r = round(n * 100) / 100 r == floor(r) ? str(floor(r)) : str(r)}// Positions { xs, ys } for n nodes joined by links ({ s, t, w } by index), in a box w by h:// Fruchterman and Reingold's forces run `steps` times from a spiral start, then fitted// to the box. Deterministic, so the server draws the same picture each time.fn network_graph_layout(n: int, links: list, w: num, h: num, steps: int) -> map { if n == 0 { return { xs: [], ys: [] } } let k = sqrt(w * h / n) * 0.6 let xs = for i in range(n) => w / 2 + 4 * sqrt(i + 0.5) * math.cos(i * 2.39996) let ys = for i in range(n) => h / 2 + 4 * sqrt(i + 0.5) * math.sin(i * 2.39996) let step = 0 while step < steps { let heat = w / 10 * (1 - step / steps) + 0.05 let dx = for i in range(n) => 0 let dy = for i in range(n) => 0 for i in range(n) { for j in range(i + 1, n) { let ex = xs[i] - xs[j] let ey = ys[i] - ys[j] let d = sqrt(ex * ex + ey * ey) let dist = d < 0.01 ? 0.01 : d let f = k * k / dist / dist dx = set(dx, i, dx[i] + ex * f) dy = set(dy, i, dy[i] + ey * f) dx = set(dx, j, dx[j] - ex * f) dy = set(dy, j, dy[j] - ey * f) } } for l in links { let ex = xs[l.s] - xs[l.t] let ey = ys[l.s] - ys[l.t] let d = sqrt(ex * ex + ey * ey) let f = d * l.w / k dx = set(dx, l.s, dx[l.s] - ex * f) dy = set(dy, l.s, dy[l.s] - ey * f) dx = set(dx, l.t, dx[l.t] + ex * f) dy = set(dy, l.t, dy[l.t] + ey * f) } for i in range(n) { // A little pull to the middle keeps parts that are not linked from drifting off. let gx = dx[i] - (xs[i] - w / 2) * 0.08 * k let gy = dy[i] - (ys[i] - h / 2) * 0.08 * k let len0 = sqrt(gx * gx + gy * gy) let move = len0 < heat ? 1 : heat / len0 xs = set(xs, i, xs[i] + gx * move) ys = set(ys, i, ys[i] + gy * move) } step += 1 } // Fit what came out into the box, keeping its shape, with a margin. let x0 = min(xs) let x1 = max(xs) let y0 = min(ys) let y1 = max(ys) let margin = 8 let sx = x1 - x0 > 0 ? (w - 2 * margin) / (x1 - x0) : 1 let sy = y1 - y0 > 0 ? (h - 2 * margin) / (y1 - y0) : 1 let s = sx < sy ? sx : sy let ox = (w - (x1 - x0) * s) / 2 let oy = (h - (y1 - y0) * s) / 2 { xs: xs | map((x) => ox + (x - x0) * s), ys: ys | map((y) => oy + (y - y0) * s) }}view NetworkGraph(nodes: list, links: list, steps: int = 300, aspect: num = 1.6, labels: bool = true, label: str = "Network graph", show_label: bool = false, class: str? = null) { style global network_graph_css let n = len(nodes) let ids = nodes | map((nd) => str(nd.id)) let name = (i) => str(nodes[i].label ?? nodes[i].id) let flows = links | filter((l) => index_of(ids, str(l.from)) != null && index_of(ids, str(l.to)) != null && str(l.from) != str(l.to)) | map((l) => { s: index_of(ids, str(l.from)), t: index_of(ids, str(l.to)), w: l.value != null && l.value > 0 ? l.value : 1 }) let w = 100 * (aspect > 0.2 ? aspect : 1.6) let h = 100 let pos = network_graph_layout(n, flows, w, h, steps) let degree = for i in range(n) => len(flows | filter((l) => l.s == i || l.t == i)) let groups = unique(nodes | filter((nd) => nd.group != null) | map((nd) => str(nd.group))) let tone = (i) => nodes[i].group == null ? "ui-network-plain" : `ui-network-s${(index_of(groups, str(nodes[i].group)) ?? 0) % 8 + 1}` let radius = (i) => 1.4 + sqrt(degree[i]) * 0.65 let top = max([1] + (flows | map((l) => l.w))) let near = (i) => flows | filter((l) => l.s == i || l.t == i) | map((l) => name(l.s == i ? l.t : l.s)) <figure class={ui_cls(["ui-network", !labels && "ui-network-nolabels", class])} aria-label={label}> {show_label && <figcaption.ui-network-title>{label}</figcaption>} {len(groups) > 0 && <ul.ui-network-legend aria-hidden="true"> {for [g, nm] in enumerate(groups) => <li><span class={`ui-network-key ui-network-s${g % 8 + 1}`}></span>{nm}</li>} </ul>} <svg.ui-network-svg viewBox={`0 0 ${network_graph_num(w)} ${h}`} aria-hidden="true"> {for l in flows => <line.ui-network-link x1={network_graph_num(pos.xs[l.s])} y1={network_graph_num(pos.ys[l.s])} x2={network_graph_num(pos.xs[l.t])} y2={network_graph_num(pos.ys[l.t])} stroke-width={network_graph_num(0.3 + 0.9 * l.w / top)}> <title>{name(l.s)} – {name(l.t)}{l.w != 1 ? `: ${l.w}` : ""}</title> </line>} {for i in range(n) => <g class={`ui-network-node ${tone(i)}`} data-id={ids[i]}> <circle cx={network_graph_num(pos.xs[i])} cy={network_graph_num(pos.ys[i])} r={network_graph_num(radius(i))}> <title>{name(i)}{nodes[i].group != null ? ` (${nodes[i].group})` : ""}: {degree[i]} link{degree[i] == 1 ? "" : "s"}</title> </circle> <text x={network_graph_num(pos.xs[i])} y={network_graph_num(pos.ys[i] + radius(i) + 2.5)}>{name(i)}</text> </g>} </svg> <table.ui-sr> <caption>{label}</caption> <thead><tr><th scope="col">Node</th>{len(groups) > 0 && <th scope="col">Group</th>}<th scope="col">Linked to</th></tr></thead> <tbody> {for i in range(n) => <tr> <th scope="row">{name(i)}</th> {len(groups) > 0 && <td>{nodes[i].group ?? ""}</td>} <td>{len(near(i)) > 0 ? join(near(i), ", ") : "None"}</td> </tr>} </tbody> </table> </figure>}Related
AreaChartFilled areas for one or more series, overlapping or stacked into a total, with a hover crosshair listing every value.6 views · 8 downloadsBarChartBars up or across, one series or several grouped or stacked, on round ticks, with values on hover and a table for screen readers.7 views · 7 downloadsBollingerChartPrices with Bollinger bands of a period and a number of deviations, closes outside them marked, as a line or candles.7 views · 5 downloadsBoxPlotQuartiles, median, whiskers and outliers for each group, worked out from the raw values, with the five numbers on hover.5 views · 7 downloadsBrushChartA large chart of a range with an overview below, where a window dragged or moved by keys picks the range shown.6 views · 5 downloadsBubbleChartPoints placed by two numbers and sized by a third, in coloured groups, labelled when big enough, with all three numbers on hover.7 views · 8 downloads
Thread
Thread 0 posts
On NetworkGraph: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.