@quantumwake/terminal-ux-dashboard-components 0.1.18 → 0.1.19

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/dist/index.cjs CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  var react = require('react');
4
4
  var jsxRuntime = require('react/jsx-runtime');
5
+ var d3ScaleChromatic = require('d3-scale-chromatic');
5
6
  var bar = require('@nivo/bar');
6
7
  var pie = require('@nivo/pie');
7
8
  var line = require('@nivo/line');
@@ -83,6 +84,8 @@ var aggExpr = (agg, col) => {
83
84
  return `MIN(${qNum(col)})`;
84
85
  case "max":
85
86
  return `MAX(${qNum(col)})`;
87
+ case "median":
88
+ return `MEDIAN(${qNum(col)})`;
86
89
  default:
87
90
  return "COUNT(*)";
88
91
  }
@@ -120,7 +123,10 @@ var buildChartSQL = ({
120
123
  xFields = [],
121
124
  yFields = [],
122
125
  valueField = null,
123
- filters = []
126
+ filters = [],
127
+ labelStat,
128
+ colorStat,
129
+ blockField
124
130
  }) => {
125
131
  const where = compileWhere(filters);
126
132
  const x = xFields[0]?.name;
@@ -157,6 +163,14 @@ var buildChartSQL = ({
157
163
  const metric = valueField ? aggExpr(valueField.agg || "count", valueField.name) : "COUNT(*)";
158
164
  return `SELECT ${qIdent(x)} AS r, ${qIdent(y)} AS c, ${metric} AS v FROM ${TABLE}${where} GROUP BY 1, 2`;
159
165
  }
166
+ case "heatmap-plus": {
167
+ if (!x || !y) return null;
168
+ const col = valueField?.name;
169
+ const lv = col ? aggExpr(labelStat || valueField?.agg || "count", col) : "COUNT(*)";
170
+ const cv = col ? aggExpr(colorStat || labelStat || valueField?.agg || "count", col) : "COUNT(*)";
171
+ const blockSel = blockField ? `, ANY_VALUE(${qIdent(blockField)}) AS block` : "";
172
+ return `SELECT ${qIdent(x)} AS r, ${qIdent(y)} AS c, ${lv} AS lv, ${cv} AS cv${blockSel}, GROUPING(${qIdent(x)}) AS gr, GROUPING(${qIdent(y)}) AS gc FROM ${TABLE}${where} GROUP BY GROUPING SETS ((${qIdent(x)}, ${qIdent(y)}), (${qIdent(x)}), (${qIdent(y)}), ())`;
173
+ }
160
174
  default:
161
175
  return null;
162
176
  }
@@ -205,6 +219,17 @@ var shapeChartData = (chartType, rows, { yFields = [] } = {}) => {
205
219
  data: colKeys.map((ck) => ({ x: ck, y: cell[rk][ck] || 0 }))
206
220
  }));
207
221
  }
222
+ case "heatmap-plus": {
223
+ return rows.map((r) => ({
224
+ r: r.r == null ? null : String(r.r),
225
+ c: r.c == null ? null : String(r.c),
226
+ lv: Number(r.lv) || 0,
227
+ cv: Number(r.cv) || 0,
228
+ gr: Number(r.gr) || 0,
229
+ gc: Number(r.gc) || 0,
230
+ ...r.block != null ? { block: String(r.block) } : {}
231
+ }));
232
+ }
208
233
  default:
209
234
  return [];
210
235
  }
@@ -350,10 +375,73 @@ var aggregate = (records, column, fn) => {
350
375
  return Math.min(...values.map(Number));
351
376
  case "max":
352
377
  return Math.max(...values.map(Number));
378
+ case "median": {
379
+ const nums = values.map(Number).filter((v) => !Number.isNaN(v)).sort((a, b) => a - b);
380
+ if (!nums.length) return 0;
381
+ const mid = Math.floor(nums.length / 2);
382
+ return nums.length % 2 ? nums[mid] : (nums[mid - 1] + nums[mid]) / 2;
383
+ }
353
384
  default:
354
385
  return records.length;
355
386
  }
356
387
  };
