ixmaps-gl 0.2.21 → 0.2.22
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/README.md +3 -3
- package/ixmaps-gl.js +68 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -286,7 +286,7 @@ of your own project files gl can render.
|
|
|
286
286
|
Load the engine from jsDelivr, pinned to a release tag (or `@main` for the latest commit):
|
|
287
287
|
|
|
288
288
|
```html
|
|
289
|
-
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.
|
|
289
|
+
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.22/ixmaps-gl.js"></script>
|
|
290
290
|
```
|
|
291
291
|
|
|
292
292
|
or from unpkg, the npm package [`ixmaps-gl`](https://www.npmjs.com/package/ixmaps-gl)
|
|
@@ -294,13 +294,13 @@ or from unpkg, the npm package [`ixmaps-gl`](https://www.npmjs.com/package/ixmap
|
|
|
294
294
|
browser script, which defines the global `ixmaps`):
|
|
295
295
|
|
|
296
296
|
```html
|
|
297
|
-
<script src="https://unpkg.com/ixmaps-gl@0.2.
|
|
297
|
+
<script src="https://unpkg.com/ixmaps-gl@0.2.22/ixmaps-gl.js"></script>
|
|
298
298
|
```
|
|
299
299
|
|
|
300
300
|
or use a local copy (`<script src="ixmaps-gl.js"></script>`). A minimal page:
|
|
301
301
|
|
|
302
302
|
```html
|
|
303
|
-
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.
|
|
303
|
+
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.22/ixmaps-gl.js"></script>
|
|
304
304
|
<div id="map_div" style="position:absolute;inset:0;"></div>
|
|
305
305
|
|
|
306
306
|
<script>
|
package/ixmaps-gl.js
CHANGED
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
// ---------------------------------------------------------------
|
|
97
97
|
// this engine's release (= package.json "version", checked by
|
|
98
98
|
// test/unit/version.test.mjs); ixmaps.glVersion, and logged once at start
|
|
99
|
-
const IXMAPS_GL_VERSION = '0.2.
|
|
99
|
+
const IXMAPS_GL_VERSION = '0.2.22';
|
|
100
100
|
|
|
101
101
|
const LIB_URLS = {
|
|
102
102
|
// Bumped 3.6.2 -> 5.x (2026-09-21, globe-projection compat fix): native
|
|
@@ -1542,6 +1542,16 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1542
1542
|
_filterCache.set(expr, pred);
|
|
1543
1543
|
return pred;
|
|
1544
1544
|
}
|
|
1545
|
+
// a load-time .filter() drops records for good, so a later changeThemeStyle
|
|
1546
|
+
// "filter" must re-filter the unfiltered data (flat re-processes the theme
|
|
1547
|
+
// with the new filter) instead of narrowing what the first filter left
|
|
1548
|
+
function refilterFrom(raw, spec, runtimes) {
|
|
1549
|
+
return expr => {
|
|
1550
|
+
const f = applyWhereFilter(raw, expr);
|
|
1551
|
+
return f.type === 'Table' ? joinTableFeatures(spec, f, runtimes) : f;
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1545
1555
|
function applyWhereFilter(fc, filterExpr) {
|
|
1546
1556
|
if (!filterExpr) return fc;
|
|
1547
1557
|
const pred = flatFilterPredicate(String(filterExpr));
|
|
@@ -2226,6 +2236,19 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
2226
2236
|
// build()'s main loop and defineLayer(), the two places a runtime is
|
|
2227
2237
|
// ever created, for where this gets populated.
|
|
2228
2238
|
const _globalThemeRegistry = new Map();
|
|
2239
|
+
// flat's theme index (maptheme.js getTheme, "theme 0,1,2,..."): a number or
|
|
2240
|
+
// numeric string below the theme count addresses the n-th theme
|
|
2241
|
+
function isThemeIndex(id, count) {
|
|
2242
|
+
return id != null && id !== '' && typeof id !== 'boolean' && !isNaN(Number(id)) && Number.isInteger(Number(id)) && Number(id) >= 0 && Number(id) < count;
|
|
2243
|
+
}
|
|
2244
|
+
// the runtime of a theme id for the module-level globals: by name, else the
|
|
2245
|
+
// n-th theme of the last built map
|
|
2246
|
+
function registryTheme(id) {
|
|
2247
|
+
const rt = _globalThemeRegistry.get(id);
|
|
2248
|
+
if (rt) return rt;
|
|
2249
|
+
const list = _lastMapApi && _lastMapApi.runtimes;
|
|
2250
|
+
return list && isThemeIndex(id, list.length) ? list[Number(id)] : undefined;
|
|
2251
|
+
}
|
|
2229
2252
|
|
|
2230
2253
|
// Same LAST-DEFINITION-WINS rationale as _globalThemeRegistry above, for
|
|
2231
2254
|
// real ixmaps-flat's map-LEVEL globals (ixmaps.getProjectString/
|
|
@@ -3762,7 +3785,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3762
3785
|
};
|
|
3763
3786
|
// a plain FEATURE(S) base layer has one flat fill (no classes); FEATURE|CATEGORICAL
|
|
3764
3787
|
// is coloured per category (see _buildFeaturesLayers), so it gets a legend
|
|
3765
|
-
|
|
3788
|
+
// a CATEGORICAL theme with no value binding (a count per aggregation cell) has no
|
|
3789
|
+
// category rows: its panel is the text legend (title / snippet / description)
|
|
3790
|
+
const countOnlyLegend = rt => !rt.categoryLabels && rt.flags.has('CATEGORICAL') && !rt.binding.value;
|
|
3791
|
+
const legendApplies = rt => (countOnlyLegend(rt) || (rt.categoryLabels && rt.categoryLabels.length))
|
|
3766
3792
|
&& (!(rt.flags.has('FEATURE') || rt.flags.has('FEATURES')) || rt.flags.has('CATEGORICAL')) && !rt.flags.has('NOLEGEND');
|
|
3767
3793
|
// One theme's panel — at build time for every theme, and again for a
|
|
3768
3794
|
// theme defined later (map.layer(...) in a myMap.then(...) chain,
|
|
@@ -3898,7 +3924,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3898
3924
|
// dropdown and Chart-size slider stay: neither is a
|
|
3899
3925
|
// "categorical item" or "colorscheme swatch", both are
|
|
3900
3926
|
// independent controls unrelated to the row list.
|
|
3901
|
-
const isTextOnly = rt.flags.has('TEXTLEGEND');
|
|
3927
|
+
const isTextOnly = rt.flags.has('TEXTLEGEND') || countOnlyLegend(rt);
|
|
3902
3928
|
|
|
3903
3929
|
// Map-view-aware per explicit request: totals reflect only
|
|
3904
3930
|
// what's CURRENTLY on screen (the map's own bounds, plus the
|
|
@@ -4577,6 +4603,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4577
4603
|
const filtered = applyWhereFilter(raw, spec.filter);
|
|
4578
4604
|
const fc = filtered.type === 'Table' ? joinTableFeatures(spec, filtered, runtimes) : filtered;
|
|
4579
4605
|
const rt = new LayerRuntime(spec, fc, this._engineOptions);
|
|
4606
|
+
if (spec.filter) rt._refilter = refilterFrom(raw, spec, runtimes);
|
|
4580
4607
|
// tags which underlying data source this runtime came from — see
|
|
4581
4608
|
// setFacetFilter/clearFacetFilter/clearAllFacetFilters below,
|
|
4582
4609
|
// which use this to propagate a facet filter to every theme
|
|
@@ -4805,6 +4832,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4805
4832
|
// verified conventions' own priority (layer name, then style.name).
|
|
4806
4833
|
function findRuntime(themeId) {
|
|
4807
4834
|
const matches = runtimes.filter(r => r.name === themeId || (r.style && r.style.name === themeId) || (r.meta && r.meta.name === themeId));
|
|
4835
|
+
// a number (or "1", "2" ...) naming no theme is flat's theme index
|
|
4836
|
+
// (maptheme.js getTheme): 0-based, base layers counted
|
|
4837
|
+
if (!matches.length && isThemeIndex(themeId, runtimes.length)) return runtimes[Number(themeId)];
|
|
4808
4838
|
if (!matches.length) { console.warn(`[ixmaps-gl] no layer named "${themeId}"`); return undefined; }
|
|
4809
4839
|
return matches.find(r => !r.flags.has('FEATURE') && !r.flags.has('FEATURES')) || matches[0];
|
|
4810
4840
|
}
|
|
@@ -5087,7 +5117,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5087
5117
|
targets.forEach(rt => {
|
|
5088
5118
|
if (validation) validation.style(themeIdOf(rt), { [key]: value });
|
|
5089
5119
|
if (key === 'filter') {
|
|
5090
|
-
|
|
5120
|
+
const expr = action === 'remove' ? '' : value;
|
|
5121
|
+
if (rt._refilter) {
|
|
5122
|
+
// the theme was loaded with a filter: re-filter its full data
|
|
5123
|
+
rt._runtimeFilterExpr = '';
|
|
5124
|
+
rt._filterExpr = expr;
|
|
5125
|
+
rt.replaceFeatures(rt._refilter(expr));
|
|
5126
|
+
} else rt.setRuntimeFilter(expr);
|
|
5091
5127
|
changed = true;
|
|
5092
5128
|
return;
|
|
5093
5129
|
}
|
|
@@ -5425,6 +5461,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5425
5461
|
spec = pspec;
|
|
5426
5462
|
}
|
|
5427
5463
|
rt.replaceFeatures(namedThemeFeatures(spec, rows, rt._specBinding));
|
|
5464
|
+
rt._refilter = spec.filter ? expr => namedThemeFeatures(Object.assign({}, spec, { filter: expr }), rows, rt._specBinding) : null;
|
|
5428
5465
|
if (glDebug()) console.info(`[ixmaps-gl debug] data for ${themeIdOf(rt)}: ${rows.length} rows → ${rt.features.length} features`);
|
|
5429
5466
|
// the legend is built from the theme's classes: build it again
|
|
5430
5467
|
if (rt._legendPanel && rt._legendPanel.parentNode) rt._legendPanel.parentNode.removeChild(rt._legendPanel);
|
|
@@ -5500,6 +5537,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5500
5537
|
const filtered = applyWhereFilter(raw, spec.filter);
|
|
5501
5538
|
const fc = filtered.type === 'Table' ? joinTableFeatures(spec, filtered, runtimes) : filtered;
|
|
5502
5539
|
const rt = new LayerRuntime(spec, fc, builder._engineOptions);
|
|
5540
|
+
if (spec.filter) rt._refilter = refilterFrom(raw, spec, runtimes);
|
|
5503
5541
|
rt._definition = pdef; // for refreshTheme
|
|
5504
5542
|
rt._deferredLoad = deferred;
|
|
5505
5543
|
rt._named = named;
|
|
@@ -10853,7 +10891,14 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10853
10891
|
// reasonable rather than wrong.
|
|
10854
10892
|
// borderOpacity (default 1): the border's own alpha (flat's
|
|
10855
10893
|
// stroke-opacity, see flatBubbleLook)
|
|
10894
|
+
// colorscheme ["none"] — flat's explicit no-fill symbol
|
|
10895
|
+
_noFillScheme() {
|
|
10896
|
+
const cs = this.style.colorscheme;
|
|
10897
|
+
return Array.isArray(cs) && cs.length === 1 && String(cs[0]).trim().toLowerCase() === 'none';
|
|
10898
|
+
}
|
|
10899
|
+
|
|
10856
10900
|
_buildSingleIcon(colorRgb, opacity, shape, borderColorRgb, borderWidth, borderOpacity = 1) {
|
|
10901
|
+
if (!colorRgb) { colorRgb = [128, 128, 128]; opacity = 0; } // no class colour (["none"]): unfilled
|
|
10857
10902
|
shape = shape || 'circle';
|
|
10858
10903
|
borderWidth = borderWidth || 0;
|
|
10859
10904
|
const key = `single-${shape}-${colorRgb.join(',')}-${opacity}-${borderColorRgb ? borderColorRgb.join(',') : 'none'}-${borderWidth}${borderOpacity !== 1 ? `-so${borderOpacity}` : ''}`;
|
|
@@ -12905,6 +12950,15 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12905
12950
|
// _buildTooltipContext's own `isGroup` check already uses).
|
|
12906
12951
|
const sizeValueOf = d => d.properties.counts ? d.properties.total : d.properties.value;
|
|
12907
12952
|
const combined = individual.concat(groups).sort((a, b) => sizeValueOf(a) - sizeValueOf(b));
|
|
12953
|
+
// .style({minvalue}) on a bubble: flat skips an item whose size value is below it
|
|
12954
|
+
// (maptheme.js 19826 / 20698, `this.nMinValue && nSizeValue < this.nMinValue`).
|
|
12955
|
+
// combined is ascending, so the items below it are a prefix.
|
|
12956
|
+
const bubbleMin = this.flags.has('SYMBOL') ? styleNum(this.style.minvalue) : NaN;
|
|
12957
|
+
if (bubbleMin) {
|
|
12958
|
+
let cut = 0;
|
|
12959
|
+
while (cut < combined.length && sizeValueOf(combined[cut]) < bubbleMin) cut++;
|
|
12960
|
+
combined.splice(0, cut);
|
|
12961
|
+
}
|
|
12908
12962
|
// maxcharts: like the flat engine (maptheme.js nMaxCharts), draw only the
|
|
12909
12963
|
// biggest N items — combined is ascending, so drop from the front
|
|
12910
12964
|
applyFlatMaxCharts(combined, this.style);
|
|
@@ -13239,6 +13293,8 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13239
13293
|
// a CATEGORICAL BUBBLE: flat's look (flatBubbleLook) in screen px,
|
|
13240
13294
|
// its stroke centered on the radius as flat's SVG circle
|
|
13241
13295
|
const lookUnit = objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(this.style.scale) || 1);
|
|
13296
|
+
// colorscheme ["none"]: flat draws the symbol unfilled (only its values / box / title)
|
|
13297
|
+
const noFill = this._noFillScheme();
|
|
13242
13298
|
const circleLook = flatLook ? d => flatBubbleLook((this.categoryColorsRgb || [])[d.properties.cat] || [128, 128, 128], radiusPx(d), lookUnit, this.style, this.flags) : null;
|
|
13243
13299
|
const lineRgb = d => (circleLook ? circleLook(d).stroke : outlineRgb(d)) || [0, 0, 0];
|
|
13244
13300
|
layers.push(new ScatterplotLayer({
|
|
@@ -13247,7 +13303,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13247
13303
|
getPosition: d => d.geometry.coordinates,
|
|
13248
13304
|
getRadius: d => radiusPx(d) - (symbolOutline || circleLook ? 0 : outlinePx(d, radiusPx(d)) / 2),
|
|
13249
13305
|
radiusUnits: 'pixels',
|
|
13250
|
-
getFillColor: d => [...((this.categoryColorsRgb || [])[d.properties.cat] || [128, 128, 128]).slice(0, 3), Math.round(this._iconAlpha(d) * (circleLook ? circleLook(d).fillOpacity : fillOpacity))],
|
|
13306
|
+
getFillColor: d => [...((this.categoryColorsRgb || [])[d.properties.cat] || [128, 128, 128]).slice(0, 3), noFill ? 0 : Math.round(this._iconAlpha(d) * (circleLook ? circleLook(d).fillOpacity : fillOpacity))],
|
|
13251
13307
|
stroked: circleLook ? combined.some(d => circleLook(d).stroke) : combined.some(d => outlineRgb(d)),
|
|
13252
13308
|
getLineColor: d => [...lineRgb(d).slice(0, 3), Math.round(this._iconAlpha(d) * (circleLook ? circleLook(d).strokeOpacity : 1))],
|
|
13253
13309
|
getLineWidth: d => (circleLook ? (circleLook(d).stroke ? circleLook(d).width : 0) : outlinePx(d, radiusPx(d))),
|
|
@@ -14957,7 +15013,8 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14957
15013
|
}
|
|
14958
15014
|
|
|
14959
15015
|
// pure: oneHot → see the pure-function sections above the class
|
|
14960
|
-
|
|
15016
|
+
// CATEGORICAL without a value binding (count per aggregation cell) has no categoryLabels: one bucket
|
|
15017
|
+
_oneHot(cat, value) { return oneHot(cat, value, this.categoryLabels ? this.categoryLabels.length : 1); }
|
|
14961
15018
|
|
|
14962
15019
|
// pure: groupCoLocated → see the pure-function sections above the class
|
|
14963
15020
|
_groupCoLocated(clusterFeatures, zoom) { return groupCoLocated(clusterFeatures, zoom, this._clusterRadiusPx, this.categoryLabels ? this.categoryLabels.length : 1, this.flags); }
|
|
@@ -15183,7 +15240,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15183
15240
|
show() { mapHandle(this.szMap).setThemeVisible(this.szTheme, true); return this; },
|
|
15184
15241
|
hide() { mapHandle(this.szMap).setThemeVisible(this.szTheme, false); return this; },
|
|
15185
15242
|
toggle() {
|
|
15186
|
-
const rt =
|
|
15243
|
+
const rt = registryTheme(this.szTheme);
|
|
15187
15244
|
mapHandle(this.szMap).setThemeVisible(this.szTheme, rt ? !!rt._hidden : false);
|
|
15188
15245
|
return this;
|
|
15189
15246
|
},
|
|
@@ -15350,7 +15407,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15350
15407
|
// no id: flat's current theme — here the one a page's facets were
|
|
15351
15408
|
// built for (its own facet script sets ixmaps.filterThemeId), else the
|
|
15352
15409
|
// last chart / choropleth theme
|
|
15353
|
-
let rt = szId != null ?
|
|
15410
|
+
let rt = szId != null ? registryTheme(szId) : null;
|
|
15354
15411
|
if (szId == null) {
|
|
15355
15412
|
const pid = pageIxmaps() && pageIxmaps().filterThemeId;
|
|
15356
15413
|
rt = (pid != null && _globalThemeRegistry.get(pid))
|
|
@@ -15441,7 +15498,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15441
15498
|
// flat's ixmaps.getThemeDefinitionObj(szId): the theme's definition —
|
|
15442
15499
|
// type, style (sizefield/valuefield as flat names them), meta
|
|
15443
15500
|
function getThemeDefinitionObj(szId) {
|
|
15444
|
-
const rt =
|
|
15501
|
+
const rt = registryTheme(szId);
|
|
15445
15502
|
if (!rt) return null;
|
|
15446
15503
|
const style = Object.assign({}, rt.style);
|
|
15447
15504
|
if (rt.binding.size && !style.sizefield) style.sizefield = rt.binding.size;
|
|
@@ -15487,11 +15544,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15487
15544
|
// before that block ran (there is none, currently) would just no-op
|
|
15488
15545
|
// here instead of redrawing.
|
|
15489
15546
|
function markThemeClass(szId, index) {
|
|
15490
|
-
const rt =
|
|
15547
|
+
const rt = registryTheme(szId);
|
|
15491
15548
|
if (rt) markRuntimeClass(rt, index);
|
|
15492
15549
|
}
|
|
15493
15550
|
function unmarkThemeClass(szId, index) {
|
|
15494
|
-
const rt =
|
|
15551
|
+
const rt = registryTheme(szId);
|
|
15495
15552
|
if (rt) unmarkRuntimeClass(rt, index);
|
|
15496
15553
|
}
|
|
15497
15554
|
// the legend's own rows mark their theme directly: its .layer() name
|