@tangle-network/charts 0.21.12 → 0.21.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -222,6 +222,11 @@
222
222
  .fw-root .fw-settle .fw-end { animation: fw-land 260ms var(--fw-ease); }
223
223
  /* A live bar grows with the clock instead of stepping each tick. */
224
224
  .fw-root .fw-row[data-running] .fw-bar { transition: width 120ms linear; }
225
+ /* The chain that gates the trace's end: an accent edge under its bars, so cutting latency there is visibly what moves the end. */
226
+ .fw-root .fw-row[data-critical] .fw-bar { box-shadow: inset 0 -2px 0 var(--fw-accent); }
227
+ /* While searching, ancestors shown only for context recede and the matching names stand out. */
228
+ .fw-root .fw-row[data-search="context"] .fw-label { opacity: 0.55; }
229
+ .fw-root .fw-row[data-search="match"] .fw-name-text { font-weight: 600; }
225
230
  /* A restart: an amber notch through the bar at that moment; the bar keeps its own state. */
226
231
  .fw-root .fw-mark { position: absolute; z-index: 2; left: calc(var(--x0) - 1px); width: 2px; top: calc(50% + var(--fw-bar-shift) - var(--fw-bar-h) / 2 - 4px); height: calc(var(--fw-bar-h) + 8px);
227
232
  border-radius: 2px; background: var(--fw-amber); box-shadow: 0 0 0 1px var(--fw-surface); opacity: var(--on); pointer-events: auto; }
