Skip to content
tangle-networkPublic

About

Shared Tangle chart library for research, benchmark records and application data.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

@tangle-network/charts

React charts use the shared UI package named in package.json; install that peer for graph and React views. Static Figures remain usable without React or UI. Agent graph status uses UI's canonical RunStatus; normalize external status strings with runStatus at the host boundary.

Benchmark figures as static SVG and HTML tables, plus optional React timing primitives.

The React entry also exports DayBars, HBars, Histogram, Lines, Meter, Spark, Gantt, BehaviorStrip and DotStrip for live operational pages. Import @tangle-network/charts/react/live.css for their layout with Brand tokens loaded; --chart-live-* properties may customize their semantic roles. Data, state colors, destination links, labels, and interpretation remain with the caller. DayBars accepts an onOpen(index) action with keyboard activation.

VersionGraph and layoutVersionGraph from @tangle-network/charts/react render version histories with lane and merge geometry, controlled selection, filtering, keyboard navigation and bounded rows. Import react/live.css and Brand tokens as above. Producers supply commit identities, parents, lane labels and scored chips; Charts preserves those facts and owns drawing, arrival motion and reduced-motion behavior.

FlowWaterfall from @tangle-network/charts/react is the shared live trace inspector: an agent timeline (one row per agent, elbow handoffs, hatched waiting) and a virtualized span waterfall over one clock. It owns hierarchy layout, folding, search, playback, zoom, span cards and running-span growth. Producers own identity, parentId, explicit status and observed totals. Import @tangle-network/charts/react/flow-waterfall.css; its fw-* classes and six-field summary support hosts without a Tailwind build. Every color, radius and shadow in it is a Brand role (text, card, hairline, well, chart grid, status inks, the categorical tones for agents), so the host loads @tangle-network/brand tokens.css (or legacy-light.css) and sets its theme; the stylesheet has no color of its own. The React-free root exports the FlowSpan and FlowTrace types.

FlowWaterfall accepts optional graph and latestEvents React nodes. Supplying graph adds Graph to its existing Agents/Steps view control, including defaultView="graph". The trace clock and controlled selection survive switches. Graph hides waterfall-only controls; latestEvents stays mounted in the header across all views.

Five figures cover the decisions a benchmark reader makes:

Figure Question
rankedRates Which setup solves most, and how uncertain is each rate?
costFrontier Which setup gives the most solves per dollar?
comparisonIntervals Is each step in the order real?
taskMatrix Where does each setup succeed and fail?
breakdown Why do attempts fail?

Operational timings also have three static figures: heatmap for complete recorded matrices with producer supplied pass, breach and no-data states; latencyRange for p50 dots, p95 bars, a supplied SLO and a minimum observation count; and phaseWaterfall for ordered, disjoint phase durations with unaccounted wall time shown explicitly. They return the same Figure | Refusal contract.

import { heatmap, latencyRange, phaseWaterfall } from "@tangle-network/charts";

heatmap(paths, harnesses.map(h => ({
  id: h.id, label: h.label,
  cells: paths.map(p => ({ status: h[p.id].status === "blocked" ? "no-data" : h[p.id].status,
    value: h[p.id].p95Ms === null ? null : h[p.id].p95Ms / 1000 })),
})), { measure: "p95 latency", unit: "s", statusRule: "5 s SLO" });

latencyRange([{ id: "pi", label: "pi", p50Ms: 3572, p95Ms: 4102, n: 9 }],
  { measure: "cold create", sloMs: 5000, minN: 5 });

phaseWaterfall([{ id: "route", label: "Route", ms: 5 }, { id: "restore", label: "Restore", ms: 9500 }],
  { measure: "create", totalMs: 30398 });

latencyRange draws one row per measure: a p50 dot, a p95 diamond, each with its recorded 95% confidence band (p50Band, p95Band), both numbers and n on the row, and every other fact on hover. metricBars draws recorded scores as columns; identities put a model and harness mark under each. Render either with renderFigure(figure, n, { presentation: "panel" }) and a measured width to fill a dashboard cell.

The producer maps its own blocked or absent states to no-data; Charts never assigns a status from a value. latencyRange refuses any row below minN; when the producer omits those rows, pass their reasons in excluded so the note names them. phaseWaterfall refuses phases whose sum exceeds the reported total because overlapping spans cannot be drawn as a cumulative waterfall. Its input order is retained, including zero-duration phases. The producer marks known unaccounted leaves with kind: "unaccounted"; Charts also marks any difference between their sum and totalMs.

Charts owns presentation and interaction; producers own interpretation and status. The library never derives failure or running state from metadata values. Ranks, intervals, frontier membership, estimate labels and pass counts come from the producer: a board record, a VerticalBench report or an agent-eval lens. A figure function returns a Refusal in place of a figure it cannot draw honestly, for example a bar over too few units, an interval that names no method, or a frontier the producer did not compute.

