← All Projects

Qflow

2D traffic visualization with PixiJS. Layout engines, organic shapes, and node identity.

OldestNewest

Real Data

c10March 2026

Rich actor model replaces flat IP arrays. Typed actors, scenarios, and connection weights.

Data ModuleAdded

actors.js (15 apps, 6 services, 12 vendors, 6 datasets with full qmap-compatible metadata), connections.js (type-based weight matrix), scenarios.js (5 presets: tiny through devtools), generator.js.

Flow FormatModified

Each flow carries both rich actor refs (id, type, name, tags, url) and flat backward-compat keys. Actor IDs replace IPs as node lookup keys.

VisualizationModified

Actor-aware player: name from actor, color by type (apps=blue, vendors=pink, datasets=green, services=amber). Scenario selector resets scene. NodeRegistry stores actor reference alongside request totals.

Narrative

The mock data system had served its purpose — flat IP arrays with consistent format. But it couldn't represent what real traffic looks like: typed actors with names, URLs, tags, and topology context. The data module system replaces IPs with rich actor objects curated from qmap.

The devtools scenario bridges old and new: it wraps raw IP-based flows in actor objects, so SSE traffic from qtap integrates naturally. The generator contract stayed the same — LiveFeed's {next()} interface is generator-agnostic, so c5–c9 infrastructure carried forward without changes.

Dev-Tools Integration

c9March 2026

SSE adapter and integration plan for live qtap dev-tools traffic.

SseFeedAdded

SSE adapter with LiveFeed-compatible interface — onBatch(cb), start(), stop(), status. Uses native EventSource with auto-reconnect. Micro-batches events every ~200ms.

AdapterAdded

adapter.js — pure functions transforming qtap SSE events (connection.opened, request.http_transaction) to qflow audit-log format.

IntegrationAdded

Mock/live toggle in feed controls. Connection status indicator. Vite proxy config for local SSE stream. Design doc for Traffic.vue integration in dev-tools app.

Narrative

The vision: qflow as bird's-eye view in qtap dev-tools, dev-tools as detail inspector, both consuming the same production SSE stream. This cycle built the adapter layer and standalone experiment — without requiring changes to the dev-tools app itself.

The SseFeed adapts qtap's SSE events into the same onBatch interface that LiveFeed uses. Switching between mock and live is a single toggle; the rest of the visualization is unchanged. The integration design document maps out how Traffic.vue would consume existing Pinia stores in the dev-tools app, with no SseFeed needed there.

Label Controls

c8March 2026

Full label design system. Position, color, background, and reusable dev-controls components.

LabelsModified

Position + offset (direction selector + 0–40px slider). Hex color input. Rounded-rect background with full control: color, opacity, stroke, pad X/Y, corner radius.

ArchitectureModified

Labels refactored from bare PIXI.Text to Container wrapper (_bg Graphics + _text Text). positionLabel() helper extracted from 4 duplicated call sites.

Dev ControlsAdded

ControlSlider, ControlColor, ControlSelect, ControlSection, DevControls — 5 reusable Vue components replacing ~52 instances of inline toolbar markup.

Narrative

Label design had been a secondary concern — hardcoded gray-blue text with no positioning control. This cycle treated labels as a full design system: any direction, any offset, any color, with a rounded background that makes labels readable against any node color.

The dev-controls component extraction was a byproduct worth noting. Five components replaced 52 instances of repeated toolbar markup across App.vue. ControlSlider alone accounted for 29 replacements. The experiment harness became dramatically more maintainable, which matters as the controls panel grows with each cycle.

Layouts + Growth Relayout

c6 + c7March 2026

Pluggable layout system, true arc packing, and growth-triggered relayout.

LayoutsModified

Renamed ArcLayout → RadialLayout (concentric rings). New ArcLayout: true bounded-arc packing where each item's angular footprint scales with its radius. Overflows to outer rings.

RelayoutAdded

Growth-based relayout — accumulates |newRadius - oldRadius| into a running delta; fires relayout when threshold exceeded. Cooldown timer prevents burst thrashing.

ArchitectureModified

computePositions(items) → pure data ({positions, width, height}). applyPositions() handles tweening. Enables layout composition (ZonedLayout stub documented).

Narrative

C6 explored pulse smoothness and directionality. C7 addressed a more pressing problem: as nodes grow with activity, layout becomes a mess of overlapping circles. The growth-based relayout system accumulates radius delta and fires a corrective relayout once the visual disorder crosses a threshold — throttled by a cooldown to avoid thrashing during bursts.

The layout interface refactor was the architectural win: computePositions returns pure data, completely decoupled from rendering. This separation enables composing layouts (a ZonedLayout with ingress arc, internal cluster, and egress arc is documented as a stub) and makes testing easier.

Live Traffic

c5March 2026

Dynamic node discovery, activity-based sizing, and streaming data architecture.

ArchitectureAdded

LiveFeed (data ingestion with ±50% jitter batching), NodeRegistry (per-node request counts + last-seen). Drop-in replacement for static mock data.

