@tangle-network/charts 0.21.11 → 0.21.13

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,8 @@
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); }
225
227
  /* A restart: an amber notch through the bar at that moment; the bar keeps its own state. */
226
228
  .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
229
  border-radius: 2px; background: var(--fw-amber); box-shadow: 0 0 0 1px var(--fw-surface); opacity: var(--on); pointer-events: auto; }
@@ -321,10 +323,6 @@
321
323
 
322
324
  /* Span card */
323
325
  .fw-root .fw-card-anchor { display: contents; }
324
- /* Agent view: the card docks under the lanes instead of floating over the plot (judges: "covers the data it describes"). */
325
- /* Two classes on the root so these win over the base .fw-root .fw-card rule that follows. */
326
- .fw-root.fw-root-agents .fw-card-anchor { display: block; padding: 0 24px 12px; }
327
- .fw-root.fw-root-agents .fw-card, .fw-root.fw-root-agents .fw-card-above .fw-card { position: relative; left: auto !important; top: auto !important; transform: none; animation-name: fw-arrive; width: auto; max-width: 560px; }
328
326
  .fw-root .fw-seg-alt { filter: brightness(.78); }
329
327
  /* Agent view: the scrubber track shows how far the run has played, so at rest it reads full, not empty. */
330
328
  .fw-root.fw-root-agents .fw-slider { background: linear-gradient(90deg, color-mix(in oklab, var(--fw-text) 32%, transparent) var(--fw-progress), var(--fw-well) var(--fw-progress)) 0 50% / 100% 4px no-repeat; }
@@ -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 };
@@ -2591,13 +2591,13 @@ function segmentOf(row) {
2591
2591
  };
2592
2592
  }
2593
2593
  /** Search keeps ancestor context and deliberately reveals matches inside collapsed branches. */
