@tangle-network/charts 0.4.0 → 0.7.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,3 +1,4 @@
1
+ import { d as hatchPath, i as eventsGap, m as markerPath, o as isRefusal } from "../events-Cw6abvgR.js";
1
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
3
  import { Fragment as Fragment$1, useEffect, useId, useRef, useState } from "react";
3
4
  //#region src/react/sparkline.tsx
@@ -246,7 +247,7 @@ function StackedBarChart(props) {
246
247
  validate(props);
247
248
  const bySeries = new Map(series.map((entry) => [entry.id, entry]));
248
249
  const formatted = (value) => isValue(value) ? formatValue(value) : unavailableLabel;
249
- const summary = (bucket) => `${bucket.label}: total ${formatted(bucket.total)}` + (hasGap(bucket) ? ` — ${unavailableLabel}` : "") + (bucket.segments.length ? ` — ${bucket.segments.map((segment) => `${bySeries.get(segment.seriesId).label} ${formatted(segment.value)}`).join(", ")}` : "");
250
+ const summary = (bucket) => `${bucket.label}: total ${formatted(bucket.total)}` + (hasGap(bucket) ? ` — ${unavailableLabel}` : "") + (bucket.segments.length ? ` — ${bucket.segments.map((segment) => `${bySeries.get(segment.seriesId).label} ${formatted(segment.value)}`).join(", ")}` : "") + (bucket.description ? ` — ${bucket.description}` : "");
250
251
  const activeIndex = selectedBucketId === null ? -1 : buckets.findIndex(({ id }) => id === selectedBucketId);
251
252
  const activeBucket = activeIndex >= 0 ? buckets[activeIndex] : null, marginTop = 12, marginRight = 16, marginLeft = 56;
252
253
  const chartHeight = 220;
@@ -489,7 +490,8 @@ function StackedBarChart(props) {
489
490
  bySeries.get(segment.seriesId).label,
490
491
  ": ",
491
492
  formatted(segment.value)
492
- ] }, segment.seriesId))
493
+ ] }, segment.seriesId)),
494
+ activeBucket.description && /* @__PURE__ */ jsx("div", { children: activeBucket.description })
493
495
  ]
494
496
  }),
