@opendata-ai/openchart-engine 7.9.0 → 7.9.2
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.js +1459 -1404
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/axes.test.ts +108 -0
- package/src/__tests__/dimensions.test.ts +157 -5
- package/src/layout/axes.ts +142 -35
- package/src/layout/dimensions.ts +10 -11
- package/src/layout/plan.ts +11 -12
- 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
|
|
@@ -6324,19 +6324,19 @@ function copy(source, target) {
|
|
|
6324
6324
|
return target.domain(source.domain()).range(source.range()).interpolate(source.interpolate()).clamp(source.clamp()).unknown(source.unknown());
|
|
6325
6325
|
}
|
|
6326
6326
|
function transformer() {
|
|
6327
|
-
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;
|
|
6328
6328
|
function rescale() {
|
|
6329
6329
|
var n = Math.min(domain.length, range2.length);
|
|
6330
|
-
if (
|
|
6330
|
+
if (clamp2 !== identity) clamp2 = clamper(domain[0], domain[n - 1]);
|
|
6331
6331
|
piecewise = n > 2 ? polymap : bimap;
|
|
6332
6332
|
output = input = null;
|
|
6333
6333
|
return scale;
|
|
6334
6334
|
}
|
|
6335
6335
|
function scale(x2) {
|
|
6336
|
-
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)));
|
|
6337
6337
|
}
|
|
6338
6338
|
scale.invert = function(y2) {
|
|
6339
|
-
return
|
|
6339
|
+
return clamp2(untransform((input || (input = piecewise(range2, domain.map(transform), number_default)))(y2)));
|
|
6340
6340
|
};
|
|
6341
6341
|
scale.domain = function(_) {
|
|
6342
6342
|
return arguments.length ? (domain = Array.from(_, number2), rescale()) : domain.slice();
|
|
@@ -6348,7 +6348,7 @@ function transformer() {
|
|
|
6348
6348
|
return range2 = Array.from(_), interpolate = round_default, rescale();
|
|
6349
6349
|
};
|
|
6350
6350
|
scale.clamp = function(_) {
|
|
6351
|
-
return arguments.length ? (
|
|
6351
|
+
return arguments.length ? (clamp2 = _ ? true : identity, rescale()) : clamp2 !== identity;
|
|
6352
6352
|
};
|
|
6353
6353
|
scale.interpolate = function(_) {
|
|
6354
6354
|
return arguments.length ? (interpolate = _, rescale()) : interpolate;
|
|
@@ -7680,15 +7680,15 @@ function utcTime() {
|
|
|
7680
7680
|
|
|
7681
7681
|
// ../../node_modules/.bun/d3-scale@4.0.2/node_modules/d3-scale/src/sequential.js
|
|
7682
7682
|
function transformer2() {
|
|
7683
|
-
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;
|
|
7684
7684
|
function scale(x2) {
|
|
7685
|
-
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));
|
|
7686
7686
|
}
|
|
7687
7687
|
scale.domain = function(_) {
|
|
7688
7688
|
return arguments.length ? ([x0, x1] = _, t02 = transform(x0 = +x0), t12 = transform(x1 = +x1), k10 = t02 === t12 ? 0 : 1 / (t12 - t02), scale) : [x0, x1];
|
|
7689
7689
|
};
|
|
7690
7690
|
scale.clamp = function(_) {
|
|
7691
|
-
return arguments.length ? (
|
|
7691
|
+
return arguments.length ? (clamp2 = !!_, scale) : clamp2;
|
|
7692
7692
|
};
|
|
7693
7693
|
scale.interpolator = function(_) {
|
|
7694
7694
|
return arguments.length ? (interpolator = _, scale) : interpolator;
|
|
@@ -10059,9 +10059,9 @@ import {
|
|
|
10059
10059
|
AXIS_TITLE_TRAILING_PAD as AXIS_TITLE_TRAILING_PAD2,
|
|
10060
10060
|
abbreviateNumber as abbreviateNumber4,
|
|
10061
10061
|
BREAKPOINT_COMPACT_MAX as BREAKPOINT_COMPACT_MAX2,
|
|
10062
|
-
estimateTextWidth as
|
|
10062
|
+
estimateTextWidth as estimateTextWidth15,
|
|
10063
10063
|
formatNumber as formatNumber5,
|
|
10064
|
-
getAxisTitleOffset,
|
|
10064
|
+
getAxisTitleOffset as getAxisTitleOffset2,
|
|
10065
10065
|
resolveTheme as resolveTheme2,
|
|
10066
10066
|
TICK_LABEL_OFFSET
|
|
10067
10067
|
} from "@opendata-ai/openchart-core";
|
|
@@ -10070,11 +10070,11 @@ import {
|
|
|
10070
10070
|
import {
|
|
10071
10071
|
AXIS_TITLE_TRAILING_PAD,
|
|
10072
10072
|
abbreviateNumber as abbreviateNumber3,
|
|
10073
|
-
axisTitleOffset,
|
|
10073
|
+
axisTitleOffset as axisTitleOffset2,
|
|
10074
10074
|
BREAKPOINT_COMPACT_MAX,
|
|
10075
10075
|
computeChrome as computeChrome2,
|
|
10076
|
-
computeXAxisExtentFromLabels,
|
|
10077
|
-
estimateTextWidth as
|
|
10076
|
+
computeXAxisExtentFromLabels as computeXAxisExtentFromLabels2,
|
|
10077
|
+
estimateTextWidth as estimateTextWidth14,
|
|
10078
10078
|
formatNumber as formatNumber4,
|
|
10079
10079
|
HPAD_COMPACT_FRACTION,
|
|
10080
10080
|
HPAD_COMPACT_MIN,
|
|
@@ -10496,6 +10496,15 @@ function placeLegend(content, chartArea, containerWidth, _theme, xAxisHeight) {
|
|
|
10496
10496
|
};
|
|
10497
10497
|
}
|
|
10498
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
|
+
|
|
10499
10508
|
// src/layout/axes/rotation.ts
|
|
10500
10509
|
var BAND_FIT_FRACTION = 0.85;
|
|
10501
10510
|
var AUTO_ROTATE_ANGLE = -45;
|
|
@@ -10505,6 +10514,50 @@ function resolveBandTickAngle(explicitAngle, maxLabelWidth, bandwidth, labelCoun
|
|
|
10505
10514
|
return maxLabelWidth > bandwidth * BAND_FIT_FRACTION ? AUTO_ROTATE_ANGLE : void 0;
|
|
10506
10515
|
}
|
|
10507
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
|
+
|
|
10508
10561
|
// src/layout/axes/ticks.ts
|
|
10509
10562
|
import {
|
|
10510
10563
|
abbreviateNumber as abbreviateNumber2,
|
|
@@ -10694,198 +10747,543 @@ function resolveExplicitTicks(values, resolvedScale) {
|
|
|
10694
10747
|
});
|
|
10695
10748
|
}
|
|
10696
10749
|
|
|
10697
|
-
// src/layout/
|
|
10698
|
-
|
|
10699
|
-
|
|
10700
|
-
|
|
10701
|
-
|
|
10702
|
-
|
|
10703
|
-
|
|
10704
|
-
|
|
10705
|
-
|
|
10706
|
-
|
|
10707
|
-
|
|
10708
|
-
const
|
|
10709
|
-
|
|
10710
|
-
for (const v of values) {
|
|
10711
|
-
const s = String(v);
|
|
10712
|
-
if (!seen.has(s)) {
|
|
10713
|
-
seen.add(s);
|
|
10714
|
-
result.push(s);
|
|
10715
|
-
}
|
|
10716
|
-
}
|
|
10717
|
-
return result;
|
|
10718
|
-
}
|
|
10719
|
-
function applyCategoricalSort(values, sort) {
|
|
10720
|
-
if (!sort) return values;
|
|
10721
|
-
const sorted = [...values].sort((a, b) => a.localeCompare(b, void 0, { numeric: true }));
|
|
10722
|
-
if (sort === "descending") sorted.reverse();
|
|
10723
|
-
return sorted;
|
|
10724
|
-
}
|
|
10725
|
-
function applyContinuousConfig(scale, channel) {
|
|
10726
|
-
if (channel.scale?.clamp) {
|
|
10727
|
-
scale.clamp(true);
|
|
10728
|
-
}
|
|
10729
|
-
if (channel.scale?.reverse) {
|
|
10730
|
-
const [r0, r1] = scale.range();
|
|
10731
|
-
scale.range([r1, r0]);
|
|
10732
|
-
}
|
|
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";
|
|
10733
10763
|
}
|
|
10734
|
-
|
|
10735
|
-
|
|
10736
|
-
|
|
10737
|
-
|
|
10738
|
-
|
|
10739
|
-
|
|
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)];
|
|
10740
10778
|
}
|
|
10741
|
-
|
|
10742
|
-
return { scale, type: "time", channel };
|
|
10779
|
+
return density;
|
|
10743
10780
|
}
|
|
10744
|
-
|
|
10745
|
-
|
|
10746
|
-
|
|
10747
|
-
|
|
10748
|
-
if (
|
|
10749
|
-
|
|
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;
|
|
10750
10792
|
}
|
|
10751
|
-
|
|
10752
|
-
return { scale, type: "utc", channel };
|
|
10793
|
+
return best;
|
|
10753
10794
|
}
|
|
10754
|
-
function
|
|
10755
|
-
|
|
10756
|
-
|
|
10757
|
-
|
|
10758
|
-
|
|
10759
|
-
|
|
10760
|
-
|
|
10761
|
-
|
|
10762
|
-
|
|
10763
|
-
|
|
10764
|
-
|
|
10765
|
-
|
|
10766
|
-
|
|
10767
|
-
|
|
10768
|
-
|
|
10769
|
-
|
|
10770
|
-
|
|
10771
|
-
|
|
10772
|
-
|
|
10773
|
-
|
|
10774
|
-
|
|
10775
|
-
|
|
10776
|
-
scale.domain([domainMin, domainMax]);
|
|
10777
|
-
scale.nice(20);
|
|
10778
|
-
}
|
|
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;
|
|
10779
10817
|
}
|
|
10818
|
+
if (candidate.length >= floor) bestWithinFloor = candidate;
|
|
10780
10819
|
}
|
|
10781
|
-
|
|
10782
|
-
|
|
10783
|
-
|
|
10784
|
-
function buildLogScale(channel, data, rangeStart, rangeEnd) {
|
|
10785
|
-
const values = parseNumbers(fieldValues(data, channel.field)).filter((v) => v > 0);
|
|
10786
|
-
const domainMin = channel.scale?.domain ? channel.scale.domain[0] : min2(values) ?? 1;
|
|
10787
|
-
const domainMax = channel.scale?.domain ? channel.scale.domain[1] : max2(values) ?? 10;
|
|
10788
|
-
const scale = log().domain([domainMin, domainMax]).range([rangeStart, rangeEnd]);
|
|
10789
|
-
if (channel.scale?.base !== void 0) {
|
|
10790
|
-
scale.base(channel.scale.base);
|
|
10791
|
-
}
|
|
10792
|
-
if (!channel.scale?.domain && channel.scale?.nice !== false) {
|
|
10793
|
-
scale.nice();
|
|
10794
|
-
}
|
|
10795
|
-
applyContinuousConfig(scale, channel);
|
|
10796
|
-
return { scale, type: "log", channel };
|
|
10820
|
+
const rawFallback = bestWithinFloor ?? buildContinuousTicks(scale, floor);
|
|
10821
|
+
const fallback = ensureMinContinuousTicks(scale, rawFallback, floor);
|
|
10822
|
+
return thinTicksUntilFit(fallback, fontSize, fontWeight, measureText, orientation);
|
|
10797
10823
|
}
|
|
10798
|
-
function
|
|
10799
|
-
const
|
|
10800
|
-
|
|
10801
|
-
|
|
10802
|
-
if (
|
|
10803
|
-
|
|
10804
|
-
} else {
|
|
10805
|
-
domainMin = min2(values) ?? 0;
|
|
10806
|
-
domainMax = max2(values) ?? 1;
|
|
10807
|
-
if (channel.scale?.zero !== false) {
|
|
10808
|
-
domainMin = Math.min(0, domainMin);
|
|
10809
|
-
domainMax = Math.max(0, domainMax);
|
|
10810
|
-
}
|
|
10811
|
-
}
|
|
10812
|
-
const scale = pow().domain([domainMin, domainMax]).range([rangeStart, rangeEnd]);
|
|
10813
|
-
if (channel.scale?.exponent !== void 0) {
|
|
10814
|
-
scale.exponent(channel.scale.exponent);
|
|
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 };
|
|
10815
10830
|
}
|
|
10816
|
-
if (
|
|
10817
|
-
|
|
10831
|
+
if (angleDeg < 0) {
|
|
10832
|
+
return { left: tick.position - projected, right: tick.position };
|
|
10818
10833
|
}
|
|
10819
|
-
|
|
10820
|
-
return { scale, type: "pow", channel };
|
|
10834
|
+
return { left: tick.position, right: tick.position + projected };
|
|
10821
10835
|
}
|
|
10822
|
-
function
|
|
10823
|
-
|
|
10824
|
-
|
|
10825
|
-
|
|
10826
|
-
|
|
10827
|
-
|
|
10828
|
-
|
|
10829
|
-
|
|
10830
|
-
|
|
10831
|
-
|
|
10832
|
-
|
|
10833
|
-
|
|
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 > spans[lastIdx].left) {
|
|
10855
|
+
keep[i] = false;
|
|
10856
|
+
continue;
|
|
10857
|
+
}
|
|
10858
|
+
break;
|
|
10834
10859
|
}
|
|
10835
10860
|
}
|
|
10836
|
-
|
|
10837
|
-
if (!channel.scale?.domain && channel.scale?.nice !== false) {
|
|
10838
|
-
scale.nice();
|
|
10839
|
-
}
|
|
10840
|
-
applyContinuousConfig(scale, channel);
|
|
10841
|
-
return { scale, type: "sqrt", channel };
|
|
10861
|
+
return ticks2.filter((_, i) => keep[i]);
|
|
10842
10862
|
}
|
|
10843
|
-
function
|
|
10844
|
-
const
|
|
10845
|
-
|
|
10846
|
-
|
|
10847
|
-
|
|
10848
|
-
|
|
10849
|
-
|
|
10850
|
-
|
|
10851
|
-
|
|
10852
|
-
|
|
10853
|
-
|
|
10854
|
-
|
|
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);
|
|
10906
|
+
}
|
|
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);
|
|
10855
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
|
+
};
|
|
10856
10965
|
}
|
|
10857
|
-
|
|
10858
|
-
|
|
10859
|
-
|
|
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
|
+
};
|
|
10860
11040
|
}
|
|
10861
|
-
|
|
10862
|
-
|
|
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
|
+
};
|
|
11059
|
+
}
|
|
10863
11060
|
}
|
|
10864
|
-
|
|
10865
|
-
|
|
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(
|
|
11080
|
+
maxTickLabelWidth,
|
|
11081
|
+
result.y.labelStyle.fontSize,
|
|
11082
|
+
totalWidth,
|
|
11083
|
+
result.y.tickPosition === "inline"
|
|
11084
|
+
);
|
|
11085
|
+
result.y.titlePosition = {
|
|
11086
|
+
x: chartArea.x - titleOff,
|
|
11087
|
+
y: chartArea.y + chartArea.height / 2,
|
|
11088
|
+
angle: -90
|
|
11089
|
+
};
|
|
11090
|
+
}
|
|
11091
|
+
}
|
|
11092
|
+
return result;
|
|
10866
11093
|
}
|
|
10867
|
-
|
|
10868
|
-
|
|
10869
|
-
|
|
10870
|
-
|
|
10871
|
-
return { scale, type: "quantile", channel };
|
|
11094
|
+
|
|
11095
|
+
// src/layout/scales.ts
|
|
11096
|
+
function fieldValues(data, field) {
|
|
11097
|
+
return data.map((d) => d[field]).filter((v) => v != null);
|
|
10872
11098
|
}
|
|
10873
|
-
function
|
|
10874
|
-
|
|
10875
|
-
const domainMin = channel.scale?.domain ? channel.scale.domain[0] : min2(values) ?? 0;
|
|
10876
|
-
const domainMax = channel.scale?.domain ? channel.scale.domain[1] : max2(values) ?? 1;
|
|
10877
|
-
const range2 = channel.scale?.range ? channel.scale.range : evenRange(rangeStart, rangeEnd, 4);
|
|
10878
|
-
const scale = quantize().domain([domainMin, domainMax]).range(range2);
|
|
10879
|
-
return { scale, type: "quantize", channel };
|
|
11099
|
+
function parseDates(values) {
|
|
11100
|
+
return values.map((v) => v instanceof Date ? v : new Date(String(v))).filter((d) => !Number.isNaN(d.getTime()));
|
|
10880
11101
|
}
|
|
10881
|
-
function
|
|
10882
|
-
|
|
10883
|
-
const range2 = channel.scale?.range ? channel.scale.range : evenRange(rangeStart, rangeEnd, domainBreaks.length + 1);
|
|
10884
|
-
const scale = threshold().domain(domainBreaks).range(range2);
|
|
10885
|
-
return { scale, type: "threshold", channel };
|
|
11102
|
+
function parseNumbers(values) {
|
|
11103
|
+
return values.map((v) => typeof v === "number" ? v : Number(v)).filter((n) => Number.isFinite(n));
|
|
10886
11104
|
}
|
|
10887
|
-
function
|
|
10888
|
-
|
|
11105
|
+
function uniqueStrings(values) {
|
|
11106
|
+
const seen = /* @__PURE__ */ new Set();
|
|
11107
|
+
const result = [];
|
|
11108
|
+
for (const v of values) {
|
|
11109
|
+
const s = String(v);
|
|
11110
|
+
if (!seen.has(s)) {
|
|
11111
|
+
seen.add(s);
|
|
11112
|
+
result.push(s);
|
|
11113
|
+
}
|
|
11114
|
+
}
|
|
11115
|
+
return result;
|
|
11116
|
+
}
|
|
11117
|
+
function applyCategoricalSort(values, sort) {
|
|
11118
|
+
if (!sort) return values;
|
|
11119
|
+
const sorted = [...values].sort((a, b) => a.localeCompare(b, void 0, { numeric: true }));
|
|
11120
|
+
if (sort === "descending") sorted.reverse();
|
|
11121
|
+
return sorted;
|
|
11122
|
+
}
|
|
11123
|
+
function applyContinuousConfig(scale, channel) {
|
|
11124
|
+
if (channel.scale?.clamp) {
|
|
11125
|
+
scale.clamp(true);
|
|
11126
|
+
}
|
|
11127
|
+
if (channel.scale?.reverse) {
|
|
11128
|
+
const [r0, r1] = scale.range();
|
|
11129
|
+
scale.range([r1, r0]);
|
|
11130
|
+
}
|
|
11131
|
+
}
|
|
11132
|
+
function buildTimeScale(channel, data, rangeStart, rangeEnd) {
|
|
11133
|
+
const values = parseDates(fieldValues(data, channel.field));
|
|
11134
|
+
const domain = channel.scale?.domain ? [new Date(channel.scale.domain[0]), new Date(channel.scale.domain[1])] : extent(values);
|
|
11135
|
+
const scale = time().domain(domain).range([rangeStart, rangeEnd]);
|
|
11136
|
+
if (!channel.scale?.domain && channel.scale?.nice === true) {
|
|
11137
|
+
scale.nice();
|
|
11138
|
+
}
|
|
11139
|
+
applyContinuousConfig(scale, channel);
|
|
11140
|
+
return { scale, type: "time", channel };
|
|
11141
|
+
}
|
|
11142
|
+
function buildUtcScale(channel, data, rangeStart, rangeEnd) {
|
|
11143
|
+
const values = parseDates(fieldValues(data, channel.field));
|
|
11144
|
+
const domain = channel.scale?.domain ? [new Date(channel.scale.domain[0]), new Date(channel.scale.domain[1])] : extent(values);
|
|
11145
|
+
const scale = utcTime().domain(domain).range([rangeStart, rangeEnd]);
|
|
11146
|
+
if (!channel.scale?.domain && channel.scale?.nice === true) {
|
|
11147
|
+
scale.nice();
|
|
11148
|
+
}
|
|
11149
|
+
applyContinuousConfig(scale, channel);
|
|
11150
|
+
return { scale, type: "utc", channel };
|
|
11151
|
+
}
|
|
11152
|
+
function buildLinearScale(channel, data, rangeStart, rangeEnd) {
|
|
11153
|
+
const values = parseNumbers(fieldValues(data, channel.field));
|
|
11154
|
+
let domainMin;
|
|
11155
|
+
let domainMax;
|
|
11156
|
+
if (channel.scale?.domain) {
|
|
11157
|
+
const [d0, d1] = channel.scale.domain;
|
|
11158
|
+
domainMin = d0;
|
|
11159
|
+
domainMax = d1;
|
|
11160
|
+
} else {
|
|
11161
|
+
domainMin = min2(values) ?? 0;
|
|
11162
|
+
domainMax = max2(values) ?? 1;
|
|
11163
|
+
if (channel.scale?.zero !== false) {
|
|
11164
|
+
domainMin = Math.min(0, domainMin);
|
|
11165
|
+
domainMax = Math.max(0, domainMax);
|
|
11166
|
+
}
|
|
11167
|
+
}
|
|
11168
|
+
const scale = linear2().domain([domainMin, domainMax]).range([rangeStart, rangeEnd]);
|
|
11169
|
+
if (!channel.scale?.domain && channel.scale?.nice !== false) {
|
|
11170
|
+
scale.nice();
|
|
11171
|
+
if (channel.scale?.zero === false) {
|
|
11172
|
+
const [nicedMin, nicedMax] = scale.domain();
|
|
11173
|
+
if (nicedMin < domainMin || nicedMax > domainMax) {
|
|
11174
|
+
scale.domain([domainMin, domainMax]);
|
|
11175
|
+
scale.nice(20);
|
|
11176
|
+
}
|
|
11177
|
+
}
|
|
11178
|
+
}
|
|
11179
|
+
applyContinuousConfig(scale, channel);
|
|
11180
|
+
return { scale, type: "linear", channel };
|
|
11181
|
+
}
|
|
11182
|
+
function buildLogScale(channel, data, rangeStart, rangeEnd) {
|
|
11183
|
+
const values = parseNumbers(fieldValues(data, channel.field)).filter((v) => v > 0);
|
|
11184
|
+
const domainMin = channel.scale?.domain ? channel.scale.domain[0] : min2(values) ?? 1;
|
|
11185
|
+
const domainMax = channel.scale?.domain ? channel.scale.domain[1] : max2(values) ?? 10;
|
|
11186
|
+
const scale = log().domain([domainMin, domainMax]).range([rangeStart, rangeEnd]);
|
|
11187
|
+
if (channel.scale?.base !== void 0) {
|
|
11188
|
+
scale.base(channel.scale.base);
|
|
11189
|
+
}
|
|
11190
|
+
if (!channel.scale?.domain && channel.scale?.nice !== false) {
|
|
11191
|
+
scale.nice();
|
|
11192
|
+
}
|
|
11193
|
+
applyContinuousConfig(scale, channel);
|
|
11194
|
+
return { scale, type: "log", channel };
|
|
11195
|
+
}
|
|
11196
|
+
function buildPowScale(channel, data, rangeStart, rangeEnd) {
|
|
11197
|
+
const values = parseNumbers(fieldValues(data, channel.field));
|
|
11198
|
+
let domainMin;
|
|
11199
|
+
let domainMax;
|
|
11200
|
+
if (channel.scale?.domain) {
|
|
11201
|
+
[domainMin, domainMax] = channel.scale.domain;
|
|
11202
|
+
} else {
|
|
11203
|
+
domainMin = min2(values) ?? 0;
|
|
11204
|
+
domainMax = max2(values) ?? 1;
|
|
11205
|
+
if (channel.scale?.zero !== false) {
|
|
11206
|
+
domainMin = Math.min(0, domainMin);
|
|
11207
|
+
domainMax = Math.max(0, domainMax);
|
|
11208
|
+
}
|
|
11209
|
+
}
|
|
11210
|
+
const scale = pow().domain([domainMin, domainMax]).range([rangeStart, rangeEnd]);
|
|
11211
|
+
if (channel.scale?.exponent !== void 0) {
|
|
11212
|
+
scale.exponent(channel.scale.exponent);
|
|
11213
|
+
}
|
|
11214
|
+
if (!channel.scale?.domain && channel.scale?.nice !== false) {
|
|
11215
|
+
scale.nice();
|
|
11216
|
+
}
|
|
11217
|
+
applyContinuousConfig(scale, channel);
|
|
11218
|
+
return { scale, type: "pow", channel };
|
|
11219
|
+
}
|
|
11220
|
+
function buildSqrtScale(channel, data, rangeStart, rangeEnd) {
|
|
11221
|
+
const values = parseNumbers(fieldValues(data, channel.field));
|
|
11222
|
+
let domainMin;
|
|
11223
|
+
let domainMax;
|
|
11224
|
+
if (channel.scale?.domain) {
|
|
11225
|
+
[domainMin, domainMax] = channel.scale.domain;
|
|
11226
|
+
} else {
|
|
11227
|
+
domainMin = min2(values) ?? 0;
|
|
11228
|
+
domainMax = max2(values) ?? 1;
|
|
11229
|
+
if (channel.scale?.zero !== false) {
|
|
11230
|
+
domainMin = Math.min(0, domainMin);
|
|
11231
|
+
domainMax = Math.max(0, domainMax);
|
|
11232
|
+
}
|
|
11233
|
+
}
|
|
11234
|
+
const scale = sqrt2().domain([domainMin, domainMax]).range([rangeStart, rangeEnd]);
|
|
11235
|
+
if (!channel.scale?.domain && channel.scale?.nice !== false) {
|
|
11236
|
+
scale.nice();
|
|
11237
|
+
}
|
|
11238
|
+
applyContinuousConfig(scale, channel);
|
|
11239
|
+
return { scale, type: "sqrt", channel };
|
|
11240
|
+
}
|
|
11241
|
+
function buildSymlogScale(channel, data, rangeStart, rangeEnd) {
|
|
11242
|
+
const values = parseNumbers(fieldValues(data, channel.field));
|
|
11243
|
+
let domainMin;
|
|
11244
|
+
let domainMax;
|
|
11245
|
+
if (channel.scale?.domain) {
|
|
11246
|
+
[domainMin, domainMax] = channel.scale.domain;
|
|
11247
|
+
} else {
|
|
11248
|
+
domainMin = min2(values) ?? 0;
|
|
11249
|
+
domainMax = max2(values) ?? 1;
|
|
11250
|
+
if (channel.scale?.zero !== false) {
|
|
11251
|
+
domainMin = Math.min(0, domainMin);
|
|
11252
|
+
domainMax = Math.max(0, domainMax);
|
|
11253
|
+
}
|
|
11254
|
+
}
|
|
11255
|
+
const scale = symlog().domain([domainMin, domainMax]).range([rangeStart, rangeEnd]);
|
|
11256
|
+
if (channel.scale?.constant !== void 0) {
|
|
11257
|
+
scale.constant(channel.scale.constant);
|
|
11258
|
+
}
|
|
11259
|
+
if (!channel.scale?.domain && channel.scale?.nice !== false) {
|
|
11260
|
+
scale.nice();
|
|
11261
|
+
}
|
|
11262
|
+
applyContinuousConfig(scale, channel);
|
|
11263
|
+
return { scale, type: "symlog", channel };
|
|
11264
|
+
}
|
|
11265
|
+
function buildQuantileScale(channel, data, rangeStart, rangeEnd) {
|
|
11266
|
+
const values = parseNumbers(fieldValues(data, channel.field));
|
|
11267
|
+
const range2 = channel.scale?.range ? channel.scale.range : evenRange(rangeStart, rangeEnd, 4);
|
|
11268
|
+
const scale = quantile2().domain(values).range(range2);
|
|
11269
|
+
return { scale, type: "quantile", channel };
|
|
11270
|
+
}
|
|
11271
|
+
function buildQuantizeScale(channel, data, rangeStart, rangeEnd) {
|
|
11272
|
+
const values = parseNumbers(fieldValues(data, channel.field));
|
|
11273
|
+
const domainMin = channel.scale?.domain ? channel.scale.domain[0] : min2(values) ?? 0;
|
|
11274
|
+
const domainMax = channel.scale?.domain ? channel.scale.domain[1] : max2(values) ?? 1;
|
|
11275
|
+
const range2 = channel.scale?.range ? channel.scale.range : evenRange(rangeStart, rangeEnd, 4);
|
|
11276
|
+
const scale = quantize().domain([domainMin, domainMax]).range(range2);
|
|
11277
|
+
return { scale, type: "quantize", channel };
|
|
11278
|
+
}
|
|
11279
|
+
function buildThresholdScale(channel, _data, rangeStart, rangeEnd) {
|
|
11280
|
+
const domainBreaks = channel.scale?.domain ? channel.scale.domain : [0.5];
|
|
11281
|
+
const range2 = channel.scale?.range ? channel.scale.range : evenRange(rangeStart, rangeEnd, domainBreaks.length + 1);
|
|
11282
|
+
const scale = threshold().domain(domainBreaks).range(range2);
|
|
11283
|
+
return { scale, type: "threshold", channel };
|
|
11284
|
+
}
|
|
11285
|
+
function evenRange(start, end, count) {
|
|
11286
|
+
if (count <= 1) return [start];
|
|
10889
11287
|
const step = (end - start) / (count - 1);
|
|
10890
11288
|
return Array.from({ length: count }, (_, i) => start + step * i);
|
|
10891
11289
|
}
|
|
@@ -11191,7 +11589,7 @@ function createMeasureFn(measureText) {
|
|
|
11191
11589
|
if (measureText) {
|
|
11192
11590
|
return (text, fontSize, fontWeight) => measureText(text, fontSize, fontWeight).width;
|
|
11193
11591
|
}
|
|
11194
|
-
return
|
|
11592
|
+
return estimateTextWidth14;
|
|
11195
11593
|
}
|
|
11196
11594
|
function sampleLabelFromMagnitude(maxAbsVal) {
|
|
11197
11595
|
return maxAbsVal >= 1e3 ? abbreviateNumber3(maxAbsVal) : formatNumber4(maxAbsVal);
|
|
@@ -11240,11 +11638,8 @@ function resolveLayoutPlan(chartSpec, renderSpec, options, theme, strategy, wate
|
|
|
11240
11638
|
};
|
|
11241
11639
|
}
|
|
11242
11640
|
const yAxisCfg = encoding.y?.axis ?? void 0;
|
|
11243
|
-
const yTickPositionExplicit = yAxisCfg?.tickPosition;
|
|
11244
11641
|
const yIsContinuous = encoding.y?.type === "quantitative" || encoding.y?.type === "temporal";
|
|
11245
|
-
const
|
|
11246
|
-
const yAxisOrient = yAxisCfg?.orient;
|
|
11247
|
-
const yIsInline = yTickPositionExplicit === "inline" || yTickPositionExplicit === void 0 && yIsLineOrArea && yIsContinuous && yAxisOrient !== "right";
|
|
11642
|
+
const yIsInline = yTickPositionIsInline(encoding.y, renderSpec.markType);
|
|
11248
11643
|
const yAxisSuppressed = encoding.y?.axis === false;
|
|
11249
11644
|
let leftGutter = hPad + axisMargin;
|
|
11250
11645
|
if (encoding.y && !isRadial && !yAxisSuppressed && !yIsInline) {
|
|
@@ -11371,7 +11766,7 @@ function resolveLayoutPlan(chartSpec, renderSpec, options, theme, strategy, wate
|
|
|
11371
11766
|
xLabels.length
|
|
11372
11767
|
);
|
|
11373
11768
|
}
|
|
11374
|
-
const xAxisExtent = isRadial || xAxisSuppressed ? 0 :
|
|
11769
|
+
const xAxisExtent = isRadial || xAxisSuppressed ? 0 : computeXAxisExtentFromLabels2({
|
|
11375
11770
|
labels: xLabels,
|
|
11376
11771
|
tickAngle: effectiveXTickAngle,
|
|
11377
11772
|
hasTitle: hasXAxisTitle,
|
|
@@ -11495,1270 +11890,904 @@ function resolveLayoutPlan(chartSpec, renderSpec, options, theme, strategy, wate
|
|
|
11495
11890
|
const yAxisDef = encoding.y?.axis;
|
|
11496
11891
|
if (yAxisDef && (yAxisDef.title || yAxisDef.label) && !isRadial) {
|
|
11497
11892
|
let estTickLabelWidth = 0;
|
|
11498
|
-
if (encoding.y?.field && yIsContinuous) {
|
|
11893
|
+
if (encoding.y?.field && yIsContinuous && !yIsInline) {
|
|
11499
11894
|
if (yTickValues.length > 0) {
|
|
11500
11895
|
const titleProvArea = {
|
|
11501
11896
|
x: finalGutter,
|
|
11502
11897
|
y: topMargin,
|
|
11503
11898
|
width: Math.max(0, width - finalGutter - hPad),
|
|
11504
11899
|
height: chartHeight
|
|
11505
|
-
};
|
|
11506
|
-
const scales = computeScales(renderSpec, titleProvArea, renderSpec.data);
|
|
11507
|
-
if (scales.y && scaleSupportsTickCount(scales.y)) {
|
|
11508
|
-
const ticks2 = buildContinuousTicks(scales.y, yTickCount || 5);
|
|
11509
|
-
for (const t of ticks2) {
|
|
11510
|
-
const w = measure(t.label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
|
|
11511
|
-
if (w > estTickLabelWidth) estTickLabelWidth = w;
|
|
11512
|
-
}
|
|
11513
|
-
}
|
|
11514
|
-
}
|
|
11515
|
-
}
|
|
11516
|
-
const titleFontSize = theme.fonts.sizes.body;
|
|
11517
|
-
const offset =
|
|
11518
|
-
const halfGlyph = Math.ceil(titleFontSize / 2);
|
|
11519
|
-
const rotatedLabelMargin = offset + halfGlyph + (width < BREAKPOINT_COMPACT_MAX ? 0 : AXIS_TITLE_TRAILING_PAD);
|
|
11520
|
-
gutterWithTitle = Math.max(finalGutter, hPad + rotatedLabelMargin);
|
|
11521
|
-
}
|
|
11522
|
-
finalPlan = {
|
|
11523
|
-
leftGutter: gutterWithTitle,
|
|
11524
|
-
xAxisExtent,
|
|
11525
|
-
legendContent,
|
|
11526
|
-
chrome,
|
|
11527
|
-
yTickValues,
|
|
11528
|
-
yTickCount,
|
|
11529
|
-
converged: prev === null || converged,
|
|
11530
|
-
inlineYLabelInset,
|
|
11531
|
-
endpointLabelsDemoted
|
|
11532
|
-
};
|
|
11533
|
-
break;
|
|
11534
|
-
}
|
|
11535
|
-
prev = { gutter: newGutter, rowCount: legendContent.rowCount, xAxisExtent };
|
|
11536
|
-
leftGutter = newGutter;
|
|
11537
|
-
}
|
|
11538
|
-
if (!finalPlan) throw new Error("resolveLayoutPlan: loop exited without producing a plan");
|
|
11539
|
-
return finalPlan;
|
|
11540
|
-
}
|
|
11541
|
-
|
|
11542
|
-
// src/compile/layer.ts
|
|
11543
|
-
function compileLayer(spec, options, compileChart2) {
|
|
11544
|
-
const leaves = flattenLayers(spec);
|
|
11545
|
-
if (leaves.length === 0) {
|
|
11546
|
-
throw new Error("LayerSpec has no leaf chart specs after flattening");
|
|
11547
|
-
}
|
|
11548
|
-
if (leaves.length === 1) {
|
|
11549
|
-
const singleSpec = buildPrimarySpec(leaves, spec);
|
|
11550
|
-
return compileChart2(singleSpec, options);
|
|
11551
|
-
}
|
|
11552
|
-
if (spec.resolve?.scale?.y === "independent") {
|
|
11553
|
-
return compileLayerIndependent(leaves, spec, options, compileChart2);
|
|
11554
|
-
}
|
|
11555
|
-
const primarySpec = buildPrimarySpec(leaves, spec);
|
|
11556
|
-
const primaryLayout = compileChart2(primarySpec, options);
|
|
11557
|
-
const allMarks = [];
|
|
11558
|
-
const seenLabels = /* @__PURE__ */ new Set();
|
|
11559
|
-
const pLegend = primaryLayout.legend;
|
|
11560
|
-
const mergedLegendEntries = "entries" in pLegend ? [...pLegend.entries] : [];
|
|
11561
|
-
for (const entry of mergedLegendEntries) {
|
|
11562
|
-
seenLabels.add(entry.label);
|
|
11563
|
-
}
|
|
11564
|
-
const indexedLeaves = leaves.map((leaf, i) => ({
|
|
11565
|
-
leaf,
|
|
11566
|
-
zIndex: leaf.zIndex ?? i
|
|
11567
|
-
}));
|
|
11568
|
-
indexedLeaves.sort((a, b) => a.zIndex - b.zIndex);
|
|
11569
|
-
const leafOptions = { ...options, frozenChartArea: primaryLayout.area };
|
|
11570
|
-
for (const { leaf } of indexedLeaves) {
|
|
11571
|
-
const themedLeaf = {
|
|
11572
|
-
...leaf,
|
|
11573
|
-
theme: leaf.theme ?? spec.theme,
|
|
11574
|
-
darkMode: leaf.darkMode ?? spec.darkMode
|
|
11575
|
-
};
|
|
11576
|
-
const leafLayout = compileChart2(themedLeaf, leafOptions);
|
|
11577
|
-
allMarks.push(...leafLayout.marks);
|
|
11578
|
-
const leafLeg = leafLayout.legend;
|
|
11579
|
-
if ("entries" in leafLeg) {
|
|
11580
|
-
for (const entry of leafLeg.entries) {
|
|
11581
|
-
if (!seenLabels.has(entry.label)) {
|
|
11582
|
-
seenLabels.add(entry.label);
|
|
11583
|
-
mergedLegendEntries.push(entry);
|
|
11584
|
-
}
|
|
11585
|
-
}
|
|
11586
|
-
}
|
|
11587
|
-
}
|
|
11588
|
-
return {
|
|
11589
|
-
...primaryLayout,
|
|
11590
|
-
marks: allMarks,
|
|
11591
|
-
legend: {
|
|
11592
|
-
...primaryLayout.legend,
|
|
11593
|
-
..."entries" in pLegend ? { entries: mergedLegendEntries } : {}
|
|
11594
|
-
}
|
|
11595
|
-
};
|
|
11596
|
-
}
|
|
11597
|
-
function estimateYAxisLabelWidth(data, encoding, baseFontSize, measure) {
|
|
11598
|
-
if (!encoding?.y) return 40;
|
|
11599
|
-
const yEnc = encoding.y;
|
|
11600
|
-
const yField = yEnc.field;
|
|
11601
|
-
if (!yField) return 40;
|
|
11602
|
-
const m = measure ?? estimateTextWidth13;
|
|
11603
|
-
const yType = yEnc.type;
|
|
11604
|
-
if (yType === "nominal" || yType === "ordinal") {
|
|
11605
|
-
let maxWidth = 0;
|
|
11606
|
-
for (const row of data) {
|
|
11607
|
-
const label = String(row[yField] ?? "");
|
|
11608
|
-
const w = m(label, baseFontSize, 400);
|
|
11609
|
-
if (w > maxWidth) maxWidth = w;
|
|
11610
|
-
}
|
|
11611
|
-
return maxWidth > 0 ? maxWidth + 10 : 40;
|
|
11612
|
-
}
|
|
11613
|
-
const yAxisFormat = encoding.y.axis?.format;
|
|
11614
|
-
let maxAbsVal = 0;
|
|
11615
|
-
for (const row of data) {
|
|
11616
|
-
const v = Number(row[yField]);
|
|
11617
|
-
if (Number.isFinite(v) && Math.abs(v) > maxAbsVal) maxAbsVal = Math.abs(v);
|
|
11618
|
-
}
|
|
11619
|
-
let sampleLabel;
|
|
11620
|
-
if (yAxisFormat) {
|
|
11621
|
-
try {
|
|
11622
|
-
const fmt = format(yAxisFormat);
|
|
11623
|
-
sampleLabel = fmt(maxAbsVal);
|
|
11624
|
-
} catch {
|
|
11625
|
-
sampleLabel = String(maxAbsVal);
|
|
11626
|
-
}
|
|
11627
|
-
} else {
|
|
11628
|
-
sampleLabel = Math.abs(maxAbsVal) >= 1e3 ? abbreviateNumber4(maxAbsVal) : formatNumber5(maxAbsVal);
|
|
11629
|
-
}
|
|
11630
|
-
const hasNeg = data.some((r) => Number(r[yField]) < 0);
|
|
11631
|
-
const labelEst = (hasNeg ? "-" : "") + sampleLabel;
|
|
11632
|
-
return m(labelEst, baseFontSize, 400) + 10;
|
|
11633
|
-
}
|
|
11634
|
-
function compileLayerIndependent(leaves, layerSpec, options, compileChart2) {
|
|
11635
|
-
if (leaves.length > 2) {
|
|
11636
|
-
throw new Error(
|
|
11637
|
-
`Independent y-scales support at most 2 layers (left and right y-axis). Got ${leaves.length} layers.`
|
|
11638
|
-
);
|
|
11639
|
-
}
|
|
11640
|
-
const leaf0 = leaves[0];
|
|
11641
|
-
const leaf1 = leaves[1];
|
|
11642
|
-
const xType0 = leaf0.encoding?.x?.type;
|
|
11643
|
-
const xType1 = leaf1.encoding?.x?.type;
|
|
11644
|
-
if (xType0 && xType1 && xType0 !== xType1) {
|
|
11645
|
-
throw new Error(
|
|
11646
|
-
`Dual-axis charts require matching x-field types across layers. Layer 0 has '${xType0}', layer 1 has '${xType1}'.`
|
|
11647
|
-
);
|
|
11648
|
-
}
|
|
11649
|
-
const theme = resolveTheme2(layerSpec.theme ?? leaf1.theme);
|
|
11650
|
-
const axisFontSize = theme.fonts?.sizes?.axisTick ?? 11;
|
|
11651
|
-
const measureFn = createMeasureFn(options.measureText);
|
|
11652
|
-
const rightAxisWidth = estimateYAxisLabelWidth(
|
|
11653
|
-
leaf1.data,
|
|
11654
|
-
leaf1.encoding,
|
|
11655
|
-
axisFontSize,
|
|
11656
|
-
measureFn
|
|
11657
|
-
);
|
|
11658
|
-
const yAxisConfig = leaf1.encoding?.y?.axis || void 0;
|
|
11659
|
-
const hasRightAxisTitle = !!yAxisConfig?.title;
|
|
11660
|
-
const tickExtent = TICK_LABEL_OFFSET + rightAxisWidth;
|
|
11661
|
-
const bodyFontSize = theme.fonts?.sizes?.body ?? 13;
|
|
11662
|
-
const axisTitleOffset4 = getAxisTitleOffset(options.width);
|
|
11663
|
-
const halfGlyph = Math.ceil(bodyFontSize / 2);
|
|
11664
|
-
const titleExtent = hasRightAxisTitle ? axisTitleOffset4 + halfGlyph + (options.width < BREAKPOINT_COMPACT_MAX2 ? 0 : AXIS_TITLE_TRAILING_PAD2) : 0;
|
|
11665
|
-
const rightReserve = Math.max(tickExtent, titleExtent);
|
|
11666
|
-
const optionsWithReserve = {
|
|
11667
|
-
...options,
|
|
11668
|
-
rightAxisReserve: rightReserve
|
|
11669
|
-
};
|
|
11670
|
-
const xField0 = leaf0.encoding?.x?.field;
|
|
11671
|
-
const xField1 = leaf1.encoding?.x?.field;
|
|
11672
|
-
const unionXValues = /* @__PURE__ */ new Set();
|
|
11673
|
-
if (xField0) for (const row of leaf0.data) unionXValues.add(row[xField0]);
|
|
11674
|
-
if (xField1) for (const row of leaf1.data) unionXValues.add(row[xField1]);
|
|
11675
|
-
let leaf0WithUnionX = ensureXDomainCoverage(leaf0, xField0, unionXValues);
|
|
11676
|
-
let leaf1WithUnionX = ensureXDomainCoverage(leaf1, xField1, unionXValues);
|
|
11677
|
-
const aligned = alignYDomains(leaf0WithUnionX, leaf1WithUnionX);
|
|
11678
|
-
if (aligned) {
|
|
11679
|
-
leaf0WithUnionX = withYDomain(leaf0WithUnionX, aligned.domain0);
|
|
11680
|
-
leaf1WithUnionX = withYDomain(leaf1WithUnionX, aligned.domain1);
|
|
11681
|
-
}
|
|
11682
|
-
const primary0 = buildPrimarySpec([leaf0WithUnionX], layerSpec);
|
|
11683
|
-
const layout0 = compileChart2(primary0, optionsWithReserve);
|
|
11684
|
-
const primary1 = buildPrimarySpec([leaf1WithUnionX], layerSpec);
|
|
11685
|
-
primary1.annotations = [];
|
|
11686
|
-
const layout1 = compileChart2(primary1, optionsWithReserve);
|
|
11687
|
-
const y2Axis = layout1.axes.y ? {
|
|
11688
|
-
...layout1.axes.y,
|
|
11689
|
-
orient: "right",
|
|
11690
|
-
gridlines: [],
|
|
11691
|
-
start: {
|
|
11692
|
-
x: layout0.area.x + layout0.area.width,
|
|
11693
|
-
y: layout0.area.y
|
|
11694
|
-
},
|
|
11695
|
-
end: {
|
|
11696
|
-
x: layout0.area.x + layout0.area.width,
|
|
11697
|
-
y: layout0.area.y + layout0.area.height
|
|
11698
|
-
},
|
|
11699
|
-
...layout1.axes.y.label ? {
|
|
11700
|
-
titlePosition: {
|
|
11701
|
-
x: layout0.area.x + layout0.area.width + getAxisTitleOffset(layout0.dimensions.width),
|
|
11702
|
-
y: layout0.area.y + layout0.area.height / 2,
|
|
11703
|
-
angle: 90
|
|
11704
|
-
}
|
|
11705
|
-
} : {}
|
|
11706
|
-
} : void 0;
|
|
11707
|
-
const layer0HasBars = layout0.marks.some((m) => m.type === "rect");
|
|
11708
|
-
const layer1HasBars = layout1.marks.some((m) => m.type === "rect");
|
|
11709
|
-
const bandCenterByCategory = /* @__PURE__ */ new Map();
|
|
11710
|
-
if (layer0HasBars && layout0.axes.x?.ticks) {
|
|
11711
|
-
for (const tick of layout0.axes.x.ticks) {
|
|
11712
|
-
bandCenterByCategory.set(String(tick.label), tick.position);
|
|
11713
|
-
}
|
|
11714
|
-
} else if (layer1HasBars && layout1.axes.x?.ticks) {
|
|
11715
|
-
for (const tick of layout1.axes.x.ticks) {
|
|
11716
|
-
bandCenterByCategory.set(String(tick.label), tick.position);
|
|
11717
|
-
}
|
|
11718
|
-
}
|
|
11719
|
-
const remapMarkX = (xField, mark) => {
|
|
11720
|
-
if (!xField || bandCenterByCategory.size === 0) return mark;
|
|
11721
|
-
if (mark.type === "line") {
|
|
11722
|
-
const newPoints = mark.points.map((p, i) => {
|
|
11723
|
-
const bx = bandCenterByCategory.get(String(mark.data[i]?.[xField] ?? ""));
|
|
11724
|
-
return bx !== void 0 ? { ...p, x: bx } : p;
|
|
11725
|
-
});
|
|
11726
|
-
const newDataPoints = mark.dataPoints?.map((dp, i) => {
|
|
11727
|
-
const bx = bandCenterByCategory.get(String(mark.data[i]?.[xField] ?? ""));
|
|
11728
|
-
return bx !== void 0 ? { ...dp, x: bx } : dp;
|
|
11729
|
-
});
|
|
11730
|
-
const newPath = line_default().x((p) => p.x).y((p) => p.y).curve(monotoneX)(newPoints) ?? void 0;
|
|
11731
|
-
return { ...mark, points: newPoints, dataPoints: newDataPoints, path: newPath };
|
|
11732
|
-
}
|
|
11733
|
-
if (mark.type === "area") {
|
|
11734
|
-
const newTopPoints = mark.topPoints.map((p, i) => {
|
|
11735
|
-
const bx = bandCenterByCategory.get(String(mark.data[i]?.[xField] ?? ""));
|
|
11736
|
-
return bx !== void 0 ? { ...p, x: bx } : p;
|
|
11737
|
-
});
|
|
11738
|
-
const newBottomPoints = mark.bottomPoints.map((p, i) => {
|
|
11739
|
-
const bx = bandCenterByCategory.get(String(mark.data[i]?.[xField] ?? ""));
|
|
11740
|
-
return bx !== void 0 ? { ...p, x: bx } : p;
|
|
11741
|
-
});
|
|
11742
|
-
const newDataPoints = mark.dataPoints?.map((dp, i) => {
|
|
11743
|
-
const bx = bandCenterByCategory.get(String(mark.data[i]?.[xField] ?? ""));
|
|
11744
|
-
return bx !== void 0 ? { ...dp, x: bx } : dp;
|
|
11745
|
-
});
|
|
11746
|
-
const areaGen = area_default().x((p) => p.x).y0((p) => p.yBottom).y1((p) => p.yTop).curve(monotoneX);
|
|
11747
|
-
const topLineGen = line_default().x((p) => p.x).y((p) => p.yTop).curve(monotoneX);
|
|
11748
|
-
const combined = newTopPoints.map((tp, i) => ({
|
|
11749
|
-
x: tp.x,
|
|
11750
|
-
yTop: tp.y,
|
|
11751
|
-
yBottom: newBottomPoints[i]?.y ?? tp.y
|
|
11752
|
-
}));
|
|
11753
|
-
const newPath = areaGen(combined) ?? "";
|
|
11754
|
-
const newTopPath = topLineGen(combined) ?? "";
|
|
11755
|
-
return {
|
|
11756
|
-
...mark,
|
|
11757
|
-
topPoints: newTopPoints,
|
|
11758
|
-
bottomPoints: newBottomPoints,
|
|
11759
|
-
dataPoints: newDataPoints,
|
|
11760
|
-
path: newPath,
|
|
11761
|
-
topPath: newTopPath
|
|
11762
|
-
};
|
|
11763
|
-
}
|
|
11764
|
-
if (mark.type === "point") {
|
|
11765
|
-
const bx = bandCenterByCategory.get(String(mark.data[xField] ?? ""));
|
|
11766
|
-
return bx !== void 0 ? { ...mark, cx: bx } : mark;
|
|
11767
|
-
}
|
|
11768
|
-
return mark;
|
|
11769
|
-
};
|
|
11770
|
-
const adjustedMarks0 = bandCenterByCategory.size > 0 && !layer0HasBars ? layout0.marks.map((m) => remapMarkX(xField0, m)) : layout0.marks;
|
|
11771
|
-
const taggedMarks1 = layout1.marks.map((mark) => {
|
|
11772
|
-
const tagged = { ...mark, yScale: "y2" };
|
|
11773
|
-
if (bandCenterByCategory.size > 0 && !layer1HasBars) {
|
|
11774
|
-
return remapMarkX(xField1, tagged);
|
|
11775
|
-
}
|
|
11776
|
-
return tagged;
|
|
11777
|
-
});
|
|
11778
|
-
const seenLabels = /* @__PURE__ */ new Set();
|
|
11779
|
-
const l0Legend = layout0.legend;
|
|
11780
|
-
const l1Legend = layout1.legend;
|
|
11781
|
-
const mergedLegendEntries = "entries" in l0Legend ? [...l0Legend.entries] : [];
|
|
11782
|
-
for (const entry of mergedLegendEntries) seenLabels.add(entry.label);
|
|
11783
|
-
const l1Entries = "entries" in l1Legend ? l1Legend.entries : [];
|
|
11784
|
-
for (const entry of l1Entries) {
|
|
11785
|
-
if (!seenLabels.has(entry.label)) {
|
|
11786
|
-
seenLabels.add(entry.label);
|
|
11787
|
-
mergedLegendEntries.push(entry);
|
|
11788
|
-
}
|
|
11789
|
-
}
|
|
11790
|
-
const l0Count = layout0.marks.length;
|
|
11791
|
-
const mergedTooltips = new Map(layout0.tooltipDescriptors);
|
|
11792
|
-
for (const [key, value2] of layout1.tooltipDescriptors) {
|
|
11793
|
-
const match = /^(rect|point|arc)-(\d+)$/.exec(key);
|
|
11794
|
-
if (match) {
|
|
11795
|
-
const offsetKey = `${match[1]}-${Number(match[2]) + l0Count}`;
|
|
11796
|
-
mergedTooltips.set(offsetKey, value2);
|
|
11797
|
-
} else {
|
|
11798
|
-
mergedTooltips.set(key, value2);
|
|
11799
|
-
}
|
|
11800
|
-
}
|
|
11801
|
-
const z0 = leaf0.zIndex ?? 0;
|
|
11802
|
-
const z1 = leaf1.zIndex ?? 1;
|
|
11803
|
-
const marks = z0 <= z1 ? [...adjustedMarks0, ...taggedMarks1] : [...taggedMarks1, ...adjustedMarks0];
|
|
11804
|
-
return {
|
|
11805
|
-
...layout0,
|
|
11806
|
-
axes: {
|
|
11807
|
-
x: layout0.axes.x,
|
|
11808
|
-
y: layout0.axes.y,
|
|
11809
|
-
y2: y2Axis
|
|
11810
|
-
},
|
|
11811
|
-
marks,
|
|
11812
|
-
legend: {
|
|
11813
|
-
...layout0.legend,
|
|
11814
|
-
..."entries" in l0Legend ? { entries: mergedLegendEntries } : {}
|
|
11815
|
-
},
|
|
11816
|
-
tooltipDescriptors: mergedTooltips
|
|
11817
|
-
};
|
|
11818
|
-
}
|
|
11819
|
-
function ensureXDomainCoverage(leaf, xField, allXValues) {
|
|
11820
|
-
if (!xField || allXValues.size === 0) return leaf;
|
|
11821
|
-
const existingXValues = /* @__PURE__ */ new Set();
|
|
11822
|
-
for (const row of leaf.data) existingXValues.add(row[xField]);
|
|
11823
|
-
const missingRows = [];
|
|
11824
|
-
for (const xVal of allXValues) {
|
|
11825
|
-
if (!existingXValues.has(xVal)) {
|
|
11826
|
-
missingRows.push({ [xField]: xVal });
|
|
11827
|
-
}
|
|
11828
|
-
}
|
|
11829
|
-
if (missingRows.length === 0) return leaf;
|
|
11830
|
-
return {
|
|
11831
|
-
...leaf,
|
|
11832
|
-
data: [...leaf.data, ...missingRows]
|
|
11833
|
-
};
|
|
11834
|
-
}
|
|
11835
|
-
function alignYDomains(leaf0, leaf1) {
|
|
11836
|
-
const yEnc0 = leaf0.encoding?.y;
|
|
11837
|
-
const yEnc1 = leaf1.encoding?.y;
|
|
11838
|
-
if (!yEnc0 || !yEnc1) return void 0;
|
|
11839
|
-
if (yEnc0.type !== "quantitative" || yEnc1.type !== "quantitative") return void 0;
|
|
11840
|
-
if (yEnc0.scale?.domain || yEnc1.scale?.domain) return void 0;
|
|
11841
|
-
const includeZero0 = yEnc0.scale?.zero !== false;
|
|
11842
|
-
const includeZero1 = yEnc1.scale?.zero !== false;
|
|
11843
|
-
const vals0 = leaf0.data.map((r) => Number(r[yEnc0.field])).filter(Number.isFinite);
|
|
11844
|
-
const vals1 = leaf1.data.map((r) => Number(r[yEnc1.field])).filter(Number.isFinite);
|
|
11845
|
-
if (vals0.length === 0 || vals1.length === 0) return void 0;
|
|
11846
|
-
const niced = (vals, includeZero) => {
|
|
11847
|
-
let lo = Math.min(...vals);
|
|
11848
|
-
let hi = Math.max(...vals);
|
|
11849
|
-
if (includeZero) {
|
|
11850
|
-
lo = Math.min(0, lo);
|
|
11851
|
-
hi = Math.max(0, hi);
|
|
11852
|
-
}
|
|
11853
|
-
const s = linear2().domain([lo, hi]);
|
|
11854
|
-
s.nice();
|
|
11855
|
-
const [dLo, dHi] = s.domain();
|
|
11856
|
-
return [dLo, dHi];
|
|
11857
|
-
};
|
|
11858
|
-
const [min0, max0] = niced(vals0, includeZero0);
|
|
11859
|
-
const [min1, max1] = niced(vals1, includeZero1);
|
|
11860
|
-
const span0 = max0 - min0;
|
|
11861
|
-
const span1 = max1 - min1;
|
|
11862
|
-
if (span0 === 0 || span1 === 0) return void 0;
|
|
11863
|
-
const zf0 = (0 - min0) / span0;
|
|
11864
|
-
const zf1 = (0 - min1) / span1;
|
|
11865
|
-
const zeroInDomain0 = zf0 >= -1e-3 && zf0 <= 1.001;
|
|
11866
|
-
const zeroInDomain1 = zf1 >= -1e-3 && zf1 <= 1.001;
|
|
11867
|
-
if (!zeroInDomain0 || !zeroInDomain1) return void 0;
|
|
11868
|
-
if (Math.abs(zf0 - zf1) < 1e-3) {
|
|
11869
|
-
return { domain0: [min0, max0], domain1: [min1, max1] };
|
|
11870
|
-
}
|
|
11871
|
-
const targetZf = Math.max(zf0, zf1);
|
|
11872
|
-
const align = (dMin, dMax, currentZf) => {
|
|
11873
|
-
if (Math.abs(currentZf - targetZf) < 1e-3) return [dMin, dMax];
|
|
11874
|
-
if (targetZf > currentZf) {
|
|
11875
|
-
const newMin = -(targetZf / (1 - targetZf)) * dMax;
|
|
11876
|
-
return [newMin, dMax];
|
|
11877
|
-
}
|
|
11878
|
-
const newMax = -dMin * (1 - targetZf) / targetZf;
|
|
11879
|
-
return [dMin, newMax];
|
|
11880
|
-
};
|
|
11881
|
-
const domain0 = align(min0, max0, zf0);
|
|
11882
|
-
const domain1 = align(min1, max1, zf1);
|
|
11883
|
-
return { domain0, domain1 };
|
|
11884
|
-
}
|
|
11885
|
-
function withYDomain(leaf, domain) {
|
|
11886
|
-
if (!leaf.encoding?.y) return leaf;
|
|
11887
|
-
return {
|
|
11888
|
-
...leaf,
|
|
11889
|
-
encoding: {
|
|
11890
|
-
...leaf.encoding,
|
|
11891
|
-
y: {
|
|
11892
|
-
...leaf.encoding.y,
|
|
11893
|
-
scale: {
|
|
11894
|
-
...leaf.encoding.y.scale,
|
|
11895
|
-
domain
|
|
11896
|
-
}
|
|
11897
|
-
}
|
|
11898
|
-
}
|
|
11899
|
-
};
|
|
11900
|
-
}
|
|
11901
|
-
function buildPrimarySpec(leaves, layerSpec) {
|
|
11902
|
-
const allData = leaves.flatMap((leaf) => leaf.data);
|
|
11903
|
-
const primary = {
|
|
11904
|
-
...leaves[0],
|
|
11905
|
-
data: allData,
|
|
11906
|
-
chrome: layerSpec.chrome ?? leaves[0].chrome,
|
|
11907
|
-
annotations: layerSpec.annotations ?? leaves[0].annotations,
|
|
11908
|
-
labels: layerSpec.labels ?? leaves[0].labels,
|
|
11909
|
-
legend: layerSpec.legend ?? leaves[0].legend,
|
|
11910
|
-
responsive: layerSpec.responsive ?? leaves[0].responsive,
|
|
11911
|
-
theme: layerSpec.theme ?? leaves[0].theme,
|
|
11912
|
-
darkMode: layerSpec.darkMode ?? leaves[0].darkMode,
|
|
11913
|
-
watermark: layerSpec.watermark ?? leaves[0].watermark,
|
|
11914
|
-
hiddenSeries: layerSpec.hiddenSeries ?? leaves[0].hiddenSeries,
|
|
11915
|
-
endpointLabels: layerSpec.endpointLabels ?? leaves[0].endpointLabels
|
|
11916
|
-
};
|
|
11917
|
-
return primary;
|
|
11918
|
-
}
|
|
11919
|
-
|
|
11920
|
-
// src/compile/watermark-obstacle.ts
|
|
11921
|
-
import { BRAND_RESERVE_WIDTH as BRAND_RESERVE_WIDTH2 } from "@opendata-ai/openchart-core";
|
|
11922
|
-
var WATERMARK_HEIGHT = 30;
|
|
11923
|
-
var X_AXIS_EXTENT_WITH_LABEL = 48;
|
|
11924
|
-
var X_AXIS_EXTENT_TICKS_ONLY = 26;
|
|
11925
|
-
function computeWatermarkObstacle(dims, watermark, axes, theme) {
|
|
11926
|
-
if (!watermark) return null;
|
|
11927
|
-
const chartArea = dims.chartArea;
|
|
11928
|
-
const brandPadding = theme.spacing.padding;
|
|
11929
|
-
const brandX = dims.total.width - brandPadding - BRAND_RESERVE_WIDTH2;
|
|
11930
|
-
const xAxisExtent = axes.x?.label ? X_AXIS_EXTENT_WITH_LABEL : axes.x ? X_AXIS_EXTENT_TICKS_ONLY : 0;
|
|
11931
|
-
const firstBottomChrome = dims.chrome.source ?? dims.chrome.byline ?? dims.chrome.footer;
|
|
11932
|
-
const brandY = firstBottomChrome ? chartArea.y + chartArea.height + xAxisExtent + firstBottomChrome.y : chartArea.y + chartArea.height + xAxisExtent + theme.spacing.chartToFooter;
|
|
11933
|
-
return { x: brandX, y: brandY, width: BRAND_RESERVE_WIDTH2, height: WATERMARK_HEIGHT };
|
|
11934
|
-
}
|
|
11935
|
-
|
|
11936
|
-
// src/compiler/sparkline-defaults.ts
|
|
11937
|
-
var TREND_DEADBAND = 5e-4;
|
|
11938
|
-
function computeTrend(values) {
|
|
11939
|
-
const finite = [];
|
|
11940
|
-
for (const v of values) {
|
|
11941
|
-
if (Number.isFinite(v)) finite.push(v);
|
|
11942
|
-
}
|
|
11943
|
-
if (finite.length < 2) return "neutral";
|
|
11944
|
-
const n = finite.length;
|
|
11945
|
-
let sumX = 0;
|
|
11946
|
-
let sumY = 0;
|
|
11947
|
-
for (let i = 0; i < n; i++) {
|
|
11948
|
-
sumX += i;
|
|
11949
|
-
sumY += finite[i];
|
|
11950
|
-
}
|
|
11951
|
-
const meanX = sumX / n;
|
|
11952
|
-
const meanY = sumY / n;
|
|
11953
|
-
let num = 0;
|
|
11954
|
-
let den = 0;
|
|
11955
|
-
for (let i = 0; i < n; i++) {
|
|
11956
|
-
const dx = i - meanX;
|
|
11957
|
-
num += dx * (finite[i] - meanY);
|
|
11958
|
-
den += dx * dx;
|
|
11959
|
-
}
|
|
11960
|
-
if (den === 0) return "neutral";
|
|
11961
|
-
const slope = num / den;
|
|
11962
|
-
const scale = Math.abs(meanY) || 1;
|
|
11963
|
-
const relSlope = slope / scale;
|
|
11964
|
-
if (Math.abs(relSlope) < TREND_DEADBAND) return "neutral";
|
|
11965
|
-
return relSlope > 0 ? "up" : "down";
|
|
11966
|
-
}
|
|
11967
|
-
function trendColor(trend, theme) {
|
|
11968
|
-
if (trend === "up") return theme.colors.positive;
|
|
11969
|
-
if (trend === "down") return theme.colors.negative;
|
|
11970
|
-
return theme.colors.categorical[0];
|
|
11971
|
-
}
|
|
11972
|
-
function computeTrendFromData(data, yField) {
|
|
11973
|
-
if (!yField) return "neutral";
|
|
11974
|
-
const values = [];
|
|
11975
|
-
for (const row of data) {
|
|
11976
|
-
const v = Number(row[yField]);
|
|
11977
|
-
if (Number.isFinite(v)) values.push(v);
|
|
11900
|
+
};
|
|
11901
|
+
const scales = computeScales(renderSpec, titleProvArea, renderSpec.data);
|
|
11902
|
+
if (scales.y && scaleSupportsTickCount(scales.y)) {
|
|
11903
|
+
const ticks2 = buildContinuousTicks(scales.y, yTickCount || 5);
|
|
11904
|
+
for (const t of ticks2) {
|
|
11905
|
+
const w = measure(t.label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
|
|
11906
|
+
if (w > estTickLabelWidth) estTickLabelWidth = w;
|
|
11907
|
+
}
|
|
11908
|
+
}
|
|
11909
|
+
}
|
|
11910
|
+
}
|
|
11911
|
+
const titleFontSize = theme.fonts.sizes.body;
|
|
11912
|
+
const offset = axisTitleOffset2(estTickLabelWidth, titleFontSize, width, yIsInline);
|
|
11913
|
+
const halfGlyph = Math.ceil(titleFontSize / 2);
|
|
11914
|
+
const rotatedLabelMargin = offset + halfGlyph + (width < BREAKPOINT_COMPACT_MAX ? 0 : AXIS_TITLE_TRAILING_PAD);
|
|
11915
|
+
gutterWithTitle = Math.max(finalGutter, hPad + rotatedLabelMargin);
|
|
11916
|
+
}
|
|
11917
|
+
finalPlan = {
|
|
11918
|
+
leftGutter: gutterWithTitle,
|
|
11919
|
+
xAxisExtent,
|
|
11920
|
+
legendContent,
|
|
11921
|
+
chrome,
|
|
11922
|
+
yTickValues,
|
|
11923
|
+
yTickCount,
|
|
11924
|
+
converged: prev === null || converged,
|
|
11925
|
+
inlineYLabelInset,
|
|
11926
|
+
endpointLabelsDemoted
|
|
11927
|
+
};
|
|
11928
|
+
break;
|
|
11929
|
+
}
|
|
11930
|
+
prev = { gutter: newGutter, rowCount: legendContent.rowCount, xAxisExtent };
|
|
11931
|
+
leftGutter = newGutter;
|
|
11978
11932
|
}
|
|
11979
|
-
|
|
11980
|
-
|
|
11981
|
-
function hasExplicitColor(markDef, encodingHasColor) {
|
|
11982
|
-
if (encodingHasColor) return { fill: true, stroke: true };
|
|
11983
|
-
return {
|
|
11984
|
-
fill: markDef?.fill !== void 0,
|
|
11985
|
-
stroke: markDef?.stroke !== void 0
|
|
11986
|
-
};
|
|
11987
|
-
}
|
|
11988
|
-
function buildSparklineAreaGradient(baseColor) {
|
|
11989
|
-
return {
|
|
11990
|
-
gradient: "linear",
|
|
11991
|
-
x1: 0,
|
|
11992
|
-
y1: 0,
|
|
11993
|
-
x2: 0,
|
|
11994
|
-
y2: 1,
|
|
11995
|
-
stops: [
|
|
11996
|
-
{ offset: 0, color: baseColor, opacity: 0.35 },
|
|
11997
|
-
{ offset: 1, color: baseColor, opacity: 0 }
|
|
11998
|
-
]
|
|
11999
|
-
};
|
|
11933
|
+
if (!finalPlan) throw new Error("resolveLayoutPlan: loop exited without producing a plan");
|
|
11934
|
+
return finalPlan;
|
|
12000
11935
|
}
|
|
12001
11936
|
|
|
12002
|
-
// src/
|
|
12003
|
-
|
|
12004
|
-
|
|
12005
|
-
|
|
12006
|
-
|
|
12007
|
-
var MIN_NODE_RADIUS = 3;
|
|
12008
|
-
var MAX_NODE_RADIUS = 12;
|
|
12009
|
-
var DEFAULT_EDGE_WIDTH = 1;
|
|
12010
|
-
var MIN_EDGE_WIDTH = 0.5;
|
|
12011
|
-
var MAX_EDGE_WIDTH = 4;
|
|
12012
|
-
var DEFAULT_STROKE_WIDTH2 = 1;
|
|
12013
|
-
function darkenColor(hex2, amount = 0.2) {
|
|
12014
|
-
const clean = hex2.replace(/^#/, "");
|
|
12015
|
-
if (clean.length !== 6 && clean.length !== 3) return hex2;
|
|
12016
|
-
const full = clean.length === 3 ? clean.split("").map((c) => c + c).join("") : clean;
|
|
12017
|
-
const r = Math.max(0, Math.round(parseInt(full.substring(0, 2), 16) * (1 - amount)));
|
|
12018
|
-
const g = Math.max(0, Math.round(parseInt(full.substring(2, 4), 16) * (1 - amount)));
|
|
12019
|
-
const b = Math.max(0, Math.round(parseInt(full.substring(4, 6), 16) * (1 - amount)));
|
|
12020
|
-
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
|
|
12021
|
-
}
|
|
12022
|
-
function hexWithOpacity(hex2, opacity) {
|
|
12023
|
-
const clean = hex2.replace(/^#/, "");
|
|
12024
|
-
if (clean.length !== 6 && clean.length !== 3) {
|
|
12025
|
-
return hex2;
|
|
12026
|
-
}
|
|
12027
|
-
const full = clean.length === 3 ? clean.split("").map((c) => c + c).join("") : clean;
|
|
12028
|
-
const r = parseInt(full.substring(0, 2), 16);
|
|
12029
|
-
const g = parseInt(full.substring(2, 4), 16);
|
|
12030
|
-
const b = parseInt(full.substring(4, 6), 16);
|
|
12031
|
-
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
|
12032
|
-
}
|
|
12033
|
-
function computeDegrees(nodes, edges) {
|
|
12034
|
-
const degrees = /* @__PURE__ */ new Map();
|
|
12035
|
-
for (const node of nodes) {
|
|
12036
|
-
degrees.set(node.id, 0);
|
|
11937
|
+
// src/compile/layer.ts
|
|
11938
|
+
function compileLayer(spec, options, compileChart2) {
|
|
11939
|
+
const leaves = flattenLayers(spec);
|
|
11940
|
+
if (leaves.length === 0) {
|
|
11941
|
+
throw new Error("LayerSpec has no leaf chart specs after flattening");
|
|
12037
11942
|
}
|
|
12038
|
-
|
|
12039
|
-
|
|
12040
|
-
|
|
11943
|
+
if (leaves.length === 1) {
|
|
11944
|
+
const singleSpec = buildPrimarySpec(leaves, spec);
|
|
11945
|
+
return compileChart2(singleSpec, options);
|
|
12041
11946
|
}
|
|
12042
|
-
|
|
12043
|
-
|
|
12044
|
-
function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
|
|
12045
|
-
const degrees = computeDegrees(nodes, edges);
|
|
12046
|
-
const maxDegree = Math.max(1, ...degrees.values());
|
|
12047
|
-
let sizeScale;
|
|
12048
|
-
if (encoding.nodeSize?.field) {
|
|
12049
|
-
const field = encoding.nodeSize.field;
|
|
12050
|
-
const values = nodes.map((n) => Number(n[field])).filter((v) => Number.isFinite(v));
|
|
12051
|
-
const sizeMin = min2(values) ?? 0;
|
|
12052
|
-
const sizeMax = max2(values) ?? 1;
|
|
12053
|
-
sizeScale = sqrt2().domain([sizeMin, sizeMax]).range([MIN_NODE_RADIUS, MAX_NODE_RADIUS]);
|
|
11947
|
+
if (spec.resolve?.scale?.y === "independent") {
|
|
11948
|
+
return compileLayerIndependent(leaves, spec, options, compileChart2);
|
|
12054
11949
|
}
|
|
12055
|
-
|
|
12056
|
-
|
|
12057
|
-
|
|
12058
|
-
|
|
12059
|
-
|
|
12060
|
-
|
|
12061
|
-
|
|
12062
|
-
|
|
12063
|
-
const colorMax = max2(values) ?? 1;
|
|
12064
|
-
const seqPalettes = Object.values(theme.colors.sequential);
|
|
12065
|
-
const palette = seqPalettes.length > 0 ? seqPalettes[0] : ["#ccc", "#333"];
|
|
12066
|
-
const domain = scaleConfig?.domain && scaleConfig.domain.length === 2 ? scaleConfig.domain : [colorMin, colorMax];
|
|
12067
|
-
const range2 = scaleConfig?.range && scaleConfig.range.length >= 2 ? scaleConfig.range : [palette[0], palette[palette.length - 1]];
|
|
12068
|
-
const colorScale = linear2().domain(domain).range(range2);
|
|
12069
|
-
colorFn = (node) => {
|
|
12070
|
-
const val = Number(node[field]);
|
|
12071
|
-
return Number.isFinite(val) ? colorScale(val) : theme.colors.categorical[0];
|
|
12072
|
-
};
|
|
12073
|
-
} else {
|
|
12074
|
-
const domain = scaleConfig?.domain && Array.isArray(scaleConfig.domain) ? scaleConfig.domain : [...new Set(nodes.map((n) => String(n[field] ?? "")))];
|
|
12075
|
-
const range2 = scaleConfig?.range && scaleConfig.range.length > 0 ? scaleConfig.range : theme.colors.categorical;
|
|
12076
|
-
const ordinalScale = ordinal().domain(domain).range(range2);
|
|
12077
|
-
colorFn = (node) => ordinalScale(String(node[field] ?? ""));
|
|
12078
|
-
}
|
|
11950
|
+
const primarySpec = buildPrimarySpec(leaves, spec);
|
|
11951
|
+
const primaryLayout = compileChart2(primarySpec, options);
|
|
11952
|
+
const allMarks = [];
|
|
11953
|
+
const seenLabels = /* @__PURE__ */ new Set();
|
|
11954
|
+
const pLegend = primaryLayout.legend;
|
|
11955
|
+
const mergedLegendEntries = "entries" in pLegend ? [...pLegend.entries] : [];
|
|
11956
|
+
for (const entry of mergedLegendEntries) {
|
|
11957
|
+
seenLabels.add(entry.label);
|
|
12079
11958
|
}
|
|
12080
|
-
const
|
|
12081
|
-
|
|
12082
|
-
|
|
12083
|
-
|
|
12084
|
-
|
|
12085
|
-
|
|
12086
|
-
|
|
12087
|
-
|
|
12088
|
-
|
|
12089
|
-
|
|
12090
|
-
|
|
12091
|
-
let label;
|
|
12092
|
-
if (encoding.nodeLabel?.field) {
|
|
12093
|
-
const labelVal = node[encoding.nodeLabel.field];
|
|
12094
|
-
label = labelVal != null ? String(labelVal) : void 0;
|
|
12095
|
-
} else {
|
|
12096
|
-
label = node.id;
|
|
12097
|
-
}
|
|
12098
|
-
const degree = degrees.get(node.id) ?? 0;
|
|
12099
|
-
const labelPriority = maxDegree > 0 ? degree / maxDegree : 0;
|
|
12100
|
-
const { id: _id, ...rest } = node;
|
|
12101
|
-
const data = { id: node.id, ...rest };
|
|
12102
|
-
const override = nodeOverrides?.[node.id];
|
|
12103
|
-
const finalFill = override?.fill ?? fill;
|
|
12104
|
-
const finalRadius = override?.radius ?? radius;
|
|
12105
|
-
const finalStrokeWidth = override?.strokeWidth ?? DEFAULT_STROKE_WIDTH2;
|
|
12106
|
-
const finalStroke = override?.stroke ?? stroke;
|
|
12107
|
-
const finalLabelPriority = override?.alwaysShowLabel ? Infinity : labelPriority;
|
|
12108
|
-
return {
|
|
12109
|
-
id: node.id,
|
|
12110
|
-
radius: finalRadius,
|
|
12111
|
-
fill: finalFill,
|
|
12112
|
-
stroke: finalStroke,
|
|
12113
|
-
strokeWidth: finalStrokeWidth,
|
|
12114
|
-
label,
|
|
12115
|
-
labelPriority: finalLabelPriority,
|
|
12116
|
-
community: void 0,
|
|
12117
|
-
data
|
|
11959
|
+
const indexedLeaves = leaves.map((leaf, i) => ({
|
|
11960
|
+
leaf,
|
|
11961
|
+
zIndex: leaf.zIndex ?? i
|
|
11962
|
+
}));
|
|
11963
|
+
indexedLeaves.sort((a, b) => a.zIndex - b.zIndex);
|
|
11964
|
+
const leafOptions = { ...options, frozenChartArea: primaryLayout.area };
|
|
11965
|
+
for (const { leaf } of indexedLeaves) {
|
|
11966
|
+
const themedLeaf = {
|
|
11967
|
+
...leaf,
|
|
11968
|
+
theme: leaf.theme ?? spec.theme,
|
|
11969
|
+
darkMode: leaf.darkMode ?? spec.darkMode
|
|
12118
11970
|
};
|
|
12119
|
-
|
|
12120
|
-
|
|
12121
|
-
|
|
12122
|
-
|
|
12123
|
-
|
|
12124
|
-
|
|
12125
|
-
|
|
12126
|
-
|
|
12127
|
-
|
|
12128
|
-
widthScale = linear2().domain([widthMin, widthMax]).range([MIN_EDGE_WIDTH, MAX_EDGE_WIDTH]);
|
|
12129
|
-
}
|
|
12130
|
-
let edgeColorFn;
|
|
12131
|
-
if (encoding.edgeColor?.field) {
|
|
12132
|
-
const field = encoding.edgeColor.field;
|
|
12133
|
-
const fieldType = encoding.edgeColor.type ?? "nominal";
|
|
12134
|
-
const scaleConfig = encoding.edgeColor.scale;
|
|
12135
|
-
if (fieldType === "quantitative") {
|
|
12136
|
-
const values = edges.map((e) => Number(e[field])).filter((v) => Number.isFinite(v));
|
|
12137
|
-
const colorMin = min2(values) ?? 0;
|
|
12138
|
-
const colorMax = max2(values) ?? 1;
|
|
12139
|
-
const seqPalettes = Object.values(theme.colors.sequential);
|
|
12140
|
-
const palette = seqPalettes.length > 0 ? seqPalettes[0] : ["#ccc", "#333"];
|
|
12141
|
-
const domain = scaleConfig?.domain && scaleConfig.domain.length === 2 ? scaleConfig.domain : [colorMin, colorMax];
|
|
12142
|
-
const range2 = scaleConfig?.range && scaleConfig.range.length >= 2 ? scaleConfig.range : [palette[0], palette[palette.length - 1]];
|
|
12143
|
-
const colorScale = linear2().domain(domain).range(range2);
|
|
12144
|
-
edgeColorFn = (edge) => {
|
|
12145
|
-
const val = Number(edge[field]);
|
|
12146
|
-
return Number.isFinite(val) ? colorScale(val) : hexWithOpacity(theme.colors.axis, 0.4);
|
|
12147
|
-
};
|
|
12148
|
-
} else {
|
|
12149
|
-
const domain = scaleConfig?.domain && Array.isArray(scaleConfig.domain) ? scaleConfig.domain : [...new Set(edges.map((e) => String(e[field] ?? "")))];
|
|
12150
|
-
const range2 = scaleConfig?.range && scaleConfig.range.length > 0 ? scaleConfig.range : theme.colors.categorical;
|
|
12151
|
-
const ordinalScale = ordinal().domain(domain).range(range2);
|
|
12152
|
-
edgeColorFn = (edge) => ordinalScale(String(edge[field] ?? ""));
|
|
12153
|
-
}
|
|
12154
|
-
}
|
|
12155
|
-
const defaultEdgeColor = hexWithOpacity(theme.colors.axis, 0.4);
|
|
12156
|
-
const EDGE_STYLES = ["solid", "dashed", "dotted"];
|
|
12157
|
-
let styleFn;
|
|
12158
|
-
if (encoding.edgeStyle?.field) {
|
|
12159
|
-
const field = encoding.edgeStyle.field;
|
|
12160
|
-
const uniqueValues = [...new Set(edges.map((e) => String(e[field] ?? "")))];
|
|
12161
|
-
const styleMap = /* @__PURE__ */ new Map();
|
|
12162
|
-
for (let i = 0; i < uniqueValues.length; i++) {
|
|
12163
|
-
styleMap.set(uniqueValues[i], EDGE_STYLES[i % EDGE_STYLES.length]);
|
|
12164
|
-
}
|
|
12165
|
-
styleFn = (edge) => styleMap.get(String(edge[field] ?? "")) ?? "solid";
|
|
12166
|
-
}
|
|
12167
|
-
return edges.map((edge) => {
|
|
12168
|
-
const { source, target, ...rest } = edge;
|
|
12169
|
-
let strokeWidth = DEFAULT_EDGE_WIDTH;
|
|
12170
|
-
if (widthScale && encoding.edgeWidth?.field) {
|
|
12171
|
-
const val = Number(edge[encoding.edgeWidth.field]);
|
|
12172
|
-
if (Number.isFinite(val)) {
|
|
12173
|
-
strokeWidth = widthScale(val);
|
|
11971
|
+
const leafLayout = compileChart2(themedLeaf, leafOptions);
|
|
11972
|
+
allMarks.push(...leafLayout.marks);
|
|
11973
|
+
const leafLeg = leafLayout.legend;
|
|
11974
|
+
if ("entries" in leafLeg) {
|
|
11975
|
+
for (const entry of leafLeg.entries) {
|
|
11976
|
+
if (!seenLabels.has(entry.label)) {
|
|
11977
|
+
seenLabels.add(entry.label);
|
|
11978
|
+
mergedLegendEntries.push(entry);
|
|
11979
|
+
}
|
|
12174
11980
|
}
|
|
12175
11981
|
}
|
|
12176
|
-
const stroke = edgeColorFn ? edgeColorFn(edge) : defaultEdgeColor;
|
|
12177
|
-
const style = styleFn ? styleFn(edge) : "solid";
|
|
12178
|
-
return {
|
|
12179
|
-
source,
|
|
12180
|
-
target,
|
|
12181
|
-
stroke,
|
|
12182
|
-
strokeWidth,
|
|
12183
|
-
style,
|
|
12184
|
-
data: { source, target, ...rest }
|
|
12185
|
-
};
|
|
12186
|
-
});
|
|
12187
|
-
}
|
|
12188
|
-
|
|
12189
|
-
// src/graphs/community.ts
|
|
12190
|
-
function assignCommunities(nodes, clusteringField) {
|
|
12191
|
-
if (!clusteringField) return;
|
|
12192
|
-
for (const node of nodes) {
|
|
12193
|
-
const value2 = node.data[clusteringField];
|
|
12194
|
-
node.community = value2 != null ? String(value2) : void 0;
|
|
12195
11982
|
}
|
|
11983
|
+
return {
|
|
11984
|
+
...primaryLayout,
|
|
11985
|
+
marks: allMarks,
|
|
11986
|
+
legend: {
|
|
11987
|
+
...primaryLayout.legend,
|
|
11988
|
+
..."entries" in pLegend ? { entries: mergedLegendEntries } : {}
|
|
11989
|
+
}
|
|
11990
|
+
};
|
|
12196
11991
|
}
|
|
12197
|
-
function
|
|
12198
|
-
|
|
12199
|
-
const
|
|
12200
|
-
|
|
12201
|
-
|
|
12202
|
-
|
|
12203
|
-
|
|
12204
|
-
|
|
11992
|
+
function estimateYAxisLabelWidth(data, encoding, baseFontSize, measure) {
|
|
11993
|
+
if (!encoding?.y) return 40;
|
|
11994
|
+
const yEnc = encoding.y;
|
|
11995
|
+
const yField = yEnc.field;
|
|
11996
|
+
if (!yField) return 40;
|
|
11997
|
+
const m = measure ?? estimateTextWidth15;
|
|
11998
|
+
const yType = yEnc.type;
|
|
11999
|
+
if (yType === "nominal" || yType === "ordinal") {
|
|
12000
|
+
let maxWidth = 0;
|
|
12001
|
+
for (const row of data) {
|
|
12002
|
+
const label = String(row[yField] ?? "");
|
|
12003
|
+
const w = m(label, baseFontSize, 400);
|
|
12004
|
+
if (w > maxWidth) maxWidth = w;
|
|
12205
12005
|
}
|
|
12006
|
+
return maxWidth > 0 ? maxWidth + 10 : 40;
|
|
12206
12007
|
}
|
|
12207
|
-
|
|
12208
|
-
|
|
12209
|
-
|
|
12210
|
-
|
|
12211
|
-
if (
|
|
12212
|
-
|
|
12213
|
-
|
|
12214
|
-
|
|
12215
|
-
|
|
12216
|
-
|
|
12008
|
+
const yAxisFormat = encoding.y.axis?.format;
|
|
12009
|
+
let maxAbsVal = 0;
|
|
12010
|
+
for (const row of data) {
|
|
12011
|
+
const v = Number(row[yField]);
|
|
12012
|
+
if (Number.isFinite(v) && Math.abs(v) > maxAbsVal) maxAbsVal = Math.abs(v);
|
|
12013
|
+
}
|
|
12014
|
+
let sampleLabel;
|
|
12015
|
+
if (yAxisFormat) {
|
|
12016
|
+
try {
|
|
12017
|
+
const fmt = format(yAxisFormat);
|
|
12018
|
+
sampleLabel = fmt(maxAbsVal);
|
|
12019
|
+
} catch {
|
|
12020
|
+
sampleLabel = String(maxAbsVal);
|
|
12217
12021
|
}
|
|
12022
|
+
} else {
|
|
12023
|
+
sampleLabel = Math.abs(maxAbsVal) >= 1e3 ? abbreviateNumber4(maxAbsVal) : formatNumber5(maxAbsVal);
|
|
12218
12024
|
}
|
|
12025
|
+
const hasNeg = data.some((r) => Number(r[yField]) < 0);
|
|
12026
|
+
const labelEst = (hasNeg ? "-" : "") + sampleLabel;
|
|
12027
|
+
return m(labelEst, baseFontSize, 400) + 10;
|
|
12219
12028
|
}
|
|
12220
|
-
|
|
12221
|
-
|
|
12222
|
-
|
|
12223
|
-
|
|
12224
|
-
|
|
12225
|
-
|
|
12226
|
-
const
|
|
12227
|
-
|
|
12228
|
-
|
|
12229
|
-
|
|
12230
|
-
|
|
12231
|
-
|
|
12029
|
+
function compileLayerIndependent(leaves, layerSpec, options, compileChart2) {
|
|
12030
|
+
if (leaves.length > 2) {
|
|
12031
|
+
throw new Error(
|
|
12032
|
+
`Independent y-scales support at most 2 layers (left and right y-axis). Got ${leaves.length} layers.`
|
|
12033
|
+
);
|
|
12034
|
+
}
|
|
12035
|
+
const leaf0 = leaves[0];
|
|
12036
|
+
const leaf1 = leaves[1];
|
|
12037
|
+
const xType0 = leaf0.encoding?.x?.type;
|
|
12038
|
+
const xType1 = leaf1.encoding?.x?.type;
|
|
12039
|
+
if (xType0 && xType1 && xType0 !== xType1) {
|
|
12040
|
+
throw new Error(
|
|
12041
|
+
`Dual-axis charts require matching x-field types across layers. Layer 0 has '${xType0}', layer 1 has '${xType1}'.`
|
|
12042
|
+
);
|
|
12043
|
+
}
|
|
12044
|
+
const theme = resolveTheme2(layerSpec.theme ?? leaf1.theme);
|
|
12045
|
+
const axisFontSize = theme.fonts?.sizes?.axisTick ?? 11;
|
|
12046
|
+
const measureFn = createMeasureFn(options.measureText);
|
|
12047
|
+
const rightAxisWidth = estimateYAxisLabelWidth(
|
|
12048
|
+
leaf1.data,
|
|
12049
|
+
leaf1.encoding,
|
|
12050
|
+
axisFontSize,
|
|
12051
|
+
measureFn
|
|
12052
|
+
);
|
|
12053
|
+
const yAxisConfig = leaf1.encoding?.y?.axis || void 0;
|
|
12054
|
+
const hasRightAxisTitle = !!yAxisConfig?.title;
|
|
12055
|
+
const tickExtent = TICK_LABEL_OFFSET + rightAxisWidth;
|
|
12056
|
+
const bodyFontSize = theme.fonts?.sizes?.body ?? 13;
|
|
12057
|
+
const axisTitleOffset4 = getAxisTitleOffset2(options.width);
|
|
12058
|
+
const halfGlyph = Math.ceil(bodyFontSize / 2);
|
|
12059
|
+
const titleExtent = hasRightAxisTitle ? axisTitleOffset4 + halfGlyph + (options.width < BREAKPOINT_COMPACT_MAX2 ? 0 : AXIS_TITLE_TRAILING_PAD2) : 0;
|
|
12060
|
+
const rightReserve = Math.max(tickExtent, titleExtent);
|
|
12061
|
+
const optionsWithReserve = {
|
|
12062
|
+
...options,
|
|
12063
|
+
rightAxisReserve: rightReserve
|
|
12232
12064
|
};
|
|
12233
|
-
|
|
12234
|
-
|
|
12235
|
-
|
|
12236
|
-
|
|
12237
|
-
|
|
12238
|
-
|
|
12239
|
-
|
|
12240
|
-
|
|
12241
|
-
|
|
12242
|
-
|
|
12243
|
-
|
|
12244
|
-
|
|
12245
|
-
|
|
12246
|
-
|
|
12065
|
+
const xField0 = leaf0.encoding?.x?.field;
|
|
12066
|
+
const xField1 = leaf1.encoding?.x?.field;
|
|
12067
|
+
const unionXValues = /* @__PURE__ */ new Set();
|
|
12068
|
+
if (xField0) for (const row of leaf0.data) unionXValues.add(row[xField0]);
|
|
12069
|
+
if (xField1) for (const row of leaf1.data) unionXValues.add(row[xField1]);
|
|
12070
|
+
let leaf0WithUnionX = ensureXDomainCoverage(leaf0, xField0, unionXValues);
|
|
12071
|
+
let leaf1WithUnionX = ensureXDomainCoverage(leaf1, xField1, unionXValues);
|
|
12072
|
+
const aligned = alignYDomains(leaf0WithUnionX, leaf1WithUnionX);
|
|
12073
|
+
if (aligned) {
|
|
12074
|
+
leaf0WithUnionX = withYDomain(leaf0WithUnionX, aligned.domain0);
|
|
12075
|
+
leaf1WithUnionX = withYDomain(leaf1WithUnionX, aligned.domain1);
|
|
12076
|
+
}
|
|
12077
|
+
const primary0 = buildPrimarySpec([leaf0WithUnionX], layerSpec);
|
|
12078
|
+
const layout0 = compileChart2(primary0, optionsWithReserve);
|
|
12079
|
+
const primary1 = buildPrimarySpec([leaf1WithUnionX], layerSpec);
|
|
12080
|
+
primary1.annotations = [];
|
|
12081
|
+
const layout1 = compileChart2(primary1, optionsWithReserve);
|
|
12082
|
+
const y2Axis = layout1.axes.y ? {
|
|
12083
|
+
...layout1.axes.y,
|
|
12084
|
+
orient: "right",
|
|
12085
|
+
gridlines: [],
|
|
12086
|
+
start: {
|
|
12087
|
+
x: layout0.area.x + layout0.area.width,
|
|
12088
|
+
y: layout0.area.y
|
|
12089
|
+
},
|
|
12090
|
+
end: {
|
|
12091
|
+
x: layout0.area.x + layout0.area.width,
|
|
12092
|
+
y: layout0.area.y + layout0.area.height
|
|
12093
|
+
},
|
|
12094
|
+
...layout1.axes.y.label ? {
|
|
12095
|
+
titlePosition: {
|
|
12096
|
+
x: layout0.area.x + layout0.area.width + getAxisTitleOffset2(layout0.dimensions.width),
|
|
12097
|
+
y: layout0.area.y + layout0.area.height / 2,
|
|
12098
|
+
angle: 90
|
|
12247
12099
|
}
|
|
12100
|
+
} : {}
|
|
12101
|
+
} : void 0;
|
|
12102
|
+
const layer0HasBars = layout0.marks.some((m) => m.type === "rect");
|
|
12103
|
+
const layer1HasBars = layout1.marks.some((m) => m.type === "rect");
|
|
12104
|
+
const bandCenterByCategory = /* @__PURE__ */ new Map();
|
|
12105
|
+
if (layer0HasBars && layout0.axes.x?.ticks) {
|
|
12106
|
+
for (const tick of layout0.axes.x.ticks) {
|
|
12107
|
+
bandCenterByCategory.set(String(tick.label), tick.position);
|
|
12248
12108
|
}
|
|
12249
|
-
|
|
12250
|
-
|
|
12109
|
+
} else if (layer1HasBars && layout1.axes.x?.ticks) {
|
|
12110
|
+
for (const tick of layout1.axes.x.ticks) {
|
|
12111
|
+
bandCenterByCategory.set(String(tick.label), tick.position);
|
|
12112
|
+
}
|
|
12113
|
+
}
|
|
12114
|
+
const remapMarkX = (xField, mark) => {
|
|
12115
|
+
if (!xField || bandCenterByCategory.size === 0) return mark;
|
|
12116
|
+
if (mark.type === "line") {
|
|
12117
|
+
const newPoints = mark.points.map((p, i) => {
|
|
12118
|
+
const bx = bandCenterByCategory.get(String(mark.data[i]?.[xField] ?? ""));
|
|
12119
|
+
return bx !== void 0 ? { ...p, x: bx } : p;
|
|
12120
|
+
});
|
|
12121
|
+
const newDataPoints = mark.dataPoints?.map((dp, i) => {
|
|
12122
|
+
const bx = bandCenterByCategory.get(String(mark.data[i]?.[xField] ?? ""));
|
|
12123
|
+
return bx !== void 0 ? { ...dp, x: bx } : dp;
|
|
12124
|
+
});
|
|
12125
|
+
const newPath = line_default().x((p) => p.x).y((p) => p.y).curve(monotoneX)(newPoints) ?? void 0;
|
|
12126
|
+
return { ...mark, points: newPoints, dataPoints: newDataPoints, path: newPath };
|
|
12127
|
+
}
|
|
12128
|
+
if (mark.type === "area") {
|
|
12129
|
+
const newTopPoints = mark.topPoints.map((p, i) => {
|
|
12130
|
+
const bx = bandCenterByCategory.get(String(mark.data[i]?.[xField] ?? ""));
|
|
12131
|
+
return bx !== void 0 ? { ...p, x: bx } : p;
|
|
12132
|
+
});
|
|
12133
|
+
const newBottomPoints = mark.bottomPoints.map((p, i) => {
|
|
12134
|
+
const bx = bandCenterByCategory.get(String(mark.data[i]?.[xField] ?? ""));
|
|
12135
|
+
return bx !== void 0 ? { ...p, x: bx } : p;
|
|
12136
|
+
});
|
|
12137
|
+
const newDataPoints = mark.dataPoints?.map((dp, i) => {
|
|
12138
|
+
const bx = bandCenterByCategory.get(String(mark.data[i]?.[xField] ?? ""));
|
|
12139
|
+
return bx !== void 0 ? { ...dp, x: bx } : dp;
|
|
12140
|
+
});
|
|
12141
|
+
const areaGen = area_default().x((p) => p.x).y0((p) => p.yBottom).y1((p) => p.yTop).curve(monotoneX);
|
|
12142
|
+
const topLineGen = line_default().x((p) => p.x).y((p) => p.yTop).curve(monotoneX);
|
|
12143
|
+
const combined = newTopPoints.map((tp, i) => ({
|
|
12144
|
+
x: tp.x,
|
|
12145
|
+
yTop: tp.y,
|
|
12146
|
+
yBottom: newBottomPoints[i]?.y ?? tp.y
|
|
12147
|
+
}));
|
|
12148
|
+
const newPath = areaGen(combined) ?? "";
|
|
12149
|
+
const newTopPath = topLineGen(combined) ?? "";
|
|
12150
|
+
return {
|
|
12151
|
+
...mark,
|
|
12152
|
+
topPoints: newTopPoints,
|
|
12153
|
+
bottomPoints: newBottomPoints,
|
|
12154
|
+
dataPoints: newDataPoints,
|
|
12155
|
+
path: newPath,
|
|
12156
|
+
topPath: newTopPath
|
|
12157
|
+
};
|
|
12158
|
+
}
|
|
12159
|
+
if (mark.type === "point") {
|
|
12160
|
+
const bx = bandCenterByCategory.get(String(mark.data[xField] ?? ""));
|
|
12161
|
+
return bx !== void 0 ? { ...mark, cx: bx } : mark;
|
|
12162
|
+
}
|
|
12163
|
+
return mark;
|
|
12164
|
+
};
|
|
12165
|
+
const adjustedMarks0 = bandCenterByCategory.size > 0 && !layer0HasBars ? layout0.marks.map((m) => remapMarkX(xField0, m)) : layout0.marks;
|
|
12166
|
+
const taggedMarks1 = layout1.marks.map((mark) => {
|
|
12167
|
+
const tagged = { ...mark, yScale: "y2" };
|
|
12168
|
+
if (bandCenterByCategory.size > 0 && !layer1HasBars) {
|
|
12169
|
+
return remapMarkX(xField1, tagged);
|
|
12170
|
+
}
|
|
12171
|
+
return tagged;
|
|
12172
|
+
});
|
|
12173
|
+
const seenLabels = /* @__PURE__ */ new Set();
|
|
12174
|
+
const l0Legend = layout0.legend;
|
|
12175
|
+
const l1Legend = layout1.legend;
|
|
12176
|
+
const mergedLegendEntries = "entries" in l0Legend ? [...l0Legend.entries] : [];
|
|
12177
|
+
for (const entry of mergedLegendEntries) seenLabels.add(entry.label);
|
|
12178
|
+
const l1Entries = "entries" in l1Legend ? l1Legend.entries : [];
|
|
12179
|
+
for (const entry of l1Entries) {
|
|
12180
|
+
if (!seenLabels.has(entry.label)) {
|
|
12181
|
+
seenLabels.add(entry.label);
|
|
12182
|
+
mergedLegendEntries.push(entry);
|
|
12183
|
+
}
|
|
12184
|
+
}
|
|
12185
|
+
const l0Count = layout0.marks.length;
|
|
12186
|
+
const mergedTooltips = new Map(layout0.tooltipDescriptors);
|
|
12187
|
+
for (const [key, value2] of layout1.tooltipDescriptors) {
|
|
12188
|
+
const match = /^(rect|point|arc)-(\d+)$/.exec(key);
|
|
12189
|
+
if (match) {
|
|
12190
|
+
const offsetKey = `${match[1]}-${Number(match[2]) + l0Count}`;
|
|
12191
|
+
mergedTooltips.set(offsetKey, value2);
|
|
12251
12192
|
} else {
|
|
12252
|
-
|
|
12253
|
-
label,
|
|
12254
|
-
color: color2,
|
|
12255
|
-
shape: "circle",
|
|
12256
|
-
active: true
|
|
12257
|
-
}));
|
|
12193
|
+
mergedTooltips.set(key, value2);
|
|
12258
12194
|
}
|
|
12259
12195
|
}
|
|
12196
|
+
const z0 = leaf0.zIndex ?? 0;
|
|
12197
|
+
const z1 = leaf1.zIndex ?? 1;
|
|
12198
|
+
const marks = z0 <= z1 ? [...adjustedMarks0, ...taggedMarks1] : [...taggedMarks1, ...adjustedMarks0];
|
|
12260
12199
|
return {
|
|
12261
|
-
|
|
12262
|
-
|
|
12263
|
-
|
|
12264
|
-
|
|
12265
|
-
|
|
12266
|
-
|
|
12267
|
-
|
|
12268
|
-
|
|
12200
|
+
...layout0,
|
|
12201
|
+
axes: {
|
|
12202
|
+
x: layout0.axes.x,
|
|
12203
|
+
y: layout0.axes.y,
|
|
12204
|
+
y2: y2Axis
|
|
12205
|
+
},
|
|
12206
|
+
marks,
|
|
12207
|
+
legend: {
|
|
12208
|
+
...layout0.legend,
|
|
12209
|
+
..."entries" in l0Legend ? { entries: mergedLegendEntries } : {}
|
|
12210
|
+
},
|
|
12211
|
+
tooltipDescriptors: mergedTooltips
|
|
12269
12212
|
};
|
|
12270
12213
|
}
|
|
12271
|
-
function
|
|
12272
|
-
|
|
12273
|
-
|
|
12274
|
-
|
|
12275
|
-
|
|
12276
|
-
|
|
12277
|
-
|
|
12278
|
-
|
|
12279
|
-
color: node.fill
|
|
12280
|
-
});
|
|
12281
|
-
}
|
|
12282
|
-
for (const [key, value2] of Object.entries(node.data)) {
|
|
12283
|
-
if (key === "id") continue;
|
|
12284
|
-
if (value2 == null) continue;
|
|
12285
|
-
fields.push({
|
|
12286
|
-
label: key,
|
|
12287
|
-
value: typeof value2 === "number" ? value2.toLocaleString() : String(value2)
|
|
12288
|
-
});
|
|
12214
|
+
function ensureXDomainCoverage(leaf, xField, allXValues) {
|
|
12215
|
+
if (!xField || allXValues.size === 0) return leaf;
|
|
12216
|
+
const existingXValues = /* @__PURE__ */ new Set();
|
|
12217
|
+
for (const row of leaf.data) existingXValues.add(row[xField]);
|
|
12218
|
+
const missingRows = [];
|
|
12219
|
+
for (const xVal of allXValues) {
|
|
12220
|
+
if (!existingXValues.has(xVal)) {
|
|
12221
|
+
missingRows.push({ [xField]: xVal });
|
|
12289
12222
|
}
|
|
12290
|
-
descriptors.set(node.id, {
|
|
12291
|
-
title: node.label ?? node.id,
|
|
12292
|
-
fields
|
|
12293
|
-
});
|
|
12294
12223
|
}
|
|
12295
|
-
return
|
|
12224
|
+
if (missingRows.length === 0) return leaf;
|
|
12225
|
+
return {
|
|
12226
|
+
...leaf,
|
|
12227
|
+
data: [...leaf.data, ...missingRows]
|
|
12228
|
+
};
|
|
12296
12229
|
}
|
|
12297
|
-
function
|
|
12298
|
-
const
|
|
12299
|
-
|
|
12300
|
-
|
|
12301
|
-
|
|
12302
|
-
|
|
12303
|
-
|
|
12304
|
-
const
|
|
12305
|
-
const
|
|
12306
|
-
const
|
|
12307
|
-
|
|
12308
|
-
|
|
12309
|
-
|
|
12310
|
-
|
|
12311
|
-
|
|
12312
|
-
|
|
12313
|
-
|
|
12314
|
-
|
|
12315
|
-
|
|
12316
|
-
|
|
12317
|
-
|
|
12318
|
-
|
|
12319
|
-
const clusteringField = graphSpec.layout.clustering?.field;
|
|
12320
|
-
const hasCommunities = !!clusteringField;
|
|
12321
|
-
assignCommunities(compiledNodes, clusteringField);
|
|
12322
|
-
const hasNodeColorEncoding = !!graphSpec.encoding.nodeColor?.field;
|
|
12323
|
-
let communityColorMap = /* @__PURE__ */ new Map();
|
|
12324
|
-
if (hasCommunities && !hasNodeColorEncoding) {
|
|
12325
|
-
communityColorMap = buildCommunityColorMap(compiledNodes, theme);
|
|
12326
|
-
applyCommunityColors(compiledNodes, communityColorMap);
|
|
12327
|
-
}
|
|
12328
|
-
const compiledEdges = resolveEdgeVisuals(graphSpec.edges, graphSpec.encoding, theme);
|
|
12329
|
-
const useCommunitiesForLegend = hasCommunities && !hasNodeColorEncoding;
|
|
12330
|
-
const legend = buildGraphLegend(
|
|
12331
|
-
compiledNodes,
|
|
12332
|
-
communityColorMap,
|
|
12333
|
-
useCommunitiesForLegend,
|
|
12334
|
-
theme,
|
|
12335
|
-
graphSpec.encoding.nodeColor?.field
|
|
12336
|
-
);
|
|
12337
|
-
const tooltipDescriptors = buildGraphTooltips(compiledNodes);
|
|
12338
|
-
const communityCount = communityColorMap.size;
|
|
12339
|
-
const altParts = [
|
|
12340
|
-
`Network graph with ${compiledNodes.length} nodes and ${compiledEdges.length} edges`
|
|
12341
|
-
];
|
|
12342
|
-
if (communityCount > 0) {
|
|
12343
|
-
altParts.push(`organized into ${communityCount} communities`);
|
|
12344
|
-
}
|
|
12345
|
-
const a11y = {
|
|
12346
|
-
altText: altParts.join(", "),
|
|
12347
|
-
dataTableFallback: compiledNodes.map((n) => [n.id, n.community ?? "", n.label ?? ""]),
|
|
12348
|
-
role: "img",
|
|
12349
|
-
keyboardNavigable: compiledNodes.length > 0
|
|
12230
|
+
function alignYDomains(leaf0, leaf1) {
|
|
12231
|
+
const yEnc0 = leaf0.encoding?.y;
|
|
12232
|
+
const yEnc1 = leaf1.encoding?.y;
|
|
12233
|
+
if (!yEnc0 || !yEnc1) return void 0;
|
|
12234
|
+
if (yEnc0.type !== "quantitative" || yEnc1.type !== "quantitative") return void 0;
|
|
12235
|
+
if (yEnc0.scale?.domain || yEnc1.scale?.domain) return void 0;
|
|
12236
|
+
const includeZero0 = yEnc0.scale?.zero !== false;
|
|
12237
|
+
const includeZero1 = yEnc1.scale?.zero !== false;
|
|
12238
|
+
const vals0 = leaf0.data.map((r) => Number(r[yEnc0.field])).filter(Number.isFinite);
|
|
12239
|
+
const vals1 = leaf1.data.map((r) => Number(r[yEnc1.field])).filter(Number.isFinite);
|
|
12240
|
+
if (vals0.length === 0 || vals1.length === 0) return void 0;
|
|
12241
|
+
const niced = (vals, includeZero) => {
|
|
12242
|
+
let lo = Math.min(...vals);
|
|
12243
|
+
let hi = Math.max(...vals);
|
|
12244
|
+
if (includeZero) {
|
|
12245
|
+
lo = Math.min(0, lo);
|
|
12246
|
+
hi = Math.max(0, hi);
|
|
12247
|
+
}
|
|
12248
|
+
const s = linear2().domain([lo, hi]);
|
|
12249
|
+
s.nice();
|
|
12250
|
+
const [dLo, dHi] = s.domain();
|
|
12251
|
+
return [dLo, dHi];
|
|
12350
12252
|
};
|
|
12351
|
-
const
|
|
12352
|
-
const
|
|
12353
|
-
const
|
|
12354
|
-
|
|
12355
|
-
|
|
12356
|
-
|
|
12357
|
-
|
|
12358
|
-
|
|
12359
|
-
|
|
12360
|
-
|
|
12361
|
-
|
|
12362
|
-
|
|
12253
|
+
const [min0, max0] = niced(vals0, includeZero0);
|
|
12254
|
+
const [min1, max1] = niced(vals1, includeZero1);
|
|
12255
|
+
const span0 = max0 - min0;
|
|
12256
|
+
const span1 = max1 - min1;
|
|
12257
|
+
if (span0 === 0 || span1 === 0) return void 0;
|
|
12258
|
+
const zf0 = (0 - min0) / span0;
|
|
12259
|
+
const zf1 = (0 - min1) / span1;
|
|
12260
|
+
const zeroInDomain0 = zf0 >= -1e-3 && zf0 <= 1.001;
|
|
12261
|
+
const zeroInDomain1 = zf1 >= -1e-3 && zf1 <= 1.001;
|
|
12262
|
+
if (!zeroInDomain0 || !zeroInDomain1) return void 0;
|
|
12263
|
+
if (Math.abs(zf0 - zf1) < 1e-3) {
|
|
12264
|
+
return { domain0: [min0, max0], domain1: [min1, max1] };
|
|
12265
|
+
}
|
|
12266
|
+
const targetZf = Math.max(zf0, zf1);
|
|
12267
|
+
const align = (dMin, dMax, currentZf) => {
|
|
12268
|
+
if (Math.abs(currentZf - targetZf) < 1e-3) return [dMin, dMax];
|
|
12269
|
+
if (targetZf > currentZf) {
|
|
12270
|
+
const newMin = -(targetZf / (1 - targetZf)) * dMax;
|
|
12271
|
+
return [newMin, dMax];
|
|
12272
|
+
}
|
|
12273
|
+
const newMax = -dMin * (1 - targetZf) / targetZf;
|
|
12274
|
+
return [dMin, newMax];
|
|
12363
12275
|
};
|
|
12364
|
-
const
|
|
12365
|
-
|
|
12366
|
-
|
|
12367
|
-
|
|
12368
|
-
|
|
12369
|
-
|
|
12370
|
-
footer: graphSpec.chrome.footer
|
|
12371
|
-
},
|
|
12372
|
-
theme,
|
|
12373
|
-
options.width,
|
|
12374
|
-
options.measureText,
|
|
12375
|
-
"full",
|
|
12376
|
-
void 0,
|
|
12377
|
-
watermark
|
|
12378
|
-
);
|
|
12276
|
+
const domain0 = align(min0, max0, zf0);
|
|
12277
|
+
const domain1 = align(min1, max1, zf1);
|
|
12278
|
+
return { domain0, domain1 };
|
|
12279
|
+
}
|
|
12280
|
+
function withYDomain(leaf, domain) {
|
|
12281
|
+
if (!leaf.encoding?.y) return leaf;
|
|
12379
12282
|
return {
|
|
12380
|
-
|
|
12381
|
-
|
|
12382
|
-
|
|
12383
|
-
|
|
12384
|
-
|
|
12385
|
-
|
|
12386
|
-
|
|
12387
|
-
|
|
12388
|
-
|
|
12389
|
-
|
|
12390
|
-
}
|
|
12391
|
-
simulationConfig,
|
|
12392
|
-
watermark
|
|
12283
|
+
...leaf,
|
|
12284
|
+
encoding: {
|
|
12285
|
+
...leaf.encoding,
|
|
12286
|
+
y: {
|
|
12287
|
+
...leaf.encoding.y,
|
|
12288
|
+
scale: {
|
|
12289
|
+
...leaf.encoding.y.scale,
|
|
12290
|
+
domain
|
|
12291
|
+
}
|
|
12292
|
+
}
|
|
12293
|
+
}
|
|
12393
12294
|
};
|
|
12394
12295
|
}
|
|
12395
|
-
|
|
12396
|
-
|
|
12397
|
-
|
|
12398
|
-
|
|
12399
|
-
|
|
12400
|
-
|
|
12401
|
-
|
|
12402
|
-
|
|
12403
|
-
|
|
12404
|
-
|
|
12296
|
+
function buildPrimarySpec(leaves, layerSpec) {
|
|
12297
|
+
const allData = leaves.flatMap((leaf) => leaf.data);
|
|
12298
|
+
const primary = {
|
|
12299
|
+
...leaves[0],
|
|
12300
|
+
data: allData,
|
|
12301
|
+
chrome: layerSpec.chrome ?? leaves[0].chrome,
|
|
12302
|
+
annotations: layerSpec.annotations ?? leaves[0].annotations,
|
|
12303
|
+
labels: layerSpec.labels ?? leaves[0].labels,
|
|
12304
|
+
legend: layerSpec.legend ?? leaves[0].legend,
|
|
12305
|
+
responsive: layerSpec.responsive ?? leaves[0].responsive,
|
|
12306
|
+
theme: layerSpec.theme ?? leaves[0].theme,
|
|
12307
|
+
darkMode: layerSpec.darkMode ?? leaves[0].darkMode,
|
|
12308
|
+
watermark: layerSpec.watermark ?? leaves[0].watermark,
|
|
12309
|
+
hiddenSeries: layerSpec.hiddenSeries ?? leaves[0].hiddenSeries,
|
|
12310
|
+
endpointLabels: layerSpec.endpointLabels ?? leaves[0].endpointLabels
|
|
12311
|
+
};
|
|
12312
|
+
return primary;
|
|
12313
|
+
}
|
|
12405
12314
|
|
|
12406
|
-
// src/
|
|
12407
|
-
import {
|
|
12408
|
-
var
|
|
12409
|
-
var
|
|
12410
|
-
|
|
12411
|
-
|
|
12315
|
+
// src/compile/watermark-obstacle.ts
|
|
12316
|
+
import { BRAND_RESERVE_WIDTH as BRAND_RESERVE_WIDTH2 } from "@opendata-ai/openchart-core";
|
|
12317
|
+
var WATERMARK_HEIGHT = 30;
|
|
12318
|
+
var X_AXIS_EXTENT_WITH_LABEL = 48;
|
|
12319
|
+
var X_AXIS_EXTENT_TICKS_ONLY = 26;
|
|
12320
|
+
function computeWatermarkObstacle(dims, watermark, axes, theme) {
|
|
12321
|
+
if (!watermark) return null;
|
|
12322
|
+
const chartArea = dims.chartArea;
|
|
12323
|
+
const brandPadding = theme.spacing.padding;
|
|
12324
|
+
const brandX = dims.total.width - brandPadding - BRAND_RESERVE_WIDTH2;
|
|
12325
|
+
const xAxisExtent = axes.x?.label ? X_AXIS_EXTENT_WITH_LABEL : axes.x ? X_AXIS_EXTENT_TICKS_ONLY : 0;
|
|
12326
|
+
const firstBottomChrome = dims.chrome.source ?? dims.chrome.byline ?? dims.chrome.footer;
|
|
12327
|
+
const brandY = firstBottomChrome ? chartArea.y + chartArea.height + xAxisExtent + firstBottomChrome.y : chartArea.y + chartArea.height + xAxisExtent + theme.spacing.chartToFooter;
|
|
12328
|
+
return { x: brandX, y: brandY, width: BRAND_RESERVE_WIDTH2, height: WATERMARK_HEIGHT };
|
|
12412
12329
|
}
|
|
12413
|
-
|
|
12414
|
-
|
|
12415
|
-
|
|
12416
|
-
|
|
12417
|
-
|
|
12418
|
-
|
|
12419
|
-
|
|
12420
|
-
const aBottom = sorted[i].position + labelHeight / 2;
|
|
12421
|
-
const bTop = sorted[i + 1].position - labelHeight / 2;
|
|
12422
|
-
if (aBottom + minGap > bTop) return true;
|
|
12423
|
-
}
|
|
12424
|
-
return false;
|
|
12330
|
+
|
|
12331
|
+
// src/compiler/sparkline-defaults.ts
|
|
12332
|
+
var TREND_DEADBAND = 5e-4;
|
|
12333
|
+
function computeTrend(values) {
|
|
12334
|
+
const finite = [];
|
|
12335
|
+
for (const v of values) {
|
|
12336
|
+
if (Number.isFinite(v)) finite.push(v);
|
|
12425
12337
|
}
|
|
12426
|
-
|
|
12427
|
-
|
|
12428
|
-
|
|
12429
|
-
|
|
12430
|
-
|
|
12431
|
-
|
|
12338
|
+
if (finite.length < 2) return "neutral";
|
|
12339
|
+
const n = finite.length;
|
|
12340
|
+
let sumX = 0;
|
|
12341
|
+
let sumY = 0;
|
|
12342
|
+
for (let i = 0; i < n; i++) {
|
|
12343
|
+
sumX += i;
|
|
12344
|
+
sumY += finite[i];
|
|
12432
12345
|
}
|
|
12433
|
-
|
|
12434
|
-
|
|
12435
|
-
|
|
12436
|
-
|
|
12437
|
-
let
|
|
12438
|
-
|
|
12439
|
-
|
|
12440
|
-
|
|
12441
|
-
thinned.push(current[i]);
|
|
12442
|
-
}
|
|
12443
|
-
if (current.length > 1) thinned.push(current[current.length - 1]);
|
|
12444
|
-
current = thinned;
|
|
12445
|
-
if (!ticksOverlap(current, fontSize, fontWeight, measureText, orientation)) break;
|
|
12346
|
+
const meanX = sumX / n;
|
|
12347
|
+
const meanY = sumY / n;
|
|
12348
|
+
let num = 0;
|
|
12349
|
+
let den = 0;
|
|
12350
|
+
for (let i = 0; i < n; i++) {
|
|
12351
|
+
const dx = i - meanX;
|
|
12352
|
+
num += dx * (finite[i] - meanY);
|
|
12353
|
+
den += dx * dx;
|
|
12446
12354
|
}
|
|
12447
|
-
return
|
|
12355
|
+
if (den === 0) return "neutral";
|
|
12356
|
+
const slope = num / den;
|
|
12357
|
+
const scale = Math.abs(meanY) || 1;
|
|
12358
|
+
const relSlope = slope / scale;
|
|
12359
|
+
if (Math.abs(relSlope) < TREND_DEADBAND) return "neutral";
|
|
12360
|
+
return relSlope > 0 ? "up" : "down";
|
|
12448
12361
|
}
|
|
12449
|
-
|
|
12450
|
-
|
|
12451
|
-
|
|
12452
|
-
|
|
12453
|
-
var WIDTH_MINIMAL_THRESHOLD = 150;
|
|
12454
|
-
var WIDTH_REDUCED_THRESHOLD = 300;
|
|
12455
|
-
var DENSITY_ORDER = ["full", "reduced", "minimal"];
|
|
12456
|
-
var GROUNDED_MARKS = /* @__PURE__ */ new Set(["bar", "tick", "lollipop"]);
|
|
12457
|
-
function effectiveDensity(baseDensity, axisLength, minimalThreshold, reducedThreshold) {
|
|
12458
|
-
let density = baseDensity;
|
|
12459
|
-
if (axisLength < minimalThreshold) {
|
|
12460
|
-
density = "minimal";
|
|
12461
|
-
} else if (axisLength < reducedThreshold) {
|
|
12462
|
-
const baseIdx = DENSITY_ORDER.indexOf(baseDensity);
|
|
12463
|
-
const reducedIdx = DENSITY_ORDER.indexOf("reduced");
|
|
12464
|
-
density = DENSITY_ORDER[Math.max(baseIdx, reducedIdx)];
|
|
12465
|
-
}
|
|
12466
|
-
return density;
|
|
12362
|
+
function trendColor(trend, theme) {
|
|
12363
|
+
if (trend === "up") return theme.colors.positive;
|
|
12364
|
+
if (trend === "down") return theme.colors.negative;
|
|
12365
|
+
return theme.colors.categorical[0];
|
|
12467
12366
|
}
|
|
12468
|
-
|
|
12469
|
-
|
|
12470
|
-
|
|
12471
|
-
|
|
12472
|
-
|
|
12473
|
-
|
|
12474
|
-
let best = ticks2;
|
|
12475
|
-
for (let n = Math.max(requestedCount, MIN_QUANTITATIVE_TICKS) + 1; n <= 10; n++) {
|
|
12476
|
-
const candidate = buildContinuousTicks(scale, n);
|
|
12477
|
-
if (candidate.length >= MIN_QUANTITATIVE_TICKS) return candidate;
|
|
12478
|
-
if (candidate.length > best.length) best = candidate;
|
|
12367
|
+
function computeTrendFromData(data, yField) {
|
|
12368
|
+
if (!yField) return "neutral";
|
|
12369
|
+
const values = [];
|
|
12370
|
+
for (const row of data) {
|
|
12371
|
+
const v = Number(row[yField]);
|
|
12372
|
+
if (Number.isFinite(v)) values.push(v);
|
|
12479
12373
|
}
|
|
12480
|
-
return
|
|
12374
|
+
return computeTrend(values);
|
|
12481
12375
|
}
|
|
12482
|
-
function
|
|
12483
|
-
if (
|
|
12484
|
-
|
|
12485
|
-
|
|
12486
|
-
|
|
12487
|
-
|
|
12488
|
-
if (!overshoots && !overlaps) return flooredTicks;
|
|
12489
|
-
const minThreshold = orientation === "vertical" ? HEIGHT_MINIMAL_THRESHOLD : WIDTH_MINIMAL_THRESHOLD;
|
|
12490
|
-
const floor = axisLength >= minThreshold ? CONTINUOUS_TICK_FLOOR : 2;
|
|
12491
|
-
let bestWithinFloor;
|
|
12492
|
-
for (let n = initialCount - 1; n >= 2; n--) {
|
|
12493
|
-
const candidate = buildContinuousTicks(scale, n);
|
|
12494
|
-
const candidateOvershoots = candidate.length > tolerance;
|
|
12495
|
-
const candidateOverlaps = ticksOverlap(
|
|
12496
|
-
candidate,
|
|
12497
|
-
fontSize,
|
|
12498
|
-
fontWeight,
|
|
12499
|
-
measureText,
|
|
12500
|
-
orientation
|
|
12501
|
-
);
|
|
12502
|
-
if (!candidateOvershoots && !candidateOverlaps) {
|
|
12503
|
-
return candidate;
|
|
12504
|
-
}
|
|
12505
|
-
if (candidate.length >= floor) bestWithinFloor = candidate;
|
|
12506
|
-
}
|
|
12507
|
-
const rawFallback = bestWithinFloor ?? buildContinuousTicks(scale, floor);
|
|
12508
|
-
const fallback = ensureMinContinuousTicks(scale, rawFallback, floor);
|
|
12509
|
-
return thinTicksUntilFit(fallback, fontSize, fontWeight, measureText, orientation);
|
|
12376
|
+
function hasExplicitColor(markDef, encodingHasColor) {
|
|
12377
|
+
if (encodingHasColor) return { fill: true, stroke: true };
|
|
12378
|
+
return {
|
|
12379
|
+
fill: markDef?.fill !== void 0,
|
|
12380
|
+
stroke: markDef?.stroke !== void 0
|
|
12381
|
+
};
|
|
12510
12382
|
}
|
|
12511
|
-
function
|
|
12512
|
-
|
|
12513
|
-
|
|
12514
|
-
|
|
12515
|
-
|
|
12516
|
-
|
|
12517
|
-
|
|
12518
|
-
|
|
12519
|
-
|
|
12520
|
-
|
|
12521
|
-
|
|
12522
|
-
}
|
|
12523
|
-
return false;
|
|
12383
|
+
function buildSparklineAreaGradient(baseColor) {
|
|
12384
|
+
return {
|
|
12385
|
+
gradient: "linear",
|
|
12386
|
+
x1: 0,
|
|
12387
|
+
y1: 0,
|
|
12388
|
+
x2: 0,
|
|
12389
|
+
y2: 1,
|
|
12390
|
+
stops: [
|
|
12391
|
+
{ offset: 0, color: baseColor, opacity: 0.35 },
|
|
12392
|
+
{ offset: 1, color: baseColor, opacity: 0 }
|
|
12393
|
+
]
|
|
12394
|
+
};
|
|
12524
12395
|
}
|
|
12525
|
-
|
|
12526
|
-
|
|
12527
|
-
|
|
12528
|
-
|
|
12529
|
-
|
|
12530
|
-
|
|
12531
|
-
|
|
12532
|
-
|
|
12533
|
-
|
|
12534
|
-
|
|
12535
|
-
|
|
12396
|
+
|
|
12397
|
+
// src/graphs/compile-graph.ts
|
|
12398
|
+
import { adaptTheme as adaptTheme2, computeChrome as computeChrome3, resolveTheme as resolveTheme3 } from "@opendata-ai/openchart-core";
|
|
12399
|
+
|
|
12400
|
+
// src/graphs/encoding.ts
|
|
12401
|
+
var DEFAULT_NODE_RADIUS = 5;
|
|
12402
|
+
var MIN_NODE_RADIUS = 3;
|
|
12403
|
+
var MAX_NODE_RADIUS = 12;
|
|
12404
|
+
var DEFAULT_EDGE_WIDTH = 1;
|
|
12405
|
+
var MIN_EDGE_WIDTH = 0.5;
|
|
12406
|
+
var MAX_EDGE_WIDTH = 4;
|
|
12407
|
+
var DEFAULT_STROKE_WIDTH2 = 1;
|
|
12408
|
+
function darkenColor(hex2, amount = 0.2) {
|
|
12409
|
+
const clean = hex2.replace(/^#/, "");
|
|
12410
|
+
if (clean.length !== 6 && clean.length !== 3) return hex2;
|
|
12411
|
+
const full = clean.length === 3 ? clean.split("").map((c) => c + c).join("") : clean;
|
|
12412
|
+
const r = Math.max(0, Math.round(parseInt(full.substring(0, 2), 16) * (1 - amount)));
|
|
12413
|
+
const g = Math.max(0, Math.round(parseInt(full.substring(2, 4), 16) * (1 - amount)));
|
|
12414
|
+
const b = Math.max(0, Math.round(parseInt(full.substring(4, 6), 16) * (1 - amount)));
|
|
12415
|
+
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
|
|
12416
|
+
}
|
|
12417
|
+
function hexWithOpacity(hex2, opacity) {
|
|
12418
|
+
const clean = hex2.replace(/^#/, "");
|
|
12419
|
+
if (clean.length !== 6 && clean.length !== 3) {
|
|
12420
|
+
return hex2;
|
|
12536
12421
|
}
|
|
12537
|
-
|
|
12422
|
+
const full = clean.length === 3 ? clean.split("").map((c) => c + c).join("") : clean;
|
|
12423
|
+
const r = parseInt(full.substring(0, 2), 16);
|
|
12424
|
+
const g = parseInt(full.substring(2, 4), 16);
|
|
12425
|
+
const b = parseInt(full.substring(4, 6), 16);
|
|
12426
|
+
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
|
12538
12427
|
}
|
|
12539
|
-
function
|
|
12540
|
-
const
|
|
12541
|
-
const
|
|
12542
|
-
|
|
12543
|
-
|
|
12544
|
-
|
|
12545
|
-
|
|
12546
|
-
|
|
12547
|
-
|
|
12548
|
-
|
|
12549
|
-
|
|
12550
|
-
|
|
12551
|
-
|
|
12552
|
-
|
|
12553
|
-
|
|
12554
|
-
|
|
12555
|
-
|
|
12556
|
-
|
|
12557
|
-
|
|
12558
|
-
|
|
12559
|
-
|
|
12560
|
-
|
|
12561
|
-
|
|
12562
|
-
|
|
12563
|
-
|
|
12564
|
-
|
|
12565
|
-
|
|
12566
|
-
|
|
12567
|
-
|
|
12568
|
-
|
|
12569
|
-
|
|
12570
|
-
|
|
12571
|
-
|
|
12572
|
-
|
|
12573
|
-
|
|
12574
|
-
|
|
12575
|
-
|
|
12576
|
-
|
|
12577
|
-
|
|
12578
|
-
|
|
12579
|
-
allTicks = categoricalTicks(scales.x, xDensity, "horizontal");
|
|
12428
|
+
function computeDegrees(nodes, edges) {
|
|
12429
|
+
const degrees = /* @__PURE__ */ new Map();
|
|
12430
|
+
for (const node of nodes) {
|
|
12431
|
+
degrees.set(node.id, 0);
|
|
12432
|
+
}
|
|
12433
|
+
for (const edge of edges) {
|
|
12434
|
+
degrees.set(edge.source, (degrees.get(edge.source) ?? 0) + 1);
|
|
12435
|
+
degrees.set(edge.target, (degrees.get(edge.target) ?? 0) + 1);
|
|
12436
|
+
}
|
|
12437
|
+
return degrees;
|
|
12438
|
+
}
|
|
12439
|
+
function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
|
|
12440
|
+
const degrees = computeDegrees(nodes, edges);
|
|
12441
|
+
const maxDegree = Math.max(1, ...degrees.values());
|
|
12442
|
+
let sizeScale;
|
|
12443
|
+
if (encoding.nodeSize?.field) {
|
|
12444
|
+
const field = encoding.nodeSize.field;
|
|
12445
|
+
const values = nodes.map((n) => Number(n[field])).filter((v) => Number.isFinite(v));
|
|
12446
|
+
const sizeMin = min2(values) ?? 0;
|
|
12447
|
+
const sizeMax = max2(values) ?? 1;
|
|
12448
|
+
sizeScale = sqrt2().domain([sizeMin, sizeMax]).range([MIN_NODE_RADIUS, MAX_NODE_RADIUS]);
|
|
12449
|
+
}
|
|
12450
|
+
let colorFn;
|
|
12451
|
+
if (encoding.nodeColor?.field) {
|
|
12452
|
+
const field = encoding.nodeColor.field;
|
|
12453
|
+
const fieldType = encoding.nodeColor.type ?? "nominal";
|
|
12454
|
+
const scaleConfig = encoding.nodeColor.scale;
|
|
12455
|
+
if (fieldType === "quantitative") {
|
|
12456
|
+
const values = nodes.map((n) => Number(n[field])).filter((v) => Number.isFinite(v));
|
|
12457
|
+
const colorMin = min2(values) ?? 0;
|
|
12458
|
+
const colorMax = max2(values) ?? 1;
|
|
12459
|
+
const seqPalettes = Object.values(theme.colors.sequential);
|
|
12460
|
+
const palette = seqPalettes.length > 0 ? seqPalettes[0] : ["#ccc", "#333"];
|
|
12461
|
+
const domain = scaleConfig?.domain && scaleConfig.domain.length === 2 ? scaleConfig.domain : [colorMin, colorMax];
|
|
12462
|
+
const range2 = scaleConfig?.range && scaleConfig.range.length >= 2 ? scaleConfig.range : [palette[0], palette[palette.length - 1]];
|
|
12463
|
+
const colorScale = linear2().domain(domain).range(range2);
|
|
12464
|
+
colorFn = (node) => {
|
|
12465
|
+
const val = Number(node[field]);
|
|
12466
|
+
return Number.isFinite(val) ? colorScale(val) : theme.colors.categorical[0];
|
|
12467
|
+
};
|
|
12580
12468
|
} else {
|
|
12581
|
-
|
|
12469
|
+
const domain = scaleConfig?.domain && Array.isArray(scaleConfig.domain) ? scaleConfig.domain : [...new Set(nodes.map((n) => String(n[field] ?? "")))];
|
|
12470
|
+
const range2 = scaleConfig?.range && scaleConfig.range.length > 0 ? scaleConfig.range : theme.colors.categorical;
|
|
12471
|
+
const ordinalScale = ordinal().domain(domain).range(range2);
|
|
12472
|
+
colorFn = (node) => ordinalScale(String(node[field] ?? ""));
|
|
12582
12473
|
}
|
|
12583
|
-
|
|
12584
|
-
|
|
12585
|
-
|
|
12586
|
-
|
|
12587
|
-
|
|
12588
|
-
|
|
12589
|
-
|
|
12590
|
-
|
|
12591
|
-
for (const t of allTicks) {
|
|
12592
|
-
const w = measureLabel(t.label, fontSize, fontWeight, measureText);
|
|
12593
|
-
if (w > maxLabelWidth) maxLabelWidth = w;
|
|
12474
|
+
}
|
|
12475
|
+
const defaultColor = theme.colors.categorical[0];
|
|
12476
|
+
return nodes.map((node) => {
|
|
12477
|
+
let radius = DEFAULT_NODE_RADIUS;
|
|
12478
|
+
if (sizeScale && encoding.nodeSize?.field) {
|
|
12479
|
+
const val = Number(node[encoding.nodeSize.field]);
|
|
12480
|
+
if (Number.isFinite(val)) {
|
|
12481
|
+
radius = sizeScale(val);
|
|
12594
12482
|
}
|
|
12595
|
-
tickAngle = resolveBandTickAngle(void 0, maxLabelWidth, bandwidth, allTicks.length);
|
|
12596
12483
|
}
|
|
12597
|
-
const
|
|
12598
|
-
|
|
12599
|
-
|
|
12600
|
-
|
|
12601
|
-
|
|
12602
|
-
|
|
12603
|
-
ticks2 = thinBandTicksIfNeeded(allTicks, effectiveAngle, fontSize, fontWeight, measureText);
|
|
12604
|
-
} else if (isContinuousX) {
|
|
12605
|
-
ticks2 = fitContinuousTicks(
|
|
12606
|
-
scales.x,
|
|
12607
|
-
allTicks,
|
|
12608
|
-
xTargetCount ?? allTicks.length,
|
|
12609
|
-
fontSize,
|
|
12610
|
-
fontWeight,
|
|
12611
|
-
chartArea.width,
|
|
12612
|
-
"horizontal",
|
|
12613
|
-
measureText
|
|
12614
|
-
);
|
|
12484
|
+
const fill = colorFn ? colorFn(node) : defaultColor;
|
|
12485
|
+
const stroke = darkenColor(fill);
|
|
12486
|
+
let label;
|
|
12487
|
+
if (encoding.nodeLabel?.field) {
|
|
12488
|
+
const labelVal = node[encoding.nodeLabel.field];
|
|
12489
|
+
label = labelVal != null ? String(labelVal) : void 0;
|
|
12615
12490
|
} else {
|
|
12616
|
-
|
|
12491
|
+
label = node.id;
|
|
12617
12492
|
}
|
|
12618
|
-
const
|
|
12619
|
-
const
|
|
12620
|
-
const
|
|
12621
|
-
const
|
|
12622
|
-
|
|
12623
|
-
|
|
12624
|
-
|
|
12625
|
-
|
|
12626
|
-
|
|
12627
|
-
|
|
12628
|
-
|
|
12629
|
-
|
|
12630
|
-
|
|
12631
|
-
|
|
12632
|
-
|
|
12633
|
-
|
|
12634
|
-
|
|
12635
|
-
|
|
12636
|
-
|
|
12637
|
-
|
|
12638
|
-
labelFlush: axisConfig?.labelFlush,
|
|
12639
|
-
tickPosition: xTickPosition
|
|
12493
|
+
const degree = degrees.get(node.id) ?? 0;
|
|
12494
|
+
const labelPriority = maxDegree > 0 ? degree / maxDegree : 0;
|
|
12495
|
+
const { id: _id, ...rest } = node;
|
|
12496
|
+
const data = { id: node.id, ...rest };
|
|
12497
|
+
const override = nodeOverrides?.[node.id];
|
|
12498
|
+
const finalFill = override?.fill ?? fill;
|
|
12499
|
+
const finalRadius = override?.radius ?? radius;
|
|
12500
|
+
const finalStrokeWidth = override?.strokeWidth ?? DEFAULT_STROKE_WIDTH2;
|
|
12501
|
+
const finalStroke = override?.stroke ?? stroke;
|
|
12502
|
+
const finalLabelPriority = override?.alwaysShowLabel ? Infinity : labelPriority;
|
|
12503
|
+
return {
|
|
12504
|
+
id: node.id,
|
|
12505
|
+
radius: finalRadius,
|
|
12506
|
+
fill: finalFill,
|
|
12507
|
+
stroke: finalStroke,
|
|
12508
|
+
strokeWidth: finalStrokeWidth,
|
|
12509
|
+
label,
|
|
12510
|
+
labelPriority: finalLabelPriority,
|
|
12511
|
+
community: void 0,
|
|
12512
|
+
data
|
|
12640
12513
|
};
|
|
12514
|
+
});
|
|
12515
|
+
}
|
|
12516
|
+
function resolveEdgeVisuals(edges, encoding, theme) {
|
|
12517
|
+
let widthScale;
|
|
12518
|
+
if (encoding.edgeWidth?.field) {
|
|
12519
|
+
const field = encoding.edgeWidth.field;
|
|
12520
|
+
const values = edges.map((e) => Number(e[field])).filter((v) => Number.isFinite(v));
|
|
12521
|
+
const widthMin = min2(values) ?? 0;
|
|
12522
|
+
const widthMax = max2(values) ?? 1;
|
|
12523
|
+
widthScale = linear2().domain([widthMin, widthMax]).range([MIN_EDGE_WIDTH, MAX_EDGE_WIDTH]);
|
|
12641
12524
|
}
|
|
12642
|
-
|
|
12643
|
-
|
|
12644
|
-
const
|
|
12645
|
-
const
|
|
12646
|
-
|
|
12647
|
-
if (
|
|
12648
|
-
|
|
12649
|
-
|
|
12650
|
-
|
|
12651
|
-
|
|
12652
|
-
const
|
|
12653
|
-
const
|
|
12654
|
-
|
|
12655
|
-
|
|
12656
|
-
|
|
12657
|
-
|
|
12658
|
-
|
|
12659
|
-
|
|
12525
|
+
let edgeColorFn;
|
|
12526
|
+
if (encoding.edgeColor?.field) {
|
|
12527
|
+
const field = encoding.edgeColor.field;
|
|
12528
|
+
const fieldType = encoding.edgeColor.type ?? "nominal";
|
|
12529
|
+
const scaleConfig = encoding.edgeColor.scale;
|
|
12530
|
+
if (fieldType === "quantitative") {
|
|
12531
|
+
const values = edges.map((e) => Number(e[field])).filter((v) => Number.isFinite(v));
|
|
12532
|
+
const colorMin = min2(values) ?? 0;
|
|
12533
|
+
const colorMax = max2(values) ?? 1;
|
|
12534
|
+
const seqPalettes = Object.values(theme.colors.sequential);
|
|
12535
|
+
const palette = seqPalettes.length > 0 ? seqPalettes[0] : ["#ccc", "#333"];
|
|
12536
|
+
const domain = scaleConfig?.domain && scaleConfig.domain.length === 2 ? scaleConfig.domain : [colorMin, colorMax];
|
|
12537
|
+
const range2 = scaleConfig?.range && scaleConfig.range.length >= 2 ? scaleConfig.range : [palette[0], palette[palette.length - 1]];
|
|
12538
|
+
const colorScale = linear2().domain(domain).range(range2);
|
|
12539
|
+
edgeColorFn = (edge) => {
|
|
12540
|
+
const val = Number(edge[field]);
|
|
12541
|
+
return Number.isFinite(val) ? colorScale(val) : hexWithOpacity(theme.colors.axis, 0.4);
|
|
12542
|
+
};
|
|
12660
12543
|
} else {
|
|
12661
|
-
|
|
12544
|
+
const domain = scaleConfig?.domain && Array.isArray(scaleConfig.domain) ? scaleConfig.domain : [...new Set(edges.map((e) => String(e[field] ?? "")))];
|
|
12545
|
+
const range2 = scaleConfig?.range && scaleConfig.range.length > 0 ? scaleConfig.range : theme.colors.categorical;
|
|
12546
|
+
const ordinalScale = ordinal().domain(domain).range(range2);
|
|
12547
|
+
edgeColorFn = (edge) => ordinalScale(String(edge[field] ?? ""));
|
|
12662
12548
|
}
|
|
12663
|
-
|
|
12664
|
-
|
|
12665
|
-
|
|
12666
|
-
|
|
12667
|
-
|
|
12668
|
-
|
|
12669
|
-
|
|
12670
|
-
|
|
12671
|
-
|
|
12672
|
-
|
|
12673
|
-
fontWeight,
|
|
12674
|
-
chartArea.height,
|
|
12675
|
-
"vertical",
|
|
12676
|
-
measureText
|
|
12677
|
-
);
|
|
12678
|
-
} else {
|
|
12679
|
-
ticks2 = thinTicksUntilFit(allTicks, fontSize, fontWeight, measureText, "vertical");
|
|
12549
|
+
}
|
|
12550
|
+
const defaultEdgeColor = hexWithOpacity(theme.colors.axis, 0.4);
|
|
12551
|
+
const EDGE_STYLES = ["solid", "dashed", "dotted"];
|
|
12552
|
+
let styleFn;
|
|
12553
|
+
if (encoding.edgeStyle?.field) {
|
|
12554
|
+
const field = encoding.edgeStyle.field;
|
|
12555
|
+
const uniqueValues = [...new Set(edges.map((e) => String(e[field] ?? "")))];
|
|
12556
|
+
const styleMap = /* @__PURE__ */ new Map();
|
|
12557
|
+
for (let i = 0; i < uniqueValues.length; i++) {
|
|
12558
|
+
styleMap.set(uniqueValues[i], EDGE_STYLES[i % EDGE_STYLES.length]);
|
|
12680
12559
|
}
|
|
12681
|
-
|
|
12682
|
-
|
|
12683
|
-
|
|
12684
|
-
}
|
|
12685
|
-
|
|
12686
|
-
|
|
12687
|
-
|
|
12688
|
-
|
|
12689
|
-
|
|
12690
|
-
|
|
12691
|
-
|
|
12692
|
-
const
|
|
12693
|
-
|
|
12694
|
-
|
|
12695
|
-
|
|
12696
|
-
|
|
12697
|
-
|
|
12698
|
-
|
|
12699
|
-
|
|
12700
|
-
|
|
12701
|
-
start: { x: chartArea.x, y: chartArea.y },
|
|
12702
|
-
end: { x: chartArea.x, y: chartArea.y + chartArea.height },
|
|
12703
|
-
orient: axisConfig?.orient,
|
|
12704
|
-
domainLine: yDomainLine,
|
|
12705
|
-
tickMarks: yTickMarks,
|
|
12706
|
-
offset: axisConfig?.offset,
|
|
12707
|
-
titlePadding: axisConfig?.titlePadding,
|
|
12708
|
-
labelPadding: axisConfig?.labelPadding,
|
|
12709
|
-
labelOverlap: axisConfig?.labelOverlap,
|
|
12710
|
-
labelFlush: axisConfig?.labelFlush,
|
|
12711
|
-
tickPosition: yTickPosition
|
|
12560
|
+
styleFn = (edge) => styleMap.get(String(edge[field] ?? "")) ?? "solid";
|
|
12561
|
+
}
|
|
12562
|
+
return edges.map((edge) => {
|
|
12563
|
+
const { source, target, ...rest } = edge;
|
|
12564
|
+
let strokeWidth = DEFAULT_EDGE_WIDTH;
|
|
12565
|
+
if (widthScale && encoding.edgeWidth?.field) {
|
|
12566
|
+
const val = Number(edge[encoding.edgeWidth.field]);
|
|
12567
|
+
if (Number.isFinite(val)) {
|
|
12568
|
+
strokeWidth = widthScale(val);
|
|
12569
|
+
}
|
|
12570
|
+
}
|
|
12571
|
+
const stroke = edgeColorFn ? edgeColorFn(edge) : defaultEdgeColor;
|
|
12572
|
+
const style = styleFn ? styleFn(edge) : "solid";
|
|
12573
|
+
return {
|
|
12574
|
+
source,
|
|
12575
|
+
target,
|
|
12576
|
+
stroke,
|
|
12577
|
+
strokeWidth,
|
|
12578
|
+
style,
|
|
12579
|
+
data: { source, target, ...rest }
|
|
12712
12580
|
};
|
|
12581
|
+
});
|
|
12582
|
+
}
|
|
12583
|
+
|
|
12584
|
+
// src/graphs/community.ts
|
|
12585
|
+
function assignCommunities(nodes, clusteringField) {
|
|
12586
|
+
if (!clusteringField) return;
|
|
12587
|
+
for (const node of nodes) {
|
|
12588
|
+
const value2 = node.data[clusteringField];
|
|
12589
|
+
node.community = value2 != null ? String(value2) : void 0;
|
|
12713
12590
|
}
|
|
12714
|
-
|
|
12715
|
-
|
|
12716
|
-
|
|
12717
|
-
|
|
12718
|
-
|
|
12719
|
-
|
|
12720
|
-
|
|
12721
|
-
|
|
12722
|
-
|
|
12723
|
-
xAxisHeight: theme.spacing.xAxisHeight,
|
|
12724
|
-
measure: (t, fs, fw) => measureLabel(t, fs, fw, measureText)
|
|
12725
|
-
});
|
|
12726
|
-
if (result.x.label) {
|
|
12727
|
-
const rotatedBand = isRotated ? result.x.extent - X_AXIS_TITLE_BAND_ROTATED + 14 : 35;
|
|
12728
|
-
result.x.titlePosition = {
|
|
12729
|
-
x: chartArea.x + chartArea.width / 2,
|
|
12730
|
-
y: chartArea.y + chartArea.height + rotatedBand
|
|
12731
|
-
};
|
|
12591
|
+
}
|
|
12592
|
+
function buildCommunityColorMap(nodes, theme) {
|
|
12593
|
+
const colorMap = /* @__PURE__ */ new Map();
|
|
12594
|
+
const palette = theme.colors.categorical;
|
|
12595
|
+
let colorIndex = 0;
|
|
12596
|
+
for (const node of nodes) {
|
|
12597
|
+
if (node.community != null && !colorMap.has(node.community)) {
|
|
12598
|
+
colorMap.set(node.community, palette[colorIndex % palette.length]);
|
|
12599
|
+
colorIndex++;
|
|
12732
12600
|
}
|
|
12733
12601
|
}
|
|
12734
|
-
|
|
12735
|
-
|
|
12736
|
-
|
|
12737
|
-
|
|
12738
|
-
|
|
12739
|
-
|
|
12740
|
-
|
|
12741
|
-
|
|
12742
|
-
|
|
12602
|
+
return colorMap;
|
|
12603
|
+
}
|
|
12604
|
+
function applyCommunityColors(nodes, colorMap) {
|
|
12605
|
+
for (const node of nodes) {
|
|
12606
|
+
if (node.community != null) {
|
|
12607
|
+
const communityColor = colorMap.get(node.community);
|
|
12608
|
+
if (communityColor) {
|
|
12609
|
+
node.fill = communityColor;
|
|
12610
|
+
node.stroke = darkenColor(communityColor);
|
|
12611
|
+
}
|
|
12612
|
+
}
|
|
12613
|
+
}
|
|
12614
|
+
}
|
|
12615
|
+
|
|
12616
|
+
// src/graphs/compile-graph.ts
|
|
12617
|
+
var SWATCH_SIZE2 = 12;
|
|
12618
|
+
var SWATCH_GAP2 = 6;
|
|
12619
|
+
var ENTRY_GAP2 = 16;
|
|
12620
|
+
function buildGraphLegend(nodes, communityColorMap, hasCommunities, theme, nodeColorField) {
|
|
12621
|
+
const labelStyle = {
|
|
12622
|
+
fontFamily: theme.fonts.family,
|
|
12623
|
+
fontSize: theme.fonts.sizes.small,
|
|
12624
|
+
fontWeight: theme.fonts.weights.normal,
|
|
12625
|
+
fill: theme.colors.text,
|
|
12626
|
+
lineHeight: 1.3
|
|
12627
|
+
};
|
|
12628
|
+
let entries;
|
|
12629
|
+
if (hasCommunities && communityColorMap.size > 0) {
|
|
12630
|
+
entries = [...communityColorMap.entries()].map(([label, color2]) => ({
|
|
12631
|
+
label,
|
|
12632
|
+
color: color2,
|
|
12633
|
+
shape: "circle",
|
|
12634
|
+
active: true
|
|
12635
|
+
}));
|
|
12636
|
+
} else {
|
|
12637
|
+
const categoryColors = /* @__PURE__ */ new Map();
|
|
12638
|
+
for (const node of nodes) {
|
|
12639
|
+
const category = nodeColorField ? String(node.data[nodeColorField] ?? node.label ?? node.id) : node.label ?? node.id;
|
|
12640
|
+
if (!categoryColors.has(category)) {
|
|
12641
|
+
categoryColors.set(category, node.fill);
|
|
12642
|
+
}
|
|
12643
|
+
}
|
|
12644
|
+
if (categoryColors.size <= 1) {
|
|
12645
|
+
entries = [];
|
|
12743
12646
|
} else {
|
|
12744
|
-
|
|
12745
|
-
|
|
12746
|
-
|
|
12747
|
-
|
|
12748
|
-
|
|
12749
|
-
|
|
12750
|
-
return Math.max(max4, w);
|
|
12751
|
-
}, 0);
|
|
12752
|
-
const titleOff = axisTitleOffset2(maxTickLabelWidth, result.y.labelStyle.fontSize, totalWidth);
|
|
12753
|
-
result.y.titlePosition = {
|
|
12754
|
-
x: chartArea.x - titleOff,
|
|
12755
|
-
y: chartArea.y + chartArea.height / 2,
|
|
12756
|
-
angle: -90
|
|
12757
|
-
};
|
|
12647
|
+
entries = [...categoryColors.entries()].map(([label, color2]) => ({
|
|
12648
|
+
label,
|
|
12649
|
+
color: color2,
|
|
12650
|
+
shape: "circle",
|
|
12651
|
+
active: true
|
|
12652
|
+
}));
|
|
12758
12653
|
}
|
|
12759
12654
|
}
|
|
12760
|
-
return
|
|
12655
|
+
return {
|
|
12656
|
+
position: "top",
|
|
12657
|
+
entries,
|
|
12658
|
+
bounds: { x: 0, y: 0, width: 0, height: 0 },
|
|
12659
|
+
labelStyle,
|
|
12660
|
+
swatchSize: SWATCH_SIZE2,
|
|
12661
|
+
swatchGap: SWATCH_GAP2,
|
|
12662
|
+
entryGap: ENTRY_GAP2,
|
|
12663
|
+
swatchChipFill: theme.colors.annotationFill
|
|
12664
|
+
};
|
|
12665
|
+
}
|
|
12666
|
+
function buildGraphTooltips(nodes) {
|
|
12667
|
+
const descriptors = /* @__PURE__ */ new Map();
|
|
12668
|
+
for (const node of nodes) {
|
|
12669
|
+
const fields = [];
|
|
12670
|
+
if (node.community != null) {
|
|
12671
|
+
fields.push({
|
|
12672
|
+
label: "Community",
|
|
12673
|
+
value: node.community,
|
|
12674
|
+
color: node.fill
|
|
12675
|
+
});
|
|
12676
|
+
}
|
|
12677
|
+
for (const [key, value2] of Object.entries(node.data)) {
|
|
12678
|
+
if (key === "id") continue;
|
|
12679
|
+
if (value2 == null) continue;
|
|
12680
|
+
fields.push({
|
|
12681
|
+
label: key,
|
|
12682
|
+
value: typeof value2 === "number" ? value2.toLocaleString() : String(value2)
|
|
12683
|
+
});
|
|
12684
|
+
}
|
|
12685
|
+
descriptors.set(node.id, {
|
|
12686
|
+
title: node.label ?? node.id,
|
|
12687
|
+
fields
|
|
12688
|
+
});
|
|
12689
|
+
}
|
|
12690
|
+
return descriptors;
|
|
12691
|
+
}
|
|
12692
|
+
function compileGraph(spec, options) {
|
|
12693
|
+
const { spec: normalized } = compile(spec);
|
|
12694
|
+
if (!("type" in normalized) || normalized.type !== "graph") {
|
|
12695
|
+
throw new Error(
|
|
12696
|
+
"compileGraph received a non-graph spec. Use compileChart or compileTable instead."
|
|
12697
|
+
);
|
|
12698
|
+
}
|
|
12699
|
+
const graphSpec = normalized;
|
|
12700
|
+
const rawWatermark = spec.watermark;
|
|
12701
|
+
const watermark = rawWatermark !== void 0 ? graphSpec.watermark : options.watermark ?? true;
|
|
12702
|
+
const mergedThemeConfig = options.theme ? { ...graphSpec.theme, ...options.theme } : graphSpec.theme;
|
|
12703
|
+
let theme = resolveTheme3(mergedThemeConfig);
|
|
12704
|
+
if (options.darkMode) {
|
|
12705
|
+
theme = adaptTheme2(theme);
|
|
12706
|
+
}
|
|
12707
|
+
const compiledNodes = resolveNodeVisuals(
|
|
12708
|
+
graphSpec.nodes,
|
|
12709
|
+
graphSpec.encoding,
|
|
12710
|
+
graphSpec.edges,
|
|
12711
|
+
theme,
|
|
12712
|
+
graphSpec.nodeOverrides
|
|
12713
|
+
);
|
|
12714
|
+
const clusteringField = graphSpec.layout.clustering?.field;
|
|
12715
|
+
const hasCommunities = !!clusteringField;
|
|
12716
|
+
assignCommunities(compiledNodes, clusteringField);
|
|
12717
|
+
const hasNodeColorEncoding = !!graphSpec.encoding.nodeColor?.field;
|
|
12718
|
+
let communityColorMap = /* @__PURE__ */ new Map();
|
|
12719
|
+
if (hasCommunities && !hasNodeColorEncoding) {
|
|
12720
|
+
communityColorMap = buildCommunityColorMap(compiledNodes, theme);
|
|
12721
|
+
applyCommunityColors(compiledNodes, communityColorMap);
|
|
12722
|
+
}
|
|
12723
|
+
const compiledEdges = resolveEdgeVisuals(graphSpec.edges, graphSpec.encoding, theme);
|
|
12724
|
+
const useCommunitiesForLegend = hasCommunities && !hasNodeColorEncoding;
|
|
12725
|
+
const legend = buildGraphLegend(
|
|
12726
|
+
compiledNodes,
|
|
12727
|
+
communityColorMap,
|
|
12728
|
+
useCommunitiesForLegend,
|
|
12729
|
+
theme,
|
|
12730
|
+
graphSpec.encoding.nodeColor?.field
|
|
12731
|
+
);
|
|
12732
|
+
const tooltipDescriptors = buildGraphTooltips(compiledNodes);
|
|
12733
|
+
const communityCount = communityColorMap.size;
|
|
12734
|
+
const altParts = [
|
|
12735
|
+
`Network graph with ${compiledNodes.length} nodes and ${compiledEdges.length} edges`
|
|
12736
|
+
];
|
|
12737
|
+
if (communityCount > 0) {
|
|
12738
|
+
altParts.push(`organized into ${communityCount} communities`);
|
|
12739
|
+
}
|
|
12740
|
+
const a11y = {
|
|
12741
|
+
altText: altParts.join(", "),
|
|
12742
|
+
dataTableFallback: compiledNodes.map((n) => [n.id, n.community ?? "", n.label ?? ""]),
|
|
12743
|
+
role: "img",
|
|
12744
|
+
keyboardNavigable: compiledNodes.length > 0
|
|
12745
|
+
};
|
|
12746
|
+
const collisionPadding = graphSpec.layout.collisionPadding ?? 2;
|
|
12747
|
+
const maxRadius = compiledNodes.length > 0 ? Math.max(...compiledNodes.map((n) => n.radius)) : DEFAULT_COLLISION_PADDING;
|
|
12748
|
+
const simulationConfig = {
|
|
12749
|
+
chargeStrength: graphSpec.layout.chargeStrength ?? -300,
|
|
12750
|
+
linkDistance: graphSpec.layout.linkDistance ?? 30,
|
|
12751
|
+
clustering: clusteringField ? { field: clusteringField, strength: 0.5 } : null,
|
|
12752
|
+
alphaDecay: 0.0228,
|
|
12753
|
+
velocityDecay: 0.4,
|
|
12754
|
+
collisionRadius: maxRadius + collisionPadding,
|
|
12755
|
+
collisionPadding,
|
|
12756
|
+
linkStrength: graphSpec.layout.linkStrength,
|
|
12757
|
+
centerForce: graphSpec.layout.centerForce
|
|
12758
|
+
};
|
|
12759
|
+
const chrome = computeChrome3(
|
|
12760
|
+
{
|
|
12761
|
+
title: graphSpec.chrome.title,
|
|
12762
|
+
subtitle: graphSpec.chrome.subtitle,
|
|
12763
|
+
source: graphSpec.chrome.source,
|
|
12764
|
+
byline: graphSpec.chrome.byline,
|
|
12765
|
+
footer: graphSpec.chrome.footer
|
|
12766
|
+
},
|
|
12767
|
+
theme,
|
|
12768
|
+
options.width,
|
|
12769
|
+
options.measureText,
|
|
12770
|
+
"full",
|
|
12771
|
+
void 0,
|
|
12772
|
+
watermark
|
|
12773
|
+
);
|
|
12774
|
+
return {
|
|
12775
|
+
nodes: compiledNodes,
|
|
12776
|
+
edges: compiledEdges,
|
|
12777
|
+
legend,
|
|
12778
|
+
chrome,
|
|
12779
|
+
tooltipDescriptors,
|
|
12780
|
+
a11y,
|
|
12781
|
+
theme,
|
|
12782
|
+
dimensions: {
|
|
12783
|
+
width: options.width,
|
|
12784
|
+
height: options.height
|
|
12785
|
+
},
|
|
12786
|
+
simulationConfig,
|
|
12787
|
+
watermark
|
|
12788
|
+
};
|
|
12761
12789
|
}
|
|
12790
|
+
var DEFAULT_COLLISION_PADDING = 5;
|
|
12762
12791
|
|
|
12763
12792
|
// src/layout/dimensions.ts
|
|
12764
12793
|
import {
|
|
@@ -12939,12 +12968,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
12939
12968
|
const base = theme.spacing.xAxisHeight;
|
|
12940
12969
|
xAxisHeight = hasXAxisLabel ? base + 22 : base;
|
|
12941
12970
|
}
|
|
12942
|
-
const
|
|
12943
|
-
const yTickPositionExplicitPre = yAxisCfgPre?.tickPosition;
|
|
12944
|
-
const yIsContinuousPre = encoding.y?.type === "quantitative" || encoding.y?.type === "temporal";
|
|
12945
|
-
const yIsLineOrAreaPre = spec.markType === "line" || spec.markType === "area";
|
|
12946
|
-
const yAxisOrientPre = yAxisCfgPre?.orient;
|
|
12947
|
-
const yIsInlinePre = (yTickPositionExplicitPre ?? (yIsLineOrAreaPre && yIsContinuousPre && yAxisOrientPre !== "right" ? "inline" : "gutter")) === "inline";
|
|
12971
|
+
const yIsInlinePre = yTickPositionIsInline(encoding.y, spec.markType);
|
|
12948
12972
|
const inlineTickOverhang = yIsInlinePre ? theme.fonts.sizes.axisTick + INLINE_TICK_OVERHANG_PAD : 0;
|
|
12949
12973
|
const topAxisGap = isRadial && chrome.topHeight === 0 ? 0 : axisMargin + inlineTickOverhang;
|
|
12950
12974
|
const topPad = width < NARROW_VIEWPORT_MAX3 ? padding + TOP_PAD_EXTRA_NARROW2 : padding;
|
|
@@ -13097,7 +13121,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
13097
13121
|
const yFieldForTitle = encoding.y?.field;
|
|
13098
13122
|
const yAxisFormatForTitle = yAxis?.format;
|
|
13099
13123
|
let estTickLabelWidth = 0;
|
|
13100
|
-
if (yFieldForTitle && (encoding.y?.type === "quantitative" || encoding.y?.type === "temporal")) {
|
|
13124
|
+
if (yFieldForTitle && !yIsInlinePre && (encoding.y?.type === "quantitative" || encoding.y?.type === "temporal")) {
|
|
13101
13125
|
let maxAbsValForTitle = 0;
|
|
13102
13126
|
for (const row of spec.data) {
|
|
13103
13127
|
const v = Number(row[yFieldForTitle]);
|
|
@@ -13127,7 +13151,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
13127
13151
|
);
|
|
13128
13152
|
}
|
|
13129
13153
|
const titleFontSize = theme.fonts.sizes.body;
|
|
13130
|
-
const offset = axisTitleOffset3(estTickLabelWidth, titleFontSize, width);
|
|
13154
|
+
const offset = axisTitleOffset3(estTickLabelWidth, titleFontSize, width, yIsInlinePre);
|
|
13131
13155
|
const halfGlyph = Math.ceil(titleFontSize / 2);
|
|
13132
13156
|
const rotatedLabelMargin = offset + halfGlyph + (width < BREAKPOINT_COMPACT_MAX3 ? 0 : AXIS_TITLE_TRAILING_PAD3);
|
|
13133
13157
|
margins.left = Math.max(margins.left, hPad + rotatedLabelMargin);
|
|
@@ -14941,6 +14965,9 @@ import {
|
|
|
14941
14965
|
computeChrome as computeChrome7,
|
|
14942
14966
|
estimateTextWidth as estimateTextWidth20,
|
|
14943
14967
|
formatNumber as formatNumber8,
|
|
14968
|
+
getBreakpoint,
|
|
14969
|
+
HPAD_COMPACT_FRACTION as HPAD_COMPACT_FRACTION3,
|
|
14970
|
+
HPAD_COMPACT_MIN as HPAD_COMPACT_MIN3,
|
|
14944
14971
|
pickLabelColor as pickLabelColor2,
|
|
14945
14972
|
resolveTheme as resolveTheme5,
|
|
14946
14973
|
SEQUENTIAL_PALETTES
|
|
@@ -14950,6 +14977,9 @@ var TILE_STROKE_WIDTH = 1;
|
|
|
14950
14977
|
var LEGEND_SWATCH_SIZE = 10;
|
|
14951
14978
|
var LEGEND_SWATCH_GAP = 6;
|
|
14952
14979
|
var LEGEND_ENTRY_GAP = 16;
|
|
14980
|
+
function clamp(value2, min4, max4) {
|
|
14981
|
+
return Math.min(max4, Math.max(min4, value2));
|
|
14982
|
+
}
|
|
14953
14983
|
function compileTileMap(spec, options) {
|
|
14954
14984
|
const { spec: normalized } = compile(spec);
|
|
14955
14985
|
if (!("type" in normalized) || normalized.type !== "tilemap") {
|
|
@@ -14982,7 +15012,7 @@ function compileTileMap(spec, options) {
|
|
|
14982
15012
|
void 0,
|
|
14983
15013
|
watermark
|
|
14984
15014
|
);
|
|
14985
|
-
const padding = theme.spacing.padding;
|
|
15015
|
+
const padding = getBreakpoint(options.width) === "compact" ? Math.max(HPAD_COMPACT_MIN3, Math.round(theme.spacing.padding * HPAD_COMPACT_FRACTION3)) : theme.spacing.padding;
|
|
14986
15016
|
const fullArea = {
|
|
14987
15017
|
x: padding,
|
|
14988
15018
|
y: padding + chrome.topHeight,
|
|
@@ -15095,7 +15125,7 @@ function compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDa
|
|
|
15095
15125
|
keyboardNavigable: tiles.length > 0
|
|
15096
15126
|
};
|
|
15097
15127
|
const resolvedAnimation = resolveAnimation(tilemapSpec.animation);
|
|
15098
|
-
const padding =
|
|
15128
|
+
const padding = fullArea.x;
|
|
15099
15129
|
const contentHeight = tileGridOffsetY + tilePositions.gridHeight + legendGap2 + legendTotalHeight + chrome.bottomHeight + padding;
|
|
15100
15130
|
return {
|
|
15101
15131
|
area: fullArea,
|
|
@@ -15150,16 +15180,20 @@ function compileCategorical(tilemapSpec, options, theme, chrome, fullArea, isDar
|
|
|
15150
15180
|
}
|
|
15151
15181
|
}
|
|
15152
15182
|
const showLegend = tilemapSpec.legend?.show !== false;
|
|
15183
|
+
const legendLabelFontSize = 11;
|
|
15153
15184
|
const legendRowHeight = LEGEND_SWATCH_SIZE + 6;
|
|
15154
|
-
const
|
|
15185
|
+
const legendLabels = categories.map(formatCategoryLabel);
|
|
15155
15186
|
const legendGap2 = showLegend ? 8 : 0;
|
|
15156
|
-
const
|
|
15187
|
+
const estimatedRows = showLegend ? countLegendRows(legendLabels, fullArea.width, legendLabelFontSize) : 0;
|
|
15188
|
+
const tileAreaHeight = fullArea.height - legendRowHeight * estimatedRows - legendGap2;
|
|
15157
15189
|
const tilePositions = computeTilePositions(fullArea.width, tileAreaHeight, 5);
|
|
15158
15190
|
const tileGridOffsetX = fullArea.x + (fullArea.width - tilePositions.gridWidth) / 2;
|
|
15159
15191
|
const tileGridOffsetY = fullArea.y;
|
|
15160
15192
|
const legendX = tileGridOffsetX;
|
|
15161
15193
|
const legendY = tileGridOffsetY + tilePositions.gridHeight + legendGap2;
|
|
15162
15194
|
const legendWidth = tilePositions.gridWidth;
|
|
15195
|
+
const legendRows = showLegend ? countLegendRows(legendLabels, legendWidth, legendLabelFontSize) : 0;
|
|
15196
|
+
const legendTotalHeight = showLegend ? legendRowHeight * legendRows : 0;
|
|
15163
15197
|
const neutralFill = isDarkMode ? "#1e2a30" : "#e0e0e0";
|
|
15164
15198
|
const neutralStroke = isDarkMode ? "rgba(255,255,255,0.08)" : "#d0d0d0";
|
|
15165
15199
|
const tiles = [];
|
|
@@ -15195,7 +15229,7 @@ function compileCategorical(tilemapSpec, options, theme, chrome, fullArea, isDar
|
|
|
15195
15229
|
if (showLegend) {
|
|
15196
15230
|
const labelStyle = {
|
|
15197
15231
|
fontFamily: theme.fonts.family,
|
|
15198
|
-
fontSize:
|
|
15232
|
+
fontSize: legendLabelFontSize,
|
|
15199
15233
|
fontWeight: 400,
|
|
15200
15234
|
fill: theme.colors.text,
|
|
15201
15235
|
lineHeight: 1.2
|
|
@@ -15208,7 +15242,7 @@ function compileCategorical(tilemapSpec, options, theme, chrome, fullArea, isDar
|
|
|
15208
15242
|
categoricalLegend = {
|
|
15209
15243
|
type: "categorical",
|
|
15210
15244
|
position: "bottom",
|
|
15211
|
-
bounds: { x: legendX, y: legendY, width: legendWidth, height:
|
|
15245
|
+
bounds: { x: legendX, y: legendY, width: legendWidth, height: legendTotalHeight },
|
|
15212
15246
|
labelStyle,
|
|
15213
15247
|
entries,
|
|
15214
15248
|
swatchSize: LEGEND_SWATCH_SIZE,
|
|
@@ -15226,7 +15260,7 @@ function compileCategorical(tilemapSpec, options, theme, chrome, fullArea, isDar
|
|
|
15226
15260
|
keyboardNavigable: tiles.length > 0
|
|
15227
15261
|
};
|
|
15228
15262
|
const resolvedAnimation = resolveAnimation(tilemapSpec.animation);
|
|
15229
|
-
const padding =
|
|
15263
|
+
const padding = fullArea.x;
|
|
15230
15264
|
const contentHeight = tileGridOffsetY + tilePositions.gridHeight + legendGap2 + legendTotalHeight + chrome.bottomHeight + padding;
|
|
15231
15265
|
return {
|
|
15232
15266
|
area: fullArea,
|
|
@@ -15265,27 +15299,32 @@ function buildTileMark(opts) {
|
|
|
15265
15299
|
const tileTopY = gridOffsetY + pos.y;
|
|
15266
15300
|
const textColor = hasData ? pickLabelColor2(fill, isDarkMode) : "#ffffff";
|
|
15267
15301
|
const isLightText = textColor === "#ffffff";
|
|
15302
|
+
const showValue = tileSize >= 24;
|
|
15303
|
+
const codeFontSize = clamp(Math.round(tileSize * 0.5 * 10) / 10, 6.5, 11);
|
|
15304
|
+
const valueFontSize = clamp(Math.round(tileSize * 0.42 * 10) / 10, 6.5, 10);
|
|
15268
15305
|
const labelStyle = {
|
|
15269
15306
|
fontFamily: theme.fonts.family,
|
|
15270
|
-
fontSize:
|
|
15307
|
+
fontSize: codeFontSize,
|
|
15271
15308
|
fontWeight: 700,
|
|
15272
15309
|
fill: textColor,
|
|
15273
15310
|
lineHeight: 1.2
|
|
15274
15311
|
};
|
|
15275
15312
|
const valueLabelStyle = {
|
|
15276
15313
|
fontFamily: theme.fonts.family,
|
|
15277
|
-
fontSize:
|
|
15314
|
+
fontSize: valueFontSize,
|
|
15278
15315
|
fontWeight: 300,
|
|
15279
15316
|
fill: isLightText ? "rgba(255,255,255,0.6)" : "rgba(0,0,0,0.45)",
|
|
15280
15317
|
lineHeight: 1.2
|
|
15281
15318
|
};
|
|
15282
|
-
const
|
|
15319
|
+
const codeCenterY = showValue ? tileTopY + tileSize * 0.28 : tileTopY + tileSize * 0.5;
|
|
15320
|
+
const valueLabel = showValue ? {
|
|
15283
15321
|
text: formattedValue,
|
|
15284
15322
|
x: tileCenterX,
|
|
15285
15323
|
y: tileTopY + tileSize * 0.78,
|
|
15286
15324
|
style: valueLabelStyle,
|
|
15287
15325
|
visible: true
|
|
15288
|
-
};
|
|
15326
|
+
} : { text: "", x: 0, y: 0, style: valueLabelStyle, visible: false };
|
|
15327
|
+
const cornerRadius = Math.min(TILE_CORNER_RADIUS, Math.round(tileSize * 0.22));
|
|
15289
15328
|
const data = {
|
|
15290
15329
|
state: stateCode,
|
|
15291
15330
|
value: value2,
|
|
@@ -15304,14 +15343,14 @@ function buildTileMark(opts) {
|
|
|
15304
15343
|
fillOpacity,
|
|
15305
15344
|
stroke,
|
|
15306
15345
|
strokeWidth: TILE_STROKE_WIDTH,
|
|
15307
|
-
cornerRadius
|
|
15346
|
+
cornerRadius,
|
|
15308
15347
|
value: value2,
|
|
15309
15348
|
formattedValue,
|
|
15310
15349
|
hasData,
|
|
15311
15350
|
label: {
|
|
15312
15351
|
text: stateCode,
|
|
15313
15352
|
x: tileCenterX,
|
|
15314
|
-
y:
|
|
15353
|
+
y: codeCenterY,
|
|
15315
15354
|
style: labelStyle,
|
|
15316
15355
|
visible: true
|
|
15317
15356
|
},
|
|
@@ -15350,6 +15389,22 @@ function buildTooltips(tiles, fieldLabel = "Value") {
|
|
|
15350
15389
|
function formatCategoryLabel(category) {
|
|
15351
15390
|
return category.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
15352
15391
|
}
|
|
15392
|
+
function countLegendRows(labels, availableWidth, fontSize) {
|
|
15393
|
+
if (labels.length === 0) return 0;
|
|
15394
|
+
let rows = 1;
|
|
15395
|
+
let offsetX = 0;
|
|
15396
|
+
for (let i = 0; i < labels.length; i++) {
|
|
15397
|
+
const labelWidth = estimateTextWidth20(labels[i], fontSize);
|
|
15398
|
+
const entryWidth = LEGEND_SWATCH_SIZE + LEGEND_SWATCH_GAP + labelWidth + LEGEND_ENTRY_GAP;
|
|
15399
|
+
if (i > 0 && offsetX + entryWidth > availableWidth) {
|
|
15400
|
+
rows++;
|
|
15401
|
+
offsetX = entryWidth;
|
|
15402
|
+
} else {
|
|
15403
|
+
offsetX += entryWidth;
|
|
15404
|
+
}
|
|
15405
|
+
}
|
|
15406
|
+
return rows;
|
|
15407
|
+
}
|
|
15353
15408
|
function emptyLayout4(chrome, theme, options, watermark) {
|
|
15354
15409
|
return {
|
|
15355
15410
|
area: { x: 0, y: 0, width: 0, height: 0 },
|
|
@@ -16165,7 +16220,7 @@ function compileChart(spec, options) {
|
|
|
16165
16220
|
if (rawTransforms && rawTransforms.length > 0) {
|
|
16166
16221
|
chartSpec = { ...chartSpec, data: runTransforms(chartSpec.data, rawTransforms) };
|
|
16167
16222
|
}
|
|
16168
|
-
const breakpoint =
|
|
16223
|
+
const breakpoint = getBreakpoint2(options.width);
|
|
16169
16224
|
const heightClass = getHeightClass(options.height);
|
|
16170
16225
|
let strategy = getLayoutStrategy(breakpoint, heightClass);
|
|
16171
16226
|
const rawSpec = expandedSpec;
|