An interactive lab with Python, R and MapLibre — turn a line layer into a graph, and see how a few meters of tolerance change how many pieces the network actually has.
Published
September 13, 2026
A road network, a river system, a set of pipelines — any layer of lines is only a network once you decide which endpoints count as the same junction. Two segments that were meant to meet but miss by a few centimeters (a common noding gap in real data) become two separate, disconnected pieces of the graph unless you tell the algorithm how close is “close enough.” That single decision — a snap tolerance, in meters — can change how many disconnected pieces a network has without changing a single coordinate.
Want to explore this interactively, with an adjustable tolerance and the graph drawn directly on the map? Use the standalone tool — same idea, adjustable live. This article fixes two tolerance values (0m and 5m) side by side, in both languages, to make the underlying algorithm easy to read from top to bottom — and draws the resulting graph too, as a diagram below the map.
No file uploaded? Both cells below fall back to the same eight-line example: a closed block (four segments forming one connected loop), a fully separate cluster elsewhere on the map, and a two-segment “spur” whose first endpoint sits about 4m from the block’s corner — not exactly on it. That 4m gap is deliberately part of the example, not a mistake in it: it’s what section 3 below is about.
// `instance` (the live WebGeoDS.renderForceGraph() handle) is a plain// mutated property, not a second `mutable` -- reading/writing it below// never triggers OJS's own reactivity, unlike a `mutable diagram0m`// would have (found the hard way: referencing a mutable's current// value from inside resetNetworkArticle made THAT cell, and the reset// button built from it, rebuild every time a diagram was (re)drawn).graphPanel = (label) => {const wrap =document.createElement("div"); wrap.style.flex="1"; wrap.style.minWidth="260px";const heading =document.createElement("div"); heading.className="webgeods-label"; heading.textContent= label; wrap.appendChild(heading);const box =document.createElement("div"); box.style.cssText="height: 220px; border: 1px solid #d8cdb8; border-radius: 4px;"; wrap.appendChild(box);return { wrap, box,instance:null };}
// Same palette the standalone tool colors its own graph with// (WebGeoDS.DEFAULT_PALETTE, shared/ui.js) -- one color per connected// component, so "how many colors" is a direct visual answer to "how// many pieces."componentColor = (f) => WebGeoDS.DEFAULT_PALETTE[(f.properties.component??0) % WebGeoDS.DEFAULT_PALETTE.length]
// Draws the map + both diagrams from WHICHEVER language's result comes// in first -- called from both the Python and the R effect cells// below, so a reader who runs only one language (in either order)// still sees the map and the graphs, not a silently empty page.// Harmless if both run: the second call just redraws the same picture// the first one already drew, since Python and R compute the// identical graph.drawNetwork =async (result) => { mutable crsWarning = result.crsWarning;await sharedMap.setGeoJSON("network-from-lines-map", result.features, { paint: LINE_MAP_PAINT });await sharedMap.fitToData(result.features); graph0m.instance?.destroy(); graph0m.instance= WebGeoDS.renderForceGraph( graph0m.box, { nodes: result.at0m.nodeFeatures,links: result.at0m.edgeFeatures }, { nodeColor: componentColor,linkColor: componentColor } ); graph5m.instance?.destroy(); graph5m.instance= WebGeoDS.renderForceGraph( graph5m.box, { nodes: result.at5m.nodeFeatures,links: result.at5m.edgeFeatures }, { nodeColor: componentColor,linkColor: componentColor } );}
The map above shows where the lines are; the two diagrams below show how they’re connected — same graph the counts above describe, laid out by force instead of geography, colored by connected component. Drawn from whichever language you ran (if you run both, the second one just redraws the same picture — Python and R agree on the graph, so there’s nothing a second drawing would show differently).
resetMapButton = WebGeoDS.resetButton(resetNetworkArticle,"🔄 Reset map and cards")
3. Same lines, two different answers
The numbers above describe the built-in fallback specifically — if you uploaded your own file, the counts will differ, but the same effect is what to look for. Both languages ran the exact same two tolerances (0m and 5m) on the exact same eight lines, and got the same shape of answer:
At 0m, the “spur” (two segments starting about 4m from the block’s corner) counts as its own piece — three components in total: the block, the spur, and the fully separate cluster.
At 5m, that 4m gap is within tolerance, so the spur’s near endpoint merges into the block’s corner — two components: the now-joined block+spur, and the still-separate cluster.
Nothing about the geometry changed between the two runs — same coordinates, same lines. Only the definition of “the same point” changed, and that alone was enough to change how many pieces the network has. This is why real-world network analysis (road networks, utility networks, hydrology) always involves a noding/snapping step before anything else: the raw geometry rarely already agrees with itself down to the last centimeter, and the tolerance you pick is a real analytical decision, not a technicality to skip past.
4. Where to next
Want an adjustable tolerance, the graph drawn directly on the map itself, and a way to click a node and see it highlighted in both views at once? → The standalone Network from Lines tool runs the same algorithm with a tolerance you control live, and lets you download the result.
Wondering why the coordinate system needed picking at all? → Coordinate Reference Systems covers the geographic/projected distinction this article leaned on in section 2.