graphein 0.12.0 → 0.14.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 +147 -11
- package/dist/index.js +1510 -626
- 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";
|
|
@@ -6020,6 +6073,7 @@ function resolveSketch(spec) {
|
|
|
6020
6073
|
|
|
6021
6074
|
// src/layout/frame.ts
|
|
6022
6075
|
var TICK_SIZE = 6;
|
|
6076
|
+
var NO_PADDING = { top: 0, right: 0, bottom: 0, left: 0 };
|
|
6023
6077
|
var AXIS_LABEL_GAP = 6;
|
|
6024
6078
|
var AXIS_TITLE_GAP = 4;
|
|
6025
6079
|
var LEGEND_SWATCH = 11;
|
|
@@ -6638,6 +6692,155 @@ function drawLegend(surface, items, model) {
|
|
|
6638
6692
|
}
|
|
6639
6693
|
}
|
|
6640
6694
|
|
|
6695
|
+
// src/charts/emphasis.ts
|
|
6696
|
+
function rowAlpha(emphasis, row) {
|
|
6697
|
+
if (!emphasis) return 1;
|
|
6698
|
+
return emphasis.match(row) ? 1 : emphasis.dim;
|
|
6699
|
+
}
|
|
6700
|
+
function seriesEmphasisMode(emphasis, series) {
|
|
6701
|
+
if (!emphasis) return "all";
|
|
6702
|
+
let matched = 0;
|
|
6703
|
+
for (const row of series.rows) if (emphasis.match(row)) matched++;
|
|
6704
|
+
if (matched === 0) return "none";
|
|
6705
|
+
if (matched === series.rows.length) return "all";
|
|
6706
|
+
return "partial";
|
|
6707
|
+
}
|
|
6708
|
+
|
|
6709
|
+
// src/charts/scatter.ts
|
|
6710
|
+
var QUANTIZE_MIN_POINTS = 4e3;
|
|
6711
|
+
var QUANTIZE_AREA_DIVISOR = 4;
|
|
6712
|
+
var FIXED_RADIUS = 3.75;
|
|
6713
|
+
var MIN_SIZED_RADIUS = 3;
|
|
6714
|
+
function maxSizedRadius(plotSize) {
|
|
6715
|
+
return clamp(plotSize * 0.05, 10, 26);
|
|
6716
|
+
}
|
|
6717
|
+
function maxScatterRadius(spec, plotW, plotH) {
|
|
6718
|
+
return spec.encoding.size?.field ? maxSizedRadius(Math.min(plotW, plotH)) : FIXED_RADIUS;
|
|
6719
|
+
}
|
|
6720
|
+
function isMissing(value) {
|
|
6721
|
+
return value == null || value === "";
|
|
6722
|
+
}
|
|
6723
|
+
function isInvalidPositionValue(value) {
|
|
6724
|
+
return isMissing(value) || typeof value === "number" && Number.isNaN(value) || value instanceof Date && Number.isNaN(value.getTime());
|
|
6725
|
+
}
|
|
6726
|
+
function fixedRadius() {
|
|
6727
|
+
return FIXED_RADIUS;
|
|
6728
|
+
}
|
|
6729
|
+
function shouldQuantize(count, plot, hasSize) {
|
|
6730
|
+
return !hasSize && count > QUANTIZE_MIN_POINTS && count > plot.width * plot.height / QUANTIZE_AREA_DIVISOR;
|
|
6731
|
+
}
|
|
6732
|
+
function quantizePoints(points) {
|
|
6733
|
+
if (points.length === 0) return [];
|
|
6734
|
+
let minX = Infinity;
|
|
6735
|
+
let maxX = -Infinity;
|
|
6736
|
+
let minY = Infinity;
|
|
6737
|
+
let maxY = -Infinity;
|
|
6738
|
+
for (const p of points) {
|
|
6739
|
+
if (p.x < minX) minX = p.x;
|
|
6740
|
+
if (p.x > maxX) maxX = p.x;
|
|
6741
|
+
if (p.y < minY) minY = p.y;
|
|
6742
|
+
if (p.y > maxY) maxY = p.y;
|
|
6743
|
+
}
|
|
6744
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6745
|
+
const keep = [];
|
|
6746
|
+
for (const p of points) {
|
|
6747
|
+
const extentPoint = p.x === minX || p.x === maxX || p.y === minY || p.y === maxY;
|
|
6748
|
+
const ix = Math.round(p.x);
|
|
6749
|
+
const iy = Math.round(p.y);
|
|
6750
|
+
const key = `${p.seriesKey}|${ix}|${iy}`;
|
|
6751
|
+
if (extentPoint || !seen.has(key)) {
|
|
6752
|
+
keep.push(p);
|
|
6753
|
+
seen.add(key);
|
|
6754
|
+
}
|
|
6755
|
+
}
|
|
6756
|
+
return keep;
|
|
6757
|
+
}
|
|
6758
|
+
function radiusScale(rows, field, plotSize) {
|
|
6759
|
+
const rMin = MIN_SIZED_RADIUS;
|
|
6760
|
+
const rMax = maxSizedRadius(plotSize);
|
|
6761
|
+
const mid = (rMin + rMax) / 2;
|
|
6762
|
+
const domain = extent(rows, field);
|
|
6763
|
+
if (!domain) return () => mid;
|
|
6764
|
+
const [min, max] = domain;
|
|
6765
|
+
if (min === max) return () => mid;
|
|
6766
|
+
return (value) => {
|
|
6767
|
+
const n = toNumber(value);
|
|
6768
|
+
if (Number.isNaN(n)) return rMin;
|
|
6769
|
+
const norm = clamp((n - min) / (max - min), 0, 1);
|
|
6770
|
+
return rMin + (rMax - rMin) * Math.sqrt(norm);
|
|
6771
|
+
};
|
|
6772
|
+
}
|
|
6773
|
+
function drawScatter(surface, model) {
|
|
6774
|
+
const ctx = surface.marks.ctx;
|
|
6775
|
+
const spec = model.spec;
|
|
6776
|
+
const { plot, tokens } = model;
|
|
6777
|
+
const readX = accessor(model.x.field);
|
|
6778
|
+
const readY = accessor(model.y.field ?? "");
|
|
6779
|
+
const sizeField = spec.encoding.size?.field;
|
|
6780
|
+
const allRows = model.series.flatMap((series) => series.rows);
|
|
6781
|
+
const readSize = sizeField ? accessor(sizeField) : null;
|
|
6782
|
+
const radius = sizeField ? radiusScale(allRows, sizeField, Math.min(plot.width, plot.height)) : fixedRadius;
|
|
6783
|
+
const points = [];
|
|
6784
|
+
for (const series of model.series) {
|
|
6785
|
+
for (const row of series.rows) {
|
|
6786
|
+
const xValue = readX(row);
|
|
6787
|
+
const yValue = readY(row);
|
|
6788
|
+
const yNumber = toNumber(yValue);
|
|
6789
|
+
const px = model.x.pixel(xValue);
|
|
6790
|
+
if (px == null || isInvalidPositionValue(xValue) || isMissing(yValue) || Number.isNaN(yNumber)) {
|
|
6791
|
+
continue;
|
|
6792
|
+
}
|
|
6793
|
+
const py = model.y.pixel(yValue);
|
|
6794
|
+
if (!Number.isFinite(px) || !Number.isFinite(py)) continue;
|
|
6795
|
+
points.push({
|
|
6796
|
+
x: px,
|
|
6797
|
+
y: py,
|
|
6798
|
+
r: readSize ? radius(readSize(row)) : radius(),
|
|
6799
|
+
color: series.color,
|
|
6800
|
+
alpha: rowAlpha(model.emphasis, row),
|
|
6801
|
+
seriesKey: series.key
|
|
6802
|
+
});
|
|
6803
|
+
}
|
|
6804
|
+
}
|
|
6805
|
+
const originalCount = points.length;
|
|
6806
|
+
const drawPoints = shouldQuantize(originalCount, plot, !!sizeField) ? quantizePoints(points) : points;
|
|
6807
|
+
model.markStats = { drawn: drawPoints.length, original: originalCount };
|
|
6808
|
+
if (sizeField) drawPoints.sort((a, b) => b.r - a.r);
|
|
6809
|
+
ctx.save();
|
|
6810
|
+
ctx.beginPath();
|
|
6811
|
+
ctx.rect(plot.x, plot.y, plot.width, plot.height);
|
|
6812
|
+
ctx.clip();
|
|
6813
|
+
if (model.sketch) {
|
|
6814
|
+
const pen = new RoughPen(ctx, model.sketch);
|
|
6815
|
+
for (const point of drawPoints) {
|
|
6816
|
+
const prev = ctx.globalAlpha;
|
|
6817
|
+
if (point.alpha !== 1) ctx.globalAlpha = point.alpha;
|
|
6818
|
+
pen.circle(point.x, point.y, point.r, {
|
|
6819
|
+
fill: point.color,
|
|
6820
|
+
fillAlpha: 0.7,
|
|
6821
|
+
stroke: tokens.color.background,
|
|
6822
|
+
strokeWidth: 1
|
|
6823
|
+
});
|
|
6824
|
+
if (point.alpha !== 1) ctx.globalAlpha = prev;
|
|
6825
|
+
}
|
|
6826
|
+
ctx.restore();
|
|
6827
|
+
return;
|
|
6828
|
+
}
|
|
6829
|
+
ctx.lineWidth = 1;
|
|
6830
|
+
ctx.strokeStyle = tokens.color.background;
|
|
6831
|
+
for (const point of drawPoints) {
|
|
6832
|
+
ctx.beginPath();
|
|
6833
|
+
ctx.arc(point.x, point.y, point.r, 0, Math.PI * 2);
|
|
6834
|
+
ctx.fillStyle = point.color;
|
|
6835
|
+
ctx.globalAlpha = 0.7 * point.alpha;
|
|
6836
|
+
ctx.fill();
|
|
6837
|
+
ctx.globalAlpha = point.alpha;
|
|
6838
|
+
ctx.stroke();
|
|
6839
|
+
}
|
|
6840
|
+
ctx.globalAlpha = 1;
|
|
6841
|
+
ctx.restore();
|
|
6842
|
+
}
|
|
6843
|
+
|
|
6641
6844
|
// src/runtime/cartesian.ts
|
|
6642
6845
|
var CURVES = {
|
|
6643
6846
|
linear: curveLinear,
|
|
@@ -6728,8 +6931,43 @@ function formatNumericTick(value, userFormat) {
|
|
|
6728
6931
|
if (userFormat) return formatValue(value, userFormat);
|
|
6729
6932
|
return formatNumber2(value, ",");
|
|
6730
6933
|
}
|
|
6731
|
-
var DEFAULT_PADDING = { top: 12, right: 16, bottom: 12, left: 12 };
|
|
6732
6934
|
var LEGEND_SWATCH2 = 11;
|
|
6935
|
+
var SCATTER_EDGE_MARGIN = 2;
|
|
6936
|
+
var ZERO_INSETS = { top: 0, right: 0, bottom: 0, left: 0 };
|
|
6937
|
+
function annotationEdgeInsets(spec, yDomain, fontFamily, fontSize, fontWeight) {
|
|
6938
|
+
const explicit = spec.annotations;
|
|
6939
|
+
const points = Array.isArray(explicit) ? explicit.filter((a) => a.type === "point") : [];
|
|
6940
|
+
for (const a of autoInsightAnnotations(spec)) points.push(a);
|
|
6941
|
+
if (points.length === 0) return ZERO_INSETS;
|
|
6942
|
+
const span = yDomain[1] - yDomain[0] || 1;
|
|
6943
|
+
const font = fontString(fontSize, fontFamily, fontWeight);
|
|
6944
|
+
let maxR = 0;
|
|
6945
|
+
let maxLabelHalf = 0;
|
|
6946
|
+
let anyLabel = false;
|
|
6947
|
+
let maxFy = -Infinity;
|
|
6948
|
+
let minFy = Infinity;
|
|
6949
|
+
for (const ann of points) {
|
|
6950
|
+
const r = ann.markerRadius ?? 3.5;
|
|
6951
|
+
if (r > maxR) maxR = r;
|
|
6952
|
+
if (ann.label) {
|
|
6953
|
+
anyLabel = true;
|
|
6954
|
+
const half = (measureText(ann.label, font).width + 6) / 2;
|
|
6955
|
+
if (half > maxLabelHalf) maxLabelHalf = half;
|
|
6956
|
+
}
|
|
6957
|
+
const yv = toNumber(ann.y);
|
|
6958
|
+
if (!Number.isNaN(yv)) {
|
|
6959
|
+
const fy = (yv - yDomain[0]) / span;
|
|
6960
|
+
if (fy > maxFy) maxFy = fy;
|
|
6961
|
+
if (fy < minFy) minFy = fy;
|
|
6962
|
+
}
|
|
6963
|
+
}
|
|
6964
|
+
const halo = maxR + 1.6;
|
|
6965
|
+
const gap = maxR + 4;
|
|
6966
|
+
const hReserve = Math.ceil(Math.max(maxLabelHalf, halo)) + 2;
|
|
6967
|
+
const top = maxFy >= 0.5 ? Math.ceil(anyLabel ? gap + fontSize : halo) + 2 : 0;
|
|
6968
|
+
const bottom = minFy <= 0.5 ? Math.ceil(halo) + 2 : 0;
|
|
6969
|
+
return { top, right: hReserve, bottom, left: hReserve };
|
|
6970
|
+
}
|
|
6733
6971
|
function legendInteractive(spec) {
|
|
6734
6972
|
return typeof spec.legend === "object" && spec.legend.interactive === true;
|
|
6735
6973
|
}
|
|
@@ -6894,7 +7132,7 @@ function buildCartesianModel(spec, tokens, opts) {
|
|
|
6894
7132
|
color: s.color,
|
|
6895
7133
|
symbol: spec.type === "scatter" ? "circle" : spec.type === "line" ? "line" : "square"
|
|
6896
7134
|
}));
|
|
6897
|
-
const padding = { ...
|
|
7135
|
+
const padding = { ...NO_PADDING, ...spec.padding };
|
|
6898
7136
|
const titleInput4 = resolveTitle(spec);
|
|
6899
7137
|
const categoryAxisInput = {
|
|
6900
7138
|
show: xShow,
|
|
@@ -6937,7 +7175,18 @@ function buildCartesianModel(spec, tokens, opts) {
|
|
|
6937
7175
|
};
|
|
6938
7176
|
}) : void 0;
|
|
6939
7177
|
const plot = frame.plot;
|
|
6940
|
-
const
|
|
7178
|
+
const scatterInset = spec.type === "scatter" ? Math.min(
|
|
7179
|
+
maxScatterRadius(spec, plot.width, plot.height) + SCATTER_EDGE_MARGIN,
|
|
7180
|
+
Math.min(plot.width, plot.height) * 0.4
|
|
7181
|
+
) : 0;
|
|
7182
|
+
const annIns = horizontal ? ZERO_INSETS : annotationEdgeInsets(spec, yDomain, tokens.font.family, tokens.font.size.small, tokens.font.weight.medium);
|
|
7183
|
+
const capV = plot.height * 0.4;
|
|
7184
|
+
const capH = plot.width * 0.4;
|
|
7185
|
+
const insTop = Math.min(Math.max(scatterInset, annIns.top), capV);
|
|
7186
|
+
const insBottom = Math.min(Math.max(scatterInset, annIns.bottom), capV);
|
|
7187
|
+
const insLeft = Math.min(Math.max(scatterInset, annIns.left), capH);
|
|
7188
|
+
const insRight = Math.min(Math.max(scatterInset, annIns.right), capH);
|
|
7189
|
+
const valueRange = horizontal ? [plot.x + insLeft, plot.x + plot.width - insRight] : [plot.y + plot.height - insBottom, plot.y + insTop];
|
|
6941
7190
|
const yScale = yIsLog ? logScale({ domain: yDomain, range: valueRange, base: yBase, clamp: yClamp }) : linearScale({ domain: yDomain, range: valueRange, clamp: yClamp });
|
|
6942
7191
|
const yPixel = (value) => yScale.map(toNumber(value));
|
|
6943
7192
|
const valueMin = horizontal ? plot.x : plot.y;
|
|
@@ -6948,7 +7197,7 @@ function buildCartesianModel(spec, tokens, opts) {
|
|
|
6948
7197
|
type: xType,
|
|
6949
7198
|
categories,
|
|
6950
7199
|
domainNum: xDomainNum,
|
|
6951
|
-
range: horizontal ? [plot.y, plot.y + plot.height] : [plot.x, plot.x + plot.width],
|
|
7200
|
+
range: horizontal ? [plot.y + insTop, plot.y + plot.height - insBottom] : [plot.x + insLeft, plot.x + plot.width - insRight],
|
|
6952
7201
|
paddingInner: spec.type === "bar" ? 0.2 : spec.type === "box" ? 0.32 : 0.5,
|
|
6953
7202
|
scaleCfg: enc.x.scale
|
|
6954
7203
|
});
|
|
@@ -7075,21 +7324,32 @@ function maxFinite(values, fallback) {
|
|
|
7075
7324
|
return max === -Infinity ? fallback : max;
|
|
7076
7325
|
}
|
|
7077
7326
|
|
|
7078
|
-
// src/
|
|
7079
|
-
function
|
|
7080
|
-
|
|
7081
|
-
return emphasis.match(row) ? 1 : emphasis.dim;
|
|
7327
|
+
// src/render/metrics.ts
|
|
7328
|
+
function diagonal(plot) {
|
|
7329
|
+
return Math.hypot(plot.width, plot.height);
|
|
7082
7330
|
}
|
|
7083
|
-
function
|
|
7084
|
-
|
|
7085
|
-
return series.rows.some((row) => emphasis.match(row));
|
|
7331
|
+
function lineStrokeWidth(plot, base = 2) {
|
|
7332
|
+
return clamp(diagonal(plot) / 260, base, 4.5);
|
|
7086
7333
|
}
|
|
7087
|
-
function
|
|
7088
|
-
|
|
7089
|
-
|
|
7334
|
+
function markerRadius(plot, count, base = 3) {
|
|
7335
|
+
const bySize = diagonal(plot) / 150;
|
|
7336
|
+
const spacing = plot.width / Math.max(2, count);
|
|
7337
|
+
const bySpace = spacing * 0.34;
|
|
7338
|
+
return clamp(Math.min(bySize, bySpace), Math.min(base, 2.5), 8);
|
|
7339
|
+
}
|
|
7340
|
+
function markerStroke(radius) {
|
|
7341
|
+
return clamp(radius * 0.32, 1, 2.5);
|
|
7090
7342
|
}
|
|
7091
7343
|
|
|
7092
7344
|
// src/charts/line.ts
|
|
7345
|
+
function maxSeriesLength(allSeries) {
|
|
7346
|
+
let n = 0;
|
|
7347
|
+
for (const s of allSeries) {
|
|
7348
|
+
const finite4 = s.points.filter((p) => !Number.isNaN(p.x) && !Number.isNaN(p.y)).length;
|
|
7349
|
+
if (finite4 > n) n = finite4;
|
|
7350
|
+
}
|
|
7351
|
+
return n;
|
|
7352
|
+
}
|
|
7093
7353
|
function buildSeriesPoints(model, s) {
|
|
7094
7354
|
const readX = accessor(model.x.field);
|
|
7095
7355
|
const readY = accessor(model.y.field ?? "");
|
|
@@ -7114,6 +7374,24 @@ function buildSeriesPoints(model, s) {
|
|
|
7114
7374
|
gap: () => ({ x: NaN, y: NaN })
|
|
7115
7375
|
});
|
|
7116
7376
|
}
|
|
7377
|
+
function matchPointPixels(model, s) {
|
|
7378
|
+
const emphasis = model.emphasis;
|
|
7379
|
+
if (!emphasis) return [];
|
|
7380
|
+
const readX = accessor(model.x.field);
|
|
7381
|
+
const readY = accessor(model.y.field ?? "");
|
|
7382
|
+
const out = [];
|
|
7383
|
+
for (const d of s.rows) {
|
|
7384
|
+
if (!emphasis.match(d)) continue;
|
|
7385
|
+
const px = model.x.pixel(readX(d));
|
|
7386
|
+
if (px == null) continue;
|
|
7387
|
+
const yRaw = readY(d);
|
|
7388
|
+
if (yRaw == null || yRaw === "") continue;
|
|
7389
|
+
const py = model.y.pixel(yRaw);
|
|
7390
|
+
if (Number.isNaN(py)) continue;
|
|
7391
|
+
out.push({ x: px, y: py });
|
|
7392
|
+
}
|
|
7393
|
+
return out;
|
|
7394
|
+
}
|
|
7117
7395
|
function finiteRuns(points) {
|
|
7118
7396
|
const runs = [];
|
|
7119
7397
|
let run = [];
|
|
@@ -7128,7 +7406,7 @@ function finiteRuns(points) {
|
|
|
7128
7406
|
if (run.length) runs.push(run);
|
|
7129
7407
|
return runs;
|
|
7130
7408
|
}
|
|
7131
|
-
function drawLineSketch(ctx, model, allSeries, pen, filled, showPoints) {
|
|
7409
|
+
function drawLineSketch(ctx, model, allSeries, pen, filled, showPoints, strokeWidth, markerR, markerSW) {
|
|
7132
7410
|
if (filled) {
|
|
7133
7411
|
for (const { series, points, alpha } of allSeries) {
|
|
7134
7412
|
const prev = ctx.globalAlpha;
|
|
@@ -7147,7 +7425,7 @@ function drawLineSketch(ctx, model, allSeries, pen, filled, showPoints) {
|
|
|
7147
7425
|
for (const { series, points, alpha } of allSeries) {
|
|
7148
7426
|
const prev = ctx.globalAlpha;
|
|
7149
7427
|
if (alpha !== 1) ctx.globalAlpha = alpha;
|
|
7150
|
-
pen.trendStroke(points, { stroke: series.color });
|
|
7428
|
+
pen.trendStroke(points, { stroke: series.color, strokeWidth });
|
|
7151
7429
|
if (alpha !== 1) ctx.globalAlpha = prev;
|
|
7152
7430
|
}
|
|
7153
7431
|
if (showPoints) {
|
|
@@ -7156,15 +7434,27 @@ function drawLineSketch(ctx, model, allSeries, pen, filled, showPoints) {
|
|
|
7156
7434
|
if (alpha !== 1) ctx.globalAlpha = alpha;
|
|
7157
7435
|
for (const p of points) {
|
|
7158
7436
|
if (Number.isNaN(p.x) || Number.isNaN(p.y)) continue;
|
|
7159
|
-
pen.circle(p.x, p.y,
|
|
7437
|
+
pen.circle(p.x, p.y, markerR, {
|
|
7160
7438
|
fill: series.color,
|
|
7161
7439
|
stroke: model.tokens.color.background,
|
|
7162
|
-
strokeWidth:
|
|
7440
|
+
strokeWidth: markerSW
|
|
7163
7441
|
});
|
|
7164
7442
|
}
|
|
7165
7443
|
if (alpha !== 1) ctx.globalAlpha = prev;
|
|
7166
7444
|
}
|
|
7167
7445
|
}
|
|
7446
|
+
for (const { series, mode, matchPoints } of allSeries) {
|
|
7447
|
+
if (mode !== "partial") continue;
|
|
7448
|
+
ctx.globalAlpha = 1;
|
|
7449
|
+
for (const p of matchPoints) {
|
|
7450
|
+
pen.circle(p.x, p.y, markerR, {
|
|
7451
|
+
fill: series.color,
|
|
7452
|
+
stroke: model.tokens.color.background,
|
|
7453
|
+
strokeWidth: markerSW
|
|
7454
|
+
});
|
|
7455
|
+
}
|
|
7456
|
+
}
|
|
7457
|
+
ctx.globalAlpha = 1;
|
|
7168
7458
|
}
|
|
7169
7459
|
function drawLine(surface, model) {
|
|
7170
7460
|
const ctx = surface.marks.ctx;
|
|
@@ -7173,18 +7463,26 @@ function drawLine(surface, model) {
|
|
|
7173
7463
|
const curve = resolveCurve(spec.curve);
|
|
7174
7464
|
const lineGen = line({ curve });
|
|
7175
7465
|
const areaGen = spec.area ? area({ curve }) : null;
|
|
7176
|
-
const allSeries = model.series.map((s) =>
|
|
7177
|
-
|
|
7178
|
-
|
|
7179
|
-
|
|
7180
|
-
|
|
7466
|
+
const allSeries = model.series.map((s) => {
|
|
7467
|
+
const mode = seriesEmphasisMode(model.emphasis, s);
|
|
7468
|
+
return {
|
|
7469
|
+
series: s,
|
|
7470
|
+
points: buildSeriesPoints(model, s),
|
|
7471
|
+
alpha: mode === "all" ? 1 : model.emphasis?.dim ?? 1,
|
|
7472
|
+
mode,
|
|
7473
|
+
matchPoints: matchPointPixels(model, s)
|
|
7474
|
+
};
|
|
7475
|
+
});
|
|
7181
7476
|
const showPoints = spec.points || allSeries.some((s) => s.points.filter((p) => !Number.isNaN(p.x)).length === 1);
|
|
7477
|
+
const strokeWidth = lineStrokeWidth(plot, Math.max(1.75, tokens.stroke.thick));
|
|
7478
|
+
const markerR = markerRadius(plot, maxSeriesLength(allSeries));
|
|
7479
|
+
const markerSW = markerStroke(markerR);
|
|
7182
7480
|
ctx.save();
|
|
7183
7481
|
ctx.beginPath();
|
|
7184
7482
|
ctx.rect(plot.x, plot.y, plot.width, plot.height);
|
|
7185
7483
|
ctx.clip();
|
|
7186
7484
|
if (model.sketch) {
|
|
7187
|
-
drawLineSketch(ctx, model, allSeries, new RoughPen(ctx, model.sketch), Boolean(areaGen), showPoints);
|
|
7485
|
+
drawLineSketch(ctx, model, allSeries, new RoughPen(ctx, model.sketch), Boolean(areaGen), showPoints, strokeWidth, markerR, markerSW);
|
|
7188
7486
|
ctx.restore();
|
|
7189
7487
|
return;
|
|
7190
7488
|
}
|
|
@@ -7207,7 +7505,7 @@ function drawLine(surface, model) {
|
|
|
7207
7505
|
}
|
|
7208
7506
|
ctx.lineJoin = "round";
|
|
7209
7507
|
ctx.lineCap = "round";
|
|
7210
|
-
ctx.lineWidth =
|
|
7508
|
+
ctx.lineWidth = strokeWidth;
|
|
7211
7509
|
for (const { series, points, alpha } of allSeries) {
|
|
7212
7510
|
ctx.beginPath();
|
|
7213
7511
|
lineGen(points, ctx);
|
|
@@ -7220,28 +7518,52 @@ function drawLine(surface, model) {
|
|
|
7220
7518
|
for (const { series, points, alpha } of allSeries) {
|
|
7221
7519
|
ctx.fillStyle = series.color;
|
|
7222
7520
|
ctx.strokeStyle = tokens.color.background;
|
|
7223
|
-
ctx.lineWidth =
|
|
7521
|
+
ctx.lineWidth = markerSW;
|
|
7224
7522
|
ctx.globalAlpha = alpha;
|
|
7225
7523
|
for (const p of points) {
|
|
7226
7524
|
if (Number.isNaN(p.x) || Number.isNaN(p.y)) continue;
|
|
7227
7525
|
ctx.beginPath();
|
|
7228
|
-
ctx.arc(p.x, p.y,
|
|
7526
|
+
ctx.arc(p.x, p.y, markerR, 0, Math.PI * 2);
|
|
7229
7527
|
ctx.fill();
|
|
7230
7528
|
ctx.stroke();
|
|
7231
7529
|
}
|
|
7232
7530
|
ctx.globalAlpha = 1;
|
|
7233
7531
|
}
|
|
7234
7532
|
}
|
|
7533
|
+
for (const { series, mode, matchPoints } of allSeries) {
|
|
7534
|
+
if (mode !== "partial") continue;
|
|
7535
|
+
ctx.fillStyle = series.color;
|
|
7536
|
+
ctx.strokeStyle = tokens.color.background;
|
|
7537
|
+
ctx.lineWidth = markerSW;
|
|
7538
|
+
ctx.globalAlpha = 1;
|
|
7539
|
+
for (const p of matchPoints) {
|
|
7540
|
+
ctx.beginPath();
|
|
7541
|
+
ctx.arc(p.x, p.y, markerR, 0, Math.PI * 2);
|
|
7542
|
+
ctx.fill();
|
|
7543
|
+
ctx.stroke();
|
|
7544
|
+
}
|
|
7545
|
+
}
|
|
7546
|
+
ctx.globalAlpha = 1;
|
|
7235
7547
|
ctx.restore();
|
|
7236
7548
|
}
|
|
7237
7549
|
|
|
7238
7550
|
// src/charts/area.ts
|
|
7551
|
+
function maxBandLength(bands) {
|
|
7552
|
+
let n = 0;
|
|
7553
|
+
for (const b of bands) {
|
|
7554
|
+
const finite4 = b.points.filter((p) => !Number.isNaN(p.x) && !Number.isNaN(p.y1)).length;
|
|
7555
|
+
if (finite4 > n) n = finite4;
|
|
7556
|
+
}
|
|
7557
|
+
return n;
|
|
7558
|
+
}
|
|
7239
7559
|
function buildBands(model, stacked) {
|
|
7240
7560
|
const readX = accessor(model.x.field);
|
|
7241
7561
|
const readY = accessor(model.y.field ?? "");
|
|
7242
7562
|
const cumulative = /* @__PURE__ */ new Map();
|
|
7243
7563
|
const threshold = stacked ? Infinity : Math.max(2, Math.round(model.plot.width));
|
|
7244
7564
|
return model.series.map((series) => {
|
|
7565
|
+
const mode = seriesEmphasisMode(model.emphasis, series);
|
|
7566
|
+
const matchPoints = [];
|
|
7245
7567
|
const points = [];
|
|
7246
7568
|
for (const row of series.rows) {
|
|
7247
7569
|
const xv = readX(row);
|
|
@@ -7254,19 +7576,23 @@ function buildBands(model, stacked) {
|
|
|
7254
7576
|
const base = cumulative.get(key) ?? 0;
|
|
7255
7577
|
const top = base + (missing ? 0 : value);
|
|
7256
7578
|
cumulative.set(key, top);
|
|
7579
|
+
const y1 = missing ? NaN : model.y.pixel(top);
|
|
7257
7580
|
points.push({
|
|
7258
7581
|
x: px == null ? NaN : px,
|
|
7259
7582
|
y0: model.y.pixel(base),
|
|
7260
|
-
y1
|
|
7583
|
+
y1,
|
|
7261
7584
|
sortKey: px == null ? Number.POSITIVE_INFINITY : px
|
|
7262
7585
|
});
|
|
7586
|
+
if (model.emphasis?.match(row) && px != null && !Number.isNaN(y1)) matchPoints.push({ x: px, y: y1 });
|
|
7263
7587
|
} else {
|
|
7588
|
+
const y1 = missing ? NaN : model.y.pixel(value);
|
|
7264
7589
|
points.push({
|
|
7265
7590
|
x: px == null ? NaN : px,
|
|
7266
7591
|
y0: model.y.baseline,
|
|
7267
|
-
y1
|
|
7592
|
+
y1,
|
|
7268
7593
|
sortKey: px == null ? Number.POSITIVE_INFINITY : px
|
|
7269
7594
|
});
|
|
7595
|
+
if (model.emphasis?.match(row) && px != null && !Number.isNaN(y1)) matchPoints.push({ x: px, y: y1 });
|
|
7270
7596
|
}
|
|
7271
7597
|
}
|
|
7272
7598
|
points.sort((a, b) => a.sortKey - b.sortKey);
|
|
@@ -7276,10 +7602,10 @@ function buildBands(model, stacked) {
|
|
|
7276
7602
|
getY: (p) => p.y1,
|
|
7277
7603
|
gap: () => ({ x: NaN, y0: NaN, y1: NaN })
|
|
7278
7604
|
}) : band;
|
|
7279
|
-
return { series, points: reduced, alpha:
|
|
7605
|
+
return { series, points: reduced, alpha: mode === "all" ? 1 : model.emphasis?.dim ?? 1, mode, matchPoints };
|
|
7280
7606
|
});
|
|
7281
7607
|
}
|
|
7282
|
-
function drawAreaSketch(ctx, model, bands, pen, stacked) {
|
|
7608
|
+
function drawAreaSketch(ctx, model, bands, pen, stacked, strokeWidth, markerR, markerSW) {
|
|
7283
7609
|
for (const { series, points, alpha } of bands) {
|
|
7284
7610
|
const prev = ctx.globalAlpha;
|
|
7285
7611
|
if (alpha !== 1) ctx.globalAlpha = alpha;
|
|
@@ -7307,10 +7633,22 @@ function drawAreaSketch(ctx, model, bands, pen, stacked) {
|
|
|
7307
7633
|
if (alpha !== 1) ctx.globalAlpha = alpha;
|
|
7308
7634
|
pen.trendStroke(
|
|
7309
7635
|
points.map((p) => ({ x: p.x, y: p.y1 })),
|
|
7310
|
-
{ stroke: series.color }
|
|
7636
|
+
{ stroke: series.color, strokeWidth }
|
|
7311
7637
|
);
|
|
7312
7638
|
if (alpha !== 1) ctx.globalAlpha = prev;
|
|
7313
7639
|
}
|
|
7640
|
+
for (const { series, mode, matchPoints } of bands) {
|
|
7641
|
+
if (mode !== "partial") continue;
|
|
7642
|
+
ctx.globalAlpha = 1;
|
|
7643
|
+
for (const p of matchPoints) {
|
|
7644
|
+
pen.circle(p.x, p.y, markerR, {
|
|
7645
|
+
fill: series.color,
|
|
7646
|
+
stroke: model.tokens.color.background,
|
|
7647
|
+
strokeWidth: markerSW
|
|
7648
|
+
});
|
|
7649
|
+
}
|
|
7650
|
+
}
|
|
7651
|
+
ctx.globalAlpha = 1;
|
|
7314
7652
|
}
|
|
7315
7653
|
function drawArea(surface, model) {
|
|
7316
7654
|
const ctx = surface.marks.ctx;
|
|
@@ -7321,12 +7659,15 @@ function drawArea(surface, model) {
|
|
|
7321
7659
|
const lineGen = line({ curve });
|
|
7322
7660
|
const stacked = model.stacked;
|
|
7323
7661
|
const bands = buildBands(model, stacked);
|
|
7662
|
+
const strokeWidth = lineStrokeWidth(plot, Math.max(1.5, tokens.stroke.base));
|
|
7663
|
+
const markerR = markerRadius(plot, maxBandLength(bands));
|
|
7664
|
+
const markerSW = markerStroke(markerR);
|
|
7324
7665
|
ctx.save();
|
|
7325
7666
|
ctx.beginPath();
|
|
7326
7667
|
ctx.rect(plot.x, plot.y, plot.width, plot.height);
|
|
7327
7668
|
ctx.clip();
|
|
7328
7669
|
if (model.sketch) {
|
|
7329
|
-
drawAreaSketch(ctx, model, bands, new RoughPen(ctx, model.sketch), stacked);
|
|
7670
|
+
drawAreaSketch(ctx, model, bands, new RoughPen(ctx, model.sketch), stacked, strokeWidth, markerR, markerSW);
|
|
7330
7671
|
ctx.restore();
|
|
7331
7672
|
return;
|
|
7332
7673
|
}
|
|
@@ -7348,7 +7689,7 @@ function drawArea(surface, model) {
|
|
|
7348
7689
|
}
|
|
7349
7690
|
ctx.lineJoin = "round";
|
|
7350
7691
|
ctx.lineCap = "round";
|
|
7351
|
-
ctx.lineWidth =
|
|
7692
|
+
ctx.lineWidth = strokeWidth;
|
|
7352
7693
|
for (const { series, points, alpha } of bands) {
|
|
7353
7694
|
ctx.beginPath();
|
|
7354
7695
|
lineGen(
|
|
@@ -7360,6 +7701,20 @@ function drawArea(surface, model) {
|
|
|
7360
7701
|
ctx.stroke();
|
|
7361
7702
|
ctx.globalAlpha = 1;
|
|
7362
7703
|
}
|
|
7704
|
+
for (const { series, mode, matchPoints } of bands) {
|
|
7705
|
+
if (mode !== "partial") continue;
|
|
7706
|
+
ctx.fillStyle = series.color;
|
|
7707
|
+
ctx.strokeStyle = tokens.color.background;
|
|
7708
|
+
ctx.lineWidth = markerSW;
|
|
7709
|
+
ctx.globalAlpha = 1;
|
|
7710
|
+
for (const p of matchPoints) {
|
|
7711
|
+
ctx.beginPath();
|
|
7712
|
+
ctx.arc(p.x, p.y, markerR, 0, Math.PI * 2);
|
|
7713
|
+
ctx.fill();
|
|
7714
|
+
ctx.stroke();
|
|
7715
|
+
}
|
|
7716
|
+
}
|
|
7717
|
+
ctx.globalAlpha = 1;
|
|
7363
7718
|
ctx.restore();
|
|
7364
7719
|
}
|
|
7365
7720
|
|
|
@@ -7427,6 +7782,8 @@ function drawGroupedBars(ctx, model, radius, pen) {
|
|
|
7427
7782
|
const subWidth = seriesCount > 1 ? bandWidth / seriesCount : bandWidth;
|
|
7428
7783
|
const gap = seriesCount > 1 && subWidth > 3 ? Math.min(1, subWidth * 0.15) : 0;
|
|
7429
7784
|
const barThick = Math.max(0, subWidth - gap);
|
|
7785
|
+
const t = model.entranceProgress ?? 1;
|
|
7786
|
+
const grow = (pixel) => model.y.baseline + (pixel - model.y.baseline) * t;
|
|
7430
7787
|
model.series.forEach((series, seriesIndex) => {
|
|
7431
7788
|
ctx.fillStyle = series.color;
|
|
7432
7789
|
for (const row of series.rows) {
|
|
@@ -7436,7 +7793,7 @@ function drawGroupedBars(ctx, model, radius, pen) {
|
|
|
7436
7793
|
const center2 = model.x.pixel(readX(row));
|
|
7437
7794
|
if (center2 == null) continue;
|
|
7438
7795
|
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);
|
|
7796
|
+
const rect = orientedRect(horizontal, catStart, barThick, grow(model.y.baseline), grow(model.y.pixel(value)), series.color);
|
|
7440
7797
|
if (rect) drawRect(ctx, rect, radius, pen, rowAlpha(model.emphasis, row));
|
|
7441
7798
|
}
|
|
7442
7799
|
});
|
|
@@ -7450,6 +7807,8 @@ function drawStackedBars(ctx, model, radius, pen) {
|
|
|
7450
7807
|
const segments = [];
|
|
7451
7808
|
const roundedEnds = /* @__PURE__ */ new Map();
|
|
7452
7809
|
const bandWidth = model.x.bandwidth;
|
|
7810
|
+
const t = model.entranceProgress ?? 1;
|
|
7811
|
+
const grow = (pixel) => model.y.baseline + (pixel - model.y.baseline) * t;
|
|
7453
7812
|
for (const series of model.series) {
|
|
7454
7813
|
for (const row of series.rows) {
|
|
7455
7814
|
const xValue = readX(row);
|
|
@@ -7464,7 +7823,7 @@ function drawStackedBars(ctx, model, radius, pen) {
|
|
|
7464
7823
|
const base = totals.get(categoryKey2) ?? 0;
|
|
7465
7824
|
const top = base + value;
|
|
7466
7825
|
totals.set(categoryKey2, top);
|
|
7467
|
-
const rect = orientedRect(horizontal, center2 - bandWidth / 2, bandWidth, model.y.pixel(base), model.y.pixel(top), series.color);
|
|
7826
|
+
const rect = orientedRect(horizontal, center2 - bandWidth / 2, bandWidth, grow(model.y.pixel(base)), grow(model.y.pixel(top)), series.color);
|
|
7468
7827
|
if (!rect) continue;
|
|
7469
7828
|
segments.push({ ...rect, stackEndKey, alpha: rowAlpha(model.emphasis, row) });
|
|
7470
7829
|
roundedEnds.set(stackEndKey, segments.length - 1);
|
|
@@ -7494,133 +7853,6 @@ function drawBar(surface, model) {
|
|
|
7494
7853
|
ctx.restore();
|
|
7495
7854
|
}
|
|
7496
7855
|
|
|
7497
|
-
// src/charts/scatter.ts
|
|
7498
|
-
var QUANTIZE_MIN_POINTS = 4e3;
|
|
7499
|
-
var QUANTIZE_AREA_DIVISOR = 4;
|
|
7500
|
-
function isMissing(value) {
|
|
7501
|
-
return value == null || value === "";
|
|
7502
|
-
}
|
|
7503
|
-
function isInvalidPositionValue(value) {
|
|
7504
|
-
return isMissing(value) || typeof value === "number" && Number.isNaN(value) || value instanceof Date && Number.isNaN(value.getTime());
|
|
7505
|
-
}
|
|
7506
|
-
function fixedRadius() {
|
|
7507
|
-
return 3.75;
|
|
7508
|
-
}
|
|
7509
|
-
function shouldQuantize(count, plot, hasSize) {
|
|
7510
|
-
return !hasSize && count > QUANTIZE_MIN_POINTS && count > plot.width * plot.height / QUANTIZE_AREA_DIVISOR;
|
|
7511
|
-
}
|
|
7512
|
-
function quantizePoints(points) {
|
|
7513
|
-
if (points.length === 0) return [];
|
|
7514
|
-
let minX = Infinity;
|
|
7515
|
-
let maxX = -Infinity;
|
|
7516
|
-
let minY = Infinity;
|
|
7517
|
-
let maxY = -Infinity;
|
|
7518
|
-
for (const p of points) {
|
|
7519
|
-
if (p.x < minX) minX = p.x;
|
|
7520
|
-
if (p.x > maxX) maxX = p.x;
|
|
7521
|
-
if (p.y < minY) minY = p.y;
|
|
7522
|
-
if (p.y > maxY) maxY = p.y;
|
|
7523
|
-
}
|
|
7524
|
-
const seen = /* @__PURE__ */ new Set();
|
|
7525
|
-
const keep = [];
|
|
7526
|
-
for (const p of points) {
|
|
7527
|
-
const extentPoint = p.x === minX || p.x === maxX || p.y === minY || p.y === maxY;
|
|
7528
|
-
const ix = Math.round(p.x);
|
|
7529
|
-
const iy = Math.round(p.y);
|
|
7530
|
-
const key = `${p.seriesKey}|${ix}|${iy}`;
|
|
7531
|
-
if (extentPoint || !seen.has(key)) {
|
|
7532
|
-
keep.push(p);
|
|
7533
|
-
seen.add(key);
|
|
7534
|
-
}
|
|
7535
|
-
}
|
|
7536
|
-
return keep;
|
|
7537
|
-
}
|
|
7538
|
-
function radiusScale(rows, field, plotSize) {
|
|
7539
|
-
const rMin = 3;
|
|
7540
|
-
const rMax = clamp(plotSize * 0.05, 10, 26);
|
|
7541
|
-
const mid = (rMin + rMax) / 2;
|
|
7542
|
-
const domain = extent(rows, field);
|
|
7543
|
-
if (!domain) return () => mid;
|
|
7544
|
-
const [min, max] = domain;
|
|
7545
|
-
if (min === max) return () => mid;
|
|
7546
|
-
return (value) => {
|
|
7547
|
-
const n = toNumber(value);
|
|
7548
|
-
if (Number.isNaN(n)) return rMin;
|
|
7549
|
-
const norm = clamp((n - min) / (max - min), 0, 1);
|
|
7550
|
-
return rMin + (rMax - rMin) * Math.sqrt(norm);
|
|
7551
|
-
};
|
|
7552
|
-
}
|
|
7553
|
-
function drawScatter(surface, model) {
|
|
7554
|
-
const ctx = surface.marks.ctx;
|
|
7555
|
-
const spec = model.spec;
|
|
7556
|
-
const { plot, tokens } = model;
|
|
7557
|
-
const readX = accessor(model.x.field);
|
|
7558
|
-
const readY = accessor(model.y.field ?? "");
|
|
7559
|
-
const sizeField = spec.encoding.size?.field;
|
|
7560
|
-
const allRows = model.series.flatMap((series) => series.rows);
|
|
7561
|
-
const readSize = sizeField ? accessor(sizeField) : null;
|
|
7562
|
-
const radius = sizeField ? radiusScale(allRows, sizeField, Math.min(plot.width, plot.height)) : fixedRadius;
|
|
7563
|
-
const points = [];
|
|
7564
|
-
for (const series of model.series) {
|
|
7565
|
-
for (const row of series.rows) {
|
|
7566
|
-
const xValue = readX(row);
|
|
7567
|
-
const yValue = readY(row);
|
|
7568
|
-
const yNumber = toNumber(yValue);
|
|
7569
|
-
const px = model.x.pixel(xValue);
|
|
7570
|
-
if (px == null || isInvalidPositionValue(xValue) || isMissing(yValue) || Number.isNaN(yNumber)) {
|
|
7571
|
-
continue;
|
|
7572
|
-
}
|
|
7573
|
-
const py = model.y.pixel(yValue);
|
|
7574
|
-
if (!Number.isFinite(px) || !Number.isFinite(py)) continue;
|
|
7575
|
-
points.push({
|
|
7576
|
-
x: px,
|
|
7577
|
-
y: py,
|
|
7578
|
-
r: readSize ? radius(readSize(row)) : radius(),
|
|
7579
|
-
color: series.color,
|
|
7580
|
-
alpha: rowAlpha(model.emphasis, row),
|
|
7581
|
-
seriesKey: series.key
|
|
7582
|
-
});
|
|
7583
|
-
}
|
|
7584
|
-
}
|
|
7585
|
-
const originalCount = points.length;
|
|
7586
|
-
const drawPoints = shouldQuantize(originalCount, plot, !!sizeField) ? quantizePoints(points) : points;
|
|
7587
|
-
model.markStats = { drawn: drawPoints.length, original: originalCount };
|
|
7588
|
-
if (sizeField) drawPoints.sort((a, b) => b.r - a.r);
|
|
7589
|
-
ctx.save();
|
|
7590
|
-
ctx.beginPath();
|
|
7591
|
-
ctx.rect(plot.x, plot.y, plot.width, plot.height);
|
|
7592
|
-
ctx.clip();
|
|
7593
|
-
if (model.sketch) {
|
|
7594
|
-
const pen = new RoughPen(ctx, model.sketch);
|
|
7595
|
-
for (const point of drawPoints) {
|
|
7596
|
-
const prev = ctx.globalAlpha;
|
|
7597
|
-
if (point.alpha !== 1) ctx.globalAlpha = point.alpha;
|
|
7598
|
-
pen.circle(point.x, point.y, point.r, {
|
|
7599
|
-
fill: point.color,
|
|
7600
|
-
fillAlpha: 0.7,
|
|
7601
|
-
stroke: tokens.color.background,
|
|
7602
|
-
strokeWidth: 1
|
|
7603
|
-
});
|
|
7604
|
-
if (point.alpha !== 1) ctx.globalAlpha = prev;
|
|
7605
|
-
}
|
|
7606
|
-
ctx.restore();
|
|
7607
|
-
return;
|
|
7608
|
-
}
|
|
7609
|
-
ctx.lineWidth = 1;
|
|
7610
|
-
ctx.strokeStyle = tokens.color.background;
|
|
7611
|
-
for (const point of drawPoints) {
|
|
7612
|
-
ctx.beginPath();
|
|
7613
|
-
ctx.arc(point.x, point.y, point.r, 0, Math.PI * 2);
|
|
7614
|
-
ctx.fillStyle = point.color;
|
|
7615
|
-
ctx.globalAlpha = 0.7 * point.alpha;
|
|
7616
|
-
ctx.fill();
|
|
7617
|
-
ctx.globalAlpha = point.alpha;
|
|
7618
|
-
ctx.stroke();
|
|
7619
|
-
}
|
|
7620
|
-
ctx.globalAlpha = 1;
|
|
7621
|
-
ctx.restore();
|
|
7622
|
-
}
|
|
7623
|
-
|
|
7624
7856
|
// src/charts/box.ts
|
|
7625
7857
|
function quantileSorted(sorted, p) {
|
|
7626
7858
|
const n = sorted.length;
|
|
@@ -7865,7 +8097,7 @@ function buildBoxInteraction(model) {
|
|
|
7865
8097
|
}
|
|
7866
8098
|
|
|
7867
8099
|
// src/charts/chrome.ts
|
|
7868
|
-
var CHROME_PAD = { top:
|
|
8100
|
+
var CHROME_PAD = { top: 0, right: 0, bottom: 0, left: 0 };
|
|
7869
8101
|
function resolveTitle2(title2) {
|
|
7870
8102
|
if (typeof title2 === "string") return { text: title2, align: "left" };
|
|
7871
8103
|
if (title2 && typeof title2 === "object") {
|
|
@@ -8597,6 +8829,8 @@ function drawPie(surface, spec, tokens, size, context) {
|
|
|
8597
8829
|
const colorField = pie.encoding.color.field;
|
|
8598
8830
|
const emphasis = context?.emphasis ?? null;
|
|
8599
8831
|
const sliceAlpha = (slice) => emphasis ? emphasis.match({ [colorField]: slice.category }) ? 1 : emphasis.dim : 1;
|
|
8832
|
+
const sweepT = context?.sweepProgress ?? 1;
|
|
8833
|
+
const sweepEnd = TAU3 * sweepT;
|
|
8600
8834
|
const content = drawTitleBlock(surface, tokens, size, pie.title);
|
|
8601
8835
|
const slices = buildSlices(pie, tokens.color.palette);
|
|
8602
8836
|
const legendPosition = resolveLegendPosition(pie, content.width, content.height);
|
|
@@ -8654,9 +8888,11 @@ function drawPie(surface, spec, tokens, size, context) {
|
|
|
8654
8888
|
if (sketch) {
|
|
8655
8889
|
const pen = new RoughPen(ctx, sketch);
|
|
8656
8890
|
for (const slice of slices) {
|
|
8891
|
+
const end = Math.min(slice.endAngle, sweepEnd);
|
|
8892
|
+
if (end <= slice.startAngle) continue;
|
|
8657
8893
|
const alpha = sliceAlpha(slice);
|
|
8658
8894
|
if (alpha !== 1) ctx.globalAlpha = alpha;
|
|
8659
|
-
pen.wedge(cx, cy, innerRadius, outerRadius, slice.startAngle,
|
|
8895
|
+
pen.wedge(cx, cy, innerRadius, outerRadius, slice.startAngle, end, {
|
|
8660
8896
|
fill: slice.color,
|
|
8661
8897
|
stroke: separator
|
|
8662
8898
|
});
|
|
@@ -8665,11 +8901,13 @@ function drawPie(surface, spec, tokens, size, context) {
|
|
|
8665
8901
|
ctx.restore();
|
|
8666
8902
|
} else {
|
|
8667
8903
|
for (const slice of slices) {
|
|
8904
|
+
const end = Math.min(slice.endAngle, sweepEnd);
|
|
8905
|
+
if (end <= slice.startAngle) continue;
|
|
8668
8906
|
const wedge = arc({
|
|
8669
8907
|
innerRadius,
|
|
8670
8908
|
outerRadius,
|
|
8671
8909
|
startAngle: slice.startAngle,
|
|
8672
|
-
endAngle:
|
|
8910
|
+
endAngle: end
|
|
8673
8911
|
});
|
|
8674
8912
|
ctx.beginPath();
|
|
8675
8913
|
wedge(ctx, cx, cy);
|
|
@@ -8681,7 +8919,7 @@ function drawPie(surface, spec, tokens, size, context) {
|
|
|
8681
8919
|
}
|
|
8682
8920
|
ctx.restore();
|
|
8683
8921
|
}
|
|
8684
|
-
if (labelPlan && labelCfg) {
|
|
8922
|
+
if (sweepT >= 1 && labelPlan && labelCfg) {
|
|
8685
8923
|
ctx.save();
|
|
8686
8924
|
ctx.lineWidth = 1;
|
|
8687
8925
|
ctx.lineJoin = "round";
|
|
@@ -8725,7 +8963,7 @@ function drawPie(surface, spec, tokens, size, context) {
|
|
|
8725
8963
|
});
|
|
8726
8964
|
}
|
|
8727
8965
|
}
|
|
8728
|
-
if (innerRadius >= 34) {
|
|
8966
|
+
if (sweepT >= 1 && innerRadius >= 34) {
|
|
8729
8967
|
addOverlayText(surface, tokens, {
|
|
8730
8968
|
left: cx,
|
|
8731
8969
|
top: cy - tokens.font.size.small - 2,
|
|
@@ -9010,19 +9248,9 @@ function drawFunnel(surface, spec, tokens, size, context) {
|
|
|
9010
9248
|
|
|
9011
9249
|
// src/charts/kpi.ts
|
|
9012
9250
|
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
9013
|
-
var SKETCH_BORDER_RADIUS = "255px 15px 225px 15px / 15px 225px 15px 255px";
|
|
9014
9251
|
function clamp5(value, min, max) {
|
|
9015
9252
|
return Math.max(min, Math.min(max, value));
|
|
9016
9253
|
}
|
|
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
9254
|
function valueField(ref) {
|
|
9027
9255
|
return typeof ref === "number" ? null : ref.field;
|
|
9028
9256
|
}
|
|
@@ -9033,6 +9261,12 @@ function formatDelta(delta, specFormat) {
|
|
|
9033
9261
|
if (abs <= 1) return sign2 + formatNumber2(abs, ".1%");
|
|
9034
9262
|
return sign2 + formatNumber2(abs, ",.0f");
|
|
9035
9263
|
}
|
|
9264
|
+
function flexAlign(align) {
|
|
9265
|
+
return align === "start" ? "flex-start" : align === "end" ? "flex-end" : "center";
|
|
9266
|
+
}
|
|
9267
|
+
function textAlignOf(align) {
|
|
9268
|
+
return align === "start" ? "left" : align === "end" ? "right" : "center";
|
|
9269
|
+
}
|
|
9036
9270
|
function cardRectFor(surface, tokens, size, spec) {
|
|
9037
9271
|
if (spec.title) return drawTitleBlock(surface, tokens, size, spec.title);
|
|
9038
9272
|
return {
|
|
@@ -9042,41 +9276,25 @@ function cardRectFor(surface, tokens, size, spec) {
|
|
|
9042
9276
|
height: Math.max(0, size.height - CHROME_PAD.top - CHROME_PAD.bottom)
|
|
9043
9277
|
};
|
|
9044
9278
|
}
|
|
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;
|
|
9279
|
+
function valueFontSize(rect, contentWidth, valueText, unit, isRich) {
|
|
9280
|
+
const effectiveLen = valueText.length + (unit ? unit.length * 0.5 + 0.4 : 0);
|
|
9281
|
+
const sizeByCard = Math.min(rect.width, rect.height) * (isRich ? 0.17 : 0.28);
|
|
9282
|
+
const sizeByText = contentWidth / Math.max(1, effectiveLen * 0.58);
|
|
9283
|
+
const lo = isRich ? 22 : 26;
|
|
9284
|
+
const hi = isRich ? 52 : 76;
|
|
9285
|
+
return clamp5(Math.min(sizeByCard, sizeByText), lo, hi);
|
|
9067
9286
|
}
|
|
9068
|
-
function
|
|
9069
|
-
|
|
9070
|
-
el.textContent = text;
|
|
9071
|
-
Object.assign(el.style, style);
|
|
9072
|
-
card.appendChild(el);
|
|
9073
|
-
return el;
|
|
9287
|
+
function unitFontSize(valueSize) {
|
|
9288
|
+
return Math.max(12, valueSize * 0.46);
|
|
9074
9289
|
}
|
|
9075
9290
|
function sparklineField(spec) {
|
|
9076
9291
|
if (!spec.sparkline) return null;
|
|
9077
|
-
if (typeof spec.sparkline === "object") return spec.sparkline.field;
|
|
9292
|
+
if (typeof spec.sparkline === "object") return spec.sparkline.field ?? valueField(spec.value);
|
|
9078
9293
|
return valueField(spec.value);
|
|
9079
9294
|
}
|
|
9295
|
+
function sparklineMarkers(spec) {
|
|
9296
|
+
return typeof spec.sparkline === "object" ? spec.sparkline.markers !== false : true;
|
|
9297
|
+
}
|
|
9080
9298
|
function sparklineValues(data, field) {
|
|
9081
9299
|
const read = accessor(field);
|
|
9082
9300
|
const values = [];
|
|
@@ -9086,41 +9304,165 @@ function sparklineValues(data, field) {
|
|
|
9086
9304
|
}
|
|
9087
9305
|
return values;
|
|
9088
9306
|
}
|
|
9089
|
-
function
|
|
9307
|
+
function kpiSparklineSeries(spec, data) {
|
|
9308
|
+
const field = sparklineField(spec);
|
|
9309
|
+
return field ? sparklineValues(data, field) : [];
|
|
9310
|
+
}
|
|
9311
|
+
function morphSparkline(to, from, progress) {
|
|
9312
|
+
if (progress == null || progress >= 1 || to.length === 0) return to;
|
|
9313
|
+
const t = progress;
|
|
9314
|
+
if (from && from.length === to.length) return to.map((v, i) => from[i] + (v - from[i]) * t);
|
|
9315
|
+
const base = to[0];
|
|
9316
|
+
return to.map((v) => base + (v - base) * t);
|
|
9317
|
+
}
|
|
9318
|
+
function resolveComparison(cmp, data, parentFormat) {
|
|
9319
|
+
const delta = resolveValueRef(cmp.delta, data);
|
|
9320
|
+
if (delta == null) return null;
|
|
9321
|
+
const rawPositive = delta >= 0;
|
|
9322
|
+
const tri = rawPositive ? "\u25B2" : "\u25BC";
|
|
9323
|
+
let text = `${tri} ${formatValue(delta, cmp.format ?? ".0%")}`;
|
|
9324
|
+
if (cmp.amount !== void 0) {
|
|
9325
|
+
const amount = resolveValueRef(cmp.amount, data);
|
|
9326
|
+
if (amount != null) text += ` : ${formatValue(amount, cmp.amountFormat ?? parentFormat ?? ",.0f")}`;
|
|
9327
|
+
}
|
|
9328
|
+
return { label: cmp.label, text, positive: cmp.invert ? !rawPositive : rawPositive };
|
|
9329
|
+
}
|
|
9330
|
+
function layoutKpi(rect, tokens, framed, f) {
|
|
9331
|
+
const font = tokens.font;
|
|
9332
|
+
const pad3 = framed ? 0 : tokens.spacing.lg;
|
|
9333
|
+
const left = rect.x + pad3;
|
|
9334
|
+
const contentWidth = Math.max(1, rect.width - pad3 * 2);
|
|
9335
|
+
const availTop = rect.y + pad3;
|
|
9336
|
+
const availH = Math.max(0, rect.height - pad3 * 2);
|
|
9337
|
+
const labelSize = Math.round(clamp5(f.valueSize * 0.4, font.size.small, font.size.title));
|
|
9338
|
+
const labelH = f.hasLabel ? Math.round(labelSize * 1.35) : 0;
|
|
9339
|
+
const valueH = Math.round(f.valueSize * 1.06);
|
|
9340
|
+
const deltaSize = font.size.large;
|
|
9341
|
+
const deltaH = f.hasDelta ? Math.round(deltaSize * 1.5) : 0;
|
|
9342
|
+
const compSize = font.size.large;
|
|
9343
|
+
const compRowH = Math.round(compSize * 1.55);
|
|
9344
|
+
const gapLabel = Math.round(labelSize * 0.25);
|
|
9345
|
+
const gapDelta = f.hasDelta ? tokens.spacing.xs : 0;
|
|
9346
|
+
const gapSpark = tokens.spacing.sm;
|
|
9347
|
+
const dividerPad = tokens.spacing.sm;
|
|
9348
|
+
const headerH = (f.hasLabel && f.labelAbove ? labelH + gapLabel : 0) + valueH + (f.hasLabel && !f.labelAbove ? gapLabel + labelH : 0) + (f.hasDelta ? gapDelta + deltaH : 0);
|
|
9349
|
+
const footerH = f.compCount > 0 ? dividerPad + 1 + dividerPad + f.compCount * compRowH : 0;
|
|
9350
|
+
let headerTop;
|
|
9351
|
+
let footerTop;
|
|
9352
|
+
let spark = null;
|
|
9353
|
+
if (f.hasSparkline) {
|
|
9354
|
+
headerTop = availTop;
|
|
9355
|
+
footerTop = availTop + availH - footerH;
|
|
9356
|
+
const sparkTop = headerTop + headerH + gapSpark;
|
|
9357
|
+
const sparkBottom = footerTop - (footerH > 0 ? gapSpark : 0);
|
|
9358
|
+
spark = { x: left, y: sparkTop, width: contentWidth, height: Math.max(24, sparkBottom - sparkTop) };
|
|
9359
|
+
} else {
|
|
9360
|
+
const groupH = headerH + (footerH > 0 ? tokens.spacing.md + footerH : 0);
|
|
9361
|
+
headerTop = availTop + Math.max(0, (availH - groupH) / 2);
|
|
9362
|
+
footerTop = headerTop + headerH + (footerH > 0 ? tokens.spacing.md : 0);
|
|
9363
|
+
}
|
|
9364
|
+
let y = headerTop;
|
|
9365
|
+
let labelY = -1;
|
|
9366
|
+
if (f.hasLabel && f.labelAbove) {
|
|
9367
|
+
labelY = y;
|
|
9368
|
+
y += labelH + gapLabel;
|
|
9369
|
+
}
|
|
9370
|
+
const valueY = y;
|
|
9371
|
+
y += valueH;
|
|
9372
|
+
if (f.hasLabel && !f.labelAbove) {
|
|
9373
|
+
y += gapLabel;
|
|
9374
|
+
labelY = y;
|
|
9375
|
+
y += labelH;
|
|
9376
|
+
}
|
|
9377
|
+
let deltaY = -1;
|
|
9378
|
+
if (f.hasDelta) {
|
|
9379
|
+
y += gapDelta;
|
|
9380
|
+
deltaY = y;
|
|
9381
|
+
}
|
|
9382
|
+
let dividerY = -1;
|
|
9383
|
+
let compStartY = -1;
|
|
9384
|
+
if (f.compCount > 0) {
|
|
9385
|
+
dividerY = footerTop + dividerPad;
|
|
9386
|
+
compStartY = dividerY + 1 + dividerPad;
|
|
9387
|
+
}
|
|
9388
|
+
return {
|
|
9389
|
+
left,
|
|
9390
|
+
contentWidth,
|
|
9391
|
+
labelSize,
|
|
9392
|
+
labelY,
|
|
9393
|
+
valueY,
|
|
9394
|
+
valueH,
|
|
9395
|
+
deltaSize,
|
|
9396
|
+
deltaY,
|
|
9397
|
+
spark,
|
|
9398
|
+
compSize,
|
|
9399
|
+
compRowH,
|
|
9400
|
+
dividerY,
|
|
9401
|
+
compStartY
|
|
9402
|
+
};
|
|
9403
|
+
}
|
|
9404
|
+
function sparkMarkers(values, xAt, yAt, r, tokens) {
|
|
9405
|
+
const n = values.length;
|
|
9406
|
+
if (n < 2) return [];
|
|
9407
|
+
let minIdx = 0;
|
|
9408
|
+
let maxIdx = 0;
|
|
9409
|
+
for (let i = 1; i < n; i++) {
|
|
9410
|
+
if (values[i] < values[minIdx]) minIdx = i;
|
|
9411
|
+
if (values[i] > values[maxIdx]) maxIdx = i;
|
|
9412
|
+
}
|
|
9413
|
+
const lastIdx = n - 1;
|
|
9414
|
+
const out = [];
|
|
9415
|
+
if (maxIdx !== lastIdx) out.push({ x: xAt(maxIdx), y: yAt(values[maxIdx]), r, fill: tokens.color.positive });
|
|
9416
|
+
if (minIdx !== lastIdx) out.push({ x: xAt(minIdx), y: yAt(values[minIdx]), r, fill: tokens.color.negative });
|
|
9417
|
+
const lastStroke = lastIdx === maxIdx ? tokens.color.positive : lastIdx === minIdx ? tokens.color.negative : tokens.color.text;
|
|
9418
|
+
out.push({
|
|
9419
|
+
x: xAt(lastIdx),
|
|
9420
|
+
y: yAt(values[lastIdx]),
|
|
9421
|
+
r,
|
|
9422
|
+
fill: tokens.color.surface,
|
|
9423
|
+
stroke: lastStroke,
|
|
9424
|
+
strokeWidth: 1.75
|
|
9425
|
+
});
|
|
9426
|
+
return out;
|
|
9427
|
+
}
|
|
9428
|
+
function sparkScales(values, width, height, markers) {
|
|
9429
|
+
const n = values.length;
|
|
9430
|
+
const r = clamp5(Math.min(height, 40) * 0.12, 2.5, 4);
|
|
9431
|
+
const padX = markers ? r + 1 : 1.75;
|
|
9432
|
+
const padY = markers ? r + 1 : 1.75;
|
|
9433
|
+
const innerW = Math.max(1, width - padX * 2);
|
|
9434
|
+
const innerH = Math.max(1, height - padY * 2);
|
|
9435
|
+
const min = Math.min(...values);
|
|
9436
|
+
const max = Math.max(...values);
|
|
9437
|
+
const span = max - min;
|
|
9438
|
+
const xAt = (i) => n === 1 ? width / 2 : padX + i / (n - 1) * innerW;
|
|
9439
|
+
const yAt = (v) => padY + (1 - (span === 0 ? 0.5 : (v - min) / span)) * innerH;
|
|
9440
|
+
return { xAt, yAt, r };
|
|
9441
|
+
}
|
|
9442
|
+
function buildSparkline(tokens, width, height, values, markers) {
|
|
9090
9443
|
if (values.length === 0 || width <= 0 || height <= 0) return null;
|
|
9091
9444
|
const svg = document.createElementNS(SVG_NS, "svg");
|
|
9092
9445
|
svg.setAttribute("width", String(width));
|
|
9093
9446
|
svg.setAttribute("height", String(height));
|
|
9094
9447
|
svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
|
|
9095
|
-
svg.setAttribute("preserveAspectRatio", "none");
|
|
9096
9448
|
svg.style.display = "block";
|
|
9097
9449
|
svg.style.overflow = "visible";
|
|
9098
9450
|
const stroke = 1.75;
|
|
9099
|
-
const
|
|
9100
|
-
const
|
|
9451
|
+
const line2 = tokens.color.textMuted;
|
|
9452
|
+
const { xAt, yAt, r } = sparkScales(values, width, height, markers);
|
|
9101
9453
|
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
9454
|
if (n === 1) {
|
|
9111
9455
|
const dot = document.createElementNS(SVG_NS, "circle");
|
|
9112
9456
|
dot.setAttribute("cx", String(width / 2));
|
|
9113
9457
|
dot.setAttribute("cy", String(yAt(values[0])));
|
|
9114
|
-
dot.setAttribute("r", "2.
|
|
9458
|
+
dot.setAttribute("r", "2.5");
|
|
9115
9459
|
dot.setAttribute("fill", tokens.color.accent);
|
|
9116
9460
|
svg.appendChild(dot);
|
|
9117
9461
|
return svg;
|
|
9118
9462
|
}
|
|
9119
9463
|
let lineD = "";
|
|
9120
|
-
for (let i = 0; i < n; i++) {
|
|
9121
|
-
|
|
9122
|
-
}
|
|
9123
|
-
const areaD = `${lineD}L${width.toFixed(2)} ${height}L0 ${height}Z`;
|
|
9464
|
+
for (let i = 0; i < n; i++) lineD += `${i === 0 ? "M" : "L"}${xAt(i).toFixed(2)} ${yAt(values[i]).toFixed(2)}`;
|
|
9465
|
+
const areaD = `${lineD}L${xAt(n - 1).toFixed(2)} ${height}L${xAt(0).toFixed(2)} ${height}Z`;
|
|
9124
9466
|
const gradId = `graphein-spark-${Math.random().toString(36).slice(2, 9)}`;
|
|
9125
9467
|
const defs = document.createElementNS(SVG_NS, "defs");
|
|
9126
9468
|
const grad = document.createElementNS(SVG_NS, "linearGradient");
|
|
@@ -9131,11 +9473,11 @@ function buildSparkline(tokens, width, height, values) {
|
|
|
9131
9473
|
grad.setAttribute("y2", "1");
|
|
9132
9474
|
const stopTop = document.createElementNS(SVG_NS, "stop");
|
|
9133
9475
|
stopTop.setAttribute("offset", "0");
|
|
9134
|
-
stopTop.setAttribute("stop-color",
|
|
9135
|
-
stopTop.setAttribute("stop-opacity", "0.
|
|
9476
|
+
stopTop.setAttribute("stop-color", line2);
|
|
9477
|
+
stopTop.setAttribute("stop-opacity", "0.18");
|
|
9136
9478
|
const stopBottom = document.createElementNS(SVG_NS, "stop");
|
|
9137
9479
|
stopBottom.setAttribute("offset", "1");
|
|
9138
|
-
stopBottom.setAttribute("stop-color",
|
|
9480
|
+
stopBottom.setAttribute("stop-color", line2);
|
|
9139
9481
|
stopBottom.setAttribute("stop-opacity", "0");
|
|
9140
9482
|
grad.appendChild(stopTop);
|
|
9141
9483
|
grad.appendChild(stopBottom);
|
|
@@ -9148,14 +9490,43 @@ function buildSparkline(tokens, width, height, values) {
|
|
|
9148
9490
|
const path = document.createElementNS(SVG_NS, "path");
|
|
9149
9491
|
path.setAttribute("d", lineD);
|
|
9150
9492
|
path.setAttribute("fill", "none");
|
|
9151
|
-
path.setAttribute("stroke",
|
|
9493
|
+
path.setAttribute("stroke", line2);
|
|
9152
9494
|
path.setAttribute("stroke-width", String(stroke));
|
|
9153
9495
|
path.setAttribute("stroke-linejoin", "round");
|
|
9154
9496
|
path.setAttribute("stroke-linecap", "round");
|
|
9155
9497
|
svg.appendChild(path);
|
|
9498
|
+
if (markers) {
|
|
9499
|
+
for (const m of sparkMarkers(values, xAt, yAt, r, tokens)) {
|
|
9500
|
+
const c = document.createElementNS(SVG_NS, "circle");
|
|
9501
|
+
c.setAttribute("cx", m.x.toFixed(2));
|
|
9502
|
+
c.setAttribute("cy", m.y.toFixed(2));
|
|
9503
|
+
c.setAttribute("r", String(m.r));
|
|
9504
|
+
c.setAttribute("fill", m.fill ?? "none");
|
|
9505
|
+
if (m.stroke) {
|
|
9506
|
+
c.setAttribute("stroke", m.stroke);
|
|
9507
|
+
c.setAttribute("stroke-width", String(m.strokeWidth ?? 1.75));
|
|
9508
|
+
}
|
|
9509
|
+
svg.appendChild(c);
|
|
9510
|
+
}
|
|
9511
|
+
}
|
|
9156
9512
|
return svg;
|
|
9157
9513
|
}
|
|
9158
|
-
function
|
|
9514
|
+
function paintSparkMarkers(ctx, values, xAt, yAt, r, tokens) {
|
|
9515
|
+
for (const m of sparkMarkers(values, xAt, yAt, r, tokens)) {
|
|
9516
|
+
ctx.beginPath();
|
|
9517
|
+
ctx.arc(m.x, m.y, m.r, 0, Math.PI * 2);
|
|
9518
|
+
if (m.fill) {
|
|
9519
|
+
ctx.fillStyle = m.fill;
|
|
9520
|
+
ctx.fill();
|
|
9521
|
+
}
|
|
9522
|
+
if (m.stroke) {
|
|
9523
|
+
ctx.lineWidth = m.strokeWidth ?? 1.75;
|
|
9524
|
+
ctx.strokeStyle = m.stroke;
|
|
9525
|
+
ctx.stroke();
|
|
9526
|
+
}
|
|
9527
|
+
}
|
|
9528
|
+
}
|
|
9529
|
+
function buildSparklineCanvas(tokens, width, height, values, markers, sketch) {
|
|
9159
9530
|
if (values.length === 0 || width <= 0 || height <= 0) return null;
|
|
9160
9531
|
const dpr = getDevicePixelRatio();
|
|
9161
9532
|
const canvas = document.createElement("canvas");
|
|
@@ -9167,207 +9538,339 @@ function buildSparklineCanvas(tokens, width, height, values, sketch) {
|
|
|
9167
9538
|
const ctx = canvas.getContext("2d");
|
|
9168
9539
|
if (!ctx) return null;
|
|
9169
9540
|
ctx.scale(dpr, dpr);
|
|
9170
|
-
const
|
|
9171
|
-
const pad3 = stroke;
|
|
9172
|
-
const innerH = Math.max(1, height - pad3 * 2);
|
|
9541
|
+
const { xAt, yAt, r } = sparkScales(values, width, height, markers);
|
|
9173
9542
|
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
9543
|
const pen = new RoughPen(ctx, sketch);
|
|
9180
9544
|
if (n === 1) {
|
|
9181
9545
|
pen.circle(width / 2, yAt(values[0]), 2.5, { fill: tokens.color.accent });
|
|
9182
9546
|
return canvas;
|
|
9183
9547
|
}
|
|
9184
9548
|
const points = values.map((v, i) => ({ x: xAt(i), y: yAt(v) }));
|
|
9185
|
-
pen.trendStroke(points, { stroke: tokens.color.
|
|
9549
|
+
pen.trendStroke(points, { stroke: tokens.color.textMuted, strokeWidth: 1.75 });
|
|
9550
|
+
if (markers) paintSparkMarkers(ctx, values, xAt, yAt, r, tokens);
|
|
9186
9551
|
return canvas;
|
|
9187
9552
|
}
|
|
9188
|
-
function
|
|
9189
|
-
|
|
9190
|
-
|
|
9191
|
-
|
|
9553
|
+
function paintSparklineCanvas(ctx, tokens, box, values, markers) {
|
|
9554
|
+
if (values.length === 0 || box.width <= 0 || box.height <= 0) return;
|
|
9555
|
+
const { xAt: xAt0, yAt: yAt0, r } = sparkScales(values, box.width, box.height, markers);
|
|
9556
|
+
const xAt = (i) => box.x + xAt0(i);
|
|
9557
|
+
const yAt = (v) => box.y + yAt0(v);
|
|
9558
|
+
const n = values.length;
|
|
9559
|
+
ctx.save();
|
|
9560
|
+
if (n === 1) {
|
|
9561
|
+
ctx.beginPath();
|
|
9562
|
+
ctx.arc(xAt(0), yAt(values[0]), 2.5, 0, Math.PI * 2);
|
|
9563
|
+
ctx.fillStyle = tokens.color.accent;
|
|
9564
|
+
ctx.fill();
|
|
9565
|
+
ctx.restore();
|
|
9192
9566
|
return;
|
|
9193
9567
|
}
|
|
9568
|
+
ctx.globalAlpha = 0.16;
|
|
9569
|
+
ctx.beginPath();
|
|
9570
|
+
ctx.moveTo(xAt(0), yAt(values[0]));
|
|
9571
|
+
for (let i = 1; i < n; i++) ctx.lineTo(xAt(i), yAt(values[i]));
|
|
9572
|
+
ctx.lineTo(xAt(n - 1), box.y + box.height);
|
|
9573
|
+
ctx.lineTo(xAt(0), box.y + box.height);
|
|
9574
|
+
ctx.closePath();
|
|
9575
|
+
ctx.fillStyle = tokens.color.textMuted;
|
|
9576
|
+
ctx.fill();
|
|
9577
|
+
ctx.globalAlpha = 1;
|
|
9578
|
+
ctx.beginPath();
|
|
9579
|
+
for (let i = 0; i < n; i++) {
|
|
9580
|
+
const px = xAt(i);
|
|
9581
|
+
const py = yAt(values[i]);
|
|
9582
|
+
if (i === 0) ctx.moveTo(px, py);
|
|
9583
|
+
else ctx.lineTo(px, py);
|
|
9584
|
+
}
|
|
9585
|
+
ctx.strokeStyle = tokens.color.textMuted;
|
|
9586
|
+
ctx.lineWidth = 1.75;
|
|
9587
|
+
ctx.lineJoin = "round";
|
|
9588
|
+
ctx.lineCap = "round";
|
|
9589
|
+
ctx.stroke();
|
|
9590
|
+
if (markers) paintSparkMarkers(ctx, values, xAt, yAt, r, tokens);
|
|
9591
|
+
ctx.restore();
|
|
9592
|
+
}
|
|
9593
|
+
function drawKpi(surface, spec, tokens, size, context) {
|
|
9594
|
+
const kpi = spec;
|
|
9194
9595
|
const data = kpi.data ?? [];
|
|
9195
|
-
const value =
|
|
9196
|
-
const delta =
|
|
9596
|
+
const value = resolveValueRef(kpi.value, data);
|
|
9597
|
+
const delta = resolveValueRef(kpi.delta, data);
|
|
9197
9598
|
const sparkField = sparklineField(kpi);
|
|
9198
9599
|
const sparkValues = sparkField ? sparklineValues(data, sparkField) : [];
|
|
9199
9600
|
const hasSparkline = sparkValues.length > 0;
|
|
9200
|
-
const
|
|
9601
|
+
const markers = sparklineMarkers(kpi);
|
|
9602
|
+
const comparisons = kpi.comparisons?.map((c) => resolveComparison(c, data, kpi.format)).filter((c) => c != null) ?? [];
|
|
9603
|
+
const hasDelta = delta != null;
|
|
9604
|
+
const isRich = hasDelta || hasSparkline || comparisons.length > 0;
|
|
9605
|
+
const align = kpi.align ?? "center";
|
|
9606
|
+
const labelAbove = kpi.labelPosition === "above" || kpi.labelPosition == null && isRich;
|
|
9201
9607
|
const framed = context?.framed === true;
|
|
9202
9608
|
const rect = cardRectFor(surface, tokens, size, kpi);
|
|
9203
|
-
const
|
|
9204
|
-
|
|
9205
|
-
|
|
9206
|
-
|
|
9609
|
+
const progress = context?.valueProgress;
|
|
9610
|
+
const from = context?.valueFrom ?? 0;
|
|
9611
|
+
const displayValue = value != null && progress != null && progress < 1 ? from + (value - from) * progress : value;
|
|
9612
|
+
const displaySparkValues = morphSparkline(sparkValues, context?.sparklineFrom, progress);
|
|
9613
|
+
const valueText = value == null ? "\u2014" : formatValue(value, kpi.format);
|
|
9614
|
+
const displayText = displayValue == null ? "\u2014" : formatValue(displayValue, kpi.format);
|
|
9615
|
+
const fromText = context?.valueFrom != null && value != null ? formatValue(context.valueFrom, kpi.format) : valueText;
|
|
9616
|
+
const sizingText = fromText.length > valueText.length ? fromText : valueText;
|
|
9617
|
+
const pad3 = framed ? 0 : tokens.spacing.lg;
|
|
9618
|
+
const contentWidth = Math.max(1, rect.width - pad3 * 2);
|
|
9619
|
+
const valueSize = valueFontSize(rect, contentWidth, sizingText, kpi.unit, isRich);
|
|
9620
|
+
const geom = layoutKpi(rect, tokens, framed, {
|
|
9621
|
+
hasLabel: !!kpi.label,
|
|
9622
|
+
labelAbove,
|
|
9623
|
+
hasDelta,
|
|
9624
|
+
hasSparkline,
|
|
9625
|
+
compCount: comparisons.length,
|
|
9626
|
+
valueSize
|
|
9627
|
+
});
|
|
9628
|
+
const sketch = resolveSketch(spec);
|
|
9629
|
+
if (surface.headless) {
|
|
9630
|
+
drawKpiCanvas(surface, kpi, tokens, {
|
|
9631
|
+
align,
|
|
9632
|
+
valueSize,
|
|
9633
|
+
displayText,
|
|
9634
|
+
delta,
|
|
9635
|
+
comparisons,
|
|
9636
|
+
sparkValues: displaySparkValues,
|
|
9637
|
+
markers,
|
|
9638
|
+
geom
|
|
9639
|
+
});
|
|
9640
|
+
return;
|
|
9641
|
+
}
|
|
9642
|
+
const card = document.createElement("div");
|
|
9643
|
+
card.style.position = "absolute";
|
|
9644
|
+
card.style.left = `${rect.x}px`;
|
|
9645
|
+
card.style.top = `${rect.y}px`;
|
|
9646
|
+
card.style.width = `${rect.width}px`;
|
|
9647
|
+
card.style.height = `${rect.height}px`;
|
|
9648
|
+
card.style.boxSizing = "border-box";
|
|
9649
|
+
card.style.overflow = "hidden";
|
|
9650
|
+
card.style.pointerEvents = "none";
|
|
9651
|
+
const ox = rect.x;
|
|
9652
|
+
const oy = rect.y;
|
|
9653
|
+
const addAt = (el, left, top, width) => {
|
|
9654
|
+
el.style.position = "absolute";
|
|
9655
|
+
el.style.left = `${left - ox}px`;
|
|
9656
|
+
el.style.top = `${top - oy}px`;
|
|
9657
|
+
el.style.width = `${width}px`;
|
|
9658
|
+
card.appendChild(el);
|
|
9659
|
+
};
|
|
9660
|
+
if (kpi.label && geom.labelY >= 0) {
|
|
9661
|
+
const el = document.createElement("div");
|
|
9662
|
+
el.textContent = kpi.label;
|
|
9663
|
+
Object.assign(el.style, {
|
|
9207
9664
|
color: tokens.color.textMuted,
|
|
9208
9665
|
fontFamily: tokens.font.family,
|
|
9209
|
-
fontSize: `${
|
|
9666
|
+
fontSize: `${geom.labelSize}px`,
|
|
9210
9667
|
fontWeight: String(tokens.font.weight.medium),
|
|
9211
|
-
letterSpacing: "0.04em",
|
|
9212
9668
|
lineHeight: "1.25",
|
|
9213
|
-
|
|
9214
|
-
maxWidth: "100%",
|
|
9669
|
+
textAlign: align === "start" ? "left" : align === "end" ? "right" : "center",
|
|
9215
9670
|
overflow: "hidden",
|
|
9216
9671
|
textOverflow: "ellipsis",
|
|
9217
9672
|
whiteSpace: "nowrap"
|
|
9218
9673
|
});
|
|
9219
|
-
|
|
9220
|
-
|
|
9221
|
-
const
|
|
9222
|
-
|
|
9223
|
-
|
|
9224
|
-
|
|
9225
|
-
|
|
9226
|
-
|
|
9674
|
+
addAt(el, geom.left, geom.labelY, geom.contentWidth);
|
|
9675
|
+
}
|
|
9676
|
+
const valueWrap = document.createElement("div");
|
|
9677
|
+
valueWrap.style.display = "flex";
|
|
9678
|
+
valueWrap.style.alignItems = "baseline";
|
|
9679
|
+
valueWrap.style.justifyContent = flexAlign(align);
|
|
9680
|
+
valueWrap.style.gap = "0.12em";
|
|
9681
|
+
valueWrap.style.height = `${geom.valueH}px`;
|
|
9682
|
+
valueWrap.style.overflow = "hidden";
|
|
9683
|
+
const num2 = document.createElement("div");
|
|
9684
|
+
num2.textContent = displayText;
|
|
9685
|
+
Object.assign(num2.style, {
|
|
9686
|
+
color: kpi.color ?? tokens.color.text,
|
|
9227
9687
|
fontFamily: tokens.font.family,
|
|
9228
9688
|
fontSize: `${valueSize}px`,
|
|
9229
9689
|
fontWeight: String(tokens.font.weight.bold),
|
|
9230
9690
|
letterSpacing: "-0.03em",
|
|
9231
9691
|
lineHeight: "1.02",
|
|
9232
|
-
maxWidth: "100%",
|
|
9233
9692
|
overflow: "hidden",
|
|
9234
9693
|
textOverflow: "ellipsis",
|
|
9235
9694
|
whiteSpace: "nowrap"
|
|
9236
9695
|
});
|
|
9237
|
-
|
|
9696
|
+
valueWrap.appendChild(num2);
|
|
9697
|
+
if (kpi.unit) {
|
|
9698
|
+
const unit = document.createElement("div");
|
|
9699
|
+
unit.textContent = kpi.unit;
|
|
9700
|
+
Object.assign(unit.style, {
|
|
9701
|
+
color: tokens.color.textMuted,
|
|
9702
|
+
fontFamily: tokens.font.family,
|
|
9703
|
+
fontSize: `${unitFontSize(valueSize)}px`,
|
|
9704
|
+
fontWeight: String(tokens.font.weight.medium),
|
|
9705
|
+
whiteSpace: "nowrap",
|
|
9706
|
+
flex: "0 0 auto"
|
|
9707
|
+
});
|
|
9708
|
+
valueWrap.appendChild(unit);
|
|
9709
|
+
}
|
|
9710
|
+
addAt(valueWrap, geom.left, geom.valueY, geom.contentWidth);
|
|
9711
|
+
if (hasDelta && geom.deltaY >= 0 && delta != null) {
|
|
9238
9712
|
const positive = delta >= 0;
|
|
9239
|
-
|
|
9713
|
+
const el = document.createElement("div");
|
|
9714
|
+
el.textContent = `${positive ? "\u25B2" : "\u25BC"} ${formatDelta(delta, kpi.format)}`;
|
|
9715
|
+
Object.assign(el.style, {
|
|
9240
9716
|
color: positive ? tokens.color.positive : tokens.color.negative,
|
|
9241
9717
|
fontFamily: tokens.font.family,
|
|
9242
|
-
fontSize: `${
|
|
9718
|
+
fontSize: `${geom.deltaSize}px`,
|
|
9243
9719
|
fontWeight: String(tokens.font.weight.medium),
|
|
9244
|
-
lineHeight: "1",
|
|
9720
|
+
lineHeight: "1.2",
|
|
9721
|
+
textAlign: align === "start" ? "left" : align === "end" ? "right" : "center",
|
|
9245
9722
|
whiteSpace: "nowrap"
|
|
9246
9723
|
});
|
|
9724
|
+
addAt(el, geom.left, geom.deltaY, geom.contentWidth);
|
|
9725
|
+
}
|
|
9726
|
+
if (geom.spark) {
|
|
9727
|
+
const node = sketch ? buildSparklineCanvas(tokens, geom.spark.width, geom.spark.height, displaySparkValues, markers, sketch) : buildSparkline(tokens, geom.spark.width, geom.spark.height, displaySparkValues, markers);
|
|
9728
|
+
if (node) addAt(node, geom.spark.x, geom.spark.y, geom.spark.width);
|
|
9729
|
+
}
|
|
9730
|
+
if (comparisons.length > 0 && geom.dividerY >= 0) {
|
|
9731
|
+
const divider = document.createElement("div");
|
|
9732
|
+
divider.style.height = "1px";
|
|
9733
|
+
divider.style.background = tokens.color.border;
|
|
9734
|
+
addAt(divider, geom.left, geom.dividerY, geom.contentWidth);
|
|
9735
|
+
comparisons.forEach((c, i) => {
|
|
9736
|
+
const row = document.createElement("div");
|
|
9737
|
+
row.style.display = "flex";
|
|
9738
|
+
row.style.alignItems = "baseline";
|
|
9739
|
+
row.style.gap = "0.5em";
|
|
9740
|
+
row.style.height = `${geom.compRowH}px`;
|
|
9741
|
+
row.style.overflow = "hidden";
|
|
9742
|
+
const label = document.createElement("span");
|
|
9743
|
+
label.textContent = c.label;
|
|
9744
|
+
Object.assign(label.style, {
|
|
9745
|
+
color: tokens.color.text,
|
|
9746
|
+
fontFamily: tokens.font.family,
|
|
9747
|
+
fontSize: `${geom.compSize}px`,
|
|
9748
|
+
fontWeight: String(tokens.font.weight.medium),
|
|
9749
|
+
whiteSpace: "nowrap",
|
|
9750
|
+
flex: "0 0 auto"
|
|
9751
|
+
});
|
|
9752
|
+
const val = document.createElement("span");
|
|
9753
|
+
val.textContent = c.text;
|
|
9754
|
+
Object.assign(val.style, {
|
|
9755
|
+
color: c.positive ? tokens.color.positive : tokens.color.negative,
|
|
9756
|
+
fontFamily: tokens.font.family,
|
|
9757
|
+
fontSize: `${geom.compSize}px`,
|
|
9758
|
+
fontWeight: String(tokens.font.weight.medium),
|
|
9759
|
+
whiteSpace: "nowrap"
|
|
9760
|
+
});
|
|
9761
|
+
row.appendChild(label);
|
|
9762
|
+
row.appendChild(val);
|
|
9763
|
+
addAt(row, geom.left, geom.compStartY + i * geom.compRowH, geom.contentWidth);
|
|
9764
|
+
});
|
|
9247
9765
|
}
|
|
9248
9766
|
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
9767
|
}
|
|
9262
|
-
function drawKpiCanvas(surface, kpi, tokens,
|
|
9263
|
-
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) {
|
|
9768
|
+
function drawKpiCanvas(surface, kpi, tokens, p) {
|
|
9769
|
+
const ctx = surface.marks.ctx;
|
|
9770
|
+
const { geom } = p;
|
|
9771
|
+
const alignX = () => p.align === "start" ? geom.left : p.align === "end" ? geom.left + geom.contentWidth : geom.left + geom.contentWidth / 2;
|
|
9772
|
+
const alignCanvas = textAlignOf(p.align);
|
|
9773
|
+
if (kpi.label && geom.labelY >= 0) {
|
|
9297
9774
|
paintCanvasText(ctx, {
|
|
9298
|
-
x:
|
|
9299
|
-
y,
|
|
9300
|
-
text: kpi.label
|
|
9301
|
-
font: fontString(
|
|
9775
|
+
x: alignX(),
|
|
9776
|
+
y: geom.labelY,
|
|
9777
|
+
text: kpi.label,
|
|
9778
|
+
font: fontString(geom.labelSize, tokens.font.family, tokens.font.weight.medium),
|
|
9302
9779
|
color: tokens.color.textMuted,
|
|
9303
|
-
size:
|
|
9780
|
+
size: geom.labelSize,
|
|
9781
|
+
align: alignCanvas,
|
|
9304
9782
|
baseline: "top"
|
|
9305
9783
|
});
|
|
9306
|
-
y += labelH + tokens.spacing.sm;
|
|
9307
9784
|
}
|
|
9785
|
+
const unitSize = kpi.unit ? unitFontSize(p.valueSize) : 0;
|
|
9786
|
+
const numberFont = fontString(p.valueSize, tokens.font.family, tokens.font.weight.bold);
|
|
9787
|
+
const unitFont = kpi.unit ? fontString(unitSize, tokens.font.family, tokens.font.weight.medium) : "";
|
|
9788
|
+
ctx.save();
|
|
9789
|
+
ctx.font = numberFont;
|
|
9790
|
+
const numberW = ctx.measureText(p.displayText).width;
|
|
9791
|
+
let unitW = 0;
|
|
9792
|
+
if (kpi.unit) {
|
|
9793
|
+
ctx.font = unitFont;
|
|
9794
|
+
unitW = ctx.measureText(kpi.unit).width;
|
|
9795
|
+
}
|
|
9796
|
+
ctx.restore();
|
|
9797
|
+
const unitGap = kpi.unit ? p.valueSize * 0.12 : 0;
|
|
9798
|
+
const totalW = numberW + (kpi.unit ? unitGap + unitW : 0);
|
|
9799
|
+
const groupX = p.align === "start" ? geom.left : p.align === "end" ? geom.left + geom.contentWidth - totalW : geom.left + (geom.contentWidth - totalW) / 2;
|
|
9800
|
+
const baselineY = geom.valueY + p.valueSize * 0.82;
|
|
9308
9801
|
paintCanvasText(ctx, {
|
|
9309
|
-
x:
|
|
9310
|
-
y,
|
|
9311
|
-
text:
|
|
9312
|
-
font:
|
|
9313
|
-
color: tokens.color.text,
|
|
9314
|
-
size: valueSize,
|
|
9315
|
-
|
|
9802
|
+
x: groupX,
|
|
9803
|
+
y: baselineY,
|
|
9804
|
+
text: p.displayText,
|
|
9805
|
+
font: numberFont,
|
|
9806
|
+
color: kpi.color ?? tokens.color.text,
|
|
9807
|
+
size: p.valueSize,
|
|
9808
|
+
align: "left",
|
|
9809
|
+
baseline: "alphabetic"
|
|
9316
9810
|
});
|
|
9317
|
-
|
|
9318
|
-
if (delta != null) {
|
|
9319
|
-
const positive = delta >= 0;
|
|
9811
|
+
if (kpi.unit) {
|
|
9320
9812
|
paintCanvasText(ctx, {
|
|
9321
|
-
x:
|
|
9322
|
-
y,
|
|
9323
|
-
text:
|
|
9324
|
-
font:
|
|
9813
|
+
x: groupX + numberW + unitGap,
|
|
9814
|
+
y: baselineY,
|
|
9815
|
+
text: kpi.unit,
|
|
9816
|
+
font: unitFont,
|
|
9817
|
+
color: tokens.color.textMuted,
|
|
9818
|
+
size: unitSize,
|
|
9819
|
+
align: "left",
|
|
9820
|
+
baseline: "alphabetic"
|
|
9821
|
+
});
|
|
9822
|
+
}
|
|
9823
|
+
if (p.delta != null && geom.deltaY >= 0) {
|
|
9824
|
+
const positive = p.delta >= 0;
|
|
9825
|
+
paintCanvasText(ctx, {
|
|
9826
|
+
x: alignX(),
|
|
9827
|
+
y: geom.deltaY,
|
|
9828
|
+
text: `${positive ? "\u25B2" : "\u25BC"} ${formatDelta(p.delta, kpi.format)}`,
|
|
9829
|
+
font: fontString(geom.deltaSize, tokens.font.family, tokens.font.weight.medium),
|
|
9325
9830
|
color: positive ? tokens.color.positive : tokens.color.negative,
|
|
9326
|
-
size:
|
|
9831
|
+
size: geom.deltaSize,
|
|
9832
|
+
align: alignCanvas,
|
|
9327
9833
|
baseline: "top"
|
|
9328
9834
|
});
|
|
9329
9835
|
}
|
|
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);
|
|
9836
|
+
if (geom.spark && p.sparkValues.length > 0) {
|
|
9837
|
+
paintSparklineCanvas(ctx, tokens, geom.spark, p.sparkValues, p.markers);
|
|
9337
9838
|
}
|
|
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
|
-
|
|
9839
|
+
if (p.comparisons.length > 0 && geom.dividerY >= 0) {
|
|
9840
|
+
ctx.save();
|
|
9841
|
+
ctx.fillStyle = tokens.color.border;
|
|
9842
|
+
ctx.fillRect(geom.left, geom.dividerY, geom.contentWidth, 1);
|
|
9843
|
+
ctx.restore();
|
|
9844
|
+
const compFont = fontString(geom.compSize, tokens.font.family, tokens.font.weight.medium);
|
|
9845
|
+
const gap = geom.compSize * 0.5;
|
|
9846
|
+
p.comparisons.forEach((c, i) => {
|
|
9847
|
+
const rowY = geom.compStartY + i * geom.compRowH + geom.compRowH / 2;
|
|
9848
|
+
ctx.save();
|
|
9849
|
+
ctx.font = compFont;
|
|
9850
|
+
const labelW = ctx.measureText(c.label).width;
|
|
9851
|
+
ctx.restore();
|
|
9852
|
+
paintCanvasText(ctx, {
|
|
9853
|
+
x: geom.left,
|
|
9854
|
+
y: rowY,
|
|
9855
|
+
text: c.label,
|
|
9856
|
+
font: compFont,
|
|
9857
|
+
color: tokens.color.text,
|
|
9858
|
+
size: geom.compSize,
|
|
9859
|
+
align: "left",
|
|
9860
|
+
baseline: "middle"
|
|
9861
|
+
});
|
|
9862
|
+
paintCanvasText(ctx, {
|
|
9863
|
+
x: geom.left + labelW + gap,
|
|
9864
|
+
y: rowY,
|
|
9865
|
+
text: c.text,
|
|
9866
|
+
font: compFont,
|
|
9867
|
+
color: c.positive ? tokens.color.positive : tokens.color.negative,
|
|
9868
|
+
size: geom.compSize,
|
|
9869
|
+
align: "left",
|
|
9870
|
+
baseline: "middle"
|
|
9871
|
+
});
|
|
9872
|
+
});
|
|
9369
9873
|
}
|
|
9370
|
-
ctx.restore();
|
|
9371
9874
|
}
|
|
9372
9875
|
|
|
9373
9876
|
// src/charts/sankey.ts
|
|
@@ -9496,7 +9999,7 @@ function layout(graph, area2, nodeWidth, pad3) {
|
|
|
9496
9999
|
const kx = maxLayer > 0 ? (area2.width - nodeWidth) / maxLayer : 0;
|
|
9497
10000
|
const maxColumnSize = columns.reduce((m, col) => Math.max(m, col.length), 0);
|
|
9498
10001
|
const desiredPad = Math.max(0, pad3);
|
|
9499
|
-
const effectivePad = maxColumnSize > 1
|
|
10002
|
+
const effectivePad = maxColumnSize > 1 ? Math.min(desiredPad, area2.height * 0.35 / (maxColumnSize - 1)) : desiredPad;
|
|
9500
10003
|
for (const col of columns) {
|
|
9501
10004
|
for (const n of col) {
|
|
9502
10005
|
n.x0 = area2.x + n.layer * kx;
|
|
@@ -9618,15 +10121,24 @@ function drawSankey(surface, spec, tokens, size) {
|
|
|
9618
10121
|
const graph = buildGraph(sankey, tokens.color.palette);
|
|
9619
10122
|
if (graph.nodes.length === 0 || content.width <= 0 || content.height <= 0) return;
|
|
9620
10123
|
const labelFont = fontString(f.size.small, f.family, f.weight.medium);
|
|
9621
|
-
|
|
9622
|
-
|
|
9623
|
-
const
|
|
10124
|
+
const valueFont = fontString(f.size.tiny, f.family, f.weight.normal);
|
|
10125
|
+
const showValues = sankey.nodeValues !== false;
|
|
10126
|
+
const labelExtent = (n) => {
|
|
10127
|
+
const nameW = measureText(n.name, labelFont).width;
|
|
10128
|
+
const valW = showValues ? measureText(formatValue(n.value, sankey.encoding.value.format), valueFont).width : 0;
|
|
10129
|
+
return Math.max(nameW, valW);
|
|
10130
|
+
};
|
|
10131
|
+
const colExtent = (col) => col.reduce((m, n) => Math.max(m, labelExtent(n)), 0);
|
|
10132
|
+
const LABEL_GAP = 6;
|
|
10133
|
+
const capGutter = content.width * 0.3;
|
|
10134
|
+
const leftGutter = Math.min(colExtent(graph.columns[0] ?? []) + LABEL_GAP + 4, capGutter);
|
|
10135
|
+
const rightGutter = graph.maxLayer > 0 ? Math.min(colExtent(graph.columns[graph.maxLayer] ?? []) + LABEL_GAP + 4, capGutter) : 0;
|
|
9624
10136
|
const nodeWidth = sankey.nodeWidth ?? 16;
|
|
9625
10137
|
const pad3 = sankey.nodePadding ?? 14;
|
|
9626
10138
|
const area2 = {
|
|
9627
|
-
x: content.x +
|
|
10139
|
+
x: content.x + leftGutter,
|
|
9628
10140
|
y: content.y + 4,
|
|
9629
|
-
width: Math.max(10, content.width -
|
|
10141
|
+
width: Math.max(10, content.width - leftGutter - rightGutter),
|
|
9630
10142
|
height: Math.max(10, content.height - 8)
|
|
9631
10143
|
};
|
|
9632
10144
|
layout(graph, area2, nodeWidth, pad3);
|
|
@@ -9650,12 +10162,13 @@ function drawSankey(surface, spec, tokens, size) {
|
|
|
9650
10162
|
}
|
|
9651
10163
|
}
|
|
9652
10164
|
ctx.restore();
|
|
9653
|
-
const showValues = sankey.nodeValues !== false;
|
|
9654
10165
|
const mid = area2.x + area2.width / 2;
|
|
9655
10166
|
for (const n of graph.nodes) {
|
|
9656
|
-
const
|
|
10167
|
+
const outerLeft = n.layer === 0;
|
|
10168
|
+
const outerRight = n.layer === graph.maxLayer && graph.maxLayer > 0;
|
|
10169
|
+
const labelRight = outerRight ? true : outerLeft ? false : n.x0 < mid;
|
|
9657
10170
|
const cy = center(n);
|
|
9658
|
-
const left = labelRight ? n.x1 +
|
|
10171
|
+
const left = labelRight ? n.x1 + LABEL_GAP : n.x0 - LABEL_GAP;
|
|
9659
10172
|
const transform2 = labelRight ? "translateY(-50%)" : "translate(-100%,-50%)";
|
|
9660
10173
|
addOverlayText(surface, tokens, {
|
|
9661
10174
|
left,
|
|
@@ -9834,18 +10347,24 @@ function drawLegend3(surface, tokens, legend, domain, scheme, format) {
|
|
|
9834
10347
|
transform: "translateX(-100%)"
|
|
9835
10348
|
});
|
|
9836
10349
|
}
|
|
9837
|
-
function drawChoropleth(surface, spec, tokens, size) {
|
|
10350
|
+
function drawChoropleth(surface, spec, tokens, size, context) {
|
|
9838
10351
|
const choropleth = spec;
|
|
10352
|
+
const emphasis = context?.emphasis ?? null;
|
|
9839
10353
|
const content = drawTitleBlock(surface, tokens, size, choropleth.title);
|
|
9840
10354
|
const features = choropleth.geo?.features ?? [];
|
|
9841
10355
|
if (features.length === 0 || content.width <= 0 || content.height <= 0) return;
|
|
9842
10356
|
const readKey = accessor(choropleth.encoding.key.field);
|
|
9843
10357
|
const readColor = accessor(choropleth.encoding.color.field);
|
|
9844
10358
|
const valueByKey = /* @__PURE__ */ new Map();
|
|
10359
|
+
const rowByKey = /* @__PURE__ */ new Map();
|
|
9845
10360
|
for (const row of choropleth.data ?? []) {
|
|
9846
|
-
const
|
|
9847
|
-
const
|
|
9848
|
-
|
|
10361
|
+
const datum = row;
|
|
10362
|
+
const k = toKey(readKey(datum));
|
|
10363
|
+
const v = toNumber(readColor(datum));
|
|
10364
|
+
if (k !== "" && Number.isFinite(v)) {
|
|
10365
|
+
valueByKey.set(k, v);
|
|
10366
|
+
rowByKey.set(k, datum);
|
|
10367
|
+
}
|
|
9849
10368
|
}
|
|
9850
10369
|
const projection = choropleth.projection ?? "mercator";
|
|
9851
10370
|
const project = projector(
|
|
@@ -9863,6 +10382,7 @@ function drawChoropleth(surface, spec, tokens, size) {
|
|
|
9863
10382
|
for (const feature of features) {
|
|
9864
10383
|
const key = featureKey(feature, choropleth.featureId);
|
|
9865
10384
|
const value = valueByKey.has(key) ? valueByKey.get(key) : null;
|
|
10385
|
+
const row = rowByKey.get(key);
|
|
9866
10386
|
if (value != null) {
|
|
9867
10387
|
if (value < vMin) vMin = value;
|
|
9868
10388
|
if (value > vMax) vMax = value;
|
|
@@ -9885,7 +10405,7 @@ function drawChoropleth(surface, spec, tokens, size) {
|
|
|
9885
10405
|
}
|
|
9886
10406
|
if (rings.length) polys.push(rings);
|
|
9887
10407
|
}
|
|
9888
|
-
projected.push({ key, name: featureName(feature, key), value, polys });
|
|
10408
|
+
projected.push({ key, name: featureName(feature, key), value, row, polys });
|
|
9889
10409
|
}
|
|
9890
10410
|
const spanX = maxX - minX;
|
|
9891
10411
|
const spanY = maxY - minY;
|
|
@@ -9932,6 +10452,7 @@ function drawChoropleth(surface, spec, tokens, size) {
|
|
|
9932
10452
|
key: pf.key,
|
|
9933
10453
|
name: pf.name,
|
|
9934
10454
|
value: pf.value,
|
|
10455
|
+
row: pf.row,
|
|
9935
10456
|
color: pf.value == null ? noDataFill : rgbaToCss(cscale.map(pf.value)),
|
|
9936
10457
|
polys,
|
|
9937
10458
|
cx: count ? cx / count : 0,
|
|
@@ -9946,6 +10467,7 @@ function drawChoropleth(surface, spec, tokens, size) {
|
|
|
9946
10467
|
ctx.strokeStyle = tokens.color.background;
|
|
9947
10468
|
ctx.lineWidth = 0.75;
|
|
9948
10469
|
for (const f of screen) {
|
|
10470
|
+
ctx.globalAlpha = rowAlpha(emphasis, f.row ?? { [choropleth.encoding.key.field]: f.key });
|
|
9949
10471
|
ctx.fillStyle = f.color;
|
|
9950
10472
|
for (const polygon of f.polys) {
|
|
9951
10473
|
ctx.beginPath();
|
|
@@ -9966,6 +10488,7 @@ function drawChoropleth(surface, spec, tokens, size) {
|
|
|
9966
10488
|
}
|
|
9967
10489
|
}
|
|
9968
10490
|
}
|
|
10491
|
+
ctx.globalAlpha = 1;
|
|
9969
10492
|
}
|
|
9970
10493
|
ctx.restore();
|
|
9971
10494
|
if (legendHeight > 0) {
|
|
@@ -10120,7 +10643,7 @@ function colorToTone(color) {
|
|
|
10120
10643
|
var HEADER_ROW_HEIGHT = 32;
|
|
10121
10644
|
var MIN_COL_WIDTH = 72;
|
|
10122
10645
|
var DEFAULT_BODY_ROW_HEIGHT = 30;
|
|
10123
|
-
var
|
|
10646
|
+
var SKETCH_BORDER_RADIUS = "14px 6px 16px 7px / 7px 15px 6px 14px";
|
|
10124
10647
|
function formatCellValue(value, column) {
|
|
10125
10648
|
return formatValue(value, column.format);
|
|
10126
10649
|
}
|
|
@@ -10160,7 +10683,7 @@ function buildTable(opts) {
|
|
|
10160
10683
|
pointerEvents: "auto",
|
|
10161
10684
|
boxSizing: "border-box",
|
|
10162
10685
|
border: `1px solid ${opts.sketch ? "transparent" : tokens.color.border}`,
|
|
10163
|
-
borderRadius: opts.sketch ?
|
|
10686
|
+
borderRadius: opts.sketch ? SKETCH_BORDER_RADIUS : `${tokens.radius.sm}px`,
|
|
10164
10687
|
background: tokens.color.background
|
|
10165
10688
|
});
|
|
10166
10689
|
container.replaceChildren();
|
|
@@ -13003,7 +13526,6 @@ function scatterInteraction(model) {
|
|
|
13003
13526
|
}
|
|
13004
13527
|
|
|
13005
13528
|
// src/runtime/combo.ts
|
|
13006
|
-
var DEFAULT_PADDING2 = { top: 12, right: 16, bottom: 12, left: 12 };
|
|
13007
13529
|
function fmtTick(v, f) {
|
|
13008
13530
|
return f ? formatValue(v, f) : formatNumber2(v, ",");
|
|
13009
13531
|
}
|
|
@@ -13189,7 +13711,7 @@ function buildComboModel(spec, tokens, opts) {
|
|
|
13189
13711
|
const legendPosition = typeof spec.legend === "object" && spec.legend.position || (legendItems.length > 8 ? "right" : "top");
|
|
13190
13712
|
const xShow = xAxisCfg.show !== false;
|
|
13191
13713
|
const yShow = spec.axes?.y?.show !== false;
|
|
13192
|
-
const padding = { ...
|
|
13714
|
+
const padding = { ...NO_PADDING, ...spec.padding };
|
|
13193
13715
|
const titleInput4 = typeof spec.title === "string" ? { text: spec.title } : spec.title ? { text: spec.title.text, subtitle: spec.title.subtitle } : void 0;
|
|
13194
13716
|
const frame = computeFrame({
|
|
13195
13717
|
width: opts.width,
|
|
@@ -13411,7 +13933,6 @@ function renderLayer(surface, lm) {
|
|
|
13411
13933
|
}
|
|
13412
13934
|
|
|
13413
13935
|
// src/runtime/histogram.ts
|
|
13414
|
-
var DEFAULT_PADDING3 = { top: 12, right: 16, bottom: 12, left: 12 };
|
|
13415
13936
|
function firstPaletteColor(tokens) {
|
|
13416
13937
|
return rgbaToCss(ordinalColorScale({ domain: ["0"], palette: tokens.color.palette }).map("0"));
|
|
13417
13938
|
}
|
|
@@ -13463,7 +13984,7 @@ function buildHistogramModel(spec, tokens, opts) {
|
|
|
13463
13984
|
const xLabels = xTickValues.map((v) => xFmt ? formatValue(v, xFmt) : formatNumber2(v, ","));
|
|
13464
13985
|
const yFmt = spec.axes?.y?.format ?? (spec.density ? ".3~f" : void 0);
|
|
13465
13986
|
const yLabels = yTickValues.map((v) => yFmt ? formatValue(v, yFmt) : formatNumber2(v, ","));
|
|
13466
|
-
const padding = { ...
|
|
13987
|
+
const padding = { ...NO_PADDING, ...spec.padding };
|
|
13467
13988
|
const titleInput4 = typeof spec.title === "string" ? { text: spec.title } : spec.title ? { text: spec.title.text, subtitle: spec.title.subtitle } : void 0;
|
|
13468
13989
|
const xShow = spec.axes?.x?.show !== false;
|
|
13469
13990
|
const yShow = spec.axes?.y?.show !== false;
|
|
@@ -13669,6 +14190,12 @@ function labelFor(value) {
|
|
|
13669
14190
|
const key = toKey(value);
|
|
13670
14191
|
return key === "" ? "(blank)" : key;
|
|
13671
14192
|
}
|
|
14193
|
+
function textureShade(fill) {
|
|
14194
|
+
const lum = (0.299 * fill.r + 0.587 * fill.g + 0.114 * fill.b) / 255;
|
|
14195
|
+
const k = lum > 0.5 ? 0.68 : 1.4;
|
|
14196
|
+
const c = (n) => Math.max(0, Math.min(255, Math.round(n * k)));
|
|
14197
|
+
return rgbaToCss({ r: c(fill.r), g: c(fill.g), b: c(fill.b), a: 1 });
|
|
14198
|
+
}
|
|
13672
14199
|
function buildLeaves(treemap) {
|
|
13673
14200
|
const readCategory = accessor(treemap.encoding.category.field);
|
|
13674
14201
|
const readValue = accessor(treemap.encoding.value.field);
|
|
@@ -13752,13 +14279,20 @@ function tileProbe(treemap, tile) {
|
|
|
13752
14279
|
if (groupField) probe[groupField] = tile.group;
|
|
13753
14280
|
return probe;
|
|
13754
14281
|
}
|
|
13755
|
-
function drawRect2(ctx, pen, rect, fill, stroke) {
|
|
14282
|
+
function drawRect2(ctx, pen, rect, fill, fillCss, stroke) {
|
|
13756
14283
|
if (rect.width <= 0 || rect.height <= 0) return;
|
|
13757
14284
|
if (pen) {
|
|
13758
|
-
pen.rect(rect.x, rect.y, rect.width, rect.height, { fill
|
|
14285
|
+
pen.rect(rect.x, rect.y, rect.width, rect.height, { fill: fillCss, fillStyle: "solid" });
|
|
14286
|
+
pen.rect(rect.x, rect.y, rect.width, rect.height, {
|
|
14287
|
+
fill: textureShade(fill),
|
|
14288
|
+
fillStyle: "hachure",
|
|
14289
|
+
fillAlpha: 0.4,
|
|
14290
|
+
hachureGap: 7,
|
|
14291
|
+
stroke
|
|
14292
|
+
});
|
|
13759
14293
|
return;
|
|
13760
14294
|
}
|
|
13761
|
-
ctx.fillStyle =
|
|
14295
|
+
ctx.fillStyle = fillCss;
|
|
13762
14296
|
ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
|
|
13763
14297
|
ctx.strokeStyle = stroke;
|
|
13764
14298
|
ctx.lineWidth = 1;
|
|
@@ -13828,7 +14362,7 @@ function drawTreemap(surface, spec, tokens, size, context) {
|
|
|
13828
14362
|
const drawTile = (tile) => {
|
|
13829
14363
|
const alpha = emphasis ? emphasis.match(tileProbe(treemap, tile)) ? 1 : emphasis.dim : 1;
|
|
13830
14364
|
if (alpha !== 1) ctx.globalAlpha = alpha;
|
|
13831
|
-
drawRect2(ctx, pen, tile.rect, tile.fillCss, stroke);
|
|
14365
|
+
drawRect2(ctx, pen, tile.rect, tile.fill, tile.fillCss, stroke);
|
|
13832
14366
|
if (alpha !== 1) ctx.globalAlpha = 1;
|
|
13833
14367
|
};
|
|
13834
14368
|
ctx.save();
|
|
@@ -13865,11 +14399,13 @@ function drawTreemap(surface, spec, tokens, size, context) {
|
|
|
13865
14399
|
ctx.strokeRect(rect.x + 0.5, rect.y + 0.5, Math.max(0, rect.width - 1), Math.max(0, rect.height - 1));
|
|
13866
14400
|
}
|
|
13867
14401
|
if (headerH > 0) {
|
|
14402
|
+
const headerFont = fontString(tokens.font.size.tiny, tokens.font.family, tokens.font.weight.bold);
|
|
14403
|
+
const headerW = Math.max(0, rect.width - 10);
|
|
13868
14404
|
addOverlayText(surface, tokens, {
|
|
13869
14405
|
left: rect.x + 5,
|
|
13870
14406
|
top: rect.y + 4,
|
|
13871
|
-
width:
|
|
13872
|
-
text: group.label,
|
|
14407
|
+
width: headerW,
|
|
14408
|
+
text: ellipsize(group.label, headerW, headerFont),
|
|
13873
14409
|
color: tokens.color.text,
|
|
13874
14410
|
size: tokens.font.size.tiny,
|
|
13875
14411
|
weight: tokens.font.weight.bold,
|
|
@@ -13921,11 +14457,6 @@ function drawTreemap(surface, spec, tokens, size, context) {
|
|
|
13921
14457
|
function clamp8(value, min, max) {
|
|
13922
14458
|
return Math.max(min, Math.min(max, value));
|
|
13923
14459
|
}
|
|
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
14460
|
function valueField2(ref) {
|
|
13930
14461
|
return typeof ref === "number" ? null : ref.field;
|
|
13931
14462
|
}
|
|
@@ -13949,7 +14480,7 @@ function arcPoints(cx, cy, radius, startAngle, endAngle) {
|
|
|
13949
14480
|
}
|
|
13950
14481
|
return points;
|
|
13951
14482
|
}
|
|
13952
|
-
function strokePoints(ctx, pen, points, color, width) {
|
|
14483
|
+
function strokePoints(ctx, pen, points, color, width, cap2 = "butt") {
|
|
13953
14484
|
if (points.length < 2) return;
|
|
13954
14485
|
if (pen) {
|
|
13955
14486
|
pen.polyline(points, { stroke: color, strokeWidth: width });
|
|
@@ -13960,29 +14491,12 @@ function strokePoints(ctx, pen, points, color, width) {
|
|
|
13960
14491
|
ctx.moveTo(points[0].x, points[0].y);
|
|
13961
14492
|
for (let i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y);
|
|
13962
14493
|
ctx.lineWidth = width;
|
|
13963
|
-
ctx.lineCap =
|
|
14494
|
+
ctx.lineCap = cap2;
|
|
13964
14495
|
ctx.lineJoin = "round";
|
|
13965
14496
|
ctx.strokeStyle = color;
|
|
13966
14497
|
ctx.stroke();
|
|
13967
14498
|
ctx.restore();
|
|
13968
14499
|
}
|
|
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
14500
|
function drawCenteredMessage(surface, tokens, rect, text) {
|
|
13987
14501
|
addOverlayText(surface, tokens, {
|
|
13988
14502
|
left: rect.x,
|
|
@@ -13994,86 +14508,157 @@ function drawCenteredMessage(surface, tokens, rect, text) {
|
|
|
13994
14508
|
align: "center"
|
|
13995
14509
|
});
|
|
13996
14510
|
}
|
|
14511
|
+
var LEGEND_SWATCH3 = 11;
|
|
14512
|
+
function measureLegendWidth(entries, font) {
|
|
14513
|
+
let maxLabel = 0;
|
|
14514
|
+
for (const e of entries) maxLabel = Math.max(maxLabel, measureText(e.label, font).width);
|
|
14515
|
+
return Math.ceil(LEGEND_SWATCH3 + 6 + maxLabel + 4);
|
|
14516
|
+
}
|
|
14517
|
+
function drawGaugeLegend(surface, tokens, area2, entries) {
|
|
14518
|
+
if (entries.length === 0) return;
|
|
14519
|
+
const labelSize = tokens.font.size.small;
|
|
14520
|
+
const rowH = Math.round(labelSize * 1.7);
|
|
14521
|
+
const font = fontString(labelSize, tokens.font.family, tokens.font.weight.normal);
|
|
14522
|
+
const colW = measureLegendWidth(entries, font);
|
|
14523
|
+
const x = area2.x + area2.width - colW;
|
|
14524
|
+
let y = area2.y;
|
|
14525
|
+
if (surface.headless) {
|
|
14526
|
+
const ctx = surface.marks.ctx;
|
|
14527
|
+
for (const e of entries) {
|
|
14528
|
+
const midY = y + rowH / 2;
|
|
14529
|
+
paintLegendSwatch(ctx, x, midY, "square", e.color, LEGEND_SWATCH3);
|
|
14530
|
+
paintCanvasText(ctx, {
|
|
14531
|
+
x: x + LEGEND_SWATCH3 + 6,
|
|
14532
|
+
y: midY,
|
|
14533
|
+
text: e.label,
|
|
14534
|
+
font,
|
|
14535
|
+
color: tokens.color.text,
|
|
14536
|
+
size: labelSize,
|
|
14537
|
+
baseline: "middle"
|
|
14538
|
+
});
|
|
14539
|
+
y += rowH;
|
|
14540
|
+
}
|
|
14541
|
+
return;
|
|
14542
|
+
}
|
|
14543
|
+
for (const e of entries) {
|
|
14544
|
+
const row = document.createElement("div");
|
|
14545
|
+
row.style.position = "absolute";
|
|
14546
|
+
row.style.left = `${x}px`;
|
|
14547
|
+
row.style.top = `${y}px`;
|
|
14548
|
+
row.style.display = "flex";
|
|
14549
|
+
row.style.alignItems = "center";
|
|
14550
|
+
row.style.gap = "6px";
|
|
14551
|
+
row.style.pointerEvents = "none";
|
|
14552
|
+
const swatch = document.createElement("span");
|
|
14553
|
+
swatch.style.flex = "0 0 auto";
|
|
14554
|
+
swatch.style.width = `${LEGEND_SWATCH3}px`;
|
|
14555
|
+
swatch.style.height = `${LEGEND_SWATCH3}px`;
|
|
14556
|
+
swatch.style.borderRadius = "2px";
|
|
14557
|
+
swatch.style.background = e.color;
|
|
14558
|
+
const label = document.createElement("span");
|
|
14559
|
+
label.textContent = e.label;
|
|
14560
|
+
label.style.font = font;
|
|
14561
|
+
label.style.color = tokens.color.text;
|
|
14562
|
+
label.style.whiteSpace = "nowrap";
|
|
14563
|
+
row.appendChild(swatch);
|
|
14564
|
+
row.appendChild(label);
|
|
14565
|
+
surface.overlay.appendChild(row);
|
|
14566
|
+
y += rowH;
|
|
14567
|
+
}
|
|
14568
|
+
}
|
|
14569
|
+
function bandFillColor(gauge, value, tokens, accent) {
|
|
14570
|
+
const bands = gauge.bands;
|
|
14571
|
+
if (!bands?.length) return accent;
|
|
14572
|
+
const palette = tokens.color.palette;
|
|
14573
|
+
for (let i = 0; i < bands.length; i++) {
|
|
14574
|
+
if (value <= bands[i].to) return bands[i].color ?? palette[i % palette.length] ?? accent;
|
|
14575
|
+
}
|
|
14576
|
+
const last = bands.length - 1;
|
|
14577
|
+
return bands[last].color ?? palette[last % palette.length] ?? accent;
|
|
14578
|
+
}
|
|
13997
14579
|
function drawGauge(surface, spec, tokens, size, context) {
|
|
13998
14580
|
const gauge = spec;
|
|
13999
14581
|
const ctx = surface.marks.ctx;
|
|
14000
14582
|
const content = drawTitleBlock(surface, tokens, size, gauge.title);
|
|
14001
14583
|
const data = gauge.data ?? [];
|
|
14002
|
-
const value =
|
|
14003
|
-
const target =
|
|
14584
|
+
const value = resolveValueRef(gauge.value, data);
|
|
14585
|
+
const target = resolveValueRef(gauge.target, data);
|
|
14586
|
+
const compare2 = resolveValueRef(gauge.compare, data);
|
|
14004
14587
|
const min = gauge.min ?? 0;
|
|
14005
14588
|
const max = gauge.max;
|
|
14006
14589
|
if (value == null || !Number.isFinite(min) || !Number.isFinite(max) || max <= min || content.width <= 0 || content.height <= 0) {
|
|
14007
14590
|
drawCenteredMessage(surface, tokens, content, "No value");
|
|
14008
14591
|
return;
|
|
14009
14592
|
}
|
|
14593
|
+
const sketch = resolveSketch(spec);
|
|
14594
|
+
const pen = sketch ? new RoughPen(ctx, sketch) : null;
|
|
14595
|
+
const accent = tokens.color.palette[0] ?? tokens.color.accent;
|
|
14596
|
+
const fill = bandFillColor(gauge, value, tokens, accent);
|
|
14597
|
+
const track = tokens.dark ? withAlpha3(tokens.color.textMuted, 0.3) : tokens.color.border;
|
|
14598
|
+
const compareColor = tokens.color.textMuted;
|
|
14599
|
+
const markColor = tokens.dark ? withAlpha3("#ffffff", 0.92) : withAlpha3(tokens.color.text, 0.82);
|
|
14600
|
+
const targetMark = target != null && target >= min && target <= max ? target : null;
|
|
14010
14601
|
const valueText = formatValue(value, gauge.format);
|
|
14011
14602
|
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
14603
|
const captionSize = tokens.font.size.small;
|
|
14014
|
-
const
|
|
14015
|
-
const
|
|
14016
|
-
const
|
|
14017
|
-
|
|
14018
|
-
|
|
14019
|
-
|
|
14020
|
-
|
|
14021
|
-
|
|
14604
|
+
const captionReserve = captionSize + 16;
|
|
14605
|
+
const showLegend = gauge.legend ?? compare2 != null;
|
|
14606
|
+
const legendEntries = [];
|
|
14607
|
+
if (showLegend) {
|
|
14608
|
+
legendEntries.push({ label: gauge.valueLabel ?? "Actual", color: fill, symbol: "square" });
|
|
14609
|
+
if (compare2 != null) {
|
|
14610
|
+
legendEntries.push({ label: gauge.compareLabel ?? "Prior period", color: compareColor, symbol: "square" });
|
|
14611
|
+
}
|
|
14612
|
+
legendEntries.push({ label: gauge.trackLabel ?? "Target", color: targetMark != null ? markColor : track, symbol: "square" });
|
|
14613
|
+
}
|
|
14614
|
+
const legendFont = fontString(tokens.font.size.small, tokens.font.family, tokens.font.weight.normal);
|
|
14615
|
+
const legendW = legendEntries.length ? measureLegendWidth(legendEntries, legendFont) + 12 : 0;
|
|
14616
|
+
const dialW = Math.max(40, content.width - legendW);
|
|
14617
|
+
const availH = Math.max(0, content.height - captionReserve);
|
|
14618
|
+
const provR = Math.max(18, Math.min(dialW / 2, availH));
|
|
14619
|
+
const trackWidth = clamp8(provR * 0.17, 14, 54);
|
|
14620
|
+
const edgeGuard = trackWidth / 2 + clamp8(trackWidth * 0.3, 4, 10) + 1;
|
|
14621
|
+
const radius = Math.max(18, Math.min(provR, dialW / 2 - edgeGuard, availH - edgeGuard));
|
|
14622
|
+
const cx = content.x + dialW / 2;
|
|
14623
|
+
const blockH = radius + trackWidth / 2 + captionReserve;
|
|
14624
|
+
const topMargin = Math.max(0, (content.height - blockH) / 2);
|
|
14625
|
+
const cy = content.y + topMargin + radius + trackWidth / 2;
|
|
14022
14626
|
const toAngle = (v) => Math.PI - clamp8((v - min) / (max - min), 0, 1) * Math.PI;
|
|
14023
|
-
const
|
|
14024
|
-
const
|
|
14025
|
-
const
|
|
14627
|
+
const sweepT = context?.sweepProgress ?? 1;
|
|
14628
|
+
const from = context?.valueFrom ?? min;
|
|
14629
|
+
const displayValue = from + (value - from) * sweepT;
|
|
14630
|
+
const shownText = formatValue(displayValue, gauge.format);
|
|
14631
|
+
const valueAngle = toAngle(displayValue);
|
|
14026
14632
|
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);
|
|
14633
|
+
if (valueAngle < Math.PI) {
|
|
14634
|
+
strokePoints(ctx, pen, arcPoints(cx, cy, radius, Math.PI, valueAngle), fill, trackWidth);
|
|
14635
|
+
}
|
|
14636
|
+
if (compare2 != null) {
|
|
14637
|
+
const innerWidth = clamp8(trackWidth * 0.42, 6, 22);
|
|
14638
|
+
const innerRadius = Math.max(radius * 0.35, radius - trackWidth / 2 - 2 - innerWidth / 2);
|
|
14639
|
+
const compareFrom = context?.compareFrom ?? min;
|
|
14640
|
+
const displayCompare = compareFrom + (compare2 - compareFrom) * sweepT;
|
|
14641
|
+
const compareAngle = toAngle(displayCompare);
|
|
14642
|
+
if (compareAngle < Math.PI) {
|
|
14643
|
+
strokePoints(ctx, pen, arcPoints(cx, cy, innerRadius, Math.PI, compareAngle), compareColor, innerWidth);
|
|
14644
|
+
}
|
|
14645
|
+
}
|
|
14646
|
+
if (targetMark != null) {
|
|
14647
|
+
const a = toAngle(targetMark);
|
|
14648
|
+
const overhang = clamp8(trackWidth * 0.3, 4, 10);
|
|
14649
|
+
const outer = pointOnArc(cx, cy, radius + trackWidth / 2 + overhang, a);
|
|
14650
|
+
const inner = pointOnArc(cx, cy, radius - trackWidth / 2 - overhang, a);
|
|
14651
|
+
strokePoints(ctx, pen, [outer, inner], markColor, 3, "round");
|
|
14652
|
+
}
|
|
14653
|
+
if (legendEntries.length) {
|
|
14654
|
+
drawGaugeLegend(surface, tokens, { x: content.x, y: content.y, width: content.width, height: content.height }, legendEntries);
|
|
14655
|
+
}
|
|
14656
|
+
const valueSize = clamp8(radius * 0.5, 22, 72);
|
|
14072
14657
|
addOverlayText(surface, tokens, {
|
|
14073
14658
|
left: content.x,
|
|
14074
|
-
top:
|
|
14075
|
-
width:
|
|
14076
|
-
text:
|
|
14659
|
+
top: cy - radius * 0.32 - valueSize / 2,
|
|
14660
|
+
width: dialW,
|
|
14661
|
+
text: shownText,
|
|
14077
14662
|
color: tokens.color.text,
|
|
14078
14663
|
size: valueSize,
|
|
14079
14664
|
weight: tokens.font.weight.bold,
|
|
@@ -14081,20 +14666,20 @@ function drawGauge(surface, spec, tokens, size, context) {
|
|
|
14081
14666
|
});
|
|
14082
14667
|
addOverlayText(surface, tokens, {
|
|
14083
14668
|
left: content.x,
|
|
14084
|
-
top:
|
|
14085
|
-
width:
|
|
14669
|
+
top: cy + 8,
|
|
14670
|
+
width: dialW,
|
|
14086
14671
|
text: caption,
|
|
14087
14672
|
color: tokens.color.textMuted,
|
|
14088
14673
|
size: captionSize,
|
|
14089
14674
|
align: "center"
|
|
14090
14675
|
});
|
|
14091
14676
|
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;
|
|
14677
|
+
const withinDial = px >= cx - radius - trackWidth && px <= cx + radius + trackWidth && py >= cy - radius - trackWidth && py <= cy + captionReserve + 4;
|
|
14096
14678
|
if (!withinDial) return null;
|
|
14097
|
-
const rows = [{ label: "value", value: valueText }];
|
|
14679
|
+
const rows = [{ label: gauge.valueLabel ?? "value", value: valueText }];
|
|
14680
|
+
if (compare2 != null) {
|
|
14681
|
+
rows.push({ label: gauge.compareLabel ?? "prior period", value: formatValue(compare2, gauge.format), muted: true });
|
|
14682
|
+
}
|
|
14098
14683
|
if (target != null) rows.push({ label: "target", value: formatValue(target, gauge.format), muted: true });
|
|
14099
14684
|
return {
|
|
14100
14685
|
key: "value",
|
|
@@ -14122,7 +14707,7 @@ function titleText2(title2) {
|
|
|
14122
14707
|
function valueField3(ref) {
|
|
14123
14708
|
return typeof ref === "number" ? null : ref.field;
|
|
14124
14709
|
}
|
|
14125
|
-
function
|
|
14710
|
+
function resolveValue(ref, data) {
|
|
14126
14711
|
if (ref == null) return null;
|
|
14127
14712
|
if (typeof ref === "number") return Number.isFinite(ref) ? ref : null;
|
|
14128
14713
|
return aggregateValues((data ?? []).map(accessor(ref.field)), ref.aggregate ?? "sum");
|
|
@@ -14154,8 +14739,8 @@ function drawBullet(surface, spec, tokens, size, context) {
|
|
|
14154
14739
|
const ctx = surface.marks.ctx;
|
|
14155
14740
|
const content = drawTitleBlock(surface, tokens, size, bullet.title);
|
|
14156
14741
|
const data = bullet.data ?? [];
|
|
14157
|
-
const value =
|
|
14158
|
-
const target =
|
|
14742
|
+
const value = resolveValue(bullet.value, data);
|
|
14743
|
+
const target = resolveValue(bullet.target, data);
|
|
14159
14744
|
const caption = bullet.label ?? titleText2(bullet.title) ?? valueField3(bullet.value) ?? "Value";
|
|
14160
14745
|
const fmt = bullet.format;
|
|
14161
14746
|
const finiteRanges = (bullet.ranges ?? []).filter((r) => Number.isFinite(r)).sort((a, b) => a - b);
|
|
@@ -14176,10 +14761,7 @@ function drawBullet(surface, spec, tokens, size, context) {
|
|
|
14176
14761
|
}
|
|
14177
14762
|
const labelFont = fontString(tokens.font.size.small, tokens.font.family, tokens.font.weight.bold);
|
|
14178
14763
|
const valueFont = fontString(tokens.font.size.small, tokens.font.family, tokens.font.weight.normal);
|
|
14179
|
-
const labelW = Math.
|
|
14180
|
-
Math.max(72, content.width * 0.18, measureText(caption, labelFont).width + 18),
|
|
14181
|
-
Math.max(72, content.width * 0.38)
|
|
14182
|
-
);
|
|
14764
|
+
const labelW = clamp9(measureText(caption, labelFont).width + 14, 44, Math.max(72, content.width * 0.4));
|
|
14183
14765
|
const axisReserve = Math.max(24, tokens.font.size.tiny * 2);
|
|
14184
14766
|
const availableTrackW = Math.max(0, content.width - labelW);
|
|
14185
14767
|
const trackW = Math.max(0, availableTrackW - 4);
|
|
@@ -14297,7 +14879,6 @@ function drawBullet(surface, spec, tokens, size, context) {
|
|
|
14297
14879
|
// src/charts/calendarHeatmap.ts
|
|
14298
14880
|
var DAY_MS2 = 864e5;
|
|
14299
14881
|
var GAP2 = 2;
|
|
14300
|
-
var MAX_CELL = 16;
|
|
14301
14882
|
var WEEKDAY_LABELS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
14302
14883
|
var MONTH_LABELS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
14303
14884
|
function pad22(n) {
|
|
@@ -14320,9 +14901,9 @@ function addDays(date, days) {
|
|
|
14320
14901
|
d.setDate(d.getDate() + days);
|
|
14321
14902
|
return d;
|
|
14322
14903
|
}
|
|
14323
|
-
function drawRoundedCell2(ctx, x, y,
|
|
14904
|
+
function drawRoundedCell2(ctx, x, y, w, h, radius) {
|
|
14324
14905
|
ctx.beginPath();
|
|
14325
|
-
roundedRect(ctx, x, y,
|
|
14906
|
+
roundedRect(ctx, x, y, w, h, radius);
|
|
14326
14907
|
ctx.fill();
|
|
14327
14908
|
}
|
|
14328
14909
|
function noData(surface, tokens, content) {
|
|
@@ -14338,7 +14919,7 @@ function noData(surface, tokens, content) {
|
|
|
14338
14919
|
}
|
|
14339
14920
|
function normalizeDays(days) {
|
|
14340
14921
|
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 }));
|
|
14922
|
+
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
14923
|
}
|
|
14343
14924
|
const out = [];
|
|
14344
14925
|
const dayMap = days;
|
|
@@ -14360,19 +14941,20 @@ function buildCalendarGrid(days) {
|
|
|
14360
14941
|
const start = addDays(minDay, -minDay.getDay());
|
|
14361
14942
|
const end = addDays(maxDay, 6 - maxDay.getDay());
|
|
14362
14943
|
const weeks = Math.floor(diffDays(end, start) / 7) + 1;
|
|
14363
|
-
const byValue = new Map(values.map((d) => [d.isoDate, d.value]));
|
|
14944
|
+
const byValue = new Map(values.map((d) => [d.isoDate, { value: d.value, row: d.row }]));
|
|
14364
14945
|
const cells = [];
|
|
14365
14946
|
const byIsoDate = /* @__PURE__ */ new Map();
|
|
14366
14947
|
for (let d = start; diffDays(end, d) >= 0; d = addDays(d, 1)) {
|
|
14367
14948
|
const isoDate = isoDay(d);
|
|
14368
|
-
const
|
|
14949
|
+
const dayValue = byValue.get(isoDate);
|
|
14369
14950
|
const cell = {
|
|
14370
14951
|
isoDate,
|
|
14371
14952
|
date: d,
|
|
14372
14953
|
week: Math.floor(diffDays(d, start) / 7),
|
|
14373
14954
|
weekday: d.getDay(),
|
|
14374
|
-
value,
|
|
14375
|
-
|
|
14955
|
+
value: dayValue?.value,
|
|
14956
|
+
row: dayValue?.row,
|
|
14957
|
+
present: dayValue !== void 0
|
|
14376
14958
|
};
|
|
14377
14959
|
cells.push(cell);
|
|
14378
14960
|
byIsoDate.set(isoDate, cell);
|
|
@@ -14391,9 +14973,9 @@ function collectDays(spec) {
|
|
|
14391
14973
|
const isoDate = isoDay(day);
|
|
14392
14974
|
const current = totals.get(isoDate);
|
|
14393
14975
|
if (current) current.value += value;
|
|
14394
|
-
else totals.set(isoDate, { date: day, value });
|
|
14976
|
+
else totals.set(isoDate, { date: day, value, row });
|
|
14395
14977
|
}
|
|
14396
|
-
return [...totals.entries()].map(([isoDate, { date, value }]) => ({ isoDate, date, value }));
|
|
14978
|
+
return [...totals.entries()].map(([isoDate, { date, value, row }]) => ({ isoDate, date, value, row }));
|
|
14397
14979
|
}
|
|
14398
14980
|
function monthAnchors(grid) {
|
|
14399
14981
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -14417,6 +14999,8 @@ function humanDate(date) {
|
|
|
14417
14999
|
function drawCalendarHeatmap(surface, spec, tokens, size, context) {
|
|
14418
15000
|
const cal = spec;
|
|
14419
15001
|
const ctx = surface.marks.ctx;
|
|
15002
|
+
const emphasis = context?.emphasis ?? null;
|
|
15003
|
+
const dateField = cal.encoding.date.field;
|
|
14420
15004
|
const content = drawTitleBlock(surface, tokens, size, cal.title);
|
|
14421
15005
|
const days = collectDays(cal);
|
|
14422
15006
|
const grid = buildCalendarGrid(days);
|
|
@@ -14430,16 +15014,26 @@ function drawCalendarHeatmap(surface, spec, tokens, size, context) {
|
|
|
14430
15014
|
const labelHeight = Math.ceil(labelSize * 1.7) + 2;
|
|
14431
15015
|
const availableW = Math.max(0, content.width - leftGutter);
|
|
14432
15016
|
const availableH = Math.max(0, content.height - labelHeight);
|
|
14433
|
-
const
|
|
14434
|
-
|
|
15017
|
+
const MAX_ASPECT = 1.75;
|
|
15018
|
+
const rawW = (availableW - GAP2 * Math.max(0, grid.weeks - 1)) / grid.weeks;
|
|
15019
|
+
const rawH = (availableH - GAP2 * 6) / 7;
|
|
15020
|
+
const cellW = Math.floor(Math.max(1, Math.min(rawW, rawH * MAX_ASPECT)));
|
|
15021
|
+
const cellH = Math.floor(Math.max(1, Math.min(rawH, rawW * MAX_ASPECT)));
|
|
15022
|
+
if (!Number.isFinite(cellW) || !Number.isFinite(cellH) || cellW <= 0 || cellH <= 0) {
|
|
14435
15023
|
noData(surface, tokens, content);
|
|
14436
15024
|
return;
|
|
14437
15025
|
}
|
|
15026
|
+
const stepX = cellW + GAP2;
|
|
15027
|
+
const stepY = cellH + GAP2;
|
|
15028
|
+
const gridW = grid.weeks * cellW + Math.max(0, grid.weeks - 1) * GAP2;
|
|
15029
|
+
const gridH = 7 * cellH + 6 * GAP2;
|
|
15030
|
+
const vpad = Math.max(0, (availableH - gridH) / 2);
|
|
15031
|
+
const hpad = Math.max(0, (availableW - gridW) / 2);
|
|
14438
15032
|
const gridRect = {
|
|
14439
|
-
x: content.x + leftGutter,
|
|
14440
|
-
y: content.y + labelHeight,
|
|
14441
|
-
width:
|
|
14442
|
-
height:
|
|
15033
|
+
x: content.x + leftGutter + hpad,
|
|
15034
|
+
y: content.y + labelHeight + vpad,
|
|
15035
|
+
width: gridW,
|
|
15036
|
+
height: gridH
|
|
14443
15037
|
};
|
|
14444
15038
|
const values = days.map((d) => d.value);
|
|
14445
15039
|
const min = Math.min(...values);
|
|
@@ -14448,33 +15042,35 @@ function drawCalendarHeatmap(surface, spec, tokens, size, context) {
|
|
|
14448
15042
|
const cscale = sequentialColorScale({ domain: [min, max], interpolator: interp });
|
|
14449
15043
|
const sketch = resolveSketch(spec);
|
|
14450
15044
|
const pen = sketch ? new RoughPen(ctx, sketch) : null;
|
|
14451
|
-
const radius = Math.min(3, tokens.radius.sm,
|
|
15045
|
+
const radius = Math.min(3, tokens.radius.sm, Math.min(cellW, cellH) / 3);
|
|
14452
15046
|
const laidOut = /* @__PURE__ */ new Map();
|
|
14453
15047
|
ctx.save();
|
|
14454
15048
|
for (const c of grid.cells) {
|
|
14455
|
-
const x = gridRect.x + c.week *
|
|
14456
|
-
const y = gridRect.y + c.weekday *
|
|
15049
|
+
const x = gridRect.x + c.week * stepX;
|
|
15050
|
+
const y = gridRect.y + c.weekday * stepY;
|
|
14457
15051
|
const fill = c.present && c.value !== void 0 ? rgbaToCss(cscale.map(c.value)) : tokens.color.surface;
|
|
14458
|
-
laidOut.set(c.isoDate, { ...c, x, y,
|
|
15052
|
+
laidOut.set(c.isoDate, { ...c, x, y, w: cellW, h: cellH, fill });
|
|
15053
|
+
ctx.globalAlpha = c.present ? rowAlpha(emphasis, c.row ?? { [dateField]: c.isoDate }) : 1;
|
|
14459
15054
|
if (pen) {
|
|
14460
|
-
pen.rect(x, y,
|
|
15055
|
+
pen.rect(x, y, cellW, cellH, {
|
|
14461
15056
|
fill,
|
|
14462
15057
|
fillStyle: "solid",
|
|
14463
15058
|
stroke: c.present ? fill : tokens.color.border
|
|
14464
15059
|
});
|
|
14465
15060
|
} else {
|
|
14466
15061
|
ctx.fillStyle = fill;
|
|
14467
|
-
drawRoundedCell2(ctx, x, y,
|
|
15062
|
+
drawRoundedCell2(ctx, x, y, cellW, cellH, radius);
|
|
14468
15063
|
ctx.strokeStyle = tokens.color.border;
|
|
14469
15064
|
ctx.lineWidth = 1;
|
|
14470
|
-
ctx.strokeRect(x + 0.5, y + 0.5, Math.max(0,
|
|
15065
|
+
ctx.strokeRect(x + 0.5, y + 0.5, Math.max(0, cellW - 1), Math.max(0, cellH - 1));
|
|
14471
15066
|
}
|
|
15067
|
+
ctx.globalAlpha = 1;
|
|
14472
15068
|
}
|
|
14473
15069
|
ctx.restore();
|
|
14474
15070
|
for (const m of monthAnchors(grid)) {
|
|
14475
15071
|
addOverlayText(surface, tokens, {
|
|
14476
|
-
left: gridRect.x + m.week *
|
|
14477
|
-
top:
|
|
15072
|
+
left: gridRect.x + m.week * stepX,
|
|
15073
|
+
top: gridRect.y - labelHeight,
|
|
14478
15074
|
text: m.label,
|
|
14479
15075
|
color: tokens.color.textMuted,
|
|
14480
15076
|
size: labelSize
|
|
@@ -14483,7 +15079,7 @@ function drawCalendarHeatmap(surface, spec, tokens, size, context) {
|
|
|
14483
15079
|
for (const weekday of [1, 3, 5]) {
|
|
14484
15080
|
addOverlayText(surface, tokens, {
|
|
14485
15081
|
left: content.x,
|
|
14486
|
-
top: gridRect.y + weekday *
|
|
15082
|
+
top: gridRect.y + weekday * stepY + cellH / 2,
|
|
14487
15083
|
width: Math.max(0, leftGutter - tokens.spacing.xs),
|
|
14488
15084
|
text: WEEKDAY_LABELS[weekday],
|
|
14489
15085
|
color: tokens.color.textMuted,
|
|
@@ -14495,11 +15091,11 @@ function drawCalendarHeatmap(surface, spec, tokens, size, context) {
|
|
|
14495
15091
|
const colorLabel = cal.encoding.color.title ?? cal.encoding.color.field;
|
|
14496
15092
|
const hitCell = (px, py) => {
|
|
14497
15093
|
if (px < gridRect.x || py < gridRect.y || px > gridRect.x + gridRect.width || py > gridRect.y + gridRect.height) return null;
|
|
14498
|
-
const week = Math.floor((px - gridRect.x) /
|
|
14499
|
-
const weekday = Math.floor((py - gridRect.y) /
|
|
14500
|
-
const x = gridRect.x + week *
|
|
14501
|
-
const y = gridRect.y + weekday *
|
|
14502
|
-
if (week < 0 || week >= grid.weeks || weekday < 0 || weekday > 6 || px > x +
|
|
15094
|
+
const week = Math.floor((px - gridRect.x) / stepX);
|
|
15095
|
+
const weekday = Math.floor((py - gridRect.y) / stepY);
|
|
15096
|
+
const x = gridRect.x + week * stepX;
|
|
15097
|
+
const y = gridRect.y + weekday * stepY;
|
|
15098
|
+
if (week < 0 || week >= grid.weeks || weekday < 0 || weekday > 6 || px > x + cellW || py > y + cellH) return null;
|
|
14503
15099
|
const match = grid.cells.find((c) => c.week === week && c.weekday === weekday);
|
|
14504
15100
|
if (!match || !match.present) return null;
|
|
14505
15101
|
return laidOut.get(match.isoDate) ?? null;
|
|
@@ -14528,7 +15124,6 @@ function drawCalendarHeatmap(surface, spec, tokens, size, context) {
|
|
|
14528
15124
|
}
|
|
14529
15125
|
|
|
14530
15126
|
// src/runtime/waterfall.ts
|
|
14531
|
-
var DEFAULT_PADDING4 = { top: 12, right: 16, bottom: 12, left: 12 };
|
|
14532
15127
|
function labelFor2(value) {
|
|
14533
15128
|
if (value == null) return "";
|
|
14534
15129
|
if (value instanceof Date) return value.toISOString().slice(0, 10);
|
|
@@ -14636,7 +15231,7 @@ function buildWaterfallModel(spec, tokens, opts) {
|
|
|
14636
15231
|
const fmt = spec.encoding.value.format ?? spec.axes?.y?.format;
|
|
14637
15232
|
const yLabels = yTickValues.map((v) => fmt ? formatValue(v, fmt) : formatNumber2(v, ","));
|
|
14638
15233
|
const categories = bars.map((b) => b.label);
|
|
14639
|
-
const padding = { ...
|
|
15234
|
+
const padding = { ...NO_PADDING, ...spec.padding };
|
|
14640
15235
|
const xShow = spec.axes?.x?.show !== false;
|
|
14641
15236
|
const yShow = spec.axes?.y?.show !== false;
|
|
14642
15237
|
const xTitle = spec.axes?.x?.title ?? spec.encoding.stage.title;
|
|
@@ -14746,13 +15341,15 @@ function buildWaterfallModel(spec, tokens, opts) {
|
|
|
14746
15341
|
}
|
|
14747
15342
|
|
|
14748
15343
|
// src/charts/waterfall.ts
|
|
14749
|
-
function drawConnectors(surface, model) {
|
|
15344
|
+
function drawConnectors(surface, model, alphaOf) {
|
|
14750
15345
|
if (model.connectors.length === 0) return;
|
|
14751
15346
|
const ctx = surface.marks.ctx;
|
|
14752
15347
|
const color = model.tokens.color.axis;
|
|
14753
15348
|
if (model.sketch) {
|
|
14754
15349
|
const pen = new RoughPen(ctx, model.sketch);
|
|
14755
|
-
for (
|
|
15350
|
+
for (let i = 0; i < model.connectors.length; i += 1) {
|
|
15351
|
+
const c = model.connectors[i];
|
|
15352
|
+
ctx.globalAlpha = alphaOf(model.bars[i]);
|
|
14756
15353
|
pen.polyline(
|
|
14757
15354
|
[
|
|
14758
15355
|
{ x: c.x1, y: c.y },
|
|
@@ -14760,6 +15357,7 @@ function drawConnectors(surface, model) {
|
|
|
14760
15357
|
],
|
|
14761
15358
|
{ stroke: color }
|
|
14762
15359
|
);
|
|
15360
|
+
ctx.globalAlpha = 1;
|
|
14763
15361
|
}
|
|
14764
15362
|
return;
|
|
14765
15363
|
}
|
|
@@ -14767,23 +15365,28 @@ function drawConnectors(surface, model) {
|
|
|
14767
15365
|
ctx.strokeStyle = color;
|
|
14768
15366
|
ctx.lineWidth = 1;
|
|
14769
15367
|
ctx.setLineDash([2, 2]);
|
|
14770
|
-
|
|
14771
|
-
|
|
15368
|
+
for (let i = 0; i < model.connectors.length; i += 1) {
|
|
15369
|
+
const c = model.connectors[i];
|
|
14772
15370
|
const y = Math.round(c.y) + 0.5;
|
|
15371
|
+
ctx.globalAlpha = alphaOf(model.bars[i]);
|
|
15372
|
+
ctx.beginPath();
|
|
14773
15373
|
ctx.moveTo(c.x1, y);
|
|
14774
15374
|
ctx.lineTo(c.x2, y);
|
|
15375
|
+
ctx.stroke();
|
|
15376
|
+
ctx.globalAlpha = 1;
|
|
14775
15377
|
}
|
|
14776
|
-
ctx.stroke();
|
|
14777
15378
|
ctx.restore();
|
|
14778
15379
|
}
|
|
14779
|
-
function drawBars2(surface, model) {
|
|
15380
|
+
function drawBars2(surface, model, alphaOf) {
|
|
14780
15381
|
const ctx = surface.marks.ctx;
|
|
14781
15382
|
const pen = model.sketch ? new RoughPen(ctx, model.sketch) : null;
|
|
14782
15383
|
for (const bar of model.bars) {
|
|
14783
15384
|
const { rect } = bar;
|
|
14784
15385
|
if (rect.width <= 0 || rect.height <= 0) continue;
|
|
15386
|
+
ctx.globalAlpha = alphaOf(bar);
|
|
14785
15387
|
if (pen) {
|
|
14786
15388
|
pen.rect(rect.x, rect.y, rect.width, rect.height, { stroke: bar.color, fill: bar.color });
|
|
15389
|
+
ctx.globalAlpha = 1;
|
|
14787
15390
|
continue;
|
|
14788
15391
|
}
|
|
14789
15392
|
const r = Math.min(model.cornerRadius, rect.width / 2, rect.height / 2);
|
|
@@ -14791,6 +15394,7 @@ function drawBars2(surface, model) {
|
|
|
14791
15394
|
roundedRect(ctx, rect.x, rect.y, rect.width, rect.height, [r, r, r, r]);
|
|
14792
15395
|
ctx.fillStyle = bar.color;
|
|
14793
15396
|
ctx.fill();
|
|
15397
|
+
ctx.globalAlpha = 1;
|
|
14794
15398
|
}
|
|
14795
15399
|
}
|
|
14796
15400
|
function barLabelText(bar, format) {
|
|
@@ -14820,17 +15424,19 @@ function drawLabels(surface, model) {
|
|
|
14820
15424
|
});
|
|
14821
15425
|
}
|
|
14822
15426
|
}
|
|
14823
|
-
function drawWaterfall(surface, spec, tokens, size,
|
|
15427
|
+
function drawWaterfall(surface, spec, tokens, size, context) {
|
|
14824
15428
|
const waterfall = spec;
|
|
14825
15429
|
const model = buildWaterfallModel(waterfall, tokens, size);
|
|
15430
|
+
const emphasis = context?.emphasis ?? null;
|
|
15431
|
+
const stageField = waterfall.encoding.stage.field;
|
|
15432
|
+
const alphaOf = (bar) => bar.stageValue === void 0 ? 1 : rowAlpha(emphasis, { [stageField]: bar.stageValue });
|
|
14826
15433
|
drawAxesUnderlay(surface, model.base);
|
|
14827
|
-
drawConnectors(surface, model);
|
|
14828
|
-
drawBars2(surface, model);
|
|
15434
|
+
drawConnectors(surface, model, alphaOf);
|
|
15435
|
+
drawBars2(surface, model, alphaOf);
|
|
14829
15436
|
drawLabels(surface, model);
|
|
14830
15437
|
drawOverlay(surface, model.base);
|
|
14831
15438
|
if (model.bars.length === 0) return;
|
|
14832
15439
|
const { plot, format } = model;
|
|
14833
|
-
const stageField = waterfall.encoding.stage.field;
|
|
14834
15440
|
const valueLabel2 = waterfall.encoding.value.title ?? waterfall.encoding.value.field;
|
|
14835
15441
|
const fmtNum = (v) => format ? formatValue(v, format) : formatNumber2(v, ",");
|
|
14836
15442
|
const hitBar = (px, py) => {
|
|
@@ -14866,7 +15472,6 @@ function drawWaterfall(surface, spec, tokens, size, _context) {
|
|
|
14866
15472
|
}
|
|
14867
15473
|
|
|
14868
15474
|
// src/runtime/slope.ts
|
|
14869
|
-
var DEFAULT_PADDING5 = { top: 16, right: 76, bottom: 26, left: 76 };
|
|
14870
15475
|
function labelFor3(value) {
|
|
14871
15476
|
if (value == null) return "";
|
|
14872
15477
|
if (value instanceof Date) return value.toISOString().slice(0, 10);
|
|
@@ -14902,7 +15507,44 @@ function buildSlopeModel(spec, tokens, opts) {
|
|
|
14902
15507
|
];
|
|
14903
15508
|
const fmt = spec.format ?? spec.encoding.y.format ?? spec.axes?.y?.format;
|
|
14904
15509
|
const yLabels = yTickValues.map((v) => fmtTick2(v, fmt));
|
|
14905
|
-
const
|
|
15510
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
15511
|
+
for (const row of data) {
|
|
15512
|
+
const rawSeries = readSeries(row);
|
|
15513
|
+
const key = toKey(rawSeries);
|
|
15514
|
+
let group = grouped.get(key);
|
|
15515
|
+
if (!group) {
|
|
15516
|
+
group = { key, label: labelFor3(rawSeries), value: rawSeries, values: /* @__PURE__ */ new Map() };
|
|
15517
|
+
grouped.set(key, group);
|
|
15518
|
+
}
|
|
15519
|
+
const rawX = readX(row);
|
|
15520
|
+
const catKey = labelFor3(rawX);
|
|
15521
|
+
const value = toNumber(readY(row));
|
|
15522
|
+
if (!Number.isNaN(value) && !group.values.has(catKey)) group.values.set(catKey, value);
|
|
15523
|
+
}
|
|
15524
|
+
const showLabels = spec.labels !== false;
|
|
15525
|
+
let leftLabelW = 0;
|
|
15526
|
+
let rightLabelW = 0;
|
|
15527
|
+
if (showLabels) {
|
|
15528
|
+
const boldTiny = fontString(tokens.font.size.tiny, tokens.font.family, tokens.font.weight.bold);
|
|
15529
|
+
const normTiny = fontString(tokens.font.size.tiny, tokens.font.family, tokens.font.weight.normal);
|
|
15530
|
+
for (const group of grouped.values()) {
|
|
15531
|
+
const present = categories.filter((c) => group.values.has(c));
|
|
15532
|
+
if (present.length === 0) continue;
|
|
15533
|
+
const firstV = group.values.get(present[0]);
|
|
15534
|
+
const lastV = group.values.get(present[present.length - 1]);
|
|
15535
|
+
leftLabelW = Math.max(
|
|
15536
|
+
leftLabelW,
|
|
15537
|
+
measureText(group.label, boldTiny).width,
|
|
15538
|
+
measureText(fmtTick2(firstV, fmt), normTiny).width
|
|
15539
|
+
);
|
|
15540
|
+
rightLabelW = Math.max(rightLabelW, measureText(fmtTick2(lastV, fmt), normTiny).width);
|
|
15541
|
+
}
|
|
15542
|
+
leftLabelW = Math.ceil(leftLabelW);
|
|
15543
|
+
rightLabelW = Math.ceil(rightLabelW);
|
|
15544
|
+
}
|
|
15545
|
+
const labelBand = tokens.font.size.tiny + 4;
|
|
15546
|
+
const basePadding = showLabels ? { top: labelBand, right: rightLabelW + 8, bottom: labelBand, left: leftLabelW + 8 } : { top: 0, right: 0, bottom: 0, left: 0 };
|
|
15547
|
+
const padding = { ...basePadding, ...spec.padding };
|
|
14906
15548
|
const xShow = spec.axes?.x?.show !== false;
|
|
14907
15549
|
const yShow = spec.axes?.y?.show === true;
|
|
14908
15550
|
const xTitle = spec.axes?.x?.title ?? spec.encoding.x.title;
|
|
@@ -14919,20 +15561,6 @@ function buildSlopeModel(spec, tokens, opts) {
|
|
|
14919
15561
|
const plot = frame.plot;
|
|
14920
15562
|
const point = pointScale({ domain: categories, range: [plot.x, plot.x + plot.width], padding: 0.5 });
|
|
14921
15563
|
const yScale = linearScale({ domain: yDomain, range: [plot.y + plot.height, plot.y] });
|
|
14922
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
14923
|
-
for (const row of data) {
|
|
14924
|
-
const rawSeries = readSeries(row);
|
|
14925
|
-
const key = toKey(rawSeries);
|
|
14926
|
-
let group = grouped.get(key);
|
|
14927
|
-
if (!group) {
|
|
14928
|
-
group = { key, label: labelFor3(rawSeries), value: rawSeries, values: /* @__PURE__ */ new Map() };
|
|
14929
|
-
grouped.set(key, group);
|
|
14930
|
-
}
|
|
14931
|
-
const rawX = readX(row);
|
|
14932
|
-
const catKey = labelFor3(rawX);
|
|
14933
|
-
const value = toNumber(readY(row));
|
|
14934
|
-
if (!Number.isNaN(value) && !group.values.has(catKey)) group.values.set(catKey, value);
|
|
14935
|
-
}
|
|
14936
15564
|
const palette = ordinalColorScale({ domain: [...grouped.keys()], palette: tokens.color.palette });
|
|
14937
15565
|
const series = [...grouped.values()].map((group) => {
|
|
14938
15566
|
const points = categories.map((catKey) => {
|
|
@@ -15001,7 +15629,9 @@ function buildSlopeModel(spec, tokens, opts) {
|
|
|
15001
15629
|
series,
|
|
15002
15630
|
categories,
|
|
15003
15631
|
format: fmt,
|
|
15004
|
-
showLabels
|
|
15632
|
+
showLabels,
|
|
15633
|
+
leftLabelW,
|
|
15634
|
+
rightLabelW,
|
|
15005
15635
|
base,
|
|
15006
15636
|
sketch
|
|
15007
15637
|
};
|
|
@@ -15009,13 +15639,15 @@ function buildSlopeModel(spec, tokens, opts) {
|
|
|
15009
15639
|
|
|
15010
15640
|
// src/charts/slope.ts
|
|
15011
15641
|
var HIT_RADIUS2 = 8;
|
|
15012
|
-
var LABEL_WIDTH = 68;
|
|
15013
15642
|
function fmtValue(v, format) {
|
|
15014
15643
|
return format ? formatValue(v, format) : formatNumber2(v, ",");
|
|
15015
15644
|
}
|
|
15016
|
-
function drawLines(surface, model) {
|
|
15645
|
+
function drawLines(surface, model, alphaOf) {
|
|
15017
15646
|
const ctx = surface.marks.ctx;
|
|
15018
15647
|
const pen = model.sketch ? new RoughPen(ctx, model.sketch) : null;
|
|
15648
|
+
const strokeW = lineStrokeWidth(model.plot);
|
|
15649
|
+
const dotR = Math.min(6, markerRadius(model.plot, 2));
|
|
15650
|
+
const dotSW = markerStroke(dotR);
|
|
15019
15651
|
ctx.save();
|
|
15020
15652
|
ctx.beginPath();
|
|
15021
15653
|
ctx.rect(model.plot.x, model.plot.y, model.plot.width, model.plot.height);
|
|
@@ -15023,12 +15655,13 @@ function drawLines(surface, model) {
|
|
|
15023
15655
|
for (const series of model.series) {
|
|
15024
15656
|
const points = series.points;
|
|
15025
15657
|
if (points.length === 0) continue;
|
|
15658
|
+
ctx.globalAlpha = alphaOf(series);
|
|
15026
15659
|
if (pen && points.length >= 2) {
|
|
15027
|
-
pen.polyline(points, { stroke: series.color });
|
|
15660
|
+
pen.polyline(points, { stroke: series.color, strokeWidth: strokeW });
|
|
15028
15661
|
} else if (points.length >= 2) {
|
|
15029
15662
|
ctx.beginPath();
|
|
15030
15663
|
ctx.strokeStyle = series.color;
|
|
15031
|
-
ctx.lineWidth =
|
|
15664
|
+
ctx.lineWidth = strokeW;
|
|
15032
15665
|
ctx.lineJoin = "round";
|
|
15033
15666
|
ctx.lineCap = "round";
|
|
15034
15667
|
ctx.moveTo(points[0].x, points[0].y);
|
|
@@ -15037,58 +15670,65 @@ function drawLines(surface, model) {
|
|
|
15037
15670
|
}
|
|
15038
15671
|
for (const p of points) {
|
|
15039
15672
|
if (pen) {
|
|
15040
|
-
pen.circle(p.x, p.y,
|
|
15673
|
+
pen.circle(p.x, p.y, dotR, { fill: series.color, stroke: series.color });
|
|
15041
15674
|
} else {
|
|
15042
15675
|
ctx.beginPath();
|
|
15043
15676
|
ctx.fillStyle = series.color;
|
|
15044
15677
|
ctx.strokeStyle = model.tokens.color.background;
|
|
15045
|
-
ctx.lineWidth =
|
|
15046
|
-
ctx.arc(p.x, p.y,
|
|
15678
|
+
ctx.lineWidth = dotSW;
|
|
15679
|
+
ctx.arc(p.x, p.y, dotR, 0, Math.PI * 2);
|
|
15047
15680
|
ctx.fill();
|
|
15048
15681
|
ctx.stroke();
|
|
15049
15682
|
}
|
|
15050
15683
|
}
|
|
15684
|
+
ctx.globalAlpha = 1;
|
|
15051
15685
|
}
|
|
15052
15686
|
ctx.restore();
|
|
15053
15687
|
}
|
|
15054
|
-
function drawEndLabels(surface, model) {
|
|
15688
|
+
function drawEndLabels(surface, model, alphaOf) {
|
|
15055
15689
|
if (!model.showLabels) return;
|
|
15056
15690
|
const { tokens } = model;
|
|
15057
15691
|
const labelSize = tokens.font.size.tiny;
|
|
15058
15692
|
const valueSize = tokens.font.size.tiny;
|
|
15693
|
+
const leftW = model.leftLabelW;
|
|
15694
|
+
const rightW = model.rightLabelW;
|
|
15059
15695
|
for (const series of model.series) {
|
|
15060
15696
|
if (series.points.length === 0) continue;
|
|
15697
|
+
const opacity = alphaOf(series) === 1 ? 1 : 0.5;
|
|
15061
15698
|
const first = series.points[0];
|
|
15062
15699
|
const last = series.points[series.points.length - 1];
|
|
15063
15700
|
addOverlayText(surface, tokens, {
|
|
15064
|
-
left: first.x - 8 -
|
|
15701
|
+
left: first.x - 8 - leftW,
|
|
15065
15702
|
top: first.y - labelSize - 2,
|
|
15066
|
-
width:
|
|
15703
|
+
width: leftW,
|
|
15067
15704
|
text: series.label,
|
|
15068
15705
|
color: series.color,
|
|
15069
15706
|
size: labelSize,
|
|
15070
15707
|
weight: tokens.font.weight.bold,
|
|
15071
|
-
align: "right"
|
|
15708
|
+
align: "right",
|
|
15709
|
+
opacity
|
|
15072
15710
|
});
|
|
15073
15711
|
addOverlayText(surface, tokens, {
|
|
15074
|
-
left: first.x - 8 -
|
|
15712
|
+
left: first.x - 8 - leftW,
|
|
15075
15713
|
top: first.y + 1,
|
|
15076
|
-
width:
|
|
15714
|
+
width: leftW,
|
|
15077
15715
|
text: fmtValue(first.value, model.format),
|
|
15078
15716
|
color: tokens.color.textMuted,
|
|
15079
15717
|
size: valueSize,
|
|
15080
|
-
align: "right"
|
|
15718
|
+
align: "right",
|
|
15719
|
+
opacity
|
|
15081
15720
|
});
|
|
15082
15721
|
if (last !== first) {
|
|
15083
15722
|
addOverlayText(surface, tokens, {
|
|
15084
15723
|
left: last.x + 8,
|
|
15085
15724
|
top: last.y - valueSize / 2,
|
|
15086
|
-
width:
|
|
15725
|
+
width: rightW,
|
|
15087
15726
|
text: fmtValue(last.value, model.format),
|
|
15088
15727
|
color: series.color,
|
|
15089
15728
|
size: valueSize,
|
|
15090
15729
|
weight: tokens.font.weight.medium,
|
|
15091
|
-
align: "left"
|
|
15730
|
+
align: "left",
|
|
15731
|
+
opacity
|
|
15092
15732
|
});
|
|
15093
15733
|
}
|
|
15094
15734
|
}
|
|
@@ -15106,16 +15746,18 @@ function nearestPoint(model, px, py) {
|
|
|
15106
15746
|
}
|
|
15107
15747
|
return best ? { series: best.series, point: best.point } : null;
|
|
15108
15748
|
}
|
|
15109
|
-
function drawSlope(surface, spec, tokens, size,
|
|
15749
|
+
function drawSlope(surface, spec, tokens, size, context) {
|
|
15110
15750
|
const slope = spec;
|
|
15111
15751
|
const model = buildSlopeModel(slope, tokens, size);
|
|
15752
|
+
const emphasis = context?.emphasis ?? null;
|
|
15753
|
+
const seriesField = slope.encoding.series.field;
|
|
15754
|
+
const alphaOf = (series) => emphasis ? emphasis.match({ [seriesField]: series.value }) ? 1 : emphasis.dim : 1;
|
|
15112
15755
|
drawAxesUnderlay(surface, model.base);
|
|
15113
|
-
drawLines(surface, model);
|
|
15756
|
+
drawLines(surface, model, alphaOf);
|
|
15114
15757
|
drawOverlay(surface, model.base);
|
|
15115
|
-
drawEndLabels(surface, model);
|
|
15758
|
+
drawEndLabels(surface, model, alphaOf);
|
|
15116
15759
|
if (model.series.length === 0) return;
|
|
15117
15760
|
const { plot } = model;
|
|
15118
|
-
const seriesField = slope.encoding.series.field;
|
|
15119
15761
|
return {
|
|
15120
15762
|
region: { x: plot.x, y: plot.y, width: plot.width, height: plot.height },
|
|
15121
15763
|
hitTest: (px, py) => {
|
|
@@ -15254,7 +15896,7 @@ function measureMax2(labels, font) {
|
|
|
15254
15896
|
function valueLabel(value, format) {
|
|
15255
15897
|
return format ? formatValue(value, format) : formatNumber2(value, ",");
|
|
15256
15898
|
}
|
|
15257
|
-
function drawConnector(ctx, pen, x1, x2, y, tokens) {
|
|
15899
|
+
function drawConnector(ctx, pen, x1, x2, y, width, tokens) {
|
|
15258
15900
|
if (pen) {
|
|
15259
15901
|
pen.polyline(
|
|
15260
15902
|
[
|
|
@@ -15270,13 +15912,12 @@ function drawConnector(ctx, pen, x1, x2, y, tokens) {
|
|
|
15270
15912
|
ctx.moveTo(x1, y);
|
|
15271
15913
|
ctx.lineTo(x2, y);
|
|
15272
15914
|
ctx.strokeStyle = tokens.color.axis;
|
|
15273
|
-
ctx.lineWidth =
|
|
15915
|
+
ctx.lineWidth = width;
|
|
15274
15916
|
ctx.lineCap = "round";
|
|
15275
15917
|
ctx.stroke();
|
|
15276
15918
|
ctx.restore();
|
|
15277
15919
|
}
|
|
15278
|
-
function drawDot(ctx, pen, x, y, color, tokens) {
|
|
15279
|
-
const r = 5;
|
|
15920
|
+
function drawDot(ctx, pen, x, y, r, color, tokens) {
|
|
15280
15921
|
if (pen) {
|
|
15281
15922
|
pen.circle(x, y, r + 1.5, { fill: tokens.color.background, stroke: tokens.color.background });
|
|
15282
15923
|
pen.circle(x, y, r, { fill: color, stroke: tokens.color.background });
|
|
@@ -15287,16 +15928,16 @@ function drawDot(ctx, pen, x, y, color, tokens) {
|
|
|
15287
15928
|
ctx.arc(x, y, r, 0, Math.PI * 2);
|
|
15288
15929
|
ctx.fillStyle = color;
|
|
15289
15930
|
ctx.fill();
|
|
15290
|
-
ctx.lineWidth = 1.5;
|
|
15931
|
+
ctx.lineWidth = Math.max(1.5, r * 0.3);
|
|
15291
15932
|
ctx.strokeStyle = tokens.color.background;
|
|
15292
15933
|
ctx.stroke();
|
|
15293
15934
|
ctx.restore();
|
|
15294
15935
|
}
|
|
15295
|
-
function layoutPlot(content, labelWidth, tokens) {
|
|
15936
|
+
function layoutPlot(content, labelWidth, rightReserve, tokens) {
|
|
15296
15937
|
const leftGutter = Math.ceil(Math.min(content.width * 0.3, Math.max(42, labelWidth + tokens.spacing.sm)));
|
|
15297
15938
|
const axisHeight = 28;
|
|
15298
15939
|
const topPad = Math.min(6, CHROME_PAD.top / 2);
|
|
15299
|
-
const rightPad = 8;
|
|
15940
|
+
const rightPad = Math.max(8, Math.ceil(rightReserve));
|
|
15300
15941
|
return {
|
|
15301
15942
|
leftGutter,
|
|
15302
15943
|
axisHeight,
|
|
@@ -15308,10 +15949,12 @@ function layoutPlot(content, labelWidth, tokens) {
|
|
|
15308
15949
|
}
|
|
15309
15950
|
};
|
|
15310
15951
|
}
|
|
15311
|
-
function drawDumbbell(surface, spec, tokens, size,
|
|
15952
|
+
function drawDumbbell(surface, spec, tokens, size, context) {
|
|
15312
15953
|
const dumbbell = spec;
|
|
15313
15954
|
const model = buildDumbbellModel(dumbbell, tokens);
|
|
15314
15955
|
const ctx = surface.marks.ctx;
|
|
15956
|
+
const emphasis = context?.emphasis ?? null;
|
|
15957
|
+
const categoryField = dumbbell.encoding.category.field;
|
|
15315
15958
|
let content = drawTitleBlock(surface, tokens, size, dumbbell.title);
|
|
15316
15959
|
const legendEntries = model.groups.map((group) => ({ label: group.label, color: group.color, symbol: "circle" }));
|
|
15317
15960
|
content = drawCategoricalLegend(surface, tokens, content, legendEntries, "top");
|
|
@@ -15329,7 +15972,16 @@ function drawDumbbell(surface, spec, tokens, size, _context) {
|
|
|
15329
15972
|
}
|
|
15330
15973
|
const labelFont = fontString(tokens.font.size.small, tokens.font.family, tokens.font.weight.normal);
|
|
15331
15974
|
const widestCategory = measureMax2(model.rows.map((row) => row.catLabel), labelFont);
|
|
15332
|
-
|
|
15975
|
+
let valueReserve = 0;
|
|
15976
|
+
if (dumbbell.labels) {
|
|
15977
|
+
const valueFont = fontString(tokens.font.size.tiny, tokens.font.family, tokens.font.weight.normal);
|
|
15978
|
+
const widestValue = measureMax2(
|
|
15979
|
+
model.rows.flatMap((row) => row.dots.map((dot) => formatDumbbellValue(dot.value, model.format))),
|
|
15980
|
+
valueFont
|
|
15981
|
+
);
|
|
15982
|
+
valueReserve = Math.min(content.width * 0.2, widestValue + 9);
|
|
15983
|
+
}
|
|
15984
|
+
const { plot, leftGutter } = layoutPlot(content, widestCategory, valueReserve, tokens);
|
|
15333
15985
|
if (plot.width <= 8 || plot.height <= 8) return;
|
|
15334
15986
|
const xScale = linearScale({ domain: model.valueDomain, range: [plot.x, plot.x + plot.width] });
|
|
15335
15987
|
const yBand = bandScale({ domain: model.categories, range: [plot.y, plot.y + plot.height], paddingInner: 0.45, paddingOuter: 0.25 });
|
|
@@ -15356,6 +16008,9 @@ function drawDumbbell(surface, spec, tokens, size, _context) {
|
|
|
15356
16008
|
}
|
|
15357
16009
|
ctx.restore();
|
|
15358
16010
|
const pixelRows = [];
|
|
16011
|
+
const dotR = Math.max(4, Math.min(13, yBand.bandwidth * 0.16));
|
|
16012
|
+
const connectorW = Math.max(3, Math.min(9, dotR * 0.7));
|
|
16013
|
+
const labelGap = Math.round(dotR + 4);
|
|
15359
16014
|
for (const row of model.rows) {
|
|
15360
16015
|
const bandTop = yBand.map(row.catKey);
|
|
15361
16016
|
if (bandTop === void 0) continue;
|
|
@@ -15372,8 +16027,10 @@ function drawDumbbell(surface, spec, tokens, size, _context) {
|
|
|
15372
16027
|
}).sort((a, b) => a.x - b.x);
|
|
15373
16028
|
if (pixelDots.length === 0) continue;
|
|
15374
16029
|
pixelRows.push({ ...row, y: cy, pixelDots });
|
|
15375
|
-
|
|
15376
|
-
|
|
16030
|
+
ctx.globalAlpha = rowAlpha(emphasis, { [categoryField]: row.catRawValue });
|
|
16031
|
+
drawConnector(ctx, pen, pixelDots[0].x, pixelDots[pixelDots.length - 1].x, cy, connectorW, tokens);
|
|
16032
|
+
for (const dot of pixelDots) drawDot(ctx, pen, dot.x, cy, dotR, dot.color, tokens);
|
|
16033
|
+
ctx.globalAlpha = 1;
|
|
15377
16034
|
addOverlayText(surface, tokens, {
|
|
15378
16035
|
left: content.x,
|
|
15379
16036
|
top: cy,
|
|
@@ -15389,7 +16046,7 @@ function drawDumbbell(surface, spec, tokens, size, _context) {
|
|
|
15389
16046
|
const dot = pixelDots[i];
|
|
15390
16047
|
const atLeft = i === 0 && pixelDots.length > 1;
|
|
15391
16048
|
addOverlayText(surface, tokens, {
|
|
15392
|
-
left: atLeft ? dot.x -
|
|
16049
|
+
left: atLeft ? dot.x - labelGap : dot.x + labelGap,
|
|
15393
16050
|
top: cy,
|
|
15394
16051
|
width: 54,
|
|
15395
16052
|
text: formatDumbbellValue(dot.value, model.format),
|
|
@@ -15401,7 +16058,6 @@ function drawDumbbell(surface, spec, tokens, size, _context) {
|
|
|
15401
16058
|
}
|
|
15402
16059
|
}
|
|
15403
16060
|
}
|
|
15404
|
-
const categoryField = dumbbell.encoding.category.field;
|
|
15405
16061
|
const nearestDot = (px, py) => {
|
|
15406
16062
|
let best = null;
|
|
15407
16063
|
for (const row of pixelRows) {
|
|
@@ -15578,10 +16234,13 @@ function labelBox(model, horizontalRule, pos, p) {
|
|
|
15578
16234
|
const inset = 6;
|
|
15579
16235
|
if (horizontalRule) {
|
|
15580
16236
|
const top = p - 3;
|
|
15581
|
-
|
|
15582
|
-
|
|
15583
|
-
|
|
15584
|
-
|
|
16237
|
+
if (pos === "start") {
|
|
16238
|
+
return { left: plot.x + inset, top, align: "left", transform: "translateY(-100%)" };
|
|
16239
|
+
}
|
|
16240
|
+
if (pos === "middle") {
|
|
16241
|
+
return { left: plot.x + plot.width / 2, top, align: "center", transform: "translateX(-50%) translateY(-100%)" };
|
|
16242
|
+
}
|
|
16243
|
+
return { left: plot.x + plot.width - inset, top, align: "right", transform: "translateX(-100%) translateY(-100%)" };
|
|
15585
16244
|
}
|
|
15586
16245
|
const left = p;
|
|
15587
16246
|
const transform2 = "translateX(-50%)";
|
|
@@ -15756,7 +16415,8 @@ function drawTrendlineLabels(surface, model) {
|
|
|
15756
16415
|
const y1 = model.y.pixel(g.fit.predict(g.xMax));
|
|
15757
16416
|
if (x1 == null || !Number.isFinite(x1) || !Number.isFinite(y1)) continue;
|
|
15758
16417
|
const text = `R\xB2=${g.fit.r2.toFixed(2)}`;
|
|
15759
|
-
const
|
|
16418
|
+
const labelW = measureText(text, font).width;
|
|
16419
|
+
const nearRight = x1 + 6 + labelW > plot.x + plot.width;
|
|
15760
16420
|
const left = nearRight ? x1 - 6 : x1 + 6;
|
|
15761
16421
|
const align = nearRight ? "right" : "left";
|
|
15762
16422
|
const transform2 = nearRight ? "translateX(-100%) translateY(-50%)" : "translateY(-50%)";
|
|
@@ -16271,6 +16931,7 @@ function dependentParams(highlight, filter, ownParams = []) {
|
|
|
16271
16931
|
var MARK_CONTRAST_MIN = 1.5;
|
|
16272
16932
|
var TEXT_CONTRAST_MIN = 4.5;
|
|
16273
16933
|
var MAX_DISTINGUISHABLE_SERIES = 8;
|
|
16934
|
+
var DEFAULT_POINT_MARKER_RADIUS = 3.5;
|
|
16274
16935
|
var SEVERITY_RANK = { error: 0, warning: 1, info: 2 };
|
|
16275
16936
|
var VALUE_CHART_TYPES = /* @__PURE__ */ new Set(["kpi", "gauge", "bullet"]);
|
|
16276
16937
|
var CONTINUOUS_COLOR_TYPES = /* @__PURE__ */ new Set([
|
|
@@ -16328,6 +16989,7 @@ function buildRenderReport(input) {
|
|
|
16328
16989
|
if (model) {
|
|
16329
16990
|
checkDegenerateAxis(model, add);
|
|
16330
16991
|
checkClippedMarks(model, spec, add);
|
|
16992
|
+
checkAnnotationOverflow(model, spec, tokens, add);
|
|
16331
16993
|
checkAxisLabelOverlap(model, spec, tokens, add);
|
|
16332
16994
|
checkLegendOverflow(model, spec, add);
|
|
16333
16995
|
}
|
|
@@ -16447,6 +17109,69 @@ function checkClippedMarks(model, spec, add) {
|
|
|
16447
17109
|
});
|
|
16448
17110
|
}
|
|
16449
17111
|
}
|
|
17112
|
+
function checkAnnotationOverflow(model, spec, tokens, add) {
|
|
17113
|
+
if (model.orientation === "horizontal") return;
|
|
17114
|
+
const explicit = spec.annotations;
|
|
17115
|
+
const points = Array.isArray(explicit) ? explicit.filter((a) => a.type === "point") : [];
|
|
17116
|
+
for (const a of autoInsightAnnotations(spec)) points.push(a);
|
|
17117
|
+
if (points.length === 0) return;
|
|
17118
|
+
const { plot } = model;
|
|
17119
|
+
const fontSize = tokens.font.size.small;
|
|
17120
|
+
const font = fontString(fontSize, tokens.font.family, tokens.font.weight.medium);
|
|
17121
|
+
const EPS = 0.75;
|
|
17122
|
+
const sides = /* @__PURE__ */ new Set();
|
|
17123
|
+
const artifacts = /* @__PURE__ */ new Set();
|
|
17124
|
+
let clippedAnnotations = 0;
|
|
17125
|
+
let clippedArtifacts = 0;
|
|
17126
|
+
for (const ann of points) {
|
|
17127
|
+
const cat = model.x.pixel(ann.x);
|
|
17128
|
+
const val = model.y.pixel(ann.y);
|
|
17129
|
+
if (cat == null || val == null || !Number.isFinite(cat) || !Number.isFinite(val)) continue;
|
|
17130
|
+
const { x: px, y: py } = model.project(cat, val);
|
|
17131
|
+
const r = ann.markerRadius ?? DEFAULT_POINT_MARKER_RADIUS;
|
|
17132
|
+
const halo = r + 1.6;
|
|
17133
|
+
const markerSides = overflowSides(px - halo, py - halo, px + halo, py + halo, plot, EPS);
|
|
17134
|
+
let annClipped = false;
|
|
17135
|
+
if (markerSides.length) {
|
|
17136
|
+
clippedArtifacts++;
|
|
17137
|
+
annClipped = true;
|
|
17138
|
+
artifacts.add("marker");
|
|
17139
|
+
markerSides.forEach((s) => sides.add(s));
|
|
17140
|
+
}
|
|
17141
|
+
if (!ann.label) continue;
|
|
17142
|
+
const gap = r + 4;
|
|
17143
|
+
const width = measureText(ann.label, font).width + 6;
|
|
17144
|
+
const aboveTop = py - gap - fontSize;
|
|
17145
|
+
const top = aboveTop < plot.y ? py + gap : aboveTop;
|
|
17146
|
+
const labelSides = overflowSides(px - width / 2, top, px + width / 2, top + fontSize, plot, EPS);
|
|
17147
|
+
if (labelSides.length) {
|
|
17148
|
+
clippedArtifacts++;
|
|
17149
|
+
annClipped = true;
|
|
17150
|
+
artifacts.add("label");
|
|
17151
|
+
labelSides.forEach((s) => sides.add(s));
|
|
17152
|
+
}
|
|
17153
|
+
if (annClipped) clippedAnnotations++;
|
|
17154
|
+
}
|
|
17155
|
+
if (clippedAnnotations > 0) {
|
|
17156
|
+
const artifactList = [...artifacts].join(" and ");
|
|
17157
|
+
const sideList = [...sides].join(", ");
|
|
17158
|
+
add({
|
|
17159
|
+
code: "annotation-clipped",
|
|
17160
|
+
severity: "warning",
|
|
17161
|
+
message: `${clippedAnnotations} point annotation ${artifactList} overflow${clippedAnnotations === 1 ? "s" : ""} the plot (${sideList}); the chart is too small to fit its annotation labels.`,
|
|
17162
|
+
details: { clipped: clippedAnnotations, clippedArtifacts, artifacts: [...artifacts], sides: [...sides] },
|
|
17163
|
+
hint: "Enlarge the chart, shorten annotation labels, or remove labels/insights that cannot fit."
|
|
17164
|
+
});
|
|
17165
|
+
}
|
|
17166
|
+
}
|
|
17167
|
+
function overflowSides(left, top, right, bottom, plot, eps) {
|
|
17168
|
+
const sides = [];
|
|
17169
|
+
if (left < plot.x - eps) sides.push("left");
|
|
17170
|
+
if (right > plot.x + plot.width + eps) sides.push("right");
|
|
17171
|
+
if (top < plot.y - eps) sides.push("top");
|
|
17172
|
+
if (bottom > plot.y + plot.height + eps) sides.push("bottom");
|
|
17173
|
+
return sides;
|
|
17174
|
+
}
|
|
16450
17175
|
function checkAxisLabelOverlap(model, spec, tokens, add) {
|
|
16451
17176
|
const ticks2 = model.xTicks;
|
|
16452
17177
|
if (ticks2.length < 2) return;
|
|
@@ -16534,7 +17259,7 @@ function manualYDomainFix(spec) {
|
|
|
16534
17259
|
// src/runtime/facet.ts
|
|
16535
17260
|
var FACETABLE_TYPES2 = /* @__PURE__ */ new Set(["line", "area", "bar", "scatter"]);
|
|
16536
17261
|
var FACET_GAP = 16;
|
|
16537
|
-
var FACET_PADDING = { top:
|
|
17262
|
+
var FACET_PADDING = { top: 0, right: 0, bottom: 0, left: 0 };
|
|
16538
17263
|
var SEVERITY_RANK2 = { error: 0, warning: 1, info: 2 };
|
|
16539
17264
|
function isFaceted(spec) {
|
|
16540
17265
|
const facet = spec.facet;
|
|
@@ -16737,6 +17462,8 @@ var CANVAS_MARK_TYPES = /* @__PURE__ */ new Set([
|
|
|
16737
17462
|
"sankey",
|
|
16738
17463
|
"choropleth"
|
|
16739
17464
|
]);
|
|
17465
|
+
var COUNT_UP_TYPES = /* @__PURE__ */ new Set(["kpi"]);
|
|
17466
|
+
var SWEEP_TYPES = /* @__PURE__ */ new Set(["pie", "gauge"]);
|
|
16740
17467
|
var EMPTY_DATA = [];
|
|
16741
17468
|
function legendConfigFor(model) {
|
|
16742
17469
|
const legend = model.spec.legend;
|
|
@@ -16832,6 +17559,9 @@ function render(target, spec, options) {
|
|
|
16832
17559
|
let interaction;
|
|
16833
17560
|
let entrance;
|
|
16834
17561
|
let updateTransition;
|
|
17562
|
+
let lastValue = null;
|
|
17563
|
+
let lastCompare = null;
|
|
17564
|
+
let lastSparkline = null;
|
|
16835
17565
|
let destroyed = false;
|
|
16836
17566
|
let lastDrawnW = -1;
|
|
16837
17567
|
let lastDrawnH = -1;
|
|
@@ -16859,7 +17589,7 @@ function render(target, spec, options) {
|
|
|
16859
17589
|
}
|
|
16860
17590
|
}
|
|
16861
17591
|
const hostListeners = /* @__PURE__ */ new Set();
|
|
16862
|
-
const draw = (animateEntrance) => {
|
|
17592
|
+
const draw = (animateEntrance, valueTweenFrom, compareTweenFrom, sparklineTweenFrom) => {
|
|
16863
17593
|
entrance?.cancel();
|
|
16864
17594
|
entrance = void 0;
|
|
16865
17595
|
updateTransition?.cancel();
|
|
@@ -16889,6 +17619,11 @@ function render(target, spec, options) {
|
|
|
16889
17619
|
let facetRpt;
|
|
16890
17620
|
let legendSelect = null;
|
|
16891
17621
|
const type = currentSpec.type;
|
|
17622
|
+
if (COUNT_UP_TYPES.has(type) || SWEEP_TYPES.has(type)) {
|
|
17623
|
+
lastValue = resolveValueRef(effectiveSpec.value, effectiveSpec.data ?? EMPTY_DATA);
|
|
17624
|
+
lastCompare = SWEEP_TYPES.has(type) ? resolveValueRef(effectiveSpec.compare, effectiveSpec.data ?? EMPTY_DATA) : null;
|
|
17625
|
+
lastSparkline = type === "kpi" ? kpiSparklineSeries(effectiveSpec, effectiveSpec.data ?? EMPTY_DATA) : null;
|
|
17626
|
+
}
|
|
16892
17627
|
const facetLayout = isFaceted(effectiveSpec) ? buildFacetModels(effectiveSpec, tokens, size) : null;
|
|
16893
17628
|
if (facetLayout) {
|
|
16894
17629
|
drawFacet(surface, effectiveSpec, facetLayout, tokens);
|
|
@@ -16919,7 +17654,8 @@ function render(target, spec, options) {
|
|
|
16919
17654
|
tokens,
|
|
16920
17655
|
currentSpec.background,
|
|
16921
17656
|
anim,
|
|
16922
|
-
finishEntrance
|
|
17657
|
+
finishEntrance,
|
|
17658
|
+
type === "bar" ? "grow" : "wipe"
|
|
16923
17659
|
);
|
|
16924
17660
|
}
|
|
16925
17661
|
} else {
|
|
@@ -16937,11 +17673,44 @@ function render(target, spec, options) {
|
|
|
16937
17673
|
}
|
|
16938
17674
|
};
|
|
16939
17675
|
interactionModel = renderer(surface, effectiveSpec, tokens, size, ctx) ?? null;
|
|
17676
|
+
if (anim.enabled) {
|
|
17677
|
+
if (COUNT_UP_TYPES.has(type) || SWEEP_TYPES.has(type)) {
|
|
17678
|
+
const key = COUNT_UP_TYPES.has(type) ? "valueProgress" : "sweepProgress";
|
|
17679
|
+
entrance = runCustomProgress(anim, finishEntrance, (t) => {
|
|
17680
|
+
surface.overlay.replaceChildren();
|
|
17681
|
+
surface.marks.clear();
|
|
17682
|
+
paintBackground(surface, tokens, currentSpec.background);
|
|
17683
|
+
renderer(surface, effectiveSpec, tokens, size, { ...ctx, [key]: t });
|
|
17684
|
+
});
|
|
17685
|
+
} else {
|
|
17686
|
+
entrance = runFadeEntrance(surface, anim, finishEntrance);
|
|
17687
|
+
}
|
|
17688
|
+
} else if ((valueTweenFrom !== void 0 || sparklineTweenFrom !== void 0) && (COUNT_UP_TYPES.has(type) || SWEEP_TYPES.has(type))) {
|
|
17689
|
+
const tween = resolveUpdate(currentSpec.animation, {
|
|
17690
|
+
reducedMotion: prefersReducedMotion(),
|
|
17691
|
+
disabled: globalThis.__GRAPHEIN_DISABLE_ANIM === true
|
|
17692
|
+
});
|
|
17693
|
+
if (tween.enabled) {
|
|
17694
|
+
const key = COUNT_UP_TYPES.has(type) ? "valueProgress" : "sweepProgress";
|
|
17695
|
+
entrance = runCustomProgress(tween, finishEntrance, (t) => {
|
|
17696
|
+
surface.overlay.replaceChildren();
|
|
17697
|
+
surface.marks.clear();
|
|
17698
|
+
paintBackground(surface, tokens, currentSpec.background);
|
|
17699
|
+
renderer(surface, effectiveSpec, tokens, size, {
|
|
17700
|
+
...ctx,
|
|
17701
|
+
valueFrom: valueTweenFrom,
|
|
17702
|
+
compareFrom: compareTweenFrom,
|
|
17703
|
+
sparklineFrom: sparklineTweenFrom,
|
|
17704
|
+
[key]: t
|
|
17705
|
+
});
|
|
17706
|
+
});
|
|
17707
|
+
}
|
|
17708
|
+
}
|
|
16940
17709
|
} else {
|
|
16941
17710
|
drawPlaceholder(surface, tokens, `\u201C${type}\u201D renderer not yet registered`);
|
|
16942
|
-
|
|
16943
|
-
|
|
16944
|
-
|
|
17711
|
+
if (anim.enabled) {
|
|
17712
|
+
entrance = runFadeEntrance(surface, anim, finishEntrance);
|
|
17713
|
+
}
|
|
16945
17714
|
}
|
|
16946
17715
|
}
|
|
16947
17716
|
if (!interaction) interaction = new InteractionController(surface, tokens);
|
|
@@ -17065,8 +17834,18 @@ function render(target, spec, options) {
|
|
|
17065
17834
|
});
|
|
17066
17835
|
const wantFade = anim.enabled && CANVAS_MARK_TYPES.has(prevType) && CANVAS_MARK_TYPES.has(next.type);
|
|
17067
17836
|
const before = wantFade ? snapshotLayer(surface.marks) : void 0;
|
|
17837
|
+
const prevValue = lastValue;
|
|
17838
|
+
const prevCompare = lastCompare;
|
|
17839
|
+
const prevSparkline = lastSparkline;
|
|
17840
|
+
const wantValueTween = prevType === next.type && (COUNT_UP_TYPES.has(next.type) || SWEEP_TYPES.has(next.type)) && prevValue != null;
|
|
17841
|
+
const wantSparklineTween = prevType === next.type && next.type === "kpi" && prevSparkline != null && prevSparkline.length > 0;
|
|
17068
17842
|
currentSpec = next;
|
|
17069
|
-
draw(
|
|
17843
|
+
draw(
|
|
17844
|
+
false,
|
|
17845
|
+
wantValueTween ? prevValue ?? void 0 : void 0,
|
|
17846
|
+
wantValueTween ? prevCompare ?? void 0 : void 0,
|
|
17847
|
+
wantValueTween || wantSparklineTween ? prevSparkline ?? void 0 : void 0
|
|
17848
|
+
);
|
|
17070
17849
|
if (before && surface.width === prevW && surface.height === prevH && surface.marks.canvas.width === before.width && surface.marks.canvas.height === before.height) {
|
|
17071
17850
|
updateTransition = runUpdateCrossfade(surface, before, anim);
|
|
17072
17851
|
}
|
|
@@ -17086,7 +17865,7 @@ function render(target, spec, options) {
|
|
|
17086
17865
|
}
|
|
17087
17866
|
};
|
|
17088
17867
|
}
|
|
17089
|
-
function runCartesianEntrance(surface, model, renderer, tokens, background, anim, onDone) {
|
|
17868
|
+
function runCartesianEntrance(surface, model, renderer, tokens, background, anim, onDone, mode = "wipe") {
|
|
17090
17869
|
const ctx = surface.marks.ctx;
|
|
17091
17870
|
const { plot } = model;
|
|
17092
17871
|
const bg = background ?? tokens.color.background;
|
|
@@ -17097,6 +17876,14 @@ function runCartesianEntrance(surface, model, renderer, tokens, background, anim
|
|
|
17097
17876
|
ctx.fillRect(0, 0, surface.width, surface.height);
|
|
17098
17877
|
ctx.restore();
|
|
17099
17878
|
drawAxesUnderlay(surface, model);
|
|
17879
|
+
if (mode === "grow") {
|
|
17880
|
+
model.entranceProgress = t;
|
|
17881
|
+
renderer(surface, model);
|
|
17882
|
+
model.entranceProgress = void 0;
|
|
17883
|
+
drawTrendlines(surface, model);
|
|
17884
|
+
drawAnnotations(surface, model);
|
|
17885
|
+
return;
|
|
17886
|
+
}
|
|
17100
17887
|
if (t <= 0) return;
|
|
17101
17888
|
ctx.save();
|
|
17102
17889
|
ctx.globalAlpha = Math.min(1, 0.2 + 0.8 * t);
|
|
@@ -17119,6 +17906,17 @@ function runCartesianEntrance(surface, model, renderer, tokens, background, anim
|
|
|
17119
17906
|
}
|
|
17120
17907
|
});
|
|
17121
17908
|
}
|
|
17909
|
+
function runCustomProgress(anim, onDone, paint) {
|
|
17910
|
+
return animate({
|
|
17911
|
+
duration: anim.duration,
|
|
17912
|
+
easing: anim.easing,
|
|
17913
|
+
onUpdate: paint,
|
|
17914
|
+
onComplete: () => {
|
|
17915
|
+
paint(1);
|
|
17916
|
+
onDone();
|
|
17917
|
+
}
|
|
17918
|
+
});
|
|
17919
|
+
}
|
|
17122
17920
|
function runFadeEntrance(surface, anim, onDone) {
|
|
17123
17921
|
const root = surface.root;
|
|
17124
17922
|
const apply = (t) => {
|
|
@@ -17270,12 +18068,17 @@ function autoParamName(view) {
|
|
|
17270
18068
|
const existing = view.spec.params?.[0]?.name;
|
|
17271
18069
|
return existing ?? `__sel__${view.id}`;
|
|
17272
18070
|
}
|
|
18071
|
+
function roleOf(view) {
|
|
18072
|
+
return view.interact ?? "auto";
|
|
18073
|
+
}
|
|
17273
18074
|
function wireViews(views, interactions) {
|
|
17274
18075
|
const out = views.map((v) => ({ ...v, spec: v.spec }));
|
|
17275
18076
|
if (interactions === "none") return out;
|
|
17276
18077
|
const sources = [];
|
|
17277
18078
|
out.forEach((v, i) => {
|
|
17278
18079
|
const type = v.spec.type;
|
|
18080
|
+
const role = roleOf(v);
|
|
18081
|
+
if (role === "none" || role === "target") return;
|
|
17279
18082
|
if (isSlicerType(type)) {
|
|
17280
18083
|
const s = v.spec;
|
|
17281
18084
|
sources.push({
|
|
@@ -17306,11 +18109,15 @@ function wireViews(views, interactions) {
|
|
|
17306
18109
|
name: src.param,
|
|
17307
18110
|
select: { type: "point", on: "click", fields: src.fields }
|
|
17308
18111
|
});
|
|
17309
|
-
|
|
18112
|
+
if (roleOf(out[src.view]) !== "source") {
|
|
18113
|
+
out[src.view].spec = addHighlight(out[src.view].spec, src.param);
|
|
18114
|
+
}
|
|
17310
18115
|
}
|
|
17311
18116
|
out.forEach((v, j) => {
|
|
17312
18117
|
if (isSlicerType(v.spec.type)) return;
|
|
17313
18118
|
if (j === src.view) return;
|
|
18119
|
+
const trole = roleOf(v);
|
|
18120
|
+
if (trole === "none" || trole === "source") return;
|
|
17314
18121
|
out[j].spec = addFilterParam(out[j].spec, src.param);
|
|
17315
18122
|
});
|
|
17316
18123
|
}
|
|
@@ -17360,6 +18167,41 @@ var DEFAULT_BREAKPOINTS = [
|
|
|
17360
18167
|
{ maxWidth: 960, cols: 6 }
|
|
17361
18168
|
];
|
|
17362
18169
|
var STRIP_SLICERS = /* @__PURE__ */ new Set(["dropdown", "search", "dateRange", "range"]);
|
|
18170
|
+
var DASHBOARD_CLASS = "graphein-dashboard";
|
|
18171
|
+
var DASHBOARD_CELL_CLASS = "graphein-dashboard-cell";
|
|
18172
|
+
var DASHBOARD_SOURCE_CLASS = "is-source";
|
|
18173
|
+
function dashboardStyles(tokens) {
|
|
18174
|
+
const accent = tokens.color.accent;
|
|
18175
|
+
const surface = tokens.color.surface;
|
|
18176
|
+
return `
|
|
18177
|
+
.${DASHBOARD_CLASS} .${DASHBOARD_CELL_CLASS}[data-framed="true"] {
|
|
18178
|
+
transition: border-color 120ms ease, background-color 120ms ease;
|
|
18179
|
+
}
|
|
18180
|
+
.${DASHBOARD_CLASS} .${DASHBOARD_CELL_CLASS}[data-framed="true"]:hover {
|
|
18181
|
+
border-color: ${accent};
|
|
18182
|
+
background-color: ${surface};
|
|
18183
|
+
}
|
|
18184
|
+
.${DASHBOARD_CLASS} .${DASHBOARD_CELL_CLASS}:focus-visible {
|
|
18185
|
+
outline: 2px solid ${accent};
|
|
18186
|
+
outline-offset: 2px;
|
|
18187
|
+
}
|
|
18188
|
+
.${DASHBOARD_CLASS} .${DASHBOARD_CELL_CLASS}.${DASHBOARD_SOURCE_CLASS} {
|
|
18189
|
+
border-color: ${accent};
|
|
18190
|
+
box-shadow: inset 0 0 0 1px ${accent};
|
|
18191
|
+
}
|
|
18192
|
+
`;
|
|
18193
|
+
}
|
|
18194
|
+
function selectionIsActive(value) {
|
|
18195
|
+
if (value == null) return false;
|
|
18196
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
18197
|
+
if (typeof value !== "object") return true;
|
|
18198
|
+
const v = value;
|
|
18199
|
+
if (v.kind === "point") return Array.isArray(v.tuples) && v.tuples.length > 0;
|
|
18200
|
+
if (v.kind === "set") return Array.isArray(v.values) && v.values.length > 0;
|
|
18201
|
+
if (v.kind === "range") return v.min != null || v.max != null;
|
|
18202
|
+
if (v.kind === "text") return typeof v.query === "string" && v.query.length > 0;
|
|
18203
|
+
return true;
|
|
18204
|
+
}
|
|
17363
18205
|
function stripCellHeight(view, rowHeight) {
|
|
17364
18206
|
const titled = !!(view.title || view.subtitle);
|
|
17365
18207
|
const titlePad = titled ? 30 : 0;
|
|
@@ -17502,6 +18344,12 @@ function renderDashboard(target, spec, options) {
|
|
|
17502
18344
|
let root;
|
|
17503
18345
|
let resizeObs;
|
|
17504
18346
|
let destroyed = false;
|
|
18347
|
+
let sourceCells = /* @__PURE__ */ new Map();
|
|
18348
|
+
const syncSourceCell = (name, value) => {
|
|
18349
|
+
const cell = sourceCells.get(name);
|
|
18350
|
+
if (!cell) return;
|
|
18351
|
+
cell.classList.toggle(DASHBOARD_SOURCE_CLASS, selectionIsActive(value));
|
|
18352
|
+
};
|
|
17505
18353
|
const build = () => {
|
|
17506
18354
|
const tokens = resolveTheme(currentSpec.theme);
|
|
17507
18355
|
const layout2 = resolveDashboardLayout(currentSpec.layout);
|
|
@@ -17514,8 +18362,9 @@ function renderDashboard(target, spec, options) {
|
|
|
17514
18362
|
store.set(param.name, param.value ?? null);
|
|
17515
18363
|
}
|
|
17516
18364
|
}
|
|
18365
|
+
sourceCells = /* @__PURE__ */ new Map();
|
|
17517
18366
|
root = document.createElement("div");
|
|
17518
|
-
root.className =
|
|
18367
|
+
root.className = DASHBOARD_CLASS;
|
|
17519
18368
|
Object.assign(root.style, {
|
|
17520
18369
|
boxSizing: "border-box",
|
|
17521
18370
|
width: currentSpec.dimensions?.width ? `${currentSpec.dimensions.width}px` : "100%",
|
|
@@ -17529,6 +18378,10 @@ function renderDashboard(target, spec, options) {
|
|
|
17529
18378
|
flexDirection: "column",
|
|
17530
18379
|
gap: `${layout2.gap}px`
|
|
17531
18380
|
});
|
|
18381
|
+
const style = document.createElement("style");
|
|
18382
|
+
style.setAttribute("data-graphein-dashboard-style", "");
|
|
18383
|
+
style.textContent = dashboardStyles(tokens);
|
|
18384
|
+
root.appendChild(style);
|
|
17532
18385
|
const titleText3 = typeof currentSpec.title === "string" ? currentSpec.title : currentSpec.title?.text;
|
|
17533
18386
|
if (titleText3 || currentSpec.subtitle) {
|
|
17534
18387
|
const header = document.createElement("div");
|
|
@@ -17588,6 +18441,7 @@ function renderDashboard(target, spec, options) {
|
|
|
17588
18441
|
};
|
|
17589
18442
|
const cellChrome = (cell, view) => {
|
|
17590
18443
|
const framed = view?.frame ?? true;
|
|
18444
|
+
cell.dataset.framed = framed ? "true" : "false";
|
|
17591
18445
|
Object.assign(cell.style, {
|
|
17592
18446
|
minWidth: "0",
|
|
17593
18447
|
minHeight: "0",
|
|
@@ -17599,9 +18453,10 @@ function renderDashboard(target, spec, options) {
|
|
|
17599
18453
|
background: view?.background ?? (framed ? tokens.color.surface : "transparent")
|
|
17600
18454
|
});
|
|
17601
18455
|
};
|
|
17602
|
-
const
|
|
18456
|
+
const createCard = (view) => {
|
|
17603
18457
|
const cell = document.createElement("div");
|
|
17604
18458
|
cellChrome(cell, view);
|
|
18459
|
+
cell.classList.add(DASHBOARD_CELL_CLASS);
|
|
17605
18460
|
cell.setAttribute("data-view-id", view.id);
|
|
17606
18461
|
const hasCardChrome = !!(view.title || view.subtitle || view.accent || view.background || view.frame === false || view.padding === "none");
|
|
17607
18462
|
if (!hasCardChrome) return { cell, host: cell };
|
|
@@ -17628,13 +18483,14 @@ function renderDashboard(target, spec, options) {
|
|
|
17628
18483
|
padding: `${tokens.spacing.sm}px ${tokens.spacing.md}px ${tokens.spacing.xs}px ${tokens.spacing.md}px`,
|
|
17629
18484
|
display: "flex",
|
|
17630
18485
|
flexDirection: "column",
|
|
17631
|
-
gap: "2px"
|
|
18486
|
+
gap: "2px",
|
|
18487
|
+
borderBottom: `1px solid ${tokens.color.border}`
|
|
17632
18488
|
});
|
|
17633
18489
|
if (view.title) {
|
|
17634
18490
|
const title2 = document.createElement("div");
|
|
17635
18491
|
title2.textContent = view.title;
|
|
17636
18492
|
Object.assign(title2.style, {
|
|
17637
|
-
font: `${tokens.font.weight.
|
|
18493
|
+
font: `${tokens.font.weight.medium} ${tokens.font.size.base}px ${tokens.font.family}`,
|
|
17638
18494
|
color: tokens.color.text,
|
|
17639
18495
|
letterSpacing: "-0.01em"
|
|
17640
18496
|
});
|
|
@@ -17662,6 +18518,24 @@ function renderDashboard(target, spec, options) {
|
|
|
17662
18518
|
cell.appendChild(host);
|
|
17663
18519
|
return { cell, host };
|
|
17664
18520
|
};
|
|
18521
|
+
const publishingParams = (view) => {
|
|
18522
|
+
const names = /* @__PURE__ */ new Set();
|
|
18523
|
+
for (const param of view.spec.params ?? []) {
|
|
18524
|
+
if (param?.name) names.add(param.name);
|
|
18525
|
+
}
|
|
18526
|
+
if (isSlicerType(view.spec.type)) {
|
|
18527
|
+
const slicer = view.spec;
|
|
18528
|
+
const name = typeof slicer.param === "string" ? slicer.param : typeof slicer.field === "string" ? slicer.field : void 0;
|
|
18529
|
+
if (name) names.add(name);
|
|
18530
|
+
}
|
|
18531
|
+
return [...names];
|
|
18532
|
+
};
|
|
18533
|
+
const registerSourceCell = (view, cell) => {
|
|
18534
|
+
for (const name of publishingParams(view)) {
|
|
18535
|
+
sourceCells.set(name, cell);
|
|
18536
|
+
syncSourceCell(name, store.get(name));
|
|
18537
|
+
}
|
|
18538
|
+
};
|
|
17665
18539
|
const sectionPlans = resolveDashboardSections(gridViews, layout2);
|
|
17666
18540
|
const layoutGroups = [];
|
|
17667
18541
|
const deferredMounts = [];
|
|
@@ -17674,7 +18548,8 @@ function renderDashboard(target, spec, options) {
|
|
|
17674
18548
|
gap: `${layout2.gap}px`
|
|
17675
18549
|
});
|
|
17676
18550
|
for (const view of stripViews) {
|
|
17677
|
-
const { cell, host } =
|
|
18551
|
+
const { cell, host } = createCard(view);
|
|
18552
|
+
registerSourceCell(view, cell);
|
|
17678
18553
|
Object.assign(cell.style, {
|
|
17679
18554
|
flex: "1 1 220px",
|
|
17680
18555
|
minWidth: "200px",
|
|
@@ -17720,7 +18595,13 @@ function renderDashboard(target, spec, options) {
|
|
|
17720
18595
|
if (plan.section?.title) {
|
|
17721
18596
|
const title2 = document.createElement("span");
|
|
17722
18597
|
title2.textContent = plan.section.title;
|
|
17723
|
-
Object.assign(title2.style, {
|
|
18598
|
+
Object.assign(title2.style, {
|
|
18599
|
+
color: tokens.color.textMuted,
|
|
18600
|
+
fontWeight: String(tokens.font.weight.medium),
|
|
18601
|
+
fontSize: `${tokens.font.size.small}px`,
|
|
18602
|
+
letterSpacing: "0.06em",
|
|
18603
|
+
textTransform: "uppercase"
|
|
18604
|
+
});
|
|
17724
18605
|
titles.appendChild(title2);
|
|
17725
18606
|
}
|
|
17726
18607
|
if (plan.section?.subtitle) {
|
|
@@ -17772,7 +18653,8 @@ function renderDashboard(target, spec, options) {
|
|
|
17772
18653
|
parent.appendChild(grid);
|
|
17773
18654
|
const cells = placeViews(plan.views, plan.cols, layout2.preset ?? "auto");
|
|
17774
18655
|
for (const cellInfo of cells) {
|
|
17775
|
-
const { cell, host } =
|
|
18656
|
+
const { cell, host } = createCard(cellInfo.view);
|
|
18657
|
+
registerSourceCell(cellInfo.view, cell);
|
|
17776
18658
|
grid.appendChild(cell);
|
|
17777
18659
|
cellInfo.el = cell;
|
|
17778
18660
|
deferredMounts.push({ el: host, view: cellInfo.view, mounted: false });
|
|
@@ -17845,8 +18727,10 @@ function renderDashboard(target, spec, options) {
|
|
|
17845
18727
|
instances.length = 0;
|
|
17846
18728
|
if (root && root.parentNode) root.parentNode.removeChild(root);
|
|
17847
18729
|
root = void 0;
|
|
18730
|
+
sourceCells = /* @__PURE__ */ new Map();
|
|
17848
18731
|
};
|
|
17849
18732
|
const unsubscribe = store.subscribe((name, value) => {
|
|
18733
|
+
syncSourceCell(name, value);
|
|
17850
18734
|
for (const listener of [...hostListeners]) listener(name, value);
|
|
17851
18735
|
});
|
|
17852
18736
|
build();
|
|
@@ -18064,6 +18948,6 @@ function renderToContext(target, spec) {
|
|
|
18064
18948
|
// src/index.ts
|
|
18065
18949
|
var VERSION = "0.1.0";
|
|
18066
18950
|
|
|
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 };
|
|
18951
|
+
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, NO_PADDING, 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
18952
|
//# sourceMappingURL=index.js.map
|
|
18069
18953
|
//# sourceMappingURL=index.js.map
|