The producer's estimate controls rate marks: descriptive draws a rate bar and may show a named Wilson score interval when that estimate meets the producer's unit threshold. Task-clustered bootstrap intervals require a bootstrap estimate. Intervals without support are marked as not drawn in the data table and note. Cost-per-pass figures print unknown for missing, zero, or unproven cost; zero passes print no pass. A matrix preserves an observed zero cost as $0 and omits a null cost. Rank numbers require every row to carry both a rank and a bootstrap estimate. For an unranked descriptive report, rankedRates accepts order: "input" to retain the producer's row order and make no ranking claim. The default keeps supported ranks or sorts unranked rows by observed rate. Malformed rates, intervals, matrix counts, and breakdown counts return a refusal instead of a misleading mark. Unmeasured and flagged matrix attempts keep distinct marks and table counts, even without tier grades. An interval whisker is omitted when its bounds exclude the row's rate.

The static root has no runtime dependencies and uses no DOM, so it runs in a Worker, a build step or a browser. The separate opt-in @tangle-network/charts/react entrypoint requires React; it is never re-exported by the root.

AgentGraph lives at @tangle-network/charts/react/agent-graph, with its stylesheet at @tangle-network/charts/react/agent-graph.css. It draws agent trees and tool-call hubs, keeps controlled selection across live updates, and pages the attention list on phones. Install the optional react-dom, @xyflow/react and elkjs peers when using the topology entry. The host loads Brand tokens and its theme, as for FlowWaterfall. Hosts supply identity through renderAgentMark, renderToolMark and renderEventIcon; Charts owns geometry, cards, metrics, activity and interaction. Sandbox UI supplies its model/provider marks around this same renderer. Shared formatting, state, motion and lazy-boundary helpers are available from @tangle-network/charts/react/agent-graph/presentation for that composition. The ordinary React entry and static root keep their existing dependency boundaries.

Use

import { isRefusal, rankedRates, renderFigure, renderNotes, renderRefusals } from "@tangle-network/charts";
import "@tangle-network/charts/figure.css";

const fig = rankedRates(rows, { measure: "solve rate", passTier: "B" });
const html = isRefusal(fig) ? renderRefusals([fig]) : renderFigure(fig, 1) + renderNotes([fig]);

Each figure returns:

  • finding, lede and read: sentences generated from the input. No function accepts heading text.
  • svg.wide (720 px) and svg.narrow (358 px, or null when the table is the phone view).
  • table: an HTML table with every number the plot shows.
  • note: the method, the n and every exclusion, rendered by renderNotes.

Colour

Marks carry classes, and figure.css maps them to the --chart-* tokens in @tangle-network/brand tokens.css. A render contains no colour value, so it follows the page theme. A document without brand tokens (a PDF, a sandboxed iframe) embeds chartTokensCss and figureCss in a <style> element. chartTokensCss is light by default, dark under prefers-color-scheme: dark or data-theme="dark", and light in print.

One accent marks the measure a reader decides on. One muted grey marks everything secondary. An indigo ramp marks App Grade tiers C, B, A and S; F is an outline. Identity is carried by direct labels, never by hue.

Develop

Local Beelink gate (set TOUCHED_INTEGRATION_TEST to the touched integration test file; the gate installs with npm ci from package-lock.json): beelink-gate auto https://github.com/tangle-network/charts.git "$(git rev-parse HEAD)" -- sh -c 'npm run typecheck && npm exec -- vitest run "$1"' _ "${TOUCHED_INTEGRATION_TEST:?set touched integration test path}".

npm ci
npm run gen:css       # after editing src/figure.css or Brand tokens
npm run typecheck
npm test
npm run build         # fails when src/css.generated.ts is stale

Release

Versions publish from this repository's release workflow through npm trusted publishing. Merge the verified change, then push a vX.Y.Z tag matching package.json; the workflow runs frozen install, typecheck, tests, build, and npm publish --provenance.

Chart-first vertical rates

rankedRates(rows, { measure: 'pass rate', orientation: 'vertical', order: 'input', identities }) draws vertical bars on a full 0–100% axis. order: 'input' keeps unranked producer order; omit it to keep the existing ordering behavior. Optional identities maps setup IDs to model and harness marks, each with an explicit { label, src }. Images must use same-origin absolute paths or HTTPS URLs; the library never infers a provider from a model name. Hover or focus a column to read its actual count, eligible interval, known cost and run time. Zero has no positive bar, and ineligible estimates keep their count without a bar.

Use renderFigure(figure, noteNumber, { presentation: 'chart' }) for a chart-first surface. The plot fills its container; data, reading instructions and method stay in a closed disclosure. The default report presentation, underlying table and note links remain available. Many columns keep a readable minimum width and scroll horizontally.

Recorded scalar values and histories

metricBars draws continuous scores or counts on an explicit zero-based axis. It preserves input order and draws no rank. A known zero has no positive bar; a null value prints not measured. Observation counts use the caller's explicit unit.

import { isRefusal, metricBars, renderFigure, renderRefusals } from "@tangle-network/charts";

const figure = metricBars([
  { id: "a", label: "Setup A", value: 0, observations: { count: 13, label: "tasks" } },
  { id: "b", label: "Setup B", value: null, observations: { count: 0, label: "tasks" } },
], { measure: "mean composite score", unit: "score", domain: [0, 1] });
const html = isRefusal(figure) ? renderRefusals([figure]) : renderFigure(figure, 1, { presentation: "chart" });

