@tangle-network/charts 0.2.0 → 0.3.0

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.
package/README.md CHANGED
@@ -1,6 +1,17 @@
1
1
  # @tangle-network/charts
2
2
 
3
- Benchmark figures as static SVG and HTML tables.
3
+ Benchmark figures as static SVG and HTML tables, plus optional React timing primitives.
4
+
5
+ `WaterfallHeader`, `WaterfallTimeAxis`, `WaterfallBar` and `WaterfallRow` from
6
+ `@tangle-network/charts/react` own the timing presentation extracted from
7
+ Intelligence's trace viewer. Supply actual span start/end milliseconds and the
8
+ same `{ startMs, endMs }` window to every row. Hierarchy, selection, attribution,
9
+ formatting and failure interpretation stay with the consumer. The original
10
+ minimum/clipped marker remains visible; its tooltip reports the actual duration.
11
+ Set `--waterfall-timing-width`, `--waterfall-duration-width` and the existing
12
+ `--intel-waterfall-*` colors for the consumer's layout and theme. No stylesheet,
13
+ router or React DOM dependency is required.
14
+
4
15
  Five figures cover the decisions a benchmark reader makes:
5
16
 
6
17
  | Figure | Question |
@@ -1,4 +1,4 @@
1
- import { ReactElement } from "react";
1
+ import { ReactElement, ReactNode } from "react";
2
2
  //#region src/react/sparkline.d.ts
3
3
  type SparklineDirection = "rising" | "falling" | "flat";
4
4
  type SparklineValues = readonly (number | null)[];
@@ -81,4 +81,54 @@ interface StackedBarChartProps {
81
81
  }
82
82
  export declare function StackedBarChart(props: StackedBarChartProps): import("react").JSX.Element;
83
83
  //#endregion
84
- export type { SparklineDirection, SparklineGeometry, SparklineGeometryOptions, SparklineLabelOptions, SparklinePoint, SparklineProps, SparklineValues, StackedBarBucket, StackedBarChartProps, StackedBarSegment, StackedBarSeries };
84
+ //#region src/react/waterfall.d.ts
85
+ interface WaterfallWindow {
86
+ readonly startMs: number;
87
+ readonly endMs: number;
88
+ }
89
+ interface WaterfallSpanGeometry {
90
+ readonly offsetPct: number;
91
+ readonly widthPct: number;
92
+ readonly startOffsetMs: number;
93
+ readonly durationMs: number;
94
+ readonly clipped: boolean;
95
+ }
96
+ /** The Intelligence clipping/minimum-marker policy, applied to relative times. */
97
+ export declare function waterfallSpanGeometry(startMs: number, endMs: number, window: WaterfallWindow): WaterfallSpanGeometry;
98
+ /** Intelligence's round-duration ticks, with the original 208px default. */
99
+ export declare function waterfallTicks(windowMs: number, widthPx?: number, minLabelGapPx?: number): number[];
100
+ export declare function formatWaterfallDuration(ms: number): string;
101
+ interface WaterfallTimeAxisProps {
102
+ windowMs: number;
103
+ originMs?: number;
104
+ widthPx?: number;
105
+ formatDuration?: (ms: number) => string;
106
+ className?: string;
107
+ }
108
+ export declare function WaterfallTimeAxis({ windowMs, originMs, widthPx, formatDuration, className }: WaterfallTimeAxisProps): import("react").JSX.Element | null;
109
+ interface WaterfallBarProps {
110
+ startMs: number;
111
+ endMs: number;
112
+ window: WaterfallWindow;
113
+ tone?: "default" | "error" | "critical";
114
+ formatDuration?: (ms: number) => string;
115
+ className?: string;
116
+ }
117
+ export declare function WaterfallBar({ startMs, endMs, window, tone, formatDuration, className }: WaterfallBarProps): import("react").JSX.Element;
118
+ interface WaterfallRowProps extends Omit<WaterfallBarProps, "className"> {
119
+ label: ReactNode;
120
+ attribution?: ReactNode;
121
+ timingClassName?: string;
122
+ durationClassName?: string;
123
+ className?: string;
124
+ }
125
+ /** Non-interactive row body; its caller owns treeitems, buttons and selection. */
126
+ export declare function WaterfallRow({ label, attribution, startMs, endMs, window, tone, formatDuration, className, timingClassName, durationClassName }: WaterfallRowProps): import("react").JSX.Element;
127
+ interface WaterfallHeaderProps extends WaterfallTimeAxisProps {
128
+ label?: ReactNode;
129
+ attribution?: ReactNode;
130
+ durationLabel?: ReactNode;
131
+ }
132
+ export declare function WaterfallHeader({ label, attribution, durationLabel, ...axis }: WaterfallHeaderProps): import("react").JSX.Element;
133
+ //#endregion
134
+ export type { SparklineDirection, SparklineGeometry, SparklineGeometryOptions, SparklineLabelOptions, SparklinePoint, SparklineProps, SparklineValues, StackedBarBucket, StackedBarChartProps, StackedBarSegment, StackedBarSeries, WaterfallBarProps, WaterfallHeaderProps, WaterfallRowProps, WaterfallSpanGeometry, WaterfallTimeAxisProps, WaterfallWindow };
@@ -535,4 +535,224 @@ function StackedBarChart(props) {
535
535
  });
