@quantumwake/terminal-ux-dashboard-components 0.1.28 → 0.1.30

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
@@ -49,10 +49,35 @@ SQL service `/query`; a read-only host against DuckDB-WASM `read_parquet`).
49
49
  - ⏳ Components (landing in tranches): views/, `SqlConsole`,
50
50
  `ChartBuilder`, `DashboardRenderer`, `DataExplorer`
51
51
 
52
+ ## Value formatting (`yFormat`, `formatBytes`)
53
+
54
+ `LineView`, `BarView` and `SparklineView` accept a `yFormat?: (value: number) =>
55
+ string` prop that formats every numeric value rendered for the user — y-axis
56
+ tick labels, hover/tooltip values, in-bar labels (`BarView`), and the
57
+ latest-value readout (`SparklineView`). It's unset by default, so nothing
58
+ changes for existing callers: ticks keep `Number(value).toLocaleString()` and
59
+ the tooltip/label keep Nivo's own default formatting.
60
+
61
+ `BarView`'s value axis follows `layout` — vertical puts it on the left,
62
+ horizontal on the bottom — `yFormat` applies to whichever axis that is.
63
+
64
+ ```tsx
65
+ import { LineView, BarView, formatBytes } from '@quantumwake/terminal-ux-dashboard-components';
66
+
67
+ <LineView records={records} xColumn="t" yColumn="bytes_per_sec" yFormat={(v) => formatBytes(v, true)} />
68
+ <BarView records={records} groupColumn="host" valueColumn="bytes" yFormat={formatBytes} />
69
+ ```
70
+
71
+ `formatBytes(value, perSecond?)` is the shared unit rule for byte-ish series:
72
+ B under 1024, then KB/MB/GB/TB with one decimal place (`"1.5 MB"`), capped at
73
+ TB. Pass `perSecond: true` to append `/s` for a throughput series
74
+ (`"2.0 GB/s"`).
75
+
52
76
  ## Build
53
77
 
54
78
  ```
55
79
  npm install
56
80
  npm run build # tsup → dist (esm + cjs + d.ts)
57
81
  npm run lint # tsc --noEmit
82
+ npm run test # vitest run
58
83
  ```
package/dist/index.cjs CHANGED
@@ -436,6 +436,23 @@ var aggregate = (records, column, fn) => {
436
436
  return records.length;
437
437
  }
438
438
  };
439
+
440
+ // src/format.ts
441
+ var BYTE_UNITS = ["KB", "MB", "GB", "TB"];
442
+ function formatBytes(value, perSecond = false) {
443
+ const suffix = perSecond ? "/s" : "";
444
+ if (!Number.isFinite(value)) return `\u2014 B${suffix}`;
445
+ const sign = value < 0 ? "-" : "";
446
+ const abs = Math.abs(value);
447
+ if (abs < 1024) return `${sign}${Math.round(abs)} B${suffix}`;
448
+ let scaled = abs / 1024;
449
+ let unit = 0;
450
+ while (scaled >= 1024 && unit < BYTE_UNITS.length - 1) {
451
+ scaled /= 1024;
452
+ unit++;
453
+ }
454
+ return `${sign}${scaled.toFixed(1)} ${BYTE_UNITS[unit]}${suffix}`;
455
+ }
439
456
  var HEAT_STATS = ["count", "sum", "avg", "min", "max", "median"];
440
457
  var cellKey = (row, col) => `${row}\0${col}`;
441
458
  var normalizePartition = (cells, method, vmin, vmax) => {
@@ -508,8 +525,8 @@ function MetricView({ records, config, value: presetValue }) {
508
525
  function InsightView({ config }) {
509
526
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-full p-4 overflow-auto", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-midnight-text-body leading-relaxed whitespace-pre-wrap", children: config.text }) });
510
527
  }