timeSeries draws recorded scalar values at finite positions in order; equal positions, such as two runs recorded at one time, share a place and are never shifted. The caller includes every configured position, with null for a missing observation. Null breaks the line; the library never interpolates a missing value. secondary: true de-emphasises a series beside a primary in the same colorIndex, such as a max beside its p50. crosshair: true draws a vertical rule at the point whose tip shows. selectable: true gives valued points button semantics for host-owned click and Enter/Space actions; data-point numbers valued points across rows in input order. React FigureView accepts onPointSelect(index) to open producer-owned details; it shares nearest-mark hover/click selection when hit areas overlap, with Enter/Space for focused marks. A host with its own pointer rule, such as nearest point within 48 px, sets data-near on that [data-point] group to show its tip, and sets pointer-events: none on .tgc-series-observation so hover does not show a second one; keyboard focus still shows each tip.

import { isRefusal, renderFigure, renderRefusals, timeSeries } from "@tangle-network/charts";

const figure = timeSeries([{ id: "a", label: "Setup A", points: [
  { x: 1, y: 0, observations: { count: 13, label: "tasks" } },
  { x: 2, y: null, observations: { count: 0, label: "tasks" } },
  { x: 3, y: 0.5, observations: { count: 5, label: "tasks" } },
] }], { x: { label: "shot", domain: [1, 3] }, y: { label: "mean composite score", unit: "score", domain: [0, 1] } });
const html = isRefusal(figure) ? renderRefusals([figure]) : renderFigure(figure, 1, { presentation: "chart" });

Both examples show behavior controls, not benchmark results. Both functions refuse out-of-domain values, invalid intervals and measured values with zero observations. Intervals arrive with their recorded bounds, confidence level and method; neither function computes them. Optional annotations carry independent recorded facts, such as all checks passing. Hover or focus a mark for its value, observation count, interval and annotation. The data table retains missing positions and full labels.

Run events, lanes, parts of a whole and small multiples

A run page draws recorded events, never inferred ones. Every addition below has a static form (Figure | Refusal, no DOM) and a React form, takes its colours from Brand tokens through var(), and hatches missing data instead of drawing zero.

Need Static React
Vertical events (first pass, deadline, died, release, steer, ruling, wake) and shaded spans (after pass, idle, no data, blocked) timeSeries(rows, { ..., marks, bands }) <Lines marks bands>
Lanes of spans with marks; an open span runs to now timeline(lanes, { measure, now, marks }) <Gantt lanes now marks>
An agent tree over time: lanes under their parents, attempt ticks with ok and failed counts, walls across every lane, folding by subtree timeline(lanes, { measure, start, now, marks, bands, collapsed }) <TimelineView lanes options defaultCollapsed>
Part of a whole with untraced time hatched phaseWaterfall (unaccounted leaves and the remainder are hatched) <Meter total segments>
One panel per arm on shared axes, with no-data panels and early ends smallMultiples(panels, { x, y, shared }) <FigureView figure>
Series that stay distinct without colour later series add a dash and a marker shape series[].dash, series[].marker
type ChartMark = { at: number; label: string; kind: "pass" | "deadline" | "died" | "event" | "release" | "steer" | "ruling" | "wake";
  verdict?: "pass" | "fail" | "unknown"; source?: string; lane?: string };
type ChartBand = { from: number; to: number; label: string; kind: "after-pass" | "idle" | "missing" | "blocked" };
type TimelineLane = { id: string; label: string; spans: Array<{ start: number; end: number | null; state: string; note?: string }>;
  parent?: string; depth?: number; ticks?: TimelineTick[]; detail?: string[] };
type TimelineTick = { at: number; outcome: "ok" | "quota" | "lost" | "rate-limit" | "oom" | "refused" | "aborted" | "other"; note?: string };
type MultiplePanel = { id: string; label: string; unit?: string; domain?: [number, number]; points?: TimePoint[];
  missing?: { why: string; detail?: string }; end?: { at: number; kind: ChartMark["kind"]; label?: string }; marks?: ChartMark[] };
type MeterSegment = { label: string; value: number | null; color?: string; kind?: "measured" | "untraced"; pattern?: "hatched" };

Mark kinds differ by stroke as well as colour: pass is solid, deadline dashed, died ends in a cross, and other events are dotted. A release carries a diamond head, a steer a triangle, a ruling a square and a wake a circle; releases, steers and wakes stop at the plot's edge so frequent events never wall off the data, and a ruling crosses it. A recorded verdict colours a mark (pass and fail take the status tones, unknown the muted tone), and a source, such as the operator or the keeper, follows its label. Mark labels take up to three rows above the data, placed by measured width so none overlap; marks of one kind and verdict at one place share a rule and a label (rc17 (+18)), and a label with no room is left to the hover and the table. A blocked band is a system-wide wall, such as a quota window: a breach wash across every lane under a solid top edge. Marks and bands appear in the figure's table (Recorded events by …) and, for React, in a text list under the plot. timeSeries and smallMultiples refuse marks or bands outside the x domain, and timeline names them at its edge instead (below); React components throw RangeError for marks or bands they cannot place, as StackedBarChart does. Axis labels never overlap at 720 or 358 px: timeSeries labels recorded positions spread evenly across the plot, and timeline uses the smallest clock step (1 min to 7 days) whose labels fit, naming the date at midnight.