495
497
  /* @__PURE__ */ jsx("div", {
@@ -1069,17 +1071,24 @@ function Histogram({ bins, format = formatCompact, color = LIVE_SERIES[0], unit,
1069
1071
  }
1070
1072
  /** Lines over time on one axis (at most four series); the crosshair readout lists each series at the nearest time. A
1071
1073
  * `dots` series draws its points as marks (checkpoints) instead of a line; `axis` labels the time axis (default MM-DD). */
1072
- function Lines({ series, format = formatCompact, rows = 13, axis }) {
1074
+ function Lines({ series, format = formatCompact, rows = 13, axis, marks = [], bands = [] }) {
1073
1075
  const [ref, width, em] = useWidth();
1074
1076
  const height = Math.round(rows * em);
1075
1077
  const [hover, setHover] = useState(null);
1078
+ const gap = eventsGap(marks, bands);
1079
+ if (gap) throw new RangeError(gap);
1076
1080
  const all = series.flatMap((s) => s.points);
1077
1081
  if (!all.length) return /* @__PURE__ */ jsx("p", {
1078
1082
  className: "ov-empty",
1079
1083
  children: "No history yet."
1080
1084
  });
1081
- const t0 = Math.min(...all.map((p) => p[0]));
1082
- const t1 = Math.max(...all.map((p) => p[0]));
1085
+ const times = [
1086
+ ...all.map((p) => p[0]),
1087
+ ...marks.map((m) => m.at),
1088
+ ...bands.flatMap((b) => [b.from, b.to])
1089
+ ];
1090
+ const t0 = Math.min(...times);
1091
+ const t1 = Math.max(...times);
1083
1092
  const { top, ticks } = ticksFor(Math.max(...all.map((p) => p[1])));
1084
1093
  const left = Math.round(3.4 * em);
1085
1094
  const bottom = Math.round(1.8 * em);
@@ -1098,87 +1107,234 @@ function Lines({ series, format = formatCompact, rows = 13, axis }) {
1098
1107
  setHover(px >= left ? t0 + (px - left) / Math.max(1, plotW) * (t1 - t0) : null);
1099
1108
  },
1100
1109
  onMouseLeave: () => setHover(null),
1101
- children: [width > 0 && /* @__PURE__ */ jsxs("svg", {
1102
- width,
1103
- height,
1104
- role: "img",
1105
- "aria-label": series.map((s) => s.name).join(", "),
1106
- children: [
1107
- ticks.map((t) => /* @__PURE__ */ jsxs("g", { children: [/* @__PURE__ */ jsx("line", {
1108
- x1: left,
1109
- x2: width - 10,
1110
- y1: y(t),
1111
- y2: y(t),
1112
- className: "ov-gridline"
1113
- }), /* @__PURE__ */ jsx("text", {
1114
- x: left - em * .4,
1115
- y: y(t),
1116
- dominantBaseline: "middle",
1117
- className: "ov-axis",
1118
- textAnchor: "end",
1119
- children: format(t)
1120
- })] }, t)),
1121
- [
1122
- t0,
1123
- (t0 + t1) / 2,
1124
- t1
1125
- ].map((t, i) => /* @__PURE__ */ jsx("text", {
1126
- x: x(t),
1127
- y: height - em * .35,
1128
- className: "ov-axis",
1129
- textAnchor: i === 0 ? "start" : i === 2 ? "end" : "middle",
1130
- children: axis ? axis(t) : day(t).slice(0, 5)
1131
- }, i)),
1132
- series.map((s) => s.dots ? /* @__PURE__ */ jsx("g", { children: s.points.map((p) => /* @__PURE__ */ jsx("circle", {
1133
- cx: x(p[0]),
1134
- cy: y(p[1]),
1135
- r: Math.max(3, em / 5),
1136
- fill: s.color,
1137
- stroke: "var(--chart-live-surface, #fff)",
1138
- strokeWidth: 1.5
1139
- }, p[0])) }, s.name) : /* @__PURE__ */ jsx("polyline", {
1140
- fill: "none",
1141
- stroke: s.color,
1142
- strokeWidth: Math.max(2, em / 8),
1143
- strokeLinejoin: "round",
1144
- points: s.points.map((p) => `${x(p[0])},${y(p[1])}`).join(" ")
1145
- }, s.name)),
1146
- hover !== null && /* @__PURE__ */ jsx("line", {
1147
- x1: x(hover),
1148
- x2: x(hover),
1149
- y1: 8,
1150
- y2: 8 + plotH,
1151
- className: "ov-crosshair"
1152
- }),
1153
- hover !== null && series.map((s) => {
1154
- const p = s.points.length ? nearest(s.points, hover) : null;
1155
- return p ? /* @__PURE__ */ jsx("circle", {
1110
+ children: [
1111
+ width > 0 && /* @__PURE__ */ jsxs("svg", {
1112
+ width,
1113
+ height,
1114
+ role: "img",
1115
+ "aria-label": series.map((s) => s.name).join(", "),
1116
+ children: [
1117
+ ticks.map((t) => /* @__PURE__ */ jsxs("g", { children: [/* @__PURE__ */ jsx("line", {
1118
+ x1: left,
1119
+ x2: width - 10,
1120
+ y1: y(t),
1121
+ y2: y(t),
1122
+ className: "ov-gridline"
1123
+ }), /* @__PURE__ */ jsx("text", {
1124
+ x: left - em * .4,
1125
+ y: y(t),
1126
+ dominantBaseline: "middle",
1127
+ className: "ov-axis",
1128
+ textAnchor: "end",
1129
+ children: format(t)
1130
+ })] }, t)),
1131
+ [
1132
+ t0,
1133
+ (t0 + t1) / 2,
1134
+ t1
1135
+ ].map((t, i) => /* @__PURE__ */ jsx("text", {
1136
+ x: x(t),
1137
+ y: height - em * .35,
1138
+ className: "ov-axis",
1139
+ textAnchor: i === 0 ? "start" : i === 2 ? "end" : "middle",
1140
+ children: axis ? axis(t) : day(t).slice(0, 5)
1141
+ }, i)),
1142
+ bands.map((band, i) => /* @__PURE__ */ jsx(Band, {
1143
+ band,
1144
+ x: x(band.from),
1145
+ y: 8,
1146
+ width: x(band.to) - x(band.from),
1147
+ height: plotH
1148
+ }, `band-${i}`)),
1149
+ series.map((s) => s.dots ? /* @__PURE__ */ jsx("g", { children: s.points.map((p) => /* @__PURE__ */ jsx("circle", {
1156
1150
  cx: x(p[0]),
1157
1151
  cy: y(p[1]),
1158
- r: Math.max(4, em / 4),
1152
+ r: Math.max(3, em / 5),
1159
1153
  fill: s.color,
1160
1154
  stroke: "var(--chart-live-surface, #fff)",
1161
- strokeWidth: 2
1162
- }, s.name) : null;
1155
+ strokeWidth: 1.5
1156
+ }, p[0])) }, s.name) : /* @__PURE__ */ jsxs("g", {
1157
+ "data-series": s.name,
1158
+ "data-dash": s.dash ?? "solid",
1159
+ "data-marker": s.marker,
1160
+ children: [/* @__PURE__ */ jsx("polyline", {
1161
+ fill: "none",
1162
+ stroke: s.color,
1163
+ strokeWidth: Math.max(2, em / 8),
1164
+ strokeLinejoin: "round",
1165
+ strokeLinecap: s.dash === "dot" ? "round" : void 0,
1166
+ strokeDasharray: dashArray(s.dash, em),
1167
+ points: s.points.map((p) => `${x(p[0])},${y(p[1])}`).join(" ")
1168
+ }), s.marker && (s.points.length <= 30 ? s.points : s.points.slice(-1)).map((p) => /* @__PURE__ */ jsx("path", {
1169
+ d: markerPath(s.marker, x(p[0]), y(p[1]), Math.max(3, em / 5)),
1170
+ fill: s.color,
1171
+ stroke: "var(--chart-live-surface, var(--chart-canvas))",
1172
+ strokeWidth: 1.5
1173
+ }, p[0]))]
1174
+ }, s.name)),
1175
+ [...marks].sort((a, b) => a.at - b.at).map((mark, i) => /* @__PURE__ */ jsx(Mark, {
1176
+ mark,
1177
+ x: x(mark.at),
1178
+ top: 8,
1179
+ bottom: 8 + plotH,
1180
+ row: i % 3,
1181
+ em,
1182
+ flip: x(mark.at) > left + plotW * .62
1183
+ }, `mark-${i}`)),
1184
+ hover !== null && /* @__PURE__ */ jsx("line", {
1185
+ x1: x(hover),
1186
+ x2: x(hover),
1187
+ y1: 8,
1188
+ y2: 8 + plotH,
1189
+ className: "ov-crosshair"
1190
+ }),
1191
+ hover !== null && series.map((s) => {
1192
+ const p = s.points.length ? nearest(s.points, hover) : null;
1193
+ return p ? /* @__PURE__ */ jsx("circle", {
1194
+ cx: x(p[0]),
1195
+ cy: y(p[1]),
1196
+ r: Math.max(4, em / 4),
1197
+ fill: s.color,
1198
+ stroke: "var(--chart-live-surface, #fff)",
1199
+ strokeWidth: 2
1200
+ }, s.name) : null;
1201
+ })
1202
+ ]
1203
+ }),
1204
+ hover !== null && /* @__PURE__ */ jsx(Tooltip, { tip: {
1205
+ x: Math.min(x(hover) + 10, width - em * 14),
1206
+ y: 8,
1207
+ title: day(hover),
1208
+ lines: series.filter((s) => s.points.length).map((s) => {
1209
+ const p = nearest(s.points, hover);
1210
+ return {
1211
+ label: s.label ? s.label(p[0]) : s.name,
1212
+ value: format(p[1]),
1213
+ color: s.color
1214
+ };
1163
1215
  })
1164
- ]
1165
- }), hover !== null && /* @__PURE__ */ jsx(Tooltip, { tip: {
1166
- x: Math.min(x(hover) + 10, width - em * 14),
1167
- y: 8,
1168
- title: day(hover),
1169
- lines: series.filter((s) => s.points.length).map((s) => {
1170
- const p = nearest(s.points, hover);
1171
- return {
1172
- label: s.label ? s.label(p[0]) : s.name,
1173
- value: format(p[1]),
1174
- color: s.color
1175
- };
1216
+ } }),
1217
+ series.length > 1 && series.some((s) => s.dash || s.marker) && /* @__PURE__ */ jsx("ul", {
1218
+ className: "ov-key",
1219
+ "aria-label": "Series",
1220
+ children: series.map((s) => /* @__PURE__ */ jsxs("li", { children: [/* @__PURE__ */ jsxs("svg", {
1221
+ width: em * 2,
1222
+ height: em,
1223
+ "aria-hidden": "true",
1224
+ children: [/* @__PURE__ */ jsx("line", {
1225
+ x1: 0,
1226
+ x2: em * 2,
1227
+ y1: em / 2,
1228
+ y2: em / 2,
1229
+ stroke: s.color,
1230
+ strokeWidth: Math.max(2, em / 8),
1231
+ strokeDasharray: dashArray(s.dash, em),
1232
+ strokeLinecap: s.dash === "dot" ? "round" : void 0
1233
+ }), s.marker && /* @__PURE__ */ jsx("path", {
1234
+ d: markerPath(s.marker, em, em / 2, Math.max(3, em / 5)),
1235
+ fill: s.color
1236
+ })]
1237
+ }), s.name] }, s.name))
1238
+ }),
1239
+ /* @__PURE__ */ jsx(EventList, {
1240
+ marks,
1241
+ bands,
1242
+ format: axis ?? ((t) => day(t))
1176
1243
  })
1177
- } })]
1244
+ ]
1245
+ });
1246
+ }
1247
+ function dashArray(dash, em) {
1248
+ return dash === "dash" ? `${Math.round(em * .55)} ${Math.round(em * .3)}` : dash === "dot" ? `0.1 ${Math.round(em * .35)}` : void 0;
1249
+ }
1250
+ /** A shaded span under the data; a `missing` span is hatched because it has no data. */
1251
+ function Band({ band, x, y, width, height }) {
1252
+ return /* @__PURE__ */ jsxs("g", {
1253
+ "data-band": band.kind,
1254
+ "data-hatched": band.kind === "missing" ? "true" : void 0,
1255
+ children: [
1256
+ /* @__PURE__ */ jsx("title", { children: band.label }),
1257
+ /* @__PURE__ */ jsx("rect", {
1258
+ x,
1259
+ y,
1260
+ width: Math.max(0, width),
1261
+ height,
1262
+ className: band.kind === "missing" ? "ov-hatch-bg" : `ov-band ov-band-${band.kind}`
1263
+ }),
1264
+ band.kind === "missing" && /* @__PURE__ */ jsx("path", {
1265
+ d: hatchPath(x, y, width, height),
1266
+ className: "ov-hatch"
1267
+ })
1268
+ ]
1178
1269
  });
1179
1270
  }
1180
- /** A used-of-total meter with its numbers. */
1181
- function Meter({ used, total, format = formatCompact, tone, text }) {
1271
+ /** A vertical event rule with its label; kinds differ by stroke pattern and the died cross as well as colour. */
1272
+ function Mark({ mark, x, top, bottom, row, em, flip }) {
1273
+ const s = Math.max(3, em / 4);
1274
+ return /* @__PURE__ */ jsxs("g", {
1275
+ "data-mark": mark.kind,
1276
+ children: [
1277
+ /* @__PURE__ */ jsx("title", { children: mark.label }),
1278
+ /* @__PURE__ */ jsx("line", {
1279
+ x1: x,
1280
+ x2: x,
1281
+ y1: top,
1282
+ y2: bottom,
1283
+ className: `ov-mark ov-mark-${mark.kind}`
1284
+ }),
1285
+ mark.kind === "died" && /* @__PURE__ */ jsx("path", {
1286
+ d: `M${x - s} ${bottom - 2 * s}L${x + s} ${bottom}M${x - s} ${bottom}L${x + s} ${bottom - 2 * s}`,
1287
+ className: "ov-mark ov-mark-died"
1288
+ }),
1289
+ /* @__PURE__ */ jsx("text", {
1290
+ x: flip ? x - em * .3 : x + em * .3,
1291
+ y: top + em * (.9 + row),
1292
+ textAnchor: flip ? "end" : "start",
1293
+ className: "ov-mark-label",
1294
+ children: mark.label
1295
+ })
1296
+ ]
1297
+ });
1298
+ }
1299
+ /** Every mark and band as text, in time order: the table form of the events drawn on the plot. */
1300
+ function EventList({ marks, bands, format }) {
1301
+ if (!marks.length && !bands.length) return null;
1302
+ const items = [...marks.map((m) => ({
1303
+ at: m.at,
1304
+ kind: m.kind,
1305
+ text: `${m.label} · ${format(m.at)}`
1306
+ })), ...bands.map((b) => ({
1307
+ at: b.from,
1308
+ kind: b.kind,
1309
+ text: `${b.label} · ${format(b.from)} to ${format(b.to)}${b.kind === "missing" ? " · no data" : ""}`
1310
+ }))].sort((a, b) => a.at - b.at);
1311
+ return /* @__PURE__ */ jsx("ul", {
1312
+ className: "ov-events",
1313
+ "aria-label": "Recorded events",
1314
+ children: items.map((item, i) => /* @__PURE__ */ jsxs("li", {
1315
+ "data-kind": item.kind,
1316
+ children: [/* @__PURE__ */ jsx("i", {
1317
+ className: `ov-event-swatch ov-event-${item.kind}`,
1318
+ "aria-hidden": "true"
1319
+ }), item.text]
1320
+ }, i))
1321
+ });
1322
+ }
1323
+ const SEGMENT_TONES = [
1324
+ "var(--chart-accent)",
1325
+ "var(--chart-tier-c)",
1326
+ "var(--chart-muted)"
1327
+ ];
1328
+ /** A used-of-total meter with its numbers. With `segments` it is a part-of-whole bar: the remainder of `total` that no
1329
+ * segment accounts for is drawn hatched and labelled `untraced`, and a missing total hatches the whole track. */
1330
+ function Meter({ used, total, format = formatCompact, tone, text, segments, untraced = "untraced" }) {
1331
+ if (segments) return /* @__PURE__ */ jsx(SegmentMeter, {
1332
+ segments,
1333
+ total,
1334
+ format,
1335
+ text,
1336
+ untraced
1337
+ });
1182
1338
  const share = used !== null && used !== void 0 && total ? Math.min(1, used / total) : null;
1183
1339
  return /* @__PURE__ */ jsxs("div", {
1184
1340
  className: "ov-meter",
@@ -1194,6 +1350,64 @@ function Meter({ used, total, format = formatCompact, tone, text }) {
1194
1350
  })]
1195
1351
  });
1196
1352
  }
1353
+ function SegmentMeter({ segments, total, format, text, untraced }) {
1354
+ 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.`);
1355
+ const known = segments.reduce((sum, segment) => sum + (segment.value ?? 0), 0);
1356
+ const whole = total !== null && total !== void 0 && Number.isFinite(total) && total > 0 ? total : null;
1357
+ if (whole !== null && known > whole * (1 + 1e-9)) throw new RangeError(`Segments sum to ${known}, more than the total ${whole}.`);
1358
+ const remainder = whole === null ? 0 : whole - known;
1359
+ const parts = [...segments.map((segment, i) => ({
1360
+ ...segment,
1361
+ color: segment.color ?? SEGMENT_TONES[i % SEGMENT_TONES.length],
1362
+ remainder: false
1363
+ })), ...remainder > whole * 1e-9 ? [{
1364
+ label: untraced,
1365
+ value: remainder,
1366
+ kind: "untraced",
1367
+ color: void 0,
1368
+ remainder: true
1369
+ }] : []];
1370
+ const share = (value) => whole === null ? null : value / whole;
1371
+ return /* @__PURE__ */ jsxs("div", {
1372
+ className: "ov-meter",
1373
+ "data-segmented": "true",
1374
+ children: [
1375
+ /* @__PURE__ */ jsx("span", {
1376
+ className: `ov-meter-track${whole === null ? " ov-hatched" : ""}`,
1377
+ "data-hatched": whole === null ? "true" : void 0,
1378
+ "aria-hidden": "true",
1379
+ children: whole !== null && parts.filter((part) => part.value !== null && part.value > 0).map((part) => /* @__PURE__ */ jsx("span", {
1380
+ "data-segment": part.label,
1381
+ "data-hatched": part.kind === "untraced" ? "true" : void 0,
1382
+ className: part.kind === "untraced" ? "ov-hatched" : void 0,
1383
+ style: {
1384
+ width: `${part.value / whole * 100}%`,
1385
+ background: part.kind === "untraced" ? void 0 : part.color
1386
+ }
1387
+ }, part.label))
1388
+ }),
1389
+ /* @__PURE__ */ jsx("ul", {
1390
+ className: "ov-meter-key",
1391
+ children: parts.map((part) => /* @__PURE__ */ jsxs("li", {
1392
+ "data-segment": part.label,
1393
+ children: [
1394
+ /* @__PURE__ */ jsx("i", {
1395
+ className: part.kind === "untraced" || part.value === null ? "ov-hatched" : void 0,
1396
+ style: part.kind === "untraced" || part.value === null ? void 0 : { background: part.color },
1397
+ "aria-hidden": "true"
1398
+ }),
1399
+ /* @__PURE__ */ jsx("span", { children: part.label }),
1400
+ /* @__PURE__ */ jsx("b", { children: part.value === null ? "not recorded" : `${format(part.value)}${whole === null ? "" : ` · ${formatShare(share(part.value))}`}` })
1401
+ ]
1402
+ }, part.label))
1403
+ }),
1404
+ /* @__PURE__ */ jsx("span", {
1405
+ className: "ov-meter-text",
1406
+ children: text ?? (whole === null ? "total not recorded" : `${format(known)} of ${format(whole)} accounted · ${formatShare(known / whole)}`)
1407
+ })
1408
+ ]
1409
+ });
1410
+ }
1197
1411
  /** A small line of one series for a row in a list (one hue; the row's label carries identity). */
1198
1412
  function Spark({ points, max = 1, color = LIVE_SERIES[0] }) {
1199
1413
  if (points.length < 2) return /* @__PURE__ */ jsx("span", {
@@ -1219,97 +1433,254 @@ function Spark({ points, max = 1, color = LIVE_SERIES[0] }) {
1219
1433
  });
1220
1434
  }
1221
1435
  /** 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
1222
- * state; hovering a row names it, its state and its span. Times are epoch ms; `axis` labels the time axis. */
1223
- function Gantt({ rows, colors, axis, span }) {
1436
+ * state; hovering a span names it, its state and its span. Times are epoch ms; `axis` labels the time axis.
1437
+ * `lanes` give a row several spans (the same shape as the static `timeline`); a span with `end: null` is open and runs
1438
+ * to `now`. `marks` draw recorded events across every lane, and states in `hatch` are drawn hatched (no data). */
1439
+ function Gantt({ rows, lanes: laneInput, colors, axis, span, marks = [], now, hatch = ["missing", "untraced"] }) {
1224
1440
  const [ref, width, em] = useWidth();
1225
1441
  const [tip, setTip] = useState(null);
1226
- if (!rows.length) return /* @__PURE__ */ jsx("p", {
1442
+ const lanes = laneInput ?? (rows ?? []).map((row, i) => ({
1443
+ id: `${row.label}-${i}`,
1444
+ label: row.label,
1445
+ spans: [{
1446
+ start: row.start,
1447
+ end: row.end,
1448
+ state: row.state,
1449
+ note: row.note
1450
+ }]
1451
+ }));
1452
+ const gap = eventsGap(marks);
1453
+ if (gap) throw new RangeError(gap);
1454
+ if (now !== void 0 && !Number.isFinite(now)) throw new RangeError("now must be a finite time.");
1455
+ if (laneInput) for (const lane of lanes) for (const s of lane.spans) {
1456
+ 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.`);
1457
+ 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.`);
1458
+ }
1459
+ const spans = lanes.flatMap((lane) => lane.spans);
1460
+ if (!spans.length) return /* @__PURE__ */ jsx("p", {
1227
1461
  className: "ov-empty",
1228
1462
  children: "No agent has started."
1229
1463
  });
1464
+ const ends = (s) => s.end ?? now;
1230
1465
  const rowH = Math.round(1.25 * em);
1231
1466
  const labelW = Math.min(Math.round(13 * em), Math.round(width * .32));
1232
- const top = 4;
1467
+ const top = marks.length ? 4 + Math.round(Math.min(3, marks.length) * em + .3 * em) : 4;
1233
1468
  const bottom = Math.round(1.8 * em);
1234
- const height = top + rows.length * rowH + bottom;
1235
- const t0 = Math.min(...rows.map((row) => row.start));
1236
- const t1 = Math.max(...rows.map((row) => row.end), t0 + 6e4);
1469
+ const height = top + lanes.length * rowH + bottom;
1470
+ const times = [
1471
+ ...spans.flatMap((s) => [s.start, ends(s)]),
1472
+ ...marks.map((m) => m.at),
1473
+ ...now === void 0 ? [] : [now]
1474
+ ];
1475
+ const t0 = Math.min(...times);
1476
+ const t1 = Math.max(...times, t0 + 6e4);
1237
1477
  const plotW = Math.max(0, width - labelW - 10);
1238
1478
  const x = (t) => labelW + (t - t0) / (t1 - t0) * plotW;
1239
- const fallback = colors.unknown ?? "#8e8e8e";
1479
+ const fallback = colors.unknown ?? "var(--chart-live-faint, var(--chart-muted))";
1480
+ const hatched = new Set(hatch);
1481
+ const plotBottom = top + lanes.length * rowH;
1240
1482
  return /* @__PURE__ */ jsxs("div", {
1241
1483
  ref,
1242
1484
  className: "ov-plot",
1243
1485
  onMouseLeave: () => setTip(null),
1244
- children: [width > 0 && /* @__PURE__ */ jsxs("svg", {
1245
- width,
1246
- height,
1247
- role: "img",
1248
- "aria-label": `${rows.length} agents over time`,
1249
- children: [[
1250
- 0,
1251
- .25,
1252
- .5,
1253
- .75,
1254
- 1
1255
- ].map((f) => /* @__PURE__ */ jsxs("g", { children: [/* @__PURE__ */ jsx("line", {
1256
- x1: labelW + f * plotW,
1257
- x2: labelW + f * plotW,
1258
- y1: top,
1259
- y2: top + rows.length * rowH,
1260
- className: "ov-gridline"
1261
- }), /* @__PURE__ */ jsx("text", {
1262
- x: labelW + f * plotW,
1263
- y: height - em * .35,
1264
- className: "ov-axis",
1265
- textAnchor: f === 0 ? "start" : f === 1 ? "end" : "middle",
1266
- children: axis(t0 + f * (t1 - t0))
1267
- })] }, f)), rows.map((row, i) => {
1268
- const y = top + i * rowH;
1269
- const color = colors[row.state] ?? fallback;
1270
- return /* @__PURE__ */ jsxs("g", {
1271
- onMouseEnter: () => setTip({
1272
- x: Math.min(x(row.end) + 8, width - em * 16),
1273
- y: Math.max(0, y - em),
1274
- title: row.label,
1275
- lines: [{
1276
- label: row.state,
1277
- value: span(row.start, row.end),
1278
- color
1279
- }, ...row.note ? [{
1280
- label: row.note,
1281
- value: ""
1282
- }] : []]
1486
+ children: [
1487
+ width > 0 && /* @__PURE__ */ jsxs("svg", {
1488
+ width,
1489
+ height,
1490
+ role: "img",
1491
+ "aria-label": `${lanes.length} ${laneInput ? "lanes" : "agents"} over time`,
1492
+ children: [
1493
+ [
1494
+ 0,
1495
+ .25,
1496
+ .5,
1497
+ .75,
1498
+ 1
1499
+ ].map((f) => /* @__PURE__ */ jsxs("g", { children: [/* @__PURE__ */ jsx("line", {
1500
+ x1: labelW + f * plotW,
1501
+ x2: labelW + f * plotW,
1502
+ y1: top,
1503
+ y2: plotBottom,
1504
+ className: "ov-gridline"
1505
+ }), /* @__PURE__ */ jsx("text", {
1506
+ x: labelW + f * plotW,
1507
+ y: height - em * .35,
1508
+ className: "ov-axis",
1509
+ textAnchor: f === 0 ? "start" : f === 1 ? "end" : "middle",
1510
+ children: axis(t0 + f * (t1 - t0))
1511
+ })] }, f)),
1512
+ lanes.map((lane, i) => {
1513
+ const y = top + i * rowH;
1514
+ return /* @__PURE__ */ jsxs("g", {
1515
+ "data-lane": lane.id,
1516
+ children: [
1517
+ /* @__PURE__ */ jsx("rect", {
1518
+ x: 0,
1519
+ y,
1520
+ width,
1521
+ height: rowH,
1522
+ fill: "transparent",
1523
+ onMouseEnter: () => setTip({
1524
+ x: Math.min(x(ends(lane.spans.at(-1) ?? { end: t1 })) + 8, width - em * 16),
1525
+ y: Math.max(0, y - em),
1526
+ title: lane.label,
1527
+ lines: lane.spans.flatMap((s) => [{
1528
+ label: s.end === null ? `${s.state} (open)` : s.state,
1529
+ value: span(s.start, ends(s)),
1530
+ color: hatched.has(s.state) ? void 0 : colors[s.state] ?? fallback
1531
+ }, ...s.note ? [{
1532
+ label: s.note,
1533
+ value: ""
1534
+ }] : []])
1535
+ })
1536
+ }),
1537
+ /* @__PURE__ */ jsx("text", {
1538
+ x: labelW - em * .5,
1539
+ y: y + rowH / 2,
1540
+ dominantBaseline: "middle",
1541
+ textAnchor: "end",
1542
+ className: "ov-axis ov-gantt-label",
1543
+ children: lane.label.length > 26 ? `${lane.label.slice(0, 25)}…` : lane.label
1544
+ }),
1545
+ lane.spans.map((s, j) => {
1546
+ const color = colors[s.state] ?? fallback;
1547
+ const bx = x(s.start), bw = Math.max(2, x(ends(s)) - bx), by = y + rowH * .18, bh = rowH * .64;
1548
+ const open = s.end === null;
1549
+ return /* @__PURE__ */ jsx("g", {
1550
+ "data-state": s.state,
1551
+ "data-open": open ? "true" : void 0,
1552
+ "data-hatched": hatched.has(s.state) ? "true" : void 0,
1553
+ onMouseEnter: () => setTip({
1554
+ x: Math.min(bx + bw + 8, width - em * 16),
1555
+ y: Math.max(0, y - em),
1556
+ title: lane.label,
1557
+ lines: [{
1558
+ label: open ? `${s.state} (open)` : s.state,
1559
+ value: span(s.start, ends(s)),
1560
+ color: hatched.has(s.state) ? void 0 : color
1561
+ }, ...s.note ? [{
1562
+ label: s.note,
1563
+ value: ""
1564
+ }] : []]
1565
+ }),
1566
+ children: hatched.has(s.state) ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("rect", {
1567
+ x: bx,
1568
+ y: by,
1569
+ width: bw,
1570
+ height: bh,
1571
+ className: "ov-hatch-bg"
1572
+ }), /* @__PURE__ */ jsx("path", {
1573
+ d: hatchPath(bx, by, bw, bh, 4),
1574
+ className: "ov-hatch"
1575
+ })] }) : /* @__PURE__ */ jsx("rect", {
1576
+ x: bx,
1577
+ y: by,
1578
+ width: bw,
1579
+ height: bh,
1580
+ rx: 2,
1581
+ fill: color,
1582
+ className: open ? "ov-gantt-open" : void 0
1583
+ })
1584
+ }, j);
1585
+ })
1586
+ ]
1587
+ }, lane.id);
1283
1588
  }),
1284
- children: [
1285
- /* @__PURE__ */ jsx("rect", {
1286
- x: 0,
1287
- y,
1288
- width,
1289
- height: rowH,
1290
- fill: "transparent"
1291
- }),
1292
- /* @__PURE__ */ jsx("text", {
1293
- x: labelW - em * .5,
1294
- y: y + rowH / 2,
1295
- dominantBaseline: "middle",
1296
- textAnchor: "end",
1297
- className: "ov-axis ov-gantt-label",
1298
- children: row.label.length > 26 ? `${row.label.slice(0, 25)}…` : row.label
1299
- }),
1300
- /* @__PURE__ */ jsx("rect", {
1301
- x: x(row.start),
1302
- y: y + rowH * .18,
1303
- width: Math.max(2, x(row.end) - x(row.start)),
1304
- height: rowH * .64,
1305
- rx: 2,
1306
- fill: color
1307
- })
1308
- ]
1309
- }, `${row.label}-${i}`);
1310
- })]
1311
- }), /* @__PURE__ */ jsx(Tooltip, { tip })]
1589
+ now !== void 0 && /* @__PURE__ */ jsx("line", {
1590
+ x1: x(now),
1591
+ x2: x(now),
1592
+ y1: top,
1593
+ y2: plotBottom,
1594
+ className: "ov-now",
1595
+ "data-now": "true"
1596
+ }),
1597
+ [...marks].sort((a, b) => a.at - b.at).map((mark, i) => /* @__PURE__ */ jsx(Mark, {
1598
+ mark,
1599
+ x: x(mark.at),
1600
+ top: 4,
1601
+ bottom: plotBottom,
1602
+ row: i % 3,
1603
+ em,
1604
+ flip: x(mark.at) > labelW + plotW * .62
1605
+ }, `mark-${i}`))
1606
+ ]
1607
+ }),
1608
+ /* @__PURE__ */ jsx(Tooltip, { tip }),
1609
+ /* @__PURE__ */ jsx(EventList, {
1610
+ marks,
1611
+ bands: [],
1612
+ format: axis
1613
+ })
1614
+ ]
1615
+ });
1616
+ }
1617
+ //#endregion
1618
+ //#region src/react/figure-view.tsx
1619
+ /** Embeds a static Figure or Refusal in React: the wide or phone render chosen by the container's width, the data
1620
+ * table in a disclosure. The SVG and table strings come from this library's figure functions, which escape input. */
1621
+ function FigureView({ figure, presentation = "chart" }) {
1622
+ const [ref, width] = useWidth();
1623
+ if (isRefusal(figure)) return /* @__PURE__ */ jsx("p", {
1624
+ className: "ov-refusal",
1625
+ role: "note",
1626
+ "data-refused": figure.id,
1627
+ children: figure.refused
1628
+ });
1629
+ const layout = width > 0 && width < 720 ? figure.svg.narrow ? "narrow" : "table" : "wide";
1630
+ const plot = layout === "wide" ? figure.svg.wide : layout === "narrow" ? figure.svg.narrow : figure.table;
1631
+ const method = [
1632
+ figure.note.method,
1633
+ figure.note.n,
1634
+ ...figure.note.exclusions
1635
+ ].join(" ");
1636
+ return /* @__PURE__ */ jsxs("figure", {
1637
+ ref,
1638
+ className: "tgc-figure tgc-figure-chart tgc-figure-view",
1639
+ "data-figure": figure.id,
1640
+ "data-layout": layout,
1641
+ "aria-label": figure.finding,
1642
+ children: [
1643
+ presentation === "report" && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("h3", {
1644
+ className: "tgc-finding",
1645
+ children: figure.finding
1646
+ }), /* @__PURE__ */ jsx("p", {
1647
+ className: "tgc-lede",
1648
+ children: figure.lede
1649
+ })] }),
1650
+ /* @__PURE__ */ jsx("div", {
1651
+ className: `tgc-view-plot tgc-view-${layout}`,
1652
+ role: figure.svg.interactive ? "group" : "img",
1653
+ "aria-label": `${figure.finding} ${figure.lede}`,
1654
+ style: layout === "narrow" ? { maxWidth: 358 } : void 0,
1655
+ dangerouslySetInnerHTML: { __html: plot }
1656
+ }),
1657
+ /* @__PURE__ */ jsxs("details", {
1658
+ className: "tgc-data",
1659
+ children: [
1660
+ /* @__PURE__ */ jsx("summary", { children: "Data and method" }),
1661
+ /* @__PURE__ */ jsx("p", {
1662
+ className: "tgc-lede",
1663
+ children: figure.lede
1664
+ }),
1665
+ /* @__PURE__ */ jsx("p", {
1666
+ className: "tgc-read",
1667
+ children: figure.read
1668
+ }),
1669
+ /* @__PURE__ */ jsx("p", {
1670
+ className: "tgc-read",
1671
+ children: method
1672
+ }),
1673
+ /* @__PURE__ */ jsx("div", {
1674
+ className: "tgc-scroll",
1675
+ role: "region",
1676
+ "aria-label": `${figure.finding} data`,
1677
+ tabIndex: 0,
1678
+ dangerouslySetInnerHTML: { __html: figure.table }
1679
+ })
1680
+ ]
1681
+ })
1682
+ ]
1312
1683
  });
1313
1684
  }
1314
1685
  //#endregion
1315
- export { DEFAULT_SPARKLINE_EMPTY_LABEL, DEFAULT_SPARKLINE_HEIGHT, DEFAULT_SPARKLINE_LABEL, DEFAULT_SPARKLINE_UNAVAILABLE_LABEL, DEFAULT_SPARKLINE_WIDTH, DayBars, Gantt, HBars, Histogram, LIVE_SERIES, Lines, Meter, Spark, Sparkline, StackedBarChart, WaterfallBar, WaterfallHeader, WaterfallRow, WaterfallTimeAxis, formatCompact, formatSparklineValue, formatWaterfallDuration, sparklineGeometry, sparklineLabel, sparklinePointsAttribute, sparklineReadings, waterfallSpanGeometry, waterfallTicks };
1686
+ 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 };