511
- function formatVal(value, s, numeric) {
512
- let str = numeric ? Number(value).toLocaleString() : String(value);
528
+ function formatVal(value, s, numeric, custom) {
529
+ let str = custom ? custom(value) : numeric ? Number(value).toLocaleString() : String(value);
513
530
  if (s.tickTruncate > 0 && str.length > s.tickTruncate) str = `${str.slice(0, s.tickTruncate)}\u2026`;
514
531
  return str;
515
532
  }
@@ -558,7 +575,7 @@ function makeAxis(style, axis, columnName, opts2 = {}) {
558
575
  }
559
576
  if (s.tickWrap) {
560
577
  out.renderTick = (tick) => {
561
- const lines = wrapText(formatVal(tick.value, s, numeric), s.tickWrapWidth);
578
+ const lines = wrapText(formatVal(tick.value, s, numeric, opts2.format), s.tickWrapWidth);
562
579
  const firstDy = isX ? "0" : `${-((lines.length - 1) * 0.55)}em`;
563
580
  return /* @__PURE__ */ jsxRuntime.jsxs("g", { transform: `translate(${tick.x},${tick.y})`, children: [
564
581
  /* @__PURE__ */ jsxRuntime.jsx("line", { x2: isX ? 0 : -5, y2: isX ? 5 : 0, style: { stroke: s.textColor, strokeWidth: 1, opacity: 0.3 } }),
@@ -576,13 +593,13 @@ function makeAxis(style, axis, columnName, opts2 = {}) {
576
593
  };
577
594
  } else {
578
595
  out.tickRotation = rotate;
579
- out.format = (v) => formatVal(v, s, numeric);
596
+ out.format = (v) => formatVal(v, s, numeric, opts2.format);
580
597
  }
581
598
  const maxTicks = isX ? s.maxXTicks : s.maxYTicks;
582
599
  if (numeric && maxTicks > 0) out.tickValues = maxTicks;
583
600
  return out;
584
601
  }
585
- function BarView({ records, groupColumn, valueColumn, aggFn = "count", data: presetData, layout = "vertical", style }) {
602
+ function BarView({ records, groupColumn, valueColumn, aggFn = "count", data: presetData, layout = "vertical", style, yFormat }) {
586
603
  const computed = react.useMemo(() => {
587
604
  if (presetData || !records) return [];
588
605
  const groups = groupBy(records, groupColumn);
@@ -593,8 +610,9 @@ function BarView({ records, groupColumn, valueColumn, aggFn = "count", data: pre
593
610
  const { frameClass, frameStyle, margin } = chartSizing(style, { top: 20, right: 20, bottom: 60, left: 60 });
594
611
  const horizontal = layout === "horizontal";
595
612
  const plotted = horizontal ? [...data].reverse() : data;
596
- const axisBottom = horizontal ? makeAxis(style, "x", valueColumn, { numeric: true }) : makeAxis(style, "x", groupColumn);
597
- const axisLeft = horizontal ? makeAxis(style, "y", groupColumn) : makeAxis(style, "y", valueColumn, { numeric: true });
613
+ const valueFormat = yFormat ? (v) => yFormat(Number(v)) : void 0;
614
+ const axisBottom = horizontal ? makeAxis(style, "x", valueColumn, { numeric: true, format: valueFormat }) : makeAxis(style, "x", groupColumn);
615
+ const axisLeft = horizontal ? makeAxis(style, "y", groupColumn) : makeAxis(style, "y", valueColumn, { numeric: true, format: valueFormat });
598
616
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: frameClass, style: frameStyle, children: /* @__PURE__ */ jsxRuntime.jsx(
599
617
  bar.ResponsiveBar,
600
618
  {
@@ -608,6 +626,7 @@ function BarView({ records, groupColumn, valueColumn, aggFn = "count", data: pre
608
626
  borderColor: { from: "color", modifiers: [["darker", 1.6]] },
609
627
  axisBottom,
610
628
  axisLeft,
629
+ valueFormat,
611
630
  enableLabel: s.barLabels,
612
631
  labelSkipWidth: 12,
613
632
  labelSkipHeight: 12,
@@ -656,7 +675,11 @@ function PieView({ records, groupColumn, data: presetData, style }) {
656
675
  }
657
676
  ) });
658
677
  }
659
- function LineView({ records, xColumn, yColumn, data: presetData, style }) {
678
+ var fmtClock = (d) => d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit", hourCycle: "h23" });
679
+ var toDate = (x) => x instanceof Date ? x : new Date(x);
680
+ function LineView({ records, xColumn, yColumn, data: presetData, style, xScale = "point", xFormat, xDomain, yFormat }) {
681
+ const timed = xScale === "time";
682
+ const clock = xFormat ?? fmtClock;
660
683
  const computed = react.useMemo(() => {
661
684
  if (presetData || !records) return [];
662
685
  const sorted = [...records].filter((r) => r[xColumn] != null && r[yColumn] != null).sort((a, b) => {
@@ -669,15 +692,27 @@ function LineView({ records, xColumn, yColumn, data: presetData, style }) {
669
692
  data: sorted.map((r) => ({ x: String(r[xColumn]), y: Number(r[yColumn]) || 0 }))
670
693
  }];
671
694
  }, [records, xColumn, yColumn, presetData]);
672
- const data = presetData || computed;
695
+ const shaped = presetData || computed;
696
+ const data = react.useMemo(
697
+ () => timed ? shaped.map((serie) => ({ ...serie, data: serie.data.map((d) => ({ ...d, x: toDate(d.x) })) })) : shaped,
698
+ [shaped, timed]
699
+ );
673
700
  const s = withStyleDefaults(style);
674
701
  const { frameClass, frameStyle, margin } = chartSizing(style, { top: 20, right: 20, bottom: 60, left: 60 });
702
+ const yTickFormat = yFormat ? (v) => yFormat(Number(v)) : void 0;
675
703
  const colorById = new Map(data.map((serie, i) => [
676
704
  serie.id,
677
705
  serie.color ?? (s.seriesColors.length || data.length > 1 ? seriesColor(i, s) : "rgba(96, 165, 250, 0.9)")
678
706
  ]));
679
707
  const longest = data.reduce((a, b) => b.data.length > a.data.length ? b : a, { id: "", data: [] });
680
- const tickValues = thinTicks(longest.data.map((d) => d.x), s.maxXTicks);
708
+ const tickValues = timed ? void 0 : thinTicks(longest.data.map((d) => d.x), s.maxXTicks);
709
+ const axisBottom = (() => {
710
+ const base = makeAxis(style, "x", xColumn);
711
+ if (!timed) return { ...base, ...tickValues ? { tickValues } : {} };
712
+ delete base.renderTick;
713
+ return { ...base, tickRotation: s.xTickRotation, format: clock, ...s.maxXTicks > 0 ? { tickValues: s.maxXTicks } : {} };
714
+ })();
715
+ const nivoXScale = timed ? { type: "time", format: "native", precision: "millisecond", useUTC: false, min: xDomain?.[0] ?? "auto", max: xDomain?.[1] ?? "auto" } : { type: "point" };
681
716
  let yScale = { type: "linear", min: "auto", max: "auto" };
682
717
  if (s.yFromZero) {
683
718
  const dataMax = Math.max(0, ...data.flatMap((serie) => serie.data.map((d) => Math.max(d.y ?? 0, d.hi ?? d.y ?? 0))));
@@ -708,7 +743,8 @@ function LineView({ records, xColumn, yColumn, data: presetData, style }) {
708
743
  {
709
744
  data,
710
745
  margin,
711
- xScale: { type: "point" },
746
+ xScale: nivoXScale,
747
+ xFormat: timed ? (v) => clock(v) : void 0,
712
748
  yScale,
713
749
  curve: "monotoneX",
714
750
  enableArea: s.areaOpacity > 0,
@@ -719,8 +755,9 @@ function LineView({ records, xColumn, yColumn, data: presetData, style }) {
719
755
  pointBorderWidth: 2,
720
756
  pointBorderColor: { from: "serieColor" },
721
757
  enableGridX: false,
722
- axisBottom: { ...makeAxis(style, "x", xColumn), ...tickValues ? { tickValues } : {} },
723
- axisLeft: makeAxis(style, "y", yColumn, { numeric: true }),
758
+ axisBottom,
759
+ axisLeft: makeAxis(style, "y", yColumn, { numeric: true, format: yTickFormat }),
760
+ yFormat: yTickFormat,
724
761
  useMesh: true,
725
762
  layers: ["grid", "markers", "axes", "areas", bandsLayer, linesLayer, "crosshair", "slices", "mesh", "legends"],
726
763
  animate: s.animate,
@@ -737,7 +774,7 @@ function niceCeil(v) {
737
774
  }
738
775
  return 10 * mag;
739
776
  }
740
- function SparklineView({ data, height = 56, color = "#3987e5", showValue = false, format }) {
777
+ function SparklineView({ data, height = 56, color = "#3987e5", showValue = false, format, yFormat }) {
741
778
  const gradientId = react.useId();
742
779
  const width = 260;
743
780
  const pts = data && data.length ? data : [0];
@@ -747,7 +784,8 @@ function SparklineView({ data, height = 56, color = "#3987e5", showValue = false
747
784
  const line = pts.map((v, i) => `${i === 0 ? "M" : "L"} ${xy(v, i)[0].toFixed(1)} ${xy(v, i)[1].toFixed(1)}`).join(" ");
748
785
  const area = `${line} L ${width} ${height} L 0 ${height} Z`;
749
786
  const current = pts[pts.length - 1];
750
- const readout = format ? format(current) : current.toLocaleString(void 0, { maximumFractionDigits: 1 });
787
+ const fmt = format ?? yFormat;
788
+ const readout = fmt ? fmt(current) : current.toLocaleString(void 0, { maximumFractionDigits: 1 });
751
789
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
752
790
  /* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: `0 0 ${width} ${height}`, height, className: "w-full", preserveAspectRatio: "none", children: [
753
791
  /* @__PURE__ */ jsxRuntime.jsx("defs", { children: /* @__PURE__ */ jsxRuntime.jsxs("linearGradient", { id: gradientId, x1: "0", y1: "0", x2: "0", y2: "1", children: [
@@ -3080,6 +3118,7 @@ exports.buildChartSQL = buildChartSQL;
3080
3118
  exports.buildNivoTheme = buildNivoTheme;
3081
3119
  exports.chartSizing = chartSizing;
3082
3120
  exports.compileWhere = compileWhere;
3121
+ exports.formatBytes = formatBytes;
3083
3122
  exports.groupBy = groupBy;
3084
3123
  exports.heatColor = heatColor;
3085
3124
  exports.heatLabelColor = heatLabelColor;