timeline refuses an open span without now, a span, tick or now outside a supplied start/end window and reversed times. States named in hatch (default missing, untraced) are hatched; states in muted (default idle) use the muted tone. The window defaults to the extent of the data (spans, ticks and now), so a run that ended early is not drawn out to its deadline: a mark past the window, such as the deadline, is named at the edge it falls past with its distance (Deadline +21.9 h →), listed in the note, and not drawn; bands are clipped to the window. Pass start (the run's start) to show time before the first span.

Lanes form a tree through parent: each lane is drawn under its parent with a connector, siblings and roots keep their input order, and depth defaults to the parent's depth plus one. A stated depth that disagrees with the parent, a parent that is not a lane and a parent chain that loops are refused. ticks are recorded attempts, such as driver turns, at the time each ended: ok ticks rise above the bar and failed ticks drop below it in their outcome's colour from the shared outcome palette (below), and the lane ends with its counts, 21✓ 250✗. A lane without ticks shows no counts; an empty ticks shows 0✓ 0✗. Ticks of one outcome within 2 px are drawn as one, with the count and the commonest note in its hover. An unknown outcome is refused. A mark with a lane is drawn on that lane only (a steer to one agent, a wake of the root) instead of across every lane; a lane that is not in the figure is refused. detail is the caller's facts about a lane (profile, model, status, attempts, cost, top error), one hover line each on the lane's label and every span, and a column of the lanes table. collapsed folds the named lanes' subtrees: descendants are hidden, the label gains +N, and the folded lane's ticks and counts total its subtree. Passing collapsed, even empty, makes each parent lane's label a fold toggle (data-toggle, a button with aria-expanded); TimelineView keeps the fold state and wires the toggles to a click, Enter or Space, returning focus to the toggle. Gantt keeps its single-span rows input and producer lane order; lanes uses the same prepared plot as timeline. The React adapter owns selection, focus and bounded row visibility; report tables are generated only for reports. The live stylesheet includes the shared figure rules.

Meter with segments draws each recorded part of total; the remainder no segment accounts for is hatched and labelled untraced (rename it with untraced). A null segment is listed as not recorded and takes no width; a missing total hatches the whole track. Parts that sum past the total throw. Recorded zero remains zero; an unknown total is never inferred from the parts.

scaleMax is a finite positive comparative axis maximum, at least total: <Meter total={4} scaleMax={10} segments={[{label: "Work", value: 3}, {label: "Idle", value: 1, pattern: "hatched"}]} compact /> fills 40% of the track with no untraced remainder. The unused comparative scale stays plain; percentages still refer to the recorded total. pattern: "hatched" textures a measured segment without changing its meaning to untraced. compact puts the track and formatted total (or used value) on one row; text overrides that visible count. Hover or keyboard focus opens the shared top-layer readout above clipped or transformed rows, with the full recorded summary and segment values, including unknowns; Escape dismisses it. Default used/total, segmented keys and step layouts retain their existing presentation. Only the compact readout is a tab stop.

smallMultiples draws one panel per arm. A missing panel is hatched and reads no data: <why>; an end stops the line at a mark of its kind. It refuses mixed units, values outside the shared domain, points after an end and a missing panel that also carries data. shared: "x" keeps one x domain and requires each panel's own y domain. The lede counts n of m <noun>s have data.

TimelineView is FigureView around timeline(lanes, { ...options, collapsed }); defaultCollapsed names the lanes folded on first render.

FigureView embeds any static Figure | Refusal in React: the wide render in containers of 720 px or more, the phone render (or the table, when a figure has none) below that, and the data, reading and method in a closed disclosure. A refusal renders as its sentence. Import figure.css for its styles.

node scripts/capture-run-page.mjs [dir] (after npm run build) captures each addition at 1440 px light and 390 px dark; the reviewed images are docs/screenshots/run-page-*.png.

Lines accepts named ticks: [{value, label}] and per-series interpolation: "step". A dot series may provide label(t, index), href(t, index) and highlight(t, index) for distinct, keyboard-accessible observations (including equal timestamps). New points use Brand arrival motion; reduced motion stays still. Producers own release identity and best selection. strip: {label, series?, points: [{at, id, label, href?}]} places unscored observations below the numeric plot. Use the same series name and point id when an observation gains a score to retain keyboard focus; missing values never occupy zero.

Release checks: the climb and the bar

A play's page asks how close its releases are to the check and to the goal's bar. timeSeries and smallMultiples draw it from the producer's recorded rows; Charts decides no verdict, comparability or checker.

const climb = timeSeries(rows, {
  x: { label: "release", domain: [-0.5, 16.5], categories: tags.map((tag, at) => ({ at, label: tag })) },
  y: { label: "check progress", unit: "", domain: [-0.05, 1.06], strip: "no score",
    ticks: [...stages.map((label, i) => ({ at: i / stages.length, label })), { at: 1, label: "SOLVED" }] },
  bands: [{ from: 0, to: 16, label: "18b · 5 published, 7 withheld", kind: "group" }],
  marks: [{ at: 9.5, label: "10.5 h", kind: "event", source: "gap" }, { at: 13.47, label: "v4 · operator amendment 20:52Z", kind: "ruling" }],
  markers: { circle: "check", diamond: "gate" },
});
// rows: one per checker (and run), `colorIndex` 0 for the current checker; points such as
// { x: 13, y: 1, verdict: "pass", glyph: "check", marker: "circle", count: 3, firstAt, lastAt, annotation: line }
// { x: 15, y: null, verdict: "unknown", marker: "diamond", label: "6/15", count: 12 }   → the strip, never a height
Field Drawn as
verdict pass filled, fail outlined, unknown dashed; with y: null, a square in the strip under the plot (hatched when unknown)
count, firstAt, lastAt ×n beside the mark; first and last times in the hover and the table
comparable false outlined with *, null outlined with ?
glyph ✓ (check) or ✕ (cross), the only marks in the status tones
marker, label the mark's shape, named by the figure's markers; a short direct label such as 9/15
y.scale, y.targets, y.ticks log10 y; target bands (dashed when stretch); caller-named gridlines
x.categories an ordinal axis of weighted columns; points sharing a column are spread across it
colorIndex (row) the checker palette, 0 current; dashes follow the index

The bar panels share the climb's columns: smallMultiples(panels, { shared: "x", x: { label, domain, categories }, columns: 2, connect: false, markers }), each panel with its own unit, format, scale: "log", domain and targets. docs/fixtures/magnetar-play.json holds the Magnetar play's real 17c, 18b and 19b rows in these shapes (docs/fixtures/export-magnetar-play.mjs exports them on the Lab host), and src/run-page-figures.test.ts draws them.

Attempt outcomes, checkers and run endings have one palette in every view. Green, red and amber are status colours and mean one thing everywhere: an ok attempt and a solved run are green, a run that died is red, a stalled run and a rate-limited attempt are amber. OUTCOME_ORDER is the stack order (ok, quota, lost, rate-limit, oom, refused, aborted, other; an open attempt is hatched, never an outcome); OUTCOME_COLORS[outcome], checkerColor(index) and ENDING_COLORS[ending] return the var(--chart-outcome-*), var(--chart-checker-*) and var(--chart-ending-*) tokens figure.css defines as light-dark() pairs, so they follow the page's color-scheme. Aborted and other are two distinct greys and always carry a label. Checker hues (blue for the current checker, then magenta, cyan and violet) avoid the status colours, so a checker's colour never reads as a verdict; a failing verdict without a value is a neutral ring in the strip. ENDING_ORDER is solved, not solved, died, stalled, cancelled, live, never started. node scripts/palette-check.mjs <dataviz validate_palette.js> checks both palettes, adjacent pairs in order, on the light surface and on #161616.

Play page figure 1440 light 390 dark
Climb climb climb phone
Against the bar bar bar phone

Plots without visible report controls

Use renderFigure(figure, noteNumber, { presentation: "plot" }) to show the responsive plot alone. The complete data table remains available to assistive technology. This presentation has no visible finding, method text, disclosure or table control. Vertical plot axes use larger tick labels. The existing report and chart presentations keep their output and paper behavior.

Plot presentation uses labels of at least 14px and numeric axes of 15px. Dedicated phone SVGs retain their intended width inside a keyboard-accessible scroll region. Wide plots reserve side gutters so enlarged labels remain visible. These typography rules do not apply to report or chart presentation.

Optional React renderers

import { useState } from "react";
import { Sparkline, StackedBarChart } from "@tangle-network/charts/react";

export function RecordedUnits() {
  const [selectedBucketId, onSelectionChange] = useState<string | null>(null);
  return <>
    <Sparkline label="Completion delta" values={[-2, 0, null, 3]} format={String} />
    <StackedBarChart
      label="Recorded units"
      series={[{ id: "a", label: "Series A", color: "var(--chart-accent)" }]}
      buckets={[
        { id: "first", label: "First window", total: 3, segments: [{ seriesId: "a", value: 3 }] },
        { id: "missing", label: "Second window", total: null, segments: [{ seriesId: "a", value: null }] },
      ]}
      maxValue={10}
      formatValue={(value) => `${value} units`}
      selectedBucketId={selectedBucketId}
      onSelectionChange={onSelectionChange}
    />
  </>;
}

Install React 18 or 19 in the consuming app. It is an optional peer so static-only consumers need neither React nor its types. The build bundles canonical statusMeta and RunStatusIcon from @tangle-network/ui/run; consumers need no UI-package runtime dependency. The renderers need no generated stylesheet; figure.css remains exclusively the unchanged static-figure stylesheet. Supply application font/color styles and readable series colors. Apps with server/client component boundaries mark their interactive wrapper as a client component.

Different scales, on purpose. Sparkline ports Agent App's observed-extent scale; it does not implicitly include zero. Equal readings sit at mid-height, a single reading is a point, null/nonfinite samples retain their sample index and break the line, and finite negative readings are supported. Width/height props control its viewBox. Its accessible name describes the count, gaps, range and direction; the SVG description contains each sample's formatted value. It is not animated and has no selection interaction. The default en-US formatter retains Agent App's two-decimal behavior; pass format={String} or a domain-specific formatter when exact precision is important.

StackedBarChart always uses the caller's explicit [0, maxValue] domain. It accepts already-grouped buckets with unique IDs, labels, supplied totals and ordered segments referencing declared series IDs. It never fetches, sorts, groups, calculates billed amounts, chooses a currency, maps products/categories, or invents a maximum. Input bucket order is the sample axis, not an inferred date axis; supply every intended bucket, including gaps. Omitted segments are not synthesized as zero.

A null/nonfinite total or segment makes the whole stack a gap rather than a misleading complete bar. The known values remain in the readout and data table. Zero remains measured zero and draws no positive segment. As in the handoff, positive subpixel segments have a one-pixel visual minimum; their reported values and the total are not changed. Negative or out-of-domain finite values, duplicate/unknown identities, and inconsistent complete totals throw RangeError rather than clamp or silently aggregate. Nonnegative stacks do not model credits or diverging signed stacks. The existing static Refusal API is unchanged; it is not repurposed as React component state.

Selection is controlled by bucket ID. Hover, focus, click, Enter, Space and touch activation select rather than toggle; mouse leave, blur and Escape dismiss. A removed selected ID shows no stale readout, and reordering never selects another bucket by index. The stabilized SVG keeps full-height hit targets and highlights the actual stack. A named keyboard-scrollable region contains its fluid SVG with a 400px minimum. A cursor-adjacent tooltip repeats the selected values for mouse and pen users. It flips at viewport edges and uses a native manual popover to escape transformed or clipped panels. Browsers without popover support use a fixed-position fallback, which a transformed ancestor can constrain. The selected breakdown remains in flow and live-announced; every bucket also carries its formatted values in its accessible name, and a native “View data” disclosure contains the full table. No Tailwind build or hover-only tooltip is required to read the values.

Provenance and extraction boundary. Sparkline comes from tangle-network/agent-app, src/web-react/sparkline.tsx and its tests at a8947683a050394d13848bb7e4cbbe7cf08d208e. Stacked SVG rendering and select-never-toggle regressions come from merged ADC PR #8746, products/platform/web/src/client/components/UsageChartSvg.tsx and UsageChart.test.tsx at a1c3958fd73a3894d4515f9dc476dae9ace30ce0. Platform's aggregation, currency/product/category functions, legend composition and portal-tooltip positioning are not copied. The persistent readout and native pointer popover avoid a new React DOM peer. This is not a new universal usage-chart API, a zero-baseline replacement for SpendCard's sparkline, or a consumer migration; Platform and Agent App are not edited.

Thousands of dots: DotMap and unitDotLayout

unitDotLayout(groups, width, height) places one dot per item in a rectangle per group, the rectangles a squarified treemap sized by count, so a fleet of 6,000 runs reads as its groups first and its items second with no item hidden in an aggregate. It returns the rectangles, every item's x, y (interleaved, groups in input order) and a radius per item from its group's cell.

DotMap draws those positions, or a semantic map's x, y, on one canvas: wheel or pinch zooms about the pointer, drag pans, a click selects, double-click or 0 fits, and + - ←→↑↓ work from the keyboard. Colours are palette indexes (colors: Uint8Array into palette, CSS colours or custom properties), so a lens change repaints without a new layout; dimmed greys filtered points, radii may vary per point (a dot sized by its run's hours inside its cell), and labels name clusters without overlapping. Hit tests use a uniform grid, one path is filled per colour, and the canvas records its last frame's draw time in data-frame-ms. The caller owns hover text (describe), selection, the legend and the details.

