ixmaps-gl 0.2.7 → 0.2.8

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 CHANGED
@@ -203,7 +203,7 @@ of your own project files gl can render.
203
203
  Load the engine from jsDelivr, pinned to a release tag (or `@main` for the latest commit):
204
204
 
205
205
  ```html
206
- <script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.7/ixmaps-gl.js"></script>
206
+ <script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.8/ixmaps-gl.js"></script>
207
207
  ```
208
208
 
209
209
  or from unpkg, the npm package [`ixmaps-gl`](https://www.npmjs.com/package/ixmaps-gl)
@@ -211,13 +211,13 @@ or from unpkg, the npm package [`ixmaps-gl`](https://www.npmjs.com/package/ixmap
211
211
  browser script, which defines the global `ixmaps`):
212
212
 
213
213
  ```html
214
- <script src="https://unpkg.com/ixmaps-gl@0.2.7/ixmaps-gl.js"></script>
214
+ <script src="https://unpkg.com/ixmaps-gl@0.2.8/ixmaps-gl.js"></script>
215
215
  ```
216
216
 
217
217
  or use a local copy (`<script src="ixmaps-gl.js"></script>`). A minimal page:
218
218
 
219
219
  ```html
220
- <script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.7/ixmaps-gl.js"></script>
220
+ <script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.8/ixmaps-gl.js"></script>
221
221
  <div id="map_div" style="position:absolute;inset:0;"></div>
222
222
 
223
223
  <script>
package/ixmaps-gl.js CHANGED
@@ -81,7 +81,7 @@
81
81
  // ---------------------------------------------------------------
82
82
  // this engine's release (= package.json "version", checked by
83
83
  // test/unit/version.test.mjs); ixmaps.glVersion, and logged once at start
84
- const IXMAPS_GL_VERSION = '0.2.7';
84
+ const IXMAPS_GL_VERSION = '0.2.8';
85
85
 
86
86
  const LIB_URLS = {
87
87
  // Bumped 3.6.2 -> 5.x (2026-09-21, globe-projection compat fix): native
@@ -6207,6 +6207,25 @@
6207
6207
  return { present, radii, offsets, fitScale, maxR };
6208
6208
  }
6209
6209
 
6210
+ // A group while legend classes are marked (flat's MapTheme.markClass,
6211
+ // maptheme.js 14339-14420, evidence "isolate" — the default): the parts
6212
+ // of the other categories are hidden, the marked ones keep their own
6213
+ // size and move to the group's center (the SEQUENCE transform reset to
6214
+ // its scale only). Biggest first, so a smaller marked part stays on top.
6215
+ // null when no marked category is in the group.
6216
+ function isolatedBubblePackLayout(counts, size, marked) {
6217
+ const full = computeBubblePackLayout(counts, size);
6218
+ const keep = full.present.map((p, k) => k).filter(k => marked.has(full.present[k].i))
6219
+ .sort((a, b) => full.radii[b] - full.radii[a]);
6220
+ if (!keep.length) return null;
6221
+ return {
6222
+ present: keep.map(k => full.present[k]),
6223
+ radii: keep.map(k => full.radii[k]),
6224
+ offsets: keep.map(() => ({ x: 0, y: 0 })),
6225
+ fitScale: full.fitScale, maxR: full.maxR
6226
+ };
6227
+ }
6228
+
6210
6229
  // ---------------------------------------------------------------
6211
6230
  // Classification — pure functions (no LayerRuntime state): class
6212
6231
  // breaks for a numeric field, and which class a value falls in.
@@ -8011,7 +8030,7 @@
8011
8030
  return this._glowIconCache.get(key);
8012
8031
  }
8013
8032
 
8014
- _buildBubbleIcon(counts, colors) {
8033
+ _buildBubbleIcon(counts, colors, marked = null) {
8015
8034
  // The cache key quantizes each count's RATIO to the group's total,
8016
8035
  // not the raw count — computeBubblePackLayout's radii are purely
8017
8036
  // sqrt(c/total), so two clusters with wildly different absolute
@@ -8036,13 +8055,18 @@
8036
8055
  // smooth.
8037
8056
  const total = counts.reduce((a, b) => a + b, 0) || 1;
8038
8057
  const RATIO_BUCKETS = 15;
8039
- const key = counts.map(c => Math.round((c / total) * RATIO_BUCKETS)).join('-');
8058
+ // an isolated group shows only its marked parts, alone and at their
8059
+ // own size: their radius (√share, 1/100 steps) keys the icon, not the
8060
+ // coarse share buckets that can put a small marked part at 0
8061
+ const key = counts.map(c => Math.round((c / total) * RATIO_BUCKETS)).join('-') + (marked
8062
+ ? '|iso:' + counts.map((c, i) => (c > 0 && marked.has(i) ? i + ':' + Math.round(Math.sqrt(c / total) * 100) : null)).filter(Boolean).join(',')
8063
+ : '');
8040
8064
  if (this._iconCache.has(key)) return this._iconCache.get(key);
8041
8065
  const size = BUBBLE_ICON_SIZE;
8042
8066
  const canvas = document.createElement('canvas');
8043
8067
  canvas.width = size; canvas.height = size;
8044
8068
  const ctx = canvas.getContext('2d');
8045
- const { present, radii, offsets, fitScale } = computeBubblePackLayout(counts, size);
8069
+ const { present, radii, offsets, fitScale } = (marked && isolatedBubblePackLayout(counts, size, marked)) || computeBubblePackLayout(counts, size);
8046
8070
  const cx = size / 2, cy = size / 2;
8047
8071
  present.forEach((p, i) => {
8048
8072
  ctx.beginPath();
@@ -8341,6 +8365,15 @@
8341
8365
  // counts as marked if ANY of its constituent categories is marked —
8342
8366
  // a mixed cluster shouldn't dim just because one of several
8343
8367
  // categories inside it happens to be unmarked.
8368
+ // the marked classes a group isolates to (isolatedBubblePackLayout),
8369
+ // or null: none marked, a page's own mark handler, or evidence
8370
+ // "isolate_gray" (keeps every part, dimmed via _iconAlpha)
8371
+ _groupIsolation(d) {
8372
+ if (!d.properties.counts || !this._markedClasses.size || this._onMarksChanged) return null;
8373
+ if (String(this.style.evidence || 'isolate') === 'isolate_gray') return null;
8374
+ return d.properties.counts.some((c, i) => c > 0 && this._markedClasses.has(i)) ? this._markedClasses : null;
8375
+ }
8376
+
8344
8377
  _iconAlpha(d) {
8345
8378
  if (!this._markedClasses.size) return 255;
8346
8379
  if (d.properties.counts) {
@@ -9850,7 +9883,7 @@
9850
9883
  id: `ix-glow-${this.name}-g${this._iconGeneration}`,
9851
9884
  data: combined, pickable: false,
9852
9885
  getPosition: d => d.geometry.coordinates,
9853
- getIcon: d => this._getGlowIcon(this.categoryColorsRgb[d.properties.counts ? dominant(d.properties.counts) : d.properties.cat]),
9886
+ getIcon: d => this._getGlowIcon(this.categoryColorsRgb[d.properties.counts ? dominant(this._groupIsolation(d) ? d.properties.counts.map((c, i) => (this._markedClasses.has(i) ? c : 0)) : d.properties.counts) : d.properties.cat]),
9854
9887
  getSize: d => valueRadius(sizeValueOf(d), liveZoom, this.style, this.mapOptions, this.flags, this._maxSizeValue) * (d.properties.counts ? 9 : 11),
9855
9888
  getColor: d => [255, 255, 255, this._iconAlpha(d)],
9856
9889
  ...(alignOf.active ? { getPixelOffset: alignOf } : {}),
@@ -10000,7 +10033,7 @@
10000
10033
  data: combined, pickable: true,
10001
10034
  getPosition: d => d.geometry.coordinates,
10002
10035
  getIcon: d => d.properties.counts
10003
- ? this._buildBubbleIcon(d.properties.counts, this.categoryColorsRgb)
10036
+ ? this._buildBubbleIcon(d.properties.counts, this.categoryColorsRgb, this._groupIsolation(d))
10004
10037
  : this._buildSingleIcon(this.categoryColorsRgb[d.properties.cat], fillOpacity, this._resolveSymbolShape(d.properties), singleBorderColorRgb, singleBorderWidthPx),
10005
10038
  getSize: d => valueRadius(sizeValueOf(d), liveZoom, this.style, this.mapOptions, this.flags, this._maxSizeValue) * 2,
10006
10039
  getColor: d => [255, 255, 255, this._iconAlpha(d)],
@@ -10073,7 +10106,8 @@
10073
10106
  const outerRadiusPx = valueRadius(d.properties.total, liveZoom, this.style, this.mapOptions, this.flags, this._maxSizeValue);
10074
10107
  const iconSizePx = outerRadiusPx * 2; // matches the cluster IconLayer's own getSize (*2) below
10075
10108
  const pxPerCanvasUnit = iconSizePx / BUBBLE_ICON_SIZE;
10076
- const { present, radii, offsets, fitScale } = computeBubblePackLayout(counts, BUBBLE_ICON_SIZE);
10109
+ const iso = this._groupIsolation(d);
10110
+ const { present, radii, offsets, fitScale } = (iso && isolatedBubblePackLayout(counts, BUBBLE_ICON_SIZE, iso)) || computeBubblePackLayout(counts, BUBBLE_ICON_SIZE);
10077
10111
 
10078
10112
  present.forEach((p, i) => {
10079
10113
  // one part for a range-classed cell: its class value, if separate
@@ -10746,7 +10780,7 @@
10746
10780
  // test-only: lets test/unit/*.test.mjs call pure internals directly (the
10747
10781
  // engine runs in a Node vm there); deliberately NOT on the ixmaps object
10748
10782
  global.__ixmapsGlInternals = {
10749
- IXMAPS_GL_VERSION,
10783
+ IXMAPS_GL_VERSION, computeBubblePackLayout, isolatedBubblePackLayout,
10750
10784
  normalizeTheme, projectThemeToDefinition, withoutProjectCode, groupRecordCount, resolveBasemapStyleUrl, resolveMapTypeColor, LayerBuilder, LayerRuntime, typeStyleNumbers, styleNum,
10751
10785
  resolveScriptUrl, isTrustedScriptUrl, loadProcessingScript, loadBrokerData, applyBrokerThemePatch, makeBrokerTheme,
10752
10786
  equalIntervalBreaks, quantileBreaks, naturalBreaks, evenStrideSample, jenksBreakValues, partsFromBreakValues, resolvePartsClass,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ixmaps-gl",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
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",