388
+ var HEAT_STATS = ["count", "sum", "avg", "min", "max", "median"];
389
+ var cellKey = (row, col) => `${row}\0${col}`;
390
+ var normalizePartition = (cells, method, vmin, vmax) => {
391
+ const out = /* @__PURE__ */ new Map();
392
+ const values = cells.map((c) => c.colorValue).filter((v) => Number.isFinite(v));
393
+ if (!values.length) return out;
394
+ if (method === "rank") {
395
+ const sorted = [...values].sort((a, b) => a - b);
396
+ for (const c of cells) {
397
+ const below = sorted.findIndex((v) => v >= c.colorValue);
398
+ const ties = sorted.filter((v) => v === c.colorValue).length;
399
+ out.set(cellKey(c.row, c.col), (below + 0.5 * ties) / sorted.length);
400
+ }
401
+ return out;
402
+ }
403
+ const lo = vmin ?? Math.min(...values);
404
+ const hi = vmax ?? Math.max(...values);
405
+ for (const c of cells) {
406
+ const t = hi === lo ? 0.5 : (c.colorValue - lo) / (hi - lo);
407
+ out.set(cellKey(c.row, c.col), Math.max(0, Math.min(1, t)));
408
+ }
409
+ return out;
410
+ };
411
+ var normalizeCells = (cells, { scope = "global", method = "linear", vmin, vmax } = {}) => {
412
+ const partitions = /* @__PURE__ */ new Map();
413
+ const partOf = (c) => {
414
+ switch (scope) {
415
+ case "row":
416
+ return `r:${c.row}`;
417
+ case "column":
418
+ return `c:${c.col}`;
419
+ case "block":
420
+ return c.block != null ? `b:${c.block}` : "global";
421
+ default:
422
+ return "global";
423
+ }
424
+ };
425
+ for (const c of cells) {
426
+ const k = partOf(c);
427
+ const part = partitions.get(k);
428
+ if (part) part.push(c);
429
+ else partitions.set(k, [c]);
430
+ }
431
+ const out = /* @__PURE__ */ new Map();
432
+ for (const part of partitions.values()) {
433
+ for (const [k, t] of normalizePartition(part, method, vmin, vmax)) out.set(k, t);
434
+ }
435
+ return out;
436
+ };
437
+ var heatColor = (t) => d3ScaleChromatic.interpolateRdYlGn(Math.max(0, Math.min(1, t)));
438
+ var heatLabelColor = (t) => {
439
+ const m = heatColor(t).match(/\d+/g);
440
+ if (!m) return "#0f172a";
441
+ const [r, g, b] = m.map(Number);
442
+ const lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
443
+ return lum > 140 ? "#0f172a" : "#f8fafc";
444
+ };
357
445
  function MetricView({ records, config, value: presetValue }) {
358
446
  const value = react.useMemo(() => {
359
447
  if (presetValue != null) return presetValue;
@@ -669,6 +757,159 @@ function HeatmapView({
669
757
  }
670
758
  ) });
671
759
  }
