graphein 0.6.0 → 0.7.0

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
@@ -8739,6 +8739,10 @@ function buildSparklineCanvas(tokens, width, height, values, sketch) {
8739
8739
  }
8740
8740
  function drawKpi(surface, spec, tokens, size, context) {
8741
8741
  const kpi = spec;
8742
+ if (surface.headless) {
8743
+ drawKpiCanvas(surface, kpi, tokens, size, context);
8744
+ return;
8745
+ }
8742
8746
  const data = kpi.data ?? [];
8743
8747
  const value = resolveValue(kpi.value, data);
8744
8748
  const delta = resolveValue(kpi.delta, data);
@@ -8807,6 +8811,116 @@ function drawKpi(surface, spec, tokens, size, context) {
8807
8811
  }
8808
8812
  }
8809
8813
  }
8814
+ function drawKpiCanvas(surface, kpi, tokens, size, context) {
8815
+ const ctx = surface.marks.ctx;
8816
+ const data = kpi.data ?? [];
8817
+ const value = resolveValue(kpi.value, data);
8818
+ const delta = resolveValue(kpi.delta, data);
8819
+ const sparkField = sparklineField(kpi);
8820
+ const sparkValues = sparkField ? sparklineValues(data, sparkField) : [];
8821
+ const hasSparkline = sparkValues.length > 0;
8822
+ const framed = context?.framed === true;
8823
+ const rect = cardRectFor(surface, tokens, size, kpi);
8824
+ const pad3 = tokens.spacing.lg;
8825
+ const sparkReserve = hasSparkline ? Math.min(44, Math.max(28, rect.height * 0.18)) + tokens.spacing.sm : 0;
8826
+ if (!framed) {
8827
+ ctx.save();
8828
+ ctx.beginPath();
8829
+ roundedRect(ctx, rect.x, rect.y, rect.width, rect.height, tokens.radius.lg);
8830
+ ctx.fillStyle = tokens.color.surface;
8831
+ ctx.fill();
8832
+ ctx.lineWidth = 1;
8833
+ ctx.strokeStyle = tokens.color.border;
8834
+ ctx.stroke();
8835
+ ctx.restore();
8836
+ }
8837
+ const left = rect.x + (framed ? 0 : pad3);
8838
+ const contentWidth = Math.max(1, rect.width - (framed ? 0 : pad3 * 2));
8839
+ const valueText = value == null ? "\u2014" : formatValue(value, kpi.format);
8840
+ const sizeByCard = Math.min(rect.width, rect.height) * 0.22;
8841
+ const sizeByText = contentWidth / Math.max(1, valueText.length * 0.58);
8842
+ const valueSize = clamp5(Math.min(sizeByCard, sizeByText), 28, 64);
8843
+ const labelH = kpi.label ? tokens.font.size.base * 1.25 : 0;
8844
+ const deltaH = delta != null ? tokens.font.size.large : 0;
8845
+ const stackH = labelH + valueSize + deltaH + tokens.spacing.sm * 2;
8846
+ const avail = rect.height - sparkReserve;
8847
+ let y = rect.y + Math.max(framed ? 0 : pad3, (avail - stackH) / 2);
8848
+ if (kpi.label) {
8849
+ paintCanvasText(ctx, {
8850
+ x: left,
8851
+ y,
8852
+ text: kpi.label.toUpperCase(),
8853
+ font: fontString(tokens.font.size.base, tokens.font.family, tokens.font.weight.medium),
8854
+ color: tokens.color.textMuted,
8855
+ size: tokens.font.size.base,
8856
+ baseline: "top"
8857
+ });
8858
+ y += labelH + tokens.spacing.sm;
8859
+ }
8860
+ paintCanvasText(ctx, {
8861
+ x: left,
8862
+ y,
8863
+ text: valueText,
8864
+ font: fontString(valueSize, tokens.font.family, tokens.font.weight.bold),
8865
+ color: tokens.color.text,
8866
+ size: valueSize,
8867
+ baseline: "top"
8868
+ });
8869
+ y += valueSize + tokens.spacing.sm;
8870
+ if (delta != null) {
8871
+ const positive = delta >= 0;
8872
+ paintCanvasText(ctx, {
8873
+ x: left,
8874
+ y,
8875
+ text: `${positive ? "\u25B2" : "\u25BC"} ${formatDelta(delta, kpi.format)}`,
8876
+ font: fontString(tokens.font.size.large, tokens.font.family, tokens.font.weight.medium),
8877
+ color: positive ? tokens.color.positive : tokens.color.negative,
8878
+ size: tokens.font.size.large,
8879
+ baseline: "top"
8880
+ });
8881
+ }
8882
+ if (hasSparkline) {
8883
+ const sparkH = clamp5(rect.height * 0.18, 24, 40);
8884
+ const inset = framed ? 0 : pad3;
8885
+ const sparkW = Math.max(0, rect.width - inset * 2);
8886
+ const x0 = rect.x + inset;
8887
+ const y1 = rect.y + rect.height - inset;
8888
+ paintSparklineCanvas(ctx, tokens, x0, y1 - sparkH, sparkW, sparkH, sparkValues);
8889
+ }
8890
+ }
8891
+ function paintSparklineCanvas(ctx, tokens, x0, y0, width, height, values) {
8892
+ if (values.length === 0 || width <= 0 || height <= 0) return;
8893
+ const stroke = 1.75;
8894
+ const pad3 = stroke;
8895
+ const innerH = Math.max(1, height - pad3 * 2);
8896
+ const n = values.length;
8897
+ const min = Math.min(...values);
8898
+ const max = Math.max(...values);
8899
+ const span = max - min;
8900
+ const xAt = (i) => x0 + (n === 1 ? width / 2 : i / (n - 1) * width);
8901
+ const yAt = (v) => y0 + pad3 + (1 - (span === 0 ? 0.5 : (v - min) / span)) * innerH;
8902
+ ctx.save();
8903
+ if (n === 1) {
8904
+ ctx.beginPath();
8905
+ ctx.arc(xAt(0), yAt(values[0]), 2.25, 0, Math.PI * 2);
8906
+ ctx.fillStyle = tokens.color.accent;
8907
+ ctx.fill();
8908
+ } else {
8909
+ ctx.beginPath();
8910
+ for (let i = 0; i < n; i++) {
8911
+ const px = xAt(i);
8912
+ const py = yAt(values[i]);
8913
+ if (i === 0) ctx.moveTo(px, py);
8914
+ else ctx.lineTo(px, py);
8915
+ }
8916
+ ctx.strokeStyle = tokens.color.accent;
8917
+ ctx.lineWidth = stroke;
8918
+ ctx.lineJoin = "round";
8919
+ ctx.lineCap = "round";
8920
+ ctx.stroke();
8921
+ }
8922
+ ctx.restore();
8923
+ }
8810
8924
 
8811
8925
  // src/charts/sankey.ts
8812
8926
  var center = (n) => (n.y0 + n.y1) / 2;
@@ -9622,6 +9736,43 @@ function buildTable(opts) {
9622
9736
  if (footer) table.appendChild(footer);
9623
9737
  container.appendChild(table);
9624
9738
  }
9739
+ function paintTableCanvas(opts) {
9740
+ const { ctx, rect, tokens } = opts;
9741
+ const metrics = densityMetrics(opts.density);
9742
+ const available = Math.max(0, rect.width - 2);
9743
+ const widths = computeFittedWidths(opts.columns, available);
9744
+ opts.widths = widths;
9745
+ ctx.save();
9746
+ ctx.beginPath();
9747
+ roundedRect(ctx, rect.x, rect.y, Math.max(0, rect.width), Math.max(0, rect.height), tokens.radius.sm);
9748
+ ctx.fillStyle = tokens.color.background;
9749
+ ctx.fill();
9750
+ ctx.lineWidth = 1;
9751
+ ctx.strokeStyle = tokens.color.border;
9752
+ ctx.stroke();
9753
+ ctx.clip();
9754
+ const headerRows = normalizedHeaderRows(opts);
9755
+ const headerHeight = headerRows.length * metrics.headerHeight;
9756
+ const footerHeight = opts.footerRow ? metrics.rowHeight : 0;
9757
+ const bodyTop = rect.y + headerHeight;
9758
+ const bodyBottom = rect.y + rect.height - footerHeight;
9759
+ const bodyHeight = Math.max(0, bodyBottom - bodyTop);
9760
+ const visibleRows = Math.max(0, Math.min(opts.rowCount, Math.floor(bodyHeight / metrics.rowHeight)));
9761
+ const clipped = visibleRows < opts.rowCount;
9762
+ const cueReserve = clipped && opts.clippedCue !== false && visibleRows > 0 ? metrics.rowHeight : 0;
9763
+ const rowsToDraw = Math.max(0, visibleRows - (cueReserve > 0 ? 1 : 0));
9764
+ paintHeaderCanvas(opts, widths, headerRows, metrics);
9765
+ for (let rowIndex = 0; rowIndex < rowsToDraw; rowIndex += 1) {
9766
+ paintBodyRowCanvas(opts, widths, rowIndex, bodyTop + rowIndex * metrics.rowHeight, metrics);
9767
+ }
9768
+ if (cueReserve > 0) {
9769
+ paintClippedCueCanvas(opts, bodyTop + rowsToDraw * metrics.rowHeight, metrics, opts.rowCount - rowsToDraw);
9770
+ }
9771
+ if (opts.footerRow) {
9772
+ paintFooterCanvas(opts, widths, rect.y + rect.height - metrics.rowHeight, metrics);
9773
+ }
9774
+ ctx.restore();
9775
+ }
9625
9776
  var FILL_EPS = 0.5;