Behavior over time: BehaviorStrip, DotStrip, activities and candidates

BehaviorStrip draws every agent's behavior in every window of a run on one canvas: rows: [{ id, label, windows: (category | null)[] }] in the producer's (tree) order, windowMs and startedAt (epoch ms of window 0), markers: [{ at, label, verdict? }] as dashed release rules named with their verdict, events: [{ rowId, at, label? }] as message ticks on a row, calibration: { [category]: { calibrated, auc?, ci?, n? } }, controlled selected: { rowId, windowIndex } with onSelect(rowId, windowIndex), renderRowLabel(row) and live (new windows append at a fixed cell width, so earlier cells never move). Three hues read as reading, writing and verifying (Brand's --chart-category-1..3, the storyboard's validated pairs), textures tell sub-classes apart (hatch for reviewing, process files and building; dots for integrating), looping alone takes the serious status colour, and thinking, running and idle climb a neutral ladder. With a calibration table, a category it does not mark calibrated draws at neutral step 3 with an uncalibrated badge in the key; without one the strip asserts nothing. The key is always drawn and text stays in text roles. BEHAVIOR_CATEGORIES is the storyboard's table; an unknown category draws as neutral step 3 named by itself. Rows past 40 are windowed as CountHeatmap windows its rows; the canvas draws only mounted rows and records its last frame in data-frame-ms. Keyboard: each row is a tab stop named by its top three categories in minutes (behaviorRowLabel), ←/→ move the window cursor, Home/End jump to the ends, ↑/↓ move rows, Enter selects; hover and the cursor open the shared tooltip with the window's clock, behavior, messages and release.

