graphein 0.12.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 +1039 -384
- 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
|
}
|
|
@@ -10160,7 +10579,7 @@ function buildTable(opts) {
|
|
|
10160
10579
|
pointerEvents: "auto",
|
|
10161
10580
|
boxSizing: "border-box",
|
|
10162
10581
|
border: `1px solid ${opts.sketch ? "transparent" : tokens.color.border}`,
|
|
10163
|
-
borderRadius: opts.sketch ?
|
|
10582
|
+
borderRadius: opts.sketch ? SKETCH_BORDER_RADIUS : `${tokens.radius.sm}px`,
|
|
10164
10583
|
background: tokens.color.background
|
|
10165
10584
|
});
|
|
10166
10585
|
container.replaceChildren();
|
|
@@ -13921,11 +14340,6 @@ function drawTreemap(surface, spec, tokens, size, context) {
|
|
|
13921
14340
|
function clamp8(value, min, max) {
|
|
13922
14341
|
return Math.max(min, Math.min(max, value));
|
|
13923
14342
|
}
|
|
13924
|
-
function resolveValue2(ref, data) {
|
|
13925
|
-
if (ref == null) return null;
|
|
13926
|
-
if (typeof ref === "number") return Number.isFinite(ref) ? ref : null;
|
|
13927
|
-
return aggregateValues((data ?? []).map(accessor(ref.field)), ref.aggregate ?? "sum");
|
|
13928
|
-
}
|
|
13929
14343
|
function valueField2(ref) {
|
|
13930
14344
|
return typeof ref === "number" ? null : ref.field;
|
|
13931
14345
|
}
|
|
@@ -13949,7 +14363,7 @@ function arcPoints(cx, cy, radius, startAngle, endAngle) {
|
|
|
13949
14363
|
}
|
|
13950
14364
|
return points;
|
|
13951
14365
|
}
|
|
13952
|
-
function strokePoints(ctx, pen, points, color, width) {
|
|
14366
|
+
function strokePoints(ctx, pen, points, color, width, cap2 = "butt") {
|
|
13953
14367
|
if (points.length < 2) return;
|
|
13954
14368
|
if (pen) {
|
|
13955
14369
|
pen.polyline(points, { stroke: color, strokeWidth: width });
|
|
@@ -13960,29 +14374,12 @@ function strokePoints(ctx, pen, points, color, width) {
|
|
|
13960
14374
|
ctx.moveTo(points[0].x, points[0].y);
|
|
13961
14375
|
for (let i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y);
|
|
13962
14376
|
ctx.lineWidth = width;
|
|
13963
|
-
ctx.lineCap =
|
|
14377
|
+
ctx.lineCap = cap2;
|
|
13964
14378
|
ctx.lineJoin = "round";
|
|
13965
14379
|
ctx.strokeStyle = color;
|
|
13966
14380
|
ctx.stroke();
|
|
13967
14381
|
ctx.restore();
|
|
13968
14382
|
}
|
|
13969
|
-
function drawCircle(ctx, pen, cx, cy, radius, fill, stroke) {
|
|
13970
|
-
if (pen) {
|
|
13971
|
-
pen.circle(cx, cy, radius, { fill, stroke: stroke ?? fill, strokeWidth: 1 });
|
|
13972
|
-
return;
|
|
13973
|
-
}
|
|
13974
|
-
ctx.save();
|
|
13975
|
-
ctx.beginPath();
|
|
13976
|
-
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
|
|
13977
|
-
ctx.fillStyle = fill;
|
|
13978
|
-
ctx.fill();
|
|
13979
|
-
if (stroke) {
|
|
13980
|
-
ctx.strokeStyle = stroke;
|
|
13981
|
-
ctx.lineWidth = 1;
|
|
13982
|
-
ctx.stroke();
|
|
13983
|
-
}
|
|
13984
|
-
ctx.restore();
|
|
13985
|
-
}
|
|
13986
14383
|
function drawCenteredMessage(surface, tokens, rect, text) {
|
|
13987
14384
|
addOverlayText(surface, tokens, {
|
|
13988
14385
|
left: rect.x,
|
|
@@ -13994,86 +14391,158 @@ function drawCenteredMessage(surface, tokens, rect, text) {
|
|
|
13994
14391
|
align: "center"
|
|
13995
14392
|
});
|
|
13996
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
|
+
}
|
|
13997
14462
|
function drawGauge(surface, spec, tokens, size, context) {
|
|
13998
14463
|
const gauge = spec;
|
|
13999
14464
|
const ctx = surface.marks.ctx;
|
|
14000
14465
|
const content = drawTitleBlock(surface, tokens, size, gauge.title);
|
|
14001
14466
|
const data = gauge.data ?? [];
|
|
14002
|
-
const value =
|
|
14003
|
-
const target =
|
|
14467
|
+
const value = resolveValueRef(gauge.value, data);
|
|
14468
|
+
const target = resolveValueRef(gauge.target, data);
|
|
14469
|
+
const compare2 = resolveValueRef(gauge.compare, data);
|
|
14004
14470
|
const min = gauge.min ?? 0;
|
|
14005
14471
|
const max = gauge.max;
|
|
14006
14472
|
if (value == null || !Number.isFinite(min) || !Number.isFinite(max) || max <= min || content.width <= 0 || content.height <= 0) {
|
|
14007
14473
|
drawCenteredMessage(surface, tokens, content, "No value");
|
|
14008
14474
|
return;
|
|
14009
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;
|
|
14010
14484
|
const valueText = formatValue(value, gauge.format);
|
|
14011
14485
|
const caption = gauge.label ?? titleText(gauge.title) ?? valueField2(gauge.value) ?? "Value";
|
|
14012
|
-
const valueSize = clamp8(Math.min(content.width / Math.max(6, valueText.length * 0.55), tokens.font.size.title * 2.4), 24, 42);
|
|
14013
14486
|
const captionSize = tokens.font.size.small;
|
|
14014
|
-
const
|
|
14015
|
-
const
|
|
14016
|
-
const
|
|
14017
|
-
|
|
14018
|
-
|
|
14019
|
-
|
|
14020
|
-
|
|
14021
|
-
|
|
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;
|
|
14022
14510
|
const toAngle = (v) => Math.PI - clamp8((v - min) / (max - min), 0, 1) * Math.PI;
|
|
14023
|
-
const
|
|
14024
|
-
const
|
|
14025
|
-
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);
|
|
14026
14516
|
strokePoints(ctx, pen, arcPoints(cx, cy, radius, Math.PI, 0), track, trackWidth);
|
|
14027
|
-
if (
|
|
14028
|
-
|
|
14029
|
-
|
|
14030
|
-
|
|
14031
|
-
|
|
14032
|
-
|
|
14033
|
-
|
|
14034
|
-
|
|
14035
|
-
|
|
14036
|
-
if (
|
|
14037
|
-
strokePoints(ctx, pen, arcPoints(cx, cy,
|
|
14038
|
-
}
|
|
14039
|
-
}
|
|
14040
|
-
|
|
14041
|
-
|
|
14042
|
-
|
|
14043
|
-
|
|
14044
|
-
|
|
14045
|
-
|
|
14046
|
-
|
|
14047
|
-
|
|
14048
|
-
|
|
14049
|
-
|
|
14050
|
-
|
|
14051
|
-
const leftTick = pointOnArc(cx, cy, radius, Math.PI);
|
|
14052
|
-
const rightTick = pointOnArc(cx, cy, radius, 0);
|
|
14053
|
-
addOverlayText(surface, tokens, {
|
|
14054
|
-
left: leftTick.x - 32,
|
|
14055
|
-
top: leftTick.y + 6,
|
|
14056
|
-
width: 64,
|
|
14057
|
-
text: formatValue(min, gauge.format),
|
|
14058
|
-
color: tokens.color.textMuted,
|
|
14059
|
-
size: tokens.font.size.tiny,
|
|
14060
|
-
align: "center"
|
|
14061
|
-
});
|
|
14062
|
-
addOverlayText(surface, tokens, {
|
|
14063
|
-
left: rightTick.x - 32,
|
|
14064
|
-
top: rightTick.y + 6,
|
|
14065
|
-
width: 64,
|
|
14066
|
-
text: formatValue(max, gauge.format),
|
|
14067
|
-
color: tokens.color.textMuted,
|
|
14068
|
-
size: tokens.font.size.tiny,
|
|
14069
|
-
align: "center"
|
|
14070
|
-
});
|
|
14071
|
-
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);
|
|
14072
14541
|
addOverlayText(surface, tokens, {
|
|
14073
14542
|
left: content.x,
|
|
14074
|
-
top:
|
|
14075
|
-
width:
|
|
14076
|
-
text:
|
|
14543
|
+
top: cy - radius * 0.32 - valueSize / 2,
|
|
14544
|
+
width: dialW,
|
|
14545
|
+
text: shownText,
|
|
14077
14546
|
color: tokens.color.text,
|
|
14078
14547
|
size: valueSize,
|
|
14079
14548
|
weight: tokens.font.weight.bold,
|
|
@@ -14081,20 +14550,20 @@ function drawGauge(surface, spec, tokens, size, context) {
|
|
|
14081
14550
|
});
|
|
14082
14551
|
addOverlayText(surface, tokens, {
|
|
14083
14552
|
left: content.x,
|
|
14084
|
-
top:
|
|
14085
|
-
width:
|
|
14553
|
+
top: cy + 8,
|
|
14554
|
+
width: dialW,
|
|
14086
14555
|
text: caption,
|
|
14087
14556
|
color: tokens.color.textMuted,
|
|
14088
14557
|
size: captionSize,
|
|
14089
14558
|
align: "center"
|
|
14090
14559
|
});
|
|
14091
14560
|
const hitTest = (px, py) => {
|
|
14092
|
-
const
|
|
14093
|
-
const dy = py - cy;
|
|
14094
|
-
const distance = Math.hypot(dx, dy);
|
|
14095
|
-
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;
|
|
14096
14562
|
if (!withinDial) return null;
|
|
14097
|
-
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
|
+
}
|
|
14098
14567
|
if (target != null) rows.push({ label: "target", value: formatValue(target, gauge.format), muted: true });
|
|
14099
14568
|
return {
|
|
14100
14569
|
key: "value",
|
|
@@ -14122,7 +14591,7 @@ function titleText2(title2) {
|
|
|
14122
14591
|
function valueField3(ref) {
|
|
14123
14592
|
return typeof ref === "number" ? null : ref.field;
|
|
14124
14593
|
}
|
|
14125
|
-
function
|
|
14594
|
+
function resolveValue(ref, data) {
|
|
14126
14595
|
if (ref == null) return null;
|
|
14127
14596
|
if (typeof ref === "number") return Number.isFinite(ref) ? ref : null;
|
|
14128
14597
|
return aggregateValues((data ?? []).map(accessor(ref.field)), ref.aggregate ?? "sum");
|
|
@@ -14154,8 +14623,8 @@ function drawBullet(surface, spec, tokens, size, context) {
|
|
|
14154
14623
|
const ctx = surface.marks.ctx;
|
|
14155
14624
|
const content = drawTitleBlock(surface, tokens, size, bullet.title);
|
|
14156
14625
|
const data = bullet.data ?? [];
|
|
14157
|
-
const value =
|
|
14158
|
-
const target =
|
|
14626
|
+
const value = resolveValue(bullet.value, data);
|
|
14627
|
+
const target = resolveValue(bullet.target, data);
|
|
14159
14628
|
const caption = bullet.label ?? titleText2(bullet.title) ?? valueField3(bullet.value) ?? "Value";
|
|
14160
14629
|
const fmt = bullet.format;
|
|
14161
14630
|
const finiteRanges = (bullet.ranges ?? []).filter((r) => Number.isFinite(r)).sort((a, b) => a - b);
|
|
@@ -14338,7 +14807,7 @@ function noData(surface, tokens, content) {
|
|
|
14338
14807
|
}
|
|
14339
14808
|
function normalizeDays(days) {
|
|
14340
14809
|
if (Array.isArray(days)) {
|
|
14341
|
-
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 }));
|
|
14342
14811
|
}
|
|
14343
14812
|
const out = [];
|
|
14344
14813
|
const dayMap = days;
|
|
@@ -14360,19 +14829,20 @@ function buildCalendarGrid(days) {
|
|
|
14360
14829
|
const start = addDays(minDay, -minDay.getDay());
|
|
14361
14830
|
const end = addDays(maxDay, 6 - maxDay.getDay());
|
|
14362
14831
|
const weeks = Math.floor(diffDays(end, start) / 7) + 1;
|
|
14363
|
-
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 }]));
|
|
14364
14833
|
const cells = [];
|
|
14365
14834
|
const byIsoDate = /* @__PURE__ */ new Map();
|
|
14366
14835
|
for (let d = start; diffDays(end, d) >= 0; d = addDays(d, 1)) {
|
|
14367
14836
|
const isoDate = isoDay(d);
|
|
14368
|
-
const
|
|
14837
|
+
const dayValue = byValue.get(isoDate);
|
|
14369
14838
|
const cell = {
|
|
14370
14839
|
isoDate,
|
|
14371
14840
|
date: d,
|
|
14372
14841
|
week: Math.floor(diffDays(d, start) / 7),
|
|
14373
14842
|
weekday: d.getDay(),
|
|
14374
|
-
value,
|
|
14375
|
-
|
|
14843
|
+
value: dayValue?.value,
|
|
14844
|
+
row: dayValue?.row,
|
|
14845
|
+
present: dayValue !== void 0
|
|
14376
14846
|
};
|
|
14377
14847
|
cells.push(cell);
|
|
14378
14848
|
byIsoDate.set(isoDate, cell);
|
|
@@ -14391,9 +14861,9 @@ function collectDays(spec) {
|
|
|
14391
14861
|
const isoDate = isoDay(day);
|
|
14392
14862
|
const current = totals.get(isoDate);
|
|
14393
14863
|
if (current) current.value += value;
|
|
14394
|
-
else totals.set(isoDate, { date: day, value });
|
|
14864
|
+
else totals.set(isoDate, { date: day, value, row });
|
|
14395
14865
|
}
|
|
14396
|
-
return [...totals.entries()].map(([isoDate, { date, value }]) => ({ isoDate, date, value }));
|
|
14866
|
+
return [...totals.entries()].map(([isoDate, { date, value, row }]) => ({ isoDate, date, value, row }));
|
|
14397
14867
|
}
|
|
14398
14868
|
function monthAnchors(grid) {
|
|
14399
14869
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -14417,6 +14887,8 @@ function humanDate(date) {
|
|
|
14417
14887
|
function drawCalendarHeatmap(surface, spec, tokens, size, context) {
|
|
14418
14888
|
const cal = spec;
|
|
14419
14889
|
const ctx = surface.marks.ctx;
|
|
14890
|
+
const emphasis = context?.emphasis ?? null;
|
|
14891
|
+
const dateField = cal.encoding.date.field;
|
|
14420
14892
|
const content = drawTitleBlock(surface, tokens, size, cal.title);
|
|
14421
14893
|
const days = collectDays(cal);
|
|
14422
14894
|
const grid = buildCalendarGrid(days);
|
|
@@ -14456,6 +14928,7 @@ function drawCalendarHeatmap(surface, spec, tokens, size, context) {
|
|
|
14456
14928
|
const y = gridRect.y + c.weekday * (cell + GAP2);
|
|
14457
14929
|
const fill = c.present && c.value !== void 0 ? rgbaToCss(cscale.map(c.value)) : tokens.color.surface;
|
|
14458
14930
|
laidOut.set(c.isoDate, { ...c, x, y, size: cell, fill });
|
|
14931
|
+
ctx.globalAlpha = c.present ? rowAlpha(emphasis, c.row ?? { [dateField]: c.isoDate }) : 1;
|
|
14459
14932
|
if (pen) {
|
|
14460
14933
|
pen.rect(x, y, cell, cell, {
|
|
14461
14934
|
fill,
|
|
@@ -14469,6 +14942,7 @@ function drawCalendarHeatmap(surface, spec, tokens, size, context) {
|
|
|
14469
14942
|
ctx.lineWidth = 1;
|
|
14470
14943
|
ctx.strokeRect(x + 0.5, y + 0.5, Math.max(0, cell - 1), Math.max(0, cell - 1));
|
|
14471
14944
|
}
|
|
14945
|
+
ctx.globalAlpha = 1;
|
|
14472
14946
|
}
|
|
14473
14947
|
ctx.restore();
|
|
14474
14948
|
for (const m of monthAnchors(grid)) {
|
|
@@ -14746,13 +15220,15 @@ function buildWaterfallModel(spec, tokens, opts) {
|
|
|
14746
15220
|
}
|
|
14747
15221
|
|
|
14748
15222
|
// src/charts/waterfall.ts
|
|
14749
|
-
function drawConnectors(surface, model) {
|
|
15223
|
+
function drawConnectors(surface, model, alphaOf) {
|
|
14750
15224
|
if (model.connectors.length === 0) return;
|
|
14751
15225
|
const ctx = surface.marks.ctx;
|
|
14752
15226
|
const color = model.tokens.color.axis;
|
|
14753
15227
|
if (model.sketch) {
|
|
14754
15228
|
const pen = new RoughPen(ctx, model.sketch);
|
|
14755
|
-
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]);
|
|
14756
15232
|
pen.polyline(
|
|
14757
15233
|
[
|
|
14758
15234
|
{ x: c.x1, y: c.y },
|
|
@@ -14760,6 +15236,7 @@ function drawConnectors(surface, model) {
|
|
|
14760
15236
|
],
|
|
14761
15237
|
{ stroke: color }
|
|
14762
15238
|
);
|
|
15239
|
+
ctx.globalAlpha = 1;
|
|
14763
15240
|
}
|
|
14764
15241
|
return;
|
|
14765
15242
|
}
|
|
@@ -14767,23 +15244,28 @@ function drawConnectors(surface, model) {
|
|
|
14767
15244
|
ctx.strokeStyle = color;
|
|
14768
15245
|
ctx.lineWidth = 1;
|
|
14769
15246
|
ctx.setLineDash([2, 2]);
|
|
14770
|
-
|
|
14771
|
-
|
|
15247
|
+
for (let i = 0; i < model.connectors.length; i += 1) {
|
|
15248
|
+
const c = model.connectors[i];
|
|
14772
15249
|
const y = Math.round(c.y) + 0.5;
|
|
15250
|
+
ctx.globalAlpha = alphaOf(model.bars[i]);
|
|
15251
|
+
ctx.beginPath();
|
|
14773
15252
|
ctx.moveTo(c.x1, y);
|
|
14774
15253
|
ctx.lineTo(c.x2, y);
|
|
15254
|
+
ctx.stroke();
|
|
15255
|
+
ctx.globalAlpha = 1;
|
|
14775
15256
|
}
|
|
14776
|
-
ctx.stroke();
|
|
14777
15257
|
ctx.restore();
|
|
14778
15258
|
}
|
|
14779
|
-
function drawBars2(surface, model) {
|
|
15259
|
+
function drawBars2(surface, model, alphaOf) {
|
|
14780
15260
|
const ctx = surface.marks.ctx;
|
|
14781
15261
|
const pen = model.sketch ? new RoughPen(ctx, model.sketch) : null;
|
|
14782
15262
|
for (const bar of model.bars) {
|
|
14783
15263
|
const { rect } = bar;
|
|
14784
15264
|
if (rect.width <= 0 || rect.height <= 0) continue;
|
|
15265
|
+
ctx.globalAlpha = alphaOf(bar);
|
|
14785
15266
|
if (pen) {
|
|
14786
15267
|
pen.rect(rect.x, rect.y, rect.width, rect.height, { stroke: bar.color, fill: bar.color });
|
|
15268
|
+
ctx.globalAlpha = 1;
|
|
14787
15269
|
continue;
|
|
14788
15270
|
}
|
|
14789
15271
|
const r = Math.min(model.cornerRadius, rect.width / 2, rect.height / 2);
|
|
@@ -14791,6 +15273,7 @@ function drawBars2(surface, model) {
|
|
|
14791
15273
|
roundedRect(ctx, rect.x, rect.y, rect.width, rect.height, [r, r, r, r]);
|
|
14792
15274
|
ctx.fillStyle = bar.color;
|
|
14793
15275
|
ctx.fill();
|
|
15276
|
+
ctx.globalAlpha = 1;
|
|
14794
15277
|
}
|
|
14795
15278
|
}
|
|
14796
15279
|
function barLabelText(bar, format) {
|
|
@@ -14820,17 +15303,19 @@ function drawLabels(surface, model) {
|
|
|
14820
15303
|
});
|
|
14821
15304
|
}
|
|
14822
15305
|
}
|
|
14823
|
-
function drawWaterfall(surface, spec, tokens, size,
|
|
15306
|
+
function drawWaterfall(surface, spec, tokens, size, context) {
|
|
14824
15307
|
const waterfall = spec;
|
|
14825
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 });
|
|
14826
15312
|
drawAxesUnderlay(surface, model.base);
|
|
14827
|
-
drawConnectors(surface, model);
|
|
14828
|
-
drawBars2(surface, model);
|
|
15313
|
+
drawConnectors(surface, model, alphaOf);
|
|
15314
|
+
drawBars2(surface, model, alphaOf);
|
|
14829
15315
|
drawLabels(surface, model);
|
|
14830
15316
|
drawOverlay(surface, model.base);
|
|
14831
15317
|
if (model.bars.length === 0) return;
|
|
14832
15318
|
const { plot, format } = model;
|
|
14833
|
-
const stageField = waterfall.encoding.stage.field;
|
|
14834
15319
|
const valueLabel2 = waterfall.encoding.value.title ?? waterfall.encoding.value.field;
|
|
14835
15320
|
const fmtNum = (v) => format ? formatValue(v, format) : formatNumber2(v, ",");
|
|
14836
15321
|
const hitBar = (px, py) => {
|
|
@@ -15013,7 +15498,7 @@ var LABEL_WIDTH = 68;
|
|
|
15013
15498
|
function fmtValue(v, format) {
|
|
15014
15499
|
return format ? formatValue(v, format) : formatNumber2(v, ",");
|
|
15015
15500
|
}
|
|
15016
|
-
function drawLines(surface, model) {
|
|
15501
|
+
function drawLines(surface, model, alphaOf) {
|
|
15017
15502
|
const ctx = surface.marks.ctx;
|
|
15018
15503
|
const pen = model.sketch ? new RoughPen(ctx, model.sketch) : null;
|
|
15019
15504
|
ctx.save();
|
|
@@ -15023,6 +15508,7 @@ function drawLines(surface, model) {
|
|
|
15023
15508
|
for (const series of model.series) {
|
|
15024
15509
|
const points = series.points;
|
|
15025
15510
|
if (points.length === 0) continue;
|
|
15511
|
+
ctx.globalAlpha = alphaOf(series);
|
|
15026
15512
|
if (pen && points.length >= 2) {
|
|
15027
15513
|
pen.polyline(points, { stroke: series.color });
|
|
15028
15514
|
} else if (points.length >= 2) {
|
|
@@ -15048,16 +15534,18 @@ function drawLines(surface, model) {
|
|
|
15048
15534
|
ctx.stroke();
|
|
15049
15535
|
}
|
|
15050
15536
|
}
|
|
15537
|
+
ctx.globalAlpha = 1;
|
|
15051
15538
|
}
|
|
15052
15539
|
ctx.restore();
|
|
15053
15540
|
}
|
|
15054
|
-
function drawEndLabels(surface, model) {
|
|
15541
|
+
function drawEndLabels(surface, model, alphaOf) {
|
|
15055
15542
|
if (!model.showLabels) return;
|
|
15056
15543
|
const { tokens } = model;
|
|
15057
15544
|
const labelSize = tokens.font.size.tiny;
|
|
15058
15545
|
const valueSize = tokens.font.size.tiny;
|
|
15059
15546
|
for (const series of model.series) {
|
|
15060
15547
|
if (series.points.length === 0) continue;
|
|
15548
|
+
const opacity = alphaOf(series) === 1 ? 1 : 0.5;
|
|
15061
15549
|
const first = series.points[0];
|
|
15062
15550
|
const last = series.points[series.points.length - 1];
|
|
15063
15551
|
addOverlayText(surface, tokens, {
|
|
@@ -15068,7 +15556,8 @@ function drawEndLabels(surface, model) {
|
|
|
15068
15556
|
color: series.color,
|
|
15069
15557
|
size: labelSize,
|
|
15070
15558
|
weight: tokens.font.weight.bold,
|
|
15071
|
-
align: "right"
|
|
15559
|
+
align: "right",
|
|
15560
|
+
opacity
|
|
15072
15561
|
});
|
|
15073
15562
|
addOverlayText(surface, tokens, {
|
|
15074
15563
|
left: first.x - 8 - LABEL_WIDTH,
|
|
@@ -15077,7 +15566,8 @@ function drawEndLabels(surface, model) {
|
|
|
15077
15566
|
text: fmtValue(first.value, model.format),
|
|
15078
15567
|
color: tokens.color.textMuted,
|
|
15079
15568
|
size: valueSize,
|
|
15080
|
-
align: "right"
|
|
15569
|
+
align: "right",
|
|
15570
|
+
opacity
|
|
15081
15571
|
});
|
|
15082
15572
|
if (last !== first) {
|
|
15083
15573
|
addOverlayText(surface, tokens, {
|
|
@@ -15088,7 +15578,8 @@ function drawEndLabels(surface, model) {
|
|
|
15088
15578
|
color: series.color,
|
|
15089
15579
|
size: valueSize,
|
|
15090
15580
|
weight: tokens.font.weight.medium,
|
|
15091
|
-
align: "left"
|
|
15581
|
+
align: "left",
|
|
15582
|
+
opacity
|
|
15092
15583
|
});
|
|
15093
15584
|
}
|
|
15094
15585
|
}
|
|
@@ -15106,16 +15597,18 @@ function nearestPoint(model, px, py) {
|
|
|
15106
15597
|
}
|
|
15107
15598
|
return best ? { series: best.series, point: best.point } : null;
|
|
15108
15599
|
}
|
|
15109
|
-
function drawSlope(surface, spec, tokens, size,
|
|
15600
|
+
function drawSlope(surface, spec, tokens, size, context) {
|
|
15110
15601
|
const slope = spec;
|
|
15111
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;
|
|
15112
15606
|
drawAxesUnderlay(surface, model.base);
|
|
15113
|
-
drawLines(surface, model);
|
|
15607
|
+
drawLines(surface, model, alphaOf);
|
|
15114
15608
|
drawOverlay(surface, model.base);
|
|
15115
|
-
drawEndLabels(surface, model);
|
|
15609
|
+
drawEndLabels(surface, model, alphaOf);
|
|
15116
15610
|
if (model.series.length === 0) return;
|
|
15117
15611
|
const { plot } = model;
|
|
15118
|
-
const seriesField = slope.encoding.series.field;
|
|
15119
15612
|
return {
|
|
15120
15613
|
region: { x: plot.x, y: plot.y, width: plot.width, height: plot.height },
|
|
15121
15614
|
hitTest: (px, py) => {
|
|
@@ -15308,10 +15801,12 @@ function layoutPlot(content, labelWidth, tokens) {
|
|
|
15308
15801
|
}
|
|
15309
15802
|
};
|
|
15310
15803
|
}
|
|
15311
|
-
function drawDumbbell(surface, spec, tokens, size,
|
|
15804
|
+
function drawDumbbell(surface, spec, tokens, size, context) {
|
|
15312
15805
|
const dumbbell = spec;
|
|
15313
15806
|
const model = buildDumbbellModel(dumbbell, tokens);
|
|
15314
15807
|
const ctx = surface.marks.ctx;
|
|
15808
|
+
const emphasis = context?.emphasis ?? null;
|
|
15809
|
+
const categoryField = dumbbell.encoding.category.field;
|
|
15315
15810
|
let content = drawTitleBlock(surface, tokens, size, dumbbell.title);
|
|
15316
15811
|
const legendEntries = model.groups.map((group) => ({ label: group.label, color: group.color, symbol: "circle" }));
|
|
15317
15812
|
content = drawCategoricalLegend(surface, tokens, content, legendEntries, "top");
|
|
@@ -15372,8 +15867,10 @@ function drawDumbbell(surface, spec, tokens, size, _context) {
|
|
|
15372
15867
|
}).sort((a, b) => a.x - b.x);
|
|
15373
15868
|
if (pixelDots.length === 0) continue;
|
|
15374
15869
|
pixelRows.push({ ...row, y: cy, pixelDots });
|
|
15870
|
+
ctx.globalAlpha = rowAlpha(emphasis, { [categoryField]: row.catRawValue });
|
|
15375
15871
|
drawConnector(ctx, pen, pixelDots[0].x, pixelDots[pixelDots.length - 1].x, cy, tokens);
|
|
15376
15872
|
for (const dot of pixelDots) drawDot(ctx, pen, dot.x, cy, dot.color, tokens);
|
|
15873
|
+
ctx.globalAlpha = 1;
|
|
15377
15874
|
addOverlayText(surface, tokens, {
|
|
15378
15875
|
left: content.x,
|
|
15379
15876
|
top: cy,
|
|
@@ -15401,7 +15898,6 @@ function drawDumbbell(surface, spec, tokens, size, _context) {
|
|
|
15401
15898
|
}
|
|
15402
15899
|
}
|
|
15403
15900
|
}
|
|
15404
|
-
const categoryField = dumbbell.encoding.category.field;
|
|
15405
15901
|
const nearestDot = (px, py) => {
|
|
15406
15902
|
let best = null;
|
|
15407
15903
|
for (const row of pixelRows) {
|
|
@@ -16737,6 +17233,8 @@ var CANVAS_MARK_TYPES = /* @__PURE__ */ new Set([
|
|
|
16737
17233
|
"sankey",
|
|
16738
17234
|
"choropleth"
|
|
16739
17235
|
]);
|
|
17236
|
+
var COUNT_UP_TYPES = /* @__PURE__ */ new Set(["kpi"]);
|
|
17237
|
+
var SWEEP_TYPES = /* @__PURE__ */ new Set(["pie", "gauge"]);
|
|
16740
17238
|
var EMPTY_DATA = [];
|
|
16741
17239
|
function legendConfigFor(model) {
|
|
16742
17240
|
const legend = model.spec.legend;
|
|
@@ -16832,6 +17330,9 @@ function render(target, spec, options) {
|
|
|
16832
17330
|
let interaction;
|
|
16833
17331
|
let entrance;
|
|
16834
17332
|
let updateTransition;
|
|
17333
|
+
let lastValue = null;
|
|
17334
|
+
let lastCompare = null;
|
|
17335
|
+
let lastSparkline = null;
|
|
16835
17336
|
let destroyed = false;
|
|
16836
17337
|
let lastDrawnW = -1;
|
|
16837
17338
|
let lastDrawnH = -1;
|
|
@@ -16859,7 +17360,7 @@ function render(target, spec, options) {
|
|
|
16859
17360
|
}
|
|
16860
17361
|
}
|
|
16861
17362
|
const hostListeners = /* @__PURE__ */ new Set();
|
|
16862
|
-
const draw = (animateEntrance) => {
|
|
17363
|
+
const draw = (animateEntrance, valueTweenFrom, compareTweenFrom, sparklineTweenFrom) => {
|
|
16863
17364
|
entrance?.cancel();
|
|
16864
17365
|
entrance = void 0;
|
|
16865
17366
|
updateTransition?.cancel();
|
|
@@ -16889,6 +17390,11 @@ function render(target, spec, options) {
|
|
|
16889
17390
|
let facetRpt;
|
|
16890
17391
|
let legendSelect = null;
|
|
16891
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
|
+
}
|
|
16892
17398
|
const facetLayout = isFaceted(effectiveSpec) ? buildFacetModels(effectiveSpec, tokens, size) : null;
|
|
16893
17399
|
if (facetLayout) {
|
|
16894
17400
|
drawFacet(surface, effectiveSpec, facetLayout, tokens);
|
|
@@ -16919,7 +17425,8 @@ function render(target, spec, options) {
|
|
|
16919
17425
|
tokens,
|
|
16920
17426
|
currentSpec.background,
|
|
16921
17427
|
anim,
|
|
16922
|
-
finishEntrance
|
|
17428
|
+
finishEntrance,
|
|
17429
|
+
type === "bar" ? "grow" : "wipe"
|
|
16923
17430
|
);
|
|
16924
17431
|
}
|
|
16925
17432
|
} else {
|
|
@@ -16937,11 +17444,44 @@ function render(target, spec, options) {
|
|
|
16937
17444
|
}
|
|
16938
17445
|
};
|
|
16939
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
|
+
}
|
|
16940
17480
|
} else {
|
|
16941
17481
|
drawPlaceholder(surface, tokens, `\u201C${type}\u201D renderer not yet registered`);
|
|
16942
|
-
|
|
16943
|
-
|
|
16944
|
-
|
|
17482
|
+
if (anim.enabled) {
|
|
17483
|
+
entrance = runFadeEntrance(surface, anim, finishEntrance);
|
|
17484
|
+
}
|
|
16945
17485
|
}
|
|
16946
17486
|
}
|
|
16947
17487
|
if (!interaction) interaction = new InteractionController(surface, tokens);
|
|
@@ -17065,8 +17605,18 @@ function render(target, spec, options) {
|
|
|
17065
17605
|
});
|
|
17066
17606
|
const wantFade = anim.enabled && CANVAS_MARK_TYPES.has(prevType) && CANVAS_MARK_TYPES.has(next.type);
|
|
17067
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;
|
|
17068
17613
|
currentSpec = next;
|
|
17069
|
-
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
|
+
);
|
|
17070
17620
|
if (before && surface.width === prevW && surface.height === prevH && surface.marks.canvas.width === before.width && surface.marks.canvas.height === before.height) {
|
|
17071
17621
|
updateTransition = runUpdateCrossfade(surface, before, anim);
|
|
17072
17622
|
}
|
|
@@ -17086,7 +17636,7 @@ function render(target, spec, options) {
|
|
|
17086
17636
|
}
|
|
17087
17637
|
};
|
|
17088
17638
|
}
|
|
17089
|
-
function runCartesianEntrance(surface, model, renderer, tokens, background, anim, onDone) {
|
|
17639
|
+
function runCartesianEntrance(surface, model, renderer, tokens, background, anim, onDone, mode = "wipe") {
|
|
17090
17640
|
const ctx = surface.marks.ctx;
|
|
17091
17641
|
const { plot } = model;
|
|
17092
17642
|
const bg = background ?? tokens.color.background;
|
|
@@ -17097,6 +17647,14 @@ function runCartesianEntrance(surface, model, renderer, tokens, background, anim
|
|
|
17097
17647
|
ctx.fillRect(0, 0, surface.width, surface.height);
|
|
17098
17648
|
ctx.restore();
|
|
17099
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
|
+
}
|
|
17100
17658
|
if (t <= 0) return;
|
|
17101
17659
|
ctx.save();
|
|
17102
17660
|
ctx.globalAlpha = Math.min(1, 0.2 + 0.8 * t);
|
|
@@ -17119,6 +17677,17 @@ function runCartesianEntrance(surface, model, renderer, tokens, background, anim
|
|
|
17119
17677
|
}
|
|
17120
17678
|
});
|
|
17121
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
|
+
}
|
|
17122
17691
|
function runFadeEntrance(surface, anim, onDone) {
|
|
17123
17692
|
const root = surface.root;
|
|
17124
17693
|
const apply = (t) => {
|
|
@@ -17270,12 +17839,17 @@ function autoParamName(view) {
|
|
|
17270
17839
|
const existing = view.spec.params?.[0]?.name;
|
|
17271
17840
|
return existing ?? `__sel__${view.id}`;
|
|
17272
17841
|
}
|
|
17842
|
+
function roleOf(view) {
|
|
17843
|
+
return view.interact ?? "auto";
|
|
17844
|
+
}
|
|
17273
17845
|
function wireViews(views, interactions) {
|
|
17274
17846
|
const out = views.map((v) => ({ ...v, spec: v.spec }));
|
|
17275
17847
|
if (interactions === "none") return out;
|
|
17276
17848
|
const sources = [];
|
|
17277
17849
|
out.forEach((v, i) => {
|
|
17278
17850
|
const type = v.spec.type;
|
|
17851
|
+
const role = roleOf(v);
|
|
17852
|
+
if (role === "none" || role === "target") return;
|
|
17279
17853
|
if (isSlicerType(type)) {
|
|
17280
17854
|
const s = v.spec;
|
|
17281
17855
|
sources.push({
|
|
@@ -17306,11 +17880,15 @@ function wireViews(views, interactions) {
|
|
|
17306
17880
|
name: src.param,
|
|
17307
17881
|
select: { type: "point", on: "click", fields: src.fields }
|
|
17308
17882
|
});
|
|
17309
|
-
|
|
17883
|
+
if (roleOf(out[src.view]) !== "source") {
|
|
17884
|
+
out[src.view].spec = addHighlight(out[src.view].spec, src.param);
|
|
17885
|
+
}
|
|
17310
17886
|
}
|
|
17311
17887
|
out.forEach((v, j) => {
|
|
17312
17888
|
if (isSlicerType(v.spec.type)) return;
|
|
17313
17889
|
if (j === src.view) return;
|
|
17890
|
+
const trole = roleOf(v);
|
|
17891
|
+
if (trole === "none" || trole === "source") return;
|
|
17314
17892
|
out[j].spec = addFilterParam(out[j].spec, src.param);
|
|
17315
17893
|
});
|
|
17316
17894
|
}
|
|
@@ -17360,6 +17938,41 @@ var DEFAULT_BREAKPOINTS = [
|
|
|
17360
17938
|
{ maxWidth: 960, cols: 6 }
|
|
17361
17939
|
];
|
|
17362
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
|
+
}
|
|
17363
17976
|
function stripCellHeight(view, rowHeight) {
|
|
17364
17977
|
const titled = !!(view.title || view.subtitle);
|
|
17365
17978
|
const titlePad = titled ? 30 : 0;
|
|
@@ -17502,6 +18115,12 @@ function renderDashboard(target, spec, options) {
|
|
|
17502
18115
|
let root;
|
|
17503
18116
|
let resizeObs;
|
|
17504
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
|
+
};
|
|
17505
18124
|
const build = () => {
|
|
17506
18125
|
const tokens = resolveTheme(currentSpec.theme);
|
|
17507
18126
|
const layout2 = resolveDashboardLayout(currentSpec.layout);
|
|
@@ -17514,8 +18133,9 @@ function renderDashboard(target, spec, options) {
|
|
|
17514
18133
|
store.set(param.name, param.value ?? null);
|
|
17515
18134
|
}
|
|
17516
18135
|
}
|
|
18136
|
+
sourceCells = /* @__PURE__ */ new Map();
|
|
17517
18137
|
root = document.createElement("div");
|
|
17518
|
-
root.className =
|
|
18138
|
+
root.className = DASHBOARD_CLASS;
|
|
17519
18139
|
Object.assign(root.style, {
|
|
17520
18140
|
boxSizing: "border-box",
|
|
17521
18141
|
width: currentSpec.dimensions?.width ? `${currentSpec.dimensions.width}px` : "100%",
|
|
@@ -17529,6 +18149,10 @@ function renderDashboard(target, spec, options) {
|
|
|
17529
18149
|
flexDirection: "column",
|
|
17530
18150
|
gap: `${layout2.gap}px`
|
|
17531
18151
|
});
|
|
18152
|
+
const style = document.createElement("style");
|
|
18153
|
+
style.setAttribute("data-graphein-dashboard-style", "");
|
|
18154
|
+
style.textContent = dashboardStyles(tokens);
|
|
18155
|
+
root.appendChild(style);
|
|
17532
18156
|
const titleText3 = typeof currentSpec.title === "string" ? currentSpec.title : currentSpec.title?.text;
|
|
17533
18157
|
if (titleText3 || currentSpec.subtitle) {
|
|
17534
18158
|
const header = document.createElement("div");
|
|
@@ -17588,6 +18212,7 @@ function renderDashboard(target, spec, options) {
|
|
|
17588
18212
|
};
|
|
17589
18213
|
const cellChrome = (cell, view) => {
|
|
17590
18214
|
const framed = view?.frame ?? true;
|
|
18215
|
+
cell.dataset.framed = framed ? "true" : "false";
|
|
17591
18216
|
Object.assign(cell.style, {
|
|
17592
18217
|
minWidth: "0",
|
|
17593
18218
|
minHeight: "0",
|
|
@@ -17599,9 +18224,10 @@ function renderDashboard(target, spec, options) {
|
|
|
17599
18224
|
background: view?.background ?? (framed ? tokens.color.surface : "transparent")
|
|
17600
18225
|
});
|
|
17601
18226
|
};
|
|
17602
|
-
const
|
|
18227
|
+
const createCard = (view) => {
|
|
17603
18228
|
const cell = document.createElement("div");
|
|
17604
18229
|
cellChrome(cell, view);
|
|
18230
|
+
cell.classList.add(DASHBOARD_CELL_CLASS);
|
|
17605
18231
|
cell.setAttribute("data-view-id", view.id);
|
|
17606
18232
|
const hasCardChrome = !!(view.title || view.subtitle || view.accent || view.background || view.frame === false || view.padding === "none");
|
|
17607
18233
|
if (!hasCardChrome) return { cell, host: cell };
|
|
@@ -17628,13 +18254,14 @@ function renderDashboard(target, spec, options) {
|
|
|
17628
18254
|
padding: `${tokens.spacing.sm}px ${tokens.spacing.md}px ${tokens.spacing.xs}px ${tokens.spacing.md}px`,
|
|
17629
18255
|
display: "flex",
|
|
17630
18256
|
flexDirection: "column",
|
|
17631
|
-
gap: "2px"
|
|
18257
|
+
gap: "2px",
|
|
18258
|
+
borderBottom: `1px solid ${tokens.color.border}`
|
|
17632
18259
|
});
|
|
17633
18260
|
if (view.title) {
|
|
17634
18261
|
const title2 = document.createElement("div");
|
|
17635
18262
|
title2.textContent = view.title;
|
|
17636
18263
|
Object.assign(title2.style, {
|
|
17637
|
-
font: `${tokens.font.weight.
|
|
18264
|
+
font: `${tokens.font.weight.medium} ${tokens.font.size.base}px ${tokens.font.family}`,
|
|
17638
18265
|
color: tokens.color.text,
|
|
17639
18266
|
letterSpacing: "-0.01em"
|
|
17640
18267
|
});
|
|
@@ -17662,6 +18289,24 @@ function renderDashboard(target, spec, options) {
|
|
|
17662
18289
|
cell.appendChild(host);
|
|
17663
18290
|
return { cell, host };
|
|
17664
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
|
+
};
|
|
17665
18310
|
const sectionPlans = resolveDashboardSections(gridViews, layout2);
|
|
17666
18311
|
const layoutGroups = [];
|
|
17667
18312
|
const deferredMounts = [];
|
|
@@ -17674,7 +18319,8 @@ function renderDashboard(target, spec, options) {
|
|
|
17674
18319
|
gap: `${layout2.gap}px`
|
|
17675
18320
|
});
|
|
17676
18321
|
for (const view of stripViews) {
|
|
17677
|
-
const { cell, host } =
|
|
18322
|
+
const { cell, host } = createCard(view);
|
|
18323
|
+
registerSourceCell(view, cell);
|
|
17678
18324
|
Object.assign(cell.style, {
|
|
17679
18325
|
flex: "1 1 220px",
|
|
17680
18326
|
minWidth: "200px",
|
|
@@ -17720,7 +18366,13 @@ function renderDashboard(target, spec, options) {
|
|
|
17720
18366
|
if (plan.section?.title) {
|
|
17721
18367
|
const title2 = document.createElement("span");
|
|
17722
18368
|
title2.textContent = plan.section.title;
|
|
17723
|
-
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
|
+
});
|
|
17724
18376
|
titles.appendChild(title2);
|
|
17725
18377
|
}
|
|
17726
18378
|
if (plan.section?.subtitle) {
|
|
@@ -17772,7 +18424,8 @@ function renderDashboard(target, spec, options) {
|
|
|
17772
18424
|
parent.appendChild(grid);
|
|
17773
18425
|
const cells = placeViews(plan.views, plan.cols, layout2.preset ?? "auto");
|
|
17774
18426
|
for (const cellInfo of cells) {
|
|
17775
|
-
const { cell, host } =
|
|
18427
|
+
const { cell, host } = createCard(cellInfo.view);
|
|
18428
|
+
registerSourceCell(cellInfo.view, cell);
|
|
17776
18429
|
grid.appendChild(cell);
|
|
17777
18430
|
cellInfo.el = cell;
|
|
17778
18431
|
deferredMounts.push({ el: host, view: cellInfo.view, mounted: false });
|
|
@@ -17845,8 +18498,10 @@ function renderDashboard(target, spec, options) {
|
|
|
17845
18498
|
instances.length = 0;
|
|
17846
18499
|
if (root && root.parentNode) root.parentNode.removeChild(root);
|
|
17847
18500
|
root = void 0;
|
|
18501
|
+
sourceCells = /* @__PURE__ */ new Map();
|
|
17848
18502
|
};
|
|
17849
18503
|
const unsubscribe = store.subscribe((name, value) => {
|
|
18504
|
+
syncSourceCell(name, value);
|
|
17850
18505
|
for (const listener of [...hostListeners]) listener(name, value);
|
|
17851
18506
|
});
|
|
17852
18507
|
build();
|
|
@@ -18064,6 +18719,6 @@ function renderToContext(target, spec) {
|
|
|
18064
18719
|
// src/index.ts
|
|
18065
18720
|
var VERSION = "0.1.0";
|
|
18066
18721
|
|
|
18067
|
-
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 };
|
|
18068
18723
|
//# sourceMappingURL=index.js.map
|
|
18069
18724
|
//# sourceMappingURL=index.js.map
|