760
+ var MAX_AXIS2 = 30;
761
+ var TOTAL_ID = "\u03A3";
762
+ var NULL = "(null)";
763
+ var fmtNum2 = (n) => {
764
+ if (typeof n !== "number" || Number.isNaN(n)) return "";
765
+ return Number.isInteger(n) ? n.toLocaleString() : n.toLocaleString(void 0, { maximumFractionDigits: 2 });
766
+ };
767
+ var orderIds2 = (ids, margin, order) => {
768
+ const sorted = [...ids];
769
+ if (order.endsWith("-desc")) sorted.sort((a, b) => (margin[b]?.lv ?? 0) - (margin[a]?.lv ?? 0));
770
+ else if (order.endsWith("-asc")) sorted.sort((a, b) => (margin[a]?.lv ?? 0) - (margin[b]?.lv ?? 0));
771
+ else sorted.sort();
772
+ return sorted;
773
+ };
774
+ function fromRecords(records, rowCol, colCol, valueCol, labelStat, colorStat, blockField) {
775
+ const col = valueCol || rowCol;
776
+ const statOf = (rs, s) => valueCol ? aggregate(rs, col, s) : rs.length;
777
+ const both = (rs) => ({ lv: statOf(rs, labelStat), cv: statOf(rs, colorStat) });
778
+ const byRow = groupBy(records, rowCol);
779
+ const rowIds = Object.keys(byRow);
780
+ const colIds = [...new Set(records.map((r) => String(r[colCol] ?? NULL)))];
781
+ const cells = {};
782
+ for (const rid of rowIds) {
783
+ cells[rid] = {};
784
+ const byCol2 = groupBy(byRow[rid], colCol);
785
+ for (const cid of colIds) {
786
+ const cellRows = byCol2[cid] || [];
787
+ const stat = both(cellRows);
788
+ if (blockField && cellRows.length) stat.block = String(cellRows[0][blockField] ?? NULL);
789
+ cells[rid][cid] = stat;
790
+ }
791
+ }
792
+ const rowMargin = {};
793
+ for (const rid of rowIds) rowMargin[rid] = both(byRow[rid]);
794
+ const colMargin = {};
795
+ const byCol = groupBy(records, colCol);
796
+ for (const cid of colIds) colMargin[cid] = both(byCol[cid] || []);
797
+ return { rowIds, colIds, cells, rowMargin, colMargin, grand: both(records) };
798
+ }
799
+ function fromData(data) {
800
+ const cells = {};
801
+ const rowMargin = {};
802
+ const colMargin = {};
803
+ let grand = { lv: 0, cv: 0 };
804
+ const rowIds = [];
805
+ const colIds = [];
806
+ for (const d of data) {
807
+ const stat = { lv: d.lv, cv: d.cv, ...d.block != null ? { block: d.block } : {} };
808
+ if (d.gr && d.gc) {
809
+ grand = stat;
810
+ continue;
811
+ }
812
+ if (d.gr) {
813
+ if (d.c != null) colMargin[d.c] = stat;
814
+ continue;
815
+ }
816
+ if (d.gc) {
817
+ if (d.r != null) rowMargin[d.r] = stat;
818
+ continue;
819
+ }
820
+ const r = d.r ?? NULL, c = d.c ?? NULL;
821
+ if (!cells[r]) {
822
+ cells[r] = {};
823
+ rowIds.push(r);
824
+ }
825
+ if (!colIds.includes(c)) colIds.push(c);
826
+ cells[r][c] = stat;
827
+ }
828
+ return { rowIds, colIds, cells, rowMargin, colMargin, grand };
829
+ }
830
+ function HeatmapPlusView({
831
+ records,
832
+ rowColumn,
833
+ colColumn,
834
+ valueColumn,
835
+ labelStat = "count",
836
+ colorStat = "count",
837
+ blockField,
838
+ data: presetData,
839
+ scope = "global",
840
+ method = "linear",
841
+ vmin,
842
+ vmax,
843
+ rowOrder = "alpha",
844
+ colOrder = "alpha",
845
+ showMargins = false,
846
+ style
847
+ }) {
848
+ const s = withStyleDefaults(style);
849
+ const model = react.useMemo(() => {
850
+ if (presetData) return fromData(presetData);
851
+ if (records) return fromRecords(records, rowColumn, colColumn, valueColumn, labelStat, colorStat, blockField);
852
+ return null;
853
+ }, [presetData, records, rowColumn, colColumn, valueColumn, labelStat, colorStat, blockField]);
854
+ const built = react.useMemo(() => {
855
+ if (!model || !model.rowIds.length || !model.colIds.length) return null;
856
+ const { cells, rowMargin, colMargin, grand } = model;
857
+ const rows = orderIds2(model.rowIds, rowMargin, rowOrder).slice(0, MAX_AXIS2);
858
+ const cols = orderIds2(model.colIds, colMargin, colOrder).slice(0, MAX_AXIS2);
859
+ const bodyCells = [];
860
+ for (const rid of rows) for (const cid of cols) {
861
+ bodyCells.push({ row: rid, col: cid, colorValue: cells[rid]?.[cid]?.cv ?? NaN, block: cells[rid]?.[cid]?.block });
862
+ }
863
+ const t2 = new Map(normalizeCells(bodyCells, { scope, method, vmin, vmax }));
864
+ if (showMargins) {
865
+ const rowMc = rows.map((rid) => ({ row: rid, col: TOTAL_ID, colorValue: rowMargin[rid]?.cv ?? NaN }));
866
+ const colMc = cols.map((cid) => ({ row: TOTAL_ID, col: cid, colorValue: colMargin[cid]?.cv ?? NaN }));
867
+ for (const [k, v] of normalizeCells(rowMc, { scope: "global", method })) t2.set(k, v);
868
+ for (const [k, v] of normalizeCells(colMc, { scope: "global", method })) t2.set(k, v);
869
+ t2.set(cellKey(TOTAL_ID, TOTAL_ID), 0.5);
870
+ }
871
+ const colKeys = showMargins ? [...cols, TOTAL_ID] : cols;
872
+ const nivoData2 = rows.map((rid) => ({
873
+ id: rid,
874
+ data: colKeys.map((cid) => ({
875
+ x: cid,
876
+ y: cid === TOTAL_ID ? rowMargin[rid]?.lv ?? 0 : cells[rid]?.[cid]?.lv ?? 0
877
+ }))
878
+ }));
879
+ if (showMargins) {
880
+ nivoData2.push({
881
+ id: TOTAL_ID,
882
+ data: colKeys.map((cid) => ({
883
+ x: cid,
884
+ y: cid === TOTAL_ID ? grand.lv : colMargin[cid]?.lv ?? 0
885
+ }))
886
+ });
887
+ }
888
+ return { nivoData: nivoData2, t: t2 };
889
+ }, [model, rowOrder, colOrder, scope, method, vmin, vmax, showMargins]);
890
+ if (!built) {
891
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-8 text-center text-midnight-text-muted", children: "Not enough distinct values for a heatmap" });
892
+ }
893
+ const { nivoData, t } = built;
894
+ const tAt = (serieId, x) => t.get(cellKey(serieId, x)) ?? 0;
895
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-full w-full min-h-[160px]", children: /* @__PURE__ */ jsxRuntime.jsx(
896
+ heatmap.ResponsiveHeatMap,
897
+ {
898
+ data: nivoData,
899
+ margin: { top: 60, right: 20, bottom: showMargins ? 60 : 20, left: 100 },
900
+ valueFormat: ((v) => fmtNum2(v)),
901
+ axisTop: { tickSize: 5, tickPadding: 5, tickRotation: s.xTickRotation, legend: s.showXLegend ? s.xAxisLabel || colColumn : "", legendPosition: s.xLegendPosition, legendOffset: -50 },
902
+ axisLeft: { tickSize: 5, tickPadding: 5, legend: s.showYLegend ? s.yAxisLabel || rowColumn : "", legendPosition: s.yLegendPosition, legendOffset: -80 },
903
+ colors: ((cell) => heatColor(tAt(cell.serieId, cell.data.x))),
904
+ emptyColor: "#1e293b",
905
+ borderWidth: 1,
906
+ borderColor: "#334155",
907
+ labelTextColor: ((cell) => heatLabelColor(tAt(cell.serieId, cell.data.x))),
908
+ hoverTarget: "cell",
909
+ theme: buildNivoTheme(style)
910
+ }
911
+ ) });
912
+ }
672
913
  function PivotView({ records }) {
673
914
  const [pivotState, setPivotState] = react.useState({});
674
915
  if (!records?.length) {
@@ -1205,6 +1446,7 @@ var CHART_TYPES = [
1205
1446
  { id: "line", icon: lucideReact.TrendingUp, label: "Line" },
1206
1447
  { id: "scatter", icon: lucideReact.ScatterChart, label: "Scatter" },
1207
1448
  { id: "heatmap", icon: lucideReact.LayoutGrid, label: "Heatmap" },
1449
+ { id: "heatmap-plus", icon: lucideReact.Grid3x3, label: "Heatmap+" },
1208
1450
  { id: "grouped-bar", icon: lucideReact.BarChart3, label: "Grouped Bar" }
1209
1451
  ];
1210
1452
  var AGG_OPTIONS = ["count", "distinct", "sum", "avg", "min", "max"];
@@ -1225,6 +1467,17 @@ var ORDER_OPTIONS = [
1225
1467
  { id: "total-asc", label: "Total \u2191" }
1226
1468
  ];
1227
1469
  var MARGIN_AGGS = ["sum", "avg", "min", "max", "count"].map((a) => ({ id: a, label: a }));
1470
+ var HEAT_STAT_OPTIONS = HEAT_STATS.map((a) => ({ id: a, label: a }));
1471
+ var SCOPE_OPTIONS = [
1472
+ { id: "global", label: "Global" },
1473
+ { id: "row", label: "Per row" },
1474
+ { id: "column", label: "Per column" },
1475
+ { id: "block", label: "Per block" }
1476
+ ];
1477
+ var METHOD_OPTIONS = [
1478
+ { id: "linear", label: "Linear" },
1479
+ { id: "rank", label: "Rank" }
1480
+ ];
1228
1481
  var FILTER_OP_OPTIONS = FILTER_OPS.map((o) => ({ id: o, label: o }));
1229
1482
  var FieldPill = ({ name, type, onRemove, onChangeAgg, agg, showAgg }) => {
1230
1483
  const typeColor = {
@@ -1413,6 +1666,7 @@ var ChartPreview = ({
1413
1666
  sqlLoading,
1414
1667
  sqlError,
1415
1668
  heatmapOpts = {},
1669
+ heatPlusOpts = {},
1416
1670
  style
1417
1671
  }) => {
1418
1672
  const xCol = xFields[0]?.name;
@@ -1451,6 +1705,8 @@ var ChartPreview = ({
1451
1705
  return /* @__PURE__ */ jsxRuntime.jsx(react.Suspense, { fallback: null, children: /* @__PURE__ */ jsxRuntime.jsx(ScatterView, { data: shaped, xColumn: xCol, yColumn: yCol, style }) });
1452
1706
  case "heatmap":
1453
1707
  return shaped.length ? /* @__PURE__ */ jsxRuntime.jsx(react.Suspense, { fallback: null, children: /* @__PURE__ */ jsxRuntime.jsx(HeatmapView, { data: shaped, rowColumn: xCol, colColumn: yCol, ...heatmapOpts, style }) }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center h-full text-sm text-midnight-text-muted", children: "No data" });
1708
+ case "heatmap-plus":
1709
+ return shaped.length ? /* @__PURE__ */ jsxRuntime.jsx(react.Suspense, { fallback: null, children: /* @__PURE__ */ jsxRuntime.jsx(HeatmapPlusView, { data: shaped, rowColumn: xCol, colColumn: yCol, ...heatPlusOpts, style }) }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center h-full text-sm text-midnight-text-muted", children: "No data" });
1454
1710
  default:
1455
1711
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center h-full text-sm text-midnight-text-muted", children: "Select a chart type" });
1456
1712
  }
@@ -1472,6 +1728,8 @@ var ChartPreview = ({
1472
1728
  return xCol && yCol ? /* @__PURE__ */ jsxRuntime.jsx(react.Suspense, { fallback: null, children: /* @__PURE__ */ jsxRuntime.jsx(ScatterView, { records, xColumn: xCol, yColumn: yCol, style }) }) : null;
1473
1729
  case "heatmap":
1474
1730
  return xCol && yCol ? /* @__PURE__ */ jsxRuntime.jsx(react.Suspense, { fallback: null, children: /* @__PURE__ */ jsxRuntime.jsx(HeatmapView, { records, rowColumn: xCol, colColumn: yCol, valueColumn: valueField?.name, aggFn: valueField?.agg || "count", ...heatmapOpts, style }) }) : null;
1731
+ case "heatmap-plus":
1732
+ return xCol && yCol ? /* @__PURE__ */ jsxRuntime.jsx(react.Suspense, { fallback: null, children: /* @__PURE__ */ jsxRuntime.jsx(HeatmapPlusView, { records, rowColumn: xCol, colColumn: yCol, valueColumn: valueField?.name, ...heatPlusOpts, style }) }) : null;
1475
1733
  default:
1476
1734
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center h-full text-sm text-midnight-text-muted", children: "Select a chart type" });
1477
1735
  }
@@ -1495,6 +1753,13 @@ function ChartBuilder({ records, columns, stateId, onSave }) {
1495
1753
  const [colOrder, setColOrder] = react.useState("alpha");
1496
1754
  const [marginAgg, setMarginAgg] = react.useState("sum");
1497
1755
  const [showTotals, setShowTotals] = react.useState(false);
1756
+ const [labelStat, setLabelStat] = react.useState("count");
1757
+ const [colorStat, setColorStat] = react.useState("count");
1758
+ const [scope, setScope] = react.useState("global");
1759
+ const [method, setMethod] = react.useState("linear");
1760
+ const [vmin, setVmin] = react.useState("");
1761
+ const [vmax, setVmax] = react.useState("");
1762
+ const [showMargins, setShowMargins] = react.useState(false);
1498
1763
  const [style, setStyle] = react.useState(DEFAULT_CHART_STYLE);
1499
1764
  const [showStyle, setShowStyle] = react.useState(false);
1500
1765
  const [showFields, setShowFields] = react.useState(false);
@@ -1503,8 +1768,17 @@ function ChartBuilder({ records, columns, stateId, onSave }) {
1503
1768
  const [sqlLoading, setSqlLoading] = react.useState(false);
1504
1769
  const [sqlError, setSqlError] = react.useState(null);
1505
1770
  const generatedSql = react.useMemo(
1506
- () => buildChartSQL({ chartType, xFields, yFields, valueField, filters }),
1507
- [chartType, xFields, yFields, valueField, filters]
1771
+ () => buildChartSQL({
1772
+ chartType,
1773
+ xFields,
1774
+ yFields,
1775
+ valueField,
1776
+ filters,
1777
+ labelStat,
1778
+ colorStat,
1779
+ blockField: colorField?.name
1780
+ }),
1781
+ [chartType, xFields, yFields, valueField, filters, labelStat, colorStat, colorField]
1508
1782
  );
1509
1783
  react.useEffect(() => {
1510
1784
  if (engineMode !== "sql") return void 0;
@@ -1602,6 +1876,26 @@ function ChartBuilder({ records, columns, stateId, onSave }) {
1602
1876
  config.marginAgg = marginAgg;
1603
1877
  config.showTotals = showTotals;
1604
1878
  }
1879
+ if (chartType === "heatmap-plus") {
1880
+ config.row = xFields[0]?.name;
1881
+ config.col = yFields[0]?.name;
1882
+ if (valueField) config.value = valueField.name;
1883
+ else delete config.value;
1884
+ config.labelStat = labelStat;
1885
+ config.colorStat = colorStat;
1886
+ if (colorField) config.blockField = colorField.name;
1887
+ else delete config.blockField;
1888
+ config.scope = scope;
1889
+ config.method = method;
1890
+ const nMin = parseFloat(vmin), nMax = parseFloat(vmax);
1891
+ if (Number.isFinite(nMin)) config.vmin = nMin;
1892
+ else delete config.vmin;
1893
+ if (Number.isFinite(nMax)) config.vmax = nMax;
1894
+ else delete config.vmax;
1895
+ config.rowOrder = rowOrder;
1896
+ config.colOrder = colOrder;
1897
+ config.showMargins = showMargins;
1898
+ }
1605
1899
  if (engineMode === "sql" && generatedSql) {
1606
1900
  config.sql = generatedSql;
1607
1901
  config.chartType = chartType;
@@ -1622,6 +1916,7 @@ function ChartBuilder({ records, columns, stateId, onSave }) {
1622
1916
  height: fillContainer ? 8 : 2
1623
1917
  });
1624
1918
  };
1919
+ const isHeat = chartType === "heatmap" || chartType === "heatmap-plus";
1625
1920
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex h-full", children: [
1626
1921
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `w-[280px] shrink-0 border-r ${theme.border} bg-midnight-elevated overflow-y-auto p-3 space-y-4`, children: [
1627
1922
  /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
@@ -1657,30 +1952,30 @@ function ChartBuilder({ records, columns, stateId, onSave }) {
1657
1952
  /* @__PURE__ */ jsxRuntime.jsx(
1658
1953
  DropZone,
1659
1954
  {
1660
- label: chartType === "heatmap" ? "Rows" : "X Axis / Group By",
1955
+ label: isHeat ? "Rows" : "X Axis / Group By",
1661
1956
  fields: xFields,
1662
1957
  columns,
1663
1958
  onAdd: (f) => addField("x", f),
1664
1959
  onRemove: (i) => removeField("x", i),
1665
1960
  onChangeAgg: () => {
1666
1961
  },
1667
- maxFields: chartType === "pie" || chartType === "heatmap" ? 1 : 3
1962
+ maxFields: chartType === "pie" || isHeat ? 1 : 3
1668
1963
  }
1669
1964
  ),
1670
1965
  /* @__PURE__ */ jsxRuntime.jsx(
1671
1966
  DropZone,
1672
1967
  {
1673
- label: chartType === "heatmap" ? "Columns" : "Y Axis / Values",
1968
+ label: isHeat ? "Columns" : "Y Axis / Values",
1674
1969
  fields: yFields,
1675
1970
  columns,
1676
1971
  onAdd: (f) => addField("y", f),
1677
1972
  onRemove: (i) => removeField("y", i),
1678
1973
  onChangeAgg: (i, agg) => changeAgg("y", i, agg),
1679
- showAgg: chartType !== "heatmap",
1680
- maxFields: chartType === "heatmap" ? 1 : 5
1974
+ showAgg: !isHeat,
1975
+ maxFields: isHeat ? 1 : 5
1681
1976
  }
1682
1977
  ),
1683
- chartType === "heatmap" && /* @__PURE__ */ jsxRuntime.jsx(
1978
+ isHeat && /* @__PURE__ */ jsxRuntime.jsx(
1684
1979
  DropZone,
1685
1980
  {
1686
1981
  label: "Cell Value (optional \u2014 defaults to count)",
@@ -1689,10 +1984,93 @@ function ChartBuilder({ records, columns, stateId, onSave }) {
1689
1984
  onAdd: (f) => addField("value", f),
1690
1985
  onRemove: () => removeField("value"),
1691
1986
  onChangeAgg: (i, agg) => changeAgg("value", i, agg),
1692
- showAgg: true,
1987
+ showAgg: chartType === "heatmap",
1693
1988
  maxFields: 1
1694
1989
  }
1695
1990
  ),
1991
+ chartType === "heatmap-plus" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1992
+ /* @__PURE__ */ jsxRuntime.jsx(
1993
+ DropZone,
1994
+ {
1995
+ label: "Block (optional \u2014 color-scope grouping)",
1996
+ fields: colorField ? [colorField] : [],
1997
+ columns,
1998
+ onAdd: (f) => addField("color", f),
1999
+ onRemove: () => removeField("color"),
2000
+ onChangeAgg: () => {
2001
+ },
2002
+ maxFields: 1
2003
+ }
2004
+ ),
2005
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2006
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs uppercase text-midnight-text-muted mb-2 font-mono", children: "Cell Stats" }),
2007
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
2008
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-3 min-h-[28px]", children: [
2009
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-mono text-midnight-text-muted", children: "Label stat" }),
2010
+ /* @__PURE__ */ jsxRuntime.jsx(SelectControl, { value: labelStat, options: HEAT_STAT_OPTIONS, onChange: (v) => setLabelStat(v) })
2011
+ ] }),
2012
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-3 min-h-[28px]", children: [
2013
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-mono text-midnight-text-muted", children: "Color stat" }),
2014
+ /* @__PURE__ */ jsxRuntime.jsx(SelectControl, { value: colorStat, options: HEAT_STAT_OPTIONS, onChange: (v) => setColorStat(v) })
2015
+ ] })
2016
+ ] })
2017
+ ] }),
2018
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2019
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs uppercase text-midnight-text-muted mb-2 font-mono", children: "Color Engine" }),
2020
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
2021
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-3 min-h-[28px]", children: [
2022
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-mono text-midnight-text-muted", children: "Scale scope" }),
2023
+ /* @__PURE__ */ jsxRuntime.jsx(SelectControl, { value: scope, options: SCOPE_OPTIONS, onChange: (v) => setScope(v) })
2024
+ ] }),
2025
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-3 min-h-[28px]", children: [
2026
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-mono text-midnight-text-muted", children: "Scale method" }),
2027
+ /* @__PURE__ */ jsxRuntime.jsx(SelectControl, { value: method, options: METHOD_OPTIONS, onChange: (v) => setMethod(v) })
2028
+ ] }),
2029
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-3 min-h-[28px]", children: [
2030
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-mono text-midnight-text-muted", children: "Fixed min / max" }),
2031
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-1", children: [
2032
+ /* @__PURE__ */ jsxRuntime.jsx(
2033
+ "input",
2034
+ {
2035
+ value: vmin,
2036
+ onChange: (e) => setVmin(e.target.value),
2037
+ placeholder: "auto",
2038
+ inputMode: "decimal",
2039
+ className: "w-14 px-2 py-1 text-xs font-mono bg-midnight-surface border border-midnight-border text-midnight-text-body outline-none focus:border-midnight-accent"
2040
+ }
2041
+ ),
2042
+ /* @__PURE__ */ jsxRuntime.jsx(
2043
+ "input",
2044
+ {
2045
+ value: vmax,
2046
+ onChange: (e) => setVmax(e.target.value),
2047
+ placeholder: "auto",
2048
+ inputMode: "decimal",
2049
+ className: "w-14 px-2 py-1 text-xs font-mono bg-midnight-surface border border-midnight-border text-midnight-text-body outline-none focus:border-midnight-accent"
2050
+ }
2051
+ )
2052
+ ] })
2053
+ ] })
2054
+ ] })
2055
+ ] }),
2056
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2057
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs uppercase text-midnight-text-muted mb-2 font-mono", children: "Order & Margins" }),
2058
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
2059
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-3 min-h-[28px]", children: [
2060
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-mono text-midnight-text-muted", children: "Order rows" }),
2061
+ /* @__PURE__ */ jsxRuntime.jsx(SelectControl, { value: rowOrder, options: ORDER_OPTIONS, onChange: (v) => setRowOrder(v) })
2062
+ ] }),
2063
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-3 min-h-[28px]", children: [
2064
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-mono text-midnight-text-muted", children: "Order columns" }),
2065
+ /* @__PURE__ */ jsxRuntime.jsx(SelectControl, { value: colOrder, options: ORDER_OPTIONS, onChange: (v) => setColOrder(v) })
2066
+ ] }),
2067
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex items-center gap-2 cursor-pointer", children: [
2068
+ /* @__PURE__ */ jsxRuntime.jsx("input", { type: "checkbox", checked: showMargins, onChange: (e) => setShowMargins(e.target.checked), className: "accent-midnight-accent" }),
2069
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-mono text-midnight-text-muted", children: "Show margins (row / col / grand)" })
2070
+ ] })
2071
+ ] })
2072
+ ] })
2073
+ ] }),
1696
2074
  chartType === "heatmap" && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1697