NodesModified

Dynamic discovery — nodes created on-the-fly as new IPs arrive. Activity-based sizing: radius = sqrt(totalRequests) × 2.5. Labels scale with node radius (minimum 8px).

ViewportAdded

Auto-zoom via fitToContent() — scales scene to fill viewport height, upscaling up to 3x with 400ms easing. High-res labels rendered at 3x for crispness under zoom.

PulsesAdded

5 pulse line modes: straight, isometric (default), curved (quadratic bezier with random curvature direction and intensity).

ControlsAdded

Generation controls: speed, batch size, discovery rate, burst chance, hotness. Layout selectors for sources and destinations.

Narrative

The leap from static mock data to a live streaming model required a new architecture. LiveFeed wraps a traffic generator and emits batches on a configurable timer. NodeRegistry tracks each source and destination independently — request counts drive node size, last-seen timestamps enable decay.

Auto-zoom was the visual payoff: as the scene evolves, fitToContent() keeps everything visible without manual intervention. The high-res label rendering at 3x resolution ensures text stays readable even when the viewport scales up. Together these made the visualization feel genuinely reactive rather than just animated.

Organic Shapes

c4February 19, 2026

Blob shapes, squircles, and breathing animations. Nodes feel alive.

ShapesAdded

Shape class hierarchy: BaseShape → CircleShape, SquareShape, HexagonShape, BlobShape, SquircleShape. Simplex noise deformation with Catmull-Rom-to-bezier curves.

AnimationAdded

Continuous noise3D-driven vertex morphing. Breathing animation throttled every 3rd frame for performance.

ControlsAdded

Full parameter panel for live shape/animation updates without scene rebuild.

Narrative

Blob shapes use simplex noise to deform a circle, then smooth the result with Catmull-Rom-to-bezier curve conversion. The breathing animation applies noise3D to vertex positions continuously — each node undulates slightly, giving the scene an organic, living quality.

Squircles (parametric superellipse) offered a middle ground between circles and squares, with adjustable roundness. The shape class hierarchy made all five shapes swappable at runtime. Performance was kept in check by throttling the breathing animation to every 3rd frame.

Node Identity

c3February 18, 2026

Labels, shapes, favicons, and color differentiation give nodes visual meaning.

LabelsAdded

Three modes: off, hover, always-visible. Item labeling system for node identification.

ShapesAdded

Circle, square, hexagon — live-swappable via UI. Different shapes encode different entity types.

FaviconsAdded

Circular badge overlay using Google's favicon proxy. DecorationImage class with Image() + img.decode() + PIXI.Texture.from().

ColorAdded

Entity type differentiation: external vendors (pink), internal services (green), sources (blue).

Narrative

Before this cycle, every node was a circle. After it, nodes encode three dimensions of meaning: shape (entity type), color (internal/external/source), and favicon (specific service identity). The visualization went from abstract to informative.

Favicon loading was the hardest problem. PixiJS 8's Assets.load() fails on blob and query URLs from Google's favicon proxy. The fix: a custom DecorationImage class hierarchy using the browser's native Image() constructor, img.decode() for async loading, then PIXI.Texture.from(img) to bridge into PixiJS.

Layout Exploration

c2February 18, 2026

Grid centering for sources, circular arc for destinations. Power-law traffic distribution.

LayoutsAdded

ArcLayout with fullCircle mode — destinations distributed around 360 degrees. Auto-centered grid layout for sources.

Mock DataModified

Enhanced to 34 sources, 27 destinations with power-law traffic distribution for realistic visual weight.

PulsesModified

Fixed orthogonal line calculations for proper 45-degree angle paths between source and destination.

Narrative

Sources on the left in a grid, destinations on the right in an arc — the spatial metaphor immediately made traffic patterns readable. Power-law distribution in the mock data meant a few connections dominated visually, matching real-world network traffic patterns.

Three grid dimension math bugs were found and fixed: column formula bias, cell spacing miscalculation, and off-by-one centering. Layout math is deceptively tricky when it needs to look effortless.

Baseline + Navigation

c0 + c1February 18, 2026

Post-transplant stabilization. Zoom, pan, and responsive canvas.

CoreModified

Fixed blocking bugs from project migration — PIXI app leak, factory cache mismatch. Verified visualization renders with mock data.

NavigationAdded

Scroll-to-zoom (toward cursor) and click-drag panning. Canvas responsive to container resizing.

RenderingModified

Fixed pulse line alignment. Discovered isRenderGroup = true fix for PixiJS 8 dynamic children.

Narrative

Qflow was transplanted from an earlier prototype. The first two cycles stabilized the foundation — fixing a PIXI app leak that caused canvas duplication and a factory cache mismatch that broke node rendering.

The PixiJS 8 render group discovery was the key learning: dynamically-added children require isRenderGroup = true on their parent container, or transforms are cached incorrectly. This fix unblocked all subsequent visual work.