graphein 0.5.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.d.ts +23 -3
- package/dist/index.js +1326 -193
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -456,6 +456,10 @@ function logScale(opts) {
|
|
|
456
456
|
map(value) {
|
|
457
457
|
const [d0, d1] = transformedDomain;
|
|
458
458
|
const [r0, r1] = range;
|
|
459
|
+
if (!Number.isFinite(value) || value * sign2 <= 0) return NaN;
|
|
460
|
+
if (d0 === d1) {
|
|
461
|
+
return clamp11 ? clampValue2(r0, Math.min(r0, r1), Math.max(r0, r1)) : r0;
|
|
462
|
+
}
|
|
459
463
|
const transformed = transform(value, sign2, base);
|
|
460
464
|
const mapped = r0 + (transformed - d0) / (d1 - d0) * (r1 - r0);
|
|
461
465
|
return clamp11 ? clampValue2(mapped, Math.min(r0, r1), Math.max(r0, r1)) : mapped;
|
|
@@ -463,6 +467,7 @@ function logScale(opts) {
|
|
|
463
467
|
invert(pixel) {
|
|
464
468
|
const [d0, d1] = transformedDomain;
|
|
465
469
|
const [r0, r1] = range;
|
|
470
|
+
if (d0 === d1) return domain[0];
|
|
466
471
|
const input = clamp11 ? clampValue2(pixel, Math.min(r0, r1), Math.max(r0, r1)) : pixel;
|
|
467
472
|
return untransform(d0 + (input - r0) / (r1 - r0) * (d1 - d0), sign2, base);
|
|
468
473
|
},
|
|
@@ -1938,6 +1943,10 @@ var SI_SYMBOLS = [
|
|
|
1938
1943
|
function groupThousands(intDigits) {
|
|
1939
1944
|
return intDigits.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
1940
1945
|
}
|
|
1946
|
+
function clampDigits(value, min, max) {
|
|
1947
|
+
if (!Number.isFinite(value)) return min;
|
|
1948
|
+
return Math.min(max, Math.max(min, Math.trunc(value)));
|
|
1949
|
+
}
|
|
1941
1950
|
function applyGroup(numStr, group) {
|
|
1942
1951
|
if (!group) return numStr;
|
|
1943
1952
|
const neg = numStr.startsWith("-");
|
|
@@ -1957,11 +1966,11 @@ function formatSI(value, precision) {
|
|
|
1957
1966
|
if (abs >= factor) {
|
|
1958
1967
|
const scaled = value / factor;
|
|
1959
1968
|
const digits2 = precision ?? (Math.abs(scaled) >= 100 ? 0 : Math.abs(scaled) >= 10 ? 1 : 2);
|
|
1960
|
-
return trimZeros(scaled.toFixed(digits2)) + symbol;
|
|
1969
|
+
return trimZeros(scaled.toFixed(clampDigits(digits2, 0, 100))) + symbol;
|
|
1961
1970
|
}
|
|
1962
1971
|
}
|
|
1963
1972
|
const digits = precision ?? (abs >= 100 || abs === 0 ? 0 : abs >= 1 ? 1 : 2);
|
|
1964
|
-
return trimZeros(value.toFixed(digits));
|
|
1973
|
+
return trimZeros(value.toFixed(clampDigits(digits, 0, 100)));
|
|
1965
1974
|
}
|
|
1966
1975
|
function formatNumber2(value, spec) {
|
|
1967
1976
|
if (value == null || Number.isNaN(value)) return "";
|
|
@@ -1971,10 +1980,10 @@ function formatNumber2(value, spec) {
|
|
|
1971
1980
|
let body;
|
|
1972
1981
|
switch (type) {
|
|
1973
1982
|
case "f":
|
|
1974
|
-
body = applyGroup(value.toFixed(precision ?? 2), group);
|
|
1983
|
+
body = applyGroup(value.toFixed(clampDigits(precision ?? 2, 0, 100)), group);
|
|
1975
1984
|
break;
|
|
1976
1985
|
case "%":
|
|
1977
|
-
body = applyGroup((value * 100).toFixed(precision ?? 0), group) + "%";
|
|
1986
|
+
body = applyGroup((value * 100).toFixed(clampDigits(precision ?? 0, 0, 100)), group) + "%";
|
|
1978
1987
|
break;
|
|
1979
1988
|
case "s":
|
|
1980
1989
|
body = formatSI(value, precision);
|
|
@@ -1983,17 +1992,17 @@ function formatNumber2(value, spec) {
|
|
|
1983
1992
|
body = applyGroup(Math.round(value).toString(), group);
|
|
1984
1993
|
break;
|
|
1985
1994
|
case "e":
|
|
1986
|
-
body = value.toExponential(precision ?? 2);
|
|
1995
|
+
body = value.toExponential(clampDigits(precision ?? 2, 0, 100));
|
|
1987
1996
|
break;
|
|
1988
1997
|
case "g":
|
|
1989
|
-
body = trimZeros(Number(value.toPrecision(precision ?? 6)).toString());
|
|
1998
|
+
body = trimZeros(Number(value.toPrecision(clampDigits(precision ?? 6, 1, 100))).toString());
|
|
1990
1999
|
body = applyGroup(body, group);
|
|
1991
2000
|
break;
|
|
1992
2001
|
default: {
|
|
1993
2002
|
if (Number.isInteger(value)) {
|
|
1994
2003
|
body = applyGroup(value.toString(), group);
|
|
1995
2004
|
} else {
|
|
1996
|
-
const digits = precision ?? 6;
|
|
2005
|
+
const digits = clampDigits(precision ?? 6, 0, 100);
|
|
1997
2006
|
body = applyGroup(trimZeros(value.toFixed(digits)), group);
|
|
1998
2007
|
}
|
|
1999
2008
|
}
|
|
@@ -2122,7 +2131,13 @@ function toNumber(value) {
|
|
|
2122
2131
|
}
|
|
2123
2132
|
return NaN;
|
|
2124
2133
|
}
|
|
2134
|
+
var ISO_YEAR_RE = /^(\d{4})$/;
|
|
2125
2135
|
var ISO_DATE_RE = /^(\d{4})-(\d{2})(?:-(\d{2}))?$/;
|
|
2136
|
+
var ISO_DATETIME_RE = /^(\d{4})-(\d{2})-(\d{2})[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?(?:Z|[+-]\d{2}:?\d{2})?$/;
|
|
2137
|
+
function isLocalCalendarDate(year, month, day) {
|
|
2138
|
+
const d = new Date(year, month - 1, day);
|
|
2139
|
+
return d.getFullYear() === year && d.getMonth() === month - 1 && d.getDate() === day;
|
|
2140
|
+
}
|
|
2126
2141
|
function toDate(value) {
|
|
2127
2142
|
if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value;
|
|
2128
2143
|
if (typeof value === "number") {
|
|
@@ -2130,8 +2145,20 @@ function toDate(value) {
|
|
|
2130
2145
|
return Number.isNaN(d.getTime()) ? null : d;
|
|
2131
2146
|
}
|
|
2132
2147
|
if (typeof value === "string") {
|
|
2148
|
+
const y = ISO_YEAR_RE.exec(value);
|
|
2149
|
+
if (y) {
|
|
2150
|
+
const d = new Date(Date.parse(value));
|
|
2151
|
+
return d.getUTCFullYear() === +y[1] && d.getUTCMonth() === 0 && d.getUTCDate() === 1 ? d : null;
|
|
2152
|
+
}
|
|
2133
2153
|
const m = ISO_DATE_RE.exec(value);
|
|
2134
|
-
if (m)
|
|
2154
|
+
if (m) {
|
|
2155
|
+
const year = +m[1];
|
|
2156
|
+
const month = +m[2];
|
|
2157
|
+
const day = m[3] ? +m[3] : 1;
|
|
2158
|
+
return isLocalCalendarDate(year, month, day) ? new Date(year, month - 1, day) : null;
|
|
2159
|
+
}
|
|
2160
|
+
const dt = ISO_DATETIME_RE.exec(value);
|
|
2161
|
+
if (dt && !isLocalCalendarDate(+dt[1], +dt[2], +dt[3])) return null;
|
|
2135
2162
|
const ms = Date.parse(value);
|
|
2136
2163
|
return Number.isNaN(ms) ? null : new Date(ms);
|
|
2137
2164
|
}
|
|
@@ -2527,7 +2554,7 @@ function coerceFinite(value) {
|
|
|
2527
2554
|
if (value === null || value === void 0) {
|
|
2528
2555
|
return null;
|
|
2529
2556
|
}
|
|
2530
|
-
const numberValue =
|
|
2557
|
+
const numberValue = toNumber(value);
|
|
2531
2558
|
return Number.isFinite(numberValue) ? numberValue : null;
|
|
2532
2559
|
}
|
|
2533
2560
|
function makePathKey(path) {
|
|
@@ -3561,7 +3588,7 @@ function computeBins(values, opts = {}) {
|
|
|
3561
3588
|
[min, max] = opts.extent;
|
|
3562
3589
|
} else {
|
|
3563
3590
|
for (const v of values) {
|
|
3564
|
-
if (Number.
|
|
3591
|
+
if (!Number.isFinite(v)) continue;
|
|
3565
3592
|
if (v < min) min = v;
|
|
3566
3593
|
if (v > max) max = v;
|
|
3567
3594
|
}
|
|
@@ -3585,7 +3612,7 @@ function applyBin(transform2, data) {
|
|
|
3585
3612
|
const finite4 = [];
|
|
3586
3613
|
for (const row of data) {
|
|
3587
3614
|
const n = toNumber(read(row));
|
|
3588
|
-
if (
|
|
3615
|
+
if (Number.isFinite(n)) finite4.push(n);
|
|
3589
3616
|
}
|
|
3590
3617
|
const layout2 = computeBins(finite4, {
|
|
3591
3618
|
maxbins: transform2.maxbins,
|
|
@@ -3599,7 +3626,7 @@ function applyBin(transform2, data) {
|
|
|
3599
3626
|
const out = { ...row };
|
|
3600
3627
|
const v = toNumber(read(row));
|
|
3601
3628
|
const outside = transform2.extent != null && (v < transform2.extent[0] || v > transform2.extent[1]);
|
|
3602
|
-
if (layout2 == null || Number.
|
|
3629
|
+
if (layout2 == null || !Number.isFinite(v) || outside) {
|
|
3603
3630
|
out[startField] = null;
|
|
3604
3631
|
if (endField) out[endField] = null;
|
|
3605
3632
|
return out;
|
|
@@ -4646,6 +4673,26 @@ function validateSpec(spec) {
|
|
|
4646
4673
|
}
|
|
4647
4674
|
}
|
|
4648
4675
|
}
|
|
4676
|
+
if (isObject3(spec.encoding)) {
|
|
4677
|
+
for (const ch of ["x", "y"]) {
|
|
4678
|
+
const channel = spec.encoding[ch];
|
|
4679
|
+
if (!isObject3(channel) || !isObject3(channel.scale)) continue;
|
|
4680
|
+
const scale = channel.scale;
|
|
4681
|
+
if (scale.type !== "log") continue;
|
|
4682
|
+
const domain = scale.domain;
|
|
4683
|
+
if (Array.isArray(domain) && domain.some((v) => typeof v === "number" && v <= 0)) {
|
|
4684
|
+
warn(
|
|
4685
|
+
`encoding.${ch}.scale.domain`,
|
|
4686
|
+
'A "log" scale needs a strictly positive domain; values <= 0 have no logarithm. The domain will be clamped to the positive data range.',
|
|
4687
|
+
{
|
|
4688
|
+
rule: "log-scale-nonpositive-domain",
|
|
4689
|
+
severity: "warning",
|
|
4690
|
+
fix: [{ op: "remove", path: toPointer(`encoding.${ch}.scale.domain`) }]
|
|
4691
|
+
}
|
|
4692
|
+
);
|
|
4693
|
+
}
|
|
4694
|
+
}
|
|
4695
|
+
}
|
|
4649
4696
|
if (type === "kpi") {
|
|
4650
4697
|
const value = spec.value;
|
|
4651
4698
|
if (value === void 0) {
|
|
@@ -6277,6 +6324,21 @@ function stackedYExtent(rows, xField, yField) {
|
|
|
6277
6324
|
for (const v of neg.values()) if (v < min) min = v;
|
|
6278
6325
|
return [min, max];
|
|
6279
6326
|
}
|
|
6327
|
+
function positiveLogDomain(requested, data, field, base) {
|
|
6328
|
+
let lo = requested[0];
|
|
6329
|
+
let hi = requested[1];
|
|
6330
|
+
if (!(lo > 0) || !(hi > 0)) {
|
|
6331
|
+
const read = accessor(field);
|
|
6332
|
+
const positives = data.map((d) => toNumber(read(d))).filter((v) => Number.isFinite(v) && v > 0);
|
|
6333
|
+
const pmin = positives.length ? Math.min(...positives) : 1;
|
|
6334
|
+
const pmax = positives.length ? Math.max(...positives) : base;
|
|
6335
|
+
if (!(lo > 0)) lo = pmin;
|
|
6336
|
+
if (!(hi > 0)) hi = pmax;
|
|
6337
|
+
}
|
|
6338
|
+
if (hi < lo) [lo, hi] = [hi, lo];
|
|
6339
|
+
if (lo === hi) hi = lo * base;
|
|
6340
|
+
return [lo, hi];
|
|
6341
|
+
}
|
|
6280
6342
|
function resolveSeriesField(spec) {
|
|
6281
6343
|
const enc = spec.encoding;
|
|
6282
6344
|
if (enc.series?.field) return enc.series.field;
|
|
@@ -6338,7 +6400,8 @@ function buildCartesianModel(spec, tokens, opts) {
|
|
|
6338
6400
|
let categories;
|
|
6339
6401
|
let xDomainNum = null;
|
|
6340
6402
|
if (xKind === "band" || xKind === "point") {
|
|
6341
|
-
|
|
6403
|
+
const cfgDomain = enc.x.scale?.domain;
|
|
6404
|
+
categories = opts.shared?.categories ?? (Array.isArray(cfgDomain) && typeof cfgDomain[0] === "string" ? cfgDomain : uniqueStrings(data, xField));
|
|
6342
6405
|
} else if (xKind === "time") {
|
|
6343
6406
|
if (opts.shared?.xDomain) {
|
|
6344
6407
|
xDomainNum = opts.shared.xDomain;
|
|
@@ -6351,12 +6414,23 @@ function buildCartesianModel(spec, tokens, opts) {
|
|
|
6351
6414
|
} else {
|
|
6352
6415
|
xDomainNum = extent(data, xField) ?? [0, 1];
|
|
6353
6416
|
const xScaleCfg = enc.x.scale;
|
|
6354
|
-
if (xScaleCfg?.
|
|
6417
|
+
if (xScaleCfg?.type === "log") {
|
|
6418
|
+
xDomainNum = positiveLogDomain(
|
|
6419
|
+
xScaleCfg.domain && typeof xScaleCfg.domain[0] === "number" ? [xScaleCfg.domain[0], xScaleCfg.domain[1]] : xDomainNum,
|
|
6420
|
+
data,
|
|
6421
|
+
xField,
|
|
6422
|
+
xScaleCfg.base ?? 10
|
|
6423
|
+
);
|
|
6424
|
+
} else if (xScaleCfg?.domain && typeof xScaleCfg.domain[0] === "number") {
|
|
6355
6425
|
xDomainNum = [xScaleCfg.domain[0], xScaleCfg.domain[1]];
|
|
6356
6426
|
} else if (xScaleCfg?.nice !== false) {
|
|
6357
6427
|
xDomainNum = niceDomain(xDomainNum[0], xDomainNum[1], 10);
|
|
6358
6428
|
}
|
|
6359
6429
|
}
|
|
6430
|
+
const yScaleCfg = enc.y.scale;
|
|
6431
|
+
const yIsLog = yScaleCfg?.type === "log";
|
|
6432
|
+
const yBase = yScaleCfg?.base ?? 10;
|
|
6433
|
+
const yClamp = yScaleCfg?.clamp === true;
|
|
6360
6434
|
const sharedY = opts.shared?.yDomain;
|
|
6361
6435
|
let yDomain;
|
|
6362
6436
|
if (sharedY) {
|
|
@@ -6367,24 +6441,39 @@ function buildCartesianModel(spec, tokens, opts) {
|
|
|
6367
6441
|
const ext = extent(data, yField) ?? [0, 1];
|
|
6368
6442
|
yDomain = [ext[0], ext[1]];
|
|
6369
6443
|
}
|
|
6370
|
-
|
|
6444
|
+
const forceZero = yIsLog ? false : yScaleCfg?.zero ?? wantsZeroBaseline(spec);
|
|
6445
|
+
if (forceZero && !sharedY) {
|
|
6371
6446
|
yDomain = [Math.min(0, yDomain[0]), Math.max(0, yDomain[1])];
|
|
6372
6447
|
}
|
|
6373
|
-
const yScaleCfg = enc.y.scale;
|
|
6374
6448
|
const xAxisCfg = axisCfg(spec, "x");
|
|
6375
6449
|
const yAxisCfg = axisCfg(spec, "y");
|
|
6376
6450
|
const yTickCount = yAxisCfg.ticks ?? 6;
|
|
6451
|
+
const yExplicitTicks = yAxisCfg.tickValues && yAxisCfg.tickValues.length ? yAxisCfg.tickValues.slice() : null;
|
|
6377
6452
|
const yExplicit = yScaleCfg?.domain && typeof yScaleCfg.domain[0] === "number" || !!sharedY;
|
|
6378
6453
|
let yTickValues;
|
|
6379
|
-
if (
|
|
6454
|
+
if (yIsLog) {
|
|
6455
|
+
if (yScaleCfg?.domain && typeof yScaleCfg.domain[0] === "number") {
|
|
6456
|
+
yDomain = positiveLogDomain([yScaleCfg.domain[0], yScaleCfg.domain[1]], data, yField, yBase);
|
|
6457
|
+
} else if (!sharedY) {
|
|
6458
|
+
yDomain = positiveLogDomain(yDomain, data, yField, yBase);
|
|
6459
|
+
}
|
|
6460
|
+
const lo = Math.min(yDomain[0], yDomain[1]);
|
|
6461
|
+
const hi = Math.max(yDomain[0], yDomain[1]);
|
|
6462
|
+
yTickValues = (yExplicitTicks ?? logScale({ domain: yDomain, range: [0, 1], base: yBase }).ticks()).filter(
|
|
6463
|
+
(v) => v > 0 && v >= lo - 1e-9 && v <= hi + 1e-9
|
|
6464
|
+
);
|
|
6465
|
+
} else if (yExplicit) {
|
|
6380
6466
|
if (yScaleCfg?.domain && typeof yScaleCfg.domain[0] === "number") {
|
|
6381
6467
|
yDomain = [yScaleCfg.domain[0], yScaleCfg.domain[1]];
|
|
6382
6468
|
}
|
|
6383
6469
|
const lo = Math.min(yDomain[0], yDomain[1]);
|
|
6384
6470
|
const hi = Math.max(yDomain[0], yDomain[1]);
|
|
6385
|
-
yTickValues = numericTickValues(yDomain[0], yDomain[1], yTickCount).filter(
|
|
6471
|
+
yTickValues = (yExplicitTicks ?? numericTickValues(yDomain[0], yDomain[1], yTickCount)).filter(
|
|
6386
6472
|
(v) => v >= lo - 1e-9 && v <= hi + 1e-9
|
|
6387
6473
|
);
|
|
6474
|
+
} else if (yExplicitTicks) {
|
|
6475
|
+
yTickValues = yExplicitTicks;
|
|
6476
|
+
yDomain = [Math.min(yDomain[0], ...yExplicitTicks), Math.max(yDomain[1], ...yExplicitTicks)];
|
|
6388
6477
|
} else if (yScaleCfg?.nice !== false) {
|
|
6389
6478
|
yTickValues = numericTickValues(yDomain[0], yDomain[1], yTickCount);
|
|
6390
6479
|
if (yTickValues.length >= 2) {
|
|
@@ -6412,7 +6501,19 @@ function buildCartesianModel(spec, tokens, opts) {
|
|
|
6412
6501
|
xStepMs = xt.length > 1 ? xt[1] - xt[0] : 0;
|
|
6413
6502
|
xLabels = xt.map((v) => enc.x.format ? formatValue(v, enc.x.format) : smartDate(v, xStepMs));
|
|
6414
6503
|
} else {
|
|
6415
|
-
const
|
|
6504
|
+
const xLo = Math.min(xDomainNum[0], xDomainNum[1]);
|
|
6505
|
+
const xHi = Math.max(xDomainNum[0], xDomainNum[1]);
|
|
6506
|
+
const xExplicitTicks = xAxisCfg.tickValues && xAxisCfg.tickValues.length ? xAxisCfg.tickValues.slice() : null;
|
|
6507
|
+
let xt;
|
|
6508
|
+
if (xExplicitTicks) {
|
|
6509
|
+
xt = xExplicitTicks.filter((v) => v >= xLo - 1e-9 && v <= xHi + 1e-9);
|
|
6510
|
+
} else if (enc.x.scale?.type === "log") {
|
|
6511
|
+
xt = logScale({ domain: xDomainNum, range: [0, 1], base: enc.x.scale.base ?? 10 }).ticks().filter((v) => v > 0 && v >= xLo - 1e-9 && v <= xHi + 1e-9);
|
|
6512
|
+
} else {
|
|
6513
|
+
xt = numericTickValues(xDomainNum[0], xDomainNum[1], xAxisCfg.ticks ?? 8).filter(
|
|
6514
|
+
(v) => v >= xLo - 1e-9 && v <= xHi + 1e-9
|
|
6515
|
+
);
|
|
6516
|
+
}
|
|
6416
6517
|
xTickValues = xt;
|
|
6417
6518
|
xLabels = xt.map((v) => formatNumericTick(v, enc.x.format ?? xAxisCfg.format));
|
|
6418
6519
|
}
|
|
@@ -6439,9 +6540,10 @@ function buildCartesianModel(spec, tokens, opts) {
|
|
|
6439
6540
|
yAxis: { show: yShow, labels: yLabels, title: resolveAxisTitle(enc.y, yAxisCfg) }
|
|
6440
6541
|
});
|
|
6441
6542
|
const plot = frame.plot;
|
|
6442
|
-
const
|
|
6543
|
+
const yRange = [plot.y + plot.height, plot.y];
|
|
6544
|
+
const yScale = yIsLog ? logScale({ domain: yDomain, range: yRange, base: yBase, clamp: yClamp }) : linearScale({ domain: yDomain, range: yRange, clamp: yClamp });
|
|
6443
6545
|
const yPixel = (value) => yScale.map(toNumber(value));
|
|
6444
|
-
const baseline = clampPx(yScale.map(0), plot.y, plot.y + plot.height);
|
|
6546
|
+
const baseline = yIsLog ? plot.y + plot.height : clampPx(yScale.map(0), plot.y, plot.y + plot.height);
|
|
6445
6547
|
const xModel = buildXModel(xKind, {
|
|
6446
6548
|
field: xField,
|
|
6447
6549
|
type: xType,
|
|
@@ -6517,7 +6619,8 @@ function buildXModel(kind, args) {
|
|
|
6517
6619
|
};
|
|
6518
6620
|
}
|
|
6519
6621
|
const domain = args.domainNum ?? [0, 1];
|
|
6520
|
-
const
|
|
6622
|
+
const clamp11 = args.scaleCfg?.clamp === true;
|
|
6623
|
+
const continuous = kind === "time" ? timeScale({ domain, range: args.range }) : args.scaleCfg?.type === "log" ? logScale({ domain, range: args.range, base: args.scaleCfg.base ?? 10, clamp: clamp11 }) : linearScale({ domain, range: args.range, clamp: clamp11 });
|
|
6521
6624
|
return {
|
|
6522
6625
|
kind,
|
|
6523
6626
|
field: args.field,
|
|
@@ -7231,6 +7334,12 @@ function boxProbe(model, geom) {
|
|
|
7231
7334
|
if (model.seriesField) probe[model.seriesField] = geom.seriesValue;
|
|
7232
7335
|
return probe;
|
|
7233
7336
|
}
|
|
7337
|
+
function hitsBoxGeom(model, geom, showOutliers, px, py) {
|
|
7338
|
+
const inWhiskerExtent = px >= geom.left - 4 && px <= geom.right + 4 && py >= geom.top - 6 && py <= geom.bottom + 6;
|
|
7339
|
+
if (inWhiskerExtent) return true;
|
|
7340
|
+
if (!showOutliers || geom.stats.outliers.length === 0) return false;
|
|
7341
|
+
return geom.stats.outliers.some((o) => Math.abs(px - geom.cx) <= 6 && Math.abs(py - model.y.pixel(o)) <= 6);
|
|
7342
|
+
}
|
|
7234
7343
|
function drawBox(surface, model) {
|
|
7235
7344
|
const layout2 = computeBoxes(model);
|
|
7236
7345
|
if (!layout2) return;
|
|
@@ -7262,9 +7371,7 @@ function buildBoxInteraction(model) {
|
|
|
7262
7371
|
return {
|
|
7263
7372
|
region: model.plot,
|
|
7264
7373
|
hitTest: (px, py) => {
|
|
7265
|
-
const hit = geoms.find(
|
|
7266
|
-
(g) => px >= g.left - 4 && px <= g.right + 4 && py >= g.top - 6 && py <= g.bottom + 6
|
|
7267
|
-
);
|
|
7374
|
+
const hit = geoms.find((g) => hitsBoxGeom(model, g, showOutliers, px, py));
|
|
7268
7375
|
if (!hit) return null;
|
|
7269
7376
|
const s = hit.stats;
|
|
7270
7377
|
const rows = [
|
|
@@ -7297,10 +7404,15 @@ function buildBoxInteraction(model) {
|
|
|
7297
7404
|
};
|
|
7298
7405
|
},
|
|
7299
7406
|
pick: (px, py) => {
|
|
7300
|
-
const hit = geoms.find(
|
|
7301
|
-
(g) => px >= g.left - 4 && px <= g.right + 4 && py >= g.top - 6 && py <= g.bottom + 6
|
|
7302
|
-
);
|
|
7407
|
+
const hit = geoms.find((g) => hitsBoxGeom(model, g, showOutliers, px, py));
|
|
7303
7408
|
if (!hit) return null;
|
|
7409
|
+
if (model.series.length > 1 && model.seriesField && hit.seriesValue !== void 0) {
|
|
7410
|
+
return {
|
|
7411
|
+
kind: "point",
|
|
7412
|
+
fields: [model.x.field, model.seriesField],
|
|
7413
|
+
tuples: [[hit.xValue, hit.seriesValue]]
|
|
7414
|
+
};
|
|
7415
|
+
}
|
|
7304
7416
|
return { kind: "point", fields: [model.x.field], tuples: [[hit.xValue]] };
|
|
7305
7417
|
}
|
|
7306
7418
|
};
|
|
@@ -7557,6 +7669,18 @@ function measureMax(labels, font) {
|
|
|
7557
7669
|
for (const label of labels) max = Math.max(max, measureText(label, font).width);
|
|
7558
7670
|
return max;
|
|
7559
7671
|
}
|
|
7672
|
+
function heatmapColorDomain(rows, field) {
|
|
7673
|
+
const read = accessor(field);
|
|
7674
|
+
let min = Infinity;
|
|
7675
|
+
let max = -Infinity;
|
|
7676
|
+
for (const row of rows) {
|
|
7677
|
+
const value = toNumber(read(row));
|
|
7678
|
+
if (!finite3(value)) continue;
|
|
7679
|
+
min = Math.min(min, value);
|
|
7680
|
+
max = Math.max(max, value);
|
|
7681
|
+
}
|
|
7682
|
+
return min === Infinity ? null : [min, max];
|
|
7683
|
+
}
|
|
7560
7684
|
function drawRoundedCell(ctx, x, y, width, height, radius) {
|
|
7561
7685
|
if (!finite3(x) || !finite3(y) || !finite3(width) || !finite3(height) || width <= 0 || height <= 0) return;
|
|
7562
7686
|
ctx.beginPath();
|
|
@@ -7605,7 +7729,7 @@ function drawHeatmap(surface, spec, tokens, size, context) {
|
|
|
7605
7729
|
const { x, y, color } = heatmap.encoding;
|
|
7606
7730
|
const xCats = uniqueStrings(rows, x.field);
|
|
7607
7731
|
const yCats = uniqueStrings(rows, y.field);
|
|
7608
|
-
const colorDomain =
|
|
7732
|
+
const colorDomain = heatmapColorDomain(rows, color.field);
|
|
7609
7733
|
if (xCats.length === 0 || yCats.length === 0 || !colorDomain || content.width <= 0 || content.height <= 0) return;
|
|
7610
7734
|
const labelSize = tokens.font.size.small;
|
|
7611
7735
|
const labelFont = fontString(labelSize, tokens.font.family, tokens.font.weight.normal);
|
|
@@ -7772,16 +7896,33 @@ function clamp3(value, min, max) {
|
|
|
7772
7896
|
function direction(angle) {
|
|
7773
7897
|
return { x: Math.sin(angle), y: -Math.cos(angle) };
|
|
7774
7898
|
}
|
|
7775
|
-
function
|
|
7776
|
-
const
|
|
7777
|
-
const
|
|
7778
|
-
return
|
|
7899
|
+
function reconcilePercents(values) {
|
|
7900
|
+
const cleaned = values.map((value) => Number.isFinite(value) && value > 0 ? value : 0);
|
|
7901
|
+
const total = cleaned.reduce((sum, value) => sum + value, 0);
|
|
7902
|
+
if (total <= 0) return cleaned.map(() => 0);
|
|
7903
|
+
const exact = cleaned.map((value, index) => {
|
|
7904
|
+
const pct = value / total * 100;
|
|
7905
|
+
const floor = Math.floor(pct);
|
|
7906
|
+
return { index, floor, remainder: pct - floor };
|
|
7907
|
+
});
|
|
7908
|
+
let remaining = 100 - exact.reduce((sum, item) => sum + item.floor, 0);
|
|
7909
|
+
const out = exact.map((item) => item.floor);
|
|
7910
|
+
for (const item of [...exact].sort((a, b) => b.remainder - a.remainder || a.index - b.index)) {
|
|
7911
|
+
if (remaining <= 0) break;
|
|
7912
|
+
out[item.index]++;
|
|
7913
|
+
remaining--;
|
|
7914
|
+
}
|
|
7915
|
+
return out;
|
|
7916
|
+
}
|
|
7917
|
+
function formatPercent(percent) {
|
|
7918
|
+
return `${percent}%`;
|
|
7779
7919
|
}
|
|
7780
|
-
function labelText(slice, placement, options) {
|
|
7920
|
+
function labelText(slice, placement, options, percents) {
|
|
7781
7921
|
const content = options.content ?? (placement === "inside" ? "percent" : "category-percent");
|
|
7922
|
+
const percentText = formatPercent(percents.get(slice.index) ?? 0);
|
|
7782
7923
|
switch (content) {
|
|
7783
7924
|
case "percent":
|
|
7784
|
-
return
|
|
7925
|
+
return percentText;
|
|
7785
7926
|
case "value":
|
|
7786
7927
|
return options.formatValue(slice.value);
|
|
7787
7928
|
case "category":
|
|
@@ -7790,7 +7931,7 @@ function labelText(slice, placement, options) {
|
|
|
7790
7931
|
return `${slice.label} \xB7 ${options.formatValue(slice.value)}`;
|
|
7791
7932
|
case "category-percent":
|
|
7792
7933
|
default:
|
|
7793
|
-
return `${slice.label} \xB7 ${
|
|
7934
|
+
return `${slice.label} \xB7 ${percentText}`;
|
|
7794
7935
|
}
|
|
7795
7936
|
}
|
|
7796
7937
|
function relaxColumn(idealYs, lineHeight2, top, bottom) {
|
|
@@ -7820,6 +7961,10 @@ function planPieLabels(options) {
|
|
|
7820
7961
|
const origInnerR = options.innerR;
|
|
7821
7962
|
const insideSlices = [];
|
|
7822
7963
|
const outsideSlices = [];
|
|
7964
|
+
const percents = /* @__PURE__ */ new Map();
|
|
7965
|
+
reconcilePercents(slices.map((slice) => slice.value)).forEach((percent, i) => {
|
|
7966
|
+
percents.set(slices[i].index, percent);
|
|
7967
|
+
});
|
|
7823
7968
|
const insideLabelR = origInnerR > 0 ? (origInnerR + origOuterR) / 2 : origOuterR * 0.62;
|
|
7824
7969
|
const radialBand = origInnerR > 0 ? origOuterR - origInnerR : origOuterR;
|
|
7825
7970
|
for (const slice of slices) {
|
|
@@ -7829,20 +7974,20 @@ function planPieLabels(options) {
|
|
|
7829
7974
|
if (placement === "inside") goesInside = true;
|
|
7830
7975
|
else if (placement === "outside") goesInside = false;
|
|
7831
7976
|
else {
|
|
7832
|
-
const insideText = labelText(slice, "inside", options);
|
|
7977
|
+
const insideText = labelText(slice, "inside", options, percents);
|
|
7833
7978
|
const tangential = (slice.endAngle - slice.startAngle) * insideLabelR;
|
|
7834
7979
|
const fitsTangential = measure(insideText) + INSIDE_PAD <= tangential;
|
|
7835
7980
|
const fitsRadial = lineHeight2 * 0.8 <= radialBand;
|
|
7836
7981
|
goesInside = fitsTangential && fitsRadial;
|
|
7837
7982
|
}
|
|
7838
7983
|
if (goesInside) {
|
|
7839
|
-
insideSlices.push({ slice, mid, text: labelText(slice, "inside", options) });
|
|
7984
|
+
insideSlices.push({ slice, mid, text: labelText(slice, "inside", options, percents) });
|
|
7840
7985
|
} else {
|
|
7841
7986
|
const dir = direction(mid);
|
|
7842
7987
|
outsideSlices.push({
|
|
7843
7988
|
slice,
|
|
7844
7989
|
mid,
|
|
7845
|
-
text: labelText(slice, "outside", options),
|
|
7990
|
+
text: labelText(slice, "outside", options, percents),
|
|
7846
7991
|
side: dir.x >= 0 ? "right" : "left"
|
|
7847
7992
|
});
|
|
7848
7993
|
}
|
|
@@ -8229,17 +8374,29 @@ function buildStages(spec, palette) {
|
|
|
8229
8374
|
order.push(key);
|
|
8230
8375
|
}
|
|
8231
8376
|
}
|
|
8232
|
-
|
|
8377
|
+
const stages = [];
|
|
8378
|
+
for (const key of order) {
|
|
8233
8379
|
const { category, value } = totals.get(key);
|
|
8380
|
+
if (value <= 0) continue;
|
|
8234
8381
|
const rgba2 = colorScale.map(key);
|
|
8235
|
-
|
|
8382
|
+
stages.push({
|
|
8236
8383
|
key,
|
|
8237
8384
|
label: key === "" ? "(blank)" : key,
|
|
8238
8385
|
value,
|
|
8239
8386
|
category,
|
|
8240
8387
|
color: rgbaToCss(rgba2),
|
|
8241
8388
|
rgba: rgba2
|
|
8242
|
-
};
|
|
8389
|
+
});
|
|
8390
|
+
}
|
|
8391
|
+
return stages;
|
|
8392
|
+
}
|
|
8393
|
+
function safeRatio(numerator, denominator) {
|
|
8394
|
+
return Number.isFinite(numerator) && Number.isFinite(denominator) && denominator !== 0 ? numerator / denominator : 0;
|
|
8395
|
+
}
|
|
8396
|
+
function funnelPercents(stages, mode = "first") {
|
|
8397
|
+
return stages.map((stage, i) => {
|
|
8398
|
+
if (mode === "previous") return i === 0 ? 1 : safeRatio(stage.value, stages[i - 1].value);
|
|
8399
|
+
return safeRatio(stage.value, stages[0]?.value ?? 0);
|
|
8243
8400
|
});
|
|
8244
8401
|
}
|
|
8245
8402
|
function drawFunnel(surface, spec, tokens, size, context) {
|
|
@@ -8277,12 +8434,10 @@ function drawFunnel(surface, spec, tokens, size, context) {
|
|
|
8277
8434
|
const n = stages.length;
|
|
8278
8435
|
const gap = Math.min(6, Math.max(2, content.height / n / 12));
|
|
8279
8436
|
const bandH = content.height / n;
|
|
8280
|
-
const widthOf = (v) => v / maxValue * funnelW;
|
|
8437
|
+
const widthOf = (v) => Math.max(0, v) / maxValue * funnelW;
|
|
8281
8438
|
const alphaOf = (s) => emphasis ? emphasis.match({ [stageField]: s.category }) ? 1 : emphasis.dim : 1;
|
|
8282
|
-
const
|
|
8283
|
-
|
|
8284
|
-
return stages[i].value / stages[0].value;
|
|
8285
|
-
};
|
|
8439
|
+
const percents = funnelPercents(stages, mode);
|
|
8440
|
+
const pctOf = (i) => percents[i] ?? 0;
|
|
8286
8441
|
const sketch = resolveSketch(spec);
|
|
8287
8442
|
const pen = sketch ? new RoughPen(ctx, sketch) : null;
|
|
8288
8443
|
const bands = [];
|
|
@@ -8426,8 +8581,8 @@ function valueField(ref) {
|
|
|
8426
8581
|
function formatDelta(delta, specFormat) {
|
|
8427
8582
|
const sign2 = delta >= 0 ? "+" : "-";
|
|
8428
8583
|
const abs = Math.abs(delta);
|
|
8429
|
-
if (abs <= 1) return sign2 + formatNumber2(abs, ".1%");
|
|
8430
8584
|
if (specFormat) return sign2 + formatValue(abs, specFormat);
|
|
8585
|
+
if (abs <= 1) return sign2 + formatNumber2(abs, ".1%");
|
|
8431
8586
|
return sign2 + formatNumber2(abs, ",.0f");
|
|
8432
8587
|
}
|
|
8433
8588
|
function cardRectFor(surface, tokens, size, spec) {
|
|
@@ -8584,6 +8739,10 @@ function buildSparklineCanvas(tokens, width, height, values, sketch) {
|
|
|
8584
8739
|
}
|
|
8585
8740
|
function drawKpi(surface, spec, tokens, size, context) {
|
|
8586
8741
|
const kpi = spec;
|
|
8742
|
+
if (surface.headless) {
|
|
8743
|
+
drawKpiCanvas(surface, kpi, tokens, size, context);
|
|
8744
|
+
return;
|
|
8745
|
+
}
|
|
8587
8746
|
const data = kpi.data ?? [];
|
|
8588
8747
|
const value = resolveValue(kpi.value, data);
|
|
8589
8748
|
const delta = resolveValue(kpi.delta, data);
|
|
@@ -8652,6 +8811,116 @@ function drawKpi(surface, spec, tokens, size, context) {
|
|
|
8652
8811
|
}
|
|
8653
8812
|
}
|
|
8654
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
|
+
}
|
|
8655
8924
|
|
|
8656
8925
|
// src/charts/sankey.ts
|
|
8657
8926
|
var center = (n) => (n.y0 + n.y1) / 2;
|
|
@@ -8698,11 +8967,13 @@ function buildGraph(spec, palette) {
|
|
|
8698
8967
|
target.targetLinks.push(link);
|
|
8699
8968
|
links.push(link);
|
|
8700
8969
|
}
|
|
8701
|
-
|
|
8702
|
-
|
|
8703
|
-
|
|
8704
|
-
|
|
8970
|
+
const feedback = removeFeedbackLinks(order);
|
|
8971
|
+
if (feedback.size > 0) {
|
|
8972
|
+
for (let i = links.length - 1; i >= 0; i -= 1) {
|
|
8973
|
+
if (feedback.has(links[i])) links.splice(i, 1);
|
|
8974
|
+
}
|
|
8705
8975
|
}
|
|
8976
|
+
computeNodeValues(order);
|
|
8706
8977
|
assignLayers(order);
|
|
8707
8978
|
const maxLayer = order.reduce((m, n) => Math.max(m, n.layer), 0);
|
|
8708
8979
|
const columns = Array.from({ length: maxLayer + 1 }, () => []);
|
|
@@ -8710,6 +8981,7 @@ function buildGraph(spec, palette) {
|
|
|
8710
8981
|
return { nodes: order, links, columns, maxLayer };
|
|
8711
8982
|
}
|
|
8712
8983
|
function assignLayers(nodes) {
|
|
8984
|
+
for (const n of nodes) n.layer = 0;
|
|
8713
8985
|
const indeg = /* @__PURE__ */ new Map();
|
|
8714
8986
|
for (const n of nodes) indeg.set(n, n.targetLinks.length);
|
|
8715
8987
|
const queue = nodes.filter((n) => (indeg.get(n) ?? 0) === 0);
|
|
@@ -8725,14 +8997,58 @@ function assignLayers(nodes) {
|
|
|
8725
8997
|
if (d === 0) queue.push(t);
|
|
8726
8998
|
}
|
|
8727
8999
|
}
|
|
9000
|
+
for (const n of nodes) {
|
|
9001
|
+
if (!seen.has(n)) {
|
|
9002
|
+
seen.add(n);
|
|
9003
|
+
const incoming = n.targetLinks.reduce((m, l) => Math.max(m, l.source.layer + 1), 0);
|
|
9004
|
+
n.layer = Math.max(n.layer, incoming);
|
|
9005
|
+
}
|
|
9006
|
+
}
|
|
8728
9007
|
const maxLayer = nodes.reduce((m, n) => Math.max(m, n.layer), 0);
|
|
8729
9008
|
for (const n of nodes) {
|
|
8730
9009
|
if (n.sourceLinks.length === 0 && n.targetLinks.length > 0) n.layer = maxLayer;
|
|
8731
9010
|
}
|
|
8732
9011
|
}
|
|
9012
|
+
function removeFeedbackLinks(nodes) {
|
|
9013
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
9014
|
+
const visited = /* @__PURE__ */ new Set();
|
|
9015
|
+
const feedback = /* @__PURE__ */ new Set();
|
|
9016
|
+
const visit = (n) => {
|
|
9017
|
+
visiting.add(n);
|
|
9018
|
+
for (const link of n.sourceLinks) {
|
|
9019
|
+
const target = link.target;
|
|
9020
|
+
if (visiting.has(target)) {
|
|
9021
|
+
feedback.add(link);
|
|
9022
|
+
continue;
|
|
9023
|
+
}
|
|
9024
|
+
if (!visited.has(target)) visit(target);
|
|
9025
|
+
}
|
|
9026
|
+
visiting.delete(n);
|
|
9027
|
+
visited.add(n);
|
|
9028
|
+
};
|
|
9029
|
+
for (const n of nodes) {
|
|
9030
|
+
if (!visited.has(n)) visit(n);
|
|
9031
|
+
}
|
|
9032
|
+
if (feedback.size === 0) return feedback;
|
|
9033
|
+
for (const n of nodes) {
|
|
9034
|
+
n.sourceLinks = n.sourceLinks.filter((l) => !feedback.has(l));
|
|
9035
|
+
n.targetLinks = n.targetLinks.filter((l) => !feedback.has(l));
|
|
9036
|
+
}
|
|
9037
|
+
return feedback;
|
|
9038
|
+
}
|
|
9039
|
+
function computeNodeValues(nodes) {
|
|
9040
|
+
for (const n of nodes) {
|
|
9041
|
+
const out = n.sourceLinks.reduce((s, l) => s + l.value, 0);
|
|
9042
|
+
const inc = n.targetLinks.reduce((s, l) => s + l.value, 0);
|
|
9043
|
+
n.value = Math.max(out, inc);
|
|
9044
|
+
}
|
|
9045
|
+
}
|
|
8733
9046
|
function layout(graph, area2, nodeWidth, pad3) {
|
|
8734
9047
|
const { columns, links, maxLayer } = graph;
|
|
8735
9048
|
const kx = maxLayer > 0 ? (area2.width - nodeWidth) / maxLayer : 0;
|
|
9049
|
+
const maxColumnSize = columns.reduce((m, col) => Math.max(m, col.length), 0);
|
|
9050
|
+
const desiredPad = Math.max(0, pad3);
|
|
9051
|
+
const effectivePad = maxColumnSize > 1 && desiredPad * (maxColumnSize - 1) >= area2.height && area2.height > 0 ? area2.height * 0.25 / (maxColumnSize - 1) : desiredPad;
|
|
8736
9052
|
for (const col of columns) {
|
|
8737
9053
|
for (const n of col) {
|
|
8738
9054
|
n.x0 = area2.x + n.layer * kx;
|
|
@@ -8743,26 +9059,26 @@ function layout(graph, area2, nodeWidth, pad3) {
|
|
|
8743
9059
|
for (const col of columns) {
|
|
8744
9060
|
if (col.length === 0) continue;
|
|
8745
9061
|
const sum = col.reduce((s, n) => s + n.value, 0);
|
|
8746
|
-
const avail = area2.height - (col.length - 1) *
|
|
8747
|
-
if (sum > 0
|
|
9062
|
+
const avail = Math.max(0, area2.height - (col.length - 1) * effectivePad);
|
|
9063
|
+
if (sum > 0) ky = Math.min(ky, avail / sum);
|
|
8748
9064
|
}
|
|
8749
|
-
if (!Number.isFinite(ky) || ky
|
|
9065
|
+
if (!Number.isFinite(ky) || ky < 0) ky = 0;
|
|
8750
9066
|
for (const col of columns) {
|
|
8751
9067
|
let y = area2.y;
|
|
8752
9068
|
for (const n of col) {
|
|
8753
9069
|
n.y0 = y;
|
|
8754
|
-
n.y1 = y + Math.max(
|
|
8755
|
-
y = n.y1 +
|
|
9070
|
+
n.y1 = y + Math.max(0, n.value * ky);
|
|
9071
|
+
y = n.y1 + effectivePad;
|
|
8756
9072
|
}
|
|
8757
9073
|
centerColumn(col, area2);
|
|
8758
9074
|
}
|
|
8759
|
-
for (const l of links) l.width = Math.max(
|
|
9075
|
+
for (const l of links) l.width = Math.max(0, l.value * ky);
|
|
8760
9076
|
for (let i = 0; i < 6; i += 1) {
|
|
8761
9077
|
const alpha = Math.pow(0.97, i);
|
|
8762
9078
|
relax(columns, true, alpha);
|
|
8763
|
-
resolveAll(columns, area2,
|
|
9079
|
+
resolveAll(columns, area2, effectivePad);
|
|
8764
9080
|
relax(columns, false, alpha);
|
|
8765
|
-
resolveAll(columns, area2,
|
|
9081
|
+
resolveAll(columns, area2, effectivePad);
|
|
8766
9082
|
}
|
|
8767
9083
|
computeLinkBreadths(graph);
|
|
8768
9084
|
}
|
|
@@ -9004,6 +9320,7 @@ function featureKey(feature, featureId) {
|
|
|
9004
9320
|
if (featureId === "id" && feature.id != null) return toKey(feature.id);
|
|
9005
9321
|
const v = feature.properties?.[featureId];
|
|
9006
9322
|
if (v != null) return toKey(v);
|
|
9323
|
+
return "";
|
|
9007
9324
|
}
|
|
9008
9325
|
if (feature.id != null) return toKey(feature.id);
|
|
9009
9326
|
const props = feature.properties ?? {};
|
|
@@ -9419,6 +9736,43 @@ function buildTable(opts) {
|
|
|
9419
9736
|
if (footer) table.appendChild(footer);
|
|
9420
9737
|
container.appendChild(table);
|
|
9421
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
|
+
}
|
|
9422
9776
|
var FILL_EPS = 0.5;
|
|
9423
9777
|
var cachedScrollbarWidth = null;
|
|
9424
9778
|
function scrollbarWidth() {
|
|
@@ -9766,6 +10120,263 @@ function densityMetrics(density) {
|
|
|
9766
10120
|
if (density === "compact") return { rowHeight: 24, headerHeight: 28, padding: "3px 8px", paddingX: 8 };
|
|
9767
10121
|
return { rowHeight: DEFAULT_BODY_ROW_HEIGHT, headerHeight: HEADER_ROW_HEIGHT, padding: "6px 10px", paddingX: 10 };
|
|
9768
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
|
+
}
|
|
9769
10380
|
function applyStickyColumn(cell, opts, colIndex, isHeader) {
|
|
9770
10381
|
if (colIndex == null || colIndex >= (opts.rowHeaderSpan ?? 0)) return;
|
|
9771
10382
|
cell.style.position = "sticky";
|
|
@@ -9822,36 +10433,71 @@ var ROW_HEIGHT = 30;
|
|
|
9822
10433
|
var BUFFER_ROWS = 8;
|
|
9823
10434
|
function drawTable(surface, spec, tokens, size) {
|
|
9824
10435
|
if (spec.type !== "table") return;
|
|
10436
|
+
if (surface.headless) {
|
|
10437
|
+
drawTableCanvas(surface, spec, tokens, size);
|
|
10438
|
+
return;
|
|
10439
|
+
}
|
|
9825
10440
|
renderTable(surface, spec, tokens, size);
|
|
9826
10441
|
}
|
|
9827
|
-
function
|
|
10442
|
+
function drawTableCanvas(surface, spec, tokens, size) {
|
|
9828
10443
|
const rect = drawTitleBlock(surface, tokens, size, spec.title);
|
|
9829
10444
|
const data = spec.data ?? [];
|
|
9830
10445
|
const columns = resolveColumns(data, spec);
|
|
9831
10446
|
const read = columns.map((column) => accessor(column.key));
|
|
9832
|
-
const
|
|
9833
|
-
|
|
9834
|
-
let sortedRows = sortRows(data, columns, read, sortState);
|
|
10447
|
+
const sortState = initialSortState(spec, columns);
|
|
10448
|
+
const sortedRows = sortRows(data, columns, read, sortState);
|
|
9835
10449
|
const domains = columns.map(
|
|
9836
10450
|
(column, colIndex) => resolveConditionalDomain(
|
|
9837
10451
|
column.conditionalFormat,
|
|
9838
10452
|
data.map((row) => numericRaw(read[colIndex](row)))
|
|
9839
10453
|
)
|
|
9840
10454
|
);
|
|
9841
|
-
|
|
9842
|
-
|
|
9843
|
-
|
|
9844
|
-
|
|
9845
|
-
|
|
9846
|
-
|
|
9847
|
-
|
|
9848
|
-
|
|
9849
|
-
|
|
9850
|
-
|
|
9851
|
-
|
|
9852
|
-
|
|
9853
|
-
|
|
9854
|
-
|
|
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
|
+
}
|
|
10473
|
+
function renderTable(surface, spec, tokens, size) {
|
|
10474
|
+
const rect = drawTitleBlock(surface, tokens, size, spec.title);
|
|
10475
|
+
const data = spec.data ?? [];
|
|
10476
|
+
const columns = resolveColumns(data, spec);
|
|
10477
|
+
const read = columns.map((column) => accessor(column.key));
|
|
10478
|
+
const sketch = resolveSketch(spec) != null;
|
|
10479
|
+
let sortState = initialSortState(spec, columns);
|
|
10480
|
+
let sortedRows = sortRows(data, columns, read, sortState);
|
|
10481
|
+
const domains = columns.map(
|
|
10482
|
+
(column, colIndex) => resolveConditionalDomain(
|
|
10483
|
+
column.conditionalFormat,
|
|
10484
|
+
data.map((row) => numericRaw(read[colIndex](row)))
|
|
10485
|
+
)
|
|
10486
|
+
);
|
|
10487
|
+
const host = document.createElement("div");
|
|
10488
|
+
surface.overlay.appendChild(host);
|
|
10489
|
+
let frame = 0;
|
|
10490
|
+
const renderWindow = () => {
|
|
10491
|
+
const scrollTop = host.scrollTop || 0;
|
|
10492
|
+
const rowHeight = densityRowHeight(spec.density);
|
|
10493
|
+
const start = Math.max(0, Math.floor(scrollTop / rowHeight) - BUFFER_ROWS);
|
|
10494
|
+
const visible = Math.ceil(Math.max(rect.height, host.clientHeight || rect.height) / rowHeight) + BUFFER_ROWS * 2;
|
|
10495
|
+
const end = Math.min(sortedRows.length, start + visible);
|
|
10496
|
+
buildTable({
|
|
10497
|
+
container: host,
|
|
10498
|
+
tokens,
|
|
10499
|
+
rect,
|
|
10500
|
+
columns,
|
|
9855
10501
|
rowCount: sortedRows.length,
|
|
9856
10502
|
getCell(rowIndex, colIndex) {
|
|
9857
10503
|
const value = read[colIndex](sortedRows[rowIndex]);
|
|
@@ -9994,10 +10640,80 @@ function requestFrame(callback) {
|
|
|
9994
10640
|
var PATH_SEPARATOR2 = "\0";
|
|
9995
10641
|
function drawMatrix(surface, spec, tokens, size) {
|
|
9996
10642
|
if (spec.type !== "matrix") return;
|
|
10643
|
+
if (surface.headless) {
|
|
10644
|
+
drawMatrixCanvas(surface, spec, tokens, size);
|
|
10645
|
+
return;
|
|
10646
|
+
}
|
|
9997
10647
|
renderMatrix(surface, spec, tokens, size);
|
|
9998
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
|
+
}
|
|
9999
10681
|
function renderMatrix(surface, spec, tokens, size) {
|
|
10000
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) {
|
|
10001
10717
|
const data = spec.data ?? [];
|
|
10002
10718
|
const result = pivot(data, {
|
|
10003
10719
|
rows: spec.rows,
|
|
@@ -10048,37 +10764,14 @@ function renderMatrix(surface, spec, tokens, size) {
|
|
|
10048
10764
|
)
|
|
10049
10765
|
);
|
|
10050
10766
|
});
|
|
10051
|
-
|
|
10052
|
-
|
|
10053
|
-
buildTable({
|
|
10054
|
-
container: host,
|
|
10055
|
-
tokens,
|
|
10056
|
-
rect,
|
|
10767
|
+
return {
|
|
10768
|
+
rows: result.rows,
|
|
10057
10769
|
columns,
|
|
10058
|
-
|
|
10059
|
-
|
|
10060
|
-
|
|
10061
|
-
|
|
10062
|
-
|
|
10063
|
-
const value = row.cellsByColumnKey.get(leaf.path.join(PATH_SEPARATOR2))?.values[valueIndex] ?? null;
|
|
10064
|
-
const display = displayMatrixValue(value, rowIndex, colIndex - 1, denominators, spec);
|
|
10065
|
-
return { value: display, raw: display };
|
|
10066
|
-
},
|
|
10067
|
-
rowClass(rowIndex) {
|
|
10068
|
-
const row = result.rows[rowIndex];
|
|
10069
|
-
return row.isGrandTotal === true ? "grandtotal" : row.isSubtotal === true ? "subtotal" : "normal";
|
|
10070
|
-
},
|
|
10071
|
-
cellIndent(rowIndex, colIndex) {
|
|
10072
|
-
return colIndex === 0 ? result.rows[rowIndex].depth * 16 : 0;
|
|
10073
|
-
},
|
|
10074
|
-
rowHeaderSpan: 1,
|
|
10075
|
-
stickyHeader: true,
|
|
10076
|
-
striped: false,
|
|
10077
|
-
conditionalDomains: bodyDomains,
|
|
10078
|
-
headerRows: buildMatrixHeaderRows(result.columnTree, sortedLeaves, spec, sortedLeaves !== result.columnLeaves),
|
|
10079
|
-
density: spec.density,
|
|
10080
|
-
sketch: resolveSketch(spec) != null
|
|
10081
|
-
});
|
|
10770
|
+
leafValueColumns,
|
|
10771
|
+
denominators,
|
|
10772
|
+
bodyDomains,
|
|
10773
|
+
headerRows: buildMatrixHeaderRows(result.columnTree, sortedLeaves, spec, sortedLeaves !== result.columnLeaves)
|
|
10774
|
+
};
|
|
10082
10775
|
}
|
|
10083
10776
|
function rowLabel(row) {
|
|
10084
10777
|
if (row.isGrandTotal === true) return row.label;
|
|
@@ -10659,6 +11352,10 @@ function emptyNotice(host, message, color) {
|
|
|
10659
11352
|
// src/charts/slicers/dropdown.ts
|
|
10660
11353
|
function drawDropdown(surface, spec, tokens, size, context) {
|
|
10661
11354
|
const s = spec;
|
|
11355
|
+
if (surface.headless) {
|
|
11356
|
+
drawDropdownCanvas(surface, s, tokens, size, context);
|
|
11357
|
+
return;
|
|
11358
|
+
}
|
|
10662
11359
|
const shell = mountSlicerShell(surface, tokens, size, {
|
|
10663
11360
|
title: s.title,
|
|
10664
11361
|
label: slicerLabel(s)
|
|
@@ -10739,10 +11436,61 @@ function drawDropdown(surface, spec, tokens, size, context) {
|
|
|
10739
11436
|
trigger.addEventListener("click", openMenu);
|
|
10740
11437
|
refreshChrome();
|
|
10741
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
|
+
}
|
|
10742
11486
|
|
|
10743
11487
|
// src/charts/slicers/search.ts
|
|
10744
11488
|
function drawSearch(surface, spec, tokens, size, context) {
|
|
10745
11489
|
const s = spec;
|
|
11490
|
+
if (surface.headless) {
|
|
11491
|
+
drawSearchCanvas(surface, s, tokens, size, context);
|
|
11492
|
+
return;
|
|
11493
|
+
}
|
|
10746
11494
|
const shell = mountSlicerShell(surface, tokens, size, {
|
|
10747
11495
|
title: s.title,
|
|
10748
11496
|
label: slicerLabel(s)
|
|
@@ -10782,10 +11530,47 @@ function drawSearch(surface, spec, tokens, size, context) {
|
|
|
10782
11530
|
});
|
|
10783
11531
|
shell.setClear(initial ? clear : null);
|
|
10784
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
|
+
}
|
|
10785
11566
|
|
|
10786
11567
|
// src/charts/slicers/list.ts
|
|
10787
11568
|
function drawList(surface, spec, tokens, size, context) {
|
|
10788
11569
|
const s = spec;
|
|
11570
|
+
if (surface.headless) {
|
|
11571
|
+
drawListCanvas(surface, s, tokens, size, context);
|
|
11572
|
+
return;
|
|
11573
|
+
}
|
|
10789
11574
|
const shell = mountSlicerShell(surface, tokens, size, {
|
|
10790
11575
|
title: s.title,
|
|
10791
11576
|
label: slicerLabel(s)
|
|
@@ -10909,10 +11694,95 @@ function drawList(surface, spec, tokens, size, context) {
|
|
|
10909
11694
|
rebuild();
|
|
10910
11695
|
shell.setClear(selected.size ? clearAll : null);
|
|
10911
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
|
+
});
|
|
11748
|
+
}
|
|
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();
|
|
11777
|
+
}
|
|
10912
11778
|
|
|
10913
11779
|
// src/charts/slicers/range.ts
|
|
10914
11780
|
function drawRange(surface, spec, tokens, size, context) {
|
|
10915
11781
|
const s = spec;
|
|
11782
|
+
if (surface.headless) {
|
|
11783
|
+
drawRangeCanvas(surface, s, tokens, size, context);
|
|
11784
|
+
return;
|
|
11785
|
+
}
|
|
10916
11786
|
const shell = mountSlicerShell(surface, tokens, size, {
|
|
10917
11787
|
title: s.title,
|
|
10918
11788
|
label: slicerLabel(s)
|
|
@@ -10954,6 +11824,80 @@ function drawRange(surface, spec, tokens, size, context) {
|
|
|
10954
11824
|
const active = current?.kind === "range" && (low > min || high < max);
|
|
10955
11825
|
shell.setClear(active ? clear : null);
|
|
10956
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
|
+
}
|
|
10957
11901
|
|
|
10958
11902
|
// src/charts/slicers/dateRange.ts
|
|
10959
11903
|
var DAY_MS = 864e5;
|
|
@@ -10978,6 +11922,10 @@ function toISODate(ms) {
|
|
|
10978
11922
|
}
|
|
10979
11923
|
function drawDateRange(surface, spec, tokens, size, context) {
|
|
10980
11924
|
const s = spec;
|
|
11925
|
+
if (surface.headless) {
|
|
11926
|
+
drawDateRangeCanvas(surface, s, tokens, size, context);
|
|
11927
|
+
return;
|
|
11928
|
+
}
|
|
10981
11929
|
const shell = mountSlicerShell(surface, tokens, size, {
|
|
10982
11930
|
title: s.title,
|
|
10983
11931
|
label: slicerLabel(s)
|
|
@@ -11050,6 +11998,89 @@ function drawDateRange(surface, spec, tokens, size, context) {
|
|
|
11050
11998
|
const active = current?.kind === "range" && (low > min || high < max);
|
|
11051
11999
|
shell.setClear(active ? clear : null);
|
|
11052
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
|
+
}
|
|
11053
12084
|
|
|
11054
12085
|
// src/interaction/cartesian.ts
|
|
11055
12086
|
var HIT_RADIUS = 26;
|
|
@@ -12015,6 +13046,12 @@ function buildGroups(leaves) {
|
|
|
12015
13046
|
}
|
|
12016
13047
|
return order.map((key) => groups.get(key));
|
|
12017
13048
|
}
|
|
13049
|
+
function tileProbe(treemap, tile) {
|
|
13050
|
+
const probe = { [treemap.encoding.category.field]: tile.category };
|
|
13051
|
+
const groupField = treemap.encoding.group?.field;
|
|
13052
|
+
if (groupField) probe[groupField] = tile.group;
|
|
13053
|
+
return probe;
|
|
13054
|
+
}
|
|
12018
13055
|
function drawRect2(ctx, pen, rect, fill, stroke) {
|
|
12019
13056
|
if (rect.width <= 0 || rect.height <= 0) return;
|
|
12020
13057
|
if (pen) {
|
|
@@ -12059,6 +13096,7 @@ function addTileLabels(surface, tokens, tile, valueFormat) {
|
|
|
12059
13096
|
function drawTreemap(surface, spec, tokens, size, context) {
|
|
12060
13097
|
const treemap = spec;
|
|
12061
13098
|
const ctx = surface.marks.ctx;
|
|
13099
|
+
const emphasis = context?.emphasis ?? null;
|
|
12062
13100
|
const content = drawTitleBlock(surface, tokens, size, treemap.title);
|
|
12063
13101
|
const leaves = buildLeaves(treemap);
|
|
12064
13102
|
colorLeaves(leaves, treemap, tokens);
|
|
@@ -12080,6 +13118,12 @@ function drawTreemap(surface, spec, tokens, size, context) {
|
|
|
12080
13118
|
const tiles = [];
|
|
12081
13119
|
const stroke = tokens.color.background;
|
|
12082
13120
|
const groupRects = [];
|
|
13121
|
+
const drawTile = (tile) => {
|
|
13122
|
+
const alpha = emphasis ? emphasis.match(tileProbe(treemap, tile)) ? 1 : emphasis.dim : 1;
|
|
13123
|
+
if (alpha !== 1) ctx.globalAlpha = alpha;
|
|
13124
|
+
drawRect2(ctx, pen, tile.rect, tile.fillCss, stroke);
|
|
13125
|
+
if (alpha !== 1) ctx.globalAlpha = 1;
|
|
13126
|
+
};
|
|
12083
13127
|
ctx.save();
|
|
12084
13128
|
if (treemap.encoding.group) {
|
|
12085
13129
|
const groups = buildGroups(leaves);
|
|
@@ -12094,14 +13138,14 @@ function drawTreemap(surface, spec, tokens, size, context) {
|
|
|
12094
13138
|
for (const leafNode of layoutTreemap(groupNode.item.leaves, inner)) {
|
|
12095
13139
|
const tile = { ...leafNode.item, rect: leafNode.rect };
|
|
12096
13140
|
tiles.push(tile);
|
|
12097
|
-
|
|
13141
|
+
drawTile(tile);
|
|
12098
13142
|
}
|
|
12099
13143
|
}
|
|
12100
13144
|
} else {
|
|
12101
13145
|
for (const leafNode of layoutTreemap(leaves, content)) {
|
|
12102
13146
|
const tile = { ...leafNode.item, rect: leafNode.rect };
|
|
12103
13147
|
tiles.push(tile);
|
|
12104
|
-
|
|
13148
|
+
drawTile(tile);
|
|
12105
13149
|
}
|
|
12106
13150
|
}
|
|
12107
13151
|
ctx.restore();
|
|
@@ -14863,88 +15907,6 @@ function facetReport(spec, layout2, tokens, size) {
|
|
|
14863
15907
|
};
|
|
14864
15908
|
}
|
|
14865
15909
|
|
|
14866
|
-
// src/runtime/headless.ts
|
|
14867
|
-
var DOM_ONLY_TYPES = /* @__PURE__ */ new Set([
|
|
14868
|
-
"kpi",
|
|
14869
|
-
"table",
|
|
14870
|
-
"matrix",
|
|
14871
|
-
"dropdown",
|
|
14872
|
-
"list",
|
|
14873
|
-
"search",
|
|
14874
|
-
"range",
|
|
14875
|
-
"dateRange",
|
|
14876
|
-
"dashboard"
|
|
14877
|
-
]);
|
|
14878
|
-
function headlessTokens(spec) {
|
|
14879
|
-
const tokens = resolveTheme(spec.theme);
|
|
14880
|
-
const sketch = resolveSketch(spec);
|
|
14881
|
-
return sketch?.font ? withSketchFont(tokens) : tokens;
|
|
14882
|
-
}
|
|
14883
|
-
function renderToContext(target, spec) {
|
|
14884
|
-
const type = spec.type;
|
|
14885
|
-
if (DOM_ONLY_TYPES.has(type)) {
|
|
14886
|
-
throw new Error(
|
|
14887
|
-
`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).`
|
|
14888
|
-
);
|
|
14889
|
-
}
|
|
14890
|
-
const ctx = target.marks;
|
|
14891
|
-
const size = { width: target.width, height: target.height };
|
|
14892
|
-
const tokens = headlessTokens(spec);
|
|
14893
|
-
const surface = {
|
|
14894
|
-
headless: true,
|
|
14895
|
-
width: size.width,
|
|
14896
|
-
height: size.height,
|
|
14897
|
-
dpr: 1,
|
|
14898
|
-
marks: { ctx },
|
|
14899
|
-
interaction: { ctx: target.interaction ?? ctx }
|
|
14900
|
-
};
|
|
14901
|
-
const bg = spec.background ?? tokens.color.background;
|
|
14902
|
-
ctx.save();
|
|
14903
|
-
ctx.fillStyle = bg;
|
|
14904
|
-
ctx.fillRect(0, 0, size.width, size.height);
|
|
14905
|
-
ctx.restore();
|
|
14906
|
-
const src = spec.data ?? [];
|
|
14907
|
-
const data = spec.transform?.length ? applyTransforms(spec.transform, src) : src;
|
|
14908
|
-
const effectiveSpec = data === src ? spec : { ...spec, data };
|
|
14909
|
-
if (isFaceted(effectiveSpec)) {
|
|
14910
|
-
const layout2 = buildFacetModels(effectiveSpec, tokens, size);
|
|
14911
|
-
if (layout2) {
|
|
14912
|
-
drawFacet(surface, effectiveSpec, layout2, tokens);
|
|
14913
|
-
return facetReport(effectiveSpec, layout2, tokens, size);
|
|
14914
|
-
}
|
|
14915
|
-
}
|
|
14916
|
-
let reportModel;
|
|
14917
|
-
if (CARTESIAN_TYPES.has(type)) {
|
|
14918
|
-
const renderer = cartesianRenderers[type];
|
|
14919
|
-
const model = buildCartesianModel(effectiveSpec, tokens, size);
|
|
14920
|
-
reportModel = model;
|
|
14921
|
-
drawAxesUnderlay(surface, model);
|
|
14922
|
-
if (renderer) renderer(surface, model);
|
|
14923
|
-
drawTrendlines(surface, model);
|
|
14924
|
-
drawAnnotations(surface, model);
|
|
14925
|
-
drawOverlay(surface, model);
|
|
14926
|
-
drawAnnotationLabels(surface, model);
|
|
14927
|
-
drawTrendlineLabels(surface, model);
|
|
14928
|
-
} else {
|
|
14929
|
-
const renderer = customRenderers[type];
|
|
14930
|
-
if (!renderer) {
|
|
14931
|
-
throw new Error(`Graphein: no renderer registered for chart type "${type}".`);
|
|
14932
|
-
}
|
|
14933
|
-
const rctx = {
|
|
14934
|
-
emphasis: null,
|
|
14935
|
-
store: createSelectionStore(),
|
|
14936
|
-
param: void 0,
|
|
14937
|
-
def: void 0,
|
|
14938
|
-
sourceData: src,
|
|
14939
|
-
framed: false,
|
|
14940
|
-
requestRedraw: () => {
|
|
14941
|
-
}
|
|
14942
|
-
};
|
|
14943
|
-
renderer(surface, effectiveSpec, tokens, size, rctx);
|
|
14944
|
-
}
|
|
14945
|
-
return buildRenderReport({ type, spec, data, tokens, size, model: reportModel });
|
|
14946
|
-
}
|
|
14947
|
-
|
|
14948
15910
|
// src/render/fonts.ts
|
|
14949
15911
|
function fontFaceSet() {
|
|
14950
15912
|
try {
|
|
@@ -16055,9 +17017,180 @@ function renderDashboard(target, spec, options) {
|
|
|
16055
17017
|
};
|
|
16056
17018
|
}
|
|
16057
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
|
+
|
|
16058
17191
|
// src/index.ts
|
|
16059
17192
|
var VERSION = "0.1.0";
|
|
16060
17193
|
|
|
16061
|
-
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 };
|
|
16062
17195
|
//# sourceMappingURL=index.js.map
|
|
16063
17196
|
//# sourceMappingURL=index.js.map
|