@mygen/traffic-tree
Turn a privacy-safe aggregate navigation graph into a bounded forest ready for a visual renderer. Traffic Tree contains no tracker, database client, authentication, consent UI, or Astro dependency.
Install
pnpm add @mygen/traffic-tree
Use
import { buildForest, type TrafficDataSource } from '@mygen/traffic-tree';
const source: TrafficDataSource = {
async getSnapshot(query) {
return fetch(
`/api/admin/traffic?from=${query.window.from}&to=${query.window.to}`,
).then((r) => r.json());
},
};
const snapshot = await source.getSnapshot({
window: { from: '2026-07-01T00:00:00.000Z', to: '2026-07-08T00:00:00.000Z' },
});
const forest = buildForest(snapshot);
The default policy renders only branches with at least 10 transitions, stops at
depth 5, and shows at most four child branches per route. Suppressed transitions
are reported as each node's canopy, so a renderer can show the long tail
without exposing individual routes. Hosts can tune that visible fan-out with
maxChildren; Local95 uses a deliberately compact value of two.
Nodes may include an optional host-owned group string. The botanical renderer
uses it only to keep related shoots adjacent; the package does not interpret the
group vocabulary.
Privacy boundary
The host application is responsible for consent, access control, collection, retention, and aggregation. Only supply stable, deliberately small route IDs and safe human labels. Do not send raw paths, query strings, referrers, IP addresses, identifiers, or other personal data to this package.
See the contract for terminal and incomplete-graph behavior.
Browser client
The framework-free SVG renderer is exported from @mygen/traffic-tree/web.
It displays snapshots supplied by the host; it does not collect navigation data.
import { mountTrafficTree } from '@mygen/traffic-tree/web';
mountTrafficTree(document.querySelector('#traffic-tree')!, {
layout: 'botanical',
loadSnapshot: async (query) => {
const response = await fetch('/admin/traffic/snapshot', {
method: 'POST',
credentials: 'same-origin',
cache: 'no-store',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(query),
});
if (!response.ok) throw new Error('snapshot unavailable');
return response.json();
},
forest: { primaryRoot: { id: 'home' } },
});
The browser client includes 24-hour, 7-day, and 30-day controls and polls its
loader every 30 seconds while the page is visible. Disable polling with
refreshIntervalMs: false. Background refreshes preserve the viewer's scroll
position and focused node; the grove re-centres only on first paint and on a
range change.
Hosts backed by coarser aggregates can expose only accurate controls while preserving their preferred order:
mountTrafficTree(target, {
ranges: ['7d', '30d'],
initialRange: '7d',
loadSnapshot,
});
Omitting ranges preserves the default 24-hour, 7-day, and 30-day controls.
The default botanical layout chooses the strongest incoming transition as
each route's stem and renders secondary transitions as lighter grafts. Node
height is not tied to route depth: each limb's length follows the strength of
the flow it carries, so strong paths stretch into tall stems while trickles
stay short twigs, and every tree earns its own silhouette. Small horizontal
sways and branch bends come from a deterministic hash of the route ID — the
same snapshot always renders the same grove. Set layout: 'layered' to use
the compact strict-depth general-DAG fallback.
Snapshots may also include an optional daily array of { day, navigations }
aggregates. The renderer draws it as a small activity line along the grove's
ground. These are event totals, not unique-person counts. The renderer inherits
the host's --paper, --paper-raised, --ink, --ink-soft, and --accent
CSS tokens when available, so it follows the host's light and dark modes.
The renderer merges repeated route IDs at the same visual depth. Multiple incoming branches therefore converge on one route node; the same route may still appear at another depth where it represents a distinct navigation step.