DotStrip draws one row per metric and one dot per entity on a 0..max axis: rows: [{ id, label, dots: [{ id, label, value, outlier?, note? }], max?, format? }]. An outlier is the producer's outlier: true on a recorded value and nothing else (dotStripRows is the model); it draws larger, in the serious colour with a surface ring. Each row ends with max <value> or unknown on every entity; a null value is listed and never placed. Dots are tab stops with hover and focus tooltips, onSelect(rowId, dotId) on click, Enter or Space, and the key is always drawn.

AgentGraphActivity adds testing, messaging, reviewing and looping; a live card shows the activity as a chip with its icon and label (AGENT_ACTIVITIES), looping in the serious colour, and activityCalibrated: false draws the chip in the neutral step with an uncalibrated badge. ChartMark adds the kind candidate: an unmerged release candidate, a hollow diamond on a dotted stem, named with its producer-formatted age (KEEP #2871 · 3 h) in the plot, the event list and the table; Lines and timeSeries accept it as any other kind.

node tests/behavior-strip-browser-proof.mjs (after npm run build) is the packed browser proof: 100 rows × 288 windows and run e's real 43 rows (docs/fixtures/observation-run-e.json, from the observation storyboard, with the held-out calibration table of policy dl-ui-behavior-windows@1) in both themes at 1512 and 390 px, the keyboard cursor and row focus across the window boundary, the live append, the 13 producer-marked outliers, the candidates and the activity chips, then fps, p95 frame time and frames over 50 ms at idle and under wheel scrolling; BEHAVIOR_STRIP_EVIDENCE names its output directory.

React and package-boundary checks

npm ci
npm run typecheck
npm test
npm run build

The package-boundary test builds and packs Charts, installs the tarball in an OS-temporary consumer with a normal offline npm install (no peer-omission flags), asserts React/React DOM/React types cannot resolve, and executes the static root without DOM globals. It compares root exports, static SVG/tables, Refusals, report/chart/plot HTML and CSS with the untouched source implementation, then compiles a static-only consumer with lib: ["ES2022"], types: [] and skipLibCheck: false. Only afterward does it supply the installed React peer/types to verify the packed React runtime and declarations. It runs under npm test.

The viewport and keyboard proof covers100,124and1000 lanes in both themes and widths, real stylesheet imports, native focus through clock updates, dense marker hover, End/Home navigation and folded historical records.

The retained packed-browser fixture, ported from brand, and its browser proof check Chromium at 1100px and 390px, keyboard selection, emulated touch, light/dark Brand tokens, reduced motion, and top-layer positioning inside a transformed, paint-contained panel. It passes with React 18 and React 19; the React 18 consumer also compiles the packed declarations with React 18 types. Physical touch hardware and screen-reader audio remain unchecked.

Figure browser proof

node scripts/render-figures.mjs writes a comparison page from the retained fixtures and approved speed reference. node scripts/capture-figures.mjs uses Playwright Chromium to capture all three figures at phone and desktop widths in light and dark themes. The reviewed images are in docs/screenshots.

Live trace waterfall

import { FlowWaterfall } from '@tangle-network/charts/react';
import { catalogPrice, otlpFlowTrace, parseOtlpJsonl } from '@tangle-network/charts';
import '@tangle-network/charts/react/flow-waterfall.css';

// The one input contract: OTLP-JSONL from `traces convert <ref> --out spans.otlp.jsonl`.
const { spans } = parseOtlpJsonl(text);
// List prices come from the price owner at runtime (the Router's /v1/models), never a copied table.
const trace = otlpFlowTrace(spans, { price: catalogPrice(await (await fetch(catalogUrl)).json()) });
<FlowWaterfall trace={trace} live={running} defaultView="agents" renderIcon={brandMark} />;

defaultView="agents" opens the agent timeline: one row per agent with its model, thinking and tool marks, hatched waiting while it delegates, elbow connectors to each agent it spawned and back, retry and failure glyphs, and a headline with the measured speedup over sequential (agent busy time over wall time). Expanding an agent opens the span view with that agent unfolded. The span view virtualizes rows (5,000 spans scroll at 58+ fps in headless Chromium), pins ancestors while their children scroll, and labels each bar with its duration and recorded cost. Hover shows a span card; click pins it and zooms the axis to a span too narrow to read. onSelect(span) hears each opened row and pinned bar, so a host can open its own drill-down; markers={[{ at, label, tone }]} draws labelled instants, such as releases, across the rows. Give an agent span an outcome ({ tone, label }) and its lane takes the outcome color and the header a legend. detail becomes the agent's subtitle. The card is a table of what the row recorded, every value with its unit: model, duration, tokens in, out and cached (the span's promptTokens, completionTokens and cachedTokens, else the sum of its children's), LLM latency (median and p95 of its model calls), cost, LLM calls, tool calls by tool (the name before " · "), failed steps, and retries and restarts (retryOf, marks of kind restart); a value nobody recorded is left out. facts ([{ label, value }]) supplies authoritative host rows: a matching label replaces the inferred metric, case-insensitively, including explicit unknown or zero values; additional labels append. The last explicit value wins for a repeated label. Agent lane summaries also use Model calls and Tool calls facts verbatim (Model calls: 413), including unknowns and zero; no count is parsed from a formatted fact. Model/Provider identity and Ran for/Cost end labels also honor facts verbatim; full values remain in the card and label title when phone metadata clips. Opaque duration/cost facts do not compete in numeric superlatives. Family lanes summarize agent and failure counts. Synthetic family cards show Not recorded for model identity, model/tool counts, model latency, failures or retries when any member supplies that authoritative fact: formatted counts and percentiles cannot be aggregated. Family tokens become unknown if a member supplies a Tokens fact without its own numeric usage. Cost rollups use full numeric span prices when a member supplies a Cost fact; partial known prices retain their lower bound, and an entirely opaque price is Not recorded. Strings and percentiles are never parsed or summed. The card opens at the pointer, above or below its row, whichever side fits the viewport. onTimeChange reports where the reader scrubbed (null when live), and markers double as an event track (click, N or P to jump). A partly priced trace shows its floor and coverage (costCoverage), and billedUsd shows the real charge beside the list price. Ctrl/Cmd+wheel or +/-/0 zoom, Shift+wheel pans, the scrub track under the controls shows the whole trace, and arrow keys walk the tree.

Live views append spans as they arrive: traces watch <id|run-dir> --format jsonl --spans emits a traces.stream.span event per new or changed span, and otlpFlowTrace keeps the last span per span_id. Running agent spans grow to the clock between deltas without React renders. activityAt supplies trace-relative event times and medianEventGapMs preserves producer cadence before tail truncation; without it, child-step activity supplies the samples. Silence beyond five minutes and three median event gaps hatches the tail and names the quiet duration. The Errors filter expands matching agents and steps; its rug locates each error, marking unreported times separately. Containers below 600px use stacked 52px rows and a Key disclosure. Search supports Cmd/Ctrl+F and keeps ancestor context. Token totals are optional: omit unknown counts, retain measured zero, and guard optional token fields when reading a trace. Missing, partial and nonfinite usage displays a dash, including during playback. Missing cost and authoritative LLM counts stay Unknown; a cost priced from a catalog is labelled list price. Invalid intervals retain a per-row — timing mark. The static phase waterfall remains a separate chart for disjoint phases.

scripts/flow-waterfall-demo.mjs serves the demo routes (reference-shaped fixtures, recorded runs, a synthetic load trace and OTLP files from FW_DATA_DIR); scripts/flow-waterfall-capture.mjs captures frames, hover cards, fps, first paint and recordings, and with FW_LIVE_RELAY records a live session through scripts/flow-waterfall-live-relay.mjs with every span's event-to-pixel time.

Provenance. FlowWaterfall and its recorded Discovery fixture move from agent-app #1267/#1273 at 78a3e8f7fd9bf93b23afa8290db6d721db544237; the independent implementation copied no paid reference assets. Fixture provenance records measured timing, observed token totals and unknowns. trace-release.json and trace-incident.json are invented fixtures shaped like the design references (agent counts, durations and states), with original text. The historical baseline and fixed rate measurements are retained. The packed public consumer proof measures all five rates against actual browser wall time during live growth, with ±15% acceptance, using React 18 and 19 at 1100px and 390px.

npm run build
CHARTS_REACT_VERSION=18 CHARTS_PROOF_DIR=/tmp/charts-react18 node scripts/charts-browser-proof.mjs --record
CHARTS_REACT_VERSION=19 CHARTS_PROOF_DIR=/tmp/charts-react19 node scripts/charts-browser-proof.mjs --record

Consumers: agent-record adapts record provenance; agent-app supplies application trace producers; Creative consumes both. Intelligence already uses the row/axis primitives with its controlled virtualized tree. Tangle Website’s RecordedRunWaterfall uses the Header/Row primitives; Blueprint Agent’s execution timeline is a separately owned follow-up. Keep these existing primitives exported. Agent-record’s earlier agent-app dependency increased its installed graph from 134 to 256 package versions (tangle-tools #1674); adopting the lower chart owner removes that framework dependency, with the actual new graph measured by the deployment owner.

Record duration, clock, compact counts, USD and UTC helpers are re-exported from @tangle-network/ui/utils/format. The pure formatter leaf is bundled into static figures and declarations, so a static-only consumer needs neither UI nor React. React clocks remain external to preserve their single physical binding.

Topology metrics, Hub details and waterfall cards use UI's shared HoverCard/Popover intent, positioning and focus lifecycle. Delegated DOM and measured pointer anchors preserve the original row/keyboard semantics; local portal containers retain theme variables without copying computed styles. Waterfall playback and zoom commands live with their clock/window owners, including cancellation when a reader unmounts.

About

Shared Tangle chart library for research, benchmark records and application data.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages