@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.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createContext, useContext, useMemo, useState, useEffect, useRef, useCallback, Suspense } from 'react';
2
2
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
+ import { interpolateRdYlGn } from 'd3-scale-chromatic';
3
4
  import { ResponsiveBar } from '@nivo/bar';
4
5
  import { ResponsivePie } from '@nivo/pie';
5
6
  import { ResponsiveLine } from '@nivo/line';
@@ -8,7 +9,7 @@ import { ResponsiveHeatMap } from '@nivo/heatmap';
8
9
  import PivotTableUI from 'react-pivottable/PivotTableUI';
9
10
  import 'react-pivottable/pivottable.css';
10
11
  import { TerminalSlider, TerminalInput, TerminalToggle, TerminalSelect } from '@quantumwake/terminal-ux-components';
11
- import { Loader2, Play, ChevronDown, AlertCircle, Rows3, BarChart3, PieChart, TrendingUp, ScatterChart, LayoutGrid, Code, Sparkles, Terminal, RefreshCw, Minimize2, Maximize2, Plus, Save, FolderOpen, Trash2, X, Filter, Database, Send, GripVertical } from 'lucide-react';
12
+ import { Loader2, Play, ChevronDown, AlertCircle, Rows3, BarChart3, PieChart, TrendingUp, ScatterChart, LayoutGrid, Grid3x3, Code, Sparkles, Terminal, RefreshCw, Minimize2, Maximize2, Plus, Save, FolderOpen, Trash2, X, Filter, Database, Send, GripVertical } from 'lucide-react';
12
13
  import CodeMirror, { EditorView, Prec, keymap } from '@uiw/react-codemirror';
13
14
  import { sql, PostgreSQL, schemaCompletionSource, keywordCompletionSource } from '@codemirror/lang-sql';
14
15
  import { snippetCompletion, completeFromList, autocompletion, acceptCompletion } from '@codemirror/autocomplete';
@@ -75,6 +76,8 @@ var aggExpr = (agg, col) => {
75
76
  return `MIN(${qNum(col)})`;
76
77
  case "max":
77
78
  return `MAX(${qNum(col)})`;
79
+ case "median":
80
+ return `MEDIAN(${qNum(col)})`;
78
81
  default:
79
82
  return "COUNT(*)";
80
83
  }
@@ -112,7 +115,10 @@ var buildChartSQL = ({
112
115
  xFields = [],
113
116
  yFields = [],
114
117
  valueField = null,
115
- filters = []
118
+ filters = [],
119
+ labelStat,
120
+ colorStat,
121
+ blockField
116
122
  }) => {
117
123
  const where = compileWhere(filters);
118
124
  const x = xFields[0]?.name;
@@ -149,6 +155,14 @@ var buildChartSQL = ({
149
155
  const metric = valueField ? aggExpr(valueField.agg || "count", valueField.name) : "COUNT(*)";
150
156
  return `SELECT ${qIdent(x)} AS r, ${qIdent(y)} AS c, ${metric} AS v FROM ${TABLE}${where} GROUP BY 1, 2`;
151
157
  }
158
+ case "heatmap-plus": {
159
+ if (!x || !y) return null;
160
+ const col = valueField?.name;
161
+ const lv = col ? aggExpr(labelStat || valueField?.agg || "count", col) : "COUNT(*)";
162
+ const cv = col ? aggExpr(colorStat || labelStat || valueField?.agg || "count", col) : "COUNT(*)";
163
+ const blockSel = blockField ? `, ANY_VALUE(${qIdent(blockField)}) AS block` : "";
164
+ 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)}), ())`;
165
+ }
152
166
  default:
153
167
  return null;
154
168
  }
@@ -197,6 +211,17 @@ var shapeChartData = (chartType, rows, { yFields = [] } = {}) => {
197
211
  data: colKeys.map((ck) => ({ x: ck, y: cell[rk][ck] || 0 }))
198
212
  }));
199
213
  }
214
+ case "heatmap-plus": {
215
+ return rows.map((r) => ({
216
+ r: r.r == null ? null : String(r.r),
217
+ c: r.c == null ? null : String(r.c),
218
+ lv: Number(r.lv) || 0,
219
+ cv: Number(r.cv) || 0,
220
+ gr: Number(r.gr) || 0,
221
+ gc: Number(r.gc) || 0,
222
+ ...r.block != null ? { block: String(r.block) } : {}
223
+ }));
224
+ }
200
225
  default:
201
226
  return [];
202
227
  }
@@ -342,10 +367,73 @@ var aggregate = (records, column, fn) => {
342
367
  return Math.min(...values.map(Number));
343
368
  case "max":
344
369
  return Math.max(...values.map(Number));
370
+ case "median": {
371
+ const nums = values.map(Number).filter((v) => !Number.isNaN(v)).sort((a, b) => a - b);
372
+ if (!nums.length) return 0;
373
+ const mid = Math.floor(nums.length / 2);
374
+ return nums.length % 2 ? nums[mid] : (nums[mid - 1] + nums[mid]) / 2;
375
+ }
345
376
  default:
346
377
  return records.length;
347
378
  }
348
379
  };
380
+ var HEAT_STATS = ["count", "sum", "avg", "min", "max", "median"];
381
+ var cellKey = (row, col) => `${row}\0${col}`;
382
+ var normalizePartition = (cells, method, vmin, vmax) => {
383
+ const out = /* @__PURE__ */ new Map();
384
+ const values = cells.map((c) => c.colorValue).filter((v) => Number.isFinite(v));
385
+ if (!values.length) return out;
386
+ if (method === "rank") {
387
+ const sorted = [...values].sort((a, b) => a - b);
388
+ for (const c of cells) {
389
+ const below = sorted.findIndex((v) => v >= c.colorValue);
390
+ const ties = sorted.filter((v) => v === c.colorValue).length;
391
+ out.set(cellKey(c.row, c.col), (below + 0.5 * ties) / sorted.length);
392
+ }
393
+ return out;
394
+ }
395
+ const lo = vmin ?? Math.min(...values);
396
+ const hi = vmax ?? Math.max(...values);
397
+ for (const c of cells) {
398
+ const t = hi === lo ? 0.5 : (c.colorValue - lo) / (hi - lo);
399
+ out.set(cellKey(c.row, c.col), Math.max(0, Math.min(1, t)));
400
+ }
401
+ return out;
402
+ };
403
+ var normalizeCells = (cells, { scope = "global", method = "linear", vmin, vmax } = {}) => {
404
+ const partitions = /* @__PURE__ */ new Map();
405
+ const partOf = (c) => {
406
+ switch (scope) {
407
+ case "row":
408
+ return `r:${c.row}`;
409
+ case "column":
410
+ return `c:${c.col}`;
411
+ case "block":
412
+ return c.block != null ? `b:${c.block}` : "global";
413
+ default:
414
+ return "global";
415
+ }
416
+ };
417
+ for (const c of cells) {
418
+ const k = partOf(c);
419
+ const part = partitions.get(k);
420
+ if (part) part.push(c);
421
+ else partitions.set(k, [c]);
422
+ }
423
+ const out = /* @__PURE__ */ new Map();
424
+ for (const part of partitions.values()) {
425
+ for (const [k, t] of normalizePartition(part, method, vmin, vmax)) out.set(k, t);
426
+ }
427
+ return out;
428
+ };
429
+ var heatColor = (t) => interpolateRdYlGn(Math.max(0, Math.min(1, t)));
430
+ var heatLabelColor = (t) => {
431
+ const m = heatColor(t).match(/\d+/g);
432
+ if (!m) return "#0f172a";
433
+ const [r, g, b] = m.map(Number);
434
+ const lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
435
+ return lum > 140 ? "#0f172a" : "#f8fafc";
436
+ };
349
437
  function MetricView({ records, config, value: presetValue }) {
350
438
  const value = useMemo(() => {
351
439
  if (presetValue != null) return presetValue;
@@ -661,6 +749,159 @@ function HeatmapView({
661
749
  }
662
750
  ) });
663
751
  }
752
+ var MAX_AXIS2 = 30;
753
+ var TOTAL_ID = "\u03A3";
754
+ var NULL = "(null)";
755
+ var fmtNum2 = (n) => {
756
+ if (typeof n !== "number" || Number.isNaN(n)) return "";
757
+ return Number.isInteger(n) ? n.toLocaleString() : n.toLocaleString(void 0, { maximumFractionDigits: 2 });
758
+ };
759
+ var orderIds2 = (ids, margin, order) => {
760
+ const sorted = [...ids];
761
+ if (order.endsWith("-desc")) sorted.sort((a, b) => (margin[b]?.lv ?? 0) - (margin[a]?.lv ?? 0));
762
+ else if (order.endsWith("-asc")) sorted.sort((a, b) => (margin[a]?.lv ?? 0) - (margin[b]?.lv ?? 0));
763
+ else sorted.sort();
764
+ return sorted;
765
+ };
766
+ function fromRecords(records, rowCol, colCol, valueCol, labelStat, colorStat, blockField) {
767
+ const col = valueCol || rowCol;
768
+ const statOf = (rs, s) => valueCol ? aggregate(rs, col, s) : rs.length;
769
+ const both = (rs) => ({ lv: statOf(rs, labelStat), cv: statOf(rs, colorStat) });
770
+ const byRow = groupBy(records, rowCol);
771
+ const rowIds = Object.keys(byRow);
772
+ const colIds = [...new Set(records.map((r) => String(r[colCol] ?? NULL)))];
773
+ const cells = {};
774
+ for (const rid of rowIds) {
775
+ cells[rid] = {};
776
+ const byCol2 = groupBy(byRow[rid], colCol);
777
+ for (const cid of colIds) {
778
+ const cellRows = byCol2[cid] || [];
779
+ const stat = both(cellRows);
780
+ if (blockField && cellRows.length) stat.block = String(cellRows[0][blockField] ?? NULL);
781
+ cells[rid][cid] = stat;
782
+ }
783
+ }
784
+ const rowMargin = {};
785
+ for (const rid of rowIds) rowMargin[rid] = both(byRow[rid]);
786
+ const colMargin = {};
787
+ const byCol = groupBy(records, colCol);
788
+ for (const cid of colIds) colMargin[cid] = both(byCol[cid] || []);
789
+ return { rowIds, colIds, cells, rowMargin, colMargin, grand: both(records) };
790
+ }
791
+ function fromData(data) {
792
+ const cells = {};
793
+ const rowMargin = {};
794
+ const colMargin = {};
795
+ let grand = { lv: 0, cv: 0 };
796
+ const rowIds = [];
797
+ const colIds = [];
798
+ for (const d of data) {
799
+ const stat = { lv: d.lv, cv: d.cv, ...d.block != null ? { block: d.block } : {} };
800
+ if (d.gr && d.gc) {
801
+ grand = stat;
802
+ continue;
803
+ }
804
+ if (d.gr) {
805
+ if (d.c != null) colMargin[d.c] = stat;
806
+ continue;
807
+ }
808
+ if (d.gc) {
809
+ if (d.r != null) rowMargin[d.r] = stat;
810
+ continue;
811
+ }
812
+ const r = d.r ?? NULL, c = d.c ?? NULL;
813
+ if (!cells[r]) {
814
+ cells[r] = {};
815
+ rowIds.push(r);
816
+ }
817
+ if (!colIds.includes(c)) colIds.push(c);
818
+ cells[r][c] = stat;
819
+ }
820
+ return { rowIds, colIds, cells, rowMargin, colMargin, grand };
821
+ }
822
+ function HeatmapPlusView({
823
+ records,
824
+ rowColumn,
825
+ colColumn,
826
+ valueColumn,
827
+ labelStat = "count",
828
+ colorStat = "count",
829
+ blockField,
830
+ data: presetData,
831
+ scope = "global",
832
+ method = "linear",
833
+ vmin,
834
+ vmax,
835
+ rowOrder = "alpha",
836
+ colOrder = "alpha",
837
+ showMargins = false,
838
+ style
839
+ }) {
840
+ const s = withStyleDefaults(style);
841
+ const model = useMemo(() => {
842
+ if (presetData) return fromData(presetData);
843
+ if (records) return fromRecords(records, rowColumn, colColumn, valueColumn, labelStat, colorStat, blockField);
844
+ return null;
845
+ }, [presetData, records, rowColumn, colColumn, valueColumn, labelStat, colorStat, blockField]);
846
+ const built = useMemo(() => {
847
+ if (!model || !model.rowIds.length || !model.colIds.length) return null;
848
+ const { cells, rowMargin, colMargin, grand } = model;
849
+ const rows = orderIds2(model.rowIds, rowMargin, rowOrder).slice(0, MAX_AXIS2);
850
+ const cols = orderIds2(model.colIds, colMargin, colOrder).slice(0, MAX_AXIS2);
851
+ const bodyCells = [];
852
+ for (const rid of rows) for (const cid of cols) {
853
+ bodyCells.push({ row: rid, col: cid, colorValue: cells[rid]?.[cid]?.cv ?? NaN, block: cells[rid]?.[cid]?.block });
854
+ }
855
+ const t2 = new Map(normalizeCells(bodyCells, { scope, method, vmin, vmax }));
856
+ if (showMargins) {
857
+ const rowMc = rows.map((rid) => ({ row: rid, col: TOTAL_ID, colorValue: rowMargin[rid]?.cv ?? NaN }));
858
+ const colMc = cols.map((cid) => ({ row: TOTAL_ID, col: cid, colorValue: colMargin[cid]?.cv ?? NaN }));
859
+ for (const [k, v] of normalizeCells(rowMc, { scope: "global", method })) t2.set(k, v);
860
+ for (const [k, v] of normalizeCells(colMc, { scope: "global", method })) t2.set(k, v);
861
+ t2.set(cellKey(TOTAL_ID, TOTAL_ID), 0.5);
862
+ }
863
+ const colKeys = showMargins ? [...cols, TOTAL_ID] : cols;
864
+ const nivoData2 = rows.map((rid) => ({
865
+ id: rid,
866
+ data: colKeys.map((cid) => ({
867
+ x: cid,
868
+ y: cid === TOTAL_ID ? rowMargin[rid]?.lv ?? 0 : cells[rid]?.[cid]?.lv ?? 0
869
+ }))
870
+ }));
871
+ if (showMargins) {
872
+ nivoData2.push({
873
+ id: TOTAL_ID,
874
+ data: colKeys.map((cid) => ({
875
+ x: cid,
876
+ y: cid === TOTAL_ID ? grand.lv : colMargin[cid]?.lv ?? 0
877
+ }))
878
+ });
879
+ }
880
+ return { nivoData: nivoData2, t: t2 };
881
+ }, [model, rowOrder, colOrder, scope, method, vmin, vmax, showMargins]);
882
+ if (!built) {
883
+ return /* @__PURE__ */ jsx("div", { className: "p-8 text-center text-midnight-text-muted", children: "Not enough distinct values for a heatmap" });
884
+ }
885
+ const { nivoData, t } = built;
886
+ const tAt = (serieId, x) => t.get(cellKey(serieId, x)) ?? 0;
887
+ return /* @__PURE__ */ jsx("div", { className: "h-full w-full min-h-[160px]", children: /* @__PURE__ */ jsx(
888
+ ResponsiveHeatMap,
889
+ {
890
+ data: nivoData,
891
+ margin: { top: 60, right: 20, bottom: showMargins ? 60 : 20, left: 100 },
892
+ valueFormat: ((v) => fmtNum2(v)),
893
+ axisTop: { tickSize: 5, tickPadding: 5, tickRotation: s.xTickRotation, legend: s.showXLegend ? s.xAxisLabel || colColumn : "", legendPosition: s.xLegendPosition, legendOffset: -50 },
894
+ axisLeft: { tickSize: 5, tickPadding: 5, legend: s.showYLegend ? s.yAxisLabel || rowColumn : "", legendPosition: s.yLegendPosition, legendOffset: -80 },
895
+ colors: ((cell) => heatColor(tAt(cell.serieId, cell.data.x))),
896
+ emptyColor: "#1e293b",
897
+ borderWidth: 1,
898
+ borderColor: "#334155",
899
+ labelTextColor: ((cell) => heatLabelColor(tAt(cell.serieId, cell.data.x))),
900
+ hoverTarget: "cell",
901
+ theme: buildNivoTheme(style)
902
+ }
903
+ ) });
904
+ }
664
905
  function PivotView({ records }) {
665
906
  const [pivotState, setPivotState] = useState({});
666
907
  if (!records?.length) {
@@ -1197,6 +1438,7 @@ var CHART_TYPES = [
1197
1438
  { id: "line", icon: TrendingUp, label: "Line" },
1198
1439
  { id: "scatter", icon: ScatterChart, label: "Scatter" },
1199
1440
  { id: "heatmap", icon: LayoutGrid, label: "Heatmap" },
1441
+ { id: "heatmap-plus", icon: Grid3x3, label: "Heatmap+" },
1200
1442
  { id: "grouped-bar", icon: BarChart3, label: "Grouped Bar" }
1201
1443
  ];
1202
1444
  var AGG_OPTIONS = ["count", "distinct", "sum", "avg", "min", "max"];
@@ -1217,6 +1459,17 @@ var ORDER_OPTIONS = [
1217
1459
  { id: "total-asc", label: "Total \u2191" }
1218
1460
  ];
1219
1461
  var MARGIN_AGGS = ["sum", "avg", "min", "max", "count"].map((a) => ({ id: a, label: a }));
1462
+ var HEAT_STAT_OPTIONS = HEAT_STATS.map((a) => ({ id: a, label: a }));
1463
+ var SCOPE_OPTIONS = [
1464
+ { id: "global", label: "Global" },
1465
+ { id: "row", label: "Per row" },
1466
+ { id: "column", label: "Per column" },
1467
+ { id: "block", label: "Per block" }
1468
+ ];
1469
+ var METHOD_OPTIONS = [
1470
+ { id: "linear", label: "Linear" },
1471
+ { id: "rank", label: "Rank" }
1472
+ ];
1220
1473
  var FILTER_OP_OPTIONS = FILTER_OPS.map((o) => ({ id: o, label: o }));
1221
1474
  var FieldPill = ({ name, type, onRemove, onChangeAgg, agg, showAgg }) => {
1222
1475
  const typeColor = {
@@ -1405,6 +1658,7 @@ var ChartPreview = ({
1405
1658
  sqlLoading,
1406
1659
  sqlError,
1407
1660
  heatmapOpts = {},
1661
+ heatPlusOpts = {},
1408
1662
  style
1409
1663
  }) => {
1410
1664
  const xCol = xFields[0]?.name;
@@ -1443,6 +1697,8 @@ var ChartPreview = ({
1443
1697
  return /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(ScatterView, { data: shaped, xColumn: xCol, yColumn: yCol, style }) });
1444
1698
  case "heatmap":
1445
1699
  return shaped.length ? /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(HeatmapView, { data: shaped, rowColumn: xCol, colColumn: yCol, ...heatmapOpts, style }) }) : /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center h-full text-sm text-midnight-text-muted", children: "No data" });
1700
+ case "heatmap-plus":
1701
+ return shaped.length ? /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(HeatmapPlusView, { data: shaped, rowColumn: xCol, colColumn: yCol, ...heatPlusOpts, style }) }) : /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center h-full text-sm text-midnight-text-muted", children: "No data" });
1446
1702
  default:
1447
1703
  return /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center h-full text-sm text-midnight-text-muted", children: "Select a chart type" });
1448
1704
  }
@@ -1464,6 +1720,8 @@ var ChartPreview = ({
1464
1720
  return xCol && yCol ? /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(ScatterView, { records, xColumn: xCol, yColumn: yCol, style }) }) : null;
1465
1721
  case "heatmap":
1466
1722
  return xCol && yCol ? /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(HeatmapView, { records, rowColumn: xCol, colColumn: yCol, valueColumn: valueField?.name, aggFn: valueField?.agg || "count", ...heatmapOpts, style }) }) : null;
1723
+ case "heatmap-plus":
1724
+ return xCol && yCol ? /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(HeatmapPlusView, { records, rowColumn: xCol, colColumn: yCol, valueColumn: valueField?.name, ...heatPlusOpts, style }) }) : null;
1467
1725
  default:
1468
1726
  return /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center h-full text-sm text-midnight-text-muted", children: "Select a chart type" });
1469
1727
  }
@@ -1487,6 +1745,13 @@ function ChartBuilder({ records, columns, stateId, onSave }) {
1487
1745
  const [colOrder, setColOrder] = useState("alpha");
1488
1746
  const [marginAgg, setMarginAgg] = useState("sum");
1489
1747
  const [showTotals, setShowTotals] = useState(false);
1748
+ const [labelStat, setLabelStat] = useState("count");
1749
+ const [colorStat, setColorStat] = useState("count");
1750
+ const [scope, setScope] = useState("global");
1751
+ const [method, setMethod] = useState("linear");
1752
+ const [vmin, setVmin] = useState("");
1753
+ const [vmax, setVmax] = useState("");
1754
+ const [showMargins, setShowMargins] = useState(false);
1490
1755
  const [style, setStyle] = useState(DEFAULT_CHART_STYLE);
1491
1756
  const [showStyle, setShowStyle] = useState(false);
1492
1757
  const [showFields, setShowFields] = useState(false);
@@ -1495,8 +1760,17 @@ function ChartBuilder({ records, columns, stateId, onSave }) {
1495
1760
  const [sqlLoading, setSqlLoading] = useState(false);
1496
1761
  const [sqlError, setSqlError] = useState(null);
1497
1762
  const generatedSql = useMemo(
1498
- () => buildChartSQL({ chartType, xFields, yFields, valueField, filters }),
1499
- [chartType, xFields, yFields, valueField, filters]
1763
+ () => buildChartSQL({
1764
+ chartType,
1765
+ xFields,
1766
+ yFields,
1767
+ valueField,
1768
+ filters,
1769
+ labelStat,
1770
+ colorStat,
1771
+ blockField: colorField?.name
1772
+ }),
1773
+ [chartType, xFields, yFields, valueField, filters, labelStat, colorStat, colorField]
1500
1774
  );
1501
1775
  useEffect(() => {
1502
1776
  if (engineMode !== "sql") return void 0;
@@ -1594,6 +1868,26 @@ function ChartBuilder({ records, columns, stateId, onSave }) {
1594
1868
  config.marginAgg = marginAgg;
1595
1869
  config.showTotals = showTotals;
1596
1870
  }
1871
+ if (chartType === "heatmap-plus") {
1872
+ config.row = xFields[0]?.name;
1873
+ config.col = yFields[0]?.name;
1874
+ if (valueField) config.value = valueField.name;
1875
+ else delete config.value;
1876
+ config.labelStat = labelStat;
1877
+ config.colorStat = colorStat;
1878
+ if (colorField) config.blockField = colorField.name;
1879
+ else delete config.blockField;
1880
+ config.scope = scope;
1881
+ config.method = method;
1882
+ const nMin = parseFloat(vmin), nMax = parseFloat(vmax);
1883
+ if (Number.isFinite(nMin)) config.vmin = nMin;
1884
+ else delete config.vmin;
1885
+ if (Number.isFinite(nMax)) config.vmax = nMax;
1886
+ else delete config.vmax;
1887
+ config.rowOrder = rowOrder;
1888
+ config.colOrder = colOrder;
1889
+ config.showMargins = showMargins;
1890
+ }
1597
1891
  if (engineMode === "sql" && generatedSql) {
1598
1892
  config.sql = generatedSql;
1599
1893
  config.chartType = chartType;
@@ -1614,6 +1908,7 @@ function ChartBuilder({ records, columns, stateId, onSave }) {
1614
1908
  height: fillContainer ? 8 : 2
1615
1909
  });
1616
1910
  };
1911
+ const isHeat = chartType === "heatmap" || chartType === "heatmap-plus";
1617
1912
  return /* @__PURE__ */ jsxs("div", { className: "flex h-full", children: [
1618
1913
  /* @__PURE__ */ jsxs("div", { className: `w-[280px] shrink-0 border-r ${theme.border} bg-midnight-elevated overflow-y-auto p-3 space-y-4`, children: [
1619
1914
  /* @__PURE__ */ jsxs("div", { children: [
@@ -1649,30 +1944,30 @@ function ChartBuilder({ records, columns, stateId, onSave }) {
1649
1944
  /* @__PURE__ */ jsx(
1650
1945
  DropZone,
1651
1946
  {
1652
- label: chartType === "heatmap" ? "Rows" : "X Axis / Group By",
1947
+ label: isHeat ? "Rows" : "X Axis / Group By",
1653
1948
  fields: xFields,
1654
1949
  columns,
1655
1950
  onAdd: (f) => addField("x", f),
1656
1951
  onRemove: (i) => removeField("x", i),
1657
1952
  onChangeAgg: () => {
1658
1953
  },
1659
- maxFields: chartType === "pie" || chartType === "heatmap" ? 1 : 3
1954
+ maxFields: chartType === "pie" || isHeat ? 1 : 3
1660
1955
  }
1661
1956
  ),
1662
1957
  /* @__PURE__ */ jsx(
1663
1958
  DropZone,
1664
1959
  {
1665
- label: chartType === "heatmap" ? "Columns" : "Y Axis / Values",
1960
+ label: isHeat ? "Columns" : "Y Axis / Values",
1666
1961
  fields: yFields,
1667
1962
  columns,
1668
1963
  onAdd: (f) => addField("y", f),
1669
1964
  onRemove: (i) => removeField("y", i),
1670
1965
  onChangeAgg: (i, agg) => changeAgg("y", i, agg),
1671
- showAgg: chartType !== "heatmap",
1672
- maxFields: chartType === "heatmap" ? 1 : 5
1966
+ showAgg: !isHeat,
1967
+ maxFields: isHeat ? 1 : 5
1673
1968
  }
1674
1969
  ),
1675
- chartType === "heatmap" && /* @__PURE__ */ jsx(
1970
+ isHeat && /* @__PURE__ */ jsx(
1676
1971
  DropZone,
1677
1972
  {
1678
1973
  label: "Cell Value (optional \u2014 defaults to count)",
@@ -1681,10 +1976,93 @@ function ChartBuilder({ records, columns, stateId, onSave }) {
1681
1976
  onAdd: (f) => addField("value", f),
1682
1977
  onRemove: () => removeField("value"),
1683
1978
  onChangeAgg: (i, agg) => changeAgg("value", i, agg),
1684
- showAgg: true,
1979
+ showAgg: chartType === "heatmap",
1685
1980
  maxFields: 1
1686
1981
  }
1687
1982
  ),
1983
+ chartType === "heatmap-plus" && /* @__PURE__ */ jsxs(Fragment, { children: [
1984
+ /* @__PURE__ */ jsx(
1985
+ DropZone,
1986
+ {
1987
+ label: "Block (optional \u2014 color-scope grouping)",
1988
+ fields: colorField ? [colorField] : [],
1989
+ columns,
1990
+ onAdd: (f) => addField("color", f),
1991
+ onRemove: () => removeField("color"),
1992
+ onChangeAgg: () => {
1993
+ },
1994
+ maxFields: 1
1995
+ }
1996
+ ),
1997
+ /* @__PURE__ */ jsxs("div", { children: [
1998
+ /* @__PURE__ */ jsx("div", { className: "text-xs uppercase text-midnight-text-muted mb-2 font-mono", children: "Cell Stats" }),
1999
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
2000
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 min-h-[28px]", children: [
2001
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-mono text-midnight-text-muted", children: "Label stat" }),
2002
+ /* @__PURE__ */ jsx(SelectControl, { value: labelStat, options: HEAT_STAT_OPTIONS, onChange: (v) => setLabelStat(v) })
2003
+ ] }),
2004
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 min-h-[28px]", children: [
2005
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-mono text-midnight-text-muted", children: "Color stat" }),
2006
+ /* @__PURE__ */ jsx(SelectControl, { value: colorStat, options: HEAT_STAT_OPTIONS, onChange: (v) => setColorStat(v) })
2007
+ ] })
2008
+ ] })
2009
+ ] }),
2010
+ /* @__PURE__ */ jsxs("div", { children: [
2011
+ /* @__PURE__ */ jsx("div", { className: "text-xs uppercase text-midnight-text-muted mb-2 font-mono", children: "Color Engine" }),
2012
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
2013
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 min-h-[28px]", children: [
2014
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-mono text-midnight-text-muted", children: "Scale scope" }),
2015
+ /* @__PURE__ */ jsx(SelectControl, { value: scope, options: SCOPE_OPTIONS, onChange: (v) => setScope(v) })
2016
+ ] }),
2017
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 min-h-[28px]", children: [
2018
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-mono text-midnight-text-muted", children: "Scale method" }),
2019
+ /* @__PURE__ */ jsx(SelectControl, { value: method, options: METHOD_OPTIONS, onChange: (v) => setMethod(v) })
2020
+ ] }),
2021
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 min-h-[28px]", children: [
2022
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-mono text-midnight-text-muted", children: "Fixed min / max" }),
2023
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-1", children: [
2024
+ /* @__PURE__ */ jsx(
2025
+ "input",
2026
+ {
2027
+ value: vmin,
2028
+ onChange: (e) => setVmin(e.target.value),
2029
+ placeholder: "auto",
2030
+ inputMode: "decimal",
2031
+ 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"
2032
+ }
2033
+ ),
2034
+ /* @__PURE__ */ jsx(
2035
+ "input",
2036
+ {
2037
+ value: vmax,
2038
+ onChange: (e) => setVmax(e.target.value),
2039
+ placeholder: "auto",
2040
+ inputMode: "decimal",
2041
+ 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"
2042
+ }
2043
+ )
2044
+ ] })
2045
+ ] })
2046
+ ] })
2047
+ ] }),
2048
+ /* @__PURE__ */ jsxs("div", { children: [
2049
+ /* @__PURE__ */ jsx("div", { className: "text-xs uppercase text-midnight-text-muted mb-2 font-mono", children: "Order & Margins" }),
2050
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
2051
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 min-h-[28px]", children: [
2052
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-mono text-midnight-text-muted", children: "Order rows" }),
2053
+ /* @__PURE__ */ jsx(SelectControl, { value: rowOrder, options: ORDER_OPTIONS, onChange: (v) => setRowOrder(v) })
2054
+ ] }),
2055
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 min-h-[28px]", children: [
2056
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-mono text-midnight-text-muted", children: "Order columns" }),
2057
+ /* @__PURE__ */ jsx(SelectControl, { value: colOrder, options: ORDER_OPTIONS, onChange: (v) => setColOrder(v) })
2058
+ ] }),
2059
+ /* @__PURE__ */ jsxs("label", { className: "flex items-center gap-2 cursor-pointer", children: [
2060
+ /* @__PURE__ */ jsx("input", { type: "checkbox", checked: showMargins, onChange: (e) => setShowMargins(e.target.checked), className: "accent-midnight-accent" }),
2061
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-mono text-midnight-text-muted", children: "Show margins (row / col / grand)" })
2062
+ ] })
2063
+ ] })
2064
+ ] })
2065
+ ] }),
1688
2066
  chartType === "heatmap" && /* @__PURE__ */ jsxs("div", { children: [
1689
2067
  /* @__PURE__ */ jsx("div", { className: "text-xs uppercase text-midnight-text-muted mb-2 font-mono", children: "Order & Totals" }),
1690
2068
  /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
@@ -1818,6 +2196,18 @@ function ChartBuilder({ records, columns, stateId, onSave }) {
1818
2196
  sqlLoading,
1819
2197
  sqlError,
1820
2198
  heatmapOpts: { rowOrder, colOrder, marginAgg, showTotals },
2199
+ heatPlusOpts: {
2200
+ labelStat,
2201
+ colorStat,
2202
+ blockField: colorField?.name,
2203
+ scope,
2204
+ method,
2205
+ vmin: Number.isFinite(parseFloat(vmin)) ? parseFloat(vmin) : void 0,
2206
+ vmax: Number.isFinite(parseFloat(vmax)) ? parseFloat(vmax) : void 0,
2207
+ rowOrder,
2208
+ colOrder,
2209
+ showMargins
2210
+ },
1821
2211
  style
1822
2212
  }
1823
2213
  ) })
@@ -1826,7 +2216,7 @@ function ChartBuilder({ records, columns, stateId, onSave }) {
1826
2216
  function ViewLoading() {
1827
2217
  return /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center h-full text-xs text-midnight-text-muted", children: "Loading..." });
1828
2218
  }
1829
- var SQL_CHART_TYPES = /* @__PURE__ */ new Set(["bar", "grouped-bar", "pie", "line", "scatter", "heatmap"]);
2219
+ var SQL_CHART_TYPES = /* @__PURE__ */ new Set(["bar", "grouped-bar", "pie", "line", "scatter", "heatmap", "heatmap-plus"]);
1830
2220
  var SQL_DATA_TYPES = /* @__PURE__ */ new Set(["table", "metric"]);
1831
2221
  var TABLE_PREVIEW_ROWS = 200;
1832
2222
  function tableSql(config) {
@@ -1857,6 +2247,14 @@ function panelToChartConfig(chartType, config) {
1857
2247
  if (config.col) cfg.yFields = [{ name: config.col }];
1858
2248
  if (config.value) cfg.valueField = { name: config.value, agg: config.agg };
1859
2249
  break;
2250
+ case "heatmap-plus":
2251
+ if (config.row) cfg.xFields = [{ name: config.row }];
2252
+ if (config.col) cfg.yFields = [{ name: config.col }];
2253
+ if (config.value) cfg.valueField = { name: config.value, agg: config.labelStat };
2254
+ cfg.labelStat = config.labelStat;
2255
+ cfg.colorStat = config.colorStat;
2256
+ cfg.blockField = config.blockField;
2257
+ break;
1860
2258
  }
1861
2259
  return cfg;
1862
2260
  }
@@ -2005,6 +2403,9 @@ function SqlPanel({ panel }) {
2005
2403
  case "heatmap":
2006
2404
  chart = shaped.length ? /* @__PURE__ */ 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__ */ jsx("div", { className: "flex items-center justify-center h-full text-xs text-midnight-text-muted", children: "No data" });
2007
2405
  break;
2406
+ case "heatmap-plus":
2407
+ chart = shaped.length ? /* @__PURE__ */ 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__ */ jsx("div", { className: "flex items-center justify-center h-full text-xs text-midnight-text-muted", children: "No data" });
2408
+ break;
2008
2409
  default:
2009
2410
  chart = /* @__PURE__ */ jsxs("div", { className: "p-4 text-xs text-midnight-text-muted", children: [
2010
2411
  "Unsupported SQL chart: ",
@@ -2038,6 +2439,8 @@ function PanelContent({ panel, records, columns }) {
2038
2439
  return config.x && config.y ? /* @__PURE__ */ jsx(ScatterView, { records, xColumn: config.x, yColumn: config.y, style: config.style }) : null;
2039
2440
  case "heatmap":
2040
2441
  return config.row && config.col ? /* @__PURE__ */ 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;
2442
+ case "heatmap-plus":
2443
+ return config.row && config.col ? /* @__PURE__ */ 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;
2041
2444
  case "pivot":
2042
2445
  return /* @__PURE__ */ jsx(PivotView, { records });
2043
2446
  case "metric":
@@ -2497,6 +2900,6 @@ function DataExplorer({
2497
2900
  ] });
2498
2901
  }
2499
2902
 
2500
- export { BarView, ChartBuilder, ChartStyleControls, DEFAULT_CHART_STYLE, DashboardProvider, DashboardRenderer, DataExplorer, HeatmapView, InsightView, LEGEND_ANCHORS, LineView, MetricView, PanelChart, PieView, PivotView, ScatterView, SqlConsole, aggExpr, aggregate, axisLegend, buildChartSQL, buildNivoTheme, compileWhere, groupBy, legendConfig, qIdent, qLit, shapeChartData, useCapabilities, useDashboard, withStyleDefaults };
2903
+ export { BarView, ChartBuilder, ChartStyleControls, DEFAULT_CHART_STYLE, DashboardProvider, DashboardRenderer, DataExplorer, HEAT_STATS, HeatmapPlusView, HeatmapView, InsightView, LEGEND_ANCHORS, LineView, MetricView, PanelChart, PieView, PivotView, ScatterView, SqlConsole, aggExpr, aggregate, axisLegend, buildChartSQL, buildNivoTheme, compileWhere, groupBy, heatColor, heatLabelColor, legendConfig, normalizeCells, qIdent, qLit, shapeChartData, useCapabilities, useDashboard, withStyleDefaults };
2501
2904
  //# sourceMappingURL=index.js.map
2502
2905
  //# sourceMappingURL=index.js.map