9626
9777
  var cachedScrollbarWidth = null;
9627
9778
  function scrollbarWidth() {
@@ -9969,6 +10120,263 @@ function densityMetrics(density) {
9969
10120
  if (density === "compact") return { rowHeight: 24, headerHeight: 28, padding: "3px 8px", paddingX: 8 };
9970
10121
  return { rowHeight: DEFAULT_BODY_ROW_HEIGHT, headerHeight: HEADER_ROW_HEIGHT, padding: "6px 10px", paddingX: 10 };
9971
10122
  }
10123
+ function normalizedHeaderRows(opts) {
10124
+ if (opts.headerRows) return opts.headerRows;
10125
+ return [
10126
+ opts.columns.map((column, colIndex) => ({
10127
+ title: column.title,
10128
+ align: column.align,
10129
+ colIndex
10130
+ }))
10131
+ ];
10132
+ }
10133
+ function paintHeaderCanvas(opts, widths, rows, metrics) {
10134
+ rows.forEach((row, rowIndex) => {
10135
+ let cursor = 0;
10136
+ for (const cell of row) {
10137
+ const start = cell.colIndex ?? cursor;
10138
+ const colSpan = Math.max(1, cell.colSpan ?? 1);
10139
+ const rowSpan = Math.max(1, cell.rowSpan ?? 1);
10140
+ const x = opts.rect.x + sumWidths(widths.slice(0, start));
10141
+ const y = opts.rect.y + rowIndex * metrics.headerHeight;
10142
+ const w = sumWidths(widths.slice(start, start + colSpan));
10143
+ const h = metrics.headerHeight * rowSpan;
10144
+ paintCellBox(opts.ctx, x, y, w, h, opts.tokens.color.surface, opts.tokens.color.border);
10145
+ paintCellText(opts, {
10146
+ x,
10147
+ y,
10148
+ width: w,
10149
+ height: h,
10150
+ text: cell.title,
10151
+ align: cell.align ?? "left",
10152
+ color: opts.tokens.color.text,
10153
+ weight: opts.tokens.font.weight.bold,
10154
+ paddingX: metrics.paddingX
10155
+ });
10156
+ cursor = start + colSpan;
10157
+ }
10158
+ });
10159
+ }
10160
+ function paintBodyRowCanvas(opts, widths, rowIndex, y, metrics) {
10161
+ const cls = opts.rowClass?.(rowIndex) ?? "normal";
10162
+ let x = opts.rect.x;
10163
+ for (let colIndex = 0; colIndex < opts.columns.length; colIndex += 1) {
10164
+ const column = opts.columns[colIndex];
10165
+ const width = widths[colIndex] ?? 0;
10166
+ const cell = opts.getCell(rowIndex, colIndex);
10167
+ const baseBg = canvasRowBackground(opts, rowIndex, cls);
10168
+ const conditional = resolveCanvasConditional(opts, column, colIndex, cell, baseBg);
10169
+ const bg = conditional.background ?? baseBg;
10170
+ paintCellBox(opts.ctx, x, y, width, metrics.rowHeight, bg, opts.tokens.color.border);
10171
+ if (conditional.bar) paintDataBar(opts, x, y, width, metrics.rowHeight, conditional.bar);
10172
+ const indent = opts.cellIndent?.(rowIndex, colIndex) ?? 0;
10173
+ const text = conditional.showValue === false ? "" : formatDisplayValue(cell.value, column);
10174
+ const color = negativeTextColor(cell.value, column, conditional.color ?? opts.tokens.color.text);
10175
+ const weight = conditional.weight ?? (cls === "normal" ? opts.tokens.font.weight.normal : opts.tokens.font.weight.bold);
10176
+ paintCellValueWithIcon(opts, {
10177
+ x,
10178
+ y,
10179
+ width,
10180
+ height: metrics.rowHeight,
10181
+ text,
10182
+ align: column.align,
10183
+ color,
10184
+ weight,
10185
+ paddingX: metrics.paddingX + indent,
10186
+ icon: conditional.icon,
10187
+ iconColor: conditional.iconColor,
10188
+ iconPosition: conditional.iconPosition
10189
+ });
10190
+ x += width;
10191
+ }
10192
+ }
10193
+ function paintFooterCanvas(opts, widths, y, metrics) {
10194
+ let x = opts.rect.x;
10195
+ for (let colIndex = 0; colIndex < opts.columns.length; colIndex += 1) {
10196
+ const column = opts.columns[colIndex];
10197
+ const width = widths[colIndex] ?? 0;
10198
+ const cell = opts.footerRow?.cells[colIndex] ?? { value: null};
10199
+ paintCellBox(opts.ctx, x, y, width, metrics.rowHeight, opts.tokens.color.surface, opts.tokens.color.border, true);
10200
+ paintCellText(opts, {
10201
+ x,
10202
+ y,
10203
+ width,
10204
+ height: metrics.rowHeight,
10205
+ text: cell.label ? String(cell.value ?? "") : formatDisplayValue(cell.value, column),
10206
+ align: column.align,
10207
+ color: negativeTextColor(cell.value, column, opts.tokens.color.text),
10208
+ weight: opts.tokens.font.weight.bold,
10209
+ paddingX: metrics.paddingX
10210
+ });
10211
+ x += width;
10212
+ }
10213
+ }
10214
+ function paintClippedCueCanvas(opts, y, metrics, remaining) {
10215
+ paintCellBox(opts.ctx, opts.rect.x, y, opts.rect.width, metrics.rowHeight, opts.tokens.color.background, opts.tokens.color.border);
10216
+ paintCellText(opts, {
10217
+ x: opts.rect.x,
10218
+ y,
10219
+ width: opts.rect.width,
10220
+ height: metrics.rowHeight,
10221
+ text: `\u2026${remaining} more`,
10222
+ align: "center",
10223
+ color: opts.tokens.color.textMuted,
10224
+ weight: opts.tokens.font.weight.medium,
10225
+ paddingX: metrics.paddingX
10226
+ });
10227
+ }
10228
+ function paintCellBox(ctx, x, y, width, height, background, border, topBorder = false) {
10229
+ ctx.save();
10230
+ ctx.fillStyle = background;
10231
+ ctx.fillRect(x, y, width, height);
10232
+ ctx.strokeStyle = border;
10233
+ ctx.lineWidth = 1;
10234
+ ctx.beginPath();
10235
+ ctx.moveTo(x + width, y);
10236
+ ctx.lineTo(x + width, y + height);
10237
+ ctx.moveTo(x, y + height);
10238
+ ctx.lineTo(x + width, y + height);
10239
+ if (topBorder) {
10240
+ ctx.moveTo(x, y);
10241
+ ctx.lineTo(x + width, y);
10242
+ }
10243
+ ctx.stroke();
10244
+ ctx.restore();
10245
+ }
10246
+ function paintCellValueWithIcon(opts, input) {
10247
+ const font = fontString(opts.tokens.font.size.small, opts.tokens.font.family, input.weight);
10248
+ const gap = input.icon ? 6 : 0;
10249
+ const iconFont = fontString(opts.tokens.font.size.small, opts.tokens.font.family, opts.tokens.font.weight.bold);
10250
+ const iconW = input.icon ? measureText(input.icon, iconFont).width : 0;
10251
+ const textMax = Math.max(0, input.width - input.paddingX * 2 - iconW - gap);
10252
+ const text = ellipsize(input.text, textMax, font);
10253
+ const textW = measureText(text, font).width;
10254
+ const groupW = textW + iconW + gap;
10255
+ let start = input.x + input.paddingX;
10256
+ if (input.align === "right") start = input.x + input.width - input.paddingX - groupW;
10257
+ else if (input.align === "center") start = input.x + (input.width - groupW) / 2;
10258
+ const midY = input.y + input.height / 2;
10259
+ const iconFirst = input.icon && input.iconPosition !== "right";
10260
+ const iconX = iconFirst ? start : start + textW + gap;
10261
+ const textX = iconFirst ? start + iconW + gap : start;
10262
+ if (input.icon) {
10263
+ paintCanvasText(opts.ctx, {
10264
+ x: iconX,
10265
+ y: midY,
10266
+ text: input.icon,
10267
+ font: iconFont,
10268
+ color: input.iconColor ?? input.color,
10269
+ size: opts.tokens.font.size.small,
10270
+ baseline: "middle"
10271
+ });
10272
+ }
10273
+ paintRawCellText(opts, textX, midY, text, font, input.color, opts.tokens.font.size.small);
10274
+ }
10275
+ function paintCellText(opts, input) {
10276
+ const font = fontString(opts.tokens.font.size.small, opts.tokens.font.family, input.weight);
10277
+ const maxWidth = Math.max(0, input.width - input.paddingX * 2);
10278
+ const text = ellipsize(input.text, maxWidth, font);
10279
+ const x = input.align === "right" ? input.x + input.width - input.paddingX : input.align === "center" ? input.x + input.width / 2 : input.x + input.paddingX;
10280
+ paintRawCellText(opts, x, input.y + input.height / 2, text, font, input.color, opts.tokens.font.size.small, input.align);
10281
+ }
10282
+ function paintRawCellText(opts, x, y, text, font, color, size, align = "left") {
10283
+ if (!text) return;
10284
+ paintCanvasText(opts.ctx, {
10285
+ x,
10286
+ y,
10287
+ text,
10288
+ font,
10289
+ color,
10290
+ size,
10291
+ align,
10292
+ baseline: "middle"
10293
+ });
10294
+ }
10295
+ function ellipsize(text, maxWidth, font) {
10296
+ if (maxWidth <= 0 || text.length === 0) return "";
10297
+ if (measureText(text, font).width <= maxWidth) return text;
10298
+ const ellipsis = "\u2026";
10299
+ const ellipsisW = measureText(ellipsis, font).width;
10300
+ if (ellipsisW > maxWidth) return "";
10301
+ let lo = 0;
10302
+ let hi = text.length;
10303
+ while (lo < hi) {
10304
+ const mid = Math.ceil((lo + hi) / 2);
10305
+ if (measureText(text.slice(0, mid) + ellipsis, font).width <= maxWidth) lo = mid;
10306
+ else hi = mid - 1;
10307
+ }
10308
+ return text.slice(0, lo) + ellipsis;
10309
+ }
10310
+ function resolveCanvasConditional(opts, column, colIndex, cell, rowBg) {
10311
+ const cf = column.conditionalFormat;
10312
+ const raw = cell.raw;
10313
+ const domain = opts.conditionalDomains?.[colIndex] ?? null;
10314
+ if (!cf) return {};
10315
+ if ((cf.type === "colorScale" || cf.type === "bar" || cf.type === "icon") && (raw == null || !Number.isFinite(raw))) {
10316
+ return {};
10317
+ }
10318
+ const numeric = raw;
10319
+ if (cf.type === "colorScale") {
10320
+ if (!domain) return {};
10321
+ const midpoint = cf.midpoint ?? (domain[0] + domain[1]) / 2;
10322
+ const scale = cf.diverging === true || cf.midpoint !== void 0 ? divergingColorScale({ domain: [domain[0], midpoint, domain[1]], interpolator: diverging(cf.scheme ?? "redblue") }) : sequentialColorScale({ domain, interpolator: sequential(cf.scheme ?? "blues") });
10323
+ const color2 = scale.map(numeric);
10324
+ if (cf.target === "text") return { color: rgbaToCss(color2) };
10325
+ return { background: rgbaToCss(color2), color: rgbaToCss(readableTextColor(color2)) };
10326
+ }
10327
+ if (cf.type === "rules") {
10328
+ const style = evalRules(raw, cf.rules);
10329
+ return {
10330
+ background: style.background,
10331
+ color: style.color,
10332
+ weight: style.weight === "bold" ? opts.tokens.font.weight.bold : style.weight === "normal" ? opts.tokens.font.weight.normal : void 0,
10333
+ icon: style.icon,
10334
+ iconColor: style.color,
10335
+ iconPosition: "left"
10336
+ };
10337
+ }
10338
+ if (cf.type === "icon") {
10339
+ const semantic = iconForValue(cf.set, numeric, domain, cf.rules);
10340
+ if (!semantic) return {};
10341
+ return {
10342
+ icon: semantic.icon,
10343
+ iconColor: toneColor(semantic.tone, { up: opts.tokens.color.positive, mid: "#d97706", down: opts.tokens.color.negative }),
10344
+ iconPosition: cf.position ?? "left"
10345
+ };
10346
+ }
10347
+ if (!domain) return {};
10348
+ const diverging2 = cf.negativeColor !== void 0 || cf.baseline === "zero";
10349
+ const baseline = diverging2 ? normalized(0, domain) : 0;
10350
+ const value = normalized(numeric, domain);
10351
+ const leftRatio = diverging2 ? Math.min(baseline, value) : 0;
10352
+ const widthRatio = diverging2 ? Math.abs(value - baseline) : value;
10353
+ const positive = numeric >= 0;
10354
+ const color = alphaColor(positive ? cf.color ?? opts.tokens.color.accent : cf.negativeColor ?? opts.tokens.color.negative, 0.85);
10355
+ return { background: rowBg, showValue: cf.showValue !== false, bar: { leftRatio, widthRatio, color } };
10356
+ }
10357
+ function paintDataBar(opts, x, y, width, height, bar) {
10358
+ const left = x + 4 + bar.leftRatio * Math.max(0, width - 8);
10359
+ const barW = bar.widthRatio * Math.max(0, width - 8);
10360
+ opts.ctx.save();
10361
+ opts.ctx.globalAlpha *= 0.9;
10362
+ opts.ctx.beginPath();
10363
+ roundedRect(opts.ctx, left, y + 5, barW, Math.max(0, height - 10), Math.max(2, opts.tokens.radius.sm - 1));
10364
+ opts.ctx.fillStyle = bar.color;
10365
+ opts.ctx.fill();
10366
+ opts.ctx.restore();
10367
+ }
10368
+ function canvasRowBackground(opts, rowIndex, cls) {
10369
+ if (cls !== "normal") return opts.tokens.color.surface;
10370
+ if (opts.striped === true && rowIndex % 2 === 1) return subtleStripe(opts.tokens);
10371
+ return opts.tokens.color.background;
10372
+ }
10373
+ function negativeTextColor(value, column, fallback) {
10374
+ const raw = typeof value === "number" ? value : Number(value);
10375
+ if (Number.isFinite(raw) && raw < 0 && (column.negativeStyle === "red" || column.negativeStyle === "parens-red")) {
10376
+ return "#dc2626";
10377
+ }
10378
+ return fallback;
10379
+ }
9972
10380
  function applyStickyColumn(cell, opts, colIndex, isHeader) {
9973
10381
  if (colIndex == null || colIndex >= (opts.rowHeaderSpan ?? 0)) return;
9974
10382
  cell.style.position = "sticky";
@@ -10025,8 +10433,43 @@ var ROW_HEIGHT = 30;
10025
10433
  var BUFFER_ROWS = 8;
10026
10434
  function drawTable(surface, spec, tokens, size) {
10027
10435
  if (spec.type !== "table") return;
10436
+ if (surface.headless) {
10437
+ drawTableCanvas(surface, spec, tokens, size);
10438
+ return;
10439
+ }
10028
10440
  renderTable(surface, spec, tokens, size);
10029
10441
  }
10442
+ function drawTableCanvas(surface, spec, tokens, size) {
10443
+ const rect = drawTitleBlock(surface, tokens, size, spec.title);
10444
+ const data = spec.data ?? [];
10445
+ const columns = resolveColumns(data, spec);
10446
+ const read = columns.map((column) => accessor(column.key));
10447
+ const sortState = initialSortState(spec, columns);
10448
+ const sortedRows = sortRows(data, columns, read, sortState);
10449
+ const domains = columns.map(
10450
+ (column, colIndex) => resolveConditionalDomain(
10451
+ column.conditionalFormat,
10452
+ data.map((row) => numericRaw(read[colIndex](row)))
10453
+ )
10454
+ );
10455
+ paintTableCanvas({
10456
+ ctx: surface.marks.ctx,
10457
+ tokens,
10458
+ rect,
10459
+ columns,
10460
+ rowCount: sortedRows.length,
10461
+ getCell(rowIndex, colIndex) {
10462
+ const value = read[colIndex](sortedRows[rowIndex]);
10463
+ return { value, raw: numericRaw(value) };
10464
+ },
10465
+ striped: spec.striped === true,
10466
+ conditionalDomains: domains,
10467
+ headerRows: buildTableHeaderRows(columns),
10468
+ footerRow: buildTotalsRow(sortedRows, columns, read, spec),
10469
+ density: spec.density,
10470
+ sketch: resolveSketch(spec) != null
10471
+ });
10472
+ }
10030
10473
  function renderTable(surface, spec, tokens, size) {
10031
10474
  const rect = drawTitleBlock(surface, tokens, size, spec.title);
10032
10475
  const data = spec.data ?? [];
@@ -10197,10 +10640,80 @@ function requestFrame(callback) {
10197
10640
  var PATH_SEPARATOR2 = "\0";
10198
10641
  function drawMatrix(surface, spec, tokens, size) {
10199
10642
  if (spec.type !== "matrix") return;
10643
+ if (surface.headless) {
10644
+ drawMatrixCanvas(surface, spec, tokens, size);
10645
+ return;
10646
+ }
10200
10647
  renderMatrix(surface, spec, tokens, size);
10201
10648
  }
10649
+ function drawMatrixCanvas(surface, spec, tokens, size) {
10650
+ const rect = drawTitleBlock(surface, tokens, size, spec.title);
10651
+ const prepared = prepareMatrix(spec);
10652
+ paintTableCanvas({
10653
+ ctx: surface.marks.ctx,
10654
+ tokens,
10655
+ rect,
10656
+ columns: prepared.columns,
10657
+ rowCount: prepared.rows.length,
10658
+ getCell(rowIndex, colIndex) {
10659
+ const row = prepared.rows[rowIndex];
10660
+ if (colIndex === 0) return { value: rowLabel(row), raw: null };
10661
+ const { leaf, valueIndex } = prepared.leafValueColumns[colIndex - 1];
10662
+ const value = row.cellsByColumnKey.get(leaf.path.join(PATH_SEPARATOR2))?.values[valueIndex] ?? null;
10663
+ const display = displayMatrixValue(value, rowIndex, colIndex - 1, prepared.denominators, spec);
10664
+ return { value: display, raw: display };
10665
+ },
10666
+ rowClass(rowIndex) {
10667
+ const row = prepared.rows[rowIndex];
10668
+ return row.isGrandTotal === true ? "grandtotal" : row.isSubtotal === true ? "subtotal" : "normal";
10669
+ },
10670
+ cellIndent(rowIndex, colIndex) {
10671
+ return colIndex === 0 ? prepared.rows[rowIndex].depth * 16 : 0;
10672
+ },
10673
+ rowHeaderSpan: 1,
10674
+ striped: false,
10675
+ conditionalDomains: prepared.bodyDomains,
10676
+ headerRows: prepared.headerRows,
10677
+ density: spec.density,
10678
+ sketch: resolveSketch(spec) != null
10679
+ });
10680
+ }
10202
10681
  function renderMatrix(surface, spec, tokens, size) {
10203
10682
  const rect = drawTitleBlock(surface, tokens, size, spec.title);
10683
+ const prepared = prepareMatrix(spec);
10684
+ const host = document.createElement("div");
10685
+ surface.overlay.appendChild(host);
10686
+ buildTable({
10687
+ container: host,
10688
+ tokens,
10689
+ rect,
10690
+ columns: prepared.columns,
10691
+ rowCount: prepared.rows.length,
10692
+ getCell(rowIndex, colIndex) {
10693
+ const row = prepared.rows[rowIndex];
10694
+ if (colIndex === 0) return { value: rowLabel(row), raw: null };
10695
+ const { leaf, valueIndex } = prepared.leafValueColumns[colIndex - 1];
10696
+ const value = row.cellsByColumnKey.get(leaf.path.join(PATH_SEPARATOR2))?.values[valueIndex] ?? null;
10697
+ const display = displayMatrixValue(value, rowIndex, colIndex - 1, prepared.denominators, spec);
10698
+ return { value: display, raw: display };
10699
+ },
10700
+ rowClass(rowIndex) {
10701
+ const row = prepared.rows[rowIndex];
10702
+ return row.isGrandTotal === true ? "grandtotal" : row.isSubtotal === true ? "subtotal" : "normal";
10703
+ },
10704
+ cellIndent(rowIndex, colIndex) {
10705
+ return colIndex === 0 ? prepared.rows[rowIndex].depth * 16 : 0;
10706
+ },
10707
+ rowHeaderSpan: 1,
10708
+ stickyHeader: true,
10709
+ striped: false,
10710
+ conditionalDomains: prepared.bodyDomains,
10711
+ headerRows: prepared.headerRows,
10712
+ density: spec.density,
10713
+ sketch: resolveSketch(spec) != null
10714
+ });
10715
+ }
10716
+ function prepareMatrix(spec) {
10204
10717
  const data = spec.data ?? [];
10205
10718
  const result = pivot(data, {
10206
10719
  rows: spec.rows,
@@ -10251,37 +10764,14 @@ function renderMatrix(surface, spec, tokens, size) {
10251
10764
  )
10252
10765
  );
10253
10766
  });
10254
- const host = document.createElement("div");
10255
- surface.overlay.appendChild(host);
10256
- buildTable({
10257
- container: host,
10258
- tokens,
10259
- rect,
10767
+ return {
10768
+ rows: result.rows,
10260
10769
  columns,
10261
- rowCount: result.rows.length,
10262
- getCell(rowIndex, colIndex) {
10263
- const row = result.rows[rowIndex];
10264
- if (colIndex === 0) return { value: rowLabel(row), raw: null };
10265
- const { leaf, valueIndex } = leafValueColumns[colIndex - 1];
10266
- const value = row.cellsByColumnKey.get(leaf.path.join(PATH_SEPARATOR2))?.values[valueIndex] ?? null;
10267
- const display = displayMatrixValue(value, rowIndex, colIndex - 1, denominators, spec);
10268
- return { value: display, raw: display };
10269
- },
10270
- rowClass(rowIndex) {
10271
- const row = result.rows[rowIndex];
10272
- return row.isGrandTotal === true ? "grandtotal" : row.isSubtotal === true ? "subtotal" : "normal";
10273
- },
10274
- cellIndent(rowIndex, colIndex) {
10275
- return colIndex === 0 ? result.rows[rowIndex].depth * 16 : 0;
10276
- },
10277
- rowHeaderSpan: 1,
10278
- stickyHeader: true,
10279
- striped: false,
10280
- conditionalDomains: bodyDomains,
10281
- headerRows: buildMatrixHeaderRows(result.columnTree, sortedLeaves, spec, sortedLeaves !== result.columnLeaves),
10282
- density: spec.density,
10283
- sketch: resolveSketch(spec) != null
10284
- });
10770
+ leafValueColumns,
10771
+ denominators,
10772
+ bodyDomains,
10773
+ headerRows: buildMatrixHeaderRows(result.columnTree, sortedLeaves, spec, sortedLeaves !== result.columnLeaves)
10774
+ };
10285
10775
  }
10286
10776
  function rowLabel(row) {
10287
10777
  if (row.isGrandTotal === true) return row.label;
@@ -10862,6 +11352,10 @@ function emptyNotice(host, message, color) {
10862
11352
  // src/charts/slicers/dropdown.ts
10863
11353
  function drawDropdown(surface, spec, tokens, size, context) {
10864
11354
  const s = spec;
11355
+ if (surface.headless) {
11356
+ drawDropdownCanvas(surface, s, tokens, size, context);
11357
+ return;
11358
+ }
10865
11359
  const shell = mountSlicerShell(surface, tokens, size, {
10866
11360
  title: s.title,
10867
11361
  label: slicerLabel(s)
@@ -10942,10 +11436,61 @@ function drawDropdown(surface, spec, tokens, size, context) {
10942
11436
  trigger.addEventListener("click", openMenu);
10943
11437
  refreshChrome();
10944
11438
  }
11439
+ function drawDropdownCanvas(surface, s, tokens, size, context) {
11440
+ const ctx = surface.marks.ctx;
11441
+ const rect = drawTitleBlock(surface, tokens, size, s.title ?? slicerLabel(s));
11442
+ const options = slicerOptions(s, context);
11443
+ const display = (v) => formatValue(v, void 0) || "\u2014";
11444
+ const placeholder = s.placeholder ?? (s.multiple ? "Any" : "All");
11445
+ const current = currentValue(s, context);
11446
+ const selected = new Set((current?.kind === "set" ? current.values : []).map((v) => toKey(v)));
11447
+ const picks = options.filter((o) => selected.has(toKey(o)));
11448
+ const label = selected.size === 0 ? placeholder : picks.length === 1 ? display(picks[0]) : s.multiple ? `${picks.length} selected` : display(picks[0]);
11449
+ const h = Math.min(42, Math.max(32, rect.height));
11450
+ const y = rect.y + Math.max(0, (rect.height - h) * 0.12);
11451
+ ctx.save();
11452
+ ctx.beginPath();
11453
+ roundedRect(ctx, rect.x, y, rect.width, h, tokens.radius.md);
11454
+ ctx.fillStyle = tokens.color.surface;
11455
+ ctx.fill();
11456
+ ctx.lineWidth = 1;
11457
+ ctx.strokeStyle = selected.size ? tokens.color.accent : tokens.color.border;
11458
+ ctx.stroke();
11459
+ ctx.restore();
11460
+ const font = fontString(tokens.font.size.base, tokens.font.family, tokens.font.weight.normal);
11461
+ ctx.save();
11462
+ ctx.beginPath();
11463
+ ctx.rect(rect.x + tokens.spacing.sm, y, Math.max(0, rect.width - tokens.spacing.sm * 4 - 14), h);
11464
+ ctx.clip();
11465
+ paintCanvasText(ctx, {
11466
+ x: rect.x + tokens.spacing.sm,
11467
+ y: y + h / 2,
11468
+ text: label,
11469
+ font,
11470
+ color: selected.size ? tokens.color.text : tokens.color.textMuted,
11471
+ size: tokens.font.size.base,
11472
+ baseline: "middle"
11473
+ });
11474
+ ctx.restore();
11475
+ paintCanvasText(ctx, {
11476
+ x: rect.x + rect.width - tokens.spacing.sm,
11477
+ y: y + h / 2,
11478
+ text: "\u25BE",
11479
+ font,
11480
+ color: tokens.color.textMuted,
11481
+ size: tokens.font.size.base,
11482
+ align: "right",
11483
+ baseline: "middle"
11484
+ });
11485
+ }
10945
11486
 
10946
11487
  // src/charts/slicers/search.ts
10947
11488
  function drawSearch(surface, spec, tokens, size, context) {
10948
11489
  const s = spec;
11490
+ if (surface.headless) {
11491
+ drawSearchCanvas(surface, s, tokens, size, context);
11492
+ return;
11493
+ }
10949
11494
  const shell = mountSlicerShell(surface, tokens, size, {
10950
11495
  title: s.title,
10951
11496
  label: slicerLabel(s)
@@ -10985,10 +11530,47 @@ function drawSearch(surface, spec, tokens, size, context) {
10985
11530
  });
10986
11531
  shell.setClear(initial ? clear : null);
10987
11532
  }
11533
+ function drawSearchCanvas(surface, s, tokens, size, context) {
11534
+ const ctx = surface.marks.ctx;
11535
+ const rect = drawTitleBlock(surface, tokens, size, s.title ?? slicerLabel(s));
11536
+ const current = currentValue(s, context);
11537
+ const value = current?.kind === "text" ? current.query : "";
11538
+ const text = value || (s.placeholder ?? `Search ${s.field}\u2026`);
11539
+ const h = Math.min(42, Math.max(32, rect.height));
11540
+ const y = rect.y + Math.max(0, (rect.height - h) * 0.12);
11541
+ ctx.save();
11542
+ ctx.beginPath();
11543
+ roundedRect(ctx, rect.x, y, rect.width, h, tokens.radius.md);
11544
+ ctx.fillStyle = tokens.color.surface;
11545
+ ctx.fill();
11546
+ ctx.lineWidth = 1;
11547
+ ctx.strokeStyle = value ? tokens.color.accent : tokens.color.border;
11548
+ ctx.stroke();
11549
+ ctx.restore();
11550
+ const font = fontString(tokens.font.size.base, tokens.font.family, tokens.font.weight.normal);
11551
+ ctx.save();
11552
+ ctx.beginPath();
11553
+ ctx.rect(rect.x + tokens.spacing.sm, y, Math.max(0, rect.width - tokens.spacing.sm * 2), h);
11554
+ ctx.clip();
11555
+ paintCanvasText(ctx, {
11556
+ x: rect.x + tokens.spacing.sm,
11557
+ y: y + h / 2,
11558
+ text,
11559
+ font,
11560
+ color: value ? tokens.color.text : tokens.color.textMuted,
11561
+ size: tokens.font.size.base,
11562
+ baseline: "middle"
11563
+ });
11564
+ ctx.restore();
11565
+ }
10988
11566
 
10989
11567
  // src/charts/slicers/list.ts
10990
11568
  function drawList(surface, spec, tokens, size, context) {
10991
11569
  const s = spec;
11570
+ if (surface.headless) {
11571
+ drawListCanvas(surface, s, tokens, size, context);
11572
+ return;
11573
+ }
10992
11574
  const shell = mountSlicerShell(surface, tokens, size, {
10993
11575
  title: s.title,
10994
11576
  label: slicerLabel(s)
@@ -11106,16 +11688,101 @@ function drawList(surface, spec, tokens, size, context) {
11106
11688
  }
11107
11689
  }
11108
11690
  });
11109
- listEl.appendChild(row);
11691
+ listEl.appendChild(row);
11692
+ }
11693
+ };
11694
+ rebuild();
11695
+ shell.setClear(selected.size ? clearAll : null);
11696
+ }
11697
+ function drawListCanvas(surface, s, tokens, size, context) {
11698
+ const ctx = surface.marks.ctx;
11699
+ const rect = drawTitleBlock(surface, tokens, size, s.title ?? slicerLabel(s));
11700
+ const options = slicerOptions(s, context);
11701
+ const display = (v) => formatValue(v, void 0) || "\u2014";
11702
+ const current = currentValue(s, context);
11703
+ const selected = new Set((current?.kind === "set" ? current.values : []).map((v) => toKey(v)));
11704
+ const rowH = Math.max(24, tokens.font.size.base + tokens.spacing.sm + 2);
11705
+ const pad3 = tokens.spacing.xs;
11706
+ const visibleRows = Math.max(1, Math.floor((rect.height - pad3 * 2) / rowH));
11707
+ ctx.save();
11708
+ ctx.beginPath();
11709
+ roundedRect(ctx, rect.x, rect.y, rect.width, rect.height, tokens.radius.md);
11710
+ ctx.fillStyle = tokens.color.surface;
11711
+ ctx.fill();
11712
+ ctx.lineWidth = 1;
11713
+ ctx.strokeStyle = tokens.color.border;
11714
+ ctx.stroke();
11715
+ ctx.clip();
11716
+ const font = fontString(tokens.font.size.base, tokens.font.family, tokens.font.weight.normal);
11717
+ const checkSize = 12;
11718
+ options.slice(0, visibleRows).forEach((opt, i) => {
11719
+ const y = rect.y + pad3 + i * rowH;
11720
+ const key = toKey(opt);
11721
+ const active = selected.has(key);
11722
+ if (active) {
11723
+ ctx.beginPath();
11724
+ roundedRect(ctx, rect.x + pad3, y + 1, rect.width - pad3 * 2, rowH - 2, tokens.radius.sm);
11725
+ ctx.fillStyle = tokens.color.background;
11726
+ ctx.fill();
11727
+ }
11728
+ const bx = rect.x + tokens.spacing.sm;
11729
+ const by = y + rowH / 2 - checkSize / 2;
11730
+ ctx.beginPath();
11731
+ roundedRect(ctx, bx, by, checkSize, checkSize, 3);
11732
+ ctx.fillStyle = active ? tokens.color.accent : tokens.color.background;
11733
+ ctx.fill();
11734
+ ctx.lineWidth = 1;
11735
+ ctx.strokeStyle = active ? tokens.color.accent : tokens.color.border;
11736
+ ctx.stroke();
11737
+ if (active) {
11738
+ paintCanvasText(ctx, {
11739
+ x: bx + checkSize / 2,
11740
+ y: y + rowH / 2,
11741
+ text: "\u2713",
11742
+ font: fontString(tokens.font.size.small, tokens.font.family, tokens.font.weight.bold),
11743
+ color: tokens.color.surface,
11744
+ size: tokens.font.size.small,
11745
+ align: "center",
11746
+ baseline: "middle"
11747
+ });
11110
11748
  }
11111
- };
11112
- rebuild();
11113
- shell.setClear(selected.size ? clearAll : null);
11749
+ ctx.save();
11750
+ ctx.beginPath();
11751
+ ctx.rect(bx + checkSize + tokens.spacing.sm, y, Math.max(0, rect.width - checkSize - tokens.spacing.sm * 4), rowH);
11752
+ ctx.clip();
11753
+ paintCanvasText(ctx, {
11754
+ x: bx + checkSize + tokens.spacing.sm,
11755
+ y: y + rowH / 2,
11756
+ text: display(opt),
11757
+ font,
11758
+ color: tokens.color.text,
11759
+ size: tokens.font.size.base,
11760
+ baseline: "middle"
11761
+ });
11762
+ ctx.restore();
11763
+ });
11764
+ if (options.length > visibleRows) {
11765
+ paintCanvasText(ctx, {
11766
+ x: rect.x + rect.width - tokens.spacing.sm,
11767
+ y: rect.y + rect.height - tokens.spacing.xs,
11768
+ text: `+${options.length - visibleRows} more`,
11769
+ font: fontString(tokens.font.size.small, tokens.font.family, tokens.font.weight.medium),
11770
+ color: tokens.color.textMuted,
11771
+ size: tokens.font.size.small,
11772
+ align: "right",
11773
+ baseline: "alphabetic"
11774
+ });
11775
+ }
11776
+ ctx.restore();
11114
11777
  }
11115
11778
 
11116
11779
  // src/charts/slicers/range.ts
11117
11780
  function drawRange(surface, spec, tokens, size, context) {
11118
11781
  const s = spec;
11782
+ if (surface.headless) {
11783
+ drawRangeCanvas(surface, s, tokens, size, context);
11784
+ return;
11785
+ }
11119
11786
  const shell = mountSlicerShell(surface, tokens, size, {
11120
11787
  title: s.title,
11121
11788
  label: slicerLabel(s)
@@ -11157,6 +11824,80 @@ function drawRange(surface, spec, tokens, size, context) {
11157
11824
  const active = current?.kind === "range" && (low > min || high < max);
11158
11825
  shell.setClear(active ? clear : null);
11159
11826
  }
11827
+ function drawRangeCanvas(surface, s, tokens, size, context) {
11828
+ const ctx = surface.marks.ctx;
11829
+ const rect = drawTitleBlock(surface, tokens, size, s.title ?? slicerLabel(s));
11830
+ const dataExtent = extent(slicerSource(s, context), s.field);
11831
+ const min = s.min ?? dataExtent?.[0] ?? 0;
11832
+ const max = s.max ?? dataExtent?.[1] ?? 1;
11833
+ const font = fontString(tokens.font.size.small, tokens.font.family, tokens.font.weight.medium);
11834
+ if (!(max > min)) {
11835
+ paintCanvasText(ctx, {
11836
+ x: rect.x,
11837
+ y: rect.y,
11838
+ text: `No numeric range for "${s.field}".`,
11839
+ font,
11840
+ color: tokens.color.textMuted,
11841
+ size: tokens.font.size.small,
11842
+ baseline: "top"
11843
+ });
11844
+ return;
11845
+ }
11846
+ const current = currentValue(s, context);
11847
+ const rawLow = current?.kind === "range" && current.min != null ? toNumber(current.min) : min;
11848
+ const rawHigh = current?.kind === "range" && current.max != null ? toNumber(current.max) : max;
11849
+ const low = Number.isNaN(rawLow) ? min : Math.max(min, Math.min(max, rawLow));
11850
+ const high = Number.isNaN(rawHigh) ? max : Math.max(low, Math.min(max, rawHigh));
11851
+ const fmt = (v) => formatValue(v, s.format);
11852
+ paintSliderSnapshot(ctx, tokens, rect, min, max, low, high, fmt);
11853
+ }
11854
+ function paintSliderSnapshot(ctx, tokens, rect, min, max, low, high, format) {
11855
+ const span = Math.max(1e-9, max - min);
11856
+ const trackX = rect.x + 8;
11857
+ const trackW = Math.max(0, rect.width - 16);
11858
+ const midY = rect.y + Math.min(Math.max(rect.height * 0.58, 34), Math.max(34, rect.height - 14));
11859
+ const lowX = trackX + (low - min) / span * trackW;
11860
+ const highX = trackX + (high - min) / span * trackW;
11861
+ const font = fontString(tokens.font.size.small, tokens.font.family, tokens.font.weight.medium);
11862
+ paintCanvasText(ctx, {
11863
+ x: rect.x,
11864
+ y: rect.y,
11865
+ text: format(low),
11866
+ font,
11867
+ color: tokens.color.textMuted,
11868
+ size: tokens.font.size.small,
11869
+ baseline: "top"
11870
+ });
11871
+ paintCanvasText(ctx, {
11872
+ x: rect.x + rect.width,
11873
+ y: rect.y,
11874
+ text: format(high),
11875
+ font,
11876
+ color: tokens.color.textMuted,
11877
+ size: tokens.font.size.small,
11878
+ align: "right",
11879
+ baseline: "top"
11880
+ });
11881
+ ctx.save();
11882
+ ctx.beginPath();
11883
+ roundedRect(ctx, trackX, midY - 2, trackW, 4, 2);
11884
+ ctx.fillStyle = tokens.color.border;
11885
+ ctx.fill();
11886
+ ctx.beginPath();
11887
+ roundedRect(ctx, lowX, midY - 2, Math.max(0, highX - lowX), 4, 2);
11888
+ ctx.fillStyle = tokens.color.accent;
11889
+ ctx.fill();
11890
+ for (const x of [lowX, highX]) {
11891
+ ctx.beginPath();
11892
+ ctx.arc(x, midY, 8, 0, Math.PI * 2);
11893
+ ctx.fillStyle = tokens.color.surface;
11894
+ ctx.fill();
11895
+ ctx.lineWidth = 2;
11896
+ ctx.strokeStyle = tokens.color.accent;
11897
+ ctx.stroke();
11898
+ }
11899
+ ctx.restore();
11900
+ }
11160
11901
 
11161
11902
  // src/charts/slicers/dateRange.ts
11162
11903
  var DAY_MS = 864e5;
@@ -11181,6 +11922,10 @@ function toISODate(ms) {
11181
11922
  }
11182
11923
  function drawDateRange(surface, spec, tokens, size, context) {
11183
11924
  const s = spec;
11925
+ if (surface.headless) {
11926
+ drawDateRangeCanvas(surface, s, tokens, size, context);
11927
+ return;
11928
+ }
11184
11929
  const shell = mountSlicerShell(surface, tokens, size, {
11185
11930
  title: s.title,
11186
11931
  label: slicerLabel(s)
@@ -11253,6 +11998,89 @@ function drawDateRange(surface, spec, tokens, size, context) {
11253
11998
  const active = current?.kind === "range" && (low > min || high < max);
11254
11999
  shell.setClear(active ? clear : null);
11255
12000
  }
12001
+ function drawDateRangeCanvas(surface, s, tokens, size, context) {
12002
+ const ctx = surface.marks.ctx;
12003
+ const rect = drawTitleBlock(surface, tokens, size, s.title ?? slicerLabel(s));
12004
+ const ext = dateExtent(slicerSource(s, context), s.field);
12005
+ const font = fontString(tokens.font.size.small, tokens.font.family, tokens.font.weight.medium);
12006
+ if (!ext || !(ext[1] > ext[0])) {
12007
+ paintCanvasText(ctx, {
12008
+ x: rect.x,
12009
+ y: rect.y,
12010
+ text: `No date range for "${s.field}".`,
12011
+ font,
12012
+ color: tokens.color.textMuted,
12013
+ size: tokens.font.size.small,
12014
+ baseline: "top"
12015
+ });
12016
+ return;
12017
+ }
12018
+ const [min, max] = ext;
12019
+ const current = currentValue(s, context);
12020
+ const toMs = (v, fallback) => {
12021
+ if (v == null) return fallback;
12022
+ const d = toDate(v);
12023
+ return d ? d.getTime() : fallback;
12024
+ };
12025
+ const low = current?.kind === "range" ? toMs(current.min, min) : min;
12026
+ const high = current?.kind === "range" ? toMs(current.max, max) : max;
12027
+ paintDateFields(ctx, tokens, rect, toISODate(low), toISODate(high));
12028
+ }
12029
+ function paintDateFields(ctx, tokens, rect, low, high) {
12030
+ const labelFont = fontString(tokens.font.size.small, tokens.font.family, tokens.font.weight.medium);
12031
+ const valueFont = fontString(tokens.font.size.base, tokens.font.family, tokens.font.weight.normal);
12032
+ const gap = tokens.spacing.sm;
12033
+ const fieldH = 36;
12034
+ const labelH = tokens.font.size.small + 4;
12035
+ const y = rect.y + Math.max(0, (rect.height - (labelH + fieldH)) * 0.12);
12036
+ const arrowW = 18;
12037
+ const fieldW = Math.max(44, (rect.width - gap * 2 - arrowW) / 2);
12038
+ const paintField = (x, label, value) => {
12039
+ paintCanvasText(ctx, {
12040
+ x,
12041
+ y,
12042
+ text: label,
12043
+ font: labelFont,
12044
+ color: tokens.color.textMuted,
12045
+ size: tokens.font.size.small,
12046
+ baseline: "top"
12047
+ });
12048
+ const fy = y + labelH;
12049
+ ctx.save();
12050
+ ctx.beginPath();
12051
+ roundedRect(ctx, x, fy, fieldW, fieldH, tokens.radius.md);
12052
+ ctx.fillStyle = tokens.color.surface;
12053
+ ctx.fill();
12054
+ ctx.lineWidth = 1;
12055
+ ctx.strokeStyle = tokens.color.border;
12056
+ ctx.stroke();
12057
+ ctx.beginPath();
12058
+ ctx.rect(x + tokens.spacing.sm, fy, Math.max(0, fieldW - tokens.spacing.sm * 2), fieldH);
12059
+ ctx.clip();
12060
+ paintCanvasText(ctx, {
12061
+ x: x + tokens.spacing.sm,
12062
+ y: fy + fieldH / 2,
12063
+ text: value,
12064
+ font: valueFont,
12065
+ color: tokens.color.text,
12066
+ size: tokens.font.size.base,
12067
+ baseline: "middle"
12068
+ });
12069
+ ctx.restore();
12070
+ };
12071
+ paintField(rect.x, "From", low);
12072
+ paintCanvasText(ctx, {
12073
+ x: rect.x + fieldW + gap + arrowW / 2,
12074
+ y: y + labelH + fieldH / 2,
12075
+ text: "\u2192",
12076
+ font: valueFont,
12077
+ color: tokens.color.accent,
12078
+ size: tokens.font.size.base,
12079
+ align: "center",
12080
+ baseline: "middle"
12081
+ });
12082
+ paintField(rect.x + fieldW + gap * 2 + arrowW, "To", high);
12083
+ }
11256
12084
 
11257
12085
  // src/interaction/cartesian.ts
11258
12086
  var HIT_RADIUS = 26;
@@ -15079,88 +15907,6 @@ function facetReport(spec, layout2, tokens, size) {
15079
15907
  };
15080
15908
  }
15081
15909
 
15082
- // src/runtime/headless.ts
15083
- var DOM_ONLY_TYPES = /* @__PURE__ */ new Set([
15084
- "kpi",
15085
- "table",
15086
- "matrix",
15087
- "dropdown",
15088
- "list",
15089
- "search",
15090
- "range",
15091
- "dateRange",
15092
- "dashboard"
15093
- ]);
15094
- function headlessTokens(spec) {
15095
- const tokens = resolveTheme(spec.theme);
15096
- const sketch = resolveSketch(spec);
15097
- return sketch?.font ? withSketchFont(tokens) : tokens;
15098
- }
15099
- function renderToContext(target, spec) {
15100
- const type = spec.type;
15101
- if (DOM_ONLY_TYPES.has(type)) {
15102
- throw new Error(
15103
- `Graphein: "${type}" is a DOM-only visual and cannot be rendered to a canvas headlessly. Headless rendering supports the canvas charts (line, area, bar, scatter, box, pie, heatmap, sankey, choropleth, combo, histogram, funnel).`
15104
- );
15105
- }
15106
- const ctx = target.marks;
15107
- const size = { width: target.width, height: target.height };
15108
- const tokens = headlessTokens(spec);
15109
- const surface = {
15110
- headless: true,
15111
- width: size.width,
15112
- height: size.height,
15113
- dpr: 1,
15114
- marks: { ctx },
15115
- interaction: { ctx: target.interaction ?? ctx }
15116
- };
15117
- const bg = spec.background ?? tokens.color.background;
15118
- ctx.save();
15119
- ctx.fillStyle = bg;
15120
- ctx.fillRect(0, 0, size.width, size.height);
15121
- ctx.restore();
15122
- const src = spec.data ?? [];
15123
- const data = spec.transform?.length ? applyTransforms(spec.transform, src) : src;
15124
- const effectiveSpec = data === src ? spec : { ...spec, data };
15125
- if (isFaceted(effectiveSpec)) {
15126
- const layout2 = buildFacetModels(effectiveSpec, tokens, size);
15127
- if (layout2) {
15128
- drawFacet(surface, effectiveSpec, layout2, tokens);
15129
- return facetReport(effectiveSpec, layout2, tokens, size);
15130
- }
15131
- }
15132
- let reportModel;
15133
- if (CARTESIAN_TYPES.has(type)) {
15134
- const renderer = cartesianRenderers[type];
15135
- const model = buildCartesianModel(effectiveSpec, tokens, size);
15136
- reportModel = model;
15137
- drawAxesUnderlay(surface, model);
15138
- if (renderer) renderer(surface, model);
15139
- drawTrendlines(surface, model);
15140
- drawAnnotations(surface, model);
15141
- drawOverlay(surface, model);
15142
- drawAnnotationLabels(surface, model);
15143
- drawTrendlineLabels(surface, model);
15144
- } else {
15145
- const renderer = customRenderers[type];
15146
- if (!renderer) {
15147
- throw new Error(`Graphein: no renderer registered for chart type "${type}".`);
15148
- }
15149
- const rctx = {
15150
- emphasis: null,
15151
- store: createSelectionStore(),
15152
- param: void 0,
15153
- def: void 0,
15154
- sourceData: src,
15155
- framed: false,
15156
- requestRedraw: () => {
15157
- }
15158
- };
15159
- renderer(surface, effectiveSpec, tokens, size, rctx);
15160
- }
15161
- return buildRenderReport({ type, spec, data, tokens, size, model: reportModel });
15162
- }
15163
-
15164
15910
  // src/render/fonts.ts
15165
15911
  function fontFaceSet() {
15166
15912
  try {
@@ -16271,9 +17017,180 @@ function renderDashboard(target, spec, options) {
16271
17017
  };
16272
17018
  }
16273
17019
 
17020
+ // src/runtime/dashboardHeadless.ts
17021
+ var HEADER_H = 44;
17022
+ var SECTION_TITLE_H = 26;
17023
+ function renderDashboardToContext(target, spec) {
17024
+ const ctx = target.marks;
17025
+ const tokens = resolveTheme(spec.theme);
17026
+ const layout2 = resolveDashboardLayout(spec.layout);
17027
+ const gap = layout2.gap;
17028
+ const pad3 = layout2.padding ?? Math.round(gap * 1.4);
17029
+ ctx.save();
17030
+ ctx.fillStyle = spec.background ?? tokens.color.background;
17031
+ ctx.fillRect(0, 0, target.width, target.height);
17032
+ ctx.restore();
17033
+ const titleText3 = typeof spec.title === "string" ? spec.title : spec.title?.text;
17034
+ let cursorY = pad3;
17035
+ if (titleText3 || spec.subtitle) {
17036
+ ctx.save();
17037
+ ctx.fillStyle = tokens.color.accent;
17038
+ ctx.fillRect(pad3, cursorY + 2, 5, 30);
17039
+ ctx.fillStyle = tokens.color.text;
17040
+ ctx.textBaseline = "top";
17041
+ ctx.font = `${tokens.font.weight.bold} ${tokens.font.size.title}px ${tokens.font.family}`;
17042
+ if (titleText3) ctx.fillText(titleText3, pad3 + 14, cursorY + 2);
17043
+ if (spec.subtitle) {
17044
+ ctx.fillStyle = tokens.color.textMuted;
17045
+ ctx.font = `${tokens.font.weight.normal} ${tokens.font.size.small}px ${tokens.font.family}`;
17046
+ ctx.fillText(spec.subtitle, pad3 + 14, cursorY + 24);
17047
+ }
17048
+ ctx.restore();
17049
+ cursorY += HEADER_H;
17050
+ }
17051
+ const wired = wireViews(spec.views, spec.interactions ?? "auto");
17052
+ const sections = resolveDashboardSections(wired, layout2);
17053
+ let markCount = 0;
17054
+ let allOk = true;
17055
+ let views = 0;
17056
+ for (const section of sections) {
17057
+ if (section.collapsed) continue;
17058
+ if (section.section?.title) {
17059
+ ctx.save();
17060
+ ctx.fillStyle = tokens.color.text;
17061
+ ctx.textBaseline = "top";
17062
+ ctx.font = `${tokens.font.weight.bold} ${tokens.font.size.base}px ${tokens.font.family}`;
17063
+ ctx.fillText(section.section.title, pad3, cursorY);
17064
+ ctx.restore();
17065
+ cursorY += SECTION_TITLE_H;
17066
+ }
17067
+ const cols = Math.max(1, section.cols);
17068
+ const innerW = target.width - pad3 * 2;
17069
+ const colW = (innerW - gap * (cols - 1)) / cols;
17070
+ const cells = placeViews(section.views, cols, layout2.preset);
17071
+ const flow = { x: 1, y: 1, rowH: 0 };
17072
+ let maxBottom = cursorY;
17073
+ for (const cell of cells) {
17074
+ let gx = cell.x;
17075
+ let gy = cell.y;
17076
+ if (gx == null || gy == null) {
17077
+ if (flow.x + cell.w - 1 > cols) {
17078
+ flow.x = 1;
17079
+ flow.y += Math.max(1, flow.rowH);
17080
+ flow.rowH = 0;
17081
+ }
17082
+ gx = flow.x;
17083
+ gy = flow.y;
17084
+ flow.x += cell.w;
17085
+ flow.rowH = Math.max(flow.rowH, cell.h);
17086
+ }
17087
+ const cx = pad3 + (gx - 1) * (colW + gap);
17088
+ const cy = cursorY + (gy - 1) * (section.rowHeight + gap);
17089
+ const cw = cell.w * colW + (cell.w - 1) * gap;
17090
+ const ch = cell.h * section.rowHeight + (cell.h - 1) * gap;
17091
+ const viewSpec = inheritInto(cell.view.spec, spec, tokens);
17092
+ ctx.save();
17093
+ ctx.translate(cx, cy);
17094
+ ctx.beginPath();
17095
+ ctx.rect(0, 0, cw, ch);
17096
+ ctx.clip();
17097
+ try {
17098
+ const r = renderToContext({ marks: ctx, interaction: target.interaction, width: cw, height: ch }, viewSpec);
17099
+ markCount += r.markCount;
17100
+ if (!r.ok) allOk = false;
17101
+ views++;
17102
+ } catch {
17103
+ allOk = false;
17104
+ }
17105
+ ctx.restore();
17106
+ maxBottom = Math.max(maxBottom, cy + ch);
17107
+ }
17108
+ cursorY = maxBottom + gap;
17109
+ }
17110
+ return {
17111
+ type: "dashboard",
17112
+ size: { width: target.width, height: target.height },
17113
+ markCount,
17114
+ seriesCount: views,
17115
+ colorCount: 1,
17116
+ ok: allOk,
17117
+ diagnostics: [],
17118
+ summary: `Dashboard of ${views} view${views === 1 ? "" : "s"}.`
17119
+ };
17120
+ }
17121
+
17122
+ // src/runtime/headless.ts
17123
+ function headlessTokens(spec) {
17124
+ const tokens = resolveTheme(spec.theme);
17125
+ const sketch = resolveSketch(spec);
17126
+ return sketch?.font ? withSketchFont(tokens) : tokens;
17127
+ }
17128
+ function renderToContext(target, spec) {
17129
+ const type = spec.type;
17130
+ if (type === "dashboard") {
17131
+ return renderDashboardToContext(target, spec);
17132
+ }
17133
+ const ctx = target.marks;
17134
+ const size = { width: target.width, height: target.height };
17135
+ const tokens = headlessTokens(spec);
17136
+ const surface = {
17137
+ headless: true,
17138
+ width: size.width,
17139
+ height: size.height,
17140
+ dpr: 1,
17141
+ marks: { ctx },
17142
+ interaction: { ctx: target.interaction ?? ctx }
17143
+ };
17144
+ const bg = spec.background ?? tokens.color.background;
17145
+ ctx.save();
17146
+ ctx.fillStyle = bg;
17147
+ ctx.fillRect(0, 0, size.width, size.height);
17148
+ ctx.restore();
17149
+ const src = spec.data ?? [];
17150
+ const data = spec.transform?.length ? applyTransforms(spec.transform, src) : src;
17151
+ const effectiveSpec = data === src ? spec : { ...spec, data };
17152
+ if (isFaceted(effectiveSpec)) {
17153
+ const layout2 = buildFacetModels(effectiveSpec, tokens, size);
17154
+ if (layout2) {
17155
+ drawFacet(surface, effectiveSpec, layout2, tokens);
17156
+ return facetReport(effectiveSpec, layout2, tokens, size);
17157
+ }
17158
+ }
17159
+ let reportModel;
17160
+ if (CARTESIAN_TYPES.has(type)) {
17161
+ const renderer = cartesianRenderers[type];
17162
+ const model = buildCartesianModel(effectiveSpec, tokens, size);
17163
+ reportModel = model;
17164
+ drawAxesUnderlay(surface, model);
17165
+ if (renderer) renderer(surface, model);
17166
+ drawTrendlines(surface, model);
17167
+ drawAnnotations(surface, model);
17168
+ drawOverlay(surface, model);
17169
+ drawAnnotationLabels(surface, model);
17170
+ drawTrendlineLabels(surface, model);
17171
+ } else {
17172
+ const renderer = customRenderers[type];
17173
+ if (!renderer) {
17174
+ throw new Error(`Graphein: no renderer registered for chart type "${type}".`);
17175
+ }
17176
+ const rctx = {
17177
+ emphasis: null,
17178
+ store: createSelectionStore(),
17179
+ param: void 0,
17180
+ def: void 0,
17181
+ sourceData: src,
17182
+ framed: false,
17183
+ requestRedraw: () => {
17184
+ }
17185
+ };
17186
+ renderer(surface, effectiveSpec, tokens, size, rctx);
17187
+ }
17188
+ return buildRenderReport({ type, spec, data, tokens, size, model: reportModel });
17189
+ }
17190
+
16274
17191
  // src/index.ts
16275
17192
  var VERSION = "0.1.0";
16276
17193
 
16277
- export { CARTESIAN_TYPES, CHART_TYPES, CanvasLayer, DEFAULT_ENTRANCE_DURATION, DEFAULT_ROUGH_STYLE, DEFAULT_UPDATE_DURATION, DIM_ALPHA, ExpressionError, FACETABLE_TYPES2 as FACETABLE_TYPES, FUNCTION_NAMES, InteractionController, RoughPen, SKETCH_FONT_FAMILY, SKETCH_FONT_NAME, SLICER_TYPES, SR_ONLY_STYLE, Surface, TICK_SIZE, TIME_UNITS, TRANSFORM_KINDS, Tooltip, VERSION, aggregateValues, analyzeChart, animate, applyA11y, applyAggregate, applyBin, applyCalculate, applyFilter, applyFold, applyPatch, applyPick, applyTimeUnit, applyTransform, applyTransforms, arc, area, assertValidSpec, autoInsightAnnotations, backOut, bandScale, bounceOut, buildCartesianInteraction, buildCartesianModel, buildComboModel, buildDataTableFallback, buildFacetModels, buildHistogramModel, buildRenderReport, cartesianInteractionBuilders, cartesianRenderers, categorical, chartTitleText, chartTypeLabel, checkExpression, clamp012 as clamp01, clamp255, compileExpression, compilePredicate, computeBackingSize, computeBins, computeFrame, computeSeriesInsights, contrastRatio, createDiv, createRoughPen, createSelectionStore, cubicIn, cubicInOut, cubicOut, curveCatmullRom, curveLinear, curveMonotoneX, curveStep, curveStepAfter, curveStepBefore, customRenderers, darkTheme, decimate, dependentParams, diverging, divergingColorScale, divergingSchemes, drawAnnotationLabels, drawAnnotations, drawAxesUnderlay, drawFacet, drawLegend, drawOverlay, drawTitle, drawTrendlineLabels, drawTrendlines, easings, elasticOut, ensureSketchFont, expoInOut, expoOut, facetReport, fillParentStyle, filterRows, fontString, formatDate, formatNumber2 as formatNumber, formatValue, getDevicePixelRatio, groupBy, hashString, inferFieldType, inferFieldTypes, inferValueType, interpolateArray, interpolateNumber, interpolateNumberArray, interpolateObject, interpolateOklab, interpolateRgb, isBrowser, isEmptyValue, isFaceted, isParamClause, isSlicerType, keyFields, legendSwatchWidth, lightTheme, line, linear, linearRegression, linearScale, linearToSrgb, lintSpec, literalToValue, logScale, lttbRun, makeMatcher, matchesValue, measureText, monotoneXTangents, mulberry32, niceDomain, oklabToOklch, oklabToRgb, oklchToOklab, ordinalColorScale, overlayTextToCanvasCmd, paintCanvasText, paintLegendSwatch, parseColor, parseNumberFormat, pivot, pointScale, polygonHachureLines, prefersReducedMotion, prettyDate, quadIn, quadInOut, quadOut, rampFromStops, readableTextColor, relativeLuminance, render, renderDashboard, renderToContext, repairSpec, resolveCurve, resolveDashboardLayout, resolveDashboardSections, resolveEmphasis, resolveEntrance, resolveFilterValues, resolveInsightOptions, resolveSketch, resolveTheme, resolveUpdate, rgbToOklab, rgbaToCss, rotatePoint, roundedRect, sampleArc, sequential, sequentialColorScale, sequentialSchemes, setMeasureContext, setStyle, sinInOut, slicerParamName, smartDate, specFields, srgbToLinear, summarize, summarizeChart, themes, tickIncrement, tickStep, ticks, timeScale, timeTickFormat, timeTicks, toHex, toPointer, tooltipEnabled, truncateTo, validateDashboard, validateSpec, wireViews, withAlpha, withSketchFont };
17194
+ export { CARTESIAN_TYPES, CHART_TYPES, CanvasLayer, DEFAULT_ENTRANCE_DURATION, DEFAULT_ROUGH_STYLE, DEFAULT_UPDATE_DURATION, DIM_ALPHA, ExpressionError, FACETABLE_TYPES2 as FACETABLE_TYPES, FUNCTION_NAMES, InteractionController, RoughPen, SKETCH_FONT_FAMILY, SKETCH_FONT_NAME, SLICER_TYPES, SR_ONLY_STYLE, Surface, TICK_SIZE, TIME_UNITS, TRANSFORM_KINDS, Tooltip, VERSION, aggregateValues, analyzeChart, animate, applyA11y, applyAggregate, applyBin, applyCalculate, applyFilter, applyFold, applyPatch, applyPick, applyTimeUnit, applyTransform, applyTransforms, arc, area, assertValidSpec, autoInsightAnnotations, backOut, bandScale, bounceOut, buildCartesianInteraction, buildCartesianModel, buildComboModel, buildDataTableFallback, buildFacetModels, buildHistogramModel, buildRenderReport, cartesianInteractionBuilders, cartesianRenderers, categorical, chartTitleText, chartTypeLabel, checkExpression, clamp012 as clamp01, clamp255, compileExpression, compilePredicate, computeBackingSize, computeBins, computeFrame, computeSeriesInsights, contrastRatio, createDiv, createRoughPen, createSelectionStore, cubicIn, cubicInOut, cubicOut, curveCatmullRom, curveLinear, curveMonotoneX, curveStep, curveStepAfter, curveStepBefore, customRenderers, darkTheme, decimate, defaultSpan, dependentParams, diverging, divergingColorScale, divergingSchemes, drawAnnotationLabels, drawAnnotations, drawAxesUnderlay, drawFacet, drawLegend, drawOverlay, drawTitle, drawTrendlineLabels, drawTrendlines, easings, elasticOut, ensureSketchFont, expoInOut, expoOut, facetReport, fillParentStyle, filterRows, fontString, formatDate, formatNumber2 as formatNumber, formatValue, getDevicePixelRatio, groupBy, hashString, inferFieldType, inferFieldTypes, inferValueType, inheritInto, interpolateArray, interpolateNumber, interpolateNumberArray, interpolateObject, interpolateOklab, interpolateRgb, isBrowser, isEmptyValue, isFaceted, isParamClause, isSlicerType, keyFields, legendSwatchWidth, lightTheme, line, linear, linearRegression, linearScale, linearToSrgb, lintSpec, literalToValue, logScale, lttbRun, makeMatcher, matchesValue, measureText, monotoneXTangents, mulberry32, niceDomain, oklabToOklch, oklabToRgb, oklchToOklab, ordinalColorScale, overlayTextToCanvasCmd, paintCanvasText, paintLegendSwatch, parseColor, parseNumberFormat, pivot, placeViews, pointScale, polygonHachureLines, prefersReducedMotion, prettyDate, quadIn, quadInOut, quadOut, rampFromStops, readableTextColor, relativeLuminance, render, renderDashboard, renderToContext, repairSpec, resolveCurve, resolveDashboardLayout, resolveDashboardSections, resolveEmphasis, resolveEntrance, resolveFilterValues, resolveInsightOptions, resolveSketch, resolveTheme, resolveUpdate, rgbToOklab, rgbaToCss, rotatePoint, roundedRect, sampleArc, sequential, sequentialColorScale, sequentialSchemes, setMeasureContext, setStyle, sinInOut, slicerParamName, smartDate, specFields, srgbToLinear, summarize, summarizeChart, themes, tickIncrement, tickStep, ticks, timeScale, timeTickFormat, timeTicks, toHex, toPointer, tooltipEnabled, truncateTo, validateDashboard, validateSpec, wireViews, withAlpha, withSketchFont };
16278
17195
  //# sourceMappingURL=index.js.map
16279
17196
  //# sourceMappingURL=index.js.map