@@ -376,6 +376,12 @@ export declare function DotMap({ positions, radii, colors, palette, extent, rect
376
376
  //#region src/react/flow-waterfall-row.d.ts
377
377
  type FlowView = 'spans' | 'agents';
378
378
  //#endregion
379
+ //#region src/react/flow-waterfall-time.d.ts
380
+ interface ViewWindow {
381
+ startMs: number;
382
+ endMs: number;
383
+ }
384
+ //#endregion
379
385
  //#region src/react/flow-waterfall.d.ts
380
386
  interface FlowWaterfallProps {
381
387
  trace: FlowTrace;
@@ -412,9 +418,22 @@ interface FlowWaterfallProps {
412
418
  onDeselect?: () => void;
413
419
  /** Show the stat strip (default); a page with its own stat row passes false. */
414
420
  summary?: boolean;
421
+ /** Mark the steps on the chain that gates the trace's end. */
422
+ criticalPath?: boolean;
423
+ /** Controlled search text; pass with `onQueryChange` to keep it in the host's state, such as the URL. */
424
+ query?: string;
425
+ onQueryChange?: (query: string) => void;
426
+ /** Controlled folded span ids; pass with `onCollapsedChange`. Overrides `defaultCollapsed`. */
427
+ collapsed?: ReadonlySet<string>;
428
+ onCollapsedChange?: (collapsed: ReadonlySet<string>) => void;
429
+ /** Controlled time window, `null` for the whole trace; pass with `onZoomChange`. */
430
+ zoom?: ViewWindow | null;
431
+ onZoomChange?: (zoom: ViewWindow | null) => void;
432
+ /** Replaces the built-in search match (name, model, kind, status), such as to search the host's attributes. */
433
+ matchSpan?: (span: FlowSpan, query: string) => boolean;
415
434
  }
416
435
  /** Shared trace inspector: the agent timeline and the span waterfall over one clock.
417
436
  * Data remains producer-owned; playback, zoom and folding change presentation only. */
418
- export declare function FlowWaterfall({ trace, live, startedAt, label, defaultCollapsed, defaultView, renderIcon, maxHeight, onSelect, markers, onTimeChange, agentFill: fillProp, selectedId, onDeselect, description, summary: showSummary }: FlowWaterfallProps): import("react").JSX.Element;
437
+ export declare function FlowWaterfall({ trace, live, startedAt, label, defaultCollapsed, defaultView, renderIcon, maxHeight, onSelect, markers, onTimeChange, agentFill: fillProp, selectedId, onDeselect, description, summary: showSummary, criticalPath, query: queryProp, onQueryChange, collapsed: collapsedProp, onCollapsedChange, zoom: zoomProp, onZoomChange, matchSpan }: FlowWaterfallProps): import("react").JSX.Element;
419
438
  //#endregion
420
- export type { BandKind, ChartBand, ChartMark, DotMapLabel, DotMapProps, DotMapRect, Figure, FigureAtWidth, FigureViewProps, FlowWaterfallProps, GapBand, GapProps, LineSeries, MarkKind, MarkVerdict, MarkerShape, MeterSegment, PointFields, PointGlyph, Refusal, SparklineDirection, SparklineGeometry, SparklineGeometryOptions, SparklineLabelOptions, SparklinePoint, SparklineProps, SparklineValues, StackedBarBucket, StackedBarChartProps, StackedBarSegment, StackedBarSeries, TickOutcome, TimelineLane, TimelineOptions, TimelineSpan, TimelineTick, TimelineViewProps, WaterfallBarProps, WaterfallHeaderProps, WaterfallRowProps, WaterfallSpanGeometry, WaterfallTimeAxisProps, WaterfallWindow, XCategory, YScale, YTarget, YTick };
439
+ export type { BandKind, ChartBand, ChartMark, DotMapLabel, DotMapProps, DotMapRect, Figure, FigureAtWidth, FigureViewProps, FlowWaterfallProps, ViewWindow as FlowWaterfallWindow, GapBand, GapProps, LineSeries, MarkKind, MarkVerdict, MarkerShape, MeterSegment, PointFields, PointGlyph, Refusal, SparklineDirection, SparklineGeometry, SparklineGeometryOptions, SparklineLabelOptions, SparklinePoint, SparklineProps, SparklineValues, StackedBarBucket, StackedBarChartProps, StackedBarSegment, StackedBarSeries, TickOutcome, TimelineLane, TimelineOptions, TimelineSpan, TimelineTick, TimelineViewProps, WaterfallBarProps, WaterfallHeaderProps, WaterfallRowProps, WaterfallSpanGeometry, WaterfallTimeAxisProps, WaterfallWindow, XCategory, YScale, YTarget, YTick };
@@ -2590,15 +2590,19 @@ function segmentOf(row) {
2590
2590
  name: row.name
2591
2591
  };
2592
2592
  }
2593
+ /** The rows a search term matches by themselves, before ancestor context is added. */
2594
+ function searchMatchIds(rows, query, matchSpan) {
2595
+ const term = query.trim();
2596
+ const lower = term.toLowerCase();
2597
+ return new Set(rows.filter((row) => matchSpan ? matchSpan(row.span, term) : `${row.name} ${row.span.detail ?? ""} ${row.kind} ${row.ok ? "" : "failed"} ${row.span.status ?? ""}`.toLowerCase().includes(lower)).map((row) => row.id));
2598
+ }
2593
2599
  /** Search keeps ancestor context and deliberately reveals matches inside collapsed branches. */
2594
- function visibleFlowWaterfallRows(rows, query, collapsed) {
2600
+ function visibleFlowWaterfallRows(rows, query, collapsed, matchSpan) {
2595
2601
  const byId = new Map(rows.map((row) => [row.id, row]));
2596
- const term = query.trim().toLowerCase();
2597
- if (term) {
2602
+ if (query.trim()) {
2598
2603
  const matching = /* @__PURE__ */ new Set();
2599
- for (const row of rows) {
2600
- if (!`${row.name} ${row.span.detail ?? ""} ${row.kind} ${row.ok ? "" : "failed"} ${row.span.status ?? ""}`.toLowerCase().includes(term)) continue;
2601
- let cursor = row;
2604
+ for (const id of searchMatchIds(rows, query, matchSpan)) {
2605
+ let cursor = byId.get(id);
2602
2606
  while (cursor && !matching.has(cursor.id)) {
2603
2607
  matching.add(cursor.id);
2604
2608
  cursor = cursor.parentId ? byId.get(cursor.parentId) : void 0;
@@ -2628,6 +2632,33 @@ function slowestRow(rows, clockMs) {
2628
2632
  }
2629
2633
  return best;
2630
2634
  }
2635
+ /** The chain of steps that gates the trace's end: from each root, descend into the latest-ending child until a leaf; the root whose
2636
+ * chain ends last wins. Cutting latency anywhere else leaves the trace's end unchanged. */
2637
+ function criticalPathIds(rows) {
2638
+ const children = /* @__PURE__ */ new Map();
2639
+ for (const row of rows) {
2640
+ if (!row.parentId) continue;
2641
+ const siblings = children.get(row.parentId);
2642
+ if (siblings) siblings.push(row);
2643
+ else children.set(row.parentId, [row]);
2644
+ }
2645
+ let best;
2646
+ for (const root of rows) {
2647
+ if (root.parentId) continue;
2648
+ const chain = [];
2649
+ let end = root.span.endMs;
2650
+ for (let cursor = root; cursor;) {
2651
+ chain.push(cursor.id);
2652
+ end = Math.max(end, cursor.span.endMs);
2653
+ cursor = children.get(cursor.id)?.reduce((latest, child) => !latest || child.span.endMs > latest.span.endMs ? child : latest, void 0);
2654
+ }
2655
+ if (!best || end > best.end) best = {
2656
+ end,
2657
+ chain
2658
+ };
2659
+ }
2660
+ return new Set(best?.chain);
2661
+ }
2631
2662
  /** The agents a reader should look at first: the longest one by the clock, and the costliest when two or more are priced. */
2632
2663
  function agentCallouts(agents, clockMs) {
2633
2664
  let slowest, slowestMs = -1, costliest, top = -1, priced = 0, timed = 0;
@@ -3324,7 +3355,7 @@ function endLabel(row, clockMs) {
3324
3355
  }
3325
3356
  /** One trace row. Geometry comes from CSS variables, so a row re-renders only when its own state changes. */
3326
3357
  const FlowRow = memo(function FlowRow(props) {
3327
- const { row, view, top, collapsed, searching, slowest, costliest, active, focusable, selected, sticky, clockMs, renderIcon, fillByAgent, motion } = props;
3358
+ const { row, view, top, collapsed, searching, slowest, costliest, critical, matched, active, focusable, selected, sticky, clockMs, renderIcon, fillByAgent, motion } = props;
3328
3359
  const span = row.span;
3329
3360
  const running = spanRunning(span);
3330
3361
  const agentView = view === "agents";
@@ -3345,6 +3376,8 @@ const FlowRow = memo(function FlowRow(props) {
3345
3376
  "data-span-id": row.id,
3346
3377
  "data-span-kind": kind,
3347
3378
  "data-slowest": slowest || void 0,
3379
+ "data-critical": critical || void 0,
3380
+ "data-search": searching ? matched ? "match" : "context" : void 0,
3348
3381
  "data-running": running || void 0,
3349
3382
  "aria-level": agentView ? void 0 : row.depth + 1,
3350
3383
  "aria-expanded": row.hasChildren && !agentView ? !folded : void 0,
@@ -3477,7 +3510,7 @@ const FlowRow = memo(function FlowRow(props) {
3477
3510
  role: "gridcell",
3478
3511
  className: "fw-lane",
3479
3512
  "data-waterfall-track": true,
3480
- "aria-label": `${KIND_LABEL[span.kind]}, ${label}${slowest ? `, slowest ${agentView ? "agent" : "step"}` : ""}${costliest ? ", costliest agent" : ""}`,
3513
+ "aria-label": `${KIND_LABEL[span.kind]}, ${label}${slowest ? `, slowest ${agentView ? "agent" : "step"}` : ""}${costliest ? ", costliest agent" : ""}${critical ? ", critical path" : ""}`,
3481
3514
  children: invalid ? /* @__PURE__ */ jsx("span", {
3482
3515
  className: "fw-invalid",
3483
3516
  children: "—"
@@ -3539,7 +3572,7 @@ const FlowRow = memo(function FlowRow(props) {
3539
3572
  ] })
3540
3573
  })]
3541
3574
  });
3542
- }, (a, b) => a.row === b.row && a.view === b.view && a.top === b.top && a.collapsed === b.collapsed && a.searching === b.searching && a.slowest === b.slowest && a.costliest === b.costliest && a.active === b.active && a.focusable === b.focusable && a.selected === b.selected && a.sticky === b.sticky && a.motion === b.motion && a.renderIcon === b.renderIcon && a.fillByAgent === b.fillByAgent && a.sharedDetail === b.sharedDetail && a.expanded === b.expanded && (a.clockMs === b.clockMs || !open(a.row, a.clockMs) && !open(b.row, b.clockMs)));
3575
+ }, (a, b) => a.row === b.row && a.view === b.view && a.top === b.top && a.collapsed === b.collapsed && a.searching === b.searching && a.slowest === b.slowest && a.costliest === b.costliest && a.critical === b.critical && a.matched === b.matched && a.active === b.active && a.focusable === b.focusable && a.selected === b.selected && a.sticky === b.sticky && a.motion === b.motion && a.renderIcon === b.renderIcon && a.fillByAgent === b.fillByAgent && a.sharedDetail === b.sharedDetail && a.expanded === b.expanded && (a.clockMs === b.clockMs || !open(a.row, a.clockMs) && !open(b.row, b.clockMs)));
3543
3576
  /** An elbow connector between two rows at one moment, drawn in the row's tone; appears when the clock reaches it. */
3544
3577
  function LinkMark({ link, rowHeight, hot }) {
3545
3578
  const upper = Math.min(link.from, link.to), lower = Math.max(link.from, link.to);
@@ -3751,6 +3784,12 @@ function FlowBody(props) {
3751
3784
  props.byId,
3752
3785
  view
3753
3786
  ]);
3787
+ const matched = useMemo(() => searching ? searchMatchIds(rows, props.query, props.matchSpan) : void 0, [
3788
+ searching,
3789
+ rows,
3790
+ props.query,
3791
+ props.matchSpan
3792
+ ]);
3754
3793
  const index = useMemo(() => new Map(rows.map((row, i) => [row.id, i])), [rows]);
3755
3794
  const motion = useRowMotion(rows);
3756
3795
  const [jumped, setJumped] = useState();
@@ -3824,13 +3863,24 @@ function FlowBody(props) {
3824
3863
  }, []);
3825
3864
  const first = Math.max(0, chunk * CHUNK - OVERSCAN);
3826
3865
  const last = Math.min(rows.length, chunk * CHUNK + Math.ceil(viewport / rowHeight) + CHUNK + OVERSCAN);
3866
+ const scrollToRow = (i, element) => {
3867
+ const offset = (view === "spans" ? Math.min(STICKY_LEVELS, rows[i].depth) : 0) * rowHeight;
3868
+ if (i * rowHeight - offset < element.scrollTop) element.scrollTop = Math.max(0, i * rowHeight - offset);
3869
+ else if ((i + 1) * rowHeight > element.scrollTop + element.clientHeight) element.scrollTop = (i + 1) * rowHeight - element.clientHeight;
3870
+ };
3871
+ const shownSelection = useRef(void 0);
3872
+ useEffect(() => {
3873
+ if (!selectedId || selectedId === shownSelection.current) return;
3874
+ const i = index.get(selectedId);
3875
+ if (i === void 0 || !scroller.current) return;
3876
+ shownSelection.current = selectedId;
3877
+ scrollToRow(i, scroller.current);
3878
+ });
3827
3879
  const focusRow = (id) => {
3828
3880
  const i = index.get(id);
3829
3881
  const element = scroller.current;
3830
3882
  if (i === void 0 || !element) return;
3831
- const offset = (view === "spans" ? Math.min(STICKY_LEVELS, rows[i].depth) : 0) * rowHeight;
3832
- if (i * rowHeight - offset < element.scrollTop) element.scrollTop = Math.max(0, i * rowHeight - offset);
3833
- else if ((i + 1) * rowHeight > element.scrollTop + element.clientHeight) element.scrollTop = (i + 1) * rowHeight - element.clientHeight;
3883
+ scrollToRow(i, element);
3834
3884
  props.onActiveChange(id);
3835
3885
  requestAnimationFrame(() => element.querySelector(`.fw-canvas [data-span-id="${CSS.escape(id)}"]`)?.focus({ preventScroll: true }));
3836
3886
  };
@@ -3997,6 +4047,8 @@ function FlowBody(props) {
3997
4047
  searching,
3998
4048
  slowest: item.id === slowestId,
3999
4049
  costliest: item.id === props.costliestId,
4050
+ critical: props.criticalIds?.has(item.id),
4051
+ matched: matched?.has(item.id),
4000
4052
  active: item.id === activeId,
4001
4053
  focusable: !isSticky && (item.id === activeId || (activeId === void 0 || !index.has(activeId)) && i === 0),
4002
4054
  selected: item.id === selectedId,
@@ -4372,6 +4424,22 @@ const NAME_WIDTH = {
4372
4424
  spans: 320,
4373
4425
  agents: 240
4374
4426
  };
4427
+ /** State the host may own: its value wins when given, and every change is reported either way. */
4428
+ function useHostable(value, onChange, initial) {
4429
+ const [own, setOwn] = useState(initial);
4430
+ const current = value === void 0 ? own : value;
4431
+ const latest = useRef(current);
4432
+ latest.current = current;
4433
+ const report = useRef(onChange);
4434
+ report.current = onChange;
4435
+ const controlled = value !== void 0;
4436
+ return [current, useCallback((next) => {
4437
+ const resolved = typeof next === "function" ? next(latest.current) : next;
4438
+ latest.current = resolved;
4439
+ if (!controlled) setOwn(resolved);
4440
+ report.current?.(resolved);
4441
+ }, [controlled])];
4442
+ }
4375
4443
  /** The summary strip: as of the playhead while scrubbing, the producer's totals otherwise. */
4376
4444
  function summaryCells(trace, at, durationMs) {
4377
4445
  const cost = costSummary(at ? at.costUsd : trace.costUsd, trace);
@@ -4474,7 +4542,7 @@ function OutcomeLegend({ agents }) {
4474
4542
  }
4475
4543
  /** Shared trace inspector: the agent timeline and the span waterfall over one clock.
4476
4544
  * Data remains producer-owned; playback, zoom and folding change presentation only. */
4477
- function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defaultCollapsed = false, defaultView = "spans", renderIcon, maxHeight, onSelect, markers, onTimeChange, agentFill: fillProp, selectedId, onDeselect, description, summary: showSummary = true }) {
4545
+ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defaultCollapsed = false, defaultView = "spans", renderIcon, maxHeight, onSelect, markers, onTimeChange, agentFill: fillProp, selectedId, onDeselect, description, summary: showSummary = true, criticalPath = false, query: queryProp, onQueryChange, collapsed: collapsedProp, onCollapsedChange, zoom: zoomProp, onZoomChange, matchSpan }) {
4478
4546
  const root = useRef(null);
4479
4547
  const search = useRef(null);
4480
4548
  const slider = useRef(null);
@@ -4483,12 +4551,12 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4483
4551
  const searchId = useId();
4484
4552
  const cardId = useId();
4485
4553
  const [view, setView] = useState(defaultView);
4486
- const [query, setQuery] = useState("");
4487
- const [collapsed, setCollapsed] = useState(() => new Set(defaultCollapsed ? trace.spans.flatMap((span) => span.parentId ? [span.parentId] : []) : []));
4554
+ const [query, setQuery] = useHostable(queryProp, onQueryChange, "");
4555
+ const [collapsed, setCollapsed] = useHostable(collapsedProp, onCollapsedChange, () => new Set(defaultCollapsed ? trace.spans.flatMap((span) => span.parentId ? [span.parentId] : []) : []));
4488
4556
  const [speed, setSpeed] = useState(1);
4489
4557
  const [playing, setPlaying] = useState(false);
4490
4558
  const [position, setPosition] = useState(null);
4491
- const [zoom, setZoom] = useState(null);
4559
+ const [zoom, setZoom] = useHostable(zoomProp, onZoomChange, null);
4492
4560
  const [nameWidth, setNameWidth] = useState(NAME_WIDTH[defaultView]);
4493
4561
  const [laneWidth, setLaneWidth] = useState(0);
4494
4562
  const [card, setCard] = useState(null);
@@ -4544,6 +4612,7 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4544
4612
  resolution
4545
4613
  ]);
4546
4614
  const byId = useMemo(() => new Map(rows.map((row) => [row.id, row])), [rows]);
4615
+ const critical = useMemo(() => criticalPathIds(rows), [rows]);
4547
4616
  const agents = useMemo(() => {
4548
4617
  const pipelines = rows.filter((row) => row.kind === "pipeline");
4549
4618
  return pipelines.length ? pipelines : rows.filter((row) => !row.parentId);
@@ -4552,14 +4621,15 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4552
4621
  const [flippedFamilies, setFlippedFamilies] = useState(/* @__PURE__ */ new Set());
4553
4622
  const openFamilies = useMemo(() => defaultOpenFamilies(agents, UNFOLD_ROWS, flippedFamilies), [agents, flippedFamilies]);
4554
4623
  const [openSteps, setOpenSteps] = useState(/* @__PURE__ */ new Set());
4555
- const visible = useMemo(() => view === "agents" ? agentViewRows(agents, rows, openFamilies, openSteps) : visibleFlowWaterfallRows(rows, query, collapsed), [
4624
+ const visible = useMemo(() => view === "agents" ? agentViewRows(agents, rows, openFamilies, openSteps) : visibleFlowWaterfallRows(rows, query, collapsed, matchSpan), [
4556
4625
  view,
4557
4626
  agents,
4558
4627
  rows,
4559
4628
  openFamilies,
4560
4629
  openSteps,
4561
4630
  query,
4562
- collapsed
4631
+ collapsed,
4632
+ matchSpan
4563
4633
  ]);
4564
4634
  const slowest = view === "spans" ? slowestRow(rows, clockMs) : void 0;
4565
4635
  const callouts = useMemo(() => view === "agents" ? agentCallouts(visible, clockMs) : {}, [
@@ -4933,6 +5003,8 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4933
5003
  maxHeight: rowsHeight,
4934
5004
  collapsed,
4935
5005
  searching: Boolean(query.trim()),
5006
+ query,
5007
+ matchSpan,
4936
5008
  pauses: trace.pauses,
4937
5009
  cursorLine: position !== null || playing,
4938
5010
  sharedDetail,
@@ -4940,6 +5012,7 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4940
5012
  holdHeight: isLive,
4941
5013
  slowestId: slowest?.id ?? callouts.slowestId,
4942
5014
  costliestId: callouts.costliestId,
5015
+ criticalIds: criticalPath && view === "spans" ? critical : void 0,
4943
5016
  activeId,
4944
5017
  selectedId: selection,
4945
5018
  focusId: cardAgent?.id ?? (view === "agents" ? activeId : void 0),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/charts",
3
- "version": "0.21.12",
3
+ "version": "0.21.14",
4
4
  "description": "Static SVG and HTML figures for benchmarks and operational timing. Draws from producer data and refuses what it cannot draw honestly.",
5
5
  "type": "module",
6
6
  "license": "MIT",