2594
- function visibleFlowWaterfallRows(rows, query, collapsed) {
2594
+ function visibleFlowWaterfallRows(rows, query, collapsed, matchSpan) {
2595
2595
  const byId = new Map(rows.map((row) => [row.id, row]));
2596
2596
  const term = query.trim().toLowerCase();
2597
2597
  if (term) {
2598
2598
  const matching = /* @__PURE__ */ new Set();
2599
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;
2600
+ if (matchSpan ? !matchSpan(row.span, query.trim()) : !`${row.name} ${row.span.detail ?? ""} ${row.kind} ${row.ok ? "" : "failed"} ${row.span.status ?? ""}`.toLowerCase().includes(term)) continue;
2601
2601
  let cursor = row;
2602
2602
  while (cursor && !matching.has(cursor.id)) {
2603
2603
  matching.add(cursor.id);
@@ -2628,6 +2628,28 @@ function slowestRow(rows, clockMs) {
2628
2628
  }
2629
2629
  return best;
2630
2630
  }
2631
+ /** 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
2632
+ * chain ends last wins. Cutting latency anywhere else leaves the trace's end unchanged. */
2633
+ function criticalPathIds(rows) {
2634
+ const children = /* @__PURE__ */ new Map();
2635
+ for (const row of rows) if (row.parentId) children.set(row.parentId, [...children.get(row.parentId) ?? [], row]);
2636
+ let best;
2637
+ for (const root of rows) {
2638
+ if (root.parentId) continue;
2639
+ const chain = [];
2640
+ let end = root.span.endMs;
2641
+ for (let cursor = root; cursor;) {
2642
+ chain.push(cursor.id);
2643
+ end = Math.max(end, cursor.span.endMs);
2644
+ cursor = children.get(cursor.id)?.reduce((latest, child) => !latest || child.span.endMs > latest.span.endMs ? child : latest, void 0);
2645
+ }
2646
+ if (!best || end > best.end) best = {
2647
+ end,
2648
+ chain
2649
+ };
2650
+ }
2651
+ return new Set(best?.chain);
2652
+ }
2631
2653
  /** The agents a reader should look at first: the longest one by the clock, and the costliest when two or more are priced. */
2632
2654
  function agentCallouts(agents, clockMs) {
2633
2655
  let slowest, slowestMs = -1, costliest, top = -1, priced = 0, timed = 0;
@@ -3324,7 +3346,7 @@ function endLabel(row, clockMs) {
3324
3346
  }
3325
3347
  /** One trace row. Geometry comes from CSS variables, so a row re-renders only when its own state changes. */
3326
3348
  const FlowRow = memo(function FlowRow(props) {
3327
- const { row, view, top, collapsed, searching, slowest, costliest, active, focusable, selected, sticky, clockMs, renderIcon, fillByAgent, motion } = props;
3349
+ const { row, view, top, collapsed, searching, slowest, costliest, critical, active, focusable, selected, sticky, clockMs, renderIcon, fillByAgent, motion } = props;
3328
3350
  const span = row.span;
3329
3351
  const running = spanRunning(span);
3330
3352
  const agentView = view === "agents";
@@ -3345,6 +3367,7 @@ const FlowRow = memo(function FlowRow(props) {
3345
3367
  "data-span-id": row.id,
3346
3368
  "data-span-kind": kind,
3347
3369
  "data-slowest": slowest || void 0,
3370
+ "data-critical": critical || void 0,
3348
3371
  "data-running": running || void 0,
3349
3372
  "aria-level": agentView ? void 0 : row.depth + 1,
3350
3373
  "aria-expanded": row.hasChildren && !agentView ? !folded : void 0,
@@ -3477,7 +3500,7 @@ const FlowRow = memo(function FlowRow(props) {
3477
3500
  role: "gridcell",
3478
3501
  className: "fw-lane",
3479
3502
  "data-waterfall-track": true,
3480
- "aria-label": `${KIND_LABEL[span.kind]}, ${label}${slowest ? `, slowest ${agentView ? "agent" : "step"}` : ""}${costliest ? ", costliest agent" : ""}`,
3503
+ "aria-label": `${KIND_LABEL[span.kind]}, ${label}${slowest ? `, slowest ${agentView ? "agent" : "step"}` : ""}${costliest ? ", costliest agent" : ""}${critical ? ", critical path" : ""}`,
3481
3504
  children: invalid ? /* @__PURE__ */ jsx("span", {
3482
3505
  className: "fw-invalid",
3483
3506
  children: "—"
@@ -3539,7 +3562,7 @@ const FlowRow = memo(function FlowRow(props) {
3539
3562
  ] })
3540
3563
  })]
3541
3564
  });
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)));
3565
+ }, (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.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
3566
  /** An elbow connector between two rows at one moment, drawn in the row's tone; appears when the clock reaches it. */
3544
3567
  function LinkMark({ link, rowHeight, hot }) {
3545
3568
  const upper = Math.min(link.from, link.to), lower = Math.max(link.from, link.to);
@@ -3997,6 +4020,7 @@ function FlowBody(props) {
3997
4020
  searching,
3998
4021
  slowest: item.id === slowestId,
3999
4022
  costliest: item.id === props.costliestId,
4023
+ critical: props.criticalIds?.has(item.id),
4000
4024
  active: item.id === activeId,
4001
4025
  focusable: !isSticky && (item.id === activeId || (activeId === void 0 || !index.has(activeId)) && i === 0),
4002
4026
  selected: item.id === selectedId,
@@ -4372,6 +4396,22 @@ const NAME_WIDTH = {
4372
4396
  spans: 320,
4373
4397
  agents: 240
4374
4398
  };
4399
+ /** State the host may own: its value wins when given, and every change is reported either way. */
4400
+ function useHostable(value, onChange, initial) {
4401
+ const [own, setOwn] = useState(initial);
4402
+ const current = value === void 0 ? own : value;
4403
+ const latest = useRef(current);
4404
+ latest.current = current;
4405
+ const report = useRef(onChange);
4406
+ report.current = onChange;
4407
+ const controlled = value !== void 0;
4408
+ return [current, useCallback((next) => {
4409
+ const resolved = typeof next === "function" ? next(latest.current) : next;
4410
+ latest.current = resolved;
4411
+ if (!controlled) setOwn(resolved);
4412
+ report.current?.(resolved);
4413
+ }, [controlled])];
4414
+ }
4375
4415
  /** The summary strip: as of the playhead while scrubbing, the producer's totals otherwise. */
4376
4416
  function summaryCells(trace, at, durationMs) {
4377
4417
  const cost = costSummary(at ? at.costUsd : trace.costUsd, trace);
@@ -4474,7 +4514,7 @@ function OutcomeLegend({ agents }) {
4474
4514
  }
4475
4515
  /** Shared trace inspector: the agent timeline and the span waterfall over one clock.
4476
4516
  * 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 }) {
4517
+ 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
4518
  const root = useRef(null);
4479
4519
  const search = useRef(null);
4480
4520
  const slider = useRef(null);
@@ -4483,12 +4523,12 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4483
4523
  const searchId = useId();
4484
4524
  const cardId = useId();
4485
4525
  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] : []) : []));
4526
+ const [query, setQuery] = useHostable(queryProp, onQueryChange, "");
4527
+ const [collapsed, setCollapsed] = useHostable(collapsedProp, onCollapsedChange, () => new Set(defaultCollapsed ? trace.spans.flatMap((span) => span.parentId ? [span.parentId] : []) : []));
4488
4528
  const [speed, setSpeed] = useState(1);
4489
4529
  const [playing, setPlaying] = useState(false);
4490
4530
  const [position, setPosition] = useState(null);
4491
- const [zoom, setZoom] = useState(null);
4531
+ const [zoom, setZoom] = useHostable(zoomProp, onZoomChange, null);
4492
4532
  const [nameWidth, setNameWidth] = useState(NAME_WIDTH[defaultView]);
4493
4533
  const [laneWidth, setLaneWidth] = useState(0);
4494
4534
  const [card, setCard] = useState(null);
@@ -4544,6 +4584,7 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4544
4584
  resolution
4545
4585
  ]);
4546
4586
  const byId = useMemo(() => new Map(rows.map((row) => [row.id, row])), [rows]);
4587
+ const critical = useMemo(() => criticalPathIds(rows), [rows]);
4547
4588
  const agents = useMemo(() => {
4548
4589
  const pipelines = rows.filter((row) => row.kind === "pipeline");
4549
4590
  return pipelines.length ? pipelines : rows.filter((row) => !row.parentId);
@@ -4552,14 +4593,15 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4552
4593
  const [flippedFamilies, setFlippedFamilies] = useState(/* @__PURE__ */ new Set());
4553
4594
  const openFamilies = useMemo(() => defaultOpenFamilies(agents, UNFOLD_ROWS, flippedFamilies), [agents, flippedFamilies]);
4554
4595
  const [openSteps, setOpenSteps] = useState(/* @__PURE__ */ new Set());
4555
- const visible = useMemo(() => view === "agents" ? agentViewRows(agents, rows, openFamilies, openSteps) : visibleFlowWaterfallRows(rows, query, collapsed), [
4596
+ const visible = useMemo(() => view === "agents" ? agentViewRows(agents, rows, openFamilies, openSteps) : visibleFlowWaterfallRows(rows, query, collapsed, matchSpan), [
4556
4597
  view,
4557
4598
  agents,
4558
4599
  rows,
4559
4600
  openFamilies,
4560
4601
  openSteps,
4561
4602
  query,
4562
- collapsed
4603
+ collapsed,
4604
+ matchSpan
4563
4605
  ]);
4564
4606
  const slowest = view === "spans" ? slowestRow(rows, clockMs) : void 0;
4565
4607
  const callouts = useMemo(() => view === "agents" ? agentCallouts(visible, clockMs) : {}, [
@@ -4940,6 +4982,7 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4940
4982
  holdHeight: isLive,
4941
4983
  slowestId: slowest?.id ?? callouts.slowestId,
4942
4984
  costliestId: callouts.costliestId,
4985
+ criticalIds: criticalPath && view === "spans" ? critical : void 0,
4943
4986
  activeId,
4944
4987
  selectedId: selection,
4945
4988
  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.11",
3
+ "version": "0.21.13",
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",