@opendata-ai/openchart-engine 8.0.0-rc.2 → 8.0.0-rc.3
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 +2 -1
- package/dist/index.js +374 -25
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/compile/size-scale.ts +2 -0
- package/src/compiler/normalize.ts +1 -0
- package/src/compiler/validate.ts +73 -12
- package/src/geo/__tests__/compile-map.test.ts +211 -0
- package/src/geo/compile-map.ts +402 -15
- package/src/geo/types.ts +2 -0
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _opendata_ai_openchart_core from '@opendata-ai/openchart-core';
|
|
2
|
-
import { LegendLayout, ResolvedChrome, TooltipContent, A11yMetadata, ResolvedTheme, CompileOptions, ChartLayout, LayerSpec, CompileTableOptions, TableLayout, ResolvedAnimationPhase, AnimationSpec, ResolvedAnimation, Point, DataRow, BarListEncoding, ThemeConfig, DarkMode, MapGeo, MapEncoding, LegendConfig, TileMapEncoding, TileMapPalette, MarkType, MarkDef, Encoding, ChromeText, Annotation, LabelConfig, Display, ColumnConfig, GraphSpec, GraphEncoding, GraphLayoutConfig, NodeOverride, SankeyEncoding, SankeyNodeAlign, SankeyLinkColor, ScaleType, EncodingChannel, FieldFormatContext, Rect, LayoutStrategy, VizSpec, Mark, BinTransform, CalculateTransform, ConditionalValueDef, FilterPredicate, TimeUnitTransform, Transform } from '@opendata-ai/openchart-core';
|
|
2
|
+
import { LegendLayout, ResolvedChrome, TooltipContent, A11yMetadata, ResolvedTheme, CompileOptions, ChartLayout, LayerSpec, CompileTableOptions, TableLayout, ResolvedAnimationPhase, AnimationSpec, ResolvedAnimation, Point, DataRow, BarListEncoding, ThemeConfig, DarkMode, MapGeo, MapEncoding, LegendConfig, MapPointsLayer, TileMapEncoding, TileMapPalette, MarkType, MarkDef, Encoding, ChromeText, Annotation, LabelConfig, Display, ColumnConfig, GraphSpec, GraphEncoding, GraphLayoutConfig, NodeOverride, SankeyEncoding, SankeyNodeAlign, SankeyLinkColor, ScaleType, EncodingChannel, FieldFormatContext, Rect, LayoutStrategy, VizSpec, Mark, BinTransform, CalculateTransform, ConditionalValueDef, FilterPredicate, TimeUnitTransform, Transform } from '@opendata-ai/openchart-core';
|
|
3
3
|
export { BarListLayout, BarListSpec, ChartLayout, ChartSpec, CompileOptions, CompileTableOptions, GraphLayout, GraphSpec, LayerSpec, MapLayout, MapSpec, SankeyLayout, SankeySpec, TableLayout, TableSpec, TileMapLayout, TileMapSpec, VizSpec } from '@opendata-ai/openchart-core';
|
|
4
4
|
import { ScaleLinear, ScaleTime, ScaleLogarithmic, ScalePower, ScaleSymLog, ScaleBand, ScalePoint, ScaleOrdinal, ScaleQuantile, ScaleQuantize, ScaleThreshold } from 'd3-scale';
|
|
5
5
|
|
|
@@ -301,6 +301,7 @@ interface NormalizedMapSpec {
|
|
|
301
301
|
watermark: boolean;
|
|
302
302
|
animation?: AnimationSpec;
|
|
303
303
|
valueFormat?: string;
|
|
304
|
+
points?: MapPointsLayer;
|
|
304
305
|
}
|
|
305
306
|
|
|
306
307
|
/**
|
package/dist/index.js
CHANGED
|
@@ -5545,7 +5545,9 @@ var SIZE_SCALE_DEFAULTS = {
|
|
|
5545
5545
|
* Text glyphs. Linear, not sqrt -- a word reads by its height, which is
|
|
5546
5546
|
* linear in font size, where a disc reads by its area.
|
|
5547
5547
|
*/
|
|
5548
|
-
text: { curve: "linear", range: [11, 32] }
|
|
5548
|
+
text: { curve: "linear", range: [11, 32] },
|
|
5549
|
+
/** Map point dots. sqrt for area-proportional, tighter range than scatter. */
|
|
5550
|
+
mapPoint: { curve: "sqrt", range: [3, 20] }
|
|
5549
5551
|
};
|
|
5550
5552
|
function sizeScaleDefaultsFor(markType) {
|
|
5551
5553
|
const found = SIZE_SCALE_DEFAULTS[markType];
|
|
@@ -10631,7 +10633,8 @@ function normalizeMapSpec(spec, _warnings) {
|
|
|
10631
10633
|
darkMode: spec.darkMode ?? "off",
|
|
10632
10634
|
watermark: spec.watermark ?? true,
|
|
10633
10635
|
animation: spec.animation,
|
|
10634
|
-
valueFormat: spec.valueFormat
|
|
10636
|
+
valueFormat: spec.valueFormat,
|
|
10637
|
+
points: spec.points
|
|
10635
10638
|
};
|
|
10636
10639
|
}
|
|
10637
10640
|
function normalizeBarListSpec(spec, _warnings) {
|
|
@@ -11802,25 +11805,76 @@ function validateMapSpec(spec, errors) {
|
|
|
11802
11805
|
}
|
|
11803
11806
|
}
|
|
11804
11807
|
const encoding = spec.encoding;
|
|
11805
|
-
|
|
11806
|
-
|
|
11807
|
-
|
|
11808
|
+
const hasPoints = spec.points && typeof spec.points === "object" && !Array.isArray(spec.points);
|
|
11809
|
+
const keyCh = encoding.key;
|
|
11810
|
+
if (!keyCh || typeof keyCh !== "object") {
|
|
11811
|
+
errors.push({
|
|
11812
|
+
message: 'Spec error: map encoding requires "key" channel',
|
|
11813
|
+
path: "encoding.key",
|
|
11814
|
+
code: "MISSING_FIELD",
|
|
11815
|
+
suggestion: 'Add encoding.key, e.g. key: { field: "id", type: "nominal" }'
|
|
11816
|
+
});
|
|
11817
|
+
} else if (!keyCh.field || typeof keyCh.field !== "string") {
|
|
11818
|
+
errors.push({
|
|
11819
|
+
message: 'Spec error: encoding.key must have a "field" string',
|
|
11820
|
+
path: "encoding.key.field",
|
|
11821
|
+
code: "MISSING_FIELD",
|
|
11822
|
+
suggestion: 'Add a field name, e.g. key: { field: "id" }'
|
|
11823
|
+
});
|
|
11824
|
+
}
|
|
11825
|
+
const colorCh = encoding.color;
|
|
11826
|
+
if (!hasPoints) {
|
|
11827
|
+
if (!colorCh || typeof colorCh !== "object") {
|
|
11808
11828
|
errors.push({
|
|
11809
|
-
message:
|
|
11810
|
-
path:
|
|
11829
|
+
message: 'Spec error: map encoding requires "color" channel (or add a "points" layer for basemap-only mode)',
|
|
11830
|
+
path: "encoding.color",
|
|
11811
11831
|
code: "MISSING_FIELD",
|
|
11812
|
-
suggestion:
|
|
11832
|
+
suggestion: 'Add encoding.color, e.g. color: { field: "value", type: "quantitative" }'
|
|
11813
11833
|
});
|
|
11814
|
-
|
|
11815
|
-
}
|
|
11816
|
-
if (!ch.field || typeof ch.field !== "string") {
|
|
11834
|
+
} else if (!colorCh.field || typeof colorCh.field !== "string") {
|
|
11817
11835
|
errors.push({
|
|
11818
|
-
message:
|
|
11819
|
-
path:
|
|
11836
|
+
message: 'Spec error: encoding.color must have a "field" string',
|
|
11837
|
+
path: "encoding.color.field",
|
|
11820
11838
|
code: "MISSING_FIELD",
|
|
11821
|
-
suggestion:
|
|
11839
|
+
suggestion: 'Add a field name, e.g. color: { field: "value" }'
|
|
11822
11840
|
});
|
|
11823
11841
|
}
|
|
11842
|
+
} else if (colorCh && typeof colorCh === "object" && (!colorCh.field || typeof colorCh.field !== "string")) {
|
|
11843
|
+
errors.push({
|
|
11844
|
+
message: 'Spec error: encoding.color must have a "field" string when provided',
|
|
11845
|
+
path: "encoding.color.field",
|
|
11846
|
+
code: "MISSING_FIELD",
|
|
11847
|
+
suggestion: 'Add a field name, e.g. color: { field: "value" }'
|
|
11848
|
+
});
|
|
11849
|
+
}
|
|
11850
|
+
if (hasPoints) {
|
|
11851
|
+
const points = spec.points;
|
|
11852
|
+
if (!Array.isArray(points.data)) {
|
|
11853
|
+
errors.push({
|
|
11854
|
+
message: "Spec error: points.data must be an array of data rows",
|
|
11855
|
+
path: "points.data",
|
|
11856
|
+
code: "INVALID_TYPE",
|
|
11857
|
+
suggestion: "Provide data as an array of objects with lat/lng fields"
|
|
11858
|
+
});
|
|
11859
|
+
}
|
|
11860
|
+
for (const ch of ["longitude", "latitude"]) {
|
|
11861
|
+
const enc = points[ch];
|
|
11862
|
+
if (!enc || typeof enc !== "object") {
|
|
11863
|
+
errors.push({
|
|
11864
|
+
message: `Spec error: points requires "${ch}" encoding channel`,
|
|
11865
|
+
path: `points.${ch}`,
|
|
11866
|
+
code: "MISSING_FIELD",
|
|
11867
|
+
suggestion: `Add points.${ch}, e.g. ${ch}: { field: "${ch === "longitude" ? "lon" : "lat"}", type: "quantitative" }`
|
|
11868
|
+
});
|
|
11869
|
+
} else if (!enc.field || typeof enc.field !== "string") {
|
|
11870
|
+
errors.push({
|
|
11871
|
+
message: `Spec error: points.${ch} must have a "field" string`,
|
|
11872
|
+
path: `points.${ch}.field`,
|
|
11873
|
+
code: "MISSING_FIELD",
|
|
11874
|
+
suggestion: `Add a field name, e.g. ${ch}: { field: "${ch === "longitude" ? "lon" : "lat"}" }`
|
|
11875
|
+
});
|
|
11876
|
+
}
|
|
11877
|
+
}
|
|
11824
11878
|
}
|
|
11825
11879
|
}
|
|
11826
11880
|
function validateBarListSpec(spec, errors) {
|
|
@@ -17767,11 +17821,11 @@ function compileMap(spec, options) {
|
|
|
17767
17821
|
const compileWarnings = [];
|
|
17768
17822
|
const showLegend = mapSpec.legend?.show !== false;
|
|
17769
17823
|
const colorEncoding = mapSpec.encoding.color;
|
|
17770
|
-
const isQuantitative = colorEncoding
|
|
17824
|
+
const isQuantitative = colorEncoding?.type === "quantitative";
|
|
17771
17825
|
const legendPosition = mapSpec.legend?.position === "bottom" ? "bottom" : "top";
|
|
17772
17826
|
let legendBlockHeight = 0;
|
|
17773
17827
|
let continuousContent = null;
|
|
17774
|
-
if (showLegend && isQuantitative) {
|
|
17828
|
+
if (showLegend && colorEncoding && isQuantitative) {
|
|
17775
17829
|
const colorValues = [];
|
|
17776
17830
|
for (const row of mapSpec.data) {
|
|
17777
17831
|
const v = row[colorEncoding.field];
|
|
@@ -17794,10 +17848,19 @@ function compileMap(spec, options) {
|
|
|
17794
17848
|
const labelRowHeight = Math.ceil(theme.fonts.sizes.small * 1.3);
|
|
17795
17849
|
legendBlockHeight = continuousContent.barHeight + CONTINUOUS_LABEL_GAP + labelRowHeight;
|
|
17796
17850
|
}
|
|
17797
|
-
} else if (showLegend) {
|
|
17851
|
+
} else if (showLegend && colorEncoding) {
|
|
17798
17852
|
const labelHeight = Math.ceil(theme.fonts.sizes.small * 1.3);
|
|
17799
17853
|
legendBlockHeight = Math.max(10, labelHeight) + 6;
|
|
17800
17854
|
}
|
|
17855
|
+
if (showLegend && mapSpec.points?.color) {
|
|
17856
|
+
if (mapSpec.points.color.type === "quantitative") {
|
|
17857
|
+
const labelRowHeight = Math.ceil(theme.fonts.sizes.small * 1.3);
|
|
17858
|
+
legendBlockHeight += CONTINUOUS_BAR_HEIGHT + CONTINUOUS_LABEL_GAP + labelRowHeight + 8;
|
|
17859
|
+
} else {
|
|
17860
|
+
const ptLabelHeight = Math.ceil(theme.fonts.sizes.small * 1.3);
|
|
17861
|
+
legendBlockHeight += Math.max(10, ptLabelHeight) + 6 + 8;
|
|
17862
|
+
}
|
|
17863
|
+
}
|
|
17801
17864
|
const legendReserveGap = legendBlockHeight > 0 ? 8 : 0;
|
|
17802
17865
|
const mapAreaHeight = fullArea.height - legendBlockHeight - legendReserveGap;
|
|
17803
17866
|
if (mapAreaHeight <= 0) {
|
|
@@ -17829,7 +17892,8 @@ function compileMap(spec, options) {
|
|
|
17829
17892
|
mapSpec.encoding.key.field,
|
|
17830
17893
|
mapSpec.geo.idField
|
|
17831
17894
|
);
|
|
17832
|
-
|
|
17895
|
+
const filteredJoinWarnings = mapSpec.data.length === 0 && mapSpec.points ? joinWarnings.filter((w) => w.code !== "UNMATCHED_FEATURES") : joinWarnings;
|
|
17896
|
+
compileWarnings.push(...filteredJoinWarnings);
|
|
17833
17897
|
const droppedFeatures = [];
|
|
17834
17898
|
const formatter = buildD3Formatter2(mapSpec.valueFormat) ?? formatNumber3;
|
|
17835
17899
|
const neutralFill = isDarkMode ? "#2a2a2a" : "#e8e8e8";
|
|
@@ -17837,13 +17901,21 @@ function compileMap(spec, options) {
|
|
|
17837
17901
|
let featureMarks;
|
|
17838
17902
|
let continuousLegend = null;
|
|
17839
17903
|
let categoricalLegend = null;
|
|
17840
|
-
if (
|
|
17904
|
+
if (!colorEncoding) {
|
|
17905
|
+
featureMarks = buildBasemapMarks({
|
|
17906
|
+
geoFeatures,
|
|
17907
|
+
pathGen,
|
|
17908
|
+
neutralFill,
|
|
17909
|
+
neutralStroke,
|
|
17910
|
+
droppedFeatures
|
|
17911
|
+
});
|
|
17912
|
+
} else if (isQuantitative) {
|
|
17841
17913
|
const result = buildQuantitativeMarks({
|
|
17842
17914
|
geoFeatures,
|
|
17843
17915
|
pathGen,
|
|
17844
17916
|
joined,
|
|
17845
17917
|
colorField: colorEncoding.field,
|
|
17846
|
-
palette:
|
|
17918
|
+
palette: colorEncoding.scale?.scheme ?? "blue",
|
|
17847
17919
|
isDarkMode: !!isDarkMode,
|
|
17848
17920
|
neutralFill,
|
|
17849
17921
|
neutralStroke,
|
|
@@ -17928,6 +18000,187 @@ function compileMap(spec, options) {
|
|
|
17928
18000
|
const interiorStroke = isDarkMode ? "rgba(255,255,255,0.3)" : "rgba(0,0,0,0.15)";
|
|
17929
18001
|
const outlineStroke = isDarkMode ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.3)";
|
|
17930
18002
|
const borders = { interiorPath, outlinePath, interiorStroke, outlineStroke };
|
|
18003
|
+
const pointMarks = [];
|
|
18004
|
+
let pointCategoricalLegend = null;
|
|
18005
|
+
let pointContinuousLegend = null;
|
|
18006
|
+
if (mapSpec.points) {
|
|
18007
|
+
const pts = mapSpec.points;
|
|
18008
|
+
const lonField = pts.longitude.field;
|
|
18009
|
+
const latField = pts.latitude.field;
|
|
18010
|
+
const keyChannel = pts.key;
|
|
18011
|
+
const sizeScale = buildSizeScale(pts.size, pts.data, SIZE_SCALE_DEFAULTS.mapPoint);
|
|
18012
|
+
let pointColorScale = null;
|
|
18013
|
+
const pointCategories = [];
|
|
18014
|
+
const pointCategoryColors = /* @__PURE__ */ new Map();
|
|
18015
|
+
if (pts.color) {
|
|
18016
|
+
if (pts.color.type === "quantitative") {
|
|
18017
|
+
const colorValues = [];
|
|
18018
|
+
for (const row of pts.data) {
|
|
18019
|
+
const raw = row[pts.color.field];
|
|
18020
|
+
if (raw != null) {
|
|
18021
|
+
const v = Number(raw);
|
|
18022
|
+
if (Number.isFinite(v)) colorValues.push(v);
|
|
18023
|
+
}
|
|
18024
|
+
}
|
|
18025
|
+
if (colorValues.length > 0) {
|
|
18026
|
+
const palette = pts.color.scale?.scheme ?? "blue";
|
|
18027
|
+
const paletteStops = [...SEQUENTIAL_PALETTES[palette] ?? SEQUENTIAL_PALETTES.blue];
|
|
18028
|
+
const qScale = quantile2().domain(colorValues).range(paletteStops);
|
|
18029
|
+
pointColorScale = (val) => {
|
|
18030
|
+
const v = Number(val);
|
|
18031
|
+
return Number.isFinite(v) ? qScale(v) : CATEGORICAL_PALETTE[0];
|
|
18032
|
+
};
|
|
18033
|
+
}
|
|
18034
|
+
} else {
|
|
18035
|
+
const seen = /* @__PURE__ */ new Set();
|
|
18036
|
+
for (const row of pts.data) {
|
|
18037
|
+
const raw = row[pts.color.field];
|
|
18038
|
+
if (raw != null) {
|
|
18039
|
+
const cat = String(raw);
|
|
18040
|
+
if (!seen.has(cat)) {
|
|
18041
|
+
seen.add(cat);
|
|
18042
|
+
pointCategories.push(cat);
|
|
18043
|
+
pointCategoryColors.set(
|
|
18044
|
+
cat,
|
|
18045
|
+
CATEGORICAL_PALETTE[(pointCategories.length - 1) % CATEGORICAL_PALETTE.length]
|
|
18046
|
+
);
|
|
18047
|
+
}
|
|
18048
|
+
}
|
|
18049
|
+
}
|
|
18050
|
+
pointColorScale = (val) => {
|
|
18051
|
+
if (val == null) return CATEGORICAL_PALETTE[0];
|
|
18052
|
+
return pointCategoryColors.get(String(val)) ?? CATEGORICAL_PALETTE[0];
|
|
18053
|
+
};
|
|
18054
|
+
}
|
|
18055
|
+
}
|
|
18056
|
+
const defaultFill = CATEGORICAL_PALETTE[0];
|
|
18057
|
+
let animIndex = 0;
|
|
18058
|
+
for (let i = 0; i < pts.data.length; i++) {
|
|
18059
|
+
const row = pts.data[i];
|
|
18060
|
+
const lon = Number(row[lonField]);
|
|
18061
|
+
const lat = Number(row[latField]);
|
|
18062
|
+
if (!Number.isFinite(lon) || !Number.isFinite(lat)) continue;
|
|
18063
|
+
const projected = projection2([lon, lat]);
|
|
18064
|
+
if (!projected) {
|
|
18065
|
+
compileWarnings.push({
|
|
18066
|
+
code: "POINT_NULL_PROJECTION",
|
|
18067
|
+
message: `Point at [${lon}, ${lat}] projects to null and was dropped.`,
|
|
18068
|
+
context: { lon, lat }
|
|
18069
|
+
});
|
|
18070
|
+
continue;
|
|
18071
|
+
}
|
|
18072
|
+
const r = sizeScale ? sizeScale.scale(Number(row[sizeScale.field])) : 5;
|
|
18073
|
+
const fill = pointColorScale && pts.color ? pointColorScale(row[pts.color.field]) : defaultFill;
|
|
18074
|
+
const key = keyChannel ? String(row[keyChannel.field] ?? i) : String(i);
|
|
18075
|
+
const stroke = isDarkMode ? "rgba(0,0,0,0.5)" : "rgba(255,255,255,0.8)";
|
|
18076
|
+
pointMarks.push({
|
|
18077
|
+
type: "map-point",
|
|
18078
|
+
cx: projected[0],
|
|
18079
|
+
cy: projected[1],
|
|
18080
|
+
r,
|
|
18081
|
+
fill,
|
|
18082
|
+
stroke,
|
|
18083
|
+
strokeWidth: 1,
|
|
18084
|
+
key,
|
|
18085
|
+
data: row,
|
|
18086
|
+
aria: { role: "img", label: key },
|
|
18087
|
+
animationIndex: animIndex++
|
|
18088
|
+
});
|
|
18089
|
+
}
|
|
18090
|
+
if (pts.color && pts.color.type !== "quantitative" && pointCategories.length > 0 && showLegend) {
|
|
18091
|
+
const labelStyle = {
|
|
18092
|
+
fontFamily: theme.fonts.family,
|
|
18093
|
+
fontSize: theme.fonts.sizes.small,
|
|
18094
|
+
fontWeight: theme.fonts.weights.normal,
|
|
18095
|
+
fill: theme.colors.text,
|
|
18096
|
+
lineHeight: 1.2
|
|
18097
|
+
};
|
|
18098
|
+
const entries = pointCategories.map((cat) => ({
|
|
18099
|
+
label: cat,
|
|
18100
|
+
color: pointCategoryColors.get(cat),
|
|
18101
|
+
shape: "circle"
|
|
18102
|
+
}));
|
|
18103
|
+
const choroplethLegendHeight = continuousLegend ? continuousLegend.bounds.height : categoricalLegend ? categoricalLegend.bounds.height : 0;
|
|
18104
|
+
const choroplethLegendGap = choroplethLegendHeight > 0 ? 8 : 0;
|
|
18105
|
+
const legendX = fullArea.x;
|
|
18106
|
+
const legendY = fullArea.y + mapAreaHeight + legendReserveGap + choroplethLegendHeight + choroplethLegendGap;
|
|
18107
|
+
const legendWidth = fullArea.width;
|
|
18108
|
+
const swatchSize = 10;
|
|
18109
|
+
const swatchGap = 6;
|
|
18110
|
+
const entryGap = 16;
|
|
18111
|
+
pointCategoricalLegend = {
|
|
18112
|
+
type: "categorical",
|
|
18113
|
+
position: "bottom",
|
|
18114
|
+
bounds: { x: legendX, y: legendY, width: legendWidth, height: swatchSize + 6 },
|
|
18115
|
+
labelStyle,
|
|
18116
|
+
entries,
|
|
18117
|
+
swatchSize,
|
|
18118
|
+
swatchGap,
|
|
18119
|
+
entryGap,
|
|
18120
|
+
swatchChipFill: isDarkMode ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.04)"
|
|
18121
|
+
};
|
|
18122
|
+
}
|
|
18123
|
+
if (pts.color && pts.color.type === "quantitative" && showLegend) {
|
|
18124
|
+
const ptColorValues = [];
|
|
18125
|
+
for (const row of pts.data) {
|
|
18126
|
+
const v = row[pts.color.field];
|
|
18127
|
+
if (v != null) {
|
|
18128
|
+
const n = Number(v);
|
|
18129
|
+
if (Number.isFinite(n)) ptColorValues.push(n);
|
|
18130
|
+
}
|
|
18131
|
+
}
|
|
18132
|
+
const ptLegendChannel = {
|
|
18133
|
+
...pts.color,
|
|
18134
|
+
scale: { ...pts.color.scale, type: "quantile" }
|
|
18135
|
+
};
|
|
18136
|
+
const ptContinuousContent = computeContinuousLegendContentForChannel(
|
|
18137
|
+
ptColorValues,
|
|
18138
|
+
ptLegendChannel,
|
|
18139
|
+
theme,
|
|
18140
|
+
fullArea.width
|
|
18141
|
+
);
|
|
18142
|
+
if (ptContinuousContent) {
|
|
18143
|
+
const labelStyle = {
|
|
18144
|
+
fontFamily: theme.fonts.family,
|
|
18145
|
+
fontSize: theme.fonts.sizes.small,
|
|
18146
|
+
fontWeight: theme.fonts.weights.normal,
|
|
18147
|
+
fill: theme.colors.text,
|
|
18148
|
+
lineHeight: 1.3,
|
|
18149
|
+
fontVariant: "tabular-nums"
|
|
18150
|
+
};
|
|
18151
|
+
const choroplethLegendHeight = continuousLegend ? continuousLegend.bounds.height : categoricalLegend ? categoricalLegend.bounds.height : 0;
|
|
18152
|
+
const choroplethLegendGap = choroplethLegendHeight > 0 ? 8 : 0;
|
|
18153
|
+
const legendX = fullArea.x;
|
|
18154
|
+
const legendY = fullArea.y + mapAreaHeight + legendReserveGap + choroplethLegendHeight + choroplethLegendGap;
|
|
18155
|
+
const ptLabelRowHeight = Math.ceil(theme.fonts.sizes.small * 1.3);
|
|
18156
|
+
const ptLegendHeight = ptContinuousContent.barHeight + CONTINUOUS_LABEL_GAP + ptLabelRowHeight;
|
|
18157
|
+
const bounds = {
|
|
18158
|
+
x: legendX,
|
|
18159
|
+
y: legendY,
|
|
18160
|
+
width: ptContinuousContent.barWidth,
|
|
18161
|
+
height: ptLegendHeight
|
|
18162
|
+
};
|
|
18163
|
+
const bar = {
|
|
18164
|
+
x: bounds.x,
|
|
18165
|
+
y: bounds.y,
|
|
18166
|
+
width: ptContinuousContent.barWidth,
|
|
18167
|
+
height: ptContinuousContent.barHeight
|
|
18168
|
+
};
|
|
18169
|
+
pointContinuousLegend = {
|
|
18170
|
+
type: "continuous",
|
|
18171
|
+
mode: ptContinuousContent.mode,
|
|
18172
|
+
position: "bottom",
|
|
18173
|
+
bounds,
|
|
18174
|
+
labelStyle,
|
|
18175
|
+
bar,
|
|
18176
|
+
colorStops: ptContinuousContent.colorStops,
|
|
18177
|
+
bins: ptContinuousContent.bins.map((b) => ({ ...b, x: b.x + bar.x })),
|
|
18178
|
+
ticks: ptContinuousContent.ticks.map((t) => ({ ...t, x: t.x + bar.x })),
|
|
18179
|
+
labelY: bar.y + bar.height + CONTINUOUS_LABEL_GAP + labelStyle.fontSize
|
|
18180
|
+
};
|
|
18181
|
+
}
|
|
18182
|
+
}
|
|
18183
|
+
}
|
|
17931
18184
|
const tooltipChannels = mapSpec.encoding.tooltip ? Array.isArray(mapSpec.encoding.tooltip) ? mapSpec.encoding.tooltip : [mapSpec.encoding.tooltip] : null;
|
|
17932
18185
|
const tooltipDescriptors = /* @__PURE__ */ new Map();
|
|
17933
18186
|
for (const mark of featureMarks) {
|
|
@@ -17945,12 +18198,12 @@ function compileMap(spec, options) {
|
|
|
17945
18198
|
});
|
|
17946
18199
|
}
|
|
17947
18200
|
}
|
|
17948
|
-
} else if (isQuantitative && mark.data[colorEncoding.field] != null) {
|
|
18201
|
+
} else if (colorEncoding && isQuantitative && mark.data[colorEncoding.field] != null) {
|
|
17949
18202
|
fields.push({
|
|
17950
18203
|
label: colorEncoding.title ?? colorEncoding.field,
|
|
17951
18204
|
value: formatter(Number(mark.data[colorEncoding.field]))
|
|
17952
18205
|
});
|
|
17953
|
-
} else if (!isQuantitative && mark.data[colorEncoding.field] != null) {
|
|
18206
|
+
} else if (colorEncoding && !isQuantitative && mark.data[colorEncoding.field] != null) {
|
|
17954
18207
|
fields.push({
|
|
17955
18208
|
label: colorEncoding.title ?? colorEncoding.field,
|
|
17956
18209
|
value: String(mark.data[colorEncoding.field])
|
|
@@ -17958,11 +18211,61 @@ function compileMap(spec, options) {
|
|
|
17958
18211
|
}
|
|
17959
18212
|
tooltipDescriptors.set(String(mark.id), { fields });
|
|
17960
18213
|
}
|
|
18214
|
+
if (mapSpec.points && pointMarks.length > 0) {
|
|
18215
|
+
const ptTooltipChannels = mapSpec.points.tooltip ? Array.isArray(mapSpec.points.tooltip) ? mapSpec.points.tooltip : [mapSpec.points.tooltip] : null;
|
|
18216
|
+
for (const pm of pointMarks) {
|
|
18217
|
+
const fields = [];
|
|
18218
|
+
if (ptTooltipChannels) {
|
|
18219
|
+
for (const ch of ptTooltipChannels) {
|
|
18220
|
+
const val = pm.data[ch.field];
|
|
18221
|
+
if (val != null) {
|
|
18222
|
+
fields.push({
|
|
18223
|
+
label: ch.title ?? ch.field,
|
|
18224
|
+
value: ch.type === "quantitative" ? formatter(Number(val)) : String(val)
|
|
18225
|
+
});
|
|
18226
|
+
}
|
|
18227
|
+
}
|
|
18228
|
+
} else {
|
|
18229
|
+
const pts = mapSpec.points;
|
|
18230
|
+
if (pts.key) {
|
|
18231
|
+
const val = pm.data[pts.key.field];
|
|
18232
|
+
if (val != null)
|
|
18233
|
+
fields.push({ label: pts.key.title ?? pts.key.field, value: String(val) });
|
|
18234
|
+
}
|
|
18235
|
+
if (pts.color) {
|
|
18236
|
+
const val = pm.data[pts.color.field];
|
|
18237
|
+
if (val != null)
|
|
18238
|
+
fields.push({
|
|
18239
|
+
label: pts.color.title ?? pts.color.field,
|
|
18240
|
+
value: pts.color.type === "quantitative" ? formatter(Number(val)) : String(val)
|
|
18241
|
+
});
|
|
18242
|
+
}
|
|
18243
|
+
if (pts.size) {
|
|
18244
|
+
const val = pm.data[pts.size.field];
|
|
18245
|
+
if (val != null)
|
|
18246
|
+
fields.push({ label: pts.size.title ?? pts.size.field, value: formatter(Number(val)) });
|
|
18247
|
+
}
|
|
18248
|
+
}
|
|
18249
|
+
if (fields.length > 0) {
|
|
18250
|
+
tooltipDescriptors.set(`point:${pm.key}`, { fields });
|
|
18251
|
+
}
|
|
18252
|
+
}
|
|
18253
|
+
}
|
|
18254
|
+
const a11yDataField = colorEncoding?.field;
|
|
17961
18255
|
const a11y = {
|
|
17962
|
-
altText: `Map showing ${geoFeatures.length} regions with ${isQuantitative ? "quantitative" : "categorical"} data`,
|
|
17963
|
-
dataTableFallback:
|
|
18256
|
+
altText: pointMarks.length > 0 ? colorEncoding ? `Map showing ${geoFeatures.length} regions with ${isQuantitative ? "quantitative" : "categorical"} data and ${pointMarks.length} point markers` : `Map showing ${geoFeatures.length} regions with ${pointMarks.length} point markers` : colorEncoding ? `Map showing ${geoFeatures.length} regions with ${isQuantitative ? "quantitative" : "categorical"} data` : `Map showing ${geoFeatures.length} regions`,
|
|
18257
|
+
dataTableFallback: [
|
|
18258
|
+
...featureMarks.filter((m) => m.data).map((m) => [
|
|
18259
|
+
m.name ?? String(m.id),
|
|
18260
|
+
a11yDataField ? String(m.data?.[a11yDataField] ?? "") : ""
|
|
18261
|
+
]),
|
|
18262
|
+
...pointMarks.map((pm) => [
|
|
18263
|
+
pm.key,
|
|
18264
|
+
mapSpec.points?.color ? String(pm.data[mapSpec.points.color.field] ?? "") : ""
|
|
18265
|
+
])
|
|
18266
|
+
],
|
|
17964
18267
|
role: "img",
|
|
17965
|
-
keyboardNavigable: featureMarks.length > 0
|
|
18268
|
+
keyboardNavigable: featureMarks.length > 0 || pointMarks.length > 0
|
|
17966
18269
|
};
|
|
17967
18270
|
const resolvedFocus = resolveFocus(mapSpec.geo.focus, featureMarks, compileWarnings);
|
|
17968
18271
|
const resolvedAnimation = resolveAnimation(mapSpec.animation);
|
|
@@ -17987,6 +18290,9 @@ function compileMap(spec, options) {
|
|
|
17987
18290
|
borders,
|
|
17988
18291
|
continuousLegend,
|
|
17989
18292
|
categoricalLegend,
|
|
18293
|
+
pointMarks,
|
|
18294
|
+
pointCategoricalLegend,
|
|
18295
|
+
pointContinuousLegend,
|
|
17990
18296
|
tooltipDescriptors,
|
|
17991
18297
|
a11y,
|
|
17992
18298
|
theme,
|
|
@@ -18000,6 +18306,46 @@ function compileMap(spec, options) {
|
|
|
18000
18306
|
focus: resolvedFocus
|
|
18001
18307
|
};
|
|
18002
18308
|
}
|
|
18309
|
+
function buildBasemapMarks(opts) {
|
|
18310
|
+
const { geoFeatures, pathGen, neutralFill, neutralStroke, droppedFeatures } = opts;
|
|
18311
|
+
const marks = [];
|
|
18312
|
+
let animIndex = 0;
|
|
18313
|
+
for (const feat of geoFeatures) {
|
|
18314
|
+
const pathD = pathGen(feat);
|
|
18315
|
+
if (!pathD) {
|
|
18316
|
+
const name2 = feat.properties?.name ?? String(feat.id);
|
|
18317
|
+
droppedFeatures.push(name2);
|
|
18318
|
+
continue;
|
|
18319
|
+
}
|
|
18320
|
+
const featureId = feat.id;
|
|
18321
|
+
const props = feat.properties;
|
|
18322
|
+
const name = props?.name ?? props?.NAME;
|
|
18323
|
+
const b = pathGen.bounds(feat);
|
|
18324
|
+
const bx = Number.isFinite(b[0][0]) ? b[0][0] : 0;
|
|
18325
|
+
const by = Number.isFinite(b[0][1]) ? b[0][1] : 0;
|
|
18326
|
+
const bw = Number.isFinite(b[1][0]) ? b[1][0] - bx : 0;
|
|
18327
|
+
const bh = Number.isFinite(b[1][1]) ? b[1][1] - by : 0;
|
|
18328
|
+
const bounds = { x: bx, y: by, width: bw, height: bh };
|
|
18329
|
+
const rawCentroid = pathGen.centroid(feat);
|
|
18330
|
+
const cx = Number.isFinite(rawCentroid[0]) ? rawCentroid[0] : bx + bw / 2;
|
|
18331
|
+
const cy = Number.isFinite(rawCentroid[1]) ? rawCentroid[1] : by + bh / 2;
|
|
18332
|
+
marks.push({
|
|
18333
|
+
type: "map-feature",
|
|
18334
|
+
path: pathD,
|
|
18335
|
+
fill: neutralFill,
|
|
18336
|
+
stroke: neutralStroke,
|
|
18337
|
+
strokeWidth: 0.5,
|
|
18338
|
+
id: featureId,
|
|
18339
|
+
name,
|
|
18340
|
+
data: null,
|
|
18341
|
+
aria: { role: "img", label: name ?? String(featureId) },
|
|
18342
|
+
animationIndex: animIndex++,
|
|
18343
|
+
bounds,
|
|
18344
|
+
centroid: [cx, cy]
|
|
18345
|
+
});
|
|
18346
|
+
}
|
|
18347
|
+
return marks;
|
|
18348
|
+
}
|
|
18003
18349
|
function buildQuantitativeMarks(opts) {
|
|
18004
18350
|
const {
|
|
18005
18351
|
geoFeatures,
|
|
@@ -18256,6 +18602,9 @@ function emptyLayout3(chrome, theme, options, watermark) {
|
|
|
18256
18602
|
},
|
|
18257
18603
|
continuousLegend: null,
|
|
18258
18604
|
categoricalLegend: null,
|
|
18605
|
+
pointMarks: [],
|
|
18606
|
+
pointCategoricalLegend: null,
|
|
18607
|
+
pointContinuousLegend: null,
|
|
18259
18608
|
tooltipDescriptors: /* @__PURE__ */ new Map(),
|
|
18260
18609
|
a11y: {
|
|
18261
18610
|
altText: "Empty map",
|