graphein 0.6.0 → 0.7.1
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.d.ts +23 -3
- package/dist/index.js +1037 -119
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
10255
|
-
|
|
10256
|
-
buildTable({
|
|
10257
|
-
container: host,
|
|
10258
|
-
tokens,
|
|
10259
|
-
rect,
|
|
10767
|
+
return {
|
|
10768
|
+
rows: result.rows,
|
|
10260
10769
|
columns,
|
|
10261
|
-
|
|
10262
|
-
|
|
10263
|
-
|
|
10264
|
-
|
|
10265
|
-
|
|
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
|
-
|
|
11113
|
-
|
|
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 {
|
|
@@ -15194,6 +15940,7 @@ var CANVAS_MARK_TYPES = /* @__PURE__ */ new Set([
|
|
|
15194
15940
|
"sankey",
|
|
15195
15941
|
"choropleth"
|
|
15196
15942
|
]);
|
|
15943
|
+
var EMPTY_DATA = [];
|
|
15197
15944
|
function resolveContainer(target) {
|
|
15198
15945
|
if (typeof target === "string") {
|
|
15199
15946
|
const el = document.querySelector(target);
|
|
@@ -15245,7 +15992,7 @@ function render(target, spec, options) {
|
|
|
15245
15992
|
let txDef;
|
|
15246
15993
|
let txOut = [];
|
|
15247
15994
|
const effectiveData2 = () => {
|
|
15248
|
-
const src = currentSpec.data ??
|
|
15995
|
+
const src = currentSpec.data ?? EMPTY_DATA;
|
|
15249
15996
|
const tx = currentSpec.transform;
|
|
15250
15997
|
if (!tx || tx.length === 0) return src;
|
|
15251
15998
|
if (txSrc === src && txDef === tx) return txOut;
|
|
@@ -15283,7 +16030,7 @@ function render(target, spec, options) {
|
|
|
15283
16030
|
const filterValues = resolveFilterValues(currentSpec.filter, store);
|
|
15284
16031
|
const baseData = effectiveData2();
|
|
15285
16032
|
const filtered = filterValues.length ? filterRows(baseData, filterValues) : baseData;
|
|
15286
|
-
const effectiveSpec = filtered ===
|
|
16033
|
+
const effectiveSpec = filtered === (currentSpec.data ?? EMPTY_DATA) ? currentSpec : { ...currentSpec, data: filtered };
|
|
15287
16034
|
const emphasis = resolveEmphasis(currentSpec.highlight, store, DIM_ALPHA);
|
|
15288
16035
|
const ownParam = currentSpec.params?.[0];
|
|
15289
16036
|
let interactionModel = null;
|
|
@@ -16271,9 +17018,180 @@ function renderDashboard(target, spec, options) {
|
|
|
16271
17018
|
};
|
|
16272
17019
|
}
|
|
16273
17020
|
|
|
17021
|
+
// src/runtime/dashboardHeadless.ts
|
|
17022
|
+
var HEADER_H = 44;
|
|
17023
|
+
var SECTION_TITLE_H = 26;
|
|
17024
|
+
function renderDashboardToContext(target, spec) {
|
|
17025
|
+
const ctx = target.marks;
|
|
17026
|
+
const tokens = resolveTheme(spec.theme);
|
|
17027
|
+
const layout2 = resolveDashboardLayout(spec.layout);
|
|
17028
|
+
const gap = layout2.gap;
|
|
17029
|
+
const pad3 = layout2.padding ?? Math.round(gap * 1.4);
|
|
17030
|
+
ctx.save();
|
|
17031
|
+
ctx.fillStyle = spec.background ?? tokens.color.background;
|
|
17032
|
+
ctx.fillRect(0, 0, target.width, target.height);
|
|
17033
|
+
ctx.restore();
|
|
17034
|
+
const titleText3 = typeof spec.title === "string" ? spec.title : spec.title?.text;
|
|
17035
|
+
let cursorY = pad3;
|
|
17036
|
+
if (titleText3 || spec.subtitle) {
|
|
17037
|
+
ctx.save();
|
|
17038
|
+
ctx.fillStyle = tokens.color.accent;
|
|
17039
|
+
ctx.fillRect(pad3, cursorY + 2, 5, 30);
|
|
17040
|
+
ctx.fillStyle = tokens.color.text;
|
|
17041
|
+
ctx.textBaseline = "top";
|
|
17042
|
+
ctx.font = `${tokens.font.weight.bold} ${tokens.font.size.title}px ${tokens.font.family}`;
|
|
17043
|
+
if (titleText3) ctx.fillText(titleText3, pad3 + 14, cursorY + 2);
|
|
17044
|
+
if (spec.subtitle) {
|
|
17045
|
+
ctx.fillStyle = tokens.color.textMuted;
|
|
17046
|
+
ctx.font = `${tokens.font.weight.normal} ${tokens.font.size.small}px ${tokens.font.family}`;
|
|
17047
|
+
ctx.fillText(spec.subtitle, pad3 + 14, cursorY + 24);
|
|
17048
|
+
}
|
|
17049
|
+
ctx.restore();
|
|
17050
|
+
cursorY += HEADER_H;
|
|
17051
|
+
}
|
|
17052
|
+
const wired = wireViews(spec.views, spec.interactions ?? "auto");
|
|
17053
|
+
const sections = resolveDashboardSections(wired, layout2);
|
|
17054
|
+
let markCount = 0;
|
|
17055
|
+
let allOk = true;
|
|
17056
|
+
let views = 0;
|
|
17057
|
+
for (const section of sections) {
|
|
17058
|
+
if (section.collapsed) continue;
|
|
17059
|
+
if (section.section?.title) {
|
|
17060
|
+
ctx.save();
|
|
17061
|
+
ctx.fillStyle = tokens.color.text;
|
|
17062
|
+
ctx.textBaseline = "top";
|
|
17063
|
+
ctx.font = `${tokens.font.weight.bold} ${tokens.font.size.base}px ${tokens.font.family}`;
|
|
17064
|
+
ctx.fillText(section.section.title, pad3, cursorY);
|
|
17065
|
+
ctx.restore();
|
|
17066
|
+
cursorY += SECTION_TITLE_H;
|
|
17067
|
+
}
|
|
17068
|
+
const cols = Math.max(1, section.cols);
|
|
17069
|
+
const innerW = target.width - pad3 * 2;
|
|
17070
|
+
const colW = (innerW - gap * (cols - 1)) / cols;
|
|
17071
|
+
const cells = placeViews(section.views, cols, layout2.preset);
|
|
17072
|
+
const flow = { x: 1, y: 1, rowH: 0 };
|
|
17073
|
+
let maxBottom = cursorY;
|
|
17074
|
+
for (const cell of cells) {
|
|
17075
|
+
let gx = cell.x;
|
|
17076
|
+
let gy = cell.y;
|
|
17077
|
+
if (gx == null || gy == null) {
|
|
17078
|
+
if (flow.x + cell.w - 1 > cols) {
|
|
17079
|
+
flow.x = 1;
|
|
17080
|
+
flow.y += Math.max(1, flow.rowH);
|
|
17081
|
+
flow.rowH = 0;
|
|
17082
|
+
}
|
|
17083
|
+
gx = flow.x;
|
|
17084
|
+
gy = flow.y;
|
|
17085
|
+
flow.x += cell.w;
|
|
17086
|
+
flow.rowH = Math.max(flow.rowH, cell.h);
|
|
17087
|
+
}
|
|
17088
|
+
const cx = pad3 + (gx - 1) * (colW + gap);
|
|
17089
|
+
const cy = cursorY + (gy - 1) * (section.rowHeight + gap);
|
|
17090
|
+
const cw = cell.w * colW + (cell.w - 1) * gap;
|
|
17091
|
+
const ch = cell.h * section.rowHeight + (cell.h - 1) * gap;
|
|
17092
|
+
const viewSpec = inheritInto(cell.view.spec, spec, tokens);
|
|
17093
|
+
ctx.save();
|
|
17094
|
+
ctx.translate(cx, cy);
|
|
17095
|
+
ctx.beginPath();
|
|
17096
|
+
ctx.rect(0, 0, cw, ch);
|
|
17097
|
+
ctx.clip();
|
|
17098
|
+
try {
|
|
17099
|
+
const r = renderToContext({ marks: ctx, interaction: target.interaction, width: cw, height: ch }, viewSpec);
|
|
17100
|
+
markCount += r.markCount;
|
|
17101
|
+
if (!r.ok) allOk = false;
|
|
17102
|
+
views++;
|
|
17103
|
+
} catch {
|
|
17104
|
+
allOk = false;
|
|
17105
|
+
}
|
|
17106
|
+
ctx.restore();
|
|
17107
|
+
maxBottom = Math.max(maxBottom, cy + ch);
|
|
17108
|
+
}
|
|
17109
|
+
cursorY = maxBottom + gap;
|
|
17110
|
+
}
|
|
17111
|
+
return {
|
|
17112
|
+
type: "dashboard",
|
|
17113
|
+
size: { width: target.width, height: target.height },
|
|
17114
|
+
markCount,
|
|
17115
|
+
seriesCount: views,
|
|
17116
|
+
colorCount: 1,
|
|
17117
|
+
ok: allOk,
|
|
17118
|
+
diagnostics: [],
|
|
17119
|
+
summary: `Dashboard of ${views} view${views === 1 ? "" : "s"}.`
|
|
17120
|
+
};
|
|
17121
|
+
}
|
|
17122
|
+
|
|
17123
|
+
// src/runtime/headless.ts
|
|
17124
|
+
function headlessTokens(spec) {
|
|
17125
|
+
const tokens = resolveTheme(spec.theme);
|
|
17126
|
+
const sketch = resolveSketch(spec);
|
|
17127
|
+
return sketch?.font ? withSketchFont(tokens) : tokens;
|
|
17128
|
+
}
|
|
17129
|
+
function renderToContext(target, spec) {
|
|
17130
|
+
const type = spec.type;
|
|
17131
|
+
if (type === "dashboard") {
|
|
17132
|
+
return renderDashboardToContext(target, spec);
|
|
17133
|
+
}
|
|
17134
|
+
const ctx = target.marks;
|
|
17135
|
+
const size = { width: target.width, height: target.height };
|
|
17136
|
+
const tokens = headlessTokens(spec);
|
|
17137
|
+
const surface = {
|
|
17138
|
+
headless: true,
|
|
17139
|
+
width: size.width,
|
|
17140
|
+
height: size.height,
|
|
17141
|
+
dpr: 1,
|
|
17142
|
+
marks: { ctx },
|
|
17143
|
+
interaction: { ctx: target.interaction ?? ctx }
|
|
17144
|
+
};
|
|
17145
|
+
const bg = spec.background ?? tokens.color.background;
|
|
17146
|
+
ctx.save();
|
|
17147
|
+
ctx.fillStyle = bg;
|
|
17148
|
+
ctx.fillRect(0, 0, size.width, size.height);
|
|
17149
|
+
ctx.restore();
|
|
17150
|
+
const src = spec.data ?? [];
|
|
17151
|
+
const data = spec.transform?.length ? applyTransforms(spec.transform, src) : src;
|
|
17152
|
+
const effectiveSpec = data === src ? spec : { ...spec, data };
|
|
17153
|
+
if (isFaceted(effectiveSpec)) {
|
|
17154
|
+
const layout2 = buildFacetModels(effectiveSpec, tokens, size);
|
|
17155
|
+
if (layout2) {
|
|
17156
|
+
drawFacet(surface, effectiveSpec, layout2, tokens);
|
|
17157
|
+
return facetReport(effectiveSpec, layout2, tokens, size);
|
|
17158
|
+
}
|
|
17159
|
+
}
|
|
17160
|
+
let reportModel;
|
|
17161
|
+
if (CARTESIAN_TYPES.has(type)) {
|
|
17162
|
+
const renderer = cartesianRenderers[type];
|
|
17163
|
+
const model = buildCartesianModel(effectiveSpec, tokens, size);
|
|
17164
|
+
reportModel = model;
|
|
17165
|
+
drawAxesUnderlay(surface, model);
|
|
17166
|
+
if (renderer) renderer(surface, model);
|
|
17167
|
+
drawTrendlines(surface, model);
|
|
17168
|
+
drawAnnotations(surface, model);
|
|
17169
|
+
drawOverlay(surface, model);
|
|
17170
|
+
drawAnnotationLabels(surface, model);
|
|
17171
|
+
drawTrendlineLabels(surface, model);
|
|
17172
|
+
} else {
|
|
17173
|
+
const renderer = customRenderers[type];
|
|
17174
|
+
if (!renderer) {
|
|
17175
|
+
throw new Error(`Graphein: no renderer registered for chart type "${type}".`);
|
|
17176
|
+
}
|
|
17177
|
+
const rctx = {
|
|
17178
|
+
emphasis: null,
|
|
17179
|
+
store: createSelectionStore(),
|
|
17180
|
+
param: void 0,
|
|
17181
|
+
def: void 0,
|
|
17182
|
+
sourceData: src,
|
|
17183
|
+
framed: false,
|
|
17184
|
+
requestRedraw: () => {
|
|
17185
|
+
}
|
|
17186
|
+
};
|
|
17187
|
+
renderer(surface, effectiveSpec, tokens, size, rctx);
|
|
17188
|
+
}
|
|
17189
|
+
return buildRenderReport({ type, spec, data, tokens, size, model: reportModel });
|
|
17190
|
+
}
|
|
17191
|
+
|
|
16274
17192
|
// src/index.ts
|
|
16275
17193
|
var VERSION = "0.1.0";
|
|
16276
17194
|
|
|
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 };
|
|
17195
|
+
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
17196
|
//# sourceMappingURL=index.js.map
|
|
16279
17197
|
//# sourceMappingURL=index.js.map
|