graphein 0.11.0 → 0.13.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 +139 -11
- package/dist/index.js +1283 -428
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2349,6 +2349,15 @@ function aggregateValues(values, op) {
|
|
|
2349
2349
|
}
|
|
2350
2350
|
return null;
|
|
2351
2351
|
}
|
|
2352
|
+
function resolveValueRef(ref, data) {
|
|
2353
|
+
if (ref == null) return null;
|
|
2354
|
+
if (typeof ref === "number") return Number.isFinite(ref) ? ref : null;
|
|
2355
|
+
const read = accessor(ref.field);
|
|
2356
|
+
return aggregateValues(
|
|
2357
|
+
(data ?? []).map((d) => read(d)),
|
|
2358
|
+
ref.aggregate ?? "sum"
|
|
2359
|
+
);
|
|
2360
|
+
}
|
|
2352
2361
|
function groupBy(rows, keyFields2) {
|
|
2353
2362
|
const grouped = /* @__PURE__ */ new Map();
|
|
2354
2363
|
for (const row of rows) {
|
|
@@ -2646,7 +2655,7 @@ function fieldTitle(f) {
|
|
|
2646
2655
|
if (!f) return void 0;
|
|
2647
2656
|
return f.title ?? f.field;
|
|
2648
2657
|
}
|
|
2649
|
-
function
|
|
2658
|
+
function resolveValueRef2(ref, data) {
|
|
2650
2659
|
if (ref == null) return null;
|
|
2651
2660
|
if (typeof ref === "number") return Number.isFinite(ref) ? ref : null;
|
|
2652
2661
|
return aggregateValues(data.map(accessor(ref.field)), ref.aggregate ?? "sum");
|
|
@@ -2747,9 +2756,9 @@ function analyzeChart(spec) {
|
|
|
2747
2756
|
if (rows.length === 0 && !VALUE_TYPES.has(type)) return null;
|
|
2748
2757
|
if (VALUE_TYPES.has(type)) {
|
|
2749
2758
|
const s = spec;
|
|
2750
|
-
const value =
|
|
2759
|
+
const value = resolveValueRef2(s.value, rows);
|
|
2751
2760
|
if (value === null) return null;
|
|
2752
|
-
const target =
|
|
2761
|
+
const target = resolveValueRef2(s.target, rows);
|
|
2753
2762
|
const toTarget = target === null ? null : value - target;
|
|
2754
2763
|
const pctOfTarget = target ? value / target : null;
|
|
2755
2764
|
return {
|
|
@@ -4248,18 +4257,58 @@ function validateSpec(spec) {
|
|
|
4248
4257
|
}
|
|
4249
4258
|
}
|
|
4250
4259
|
if (type === "kpi") {
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
err("
|
|
4260
|
+
checkValueRef("value", spec.value, "kpi", true);
|
|
4261
|
+
checkValueRef("delta", spec.delta, "delta", false);
|
|
4262
|
+
if (spec.align !== void 0 && !["start", "center", "end"].includes(spec.align)) {
|
|
4263
|
+
err("align", '"align" must be one of: start, center, end.');
|
|
4264
|
+
}
|
|
4265
|
+
if (spec.labelPosition !== void 0 && !["above", "below"].includes(spec.labelPosition)) {
|
|
4266
|
+
err("labelPosition", '"labelPosition" must be one of: above, below.');
|
|
4267
|
+
}
|
|
4268
|
+
if (spec.unit !== void 0 && typeof spec.unit !== "string") {
|
|
4269
|
+
err("unit", '"unit" must be a string.');
|
|
4270
|
+
}
|
|
4271
|
+
if (spec.color !== void 0 && typeof spec.color !== "string") {
|
|
4272
|
+
err("color", '"color" must be a string (a CSS color).');
|
|
4273
|
+
}
|
|
4274
|
+
if (spec.sparkline !== void 0 && typeof spec.sparkline !== "boolean") {
|
|
4275
|
+
if (!isObject3(spec.sparkline)) {
|
|
4276
|
+
err("sparkline", '"sparkline" must be a boolean or an object with { field?, markers? }.');
|
|
4277
|
+
} else {
|
|
4278
|
+
if (spec.sparkline.field !== void 0) checkField("sparkline.field", spec.sparkline.field);
|
|
4279
|
+
if (spec.sparkline.markers !== void 0 && typeof spec.sparkline.markers !== "boolean") {
|
|
4280
|
+
err("sparkline.markers", '"sparkline.markers" must be a boolean.');
|
|
4281
|
+
}
|
|
4282
|
+
}
|
|
4283
|
+
}
|
|
4284
|
+
if (spec.comparisons !== void 0) {
|
|
4285
|
+
if (!Array.isArray(spec.comparisons)) {
|
|
4286
|
+
err("comparisons", '"comparisons" must be an array of { label, delta, amount? }.');
|
|
4287
|
+
} else {
|
|
4288
|
+
spec.comparisons.forEach((c, i) => {
|
|
4289
|
+
if (!isObject3(c)) {
|
|
4290
|
+
err(`comparisons[${i}]`, 'Each comparison must be an object with a "label" and "delta".');
|
|
4291
|
+
return;
|
|
4292
|
+
}
|
|
4293
|
+
if (typeof c.label !== "string" || c.label === "") {
|
|
4294
|
+
err(`comparisons[${i}].label`, 'Each comparison needs a non-empty "label".');
|
|
4295
|
+
}
|
|
4296
|
+
checkValueRef(`comparisons[${i}].delta`, c.delta, "comparison", true);
|
|
4297
|
+
if (c.amount !== void 0) checkValueRef(`comparisons[${i}].amount`, c.amount, "comparison amount", false);
|
|
4298
|
+
if (c.invert !== void 0 && typeof c.invert !== "boolean") {
|
|
4299
|
+
err(`comparisons[${i}].invert`, '"invert" must be a boolean.');
|
|
4300
|
+
}
|
|
4301
|
+
});
|
|
4302
|
+
}
|
|
4258
4303
|
}
|
|
4259
4304
|
}
|
|
4260
4305
|
if (type === "gauge") {
|
|
4261
4306
|
checkValueRef("value", spec.value, "gauge", true);
|
|
4262
4307
|
checkValueRef("target", spec.target, "target", false);
|
|
4308
|
+
checkValueRef("compare", spec.compare, "compare", false);
|
|
4309
|
+
if (spec.legend !== void 0 && typeof spec.legend !== "boolean") {
|
|
4310
|
+
err("legend", '"legend" must be a boolean.');
|
|
4311
|
+
}
|
|
4263
4312
|
const max = spec.max;
|
|
4264
4313
|
if (max === void 0) {
|
|
4265
4314
|
err("max", `"gauge" requires a numeric "max" (the scale's full-scale value).`);
|
|
@@ -5709,15 +5758,19 @@ function overlayTextToCanvasCmd(o, font) {
|
|
|
5709
5758
|
const m = t.match(/rotate\((-?\d+(?:\.\d+)?)deg\)/);
|
|
5710
5759
|
return m ? parseFloat(m[1]) * Math.PI / 180 : void 0;
|
|
5711
5760
|
})();
|
|
5761
|
+
const shiftX100 = t.includes("translateX(-100%)") || t.includes("translate(-100%");
|
|
5762
|
+
const shiftX50 = t.includes("translateX(-50%)") || t.includes("translate(-50%");
|
|
5712
5763
|
let align = "left";
|
|
5713
5764
|
let x = o.left;
|
|
5714
5765
|
const y = o.top;
|
|
5715
5766
|
if (o.width != null) {
|
|
5716
5767
|
align = o.align ?? "left";
|
|
5717
5768
|
x = align === "right" ? o.left + o.width : align === "center" ? o.left + o.width / 2 : o.left;
|
|
5718
|
-
|
|
5769
|
+
if (shiftX100) x -= o.width;
|
|
5770
|
+
else if (shiftX50) x -= o.width / 2;
|
|
5771
|
+
} else if (shiftX100) {
|
|
5719
5772
|
align = "right";
|
|
5720
|
-
} else if (
|
|
5773
|
+
} else if (shiftX50) {
|
|
5721
5774
|
align = "center";
|
|
5722
5775
|
}
|
|
5723
5776
|
const baseline = t.includes("translateY(-50%)") || t.includes(",-50%") || t.includes(", -50%") ? "middle" : "top";
|
|
@@ -7080,13 +7133,13 @@ function rowAlpha(emphasis, row) {
|
|
|
7080
7133
|
if (!emphasis) return 1;
|
|
7081
7134
|
return emphasis.match(row) ? 1 : emphasis.dim;
|
|
7082
7135
|
}
|
|
7083
|
-
function
|
|
7084
|
-
if (!emphasis) return
|
|
7085
|
-
|
|
7086
|
-
|
|
7087
|
-
|
|
7088
|
-
if (
|
|
7089
|
-
return
|
|
7136
|
+
function seriesEmphasisMode(emphasis, series) {
|
|
7137
|
+
if (!emphasis) return "all";
|
|
7138
|
+
let matched = 0;
|
|
7139
|
+
for (const row of series.rows) if (emphasis.match(row)) matched++;
|
|
7140
|
+
if (matched === 0) return "none";
|
|
7141
|
+
if (matched === series.rows.length) return "all";
|
|
7142
|
+
return "partial";
|
|
7090
7143
|
}
|
|
7091
7144
|
|
|
7092
7145
|
// src/charts/line.ts
|
|
@@ -7114,6 +7167,24 @@ function buildSeriesPoints(model, s) {
|
|
|
7114
7167
|
gap: () => ({ x: NaN, y: NaN })
|
|
7115
7168
|
});
|
|
7116
7169
|
}
|
|
7170
|
+
function matchPointPixels(model, s) {
|
|
7171
|
+
const emphasis = model.emphasis;
|
|
7172
|
+
if (!emphasis) return [];
|
|
7173
|
+
const readX = accessor(model.x.field);
|
|
7174
|
+
const readY = accessor(model.y.field ?? "");
|
|
7175
|
+
const out = [];
|
|
7176
|
+
for (const d of s.rows) {
|
|
7177
|
+
if (!emphasis.match(d)) continue;
|
|
7178
|
+
const px = model.x.pixel(readX(d));
|
|
7179
|
+
if (px == null) continue;
|
|
7180
|
+
const yRaw = readY(d);
|
|
7181
|
+
if (yRaw == null || yRaw === "") continue;
|
|
7182
|
+
const py = model.y.pixel(yRaw);
|
|
7183
|
+
if (Number.isNaN(py)) continue;
|
|
7184
|
+
out.push({ x: px, y: py });
|
|
7185
|
+
}
|
|
7186
|
+
return out;
|
|
7187
|
+
}
|
|
7117
7188
|
function finiteRuns(points) {
|
|
7118
7189
|
const runs = [];
|
|
7119
7190
|
let run = [];
|
|
@@ -7165,6 +7236,18 @@ function drawLineSketch(ctx, model, allSeries, pen, filled, showPoints) {
|
|
|
7165
7236
|
if (alpha !== 1) ctx.globalAlpha = prev;
|
|
7166
7237
|
}
|
|
7167
7238
|
}
|
|
7239
|
+
for (const { series, mode, matchPoints } of allSeries) {
|
|
7240
|
+
if (mode !== "partial") continue;
|
|
7241
|
+
ctx.globalAlpha = 1;
|
|
7242
|
+
for (const p of matchPoints) {
|
|
7243
|
+
pen.circle(p.x, p.y, 3.4, {
|
|
7244
|
+
fill: series.color,
|
|
7245
|
+
stroke: model.tokens.color.background,
|
|
7246
|
+
strokeWidth: 1.5
|
|
7247
|
+
});
|
|
7248
|
+
}
|
|
7249
|
+
}
|
|
7250
|
+
ctx.globalAlpha = 1;
|
|
7168
7251
|
}
|
|
7169
7252
|
function drawLine(surface, model) {
|
|
7170
7253
|
const ctx = surface.marks.ctx;
|
|
@@ -7173,11 +7256,16 @@ function drawLine(surface, model) {
|
|
|
7173
7256
|
const curve = resolveCurve(spec.curve);
|
|
7174
7257
|
const lineGen = line({ curve });
|
|
7175
7258
|
const areaGen = spec.area ? area({ curve }) : null;
|
|
7176
|
-
const allSeries = model.series.map((s) =>
|
|
7177
|
-
|
|
7178
|
-
|
|
7179
|
-
|
|
7180
|
-
|
|
7259
|
+
const allSeries = model.series.map((s) => {
|
|
7260
|
+
const mode = seriesEmphasisMode(model.emphasis, s);
|
|
7261
|
+
return {
|
|
7262
|
+
series: s,
|
|
7263
|
+
points: buildSeriesPoints(model, s),
|
|
7264
|
+
alpha: mode === "all" ? 1 : model.emphasis?.dim ?? 1,
|
|
7265
|
+
mode,
|
|
7266
|
+
matchPoints: matchPointPixels(model, s)
|
|
7267
|
+
};
|
|
7268
|
+
});
|
|
7181
7269
|
const showPoints = spec.points || allSeries.some((s) => s.points.filter((p) => !Number.isNaN(p.x)).length === 1);
|
|
7182
7270
|
ctx.save();
|
|
7183
7271
|
ctx.beginPath();
|
|
@@ -7232,6 +7320,20 @@ function drawLine(surface, model) {
|
|
|
7232
7320
|
ctx.globalAlpha = 1;
|
|
7233
7321
|
}
|
|
7234
7322
|
}
|
|
7323
|
+
for (const { series, mode, matchPoints } of allSeries) {
|
|
7324
|
+
if (mode !== "partial") continue;
|
|
7325
|
+
ctx.fillStyle = series.color;
|
|
7326
|
+
ctx.strokeStyle = tokens.color.background;
|
|
7327
|
+
ctx.lineWidth = 1.5;
|
|
7328
|
+
ctx.globalAlpha = 1;
|
|
7329
|
+
for (const p of matchPoints) {
|
|
7330
|
+
ctx.beginPath();
|
|
7331
|
+
ctx.arc(p.x, p.y, 3, 0, Math.PI * 2);
|
|
7332
|
+
ctx.fill();
|
|
7333
|
+
ctx.stroke();
|
|
7334
|
+
}
|
|
7335
|
+
}
|
|
7336
|
+
ctx.globalAlpha = 1;
|
|
7235
7337
|
ctx.restore();
|
|
7236
7338
|
}
|
|
7237
7339
|
|
|
@@ -7242,6 +7344,8 @@ function buildBands(model, stacked) {
|
|
|
7242
7344
|
const cumulative = /* @__PURE__ */ new Map();
|
|
7243
7345
|
const threshold = stacked ? Infinity : Math.max(2, Math.round(model.plot.width));
|
|
7244
7346
|
return model.series.map((series) => {
|
|
7347
|
+
const mode = seriesEmphasisMode(model.emphasis, series);
|
|
7348
|
+
const matchPoints = [];
|
|
7245
7349
|
const points = [];
|
|
7246
7350
|
for (const row of series.rows) {
|
|
7247
7351
|
const xv = readX(row);
|
|
@@ -7254,19 +7358,23 @@ function buildBands(model, stacked) {
|
|
|
7254
7358
|
const base = cumulative.get(key) ?? 0;
|
|
7255
7359
|
const top = base + (missing ? 0 : value);
|
|
7256
7360
|
cumulative.set(key, top);
|
|
7361
|
+
const y1 = missing ? NaN : model.y.pixel(top);
|
|
7257
7362
|
points.push({
|
|
7258
7363
|
x: px == null ? NaN : px,
|
|
7259
7364
|
y0: model.y.pixel(base),
|
|
7260
|
-
y1
|
|
7365
|
+
y1,
|
|
7261
7366
|
sortKey: px == null ? Number.POSITIVE_INFINITY : px
|
|
7262
7367
|
});
|
|
7368
|
+
if (model.emphasis?.match(row) && px != null && !Number.isNaN(y1)) matchPoints.push({ x: px, y: y1 });
|
|
7263
7369
|
} else {
|
|
7370
|
+
const y1 = missing ? NaN : model.y.pixel(value);
|
|
7264
7371
|
points.push({
|
|
7265
7372
|
x: px == null ? NaN : px,
|
|
7266
7373
|
y0: model.y.baseline,
|
|
7267
|
-
y1
|
|
7374
|
+
y1,
|
|
7268
7375
|
sortKey: px == null ? Number.POSITIVE_INFINITY : px
|
|
7269
7376
|
});
|
|
7377
|
+
if (model.emphasis?.match(row) && px != null && !Number.isNaN(y1)) matchPoints.push({ x: px, y: y1 });
|
|
7270
7378
|
}
|
|
7271
7379
|
}
|
|
7272
7380
|
points.sort((a, b) => a.sortKey - b.sortKey);
|
|
@@ -7276,7 +7384,7 @@ function buildBands(model, stacked) {
|
|
|
7276
7384
|
getY: (p) => p.y1,
|
|
7277
7385
|
gap: () => ({ x: NaN, y0: NaN, y1: NaN })
|
|
7278
7386
|
}) : band;
|
|
7279
|
-
return { series, points: reduced, alpha:
|
|
7387
|
+
return { series, points: reduced, alpha: mode === "all" ? 1 : model.emphasis?.dim ?? 1, mode, matchPoints };
|
|
7280
7388
|
});
|
|
7281
7389
|
}
|
|
7282
7390
|
function drawAreaSketch(ctx, model, bands, pen, stacked) {
|
|
@@ -7311,6 +7419,18 @@ function drawAreaSketch(ctx, model, bands, pen, stacked) {
|
|
|
7311
7419
|
);
|
|
7312
7420
|
if (alpha !== 1) ctx.globalAlpha = prev;
|
|
7313
7421
|
}
|
|
7422
|
+
for (const { series, mode, matchPoints } of bands) {
|
|
7423
|
+
if (mode !== "partial") continue;
|
|
7424
|
+
ctx.globalAlpha = 1;
|
|
7425
|
+
for (const p of matchPoints) {
|
|
7426
|
+
pen.circle(p.x, p.y, 3.4, {
|
|
7427
|
+
fill: series.color,
|
|
7428
|
+
stroke: model.tokens.color.background,
|
|
7429
|
+
strokeWidth: 1.5
|
|
7430
|
+
});
|
|
7431
|
+
}
|
|
7432
|
+
}
|
|
7433
|
+
ctx.globalAlpha = 1;
|
|
7314
7434
|
}
|
|
7315
7435
|
function drawArea(surface, model) {
|
|
7316
7436
|
const ctx = surface.marks.ctx;
|
|
@@ -7360,6 +7480,20 @@ function drawArea(surface, model) {
|
|
|
7360
7480
|
ctx.stroke();
|
|
7361
7481
|
ctx.globalAlpha = 1;
|
|
7362
7482
|
}
|
|
7483
|
+
for (const { series, mode, matchPoints } of bands) {
|
|
7484
|
+
if (mode !== "partial") continue;
|
|
7485
|
+
ctx.fillStyle = series.color;
|
|
7486
|
+
ctx.strokeStyle = tokens.color.background;
|
|
7487
|
+
ctx.lineWidth = 1.5;
|
|
7488
|
+
ctx.globalAlpha = 1;
|
|
7489
|
+
for (const p of matchPoints) {
|
|
7490
|
+
ctx.beginPath();
|
|
7491
|
+
ctx.arc(p.x, p.y, 3, 0, Math.PI * 2);
|
|
7492
|
+
ctx.fill();
|
|
7493
|
+
ctx.stroke();
|
|
7494
|
+
}
|
|
7495
|
+
}
|
|
7496
|
+
ctx.globalAlpha = 1;
|
|
7363
7497
|
ctx.restore();
|
|
7364
7498
|
}
|
|
7365
7499
|
|
|
@@ -7427,6 +7561,8 @@ function drawGroupedBars(ctx, model, radius, pen) {
|
|
|
7427
7561
|
const subWidth = seriesCount > 1 ? bandWidth / seriesCount : bandWidth;
|
|
7428
7562
|
const gap = seriesCount > 1 && subWidth > 3 ? Math.min(1, subWidth * 0.15) : 0;
|
|
7429
7563
|
const barThick = Math.max(0, subWidth - gap);
|
|
7564
|
+
const t = model.entranceProgress ?? 1;
|
|
7565
|
+
const grow = (pixel) => model.y.baseline + (pixel - model.y.baseline) * t;
|
|
7430
7566
|
model.series.forEach((series, seriesIndex) => {
|
|
7431
7567
|
ctx.fillStyle = series.color;
|
|
7432
7568
|
for (const row of series.rows) {
|
|
@@ -7436,7 +7572,7 @@ function drawGroupedBars(ctx, model, radius, pen) {
|
|
|
7436
7572
|
const center2 = model.x.pixel(readX(row));
|
|
7437
7573
|
if (center2 == null) continue;
|
|
7438
7574
|
const catStart = center2 - bandWidth / 2 + seriesIndex * subWidth + gap / 2;
|
|
7439
|
-
const rect = orientedRect(horizontal, catStart, barThick, model.y.baseline, model.y.pixel(value), series.color);
|
|
7575
|
+
const rect = orientedRect(horizontal, catStart, barThick, grow(model.y.baseline), grow(model.y.pixel(value)), series.color);
|
|
7440
7576
|
if (rect) drawRect(ctx, rect, radius, pen, rowAlpha(model.emphasis, row));
|
|
7441
7577
|
}
|
|
7442
7578
|
});
|
|
@@ -7450,6 +7586,8 @@ function drawStackedBars(ctx, model, radius, pen) {
|
|
|
7450
7586
|
const segments = [];
|
|
7451
7587
|
const roundedEnds = /* @__PURE__ */ new Map();
|
|
7452
7588
|
const bandWidth = model.x.bandwidth;
|
|
7589
|
+
const t = model.entranceProgress ?? 1;
|
|
7590
|
+
const grow = (pixel) => model.y.baseline + (pixel - model.y.baseline) * t;
|
|
7453
7591
|
for (const series of model.series) {
|
|
7454
7592
|
for (const row of series.rows) {
|
|
7455
7593
|
const xValue = readX(row);
|
|
@@ -7464,7 +7602,7 @@ function drawStackedBars(ctx, model, radius, pen) {
|
|
|
7464
7602
|
const base = totals.get(categoryKey2) ?? 0;
|
|
7465
7603
|
const top = base + value;
|
|
7466
7604
|
totals.set(categoryKey2, top);
|
|
7467
|
-
const rect = orientedRect(horizontal, center2 - bandWidth / 2, bandWidth, model.y.pixel(base), model.y.pixel(top), series.color);
|
|
7605
|
+
const rect = orientedRect(horizontal, center2 - bandWidth / 2, bandWidth, grow(model.y.pixel(base)), grow(model.y.pixel(top)), series.color);
|
|
7468
7606
|
if (!rect) continue;
|
|
7469
7607
|
segments.push({ ...rect, stackEndKey, alpha: rowAlpha(model.emphasis, row) });
|
|
7470
7608
|
roundedEnds.set(stackEndKey, segments.length - 1);
|
|
@@ -8597,6 +8735,8 @@ function drawPie(surface, spec, tokens, size, context) {
|
|
|
8597
8735
|
const colorField = pie.encoding.color.field;
|
|
8598
8736
|
const emphasis = context?.emphasis ?? null;
|
|
8599
8737
|
const sliceAlpha = (slice) => emphasis ? emphasis.match({ [colorField]: slice.category }) ? 1 : emphasis.dim : 1;
|
|
8738
|
+
const sweepT = context?.sweepProgress ?? 1;
|
|
8739
|
+
const sweepEnd = TAU3 * sweepT;
|
|
8600
8740
|
const content = drawTitleBlock(surface, tokens, size, pie.title);
|
|
8601
8741
|
const slices = buildSlices(pie, tokens.color.palette);
|
|
8602
8742
|
const legendPosition = resolveLegendPosition(pie, content.width, content.height);
|
|
@@ -8654,9 +8794,11 @@ function drawPie(surface, spec, tokens, size, context) {
|
|
|
8654
8794
|
if (sketch) {
|
|
8655
8795
|
const pen = new RoughPen(ctx, sketch);
|
|
8656
8796
|
for (const slice of slices) {
|
|
8797
|
+
const end = Math.min(slice.endAngle, sweepEnd);
|
|
8798
|
+
if (end <= slice.startAngle) continue;
|
|
8657
8799
|
const alpha = sliceAlpha(slice);
|
|
8658
8800
|
if (alpha !== 1) ctx.globalAlpha = alpha;
|
|
8659
|
-
pen.wedge(cx, cy, innerRadius, outerRadius, slice.startAngle,
|
|
8801
|
+
pen.wedge(cx, cy, innerRadius, outerRadius, slice.startAngle, end, {
|
|
8660
8802
|
fill: slice.color,
|
|
8661
8803
|
stroke: separator
|
|
8662
8804
|
});
|
|
@@ -8665,11 +8807,13 @@ function drawPie(surface, spec, tokens, size, context) {
|
|
|
8665
8807
|
ctx.restore();
|
|
8666
8808
|
} else {
|
|
8667
8809
|
for (const slice of slices) {
|
|
8810
|
+
const end = Math.min(slice.endAngle, sweepEnd);
|
|
8811
|
+
if (end <= slice.startAngle) continue;
|
|
8668
8812
|
const wedge = arc({
|
|
8669
8813
|
innerRadius,
|
|
8670
8814
|
outerRadius,
|
|
8671
8815
|
startAngle: slice.startAngle,
|
|
8672
|
-
endAngle:
|
|
8816
|
+
endAngle: end
|
|
8673
8817
|
});
|
|
8674
8818
|
ctx.beginPath();
|
|
8675
8819
|
wedge(ctx, cx, cy);
|
|
@@ -8681,7 +8825,7 @@ function drawPie(surface, spec, tokens, size, context) {
|
|
|
8681
8825
|
}
|
|
8682
8826
|
ctx.restore();
|
|
8683
8827
|
}
|
|
8684
|
-
if (labelPlan && labelCfg) {
|
|
8828
|
+
if (sweepT >= 1 && labelPlan && labelCfg) {
|
|
8685
8829
|
ctx.save();
|
|
8686
8830
|
ctx.lineWidth = 1;
|
|
8687
8831
|
ctx.lineJoin = "round";
|
|
@@ -8725,7 +8869,7 @@ function drawPie(surface, spec, tokens, size, context) {
|
|
|
8725
8869
|
});
|
|
8726
8870
|
}
|
|
8727
8871
|
}
|
|
8728
|
-
if (innerRadius >= 34) {
|
|
8872
|
+
if (sweepT >= 1 && innerRadius >= 34) {
|
|
8729
8873
|
addOverlayText(surface, tokens, {
|
|
8730
8874
|
left: cx,
|
|
8731
8875
|
top: cy - tokens.font.size.small - 2,
|
|
@@ -9010,19 +9154,9 @@ function drawFunnel(surface, spec, tokens, size, context) {
|
|
|
9010
9154
|
|
|
9011
9155
|
// src/charts/kpi.ts
|
|
9012
9156
|
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
9013
|
-
var SKETCH_BORDER_RADIUS = "255px 15px 225px 15px / 15px 225px 15px 255px";
|
|
9014
9157
|
function clamp5(value, min, max) {
|
|
9015
9158
|
return Math.max(min, Math.min(max, value));
|
|
9016
9159
|
}
|
|
9017
|
-
function resolveValue(ref, data) {
|
|
9018
|
-
if (ref == null) return null;
|
|
9019
|
-
if (typeof ref === "number") return Number.isFinite(ref) ? ref : null;
|
|
9020
|
-
const read = accessor(ref.field);
|
|
9021
|
-
return aggregateValues(
|
|
9022
|
-
data.map((d) => read(d)),
|
|
9023
|
-
ref.aggregate ?? "sum"
|
|
9024
|
-
);
|
|
9025
|
-
}
|
|
9026
9160
|
function valueField(ref) {
|
|
9027
9161
|
return typeof ref === "number" ? null : ref.field;
|
|
9028
9162
|
}
|
|
@@ -9033,6 +9167,12 @@ function formatDelta(delta, specFormat) {
|
|
|
9033
9167
|
if (abs <= 1) return sign2 + formatNumber2(abs, ".1%");
|
|
9034
9168
|
return sign2 + formatNumber2(abs, ",.0f");
|
|
9035
9169
|
}
|
|
9170
|
+
function flexAlign(align) {
|
|
9171
|
+
return align === "start" ? "flex-start" : align === "end" ? "flex-end" : "center";
|
|
9172
|
+
}
|
|
9173
|
+
function textAlignOf(align) {
|
|
9174
|
+
return align === "start" ? "left" : align === "end" ? "right" : "center";
|
|
9175
|
+
}
|
|
9036
9176
|
function cardRectFor(surface, tokens, size, spec) {
|
|
9037
9177
|
if (spec.title) return drawTitleBlock(surface, tokens, size, spec.title);
|
|
9038
9178
|
return {
|
|
@@ -9042,41 +9182,25 @@ function cardRectFor(surface, tokens, size, spec) {
|
|
|
9042
9182
|
height: Math.max(0, size.height - CHROME_PAD.top - CHROME_PAD.bottom)
|
|
9043
9183
|
};
|
|
9044
9184
|
}
|
|
9045
|
-
function
|
|
9046
|
-
const
|
|
9047
|
-
const
|
|
9048
|
-
const
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
|
|
9052
|
-
card.style.width = `${rect.width}px`;
|
|
9053
|
-
card.style.height = `${rect.height}px`;
|
|
9054
|
-
card.style.boxSizing = "border-box";
|
|
9055
|
-
card.style.background = framed ? "transparent" : tokens.color.surface;
|
|
9056
|
-
card.style.border = framed ? "none" : `1px solid ${tokens.color.border}`;
|
|
9057
|
-
card.style.borderRadius = `${tokens.radius.lg}px`;
|
|
9058
|
-
card.style.padding = framed ? `0 0 ${sparkReserve}px` : `${pad3}px ${pad3}px ${pad3 + sparkReserve}px`;
|
|
9059
|
-
card.style.display = "flex";
|
|
9060
|
-
card.style.flexDirection = "column";
|
|
9061
|
-
card.style.justifyContent = "center";
|
|
9062
|
-
card.style.alignItems = "flex-start";
|
|
9063
|
-
card.style.gap = `${tokens.spacing.sm}px`;
|
|
9064
|
-
card.style.overflow = "hidden";
|
|
9065
|
-
card.style.pointerEvents = "none";
|
|
9066
|
-
return card;
|
|
9185
|
+
function valueFontSize(rect, contentWidth, valueText, unit, isRich) {
|
|
9186
|
+
const effectiveLen = valueText.length + (unit ? unit.length * 0.5 + 0.4 : 0);
|
|
9187
|
+
const sizeByCard = Math.min(rect.width, rect.height) * (isRich ? 0.17 : 0.28);
|
|
9188
|
+
const sizeByText = contentWidth / Math.max(1, effectiveLen * 0.58);
|
|
9189
|
+
const lo = isRich ? 22 : 26;
|
|
9190
|
+
const hi = isRich ? 52 : 76;
|
|
9191
|
+
return clamp5(Math.min(sizeByCard, sizeByText), lo, hi);
|
|
9067
9192
|
}
|
|
9068
|
-
function
|
|
9069
|
-
|
|
9070
|
-
el.textContent = text;
|
|
9071
|
-
Object.assign(el.style, style);
|
|
9072
|
-
card.appendChild(el);
|
|
9073
|
-
return el;
|
|
9193
|
+
function unitFontSize(valueSize) {
|
|
9194
|
+
return Math.max(12, valueSize * 0.46);
|
|
9074
9195
|
}
|
|
9075
9196
|
function sparklineField(spec) {
|
|
9076
9197
|
if (!spec.sparkline) return null;
|
|
9077
|
-
if (typeof spec.sparkline === "object") return spec.sparkline.field;
|
|
9198
|
+
if (typeof spec.sparkline === "object") return spec.sparkline.field ?? valueField(spec.value);
|
|
9078
9199
|
return valueField(spec.value);
|
|
9079
9200
|
}
|
|
9201
|
+
function sparklineMarkers(spec) {
|
|
9202
|
+
return typeof spec.sparkline === "object" ? spec.sparkline.markers !== false : true;
|
|
9203
|
+
}
|
|
9080
9204
|
function sparklineValues(data, field) {
|
|
9081
9205
|
const read = accessor(field);
|
|
9082
9206
|
const values = [];
|
|
@@ -9086,41 +9210,165 @@ function sparklineValues(data, field) {
|
|
|
9086
9210
|
}
|
|
9087
9211
|
return values;
|
|
9088
9212
|
}
|
|
9089
|
-
function
|
|
9213
|
+
function kpiSparklineSeries(spec, data) {
|
|
9214
|
+
const field = sparklineField(spec);
|
|
9215
|
+
return field ? sparklineValues(data, field) : [];
|
|
9216
|
+
}
|
|
9217
|
+
function morphSparkline(to, from, progress) {
|
|
9218
|
+
if (progress == null || progress >= 1 || to.length === 0) return to;
|
|
9219
|
+
const t = progress;
|
|
9220
|
+
if (from && from.length === to.length) return to.map((v, i) => from[i] + (v - from[i]) * t);
|
|
9221
|
+
const base = to[0];
|
|
9222
|
+
return to.map((v) => base + (v - base) * t);
|
|
9223
|
+
}
|
|
9224
|
+
function resolveComparison(cmp, data, parentFormat) {
|
|
9225
|
+
const delta = resolveValueRef(cmp.delta, data);
|
|
9226
|
+
if (delta == null) return null;
|
|
9227
|
+
const rawPositive = delta >= 0;
|
|
9228
|
+
const tri = rawPositive ? "\u25B2" : "\u25BC";
|
|
9229
|
+
let text = `${tri} ${formatValue(delta, cmp.format ?? ".0%")}`;
|
|
9230
|
+
if (cmp.amount !== void 0) {
|
|
9231
|
+
const amount = resolveValueRef(cmp.amount, data);
|
|
9232
|
+
if (amount != null) text += ` : ${formatValue(amount, cmp.amountFormat ?? parentFormat ?? ",.0f")}`;
|
|
9233
|
+
}
|
|
9234
|
+
return { label: cmp.label, text, positive: cmp.invert ? !rawPositive : rawPositive };
|
|
9235
|
+
}
|
|
9236
|
+
function layoutKpi(rect, tokens, framed, f) {
|
|
9237
|
+
const font = tokens.font;
|
|
9238
|
+
const pad3 = framed ? 0 : tokens.spacing.lg;
|
|
9239
|
+
const left = rect.x + pad3;
|
|
9240
|
+
const contentWidth = Math.max(1, rect.width - pad3 * 2);
|
|
9241
|
+
const availTop = rect.y + pad3;
|
|
9242
|
+
const availH = Math.max(0, rect.height - pad3 * 2);
|
|
9243
|
+
const labelSize = Math.round(clamp5(f.valueSize * 0.4, font.size.small, font.size.title));
|
|
9244
|
+
const labelH = f.hasLabel ? Math.round(labelSize * 1.35) : 0;
|
|
9245
|
+
const valueH = Math.round(f.valueSize * 1.06);
|
|
9246
|
+
const deltaSize = font.size.large;
|
|
9247
|
+
const deltaH = f.hasDelta ? Math.round(deltaSize * 1.5) : 0;
|
|
9248
|
+
const compSize = font.size.large;
|
|
9249
|
+
const compRowH = Math.round(compSize * 1.55);
|
|
9250
|
+
const gapLabel = Math.round(labelSize * 0.25);
|
|
9251
|
+
const gapDelta = f.hasDelta ? tokens.spacing.xs : 0;
|
|
9252
|
+
const gapSpark = tokens.spacing.sm;
|
|
9253
|
+
const dividerPad = tokens.spacing.sm;
|
|
9254
|
+
const headerH = (f.hasLabel && f.labelAbove ? labelH + gapLabel : 0) + valueH + (f.hasLabel && !f.labelAbove ? gapLabel + labelH : 0) + (f.hasDelta ? gapDelta + deltaH : 0);
|
|
9255
|
+
const footerH = f.compCount > 0 ? dividerPad + 1 + dividerPad + f.compCount * compRowH : 0;
|
|
9256
|
+
let headerTop;
|
|
9257
|
+
let footerTop;
|
|
9258
|
+
let spark = null;
|
|
9259
|
+
if (f.hasSparkline) {
|
|
9260
|
+
headerTop = availTop;
|
|
9261
|
+
footerTop = availTop + availH - footerH;
|
|
9262
|
+
const sparkTop = headerTop + headerH + gapSpark;
|
|
9263
|
+
const sparkBottom = footerTop - (footerH > 0 ? gapSpark : 0);
|
|
9264
|
+
spark = { x: left, y: sparkTop, width: contentWidth, height: Math.max(24, sparkBottom - sparkTop) };
|
|
9265
|
+
} else {
|
|
9266
|
+
const groupH = headerH + (footerH > 0 ? tokens.spacing.md + footerH : 0);
|
|
9267
|
+
headerTop = availTop + Math.max(0, (availH - groupH) / 2);
|
|
9268
|
+
footerTop = headerTop + headerH + (footerH > 0 ? tokens.spacing.md : 0);
|
|
9269
|
+
}
|
|
9270
|
+
let y = headerTop;
|
|
9271
|
+
let labelY = -1;
|
|
9272
|
+
if (f.hasLabel && f.labelAbove) {
|
|
9273
|
+
labelY = y;
|
|
9274
|
+
y += labelH + gapLabel;
|
|
9275
|
+
}
|
|
9276
|
+
const valueY = y;
|
|
9277
|
+
y += valueH;
|
|
9278
|
+
if (f.hasLabel && !f.labelAbove) {
|
|
9279
|
+
y += gapLabel;
|
|
9280
|
+
labelY = y;
|
|
9281
|
+
y += labelH;
|
|
9282
|
+
}
|
|
9283
|
+
let deltaY = -1;
|
|
9284
|
+
if (f.hasDelta) {
|
|
9285
|
+
y += gapDelta;
|
|
9286
|
+
deltaY = y;
|
|
9287
|
+
}
|
|
9288
|
+
let dividerY = -1;
|
|
9289
|
+
let compStartY = -1;
|
|
9290
|
+
if (f.compCount > 0) {
|
|
9291
|
+
dividerY = footerTop + dividerPad;
|
|
9292
|
+
compStartY = dividerY + 1 + dividerPad;
|
|
9293
|
+
}
|
|
9294
|
+
return {
|
|
9295
|
+
left,
|
|
9296
|
+
contentWidth,
|
|
9297
|
+
labelSize,
|
|
9298
|
+
labelY,
|
|
9299
|
+
valueY,
|
|
9300
|
+
valueH,
|
|
9301
|
+
deltaSize,
|
|
9302
|
+
deltaY,
|
|
9303
|
+
spark,
|
|
9304
|
+
compSize,
|
|
9305
|
+
compRowH,
|
|
9306
|
+
dividerY,
|
|
9307
|
+
compStartY
|
|
9308
|
+
};
|
|
9309
|
+
}
|
|
9310
|
+
function sparkMarkers(values, xAt, yAt, r, tokens) {
|
|
9311
|
+
const n = values.length;
|
|
9312
|
+
if (n < 2) return [];
|
|
9313
|
+
let minIdx = 0;
|
|
9314
|
+
let maxIdx = 0;
|
|
9315
|
+
for (let i = 1; i < n; i++) {
|
|
9316
|
+
if (values[i] < values[minIdx]) minIdx = i;
|
|
9317
|
+
if (values[i] > values[maxIdx]) maxIdx = i;
|
|
9318
|
+
}
|
|
9319
|
+
const lastIdx = n - 1;
|
|
9320
|
+
const out = [];
|
|
9321
|
+
if (maxIdx !== lastIdx) out.push({ x: xAt(maxIdx), y: yAt(values[maxIdx]), r, fill: tokens.color.positive });
|
|
9322
|
+
if (minIdx !== lastIdx) out.push({ x: xAt(minIdx), y: yAt(values[minIdx]), r, fill: tokens.color.negative });
|
|
9323
|
+
const lastStroke = lastIdx === maxIdx ? tokens.color.positive : lastIdx === minIdx ? tokens.color.negative : tokens.color.text;
|
|
9324
|
+
out.push({
|
|
9325
|
+
x: xAt(lastIdx),
|
|
9326
|
+
y: yAt(values[lastIdx]),
|
|
9327
|
+
r,
|
|
9328
|
+
fill: tokens.color.surface,
|
|
9329
|
+
stroke: lastStroke,
|
|
9330
|
+
strokeWidth: 1.75
|
|
9331
|
+
});
|
|
9332
|
+
return out;
|
|
9333
|
+
}
|
|
9334
|
+
function sparkScales(values, width, height, markers) {
|
|
9335
|
+
const n = values.length;
|
|
9336
|
+
const r = clamp5(Math.min(height, 40) * 0.12, 2.5, 4);
|
|
9337
|
+
const padX = markers ? r + 1 : 1.75;
|
|
9338
|
+
const padY = markers ? r + 1 : 1.75;
|
|
9339
|
+
const innerW = Math.max(1, width - padX * 2);
|
|
9340
|
+
const innerH = Math.max(1, height - padY * 2);
|
|
9341
|
+
const min = Math.min(...values);
|
|
9342
|
+
const max = Math.max(...values);
|
|
9343
|
+
const span = max - min;
|
|
9344
|
+
const xAt = (i) => n === 1 ? width / 2 : padX + i / (n - 1) * innerW;
|
|
9345
|
+
const yAt = (v) => padY + (1 - (span === 0 ? 0.5 : (v - min) / span)) * innerH;
|
|
9346
|
+
return { xAt, yAt, r };
|
|
9347
|
+
}
|
|
9348
|
+
function buildSparkline(tokens, width, height, values, markers) {
|
|
9090
9349
|
if (values.length === 0 || width <= 0 || height <= 0) return null;
|
|
9091
9350
|
const svg = document.createElementNS(SVG_NS, "svg");
|
|
9092
9351
|
svg.setAttribute("width", String(width));
|
|
9093
9352
|
svg.setAttribute("height", String(height));
|
|
9094
9353
|
svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
|
|
9095
|
-
svg.setAttribute("preserveAspectRatio", "none");
|
|
9096
9354
|
svg.style.display = "block";
|
|
9097
9355
|
svg.style.overflow = "visible";
|
|
9098
9356
|
const stroke = 1.75;
|
|
9099
|
-
const
|
|
9100
|
-
const
|
|
9357
|
+
const line2 = tokens.color.textMuted;
|
|
9358
|
+
const { xAt, yAt, r } = sparkScales(values, width, height, markers);
|
|
9101
9359
|
const n = values.length;
|
|
9102
|
-
const min = Math.min(...values);
|
|
9103
|
-
const max = Math.max(...values);
|
|
9104
|
-
const span = max - min;
|
|
9105
|
-
const xAt = (i) => n === 1 ? width / 2 : i / (n - 1) * width;
|
|
9106
|
-
const yAt = (v) => {
|
|
9107
|
-
const t = span === 0 ? 0.5 : (v - min) / span;
|
|
9108
|
-
return pad3 + (1 - t) * innerH;
|
|
9109
|
-
};
|
|
9110
9360
|
if (n === 1) {
|
|
9111
9361
|
const dot = document.createElementNS(SVG_NS, "circle");
|
|
9112
9362
|
dot.setAttribute("cx", String(width / 2));
|
|
9113
9363
|
dot.setAttribute("cy", String(yAt(values[0])));
|
|
9114
|
-
dot.setAttribute("r", "2.
|
|
9364
|
+
dot.setAttribute("r", "2.5");
|
|
9115
9365
|
dot.setAttribute("fill", tokens.color.accent);
|
|
9116
9366
|
svg.appendChild(dot);
|
|
9117
9367
|
return svg;
|
|
9118
9368
|
}
|
|
9119
9369
|
let lineD = "";
|
|
9120
|
-
for (let i = 0; i < n; i++) {
|
|
9121
|
-
|
|
9122
|
-
}
|
|
9123
|
-
const areaD = `${lineD}L${width.toFixed(2)} ${height}L0 ${height}Z`;
|
|
9370
|
+
for (let i = 0; i < n; i++) lineD += `${i === 0 ? "M" : "L"}${xAt(i).toFixed(2)} ${yAt(values[i]).toFixed(2)}`;
|
|
9371
|
+
const areaD = `${lineD}L${xAt(n - 1).toFixed(2)} ${height}L${xAt(0).toFixed(2)} ${height}Z`;
|
|
9124
9372
|
const gradId = `graphein-spark-${Math.random().toString(36).slice(2, 9)}`;
|
|
9125
9373
|
const defs = document.createElementNS(SVG_NS, "defs");
|
|
9126
9374
|
const grad = document.createElementNS(SVG_NS, "linearGradient");
|
|
@@ -9131,11 +9379,11 @@ function buildSparkline(tokens, width, height, values) {
|
|
|
9131
9379
|
grad.setAttribute("y2", "1");
|
|
9132
9380
|
const stopTop = document.createElementNS(SVG_NS, "stop");
|
|
9133
9381
|
stopTop.setAttribute("offset", "0");
|
|
9134
|
-
stopTop.setAttribute("stop-color",
|
|
9135
|
-
stopTop.setAttribute("stop-opacity", "0.
|
|
9382
|
+
stopTop.setAttribute("stop-color", line2);
|
|
9383
|
+
stopTop.setAttribute("stop-opacity", "0.18");
|
|
9136
9384
|
const stopBottom = document.createElementNS(SVG_NS, "stop");
|
|
9137
9385
|
stopBottom.setAttribute("offset", "1");
|
|
9138
|
-
stopBottom.setAttribute("stop-color",
|
|
9386
|
+
stopBottom.setAttribute("stop-color", line2);
|
|
9139
9387
|
stopBottom.setAttribute("stop-opacity", "0");
|
|
9140
9388
|
grad.appendChild(stopTop);
|
|
9141
9389
|
grad.appendChild(stopBottom);
|
|
@@ -9148,14 +9396,43 @@ function buildSparkline(tokens, width, height, values) {
|
|
|
9148
9396
|
const path = document.createElementNS(SVG_NS, "path");
|
|
9149
9397
|
path.setAttribute("d", lineD);
|
|
9150
9398
|
path.setAttribute("fill", "none");
|
|
9151
|
-
path.setAttribute("stroke",
|
|
9399
|
+
path.setAttribute("stroke", line2);
|
|
9152
9400
|
path.setAttribute("stroke-width", String(stroke));
|
|
9153
9401
|
path.setAttribute("stroke-linejoin", "round");
|
|
9154
9402
|
path.setAttribute("stroke-linecap", "round");
|
|
9155
9403
|
svg.appendChild(path);
|
|
9404
|
+
if (markers) {
|
|
9405
|
+
for (const m of sparkMarkers(values, xAt, yAt, r, tokens)) {
|
|
9406
|
+
const c = document.createElementNS(SVG_NS, "circle");
|
|
9407
|
+
c.setAttribute("cx", m.x.toFixed(2));
|
|
9408
|
+
c.setAttribute("cy", m.y.toFixed(2));
|
|
9409
|
+
c.setAttribute("r", String(m.r));
|
|
9410
|
+
c.setAttribute("fill", m.fill ?? "none");
|
|
9411
|
+
if (m.stroke) {
|
|
9412
|
+
c.setAttribute("stroke", m.stroke);
|
|
9413
|
+
c.setAttribute("stroke-width", String(m.strokeWidth ?? 1.75));
|
|
9414
|
+
}
|
|
9415
|
+
svg.appendChild(c);
|
|
9416
|
+
}
|
|
9417
|
+
}
|
|
9156
9418
|
return svg;
|
|
9157
9419
|
}
|
|
9158
|
-
function
|
|
9420
|
+
function paintSparkMarkers(ctx, values, xAt, yAt, r, tokens) {
|
|
9421
|
+
for (const m of sparkMarkers(values, xAt, yAt, r, tokens)) {
|
|
9422
|
+
ctx.beginPath();
|
|
9423
|
+
ctx.arc(m.x, m.y, m.r, 0, Math.PI * 2);
|
|
9424
|
+
if (m.fill) {
|
|
9425
|
+
ctx.fillStyle = m.fill;
|
|
9426
|
+
ctx.fill();
|
|
9427
|
+
}
|
|
9428
|
+
if (m.stroke) {
|
|
9429
|
+
ctx.lineWidth = m.strokeWidth ?? 1.75;
|
|
9430
|
+
ctx.strokeStyle = m.stroke;
|
|
9431
|
+
ctx.stroke();
|
|
9432
|
+
}
|
|
9433
|
+
}
|
|
9434
|
+
}
|
|
9435
|
+
function buildSparklineCanvas(tokens, width, height, values, markers, sketch) {
|
|
9159
9436
|
if (values.length === 0 || width <= 0 || height <= 0) return null;
|
|
9160
9437
|
const dpr = getDevicePixelRatio();
|
|
9161
9438
|
const canvas = document.createElement("canvas");
|
|
@@ -9167,207 +9444,339 @@ function buildSparklineCanvas(tokens, width, height, values, sketch) {
|
|
|
9167
9444
|
const ctx = canvas.getContext("2d");
|
|
9168
9445
|
if (!ctx) return null;
|
|
9169
9446
|
ctx.scale(dpr, dpr);
|
|
9170
|
-
const
|
|
9171
|
-
const pad3 = stroke;
|
|
9172
|
-
const innerH = Math.max(1, height - pad3 * 2);
|
|
9447
|
+
const { xAt, yAt, r } = sparkScales(values, width, height, markers);
|
|
9173
9448
|
const n = values.length;
|
|
9174
|
-
const min = Math.min(...values);
|
|
9175
|
-
const max = Math.max(...values);
|
|
9176
|
-
const span = max - min;
|
|
9177
|
-
const xAt = (i) => n === 1 ? width / 2 : i / (n - 1) * width;
|
|
9178
|
-
const yAt = (v) => pad3 + (1 - (span === 0 ? 0.5 : (v - min) / span)) * innerH;
|
|
9179
9449
|
const pen = new RoughPen(ctx, sketch);
|
|
9180
9450
|
if (n === 1) {
|
|
9181
9451
|
pen.circle(width / 2, yAt(values[0]), 2.5, { fill: tokens.color.accent });
|
|
9182
9452
|
return canvas;
|
|
9183
9453
|
}
|
|
9184
9454
|
const points = values.map((v, i) => ({ x: xAt(i), y: yAt(v) }));
|
|
9185
|
-
pen.trendStroke(points, { stroke: tokens.color.
|
|
9455
|
+
pen.trendStroke(points, { stroke: tokens.color.textMuted, strokeWidth: 1.75 });
|
|
9456
|
+
if (markers) paintSparkMarkers(ctx, values, xAt, yAt, r, tokens);
|
|
9186
9457
|
return canvas;
|
|
9187
9458
|
}
|
|
9188
|
-
function
|
|
9189
|
-
|
|
9190
|
-
|
|
9191
|
-
|
|
9459
|
+
function paintSparklineCanvas(ctx, tokens, box, values, markers) {
|
|
9460
|
+
if (values.length === 0 || box.width <= 0 || box.height <= 0) return;
|
|
9461
|
+
const { xAt: xAt0, yAt: yAt0, r } = sparkScales(values, box.width, box.height, markers);
|
|
9462
|
+
const xAt = (i) => box.x + xAt0(i);
|
|
9463
|
+
const yAt = (v) => box.y + yAt0(v);
|
|
9464
|
+
const n = values.length;
|
|
9465
|
+
ctx.save();
|
|
9466
|
+
if (n === 1) {
|
|
9467
|
+
ctx.beginPath();
|
|
9468
|
+
ctx.arc(xAt(0), yAt(values[0]), 2.5, 0, Math.PI * 2);
|
|
9469
|
+
ctx.fillStyle = tokens.color.accent;
|
|
9470
|
+
ctx.fill();
|
|
9471
|
+
ctx.restore();
|
|
9192
9472
|
return;
|
|
9193
9473
|
}
|
|
9474
|
+
ctx.globalAlpha = 0.16;
|
|
9475
|
+
ctx.beginPath();
|
|
9476
|
+
ctx.moveTo(xAt(0), yAt(values[0]));
|
|
9477
|
+
for (let i = 1; i < n; i++) ctx.lineTo(xAt(i), yAt(values[i]));
|
|
9478
|
+
ctx.lineTo(xAt(n - 1), box.y + box.height);
|
|
9479
|
+
ctx.lineTo(xAt(0), box.y + box.height);
|
|
9480
|
+
ctx.closePath();
|
|
9481
|
+
ctx.fillStyle = tokens.color.textMuted;
|
|
9482
|
+
ctx.fill();
|
|
9483
|
+
ctx.globalAlpha = 1;
|
|
9484
|
+
ctx.beginPath();
|
|
9485
|
+
for (let i = 0; i < n; i++) {
|
|
9486
|
+
const px = xAt(i);
|
|
9487
|
+
const py = yAt(values[i]);
|
|
9488
|
+
if (i === 0) ctx.moveTo(px, py);
|
|
9489
|
+
else ctx.lineTo(px, py);
|
|
9490
|
+
}
|
|
9491
|
+
ctx.strokeStyle = tokens.color.textMuted;
|
|
9492
|
+
ctx.lineWidth = 1.75;
|
|
9493
|
+
ctx.lineJoin = "round";
|
|
9494
|
+
ctx.lineCap = "round";
|
|
9495
|
+
ctx.stroke();
|
|
9496
|
+
if (markers) paintSparkMarkers(ctx, values, xAt, yAt, r, tokens);
|
|
9497
|
+
ctx.restore();
|
|
9498
|
+
}
|
|
9499
|
+
function drawKpi(surface, spec, tokens, size, context) {
|
|
9500
|
+
const kpi = spec;
|
|
9194
9501
|
const data = kpi.data ?? [];
|
|
9195
|
-
const value =
|
|
9196
|
-
const delta =
|
|
9502
|
+
const value = resolveValueRef(kpi.value, data);
|
|
9503
|
+
const delta = resolveValueRef(kpi.delta, data);
|
|
9197
9504
|
const sparkField = sparklineField(kpi);
|
|
9198
9505
|
const sparkValues = sparkField ? sparklineValues(data, sparkField) : [];
|
|
9199
9506
|
const hasSparkline = sparkValues.length > 0;
|
|
9200
|
-
const
|
|
9507
|
+
const markers = sparklineMarkers(kpi);
|
|
9508
|
+
const comparisons = kpi.comparisons?.map((c) => resolveComparison(c, data, kpi.format)).filter((c) => c != null) ?? [];
|
|
9509
|
+
const hasDelta = delta != null;
|
|
9510
|
+
const isRich = hasDelta || hasSparkline || comparisons.length > 0;
|
|
9511
|
+
const align = kpi.align ?? "center";
|
|
9512
|
+
const labelAbove = kpi.labelPosition === "above" || kpi.labelPosition == null && isRich;
|
|
9201
9513
|
const framed = context?.framed === true;
|
|
9202
9514
|
const rect = cardRectFor(surface, tokens, size, kpi);
|
|
9203
|
-
const
|
|
9204
|
-
|
|
9205
|
-
|
|
9206
|
-
|
|
9515
|
+
const progress = context?.valueProgress;
|
|
9516
|
+
const from = context?.valueFrom ?? 0;
|
|
9517
|
+
const displayValue = value != null && progress != null && progress < 1 ? from + (value - from) * progress : value;
|
|
9518
|
+
const displaySparkValues = morphSparkline(sparkValues, context?.sparklineFrom, progress);
|
|
9519
|
+
const valueText = value == null ? "\u2014" : formatValue(value, kpi.format);
|
|
9520
|
+
const displayText = displayValue == null ? "\u2014" : formatValue(displayValue, kpi.format);
|
|
9521
|
+
const fromText = context?.valueFrom != null && value != null ? formatValue(context.valueFrom, kpi.format) : valueText;
|
|
9522
|
+
const sizingText = fromText.length > valueText.length ? fromText : valueText;
|
|
9523
|
+
const pad3 = framed ? 0 : tokens.spacing.lg;
|
|
9524
|
+
const contentWidth = Math.max(1, rect.width - pad3 * 2);
|
|
9525
|
+
const valueSize = valueFontSize(rect, contentWidth, sizingText, kpi.unit, isRich);
|
|
9526
|
+
const geom = layoutKpi(rect, tokens, framed, {
|
|
9527
|
+
hasLabel: !!kpi.label,
|
|
9528
|
+
labelAbove,
|
|
9529
|
+
hasDelta,
|
|
9530
|
+
hasSparkline,
|
|
9531
|
+
compCount: comparisons.length,
|
|
9532
|
+
valueSize
|
|
9533
|
+
});
|
|
9534
|
+
const sketch = resolveSketch(spec);
|
|
9535
|
+
if (surface.headless) {
|
|
9536
|
+
drawKpiCanvas(surface, kpi, tokens, {
|
|
9537
|
+
align,
|
|
9538
|
+
valueSize,
|
|
9539
|
+
displayText,
|
|
9540
|
+
delta,
|
|
9541
|
+
comparisons,
|
|
9542
|
+
sparkValues: displaySparkValues,
|
|
9543
|
+
markers,
|
|
9544
|
+
geom
|
|
9545
|
+
});
|
|
9546
|
+
return;
|
|
9547
|
+
}
|
|
9548
|
+
const card = document.createElement("div");
|
|
9549
|
+
card.style.position = "absolute";
|
|
9550
|
+
card.style.left = `${rect.x}px`;
|
|
9551
|
+
card.style.top = `${rect.y}px`;
|
|
9552
|
+
card.style.width = `${rect.width}px`;
|
|
9553
|
+
card.style.height = `${rect.height}px`;
|
|
9554
|
+
card.style.boxSizing = "border-box";
|
|
9555
|
+
card.style.overflow = "hidden";
|
|
9556
|
+
card.style.pointerEvents = "none";
|
|
9557
|
+
const ox = rect.x;
|
|
9558
|
+
const oy = rect.y;
|
|
9559
|
+
const addAt = (el, left, top, width) => {
|
|
9560
|
+
el.style.position = "absolute";
|
|
9561
|
+
el.style.left = `${left - ox}px`;
|
|
9562
|
+
el.style.top = `${top - oy}px`;
|
|
9563
|
+
el.style.width = `${width}px`;
|
|
9564
|
+
card.appendChild(el);
|
|
9565
|
+
};
|
|
9566
|
+
if (kpi.label && geom.labelY >= 0) {
|
|
9567
|
+
const el = document.createElement("div");
|
|
9568
|
+
el.textContent = kpi.label;
|
|
9569
|
+
Object.assign(el.style, {
|
|
9207
9570
|
color: tokens.color.textMuted,
|
|
9208
9571
|
fontFamily: tokens.font.family,
|
|
9209
|
-
fontSize: `${
|
|
9572
|
+
fontSize: `${geom.labelSize}px`,
|
|
9210
9573
|
fontWeight: String(tokens.font.weight.medium),
|
|
9211
|
-
letterSpacing: "0.04em",
|
|
9212
9574
|
lineHeight: "1.25",
|
|
9213
|
-
|
|
9214
|
-
maxWidth: "100%",
|
|
9575
|
+
textAlign: align === "start" ? "left" : align === "end" ? "right" : "center",
|
|
9215
9576
|
overflow: "hidden",
|
|
9216
9577
|
textOverflow: "ellipsis",
|
|
9217
9578
|
whiteSpace: "nowrap"
|
|
9218
9579
|
});
|
|
9219
|
-
|
|
9220
|
-
|
|
9221
|
-
const
|
|
9222
|
-
|
|
9223
|
-
|
|
9224
|
-
|
|
9225
|
-
|
|
9226
|
-
|
|
9580
|
+
addAt(el, geom.left, geom.labelY, geom.contentWidth);
|
|
9581
|
+
}
|
|
9582
|
+
const valueWrap = document.createElement("div");
|
|
9583
|
+
valueWrap.style.display = "flex";
|
|
9584
|
+
valueWrap.style.alignItems = "baseline";
|
|
9585
|
+
valueWrap.style.justifyContent = flexAlign(align);
|
|
9586
|
+
valueWrap.style.gap = "0.12em";
|
|
9587
|
+
valueWrap.style.height = `${geom.valueH}px`;
|
|
9588
|
+
valueWrap.style.overflow = "hidden";
|
|
9589
|
+
const num2 = document.createElement("div");
|
|
9590
|
+
num2.textContent = displayText;
|
|
9591
|
+
Object.assign(num2.style, {
|
|
9592
|
+
color: kpi.color ?? tokens.color.text,
|
|
9227
9593
|
fontFamily: tokens.font.family,
|
|
9228
9594
|
fontSize: `${valueSize}px`,
|
|
9229
9595
|
fontWeight: String(tokens.font.weight.bold),
|
|
9230
9596
|
letterSpacing: "-0.03em",
|
|
9231
9597
|
lineHeight: "1.02",
|
|
9232
|
-
maxWidth: "100%",
|
|
9233
9598
|
overflow: "hidden",
|
|
9234
9599
|
textOverflow: "ellipsis",
|
|
9235
9600
|
whiteSpace: "nowrap"
|
|
9236
9601
|
});
|
|
9237
|
-
|
|
9602
|
+
valueWrap.appendChild(num2);
|
|
9603
|
+
if (kpi.unit) {
|
|
9604
|
+
const unit = document.createElement("div");
|
|
9605
|
+
unit.textContent = kpi.unit;
|
|
9606
|
+
Object.assign(unit.style, {
|
|
9607
|
+
color: tokens.color.textMuted,
|
|
9608
|
+
fontFamily: tokens.font.family,
|
|
9609
|
+
fontSize: `${unitFontSize(valueSize)}px`,
|
|
9610
|
+
fontWeight: String(tokens.font.weight.medium),
|
|
9611
|
+
whiteSpace: "nowrap",
|
|
9612
|
+
flex: "0 0 auto"
|
|
9613
|
+
});
|
|
9614
|
+
valueWrap.appendChild(unit);
|
|
9615
|
+
}
|
|
9616
|
+
addAt(valueWrap, geom.left, geom.valueY, geom.contentWidth);
|
|
9617
|
+
if (hasDelta && geom.deltaY >= 0 && delta != null) {
|
|
9238
9618
|
const positive = delta >= 0;
|
|
9239
|
-
|
|
9619
|
+
const el = document.createElement("div");
|
|
9620
|
+
el.textContent = `${positive ? "\u25B2" : "\u25BC"} ${formatDelta(delta, kpi.format)}`;
|
|
9621
|
+
Object.assign(el.style, {
|
|
9240
9622
|
color: positive ? tokens.color.positive : tokens.color.negative,
|
|
9241
9623
|
fontFamily: tokens.font.family,
|
|
9242
|
-
fontSize: `${
|
|
9624
|
+
fontSize: `${geom.deltaSize}px`,
|
|
9243
9625
|
fontWeight: String(tokens.font.weight.medium),
|
|
9244
|
-
lineHeight: "1",
|
|
9626
|
+
lineHeight: "1.2",
|
|
9627
|
+
textAlign: align === "start" ? "left" : align === "end" ? "right" : "center",
|
|
9245
9628
|
whiteSpace: "nowrap"
|
|
9246
9629
|
});
|
|
9630
|
+
addAt(el, geom.left, geom.deltaY, geom.contentWidth);
|
|
9631
|
+
}
|
|
9632
|
+
if (geom.spark) {
|
|
9633
|
+
const node = sketch ? buildSparklineCanvas(tokens, geom.spark.width, geom.spark.height, displaySparkValues, markers, sketch) : buildSparkline(tokens, geom.spark.width, geom.spark.height, displaySparkValues, markers);
|
|
9634
|
+
if (node) addAt(node, geom.spark.x, geom.spark.y, geom.spark.width);
|
|
9635
|
+
}
|
|
9636
|
+
if (comparisons.length > 0 && geom.dividerY >= 0) {
|
|
9637
|
+
const divider = document.createElement("div");
|
|
9638
|
+
divider.style.height = "1px";
|
|
9639
|
+
divider.style.background = tokens.color.border;
|
|
9640
|
+
addAt(divider, geom.left, geom.dividerY, geom.contentWidth);
|
|
9641
|
+
comparisons.forEach((c, i) => {
|
|
9642
|
+
const row = document.createElement("div");
|
|
9643
|
+
row.style.display = "flex";
|
|
9644
|
+
row.style.alignItems = "baseline";
|
|
9645
|
+
row.style.gap = "0.5em";
|
|
9646
|
+
row.style.height = `${geom.compRowH}px`;
|
|
9647
|
+
row.style.overflow = "hidden";
|
|
9648
|
+
const label = document.createElement("span");
|
|
9649
|
+
label.textContent = c.label;
|
|
9650
|
+
Object.assign(label.style, {
|
|
9651
|
+
color: tokens.color.text,
|
|
9652
|
+
fontFamily: tokens.font.family,
|
|
9653
|
+
fontSize: `${geom.compSize}px`,
|
|
9654
|
+
fontWeight: String(tokens.font.weight.medium),
|
|
9655
|
+
whiteSpace: "nowrap",
|
|
9656
|
+
flex: "0 0 auto"
|
|
9657
|
+
});
|
|
9658
|
+
const val = document.createElement("span");
|
|
9659
|
+
val.textContent = c.text;
|
|
9660
|
+
Object.assign(val.style, {
|
|
9661
|
+
color: c.positive ? tokens.color.positive : tokens.color.negative,
|
|
9662
|
+
fontFamily: tokens.font.family,
|
|
9663
|
+
fontSize: `${geom.compSize}px`,
|
|
9664
|
+
fontWeight: String(tokens.font.weight.medium),
|
|
9665
|
+
whiteSpace: "nowrap"
|
|
9666
|
+
});
|
|
9667
|
+
row.appendChild(label);
|
|
9668
|
+
row.appendChild(val);
|
|
9669
|
+
addAt(row, geom.left, geom.compStartY + i * geom.compRowH, geom.contentWidth);
|
|
9670
|
+
});
|
|
9247
9671
|
}
|
|
9248
9672
|
surface.overlay.appendChild(card);
|
|
9249
|
-
if (hasSparkline) {
|
|
9250
|
-
const pad3 = framed ? 0 : tokens.spacing.lg;
|
|
9251
|
-
const sparkH = clamp5(rect.height * 0.18, 24, 40);
|
|
9252
|
-
const sparkW = Math.max(0, rect.width - pad3 * 2);
|
|
9253
|
-
const node = sketch ? buildSparklineCanvas(tokens, sparkW, sparkH, sparkValues, sketch) : buildSparkline(tokens, sparkW, sparkH, sparkValues);
|
|
9254
|
-
if (node) {
|
|
9255
|
-
node.style.position = "absolute";
|
|
9256
|
-
node.style.left = `${pad3}px`;
|
|
9257
|
-
node.style.bottom = `${pad3}px`;
|
|
9258
|
-
card.appendChild(node);
|
|
9259
|
-
}
|
|
9260
|
-
}
|
|
9261
9673
|
}
|
|
9262
|
-
function drawKpiCanvas(surface, kpi, tokens,
|
|
9674
|
+
function drawKpiCanvas(surface, kpi, tokens, p) {
|
|
9263
9675
|
const ctx = surface.marks.ctx;
|
|
9264
|
-
const
|
|
9265
|
-
const
|
|
9266
|
-
const
|
|
9267
|
-
|
|
9268
|
-
const sparkValues = sparkField ? sparklineValues(data, sparkField) : [];
|
|
9269
|
-
const hasSparkline = sparkValues.length > 0;
|
|
9270
|
-
const framed = context?.framed === true;
|
|
9271
|
-
const rect = cardRectFor(surface, tokens, size, kpi);
|
|
9272
|
-
const pad3 = tokens.spacing.lg;
|
|
9273
|
-
const sparkReserve = hasSparkline ? Math.min(44, Math.max(28, rect.height * 0.18)) + tokens.spacing.sm : 0;
|
|
9274
|
-
if (!framed) {
|
|
9275
|
-
ctx.save();
|
|
9276
|
-
ctx.beginPath();
|
|
9277
|
-
roundedRect(ctx, rect.x, rect.y, rect.width, rect.height, tokens.radius.lg);
|
|
9278
|
-
ctx.fillStyle = tokens.color.surface;
|
|
9279
|
-
ctx.fill();
|
|
9280
|
-
ctx.lineWidth = 1;
|
|
9281
|
-
ctx.strokeStyle = tokens.color.border;
|
|
9282
|
-
ctx.stroke();
|
|
9283
|
-
ctx.restore();
|
|
9284
|
-
}
|
|
9285
|
-
const left = rect.x + (framed ? 0 : pad3);
|
|
9286
|
-
const contentWidth = Math.max(1, rect.width - (framed ? 0 : pad3 * 2));
|
|
9287
|
-
const valueText = value == null ? "\u2014" : formatValue(value, kpi.format);
|
|
9288
|
-
const sizeByCard = Math.min(rect.width, rect.height) * 0.22;
|
|
9289
|
-
const sizeByText = contentWidth / Math.max(1, valueText.length * 0.58);
|
|
9290
|
-
const valueSize = clamp5(Math.min(sizeByCard, sizeByText), 28, 64);
|
|
9291
|
-
const labelH = kpi.label ? tokens.font.size.base * 1.25 : 0;
|
|
9292
|
-
const deltaH = delta != null ? tokens.font.size.large : 0;
|
|
9293
|
-
const stackH = labelH + valueSize + deltaH + tokens.spacing.sm * 2;
|
|
9294
|
-
const avail = rect.height - sparkReserve;
|
|
9295
|
-
let y = rect.y + Math.max(framed ? 0 : pad3, (avail - stackH) / 2);
|
|
9296
|
-
if (kpi.label) {
|
|
9676
|
+
const { geom } = p;
|
|
9677
|
+
const alignX = () => p.align === "start" ? geom.left : p.align === "end" ? geom.left + geom.contentWidth : geom.left + geom.contentWidth / 2;
|
|
9678
|
+
const alignCanvas = textAlignOf(p.align);
|
|
9679
|
+
if (kpi.label && geom.labelY >= 0) {
|
|
9297
9680
|
paintCanvasText(ctx, {
|
|
9298
|
-
x:
|
|
9299
|
-
y,
|
|
9300
|
-
text: kpi.label
|
|
9301
|
-
font: fontString(
|
|
9681
|
+
x: alignX(),
|
|
9682
|
+
y: geom.labelY,
|
|
9683
|
+
text: kpi.label,
|
|
9684
|
+
font: fontString(geom.labelSize, tokens.font.family, tokens.font.weight.medium),
|
|
9302
9685
|
color: tokens.color.textMuted,
|
|
9303
|
-
size:
|
|
9686
|
+
size: geom.labelSize,
|
|
9687
|
+
align: alignCanvas,
|
|
9304
9688
|
baseline: "top"
|
|
9305
9689
|
});
|
|
9306
|
-
y += labelH + tokens.spacing.sm;
|
|
9307
9690
|
}
|
|
9691
|
+
const unitSize = kpi.unit ? unitFontSize(p.valueSize) : 0;
|
|
9692
|
+
const numberFont = fontString(p.valueSize, tokens.font.family, tokens.font.weight.bold);
|
|
9693
|
+
const unitFont = kpi.unit ? fontString(unitSize, tokens.font.family, tokens.font.weight.medium) : "";
|
|
9694
|
+
ctx.save();
|
|
9695
|
+
ctx.font = numberFont;
|
|
9696
|
+
const numberW = ctx.measureText(p.displayText).width;
|
|
9697
|
+
let unitW = 0;
|
|
9698
|
+
if (kpi.unit) {
|
|
9699
|
+
ctx.font = unitFont;
|
|
9700
|
+
unitW = ctx.measureText(kpi.unit).width;
|
|
9701
|
+
}
|
|
9702
|
+
ctx.restore();
|
|
9703
|
+
const unitGap = kpi.unit ? p.valueSize * 0.12 : 0;
|
|
9704
|
+
const totalW = numberW + (kpi.unit ? unitGap + unitW : 0);
|
|
9705
|
+
const groupX = p.align === "start" ? geom.left : p.align === "end" ? geom.left + geom.contentWidth - totalW : geom.left + (geom.contentWidth - totalW) / 2;
|
|
9706
|
+
const baselineY = geom.valueY + p.valueSize * 0.82;
|
|
9308
9707
|
paintCanvasText(ctx, {
|
|
9309
|
-
x:
|
|
9310
|
-
y,
|
|
9311
|
-
text:
|
|
9312
|
-
font:
|
|
9313
|
-
color: tokens.color.text,
|
|
9314
|
-
size: valueSize,
|
|
9315
|
-
|
|
9708
|
+
x: groupX,
|
|
9709
|
+
y: baselineY,
|
|
9710
|
+
text: p.displayText,
|
|
9711
|
+
font: numberFont,
|
|
9712
|
+
color: kpi.color ?? tokens.color.text,
|
|
9713
|
+
size: p.valueSize,
|
|
9714
|
+
align: "left",
|
|
9715
|
+
baseline: "alphabetic"
|
|
9316
9716
|
});
|
|
9317
|
-
|
|
9318
|
-
if (delta != null) {
|
|
9319
|
-
const positive = delta >= 0;
|
|
9717
|
+
if (kpi.unit) {
|
|
9320
9718
|
paintCanvasText(ctx, {
|
|
9321
|
-
x:
|
|
9322
|
-
y,
|
|
9323
|
-
text:
|
|
9324
|
-
font:
|
|
9719
|
+
x: groupX + numberW + unitGap,
|
|
9720
|
+
y: baselineY,
|
|
9721
|
+
text: kpi.unit,
|
|
9722
|
+
font: unitFont,
|
|
9723
|
+
color: tokens.color.textMuted,
|
|
9724
|
+
size: unitSize,
|
|
9725
|
+
align: "left",
|
|
9726
|
+
baseline: "alphabetic"
|
|
9727
|
+
});
|
|
9728
|
+
}
|
|
9729
|
+
if (p.delta != null && geom.deltaY >= 0) {
|
|
9730
|
+
const positive = p.delta >= 0;
|
|
9731
|
+
paintCanvasText(ctx, {
|
|
9732
|
+
x: alignX(),
|
|
9733
|
+
y: geom.deltaY,
|
|
9734
|
+
text: `${positive ? "\u25B2" : "\u25BC"} ${formatDelta(p.delta, kpi.format)}`,
|
|
9735
|
+
font: fontString(geom.deltaSize, tokens.font.family, tokens.font.weight.medium),
|
|
9325
9736
|
color: positive ? tokens.color.positive : tokens.color.negative,
|
|
9326
|
-
size:
|
|
9737
|
+
size: geom.deltaSize,
|
|
9738
|
+
align: alignCanvas,
|
|
9327
9739
|
baseline: "top"
|
|
9328
9740
|
});
|
|
9329
9741
|
}
|
|
9330
|
-
if (
|
|
9331
|
-
|
|
9332
|
-
const inset = framed ? 0 : pad3;
|
|
9333
|
-
const sparkW = Math.max(0, rect.width - inset * 2);
|
|
9334
|
-
const x0 = rect.x + inset;
|
|
9335
|
-
const y1 = rect.y + rect.height - inset;
|
|
9336
|
-
paintSparklineCanvas(ctx, tokens, x0, y1 - sparkH, sparkW, sparkH, sparkValues);
|
|
9742
|
+
if (geom.spark && p.sparkValues.length > 0) {
|
|
9743
|
+
paintSparklineCanvas(ctx, tokens, geom.spark, p.sparkValues, p.markers);
|
|
9337
9744
|
}
|
|
9338
|
-
|
|
9339
|
-
|
|
9340
|
-
|
|
9341
|
-
|
|
9342
|
-
|
|
9343
|
-
|
|
9344
|
-
|
|
9345
|
-
|
|
9346
|
-
|
|
9347
|
-
|
|
9348
|
-
|
|
9349
|
-
|
|
9350
|
-
|
|
9351
|
-
|
|
9352
|
-
|
|
9353
|
-
|
|
9354
|
-
|
|
9355
|
-
|
|
9356
|
-
|
|
9357
|
-
|
|
9358
|
-
|
|
9359
|
-
|
|
9360
|
-
|
|
9361
|
-
|
|
9362
|
-
|
|
9363
|
-
|
|
9364
|
-
|
|
9365
|
-
|
|
9366
|
-
|
|
9367
|
-
|
|
9368
|
-
|
|
9745
|
+
if (p.comparisons.length > 0 && geom.dividerY >= 0) {
|
|
9746
|
+
ctx.save();
|
|
9747
|
+
ctx.fillStyle = tokens.color.border;
|
|
9748
|
+
ctx.fillRect(geom.left, geom.dividerY, geom.contentWidth, 1);
|
|
9749
|
+
ctx.restore();
|
|
9750
|
+
const compFont = fontString(geom.compSize, tokens.font.family, tokens.font.weight.medium);
|
|
9751
|
+
const gap = geom.compSize * 0.5;
|
|
9752
|
+
p.comparisons.forEach((c, i) => {
|
|
9753
|
+
const rowY = geom.compStartY + i * geom.compRowH + geom.compRowH / 2;
|
|
9754
|
+
ctx.save();
|
|
9755
|
+
ctx.font = compFont;
|
|
9756
|
+
const labelW = ctx.measureText(c.label).width;
|
|
9757
|
+
ctx.restore();
|
|
9758
|
+
paintCanvasText(ctx, {
|
|
9759
|
+
x: geom.left,
|
|
9760
|
+
y: rowY,
|
|
9761
|
+
text: c.label,
|
|
9762
|
+
font: compFont,
|
|
9763
|
+
color: tokens.color.text,
|
|
9764
|
+
size: geom.compSize,
|
|
9765
|
+
align: "left",
|
|
9766
|
+
baseline: "middle"
|
|
9767
|
+
});
|
|
9768
|
+
paintCanvasText(ctx, {
|
|
9769
|
+
x: geom.left + labelW + gap,
|
|
9770
|
+
y: rowY,
|
|
9771
|
+
text: c.text,
|
|
9772
|
+
font: compFont,
|
|
9773
|
+
color: c.positive ? tokens.color.positive : tokens.color.negative,
|
|
9774
|
+
size: geom.compSize,
|
|
9775
|
+
align: "left",
|
|
9776
|
+
baseline: "middle"
|
|
9777
|
+
});
|
|
9778
|
+
});
|
|
9369
9779
|
}
|
|
9370
|
-
ctx.restore();
|
|
9371
9780
|
}
|
|
9372
9781
|
|
|
9373
9782
|
// src/charts/sankey.ts
|
|
@@ -9834,18 +10243,24 @@ function drawLegend3(surface, tokens, legend, domain, scheme, format) {
|
|
|
9834
10243
|
transform: "translateX(-100%)"
|
|
9835
10244
|
});
|
|
9836
10245
|
}
|
|
9837
|
-
function drawChoropleth(surface, spec, tokens, size) {
|
|
10246
|
+
function drawChoropleth(surface, spec, tokens, size, context) {
|
|
9838
10247
|
const choropleth = spec;
|
|
10248
|
+
const emphasis = context?.emphasis ?? null;
|
|
9839
10249
|
const content = drawTitleBlock(surface, tokens, size, choropleth.title);
|
|
9840
10250
|
const features = choropleth.geo?.features ?? [];
|
|
9841
10251
|
if (features.length === 0 || content.width <= 0 || content.height <= 0) return;
|
|
9842
10252
|
const readKey = accessor(choropleth.encoding.key.field);
|
|
9843
10253
|
const readColor = accessor(choropleth.encoding.color.field);
|
|
9844
10254
|
const valueByKey = /* @__PURE__ */ new Map();
|
|
10255
|
+
const rowByKey = /* @__PURE__ */ new Map();
|
|
9845
10256
|
for (const row of choropleth.data ?? []) {
|
|
9846
|
-
const
|
|
9847
|
-
const
|
|
9848
|
-
|
|
10257
|
+
const datum = row;
|
|
10258
|
+
const k = toKey(readKey(datum));
|
|
10259
|
+
const v = toNumber(readColor(datum));
|
|
10260
|
+
if (k !== "" && Number.isFinite(v)) {
|
|
10261
|
+
valueByKey.set(k, v);
|
|
10262
|
+
rowByKey.set(k, datum);
|
|
10263
|
+
}
|
|
9849
10264
|
}
|
|
9850
10265
|
const projection = choropleth.projection ?? "mercator";
|
|
9851
10266
|
const project = projector(
|
|
@@ -9863,6 +10278,7 @@ function drawChoropleth(surface, spec, tokens, size) {
|
|
|
9863
10278
|
for (const feature of features) {
|
|
9864
10279
|
const key = featureKey(feature, choropleth.featureId);
|
|
9865
10280
|
const value = valueByKey.has(key) ? valueByKey.get(key) : null;
|
|
10281
|
+
const row = rowByKey.get(key);
|
|
9866
10282
|
if (value != null) {
|
|
9867
10283
|
if (value < vMin) vMin = value;
|
|
9868
10284
|
if (value > vMax) vMax = value;
|
|
@@ -9885,7 +10301,7 @@ function drawChoropleth(surface, spec, tokens, size) {
|
|
|
9885
10301
|
}
|
|
9886
10302
|
if (rings.length) polys.push(rings);
|
|
9887
10303
|
}
|
|
9888
|
-
projected.push({ key, name: featureName(feature, key), value, polys });
|
|
10304
|
+
projected.push({ key, name: featureName(feature, key), value, row, polys });
|
|
9889
10305
|
}
|
|
9890
10306
|
const spanX = maxX - minX;
|
|
9891
10307
|
const spanY = maxY - minY;
|
|
@@ -9932,6 +10348,7 @@ function drawChoropleth(surface, spec, tokens, size) {
|
|
|
9932
10348
|
key: pf.key,
|
|
9933
10349
|
name: pf.name,
|
|
9934
10350
|
value: pf.value,
|
|
10351
|
+
row: pf.row,
|
|
9935
10352
|
color: pf.value == null ? noDataFill : rgbaToCss(cscale.map(pf.value)),
|
|
9936
10353
|
polys,
|
|
9937
10354
|
cx: count ? cx / count : 0,
|
|
@@ -9946,6 +10363,7 @@ function drawChoropleth(surface, spec, tokens, size) {
|
|
|
9946
10363
|
ctx.strokeStyle = tokens.color.background;
|
|
9947
10364
|
ctx.lineWidth = 0.75;
|
|
9948
10365
|
for (const f of screen) {
|
|
10366
|
+
ctx.globalAlpha = rowAlpha(emphasis, f.row ?? { [choropleth.encoding.key.field]: f.key });
|
|
9949
10367
|
ctx.fillStyle = f.color;
|
|
9950
10368
|
for (const polygon of f.polys) {
|
|
9951
10369
|
ctx.beginPath();
|
|
@@ -9966,6 +10384,7 @@ function drawChoropleth(surface, spec, tokens, size) {
|
|
|
9966
10384
|
}
|
|
9967
10385
|
}
|
|
9968
10386
|
}
|
|
10387
|
+
ctx.globalAlpha = 1;
|
|
9969
10388
|
}
|
|
9970
10389
|
ctx.restore();
|
|
9971
10390
|
if (legendHeight > 0) {
|
|
@@ -10120,7 +10539,7 @@ function colorToTone(color) {
|
|
|
10120
10539
|
var HEADER_ROW_HEIGHT = 32;
|
|
10121
10540
|
var MIN_COL_WIDTH = 72;
|
|
10122
10541
|
var DEFAULT_BODY_ROW_HEIGHT = 30;
|
|
10123
|
-
var
|
|
10542
|
+
var SKETCH_BORDER_RADIUS = "14px 6px 16px 7px / 7px 15px 6px 14px";
|
|
10124
10543
|
function formatCellValue(value, column) {
|
|
10125
10544
|
return formatValue(value, column.format);
|
|
10126
10545
|
}
|
|
@@ -10159,8 +10578,8 @@ function buildTable(opts) {
|
|
|
10159
10578
|
overflow: "auto",
|
|
10160
10579
|
pointerEvents: "auto",
|
|
10161
10580
|
boxSizing: "border-box",
|
|
10162
|
-
border: `1px solid ${tokens.color.border}`,
|
|
10163
|
-
borderRadius: opts.sketch ?
|
|
10581
|
+
border: `1px solid ${opts.sketch ? "transparent" : tokens.color.border}`,
|
|
10582
|
+
borderRadius: opts.sketch ? SKETCH_BORDER_RADIUS : `${tokens.radius.sm}px`,
|
|
10164
10583
|
background: tokens.color.background
|
|
10165
10584
|
});
|
|
10166
10585
|
container.replaceChildren();
|
|
@@ -10190,14 +10609,23 @@ function paintTableCanvas(opts) {
|
|
|
10190
10609
|
const available = Math.max(0, rect.width - 2);
|
|
10191
10610
|
const widths = computeFittedWidths(opts.columns, available);
|
|
10192
10611
|
opts.widths = widths;
|
|
10612
|
+
const pen = opts.sketch ? new RoughPen(ctx, opts.sketch) : null;
|
|
10613
|
+
const w = Math.max(0, rect.width);
|
|
10614
|
+
const h = Math.max(0, rect.height);
|
|
10193
10615
|
ctx.save();
|
|
10194
10616
|
ctx.beginPath();
|
|
10195
|
-
|
|
10196
|
-
|
|
10197
|
-
|
|
10198
|
-
|
|
10199
|
-
|
|
10200
|
-
|
|
10617
|
+
if (pen) {
|
|
10618
|
+
ctx.rect(rect.x, rect.y, w, h);
|
|
10619
|
+
ctx.fillStyle = tokens.color.background;
|
|
10620
|
+
ctx.fill();
|
|
10621
|
+
} else {
|
|
10622
|
+
roundedRect(ctx, rect.x, rect.y, w, h, tokens.radius.sm);
|
|
10623
|
+
ctx.fillStyle = tokens.color.background;
|
|
10624
|
+
ctx.fill();
|
|
10625
|
+
ctx.lineWidth = 1;
|
|
10626
|
+
ctx.strokeStyle = tokens.color.border;
|
|
10627
|
+
ctx.stroke();
|
|
10628
|
+
}
|
|
10201
10629
|
ctx.clip();
|
|
10202
10630
|
const headerRows = normalizedHeaderRows(opts);
|
|
10203
10631
|
const headerHeight = headerRows.length * metrics.headerHeight;
|
|
@@ -10209,17 +10637,18 @@ function paintTableCanvas(opts) {
|
|
|
10209
10637
|
const clipped = visibleRows < opts.rowCount;
|
|
10210
10638
|
const cueReserve = clipped && opts.clippedCue !== false && visibleRows > 0 ? metrics.rowHeight : 0;
|
|
10211
10639
|
const rowsToDraw = Math.max(0, visibleRows - (cueReserve > 0 ? 1 : 0));
|
|
10212
|
-
paintHeaderCanvas(opts, widths, headerRows, metrics);
|
|
10640
|
+
paintHeaderCanvas(opts, widths, headerRows, metrics, pen);
|
|
10213
10641
|
for (let rowIndex = 0; rowIndex < rowsToDraw; rowIndex += 1) {
|
|
10214
|
-
paintBodyRowCanvas(opts, widths, rowIndex, bodyTop + rowIndex * metrics.rowHeight, metrics);
|
|
10642
|
+
paintBodyRowCanvas(opts, widths, rowIndex, bodyTop + rowIndex * metrics.rowHeight, metrics, pen);
|
|
10215
10643
|
}
|
|
10216
10644
|
if (cueReserve > 0) {
|
|
10217
10645
|
paintClippedCueCanvas(opts, bodyTop + rowsToDraw * metrics.rowHeight, metrics, opts.rowCount - rowsToDraw);
|
|
10218
10646
|
}
|
|
10219
10647
|
if (opts.footerRow) {
|
|
10220
|
-
paintFooterCanvas(opts, widths, rect.y + rect.height - metrics.rowHeight, metrics);
|
|
10648
|
+
paintFooterCanvas(opts, widths, rect.y + rect.height - metrics.rowHeight, metrics, pen);
|
|
10221
10649
|
}
|
|
10222
10650
|
ctx.restore();
|
|
10651
|
+
if (pen) pen.rect(rect.x, rect.y, w, h, { stroke: tokens.color.border });
|
|
10223
10652
|
}
|
|
10224
10653
|
var FILL_EPS = 0.5;
|
|
10225
10654
|
var cachedScrollbarWidth = null;
|
|
@@ -10312,14 +10741,14 @@ function makeHeaderCell(opts, title2, colIndex, align, rowIndex = 0) {
|
|
|
10312
10741
|
th.textContent = title2;
|
|
10313
10742
|
const isSorted = colIndex != null && opts.sortState?.col === colIndex;
|
|
10314
10743
|
if (isSorted) th.setAttribute("aria-sort", opts.sortState?.dir === "desc" ? "descending" : "ascending");
|
|
10315
|
-
setCellBaseStyles(th, opts.tokens, align ?? "left", opts.stickyHeader !== false, metrics.padding);
|
|
10744
|
+
setCellBaseStyles(th, opts.tokens, align ?? "left", opts.stickyHeader !== false, metrics.padding, gridBorderColor(opts));
|
|
10316
10745
|
setStyles(th, {
|
|
10317
10746
|
top: `${rowIndex * metrics.headerHeight}px`,
|
|
10318
10747
|
zIndex: "4",
|
|
10319
10748
|
background: opts.tokens.color.surface,
|
|
10320
10749
|
color: opts.tokens.color.text,
|
|
10321
10750
|
fontWeight: String(opts.tokens.font.weight.bold),
|
|
10322
|
-
borderBottom: `1px solid ${opts
|
|
10751
|
+
borderBottom: `1px solid ${gridBorderColor(opts)}`,
|
|
10323
10752
|
userSelect: "none",
|
|
10324
10753
|
height: `${metrics.headerHeight}px`
|
|
10325
10754
|
});
|
|
@@ -10377,19 +10806,19 @@ function makeBodyRow(opts, rowIndex, rowHeight) {
|
|
|
10377
10806
|
const td = document.createElement("td");
|
|
10378
10807
|
const cell = opts.getCell(rowIndex, colIndex);
|
|
10379
10808
|
const rowBg = rowBackground(opts, rowIndex, cls);
|
|
10380
|
-
setCellBaseStyles(td, opts.tokens, column.align, false, metrics.padding);
|
|
10809
|
+
setCellBaseStyles(td, opts.tokens, column.align, false, metrics.padding, gridBorderColor(opts));
|
|
10381
10810
|
setStyles(td, {
|
|
10382
10811
|
position: colIndex < (opts.rowHeaderSpan ?? 0) ? "sticky" : "relative",
|
|
10383
10812
|
background: rowBg,
|
|
10384
10813
|
fontWeight: cls === "normal" ? String(opts.tokens.font.weight.normal) : String(opts.tokens.font.weight.bold),
|
|
10385
10814
|
color: opts.tokens.color.text,
|
|
10386
|
-
borderBottom: `1px solid ${opts
|
|
10815
|
+
borderBottom: `1px solid ${gridBorderColor(opts)}`,
|
|
10387
10816
|
height: rowHeight ? `${rowHeight}px` : `${metrics.rowHeight}px`,
|
|
10388
10817
|
whiteSpace: column.wrap === true ? "normal" : "nowrap"
|
|
10389
10818
|
});
|
|
10390
10819
|
const indent = opts.cellIndent?.(rowIndex, colIndex) ?? 0;
|
|
10391
10820
|
if (indent > 0) td.style.paddingLeft = `${metrics.paddingX + indent}px`;
|
|
10392
|
-
applyConditionalFormatting(td, opts, column, colIndex, cell, rowBg);
|
|
10821
|
+
applyConditionalFormatting(td, opts, column, colIndex, cell, rowBg, rowIndex);
|
|
10393
10822
|
if (td.childNodes.length === 0) {
|
|
10394
10823
|
td.textContent = formatDisplayValue(cell.value, column);
|
|
10395
10824
|
applyNegativeStyle(td, cell.value, column);
|
|
@@ -10407,7 +10836,7 @@ function buildFooter(opts) {
|
|
|
10407
10836
|
opts.columns.forEach((column, colIndex) => {
|
|
10408
10837
|
const td = document.createElement("td");
|
|
10409
10838
|
const cell = opts.footerRow?.cells[colIndex] ?? { value: null};
|
|
10410
|
-
setCellBaseStyles(td, opts.tokens, column.align, false, metrics.padding);
|
|
10839
|
+
setCellBaseStyles(td, opts.tokens, column.align, false, metrics.padding, gridBorderColor(opts));
|
|
10411
10840
|
setStyles(td, {
|
|
10412
10841
|
position: colIndex < (opts.rowHeaderSpan ?? 0) ? "sticky" : "sticky",
|
|
10413
10842
|
bottom: "0",
|
|
@@ -10415,8 +10844,8 @@ function buildFooter(opts) {
|
|
|
10415
10844
|
background: opts.tokens.color.surface,
|
|
10416
10845
|
color: opts.tokens.color.text,
|
|
10417
10846
|
fontWeight: String(opts.tokens.font.weight.bold),
|
|
10418
|
-
borderTop: `1px solid ${opts
|
|
10419
|
-
borderBottom: `1px solid ${opts
|
|
10847
|
+
borderTop: `1px solid ${gridBorderColor(opts)}`,
|
|
10848
|
+
borderBottom: `1px solid ${gridBorderColor(opts)}`,
|
|
10420
10849
|
height: `${metrics.rowHeight}px`,
|
|
10421
10850
|
whiteSpace: column.wrap === true ? "normal" : "nowrap"
|
|
10422
10851
|
});
|
|
@@ -10442,7 +10871,7 @@ function makeSpacerRow(opts, height) {
|
|
|
10442
10871
|
tr.appendChild(td);
|
|
10443
10872
|
return tr;
|
|
10444
10873
|
}
|
|
10445
|
-
function applyConditionalFormatting(td, opts, column, colIndex, cell, rowBg) {
|
|
10874
|
+
function applyConditionalFormatting(td, opts, column, colIndex, cell, rowBg, rowIndex = 0) {
|
|
10446
10875
|
const cf = column.conditionalFormat;
|
|
10447
10876
|
const raw = cell.raw;
|
|
10448
10877
|
const domain = opts.conditionalDomains?.[colIndex] ?? null;
|
|
@@ -10487,13 +10916,27 @@ function applyConditionalFormatting(td, opts, column, colIndex, cell, rowBg) {
|
|
|
10487
10916
|
}
|
|
10488
10917
|
if (!domain) return;
|
|
10489
10918
|
const ratio = normalized(numeric, domain);
|
|
10490
|
-
const bar = document.createElement("div");
|
|
10491
10919
|
const diverging2 = cf.negativeColor !== void 0 || cf.baseline === "zero";
|
|
10492
10920
|
const positive = numeric >= 0;
|
|
10493
10921
|
const barColor = alphaColor(positive ? cf.color ?? opts.tokens.color.accent : cf.negativeColor ?? opts.tokens.color.negative, 0.85);
|
|
10494
10922
|
const baseline = diverging2 ? normalized(0, domain) : 0;
|
|
10495
10923
|
const left = diverging2 ? Math.min(baseline, normalized(numeric, domain)) : 0;
|
|
10496
10924
|
const width = diverging2 ? Math.abs(normalized(numeric, domain) - baseline) : ratio;
|
|
10925
|
+
const text = document.createElement("span");
|
|
10926
|
+
text.textContent = cf.showValue === false ? "" : formatDisplayValue(cell.value, column);
|
|
10927
|
+
setStyles(text, overlayTextStyles(opts.tokens.color.text));
|
|
10928
|
+
td.style.background = rowBg;
|
|
10929
|
+
if (opts.sketch) {
|
|
10930
|
+
const colWidth = opts.widths?.[colIndex] ?? column.width ?? defaultColumnWidth(column);
|
|
10931
|
+
const rowHeight = densityMetrics(opts.density).rowHeight;
|
|
10932
|
+
const seed = (opts.sketch.seed ^ Math.imul(rowIndex + 1, 374761393) ^ Math.imul(colIndex + 1, 668265263)) >>> 0;
|
|
10933
|
+
const canvas = buildSketchDataBar(opts.sketch, barColor, colWidth, rowHeight, left, width, seed);
|
|
10934
|
+
if (canvas) td.appendChild(canvas);
|
|
10935
|
+
td.appendChild(text);
|
|
10936
|
+
applyNegativeStyle(text, cell.value, column);
|
|
10937
|
+
return;
|
|
10938
|
+
}
|
|
10939
|
+
const bar = document.createElement("div");
|
|
10497
10940
|
setStyles(bar, {
|
|
10498
10941
|
position: "absolute",
|
|
10499
10942
|
left: `calc(${Math.round(left * 1e3) / 10}% + 4px)`,
|
|
@@ -10505,14 +10948,38 @@ function applyConditionalFormatting(td, opts, column, colIndex, cell, rowBg) {
|
|
|
10505
10948
|
opacity: "0.9",
|
|
10506
10949
|
pointerEvents: "none"
|
|
10507
10950
|
});
|
|
10508
|
-
const text = document.createElement("span");
|
|
10509
|
-
text.textContent = cf.showValue === false ? "" : formatDisplayValue(cell.value, column);
|
|
10510
|
-
setStyles(text, overlayTextStyles(opts.tokens.color.text));
|
|
10511
|
-
td.style.background = rowBg;
|
|
10512
10951
|
td.append(bar, text);
|
|
10513
10952
|
applyNegativeStyle(text, cell.value, column);
|
|
10514
10953
|
}
|
|
10515
|
-
function
|
|
10954
|
+
function buildSketchDataBar(sketch, color, colWidth, rowHeight, leftRatio, widthRatio, seed) {
|
|
10955
|
+
const avail = Math.max(0, colWidth - 8);
|
|
10956
|
+
const barW = widthRatio * avail;
|
|
10957
|
+
const barH = Math.max(0, rowHeight - 10);
|
|
10958
|
+
if (barW <= 0.5 || barH <= 0) return null;
|
|
10959
|
+
const pad3 = 3;
|
|
10960
|
+
const leftPx = 4 + leftRatio * avail;
|
|
10961
|
+
const dpr = getDevicePixelRatio();
|
|
10962
|
+
const canvas = document.createElement("canvas");
|
|
10963
|
+
const cssW = barW + pad3 * 2;
|
|
10964
|
+
const cssH = barH + pad3 * 2;
|
|
10965
|
+
canvas.width = Math.max(1, Math.round(cssW * dpr));
|
|
10966
|
+
canvas.height = Math.max(1, Math.round(cssH * dpr));
|
|
10967
|
+
setStyles(canvas, {
|
|
10968
|
+
position: "absolute",
|
|
10969
|
+
left: `${leftPx - pad3}px`,
|
|
10970
|
+
top: `${5 - pad3}px`,
|
|
10971
|
+
width: `${cssW}px`,
|
|
10972
|
+
height: `${cssH}px`,
|
|
10973
|
+
pointerEvents: "none"
|
|
10974
|
+
});
|
|
10975
|
+
const ctx = canvas.getContext("2d");
|
|
10976
|
+
if (!ctx) return null;
|
|
10977
|
+
ctx.scale(dpr, dpr);
|
|
10978
|
+
const pen = new RoughPen(ctx, { ...sketch, seed });
|
|
10979
|
+
pen.rect(pad3, pad3, barW, barH, { stroke: color, fill: color, fillAlpha: 0.9, strokeAlpha: 0.9 });
|
|
10980
|
+
return canvas;
|
|
10981
|
+
}
|
|
10982
|
+
function setCellBaseStyles(cell, tokens, align, sticky, padding = "6px 10px", borderColor = tokens.color.border) {
|
|
10516
10983
|
setStyles(cell, {
|
|
10517
10984
|
boxSizing: "border-box",
|
|
10518
10985
|
padding,
|
|
@@ -10520,11 +10987,14 @@ function setCellBaseStyles(cell, tokens, align, sticky, padding = "6px 10px") {
|
|
|
10520
10987
|
whiteSpace: "nowrap",
|
|
10521
10988
|
overflow: "hidden",
|
|
10522
10989
|
textOverflow: "ellipsis",
|
|
10523
|
-
borderRight: `1px solid ${
|
|
10990
|
+
borderRight: `1px solid ${borderColor}`,
|
|
10524
10991
|
verticalAlign: "middle"
|
|
10525
10992
|
});
|
|
10526
10993
|
if (sticky) cell.style.position = "sticky";
|
|
10527
10994
|
}
|
|
10995
|
+
function gridBorderColor(opts) {
|
|
10996
|
+
return opts.sketch ? "transparent" : opts.tokens.color.border;
|
|
10997
|
+
}
|
|
10528
10998
|
function renderIconValue(td, icon, color, value, position) {
|
|
10529
10999
|
const iconSpan = document.createElement("span");
|
|
10530
11000
|
iconSpan.textContent = icon;
|
|
@@ -10578,7 +11048,7 @@ function normalizedHeaderRows(opts) {
|
|
|
10578
11048
|
}))
|
|
10579
11049
|
];
|
|
10580
11050
|
}
|
|
10581
|
-
function paintHeaderCanvas(opts, widths, rows, metrics) {
|
|
11051
|
+
function paintHeaderCanvas(opts, widths, rows, metrics, pen) {
|
|
10582
11052
|
rows.forEach((row, rowIndex) => {
|
|
10583
11053
|
let cursor = 0;
|
|
10584
11054
|
for (const cell of row) {
|
|
@@ -10589,7 +11059,7 @@ function paintHeaderCanvas(opts, widths, rows, metrics) {
|
|
|
10589
11059
|
const y = opts.rect.y + rowIndex * metrics.headerHeight;
|
|
10590
11060
|
const w = sumWidths(widths.slice(start, start + colSpan));
|
|
10591
11061
|
const h = metrics.headerHeight * rowSpan;
|
|
10592
|
-
paintCellBox(opts.ctx, x, y, w, h, opts.tokens.color.surface, opts.tokens.color.border);
|
|
11062
|
+
paintCellBox(opts.ctx, x, y, w, h, opts.tokens.color.surface, opts.tokens.color.border, false, pen);
|
|
10593
11063
|
paintCellText(opts, {
|
|
10594
11064
|
x,
|
|
10595
11065
|
y,
|
|
@@ -10605,7 +11075,7 @@ function paintHeaderCanvas(opts, widths, rows, metrics) {
|
|
|
10605
11075
|
}
|
|
10606
11076
|
});
|
|
10607
11077
|
}
|
|
10608
|
-
function paintBodyRowCanvas(opts, widths, rowIndex, y, metrics) {
|
|
11078
|
+
function paintBodyRowCanvas(opts, widths, rowIndex, y, metrics, pen) {
|
|
10609
11079
|
const cls = opts.rowClass?.(rowIndex) ?? "normal";
|
|
10610
11080
|
let x = opts.rect.x;
|
|
10611
11081
|
for (let colIndex = 0; colIndex < opts.columns.length; colIndex += 1) {
|
|
@@ -10615,8 +11085,8 @@ function paintBodyRowCanvas(opts, widths, rowIndex, y, metrics) {
|
|
|
10615
11085
|
const baseBg = canvasRowBackground(opts, rowIndex, cls);
|
|
10616
11086
|
const conditional = resolveCanvasConditional(opts, column, colIndex, cell, baseBg);
|
|
10617
11087
|
const bg = conditional.background ?? baseBg;
|
|
10618
|
-
paintCellBox(opts.ctx, x, y, width, metrics.rowHeight, bg, opts.tokens.color.border);
|
|
10619
|
-
if (conditional.bar) paintDataBar(opts, x, y, width, metrics.rowHeight, conditional.bar);
|
|
11088
|
+
paintCellBox(opts.ctx, x, y, width, metrics.rowHeight, bg, opts.tokens.color.border, false, pen);
|
|
11089
|
+
if (conditional.bar) paintDataBar(opts, x, y, width, metrics.rowHeight, conditional.bar, pen);
|
|
10620
11090
|
const indent = opts.cellIndent?.(rowIndex, colIndex) ?? 0;
|
|
10621
11091
|
const text = conditional.showValue === false ? "" : formatDisplayValue(cell.value, column);
|
|
10622
11092
|
const color = negativeTextColor(cell.value, column, conditional.color ?? opts.tokens.color.text);
|
|
@@ -10638,13 +11108,13 @@ function paintBodyRowCanvas(opts, widths, rowIndex, y, metrics) {
|
|
|
10638
11108
|
x += width;
|
|
10639
11109
|
}
|
|
10640
11110
|
}
|
|
10641
|
-
function paintFooterCanvas(opts, widths, y, metrics) {
|
|
11111
|
+
function paintFooterCanvas(opts, widths, y, metrics, pen) {
|
|
10642
11112
|
let x = opts.rect.x;
|
|
10643
11113
|
for (let colIndex = 0; colIndex < opts.columns.length; colIndex += 1) {
|
|
10644
11114
|
const column = opts.columns[colIndex];
|
|
10645
11115
|
const width = widths[colIndex] ?? 0;
|
|
10646
11116
|
const cell = opts.footerRow?.cells[colIndex] ?? { value: null};
|
|
10647
|
-
paintCellBox(opts.ctx, x, y, width, metrics.rowHeight, opts.tokens.color.surface, opts.tokens.color.border, true);
|
|
11117
|
+
paintCellBox(opts.ctx, x, y, width, metrics.rowHeight, opts.tokens.color.surface, opts.tokens.color.border, true, pen);
|
|
10648
11118
|
paintCellText(opts, {
|
|
10649
11119
|
x,
|
|
10650
11120
|
y,
|
|
@@ -10673,10 +11143,25 @@ function paintClippedCueCanvas(opts, y, metrics, remaining) {
|
|
|
10673
11143
|
paddingX: metrics.paddingX
|
|
10674
11144
|
});
|
|
10675
11145
|
}
|
|
10676
|
-
function paintCellBox(ctx, x, y, width, height, background, border, topBorder = false) {
|
|
11146
|
+
function paintCellBox(ctx, x, y, width, height, background, border, topBorder = false, pen) {
|
|
10677
11147
|
ctx.save();
|
|
10678
11148
|
ctx.fillStyle = background;
|
|
10679
11149
|
ctx.fillRect(x, y, width, height);
|
|
11150
|
+
if (pen) {
|
|
11151
|
+
pen.polyline(
|
|
11152
|
+
[
|
|
11153
|
+
{ x: x + width, y },
|
|
11154
|
+
{ x: x + width, y: y + height },
|
|
11155
|
+
{ x, y: y + height }
|
|
11156
|
+
],
|
|
11157
|
+
{ stroke: border, strokeWidth: 1 }
|
|
11158
|
+
);
|
|
11159
|
+
if (topBorder) {
|
|
11160
|
+
pen.polyline([{ x, y }, { x: x + width, y }], { stroke: border, strokeWidth: 1 });
|
|
11161
|
+
}
|
|
11162
|
+
ctx.restore();
|
|
11163
|
+
return;
|
|
11164
|
+
}
|
|
10680
11165
|
ctx.strokeStyle = border;
|
|
10681
11166
|
ctx.lineWidth = 1;
|
|
10682
11167
|
ctx.beginPath();
|
|
@@ -10787,9 +11272,18 @@ function resolveCanvasConditional(opts, column, colIndex, cell, rowBg) {
|
|
|
10787
11272
|
const color = alphaColor(positive ? cf.color ?? opts.tokens.color.accent : cf.negativeColor ?? opts.tokens.color.negative, 0.85);
|
|
10788
11273
|
return { background: rowBg, showValue: cf.showValue !== false, bar: { leftRatio, widthRatio, color } };
|
|
10789
11274
|
}
|
|
10790
|
-
function paintDataBar(opts, x, y, width, height, bar) {
|
|
11275
|
+
function paintDataBar(opts, x, y, width, height, bar, pen) {
|
|
10791
11276
|
const left = x + 4 + bar.leftRatio * Math.max(0, width - 8);
|
|
10792
11277
|
const barW = bar.widthRatio * Math.max(0, width - 8);
|
|
11278
|
+
if (pen) {
|
|
11279
|
+
pen.rect(left, y + 5, barW, Math.max(0, height - 10), {
|
|
11280
|
+
stroke: bar.color,
|
|
11281
|
+
fill: bar.color,
|
|
11282
|
+
fillAlpha: 0.9,
|
|
11283
|
+
strokeAlpha: 0.9
|
|
11284
|
+
});
|
|
11285
|
+
return;
|
|
11286
|
+
}
|
|
10793
11287
|
opts.ctx.save();
|
|
10794
11288
|
opts.ctx.globalAlpha *= 0.9;
|
|
10795
11289
|
opts.ctx.beginPath();
|
|
@@ -10798,6 +11292,72 @@ function paintDataBar(opts, x, y, width, height, bar) {
|
|
|
10798
11292
|
opts.ctx.fill();
|
|
10799
11293
|
opts.ctx.restore();
|
|
10800
11294
|
}
|
|
11295
|
+
function createSketchGridCanvas(rect) {
|
|
11296
|
+
const dpr = getDevicePixelRatio();
|
|
11297
|
+
const w = Math.max(0, rect.width);
|
|
11298
|
+
const h = Math.max(0, rect.height);
|
|
11299
|
+
const canvas = document.createElement("canvas");
|
|
11300
|
+
canvas.width = Math.max(1, Math.round(w * dpr));
|
|
11301
|
+
canvas.height = Math.max(1, Math.round(h * dpr));
|
|
11302
|
+
setStyles(canvas, {
|
|
11303
|
+
position: "absolute",
|
|
11304
|
+
left: `${rect.x}px`,
|
|
11305
|
+
top: `${rect.y}px`,
|
|
11306
|
+
width: `${w}px`,
|
|
11307
|
+
height: `${h}px`,
|
|
11308
|
+
pointerEvents: "none"
|
|
11309
|
+
});
|
|
11310
|
+
return { canvas, ctx: canvas.getContext("2d"), dpr };
|
|
11311
|
+
}
|
|
11312
|
+
function paintSketchGridOverlay(ctx, o) {
|
|
11313
|
+
const w = Math.max(0, o.rect.width);
|
|
11314
|
+
const h = Math.max(0, o.rect.height);
|
|
11315
|
+
ctx.save();
|
|
11316
|
+
ctx.setTransform(o.dpr, 0, 0, o.dpr, 0, 0);
|
|
11317
|
+
ctx.clearRect(0, 0, w, h);
|
|
11318
|
+
const metrics = densityMetrics(o.density);
|
|
11319
|
+
const headerHeight = o.headerRowCount * metrics.headerHeight;
|
|
11320
|
+
const footerHeight = o.hasFooter ? metrics.rowHeight : 0;
|
|
11321
|
+
const bodyTop = headerHeight;
|
|
11322
|
+
const bodyBottom = Math.max(bodyTop, h - footerHeight);
|
|
11323
|
+
const border = o.tokens.color.border;
|
|
11324
|
+
const base = o.sketch.seed >>> 0;
|
|
11325
|
+
const stroke = (x1, y1, x2, y2, seed) => {
|
|
11326
|
+
const pen = new RoughPen(ctx, { ...o.sketch, seed: seed >>> 0 });
|
|
11327
|
+
pen.polyline(
|
|
11328
|
+
[
|
|
11329
|
+
{ x: x1, y: y1 },
|
|
11330
|
+
{ x: x2, y: y2 }
|
|
11331
|
+
],
|
|
11332
|
+
{ stroke: border, strokeWidth: 1 }
|
|
11333
|
+
);
|
|
11334
|
+
};
|
|
11335
|
+
let cursor = 0;
|
|
11336
|
+
for (let i = 0; i < o.columns.length - 1; i += 1) {
|
|
11337
|
+
cursor += o.widths[i] ?? 0;
|
|
11338
|
+
const pinned = i < (o.rowHeaderSpan ?? 0);
|
|
11339
|
+
const x = pinned ? cursor : cursor - o.scrollLeft;
|
|
11340
|
+
if (x <= 0 || x >= w) continue;
|
|
11341
|
+
stroke(x, 0, x, h, base ^ Math.imul(i + 1, 2246822519));
|
|
11342
|
+
}
|
|
11343
|
+
ctx.save();
|
|
11344
|
+
ctx.beginPath();
|
|
11345
|
+
ctx.rect(0, bodyTop, w, Math.max(0, bodyBottom - bodyTop));
|
|
11346
|
+
ctx.clip();
|
|
11347
|
+
const firstRow = Math.max(0, Math.floor(o.scrollTop / metrics.rowHeight) - 1);
|
|
11348
|
+
for (let r = firstRow; r < o.rowCount; r += 1) {
|
|
11349
|
+
const y = bodyTop + (r + 1) * metrics.rowHeight - o.scrollTop;
|
|
11350
|
+
if (y <= bodyTop) continue;
|
|
11351
|
+
if (y >= bodyBottom) break;
|
|
11352
|
+
stroke(0, y, w, y, base ^ Math.imul(r + 1, 3266489917));
|
|
11353
|
+
}
|
|
11354
|
+
ctx.restore();
|
|
11355
|
+
if (headerHeight > 0 && headerHeight < h) stroke(0, headerHeight, w, headerHeight, (base ^ 2654435769) >>> 0);
|
|
11356
|
+
if (footerHeight > 0 && bodyBottom > bodyTop) stroke(0, bodyBottom, w, bodyBottom, (base ^ 2246822507) >>> 0);
|
|
11357
|
+
const framePen = new RoughPen(ctx, { ...o.sketch, seed: base });
|
|
11358
|
+
framePen.rect(1.5, 1.5, Math.max(0, w - 3), Math.max(0, h - 3), { stroke: border, strokeWidth: 1 });
|
|
11359
|
+
ctx.restore();
|
|
11360
|
+
}
|
|
10801
11361
|
function canvasRowBackground(opts, rowIndex, cls) {
|
|
10802
11362
|
if (cls !== "normal") return opts.tokens.color.surface;
|
|
10803
11363
|
if (opts.striped === true && rowIndex % 2 === 1) return subtleStripe(opts.tokens);
|
|
@@ -10900,7 +11460,7 @@ function drawTableCanvas(surface, spec, tokens, size) {
|
|
|
10900
11460
|
headerRows: buildTableHeaderRows(columns),
|
|
10901
11461
|
footerRow: buildTotalsRow(sortedRows, columns, read, spec),
|
|
10902
11462
|
density: spec.density,
|
|
10903
|
-
sketch: resolveSketch(spec)
|
|
11463
|
+
sketch: resolveSketch(spec)
|
|
10904
11464
|
});
|
|
10905
11465
|
}
|
|
10906
11466
|
function renderTable(surface, spec, tokens, size) {
|
|
@@ -10908,7 +11468,7 @@ function renderTable(surface, spec, tokens, size) {
|
|
|
10908
11468
|
const data = spec.data ?? [];
|
|
10909
11469
|
const columns = resolveColumns(data, spec);
|
|
10910
11470
|
const read = columns.map((column) => accessor(column.key));
|
|
10911
|
-
const sketch = resolveSketch(spec)
|
|
11471
|
+
const sketch = resolveSketch(spec);
|
|
10912
11472
|
let sortState = initialSortState(spec, columns);
|
|
10913
11473
|
let sortedRows = sortRows(data, columns, read, sortState);
|
|
10914
11474
|
const domains = columns.map(
|
|
@@ -10919,6 +11479,9 @@ function renderTable(surface, spec, tokens, size) {
|
|
|
10919
11479
|
);
|
|
10920
11480
|
const host = document.createElement("div");
|
|
10921
11481
|
surface.overlay.appendChild(host);
|
|
11482
|
+
const headerRowCount = buildTableHeaderRows(columns)?.length ?? 1;
|
|
11483
|
+
const overlay = sketch ? createSketchGridCanvas(rect) : null;
|
|
11484
|
+
if (overlay) surface.overlay.appendChild(overlay.canvas);
|
|
10922
11485
|
let frame = 0;
|
|
10923
11486
|
const renderWindow = () => {
|
|
10924
11487
|
const scrollTop = host.scrollTop || 0;
|
|
@@ -10926,7 +11489,8 @@ function renderTable(surface, spec, tokens, size) {
|
|
|
10926
11489
|
const start = Math.max(0, Math.floor(scrollTop / rowHeight) - BUFFER_ROWS);
|
|
10927
11490
|
const visible = Math.ceil(Math.max(rect.height, host.clientHeight || rect.height) / rowHeight) + BUFFER_ROWS * 2;
|
|
10928
11491
|
const end = Math.min(sortedRows.length, start + visible);
|
|
10929
|
-
|
|
11492
|
+
const footerRow = buildTotalsRow(sortedRows, columns, read, spec);
|
|
11493
|
+
const buildOpts = {
|
|
10930
11494
|
container: host,
|
|
10931
11495
|
tokens,
|
|
10932
11496
|
rect,
|
|
@@ -10947,11 +11511,28 @@ function renderTable(surface, spec, tokens, size) {
|
|
|
10947
11511
|
sortState: sortState ?? void 0,
|
|
10948
11512
|
conditionalDomains: domains,
|
|
10949
11513
|
headerRows: buildTableHeaderRows(columns),
|
|
10950
|
-
footerRow
|
|
11514
|
+
footerRow,
|
|
10951
11515
|
density: spec.density,
|
|
10952
11516
|
visibleRange: { start, end, rowHeight },
|
|
10953
11517
|
sketch
|
|
10954
|
-
}
|
|
11518
|
+
};
|
|
11519
|
+
buildTable(buildOpts);
|
|
11520
|
+
if (sketch && overlay?.ctx) {
|
|
11521
|
+
paintSketchGridOverlay(overlay.ctx, {
|
|
11522
|
+
sketch,
|
|
11523
|
+
tokens,
|
|
11524
|
+
rect,
|
|
11525
|
+
dpr: overlay.dpr,
|
|
11526
|
+
columns,
|
|
11527
|
+
widths: buildOpts.widths ?? [],
|
|
11528
|
+
rowCount: sortedRows.length,
|
|
11529
|
+
headerRowCount,
|
|
11530
|
+
hasFooter: Boolean(footerRow),
|
|
11531
|
+
density: spec.density,
|
|
11532
|
+
scrollLeft: host.scrollLeft || 0,
|
|
11533
|
+
scrollTop
|
|
11534
|
+
});
|
|
11535
|
+
}
|
|
10955
11536
|
};
|
|
10956
11537
|
host.addEventListener("scroll", () => {
|
|
10957
11538
|
if (frame !== 0) return;
|
|
@@ -11108,15 +11689,16 @@ function drawMatrixCanvas(surface, spec, tokens, size) {
|
|
|
11108
11689
|
conditionalDomains: prepared.bodyDomains,
|
|
11109
11690
|
headerRows: prepared.headerRows,
|
|
11110
11691
|
density: spec.density,
|
|
11111
|
-
sketch: resolveSketch(spec)
|
|
11692
|
+
sketch: resolveSketch(spec)
|
|
11112
11693
|
});
|
|
11113
11694
|
}
|
|
11114
11695
|
function renderMatrix(surface, spec, tokens, size) {
|
|
11115
11696
|
const rect = drawTitleBlock(surface, tokens, size, spec.title);
|
|
11116
11697
|
const prepared = prepareMatrix(spec);
|
|
11698
|
+
const sketch = resolveSketch(spec);
|
|
11117
11699
|
const host = document.createElement("div");
|
|
11118
11700
|
surface.overlay.appendChild(host);
|
|
11119
|
-
|
|
11701
|
+
const buildOpts = {
|
|
11120
11702
|
container: host,
|
|
11121
11703
|
tokens,
|
|
11122
11704
|
rect,
|
|
@@ -11143,8 +11725,45 @@ function renderMatrix(surface, spec, tokens, size) {
|
|
|
11143
11725
|
conditionalDomains: prepared.bodyDomains,
|
|
11144
11726
|
headerRows: prepared.headerRows,
|
|
11145
11727
|
density: spec.density,
|
|
11146
|
-
sketch
|
|
11147
|
-
}
|
|
11728
|
+
sketch
|
|
11729
|
+
};
|
|
11730
|
+
buildTable(buildOpts);
|
|
11731
|
+
if (sketch) {
|
|
11732
|
+
const overlay = createSketchGridCanvas(rect);
|
|
11733
|
+
if (overlay.ctx) {
|
|
11734
|
+
surface.overlay.appendChild(overlay.canvas);
|
|
11735
|
+
const ctx = overlay.ctx;
|
|
11736
|
+
const draw = () => {
|
|
11737
|
+
paintSketchGridOverlay(ctx, {
|
|
11738
|
+
sketch,
|
|
11739
|
+
tokens,
|
|
11740
|
+
rect,
|
|
11741
|
+
dpr: overlay.dpr,
|
|
11742
|
+
columns: prepared.columns,
|
|
11743
|
+
widths: buildOpts.widths ?? [],
|
|
11744
|
+
rowCount: prepared.rows.length,
|
|
11745
|
+
headerRowCount: prepared.headerRows.length,
|
|
11746
|
+
hasFooter: false,
|
|
11747
|
+
rowHeaderSpan: 1,
|
|
11748
|
+
density: spec.density,
|
|
11749
|
+
scrollLeft: host.scrollLeft || 0,
|
|
11750
|
+
scrollTop: host.scrollTop || 0
|
|
11751
|
+
});
|
|
11752
|
+
};
|
|
11753
|
+
draw();
|
|
11754
|
+
let frame = 0;
|
|
11755
|
+
host.addEventListener("scroll", () => {
|
|
11756
|
+
if (frame !== 0) return;
|
|
11757
|
+
frame = typeof requestAnimationFrame === "function" ? requestAnimationFrame(() => {
|
|
11758
|
+
frame = 0;
|
|
11759
|
+
draw();
|
|
11760
|
+
}) : window.setTimeout(() => {
|
|
11761
|
+
frame = 0;
|
|
11762
|
+
draw();
|
|
11763
|
+
}, 16);
|
|
11764
|
+
});
|
|
11765
|
+
}
|
|
11766
|
+
}
|
|
11148
11767
|
}
|
|
11149
11768
|
function prepareMatrix(spec) {
|
|
11150
11769
|
const data = spec.data ?? [];
|
|
@@ -13721,11 +14340,6 @@ function drawTreemap(surface, spec, tokens, size, context) {
|
|
|
13721
14340
|
function clamp8(value, min, max) {
|
|
13722
14341
|
return Math.max(min, Math.min(max, value));
|
|
13723
14342
|
}
|
|
13724
|
-
function resolveValue2(ref, data) {
|
|
13725
|
-
if (ref == null) return null;
|
|
13726
|
-
if (typeof ref === "number") return Number.isFinite(ref) ? ref : null;
|
|
13727
|
-
return aggregateValues((data ?? []).map(accessor(ref.field)), ref.aggregate ?? "sum");
|
|
13728
|
-
}
|
|
13729
14343
|
function valueField2(ref) {
|
|
13730
14344
|
return typeof ref === "number" ? null : ref.field;
|
|
13731
14345
|
}
|
|
@@ -13749,7 +14363,7 @@ function arcPoints(cx, cy, radius, startAngle, endAngle) {
|
|
|
13749
14363
|
}
|
|
13750
14364
|
return points;
|
|
13751
14365
|
}
|
|
13752
|
-
function strokePoints(ctx, pen, points, color, width) {
|
|
14366
|
+
function strokePoints(ctx, pen, points, color, width, cap2 = "butt") {
|
|
13753
14367
|
if (points.length < 2) return;
|
|
13754
14368
|
if (pen) {
|
|
13755
14369
|
pen.polyline(points, { stroke: color, strokeWidth: width });
|
|
@@ -13760,29 +14374,12 @@ function strokePoints(ctx, pen, points, color, width) {
|
|
|
13760
14374
|
ctx.moveTo(points[0].x, points[0].y);
|
|
13761
14375
|
for (let i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y);
|
|
13762
14376
|
ctx.lineWidth = width;
|
|
13763
|
-
ctx.lineCap =
|
|
14377
|
+
ctx.lineCap = cap2;
|
|
13764
14378
|
ctx.lineJoin = "round";
|
|
13765
14379
|
ctx.strokeStyle = color;
|
|
13766
14380
|
ctx.stroke();
|
|
13767
14381
|
ctx.restore();
|
|
13768
14382
|
}
|
|
13769
|
-
function drawCircle(ctx, pen, cx, cy, radius, fill, stroke) {
|
|
13770
|
-
if (pen) {
|
|
13771
|
-
pen.circle(cx, cy, radius, { fill, stroke: stroke ?? fill, strokeWidth: 1 });
|
|
13772
|
-
return;
|
|
13773
|
-
}
|
|
13774
|
-
ctx.save();
|
|
13775
|
-
ctx.beginPath();
|
|
13776
|
-
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
|
|
13777
|
-
ctx.fillStyle = fill;
|
|
13778
|
-
ctx.fill();
|
|
13779
|
-
if (stroke) {
|
|
13780
|
-
ctx.strokeStyle = stroke;
|
|
13781
|
-
ctx.lineWidth = 1;
|
|
13782
|
-
ctx.stroke();
|
|
13783
|
-
}
|
|
13784
|
-
ctx.restore();
|
|
13785
|
-
}
|
|
13786
14383
|
function drawCenteredMessage(surface, tokens, rect, text) {
|
|
13787
14384
|
addOverlayText(surface, tokens, {
|
|
13788
14385
|
left: rect.x,
|
|
@@ -13794,86 +14391,158 @@ function drawCenteredMessage(surface, tokens, rect, text) {
|
|
|
13794
14391
|
align: "center"
|
|
13795
14392
|
});
|
|
13796
14393
|
}
|
|
14394
|
+
var LEGEND_SWATCH3 = 11;
|
|
14395
|
+
function measureLegendWidth(entries, font) {
|
|
14396
|
+
let maxLabel = 0;
|
|
14397
|
+
for (const e of entries) maxLabel = Math.max(maxLabel, measureText(e.label, font).width);
|
|
14398
|
+
return Math.ceil(LEGEND_SWATCH3 + 6 + maxLabel + 4);
|
|
14399
|
+
}
|
|
14400
|
+
function drawGaugeLegend(surface, tokens, area2, entries) {
|
|
14401
|
+
if (entries.length === 0) return;
|
|
14402
|
+
const labelSize = tokens.font.size.small;
|
|
14403
|
+
const rowH = Math.round(labelSize * 1.7);
|
|
14404
|
+
const font = fontString(labelSize, tokens.font.family, tokens.font.weight.normal);
|
|
14405
|
+
const colW = measureLegendWidth(entries, font);
|
|
14406
|
+
const x = area2.x + area2.width - colW;
|
|
14407
|
+
let y = area2.y;
|
|
14408
|
+
if (surface.headless) {
|
|
14409
|
+
const ctx = surface.marks.ctx;
|
|
14410
|
+
for (const e of entries) {
|
|
14411
|
+
const midY = y + rowH / 2;
|
|
14412
|
+
paintLegendSwatch(ctx, x, midY, "square", e.color, LEGEND_SWATCH3);
|
|
14413
|
+
paintCanvasText(ctx, {
|
|
14414
|
+
x: x + LEGEND_SWATCH3 + 6,
|
|
14415
|
+
y: midY,
|
|
14416
|
+
text: e.label,
|
|
14417
|
+
font,
|
|
14418
|
+
color: tokens.color.text,
|
|
14419
|
+
size: labelSize,
|
|
14420
|
+
baseline: "middle"
|
|
14421
|
+
});
|
|
14422
|
+
y += rowH;
|
|
14423
|
+
}
|
|
14424
|
+
return;
|
|
14425
|
+
}
|
|
14426
|
+
for (const e of entries) {
|
|
14427
|
+
const row = document.createElement("div");
|
|
14428
|
+
row.style.position = "absolute";
|
|
14429
|
+
row.style.left = `${x}px`;
|
|
14430
|
+
row.style.top = `${y}px`;
|
|
14431
|
+
row.style.display = "flex";
|
|
14432
|
+
row.style.alignItems = "center";
|
|
14433
|
+
row.style.gap = "6px";
|
|
14434
|
+
row.style.pointerEvents = "none";
|
|
14435
|
+
const swatch = document.createElement("span");
|
|
14436
|
+
swatch.style.flex = "0 0 auto";
|
|
14437
|
+
swatch.style.width = `${LEGEND_SWATCH3}px`;
|
|
14438
|
+
swatch.style.height = `${LEGEND_SWATCH3}px`;
|
|
14439
|
+
swatch.style.borderRadius = "2px";
|
|
14440
|
+
swatch.style.background = e.color;
|
|
14441
|
+
const label = document.createElement("span");
|
|
14442
|
+
label.textContent = e.label;
|
|
14443
|
+
label.style.font = font;
|
|
14444
|
+
label.style.color = tokens.color.text;
|
|
14445
|
+
label.style.whiteSpace = "nowrap";
|
|
14446
|
+
row.appendChild(swatch);
|
|
14447
|
+
row.appendChild(label);
|
|
14448
|
+
surface.overlay.appendChild(row);
|
|
14449
|
+
y += rowH;
|
|
14450
|
+
}
|
|
14451
|
+
}
|
|
14452
|
+
function bandFillColor(gauge, value, tokens, accent) {
|
|
14453
|
+
const bands = gauge.bands;
|
|
14454
|
+
if (!bands?.length) return accent;
|
|
14455
|
+
const palette = tokens.color.palette;
|
|
14456
|
+
for (let i = 0; i < bands.length; i++) {
|
|
14457
|
+
if (value <= bands[i].to) return bands[i].color ?? palette[i % palette.length] ?? accent;
|
|
14458
|
+
}
|
|
14459
|
+
const last = bands.length - 1;
|
|
14460
|
+
return bands[last].color ?? palette[last % palette.length] ?? accent;
|
|
14461
|
+
}
|
|
13797
14462
|
function drawGauge(surface, spec, tokens, size, context) {
|
|
13798
14463
|
const gauge = spec;
|
|
13799
14464
|
const ctx = surface.marks.ctx;
|
|
13800
14465
|
const content = drawTitleBlock(surface, tokens, size, gauge.title);
|
|
13801
14466
|
const data = gauge.data ?? [];
|
|
13802
|
-
const value =
|
|
13803
|
-
const target =
|
|
14467
|
+
const value = resolveValueRef(gauge.value, data);
|
|
14468
|
+
const target = resolveValueRef(gauge.target, data);
|
|
14469
|
+
const compare2 = resolveValueRef(gauge.compare, data);
|
|
13804
14470
|
const min = gauge.min ?? 0;
|
|
13805
14471
|
const max = gauge.max;
|
|
13806
14472
|
if (value == null || !Number.isFinite(min) || !Number.isFinite(max) || max <= min || content.width <= 0 || content.height <= 0) {
|
|
13807
14473
|
drawCenteredMessage(surface, tokens, content, "No value");
|
|
13808
14474
|
return;
|
|
13809
14475
|
}
|
|
14476
|
+
const sketch = resolveSketch(spec);
|
|
14477
|
+
const pen = sketch ? new RoughPen(ctx, sketch) : null;
|
|
14478
|
+
const accent = tokens.color.palette[0] ?? tokens.color.accent;
|
|
14479
|
+
const fill = bandFillColor(gauge, value, tokens, accent);
|
|
14480
|
+
const track = tokens.dark ? withAlpha3(tokens.color.textMuted, 0.3) : tokens.color.border;
|
|
14481
|
+
const compareColor = tokens.color.textMuted;
|
|
14482
|
+
const markColor = tokens.dark ? withAlpha3("#ffffff", 0.92) : withAlpha3(tokens.color.text, 0.82);
|
|
14483
|
+
const targetMark = target != null && target >= min && target <= max ? target : null;
|
|
13810
14484
|
const valueText = formatValue(value, gauge.format);
|
|
13811
14485
|
const caption = gauge.label ?? titleText(gauge.title) ?? valueField2(gauge.value) ?? "Value";
|
|
13812
|
-
const valueSize = clamp8(Math.min(content.width / Math.max(6, valueText.length * 0.55), tokens.font.size.title * 2.4), 24, 42);
|
|
13813
14486
|
const captionSize = tokens.font.size.small;
|
|
13814
|
-
const
|
|
13815
|
-
const
|
|
13816
|
-
const
|
|
13817
|
-
|
|
13818
|
-
|
|
13819
|
-
|
|
13820
|
-
|
|
13821
|
-
|
|
14487
|
+
const captionReserve = captionSize + 16;
|
|
14488
|
+
const showLegend = gauge.legend ?? compare2 != null;
|
|
14489
|
+
const legendEntries = [];
|
|
14490
|
+
if (showLegend) {
|
|
14491
|
+
legendEntries.push({ label: gauge.valueLabel ?? "Actual", color: fill, symbol: "square" });
|
|
14492
|
+
if (compare2 != null) {
|
|
14493
|
+
legendEntries.push({ label: gauge.compareLabel ?? "Prior period", color: compareColor, symbol: "square" });
|
|
14494
|
+
}
|
|
14495
|
+
legendEntries.push({ label: gauge.trackLabel ?? "Target", color: targetMark != null ? markColor : track, symbol: "square" });
|
|
14496
|
+
}
|
|
14497
|
+
const legendFont = fontString(tokens.font.size.small, tokens.font.family, tokens.font.weight.normal);
|
|
14498
|
+
const legendW = legendEntries.length ? measureLegendWidth(legendEntries, legendFont) + 12 : 0;
|
|
14499
|
+
const dialW = Math.max(40, content.width - legendW);
|
|
14500
|
+
const radius = clamp8(
|
|
14501
|
+
Math.min((dialW - 16) / 2, content.height - captionReserve - 16),
|
|
14502
|
+
18,
|
|
14503
|
+
Math.min(dialW, content.height * 2)
|
|
14504
|
+
);
|
|
14505
|
+
const trackWidth = clamp8(radius * 0.17, 14, 54);
|
|
14506
|
+
const cx = content.x + dialW / 2;
|
|
14507
|
+
const blockH = radius + trackWidth / 2 + captionReserve;
|
|
14508
|
+
const topMargin = Math.max(0, (content.height - blockH) / 2);
|
|
14509
|
+
const cy = content.y + topMargin + radius + trackWidth / 2;
|
|
13822
14510
|
const toAngle = (v) => Math.PI - clamp8((v - min) / (max - min), 0, 1) * Math.PI;
|
|
13823
|
-
const
|
|
13824
|
-
const
|
|
13825
|
-
const
|
|
14511
|
+
const sweepT = context?.sweepProgress ?? 1;
|
|
14512
|
+
const from = context?.valueFrom ?? min;
|
|
14513
|
+
const displayValue = from + (value - from) * sweepT;
|
|
14514
|
+
const shownText = formatValue(displayValue, gauge.format);
|
|
14515
|
+
const valueAngle = toAngle(displayValue);
|
|
13826
14516
|
strokePoints(ctx, pen, arcPoints(cx, cy, radius, Math.PI, 0), track, trackWidth);
|
|
13827
|
-
if (
|
|
13828
|
-
|
|
13829
|
-
|
|
13830
|
-
|
|
13831
|
-
|
|
13832
|
-
|
|
13833
|
-
|
|
13834
|
-
|
|
13835
|
-
|
|
13836
|
-
if (
|
|
13837
|
-
strokePoints(ctx, pen, arcPoints(cx, cy,
|
|
13838
|
-
}
|
|
13839
|
-
}
|
|
13840
|
-
|
|
13841
|
-
|
|
13842
|
-
|
|
13843
|
-
|
|
13844
|
-
|
|
13845
|
-
|
|
13846
|
-
|
|
13847
|
-
|
|
13848
|
-
|
|
13849
|
-
|
|
13850
|
-
|
|
13851
|
-
const leftTick = pointOnArc(cx, cy, radius, Math.PI);
|
|
13852
|
-
const rightTick = pointOnArc(cx, cy, radius, 0);
|
|
13853
|
-
addOverlayText(surface, tokens, {
|
|
13854
|
-
left: leftTick.x - 32,
|
|
13855
|
-
top: leftTick.y + 6,
|
|
13856
|
-
width: 64,
|
|
13857
|
-
text: formatValue(min, gauge.format),
|
|
13858
|
-
color: tokens.color.textMuted,
|
|
13859
|
-
size: tokens.font.size.tiny,
|
|
13860
|
-
align: "center"
|
|
13861
|
-
});
|
|
13862
|
-
addOverlayText(surface, tokens, {
|
|
13863
|
-
left: rightTick.x - 32,
|
|
13864
|
-
top: rightTick.y + 6,
|
|
13865
|
-
width: 64,
|
|
13866
|
-
text: formatValue(max, gauge.format),
|
|
13867
|
-
color: tokens.color.textMuted,
|
|
13868
|
-
size: tokens.font.size.tiny,
|
|
13869
|
-
align: "center"
|
|
13870
|
-
});
|
|
13871
|
-
const readoutTop = Math.min(content.y + content.height - valueSize - captionSize - 8, cy + 14);
|
|
14517
|
+
if (valueAngle < Math.PI) {
|
|
14518
|
+
strokePoints(ctx, pen, arcPoints(cx, cy, radius, Math.PI, valueAngle), fill, trackWidth);
|
|
14519
|
+
}
|
|
14520
|
+
if (compare2 != null) {
|
|
14521
|
+
const innerWidth = clamp8(trackWidth * 0.42, 6, 22);
|
|
14522
|
+
const innerRadius = Math.max(radius * 0.35, radius - trackWidth / 2 - 2 - innerWidth / 2);
|
|
14523
|
+
const compareFrom = context?.compareFrom ?? min;
|
|
14524
|
+
const displayCompare = compareFrom + (compare2 - compareFrom) * sweepT;
|
|
14525
|
+
const compareAngle = toAngle(displayCompare);
|
|
14526
|
+
if (compareAngle < Math.PI) {
|
|
14527
|
+
strokePoints(ctx, pen, arcPoints(cx, cy, innerRadius, Math.PI, compareAngle), compareColor, innerWidth);
|
|
14528
|
+
}
|
|
14529
|
+
}
|
|
14530
|
+
if (targetMark != null) {
|
|
14531
|
+
const a = toAngle(targetMark);
|
|
14532
|
+
const overhang = clamp8(trackWidth * 0.3, 4, 10);
|
|
14533
|
+
const outer = pointOnArc(cx, cy, radius + trackWidth / 2 + overhang, a);
|
|
14534
|
+
const inner = pointOnArc(cx, cy, radius - trackWidth / 2 - overhang, a);
|
|
14535
|
+
strokePoints(ctx, pen, [outer, inner], markColor, 3, "round");
|
|
14536
|
+
}
|
|
14537
|
+
if (legendEntries.length) {
|
|
14538
|
+
drawGaugeLegend(surface, tokens, { x: content.x, y: content.y, width: content.width, height: content.height }, legendEntries);
|
|
14539
|
+
}
|
|
14540
|
+
const valueSize = clamp8(radius * 0.5, 22, 72);
|
|
13872
14541
|
addOverlayText(surface, tokens, {
|
|
13873
14542
|
left: content.x,
|
|
13874
|
-
top:
|
|
13875
|
-
width:
|
|
13876
|
-
text:
|
|
14543
|
+
top: cy - radius * 0.32 - valueSize / 2,
|
|
14544
|
+
width: dialW,
|
|
14545
|
+
text: shownText,
|
|
13877
14546
|
color: tokens.color.text,
|
|
13878
14547
|
size: valueSize,
|
|
13879
14548
|
weight: tokens.font.weight.bold,
|
|
@@ -13881,20 +14550,20 @@ function drawGauge(surface, spec, tokens, size, context) {
|
|
|
13881
14550
|
});
|
|
13882
14551
|
addOverlayText(surface, tokens, {
|
|
13883
14552
|
left: content.x,
|
|
13884
|
-
top:
|
|
13885
|
-
width:
|
|
14553
|
+
top: cy + 8,
|
|
14554
|
+
width: dialW,
|
|
13886
14555
|
text: caption,
|
|
13887
14556
|
color: tokens.color.textMuted,
|
|
13888
14557
|
size: captionSize,
|
|
13889
14558
|
align: "center"
|
|
13890
14559
|
});
|
|
13891
14560
|
const hitTest = (px, py) => {
|
|
13892
|
-
const
|
|
13893
|
-
const dy = py - cy;
|
|
13894
|
-
const distance = Math.hypot(dx, dy);
|
|
13895
|
-
const withinDial = distance <= radius + trackWidth * 2 && py >= cy - radius - trackWidth * 2 && py <= cy + readoutReserve;
|
|
14561
|
+
const withinDial = px >= cx - radius - trackWidth && px <= cx + radius + trackWidth && py >= cy - radius - trackWidth && py <= cy + captionReserve + 4;
|
|
13896
14562
|
if (!withinDial) return null;
|
|
13897
|
-
const rows = [{ label: "value", value: valueText }];
|
|
14563
|
+
const rows = [{ label: gauge.valueLabel ?? "value", value: valueText }];
|
|
14564
|
+
if (compare2 != null) {
|
|
14565
|
+
rows.push({ label: gauge.compareLabel ?? "prior period", value: formatValue(compare2, gauge.format), muted: true });
|
|
14566
|
+
}
|
|
13898
14567
|
if (target != null) rows.push({ label: "target", value: formatValue(target, gauge.format), muted: true });
|
|
13899
14568
|
return {
|
|
13900
14569
|
key: "value",
|
|
@@ -13922,7 +14591,7 @@ function titleText2(title2) {
|
|
|
13922
14591
|
function valueField3(ref) {
|
|
13923
14592
|
return typeof ref === "number" ? null : ref.field;
|
|
13924
14593
|
}
|
|
13925
|
-
function
|
|
14594
|
+
function resolveValue(ref, data) {
|
|
13926
14595
|
if (ref == null) return null;
|
|
13927
14596
|
if (typeof ref === "number") return Number.isFinite(ref) ? ref : null;
|
|
13928
14597
|
return aggregateValues((data ?? []).map(accessor(ref.field)), ref.aggregate ?? "sum");
|
|
@@ -13954,8 +14623,8 @@ function drawBullet(surface, spec, tokens, size, context) {
|
|
|
13954
14623
|
const ctx = surface.marks.ctx;
|
|
13955
14624
|
const content = drawTitleBlock(surface, tokens, size, bullet.title);
|
|
13956
14625
|
const data = bullet.data ?? [];
|
|
13957
|
-
const value =
|
|
13958
|
-
const target =
|
|
14626
|
+
const value = resolveValue(bullet.value, data);
|
|
14627
|
+
const target = resolveValue(bullet.target, data);
|
|
13959
14628
|
const caption = bullet.label ?? titleText2(bullet.title) ?? valueField3(bullet.value) ?? "Value";
|
|
13960
14629
|
const fmt = bullet.format;
|
|
13961
14630
|
const finiteRanges = (bullet.ranges ?? []).filter((r) => Number.isFinite(r)).sort((a, b) => a - b);
|
|
@@ -14138,7 +14807,7 @@ function noData(surface, tokens, content) {
|
|
|
14138
14807
|
}
|
|
14139
14808
|
function normalizeDays(days) {
|
|
14140
14809
|
if (Array.isArray(days)) {
|
|
14141
|
-
return days.filter((d) => Number.isFinite(d.value)).map((d) => ({ isoDate: isoDay(d.date), date: startOfLocalDay(d.date), value: d.value }));
|
|
14810
|
+
return days.filter((d) => Number.isFinite(d.value)).map((d) => ({ isoDate: isoDay(d.date), date: startOfLocalDay(d.date), value: d.value, row: d.row }));
|
|
14142
14811
|
}
|
|
14143
14812
|
const out = [];
|
|
14144
14813
|
const dayMap = days;
|
|
@@ -14160,19 +14829,20 @@ function buildCalendarGrid(days) {
|
|
|
14160
14829
|
const start = addDays(minDay, -minDay.getDay());
|
|
14161
14830
|
const end = addDays(maxDay, 6 - maxDay.getDay());
|
|
14162
14831
|
const weeks = Math.floor(diffDays(end, start) / 7) + 1;
|
|
14163
|
-
const byValue = new Map(values.map((d) => [d.isoDate, d.value]));
|
|
14832
|
+
const byValue = new Map(values.map((d) => [d.isoDate, { value: d.value, row: d.row }]));
|
|
14164
14833
|
const cells = [];
|
|
14165
14834
|
const byIsoDate = /* @__PURE__ */ new Map();
|
|
14166
14835
|
for (let d = start; diffDays(end, d) >= 0; d = addDays(d, 1)) {
|
|
14167
14836
|
const isoDate = isoDay(d);
|
|
14168
|
-
const
|
|
14837
|
+
const dayValue = byValue.get(isoDate);
|
|
14169
14838
|
const cell = {
|
|
14170
14839
|
isoDate,
|
|
14171
14840
|
date: d,
|
|
14172
14841
|
week: Math.floor(diffDays(d, start) / 7),
|
|
14173
14842
|
weekday: d.getDay(),
|
|
14174
|
-
value,
|
|
14175
|
-
|
|
14843
|
+
value: dayValue?.value,
|
|
14844
|
+
row: dayValue?.row,
|
|
14845
|
+
present: dayValue !== void 0
|
|
14176
14846
|
};
|
|
14177
14847
|
cells.push(cell);
|
|
14178
14848
|
byIsoDate.set(isoDate, cell);
|
|
@@ -14191,9 +14861,9 @@ function collectDays(spec) {
|
|
|
14191
14861
|
const isoDate = isoDay(day);
|
|
14192
14862
|
const current = totals.get(isoDate);
|
|
14193
14863
|
if (current) current.value += value;
|
|
14194
|
-
else totals.set(isoDate, { date: day, value });
|
|
14864
|
+
else totals.set(isoDate, { date: day, value, row });
|
|
14195
14865
|
}
|
|
14196
|
-
return [...totals.entries()].map(([isoDate, { date, value }]) => ({ isoDate, date, value }));
|
|
14866
|
+
return [...totals.entries()].map(([isoDate, { date, value, row }]) => ({ isoDate, date, value, row }));
|
|
14197
14867
|
}
|
|
14198
14868
|
function monthAnchors(grid) {
|
|
14199
14869
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -14217,6 +14887,8 @@ function humanDate(date) {
|
|
|
14217
14887
|
function drawCalendarHeatmap(surface, spec, tokens, size, context) {
|
|
14218
14888
|
const cal = spec;
|
|
14219
14889
|
const ctx = surface.marks.ctx;
|
|
14890
|
+
const emphasis = context?.emphasis ?? null;
|
|
14891
|
+
const dateField = cal.encoding.date.field;
|
|
14220
14892
|
const content = drawTitleBlock(surface, tokens, size, cal.title);
|
|
14221
14893
|
const days = collectDays(cal);
|
|
14222
14894
|
const grid = buildCalendarGrid(days);
|
|
@@ -14256,6 +14928,7 @@ function drawCalendarHeatmap(surface, spec, tokens, size, context) {
|
|
|
14256
14928
|
const y = gridRect.y + c.weekday * (cell + GAP2);
|
|
14257
14929
|
const fill = c.present && c.value !== void 0 ? rgbaToCss(cscale.map(c.value)) : tokens.color.surface;
|
|
14258
14930
|
laidOut.set(c.isoDate, { ...c, x, y, size: cell, fill });
|
|
14931
|
+
ctx.globalAlpha = c.present ? rowAlpha(emphasis, c.row ?? { [dateField]: c.isoDate }) : 1;
|
|
14259
14932
|
if (pen) {
|
|
14260
14933
|
pen.rect(x, y, cell, cell, {
|
|
14261
14934
|
fill,
|
|
@@ -14269,6 +14942,7 @@ function drawCalendarHeatmap(surface, spec, tokens, size, context) {
|
|
|
14269
14942
|
ctx.lineWidth = 1;
|
|
14270
14943
|
ctx.strokeRect(x + 0.5, y + 0.5, Math.max(0, cell - 1), Math.max(0, cell - 1));
|
|
14271
14944
|
}
|
|
14945
|
+
ctx.globalAlpha = 1;
|
|
14272
14946
|
}
|
|
14273
14947
|
ctx.restore();
|
|
14274
14948
|
for (const m of monthAnchors(grid)) {
|
|
@@ -14546,13 +15220,15 @@ function buildWaterfallModel(spec, tokens, opts) {
|
|
|
14546
15220
|
}
|
|
14547
15221
|
|
|
14548
15222
|
// src/charts/waterfall.ts
|
|
14549
|
-
function drawConnectors(surface, model) {
|
|
15223
|
+
function drawConnectors(surface, model, alphaOf) {
|
|
14550
15224
|
if (model.connectors.length === 0) return;
|
|
14551
15225
|
const ctx = surface.marks.ctx;
|
|
14552
15226
|
const color = model.tokens.color.axis;
|
|
14553
15227
|
if (model.sketch) {
|
|
14554
15228
|
const pen = new RoughPen(ctx, model.sketch);
|
|
14555
|
-
for (
|
|
15229
|
+
for (let i = 0; i < model.connectors.length; i += 1) {
|
|
15230
|
+
const c = model.connectors[i];
|
|
15231
|
+
ctx.globalAlpha = alphaOf(model.bars[i]);
|
|
14556
15232
|
pen.polyline(
|
|
14557
15233
|
[
|
|
14558
15234
|
{ x: c.x1, y: c.y },
|
|
@@ -14560,6 +15236,7 @@ function drawConnectors(surface, model) {
|
|
|
14560
15236
|
],
|
|
14561
15237
|
{ stroke: color }
|
|
14562
15238
|
);
|
|
15239
|
+
ctx.globalAlpha = 1;
|
|
14563
15240
|
}
|
|
14564
15241
|
return;
|
|
14565
15242
|
}
|
|
@@ -14567,23 +15244,28 @@ function drawConnectors(surface, model) {
|
|
|
14567
15244
|
ctx.strokeStyle = color;
|
|
14568
15245
|
ctx.lineWidth = 1;
|
|
14569
15246
|
ctx.setLineDash([2, 2]);
|
|
14570
|
-
|
|
14571
|
-
|
|
15247
|
+
for (let i = 0; i < model.connectors.length; i += 1) {
|
|
15248
|
+
const c = model.connectors[i];
|
|
14572
15249
|
const y = Math.round(c.y) + 0.5;
|
|
15250
|
+
ctx.globalAlpha = alphaOf(model.bars[i]);
|
|
15251
|
+
ctx.beginPath();
|
|
14573
15252
|
ctx.moveTo(c.x1, y);
|
|
14574
15253
|
ctx.lineTo(c.x2, y);
|
|
15254
|
+
ctx.stroke();
|
|
15255
|
+
ctx.globalAlpha = 1;
|
|
14575
15256
|
}
|
|
14576
|
-
ctx.stroke();
|
|
14577
15257
|
ctx.restore();
|
|
14578
15258
|
}
|
|
14579
|
-
function drawBars2(surface, model) {
|
|
15259
|
+
function drawBars2(surface, model, alphaOf) {
|
|
14580
15260
|
const ctx = surface.marks.ctx;
|
|
14581
15261
|
const pen = model.sketch ? new RoughPen(ctx, model.sketch) : null;
|
|
14582
15262
|
for (const bar of model.bars) {
|
|
14583
15263
|
const { rect } = bar;
|
|
14584
15264
|
if (rect.width <= 0 || rect.height <= 0) continue;
|
|
15265
|
+
ctx.globalAlpha = alphaOf(bar);
|
|
14585
15266
|
if (pen) {
|
|
14586
15267
|
pen.rect(rect.x, rect.y, rect.width, rect.height, { stroke: bar.color, fill: bar.color });
|
|
15268
|
+
ctx.globalAlpha = 1;
|
|
14587
15269
|
continue;
|
|
14588
15270
|
}
|
|
14589
15271
|
const r = Math.min(model.cornerRadius, rect.width / 2, rect.height / 2);
|
|
@@ -14591,6 +15273,7 @@ function drawBars2(surface, model) {
|
|
|
14591
15273
|
roundedRect(ctx, rect.x, rect.y, rect.width, rect.height, [r, r, r, r]);
|
|
14592
15274
|
ctx.fillStyle = bar.color;
|
|
14593
15275
|
ctx.fill();
|
|
15276
|
+
ctx.globalAlpha = 1;
|
|
14594
15277
|
}
|
|
14595
15278
|
}
|
|
14596
15279
|
function barLabelText(bar, format) {
|
|
@@ -14620,17 +15303,19 @@ function drawLabels(surface, model) {
|
|
|
14620
15303
|
});
|
|
14621
15304
|
}
|
|
14622
15305
|
}
|
|
14623
|
-
function drawWaterfall(surface, spec, tokens, size,
|
|
15306
|
+
function drawWaterfall(surface, spec, tokens, size, context) {
|
|
14624
15307
|
const waterfall = spec;
|
|
14625
15308
|
const model = buildWaterfallModel(waterfall, tokens, size);
|
|
15309
|
+
const emphasis = context?.emphasis ?? null;
|
|
15310
|
+
const stageField = waterfall.encoding.stage.field;
|
|
15311
|
+
const alphaOf = (bar) => bar.stageValue === void 0 ? 1 : rowAlpha(emphasis, { [stageField]: bar.stageValue });
|
|
14626
15312
|
drawAxesUnderlay(surface, model.base);
|
|
14627
|
-
drawConnectors(surface, model);
|
|
14628
|
-
drawBars2(surface, model);
|
|
15313
|
+
drawConnectors(surface, model, alphaOf);
|
|
15314
|
+
drawBars2(surface, model, alphaOf);
|
|
14629
15315
|
drawLabels(surface, model);
|
|
14630
15316
|
drawOverlay(surface, model.base);
|
|
14631
15317
|
if (model.bars.length === 0) return;
|
|
14632
15318
|
const { plot, format } = model;
|
|
14633
|
-
const stageField = waterfall.encoding.stage.field;
|
|
14634
15319
|
const valueLabel2 = waterfall.encoding.value.title ?? waterfall.encoding.value.field;
|
|
14635
15320
|
const fmtNum = (v) => format ? formatValue(v, format) : formatNumber2(v, ",");
|
|
14636
15321
|
const hitBar = (px, py) => {
|
|
@@ -14813,7 +15498,7 @@ var LABEL_WIDTH = 68;
|
|
|
14813
15498
|
function fmtValue(v, format) {
|
|
14814
15499
|
return format ? formatValue(v, format) : formatNumber2(v, ",");
|
|
14815
15500
|
}
|
|
14816
|
-
function drawLines(surface, model) {
|
|
15501
|
+
function drawLines(surface, model, alphaOf) {
|
|
14817
15502
|
const ctx = surface.marks.ctx;
|
|
14818
15503
|
const pen = model.sketch ? new RoughPen(ctx, model.sketch) : null;
|
|
14819
15504
|
ctx.save();
|
|
@@ -14823,6 +15508,7 @@ function drawLines(surface, model) {
|
|
|
14823
15508
|
for (const series of model.series) {
|
|
14824
15509
|
const points = series.points;
|
|
14825
15510
|
if (points.length === 0) continue;
|
|
15511
|
+
ctx.globalAlpha = alphaOf(series);
|
|
14826
15512
|
if (pen && points.length >= 2) {
|
|
14827
15513
|
pen.polyline(points, { stroke: series.color });
|
|
14828
15514
|
} else if (points.length >= 2) {
|
|
@@ -14848,16 +15534,18 @@ function drawLines(surface, model) {
|
|
|
14848
15534
|
ctx.stroke();
|
|
14849
15535
|
}
|
|
14850
15536
|
}
|
|
15537
|
+
ctx.globalAlpha = 1;
|
|
14851
15538
|
}
|
|
14852
15539
|
ctx.restore();
|
|
14853
15540
|
}
|
|
14854
|
-
function drawEndLabels(surface, model) {
|
|
15541
|
+
function drawEndLabels(surface, model, alphaOf) {
|
|
14855
15542
|
if (!model.showLabels) return;
|
|
14856
15543
|
const { tokens } = model;
|
|
14857
15544
|
const labelSize = tokens.font.size.tiny;
|
|
14858
15545
|
const valueSize = tokens.font.size.tiny;
|
|
14859
15546
|
for (const series of model.series) {
|
|
14860
15547
|
if (series.points.length === 0) continue;
|
|
15548
|
+
const opacity = alphaOf(series) === 1 ? 1 : 0.5;
|
|
14861
15549
|
const first = series.points[0];
|
|
14862
15550
|
const last = series.points[series.points.length - 1];
|
|
14863
15551
|
addOverlayText(surface, tokens, {
|
|
@@ -14868,7 +15556,8 @@ function drawEndLabels(surface, model) {
|
|
|
14868
15556
|
color: series.color,
|
|
14869
15557
|
size: labelSize,
|
|
14870
15558
|
weight: tokens.font.weight.bold,
|
|
14871
|
-
align: "right"
|
|
15559
|
+
align: "right",
|
|
15560
|
+
opacity
|
|
14872
15561
|
});
|
|
14873
15562
|
addOverlayText(surface, tokens, {
|
|
14874
15563
|
left: first.x - 8 - LABEL_WIDTH,
|
|
@@ -14877,7 +15566,8 @@ function drawEndLabels(surface, model) {
|
|
|
14877
15566
|
text: fmtValue(first.value, model.format),
|
|
14878
15567
|
color: tokens.color.textMuted,
|
|
14879
15568
|
size: valueSize,
|
|
14880
|
-
align: "right"
|
|
15569
|
+
align: "right",
|
|
15570
|
+
opacity
|
|
14881
15571
|
});
|
|
14882
15572
|
if (last !== first) {
|
|
14883
15573
|
addOverlayText(surface, tokens, {
|
|
@@ -14888,7 +15578,8 @@ function drawEndLabels(surface, model) {
|
|
|
14888
15578
|
color: series.color,
|
|
14889
15579
|
size: valueSize,
|
|
14890
15580
|
weight: tokens.font.weight.medium,
|
|
14891
|
-
align: "left"
|
|
15581
|
+
align: "left",
|
|
15582
|
+
opacity
|
|
14892
15583
|
});
|
|
14893
15584
|
}
|
|
14894
15585
|
}
|
|
@@ -14906,16 +15597,18 @@ function nearestPoint(model, px, py) {
|
|
|
14906
15597
|
}
|
|
14907
15598
|
return best ? { series: best.series, point: best.point } : null;
|
|
14908
15599
|
}
|
|
14909
|
-
function drawSlope(surface, spec, tokens, size,
|
|
15600
|
+
function drawSlope(surface, spec, tokens, size, context) {
|
|
14910
15601
|
const slope = spec;
|
|
14911
15602
|
const model = buildSlopeModel(slope, tokens, size);
|
|
15603
|
+
const emphasis = context?.emphasis ?? null;
|
|
15604
|
+
const seriesField = slope.encoding.series.field;
|
|
15605
|
+
const alphaOf = (series) => emphasis ? emphasis.match({ [seriesField]: series.value }) ? 1 : emphasis.dim : 1;
|
|
14912
15606
|
drawAxesUnderlay(surface, model.base);
|
|
14913
|
-
drawLines(surface, model);
|
|
15607
|
+
drawLines(surface, model, alphaOf);
|
|
14914
15608
|
drawOverlay(surface, model.base);
|
|
14915
|
-
drawEndLabels(surface, model);
|
|
15609
|
+
drawEndLabels(surface, model, alphaOf);
|
|
14916
15610
|
if (model.series.length === 0) return;
|
|
14917
15611
|
const { plot } = model;
|
|
14918
|
-
const seriesField = slope.encoding.series.field;
|
|
14919
15612
|
return {
|
|
14920
15613
|
region: { x: plot.x, y: plot.y, width: plot.width, height: plot.height },
|
|
14921
15614
|
hitTest: (px, py) => {
|
|
@@ -15108,10 +15801,12 @@ function layoutPlot(content, labelWidth, tokens) {
|
|
|
15108
15801
|
}
|
|
15109
15802
|
};
|
|
15110
15803
|
}
|
|
15111
|
-
function drawDumbbell(surface, spec, tokens, size,
|
|
15804
|
+
function drawDumbbell(surface, spec, tokens, size, context) {
|
|
15112
15805
|
const dumbbell = spec;
|
|
15113
15806
|
const model = buildDumbbellModel(dumbbell, tokens);
|
|
15114
15807
|
const ctx = surface.marks.ctx;
|
|
15808
|
+
const emphasis = context?.emphasis ?? null;
|
|
15809
|
+
const categoryField = dumbbell.encoding.category.field;
|
|
15115
15810
|
let content = drawTitleBlock(surface, tokens, size, dumbbell.title);
|
|
15116
15811
|
const legendEntries = model.groups.map((group) => ({ label: group.label, color: group.color, symbol: "circle" }));
|
|
15117
15812
|
content = drawCategoricalLegend(surface, tokens, content, legendEntries, "top");
|
|
@@ -15172,8 +15867,10 @@ function drawDumbbell(surface, spec, tokens, size, _context) {
|
|
|
15172
15867
|
}).sort((a, b) => a.x - b.x);
|
|
15173
15868
|
if (pixelDots.length === 0) continue;
|
|
15174
15869
|
pixelRows.push({ ...row, y: cy, pixelDots });
|
|
15870
|
+
ctx.globalAlpha = rowAlpha(emphasis, { [categoryField]: row.catRawValue });
|
|
15175
15871
|
drawConnector(ctx, pen, pixelDots[0].x, pixelDots[pixelDots.length - 1].x, cy, tokens);
|
|
15176
15872
|
for (const dot of pixelDots) drawDot(ctx, pen, dot.x, cy, dot.color, tokens);
|
|
15873
|
+
ctx.globalAlpha = 1;
|
|
15177
15874
|
addOverlayText(surface, tokens, {
|
|
15178
15875
|
left: content.x,
|
|
15179
15876
|
top: cy,
|
|
@@ -15201,7 +15898,6 @@ function drawDumbbell(surface, spec, tokens, size, _context) {
|
|
|
15201
15898
|
}
|
|
15202
15899
|
}
|
|
15203
15900
|
}
|
|
15204
|
-
const categoryField = dumbbell.encoding.category.field;
|
|
15205
15901
|
const nearestDot = (px, py) => {
|
|
15206
15902
|
let best = null;
|
|
15207
15903
|
for (const row of pixelRows) {
|
|
@@ -16537,6 +17233,8 @@ var CANVAS_MARK_TYPES = /* @__PURE__ */ new Set([
|
|
|
16537
17233
|
"sankey",
|
|
16538
17234
|
"choropleth"
|
|
16539
17235
|
]);
|
|
17236
|
+
var COUNT_UP_TYPES = /* @__PURE__ */ new Set(["kpi"]);
|
|
17237
|
+
var SWEEP_TYPES = /* @__PURE__ */ new Set(["pie", "gauge"]);
|
|
16540
17238
|
var EMPTY_DATA = [];
|
|
16541
17239
|
function legendConfigFor(model) {
|
|
16542
17240
|
const legend = model.spec.legend;
|
|
@@ -16632,6 +17330,9 @@ function render(target, spec, options) {
|
|
|
16632
17330
|
let interaction;
|
|
16633
17331
|
let entrance;
|
|
16634
17332
|
let updateTransition;
|
|
17333
|
+
let lastValue = null;
|
|
17334
|
+
let lastCompare = null;
|
|
17335
|
+
let lastSparkline = null;
|
|
16635
17336
|
let destroyed = false;
|
|
16636
17337
|
let lastDrawnW = -1;
|
|
16637
17338
|
let lastDrawnH = -1;
|
|
@@ -16659,7 +17360,7 @@ function render(target, spec, options) {
|
|
|
16659
17360
|
}
|
|
16660
17361
|
}
|
|
16661
17362
|
const hostListeners = /* @__PURE__ */ new Set();
|
|
16662
|
-
const draw = (animateEntrance) => {
|
|
17363
|
+
const draw = (animateEntrance, valueTweenFrom, compareTweenFrom, sparklineTweenFrom) => {
|
|
16663
17364
|
entrance?.cancel();
|
|
16664
17365
|
entrance = void 0;
|
|
16665
17366
|
updateTransition?.cancel();
|
|
@@ -16689,6 +17390,11 @@ function render(target, spec, options) {
|
|
|
16689
17390
|
let facetRpt;
|
|
16690
17391
|
let legendSelect = null;
|
|
16691
17392
|
const type = currentSpec.type;
|
|
17393
|
+
if (COUNT_UP_TYPES.has(type) || SWEEP_TYPES.has(type)) {
|
|
17394
|
+
lastValue = resolveValueRef(effectiveSpec.value, effectiveSpec.data ?? EMPTY_DATA);
|
|
17395
|
+
lastCompare = SWEEP_TYPES.has(type) ? resolveValueRef(effectiveSpec.compare, effectiveSpec.data ?? EMPTY_DATA) : null;
|
|
17396
|
+
lastSparkline = type === "kpi" ? kpiSparklineSeries(effectiveSpec, effectiveSpec.data ?? EMPTY_DATA) : null;
|
|
17397
|
+
}
|
|
16692
17398
|
const facetLayout = isFaceted(effectiveSpec) ? buildFacetModels(effectiveSpec, tokens, size) : null;
|
|
16693
17399
|
if (facetLayout) {
|
|
16694
17400
|
drawFacet(surface, effectiveSpec, facetLayout, tokens);
|
|
@@ -16719,7 +17425,8 @@ function render(target, spec, options) {
|
|
|
16719
17425
|
tokens,
|
|
16720
17426
|
currentSpec.background,
|
|
16721
17427
|
anim,
|
|
16722
|
-
finishEntrance
|
|
17428
|
+
finishEntrance,
|
|
17429
|
+
type === "bar" ? "grow" : "wipe"
|
|
16723
17430
|
);
|
|
16724
17431
|
}
|
|
16725
17432
|
} else {
|
|
@@ -16737,11 +17444,44 @@ function render(target, spec, options) {
|
|
|
16737
17444
|
}
|
|
16738
17445
|
};
|
|
16739
17446
|
interactionModel = renderer(surface, effectiveSpec, tokens, size, ctx) ?? null;
|
|
17447
|
+
if (anim.enabled) {
|
|
17448
|
+
if (COUNT_UP_TYPES.has(type) || SWEEP_TYPES.has(type)) {
|
|
17449
|
+
const key = COUNT_UP_TYPES.has(type) ? "valueProgress" : "sweepProgress";
|
|
17450
|
+
entrance = runCustomProgress(anim, finishEntrance, (t) => {
|
|
17451
|
+
surface.overlay.replaceChildren();
|
|
17452
|
+
surface.marks.clear();
|
|
17453
|
+
paintBackground(surface, tokens, currentSpec.background);
|
|
17454
|
+
renderer(surface, effectiveSpec, tokens, size, { ...ctx, [key]: t });
|
|
17455
|
+
});
|
|
17456
|
+
} else {
|
|
17457
|
+
entrance = runFadeEntrance(surface, anim, finishEntrance);
|
|
17458
|
+
}
|
|
17459
|
+
} else if ((valueTweenFrom !== void 0 || sparklineTweenFrom !== void 0) && (COUNT_UP_TYPES.has(type) || SWEEP_TYPES.has(type))) {
|
|
17460
|
+
const tween = resolveUpdate(currentSpec.animation, {
|
|
17461
|
+
reducedMotion: prefersReducedMotion(),
|
|
17462
|
+
disabled: globalThis.__GRAPHEIN_DISABLE_ANIM === true
|
|
17463
|
+
});
|
|
17464
|
+
if (tween.enabled) {
|
|
17465
|
+
const key = COUNT_UP_TYPES.has(type) ? "valueProgress" : "sweepProgress";
|
|
17466
|
+
entrance = runCustomProgress(tween, finishEntrance, (t) => {
|
|
17467
|
+
surface.overlay.replaceChildren();
|
|
17468
|
+
surface.marks.clear();
|
|
17469
|
+
paintBackground(surface, tokens, currentSpec.background);
|
|
17470
|
+
renderer(surface, effectiveSpec, tokens, size, {
|
|
17471
|
+
...ctx,
|
|
17472
|
+
valueFrom: valueTweenFrom,
|
|
17473
|
+
compareFrom: compareTweenFrom,
|
|
17474
|
+
sparklineFrom: sparklineTweenFrom,
|
|
17475
|
+
[key]: t
|
|
17476
|
+
});
|
|
17477
|
+
});
|
|
17478
|
+
}
|
|
17479
|
+
}
|
|
16740
17480
|
} else {
|
|
16741
17481
|
drawPlaceholder(surface, tokens, `\u201C${type}\u201D renderer not yet registered`);
|
|
16742
|
-
|
|
16743
|
-
|
|
16744
|
-
|
|
17482
|
+
if (anim.enabled) {
|
|
17483
|
+
entrance = runFadeEntrance(surface, anim, finishEntrance);
|
|
17484
|
+
}
|
|
16745
17485
|
}
|
|
16746
17486
|
}
|
|
16747
17487
|
if (!interaction) interaction = new InteractionController(surface, tokens);
|
|
@@ -16865,8 +17605,18 @@ function render(target, spec, options) {
|
|
|
16865
17605
|
});
|
|
16866
17606
|
const wantFade = anim.enabled && CANVAS_MARK_TYPES.has(prevType) && CANVAS_MARK_TYPES.has(next.type);
|
|
16867
17607
|
const before = wantFade ? snapshotLayer(surface.marks) : void 0;
|
|
17608
|
+
const prevValue = lastValue;
|
|
17609
|
+
const prevCompare = lastCompare;
|
|
17610
|
+
const prevSparkline = lastSparkline;
|
|
17611
|
+
const wantValueTween = prevType === next.type && (COUNT_UP_TYPES.has(next.type) || SWEEP_TYPES.has(next.type)) && prevValue != null;
|
|
17612
|
+
const wantSparklineTween = prevType === next.type && next.type === "kpi" && prevSparkline != null && prevSparkline.length > 0;
|
|
16868
17613
|
currentSpec = next;
|
|
16869
|
-
draw(
|
|
17614
|
+
draw(
|
|
17615
|
+
false,
|
|
17616
|
+
wantValueTween ? prevValue ?? void 0 : void 0,
|
|
17617
|
+
wantValueTween ? prevCompare ?? void 0 : void 0,
|
|
17618
|
+
wantValueTween || wantSparklineTween ? prevSparkline ?? void 0 : void 0
|
|
17619
|
+
);
|
|
16870
17620
|
if (before && surface.width === prevW && surface.height === prevH && surface.marks.canvas.width === before.width && surface.marks.canvas.height === before.height) {
|
|
16871
17621
|
updateTransition = runUpdateCrossfade(surface, before, anim);
|
|
16872
17622
|
}
|
|
@@ -16886,7 +17636,7 @@ function render(target, spec, options) {
|
|
|
16886
17636
|
}
|
|
16887
17637
|
};
|
|
16888
17638
|
}
|
|
16889
|
-
function runCartesianEntrance(surface, model, renderer, tokens, background, anim, onDone) {
|
|
17639
|
+
function runCartesianEntrance(surface, model, renderer, tokens, background, anim, onDone, mode = "wipe") {
|
|
16890
17640
|
const ctx = surface.marks.ctx;
|
|
16891
17641
|
const { plot } = model;
|
|
16892
17642
|
const bg = background ?? tokens.color.background;
|
|
@@ -16897,6 +17647,14 @@ function runCartesianEntrance(surface, model, renderer, tokens, background, anim
|
|
|
16897
17647
|
ctx.fillRect(0, 0, surface.width, surface.height);
|
|
16898
17648
|
ctx.restore();
|
|
16899
17649
|
drawAxesUnderlay(surface, model);
|
|
17650
|
+
if (mode === "grow") {
|
|
17651
|
+
model.entranceProgress = t;
|
|
17652
|
+
renderer(surface, model);
|
|
17653
|
+
model.entranceProgress = void 0;
|
|
17654
|
+
drawTrendlines(surface, model);
|
|
17655
|
+
drawAnnotations(surface, model);
|
|
17656
|
+
return;
|
|
17657
|
+
}
|
|
16900
17658
|
if (t <= 0) return;
|
|
16901
17659
|
ctx.save();
|
|
16902
17660
|
ctx.globalAlpha = Math.min(1, 0.2 + 0.8 * t);
|
|
@@ -16919,6 +17677,17 @@ function runCartesianEntrance(surface, model, renderer, tokens, background, anim
|
|
|
16919
17677
|
}
|
|
16920
17678
|
});
|
|
16921
17679
|
}
|
|
17680
|
+
function runCustomProgress(anim, onDone, paint) {
|
|
17681
|
+
return animate({
|
|
17682
|
+
duration: anim.duration,
|
|
17683
|
+
easing: anim.easing,
|
|
17684
|
+
onUpdate: paint,
|
|
17685
|
+
onComplete: () => {
|
|
17686
|
+
paint(1);
|
|
17687
|
+
onDone();
|
|
17688
|
+
}
|
|
17689
|
+
});
|
|
17690
|
+
}
|
|
16922
17691
|
function runFadeEntrance(surface, anim, onDone) {
|
|
16923
17692
|
const root = surface.root;
|
|
16924
17693
|
const apply = (t) => {
|
|
@@ -17070,12 +17839,17 @@ function autoParamName(view) {
|
|
|
17070
17839
|
const existing = view.spec.params?.[0]?.name;
|
|
17071
17840
|
return existing ?? `__sel__${view.id}`;
|
|
17072
17841
|
}
|
|
17842
|
+
function roleOf(view) {
|
|
17843
|
+
return view.interact ?? "auto";
|
|
17844
|
+
}
|
|
17073
17845
|
function wireViews(views, interactions) {
|
|
17074
17846
|
const out = views.map((v) => ({ ...v, spec: v.spec }));
|
|
17075
17847
|
if (interactions === "none") return out;
|
|
17076
17848
|
const sources = [];
|
|
17077
17849
|
out.forEach((v, i) => {
|
|
17078
17850
|
const type = v.spec.type;
|
|
17851
|
+
const role = roleOf(v);
|
|
17852
|
+
if (role === "none" || role === "target") return;
|
|
17079
17853
|
if (isSlicerType(type)) {
|
|
17080
17854
|
const s = v.spec;
|
|
17081
17855
|
sources.push({
|
|
@@ -17106,11 +17880,15 @@ function wireViews(views, interactions) {
|
|
|
17106
17880
|
name: src.param,
|
|
17107
17881
|
select: { type: "point", on: "click", fields: src.fields }
|
|
17108
17882
|
});
|
|
17109
|
-
|
|
17883
|
+
if (roleOf(out[src.view]) !== "source") {
|
|
17884
|
+
out[src.view].spec = addHighlight(out[src.view].spec, src.param);
|
|
17885
|
+
}
|
|
17110
17886
|
}
|
|
17111
17887
|
out.forEach((v, j) => {
|
|
17112
17888
|
if (isSlicerType(v.spec.type)) return;
|
|
17113
17889
|
if (j === src.view) return;
|
|
17890
|
+
const trole = roleOf(v);
|
|
17891
|
+
if (trole === "none" || trole === "source") return;
|
|
17114
17892
|
out[j].spec = addFilterParam(out[j].spec, src.param);
|
|
17115
17893
|
});
|
|
17116
17894
|
}
|
|
@@ -17160,6 +17938,41 @@ var DEFAULT_BREAKPOINTS = [
|
|
|
17160
17938
|
{ maxWidth: 960, cols: 6 }
|
|
17161
17939
|
];
|
|
17162
17940
|
var STRIP_SLICERS = /* @__PURE__ */ new Set(["dropdown", "search", "dateRange", "range"]);
|
|
17941
|
+
var DASHBOARD_CLASS = "graphein-dashboard";
|
|
17942
|
+
var DASHBOARD_CELL_CLASS = "graphein-dashboard-cell";
|
|
17943
|
+
var DASHBOARD_SOURCE_CLASS = "is-source";
|
|
17944
|
+
function dashboardStyles(tokens) {
|
|
17945
|
+
const accent = tokens.color.accent;
|
|
17946
|
+
const surface = tokens.color.surface;
|
|
17947
|
+
return `
|
|
17948
|
+
.${DASHBOARD_CLASS} .${DASHBOARD_CELL_CLASS}[data-framed="true"] {
|
|
17949
|
+
transition: border-color 120ms ease, background-color 120ms ease;
|
|
17950
|
+
}
|
|
17951
|
+
.${DASHBOARD_CLASS} .${DASHBOARD_CELL_CLASS}[data-framed="true"]:hover {
|
|
17952
|
+
border-color: ${accent};
|
|
17953
|
+
background-color: ${surface};
|
|
17954
|
+
}
|
|
17955
|
+
.${DASHBOARD_CLASS} .${DASHBOARD_CELL_CLASS}:focus-visible {
|
|
17956
|
+
outline: 2px solid ${accent};
|
|
17957
|
+
outline-offset: 2px;
|
|
17958
|
+
}
|
|
17959
|
+
.${DASHBOARD_CLASS} .${DASHBOARD_CELL_CLASS}.${DASHBOARD_SOURCE_CLASS} {
|
|
17960
|
+
border-color: ${accent};
|
|
17961
|
+
box-shadow: inset 0 0 0 1px ${accent};
|
|
17962
|
+
}
|
|
17963
|
+
`;
|
|
17964
|
+
}
|
|
17965
|
+
function selectionIsActive(value) {
|
|
17966
|
+
if (value == null) return false;
|
|
17967
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
17968
|
+
if (typeof value !== "object") return true;
|
|
17969
|
+
const v = value;
|
|
17970
|
+
if (v.kind === "point") return Array.isArray(v.tuples) && v.tuples.length > 0;
|
|
17971
|
+
if (v.kind === "set") return Array.isArray(v.values) && v.values.length > 0;
|
|
17972
|
+
if (v.kind === "range") return v.min != null || v.max != null;
|
|
17973
|
+
if (v.kind === "text") return typeof v.query === "string" && v.query.length > 0;
|
|
17974
|
+
return true;
|
|
17975
|
+
}
|
|
17163
17976
|
function stripCellHeight(view, rowHeight) {
|
|
17164
17977
|
const titled = !!(view.title || view.subtitle);
|
|
17165
17978
|
const titlePad = titled ? 30 : 0;
|
|
@@ -17302,6 +18115,12 @@ function renderDashboard(target, spec, options) {
|
|
|
17302
18115
|
let root;
|
|
17303
18116
|
let resizeObs;
|
|
17304
18117
|
let destroyed = false;
|
|
18118
|
+
let sourceCells = /* @__PURE__ */ new Map();
|
|
18119
|
+
const syncSourceCell = (name, value) => {
|
|
18120
|
+
const cell = sourceCells.get(name);
|
|
18121
|
+
if (!cell) return;
|
|
18122
|
+
cell.classList.toggle(DASHBOARD_SOURCE_CLASS, selectionIsActive(value));
|
|
18123
|
+
};
|
|
17305
18124
|
const build = () => {
|
|
17306
18125
|
const tokens = resolveTheme(currentSpec.theme);
|
|
17307
18126
|
const layout2 = resolveDashboardLayout(currentSpec.layout);
|
|
@@ -17314,8 +18133,9 @@ function renderDashboard(target, spec, options) {
|
|
|
17314
18133
|
store.set(param.name, param.value ?? null);
|
|
17315
18134
|
}
|
|
17316
18135
|
}
|
|
18136
|
+
sourceCells = /* @__PURE__ */ new Map();
|
|
17317
18137
|
root = document.createElement("div");
|
|
17318
|
-
root.className =
|
|
18138
|
+
root.className = DASHBOARD_CLASS;
|
|
17319
18139
|
Object.assign(root.style, {
|
|
17320
18140
|
boxSizing: "border-box",
|
|
17321
18141
|
width: currentSpec.dimensions?.width ? `${currentSpec.dimensions.width}px` : "100%",
|
|
@@ -17329,6 +18149,10 @@ function renderDashboard(target, spec, options) {
|
|
|
17329
18149
|
flexDirection: "column",
|
|
17330
18150
|
gap: `${layout2.gap}px`
|
|
17331
18151
|
});
|
|
18152
|
+
const style = document.createElement("style");
|
|
18153
|
+
style.setAttribute("data-graphein-dashboard-style", "");
|
|
18154
|
+
style.textContent = dashboardStyles(tokens);
|
|
18155
|
+
root.appendChild(style);
|
|
17332
18156
|
const titleText3 = typeof currentSpec.title === "string" ? currentSpec.title : currentSpec.title?.text;
|
|
17333
18157
|
if (titleText3 || currentSpec.subtitle) {
|
|
17334
18158
|
const header = document.createElement("div");
|
|
@@ -17388,6 +18212,7 @@ function renderDashboard(target, spec, options) {
|
|
|
17388
18212
|
};
|
|
17389
18213
|
const cellChrome = (cell, view) => {
|
|
17390
18214
|
const framed = view?.frame ?? true;
|
|
18215
|
+
cell.dataset.framed = framed ? "true" : "false";
|
|
17391
18216
|
Object.assign(cell.style, {
|
|
17392
18217
|
minWidth: "0",
|
|
17393
18218
|
minHeight: "0",
|
|
@@ -17399,9 +18224,10 @@ function renderDashboard(target, spec, options) {
|
|
|
17399
18224
|
background: view?.background ?? (framed ? tokens.color.surface : "transparent")
|
|
17400
18225
|
});
|
|
17401
18226
|
};
|
|
17402
|
-
const
|
|
18227
|
+
const createCard = (view) => {
|
|
17403
18228
|
const cell = document.createElement("div");
|
|
17404
18229
|
cellChrome(cell, view);
|
|
18230
|
+
cell.classList.add(DASHBOARD_CELL_CLASS);
|
|
17405
18231
|
cell.setAttribute("data-view-id", view.id);
|
|
17406
18232
|
const hasCardChrome = !!(view.title || view.subtitle || view.accent || view.background || view.frame === false || view.padding === "none");
|
|
17407
18233
|
if (!hasCardChrome) return { cell, host: cell };
|
|
@@ -17428,13 +18254,14 @@ function renderDashboard(target, spec, options) {
|
|
|
17428
18254
|
padding: `${tokens.spacing.sm}px ${tokens.spacing.md}px ${tokens.spacing.xs}px ${tokens.spacing.md}px`,
|
|
17429
18255
|
display: "flex",
|
|
17430
18256
|
flexDirection: "column",
|
|
17431
|
-
gap: "2px"
|
|
18257
|
+
gap: "2px",
|
|
18258
|
+
borderBottom: `1px solid ${tokens.color.border}`
|
|
17432
18259
|
});
|
|
17433
18260
|
if (view.title) {
|
|
17434
18261
|
const title2 = document.createElement("div");
|
|
17435
18262
|
title2.textContent = view.title;
|
|
17436
18263
|
Object.assign(title2.style, {
|
|
17437
|
-
font: `${tokens.font.weight.
|
|
18264
|
+
font: `${tokens.font.weight.medium} ${tokens.font.size.base}px ${tokens.font.family}`,
|
|
17438
18265
|
color: tokens.color.text,
|
|
17439
18266
|
letterSpacing: "-0.01em"
|
|
17440
18267
|
});
|
|
@@ -17462,6 +18289,24 @@ function renderDashboard(target, spec, options) {
|
|
|
17462
18289
|
cell.appendChild(host);
|
|
17463
18290
|
return { cell, host };
|
|
17464
18291
|
};
|
|
18292
|
+
const publishingParams = (view) => {
|
|
18293
|
+
const names = /* @__PURE__ */ new Set();
|
|
18294
|
+
for (const param of view.spec.params ?? []) {
|
|
18295
|
+
if (param?.name) names.add(param.name);
|
|
18296
|
+
}
|
|
18297
|
+
if (isSlicerType(view.spec.type)) {
|
|
18298
|
+
const slicer = view.spec;
|
|
18299
|
+
const name = typeof slicer.param === "string" ? slicer.param : typeof slicer.field === "string" ? slicer.field : void 0;
|
|
18300
|
+
if (name) names.add(name);
|
|
18301
|
+
}
|
|
18302
|
+
return [...names];
|
|
18303
|
+
};
|
|
18304
|
+
const registerSourceCell = (view, cell) => {
|
|
18305
|
+
for (const name of publishingParams(view)) {
|
|
18306
|
+
sourceCells.set(name, cell);
|
|
18307
|
+
syncSourceCell(name, store.get(name));
|
|
18308
|
+
}
|
|
18309
|
+
};
|
|
17465
18310
|
const sectionPlans = resolveDashboardSections(gridViews, layout2);
|
|
17466
18311
|
const layoutGroups = [];
|
|
17467
18312
|
const deferredMounts = [];
|
|
@@ -17474,7 +18319,8 @@ function renderDashboard(target, spec, options) {
|
|
|
17474
18319
|
gap: `${layout2.gap}px`
|
|
17475
18320
|
});
|
|
17476
18321
|
for (const view of stripViews) {
|
|
17477
|
-
const { cell, host } =
|
|
18322
|
+
const { cell, host } = createCard(view);
|
|
18323
|
+
registerSourceCell(view, cell);
|
|
17478
18324
|
Object.assign(cell.style, {
|
|
17479
18325
|
flex: "1 1 220px",
|
|
17480
18326
|
minWidth: "200px",
|
|
@@ -17520,7 +18366,13 @@ function renderDashboard(target, spec, options) {
|
|
|
17520
18366
|
if (plan.section?.title) {
|
|
17521
18367
|
const title2 = document.createElement("span");
|
|
17522
18368
|
title2.textContent = plan.section.title;
|
|
17523
|
-
Object.assign(title2.style, {
|
|
18369
|
+
Object.assign(title2.style, {
|
|
18370
|
+
color: tokens.color.textMuted,
|
|
18371
|
+
fontWeight: String(tokens.font.weight.medium),
|
|
18372
|
+
fontSize: `${tokens.font.size.small}px`,
|
|
18373
|
+
letterSpacing: "0.06em",
|
|
18374
|
+
textTransform: "uppercase"
|
|
18375
|
+
});
|
|
17524
18376
|
titles.appendChild(title2);
|
|
17525
18377
|
}
|
|
17526
18378
|
if (plan.section?.subtitle) {
|
|
@@ -17572,7 +18424,8 @@ function renderDashboard(target, spec, options) {
|
|
|
17572
18424
|
parent.appendChild(grid);
|
|
17573
18425
|
const cells = placeViews(plan.views, plan.cols, layout2.preset ?? "auto");
|
|
17574
18426
|
for (const cellInfo of cells) {
|
|
17575
|
-
const { cell, host } =
|
|
18427
|
+
const { cell, host } = createCard(cellInfo.view);
|
|
18428
|
+
registerSourceCell(cellInfo.view, cell);
|
|
17576
18429
|
grid.appendChild(cell);
|
|
17577
18430
|
cellInfo.el = cell;
|
|
17578
18431
|
deferredMounts.push({ el: host, view: cellInfo.view, mounted: false });
|
|
@@ -17645,8 +18498,10 @@ function renderDashboard(target, spec, options) {
|
|
|
17645
18498
|
instances.length = 0;
|
|
17646
18499
|
if (root && root.parentNode) root.parentNode.removeChild(root);
|
|
17647
18500
|
root = void 0;
|
|
18501
|
+
sourceCells = /* @__PURE__ */ new Map();
|
|
17648
18502
|
};
|
|
17649
18503
|
const unsubscribe = store.subscribe((name, value) => {
|
|
18504
|
+
syncSourceCell(name, value);
|
|
17650
18505
|
for (const listener of [...hostListeners]) listener(name, value);
|
|
17651
18506
|
});
|
|
17652
18507
|
build();
|
|
@@ -17864,6 +18719,6 @@ function renderToContext(target, spec) {
|
|
|
17864
18719
|
// src/index.ts
|
|
17865
18720
|
var VERSION = "0.1.0";
|
|
17866
18721
|
|
|
17867
|
-
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, categoricalSchemes, 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, ellipsize, 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, profileColumns, quadIn, quadInOut, quadOut, rampFromStops, readableTextColor, recommendChart, relativeLuminance, render, renderDashboard, renderToContext, repairReport, 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 };
|
|
18722
|
+
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, categoricalSchemes, 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, ellipsize, 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, profileColumns, quadIn, quadInOut, quadOut, rampFromStops, readableTextColor, recommendChart, relativeLuminance, render, renderDashboard, renderToContext, repairReport, repairSpec, resolveCurve, resolveDashboardLayout, resolveDashboardSections, resolveEmphasis, resolveEntrance, resolveFilterValues, resolveInsightOptions, resolveSketch, resolveTheme, resolveUpdate, resolveValueRef, 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 };
|
|
17868
18723
|
//# sourceMappingURL=index.js.map
|
|
17869
18724
|
//# sourceMappingURL=index.js.map
|