536
536
  }
537
537
  //#endregion
538
- export { DEFAULT_SPARKLINE_EMPTY_LABEL, DEFAULT_SPARKLINE_HEIGHT, DEFAULT_SPARKLINE_LABEL, DEFAULT_SPARKLINE_UNAVAILABLE_LABEL, DEFAULT_SPARKLINE_WIDTH, Sparkline, StackedBarChart, formatSparklineValue, sparklineGeometry, sparklineLabel, sparklinePointsAttribute, sparklineReadings };
538
+ //#region src/react/waterfall.tsx
539
+ /** The Intelligence clipping/minimum-marker policy, applied to relative times. */
540
+ function waterfallSpanGeometry(startMs, endMs, window) {
541
+ if (![
542
+ startMs,
543
+ endMs,
544
+ window.startMs,
545
+ window.endMs
546
+ ].every(Number.isFinite) || endMs < startMs || window.endMs < window.startMs) throw new RangeError("Waterfall requires finite, ordered span and window times");
547
+ const durationMs = endMs - startMs;
548
+ const startOffsetMs = startMs - window.startMs;
549
+ const windowMs = window.endMs - window.startMs;
550
+ const span = windowMs === 0 ? 1 : windowMs;
551
+ const rawOffsetPct = startOffsetMs / span * 100;
552
+ const endPct = rawOffsetPct + durationMs / span * 100;
553
+ const fullyOutside = endPct <= 0 || rawOffsetPct >= 100;
554
+ const offsetPct = Math.min(99, Math.max(0, rawOffsetPct));
555
+ return {
556
+ offsetPct,
557
+ widthPct: fullyOutside ? .5 : Math.max(.5, Math.min(100 - offsetPct, endPct - offsetPct)),
558
+ startOffsetMs,
559
+ durationMs,
560
+ clipped: fullyOutside || rawOffsetPct < 0 || endPct > 100
561
+ };
562
+ }
563
+ function niceTickMs(spanMs, target = 4) {
564
+ if (!(spanMs > 0)) return 1;
565
+ const rough = spanMs / target;
566
+ const mag = 10 ** Math.floor(Math.log10(rough));
567
+ const norm = rough / mag;
568
+ return (norm >= 5 ? 5 : norm >= 2 ? 2 : 1) * mag;
569
+ }
570
+ /** Intelligence's round-duration ticks, with the original 208px default. */
571
+ function waterfallTicks(windowMs, widthPx = 208, minLabelGapPx = 64) {
572
+ if (!Number.isFinite(windowMs) || windowMs < 0 || !Number.isFinite(widthPx) || widthPx <= 0 || !Number.isFinite(minLabelGapPx) || minLabelGapPx <= 0) throw new RangeError("Waterfall axis requires finite, nonnegative time and positive dimensions");
573
+ if (windowMs === 0) return [];
574
+ const step = niceTickMs(windowMs, Math.max(2, Math.floor(widthPx / minLabelGapPx)));
575
+ const raw = [];
576
+ for (let t = 0; t <= windowMs + 1e-6; t += step) raw.push(t);
577
+ const minGapPct = minLabelGapPx / widthPx * 100;
578
+ const kept = [];
579
+ let lastPct = Number.NEGATIVE_INFINITY;
580
+ for (const t of raw) {
581
+ const pct = t / windowMs * 100;
582
+ if (kept.length === 0 || pct - lastPct >= minGapPct) {
583
+ kept.push(t);
584
+ lastPct = pct;
585
+ }
586
+ }
587
+ return kept;
588
+ }
589
+ function formatWaterfallDuration(ms) {
590
+ if (ms < 1e3) return `${Math.round(ms)}ms`;
591
+ if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
592
+ if (ms < 36e5) return `${(ms / 6e4).toFixed(1)}m`;
593
+ return `${(ms / 36e5).toFixed(1)}h`;
594
+ }
595
+ const timingCell = {
596
+ width: "var(--waterfall-timing-width, 208px)",
597
+ flexShrink: 0
598
+ };
599
+ const rowStyle = {
600
+ display: "flex",
601
+ alignItems: "center",
602
+ gap: "0.5rem",
603
+ width: "100%",
604
+ minWidth: 0
605
+ };
606
+ const labelStyle = {
607
+ display: "flex",
608
+ alignItems: "center",
609
+ gap: "0.5rem",
610
+ flex: "1 1 0%",
611
+ minWidth: 0
612
+ };
613
+ const durationStyle = {
614
+ width: "var(--waterfall-duration-width, 4rem)",
615
+ flexShrink: 0,
616
+ textAlign: "right",
617
+ fontFamily: "var(--font-mono, monospace)",
618
+ fontVariantNumeric: "tabular-nums"
619
+ };
620
+ const ordered = (start, end) => Number.isFinite(start) && Number.isFinite(end) && end >= start;
621
+ function WaterfallTimeAxis({ windowMs, originMs = 0, widthPx = 208, formatDuration = formatWaterfallDuration, className }) {
622
+ if (!ordered(0, windowMs) || !Number.isFinite(originMs)) return null;
623
+ const ticks = waterfallTicks(windowMs, widthPx);
624
+ if (windowMs === 0) return null;
625
+ return /* @__PURE__ */ jsx("div", {
626
+ className,
627
+ "data-waterfall-axis": "",
628
+ "aria-hidden": "true",
629
+ style: {
630
+ ...timingCell,
631
+ position: "relative",
632
+ height: "var(--waterfall-axis-height, 1.5rem)"
633
+ },
634
+ children: ticks.map((t) => {
635
+ const pct = t / windowMs * 100;
636
+ return /* @__PURE__ */ jsxs("span", {
637
+ style: {
638
+ position: "absolute",
639
+ top: 0,
640
+ bottom: 0,
641
+ left: `${pct}%`,
642
+ display: "flex",
643
+ flexDirection: "column",
644
+ alignItems: "start"
645
+ },
646
+ children: [/* @__PURE__ */ jsx("span", { style: {
647
+ height: "0.5rem",
648
+ width: 1,
649
+ background: "var(--intel-chart-grid, var(--color-border, #d1d3e1))"
650
+ } }), /* @__PURE__ */ jsx("span", {
651
+ style: {
652
+ marginTop: "0.25rem",
653
+ whiteSpace: "nowrap",
654
+ fontFamily: "var(--font-mono, monospace)",
655
+ fontSize: "var(--waterfall-axis-font-size, 11px)",
656
+ lineHeight: 1,
657
+ fontVariantNumeric: "tabular-nums",
658
+ transform: pct > 75 ? "translateX(-100%)" : void 0
659
+ },
660
+ children: formatDuration(originMs + t)
661
+ })]
662
+ }, t);
663
+ })
664
+ });
665
+ }
666
+ function WaterfallBar({ startMs, endMs, window, tone = "default", formatDuration = formatWaterfallDuration, className }) {
667
+ if (!ordered(startMs, endMs) || !ordered(window.startMs, window.endMs)) return /* @__PURE__ */ jsx("span", {
668
+ className,
669
+ "data-waterfall-track": "",
670
+ "data-unavailable": "true",
671
+ title: "Timing unavailable",
672
+ style: {
673
+ ...timingCell,
674
+ textAlign: "center"
675
+ },
676
+ children: "—"
677
+ });
678
+ const geometry = waterfallSpanGeometry(startMs, endMs, window);
679
+ const color = tone === "error" ? "var(--intel-waterfall-error, #dc2626)" : tone === "critical" ? "var(--intel-waterfall-critical, #0891b2)" : "var(--intel-waterfall-bar, #2563eb)";
680
+ return /* @__PURE__ */ jsx("span", {
681
+ className,
682
+ "data-waterfall-track": "",
683
+ "data-tone": tone,
684
+ title: `+${formatDuration(geometry.startOffsetMs)} · ${formatDuration(geometry.durationMs)}${tone === "critical" ? " · critical path" : ""}`,
685
+ style: {
686
+ ...timingCell,
687
+ position: "relative",
688
+ height: "var(--waterfall-bar-height, 0.875rem)",
689
+ overflow: "hidden",
690
+ borderRadius: "var(--waterfall-bar-radius, 999px)",
691
+ background: "var(--intel-waterfall-track, rgb(148 163 184 / 22%))"
692
+ },
693
+ children: /* @__PURE__ */ jsx("span", {
694
+ "data-waterfall-bar": "",
695
+ "data-clipped": geometry.clipped || void 0,
696
+ style: {
697
+ position: "absolute",
698
+ top: 0,
699
+ height: "100%",
700
+ borderRadius: "inherit",
701
+ background: color,
702
+ left: `${geometry.offsetPct}%`,
703
+ width: `${geometry.widthPct}%`,
704
+ boxShadow: tone === "default" ? void 0 : `0 0 0 1px ${color}`
705
+ }
706
+ })
707
+ });
708
+ }
709
+ /** Non-interactive row body; its caller owns treeitems, buttons and selection. */
710
+ function WaterfallRow({ label, attribution, startMs, endMs, window, tone, formatDuration = formatWaterfallDuration, className, timingClassName, durationClassName }) {
711
+ return /* @__PURE__ */ jsxs("span", {
712
+ "data-waterfall-row": "",
713
+ className,
714
+ style: rowStyle,
715
+ children: [
716
+ /* @__PURE__ */ jsx("span", {
717
+ "data-waterfall-label": "",
718
+ style: labelStyle,
719
+ children: label
720
+ }),
721
+ attribution,
722
+ /* @__PURE__ */ jsx(WaterfallBar, {
723
+ startMs,
724
+ endMs,
725
+ window,
726
+ tone,
727
+ formatDuration,
728
+ className: timingClassName
729
+ }),
730
+ /* @__PURE__ */ jsx("span", {
731
+ "data-waterfall-duration": "",
732
+ className: durationClassName,
733
+ style: durationStyle,
734
+ children: ordered(startMs, endMs) ? formatDuration(endMs - startMs) : "—"
735
+ })
736
+ ]
737
+ });
738
+ }
739
+ function WaterfallHeader({ label = "Span", attribution, durationLabel = "Duration", ...axis }) {
740
+ return /* @__PURE__ */ jsxs("div", {
741
+ "data-waterfall-header": "",
742
+ style: rowStyle,
743
+ children: [
744
+ /* @__PURE__ */ jsx("span", {
745
+ style: labelStyle,
746
+ children: label
747
+ }),
748
+ attribution,
749
+ /* @__PURE__ */ jsx(WaterfallTimeAxis, { ...axis }),
750
+ /* @__PURE__ */ jsx("span", {
751
+ style: durationStyle,
752
+ children: durationLabel
753
+ })
754
+ ]
755
+ });
756
+ }
757
+ //#endregion
758
+ export { DEFAULT_SPARKLINE_EMPTY_LABEL, DEFAULT_SPARKLINE_HEIGHT, DEFAULT_SPARKLINE_LABEL, DEFAULT_SPARKLINE_UNAVAILABLE_LABEL, DEFAULT_SPARKLINE_WIDTH, Sparkline, StackedBarChart, WaterfallBar, WaterfallHeader, WaterfallRow, WaterfallTimeAxis, formatSparklineValue, formatWaterfallDuration, sparklineGeometry, sparklineLabel, sparklinePointsAttribute, sparklineReadings, waterfallSpanGeometry, waterfallTicks };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/charts",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Benchmark figures as static SVG and HTML tables: ranked rates, cost frontier, comparison intervals, task matrix and breakdown. Draws from producer data and refuses what it cannot draw honestly.",
5
5
  "type": "module",
6
6
  "license": "MIT",