@quantumwake/terminal-ux-dashboard-components 0.1.29 → 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,
@@ -658,7 +677,7 @@ function PieView({ records, groupColumn, data: presetData, style }) {
658
677
  }
659
678
  var fmtClock = (d) => d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit", hourCycle: "h23" });
660
679
  var toDate = (x) => x instanceof Date ? x : new Date(x);
661
- function LineView({ records, xColumn, yColumn, data: presetData, style, xScale = "point", xFormat, xDomain }) {
680
+ function LineView({ records, xColumn, yColumn, data: presetData, style, xScale = "point", xFormat, xDomain, yFormat }) {
662
681
  const timed = xScale === "time";
663
682
  const clock = xFormat ?? fmtClock;
664
683
  const computed = react.useMemo(() => {
@@ -680,6 +699,7 @@ function LineView({ records, xColumn, yColumn, data: presetData, style, xScale =
680
699
  );
681
700
  const s = withStyleDefaults(style);
682
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;
683
703
  const colorById = new Map(data.map((serie, i) => [
684
704
  serie.id,
685
705
  serie.color ?? (s.seriesColors.length || data.length > 1 ? seriesColor(i, s) : "rgba(96, 165, 250, 0.9)")
@@ -736,7 +756,8 @@ function LineView({ records, xColumn, yColumn, data: presetData, style, xScale =
736
756
  pointBorderColor: { from: "serieColor" },
737
757
  enableGridX: false,
738
758
  axisBottom,
739
- axisLeft: makeAxis(style, "y", yColumn, { numeric: true }),
759
+ axisLeft: makeAxis(style, "y", yColumn, { numeric: true, format: yTickFormat }),
760
+ yFormat: yTickFormat,
740
761
  useMesh: true,
741
762
  layers: ["grid", "markers", "axes", "areas", bandsLayer, linesLayer, "crosshair", "slices", "mesh", "legends"],
742
763
  animate: s.animate,
@@ -753,7 +774,7 @@ function niceCeil(v) {
753
774
  }
754
775
  return 10 * mag;
755
776
  }
756
- function SparklineView({ data, height = 56, color = "#3987e5", showValue = false, format }) {
777
+ function SparklineView({ data, height = 56, color = "#3987e5", showValue = false, format, yFormat }) {
757
778
  const gradientId = react.useId();
758
779
  const width = 260;
759
780
  const pts = data && data.length ? data : [0];
@@ -763,7 +784,8 @@ function SparklineView({ data, height = 56, color = "#3987e5", showValue = false
763
784
  const line = pts.map((v, i) => `${i === 0 ? "M" : "L"} ${xy(v, i)[0].toFixed(1)} ${xy(v, i)[1].toFixed(1)}`).join(" ");
764
785
  const area = `${line} L ${width} ${height} L 0 ${height} Z`;
765
786
  const current = pts[pts.length - 1];
766
- 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 });
767
789
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
768
790
  /* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: `0 0 ${width} ${height}`, height, className: "w-full", preserveAspectRatio: "none", children: [
769
791
  /* @__PURE__ */ jsxRuntime.jsx("defs", { children: /* @__PURE__ */ jsxRuntime.jsxs("linearGradient", { id: gradientId, x1: "0", y1: "0", x2: "0", y2: "1", children: [
@@ -3096,6 +3118,7 @@ exports.buildChartSQL = buildChartSQL;
3096
3118
  exports.buildNivoTheme = buildNivoTheme;
3097
3119
  exports.chartSizing = chartSizing;
3098
3120
  exports.compileWhere = compileWhere;
3121
+ exports.formatBytes = formatBytes;
3099
3122
  exports.groupBy = groupBy;
3100
3123
  exports.heatColor = heatColor;
3101
3124
  exports.heatLabelColor = heatLabelColor;