2075
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs uppercase text-midnight-text-muted mb-2 font-mono", children: "Order & Totals" }),
1698
2076
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
@@ -1826,6 +2204,18 @@ function ChartBuilder({ records, columns, stateId, onSave }) {
1826
2204
  sqlLoading,
1827
2205
  sqlError,
1828
2206
  heatmapOpts: { rowOrder, colOrder, marginAgg, showTotals },
2207
+ heatPlusOpts: {
2208
+ labelStat,
2209
+ colorStat,
2210
+ blockField: colorField?.name,
2211
+ scope,
2212
+ method,
2213
+ vmin: Number.isFinite(parseFloat(vmin)) ? parseFloat(vmin) : void 0,
2214
+ vmax: Number.isFinite(parseFloat(vmax)) ? parseFloat(vmax) : void 0,
2215
+ rowOrder,
2216
+ colOrder,
2217
+ showMargins
2218
+ },
1829
2219
  style
1830
2220
  }
1831
2221
  ) })
@@ -1834,7 +2224,7 @@ function ChartBuilder({ records, columns, stateId, onSave }) {
1834
2224
  function ViewLoading() {
1835
2225
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center h-full text-xs text-midnight-text-muted", children: "Loading..." });
1836
2226
  }
1837
- var SQL_CHART_TYPES = /* @__PURE__ */ new Set(["bar", "grouped-bar", "pie", "line", "scatter", "heatmap"]);
2227
+ var SQL_CHART_TYPES = /* @__PURE__ */ new Set(["bar", "grouped-bar", "pie", "line", "scatter", "heatmap", "heatmap-plus"]);
1838
2228
  var SQL_DATA_TYPES = /* @__PURE__ */ new Set(["table", "metric"]);
1839
2229
  var TABLE_PREVIEW_ROWS = 200;
1840
2230
  function tableSql(config) {
@@ -1865,6 +2255,14 @@ function panelToChartConfig(chartType, config) {
1865
2255
  if (config.col) cfg.yFields = [{ name: config.col }];
1866
2256
  if (config.value) cfg.valueField = { name: config.value, agg: config.agg };
1867
2257
  break;
2258
+ case "heatmap-plus":
2259
+ if (config.row) cfg.xFields = [{ name: config.row }];
2260
+ if (config.col) cfg.yFields = [{ name: config.col }];
2261
+ if (config.value) cfg.valueField = { name: config.value, agg: config.labelStat };
2262
+ cfg.labelStat = config.labelStat;
2263
+ cfg.colorStat = config.colorStat;
2264
+ cfg.blockField = config.blockField;
2265
+ break;
1868
2266
  }
1869
2267
  return cfg;
1870
2268
  }
@@ -2013,6 +2411,9 @@ function SqlPanel({ panel }) {
2013
2411
  case "heatmap":
2014
2412
  chart = shaped.length ? /* @__PURE__ */ jsxRuntime.jsx(HeatmapView, { data: shaped, rowColumn: config.row || "", colColumn: config.col || "", rowOrder: config.rowOrder, colOrder: config.colOrder, marginAgg: config.marginAgg, showTotals: config.showTotals, style: config.style }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center h-full text-xs text-midnight-text-muted", children: "No data" });
2015
2413
  break;
2414
+ case "heatmap-plus":
2415
+ chart = shaped.length ? /* @__PURE__ */ jsxRuntime.jsx(HeatmapPlusView, { data: shaped, rowColumn: config.row || "", colColumn: config.col || "", scope: config.scope, method: config.method, vmin: config.vmin, vmax: config.vmax, rowOrder: config.rowOrder, colOrder: config.colOrder, showMargins: config.showMargins, style: config.style }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center h-full text-xs text-midnight-text-muted", children: "No data" });
2416
+ break;
2016
2417
  default:
2017
2418
  chart = /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-4 text-xs text-midnight-text-muted", children: [
2018
2419
  "Unsupported SQL chart: ",
@@ -2046,6 +2447,8 @@ function PanelContent({ panel, records, columns }) {
2046
2447
  return config.x && config.y ? /* @__PURE__ */ jsxRuntime.jsx(ScatterView, { records, xColumn: config.x, yColumn: config.y, style: config.style }) : null;
2047
2448
  case "heatmap":
2048
2449
  return config.row && config.col ? /* @__PURE__ */ jsxRuntime.jsx(HeatmapView, { records, rowColumn: config.row, colColumn: config.col, valueColumn: config.value, aggFn: config.agg || "count", rowOrder: config.rowOrder, colOrder: config.colOrder, marginAgg: config.marginAgg, showTotals: config.showTotals, style: config.style }) : null;
2450
+ case "heatmap-plus":
2451
+ return config.row && config.col ? /* @__PURE__ */ jsxRuntime.jsx(HeatmapPlusView, { records, rowColumn: config.row, colColumn: config.col, valueColumn: config.value, labelStat: config.labelStat, colorStat: config.colorStat, blockField: config.blockField, scope: config.scope, method: config.method, vmin: config.vmin, vmax: config.vmax, rowOrder: config.rowOrder, colOrder: config.colOrder, showMargins: config.showMargins, style: config.style }) : null;
2049
2452
  case "pivot":
2050
2453
  return /* @__PURE__ */ jsxRuntime.jsx(PivotView, { records });
2051
2454
  case "metric":
@@ -2512,6 +2915,8 @@ exports.DEFAULT_CHART_STYLE = DEFAULT_CHART_STYLE;
2512
2915
  exports.DashboardProvider = DashboardProvider;
2513
2916
  exports.DashboardRenderer = DashboardRenderer;
2514
2917
  exports.DataExplorer = DataExplorer;
2918
+ exports.HEAT_STATS = HEAT_STATS;
2919
+ exports.HeatmapPlusView = HeatmapPlusView;
2515
2920
  exports.HeatmapView = HeatmapView;
2516
2921
  exports.InsightView = InsightView;
2517
2922
  exports.LEGEND_ANCHORS = LEGEND_ANCHORS;
@@ -2529,7 +2934,10 @@ exports.buildChartSQL = buildChartSQL;
2529
2934
  exports.buildNivoTheme = buildNivoTheme;
2530
2935
  exports.compileWhere = compileWhere;
2531
2936
  exports.groupBy = groupBy;
2937
+ exports.heatColor = heatColor;
2938
+ exports.heatLabelColor = heatLabelColor;
2532
2939
  exports.legendConfig = legendConfig;
2940
+ exports.normalizeCells = normalizeCells;
2533
2941
  exports.qIdent = qIdent;
2534
2942
  exports.qLit = qLit;
2535
2943
  exports.shapeChartData = shapeChartData;