@tangle-network/charts 0.3.1 → 0.6.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.
@@ -1,5 +1,6 @@
1
- import { jsx, jsxs } from "react/jsx-runtime";
2
- import { Fragment, useId, useRef, useState } from "react";
1
+ import { d as hatchPath, i as eventsGap, m as markerPath, o as isRefusal } from "../events-Cw6abvgR.js";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { Fragment as Fragment$1, useEffect, useId, useRef, useState } from "react";
3
4
  //#region src/react/sparkline.tsx
4
5
  const DEFAULT_SPARKLINE_WIDTH = 96;
5
6
  const DEFAULT_SPARKLINE_HEIGHT = 24;
@@ -515,7 +516,7 @@ function StackedBarChart(props) {
515
516
  children: "Value"
516
517
  })
517
518
  ] }) }),
518
- /* @__PURE__ */ jsx("tbody", { children: buckets.map((bucket) => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("tr", { children: [
519
+ /* @__PURE__ */ jsx("tbody", { children: buckets.map((bucket) => /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("tr", { children: [
519
520
  /* @__PURE__ */ jsx("th", {
520
521
  scope: "row",
521
522
  children: bucket.label
@@ -755,4 +756,930 @@ function WaterfallHeader({ label = "Span", attribution, durationLabel = "Duratio
755
756
  });
756
757
  }
757
758
  //#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 };
759
+ //#region src/react/live.tsx
760
+ /** Reusable, em-scaled charts for live operational series. Values, labels and status colors belong to callers. */
761
+ const LIVE_SERIES = [
762
+ "#8b7cf6",
763
+ "#0ea5a0",
764
+ "#c98500",
765
+ "#e2508f",
766
+ "#4a90e2"
767
+ ];
768
+ const formatCompact = (value) => value === null || value === void 0 || !Number.isFinite(value) ? "—" : new Intl.NumberFormat("en-US", {
769
+ notation: "compact",
770
+ maximumFractionDigits: 1
771
+ }).format(value);
772
+ const formatShare = (value) => value === null || !Number.isFinite(value) ? "—" : `${Math.round(value * 100)}%`;
773
+ /** The rendered width and font size of an element, so an SVG chart draws at its real size and its geometry follows
774
+ * the page's type. */
775
+ function useWidth() {
776
+ const [element, ref] = useState(null);
777
+ const [size, setSize] = useState({
778
+ width: 0,
779
+ em: 16
780
+ });
781
+ useEffect(() => {
782
+ if (!element) return;
783
+ const observer = new ResizeObserver(([entry]) => setSize({
784
+ width: Math.floor(entry.contentRect.width),
785
+ em: parseFloat(getComputedStyle(element).fontSize) || 16
786
+ }));
787
+ observer.observe(element);
788
+ return () => observer.disconnect();
789
+ }, [element]);
790
+ return [
791
+ ref,
792
+ size.width,
793
+ size.em
794
+ ];
795
+ }
796
+ /** A nice ceiling for an axis and its three gridline values. */
797
+ function ticksFor(max) {
798
+ if (!(max > 0)) return {
799
+ top: 1,
800
+ ticks: [
801
+ 0,
802
+ .5,
803
+ 1
804
+ ]
805
+ };
806
+ const power = 10 ** Math.floor(Math.log10(max));
807
+ const step = [
808
+ 1,
809
+ 2,
810
+ 2.5,
811
+ 5,
812
+ 10
813
+ ].map((m) => m * power).find((s) => s * 4 >= max) ?? power * 10;
814
+ const top = Math.ceil(max / step) * step;
815
+ return {
816
+ top,
817
+ ticks: [
818
+ 0,
819
+ top / 2,
820
+ top
821
+ ]
822
+ };
823
+ }
824
+ function Tooltip({ tip }) {
825
+ if (!tip) return null;
826
+ return /* @__PURE__ */ jsxs("div", {
827
+ className: "ov-tip",
828
+ role: "tooltip",
829
+ style: {
830
+ left: tip.x,
831
+ top: tip.y
832
+ },
833
+ children: [/* @__PURE__ */ jsx("strong", { children: tip.title }), tip.lines.map((line) => /* @__PURE__ */ jsxs("span", {
834
+ className: "ov-tip-line",
835
+ children: [
836
+ line.color && /* @__PURE__ */ jsx("i", { style: { background: line.color } }),
837
+ /* @__PURE__ */ jsx("span", { children: line.label }),
838
+ /* @__PURE__ */ jsx("b", { children: line.value })
839
+ ]
840
+ }, line.label))]
841
+ });
842
+ }
843
+ /** Bars per day (or per any label: `tick` shortens it on the axis), stacked by series, on one axis; hovering a bar lists
844
+ * every series' value for it, and `onOpen` makes a bar a link. */
845
+ function DayBars({ days, series, format = formatCompact, rows = 13, missing = "No runs started", tick = (day) => day.slice(5), onOpen }) {
846
+ const [ref, width, em] = useWidth();
847
+ const height = Math.round(rows * em);
848
+ const [tip, setTip] = useState(null);
849
+ const totals = days.map((_, i) => series.reduce((sum, s) => sum + (s.values[i] ?? 0), 0));
850
+ const { top, ticks } = ticksFor(Math.max(...totals));
851
+ const left = Math.round(3.4 * em);
852
+ const bottom = Math.round(1.8 * em);
853
+ const plotW = Math.max(0, width - left - 8);
854
+ const plotH = height - bottom - 8;
855
+ const slot = days.length ? plotW / days.length : 0;
856
+ const bar = Math.max(3, slot * .72);
857
+ const labelEvery = Math.max(1, Math.ceil(days.length / Math.max(1, Math.floor(plotW / (4.5 * em)))));
858
+ const labelGap = Math.ceil(4.5 * em / Math.max(slot, 1));
859
+ const y = (v) => 8 + plotH - v / top * plotH;
860
+ return /* @__PURE__ */ jsxs("div", {
861
+ ref,
862
+ className: "ov-plot",
863
+ onMouseLeave: () => setTip(null),
864
+ children: [width > 0 && /* @__PURE__ */ jsxs("svg", {
865
+ width,
866
+ height,
867
+ role: "img",
868
+ "aria-label": `${series.map((s) => s.name).join(", ")} per day`,
869
+ children: [ticks.map((t) => /* @__PURE__ */ jsxs("g", { children: [/* @__PURE__ */ jsx("line", {
870
+ x1: left,
871
+ x2: width - 8,
872
+ y1: y(t),
873
+ y2: y(t),
874
+ className: "ov-gridline"
875
+ }), /* @__PURE__ */ jsx("text", {
876
+ x: left - em * .4,
877
+ y: y(t),
878
+ dominantBaseline: "middle",
879
+ className: "ov-axis",
880
+ textAnchor: "end",
881
+ children: format(t)
882
+ })] }, t)), days.map((day, i) => {
883
+ let base = 0;
884
+ const x = left + i * slot + (slot - bar) / 2;
885
+ const showTip = () => setTip({
886
+ x: Math.min(left + i * slot + slot + 8, width - em * 14),
887
+ y: 8,
888
+ title: `${day} · ${series.every((s) => s.values[i] === null) ? missing : format(totals[i] ?? 0)}`,
889
+ lines: series.map((s) => ({
890
+ label: s.name,
891
+ value: s.values[i] === null ? missing : format(s.values[i] ?? 0),
892
+ color: s.color
893
+ }))
894
+ });
895
+ return /* @__PURE__ */ jsxs("g", {
896
+ role: onOpen ? "button" : void 0,
897
+ tabIndex: onOpen ? 0 : void 0,
898
+ "aria-label": onOpen ? `Open ${day}` : void 0,
899
+ onFocus: onOpen ? showTip : void 0,
900
+ onBlur: onOpen ? () => setTip(null) : void 0,
901
+ onKeyDown: onOpen ? (event) => {
902
+ if (event.key === "Enter" || event.key === " ") {
903
+ event.preventDefault();
904
+ onOpen(i);
905
+ }
906
+ } : void 0,
907
+ children: [
908
+ series.map((s) => {
909
+ const v = s.values[i] ?? 0;
910
+ if (!v) return null;
911
+ const y0 = y(base);
912
+ base += v;
913
+ const y1 = y(base);
914
+ return /* @__PURE__ */ jsx("rect", {
915
+ x,
916
+ y: y1,
917
+ width: bar,
918
+ height: Math.max(2, y0 - y1 - 2),
919
+ rx: 2,
920
+ fill: s.color
921
+ }, s.name);
922
+ }),
923
+ (i === days.length - 1 || i % labelEvery === 0 && i + labelGap <= days.length - 1) && /* @__PURE__ */ jsx("text", {
924
+ x: x + bar / 2,
925
+ y: height - em * .35,
926
+ className: "ov-axis",
927
+ textAnchor: "middle",
928
+ children: tick(day)
929
+ }),
930
+ /* @__PURE__ */ jsx("rect", {
931
+ x: left + i * slot,
932
+ y: 8,
933
+ width: slot,
934
+ height: plotH,
935
+ fill: "transparent",
936
+ style: onOpen ? { cursor: "pointer" } : void 0,
937
+ onClick: onOpen ? () => onOpen(i) : void 0,
938
+ onMouseEnter: showTip
939
+ })
940
+ ]
941
+ }, day);
942
+ })]
943
+ }), /* @__PURE__ */ jsx(Tooltip, { tip })]
944
+ });
945
+ }
946
+ /** Ranked horizontal bars with the value and an optional note on each row; past `limit` rows the rest are counted. */
947
+ function HBars({ rows, format = formatCompact, color = LIVE_SERIES[0], max, limit = 8 }) {
948
+ const top = max ?? Math.max(1, ...rows.map((row) => row.value));
949
+ if (!rows.length) return /* @__PURE__ */ jsx("p", {
950
+ className: "ov-empty",
951
+ children: "Nothing recorded."
952
+ });
953
+ const rest = rows.slice(limit);
954
+ return /* @__PURE__ */ jsxs("ol", {
955
+ className: "ov-hbars",
956
+ children: [rows.slice(0, limit).map((row) => {
957
+ const content = /* @__PURE__ */ jsxs(Fragment, { children: [
958
+ /* @__PURE__ */ jsx("span", {
959
+ className: "ov-hbar-label",
960
+ title: row.label,
961
+ children: row.label
962
+ }),
963
+ /* @__PURE__ */ jsx("span", {
964
+ className: "ov-hbar-track",
965
+ children: /* @__PURE__ */ jsx("span", { style: {
966
+ width: `${Math.max(1, row.value / top * 100)}%`,
967
+ background: row.color ?? color
968
+ } })
969
+ }),
970
+ /* @__PURE__ */ jsx("span", {
971
+ className: "ov-hbar-value",
972
+ children: format(row.value)
973
+ }),
974
+ row.note && /* @__PURE__ */ jsx("span", {
975
+ className: "ov-hbar-note",
976
+ children: row.note
977
+ })
978
+ ] });
979
+ return /* @__PURE__ */ jsx("li", { children: row.href ? /* @__PURE__ */ jsx("a", {
980
+ href: row.href,
981
+ children: content
982
+ }) : /* @__PURE__ */ jsx("div", { children: content }) }, row.label);
983
+ }), rest.length > 0 && /* @__PURE__ */ jsxs("li", {
984
+ className: "ov-hbars-more",
985
+ children: [
986
+ "+",
987
+ rest.length,
988
+ " more · ",
989
+ format(rest.reduce((a, row) => a + row.value, 0))
990
+ ]
991
+ })]
992
+ });
993
+ }
994
+ /** A histogram of counts per bin; each bar's tooltip names its range and count. */
995
+ function Histogram({ bins, format = formatCompact, color = LIVE_SERIES[0], unit, rows = 12 }) {
996
+ const [ref, width, em] = useWidth();
997
+ const height = Math.round(rows * em);
998
+ const [tip, setTip] = useState(null);
999
+ const first = bins.findIndex((b) => b.n > 0);
1000
+ const last = bins.length - 1 - [...bins].reverse().findIndex((b) => b.n > 0);
1001
+ const shown = first < 0 ? [] : bins.slice(first, last + 1);
1002
+ const { top, ticks } = ticksFor(Math.max(...shown.map((b) => b.n), 1));
1003
+ const left = Math.round(3 * em);
1004
+ const bottom = Math.round(1.8 * em);
1005
+ const plotW = Math.max(0, width - left - 8);
1006
+ const plotH = height - bottom - 8;
1007
+ const slot = shown.length ? plotW / shown.length : 0;
1008
+ const y = (v) => 8 + plotH - v / top * plotH;
1009
+ if (!shown.length) return /* @__PURE__ */ jsx("p", {
1010
+ className: "ov-empty",
1011
+ children: "Nothing recorded."
1012
+ });
1013
+ return /* @__PURE__ */ jsxs("div", {
1014
+ ref,
1015
+ className: "ov-plot",
1016
+ onMouseLeave: () => setTip(null),
1017
+ children: [width > 0 && /* @__PURE__ */ jsxs("svg", {
1018
+ width,
1019
+ height,
1020
+ role: "img",
1021
+ "aria-label": `Distribution of ${unit}`,
1022
+ children: [ticks.map((t) => /* @__PURE__ */ jsxs("g", { children: [/* @__PURE__ */ jsx("line", {
1023
+ x1: left,
1024
+ x2: width - 8,
1025
+ y1: y(t),
1026
+ y2: y(t),
1027
+ className: "ov-gridline"
1028
+ }), /* @__PURE__ */ jsx("text", {
1029
+ x: left - em * .4,
1030
+ y: y(t),
1031
+ dominantBaseline: "middle",
1032
+ className: "ov-axis",
1033
+ textAnchor: "end",
1034
+ children: formatCompact(t)
1035
+ })] }, t)), shown.map((bin, i) => /* @__PURE__ */ jsxs("g", { children: [
1036
+ /* @__PURE__ */ jsx("rect", {
1037
+ x: left + i * slot + 2,
1038
+ y: y(bin.n),
1039
+ width: Math.max(3, slot - 4),
1040
+ height: Math.max(bin.n ? 2 : 0, 8 + plotH - y(bin.n)),
1041
+ rx: 2,
1042
+ fill: color
1043
+ }),
1044
+ (i % Math.max(1, Math.ceil(shown.length / 6)) === 0 || i === shown.length - 1) && /* @__PURE__ */ jsx("text", {
1045
+ x: left + i * slot + slot / 2,
1046
+ y: height - em * .35,
1047
+ className: "ov-axis",
1048
+ textAnchor: "middle",
1049
+ children: format(bin.lo)
1050
+ }),
1051
+ /* @__PURE__ */ jsx("rect", {
1052
+ x: left + i * slot,
1053
+ y: 8,
1054
+ width: slot,
1055
+ height: plotH,
1056
+ fill: "transparent",
1057
+ onMouseEnter: () => setTip({
1058
+ x: Math.min(left + i * slot + slot + 6, width - em * 14),
1059
+ y: 8,
1060
+ title: `${format(bin.lo)} to ${format(bin.hi)} ${unit}`,
1061
+ lines: [{
1062
+ label: "count",
1063
+ value: String(bin.n)
1064
+ }]
1065
+ })
1066
+ })
1067
+ ] }, i))]
1068
+ }), /* @__PURE__ */ jsx(Tooltip, { tip })]
1069
+ });
1070
+ }
1071
+ /** Lines over time on one axis (at most four series); the crosshair readout lists each series at the nearest time. A
1072
+ * `dots` series draws its points as marks (checkpoints) instead of a line; `axis` labels the time axis (default MM-DD). */
1073
+ function Lines({ series, format = formatCompact, rows = 13, axis, marks = [], bands = [] }) {
1074
+ const [ref, width, em] = useWidth();
1075
+ const height = Math.round(rows * em);
1076
+ const [hover, setHover] = useState(null);
1077
+ const gap = eventsGap(marks, bands);
1078
+ if (gap) throw new RangeError(gap);
1079
+ const all = series.flatMap((s) => s.points);
1080
+ if (!all.length) return /* @__PURE__ */ jsx("p", {
1081
+ className: "ov-empty",
1082
+ children: "No history yet."
1083
+ });
1084
+ const times = [
1085
+ ...all.map((p) => p[0]),
1086
+ ...marks.map((m) => m.at),
1087
+ ...bands.flatMap((b) => [b.from, b.to])
1088
+ ];
1089
+ const t0 = Math.min(...times);
1090
+ const t1 = Math.max(...times);
1091
+ const { top, ticks } = ticksFor(Math.max(...all.map((p) => p[1])));
1092
+ const left = Math.round(3.4 * em);
1093
+ const bottom = Math.round(1.8 * em);
1094
+ const plotW = Math.max(0, width - left - 10);
1095
+ const plotH = height - bottom - 8;
1096
+ const x = (t) => left + (t1 > t0 ? (t - t0) / (t1 - t0) * plotW : 0);
1097
+ const y = (v) => 8 + plotH - v / top * plotH;
1098
+ const nearest = (points, t) => points.reduce((best, p) => Math.abs(p[0] - t) < Math.abs(best[0] - t) ? p : best, points[0]);
1099
+ const day = (t) => new Date(t).toISOString().slice(5, 16).replace("T", " ");
1100
+ return /* @__PURE__ */ jsxs("div", {
1101
+ ref,
1102
+ className: "ov-plot",
1103
+ onMouseMove: (event) => {
1104
+ const box = event.currentTarget.getBoundingClientRect();
1105
+ const px = event.clientX - box.left;
1106
+ setHover(px >= left ? t0 + (px - left) / Math.max(1, plotW) * (t1 - t0) : null);
1107
+ },
1108
+ onMouseLeave: () => setHover(null),
1109
+ children: [
1110
+ width > 0 && /* @__PURE__ */ jsxs("svg", {
1111
+ width,
1112
+ height,
1113
+ role: "img",
1114
+ "aria-label": series.map((s) => s.name).join(", "),
1115
+ children: [
1116
+ ticks.map((t) => /* @__PURE__ */ jsxs("g", { children: [/* @__PURE__ */ jsx("line", {
1117
+ x1: left,
1118
+ x2: width - 10,
1119
+ y1: y(t),
1120
+ y2: y(t),
1121
+ className: "ov-gridline"
1122
+ }), /* @__PURE__ */ jsx("text", {
1123
+ x: left - em * .4,
1124
+ y: y(t),
1125
+ dominantBaseline: "middle",
1126
+ className: "ov-axis",
1127
+ textAnchor: "end",
1128
+ children: format(t)
1129
+ })] }, t)),
1130
+ [
1131
+ t0,
1132
+ (t0 + t1) / 2,
1133
+ t1
1134
+ ].map((t, i) => /* @__PURE__ */ jsx("text", {
1135
+ x: x(t),
1136
+ y: height - em * .35,
1137
+ className: "ov-axis",
1138
+ textAnchor: i === 0 ? "start" : i === 2 ? "end" : "middle",
1139
+ children: axis ? axis(t) : day(t).slice(0, 5)
1140
+ }, i)),
1141
+ bands.map((band, i) => /* @__PURE__ */ jsx(Band, {
1142
+ band,
1143
+ x: x(band.from),
1144
+ y: 8,
1145
+ width: x(band.to) - x(band.from),
1146
+ height: plotH
1147
+ }, `band-${i}`)),
1148
+ series.map((s) => s.dots ? /* @__PURE__ */ jsx("g", { children: s.points.map((p) => /* @__PURE__ */ jsx("circle", {
1149
+ cx: x(p[0]),
1150
+ cy: y(p[1]),
1151
+ r: Math.max(3, em / 5),
1152
+ fill: s.color,
1153
+ stroke: "var(--chart-live-surface, #fff)",
1154
+ strokeWidth: 1.5
1155
+ }, p[0])) }, s.name) : /* @__PURE__ */ jsxs("g", {
1156
+ "data-series": s.name,
1157
+ "data-dash": s.dash ?? "solid",
1158
+ "data-marker": s.marker,
1159
+ children: [/* @__PURE__ */ jsx("polyline", {
1160
+ fill: "none",
1161
+ stroke: s.color,
1162
+ strokeWidth: Math.max(2, em / 8),
1163
+ strokeLinejoin: "round",
1164
+ strokeLinecap: s.dash === "dot" ? "round" : void 0,
1165
+ strokeDasharray: dashArray(s.dash, em),
1166
+ points: s.points.map((p) => `${x(p[0])},${y(p[1])}`).join(" ")
1167
+ }), s.marker && (s.points.length <= 30 ? s.points : s.points.slice(-1)).map((p) => /* @__PURE__ */ jsx("path", {
1168
+ d: markerPath(s.marker, x(p[0]), y(p[1]), Math.max(3, em / 5)),
1169
+ fill: s.color,
1170
+ stroke: "var(--chart-live-surface, var(--chart-canvas))",
1171
+ strokeWidth: 1.5
1172
+ }, p[0]))]
1173
+ }, s.name)),
1174
+ [...marks].sort((a, b) => a.at - b.at).map((mark, i) => /* @__PURE__ */ jsx(Mark, {
1175
+ mark,
1176
+ x: x(mark.at),
1177
+ top: 8,
1178
+ bottom: 8 + plotH,
1179
+ row: i % 3,
1180
+ em,
1181
+ flip: x(mark.at) > left + plotW * .62
1182
+ }, `mark-${i}`)),
1183
+ hover !== null && /* @__PURE__ */ jsx("line", {
1184
+ x1: x(hover),
1185
+ x2: x(hover),
1186
+ y1: 8,
1187
+ y2: 8 + plotH,
1188
+ className: "ov-crosshair"
1189
+ }),
1190
+ hover !== null && series.map((s) => {
1191
+ const p = s.points.length ? nearest(s.points, hover) : null;
1192
+ return p ? /* @__PURE__ */ jsx("circle", {
1193
+ cx: x(p[0]),
1194
+ cy: y(p[1]),
1195
+ r: Math.max(4, em / 4),
1196
+ fill: s.color,
1197
+ stroke: "var(--chart-live-surface, #fff)",
1198
+ strokeWidth: 2
1199
+ }, s.name) : null;
1200
+ })
1201
+ ]
1202
+ }),
1203
+ hover !== null && /* @__PURE__ */ jsx(Tooltip, { tip: {
1204
+ x: Math.min(x(hover) + 10, width - em * 14),
1205
+ y: 8,
1206
+ title: day(hover),
1207
+ lines: series.filter((s) => s.points.length).map((s) => {
1208
+ const p = nearest(s.points, hover);
1209
+ return {
1210
+ label: s.label ? s.label(p[0]) : s.name,
1211
+ value: format(p[1]),
1212
+ color: s.color
1213
+ };
1214
+ })
1215
+ } }),
1216
+ series.length > 1 && series.some((s) => s.dash || s.marker) && /* @__PURE__ */ jsx("ul", {
1217
+ className: "ov-key",
1218
+ "aria-label": "Series",
1219
+ children: series.map((s) => /* @__PURE__ */ jsxs("li", { children: [/* @__PURE__ */ jsxs("svg", {
1220
+ width: em * 2,
1221
+ height: em,
1222
+ "aria-hidden": "true",
1223
+ children: [/* @__PURE__ */ jsx("line", {
1224
+ x1: 0,
1225
+ x2: em * 2,
1226
+ y1: em / 2,
1227
+ y2: em / 2,
1228
+ stroke: s.color,
1229
+ strokeWidth: Math.max(2, em / 8),
1230
+ strokeDasharray: dashArray(s.dash, em),
1231
+ strokeLinecap: s.dash === "dot" ? "round" : void 0
1232
+ }), s.marker && /* @__PURE__ */ jsx("path", {
1233
+ d: markerPath(s.marker, em, em / 2, Math.max(3, em / 5)),
1234
+ fill: s.color
1235
+ })]
1236
+ }), s.name] }, s.name))
1237
+ }),
1238
+ /* @__PURE__ */ jsx(EventList, {
1239
+ marks,
1240
+ bands,
1241
+ format: axis ?? ((t) => day(t))
1242
+ })
1243
+ ]
1244
+ });
1245
+ }
1246
+ function dashArray(dash, em) {
1247
+ return dash === "dash" ? `${Math.round(em * .55)} ${Math.round(em * .3)}` : dash === "dot" ? `0.1 ${Math.round(em * .35)}` : void 0;
1248
+ }
1249
+ /** A shaded span under the data; a `missing` span is hatched because it has no data. */
1250
+ function Band({ band, x, y, width, height }) {
1251
+ return /* @__PURE__ */ jsxs("g", {
1252
+ "data-band": band.kind,
1253
+ "data-hatched": band.kind === "missing" ? "true" : void 0,
1254
+ children: [
1255
+ /* @__PURE__ */ jsx("title", { children: band.label }),
1256
+ /* @__PURE__ */ jsx("rect", {
1257
+ x,
1258
+ y,
1259
+ width: Math.max(0, width),
1260
+ height,
1261
+ className: band.kind === "missing" ? "ov-hatch-bg" : `ov-band ov-band-${band.kind}`
1262
+ }),
1263
+ band.kind === "missing" && /* @__PURE__ */ jsx("path", {
1264
+ d: hatchPath(x, y, width, height),
1265
+ className: "ov-hatch"
1266
+ })
1267
+ ]
1268
+ });
1269
+ }
1270
+ /** A vertical event rule with its label; kinds differ by stroke pattern and the died cross as well as colour. */
1271
+ function Mark({ mark, x, top, bottom, row, em, flip }) {
1272
+ const s = Math.max(3, em / 4);
1273
+ return /* @__PURE__ */ jsxs("g", {
1274
+ "data-mark": mark.kind,
1275
+ children: [
1276
+ /* @__PURE__ */ jsx("title", { children: mark.label }),
1277
+ /* @__PURE__ */ jsx("line", {
1278
+ x1: x,
1279
+ x2: x,
1280
+ y1: top,
1281
+ y2: bottom,
1282
+ className: `ov-mark ov-mark-${mark.kind}`
1283
+ }),
1284
+ mark.kind === "died" && /* @__PURE__ */ jsx("path", {
1285
+ d: `M${x - s} ${bottom - 2 * s}L${x + s} ${bottom}M${x - s} ${bottom}L${x + s} ${bottom - 2 * s}`,
1286
+ className: "ov-mark ov-mark-died"
1287
+ }),
1288
+ /* @__PURE__ */ jsx("text", {
1289
+ x: flip ? x - em * .3 : x + em * .3,
1290
+ y: top + em * (.9 + row),
1291
+ textAnchor: flip ? "end" : "start",
1292
+ className: "ov-mark-label",
1293
+ children: mark.label
1294
+ })
1295
+ ]
1296
+ });
1297
+ }
1298
+ /** Every mark and band as text, in time order: the table form of the events drawn on the plot. */
1299
+ function EventList({ marks, bands, format }) {
1300
+ if (!marks.length && !bands.length) return null;
1301
+ const items = [...marks.map((m) => ({
1302
+ at: m.at,
1303
+ kind: m.kind,
1304
+ text: `${m.label} · ${format(m.at)}`
1305
+ })), ...bands.map((b) => ({
1306
+ at: b.from,
1307
+ kind: b.kind,
1308
+ text: `${b.label} · ${format(b.from)} to ${format(b.to)}${b.kind === "missing" ? " · no data" : ""}`
1309
+ }))].sort((a, b) => a.at - b.at);
1310
+ return /* @__PURE__ */ jsx("ul", {
1311
+ className: "ov-events",
1312
+ "aria-label": "Recorded events",
1313
+ children: items.map((item, i) => /* @__PURE__ */ jsxs("li", {
1314
+ "data-kind": item.kind,
1315
+ children: [/* @__PURE__ */ jsx("i", {
1316
+ className: `ov-event-swatch ov-event-${item.kind}`,
1317
+ "aria-hidden": "true"
1318
+ }), item.text]
1319
+ }, i))
1320
+ });
1321
+ }
1322
+ const SEGMENT_TONES = [
1323
+ "var(--chart-accent)",
1324
+ "var(--chart-tier-c)",
1325
+ "var(--chart-muted)"
1326
+ ];
1327
+ /** A used-of-total meter with its numbers. With `segments` it is a part-of-whole bar: the remainder of `total` that no
1328
+ * segment accounts for is drawn hatched and labelled `untraced`, and a missing total hatches the whole track. */
1329
+ function Meter({ used, total, format = formatCompact, tone, text, segments, untraced = "untraced" }) {
1330
+ if (segments) return /* @__PURE__ */ jsx(SegmentMeter, {
1331
+ segments,
1332
+ total,
1333
+ format,
1334
+ text,
1335
+ untraced
1336
+ });
1337
+ const share = used !== null && used !== void 0 && total ? Math.min(1, used / total) : null;
1338
+ return /* @__PURE__ */ jsxs("div", {
1339
+ className: "ov-meter",
1340
+ children: [/* @__PURE__ */ jsx("span", {
1341
+ className: "ov-meter-track",
1342
+ children: /* @__PURE__ */ jsx("span", { style: {
1343
+ width: `${(share ?? 0) * 100}%`,
1344
+ background: tone ?? (share !== null && share > .9 ? "var(--chart-live-danger, #d85050)" : share !== null && share > .7 ? "var(--chart-live-warning, #c98500)" : LIVE_SERIES[0])
1345
+ } })
1346
+ }), /* @__PURE__ */ jsx("span", {
1347
+ className: "ov-meter-text",
1348
+ children: text ?? `${format(used ?? NaN)} of ${format(total ?? NaN)} · ${formatShare(share)}`
1349
+ })]
1350
+ });
1351
+ }
1352
+ function SegmentMeter({ segments, total, format, text, untraced }) {
1353
+ for (const segment of segments) if (segment.value !== null && (!Number.isFinite(segment.value) || segment.value < 0)) throw new RangeError(`${segment.label}: a segment value must be a nonnegative finite number or null.`);
1354
+ const known = segments.reduce((sum, segment) => sum + (segment.value ?? 0), 0);
1355
+ const whole = total !== null && total !== void 0 && Number.isFinite(total) && total > 0 ? total : null;
1356
+ if (whole !== null && known > whole * (1 + 1e-9)) throw new RangeError(`Segments sum to ${known}, more than the total ${whole}.`);
1357
+ const remainder = whole === null ? 0 : whole - known;
1358
+ const parts = [...segments.map((segment, i) => ({
1359
+ ...segment,
1360
+ color: segment.color ?? SEGMENT_TONES[i % SEGMENT_TONES.length],
1361
+ remainder: false
1362
+ })), ...remainder > whole * 1e-9 ? [{
1363
+ label: untraced,
1364
+ value: remainder,
1365
+ kind: "untraced",
1366
+ color: void 0,
1367
+ remainder: true
1368
+ }] : []];
1369
+ const share = (value) => whole === null ? null : value / whole;
1370
+ return /* @__PURE__ */ jsxs("div", {
1371
+ className: "ov-meter",
1372
+ "data-segmented": "true",
1373
+ children: [
1374
+ /* @__PURE__ */ jsx("span", {
1375
+ className: `ov-meter-track${whole === null ? " ov-hatched" : ""}`,
1376
+ "data-hatched": whole === null ? "true" : void 0,
1377
+ "aria-hidden": "true",
1378
+ children: whole !== null && parts.filter((part) => part.value !== null && part.value > 0).map((part) => /* @__PURE__ */ jsx("span", {
1379
+ "data-segment": part.label,
1380
+ "data-hatched": part.kind === "untraced" ? "true" : void 0,
1381
+ className: part.kind === "untraced" ? "ov-hatched" : void 0,
1382
+ style: {
1383
+ width: `${part.value / whole * 100}%`,
1384
+ background: part.kind === "untraced" ? void 0 : part.color
1385
+ }
1386
+ }, part.label))
1387
+ }),
1388
+ /* @__PURE__ */ jsx("ul", {
1389
+ className: "ov-meter-key",
1390
+ children: parts.map((part) => /* @__PURE__ */ jsxs("li", {
1391
+ "data-segment": part.label,
1392
+ children: [
1393
+ /* @__PURE__ */ jsx("i", {
1394
+ className: part.kind === "untraced" || part.value === null ? "ov-hatched" : void 0,
1395
+ style: part.kind === "untraced" || part.value === null ? void 0 : { background: part.color },
1396
+ "aria-hidden": "true"
1397
+ }),
1398
+ /* @__PURE__ */ jsx("span", { children: part.label }),
1399
+ /* @__PURE__ */ jsx("b", { children: part.value === null ? "not recorded" : `${format(part.value)}${whole === null ? "" : ` · ${formatShare(share(part.value))}`}` })
1400
+ ]
1401
+ }, part.label))
1402
+ }),
1403
+ /* @__PURE__ */ jsx("span", {
1404
+ className: "ov-meter-text",
1405
+ children: text ?? (whole === null ? "total not recorded" : `${format(known)} of ${format(whole)} accounted · ${formatShare(known / whole)}`)
1406
+ })
1407
+ ]
1408
+ });
1409
+ }
1410
+ /** A small line of one series for a row in a list (one hue; the row's label carries identity). */
1411
+ function Spark({ points, max = 1, color = LIVE_SERIES[0] }) {
1412
+ if (points.length < 2) return /* @__PURE__ */ jsx("span", {
1413
+ className: "ov-spark-empty",
1414
+ children: "collecting"
1415
+ });
1416
+ const t0 = points[0][0];
1417
+ const t1 = points.at(-1)[0];
1418
+ const w = 160;
1419
+ const h = 32;
1420
+ const domain = Math.max(1, max);
1421
+ const d = points.map((p) => `${(p[0] - t0) / Math.max(1, t1 - t0) * w},${30 - Math.min(domain, p[1]) / domain * 28}`).join(" ");
1422
+ return /* @__PURE__ */ jsx("svg", {
1423
+ viewBox: `0 0 ${w} ${h}`,
1424
+ className: "ov-spark",
1425
+ "aria-hidden": "true",
1426
+ children: /* @__PURE__ */ jsx("polyline", {
1427
+ fill: "none",
1428
+ stroke: color,
1429
+ strokeWidth: 1.6,
1430
+ points: d
1431
+ })
1432
+ });
1433
+ }
1434
+ /** One bar per row on a shared time axis (who worked when): each row's bar spans its start to its end, coloured by its
1435
+ * state; hovering a span names it, its state and its span. Times are epoch ms; `axis` labels the time axis.
1436
+ * `lanes` give a row several spans (the same shape as the static `timeline`); a span with `end: null` is open and runs
1437
+ * to `now`. `marks` draw recorded events across every lane, and states in `hatch` are drawn hatched (no data). */
1438
+ function Gantt({ rows, lanes: laneInput, colors, axis, span, marks = [], now, hatch = ["missing", "untraced"] }) {
1439
+ const [ref, width, em] = useWidth();
1440
+ const [tip, setTip] = useState(null);
1441
+ const lanes = laneInput ?? (rows ?? []).map((row, i) => ({
1442
+ id: `${row.label}-${i}`,
1443
+ label: row.label,
1444
+ spans: [{
1445
+ start: row.start,
1446
+ end: row.end,
1447
+ state: row.state,
1448
+ note: row.note
1449
+ }]
1450
+ }));
1451
+ const gap = eventsGap(marks);
1452
+ if (gap) throw new RangeError(gap);
1453
+ if (now !== void 0 && !Number.isFinite(now)) throw new RangeError("now must be a finite time.");
1454
+ if (laneInput) for (const lane of lanes) for (const s of lane.spans) {
1455
+ if (!Number.isFinite(s.start) || s.end !== null && (!Number.isFinite(s.end) || s.end < s.start)) throw new RangeError(`${lane.label}: span times must be finite and end at or after their start.`);
1456
+ if (s.end === null && (now === void 0 || now < s.start)) throw new RangeError(`${lane.label}: an open span needs a now at or after its start.`);
1457
+ }
1458
+ const spans = lanes.flatMap((lane) => lane.spans);
1459
+ if (!spans.length) return /* @__PURE__ */ jsx("p", {
1460
+ className: "ov-empty",
1461
+ children: "No agent has started."
1462
+ });
1463
+ const ends = (s) => s.end ?? now;
1464
+ const rowH = Math.round(1.25 * em);
1465
+ const labelW = Math.min(Math.round(13 * em), Math.round(width * .32));
1466
+ const top = marks.length ? 4 + Math.round(Math.min(3, marks.length) * em + .3 * em) : 4;
1467
+ const bottom = Math.round(1.8 * em);
1468
+ const height = top + lanes.length * rowH + bottom;
1469
+ const times = [
1470
+ ...spans.flatMap((s) => [s.start, ends(s)]),
1471
+ ...marks.map((m) => m.at),
1472
+ ...now === void 0 ? [] : [now]
1473
+ ];
1474
+ const t0 = Math.min(...times);
1475
+ const t1 = Math.max(...times, t0 + 6e4);
1476
+ const plotW = Math.max(0, width - labelW - 10);
1477
+ const x = (t) => labelW + (t - t0) / (t1 - t0) * plotW;
1478
+ const fallback = colors.unknown ?? "var(--chart-live-faint, var(--chart-muted))";
1479
+ const hatched = new Set(hatch);
1480
+ const plotBottom = top + lanes.length * rowH;
1481
+ return /* @__PURE__ */ jsxs("div", {
1482
+ ref,
1483
+ className: "ov-plot",
1484
+ onMouseLeave: () => setTip(null),
1485
+ children: [
1486
+ width > 0 && /* @__PURE__ */ jsxs("svg", {
1487
+ width,
1488
+ height,
1489
+ role: "img",
1490
+ "aria-label": `${lanes.length} ${laneInput ? "lanes" : "agents"} over time`,
1491
+ children: [
1492
+ [
1493
+ 0,
1494
+ .25,
1495
+ .5,
1496
+ .75,
1497
+ 1
1498
+ ].map((f) => /* @__PURE__ */ jsxs("g", { children: [/* @__PURE__ */ jsx("line", {
1499
+ x1: labelW + f * plotW,
1500
+ x2: labelW + f * plotW,
1501
+ y1: top,
1502
+ y2: plotBottom,
1503
+ className: "ov-gridline"
1504
+ }), /* @__PURE__ */ jsx("text", {
1505
+ x: labelW + f * plotW,
1506
+ y: height - em * .35,
1507
+ className: "ov-axis",
1508
+ textAnchor: f === 0 ? "start" : f === 1 ? "end" : "middle",
1509
+ children: axis(t0 + f * (t1 - t0))
1510
+ })] }, f)),
1511
+ lanes.map((lane, i) => {
1512
+ const y = top + i * rowH;
1513
+ return /* @__PURE__ */ jsxs("g", {
1514
+ "data-lane": lane.id,
1515
+ children: [
1516
+ /* @__PURE__ */ jsx("rect", {
1517
+ x: 0,
1518
+ y,
1519
+ width,
1520
+ height: rowH,
1521
+ fill: "transparent",
1522
+ onMouseEnter: () => setTip({
1523
+ x: Math.min(x(ends(lane.spans.at(-1) ?? { end: t1 })) + 8, width - em * 16),
1524
+ y: Math.max(0, y - em),
1525
+ title: lane.label,
1526
+ lines: lane.spans.flatMap((s) => [{
1527
+ label: s.end === null ? `${s.state} (open)` : s.state,
1528
+ value: span(s.start, ends(s)),
1529
+ color: hatched.has(s.state) ? void 0 : colors[s.state] ?? fallback
1530
+ }, ...s.note ? [{
1531
+ label: s.note,
1532
+ value: ""
1533
+ }] : []])
1534
+ })
1535
+ }),
1536
+ /* @__PURE__ */ jsx("text", {
1537
+ x: labelW - em * .5,
1538
+ y: y + rowH / 2,
1539
+ dominantBaseline: "middle",
1540
+ textAnchor: "end",
1541
+ className: "ov-axis ov-gantt-label",
1542
+ children: lane.label.length > 26 ? `${lane.label.slice(0, 25)}…` : lane.label
1543
+ }),
1544
+ lane.spans.map((s, j) => {
1545
+ const color = colors[s.state] ?? fallback;
1546
+ const bx = x(s.start), bw = Math.max(2, x(ends(s)) - bx), by = y + rowH * .18, bh = rowH * .64;
1547
+ const open = s.end === null;
1548
+ return /* @__PURE__ */ jsx("g", {
1549
+ "data-state": s.state,
1550
+ "data-open": open ? "true" : void 0,
1551
+ "data-hatched": hatched.has(s.state) ? "true" : void 0,
1552
+ onMouseEnter: () => setTip({
1553
+ x: Math.min(bx + bw + 8, width - em * 16),
1554
+ y: Math.max(0, y - em),
1555
+ title: lane.label,
1556
+ lines: [{
1557
+ label: open ? `${s.state} (open)` : s.state,
1558
+ value: span(s.start, ends(s)),
1559
+ color: hatched.has(s.state) ? void 0 : color
1560
+ }, ...s.note ? [{
1561
+ label: s.note,
1562
+ value: ""
1563
+ }] : []]
1564
+ }),
1565
+ children: hatched.has(s.state) ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("rect", {
1566
+ x: bx,
1567
+ y: by,
1568
+ width: bw,
1569
+ height: bh,
1570
+ className: "ov-hatch-bg"
1571
+ }), /* @__PURE__ */ jsx("path", {
1572
+ d: hatchPath(bx, by, bw, bh, 4),
1573
+ className: "ov-hatch"
1574
+ })] }) : /* @__PURE__ */ jsx("rect", {
1575
+ x: bx,
1576
+ y: by,
1577
+ width: bw,
1578
+ height: bh,
1579
+ rx: 2,
1580
+ fill: color,
1581
+ className: open ? "ov-gantt-open" : void 0
1582
+ })
1583
+ }, j);
1584
+ })
1585
+ ]
1586
+ }, lane.id);
1587
+ }),
1588
+ now !== void 0 && /* @__PURE__ */ jsx("line", {
1589
+ x1: x(now),
1590
+ x2: x(now),
1591
+ y1: top,
1592
+ y2: plotBottom,
1593
+ className: "ov-now",
1594
+ "data-now": "true"
1595
+ }),
1596
+ [...marks].sort((a, b) => a.at - b.at).map((mark, i) => /* @__PURE__ */ jsx(Mark, {
1597
+ mark,
1598
+ x: x(mark.at),
1599
+ top: 4,
1600
+ bottom: plotBottom,
1601
+ row: i % 3,
1602
+ em,
1603
+ flip: x(mark.at) > labelW + plotW * .62
1604
+ }, `mark-${i}`))
1605
+ ]
1606
+ }),
1607
+ /* @__PURE__ */ jsx(Tooltip, { tip }),
1608
+ /* @__PURE__ */ jsx(EventList, {
1609
+ marks,
1610
+ bands: [],
1611
+ format: axis
1612
+ })
1613
+ ]
1614
+ });
1615
+ }
1616
+ //#endregion
1617
+ //#region src/react/figure-view.tsx
1618
+ /** Embeds a static Figure or Refusal in React: the wide or phone render chosen by the container's width, the data
1619
+ * table in a disclosure. The SVG and table strings come from this library's figure functions, which escape input. */
1620
+ function FigureView({ figure, presentation = "chart" }) {
1621
+ const [ref, width] = useWidth();
1622
+ if (isRefusal(figure)) return /* @__PURE__ */ jsx("p", {
1623
+ className: "ov-refusal",
1624
+ role: "note",
1625
+ "data-refused": figure.id,
1626
+ children: figure.refused
1627
+ });
1628
+ const layout = width > 0 && width < 720 ? figure.svg.narrow ? "narrow" : "table" : "wide";
1629
+ const plot = layout === "wide" ? figure.svg.wide : layout === "narrow" ? figure.svg.narrow : figure.table;
1630
+ const method = [
1631
+ figure.note.method,
1632
+ figure.note.n,
1633
+ ...figure.note.exclusions
1634
+ ].join(" ");
1635
+ return /* @__PURE__ */ jsxs("figure", {
1636
+ ref,
1637
+ className: "tgc-figure tgc-figure-chart tgc-figure-view",
1638
+ "data-figure": figure.id,
1639
+ "data-layout": layout,
1640
+ "aria-label": figure.finding,
1641
+ children: [
1642
+ presentation === "report" && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("h3", {
1643
+ className: "tgc-finding",
1644
+ children: figure.finding
1645
+ }), /* @__PURE__ */ jsx("p", {
1646
+ className: "tgc-lede",
1647
+ children: figure.lede
1648
+ })] }),
1649
+ /* @__PURE__ */ jsx("div", {
1650
+ className: `tgc-view-plot tgc-view-${layout}`,
1651
+ role: figure.svg.interactive ? "group" : "img",
1652
+ "aria-label": `${figure.finding} ${figure.lede}`,
1653
+ style: layout === "narrow" ? { maxWidth: 358 } : void 0,
1654
+ dangerouslySetInnerHTML: { __html: plot }
1655
+ }),
1656
+ /* @__PURE__ */ jsxs("details", {
1657
+ className: "tgc-data",
1658
+ children: [
1659
+ /* @__PURE__ */ jsx("summary", { children: "Data and method" }),
1660
+ /* @__PURE__ */ jsx("p", {
1661
+ className: "tgc-lede",
1662
+ children: figure.lede
1663
+ }),
1664
+ /* @__PURE__ */ jsx("p", {
1665
+ className: "tgc-read",
1666
+ children: figure.read
1667
+ }),
1668
+ /* @__PURE__ */ jsx("p", {
1669
+ className: "tgc-read",
1670
+ children: method
1671
+ }),
1672
+ /* @__PURE__ */ jsx("div", {
1673
+ className: "tgc-scroll",
1674
+ role: "region",
1675
+ "aria-label": `${figure.finding} data`,
1676
+ tabIndex: 0,
1677
+ dangerouslySetInnerHTML: { __html: figure.table }
1678
+ })
1679
+ ]
1680
+ })
1681
+ ]
1682
+ });
1683
+ }
1684
+ //#endregion
1685
+ export { DEFAULT_SPARKLINE_EMPTY_LABEL, DEFAULT_SPARKLINE_HEIGHT, DEFAULT_SPARKLINE_LABEL, DEFAULT_SPARKLINE_UNAVAILABLE_LABEL, DEFAULT_SPARKLINE_WIDTH, DayBars, FigureView, Gantt, HBars, Histogram, LIVE_SERIES, Lines, Meter, Spark, Sparkline, StackedBarChart, WaterfallBar, WaterfallHeader, WaterfallRow, WaterfallTimeAxis, formatCompact, formatSparklineValue, formatWaterfallDuration, sparklineGeometry, sparklineLabel, sparklinePointsAttribute, sparklineReadings, waterfallSpanGeometry, waterfallTicks };