@opendata-ai/openchart-engine 8.3.1 → 8.4.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 +16 -3
- package/dist/index.js +1859 -682
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -10,11 +10,12 @@ import {
|
|
|
10
10
|
getLayoutStrategy,
|
|
11
11
|
isAxislessMark as isAxislessMark3,
|
|
12
12
|
MAX_SR_TABLE_ROWS,
|
|
13
|
+
resolveChromeEconomy,
|
|
13
14
|
resolveTheme as resolveTheme7
|
|
14
15
|
} from "@opendata-ai/openchart-core";
|
|
15
16
|
|
|
16
17
|
// src/annotations/collisions.ts
|
|
17
|
-
import { detectCollision } from "@opendata-ai/openchart-core";
|
|
18
|
+
import { detectCollision, overlapArea as overlapArea2 } from "@opendata-ai/openchart-core";
|
|
18
19
|
|
|
19
20
|
// src/annotations/constants.ts
|
|
20
21
|
var DEFAULT_ANNOTATION_FONT_SIZE = 13;
|
|
@@ -1157,6 +1158,12 @@ function resolveAnnotationCollisions(annotations, originalSpecs, scales, chartAr
|
|
|
1157
1158
|
};
|
|
1158
1159
|
const isText = (a) => a.type === "text" && !!a.label;
|
|
1159
1160
|
const placedBounds = [];
|
|
1161
|
+
for (const annotation of annotations) {
|
|
1162
|
+
if (isText(annotation) || !annotation.bounds) continue;
|
|
1163
|
+
if (annotation.bounds.width > 0 && annotation.bounds.height > 0) {
|
|
1164
|
+
placedBounds.push(annotation.bounds);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1160
1167
|
for (let i = 0; i < annotations.length; i++) {
|
|
1161
1168
|
const annotation = annotations[i];
|
|
1162
1169
|
if (!isText(annotation) || !isPinned(annotation, i)) continue;
|
|
@@ -1223,6 +1230,100 @@ function clampAnnotationsToBounds(annotations, svgWidth, svgHeight, measure = he
|
|
|
1223
1230
|
moveAnnotationBy(annotation, label, dx, dy, measure);
|
|
1224
1231
|
}
|
|
1225
1232
|
}
|
|
1233
|
+
var REFLINE_OBSTACLE_KINDS = /* @__PURE__ */ new Set([
|
|
1234
|
+
"mark",
|
|
1235
|
+
"data-label",
|
|
1236
|
+
"annotation",
|
|
1237
|
+
"endpoint-label"
|
|
1238
|
+
]);
|
|
1239
|
+
var MAX_REFLINE_SLIDE_CANDIDATES = 48;
|
|
1240
|
+
function hasExplicitLabelOffset(spec) {
|
|
1241
|
+
return (spec.labelOffset?.dx ?? 0) !== 0 || (spec.labelOffset?.dy ?? 0) !== 0;
|
|
1242
|
+
}
|
|
1243
|
+
function nudgeRefLineLabel(annotation, spec, obstacles, chartArea, measure = heuristicMeasure) {
|
|
1244
|
+
if (annotation.type !== "refline" || !annotation.label || !annotation.line) return false;
|
|
1245
|
+
if (hasExplicitLabelOffset(spec)) return false;
|
|
1246
|
+
const label = annotation.label;
|
|
1247
|
+
const bounds = annotation.bounds ?? annotationBlockBounds(annotation, label, measure);
|
|
1248
|
+
const relevant = obstacles.filter(
|
|
1249
|
+
(obs) => obs.width > 0 && obs.height > 0 && REFLINE_OBSTACLE_KINDS.has(obs.kind)
|
|
1250
|
+
);
|
|
1251
|
+
if (!relevant.some((obs) => detectCollision(bounds, obs))) return false;
|
|
1252
|
+
const { start, end } = annotation.line;
|
|
1253
|
+
const isHorizontal = Math.abs(end.y - start.y) <= Math.abs(end.x - start.x);
|
|
1254
|
+
const fontSize = label.style.fontSize ?? DEFAULT_ANNOTATION_FONT_SIZE;
|
|
1255
|
+
const anchor = label.style.textAnchor;
|
|
1256
|
+
const flipped = anchor === "start" ? "end" : anchor === "end" ? "start" : void 0;
|
|
1257
|
+
const mirror = (value, from, to) => to - (value - from);
|
|
1258
|
+
const candidates = [];
|
|
1259
|
+
if (isHorizontal) {
|
|
1260
|
+
const atEnd = Math.abs(label.x - end.x) <= Math.abs(label.x - start.x);
|
|
1261
|
+
const swapDx = mirror(label.x, atEnd ? end.x : start.x, atEnd ? start.x : end.x) - label.x;
|
|
1262
|
+
const flipDy = (label.y <= start.y ? 1 : -1) * (fontSize + NUDGE_PADDING);
|
|
1263
|
+
candidates.push({ dx: swapDx, dy: 0, textAnchor: flipped });
|
|
1264
|
+
candidates.push({ dx: 0, dy: flipDy });
|
|
1265
|
+
candidates.push({ dx: swapDx, dy: flipDy, textAnchor: flipped });
|
|
1266
|
+
} else {
|
|
1267
|
+
const atEnd = Math.abs(label.y - end.y) <= Math.abs(label.y - start.y);
|
|
1268
|
+
const swapDy = mirror(label.y, atEnd ? end.y : start.y, atEnd ? start.y : end.y) - label.y;
|
|
1269
|
+
const flipDx = -2 * (label.x - start.x);
|
|
1270
|
+
candidates.push({ dx: 0, dy: swapDy });
|
|
1271
|
+
candidates.push({ dx: flipDx, dy: 0, textAnchor: flipped });
|
|
1272
|
+
candidates.push({ dx: flipDx, dy: swapDy, textAnchor: flipped });
|
|
1273
|
+
}
|
|
1274
|
+
const slides = generateNudgeCandidates(bounds, relevant, NUDGE_PADDING).filter((c) => isHorizontal ? c.dy === 0 : c.dx === 0).slice(0, MAX_REFLINE_SLIDE_CANDIDATES);
|
|
1275
|
+
for (const slide of slides) {
|
|
1276
|
+
candidates.push({ dx: slide.dx, dy: slide.dy });
|
|
1277
|
+
}
|
|
1278
|
+
for (const { dx, dy, textAnchor } of candidates) {
|
|
1279
|
+
if (dx === 0 && dy === 0 && !textAnchor) continue;
|
|
1280
|
+
const trial = {
|
|
1281
|
+
...label,
|
|
1282
|
+
x: label.x + dx,
|
|
1283
|
+
y: label.y + dy,
|
|
1284
|
+
...textAnchor ? { style: { ...label.style, textAnchor } } : {}
|
|
1285
|
+
};
|
|
1286
|
+
const trialBounds = annotationBlockBounds(annotation, trial, measure);
|
|
1287
|
+
if (relevant.some((obs) => detectCollision(trialBounds, obs))) continue;
|
|
1288
|
+
const centerX = trialBounds.x + trialBounds.width / 2;
|
|
1289
|
+
const centerY = trialBounds.y + trialBounds.height / 2;
|
|
1290
|
+
const inBounds = centerX >= chartArea.x && centerX <= chartArea.x + chartArea.width && centerY >= chartArea.y - fontSize && centerY <= chartArea.y + chartArea.height + fontSize;
|
|
1291
|
+
if (!inBounds) continue;
|
|
1292
|
+
moveAnnotationBy(annotation, label, dx, dy, measure, textAnchor);
|
|
1293
|
+
return true;
|
|
1294
|
+
}
|
|
1295
|
+
return false;
|
|
1296
|
+
}
|
|
1297
|
+
var PINNED_OVERLAP_RATIO = 0.25;
|
|
1298
|
+
function collectPinnedOverlapWarnings(annotations, specs, obstacles, measure = heuristicMeasure) {
|
|
1299
|
+
const marks = obstacles.filter((obs) => obs.kind === "mark" && obs.width > 0 && obs.height > 0);
|
|
1300
|
+
if (marks.length === 0) return [];
|
|
1301
|
+
const warnings = [];
|
|
1302
|
+
for (const [i, annotation] of annotations.entries()) {
|
|
1303
|
+
if (!annotation.label) continue;
|
|
1304
|
+
const spec = specs[annotation.specIndex ?? i];
|
|
1305
|
+
if (!spec) continue;
|
|
1306
|
+
const pinned = spec.type === "text" && hasExplicitOffset(spec) || spec.type === "refline" && hasExplicitLabelOffset(spec);
|
|
1307
|
+
if (!pinned) continue;
|
|
1308
|
+
const bounds = annotation.bounds ?? annotationBlockBounds(annotation, annotation.label, measure);
|
|
1309
|
+
const area3 = bounds.width * bounds.height;
|
|
1310
|
+
if (area3 <= 0) continue;
|
|
1311
|
+
let covered = 0;
|
|
1312
|
+
let hits = 0;
|
|
1313
|
+
for (const mark of marks) {
|
|
1314
|
+
const overlap = overlapArea2(bounds, mark);
|
|
1315
|
+
if (overlap <= 0) continue;
|
|
1316
|
+
covered += overlap;
|
|
1317
|
+
hits++;
|
|
1318
|
+
}
|
|
1319
|
+
if (hits > 0 && covered / area3 > PINNED_OVERLAP_RATIO) {
|
|
1320
|
+
warnings.push(
|
|
1321
|
+
`[openchart] annotation "${annotation.label.text}" overlaps ${hits} mark${hits === 1 ? "" : "s"}; it is pinned by an explicit offset. Remove the offset to let auto-placement find a clear spot.`
|
|
1322
|
+
);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
return warnings;
|
|
1326
|
+
}
|
|
1226
1327
|
|
|
1227
1328
|
// src/annotations/resolve-range.ts
|
|
1228
1329
|
function resolveRangeAnnotation(annotation, scales, chartArea, isDark, fontFamily) {
|
|
@@ -1289,7 +1390,7 @@ function resolveRangeAnnotation(annotation, scales, chartArea, isDark, fontFamil
|
|
|
1289
1390
|
}
|
|
1290
1391
|
|
|
1291
1392
|
// src/annotations/resolve-refline.ts
|
|
1292
|
-
function resolveRefLineAnnotation(annotation, scales, chartArea, isDark, fontFamily) {
|
|
1393
|
+
function resolveRefLineAnnotation(annotation, scales, chartArea, isDark, measure = heuristicMeasure, fontFamily) {
|
|
1293
1394
|
let start;
|
|
1294
1395
|
let end;
|
|
1295
1396
|
if (annotation.y !== void 0) {
|
|
@@ -1382,7 +1483,7 @@ function resolveRefLineAnnotation(annotation, scales, chartArea, isDark, fontFam
|
|
|
1382
1483
|
};
|
|
1383
1484
|
}
|
|
1384
1485
|
const defaultStroke = isDark ? DARK_REFLINE_STROKE : LIGHT_REFLINE_STROKE;
|
|
1385
|
-
|
|
1486
|
+
const resolved = {
|
|
1386
1487
|
type: "refline",
|
|
1387
1488
|
id: annotation.id,
|
|
1388
1489
|
line: { start, end },
|
|
@@ -1392,6 +1493,10 @@ function resolveRefLineAnnotation(annotation, scales, chartArea, isDark, fontFam
|
|
|
1392
1493
|
strokeWidth: annotation.strokeWidth ?? 1,
|
|
1393
1494
|
zIndex: annotation.zIndex
|
|
1394
1495
|
};
|
|
1496
|
+
if (label) {
|
|
1497
|
+
resolved.bounds = annotationBlockBounds(resolved, label, measure);
|
|
1498
|
+
}
|
|
1499
|
+
return resolved;
|
|
1395
1500
|
}
|
|
1396
1501
|
|
|
1397
1502
|
// src/annotations/compute.ts
|
|
@@ -1461,6 +1566,7 @@ function computeAnnotations(spec, scalesOrCtx, chartArea, strategy, isDark, obst
|
|
|
1461
1566
|
ctx.scales,
|
|
1462
1567
|
ctx.chartArea,
|
|
1463
1568
|
ctx.isDark,
|
|
1569
|
+
ctx.measure,
|
|
1464
1570
|
ctx.fontFamily
|
|
1465
1571
|
);
|
|
1466
1572
|
break;
|
|
@@ -1476,6 +1582,9 @@ function computeAnnotations(spec, scalesOrCtx, chartArea, strategy, isDark, obst
|
|
|
1476
1582
|
ctx.measure
|
|
1477
1583
|
);
|
|
1478
1584
|
}
|
|
1585
|
+
if (annotation.type === "refline" && ctx.obstacles.length > 0) {
|
|
1586
|
+
nudgeRefLineLabel(resolved, annotation, ctx.obstacles, ctx.chartArea, ctx.measure);
|
|
1587
|
+
}
|
|
1479
1588
|
resolved.specIndex = specIndex;
|
|
1480
1589
|
annotations.push(resolved);
|
|
1481
1590
|
}
|
|
@@ -2274,7 +2383,40 @@ function formatLabelValue(value, ctx) {
|
|
|
2274
2383
|
}
|
|
2275
2384
|
|
|
2276
2385
|
// src/charts/utils.ts
|
|
2277
|
-
|
|
2386
|
+
import {
|
|
2387
|
+
adaptForLightLineStroke,
|
|
2388
|
+
CATEGORICAL_PALETTE,
|
|
2389
|
+
getRepresentativeColor
|
|
2390
|
+
} from "@opendata-ai/openchart-core";
|
|
2391
|
+
var DEFAULT_COLOR = CATEGORICAL_PALETTE[0];
|
|
2392
|
+
var BAR_CORNER_RADIUS = 2;
|
|
2393
|
+
function valueEndCorners(orient, negative) {
|
|
2394
|
+
if (orient === "horizontal") {
|
|
2395
|
+
return negative ? { tl: true, tr: false, br: false, bl: true } : { tl: false, tr: true, br: true, bl: false };
|
|
2396
|
+
}
|
|
2397
|
+
return negative ? { tl: false, tr: false, br: true, bl: true } : { tl: true, tr: true, br: false, bl: false };
|
|
2398
|
+
}
|
|
2399
|
+
function stackSeamStroke(theme) {
|
|
2400
|
+
return resolveKnockoutColor(theme);
|
|
2401
|
+
}
|
|
2402
|
+
function resolveKnockoutColor(theme) {
|
|
2403
|
+
return theme?.colors?.neutral?.surface ?? "#ffffff";
|
|
2404
|
+
}
|
|
2405
|
+
function isStrokeSeriesMark(markType) {
|
|
2406
|
+
return markType === "line" || markType === "area";
|
|
2407
|
+
}
|
|
2408
|
+
function adaptSeriesStroke(color, theme) {
|
|
2409
|
+
const str = getRepresentativeColor(color);
|
|
2410
|
+
return theme && !theme.isDark ? adaptForLightLineStroke(str) : str;
|
|
2411
|
+
}
|
|
2412
|
+
function hasExplicitColorRange(spec) {
|
|
2413
|
+
const colorEnc = spec.encoding?.color;
|
|
2414
|
+
if (!colorEnc || !("field" in colorEnc)) return false;
|
|
2415
|
+
return Array.isArray(colorEnc.scale?.range);
|
|
2416
|
+
}
|
|
2417
|
+
function resolveSeriesStroke(spec, color, theme) {
|
|
2418
|
+
return hasExplicitColorRange(spec) ? getRepresentativeColor(color) : adaptSeriesStroke(color, theme);
|
|
2419
|
+
}
|
|
2278
2420
|
function scaleValue(scale, scaleType, value) {
|
|
2279
2421
|
if (value == null) return null;
|
|
2280
2422
|
if (scaleType === "time" || scaleType === "utc") {
|
|
@@ -2348,6 +2490,14 @@ function getSequentialColor(scales, value, fallback = DEFAULT_COLOR) {
|
|
|
2348
2490
|
}
|
|
2349
2491
|
|
|
2350
2492
|
// src/charts/bar/compute.ts
|
|
2493
|
+
function applyStackSeams(marks, theme) {
|
|
2494
|
+
const seam = stackSeamStroke(theme);
|
|
2495
|
+
for (const mark of marks) {
|
|
2496
|
+
if (mark.stackGroup === void 0) continue;
|
|
2497
|
+
mark.stroke = seam;
|
|
2498
|
+
mark.strokeWidth = 1;
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2351
2501
|
function orientGradientForHorizontalBar(grad) {
|
|
2352
2502
|
if (grad.gradient !== "linear") return grad;
|
|
2353
2503
|
const lg = grad;
|
|
@@ -2357,7 +2507,7 @@ function orientGradientForHorizontalBar(grad) {
|
|
|
2357
2507
|
}
|
|
2358
2508
|
var MIN_BAR_WIDTH = 1;
|
|
2359
2509
|
var MIN_GROUPED_BAR_THICKNESS = 8;
|
|
2360
|
-
function computeBarMarks(spec, scales, _chartArea, _strategy, containerWidth) {
|
|
2510
|
+
function computeBarMarks(spec, scales, _chartArea, _strategy, containerWidth, theme) {
|
|
2361
2511
|
const encoding = spec.encoding;
|
|
2362
2512
|
const xChannel = encoding.x;
|
|
2363
2513
|
const yChannel = encoding.y;
|
|
@@ -2440,6 +2590,7 @@ function computeBarMarks(spec, scales, _chartArea, _strategy, containerWidth) {
|
|
|
2440
2590
|
}
|
|
2441
2591
|
const categoryField = yChannel.field;
|
|
2442
2592
|
stampBarKeys(marks, categoryField, colorField);
|
|
2593
|
+
applyStackSeams(marks, theme);
|
|
2443
2594
|
return applyMarkDefOverrides(marks, spec, bandwidth);
|
|
2444
2595
|
}
|
|
2445
2596
|
function stampBarKeys(marks, categoryField, colorField) {
|
|
@@ -2485,6 +2636,7 @@ function computeStackedBars(data, valueField, categoryField, colorField, xScale,
|
|
|
2485
2636
|
height: bandwidth,
|
|
2486
2637
|
fill: isGradientDef(color) ? orientGradientForHorizontalBar(color) : color,
|
|
2487
2638
|
cornerRadius: 0,
|
|
2639
|
+
seriesKey: groupKey3,
|
|
2488
2640
|
data: row,
|
|
2489
2641
|
aria,
|
|
2490
2642
|
orient: "horizontal",
|
|
@@ -2541,7 +2693,9 @@ function computeGroupedBars(data, valueField, categoryField, colorField, xScale,
|
|
|
2541
2693
|
width: barWidth,
|
|
2542
2694
|
height: subBandHeight,
|
|
2543
2695
|
fill: isGradientDef(color) ? orientGradientForHorizontalBar(color) : color,
|
|
2544
|
-
cornerRadius:
|
|
2696
|
+
cornerRadius: BAR_CORNER_RADIUS,
|
|
2697
|
+
cornerRadiusSides: valueEndCorners("horizontal", value < 0),
|
|
2698
|
+
seriesKey: groupKey3,
|
|
2545
2699
|
data: row,
|
|
2546
2700
|
aria,
|
|
2547
2701
|
orient: "horizontal"
|
|
@@ -2572,7 +2726,9 @@ function computeColoredBars(data, valueField, categoryField, colorField, xScale,
|
|
|
2572
2726
|
width: barWidth,
|
|
2573
2727
|
height: bandwidth,
|
|
2574
2728
|
fill: isGradientDef(color) ? orientGradientForHorizontalBar(color) : color,
|
|
2575
|
-
cornerRadius:
|
|
2729
|
+
cornerRadius: BAR_CORNER_RADIUS,
|
|
2730
|
+
cornerRadiusSides: valueEndCorners("horizontal", value < 0),
|
|
2731
|
+
seriesKey: groupKey3,
|
|
2576
2732
|
data: row,
|
|
2577
2733
|
aria,
|
|
2578
2734
|
orient: "horizontal"
|
|
@@ -2607,6 +2763,7 @@ function applyMarkDefOverrides(marks, spec, bandwidth) {
|
|
|
2607
2763
|
if (isStacked2 && !isStackRight) continue;
|
|
2608
2764
|
if (crSpec === "pill") {
|
|
2609
2765
|
mark.cornerRadius = effectiveHeight / 2;
|
|
2766
|
+
mark.cornerRadiusSides = void 0;
|
|
2610
2767
|
} else if (typeof crSpec === "number") {
|
|
2611
2768
|
mark.cornerRadius = crSpec;
|
|
2612
2769
|
} else {
|
|
@@ -2651,7 +2808,8 @@ function computeSimpleBars(data, valueField, categoryField, xScale, yScale, band
|
|
|
2651
2808
|
width: barWidth,
|
|
2652
2809
|
height: bandwidth,
|
|
2653
2810
|
fill: isGradientDef(color) ? orientGradientForHorizontalBar(color) : color,
|
|
2654
|
-
cornerRadius:
|
|
2811
|
+
cornerRadius: BAR_CORNER_RADIUS,
|
|
2812
|
+
cornerRadiusSides: valueEndCorners("horizontal", value < 0),
|
|
2655
2813
|
data: row,
|
|
2656
2814
|
aria,
|
|
2657
2815
|
orient: "horizontal"
|
|
@@ -2663,7 +2821,7 @@ function computeSimpleBars(data, valueField, categoryField, xScale, yScale, band
|
|
|
2663
2821
|
// src/charts/bar/labels.ts
|
|
2664
2822
|
import {
|
|
2665
2823
|
estimateTextWidth as estimateTextWidth3,
|
|
2666
|
-
getRepresentativeColor,
|
|
2824
|
+
getRepresentativeColor as getRepresentativeColor2,
|
|
2667
2825
|
pickLabelColor,
|
|
2668
2826
|
resolveCollisions
|
|
2669
2827
|
} from "@opendata-ai/openchart-core";
|
|
@@ -2703,7 +2861,7 @@ var LABEL_FONT_SIZE = 11;
|
|
|
2703
2861
|
var LABEL_FONT_WEIGHT = 600;
|
|
2704
2862
|
var LABEL_PADDING = 6;
|
|
2705
2863
|
var MIN_WIDTH_FOR_INSIDE_LABEL = 40;
|
|
2706
|
-
function computeBarLabels(marks,
|
|
2864
|
+
function computeBarLabels(marks, chartArea, density = "auto", labelFormatter, labelPrefix, valueField, labelColor, darkMode = false, fontSize, labelSuffix) {
|
|
2707
2865
|
const FONT_SIZE = fontSize ?? LABEL_FONT_SIZE;
|
|
2708
2866
|
const targetMarks = filterByDensity(marks, density);
|
|
2709
2867
|
const candidates = [];
|
|
@@ -2737,7 +2895,7 @@ function computeBarLabels(marks, _chartArea, density = "auto", labelFormatter, l
|
|
|
2737
2895
|
const isStacked2 = mark.stackGroup !== void 0;
|
|
2738
2896
|
const isInside = mark.width >= MIN_WIDTH_FOR_INSIDE_LABEL;
|
|
2739
2897
|
const isNegative = Number.isFinite(rawNum) ? rawNum < 0 : false;
|
|
2740
|
-
const bgColor =
|
|
2898
|
+
const bgColor = getRepresentativeColor2(mark.fill);
|
|
2741
2899
|
let anchorX;
|
|
2742
2900
|
let fill;
|
|
2743
2901
|
let textAnchor;
|
|
@@ -2757,12 +2915,12 @@ function computeBarLabels(marks, _chartArea, density = "auto", labelFormatter, l
|
|
|
2757
2915
|
}
|
|
2758
2916
|
} else {
|
|
2759
2917
|
if (isNegative) {
|
|
2760
|
-
anchorX = mark.x - LABEL_PADDING;
|
|
2761
|
-
fill = labelColor ??
|
|
2918
|
+
anchorX = Math.min(mark.x - LABEL_PADDING, chartArea.x + chartArea.width - LABEL_PADDING);
|
|
2919
|
+
fill = labelColor ?? getRepresentativeColor2(mark.fill);
|
|
2762
2920
|
textAnchor = "end";
|
|
2763
2921
|
} else {
|
|
2764
|
-
anchorX = mark.x + mark.width + LABEL_PADDING;
|
|
2765
|
-
fill = labelColor ??
|
|
2922
|
+
anchorX = Math.max(mark.x + mark.width + LABEL_PADDING, chartArea.x + LABEL_PADDING);
|
|
2923
|
+
fill = labelColor ?? getRepresentativeColor2(mark.fill);
|
|
2766
2924
|
textAnchor = "start";
|
|
2767
2925
|
}
|
|
2768
2926
|
}
|
|
@@ -2846,7 +3004,7 @@ function computeBarLabels(marks, _chartArea, density = "auto", labelFormatter, l
|
|
|
2846
3004
|
|
|
2847
3005
|
// src/charts/bar/index.ts
|
|
2848
3006
|
var barRenderer = (spec, scales, chartArea, strategy, theme, containerWidth) => {
|
|
2849
|
-
const marks = computeBarMarks(spec, scales, chartArea, strategy, containerWidth);
|
|
3007
|
+
const marks = computeBarMarks(spec, scales, chartArea, strategy, containerWidth, theme);
|
|
2850
3008
|
const valueField = spec.encoding?.x && "field" in spec.encoding.x ? spec.encoding.x.field : void 0;
|
|
2851
3009
|
const labelFormatter = resolveFieldFormatter({
|
|
2852
3010
|
surfaceFormat: spec.labels.format,
|
|
@@ -2981,7 +3139,7 @@ function minimalFreeOffset(intervals) {
|
|
|
2981
3139
|
|
|
2982
3140
|
// src/charts/beeswarm/compute.ts
|
|
2983
3141
|
var DEFAULT_DOT_RADIUS2 = 4;
|
|
2984
|
-
function computeBeeswarmMarks(spec, scales, chartArea, _strategy) {
|
|
3142
|
+
function computeBeeswarmMarks(spec, scales, chartArea, _strategy, theme) {
|
|
2985
3143
|
const encoding = spec.encoding;
|
|
2986
3144
|
const isHorizontal = encoding.x?.type === "quantitative";
|
|
2987
3145
|
const valueChannel = isHorizontal ? encoding.x : encoding.y;
|
|
@@ -3062,7 +3220,7 @@ function computeBeeswarmMarks(spec, scales, chartArea, _strategy) {
|
|
|
3062
3220
|
cy: isHorizontal ? cross : dot.position,
|
|
3063
3221
|
r: dot.radius,
|
|
3064
3222
|
fill: color,
|
|
3065
|
-
stroke:
|
|
3223
|
+
stroke: resolveKnockoutColor(theme),
|
|
3066
3224
|
strokeWidth: 1,
|
|
3067
3225
|
data: dot.row,
|
|
3068
3226
|
aria
|
|
@@ -3081,8 +3239,8 @@ function computeBeeswarmMarks(spec, scales, chartArea, _strategy) {
|
|
|
3081
3239
|
}
|
|
3082
3240
|
|
|
3083
3241
|
// src/charts/beeswarm/index.ts
|
|
3084
|
-
var beeswarmRenderer = (spec, scales, chartArea, strategy,
|
|
3085
|
-
return computeBeeswarmMarks(spec, scales, chartArea, strategy);
|
|
3242
|
+
var beeswarmRenderer = (spec, scales, chartArea, strategy, theme) => {
|
|
3243
|
+
return computeBeeswarmMarks(spec, scales, chartArea, strategy, theme);
|
|
3086
3244
|
};
|
|
3087
3245
|
|
|
3088
3246
|
// src/charts/calendar/compute.ts
|
|
@@ -3090,10 +3248,11 @@ import { defaultNumberFormatter as defaultNumberFormatter4, formatDate } from "@
|
|
|
3090
3248
|
import { utcDay, utcMonday, utcSunday } from "d3-time";
|
|
3091
3249
|
var MIN_CELL_SIZE = 7;
|
|
3092
3250
|
var MAX_STEP = 20;
|
|
3093
|
-
var
|
|
3251
|
+
var MIN_CELL_GAP = 2;
|
|
3252
|
+
var MIN_STEP = MIN_CELL_SIZE + MIN_CELL_GAP;
|
|
3094
3253
|
var BAND_GAP = 18;
|
|
3095
3254
|
var WEEKDAY_LABEL_GAP = 6;
|
|
3096
|
-
var DEFAULT_CELL_RADIUS =
|
|
3255
|
+
var DEFAULT_CELL_RADIUS = 2;
|
|
3097
3256
|
var WEEKDAY_LABELS = [
|
|
3098
3257
|
{ dow: 1, label: "Mon" },
|
|
3099
3258
|
{ dow: 3, label: "Wed" },
|
|
@@ -3168,7 +3327,7 @@ function computeCalendarMarks(spec, scales, chartArea, theme) {
|
|
|
3168
3327
|
MAX_STEP,
|
|
3169
3328
|
Math.max(MIN_STEP, Math.floor(Math.min(stepFromWidth, stepFromHeight)))
|
|
3170
3329
|
);
|
|
3171
|
-
const cellGap = step >= 14 ? 3 :
|
|
3330
|
+
const cellGap = step >= 14 ? 3 : MIN_CELL_GAP;
|
|
3172
3331
|
const cell = step - cellGap;
|
|
3173
3332
|
const gridWidth = maxColumns * step - cellGap;
|
|
3174
3333
|
const bandHeight = monthRow + 7 * step - cellGap;
|
|
@@ -3251,6 +3410,7 @@ function computeCalendarMarks(spec, scales, chartArea, theme) {
|
|
|
3251
3410
|
height: cell,
|
|
3252
3411
|
fill: getSequentialColor(scales, datum.value),
|
|
3253
3412
|
cornerRadius: cellRadius,
|
|
3413
|
+
shapeRendering: "crispEdges",
|
|
3254
3414
|
data: datum.row,
|
|
3255
3415
|
aria: {
|
|
3256
3416
|
label: `${dateLabel}, ${colorEnc.field}: ${calendarFmt(datum.value)}`
|
|
@@ -3266,6 +3426,7 @@ function computeCalendarMarks(spec, scales, chartArea, theme) {
|
|
|
3266
3426
|
height: cell,
|
|
3267
3427
|
fill: theme.colors.annotationFill,
|
|
3268
3428
|
cornerRadius: cellRadius,
|
|
3429
|
+
shapeRendering: "crispEdges",
|
|
3269
3430
|
data: {},
|
|
3270
3431
|
aria: { decorative: true }
|
|
3271
3432
|
});
|
|
@@ -3282,8 +3443,16 @@ var calendarRenderer = (spec, scales, chartArea, _strategy, theme) => {
|
|
|
3282
3443
|
|
|
3283
3444
|
// src/charts/column/compute.ts
|
|
3284
3445
|
import { isGradientDef as isGradientDef2 } from "@opendata-ai/openchart-core";
|
|
3446
|
+
function applyStackSeams2(marks, theme) {
|
|
3447
|
+
const seam = stackSeamStroke(theme);
|
|
3448
|
+
for (const mark of marks) {
|
|
3449
|
+
if (mark.stackGroup === void 0) continue;
|
|
3450
|
+
mark.stroke = seam;
|
|
3451
|
+
mark.strokeWidth = 1;
|
|
3452
|
+
}
|
|
3453
|
+
}
|
|
3285
3454
|
var MIN_COLUMN_HEIGHT = 1;
|
|
3286
|
-
function computeColumnMarks(spec, scales, _chartArea, _strategy) {
|
|
3455
|
+
function computeColumnMarks(spec, scales, _chartArea, _strategy, theme) {
|
|
3287
3456
|
const encoding = spec.encoding;
|
|
3288
3457
|
const xChannel = encoding.x;
|
|
3289
3458
|
const yChannel = encoding.y;
|
|
@@ -3365,6 +3534,7 @@ function computeColumnMarks(spec, scales, _chartArea, _strategy) {
|
|
|
3365
3534
|
}
|
|
3366
3535
|
const categoryField = xChannel.field;
|
|
3367
3536
|
stampColumnKeys(marks, categoryField, colorField);
|
|
3537
|
+
applyStackSeams2(marks, theme);
|
|
3368
3538
|
return applyMarkDefOverrides2(marks, spec, bandwidth);
|
|
3369
3539
|
}
|
|
3370
3540
|
function stampColumnKeys(marks, categoryField, colorField) {
|
|
@@ -3412,7 +3582,8 @@ function computeSimpleColumns(data, categoryField, valueField, xScale, yScale, b
|
|
|
3412
3582
|
width: bandwidth,
|
|
3413
3583
|
height: columnHeight,
|
|
3414
3584
|
fill: color,
|
|
3415
|
-
cornerRadius:
|
|
3585
|
+
cornerRadius: BAR_CORNER_RADIUS,
|
|
3586
|
+
cornerRadiusSides: valueEndCorners("vertical", value < 0),
|
|
3416
3587
|
data: row,
|
|
3417
3588
|
aria,
|
|
3418
3589
|
orient: "vertical"
|
|
@@ -3443,7 +3614,9 @@ function computeColoredColumns(data, categoryField, valueField, colorField, xSca
|
|
|
3443
3614
|
width: bandwidth,
|
|
3444
3615
|
height: columnHeight,
|
|
3445
3616
|
fill: color,
|
|
3446
|
-
cornerRadius:
|
|
3617
|
+
cornerRadius: BAR_CORNER_RADIUS,
|
|
3618
|
+
cornerRadiusSides: valueEndCorners("vertical", value < 0),
|
|
3619
|
+
seriesKey: groupKey3,
|
|
3447
3620
|
data: row,
|
|
3448
3621
|
aria,
|
|
3449
3622
|
orient: "vertical"
|
|
@@ -3491,7 +3664,9 @@ function computeGroupedColumns(data, categoryField, valueField, colorField, xSca
|
|
|
3491
3664
|
width: subBandWidth,
|
|
3492
3665
|
height: columnHeight,
|
|
3493
3666
|
fill: color,
|
|
3494
|
-
cornerRadius:
|
|
3667
|
+
cornerRadius: BAR_CORNER_RADIUS,
|
|
3668
|
+
cornerRadiusSides: valueEndCorners("vertical", value < 0),
|
|
3669
|
+
seriesKey: groupKey3,
|
|
3495
3670
|
data: row,
|
|
3496
3671
|
aria,
|
|
3497
3672
|
orient: "vertical"
|
|
@@ -3532,6 +3707,7 @@ function computeStackedColumns(data, categoryField, valueField, colorField, xSca
|
|
|
3532
3707
|
height: columnHeight,
|
|
3533
3708
|
fill: color,
|
|
3534
3709
|
cornerRadius: 0,
|
|
3710
|
+
seriesKey: groupKey3,
|
|
3535
3711
|
data: row,
|
|
3536
3712
|
aria,
|
|
3537
3713
|
orient: "vertical",
|
|
@@ -3568,6 +3744,7 @@ function applyMarkDefOverrides2(marks, spec, bandwidth) {
|
|
|
3568
3744
|
if (isStacked2 && !isStackTop) continue;
|
|
3569
3745
|
if (crSpec === "pill") {
|
|
3570
3746
|
mark.cornerRadius = effectiveWidth / 2;
|
|
3747
|
+
mark.cornerRadiusSides = void 0;
|
|
3571
3748
|
} else if (typeof crSpec === "number") {
|
|
3572
3749
|
mark.cornerRadius = crSpec;
|
|
3573
3750
|
} else {
|
|
@@ -3583,7 +3760,7 @@ function applyMarkDefOverrides2(marks, spec, bandwidth) {
|
|
|
3583
3760
|
// src/charts/column/labels.ts
|
|
3584
3761
|
import {
|
|
3585
3762
|
estimateTextWidth as estimateTextWidth4,
|
|
3586
|
-
getRepresentativeColor as
|
|
3763
|
+
getRepresentativeColor as getRepresentativeColor3,
|
|
3587
3764
|
pickLabelColor as pickLabelColor2,
|
|
3588
3765
|
resolveCollisions as resolveCollisions2,
|
|
3589
3766
|
textAscent
|
|
@@ -3630,10 +3807,10 @@ function computeColumnLabels(marks, _chartArea, density = "auto", labelFormatter
|
|
|
3630
3807
|
let fill;
|
|
3631
3808
|
if (isStacked2) {
|
|
3632
3809
|
anchorY = mark.y + mark.height / 2 - textHeight / 2;
|
|
3633
|
-
fill = labelColor ?? pickLabelColor2(
|
|
3810
|
+
fill = labelColor ?? pickLabelColor2(getRepresentativeColor3(mark.fill), darkMode);
|
|
3634
3811
|
} else {
|
|
3635
3812
|
anchorY = isNegative ? mark.y + mark.height + LABEL_OFFSET_Y : mark.y - LABEL_OFFSET_Y - textHeight;
|
|
3636
|
-
fill = labelColor ??
|
|
3813
|
+
fill = labelColor ?? getRepresentativeColor3(mark.fill);
|
|
3637
3814
|
}
|
|
3638
3815
|
fitsInSegment.push(!(isStacked2 && textHeight > mark.height - 2 * LABEL_PADDING_Y));
|
|
3639
3816
|
candidates.push({
|
|
@@ -3700,7 +3877,7 @@ function computeColumnLabels(marks, _chartArea, density = "auto", labelFormatter
|
|
|
3700
3877
|
|
|
3701
3878
|
// src/charts/column/index.ts
|
|
3702
3879
|
var columnRenderer = (spec, scales, chartArea, strategy, theme) => {
|
|
3703
|
-
const marks = computeColumnMarks(spec, scales, chartArea, strategy);
|
|
3880
|
+
const marks = computeColumnMarks(spec, scales, chartArea, strategy, theme);
|
|
3704
3881
|
const valueField = spec.encoding?.y && "field" in spec.encoding.y ? spec.encoding.y.field : void 0;
|
|
3705
3882
|
const labelFormatter = resolveFieldFormatter({
|
|
3706
3883
|
surfaceFormat: spec.labels.format,
|
|
@@ -3729,7 +3906,7 @@ var columnRenderer = (spec, scales, chartArea, strategy, theme) => {
|
|
|
3729
3906
|
var DOT_RADIUS = 6;
|
|
3730
3907
|
var STEM_WIDTH = 2;
|
|
3731
3908
|
var STEM_COLOR = "#cccccc";
|
|
3732
|
-
function computeDotMarks(spec, scales, _chartArea, _strategy) {
|
|
3909
|
+
function computeDotMarks(spec, scales, _chartArea, _strategy, theme) {
|
|
3733
3910
|
const encoding = spec.encoding;
|
|
3734
3911
|
const xChannel = encoding.x;
|
|
3735
3912
|
const yChannel = encoding.y;
|
|
@@ -3759,7 +3936,8 @@ function computeDotMarks(spec, scales, _chartArea, _strategy) {
|
|
|
3759
3936
|
xScale,
|
|
3760
3937
|
yScale,
|
|
3761
3938
|
bandwidth,
|
|
3762
|
-
scales
|
|
3939
|
+
scales,
|
|
3940
|
+
resolveKnockoutColor(theme)
|
|
3763
3941
|
);
|
|
3764
3942
|
} else {
|
|
3765
3943
|
marks = computeLollipopMarks(
|
|
@@ -3771,7 +3949,8 @@ function computeDotMarks(spec, scales, _chartArea, _strategy) {
|
|
|
3771
3949
|
bandwidth,
|
|
3772
3950
|
baseline,
|
|
3773
3951
|
scales,
|
|
3774
|
-
isSequentialColor
|
|
3952
|
+
isSequentialColor,
|
|
3953
|
+
resolveKnockoutColor(theme)
|
|
3775
3954
|
);
|
|
3776
3955
|
}
|
|
3777
3956
|
const pointMarks = marks.filter((m) => m.type === "point");
|
|
@@ -3786,7 +3965,7 @@ function computeDotMarks(spec, scales, _chartArea, _strategy) {
|
|
|
3786
3965
|
}
|
|
3787
3966
|
return marks;
|
|
3788
3967
|
}
|
|
3789
|
-
function computeDumbbellMarks(data, valueField, categoryField, colorField, xScale, yScale, bandwidth, scales) {
|
|
3968
|
+
function computeDumbbellMarks(data, valueField, categoryField, colorField, xScale, yScale, bandwidth, scales, dotKnockout) {
|
|
3790
3969
|
const marks = [];
|
|
3791
3970
|
const categoryGroups = groupByField([...data], categoryField);
|
|
3792
3971
|
for (const [category, rows] of categoryGroups) {
|
|
@@ -3834,7 +4013,7 @@ function computeDumbbellMarks(data, valueField, categoryField, colorField, xScal
|
|
|
3834
4013
|
cy,
|
|
3835
4014
|
r: DOT_RADIUS,
|
|
3836
4015
|
fill: color,
|
|
3837
|
-
stroke:
|
|
4016
|
+
stroke: dotKnockout,
|
|
3838
4017
|
strokeWidth: 2,
|
|
3839
4018
|
data: row,
|
|
3840
4019
|
aria: dotAria
|
|
@@ -3843,7 +4022,7 @@ function computeDumbbellMarks(data, valueField, categoryField, colorField, xScal
|
|
|
3843
4022
|
}
|
|
3844
4023
|
return marks;
|
|
3845
4024
|
}
|
|
3846
|
-
function computeLollipopMarks(data, valueField, categoryField, xScale, yScale, bandwidth, baseline, scales, isSequentialColor = false) {
|
|
4025
|
+
function computeLollipopMarks(data, valueField, categoryField, xScale, yScale, bandwidth, baseline, scales, isSequentialColor = false, dotKnockout = "#ffffff") {
|
|
3847
4026
|
const marks = [];
|
|
3848
4027
|
for (const row of data) {
|
|
3849
4028
|
const category = String(row[categoryField] ?? "");
|
|
@@ -3880,7 +4059,7 @@ function computeLollipopMarks(data, valueField, categoryField, xScale, yScale, b
|
|
|
3880
4059
|
cy,
|
|
3881
4060
|
r: DOT_RADIUS,
|
|
3882
4061
|
fill: color,
|
|
3883
|
-
stroke:
|
|
4062
|
+
stroke: dotKnockout,
|
|
3884
4063
|
strokeWidth: 2,
|
|
3885
4064
|
data: row,
|
|
3886
4065
|
aria: dotAria
|
|
@@ -3892,7 +4071,7 @@ function computeLollipopMarks(data, valueField, categoryField, xScale, yScale, b
|
|
|
3892
4071
|
// src/charts/dot/labels.ts
|
|
3893
4072
|
import {
|
|
3894
4073
|
estimateTextWidth as estimateTextWidth5,
|
|
3895
|
-
getRepresentativeColor as
|
|
4074
|
+
getRepresentativeColor as getRepresentativeColor4,
|
|
3896
4075
|
resolveCollisions as resolveCollisions3
|
|
3897
4076
|
} from "@opendata-ai/openchart-core";
|
|
3898
4077
|
var LABEL_FONT_SIZE3 = 11;
|
|
@@ -3934,7 +4113,7 @@ function computeDotLabels(marks, _chartArea, density = "auto", labelPrefix, labe
|
|
|
3934
4113
|
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
3935
4114
|
fontSize: LABEL_FONT_SIZE3,
|
|
3936
4115
|
fontWeight: LABEL_FONT_WEIGHT3,
|
|
3937
|
-
fill:
|
|
4116
|
+
fill: getRepresentativeColor4(mark.fill),
|
|
3938
4117
|
lineHeight: 1.2,
|
|
3939
4118
|
textAnchor: "start",
|
|
3940
4119
|
dominantBaseline: "central",
|
|
@@ -3956,8 +4135,8 @@ function computeDotLabels(marks, _chartArea, density = "auto", labelPrefix, labe
|
|
|
3956
4135
|
}
|
|
3957
4136
|
|
|
3958
4137
|
// src/charts/dot/index.ts
|
|
3959
|
-
var dotRenderer = (spec, scales, chartArea, strategy,
|
|
3960
|
-
const marks = computeDotMarks(spec, scales, chartArea, strategy);
|
|
4138
|
+
var dotRenderer = (spec, scales, chartArea, strategy, theme) => {
|
|
4139
|
+
const marks = computeDotMarks(spec, scales, chartArea, strategy, theme);
|
|
3961
4140
|
const pointMarks = marks.filter((m) => m.type === "point");
|
|
3962
4141
|
const valueField = spec.encoding?.x && "field" in spec.encoding.x ? spec.encoding.x.field : void 0;
|
|
3963
4142
|
const labelFormatter = resolveFieldFormatter({
|
|
@@ -3984,13 +4163,13 @@ var dotRenderer = (spec, scales, chartArea, strategy, _theme) => {
|
|
|
3984
4163
|
};
|
|
3985
4164
|
|
|
3986
4165
|
// src/charts/line/index.ts
|
|
3987
|
-
import { getRepresentativeColor as
|
|
4166
|
+
import { getRepresentativeColor as getRepresentativeColor7 } from "@opendata-ai/openchart-core";
|
|
3988
4167
|
|
|
3989
4168
|
// src/charts/line/area.ts
|
|
3990
|
-
import { getRepresentativeColor as
|
|
4169
|
+
import { getRepresentativeColor as getRepresentativeColor6, isGradientDef as isGradientDef3 } from "@opendata-ai/openchart-core";
|
|
3991
4170
|
import {
|
|
3992
4171
|
area as area2,
|
|
3993
|
-
line as
|
|
4172
|
+
line as line3,
|
|
3994
4173
|
stack,
|
|
3995
4174
|
stackOffsetExpand,
|
|
3996
4175
|
stackOffsetNone,
|
|
@@ -3998,6 +4177,10 @@ import {
|
|
|
3998
4177
|
stackOrderNone
|
|
3999
4178
|
} from "d3-shape";
|
|
4000
4179
|
|
|
4180
|
+
// src/charts/line/compute.ts
|
|
4181
|
+
import { getRepresentativeColor as getRepresentativeColor5 } from "@opendata-ai/openchart-core";
|
|
4182
|
+
import { line as line2 } from "d3-shape";
|
|
4183
|
+
|
|
4001
4184
|
// src/charts/line/curves.ts
|
|
4002
4185
|
import {
|
|
4003
4186
|
area,
|
|
@@ -4043,29 +4226,218 @@ function buildAreaPath(topPoints, bottomPoints, interpolate) {
|
|
|
4043
4226
|
return generator(paired) ?? "";
|
|
4044
4227
|
}
|
|
4045
4228
|
|
|
4229
|
+
// src/charts/line/compute.ts
|
|
4230
|
+
var LINE_STROKE_WIDTH = 2;
|
|
4231
|
+
var DEFAULT_POINT_RADIUS = 3;
|
|
4232
|
+
var ENDPOINT_DOT_RADIUS = 4;
|
|
4233
|
+
var STROKE_DASH_PATTERNS = ["", "6 4", "2 3", "8 4 2 4", "4 4", "1 3"];
|
|
4234
|
+
function computeLineMarks(spec, scales, _chartArea, _strategy, theme) {
|
|
4235
|
+
const encoding = spec.encoding;
|
|
4236
|
+
const xChannel = encoding.x;
|
|
4237
|
+
const yChannel = encoding.y;
|
|
4238
|
+
if (!xChannel || !yChannel || !scales.x || !scales.y) {
|
|
4239
|
+
return [];
|
|
4240
|
+
}
|
|
4241
|
+
const colorEnc = encoding.color && "field" in encoding.color ? encoding.color : void 0;
|
|
4242
|
+
const isSequentialColor = colorEnc?.type === "quantitative";
|
|
4243
|
+
const colorField = isSequentialColor ? void 0 : colorEnc?.field;
|
|
4244
|
+
const sequentialColorField = isSequentialColor ? colorEnc.field : void 0;
|
|
4245
|
+
const dashEnc = encoding.strokeDash && "field" in encoding.strokeDash ? encoding.strokeDash : void 0;
|
|
4246
|
+
const dashField = dashEnc?.field;
|
|
4247
|
+
const dashIndexByValue = /* @__PURE__ */ new Map();
|
|
4248
|
+
if (dashField) {
|
|
4249
|
+
for (const row of spec.data) {
|
|
4250
|
+
const value = row[dashField];
|
|
4251
|
+
if (value == null) continue;
|
|
4252
|
+
const key = String(value);
|
|
4253
|
+
if (!dashIndexByValue.has(key)) dashIndexByValue.set(key, dashIndexByValue.size);
|
|
4254
|
+
}
|
|
4255
|
+
}
|
|
4256
|
+
const groups = groupByField(spec.data, colorField ?? dashField);
|
|
4257
|
+
const pointSeparator = resolveKnockoutColor(theme);
|
|
4258
|
+
const mutedMarks = [];
|
|
4259
|
+
const highlightedMarks = [];
|
|
4260
|
+
const highlight = spec.highlight ?? [];
|
|
4261
|
+
const highlightActive = highlight.length > 0;
|
|
4262
|
+
for (const [seriesKey, rows] of groups) {
|
|
4263
|
+
const color = isSequentialColor ? getSequentialColor(scales, _getMidValue(rows, sequentialColorField)) : getColor(scales, seriesKey);
|
|
4264
|
+
const strokeColor = spec.markDef.stroke ?? resolveSeriesStroke(spec, color, theme);
|
|
4265
|
+
const sortedRows = xChannel.type === "nominal" || xChannel.type === "ordinal" ? rows : sortByField(rows, xChannel.field);
|
|
4266
|
+
const pointsWithData = [];
|
|
4267
|
+
const segments = [];
|
|
4268
|
+
let currentSegment = [];
|
|
4269
|
+
for (const row of sortedRows) {
|
|
4270
|
+
const xVal = scaleValue(scales.x.scale, scales.x.type, row[xChannel.field]);
|
|
4271
|
+
const yVal = scaleValue(scales.y.scale, scales.y.type, row[yChannel.field]);
|
|
4272
|
+
if (xVal === null || yVal === null) {
|
|
4273
|
+
if (currentSegment.length > 0) {
|
|
4274
|
+
segments.push(currentSegment);
|
|
4275
|
+
currentSegment = [];
|
|
4276
|
+
}
|
|
4277
|
+
continue;
|
|
4278
|
+
}
|
|
4279
|
+
const point = { x: xVal, y: yVal };
|
|
4280
|
+
currentSegment.push(point);
|
|
4281
|
+
pointsWithData.push({ ...point, row });
|
|
4282
|
+
}
|
|
4283
|
+
if (currentSegment.length > 0) {
|
|
4284
|
+
segments.push(currentSegment);
|
|
4285
|
+
}
|
|
4286
|
+
const curve = resolveCurve(spec.markDef.interpolate);
|
|
4287
|
+
const lineGenerator = line2().x((d) => d.x).y((d) => d.y).curve(curve);
|
|
4288
|
+
const allPoints = [];
|
|
4289
|
+
const pathParts = [];
|
|
4290
|
+
for (const segment of segments) {
|
|
4291
|
+
if (segment.length === 0) continue;
|
|
4292
|
+
const pathStr = lineGenerator(segment);
|
|
4293
|
+
if (pathStr) {
|
|
4294
|
+
pathParts.push(pathStr);
|
|
4295
|
+
}
|
|
4296
|
+
allPoints.push(...segment);
|
|
4297
|
+
}
|
|
4298
|
+
if (allPoints.length === 0) continue;
|
|
4299
|
+
const ariaLabel = seriesKey === "__default__" ? `Line with ${allPoints.length} data points` : `${seriesKey}: line with ${allPoints.length} data points`;
|
|
4300
|
+
const aria = {
|
|
4301
|
+
label: ariaLabel
|
|
4302
|
+
};
|
|
4303
|
+
const combinedPath = pathParts.join(" ");
|
|
4304
|
+
const seriesStyleKey = seriesKey === "__default__" ? void 0 : seriesKey;
|
|
4305
|
+
const styleOverride = seriesStyleKey ? spec.seriesStyles?.[seriesStyleKey] : void 0;
|
|
4306
|
+
const isMuted = highlightActive && !highlight.includes(seriesKey);
|
|
4307
|
+
let resolvedStrokeWidth = styleOverride?.strokeWidth ?? spec.markDef.strokeWidth ?? LINE_STROKE_WIDTH;
|
|
4308
|
+
if (isMuted) {
|
|
4309
|
+
resolvedStrokeWidth = Math.max(1, resolvedStrokeWidth * 0.75);
|
|
4310
|
+
}
|
|
4311
|
+
let strokeDasharray;
|
|
4312
|
+
if (styleOverride?.lineStyle === "dashed") strokeDasharray = "6 4";
|
|
4313
|
+
else if (styleOverride?.lineStyle === "dotted") strokeDasharray = "2 3";
|
|
4314
|
+
else if (dashField) {
|
|
4315
|
+
const dashValue = rows[0]?.[dashField];
|
|
4316
|
+
if (dashValue != null) {
|
|
4317
|
+
const idx = dashIndexByValue.get(String(dashValue)) ?? 0;
|
|
4318
|
+
const pattern = STROKE_DASH_PATTERNS[idx % STROKE_DASH_PATTERNS.length];
|
|
4319
|
+
if (pattern) strokeDasharray = pattern;
|
|
4320
|
+
}
|
|
4321
|
+
}
|
|
4322
|
+
const keyField = encoding.key && "field" in encoding.key ? encoding.key.field : void 0;
|
|
4323
|
+
const rawPointKeys = pointsWithData.map(
|
|
4324
|
+
(p) => keyField ? serializeKeyValue(p.row[keyField]) : serializeKeyValue(p.row[xChannel.field])
|
|
4325
|
+
);
|
|
4326
|
+
const lineMark = {
|
|
4327
|
+
type: "line",
|
|
4328
|
+
key: seriesStyleKey ?? "series",
|
|
4329
|
+
pointKeys: dedupeKeys(rawPointKeys),
|
|
4330
|
+
interpolate: spec.markDef.interpolate ?? "monotone",
|
|
4331
|
+
points: allPoints,
|
|
4332
|
+
path: combinedPath,
|
|
4333
|
+
stroke: strokeColor,
|
|
4334
|
+
strokeWidth: resolvedStrokeWidth,
|
|
4335
|
+
strokeDasharray,
|
|
4336
|
+
opacity: styleOverride?.opacity,
|
|
4337
|
+
seriesKey: seriesStyleKey,
|
|
4338
|
+
data: pointsWithData.map((p) => p.row),
|
|
4339
|
+
dataPoints: pointsWithData.map((p) => ({ x: p.x, y: p.y, datum: p.row })),
|
|
4340
|
+
aria
|
|
4341
|
+
};
|
|
4342
|
+
const bucket = isMuted ? mutedMarks : highlightedMarks;
|
|
4343
|
+
bucket.push(lineMark);
|
|
4344
|
+
const markPoint = spec.markDef.point;
|
|
4345
|
+
const isSingleEndpoint = markPoint === "last" || markPoint === "first";
|
|
4346
|
+
const showPoints = markPoint === true || markPoint === "transparent" || markPoint === "endpoints" || isSingleEndpoint || isSequentialColor;
|
|
4347
|
+
if (showPoints) {
|
|
4348
|
+
const isTransparent = markPoint === "transparent";
|
|
4349
|
+
const isEndpoints = markPoint === "endpoints";
|
|
4350
|
+
const seriesShowPoints = styleOverride?.showPoints !== false;
|
|
4351
|
+
const lastIdx = pointsWithData.length - 1;
|
|
4352
|
+
for (let i = 0; i < pointsWithData.length; i++) {
|
|
4353
|
+
const p = pointsWithData[i];
|
|
4354
|
+
const isEndpoint = i === 0 || i === lastIdx;
|
|
4355
|
+
const isLast = i === lastIdx;
|
|
4356
|
+
const isFirst = i === 0;
|
|
4357
|
+
if (isSingleEndpoint) {
|
|
4358
|
+
if (markPoint === "last" && !isLast) continue;
|
|
4359
|
+
if (markPoint === "first" && !isFirst) continue;
|
|
4360
|
+
}
|
|
4361
|
+
const visible = seriesShowPoints && !isTransparent && (!isEndpoints || isEndpoint);
|
|
4362
|
+
let pointColor = color;
|
|
4363
|
+
if (isSequentialColor) {
|
|
4364
|
+
const val = Number(p.row[sequentialColorField]);
|
|
4365
|
+
pointColor = Number.isFinite(val) ? getSequentialColor(scales, val) : color;
|
|
4366
|
+
}
|
|
4367
|
+
const hollow = isEndpoints && visible;
|
|
4368
|
+
const pointColorStr = getRepresentativeColor5(pointColor);
|
|
4369
|
+
const pointKey = keyField ? serializeKeyValue(p.row[keyField]) : seriesStyleKey ? `${seriesStyleKey}|${serializeKeyValue(p.row[xChannel.field])}` : serializeKeyValue(p.row[xChannel.field]);
|
|
4370
|
+
if (isSingleEndpoint) {
|
|
4371
|
+
bucket.push({
|
|
4372
|
+
type: "point",
|
|
4373
|
+
key: pointKey,
|
|
4374
|
+
cx: p.x,
|
|
4375
|
+
cy: p.y,
|
|
4376
|
+
r: ENDPOINT_DOT_RADIUS,
|
|
4377
|
+
fill: strokeColor,
|
|
4378
|
+
stroke: "transparent",
|
|
4379
|
+
strokeWidth: 0,
|
|
4380
|
+
fillOpacity: 1,
|
|
4381
|
+
data: p.row,
|
|
4382
|
+
aria: { decorative: true }
|
|
4383
|
+
});
|
|
4384
|
+
continue;
|
|
4385
|
+
}
|
|
4386
|
+
const pointMark = {
|
|
4387
|
+
type: "point",
|
|
4388
|
+
key: pointKey,
|
|
4389
|
+
cx: p.x,
|
|
4390
|
+
cy: p.y,
|
|
4391
|
+
r: visible ? DEFAULT_POINT_RADIUS : 0,
|
|
4392
|
+
fill: hollow ? "transparent" : pointColorStr,
|
|
4393
|
+
stroke: hollow ? pointColorStr : visible ? pointSeparator : "transparent",
|
|
4394
|
+
strokeWidth: visible ? 1.5 : 0,
|
|
4395
|
+
fillOpacity: isTransparent ? 0 : 1,
|
|
4396
|
+
data: p.row,
|
|
4397
|
+
aria: {
|
|
4398
|
+
label: `Data point: ${xChannel.field}=${String(p.row[xChannel.field])}, ${yChannel.field}=${String(p.row[yChannel.field])}`
|
|
4399
|
+
}
|
|
4400
|
+
};
|
|
4401
|
+
bucket.push(pointMark);
|
|
4402
|
+
}
|
|
4403
|
+
}
|
|
4404
|
+
}
|
|
4405
|
+
const allMarks = [...mutedMarks, ...highlightedMarks];
|
|
4406
|
+
const lineMarks = allMarks.filter((m) => m.type === "line");
|
|
4407
|
+
const lineKeys = dedupeKeys(lineMarks.map((m) => m.key));
|
|
4408
|
+
lineMarks.forEach((m, i) => {
|
|
4409
|
+
m.key = lineKeys[i];
|
|
4410
|
+
});
|
|
4411
|
+
const pointMarks = allMarks.filter((m) => m.type === "point");
|
|
4412
|
+
if (pointMarks.length > 0) {
|
|
4413
|
+
const pointKeys = dedupeKeys(pointMarks.map((m) => m.key ?? ""));
|
|
4414
|
+
pointMarks.forEach((m, i) => {
|
|
4415
|
+
m.key = pointKeys[i];
|
|
4416
|
+
});
|
|
4417
|
+
}
|
|
4418
|
+
return allMarks;
|
|
4419
|
+
}
|
|
4420
|
+
function _getMidValue(rows, field) {
|
|
4421
|
+
const values = rows.map((r) => Number(r[field])).filter(Number.isFinite);
|
|
4422
|
+
if (values.length === 0) return 0;
|
|
4423
|
+
const sorted = values.sort((a, b) => a - b);
|
|
4424
|
+
return sorted[Math.floor(sorted.length / 2)];
|
|
4425
|
+
}
|
|
4426
|
+
|
|
4046
4427
|
// src/charts/line/area.ts
|
|
4047
4428
|
var DEFAULT_FILL_OPACITY = 0.15;
|
|
4048
4429
|
function isStacked(stackProp) {
|
|
4049
4430
|
return stackProp !== null && stackProp !== false;
|
|
4050
4431
|
}
|
|
4051
4432
|
var SOLO_GRADIENT_STOPS = [
|
|
4052
|
-
{ offset: 0, opacity: 0.
|
|
4053
|
-
{ offset: 0.6, opacity: 0.1 },
|
|
4433
|
+
{ offset: 0, opacity: 0.2 },
|
|
4054
4434
|
{ offset: 1, opacity: 0 }
|
|
4055
4435
|
];
|
|
4056
4436
|
var OVERLAP_GRADIENT_STOPS = [
|
|
4057
|
-
{ offset: 0, opacity: 0.
|
|
4058
|
-
{ offset: 0.7, opacity: 0.04 },
|
|
4059
|
-
{ offset: 1, opacity: 0 }
|
|
4060
|
-
];
|
|
4061
|
-
var STACKED_GRADIENT_STOPS = [
|
|
4062
|
-
{ offset: 0, opacity: 0.65 },
|
|
4063
|
-
{ offset: 1, opacity: 0.35 }
|
|
4064
|
-
];
|
|
4065
|
-
var STACKED_GRADIENT_STOPS_LIGHT = [
|
|
4066
|
-
{ offset: 0, opacity: 0.65 },
|
|
4437
|
+
{ offset: 0, opacity: 0.14 },
|
|
4067
4438
|
{ offset: 1, opacity: 0 }
|
|
4068
4439
|
];
|
|
4440
|
+
var STACKED_FILL_OPACITY = 0.92;
|
|
4069
4441
|
function buildGradientFill(colorStr, stops) {
|
|
4070
4442
|
return {
|
|
4071
4443
|
gradient: "linear",
|
|
@@ -4076,7 +4448,7 @@ function buildGradientFill(colorStr, stops) {
|
|
|
4076
4448
|
stops: stops.map((s) => ({ offset: s.offset, color: colorStr, opacity: s.opacity }))
|
|
4077
4449
|
};
|
|
4078
4450
|
}
|
|
4079
|
-
function computeSingleArea(spec, scales, _chartArea) {
|
|
4451
|
+
function computeSingleArea(spec, scales, _chartArea, theme) {
|
|
4080
4452
|
const encoding = spec.encoding;
|
|
4081
4453
|
const xChannel = encoding.x;
|
|
4082
4454
|
const yChannel = encoding.y;
|
|
@@ -4123,7 +4495,7 @@ function computeSingleArea(spec, scales, _chartArea) {
|
|
|
4123
4495
|
const curve = resolveCurve(spec.markDef.interpolate);
|
|
4124
4496
|
const areaGenerator = area2().x((d) => d.x).y0((d) => d.yBottom).y1((d) => d.yTop).curve(curve);
|
|
4125
4497
|
const pathStr = areaGenerator(validPoints) ?? "";
|
|
4126
|
-
const topLineGenerator =
|
|
4498
|
+
const topLineGenerator = line3().x((d) => d.x).y((d) => d.yTop).curve(curve);
|
|
4127
4499
|
const topPathStr = topLineGenerator(validPoints) ?? "";
|
|
4128
4500
|
const topPoints = validPoints.map((p) => ({ x: p.x, y: p.yTop }));
|
|
4129
4501
|
const bottomPoints = validPoints.map((p) => ({ x: p.x, y: p.yBottom }));
|
|
@@ -4136,7 +4508,7 @@ function computeSingleArea(spec, scales, _chartArea) {
|
|
|
4136
4508
|
fillValue = markFill;
|
|
4137
4509
|
fillOpacity = isGradientDef3(markFill) ? 1 : spec.markDef.opacity ?? (y2Channel ? 0.25 : DEFAULT_FILL_OPACITY);
|
|
4138
4510
|
} else {
|
|
4139
|
-
const colorStr =
|
|
4511
|
+
const colorStr = getRepresentativeColor6(color);
|
|
4140
4512
|
fillValue = buildGradientFill(colorStr, defaultGradientStops);
|
|
4141
4513
|
fillOpacity = 1;
|
|
4142
4514
|
}
|
|
@@ -4155,8 +4527,12 @@ function computeSingleArea(spec, scales, _chartArea) {
|
|
|
4155
4527
|
topPath: topPathStr,
|
|
4156
4528
|
fill: fillValue,
|
|
4157
4529
|
fillOpacity,
|
|
4158
|
-
|
|
4159
|
-
|
|
4530
|
+
// The top edge is a foreground stroke, so it goes through the same
|
|
4531
|
+
// light-canvas adaptation as a line mark's stroke. Otherwise the area's
|
|
4532
|
+
// own top path and the LineMark the renderer draws over it (both from
|
|
4533
|
+
// this series color) come out as two different colors.
|
|
4534
|
+
stroke: spec.markDef.stroke ?? resolveSeriesStroke(spec, isGradientDef3(fillValue) ? color : fillValue, theme),
|
|
4535
|
+
strokeWidth: spec.markDef.strokeWidth ?? LINE_STROKE_WIDTH,
|
|
4160
4536
|
seriesKey: seriesKey === "__default__" ? void 0 : seriesKey,
|
|
4161
4537
|
data: validPoints.map((p) => p.row),
|
|
4162
4538
|
dataPoints: validPoints.map((p) => ({ x: p.x, y: p.yTop, datum: p.row })),
|
|
@@ -4165,13 +4541,13 @@ function computeSingleArea(spec, scales, _chartArea) {
|
|
|
4165
4541
|
}
|
|
4166
4542
|
return marks;
|
|
4167
4543
|
}
|
|
4168
|
-
function computeStackedArea(spec, scales, chartArea,
|
|
4544
|
+
function computeStackedArea(spec, scales, chartArea, theme) {
|
|
4169
4545
|
const encoding = spec.encoding;
|
|
4170
4546
|
const xChannel = encoding.x;
|
|
4171
4547
|
const yChannel = encoding.y;
|
|
4172
4548
|
const colorField = encoding.color && "field" in encoding.color ? encoding.color.field : void 0;
|
|
4173
4549
|
if (!xChannel || !yChannel || !scales.x || !scales.y || !colorField) {
|
|
4174
|
-
return computeSingleArea(spec, scales, chartArea);
|
|
4550
|
+
return computeSingleArea(spec, scales, chartArea, theme);
|
|
4175
4551
|
}
|
|
4176
4552
|
const sortedData = xChannel.type === "nominal" || xChannel.type === "ordinal" ? spec.data : sortByField(spec.data, xChannel.field);
|
|
4177
4553
|
const seriesKeys = /* @__PURE__ */ new Set();
|
|
@@ -4228,7 +4604,7 @@ function computeStackedArea(spec, scales, chartArea, darkMode) {
|
|
|
4228
4604
|
const stackCurve = resolveCurve(spec.markDef.interpolate);
|
|
4229
4605
|
const areaGenerator = area2().x((p) => p.x).y0((p) => p.yBottom).y1((p) => p.yTop).curve(stackCurve);
|
|
4230
4606
|
const pathStr = areaGenerator(validPoints) ?? "";
|
|
4231
|
-
const topLineGenerator =
|
|
4607
|
+
const topLineGenerator = line3().x((p) => p.x).y((p) => p.yTop).curve(stackCurve);
|
|
4232
4608
|
const topPathStr = topLineGenerator(validPoints) ?? "";
|
|
4233
4609
|
const topPoints = validPoints.map((p) => ({ x: p.x, y: p.yTop }));
|
|
4234
4610
|
const bottomPoints = validPoints.map((p) => ({ x: p.x, y: p.yBottom }));
|
|
@@ -4239,265 +4615,67 @@ function computeStackedArea(spec, scales, chartArea, darkMode) {
|
|
|
4239
4615
|
let fillValue;
|
|
4240
4616
|
let fillOpacity;
|
|
4241
4617
|
if (markFill != null) {
|
|
4242
|
-
fillValue = markFill;
|
|
4243
|
-
fillOpacity = isGradientDef3(markFill) ? 1 : spec.markDef.opacity ?? 0.7;
|
|
4244
|
-
} else {
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
type: "area",
|
|
4253
|
-
key: seriesKey,
|
|
4254
|
-
pointKeys: dedupeKeys(rawPointKeys),
|
|
4255
|
-
interpolate: spec.markDef.interpolate ?? "monotone",
|
|
4256
|
-
topPoints,
|
|
4257
|
-
bottomPoints,
|
|
4258
|
-
path: pathStr,
|
|
4259
|
-
topPath: topPathStr,
|
|
4260
|
-
fill: fillValue,
|
|
4261
|
-
fillOpacity,
|
|
4262
|
-
stroke: getRepresentativeColor4(color),
|
|
4263
|
-
strokeWidth: 1,
|
|
4264
|
-
seriesKey,
|
|
4265
|
-
data: layer.map((d) => {
|
|
4266
|
-
const xStr = String(d.data.__x__);
|
|
4267
|
-
return rowsByXSeries.get(`${xStr}::${seriesKey}`) ?? d.data;
|
|
4268
|
-
}),
|
|
4269
|
-
dataPoints: validPoints.map((p, idx) => {
|
|
4270
|
-
const xStr = String(layer[idx]?.data.__x__);
|
|
4271
|
-
const datum = rowsByXSeries.get(`${xStr}::${seriesKey}`) ?? layer[idx]?.data ?? {};
|
|
4272
|
-
return { x: p.x, y: p.yTop, datum };
|
|
4273
|
-
}),
|
|
4274
|
-
aria
|
|
4275
|
-
});
|
|
4276
|
-
}
|
|
4277
|
-
return marks;
|
|
4278
|
-
}
|
|
4279
|
-
function computeAreaMarks(spec, scales, chartArea, darkMode) {
|
|
4280
|
-
const encoding = spec.encoding;
|
|
4281
|
-
const yChannel = encoding.y;
|
|
4282
|
-
let marks;
|
|
4283
|
-
if (yChannel && isStacked(yChannel.stack)) {
|
|
4284
|
-
marks = computeStackedArea(spec, scales, chartArea, darkMode);
|
|
4285
|
-
} else {
|
|
4286
|
-
marks = computeSingleArea(spec, scales, chartArea);
|
|
4287
|
-
}
|
|
4288
|
-
const areaKeys = dedupeKeys(marks.map((m) => m.key));
|
|
4289
|
-
marks.forEach((m, i) => {
|
|
4290
|
-
m.key = areaKeys[i];
|
|
4291
|
-
});
|
|
4292
|
-
const highlight = spec.highlight ?? [];
|
|
4293
|
-
if (highlight.length > 0) {
|
|
4294
|
-
const highlightSet = new Set(highlight);
|
|
4295
|
-
marks.sort((a, b) => {
|
|
4296
|
-
const aMuted = a.seriesKey != null && !highlightSet.has(a.seriesKey);
|
|
4297
|
-
const bMuted = b.seriesKey != null && !highlightSet.has(b.seriesKey);
|
|
4298
|
-
if (aMuted === bMuted) return 0;
|
|
4299
|
-
return aMuted ? -1 : 1;
|
|
4300
|
-
});
|
|
4301
|
-
}
|
|
4302
|
-
return marks;
|
|
4303
|
-
}
|
|
4304
|
-
|
|
4305
|
-
// src/charts/line/compute.ts
|
|
4306
|
-
import { getRepresentativeColor as getRepresentativeColor5 } from "@opendata-ai/openchart-core";
|
|
4307
|
-
import { line as line3 } from "d3-shape";
|
|
4308
|
-
var DEFAULT_STROKE_WIDTH = 1.5;
|
|
4309
|
-
var DEFAULT_POINT_RADIUS = 3;
|
|
4310
|
-
var STROKE_DASH_PATTERNS = ["", "6 4", "2 3", "8 4 2 4", "4 4", "1 3"];
|
|
4311
|
-
function computeLineMarks(spec, scales, _chartArea, _strategy) {
|
|
4312
|
-
const encoding = spec.encoding;
|
|
4313
|
-
const xChannel = encoding.x;
|
|
4314
|
-
const yChannel = encoding.y;
|
|
4315
|
-
if (!xChannel || !yChannel || !scales.x || !scales.y) {
|
|
4316
|
-
return [];
|
|
4317
|
-
}
|
|
4318
|
-
const colorEnc = encoding.color && "field" in encoding.color ? encoding.color : void 0;
|
|
4319
|
-
const isSequentialColor = colorEnc?.type === "quantitative";
|
|
4320
|
-
const colorField = isSequentialColor ? void 0 : colorEnc?.field;
|
|
4321
|
-
const sequentialColorField = isSequentialColor ? colorEnc.field : void 0;
|
|
4322
|
-
const dashEnc = encoding.strokeDash && "field" in encoding.strokeDash ? encoding.strokeDash : void 0;
|
|
4323
|
-
const dashField = dashEnc?.field;
|
|
4324
|
-
const dashIndexByValue = /* @__PURE__ */ new Map();
|
|
4325
|
-
if (dashField) {
|
|
4326
|
-
for (const row of spec.data) {
|
|
4327
|
-
const value = row[dashField];
|
|
4328
|
-
if (value == null) continue;
|
|
4329
|
-
const key = String(value);
|
|
4330
|
-
if (!dashIndexByValue.has(key)) dashIndexByValue.set(key, dashIndexByValue.size);
|
|
4331
|
-
}
|
|
4332
|
-
}
|
|
4333
|
-
const groups = groupByField(spec.data, colorField ?? dashField);
|
|
4334
|
-
const mutedMarks = [];
|
|
4335
|
-
const highlightedMarks = [];
|
|
4336
|
-
const highlight = spec.highlight ?? [];
|
|
4337
|
-
const highlightActive = highlight.length > 0;
|
|
4338
|
-
for (const [seriesKey, rows] of groups) {
|
|
4339
|
-
const color = isSequentialColor ? getSequentialColor(scales, _getMidValue(rows, sequentialColorField)) : getColor(scales, seriesKey);
|
|
4340
|
-
const strokeColor = spec.markDef.stroke ?? getRepresentativeColor5(color);
|
|
4341
|
-
const sortedRows = xChannel.type === "nominal" || xChannel.type === "ordinal" ? rows : sortByField(rows, xChannel.field);
|
|
4342
|
-
const pointsWithData = [];
|
|
4343
|
-
const segments = [];
|
|
4344
|
-
let currentSegment = [];
|
|
4345
|
-
for (const row of sortedRows) {
|
|
4346
|
-
const xVal = scaleValue(scales.x.scale, scales.x.type, row[xChannel.field]);
|
|
4347
|
-
const yVal = scaleValue(scales.y.scale, scales.y.type, row[yChannel.field]);
|
|
4348
|
-
if (xVal === null || yVal === null) {
|
|
4349
|
-
if (currentSegment.length > 0) {
|
|
4350
|
-
segments.push(currentSegment);
|
|
4351
|
-
currentSegment = [];
|
|
4352
|
-
}
|
|
4353
|
-
continue;
|
|
4354
|
-
}
|
|
4355
|
-
const point = { x: xVal, y: yVal };
|
|
4356
|
-
currentSegment.push(point);
|
|
4357
|
-
pointsWithData.push({ ...point, row });
|
|
4358
|
-
}
|
|
4359
|
-
if (currentSegment.length > 0) {
|
|
4360
|
-
segments.push(currentSegment);
|
|
4361
|
-
}
|
|
4362
|
-
const curve = resolveCurve(spec.markDef.interpolate);
|
|
4363
|
-
const lineGenerator = line3().x((d) => d.x).y((d) => d.y).curve(curve);
|
|
4364
|
-
const allPoints = [];
|
|
4365
|
-
const pathParts = [];
|
|
4366
|
-
for (const segment of segments) {
|
|
4367
|
-
if (segment.length === 0) continue;
|
|
4368
|
-
const pathStr = lineGenerator(segment);
|
|
4369
|
-
if (pathStr) {
|
|
4370
|
-
pathParts.push(pathStr);
|
|
4371
|
-
}
|
|
4372
|
-
allPoints.push(...segment);
|
|
4373
|
-
}
|
|
4374
|
-
if (allPoints.length === 0) continue;
|
|
4375
|
-
const ariaLabel = seriesKey === "__default__" ? `Line with ${allPoints.length} data points` : `${seriesKey}: line with ${allPoints.length} data points`;
|
|
4376
|
-
const aria = {
|
|
4377
|
-
label: ariaLabel
|
|
4378
|
-
};
|
|
4379
|
-
const combinedPath = pathParts.join(" ");
|
|
4380
|
-
const seriesStyleKey = seriesKey === "__default__" ? void 0 : seriesKey;
|
|
4381
|
-
const styleOverride = seriesStyleKey ? spec.seriesStyles?.[seriesStyleKey] : void 0;
|
|
4382
|
-
const isMuted = highlightActive && !highlight.includes(seriesKey);
|
|
4383
|
-
let resolvedStrokeWidth = styleOverride?.strokeWidth ?? spec.markDef.strokeWidth ?? DEFAULT_STROKE_WIDTH;
|
|
4384
|
-
if (isMuted) {
|
|
4385
|
-
resolvedStrokeWidth = Math.max(1, resolvedStrokeWidth * 0.75);
|
|
4386
|
-
}
|
|
4387
|
-
let strokeDasharray;
|
|
4388
|
-
if (styleOverride?.lineStyle === "dashed") strokeDasharray = "6 4";
|
|
4389
|
-
else if (styleOverride?.lineStyle === "dotted") strokeDasharray = "2 3";
|
|
4390
|
-
else if (dashField) {
|
|
4391
|
-
const dashValue = rows[0]?.[dashField];
|
|
4392
|
-
if (dashValue != null) {
|
|
4393
|
-
const idx = dashIndexByValue.get(String(dashValue)) ?? 0;
|
|
4394
|
-
const pattern = STROKE_DASH_PATTERNS[idx % STROKE_DASH_PATTERNS.length];
|
|
4395
|
-
if (pattern) strokeDasharray = pattern;
|
|
4396
|
-
}
|
|
4397
|
-
}
|
|
4398
|
-
const keyField = encoding.key && "field" in encoding.key ? encoding.key.field : void 0;
|
|
4399
|
-
const rawPointKeys = pointsWithData.map(
|
|
4400
|
-
(p) => keyField ? serializeKeyValue(p.row[keyField]) : serializeKeyValue(p.row[xChannel.field])
|
|
4401
|
-
);
|
|
4402
|
-
const lineMark = {
|
|
4403
|
-
type: "line",
|
|
4404
|
-
key: seriesStyleKey ?? "series",
|
|
4618
|
+
fillValue = markFill;
|
|
4619
|
+
fillOpacity = isGradientDef3(markFill) ? 1 : spec.markDef.opacity ?? 0.7;
|
|
4620
|
+
} else {
|
|
4621
|
+
fillValue = getRepresentativeColor6(color);
|
|
4622
|
+
fillOpacity = STACKED_FILL_OPACITY;
|
|
4623
|
+
}
|
|
4624
|
+
const rawPointKeys = validPoints.map((_p, idx) => serializeKeyValue(layer[idx]?.data.__x__));
|
|
4625
|
+
marks.push({
|
|
4626
|
+
type: "area",
|
|
4627
|
+
key: seriesKey,
|
|
4405
4628
|
pointKeys: dedupeKeys(rawPointKeys),
|
|
4406
4629
|
interpolate: spec.markDef.interpolate ?? "monotone",
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4630
|
+
topPoints,
|
|
4631
|
+
bottomPoints,
|
|
4632
|
+
path: pathStr,
|
|
4633
|
+
topPath: topPathStr,
|
|
4634
|
+
fill: fillValue,
|
|
4635
|
+
fillOpacity,
|
|
4636
|
+
stroke: resolveSeriesStroke(spec, color, theme),
|
|
4637
|
+
strokeWidth: 1,
|
|
4638
|
+
seriesKey,
|
|
4639
|
+
stacked: true,
|
|
4640
|
+
stackNormalized: stackProp === "normalize",
|
|
4641
|
+
data: layer.map((d) => {
|
|
4642
|
+
const xStr = String(d.data.__x__);
|
|
4643
|
+
return rowsByXSeries.get(`${xStr}::${seriesKey}`) ?? d.data;
|
|
4644
|
+
}),
|
|
4645
|
+
dataPoints: validPoints.map((p, idx) => {
|
|
4646
|
+
const xStr = String(layer[idx]?.data.__x__);
|
|
4647
|
+
const datum = rowsByXSeries.get(`${xStr}::${seriesKey}`) ?? layer[idx]?.data ?? {};
|
|
4648
|
+
return { x: p.x, y: p.yTop, datum };
|
|
4649
|
+
}),
|
|
4416
4650
|
aria
|
|
4417
|
-
};
|
|
4418
|
-
const bucket = isMuted ? mutedMarks : highlightedMarks;
|
|
4419
|
-
bucket.push(lineMark);
|
|
4420
|
-
const markPoint = spec.markDef.point;
|
|
4421
|
-
const isSingleEndpoint = markPoint === "last" || markPoint === "first";
|
|
4422
|
-
const showPoints = markPoint === true || markPoint === "transparent" || markPoint === "endpoints" || isSingleEndpoint || isSequentialColor;
|
|
4423
|
-
if (showPoints) {
|
|
4424
|
-
const isTransparent = markPoint === "transparent";
|
|
4425
|
-
const isEndpoints = markPoint === "endpoints";
|
|
4426
|
-
const seriesShowPoints = styleOverride?.showPoints !== false;
|
|
4427
|
-
const lastIdx = pointsWithData.length - 1;
|
|
4428
|
-
for (let i = 0; i < pointsWithData.length; i++) {
|
|
4429
|
-
const p = pointsWithData[i];
|
|
4430
|
-
const isEndpoint = i === 0 || i === lastIdx;
|
|
4431
|
-
const isLast = i === lastIdx;
|
|
4432
|
-
const isFirst = i === 0;
|
|
4433
|
-
if (isSingleEndpoint) {
|
|
4434
|
-
if (markPoint === "last" && !isLast) continue;
|
|
4435
|
-
if (markPoint === "first" && !isFirst) continue;
|
|
4436
|
-
}
|
|
4437
|
-
const visible = seriesShowPoints && !isTransparent && (!isEndpoints || isEndpoint);
|
|
4438
|
-
let pointColor = color;
|
|
4439
|
-
if (isSequentialColor) {
|
|
4440
|
-
const val = Number(p.row[sequentialColorField]);
|
|
4441
|
-
pointColor = Number.isFinite(val) ? getSequentialColor(scales, val) : color;
|
|
4442
|
-
}
|
|
4443
|
-
const hollow = isEndpoints && visible;
|
|
4444
|
-
const pointColorStr = getRepresentativeColor5(pointColor);
|
|
4445
|
-
const pointKey = keyField ? serializeKeyValue(p.row[keyField]) : seriesStyleKey ? `${seriesStyleKey}|${serializeKeyValue(p.row[xChannel.field])}` : serializeKeyValue(p.row[xChannel.field]);
|
|
4446
|
-
if (isSingleEndpoint) {
|
|
4447
|
-
bucket.push({
|
|
4448
|
-
type: "point",
|
|
4449
|
-
key: pointKey,
|
|
4450
|
-
cx: p.x,
|
|
4451
|
-
cy: p.y,
|
|
4452
|
-
r: 3.5,
|
|
4453
|
-
fill: strokeColor,
|
|
4454
|
-
stroke: "transparent",
|
|
4455
|
-
strokeWidth: 0,
|
|
4456
|
-
fillOpacity: 1,
|
|
4457
|
-
data: p.row,
|
|
4458
|
-
aria: { decorative: true }
|
|
4459
|
-
});
|
|
4460
|
-
continue;
|
|
4461
|
-
}
|
|
4462
|
-
const pointMark = {
|
|
4463
|
-
type: "point",
|
|
4464
|
-
key: pointKey,
|
|
4465
|
-
cx: p.x,
|
|
4466
|
-
cy: p.y,
|
|
4467
|
-
r: visible ? DEFAULT_POINT_RADIUS : 0,
|
|
4468
|
-
fill: hollow ? "transparent" : pointColorStr,
|
|
4469
|
-
stroke: hollow ? pointColorStr : visible ? "#ffffff" : "transparent",
|
|
4470
|
-
strokeWidth: visible ? 1.5 : 0,
|
|
4471
|
-
fillOpacity: isTransparent ? 0 : 1,
|
|
4472
|
-
data: p.row,
|
|
4473
|
-
aria: {
|
|
4474
|
-
label: `Data point: ${xChannel.field}=${String(p.row[xChannel.field])}, ${yChannel.field}=${String(p.row[yChannel.field])}`
|
|
4475
|
-
}
|
|
4476
|
-
};
|
|
4477
|
-
bucket.push(pointMark);
|
|
4478
|
-
}
|
|
4479
|
-
}
|
|
4651
|
+
});
|
|
4480
4652
|
}
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4653
|
+
return marks;
|
|
4654
|
+
}
|
|
4655
|
+
function computeAreaMarks(spec, scales, chartArea, theme) {
|
|
4656
|
+
const encoding = spec.encoding;
|
|
4657
|
+
const yChannel = encoding.y;
|
|
4658
|
+
let marks;
|
|
4659
|
+
if (yChannel && isStacked(yChannel.stack)) {
|
|
4660
|
+
marks = computeStackedArea(spec, scales, chartArea, theme);
|
|
4661
|
+
} else {
|
|
4662
|
+
marks = computeSingleArea(spec, scales, chartArea, theme);
|
|
4663
|
+
}
|
|
4664
|
+
const areaKeys = dedupeKeys(marks.map((m) => m.key));
|
|
4665
|
+
marks.forEach((m, i) => {
|
|
4666
|
+
m.key = areaKeys[i];
|
|
4486
4667
|
});
|
|
4487
|
-
const
|
|
4488
|
-
if (
|
|
4489
|
-
const
|
|
4490
|
-
|
|
4491
|
-
|
|
4668
|
+
const highlight = spec.highlight ?? [];
|
|
4669
|
+
if (highlight.length > 0) {
|
|
4670
|
+
const highlightSet = new Set(highlight);
|
|
4671
|
+
marks.sort((a, b) => {
|
|
4672
|
+
const aMuted = a.seriesKey != null && !highlightSet.has(a.seriesKey);
|
|
4673
|
+
const bMuted = b.seriesKey != null && !highlightSet.has(b.seriesKey);
|
|
4674
|
+
if (aMuted === bMuted) return 0;
|
|
4675
|
+
return aMuted ? -1 : 1;
|
|
4492
4676
|
});
|
|
4493
4677
|
}
|
|
4494
|
-
return
|
|
4495
|
-
}
|
|
4496
|
-
function _getMidValue(rows, field) {
|
|
4497
|
-
const values = rows.map((r) => Number(r[field])).filter(Number.isFinite);
|
|
4498
|
-
if (values.length === 0) return 0;
|
|
4499
|
-
const sorted = values.sort((a, b) => a - b);
|
|
4500
|
-
return sorted[Math.floor(sorted.length / 2)];
|
|
4678
|
+
return marks;
|
|
4501
4679
|
}
|
|
4502
4680
|
|
|
4503
4681
|
// src/charts/line/labels.ts
|
|
@@ -4586,8 +4764,8 @@ function computeLineLabels(marks, strategy, density = "auto", labelOffsets, spec
|
|
|
4586
4764
|
|
|
4587
4765
|
// src/charts/line/index.ts
|
|
4588
4766
|
var AREA_POINT_RADIUS = 3;
|
|
4589
|
-
var lineRenderer = (spec, scales, chartArea, strategy,
|
|
4590
|
-
const marks = computeLineMarks(spec, scales, chartArea, strategy);
|
|
4767
|
+
var lineRenderer = (spec, scales, chartArea, strategy, theme) => {
|
|
4768
|
+
const marks = computeLineMarks(spec, scales, chartArea, strategy, theme);
|
|
4591
4769
|
const lineMarks = marks.filter((m) => m.type === "line");
|
|
4592
4770
|
const labelMap = computeLineLabels(
|
|
4593
4771
|
lineMarks,
|
|
@@ -4607,11 +4785,11 @@ var lineRenderer = (spec, scales, chartArea, strategy, _theme) => {
|
|
|
4607
4785
|
return marks;
|
|
4608
4786
|
};
|
|
4609
4787
|
var areaRenderer = (spec, scales, chartArea, strategy, theme) => {
|
|
4610
|
-
const areas = computeAreaMarks(spec, scales, chartArea, theme
|
|
4788
|
+
const areas = computeAreaMarks(spec, scales, chartArea, theme);
|
|
4611
4789
|
const encoding = spec.encoding;
|
|
4612
4790
|
const hasColor = !!(encoding.color && "field" in encoding.color);
|
|
4613
|
-
const lines = hasColor ? linesFromAreas(areas) : computeLineMarks(spec, scales, chartArea, strategy);
|
|
4614
|
-
const points = hasColor && spec.markDef.point ? pointsFromAreas(areas, spec.markDef.point) : [];
|
|
4791
|
+
const lines = hasColor ? linesFromAreas(areas) : computeLineMarks(spec, scales, chartArea, strategy, theme);
|
|
4792
|
+
const points = hasColor && spec.markDef.point ? pointsFromAreas(areas, spec.markDef.point, resolveKnockoutColor(theme)) : [];
|
|
4615
4793
|
return [...areas, ...lines, ...points];
|
|
4616
4794
|
};
|
|
4617
4795
|
function linesFromAreas(areas) {
|
|
@@ -4619,7 +4797,10 @@ function linesFromAreas(areas) {
|
|
|
4619
4797
|
type: "line",
|
|
4620
4798
|
points: a.topPoints,
|
|
4621
4799
|
path: a.topPath,
|
|
4622
|
-
stroke
|
|
4800
|
+
// The area already resolved its top-edge stroke (palette color, adapted
|
|
4801
|
+
// for a light canvas, or the author's markDef.stroke). Re-deriving it from
|
|
4802
|
+
// the fill here would paint an unadapted line over the adapted top path.
|
|
4803
|
+
stroke: a.stroke ?? getRepresentativeColor7(a.fill),
|
|
4623
4804
|
strokeWidth: a.strokeWidth ?? 1,
|
|
4624
4805
|
seriesKey: a.seriesKey,
|
|
4625
4806
|
data: a.data,
|
|
@@ -4627,12 +4808,12 @@ function linesFromAreas(areas) {
|
|
|
4627
4808
|
aria: { label: `${a.seriesKey ?? "Series"}: line with ${a.topPoints.length} data points` }
|
|
4628
4809
|
}));
|
|
4629
4810
|
}
|
|
4630
|
-
function pointsFromAreas(areas, pointMode) {
|
|
4811
|
+
function pointsFromAreas(areas, pointMode, separator) {
|
|
4631
4812
|
const isTransparent = pointMode === "transparent";
|
|
4632
4813
|
const isEndpoints = pointMode === "endpoints";
|
|
4633
4814
|
const points = [];
|
|
4634
4815
|
for (const a of areas) {
|
|
4635
|
-
const stroke =
|
|
4816
|
+
const stroke = a.stroke ?? getRepresentativeColor7(a.fill);
|
|
4636
4817
|
const lastIdx = a.topPoints.length - 1;
|
|
4637
4818
|
for (let i = 0; i < a.topPoints.length; i++) {
|
|
4638
4819
|
const pt = a.topPoints[i];
|
|
@@ -4645,7 +4826,7 @@ function pointsFromAreas(areas, pointMode) {
|
|
|
4645
4826
|
cy: pt.y,
|
|
4646
4827
|
r: visible ? AREA_POINT_RADIUS : 0,
|
|
4647
4828
|
fill: hollow ? "transparent" : stroke,
|
|
4648
|
-
stroke: hollow ? stroke : visible ?
|
|
4829
|
+
stroke: hollow ? stroke : visible ? separator : "transparent",
|
|
4649
4830
|
strokeWidth: visible ? 1.5 : 0,
|
|
4650
4831
|
fillOpacity: isTransparent ? 0 : 1,
|
|
4651
4832
|
data: a.data[i] ?? {},
|
|
@@ -4695,6 +4876,7 @@ function packSeats(seats, rows) {
|
|
|
4695
4876
|
slots.sort((a, b) => b.angle - a.angle || b.radius - a.radius);
|
|
4696
4877
|
return slots;
|
|
4697
4878
|
}
|
|
4879
|
+
var SEAT_STROKE_WIDTH = 0.75;
|
|
4698
4880
|
function resolveMajority(totalSeats, majorityLine) {
|
|
4699
4881
|
if (majorityLine === false) return null;
|
|
4700
4882
|
const defaultSeats = Math.floor(totalSeats / 2) + 1;
|
|
@@ -4737,6 +4919,7 @@ function computeParliamentMarks(spec, scales, chartArea, _strategy, theme) {
|
|
|
4737
4919
|
const seatRadius = typeof seatRadiusOpt === "number" && seatRadiusOpt > 0 ? seatRadiusOpt : Math.max(1.5, autoSeatRadius);
|
|
4738
4920
|
const centerX = chartArea.x + chartArea.width / 2;
|
|
4739
4921
|
const centerY = chartArea.y + chartArea.height - seatRadius;
|
|
4922
|
+
const knockout = stackSeamStroke(theme);
|
|
4740
4923
|
const marks = [];
|
|
4741
4924
|
let slotIndex = 0;
|
|
4742
4925
|
let animationIndex = 0;
|
|
@@ -4760,8 +4943,10 @@ function computeParliamentMarks(spec, scales, chartArea, _strategy, theme) {
|
|
|
4760
4943
|
cy,
|
|
4761
4944
|
r: seatRadius,
|
|
4762
4945
|
fill,
|
|
4763
|
-
|
|
4764
|
-
|
|
4946
|
+
// Knockout ring: seats are packed tight enough that same-party
|
|
4947
|
+
// neighbours merge into a blob without a hairline of canvas between.
|
|
4948
|
+
stroke: knockout,
|
|
4949
|
+
strokeWidth: SEAT_STROKE_WIDTH,
|
|
4765
4950
|
data: row,
|
|
4766
4951
|
aria,
|
|
4767
4952
|
animationIndex
|
|
@@ -4793,7 +4978,7 @@ function computeParliamentMarks(spec, scales, chartArea, _strategy, theme) {
|
|
|
4793
4978
|
x: centerX,
|
|
4794
4979
|
y: Math.max(chartArea.y + theme.fonts.sizes.small, lineTop - 6),
|
|
4795
4980
|
text: majority.label,
|
|
4796
|
-
fill: theme.colors.
|
|
4981
|
+
fill: theme.colors.axis,
|
|
4797
4982
|
fontSize: theme.fonts.sizes.small,
|
|
4798
4983
|
fontWeight: theme.fonts.weights.semibold,
|
|
4799
4984
|
textAnchor: "middle",
|
|
@@ -4811,9 +4996,15 @@ var parliamentRenderer = (spec, scales, chartArea, strategy, theme) => {
|
|
|
4811
4996
|
};
|
|
4812
4997
|
|
|
4813
4998
|
// src/charts/pie/compute.ts
|
|
4814
|
-
import {
|
|
4999
|
+
import {
|
|
5000
|
+
CATEGORICAL_FILL_PALETTE,
|
|
5001
|
+
formatPercent as formatPercent2,
|
|
5002
|
+
isConditionalDef,
|
|
5003
|
+
isGradientDef as isGradientDef4
|
|
5004
|
+
} from "@opendata-ai/openchart-core";
|
|
4815
5005
|
import { arc as d3Arc, pie as d3Pie } from "d3-shape";
|
|
4816
5006
|
var SMALL_SLICE_THRESHOLD = 0.03;
|
|
5007
|
+
var SLICE_STROKE_WIDTH = 1.5;
|
|
4817
5008
|
var PIE_PAD_ANGLE = 0.01;
|
|
4818
5009
|
function buildArcPath(geom) {
|
|
4819
5010
|
return d3Arc()({
|
|
@@ -4824,18 +5015,7 @@ function buildArcPath(geom) {
|
|
|
4824
5015
|
padAngle: PIE_PAD_ANGLE
|
|
4825
5016
|
}) ?? "";
|
|
4826
5017
|
}
|
|
4827
|
-
var DEFAULT_PALETTE =
|
|
4828
|
-
"#1b7fa3",
|
|
4829
|
-
"#c44e52",
|
|
4830
|
-
"#6a9f58",
|
|
4831
|
-
"#d47215",
|
|
4832
|
-
"#507e79",
|
|
4833
|
-
"#9a6a8d",
|
|
4834
|
-
"#c4636b",
|
|
4835
|
-
"#9c755f",
|
|
4836
|
-
"#a88f22",
|
|
4837
|
-
"#858078"
|
|
4838
|
-
];
|
|
5018
|
+
var DEFAULT_PALETTE = CATEGORICAL_FILL_PALETTE;
|
|
4839
5019
|
function groupSmallSlices(slices, threshold) {
|
|
4840
5020
|
const total = slices.reduce((sum, s) => sum + s.value, 0);
|
|
4841
5021
|
if (total === 0) return slices;
|
|
@@ -4879,7 +5059,9 @@ function computeSweepBounds(startAngle, endAngle) {
|
|
|
4879
5059
|
maxY: Math.max(...ys)
|
|
4880
5060
|
};
|
|
4881
5061
|
}
|
|
4882
|
-
function computePieMarks(spec, scales, chartArea, _strategy, isDonut = false) {
|
|
5062
|
+
function computePieMarks(spec, scales, chartArea, _strategy, isDonut = false, theme) {
|
|
5063
|
+
const palette = theme?.colors.categoricalFill ?? DEFAULT_PALETTE;
|
|
5064
|
+
const sliceStroke = resolveKnockoutColor(theme);
|
|
4883
5065
|
const encoding = spec.encoding;
|
|
4884
5066
|
const startAngle = spec.markDef.startAngle ?? 0;
|
|
4885
5067
|
const endAngle = spec.markDef.endAngle ?? Math.PI * 2;
|
|
@@ -4950,13 +5132,13 @@ function computePieMarks(spec, scales, chartArea, _strategy, isDonut = false) {
|
|
|
4950
5132
|
const colorScale = scales.color.scale;
|
|
4951
5133
|
color = colorScale(slice.label);
|
|
4952
5134
|
} else {
|
|
4953
|
-
color =
|
|
5135
|
+
color = palette[i % palette.length];
|
|
4954
5136
|
}
|
|
4955
5137
|
} else if (scales.color && categoryField) {
|
|
4956
5138
|
const colorScale = scales.color.scale;
|
|
4957
5139
|
color = colorScale(slice.label);
|
|
4958
5140
|
} else {
|
|
4959
|
-
color =
|
|
5141
|
+
color = palette[i % palette.length];
|
|
4960
5142
|
}
|
|
4961
5143
|
const path = arcGenerator(arcDatum) ?? "";
|
|
4962
5144
|
const centroidResult = arcGenerator.centroid(arcDatum);
|
|
@@ -4977,8 +5159,9 @@ function computePieMarks(spec, scales, chartArea, _strategy, isDonut = false) {
|
|
|
4977
5159
|
startAngle: arcDatum.startAngle,
|
|
4978
5160
|
endAngle: arcDatum.endAngle,
|
|
4979
5161
|
fill: color,
|
|
4980
|
-
stroke:
|
|
4981
|
-
strokeWidth:
|
|
5162
|
+
stroke: sliceStroke,
|
|
5163
|
+
strokeWidth: SLICE_STROKE_WIDTH,
|
|
5164
|
+
seriesKey: slice.label,
|
|
4982
5165
|
data: slice.originalRow,
|
|
4983
5166
|
aria
|
|
4984
5167
|
});
|
|
@@ -4996,10 +5179,12 @@ function computePieMarks(spec, scales, chartArea, _strategy, isDonut = false) {
|
|
|
4996
5179
|
|
|
4997
5180
|
// src/charts/pie/labels.ts
|
|
4998
5181
|
import { estimateTextWidth as estimateTextWidth7, resolveCollisions as resolveCollisions5 } from "@opendata-ai/openchart-core";
|
|
4999
|
-
var LABEL_FONT_SIZE5 =
|
|
5182
|
+
var LABEL_FONT_SIZE5 = 11;
|
|
5000
5183
|
var LABEL_FONT_WEIGHT5 = 500;
|
|
5001
5184
|
var LEADER_LINE_OFFSET = 12;
|
|
5002
|
-
function computePieLabels(marks, _chartArea, density = "auto", _textFill = "#333333") {
|
|
5185
|
+
function computePieLabels(marks, _chartArea, density = "auto", _textFill = "#333333", options = {}) {
|
|
5186
|
+
const fontFamily = options.fontFamily ?? "system-ui, -apple-system, sans-serif";
|
|
5187
|
+
const formatValue3 = options.formatValue;
|
|
5003
5188
|
if (marks.length === 0) return [];
|
|
5004
5189
|
const centerX = marks[0].center.x;
|
|
5005
5190
|
const centerY = marks[0].center.y;
|
|
@@ -5012,8 +5197,15 @@ function computePieLabels(marks, _chartArea, density = "auto", _textFill = "#333
|
|
|
5012
5197
|
const ariaLabel = mark.aria.label;
|
|
5013
5198
|
if (!ariaLabel) continue;
|
|
5014
5199
|
const firstColon = ariaLabel.indexOf(":");
|
|
5015
|
-
const
|
|
5016
|
-
if (!
|
|
5200
|
+
const sliceName = firstColon >= 0 ? ariaLabel.slice(0, firstColon).trim() : "";
|
|
5201
|
+
if (!sliceName) continue;
|
|
5202
|
+
const openParen = ariaLabel.lastIndexOf("(");
|
|
5203
|
+
const closeParen = ariaLabel.lastIndexOf(")");
|
|
5204
|
+
const percentText = openParen > firstColon && closeParen > openParen ? ariaLabel.slice(openParen + 1, closeParen).trim() : "";
|
|
5205
|
+
const rawValueText = openParen > firstColon ? ariaLabel.slice(firstColon + 1, openParen).trim() : "";
|
|
5206
|
+
const rawValue = Number(rawValueText);
|
|
5207
|
+
const valueText = formatValue3 && Number.isFinite(rawValue) ? formatValue3(rawValue) : percentText;
|
|
5208
|
+
const labelText = valueText ? `${sliceName} ${valueText}` : sliceName;
|
|
5017
5209
|
const textWidth = estimateTextWidth7(labelText, LABEL_FONT_SIZE5, LABEL_FONT_WEIGHT5);
|
|
5018
5210
|
const textHeight = LABEL_FONT_SIZE5 * 1.2;
|
|
5019
5211
|
const midAngle = (mark.startAngle + mark.endAngle) / 2;
|
|
@@ -5034,7 +5226,7 @@ function computePieLabels(marks, _chartArea, density = "auto", _textFill = "#333
|
|
|
5034
5226
|
// instead of zipping positionally.
|
|
5035
5227
|
index: marks.indexOf(mark),
|
|
5036
5228
|
style: {
|
|
5037
|
-
fontFamily
|
|
5229
|
+
fontFamily,
|
|
5038
5230
|
fontSize: LABEL_FONT_SIZE5,
|
|
5039
5231
|
fontWeight: LABEL_FONT_WEIGHT5,
|
|
5040
5232
|
fill: _textFill,
|
|
@@ -5078,28 +5270,96 @@ function computePieLabels(marks, _chartArea, density = "auto", _textFill = "#333
|
|
|
5078
5270
|
}
|
|
5079
5271
|
|
|
5080
5272
|
// src/charts/pie/index.ts
|
|
5273
|
+
function pieLabelOptions(spec, theme) {
|
|
5274
|
+
const valueChannel = spec.encoding.y ?? spec.encoding.x;
|
|
5275
|
+
const valueField = valueChannel && "field" in valueChannel ? valueChannel.field : void 0;
|
|
5276
|
+
if (!spec.labels.format) return { fontFamily: theme.fonts.family };
|
|
5277
|
+
const formatter = resolveFieldFormatter({
|
|
5278
|
+
surfaceFormat: spec.labels.format,
|
|
5279
|
+
values: valueField ? spec.data.map((r) => r[valueField]) : []
|
|
5280
|
+
});
|
|
5281
|
+
return { fontFamily: theme.fonts.family, formatValue: formatter };
|
|
5282
|
+
}
|
|
5283
|
+
function centerLabelMarks(spec, marks, theme) {
|
|
5284
|
+
const config = spec.markDef.centerLabel;
|
|
5285
|
+
if (!config || marks.length === 0) return [];
|
|
5286
|
+
const text = typeof config === "string" ? config : config.text;
|
|
5287
|
+
if (!text) return [];
|
|
5288
|
+
const subtitle = typeof config === "string" ? void 0 : config.subtitle;
|
|
5289
|
+
const { center } = marks[0];
|
|
5290
|
+
const valueSize = theme.fonts.sizes.metricValue;
|
|
5291
|
+
const captionSize = theme.fonts.sizes.small;
|
|
5292
|
+
const valueY = subtitle ? center.y - captionSize * 0.6 : center.y;
|
|
5293
|
+
const out = [
|
|
5294
|
+
{
|
|
5295
|
+
type: "textMark",
|
|
5296
|
+
key: "oc-center-value",
|
|
5297
|
+
x: center.x,
|
|
5298
|
+
y: valueY,
|
|
5299
|
+
text,
|
|
5300
|
+
fill: theme.colors.text,
|
|
5301
|
+
fontSize: valueSize,
|
|
5302
|
+
fontWeight: theme.fonts.weights.semibold,
|
|
5303
|
+
fontFamily: theme.fonts.family,
|
|
5304
|
+
textAnchor: "middle",
|
|
5305
|
+
dominantBaseline: "central",
|
|
5306
|
+
data: {},
|
|
5307
|
+
aria: { decorative: true }
|
|
5308
|
+
}
|
|
5309
|
+
];
|
|
5310
|
+
if (subtitle) {
|
|
5311
|
+
out.push({
|
|
5312
|
+
type: "textMark",
|
|
5313
|
+
key: "oc-center-subtitle",
|
|
5314
|
+
x: center.x,
|
|
5315
|
+
y: valueY + valueSize * 0.75 + captionSize * 0.4,
|
|
5316
|
+
text: subtitle,
|
|
5317
|
+
fill: theme.colors.axis,
|
|
5318
|
+
fontSize: captionSize,
|
|
5319
|
+
fontWeight: theme.fonts.weights.normal,
|
|
5320
|
+
fontFamily: theme.fonts.family,
|
|
5321
|
+
textAnchor: "middle",
|
|
5322
|
+
dominantBaseline: "central",
|
|
5323
|
+
data: {},
|
|
5324
|
+
aria: { decorative: true }
|
|
5325
|
+
});
|
|
5326
|
+
}
|
|
5327
|
+
return out;
|
|
5328
|
+
}
|
|
5081
5329
|
var pieRenderer = (spec, scales, chartArea, strategy, theme) => {
|
|
5082
|
-
const marks = computePieMarks(spec, scales, chartArea, strategy, false);
|
|
5083
|
-
const labels = computePieLabels(
|
|
5330
|
+
const marks = computePieMarks(spec, scales, chartArea, strategy, false, theme);
|
|
5331
|
+
const labels = computePieLabels(
|
|
5332
|
+
marks,
|
|
5333
|
+
chartArea,
|
|
5334
|
+
spec.labels.density,
|
|
5335
|
+
theme.colors.text,
|
|
5336
|
+
pieLabelOptions(spec, theme)
|
|
5337
|
+
);
|
|
5084
5338
|
for (const label of labels) {
|
|
5085
5339
|
if (label.index !== void 0 && marks[label.index]) marks[label.index].label = label;
|
|
5086
5340
|
}
|
|
5087
5341
|
return marks;
|
|
5088
5342
|
};
|
|
5089
5343
|
var donutRenderer = (spec, scales, chartArea, strategy, theme) => {
|
|
5090
|
-
const marks = computePieMarks(spec, scales, chartArea, strategy, true);
|
|
5091
|
-
const labels = computePieLabels(
|
|
5344
|
+
const marks = computePieMarks(spec, scales, chartArea, strategy, true, theme);
|
|
5345
|
+
const labels = computePieLabels(
|
|
5346
|
+
marks,
|
|
5347
|
+
chartArea,
|
|
5348
|
+
spec.labels.density,
|
|
5349
|
+
theme.colors.text,
|
|
5350
|
+
pieLabelOptions(spec, theme)
|
|
5351
|
+
);
|
|
5092
5352
|
for (const label of labels) {
|
|
5093
5353
|
if (label.index !== void 0 && marks[label.index]) marks[label.index].label = label;
|
|
5094
5354
|
}
|
|
5095
|
-
return marks;
|
|
5355
|
+
return [...marks, ...centerLabelMarks(spec, marks, theme)];
|
|
5096
5356
|
};
|
|
5097
5357
|
|
|
5098
5358
|
// src/charts/range/compute.ts
|
|
5099
|
-
import { getRepresentativeColor as
|
|
5100
|
-
var DOT_RADIUS2 =
|
|
5359
|
+
import { getRepresentativeColor as getRepresentativeColor8 } from "@opendata-ai/openchart-core";
|
|
5360
|
+
var DOT_RADIUS2 = 5.5;
|
|
5101
5361
|
var CONNECTOR_WIDTH = 2;
|
|
5102
|
-
var
|
|
5362
|
+
var CONNECTOR_OPACITY = 0.6;
|
|
5103
5363
|
var ARROW_STROKE_WIDTH = 2;
|
|
5104
5364
|
var ARROWHEAD_LENGTH2 = 8;
|
|
5105
5365
|
var ARROWHEAD_HALF_WIDTH2 = 4;
|
|
@@ -5124,6 +5384,7 @@ function computeRangeMarks(spec, scales, _chartArea, _strategy, theme) {
|
|
|
5124
5384
|
const bandScale = horizontal ? scales.y.scale : scales.x.scale;
|
|
5125
5385
|
const valueScale = horizontal ? scales.x.scale : scales.y.scale;
|
|
5126
5386
|
const bandwidth = bandScale.bandwidth();
|
|
5387
|
+
const knockout = stackSeamStroke(theme);
|
|
5127
5388
|
const style = spec.markDef.style ?? "dumbbell";
|
|
5128
5389
|
const colorEnc = encoding.color && "field" in encoding.color ? encoding.color : void 0;
|
|
5129
5390
|
const colorByDirection = spec.markDef.colorByDirection === true && !colorEnc;
|
|
@@ -5146,7 +5407,7 @@ function computeRangeMarks(spec, scales, _chartArea, _strategy, theme) {
|
|
|
5146
5407
|
const delta = endVal - startVal;
|
|
5147
5408
|
const catKey = catKeys[i];
|
|
5148
5409
|
const accent = colorEnc ? getColor(scales, String(row[colorEnc.field] ?? "")) : colorByDirection ? delta > 0 ? theme.colors.positive : delta < 0 ? theme.colors.negative : theme.colors.axis : getColor(scales, "__default__");
|
|
5149
|
-
const accentStroke =
|
|
5410
|
+
const accentStroke = getRepresentativeColor8(accent);
|
|
5150
5411
|
const spanAria = {
|
|
5151
5412
|
label: `${category}: ${startVal} to ${endVal}`
|
|
5152
5413
|
};
|
|
@@ -5223,15 +5484,16 @@ function computeRangeMarks(spec, scales, _chartArea, _strategy, theme) {
|
|
|
5223
5484
|
continue;
|
|
5224
5485
|
}
|
|
5225
5486
|
if (spanLength > 0) {
|
|
5226
|
-
const
|
|
5487
|
+
const colored = !!colorEnc || colorByDirection;
|
|
5227
5488
|
marks.push({
|
|
5228
5489
|
type: "rule",
|
|
5229
5490
|
x1,
|
|
5230
5491
|
y1,
|
|
5231
5492
|
x2,
|
|
5232
5493
|
y2,
|
|
5233
|
-
stroke:
|
|
5494
|
+
stroke: colored ? accentStroke : theme.colors.neutral[400],
|
|
5234
5495
|
strokeWidth: CONNECTOR_WIDTH,
|
|
5496
|
+
...colored ? {} : { strokeOpacity: CONNECTOR_OPACITY },
|
|
5235
5497
|
data: row,
|
|
5236
5498
|
aria: spanAria,
|
|
5237
5499
|
key: `${catKey}|range`
|
|
@@ -5243,8 +5505,8 @@ function computeRangeMarks(spec, scales, _chartArea, _strategy, theme) {
|
|
|
5243
5505
|
cx: x1,
|
|
5244
5506
|
cy: y1,
|
|
5245
5507
|
r: DOT_RADIUS2,
|
|
5246
|
-
fill: theme.colors.
|
|
5247
|
-
stroke:
|
|
5508
|
+
fill: theme.colors.neutral[400],
|
|
5509
|
+
stroke: knockout,
|
|
5248
5510
|
strokeWidth: 2,
|
|
5249
5511
|
data: row,
|
|
5250
5512
|
aria: { label: `${category}, start: ${startVal}` },
|
|
@@ -5256,7 +5518,7 @@ function computeRangeMarks(spec, scales, _chartArea, _strategy, theme) {
|
|
|
5256
5518
|
cy: y2,
|
|
5257
5519
|
r: DOT_RADIUS2,
|
|
5258
5520
|
fill: accent,
|
|
5259
|
-
stroke:
|
|
5521
|
+
stroke: knockout,
|
|
5260
5522
|
strokeWidth: 2,
|
|
5261
5523
|
data: row,
|
|
5262
5524
|
aria: { label: `${category}, end: ${endVal}` },
|
|
@@ -5270,7 +5532,7 @@ function computeRangeMarks(spec, scales, _chartArea, _strategy, theme) {
|
|
|
5270
5532
|
// src/charts/range/labels.ts
|
|
5271
5533
|
import {
|
|
5272
5534
|
estimateTextWidth as estimateTextWidth8,
|
|
5273
|
-
getRepresentativeColor as
|
|
5535
|
+
getRepresentativeColor as getRepresentativeColor9,
|
|
5274
5536
|
resolveCollisions as resolveCollisions6
|
|
5275
5537
|
} from "@opendata-ai/openchart-core";
|
|
5276
5538
|
var LABEL_FONT_SIZE6 = 11;
|
|
@@ -5306,7 +5568,7 @@ function buildCandidate(mark, text, horizontal, outerSide, index) {
|
|
|
5306
5568
|
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
5307
5569
|
fontSize: LABEL_FONT_SIZE6,
|
|
5308
5570
|
fontWeight: LABEL_FONT_WEIGHT6,
|
|
5309
|
-
fill:
|
|
5571
|
+
fill: getRepresentativeColor9(mark.fill),
|
|
5310
5572
|
lineHeight: 1.2,
|
|
5311
5573
|
textAnchor: "start",
|
|
5312
5574
|
dominantBaseline: "central",
|
|
@@ -5460,7 +5722,7 @@ function clearRenderers() {
|
|
|
5460
5722
|
}
|
|
5461
5723
|
|
|
5462
5724
|
// src/charts/rule/index.ts
|
|
5463
|
-
import { getRepresentativeColor as
|
|
5725
|
+
import { getRepresentativeColor as getRepresentativeColor10 } from "@opendata-ai/openchart-core";
|
|
5464
5726
|
function computeRuleMarks(spec, scales, chartArea) {
|
|
5465
5727
|
const encoding = spec.encoding;
|
|
5466
5728
|
const xChannel = encoding.x;
|
|
@@ -5503,7 +5765,7 @@ function computeRuleMarks(spec, scales, chartArea) {
|
|
|
5503
5765
|
const y2Val = scaleValue(scales.y.scale, scales.y.type, row[y2Channel.field]);
|
|
5504
5766
|
if (y2Val != null) y2 = y2Val;
|
|
5505
5767
|
}
|
|
5506
|
-
const color =
|
|
5768
|
+
const color = getRepresentativeColor10(
|
|
5507
5769
|
colorField ? getColor(scales, String(row[colorField] ?? "__default__")) : getColor(scales, "__default__")
|
|
5508
5770
|
);
|
|
5509
5771
|
const strokeDashEncoding = encoding.strokeDash && "field" in encoding.strokeDash ? encoding.strokeDash : void 0;
|
|
@@ -5538,8 +5800,9 @@ var ruleRenderer = (spec, scales, chartArea, _strategy, _theme) => {
|
|
|
5538
5800
|
};
|
|
5539
5801
|
|
|
5540
5802
|
// src/charts/scatter/compute.ts
|
|
5541
|
-
import { isOpaqueColor } from "@opendata-ai/openchart-core";
|
|
5542
5803
|
var DEFAULT_POINT_RADIUS2 = 5;
|
|
5804
|
+
var POINT_STROKE_WIDTH = 1.5;
|
|
5805
|
+
var DENSE_LAYER_POINTS = 200;
|
|
5543
5806
|
function resolvePosition2(value, channelType, scale) {
|
|
5544
5807
|
switch (channelType) {
|
|
5545
5808
|
case "nominal":
|
|
@@ -5584,8 +5847,8 @@ function computeScatterMarks(spec, scales, _chartArea, _strategy, theme) {
|
|
|
5584
5847
|
const sizeScale = resolvedSize?.scale;
|
|
5585
5848
|
const constFill = spec.markDef.fill;
|
|
5586
5849
|
const constSize = spec.markDef.size;
|
|
5587
|
-
const
|
|
5588
|
-
const
|
|
5850
|
+
const defaultStroke = resolveKnockoutColor(theme);
|
|
5851
|
+
const defaultFillOpacity = spec.data.length < DENSE_LAYER_POINTS ? 0.85 : 0.6;
|
|
5589
5852
|
const keyEnc = encoding.key && "field" in encoding.key ? encoding.key : void 0;
|
|
5590
5853
|
const keyField = keyEnc?.field;
|
|
5591
5854
|
const marks = [];
|
|
@@ -5627,8 +5890,9 @@ function computeScatterMarks(spec, scales, _chartArea, _strategy, theme) {
|
|
|
5627
5890
|
r: radius,
|
|
5628
5891
|
fill: color,
|
|
5629
5892
|
stroke: spec.markDef.stroke ?? defaultStroke,
|
|
5630
|
-
strokeWidth: spec.markDef.strokeWidth ??
|
|
5631
|
-
fillOpacity: spec.markDef.opacity ??
|
|
5893
|
+
strokeWidth: spec.markDef.strokeWidth ?? POINT_STROKE_WIDTH,
|
|
5894
|
+
fillOpacity: spec.markDef.opacity ?? defaultFillOpacity,
|
|
5895
|
+
...category ? { seriesKey: category } : {},
|
|
5632
5896
|
data: row,
|
|
5633
5897
|
aria
|
|
5634
5898
|
});
|
|
@@ -5649,7 +5913,7 @@ function computeScatterMarks(spec, scales, _chartArea, _strategy, theme) {
|
|
|
5649
5913
|
// src/charts/scatter/trendline.ts
|
|
5650
5914
|
var TRENDLINE_COLOR = "#666666";
|
|
5651
5915
|
var TRENDLINE_STROKE_WIDTH = 1.5;
|
|
5652
|
-
var TRENDLINE_DASH = "
|
|
5916
|
+
var TRENDLINE_DASH = "4 4";
|
|
5653
5917
|
function linearRegression(points) {
|
|
5654
5918
|
const n = points.length;
|
|
5655
5919
|
if (n < 2) return null;
|
|
@@ -5707,7 +5971,7 @@ var scatterRenderer = (spec, scales, chartArea, strategy, theme) => {
|
|
|
5707
5971
|
const trendline = spec.markDef?.trendline;
|
|
5708
5972
|
if (trendline !== false) {
|
|
5709
5973
|
const config = typeof trendline === "object" ? trendline : void 0;
|
|
5710
|
-
const stroke = config?.stroke ?? theme?.colors?.
|
|
5974
|
+
const stroke = config?.stroke ?? theme?.colors?.axis;
|
|
5711
5975
|
const trendLine = computeTrendLine(pointMarks, stroke, config?.strokeWidth);
|
|
5712
5976
|
if (trendLine) {
|
|
5713
5977
|
if (config?.layer === "above") {
|
|
@@ -5721,7 +5985,7 @@ var scatterRenderer = (spec, scales, chartArea, strategy, theme) => {
|
|
|
5721
5985
|
};
|
|
5722
5986
|
|
|
5723
5987
|
// src/charts/text/index.ts
|
|
5724
|
-
import { getRepresentativeColor as
|
|
5988
|
+
import { getRepresentativeColor as getRepresentativeColor11 } from "@opendata-ai/openchart-core";
|
|
5725
5989
|
var DEFAULT_FONT_SIZE = 12;
|
|
5726
5990
|
var ALIGN_TO_ANCHOR = {
|
|
5727
5991
|
left: "start",
|
|
@@ -5765,7 +6029,7 @@ function computeTextMarks(spec, scales) {
|
|
|
5765
6029
|
}
|
|
5766
6030
|
const text = String(row[textChannel.field] ?? "");
|
|
5767
6031
|
if (!text) continue;
|
|
5768
|
-
const color =
|
|
6032
|
+
const color = getRepresentativeColor11(
|
|
5769
6033
|
colorField ? getColor(scales, String(row[colorField] ?? "__default__")) : getColor(scales, "__default__")
|
|
5770
6034
|
);
|
|
5771
6035
|
const fallbackFontSize = markDef.fontSize ?? DEFAULT_FONT_SIZE;
|
|
@@ -5802,7 +6066,7 @@ var textRenderer = (spec, scales, _chartArea, _strategy, _theme) => {
|
|
|
5802
6066
|
};
|
|
5803
6067
|
|
|
5804
6068
|
// src/charts/tick/index.ts
|
|
5805
|
-
import { getRepresentativeColor as
|
|
6069
|
+
import { getRepresentativeColor as getRepresentativeColor12 } from "@opendata-ai/openchart-core";
|
|
5806
6070
|
var DEFAULT_TICK_LENGTH = 18;
|
|
5807
6071
|
function computeTickMarks(spec, scales, _chartArea) {
|
|
5808
6072
|
const encoding = spec.encoding;
|
|
@@ -5818,7 +6082,7 @@ function computeTickMarks(spec, scales, _chartArea) {
|
|
|
5818
6082
|
const xVal = scaleValue(scales.x.scale, scales.x.type, row[xChannel.field]);
|
|
5819
6083
|
const yVal = scaleValue(scales.y.scale, scales.y.type, row[yChannel.field]);
|
|
5820
6084
|
if (xVal == null || yVal == null) continue;
|
|
5821
|
-
const color =
|
|
6085
|
+
const color = getRepresentativeColor12(
|
|
5822
6086
|
colorField ? getColor(scales, String(row[colorField] ?? "__default__")) : getColor(scales, "__default__")
|
|
5823
6087
|
);
|
|
5824
6088
|
const aria = {
|
|
@@ -5853,7 +6117,7 @@ var tickRenderer = (spec, scales, chartArea, _strategy, _theme) => {
|
|
|
5853
6117
|
import { formatPercent as formatPercent3 } from "@opendata-ai/openchart-core";
|
|
5854
6118
|
var DEFAULT_UNITS = 100;
|
|
5855
6119
|
var DEFAULT_COLUMNS = 10;
|
|
5856
|
-
var DEFAULT_CORNER_RADIUS =
|
|
6120
|
+
var DEFAULT_CORNER_RADIUS = 2;
|
|
5857
6121
|
var GAP_FRACTION = 1 / 6;
|
|
5858
6122
|
function largestRemainderCells(values, units) {
|
|
5859
6123
|
const clean = values.map((v) => Number.isFinite(v) && v > 0 ? v : 0);
|
|
@@ -5933,6 +6197,7 @@ function computeWaffleMarks(spec, scales, chartArea, _strategy) {
|
|
|
5933
6197
|
height: cell,
|
|
5934
6198
|
fill,
|
|
5935
6199
|
cornerRadius,
|
|
6200
|
+
shapeRendering: "crispEdges",
|
|
5936
6201
|
data: row,
|
|
5937
6202
|
aria: k === 0 ? aria : { ...aria, decorative: true }
|
|
5938
6203
|
});
|
|
@@ -6558,25 +6823,25 @@ function emitSpecWarnings(warnings, sink = defaultWarnSink) {
|
|
|
6558
6823
|
|
|
6559
6824
|
// src/compiler/animation.ts
|
|
6560
6825
|
var ENTER_DEFAULTS = {
|
|
6561
|
-
duration:
|
|
6826
|
+
duration: 450,
|
|
6562
6827
|
ease: "smooth",
|
|
6563
|
-
staggerDelay:
|
|
6828
|
+
staggerDelay: 30,
|
|
6564
6829
|
staggerOrder: "index"
|
|
6565
6830
|
};
|
|
6566
6831
|
var UPDATE_DEFAULTS = {
|
|
6567
|
-
duration:
|
|
6832
|
+
duration: 450,
|
|
6568
6833
|
ease: "smooth",
|
|
6569
6834
|
staggerDelay: 0,
|
|
6570
6835
|
staggerOrder: "index"
|
|
6571
6836
|
};
|
|
6572
6837
|
var EXIT_DEFAULTS = {
|
|
6573
|
-
duration:
|
|
6838
|
+
duration: 250,
|
|
6574
6839
|
ease: "smooth",
|
|
6575
6840
|
staggerDelay: 0,
|
|
6576
6841
|
staggerOrder: "index"
|
|
6577
6842
|
};
|
|
6578
|
-
var DEFAULT_ANNOTATION_DELAY =
|
|
6579
|
-
var MAX_TOTAL_STAGGER_MS =
|
|
6843
|
+
var DEFAULT_ANNOTATION_DELAY = 150;
|
|
6844
|
+
var MAX_TOTAL_STAGGER_MS = 300;
|
|
6580
6845
|
function resolveAnimation(spec) {
|
|
6581
6846
|
if (spec === void 0 || spec === false) return void 0;
|
|
6582
6847
|
if (spec === true) {
|
|
@@ -6966,10 +7231,62 @@ function warnParliamentMajorityRange(markDef, encoding, data, warnings) {
|
|
|
6966
7231
|
);
|
|
6967
7232
|
}
|
|
6968
7233
|
}
|
|
7234
|
+
function withCalendarColorScale(encoding) {
|
|
7235
|
+
const color = encoding.color;
|
|
7236
|
+
if (!color || !("field" in color) || color.type !== "quantitative") return encoding;
|
|
7237
|
+
if (color.scale?.type) return encoding;
|
|
7238
|
+
return {
|
|
7239
|
+
...encoding,
|
|
7240
|
+
color: { ...color, scale: { ...color.scale, type: "quantize" } }
|
|
7241
|
+
};
|
|
7242
|
+
}
|
|
7243
|
+
function resolveOtherOption(other) {
|
|
7244
|
+
if (other == null) return null;
|
|
7245
|
+
const threshold = typeof other === "number" ? other : other.threshold;
|
|
7246
|
+
if (!Number.isFinite(threshold) || threshold <= 0) return null;
|
|
7247
|
+
return { threshold, label: typeof other === "object" && other.label || "Other" };
|
|
7248
|
+
}
|
|
7249
|
+
function applyWaffleOther(spec, encoding, markDef, warnings) {
|
|
7250
|
+
const option = resolveOtherOption(markDef.other);
|
|
7251
|
+
const colorField = encoding.color && "field" in encoding.color ? encoding.color.field : void 0;
|
|
7252
|
+
const valueField = (encoding.y ?? encoding.x)?.field;
|
|
7253
|
+
if (!option || !colorField || !valueField) return spec.data;
|
|
7254
|
+
const totals = /* @__PURE__ */ new Map();
|
|
7255
|
+
for (const row of spec.data) {
|
|
7256
|
+
const value = Number(row[valueField] ?? 0);
|
|
7257
|
+
if (!Number.isFinite(value) || value < 0) continue;
|
|
7258
|
+
const cat = String(row[colorField] ?? "");
|
|
7259
|
+
totals.set(cat, (totals.get(cat) ?? 0) + value);
|
|
7260
|
+
}
|
|
7261
|
+
let grandTotal = 0;
|
|
7262
|
+
for (const value of totals.values()) grandTotal += value;
|
|
7263
|
+
if (grandTotal <= 0) return spec.data;
|
|
7264
|
+
const merged = /* @__PURE__ */ new Set();
|
|
7265
|
+
for (const [cat, value] of totals) {
|
|
7266
|
+
if (value / grandTotal < option.threshold) merged.add(cat);
|
|
7267
|
+
}
|
|
7268
|
+
if (merged.size < 2) return spec.data;
|
|
7269
|
+
const kept = [];
|
|
7270
|
+
const bucketed = [];
|
|
7271
|
+
for (const row of spec.data) {
|
|
7272
|
+
const cat = String(row[colorField] ?? "");
|
|
7273
|
+
if (merged.has(cat)) {
|
|
7274
|
+
bucketed.push({ ...row, [colorField]: option.label });
|
|
7275
|
+
} else {
|
|
7276
|
+
kept.push(row);
|
|
7277
|
+
}
|
|
7278
|
+
}
|
|
7279
|
+
warnings.push(
|
|
7280
|
+
`[openchart] WAFFLE_OTHER_BUCKET: merged ${merged.size} categories under ${option.threshold * 100}% of the total into "${option.label}" (${[...merged].join(", ")})`
|
|
7281
|
+
);
|
|
7282
|
+
return [...kept, ...bucketed];
|
|
7283
|
+
}
|
|
6969
7284
|
function normalizeChartSpec(spec, warnings) {
|
|
6970
|
-
const
|
|
7285
|
+
const inferred = inferEncodingTypes(spec.encoding, spec.data, warnings);
|
|
6971
7286
|
const markType = resolveMarkType(spec.mark);
|
|
6972
7287
|
const markDef = resolveMarkDef(spec.mark);
|
|
7288
|
+
const encoding = markType === "calendar" ? withCalendarColorScale(inferred) : inferred;
|
|
7289
|
+
const data = markType === "waffle" ? applyWaffleOther(spec, encoding, markDef, warnings) : spec.data;
|
|
6973
7290
|
const display = spec.display ?? "full";
|
|
6974
7291
|
if (display === "sparkline" && markType !== "line" && markType !== "area" && markType !== "bar" && markType !== "point") {
|
|
6975
7292
|
warnings.push(
|
|
@@ -6987,7 +7304,7 @@ function normalizeChartSpec(spec, warnings) {
|
|
|
6987
7304
|
return {
|
|
6988
7305
|
markType,
|
|
6989
7306
|
markDef,
|
|
6990
|
-
data
|
|
7307
|
+
data,
|
|
6991
7308
|
encoding,
|
|
6992
7309
|
chrome: normalizeChrome(spec.chrome),
|
|
6993
7310
|
chromeLayout: spec.chromeLayout,
|
|
@@ -7024,7 +7341,17 @@ function normalizeChartSpec(spec, warnings) {
|
|
|
7024
7341
|
}
|
|
7025
7342
|
};
|
|
7026
7343
|
}
|
|
7027
|
-
function
|
|
7344
|
+
function normalizeTableDensity(spec, warnings) {
|
|
7345
|
+
if (spec.density) return spec.density;
|
|
7346
|
+
if (spec.compact) {
|
|
7347
|
+
warnings.push('[openchart] table "compact" is deprecated; use density: "condensed" instead.');
|
|
7348
|
+
return "condensed";
|
|
7349
|
+
}
|
|
7350
|
+
return "regular";
|
|
7351
|
+
}
|
|
7352
|
+
function normalizeTableSpec(spec, warnings) {
|
|
7353
|
+
const density = normalizeTableDensity(spec, warnings);
|
|
7354
|
+
const totalRow = spec.totalRow === true ? { label: "Total" } : spec.totalRow && typeof spec.totalRow === "object" ? { label: spec.totalRow.label ?? "Total" } : void 0;
|
|
7028
7355
|
return {
|
|
7029
7356
|
type: "table",
|
|
7030
7357
|
data: spec.data,
|
|
@@ -7036,7 +7363,11 @@ function normalizeTableSpec(spec, _warnings) {
|
|
|
7036
7363
|
search: spec.search ?? false,
|
|
7037
7364
|
pagination: spec.pagination ?? false,
|
|
7038
7365
|
stickyFirstColumn: spec.stickyFirstColumn ?? false,
|
|
7039
|
-
|
|
7366
|
+
density,
|
|
7367
|
+
striped: spec.striped ?? false,
|
|
7368
|
+
sort: spec.sort,
|
|
7369
|
+
totalRow,
|
|
7370
|
+
compact: density === "condensed",
|
|
7040
7371
|
responsive: spec.responsive ?? true,
|
|
7041
7372
|
animation: spec.animation,
|
|
7042
7373
|
watermark: spec.watermark ?? true
|
|
@@ -7061,6 +7392,7 @@ function normalizeSankeySpec(spec, _warnings) {
|
|
|
7061
7392
|
animation: spec.animation,
|
|
7062
7393
|
valueFormat: spec.valueFormat,
|
|
7063
7394
|
linkOpacity: spec.linkOpacity,
|
|
7395
|
+
other: spec.other,
|
|
7064
7396
|
watermark: spec.watermark ?? true
|
|
7065
7397
|
};
|
|
7066
7398
|
}
|
|
@@ -7151,7 +7483,10 @@ function normalizeGeoMapSpec(spec, _warnings) {
|
|
|
7151
7483
|
geo: {
|
|
7152
7484
|
features: spec.geo.features,
|
|
7153
7485
|
idField: spec.geo.idField ?? "id",
|
|
7154
|
-
|
|
7486
|
+
// Left undefined on purpose: compileGeoMap infers the projection from the
|
|
7487
|
+
// topology (resolveDefaultProjection) so a pre-projected atlas doesn't get
|
|
7488
|
+
// silently re-projected by an albersUsa default.
|
|
7489
|
+
projection: spec.geo.projection,
|
|
7155
7490
|
focus: spec.geo.focus ?? null
|
|
7156
7491
|
},
|
|
7157
7492
|
data: spec.data,
|
|
@@ -7444,6 +7779,40 @@ function validateRangeSpec(spec, errors) {
|
|
|
7444
7779
|
}
|
|
7445
7780
|
}
|
|
7446
7781
|
}
|
|
7782
|
+
function validateCenterLabel(spec, markType, errors) {
|
|
7783
|
+
if (!spec.mark || typeof spec.mark !== "object" || Array.isArray(spec.mark)) return;
|
|
7784
|
+
const centerLabel = spec.mark.centerLabel;
|
|
7785
|
+
if (centerLabel === void 0) return;
|
|
7786
|
+
if (markType !== "arc") {
|
|
7787
|
+
errors.push({
|
|
7788
|
+
message: `Spec error: mark.centerLabel is only supported on arc (pie/donut) marks, not "${String(markType)}"`,
|
|
7789
|
+
path: "mark.centerLabel",
|
|
7790
|
+
code: "INVALID_VALUE",
|
|
7791
|
+
suggestion: 'Remove mark.centerLabel, or use mark: { type: "arc", innerRadius: 0.6, centerLabel: "..." } for a donut center stat.'
|
|
7792
|
+
});
|
|
7793
|
+
return;
|
|
7794
|
+
}
|
|
7795
|
+
const isString = typeof centerLabel === "string";
|
|
7796
|
+
const obj = !isString && centerLabel !== null && typeof centerLabel === "object" ? centerLabel : void 0;
|
|
7797
|
+
const validObject = obj !== void 0 && typeof obj.text === "string" && obj.text.length > 0;
|
|
7798
|
+
const validString = isString && centerLabel.length > 0;
|
|
7799
|
+
if (!validString && !validObject) {
|
|
7800
|
+
errors.push({
|
|
7801
|
+
message: `Spec error: mark.centerLabel must be a non-empty string or { text, subtitle? }, got ${JSON.stringify(centerLabel)}`,
|
|
7802
|
+
path: "mark.centerLabel",
|
|
7803
|
+
code: "INVALID_VALUE",
|
|
7804
|
+
suggestion: 'Use centerLabel: "$4.2M" or centerLabel: { text: "$4.2M", subtitle: "Total revenue" }.'
|
|
7805
|
+
});
|
|
7806
|
+
}
|
|
7807
|
+
if (validObject && obj.subtitle !== void 0 && typeof obj.subtitle !== "string") {
|
|
7808
|
+
errors.push({
|
|
7809
|
+
message: `Spec error: mark.centerLabel.subtitle must be a string, got ${JSON.stringify(obj.subtitle)}`,
|
|
7810
|
+
path: "mark.centerLabel.subtitle",
|
|
7811
|
+
code: "INVALID_VALUE",
|
|
7812
|
+
suggestion: 'Use centerLabel: { text: "$4.2M", subtitle: "Total revenue" }.'
|
|
7813
|
+
});
|
|
7814
|
+
}
|
|
7815
|
+
}
|
|
7447
7816
|
var VALID_WEEK_STARTS = /* @__PURE__ */ new Set(["monday", "sunday"]);
|
|
7448
7817
|
function validateCalendarSpec(spec, errors) {
|
|
7449
7818
|
const encoding = spec.encoding;
|
|
@@ -7655,6 +8024,7 @@ function validateChartSpec(spec, errors) {
|
|
|
7655
8024
|
});
|
|
7656
8025
|
}
|
|
7657
8026
|
}
|
|
8027
|
+
validateCenterLabel(spec, markType, errors);
|
|
7658
8028
|
if (markType === "range") {
|
|
7659
8029
|
validateRangeSpec(spec, errors);
|
|
7660
8030
|
}
|
|
@@ -9445,7 +9815,10 @@ function assignAnimationIndices(marks, animation) {
|
|
|
9445
9815
|
}
|
|
9446
9816
|
|
|
9447
9817
|
// src/compile/color-scale-range.ts
|
|
9448
|
-
import {
|
|
9818
|
+
import {
|
|
9819
|
+
ACHROMATIC_RAMP,
|
|
9820
|
+
CATEGORICAL_EXTENDED_PALETTE
|
|
9821
|
+
} from "@opendata-ai/openchart-core";
|
|
9449
9822
|
|
|
9450
9823
|
// src/legend/continuous.ts
|
|
9451
9824
|
import {
|
|
@@ -9453,6 +9826,7 @@ import {
|
|
|
9453
9826
|
defaultNumberFormatter as defaultNumberFormatter5,
|
|
9454
9827
|
resolveNumberFormatter as resolveNumberFormatter2
|
|
9455
9828
|
} from "@opendata-ai/openchart-core";
|
|
9829
|
+
import { tickStep } from "d3-array";
|
|
9456
9830
|
import { scaleQuantile } from "d3-scale";
|
|
9457
9831
|
function extent(values) {
|
|
9458
9832
|
let min4 = Number.POSITIVE_INFINITY;
|
|
@@ -9464,11 +9838,18 @@ function extent(values) {
|
|
|
9464
9838
|
return [min4, max4];
|
|
9465
9839
|
}
|
|
9466
9840
|
var CONTINUOUS_BAR_HEIGHT = 10;
|
|
9841
|
+
var CONTINUOUS_BINNED_BAR_HEIGHT = 12;
|
|
9467
9842
|
var CONTINUOUS_LABEL_GAP = 4;
|
|
9468
9843
|
var BAR_WIDTH_MIN = 160;
|
|
9469
9844
|
var BAR_WIDTH_MAX = 220;
|
|
9470
9845
|
var DEFAULT_BIN_COUNT = 5;
|
|
9846
|
+
var COMPACT_SWATCH_SIZE = 11;
|
|
9847
|
+
var COMPACT_SWATCH_GAP = 2;
|
|
9848
|
+
var COMPACT_LABEL_GAP = 6;
|
|
9849
|
+
var COMPACT_LESS_LABEL = "Less";
|
|
9850
|
+
var COMPACT_MORE_LABEL = "More";
|
|
9471
9851
|
var BINNED_SCALE_TYPES = /* @__PURE__ */ new Set(["quantile", "quantize", "threshold"]);
|
|
9852
|
+
var MAX_BINS = 12;
|
|
9472
9853
|
function sampleRampColors(colors, k) {
|
|
9473
9854
|
if (k <= 0 || colors.length === 0) return [];
|
|
9474
9855
|
if (colors.length === k) return [...colors];
|
|
@@ -9517,7 +9898,47 @@ function resolveBarWidth(availableWidth) {
|
|
|
9517
9898
|
);
|
|
9518
9899
|
return Math.max(0, Math.min(preferred, Math.floor(availableWidth)));
|
|
9519
9900
|
}
|
|
9520
|
-
function
|
|
9901
|
+
function tidyBreak(value) {
|
|
9902
|
+
return Number(value.toPrecision(12));
|
|
9903
|
+
}
|
|
9904
|
+
function niceQuantizeBreaks(domainMin, domainMax, binCount, diverging) {
|
|
9905
|
+
if (!(domainMax > domainMin)) return [];
|
|
9906
|
+
if (diverging && domainMin < 0 && domainMax > 0) {
|
|
9907
|
+
const classes = binCount % 2 === 0 ? binCount + 1 : binCount;
|
|
9908
|
+
const perSide = (classes - 1) / 2;
|
|
9909
|
+
const magnitude = Math.max(Math.abs(domainMin), Math.abs(domainMax));
|
|
9910
|
+
const step2 = tickStep(0, magnitude, perSide + 0.5) || magnitude / (perSide + 0.5);
|
|
9911
|
+
if (!Number.isFinite(step2) || step2 <= 0) return [];
|
|
9912
|
+
const breaks2 = [];
|
|
9913
|
+
for (let i = 0; i < perSide * 2; i++) {
|
|
9914
|
+
breaks2.push(tidyBreak((i - perSide + 0.5) * step2));
|
|
9915
|
+
}
|
|
9916
|
+
return breaks2;
|
|
9917
|
+
}
|
|
9918
|
+
let step = tickStep(domainMin, domainMax, binCount);
|
|
9919
|
+
if (!Number.isFinite(step) || step <= 0) return [];
|
|
9920
|
+
let breaks = quantizeBreaksForStep(domainMin, domainMax, step);
|
|
9921
|
+
for (let guard = 0; guard < 10 && breaks.length + 1 > binCount; guard++) {
|
|
9922
|
+
step = coarserNiceStep(step);
|
|
9923
|
+
breaks = quantizeBreaksForStep(domainMin, domainMax, step);
|
|
9924
|
+
}
|
|
9925
|
+
return breaks;
|
|
9926
|
+
}
|
|
9927
|
+
function quantizeBreaksForStep(domainMin, domainMax, step) {
|
|
9928
|
+
const epsilon = step * 1e-9;
|
|
9929
|
+
const breaks = [];
|
|
9930
|
+
for (let b = Math.ceil(domainMin / step) * step; b < domainMax - epsilon && breaks.length < MAX_BINS; b += step) {
|
|
9931
|
+
if (b > domainMin + epsilon) breaks.push(tidyBreak(b));
|
|
9932
|
+
}
|
|
9933
|
+
return breaks;
|
|
9934
|
+
}
|
|
9935
|
+
function coarserNiceStep(step) {
|
|
9936
|
+
const power = Math.floor(Math.log10(step));
|
|
9937
|
+
const mantissa = step / 10 ** power;
|
|
9938
|
+
const next = mantissa < 1.5 ? 2 : mantissa < 3.5 ? 5 : 10;
|
|
9939
|
+
return tidyBreak(next * 10 ** power);
|
|
9940
|
+
}
|
|
9941
|
+
function resolveBinBreaks(channel, values, binCount, diverging) {
|
|
9521
9942
|
const scaleType = channel.scale?.type;
|
|
9522
9943
|
if (scaleType === "threshold") {
|
|
9523
9944
|
return channel.scale?.domain ?? [0.5];
|
|
@@ -9530,18 +9951,43 @@ function resolveBinBreaks(channel, values, binCount) {
|
|
|
9530
9951
|
const [valuesMin, valuesMax] = extent(values);
|
|
9531
9952
|
const domainMin = explicitDomain?.[0] ?? valuesMin;
|
|
9532
9953
|
const domainMax = explicitDomain?.[1] ?? valuesMax;
|
|
9533
|
-
|
|
9534
|
-
|
|
9535
|
-
|
|
9954
|
+
return niceQuantizeBreaks(domainMin, domainMax, binCount, diverging);
|
|
9955
|
+
}
|
|
9956
|
+
function resolveBinCount(channel) {
|
|
9957
|
+
if (channel.scale?.type === "threshold") {
|
|
9958
|
+
return (channel.scale?.domain ?? [0.5]).length + 1;
|
|
9536
9959
|
}
|
|
9537
|
-
|
|
9960
|
+
const explicitRange = channel.scale?.range;
|
|
9961
|
+
return explicitRange?.length ?? DEFAULT_BIN_COUNT;
|
|
9962
|
+
}
|
|
9963
|
+
function buildClassScale(values, channel, colors, binCount = DEFAULT_BIN_COUNT, options) {
|
|
9964
|
+
const breaks = resolveBinBreaks(channel, values, binCount, options?.diverging ?? false);
|
|
9965
|
+
const classColors = sampleRampColors(colors, breaks.length + 1);
|
|
9966
|
+
const binIndex = (value) => {
|
|
9967
|
+
if (!Number.isFinite(value)) return -1;
|
|
9968
|
+
let i = 0;
|
|
9969
|
+
while (i < breaks.length && value >= breaks[i]) i++;
|
|
9970
|
+
return i;
|
|
9971
|
+
};
|
|
9972
|
+
return {
|
|
9973
|
+
breaks,
|
|
9974
|
+
colors: classColors,
|
|
9975
|
+
binIndex,
|
|
9976
|
+
color: (value) => classColors[Math.max(0, binIndex(value))] ?? classColors[0]
|
|
9977
|
+
};
|
|
9978
|
+
}
|
|
9979
|
+
function classScaleForChannel(values, channel, theme) {
|
|
9980
|
+
const colors = resolveRampColors(channel, theme);
|
|
9981
|
+
return buildClassScale(values, channel, colors, resolveBinCount(channel), {
|
|
9982
|
+
diverging: isDivergingRamp(colors, theme)
|
|
9983
|
+
});
|
|
9538
9984
|
}
|
|
9539
9985
|
function hasContinuousColorScale(spec) {
|
|
9540
9986
|
const colorEnc = spec.encoding.color;
|
|
9541
9987
|
if (!colorEnc || "condition" in colorEnc || !("field" in colorEnc)) return false;
|
|
9542
9988
|
return colorEnc.type === "quantitative";
|
|
9543
9989
|
}
|
|
9544
|
-
function computeContinuousLegendContentForChannel(values, channel, theme, availableWidth) {
|
|
9990
|
+
function computeContinuousLegendContentForChannel(values, channel, theme, availableWidth, classScale) {
|
|
9545
9991
|
if (values.length === 0) return null;
|
|
9546
9992
|
const barWidth = resolveBarWidth(availableWidth);
|
|
9547
9993
|
if (barWidth <= 0) return null;
|
|
@@ -9550,17 +9996,15 @@ function computeContinuousLegendContentForChannel(values, channel, theme, availa
|
|
|
9550
9996
|
const formatStr = channel.format;
|
|
9551
9997
|
const ctx = computeFieldFormatContext2(values);
|
|
9552
9998
|
if (scaleType && BINNED_SCALE_TYPES.has(scaleType)) {
|
|
9553
|
-
const
|
|
9554
|
-
const
|
|
9555
|
-
const
|
|
9556
|
-
const colors = sampleRampColors(rampColors, binCount);
|
|
9557
|
-
const binWidth = barWidth / binCount;
|
|
9999
|
+
const scale = classScale ?? classScaleForChannel(values, channel, theme);
|
|
10000
|
+
const colors = scale.colors;
|
|
10001
|
+
const binWidth = barWidth / colors.length;
|
|
9558
10002
|
const bins = colors.map((color, i) => ({
|
|
9559
10003
|
x: i * binWidth,
|
|
9560
10004
|
width: binWidth,
|
|
9561
10005
|
color
|
|
9562
10006
|
}));
|
|
9563
|
-
const ticks2 = breaks.map((value, i) => ({
|
|
10007
|
+
const ticks2 = scale.breaks.map((value, i) => ({
|
|
9564
10008
|
value,
|
|
9565
10009
|
label: formatLegendValue(value, formatStr, ctx),
|
|
9566
10010
|
x: (i + 1) * binWidth,
|
|
@@ -9569,10 +10013,11 @@ function computeContinuousLegendContentForChannel(values, channel, theme, availa
|
|
|
9569
10013
|
return {
|
|
9570
10014
|
mode: "binned",
|
|
9571
10015
|
barWidth,
|
|
9572
|
-
barHeight:
|
|
10016
|
+
barHeight: CONTINUOUS_BINNED_BAR_HEIGHT,
|
|
9573
10017
|
colorStops: [],
|
|
9574
10018
|
bins,
|
|
9575
|
-
ticks: ticks2
|
|
10019
|
+
ticks: ticks2,
|
|
10020
|
+
classScale: scale
|
|
9576
10021
|
};
|
|
9577
10022
|
}
|
|
9578
10023
|
const [domainMin, domainMax] = extent(values);
|
|
@@ -9616,6 +10061,52 @@ function computeContinuousLegendContentForChannel(values, channel, theme, availa
|
|
|
9616
10061
|
ticks
|
|
9617
10062
|
};
|
|
9618
10063
|
}
|
|
10064
|
+
function computeCompactClassLegendContent(values, channel, theme, measure) {
|
|
10065
|
+
if (values.length === 0) return null;
|
|
10066
|
+
if (isDivergingRamp(resolveRampColors(channel, theme), theme)) return null;
|
|
10067
|
+
const scale = classScaleForChannel(values, channel, theme);
|
|
10068
|
+
const colors = scale.colors;
|
|
10069
|
+
if (colors.length === 0) return null;
|
|
10070
|
+
const bins = colors.map((color, i) => ({
|
|
10071
|
+
x: i * (COMPACT_SWATCH_SIZE + COMPACT_SWATCH_GAP),
|
|
10072
|
+
width: COMPACT_SWATCH_SIZE,
|
|
10073
|
+
color
|
|
10074
|
+
}));
|
|
10075
|
+
const barWidth = colors.length * COMPACT_SWATCH_SIZE + (colors.length - 1) * COMPACT_SWATCH_GAP;
|
|
10076
|
+
const fontSize = theme.fonts.sizes.small;
|
|
10077
|
+
const fontWeight = theme.fonts.weights.normal;
|
|
10078
|
+
const lessWidth = measure(COMPACT_LESS_LABEL, fontSize, fontWeight);
|
|
10079
|
+
const moreWidth = measure(COMPACT_MORE_LABEL, fontSize, fontWeight);
|
|
10080
|
+
const [domainMin, domainMax] = extent(values);
|
|
10081
|
+
const ticks = [
|
|
10082
|
+
{
|
|
10083
|
+
value: domainMin,
|
|
10084
|
+
label: COMPACT_LESS_LABEL,
|
|
10085
|
+
x: -COMPACT_LABEL_GAP,
|
|
10086
|
+
anchor: "end"
|
|
10087
|
+
},
|
|
10088
|
+
{
|
|
10089
|
+
value: domainMax,
|
|
10090
|
+
label: COMPACT_MORE_LABEL,
|
|
10091
|
+
x: barWidth + COMPACT_LABEL_GAP,
|
|
10092
|
+
anchor: "start"
|
|
10093
|
+
}
|
|
10094
|
+
];
|
|
10095
|
+
return {
|
|
10096
|
+
mode: "binned",
|
|
10097
|
+
barWidth,
|
|
10098
|
+
barHeight: COMPACT_SWATCH_SIZE,
|
|
10099
|
+
colorStops: [],
|
|
10100
|
+
bins,
|
|
10101
|
+
ticks,
|
|
10102
|
+
classScale: scale,
|
|
10103
|
+
barOffsetX: lessWidth + COMPACT_LABEL_GAP,
|
|
10104
|
+
// Optical centre of the cap height against the swatch row.
|
|
10105
|
+
labelBaselineOffset: COMPACT_SWATCH_SIZE / 2 + fontSize * 0.35,
|
|
10106
|
+
align: "right",
|
|
10107
|
+
blockWidth: lessWidth + COMPACT_LABEL_GAP + barWidth + COMPACT_LABEL_GAP + moreWidth
|
|
10108
|
+
};
|
|
10109
|
+
}
|
|
9619
10110
|
function computeContinuousLegendContent(spec, theme, availableWidth) {
|
|
9620
10111
|
if (!hasContinuousColorScale(spec)) return null;
|
|
9621
10112
|
const colorEnc = spec.encoding.color;
|
|
@@ -9624,12 +10115,44 @@ function computeContinuousLegendContent(spec, theme, availableWidth) {
|
|
|
9624
10115
|
}
|
|
9625
10116
|
|
|
9626
10117
|
// src/compile/color-scale-range.ts
|
|
9627
|
-
var
|
|
9628
|
-
|
|
9629
|
-
|
|
10118
|
+
var FILL_MARKS = /* @__PURE__ */ new Set([
|
|
10119
|
+
"bar",
|
|
10120
|
+
"area",
|
|
10121
|
+
"arc",
|
|
10122
|
+
"waffle",
|
|
10123
|
+
"calendar",
|
|
10124
|
+
"rect"
|
|
10125
|
+
]);
|
|
10126
|
+
function mutedSeriesColor(theme) {
|
|
10127
|
+
return theme.isDark ? ACHROMATIC_RAMP.fgFaint : "#d4d4d8";
|
|
10128
|
+
}
|
|
10129
|
+
function paletteFor(theme, markType) {
|
|
10130
|
+
return markType && FILL_MARKS.has(markType) ? theme.colors.categoricalFill : theme.colors.categorical;
|
|
10131
|
+
}
|
|
10132
|
+
function extendPalette(palette, needed) {
|
|
10133
|
+
if (needed <= palette.length) return palette;
|
|
10134
|
+
const out = [...palette];
|
|
10135
|
+
for (let i = 0; out.length < needed; i++) {
|
|
10136
|
+
out.push(CATEGORICAL_EXTENDED_PALETTE[i % CATEGORICAL_EXTENDED_PALETTE.length]);
|
|
10137
|
+
}
|
|
10138
|
+
return out;
|
|
10139
|
+
}
|
|
10140
|
+
function defaultSeriesColor(theme, markType) {
|
|
10141
|
+
return paletteFor(theme, markType)[0];
|
|
10142
|
+
}
|
|
10143
|
+
function paletteOverflowWarnings(domainLength, theme, markType) {
|
|
10144
|
+
const palette = paletteFor(theme, markType);
|
|
10145
|
+
if (domainLength <= palette.length) return [];
|
|
10146
|
+
return [
|
|
10147
|
+
`[openchart] color encoding has ${domainLength} categories but the palette carries ${palette.length}; slots ${palette.length + 1}+ come from the extended ramp and are harder to tell apart. Bucket the tail, facet, or direct-label instead.`
|
|
10148
|
+
];
|
|
10149
|
+
}
|
|
10150
|
+
function categoricalColorsForDomain(domain, theme, highlight, markType) {
|
|
10151
|
+
const palette = extendPalette(paletteFor(theme, markType), domain.length);
|
|
9630
10152
|
if (highlight && highlight.length > 0) {
|
|
9631
10153
|
const highlightSet = new Set(highlight);
|
|
9632
|
-
|
|
10154
|
+
const muted = mutedSeriesColor(theme);
|
|
10155
|
+
return domain.map((v, i) => highlightSet.has(v) ? palette[i % palette.length] : muted);
|
|
9633
10156
|
}
|
|
9634
10157
|
const strategy = theme.seriesStrategy ?? "palette";
|
|
9635
10158
|
if (strategy !== "accent-neutral") return palette;
|
|
@@ -9639,10 +10162,10 @@ function categoricalColorsForDomain(domain, theme, highlight) {
|
|
|
9639
10162
|
const neutrals = theme.isDark ? [ACHROMATIC_RAMP.fgMuted, ACHROMATIC_RAMP.fgSubtle, ACHROMATIC_RAMP.fgFaint] : [ACHROMATIC_RAMP.fgFaint, ACHROMATIC_RAMP.fgSubtle, ACHROMATIC_RAMP.fgMuted];
|
|
9640
10163
|
return domain.map((_, i) => i === 0 ? palette[0] : neutrals[(i - 1) % neutrals.length]);
|
|
9641
10164
|
}
|
|
9642
|
-
function applyColorScaleRange(scales, encoding, theme, highlight) {
|
|
9643
|
-
if (!scales.color) return;
|
|
10165
|
+
function applyColorScaleRange(scales, encoding, theme, highlight, markType) {
|
|
10166
|
+
if (!scales.color) return [];
|
|
9644
10167
|
const hasExplicitRange = !!(encoding.color && "field" in encoding.color && encoding.color.scale?.range?.length);
|
|
9645
|
-
if (hasExplicitRange) return;
|
|
10168
|
+
if (hasExplicitRange) return [];
|
|
9646
10169
|
if (scales.color.type === "sequential") {
|
|
9647
10170
|
const seqStops = Object.values(theme.colors.sequential)[0] ?? theme.colors.categorical;
|
|
9648
10171
|
scales.color.scale.range([
|
|
@@ -9655,8 +10178,11 @@ function applyColorScaleRange(scales, encoding, theme, highlight) {
|
|
|
9655
10178
|
scale.range(sampleRampColors(seqStops, scale.range().length));
|
|
9656
10179
|
} else {
|
|
9657
10180
|
const ordinalScale = scales.color.scale;
|
|
9658
|
-
ordinalScale.
|
|
10181
|
+
const domain = ordinalScale.domain();
|
|
10182
|
+
ordinalScale.range(categoricalColorsForDomain(domain, theme, highlight, markType));
|
|
10183
|
+
return paletteOverflowWarnings(domain.length, theme, markType);
|
|
9659
10184
|
}
|
|
10185
|
+
return [];
|
|
9660
10186
|
}
|
|
9661
10187
|
|
|
9662
10188
|
// src/compile/data-clip.ts
|
|
@@ -9683,14 +10209,14 @@ function filterClippedDomains(data, encoding) {
|
|
|
9683
10209
|
}
|
|
9684
10210
|
|
|
9685
10211
|
// src/compile/fill-patterns.ts
|
|
9686
|
-
import { getRepresentativeColor as
|
|
10212
|
+
import { getRepresentativeColor as getRepresentativeColor13, pickLabelColor as pickLabelColor3 } from "@opendata-ai/openchart-core";
|
|
9687
10213
|
var PATTERN_SEQUENCE = ["diagonal", "dot", "crosshatch", "vertical"];
|
|
9688
10214
|
var MIN_PATTERN_EXTENT = 12;
|
|
9689
10215
|
function applyFillPatterns(marks, markDef, theme) {
|
|
9690
10216
|
if (markDef.fillPattern !== "auto") return;
|
|
9691
10217
|
const assignment = /* @__PURE__ */ new Map();
|
|
9692
10218
|
const resolve = (fill) => {
|
|
9693
|
-
const base =
|
|
10219
|
+
const base = getRepresentativeColor13(fill);
|
|
9694
10220
|
let resolved = assignment.get(base);
|
|
9695
10221
|
if (!resolved) {
|
|
9696
10222
|
const type = PATTERN_SEQUENCE[assignment.size % PATTERN_SEQUENCE.length];
|
|
@@ -10055,7 +10581,7 @@ function evenRange(start, end, count) {
|
|
|
10055
10581
|
return Array.from({ length: count }, (_, i) => start + step * i);
|
|
10056
10582
|
}
|
|
10057
10583
|
function resolveBandPadding(scaleConfig) {
|
|
10058
|
-
const padding = scaleConfig?.padding ??
|
|
10584
|
+
const padding = scaleConfig?.padding ?? DEFAULT_BAND_PADDING;
|
|
10059
10585
|
return {
|
|
10060
10586
|
paddingInner: scaleConfig?.paddingInner ?? padding,
|
|
10061
10587
|
paddingOuter: scaleConfig?.paddingOuter ?? padding
|
|
@@ -10072,7 +10598,7 @@ function estimateBandStep(scaleConfig, plotWidth, n) {
|
|
|
10072
10598
|
if (denom <= 0) return 0;
|
|
10073
10599
|
return plotWidth / denom;
|
|
10074
10600
|
}
|
|
10075
|
-
var DEFAULT_BAND_PADDING = 0.
|
|
10601
|
+
var DEFAULT_BAND_PADDING = 0.25;
|
|
10076
10602
|
var HEATMAP_BAND_PADDING = 0.04;
|
|
10077
10603
|
function buildBandScale(channel, data, rangeStart, rangeEnd, defaultPadding = DEFAULT_BAND_PADDING) {
|
|
10078
10604
|
const values = channel.scale?.domain ? channel.scale.domain : applyCategoricalSort(uniqueFieldStrings2(data, channel.field), channel.sort);
|
|
@@ -10450,6 +10976,16 @@ var LEGEND_RIGHT_WIDTH = 120;
|
|
|
10450
10976
|
var RIGHT_LEGEND_MAX_HEIGHT_RATIO = 0.4;
|
|
10451
10977
|
var TOP_LEGEND_MAX_ROWS = 2;
|
|
10452
10978
|
var ARC_LABEL_CROWDING_LIMIT = 21;
|
|
10979
|
+
function compactClassLegend(spec, theme, measureWidth) {
|
|
10980
|
+
const colorEnc = spec.encoding.color;
|
|
10981
|
+
if (!colorEnc || !("field" in colorEnc)) return null;
|
|
10982
|
+
const values = [];
|
|
10983
|
+
for (const row of spec.data) {
|
|
10984
|
+
const value = Number(row[colorEnc.field]);
|
|
10985
|
+
if (Number.isFinite(value)) values.push(value);
|
|
10986
|
+
}
|
|
10987
|
+
return computeCompactClassLegendContent(values, colorEnc, theme, measureWidth);
|
|
10988
|
+
}
|
|
10453
10989
|
function hasLegendContent(content) {
|
|
10454
10990
|
return content.entries.length > 0 || content.continuous != null;
|
|
10455
10991
|
}
|
|
@@ -10501,7 +11037,8 @@ function extractColorEntries(spec, theme) {
|
|
|
10501
11037
|
const explicitRange = colorEnc.scale?.range;
|
|
10502
11038
|
const shape = swatchShapeForType(spec.markType);
|
|
10503
11039
|
const scaleDomain = explicitDomain ? explicitDomain.map(String) : applyCategoricalSort(dataValues, colorEnc.sort);
|
|
10504
|
-
const
|
|
11040
|
+
const rawDomainColors = explicitRange ?? categoricalColorsForDomain(scaleDomain, theme, spec.highlight, spec.markType);
|
|
11041
|
+
const domainColors = isStrokeSeriesMark(spec.markType) ? rawDomainColors.map((c) => resolveSeriesStroke(spec, c, theme)) : rawDomainColors;
|
|
10505
11042
|
const uniqueValues = explicitDomain ? [
|
|
10506
11043
|
...explicitDomain.filter((v) => dataValues.includes(v)),
|
|
10507
11044
|
...dataValues.filter((v) => !explicitDomain.includes(v))
|
|
@@ -10557,8 +11094,9 @@ function computeLegendContent(spec, strategy, theme, availableWidth, availableHe
|
|
|
10557
11094
|
return emptyContent("top", theme);
|
|
10558
11095
|
}
|
|
10559
11096
|
if (hasContinuousColorScale(spec)) {
|
|
10560
|
-
const
|
|
10561
|
-
const
|
|
11097
|
+
const compact = spec.markType === "calendar" && !spec.legend?.position ? compactClassLegend(spec, theme, measureWidth) : null;
|
|
11098
|
+
const continuous = compact ?? computeContinuousLegendContent(spec, theme, availableWidth);
|
|
11099
|
+
const continuousPosition = compact ? "bottom" : spec.legend?.position === "bottom" ? "bottom" : "top";
|
|
10562
11100
|
if (!continuous) {
|
|
10563
11101
|
return emptyContent(continuousPosition, theme);
|
|
10564
11102
|
}
|
|
@@ -10571,15 +11109,16 @@ function computeLegendContent(spec, strategy, theme, availableWidth, availableHe
|
|
|
10571
11109
|
fontVariant: "tabular-nums"
|
|
10572
11110
|
};
|
|
10573
11111
|
const labelRowHeight = Math.ceil(labelStyle2.fontSize * labelStyle2.lineHeight);
|
|
10574
|
-
const height = continuous.barHeight + CONTINUOUS_LABEL_GAP + labelRowHeight;
|
|
11112
|
+
const height = compact ? Math.max(continuous.barHeight, labelRowHeight) : continuous.barHeight + CONTINUOUS_LABEL_GAP + labelRowHeight;
|
|
11113
|
+
const blockWidth = continuous.blockWidth ?? continuous.barWidth;
|
|
10575
11114
|
return {
|
|
10576
11115
|
entries: [],
|
|
10577
11116
|
position: continuousPosition,
|
|
10578
11117
|
labelStyle: labelStyle2,
|
|
10579
11118
|
rowCount: 1,
|
|
10580
|
-
totalWidth:
|
|
11119
|
+
totalWidth: blockWidth,
|
|
10581
11120
|
height,
|
|
10582
|
-
legendWidth:
|
|
11121
|
+
legendWidth: blockWidth,
|
|
10583
11122
|
offset: spec.legend?.offset,
|
|
10584
11123
|
...categoricalDefaults(theme),
|
|
10585
11124
|
continuous
|
|
@@ -10618,7 +11157,10 @@ function computeLegendContent(spec, strategy, theme, availableWidth, availableHe
|
|
|
10618
11157
|
fontSize: theme.fonts.sizes.small,
|
|
10619
11158
|
fontWeight: theme.fonts.weights.normal,
|
|
10620
11159
|
fill: theme.colors.text,
|
|
10621
|
-
lineHeight: 1.3
|
|
11160
|
+
lineHeight: 1.3,
|
|
11161
|
+
// Legend labels are often values or years; lining figures keep the column
|
|
11162
|
+
// of entries from shimmying.
|
|
11163
|
+
fontVariant: "tabular-nums"
|
|
10622
11164
|
};
|
|
10623
11165
|
const specPosition = spec.legend?.position === "top-left" ? "top" : spec.legend?.position;
|
|
10624
11166
|
const resolvedPosition = specPosition ?? (strategy.legendPosition === "right" ? "right" : "top");
|
|
@@ -10753,10 +11295,15 @@ function placeLegend(content, chartArea, containerWidth, _theme, xAxisHeight, ax
|
|
|
10753
11295
|
const cDx = offset?.dx ?? 0;
|
|
10754
11296
|
const cDy = offset?.dy ?? 0;
|
|
10755
11297
|
const cGap = legendGap(containerWidth);
|
|
10756
|
-
const boundsX2 = chartArea.x + cDx;
|
|
11298
|
+
const boundsX2 = c.align === "right" ? chartArea.x + chartArea.width - legendWidth + cDx : chartArea.x + cDx;
|
|
10757
11299
|
const boundsY2 = position === "bottom" ? chartArea.y + chartArea.height + xAxisHeight + cGap + cDy : chartArea.y - axisGapBelowLegend - cGap - height + cDy;
|
|
10758
11300
|
const bounds2 = { x: boundsX2, y: boundsY2, width: legendWidth, height };
|
|
10759
|
-
const bar = {
|
|
11301
|
+
const bar = {
|
|
11302
|
+
x: bounds2.x + (c.barOffsetX ?? 0),
|
|
11303
|
+
y: bounds2.y,
|
|
11304
|
+
width: c.barWidth,
|
|
11305
|
+
height: c.barHeight
|
|
11306
|
+
};
|
|
10760
11307
|
return {
|
|
10761
11308
|
type: "continuous",
|
|
10762
11309
|
mode: c.mode,
|
|
@@ -10767,7 +11314,7 @@ function placeLegend(content, chartArea, containerWidth, _theme, xAxisHeight, ax
|
|
|
10767
11314
|
colorStops: c.colorStops,
|
|
10768
11315
|
bins: c.bins.map((b) => ({ ...b, x: b.x + bar.x })),
|
|
10769
11316
|
ticks: c.ticks.map((t) => ({ ...t, x: t.x + bar.x })),
|
|
10770
|
-
labelY: bar.y + bar.height + CONTINUOUS_LABEL_GAP + labelStyle.fontSize
|
|
11317
|
+
labelY: bar.y + (c.labelBaselineOffset ?? bar.height + CONTINUOUS_LABEL_GAP + labelStyle.fontSize)
|
|
10771
11318
|
};
|
|
10772
11319
|
}
|
|
10773
11320
|
if (entries.length === 0) {
|
|
@@ -10825,6 +11372,7 @@ import {
|
|
|
10825
11372
|
import { scaleLinear as scaleLinear3, scaleSqrt as scaleSqrt3 } from "d3-scale";
|
|
10826
11373
|
var LABEL_GAP = 6;
|
|
10827
11374
|
var BASELINE_PAD = 2;
|
|
11375
|
+
var TITLE_GAP = 6;
|
|
10828
11376
|
var CIRCLE_COUNT = 3;
|
|
10829
11377
|
var MIN_LEGIBLE_RANGE = [4, 18];
|
|
10830
11378
|
var SIZE_LEGEND_GAP = 12;
|
|
@@ -10882,6 +11430,15 @@ function toPrecision(value, digits) {
|
|
|
10882
11430
|
const magnitude = 10 ** (Math.floor(Math.log10(Math.abs(value))) - digits + 1);
|
|
10883
11431
|
return Math.round(value / magnitude) * magnitude;
|
|
10884
11432
|
}
|
|
11433
|
+
function sizeLegendTitleStyle(theme) {
|
|
11434
|
+
return {
|
|
11435
|
+
fontFamily: theme.fonts.family,
|
|
11436
|
+
fontSize: theme.fonts.sizes.small,
|
|
11437
|
+
fontWeight: theme.fonts.weights.medium,
|
|
11438
|
+
fill: theme.colors.neutral.secondary,
|
|
11439
|
+
lineHeight: 1.3
|
|
11440
|
+
};
|
|
11441
|
+
}
|
|
10885
11442
|
function hasSizeChannel(encoding) {
|
|
10886
11443
|
const size = encoding?.size;
|
|
10887
11444
|
return !!size && "field" in size && !!size.field && size.type === "quantitative";
|
|
@@ -10904,7 +11461,9 @@ function computeSizeLegendContent(sizeEncoding, data, theme, options) {
|
|
|
10904
11461
|
const maxRadius = drawScale(resolved.domain[1]);
|
|
10905
11462
|
if (!Number.isFinite(maxRadius) || maxRadius <= 0) return null;
|
|
10906
11463
|
const fontSize = theme.fonts.sizes.small;
|
|
10907
|
-
const
|
|
11464
|
+
const title = (sizeEncoding?.title ?? sizeEncoding?.field) || void 0;
|
|
11465
|
+
const titleRowHeight = title ? Math.ceil(fontSize * 1.3) + TITLE_GAP : 0;
|
|
11466
|
+
const baseline = titleRowHeight + maxRadius * 2;
|
|
10908
11467
|
const cx = maxRadius;
|
|
10909
11468
|
const lineHeight = fontSize * 1.25;
|
|
10910
11469
|
let prevLabelY = Number.NEGATIVE_INFINITY;
|
|
@@ -10926,10 +11485,14 @@ function computeSizeLegendContent(sizeEncoding, data, theme, options) {
|
|
|
10926
11485
|
...circles.map((c) => estimateTextWidth13(c.label, fontSize, theme.fonts.weights.normal))
|
|
10927
11486
|
);
|
|
10928
11487
|
const lastLabelBottom = (circles[circles.length - 1]?.labelY ?? 0) + fontSize * 0.5;
|
|
11488
|
+
const circleBlockWidth = maxRadius * 2 + LABEL_GAP + widestLabel;
|
|
11489
|
+
const titleWidth = title ? estimateTextWidth13(title, fontSize, theme.fonts.weights.medium) : 0;
|
|
10929
11490
|
return {
|
|
10930
11491
|
circles,
|
|
10931
|
-
width:
|
|
10932
|
-
height: Math.max(baseline, lastLabelBottom) + BASELINE_PAD
|
|
11492
|
+
width: Math.max(circleBlockWidth, titleWidth),
|
|
11493
|
+
height: Math.max(baseline, lastLabelBottom) + BASELINE_PAD,
|
|
11494
|
+
title,
|
|
11495
|
+
titleY: title ? fontSize : void 0
|
|
10933
11496
|
};
|
|
10934
11497
|
}
|
|
10935
11498
|
function placeSizeLegend(content, chartArea, theme, colorLegend) {
|
|
@@ -10956,7 +11519,10 @@ function placeSizeLegend(content, chartArea, theme, colorLegend) {
|
|
|
10956
11519
|
fill: theme.colors.text,
|
|
10957
11520
|
lineHeight: 1.3,
|
|
10958
11521
|
fontVariant: "tabular-nums"
|
|
10959
|
-
}
|
|
11522
|
+
},
|
|
11523
|
+
title: content.title,
|
|
11524
|
+
titleStyle: content.title ? sizeLegendTitleStyle(theme) : void 0,
|
|
11525
|
+
titleY: content.titleY
|
|
10960
11526
|
};
|
|
10961
11527
|
}
|
|
10962
11528
|
|
|
@@ -10965,7 +11531,6 @@ import {
|
|
|
10965
11531
|
axisTitleOffset,
|
|
10966
11532
|
computeXAxisExtentFromLabels,
|
|
10967
11533
|
estimateTextWidth as estimateTextWidth15,
|
|
10968
|
-
getAxisTitleOffset,
|
|
10969
11534
|
truncateRotatedLabel,
|
|
10970
11535
|
X_AXIS_TITLE_BAND,
|
|
10971
11536
|
X_AXIS_TITLE_BAND_ROTATED
|
|
@@ -11384,6 +11949,16 @@ function clampXTickLabels(ticks, totalWidth, tickAngle, fontSize, fontWeight, me
|
|
|
11384
11949
|
return x === tick.position ? tick : { ...tick, labelPosition: x };
|
|
11385
11950
|
});
|
|
11386
11951
|
}
|
|
11952
|
+
function capTickCount(ticks, max4) {
|
|
11953
|
+
if (max4 <= 0 || ticks.length <= max4) return ticks;
|
|
11954
|
+
if (max4 === 1) return [ticks[0]];
|
|
11955
|
+
const last = ticks.length - 1;
|
|
11956
|
+
const kept = [];
|
|
11957
|
+
for (let i = 0; i < max4; i++) {
|
|
11958
|
+
kept.push(ticks[Math.round(i * last / (max4 - 1))]);
|
|
11959
|
+
}
|
|
11960
|
+
return kept;
|
|
11961
|
+
}
|
|
11387
11962
|
function computeAxes(scales, chartArea, strategy, theme, measureText, dataContext) {
|
|
11388
11963
|
const result = {};
|
|
11389
11964
|
const baseDensity = strategy.axisLabelDensity;
|
|
@@ -11478,6 +12053,9 @@ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContex
|
|
|
11478
12053
|
} else {
|
|
11479
12054
|
ticks = thinTicksUntilFit(allTicks, fontSize, fontWeight, measureText);
|
|
11480
12055
|
}
|
|
12056
|
+
if (isContinuousX && !hasExplicitValues && dataContext?.maxXTicks != null) {
|
|
12057
|
+
ticks = capTickCount(ticks, dataContext.maxXTicks);
|
|
12058
|
+
}
|
|
11481
12059
|
if (tickAngle && Math.abs(tickAngle) > 10) {
|
|
11482
12060
|
const measureWidth = (text, size, weight) => measureLabel(text, size, weight ?? fontWeight, measureText);
|
|
11483
12061
|
ticks = ticks.map((t) => {
|
|
@@ -11497,7 +12075,7 @@ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContex
|
|
|
11497
12075
|
const xDomainLine = axisConfig?.domain ?? (GROUNDED_MARKS.has(dataContext?.markType ?? "") ? void 0 : false);
|
|
11498
12076
|
result.x = {
|
|
11499
12077
|
ticks,
|
|
11500
|
-
gridlines: axisConfig?.grid ?? dataContext?.markType === "point" ? gridlines : [],
|
|
12078
|
+
gridlines: axisConfig?.grid ?? (!dataContext?.suppressGrid && dataContext?.markType === "point") ? gridlines : [],
|
|
11501
12079
|
label: axisTitle,
|
|
11502
12080
|
labelStyle: xLabelColor ? { ...axisLabelStyle, fill: xLabelColor } : axisLabelStyle,
|
|
11503
12081
|
tickLabelStyle: xLabelColor ? { ...tickLabelStyle, fill: xLabelColor } : tickLabelStyle,
|
|
@@ -11575,7 +12153,7 @@ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContex
|
|
|
11575
12153
|
// Not for heatmaps: the cells tile the plot, so a gridline can only draw
|
|
11576
12154
|
// *through* them. The cell grid already is the grid. Explicit `grid: true`
|
|
11577
12155
|
// still wins.
|
|
11578
|
-
gridlines: axisConfig?.grid ?? dataContext?.markType !== "rect" ? gridlines : [],
|
|
12156
|
+
gridlines: axisConfig?.grid ?? (!dataContext?.suppressGrid && dataContext?.markType !== "rect") ? gridlines : [],
|
|
11579
12157
|
label: axisTitle,
|
|
11580
12158
|
labelStyle: yLabelColor ? { ...axisLabelStyle, fill: yLabelColor } : axisLabelStyle,
|
|
11581
12159
|
tickLabelStyle: yLabelColor ? { ...tickLabelStyle, fill: yLabelColor } : tickLabelStyle,
|
|
@@ -11626,7 +12204,15 @@ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContex
|
|
|
11626
12204
|
if (result.y?.label && result.y.labelStyle) {
|
|
11627
12205
|
const isRight = result.y.orient === "right";
|
|
11628
12206
|
if (isRight) {
|
|
11629
|
-
const
|
|
12207
|
+
const maxTickLabelWidth = result.y.ticks.reduce((max4, t) => {
|
|
12208
|
+
const w = estimateTextWidth15(
|
|
12209
|
+
t.label,
|
|
12210
|
+
result.y.tickLabelStyle.fontSize,
|
|
12211
|
+
result.y.tickLabelStyle.fontWeight ?? 400
|
|
12212
|
+
);
|
|
12213
|
+
return Math.max(max4, w);
|
|
12214
|
+
}, 0);
|
|
12215
|
+
const titleOff = axisTitleOffset(maxTickLabelWidth, result.y.labelStyle.fontSize, totalWidth);
|
|
11630
12216
|
result.y.titlePosition = {
|
|
11631
12217
|
x: chartArea.x + chartArea.width + titleOff,
|
|
11632
12218
|
y: chartArea.y + chartArea.height / 2,
|
|
@@ -12107,6 +12693,10 @@ function estimateYAxisLabelWidth(data, encoding, baseFontSize, measure) {
|
|
|
12107
12693
|
const v = Number(row[yField]);
|
|
12108
12694
|
if (Number.isFinite(v) && Math.abs(v) > maxAbsVal) maxAbsVal = Math.abs(v);
|
|
12109
12695
|
}
|
|
12696
|
+
if (maxAbsVal > 0) {
|
|
12697
|
+
const mag = 10 ** (Math.floor(Math.log10(maxAbsVal)) - 1);
|
|
12698
|
+
maxAbsVal = Math.ceil(maxAbsVal / mag) * mag;
|
|
12699
|
+
}
|
|
12110
12700
|
const ctx = computeFieldFormatContext5(data.map((r) => r[yField]));
|
|
12111
12701
|
let sampleLabel;
|
|
12112
12702
|
if (yAxisFormat) {
|
|
@@ -12150,7 +12740,8 @@ function compileLayerIndependent(leaves, layerSpec, options, compileChart2) {
|
|
|
12150
12740
|
const tickExtent = TICK_LABEL_OFFSET + rightAxisWidth;
|
|
12151
12741
|
const bodyFontSize = theme.fonts?.sizes?.body ?? 13;
|
|
12152
12742
|
const halfGlyph = Math.ceil(bodyFontSize / 2);
|
|
12153
|
-
const
|
|
12743
|
+
const rawTickWidth = Math.max(0, rightAxisWidth - 10);
|
|
12744
|
+
const titleCenterOffset = axisTitleOffset3(rawTickWidth, bodyFontSize, options.width);
|
|
12154
12745
|
const titleExtent = hasRightAxisTitle ? titleCenterOffset + halfGlyph + (options.width < BREAKPOINT_COMPACT_MAX2 ? 0 : AXIS_TITLE_TRAILING_PAD2) : 0;
|
|
12155
12746
|
const rightReserve = Math.max(tickExtent, titleExtent);
|
|
12156
12747
|
const optionsWithReserve = {
|
|
@@ -12186,7 +12777,7 @@ function compileLayerIndependent(leaves, layerSpec, options, compileChart2) {
|
|
|
12186
12777
|
},
|
|
12187
12778
|
...layout1.axes.y.label ? {
|
|
12188
12779
|
titlePosition: {
|
|
12189
|
-
x: layout0.area.x + layout0.area.width + axisTitleOffset3(
|
|
12780
|
+
x: layout0.area.x + layout0.area.width + axisTitleOffset3(rawTickWidth, bodyFontSize, layout0.dimensions.width),
|
|
12190
12781
|
y: layout0.area.y + layout0.area.height / 2,
|
|
12191
12782
|
angle: 90
|
|
12192
12783
|
}
|
|
@@ -12599,7 +13190,7 @@ function resolveYouDrawIt(config, marks, scales, chartArea, xField) {
|
|
|
12599
13190
|
}
|
|
12600
13191
|
|
|
12601
13192
|
// src/compiler/a11y-warnings.ts
|
|
12602
|
-
import { contrastRatio, findAccessibleColor, isOpaqueColor
|
|
13193
|
+
import { contrastRatio, findAccessibleColor, isOpaqueColor } from "@opendata-ai/openchart-core";
|
|
12603
13194
|
var GRAPHIC_MIN = 3;
|
|
12604
13195
|
var TEXT_MIN = 4.5;
|
|
12605
13196
|
var ADJACENT_FILL_MARKS = /* @__PURE__ */ new Set(["bar", "area", "arc", "waffle"]);
|
|
@@ -12607,7 +13198,7 @@ var fmt = (ratio) => `${ratio.toFixed(2)}:1`;
|
|
|
12607
13198
|
function collectContrastWarnings(scales, markType, theme) {
|
|
12608
13199
|
const warnings = [];
|
|
12609
13200
|
const bg = theme.colors.background;
|
|
12610
|
-
const bgIsCheckable =
|
|
13201
|
+
const bgIsCheckable = isOpaqueColor(bg);
|
|
12611
13202
|
const series = [];
|
|
12612
13203
|
if (scales.color && scales.color.type !== "sequential") {
|
|
12613
13204
|
const ordinal = scales.color.scale;
|
|
@@ -12746,7 +13337,7 @@ function buildSparklineAreaGradient(baseColor) {
|
|
|
12746
13337
|
x2: 0,
|
|
12747
13338
|
y2: 1,
|
|
12748
13339
|
stops: [
|
|
12749
|
-
{ offset: 0, color: baseColor, opacity: 0.
|
|
13340
|
+
{ offset: 0, color: baseColor, opacity: 0.2 },
|
|
12750
13341
|
{ offset: 1, color: baseColor, opacity: 0 }
|
|
12751
13342
|
]
|
|
12752
13343
|
};
|
|
@@ -12756,7 +13347,7 @@ function buildSparklineAreaGradient(baseColor) {
|
|
|
12756
13347
|
import {
|
|
12757
13348
|
adaptTheme as adaptTheme2,
|
|
12758
13349
|
buildD3Formatter as buildD3Formatter2,
|
|
12759
|
-
CATEGORICAL_PALETTE,
|
|
13350
|
+
CATEGORICAL_PALETTE as CATEGORICAL_PALETTE2,
|
|
12760
13351
|
computeChrome as computeChrome3,
|
|
12761
13352
|
estimateTextWidth as estimateTextWidth18,
|
|
12762
13353
|
formatNumber as formatNumber3,
|
|
@@ -12900,9 +13491,75 @@ function excludePolarFeatures(geojson) {
|
|
|
12900
13491
|
if (filtered.length === 0) return geojson;
|
|
12901
13492
|
return { type: "FeatureCollection", features: filtered };
|
|
12902
13493
|
}
|
|
13494
|
+
function resolveDefaultProjection(topology) {
|
|
13495
|
+
const bbox = topologyBBox(topology);
|
|
13496
|
+
if (!bbox) return "albersUsa";
|
|
13497
|
+
const [minX, minY, maxX, maxY] = bbox;
|
|
13498
|
+
const preProjected = minX < -180.5 || maxX > 180.5 || minY < -90.5 || maxY > 90.5;
|
|
13499
|
+
if (preProjected) return "identity";
|
|
13500
|
+
if (maxX - minX > 200) return "equalEarth";
|
|
13501
|
+
return "albersUsa";
|
|
13502
|
+
}
|
|
13503
|
+
function topologyBBox(topology) {
|
|
13504
|
+
if (!topology || typeof topology !== "object") return null;
|
|
13505
|
+
const topo = topology;
|
|
13506
|
+
if (Array.isArray(topo.bbox) && topo.bbox.length >= 4) {
|
|
13507
|
+
const box = topo.bbox.slice(0, 4);
|
|
13508
|
+
if (box.every((n) => Number.isFinite(n))) {
|
|
13509
|
+
return [box[0], box[1], box[2], box[3]];
|
|
13510
|
+
}
|
|
13511
|
+
}
|
|
13512
|
+
if (!Array.isArray(topo.arcs) || topo.arcs.length === 0) return null;
|
|
13513
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
13514
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
13515
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
13516
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
13517
|
+
const quantized = !!topo.transform;
|
|
13518
|
+
for (const arc of topo.arcs) {
|
|
13519
|
+
let x = 0;
|
|
13520
|
+
let y = 0;
|
|
13521
|
+
for (const point of arc) {
|
|
13522
|
+
if (!Array.isArray(point) || point.length < 2) continue;
|
|
13523
|
+
if (quantized) {
|
|
13524
|
+
x += point[0];
|
|
13525
|
+
y += point[1];
|
|
13526
|
+
} else {
|
|
13527
|
+
x = point[0];
|
|
13528
|
+
y = point[1];
|
|
13529
|
+
}
|
|
13530
|
+
if (x < minX) minX = x;
|
|
13531
|
+
if (x > maxX) maxX = x;
|
|
13532
|
+
if (y < minY) minY = y;
|
|
13533
|
+
if (y > maxY) maxY = y;
|
|
13534
|
+
}
|
|
13535
|
+
}
|
|
13536
|
+
if (!Number.isFinite(minX) || !Number.isFinite(minY)) return null;
|
|
13537
|
+
if (topo.transform) {
|
|
13538
|
+
const { scale, translate } = topo.transform;
|
|
13539
|
+
return [
|
|
13540
|
+
minX * scale[0] + translate[0],
|
|
13541
|
+
minY * scale[1] + translate[1],
|
|
13542
|
+
maxX * scale[0] + translate[0],
|
|
13543
|
+
maxY * scale[1] + translate[1]
|
|
13544
|
+
];
|
|
13545
|
+
}
|
|
13546
|
+
return [minX, minY, maxX, maxY];
|
|
13547
|
+
}
|
|
12903
13548
|
|
|
12904
13549
|
// src/geo/compile-geo-map.ts
|
|
12905
13550
|
var OVERLAY_LEGEND_INSET = 18;
|
|
13551
|
+
var NO_DATA_SWATCH_GAP = 12;
|
|
13552
|
+
var NO_DATA_LABEL_GAP = 5;
|
|
13553
|
+
function legendTitleFor(colorEncoding) {
|
|
13554
|
+
const base = colorEncoding.title ?? colorEncoding.field;
|
|
13555
|
+
if (!base) return void 0;
|
|
13556
|
+
const format = colorEncoding.format;
|
|
13557
|
+
if (typeof format === "string") {
|
|
13558
|
+
if (format.includes("%")) return `${base} (%)`;
|
|
13559
|
+
if (format.includes("$")) return `${base} ($)`;
|
|
13560
|
+
}
|
|
13561
|
+
return base;
|
|
13562
|
+
}
|
|
12906
13563
|
function validateGeoFeatures(geo) {
|
|
12907
13564
|
if (!geo.features) {
|
|
12908
13565
|
throw new Error(
|
|
@@ -12984,8 +13641,10 @@ function compileGeoMap(spec, options) {
|
|
|
12984
13641
|
const legendPosition = mapSpec.legend?.position === "bottom" ? "bottom" : "top";
|
|
12985
13642
|
const pointLegendOverlay = mapSpec.legend?.position === "top-left";
|
|
12986
13643
|
let legendBlockHeight = 0;
|
|
13644
|
+
let legendTitle;
|
|
12987
13645
|
let continuousContent = null;
|
|
12988
|
-
|
|
13646
|
+
let classScale = null;
|
|
13647
|
+
if (colorEncoding && isQuantitative) {
|
|
12989
13648
|
const colorValues = [];
|
|
12990
13649
|
for (const row of mapSpec.data) {
|
|
12991
13650
|
const v = row[colorEncoding.field];
|
|
@@ -12996,19 +13655,28 @@ function compileGeoMap(spec, options) {
|
|
|
12996
13655
|
}
|
|
12997
13656
|
const legendChannel = {
|
|
12998
13657
|
...colorEncoding,
|
|
12999
|
-
scale: { ...colorEncoding.scale, type: "
|
|
13658
|
+
scale: { ...colorEncoding.scale, type: colorEncoding.scale?.type ?? "quantize" }
|
|
13000
13659
|
};
|
|
13001
|
-
|
|
13002
|
-
colorValues,
|
|
13003
|
-
|
|
13004
|
-
|
|
13005
|
-
|
|
13006
|
-
|
|
13660
|
+
if (colorValues.length > 0) {
|
|
13661
|
+
classScale = classScaleForChannel(colorValues, legendChannel, theme);
|
|
13662
|
+
}
|
|
13663
|
+
if (showChoroplethLegend) {
|
|
13664
|
+
continuousContent = computeContinuousLegendContentForChannel(
|
|
13665
|
+
colorValues,
|
|
13666
|
+
legendChannel,
|
|
13667
|
+
theme,
|
|
13668
|
+
fullArea.width,
|
|
13669
|
+
classScale ?? void 0
|
|
13670
|
+
);
|
|
13671
|
+
}
|
|
13007
13672
|
if (continuousContent) {
|
|
13673
|
+
legendTitle = legendTitleFor(colorEncoding);
|
|
13008
13674
|
const labelRowHeight = Math.ceil(theme.fonts.sizes.small * 1.3);
|
|
13009
|
-
|
|
13675
|
+
const titleRowHeight = legendTitle ? labelRowHeight : 0;
|
|
13676
|
+
legendBlockHeight = titleRowHeight + continuousContent.barHeight + CONTINUOUS_LABEL_GAP + labelRowHeight;
|
|
13010
13677
|
}
|
|
13011
|
-
}
|
|
13678
|
+
}
|
|
13679
|
+
if (legendBlockHeight === 0 && showChoroplethLegend && colorEncoding && !isQuantitative) {
|
|
13012
13680
|
const labelHeight = Math.ceil(theme.fonts.sizes.small * 1.3);
|
|
13013
13681
|
legendBlockHeight = Math.max(10, labelHeight) + 6;
|
|
13014
13682
|
}
|
|
@@ -13026,7 +13694,14 @@ function compileGeoMap(spec, options) {
|
|
|
13026
13694
|
if (mapAreaHeight <= 0) {
|
|
13027
13695
|
return emptyLayout3(chrome, theme, options, watermark);
|
|
13028
13696
|
}
|
|
13029
|
-
const projectionType = mapSpec.geo.projection;
|
|
13697
|
+
const projectionType = mapSpec.geo.projection ?? resolveDefaultProjection(topology);
|
|
13698
|
+
if (options.dev && !mapSpec.geo.projection && projectionType === "identity") {
|
|
13699
|
+
compileWarnings.push({
|
|
13700
|
+
code: "PROJECTION_INFERRED",
|
|
13701
|
+
message: 'No geo.projection was given and the topology looks pre-projected (its bounding box is outside longitude/latitude range), so projection: "identity" was inferred. Set geo.projection explicitly to silence this.',
|
|
13702
|
+
context: { projection: projectionType }
|
|
13703
|
+
});
|
|
13704
|
+
}
|
|
13030
13705
|
let pointInset = 0;
|
|
13031
13706
|
if (mapSpec.points) {
|
|
13032
13707
|
const sizeRange = mapSpec.points.size?.scale?.range;
|
|
@@ -13071,8 +13746,7 @@ function compileGeoMap(spec, options) {
|
|
|
13071
13746
|
}
|
|
13072
13747
|
const droppedFeatures = [];
|
|
13073
13748
|
const formatter = buildD3Formatter2(mapSpec.encoding?.color?.format ?? mapSpec.valueFormat) ?? formatNumber3;
|
|
13074
|
-
const neutralFill = isDarkMode ?
|
|
13075
|
-
const neutralStroke = isDarkMode ? "rgba(255,255,255,0.08)" : "rgba(0,0,0,0.06)";
|
|
13749
|
+
const neutralFill = isDarkMode ? theme.colors.neutral[800] : theme.colors.neutral[100];
|
|
13076
13750
|
let featureMarks;
|
|
13077
13751
|
let continuousLegend = null;
|
|
13078
13752
|
let categoricalLegend = null;
|
|
@@ -13081,7 +13755,6 @@ function compileGeoMap(spec, options) {
|
|
|
13081
13755
|
geoFeatures,
|
|
13082
13756
|
pathGen,
|
|
13083
13757
|
neutralFill,
|
|
13084
|
-
neutralStroke,
|
|
13085
13758
|
droppedFeatures
|
|
13086
13759
|
});
|
|
13087
13760
|
} else if (isQuantitative) {
|
|
@@ -13090,10 +13763,8 @@ function compileGeoMap(spec, options) {
|
|
|
13090
13763
|
pathGen,
|
|
13091
13764
|
joined,
|
|
13092
13765
|
colorField: colorEncoding.field,
|
|
13093
|
-
|
|
13094
|
-
isDarkMode: !!isDarkMode,
|
|
13766
|
+
classScale,
|
|
13095
13767
|
neutralFill,
|
|
13096
|
-
neutralStroke,
|
|
13097
13768
|
formatter,
|
|
13098
13769
|
droppedFeatures
|
|
13099
13770
|
});
|
|
@@ -13109,6 +13780,12 @@ function compileGeoMap(spec, options) {
|
|
|
13109
13780
|
};
|
|
13110
13781
|
const legendX = fullArea.x;
|
|
13111
13782
|
const legendY = legendPosition === "bottom" ? fullArea.y + mapAreaHeight + legendReserveGap : fullArea.y + legendReserveGap;
|
|
13783
|
+
const titleStyle = legendTitle ? {
|
|
13784
|
+
...labelStyle,
|
|
13785
|
+
fontWeight: theme.fonts.weights.medium,
|
|
13786
|
+
fill: theme.colors.neutral.secondary
|
|
13787
|
+
} : void 0;
|
|
13788
|
+
const titleRowHeight = legendTitle ? Math.ceil(theme.fonts.sizes.small * 1.3) : 0;
|
|
13112
13789
|
const bounds = {
|
|
13113
13790
|
x: legendX,
|
|
13114
13791
|
y: legendY,
|
|
@@ -13117,10 +13794,25 @@ function compileGeoMap(spec, options) {
|
|
|
13117
13794
|
};
|
|
13118
13795
|
const bar = {
|
|
13119
13796
|
x: bounds.x,
|
|
13120
|
-
y: bounds.y,
|
|
13797
|
+
y: bounds.y + titleRowHeight,
|
|
13121
13798
|
width: continuousContent.barWidth,
|
|
13122
13799
|
height: continuousContent.barHeight
|
|
13123
13800
|
};
|
|
13801
|
+
const labelY = bar.y + bar.height + CONTINUOUS_LABEL_GAP + labelStyle.fontSize;
|
|
13802
|
+
const hasUnmatched = featureMarks.some((m) => {
|
|
13803
|
+
const raw = m.data?.[colorEncoding.field];
|
|
13804
|
+
return raw == null || Number.isNaN(Number(raw));
|
|
13805
|
+
});
|
|
13806
|
+
const noDataX = bar.x + bar.width + NO_DATA_SWATCH_GAP;
|
|
13807
|
+
const noData = hasUnmatched && noDataX + bar.height + NO_DATA_LABEL_GAP < fullArea.x + fullArea.width ? {
|
|
13808
|
+
x: noDataX,
|
|
13809
|
+
y: bar.y,
|
|
13810
|
+
size: bar.height,
|
|
13811
|
+
fill: neutralFill,
|
|
13812
|
+
label: "No data",
|
|
13813
|
+
labelX: noDataX + bar.height + NO_DATA_LABEL_GAP,
|
|
13814
|
+
labelY: bar.y + bar.height
|
|
13815
|
+
} : void 0;
|
|
13124
13816
|
continuousLegend = {
|
|
13125
13817
|
type: "continuous",
|
|
13126
13818
|
mode: continuousContent.mode,
|
|
@@ -13131,7 +13823,11 @@ function compileGeoMap(spec, options) {
|
|
|
13131
13823
|
colorStops: continuousContent.colorStops,
|
|
13132
13824
|
bins: continuousContent.bins.map((b) => ({ ...b, x: b.x + bar.x })),
|
|
13133
13825
|
ticks: continuousContent.ticks.map((t) => ({ ...t, x: t.x + bar.x })),
|
|
13134
|
-
labelY
|
|
13826
|
+
labelY,
|
|
13827
|
+
title: legendTitle,
|
|
13828
|
+
titleStyle,
|
|
13829
|
+
titleY: legendTitle ? bounds.y + labelStyle.fontSize : void 0,
|
|
13830
|
+
noData
|
|
13135
13831
|
};
|
|
13136
13832
|
}
|
|
13137
13833
|
} else {
|
|
@@ -13143,7 +13839,6 @@ function compileGeoMap(spec, options) {
|
|
|
13143
13839
|
colorScale: colorEncoding.scale,
|
|
13144
13840
|
isDarkMode: !!isDarkMode,
|
|
13145
13841
|
neutralFill,
|
|
13146
|
-
neutralStroke,
|
|
13147
13842
|
theme,
|
|
13148
13843
|
fullArea,
|
|
13149
13844
|
mapAreaHeight,
|
|
@@ -13174,12 +13869,20 @@ function compileGeoMap(spec, options) {
|
|
|
13174
13869
|
);
|
|
13175
13870
|
const interiorPath = pathGen(interiorMesh) ?? "";
|
|
13176
13871
|
const outlinePath = pathGen(outlineMesh) ?? "";
|
|
13177
|
-
const interiorStroke = isDarkMode ? "rgba(255,255,255,0.
|
|
13178
|
-
const outlineStroke = isDarkMode ? "rgba(255,255,255,0.
|
|
13179
|
-
const borders = {
|
|
13872
|
+
const interiorStroke = isDarkMode ? "rgba(255,255,255,0.22)" : "rgba(0,0,0,0.25)";
|
|
13873
|
+
const outlineStroke = isDarkMode ? "rgba(255,255,255,0.45)" : "rgba(0,0,0,0.55)";
|
|
13874
|
+
const borders = {
|
|
13875
|
+
interiorPath,
|
|
13876
|
+
outlinePath,
|
|
13877
|
+
interiorStroke,
|
|
13878
|
+
outlineStroke,
|
|
13879
|
+
interiorWidth: 0.6,
|
|
13880
|
+
outlineWidth: 1.1
|
|
13881
|
+
};
|
|
13180
13882
|
const pointMarks = [];
|
|
13181
13883
|
let pointCategoricalLegend = null;
|
|
13182
13884
|
let pointContinuousLegend = null;
|
|
13885
|
+
let pointSizeLegend = null;
|
|
13183
13886
|
if (mapSpec.points) {
|
|
13184
13887
|
const pts = mapSpec.points;
|
|
13185
13888
|
const lonField = pts.longitude.field;
|
|
@@ -13205,7 +13908,7 @@ function compileGeoMap(spec, options) {
|
|
|
13205
13908
|
const qScale = scaleQuantile3().domain(colorValues).range(paletteStops);
|
|
13206
13909
|
pointColorScale = (val) => {
|
|
13207
13910
|
const v = Number(val);
|
|
13208
|
-
return Number.isFinite(v) ? qScale(v) :
|
|
13911
|
+
return Number.isFinite(v) ? qScale(v) : CATEGORICAL_PALETTE2[0];
|
|
13209
13912
|
};
|
|
13210
13913
|
}
|
|
13211
13914
|
} else {
|
|
@@ -13227,7 +13930,7 @@ function compileGeoMap(spec, options) {
|
|
|
13227
13930
|
}
|
|
13228
13931
|
}
|
|
13229
13932
|
}
|
|
13230
|
-
const colorSource = explicitRange && Array.isArray(explicitRange) ? explicitRange :
|
|
13933
|
+
const colorSource = explicitRange && Array.isArray(explicitRange) ? explicitRange : CATEGORICAL_PALETTE2;
|
|
13231
13934
|
for (let i = 0; i < pointCategories.length; i++) {
|
|
13232
13935
|
pointCategoryColors.set(pointCategories[i], colorSource[i % colorSource.length]);
|
|
13233
13936
|
}
|
|
@@ -13237,9 +13940,8 @@ function compileGeoMap(spec, options) {
|
|
|
13237
13940
|
};
|
|
13238
13941
|
}
|
|
13239
13942
|
}
|
|
13240
|
-
const defaultFill =
|
|
13241
|
-
const pointOpacity = pts.opacity ?? 0.
|
|
13242
|
-
let animIndex = 0;
|
|
13943
|
+
const defaultFill = CATEGORICAL_PALETTE2[0];
|
|
13944
|
+
const pointOpacity = pts.opacity ?? 0.85;
|
|
13243
13945
|
const droppedPoints = [];
|
|
13244
13946
|
for (let i = 0; i < pts.data.length; i++) {
|
|
13245
13947
|
const row = pts.data[i];
|
|
@@ -13254,7 +13956,7 @@ function compileGeoMap(spec, options) {
|
|
|
13254
13956
|
const r = sizeScale ? sizeScale.scale(Number(row[sizeScale.field])) : 5;
|
|
13255
13957
|
const fill = pointColorScale && pts.color ? pointColorScale(row[pts.color.field]) : defaultFill;
|
|
13256
13958
|
const key = keyChannel ? String(row[keyChannel.field] ?? i) : String(i);
|
|
13257
|
-
const stroke =
|
|
13959
|
+
const stroke = resolveKnockoutColor(theme);
|
|
13258
13960
|
pointMarks.push({
|
|
13259
13961
|
type: "map-point",
|
|
13260
13962
|
cx: projected[0],
|
|
@@ -13267,9 +13969,13 @@ function compileGeoMap(spec, options) {
|
|
|
13267
13969
|
key,
|
|
13268
13970
|
data: row,
|
|
13269
13971
|
aria: { role: "img", label: key },
|
|
13270
|
-
animationIndex:
|
|
13972
|
+
animationIndex: 0
|
|
13271
13973
|
});
|
|
13272
13974
|
}
|
|
13975
|
+
pointMarks.sort((a, b) => b.r - a.r);
|
|
13976
|
+
for (let i = 0; i < pointMarks.length; i++) {
|
|
13977
|
+
pointMarks[i].animationIndex = i;
|
|
13978
|
+
}
|
|
13273
13979
|
if (droppedPoints.length > 0) {
|
|
13274
13980
|
const sample = droppedPoints.slice(0, 3).map(([lo, la]) => `[${lo}, ${la}]`).join(", ");
|
|
13275
13981
|
compileWarnings.push({
|
|
@@ -13371,6 +14077,42 @@ function compileGeoMap(spec, options) {
|
|
|
13371
14077
|
};
|
|
13372
14078
|
}
|
|
13373
14079
|
}
|
|
14080
|
+
if (pts.size && showLegend) {
|
|
14081
|
+
const sizeRange = pts.size.scale?.range ?? [
|
|
14082
|
+
...SIZE_SCALE_DEFAULTS.mapPoint.range
|
|
14083
|
+
];
|
|
14084
|
+
const content = computeSizeLegendContent(pts.size, pts.data, theme, {
|
|
14085
|
+
curve: SIZE_SCALE_DEFAULTS.mapPoint.curve,
|
|
14086
|
+
range: sizeRange
|
|
14087
|
+
});
|
|
14088
|
+
if (content) {
|
|
14089
|
+
const mapTop = legendPosition === "top" && legendBlockHeight > 0 ? fullArea.y + legendBlockHeight + legendReserveGap : fullArea.y;
|
|
14090
|
+
pointSizeLegend = {
|
|
14091
|
+
type: "size",
|
|
14092
|
+
channel: "size",
|
|
14093
|
+
position: "bottom-right",
|
|
14094
|
+
bounds: {
|
|
14095
|
+
x: fullArea.x + fullArea.width - content.width - OVERLAY_LEGEND_INSET,
|
|
14096
|
+
y: mapTop + mapAreaHeight - content.height - OVERLAY_LEGEND_INSET,
|
|
14097
|
+
width: content.width,
|
|
14098
|
+
height: content.height
|
|
14099
|
+
},
|
|
14100
|
+
circles: content.circles,
|
|
14101
|
+
stroke: theme.colors.neutral[400],
|
|
14102
|
+
labelStyle: {
|
|
14103
|
+
fontFamily: theme.fonts.family,
|
|
14104
|
+
fontSize: theme.fonts.sizes.small,
|
|
14105
|
+
fontWeight: theme.fonts.weights.normal,
|
|
14106
|
+
fill: theme.colors.neutral.secondary,
|
|
14107
|
+
lineHeight: 1.2,
|
|
14108
|
+
fontVariant: "tabular-nums"
|
|
14109
|
+
},
|
|
14110
|
+
title: content.title,
|
|
14111
|
+
titleStyle: content.title ? sizeLegendTitleStyle(theme) : void 0,
|
|
14112
|
+
titleY: content.titleY
|
|
14113
|
+
};
|
|
14114
|
+
}
|
|
14115
|
+
}
|
|
13374
14116
|
}
|
|
13375
14117
|
const tooltipChannels = mapSpec.encoding.tooltip ? Array.isArray(mapSpec.encoding.tooltip) ? mapSpec.encoding.tooltip : [mapSpec.encoding.tooltip] : null;
|
|
13376
14118
|
const tooltipDescriptors = /* @__PURE__ */ new Map();
|
|
@@ -13484,6 +14226,7 @@ function compileGeoMap(spec, options) {
|
|
|
13484
14226
|
pointMarks,
|
|
13485
14227
|
pointCategoricalLegend,
|
|
13486
14228
|
pointContinuousLegend,
|
|
14229
|
+
pointSizeLegend,
|
|
13487
14230
|
tooltipDescriptors,
|
|
13488
14231
|
a11y,
|
|
13489
14232
|
theme,
|
|
@@ -13498,7 +14241,7 @@ function compileGeoMap(spec, options) {
|
|
|
13498
14241
|
};
|
|
13499
14242
|
}
|
|
13500
14243
|
function buildBasemapMarks(opts) {
|
|
13501
|
-
const { geoFeatures, pathGen, neutralFill,
|
|
14244
|
+
const { geoFeatures, pathGen, neutralFill, droppedFeatures } = opts;
|
|
13502
14245
|
const marks = [];
|
|
13503
14246
|
let animIndex = 0;
|
|
13504
14247
|
for (const feat of geoFeatures) {
|
|
@@ -13524,8 +14267,10 @@ function buildBasemapMarks(opts) {
|
|
|
13524
14267
|
type: "map-feature",
|
|
13525
14268
|
path: pathD,
|
|
13526
14269
|
fill: neutralFill,
|
|
13527
|
-
stroke
|
|
13528
|
-
|
|
14270
|
+
// The border meshes draw every line on this map. A per-feature stroke on
|
|
14271
|
+
// top of them double-draws each shared edge into a visible wash.
|
|
14272
|
+
stroke: "none",
|
|
14273
|
+
strokeWidth: 0,
|
|
13529
14274
|
id: featureId,
|
|
13530
14275
|
name,
|
|
13531
14276
|
data: null,
|
|
@@ -13543,23 +14288,11 @@ function buildQuantitativeMarks(opts) {
|
|
|
13543
14288
|
pathGen,
|
|
13544
14289
|
joined,
|
|
13545
14290
|
colorField,
|
|
13546
|
-
|
|
13547
|
-
isDarkMode,
|
|
14291
|
+
classScale,
|
|
13548
14292
|
neutralFill,
|
|
13549
|
-
neutralStroke,
|
|
13550
14293
|
formatter,
|
|
13551
14294
|
droppedFeatures
|
|
13552
14295
|
} = opts;
|
|
13553
|
-
const values = [];
|
|
13554
|
-
for (const row of joined.values()) {
|
|
13555
|
-
const raw = row[colorField];
|
|
13556
|
-
if (raw != null) {
|
|
13557
|
-
const v = Number(raw);
|
|
13558
|
-
if (!Number.isNaN(v)) values.push(v);
|
|
13559
|
-
}
|
|
13560
|
-
}
|
|
13561
|
-
const paletteStops = [...SEQUENTIAL_PALETTES2[palette] ?? SEQUENTIAL_PALETTES2.blue];
|
|
13562
|
-
const colorScale = scaleQuantile3().domain(values).range(paletteStops);
|
|
13563
14296
|
const marks = [];
|
|
13564
14297
|
let animIndex = 0;
|
|
13565
14298
|
for (const feat of geoFeatures) {
|
|
@@ -13573,16 +14306,16 @@ function buildQuantitativeMarks(opts) {
|
|
|
13573
14306
|
const dataRow = joined.get(featureId) ?? null;
|
|
13574
14307
|
const hasData = dataRow !== null;
|
|
13575
14308
|
let fill = neutralFill;
|
|
13576
|
-
let
|
|
13577
|
-
if (hasData) {
|
|
14309
|
+
let binIndex;
|
|
14310
|
+
if (hasData && classScale) {
|
|
13578
14311
|
const raw = dataRow[colorField];
|
|
13579
14312
|
if (raw != null) {
|
|
13580
14313
|
const v = Number(raw);
|
|
13581
14314
|
if (!Number.isNaN(v)) {
|
|
13582
|
-
fill =
|
|
14315
|
+
fill = classScale.color(v);
|
|
14316
|
+
binIndex = classScale.binIndex(v);
|
|
13583
14317
|
}
|
|
13584
14318
|
}
|
|
13585
|
-
stroke = isDarkMode ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)";
|
|
13586
14319
|
}
|
|
13587
14320
|
const props = feat.properties;
|
|
13588
14321
|
const name = props?.name ?? props?.NAME;
|
|
@@ -13599,8 +14332,8 @@ function buildQuantitativeMarks(opts) {
|
|
|
13599
14332
|
type: "map-feature",
|
|
13600
14333
|
path: pathD,
|
|
13601
14334
|
fill,
|
|
13602
|
-
stroke,
|
|
13603
|
-
strokeWidth: 0
|
|
14335
|
+
stroke: "none",
|
|
14336
|
+
strokeWidth: 0,
|
|
13604
14337
|
id: featureId,
|
|
13605
14338
|
name,
|
|
13606
14339
|
data: dataRow,
|
|
@@ -13610,7 +14343,8 @@ function buildQuantitativeMarks(opts) {
|
|
|
13610
14343
|
},
|
|
13611
14344
|
animationIndex: animIndex++,
|
|
13612
14345
|
bounds,
|
|
13613
|
-
centroid: [cx, cy]
|
|
14346
|
+
centroid: [cx, cy],
|
|
14347
|
+
binIndex
|
|
13614
14348
|
});
|
|
13615
14349
|
}
|
|
13616
14350
|
return { marks };
|
|
@@ -13624,7 +14358,6 @@ function buildCategoricalMarks(opts) {
|
|
|
13624
14358
|
colorScale: scaleConfig,
|
|
13625
14359
|
isDarkMode,
|
|
13626
14360
|
neutralFill,
|
|
13627
|
-
neutralStroke,
|
|
13628
14361
|
theme,
|
|
13629
14362
|
fullArea,
|
|
13630
14363
|
mapAreaHeight,
|
|
@@ -13652,7 +14385,7 @@ function buildCategoricalMarks(opts) {
|
|
|
13652
14385
|
}
|
|
13653
14386
|
}
|
|
13654
14387
|
const explicitRange = scaleConfig?.range;
|
|
13655
|
-
const colorSource = explicitRange && Array.isArray(explicitRange) ? explicitRange :
|
|
14388
|
+
const colorSource = explicitRange && Array.isArray(explicitRange) ? explicitRange : CATEGORICAL_PALETTE2;
|
|
13656
14389
|
const categoryColors = /* @__PURE__ */ new Map();
|
|
13657
14390
|
for (let i = 0; i < categories.length; i++) {
|
|
13658
14391
|
categoryColors.set(categories[i], colorSource[i % colorSource.length]);
|
|
@@ -13670,14 +14403,12 @@ function buildCategoricalMarks(opts) {
|
|
|
13670
14403
|
const dataRow = joined.get(featureId) ?? null;
|
|
13671
14404
|
const hasData = dataRow !== null;
|
|
13672
14405
|
let fill = neutralFill;
|
|
13673
|
-
let stroke = neutralStroke;
|
|
13674
14406
|
if (hasData) {
|
|
13675
14407
|
const raw = dataRow[colorField];
|
|
13676
14408
|
if (raw != null) {
|
|
13677
14409
|
const cat = String(raw);
|
|
13678
14410
|
fill = categoryColors.get(cat) ?? neutralFill;
|
|
13679
14411
|
}
|
|
13680
|
-
stroke = isDarkMode ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)";
|
|
13681
14412
|
}
|
|
13682
14413
|
const props = feat.properties;
|
|
13683
14414
|
const name = props?.name ?? props?.NAME;
|
|
@@ -13694,8 +14425,8 @@ function buildCategoricalMarks(opts) {
|
|
|
13694
14425
|
type: "map-feature",
|
|
13695
14426
|
path: pathD,
|
|
13696
14427
|
fill,
|
|
13697
|
-
stroke,
|
|
13698
|
-
strokeWidth: 0
|
|
14428
|
+
stroke: "none",
|
|
14429
|
+
strokeWidth: 0,
|
|
13699
14430
|
id: featureId,
|
|
13700
14431
|
name,
|
|
13701
14432
|
data: dataRow,
|
|
@@ -13830,14 +14561,17 @@ function emptyLayout3(chrome, theme, options, watermark) {
|
|
|
13830
14561
|
borders: {
|
|
13831
14562
|
interiorPath: "",
|
|
13832
14563
|
outlinePath: "",
|
|
13833
|
-
interiorStroke: "rgba(0,0,0,0.
|
|
13834
|
-
outlineStroke: "rgba(0,0,0,0.
|
|
14564
|
+
interiorStroke: "rgba(0,0,0,0.25)",
|
|
14565
|
+
outlineStroke: "rgba(0,0,0,0.55)",
|
|
14566
|
+
interiorWidth: 0.6,
|
|
14567
|
+
outlineWidth: 1.1
|
|
13835
14568
|
},
|
|
13836
14569
|
continuousLegend: null,
|
|
13837
14570
|
categoricalLegend: null,
|
|
13838
14571
|
pointMarks: [],
|
|
13839
14572
|
pointCategoricalLegend: null,
|
|
13840
14573
|
pointContinuousLegend: null,
|
|
14574
|
+
pointSizeLegend: null,
|
|
13841
14575
|
tooltipDescriptors: /* @__PURE__ */ new Map(),
|
|
13842
14576
|
a11y: {
|
|
13843
14577
|
altText: "Empty map",
|
|
@@ -13926,6 +14660,7 @@ function resolveCamera(phase) {
|
|
|
13926
14660
|
|
|
13927
14661
|
// src/graphs/encoding.ts
|
|
13928
14662
|
import { max as max3, min as min3 } from "d3-array";
|
|
14663
|
+
import { interpolateRgb } from "d3-interpolate";
|
|
13929
14664
|
import { scaleLinear as scaleLinear5, scaleOrdinal as scaleOrdinal2, scaleSqrt as scaleSqrt4 } from "d3-scale";
|
|
13930
14665
|
var DEFAULT_NODE_RADIUS = 5;
|
|
13931
14666
|
var MIN_NODE_RADIUS = 3;
|
|
@@ -13933,7 +14668,7 @@ var MAX_NODE_RADIUS = 12;
|
|
|
13933
14668
|
var DEFAULT_EDGE_WIDTH = 1;
|
|
13934
14669
|
var MIN_EDGE_WIDTH = 0.5;
|
|
13935
14670
|
var MAX_EDGE_WIDTH = 4;
|
|
13936
|
-
var
|
|
14671
|
+
var DEFAULT_STROKE_WIDTH = 1.5;
|
|
13937
14672
|
function darkenColor(hex, amount = 0.2) {
|
|
13938
14673
|
const clean = hex.replace(/^#/, "");
|
|
13939
14674
|
if (clean.length !== 6 && clean.length !== 3) return hex;
|
|
@@ -13987,6 +14722,9 @@ function resolveCategoricalDomain(values, sort, scaleDomain) {
|
|
|
13987
14722
|
const sorted = [...seen].sort((a, b) => a.localeCompare(b));
|
|
13988
14723
|
return sort === "descending" ? sorted.reverse() : sorted;
|
|
13989
14724
|
}
|
|
14725
|
+
function resolveGraphSurface(theme) {
|
|
14726
|
+
return theme.colors.neutral.surface;
|
|
14727
|
+
}
|
|
13990
14728
|
function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
|
|
13991
14729
|
const degrees = computeDegrees(nodes, edges);
|
|
13992
14730
|
const maxDegree = Math.max(1, ...degrees.values());
|
|
@@ -14032,6 +14770,7 @@ function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
|
|
|
14032
14770
|
}
|
|
14033
14771
|
}
|
|
14034
14772
|
const defaultColor = theme.colors.categorical[0];
|
|
14773
|
+
const surface = resolveGraphSurface(theme);
|
|
14035
14774
|
let opacityScale;
|
|
14036
14775
|
if (encoding.nodeOpacity?.field) {
|
|
14037
14776
|
const field = encoding.nodeOpacity.field;
|
|
@@ -14070,7 +14809,7 @@ function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
|
|
|
14070
14809
|
}
|
|
14071
14810
|
}
|
|
14072
14811
|
const fill = colorFn ? colorFn(node) : defaultColor;
|
|
14073
|
-
const stroke =
|
|
14812
|
+
const stroke = surface;
|
|
14074
14813
|
let label;
|
|
14075
14814
|
if (encoding.nodeLabel?.field) {
|
|
14076
14815
|
const labelVal = node[encoding.nodeLabel.field];
|
|
@@ -14091,7 +14830,7 @@ function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
|
|
|
14091
14830
|
const override = nodeOverrides?.[node.id];
|
|
14092
14831
|
const finalFill = override?.fill ?? fill;
|
|
14093
14832
|
const finalRadius = override?.radius ?? radius;
|
|
14094
|
-
const finalStrokeWidth = override?.strokeWidth ??
|
|
14833
|
+
const finalStrokeWidth = override?.strokeWidth ?? DEFAULT_STROKE_WIDTH;
|
|
14095
14834
|
const finalStroke = override?.stroke ?? stroke;
|
|
14096
14835
|
const finalLabelPriority = override?.alwaysShowLabel ? Infinity : labelPriority;
|
|
14097
14836
|
return {
|
|
@@ -14108,7 +14847,7 @@ function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
|
|
|
14108
14847
|
};
|
|
14109
14848
|
});
|
|
14110
14849
|
}
|
|
14111
|
-
function resolveEdgeVisuals(edges, encoding, theme) {
|
|
14850
|
+
function resolveEdgeVisuals(edges, encoding, theme, nodeFills) {
|
|
14112
14851
|
let widthScale;
|
|
14113
14852
|
if (encoding.edgeWidth?.field) {
|
|
14114
14853
|
const field = encoding.edgeWidth.field;
|
|
@@ -14146,7 +14885,19 @@ function resolveEdgeVisuals(edges, encoding, theme) {
|
|
|
14146
14885
|
edgeColorFn = (edge) => ordinalScale(String(edge[field] ?? ""));
|
|
14147
14886
|
}
|
|
14148
14887
|
}
|
|
14149
|
-
const
|
|
14888
|
+
const fallbackEdgeColor = hexWithOpacity(theme.colors.axis, 0.4);
|
|
14889
|
+
const defaultEdgeColorFor = (source, target) => {
|
|
14890
|
+
if (!nodeFills) return fallbackEdgeColor;
|
|
14891
|
+
const a = nodeFills.get(source);
|
|
14892
|
+
const b = nodeFills.get(target);
|
|
14893
|
+
if (!a || !b) return fallbackEdgeColor;
|
|
14894
|
+
if (a === b) return a;
|
|
14895
|
+
try {
|
|
14896
|
+
return interpolateRgb(a, b)(0.5);
|
|
14897
|
+
} catch {
|
|
14898
|
+
return fallbackEdgeColor;
|
|
14899
|
+
}
|
|
14900
|
+
};
|
|
14150
14901
|
const EDGE_STYLES = ["solid", "dashed", "dotted"];
|
|
14151
14902
|
let styleFn;
|
|
14152
14903
|
if (encoding.edgeStyle?.field) {
|
|
@@ -14171,7 +14922,7 @@ function resolveEdgeVisuals(edges, encoding, theme) {
|
|
|
14171
14922
|
strokeWidth = widthScale(val);
|
|
14172
14923
|
}
|
|
14173
14924
|
}
|
|
14174
|
-
const stroke = edgeColorFn ? edgeColorFn(edge) :
|
|
14925
|
+
const stroke = edgeColorFn ? edgeColorFn(edge) : defaultEdgeColorFor(String(source), String(target));
|
|
14175
14926
|
const style = styleFn ? styleFn(edge) : "solid";
|
|
14176
14927
|
return {
|
|
14177
14928
|
source,
|
|
@@ -14217,7 +14968,7 @@ function applyCommunityColors(nodes, colorMap) {
|
|
|
14217
14968
|
}
|
|
14218
14969
|
|
|
14219
14970
|
// src/graphs/interaction.ts
|
|
14220
|
-
var DEFAULT_DIM_OPACITY = 0.
|
|
14971
|
+
var DEFAULT_DIM_OPACITY = 0.3;
|
|
14221
14972
|
var DEFAULT_CURSOR_RADIUS = 80;
|
|
14222
14973
|
var DEFAULT_CURSOR_STRENGTH = 30;
|
|
14223
14974
|
function resolveGraphInteraction(cfg) {
|
|
@@ -14440,7 +15191,8 @@ function compileGraph(spec, options) {
|
|
|
14440
15191
|
communityColorMap = buildCommunityColorMap(compiledNodes, theme);
|
|
14441
15192
|
applyCommunityColors(compiledNodes, communityColorMap);
|
|
14442
15193
|
}
|
|
14443
|
-
const
|
|
15194
|
+
const nodeFills = new Map(compiledNodes.map((n) => [n.id, n.fill]));
|
|
15195
|
+
const compiledEdges = resolveEdgeVisuals(graphSpec.edges, graphSpec.encoding, theme, nodeFills);
|
|
14444
15196
|
const useCommunitiesForLegend = hasCommunities && !hasNodeColorEncoding;
|
|
14445
15197
|
const nodeColorField = graphSpec.encoding.nodeColor?.field;
|
|
14446
15198
|
const legendOrder = nodeColorField ? resolveCategoricalDomain(
|
|
@@ -15154,6 +15906,7 @@ function computeGridlines(axes, chartArea, showVertical = false) {
|
|
|
15154
15906
|
// src/sankey/compile-sankey.ts
|
|
15155
15907
|
import {
|
|
15156
15908
|
adaptTheme as adaptTheme4,
|
|
15909
|
+
buildD3Formatter as buildD3Formatter3,
|
|
15157
15910
|
computeChrome as computeChrome6,
|
|
15158
15911
|
defaultNumberFormatter as defaultNumberFormatter10,
|
|
15159
15912
|
estimateTextWidth as estimateTextWidth21,
|
|
@@ -15231,8 +15984,11 @@ function generateLinkPath(link) {
|
|
|
15231
15984
|
// src/sankey/compile-sankey.ts
|
|
15232
15985
|
var LABEL_GAP3 = 6;
|
|
15233
15986
|
var LINK_OPACITY_LIGHT = 0.5;
|
|
15234
|
-
var LINK_OPACITY_DARK = 0.
|
|
15987
|
+
var LINK_OPACITY_DARK = 0.6;
|
|
15235
15988
|
var NODE_CORNER_RADIUS = 2;
|
|
15989
|
+
var VALUE_GAP = 5;
|
|
15990
|
+
var MAX_LABEL_GUTTER = 0.35;
|
|
15991
|
+
var OTHER_ID_PREFIX = "__oc_other_";
|
|
15236
15992
|
function pickColor(palette, index) {
|
|
15237
15993
|
return palette[index % palette.length];
|
|
15238
15994
|
}
|
|
@@ -15275,20 +16031,66 @@ function getLinkColors(linkStyle, sourceColor, targetColor, neutralColor) {
|
|
|
15275
16031
|
return { sourceColor, targetColor };
|
|
15276
16032
|
}
|
|
15277
16033
|
}
|
|
16034
|
+
function resolveOtherConfig(other) {
|
|
16035
|
+
if (other == null) return null;
|
|
16036
|
+
const threshold = typeof other === "number" ? other : other.threshold;
|
|
16037
|
+
if (!Number.isFinite(threshold) || threshold <= 0) return null;
|
|
16038
|
+
const label = (typeof other === "object" ? other.label : void 0) ?? "Other";
|
|
16039
|
+
return { threshold, label };
|
|
16040
|
+
}
|
|
16041
|
+
function bucketSmallNodes(nodes, cfg) {
|
|
16042
|
+
const byDepth = /* @__PURE__ */ new Map();
|
|
16043
|
+
for (const node of nodes) {
|
|
16044
|
+
const depth = node.depth ?? 0;
|
|
16045
|
+
const group = byDepth.get(depth);
|
|
16046
|
+
if (group) group.push(node);
|
|
16047
|
+
else byDepth.set(depth, [node]);
|
|
16048
|
+
}
|
|
16049
|
+
const merged = /* @__PURE__ */ new Map();
|
|
16050
|
+
for (const [depth, group] of byDepth) {
|
|
16051
|
+
const total = group.reduce((sum, n) => sum + (n.value ?? 0), 0);
|
|
16052
|
+
if (total <= 0) continue;
|
|
16053
|
+
const small = group.filter((n) => (n.value ?? 0) / total < cfg.threshold);
|
|
16054
|
+
if (small.length < 2) continue;
|
|
16055
|
+
for (const node of small) merged.set(node.id, `${OTHER_ID_PREFIX}${depth}`);
|
|
16056
|
+
}
|
|
16057
|
+
return merged;
|
|
16058
|
+
}
|
|
16059
|
+
function rewriteRowsForOther(data, sourceField, targetField, valueField, merged) {
|
|
16060
|
+
const out = [];
|
|
16061
|
+
const index = /* @__PURE__ */ new Map();
|
|
16062
|
+
for (const row of data) {
|
|
16063
|
+
const src = String(row[sourceField]);
|
|
16064
|
+
const tgt = String(row[targetField]);
|
|
16065
|
+
const newSrc = merged.get(src) ?? src;
|
|
16066
|
+
const newTgt = merged.get(tgt) ?? tgt;
|
|
16067
|
+
if (newSrc === newTgt) continue;
|
|
16068
|
+
const key = `${newSrc}\0${newTgt}`;
|
|
16069
|
+
const value = Number(row[valueField]) || 0;
|
|
16070
|
+
const existing = index.get(key);
|
|
16071
|
+
if (existing) {
|
|
16072
|
+
existing[valueField] = (Number(existing[valueField]) || 0) + value;
|
|
16073
|
+
continue;
|
|
16074
|
+
}
|
|
16075
|
+
const next = { ...row, [sourceField]: newSrc, [targetField]: newTgt, [valueField]: value };
|
|
16076
|
+
index.set(key, next);
|
|
16077
|
+
out.push(next);
|
|
16078
|
+
}
|
|
16079
|
+
return out;
|
|
16080
|
+
}
|
|
16081
|
+
function labelsLeftForDepth(depth, maxDepth, nodeLabelAlign) {
|
|
16082
|
+
if (nodeLabelAlign === "left") return true;
|
|
16083
|
+
if (nodeLabelAlign === "right") return false;
|
|
16084
|
+
if (depth === 0 && maxDepth > 0) return true;
|
|
16085
|
+
return false;
|
|
16086
|
+
}
|
|
15278
16087
|
function computeNodeLabel(node, maxDepth, theme, nodeWidth, nodeLabelAlign = "auto", containerWidth, padding) {
|
|
15279
16088
|
const depth = node.depth ?? 0;
|
|
15280
|
-
|
|
15281
|
-
if (nodeLabelAlign === "left") {
|
|
15282
|
-
placeLeft = true;
|
|
15283
|
-
} else if (nodeLabelAlign === "right") {
|
|
15284
|
-
placeLeft = false;
|
|
15285
|
-
} else {
|
|
15286
|
-
placeLeft = depth === maxDepth;
|
|
15287
|
-
}
|
|
16089
|
+
const placeLeft = labelsLeftForDepth(depth, maxDepth, nodeLabelAlign);
|
|
15288
16090
|
const style = {
|
|
15289
16091
|
fontFamily: theme.fonts.family,
|
|
15290
16092
|
fontSize: theme.fonts.sizes.small,
|
|
15291
|
-
fontWeight: theme.fonts.weights.
|
|
16093
|
+
fontWeight: theme.fonts.weights.medium,
|
|
15292
16094
|
fill: theme.colors.text,
|
|
15293
16095
|
lineHeight: 1.3
|
|
15294
16096
|
};
|
|
@@ -15421,68 +16223,134 @@ function compileSankey(spec, options) {
|
|
|
15421
16223
|
return emptyLayout4(area3, chrome, theme, { ...options, height: grownHeight }, watermark);
|
|
15422
16224
|
}
|
|
15423
16225
|
const labelFontSize = theme.fonts.sizes.small;
|
|
15424
|
-
const labelFontWeight = theme.fonts.weights.
|
|
15425
|
-
const
|
|
15426
|
-
|
|
15427
|
-
let
|
|
15428
|
-
|
|
16226
|
+
const labelFontWeight = theme.fonts.weights.medium;
|
|
16227
|
+
const valueFontWeight = theme.fonts.weights.normal;
|
|
16228
|
+
const nodeLabelAlign = sankeySpec.nodeLabelAlign ?? "auto";
|
|
16229
|
+
let workingData = sankeySpec.data;
|
|
16230
|
+
const runLayout = (rect) => computeSankeyLayout(
|
|
16231
|
+
workingData,
|
|
15429
16232
|
sourceField,
|
|
15430
16233
|
targetField,
|
|
15431
16234
|
valueField,
|
|
15432
|
-
|
|
16235
|
+
rect,
|
|
15433
16236
|
sankeySpec.nodeWidth,
|
|
15434
16237
|
sankeySpec.nodePadding,
|
|
15435
16238
|
sankeySpec.nodeAlign,
|
|
15436
16239
|
sankeySpec.iterations,
|
|
15437
16240
|
sankeySpec.nodeSort
|
|
15438
16241
|
);
|
|
15439
|
-
|
|
15440
|
-
|
|
15441
|
-
const
|
|
15442
|
-
|
|
15443
|
-
|
|
15444
|
-
|
|
15445
|
-
const
|
|
15446
|
-
if (
|
|
15447
|
-
|
|
15448
|
-
|
|
15449
|
-
|
|
15450
|
-
|
|
15451
|
-
|
|
15452
|
-
|
|
15453
|
-
|
|
15454
|
-
|
|
16242
|
+
let layoutArea = { ...area3 };
|
|
16243
|
+
let { nodes, links } = runLayout(layoutArea);
|
|
16244
|
+
const otherConfig = resolveOtherConfig(sankeySpec.other);
|
|
16245
|
+
const otherLabels = /* @__PURE__ */ new Map();
|
|
16246
|
+
const otherMembers = /* @__PURE__ */ new Map();
|
|
16247
|
+
if (otherConfig) {
|
|
16248
|
+
const merged = bucketSmallNodes(nodes, otherConfig);
|
|
16249
|
+
if (merged.size > 0) {
|
|
16250
|
+
for (const [nodeId, otherId] of merged) {
|
|
16251
|
+
otherLabels.set(otherId, otherConfig.label);
|
|
16252
|
+
const members = otherMembers.get(otherId);
|
|
16253
|
+
if (members) members.push(nodeId);
|
|
16254
|
+
else otherMembers.set(otherId, [nodeId]);
|
|
16255
|
+
}
|
|
16256
|
+
workingData = rewriteRowsForOther(workingData, sourceField, targetField, valueField, merged);
|
|
16257
|
+
({ nodes, links } = runLayout(layoutArea));
|
|
16258
|
+
}
|
|
16259
|
+
}
|
|
16260
|
+
const applyOtherLabels = (list) => {
|
|
16261
|
+
if (otherLabels.size === 0) return;
|
|
16262
|
+
for (const node of list) {
|
|
16263
|
+
const label = otherLabels.get(node.id);
|
|
16264
|
+
if (label) node.label = label;
|
|
16265
|
+
}
|
|
16266
|
+
};
|
|
16267
|
+
applyOtherLabels(nodes);
|
|
16268
|
+
const nodeFmt = resolveNodeValueFormatter(nodes, resolvedValueFormat, flowFmt);
|
|
16269
|
+
const labelBlockWidth = (node) => {
|
|
16270
|
+
const name = node.label ?? node.id;
|
|
16271
|
+
const nameWidth = estimateTextWidth21(name, labelFontSize, labelFontWeight);
|
|
16272
|
+
const valueWidth = estimateTextWidth21(
|
|
16273
|
+
formatFlowValue(node.value ?? 0, nodeFmt),
|
|
16274
|
+
labelFontSize,
|
|
16275
|
+
valueFontWeight
|
|
16276
|
+
);
|
|
16277
|
+
return nameWidth + VALUE_GAP + valueWidth;
|
|
16278
|
+
};
|
|
16279
|
+
const measureGutters = (list) => {
|
|
16280
|
+
const maxDepth2 = list.reduce((max4, n) => Math.max(max4, n.depth ?? 0), 0);
|
|
16281
|
+
const cap = area3.width * MAX_LABEL_GUTTER;
|
|
16282
|
+
let left = 0;
|
|
16283
|
+
let right = 0;
|
|
16284
|
+
for (const node of list) {
|
|
16285
|
+
const depth = node.depth ?? 0;
|
|
16286
|
+
const width = labelBlockWidth(node) + LABEL_GAP3;
|
|
16287
|
+
if (labelsLeftForDepth(depth, maxDepth2, nodeLabelAlign)) {
|
|
16288
|
+
const need = width - ((node.x0 ?? 0) - area3.x);
|
|
16289
|
+
if (need > left) left = need;
|
|
16290
|
+
} else {
|
|
16291
|
+
const need = width - (area3.x + area3.width - (node.x1 ?? 0));
|
|
16292
|
+
if (need > right) right = need;
|
|
16293
|
+
}
|
|
16294
|
+
}
|
|
16295
|
+
return {
|
|
16296
|
+
left: Math.min(Math.max(left, 0), cap),
|
|
16297
|
+
right: Math.min(Math.max(right, 0), cap)
|
|
16298
|
+
};
|
|
16299
|
+
};
|
|
16300
|
+
const gutters = measureGutters(nodes);
|
|
16301
|
+
if (gutters.left > 0 || gutters.right > 0) {
|
|
16302
|
+
const left = Math.ceil(gutters.left);
|
|
16303
|
+
const right = Math.ceil(gutters.right);
|
|
15455
16304
|
layoutArea = {
|
|
15456
|
-
x: area3.x,
|
|
16305
|
+
x: area3.x + left,
|
|
15457
16306
|
y: area3.y,
|
|
15458
|
-
width: Math.max(area3.width -
|
|
16307
|
+
width: Math.max(area3.width - left - right, 40),
|
|
15459
16308
|
height: area3.height
|
|
15460
16309
|
};
|
|
15461
|
-
({ nodes, links } =
|
|
15462
|
-
|
|
15463
|
-
sourceField,
|
|
15464
|
-
targetField,
|
|
15465
|
-
valueField,
|
|
15466
|
-
layoutArea,
|
|
15467
|
-
sankeySpec.nodeWidth,
|
|
15468
|
-
sankeySpec.nodePadding,
|
|
15469
|
-
sankeySpec.nodeAlign,
|
|
15470
|
-
sankeySpec.iterations,
|
|
15471
|
-
sankeySpec.nodeSort
|
|
15472
|
-
));
|
|
16310
|
+
({ nodes, links } = runLayout(layoutArea));
|
|
16311
|
+
applyOtherLabels(nodes);
|
|
15473
16312
|
}
|
|
15474
16313
|
const nodeColorMap = buildNodeColorMap(
|
|
15475
16314
|
nodes,
|
|
15476
16315
|
theme.colors.categorical,
|
|
15477
16316
|
colorField,
|
|
15478
|
-
|
|
16317
|
+
workingData,
|
|
15479
16318
|
sourceField,
|
|
15480
16319
|
targetField
|
|
15481
16320
|
);
|
|
16321
|
+
for (const otherId of otherMembers.keys()) {
|
|
16322
|
+
nodeColorMap.set(otherId, theme.colors.neutral[300]);
|
|
16323
|
+
}
|
|
15482
16324
|
const maxDepth = nodes.reduce((max4, n) => Math.max(max4, n.depth ?? 0), 0);
|
|
16325
|
+
const valueStyle = {
|
|
16326
|
+
fontFamily: theme.fonts.family,
|
|
16327
|
+
fontSize: theme.fonts.sizes.small,
|
|
16328
|
+
fontWeight: theme.fonts.weights.normal,
|
|
16329
|
+
fill: theme.colors.axis,
|
|
16330
|
+
fontVariant: "tabular-nums",
|
|
16331
|
+
lineHeight: 1.3
|
|
16332
|
+
};
|
|
16333
|
+
const columnX0 = /* @__PURE__ */ new Map();
|
|
16334
|
+
for (const node of nodes) {
|
|
16335
|
+
const d = node.depth ?? 0;
|
|
16336
|
+
const x0 = node.x0 ?? 0;
|
|
16337
|
+
const current = columnX0.get(d);
|
|
16338
|
+
if (current === void 0 || x0 < current) columnX0.set(d, x0);
|
|
16339
|
+
}
|
|
16340
|
+
const valueLabelFor = (node, depth, text) => {
|
|
16341
|
+
const placedLeft = labelsLeftForDepth(depth, maxDepth, nodeLabelAlign);
|
|
16342
|
+
if (placedLeft || depth === maxDepth) return { text, style: valueStyle };
|
|
16343
|
+
const nextColumn = columnX0.get(depth + 1);
|
|
16344
|
+
if (nextColumn === void 0) return { text, style: valueStyle };
|
|
16345
|
+
const available = nextColumn - LABEL_GAP3 - ((node.x1 ?? 0) + LABEL_GAP3);
|
|
16346
|
+
const needed = estimateTextWidth21(node.label ?? node.id, labelFontSize, labelFontWeight) + VALUE_GAP + estimateTextWidth21(text, labelFontSize, valueFontWeight);
|
|
16347
|
+
return needed <= available ? { text, style: valueStyle } : void 0;
|
|
16348
|
+
};
|
|
15483
16349
|
const nodeMarks = nodes.map((node) => {
|
|
15484
16350
|
const fill = nodeColorMap.get(node.id) ?? theme.colors.categorical[0];
|
|
15485
16351
|
const depth = node.depth ?? 0;
|
|
16352
|
+
const merged = otherMembers.get(node.id);
|
|
16353
|
+
const valueText = formatFlowValue(node.value ?? 0, nodeFmt);
|
|
15486
16354
|
return {
|
|
15487
16355
|
type: "sankeyNode",
|
|
15488
16356
|
x: node.x0 ?? 0,
|
|
@@ -15490,6 +16358,9 @@ function compileSankey(spec, options) {
|
|
|
15490
16358
|
width: (node.x1 ?? 0) - (node.x0 ?? 0),
|
|
15491
16359
|
height: (node.y1 ?? 0) - (node.y0 ?? 0),
|
|
15492
16360
|
fill,
|
|
16361
|
+
// Explicit: a node is a solid block of flow, not an outlined box. Without
|
|
16362
|
+
// this the renderer inherits whatever stroke the SVG context carries.
|
|
16363
|
+
stroke: "none",
|
|
15493
16364
|
cornerRadius: NODE_CORNER_RADIUS,
|
|
15494
16365
|
label: computeNodeLabel(
|
|
15495
16366
|
node,
|
|
@@ -15500,13 +16371,14 @@ function compileSankey(spec, options) {
|
|
|
15500
16371
|
options.width,
|
|
15501
16372
|
padding
|
|
15502
16373
|
),
|
|
16374
|
+
valueLabel: valueLabelFor(node, depth, valueText),
|
|
15503
16375
|
nodeId: node.id,
|
|
15504
16376
|
value: node.value ?? 0,
|
|
15505
16377
|
depth,
|
|
15506
|
-
data: { id: node.id, label: node.label },
|
|
16378
|
+
data: merged ? { id: node.id, label: node.label, merged } : { id: node.id, label: node.label },
|
|
15507
16379
|
aria: {
|
|
15508
16380
|
role: "img",
|
|
15509
|
-
label: `${node.label}: ${
|
|
16381
|
+
label: `${node.label}: ${valueText}`
|
|
15510
16382
|
},
|
|
15511
16383
|
animationIndex: 0
|
|
15512
16384
|
// Reassigned below after sorting by depth
|
|
@@ -15545,13 +16417,13 @@ function compileSankey(spec, options) {
|
|
|
15545
16417
|
const finalLegend = buildSankeyLegend(
|
|
15546
16418
|
nodeColorMap,
|
|
15547
16419
|
colorField,
|
|
15548
|
-
|
|
16420
|
+
workingData,
|
|
15549
16421
|
sourceField,
|
|
15550
16422
|
targetField,
|
|
15551
16423
|
theme,
|
|
15552
16424
|
fullArea
|
|
15553
16425
|
);
|
|
15554
|
-
const tooltipDescriptors = buildTooltipDescriptors(nodeMarks, linkMarks, flowFmt);
|
|
16426
|
+
const tooltipDescriptors = buildTooltipDescriptors(nodeMarks, linkMarks, flowFmt, nodeFmt);
|
|
15555
16427
|
const a11y = {
|
|
15556
16428
|
altText: `Sankey diagram with ${nodeMarks.length} nodes and ${linkMarks.length} links`,
|
|
15557
16429
|
dataTableFallback: linkMarks.map((l) => [l.sourceId, l.targetId, String(l.value)]),
|
|
@@ -15635,18 +16507,39 @@ function buildSankeyLegend(nodeColorMap, colorField, data, sourceField, targetFi
|
|
|
15635
16507
|
swatchChipFill: theme.colors.annotationFill
|
|
15636
16508
|
};
|
|
15637
16509
|
}
|
|
16510
|
+
var MAX_UNIFORM_DECIMALS = 2;
|
|
16511
|
+
function decimalPlaces(value) {
|
|
16512
|
+
const rounded = Number(value.toFixed(MAX_UNIFORM_DECIMALS));
|
|
16513
|
+
if (Number.isInteger(rounded)) return 0;
|
|
16514
|
+
const text = String(rounded);
|
|
16515
|
+
const dot = text.indexOf(".");
|
|
16516
|
+
return dot === -1 ? 0 : text.length - dot - 1;
|
|
16517
|
+
}
|
|
16518
|
+
function resolveNodeValueFormatter(nodes, explicitFormat, fallback) {
|
|
16519
|
+
if (explicitFormat) return fallback;
|
|
16520
|
+
const values = nodes.map((n) => n.value ?? 0).filter((v) => Number.isFinite(v));
|
|
16521
|
+
if (values.length === 0) return fallback;
|
|
16522
|
+
if (values.some((v) => Math.abs(v) >= 1e3)) return fallback;
|
|
16523
|
+
if (values.some((v) => v !== 0 && Math.abs(v) < 5e-3)) return fallback;
|
|
16524
|
+
let decimals = 0;
|
|
16525
|
+
for (const v of values) {
|
|
16526
|
+
decimals = Math.max(decimals, decimalPlaces(v));
|
|
16527
|
+
if (decimals >= MAX_UNIFORM_DECIMALS) break;
|
|
16528
|
+
}
|
|
16529
|
+
return buildD3Formatter3(`,.${decimals}f`) ?? fallback;
|
|
16530
|
+
}
|
|
15638
16531
|
var defaultFmt3 = defaultNumberFormatter10();
|
|
15639
16532
|
function formatFlowValue(value, formatter) {
|
|
15640
16533
|
if (formatter) return formatter(value);
|
|
15641
16534
|
return defaultFmt3(value);
|
|
15642
16535
|
}
|
|
15643
|
-
function buildTooltipDescriptors(nodes, links, formatter) {
|
|
16536
|
+
function buildTooltipDescriptors(nodes, links, formatter, nodeFormatter) {
|
|
15644
16537
|
const descriptors = /* @__PURE__ */ new Map();
|
|
15645
16538
|
for (const node of nodes) {
|
|
15646
16539
|
const fields = [
|
|
15647
16540
|
{
|
|
15648
16541
|
label: "Total flow",
|
|
15649
|
-
value: formatFlowValue(node.value, formatter)
|
|
16542
|
+
value: formatFlowValue(node.value, nodeFormatter ?? formatter)
|
|
15650
16543
|
}
|
|
15651
16544
|
];
|
|
15652
16545
|
descriptors.set(`node-${node.nodeId}`, {
|
|
@@ -15767,19 +16660,22 @@ function computeColumnMin(data, key) {
|
|
|
15767
16660
|
}
|
|
15768
16661
|
|
|
15769
16662
|
// src/tables/category-colors.ts
|
|
15770
|
-
import { adaptColorForDarkMode } from "@opendata-ai/openchart-core";
|
|
16663
|
+
import { adaptColorForDarkMode, findAccessibleColor as findAccessibleColor2 } from "@opendata-ai/openchart-core";
|
|
16664
|
+
import { interpolateRgb as interpolateRgb2 } from "d3-interpolate";
|
|
15771
16665
|
|
|
15772
16666
|
// src/tables/utils.ts
|
|
15773
|
-
import { contrastRatio as contrastRatio2 } from "@opendata-ai/openchart-core";
|
|
15774
|
-
function
|
|
15775
|
-
|
|
15776
|
-
|
|
15777
|
-
|
|
15778
|
-
const
|
|
15779
|
-
|
|
16667
|
+
import { contrastRatio as contrastRatio2, cssTokenDefault } from "@opendata-ai/openchart-core";
|
|
16668
|
+
function resolveTableSurface(theme) {
|
|
16669
|
+
return theme.colors.neutral.surface;
|
|
16670
|
+
}
|
|
16671
|
+
function accessibleTextColor(bg, theme) {
|
|
16672
|
+
const own = theme.colors.text;
|
|
16673
|
+
const inverse = cssTokenDefault("--oc-text", theme.isDark ? "light" : "dark");
|
|
16674
|
+
return contrastRatio2(inverse, bg) > contrastRatio2(own, bg) ? inverse : own;
|
|
15780
16675
|
}
|
|
15781
16676
|
|
|
15782
16677
|
// src/tables/category-colors.ts
|
|
16678
|
+
var CHIP_TINT = 0.14;
|
|
15783
16679
|
function computeCategoryColors(data, column, theme, darkMode) {
|
|
15784
16680
|
const result = /* @__PURE__ */ new Map();
|
|
15785
16681
|
const explicitMap = column.categoryColors;
|
|
@@ -15789,6 +16685,7 @@ function computeCategoryColors(data, column, theme, darkMode) {
|
|
|
15789
16685
|
const autoAssigned = /* @__PURE__ */ new Map();
|
|
15790
16686
|
const lightBg = "#ffffff";
|
|
15791
16687
|
const darkBg = theme.colors.background;
|
|
16688
|
+
const surface = resolveTableSurface(theme);
|
|
15792
16689
|
for (let i = 0; i < data.length; i++) {
|
|
15793
16690
|
const raw = data[i][column.key];
|
|
15794
16691
|
if (raw == null) continue;
|
|
@@ -15815,10 +16712,11 @@ function computeCategoryColors(data, column, theme, darkMode) {
|
|
|
15815
16712
|
if (darkMode && !isExplicit) {
|
|
15816
16713
|
bg = adaptColorForDarkMode(bg, lightBg, darkBg);
|
|
15817
16714
|
}
|
|
15818
|
-
const
|
|
16715
|
+
const tint = interpolateRgb2(surface, bg)(CHIP_TINT);
|
|
15819
16716
|
result.set(i, {
|
|
15820
|
-
|
|
15821
|
-
|
|
16717
|
+
accent: bg,
|
|
16718
|
+
backgroundColor: tint,
|
|
16719
|
+
color: findAccessibleColor2(bg, tint, 4.5)
|
|
15822
16720
|
});
|
|
15823
16721
|
}
|
|
15824
16722
|
return result;
|
|
@@ -15826,7 +16724,7 @@ function computeCategoryColors(data, column, theme, darkMode) {
|
|
|
15826
16724
|
|
|
15827
16725
|
// src/tables/format-cells.ts
|
|
15828
16726
|
import {
|
|
15829
|
-
buildD3Formatter as
|
|
16727
|
+
buildD3Formatter as buildD3Formatter4,
|
|
15830
16728
|
defaultNumberFormatter as defaultNumberFormatter11,
|
|
15831
16729
|
formatDate as formatDate3,
|
|
15832
16730
|
resolveNumberFormatter as resolveNumberFormatter7
|
|
@@ -15849,7 +16747,7 @@ function formatCell(value, column) {
|
|
|
15849
16747
|
};
|
|
15850
16748
|
}
|
|
15851
16749
|
if (column.format && isNumericValue(value)) {
|
|
15852
|
-
const formatter = resolveNumberFormatter7(column.format, { surface: "table" }) ??
|
|
16750
|
+
const formatter = resolveNumberFormatter7(column.format, { surface: "table" }) ?? buildD3Formatter4(column.format);
|
|
15853
16751
|
if (formatter) {
|
|
15854
16752
|
return {
|
|
15855
16753
|
value,
|
|
@@ -15881,7 +16779,7 @@ function formatCell(value, column) {
|
|
|
15881
16779
|
function formatValueForSearch(value, column) {
|
|
15882
16780
|
if (value == null) return "";
|
|
15883
16781
|
if (column.format && isNumericValue(value)) {
|
|
15884
|
-
const formatter = resolveNumberFormatter7(column.format, { surface: "table" }) ??
|
|
16782
|
+
const formatter = resolveNumberFormatter7(column.format, { surface: "table" }) ?? buildD3Formatter4(column.format);
|
|
15885
16783
|
if (formatter) {
|
|
15886
16784
|
return formatter(value);
|
|
15887
16785
|
}
|
|
@@ -15894,7 +16792,7 @@ function formatValueForSearch(value, column) {
|
|
|
15894
16792
|
|
|
15895
16793
|
// src/tables/heatmap.ts
|
|
15896
16794
|
import { adaptColorForDarkMode as adaptColorForDarkMode2, contrastRatio as contrastRatio3 } from "@opendata-ai/openchart-core";
|
|
15897
|
-
import { interpolateRgb } from "d3-interpolate";
|
|
16795
|
+
import { interpolateRgb as interpolateRgb3 } from "d3-interpolate";
|
|
15898
16796
|
import { scaleSequential } from "d3-scale";
|
|
15899
16797
|
function interpolatorFromStops(stops) {
|
|
15900
16798
|
if (stops.length === 0) return () => "#ffffff";
|
|
@@ -15905,7 +16803,7 @@ function interpolatorFromStops(stops) {
|
|
|
15905
16803
|
const lo = Math.floor(segment);
|
|
15906
16804
|
const hi = Math.min(lo + 1, stops.length - 1);
|
|
15907
16805
|
const frac = segment - lo;
|
|
15908
|
-
return
|
|
16806
|
+
return interpolateRgb3(stops[lo], stops[hi])(frac);
|
|
15909
16807
|
};
|
|
15910
16808
|
}
|
|
15911
16809
|
function resolvePalette(palette, theme) {
|
|
@@ -15947,8 +16845,7 @@ function computeHeatmapColors(data, column, theme, darkMode) {
|
|
|
15947
16845
|
let stops = resolvePalette(config.palette, theme);
|
|
15948
16846
|
if (darkMode) {
|
|
15949
16847
|
const lightBg = "#ffffff";
|
|
15950
|
-
const
|
|
15951
|
-
const darkBg = themeBg === "transparent" ? "#09090b" : themeBg;
|
|
16848
|
+
const darkBg = resolveTableSurface(theme);
|
|
15952
16849
|
if (Array.isArray(config.palette)) {
|
|
15953
16850
|
stops = stops.map((c) => {
|
|
15954
16851
|
const ratio = contrastRatio3(c, lightBg);
|
|
@@ -15961,10 +16858,12 @@ function computeHeatmapColors(data, column, theme, darkMode) {
|
|
|
15961
16858
|
}
|
|
15962
16859
|
const interpolator = interpolatorFromStops(stops);
|
|
15963
16860
|
const scale = scaleSequential(interpolator).domain(domain).clamp(true);
|
|
16861
|
+
const surface = resolveTableSurface(theme);
|
|
16862
|
+
const soften = (c) => interpolateRgb3(c, surface)(0.2);
|
|
15964
16863
|
for (const { index, value } of numericValues) {
|
|
15965
16864
|
if (value <= domain[0]) continue;
|
|
15966
|
-
const bg = scale(value);
|
|
15967
|
-
const textColor = accessibleTextColor(bg);
|
|
16865
|
+
const bg = soften(scale(value));
|
|
16866
|
+
const textColor = accessibleTextColor(bg, theme);
|
|
15968
16867
|
result.set(index, {
|
|
15969
16868
|
backgroundColor: bg,
|
|
15970
16869
|
color: textColor
|
|
@@ -16058,24 +16957,44 @@ function sortData(data, sort) {
|
|
|
16058
16957
|
}
|
|
16059
16958
|
|
|
16060
16959
|
// src/tables/sparkline.ts
|
|
16061
|
-
function
|
|
16960
|
+
function extractSparklineValues(row, columnKey, config) {
|
|
16062
16961
|
const field = config.valuesField ?? columnKey;
|
|
16063
16962
|
const raw = row[field];
|
|
16064
16963
|
if (!Array.isArray(raw)) return [];
|
|
16065
16964
|
return raw.map((v) => typeof v === "number" && Number.isFinite(v) ? v : null).filter((v) => v !== null);
|
|
16066
16965
|
}
|
|
16067
|
-
function
|
|
16966
|
+
function computeSparklineDomain(data, columnKey, config) {
|
|
16967
|
+
const domain = config.domain ?? "shared";
|
|
16968
|
+
if (Array.isArray(domain)) return domain;
|
|
16969
|
+
if (domain === "row") return null;
|
|
16970
|
+
let min4 = Infinity;
|
|
16971
|
+
let max4 = -Infinity;
|
|
16972
|
+
for (const row of data) {
|
|
16973
|
+
for (const v of extractSparklineValues(row, columnKey, config)) {
|
|
16974
|
+
if (v < min4) min4 = v;
|
|
16975
|
+
if (v > max4) max4 = v;
|
|
16976
|
+
}
|
|
16977
|
+
}
|
|
16978
|
+
return Number.isFinite(min4) && Number.isFinite(max4) ? [min4, max4] : null;
|
|
16979
|
+
}
|
|
16980
|
+
function computeSparkline(values, config, theme, _darkMode, domain) {
|
|
16068
16981
|
if (values.length === 0) return null;
|
|
16069
16982
|
const type = config.type ?? "line";
|
|
16070
16983
|
const color = config.color ?? theme.colors.categorical[0];
|
|
16071
|
-
let min4
|
|
16072
|
-
let max4
|
|
16073
|
-
|
|
16074
|
-
|
|
16075
|
-
|
|
16984
|
+
let min4;
|
|
16985
|
+
let max4;
|
|
16986
|
+
if (domain) {
|
|
16987
|
+
[min4, max4] = domain;
|
|
16988
|
+
} else {
|
|
16989
|
+
min4 = Infinity;
|
|
16990
|
+
max4 = -Infinity;
|
|
16991
|
+
for (const v of values) {
|
|
16992
|
+
if (v < min4) min4 = v;
|
|
16993
|
+
if (v > max4) max4 = v;
|
|
16994
|
+
}
|
|
16076
16995
|
}
|
|
16077
16996
|
const range = max4 - min4;
|
|
16078
|
-
const normalize = (v) => range === 0 ? 0.5 : (v - min4) / range;
|
|
16997
|
+
const normalize = (v) => range === 0 ? 0.5 : Math.max(0, Math.min(1, (v - min4) / range));
|
|
16079
16998
|
const startValue = values[0];
|
|
16080
16999
|
const endValue = values[values.length - 1];
|
|
16081
17000
|
if (type === "line") {
|
|
@@ -16108,30 +17027,41 @@ function computeSparkline(values, config, theme, _darkMode) {
|
|
|
16108
17027
|
endValue
|
|
16109
17028
|
};
|
|
16110
17029
|
}
|
|
16111
|
-
function computeSparklineForRow(row, columnKey, config, theme, darkMode) {
|
|
16112
|
-
const values =
|
|
16113
|
-
return computeSparkline(values, config, theme, darkMode);
|
|
17030
|
+
function computeSparklineForRow(row, columnKey, config, theme, darkMode, domain) {
|
|
17031
|
+
const values = extractSparklineValues(row, columnKey, config);
|
|
17032
|
+
return computeSparkline(values, config, theme, darkMode, domain);
|
|
16114
17033
|
}
|
|
16115
17034
|
|
|
16116
17035
|
// src/tables/compile-table.ts
|
|
16117
17036
|
function determineCellType(col) {
|
|
16118
17037
|
if (col.sparkline) return "sparkline";
|
|
16119
17038
|
if (col.bar) return "bar";
|
|
17039
|
+
if (col.delta) return "delta";
|
|
16120
17040
|
if (col.heatmap) return "heatmap";
|
|
16121
17041
|
if (col.image) return "image";
|
|
16122
17042
|
if (col.flag) return "flag";
|
|
16123
17043
|
if (col.categoryColors) return "category";
|
|
16124
17044
|
return "text";
|
|
16125
17045
|
}
|
|
16126
|
-
|
|
16127
|
-
|
|
17046
|
+
var NON_QUANTITATIVE_KEY = /(^|_)(id|zip|postal|code|year|fips)$/i;
|
|
17047
|
+
function snakeCaseKey(key) {
|
|
17048
|
+
return key.replace(/([a-z0-9])([A-Z])/g, "$1_$2");
|
|
17049
|
+
}
|
|
17050
|
+
function inferColumnType(col, data) {
|
|
17051
|
+
if (col.type) return col.type;
|
|
17052
|
+
if (NON_QUANTITATIVE_KEY.test(snakeCaseKey(col.key))) return "nominal";
|
|
16128
17053
|
for (const row of data) {
|
|
16129
17054
|
const val = row[col.key];
|
|
16130
|
-
if (val
|
|
16131
|
-
|
|
16132
|
-
|
|
17055
|
+
if (val == null) continue;
|
|
17056
|
+
if (typeof val === "number") return "quantitative";
|
|
17057
|
+
if (val instanceof Date) return "temporal";
|
|
17058
|
+
return "nominal";
|
|
16133
17059
|
}
|
|
16134
|
-
return "
|
|
17060
|
+
return "nominal";
|
|
17061
|
+
}
|
|
17062
|
+
function inferAlignment(col, type) {
|
|
17063
|
+
if (col.align) return col.align;
|
|
17064
|
+
return type === "quantitative" ? "right" : "left";
|
|
16135
17065
|
}
|
|
16136
17066
|
function estimateColumnWidth(col, data, fontSize) {
|
|
16137
17067
|
const MIN_WIDTH = 60;
|
|
@@ -16170,14 +17100,28 @@ function resolveColumns(columns, data, totalWidth, theme) {
|
|
|
16170
17100
|
const flexTotal = naturalWidths.reduce((sum, w, i) => sum + (isFixed[i] ? 0 : w), 0);
|
|
16171
17101
|
const remainingWidth = totalWidth - fixedTotal;
|
|
16172
17102
|
const flexScale = flexTotal > 0 && remainingWidth > 0 ? remainingWidth / flexTotal : 1;
|
|
16173
|
-
return columns.map((col, i) =>
|
|
16174
|
-
|
|
16175
|
-
|
|
16176
|
-
|
|
16177
|
-
|
|
16178
|
-
|
|
16179
|
-
|
|
16180
|
-
|
|
17103
|
+
return columns.map((col, i) => {
|
|
17104
|
+
const type = inferColumnType(col, data);
|
|
17105
|
+
return {
|
|
17106
|
+
key: col.key,
|
|
17107
|
+
label: col.label ?? col.key,
|
|
17108
|
+
width: Math.max(60, isFixed[i] ? naturalWidths[i] : Math.round(naturalWidths[i] * flexScale)),
|
|
17109
|
+
sortable: col.sortable ?? true,
|
|
17110
|
+
align: inferAlignment(col, type),
|
|
17111
|
+
type,
|
|
17112
|
+
// The first column leads the mobile card unless the author says otherwise.
|
|
17113
|
+
priority: col.priority ?? (i === 0 ? 1 : 2),
|
|
17114
|
+
cellType: determineCellType(col)
|
|
17115
|
+
};
|
|
17116
|
+
});
|
|
17117
|
+
}
|
|
17118
|
+
var DIRECTION_WORD = {
|
|
17119
|
+
up: "up",
|
|
17120
|
+
down: "down",
|
|
17121
|
+
flat: "unchanged"
|
|
17122
|
+
};
|
|
17123
|
+
function stripSign(formatted) {
|
|
17124
|
+
return formatted.replace(/^[+\u2212-]/, "");
|
|
16181
17125
|
}
|
|
16182
17126
|
function buildCell(value, column, resolvedColumn, heatmapStyle, categoryStyle, barData, sparklineData) {
|
|
16183
17127
|
const base = formatCell(value, column);
|
|
@@ -16219,6 +17163,27 @@ function buildCell(value, column, resolvedColumn, heatmapStyle, categoryStyle, b
|
|
|
16219
17163
|
sparklineData
|
|
16220
17164
|
};
|
|
16221
17165
|
}
|
|
17166
|
+
case "delta": {
|
|
17167
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
17168
|
+
return { ...base, cellType: "text" };
|
|
17169
|
+
}
|
|
17170
|
+
const delta = value;
|
|
17171
|
+
const invert = typeof column.delta === "object" ? column.delta.invert ?? false : false;
|
|
17172
|
+
const direction = delta > 0 ? "up" : delta < 0 ? "down" : "flat";
|
|
17173
|
+
const favorable = invert ? delta < 0 : delta > 0;
|
|
17174
|
+
const unfavorable = invert ? delta > 0 : delta < 0;
|
|
17175
|
+
return {
|
|
17176
|
+
...base,
|
|
17177
|
+
// The arrow carries the direction, so a sign in the formatted value
|
|
17178
|
+
// would say it twice (and contradict it under `invert`).
|
|
17179
|
+
formattedValue: stripSign(base.formattedValue),
|
|
17180
|
+
cellType: "delta",
|
|
17181
|
+
delta,
|
|
17182
|
+
direction,
|
|
17183
|
+
tone: favorable ? "positive" : unfavorable ? "negative" : "neutral",
|
|
17184
|
+
aria: base.aria ?? `${DIRECTION_WORD[direction]} ${stripSign(base.formattedValue)}`
|
|
17185
|
+
};
|
|
17186
|
+
}
|
|
16222
17187
|
case "image": {
|
|
16223
17188
|
const src = typeof value === "string" ? value : "";
|
|
16224
17189
|
const imgConfig = column.image ?? {};
|
|
@@ -16247,15 +17212,52 @@ function buildCell(value, column, resolvedColumn, heatmapStyle, categoryStyle, b
|
|
|
16247
17212
|
}
|
|
16248
17213
|
}
|
|
16249
17214
|
}
|
|
17215
|
+
function resolveSort(spec, columns, optionsSort) {
|
|
17216
|
+
if (optionsSort) return optionsSort;
|
|
17217
|
+
if (optionsSort === null) return void 0;
|
|
17218
|
+
if (spec.sort) return spec.sort;
|
|
17219
|
+
const barColumn = columns.find((c) => c.cellType === "bar" && c.sortable);
|
|
17220
|
+
return barColumn ? { column: barColumn.key, direction: "desc" } : void 0;
|
|
17221
|
+
}
|
|
17222
|
+
function buildTotalRow(label, columns, resolvedColumns, rows) {
|
|
17223
|
+
const SUMMABLE = /* @__PURE__ */ new Set(["text", "bar", "heatmap"]);
|
|
17224
|
+
const cells = resolvedColumns.map((resolved, i) => {
|
|
17225
|
+
const blank = {
|
|
17226
|
+
value: null,
|
|
17227
|
+
formattedValue: "",
|
|
17228
|
+
style: {},
|
|
17229
|
+
cellType: "text"
|
|
17230
|
+
};
|
|
17231
|
+
if (resolved.type !== "quantitative" || !SUMMABLE.has(resolved.cellType)) return blank;
|
|
17232
|
+
let sum = 0;
|
|
17233
|
+
let seen = false;
|
|
17234
|
+
for (const row of rows) {
|
|
17235
|
+
const v = row[resolved.key];
|
|
17236
|
+
if (typeof v === "number" && Number.isFinite(v)) {
|
|
17237
|
+
sum += v;
|
|
17238
|
+
seen = true;
|
|
17239
|
+
}
|
|
17240
|
+
}
|
|
17241
|
+
if (!seen) return blank;
|
|
17242
|
+
const base = formatCell(sum, columns[i]);
|
|
17243
|
+
return {
|
|
17244
|
+
...base,
|
|
17245
|
+
style: { ...base.style, fontVariant: "tabular-nums" },
|
|
17246
|
+
cellType: "text"
|
|
17247
|
+
};
|
|
17248
|
+
});
|
|
17249
|
+
return { label, cells };
|
|
17250
|
+
}
|
|
16250
17251
|
function compileTableLayout(spec, options, theme) {
|
|
16251
17252
|
const data = spec.data;
|
|
16252
17253
|
const darkMode = theme.isDark;
|
|
16253
17254
|
const resolvedColumns = resolveColumns(spec.columns, data, options.width, theme);
|
|
17255
|
+
const sort = resolveSort(spec, resolvedColumns, options.sort);
|
|
16254
17256
|
const searchIndex = spec.search ? buildSearchIndex(data, spec.columns) : /* @__PURE__ */ new Map();
|
|
16255
17257
|
let currentData = data;
|
|
16256
17258
|
let originalIndices = data.map((_, i) => i);
|
|
16257
|
-
if (
|
|
16258
|
-
const sorted = sortData(currentData,
|
|
17259
|
+
if (sort) {
|
|
17260
|
+
const sorted = sortData(currentData, sort);
|
|
16259
17261
|
originalIndices = sorted.originalIndices.map((i) => originalIndices[i]);
|
|
16260
17262
|
currentData = sorted.data;
|
|
16261
17263
|
}
|
|
@@ -16265,6 +17267,7 @@ function compileTableLayout(spec, options, theme) {
|
|
|
16265
17267
|
originalIndices = filtered.indices;
|
|
16266
17268
|
}
|
|
16267
17269
|
const totalFiltered = currentData.length;
|
|
17270
|
+
const filteredData = currentData;
|
|
16268
17271
|
let pageSize = 0;
|
|
16269
17272
|
let currentPage = 0;
|
|
16270
17273
|
let paginationState;
|
|
@@ -16288,6 +17291,7 @@ function compileTableLayout(spec, options, theme) {
|
|
|
16288
17291
|
const categoryMaps = /* @__PURE__ */ new Map();
|
|
16289
17292
|
const barMaxes = /* @__PURE__ */ new Map();
|
|
16290
17293
|
const barMins = /* @__PURE__ */ new Map();
|
|
17294
|
+
const sparklineDomains = /* @__PURE__ */ new Map();
|
|
16291
17295
|
for (let c = 0; c < spec.columns.length; c++) {
|
|
16292
17296
|
const col = spec.columns[c];
|
|
16293
17297
|
const resolved = resolvedColumns[c];
|
|
@@ -16301,6 +17305,9 @@ function compileTableLayout(spec, options, theme) {
|
|
|
16301
17305
|
barMaxes.set(col.key, computeColumnMax(data, col.key));
|
|
16302
17306
|
barMins.set(col.key, computeColumnMin(data, col.key));
|
|
16303
17307
|
}
|
|
17308
|
+
if (resolved.cellType === "sparkline" && col.sparkline) {
|
|
17309
|
+
sparklineDomains.set(col.key, computeSparklineDomain(data, col.key, col.sparkline));
|
|
17310
|
+
}
|
|
16304
17311
|
}
|
|
16305
17312
|
const rows = currentData.map((row, i) => {
|
|
16306
17313
|
const origIdx = originalIndices[i];
|
|
@@ -16323,7 +17330,14 @@ function compileTableLayout(spec, options, theme) {
|
|
|
16323
17330
|
}
|
|
16324
17331
|
let sparklineData = null;
|
|
16325
17332
|
if (resolved.cellType === "sparkline" && col.sparkline) {
|
|
16326
|
-
sparklineData = computeSparklineForRow(
|
|
17333
|
+
sparklineData = computeSparklineForRow(
|
|
17334
|
+
row,
|
|
17335
|
+
col.key,
|
|
17336
|
+
col.sparkline,
|
|
17337
|
+
theme,
|
|
17338
|
+
darkMode,
|
|
17339
|
+
sparklineDomains.get(col.key)
|
|
17340
|
+
);
|
|
16327
17341
|
}
|
|
16328
17342
|
return buildCell(value, col, resolved, heatmapStyle, categoryStyle, barData, sparklineData);
|
|
16329
17343
|
});
|
|
@@ -16351,7 +17365,7 @@ function compileTableLayout(spec, options, theme) {
|
|
|
16351
17365
|
chrome,
|
|
16352
17366
|
columns: resolvedColumns,
|
|
16353
17367
|
rows,
|
|
16354
|
-
sort
|
|
17368
|
+
sort,
|
|
16355
17369
|
pagination: paginationState,
|
|
16356
17370
|
search: {
|
|
16357
17371
|
enabled: spec.search,
|
|
@@ -16359,6 +17373,9 @@ function compileTableLayout(spec, options, theme) {
|
|
|
16359
17373
|
query: options.search ?? ""
|
|
16360
17374
|
},
|
|
16361
17375
|
stickyFirstColumn: spec.stickyFirstColumn,
|
|
17376
|
+
density: spec.density,
|
|
17377
|
+
striped: spec.striped,
|
|
17378
|
+
totalRow: spec.totalRow ? buildTotalRow(spec.totalRow.label, spec.columns, resolvedColumns, filteredData) : void 0,
|
|
16362
17379
|
compact: spec.compact,
|
|
16363
17380
|
a11y: {
|
|
16364
17381
|
caption,
|
|
@@ -16373,7 +17390,7 @@ function compileTableLayout(spec, options, theme) {
|
|
|
16373
17390
|
// src/tilemap/compile-tilemap.ts
|
|
16374
17391
|
import {
|
|
16375
17392
|
adaptTheme as adaptTheme5,
|
|
16376
|
-
CATEGORICAL_PALETTE as
|
|
17393
|
+
CATEGORICAL_PALETTE as CATEGORICAL_PALETTE3,
|
|
16377
17394
|
computeChrome as computeChrome8,
|
|
16378
17395
|
estimateTextWidth as estimateTextWidth23,
|
|
16379
17396
|
getBreakpoint as getBreakpoint2,
|
|
@@ -16383,15 +17400,28 @@ import {
|
|
|
16383
17400
|
resolveTheme as resolveTheme6,
|
|
16384
17401
|
SEQUENTIAL_PALETTES as SEQUENTIAL_PALETTES3
|
|
16385
17402
|
} from "@opendata-ai/openchart-core";
|
|
17403
|
+
import { interpolateRgb as interpolateRgb4 } from "d3-interpolate";
|
|
16386
17404
|
import { scaleLinear as scaleLinear6 } from "d3-scale";
|
|
16387
|
-
var TILE_CORNER_RADIUS =
|
|
16388
|
-
var TILE_STROKE_WIDTH =
|
|
17405
|
+
var TILE_CORNER_RADIUS = 2;
|
|
17406
|
+
var TILE_STROKE_WIDTH = 0;
|
|
17407
|
+
var NO_DATA_STROKE_WIDTH = 1;
|
|
17408
|
+
var NO_DATA_BAR_GAP = 12;
|
|
17409
|
+
var NO_DATA_LABEL_GAP2 = 5;
|
|
16389
17410
|
var LEGEND_SWATCH_SIZE = 10;
|
|
16390
17411
|
var LEGEND_SWATCH_GAP = 6;
|
|
16391
17412
|
var LEGEND_ENTRY_GAP = 16;
|
|
16392
17413
|
function clamp(value, min4, max4) {
|
|
16393
17414
|
return Math.min(max4, Math.max(min4, value));
|
|
16394
17415
|
}
|
|
17416
|
+
function blendOnSurface(fill, fillOpacity, theme) {
|
|
17417
|
+
const surface = theme.colors.neutral.surface;
|
|
17418
|
+
if (fillOpacity >= 1) return fill;
|
|
17419
|
+
try {
|
|
17420
|
+
return interpolateRgb4(surface, fill)(fillOpacity);
|
|
17421
|
+
} catch {
|
|
17422
|
+
return fill;
|
|
17423
|
+
}
|
|
17424
|
+
}
|
|
16395
17425
|
function growContentHeight(contentHeight, chromeLayout, budgetHeight, chrome) {
|
|
16396
17426
|
if (chromeLayout !== "grow") return contentHeight;
|
|
16397
17427
|
return Math.max(contentHeight, budgetHeight + chrome.topHeight + chrome.bottomHeight);
|
|
@@ -16488,10 +17518,10 @@ function compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDa
|
|
|
16488
17518
|
const max4 = values.length > 0 ? Math.max(...values) : 100;
|
|
16489
17519
|
const paletteStops = [...SEQUENTIAL_PALETTES3[tilemapSpec.palette] ?? SEQUENTIAL_PALETTES3.blue];
|
|
16490
17520
|
const baseColor = isDarkMode ? paletteStops[0] : paletteStops[paletteStops.length - 1];
|
|
16491
|
-
const opacityRange = isDarkMode ? [0.
|
|
17521
|
+
const opacityRange = isDarkMode ? [0.3, 1] : [0.25, 1];
|
|
16492
17522
|
const opacityScale = scaleLinear6().domain([min4, max4]).range(opacityRange).clamp(true);
|
|
16493
17523
|
const showLegend = tilemapSpec.legend?.show !== false;
|
|
16494
|
-
const legendBarHeight =
|
|
17524
|
+
const legendBarHeight = 10;
|
|
16495
17525
|
const legendLabelGap = 6;
|
|
16496
17526
|
const legendTotalHeight = showLegend ? legendBarHeight + legendLabelGap + 14 : 0;
|
|
16497
17527
|
const legendGap2 = showLegend ? 8 : 0;
|
|
@@ -16506,8 +17536,8 @@ function compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDa
|
|
|
16506
17536
|
surfaceFormat: tilemapSpec.encoding.value.format ?? tilemapSpec.valueFormat,
|
|
16507
17537
|
values: Array.from(stateValueMap.values())
|
|
16508
17538
|
});
|
|
16509
|
-
const neutralFill = isDarkMode ?
|
|
16510
|
-
const neutralStroke = isDarkMode ?
|
|
17539
|
+
const neutralFill = isDarkMode ? theme.colors.neutral[800] : theme.colors.neutral[100];
|
|
17540
|
+
const neutralStroke = isDarkMode ? theme.colors.neutral[600] : theme.colors.neutral[300];
|
|
16511
17541
|
const tiles = [];
|
|
16512
17542
|
for (const { state: stateCode } of US_STATE_TILES) {
|
|
16513
17543
|
const pos = tilePositions.positions.get(stateCode);
|
|
@@ -16516,7 +17546,7 @@ function compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDa
|
|
|
16516
17546
|
const value = hasData ? stateValueMap.get(stateCode) : null;
|
|
16517
17547
|
const opacity = hasData ? opacityScale(value) : 0;
|
|
16518
17548
|
const fill = hasData ? baseColor : neutralFill;
|
|
16519
|
-
const stroke = hasData ?
|
|
17549
|
+
const stroke = hasData ? "none" : neutralStroke;
|
|
16520
17550
|
const formattedValue = hasData ? formatter(value) : "\u2013";
|
|
16521
17551
|
tiles.push(
|
|
16522
17552
|
buildTileMark({
|
|
@@ -16545,10 +17575,16 @@ function compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDa
|
|
|
16545
17575
|
const o = opacityRange[0] + t * (opacityRange[1] - opacityRange[0]);
|
|
16546
17576
|
return { offset: t, color: baseColor, opacity: o };
|
|
16547
17577
|
});
|
|
17578
|
+
const hasEmptyTiles = tiles.some((t) => !t.hasData);
|
|
17579
|
+
const noDataLabel = "No data";
|
|
17580
|
+
const noDataLabelWidth = estimateTextWidth23(noDataLabel, 11);
|
|
17581
|
+
const noDataBlockWidth = hasEmptyTiles ? NO_DATA_BAR_GAP + legendBarHeight + NO_DATA_LABEL_GAP2 + noDataLabelWidth : 0;
|
|
17582
|
+
const barWidth = Math.max(0, legendWidth - noDataBlockWidth);
|
|
17583
|
+
const noDataX = legendX + barWidth + NO_DATA_BAR_GAP;
|
|
16548
17584
|
gradientLegend = {
|
|
16549
17585
|
type: "gradient",
|
|
16550
17586
|
position: "bottom",
|
|
16551
|
-
bounds: { x: legendX, y: legendY, width:
|
|
17587
|
+
bounds: { x: legendX, y: legendY, width: barWidth, height: legendBarHeight },
|
|
16552
17588
|
labelStyle: {
|
|
16553
17589
|
fontFamily: theme.fonts.family,
|
|
16554
17590
|
fontSize: 11,
|
|
@@ -16558,7 +17594,16 @@ function compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDa
|
|
|
16558
17594
|
},
|
|
16559
17595
|
colorStops: gradientColorStops,
|
|
16560
17596
|
minLabel: formatter(min4),
|
|
16561
|
-
maxLabel: formatter(max4)
|
|
17597
|
+
maxLabel: formatter(max4),
|
|
17598
|
+
noData: hasEmptyTiles && barWidth > 0 ? {
|
|
17599
|
+
x: noDataX,
|
|
17600
|
+
y: legendY,
|
|
17601
|
+
size: legendBarHeight,
|
|
17602
|
+
fill: neutralFill,
|
|
17603
|
+
label: noDataLabel,
|
|
17604
|
+
labelX: noDataX + legendBarHeight + NO_DATA_LABEL_GAP2,
|
|
17605
|
+
labelY: legendY + legendBarHeight
|
|
17606
|
+
} : void 0
|
|
16562
17607
|
};
|
|
16563
17608
|
}
|
|
16564
17609
|
const tooltipDescriptors = buildTooltips(tiles);
|
|
@@ -16620,12 +17665,12 @@ function compileCategorical(tilemapSpec, options, theme, chrome, fullArea, isDar
|
|
|
16620
17665
|
for (const cat of categories) {
|
|
16621
17666
|
categoryColors.set(
|
|
16622
17667
|
cat,
|
|
16623
|
-
tilemapSpec.colors[cat] ??
|
|
17668
|
+
tilemapSpec.colors[cat] ?? CATEGORICAL_PALETTE3[categoryColors.size % CATEGORICAL_PALETTE3.length]
|
|
16624
17669
|
);
|
|
16625
17670
|
}
|
|
16626
17671
|
} else {
|
|
16627
17672
|
for (let i = 0; i < categories.length; i++) {
|
|
16628
|
-
categoryColors.set(categories[i],
|
|
17673
|
+
categoryColors.set(categories[i], CATEGORICAL_PALETTE3[i % CATEGORICAL_PALETTE3.length]);
|
|
16629
17674
|
}
|
|
16630
17675
|
}
|
|
16631
17676
|
const showLegend = tilemapSpec.legend?.show !== false;
|
|
@@ -16643,8 +17688,8 @@ function compileCategorical(tilemapSpec, options, theme, chrome, fullArea, isDar
|
|
|
16643
17688
|
const legendWidth = tilePositions.gridWidth;
|
|
16644
17689
|
const legendRows = showLegend ? countLegendRows(legendLabels, legendWidth, legendLabelFontSize) : 0;
|
|
16645
17690
|
const legendTotalHeight = showLegend ? legendRowHeight * legendRows : 0;
|
|
16646
|
-
const neutralFill = isDarkMode ?
|
|
16647
|
-
const neutralStroke = isDarkMode ?
|
|
17691
|
+
const neutralFill = isDarkMode ? theme.colors.neutral[800] : theme.colors.neutral[100];
|
|
17692
|
+
const neutralStroke = isDarkMode ? theme.colors.neutral[600] : theme.colors.neutral[300];
|
|
16648
17693
|
const tiles = [];
|
|
16649
17694
|
for (const { state: stateCode } of US_STATE_TILES) {
|
|
16650
17695
|
const pos = tilePositions.positions.get(stateCode);
|
|
@@ -16652,7 +17697,7 @@ function compileCategorical(tilemapSpec, options, theme, chrome, fullArea, isDar
|
|
|
16652
17697
|
const hasData = stateCategoryMap.has(stateCode);
|
|
16653
17698
|
const category = hasData ? stateCategoryMap.get(stateCode) : null;
|
|
16654
17699
|
const fill = hasData ? categoryColors.get(category) ?? neutralFill : neutralFill;
|
|
16655
|
-
const stroke = hasData ?
|
|
17700
|
+
const stroke = hasData ? "none" : neutralStroke;
|
|
16656
17701
|
const formattedValue = category ? formatCategoryLabel(category) : "\u2013";
|
|
16657
17702
|
tiles.push(
|
|
16658
17703
|
buildTileMark({
|
|
@@ -16753,24 +17798,30 @@ function buildTileMark(opts) {
|
|
|
16753
17798
|
} = opts;
|
|
16754
17799
|
const tileCenterX = gridOffsetX + pos.x + tileSize / 2;
|
|
16755
17800
|
const tileTopY = gridOffsetY + pos.y;
|
|
16756
|
-
const
|
|
17801
|
+
const effectiveColor = hasData ? blendOnSurface(fill, fillOpacity, theme) : fill;
|
|
17802
|
+
const textColor = hasData ? pickLabelColor4(effectiveColor, isDarkMode) : theme.colors.axis;
|
|
16757
17803
|
const isLightText = textColor === "#ffffff";
|
|
16758
17804
|
const showValue = showValueLabels && tileSize >= 24;
|
|
16759
|
-
const codeFontSize = clamp(Math.round(tileSize * 0.5 * 10) / 10,
|
|
17805
|
+
const codeFontSize = clamp(Math.round(tileSize * 0.5 * 10) / 10, 7, 12);
|
|
16760
17806
|
const valueFontSize = clamp(Math.round(tileSize * 0.42 * 10) / 10, 6.5, 10);
|
|
16761
17807
|
const labelStyle = {
|
|
16762
17808
|
fontFamily: theme.fonts.family,
|
|
16763
17809
|
fontSize: codeFontSize,
|
|
16764
|
-
|
|
17810
|
+
// 600, not 700: at 8-12px a bold two-letter code on a colored tile turns
|
|
17811
|
+
// into a blob. The tracking in the renderer does the separating instead.
|
|
17812
|
+
fontWeight: theme.fonts.weights.semibold,
|
|
16765
17813
|
fill: textColor,
|
|
16766
17814
|
lineHeight: 1.2
|
|
16767
17815
|
};
|
|
16768
17816
|
const valueLabelStyle = {
|
|
16769
17817
|
fontFamily: theme.fonts.family,
|
|
16770
17818
|
fontSize: valueFontSize,
|
|
16771
|
-
|
|
16772
|
-
|
|
16773
|
-
|
|
17819
|
+
// 300 is a hinting-dependent weight that renders as 400 on most stacks and
|
|
17820
|
+
// as a ghost on the ones that have it. 450 is the honest secondary weight.
|
|
17821
|
+
fontWeight: 450,
|
|
17822
|
+
fill: isLightText ? "rgba(255,255,255,0.75)" : "rgba(0,0,0,0.55)",
|
|
17823
|
+
lineHeight: 1.2,
|
|
17824
|
+
fontVariant: "tabular-nums"
|
|
16774
17825
|
};
|
|
16775
17826
|
const codeCenterY = showValue ? tileTopY + tileSize * 0.28 : tileTopY + tileSize * 0.5;
|
|
16776
17827
|
const valueLabel = showValue ? {
|
|
@@ -16780,7 +17831,7 @@ function buildTileMark(opts) {
|
|
|
16780
17831
|
style: valueLabelStyle,
|
|
16781
17832
|
visible: true
|
|
16782
17833
|
} : { text: "", x: 0, y: 0, style: valueLabelStyle, visible: false };
|
|
16783
|
-
const cornerRadius = Math.min(TILE_CORNER_RADIUS, Math.round(tileSize * 0.
|
|
17834
|
+
const cornerRadius = Math.min(TILE_CORNER_RADIUS, Math.round(tileSize * 0.1));
|
|
16784
17835
|
const data = {
|
|
16785
17836
|
state: stateCode,
|
|
16786
17837
|
value,
|
|
@@ -16798,7 +17849,7 @@ function buildTileMark(opts) {
|
|
|
16798
17849
|
fill,
|
|
16799
17850
|
fillOpacity,
|
|
16800
17851
|
stroke,
|
|
16801
|
-
strokeWidth: TILE_STROKE_WIDTH,
|
|
17852
|
+
strokeWidth: hasData ? TILE_STROKE_WIDTH : NO_DATA_STROKE_WIDTH,
|
|
16802
17853
|
cornerRadius,
|
|
16803
17854
|
value,
|
|
16804
17855
|
formattedValue,
|
|
@@ -16904,7 +17955,7 @@ import {
|
|
|
16904
17955
|
defaultNumberFormatter as defaultNumberFormatter12,
|
|
16905
17956
|
formatDate as formatDate4,
|
|
16906
17957
|
formatPercent as formatPercent4,
|
|
16907
|
-
getRepresentativeColor as
|
|
17958
|
+
getRepresentativeColor as getRepresentativeColor14
|
|
16908
17959
|
} from "@opendata-ai/openchart-core";
|
|
16909
17960
|
function formatValue2(value, fieldType, formatter) {
|
|
16910
17961
|
if (value == null) return "";
|
|
@@ -16963,14 +18014,15 @@ function buildExplicitTooltipFields(row, channels, formatters) {
|
|
|
16963
18014
|
value: formatValue2(row[ch.field], ch.type, getFormatter(formatters, ch))
|
|
16964
18015
|
}));
|
|
16965
18016
|
}
|
|
16966
|
-
function buildFields(row, encoding, formatters, color) {
|
|
18017
|
+
function buildFields(row, encoding, formatters, color, titleChannel) {
|
|
16967
18018
|
if (encoding.tooltip) {
|
|
16968
18019
|
const channels = Array.isArray(encoding.tooltip) ? encoding.tooltip : [encoding.tooltip];
|
|
16969
18020
|
return buildExplicitTooltipFields(row, channels, formatters);
|
|
16970
18021
|
}
|
|
16971
18022
|
const fields = [];
|
|
18023
|
+
const skipped = [];
|
|
16972
18024
|
if (encoding.color && "field" in encoding.color) {
|
|
16973
|
-
fields.push({
|
|
18025
|
+
(titleChannel === "color" ? skipped : fields).push({
|
|
16974
18026
|
label: resolveLabel(encoding.color),
|
|
16975
18027
|
value: formatValue2(
|
|
16976
18028
|
row[encoding.color.field],
|
|
@@ -16981,7 +18033,7 @@ function buildFields(row, encoding, formatters, color) {
|
|
|
16981
18033
|
});
|
|
16982
18034
|
}
|
|
16983
18035
|
if (encoding.y) {
|
|
16984
|
-
fields.push({
|
|
18036
|
+
(titleChannel === "y" ? skipped : fields).push({
|
|
16985
18037
|
label: resolveLabel(encoding.y),
|
|
16986
18038
|
value: formatValue2(
|
|
16987
18039
|
row[encoding.y.field],
|
|
@@ -16992,7 +18044,7 @@ function buildFields(row, encoding, formatters, color) {
|
|
|
16992
18044
|
});
|
|
16993
18045
|
}
|
|
16994
18046
|
if (encoding.x) {
|
|
16995
|
-
fields.push({
|
|
18047
|
+
(titleChannel === "x" ? skipped : fields).push({
|
|
16996
18048
|
label: resolveLabel(encoding.x),
|
|
16997
18049
|
value: formatValue2(
|
|
16998
18050
|
row[encoding.x.field],
|
|
@@ -17011,7 +18063,7 @@ function buildFields(row, encoding, formatters, color) {
|
|
|
17011
18063
|
)
|
|
17012
18064
|
});
|
|
17013
18065
|
}
|
|
17014
|
-
return fields;
|
|
18066
|
+
return fields.length > 0 ? fields : skipped;
|
|
17015
18067
|
}
|
|
17016
18068
|
function computeEncodedFieldsSet(encoding) {
|
|
17017
18069
|
if (!(encoding.x?.type === "quantitative" && encoding.y?.type === "quantitative")) {
|
|
@@ -17025,50 +18077,67 @@ function computeEncodedFieldsSet(encoding) {
|
|
|
17025
18077
|
}
|
|
17026
18078
|
function getTooltipTitle(row, encoding, encodedFields) {
|
|
17027
18079
|
if (encoding.detail) {
|
|
17028
|
-
return String(row[encoding.detail.field] ?? "");
|
|
18080
|
+
return { title: String(row[encoding.detail.field] ?? "") };
|
|
17029
18081
|
}
|
|
17030
18082
|
if (encoding.x?.type === "temporal") {
|
|
17031
|
-
return formatValue2(row[encoding.x.field], "temporal");
|
|
18083
|
+
return { title: formatValue2(row[encoding.x.field], "temporal"), channel: "x" };
|
|
17032
18084
|
}
|
|
17033
18085
|
if (encoding.x?.type === "nominal" || encoding.x?.type === "ordinal") {
|
|
17034
|
-
return String(row[encoding.x.field] ?? "");
|
|
18086
|
+
return { title: String(row[encoding.x.field] ?? ""), channel: "x" };
|
|
17035
18087
|
}
|
|
17036
18088
|
if (encoding.y?.type === "nominal" || encoding.y?.type === "ordinal") {
|
|
17037
|
-
return String(row[encoding.y.field] ?? "");
|
|
18089
|
+
return { title: String(row[encoding.y.field] ?? ""), channel: "y" };
|
|
17038
18090
|
}
|
|
17039
18091
|
if (encoding.x?.type === "quantitative" && encoding.y?.type === "quantitative") {
|
|
17040
18092
|
const fields = encodedFields ?? computeEncodedFieldsSet(encoding);
|
|
17041
18093
|
for (const key of Object.keys(row)) {
|
|
17042
18094
|
const value = row[key];
|
|
17043
18095
|
if (!fields.has(key) && typeof value === "string") {
|
|
17044
|
-
return value;
|
|
18096
|
+
return { title: value };
|
|
17045
18097
|
}
|
|
17046
18098
|
}
|
|
17047
18099
|
}
|
|
17048
18100
|
if (encoding.color && "field" in encoding.color) {
|
|
17049
|
-
return String(row[encoding.color.field] ?? "");
|
|
18101
|
+
return { title: String(row[encoding.color.field] ?? ""), channel: "color" };
|
|
17050
18102
|
}
|
|
17051
|
-
return
|
|
18103
|
+
return {};
|
|
17052
18104
|
}
|
|
17053
|
-
function tooltipsForLine(mark, encoding, formatters, _markIndex, encodedFields) {
|
|
18105
|
+
function tooltipsForLine(mark, encoding, formatters, _markIndex, encodedFields, stackTotals) {
|
|
17054
18106
|
if (mark.dataPoints) {
|
|
17055
18107
|
for (const dp of mark.dataPoints) {
|
|
17056
|
-
dp.
|
|
17057
|
-
|
|
17058
|
-
|
|
17059
|
-
|
|
18108
|
+
const { title, channel } = getTooltipTitle(dp.datum, encoding, encodedFields);
|
|
18109
|
+
const fields = buildFields(dp.datum, encoding, formatters, mark.stroke, channel);
|
|
18110
|
+
if (stackTotals && encoding.x) {
|
|
18111
|
+
const total = stackTotals.get(stackKeyOf(dp.datum[encoding.x.field]));
|
|
18112
|
+
if (total) fields.push(total);
|
|
18113
|
+
}
|
|
18114
|
+
dp.tooltip = { title, fields };
|
|
17060
18115
|
}
|
|
17061
18116
|
}
|
|
17062
18117
|
return [];
|
|
17063
18118
|
}
|
|
17064
18119
|
function tooltipsForPoint(mark, encoding, formatters, markIndex, encodedFields) {
|
|
17065
|
-
const title = getTooltipTitle(mark.data, encoding, encodedFields);
|
|
17066
|
-
const fields = buildFields(
|
|
18120
|
+
const { title, channel } = getTooltipTitle(mark.data, encoding, encodedFields);
|
|
18121
|
+
const fields = buildFields(
|
|
18122
|
+
mark.data,
|
|
18123
|
+
encoding,
|
|
18124
|
+
formatters,
|
|
18125
|
+
getRepresentativeColor14(mark.fill),
|
|
18126
|
+
channel
|
|
18127
|
+
);
|
|
17067
18128
|
return [[`point-${markIndex}`, { title, fields }]];
|
|
17068
18129
|
}
|
|
17069
|
-
function tooltipsForRect(mark, encoding, formatters, markIndex, encodedFields) {
|
|
17070
|
-
const title = getTooltipTitle(mark.data, encoding, encodedFields);
|
|
17071
|
-
const fields = buildFields(
|
|
18130
|
+
function tooltipsForRect(mark, encoding, formatters, markIndex, encodedFields, stackTotals) {
|
|
18131
|
+
const { title, channel } = getTooltipTitle(mark.data, encoding, encodedFields);
|
|
18132
|
+
const fields = buildFields(
|
|
18133
|
+
mark.data,
|
|
18134
|
+
encoding,
|
|
18135
|
+
formatters,
|
|
18136
|
+
getRepresentativeColor14(mark.fill),
|
|
18137
|
+
channel
|
|
18138
|
+
);
|
|
18139
|
+
const total = mark.stackGroup !== void 0 ? stackTotals?.get(mark.stackGroup) : void 0;
|
|
18140
|
+
if (total) fields.push(total);
|
|
17072
18141
|
return [[`rect-${markIndex}`, { title, fields }]];
|
|
17073
18142
|
}
|
|
17074
18143
|
function tooltipsForArc(mark, encoding, formatters, markIndex) {
|
|
@@ -17085,7 +18154,7 @@ function tooltipsForArc(mark, encoding, formatters, markIndex) {
|
|
|
17085
18154
|
encoding.y.type,
|
|
17086
18155
|
getFormatter(formatters, encoding.y)
|
|
17087
18156
|
),
|
|
17088
|
-
color:
|
|
18157
|
+
color: getRepresentativeColor14(mark.fill)
|
|
17089
18158
|
});
|
|
17090
18159
|
}
|
|
17091
18160
|
} else if (encoding.y) {
|
|
@@ -17096,23 +18165,98 @@ function tooltipsForArc(mark, encoding, formatters, markIndex) {
|
|
|
17096
18165
|
encoding.y.type,
|
|
17097
18166
|
getFormatter(formatters, encoding.y)
|
|
17098
18167
|
),
|
|
17099
|
-
color:
|
|
18168
|
+
color: getRepresentativeColor14(mark.fill)
|
|
17100
18169
|
});
|
|
17101
18170
|
}
|
|
17102
18171
|
const title = colorEnc ? String(row[colorEnc.field] ?? "") : void 0;
|
|
17103
18172
|
return [[`arc-${markIndex}`, { title, fields }]];
|
|
17104
18173
|
}
|
|
17105
|
-
function tooltipsForArea(mark, encoding, formatters, _markIndex, encodedFields) {
|
|
18174
|
+
function tooltipsForArea(mark, encoding, formatters, _markIndex, encodedFields, stackTotals) {
|
|
17106
18175
|
if (mark.dataPoints) {
|
|
18176
|
+
const wantsTotal = mark.stacked === true && mark.stackNormalized !== true;
|
|
17107
18177
|
for (const dp of mark.dataPoints) {
|
|
17108
|
-
dp.
|
|
17109
|
-
|
|
17110
|
-
|
|
17111
|
-
|
|
18178
|
+
const { title, channel } = getTooltipTitle(dp.datum, encoding, encodedFields);
|
|
18179
|
+
const fields = buildFields(
|
|
18180
|
+
dp.datum,
|
|
18181
|
+
encoding,
|
|
18182
|
+
formatters,
|
|
18183
|
+
getRepresentativeColor14(mark.fill),
|
|
18184
|
+
channel
|
|
18185
|
+
);
|
|
18186
|
+
if (wantsTotal && encoding.x) {
|
|
18187
|
+
const total = stackTotals?.get(stackKeyOf(dp.datum[encoding.x.field]));
|
|
18188
|
+
if (total) fields.push(total);
|
|
18189
|
+
}
|
|
18190
|
+
dp.tooltip = { title, fields };
|
|
17112
18191
|
}
|
|
17113
18192
|
}
|
|
17114
18193
|
return [];
|
|
17115
18194
|
}
|
|
18195
|
+
var TOTAL_LABEL = "Total";
|
|
18196
|
+
function stackKeyOf(value) {
|
|
18197
|
+
return value instanceof Date ? String(value.getTime()) : String(value);
|
|
18198
|
+
}
|
|
18199
|
+
function stackValueChannel(encoding) {
|
|
18200
|
+
if (encoding.y?.type === "quantitative") return encoding.y;
|
|
18201
|
+
if (encoding.x?.type === "quantitative") return encoding.x;
|
|
18202
|
+
return void 0;
|
|
18203
|
+
}
|
|
18204
|
+
function isNormalizedStack(encoding) {
|
|
18205
|
+
return encoding.y?.stack === "normalize" || encoding.x?.stack === "normalize";
|
|
18206
|
+
}
|
|
18207
|
+
function computeRectStackTotals(marks, encoding, formatters) {
|
|
18208
|
+
if (isNormalizedStack(encoding)) return void 0;
|
|
18209
|
+
const valueCh = stackValueChannel(encoding);
|
|
18210
|
+
if (!valueCh) return void 0;
|
|
18211
|
+
const sums = /* @__PURE__ */ new Map();
|
|
18212
|
+
const counts = /* @__PURE__ */ new Map();
|
|
18213
|
+
for (const mark of marks) {
|
|
18214
|
+
if (mark.type !== "rect" || mark.stackGroup === void 0) continue;
|
|
18215
|
+
const value = Number(mark.data[valueCh.field]);
|
|
18216
|
+
if (!Number.isFinite(value)) continue;
|
|
18217
|
+
sums.set(mark.stackGroup, (sums.get(mark.stackGroup) ?? 0) + value);
|
|
18218
|
+
counts.set(mark.stackGroup, (counts.get(mark.stackGroup) ?? 0) + 1);
|
|
18219
|
+
}
|
|
18220
|
+
if (sums.size === 0) return void 0;
|
|
18221
|
+
const formatter = getFormatter(formatters, valueCh);
|
|
18222
|
+
const totals = /* @__PURE__ */ new Map();
|
|
18223
|
+
for (const [group, sum] of sums) {
|
|
18224
|
+
if ((counts.get(group) ?? 0) < 2) continue;
|
|
18225
|
+
totals.set(group, {
|
|
18226
|
+
label: TOTAL_LABEL,
|
|
18227
|
+
value: formatValue2(sum, valueCh.type, formatter),
|
|
18228
|
+
role: "total"
|
|
18229
|
+
});
|
|
18230
|
+
}
|
|
18231
|
+
return totals.size > 0 ? totals : void 0;
|
|
18232
|
+
}
|
|
18233
|
+
function computeAreaStackTotals(marks, encoding, formatters) {
|
|
18234
|
+
const xCh = encoding.x;
|
|
18235
|
+
const yCh = encoding.y;
|
|
18236
|
+
if (!xCh || !yCh) return void 0;
|
|
18237
|
+
const sums = /* @__PURE__ */ new Map();
|
|
18238
|
+
for (const mark of marks) {
|
|
18239
|
+
if (mark.type !== "area") continue;
|
|
18240
|
+
if (mark.stacked !== true || mark.stackNormalized === true) continue;
|
|
18241
|
+
for (const dp of mark.dataPoints ?? []) {
|
|
18242
|
+
const value = Number(dp.datum[yCh.field]);
|
|
18243
|
+
if (!Number.isFinite(value)) continue;
|
|
18244
|
+
const key = stackKeyOf(dp.datum[xCh.field]);
|
|
18245
|
+
sums.set(key, (sums.get(key) ?? 0) + value);
|
|
18246
|
+
}
|
|
18247
|
+
}
|
|
18248
|
+
if (sums.size === 0) return void 0;
|
|
18249
|
+
const formatter = getFormatter(formatters, yCh);
|
|
18250
|
+
const totals = /* @__PURE__ */ new Map();
|
|
18251
|
+
for (const [key, sum] of sums) {
|
|
18252
|
+
totals.set(key, {
|
|
18253
|
+
label: TOTAL_LABEL,
|
|
18254
|
+
value: formatValue2(sum, yCh.type, formatter),
|
|
18255
|
+
role: "total"
|
|
18256
|
+
});
|
|
18257
|
+
}
|
|
18258
|
+
return totals;
|
|
18259
|
+
}
|
|
17116
18260
|
function computeRangeTooltips(spec, marks) {
|
|
17117
18261
|
const encoding = spec.encoding;
|
|
17118
18262
|
const descriptors = /* @__PURE__ */ new Map();
|
|
@@ -17125,7 +18269,7 @@ function computeRangeTooltips(spec, marks) {
|
|
|
17125
18269
|
const startFmt = getFormatter(fmtCache, startCh);
|
|
17126
18270
|
const encodedFields = computeEncodedFieldsSet(encoding);
|
|
17127
18271
|
const contentFor = (row) => {
|
|
17128
|
-
const title = getTooltipTitle(row, encoding, encodedFields);
|
|
18272
|
+
const { title } = getTooltipTitle(row, encoding, encodedFields);
|
|
17129
18273
|
if (encoding.tooltip) {
|
|
17130
18274
|
const channels = Array.isArray(encoding.tooltip) ? encoding.tooltip : [encoding.tooltip];
|
|
17131
18275
|
return { title, fields: buildExplicitTooltipFields(row, channels, fmtCache) };
|
|
@@ -17187,7 +18331,7 @@ function computeCalendarTooltips(spec, marks) {
|
|
|
17187
18331
|
{
|
|
17188
18332
|
label: resolveLabel(colorEnc),
|
|
17189
18333
|
value: formatValue2(row[colorEnc.field], colorEnc.type, getFormatter(fmtCache, colorEnc)),
|
|
17190
|
-
color:
|
|
18334
|
+
color: getRepresentativeColor14(mark.fill)
|
|
17191
18335
|
}
|
|
17192
18336
|
]
|
|
17193
18337
|
});
|
|
@@ -17231,7 +18375,7 @@ function computeWaffleTooltips(spec, marks) {
|
|
|
17231
18375
|
{
|
|
17232
18376
|
label: resolveLabel(valueCh),
|
|
17233
18377
|
value: formatValue2(row[valueCh.field], valueCh.type, getFormatter(fmtCache, valueCh)),
|
|
17234
|
-
color:
|
|
18378
|
+
color: getRepresentativeColor14(mark.fill)
|
|
17235
18379
|
},
|
|
17236
18380
|
{ label: "Share", value: `${cellCounts.get(category) ?? 0} of ${units} units` }
|
|
17237
18381
|
]
|
|
@@ -17282,7 +18426,7 @@ function computeParliamentTooltips(spec, marks) {
|
|
|
17282
18426
|
{
|
|
17283
18427
|
label: resolveLabel(valueCh),
|
|
17284
18428
|
value: formatValue2(row[valueCh.field], valueCh.type, getFormatter(fmtCache, valueCh)),
|
|
17285
|
-
color:
|
|
18429
|
+
color: getRepresentativeColor14(mark.fill)
|
|
17286
18430
|
},
|
|
17287
18431
|
{ label: "Share", value: share }
|
|
17288
18432
|
]
|
|
@@ -17318,21 +18462,23 @@ function computeTooltipDescriptors(spec, marks) {
|
|
|
17318
18462
|
].filter((ch) => !!ch && "field" in ch);
|
|
17319
18463
|
const formatters = buildFormatterCache(spec.data, allChannels);
|
|
17320
18464
|
const encodedFields = computeEncodedFieldsSet(encoding);
|
|
18465
|
+
const rectStackTotals = computeRectStackTotals(marks, encoding, formatters);
|
|
18466
|
+
const areaStackTotals = computeAreaStackTotals(marks, encoding, formatters);
|
|
17321
18467
|
for (let i = 0; i < marks.length; i++) {
|
|
17322
18468
|
const mark = marks[i];
|
|
17323
18469
|
let entries = [];
|
|
17324
18470
|
switch (mark.type) {
|
|
17325
18471
|
case "line":
|
|
17326
|
-
entries = tooltipsForLine(mark, encoding, formatters, i, encodedFields);
|
|
18472
|
+
entries = tooltipsForLine(mark, encoding, formatters, i, encodedFields, areaStackTotals);
|
|
17327
18473
|
break;
|
|
17328
18474
|
case "area":
|
|
17329
|
-
entries = tooltipsForArea(mark, encoding, formatters, i, encodedFields);
|
|
18475
|
+
entries = tooltipsForArea(mark, encoding, formatters, i, encodedFields, areaStackTotals);
|
|
17330
18476
|
break;
|
|
17331
18477
|
case "point":
|
|
17332
18478
|
entries = tooltipsForPoint(mark, encoding, formatters, i, encodedFields);
|
|
17333
18479
|
break;
|
|
17334
18480
|
case "rect":
|
|
17335
|
-
entries = tooltipsForRect(mark, encoding, formatters, i, encodedFields);
|
|
18481
|
+
entries = tooltipsForRect(mark, encoding, formatters, i, encodedFields, rectStackTotals);
|
|
17336
18482
|
break;
|
|
17337
18483
|
case "arc":
|
|
17338
18484
|
entries = tooltipsForArc(mark, encoding, formatters, i);
|
|
@@ -17847,6 +18993,7 @@ function compileChart(spec, optionsInput) {
|
|
|
17847
18993
|
const breakpoint = getBreakpoint3(options.width);
|
|
17848
18994
|
const heightClass = getHeightClass(options.height);
|
|
17849
18995
|
let strategy = getLayoutStrategy(breakpoint, heightClass);
|
|
18996
|
+
const economy = resolveChromeEconomy(options.width, options.height);
|
|
17850
18997
|
const rawSpec = expandedSpec;
|
|
17851
18998
|
const overrides = rawSpec.overrides;
|
|
17852
18999
|
const rawEncoding = rawSpec.encoding;
|
|
@@ -18103,19 +19250,31 @@ function compileChart(spec, optionsInput) {
|
|
|
18103
19250
|
};
|
|
18104
19251
|
}
|
|
18105
19252
|
const scales = computeScales(renderSpec, scaleArea, renderSpec.data);
|
|
18106
|
-
|
|
18107
|
-
|
|
19253
|
+
emitSpecWarnings(
|
|
19254
|
+
applyColorScaleRange(
|
|
19255
|
+
scales,
|
|
19256
|
+
renderSpec.encoding,
|
|
19257
|
+
theme,
|
|
19258
|
+
renderSpec.highlight,
|
|
19259
|
+
chartSpec.markType
|
|
19260
|
+
),
|
|
19261
|
+
options.onWarn
|
|
19262
|
+
);
|
|
19263
|
+
scales.defaultColor = chartSpec.markDef.fill ?? chartSpec.markDef.stroke ?? defaultSeriesColor(theme, chartSpec.markType);
|
|
18108
19264
|
if (options.dev) {
|
|
18109
19265
|
emitSpecWarnings(collectContrastWarnings(scales, chartSpec.markType, theme), options.onWarn);
|
|
18110
19266
|
}
|
|
18111
19267
|
const isRadial = isAxislessMark3(chartSpec.markType);
|
|
18112
|
-
const
|
|
18113
|
-
const
|
|
19268
|
+
const axisless = chartSpec.display === "sparkline" || !economy.axes;
|
|
19269
|
+
const skipX = axisless && !chartSpec.userExplicit.xAxis;
|
|
19270
|
+
const skipY = axisless && !chartSpec.userExplicit.yAxis;
|
|
18114
19271
|
const axes = isRadial ? { x: void 0, y: void 0 } : computeAxes(scales, chartArea, strategy, theme, options.measureText, {
|
|
18115
19272
|
data: renderSpec.data,
|
|
18116
19273
|
encoding: renderSpec.encoding,
|
|
18117
19274
|
skipX,
|
|
18118
19275
|
skipY,
|
|
19276
|
+
suppressGrid: !economy.gridlines,
|
|
19277
|
+
maxXTicks: economy.maxXTicks,
|
|
18119
19278
|
markType: chartSpec.markType,
|
|
18120
19279
|
totalWidth: options.width,
|
|
18121
19280
|
// If the guardrail stripped chrome, the plan's y-ticks were sized
|
|
@@ -18186,6 +19345,17 @@ function compileChart(spec, optionsInput) {
|
|
|
18186
19345
|
fontFamily: theme.fonts.family,
|
|
18187
19346
|
autoThin: chartSpec.autoThin
|
|
18188
19347
|
});
|
|
19348
|
+
if (options.dev) {
|
|
19349
|
+
emitSpecWarnings(
|
|
19350
|
+
collectPinnedOverlapWarnings(
|
|
19351
|
+
annotations,
|
|
19352
|
+
chartSpec.annotations,
|
|
19353
|
+
obstacles,
|
|
19354
|
+
annotationMeasure
|
|
19355
|
+
),
|
|
19356
|
+
options.onWarn
|
|
19357
|
+
);
|
|
19358
|
+
}
|
|
18189
19359
|
let footnotes;
|
|
18190
19360
|
if (chartSpec.autoThin && annotations.length > 1) {
|
|
18191
19361
|
const result = thinAnnotations(
|
|
@@ -18378,8 +19548,14 @@ function compileFaceted(chartSpec, renderSpec, facetChannel, facetDirection, opt
|
|
|
18378
19548
|
encoding: panelEnc
|
|
18379
19549
|
};
|
|
18380
19550
|
const panelScales = computeScales(panelSpecWithDomains, gridPanel.area, panelData);
|
|
18381
|
-
applyColorScaleRange(
|
|
18382
|
-
|
|
19551
|
+
applyColorScaleRange(
|
|
19552
|
+
panelScales,
|
|
19553
|
+
panelSpecWithDomains.encoding,
|
|
19554
|
+
theme,
|
|
19555
|
+
renderSpec.highlight,
|
|
19556
|
+
chartSpec.markType
|
|
19557
|
+
);
|
|
19558
|
+
panelScales.defaultColor = chartSpec.markDef.fill ?? chartSpec.markDef.stroke ?? defaultSeriesColor(theme, chartSpec.markType);
|
|
18383
19559
|
if (options.dev && panelLayouts.length === 0) {
|
|
18384
19560
|
emitSpecWarnings(
|
|
18385
19561
|
collectContrastWarnings(panelScales, chartSpec.markType, theme),
|
|
@@ -18570,7 +19746,8 @@ function compileLayer2(spec, options) {
|
|
|
18570
19746
|
return compileLayer(expanded, resolvedOptions, compileChart);
|
|
18571
19747
|
}
|
|
18572
19748
|
function compileTable(spec, options) {
|
|
18573
|
-
const { spec: normalized } = compile(spec);
|
|
19749
|
+
const { spec: normalized, warnings: normalizeWarnings } = compile(spec);
|
|
19750
|
+
emitSpecWarnings(normalizeWarnings, options.onWarn);
|
|
18574
19751
|
const normType = "type" in normalized ? normalized.type : void 0;
|
|
18575
19752
|
if (normType !== "table") {
|
|
18576
19753
|
throw new Error(`compileTable received a non-table spec. Use compileChart instead.`);
|