Skip to content

Repository files navigation

GridTrace

A water-distribution outage and valve-isolation simulator built on the ArcGIS Maps SDK for JavaScript, with a hand-implemented graph traversal engine underneath it.

Click a pipe or junction to simulate a break, close valves to isolate the outage, and watch the affected customers, pipes, and network length recompute in real time — all driven by a BFS reachability trace written from scratch in TypeScript, not a wrapped Esri widget.

screenshot placeholder screenshot placeholder

Motivation

This project exists to demonstrate the specific combination of skills relevant to Esri Software Development & Engineering roles: geospatial web development on the current ArcGIS Maps SDK architecture, and the graph-algorithm fundamentals that underpin utility network tracing. Rather than build a generic map dashboard or wrap Esri's own Utility Network trace UI, GridTrace implements the traversal itself so the algorithm is something I can actually discuss and defend in an interview — adjacency-list representation, BFS reachability, complexity, and the modeling tradeoffs involved.

What this is (and isn't)

GridTrace models network connectivity and simulates outages using a graph I built myself over a synthetic dataset. It does not use the ArcGIS Utility Network — Esri's actual product for utility network modeling, which handles subnetworks, terminal-based directionality, and real trace configurations against enterprise geodatabases. That's a substantial, licensed, enterprise-data-dependent capability that would be dishonest to claim without actually integrating it. GridTrace's architecture is deliberately structured so that integration could be added later as a second, optional trace implementation (see Future Work) — but nothing in this repository currently talks to a Utility Network service, and no resume bullet or write-up here should imply otherwise.

Architecture

src/
  components/       # Presentational React components, organized by concern
    layout/           # App header
    controls/         # Simulation controls (failure state, valve list, reset)
    stats/            # Impact metrics panel
    legend/            # Map legend
    asset-panel/       # Selected asset detail panel
  map/                # Everything that touches @arcgis/core / map-components
    MapView.tsx         # Owns the <arcgis-map> element and view lifecycle
    layers.ts            # Builds GraphicsLayers from the graph; repaints symbology
    symbology.ts          # Visual vocabulary: normal / affected / failed / valve state
    selection.ts           # Resolves map clicks to typed asset references
  network/            # Pure TypeScript — no React, no Esri dependency
    types.ts             # Node, Edge (Pipe), Valve, Customer, TraceResult
    graph.ts               # Adjacency-list graph model, undirected, mutable valve state
    trace.ts                 # BFS outage trace + impact metrics
    trace.test.ts, graph.test.ts, testFixtures.ts
  data/               # Synthetic network generator
  hooks/              # useNetworkGraph, useTraceSimulation, useSelectedAsset
  utils/              # Small geometry helpers

Separation of concerns is the main architectural decision here, and it's deliberate:

  • network/ has zero imports from React or @arcgis/core. It's the part of this project that should read like a straightforward graph-algorithms module — you could paste graph.ts and trace.ts into an interview whiteboard exercise with nothing stripped out.
  • map/ owns all Esri-specific rendering. It translates network state into map symbology and map clicks into typed asset references. It contains no traversal logic of its own.
  • hooks/ is the seam between them: useTraceSimulation holds the active failure and valve toggles, calls into network/trace.ts, and exposes a TraceResult that both the map layer and the UI panels consume independently.
  • No global state library. Simulation state fits comfortably in a couple of hooks (useState for the failure target, a version counter to signal that mutable valve state changed, useMemo to derive the trace). Reaching for Redux/Zustand here would be complexity without payoff.

Tech stack

  • React 19 + TypeScript + Vite
  • @arcgis/core and @arcgis/map-components (current 5.x line) — the web-component-based map API, used directly with no React wrapper package
  • @esri/calcite-components (current 5.x line) — again, used directly as web components, no React wrapper
  • Vitest for unit tests
  • ESLint (flat config) with typescript-eslint and the React hooks plugin

A note on SDK compatibility

Two packages that might seem like the obvious choice here — @arcgis/map-components-react and @esri/calcite-components-react — are both deprecated as of their 5.0 releases. Esri's own guidance is that React 19's native custom-element support (property vs. attribute reflection for object-valued props, e.g. passing an @arcgis/core Map instance directly as the map property on <arcgis-map>) makes the wrapper packages unnecessary. This project uses the underlying web components directly, which is the current recommended pattern rather than an older one this brief might otherwise have specified.

The graph algorithm

Representation

The network is modeled as an undirected graph with an adjacency list (network/graph.ts). Nodes are junctions, sources, and hydrants; edges are pipes; valves sit on pipes and can be independently opened or closed, which removes or restores that edge from traversal.

Why undirected, not directed: real distribution mains are typically looped, not strictly tree-fed from a single source, and without pump/pressure-reducing-valve/terminal data — the kind an actual Utility Network tracks through its subnetwork and terminal configuration — there's no trustworthy basis for asserting flow direction on a given pipe. Outage isolation is fundamentally a connectivity question: "with this edge or valve removed, what's still reachable from a live source, and what's cut off?" That's correctly modeled as undirected reachability. A real Utility Network models directionality explicitly; that's called out under Future Work rather than approximated here.

The trace

network/trace.ts runs a multi-source BFS from every active source node, marking everything reachable. Anything left unmarked after the BFS completes is the outage.

This is a deliberate choice over a simpler "flood fill outward from the failure point": on a looped network, a single broken segment often isolates nothing, because water can still reach every downstream node via the other branch of the loop. Multi-source reachability answers the actually-correct question — "is this node still served by any path from a source" — where a naive flood fill would over-report the outage on any network with redundancy. Closing a valve removes that pipe as a traversable edge for the run; failing a pipe removes it outright; failing a node removes it and every pipe incident to it (a junction burst takes out its connections).

Complexity is O(V + E) per recompute, using a plain adjacency list and a FIFO queue — no algorithmic surprises, which is the point: this is meant to be a straightforward, correct, explainable BFS, not an over-engineered demo.

From the affected node set, the trace derives:

  • Affected pipes — any pipe with at least one affected endpoint, plus the failed pipe itself.
  • Affected customers — any customer attached to an affected node.
  • Boundary valves — valves on a pipe with exactly one affected endpoint. These are exactly the valves an operator would consider closing to contain or shrink the outage.
  • Impact metrics — affected customer count, affected account count, affected pipe count, affected network length, and boundary valve count, all derived directly from the sets above.

Test coverage

network/graph.test.ts and network/trace.test.ts (13 tests, Vitest) cover: a fully connected network reporting no outage; a leaf-pipe failure isolating only its downstream branch; a closed valve stopping propagation with no physical failure; a pre-existing disconnected node being correctly reported as unreachable; independent branches not affecting each other; boundary-valve identification; and reset-then-recompute correctness.

ArcGIS integration

The map itself is standard @arcgis/core / @arcgis/map-components usage: a Map instance with GraphicsLayers built from the graph model, an <arcgis-map> custom element owning the MapView, and view.hitTest() resolving clicks to typed asset references. Symbology (map/symbology.ts) distinguishes normal, affected, and failed states, plus open/closed valve state, using SimpleMarkerSymbol and SimpleLineSymbol. There's no Esri Popup widget — asset details render in a custom Calcite side panel instead, driven by the same selection state.

Dataset

The network is synthetic, generated deterministically in data/generateNetwork.ts — not pulled from a public utility dataset, and not randomly generated either. Random graph generation tends to produce topologies that don't resemble real distribution systems (no loops, arbitrary long edges), so instead this is a small, hand-laid-out service area: one supply source feeding a five-junction looped trunk main, with dead-end branches carrying customer taps — which is exactly where isolation matters. It's realistic enough to demonstrate the algorithm properly (14 nodes, 16 pipes, 11 valves, 18 customer accounts) without any dataset licensing or setup friction.

Running locally

npm install
npm run dev

Then open the printed local URL (typically http://localhost:5173).

Other scripts:

npm run build     # production build
npm run lint       # ESLint
npm run test        # Vitest, single run
npm run test:watch   # Vitest, watch mode

No API keys or ArcGIS credentials are required — the basemap uses Esri's public streets-vector basemap style, and the network data is generated locally.

Design decisions

  • Undirected graph, multi-source BFS — explained above; the short version is that it's the correct model for looped-network connectivity without fabricating flow-direction data.
  • Selection and failure simulation are separate actions. Clicking an asset opens its detail panel; simulating a failure or toggling a valve is an explicit action from that panel (or the valve list). This keeps "look at this asset" and "break this asset" from being the same accidental click, and gives the UI a natural place to show asset-specific facts.
  • Full symbology repaint on every trace recompute, rather than incremental diffing. The dataset is small enough (~50 graphics total) that this is simpler and still effectively instant; incremental diffing would be premature optimization here.
  • No global state library. A couple of hooks (useTraceSimulation, useSelectedAsset) cover everything the UI needs to derive; there's no cross-cutting state that would justify Redux/Zustand.

Limitations

  • The dataset is synthetic and intentionally small; it's built to exercise the algorithm clearly, not to represent a real utility's scale.
  • There's no persistence — refreshing the page resets the simulation.
  • The map is 2D-only, desktop-first. It's usable on narrower viewports but hasn't been tuned for mobile.
  • Node "failure" (a junction burst) removes all pipes incident to that node, which is a simplification — a real junction failure's isolation boundary depends on the physical layout at that junction, which isn't modeled here.

Future work

  • Optional ArcGIS Utility Network integration as a second trace implementation alongside the hand-built one — e.g. a mode toggle that runs the same failure scenario through a real Utility Network's server-side trace (where available) and compares the result against this project's BFS. This is explicitly not implemented today; the current architecture (trace logic isolated in network/, consumed only through the useTraceSimulation hook) is structured so it could be added without touching the graph model or UI.
  • Directed-edge modeling once real flow/pressure data is available, to move from "what's connected" to "what's actually still pressurized."
  • Legitimate performance metrics (trace latency, graph size scaling) once there's a dataset large enough for the numbers to mean something — deliberately not fabricated here.
  • Mobile-responsive layout pass.
  • Persisting simulation scenarios (e.g. to local storage) so a specific failure scenario can be shared or replayed.

About

Water network outage simulator with a hand-built BFS graph trace, built on ArcGIS Maps SDK for JavaScript

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages