Learning to zoom out a data-flow visualization

A visual log of the TSX Data Flow views that were too detailed, too flat, or too noisy—and the design rules emerging from a route-scoped topology view.

July 18, 2026 · 10 min read · Reflection · TSX Data Flow

I wanted an overwhelming data-flow analysis.

That was the phrase I kept using while working on the route views in TSX Data Flow. I know a real application has hundreds or thousands of render sinks, component handoffs, resources, contexts, shared UI primitives, and persistence boundaries. A route table saying there was one path was obviously not enough. A tiny curated diagram was not enough either. I wanted to see the thing.

The mistake was assuming that every piece of that analysis needed equal visual weight.

The view I am starting to like still contains a lot: the current Pluck capture route has 202 connected components, 396 visible links, and another 359 shared references summarized into hubs. The difference is that the graph is now willing to say that a route component, a terminal leaf, a common Button, and one of 43 Lucide icons are not equally important to draw.

This is a visual log of how I got there. Most of these intermediate versions were not good, but they were useful because each bad view exposed a more specific rule.

The first trajectory was too literal

The first route trajectory was a left-to-right lane of cards. I liked the basic ordering because a data flow should have some sense of source, transformation, and sink. In practice, though, the view consumed a huge amount of horizontal space. Clicking or expanding evidence reset the scroll position, the gaps did not carry an obvious meaning, and a blue line appeared without explaining what it proved.

Early left-to-right route trajectory with large cards, wide gaps, and extensive horizontal scrolling
The first card lane looked orderly, but interacting with it constantly moved the viewport and made the spacing hard to interpret.
Route trajectory incorrectly elevating a smoke-test script as a persisted domain
A one-off smoke-test script appeared as persisted domain evidence. More evidence was not helping when the evidence itself was poorly ranked.

The cards also showed source lines simply because the analyzer had them. An import line is exact evidence, but it is rarely the first thing I need to understand. The interface was exposing the analyzer's intermediate representation instead of answering what happened to the value.

Trajectory cards showing import statements as prominent evidence
Exact source evidence was promoted into the main explanation even when an import line did not explain the data flow.
Early selected trajectory with unrelated cards faded too aggressively
Selection fading was also too aggressive at card scale. The blue accent was enough; removing the surrounding content made orientation worse.

The larger problem was that the route-context page implied a story it could not support. Data appeared on the left, a flat list of components appeared in the middle, and one terminal renderer appeared on the right. A board value was obviously used in more than one style attribute, so why was there one consumer? Why were the middle components flat? What did the arrows mean?

Then one screenshot exposed an outright modeling error: Suspense appeared in the sequence even though the data was not passed into it. Independently discovered expressions had been stage-sorted and chained as if each output fed the next.

Flat route-context view with data on the left, components in the middle, and one terminal sink
The flat route context omitted most consumers and gave no useful hierarchy to the components in the middle.
Trajectory with a Suspense card incorrectly chained into the data flow
Suspense made the diagram look more complete while making the underlying claim false. The synthetic arrows had to go.

That was the first important boundary: a line is a claim. If the analyzer has not proved a value handoff through an argument, return, prop, or context, the UI should show a break or separate evidence. It should not invent a clean chain.

Getting all the paths on screen

The next version swung in the opposite direction. I asked for the large node-and-edge diagram because the route table was hiding the scale of the analysis.

Route table that reports too few paths and does not expose the underlying graph
The route table was manageable, but it concealed the hundreds or thousands of sinks I knew were present.
First large all-paths node-and-edge diagram
The all-paths graph was much closer to the object I wanted to inspect. It also immediately needed structure.

Grouping by component helped because it introduced a real hierarchy. A field such as board.description does not simply end at a source expression; it crosses component boundaries as props. Those component-to-component edges are often the part I care about when deciding where to refactor.

All-paths graph grouped into component bands
Component bands added the first useful hierarchy.
Component-grouped data path with prop nodes and a missing cross-component edge
The grouping also made missing prop handoffs easier to see.
Dense component-grouped route flow with oversized nodes
But every node was still too large, so the hierarchy did not solve the navigation cost.

The useful request at this stage was compression. One-line nodes. Narrower widths. Full-viewport rendering. Hover for source snippets. Explicit complex-path isolation. I was spending too much time panning just to find the path I wanted.

Long component-grouped path requiring extensive panning and scrolling
A long path could be technically correct and still be a bad working surface if it required constant panning.
Compact path node with a hover card showing source-code evidence
Compact nodes became more viable once hover could reveal an actual usage snippet.

Another recurring problem was the analyzer promoting mechanically unusual facts. “Shared across four components” appeared at the top even when the shared content was not useful. One case literally reported undefined as the shared value.

Path view prominently showing a low-value shared-across-components section
“Shared” was treated as important because it was detectable, not because it helped explain the route.
Path view listing undefined as shared content
undefined made the ranking problem hard to ignore.

The last improvement in this family was small but important: distinguish the actual route entry from the first component retained in a trace. A long path needs to tell me whether I am looking at the route root or merely where the available evidence starts.

Compact component path labeled with its route entry and trace start
The route entry and trace start are different facts. Naming both made the path easier to trust.

The force-directed topology experiment

The card lanes kept trying to make one path legible at full detail. What I still lacked was the big-picture connectivity. I wanted to zoom out, connect dots with lines, and see the component structure around a route.

The first force-directed version was immediately more fun. That matters. I could move around, spot clusters, and form questions before reading every label. It was also a mess in predictable ways.

First force-directed route topology with many explicit component and shared-reference edges
The first force-directed topology made the route feel like one connected object instead of a stack of reports.

Common primitives such as Button consumed a huge amount of edge space. The better representation was not another pile of arrows. A high-degree shared reference could become a hub color, with a small matching ring on each connected node. The graph would preserve the fact of the relationship without giving every relationship a separate line.

Topology where the common Button component consumes many explicit edges
Button was the first obvious case where an explicit edge per reference was the wrong visual vocabulary.

The graph also changed the value of every pixel around it. Tall toolbars that were acceptable on a report page were expensive here. I kept asking to combine controls until the interface occupied two compact rows.

Force graph with several tall toolbar rows reducing the available canvas
The first graph shell spent too much vertical space on controls and status.
Force graph with controls aggressively combined into two compact rows
Not all information was useful enough to keep. Two rows left substantially more room for the object being inspected.

Learning what not to draw

Most of the remaining iteration was subtraction. The purple context diamond and its surrounding edges formed a rat's nest. ButtonPropsContext occupied a large part of the canvas even though it was implementation detail for a common UI primitive. Components from unrelated routes appeared because they happened to use the same shared component.

Dense context hub producing a rat's nest of edges in a route topology
A context hub created a dense knot with no obvious reading order.
ButtonPropsContext receiving disproportionate visual space in the topology
ButtonPropsContext did not warrant this much attention.
TableDemo appearing in a route graph only because it shares Button references
Shared references must not pull unrelated routes into the visible subgraph.

Once the graph was route-scoped, disconnected nodes became suspicious. So did connected nodes placed far apart. A force layout can optimize an abstract energy function and still violate the visual meaning I expect: if two nodes are each other's only relationship, they should probably be close.

Visible route topology containing nodes with no rendered connections
Disconnected nodes looked like analyzer or projection mistakes, not useful context.
Dense cluster of overlapping nodes, labels, and crossing edges
A second rat's nest showed that generic force spacing was not enough.
Directly connected nodes placed unnecessarily far apart
Directly connected nodes were separated for no useful reason.

Removing noise cannot remove structure. When the Library filter route lost the component that tied its children together, the graph became cleaner and less true. The same thing happened when thick hub rings covered labels.

Library filter topology missing the component that connects several related nodes
The missing parent left a cluster without the component that explained it.
Source context for the missing Library filter component connection
The source made the absent component boundary visible as an analysis problem.
Topology nodes with multiple thick shared-reference rings obscuring labels
Summarization worked, but the first rings were so thick that they hid the labels they were supposed to support.
Close-up of a shared-reference ring still covering node text
The close-up made the correction simple: less border and less gap between rings.

Lucide icons were another high-degree family. The useful fact is usually that a component renders one or more icons from lucide-solid, not that every icon deserves an arrow across the graph. Grouping the package into a ring preserved that signal.

Many Lucide icon references creating unnecessary explicit edges across the topology
Icon references were real, but explicit icon edges did not help me understand the route.

Keeping the graph truthful

The topology was getting visually close while still missing the part I originally cared about: how data arrives. On the capture inventory route, a createResource calls a server handler. That boundary needs to appear. Otherwise the graph explains component topology while stopping just before the source of the data.

Route topology missing createResource and server-handler data-source nodes
The component topology was close, but the createResource and server source were exactly the incoming path I wanted to see.

Adding upstream traversal introduced another class of false structure. A Show appeared as if it wrapped a route shell even though that connection did not exist in the source. Suspense could also close a cycle. Then a broad cycle-prevention rule hid legitimate inbound lines.

Topology showing an unsupported Show-to-route-shell relationship
A graph edge that merely looks plausible is still wrong.
Upstream expansion creating a cycle through Suspense
Upstream expansion needed path-local cycle prevention.

Zoom created a separate legibility problem. If text shrinks with graph coordinates, there is no useful zoom level: zoom out and the labels disappear; zoom in and the overview disappears. Labels need screen-space behavior so that zoom changes topology scale without making text impossible to read.

Zoomed-out topology with labels too small to read
Graph-space text made both the overview and the labels unusable at the same time.
Terminal node connected by a long crossing edge instead of sitting near its parent
Terminal-leaf compaction shortened unnecessary arcs and pulled single-purpose nodes back toward their parents.

Finally, cycle prevention had to be edge-specific. “This node is reachable somewhere downstream” does not mean “this inbound edge closes a cycle now.” The graph should keep legitimate direct inbound relationships and reject only the particular edge that completes the loop.

Selected topology missing a legitimate inbound edge after broad cycle filtering
The first cycle rule removed a legitimate inbound connection.
Inspector showing inbound connections that are not all emphasized in the topology
The inspector made the missing line auditable and led to incremental, edge-specific cycle detection.

The view that is starting to work

The current overview is still dense. It should be. The route is dense.

What feels different is that the density now has levels. The full graph provides shape. Common shared references are summarized. Data sources remain visible toward the edge. Component clusters occupy the center. Labels stay readable. The inspector is persistent but does not take over the canvas.

Current zoomed-out route topology showing 202 connected components and summarized shared references
The current overview: 202 connected components, 396 visible links, and 359 shared references summarized instead of drawn as another wall of edges.

Selection then changes the job of the same map. The unrelated topology remains as faint spatial context while the selected component's contributors and consumers become the working view. This is where fading now feels useful. In the early card lane, fading removed content I still needed to read. In a zoomed-out map, it preserves where I am while letting a smaller subgraph come forward.

Current route topology with CaptureOutlinePanel selected, related nodes emphasized, and the rest of the map faded
Selecting CaptureOutlinePanel turns the full topology into context for one inspectable neighborhood.

The design rules I am taking from this

The graph feels better because several choices are working together:

  • Start from a route. A universal repository atlas is interesting, but a route gives the graph a useful boundary and a concrete question.
  • Draw explicit edges for relationships worth following. Summarize high-degree shared components, contexts, and icon packages with rings or hubs.
  • Keep real hierarchy. Component boundaries, route entry, data sources, and terminal renderers should survive compression.
  • Let geometry carry meaning. Single-purpose leaves belong near their parents. Unrelated routes do not belong in the graph. A disconnected node should be explainable.
  • Keep labels in screen space and details in progressive disclosure. Zoom should reveal topology, hover should provide quick source evidence, and selection should open persistent detail.
  • Treat every edge as a claim. False completeness is worse than an honest gap.

The main change in my own thinking is that “overwhelming” should describe the coverage of the analysis, not the treatment of every mark on screen. I still want all the paths. I just do not need all of them yelling at me at once.

Source: byronwall/tsx-data-flow