@opendata-ai/openchart-engine 8.0.0-rc.2 → 8.0.0-rc.20
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 +140 -2
- package/dist/index.js +1403 -210
- package/dist/index.js.map +1 -1
- package/package.json +3 -4
- package/src/__test-fixtures__/specs.ts +0 -182
- package/src/__tests__/__snapshots__/compile-snapshot.test.ts.snap +0 -2339
- package/src/__tests__/annotation-behavior.test.ts +0 -721
- package/src/__tests__/axes.test.ts +0 -1579
- package/src/__tests__/compile-animation.test.ts +0 -161
- package/src/__tests__/compile-chart.test.ts +0 -1303
- package/src/__tests__/compile-footnotes.test.ts +0 -277
- package/src/__tests__/compile-layer.test.ts +0 -969
- package/src/__tests__/compile-snapshot.test.ts +0 -156
- package/src/__tests__/compound-labels.test.ts +0 -147
- package/src/__tests__/dimensions.test.ts +0 -918
- package/src/__tests__/encoding-sugar.test.ts +0 -390
- package/src/__tests__/facet.test.ts +0 -215
- package/src/__tests__/helpers/invariants.ts +0 -299
- package/src/__tests__/known-bugs.test.ts +0 -566
- package/src/__tests__/layout-fuzz.test.ts +0 -358
- package/src/__tests__/layout-invariants.test.ts +0 -535
- package/src/__tests__/legend.test.ts +0 -721
- package/src/__tests__/mark-keys.test.ts +0 -294
- package/src/__tests__/ranking-recipes.test.ts +0 -260
- package/src/__tests__/rotation.test.ts +0 -83
- package/src/__tests__/scales.test.ts +0 -323
- package/src/__tests__/series-search.test.ts +0 -92
- package/src/__tests__/spec-sugar.test.ts +0 -801
- package/src/__tests__/you-draw-it.test.ts +0 -160
- package/src/annotations/__tests__/compute.test.ts +0 -1672
- package/src/annotations/__tests__/geometry.test.ts +0 -66
- package/src/annotations/__tests__/placement-fuzz.test.ts +0 -256
- package/src/annotations/__tests__/placement-invariants.test.ts +0 -345
- package/src/annotations/__tests__/placement.test.ts +0 -316
- package/src/annotations/__tests__/resolve-text.test.ts +0 -788
- package/src/annotations/__tests__/rich-text.test.ts +0 -91
- package/src/annotations/__tests__/thinning.test.ts +0 -285
- package/src/annotations/collisions.ts +0 -326
- package/src/annotations/compute.ts +0 -333
- package/src/annotations/constants.ts +0 -162
- package/src/annotations/geometry.ts +0 -370
- package/src/annotations/move.ts +0 -135
- package/src/annotations/placement.ts +0 -636
- package/src/annotations/position.ts +0 -131
- package/src/annotations/resolve-range.ts +0 -117
- package/src/annotations/resolve-refline.ts +0 -157
- package/src/annotations/resolve-text.ts +0 -539
- package/src/annotations/rich-text.ts +0 -65
- package/src/annotations/thinning.ts +0 -190
- package/src/barlist/__tests__/compile-barlist.test.ts +0 -200
- package/src/barlist/compile-barlist.ts +0 -391
- package/src/barlist/types.ts +0 -28
- package/src/charts/__tests__/post-process.test.ts +0 -237
- package/src/charts/__tests__/registry.test.ts +0 -116
- package/src/charts/__tests__/utils.test.ts +0 -195
- package/src/charts/_shared/__tests__/density-filter.test.ts +0 -32
- package/src/charts/_shared/density-filter.ts +0 -26
- package/src/charts/_shared/format-label-value.ts +0 -16
- package/src/charts/bar/__tests__/compute.test.ts +0 -934
- package/src/charts/bar/__tests__/gradient-orient.test.ts +0 -127
- package/src/charts/bar/__tests__/labels.test.ts +0 -292
- package/src/charts/bar/compute.ts +0 -539
- package/src/charts/bar/index.ts +0 -60
- package/src/charts/bar/labels.ts +0 -311
- package/src/charts/beeswarm/__tests__/compute.test.ts +0 -280
- package/src/charts/beeswarm/__tests__/dodge.test.ts +0 -148
- package/src/charts/beeswarm/compute.ts +0 -203
- package/src/charts/beeswarm/dodge.ts +0 -155
- package/src/charts/beeswarm/index.ts +0 -32
- package/src/charts/builtin.ts +0 -75
- package/src/charts/calendar/__tests__/compute.test.ts +0 -417
- package/src/charts/calendar/compute.ts +0 -319
- package/src/charts/calendar/index.ts +0 -32
- package/src/charts/column/__tests__/compute.test.ts +0 -540
- package/src/charts/column/__tests__/labels.test.ts +0 -179
- package/src/charts/column/compute.ts +0 -493
- package/src/charts/column/index.ts +0 -53
- package/src/charts/column/labels.ts +0 -229
- package/src/charts/dot/__tests__/compute.test.ts +0 -380
- package/src/charts/dot/__tests__/labels.test.ts +0 -98
- package/src/charts/dot/compute.ts +0 -285
- package/src/charts/dot/index.ts +0 -62
- package/src/charts/dot/labels.ts +0 -121
- package/src/charts/line/__tests__/compute.test.ts +0 -1194
- package/src/charts/line/__tests__/curves.test.ts +0 -51
- package/src/charts/line/__tests__/labels.test.ts +0 -94
- package/src/charts/line/__tests__/mark-options.test.ts +0 -175
- package/src/charts/line/area.ts +0 -484
- package/src/charts/line/compute.ts +0 -356
- package/src/charts/line/curves.ts +0 -85
- package/src/charts/line/index.ts +0 -176
- package/src/charts/line/labels.ts +0 -177
- package/src/charts/parliament/__tests__/compute.test.ts +0 -411
- package/src/charts/parliament/compute.ts +0 -313
- package/src/charts/parliament/index.ts +0 -32
- package/src/charts/pie/__tests__/compute.test.ts +0 -384
- package/src/charts/pie/__tests__/labels.test.ts +0 -132
- package/src/charts/pie/compute.ts +0 -324
- package/src/charts/pie/index.ts +0 -49
- package/src/charts/pie/labels.ts +0 -143
- package/src/charts/post-process.ts +0 -229
- package/src/charts/range/__tests__/compute.test.ts +0 -486
- package/src/charts/range/compute.ts +0 -273
- package/src/charts/range/index.ts +0 -82
- package/src/charts/range/labels.ts +0 -169
- package/src/charts/rect/__tests__/compute.test.ts +0 -120
- package/src/charts/rect/index.ts +0 -127
- package/src/charts/registry.ts +0 -72
- package/src/charts/rule/index.ts +0 -139
- package/src/charts/scatter/__tests__/compute.test.ts +0 -488
- package/src/charts/scatter/__tests__/trendline.test.ts +0 -191
- package/src/charts/scatter/compute.ts +0 -192
- package/src/charts/scatter/index.ts +0 -46
- package/src/charts/scatter/trendline.ts +0 -100
- package/src/charts/text/__tests__/compute.test.ts +0 -307
- package/src/charts/text/index.ts +0 -141
- package/src/charts/tick/__tests__/compute.test.ts +0 -75
- package/src/charts/tick/index.ts +0 -99
- package/src/charts/utils.ts +0 -170
- package/src/charts/waffle/__tests__/compute.test.ts +0 -305
- package/src/charts/waffle/compute.ts +0 -197
- package/src/charts/waffle/index.ts +0 -31
- package/src/compile/__tests__/color-scale-range.test.ts +0 -158
- package/src/compile/__tests__/color-scale-reverse.test.ts +0 -89
- package/src/compile/__tests__/data-clip.test.ts +0 -127
- package/src/compile/__tests__/fill-patterns.test.ts +0 -154
- package/src/compile/__tests__/size-scale.test.ts +0 -108
- package/src/compile/__tests__/watermark-obstacle.test.ts +0 -93
- package/src/compile/color-scale-range.ts +0 -101
- package/src/compile/data-clip.ts +0 -48
- package/src/compile/fill-patterns.ts +0 -74
- package/src/compile/layer.ts +0 -626
- package/src/compile/size-scale.ts +0 -139
- package/src/compile/spec-sugar.ts +0 -758
- package/src/compile/watermark-obstacle.ts +0 -54
- package/src/compile/you-draw-it.ts +0 -123
- package/src/compile.ts +0 -1450
- package/src/compiler/__tests__/a11y-warnings.test.ts +0 -148
- package/src/compiler/__tests__/animation.test.ts +0 -177
- package/src/compiler/__tests__/compile.test.ts +0 -87
- package/src/compiler/__tests__/fixtures/vl-parity/axis-null.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/color-value.json +0 -20
- package/src/compiler/__tests__/fixtures/vl-parity/count-aggregate.json +0 -22
- package/src/compiler/__tests__/fixtures/vl-parity/data-values.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/layer-axis-null.json +0 -14
- package/src/compiler/__tests__/fixtures/vl-parity/layer-count.json +0 -34
- package/src/compiler/__tests__/fixtures/vl-parity/layer-data-values.json +0 -22
- package/src/compiler/__tests__/fixtures/vl-parity/layer-legend-null.json +0 -41
- package/src/compiler/__tests__/fixtures/vl-parity/layer-scheme.json +0 -38
- package/src/compiler/__tests__/fixtures/vl-parity/layer-sort.json +0 -28
- package/src/compiler/__tests__/fixtures/vl-parity/layer-theta.json +0 -28
- package/src/compiler/__tests__/fixtures/vl-parity/layer-title.json +0 -30
- package/src/compiler/__tests__/fixtures/vl-parity/layer-value-def.json +0 -29
- package/src/compiler/__tests__/fixtures/vl-parity/legend-null.json +0 -32
- package/src/compiler/__tests__/fixtures/vl-parity/scheme.json +0 -25
- package/src/compiler/__tests__/fixtures/vl-parity/sort-by-value.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/theta-arc.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/title-subtitle.json +0 -21
- package/src/compiler/__tests__/fixtures/vl-parity/width-height.json +0 -23
- package/src/compiler/__tests__/keys.test.ts +0 -83
- package/src/compiler/__tests__/normalize-highlight.test.ts +0 -103
- package/src/compiler/__tests__/normalize.test.ts +0 -399
- package/src/compiler/__tests__/sparkline-defaults.test.ts +0 -153
- package/src/compiler/__tests__/validate-did-you-mean.test.ts +0 -86
- package/src/compiler/__tests__/validate-highlight.test.ts +0 -112
- package/src/compiler/__tests__/validate-near-miss.test.ts +0 -89
- package/src/compiler/__tests__/validate.test.ts +0 -629
- package/src/compiler/__tests__/vl-parity.test.ts +0 -68
- package/src/compiler/a11y-warnings.ts +0 -110
- package/src/compiler/animation.ts +0 -142
- package/src/compiler/index.ts +0 -47
- package/src/compiler/keys.ts +0 -55
- package/src/compiler/normalize.ts +0 -733
- package/src/compiler/sparkline-defaults.ts +0 -138
- package/src/compiler/types.ts +0 -241
- package/src/compiler/validate.ts +0 -1810
- package/src/endpoint-labels/__tests__/compute.test.ts +0 -587
- package/src/endpoint-labels/compute.ts +0 -621
- package/src/endpoint-labels/constants.ts +0 -54
- package/src/endpoint-labels/format.ts +0 -60
- package/src/endpoint-labels/predict.ts +0 -139
- package/src/format/__tests__/field-format.test.ts +0 -164
- package/src/format/field-format.ts +0 -27
- package/src/geo/__tests__/compile-map.test.ts +0 -621
- package/src/geo/compile-map.ts +0 -856
- package/src/geo/join.ts +0 -115
- package/src/geo/projections.ts +0 -45
- package/src/geo/types.ts +0 -24
- package/src/graphs/__tests__/community.test.ts +0 -228
- package/src/graphs/__tests__/compile-graph.test.ts +0 -396
- package/src/graphs/__tests__/encoding.test.ts +0 -415
- package/src/graphs/community.ts +0 -92
- package/src/graphs/compile-graph.ts +0 -329
- package/src/graphs/encoding.ts +0 -356
- package/src/graphs/types.ts +0 -106
- package/src/index.ts +0 -177
- package/src/layout/__tests__/metrics.test.ts +0 -97
- package/src/layout/__tests__/scales-highlight.test.ts +0 -192
- package/src/layout/axes/rotation.ts +0 -152
- package/src/layout/axes/thinning.ts +0 -98
- package/src/layout/axes/ticks.ts +0 -409
- package/src/layout/axes.ts +0 -757
- package/src/layout/dimensions.ts +0 -912
- package/src/layout/facet.ts +0 -91
- package/src/layout/gridlines.ts +0 -84
- package/src/layout/metrics.ts +0 -132
- package/src/layout/plan.ts +0 -631
- package/src/layout/scales.ts +0 -1053
- package/src/layout/shared.ts +0 -44
- package/src/legend/__tests__/continuous.test.ts +0 -318
- package/src/legend/__tests__/redundancy.test.ts +0 -108
- package/src/legend/__tests__/size.test.ts +0 -348
- package/src/legend/__tests__/suppression.test.ts +0 -382
- package/src/legend/compute.ts +0 -747
- package/src/legend/continuous.ts +0 -327
- package/src/legend/size.ts +0 -407
- package/src/legend/suppression.ts +0 -226
- package/src/legend/wrap.ts +0 -117
- package/src/sankey/__tests__/compile-sankey.test.ts +0 -458
- package/src/sankey/__tests__/layout.test.ts +0 -165
- package/src/sankey/__tests__/node-label-wrap.test.ts +0 -114
- package/src/sankey/__tests__/node-sort.test.ts +0 -45
- package/src/sankey/compile-sankey.ts +0 -706
- package/src/sankey/layout.ts +0 -184
- package/src/sankey/types.ts +0 -41
- package/src/tables/__tests__/bar-column.test.ts +0 -147
- package/src/tables/__tests__/category-colors.test.ts +0 -230
- package/src/tables/__tests__/compile-table.test.ts +0 -236
- package/src/tables/__tests__/format-cells.test.ts +0 -143
- package/src/tables/__tests__/heatmap.test.ts +0 -148
- package/src/tables/__tests__/pagination.test.ts +0 -78
- package/src/tables/__tests__/search.test.ts +0 -94
- package/src/tables/__tests__/sort.test.ts +0 -107
- package/src/tables/__tests__/sparkline.test.ts +0 -122
- package/src/tables/bar-column.ts +0 -94
- package/src/tables/category-colors.ts +0 -78
- package/src/tables/compile-table.ts +0 -427
- package/src/tables/format-cells.ts +0 -115
- package/src/tables/heatmap.ts +0 -141
- package/src/tables/pagination.ts +0 -46
- package/src/tables/search.ts +0 -66
- package/src/tables/sort.ts +0 -69
- package/src/tables/sparkline.ts +0 -113
- package/src/tables/utils.ts +0 -16
- package/src/tilemap/__tests__/compile-tilemap.test.ts +0 -576
- package/src/tilemap/compile-tilemap.ts +0 -755
- package/src/tilemap/layout.ts +0 -172
- package/src/tilemap/types.ts +0 -33
- package/src/tooltips/__tests__/compute.test.ts +0 -582
- package/src/tooltips/compute.ts +0 -700
- package/src/transforms/__tests__/aggregate.test.ts +0 -159
- package/src/transforms/__tests__/bin.test.ts +0 -88
- package/src/transforms/__tests__/calculate.test.ts +0 -146
- package/src/transforms/__tests__/conditional.test.ts +0 -127
- package/src/transforms/__tests__/filter-relative.test.ts +0 -202
- package/src/transforms/__tests__/filter.test.ts +0 -59
- package/src/transforms/__tests__/fold.test.ts +0 -79
- package/src/transforms/__tests__/index.test.ts +0 -93
- package/src/transforms/__tests__/predicates.test.ts +0 -193
- package/src/transforms/__tests__/timeunit.test.ts +0 -129
- package/src/transforms/__tests__/window.test.ts +0 -286
- package/src/transforms/aggregate.ts +0 -131
- package/src/transforms/bin.ts +0 -87
- package/src/transforms/calculate.ts +0 -60
- package/src/transforms/conditional.ts +0 -46
- package/src/transforms/filter.ts +0 -122
- package/src/transforms/fold.ts +0 -49
- package/src/transforms/index.ts +0 -60
- package/src/transforms/predicates.ts +0 -123
- package/src/transforms/timeunit.ts +0 -88
- package/src/transforms/window.ts +0 -185
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
getHeightClass,
|
|
10
10
|
getLayoutStrategy,
|
|
11
11
|
isAxislessMark as isAxislessMark3,
|
|
12
|
+
MAX_SR_TABLE_ROWS,
|
|
12
13
|
resolveTheme as resolveTheme7
|
|
13
14
|
} from "@opendata-ai/openchart-core";
|
|
14
15
|
|
|
@@ -244,7 +245,10 @@ function refreshConnector(connector, bounds, markerRadius) {
|
|
|
244
245
|
}
|
|
245
246
|
const exit = connectorExit(bounds, endpoint.x, endpoint.y);
|
|
246
247
|
if (!exit) return void 0;
|
|
247
|
-
const from = {
|
|
248
|
+
const from = {
|
|
249
|
+
x: exit.x + (connector.fromOffset?.dx ?? 0),
|
|
250
|
+
y: exit.y + (connector.fromOffset?.dy ?? 0)
|
|
251
|
+
};
|
|
248
252
|
const gap = connectorPullbackGap(markerRadius, connector.arrow);
|
|
249
253
|
const cdx = endpoint.x - from.x;
|
|
250
254
|
const cdy = endpoint.y - from.y;
|
|
@@ -506,6 +510,10 @@ function resolveTextAnnotation(annotation, scales, chartArea, isDark, measure =
|
|
|
506
510
|
from,
|
|
507
511
|
to,
|
|
508
512
|
endpoint,
|
|
513
|
+
fromOffset: annotation.connectorOffset?.from ? {
|
|
514
|
+
dx: annotation.connectorOffset.from.dx ?? 0,
|
|
515
|
+
dy: annotation.connectorOffset.from.dy ?? 0
|
|
516
|
+
} : void 0,
|
|
509
517
|
stroke: connectorStroke,
|
|
510
518
|
style: connectorStyle,
|
|
511
519
|
arrow: connectorArrow,
|
|
@@ -5545,7 +5553,9 @@ var SIZE_SCALE_DEFAULTS = {
|
|
|
5545
5553
|
* Text glyphs. Linear, not sqrt -- a word reads by its height, which is
|
|
5546
5554
|
* linear in font size, where a disc reads by its area.
|
|
5547
5555
|
*/
|
|
5548
|
-
text: { curve: "linear", range: [11, 32] }
|
|
5556
|
+
text: { curve: "linear", range: [11, 32] },
|
|
5557
|
+
/** Map point dots. sqrt for area-proportional, tighter range than scatter. */
|
|
5558
|
+
mapPoint: { curve: "sqrt", range: [3, 20] }
|
|
5549
5559
|
};
|
|
5550
5560
|
function sizeScaleDefaultsFor(markType) {
|
|
5551
5561
|
const found = SIZE_SCALE_DEFAULTS[markType];
|
|
@@ -9078,7 +9088,7 @@ function resolvePosition2(value2, channelType, scale) {
|
|
|
9078
9088
|
}
|
|
9079
9089
|
}
|
|
9080
9090
|
}
|
|
9081
|
-
function computeScatterMarks(spec, scales, _chartArea, _strategy) {
|
|
9091
|
+
function computeScatterMarks(spec, scales, _chartArea, _strategy, theme) {
|
|
9082
9092
|
const encoding = spec.encoding;
|
|
9083
9093
|
const xChannel = encoding.x;
|
|
9084
9094
|
const yChannel = encoding.y;
|
|
@@ -9096,6 +9106,10 @@ function computeScatterMarks(spec, scales, _chartArea, _strategy) {
|
|
|
9096
9106
|
const sizeField = sizeEnc?.field;
|
|
9097
9107
|
const resolvedSize = buildSizeScale(sizeEnc, spec.data, SIZE_SCALE_DEFAULTS.point);
|
|
9098
9108
|
const sizeScale = resolvedSize?.scale;
|
|
9109
|
+
const constFill = spec.markDef.fill;
|
|
9110
|
+
const constSize = spec.markDef.size;
|
|
9111
|
+
const bg = theme?.colors?.background;
|
|
9112
|
+
const defaultStroke = bg && bg !== "transparent" ? bg : "#ffffff";
|
|
9099
9113
|
const keyEnc = encoding.key && "field" in encoding.key ? encoding.key : void 0;
|
|
9100
9114
|
const keyField = keyEnc?.field;
|
|
9101
9115
|
const marks = [];
|
|
@@ -9110,10 +9124,12 @@ function computeScatterMarks(spec, scales, _chartArea, _strategy) {
|
|
|
9110
9124
|
if (isSequentialColor && colorField) {
|
|
9111
9125
|
const val = Number(row[colorField]);
|
|
9112
9126
|
color2 = Number.isFinite(val) ? getSequentialColor(scales, val) : getColor(scales, "__default__");
|
|
9113
|
-
} else {
|
|
9127
|
+
} else if (colorField) {
|
|
9114
9128
|
color2 = getColor(scales, category ?? "__default__");
|
|
9129
|
+
} else {
|
|
9130
|
+
color2 = constFill ?? getColor(scales, "__default__");
|
|
9115
9131
|
}
|
|
9116
|
-
let radius = DEFAULT_POINT_RADIUS2;
|
|
9132
|
+
let radius = constSize != null ? constSize : DEFAULT_POINT_RADIUS2;
|
|
9117
9133
|
if (sizeScale && sizeField) {
|
|
9118
9134
|
const sizeVal = Number(row[sizeField]);
|
|
9119
9135
|
if (Number.isFinite(sizeVal)) {
|
|
@@ -9134,9 +9150,9 @@ function computeScatterMarks(spec, scales, _chartArea, _strategy) {
|
|
|
9134
9150
|
cy,
|
|
9135
9151
|
r: radius,
|
|
9136
9152
|
fill: color2,
|
|
9137
|
-
stroke:
|
|
9138
|
-
strokeWidth: 1,
|
|
9139
|
-
fillOpacity: 0.7,
|
|
9153
|
+
stroke: spec.markDef.stroke ?? defaultStroke,
|
|
9154
|
+
strokeWidth: spec.markDef.strokeWidth ?? 1,
|
|
9155
|
+
fillOpacity: spec.markDef.opacity ?? 0.7,
|
|
9140
9156
|
data: row,
|
|
9141
9157
|
aria
|
|
9142
9158
|
});
|
|
@@ -9177,7 +9193,7 @@ function linearRegression(points) {
|
|
|
9177
9193
|
const intercept = (sumY - slope * sumX) / n;
|
|
9178
9194
|
return { slope, intercept };
|
|
9179
9195
|
}
|
|
9180
|
-
function computeTrendLine(marks) {
|
|
9196
|
+
function computeTrendLine(marks, stroke = TRENDLINE_COLOR, strokeWidth = TRENDLINE_STROKE_WIDTH) {
|
|
9181
9197
|
if (marks.length < 2) return null;
|
|
9182
9198
|
const points = marks.map((m) => ({ x: m.cx, y: m.cy }));
|
|
9183
9199
|
const result = linearRegression(points);
|
|
@@ -9200,8 +9216,8 @@ function computeTrendLine(marks) {
|
|
|
9200
9216
|
{ x: minX, y: y12 },
|
|
9201
9217
|
{ x: maxX, y: y2 }
|
|
9202
9218
|
],
|
|
9203
|
-
stroke
|
|
9204
|
-
strokeWidth
|
|
9219
|
+
stroke,
|
|
9220
|
+
strokeWidth,
|
|
9205
9221
|
strokeDasharray: TRENDLINE_DASH,
|
|
9206
9222
|
data: [],
|
|
9207
9223
|
aria
|
|
@@ -9209,13 +9225,20 @@ function computeTrendLine(marks) {
|
|
|
9209
9225
|
}
|
|
9210
9226
|
|
|
9211
9227
|
// src/charts/scatter/index.ts
|
|
9212
|
-
var scatterRenderer = (spec, scales, chartArea, strategy,
|
|
9213
|
-
const pointMarks = computeScatterMarks(spec, scales, chartArea, strategy);
|
|
9228
|
+
var scatterRenderer = (spec, scales, chartArea, strategy, theme) => {
|
|
9229
|
+
const pointMarks = computeScatterMarks(spec, scales, chartArea, strategy, theme);
|
|
9214
9230
|
const marks = [...pointMarks];
|
|
9215
|
-
|
|
9216
|
-
|
|
9231
|
+
const trendline = spec.markDef?.trendline;
|
|
9232
|
+
if (trendline !== false) {
|
|
9233
|
+
const config = typeof trendline === "object" ? trendline : void 0;
|
|
9234
|
+
const stroke = config?.stroke ?? theme?.colors?.text;
|
|
9235
|
+
const trendLine = computeTrendLine(pointMarks, stroke, config?.strokeWidth);
|
|
9217
9236
|
if (trendLine) {
|
|
9218
|
-
|
|
9237
|
+
if (config?.layer === "above") {
|
|
9238
|
+
marks.push(trendLine);
|
|
9239
|
+
} else {
|
|
9240
|
+
marks.unshift(trendLine);
|
|
9241
|
+
}
|
|
9219
9242
|
}
|
|
9220
9243
|
}
|
|
9221
9244
|
return marks;
|
|
@@ -9837,7 +9860,16 @@ function expandChannelSugar(spec, warnings) {
|
|
|
9837
9860
|
}
|
|
9838
9861
|
return result;
|
|
9839
9862
|
}
|
|
9840
|
-
var COUNT_GROUP_CHANNELS = [
|
|
9863
|
+
var COUNT_GROUP_CHANNELS = [
|
|
9864
|
+
"x",
|
|
9865
|
+
"y",
|
|
9866
|
+
"color",
|
|
9867
|
+
"detail",
|
|
9868
|
+
"strokeDash",
|
|
9869
|
+
"facet",
|
|
9870
|
+
"row",
|
|
9871
|
+
"column"
|
|
9872
|
+
];
|
|
9841
9873
|
function expandCountAggregate(spec) {
|
|
9842
9874
|
const encoding = spec.encoding;
|
|
9843
9875
|
if (!encoding || typeof encoding !== "object") return spec;
|
|
@@ -10072,6 +10104,12 @@ function resolveAnimation(spec) {
|
|
|
10072
10104
|
const enter = resolvePhase(config.enter, ENTER_DEFAULTS);
|
|
10073
10105
|
const update = resolvePhase(config.update, UPDATE_DEFAULTS);
|
|
10074
10106
|
const exit = resolvePhase(config.exit, EXIT_DEFAULTS);
|
|
10107
|
+
if (update && config.update && typeof config.update === "object" && !Array.isArray(config.update)) {
|
|
10108
|
+
const cap = config.update.maxMarks;
|
|
10109
|
+
if (typeof cap === "number" && Number.isFinite(cap) && cap >= 1) {
|
|
10110
|
+
update.maxMarks = Math.floor(cap);
|
|
10111
|
+
}
|
|
10112
|
+
}
|
|
10075
10113
|
if (!enter && !update && !exit) return void 0;
|
|
10076
10114
|
return {
|
|
10077
10115
|
enter: enter ?? void 0,
|
|
@@ -10464,6 +10502,7 @@ function normalizeChartSpec(spec, warnings) {
|
|
|
10464
10502
|
data: spec.data,
|
|
10465
10503
|
encoding,
|
|
10466
10504
|
chrome: normalizeChrome(spec.chrome),
|
|
10505
|
+
chromeLayout: spec.chromeLayout,
|
|
10467
10506
|
metrics: spec.metrics,
|
|
10468
10507
|
seriesSearch: normalizeSeriesSearch(spec, encoding, warnings),
|
|
10469
10508
|
youDrawIt: normalizeYouDrawIt(spec),
|
|
@@ -10538,15 +10577,10 @@ function normalizeSankeySpec(spec, _warnings) {
|
|
|
10538
10577
|
};
|
|
10539
10578
|
}
|
|
10540
10579
|
function normalizeGraphSpec(spec) {
|
|
10541
|
-
const
|
|
10580
|
+
const layout = {
|
|
10542
10581
|
type: "force",
|
|
10543
|
-
chargeStrength: -300,
|
|
10544
|
-
linkDistance: 30
|
|
10545
|
-
};
|
|
10546
|
-
const layout = spec.layout ? {
|
|
10547
|
-
...defaultLayout,
|
|
10548
10582
|
...spec.layout
|
|
10549
|
-
}
|
|
10583
|
+
};
|
|
10550
10584
|
return {
|
|
10551
10585
|
type: "graph",
|
|
10552
10586
|
nodes: spec.nodes,
|
|
@@ -10558,7 +10592,10 @@ function normalizeGraphSpec(spec) {
|
|
|
10558
10592
|
annotations: normalizeAnnotations(spec.annotations),
|
|
10559
10593
|
theme: spec.theme ?? {},
|
|
10560
10594
|
darkMode: spec.darkMode ?? "off",
|
|
10561
|
-
watermark: spec.watermark ?? true
|
|
10595
|
+
watermark: spec.watermark ?? true,
|
|
10596
|
+
animation: spec.animation,
|
|
10597
|
+
interaction: spec.interaction,
|
|
10598
|
+
legend: spec.legend
|
|
10562
10599
|
};
|
|
10563
10600
|
}
|
|
10564
10601
|
function normalizeTileMapSpec(spec, warnings) {
|
|
@@ -10631,7 +10668,8 @@ function normalizeMapSpec(spec, _warnings) {
|
|
|
10631
10668
|
darkMode: spec.darkMode ?? "off",
|
|
10632
10669
|
watermark: spec.watermark ?? true,
|
|
10633
10670
|
animation: spec.animation,
|
|
10634
|
-
valueFormat: spec.valueFormat
|
|
10671
|
+
valueFormat: spec.valueFormat,
|
|
10672
|
+
points: spec.points
|
|
10635
10673
|
};
|
|
10636
10674
|
}
|
|
10637
10675
|
function normalizeBarListSpec(spec, _warnings) {
|
|
@@ -10726,6 +10764,7 @@ import {
|
|
|
10726
10764
|
MARK_ENCODING_RULES,
|
|
10727
10765
|
MARK_TYPES,
|
|
10728
10766
|
resolveSchemeName as resolveSchemeName2,
|
|
10767
|
+
SEQUENTIAL_PALETTES,
|
|
10729
10768
|
SUPPORTED_SCHEME_NAMES
|
|
10730
10769
|
} from "@opendata-ai/openchart-core";
|
|
10731
10770
|
var VALID_FIELD_TYPES = /* @__PURE__ */ new Set(["quantitative", "temporal", "nominal", "ordinal"]);
|
|
@@ -10784,6 +10823,53 @@ function didYouMean(field, columns) {
|
|
|
10784
10823
|
const match = nearestColumn(field, columns);
|
|
10785
10824
|
return match ? ` Did you mean "${match}"?` : "";
|
|
10786
10825
|
}
|
|
10826
|
+
function checkSchemeName(channelObj, channelPath, errors) {
|
|
10827
|
+
const scale = channelObj.scale;
|
|
10828
|
+
if (scale && typeof scale.scheme === "string" && !resolveSchemeName2(scale.scheme)) {
|
|
10829
|
+
errors.push({
|
|
10830
|
+
message: `Spec error: ${channelPath}.scale.scheme "${scale.scheme}" is not a supported scheme name`,
|
|
10831
|
+
path: `${channelPath}.scale.scheme`,
|
|
10832
|
+
code: "INVALID_VALUE",
|
|
10833
|
+
suggestion: `Use one of the supported scheme names: ${[...SUPPORTED_SCHEME_NAMES].join(", ")}. Or provide explicit colors via scale.range.`
|
|
10834
|
+
});
|
|
10835
|
+
}
|
|
10836
|
+
}
|
|
10837
|
+
function checkSchemeUnused(encoding, errors, familyNote, suggestion) {
|
|
10838
|
+
for (const [channel, ch] of Object.entries(encoding)) {
|
|
10839
|
+
if (!ch || typeof ch !== "object" || Array.isArray(ch)) continue;
|
|
10840
|
+
const scale = ch.scale;
|
|
10841
|
+
if (scale && typeof scale.scheme === "string") {
|
|
10842
|
+
errors.push({
|
|
10843
|
+
message: `Spec error: encoding.${channel}.scale.scheme has no effect \u2014 ${familyNote}`,
|
|
10844
|
+
path: `encoding.${channel}.scale.scheme`,
|
|
10845
|
+
code: "INVALID_VALUE",
|
|
10846
|
+
suggestion
|
|
10847
|
+
});
|
|
10848
|
+
}
|
|
10849
|
+
}
|
|
10850
|
+
}
|
|
10851
|
+
var MAP_PALETTE_NAMES = Object.keys(SEQUENTIAL_PALETTES).join(", ");
|
|
10852
|
+
function checkMapSchemeName(channelObj, channelPath, errors) {
|
|
10853
|
+
const scale = channelObj.scale;
|
|
10854
|
+
if (!scale || typeof scale.scheme !== "string") return;
|
|
10855
|
+
if (channelObj.type === "nominal" || channelObj.type === "ordinal") {
|
|
10856
|
+
errors.push({
|
|
10857
|
+
message: `Spec error: ${channelPath}.scale.scheme has no effect \u2014 categorical map colors come from scale.range and scale.domain`,
|
|
10858
|
+
path: `${channelPath}.scale.scheme`,
|
|
10859
|
+
code: "INVALID_VALUE",
|
|
10860
|
+
suggestion: "Remove scale.scheme. To customize categorical map colors, provide explicit colors via scale.range (with scale.domain for category order)."
|
|
10861
|
+
});
|
|
10862
|
+
return;
|
|
10863
|
+
}
|
|
10864
|
+
if (!(scale.scheme in SEQUENTIAL_PALETTES)) {
|
|
10865
|
+
errors.push({
|
|
10866
|
+
message: `Spec error: ${channelPath}.scale.scheme "${scale.scheme}" is not a supported map palette`,
|
|
10867
|
+
path: `${channelPath}.scale.scheme`,
|
|
10868
|
+
code: "INVALID_VALUE",
|
|
10869
|
+
suggestion: `Maps support the sequential palettes: ${MAP_PALETTE_NAMES}. The default is "blue".`
|
|
10870
|
+
});
|
|
10871
|
+
}
|
|
10872
|
+
}
|
|
10787
10873
|
var VALID_RANGE_STYLES = /* @__PURE__ */ new Set(["dumbbell", "arrow", "bar"]);
|
|
10788
10874
|
function validateRangeSpec(spec, errors) {
|
|
10789
10875
|
const encoding = spec.encoding;
|
|
@@ -10942,6 +11028,21 @@ function validateCalendarSpec(spec, errors) {
|
|
|
10942
11028
|
seenDays.set(dayKey, i);
|
|
10943
11029
|
}
|
|
10944
11030
|
}
|
|
11031
|
+
var VALID_RENDER_MODES = /* @__PURE__ */ new Set(["auto", "svg", "canvas"]);
|
|
11032
|
+
function validateMarkRenderMode(spec, errors) {
|
|
11033
|
+
const mark = spec.mark;
|
|
11034
|
+
if (!mark || typeof mark !== "object" || Array.isArray(mark)) return;
|
|
11035
|
+
const render = mark.render;
|
|
11036
|
+
if (render === void 0) return;
|
|
11037
|
+
if (typeof render !== "string" || !VALID_RENDER_MODES.has(render)) {
|
|
11038
|
+
errors.push({
|
|
11039
|
+
message: `Spec error: mark.render ${JSON.stringify(render)} is not valid. Must be one of: auto, svg, canvas`,
|
|
11040
|
+
path: "mark.render",
|
|
11041
|
+
code: "INVALID_VALUE",
|
|
11042
|
+
suggestion: 'Use mark: { type: "point", render: "auto" } (default), "canvas" to force the canvas mark layer, or "svg" to force one circle per point.'
|
|
11043
|
+
});
|
|
11044
|
+
}
|
|
11045
|
+
}
|
|
10945
11046
|
function validateChartSpec(spec, errors) {
|
|
10946
11047
|
const markType = typeof spec.mark === "string" ? spec.mark : spec.mark?.type;
|
|
10947
11048
|
if (!Array.isArray(spec.data)) {
|
|
@@ -11047,6 +11148,7 @@ function validateChartSpec(spec, errors) {
|
|
|
11047
11148
|
if (markType === "calendar") {
|
|
11048
11149
|
validateCalendarSpec(spec, errors);
|
|
11049
11150
|
}
|
|
11151
|
+
validateMarkRenderMode(spec, errors);
|
|
11050
11152
|
if (Array.isArray(spec.transform)) {
|
|
11051
11153
|
const transforms = spec.transform;
|
|
11052
11154
|
for (let i = 0; i < transforms.length; i++) {
|
|
@@ -11145,15 +11247,7 @@ function validateChartSpec(spec, errors) {
|
|
|
11145
11247
|
suggestion: `Use one of: ${[...VALID_FIELD_TYPES].join(", ")}`
|
|
11146
11248
|
});
|
|
11147
11249
|
}
|
|
11148
|
-
|
|
11149
|
-
if (channelScale && typeof channelScale.scheme === "string" && !resolveSchemeName2(channelScale.scheme)) {
|
|
11150
|
-
errors.push({
|
|
11151
|
-
message: `Spec error: encoding.${channel}.scale.scheme "${channelScale.scheme}" is not a supported scheme name`,
|
|
11152
|
-
path: `encoding.${channel}.scale.scheme`,
|
|
11153
|
-
code: "INVALID_VALUE",
|
|
11154
|
-
suggestion: `Use one of the supported scheme names: ${[...SUPPORTED_SCHEME_NAMES].join(", ")}. Or provide explicit colors via scale.range.`
|
|
11155
|
-
});
|
|
11156
|
-
}
|
|
11250
|
+
checkSchemeName(channelObj, `encoding.${channel}`, errors);
|
|
11157
11251
|
if (channelRule && channelObj.type && channelRule.allowedTypes.length > 0) {
|
|
11158
11252
|
if (!channelRule.allowedTypes.includes(channelObj.type)) {
|
|
11159
11253
|
errors.push({
|
|
@@ -11267,6 +11361,52 @@ function validateChartSpec(spec, errors) {
|
|
|
11267
11361
|
}
|
|
11268
11362
|
}
|
|
11269
11363
|
}
|
|
11364
|
+
for (const channel of ["row", "column"]) {
|
|
11365
|
+
const ch = encoding[channel];
|
|
11366
|
+
if (ch && typeof ch === "object") {
|
|
11367
|
+
const facetCh = ch;
|
|
11368
|
+
if (!facetCh.field || typeof facetCh.field !== "string") {
|
|
11369
|
+
errors.push({
|
|
11370
|
+
message: `Spec error: encoding.${channel}.field is required and must be a non-empty string`,
|
|
11371
|
+
path: `encoding.${channel}.field`,
|
|
11372
|
+
code: "MISSING_FIELD",
|
|
11373
|
+
suggestion: `Add a field property to encoding.${channel} (e.g. { field: "category", type: "nominal" })`
|
|
11374
|
+
});
|
|
11375
|
+
} else if (!dataColumns.has(facetCh.field) && !transformFields.has(facetCh.field)) {
|
|
11376
|
+
errors.push({
|
|
11377
|
+
message: `Spec error: encoding.${channel}.field "${facetCh.field}" does not exist in data. Available columns: ${availableColumns}`,
|
|
11378
|
+
path: `encoding.${channel}.field`,
|
|
11379
|
+
code: "DATA_FIELD_MISSING",
|
|
11380
|
+
suggestion: `Use one of the available data columns: ${availableColumns}.${didYouMean(facetCh.field, columnList)}`
|
|
11381
|
+
});
|
|
11382
|
+
}
|
|
11383
|
+
if (!facetCh.type || facetCh.type !== "nominal" && facetCh.type !== "ordinal") {
|
|
11384
|
+
errors.push({
|
|
11385
|
+
message: `Spec error: encoding.${channel}.type must be "nominal" or "ordinal"${facetCh.type ? `, got "${facetCh.type}"` : ""}`,
|
|
11386
|
+
path: `encoding.${channel}.type`,
|
|
11387
|
+
code: facetCh.type ? "INVALID_VALUE" : "MISSING_FIELD",
|
|
11388
|
+
suggestion: `Set encoding.${channel}.type to "nominal" or "ordinal"`
|
|
11389
|
+
});
|
|
11390
|
+
}
|
|
11391
|
+
if (encoding.facet && typeof encoding.facet === "object") {
|
|
11392
|
+
errors.push({
|
|
11393
|
+
message: `Spec error: encoding.${channel} and encoding.facet cannot be used together`,
|
|
11394
|
+
path: `encoding.${channel}`,
|
|
11395
|
+
code: "INVALID_VALUE",
|
|
11396
|
+
suggestion: `Use either encoding.${channel} or encoding.facet, not both`
|
|
11397
|
+
});
|
|
11398
|
+
}
|
|
11399
|
+
const otherChannel = channel === "row" ? "column" : "row";
|
|
11400
|
+
if (encoding[otherChannel]) {
|
|
11401
|
+
errors.push({
|
|
11402
|
+
message: "Spec error: encoding.row and encoding.column cannot be used together (cross-product faceting is not yet supported)",
|
|
11403
|
+
path: `encoding.${channel}`,
|
|
11404
|
+
code: "INVALID_VALUE",
|
|
11405
|
+
suggestion: "Use encoding.facet with columns for grid layout, or use only row or column"
|
|
11406
|
+
});
|
|
11407
|
+
}
|
|
11408
|
+
}
|
|
11409
|
+
}
|
|
11270
11410
|
if (spec.darkMode !== void 0 && !VALID_DARK_MODES.has(spec.darkMode)) {
|
|
11271
11411
|
errors.push({
|
|
11272
11412
|
message: `Spec error: darkMode must be "auto", "force", or "off"`,
|
|
@@ -11479,7 +11619,7 @@ function validateGraphSpec(spec, errors) {
|
|
|
11479
11619
|
message: `Spec error: edges[${i}].source "${edge.source}" does not reference an existing node id`,
|
|
11480
11620
|
path: `edges[${i}].source`,
|
|
11481
11621
|
code: "DATA_FIELD_MISSING",
|
|
11482
|
-
suggestion: `Use one of the existing node ids: ${[...nodeIds].slice(0, 5).join(", ")}${nodeIds.size > 5 ? "..." : ""}`
|
|
11622
|
+
suggestion: `Use one of the existing node ids: ${[...nodeIds].slice(0, 5).join(", ")}${nodeIds.size > 5 ? "..." : ""}.${didYouMean(edge.source, [...nodeIds])}`
|
|
11483
11623
|
});
|
|
11484
11624
|
}
|
|
11485
11625
|
if (typeof edge.target !== "string" || edge.target === "") {
|
|
@@ -11494,40 +11634,60 @@ function validateGraphSpec(spec, errors) {
|
|
|
11494
11634
|
message: `Spec error: edges[${i}].target "${edge.target}" does not reference an existing node id`,
|
|
11495
11635
|
path: `edges[${i}].target`,
|
|
11496
11636
|
code: "DATA_FIELD_MISSING",
|
|
11497
|
-
suggestion: `Use one of the existing node ids: ${[...nodeIds].slice(0, 5).join(", ")}${nodeIds.size > 5 ? "..." : ""}`
|
|
11637
|
+
suggestion: `Use one of the existing node ids: ${[...nodeIds].slice(0, 5).join(", ")}${nodeIds.size > 5 ? "..." : ""}.${didYouMean(edge.target, [...nodeIds])}`
|
|
11498
11638
|
});
|
|
11499
11639
|
}
|
|
11500
11640
|
}
|
|
11501
11641
|
if (spec.encoding && typeof spec.encoding === "object") {
|
|
11502
11642
|
const encoding = spec.encoding;
|
|
11503
|
-
const
|
|
11504
|
-
const
|
|
11505
|
-
|
|
11506
|
-
|
|
11507
|
-
|
|
11643
|
+
const nodeFields = /* @__PURE__ */ new Set();
|
|
11644
|
+
for (const node of nodes) {
|
|
11645
|
+
if (node && typeof node === "object") {
|
|
11646
|
+
for (const key of Object.keys(node)) nodeFields.add(key);
|
|
11647
|
+
}
|
|
11648
|
+
}
|
|
11649
|
+
const edgeFields = /* @__PURE__ */ new Set();
|
|
11650
|
+
for (const edge of edges) {
|
|
11651
|
+
if (edge && typeof edge === "object") {
|
|
11652
|
+
for (const key of Object.keys(edge)) edgeFields.add(key);
|
|
11653
|
+
}
|
|
11654
|
+
}
|
|
11655
|
+
const nodeChannels = [
|
|
11656
|
+
"nodeColor",
|
|
11657
|
+
"nodeSize",
|
|
11658
|
+
"nodeOpacity",
|
|
11659
|
+
"nodeLabel",
|
|
11660
|
+
"nodeLabelPriority"
|
|
11661
|
+
];
|
|
11508
11662
|
for (const channel of nodeChannels) {
|
|
11509
11663
|
const ch = encoding[channel];
|
|
11510
|
-
if (ch?.field && typeof ch.field === "string" && !nodeFields.has(ch.field)) {
|
|
11664
|
+
if (ch?.field && typeof ch.field === "string" && nodes.length > 0 && !nodeFields.has(ch.field)) {
|
|
11511
11665
|
errors.push({
|
|
11512
11666
|
message: `Spec error: encoding.${channel}.field "${ch.field}" does not exist on nodes. Available fields: ${[...nodeFields].join(", ")}`,
|
|
11513
11667
|
path: `encoding.${channel}.field`,
|
|
11514
11668
|
code: "DATA_FIELD_MISSING",
|
|
11515
|
-
suggestion: `Use one of the node fields: ${[...nodeFields].join(", ")}`
|
|
11669
|
+
suggestion: `Use one of the node fields: ${[...nodeFields].join(", ")}.${didYouMean(ch.field, [...nodeFields])}`
|
|
11516
11670
|
});
|
|
11517
11671
|
}
|
|
11518
11672
|
}
|
|
11519
|
-
const edgeChannels = ["edgeColor", "edgeWidth"];
|
|
11673
|
+
const edgeChannels = ["edgeColor", "edgeWidth", "edgeStyle"];
|
|
11520
11674
|
for (const channel of edgeChannels) {
|
|
11521
11675
|
const ch = encoding[channel];
|
|
11522
|
-
if (ch?.field && typeof ch.field === "string" &&
|
|
11676
|
+
if (ch?.field && typeof ch.field === "string" && edges.length > 0 && !edgeFields.has(ch.field)) {
|
|
11523
11677
|
errors.push({
|
|
11524
11678
|
message: `Spec error: encoding.${channel}.field "${ch.field}" does not exist on edges. Available fields: ${[...edgeFields].join(", ")}`,
|
|
11525
11679
|
path: `encoding.${channel}.field`,
|
|
11526
11680
|
code: "DATA_FIELD_MISSING",
|
|
11527
|
-
suggestion: `Use one of the edge fields: ${[...edgeFields].join(", ")}`
|
|
11681
|
+
suggestion: `Use one of the edge fields: ${[...edgeFields].join(", ")}.${didYouMean(ch.field, [...edgeFields])}`
|
|
11528
11682
|
});
|
|
11529
11683
|
}
|
|
11530
11684
|
}
|
|
11685
|
+
checkSchemeUnused(
|
|
11686
|
+
encoding,
|
|
11687
|
+
errors,
|
|
11688
|
+
"graph colors come from scale.range and scale.domain",
|
|
11689
|
+
"Remove scale.scheme. To customize graph colors, provide explicit colors via scale.range."
|
|
11690
|
+
);
|
|
11531
11691
|
}
|
|
11532
11692
|
if (spec.layout && typeof spec.layout === "object") {
|
|
11533
11693
|
const layout = spec.layout;
|
|
@@ -11581,7 +11741,8 @@ function validateSankeySpec(spec, errors) {
|
|
|
11581
11741
|
}
|
|
11582
11742
|
const encoding = spec.encoding;
|
|
11583
11743
|
const dataColumns = new Set(Object.keys(firstRow));
|
|
11584
|
-
const
|
|
11744
|
+
const columnList = [...dataColumns];
|
|
11745
|
+
const availableColumns = columnList.join(", ");
|
|
11585
11746
|
for (const channel of ["source", "target", "value"]) {
|
|
11586
11747
|
const ch = encoding[channel];
|
|
11587
11748
|
if (!ch || typeof ch !== "object") {
|
|
@@ -11589,7 +11750,7 @@ function validateSankeySpec(spec, errors) {
|
|
|
11589
11750
|
message: `Spec error: sankey encoding requires "${channel}" channel`,
|
|
11590
11751
|
path: `encoding.${channel}`,
|
|
11591
11752
|
code: "MISSING_FIELD",
|
|
11592
|
-
suggestion: `Add encoding.${channel} with a field from your data (${availableColumns}). Example: ${channel}: { field: "${[
|
|
11753
|
+
suggestion: `Add encoding.${channel} with a field from your data (${availableColumns}). Example: ${channel}: { field: "${columnList[0] ?? "myField"}", type: "${channel === "value" ? "quantitative" : "nominal"}" }`
|
|
11593
11754
|
});
|
|
11594
11755
|
continue;
|
|
11595
11756
|
}
|
|
@@ -11607,10 +11768,16 @@ function validateSankeySpec(spec, errors) {
|
|
|
11607
11768
|
message: `Spec error: encoding.${channel}.field "${ch.field}" does not exist in data. Available columns: ${availableColumns}`,
|
|
11608
11769
|
path: `encoding.${channel}.field`,
|
|
11609
11770
|
code: "DATA_FIELD_MISSING",
|
|
11610
|
-
suggestion: `Use one of the available data columns: ${availableColumns}`
|
|
11771
|
+
suggestion: `Use one of the available data columns: ${availableColumns}.${didYouMean(ch.field, columnList)}`
|
|
11611
11772
|
});
|
|
11612
11773
|
}
|
|
11613
11774
|
}
|
|
11775
|
+
checkSchemeUnused(
|
|
11776
|
+
encoding,
|
|
11777
|
+
errors,
|
|
11778
|
+
"sankey node colors cycle the theme categorical palette",
|
|
11779
|
+
"Remove scale.scheme. To customize sankey colors, set theme.colors.categorical."
|
|
11780
|
+
);
|
|
11614
11781
|
if (spec.darkMode !== void 0 && !VALID_DARK_MODES.has(spec.darkMode)) {
|
|
11615
11782
|
errors.push({
|
|
11616
11783
|
message: 'Spec error: darkMode must be "auto", "force", or "off"',
|
|
@@ -11670,7 +11837,8 @@ function validateTileMapSpec(spec, errors) {
|
|
|
11670
11837
|
}
|
|
11671
11838
|
const encoding = spec.encoding;
|
|
11672
11839
|
const dataColumns = new Set(Object.keys(firstRow));
|
|
11673
|
-
const
|
|
11840
|
+
const columnList = [...dataColumns];
|
|
11841
|
+
const availableColumns = columnList.join(", ");
|
|
11674
11842
|
for (const channel of ["state", "value"]) {
|
|
11675
11843
|
const ch = encoding[channel];
|
|
11676
11844
|
if (!ch || typeof ch !== "object") {
|
|
@@ -11678,7 +11846,7 @@ function validateTileMapSpec(spec, errors) {
|
|
|
11678
11846
|
message: `Spec error: tilemap encoding requires "${channel}" channel`,
|
|
11679
11847
|
path: `encoding.${channel}`,
|
|
11680
11848
|
code: "MISSING_FIELD",
|
|
11681
|
-
suggestion: `Add encoding.${channel} with a field from your data (${availableColumns}). Example: ${channel}: { field: "${[
|
|
11849
|
+
suggestion: `Add encoding.${channel} with a field from your data (${availableColumns}). Example: ${channel}: { field: "${columnList[0] ?? "myField"}", type: "${channel === "value" ? "quantitative" : "nominal"}" }`
|
|
11682
11850
|
});
|
|
11683
11851
|
continue;
|
|
11684
11852
|
}
|
|
@@ -11696,11 +11864,19 @@ function validateTileMapSpec(spec, errors) {
|
|
|
11696
11864
|
message: `Spec error: encoding.${channel}.field "${ch.field}" does not exist in data. Available columns: ${availableColumns}`,
|
|
11697
11865
|
path: `encoding.${channel}.field`,
|
|
11698
11866
|
code: "DATA_FIELD_MISSING",
|
|
11699
|
-
suggestion: `Use one of the available data columns: ${availableColumns}`
|
|
11867
|
+
suggestion: `Use one of the available data columns: ${availableColumns}.${didYouMean(ch.field, columnList)}`
|
|
11700
11868
|
});
|
|
11701
11869
|
}
|
|
11702
11870
|
}
|
|
11703
11871
|
}
|
|
11872
|
+
if (spec.encoding && typeof spec.encoding === "object") {
|
|
11873
|
+
checkSchemeUnused(
|
|
11874
|
+
spec.encoding,
|
|
11875
|
+
errors,
|
|
11876
|
+
"tilemap fills come from the top-level palette property",
|
|
11877
|
+
`Remove scale.scheme and set the top-level palette property instead, e.g. palette: "green". Supported: ${MAP_PALETTE_NAMES}.`
|
|
11878
|
+
);
|
|
11879
|
+
}
|
|
11704
11880
|
if (spec.darkMode !== void 0 && !VALID_DARK_MODES.has(spec.darkMode)) {
|
|
11705
11881
|
errors.push({
|
|
11706
11882
|
message: 'Spec error: darkMode must be "auto", "force", or "off"',
|
|
@@ -11739,6 +11915,16 @@ function validateMapSpec(spec, errors) {
|
|
|
11739
11915
|
});
|
|
11740
11916
|
return;
|
|
11741
11917
|
}
|
|
11918
|
+
const mapEncoding = spec.encoding;
|
|
11919
|
+
if (mapEncoding.color && typeof mapEncoding.color === "object") {
|
|
11920
|
+
checkMapSchemeName(mapEncoding.color, "encoding.color", errors);
|
|
11921
|
+
}
|
|
11922
|
+
if (spec.points && typeof spec.points === "object") {
|
|
11923
|
+
const pointsLayer = spec.points;
|
|
11924
|
+
if (pointsLayer.color && typeof pointsLayer.color === "object") {
|
|
11925
|
+
checkMapSchemeName(pointsLayer.color, "points.color", errors);
|
|
11926
|
+
}
|
|
11927
|
+
}
|
|
11742
11928
|
if ("focus" in geo && geo.focus !== void 0 && geo.focus !== null) {
|
|
11743
11929
|
const focus = geo.focus;
|
|
11744
11930
|
if (typeof focus === "string" || typeof focus === "number") {
|
|
@@ -11755,12 +11941,31 @@ function validateMapSpec(spec, errors) {
|
|
|
11755
11941
|
}
|
|
11756
11942
|
} else if (typeof focus === "object") {
|
|
11757
11943
|
const focusObj = focus;
|
|
11758
|
-
if (
|
|
11944
|
+
if ("points" in focusObj) {
|
|
11945
|
+
const pts = focusObj.points;
|
|
11946
|
+
const isSubset = typeof pts === "object" && pts !== null && typeof pts.field === "string" && "value" in pts;
|
|
11947
|
+
if (pts !== true && !isSubset) {
|
|
11948
|
+
errors.push({
|
|
11949
|
+
message: "Spec error: geo.focus.points must be true or { field: string, value: string | number }",
|
|
11950
|
+
path: "geo.focus.points",
|
|
11951
|
+
code: "INVALID_TYPE",
|
|
11952
|
+
suggestion: 'Use { points: true } to fit all points, or { points: { field: "rating", value: "F" } } to fit a subset.'
|
|
11953
|
+
});
|
|
11954
|
+
}
|
|
11955
|
+
if (focusObj.padding !== void 0 && typeof focusObj.padding !== "number") {
|
|
11956
|
+
errors.push({
|
|
11957
|
+
message: "Spec error: geo.focus.padding must be a number",
|
|
11958
|
+
path: "geo.focus.padding",
|
|
11959
|
+
code: "INVALID_TYPE",
|
|
11960
|
+
suggestion: "Provide padding as a number in map-local units, e.g. padding: 16"
|
|
11961
|
+
});
|
|
11962
|
+
}
|
|
11963
|
+
} else if (!("features" in focusObj) || focusObj.features == null) {
|
|
11759
11964
|
errors.push({
|
|
11760
|
-
message: 'Spec error: geo.focus object must have a "features" property',
|
|
11965
|
+
message: 'Spec error: geo.focus object must have a "features" or "points" property',
|
|
11761
11966
|
path: "geo.focus.features",
|
|
11762
11967
|
code: "MISSING_FIELD",
|
|
11763
|
-
suggestion: 'Provide feature id(s) to focus on, e.g.
|
|
11968
|
+
suggestion: 'Provide feature id(s) to focus on, e.g. { features: "36", padding: 32 }, or { points: true } to fit the point cluster.'
|
|
11764
11969
|
});
|
|
11765
11970
|
} else {
|
|
11766
11971
|
const feats = focusObj.features;
|
|
@@ -11794,32 +11999,98 @@ function validateMapSpec(spec, errors) {
|
|
|
11794
11999
|
}
|
|
11795
12000
|
} else {
|
|
11796
12001
|
errors.push({
|
|
11797
|
-
message: "Spec error: geo.focus must be a string, number, array of ids, or object with { features, padding? }",
|
|
12002
|
+
message: "Spec error: geo.focus must be a string, number, array of ids, or object with { features, padding? } or { points: true, padding? }",
|
|
11798
12003
|
path: "geo.focus",
|
|
11799
12004
|
code: "INVALID_TYPE",
|
|
11800
|
-
suggestion: 'Use a feature id, array of ids,
|
|
12005
|
+
suggestion: 'Use a feature id, array of ids, { features: "36", padding: 16 }, or { points: true }. Pass null to clear focus.'
|
|
11801
12006
|
});
|
|
11802
12007
|
}
|
|
11803
12008
|
}
|
|
11804
12009
|
const encoding = spec.encoding;
|
|
11805
|
-
|
|
11806
|
-
|
|
11807
|
-
|
|
12010
|
+
const hasPoints = spec.points && typeof spec.points === "object" && !Array.isArray(spec.points);
|
|
12011
|
+
const specData = spec.data;
|
|
12012
|
+
const isBasemapOnly = hasPoints && (!specData || specData.length === 0);
|
|
12013
|
+
const keyCh = encoding.key;
|
|
12014
|
+
if (!isBasemapOnly) {
|
|
12015
|
+
if (!keyCh || typeof keyCh !== "object") {
|
|
11808
12016
|
errors.push({
|
|
11809
|
-
message:
|
|
11810
|
-
path:
|
|
12017
|
+
message: 'Spec error: map encoding requires "key" channel',
|
|
12018
|
+
path: "encoding.key",
|
|
11811
12019
|
code: "MISSING_FIELD",
|
|
11812
|
-
suggestion:
|
|
12020
|
+
suggestion: 'Add encoding.key, e.g. key: { field: "id", type: "nominal" }'
|
|
12021
|
+
});
|
|
12022
|
+
} else if (!keyCh.field || typeof keyCh.field !== "string") {
|
|
12023
|
+
errors.push({
|
|
12024
|
+
message: 'Spec error: encoding.key must have a "field" string',
|
|
12025
|
+
path: "encoding.key.field",
|
|
12026
|
+
code: "MISSING_FIELD",
|
|
12027
|
+
suggestion: 'Add a field name, e.g. key: { field: "id" }'
|
|
11813
12028
|
});
|
|
11814
|
-
continue;
|
|
11815
12029
|
}
|
|
11816
|
-
|
|
12030
|
+
}
|
|
12031
|
+
const colorCh = encoding.color;
|
|
12032
|
+
if (!hasPoints) {
|
|
12033
|
+
if (!colorCh || typeof colorCh !== "object") {
|
|
11817
12034
|
errors.push({
|
|
11818
|
-
message:
|
|
11819
|
-
path:
|
|
12035
|
+
message: 'Spec error: map encoding requires "color" channel (or add a "points" layer for basemap-only mode)',
|
|
12036
|
+
path: "encoding.color",
|
|
11820
12037
|
code: "MISSING_FIELD",
|
|
11821
|
-
suggestion:
|
|
12038
|
+
suggestion: 'Add encoding.color, e.g. color: { field: "value", type: "quantitative" }'
|
|
11822
12039
|
});
|
|
12040
|
+
} else if (!colorCh.field || typeof colorCh.field !== "string") {
|
|
12041
|
+
errors.push({
|
|
12042
|
+
message: 'Spec error: encoding.color must have a "field" string',
|
|
12043
|
+
path: "encoding.color.field",
|
|
12044
|
+
code: "MISSING_FIELD",
|
|
12045
|
+
suggestion: 'Add a field name, e.g. color: { field: "value" }'
|
|
12046
|
+
});
|
|
12047
|
+
}
|
|
12048
|
+
} else if (colorCh && typeof colorCh === "object" && (!colorCh.field || typeof colorCh.field !== "string")) {
|
|
12049
|
+
errors.push({
|
|
12050
|
+
message: 'Spec error: encoding.color must have a "field" string when provided',
|
|
12051
|
+
path: "encoding.color.field",
|
|
12052
|
+
code: "MISSING_FIELD",
|
|
12053
|
+
suggestion: 'Add a field name, e.g. color: { field: "value" }'
|
|
12054
|
+
});
|
|
12055
|
+
}
|
|
12056
|
+
if (hasPoints) {
|
|
12057
|
+
const points = spec.points;
|
|
12058
|
+
if (!Array.isArray(points.data)) {
|
|
12059
|
+
errors.push({
|
|
12060
|
+
message: "Spec error: points.data must be an array of data rows",
|
|
12061
|
+
path: "points.data",
|
|
12062
|
+
code: "INVALID_TYPE",
|
|
12063
|
+
suggestion: "Provide data as an array of objects with lat/lng fields"
|
|
12064
|
+
});
|
|
12065
|
+
}
|
|
12066
|
+
for (const ch of ["longitude", "latitude"]) {
|
|
12067
|
+
const enc = points[ch];
|
|
12068
|
+
if (!enc || typeof enc !== "object") {
|
|
12069
|
+
errors.push({
|
|
12070
|
+
message: `Spec error: points requires "${ch}" encoding channel`,
|
|
12071
|
+
path: `points.${ch}`,
|
|
12072
|
+
code: "MISSING_FIELD",
|
|
12073
|
+
suggestion: `Add points.${ch}, e.g. ${ch}: { field: "${ch === "longitude" ? "lon" : "lat"}", type: "quantitative" }`
|
|
12074
|
+
});
|
|
12075
|
+
} else if (!enc.field || typeof enc.field !== "string") {
|
|
12076
|
+
errors.push({
|
|
12077
|
+
message: `Spec error: points.${ch} must have a "field" string`,
|
|
12078
|
+
path: `points.${ch}.field`,
|
|
12079
|
+
code: "MISSING_FIELD",
|
|
12080
|
+
suggestion: `Add a field name, e.g. ${ch}: { field: "${ch === "longitude" ? "lon" : "lat"}" }`
|
|
12081
|
+
});
|
|
12082
|
+
}
|
|
12083
|
+
}
|
|
12084
|
+
for (const ch of ["size", "color", "key"]) {
|
|
12085
|
+
const enc = points[ch];
|
|
12086
|
+
if (enc && typeof enc === "object" && (!enc.field || typeof enc.field !== "string")) {
|
|
12087
|
+
errors.push({
|
|
12088
|
+
message: `Spec error: points.${ch} must have a "field" string when provided`,
|
|
12089
|
+
path: `points.${ch}.field`,
|
|
12090
|
+
code: "MISSING_FIELD",
|
|
12091
|
+
suggestion: `Add a field name, e.g. ${ch}: { field: "myField" }`
|
|
12092
|
+
});
|
|
12093
|
+
}
|
|
11823
12094
|
}
|
|
11824
12095
|
}
|
|
11825
12096
|
}
|
|
@@ -11863,7 +12134,8 @@ function validateBarListSpec(spec, errors) {
|
|
|
11863
12134
|
}
|
|
11864
12135
|
const encoding = spec.encoding;
|
|
11865
12136
|
const dataColumns = new Set(Object.keys(firstRow));
|
|
11866
|
-
const
|
|
12137
|
+
const columnList = [...dataColumns];
|
|
12138
|
+
const availableColumns = columnList.join(", ");
|
|
11867
12139
|
for (const channel of ["label", "value"]) {
|
|
11868
12140
|
const ch = encoding[channel];
|
|
11869
12141
|
if (!ch || typeof ch !== "object") {
|
|
@@ -11871,7 +12143,7 @@ function validateBarListSpec(spec, errors) {
|
|
|
11871
12143
|
message: `Spec error: barlist encoding requires "${channel}" channel`,
|
|
11872
12144
|
path: `encoding.${channel}`,
|
|
11873
12145
|
code: "MISSING_FIELD",
|
|
11874
|
-
suggestion: `Add encoding.${channel} with a field from your data (${availableColumns}). Example: ${channel}: { field: "${[
|
|
12146
|
+
suggestion: `Add encoding.${channel} with a field from your data (${availableColumns}). Example: ${channel}: { field: "${columnList[0] ?? "myField"}", type: "${channel === "value" ? "quantitative" : "nominal"}" }`
|
|
11875
12147
|
});
|
|
11876
12148
|
continue;
|
|
11877
12149
|
}
|
|
@@ -11889,7 +12161,7 @@ function validateBarListSpec(spec, errors) {
|
|
|
11889
12161
|
message: `Spec error: encoding.${channel}.field "${ch.field}" does not exist in data. Available columns: ${availableColumns}`,
|
|
11890
12162
|
path: `encoding.${channel}.field`,
|
|
11891
12163
|
code: "DATA_FIELD_MISSING",
|
|
11892
|
-
suggestion: `Use one of the available data columns: ${availableColumns}`
|
|
12164
|
+
suggestion: `Use one of the available data columns: ${availableColumns}.${didYouMean(ch.field, columnList)}`
|
|
11893
12165
|
});
|
|
11894
12166
|
}
|
|
11895
12167
|
}
|
|
@@ -11902,10 +12174,16 @@ function validateBarListSpec(spec, errors) {
|
|
|
11902
12174
|
message: `Spec error: encoding.${channel}.field "${field}" does not exist in data. Available columns: ${availableColumns}`,
|
|
11903
12175
|
path: `encoding.${channel}.field`,
|
|
11904
12176
|
code: "DATA_FIELD_MISSING",
|
|
11905
|
-
suggestion: `Use one of the available data columns: ${availableColumns}`
|
|
12177
|
+
suggestion: `Use one of the available data columns: ${availableColumns}.${didYouMean(field, columnList)}`
|
|
11906
12178
|
});
|
|
11907
12179
|
}
|
|
11908
12180
|
}
|
|
12181
|
+
checkSchemeUnused(
|
|
12182
|
+
encoding,
|
|
12183
|
+
errors,
|
|
12184
|
+
"bar list colors cycle the built-in barlist palette",
|
|
12185
|
+
"Remove scale.scheme. Bar list colors are not configurable via encoding; they cycle the built-in palette."
|
|
12186
|
+
);
|
|
11909
12187
|
if (spec.darkMode !== void 0 && !VALID_DARK_MODES.has(spec.darkMode)) {
|
|
11910
12188
|
errors.push({
|
|
11911
12189
|
message: 'Spec error: darkMode must be "auto", "force", or "off"',
|
|
@@ -11988,6 +12266,22 @@ function validateLayerSpec(spec, errors) {
|
|
|
11988
12266
|
}
|
|
11989
12267
|
}
|
|
11990
12268
|
}
|
|
12269
|
+
function validateAnimation(spec, errors) {
|
|
12270
|
+
const animation = spec.animation;
|
|
12271
|
+
if (!animation || typeof animation !== "object" || Array.isArray(animation)) return;
|
|
12272
|
+
const update = animation.update;
|
|
12273
|
+
if (!update || typeof update !== "object" || Array.isArray(update)) return;
|
|
12274
|
+
const maxMarks = update.maxMarks;
|
|
12275
|
+
if (maxMarks === void 0) return;
|
|
12276
|
+
if (typeof maxMarks !== "number" || !Number.isFinite(maxMarks) || maxMarks < 1) {
|
|
12277
|
+
errors.push({
|
|
12278
|
+
message: `Spec error: animation.update.maxMarks must be a number >= 1, got ${JSON.stringify(maxMarks)}`,
|
|
12279
|
+
path: "animation.update.maxMarks",
|
|
12280
|
+
code: "INVALID_VALUE",
|
|
12281
|
+
suggestion: "Set animation.update.maxMarks to the largest mark count that should still tween, e.g. animation: { update: { maxMarks: 5000 } }. Omit it to use the default of 500."
|
|
12282
|
+
});
|
|
12283
|
+
}
|
|
12284
|
+
}
|
|
11991
12285
|
function validateSpec(spec) {
|
|
11992
12286
|
const errors = [];
|
|
11993
12287
|
if (!spec || typeof spec !== "object" || Array.isArray(spec)) {
|
|
@@ -12096,6 +12390,9 @@ function validateSpec(spec) {
|
|
|
12096
12390
|
} else if (isBarList) {
|
|
12097
12391
|
validateBarListSpec(obj, errors);
|
|
12098
12392
|
}
|
|
12393
|
+
if (!isGraph) {
|
|
12394
|
+
validateAnimation(obj, errors);
|
|
12395
|
+
}
|
|
12099
12396
|
if (errors.length > 0) {
|
|
12100
12397
|
return { valid: false, errors, normalized: null };
|
|
12101
12398
|
}
|
|
@@ -12119,6 +12416,36 @@ ${errorMessages}`);
|
|
|
12119
12416
|
return { spec: normalized, warnings };
|
|
12120
12417
|
}
|
|
12121
12418
|
|
|
12419
|
+
// src/layout/shared.ts
|
|
12420
|
+
function chromeToInput(chrome) {
|
|
12421
|
+
return {
|
|
12422
|
+
eyebrow: chrome.eyebrow,
|
|
12423
|
+
title: chrome.title,
|
|
12424
|
+
subtitle: chrome.subtitle,
|
|
12425
|
+
source: chrome.source,
|
|
12426
|
+
byline: chrome.byline,
|
|
12427
|
+
footer: chrome.footer,
|
|
12428
|
+
brand: chrome.brand
|
|
12429
|
+
};
|
|
12430
|
+
}
|
|
12431
|
+
function scalePadding(basePadding, width, height) {
|
|
12432
|
+
const minDim = Math.min(width, height);
|
|
12433
|
+
if (minDim >= 500) return basePadding;
|
|
12434
|
+
if (minDim <= 200) return Math.max(Math.round(basePadding * 0.5), 4);
|
|
12435
|
+
const t = (minDim - 200) / 300;
|
|
12436
|
+
return Math.max(Math.round(basePadding * (0.5 + t * 0.5)), 4);
|
|
12437
|
+
}
|
|
12438
|
+
function bottomMargin(bottomHeight, padding, xAxisHeight) {
|
|
12439
|
+
return padding + bottomHeight + xAxisHeight;
|
|
12440
|
+
}
|
|
12441
|
+
var INLINE_TICK_OVERHANG_PAD = 6;
|
|
12442
|
+
function resolveChromeLayout(spec, options) {
|
|
12443
|
+
const encoding = spec?.encoding;
|
|
12444
|
+
const isFaceted = !!(encoding?.facet || encoding?.row || encoding?.column);
|
|
12445
|
+
if (isFaceted) return "subtract";
|
|
12446
|
+
return spec?.chromeLayout ?? options?.chromeLayout ?? "subtract";
|
|
12447
|
+
}
|
|
12448
|
+
|
|
12122
12449
|
// src/barlist/compile-barlist.ts
|
|
12123
12450
|
var DEFAULT_ROW_GAP = 8;
|
|
12124
12451
|
var LABEL_BAR_GAP = 12;
|
|
@@ -12166,14 +12493,19 @@ function compileBarList(spec, options) {
|
|
|
12166
12493
|
watermark
|
|
12167
12494
|
);
|
|
12168
12495
|
const padding = theme.spacing.padding;
|
|
12496
|
+
const chromeLayout = resolveChromeLayout(
|
|
12497
|
+
spec,
|
|
12498
|
+
options
|
|
12499
|
+
);
|
|
12500
|
+
const grownHeight = chromeLayout === "grow" ? options.height + chrome.topHeight + chrome.bottomHeight : options.height;
|
|
12169
12501
|
const fullArea = {
|
|
12170
12502
|
x: padding,
|
|
12171
12503
|
y: padding + chrome.topHeight,
|
|
12172
12504
|
width: options.width - padding * 2,
|
|
12173
|
-
height: options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
|
|
12505
|
+
height: chromeLayout === "grow" ? options.height - padding * 2 : options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
|
|
12174
12506
|
};
|
|
12175
12507
|
if (fullArea.width <= 0 || fullArea.height <= 0) {
|
|
12176
|
-
return emptyLayout2(chrome, theme, options, watermark);
|
|
12508
|
+
return emptyLayout2(chrome, theme, { ...options, height: grownHeight }, watermark);
|
|
12177
12509
|
}
|
|
12178
12510
|
const labelField = barlistSpec.encoding.label.field;
|
|
12179
12511
|
const valueField = barlistSpec.encoding.value.field;
|
|
@@ -12358,7 +12690,7 @@ function compileBarList(spec, options) {
|
|
|
12358
12690
|
a11y,
|
|
12359
12691
|
theme,
|
|
12360
12692
|
width: options.width,
|
|
12361
|
-
height:
|
|
12693
|
+
height: grownHeight,
|
|
12362
12694
|
animation: resolvedAnimation,
|
|
12363
12695
|
watermark,
|
|
12364
12696
|
measureText
|
|
@@ -13650,7 +13982,8 @@ function computeLegendContent(spec, strategy, theme, availableWidth, availableHe
|
|
|
13650
13982
|
fill: theme.colors.text,
|
|
13651
13983
|
lineHeight: 1.3
|
|
13652
13984
|
};
|
|
13653
|
-
const
|
|
13985
|
+
const specPosition = spec.legend?.position === "top-left" ? "top" : spec.legend?.position;
|
|
13986
|
+
const resolvedPosition = specPosition ?? (strategy.legendPosition === "right" ? "right" : "top");
|
|
13654
13987
|
if (entries.length === 0) {
|
|
13655
13988
|
return emptyContent(resolvedPosition, theme);
|
|
13656
13989
|
}
|
|
@@ -14651,30 +14984,6 @@ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContex
|
|
|
14651
14984
|
return result;
|
|
14652
14985
|
}
|
|
14653
14986
|
|
|
14654
|
-
// src/layout/shared.ts
|
|
14655
|
-
function chromeToInput(chrome) {
|
|
14656
|
-
return {
|
|
14657
|
-
eyebrow: chrome.eyebrow,
|
|
14658
|
-
title: chrome.title,
|
|
14659
|
-
subtitle: chrome.subtitle,
|
|
14660
|
-
source: chrome.source,
|
|
14661
|
-
byline: chrome.byline,
|
|
14662
|
-
footer: chrome.footer,
|
|
14663
|
-
brand: chrome.brand
|
|
14664
|
-
};
|
|
14665
|
-
}
|
|
14666
|
-
function scalePadding(basePadding, width, height) {
|
|
14667
|
-
const minDim = Math.min(width, height);
|
|
14668
|
-
if (minDim >= 500) return basePadding;
|
|
14669
|
-
if (minDim <= 200) return Math.max(Math.round(basePadding * 0.5), 4);
|
|
14670
|
-
const t = (minDim - 200) / 300;
|
|
14671
|
-
return Math.max(Math.round(basePadding * (0.5 + t * 0.5)), 4);
|
|
14672
|
-
}
|
|
14673
|
-
function bottomMargin(bottomHeight, padding, xAxisHeight) {
|
|
14674
|
-
return padding + bottomHeight + xAxisHeight;
|
|
14675
|
-
}
|
|
14676
|
-
var INLINE_TICK_OVERHANG_PAD = 6;
|
|
14677
|
-
|
|
14678
14987
|
// src/layout/plan.ts
|
|
14679
14988
|
function createMeasureFn(measureText) {
|
|
14680
14989
|
if (measureText) {
|
|
@@ -14896,7 +15205,9 @@ function resolveLayoutPlan(chartSpec, renderSpec, options, theme, strategy, wate
|
|
|
14896
15205
|
if (hasTopLegend) topMargin += TOP_LEGEND_GAP_ABOVE + legendContent.height + gap;
|
|
14897
15206
|
topMargin += effectiveTopAxisGap;
|
|
14898
15207
|
const bMargin = bottomMargin(chrome.bottomHeight, padding, xAxisExtent);
|
|
14899
|
-
const
|
|
15208
|
+
const chromeLayout = resolveChromeLayout(renderSpec, options);
|
|
15209
|
+
const effectiveHeight = chromeLayout === "grow" ? height + chrome.topHeight + chrome.bottomHeight : height;
|
|
15210
|
+
const chartHeight = Math.max(0, effectiveHeight - topMargin - bMargin);
|
|
14900
15211
|
const rightMargin = hPad + (isRadial ? hPad : axisMargin);
|
|
14901
15212
|
const rightAxisReserve = options.rightAxisReserve ?? 0;
|
|
14902
15213
|
const provisionalArea = {
|
|
@@ -15048,11 +15359,12 @@ function compileLayer(spec, options, compileChart2) {
|
|
|
15048
15359
|
const singleSpec = buildPrimarySpec(leaves, spec);
|
|
15049
15360
|
return compileChart2(singleSpec, options);
|
|
15050
15361
|
}
|
|
15362
|
+
const layerOptions = { ...options, layered: true };
|
|
15051
15363
|
if (spec.resolve?.scale?.y === "independent") {
|
|
15052
|
-
return compileLayerIndependent(leaves, spec,
|
|
15364
|
+
return compileLayerIndependent(leaves, spec, layerOptions, compileChart2);
|
|
15053
15365
|
}
|
|
15054
15366
|
const primarySpec = buildPrimarySpec(leaves, spec);
|
|
15055
|
-
const primaryLayout = compileChart2(primarySpec,
|
|
15367
|
+
const primaryLayout = compileChart2(primarySpec, layerOptions);
|
|
15056
15368
|
const allMarks = [];
|
|
15057
15369
|
const seenLabels = /* @__PURE__ */ new Set();
|
|
15058
15370
|
const pLegend = primaryLayout.legend;
|
|
@@ -15065,7 +15377,7 @@ function compileLayer(spec, options, compileChart2) {
|
|
|
15065
15377
|
zIndex: leaf.zIndex ?? i
|
|
15066
15378
|
}));
|
|
15067
15379
|
indexedLeaves.sort((a, b) => a.zIndex - b.zIndex);
|
|
15068
|
-
const leafOptions = { ...
|
|
15380
|
+
const leafOptions = { ...layerOptions, frozenChartArea: primaryLayout.area };
|
|
15069
15381
|
const sharedDomains = computeSharedDomains(leaves);
|
|
15070
15382
|
for (const { leaf } of indexedLeaves) {
|
|
15071
15383
|
const themedLeaf = {
|
|
@@ -15174,11 +15486,9 @@ function compileLayerIndependent(leaves, layerSpec, options, compileChart2) {
|
|
|
15174
15486
|
};
|
|
15175
15487
|
const xField0 = leaf0.encoding?.x?.field;
|
|
15176
15488
|
const xField1 = leaf1.encoding?.x?.field;
|
|
15177
|
-
const
|
|
15178
|
-
|
|
15179
|
-
|
|
15180
|
-
let leaf0WithUnionX = ensureXDomainCoverage(leaf0, xField0, unionXValues);
|
|
15181
|
-
let leaf1WithUnionX = ensureXDomainCoverage(leaf1, xField1, unionXValues);
|
|
15489
|
+
const union = withUnionXDomain(leaf0, leaf1);
|
|
15490
|
+
let leaf0WithUnionX = union.leaf0;
|
|
15491
|
+
let leaf1WithUnionX = union.leaf1;
|
|
15182
15492
|
const aligned = alignYDomains(leaf0WithUnionX, leaf1WithUnionX);
|
|
15183
15493
|
if (aligned) {
|
|
15184
15494
|
leaf0WithUnionX = withYDomain(leaf0WithUnionX, aligned.domain0);
|
|
@@ -15325,20 +15635,75 @@ function compileLayerIndependent(leaves, layerSpec, options, compileChart2) {
|
|
|
15325
15635
|
tooltipDescriptors: mergedTooltips
|
|
15326
15636
|
};
|
|
15327
15637
|
}
|
|
15328
|
-
function
|
|
15329
|
-
if (!
|
|
15330
|
-
|
|
15331
|
-
|
|
15332
|
-
|
|
15333
|
-
|
|
15334
|
-
|
|
15335
|
-
|
|
15638
|
+
function withXDomain(leaf, domain) {
|
|
15639
|
+
if (!leaf.encoding?.x) return leaf;
|
|
15640
|
+
return {
|
|
15641
|
+
...leaf,
|
|
15642
|
+
encoding: {
|
|
15643
|
+
...leaf.encoding,
|
|
15644
|
+
x: {
|
|
15645
|
+
...leaf.encoding.x,
|
|
15646
|
+
scale: {
|
|
15647
|
+
...leaf.encoding.x.scale,
|
|
15648
|
+
domain
|
|
15649
|
+
}
|
|
15650
|
+
}
|
|
15651
|
+
}
|
|
15652
|
+
};
|
|
15653
|
+
}
|
|
15654
|
+
function withUnionXDomain(leaf0, leaf1) {
|
|
15655
|
+
const xEnc0 = leaf0.encoding?.x;
|
|
15656
|
+
const xEnc1 = leaf1.encoding?.x;
|
|
15657
|
+
const xField0 = xEnc0?.field;
|
|
15658
|
+
const xField1 = xEnc1?.field;
|
|
15659
|
+
if (!xField0 || !xField1) return { leaf0, leaf1 };
|
|
15660
|
+
const xType = xEnc0?.type ?? xEnc1?.type;
|
|
15661
|
+
const pinned0 = xEnc0?.scale?.domain !== void 0;
|
|
15662
|
+
const pinned1 = xEnc1?.scale?.domain !== void 0;
|
|
15663
|
+
if (pinned0 && pinned1) return { leaf0, leaf1 };
|
|
15664
|
+
const values0 = leaf0.data.map((row) => row[xField0]);
|
|
15665
|
+
const values1 = leaf1.data.map((row) => row[xField1]);
|
|
15666
|
+
const allValues = [...values0, ...values1].filter((v) => v != null);
|
|
15667
|
+
if (allValues.length === 0) return { leaf0, leaf1 };
|
|
15668
|
+
let domain;
|
|
15669
|
+
if (xType === "temporal") {
|
|
15670
|
+
let minMs = Number.POSITIVE_INFINITY;
|
|
15671
|
+
let maxMs = Number.NEGATIVE_INFINITY;
|
|
15672
|
+
for (const v of allValues) {
|
|
15673
|
+
const ms = (v instanceof Date ? v : new Date(String(v))).getTime();
|
|
15674
|
+
if (Number.isNaN(ms)) continue;
|
|
15675
|
+
if (ms < minMs) minMs = ms;
|
|
15676
|
+
if (ms > maxMs) maxMs = ms;
|
|
15677
|
+
}
|
|
15678
|
+
if (minMs <= maxMs) {
|
|
15679
|
+
domain = [new Date(minMs).toISOString(), new Date(maxMs).toISOString()];
|
|
15680
|
+
}
|
|
15681
|
+
} else if (xType === "quantitative") {
|
|
15682
|
+
let min4 = Number.POSITIVE_INFINITY;
|
|
15683
|
+
let max4 = Number.NEGATIVE_INFINITY;
|
|
15684
|
+
for (const v of allValues) {
|
|
15685
|
+
const n = typeof v === "number" ? v : Number(v);
|
|
15686
|
+
if (!Number.isFinite(n)) continue;
|
|
15687
|
+
if (n < min4) min4 = n;
|
|
15688
|
+
if (n > max4) max4 = n;
|
|
15689
|
+
}
|
|
15690
|
+
if (min4 <= max4) domain = [min4, max4];
|
|
15691
|
+
} else {
|
|
15692
|
+
const seen = /* @__PURE__ */ new Set();
|
|
15693
|
+
const categories = [];
|
|
15694
|
+
for (const v of allValues) {
|
|
15695
|
+
const s = String(v);
|
|
15696
|
+
if (!seen.has(s)) {
|
|
15697
|
+
seen.add(s);
|
|
15698
|
+
categories.push(s);
|
|
15699
|
+
}
|
|
15336
15700
|
}
|
|
15701
|
+
if (categories.length > 0) domain = categories;
|
|
15337
15702
|
}
|
|
15338
|
-
if (
|
|
15703
|
+
if (domain === void 0) return { leaf0, leaf1 };
|
|
15339
15704
|
return {
|
|
15340
|
-
|
|
15341
|
-
|
|
15705
|
+
leaf0: pinned0 ? leaf0 : withXDomain(leaf0, domain),
|
|
15706
|
+
leaf1: pinned1 ? leaf1 : withXDomain(leaf1, domain)
|
|
15342
15707
|
};
|
|
15343
15708
|
}
|
|
15344
15709
|
function alignYDomains(leaf0, leaf1) {
|
|
@@ -15457,6 +15822,10 @@ function buildPrimarySpec(leaves, layerSpec) {
|
|
|
15457
15822
|
...leaves[0],
|
|
15458
15823
|
data: allData,
|
|
15459
15824
|
chrome: layerSpec.chrome ?? leaves[0].chrome,
|
|
15825
|
+
// Annotations come from the parent LayerSpec when present, otherwise
|
|
15826
|
+
// the first flattened leaf's. Later leaves' annotations are silently
|
|
15827
|
+
// dropped. The dual-axis path (compileLayerIndependent) explicitly
|
|
15828
|
+
// clears the secondary axis spec's annotations.
|
|
15460
15829
|
annotations: layerSpec.annotations ?? leaves[0].annotations,
|
|
15461
15830
|
labels: layerSpec.labels ?? leaves[0].labels,
|
|
15462
15831
|
legend: layerSpec.legend ?? leaves[0].legend,
|
|
@@ -15613,6 +15982,34 @@ function collectContrastWarnings(scales, markType, theme) {
|
|
|
15613
15982
|
return warnings;
|
|
15614
15983
|
}
|
|
15615
15984
|
|
|
15985
|
+
// src/compiler/mark-render-mode.ts
|
|
15986
|
+
var AUTO_CANVAS_THRESHOLD = 1e3;
|
|
15987
|
+
var AUTO_ENABLED = true;
|
|
15988
|
+
function resolveMarkRenderMode(args, warnings = []) {
|
|
15989
|
+
const { markDef, markType, pointCount, display, faceted, layered } = args;
|
|
15990
|
+
const requested = markDef?.render ?? "auto";
|
|
15991
|
+
if (requested === "svg") return "svg";
|
|
15992
|
+
const refusal = describeRefusal(markType, display, faceted, layered);
|
|
15993
|
+
if (refusal) {
|
|
15994
|
+
if (requested === "canvas") {
|
|
15995
|
+
warnings.push(
|
|
15996
|
+
`Chart warning: mark.render "canvas" is not supported ${refusal}; rendering marks as SVG instead.`
|
|
15997
|
+
);
|
|
15998
|
+
}
|
|
15999
|
+
return "svg";
|
|
16000
|
+
}
|
|
16001
|
+
if (requested === "canvas") return "canvas";
|
|
16002
|
+
if (!AUTO_ENABLED) return "svg";
|
|
16003
|
+
return pointCount > AUTO_CANVAS_THRESHOLD ? "canvas" : "svg";
|
|
16004
|
+
}
|
|
16005
|
+
function describeRefusal(markType, display, faceted, layered) {
|
|
16006
|
+
if (markType !== "point") return `for ${markType} marks (point marks only)`;
|
|
16007
|
+
if (faceted) return "on faceted charts";
|
|
16008
|
+
if (layered) return "on layered charts";
|
|
16009
|
+
if (display === "sparkline") return "on sparklines";
|
|
16010
|
+
return void 0;
|
|
16011
|
+
}
|
|
16012
|
+
|
|
15616
16013
|
// src/compiler/sparkline-defaults.ts
|
|
15617
16014
|
var TREND_DEADBAND = 5e-4;
|
|
15618
16015
|
function computeTrend(values) {
|
|
@@ -15691,7 +16088,7 @@ import {
|
|
|
15691
16088
|
HPAD_COMPACT_FRACTION as HPAD_COMPACT_FRACTION2,
|
|
15692
16089
|
HPAD_COMPACT_MIN as HPAD_COMPACT_MIN2,
|
|
15693
16090
|
resolveTheme as resolveTheme3,
|
|
15694
|
-
SEQUENTIAL_PALETTES
|
|
16091
|
+
SEQUENTIAL_PALETTES as SEQUENTIAL_PALETTES2
|
|
15695
16092
|
} from "@opendata-ai/openchart-core";
|
|
15696
16093
|
|
|
15697
16094
|
// ../../node_modules/.bun/d3-geo@3.1.1/node_modules/d3-geo/src/math.js
|
|
@@ -17669,13 +18066,32 @@ function joinDataToFeatures(features, data, dataKeyField, featureIdField) {
|
|
|
17669
18066
|
|
|
17670
18067
|
// src/geo/projections.ts
|
|
17671
18068
|
var MERCATOR_FIT_MIN_LAT = -60;
|
|
17672
|
-
function createProjection(type, width, height, geojson) {
|
|
18069
|
+
function createProjection(type, width, height, geojson, inset = 0) {
|
|
17673
18070
|
if (type === "identity") {
|
|
18071
|
+
if (inset > 0) {
|
|
18072
|
+
return identity_default4().fitExtent(
|
|
18073
|
+
[
|
|
18074
|
+
[inset, inset],
|
|
18075
|
+
[width - inset, height - inset]
|
|
18076
|
+
],
|
|
18077
|
+
geojson
|
|
18078
|
+
);
|
|
18079
|
+
}
|
|
17674
18080
|
return identity_default4().fitSize([width, height], geojson);
|
|
17675
18081
|
}
|
|
17676
18082
|
const projection2 = type === "albersUsa" ? albersUsa_default() : type === "equalEarth" ? equalEarth_default() : mercator_default();
|
|
17677
18083
|
const fitGeometry = type === "mercator" ? excludePolarFeatures(geojson) : geojson;
|
|
17678
|
-
|
|
18084
|
+
if (inset > 0) {
|
|
18085
|
+
projection2.fitExtent(
|
|
18086
|
+
[
|
|
18087
|
+
[inset, inset],
|
|
18088
|
+
[width - inset, height - inset]
|
|
18089
|
+
],
|
|
18090
|
+
fitGeometry
|
|
18091
|
+
);
|
|
18092
|
+
} else {
|
|
18093
|
+
projection2.fitSize([width, height], fitGeometry);
|
|
18094
|
+
}
|
|
17679
18095
|
return projection2;
|
|
17680
18096
|
}
|
|
17681
18097
|
function excludePolarFeatures(geojson) {
|
|
@@ -17695,6 +18111,7 @@ function excludePolarFeatures(geojson) {
|
|
|
17695
18111
|
}
|
|
17696
18112
|
|
|
17697
18113
|
// src/geo/compile-map.ts
|
|
18114
|
+
var OVERLAY_LEGEND_INSET = 18;
|
|
17698
18115
|
function validateGeoFeatures(geo) {
|
|
17699
18116
|
if (!geo.features) {
|
|
17700
18117
|
throw new Error(
|
|
@@ -17752,11 +18169,15 @@ function compileMap(spec, options) {
|
|
|
17752
18169
|
watermark
|
|
17753
18170
|
);
|
|
17754
18171
|
const padding = getBreakpoint(options.width) === "compact" ? Math.max(HPAD_COMPACT_MIN2, Math.round(theme.spacing.padding * HPAD_COMPACT_FRACTION2)) : theme.spacing.padding;
|
|
18172
|
+
const chromeLayout = resolveChromeLayout(
|
|
18173
|
+
spec,
|
|
18174
|
+
options
|
|
18175
|
+
);
|
|
17755
18176
|
const fullArea = {
|
|
17756
18177
|
x: padding,
|
|
17757
18178
|
y: padding + chrome.topHeight,
|
|
17758
18179
|
width: options.width - padding * 2,
|
|
17759
|
-
height: options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
|
|
18180
|
+
height: chromeLayout === "grow" ? options.height - padding * 2 : options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
|
|
17760
18181
|
};
|
|
17761
18182
|
if (fullArea.width <= 0 || fullArea.height <= 0) {
|
|
17762
18183
|
return emptyLayout3(chrome, theme, options, watermark);
|
|
@@ -17767,11 +18188,13 @@ function compileMap(spec, options) {
|
|
|
17767
18188
|
const compileWarnings = [];
|
|
17768
18189
|
const showLegend = mapSpec.legend?.show !== false;
|
|
17769
18190
|
const colorEncoding = mapSpec.encoding.color;
|
|
17770
|
-
const
|
|
18191
|
+
const showChoroplethLegend = showLegend && colorEncoding?.legend !== null;
|
|
18192
|
+
const isQuantitative = colorEncoding?.type === "quantitative";
|
|
17771
18193
|
const legendPosition = mapSpec.legend?.position === "bottom" ? "bottom" : "top";
|
|
18194
|
+
const pointLegendOverlay = mapSpec.legend?.position === "top-left";
|
|
17772
18195
|
let legendBlockHeight = 0;
|
|
17773
18196
|
let continuousContent = null;
|
|
17774
|
-
if (
|
|
18197
|
+
if (showChoroplethLegend && colorEncoding && isQuantitative) {
|
|
17775
18198
|
const colorValues = [];
|
|
17776
18199
|
for (const row of mapSpec.data) {
|
|
17777
18200
|
const v = row[colorEncoding.field];
|
|
@@ -17794,17 +18217,37 @@ function compileMap(spec, options) {
|
|
|
17794
18217
|
const labelRowHeight = Math.ceil(theme.fonts.sizes.small * 1.3);
|
|
17795
18218
|
legendBlockHeight = continuousContent.barHeight + CONTINUOUS_LABEL_GAP + labelRowHeight;
|
|
17796
18219
|
}
|
|
17797
|
-
} else if (
|
|
18220
|
+
} else if (showChoroplethLegend && colorEncoding) {
|
|
17798
18221
|
const labelHeight = Math.ceil(theme.fonts.sizes.small * 1.3);
|
|
17799
18222
|
legendBlockHeight = Math.max(10, labelHeight) + 6;
|
|
17800
18223
|
}
|
|
18224
|
+
if (showLegend && !pointLegendOverlay && mapSpec.points?.color) {
|
|
18225
|
+
if (mapSpec.points.color.type === "quantitative") {
|
|
18226
|
+
const labelRowHeight = Math.ceil(theme.fonts.sizes.small * 1.3);
|
|
18227
|
+
legendBlockHeight += CONTINUOUS_BAR_HEIGHT + CONTINUOUS_LABEL_GAP + labelRowHeight + 8;
|
|
18228
|
+
} else {
|
|
18229
|
+
const ptLabelHeight = Math.ceil(theme.fonts.sizes.small * 1.3);
|
|
18230
|
+
legendBlockHeight += Math.max(10, ptLabelHeight) + 6 + 8;
|
|
18231
|
+
}
|
|
18232
|
+
}
|
|
17801
18233
|
const legendReserveGap = legendBlockHeight > 0 ? 8 : 0;
|
|
17802
18234
|
const mapAreaHeight = fullArea.height - legendBlockHeight - legendReserveGap;
|
|
17803
18235
|
if (mapAreaHeight <= 0) {
|
|
17804
18236
|
return emptyLayout3(chrome, theme, options, watermark);
|
|
17805
18237
|
}
|
|
17806
18238
|
const projectionType = mapSpec.geo.projection;
|
|
17807
|
-
|
|
18239
|
+
let pointInset = 0;
|
|
18240
|
+
if (mapSpec.points) {
|
|
18241
|
+
const sizeRange = mapSpec.points.size?.scale?.range;
|
|
18242
|
+
pointInset = sizeRange ? sizeRange[1] : SIZE_SCALE_DEFAULTS.mapPoint.range[1];
|
|
18243
|
+
}
|
|
18244
|
+
const projection2 = createProjection(
|
|
18245
|
+
projectionType,
|
|
18246
|
+
fullArea.width,
|
|
18247
|
+
mapAreaHeight,
|
|
18248
|
+
geoCollection,
|
|
18249
|
+
pointInset
|
|
18250
|
+
);
|
|
17808
18251
|
const pathGen = path_default(projection2);
|
|
17809
18252
|
if (projectionType !== "identity") {
|
|
17810
18253
|
for (const feat of geoFeatures) {
|
|
@@ -17819,31 +18262,44 @@ function compileMap(spec, options) {
|
|
|
17819
18262
|
}
|
|
17820
18263
|
}
|
|
17821
18264
|
}
|
|
17822
|
-
|
|
17823
|
-
|
|
17824
|
-
|
|
17825
|
-
|
|
17826
|
-
|
|
17827
|
-
|
|
17828
|
-
|
|
17829
|
-
|
|
17830
|
-
|
|
17831
|
-
|
|
17832
|
-
|
|
18265
|
+
let joined = /* @__PURE__ */ new Map();
|
|
18266
|
+
if (mapSpec.encoding.key) {
|
|
18267
|
+
const featureObjs = geoFeatures.map((f) => ({
|
|
18268
|
+
id: f.id,
|
|
18269
|
+
properties: f.properties ?? {}
|
|
18270
|
+
}));
|
|
18271
|
+
const joinResult = joinDataToFeatures(
|
|
18272
|
+
featureObjs,
|
|
18273
|
+
mapSpec.data,
|
|
18274
|
+
mapSpec.encoding.key.field,
|
|
18275
|
+
mapSpec.geo.idField
|
|
18276
|
+
);
|
|
18277
|
+
joined = joinResult.joined;
|
|
18278
|
+
const filteredJoinWarnings = mapSpec.data.length === 0 && mapSpec.points ? joinResult.warnings.filter((w) => w.code !== "UNMATCHED_FEATURES") : joinResult.warnings;
|
|
18279
|
+
compileWarnings.push(...filteredJoinWarnings);
|
|
18280
|
+
}
|
|
17833
18281
|
const droppedFeatures = [];
|
|
17834
|
-
const formatter = buildD3Formatter2(mapSpec.valueFormat) ?? formatNumber3;
|
|
18282
|
+
const formatter = buildD3Formatter2(mapSpec.encoding?.color?.format ?? mapSpec.valueFormat) ?? formatNumber3;
|
|
17835
18283
|
const neutralFill = isDarkMode ? "#2a2a2a" : "#e8e8e8";
|
|
17836
18284
|
const neutralStroke = isDarkMode ? "rgba(255,255,255,0.08)" : "rgba(0,0,0,0.06)";
|
|
17837
18285
|
let featureMarks;
|
|
17838
18286
|
let continuousLegend = null;
|
|
17839
18287
|
let categoricalLegend = null;
|
|
17840
|
-
if (
|
|
18288
|
+
if (!colorEncoding) {
|
|
18289
|
+
featureMarks = buildBasemapMarks({
|
|
18290
|
+
geoFeatures,
|
|
18291
|
+
pathGen,
|
|
18292
|
+
neutralFill,
|
|
18293
|
+
neutralStroke,
|
|
18294
|
+
droppedFeatures
|
|
18295
|
+
});
|
|
18296
|
+
} else if (isQuantitative) {
|
|
17841
18297
|
const result = buildQuantitativeMarks({
|
|
17842
18298
|
geoFeatures,
|
|
17843
18299
|
pathGen,
|
|
17844
18300
|
joined,
|
|
17845
18301
|
colorField: colorEncoding.field,
|
|
17846
|
-
palette:
|
|
18302
|
+
palette: colorEncoding.scale?.scheme ?? "blue",
|
|
17847
18303
|
isDarkMode: !!isDarkMode,
|
|
17848
18304
|
neutralFill,
|
|
17849
18305
|
neutralStroke,
|
|
@@ -17851,7 +18307,7 @@ function compileMap(spec, options) {
|
|
|
17851
18307
|
droppedFeatures
|
|
17852
18308
|
});
|
|
17853
18309
|
featureMarks = result.marks;
|
|
17854
|
-
if (
|
|
18310
|
+
if (showChoroplethLegend && continuousContent) {
|
|
17855
18311
|
const labelStyle = {
|
|
17856
18312
|
fontFamily: theme.fonts.family,
|
|
17857
18313
|
fontSize: theme.fonts.sizes.small,
|
|
@@ -17893,6 +18349,7 @@ function compileMap(spec, options) {
|
|
|
17893
18349
|
pathGen,
|
|
17894
18350
|
joined,
|
|
17895
18351
|
colorField: colorEncoding.field,
|
|
18352
|
+
colorScale: colorEncoding.scale,
|
|
17896
18353
|
isDarkMode: !!isDarkMode,
|
|
17897
18354
|
neutralFill,
|
|
17898
18355
|
neutralStroke,
|
|
@@ -17900,7 +18357,8 @@ function compileMap(spec, options) {
|
|
|
17900
18357
|
fullArea,
|
|
17901
18358
|
mapAreaHeight,
|
|
17902
18359
|
legendReserveGap,
|
|
17903
|
-
|
|
18360
|
+
legendPosition,
|
|
18361
|
+
showLegend: showChoroplethLegend,
|
|
17904
18362
|
droppedFeatures
|
|
17905
18363
|
});
|
|
17906
18364
|
featureMarks = result.marks;
|
|
@@ -17928,6 +18386,201 @@ function compileMap(spec, options) {
|
|
|
17928
18386
|
const interiorStroke = isDarkMode ? "rgba(255,255,255,0.3)" : "rgba(0,0,0,0.15)";
|
|
17929
18387
|
const outlineStroke = isDarkMode ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.3)";
|
|
17930
18388
|
const borders = { interiorPath, outlinePath, interiorStroke, outlineStroke };
|
|
18389
|
+
const pointMarks = [];
|
|
18390
|
+
let pointCategoricalLegend = null;
|
|
18391
|
+
let pointContinuousLegend = null;
|
|
18392
|
+
if (mapSpec.points) {
|
|
18393
|
+
const pts = mapSpec.points;
|
|
18394
|
+
const lonField = pts.longitude.field;
|
|
18395
|
+
const latField = pts.latitude.field;
|
|
18396
|
+
const keyChannel = pts.key;
|
|
18397
|
+
const sizeScale = buildSizeScale(pts.size, pts.data, SIZE_SCALE_DEFAULTS.mapPoint);
|
|
18398
|
+
let pointColorScale = null;
|
|
18399
|
+
const pointCategories = [];
|
|
18400
|
+
const pointCategoryColors = /* @__PURE__ */ new Map();
|
|
18401
|
+
if (pts.color) {
|
|
18402
|
+
if (pts.color.type === "quantitative") {
|
|
18403
|
+
const colorValues = [];
|
|
18404
|
+
for (const row of pts.data) {
|
|
18405
|
+
const raw = row[pts.color.field];
|
|
18406
|
+
if (raw != null) {
|
|
18407
|
+
const v = Number(raw);
|
|
18408
|
+
if (Number.isFinite(v)) colorValues.push(v);
|
|
18409
|
+
}
|
|
18410
|
+
}
|
|
18411
|
+
if (colorValues.length > 0) {
|
|
18412
|
+
const palette = pts.color.scale?.scheme ?? "blue";
|
|
18413
|
+
const paletteStops = [...SEQUENTIAL_PALETTES2[palette] ?? SEQUENTIAL_PALETTES2.blue];
|
|
18414
|
+
const qScale = quantile2().domain(colorValues).range(paletteStops);
|
|
18415
|
+
pointColorScale = (val) => {
|
|
18416
|
+
const v = Number(val);
|
|
18417
|
+
return Number.isFinite(v) ? qScale(v) : CATEGORICAL_PALETTE[0];
|
|
18418
|
+
};
|
|
18419
|
+
}
|
|
18420
|
+
} else {
|
|
18421
|
+
const explicitDomain = pts.color.scale?.domain;
|
|
18422
|
+
const explicitRange = pts.color.scale?.range;
|
|
18423
|
+
if (explicitDomain && Array.isArray(explicitDomain)) {
|
|
18424
|
+
for (const cat of explicitDomain) {
|
|
18425
|
+
pointCategories.push(String(cat));
|
|
18426
|
+
}
|
|
18427
|
+
}
|
|
18428
|
+
const seen = new Set(pointCategories);
|
|
18429
|
+
for (const row of pts.data) {
|
|
18430
|
+
const raw = row[pts.color.field];
|
|
18431
|
+
if (raw != null) {
|
|
18432
|
+
const cat = String(raw);
|
|
18433
|
+
if (!seen.has(cat)) {
|
|
18434
|
+
seen.add(cat);
|
|
18435
|
+
pointCategories.push(cat);
|
|
18436
|
+
}
|
|
18437
|
+
}
|
|
18438
|
+
}
|
|
18439
|
+
const colorSource = explicitRange && Array.isArray(explicitRange) ? explicitRange : CATEGORICAL_PALETTE;
|
|
18440
|
+
for (let i = 0; i < pointCategories.length; i++) {
|
|
18441
|
+
pointCategoryColors.set(pointCategories[i], colorSource[i % colorSource.length]);
|
|
18442
|
+
}
|
|
18443
|
+
pointColorScale = (val) => {
|
|
18444
|
+
if (val == null) return colorSource[0];
|
|
18445
|
+
return pointCategoryColors.get(String(val)) ?? colorSource[0];
|
|
18446
|
+
};
|
|
18447
|
+
}
|
|
18448
|
+
}
|
|
18449
|
+
const defaultFill = CATEGORICAL_PALETTE[0];
|
|
18450
|
+
const pointOpacity = pts.opacity ?? 0.65;
|
|
18451
|
+
let animIndex = 0;
|
|
18452
|
+
const droppedPoints = [];
|
|
18453
|
+
for (let i = 0; i < pts.data.length; i++) {
|
|
18454
|
+
const row = pts.data[i];
|
|
18455
|
+
const lon = Number(row[lonField]);
|
|
18456
|
+
const lat = Number(row[latField]);
|
|
18457
|
+
if (!Number.isFinite(lon) || !Number.isFinite(lat)) continue;
|
|
18458
|
+
const projected = projection2([lon, lat]);
|
|
18459
|
+
if (!projected) {
|
|
18460
|
+
droppedPoints.push([lon, lat]);
|
|
18461
|
+
continue;
|
|
18462
|
+
}
|
|
18463
|
+
const r = sizeScale ? sizeScale.scale(Number(row[sizeScale.field])) : 5;
|
|
18464
|
+
const fill = pointColorScale && pts.color ? pointColorScale(row[pts.color.field]) : defaultFill;
|
|
18465
|
+
const key = keyChannel ? String(row[keyChannel.field] ?? i) : String(i);
|
|
18466
|
+
const stroke = isDarkMode ? "rgba(0,0,0,0.5)" : "rgba(255,255,255,0.8)";
|
|
18467
|
+
pointMarks.push({
|
|
18468
|
+
type: "map-point",
|
|
18469
|
+
cx: projected[0],
|
|
18470
|
+
cy: projected[1],
|
|
18471
|
+
r,
|
|
18472
|
+
fill,
|
|
18473
|
+
stroke,
|
|
18474
|
+
strokeWidth: 1,
|
|
18475
|
+
fillOpacity: pointOpacity,
|
|
18476
|
+
key,
|
|
18477
|
+
data: row,
|
|
18478
|
+
aria: { role: "img", label: key },
|
|
18479
|
+
animationIndex: animIndex++
|
|
18480
|
+
});
|
|
18481
|
+
}
|
|
18482
|
+
if (droppedPoints.length > 0) {
|
|
18483
|
+
const sample = droppedPoints.slice(0, 3).map(([lo, la]) => `[${lo}, ${la}]`).join(", ");
|
|
18484
|
+
compileWarnings.push({
|
|
18485
|
+
code: "POINT_NULL_PROJECTION",
|
|
18486
|
+
message: `${droppedPoints.length} point(s) project to null and were dropped (e.g. ${sample}). This is common with albersUsa which excludes territories. Consider using projection: "mercator" or "equalEarth".`,
|
|
18487
|
+
context: { count: droppedPoints.length, samples: droppedPoints.slice(0, 5) }
|
|
18488
|
+
});
|
|
18489
|
+
}
|
|
18490
|
+
if (pts.color && pts.color.type !== "quantitative" && pointCategories.length > 0 && showLegend) {
|
|
18491
|
+
const labelStyle = {
|
|
18492
|
+
fontFamily: theme.fonts.family,
|
|
18493
|
+
fontSize: theme.fonts.sizes.small,
|
|
18494
|
+
fontWeight: theme.fonts.weights.normal,
|
|
18495
|
+
fill: theme.colors.text,
|
|
18496
|
+
lineHeight: 1.2
|
|
18497
|
+
};
|
|
18498
|
+
const entries = pointCategories.map((cat) => ({
|
|
18499
|
+
label: cat,
|
|
18500
|
+
color: pointCategoryColors.get(cat),
|
|
18501
|
+
shape: "circle"
|
|
18502
|
+
}));
|
|
18503
|
+
const bottomChoroplethHeight = legendPosition === "bottom" ? continuousLegend?.bounds.height ?? categoricalLegend?.bounds.height ?? 0 : 0;
|
|
18504
|
+
const choroplethLegendGap = bottomChoroplethHeight > 0 ? 8 : 0;
|
|
18505
|
+
const legendX = pointLegendOverlay ? fullArea.x + OVERLAY_LEGEND_INSET : fullArea.x;
|
|
18506
|
+
const legendY = pointLegendOverlay ? fullArea.y + OVERLAY_LEGEND_INSET : fullArea.y + mapAreaHeight + legendReserveGap + bottomChoroplethHeight + choroplethLegendGap;
|
|
18507
|
+
const legendWidth = pointLegendOverlay ? fullArea.width - OVERLAY_LEGEND_INSET * 2 : fullArea.width;
|
|
18508
|
+
const swatchSize = 10;
|
|
18509
|
+
const swatchGap = 6;
|
|
18510
|
+
const entryGap = 16;
|
|
18511
|
+
pointCategoricalLegend = {
|
|
18512
|
+
type: "categorical",
|
|
18513
|
+
position: pointLegendOverlay ? "top-left" : "bottom",
|
|
18514
|
+
bounds: { x: legendX, y: legendY, width: legendWidth, height: swatchSize + 6 },
|
|
18515
|
+
labelStyle,
|
|
18516
|
+
entries,
|
|
18517
|
+
swatchSize,
|
|
18518
|
+
swatchGap,
|
|
18519
|
+
entryGap,
|
|
18520
|
+
swatchChipFill: isDarkMode ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.04)"
|
|
18521
|
+
};
|
|
18522
|
+
}
|
|
18523
|
+
if (pts.color && pts.color.type === "quantitative" && showLegend) {
|
|
18524
|
+
const ptColorValues = [];
|
|
18525
|
+
for (const row of pts.data) {
|
|
18526
|
+
const v = row[pts.color.field];
|
|
18527
|
+
if (v != null) {
|
|
18528
|
+
const n = Number(v);
|
|
18529
|
+
if (Number.isFinite(n)) ptColorValues.push(n);
|
|
18530
|
+
}
|
|
18531
|
+
}
|
|
18532
|
+
const ptLegendChannel = {
|
|
18533
|
+
...pts.color,
|
|
18534
|
+
scale: { ...pts.color.scale, type: "quantile" }
|
|
18535
|
+
};
|
|
18536
|
+
const ptContinuousContent = computeContinuousLegendContentForChannel(
|
|
18537
|
+
ptColorValues,
|
|
18538
|
+
ptLegendChannel,
|
|
18539
|
+
theme,
|
|
18540
|
+
fullArea.width
|
|
18541
|
+
);
|
|
18542
|
+
if (ptContinuousContent) {
|
|
18543
|
+
const labelStyle = {
|
|
18544
|
+
fontFamily: theme.fonts.family,
|
|
18545
|
+
fontSize: theme.fonts.sizes.small,
|
|
18546
|
+
fontWeight: theme.fonts.weights.normal,
|
|
18547
|
+
fill: theme.colors.text,
|
|
18548
|
+
lineHeight: 1.3,
|
|
18549
|
+
fontVariant: "tabular-nums"
|
|
18550
|
+
};
|
|
18551
|
+
const bottomChoroplethH = legendPosition === "bottom" ? continuousLegend?.bounds.height ?? categoricalLegend?.bounds.height ?? 0 : 0;
|
|
18552
|
+
const choroplethLegendGap = bottomChoroplethH > 0 ? 8 : 0;
|
|
18553
|
+
const legendX = pointLegendOverlay ? fullArea.x + OVERLAY_LEGEND_INSET : fullArea.x;
|
|
18554
|
+
const legendY = pointLegendOverlay ? fullArea.y + OVERLAY_LEGEND_INSET : fullArea.y + mapAreaHeight + legendReserveGap + bottomChoroplethH + choroplethLegendGap;
|
|
18555
|
+
const ptLabelRowHeight = Math.ceil(theme.fonts.sizes.small * 1.3);
|
|
18556
|
+
const ptLegendHeight = ptContinuousContent.barHeight + CONTINUOUS_LABEL_GAP + ptLabelRowHeight;
|
|
18557
|
+
const bounds = {
|
|
18558
|
+
x: legendX,
|
|
18559
|
+
y: legendY,
|
|
18560
|
+
width: ptContinuousContent.barWidth,
|
|
18561
|
+
height: ptLegendHeight
|
|
18562
|
+
};
|
|
18563
|
+
const bar = {
|
|
18564
|
+
x: bounds.x,
|
|
18565
|
+
y: bounds.y,
|
|
18566
|
+
width: ptContinuousContent.barWidth,
|
|
18567
|
+
height: ptContinuousContent.barHeight
|
|
18568
|
+
};
|
|
18569
|
+
pointContinuousLegend = {
|
|
18570
|
+
type: "continuous",
|
|
18571
|
+
mode: ptContinuousContent.mode,
|
|
18572
|
+
position: pointLegendOverlay ? "top-left" : "bottom",
|
|
18573
|
+
bounds,
|
|
18574
|
+
labelStyle,
|
|
18575
|
+
bar,
|
|
18576
|
+
colorStops: ptContinuousContent.colorStops,
|
|
18577
|
+
bins: ptContinuousContent.bins.map((b) => ({ ...b, x: b.x + bar.x })),
|
|
18578
|
+
ticks: ptContinuousContent.ticks.map((t) => ({ ...t, x: t.x + bar.x })),
|
|
18579
|
+
labelY: bar.y + bar.height + CONTINUOUS_LABEL_GAP + labelStyle.fontSize
|
|
18580
|
+
};
|
|
18581
|
+
}
|
|
18582
|
+
}
|
|
18583
|
+
}
|
|
17931
18584
|
const tooltipChannels = mapSpec.encoding.tooltip ? Array.isArray(mapSpec.encoding.tooltip) ? mapSpec.encoding.tooltip : [mapSpec.encoding.tooltip] : null;
|
|
17932
18585
|
const tooltipDescriptors = /* @__PURE__ */ new Map();
|
|
17933
18586
|
for (const mark of featureMarks) {
|
|
@@ -17945,12 +18598,12 @@ function compileMap(spec, options) {
|
|
|
17945
18598
|
});
|
|
17946
18599
|
}
|
|
17947
18600
|
}
|
|
17948
|
-
} else if (isQuantitative && mark.data[colorEncoding.field] != null) {
|
|
18601
|
+
} else if (colorEncoding && isQuantitative && mark.data[colorEncoding.field] != null) {
|
|
17949
18602
|
fields.push({
|
|
17950
18603
|
label: colorEncoding.title ?? colorEncoding.field,
|
|
17951
18604
|
value: formatter(Number(mark.data[colorEncoding.field]))
|
|
17952
18605
|
});
|
|
17953
|
-
} else if (!isQuantitative && mark.data[colorEncoding.field] != null) {
|
|
18606
|
+
} else if (colorEncoding && !isQuantitative && mark.data[colorEncoding.field] != null) {
|
|
17954
18607
|
fields.push({
|
|
17955
18608
|
label: colorEncoding.title ?? colorEncoding.field,
|
|
17956
18609
|
value: String(mark.data[colorEncoding.field])
|
|
@@ -17958,13 +18611,63 @@ function compileMap(spec, options) {
|
|
|
17958
18611
|
}
|
|
17959
18612
|
tooltipDescriptors.set(String(mark.id), { fields });
|
|
17960
18613
|
}
|
|
18614
|
+
if (mapSpec.points && pointMarks.length > 0) {
|
|
18615
|
+
const ptTooltipChannels = mapSpec.points.tooltip ? Array.isArray(mapSpec.points.tooltip) ? mapSpec.points.tooltip : [mapSpec.points.tooltip] : null;
|
|
18616
|
+
for (const pm of pointMarks) {
|
|
18617
|
+
const fields = [];
|
|
18618
|
+
if (ptTooltipChannels) {
|
|
18619
|
+
for (const ch of ptTooltipChannels) {
|
|
18620
|
+
const val = pm.data[ch.field];
|
|
18621
|
+
if (val != null) {
|
|
18622
|
+
fields.push({
|
|
18623
|
+
label: ch.title ?? ch.field,
|
|
18624
|
+
value: ch.type === "quantitative" ? formatter(Number(val)) : String(val)
|
|
18625
|
+
});
|
|
18626
|
+
}
|
|
18627
|
+
}
|
|
18628
|
+
} else {
|
|
18629
|
+
const pts = mapSpec.points;
|
|
18630
|
+
if (pts.key) {
|
|
18631
|
+
const val = pm.data[pts.key.field];
|
|
18632
|
+
if (val != null)
|
|
18633
|
+
fields.push({ label: pts.key.title ?? pts.key.field, value: String(val) });
|
|
18634
|
+
}
|
|
18635
|
+
if (pts.color) {
|
|
18636
|
+
const val = pm.data[pts.color.field];
|
|
18637
|
+
if (val != null)
|
|
18638
|
+
fields.push({
|
|
18639
|
+
label: pts.color.title ?? pts.color.field,
|
|
18640
|
+
value: pts.color.type === "quantitative" ? formatter(Number(val)) : String(val)
|
|
18641
|
+
});
|
|
18642
|
+
}
|
|
18643
|
+
if (pts.size) {
|
|
18644
|
+
const val = pm.data[pts.size.field];
|
|
18645
|
+
if (val != null)
|
|
18646
|
+
fields.push({ label: pts.size.title ?? pts.size.field, value: formatter(Number(val)) });
|
|
18647
|
+
}
|
|
18648
|
+
}
|
|
18649
|
+
if (fields.length > 0) {
|
|
18650
|
+
tooltipDescriptors.set(`point:${pm.key}`, { fields });
|
|
18651
|
+
}
|
|
18652
|
+
}
|
|
18653
|
+
}
|
|
18654
|
+
const a11yDataField = colorEncoding?.field;
|
|
17961
18655
|
const a11y = {
|
|
17962
|
-
altText: `Map showing ${geoFeatures.length} regions with ${isQuantitative ? "quantitative" : "categorical"} data`,
|
|
17963
|
-
dataTableFallback:
|
|
18656
|
+
altText: pointMarks.length > 0 ? colorEncoding ? `Map showing ${geoFeatures.length} regions with ${isQuantitative ? "quantitative" : "categorical"} data and ${pointMarks.length} point markers` : `Map showing ${geoFeatures.length} regions with ${pointMarks.length} point markers` : colorEncoding ? `Map showing ${geoFeatures.length} regions with ${isQuantitative ? "quantitative" : "categorical"} data` : `Map showing ${geoFeatures.length} regions`,
|
|
18657
|
+
dataTableFallback: [
|
|
18658
|
+
...featureMarks.filter((m) => m.data).map((m) => [
|
|
18659
|
+
m.name ?? String(m.id),
|
|
18660
|
+
a11yDataField ? String(m.data?.[a11yDataField] ?? "") : ""
|
|
18661
|
+
]),
|
|
18662
|
+
...pointMarks.map((pm) => [
|
|
18663
|
+
pm.key,
|
|
18664
|
+
mapSpec.points?.color ? String(pm.data[mapSpec.points.color.field] ?? "") : ""
|
|
18665
|
+
])
|
|
18666
|
+
],
|
|
17964
18667
|
role: "img",
|
|
17965
|
-
keyboardNavigable: featureMarks.length > 0
|
|
18668
|
+
keyboardNavigable: featureMarks.length > 0 || pointMarks.length > 0
|
|
17966
18669
|
};
|
|
17967
|
-
const resolvedFocus = resolveFocus(mapSpec.geo.focus, featureMarks, compileWarnings);
|
|
18670
|
+
const resolvedFocus = resolveFocus(mapSpec.geo.focus, featureMarks, pointMarks, compileWarnings);
|
|
17968
18671
|
const resolvedAnimation = resolveAnimation(mapSpec.animation);
|
|
17969
18672
|
const mapAreaY = legendPosition === "top" && legendBlockHeight > 0 ? fullArea.y + legendBlockHeight + legendReserveGap : fullArea.y;
|
|
17970
18673
|
const mapArea = {
|
|
@@ -17987,6 +18690,9 @@ function compileMap(spec, options) {
|
|
|
17987
18690
|
borders,
|
|
17988
18691
|
continuousLegend,
|
|
17989
18692
|
categoricalLegend,
|
|
18693
|
+
pointMarks,
|
|
18694
|
+
pointCategoricalLegend,
|
|
18695
|
+
pointContinuousLegend,
|
|
17990
18696
|
tooltipDescriptors,
|
|
17991
18697
|
a11y,
|
|
17992
18698
|
theme,
|
|
@@ -18000,6 +18706,46 @@ function compileMap(spec, options) {
|
|
|
18000
18706
|
focus: resolvedFocus
|
|
18001
18707
|
};
|
|
18002
18708
|
}
|
|
18709
|
+
function buildBasemapMarks(opts) {
|
|
18710
|
+
const { geoFeatures, pathGen, neutralFill, neutralStroke, droppedFeatures } = opts;
|
|
18711
|
+
const marks = [];
|
|
18712
|
+
let animIndex = 0;
|
|
18713
|
+
for (const feat of geoFeatures) {
|
|
18714
|
+
const pathD = pathGen(feat);
|
|
18715
|
+
if (!pathD) {
|
|
18716
|
+
const name2 = feat.properties?.name ?? String(feat.id);
|
|
18717
|
+
droppedFeatures.push(name2);
|
|
18718
|
+
continue;
|
|
18719
|
+
}
|
|
18720
|
+
const featureId = feat.id;
|
|
18721
|
+
const props = feat.properties;
|
|
18722
|
+
const name = props?.name ?? props?.NAME;
|
|
18723
|
+
const b = pathGen.bounds(feat);
|
|
18724
|
+
const bx = Number.isFinite(b[0][0]) ? b[0][0] : 0;
|
|
18725
|
+
const by = Number.isFinite(b[0][1]) ? b[0][1] : 0;
|
|
18726
|
+
const bw = Number.isFinite(b[1][0]) ? b[1][0] - bx : 0;
|
|
18727
|
+
const bh = Number.isFinite(b[1][1]) ? b[1][1] - by : 0;
|
|
18728
|
+
const bounds = { x: bx, y: by, width: bw, height: bh };
|
|
18729
|
+
const rawCentroid = pathGen.centroid(feat);
|
|
18730
|
+
const cx = Number.isFinite(rawCentroid[0]) ? rawCentroid[0] : bx + bw / 2;
|
|
18731
|
+
const cy = Number.isFinite(rawCentroid[1]) ? rawCentroid[1] : by + bh / 2;
|
|
18732
|
+
marks.push({
|
|
18733
|
+
type: "map-feature",
|
|
18734
|
+
path: pathD,
|
|
18735
|
+
fill: neutralFill,
|
|
18736
|
+
stroke: neutralStroke,
|
|
18737
|
+
strokeWidth: 0.5,
|
|
18738
|
+
id: featureId,
|
|
18739
|
+
name,
|
|
18740
|
+
data: null,
|
|
18741
|
+
aria: { role: "img", label: name ?? String(featureId) },
|
|
18742
|
+
animationIndex: animIndex++,
|
|
18743
|
+
bounds,
|
|
18744
|
+
centroid: [cx, cy]
|
|
18745
|
+
});
|
|
18746
|
+
}
|
|
18747
|
+
return marks;
|
|
18748
|
+
}
|
|
18003
18749
|
function buildQuantitativeMarks(opts) {
|
|
18004
18750
|
const {
|
|
18005
18751
|
geoFeatures,
|
|
@@ -18021,7 +18767,7 @@ function buildQuantitativeMarks(opts) {
|
|
|
18021
18767
|
if (!Number.isNaN(v)) values.push(v);
|
|
18022
18768
|
}
|
|
18023
18769
|
}
|
|
18024
|
-
const paletteStops = [...
|
|
18770
|
+
const paletteStops = [...SEQUENTIAL_PALETTES2[palette] ?? SEQUENTIAL_PALETTES2.blue];
|
|
18025
18771
|
const colorScale = quantile2().domain(values).range(paletteStops);
|
|
18026
18772
|
const marks = [];
|
|
18027
18773
|
let animIndex = 0;
|
|
@@ -18084,6 +18830,7 @@ function buildCategoricalMarks(opts) {
|
|
|
18084
18830
|
pathGen,
|
|
18085
18831
|
joined,
|
|
18086
18832
|
colorField,
|
|
18833
|
+
colorScale: scaleConfig,
|
|
18087
18834
|
isDarkMode,
|
|
18088
18835
|
neutralFill,
|
|
18089
18836
|
neutralStroke,
|
|
@@ -18091,11 +18838,18 @@ function buildCategoricalMarks(opts) {
|
|
|
18091
18838
|
fullArea,
|
|
18092
18839
|
mapAreaHeight,
|
|
18093
18840
|
legendReserveGap,
|
|
18841
|
+
legendPosition,
|
|
18094
18842
|
showLegend,
|
|
18095
18843
|
droppedFeatures
|
|
18096
18844
|
} = opts;
|
|
18097
18845
|
const categories = [];
|
|
18098
|
-
const
|
|
18846
|
+
const explicitDomain = scaleConfig?.domain;
|
|
18847
|
+
if (explicitDomain && Array.isArray(explicitDomain)) {
|
|
18848
|
+
for (const cat of explicitDomain) {
|
|
18849
|
+
categories.push(String(cat));
|
|
18850
|
+
}
|
|
18851
|
+
}
|
|
18852
|
+
const seen = new Set(categories);
|
|
18099
18853
|
for (const row of joined.values()) {
|
|
18100
18854
|
const raw = row[colorField];
|
|
18101
18855
|
if (raw != null) {
|
|
@@ -18106,9 +18860,11 @@ function buildCategoricalMarks(opts) {
|
|
|
18106
18860
|
}
|
|
18107
18861
|
}
|
|
18108
18862
|
}
|
|
18863
|
+
const explicitRange = scaleConfig?.range;
|
|
18864
|
+
const colorSource = explicitRange && Array.isArray(explicitRange) ? explicitRange : CATEGORICAL_PALETTE;
|
|
18109
18865
|
const categoryColors = /* @__PURE__ */ new Map();
|
|
18110
18866
|
for (let i = 0; i < categories.length; i++) {
|
|
18111
|
-
categoryColors.set(categories[i],
|
|
18867
|
+
categoryColors.set(categories[i], colorSource[i % colorSource.length]);
|
|
18112
18868
|
}
|
|
18113
18869
|
const marks = [];
|
|
18114
18870
|
let animIndex = 0;
|
|
@@ -18176,14 +18932,14 @@ function buildCategoricalMarks(opts) {
|
|
|
18176
18932
|
shape: "square"
|
|
18177
18933
|
}));
|
|
18178
18934
|
const legendX = fullArea.x;
|
|
18179
|
-
const legendY = fullArea.y + mapAreaHeight + legendReserveGap;
|
|
18935
|
+
const legendY = legendPosition === "bottom" ? fullArea.y + mapAreaHeight + legendReserveGap : fullArea.y + legendReserveGap;
|
|
18180
18936
|
const legendWidth = fullArea.width;
|
|
18181
18937
|
const swatchSize = 10;
|
|
18182
18938
|
const swatchGap = 6;
|
|
18183
18939
|
const entryGap = 16;
|
|
18184
18940
|
legend = {
|
|
18185
18941
|
type: "categorical",
|
|
18186
|
-
position:
|
|
18942
|
+
position: legendPosition,
|
|
18187
18943
|
bounds: { x: legendX, y: legendY, width: legendWidth, height: swatchSize + 6 },
|
|
18188
18944
|
labelStyle,
|
|
18189
18945
|
entries,
|
|
@@ -18195,8 +18951,40 @@ function buildCategoricalMarks(opts) {
|
|
|
18195
18951
|
}
|
|
18196
18952
|
return { marks, legend };
|
|
18197
18953
|
}
|
|
18198
|
-
function resolveFocus(focus, features, warnings) {
|
|
18954
|
+
function resolveFocus(focus, features, pointMarks, warnings) {
|
|
18199
18955
|
if (focus === null || focus === void 0) return null;
|
|
18956
|
+
if (typeof focus === "object" && !Array.isArray(focus) && "points" in focus) {
|
|
18957
|
+
const filter = focus.points;
|
|
18958
|
+
const matched = filter === true ? pointMarks : pointMarks.filter((p) => p.data[filter.field] === filter.value);
|
|
18959
|
+
if (matched.length === 0) {
|
|
18960
|
+
const detail = filter === true ? "the map has no points to fit" : `no points match { field: "${filter.field}", value: ${JSON.stringify(filter.value)} }`;
|
|
18961
|
+
warnings.push({
|
|
18962
|
+
code: "FOCUS_UNMATCHED",
|
|
18963
|
+
message: `geo.focus.points is set but ${detail}`,
|
|
18964
|
+
context: {}
|
|
18965
|
+
});
|
|
18966
|
+
return null;
|
|
18967
|
+
}
|
|
18968
|
+
const padding2 = focus.padding ?? 16;
|
|
18969
|
+
let minX2 = Infinity;
|
|
18970
|
+
let minY2 = Infinity;
|
|
18971
|
+
let maxX2 = -Infinity;
|
|
18972
|
+
let maxY2 = -Infinity;
|
|
18973
|
+
for (const p of matched) {
|
|
18974
|
+
minX2 = Math.min(minX2, p.cx);
|
|
18975
|
+
minY2 = Math.min(minY2, p.cy);
|
|
18976
|
+
maxX2 = Math.max(maxX2, p.cx);
|
|
18977
|
+
maxY2 = Math.max(maxY2, p.cy);
|
|
18978
|
+
}
|
|
18979
|
+
const w = Math.max(maxX2 - minX2, 1);
|
|
18980
|
+
const h = Math.max(maxY2 - minY2, 1);
|
|
18981
|
+
return {
|
|
18982
|
+
target: { x: minX2, y: minY2, width: w, height: h, padding: padding2 },
|
|
18983
|
+
// No feature ids: focus-dim is a feature concept, so a points focus
|
|
18984
|
+
// dims no features (there are no "other" features to mute).
|
|
18985
|
+
ids: []
|
|
18986
|
+
};
|
|
18987
|
+
}
|
|
18200
18988
|
let ids;
|
|
18201
18989
|
let padding = 16;
|
|
18202
18990
|
if (typeof focus === "string" || typeof focus === "number") {
|
|
@@ -18256,6 +19044,9 @@ function emptyLayout3(chrome, theme, options, watermark) {
|
|
|
18256
19044
|
},
|
|
18257
19045
|
continuousLegend: null,
|
|
18258
19046
|
categoricalLegend: null,
|
|
19047
|
+
pointMarks: [],
|
|
19048
|
+
pointCategoricalLegend: null,
|
|
19049
|
+
pointContinuousLegend: null,
|
|
18259
19050
|
tooltipDescriptors: /* @__PURE__ */ new Map(),
|
|
18260
19051
|
a11y: {
|
|
18261
19052
|
altText: "Empty map",
|
|
@@ -18283,6 +19074,65 @@ import {
|
|
|
18283
19074
|
resolveTheme as resolveTheme4
|
|
18284
19075
|
} from "@opendata-ai/openchart-core";
|
|
18285
19076
|
|
|
19077
|
+
// src/graphs/animation.ts
|
|
19078
|
+
var ENTER_DEFAULT = {
|
|
19079
|
+
duration: 500,
|
|
19080
|
+
ease: "smooth",
|
|
19081
|
+
stagger: true,
|
|
19082
|
+
cameraFit: true
|
|
19083
|
+
};
|
|
19084
|
+
var UPDATE_DEFAULT = { duration: 300, ease: "smooth" };
|
|
19085
|
+
var EXIT_DEFAULT = { duration: 300, ease: "smooth" };
|
|
19086
|
+
var CAMERA_DEFAULT = { duration: "auto", ease: "smooth" };
|
|
19087
|
+
var HOVER_DEFAULT = { duration: 150, ease: "smooth" };
|
|
19088
|
+
function fullDefaults() {
|
|
19089
|
+
return {
|
|
19090
|
+
enter: { ...ENTER_DEFAULT },
|
|
19091
|
+
update: { ...UPDATE_DEFAULT },
|
|
19092
|
+
exit: { ...EXIT_DEFAULT },
|
|
19093
|
+
camera: { ...CAMERA_DEFAULT },
|
|
19094
|
+
hover: { ...HOVER_DEFAULT }
|
|
19095
|
+
};
|
|
19096
|
+
}
|
|
19097
|
+
function resolveGraphAnimation(spec) {
|
|
19098
|
+
if (spec === void 0 || spec === true) return fullDefaults();
|
|
19099
|
+
if (spec === false) return void 0;
|
|
19100
|
+
return {
|
|
19101
|
+
enter: resolveEnter(spec.enter),
|
|
19102
|
+
update: resolveDurationPhase(spec.update, UPDATE_DEFAULT),
|
|
19103
|
+
exit: resolveDurationPhase(spec.exit, EXIT_DEFAULT),
|
|
19104
|
+
camera: resolveCamera(spec.camera),
|
|
19105
|
+
hover: resolveDurationPhase(spec.hover, HOVER_DEFAULT)
|
|
19106
|
+
};
|
|
19107
|
+
}
|
|
19108
|
+
function resolveEnter(phase) {
|
|
19109
|
+
if (phase === false) return null;
|
|
19110
|
+
if (phase === void 0 || phase === true) return { ...ENTER_DEFAULT };
|
|
19111
|
+
return {
|
|
19112
|
+
duration: phase.duration ?? ENTER_DEFAULT.duration,
|
|
19113
|
+
ease: phase.ease ?? ENTER_DEFAULT.ease,
|
|
19114
|
+
stagger: phase.stagger !== false,
|
|
19115
|
+
cameraFit: ENTER_DEFAULT.cameraFit
|
|
19116
|
+
};
|
|
19117
|
+
}
|
|
19118
|
+
function resolveDurationPhase(phase, defaults) {
|
|
19119
|
+
if (phase === false) return null;
|
|
19120
|
+
if (phase === void 0 || phase === true)
|
|
19121
|
+
return { duration: defaults.duration, ease: defaults.ease };
|
|
19122
|
+
return {
|
|
19123
|
+
duration: phase.duration ?? defaults.duration,
|
|
19124
|
+
ease: phase.ease ?? defaults.ease
|
|
19125
|
+
};
|
|
19126
|
+
}
|
|
19127
|
+
function resolveCamera(phase) {
|
|
19128
|
+
if (phase === false) return null;
|
|
19129
|
+
if (phase === void 0 || phase === true) return { ...CAMERA_DEFAULT };
|
|
19130
|
+
return {
|
|
19131
|
+
duration: phase.duration ?? CAMERA_DEFAULT.duration,
|
|
19132
|
+
ease: phase.ease ?? CAMERA_DEFAULT.ease
|
|
19133
|
+
};
|
|
19134
|
+
}
|
|
19135
|
+
|
|
18286
19136
|
// src/graphs/encoding.ts
|
|
18287
19137
|
var DEFAULT_NODE_RADIUS = 5;
|
|
18288
19138
|
var MIN_NODE_RADIUS = 3;
|
|
@@ -18322,16 +19172,42 @@ function computeDegrees(nodes, edges) {
|
|
|
18322
19172
|
}
|
|
18323
19173
|
return degrees2;
|
|
18324
19174
|
}
|
|
19175
|
+
function resolveCategoricalDomain(values, sort, scaleDomain) {
|
|
19176
|
+
if (Array.isArray(scaleDomain) && scaleDomain.length > 0) {
|
|
19177
|
+
return scaleDomain.map((v) => String(v));
|
|
19178
|
+
}
|
|
19179
|
+
const seen = [];
|
|
19180
|
+
const set = /* @__PURE__ */ new Set();
|
|
19181
|
+
for (const v of values) {
|
|
19182
|
+
if (!set.has(v)) {
|
|
19183
|
+
set.add(v);
|
|
19184
|
+
seen.push(v);
|
|
19185
|
+
}
|
|
19186
|
+
}
|
|
19187
|
+
if (sort === null) return seen;
|
|
19188
|
+
if (Array.isArray(sort)) {
|
|
19189
|
+
const pinned = sort.filter((v) => set.has(v));
|
|
19190
|
+
const pinnedSet = new Set(pinned);
|
|
19191
|
+
const rest = seen.filter((v) => !pinnedSet.has(v));
|
|
19192
|
+
return [...pinned, ...rest];
|
|
19193
|
+
}
|
|
19194
|
+
const sorted = [...seen].sort((a, b) => a.localeCompare(b));
|
|
19195
|
+
return sort === "descending" ? sorted.reverse() : sorted;
|
|
19196
|
+
}
|
|
18325
19197
|
function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
|
|
18326
19198
|
const degrees2 = computeDegrees(nodes, edges);
|
|
18327
19199
|
const maxDegree = Math.max(1, ...degrees2.values());
|
|
18328
19200
|
let sizeScale;
|
|
18329
19201
|
if (encoding.nodeSize?.field) {
|
|
18330
19202
|
const field = encoding.nodeSize.field;
|
|
19203
|
+
const scaleConfig = encoding.nodeSize.scale;
|
|
18331
19204
|
const values = nodes.map((n) => Number(n[field])).filter((v) => Number.isFinite(v));
|
|
18332
19205
|
const sizeMin = min(values) ?? 0;
|
|
18333
19206
|
const sizeMax = max(values) ?? 1;
|
|
18334
|
-
|
|
19207
|
+
const domain = scaleConfig?.domain && scaleConfig.domain.length === 2 ? scaleConfig.domain : [sizeMin, sizeMax];
|
|
19208
|
+
const range3 = scaleConfig?.range && scaleConfig.range.length >= 2 ? [Number(scaleConfig.range[0]), Number(scaleConfig.range[1])] : [MIN_NODE_RADIUS, MAX_NODE_RADIUS];
|
|
19209
|
+
const ctor = scaleConfig?.type === "linear" ? linear2 : sqrt;
|
|
19210
|
+
sizeScale = ctor().domain(domain).range(range3);
|
|
18335
19211
|
}
|
|
18336
19212
|
let colorFn;
|
|
18337
19213
|
if (encoding.nodeColor?.field) {
|
|
@@ -18352,13 +19228,39 @@ function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
|
|
|
18352
19228
|
return Number.isFinite(val) ? colorScale(val) : theme.colors.categorical[0];
|
|
18353
19229
|
};
|
|
18354
19230
|
} else {
|
|
18355
|
-
const domain =
|
|
19231
|
+
const domain = resolveCategoricalDomain(
|
|
19232
|
+
nodes.map((n) => String(n[field] ?? "")),
|
|
19233
|
+
encoding.nodeColor.sort,
|
|
19234
|
+
scaleConfig?.domain
|
|
19235
|
+
);
|
|
18356
19236
|
const range3 = scaleConfig?.range && scaleConfig.range.length > 0 ? scaleConfig.range : theme.colors.categorical;
|
|
18357
19237
|
const ordinalScale = ordinal().domain(domain).range(range3);
|
|
18358
19238
|
colorFn = (node) => ordinalScale(String(node[field] ?? ""));
|
|
18359
19239
|
}
|
|
18360
19240
|
}
|
|
18361
19241
|
const defaultColor = theme.colors.categorical[0];
|
|
19242
|
+
let opacityScale;
|
|
19243
|
+
if (encoding.nodeOpacity?.field) {
|
|
19244
|
+
const field = encoding.nodeOpacity.field;
|
|
19245
|
+
const scaleConfig = encoding.nodeOpacity.scale;
|
|
19246
|
+
const values = nodes.map((n) => Number(n[field])).filter((v) => Number.isFinite(v));
|
|
19247
|
+
const opMin = min(values) ?? 0;
|
|
19248
|
+
const opMax = max(values) ?? 1;
|
|
19249
|
+
const domain = scaleConfig?.domain && scaleConfig.domain.length === 2 ? scaleConfig.domain : [opMin, opMax];
|
|
19250
|
+
const range3 = scaleConfig?.range && scaleConfig.range.length >= 2 ? [Number(scaleConfig.range[0]), Number(scaleConfig.range[1])] : [0.25, 1];
|
|
19251
|
+
opacityScale = linear2().domain(domain).range(range3);
|
|
19252
|
+
}
|
|
19253
|
+
let labelPriorityScale;
|
|
19254
|
+
if (encoding.nodeLabelPriority?.field) {
|
|
19255
|
+
const field = encoding.nodeLabelPriority.field;
|
|
19256
|
+
const scaleConfig = encoding.nodeLabelPriority.scale;
|
|
19257
|
+
const values = nodes.map((n) => Number(n[field])).filter((v) => Number.isFinite(v));
|
|
19258
|
+
const lpMin = min(values) ?? 0;
|
|
19259
|
+
const lpMax = max(values) ?? 1;
|
|
19260
|
+
const domain = scaleConfig?.domain && scaleConfig.domain.length === 2 ? scaleConfig.domain : [lpMin, lpMax];
|
|
19261
|
+
const range3 = scaleConfig?.range && scaleConfig.range.length >= 2 ? [Number(scaleConfig.range[0]), Number(scaleConfig.range[1])] : [0, 1];
|
|
19262
|
+
labelPriorityScale = linear2().domain(domain).range(range3);
|
|
19263
|
+
}
|
|
18362
19264
|
return nodes.map((node) => {
|
|
18363
19265
|
let radius = DEFAULT_NODE_RADIUS;
|
|
18364
19266
|
if (sizeScale && encoding.nodeSize?.field) {
|
|
@@ -18367,6 +19269,13 @@ function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
|
|
|
18367
19269
|
radius = sizeScale(val);
|
|
18368
19270
|
}
|
|
18369
19271
|
}
|
|
19272
|
+
let opacity = 1;
|
|
19273
|
+
if (opacityScale && encoding.nodeOpacity?.field) {
|
|
19274
|
+
const val = Number(node[encoding.nodeOpacity.field]);
|
|
19275
|
+
if (Number.isFinite(val)) {
|
|
19276
|
+
opacity = opacityScale(val);
|
|
19277
|
+
}
|
|
19278
|
+
}
|
|
18370
19279
|
const fill = colorFn ? colorFn(node) : defaultColor;
|
|
18371
19280
|
const stroke = darkenColor(fill);
|
|
18372
19281
|
let label;
|
|
@@ -18376,8 +19285,14 @@ function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
|
|
|
18376
19285
|
} else {
|
|
18377
19286
|
label = node.id;
|
|
18378
19287
|
}
|
|
18379
|
-
|
|
18380
|
-
|
|
19288
|
+
let labelPriority;
|
|
19289
|
+
if (labelPriorityScale && encoding.nodeLabelPriority?.field) {
|
|
19290
|
+
const val = Number(node[encoding.nodeLabelPriority.field]);
|
|
19291
|
+
labelPriority = Number.isFinite(val) ? labelPriorityScale(val) : 0;
|
|
19292
|
+
} else {
|
|
19293
|
+
const degree = degrees2.get(node.id) ?? 0;
|
|
19294
|
+
labelPriority = maxDegree > 0 ? degree / maxDegree : 0;
|
|
19295
|
+
}
|
|
18381
19296
|
const { id: _id, ...rest } = node;
|
|
18382
19297
|
const data = { id: node.id, ...rest };
|
|
18383
19298
|
const override = nodeOverrides?.[node.id];
|
|
@@ -18395,6 +19310,7 @@ function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
|
|
|
18395
19310
|
label,
|
|
18396
19311
|
labelPriority: finalLabelPriority,
|
|
18397
19312
|
community: void 0,
|
|
19313
|
+
opacity,
|
|
18398
19314
|
data
|
|
18399
19315
|
};
|
|
18400
19316
|
});
|
|
@@ -18427,7 +19343,11 @@ function resolveEdgeVisuals(edges, encoding, theme) {
|
|
|
18427
19343
|
return Number.isFinite(val) ? colorScale(val) : hexWithOpacity(theme.colors.axis, 0.4);
|
|
18428
19344
|
};
|
|
18429
19345
|
} else {
|
|
18430
|
-
const domain =
|
|
19346
|
+
const domain = resolveCategoricalDomain(
|
|
19347
|
+
edges.map((e) => String(e[field] ?? "")),
|
|
19348
|
+
encoding.edgeColor.sort,
|
|
19349
|
+
scaleConfig?.domain
|
|
19350
|
+
);
|
|
18431
19351
|
const range3 = scaleConfig?.range && scaleConfig.range.length > 0 ? scaleConfig.range : theme.colors.categorical;
|
|
18432
19352
|
const ordinalScale = ordinal().domain(domain).range(range3);
|
|
18433
19353
|
edgeColorFn = (edge) => ordinalScale(String(edge[field] ?? ""));
|
|
@@ -18438,10 +19358,14 @@ function resolveEdgeVisuals(edges, encoding, theme) {
|
|
|
18438
19358
|
let styleFn;
|
|
18439
19359
|
if (encoding.edgeStyle?.field) {
|
|
18440
19360
|
const field = encoding.edgeStyle.field;
|
|
18441
|
-
const
|
|
19361
|
+
const domain = resolveCategoricalDomain(
|
|
19362
|
+
edges.map((e) => String(e[field] ?? "")),
|
|
19363
|
+
encoding.edgeStyle.sort,
|
|
19364
|
+
encoding.edgeStyle.scale?.domain
|
|
19365
|
+
);
|
|
18442
19366
|
const styleMap = /* @__PURE__ */ new Map();
|
|
18443
|
-
for (let i = 0; i <
|
|
18444
|
-
styleMap.set(
|
|
19367
|
+
for (let i = 0; i < domain.length; i++) {
|
|
19368
|
+
styleMap.set(domain[i], EDGE_STYLES[i % EDGE_STYLES.length]);
|
|
18445
19369
|
}
|
|
18446
19370
|
styleFn = (edge) => styleMap.get(String(edge[field] ?? "")) ?? "solid";
|
|
18447
19371
|
}
|
|
@@ -18499,12 +19423,49 @@ function applyCommunityColors(nodes, colorMap) {
|
|
|
18499
19423
|
}
|
|
18500
19424
|
}
|
|
18501
19425
|
|
|
19426
|
+
// src/graphs/interaction.ts
|
|
19427
|
+
var DEFAULT_DIM_OPACITY = 0.15;
|
|
19428
|
+
var DEFAULT_CURSOR_RADIUS = 80;
|
|
19429
|
+
var DEFAULT_CURSOR_STRENGTH = 30;
|
|
19430
|
+
function resolveGraphInteraction(cfg) {
|
|
19431
|
+
const hover = cfg?.hover;
|
|
19432
|
+
const cursor = cfg?.cursorRepulsion;
|
|
19433
|
+
let cursorRepulsion = null;
|
|
19434
|
+
if (cursor === true) {
|
|
19435
|
+
cursorRepulsion = { radius: DEFAULT_CURSOR_RADIUS, strength: DEFAULT_CURSOR_STRENGTH };
|
|
19436
|
+
} else if (cursor && typeof cursor === "object") {
|
|
19437
|
+
cursorRepulsion = {
|
|
19438
|
+
radius: cursor.radius ?? DEFAULT_CURSOR_RADIUS,
|
|
19439
|
+
strength: cursor.strength ?? DEFAULT_CURSOR_STRENGTH
|
|
19440
|
+
};
|
|
19441
|
+
}
|
|
19442
|
+
return {
|
|
19443
|
+
hoverMode: hover?.mode ?? "neighbors",
|
|
19444
|
+
dimOpacity: hover?.dimOpacity ?? DEFAULT_DIM_OPACITY,
|
|
19445
|
+
selectFlyTo: cfg?.select?.flyTo ?? false,
|
|
19446
|
+
cursorRepulsion,
|
|
19447
|
+
springyDrag: cfg?.springyDrag ?? false
|
|
19448
|
+
};
|
|
19449
|
+
}
|
|
19450
|
+
|
|
18502
19451
|
// src/graphs/compile-graph.ts
|
|
18503
19452
|
var graphNumberFormatter = defaultNumberFormatter8({ allIntegers: false, surface: "chart" });
|
|
18504
19453
|
var SWATCH_SIZE2 = 12;
|
|
18505
19454
|
var SWATCH_GAP2 = 6;
|
|
18506
19455
|
var ENTRY_GAP2 = 16;
|
|
18507
|
-
|
|
19456
|
+
var ENERGY_PRESETS = {
|
|
19457
|
+
gentle: { chargeStrength: -150, velocityDecay: 0.5 },
|
|
19458
|
+
balanced: { chargeStrength: -300, velocityDecay: 0.4 },
|
|
19459
|
+
energetic: { chargeStrength: -600, velocityDecay: 0.3 }
|
|
19460
|
+
};
|
|
19461
|
+
var SETTLE_PRESETS = {
|
|
19462
|
+
quick: 0.05,
|
|
19463
|
+
balanced: 0.0228,
|
|
19464
|
+
thorough: 0.01
|
|
19465
|
+
};
|
|
19466
|
+
var DEFAULT_WARMUP_TICKS = 100;
|
|
19467
|
+
var DEFAULT_WARMUP_BUDGET_MS = 250;
|
|
19468
|
+
function buildGraphLegend(nodes, communityColorMap, hasCommunities, theme, nodeColorField, legendOrder) {
|
|
18508
19469
|
const labelStyle = {
|
|
18509
19470
|
fontFamily: theme.fonts.family,
|
|
18510
19471
|
fontSize: theme.fonts.sizes.small,
|
|
@@ -18514,25 +19475,35 @@ function buildGraphLegend(nodes, communityColorMap, hasCommunities, theme, nodeC
|
|
|
18514
19475
|
};
|
|
18515
19476
|
let entries;
|
|
18516
19477
|
if (hasCommunities && communityColorMap.size > 0) {
|
|
19478
|
+
const counts = /* @__PURE__ */ new Map();
|
|
19479
|
+
for (const node of nodes) {
|
|
19480
|
+
if (node.community != null) {
|
|
19481
|
+
counts.set(node.community, (counts.get(node.community) ?? 0) + 1);
|
|
19482
|
+
}
|
|
19483
|
+
}
|
|
18517
19484
|
entries = [...communityColorMap.entries()].map(([label, color2]) => ({
|
|
18518
19485
|
label,
|
|
18519
19486
|
color: color2,
|
|
18520
19487
|
shape: "circle",
|
|
18521
|
-
active: true
|
|
19488
|
+
active: true,
|
|
19489
|
+
count: counts.get(label) ?? 0
|
|
18522
19490
|
}));
|
|
18523
19491
|
} else if (nodeColorField) {
|
|
18524
19492
|
const categoryColors = /* @__PURE__ */ new Map();
|
|
19493
|
+
const categoryCounts = /* @__PURE__ */ new Map();
|
|
18525
19494
|
for (const node of nodes) {
|
|
18526
19495
|
const category = String(node.data[nodeColorField] ?? node.label ?? node.id);
|
|
18527
19496
|
if (!categoryColors.has(category)) {
|
|
18528
19497
|
categoryColors.set(category, node.fill);
|
|
18529
19498
|
}
|
|
19499
|
+
categoryCounts.set(category, (categoryCounts.get(category) ?? 0) + 1);
|
|
18530
19500
|
}
|
|
18531
|
-
entries = categoryColors.size <= 1 ? [] : [...categoryColors.
|
|
19501
|
+
entries = categoryColors.size <= 1 ? [] : (legendOrder ?? [...categoryColors.keys()]).filter((label) => categoryColors.has(label)).map((label) => ({
|
|
18532
19502
|
label,
|
|
18533
|
-
color:
|
|
19503
|
+
color: categoryColors.get(label),
|
|
18534
19504
|
shape: "circle",
|
|
18535
|
-
active: true
|
|
19505
|
+
active: true,
|
|
19506
|
+
count: categoryCounts.get(label) ?? 0
|
|
18536
19507
|
}));
|
|
18537
19508
|
} else {
|
|
18538
19509
|
entries = [];
|
|
@@ -18548,6 +19519,30 @@ function buildGraphLegend(nodes, communityColorMap, hasCommunities, theme, nodeC
|
|
|
18548
19519
|
swatchChipFill: theme.colors.annotationFill
|
|
18549
19520
|
};
|
|
18550
19521
|
}
|
|
19522
|
+
function buildEdgeLegend(edges, encoding) {
|
|
19523
|
+
const field = encoding.edgeColor?.field;
|
|
19524
|
+
if (!field || (encoding.edgeColor?.type ?? "nominal") === "quantitative") return void 0;
|
|
19525
|
+
const categoryColors = /* @__PURE__ */ new Map();
|
|
19526
|
+
const categoryCounts = /* @__PURE__ */ new Map();
|
|
19527
|
+
for (const edge of edges) {
|
|
19528
|
+
const category = String(edge.data[field] ?? "");
|
|
19529
|
+
if (!categoryColors.has(category)) categoryColors.set(category, edge.stroke);
|
|
19530
|
+
categoryCounts.set(category, (categoryCounts.get(category) ?? 0) + 1);
|
|
19531
|
+
}
|
|
19532
|
+
if (categoryColors.size <= 1) return void 0;
|
|
19533
|
+
const order = resolveCategoricalDomain(
|
|
19534
|
+
edges.map((e) => String(e.data[field] ?? "")),
|
|
19535
|
+
encoding.edgeColor?.sort,
|
|
19536
|
+
encoding.edgeColor?.scale?.domain
|
|
19537
|
+
);
|
|
19538
|
+
return order.filter((label) => categoryColors.has(label)).map((label) => ({
|
|
19539
|
+
label,
|
|
19540
|
+
color: categoryColors.get(label),
|
|
19541
|
+
shape: "line",
|
|
19542
|
+
active: true,
|
|
19543
|
+
count: categoryCounts.get(label) ?? 0
|
|
19544
|
+
}));
|
|
19545
|
+
}
|
|
18551
19546
|
function buildGraphTooltips(nodes) {
|
|
18552
19547
|
const descriptors = /* @__PURE__ */ new Map();
|
|
18553
19548
|
for (const node of nodes) {
|
|
@@ -18574,6 +19569,21 @@ function buildGraphTooltips(nodes) {
|
|
|
18574
19569
|
}
|
|
18575
19570
|
return descriptors;
|
|
18576
19571
|
}
|
|
19572
|
+
function buildEdgeTooltip(edge) {
|
|
19573
|
+
const fields = [];
|
|
19574
|
+
for (const [key, value2] of Object.entries(edge.data)) {
|
|
19575
|
+
if (key === "source" || key === "target") continue;
|
|
19576
|
+
if (value2 == null) continue;
|
|
19577
|
+
fields.push({
|
|
19578
|
+
label: key,
|
|
19579
|
+
value: typeof value2 === "number" ? graphNumberFormatter(value2) : String(value2)
|
|
19580
|
+
});
|
|
19581
|
+
}
|
|
19582
|
+
return {
|
|
19583
|
+
title: `${edge.source} \u2192 ${edge.target}`,
|
|
19584
|
+
fields
|
|
19585
|
+
};
|
|
19586
|
+
}
|
|
18577
19587
|
function compileGraph(spec, options) {
|
|
18578
19588
|
const { spec: normalized, warnings } = compile(spec);
|
|
18579
19589
|
emitSpecWarnings(warnings, options.onWarn);
|
|
@@ -18583,6 +19593,16 @@ function compileGraph(spec, options) {
|
|
|
18583
19593
|
);
|
|
18584
19594
|
}
|
|
18585
19595
|
const graphSpec = normalized;
|
|
19596
|
+
for (const [name, ch] of [
|
|
19597
|
+
["nodeColor", graphSpec.encoding.nodeColor],
|
|
19598
|
+
["edgeColor", graphSpec.encoding.edgeColor]
|
|
19599
|
+
]) {
|
|
19600
|
+
if (ch?.sort != null && ch.type === "quantitative") {
|
|
19601
|
+
options.onWarn?.(
|
|
19602
|
+
`encoding.${name}.sort is ignored for a quantitative field (sort only orders categorical domains).`
|
|
19603
|
+
);
|
|
19604
|
+
}
|
|
19605
|
+
}
|
|
18586
19606
|
const rawWatermark = spec.watermark;
|
|
18587
19607
|
const watermark = rawWatermark !== void 0 ? graphSpec.watermark : options.watermark ?? true;
|
|
18588
19608
|
const mergedThemeConfig = options.theme ? { ...graphSpec.theme, ...options.theme } : graphSpec.theme;
|
|
@@ -18608,13 +19628,30 @@ function compileGraph(spec, options) {
|
|
|
18608
19628
|
}
|
|
18609
19629
|
const compiledEdges = resolveEdgeVisuals(graphSpec.edges, graphSpec.encoding, theme);
|
|
18610
19630
|
const useCommunitiesForLegend = hasCommunities && !hasNodeColorEncoding;
|
|
19631
|
+
const nodeColorField = graphSpec.encoding.nodeColor?.field;
|
|
19632
|
+
const legendOrder = nodeColorField ? resolveCategoricalDomain(
|
|
19633
|
+
compiledNodes.map((n) => String(n.data[nodeColorField] ?? n.label ?? n.id)),
|
|
19634
|
+
graphSpec.encoding.nodeColor?.sort,
|
|
19635
|
+
graphSpec.encoding.nodeColor?.scale?.domain
|
|
19636
|
+
) : void 0;
|
|
18611
19637
|
const legend = buildGraphLegend(
|
|
18612
19638
|
compiledNodes,
|
|
18613
19639
|
communityColorMap,
|
|
18614
19640
|
useCommunitiesForLegend,
|
|
18615
19641
|
theme,
|
|
18616
|
-
|
|
19642
|
+
nodeColorField,
|
|
19643
|
+
legendOrder
|
|
18617
19644
|
);
|
|
19645
|
+
const edgeLegend = buildEdgeLegend(compiledEdges, graphSpec.encoding);
|
|
19646
|
+
const legendField = nodeColorField ?? (useCommunitiesForLegend ? clusteringField ?? null : null);
|
|
19647
|
+
let initialHighlight;
|
|
19648
|
+
if (nodeColorField && graphSpec.encoding.nodeColor?.highlight != null) {
|
|
19649
|
+
const raw = graphSpec.encoding.nodeColor.highlight;
|
|
19650
|
+
const requested = Array.isArray(raw) ? raw : [raw];
|
|
19651
|
+
const available = new Set(legendOrder ?? []);
|
|
19652
|
+
const values = requested.filter((v) => available.has(v));
|
|
19653
|
+
if (values.length > 0) initialHighlight = { field: nodeColorField, values };
|
|
19654
|
+
}
|
|
18618
19655
|
const tooltipDescriptors = buildGraphTooltips(compiledNodes);
|
|
18619
19656
|
const communityCount = communityColorMap.size;
|
|
18620
19657
|
const altParts = [
|
|
@@ -18631,17 +19668,26 @@ function compileGraph(spec, options) {
|
|
|
18631
19668
|
};
|
|
18632
19669
|
const collisionPadding = graphSpec.layout.collisionPadding ?? 2;
|
|
18633
19670
|
const maxRadius = compiledNodes.length > 0 ? Math.max(...compiledNodes.map((n) => n.radius)) : DEFAULT_COLLISION_PADDING;
|
|
19671
|
+
const energyPreset = graphSpec.layout.energy ? ENERGY_PRESETS[graphSpec.layout.energy] : ENERGY_PRESETS.balanced;
|
|
19672
|
+
const settlePreset = graphSpec.layout.settle ? SETTLE_PRESETS[graphSpec.layout.settle] : SETTLE_PRESETS.balanced;
|
|
19673
|
+
const warmupRaw = graphSpec.layout.warmup;
|
|
19674
|
+
const warmupTicks = warmupRaw === false ? 0 : typeof warmupRaw === "number" ? Math.max(0, Math.floor(warmupRaw)) : DEFAULT_WARMUP_TICKS;
|
|
18634
19675
|
const simulationConfig = {
|
|
18635
|
-
chargeStrength: graphSpec.layout.chargeStrength ??
|
|
19676
|
+
chargeStrength: graphSpec.layout.chargeStrength ?? energyPreset.chargeStrength,
|
|
18636
19677
|
linkDistance: graphSpec.layout.linkDistance ?? 30,
|
|
18637
19678
|
clustering: clusteringField ? { field: clusteringField, strength: 0.5 } : null,
|
|
18638
|
-
alphaDecay:
|
|
18639
|
-
velocityDecay:
|
|
19679
|
+
alphaDecay: settlePreset,
|
|
19680
|
+
velocityDecay: energyPreset.velocityDecay,
|
|
18640
19681
|
collisionRadius: maxRadius + collisionPadding,
|
|
18641
19682
|
collisionPadding,
|
|
18642
19683
|
linkStrength: graphSpec.layout.linkStrength,
|
|
18643
|
-
centerForce: graphSpec.layout.centerForce
|
|
19684
|
+
centerForce: graphSpec.layout.centerForce,
|
|
19685
|
+
seed: graphSpec.layout.seed,
|
|
19686
|
+
warmupTicks,
|
|
19687
|
+
warmupBudgetMs: DEFAULT_WARMUP_BUDGET_MS
|
|
18644
19688
|
};
|
|
19689
|
+
const animation = resolveGraphAnimation(graphSpec.animation);
|
|
19690
|
+
const interaction = resolveGraphInteraction(graphSpec.interaction);
|
|
18645
19691
|
const chrome = computeChrome4(
|
|
18646
19692
|
{
|
|
18647
19693
|
title: graphSpec.chrome.title,
|
|
@@ -18670,7 +19716,12 @@ function compileGraph(spec, options) {
|
|
|
18670
19716
|
height: options.height
|
|
18671
19717
|
},
|
|
18672
19718
|
simulationConfig,
|
|
18673
|
-
watermark
|
|
19719
|
+
watermark,
|
|
19720
|
+
animation,
|
|
19721
|
+
interaction,
|
|
19722
|
+
legendField,
|
|
19723
|
+
initialHighlight,
|
|
19724
|
+
edgeLegend
|
|
18674
19725
|
};
|
|
18675
19726
|
}
|
|
18676
19727
|
var DEFAULT_COLLISION_PADDING = 5;
|
|
@@ -18763,6 +19814,7 @@ function legendLayoutHasContent(legendLayout) {
|
|
|
18763
19814
|
}
|
|
18764
19815
|
var MIN_CHART_WIDTH = 60;
|
|
18765
19816
|
var MIN_CHART_HEIGHT2 = 40;
|
|
19817
|
+
var PLOT_SHARE_MAX = 0.4;
|
|
18766
19818
|
var SERIES_SEARCH_INPUT_HEIGHT = 32;
|
|
18767
19819
|
var SERIES_SEARCH_GAP = 12;
|
|
18768
19820
|
function getMinChartDims(display) {
|
|
@@ -18805,8 +19857,10 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
18805
19857
|
watermark,
|
|
18806
19858
|
bottomLegendReservation
|
|
18807
19859
|
);
|
|
19860
|
+
const chromeLayout = resolveChromeLayout(spec, options);
|
|
19861
|
+
const effectiveHeight = chromeLayout === "grow" ? height + chrome.topHeight + chrome.bottomHeight : height;
|
|
18808
19862
|
if (isSparkline) {
|
|
18809
|
-
const total2 = { x: 0, y: 0, width, height };
|
|
19863
|
+
const total2 = { x: 0, y: 0, width, height: effectiveHeight };
|
|
18810
19864
|
const sparkPad = getSparklinePad(spec);
|
|
18811
19865
|
const xAxisSpace = userExplicit.xAxis ? 26 : 0;
|
|
18812
19866
|
const yAxisSpace = userExplicit.yAxis ? 30 : 0;
|
|
@@ -18830,7 +19884,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
18830
19884
|
x: margins2.left,
|
|
18831
19885
|
y: margins2.top,
|
|
18832
19886
|
width: Math.max(0, width - margins2.left - margins2.right),
|
|
18833
|
-
height: Math.max(0,
|
|
19887
|
+
height: Math.max(0, effectiveHeight - margins2.top - margins2.bottom)
|
|
18834
19888
|
};
|
|
18835
19889
|
return {
|
|
18836
19890
|
total: total2,
|
|
@@ -18842,7 +19896,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
18842
19896
|
effectiveAxisGap: 0
|
|
18843
19897
|
};
|
|
18844
19898
|
}
|
|
18845
|
-
const total = { x: 0, y: 0, width, height };
|
|
19899
|
+
const total = { x: 0, y: 0, width, height: effectiveHeight };
|
|
18846
19900
|
const isRadial = isAxislessMark2(spec.markType);
|
|
18847
19901
|
const encoding = spec.encoding;
|
|
18848
19902
|
const xAxisSuppressed = encoding.x?.axis === false;
|
|
@@ -19072,10 +20126,11 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
19072
20126
|
x: margins.left,
|
|
19073
20127
|
y: margins.top,
|
|
19074
20128
|
width: Math.max(0, width - margins.left - margins.right),
|
|
19075
|
-
height: Math.max(0,
|
|
20129
|
+
height: Math.max(0, effectiveHeight - margins.top - margins.bottom)
|
|
19076
20130
|
};
|
|
19077
20131
|
const minDims = getMinChartDims(spec.display);
|
|
19078
|
-
|
|
20132
|
+
const chromeEatsPlot = chromeLayout !== "grow" && chrome.topHeight > height * PLOT_SHARE_MAX;
|
|
20133
|
+
if ((chartArea.width < minDims.width || chartArea.height < minDims.height || chromeEatsPlot) && chromeMode !== "hidden") {
|
|
19079
20134
|
const fallbackMode = chromeMode === "full" ? "compact" : "hidden";
|
|
19080
20135
|
const fallbackChrome = computeChrome5(
|
|
19081
20136
|
chromeToInput(spec.chrome),
|
|
@@ -19101,7 +20156,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
19101
20156
|
x: margins.left,
|
|
19102
20157
|
y: margins.top,
|
|
19103
20158
|
width: Math.max(0, width - margins.left - margins.right),
|
|
19104
|
-
height: Math.max(0,
|
|
20159
|
+
height: Math.max(0, effectiveHeight - margins.top - margins.bottom)
|
|
19105
20160
|
};
|
|
19106
20161
|
const fallbackMetricsTopY = topPad + fallbackChrome.topHeight;
|
|
19107
20162
|
const fallbackMetricsArea = { x: hPad, width: Math.max(0, width - hPad * 2) };
|
|
@@ -19118,7 +20173,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
19118
20173
|
chartArea = {
|
|
19119
20174
|
...chartArea,
|
|
19120
20175
|
y: margins.top,
|
|
19121
|
-
height: Math.max(0,
|
|
20176
|
+
height: Math.max(0, effectiveHeight - margins.top - margins.bottom)
|
|
19122
20177
|
};
|
|
19123
20178
|
}
|
|
19124
20179
|
return {
|
|
@@ -19134,7 +20189,8 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
19134
20189
|
fallbackMetricsArea
|
|
19135
20190
|
) : void 0,
|
|
19136
20191
|
xAxisHeight,
|
|
19137
|
-
effectiveAxisGap: fallbackEffectiveAxisGap
|
|
20192
|
+
effectiveAxisGap: fallbackEffectiveAxisGap,
|
|
20193
|
+
chromeStripped: true
|
|
19138
20194
|
};
|
|
19139
20195
|
}
|
|
19140
20196
|
}
|
|
@@ -19146,7 +20202,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
19146
20202
|
chartArea = {
|
|
19147
20203
|
...chartArea,
|
|
19148
20204
|
y: margins.top,
|
|
19149
|
-
height: Math.max(0,
|
|
20205
|
+
height: Math.max(0, effectiveHeight - margins.top - margins.bottom)
|
|
19150
20206
|
};
|
|
19151
20207
|
}
|
|
19152
20208
|
return {
|
|
@@ -19898,14 +20954,19 @@ function compileSankey(spec, options) {
|
|
|
19898
20954
|
watermark
|
|
19899
20955
|
);
|
|
19900
20956
|
const padding = theme.spacing.padding;
|
|
20957
|
+
const chromeLayout = resolveChromeLayout(
|
|
20958
|
+
spec,
|
|
20959
|
+
options
|
|
20960
|
+
);
|
|
20961
|
+
const grownHeight = chromeLayout === "grow" ? options.height + chrome.topHeight + chrome.bottomHeight : options.height;
|
|
19901
20962
|
const fullArea = {
|
|
19902
20963
|
x: padding,
|
|
19903
20964
|
y: padding + chrome.topHeight,
|
|
19904
20965
|
width: options.width - padding * 2,
|
|
19905
|
-
height: options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
|
|
20966
|
+
height: chromeLayout === "grow" ? options.height - padding * 2 : options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
|
|
19906
20967
|
};
|
|
19907
20968
|
if (fullArea.width <= 0 || fullArea.height <= 0) {
|
|
19908
|
-
return emptyLayout4(fullArea, chrome, theme, options, watermark);
|
|
20969
|
+
return emptyLayout4(fullArea, chrome, theme, { ...options, height: grownHeight }, watermark);
|
|
19909
20970
|
}
|
|
19910
20971
|
const sourceField = sankeySpec.encoding.source.field;
|
|
19911
20972
|
const targetField = sankeySpec.encoding.target.field;
|
|
@@ -19945,7 +21006,7 @@ function compileSankey(spec, options) {
|
|
|
19945
21006
|
height: fullArea.height - legend.bounds.height - legendGap2
|
|
19946
21007
|
};
|
|
19947
21008
|
if (area.height <= 0) {
|
|
19948
|
-
return emptyLayout4(area, chrome, theme, options, watermark);
|
|
21009
|
+
return emptyLayout4(area, chrome, theme, { ...options, height: grownHeight }, watermark);
|
|
19949
21010
|
}
|
|
19950
21011
|
const labelFontSize = theme.fonts.sizes.small;
|
|
19951
21012
|
const labelFontWeight = theme.fonts.weights.normal;
|
|
@@ -20097,7 +21158,7 @@ function compileSankey(spec, options) {
|
|
|
20097
21158
|
theme,
|
|
20098
21159
|
dimensions: {
|
|
20099
21160
|
width: options.width,
|
|
20100
|
-
height:
|
|
21161
|
+
height: grownHeight
|
|
20101
21162
|
},
|
|
20102
21163
|
animation: resolvedAnimation,
|
|
20103
21164
|
watermark,
|
|
@@ -20906,7 +21967,7 @@ import {
|
|
|
20906
21967
|
HPAD_COMPACT_MIN as HPAD_COMPACT_MIN4,
|
|
20907
21968
|
pickLabelColor as pickLabelColor4,
|
|
20908
21969
|
resolveTheme as resolveTheme6,
|
|
20909
|
-
SEQUENTIAL_PALETTES as
|
|
21970
|
+
SEQUENTIAL_PALETTES as SEQUENTIAL_PALETTES3
|
|
20910
21971
|
} from "@opendata-ai/openchart-core";
|
|
20911
21972
|
var TILE_CORNER_RADIUS = 6;
|
|
20912
21973
|
var TILE_STROKE_WIDTH = 1;
|
|
@@ -20916,6 +21977,10 @@ var LEGEND_ENTRY_GAP = 16;
|
|
|
20916
21977
|
function clamp(value2, min4, max4) {
|
|
20917
21978
|
return Math.min(max4, Math.max(min4, value2));
|
|
20918
21979
|
}
|
|
21980
|
+
function growContentHeight(contentHeight, chromeLayout, budgetHeight, chrome) {
|
|
21981
|
+
if (chromeLayout !== "grow") return contentHeight;
|
|
21982
|
+
return Math.max(contentHeight, budgetHeight + chrome.topHeight + chrome.bottomHeight);
|
|
21983
|
+
}
|
|
20919
21984
|
function compileTileMap(spec, options) {
|
|
20920
21985
|
const sugarWarnings = [];
|
|
20921
21986
|
const expandedSpec = spec && typeof spec === "object" && !Array.isArray(spec) ? expandSpecSugar(spec, sugarWarnings) : spec;
|
|
@@ -20952,22 +22017,44 @@ function compileTileMap(spec, options) {
|
|
|
20952
22017
|
watermark
|
|
20953
22018
|
);
|
|
20954
22019
|
const padding = getBreakpoint2(options.width) === "compact" ? Math.max(HPAD_COMPACT_MIN4, Math.round(theme.spacing.padding * HPAD_COMPACT_FRACTION4)) : theme.spacing.padding;
|
|
22020
|
+
const chromeLayout = resolveChromeLayout(
|
|
22021
|
+
spec,
|
|
22022
|
+
options
|
|
22023
|
+
);
|
|
20955
22024
|
const fullArea = {
|
|
20956
22025
|
x: padding,
|
|
20957
22026
|
y: padding + chrome.topHeight,
|
|
20958
22027
|
width: options.width - padding * 2,
|
|
20959
|
-
height: options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
|
|
22028
|
+
height: chromeLayout === "grow" ? options.height - padding * 2 : options.height - chrome.topHeight - chrome.bottomHeight - padding * 2
|
|
20960
22029
|
};
|
|
20961
22030
|
if (fullArea.width <= 0 || fullArea.height <= 0) {
|
|
20962
22031
|
return emptyLayout5(chrome, theme, options, watermark);
|
|
20963
22032
|
}
|
|
20964
22033
|
const isCategorical = tilemapSpec.encoding.color !== void 0 || tilemapSpec.colors !== void 0;
|
|
20965
22034
|
if (isCategorical) {
|
|
20966
|
-
return compileCategorical(
|
|
22035
|
+
return compileCategorical(
|
|
22036
|
+
tilemapSpec,
|
|
22037
|
+
options,
|
|
22038
|
+
theme,
|
|
22039
|
+
chrome,
|
|
22040
|
+
fullArea,
|
|
22041
|
+
isDarkMode,
|
|
22042
|
+
watermark,
|
|
22043
|
+
chromeLayout
|
|
22044
|
+
);
|
|
20967
22045
|
}
|
|
20968
|
-
return compileQuantitative(
|
|
22046
|
+
return compileQuantitative(
|
|
22047
|
+
tilemapSpec,
|
|
22048
|
+
options,
|
|
22049
|
+
theme,
|
|
22050
|
+
chrome,
|
|
22051
|
+
fullArea,
|
|
22052
|
+
isDarkMode,
|
|
22053
|
+
watermark,
|
|
22054
|
+
chromeLayout
|
|
22055
|
+
);
|
|
20969
22056
|
}
|
|
20970
|
-
function compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDarkMode, watermark) {
|
|
22057
|
+
function compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDarkMode, watermark, chromeLayout) {
|
|
20971
22058
|
const stateField = tilemapSpec.encoding.state.field;
|
|
20972
22059
|
const valueField = tilemapSpec.encoding.value.field;
|
|
20973
22060
|
const stateValueMap = /* @__PURE__ */ new Map();
|
|
@@ -20984,7 +22071,7 @@ function compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDa
|
|
|
20984
22071
|
const values = Array.from(stateValueMap.values());
|
|
20985
22072
|
const min4 = values.length > 0 ? Math.min(...values) : 0;
|
|
20986
22073
|
const max4 = values.length > 0 ? Math.max(...values) : 100;
|
|
20987
|
-
const paletteStops = [...
|
|
22074
|
+
const paletteStops = [...SEQUENTIAL_PALETTES3[tilemapSpec.palette] ?? SEQUENTIAL_PALETTES3.blue];
|
|
20988
22075
|
const baseColor = isDarkMode ? paletteStops[0] : paletteStops[paletteStops.length - 1];
|
|
20989
22076
|
const opacityRange = isDarkMode ? [0.15, 1] : [0.2, 1];
|
|
20990
22077
|
const opacityScale = linear2().domain([min4, max4]).range(opacityRange).clamp(true);
|
|
@@ -21068,7 +22155,12 @@ function compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDa
|
|
|
21068
22155
|
};
|
|
21069
22156
|
const resolvedAnimation = resolveAnimation(tilemapSpec.animation);
|
|
21070
22157
|
const padding = fullArea.x;
|
|
21071
|
-
const contentHeight =
|
|
22158
|
+
const contentHeight = growContentHeight(
|
|
22159
|
+
tileGridOffsetY + tilePositions.gridHeight + legendGap2 + legendTotalHeight + chrome.bottomHeight + padding,
|
|
22160
|
+
chromeLayout,
|
|
22161
|
+
options.height,
|
|
22162
|
+
chrome
|
|
22163
|
+
);
|
|
21072
22164
|
return {
|
|
21073
22165
|
area: fullArea,
|
|
21074
22166
|
chrome,
|
|
@@ -21085,7 +22177,7 @@ function compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDa
|
|
|
21085
22177
|
measureText: options.measureText ?? ((text, fontSize) => ({ width: estimateTextWidth23(text, fontSize), height: fontSize }))
|
|
21086
22178
|
};
|
|
21087
22179
|
}
|
|
21088
|
-
function compileCategorical(tilemapSpec, options, theme, chrome, fullArea, isDarkMode, watermark) {
|
|
22180
|
+
function compileCategorical(tilemapSpec, options, theme, chrome, fullArea, isDarkMode, watermark, chromeLayout) {
|
|
21089
22181
|
const stateField = tilemapSpec.encoding.state.field;
|
|
21090
22182
|
const colorField = tilemapSpec.encoding.color?.field ?? tilemapSpec.encoding.value.field;
|
|
21091
22183
|
const stateCategoryMap = /* @__PURE__ */ new Map();
|
|
@@ -21204,7 +22296,12 @@ function compileCategorical(tilemapSpec, options, theme, chrome, fullArea, isDar
|
|
|
21204
22296
|
};
|
|
21205
22297
|
const resolvedAnimation = resolveAnimation(tilemapSpec.animation);
|
|
21206
22298
|
const padding = fullArea.x;
|
|
21207
|
-
const contentHeight =
|
|
22299
|
+
const contentHeight = growContentHeight(
|
|
22300
|
+
tileGridOffsetY + tilePositions.gridHeight + legendGap2 + legendTotalHeight + chrome.bottomHeight + padding,
|
|
22301
|
+
chromeLayout,
|
|
22302
|
+
options.height,
|
|
22303
|
+
chrome
|
|
22304
|
+
);
|
|
21208
22305
|
return {
|
|
21209
22306
|
area: fullArea,
|
|
21210
22307
|
chrome,
|
|
@@ -22264,6 +23361,38 @@ function applySparklineDefaults(spec, theme) {
|
|
|
22264
23361
|
}
|
|
22265
23362
|
return { ...spec, markDef: newMarkDef };
|
|
22266
23363
|
}
|
|
23364
|
+
function resolveScaleInvert(resolved) {
|
|
23365
|
+
if (!resolved) return void 0;
|
|
23366
|
+
const { scale, type } = resolved;
|
|
23367
|
+
if (type === "band" || type === "point") {
|
|
23368
|
+
const domain = scale.domain();
|
|
23369
|
+
const positions = domain.map((v) => {
|
|
23370
|
+
const pos = scale(v) ?? 0;
|
|
23371
|
+
return type === "band" ? pos + scale.bandwidth() / 2 : pos;
|
|
23372
|
+
});
|
|
23373
|
+
const range4 = scale.range();
|
|
23374
|
+
return {
|
|
23375
|
+
topPixel: range4[0],
|
|
23376
|
+
bottomPixel: range4[range4.length - 1],
|
|
23377
|
+
topData: 0,
|
|
23378
|
+
bottomData: 0,
|
|
23379
|
+
domain,
|
|
23380
|
+
positions
|
|
23381
|
+
};
|
|
23382
|
+
}
|
|
23383
|
+
const invert = scale.invert;
|
|
23384
|
+
if (typeof invert !== "function") return void 0;
|
|
23385
|
+
const range3 = scale.range();
|
|
23386
|
+
const topPx = range3[0];
|
|
23387
|
+
const bottomPx = range3[range3.length - 1];
|
|
23388
|
+
const topVal = invert.call(scale, topPx);
|
|
23389
|
+
const bottomVal = invert.call(scale, bottomPx);
|
|
23390
|
+
const topData = topVal instanceof Date ? +topVal : Number(topVal);
|
|
23391
|
+
const bottomData = bottomVal instanceof Date ? +bottomVal : Number(bottomVal);
|
|
23392
|
+
if (!Number.isFinite(topData) || !Number.isFinite(bottomData)) return void 0;
|
|
23393
|
+
const scaleType = type;
|
|
23394
|
+
return { topPixel: topPx, bottomPixel: bottomPx, topData, bottomData, scaleType };
|
|
23395
|
+
}
|
|
22267
23396
|
function compileChart(spec, optionsInput) {
|
|
22268
23397
|
const sugarWarnings = [];
|
|
22269
23398
|
const expandedSpec = spec && typeof spec === "object" && !Array.isArray(spec) ? expandSpecSugar(spec, sugarWarnings) : spec;
|
|
@@ -22464,12 +23593,35 @@ function compileChart(spec, optionsInput) {
|
|
|
22464
23593
|
}
|
|
22465
23594
|
};
|
|
22466
23595
|
}
|
|
23596
|
+
let facetDirection = "wrap";
|
|
23597
|
+
const rowChannel = renderSpec.encoding.row;
|
|
23598
|
+
const colChannel = renderSpec.encoding.column;
|
|
23599
|
+
if (rowChannel?.field || colChannel?.field) {
|
|
23600
|
+
const channel = rowChannel?.field ? rowChannel : colChannel;
|
|
23601
|
+
facetDirection = rowChannel?.field ? "row" : "column";
|
|
23602
|
+
const desugared = { ...renderSpec.encoding };
|
|
23603
|
+
delete desugared.row;
|
|
23604
|
+
delete desugared.column;
|
|
23605
|
+
desugared.facet = {
|
|
23606
|
+
field: channel.field,
|
|
23607
|
+
type: channel.type,
|
|
23608
|
+
sort: channel.sort,
|
|
23609
|
+
...facetDirection === "row" ? { columns: 1 } : {}
|
|
23610
|
+
};
|
|
23611
|
+
renderSpec = { ...renderSpec, encoding: desugared };
|
|
23612
|
+
const chartDesugared = { ...chartSpec.encoding };
|
|
23613
|
+
delete chartDesugared.row;
|
|
23614
|
+
delete chartDesugared.column;
|
|
23615
|
+
chartDesugared.facet = desugared.facet;
|
|
23616
|
+
chartSpec = { ...chartSpec, encoding: chartDesugared };
|
|
23617
|
+
}
|
|
22467
23618
|
const facetChannel = renderSpec.encoding.facet;
|
|
22468
23619
|
if (facetChannel?.field) {
|
|
22469
23620
|
return compileFaceted(
|
|
22470
23621
|
chartSpec,
|
|
22471
23622
|
renderSpec,
|
|
22472
23623
|
facetChannel,
|
|
23624
|
+
facetDirection,
|
|
22473
23625
|
options,
|
|
22474
23626
|
theme,
|
|
22475
23627
|
strategy,
|
|
@@ -22534,7 +23686,10 @@ function compileChart(spec, optionsInput) {
|
|
|
22534
23686
|
skipY,
|
|
22535
23687
|
markType: chartSpec.markType,
|
|
22536
23688
|
totalWidth: options.width,
|
|
22537
|
-
|
|
23689
|
+
// If the guardrail stripped chrome, the plan's y-ticks were sized
|
|
23690
|
+
// against the pre-strip (shorter) plot. Drop them so computeAxes
|
|
23691
|
+
// regenerates against the final (taller) chartArea.
|
|
23692
|
+
precomputedYTicks: !dims.chromeStripped && plan.yTickValues.length > 0 ? plan.yTickValues : void 0
|
|
22538
23693
|
});
|
|
22539
23694
|
const xAxisExtent = axes.x?.extent ?? 0;
|
|
22540
23695
|
dims.chrome.bottomAnchorY = chartArea.y + chartArea.height + xAxisExtent;
|
|
@@ -22549,6 +23704,19 @@ function compileChart(spec, optionsInput) {
|
|
|
22549
23704
|
const renderer = getChartRenderer(rendererKey);
|
|
22550
23705
|
const marks = renderer ? renderer(renderSpec, scales, chartArea, strategy, theme, options.width) : [];
|
|
22551
23706
|
applyFillPatterns(marks, chartSpec.markDef, theme);
|
|
23707
|
+
const markRenderModeWarnings = [];
|
|
23708
|
+
const markRenderMode = resolveMarkRenderMode(
|
|
23709
|
+
{
|
|
23710
|
+
markDef: chartSpec.markDef,
|
|
23711
|
+
markType: chartSpec.markType,
|
|
23712
|
+
pointCount: marks.reduce((n, m) => m.type === "point" ? n + 1 : n, 0),
|
|
23713
|
+
display: chartSpec.display,
|
|
23714
|
+
faceted: false,
|
|
23715
|
+
layered: options.layered ?? false
|
|
23716
|
+
},
|
|
23717
|
+
markRenderModeWarnings
|
|
23718
|
+
);
|
|
23719
|
+
emitSpecWarnings(markRenderModeWarnings, options.onWarn);
|
|
22552
23720
|
const xFieldForYdi = chartSpec.encoding.x && "field" in chartSpec.encoding.x ? chartSpec.encoding.x.field : void 0;
|
|
22553
23721
|
const youDrawIt = resolveYouDrawIt(chartSpec.youDrawIt, marks, scales, chartArea, xFieldForYdi);
|
|
22554
23722
|
const endpointLabels = computeEndpointLabels(
|
|
@@ -22629,6 +23797,8 @@ function compileChart(spec, optionsInput) {
|
|
|
22629
23797
|
);
|
|
22630
23798
|
assignAnimationIndices(marks, resolvedAnimation);
|
|
22631
23799
|
const chrome = footnotes ? { ...dims.chrome, footnotes } : dims.chrome;
|
|
23800
|
+
const xInvert = resolveScaleInvert(scales.x);
|
|
23801
|
+
const yInvert = resolveScaleInvert(scales.y);
|
|
22632
23802
|
return {
|
|
22633
23803
|
area: chartArea,
|
|
22634
23804
|
chrome,
|
|
@@ -22648,6 +23818,7 @@ function compileChart(spec, optionsInput) {
|
|
|
22648
23818
|
a11y: {
|
|
22649
23819
|
altText,
|
|
22650
23820
|
dataTableFallback,
|
|
23821
|
+
...chartSpec.data.length > MAX_SR_TABLE_ROWS ? { totalRows: chartSpec.data.length } : {},
|
|
22651
23822
|
role: "img",
|
|
22652
23823
|
keyboardNavigable: marks.length > 0,
|
|
22653
23824
|
...chartSpec.a11y?.hidden ? { hidden: true } : {}
|
|
@@ -22655,16 +23826,22 @@ function compileChart(spec, optionsInput) {
|
|
|
22655
23826
|
theme,
|
|
22656
23827
|
dimensions: {
|
|
22657
23828
|
width: options.width,
|
|
22658
|
-
|
|
23829
|
+
// In chromeLayout 'grow' mode, computeDimensions grows total.height by the
|
|
23830
|
+
// chrome height so the plot keeps its full budget. In the default
|
|
23831
|
+
// 'subtract' mode total.height === options.height, so this is a no-op.
|
|
23832
|
+
height: dims.total.height
|
|
22659
23833
|
},
|
|
22660
23834
|
animation: resolvedAnimation,
|
|
22661
23835
|
watermark,
|
|
22662
23836
|
display: chartSpec.display,
|
|
22663
23837
|
crosshair,
|
|
22664
|
-
measureText: options.measureText
|
|
23838
|
+
measureText: options.measureText,
|
|
23839
|
+
...xInvert ? { xInvert } : {},
|
|
23840
|
+
...yInvert ? { yInvert } : {},
|
|
23841
|
+
...markRenderMode === "canvas" ? { markRenderMode } : {}
|
|
22665
23842
|
};
|
|
22666
23843
|
}
|
|
22667
|
-
function compileFaceted(chartSpec, renderSpec, facetChannel, options, theme, strategy, watermark, resolvedAnimation, crosshair, rawSpec, optionsInput) {
|
|
23844
|
+
function compileFaceted(chartSpec, renderSpec, facetChannel, facetDirection, options, theme, strategy, watermark, resolvedAnimation, crosshair, rawSpec, optionsInput) {
|
|
22668
23845
|
const measure = createMeasureFn(options.measureText);
|
|
22669
23846
|
const panelEncoding = { ...renderSpec.encoding };
|
|
22670
23847
|
delete panelEncoding.facet;
|
|
@@ -22710,10 +23887,10 @@ function compileFaceted(chartSpec, renderSpec, facetChannel, options, theme, str
|
|
|
22710
23887
|
plan.xAxisExtent,
|
|
22711
23888
|
dims.effectiveAxisGap
|
|
22712
23889
|
);
|
|
22713
|
-
const leftReservation = plan.leftGutter;
|
|
22714
23890
|
const bottomReservation = plan.xAxisExtent;
|
|
22715
|
-
const
|
|
22716
|
-
|
|
23891
|
+
const effectiveColumns = facetDirection === "column" ? facetValues.length : facetChannel.columns;
|
|
23892
|
+
const grid = computeFacetGrid(facetValues, effectiveColumns, chartArea, {
|
|
23893
|
+
left: 0,
|
|
22717
23894
|
bottom: bottomReservation
|
|
22718
23895
|
});
|
|
22719
23896
|
if (!optionsInput.facetHeightGrown && grid.panels.length > 0 && grid.panels[0].area.height < MIN_PANEL_HEIGHT) {
|
|
@@ -22728,8 +23905,10 @@ function compileFaceted(chartSpec, renderSpec, facetChannel, options, theme, str
|
|
|
22728
23905
|
}
|
|
22729
23906
|
}
|
|
22730
23907
|
const resolveConfig = chartSpec.resolve;
|
|
22731
|
-
const
|
|
22732
|
-
const
|
|
23908
|
+
const xDefault = facetDirection === "column" ? "independent" : "shared";
|
|
23909
|
+
const yDefault = facetDirection === "row" ? "independent" : "shared";
|
|
23910
|
+
const yResolve = resolveConfig?.scale?.y ?? yDefault;
|
|
23911
|
+
const xResolve = resolveConfig?.scale?.x ?? xDefault;
|
|
22733
23912
|
const fullScales = computeScales(panelSpec, chartArea, renderSpec.data);
|
|
22734
23913
|
const sharedXDomain = xResolve === "shared" && fullScales.x ? fullScales.x.scale.domain() : void 0;
|
|
22735
23914
|
const sharedYDomain = yResolve === "shared" && fullScales.y ? fullScales.y.scale.domain() : void 0;
|
|
@@ -22794,6 +23973,16 @@ function compileFaceted(chartSpec, renderSpec, facetChannel, options, theme, str
|
|
|
22794
23973
|
tickMarks: false
|
|
22795
23974
|
};
|
|
22796
23975
|
}
|
|
23976
|
+
const isBottomRow = gridPanel.row === grid.rows - 1;
|
|
23977
|
+
if (xResolve === "shared" && facetDirection === "row" && !isBottomRow && panelAxes.x) {
|
|
23978
|
+
panelAxes.x = {
|
|
23979
|
+
...panelAxes.x,
|
|
23980
|
+
ticks: [],
|
|
23981
|
+
label: void 0,
|
|
23982
|
+
domainLine: false,
|
|
23983
|
+
tickMarks: false
|
|
23984
|
+
};
|
|
23985
|
+
}
|
|
22797
23986
|
const panelMarks = renderer ? renderer(
|
|
22798
23987
|
panelSpecWithDomains,
|
|
22799
23988
|
panelScales,
|
|
@@ -22847,10 +24036,11 @@ function compileFaceted(chartSpec, renderSpec, facetChannel, options, theme, str
|
|
|
22847
24036
|
annotations: panelAnnotations,
|
|
22848
24037
|
header: {
|
|
22849
24038
|
text: gridPanel.key,
|
|
22850
|
-
x: gridPanel.headerPos.x,
|
|
24039
|
+
x: facetDirection === "row" ? gridPanel.area.x : gridPanel.headerPos.x,
|
|
22851
24040
|
y: gridPanel.headerPos.y,
|
|
22852
24041
|
fontSize: theme.fonts.sizes.body,
|
|
22853
|
-
fontWeight: theme.fonts.weights.medium
|
|
24042
|
+
fontWeight: theme.fonts.weights.medium,
|
|
24043
|
+
...facetDirection === "row" ? { textAnchor: "start" } : {}
|
|
22854
24044
|
}
|
|
22855
24045
|
});
|
|
22856
24046
|
}
|
|
@@ -22903,6 +24093,7 @@ function compileFaceted(chartSpec, renderSpec, facetChannel, options, theme, str
|
|
|
22903
24093
|
a11y: {
|
|
22904
24094
|
altText,
|
|
22905
24095
|
dataTableFallback,
|
|
24096
|
+
...chartSpec.data.length > MAX_SR_TABLE_ROWS ? { totalRows: chartSpec.data.length } : {},
|
|
22906
24097
|
role: "img",
|
|
22907
24098
|
keyboardNavigable: allMarks.length > 0,
|
|
22908
24099
|
...chartSpec.a11y?.hidden ? { hidden: true } : {}
|
|
@@ -22918,7 +24109,8 @@ function compileFaceted(chartSpec, renderSpec, facetChannel, options, theme, str
|
|
|
22918
24109
|
panels: panelLayouts,
|
|
22919
24110
|
facetField: facetChannel.field,
|
|
22920
24111
|
columns: grid.columns,
|
|
22921
|
-
sharedScales: yResolve === "shared" && xResolve === "shared"
|
|
24112
|
+
sharedScales: yResolve === "shared" && xResolve === "shared",
|
|
24113
|
+
direction: facetDirection
|
|
22922
24114
|
}
|
|
22923
24115
|
};
|
|
22924
24116
|
}
|
|
@@ -22972,6 +24164,7 @@ export {
|
|
|
22972
24164
|
EXIT_DEFAULTS,
|
|
22973
24165
|
UPDATE_DEFAULTS,
|
|
22974
24166
|
buildAreaPath,
|
|
24167
|
+
buildEdgeTooltip,
|
|
22975
24168
|
buildLinePath,
|
|
22976
24169
|
clampStaggerDelay,
|
|
22977
24170
|
clearRenderers,
|