@opendata-ai/openchart-engine 7.6.1 → 7.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +13 -0
- package/dist/index.js +1037 -187
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__test-fixtures__/specs.ts +6 -0
- package/src/__tests__/__snapshots__/compile-snapshot.test.ts.snap +16 -0
- package/src/__tests__/axes.test.ts +3 -2
- package/src/__tests__/facet.test.ts +215 -0
- package/src/annotations/__tests__/compute.test.ts +18 -0
- package/src/annotations/__tests__/thinning.test.ts +137 -0
- package/src/annotations/compute.ts +3 -1
- package/src/annotations/thinning.ts +116 -0
- package/src/barlist/compile-barlist.ts +1 -0
- package/src/charts/bar/labels.ts +1 -0
- package/src/charts/column/labels.ts +1 -0
- package/src/charts/dot/labels.ts +1 -0
- package/src/charts/line/area.ts +16 -2
- package/src/charts/line/compute.ts +17 -10
- package/src/charts/pie/labels.ts +1 -0
- package/src/compile/__tests__/color-scale-range.test.ts +80 -1
- package/src/compile/color-scale-range.ts +44 -2
- package/src/compile.ts +314 -5
- package/src/compiler/__tests__/normalize-highlight.test.ts +103 -0
- package/src/compiler/__tests__/validate-highlight.test.ts +112 -0
- package/src/compiler/normalize.ts +67 -2
- package/src/compiler/types.ts +10 -0
- package/src/compiler/validate.ts +79 -0
- package/src/endpoint-labels/__tests__/compute.test.ts +57 -0
- package/src/endpoint-labels/compute.ts +179 -5
- package/src/endpoint-labels/predict.ts +17 -0
- package/src/layout/__tests__/scales-highlight.test.ts +192 -0
- package/src/layout/axes.ts +8 -2
- package/src/layout/dimensions.ts +6 -1
- package/src/layout/facet.ts +80 -0
- package/src/layout/plan.ts +16 -0
- package/src/legend/__tests__/redundancy.test.ts +108 -0
- package/src/legend/__tests__/suppression.test.ts +72 -0
- package/src/legend/compute.ts +18 -0
- package/src/legend/suppression.ts +27 -6
- package/src/tilemap/__tests__/compile-tilemap.test.ts +210 -0
- package/src/tilemap/compile-tilemap.ts +379 -117
- package/src/tilemap/types.ts +1 -0
package/dist/index.js
CHANGED
|
@@ -1158,7 +1158,7 @@ function computeAnnotations(spec, scalesOrCtx, chartArea, strategy, isDark, obst
|
|
|
1158
1158
|
measure: heuristicMeasure
|
|
1159
1159
|
};
|
|
1160
1160
|
}
|
|
1161
|
-
const isCompact = ctx.strategy.annotationPosition === "tooltip-only";
|
|
1161
|
+
const isCompact = ctx.strategy.annotationPosition === "tooltip-only" && !ctx.autoThin;
|
|
1162
1162
|
const svgRect = { x: 0, y: 0, width: ctx.svg.width, height: ctx.svg.height };
|
|
1163
1163
|
const annotations = [];
|
|
1164
1164
|
const autoQueue = [];
|
|
@@ -1347,6 +1347,65 @@ function computeAnnotations(spec, scalesOrCtx, chartArea, strategy, isDark, obst
|
|
|
1347
1347
|
return annotations;
|
|
1348
1348
|
}
|
|
1349
1349
|
|
|
1350
|
+
// src/annotations/thinning.ts
|
|
1351
|
+
import { detectCollision as detectCollision2 } from "@opendata-ai/openchart-core";
|
|
1352
|
+
function thinAnnotations(annotations, specs, measure) {
|
|
1353
|
+
if (annotations.length <= 1) {
|
|
1354
|
+
return { annotations, footnotes: [] };
|
|
1355
|
+
}
|
|
1356
|
+
const entries = annotations.map((a, i) => ({
|
|
1357
|
+
resolved: a,
|
|
1358
|
+
spec: i < specs.length && specs[i].type === "text" ? specs[i] : void 0,
|
|
1359
|
+
index: i
|
|
1360
|
+
}));
|
|
1361
|
+
const pinned = [];
|
|
1362
|
+
const candidates = [];
|
|
1363
|
+
for (const entry of entries) {
|
|
1364
|
+
if (entry.resolved.type !== "text" || !entry.resolved.label) {
|
|
1365
|
+
pinned.push(entry);
|
|
1366
|
+
} else if (entry.spec?.responsive === false) {
|
|
1367
|
+
pinned.push(entry);
|
|
1368
|
+
} else {
|
|
1369
|
+
candidates.push(entry);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
candidates.sort((a, b) => {
|
|
1373
|
+
const pa = a.spec?.priority ?? Number.MAX_SAFE_INTEGER;
|
|
1374
|
+
const pb = b.spec?.priority ?? Number.MAX_SAFE_INTEGER;
|
|
1375
|
+
if (pa !== pb) return pa - pb;
|
|
1376
|
+
return a.index - b.index;
|
|
1377
|
+
});
|
|
1378
|
+
const placedBounds = [];
|
|
1379
|
+
for (const entry of pinned) {
|
|
1380
|
+
if (entry.resolved.type === "text" && entry.resolved.label) {
|
|
1381
|
+
const bounds = entry.resolved.bounds ?? estimateLabelBounds(entry.resolved.label, measure);
|
|
1382
|
+
placedBounds.push(bounds);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
const footnotes = [];
|
|
1386
|
+
for (const entry of candidates) {
|
|
1387
|
+
const bounds = entry.resolved.bounds ?? estimateLabelBounds(entry.resolved.label, measure);
|
|
1388
|
+
if (bounds.width <= 0 || bounds.height <= 0) {
|
|
1389
|
+
placedBounds.push(bounds);
|
|
1390
|
+
continue;
|
|
1391
|
+
}
|
|
1392
|
+
const overlaps = placedBounds.some(
|
|
1393
|
+
(pb) => pb.width > 0 && pb.height > 0 && detectCollision2(bounds, pb)
|
|
1394
|
+
);
|
|
1395
|
+
if (overlaps) {
|
|
1396
|
+
const footnoteIndex = footnotes.length + 1;
|
|
1397
|
+
footnotes.push({
|
|
1398
|
+
index: footnoteIndex,
|
|
1399
|
+
text: entry.resolved.label.text
|
|
1400
|
+
});
|
|
1401
|
+
entry.resolved.footnoteIndex = footnoteIndex;
|
|
1402
|
+
} else {
|
|
1403
|
+
placedBounds.push(bounds);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
return { annotations, footnotes };
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1350
1409
|
// src/endpoint-labels/compute.ts
|
|
1351
1410
|
import { estimateTextWidth as estimateTextWidth2, wrapText } from "@opendata-ai/openchart-core";
|
|
1352
1411
|
|
|
@@ -1359,13 +1418,14 @@ function legendShownExplicitly(spec) {
|
|
|
1359
1418
|
function legendHiddenExplicitly(spec) {
|
|
1360
1419
|
return spec.legend?.show === false;
|
|
1361
1420
|
}
|
|
1421
|
+
var EP_CONFIG_ONLY_KEYS = /* @__PURE__ */ new Set(["show", "maxSeries"]);
|
|
1362
1422
|
function endpointLabelsExplicitlyOn(spec) {
|
|
1363
1423
|
const ep = spec.endpointLabels;
|
|
1364
1424
|
if (ep === true) return true;
|
|
1365
1425
|
if (ep === false || ep == null) return false;
|
|
1366
1426
|
if (ep.show === true) return true;
|
|
1367
1427
|
if (ep.show === false) return false;
|
|
1368
|
-
return Object.keys(ep).some((k) => k
|
|
1428
|
+
return Object.keys(ep).some((k) => !EP_CONFIG_ONLY_KEYS.has(k));
|
|
1369
1429
|
}
|
|
1370
1430
|
function endpointLabelsExplicitlyOff(spec) {
|
|
1371
1431
|
const ep = spec.endpointLabels;
|
|
@@ -1373,6 +1433,12 @@ function endpointLabelsExplicitlyOff(spec) {
|
|
|
1373
1433
|
if (typeof ep === "object" && ep != null && ep.show === false) return true;
|
|
1374
1434
|
return false;
|
|
1375
1435
|
}
|
|
1436
|
+
var DEFAULT_MAX_SERIES = 8;
|
|
1437
|
+
function getMaxSeries(spec) {
|
|
1438
|
+
const ep = spec.endpointLabels;
|
|
1439
|
+
if (typeof ep === "object" && ep != null && ep.maxSeries != null) return ep.maxSeries;
|
|
1440
|
+
return DEFAULT_MAX_SERIES;
|
|
1441
|
+
}
|
|
1376
1442
|
function resolveSuppression(spec, ctx) {
|
|
1377
1443
|
const isLineOrArea2 = spec.markType === "line" || spec.markType === "area";
|
|
1378
1444
|
const hasColorEncoding = spec.encoding.color != null;
|
|
@@ -1398,7 +1464,9 @@ function resolveSuppression(spec, ctx) {
|
|
|
1398
1464
|
showEndOfLineLabels: false
|
|
1399
1465
|
};
|
|
1400
1466
|
}
|
|
1401
|
-
const
|
|
1467
|
+
const exceedsCutoff = !endpointLabelsExplicitlyOn(spec) && ctx.seriesCount > getMaxSeries(spec);
|
|
1468
|
+
const demoted = !endpointLabelsExplicitlyOn(spec) && !!ctx.endpointLabelsDemoted;
|
|
1469
|
+
const epExplicitOff = endpointLabelsExplicitlyOff(spec) || exceedsCutoff || demoted;
|
|
1402
1470
|
const legShown = legendShownExplicitly(spec);
|
|
1403
1471
|
const legHidden = legendHiddenExplicitly(spec);
|
|
1404
1472
|
const showEndpointLabels = !epExplicitOff;
|
|
@@ -1717,7 +1785,8 @@ function emptyLayout(theme) {
|
|
|
1717
1785
|
fontSize: ENDPOINT_VALUE_FONT_SIZE,
|
|
1718
1786
|
fontWeight: ENDPOINT_VALUE_FONT_WEIGHT,
|
|
1719
1787
|
fill: theme.colors.annotationText ?? theme.colors.text,
|
|
1720
|
-
lineHeight: ENDPOINT_LINE_HEIGHT
|
|
1788
|
+
lineHeight: ENDPOINT_LINE_HEIGHT,
|
|
1789
|
+
fontVariant: "tabular-nums"
|
|
1721
1790
|
};
|
|
1722
1791
|
return {
|
|
1723
1792
|
entries: [],
|
|
@@ -1748,6 +1817,21 @@ function lastDataPoint(mark) {
|
|
|
1748
1817
|
}
|
|
1749
1818
|
return null;
|
|
1750
1819
|
}
|
|
1820
|
+
function firstDataPoint(mark) {
|
|
1821
|
+
if (mark.dataPoints && mark.dataPoints.length > 0) {
|
|
1822
|
+
const first = mark.dataPoints[0];
|
|
1823
|
+
return { x: first.x, y: first.y };
|
|
1824
|
+
}
|
|
1825
|
+
if (mark.type === "line" && mark.points.length > 0) {
|
|
1826
|
+
const first = mark.points[0];
|
|
1827
|
+
return { x: first.x, y: first.y };
|
|
1828
|
+
}
|
|
1829
|
+
if (mark.type === "area" && mark.topPoints.length > 0) {
|
|
1830
|
+
const first = mark.topPoints[0];
|
|
1831
|
+
return { x: first.x, y: first.y };
|
|
1832
|
+
}
|
|
1833
|
+
return null;
|
|
1834
|
+
}
|
|
1751
1835
|
function readValue(mark, valueField) {
|
|
1752
1836
|
if (!valueField) return null;
|
|
1753
1837
|
const dp = mark.dataPoints;
|
|
@@ -1762,6 +1846,20 @@ function readValue(mark, valueField) {
|
|
|
1762
1846
|
}
|
|
1763
1847
|
return null;
|
|
1764
1848
|
}
|
|
1849
|
+
function readFirstValue(mark, valueField) {
|
|
1850
|
+
if (!valueField) return null;
|
|
1851
|
+
const dp = mark.dataPoints;
|
|
1852
|
+
if (dp && dp.length > 0) {
|
|
1853
|
+
const datum = dp[0].datum;
|
|
1854
|
+
const v = datum[valueField];
|
|
1855
|
+
return typeof v === "number" || typeof v === "string" ? v : null;
|
|
1856
|
+
}
|
|
1857
|
+
if (mark.data.length > 0) {
|
|
1858
|
+
const v = mark.data[0][valueField];
|
|
1859
|
+
return typeof v === "number" || typeof v === "string" ? v : null;
|
|
1860
|
+
}
|
|
1861
|
+
return null;
|
|
1862
|
+
}
|
|
1765
1863
|
function bidirectionalSweep(entries, areaTop, areaBottom) {
|
|
1766
1864
|
const n = entries.length;
|
|
1767
1865
|
if (n === 0) return [];
|
|
@@ -1788,16 +1886,20 @@ function bidirectionalSweep(entries, areaTop, areaBottom) {
|
|
|
1788
1886
|
}
|
|
1789
1887
|
return result;
|
|
1790
1888
|
}
|
|
1791
|
-
function computeEndpointLabels(spec, marks, theme, chartArea, strategy) {
|
|
1889
|
+
function computeEndpointLabels(spec, marks, theme, chartArea, strategy, endpointLabelsDemoted) {
|
|
1792
1890
|
if (strategy?.labelMode === "none") return emptyLayout(theme);
|
|
1891
|
+
const highlight = spec.highlight ?? [];
|
|
1892
|
+
const highlightActive = highlight.length > 0;
|
|
1793
1893
|
const seriesCount = countColorSeries(spec);
|
|
1794
1894
|
const sup = resolveSuppression(spec, {
|
|
1795
1895
|
seriesCount,
|
|
1796
1896
|
// The 'none' branch above returned; by here labelMode is not 'none'.
|
|
1797
1897
|
labelsHiddenByStrategy: false,
|
|
1798
|
-
labelsDensityNone: spec.labels.density === "none"
|
|
1898
|
+
labelsDensityNone: spec.labels.density === "none",
|
|
1899
|
+
endpointLabelsDemoted
|
|
1799
1900
|
});
|
|
1800
|
-
|
|
1901
|
+
const epExplicitlyOff = spec.endpointLabels === false || typeof spec.endpointLabels === "object" && spec.endpointLabels?.show === false;
|
|
1902
|
+
if (!sup.showEndpointLabels && !(highlightActive && !epExplicitlyOff)) return emptyLayout(theme);
|
|
1801
1903
|
const bySeriesKey = /* @__PURE__ */ new Map();
|
|
1802
1904
|
for (const mark of marks) {
|
|
1803
1905
|
if (!isLineOrArea(mark) || !mark.seriesKey) continue;
|
|
@@ -1828,7 +1930,8 @@ function computeEndpointLabels(spec, marks, theme, chartArea, strategy) {
|
|
|
1828
1930
|
fontSize: ENDPOINT_VALUE_FONT_SIZE,
|
|
1829
1931
|
fontWeight: ENDPOINT_VALUE_FONT_WEIGHT,
|
|
1830
1932
|
fill: theme.colors.annotationText ?? theme.colors.text,
|
|
1831
|
-
lineHeight: ENDPOINT_LINE_HEIGHT
|
|
1933
|
+
lineHeight: ENDPOINT_LINE_HEIGHT,
|
|
1934
|
+
fontVariant: "tabular-nums"
|
|
1832
1935
|
};
|
|
1833
1936
|
const provisional = [];
|
|
1834
1937
|
const labelLineHeight = ENDPOINT_LABEL_FONT_SIZE * ENDPOINT_LINE_HEIGHT;
|
|
@@ -1836,6 +1939,7 @@ function computeEndpointLabels(spec, marks, theme, chartArea, strategy) {
|
|
|
1836
1939
|
for (const mark of lineOrAreaMarks) {
|
|
1837
1940
|
const seriesKey = mark.seriesKey;
|
|
1838
1941
|
if (!seriesKey) continue;
|
|
1942
|
+
if (highlightActive && !highlight.includes(seriesKey)) continue;
|
|
1839
1943
|
const last = lastDataPoint(mark);
|
|
1840
1944
|
if (!last) continue;
|
|
1841
1945
|
const labelLines = wrapText(
|
|
@@ -1868,7 +1972,8 @@ function computeEndpointLabels(spec, marks, theme, chartArea, strategy) {
|
|
|
1868
1972
|
width: entryWidth
|
|
1869
1973
|
});
|
|
1870
1974
|
}
|
|
1871
|
-
if (provisional.length
|
|
1975
|
+
if (provisional.length === 0) return emptyLayout(theme);
|
|
1976
|
+
if (provisional.length < 2 && !highlightActive) return emptyLayout(theme);
|
|
1872
1977
|
const sweepInput = provisional.map((p, i) => ({
|
|
1873
1978
|
naturalTop: p.dataY - ENDPOINT_LABEL_FONT_SIZE / 2,
|
|
1874
1979
|
height: p.height + ENDPOINT_ENTRY_GAP,
|
|
@@ -1907,6 +2012,97 @@ function computeEndpointLabels(spec, marks, theme, chartArea, strategy) {
|
|
|
1907
2012
|
}
|
|
1908
2013
|
return entry;
|
|
1909
2014
|
});
|
|
2015
|
+
const endsBoth = config?.ends === "both";
|
|
2016
|
+
let leading;
|
|
2017
|
+
let leadingBounds;
|
|
2018
|
+
if (endsBoth) {
|
|
2019
|
+
const leadingProvisional = [];
|
|
2020
|
+
for (const mark of lineOrAreaMarks) {
|
|
2021
|
+
const seriesKey = mark.seriesKey;
|
|
2022
|
+
if (!seriesKey) continue;
|
|
2023
|
+
if (highlightActive && !highlight.includes(seriesKey)) continue;
|
|
2024
|
+
const first = firstDataPoint(mark);
|
|
2025
|
+
if (!first) continue;
|
|
2026
|
+
const labelLines = wrapText(
|
|
2027
|
+
seriesKey,
|
|
2028
|
+
ENDPOINT_LABEL_FONT_SIZE,
|
|
2029
|
+
ENDPOINT_LABEL_FONT_WEIGHT,
|
|
2030
|
+
wrapWidth
|
|
2031
|
+
);
|
|
2032
|
+
const rawValue = readFirstValue(mark, valueField);
|
|
2033
|
+
const value2 = formatEndpointValue(rawValue, formatString);
|
|
2034
|
+
let entryWidth = 0;
|
|
2035
|
+
for (const line of labelLines) {
|
|
2036
|
+
const w = estimateTextWidth2(line, ENDPOINT_LABEL_FONT_SIZE, ENDPOINT_LABEL_FONT_WEIGHT);
|
|
2037
|
+
if (w > entryWidth) entryWidth = w;
|
|
2038
|
+
}
|
|
2039
|
+
const valueWidth = value2 ? estimateTextWidth2(value2, ENDPOINT_VALUE_FONT_SIZE, ENDPOINT_VALUE_FONT_WEIGHT) : 0;
|
|
2040
|
+
if (valueWidth > entryWidth) entryWidth = valueWidth;
|
|
2041
|
+
const labelHeight = labelLines.length * labelLineHeight;
|
|
2042
|
+
const valueHeight = value2 ? valueLineHeight : 0;
|
|
2043
|
+
const entryHeight = labelHeight + (value2 ? ENDPOINT_VALUE_GAP : 0) + valueHeight;
|
|
2044
|
+
const color2 = mark.type === "line" ? mark.stroke : mark.stroke ?? (typeof mark.fill === "string" ? mark.fill : theme.colors.categorical[0]);
|
|
2045
|
+
leadingProvisional.push({
|
|
2046
|
+
seriesKey,
|
|
2047
|
+
labelLines,
|
|
2048
|
+
value: value2,
|
|
2049
|
+
color: color2,
|
|
2050
|
+
dataX: first.x,
|
|
2051
|
+
dataY: first.y,
|
|
2052
|
+
height: entryHeight,
|
|
2053
|
+
width: entryWidth
|
|
2054
|
+
});
|
|
2055
|
+
}
|
|
2056
|
+
if (leadingProvisional.length >= 2 || leadingProvisional.length === 1 && highlightActive) {
|
|
2057
|
+
const leadingSweepInput = leadingProvisional.map((p, i) => ({
|
|
2058
|
+
naturalTop: p.dataY - ENDPOINT_LABEL_FONT_SIZE / 2,
|
|
2059
|
+
height: p.height + ENDPOINT_ENTRY_GAP,
|
|
2060
|
+
index: i
|
|
2061
|
+
}));
|
|
2062
|
+
const leadingSweptTops = bidirectionalSweep(
|
|
2063
|
+
leadingSweepInput,
|
|
2064
|
+
chartArea.y,
|
|
2065
|
+
chartArea.y + chartArea.height
|
|
2066
|
+
);
|
|
2067
|
+
const leadingColumnWidth = Math.max(
|
|
2068
|
+
ENDPOINT_SWATCH_SIZE + ENDPOINT_GAP + Math.max(...leadingProvisional.map((p) => p.width), 0) + 4,
|
|
2069
|
+
32
|
|
2070
|
+
);
|
|
2071
|
+
const leadingColumnX = chartArea.x - ENDPOINT_COLUMN_GAP - leadingColumnWidth;
|
|
2072
|
+
leading = leadingProvisional.map((p, i) => {
|
|
2073
|
+
const labelY = leadingSweptTops[i];
|
|
2074
|
+
const swatchRowY = labelY + ENDPOINT_LABEL_FONT_SIZE / 2;
|
|
2075
|
+
const displaced = Math.abs(swatchRowY - p.dataY) > ENDPOINT_LEADER_THRESHOLD;
|
|
2076
|
+
const entry = {
|
|
2077
|
+
seriesKey: p.seriesKey,
|
|
2078
|
+
labelLines: p.labelLines,
|
|
2079
|
+
value: p.value,
|
|
2080
|
+
color: p.color,
|
|
2081
|
+
dataY: p.dataY,
|
|
2082
|
+
labelY,
|
|
2083
|
+
showLeader: showLeader && displaced
|
|
2084
|
+
};
|
|
2085
|
+
if (showMarker) {
|
|
2086
|
+
entry.marker = {
|
|
2087
|
+
x: p.dataX - markerRadius,
|
|
2088
|
+
y: p.dataY,
|
|
2089
|
+
dataX: p.dataX,
|
|
2090
|
+
fill: markerFill,
|
|
2091
|
+
stroke: config?.markerStyle?.stroke ?? p.color,
|
|
2092
|
+
strokeWidth: markerStrokeWidth,
|
|
2093
|
+
radius: markerRadius
|
|
2094
|
+
};
|
|
2095
|
+
}
|
|
2096
|
+
return entry;
|
|
2097
|
+
});
|
|
2098
|
+
leadingBounds = {
|
|
2099
|
+
x: leadingColumnX,
|
|
2100
|
+
y: chartArea.y,
|
|
2101
|
+
width: leadingColumnWidth,
|
|
2102
|
+
height: chartArea.height
|
|
2103
|
+
};
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
1910
2106
|
return {
|
|
1911
2107
|
entries,
|
|
1912
2108
|
bounds: {
|
|
@@ -1920,7 +2116,9 @@ function computeEndpointLabels(spec, marks, theme, chartArea, strategy) {
|
|
|
1920
2116
|
swatchSize: ENDPOINT_SWATCH_SIZE,
|
|
1921
2117
|
gap: ENDPOINT_GAP,
|
|
1922
2118
|
valueGap: ENDPOINT_VALUE_GAP,
|
|
1923
|
-
swatchChipFill: theme.colors.annotationFill ?? theme.colors.background
|
|
2119
|
+
swatchChipFill: theme.colors.annotationFill ?? theme.colors.background,
|
|
2120
|
+
leading,
|
|
2121
|
+
leadingBounds
|
|
1924
2122
|
};
|
|
1925
2123
|
}
|
|
1926
2124
|
|
|
@@ -2512,7 +2710,8 @@ function computeBarLabels(marks, _chartArea, density = "auto", labelFormat, labe
|
|
|
2512
2710
|
fill,
|
|
2513
2711
|
lineHeight: 1.2,
|
|
2514
2712
|
textAnchor,
|
|
2515
|
-
dominantBaseline: "central"
|
|
2713
|
+
dominantBaseline: "central",
|
|
2714
|
+
fontVariant: "tabular-nums"
|
|
2516
2715
|
}
|
|
2517
2716
|
});
|
|
2518
2717
|
}
|
|
@@ -2935,7 +3134,8 @@ function computeColumnLabels(marks, _chartArea, density = "auto", labelFormat, l
|
|
|
2935
3134
|
fontWeight: LABEL_FONT_WEIGHT2,
|
|
2936
3135
|
fill: labelColor ?? getRepresentativeColor2(mark.fill),
|
|
2937
3136
|
lineHeight: 1.2,
|
|
2938
|
-
textAnchor: "middle"
|
|
3137
|
+
textAnchor: "middle",
|
|
3138
|
+
fontVariant: "tabular-nums"
|
|
2939
3139
|
}
|
|
2940
3140
|
});
|
|
2941
3141
|
}
|
|
@@ -3174,7 +3374,8 @@ function computeDotLabels(marks, _chartArea, density = "auto", labelPrefix, labe
|
|
|
3174
3374
|
fill: getRepresentativeColor3(mark.fill),
|
|
3175
3375
|
lineHeight: 1.2,
|
|
3176
3376
|
textAnchor: "start",
|
|
3177
|
-
dominantBaseline: "central"
|
|
3377
|
+
dominantBaseline: "central",
|
|
3378
|
+
fontVariant: "tabular-nums"
|
|
3178
3379
|
}
|
|
3179
3380
|
});
|
|
3180
3381
|
}
|
|
@@ -4363,10 +4564,23 @@ function computeStackedArea(spec, scales, chartArea, darkMode) {
|
|
|
4363
4564
|
function computeAreaMarks(spec, scales, chartArea, darkMode) {
|
|
4364
4565
|
const encoding = spec.encoding;
|
|
4365
4566
|
const yChannel = encoding.y;
|
|
4567
|
+
let marks;
|
|
4366
4568
|
if (yChannel && isStacked(yChannel.stack)) {
|
|
4367
|
-
|
|
4569
|
+
marks = computeStackedArea(spec, scales, chartArea, darkMode);
|
|
4570
|
+
} else {
|
|
4571
|
+
marks = computeSingleArea(spec, scales, chartArea);
|
|
4572
|
+
}
|
|
4573
|
+
const highlight = spec.highlight ?? [];
|
|
4574
|
+
if (highlight.length > 0) {
|
|
4575
|
+
const highlightSet = new Set(highlight);
|
|
4576
|
+
marks.sort((a, b) => {
|
|
4577
|
+
const aMuted = a.seriesKey != null && !highlightSet.has(a.seriesKey);
|
|
4578
|
+
const bMuted = b.seriesKey != null && !highlightSet.has(b.seriesKey);
|
|
4579
|
+
if (aMuted === bMuted) return 0;
|
|
4580
|
+
return aMuted ? -1 : 1;
|
|
4581
|
+
});
|
|
4368
4582
|
}
|
|
4369
|
-
return
|
|
4583
|
+
return marks;
|
|
4370
4584
|
}
|
|
4371
4585
|
|
|
4372
4586
|
// src/charts/line/compute.ts
|
|
@@ -4385,7 +4599,10 @@ function computeLineMarks(spec, scales, _chartArea, _strategy) {
|
|
|
4385
4599
|
const colorField = isSequentialColor ? void 0 : colorEnc?.field;
|
|
4386
4600
|
const sequentialColorField = isSequentialColor ? colorEnc.field : void 0;
|
|
4387
4601
|
const groups = groupByField(spec.data, colorField);
|
|
4388
|
-
const
|
|
4602
|
+
const mutedMarks = [];
|
|
4603
|
+
const highlightedMarks = [];
|
|
4604
|
+
const highlight = spec.highlight ?? [];
|
|
4605
|
+
const highlightActive = highlight.length > 0;
|
|
4389
4606
|
for (const [seriesKey, rows] of groups) {
|
|
4390
4607
|
const color2 = isSequentialColor ? getSequentialColor(scales, _getMidValue(rows, sequentialColorField)) : getColor(scales, seriesKey);
|
|
4391
4608
|
const strokeColor = spec.markDef.stroke ?? getRepresentativeColor5(color2);
|
|
@@ -4430,6 +4647,11 @@ function computeLineMarks(spec, scales, _chartArea, _strategy) {
|
|
|
4430
4647
|
const combinedPath = pathParts.join(" ");
|
|
4431
4648
|
const seriesStyleKey = seriesKey === "__default__" ? void 0 : seriesKey;
|
|
4432
4649
|
const styleOverride = seriesStyleKey ? spec.seriesStyles?.[seriesStyleKey] : void 0;
|
|
4650
|
+
const isMuted = highlightActive && !highlight.includes(seriesKey);
|
|
4651
|
+
let resolvedStrokeWidth = styleOverride?.strokeWidth ?? spec.markDef.strokeWidth ?? DEFAULT_STROKE_WIDTH;
|
|
4652
|
+
if (isMuted) {
|
|
4653
|
+
resolvedStrokeWidth = Math.max(1, resolvedStrokeWidth * 0.75);
|
|
4654
|
+
}
|
|
4433
4655
|
let strokeDasharray;
|
|
4434
4656
|
if (styleOverride?.lineStyle === "dashed") strokeDasharray = "6 4";
|
|
4435
4657
|
else if (styleOverride?.lineStyle === "dotted") strokeDasharray = "2 3";
|
|
@@ -4438,7 +4660,7 @@ function computeLineMarks(spec, scales, _chartArea, _strategy) {
|
|
|
4438
4660
|
points: allPoints,
|
|
4439
4661
|
path: combinedPath,
|
|
4440
4662
|
stroke: strokeColor,
|
|
4441
|
-
strokeWidth:
|
|
4663
|
+
strokeWidth: resolvedStrokeWidth,
|
|
4442
4664
|
strokeDasharray,
|
|
4443
4665
|
opacity: styleOverride?.opacity,
|
|
4444
4666
|
seriesKey: seriesStyleKey,
|
|
@@ -4446,7 +4668,8 @@ function computeLineMarks(spec, scales, _chartArea, _strategy) {
|
|
|
4446
4668
|
dataPoints: pointsWithData.map((p) => ({ x: p.x, y: p.y, datum: p.row })),
|
|
4447
4669
|
aria
|
|
4448
4670
|
};
|
|
4449
|
-
|
|
4671
|
+
const bucket = isMuted ? mutedMarks : highlightedMarks;
|
|
4672
|
+
bucket.push(lineMark);
|
|
4450
4673
|
const markPoint = spec.markDef.point;
|
|
4451
4674
|
const isSingleEndpoint = markPoint === "last" || markPoint === "first";
|
|
4452
4675
|
const showPoints = markPoint === true || markPoint === "transparent" || markPoint === "endpoints" || isSingleEndpoint || isSequentialColor;
|
|
@@ -4473,20 +4696,16 @@ function computeLineMarks(spec, scales, _chartArea, _strategy) {
|
|
|
4473
4696
|
const hollow = isEndpoints && visible;
|
|
4474
4697
|
const pointColorStr = getRepresentativeColor5(pointColor);
|
|
4475
4698
|
if (isSingleEndpoint) {
|
|
4476
|
-
|
|
4699
|
+
bucket.push({
|
|
4477
4700
|
type: "point",
|
|
4478
4701
|
cx: p.x,
|
|
4479
4702
|
cy: p.y,
|
|
4480
|
-
// 3.5 reads as a deliberate terminator against the 2px sparkline
|
|
4481
|
-
// line — smaller dots blur into the stroke at typical card sizes.
|
|
4482
4703
|
r: 3.5,
|
|
4483
4704
|
fill: strokeColor,
|
|
4484
4705
|
stroke: "transparent",
|
|
4485
4706
|
strokeWidth: 0,
|
|
4486
4707
|
fillOpacity: 1,
|
|
4487
4708
|
data: p.row,
|
|
4488
|
-
// No label: decorative marks render with aria-hidden="true" and
|
|
4489
|
-
// don't participate in the accessible data table.
|
|
4490
4709
|
aria: { decorative: true }
|
|
4491
4710
|
});
|
|
4492
4711
|
continue;
|
|
@@ -4505,11 +4724,11 @@ function computeLineMarks(spec, scales, _chartArea, _strategy) {
|
|
|
4505
4724
|
label: `Data point: ${xChannel.field}=${String(p.row[xChannel.field])}, ${yChannel.field}=${String(p.row[yChannel.field])}`
|
|
4506
4725
|
}
|
|
4507
4726
|
};
|
|
4508
|
-
|
|
4727
|
+
bucket.push(pointMark);
|
|
4509
4728
|
}
|
|
4510
4729
|
}
|
|
4511
4730
|
}
|
|
4512
|
-
return
|
|
4731
|
+
return [...mutedMarks, ...highlightedMarks];
|
|
4513
4732
|
}
|
|
4514
4733
|
function _getMidValue(rows, field) {
|
|
4515
4734
|
const values = rows.map((r) => Number(r[field])).filter(Number.isFinite);
|
|
@@ -4853,7 +5072,8 @@ function computePieLabels(marks, _chartArea, density = "auto", _textFill = "#333
|
|
|
4853
5072
|
fill: _textFill,
|
|
4854
5073
|
lineHeight: 1.2,
|
|
4855
5074
|
textAnchor: isRight ? "start" : "end",
|
|
4856
|
-
dominantBaseline: "central"
|
|
5075
|
+
dominantBaseline: "central",
|
|
5076
|
+
fontVariant: "tabular-nums"
|
|
4857
5077
|
}
|
|
4858
5078
|
});
|
|
4859
5079
|
targetMarkIndices.push(mi);
|
|
@@ -8007,6 +8227,30 @@ function normalizeLabels(labels) {
|
|
|
8007
8227
|
fontSize: labels.fontSize
|
|
8008
8228
|
};
|
|
8009
8229
|
}
|
|
8230
|
+
function normalizeHighlight(encoding, data, warnings) {
|
|
8231
|
+
const color2 = encoding.color;
|
|
8232
|
+
if (!color2 || !("field" in color2)) return [];
|
|
8233
|
+
const raw = color2.highlight;
|
|
8234
|
+
if (raw == null) return [];
|
|
8235
|
+
const highlight = Array.isArray(raw) ? raw : [raw];
|
|
8236
|
+
if (highlight.length > 0 && data.length > 0) {
|
|
8237
|
+
const colorField = color2.field;
|
|
8238
|
+
const knownValues = new Set(data.map((row) => String(row[colorField])));
|
|
8239
|
+
for (const v of highlight) {
|
|
8240
|
+
if (!knownValues.has(v)) {
|
|
8241
|
+
warnings.push(
|
|
8242
|
+
`[openchart] encoding.color.highlight value "${v}" does not match any value in the "${colorField}" data column`
|
|
8243
|
+
);
|
|
8244
|
+
}
|
|
8245
|
+
}
|
|
8246
|
+
}
|
|
8247
|
+
if (highlight.length > 0 && color2.scale?.range) {
|
|
8248
|
+
warnings.push(
|
|
8249
|
+
"[openchart] encoding.color.highlight has no effect on color assignment when encoding.color.scale.range is explicitly provided"
|
|
8250
|
+
);
|
|
8251
|
+
}
|
|
8252
|
+
return highlight;
|
|
8253
|
+
}
|
|
8010
8254
|
function normalizeChartSpec(spec, warnings) {
|
|
8011
8255
|
const encoding = inferEncodingTypes(spec.encoding, spec.data, warnings);
|
|
8012
8256
|
const markType = resolveMarkType(spec.mark);
|
|
@@ -8028,13 +8272,16 @@ function normalizeChartSpec(spec, warnings) {
|
|
|
8028
8272
|
labels: normalizeLabels(spec.labels),
|
|
8029
8273
|
legend: spec.legend,
|
|
8030
8274
|
endpointLabels: spec.endpointLabels,
|
|
8031
|
-
responsive: spec.responsive ?? true,
|
|
8275
|
+
responsive: typeof spec.responsive === "object" ? true : spec.responsive ?? true,
|
|
8276
|
+
autoThin: typeof spec.responsive === "object" ? spec.responsive.autoThin ?? true : spec.responsive ?? true,
|
|
8032
8277
|
theme: spec.theme ?? {},
|
|
8033
8278
|
darkMode: spec.darkMode ?? "off",
|
|
8034
8279
|
hiddenSeries: spec.hiddenSeries ?? [],
|
|
8035
8280
|
seriesStyles: spec.seriesStyles ?? {},
|
|
8036
8281
|
watermark: spec.watermark ?? true,
|
|
8282
|
+
highlight: normalizeHighlight(encoding, spec.data, warnings),
|
|
8037
8283
|
display,
|
|
8284
|
+
resolve: spec.resolve,
|
|
8038
8285
|
// Default empty userExplicit; compileChart overwrites this with the real
|
|
8039
8286
|
// descriptor built from the raw expanded spec before normalize runs.
|
|
8040
8287
|
userExplicit: {
|
|
@@ -8119,13 +8366,26 @@ function normalizeTileMapSpec(spec, warnings) {
|
|
|
8119
8366
|
if (!Array.isArray(spec.data)) {
|
|
8120
8367
|
data = Object.entries(spec.data).map(([state, value2]) => ({ state, value: value2 }));
|
|
8121
8368
|
}
|
|
8369
|
+
const hasStringValues = !Array.isArray(spec.data) && Object.values(spec.data).some((v) => typeof v === "string");
|
|
8370
|
+
const isCategorical = spec.colors !== void 0 || spec.encoding?.color !== void 0 || hasStringValues;
|
|
8371
|
+
if (hasStringValues && !Array.isArray(spec.data)) {
|
|
8372
|
+
const hasNumericValues = Object.values(spec.data).some((v) => typeof v === "number");
|
|
8373
|
+
if (hasNumericValues) {
|
|
8374
|
+
warnings.push(
|
|
8375
|
+
"TileMap data: record map contains mixed number and string values. Treating as categorical. Use all numbers for quantitative or all strings for categorical."
|
|
8376
|
+
);
|
|
8377
|
+
}
|
|
8378
|
+
}
|
|
8122
8379
|
let encoding = spec.encoding;
|
|
8123
8380
|
if (!encoding) {
|
|
8124
8381
|
encoding = {
|
|
8125
8382
|
state: { field: "state", type: "nominal" },
|
|
8126
|
-
value: { field: "value", type: "quantitative" }
|
|
8383
|
+
value: { field: "value", type: isCategorical ? "nominal" : "quantitative" }
|
|
8127
8384
|
};
|
|
8128
8385
|
}
|
|
8386
|
+
if (isCategorical && !encoding.color) {
|
|
8387
|
+
encoding = { ...encoding, color: encoding.value };
|
|
8388
|
+
}
|
|
8129
8389
|
let matchedCount = 0;
|
|
8130
8390
|
for (const row of data) {
|
|
8131
8391
|
const stateCode = String(row[encoding.state.field]);
|
|
@@ -8144,6 +8404,7 @@ function normalizeTileMapSpec(spec, warnings) {
|
|
|
8144
8404
|
data,
|
|
8145
8405
|
encoding,
|
|
8146
8406
|
palette: spec.palette ?? "blue",
|
|
8407
|
+
colors: spec.colors,
|
|
8147
8408
|
chrome: normalizeChrome(spec.chrome),
|
|
8148
8409
|
legend: spec.legend,
|
|
8149
8410
|
theme: spec.theme ?? {},
|
|
@@ -8238,6 +8499,7 @@ function flattenLayers(spec, parentData, parentEncoding, parentTransforms, paren
|
|
|
8238
8499
|
|
|
8239
8500
|
// src/compiler/validate.ts
|
|
8240
8501
|
import {
|
|
8502
|
+
inferFieldType as inferFieldType2,
|
|
8241
8503
|
MARK_ENCODING_RULES,
|
|
8242
8504
|
MARK_TYPES
|
|
8243
8505
|
} from "@opendata-ai/openchart-core";
|
|
@@ -8440,6 +8702,68 @@ function validateChartSpec(spec, errors) {
|
|
|
8440
8702
|
}
|
|
8441
8703
|
}
|
|
8442
8704
|
}
|
|
8705
|
+
if (encoding) {
|
|
8706
|
+
const colorCh = encoding.color;
|
|
8707
|
+
if (colorCh && colorCh.highlight != null) {
|
|
8708
|
+
const hlVal = colorCh.highlight;
|
|
8709
|
+
const isValidType = typeof hlVal === "string" || Array.isArray(hlVal) && hlVal.every((v) => typeof v === "string");
|
|
8710
|
+
if (!isValidType) {
|
|
8711
|
+
errors.push({
|
|
8712
|
+
message: "Spec error: encoding.color.highlight must be a string or array of strings",
|
|
8713
|
+
path: "encoding.color.highlight",
|
|
8714
|
+
code: "INVALID_TYPE",
|
|
8715
|
+
suggestion: "Provide a series name (string) or an array of series names."
|
|
8716
|
+
});
|
|
8717
|
+
} else {
|
|
8718
|
+
const colorType = colorCh.type ?? (colorCh.field && Array.isArray(spec.data) && spec.data.length > 0 ? inferFieldType2(spec.data, colorCh.field) : void 0);
|
|
8719
|
+
if (colorType === "quantitative") {
|
|
8720
|
+
errors.push({
|
|
8721
|
+
message: "Spec error: encoding.color.highlight is not supported on quantitative color channels",
|
|
8722
|
+
path: "encoding.color.highlight",
|
|
8723
|
+
code: "INVALID_VALUE",
|
|
8724
|
+
suggestion: "Highlight works with nominal/ordinal color (categorical series). Remove highlight or change encoding.color.type to nominal."
|
|
8725
|
+
});
|
|
8726
|
+
}
|
|
8727
|
+
}
|
|
8728
|
+
}
|
|
8729
|
+
}
|
|
8730
|
+
if (encoding.facet && typeof encoding.facet === "object") {
|
|
8731
|
+
const facet = encoding.facet;
|
|
8732
|
+
if (!facet.field || typeof facet.field !== "string") {
|
|
8733
|
+
errors.push({
|
|
8734
|
+
message: "Spec error: encoding.facet.field is required and must be a non-empty string",
|
|
8735
|
+
path: "encoding.facet.field",
|
|
8736
|
+
code: "MISSING_FIELD",
|
|
8737
|
+
suggestion: 'Add a field property to encoding.facet (e.g. { field: "category", type: "nominal" })'
|
|
8738
|
+
});
|
|
8739
|
+
} else if (!dataColumns.has(facet.field) && !transformFields.has(facet.field)) {
|
|
8740
|
+
errors.push({
|
|
8741
|
+
message: `Spec error: encoding.facet.field "${facet.field}" does not exist in data. Available columns: ${availableColumns}`,
|
|
8742
|
+
path: "encoding.facet.field",
|
|
8743
|
+
code: "DATA_FIELD_MISSING",
|
|
8744
|
+
suggestion: `Use one of the available data columns: ${availableColumns}`
|
|
8745
|
+
});
|
|
8746
|
+
}
|
|
8747
|
+
if (!facet.type || facet.type !== "nominal" && facet.type !== "ordinal") {
|
|
8748
|
+
errors.push({
|
|
8749
|
+
message: `Spec error: encoding.facet.type must be "nominal" or "ordinal"${facet.type ? `, got "${facet.type}"` : ""}`,
|
|
8750
|
+
path: "encoding.facet.type",
|
|
8751
|
+
code: facet.type ? "INVALID_VALUE" : "MISSING_FIELD",
|
|
8752
|
+
suggestion: 'Set encoding.facet.type to "nominal" or "ordinal"'
|
|
8753
|
+
});
|
|
8754
|
+
}
|
|
8755
|
+
if (facet.columns !== void 0) {
|
|
8756
|
+
const cols = Number(facet.columns);
|
|
8757
|
+
if (!Number.isInteger(cols) || cols < 1) {
|
|
8758
|
+
errors.push({
|
|
8759
|
+
message: "Spec error: encoding.facet.columns must be a positive integer",
|
|
8760
|
+
path: "encoding.facet.columns",
|
|
8761
|
+
code: "INVALID_VALUE",
|
|
8762
|
+
suggestion: "Set columns to a positive integer (e.g. 3)"
|
|
8763
|
+
});
|
|
8764
|
+
}
|
|
8765
|
+
}
|
|
8766
|
+
}
|
|
8443
8767
|
if (spec.darkMode !== void 0 && !VALID_DARK_MODES.has(spec.darkMode)) {
|
|
8444
8768
|
errors.push({
|
|
8445
8769
|
message: `Spec error: darkMode must be "auto", "force", or "off"`,
|
|
@@ -9262,7 +9586,8 @@ function compileBarList(spec, options) {
|
|
|
9262
9586
|
fontSize: VALUE_FONT_SIZE,
|
|
9263
9587
|
fontWeight: VALUE_FONT_WEIGHT,
|
|
9264
9588
|
fill: valueColor,
|
|
9265
|
-
lineHeight: 1.4
|
|
9589
|
+
lineHeight: 1.4,
|
|
9590
|
+
fontVariant: "tabular-nums"
|
|
9266
9591
|
};
|
|
9267
9592
|
const rowMark = {
|
|
9268
9593
|
type: "barlist-row",
|
|
@@ -9507,7 +9832,9 @@ function assignAnimationIndices(marks, animation) {
|
|
|
9507
9832
|
}
|
|
9508
9833
|
|
|
9509
9834
|
// src/compile/color-scale-range.ts
|
|
9510
|
-
|
|
9835
|
+
import { ACHROMATIC_RAMP } from "@opendata-ai/openchart-core";
|
|
9836
|
+
var MUTED_COLOR = "#bfc3c8";
|
|
9837
|
+
function applyColorScaleRange(scales, encoding, theme, highlight) {
|
|
9511
9838
|
if (!scales.color) return;
|
|
9512
9839
|
const hasExplicitRange = !!(encoding.color && "field" in encoding.color && encoding.color.scale?.range?.length);
|
|
9513
9840
|
if (hasExplicitRange) return;
|
|
@@ -9518,7 +9845,34 @@ function applyColorScaleRange(scales, encoding, theme) {
|
|
|
9518
9845
|
seqStops[seqStops.length - 1]
|
|
9519
9846
|
]);
|
|
9520
9847
|
} else {
|
|
9521
|
-
scales.color.scale
|
|
9848
|
+
const ordinalScale = scales.color.scale;
|
|
9849
|
+
const palette = theme.colors.categorical;
|
|
9850
|
+
const domain = ordinalScale.domain();
|
|
9851
|
+
if (highlight && highlight.length > 0) {
|
|
9852
|
+
const highlightSet = new Set(highlight);
|
|
9853
|
+
const colors = domain.map(
|
|
9854
|
+
(v, i) => highlightSet.has(v) ? palette[i % palette.length] : MUTED_COLOR
|
|
9855
|
+
);
|
|
9856
|
+
ordinalScale.range(colors);
|
|
9857
|
+
} else {
|
|
9858
|
+
const strategy = theme.seriesStrategy ?? "palette";
|
|
9859
|
+
if (strategy !== "accent-neutral") {
|
|
9860
|
+
ordinalScale.range(palette);
|
|
9861
|
+
} else {
|
|
9862
|
+
const count = domain.length;
|
|
9863
|
+
if (count <= 1) {
|
|
9864
|
+
ordinalScale.range([palette[0]]);
|
|
9865
|
+
} else if (count <= 4) {
|
|
9866
|
+
const neutrals = theme.isDark ? [ACHROMATIC_RAMP.fgMuted, ACHROMATIC_RAMP.fgSubtle, ACHROMATIC_RAMP.fgFaint] : [ACHROMATIC_RAMP.fgFaint, ACHROMATIC_RAMP.fgSubtle, ACHROMATIC_RAMP.fgMuted];
|
|
9867
|
+
const colors = domain.map(
|
|
9868
|
+
(_, i) => i === 0 ? palette[0] : neutrals[(i - 1) % neutrals.length]
|
|
9869
|
+
);
|
|
9870
|
+
ordinalScale.range(colors);
|
|
9871
|
+
} else {
|
|
9872
|
+
ordinalScale.range(palette);
|
|
9873
|
+
}
|
|
9874
|
+
}
|
|
9875
|
+
}
|
|
9522
9876
|
}
|
|
9523
9877
|
}
|
|
9524
9878
|
|
|
@@ -9546,7 +9900,7 @@ import {
|
|
|
9546
9900
|
AXIS_TITLE_TRAILING_PAD as AXIS_TITLE_TRAILING_PAD2,
|
|
9547
9901
|
abbreviateNumber as abbreviateNumber4,
|
|
9548
9902
|
BREAKPOINT_COMPACT_MAX as BREAKPOINT_COMPACT_MAX2,
|
|
9549
|
-
estimateTextWidth as
|
|
9903
|
+
estimateTextWidth as estimateTextWidth13,
|
|
9550
9904
|
formatNumber as formatNumber5,
|
|
9551
9905
|
getAxisTitleOffset,
|
|
9552
9906
|
resolveTheme as resolveTheme2,
|
|
@@ -9561,7 +9915,7 @@ import {
|
|
|
9561
9915
|
BREAKPOINT_COMPACT_MAX,
|
|
9562
9916
|
computeChrome as computeChrome2,
|
|
9563
9917
|
computeXAxisExtentFromLabels,
|
|
9564
|
-
estimateTextWidth as
|
|
9918
|
+
estimateTextWidth as estimateTextWidth12,
|
|
9565
9919
|
formatNumber as formatNumber4,
|
|
9566
9920
|
HPAD_COMPACT_FRACTION,
|
|
9567
9921
|
HPAD_COMPACT_MIN,
|
|
@@ -9575,11 +9929,66 @@ import {
|
|
|
9575
9929
|
TOP_PAD_EXTRA_NARROW
|
|
9576
9930
|
} from "@opendata-ai/openchart-core";
|
|
9577
9931
|
|
|
9932
|
+
// src/endpoint-labels/predict.ts
|
|
9933
|
+
import { estimateTextWidth as estimateTextWidth9, wrapText as wrapText2 } from "@opendata-ai/openchart-core";
|
|
9934
|
+
function predictEndpointLabelsWidth(spec, _theme) {
|
|
9935
|
+
if (spec.endpointLabels === false) return 0;
|
|
9936
|
+
if (spec.markType !== "line" && spec.markType !== "area") return 0;
|
|
9937
|
+
const colorEnc = spec.encoding.color;
|
|
9938
|
+
if (!colorEnc) return 0;
|
|
9939
|
+
if ("condition" in colorEnc) return 0;
|
|
9940
|
+
if (colorEnc.type === "quantitative") return 0;
|
|
9941
|
+
const colorField = "field" in colorEnc ? colorEnc.field : void 0;
|
|
9942
|
+
if (!colorField) return 0;
|
|
9943
|
+
const seriesNames = /* @__PURE__ */ new Set();
|
|
9944
|
+
for (const row of spec.data) {
|
|
9945
|
+
seriesNames.add(String(row[colorField]));
|
|
9946
|
+
}
|
|
9947
|
+
if (seriesNames.size < 2) return 0;
|
|
9948
|
+
if (typeof spec.endpointLabels === "object" && spec.endpointLabels?.show === false) return 0;
|
|
9949
|
+
const maxSeries = typeof spec.endpointLabels === "object" && spec.endpointLabels?.maxSeries || 8;
|
|
9950
|
+
if (!endpointLabelsExplicitlyOn(spec) && seriesNames.size > maxSeries) return 0;
|
|
9951
|
+
const config = typeof spec.endpointLabels === "object" ? spec.endpointLabels : void 0;
|
|
9952
|
+
const wrapWidth = config?.width ?? ENDPOINT_WRAP_WIDTH_DEFAULT;
|
|
9953
|
+
let maxLabelWidth = 0;
|
|
9954
|
+
for (const name of seriesNames) {
|
|
9955
|
+
const lines = wrapText2(name, ENDPOINT_LABEL_FONT_SIZE, ENDPOINT_LABEL_FONT_WEIGHT, wrapWidth);
|
|
9956
|
+
for (const line of lines) {
|
|
9957
|
+
const w = estimateTextWidth9(line, ENDPOINT_LABEL_FONT_SIZE, ENDPOINT_LABEL_FONT_WEIGHT);
|
|
9958
|
+
if (w > maxLabelWidth) maxLabelWidth = w;
|
|
9959
|
+
}
|
|
9960
|
+
}
|
|
9961
|
+
const yField = config?.valueField ?? spec.encoding.y?.field;
|
|
9962
|
+
const yFormat = config?.format ?? spec.encoding.y?.axis?.format;
|
|
9963
|
+
let valueWidth = 0;
|
|
9964
|
+
if (yField) {
|
|
9965
|
+
let maxAbs = 0;
|
|
9966
|
+
for (const row of spec.data) {
|
|
9967
|
+
const v = Number(row[yField]);
|
|
9968
|
+
if (Number.isFinite(v) && Math.abs(v) > maxAbs) maxAbs = Math.abs(v);
|
|
9969
|
+
}
|
|
9970
|
+
let sample;
|
|
9971
|
+
if (yFormat) {
|
|
9972
|
+
sample = formatEndpointValue(maxAbs, yFormat);
|
|
9973
|
+
} else if (maxAbs >= 1e9) sample = "1.5B";
|
|
9974
|
+
else if (maxAbs >= 1e6) sample = "1.5M";
|
|
9975
|
+
else if (maxAbs >= 1e3) sample = "1.5K";
|
|
9976
|
+
else sample = String(Math.round(maxAbs * 100) / 100);
|
|
9977
|
+
valueWidth = estimateTextWidth9(sample, ENDPOINT_VALUE_FONT_SIZE, ENDPOINT_VALUE_FONT_WEIGHT);
|
|
9978
|
+
}
|
|
9979
|
+
const textColumn = Math.max(maxLabelWidth, valueWidth);
|
|
9980
|
+
return ENDPOINT_SWATCH_SIZE + ENDPOINT_GAP + textColumn + 4;
|
|
9981
|
+
}
|
|
9982
|
+
function isEndsBoth(spec) {
|
|
9983
|
+
const ep = spec.endpointLabels;
|
|
9984
|
+
return typeof ep === "object" && ep != null && ep.ends === "both";
|
|
9985
|
+
}
|
|
9986
|
+
|
|
9578
9987
|
// src/legend/compute.ts
|
|
9579
|
-
import { BRAND_RESERVE_WIDTH, COMPACT_WIDTH as COMPACT_WIDTH2, estimateTextWidth as
|
|
9988
|
+
import { BRAND_RESERVE_WIDTH, COMPACT_WIDTH as COMPACT_WIDTH2, estimateTextWidth as estimateTextWidth11 } from "@opendata-ai/openchart-core";
|
|
9580
9989
|
|
|
9581
9990
|
// src/legend/wrap.ts
|
|
9582
|
-
import { COMPACT_WIDTH, estimateTextWidth as
|
|
9991
|
+
import { COMPACT_WIDTH, estimateTextWidth as estimateTextWidth10 } from "@opendata-ai/openchart-core";
|
|
9583
9992
|
var SWATCH_SIZE = 12;
|
|
9584
9993
|
var SWATCH_GAP = 6;
|
|
9585
9994
|
var ENTRY_GAP = 16;
|
|
@@ -9592,7 +10001,7 @@ function measureLegendWrap(entries, maxWidth, labelStyle, maxRows, entryGap = EN
|
|
|
9592
10001
|
if (entries.length === 0) {
|
|
9593
10002
|
return { rowCount: 0, fittingCount: 0, rowWidths: [], placements: [] };
|
|
9594
10003
|
}
|
|
9595
|
-
const measureWidth = measure ??
|
|
10004
|
+
const measureWidth = measure ?? estimateTextWidth10;
|
|
9596
10005
|
let rowCount = 1;
|
|
9597
10006
|
let rowWidth = 0;
|
|
9598
10007
|
const rowWidths = [];
|
|
@@ -9721,8 +10130,8 @@ function emptyContent(position, theme) {
|
|
|
9721
10130
|
...categoricalDefaults(theme)
|
|
9722
10131
|
};
|
|
9723
10132
|
}
|
|
9724
|
-
function computeLegendContent(spec, strategy, theme, availableWidth, availableHeight, watermark = true, measure) {
|
|
9725
|
-
const measureWidth = measure ??
|
|
10133
|
+
function computeLegendContent(spec, strategy, theme, availableWidth, availableHeight, watermark = true, measure, endpointLabelsDemoted) {
|
|
10134
|
+
const measureWidth = measure ?? estimateTextWidth11;
|
|
9726
10135
|
const sparklineHidden = spec.display === "sparkline" && !spec.userExplicit.legend;
|
|
9727
10136
|
if (sparklineHidden || spec.legend?.show === false || strategy.legendMaxHeight === 0) {
|
|
9728
10137
|
return emptyContent("top", theme);
|
|
@@ -9732,11 +10141,23 @@ function computeLegendContent(spec, strategy, theme, availableWidth, availableHe
|
|
|
9732
10141
|
const suppression = resolveSuppression(spec, {
|
|
9733
10142
|
seriesCount,
|
|
9734
10143
|
labelsHiddenByStrategy: strategy.labelMode === "none",
|
|
9735
|
-
labelsDensityNone: spec.labels.density === "none"
|
|
10144
|
+
labelsDensityNone: spec.labels.density === "none",
|
|
10145
|
+
endpointLabelsDemoted
|
|
9736
10146
|
});
|
|
9737
10147
|
if (!suppression.showTraditionalLegend) {
|
|
9738
10148
|
entries = [];
|
|
9739
10149
|
}
|
|
10150
|
+
const hasRedundantLegend = spec.markType === "bar" || spec.markType === "lollipop";
|
|
10151
|
+
if (hasRedundantLegend && entries.length > 0 && !spec.userExplicit?.legend) {
|
|
10152
|
+
const colorEnc = spec.encoding.color;
|
|
10153
|
+
const colorField = colorEnc && "field" in colorEnc ? colorEnc.field : void 0;
|
|
10154
|
+
if (colorField) {
|
|
10155
|
+
const categoryField = spec.encoding.y?.type === "quantitative" ? spec.encoding.x?.field : spec.encoding.y?.field;
|
|
10156
|
+
if (colorField === categoryField) {
|
|
10157
|
+
entries = [];
|
|
10158
|
+
}
|
|
10159
|
+
}
|
|
10160
|
+
}
|
|
9740
10161
|
const labelStyle = {
|
|
9741
10162
|
fontFamily: theme.fonts.family,
|
|
9742
10163
|
fontSize: theme.fonts.sizes.small,
|
|
@@ -10611,7 +11032,7 @@ function createMeasureFn(measureText) {
|
|
|
10611
11032
|
if (measureText) {
|
|
10612
11033
|
return (text, fontSize, fontWeight) => measureText(text, fontSize, fontWeight).width;
|
|
10613
11034
|
}
|
|
10614
|
-
return
|
|
11035
|
+
return estimateTextWidth12;
|
|
10615
11036
|
}
|
|
10616
11037
|
function sampleLabelFromMagnitude(maxAbsVal) {
|
|
10617
11038
|
return maxAbsVal >= 1e3 ? abbreviateNumber3(maxAbsVal) : formatNumber4(maxAbsVal);
|
|
@@ -10743,6 +11164,14 @@ function resolveLayoutPlan(chartSpec, renderSpec, options, theme, strategy, wate
|
|
|
10743
11164
|
}
|
|
10744
11165
|
let prev = null;
|
|
10745
11166
|
let finalPlan = null;
|
|
11167
|
+
const MAX_ENDPOINT_FRACTION = 0.3;
|
|
11168
|
+
let endpointLabelsDemoted = false;
|
|
11169
|
+
if (chartSpec.autoThin) {
|
|
11170
|
+
const epWidth = predictEndpointLabelsWidth(chartSpec, theme);
|
|
11171
|
+
if (epWidth > 0 && epWidth / width > MAX_ENDPOINT_FRACTION) {
|
|
11172
|
+
endpointLabelsDemoted = true;
|
|
11173
|
+
}
|
|
11174
|
+
}
|
|
10746
11175
|
for (let iter = 0; iter < 2; iter++) {
|
|
10747
11176
|
const legendAvailWidth = width - padding - leftGutter;
|
|
10748
11177
|
const legendContent = computeLegendContent(
|
|
@@ -10752,7 +11181,8 @@ function resolveLayoutPlan(chartSpec, renderSpec, options, theme, strategy, wate
|
|
|
10752
11181
|
legendAvailWidth,
|
|
10753
11182
|
height,
|
|
10754
11183
|
watermark,
|
|
10755
|
-
measure
|
|
11184
|
+
measure,
|
|
11185
|
+
endpointLabelsDemoted
|
|
10756
11186
|
);
|
|
10757
11187
|
const bottomLegendReservation = legendContent.entries.length > 0 && legendContent.position === "bottom" ? legendContent.height + legendGap(width) : 0;
|
|
10758
11188
|
const chrome = computeChrome2(
|
|
@@ -10938,7 +11368,8 @@ function resolveLayoutPlan(chartSpec, renderSpec, options, theme, strategy, wate
|
|
|
10938
11368
|
yTickValues,
|
|
10939
11369
|
yTickCount,
|
|
10940
11370
|
converged: prev === null || converged,
|
|
10941
|
-
inlineYLabelInset
|
|
11371
|
+
inlineYLabelInset,
|
|
11372
|
+
endpointLabelsDemoted
|
|
10942
11373
|
};
|
|
10943
11374
|
break;
|
|
10944
11375
|
}
|
|
@@ -11009,7 +11440,7 @@ function estimateYAxisLabelWidth(data, encoding, baseFontSize, measure) {
|
|
|
11009
11440
|
const yEnc = encoding.y;
|
|
11010
11441
|
const yField = yEnc.field;
|
|
11011
11442
|
if (!yField) return 40;
|
|
11012
|
-
const m = measure ??
|
|
11443
|
+
const m = measure ?? estimateTextWidth13;
|
|
11013
11444
|
const yType = yEnc.type;
|
|
11014
11445
|
if (yType === "nominal" || yType === "ordinal") {
|
|
11015
11446
|
let maxWidth = 0;
|
|
@@ -11808,17 +12239,17 @@ var DEFAULT_COLLISION_PADDING = 5;
|
|
|
11808
12239
|
import {
|
|
11809
12240
|
axisTitleOffset as axisTitleOffset2,
|
|
11810
12241
|
computeXAxisExtentFromLabels as computeXAxisExtentFromLabels2,
|
|
11811
|
-
estimateTextWidth as
|
|
12242
|
+
estimateTextWidth as estimateTextWidth15,
|
|
11812
12243
|
getAxisTitleOffset as getAxisTitleOffset2,
|
|
11813
12244
|
X_AXIS_TITLE_BAND_ROTATED
|
|
11814
12245
|
} from "@opendata-ai/openchart-core";
|
|
11815
12246
|
|
|
11816
12247
|
// src/layout/axes/thinning.ts
|
|
11817
|
-
import { estimateTextWidth as
|
|
12248
|
+
import { estimateTextWidth as estimateTextWidth14 } from "@opendata-ai/openchart-core";
|
|
11818
12249
|
var MIN_TICK_GAP_FACTOR = 0.5;
|
|
11819
12250
|
var MIN_TICK_COUNT = 2;
|
|
11820
12251
|
function measureLabel(text, fontSize, fontWeight, measureText) {
|
|
11821
|
-
return measureText ? measureText(text, fontSize, fontWeight).width :
|
|
12252
|
+
return measureText ? measureText(text, fontSize, fontWeight).width : estimateTextWidth14(text, fontSize, fontWeight);
|
|
11822
12253
|
}
|
|
11823
12254
|
function ticksOverlap(ticks2, fontSize, fontWeight, measureText, orientation = "horizontal") {
|
|
11824
12255
|
if (ticks2.length < 2) return false;
|
|
@@ -11863,6 +12294,7 @@ var HEIGHT_REDUCED_THRESHOLD = 100;
|
|
|
11863
12294
|
var WIDTH_MINIMAL_THRESHOLD = 150;
|
|
11864
12295
|
var WIDTH_REDUCED_THRESHOLD = 300;
|
|
11865
12296
|
var DENSITY_ORDER = ["full", "reduced", "minimal"];
|
|
12297
|
+
var GROUNDED_MARKS = /* @__PURE__ */ new Set(["bar", "tick", "lollipop"]);
|
|
11866
12298
|
function effectiveDensity(baseDensity, axisLength, minimalThreshold, reducedThreshold) {
|
|
11867
12299
|
let density = baseDensity;
|
|
11868
12300
|
if (axisLength < minimalThreshold) {
|
|
@@ -12027,9 +12459,10 @@ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContex
|
|
|
12027
12459
|
const axisTitle = axisConfig?.title;
|
|
12028
12460
|
const xLabelColor = axisConfig?.labelColor;
|
|
12029
12461
|
const xTickPosition = axisConfig?.tickPosition ?? "gutter";
|
|
12462
|
+
const xDomainLine = axisConfig?.domain ?? (GROUNDED_MARKS.has(dataContext?.markType ?? "") ? void 0 : false);
|
|
12030
12463
|
result.x = {
|
|
12031
12464
|
ticks: ticks2,
|
|
12032
|
-
gridlines: axisConfig?.grid ? gridlines : [],
|
|
12465
|
+
gridlines: axisConfig?.grid ?? dataContext?.markType === "point" ? gridlines : [],
|
|
12033
12466
|
label: axisTitle,
|
|
12034
12467
|
labelStyle: xLabelColor ? { ...axisLabelStyle, fill: xLabelColor } : axisLabelStyle,
|
|
12035
12468
|
tickLabelStyle: xLabelColor ? { ...tickLabelStyle, fill: xLabelColor } : tickLabelStyle,
|
|
@@ -12037,7 +12470,7 @@ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContex
|
|
|
12037
12470
|
start: { x: chartArea.x, y: chartArea.y + chartArea.height },
|
|
12038
12471
|
end: { x: chartArea.x + chartArea.width, y: chartArea.y + chartArea.height },
|
|
12039
12472
|
orient: axisConfig?.orient,
|
|
12040
|
-
domainLine:
|
|
12473
|
+
domainLine: xDomainLine,
|
|
12041
12474
|
tickMarks: axisConfig?.ticks,
|
|
12042
12475
|
offset: axisConfig?.offset,
|
|
12043
12476
|
titlePadding: axisConfig?.titlePadding,
|
|
@@ -12150,7 +12583,7 @@ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContex
|
|
|
12150
12583
|
};
|
|
12151
12584
|
} else {
|
|
12152
12585
|
const maxTickLabelWidth = result.y.ticks.reduce((max4, t) => {
|
|
12153
|
-
const w =
|
|
12586
|
+
const w = estimateTextWidth15(
|
|
12154
12587
|
t.label,
|
|
12155
12588
|
result.y.tickLabelStyle.fontSize,
|
|
12156
12589
|
result.y.tickLabelStyle.fontWeight ?? 400
|
|
@@ -12187,55 +12620,6 @@ import {
|
|
|
12187
12620
|
TOP_PAD_EXTRA_NARROW as TOP_PAD_EXTRA_NARROW2
|
|
12188
12621
|
} from "@opendata-ai/openchart-core";
|
|
12189
12622
|
|
|
12190
|
-
// src/endpoint-labels/predict.ts
|
|
12191
|
-
import { estimateTextWidth as estimateTextWidth15, wrapText as wrapText2 } from "@opendata-ai/openchart-core";
|
|
12192
|
-
function predictEndpointLabelsWidth(spec, _theme) {
|
|
12193
|
-
if (spec.endpointLabels === false) return 0;
|
|
12194
|
-
if (spec.markType !== "line" && spec.markType !== "area") return 0;
|
|
12195
|
-
const colorEnc = spec.encoding.color;
|
|
12196
|
-
if (!colorEnc) return 0;
|
|
12197
|
-
if ("condition" in colorEnc) return 0;
|
|
12198
|
-
if (colorEnc.type === "quantitative") return 0;
|
|
12199
|
-
const colorField = "field" in colorEnc ? colorEnc.field : void 0;
|
|
12200
|
-
if (!colorField) return 0;
|
|
12201
|
-
const seriesNames = /* @__PURE__ */ new Set();
|
|
12202
|
-
for (const row of spec.data) {
|
|
12203
|
-
seriesNames.add(String(row[colorField]));
|
|
12204
|
-
}
|
|
12205
|
-
if (seriesNames.size < 2) return 0;
|
|
12206
|
-
if (typeof spec.endpointLabels === "object" && spec.endpointLabels?.show === false) return 0;
|
|
12207
|
-
const config = typeof spec.endpointLabels === "object" ? spec.endpointLabels : void 0;
|
|
12208
|
-
const wrapWidth = config?.width ?? ENDPOINT_WRAP_WIDTH_DEFAULT;
|
|
12209
|
-
let maxLabelWidth = 0;
|
|
12210
|
-
for (const name of seriesNames) {
|
|
12211
|
-
const lines = wrapText2(name, ENDPOINT_LABEL_FONT_SIZE, ENDPOINT_LABEL_FONT_WEIGHT, wrapWidth);
|
|
12212
|
-
for (const line of lines) {
|
|
12213
|
-
const w = estimateTextWidth15(line, ENDPOINT_LABEL_FONT_SIZE, ENDPOINT_LABEL_FONT_WEIGHT);
|
|
12214
|
-
if (w > maxLabelWidth) maxLabelWidth = w;
|
|
12215
|
-
}
|
|
12216
|
-
}
|
|
12217
|
-
const yField = config?.valueField ?? spec.encoding.y?.field;
|
|
12218
|
-
const yFormat = config?.format ?? spec.encoding.y?.axis?.format;
|
|
12219
|
-
let valueWidth = 0;
|
|
12220
|
-
if (yField) {
|
|
12221
|
-
let maxAbs = 0;
|
|
12222
|
-
for (const row of spec.data) {
|
|
12223
|
-
const v = Number(row[yField]);
|
|
12224
|
-
if (Number.isFinite(v) && Math.abs(v) > maxAbs) maxAbs = Math.abs(v);
|
|
12225
|
-
}
|
|
12226
|
-
let sample;
|
|
12227
|
-
if (yFormat) {
|
|
12228
|
-
sample = formatEndpointValue(maxAbs, yFormat);
|
|
12229
|
-
} else if (maxAbs >= 1e9) sample = "1.5B";
|
|
12230
|
-
else if (maxAbs >= 1e6) sample = "1.5M";
|
|
12231
|
-
else if (maxAbs >= 1e3) sample = "1.5K";
|
|
12232
|
-
else sample = String(Math.round(maxAbs * 100) / 100);
|
|
12233
|
-
valueWidth = estimateTextWidth15(sample, ENDPOINT_VALUE_FONT_SIZE, ENDPOINT_VALUE_FONT_WEIGHT);
|
|
12234
|
-
}
|
|
12235
|
-
const textColumn = Math.max(maxLabelWidth, valueWidth);
|
|
12236
|
-
return ENDPOINT_SWATCH_SIZE + ENDPOINT_GAP + textColumn + 4;
|
|
12237
|
-
}
|
|
12238
|
-
|
|
12239
12623
|
// src/layout/metrics.ts
|
|
12240
12624
|
import { estimateTextWidth as estimateTextWidth16 } from "@opendata-ai/openchart-core";
|
|
12241
12625
|
var DEFAULT_LABEL_FONT_SIZE = 10;
|
|
@@ -12419,13 +12803,17 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
12419
12803
|
const sup = resolveSuppression(spec, {
|
|
12420
12804
|
seriesCount,
|
|
12421
12805
|
labelsHiddenByStrategy,
|
|
12422
|
-
labelsDensityNone: labelDensity === "none"
|
|
12806
|
+
labelsDensityNone: labelDensity === "none",
|
|
12807
|
+
endpointLabelsDemoted: plan?.endpointLabelsDemoted
|
|
12423
12808
|
});
|
|
12424
12809
|
let endpointWidth = 0;
|
|
12425
12810
|
if (sup.showEndpointLabels && !labelsHiddenByStrategy) {
|
|
12426
12811
|
endpointWidth = predictEndpointLabelsWidth(spec, theme);
|
|
12427
12812
|
if (endpointWidth > 0) {
|
|
12428
12813
|
margins.right = Math.max(margins.right, hPad) + endpointWidth + 16;
|
|
12814
|
+
if (isEndsBoth(spec)) {
|
|
12815
|
+
margins.left = Math.max(margins.left, hPad) + endpointWidth + 16;
|
|
12816
|
+
}
|
|
12429
12817
|
}
|
|
12430
12818
|
}
|
|
12431
12819
|
if (endpointWidth === 0 && sup.showEndOfLineLabels && (spec.markType === "line" || spec.markType === "area") && labelDensity !== "none" && !labelsHiddenByStrategy) {
|
|
@@ -12701,6 +13089,57 @@ function resolveMetrics(spec, metricsTopY, metricsArea, remainingChartHeight, me
|
|
|
12701
13089
|
);
|
|
12702
13090
|
}
|
|
12703
13091
|
|
|
13092
|
+
// src/layout/facet.ts
|
|
13093
|
+
var MIN_PANEL_WIDTH = 200;
|
|
13094
|
+
var PANEL_GAP = 12;
|
|
13095
|
+
var HEADER_HEIGHT = 20;
|
|
13096
|
+
function autoColumns(panelCount, availableWidth) {
|
|
13097
|
+
const sqrtCols = Math.ceil(Math.sqrt(panelCount));
|
|
13098
|
+
const fitCols = Math.max(1, Math.floor(availableWidth / MIN_PANEL_WIDTH));
|
|
13099
|
+
return Math.min(sqrtCols, fitCols, panelCount);
|
|
13100
|
+
}
|
|
13101
|
+
function computeFacetGrid(facetValues, requestedColumns, totalArea, outerAxisReservation) {
|
|
13102
|
+
const n = facetValues.length;
|
|
13103
|
+
if (n === 0) return { panels: [], columns: 0, rows: 0 };
|
|
13104
|
+
const usableWidth = totalArea.width - outerAxisReservation.left;
|
|
13105
|
+
let columns = Math.min(requestedColumns ?? autoColumns(n, usableWidth), n);
|
|
13106
|
+
while (columns > 1) {
|
|
13107
|
+
const panelWidth2 = (usableWidth - (columns - 1) * PANEL_GAP) / columns;
|
|
13108
|
+
if (panelWidth2 >= MIN_PANEL_WIDTH) break;
|
|
13109
|
+
columns--;
|
|
13110
|
+
}
|
|
13111
|
+
const rows = Math.ceil(n / columns);
|
|
13112
|
+
const panelWidth = Math.max(0, (usableWidth - (columns - 1) * PANEL_GAP) / columns);
|
|
13113
|
+
const usableHeight = totalArea.height - outerAxisReservation.bottom;
|
|
13114
|
+
const panelHeight = Math.max(
|
|
13115
|
+
0,
|
|
13116
|
+
(usableHeight - rows * HEADER_HEIGHT - (rows - 1) * PANEL_GAP) / rows
|
|
13117
|
+
);
|
|
13118
|
+
const panels = [];
|
|
13119
|
+
for (let i = 0; i < n; i++) {
|
|
13120
|
+
const col = i % columns;
|
|
13121
|
+
const row = Math.floor(i / columns);
|
|
13122
|
+
const x2 = totalArea.x + outerAxisReservation.left + col * (panelWidth + PANEL_GAP);
|
|
13123
|
+
const y2 = totalArea.y + row * (HEADER_HEIGHT + panelHeight + PANEL_GAP);
|
|
13124
|
+
panels.push({
|
|
13125
|
+
key: facetValues[i],
|
|
13126
|
+
area: {
|
|
13127
|
+
x: x2,
|
|
13128
|
+
y: y2 + HEADER_HEIGHT,
|
|
13129
|
+
width: panelWidth,
|
|
13130
|
+
height: panelHeight
|
|
13131
|
+
},
|
|
13132
|
+
headerPos: {
|
|
13133
|
+
x: x2 + panelWidth / 2,
|
|
13134
|
+
y: y2 + HEADER_HEIGHT * 0.6
|
|
13135
|
+
},
|
|
13136
|
+
row,
|
|
13137
|
+
col
|
|
13138
|
+
});
|
|
13139
|
+
}
|
|
13140
|
+
return { panels, columns, rows };
|
|
13141
|
+
}
|
|
13142
|
+
|
|
12704
13143
|
// src/layout/gridlines.ts
|
|
12705
13144
|
function computeGridlines(axes, chartArea, showVertical = false) {
|
|
12706
13145
|
const horizontal = [];
|
|
@@ -14339,14 +14778,19 @@ function compileTableLayout(spec, options, theme) {
|
|
|
14339
14778
|
import {
|
|
14340
14779
|
adaptTheme as adaptTheme4,
|
|
14341
14780
|
buildD3Formatter as buildD3Formatter8,
|
|
14781
|
+
CATEGORICAL_PALETTE,
|
|
14342
14782
|
computeChrome as computeChrome7,
|
|
14343
14783
|
estimateTextWidth as estimateTextWidth20,
|
|
14344
14784
|
formatNumber as formatNumber8,
|
|
14785
|
+
pickLabelColor as pickLabelColor2,
|
|
14345
14786
|
resolveTheme as resolveTheme5,
|
|
14346
14787
|
SEQUENTIAL_PALETTES
|
|
14347
14788
|
} from "@opendata-ai/openchart-core";
|
|
14348
14789
|
var TILE_CORNER_RADIUS = 6;
|
|
14349
14790
|
var TILE_STROKE_WIDTH = 1;
|
|
14791
|
+
var LEGEND_SWATCH_SIZE = 10;
|
|
14792
|
+
var LEGEND_SWATCH_GAP = 6;
|
|
14793
|
+
var LEGEND_ENTRY_GAP = 16;
|
|
14350
14794
|
function compileTileMap(spec, options) {
|
|
14351
14795
|
const { spec: normalized } = compile(spec);
|
|
14352
14796
|
if (!("type" in normalized) || normalized.type !== "tilemap") {
|
|
@@ -14389,6 +14833,13 @@ function compileTileMap(spec, options) {
|
|
|
14389
14833
|
if (fullArea.width <= 0 || fullArea.height <= 0) {
|
|
14390
14834
|
return emptyLayout4(chrome, theme, options, watermark);
|
|
14391
14835
|
}
|
|
14836
|
+
const isCategorical = tilemapSpec.encoding.color !== void 0 || tilemapSpec.colors !== void 0;
|
|
14837
|
+
if (isCategorical) {
|
|
14838
|
+
return compileCategorical(tilemapSpec, options, theme, chrome, fullArea, isDarkMode, watermark);
|
|
14839
|
+
}
|
|
14840
|
+
return compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDarkMode, watermark);
|
|
14841
|
+
}
|
|
14842
|
+
function compileQuantitative(tilemapSpec, options, theme, chrome, fullArea, isDarkMode, watermark) {
|
|
14392
14843
|
const stateField = tilemapSpec.encoding.state.field;
|
|
14393
14844
|
const valueField = tilemapSpec.encoding.value.field;
|
|
14394
14845
|
const stateValueMap = /* @__PURE__ */ new Map();
|
|
@@ -14422,12 +14873,8 @@ function compileTileMap(spec, options) {
|
|
|
14422
14873
|
const legendY = tileGridOffsetY + tilePositions.gridHeight + legendGap2;
|
|
14423
14874
|
const legendWidth = tilePositions.gridWidth;
|
|
14424
14875
|
const formatter = buildD3Formatter8(tilemapSpec.valueFormat) ?? formatNumber8;
|
|
14425
|
-
const
|
|
14426
|
-
const
|
|
14427
|
-
const neutralStrokeLight = "#d0d0d0";
|
|
14428
|
-
const neutralStrokeDark = "rgba(255,255,255,0.08)";
|
|
14429
|
-
const neutralFill = isDarkMode ? neutralFillDark : neutralFillLight;
|
|
14430
|
-
const neutralStroke = isDarkMode ? neutralStrokeDark : neutralStrokeLight;
|
|
14876
|
+
const neutralFill = isDarkMode ? "#1e2a30" : "#e0e0e0";
|
|
14877
|
+
const neutralStroke = isDarkMode ? "rgba(255,255,255,0.08)" : "#d0d0d0";
|
|
14431
14878
|
const tiles = [];
|
|
14432
14879
|
for (const { state: stateCode } of US_STATE_TILES) {
|
|
14433
14880
|
const pos = tilePositions.positions.get(stateCode);
|
|
@@ -14438,71 +14885,25 @@ function compileTileMap(spec, options) {
|
|
|
14438
14885
|
const fill = hasData ? baseColor : neutralFill;
|
|
14439
14886
|
const stroke = hasData ? isDarkMode ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)" : neutralStroke;
|
|
14440
14887
|
const formattedValue = hasData ? formatter(value2) : "\u2013";
|
|
14441
|
-
|
|
14442
|
-
|
|
14443
|
-
|
|
14444
|
-
|
|
14445
|
-
|
|
14446
|
-
|
|
14447
|
-
|
|
14448
|
-
|
|
14449
|
-
|
|
14450
|
-
|
|
14451
|
-
|
|
14452
|
-
|
|
14453
|
-
|
|
14454
|
-
|
|
14455
|
-
|
|
14456
|
-
|
|
14457
|
-
|
|
14458
|
-
const valueLabel = sz < 24 ? { text: "", x: 0, y: 0, style: valueLabelStyle, visible: false } : {
|
|
14459
|
-
text: formattedValue,
|
|
14460
|
-
x: tileCenterX,
|
|
14461
|
-
y: tileTopY + sz * 0.78,
|
|
14462
|
-
style: valueLabelStyle,
|
|
14463
|
-
visible: true
|
|
14464
|
-
};
|
|
14465
|
-
const tile = {
|
|
14466
|
-
type: "tile",
|
|
14467
|
-
stateCode,
|
|
14468
|
-
x: tileGridOffsetX + pos.x,
|
|
14469
|
-
y: tileTopY,
|
|
14470
|
-
size: sz,
|
|
14471
|
-
fill,
|
|
14472
|
-
fillOpacity: hasData ? opacity : 1,
|
|
14473
|
-
stroke,
|
|
14474
|
-
strokeWidth: TILE_STROKE_WIDTH,
|
|
14475
|
-
cornerRadius: TILE_CORNER_RADIUS,
|
|
14476
|
-
value: value2 ?? null,
|
|
14477
|
-
formattedValue,
|
|
14478
|
-
hasData,
|
|
14479
|
-
label: {
|
|
14480
|
-
text: stateCode,
|
|
14481
|
-
x: tileCenterX,
|
|
14482
|
-
y: tileTopY + sz * 0.28,
|
|
14483
|
-
style: labelStyle,
|
|
14484
|
-
visible: true
|
|
14485
|
-
},
|
|
14486
|
-
valueLabel,
|
|
14487
|
-
data: { state: stateCode, value: value2, stateName: STATE_NAMES[stateCode] ?? stateCode },
|
|
14488
|
-
aria: {
|
|
14489
|
-
role: "img",
|
|
14490
|
-
label: `${STATE_NAMES[stateCode] ?? stateCode}: ${formattedValue}`
|
|
14491
|
-
},
|
|
14492
|
-
animationIndex: 0
|
|
14493
|
-
};
|
|
14494
|
-
tiles.push(tile);
|
|
14495
|
-
}
|
|
14496
|
-
const indices = Array.from({ length: tiles.length }, (_, i) => i);
|
|
14497
|
-
let seed = 42;
|
|
14498
|
-
for (let i = indices.length - 1; i > 0; i--) {
|
|
14499
|
-
seed = seed * 1103515245 + 12345 & 2147483647;
|
|
14500
|
-
const j = seed % (i + 1);
|
|
14501
|
-
[indices[i], indices[j]] = [indices[j], indices[i]];
|
|
14502
|
-
}
|
|
14503
|
-
for (let i = 0; i < tiles.length; i++) {
|
|
14504
|
-
tiles[i].animationIndex = indices[i];
|
|
14888
|
+
tiles.push(
|
|
14889
|
+
buildTileMark({
|
|
14890
|
+
stateCode,
|
|
14891
|
+
pos,
|
|
14892
|
+
tileSize: tilePositions.tileSize,
|
|
14893
|
+
gridOffsetX: tileGridOffsetX,
|
|
14894
|
+
gridOffsetY: tileGridOffsetY,
|
|
14895
|
+
fill,
|
|
14896
|
+
fillOpacity: hasData ? opacity : 1,
|
|
14897
|
+
stroke,
|
|
14898
|
+
value: value2,
|
|
14899
|
+
formattedValue,
|
|
14900
|
+
hasData,
|
|
14901
|
+
theme,
|
|
14902
|
+
isDarkMode: !!isDarkMode
|
|
14903
|
+
})
|
|
14904
|
+
);
|
|
14505
14905
|
}
|
|
14906
|
+
assignAnimationIndices2(tiles);
|
|
14506
14907
|
let gradientLegend = null;
|
|
14507
14908
|
if (showLegend) {
|
|
14508
14909
|
const numStops = 5;
|
|
@@ -14527,19 +14928,7 @@ function compileTileMap(spec, options) {
|
|
|
14527
14928
|
maxLabel: formatter(max4)
|
|
14528
14929
|
};
|
|
14529
14930
|
}
|
|
14530
|
-
const tooltipDescriptors =
|
|
14531
|
-
for (const tile of tiles) {
|
|
14532
|
-
const fields = [
|
|
14533
|
-
{
|
|
14534
|
-
label: "Value",
|
|
14535
|
-
value: tile.formattedValue
|
|
14536
|
-
}
|
|
14537
|
-
];
|
|
14538
|
-
tooltipDescriptors.set(tile.stateCode, {
|
|
14539
|
-
title: STATE_NAMES[tile.stateCode] ?? tile.stateCode,
|
|
14540
|
-
fields
|
|
14541
|
-
});
|
|
14542
|
-
}
|
|
14931
|
+
const tooltipDescriptors = buildTooltips(tiles);
|
|
14543
14932
|
const a11y = {
|
|
14544
14933
|
altText: `Tile map of US states showing values from ${formatter(min4)} to ${formatter(max4)}`,
|
|
14545
14934
|
dataTableFallback: tiles.map((t) => [t.stateCode, t.formattedValue]),
|
|
@@ -14547,12 +14936,145 @@ function compileTileMap(spec, options) {
|
|
|
14547
14936
|
keyboardNavigable: tiles.length > 0
|
|
14548
14937
|
};
|
|
14549
14938
|
const resolvedAnimation = resolveAnimation(tilemapSpec.animation);
|
|
14939
|
+
const padding = theme.spacing.padding;
|
|
14550
14940
|
const contentHeight = tileGridOffsetY + tilePositions.gridHeight + legendGap2 + legendTotalHeight + chrome.bottomHeight + padding;
|
|
14551
14941
|
return {
|
|
14552
14942
|
area: fullArea,
|
|
14553
14943
|
chrome,
|
|
14554
14944
|
tiles,
|
|
14555
14945
|
gradientLegend,
|
|
14946
|
+
categoricalLegend: null,
|
|
14947
|
+
tooltipDescriptors,
|
|
14948
|
+
a11y,
|
|
14949
|
+
theme,
|
|
14950
|
+
width: options.width,
|
|
14951
|
+
height: contentHeight,
|
|
14952
|
+
animation: resolvedAnimation,
|
|
14953
|
+
watermark,
|
|
14954
|
+
measureText: options.measureText ?? ((text, fontSize) => ({ width: estimateTextWidth20(text, fontSize), height: fontSize }))
|
|
14955
|
+
};
|
|
14956
|
+
}
|
|
14957
|
+
function compileCategorical(tilemapSpec, options, theme, chrome, fullArea, isDarkMode, watermark) {
|
|
14958
|
+
const stateField = tilemapSpec.encoding.state.field;
|
|
14959
|
+
const colorField = tilemapSpec.encoding.color?.field ?? tilemapSpec.encoding.value.field;
|
|
14960
|
+
const stateCategoryMap = /* @__PURE__ */ new Map();
|
|
14961
|
+
for (const row of tilemapSpec.data) {
|
|
14962
|
+
const stateCode = String(row[stateField]);
|
|
14963
|
+
const raw = row[colorField];
|
|
14964
|
+
if (STATE_CODE_SET.has(stateCode) && raw !== null && raw !== void 0) {
|
|
14965
|
+
stateCategoryMap.set(stateCode, String(raw));
|
|
14966
|
+
}
|
|
14967
|
+
}
|
|
14968
|
+
const categories = [];
|
|
14969
|
+
const seen = /* @__PURE__ */ new Set();
|
|
14970
|
+
for (const row of tilemapSpec.data) {
|
|
14971
|
+
const raw = row[colorField];
|
|
14972
|
+
if (raw !== null && raw !== void 0) {
|
|
14973
|
+
const cat = String(raw);
|
|
14974
|
+
if (!seen.has(cat)) {
|
|
14975
|
+
seen.add(cat);
|
|
14976
|
+
categories.push(cat);
|
|
14977
|
+
}
|
|
14978
|
+
}
|
|
14979
|
+
}
|
|
14980
|
+
const categoryColors = /* @__PURE__ */ new Map();
|
|
14981
|
+
if (tilemapSpec.colors) {
|
|
14982
|
+
for (const cat of categories) {
|
|
14983
|
+
categoryColors.set(
|
|
14984
|
+
cat,
|
|
14985
|
+
tilemapSpec.colors[cat] ?? CATEGORICAL_PALETTE[categoryColors.size % CATEGORICAL_PALETTE.length]
|
|
14986
|
+
);
|
|
14987
|
+
}
|
|
14988
|
+
} else {
|
|
14989
|
+
for (let i = 0; i < categories.length; i++) {
|
|
14990
|
+
categoryColors.set(categories[i], CATEGORICAL_PALETTE[i % CATEGORICAL_PALETTE.length]);
|
|
14991
|
+
}
|
|
14992
|
+
}
|
|
14993
|
+
const showLegend = tilemapSpec.legend?.show !== false;
|
|
14994
|
+
const legendRowHeight = LEGEND_SWATCH_SIZE + 6;
|
|
14995
|
+
const legendTotalHeight = showLegend ? legendRowHeight : 0;
|
|
14996
|
+
const legendGap2 = showLegend ? 8 : 0;
|
|
14997
|
+
const tileAreaHeight = fullArea.height - legendTotalHeight - legendGap2;
|
|
14998
|
+
const tilePositions = computeTilePositions(fullArea.width, tileAreaHeight, 5);
|
|
14999
|
+
const tileGridOffsetX = fullArea.x + (fullArea.width - tilePositions.gridWidth) / 2;
|
|
15000
|
+
const tileGridOffsetY = fullArea.y;
|
|
15001
|
+
const legendX = tileGridOffsetX;
|
|
15002
|
+
const legendY = tileGridOffsetY + tilePositions.gridHeight + legendGap2;
|
|
15003
|
+
const legendWidth = tilePositions.gridWidth;
|
|
15004
|
+
const neutralFill = isDarkMode ? "#1e2a30" : "#e0e0e0";
|
|
15005
|
+
const neutralStroke = isDarkMode ? "rgba(255,255,255,0.08)" : "#d0d0d0";
|
|
15006
|
+
const tiles = [];
|
|
15007
|
+
for (const { state: stateCode } of US_STATE_TILES) {
|
|
15008
|
+
const pos = tilePositions.positions.get(stateCode);
|
|
15009
|
+
if (!pos) continue;
|
|
15010
|
+
const hasData = stateCategoryMap.has(stateCode);
|
|
15011
|
+
const category = hasData ? stateCategoryMap.get(stateCode) : null;
|
|
15012
|
+
const fill = hasData ? categoryColors.get(category) ?? neutralFill : neutralFill;
|
|
15013
|
+
const stroke = hasData ? isDarkMode ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)" : neutralStroke;
|
|
15014
|
+
const formattedValue = category ? formatCategoryLabel(category) : "\u2013";
|
|
15015
|
+
tiles.push(
|
|
15016
|
+
buildTileMark({
|
|
15017
|
+
stateCode,
|
|
15018
|
+
pos,
|
|
15019
|
+
tileSize: tilePositions.tileSize,
|
|
15020
|
+
gridOffsetX: tileGridOffsetX,
|
|
15021
|
+
gridOffsetY: tileGridOffsetY,
|
|
15022
|
+
fill,
|
|
15023
|
+
fillOpacity: 1,
|
|
15024
|
+
stroke,
|
|
15025
|
+
value: null,
|
|
15026
|
+
formattedValue,
|
|
15027
|
+
hasData,
|
|
15028
|
+
theme,
|
|
15029
|
+
isDarkMode: !!isDarkMode,
|
|
15030
|
+
category: hasData ? category : void 0
|
|
15031
|
+
})
|
|
15032
|
+
);
|
|
15033
|
+
}
|
|
15034
|
+
assignAnimationIndices2(tiles);
|
|
15035
|
+
let categoricalLegend = null;
|
|
15036
|
+
if (showLegend) {
|
|
15037
|
+
const labelStyle = {
|
|
15038
|
+
fontFamily: theme.fonts.family,
|
|
15039
|
+
fontSize: 11,
|
|
15040
|
+
fontWeight: 400,
|
|
15041
|
+
fill: theme.colors.text,
|
|
15042
|
+
lineHeight: 1.2
|
|
15043
|
+
};
|
|
15044
|
+
const entries = categories.map((cat) => ({
|
|
15045
|
+
label: formatCategoryLabel(cat),
|
|
15046
|
+
color: categoryColors.get(cat),
|
|
15047
|
+
shape: "square"
|
|
15048
|
+
}));
|
|
15049
|
+
categoricalLegend = {
|
|
15050
|
+
type: "categorical",
|
|
15051
|
+
position: "bottom",
|
|
15052
|
+
bounds: { x: legendX, y: legendY, width: legendWidth, height: legendRowHeight },
|
|
15053
|
+
labelStyle,
|
|
15054
|
+
entries,
|
|
15055
|
+
swatchSize: LEGEND_SWATCH_SIZE,
|
|
15056
|
+
swatchGap: LEGEND_SWATCH_GAP,
|
|
15057
|
+
entryGap: LEGEND_ENTRY_GAP,
|
|
15058
|
+
swatchChipFill: "transparent"
|
|
15059
|
+
};
|
|
15060
|
+
}
|
|
15061
|
+
const tooltipDescriptors = buildTooltips(tiles, "Category");
|
|
15062
|
+
const categoryList = categories.map(formatCategoryLabel).join(", ");
|
|
15063
|
+
const a11y = {
|
|
15064
|
+
altText: `Tile map of US states showing categories: ${categoryList}`,
|
|
15065
|
+
dataTableFallback: tiles.map((t) => [t.stateCode, t.formattedValue]),
|
|
15066
|
+
role: "img",
|
|
15067
|
+
keyboardNavigable: tiles.length > 0
|
|
15068
|
+
};
|
|
15069
|
+
const resolvedAnimation = resolveAnimation(tilemapSpec.animation);
|
|
15070
|
+
const padding = theme.spacing.padding;
|
|
15071
|
+
const contentHeight = tileGridOffsetY + tilePositions.gridHeight + legendGap2 + legendTotalHeight + chrome.bottomHeight + padding;
|
|
15072
|
+
return {
|
|
15073
|
+
area: fullArea,
|
|
15074
|
+
chrome,
|
|
15075
|
+
tiles,
|
|
15076
|
+
gradientLegend: null,
|
|
15077
|
+
categoricalLegend,
|
|
14556
15078
|
tooltipDescriptors,
|
|
14557
15079
|
a11y,
|
|
14558
15080
|
theme,
|
|
@@ -14563,6 +15085,112 @@ function compileTileMap(spec, options) {
|
|
|
14563
15085
|
measureText: options.measureText ?? ((text, fontSize) => ({ width: estimateTextWidth20(text, fontSize), height: fontSize }))
|
|
14564
15086
|
};
|
|
14565
15087
|
}
|
|
15088
|
+
function buildTileMark(opts) {
|
|
15089
|
+
const {
|
|
15090
|
+
stateCode,
|
|
15091
|
+
pos,
|
|
15092
|
+
tileSize,
|
|
15093
|
+
gridOffsetX,
|
|
15094
|
+
gridOffsetY,
|
|
15095
|
+
fill,
|
|
15096
|
+
fillOpacity,
|
|
15097
|
+
stroke,
|
|
15098
|
+
value: value2,
|
|
15099
|
+
formattedValue,
|
|
15100
|
+
hasData,
|
|
15101
|
+
theme,
|
|
15102
|
+
isDarkMode,
|
|
15103
|
+
category
|
|
15104
|
+
} = opts;
|
|
15105
|
+
const tileCenterX = gridOffsetX + pos.x + tileSize / 2;
|
|
15106
|
+
const tileTopY = gridOffsetY + pos.y;
|
|
15107
|
+
const textColor = hasData ? pickLabelColor2(fill, isDarkMode) : "#ffffff";
|
|
15108
|
+
const isLightText = textColor === "#ffffff";
|
|
15109
|
+
const labelStyle = {
|
|
15110
|
+
fontFamily: theme.fonts.family,
|
|
15111
|
+
fontSize: tileSize > 24 ? 10 : 7,
|
|
15112
|
+
fontWeight: 700,
|
|
15113
|
+
fill: textColor,
|
|
15114
|
+
lineHeight: 1.2
|
|
15115
|
+
};
|
|
15116
|
+
const valueLabelStyle = {
|
|
15117
|
+
fontFamily: theme.fonts.family,
|
|
15118
|
+
fontSize: tileSize > 24 ? 10 : 7,
|
|
15119
|
+
fontWeight: 300,
|
|
15120
|
+
fill: isLightText ? "rgba(255,255,255,0.6)" : "rgba(0,0,0,0.45)",
|
|
15121
|
+
lineHeight: 1.2
|
|
15122
|
+
};
|
|
15123
|
+
const valueLabel = tileSize < 24 ? { text: "", x: 0, y: 0, style: valueLabelStyle, visible: false } : {
|
|
15124
|
+
text: formattedValue,
|
|
15125
|
+
x: tileCenterX,
|
|
15126
|
+
y: tileTopY + tileSize * 0.78,
|
|
15127
|
+
style: valueLabelStyle,
|
|
15128
|
+
visible: true
|
|
15129
|
+
};
|
|
15130
|
+
const data = {
|
|
15131
|
+
state: stateCode,
|
|
15132
|
+
value: value2,
|
|
15133
|
+
stateName: STATE_NAMES[stateCode] ?? stateCode
|
|
15134
|
+
};
|
|
15135
|
+
if (category !== void 0) {
|
|
15136
|
+
data.category = category;
|
|
15137
|
+
}
|
|
15138
|
+
return {
|
|
15139
|
+
type: "tile",
|
|
15140
|
+
stateCode,
|
|
15141
|
+
x: gridOffsetX + pos.x,
|
|
15142
|
+
y: tileTopY,
|
|
15143
|
+
size: tileSize,
|
|
15144
|
+
fill,
|
|
15145
|
+
fillOpacity,
|
|
15146
|
+
stroke,
|
|
15147
|
+
strokeWidth: TILE_STROKE_WIDTH,
|
|
15148
|
+
cornerRadius: TILE_CORNER_RADIUS,
|
|
15149
|
+
value: value2,
|
|
15150
|
+
formattedValue,
|
|
15151
|
+
hasData,
|
|
15152
|
+
label: {
|
|
15153
|
+
text: stateCode,
|
|
15154
|
+
x: tileCenterX,
|
|
15155
|
+
y: tileTopY + tileSize * 0.28,
|
|
15156
|
+
style: labelStyle,
|
|
15157
|
+
visible: true
|
|
15158
|
+
},
|
|
15159
|
+
valueLabel,
|
|
15160
|
+
data,
|
|
15161
|
+
aria: {
|
|
15162
|
+
role: "img",
|
|
15163
|
+
label: `${STATE_NAMES[stateCode] ?? stateCode}: ${formattedValue}`
|
|
15164
|
+
},
|
|
15165
|
+
animationIndex: 0
|
|
15166
|
+
};
|
|
15167
|
+
}
|
|
15168
|
+
function assignAnimationIndices2(tiles) {
|
|
15169
|
+
const indices = Array.from({ length: tiles.length }, (_, i) => i);
|
|
15170
|
+
let seed = 42;
|
|
15171
|
+
for (let i = indices.length - 1; i > 0; i--) {
|
|
15172
|
+
seed = seed * 1103515245 + 12345 & 2147483647;
|
|
15173
|
+
const j = seed % (i + 1);
|
|
15174
|
+
[indices[i], indices[j]] = [indices[j], indices[i]];
|
|
15175
|
+
}
|
|
15176
|
+
for (let i = 0; i < tiles.length; i++) {
|
|
15177
|
+
tiles[i].animationIndex = indices[i];
|
|
15178
|
+
}
|
|
15179
|
+
}
|
|
15180
|
+
function buildTooltips(tiles, fieldLabel = "Value") {
|
|
15181
|
+
const tooltipDescriptors = /* @__PURE__ */ new Map();
|
|
15182
|
+
for (const tile of tiles) {
|
|
15183
|
+
const fields = [{ label: fieldLabel, value: tile.formattedValue }];
|
|
15184
|
+
tooltipDescriptors.set(tile.stateCode, {
|
|
15185
|
+
title: STATE_NAMES[tile.stateCode] ?? tile.stateCode,
|
|
15186
|
+
fields
|
|
15187
|
+
});
|
|
15188
|
+
}
|
|
15189
|
+
return tooltipDescriptors;
|
|
15190
|
+
}
|
|
15191
|
+
function formatCategoryLabel(category) {
|
|
15192
|
+
return category.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
15193
|
+
}
|
|
14566
15194
|
function emptyLayout4(chrome, theme, options, watermark) {
|
|
14567
15195
|
return {
|
|
14568
15196
|
area: { x: 0, y: 0, width: 0, height: 0 },
|
|
@@ -14583,6 +15211,7 @@ function emptyLayout4(chrome, theme, options, watermark) {
|
|
|
14583
15211
|
minLabel: "0",
|
|
14584
15212
|
maxLabel: "0"
|
|
14585
15213
|
},
|
|
15214
|
+
categoricalLegend: null,
|
|
14586
15215
|
tooltipDescriptors: /* @__PURE__ */ new Map(),
|
|
14587
15216
|
a11y: {
|
|
14588
15217
|
altText: "Empty tile map",
|
|
@@ -15552,6 +16181,20 @@ function compileChart(spec, options) {
|
|
|
15552
16181
|
}
|
|
15553
16182
|
};
|
|
15554
16183
|
}
|
|
16184
|
+
const facetChannel = renderSpec.encoding.facet;
|
|
16185
|
+
if (facetChannel?.field) {
|
|
16186
|
+
return compileFaceted(
|
|
16187
|
+
chartSpec,
|
|
16188
|
+
renderSpec,
|
|
16189
|
+
facetChannel,
|
|
16190
|
+
options,
|
|
16191
|
+
theme,
|
|
16192
|
+
strategy,
|
|
16193
|
+
watermark,
|
|
16194
|
+
resolvedAnimation,
|
|
16195
|
+
crosshair
|
|
16196
|
+
);
|
|
16197
|
+
}
|
|
15555
16198
|
const measure = createMeasureFn(options.measureText);
|
|
15556
16199
|
const plan = resolveLayoutPlan(
|
|
15557
16200
|
chartSpec,
|
|
@@ -15588,7 +16231,7 @@ function compileChart(spec, options) {
|
|
|
15588
16231
|
};
|
|
15589
16232
|
}
|
|
15590
16233
|
const scales = computeScales(renderSpec, scaleArea, renderSpec.data);
|
|
15591
|
-
applyColorScaleRange(scales, renderSpec.encoding, theme);
|
|
16234
|
+
applyColorScaleRange(scales, renderSpec.encoding, theme, renderSpec.highlight);
|
|
15592
16235
|
scales.defaultColor = chartSpec.markDef.fill ?? chartSpec.markDef.stroke ?? theme.colors.categorical[0];
|
|
15593
16236
|
const isRadial = chartSpec.markType === "arc";
|
|
15594
16237
|
const skipX = chartSpec.display === "sparkline" && !chartSpec.userExplicit.xAxis;
|
|
@@ -15614,7 +16257,14 @@ function compileChart(spec, options) {
|
|
|
15614
16257
|
);
|
|
15615
16258
|
const renderer = getChartRenderer(rendererKey);
|
|
15616
16259
|
const marks = renderer ? renderer(renderSpec, scales, chartArea, strategy, theme, options.width) : [];
|
|
15617
|
-
const endpointLabels = computeEndpointLabels(
|
|
16260
|
+
const endpointLabels = computeEndpointLabels(
|
|
16261
|
+
chartSpec,
|
|
16262
|
+
marks,
|
|
16263
|
+
theme,
|
|
16264
|
+
chartArea,
|
|
16265
|
+
strategy,
|
|
16266
|
+
plan.endpointLabelsDemoted
|
|
16267
|
+
);
|
|
15618
16268
|
const obstacles = [];
|
|
15619
16269
|
if (finalLegend.bounds.width > 0) {
|
|
15620
16270
|
obstacles.push({ ...finalLegend.bounds, kind: "legend" });
|
|
@@ -15631,15 +16281,24 @@ function compileChart(spec, options) {
|
|
|
15631
16281
|
obstacles.push({ ...endpointLabels.bounds, kind: "endpoint-label" });
|
|
15632
16282
|
}
|
|
15633
16283
|
const annotationMeasure = options.measureText ? (text, font) => options.measureText(text, font.fontSize, font.fontWeight).width : heuristicMeasure;
|
|
15634
|
-
|
|
16284
|
+
let annotations = computeAnnotations(chartSpec, {
|
|
15635
16285
|
scales,
|
|
15636
16286
|
chartArea,
|
|
15637
16287
|
strategy,
|
|
15638
16288
|
isDark: theme.isDark,
|
|
15639
16289
|
obstacles,
|
|
15640
16290
|
svg: { width: dims.total.width, height: dims.total.height },
|
|
15641
|
-
measure: annotationMeasure
|
|
16291
|
+
measure: annotationMeasure,
|
|
16292
|
+
autoThin: chartSpec.autoThin
|
|
15642
16293
|
});
|
|
16294
|
+
let footnotes;
|
|
16295
|
+
if (chartSpec.autoThin && annotations.length > 1) {
|
|
16296
|
+
const result = thinAnnotations(annotations, chartSpec.annotations, annotationMeasure);
|
|
16297
|
+
annotations = result.annotations;
|
|
16298
|
+
if (result.footnotes.length > 0) {
|
|
16299
|
+
footnotes = result.footnotes;
|
|
16300
|
+
}
|
|
16301
|
+
}
|
|
15643
16302
|
const tooltipDescriptors = computeTooltipDescriptors(chartSpec, marks);
|
|
15644
16303
|
const altText = generateAltText(
|
|
15645
16304
|
{
|
|
@@ -15659,9 +16318,10 @@ function compileChart(spec, options) {
|
|
|
15659
16318
|
chartSpec.data
|
|
15660
16319
|
);
|
|
15661
16320
|
assignAnimationIndices(marks, resolvedAnimation);
|
|
16321
|
+
const chrome = footnotes ? { ...dims.chrome, footnotes } : dims.chrome;
|
|
15662
16322
|
return {
|
|
15663
16323
|
area: chartArea,
|
|
15664
|
-
chrome
|
|
16324
|
+
chrome,
|
|
15665
16325
|
metrics: dims.metrics,
|
|
15666
16326
|
axes: {
|
|
15667
16327
|
x: axes.x,
|
|
@@ -15690,6 +16350,196 @@ function compileChart(spec, options) {
|
|
|
15690
16350
|
measureText: options.measureText
|
|
15691
16351
|
};
|
|
15692
16352
|
}
|
|
16353
|
+
function compileFaceted(chartSpec, renderSpec, facetChannel, options, theme, strategy, watermark, resolvedAnimation, crosshair) {
|
|
16354
|
+
const measure = createMeasureFn(options.measureText);
|
|
16355
|
+
const panelEncoding = { ...renderSpec.encoding };
|
|
16356
|
+
delete panelEncoding.facet;
|
|
16357
|
+
const panelSpec = { ...renderSpec, encoding: panelEncoding };
|
|
16358
|
+
const panelChartSpec = { ...chartSpec, encoding: panelEncoding };
|
|
16359
|
+
const groups = groupByField(renderSpec.data, facetChannel.field);
|
|
16360
|
+
const facetValues = Array.from(groups.keys());
|
|
16361
|
+
if (facetChannel.sort !== null) {
|
|
16362
|
+
const dir = facetChannel.sort === "descending" ? -1 : 1;
|
|
16363
|
+
facetValues.sort((a, b) => dir * a.localeCompare(b));
|
|
16364
|
+
}
|
|
16365
|
+
const plan = resolveLayoutPlan(
|
|
16366
|
+
panelChartSpec,
|
|
16367
|
+
panelSpec,
|
|
16368
|
+
options,
|
|
16369
|
+
theme,
|
|
16370
|
+
strategy,
|
|
16371
|
+
watermark,
|
|
16372
|
+
measure
|
|
16373
|
+
);
|
|
16374
|
+
const legendStub = placeLegend(
|
|
16375
|
+
plan.legendContent,
|
|
16376
|
+
{ x: 0, y: 0, width: options.width, height: options.height },
|
|
16377
|
+
options.width,
|
|
16378
|
+
theme,
|
|
16379
|
+
plan.xAxisExtent
|
|
16380
|
+
);
|
|
16381
|
+
const dims = computeDimensions(
|
|
16382
|
+
panelChartSpec,
|
|
16383
|
+
options,
|
|
16384
|
+
legendStub,
|
|
16385
|
+
theme,
|
|
16386
|
+
strategy,
|
|
16387
|
+
watermark,
|
|
16388
|
+
plan
|
|
16389
|
+
);
|
|
16390
|
+
const chartArea = dims.chartArea;
|
|
16391
|
+
const finalLegend = placeLegend(
|
|
16392
|
+
plan.legendContent,
|
|
16393
|
+
chartArea,
|
|
16394
|
+
options.width,
|
|
16395
|
+
theme,
|
|
16396
|
+
plan.xAxisExtent
|
|
16397
|
+
);
|
|
16398
|
+
const leftReservation = plan.leftGutter;
|
|
16399
|
+
const bottomReservation = plan.xAxisExtent;
|
|
16400
|
+
const grid = computeFacetGrid(facetValues, facetChannel.columns, chartArea, {
|
|
16401
|
+
left: leftReservation,
|
|
16402
|
+
bottom: bottomReservation
|
|
16403
|
+
});
|
|
16404
|
+
const resolveConfig = chartSpec.resolve;
|
|
16405
|
+
const yResolve = resolveConfig?.scale?.y ?? "shared";
|
|
16406
|
+
const xResolve = resolveConfig?.scale?.x ?? "shared";
|
|
16407
|
+
const fullScales = computeScales(panelSpec, chartArea, renderSpec.data);
|
|
16408
|
+
const sharedXDomain = xResolve === "shared" && fullScales.x ? fullScales.x.scale.domain() : void 0;
|
|
16409
|
+
const sharedYDomain = yResolve === "shared" && fullScales.y ? fullScales.y.scale.domain() : void 0;
|
|
16410
|
+
const isRadial = chartSpec.markType === "arc";
|
|
16411
|
+
const rendererKey = resolveRendererKey(
|
|
16412
|
+
renderSpec.markType,
|
|
16413
|
+
panelEncoding,
|
|
16414
|
+
renderSpec.markDef
|
|
16415
|
+
);
|
|
16416
|
+
const renderer = getChartRenderer(rendererKey);
|
|
16417
|
+
const allMarks = [];
|
|
16418
|
+
const panelLayouts = [];
|
|
16419
|
+
for (const gridPanel of grid.panels) {
|
|
16420
|
+
const panelData = groups.get(gridPanel.key) ?? [];
|
|
16421
|
+
const panelSpecLocal = { ...panelSpec, data: panelData };
|
|
16422
|
+
let panelEnc = { ...panelEncoding };
|
|
16423
|
+
if (sharedXDomain && panelEnc.x) {
|
|
16424
|
+
const xCh = panelEnc.x;
|
|
16425
|
+
panelEnc = {
|
|
16426
|
+
...panelEnc,
|
|
16427
|
+
x: { ...xCh, scale: { ...xCh.scale ?? {}, domain: sharedXDomain } }
|
|
16428
|
+
};
|
|
16429
|
+
}
|
|
16430
|
+
if (sharedYDomain && panelEnc.y) {
|
|
16431
|
+
const yCh = panelEnc.y;
|
|
16432
|
+
panelEnc = {
|
|
16433
|
+
...panelEnc,
|
|
16434
|
+
y: { ...yCh, scale: { ...yCh.scale ?? {}, domain: sharedYDomain } }
|
|
16435
|
+
};
|
|
16436
|
+
}
|
|
16437
|
+
const panelSpecWithDomains = {
|
|
16438
|
+
...panelSpecLocal,
|
|
16439
|
+
encoding: panelEnc
|
|
16440
|
+
};
|
|
16441
|
+
const panelScales = computeScales(panelSpecWithDomains, gridPanel.area, panelData);
|
|
16442
|
+
applyColorScaleRange(panelScales, panelSpecWithDomains.encoding, theme, renderSpec.highlight);
|
|
16443
|
+
panelScales.defaultColor = chartSpec.markDef.fill ?? chartSpec.markDef.stroke ?? theme.colors.categorical[0];
|
|
16444
|
+
const isLeftCol = gridPanel.col === 0;
|
|
16445
|
+
const isBottomRow = gridPanel.row === grid.rows - 1;
|
|
16446
|
+
const panelAxes = isRadial ? { x: void 0, y: void 0 } : computeAxes(panelScales, gridPanel.area, strategy, theme, options.measureText, {
|
|
16447
|
+
data: panelData,
|
|
16448
|
+
encoding: panelSpecWithDomains.encoding,
|
|
16449
|
+
skipX: xResolve === "shared" ? !isBottomRow : false,
|
|
16450
|
+
skipY: yResolve === "shared" ? !isLeftCol : false,
|
|
16451
|
+
markType: chartSpec.markType,
|
|
16452
|
+
totalWidth: gridPanel.area.width
|
|
16453
|
+
});
|
|
16454
|
+
if (!isRadial) {
|
|
16455
|
+
computeGridlines(panelAxes, gridPanel.area);
|
|
16456
|
+
}
|
|
16457
|
+
const panelMarks = renderer ? renderer(
|
|
16458
|
+
panelSpecWithDomains,
|
|
16459
|
+
panelScales,
|
|
16460
|
+
gridPanel.area,
|
|
16461
|
+
strategy,
|
|
16462
|
+
theme,
|
|
16463
|
+
gridPanel.area.width
|
|
16464
|
+
) : [];
|
|
16465
|
+
const annotationMeasure = options.measureText ? (text, font) => options.measureText(text, font.fontSize, font.fontWeight).width : heuristicMeasure;
|
|
16466
|
+
const panelAnnotations = computeAnnotations(panelSpecWithDomains, {
|
|
16467
|
+
scales: panelScales,
|
|
16468
|
+
chartArea: gridPanel.area,
|
|
16469
|
+
strategy,
|
|
16470
|
+
isDark: theme.isDark,
|
|
16471
|
+
obstacles: [],
|
|
16472
|
+
svg: { width: options.width, height: options.height },
|
|
16473
|
+
measure: annotationMeasure,
|
|
16474
|
+
autoThin: panelSpecWithDomains.autoThin
|
|
16475
|
+
});
|
|
16476
|
+
allMarks.push(...panelMarks);
|
|
16477
|
+
panelLayouts.push({
|
|
16478
|
+
key: gridPanel.key,
|
|
16479
|
+
area: gridPanel.area,
|
|
16480
|
+
marks: panelMarks,
|
|
16481
|
+
axes: { x: panelAxes.x, y: panelAxes.y },
|
|
16482
|
+
annotations: panelAnnotations,
|
|
16483
|
+
header: {
|
|
16484
|
+
text: gridPanel.key,
|
|
16485
|
+
x: gridPanel.headerPos.x,
|
|
16486
|
+
y: gridPanel.headerPos.y,
|
|
16487
|
+
fontSize: theme.fonts.sizes.body,
|
|
16488
|
+
fontWeight: theme.fonts.weights.medium
|
|
16489
|
+
}
|
|
16490
|
+
});
|
|
16491
|
+
}
|
|
16492
|
+
const tooltipDescriptors = computeTooltipDescriptors(panelChartSpec, allMarks);
|
|
16493
|
+
const panelCount = facetValues.length;
|
|
16494
|
+
const baseAltText = generateAltText(
|
|
16495
|
+
{
|
|
16496
|
+
mark: chartSpec.markType,
|
|
16497
|
+
data: chartSpec.data,
|
|
16498
|
+
encoding: chartSpec.encoding,
|
|
16499
|
+
chrome: chartSpec.chrome
|
|
16500
|
+
},
|
|
16501
|
+
chartSpec.data
|
|
16502
|
+
);
|
|
16503
|
+
const altText = `${baseAltText} Faceted into ${panelCount} panels by ${facetChannel.field}.`;
|
|
16504
|
+
const dataTableFallback = generateDataTable(
|
|
16505
|
+
{
|
|
16506
|
+
mark: chartSpec.markType,
|
|
16507
|
+
data: chartSpec.data,
|
|
16508
|
+
encoding: chartSpec.encoding
|
|
16509
|
+
},
|
|
16510
|
+
chartSpec.data
|
|
16511
|
+
);
|
|
16512
|
+
assignAnimationIndices(allMarks, resolvedAnimation);
|
|
16513
|
+
return {
|
|
16514
|
+
area: chartArea,
|
|
16515
|
+
chrome: dims.chrome,
|
|
16516
|
+
metrics: dims.metrics,
|
|
16517
|
+
axes: { x: void 0, y: void 0 },
|
|
16518
|
+
marks: allMarks,
|
|
16519
|
+
annotations: [],
|
|
16520
|
+
legend: finalLegend,
|
|
16521
|
+
tooltipDescriptors,
|
|
16522
|
+
a11y: {
|
|
16523
|
+
altText,
|
|
16524
|
+
dataTableFallback,
|
|
16525
|
+
role: "img",
|
|
16526
|
+
keyboardNavigable: allMarks.length > 0
|
|
16527
|
+
},
|
|
16528
|
+
theme,
|
|
16529
|
+
dimensions: { width: options.width, height: options.height },
|
|
16530
|
+
animation: resolvedAnimation,
|
|
16531
|
+
watermark,
|
|
16532
|
+
display: chartSpec.display,
|
|
16533
|
+
crosshair,
|
|
16534
|
+
measureText: options.measureText,
|
|
16535
|
+
facet: {
|
|
16536
|
+
panels: panelLayouts,
|
|
16537
|
+
facetField: facetChannel.field,
|
|
16538
|
+
columns: grid.columns,
|
|
16539
|
+
sharedScales: yResolve === "shared" && xResolve === "shared"
|
|
16540
|
+
}
|
|
16541
|
+
};
|
|
16542
|
+
}
|
|
15693
16543
|
function compileLayer2(spec, options) {
|
|
15694
16544
|
return compileLayer(spec, options, compileChart);
|
|
15695
16545
|
}
|