@opendata-ai/openchart-engine 7.8.0 → 7.9.1
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 +63 -4
- package/dist/index.js +1644 -1428
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/compile-snapshot.test.ts.snap +31 -0
- package/src/__tests__/axes.test.ts +76 -0
- package/src/__tests__/compile-animation.test.ts +8 -8
- package/src/__tests__/compile-chart.test.ts +5 -5
- package/src/__tests__/dimensions.test.ts +116 -5
- package/src/__tests__/mark-keys.test.ts +294 -0
- package/src/charts/__tests__/post-process.test.ts +5 -26
- package/src/charts/bar/compute.ts +18 -0
- package/src/charts/column/compute.ts +18 -0
- package/src/charts/dot/compute.ts +30 -13
- package/src/charts/line/area.ts +20 -0
- package/src/charts/line/compute.ts +35 -1
- package/src/charts/line/curves.ts +47 -2
- package/src/charts/pie/compute.ts +13 -0
- package/src/charts/post-process.ts +2 -2
- package/src/charts/rule/index.ts +9 -0
- package/src/charts/scatter/compute.ts +15 -0
- package/src/charts/text/index.ts +9 -0
- package/src/charts/tick/index.ts +9 -0
- package/src/compiler/__tests__/animation.test.ts +78 -22
- package/src/compiler/__tests__/keys.test.ts +89 -0
- package/src/compiler/animation.ts +70 -50
- package/src/compiler/keys.ts +54 -0
- package/src/compiler/normalize.ts +1 -1
- package/src/index.ts +19 -1
- package/src/layout/axes.ts +125 -34
- package/src/layout/dimensions.ts +10 -11
- package/src/layout/plan.ts +11 -12
- package/src/tables/__tests__/compile-table.test.ts +5 -5
- package/src/tilemap/compile-tilemap.ts +93 -21
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
computeLabelBounds,
|
|
5
5
|
generateAltText,
|
|
6
6
|
generateDataTable,
|
|
7
|
-
getBreakpoint,
|
|
7
|
+
getBreakpoint as getBreakpoint2,
|
|
8
8
|
getHeightClass,
|
|
9
9
|
getLayoutStrategy,
|
|
10
10
|
resolveTheme as resolveTheme6
|
|
@@ -2125,6 +2125,32 @@ function computeEndpointLabels(spec, marks, theme, chartArea, strategy, endpoint
|
|
|
2125
2125
|
// src/charts/bar/compute.ts
|
|
2126
2126
|
import { isGradientDef, NARROW_VIEWPORT_MAX } from "@opendata-ai/openchart-core";
|
|
2127
2127
|
|
|
2128
|
+
// src/compiler/keys.ts
|
|
2129
|
+
var NULL_KEY = "\u2205";
|
|
2130
|
+
function serializeKeyValue(v) {
|
|
2131
|
+
if (v === null || v === void 0) return NULL_KEY;
|
|
2132
|
+
if (v instanceof Date) return String(v.getTime());
|
|
2133
|
+
return String(v);
|
|
2134
|
+
}
|
|
2135
|
+
function dedupeKeys(keys) {
|
|
2136
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2137
|
+
for (const k of keys) {
|
|
2138
|
+
counts.set(k, (counts.get(k) ?? 0) + 1);
|
|
2139
|
+
}
|
|
2140
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2141
|
+
const result = [];
|
|
2142
|
+
for (const k of keys) {
|
|
2143
|
+
if (counts.get(k) > 1) {
|
|
2144
|
+
const idx = seen.get(k) ?? 0;
|
|
2145
|
+
result.push(`${k}${idx}`);
|
|
2146
|
+
seen.set(k, idx + 1);
|
|
2147
|
+
} else {
|
|
2148
|
+
result.push(k);
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
return result;
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2128
2154
|
// src/transforms/predicates.ts
|
|
2129
2155
|
function isFieldPredicate(pred) {
|
|
2130
2156
|
return "field" in pred;
|
|
@@ -2374,8 +2400,21 @@ function computeBarMarks(spec, scales, _chartArea, _strategy, containerWidth) {
|
|
|
2374
2400
|
);
|
|
2375
2401
|
}
|
|
2376
2402
|
}
|
|
2403
|
+
const categoryField = yChannel.field;
|
|
2404
|
+
stampBarKeys(marks, categoryField, colorField);
|
|
2377
2405
|
return applyMarkDefOverrides(marks, spec, bandwidth);
|
|
2378
2406
|
}
|
|
2407
|
+
function stampBarKeys(marks, categoryField, colorField) {
|
|
2408
|
+
const rawKeys = marks.map((m) => {
|
|
2409
|
+
const series = colorField ? String(m.data[colorField] ?? "") : "";
|
|
2410
|
+
const cat = serializeKeyValue(m.data[categoryField]);
|
|
2411
|
+
return `${series}|${cat}`;
|
|
2412
|
+
});
|
|
2413
|
+
const keys = dedupeKeys(rawKeys);
|
|
2414
|
+
for (let i = 0; i < marks.length; i++) {
|
|
2415
|
+
marks[i].key = keys[i];
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2379
2418
|
function computeStackedBars(data, valueField, categoryField, colorField, xScale, yScale, bandwidth, _baseline, scales, stackMode = "zero") {
|
|
2380
2419
|
const marks = [];
|
|
2381
2420
|
const categoryGroups = groupByField(data, categoryField);
|
|
@@ -2873,8 +2912,21 @@ function computeColumnMarks(spec, scales, _chartArea, _strategy) {
|
|
|
2873
2912
|
conditionalColor
|
|
2874
2913
|
);
|
|
2875
2914
|
}
|
|
2915
|
+
const categoryField = xChannel.field;
|
|
2916
|
+
stampColumnKeys(marks, categoryField, colorField);
|
|
2876
2917
|
return applyMarkDefOverrides2(marks, spec, bandwidth);
|
|
2877
2918
|
}
|
|
2919
|
+
function stampColumnKeys(marks, categoryField, colorField) {
|
|
2920
|
+
const rawKeys = marks.map((m) => {
|
|
2921
|
+
const series = colorField ? String(m.data[colorField] ?? "") : "";
|
|
2922
|
+
const cat = serializeKeyValue(m.data[categoryField]);
|
|
2923
|
+
return `${series}|${cat}`;
|
|
2924
|
+
});
|
|
2925
|
+
const keys = dedupeKeys(rawKeys);
|
|
2926
|
+
for (let i = 0; i < marks.length; i++) {
|
|
2927
|
+
marks[i].key = keys[i];
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2878
2930
|
function computeSimpleColumns(data, categoryField, valueField, xScale, yScale, bandwidth, baseline, scales, sequentialColor = false, conditionalColor) {
|
|
2879
2931
|
const marks = [];
|
|
2880
2932
|
for (const row of data) {
|
|
@@ -3198,8 +3250,9 @@ function computeDotMarks(spec, scales, _chartArea, _strategy) {
|
|
|
3198
3250
|
const colorEnc = encoding.color && "field" in encoding.color ? encoding.color : void 0;
|
|
3199
3251
|
const isSequentialColor = colorEnc?.type === "quantitative";
|
|
3200
3252
|
const colorField = isSequentialColor ? void 0 : colorEnc?.field;
|
|
3253
|
+
let marks;
|
|
3201
3254
|
if (colorField) {
|
|
3202
|
-
|
|
3255
|
+
marks = computeDumbbellMarks(
|
|
3203
3256
|
spec.data,
|
|
3204
3257
|
xChannel.field,
|
|
3205
3258
|
yChannel.field,
|
|
@@ -3209,18 +3262,30 @@ function computeDotMarks(spec, scales, _chartArea, _strategy) {
|
|
|
3209
3262
|
bandwidth,
|
|
3210
3263
|
scales
|
|
3211
3264
|
);
|
|
3265
|
+
} else {
|
|
3266
|
+
marks = computeLollipopMarks(
|
|
3267
|
+
spec.data,
|
|
3268
|
+
xChannel.field,
|
|
3269
|
+
yChannel.field,
|
|
3270
|
+
xScale,
|
|
3271
|
+
yScale,
|
|
3272
|
+
bandwidth,
|
|
3273
|
+
baseline,
|
|
3274
|
+
scales,
|
|
3275
|
+
isSequentialColor
|
|
3276
|
+
);
|
|
3212
3277
|
}
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3278
|
+
const pointMarks = marks.filter((m) => m.type === "point");
|
|
3279
|
+
const rawKeys = pointMarks.map((m) => {
|
|
3280
|
+
const cat = serializeKeyValue(m.data[yChannel.field]);
|
|
3281
|
+
const series = colorField ? String(m.data[colorField] ?? "") : "";
|
|
3282
|
+
return series ? `${series}|${cat}` : cat;
|
|
3283
|
+
});
|
|
3284
|
+
const keys = dedupeKeys(rawKeys);
|
|
3285
|
+
for (let i = 0; i < pointMarks.length; i++) {
|
|
3286
|
+
pointMarks[i].key = keys[i];
|
|
3287
|
+
}
|
|
3288
|
+
return marks;
|
|
3224
3289
|
}
|
|
3225
3290
|
function computeDumbbellMarks(data, valueField, categoryField, colorField, xScale, yScale, bandwidth, scales) {
|
|
3226
3291
|
const marks = [];
|
|
@@ -4335,6 +4400,23 @@ function resolveCurve(interpolate) {
|
|
|
4335
4400
|
if (!interpolate) return monotoneX;
|
|
4336
4401
|
return CURVE_MAP[interpolate] ?? monotoneX;
|
|
4337
4402
|
}
|
|
4403
|
+
function buildLinePath(points, interpolate) {
|
|
4404
|
+
if (points.length === 0) return "";
|
|
4405
|
+
const curve = resolveCurve(interpolate);
|
|
4406
|
+
const generator = line_default().x((d) => d.x).y((d) => d.y).curve(curve);
|
|
4407
|
+
return generator(points) ?? "";
|
|
4408
|
+
}
|
|
4409
|
+
function buildAreaPath(topPoints, bottomPoints, interpolate) {
|
|
4410
|
+
if (topPoints.length === 0) return "";
|
|
4411
|
+
const curve = resolveCurve(interpolate);
|
|
4412
|
+
const paired = topPoints.map((tp, i) => ({
|
|
4413
|
+
x: tp.x,
|
|
4414
|
+
yTop: tp.y,
|
|
4415
|
+
yBottom: bottomPoints[i]?.y ?? 0
|
|
4416
|
+
}));
|
|
4417
|
+
const generator = area_default().x((d) => d.x).y0((d) => d.yBottom).y1((d) => d.yTop).curve(curve);
|
|
4418
|
+
return generator(paired) ?? "";
|
|
4419
|
+
}
|
|
4338
4420
|
|
|
4339
4421
|
// src/charts/line/area.ts
|
|
4340
4422
|
var DEFAULT_FILL_OPACITY = 0.15;
|
|
@@ -4436,8 +4518,15 @@ function computeSingleArea(spec, scales, _chartArea) {
|
|
|
4436
4518
|
fillValue = buildGradientFill(colorStr, defaultGradientStops);
|
|
4437
4519
|
fillOpacity = 1;
|
|
4438
4520
|
}
|
|
4521
|
+
const keyField = encoding.key && "field" in encoding.key ? encoding.key.field : void 0;
|
|
4522
|
+
const rawPointKeys = validPoints.map(
|
|
4523
|
+
(p) => keyField ? serializeKeyValue(p.row[keyField]) : serializeKeyValue(p.row[xChannel.field])
|
|
4524
|
+
);
|
|
4439
4525
|
marks.push({
|
|
4440
4526
|
type: "area",
|
|
4527
|
+
key: seriesKey === "__default__" ? "area" : seriesKey,
|
|
4528
|
+
pointKeys: dedupeKeys(rawPointKeys),
|
|
4529
|
+
interpolate: spec.markDef.interpolate ?? "monotone",
|
|
4441
4530
|
topPoints,
|
|
4442
4531
|
bottomPoints,
|
|
4443
4532
|
path: pathStr,
|
|
@@ -4536,8 +4625,12 @@ function computeStackedArea(spec, scales, chartArea, darkMode) {
|
|
|
4536
4625
|
fillValue = buildGradientFill(colorStr, stackedStops);
|
|
4537
4626
|
fillOpacity = 1;
|
|
4538
4627
|
}
|
|
4628
|
+
const rawPointKeys = validPoints.map((_p, idx) => serializeKeyValue(layer[idx]?.data.__x__));
|
|
4539
4629
|
marks.push({
|
|
4540
4630
|
type: "area",
|
|
4631
|
+
key: seriesKey,
|
|
4632
|
+
pointKeys: dedupeKeys(rawPointKeys),
|
|
4633
|
+
interpolate: spec.markDef.interpolate ?? "monotone",
|
|
4541
4634
|
topPoints,
|
|
4542
4635
|
bottomPoints,
|
|
4543
4636
|
path: pathStr,
|
|
@@ -4570,6 +4663,10 @@ function computeAreaMarks(spec, scales, chartArea, darkMode) {
|
|
|
4570
4663
|
} else {
|
|
4571
4664
|
marks = computeSingleArea(spec, scales, chartArea);
|
|
4572
4665
|
}
|
|
4666
|
+
const areaKeys = dedupeKeys(marks.map((m) => m.key));
|
|
4667
|
+
marks.forEach((m, i) => {
|
|
4668
|
+
m.key = areaKeys[i];
|
|
4669
|
+
});
|
|
4573
4670
|
const highlight = spec.highlight ?? [];
|
|
4574
4671
|
if (highlight.length > 0) {
|
|
4575
4672
|
const highlightSet = new Set(highlight);
|
|
@@ -4655,8 +4752,15 @@ function computeLineMarks(spec, scales, _chartArea, _strategy) {
|
|
|
4655
4752
|
let strokeDasharray;
|
|
4656
4753
|
if (styleOverride?.lineStyle === "dashed") strokeDasharray = "6 4";
|
|
4657
4754
|
else if (styleOverride?.lineStyle === "dotted") strokeDasharray = "2 3";
|
|
4755
|
+
const keyField = encoding.key && "field" in encoding.key ? encoding.key.field : void 0;
|
|
4756
|
+
const rawPointKeys = pointsWithData.map(
|
|
4757
|
+
(p) => keyField ? serializeKeyValue(p.row[keyField]) : serializeKeyValue(p.row[xChannel.field])
|
|
4758
|
+
);
|
|
4658
4759
|
const lineMark = {
|
|
4659
4760
|
type: "line",
|
|
4761
|
+
key: seriesStyleKey ?? "series",
|
|
4762
|
+
pointKeys: dedupeKeys(rawPointKeys),
|
|
4763
|
+
interpolate: spec.markDef.interpolate ?? "monotone",
|
|
4660
4764
|
points: allPoints,
|
|
4661
4765
|
path: combinedPath,
|
|
4662
4766
|
stroke: strokeColor,
|
|
@@ -4695,9 +4799,11 @@ function computeLineMarks(spec, scales, _chartArea, _strategy) {
|
|
|
4695
4799
|
}
|
|
4696
4800
|
const hollow = isEndpoints && visible;
|
|
4697
4801
|
const pointColorStr = getRepresentativeColor5(pointColor);
|
|
4802
|
+
const pointKey = keyField ? serializeKeyValue(p.row[keyField]) : seriesStyleKey ? `${seriesStyleKey}|${serializeKeyValue(p.row[xChannel.field])}` : serializeKeyValue(p.row[xChannel.field]);
|
|
4698
4803
|
if (isSingleEndpoint) {
|
|
4699
4804
|
bucket.push({
|
|
4700
4805
|
type: "point",
|
|
4806
|
+
key: pointKey,
|
|
4701
4807
|
cx: p.x,
|
|
4702
4808
|
cy: p.y,
|
|
4703
4809
|
r: 3.5,
|
|
@@ -4712,6 +4818,7 @@ function computeLineMarks(spec, scales, _chartArea, _strategy) {
|
|
|
4712
4818
|
}
|
|
4713
4819
|
const pointMark = {
|
|
4714
4820
|
type: "point",
|
|
4821
|
+
key: pointKey,
|
|
4715
4822
|
cx: p.x,
|
|
4716
4823
|
cy: p.y,
|
|
4717
4824
|
r: visible ? DEFAULT_POINT_RADIUS : 0,
|
|
@@ -4728,7 +4835,20 @@ function computeLineMarks(spec, scales, _chartArea, _strategy) {
|
|
|
4728
4835
|
}
|
|
4729
4836
|
}
|
|
4730
4837
|
}
|
|
4731
|
-
|
|
4838
|
+
const allMarks = [...mutedMarks, ...highlightedMarks];
|
|
4839
|
+
const lineMarks = allMarks.filter((m) => m.type === "line");
|
|
4840
|
+
const lineKeys = dedupeKeys(lineMarks.map((m) => m.key));
|
|
4841
|
+
lineMarks.forEach((m, i) => {
|
|
4842
|
+
m.key = lineKeys[i];
|
|
4843
|
+
});
|
|
4844
|
+
const pointMarks = allMarks.filter((m) => m.type === "point");
|
|
4845
|
+
if (pointMarks.length > 0) {
|
|
4846
|
+
const pointKeys = dedupeKeys(pointMarks.map((m) => m.key ?? ""));
|
|
4847
|
+
pointMarks.forEach((m, i) => {
|
|
4848
|
+
m.key = pointKeys[i];
|
|
4849
|
+
});
|
|
4850
|
+
}
|
|
4851
|
+
return allMarks;
|
|
4732
4852
|
}
|
|
4733
4853
|
function _getMidValue(rows, field) {
|
|
4734
4854
|
const values = rows.map((r) => Number(r[field])).filter(Number.isFinite);
|
|
@@ -5028,6 +5148,14 @@ function computePieMarks(spec, scales, chartArea, _strategy, isDonut = false) {
|
|
|
5028
5148
|
aria
|
|
5029
5149
|
});
|
|
5030
5150
|
}
|
|
5151
|
+
const rawKeys = marks.map((m) => {
|
|
5152
|
+
const label = categoryField ? String(m.data[categoryField] ?? "") : String(m.data.label ?? m.data.name ?? m.data.category ?? "");
|
|
5153
|
+
return serializeKeyValue(label);
|
|
5154
|
+
});
|
|
5155
|
+
const keys = dedupeKeys(rawKeys);
|
|
5156
|
+
for (let i = 0; i < marks.length; i++) {
|
|
5157
|
+
marks[i].key = keys[i];
|
|
5158
|
+
}
|
|
5031
5159
|
return marks;
|
|
5032
5160
|
}
|
|
5033
5161
|
|
|
@@ -5202,6 +5330,12 @@ function computeRuleMarks(spec, scales, chartArea) {
|
|
|
5202
5330
|
aria
|
|
5203
5331
|
});
|
|
5204
5332
|
}
|
|
5333
|
+
const posField = xChannel?.field ?? yChannel?.field;
|
|
5334
|
+
const rawKeys = marks.map((m, i) => posField ? serializeKeyValue(m.data[posField]) : String(i));
|
|
5335
|
+
const keys = dedupeKeys(rawKeys);
|
|
5336
|
+
for (let i = 0; i < marks.length; i++) {
|
|
5337
|
+
marks[i].key = keys[i];
|
|
5338
|
+
}
|
|
5205
5339
|
return marks;
|
|
5206
5340
|
}
|
|
5207
5341
|
var ruleRenderer = (spec, scales, chartArea, _strategy, _theme) => {
|
|
@@ -6190,19 +6324,19 @@ function copy(source, target) {
|
|
|
6190
6324
|
return target.domain(source.domain()).range(source.range()).interpolate(source.interpolate()).clamp(source.clamp()).unknown(source.unknown());
|
|
6191
6325
|
}
|
|
6192
6326
|
function transformer() {
|
|
6193
|
-
var domain = unit, range2 = unit, interpolate = value_default, transform, untransform, unknown,
|
|
6327
|
+
var domain = unit, range2 = unit, interpolate = value_default, transform, untransform, unknown, clamp2 = identity, piecewise, output, input;
|
|
6194
6328
|
function rescale() {
|
|
6195
6329
|
var n = Math.min(domain.length, range2.length);
|
|
6196
|
-
if (
|
|
6330
|
+
if (clamp2 !== identity) clamp2 = clamper(domain[0], domain[n - 1]);
|
|
6197
6331
|
piecewise = n > 2 ? polymap : bimap;
|
|
6198
6332
|
output = input = null;
|
|
6199
6333
|
return scale;
|
|
6200
6334
|
}
|
|
6201
6335
|
function scale(x2) {
|
|
6202
|
-
return x2 == null || isNaN(x2 = +x2) ? unknown : (output || (output = piecewise(domain.map(transform), range2, interpolate)))(transform(
|
|
6336
|
+
return x2 == null || isNaN(x2 = +x2) ? unknown : (output || (output = piecewise(domain.map(transform), range2, interpolate)))(transform(clamp2(x2)));
|
|
6203
6337
|
}
|
|
6204
6338
|
scale.invert = function(y2) {
|
|
6205
|
-
return
|
|
6339
|
+
return clamp2(untransform((input || (input = piecewise(range2, domain.map(transform), number_default)))(y2)));
|
|
6206
6340
|
};
|
|
6207
6341
|
scale.domain = function(_) {
|
|
6208
6342
|
return arguments.length ? (domain = Array.from(_, number2), rescale()) : domain.slice();
|
|
@@ -6214,7 +6348,7 @@ function transformer() {
|
|
|
6214
6348
|
return range2 = Array.from(_), interpolate = round_default, rescale();
|
|
6215
6349
|
};
|
|
6216
6350
|
scale.clamp = function(_) {
|
|
6217
|
-
return arguments.length ? (
|
|
6351
|
+
return arguments.length ? (clamp2 = _ ? true : identity, rescale()) : clamp2 !== identity;
|
|
6218
6352
|
};
|
|
6219
6353
|
scale.interpolate = function(_) {
|
|
6220
6354
|
return arguments.length ? (interpolate = _, rescale()) : interpolate;
|
|
@@ -7546,15 +7680,15 @@ function utcTime() {
|
|
|
7546
7680
|
|
|
7547
7681
|
// ../../node_modules/.bun/d3-scale@4.0.2/node_modules/d3-scale/src/sequential.js
|
|
7548
7682
|
function transformer2() {
|
|
7549
|
-
var x0 = 0, x1 = 1, t02, t12, k10, transform, interpolator = identity,
|
|
7683
|
+
var x0 = 0, x1 = 1, t02, t12, k10, transform, interpolator = identity, clamp2 = false, unknown;
|
|
7550
7684
|
function scale(x2) {
|
|
7551
|
-
return x2 == null || isNaN(x2 = +x2) ? unknown : interpolator(k10 === 0 ? 0.5 : (x2 = (transform(x2) - t02) * k10,
|
|
7685
|
+
return x2 == null || isNaN(x2 = +x2) ? unknown : interpolator(k10 === 0 ? 0.5 : (x2 = (transform(x2) - t02) * k10, clamp2 ? Math.max(0, Math.min(1, x2)) : x2));
|
|
7552
7686
|
}
|
|
7553
7687
|
scale.domain = function(_) {
|
|
7554
7688
|
return arguments.length ? ([x0, x1] = _, t02 = transform(x0 = +x0), t12 = transform(x1 = +x1), k10 = t02 === t12 ? 0 : 1 / (t12 - t02), scale) : [x0, x1];
|
|
7555
7689
|
};
|
|
7556
7690
|
scale.clamp = function(_) {
|
|
7557
|
-
return arguments.length ? (
|
|
7691
|
+
return arguments.length ? (clamp2 = !!_, scale) : clamp2;
|
|
7558
7692
|
};
|
|
7559
7693
|
scale.interpolator = function(_) {
|
|
7560
7694
|
return arguments.length ? (interpolator = _, scale) : interpolator;
|
|
@@ -7639,6 +7773,8 @@ function computeScatterMarks(spec, scales, _chartArea, _strategy) {
|
|
|
7639
7773
|
const [radiusMin, radiusMax] = explicitRange ?? [MIN_BUBBLE_RADIUS, MAX_BUBBLE_RADIUS];
|
|
7640
7774
|
sizeScale = sqrt2().domain([sizeMin, sizeMax]).range([radiusMin, radiusMax]).clamp(true);
|
|
7641
7775
|
}
|
|
7776
|
+
const keyEnc = encoding.key && "field" in encoding.key ? encoding.key : void 0;
|
|
7777
|
+
const keyField = keyEnc?.field;
|
|
7642
7778
|
const marks = [];
|
|
7643
7779
|
for (const row of spec.data) {
|
|
7644
7780
|
const rawX = row[xChannel.field];
|
|
@@ -7682,6 +7818,16 @@ function computeScatterMarks(spec, scales, _chartArea, _strategy) {
|
|
|
7682
7818
|
aria
|
|
7683
7819
|
});
|
|
7684
7820
|
}
|
|
7821
|
+
const rawKeys = marks.map((m) => {
|
|
7822
|
+
if (keyField) return serializeKeyValue(m.data[keyField]);
|
|
7823
|
+
const cat = colorField ? String(m.data[colorField] ?? "") : "";
|
|
7824
|
+
const xVal = serializeKeyValue(m.data[xChannel.field]);
|
|
7825
|
+
return cat ? `${cat}|${xVal}` : xVal;
|
|
7826
|
+
});
|
|
7827
|
+
const keys = dedupeKeys(rawKeys);
|
|
7828
|
+
for (let i = 0; i < marks.length; i++) {
|
|
7829
|
+
marks[i].key = keys[i];
|
|
7830
|
+
}
|
|
7685
7831
|
return marks;
|
|
7686
7832
|
}
|
|
7687
7833
|
|
|
@@ -7799,6 +7945,12 @@ function computeTextMarks(spec, scales) {
|
|
|
7799
7945
|
aria
|
|
7800
7946
|
});
|
|
7801
7947
|
}
|
|
7948
|
+
const posField = xChannel?.field ?? yChannel?.field;
|
|
7949
|
+
const rawKeys = marks.map((m, i) => posField ? serializeKeyValue(m.data[posField]) : String(i));
|
|
7950
|
+
const keys = dedupeKeys(rawKeys);
|
|
7951
|
+
for (let i = 0; i < marks.length; i++) {
|
|
7952
|
+
marks[i].key = keys[i];
|
|
7953
|
+
}
|
|
7802
7954
|
return marks;
|
|
7803
7955
|
}
|
|
7804
7956
|
var textRenderer = (spec, scales, _chartArea, _strategy, _theme) => {
|
|
@@ -7841,6 +7993,12 @@ function computeTickMarks(spec, scales, _chartArea) {
|
|
|
7841
7993
|
aria
|
|
7842
7994
|
});
|
|
7843
7995
|
}
|
|
7996
|
+
const posField = isHorizontal ? xChannel.field : yChannel.field;
|
|
7997
|
+
const rawKeys = marks.map((m) => serializeKeyValue(m.data[posField]));
|
|
7998
|
+
const keys = dedupeKeys(rawKeys);
|
|
7999
|
+
for (let i = 0; i < marks.length; i++) {
|
|
8000
|
+
marks[i].key = keys[i];
|
|
8001
|
+
}
|
|
7844
8002
|
return marks;
|
|
7845
8003
|
}
|
|
7846
8004
|
var tickRenderer = (spec, scales, chartArea, _strategy, _theme) => {
|
|
@@ -7893,69 +8051,70 @@ var ENTER_DEFAULTS = {
|
|
|
7893
8051
|
duration: 500,
|
|
7894
8052
|
ease: "smooth",
|
|
7895
8053
|
staggerDelay: 80,
|
|
7896
|
-
staggerOrder: "index"
|
|
7897
|
-
|
|
8054
|
+
staggerOrder: "index"
|
|
8055
|
+
};
|
|
8056
|
+
var UPDATE_DEFAULTS = {
|
|
8057
|
+
duration: 500,
|
|
8058
|
+
ease: "smooth",
|
|
8059
|
+
staggerDelay: 0,
|
|
8060
|
+
staggerOrder: "index"
|
|
7898
8061
|
};
|
|
8062
|
+
var EXIT_DEFAULTS = {
|
|
8063
|
+
duration: 300,
|
|
8064
|
+
ease: "smooth",
|
|
8065
|
+
staggerDelay: 0,
|
|
8066
|
+
staggerOrder: "index"
|
|
8067
|
+
};
|
|
8068
|
+
var DEFAULT_ANNOTATION_DELAY = 200;
|
|
7899
8069
|
var MAX_TOTAL_STAGGER_MS = 2e3;
|
|
7900
8070
|
function resolveAnimation(spec) {
|
|
7901
8071
|
if (spec === void 0 || spec === false) return void 0;
|
|
7902
8072
|
if (spec === true) {
|
|
7903
8073
|
return {
|
|
7904
|
-
|
|
7905
|
-
|
|
7906
|
-
|
|
7907
|
-
|
|
7908
|
-
staggerOrder: ENTER_DEFAULTS.staggerOrder,
|
|
7909
|
-
annotationDelay: ENTER_DEFAULTS.annotationDelay
|
|
8074
|
+
enter: { ...ENTER_DEFAULTS },
|
|
8075
|
+
update: { ...UPDATE_DEFAULTS },
|
|
8076
|
+
exit: { ...EXIT_DEFAULTS },
|
|
8077
|
+
annotationDelay: DEFAULT_ANNOTATION_DELAY
|
|
7910
8078
|
};
|
|
7911
8079
|
}
|
|
7912
8080
|
const config = spec;
|
|
7913
|
-
|
|
7914
|
-
|
|
7915
|
-
|
|
7916
|
-
|
|
8081
|
+
const enter = resolvePhase(config.enter, ENTER_DEFAULTS);
|
|
8082
|
+
const update = resolvePhase(config.update, UPDATE_DEFAULTS);
|
|
8083
|
+
const exit = resolvePhase(config.exit, EXIT_DEFAULTS);
|
|
8084
|
+
if (!enter && !update && !exit) return void 0;
|
|
7917
8085
|
return {
|
|
7918
|
-
|
|
7919
|
-
|
|
7920
|
-
|
|
7921
|
-
|
|
7922
|
-
staggerOrder: enterConfig.staggerOrder,
|
|
7923
|
-
annotationDelay: config.annotationDelay ?? ENTER_DEFAULTS.annotationDelay
|
|
8086
|
+
enter: enter ?? void 0,
|
|
8087
|
+
update: update ?? void 0,
|
|
8088
|
+
exit: exit ?? void 0,
|
|
8089
|
+
annotationDelay: config.annotationDelay ?? DEFAULT_ANNOTATION_DELAY
|
|
7924
8090
|
};
|
|
7925
8091
|
}
|
|
7926
8092
|
function clampStaggerDelay(delay, elementCount) {
|
|
7927
8093
|
if (elementCount <= 1) return 0;
|
|
7928
8094
|
return Math.min(delay, MAX_TOTAL_STAGGER_MS / elementCount);
|
|
7929
8095
|
}
|
|
7930
|
-
function
|
|
7931
|
-
|
|
7932
|
-
}
|
|
7933
|
-
function resolvePhaseConfig(phase) {
|
|
8096
|
+
function resolvePhase(phase, defaults) {
|
|
8097
|
+
if (phase === false) return null;
|
|
7934
8098
|
if (phase === void 0 || phase === true) {
|
|
7935
|
-
return {
|
|
7936
|
-
duration: ENTER_DEFAULTS.duration,
|
|
7937
|
-
ease: ENTER_DEFAULTS.ease,
|
|
7938
|
-
staggerDelay: ENTER_DEFAULTS.staggerDelay,
|
|
7939
|
-
staggerOrder: ENTER_DEFAULTS.staggerOrder
|
|
7940
|
-
};
|
|
8099
|
+
return { ...defaults };
|
|
7941
8100
|
}
|
|
7942
8101
|
const cfg = phase;
|
|
7943
|
-
const stagger = resolveStagger(cfg.stagger);
|
|
8102
|
+
const stagger = resolveStagger(cfg.stagger, defaults);
|
|
7944
8103
|
return {
|
|
7945
|
-
duration: cfg.duration ??
|
|
7946
|
-
ease: cfg.ease ??
|
|
8104
|
+
duration: cfg.duration ?? defaults.duration,
|
|
8105
|
+
ease: cfg.ease ?? defaults.ease,
|
|
7947
8106
|
staggerDelay: stagger.delay,
|
|
7948
8107
|
staggerOrder: stagger.order
|
|
7949
8108
|
};
|
|
7950
8109
|
}
|
|
7951
|
-
function resolveStagger(stagger) {
|
|
8110
|
+
function resolveStagger(stagger, defaults) {
|
|
7952
8111
|
if (stagger === false) return { delay: 0, order: "index" };
|
|
7953
8112
|
if (stagger === void 0 || stagger === true) {
|
|
7954
|
-
return { delay:
|
|
8113
|
+
return { delay: defaults.staggerDelay, order: defaults.staggerOrder };
|
|
7955
8114
|
}
|
|
7956
8115
|
return {
|
|
7957
|
-
delay: stagger.delay ??
|
|
7958
|
-
order: stagger.order ??
|
|
8116
|
+
delay: stagger.delay ?? defaults.staggerDelay,
|
|
8117
|
+
order: stagger.order ?? defaults.staggerOrder
|
|
7959
8118
|
};
|
|
7960
8119
|
}
|
|
7961
8120
|
|
|
@@ -8160,7 +8319,7 @@ function inferFieldType(data, field) {
|
|
|
8160
8319
|
}
|
|
8161
8320
|
function inferEncodingTypes(encoding, data, warnings) {
|
|
8162
8321
|
const result = { ...encoding };
|
|
8163
|
-
for (const channel of ["x", "y", "color", "size", "detail"]) {
|
|
8322
|
+
for (const channel of ["x", "y", "color", "size", "detail", "key"]) {
|
|
8164
8323
|
const spec = result[channel];
|
|
8165
8324
|
if (!spec) continue;
|
|
8166
8325
|
if ("condition" in spec) continue;
|
|
@@ -9799,8 +9958,8 @@ function getMarkPrimaryValue(mark) {
|
|
|
9799
9958
|
}
|
|
9800
9959
|
}
|
|
9801
9960
|
function assignAnimationIndices(marks, animation) {
|
|
9802
|
-
if (!animation?.
|
|
9803
|
-
if (animation.staggerOrder === "value") {
|
|
9961
|
+
if (!animation?.enter) return;
|
|
9962
|
+
if (animation.enter.staggerOrder === "value") {
|
|
9804
9963
|
const indexed = marks.map((m, i) => ({ mark: m, idx: i }));
|
|
9805
9964
|
indexed.sort((a, b) => {
|
|
9806
9965
|
const av = getMarkPrimaryValue(a.mark);
|
|
@@ -9900,9 +10059,9 @@ import {
|
|
|
9900
10059
|
AXIS_TITLE_TRAILING_PAD as AXIS_TITLE_TRAILING_PAD2,
|
|
9901
10060
|
abbreviateNumber as abbreviateNumber4,
|
|
9902
10061
|
BREAKPOINT_COMPACT_MAX as BREAKPOINT_COMPACT_MAX2,
|
|
9903
|
-
estimateTextWidth as
|
|
10062
|
+
estimateTextWidth as estimateTextWidth15,
|
|
9904
10063
|
formatNumber as formatNumber5,
|
|
9905
|
-
getAxisTitleOffset,
|
|
10064
|
+
getAxisTitleOffset as getAxisTitleOffset2,
|
|
9906
10065
|
resolveTheme as resolveTheme2,
|
|
9907
10066
|
TICK_LABEL_OFFSET
|
|
9908
10067
|
} from "@opendata-ai/openchart-core";
|
|
@@ -9911,11 +10070,11 @@ import {
|
|
|
9911
10070
|
import {
|
|
9912
10071
|
AXIS_TITLE_TRAILING_PAD,
|
|
9913
10072
|
abbreviateNumber as abbreviateNumber3,
|
|
9914
|
-
axisTitleOffset,
|
|
10073
|
+
axisTitleOffset as axisTitleOffset2,
|
|
9915
10074
|
BREAKPOINT_COMPACT_MAX,
|
|
9916
10075
|
computeChrome as computeChrome2,
|
|
9917
|
-
computeXAxisExtentFromLabels,
|
|
9918
|
-
estimateTextWidth as
|
|
10076
|
+
computeXAxisExtentFromLabels as computeXAxisExtentFromLabels2,
|
|
10077
|
+
estimateTextWidth as estimateTextWidth14,
|
|
9919
10078
|
formatNumber as formatNumber4,
|
|
9920
10079
|
HPAD_COMPACT_FRACTION,
|
|
9921
10080
|
HPAD_COMPACT_MIN,
|
|
@@ -10337,6 +10496,15 @@ function placeLegend(content, chartArea, containerWidth, _theme, xAxisHeight) {
|
|
|
10337
10496
|
};
|
|
10338
10497
|
}
|
|
10339
10498
|
|
|
10499
|
+
// src/layout/axes.ts
|
|
10500
|
+
import {
|
|
10501
|
+
axisTitleOffset,
|
|
10502
|
+
computeXAxisExtentFromLabels,
|
|
10503
|
+
estimateTextWidth as estimateTextWidth13,
|
|
10504
|
+
getAxisTitleOffset,
|
|
10505
|
+
X_AXIS_TITLE_BAND_ROTATED
|
|
10506
|
+
} from "@opendata-ai/openchart-core";
|
|
10507
|
+
|
|
10340
10508
|
// src/layout/axes/rotation.ts
|
|
10341
10509
|
var BAND_FIT_FRACTION = 0.85;
|
|
10342
10510
|
var AUTO_ROTATE_ANGLE = -45;
|
|
@@ -10346,6 +10514,50 @@ function resolveBandTickAngle(explicitAngle, maxLabelWidth, bandwidth, labelCoun
|
|
|
10346
10514
|
return maxLabelWidth > bandwidth * BAND_FIT_FRACTION ? AUTO_ROTATE_ANGLE : void 0;
|
|
10347
10515
|
}
|
|
10348
10516
|
|
|
10517
|
+
// src/layout/axes/thinning.ts
|
|
10518
|
+
import { estimateTextWidth as estimateTextWidth12 } from "@opendata-ai/openchart-core";
|
|
10519
|
+
var MIN_TICK_GAP_FACTOR = 0.5;
|
|
10520
|
+
var MIN_TICK_COUNT = 2;
|
|
10521
|
+
function measureLabel(text, fontSize, fontWeight, measureText) {
|
|
10522
|
+
return measureText ? measureText(text, fontSize, fontWeight).width : estimateTextWidth12(text, fontSize, fontWeight);
|
|
10523
|
+
}
|
|
10524
|
+
function ticksOverlap(ticks2, fontSize, fontWeight, measureText, orientation = "horizontal") {
|
|
10525
|
+
if (ticks2.length < 2) return false;
|
|
10526
|
+
const minGap = fontSize * MIN_TICK_GAP_FACTOR;
|
|
10527
|
+
if (orientation === "vertical") {
|
|
10528
|
+
const sorted = [...ticks2].sort((a, b) => a.position - b.position);
|
|
10529
|
+
const labelHeight = fontSize * 1.2;
|
|
10530
|
+
for (let i = 0; i < sorted.length - 1; i++) {
|
|
10531
|
+
const aBottom = sorted[i].position + labelHeight / 2;
|
|
10532
|
+
const bTop = sorted[i + 1].position - labelHeight / 2;
|
|
10533
|
+
if (aBottom + minGap > bTop) return true;
|
|
10534
|
+
}
|
|
10535
|
+
return false;
|
|
10536
|
+
}
|
|
10537
|
+
for (let i = 0; i < ticks2.length - 1; i++) {
|
|
10538
|
+
const aWidth = measureLabel(ticks2[i].label, fontSize, fontWeight, measureText);
|
|
10539
|
+
const bWidth = measureLabel(ticks2[i + 1].label, fontSize, fontWeight, measureText);
|
|
10540
|
+
const aRight = ticks2[i].position + aWidth / 2;
|
|
10541
|
+
const bLeft = ticks2[i + 1].position - bWidth / 2;
|
|
10542
|
+
if (aRight + minGap > bLeft) return true;
|
|
10543
|
+
}
|
|
10544
|
+
return false;
|
|
10545
|
+
}
|
|
10546
|
+
function thinTicksUntilFit(ticks2, fontSize, fontWeight, measureText, orientation = "horizontal") {
|
|
10547
|
+
if (!ticksOverlap(ticks2, fontSize, fontWeight, measureText, orientation)) return ticks2;
|
|
10548
|
+
let current = ticks2;
|
|
10549
|
+
while (current.length > MIN_TICK_COUNT) {
|
|
10550
|
+
const thinned = [current[0]];
|
|
10551
|
+
for (let i = 2; i < current.length - 1; i += 2) {
|
|
10552
|
+
thinned.push(current[i]);
|
|
10553
|
+
}
|
|
10554
|
+
if (current.length > 1) thinned.push(current[current.length - 1]);
|
|
10555
|
+
current = thinned;
|
|
10556
|
+
if (!ticksOverlap(current, fontSize, fontWeight, measureText, orientation)) break;
|
|
10557
|
+
}
|
|
10558
|
+
return current;
|
|
10559
|
+
}
|
|
10560
|
+
|
|
10349
10561
|
// src/layout/axes/ticks.ts
|
|
10350
10562
|
import {
|
|
10351
10563
|
abbreviateNumber as abbreviateNumber2,
|
|
@@ -10535,168 +10747,508 @@ function resolveExplicitTicks(values, resolvedScale) {
|
|
|
10535
10747
|
});
|
|
10536
10748
|
}
|
|
10537
10749
|
|
|
10538
|
-
// src/layout/
|
|
10539
|
-
|
|
10540
|
-
|
|
10541
|
-
|
|
10542
|
-
|
|
10543
|
-
|
|
10544
|
-
|
|
10545
|
-
|
|
10546
|
-
|
|
10750
|
+
// src/layout/axes.ts
|
|
10751
|
+
var CATEGORICAL_SCALE_TYPES = /* @__PURE__ */ new Set(["band", "point", "ordinal"]);
|
|
10752
|
+
function yTickPositionIsInline(yChannel, markType, resolvedContinuous) {
|
|
10753
|
+
if (!yChannel) return false;
|
|
10754
|
+
const axisCfg = yChannel.axis ?? void 0;
|
|
10755
|
+
const explicit = axisCfg?.tickPosition;
|
|
10756
|
+
if (explicit) return explicit === "inline";
|
|
10757
|
+
const isLineOrArea2 = markType === "line" || markType === "area";
|
|
10758
|
+
if (!isLineOrArea2) return false;
|
|
10759
|
+
if (axisCfg?.orient === "right") return false;
|
|
10760
|
+
if (resolvedContinuous !== void 0) return resolvedContinuous;
|
|
10761
|
+
const explicitScaleType = yChannel.scale?.type;
|
|
10762
|
+
return explicitScaleType ? !CATEGORICAL_SCALE_TYPES.has(explicitScaleType) : yChannel.type === "quantitative" || yChannel.type === "temporal";
|
|
10547
10763
|
}
|
|
10548
|
-
|
|
10549
|
-
|
|
10550
|
-
|
|
10551
|
-
|
|
10552
|
-
|
|
10553
|
-
|
|
10554
|
-
|
|
10555
|
-
|
|
10556
|
-
|
|
10764
|
+
var HEIGHT_MINIMAL_THRESHOLD = 80;
|
|
10765
|
+
var HEIGHT_REDUCED_THRESHOLD = 100;
|
|
10766
|
+
var WIDTH_MINIMAL_THRESHOLD = 150;
|
|
10767
|
+
var WIDTH_REDUCED_THRESHOLD = 300;
|
|
10768
|
+
var DENSITY_ORDER = ["full", "reduced", "minimal"];
|
|
10769
|
+
var GROUNDED_MARKS = /* @__PURE__ */ new Set(["bar", "tick", "lollipop"]);
|
|
10770
|
+
function effectiveDensity(baseDensity, axisLength, minimalThreshold, reducedThreshold) {
|
|
10771
|
+
let density = baseDensity;
|
|
10772
|
+
if (axisLength < minimalThreshold) {
|
|
10773
|
+
density = "minimal";
|
|
10774
|
+
} else if (axisLength < reducedThreshold) {
|
|
10775
|
+
const baseIdx = DENSITY_ORDER.indexOf(baseDensity);
|
|
10776
|
+
const reducedIdx = DENSITY_ORDER.indexOf("reduced");
|
|
10777
|
+
density = DENSITY_ORDER[Math.max(baseIdx, reducedIdx)];
|
|
10557
10778
|
}
|
|
10558
|
-
return
|
|
10559
|
-
}
|
|
10560
|
-
function applyCategoricalSort(values, sort) {
|
|
10561
|
-
if (!sort) return values;
|
|
10562
|
-
const sorted = [...values].sort((a, b) => a.localeCompare(b, void 0, { numeric: true }));
|
|
10563
|
-
if (sort === "descending") sorted.reverse();
|
|
10564
|
-
return sorted;
|
|
10779
|
+
return density;
|
|
10565
10780
|
}
|
|
10566
|
-
|
|
10567
|
-
|
|
10568
|
-
|
|
10569
|
-
|
|
10570
|
-
if (
|
|
10571
|
-
|
|
10572
|
-
|
|
10781
|
+
var CONTINUOUS_TICK_FLOOR = 4;
|
|
10782
|
+
var OVERSHOOT_TOLERANCE = 1.5;
|
|
10783
|
+
var MIN_QUANTITATIVE_TICKS = 2;
|
|
10784
|
+
function ensureMinContinuousTicks(scale, ticks2, requestedCount) {
|
|
10785
|
+
if (ticks2.length >= MIN_QUANTITATIVE_TICKS) return ticks2;
|
|
10786
|
+
if (!scale || !scaleSupportsTickCount(scale)) return ticks2;
|
|
10787
|
+
let best = ticks2;
|
|
10788
|
+
for (let n = Math.max(requestedCount, MIN_QUANTITATIVE_TICKS) + 1; n <= 10; n++) {
|
|
10789
|
+
const candidate = buildContinuousTicks(scale, n);
|
|
10790
|
+
if (candidate.length >= MIN_QUANTITATIVE_TICKS) return candidate;
|
|
10791
|
+
if (candidate.length > best.length) best = candidate;
|
|
10573
10792
|
}
|
|
10793
|
+
return best;
|
|
10574
10794
|
}
|
|
10575
|
-
function
|
|
10576
|
-
|
|
10577
|
-
const
|
|
10578
|
-
const
|
|
10579
|
-
|
|
10580
|
-
|
|
10795
|
+
function fitContinuousTicks(scale, initialTicks, initialCount, fontSize, fontWeight, axisLength, orientation, measureText) {
|
|
10796
|
+
if (!scale || !scaleSupportsTickCount(scale)) return initialTicks;
|
|
10797
|
+
const flooredTicks = ensureMinContinuousTicks(scale, initialTicks, initialCount);
|
|
10798
|
+
const tolerance = initialCount * OVERSHOOT_TOLERANCE;
|
|
10799
|
+
const overshoots = flooredTicks.length > tolerance;
|
|
10800
|
+
const overlaps = ticksOverlap(flooredTicks, fontSize, fontWeight, measureText, orientation);
|
|
10801
|
+
if (!overshoots && !overlaps) return flooredTicks;
|
|
10802
|
+
const minThreshold = orientation === "vertical" ? HEIGHT_MINIMAL_THRESHOLD : WIDTH_MINIMAL_THRESHOLD;
|
|
10803
|
+
const floor = axisLength >= minThreshold ? CONTINUOUS_TICK_FLOOR : 2;
|
|
10804
|
+
let bestWithinFloor;
|
|
10805
|
+
for (let n = initialCount - 1; n >= 2; n--) {
|
|
10806
|
+
const candidate = buildContinuousTicks(scale, n);
|
|
10807
|
+
const candidateOvershoots = candidate.length > tolerance;
|
|
10808
|
+
const candidateOverlaps = ticksOverlap(
|
|
10809
|
+
candidate,
|
|
10810
|
+
fontSize,
|
|
10811
|
+
fontWeight,
|
|
10812
|
+
measureText,
|
|
10813
|
+
orientation
|
|
10814
|
+
);
|
|
10815
|
+
if (!candidateOvershoots && !candidateOverlaps) {
|
|
10816
|
+
return candidate;
|
|
10817
|
+
}
|
|
10818
|
+
if (candidate.length >= floor) bestWithinFloor = candidate;
|
|
10581
10819
|
}
|
|
10582
|
-
|
|
10583
|
-
|
|
10820
|
+
const rawFallback = bestWithinFloor ?? buildContinuousTicks(scale, floor);
|
|
10821
|
+
const fallback = ensureMinContinuousTicks(scale, rawFallback, floor);
|
|
10822
|
+
return thinTicksUntilFit(fallback, fontSize, fontWeight, measureText, orientation);
|
|
10584
10823
|
}
|
|
10585
|
-
function
|
|
10586
|
-
const
|
|
10587
|
-
const
|
|
10588
|
-
const
|
|
10589
|
-
if (
|
|
10590
|
-
|
|
10824
|
+
function labelSpanAtAngle(tick, angleDeg, fontSize, fontWeight, measureText) {
|
|
10825
|
+
const angleRad = Math.abs(angleDeg) * Math.PI / 180;
|
|
10826
|
+
const cosA = angleRad > 0 ? Math.abs(Math.cos(angleRad)) : 1;
|
|
10827
|
+
const projected = measureLabel(tick.label, fontSize, fontWeight, measureText) * cosA;
|
|
10828
|
+
if (angleDeg === 0) {
|
|
10829
|
+
return { left: tick.position - projected / 2, right: tick.position + projected / 2 };
|
|
10591
10830
|
}
|
|
10592
|
-
|
|
10593
|
-
|
|
10594
|
-
}
|
|
10595
|
-
function buildLinearScale(channel, data, rangeStart, rangeEnd) {
|
|
10596
|
-
const values = parseNumbers(fieldValues(data, channel.field));
|
|
10597
|
-
let domainMin;
|
|
10598
|
-
let domainMax;
|
|
10599
|
-
if (channel.scale?.domain) {
|
|
10600
|
-
const [d0, d1] = channel.scale.domain;
|
|
10601
|
-
domainMin = d0;
|
|
10602
|
-
domainMax = d1;
|
|
10603
|
-
} else {
|
|
10604
|
-
domainMin = min2(values) ?? 0;
|
|
10605
|
-
domainMax = max2(values) ?? 1;
|
|
10606
|
-
if (channel.scale?.zero !== false) {
|
|
10607
|
-
domainMin = Math.min(0, domainMin);
|
|
10608
|
-
domainMax = Math.max(0, domainMax);
|
|
10609
|
-
}
|
|
10831
|
+
if (angleDeg < 0) {
|
|
10832
|
+
return { left: tick.position - projected, right: tick.position };
|
|
10610
10833
|
}
|
|
10611
|
-
|
|
10612
|
-
|
|
10613
|
-
|
|
10614
|
-
|
|
10615
|
-
|
|
10616
|
-
|
|
10617
|
-
|
|
10618
|
-
|
|
10834
|
+
return { left: tick.position, right: tick.position + projected };
|
|
10835
|
+
}
|
|
10836
|
+
function thinBandTicksIfNeeded(ticks2, angleDeg, fontSize, fontWeight, measureText) {
|
|
10837
|
+
if (ticks2.length < 3) return ticks2;
|
|
10838
|
+
const minGap = fontSize * 0.5;
|
|
10839
|
+
const spans = ticks2.map((t) => labelSpanAtAngle(t, angleDeg, fontSize, fontWeight, measureText));
|
|
10840
|
+
const keep = new Array(ticks2.length).fill(false);
|
|
10841
|
+
keep[0] = true;
|
|
10842
|
+
let lastRight = spans[0].right;
|
|
10843
|
+
for (let i = 1; i < ticks2.length; i++) {
|
|
10844
|
+
if (spans[i].left >= lastRight + minGap) {
|
|
10845
|
+
keep[i] = true;
|
|
10846
|
+
lastRight = spans[i].right;
|
|
10847
|
+
}
|
|
10848
|
+
}
|
|
10849
|
+
const lastIdx = ticks2.length - 1;
|
|
10850
|
+
if (!keep[lastIdx]) {
|
|
10851
|
+
keep[lastIdx] = true;
|
|
10852
|
+
for (let i = lastIdx - 1; i > 0; i--) {
|
|
10853
|
+
if (!keep[i]) continue;
|
|
10854
|
+
if (spans[i].right + minGap > spans[lastIdx].left) {
|
|
10855
|
+
keep[i] = false;
|
|
10856
|
+
continue;
|
|
10619
10857
|
}
|
|
10858
|
+
break;
|
|
10620
10859
|
}
|
|
10621
10860
|
}
|
|
10622
|
-
|
|
10623
|
-
return { scale, type: "linear", channel };
|
|
10624
|
-
}
|
|
10625
|
-
function buildLogScale(channel, data, rangeStart, rangeEnd) {
|
|
10626
|
-
const values = parseNumbers(fieldValues(data, channel.field)).filter((v) => v > 0);
|
|
10627
|
-
const domainMin = channel.scale?.domain ? channel.scale.domain[0] : min2(values) ?? 1;
|
|
10628
|
-
const domainMax = channel.scale?.domain ? channel.scale.domain[1] : max2(values) ?? 10;
|
|
10629
|
-
const scale = log().domain([domainMin, domainMax]).range([rangeStart, rangeEnd]);
|
|
10630
|
-
if (channel.scale?.base !== void 0) {
|
|
10631
|
-
scale.base(channel.scale.base);
|
|
10632
|
-
}
|
|
10633
|
-
if (!channel.scale?.domain && channel.scale?.nice !== false) {
|
|
10634
|
-
scale.nice();
|
|
10635
|
-
}
|
|
10636
|
-
applyContinuousConfig(scale, channel);
|
|
10637
|
-
return { scale, type: "log", channel };
|
|
10861
|
+
return ticks2.filter((_, i) => keep[i]);
|
|
10638
10862
|
}
|
|
10639
|
-
function
|
|
10640
|
-
const
|
|
10641
|
-
|
|
10642
|
-
|
|
10643
|
-
|
|
10644
|
-
|
|
10645
|
-
|
|
10646
|
-
|
|
10647
|
-
|
|
10648
|
-
|
|
10649
|
-
|
|
10650
|
-
|
|
10863
|
+
function computeAxes(scales, chartArea, strategy, theme, measureText, dataContext) {
|
|
10864
|
+
const result = {};
|
|
10865
|
+
const baseDensity = strategy.axisLabelDensity;
|
|
10866
|
+
const yDensity = effectiveDensity(
|
|
10867
|
+
baseDensity,
|
|
10868
|
+
chartArea.height,
|
|
10869
|
+
HEIGHT_MINIMAL_THRESHOLD,
|
|
10870
|
+
HEIGHT_REDUCED_THRESHOLD
|
|
10871
|
+
);
|
|
10872
|
+
const xDensity = effectiveDensity(
|
|
10873
|
+
baseDensity,
|
|
10874
|
+
chartArea.width,
|
|
10875
|
+
WIDTH_MINIMAL_THRESHOLD,
|
|
10876
|
+
WIDTH_REDUCED_THRESHOLD
|
|
10877
|
+
);
|
|
10878
|
+
const tickLabelStyle = {
|
|
10879
|
+
fontFamily: theme.fonts.family,
|
|
10880
|
+
fontSize: theme.fonts.sizes.axisTick,
|
|
10881
|
+
fontWeight: theme.fonts.weights.normal,
|
|
10882
|
+
fill: theme.colors.axis,
|
|
10883
|
+
lineHeight: 1.2,
|
|
10884
|
+
fontVariant: "tabular-nums"
|
|
10885
|
+
};
|
|
10886
|
+
const axisLabelStyle = {
|
|
10887
|
+
fontFamily: theme.fonts.family,
|
|
10888
|
+
fontSize: theme.fonts.sizes.body,
|
|
10889
|
+
fontWeight: theme.fonts.weights.medium,
|
|
10890
|
+
fill: theme.colors.text,
|
|
10891
|
+
lineHeight: 1.3
|
|
10892
|
+
};
|
|
10893
|
+
const { fontSize } = tickLabelStyle;
|
|
10894
|
+
const { fontWeight } = tickLabelStyle;
|
|
10895
|
+
if (scales.x && !dataContext?.skipX && scales.x.channel.axis !== false) {
|
|
10896
|
+
const axisConfig = scales.x.channel.axis;
|
|
10897
|
+
const isContinuousX = scales.x.type !== "band" && scales.x.type !== "point" && scales.x.type !== "ordinal";
|
|
10898
|
+
const xTargetCount = isContinuousX ? targetTickCount(chartArea.width, xDensity, "x") : void 0;
|
|
10899
|
+
let allTicks;
|
|
10900
|
+
if (axisConfig?.values) {
|
|
10901
|
+
allTicks = resolveExplicitTicks(axisConfig.values, scales.x);
|
|
10902
|
+
} else if (!isContinuousX) {
|
|
10903
|
+
allTicks = categoricalTicks(scales.x, xDensity, "horizontal");
|
|
10904
|
+
} else {
|
|
10905
|
+
allTicks = continuousTicks(scales.x, xDensity, xTargetCount);
|
|
10651
10906
|
}
|
|
10652
|
-
|
|
10653
|
-
|
|
10654
|
-
|
|
10655
|
-
|
|
10656
|
-
|
|
10657
|
-
|
|
10658
|
-
|
|
10659
|
-
|
|
10660
|
-
|
|
10661
|
-
|
|
10662
|
-
|
|
10663
|
-
|
|
10664
|
-
|
|
10665
|
-
|
|
10666
|
-
|
|
10667
|
-
|
|
10668
|
-
|
|
10669
|
-
|
|
10670
|
-
|
|
10671
|
-
|
|
10672
|
-
|
|
10673
|
-
|
|
10674
|
-
|
|
10907
|
+
const gridlines = allTicks.map((t) => ({
|
|
10908
|
+
position: t.position,
|
|
10909
|
+
major: true
|
|
10910
|
+
}));
|
|
10911
|
+
let tickAngle = axisConfig?.labelAngle;
|
|
10912
|
+
if (tickAngle === void 0 && scales.x.type === "band" && allTicks.length > 1) {
|
|
10913
|
+
const bandwidth = scales.x.scale.bandwidth();
|
|
10914
|
+
let maxLabelWidth = 0;
|
|
10915
|
+
for (const t of allTicks) {
|
|
10916
|
+
const w = measureLabel(t.label, fontSize, fontWeight, measureText);
|
|
10917
|
+
if (w > maxLabelWidth) maxLabelWidth = w;
|
|
10918
|
+
}
|
|
10919
|
+
tickAngle = resolveBandTickAngle(void 0, maxLabelWidth, bandwidth, allTicks.length);
|
|
10920
|
+
}
|
|
10921
|
+
const hasExplicitValues = !!axisConfig?.values;
|
|
10922
|
+
let ticks2;
|
|
10923
|
+
if (hasExplicitValues) {
|
|
10924
|
+
ticks2 = allTicks;
|
|
10925
|
+
} else if (scales.x.type === "band") {
|
|
10926
|
+
const effectiveAngle = tickAngle ?? 0;
|
|
10927
|
+
ticks2 = thinBandTicksIfNeeded(allTicks, effectiveAngle, fontSize, fontWeight, measureText);
|
|
10928
|
+
} else if (isContinuousX) {
|
|
10929
|
+
ticks2 = fitContinuousTicks(
|
|
10930
|
+
scales.x,
|
|
10931
|
+
allTicks,
|
|
10932
|
+
xTargetCount ?? allTicks.length,
|
|
10933
|
+
fontSize,
|
|
10934
|
+
fontWeight,
|
|
10935
|
+
chartArea.width,
|
|
10936
|
+
"horizontal",
|
|
10937
|
+
measureText
|
|
10938
|
+
);
|
|
10939
|
+
} else {
|
|
10940
|
+
ticks2 = thinTicksUntilFit(allTicks, fontSize, fontWeight, measureText);
|
|
10675
10941
|
}
|
|
10942
|
+
const axisTitle = axisConfig?.title;
|
|
10943
|
+
const xLabelColor = axisConfig?.labelColor;
|
|
10944
|
+
const xTickPosition = axisConfig?.tickPosition ?? "gutter";
|
|
10945
|
+
const xDomainLine = axisConfig?.domain ?? (GROUNDED_MARKS.has(dataContext?.markType ?? "") ? void 0 : false);
|
|
10946
|
+
result.x = {
|
|
10947
|
+
ticks: ticks2,
|
|
10948
|
+
gridlines: axisConfig?.grid ?? dataContext?.markType === "point" ? gridlines : [],
|
|
10949
|
+
label: axisTitle,
|
|
10950
|
+
labelStyle: xLabelColor ? { ...axisLabelStyle, fill: xLabelColor } : axisLabelStyle,
|
|
10951
|
+
tickLabelStyle: xLabelColor ? { ...tickLabelStyle, fill: xLabelColor } : tickLabelStyle,
|
|
10952
|
+
tickAngle,
|
|
10953
|
+
start: { x: chartArea.x, y: chartArea.y + chartArea.height },
|
|
10954
|
+
end: { x: chartArea.x + chartArea.width, y: chartArea.y + chartArea.height },
|
|
10955
|
+
orient: axisConfig?.orient,
|
|
10956
|
+
domainLine: xDomainLine,
|
|
10957
|
+
tickMarks: axisConfig?.ticks,
|
|
10958
|
+
offset: axisConfig?.offset,
|
|
10959
|
+
titlePadding: axisConfig?.titlePadding,
|
|
10960
|
+
labelPadding: axisConfig?.labelPadding,
|
|
10961
|
+
labelOverlap: axisConfig?.labelOverlap,
|
|
10962
|
+
labelFlush: axisConfig?.labelFlush,
|
|
10963
|
+
tickPosition: xTickPosition
|
|
10964
|
+
};
|
|
10676
10965
|
}
|
|
10677
|
-
|
|
10678
|
-
|
|
10679
|
-
|
|
10966
|
+
if (scales.y && !dataContext?.skipY && scales.y.channel.axis !== false) {
|
|
10967
|
+
const axisConfig = scales.y.channel.axis;
|
|
10968
|
+
const isContinuousY = scales.y.type !== "band" && scales.y.type !== "point" && scales.y.type !== "ordinal";
|
|
10969
|
+
const yTargetCount = isContinuousY ? targetTickCount(chartArea.height, yDensity, "y") : void 0;
|
|
10970
|
+
let allTicks;
|
|
10971
|
+
if (axisConfig?.values) {
|
|
10972
|
+
allTicks = resolveExplicitTicks(axisConfig.values, scales.y);
|
|
10973
|
+
} else if (dataContext?.precomputedYTicks && dataContext.precomputedYTicks.length > 0 && isContinuousY) {
|
|
10974
|
+
allTicks = resolveExplicitTicks(dataContext.precomputedYTicks, scales.y);
|
|
10975
|
+
} else if (!isContinuousY) {
|
|
10976
|
+
const yFieldName = dataContext?.encoding.y?.field;
|
|
10977
|
+
const yLabelField = axisConfig?.labelField;
|
|
10978
|
+
allTicks = categoricalTicks(
|
|
10979
|
+
scales.y,
|
|
10980
|
+
yDensity,
|
|
10981
|
+
"vertical",
|
|
10982
|
+
yFieldName && yLabelField && dataContext ? { data: dataContext.data, fieldName: yFieldName, labelField: yLabelField } : void 0
|
|
10983
|
+
);
|
|
10984
|
+
} else {
|
|
10985
|
+
allTicks = continuousTicks(scales.y, yDensity, yTargetCount);
|
|
10986
|
+
}
|
|
10987
|
+
const shouldThinY = scales.y.type !== "band" && !axisConfig?.values && !dataContext?.precomputedYTicks?.length;
|
|
10988
|
+
let ticks2;
|
|
10989
|
+
if (!shouldThinY) {
|
|
10990
|
+
ticks2 = allTicks;
|
|
10991
|
+
} else if (isContinuousY) {
|
|
10992
|
+
ticks2 = fitContinuousTicks(
|
|
10993
|
+
scales.y,
|
|
10994
|
+
allTicks,
|
|
10995
|
+
yTargetCount ?? allTicks.length,
|
|
10996
|
+
fontSize,
|
|
10997
|
+
fontWeight,
|
|
10998
|
+
chartArea.height,
|
|
10999
|
+
"vertical",
|
|
11000
|
+
measureText
|
|
11001
|
+
);
|
|
11002
|
+
} else {
|
|
11003
|
+
ticks2 = thinTicksUntilFit(allTicks, fontSize, fontWeight, measureText, "vertical");
|
|
11004
|
+
}
|
|
11005
|
+
const gridlines = ticks2.map((t) => ({
|
|
11006
|
+
position: t.position,
|
|
11007
|
+
major: true
|
|
11008
|
+
}));
|
|
11009
|
+
const axisTitle = axisConfig?.title;
|
|
11010
|
+
const tickAngle = axisConfig?.labelAngle;
|
|
11011
|
+
const yLabelColor = axisConfig?.labelColor;
|
|
11012
|
+
const isContinuousYAxis = scales.y.type !== "band" && scales.y.type !== "point" && scales.y.type !== "ordinal";
|
|
11013
|
+
const yTickPosition = yTickPositionIsInline(
|
|
11014
|
+
dataContext?.encoding.y,
|
|
11015
|
+
dataContext?.markType ?? "",
|
|
11016
|
+
isContinuousYAxis
|
|
11017
|
+
) ? "inline" : "gutter";
|
|
11018
|
+
const yDomainLine = axisConfig?.domain ?? (yTickPosition === "inline" ? false : void 0);
|
|
11019
|
+
const yTickMarks = axisConfig?.ticks ?? (yTickPosition === "inline" ? false : void 0);
|
|
11020
|
+
result.y = {
|
|
11021
|
+
ticks: ticks2,
|
|
11022
|
+
// Y-axis gridlines are shown by default (standard editorial practice)
|
|
11023
|
+
gridlines,
|
|
11024
|
+
label: axisTitle,
|
|
11025
|
+
labelStyle: yLabelColor ? { ...axisLabelStyle, fill: yLabelColor } : axisLabelStyle,
|
|
11026
|
+
tickLabelStyle: yLabelColor ? { ...tickLabelStyle, fill: yLabelColor } : tickLabelStyle,
|
|
11027
|
+
tickAngle,
|
|
11028
|
+
start: { x: chartArea.x, y: chartArea.y },
|
|
11029
|
+
end: { x: chartArea.x, y: chartArea.y + chartArea.height },
|
|
11030
|
+
orient: axisConfig?.orient,
|
|
11031
|
+
domainLine: yDomainLine,
|
|
11032
|
+
tickMarks: yTickMarks,
|
|
11033
|
+
offset: axisConfig?.offset,
|
|
11034
|
+
titlePadding: axisConfig?.titlePadding,
|
|
11035
|
+
labelPadding: axisConfig?.labelPadding,
|
|
11036
|
+
labelOverlap: axisConfig?.labelOverlap,
|
|
11037
|
+
labelFlush: axisConfig?.labelFlush,
|
|
11038
|
+
tickPosition: yTickPosition
|
|
11039
|
+
};
|
|
10680
11040
|
}
|
|
10681
|
-
|
|
10682
|
-
|
|
10683
|
-
|
|
10684
|
-
|
|
10685
|
-
|
|
10686
|
-
|
|
10687
|
-
|
|
10688
|
-
|
|
10689
|
-
|
|
10690
|
-
|
|
10691
|
-
|
|
10692
|
-
|
|
10693
|
-
if (
|
|
10694
|
-
|
|
10695
|
-
|
|
11041
|
+
const totalWidth = dataContext?.totalWidth ?? chartArea.x + chartArea.width;
|
|
11042
|
+
if (result.x) {
|
|
11043
|
+
const isRotated = !!result.x.tickAngle && Math.abs(result.x.tickAngle) > 10;
|
|
11044
|
+
result.x.extent = computeXAxisExtentFromLabels({
|
|
11045
|
+
labels: result.x.ticks.map((t) => t.label),
|
|
11046
|
+
tickAngle: result.x.tickAngle,
|
|
11047
|
+
hasTitle: !!result.x.label,
|
|
11048
|
+
tickFontSize: result.x.tickLabelStyle.fontSize,
|
|
11049
|
+
tickFontWeight: result.x.tickLabelStyle.fontWeight,
|
|
11050
|
+
xAxisHeight: theme.spacing.xAxisHeight,
|
|
11051
|
+
measure: (t, fs, fw) => measureLabel(t, fs, fw, measureText)
|
|
11052
|
+
});
|
|
11053
|
+
if (result.x.label) {
|
|
11054
|
+
const rotatedBand = isRotated ? result.x.extent - X_AXIS_TITLE_BAND_ROTATED + 14 : 35;
|
|
11055
|
+
result.x.titlePosition = {
|
|
11056
|
+
x: chartArea.x + chartArea.width / 2,
|
|
11057
|
+
y: chartArea.y + chartArea.height + rotatedBand
|
|
11058
|
+
};
|
|
10696
11059
|
}
|
|
10697
11060
|
}
|
|
10698
|
-
|
|
10699
|
-
|
|
11061
|
+
if (result.y?.label && result.y.labelStyle) {
|
|
11062
|
+
const isRight = result.y.orient === "right";
|
|
11063
|
+
if (isRight) {
|
|
11064
|
+
const titleOff = getAxisTitleOffset(totalWidth);
|
|
11065
|
+
result.y.titlePosition = {
|
|
11066
|
+
x: chartArea.x + chartArea.width + titleOff,
|
|
11067
|
+
y: chartArea.y + chartArea.height / 2,
|
|
11068
|
+
angle: 90
|
|
11069
|
+
};
|
|
11070
|
+
} else {
|
|
11071
|
+
const maxTickLabelWidth = result.y.ticks.reduce((max4, t) => {
|
|
11072
|
+
const w = estimateTextWidth13(
|
|
11073
|
+
t.label,
|
|
11074
|
+
result.y.tickLabelStyle.fontSize,
|
|
11075
|
+
result.y.tickLabelStyle.fontWeight ?? 400
|
|
11076
|
+
);
|
|
11077
|
+
return Math.max(max4, w);
|
|
11078
|
+
}, 0);
|
|
11079
|
+
const titleOff = axisTitleOffset(maxTickLabelWidth, result.y.labelStyle.fontSize, totalWidth);
|
|
11080
|
+
result.y.titlePosition = {
|
|
11081
|
+
x: chartArea.x - titleOff,
|
|
11082
|
+
y: chartArea.y + chartArea.height / 2,
|
|
11083
|
+
angle: -90
|
|
11084
|
+
};
|
|
11085
|
+
}
|
|
11086
|
+
}
|
|
11087
|
+
return result;
|
|
11088
|
+
}
|
|
11089
|
+
|
|
11090
|
+
// src/layout/scales.ts
|
|
11091
|
+
function fieldValues(data, field) {
|
|
11092
|
+
return data.map((d) => d[field]).filter((v) => v != null);
|
|
11093
|
+
}
|
|
11094
|
+
function parseDates(values) {
|
|
11095
|
+
return values.map((v) => v instanceof Date ? v : new Date(String(v))).filter((d) => !Number.isNaN(d.getTime()));
|
|
11096
|
+
}
|
|
11097
|
+
function parseNumbers(values) {
|
|
11098
|
+
return values.map((v) => typeof v === "number" ? v : Number(v)).filter((n) => Number.isFinite(n));
|
|
11099
|
+
}
|
|
11100
|
+
function uniqueStrings(values) {
|
|
11101
|
+
const seen = /* @__PURE__ */ new Set();
|
|
11102
|
+
const result = [];
|
|
11103
|
+
for (const v of values) {
|
|
11104
|
+
const s = String(v);
|
|
11105
|
+
if (!seen.has(s)) {
|
|
11106
|
+
seen.add(s);
|
|
11107
|
+
result.push(s);
|
|
11108
|
+
}
|
|
11109
|
+
}
|
|
11110
|
+
return result;
|
|
11111
|
+
}
|
|
11112
|
+
function applyCategoricalSort(values, sort) {
|
|
11113
|
+
if (!sort) return values;
|
|
11114
|
+
const sorted = [...values].sort((a, b) => a.localeCompare(b, void 0, { numeric: true }));
|
|
11115
|
+
if (sort === "descending") sorted.reverse();
|
|
11116
|
+
return sorted;
|
|
11117
|
+
}
|
|
11118
|
+
function applyContinuousConfig(scale, channel) {
|
|
11119
|
+
if (channel.scale?.clamp) {
|
|
11120
|
+
scale.clamp(true);
|
|
11121
|
+
}
|
|
11122
|
+
if (channel.scale?.reverse) {
|
|
11123
|
+
const [r0, r1] = scale.range();
|
|
11124
|
+
scale.range([r1, r0]);
|
|
11125
|
+
}
|
|
11126
|
+
}
|
|
11127
|
+
function buildTimeScale(channel, data, rangeStart, rangeEnd) {
|
|
11128
|
+
const values = parseDates(fieldValues(data, channel.field));
|
|
11129
|
+
const domain = channel.scale?.domain ? [new Date(channel.scale.domain[0]), new Date(channel.scale.domain[1])] : extent(values);
|
|
11130
|
+
const scale = time().domain(domain).range([rangeStart, rangeEnd]);
|
|
11131
|
+
if (!channel.scale?.domain && channel.scale?.nice === true) {
|
|
11132
|
+
scale.nice();
|
|
11133
|
+
}
|
|
11134
|
+
applyContinuousConfig(scale, channel);
|
|
11135
|
+
return { scale, type: "time", channel };
|
|
11136
|
+
}
|
|
11137
|
+
function buildUtcScale(channel, data, rangeStart, rangeEnd) {
|
|
11138
|
+
const values = parseDates(fieldValues(data, channel.field));
|
|
11139
|
+
const domain = channel.scale?.domain ? [new Date(channel.scale.domain[0]), new Date(channel.scale.domain[1])] : extent(values);
|
|
11140
|
+
const scale = utcTime().domain(domain).range([rangeStart, rangeEnd]);
|
|
11141
|
+
if (!channel.scale?.domain && channel.scale?.nice === true) {
|
|
11142
|
+
scale.nice();
|
|
11143
|
+
}
|
|
11144
|
+
applyContinuousConfig(scale, channel);
|
|
11145
|
+
return { scale, type: "utc", channel };
|
|
11146
|
+
}
|
|
11147
|
+
function buildLinearScale(channel, data, rangeStart, rangeEnd) {
|
|
11148
|
+
const values = parseNumbers(fieldValues(data, channel.field));
|
|
11149
|
+
let domainMin;
|
|
11150
|
+
let domainMax;
|
|
11151
|
+
if (channel.scale?.domain) {
|
|
11152
|
+
const [d0, d1] = channel.scale.domain;
|
|
11153
|
+
domainMin = d0;
|
|
11154
|
+
domainMax = d1;
|
|
11155
|
+
} else {
|
|
11156
|
+
domainMin = min2(values) ?? 0;
|
|
11157
|
+
domainMax = max2(values) ?? 1;
|
|
11158
|
+
if (channel.scale?.zero !== false) {
|
|
11159
|
+
domainMin = Math.min(0, domainMin);
|
|
11160
|
+
domainMax = Math.max(0, domainMax);
|
|
11161
|
+
}
|
|
11162
|
+
}
|
|
11163
|
+
const scale = linear2().domain([domainMin, domainMax]).range([rangeStart, rangeEnd]);
|
|
11164
|
+
if (!channel.scale?.domain && channel.scale?.nice !== false) {
|
|
11165
|
+
scale.nice();
|
|
11166
|
+
if (channel.scale?.zero === false) {
|
|
11167
|
+
const [nicedMin, nicedMax] = scale.domain();
|
|
11168
|
+
if (nicedMin < domainMin || nicedMax > domainMax) {
|
|
11169
|
+
scale.domain([domainMin, domainMax]);
|
|
11170
|
+
scale.nice(20);
|
|
11171
|
+
}
|
|
11172
|
+
}
|
|
11173
|
+
}
|
|
11174
|
+
applyContinuousConfig(scale, channel);
|
|
11175
|
+
return { scale, type: "linear", channel };
|
|
11176
|
+
}
|
|
11177
|
+
function buildLogScale(channel, data, rangeStart, rangeEnd) {
|
|
11178
|
+
const values = parseNumbers(fieldValues(data, channel.field)).filter((v) => v > 0);
|
|
11179
|
+
const domainMin = channel.scale?.domain ? channel.scale.domain[0] : min2(values) ?? 1;
|
|
11180
|
+
const domainMax = channel.scale?.domain ? channel.scale.domain[1] : max2(values) ?? 10;
|
|
11181
|
+
const scale = log().domain([domainMin, domainMax]).range([rangeStart, rangeEnd]);
|
|
11182
|
+
if (channel.scale?.base !== void 0) {
|
|
11183
|
+
scale.base(channel.scale.base);
|
|
11184
|
+
}
|
|
11185
|
+
if (!channel.scale?.domain && channel.scale?.nice !== false) {
|
|
11186
|
+
scale.nice();
|
|
11187
|
+
}
|
|
11188
|
+
applyContinuousConfig(scale, channel);
|
|
11189
|
+
return { scale, type: "log", channel };
|
|
11190
|
+
}
|
|
11191
|
+
function buildPowScale(channel, data, rangeStart, rangeEnd) {
|
|
11192
|
+
const values = parseNumbers(fieldValues(data, channel.field));
|
|
11193
|
+
let domainMin;
|
|
11194
|
+
let domainMax;
|
|
11195
|
+
if (channel.scale?.domain) {
|
|
11196
|
+
[domainMin, domainMax] = channel.scale.domain;
|
|
11197
|
+
} else {
|
|
11198
|
+
domainMin = min2(values) ?? 0;
|
|
11199
|
+
domainMax = max2(values) ?? 1;
|
|
11200
|
+
if (channel.scale?.zero !== false) {
|
|
11201
|
+
domainMin = Math.min(0, domainMin);
|
|
11202
|
+
domainMax = Math.max(0, domainMax);
|
|
11203
|
+
}
|
|
11204
|
+
}
|
|
11205
|
+
const scale = pow().domain([domainMin, domainMax]).range([rangeStart, rangeEnd]);
|
|
11206
|
+
if (channel.scale?.exponent !== void 0) {
|
|
11207
|
+
scale.exponent(channel.scale.exponent);
|
|
11208
|
+
}
|
|
11209
|
+
if (!channel.scale?.domain && channel.scale?.nice !== false) {
|
|
11210
|
+
scale.nice();
|
|
11211
|
+
}
|
|
11212
|
+
applyContinuousConfig(scale, channel);
|
|
11213
|
+
return { scale, type: "pow", channel };
|
|
11214
|
+
}
|
|
11215
|
+
function buildSqrtScale(channel, data, rangeStart, rangeEnd) {
|
|
11216
|
+
const values = parseNumbers(fieldValues(data, channel.field));
|
|
11217
|
+
let domainMin;
|
|
11218
|
+
let domainMax;
|
|
11219
|
+
if (channel.scale?.domain) {
|
|
11220
|
+
[domainMin, domainMax] = channel.scale.domain;
|
|
11221
|
+
} else {
|
|
11222
|
+
domainMin = min2(values) ?? 0;
|
|
11223
|
+
domainMax = max2(values) ?? 1;
|
|
11224
|
+
if (channel.scale?.zero !== false) {
|
|
11225
|
+
domainMin = Math.min(0, domainMin);
|
|
11226
|
+
domainMax = Math.max(0, domainMax);
|
|
11227
|
+
}
|
|
11228
|
+
}
|
|
11229
|
+
const scale = sqrt2().domain([domainMin, domainMax]).range([rangeStart, rangeEnd]);
|
|
11230
|
+
if (!channel.scale?.domain && channel.scale?.nice !== false) {
|
|
11231
|
+
scale.nice();
|
|
11232
|
+
}
|
|
11233
|
+
applyContinuousConfig(scale, channel);
|
|
11234
|
+
return { scale, type: "sqrt", channel };
|
|
11235
|
+
}
|
|
11236
|
+
function buildSymlogScale(channel, data, rangeStart, rangeEnd) {
|
|
11237
|
+
const values = parseNumbers(fieldValues(data, channel.field));
|
|
11238
|
+
let domainMin;
|
|
11239
|
+
let domainMax;
|
|
11240
|
+
if (channel.scale?.domain) {
|
|
11241
|
+
[domainMin, domainMax] = channel.scale.domain;
|
|
11242
|
+
} else {
|
|
11243
|
+
domainMin = min2(values) ?? 0;
|
|
11244
|
+
domainMax = max2(values) ?? 1;
|
|
11245
|
+
if (channel.scale?.zero !== false) {
|
|
11246
|
+
domainMin = Math.min(0, domainMin);
|
|
11247
|
+
domainMax = Math.max(0, domainMax);
|
|
11248
|
+
}
|
|
11249
|
+
}
|
|
11250
|
+
const scale = symlog().domain([domainMin, domainMax]).range([rangeStart, rangeEnd]);
|
|
11251
|
+
if (channel.scale?.constant !== void 0) {
|
|
10700
11252
|
scale.constant(channel.scale.constant);
|
|
10701
11253
|
}
|
|
10702
11254
|
if (!channel.scale?.domain && channel.scale?.nice !== false) {
|
|
@@ -11032,7 +11584,7 @@ function createMeasureFn(measureText) {
|
|
|
11032
11584
|
if (measureText) {
|
|
11033
11585
|
return (text, fontSize, fontWeight) => measureText(text, fontSize, fontWeight).width;
|
|
11034
11586
|
}
|
|
11035
|
-
return
|
|
11587
|
+
return estimateTextWidth14;
|
|
11036
11588
|
}
|
|
11037
11589
|
function sampleLabelFromMagnitude(maxAbsVal) {
|
|
11038
11590
|
return maxAbsVal >= 1e3 ? abbreviateNumber3(maxAbsVal) : formatNumber4(maxAbsVal);
|
|
@@ -11081,11 +11633,8 @@ function resolveLayoutPlan(chartSpec, renderSpec, options, theme, strategy, wate
|
|
|
11081
11633
|
};
|
|
11082
11634
|
}
|
|
11083
11635
|
const yAxisCfg = encoding.y?.axis ?? void 0;
|
|
11084
|
-
const yTickPositionExplicit = yAxisCfg?.tickPosition;
|
|
11085
11636
|
const yIsContinuous = encoding.y?.type === "quantitative" || encoding.y?.type === "temporal";
|
|
11086
|
-
const
|
|
11087
|
-
const yAxisOrient = yAxisCfg?.orient;
|
|
11088
|
-
const yIsInline = yTickPositionExplicit === "inline" || yTickPositionExplicit === void 0 && yIsLineOrArea && yIsContinuous && yAxisOrient !== "right";
|
|
11637
|
+
const yIsInline = yTickPositionIsInline(encoding.y, renderSpec.markType);
|
|
11089
11638
|
const yAxisSuppressed = encoding.y?.axis === false;
|
|
11090
11639
|
let leftGutter = hPad + axisMargin;
|
|
11091
11640
|
if (encoding.y && !isRadial && !yAxisSuppressed && !yIsInline) {
|
|
@@ -11212,7 +11761,7 @@ function resolveLayoutPlan(chartSpec, renderSpec, options, theme, strategy, wate
|
|
|
11212
11761
|
xLabels.length
|
|
11213
11762
|
);
|
|
11214
11763
|
}
|
|
11215
|
-
const xAxisExtent = isRadial || xAxisSuppressed ? 0 :
|
|
11764
|
+
const xAxisExtent = isRadial || xAxisSuppressed ? 0 : computeXAxisExtentFromLabels2({
|
|
11216
11765
|
labels: xLabels,
|
|
11217
11766
|
tickAngle: effectiveXTickAngle,
|
|
11218
11767
|
hasTitle: hasXAxisTitle,
|
|
@@ -11336,1270 +11885,904 @@ function resolveLayoutPlan(chartSpec, renderSpec, options, theme, strategy, wate
|
|
|
11336
11885
|
const yAxisDef = encoding.y?.axis;
|
|
11337
11886
|
if (yAxisDef && (yAxisDef.title || yAxisDef.label) && !isRadial) {
|
|
11338
11887
|
let estTickLabelWidth = 0;
|
|
11339
|
-
if (encoding.y?.field && yIsContinuous) {
|
|
11888
|
+
if (encoding.y?.field && yIsContinuous && !yIsInline) {
|
|
11340
11889
|
if (yTickValues.length > 0) {
|
|
11341
11890
|
const titleProvArea = {
|
|
11342
11891
|
x: finalGutter,
|
|
11343
11892
|
y: topMargin,
|
|
11344
11893
|
width: Math.max(0, width - finalGutter - hPad),
|
|
11345
11894
|
height: chartHeight
|
|
11346
|
-
};
|
|
11347
|
-
const scales = computeScales(renderSpec, titleProvArea, renderSpec.data);
|
|
11348
|
-
if (scales.y && scaleSupportsTickCount(scales.y)) {
|
|
11349
|
-
const ticks2 = buildContinuousTicks(scales.y, yTickCount || 5);
|
|
11350
|
-
for (const t of ticks2) {
|
|
11351
|
-
const w = measure(t.label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
|
|
11352
|
-
if (w > estTickLabelWidth) estTickLabelWidth = w;
|
|
11353
|
-
}
|
|
11354
|
-
}
|
|
11355
|
-
}
|
|
11356
|
-
}
|
|
11357
|
-
const titleFontSize = theme.fonts.sizes.body;
|
|
11358
|
-
const offset =
|
|
11359
|
-
const halfGlyph = Math.ceil(titleFontSize / 2);
|
|
11360
|
-
const rotatedLabelMargin = offset + halfGlyph + (width < BREAKPOINT_COMPACT_MAX ? 0 : AXIS_TITLE_TRAILING_PAD);
|
|
11361
|
-
gutterWithTitle = Math.max(finalGutter, hPad + rotatedLabelMargin);
|
|
11362
|
-
}
|
|
11363
|
-
finalPlan = {
|
|
11364
|
-
leftGutter: gutterWithTitle,
|
|
11365
|
-
xAxisExtent,
|
|
11366
|
-
legendContent,
|
|
11367
|
-
chrome,
|
|
11368
|
-
yTickValues,
|
|
11369
|
-
yTickCount,
|
|
11370
|
-
converged: prev === null || converged,
|
|
11371
|
-
inlineYLabelInset,
|
|
11372
|
-
endpointLabelsDemoted
|
|
11373
|
-
};
|
|
11374
|
-
break;
|
|
11375
|
-
}
|
|
11376
|
-
prev = { gutter: newGutter, rowCount: legendContent.rowCount, xAxisExtent };
|
|
11377
|
-
leftGutter = newGutter;
|
|
11378
|
-
}
|
|
11379
|
-
if (!finalPlan) throw new Error("resolveLayoutPlan: loop exited without producing a plan");
|
|
11380
|
-
return finalPlan;
|
|
11381
|
-
}
|
|
11382
|
-
|
|
11383
|
-
// src/compile/layer.ts
|
|
11384
|
-
function compileLayer(spec, options, compileChart2) {
|
|
11385
|
-
const leaves = flattenLayers(spec);
|
|
11386
|
-
if (leaves.length === 0) {
|
|
11387
|
-
throw new Error("LayerSpec has no leaf chart specs after flattening");
|
|
11388
|
-
}
|
|
11389
|
-
if (leaves.length === 1) {
|
|
11390
|
-
const singleSpec = buildPrimarySpec(leaves, spec);
|
|
11391
|
-
return compileChart2(singleSpec, options);
|
|
11392
|
-
}
|
|
11393
|
-
if (spec.resolve?.scale?.y === "independent") {
|
|
11394
|
-
return compileLayerIndependent(leaves, spec, options, compileChart2);
|
|
11395
|
-
}
|
|
11396
|
-
const primarySpec = buildPrimarySpec(leaves, spec);
|
|
11397
|
-
const primaryLayout = compileChart2(primarySpec, options);
|
|
11398
|
-
const allMarks = [];
|
|
11399
|
-
const seenLabels = /* @__PURE__ */ new Set();
|
|
11400
|
-
const pLegend = primaryLayout.legend;
|
|
11401
|
-
const mergedLegendEntries = "entries" in pLegend ? [...pLegend.entries] : [];
|
|
11402
|
-
for (const entry of mergedLegendEntries) {
|
|
11403
|
-
seenLabels.add(entry.label);
|
|
11404
|
-
}
|
|
11405
|
-
const indexedLeaves = leaves.map((leaf, i) => ({
|
|
11406
|
-
leaf,
|
|
11407
|
-
zIndex: leaf.zIndex ?? i
|
|
11408
|
-
}));
|
|
11409
|
-
indexedLeaves.sort((a, b) => a.zIndex - b.zIndex);
|
|
11410
|
-
const leafOptions = { ...options, frozenChartArea: primaryLayout.area };
|
|
11411
|
-
for (const { leaf } of indexedLeaves) {
|
|
11412
|
-
const themedLeaf = {
|
|
11413
|
-
...leaf,
|
|
11414
|
-
theme: leaf.theme ?? spec.theme,
|
|
11415
|
-
darkMode: leaf.darkMode ?? spec.darkMode
|
|
11416
|
-
};
|
|
11417
|
-
const leafLayout = compileChart2(themedLeaf, leafOptions);
|
|
11418
|
-
allMarks.push(...leafLayout.marks);
|
|
11419
|
-
const leafLeg = leafLayout.legend;
|
|
11420
|
-
if ("entries" in leafLeg) {
|
|
11421
|
-
for (const entry of leafLeg.entries) {
|
|
11422
|
-
if (!seenLabels.has(entry.label)) {
|
|
11423
|
-
seenLabels.add(entry.label);
|
|
11424
|
-
mergedLegendEntries.push(entry);
|
|
11425
|
-
}
|
|
11426
|
-
}
|
|
11427
|
-
}
|
|
11428
|
-
}
|
|
11429
|
-
return {
|
|
11430
|
-
...primaryLayout,
|
|
11431
|
-
marks: allMarks,
|
|
11432
|
-
legend: {
|
|
11433
|
-
...primaryLayout.legend,
|
|
11434
|
-
..."entries" in pLegend ? { entries: mergedLegendEntries } : {}
|
|
11435
|
-
}
|
|
11436
|
-
};
|
|
11437
|
-
}
|
|
11438
|
-
function estimateYAxisLabelWidth(data, encoding, baseFontSize, measure) {
|
|
11439
|
-
if (!encoding?.y) return 40;
|
|
11440
|
-
const yEnc = encoding.y;
|
|
11441
|
-
const yField = yEnc.field;
|
|
11442
|
-
if (!yField) return 40;
|
|
11443
|
-
const m = measure ?? estimateTextWidth13;
|
|
11444
|
-
const yType = yEnc.type;
|
|
11445
|
-
if (yType === "nominal" || yType === "ordinal") {
|
|
11446
|
-
let maxWidth = 0;
|
|
11447
|
-
for (const row of data) {
|
|
11448
|
-
const label = String(row[yField] ?? "");
|
|
11449
|
-
const w = m(label, baseFontSize, 400);
|
|
11450
|
-
if (w > maxWidth) maxWidth = w;
|
|
11451
|
-
}
|
|
11452
|
-
return maxWidth > 0 ? maxWidth + 10 : 40;
|
|
11453
|
-
}
|
|
11454
|
-
const yAxisFormat = encoding.y.axis?.format;
|
|
11455
|
-
let maxAbsVal = 0;
|
|
11456
|
-
for (const row of data) {
|
|
11457
|
-
const v = Number(row[yField]);
|
|
11458
|
-
if (Number.isFinite(v) && Math.abs(v) > maxAbsVal) maxAbsVal = Math.abs(v);
|
|
11459
|
-
}
|
|
11460
|
-
let sampleLabel;
|
|
11461
|
-
if (yAxisFormat) {
|
|
11462
|
-
try {
|
|
11463
|
-
const fmt = format(yAxisFormat);
|
|
11464
|
-
sampleLabel = fmt(maxAbsVal);
|
|
11465
|
-
} catch {
|
|
11466
|
-
sampleLabel = String(maxAbsVal);
|
|
11467
|
-
}
|
|
11468
|
-
} else {
|
|
11469
|
-
sampleLabel = Math.abs(maxAbsVal) >= 1e3 ? abbreviateNumber4(maxAbsVal) : formatNumber5(maxAbsVal);
|
|
11470
|
-
}
|
|
11471
|
-
const hasNeg = data.some((r) => Number(r[yField]) < 0);
|
|
11472
|
-
const labelEst = (hasNeg ? "-" : "") + sampleLabel;
|
|
11473
|
-
return m(labelEst, baseFontSize, 400) + 10;
|
|
11474
|
-
}
|
|
11475
|
-
function compileLayerIndependent(leaves, layerSpec, options, compileChart2) {
|
|
11476
|
-
if (leaves.length > 2) {
|
|
11477
|
-
throw new Error(
|
|
11478
|
-
`Independent y-scales support at most 2 layers (left and right y-axis). Got ${leaves.length} layers.`
|
|
11479
|
-
);
|
|
11480
|
-
}
|
|
11481
|
-
const leaf0 = leaves[0];
|
|
11482
|
-
const leaf1 = leaves[1];
|
|
11483
|
-
const xType0 = leaf0.encoding?.x?.type;
|
|
11484
|
-
const xType1 = leaf1.encoding?.x?.type;
|
|
11485
|
-
if (xType0 && xType1 && xType0 !== xType1) {
|
|
11486
|
-
throw new Error(
|
|
11487
|
-
`Dual-axis charts require matching x-field types across layers. Layer 0 has '${xType0}', layer 1 has '${xType1}'.`
|
|
11488
|
-
);
|
|
11489
|
-
}
|
|
11490
|
-
const theme = resolveTheme2(layerSpec.theme ?? leaf1.theme);
|
|
11491
|
-
const axisFontSize = theme.fonts?.sizes?.axisTick ?? 11;
|
|
11492
|
-
const measureFn = createMeasureFn(options.measureText);
|
|
11493
|
-
const rightAxisWidth = estimateYAxisLabelWidth(
|
|
11494
|
-
leaf1.data,
|
|
11495
|
-
leaf1.encoding,
|
|
11496
|
-
axisFontSize,
|
|
11497
|
-
measureFn
|
|
11498
|
-
);
|
|
11499
|
-
const yAxisConfig = leaf1.encoding?.y?.axis || void 0;
|
|
11500
|
-
const hasRightAxisTitle = !!yAxisConfig?.title;
|
|
11501
|
-
const tickExtent = TICK_LABEL_OFFSET + rightAxisWidth;
|
|
11502
|
-
const bodyFontSize = theme.fonts?.sizes?.body ?? 13;
|
|
11503
|
-
const axisTitleOffset4 = getAxisTitleOffset(options.width);
|
|
11504
|
-
const halfGlyph = Math.ceil(bodyFontSize / 2);
|
|
11505
|
-
const titleExtent = hasRightAxisTitle ? axisTitleOffset4 + halfGlyph + (options.width < BREAKPOINT_COMPACT_MAX2 ? 0 : AXIS_TITLE_TRAILING_PAD2) : 0;
|
|
11506
|
-
const rightReserve = Math.max(tickExtent, titleExtent);
|
|
11507
|
-
const optionsWithReserve = {
|
|
11508
|
-
...options,
|
|
11509
|
-
rightAxisReserve: rightReserve
|
|
11510
|
-
};
|
|
11511
|
-
const xField0 = leaf0.encoding?.x?.field;
|
|
11512
|
-
const xField1 = leaf1.encoding?.x?.field;
|
|
11513
|
-
const unionXValues = /* @__PURE__ */ new Set();
|
|
11514
|
-
if (xField0) for (const row of leaf0.data) unionXValues.add(row[xField0]);
|
|
11515
|
-
if (xField1) for (const row of leaf1.data) unionXValues.add(row[xField1]);
|
|
11516
|
-
let leaf0WithUnionX = ensureXDomainCoverage(leaf0, xField0, unionXValues);
|
|
11517
|
-
let leaf1WithUnionX = ensureXDomainCoverage(leaf1, xField1, unionXValues);
|
|
11518
|
-
const aligned = alignYDomains(leaf0WithUnionX, leaf1WithUnionX);
|
|
11519
|
-
if (aligned) {
|
|
11520
|
-
leaf0WithUnionX = withYDomain(leaf0WithUnionX, aligned.domain0);
|
|
11521
|
-
leaf1WithUnionX = withYDomain(leaf1WithUnionX, aligned.domain1);
|
|
11522
|
-
}
|
|
11523
|
-
const primary0 = buildPrimarySpec([leaf0WithUnionX], layerSpec);
|
|
11524
|
-
const layout0 = compileChart2(primary0, optionsWithReserve);
|
|
11525
|
-
const primary1 = buildPrimarySpec([leaf1WithUnionX], layerSpec);
|
|
11526
|
-
primary1.annotations = [];
|
|
11527
|
-
const layout1 = compileChart2(primary1, optionsWithReserve);
|
|
11528
|
-
const y2Axis = layout1.axes.y ? {
|
|
11529
|
-
...layout1.axes.y,
|
|
11530
|
-
orient: "right",
|
|
11531
|
-
gridlines: [],
|
|
11532
|
-
start: {
|
|
11533
|
-
x: layout0.area.x + layout0.area.width,
|
|
11534
|
-
y: layout0.area.y
|
|
11535
|
-
},
|
|
11536
|
-
end: {
|
|
11537
|
-
x: layout0.area.x + layout0.area.width,
|
|
11538
|
-
y: layout0.area.y + layout0.area.height
|
|
11539
|
-
},
|
|
11540
|
-
...layout1.axes.y.label ? {
|
|
11541
|
-
titlePosition: {
|
|
11542
|
-
x: layout0.area.x + layout0.area.width + getAxisTitleOffset(layout0.dimensions.width),
|
|
11543
|
-
y: layout0.area.y + layout0.area.height / 2,
|
|
11544
|
-
angle: 90
|
|
11545
|
-
}
|
|
11546
|
-
} : {}
|
|
11547
|
-
} : void 0;
|
|
11548
|
-
const layer0HasBars = layout0.marks.some((m) => m.type === "rect");
|
|
11549
|
-
const layer1HasBars = layout1.marks.some((m) => m.type === "rect");
|
|
11550
|
-
const bandCenterByCategory = /* @__PURE__ */ new Map();
|
|
11551
|
-
if (layer0HasBars && layout0.axes.x?.ticks) {
|
|
11552
|
-
for (const tick of layout0.axes.x.ticks) {
|
|
11553
|
-
bandCenterByCategory.set(String(tick.label), tick.position);
|
|
11554
|
-
}
|
|
11555
|
-
} else if (layer1HasBars && layout1.axes.x?.ticks) {
|
|
11556
|
-
for (const tick of layout1.axes.x.ticks) {
|
|
11557
|
-
bandCenterByCategory.set(String(tick.label), tick.position);
|
|
11558
|
-
}
|
|
11559
|
-
}
|
|
11560
|
-
const remapMarkX = (xField, mark) => {
|
|
11561
|
-
if (!xField || bandCenterByCategory.size === 0) return mark;
|
|
11562
|
-
if (mark.type === "line") {
|
|
11563
|
-
const newPoints = mark.points.map((p, i) => {
|
|
11564
|
-
const bx = bandCenterByCategory.get(String(mark.data[i]?.[xField] ?? ""));
|
|
11565
|
-
return bx !== void 0 ? { ...p, x: bx } : p;
|
|
11566
|
-
});
|
|
11567
|
-
const newDataPoints = mark.dataPoints?.map((dp, i) => {
|
|
11568
|
-
const bx = bandCenterByCategory.get(String(mark.data[i]?.[xField] ?? ""));
|
|
11569
|
-
return bx !== void 0 ? { ...dp, x: bx } : dp;
|
|
11570
|
-
});
|
|
11571
|
-
const newPath = line_default().x((p) => p.x).y((p) => p.y).curve(monotoneX)(newPoints) ?? void 0;
|
|
11572
|
-
return { ...mark, points: newPoints, dataPoints: newDataPoints, path: newPath };
|
|
11573
|
-
}
|
|
11574
|
-
if (mark.type === "area") {
|
|
11575
|
-
const newTopPoints = mark.topPoints.map((p, i) => {
|
|
11576
|
-
const bx = bandCenterByCategory.get(String(mark.data[i]?.[xField] ?? ""));
|
|
11577
|
-
return bx !== void 0 ? { ...p, x: bx } : p;
|
|
11578
|
-
});
|
|
11579
|
-
const newBottomPoints = mark.bottomPoints.map((p, i) => {
|
|
11580
|
-
const bx = bandCenterByCategory.get(String(mark.data[i]?.[xField] ?? ""));
|
|
11581
|
-
return bx !== void 0 ? { ...p, x: bx } : p;
|
|
11582
|
-
});
|
|
11583
|
-
const newDataPoints = mark.dataPoints?.map((dp, i) => {
|
|
11584
|
-
const bx = bandCenterByCategory.get(String(mark.data[i]?.[xField] ?? ""));
|
|
11585
|
-
return bx !== void 0 ? { ...dp, x: bx } : dp;
|
|
11586
|
-
});
|
|
11587
|
-
const areaGen = area_default().x((p) => p.x).y0((p) => p.yBottom).y1((p) => p.yTop).curve(monotoneX);
|
|
11588
|
-
const topLineGen = line_default().x((p) => p.x).y((p) => p.yTop).curve(monotoneX);
|
|
11589
|
-
const combined = newTopPoints.map((tp, i) => ({
|
|
11590
|
-
x: tp.x,
|
|
11591
|
-
yTop: tp.y,
|
|
11592
|
-
yBottom: newBottomPoints[i]?.y ?? tp.y
|
|
11593
|
-
}));
|
|
11594
|
-
const newPath = areaGen(combined) ?? "";
|
|
11595
|
-
const newTopPath = topLineGen(combined) ?? "";
|
|
11596
|
-
return {
|
|
11597
|
-
...mark,
|
|
11598
|
-
topPoints: newTopPoints,
|
|
11599
|
-
bottomPoints: newBottomPoints,
|
|
11600
|
-
dataPoints: newDataPoints,
|
|
11601
|
-
path: newPath,
|
|
11602
|
-
topPath: newTopPath
|
|
11603
|
-
};
|
|
11604
|
-
}
|
|
11605
|
-
if (mark.type === "point") {
|
|
11606
|
-
const bx = bandCenterByCategory.get(String(mark.data[xField] ?? ""));
|
|
11607
|
-
return bx !== void 0 ? { ...mark, cx: bx } : mark;
|
|
11608
|
-
}
|
|
11609
|
-
return mark;
|
|
11610
|
-
};
|
|
11611
|
-
const adjustedMarks0 = bandCenterByCategory.size > 0 && !layer0HasBars ? layout0.marks.map((m) => remapMarkX(xField0, m)) : layout0.marks;
|
|
11612
|
-
const taggedMarks1 = layout1.marks.map((mark) => {
|
|
11613
|
-
const tagged = { ...mark, yScale: "y2" };
|
|
11614
|
-
if (bandCenterByCategory.size > 0 && !layer1HasBars) {
|
|
11615
|
-
return remapMarkX(xField1, tagged);
|
|
11616
|
-
}
|
|
11617
|
-
return tagged;
|
|
11618
|
-
});
|
|
11619
|
-
const seenLabels = /* @__PURE__ */ new Set();
|
|
11620
|
-
const l0Legend = layout0.legend;
|
|
11621
|
-
const l1Legend = layout1.legend;
|
|
11622
|
-
const mergedLegendEntries = "entries" in l0Legend ? [...l0Legend.entries] : [];
|
|
11623
|
-
for (const entry of mergedLegendEntries) seenLabels.add(entry.label);
|
|
11624
|
-
const l1Entries = "entries" in l1Legend ? l1Legend.entries : [];
|
|
11625
|
-
for (const entry of l1Entries) {
|
|
11626
|
-
if (!seenLabels.has(entry.label)) {
|
|
11627
|
-
seenLabels.add(entry.label);
|
|
11628
|
-
mergedLegendEntries.push(entry);
|
|
11629
|
-
}
|
|
11630
|
-
}
|
|
11631
|
-
const l0Count = layout0.marks.length;
|
|
11632
|
-
const mergedTooltips = new Map(layout0.tooltipDescriptors);
|
|
11633
|
-
for (const [key, value2] of layout1.tooltipDescriptors) {
|
|
11634
|
-
const match = /^(rect|point|arc)-(\d+)$/.exec(key);
|
|
11635
|
-
if (match) {
|
|
11636
|
-
const offsetKey = `${match[1]}-${Number(match[2]) + l0Count}`;
|
|
11637
|
-
mergedTooltips.set(offsetKey, value2);
|
|
11638
|
-
} else {
|
|
11639
|
-
mergedTooltips.set(key, value2);
|
|
11640
|
-
}
|
|
11641
|
-
}
|
|
11642
|
-
const z0 = leaf0.zIndex ?? 0;
|
|
11643
|
-
const z1 = leaf1.zIndex ?? 1;
|
|
11644
|
-
const marks = z0 <= z1 ? [...adjustedMarks0, ...taggedMarks1] : [...taggedMarks1, ...adjustedMarks0];
|
|
11645
|
-
return {
|
|
11646
|
-
...layout0,
|
|
11647
|
-
axes: {
|
|
11648
|
-
x: layout0.axes.x,
|
|
11649
|
-
y: layout0.axes.y,
|
|
11650
|
-
y2: y2Axis
|
|
11651
|
-
},
|
|
11652
|
-
marks,
|
|
11653
|
-
legend: {
|
|
11654
|
-
...layout0.legend,
|
|
11655
|
-
..."entries" in l0Legend ? { entries: mergedLegendEntries } : {}
|
|
11656
|
-
},
|
|
11657
|
-
tooltipDescriptors: mergedTooltips
|
|
11658
|
-
};
|
|
11659
|
-
}
|
|
11660
|
-
function ensureXDomainCoverage(leaf, xField, allXValues) {
|
|
11661
|
-
if (!xField || allXValues.size === 0) return leaf;
|
|
11662
|
-
const existingXValues = /* @__PURE__ */ new Set();
|
|
11663
|
-
for (const row of leaf.data) existingXValues.add(row[xField]);
|
|
11664
|
-
const missingRows = [];
|
|
11665
|
-
for (const xVal of allXValues) {
|
|
11666
|
-
if (!existingXValues.has(xVal)) {
|
|
11667
|
-
missingRows.push({ [xField]: xVal });
|
|
11668
|
-
}
|
|
11669
|
-
}
|
|
11670
|
-
if (missingRows.length === 0) return leaf;
|
|
11671
|
-
return {
|
|
11672
|
-
...leaf,
|
|
11673
|
-
data: [...leaf.data, ...missingRows]
|
|
11674
|
-
};
|
|
11675
|
-
}
|
|
11676
|
-
function alignYDomains(leaf0, leaf1) {
|
|
11677
|
-
const yEnc0 = leaf0.encoding?.y;
|
|
11678
|
-
const yEnc1 = leaf1.encoding?.y;
|
|
11679
|
-
if (!yEnc0 || !yEnc1) return void 0;
|
|
11680
|
-
if (yEnc0.type !== "quantitative" || yEnc1.type !== "quantitative") return void 0;
|
|
11681
|
-
if (yEnc0.scale?.domain || yEnc1.scale?.domain) return void 0;
|
|
11682
|
-
const includeZero0 = yEnc0.scale?.zero !== false;
|
|
11683
|
-
const includeZero1 = yEnc1.scale?.zero !== false;
|
|
11684
|
-
const vals0 = leaf0.data.map((r) => Number(r[yEnc0.field])).filter(Number.isFinite);
|
|
11685
|
-
const vals1 = leaf1.data.map((r) => Number(r[yEnc1.field])).filter(Number.isFinite);
|
|
11686
|
-
if (vals0.length === 0 || vals1.length === 0) return void 0;
|
|
11687
|
-
const niced = (vals, includeZero) => {
|
|
11688
|
-
let lo = Math.min(...vals);
|
|
11689
|
-
let hi = Math.max(...vals);
|
|
11690
|
-
if (includeZero) {
|
|
11691
|
-
lo = Math.min(0, lo);
|
|
11692
|
-
hi = Math.max(0, hi);
|
|
11693
|
-
}
|
|
11694
|
-
const s = linear2().domain([lo, hi]);
|
|
11695
|
-
s.nice();
|
|
11696
|
-
const [dLo, dHi] = s.domain();
|
|
11697
|
-
return [dLo, dHi];
|
|
11698
|
-
};
|
|
11699
|
-
const [min0, max0] = niced(vals0, includeZero0);
|
|
11700
|
-
const [min1, max1] = niced(vals1, includeZero1);
|
|
11701
|
-
const span0 = max0 - min0;
|
|
11702
|
-
const span1 = max1 - min1;
|
|
11703
|
-
if (span0 === 0 || span1 === 0) return void 0;
|
|
11704
|
-
const zf0 = (0 - min0) / span0;
|
|
11705
|
-
const zf1 = (0 - min1) / span1;
|
|
11706
|
-
const zeroInDomain0 = zf0 >= -1e-3 && zf0 <= 1.001;
|
|
11707
|
-
const zeroInDomain1 = zf1 >= -1e-3 && zf1 <= 1.001;
|
|
11708
|
-
if (!zeroInDomain0 || !zeroInDomain1) return void 0;
|
|
11709
|
-
if (Math.abs(zf0 - zf1) < 1e-3) {
|
|
11710
|
-
return { domain0: [min0, max0], domain1: [min1, max1] };
|
|
11711
|
-
}
|
|
11712
|
-
const targetZf = Math.max(zf0, zf1);
|
|
11713
|
-
const align = (dMin, dMax, currentZf) => {
|
|
11714
|
-
if (Math.abs(currentZf - targetZf) < 1e-3) return [dMin, dMax];
|
|
11715
|
-
if (targetZf > currentZf) {
|
|
11716
|
-
const newMin = -(targetZf / (1 - targetZf)) * dMax;
|
|
11717
|
-
return [newMin, dMax];
|
|
11718
|
-
}
|
|
11719
|
-
const newMax = -dMin * (1 - targetZf) / targetZf;
|
|
11720
|
-
return [dMin, newMax];
|
|
11721
|
-
};
|
|
11722
|
-
const domain0 = align(min0, max0, zf0);
|
|
11723
|
-
const domain1 = align(min1, max1, zf1);
|
|
11724
|
-
return { domain0, domain1 };
|
|
11725
|
-
}
|
|
11726
|
-
function withYDomain(leaf, domain) {
|
|
11727
|
-
if (!leaf.encoding?.y) return leaf;
|
|
11728
|
-
return {
|
|
11729
|
-
...leaf,
|
|
11730
|
-
encoding: {
|
|
11731
|
-
...leaf.encoding,
|
|
11732
|
-
y: {
|
|
11733
|
-
...leaf.encoding.y,
|
|
11734
|
-
scale: {
|
|
11735
|
-
...leaf.encoding.y.scale,
|
|
11736
|
-
domain
|
|
11737
|
-
}
|
|
11738
|
-
}
|
|
11739
|
-
}
|
|
11740
|
-
};
|
|
11741
|
-
}
|
|
11742
|
-
function buildPrimarySpec(leaves, layerSpec) {
|
|
11743
|
-
const allData = leaves.flatMap((leaf) => leaf.data);
|
|
11744
|
-
const primary = {
|
|
11745
|
-
...leaves[0],
|
|
11746
|
-
data: allData,
|
|
11747
|
-
chrome: layerSpec.chrome ?? leaves[0].chrome,
|
|
11748
|
-
annotations: layerSpec.annotations ?? leaves[0].annotations,
|
|
11749
|
-
labels: layerSpec.labels ?? leaves[0].labels,
|
|
11750
|
-
legend: layerSpec.legend ?? leaves[0].legend,
|
|
11751
|
-
responsive: layerSpec.responsive ?? leaves[0].responsive,
|
|
11752
|
-
theme: layerSpec.theme ?? leaves[0].theme,
|
|
11753
|
-
darkMode: layerSpec.darkMode ?? leaves[0].darkMode,
|
|
11754
|
-
watermark: layerSpec.watermark ?? leaves[0].watermark,
|
|
11755
|
-
hiddenSeries: layerSpec.hiddenSeries ?? leaves[0].hiddenSeries,
|
|
11756
|
-
endpointLabels: layerSpec.endpointLabels ?? leaves[0].endpointLabels
|
|
11757
|
-
};
|
|
11758
|
-
return primary;
|
|
11759
|
-
}
|
|
11760
|
-
|
|
11761
|
-
// src/compile/watermark-obstacle.ts
|
|
11762
|
-
import { BRAND_RESERVE_WIDTH as BRAND_RESERVE_WIDTH2 } from "@opendata-ai/openchart-core";
|
|
11763
|
-
var WATERMARK_HEIGHT = 30;
|
|
11764
|
-
var X_AXIS_EXTENT_WITH_LABEL = 48;
|
|
11765
|
-
var X_AXIS_EXTENT_TICKS_ONLY = 26;
|
|
11766
|
-
function computeWatermarkObstacle(dims, watermark, axes, theme) {
|
|
11767
|
-
if (!watermark) return null;
|
|
11768
|
-
const chartArea = dims.chartArea;
|
|
11769
|
-
const brandPadding = theme.spacing.padding;
|
|
11770
|
-
const brandX = dims.total.width - brandPadding - BRAND_RESERVE_WIDTH2;
|
|
11771
|
-
const xAxisExtent = axes.x?.label ? X_AXIS_EXTENT_WITH_LABEL : axes.x ? X_AXIS_EXTENT_TICKS_ONLY : 0;
|
|
11772
|
-
const firstBottomChrome = dims.chrome.source ?? dims.chrome.byline ?? dims.chrome.footer;
|
|
11773
|
-
const brandY = firstBottomChrome ? chartArea.y + chartArea.height + xAxisExtent + firstBottomChrome.y : chartArea.y + chartArea.height + xAxisExtent + theme.spacing.chartToFooter;
|
|
11774
|
-
return { x: brandX, y: brandY, width: BRAND_RESERVE_WIDTH2, height: WATERMARK_HEIGHT };
|
|
11775
|
-
}
|
|
11776
|
-
|
|
11777
|
-
// src/compiler/sparkline-defaults.ts
|
|
11778
|
-
var TREND_DEADBAND = 5e-4;
|
|
11779
|
-
function computeTrend(values) {
|
|
11780
|
-
const finite = [];
|
|
11781
|
-
for (const v of values) {
|
|
11782
|
-
if (Number.isFinite(v)) finite.push(v);
|
|
11783
|
-
}
|
|
11784
|
-
if (finite.length < 2) return "neutral";
|
|
11785
|
-
const n = finite.length;
|
|
11786
|
-
let sumX = 0;
|
|
11787
|
-
let sumY = 0;
|
|
11788
|
-
for (let i = 0; i < n; i++) {
|
|
11789
|
-
sumX += i;
|
|
11790
|
-
sumY += finite[i];
|
|
11791
|
-
}
|
|
11792
|
-
const meanX = sumX / n;
|
|
11793
|
-
const meanY = sumY / n;
|
|
11794
|
-
let num = 0;
|
|
11795
|
-
let den = 0;
|
|
11796
|
-
for (let i = 0; i < n; i++) {
|
|
11797
|
-
const dx = i - meanX;
|
|
11798
|
-
num += dx * (finite[i] - meanY);
|
|
11799
|
-
den += dx * dx;
|
|
11800
|
-
}
|
|
11801
|
-
if (den === 0) return "neutral";
|
|
11802
|
-
const slope = num / den;
|
|
11803
|
-
const scale = Math.abs(meanY) || 1;
|
|
11804
|
-
const relSlope = slope / scale;
|
|
11805
|
-
if (Math.abs(relSlope) < TREND_DEADBAND) return "neutral";
|
|
11806
|
-
return relSlope > 0 ? "up" : "down";
|
|
11807
|
-
}
|
|
11808
|
-
function trendColor(trend, theme) {
|
|
11809
|
-
if (trend === "up") return theme.colors.positive;
|
|
11810
|
-
if (trend === "down") return theme.colors.negative;
|
|
11811
|
-
return theme.colors.categorical[0];
|
|
11812
|
-
}
|
|
11813
|
-
function computeTrendFromData(data, yField) {
|
|
11814
|
-
if (!yField) return "neutral";
|
|
11815
|
-
const values = [];
|
|
11816
|
-
for (const row of data) {
|
|
11817
|
-
const v = Number(row[yField]);
|
|
11818
|
-
if (Number.isFinite(v)) values.push(v);
|
|
11895
|
+
};
|
|
11896
|
+
const scales = computeScales(renderSpec, titleProvArea, renderSpec.data);
|
|
11897
|
+
if (scales.y && scaleSupportsTickCount(scales.y)) {
|
|
11898
|
+
const ticks2 = buildContinuousTicks(scales.y, yTickCount || 5);
|
|
11899
|
+
for (const t of ticks2) {
|
|
11900
|
+
const w = measure(t.label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
|
|
11901
|
+
if (w > estTickLabelWidth) estTickLabelWidth = w;
|
|
11902
|
+
}
|
|
11903
|
+
}
|
|
11904
|
+
}
|
|
11905
|
+
}
|
|
11906
|
+
const titleFontSize = theme.fonts.sizes.body;
|
|
11907
|
+
const offset = axisTitleOffset2(estTickLabelWidth, titleFontSize, width, yIsInline);
|
|
11908
|
+
const halfGlyph = Math.ceil(titleFontSize / 2);
|
|
11909
|
+
const rotatedLabelMargin = offset + halfGlyph + (width < BREAKPOINT_COMPACT_MAX ? 0 : AXIS_TITLE_TRAILING_PAD);
|
|
11910
|
+
gutterWithTitle = Math.max(finalGutter, hPad + rotatedLabelMargin);
|
|
11911
|
+
}
|
|
11912
|
+
finalPlan = {
|
|
11913
|
+
leftGutter: gutterWithTitle,
|
|
11914
|
+
xAxisExtent,
|
|
11915
|
+
legendContent,
|
|
11916
|
+
chrome,
|
|
11917
|
+
yTickValues,
|
|
11918
|
+
yTickCount,
|
|
11919
|
+
converged: prev === null || converged,
|
|
11920
|
+
inlineYLabelInset,
|
|
11921
|
+
endpointLabelsDemoted
|
|
11922
|
+
};
|
|
11923
|
+
break;
|
|
11924
|
+
}
|
|
11925
|
+
prev = { gutter: newGutter, rowCount: legendContent.rowCount, xAxisExtent };
|
|
11926
|
+
leftGutter = newGutter;
|
|
11819
11927
|
}
|
|
11820
|
-
|
|
11821
|
-
|
|
11822
|
-
function hasExplicitColor(markDef, encodingHasColor) {
|
|
11823
|
-
if (encodingHasColor) return { fill: true, stroke: true };
|
|
11824
|
-
return {
|
|
11825
|
-
fill: markDef?.fill !== void 0,
|
|
11826
|
-
stroke: markDef?.stroke !== void 0
|
|
11827
|
-
};
|
|
11828
|
-
}
|
|
11829
|
-
function buildSparklineAreaGradient(baseColor) {
|
|
11830
|
-
return {
|
|
11831
|
-
gradient: "linear",
|
|
11832
|
-
x1: 0,
|
|
11833
|
-
y1: 0,
|
|
11834
|
-
x2: 0,
|
|
11835
|
-
y2: 1,
|
|
11836
|
-
stops: [
|
|
11837
|
-
{ offset: 0, color: baseColor, opacity: 0.35 },
|
|
11838
|
-
{ offset: 1, color: baseColor, opacity: 0 }
|
|
11839
|
-
]
|
|
11840
|
-
};
|
|
11928
|
+
if (!finalPlan) throw new Error("resolveLayoutPlan: loop exited without producing a plan");
|
|
11929
|
+
return finalPlan;
|
|
11841
11930
|
}
|
|
11842
11931
|
|
|
11843
|
-
// src/
|
|
11844
|
-
|
|
11845
|
-
|
|
11846
|
-
|
|
11847
|
-
|
|
11848
|
-
var MIN_NODE_RADIUS = 3;
|
|
11849
|
-
var MAX_NODE_RADIUS = 12;
|
|
11850
|
-
var DEFAULT_EDGE_WIDTH = 1;
|
|
11851
|
-
var MIN_EDGE_WIDTH = 0.5;
|
|
11852
|
-
var MAX_EDGE_WIDTH = 4;
|
|
11853
|
-
var DEFAULT_STROKE_WIDTH2 = 1;
|
|
11854
|
-
function darkenColor(hex2, amount = 0.2) {
|
|
11855
|
-
const clean = hex2.replace(/^#/, "");
|
|
11856
|
-
if (clean.length !== 6 && clean.length !== 3) return hex2;
|
|
11857
|
-
const full = clean.length === 3 ? clean.split("").map((c) => c + c).join("") : clean;
|
|
11858
|
-
const r = Math.max(0, Math.round(parseInt(full.substring(0, 2), 16) * (1 - amount)));
|
|
11859
|
-
const g = Math.max(0, Math.round(parseInt(full.substring(2, 4), 16) * (1 - amount)));
|
|
11860
|
-
const b = Math.max(0, Math.round(parseInt(full.substring(4, 6), 16) * (1 - amount)));
|
|
11861
|
-
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
|
|
11862
|
-
}
|
|
11863
|
-
function hexWithOpacity(hex2, opacity) {
|
|
11864
|
-
const clean = hex2.replace(/^#/, "");
|
|
11865
|
-
if (clean.length !== 6 && clean.length !== 3) {
|
|
11866
|
-
return hex2;
|
|
11867
|
-
}
|
|
11868
|
-
const full = clean.length === 3 ? clean.split("").map((c) => c + c).join("") : clean;
|
|
11869
|
-
const r = parseInt(full.substring(0, 2), 16);
|
|
11870
|
-
const g = parseInt(full.substring(2, 4), 16);
|
|
11871
|
-
const b = parseInt(full.substring(4, 6), 16);
|
|
11872
|
-
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
|
11873
|
-
}
|
|
11874
|
-
function computeDegrees(nodes, edges) {
|
|
11875
|
-
const degrees = /* @__PURE__ */ new Map();
|
|
11876
|
-
for (const node of nodes) {
|
|
11877
|
-
degrees.set(node.id, 0);
|
|
11932
|
+
// src/compile/layer.ts
|
|
11933
|
+
function compileLayer(spec, options, compileChart2) {
|
|
11934
|
+
const leaves = flattenLayers(spec);
|
|
11935
|
+
if (leaves.length === 0) {
|
|
11936
|
+
throw new Error("LayerSpec has no leaf chart specs after flattening");
|
|
11878
11937
|
}
|
|
11879
|
-
|
|
11880
|
-
|
|
11881
|
-
|
|
11938
|
+
if (leaves.length === 1) {
|
|
11939
|
+
const singleSpec = buildPrimarySpec(leaves, spec);
|
|
11940
|
+
return compileChart2(singleSpec, options);
|
|
11882
11941
|
}
|
|
11883
|
-
|
|
11884
|
-
|
|
11885
|
-
function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
|
|
11886
|
-
const degrees = computeDegrees(nodes, edges);
|
|
11887
|
-
const maxDegree = Math.max(1, ...degrees.values());
|
|
11888
|
-
let sizeScale;
|
|
11889
|
-
if (encoding.nodeSize?.field) {
|
|
11890
|
-
const field = encoding.nodeSize.field;
|
|
11891
|
-
const values = nodes.map((n) => Number(n[field])).filter((v) => Number.isFinite(v));
|
|
11892
|
-
const sizeMin = min2(values) ?? 0;
|
|
11893
|
-
const sizeMax = max2(values) ?? 1;
|
|
11894
|
-
sizeScale = sqrt2().domain([sizeMin, sizeMax]).range([MIN_NODE_RADIUS, MAX_NODE_RADIUS]);
|
|
11942
|
+
if (spec.resolve?.scale?.y === "independent") {
|
|
11943
|
+
return compileLayerIndependent(leaves, spec, options, compileChart2);
|
|
11895
11944
|
}
|
|
11896
|
-
|
|
11897
|
-
|
|
11898
|
-
|
|
11899
|
-
|
|
11900
|
-
|
|
11901
|
-
|
|
11902
|
-
|
|
11903
|
-
|
|
11904
|
-
const colorMax = max2(values) ?? 1;
|
|
11905
|
-
const seqPalettes = Object.values(theme.colors.sequential);
|
|
11906
|
-
const palette = seqPalettes.length > 0 ? seqPalettes[0] : ["#ccc", "#333"];
|
|
11907
|
-
const domain = scaleConfig?.domain && scaleConfig.domain.length === 2 ? scaleConfig.domain : [colorMin, colorMax];
|
|
11908
|
-
const range2 = scaleConfig?.range && scaleConfig.range.length >= 2 ? scaleConfig.range : [palette[0], palette[palette.length - 1]];
|
|
11909
|
-
const colorScale = linear2().domain(domain).range(range2);
|
|
11910
|
-
colorFn = (node) => {
|
|
11911
|
-
const val = Number(node[field]);
|
|
11912
|
-
return Number.isFinite(val) ? colorScale(val) : theme.colors.categorical[0];
|
|
11913
|
-
};
|
|
11914
|
-
} else {
|
|
11915
|
-
const domain = scaleConfig?.domain && Array.isArray(scaleConfig.domain) ? scaleConfig.domain : [...new Set(nodes.map((n) => String(n[field] ?? "")))];
|
|
11916
|
-
const range2 = scaleConfig?.range && scaleConfig.range.length > 0 ? scaleConfig.range : theme.colors.categorical;
|
|
11917
|
-
const ordinalScale = ordinal().domain(domain).range(range2);
|
|
11918
|
-
colorFn = (node) => ordinalScale(String(node[field] ?? ""));
|
|
11919
|
-
}
|
|
11945
|
+
const primarySpec = buildPrimarySpec(leaves, spec);
|
|
11946
|
+
const primaryLayout = compileChart2(primarySpec, options);
|
|
11947
|
+
const allMarks = [];
|
|
11948
|
+
const seenLabels = /* @__PURE__ */ new Set();
|
|
11949
|
+
const pLegend = primaryLayout.legend;
|
|
11950
|
+
const mergedLegendEntries = "entries" in pLegend ? [...pLegend.entries] : [];
|
|
11951
|
+
for (const entry of mergedLegendEntries) {
|
|
11952
|
+
seenLabels.add(entry.label);
|
|
11920
11953
|
}
|
|
11921
|
-
const
|
|
11922
|
-
|
|
11923
|
-
|
|
11924
|
-
|
|
11925
|
-
|
|
11926
|
-
|
|
11927
|
-
|
|
11928
|
-
|
|
11929
|
-
|
|
11930
|
-
|
|
11931
|
-
|
|
11932
|
-
let label;
|
|
11933
|
-
if (encoding.nodeLabel?.field) {
|
|
11934
|
-
const labelVal = node[encoding.nodeLabel.field];
|
|
11935
|
-
label = labelVal != null ? String(labelVal) : void 0;
|
|
11936
|
-
} else {
|
|
11937
|
-
label = node.id;
|
|
11938
|
-
}
|
|
11939
|
-
const degree = degrees.get(node.id) ?? 0;
|
|
11940
|
-
const labelPriority = maxDegree > 0 ? degree / maxDegree : 0;
|
|
11941
|
-
const { id: _id, ...rest } = node;
|
|
11942
|
-
const data = { id: node.id, ...rest };
|
|
11943
|
-
const override = nodeOverrides?.[node.id];
|
|
11944
|
-
const finalFill = override?.fill ?? fill;
|
|
11945
|
-
const finalRadius = override?.radius ?? radius;
|
|
11946
|
-
const finalStrokeWidth = override?.strokeWidth ?? DEFAULT_STROKE_WIDTH2;
|
|
11947
|
-
const finalStroke = override?.stroke ?? stroke;
|
|
11948
|
-
const finalLabelPriority = override?.alwaysShowLabel ? Infinity : labelPriority;
|
|
11949
|
-
return {
|
|
11950
|
-
id: node.id,
|
|
11951
|
-
radius: finalRadius,
|
|
11952
|
-
fill: finalFill,
|
|
11953
|
-
stroke: finalStroke,
|
|
11954
|
-
strokeWidth: finalStrokeWidth,
|
|
11955
|
-
label,
|
|
11956
|
-
labelPriority: finalLabelPriority,
|
|
11957
|
-
community: void 0,
|
|
11958
|
-
data
|
|
11954
|
+
const indexedLeaves = leaves.map((leaf, i) => ({
|
|
11955
|
+
leaf,
|
|
11956
|
+
zIndex: leaf.zIndex ?? i
|
|
11957
|
+
}));
|
|
11958
|
+
indexedLeaves.sort((a, b) => a.zIndex - b.zIndex);
|
|
11959
|
+
const leafOptions = { ...options, frozenChartArea: primaryLayout.area };
|
|
11960
|
+
for (const { leaf } of indexedLeaves) {
|
|
11961
|
+
const themedLeaf = {
|
|
11962
|
+
...leaf,
|
|
11963
|
+
theme: leaf.theme ?? spec.theme,
|
|
11964
|
+
darkMode: leaf.darkMode ?? spec.darkMode
|
|
11959
11965
|
};
|
|
11960
|
-
|
|
11961
|
-
|
|
11962
|
-
|
|
11963
|
-
|
|
11964
|
-
|
|
11965
|
-
|
|
11966
|
-
|
|
11967
|
-
|
|
11968
|
-
|
|
11969
|
-
widthScale = linear2().domain([widthMin, widthMax]).range([MIN_EDGE_WIDTH, MAX_EDGE_WIDTH]);
|
|
11970
|
-
}
|
|
11971
|
-
let edgeColorFn;
|
|
11972
|
-
if (encoding.edgeColor?.field) {
|
|
11973
|
-
const field = encoding.edgeColor.field;
|
|
11974
|
-
const fieldType = encoding.edgeColor.type ?? "nominal";
|
|
11975
|
-
const scaleConfig = encoding.edgeColor.scale;
|
|
11976
|
-
if (fieldType === "quantitative") {
|
|
11977
|
-
const values = edges.map((e) => Number(e[field])).filter((v) => Number.isFinite(v));
|
|
11978
|
-
const colorMin = min2(values) ?? 0;
|
|
11979
|
-
const colorMax = max2(values) ?? 1;
|
|
11980
|
-
const seqPalettes = Object.values(theme.colors.sequential);
|
|
11981
|
-
const palette = seqPalettes.length > 0 ? seqPalettes[0] : ["#ccc", "#333"];
|
|
11982
|
-
const domain = scaleConfig?.domain && scaleConfig.domain.length === 2 ? scaleConfig.domain : [colorMin, colorMax];
|
|
11983
|
-
const range2 = scaleConfig?.range && scaleConfig.range.length >= 2 ? scaleConfig.range : [palette[0], palette[palette.length - 1]];
|
|
11984
|
-
const colorScale = linear2().domain(domain).range(range2);
|
|
11985
|
-
edgeColorFn = (edge) => {
|
|
11986
|
-
const val = Number(edge[field]);
|
|
11987
|
-
return Number.isFinite(val) ? colorScale(val) : hexWithOpacity(theme.colors.axis, 0.4);
|
|
11988
|
-
};
|
|
11989
|
-
} else {
|
|
11990
|
-
const domain = scaleConfig?.domain && Array.isArray(scaleConfig.domain) ? scaleConfig.domain : [...new Set(edges.map((e) => String(e[field] ?? "")))];
|
|
11991
|
-
const range2 = scaleConfig?.range && scaleConfig.range.length > 0 ? scaleConfig.range : theme.colors.categorical;
|
|
11992
|
-
const ordinalScale = ordinal().domain(domain).range(range2);
|
|
11993
|
-
edgeColorFn = (edge) => ordinalScale(String(edge[field] ?? ""));
|
|
11994
|
-
}
|
|
11995
|
-
}
|
|
11996
|
-
const defaultEdgeColor = hexWithOpacity(theme.colors.axis, 0.4);
|
|
11997
|
-
const EDGE_STYLES = ["solid", "dashed", "dotted"];
|
|
11998
|
-
let styleFn;
|
|
11999
|
-
if (encoding.edgeStyle?.field) {
|
|
12000
|
-
const field = encoding.edgeStyle.field;
|
|
12001
|
-
const uniqueValues = [...new Set(edges.map((e) => String(e[field] ?? "")))];
|
|
12002
|
-
const styleMap = /* @__PURE__ */ new Map();
|
|
12003
|
-
for (let i = 0; i < uniqueValues.length; i++) {
|
|
12004
|
-
styleMap.set(uniqueValues[i], EDGE_STYLES[i % EDGE_STYLES.length]);
|
|
12005
|
-
}
|
|
12006
|
-
styleFn = (edge) => styleMap.get(String(edge[field] ?? "")) ?? "solid";
|
|
12007
|
-
}
|
|
12008
|
-
return edges.map((edge) => {
|
|
12009
|
-
const { source, target, ...rest } = edge;
|
|
12010
|
-
let strokeWidth = DEFAULT_EDGE_WIDTH;
|
|
12011
|
-
if (widthScale && encoding.edgeWidth?.field) {
|
|
12012
|
-
const val = Number(edge[encoding.edgeWidth.field]);
|
|
12013
|
-
if (Number.isFinite(val)) {
|
|
12014
|
-
strokeWidth = widthScale(val);
|
|
11966
|
+
const leafLayout = compileChart2(themedLeaf, leafOptions);
|
|
11967
|
+
allMarks.push(...leafLayout.marks);
|
|
11968
|
+
const leafLeg = leafLayout.legend;
|
|
11969
|
+
if ("entries" in leafLeg) {
|
|
11970
|
+
for (const entry of leafLeg.entries) {
|
|
11971
|
+
if (!seenLabels.has(entry.label)) {
|
|
11972
|
+
seenLabels.add(entry.label);
|
|
11973
|
+
mergedLegendEntries.push(entry);
|
|
11974
|
+
}
|
|
12015
11975
|
}
|
|
12016
11976
|
}
|
|
12017
|
-
const stroke = edgeColorFn ? edgeColorFn(edge) : defaultEdgeColor;
|
|
12018
|
-
const style = styleFn ? styleFn(edge) : "solid";
|
|
12019
|
-
return {
|
|
12020
|
-
source,
|
|
12021
|
-
target,
|
|
12022
|
-
stroke,
|
|
12023
|
-
strokeWidth,
|
|
12024
|
-
style,
|
|
12025
|
-
data: { source, target, ...rest }
|
|
12026
|
-
};
|
|
12027
|
-
});
|
|
12028
|
-
}
|
|
12029
|
-
|
|
12030
|
-
// src/graphs/community.ts
|
|
12031
|
-
function assignCommunities(nodes, clusteringField) {
|
|
12032
|
-
if (!clusteringField) return;
|
|
12033
|
-
for (const node of nodes) {
|
|
12034
|
-
const value2 = node.data[clusteringField];
|
|
12035
|
-
node.community = value2 != null ? String(value2) : void 0;
|
|
12036
11977
|
}
|
|
11978
|
+
return {
|
|
11979
|
+
...primaryLayout,
|
|
11980
|
+
marks: allMarks,
|
|
11981
|
+
legend: {
|
|
11982
|
+
...primaryLayout.legend,
|
|
11983
|
+
..."entries" in pLegend ? { entries: mergedLegendEntries } : {}
|
|
11984
|
+
}
|
|
11985
|
+
};
|
|
12037
11986
|
}
|
|
12038
|
-
function
|
|
12039
|
-
|
|
12040
|
-
const
|
|
12041
|
-
|
|
12042
|
-
|
|
12043
|
-
|
|
12044
|
-
|
|
12045
|
-
|
|
11987
|
+
function estimateYAxisLabelWidth(data, encoding, baseFontSize, measure) {
|
|
11988
|
+
if (!encoding?.y) return 40;
|
|
11989
|
+
const yEnc = encoding.y;
|
|
11990
|
+
const yField = yEnc.field;
|
|
11991
|
+
if (!yField) return 40;
|
|
11992
|
+
const m = measure ?? estimateTextWidth15;
|
|
11993
|
+
const yType = yEnc.type;
|
|
11994
|
+
if (yType === "nominal" || yType === "ordinal") {
|
|
11995
|
+
let maxWidth = 0;
|
|
11996
|
+
for (const row of data) {
|
|
11997
|
+
const label = String(row[yField] ?? "");
|
|
11998
|
+
const w = m(label, baseFontSize, 400);
|
|
11999
|
+
if (w > maxWidth) maxWidth = w;
|
|
12046
12000
|
}
|
|
12001
|
+
return maxWidth > 0 ? maxWidth + 10 : 40;
|
|
12047
12002
|
}
|
|
12048
|
-
|
|
12049
|
-
|
|
12050
|
-
|
|
12051
|
-
|
|
12052
|
-
if (
|
|
12053
|
-
|
|
12054
|
-
|
|
12055
|
-
|
|
12056
|
-
|
|
12057
|
-
|
|
12003
|
+
const yAxisFormat = encoding.y.axis?.format;
|
|
12004
|
+
let maxAbsVal = 0;
|
|
12005
|
+
for (const row of data) {
|
|
12006
|
+
const v = Number(row[yField]);
|
|
12007
|
+
if (Number.isFinite(v) && Math.abs(v) > maxAbsVal) maxAbsVal = Math.abs(v);
|
|
12008
|
+
}
|
|
12009
|
+
let sampleLabel;
|
|
12010
|
+
if (yAxisFormat) {
|
|
12011
|
+
try {
|
|
12012
|
+
const fmt = format(yAxisFormat);
|
|
12013
|
+
sampleLabel = fmt(maxAbsVal);
|
|
12014
|
+
} catch {
|
|
12015
|
+
sampleLabel = String(maxAbsVal);
|
|
12058
12016
|
}
|
|
12017
|
+
} else {
|
|
12018
|
+
sampleLabel = Math.abs(maxAbsVal) >= 1e3 ? abbreviateNumber4(maxAbsVal) : formatNumber5(maxAbsVal);
|
|
12059
12019
|
}
|
|
12020
|
+
const hasNeg = data.some((r) => Number(r[yField]) < 0);
|
|
12021
|
+
const labelEst = (hasNeg ? "-" : "") + sampleLabel;
|
|
12022
|
+
return m(labelEst, baseFontSize, 400) + 10;
|
|
12060
12023
|
}
|
|
12061
|
-
|
|
12062
|
-
|
|
12063
|
-
|
|
12064
|
-
|
|
12065
|
-
|
|
12066
|
-
|
|
12067
|
-
const
|
|
12068
|
-
|
|
12069
|
-
|
|
12070
|
-
|
|
12071
|
-
|
|
12072
|
-
|
|
12024
|
+
function compileLayerIndependent(leaves, layerSpec, options, compileChart2) {
|
|
12025
|
+
if (leaves.length > 2) {
|
|
12026
|
+
throw new Error(
|
|
12027
|
+
`Independent y-scales support at most 2 layers (left and right y-axis). Got ${leaves.length} layers.`
|
|
12028
|
+
);
|
|
12029
|
+
}
|
|
12030
|
+
const leaf0 = leaves[0];
|
|
12031
|
+
const leaf1 = leaves[1];
|
|
12032
|
+
const xType0 = leaf0.encoding?.x?.type;
|
|
12033
|
+
const xType1 = leaf1.encoding?.x?.type;
|
|
12034
|
+
if (xType0 && xType1 && xType0 !== xType1) {
|
|
12035
|
+
throw new Error(
|
|
12036
|
+
`Dual-axis charts require matching x-field types across layers. Layer 0 has '${xType0}', layer 1 has '${xType1}'.`
|
|
12037
|
+
);
|
|
12038
|
+
}
|
|
12039
|
+
const theme = resolveTheme2(layerSpec.theme ?? leaf1.theme);
|
|
12040
|
+
const axisFontSize = theme.fonts?.sizes?.axisTick ?? 11;
|
|
12041
|
+
const measureFn = createMeasureFn(options.measureText);
|
|
12042
|
+
const rightAxisWidth = estimateYAxisLabelWidth(
|
|
12043
|
+
leaf1.data,
|
|
12044
|
+
leaf1.encoding,
|
|
12045
|
+
axisFontSize,
|
|
12046
|
+
measureFn
|
|
12047
|
+
);
|
|
12048
|
+
const yAxisConfig = leaf1.encoding?.y?.axis || void 0;
|
|
12049
|
+
const hasRightAxisTitle = !!yAxisConfig?.title;
|
|
12050
|
+
const tickExtent = TICK_LABEL_OFFSET + rightAxisWidth;
|
|
12051
|
+
const bodyFontSize = theme.fonts?.sizes?.body ?? 13;
|
|
12052
|
+
const axisTitleOffset4 = getAxisTitleOffset2(options.width);
|
|
12053
|
+
const halfGlyph = Math.ceil(bodyFontSize / 2);
|
|
12054
|
+
const titleExtent = hasRightAxisTitle ? axisTitleOffset4 + halfGlyph + (options.width < BREAKPOINT_COMPACT_MAX2 ? 0 : AXIS_TITLE_TRAILING_PAD2) : 0;
|
|
12055
|
+
const rightReserve = Math.max(tickExtent, titleExtent);
|
|
12056
|
+
const optionsWithReserve = {
|
|
12057
|
+
...options,
|
|
12058
|
+
rightAxisReserve: rightReserve
|
|
12073
12059
|
};
|
|
12074
|
-
|
|
12075
|
-
|
|
12076
|
-
|
|
12077
|
-
|
|
12078
|
-
|
|
12079
|
-
|
|
12080
|
-
|
|
12081
|
-
|
|
12082
|
-
|
|
12083
|
-
|
|
12084
|
-
|
|
12085
|
-
|
|
12086
|
-
|
|
12087
|
-
|
|
12060
|
+
const xField0 = leaf0.encoding?.x?.field;
|
|
12061
|
+
const xField1 = leaf1.encoding?.x?.field;
|
|
12062
|
+
const unionXValues = /* @__PURE__ */ new Set();
|
|
12063
|
+
if (xField0) for (const row of leaf0.data) unionXValues.add(row[xField0]);
|
|
12064
|
+
if (xField1) for (const row of leaf1.data) unionXValues.add(row[xField1]);
|
|
12065
|
+
let leaf0WithUnionX = ensureXDomainCoverage(leaf0, xField0, unionXValues);
|
|
12066
|
+
let leaf1WithUnionX = ensureXDomainCoverage(leaf1, xField1, unionXValues);
|
|
12067
|
+
const aligned = alignYDomains(leaf0WithUnionX, leaf1WithUnionX);
|
|
12068
|
+
if (aligned) {
|
|
12069
|
+
leaf0WithUnionX = withYDomain(leaf0WithUnionX, aligned.domain0);
|
|
12070
|
+
leaf1WithUnionX = withYDomain(leaf1WithUnionX, aligned.domain1);
|
|
12071
|
+
}
|
|
12072
|
+
const primary0 = buildPrimarySpec([leaf0WithUnionX], layerSpec);
|
|
12073
|
+
const layout0 = compileChart2(primary0, optionsWithReserve);
|
|
12074
|
+
const primary1 = buildPrimarySpec([leaf1WithUnionX], layerSpec);
|
|
12075
|
+
primary1.annotations = [];
|
|
12076
|
+
const layout1 = compileChart2(primary1, optionsWithReserve);
|
|
12077
|
+
const y2Axis = layout1.axes.y ? {
|
|
12078
|
+
...layout1.axes.y,
|
|
12079
|
+
orient: "right",
|
|
12080
|
+
gridlines: [],
|
|
12081
|
+
start: {
|
|
12082
|
+
x: layout0.area.x + layout0.area.width,
|
|
12083
|
+
y: layout0.area.y
|
|
12084
|
+
},
|
|
12085
|
+
end: {
|
|
12086
|
+
x: layout0.area.x + layout0.area.width,
|
|
12087
|
+
y: layout0.area.y + layout0.area.height
|
|
12088
|
+
},
|
|
12089
|
+
...layout1.axes.y.label ? {
|
|
12090
|
+
titlePosition: {
|
|
12091
|
+
x: layout0.area.x + layout0.area.width + getAxisTitleOffset2(layout0.dimensions.width),
|
|
12092
|
+
y: layout0.area.y + layout0.area.height / 2,
|
|
12093
|
+
angle: 90
|
|
12088
12094
|
}
|
|
12095
|
+
} : {}
|
|
12096
|
+
} : void 0;
|
|
12097
|
+
const layer0HasBars = layout0.marks.some((m) => m.type === "rect");
|
|
12098
|
+
const layer1HasBars = layout1.marks.some((m) => m.type === "rect");
|
|
12099
|
+
const bandCenterByCategory = /* @__PURE__ */ new Map();
|
|
12100
|
+
if (layer0HasBars && layout0.axes.x?.ticks) {
|
|
12101
|
+
for (const tick of layout0.axes.x.ticks) {
|
|
12102
|
+
bandCenterByCategory.set(String(tick.label), tick.position);
|
|
12089
12103
|
}
|
|
12090
|
-
|
|
12091
|
-
|
|
12104
|
+
} else if (layer1HasBars && layout1.axes.x?.ticks) {
|
|
12105
|
+
for (const tick of layout1.axes.x.ticks) {
|
|
12106
|
+
bandCenterByCategory.set(String(tick.label), tick.position);
|
|
12107
|
+
}
|
|
12108
|
+
}
|
|
12109
|
+
const remapMarkX = (xField, mark) => {
|
|
12110
|
+
if (!xField || bandCenterByCategory.size === 0) return mark;
|
|
12111
|
+
if (mark.type === "line") {
|
|
12112
|
+
const newPoints = mark.points.map((p, i) => {
|
|
12113
|
+
const bx = bandCenterByCategory.get(String(mark.data[i]?.[xField] ?? ""));
|
|
12114
|
+
return bx !== void 0 ? { ...p, x: bx } : p;
|
|
12115
|
+
});
|
|
12116
|
+
const newDataPoints = mark.dataPoints?.map((dp, i) => {
|
|
12117
|
+
const bx = bandCenterByCategory.get(String(mark.data[i]?.[xField] ?? ""));
|
|
12118
|
+
return bx !== void 0 ? { ...dp, x: bx } : dp;
|
|
12119
|
+
});
|
|
12120
|
+
const newPath = line_default().x((p) => p.x).y((p) => p.y).curve(monotoneX)(newPoints) ?? void 0;
|
|
12121
|
+
return { ...mark, points: newPoints, dataPoints: newDataPoints, path: newPath };
|
|
12122
|
+
}
|
|
12123
|
+
if (mark.type === "area") {
|
|
12124
|
+
const newTopPoints = mark.topPoints.map((p, i) => {
|
|
12125
|
+
const bx = bandCenterByCategory.get(String(mark.data[i]?.[xField] ?? ""));
|
|
12126
|
+
return bx !== void 0 ? { ...p, x: bx } : p;
|
|
12127
|
+
});
|
|
12128
|
+
const newBottomPoints = mark.bottomPoints.map((p, i) => {
|
|
12129
|
+
const bx = bandCenterByCategory.get(String(mark.data[i]?.[xField] ?? ""));
|
|
12130
|
+
return bx !== void 0 ? { ...p, x: bx } : p;
|
|
12131
|
+
});
|
|
12132
|
+
const newDataPoints = mark.dataPoints?.map((dp, i) => {
|
|
12133
|
+
const bx = bandCenterByCategory.get(String(mark.data[i]?.[xField] ?? ""));
|
|
12134
|
+
return bx !== void 0 ? { ...dp, x: bx } : dp;
|
|
12135
|
+
});
|
|
12136
|
+
const areaGen = area_default().x((p) => p.x).y0((p) => p.yBottom).y1((p) => p.yTop).curve(monotoneX);
|
|
12137
|
+
const topLineGen = line_default().x((p) => p.x).y((p) => p.yTop).curve(monotoneX);
|
|
12138
|
+
const combined = newTopPoints.map((tp, i) => ({
|
|
12139
|
+
x: tp.x,
|
|
12140
|
+
yTop: tp.y,
|
|
12141
|
+
yBottom: newBottomPoints[i]?.y ?? tp.y
|
|
12142
|
+
}));
|
|
12143
|
+
const newPath = areaGen(combined) ?? "";
|
|
12144
|
+
const newTopPath = topLineGen(combined) ?? "";
|
|
12145
|
+
return {
|
|
12146
|
+
...mark,
|
|
12147
|
+
topPoints: newTopPoints,
|
|
12148
|
+
bottomPoints: newBottomPoints,
|
|
12149
|
+
dataPoints: newDataPoints,
|
|
12150
|
+
path: newPath,
|
|
12151
|
+
topPath: newTopPath
|
|
12152
|
+
};
|
|
12153
|
+
}
|
|
12154
|
+
if (mark.type === "point") {
|
|
12155
|
+
const bx = bandCenterByCategory.get(String(mark.data[xField] ?? ""));
|
|
12156
|
+
return bx !== void 0 ? { ...mark, cx: bx } : mark;
|
|
12157
|
+
}
|
|
12158
|
+
return mark;
|
|
12159
|
+
};
|
|
12160
|
+
const adjustedMarks0 = bandCenterByCategory.size > 0 && !layer0HasBars ? layout0.marks.map((m) => remapMarkX(xField0, m)) : layout0.marks;
|
|
12161
|
+
const taggedMarks1 = layout1.marks.map((mark) => {
|
|
12162
|
+
const tagged = { ...mark, yScale: "y2" };
|
|
12163
|
+
if (bandCenterByCategory.size > 0 && !layer1HasBars) {
|
|
12164
|
+
return remapMarkX(xField1, tagged);
|
|
12165
|
+
}
|
|
12166
|
+
return tagged;
|
|
12167
|
+
});
|
|
12168
|
+
const seenLabels = /* @__PURE__ */ new Set();
|
|
12169
|
+
const l0Legend = layout0.legend;
|
|
12170
|
+
const l1Legend = layout1.legend;
|
|
12171
|
+
const mergedLegendEntries = "entries" in l0Legend ? [...l0Legend.entries] : [];
|
|
12172
|
+
for (const entry of mergedLegendEntries) seenLabels.add(entry.label);
|
|
12173
|
+
const l1Entries = "entries" in l1Legend ? l1Legend.entries : [];
|
|
12174
|
+
for (const entry of l1Entries) {
|
|
12175
|
+
if (!seenLabels.has(entry.label)) {
|
|
12176
|
+
seenLabels.add(entry.label);
|
|
12177
|
+
mergedLegendEntries.push(entry);
|
|
12178
|
+
}
|
|
12179
|
+
}
|
|
12180
|
+
const l0Count = layout0.marks.length;
|
|
12181
|
+
const mergedTooltips = new Map(layout0.tooltipDescriptors);
|
|
12182
|
+
for (const [key, value2] of layout1.tooltipDescriptors) {
|
|
12183
|
+
const match = /^(rect|point|arc)-(\d+)$/.exec(key);
|
|
12184
|
+
if (match) {
|
|
12185
|
+
const offsetKey = `${match[1]}-${Number(match[2]) + l0Count}`;
|
|
12186
|
+
mergedTooltips.set(offsetKey, value2);
|
|
12092
12187
|
} else {
|
|
12093
|
-
|
|
12094
|
-
label,
|
|
12095
|
-
color: color2,
|
|
12096
|
-
shape: "circle",
|
|
12097
|
-
active: true
|
|
12098
|
-
}));
|
|
12188
|
+
mergedTooltips.set(key, value2);
|
|
12099
12189
|
}
|
|
12100
12190
|
}
|
|
12191
|
+
const z0 = leaf0.zIndex ?? 0;
|
|
12192
|
+
const z1 = leaf1.zIndex ?? 1;
|
|
12193
|
+
const marks = z0 <= z1 ? [...adjustedMarks0, ...taggedMarks1] : [...taggedMarks1, ...adjustedMarks0];
|
|
12101
12194
|
return {
|
|
12102
|
-
|
|
12103
|
-
|
|
12104
|
-
|
|
12105
|
-
|
|
12106
|
-
|
|
12107
|
-
|
|
12108
|
-
|
|
12109
|
-
|
|
12195
|
+
...layout0,
|
|
12196
|
+
axes: {
|
|
12197
|
+
x: layout0.axes.x,
|
|
12198
|
+
y: layout0.axes.y,
|
|
12199
|
+
y2: y2Axis
|
|
12200
|
+
},
|
|
12201
|
+
marks,
|
|
12202
|
+
legend: {
|
|
12203
|
+
...layout0.legend,
|
|
12204
|
+
..."entries" in l0Legend ? { entries: mergedLegendEntries } : {}
|
|
12205
|
+
},
|
|
12206
|
+
tooltipDescriptors: mergedTooltips
|
|
12110
12207
|
};
|
|
12111
12208
|
}
|
|
12112
|
-
function
|
|
12113
|
-
|
|
12114
|
-
|
|
12115
|
-
|
|
12116
|
-
|
|
12117
|
-
|
|
12118
|
-
|
|
12119
|
-
|
|
12120
|
-
color: node.fill
|
|
12121
|
-
});
|
|
12122
|
-
}
|
|
12123
|
-
for (const [key, value2] of Object.entries(node.data)) {
|
|
12124
|
-
if (key === "id") continue;
|
|
12125
|
-
if (value2 == null) continue;
|
|
12126
|
-
fields.push({
|
|
12127
|
-
label: key,
|
|
12128
|
-
value: typeof value2 === "number" ? value2.toLocaleString() : String(value2)
|
|
12129
|
-
});
|
|
12209
|
+
function ensureXDomainCoverage(leaf, xField, allXValues) {
|
|
12210
|
+
if (!xField || allXValues.size === 0) return leaf;
|
|
12211
|
+
const existingXValues = /* @__PURE__ */ new Set();
|
|
12212
|
+
for (const row of leaf.data) existingXValues.add(row[xField]);
|
|
12213
|
+
const missingRows = [];
|
|
12214
|
+
for (const xVal of allXValues) {
|
|
12215
|
+
if (!existingXValues.has(xVal)) {
|
|
12216
|
+
missingRows.push({ [xField]: xVal });
|
|
12130
12217
|
}
|
|
12131
|
-
descriptors.set(node.id, {
|
|
12132
|
-
title: node.label ?? node.id,
|
|
12133
|
-
fields
|
|
12134
|
-
});
|
|
12135
12218
|
}
|
|
12136
|
-
return
|
|
12219
|
+
if (missingRows.length === 0) return leaf;
|
|
12220
|
+
return {
|
|
12221
|
+
...leaf,
|
|
12222
|
+
data: [...leaf.data, ...missingRows]
|
|
12223
|
+
};
|
|
12137
12224
|
}
|
|
12138
|
-
function
|
|
12139
|
-
const
|
|
12140
|
-
|
|
12141
|
-
|
|
12142
|
-
|
|
12143
|
-
|
|
12144
|
-
|
|
12145
|
-
const
|
|
12146
|
-
const
|
|
12147
|
-
const
|
|
12148
|
-
|
|
12149
|
-
|
|
12150
|
-
|
|
12151
|
-
|
|
12152
|
-
|
|
12153
|
-
|
|
12154
|
-
|
|
12155
|
-
|
|
12156
|
-
|
|
12157
|
-
|
|
12158
|
-
|
|
12159
|
-
|
|
12160
|
-
const clusteringField = graphSpec.layout.clustering?.field;
|
|
12161
|
-
const hasCommunities = !!clusteringField;
|
|
12162
|
-
assignCommunities(compiledNodes, clusteringField);
|
|
12163
|
-
const hasNodeColorEncoding = !!graphSpec.encoding.nodeColor?.field;
|
|
12164
|
-
let communityColorMap = /* @__PURE__ */ new Map();
|
|
12165
|
-
if (hasCommunities && !hasNodeColorEncoding) {
|
|
12166
|
-
communityColorMap = buildCommunityColorMap(compiledNodes, theme);
|
|
12167
|
-
applyCommunityColors(compiledNodes, communityColorMap);
|
|
12168
|
-
}
|
|
12169
|
-
const compiledEdges = resolveEdgeVisuals(graphSpec.edges, graphSpec.encoding, theme);
|
|
12170
|
-
const useCommunitiesForLegend = hasCommunities && !hasNodeColorEncoding;
|
|
12171
|
-
const legend = buildGraphLegend(
|
|
12172
|
-
compiledNodes,
|
|
12173
|
-
communityColorMap,
|
|
12174
|
-
useCommunitiesForLegend,
|
|
12175
|
-
theme,
|
|
12176
|
-
graphSpec.encoding.nodeColor?.field
|
|
12177
|
-
);
|
|
12178
|
-
const tooltipDescriptors = buildGraphTooltips(compiledNodes);
|
|
12179
|
-
const communityCount = communityColorMap.size;
|
|
12180
|
-
const altParts = [
|
|
12181
|
-
`Network graph with ${compiledNodes.length} nodes and ${compiledEdges.length} edges`
|
|
12182
|
-
];
|
|
12183
|
-
if (communityCount > 0) {
|
|
12184
|
-
altParts.push(`organized into ${communityCount} communities`);
|
|
12185
|
-
}
|
|
12186
|
-
const a11y = {
|
|
12187
|
-
altText: altParts.join(", "),
|
|
12188
|
-
dataTableFallback: compiledNodes.map((n) => [n.id, n.community ?? "", n.label ?? ""]),
|
|
12189
|
-
role: "img",
|
|
12190
|
-
keyboardNavigable: compiledNodes.length > 0
|
|
12225
|
+
function alignYDomains(leaf0, leaf1) {
|
|
12226
|
+
const yEnc0 = leaf0.encoding?.y;
|
|
12227
|
+
const yEnc1 = leaf1.encoding?.y;
|
|
12228
|
+
if (!yEnc0 || !yEnc1) return void 0;
|
|
12229
|
+
if (yEnc0.type !== "quantitative" || yEnc1.type !== "quantitative") return void 0;
|
|
12230
|
+
if (yEnc0.scale?.domain || yEnc1.scale?.domain) return void 0;
|
|
12231
|
+
const includeZero0 = yEnc0.scale?.zero !== false;
|
|
12232
|
+
const includeZero1 = yEnc1.scale?.zero !== false;
|
|
12233
|
+
const vals0 = leaf0.data.map((r) => Number(r[yEnc0.field])).filter(Number.isFinite);
|
|
12234
|
+
const vals1 = leaf1.data.map((r) => Number(r[yEnc1.field])).filter(Number.isFinite);
|
|
12235
|
+
if (vals0.length === 0 || vals1.length === 0) return void 0;
|
|
12236
|
+
const niced = (vals, includeZero) => {
|
|
12237
|
+
let lo = Math.min(...vals);
|
|
12238
|
+
let hi = Math.max(...vals);
|
|
12239
|
+
if (includeZero) {
|
|
12240
|
+
lo = Math.min(0, lo);
|
|
12241
|
+
hi = Math.max(0, hi);
|
|
12242
|
+
}
|
|
12243
|
+
const s = linear2().domain([lo, hi]);
|
|
12244
|
+
s.nice();
|
|
12245
|
+
const [dLo, dHi] = s.domain();
|
|
12246
|
+
return [dLo, dHi];
|
|
12191
12247
|
};
|
|
12192
|
-
const
|
|
12193
|
-
const
|
|
12194
|
-
const
|
|
12195
|
-
|
|
12196
|
-
|
|
12197
|
-
|
|
12198
|
-
|
|
12199
|
-
|
|
12200
|
-
|
|
12201
|
-
|
|
12202
|
-
|
|
12203
|
-
|
|
12248
|
+
const [min0, max0] = niced(vals0, includeZero0);
|
|
12249
|
+
const [min1, max1] = niced(vals1, includeZero1);
|
|
12250
|
+
const span0 = max0 - min0;
|
|
12251
|
+
const span1 = max1 - min1;
|
|
12252
|
+
if (span0 === 0 || span1 === 0) return void 0;
|
|
12253
|
+
const zf0 = (0 - min0) / span0;
|
|
12254
|
+
const zf1 = (0 - min1) / span1;
|
|
12255
|
+
const zeroInDomain0 = zf0 >= -1e-3 && zf0 <= 1.001;
|
|
12256
|
+
const zeroInDomain1 = zf1 >= -1e-3 && zf1 <= 1.001;
|
|
12257
|
+
if (!zeroInDomain0 || !zeroInDomain1) return void 0;
|
|
12258
|
+
if (Math.abs(zf0 - zf1) < 1e-3) {
|
|
12259
|
+
return { domain0: [min0, max0], domain1: [min1, max1] };
|
|
12260
|
+
}
|
|
12261
|
+
const targetZf = Math.max(zf0, zf1);
|
|
12262
|
+
const align = (dMin, dMax, currentZf) => {
|
|
12263
|
+
if (Math.abs(currentZf - targetZf) < 1e-3) return [dMin, dMax];
|
|
12264
|
+
if (targetZf > currentZf) {
|
|
12265
|
+
const newMin = -(targetZf / (1 - targetZf)) * dMax;
|
|
12266
|
+
return [newMin, dMax];
|
|
12267
|
+
}
|
|
12268
|
+
const newMax = -dMin * (1 - targetZf) / targetZf;
|
|
12269
|
+
return [dMin, newMax];
|
|
12204
12270
|
};
|
|
12205
|
-
const
|
|
12206
|
-
|
|
12207
|
-
|
|
12208
|
-
|
|
12209
|
-
|
|
12210
|
-
|
|
12211
|
-
footer: graphSpec.chrome.footer
|
|
12212
|
-
},
|
|
12213
|
-
theme,
|
|
12214
|
-
options.width,
|
|
12215
|
-
options.measureText,
|
|
12216
|
-
"full",
|
|
12217
|
-
void 0,
|
|
12218
|
-
watermark
|
|
12219
|
-
);
|
|
12271
|
+
const domain0 = align(min0, max0, zf0);
|
|
12272
|
+
const domain1 = align(min1, max1, zf1);
|
|
12273
|
+
return { domain0, domain1 };
|
|
12274
|
+
}
|
|
12275
|
+
function withYDomain(leaf, domain) {
|
|
12276
|
+
if (!leaf.encoding?.y) return leaf;
|
|
12220
12277
|
return {
|
|
12221
|
-
|
|
12222
|
-
|
|
12223
|
-
|
|
12224
|
-
|
|
12225
|
-
|
|
12226
|
-
|
|
12227
|
-
|
|
12228
|
-
|
|
12229
|
-
|
|
12230
|
-
|
|
12231
|
-
}
|
|
12232
|
-
simulationConfig,
|
|
12233
|
-
watermark
|
|
12278
|
+
...leaf,
|
|
12279
|
+
encoding: {
|
|
12280
|
+
...leaf.encoding,
|
|
12281
|
+
y: {
|
|
12282
|
+
...leaf.encoding.y,
|
|
12283
|
+
scale: {
|
|
12284
|
+
...leaf.encoding.y.scale,
|
|
12285
|
+
domain
|
|
12286
|
+
}
|
|
12287
|
+
}
|
|
12288
|
+
}
|
|
12234
12289
|
};
|
|
12235
12290
|
}
|
|
12236
|
-
|
|
12237
|
-
|
|
12238
|
-
|
|
12239
|
-
|
|
12240
|
-
|
|
12241
|
-
|
|
12242
|
-
|
|
12243
|
-
|
|
12244
|
-
|
|
12245
|
-
|
|
12291
|
+
function buildPrimarySpec(leaves, layerSpec) {
|
|
12292
|
+
const allData = leaves.flatMap((leaf) => leaf.data);
|
|
12293
|
+
const primary = {
|
|
12294
|
+
...leaves[0],
|
|
12295
|
+
data: allData,
|
|
12296
|
+
chrome: layerSpec.chrome ?? leaves[0].chrome,
|
|
12297
|
+
annotations: layerSpec.annotations ?? leaves[0].annotations,
|
|
12298
|
+
labels: layerSpec.labels ?? leaves[0].labels,
|
|
12299
|
+
legend: layerSpec.legend ?? leaves[0].legend,
|
|
12300
|
+
responsive: layerSpec.responsive ?? leaves[0].responsive,
|
|
12301
|
+
theme: layerSpec.theme ?? leaves[0].theme,
|
|
12302
|
+
darkMode: layerSpec.darkMode ?? leaves[0].darkMode,
|
|
12303
|
+
watermark: layerSpec.watermark ?? leaves[0].watermark,
|
|
12304
|
+
hiddenSeries: layerSpec.hiddenSeries ?? leaves[0].hiddenSeries,
|
|
12305
|
+
endpointLabels: layerSpec.endpointLabels ?? leaves[0].endpointLabels
|
|
12306
|
+
};
|
|
12307
|
+
return primary;
|
|
12308
|
+
}
|
|
12246
12309
|
|
|
12247
|
-
// src/
|
|
12248
|
-
import {
|
|
12249
|
-
var
|
|
12250
|
-
var
|
|
12251
|
-
|
|
12252
|
-
|
|
12310
|
+
// src/compile/watermark-obstacle.ts
|
|
12311
|
+
import { BRAND_RESERVE_WIDTH as BRAND_RESERVE_WIDTH2 } from "@opendata-ai/openchart-core";
|
|
12312
|
+
var WATERMARK_HEIGHT = 30;
|
|
12313
|
+
var X_AXIS_EXTENT_WITH_LABEL = 48;
|
|
12314
|
+
var X_AXIS_EXTENT_TICKS_ONLY = 26;
|
|
12315
|
+
function computeWatermarkObstacle(dims, watermark, axes, theme) {
|
|
12316
|
+
if (!watermark) return null;
|
|
12317
|
+
const chartArea = dims.chartArea;
|
|
12318
|
+
const brandPadding = theme.spacing.padding;
|
|
12319
|
+
const brandX = dims.total.width - brandPadding - BRAND_RESERVE_WIDTH2;
|
|
12320
|
+
const xAxisExtent = axes.x?.label ? X_AXIS_EXTENT_WITH_LABEL : axes.x ? X_AXIS_EXTENT_TICKS_ONLY : 0;
|
|
12321
|
+
const firstBottomChrome = dims.chrome.source ?? dims.chrome.byline ?? dims.chrome.footer;
|
|
12322
|
+
const brandY = firstBottomChrome ? chartArea.y + chartArea.height + xAxisExtent + firstBottomChrome.y : chartArea.y + chartArea.height + xAxisExtent + theme.spacing.chartToFooter;
|
|
12323
|
+
return { x: brandX, y: brandY, width: BRAND_RESERVE_WIDTH2, height: WATERMARK_HEIGHT };
|
|
12253
12324
|
}
|
|
12254
|
-
|
|
12255
|
-
|
|
12256
|
-
|
|
12257
|
-
|
|
12258
|
-
|
|
12259
|
-
|
|
12260
|
-
|
|
12261
|
-
const aBottom = sorted[i].position + labelHeight / 2;
|
|
12262
|
-
const bTop = sorted[i + 1].position - labelHeight / 2;
|
|
12263
|
-
if (aBottom + minGap > bTop) return true;
|
|
12264
|
-
}
|
|
12265
|
-
return false;
|
|
12325
|
+
|
|
12326
|
+
// src/compiler/sparkline-defaults.ts
|
|
12327
|
+
var TREND_DEADBAND = 5e-4;
|
|
12328
|
+
function computeTrend(values) {
|
|
12329
|
+
const finite = [];
|
|
12330
|
+
for (const v of values) {
|
|
12331
|
+
if (Number.isFinite(v)) finite.push(v);
|
|
12266
12332
|
}
|
|
12267
|
-
|
|
12268
|
-
|
|
12269
|
-
|
|
12270
|
-
|
|
12271
|
-
|
|
12272
|
-
|
|
12333
|
+
if (finite.length < 2) return "neutral";
|
|
12334
|
+
const n = finite.length;
|
|
12335
|
+
let sumX = 0;
|
|
12336
|
+
let sumY = 0;
|
|
12337
|
+
for (let i = 0; i < n; i++) {
|
|
12338
|
+
sumX += i;
|
|
12339
|
+
sumY += finite[i];
|
|
12273
12340
|
}
|
|
12274
|
-
|
|
12275
|
-
|
|
12276
|
-
|
|
12277
|
-
|
|
12278
|
-
let
|
|
12279
|
-
|
|
12280
|
-
|
|
12281
|
-
|
|
12282
|
-
thinned.push(current[i]);
|
|
12283
|
-
}
|
|
12284
|
-
if (current.length > 1) thinned.push(current[current.length - 1]);
|
|
12285
|
-
current = thinned;
|
|
12286
|
-
if (!ticksOverlap(current, fontSize, fontWeight, measureText, orientation)) break;
|
|
12341
|
+
const meanX = sumX / n;
|
|
12342
|
+
const meanY = sumY / n;
|
|
12343
|
+
let num = 0;
|
|
12344
|
+
let den = 0;
|
|
12345
|
+
for (let i = 0; i < n; i++) {
|
|
12346
|
+
const dx = i - meanX;
|
|
12347
|
+
num += dx * (finite[i] - meanY);
|
|
12348
|
+
den += dx * dx;
|
|
12287
12349
|
}
|
|
12288
|
-
return
|
|
12350
|
+
if (den === 0) return "neutral";
|
|
12351
|
+
const slope = num / den;
|
|
12352
|
+
const scale = Math.abs(meanY) || 1;
|
|
12353
|
+
const relSlope = slope / scale;
|
|
12354
|
+
if (Math.abs(relSlope) < TREND_DEADBAND) return "neutral";
|
|
12355
|
+
return relSlope > 0 ? "up" : "down";
|
|
12289
12356
|
}
|
|
12290
|
-
|
|
12291
|
-
|
|
12292
|
-
|
|
12293
|
-
|
|
12294
|
-
var WIDTH_MINIMAL_THRESHOLD = 150;
|
|
12295
|
-
var WIDTH_REDUCED_THRESHOLD = 300;
|
|
12296
|
-
var DENSITY_ORDER = ["full", "reduced", "minimal"];
|
|
12297
|
-
var GROUNDED_MARKS = /* @__PURE__ */ new Set(["bar", "tick", "lollipop"]);
|
|
12298
|
-
function effectiveDensity(baseDensity, axisLength, minimalThreshold, reducedThreshold) {
|
|
12299
|
-
let density = baseDensity;
|
|
12300
|
-
if (axisLength < minimalThreshold) {
|
|
12301
|
-
density = "minimal";
|
|
12302
|
-
} else if (axisLength < reducedThreshold) {
|
|
12303
|
-
const baseIdx = DENSITY_ORDER.indexOf(baseDensity);
|
|
12304
|
-
const reducedIdx = DENSITY_ORDER.indexOf("reduced");
|
|
12305
|
-
density = DENSITY_ORDER[Math.max(baseIdx, reducedIdx)];
|
|
12306
|
-
}
|
|
12307
|
-
return density;
|
|
12357
|
+
function trendColor(trend, theme) {
|
|
12358
|
+
if (trend === "up") return theme.colors.positive;
|
|
12359
|
+
if (trend === "down") return theme.colors.negative;
|
|
12360
|
+
return theme.colors.categorical[0];
|
|
12308
12361
|
}
|
|
12309
|
-
|
|
12310
|
-
|
|
12311
|
-
|
|
12312
|
-
|
|
12313
|
-
|
|
12314
|
-
|
|
12315
|
-
let best = ticks2;
|
|
12316
|
-
for (let n = Math.max(requestedCount, MIN_QUANTITATIVE_TICKS) + 1; n <= 10; n++) {
|
|
12317
|
-
const candidate = buildContinuousTicks(scale, n);
|
|
12318
|
-
if (candidate.length >= MIN_QUANTITATIVE_TICKS) return candidate;
|
|
12319
|
-
if (candidate.length > best.length) best = candidate;
|
|
12362
|
+
function computeTrendFromData(data, yField) {
|
|
12363
|
+
if (!yField) return "neutral";
|
|
12364
|
+
const values = [];
|
|
12365
|
+
for (const row of data) {
|
|
12366
|
+
const v = Number(row[yField]);
|
|
12367
|
+
if (Number.isFinite(v)) values.push(v);
|
|
12320
12368
|
}
|
|
12321
|
-
return
|
|
12369
|
+
return computeTrend(values);
|
|
12322
12370
|
}
|
|
12323
|
-
function
|
|
12324
|
-
if (
|
|
12325
|
-
|
|
12326
|
-
|
|
12327
|
-
|
|
12328
|
-
|
|
12329
|
-
if (!overshoots && !overlaps) return flooredTicks;
|
|
12330
|
-
const minThreshold = orientation === "vertical" ? HEIGHT_MINIMAL_THRESHOLD : WIDTH_MINIMAL_THRESHOLD;
|
|
12331
|
-
const floor = axisLength >= minThreshold ? CONTINUOUS_TICK_FLOOR : 2;
|
|
12332
|
-
let bestWithinFloor;
|
|
12333
|
-
for (let n = initialCount - 1; n >= 2; n--) {
|
|
12334
|
-
const candidate = buildContinuousTicks(scale, n);
|
|
12335
|
-
const candidateOvershoots = candidate.length > tolerance;
|
|
12336
|
-
const candidateOverlaps = ticksOverlap(
|
|
12337
|
-
candidate,
|
|
12338
|
-
fontSize,
|
|
12339
|
-
fontWeight,
|
|
12340
|
-
measureText,
|
|
12341
|
-
orientation
|
|
12342
|
-
);
|
|
12343
|
-
if (!candidateOvershoots && !candidateOverlaps) {
|
|
12344
|
-
return candidate;
|
|
12345
|
-
}
|
|
12346
|
-
if (candidate.length >= floor) bestWithinFloor = candidate;
|
|
12347
|
-
}
|
|
12348
|
-
const rawFallback = bestWithinFloor ?? buildContinuousTicks(scale, floor);
|
|
12349
|
-
const fallback = ensureMinContinuousTicks(scale, rawFallback, floor);
|
|
12350
|
-
return thinTicksUntilFit(fallback, fontSize, fontWeight, measureText, orientation);
|
|
12371
|
+
function hasExplicitColor(markDef, encodingHasColor) {
|
|
12372
|
+
if (encodingHasColor) return { fill: true, stroke: true };
|
|
12373
|
+
return {
|
|
12374
|
+
fill: markDef?.fill !== void 0,
|
|
12375
|
+
stroke: markDef?.stroke !== void 0
|
|
12376
|
+
};
|
|
12351
12377
|
}
|
|
12352
|
-
function
|
|
12353
|
-
|
|
12354
|
-
|
|
12355
|
-
|
|
12356
|
-
|
|
12357
|
-
|
|
12358
|
-
|
|
12359
|
-
|
|
12360
|
-
|
|
12361
|
-
|
|
12362
|
-
|
|
12363
|
-
}
|
|
12364
|
-
return false;
|
|
12378
|
+
function buildSparklineAreaGradient(baseColor) {
|
|
12379
|
+
return {
|
|
12380
|
+
gradient: "linear",
|
|
12381
|
+
x1: 0,
|
|
12382
|
+
y1: 0,
|
|
12383
|
+
x2: 0,
|
|
12384
|
+
y2: 1,
|
|
12385
|
+
stops: [
|
|
12386
|
+
{ offset: 0, color: baseColor, opacity: 0.35 },
|
|
12387
|
+
{ offset: 1, color: baseColor, opacity: 0 }
|
|
12388
|
+
]
|
|
12389
|
+
};
|
|
12365
12390
|
}
|
|
12366
|
-
|
|
12367
|
-
|
|
12368
|
-
|
|
12369
|
-
|
|
12370
|
-
|
|
12371
|
-
|
|
12372
|
-
|
|
12373
|
-
|
|
12374
|
-
|
|
12375
|
-
|
|
12376
|
-
|
|
12391
|
+
|
|
12392
|
+
// src/graphs/compile-graph.ts
|
|
12393
|
+
import { adaptTheme as adaptTheme2, computeChrome as computeChrome3, resolveTheme as resolveTheme3 } from "@opendata-ai/openchart-core";
|
|
12394
|
+
|
|
12395
|
+
// src/graphs/encoding.ts
|
|
12396
|
+
var DEFAULT_NODE_RADIUS = 5;
|
|
12397
|
+
var MIN_NODE_RADIUS = 3;
|
|
12398
|
+
var MAX_NODE_RADIUS = 12;
|
|
12399
|
+
var DEFAULT_EDGE_WIDTH = 1;
|
|
12400
|
+
var MIN_EDGE_WIDTH = 0.5;
|
|
12401
|
+
var MAX_EDGE_WIDTH = 4;
|
|
12402
|
+
var DEFAULT_STROKE_WIDTH2 = 1;
|
|
12403
|
+
function darkenColor(hex2, amount = 0.2) {
|
|
12404
|
+
const clean = hex2.replace(/^#/, "");
|
|
12405
|
+
if (clean.length !== 6 && clean.length !== 3) return hex2;
|
|
12406
|
+
const full = clean.length === 3 ? clean.split("").map((c) => c + c).join("") : clean;
|
|
12407
|
+
const r = Math.max(0, Math.round(parseInt(full.substring(0, 2), 16) * (1 - amount)));
|
|
12408
|
+
const g = Math.max(0, Math.round(parseInt(full.substring(2, 4), 16) * (1 - amount)));
|
|
12409
|
+
const b = Math.max(0, Math.round(parseInt(full.substring(4, 6), 16) * (1 - amount)));
|
|
12410
|
+
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
|
|
12411
|
+
}
|
|
12412
|
+
function hexWithOpacity(hex2, opacity) {
|
|
12413
|
+
const clean = hex2.replace(/^#/, "");
|
|
12414
|
+
if (clean.length !== 6 && clean.length !== 3) {
|
|
12415
|
+
return hex2;
|
|
12377
12416
|
}
|
|
12378
|
-
|
|
12417
|
+
const full = clean.length === 3 ? clean.split("").map((c) => c + c).join("") : clean;
|
|
12418
|
+
const r = parseInt(full.substring(0, 2), 16);
|
|
12419
|
+
const g = parseInt(full.substring(2, 4), 16);
|
|
12420
|
+
const b = parseInt(full.substring(4, 6), 16);
|
|
12421
|
+
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
|
12379
12422
|
}
|
|
12380
|
-
function
|
|
12381
|
-
const
|
|
12382
|
-
const
|
|
12383
|
-
|
|
12384
|
-
|
|
12385
|
-
|
|
12386
|
-
|
|
12387
|
-
|
|
12388
|
-
|
|
12389
|
-
|
|
12390
|
-
|
|
12391
|
-
|
|
12392
|
-
|
|
12393
|
-
|
|
12394
|
-
|
|
12395
|
-
|
|
12396
|
-
|
|
12397
|
-
|
|
12398
|
-
|
|
12399
|
-
|
|
12400
|
-
|
|
12401
|
-
|
|
12402
|
-
|
|
12403
|
-
|
|
12404
|
-
|
|
12405
|
-
|
|
12406
|
-
|
|
12407
|
-
|
|
12408
|
-
|
|
12409
|
-
|
|
12410
|
-
|
|
12411
|
-
|
|
12412
|
-
|
|
12413
|
-
|
|
12414
|
-
|
|
12415
|
-
|
|
12416
|
-
|
|
12417
|
-
|
|
12418
|
-
|
|
12419
|
-
|
|
12420
|
-
allTicks = categoricalTicks(scales.x, xDensity, "horizontal");
|
|
12423
|
+
function computeDegrees(nodes, edges) {
|
|
12424
|
+
const degrees = /* @__PURE__ */ new Map();
|
|
12425
|
+
for (const node of nodes) {
|
|
12426
|
+
degrees.set(node.id, 0);
|
|
12427
|
+
}
|
|
12428
|
+
for (const edge of edges) {
|
|
12429
|
+
degrees.set(edge.source, (degrees.get(edge.source) ?? 0) + 1);
|
|
12430
|
+
degrees.set(edge.target, (degrees.get(edge.target) ?? 0) + 1);
|
|
12431
|
+
}
|
|
12432
|
+
return degrees;
|
|
12433
|
+
}
|
|
12434
|
+
function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
|
|
12435
|
+
const degrees = computeDegrees(nodes, edges);
|
|
12436
|
+
const maxDegree = Math.max(1, ...degrees.values());
|
|
12437
|
+
let sizeScale;
|
|
12438
|
+
if (encoding.nodeSize?.field) {
|
|
12439
|
+
const field = encoding.nodeSize.field;
|
|
12440
|
+
const values = nodes.map((n) => Number(n[field])).filter((v) => Number.isFinite(v));
|
|
12441
|
+
const sizeMin = min2(values) ?? 0;
|
|
12442
|
+
const sizeMax = max2(values) ?? 1;
|
|
12443
|
+
sizeScale = sqrt2().domain([sizeMin, sizeMax]).range([MIN_NODE_RADIUS, MAX_NODE_RADIUS]);
|
|
12444
|
+
}
|
|
12445
|
+
let colorFn;
|
|
12446
|
+
if (encoding.nodeColor?.field) {
|
|
12447
|
+
const field = encoding.nodeColor.field;
|
|
12448
|
+
const fieldType = encoding.nodeColor.type ?? "nominal";
|
|
12449
|
+
const scaleConfig = encoding.nodeColor.scale;
|
|
12450
|
+
if (fieldType === "quantitative") {
|
|
12451
|
+
const values = nodes.map((n) => Number(n[field])).filter((v) => Number.isFinite(v));
|
|
12452
|
+
const colorMin = min2(values) ?? 0;
|
|
12453
|
+
const colorMax = max2(values) ?? 1;
|
|
12454
|
+
const seqPalettes = Object.values(theme.colors.sequential);
|
|
12455
|
+
const palette = seqPalettes.length > 0 ? seqPalettes[0] : ["#ccc", "#333"];
|
|
12456
|
+
const domain = scaleConfig?.domain && scaleConfig.domain.length === 2 ? scaleConfig.domain : [colorMin, colorMax];
|
|
12457
|
+
const range2 = scaleConfig?.range && scaleConfig.range.length >= 2 ? scaleConfig.range : [palette[0], palette[palette.length - 1]];
|
|
12458
|
+
const colorScale = linear2().domain(domain).range(range2);
|
|
12459
|
+
colorFn = (node) => {
|
|
12460
|
+
const val = Number(node[field]);
|
|
12461
|
+
return Number.isFinite(val) ? colorScale(val) : theme.colors.categorical[0];
|
|
12462
|
+
};
|
|
12421
12463
|
} else {
|
|
12422
|
-
|
|
12464
|
+
const domain = scaleConfig?.domain && Array.isArray(scaleConfig.domain) ? scaleConfig.domain : [...new Set(nodes.map((n) => String(n[field] ?? "")))];
|
|
12465
|
+
const range2 = scaleConfig?.range && scaleConfig.range.length > 0 ? scaleConfig.range : theme.colors.categorical;
|
|
12466
|
+
const ordinalScale = ordinal().domain(domain).range(range2);
|
|
12467
|
+
colorFn = (node) => ordinalScale(String(node[field] ?? ""));
|
|
12423
12468
|
}
|
|
12424
|
-
|
|
12425
|
-
|
|
12426
|
-
|
|
12427
|
-
|
|
12428
|
-
|
|
12429
|
-
|
|
12430
|
-
|
|
12431
|
-
|
|
12432
|
-
for (const t of allTicks) {
|
|
12433
|
-
const w = measureLabel(t.label, fontSize, fontWeight, measureText);
|
|
12434
|
-
if (w > maxLabelWidth) maxLabelWidth = w;
|
|
12469
|
+
}
|
|
12470
|
+
const defaultColor = theme.colors.categorical[0];
|
|
12471
|
+
return nodes.map((node) => {
|
|
12472
|
+
let radius = DEFAULT_NODE_RADIUS;
|
|
12473
|
+
if (sizeScale && encoding.nodeSize?.field) {
|
|
12474
|
+
const val = Number(node[encoding.nodeSize.field]);
|
|
12475
|
+
if (Number.isFinite(val)) {
|
|
12476
|
+
radius = sizeScale(val);
|
|
12435
12477
|
}
|
|
12436
|
-
tickAngle = resolveBandTickAngle(void 0, maxLabelWidth, bandwidth, allTicks.length);
|
|
12437
12478
|
}
|
|
12438
|
-
const
|
|
12439
|
-
|
|
12440
|
-
|
|
12441
|
-
|
|
12442
|
-
|
|
12443
|
-
|
|
12444
|
-
ticks2 = thinBandTicksIfNeeded(allTicks, effectiveAngle, fontSize, fontWeight, measureText);
|
|
12445
|
-
} else if (isContinuousX) {
|
|
12446
|
-
ticks2 = fitContinuousTicks(
|
|
12447
|
-
scales.x,
|
|
12448
|
-
allTicks,
|
|
12449
|
-
xTargetCount ?? allTicks.length,
|
|
12450
|
-
fontSize,
|
|
12451
|
-
fontWeight,
|
|
12452
|
-
chartArea.width,
|
|
12453
|
-
"horizontal",
|
|
12454
|
-
measureText
|
|
12455
|
-
);
|
|
12479
|
+
const fill = colorFn ? colorFn(node) : defaultColor;
|
|
12480
|
+
const stroke = darkenColor(fill);
|
|
12481
|
+
let label;
|
|
12482
|
+
if (encoding.nodeLabel?.field) {
|
|
12483
|
+
const labelVal = node[encoding.nodeLabel.field];
|
|
12484
|
+
label = labelVal != null ? String(labelVal) : void 0;
|
|
12456
12485
|
} else {
|
|
12457
|
-
|
|
12486
|
+
label = node.id;
|
|
12458
12487
|
}
|
|
12459
|
-
const
|
|
12460
|
-
const
|
|
12461
|
-
const
|
|
12462
|
-
const
|
|
12463
|
-
|
|
12464
|
-
|
|
12465
|
-
|
|
12466
|
-
|
|
12467
|
-
|
|
12468
|
-
|
|
12469
|
-
|
|
12470
|
-
|
|
12471
|
-
|
|
12472
|
-
|
|
12473
|
-
|
|
12474
|
-
|
|
12475
|
-
|
|
12476
|
-
|
|
12477
|
-
|
|
12478
|
-
|
|
12479
|
-
labelFlush: axisConfig?.labelFlush,
|
|
12480
|
-
tickPosition: xTickPosition
|
|
12488
|
+
const degree = degrees.get(node.id) ?? 0;
|
|
12489
|
+
const labelPriority = maxDegree > 0 ? degree / maxDegree : 0;
|
|
12490
|
+
const { id: _id, ...rest } = node;
|
|
12491
|
+
const data = { id: node.id, ...rest };
|
|
12492
|
+
const override = nodeOverrides?.[node.id];
|
|
12493
|
+
const finalFill = override?.fill ?? fill;
|
|
12494
|
+
const finalRadius = override?.radius ?? radius;
|
|
12495
|
+
const finalStrokeWidth = override?.strokeWidth ?? DEFAULT_STROKE_WIDTH2;
|
|
12496
|
+
const finalStroke = override?.stroke ?? stroke;
|
|
12497
|
+
const finalLabelPriority = override?.alwaysShowLabel ? Infinity : labelPriority;
|
|
12498
|
+
return {
|
|
12499
|
+
id: node.id,
|
|
12500
|
+
radius: finalRadius,
|
|
12501
|
+
fill: finalFill,
|
|
12502
|
+
stroke: finalStroke,
|
|
12503
|
+
strokeWidth: finalStrokeWidth,
|
|
12504
|
+
label,
|
|
12505
|
+
labelPriority: finalLabelPriority,
|
|
12506
|
+
community: void 0,
|
|
12507
|
+
data
|
|
12481
12508
|
};
|
|
12509
|
+
});
|
|
12510
|
+
}
|
|
12511
|
+
function resolveEdgeVisuals(edges, encoding, theme) {
|
|
12512
|
+
let widthScale;
|
|
12513
|
+
if (encoding.edgeWidth?.field) {
|
|
12514
|
+
const field = encoding.edgeWidth.field;
|
|
12515
|
+
const values = edges.map((e) => Number(e[field])).filter((v) => Number.isFinite(v));
|
|
12516
|
+
const widthMin = min2(values) ?? 0;
|
|
12517
|
+
const widthMax = max2(values) ?? 1;
|
|
12518
|
+
widthScale = linear2().domain([widthMin, widthMax]).range([MIN_EDGE_WIDTH, MAX_EDGE_WIDTH]);
|
|
12482
12519
|
}
|
|
12483
|
-
|
|
12484
|
-
|
|
12485
|
-
const
|
|
12486
|
-
const
|
|
12487
|
-
|
|
12488
|
-
if (
|
|
12489
|
-
|
|
12490
|
-
|
|
12491
|
-
|
|
12492
|
-
|
|
12493
|
-
const
|
|
12494
|
-
const
|
|
12495
|
-
|
|
12496
|
-
|
|
12497
|
-
|
|
12498
|
-
|
|
12499
|
-
|
|
12500
|
-
|
|
12520
|
+
let edgeColorFn;
|
|
12521
|
+
if (encoding.edgeColor?.field) {
|
|
12522
|
+
const field = encoding.edgeColor.field;
|
|
12523
|
+
const fieldType = encoding.edgeColor.type ?? "nominal";
|
|
12524
|
+
const scaleConfig = encoding.edgeColor.scale;
|
|
12525
|
+
if (fieldType === "quantitative") {
|
|
12526
|
+
const values = edges.map((e) => Number(e[field])).filter((v) => Number.isFinite(v));
|
|
12527
|
+
const colorMin = min2(values) ?? 0;
|
|
12528
|
+
const colorMax = max2(values) ?? 1;
|
|
12529
|
+
const seqPalettes = Object.values(theme.colors.sequential);
|
|
12530
|
+
const palette = seqPalettes.length > 0 ? seqPalettes[0] : ["#ccc", "#333"];
|
|
12531
|
+
const domain = scaleConfig?.domain && scaleConfig.domain.length === 2 ? scaleConfig.domain : [colorMin, colorMax];
|
|
12532
|
+
const range2 = scaleConfig?.range && scaleConfig.range.length >= 2 ? scaleConfig.range : [palette[0], palette[palette.length - 1]];
|
|
12533
|
+
const colorScale = linear2().domain(domain).range(range2);
|
|
12534
|
+
edgeColorFn = (edge) => {
|
|
12535
|
+
const val = Number(edge[field]);
|
|
12536
|
+
return Number.isFinite(val) ? colorScale(val) : hexWithOpacity(theme.colors.axis, 0.4);
|
|
12537
|
+
};
|
|
12501
12538
|
} else {
|
|
12502
|
-
|
|
12539
|
+
const domain = scaleConfig?.domain && Array.isArray(scaleConfig.domain) ? scaleConfig.domain : [...new Set(edges.map((e) => String(e[field] ?? "")))];
|
|
12540
|
+
const range2 = scaleConfig?.range && scaleConfig.range.length > 0 ? scaleConfig.range : theme.colors.categorical;
|
|
12541
|
+
const ordinalScale = ordinal().domain(domain).range(range2);
|
|
12542
|
+
edgeColorFn = (edge) => ordinalScale(String(edge[field] ?? ""));
|
|
12503
12543
|
}
|
|
12504
|
-
|
|
12505
|
-
|
|
12506
|
-
|
|
12507
|
-
|
|
12508
|
-
|
|
12509
|
-
|
|
12510
|
-
|
|
12511
|
-
|
|
12512
|
-
|
|
12513
|
-
|
|
12514
|
-
fontWeight,
|
|
12515
|
-
chartArea.height,
|
|
12516
|
-
"vertical",
|
|
12517
|
-
measureText
|
|
12518
|
-
);
|
|
12519
|
-
} else {
|
|
12520
|
-
ticks2 = thinTicksUntilFit(allTicks, fontSize, fontWeight, measureText, "vertical");
|
|
12544
|
+
}
|
|
12545
|
+
const defaultEdgeColor = hexWithOpacity(theme.colors.axis, 0.4);
|
|
12546
|
+
const EDGE_STYLES = ["solid", "dashed", "dotted"];
|
|
12547
|
+
let styleFn;
|
|
12548
|
+
if (encoding.edgeStyle?.field) {
|
|
12549
|
+
const field = encoding.edgeStyle.field;
|
|
12550
|
+
const uniqueValues = [...new Set(edges.map((e) => String(e[field] ?? "")))];
|
|
12551
|
+
const styleMap = /* @__PURE__ */ new Map();
|
|
12552
|
+
for (let i = 0; i < uniqueValues.length; i++) {
|
|
12553
|
+
styleMap.set(uniqueValues[i], EDGE_STYLES[i % EDGE_STYLES.length]);
|
|
12521
12554
|
}
|
|
12522
|
-
|
|
12523
|
-
|
|
12524
|
-
|
|
12525
|
-
}
|
|
12526
|
-
|
|
12527
|
-
|
|
12528
|
-
|
|
12529
|
-
|
|
12530
|
-
|
|
12531
|
-
|
|
12532
|
-
|
|
12533
|
-
const
|
|
12534
|
-
|
|
12535
|
-
|
|
12536
|
-
|
|
12537
|
-
|
|
12538
|
-
|
|
12539
|
-
|
|
12540
|
-
|
|
12541
|
-
|
|
12542
|
-
start: { x: chartArea.x, y: chartArea.y },
|
|
12543
|
-
end: { x: chartArea.x, y: chartArea.y + chartArea.height },
|
|
12544
|
-
orient: axisConfig?.orient,
|
|
12545
|
-
domainLine: yDomainLine,
|
|
12546
|
-
tickMarks: yTickMarks,
|
|
12547
|
-
offset: axisConfig?.offset,
|
|
12548
|
-
titlePadding: axisConfig?.titlePadding,
|
|
12549
|
-
labelPadding: axisConfig?.labelPadding,
|
|
12550
|
-
labelOverlap: axisConfig?.labelOverlap,
|
|
12551
|
-
labelFlush: axisConfig?.labelFlush,
|
|
12552
|
-
tickPosition: yTickPosition
|
|
12555
|
+
styleFn = (edge) => styleMap.get(String(edge[field] ?? "")) ?? "solid";
|
|
12556
|
+
}
|
|
12557
|
+
return edges.map((edge) => {
|
|
12558
|
+
const { source, target, ...rest } = edge;
|
|
12559
|
+
let strokeWidth = DEFAULT_EDGE_WIDTH;
|
|
12560
|
+
if (widthScale && encoding.edgeWidth?.field) {
|
|
12561
|
+
const val = Number(edge[encoding.edgeWidth.field]);
|
|
12562
|
+
if (Number.isFinite(val)) {
|
|
12563
|
+
strokeWidth = widthScale(val);
|
|
12564
|
+
}
|
|
12565
|
+
}
|
|
12566
|
+
const stroke = edgeColorFn ? edgeColorFn(edge) : defaultEdgeColor;
|
|
12567
|
+
const style = styleFn ? styleFn(edge) : "solid";
|
|
12568
|
+
return {
|
|
12569
|
+
source,
|
|
12570
|
+
target,
|
|
12571
|
+
stroke,
|
|
12572
|
+
strokeWidth,
|
|
12573
|
+
style,
|
|
12574
|
+
data: { source, target, ...rest }
|
|
12553
12575
|
};
|
|
12576
|
+
});
|
|
12577
|
+
}
|
|
12578
|
+
|
|
12579
|
+
// src/graphs/community.ts
|
|
12580
|
+
function assignCommunities(nodes, clusteringField) {
|
|
12581
|
+
if (!clusteringField) return;
|
|
12582
|
+
for (const node of nodes) {
|
|
12583
|
+
const value2 = node.data[clusteringField];
|
|
12584
|
+
node.community = value2 != null ? String(value2) : void 0;
|
|
12554
12585
|
}
|
|
12555
|
-
|
|
12556
|
-
|
|
12557
|
-
|
|
12558
|
-
|
|
12559
|
-
|
|
12560
|
-
|
|
12561
|
-
|
|
12562
|
-
|
|
12563
|
-
|
|
12564
|
-
xAxisHeight: theme.spacing.xAxisHeight,
|
|
12565
|
-
measure: (t, fs, fw) => measureLabel(t, fs, fw, measureText)
|
|
12566
|
-
});
|
|
12567
|
-
if (result.x.label) {
|
|
12568
|
-
const rotatedBand = isRotated ? result.x.extent - X_AXIS_TITLE_BAND_ROTATED + 14 : 35;
|
|
12569
|
-
result.x.titlePosition = {
|
|
12570
|
-
x: chartArea.x + chartArea.width / 2,
|
|
12571
|
-
y: chartArea.y + chartArea.height + rotatedBand
|
|
12572
|
-
};
|
|
12586
|
+
}
|
|
12587
|
+
function buildCommunityColorMap(nodes, theme) {
|
|
12588
|
+
const colorMap = /* @__PURE__ */ new Map();
|
|
12589
|
+
const palette = theme.colors.categorical;
|
|
12590
|
+
let colorIndex = 0;
|
|
12591
|
+
for (const node of nodes) {
|
|
12592
|
+
if (node.community != null && !colorMap.has(node.community)) {
|
|
12593
|
+
colorMap.set(node.community, palette[colorIndex % palette.length]);
|
|
12594
|
+
colorIndex++;
|
|
12573
12595
|
}
|
|
12574
12596
|
}
|
|
12575
|
-
|
|
12576
|
-
|
|
12577
|
-
|
|
12578
|
-
|
|
12579
|
-
|
|
12580
|
-
|
|
12581
|
-
|
|
12582
|
-
|
|
12583
|
-
|
|
12597
|
+
return colorMap;
|
|
12598
|
+
}
|
|
12599
|
+
function applyCommunityColors(nodes, colorMap) {
|
|
12600
|
+
for (const node of nodes) {
|
|
12601
|
+
if (node.community != null) {
|
|
12602
|
+
const communityColor = colorMap.get(node.community);
|
|
12603
|
+
if (communityColor) {
|
|
12604
|
+
node.fill = communityColor;
|
|
12605
|
+
node.stroke = darkenColor(communityColor);
|
|
12606
|
+
}
|
|
12607
|
+
}
|
|
12608
|
+
}
|
|
12609
|
+
}
|
|
12610
|
+
|
|
12611
|
+
// src/graphs/compile-graph.ts
|
|
12612
|
+
var SWATCH_SIZE2 = 12;
|
|
12613
|
+
var SWATCH_GAP2 = 6;
|
|
12614
|
+
var ENTRY_GAP2 = 16;
|
|
12615
|
+
function buildGraphLegend(nodes, communityColorMap, hasCommunities, theme, nodeColorField) {
|
|
12616
|
+
const labelStyle = {
|
|
12617
|
+
fontFamily: theme.fonts.family,
|
|
12618
|
+
fontSize: theme.fonts.sizes.small,
|
|
12619
|
+
fontWeight: theme.fonts.weights.normal,
|
|
12620
|
+
fill: theme.colors.text,
|
|
12621
|
+
lineHeight: 1.3
|
|
12622
|
+
};
|
|
12623
|
+
let entries;
|
|
12624
|
+
if (hasCommunities && communityColorMap.size > 0) {
|
|
12625
|
+
entries = [...communityColorMap.entries()].map(([label, color2]) => ({
|
|
12626
|
+
label,
|
|
12627
|
+
color: color2,
|
|
12628
|
+
shape: "circle",
|
|
12629
|
+
active: true
|
|
12630
|
+
}));
|
|
12631
|
+
} else {
|
|
12632
|
+
const categoryColors = /* @__PURE__ */ new Map();
|
|
12633
|
+
for (const node of nodes) {
|
|
12634
|
+
const category = nodeColorField ? String(node.data[nodeColorField] ?? node.label ?? node.id) : node.label ?? node.id;
|
|
12635
|
+
if (!categoryColors.has(category)) {
|
|
12636
|
+
categoryColors.set(category, node.fill);
|
|
12637
|
+
}
|
|
12638
|
+
}
|
|
12639
|
+
if (categoryColors.size <= 1) {
|
|
12640
|
+
entries = [];
|
|
12584
12641
|
} else {
|
|
12585
|
-
|
|
12586
|
-
|
|
12587
|
-
|
|
12588
|
-
|
|
12589
|
-
|
|
12590
|
-
|
|
12591
|
-
return Math.max(max4, w);
|
|
12592
|
-
}, 0);
|
|
12593
|
-
const titleOff = axisTitleOffset2(maxTickLabelWidth, result.y.labelStyle.fontSize, totalWidth);
|
|
12594
|
-
result.y.titlePosition = {
|
|
12595
|
-
x: chartArea.x - titleOff,
|
|
12596
|
-
y: chartArea.y + chartArea.height / 2,
|
|
12597
|
-
angle: -90
|
|
12598
|
-
};
|
|
12642
|
+
entries = [...categoryColors.entries()].map(([label, color2]) => ({
|
|
12643
|
+
label,
|
|
12644
|
+
color: color2,
|
|
12645
|
+
shape: "circle",
|
|
12646
|
+
active: true
|
|
12647
|
+
}));
|
|
12599
12648
|
}
|
|
12600
12649
|
}
|
|
12601
|
-
return
|
|
12650
|
+
return {
|
|
12651
|
+
position: "top",
|
|
12652
|
+
entries,
|
|
12653
|
+
bounds: { x: 0, y: 0, width: 0, height: 0 },
|
|
12654
|
+
labelStyle,
|
|
12655
|
+
swatchSize: SWATCH_SIZE2,
|
|
12656
|
+
swatchGap: SWATCH_GAP2,
|
|
12657
|
+
entryGap: ENTRY_GAP2,
|
|
12658
|
+
swatchChipFill: theme.colors.annotationFill
|
|
12659
|
+
};
|
|
12660
|
+
}
|
|
12661
|
+
function buildGraphTooltips(nodes) {
|
|
12662
|
+
const descriptors = /* @__PURE__ */ new Map();
|
|
12663
|
+
for (const node of nodes) {
|
|
12664
|
+
const fields = [];
|
|
12665
|
+
if (node.community != null) {
|
|
12666
|
+
fields.push({
|
|
12667
|
+
label: "Community",
|
|
12668
|
+
value: node.community,
|
|
12669
|
+
color: node.fill
|
|
12670
|
+
});
|
|
12671
|
+
}
|
|
12672
|
+
for (const [key, value2] of Object.entries(node.data)) {
|
|
12673
|
+
if (key === "id") continue;
|
|
12674
|
+
if (value2 == null) continue;
|
|
12675
|
+
fields.push({
|
|
12676
|
+
label: key,
|
|
12677
|
+
value: typeof value2 === "number" ? value2.toLocaleString() : String(value2)
|
|
12678
|
+
});
|
|
12679
|
+
}
|
|
12680
|
+
descriptors.set(node.id, {
|
|
12681
|
+
title: node.label ?? node.id,
|
|
12682
|
+
fields
|
|
12683
|
+
});
|
|
12684
|
+
}
|
|
12685
|
+
return descriptors;
|
|
12686
|
+
}
|
|
12687
|
+
function compileGraph(spec, options) {
|
|
12688
|
+
const { spec: normalized } = compile(spec);
|
|
12689
|
+
if (!("type" in normalized) || normalized.type !== "graph") {
|
|
12690
|
+
throw new Error(
|
|
12691
|
+
"compileGraph received a non-graph spec. Use compileChart or compileTable instead."
|
|
12692
|
+
);
|
|
12693
|
+
}
|
|
12694
|
+
const graphSpec = normalized;
|
|
12695
|
+
const rawWatermark = spec.watermark;
|
|
12696
|
+
const watermark = rawWatermark !== void 0 ? graphSpec.watermark : options.watermark ?? true;
|
|
12697
|
+
const mergedThemeConfig = options.theme ? { ...graphSpec.theme, ...options.theme } : graphSpec.theme;
|
|
12698
|
+
let theme = resolveTheme3(mergedThemeConfig);
|
|
12699
|
+
if (options.darkMode) {
|
|
12700
|
+
theme = adaptTheme2(theme);
|
|
12701
|
+
}
|
|
12702
|
+
const compiledNodes = resolveNodeVisuals(
|
|
12703
|
+
graphSpec.nodes,
|
|
12704
|
+
graphSpec.encoding,
|
|
12705
|
+
graphSpec.edges,
|
|
12706
|
+
theme,
|
|
12707
|
+
graphSpec.nodeOverrides
|
|
12708
|
+
);
|
|
12709
|
+
const clusteringField = graphSpec.layout.clustering?.field;
|
|
12710
|
+
const hasCommunities = !!clusteringField;
|
|
12711
|
+
assignCommunities(compiledNodes, clusteringField);
|
|
12712
|
+
const hasNodeColorEncoding = !!graphSpec.encoding.nodeColor?.field;
|
|
12713
|
+
let communityColorMap = /* @__PURE__ */ new Map();
|
|
12714
|
+
if (hasCommunities && !hasNodeColorEncoding) {
|
|
12715
|
+
communityColorMap = buildCommunityColorMap(compiledNodes, theme);
|
|
12716
|
+
applyCommunityColors(compiledNodes, communityColorMap);
|
|
12717
|
+
}
|
|
12718
|
+
const compiledEdges = resolveEdgeVisuals(graphSpec.edges, graphSpec.encoding, theme);
|
|
12719
|
+
const useCommunitiesForLegend = hasCommunities && !hasNodeColorEncoding;
|
|
12720
|
+
const legend = buildGraphLegend(
|
|
12721
|
+
compiledNodes,
|
|
12722
|
+
communityColorMap,
|
|
12723
|
+
useCommunitiesForLegend,
|
|
12724
|
+
theme,
|
|
12725
|
+
graphSpec.encoding.nodeColor?.field
|
|
12726
|
+
);
|
|
12727
|
+
const tooltipDescriptors = buildGraphTooltips(compiledNodes);
|
|
12728
|
+
const communityCount = communityColorMap.size;
|
|
12729
|
+
const altParts = [
|
|
12730
|
+
`Network graph with ${compiledNodes.length} nodes and ${compiledEdges.length} edges`
|
|
12731
|
+
];
|
|
12732
|
+
if (communityCount > 0) {
|
|
12733
|
+
altParts.push(`organized into ${communityCount} communities`);
|
|
12734
|
+
}
|
|
12735
|
+
const a11y = {
|
|
12736
|
+
altText: altParts.join(", "),
|
|
12737
|
+
dataTableFallback: compiledNodes.map((n) => [n.id, n.community ?? "", n.label ?? ""]),
|
|
12738
|
+
role: "img",
|
|
12739
|
+
keyboardNavigable: compiledNodes.length > 0
|
|
12740
|
+
};
|
|
12741
|
+
const collisionPadding = graphSpec.layout.collisionPadding ?? 2;
|
|
12742
|
+
const maxRadius = compiledNodes.length > 0 ? Math.max(...compiledNodes.map((n) => n.radius)) : DEFAULT_COLLISION_PADDING;
|
|
12743
|
+
const simulationConfig = {
|
|
12744
|
+
chargeStrength: graphSpec.layout.chargeStrength ?? -300,
|
|
12745
|
+
linkDistance: graphSpec.layout.linkDistance ?? 30,
|
|
12746
|
+
clustering: clusteringField ? { field: clusteringField, strength: 0.5 } : null,
|
|
12747
|
+
alphaDecay: 0.0228,
|
|
12748
|
+
velocityDecay: 0.4,
|
|
12749
|
+
collisionRadius: maxRadius + collisionPadding,
|
|
12750
|
+
collisionPadding,
|
|
12751
|
+
linkStrength: graphSpec.layout.linkStrength,
|
|
12752
|
+
centerForce: graphSpec.layout.centerForce
|
|
12753
|
+
};
|
|
12754
|
+
const chrome = computeChrome3(
|
|
12755
|
+
{
|
|
12756
|
+
title: graphSpec.chrome.title,
|
|
12757
|
+
subtitle: graphSpec.chrome.subtitle,
|
|
12758
|
+
source: graphSpec.chrome.source,
|
|
12759
|
+
byline: graphSpec.chrome.byline,
|
|
12760
|
+
footer: graphSpec.chrome.footer
|
|
12761
|
+
},
|
|
12762
|
+
theme,
|
|
12763
|
+
options.width,
|
|
12764
|
+
options.measureText,
|
|
12765
|
+
"full",
|
|
12766
|
+
void 0,
|
|
12767
|
+
watermark
|
|
12768
|
+
);
|
|
12769
|
+
return {
|
|
12770
|
+
nodes: compiledNodes,
|
|
12771
|
+
edges: compiledEdges,
|
|
12772
|
+
legend,
|
|
12773
|
+
chrome,
|
|
12774
|
+
tooltipDescriptors,
|
|
12775
|
+
a11y,
|
|
12776
|
+
theme,
|
|
12777
|
+
dimensions: {
|
|
12778
|
+
width: options.width,
|
|
12779
|
+
height: options.height
|
|
12780
|
+
},
|
|
12781
|
+
simulationConfig,
|
|
12782
|
+
watermark
|
|
12783
|
+
};
|
|
12602
12784
|
}
|
|
12785
|
+
var DEFAULT_COLLISION_PADDING = 5;
|
|
12603
12786
|
|
|
12604
12787
|
// src/layout/dimensions.ts
|
|
12605
12788
|
import {
|
|
@@ -12780,12 +12963,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
12780
12963
|
const base = theme.spacing.xAxisHeight;
|
|
12781
12964
|
xAxisHeight = hasXAxisLabel ? base + 22 : base;
|
|
12782
12965
|
}
|
|
12783
|
-
const
|
|
12784
|
-
const yTickPositionExplicitPre = yAxisCfgPre?.tickPosition;
|
|
12785
|
-
const yIsContinuousPre = encoding.y?.type === "quantitative" || encoding.y?.type === "temporal";
|
|
12786
|
-
const yIsLineOrAreaPre = spec.markType === "line" || spec.markType === "area";
|
|
12787
|
-
const yAxisOrientPre = yAxisCfgPre?.orient;
|
|
12788
|
-
const yIsInlinePre = (yTickPositionExplicitPre ?? (yIsLineOrAreaPre && yIsContinuousPre && yAxisOrientPre !== "right" ? "inline" : "gutter")) === "inline";
|
|
12966
|
+
const yIsInlinePre = yTickPositionIsInline(encoding.y, spec.markType);
|
|
12789
12967
|
const inlineTickOverhang = yIsInlinePre ? theme.fonts.sizes.axisTick + INLINE_TICK_OVERHANG_PAD : 0;
|
|
12790
12968
|
const topAxisGap = isRadial && chrome.topHeight === 0 ? 0 : axisMargin + inlineTickOverhang;
|
|
12791
12969
|
const topPad = width < NARROW_VIEWPORT_MAX3 ? padding + TOP_PAD_EXTRA_NARROW2 : padding;
|
|
@@ -12938,7 +13116,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
12938
13116
|
const yFieldForTitle = encoding.y?.field;
|
|
12939
13117
|
const yAxisFormatForTitle = yAxis?.format;
|
|
12940
13118
|
let estTickLabelWidth = 0;
|
|
12941
|
-
if (yFieldForTitle && (encoding.y?.type === "quantitative" || encoding.y?.type === "temporal")) {
|
|
13119
|
+
if (yFieldForTitle && !yIsInlinePre && (encoding.y?.type === "quantitative" || encoding.y?.type === "temporal")) {
|
|
12942
13120
|
let maxAbsValForTitle = 0;
|
|
12943
13121
|
for (const row of spec.data) {
|
|
12944
13122
|
const v = Number(row[yFieldForTitle]);
|
|
@@ -12968,7 +13146,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
12968
13146
|
);
|
|
12969
13147
|
}
|
|
12970
13148
|
const titleFontSize = theme.fonts.sizes.body;
|
|
12971
|
-
const offset = axisTitleOffset3(estTickLabelWidth, titleFontSize, width);
|
|
13149
|
+
const offset = axisTitleOffset3(estTickLabelWidth, titleFontSize, width, yIsInlinePre);
|
|
12972
13150
|
const halfGlyph = Math.ceil(titleFontSize / 2);
|
|
12973
13151
|
const rotatedLabelMargin = offset + halfGlyph + (width < BREAKPOINT_COMPACT_MAX3 ? 0 : AXIS_TITLE_TRAILING_PAD3);
|
|
12974
13152
|
margins.left = Math.max(margins.left, hPad + rotatedLabelMargin);
|
|
@@ -14782,6 +14960,9 @@ import {
|
|
|
14782
14960
|
computeChrome as computeChrome7,
|
|
14783
14961
|
estimateTextWidth as estimateTextWidth20,
|
|
14784
14962
|
formatNumber as formatNumber8,
|
|
14963
|
+
getBreakpoint,
|
|
14964
|
+
HPAD_COMPACT_FRACTION as HPAD_COMPACT_FRACTION3,
|
|
14965
|
+
HPAD_COMPACT_MIN as HPAD_COMPACT_MIN3,
|
|
14785
14966
|
pickLabelColor as pickLabelColor2,
|
|
14786
14967
|
resolveTheme as resolveTheme5,
|
|
14787
14968
|
SEQUENTIAL_PALETTES
|
|
@@ -14791,6 +14972,9 @@ var TILE_STROKE_WIDTH = 1;
|
|
|
14791
14972
|
var LEGEND_SWATCH_SIZE = 10;
|
|
14792
14973
|
var LEGEND_SWATCH_GAP = 6;
|
|
14793
14974
|
var LEGEND_ENTRY_GAP = 16;
|
|
14975
|
+
function clamp(value2, min4, max4) {
|
|
14976
|
+
return Math.min(max4, Math.max(min4, value2));
|
|
14977
|
+
}
|
|
14794
14978
|
function compileTileMap(spec, options) {
|
|
14795
14979
|
const { spec: normalized } = compile(spec);
|
|
14796
14980
|
if (!("type" in normalized) || normalized.type !== "tilemap") {
|
|
@@ -14823,7 +15007,7 @@ function compileTileMap(spec, options) {
|
|
|
14823
15007
|
void 0,
|
|
14824
15008
|
watermark
|
|
14825
15009
|
);
|
|
14826
|
-
const padding = theme.spacing.padding;
|
|
15010
|
+
const padding = getBreakpoint(options.width) === "compact" ? Math.max(HPAD_COMPACT_MIN3, Math.round(theme.spacing.padding * HPAD_COMPACT_FRACTION3)) : theme.spacing.padding;
|
|
14827
15011
|
const fullArea = {
|
|
14828
15012
|
x: padding,
|
|
14829
15013
|
y: padding + chrome.topHeight,
|
|
@@ -14936,7 +15120,7 @@ function compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDa
|
|
|
14936
15120
|
keyboardNavigable: tiles.length > 0
|
|
14937
15121
|
};
|
|
14938
15122
|
const resolvedAnimation = resolveAnimation(tilemapSpec.animation);
|
|
14939
|
-
const padding =
|
|
15123
|
+
const padding = fullArea.x;
|
|
14940
15124
|
const contentHeight = tileGridOffsetY + tilePositions.gridHeight + legendGap2 + legendTotalHeight + chrome.bottomHeight + padding;
|
|
14941
15125
|
return {
|
|
14942
15126
|
area: fullArea,
|
|
@@ -14991,16 +15175,20 @@ function compileCategorical(tilemapSpec, options, theme, chrome, fullArea, isDar
|
|
|
14991
15175
|
}
|
|
14992
15176
|
}
|
|
14993
15177
|
const showLegend = tilemapSpec.legend?.show !== false;
|
|
15178
|
+
const legendLabelFontSize = 11;
|
|
14994
15179
|
const legendRowHeight = LEGEND_SWATCH_SIZE + 6;
|
|
14995
|
-
const
|
|
15180
|
+
const legendLabels = categories.map(formatCategoryLabel);
|
|
14996
15181
|
const legendGap2 = showLegend ? 8 : 0;
|
|
14997
|
-
const
|
|
15182
|
+
const estimatedRows = showLegend ? countLegendRows(legendLabels, fullArea.width, legendLabelFontSize) : 0;
|
|
15183
|
+
const tileAreaHeight = fullArea.height - legendRowHeight * estimatedRows - legendGap2;
|
|
14998
15184
|
const tilePositions = computeTilePositions(fullArea.width, tileAreaHeight, 5);
|
|
14999
15185
|
const tileGridOffsetX = fullArea.x + (fullArea.width - tilePositions.gridWidth) / 2;
|
|
15000
15186
|
const tileGridOffsetY = fullArea.y;
|
|
15001
15187
|
const legendX = tileGridOffsetX;
|
|
15002
15188
|
const legendY = tileGridOffsetY + tilePositions.gridHeight + legendGap2;
|
|
15003
15189
|
const legendWidth = tilePositions.gridWidth;
|
|
15190
|
+
const legendRows = showLegend ? countLegendRows(legendLabels, legendWidth, legendLabelFontSize) : 0;
|
|
15191
|
+
const legendTotalHeight = showLegend ? legendRowHeight * legendRows : 0;
|
|
15004
15192
|
const neutralFill = isDarkMode ? "#1e2a30" : "#e0e0e0";
|
|
15005
15193
|
const neutralStroke = isDarkMode ? "rgba(255,255,255,0.08)" : "#d0d0d0";
|
|
15006
15194
|
const tiles = [];
|
|
@@ -15036,7 +15224,7 @@ function compileCategorical(tilemapSpec, options, theme, chrome, fullArea, isDar
|
|
|
15036
15224
|
if (showLegend) {
|
|
15037
15225
|
const labelStyle = {
|
|
15038
15226
|
fontFamily: theme.fonts.family,
|
|
15039
|
-
fontSize:
|
|
15227
|
+
fontSize: legendLabelFontSize,
|
|
15040
15228
|
fontWeight: 400,
|
|
15041
15229
|
fill: theme.colors.text,
|
|
15042
15230
|
lineHeight: 1.2
|
|
@@ -15049,7 +15237,7 @@ function compileCategorical(tilemapSpec, options, theme, chrome, fullArea, isDar
|
|
|
15049
15237
|
categoricalLegend = {
|
|
15050
15238
|
type: "categorical",
|
|
15051
15239
|
position: "bottom",
|
|
15052
|
-
bounds: { x: legendX, y: legendY, width: legendWidth, height:
|
|
15240
|
+
bounds: { x: legendX, y: legendY, width: legendWidth, height: legendTotalHeight },
|
|
15053
15241
|
labelStyle,
|
|
15054
15242
|
entries,
|
|
15055
15243
|
swatchSize: LEGEND_SWATCH_SIZE,
|
|
@@ -15067,7 +15255,7 @@ function compileCategorical(tilemapSpec, options, theme, chrome, fullArea, isDar
|
|
|
15067
15255
|
keyboardNavigable: tiles.length > 0
|
|
15068
15256
|
};
|
|
15069
15257
|
const resolvedAnimation = resolveAnimation(tilemapSpec.animation);
|
|
15070
|
-
const padding =
|
|
15258
|
+
const padding = fullArea.x;
|
|
15071
15259
|
const contentHeight = tileGridOffsetY + tilePositions.gridHeight + legendGap2 + legendTotalHeight + chrome.bottomHeight + padding;
|
|
15072
15260
|
return {
|
|
15073
15261
|
area: fullArea,
|
|
@@ -15106,27 +15294,32 @@ function buildTileMark(opts) {
|
|
|
15106
15294
|
const tileTopY = gridOffsetY + pos.y;
|
|
15107
15295
|
const textColor = hasData ? pickLabelColor2(fill, isDarkMode) : "#ffffff";
|
|
15108
15296
|
const isLightText = textColor === "#ffffff";
|
|
15297
|
+
const showValue = tileSize >= 24;
|
|
15298
|
+
const codeFontSize = clamp(Math.round(tileSize * 0.5 * 10) / 10, 6.5, 11);
|
|
15299
|
+
const valueFontSize = clamp(Math.round(tileSize * 0.42 * 10) / 10, 6.5, 10);
|
|
15109
15300
|
const labelStyle = {
|
|
15110
15301
|
fontFamily: theme.fonts.family,
|
|
15111
|
-
fontSize:
|
|
15302
|
+
fontSize: codeFontSize,
|
|
15112
15303
|
fontWeight: 700,
|
|
15113
15304
|
fill: textColor,
|
|
15114
15305
|
lineHeight: 1.2
|
|
15115
15306
|
};
|
|
15116
15307
|
const valueLabelStyle = {
|
|
15117
15308
|
fontFamily: theme.fonts.family,
|
|
15118
|
-
fontSize:
|
|
15309
|
+
fontSize: valueFontSize,
|
|
15119
15310
|
fontWeight: 300,
|
|
15120
15311
|
fill: isLightText ? "rgba(255,255,255,0.6)" : "rgba(0,0,0,0.45)",
|
|
15121
15312
|
lineHeight: 1.2
|
|
15122
15313
|
};
|
|
15123
|
-
const
|
|
15314
|
+
const codeCenterY = showValue ? tileTopY + tileSize * 0.28 : tileTopY + tileSize * 0.5;
|
|
15315
|
+
const valueLabel = showValue ? {
|
|
15124
15316
|
text: formattedValue,
|
|
15125
15317
|
x: tileCenterX,
|
|
15126
15318
|
y: tileTopY + tileSize * 0.78,
|
|
15127
15319
|
style: valueLabelStyle,
|
|
15128
15320
|
visible: true
|
|
15129
|
-
};
|
|
15321
|
+
} : { text: "", x: 0, y: 0, style: valueLabelStyle, visible: false };
|
|
15322
|
+
const cornerRadius = Math.min(TILE_CORNER_RADIUS, Math.round(tileSize * 0.22));
|
|
15130
15323
|
const data = {
|
|
15131
15324
|
state: stateCode,
|
|
15132
15325
|
value: value2,
|
|
@@ -15145,14 +15338,14 @@ function buildTileMark(opts) {
|
|
|
15145
15338
|
fillOpacity,
|
|
15146
15339
|
stroke,
|
|
15147
15340
|
strokeWidth: TILE_STROKE_WIDTH,
|
|
15148
|
-
cornerRadius
|
|
15341
|
+
cornerRadius,
|
|
15149
15342
|
value: value2,
|
|
15150
15343
|
formattedValue,
|
|
15151
15344
|
hasData,
|
|
15152
15345
|
label: {
|
|
15153
15346
|
text: stateCode,
|
|
15154
15347
|
x: tileCenterX,
|
|
15155
|
-
y:
|
|
15348
|
+
y: codeCenterY,
|
|
15156
15349
|
style: labelStyle,
|
|
15157
15350
|
visible: true
|
|
15158
15351
|
},
|
|
@@ -15191,6 +15384,22 @@ function buildTooltips(tiles, fieldLabel = "Value") {
|
|
|
15191
15384
|
function formatCategoryLabel(category) {
|
|
15192
15385
|
return category.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
15193
15386
|
}
|
|
15387
|
+
function countLegendRows(labels, availableWidth, fontSize) {
|
|
15388
|
+
if (labels.length === 0) return 0;
|
|
15389
|
+
let rows = 1;
|
|
15390
|
+
let offsetX = 0;
|
|
15391
|
+
for (let i = 0; i < labels.length; i++) {
|
|
15392
|
+
const labelWidth = estimateTextWidth20(labels[i], fontSize);
|
|
15393
|
+
const entryWidth = LEGEND_SWATCH_SIZE + LEGEND_SWATCH_GAP + labelWidth + LEGEND_ENTRY_GAP;
|
|
15394
|
+
if (i > 0 && offsetX + entryWidth > availableWidth) {
|
|
15395
|
+
rows++;
|
|
15396
|
+
offsetX = entryWidth;
|
|
15397
|
+
} else {
|
|
15398
|
+
offsetX += entryWidth;
|
|
15399
|
+
}
|
|
15400
|
+
}
|
|
15401
|
+
return rows;
|
|
15402
|
+
}
|
|
15194
15403
|
function emptyLayout4(chrome, theme, options, watermark) {
|
|
15195
15404
|
return {
|
|
15196
15405
|
area: { x: 0, y: 0, width: 0, height: 0 },
|
|
@@ -16006,7 +16215,7 @@ function compileChart(spec, options) {
|
|
|
16006
16215
|
if (rawTransforms && rawTransforms.length > 0) {
|
|
16007
16216
|
chartSpec = { ...chartSpec, data: runTransforms(chartSpec.data, rawTransforms) };
|
|
16008
16217
|
}
|
|
16009
|
-
const breakpoint =
|
|
16218
|
+
const breakpoint = getBreakpoint2(options.width);
|
|
16010
16219
|
const heightClass = getHeightClass(options.height);
|
|
16011
16220
|
let strategy = getLayoutStrategy(breakpoint, heightClass);
|
|
16012
16221
|
const rawSpec = expandedSpec;
|
|
@@ -16572,6 +16781,11 @@ function compileBarList2(spec, options) {
|
|
|
16572
16781
|
return compileBarList(spec, options);
|
|
16573
16782
|
}
|
|
16574
16783
|
export {
|
|
16784
|
+
ENTER_DEFAULTS,
|
|
16785
|
+
EXIT_DEFAULTS,
|
|
16786
|
+
UPDATE_DEFAULTS,
|
|
16787
|
+
buildAreaPath,
|
|
16788
|
+
buildLinePath,
|
|
16575
16789
|
clampStaggerDelay,
|
|
16576
16790
|
clearRenderers,
|
|
16577
16791
|
compile,
|
|
@@ -16582,6 +16796,7 @@ export {
|
|
|
16582
16796
|
compileSankey2 as compileSankey,
|
|
16583
16797
|
compileTable,
|
|
16584
16798
|
compileTileMap2 as compileTileMap,
|
|
16799
|
+
dedupeKeys,
|
|
16585
16800
|
evaluatePredicate,
|
|
16586
16801
|
getChartRenderer,
|
|
16587
16802
|
heuristicMeasure,
|
|
@@ -16595,6 +16810,7 @@ export {
|
|
|
16595
16810
|
runFilter,
|
|
16596
16811
|
runTimeUnit,
|
|
16597
16812
|
runTransforms,
|
|
16813
|
+
serializeKeyValue,
|
|
16598
16814
|
validateSpec
|
|
16599
16815
|
};
|
|
16600
16816
|
//# sourceMappingURL=index.js.map
|