@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 +25 -0
- package/dist/index.cjs +54 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -7
- package/dist/index.d.ts +40 -7
- package/dist/index.js +54 -16
- package/dist/index.js.map +1 -1
- package/package.json +8 -2
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
|
|
597
|
-
const
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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;
|