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.
Files changed (3) hide show
  1. package/README.md +3 -3
  2. package/ixmaps-gl.js +68 -11
  3. 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.21/ixmaps-gl.js"></script>
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.21/ixmaps-gl.js"></script>
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.21/ixmaps-gl.js"></script>
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.21';
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
- const legendApplies = rt => rt.categoryLabels && rt.categoryLabels.length
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
- rt.setRuntimeFilter(action === 'remove' ? '' : value);
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
- _oneHot(cat, value) { return oneHot(cat, value, this.categoryLabels.length); }
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 = _globalThemeRegistry.get(this.szTheme);
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 ? _globalThemeRegistry.get(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 = _globalThemeRegistry.get(szId);
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 = _globalThemeRegistry.get(szId);
15547
+ const rt = registryTheme(szId);
15491
15548
  if (rt) markRuntimeClass(rt, index);
15492
15549
  }
15493
15550
  function unmarkThemeClass(szId, index) {
15494
- const rt = _globalThemeRegistry.get(szId);
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ixmaps-gl",
3
- "version": "0.2.21",
3
+ "version": "0.2.22",
4
4
  "description": "MapLibre GL + deck.gl reimplementation of the ixmaps-flat declarative mapping engine",
5
5
  "main": "ixmaps-gl.js",
6
6
  "browser": "ixmaps-gl.js",