ixmaps-gl 0.2.10 → 0.2.11

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 +5 -4
  2. package/ixmaps-gl.js +136 -43
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -86,7 +86,8 @@ circle/square/diamond/triangle. See the top-of-file comment in
86
86
  A page's `.require(url)` scripts (page code, e.g. a user chart function) load in order before the
87
87
  layers are built. `ixmaps.map()` is the map handle as in flat — its calls wait until the map is ready —
88
88
  with `add(theme, flags)` / `replace` / `replaceTheme` / `remove`, `changeThemeStyle` for any style key,
89
- `getZoom()` (flat's zoom), `resize()`, `setBasemapOpacity` and `setMapType(id)` (also `setMapTypeId`, `mapType`: a basemap name reading dark/black/night/matter loads CARTO Dark Matter, any other name CARTO Positron, a color a plain background — the same rule the `mapType` option follows at start); the page's `htmlgui_onNewTheme(id)` and
89
+ `getZoom()` (flat's zoom), `resize()`, `attribution(text)` (the page's attribution, bottom left on the map as in flat, in the style of the basemap credit and on its line — also
90
+ `.attribution()` on the builder, the `attribution` map option and `ixmaps.setAttribution(text)`; the basemap's credit stays bottom right), `setBasemapOpacity` and `setMapType(id)` (also `setMapTypeId`, `mapType`: a basemap name reading dark/black/night/matter loads CARTO Dark Matter, any other name CARTO Positron, a color a plain background — the same rule the `mapType` option follows at start); the page's `htmlgui_onNewTheme(id)` and
90
91
  `htmlgui_onZoomAndPan()` (also once on load) are called (as in flat, a theme swapped in with `replace` comes last
91
92
  in the legend, while FEATURE and CHOROPLETH shapes always draw under the charts), and `map`, `getZoom` and flat's
92
93
  `formatValue` exist as globals. `.data({name})` without a URL waits for data of that name: from
@@ -207,7 +208,7 @@ of your own project files gl can render.
207
208
  Load the engine from jsDelivr, pinned to a release tag (or `@main` for the latest commit):
208
209
 
209
210
  ```html
210
- <script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.10/ixmaps-gl.js"></script>
211
+ <script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.11/ixmaps-gl.js"></script>
211
212
  ```
212
213
 
213
214
  or from unpkg, the npm package [`ixmaps-gl`](https://www.npmjs.com/package/ixmaps-gl)
@@ -215,13 +216,13 @@ or from unpkg, the npm package [`ixmaps-gl`](https://www.npmjs.com/package/ixmap
215
216
  browser script, which defines the global `ixmaps`):
216
217
 
217
218
  ```html
218
- <script src="https://unpkg.com/ixmaps-gl@0.2.10/ixmaps-gl.js"></script>
219
+ <script src="https://unpkg.com/ixmaps-gl@0.2.11/ixmaps-gl.js"></script>
219
220
  ```
220
221
 
221
222
  or use a local copy (`<script src="ixmaps-gl.js"></script>`). A minimal page:
222
223
 
223
224
  ```html
224
- <script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.10/ixmaps-gl.js"></script>
225
+ <script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.11/ixmaps-gl.js"></script>
225
226
  <div id="map_div" style="position:absolute;inset:0;"></div>
226
227
 
227
228
  <script>
package/ixmaps-gl.js CHANGED
@@ -82,7 +82,7 @@
82
82
  // ---------------------------------------------------------------
83
83
  // this engine's release (= package.json "version", checked by
84
84
  // test/unit/version.test.mjs); ixmaps.glVersion, and logged once at start
85
- const IXMAPS_GL_VERSION = '0.2.10';
85
+ const IXMAPS_GL_VERSION = '0.2.11';
86
86
 
87
87
  const LIB_URLS = {
88
88
  // Bumped 3.6.2 -> 5.x (2026-09-21, globe-projection compat fix): native
@@ -308,7 +308,7 @@
308
308
  // crafted link must not be able to load arbitrary code into the page.
309
309
  // ---------------------------------------------------------------
310
310
  // v0.1.6: knows this engine's aliases, lookupfield rule, meta↔style, builder methods, project loading
311
- const VALIDATOR_URL_DEFAULT = 'https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-grammar@v0.1.24/dist/validate.mjs';
311
+ const VALIDATOR_URL_DEFAULT = 'https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-grammar@v0.1.25/dist/validate.mjs';
312
312
  // a page may set ixmaps.validate BEFORE loading this script — the
313
313
  // `global.ixmaps = {...}` export below would otherwise overwrite it
314
314
  const _preloadValidate = global.ixmaps && global.ixmaps.validate;
@@ -2125,7 +2125,7 @@
2125
2125
  // edit it by hand; `npm run unit` fails when it drifts.
2126
2126
  // ---------------------------------------------------------------
2127
2127
  // <grammar:binding-aliases>
2128
- // generated from ixmaps-grammar 0.1.24 (ixmaps-flat 1.0.42, 2b978d5 2026-10-02) — 52 aliases
2128
+ // generated from ixmaps-grammar 0.1.25 (ixmaps-flat 1.0.42, 2b978d5 2026-10-02) — 52 aliases
2129
2129
  const FLAT_BINDING_ALIASES = {
2130
2130
  "aggregation": "style.aggregationfield",
2131
2131
  "aggregationfield": "style.aggregationfield",
@@ -2187,7 +2187,7 @@
2187
2187
  // both — so .style({tooltip}) is a tooltip and .meta({fillopacity}) a style
2188
2188
  // property. GENERATED by test/sync-grammar.mjs — do not edit.
2189
2189
  // <grammar:meta-keys>
2190
- // generated from ixmaps-grammar 0.1.24 — 5 meta keys
2190
+ // generated from ixmaps-grammar 0.1.25 — 5 meta keys
2191
2191
  const FLAT_META_KEYS = ["description","name","snippet","title","tooltip"];
2192
2192
  // </grammar:meta-keys>
2193
2193
 
@@ -2643,6 +2643,39 @@
2643
2643
  };
2644
2644
  }
2645
2645
 
2646
+ // ---------------------------------------------------------------
2647
+ // flat's map attribution (ui/html/mappage.html #attribution-div): the
2648
+ // page's text bottom left on the map; hidden while empty ("null" counts
2649
+ // as empty, htmlgui_flat.js 522-526). Here a MapLibre control in the
2650
+ // bottom-left corner, styled like MapLibre's own attribution (the
2651
+ // basemap credit, bottom right — same line, same pill: white, 12px
2652
+ // radius, 12px/20px Helvetica Neue, links rgba(0,0,0,0.75)).
2653
+ // ---------------------------------------------------------------
2654
+ let _attributionCss = false;
2655
+ function createAttribution(map) {
2656
+ if (!_attributionCss && typeof document !== 'undefined') {
2657
+ _attributionCss = true;
2658
+ const css = document.createElement('style');
2659
+ css.textContent = '.ixmaps-gl-attribution a{color:rgba(0,0,0,0.75);text-decoration:none}'
2660
+ + '.ixmaps-gl-attribution a:hover{color:inherit;text-decoration:underline}';
2661
+ document.head.appendChild(css);
2662
+ }
2663
+ const box = document.createElement('div');
2664
+ box.className = 'maplibregl-ctrl ixmaps-gl-attribution';
2665
+ box.style.cssText = 'background:#fff;border-radius:12px;padding:2px 8px;min-height:20px;box-sizing:content-box;'
2666
+ + 'font:12px/20px "Helvetica Neue",Arial,Helvetica,sans-serif;color:#000;display:none;';
2667
+ map.addControl({ onAdd: () => box, onRemove: () => box.remove() }, 'bottom-left');
2668
+ let text = '';
2669
+ return {
2670
+ set(t) {
2671
+ text = t == null || t === 'null' ? '' : String(t);
2672
+ box.innerHTML = text;
2673
+ box.style.display = text ? 'block' : 'none';
2674
+ },
2675
+ get() { return text; }
2676
+ };
2677
+ }
2678
+
2646
2679
  // ---------------------------------------------------------------
2647
2680
  // Hover tooltips and the click-to-pin tooltip of a map, built once per map
2648
2681
  // by MapBuilder.build(). ctx: the builder (its map options, for the
@@ -2677,8 +2710,8 @@
2677
2710
  };
2678
2711
  const pinnedTooltipEl = document.createElement('div');
2679
2712
  pinnedTooltipEl.style.cssText = 'position:absolute;top:0;left:0;z-index:6;display:none;' +
2680
- 'pointer-events:auto;max-width:320px;max-height:320px;overflow:auto;' +
2681
- 'padding:0.6em 1.6em 0.6em 0.7em;font-size:0.85em;';
2713
+ 'pointer-events:auto;max-width:440px;max-height:360px;overflow:auto;' +
2714
+ 'padding:0.6em 1.6em 0.6em 0.7em;font-size:1em;';
2682
2715
 
2683
2716
  function updatePinnedTooltipPosition() {
2684
2717
  if (!pinned) return;
@@ -2775,7 +2808,7 @@
2775
2808
  const rt = findRuntimeForLayerId(layer.id);
2776
2809
  if (!rt) return null;
2777
2810
  const html = rt.buildTooltipHtml(object);
2778
- return html ? { html, style: Object.assign({ fontSize: '0.85em', padding: '0.5em 0.7em', maxWidth: '320px' }, tooltipLook()) } : null;
2811
+ return html ? { html, style: Object.assign({ fontSize: '1em', padding: '0.5em 0.7em', maxWidth: '440px' }, tooltipLook()) } : null;
2779
2812
  }
2780
2813
  function clickFor(picked) {
2781
2814
  const info = picked && Object.assign({}, picked, { object: pickedFeature(picked.object) });
@@ -3587,7 +3620,13 @@
3587
3620
  // needs (a chart library, a user chart) — page code, like a <script>
3588
3621
  // tag; loaded in order before the layers (see loadRequiredScripts)
3589
3622
  require(url) { this._required = (this._required || []).concat(url); return this; }
3590
- attribution(a) { this._attributionText = a; return this; }
3623
+ // flat's .attribution(text): the page's attribution, bottom left on the
3624
+ // map (createAttribution) — also after the map is built
3625
+ attribution(a) {
3626
+ this._attributionText = a;
3627
+ if (this._setAttribution) this._setAttribution(a);
3628
+ return this;
3629
+ }
3591
3630
  legend(html) { this._legendHtml = html; return this; }
3592
3631
  // GL-PORT COMPAT: real ixmaps-flat's map.layer(name) returns a NEW
3593
3632
  // per-layer builder for a .data()...define() chain on the LAYER
@@ -3781,13 +3820,10 @@
3781
3820
  : resolveBasemapStyleUrl(this.mapOptions.mapType),
3782
3821
  center: [lon, lat],
3783
3822
  zoom: this._viewZoom != null && this._viewZoom !== '' ? flatToMapLibreZoom(Number(this._viewZoom)) : 8,
3784
- // .attribution(a) (MapBuilder, above) was stored but never read —
3785
- // unlike .legend()'s parallel _legendHtml, which the splash/legend
3786
- // block below actually renders. MapLibre's own AttributionControl
3787
- // is added automatically (not disabled anywhere in this file) and
3788
- // accepts extra text via customAttribution, appended alongside the
3789
- // basemap's own required CARTO/OpenStreetMap credit rather than
3790
- // replacing it.
3823
+ // MapLibre's own AttributionControl (bottom right) keeps the
3824
+ // basemap's required CARTO/OpenStreetMap credit; the page's own
3825
+ // .attribution() goes bottom left, as flat shows it
3826
+ // (createAttribution).
3791
3827
  //
3792
3828
  // A color `mapType` (mapTypeColor truthy — see
3793
3829
  // buildBlankBackgroundStyle above) has no real basemap at all, so
@@ -3798,7 +3834,6 @@
3798
3834
  // customAttribution's own required-credit behavior for a REAL
3799
3835
  // basemap is left untouched.
3800
3836
  ...(mapTypeColor ? { attributionControl: false } : {}),
3801
- ...(this._attributionText ? { customAttribution: this._attributionText } : {}),
3802
3837
  // MapLibre's default (true) is kept: with false it also stops the
3803
3838
  // zoom-out where the world gets narrower than the window, so a
3804
3839
  // world map can't open fully zoomed out. The deck.gl overlay takes
@@ -3912,6 +3947,10 @@
3912
3947
  map.addControl(overlay);
3913
3948
  map.addControl(new maplibregl.NavigationControl(), 'top-left');
3914
3949
  tooltips.mount();
3950
+ // the page's attribution (.attribution(), Map option attribution)
3951
+ const attributionBox = createAttribution(map);
3952
+ attributionBox.set(this._attributionText != null ? this._attributionText : this.mapOptions.attribution);
3953
+ this._setAttribution = text => attributionBox.set(text);
3915
3954
 
3916
3955
  if (this._legendHtml && el.parentElement) {
3917
3956
  const legendEl = document.createElement('div');
@@ -4083,6 +4122,11 @@
4083
4122
  mapType: (id) => engineApi.setMapType(id),
4084
4123
  // flat's getMapTypeId(): the basemap type name in use
4085
4124
  getMapTypeId: () => String(builder.mapOptions.mapType || ''),
4125
+ // flat's attribution (htmlgui.js htmlgui_set/getAttributionString)
4126
+ setAttribution: (text) => { builder.attribution(text); return engineApi; },
4127
+ // flat's map handle ixmaps.map().attribution(text) (htmlgui_flat.js 1381)
4128
+ attribution: (text) => engineApi.setAttribution(text),
4129
+ getAttribution: () => attributionBox.get(),
4086
4130
  setBasemapOpacity: (delta, mode) => {
4087
4131
  _basemapOpacity = mode === 'relative'
4088
4132
  ? Math.max(0, Math.min(1, _basemapOpacity + (parseFloat(delta) || 0)))
@@ -7269,7 +7313,10 @@
7269
7313
  const geometry = { type: 'Point', coordinates: [ll.lng, ll.lat] };
7270
7314
  const hasClass = cell.first.properties.classValue !== undefined;
7271
7315
  let props = cell.n > 1
7272
- ? { cluster: true, point_count: cell.n, value: cell.value, ...(hasClass ? { classValue: cell.classValue } : {}) }
7316
+ // titleRaw: the cell's first record — flat titles an aggregated
7317
+ // item by it (itemTitle)
7318
+ ? { cluster: true, point_count: cell.n, value: cell.value, ...(hasClass ? { classValue: cell.classValue } : {}),
7319
+ titleRaw: cell.first.properties.raw || cell.first.properties }
7273
7320
  : cell.first.properties;
7274
7321
  if (cell.series && cell.n > 1) props = Object.assign({}, props, { series: cell.series });
7275
7322
  // a field aggregation's cell key (the field value): the categories of
@@ -7335,6 +7382,7 @@
7335
7382
  let cell = cells.get(key);
7336
7383
  if (!cell) { cell = { sumX: 0, sumY: 0, n: 0, cx: snapped.x, cy: snapped.y, counts: new Array(n).fill(0), recordCounts: new Array(n).fill(0), classTotal: undefined }; cells.set(key, cell); }
7337
7384
  cell.sumX += p.x; cell.sumY += p.y; cell.n++;
7385
+ if (!cell.titleRaw) cell.titleRaw = f.properties.titleRaw || f.properties.raw || null;
7338
7386
  cell.counts[f.properties.cat] += f.properties.value;
7339
7387
  if (f.properties.classValue !== undefined) cell.classTotal = (cell.classTotal || 0) + f.properties.classValue;
7340
7388
  // point_count is the aggregated-record count of a multi-record
@@ -7348,7 +7396,8 @@
7348
7396
  const ll = atCellCenter && cellPx ? worldPixelToLngLat(cell.cx, cell.cy, zoom) : worldPixelToLngLat(cell.sumX / cell.n, cell.sumY / cell.n, zoom);
7349
7397
  return {
7350
7398
  geometry: { type: 'Point', coordinates: [ll.lng, ll.lat] },
7351
- properties: { counts: cell.counts, total: cell.counts.reduce((a, c) => a + c, 0), recordCounts: cell.recordCounts, ...(cell.classTotal !== undefined ? { classTotal: cell.classTotal } : {}) }
7399
+ properties: { counts: cell.counts, total: cell.counts.reduce((a, c) => a + c, 0), recordCounts: cell.recordCounts, ...(cell.classTotal !== undefined ? { classTotal: cell.classTotal } : {}),
7400
+ ...(cell.titleRaw ? { titleRaw: cell.titleRaw } : {}) }
7352
7401
  };
7353
7402
  });
7354
7403
  }
@@ -8253,14 +8302,31 @@
8253
8302
  // only available for an individual, unaggregated point — an
8254
8303
  // aggregated/grouped bubble has no single underlying record to
8255
8304
  // tabulate, so it resolves to an empty string there.
8305
+ // an item's title as flat gives it (maptheme.js 11838-11886): the title
8306
+ // field of its record — of an aggregated item its first record; an
8307
+ // aggregated grid cell (no aggregation field) of more than 3 records,
8308
+ // or without a title, is titled "(n)"
8309
+ _itemTitle(props) {
8310
+ const raw = props.raw || props.titleRaw;
8311
+ const title = this.binding.title && raw && raw[this.binding.title] != null ? String(raw[this.binding.title]) : '';
8312
+ const aggregated = !!(props.counts || props.cluster);
8313
+ if (aggregated && this._clusterRadiusPx && !this._clusterField) {
8314
+ const n = groupRecordCount(props);
8315
+ if (!title || n > 3) return '(' + n + ')';
8316
+ }
8317
+ return title;
8318
+ }
8319
+
8256
8320
  buildTooltipHtml(object) {
8257
- if (!this.meta.tooltip) return null;
8321
+ // no .meta({tooltip}): flat's default template (tooltip_mustache.js 394)
8322
+ // — the theme title, the item's title and its chart
8323
+ const template = this.meta.tooltip || FLAT_DEFAULT_TOOLTIP;
8258
8324
  if (!global.Mustache) {
8259
- console.warn('[ixmaps-gl] .meta({tooltip}) is set but Mustache.js is not loaded — ' +
8325
+ console.warn('[ixmaps-gl] tooltips need Mustache.js, which is not loaded — ' +
8260
8326
  'include https://unpkg.com/mustache@4.2.0/mustache.min.js before ixmaps-gl.js.');
8261
8327
  return null;
8262
8328
  }
8263
- return global.Mustache.render(this.meta.tooltip, this._buildTooltipContext(object));
8329
+ return global.Mustache.render(template, this._buildTooltipContext(object));
8264
8330
  }
8265
8331
 
8266
8332
  _buildTooltipContext(object) {
@@ -8281,9 +8347,7 @@
8281
8347
  label: isGroup ? '' : (this.categoryDisplayLabels ? (this.categoryDisplayLabels[props.cat] || '') : ''),
8282
8348
  // flat: the item's title is its titlefield value (binding title/
8283
8349
  // titlefield, e.g. the comune name); else the class label
8284
- title: isGroup ? '' : (this.binding.title && props.raw && props.raw[this.binding.title] != null
8285
- ? String(props.raw[this.binding.title])
8286
- : (this.categoryDisplayLabels ? (this.categoryDisplayLabels[props.cat] || '') : '')),
8350
+ title: this._itemTitle(props) || (isGroup ? '' : (this.categoryDisplayLabels ? (this.categoryDisplayLabels[props.cat] || '') : '')),
8287
8351
  count: isGroup ? (props.recordCounts ? props.recordCounts.reduce((a, c) => a + c, 0) : '') : 1,
8288
8352
  class: isGroup ? '' : props.cat
8289
8353
  }
@@ -8316,11 +8380,16 @@
8316
8380
  // breakdown, one line per present category, each with its own legend
8317
8381
  // color swatch and its OWN value (never summed across categories).
8318
8382
  _renderItemChartHtml(props) {
8383
+ const unitOf = this.style.units ? ' ' + this.style.units : '';
8384
+ // an AGGREGATE CATEGORICAL choropleth polygon: its sum per category
8385
+ if (props.parts) {
8386
+ const labels = this.categoryDisplayLabels || this.categoryLabels || [];
8387
+ return tooltipTable(props.parts.map((v, i) => (v > 0 ? { rgb: this.categoryColorsRgb[i], label: labels[i] || '(n/d)', value: this._formatTooltipValue(v) + unitOf } : null)).filter(Boolean));
8388
+ }
8319
8389
  if (this.flags.has('CHOROPLETH') && (this.flags.has('DOMINANT') || this.flags.has('COMPOSECOLOR')) && props.raw) {
8320
8390
  return this._renderMultiFieldBarsHtml(props.raw);
8321
8391
  }
8322
- const swatch = rgb => `<span style="display:inline-block;width:0.7em;height:0.7em;border-radius:50%;background:rgb(${rgb.join(',')});margin-right:0.4em;vertical-align:middle"></span>`;
8323
- const unit = this.style.units ? ' ' + this.style.units : '';
8392
+ const unit = unitOf;
8324
8393
 
8325
8394
  // PLOT|GRIDSIZE curve cells: properties.values is an array parallel
8326
8395
  // to _plotCategories() (e.g. one FERITI sum per year), not a single
@@ -8330,10 +8399,10 @@
8330
8399
  if (Array.isArray(props.values)) {
8331
8400
  const categories = this._isItemPlot() ? this._itemPlotLabels() : this._plotCategories();
8332
8401
  const labels = this._isItemPlot() ? categories : Array.isArray(this.style.label) ? this.style.label.map(String) : categories;
8333
- return props.values.map((v, i) => {
8402
+ return tooltipTable(props.values.map((v, i) => {
8334
8403
  if (v == null || isNaN(v)) return null;
8335
- return `<div>${labels[i] || categories[i] || i}: ${this._formatTooltipValue(v)}${unit}</div>`;
8336
- }).filter(Boolean).join('');
8404
+ return { rgb: null, label: labels[i] || categories[i] || i, value: this._formatTooltipValue(v) + unit };
8405
+ }).filter(Boolean));
8337
8406
  }
8338
8407
 
8339
8408
  if (props.counts) {
@@ -8352,19 +8421,17 @@
8352
8421
  // zoom until something resets it. Root cause of the "trackpad
8353
8422
  // zoom sometimes just stops updating" reports.
8354
8423
  const labels = this.categoryDisplayLabels || this.categoryLabels || [];
8355
- return labels.map((label, i) => {
8424
+ return tooltipTable(labels.map((label, i) => {
8356
8425
  if (!(props.counts[i] > 0)) return null;
8357
- const rgb = this.categoryColorsRgb[i];
8358
- return `<div>${swatch(rgb)}${label || '(n/d)'}: ${this._formatTooltipValue(props.counts[i])}${unit}</div>`;
8359
- }).filter(Boolean).join('');
8426
+ return { rgb: this.categoryColorsRgb[i], label: label || '(n/d)', value: this._formatTooltipValue(props.counts[i]) + unit };
8427
+ }).filter(Boolean));
8360
8428
  }
8361
8429
  if (props.cat != null) {
8362
8430
  const label = this.categoryDisplayLabels ? (this.categoryDisplayLabels[props.cat] || '') : '';
8363
8431
  const rgb = this.categoryColorsRgb ? this.categoryColorsRgb[props.cat] : null;
8364
8432
  // no bound size field (e.g. a plain DOT|CATEGORICAL layer) -> just
8365
8433
  // the category label, no ": undefined" value suffix
8366
- const valueSuffix = this._hasValue(props.value) ? `: ${this._formatTooltipValue(props.value)}${unit}` : '';
8367
- return `<div>${rgb ? swatch(rgb) : ''}${label}${valueSuffix}</div>`;
8434
+ return tooltipTable([{ rgb, label, value: this._hasValue(props.value) ? this._formatTooltipValue(props.value) + unit : '' }]);
8368
8435
  }
8369
8436
  return '';
8370
8437
  }
@@ -8425,7 +8492,7 @@
8425
8492
  .filter(k => k !== 'geometry')
8426
8493
  .map(k => `<tr><td style="padding:0 0.6em 0 0;color:#888">${k}</td><td>${this._isNumericValue(raw[k]) ? this._formatTooltipValue(raw[k]) : raw[k]}</td></tr>`)
8427
8494
  .join('');
8428
- return `<table style="font-size:0.85em;border-collapse:collapse">${rows}</table>`;
8495
+ return `<table style="font-size:0.95em;border-collapse:collapse">${rows}</table>`;
8429
8496
  }
8430
8497
 
8431
8498
  _isNumericValue(v) {
@@ -8691,7 +8758,7 @@
8691
8758
  geometry: f.geometry,
8692
8759
  properties: {
8693
8760
  // no part above 0: no item in flat, not painted
8694
- value: agg ? agg.value : undefined, raw: agg ? f.properties : {}, cat: agg ? agg.index : null,
8761
+ value: agg ? agg.value : undefined, raw: agg ? f.properties : {}, cat: agg ? agg.index : null, parts: agg ? agg.parts : undefined,
8695
8762
  dopacityAlpha: agg && dopacityActive ? dominantDopacityAlpha(this.style, this.flags, agg.value, aggMaxA[agg.index]) : null
8696
8763
  }
8697
8764
  };
@@ -9720,7 +9787,8 @@
9720
9787
  return {
9721
9788
  geometry: f.geometry,
9722
9789
  properties: { counts: this._oneHot(f.properties.cat, f.properties.point_count), total: f.properties.value,
9723
- ...(f.properties.classValue !== undefined ? { classTotal: f.properties.classValue } : {}) }
9790
+ ...(f.properties.classValue !== undefined ? { classTotal: f.properties.classValue } : {}),
9791
+ ...(f.properties.titleRaw ? { titleRaw: f.properties.titleRaw } : {}) }
9724
9792
  };
9725
9793
  });
9726
9794
  return { individual, groups };
@@ -10091,9 +10159,7 @@
10091
10159
  const boxR = circular ? r * 1.1 + margin : r;
10092
10160
  let title = null;
10093
10161
  if (titleShown) {
10094
- const raw = d.properties.raw;
10095
- const n = groupRecordCount(d.properties);
10096
- const text = n > 3 ? '(' + n + ')' : (raw && raw[this.binding.title] != null ? String(raw[this.binding.title]) : '');
10162
+ const text = this._itemTitle(d.properties);
10097
10163
  if (text) {
10098
10164
  const f = titleFontPx;
10099
10165
  const lines = wrapTitleLines(text, f, boxR * 2).split('\n');
@@ -10761,6 +10827,29 @@
10761
10827
 
10762
10828
  // flat's page hook ixmaps.htmlgui_onNewTheme(szId), called when a theme
10763
10829
  // is created (maptheme.js 927) — with the theme's id (its name)
10830
+ // flat's tooltip for a theme without its own template (ui/js/tools/
10831
+ // tooltip_mustache.js 394)
10832
+ // tooltip_mustache.js 394) — the same parts, structured: the theme title
10833
+ // as h2, the item title as h3, the chart (tooltipTable) below
10834
+ // (inline styles throughout: a page's own h2 / table / td rules must not
10835
+ // restyle the tooltip)
10836
+ const FLAT_DEFAULT_TOOLTIP = "<h2 style='margin:0 0 0.15em;padding:0;font-size:1.25em;font-weight:600;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis'>{{theme.title}}</h2>"
10837
+ + "{{#theme.item.title}}<h3 style='margin:0 0 0.4em;padding:0;font-size:1.1em;font-weight:600;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis'>{{theme.item.title}}</h3>{{/theme.item.title}}"
10838
+ + "{{theme.item.chart}}";
10839
+ // {{theme.item.chart}} as a table: per row a color swatch (or none), the
10840
+ // label (left, one line) and the value (right-aligned)
10841
+ function tooltipTable(rows) {
10842
+ if (!rows.length) return '';
10843
+ const swatch = rgb => (rgb ? `<span style="display:inline-block;width:0.75em;height:0.75em;border-radius:50%;background:rgb(${rgb.slice(0, 3).join(',')})"></span>` : '');
10844
+ const td = 'border:none;background:transparent;font:inherit;color:inherit;line-height:1.15;';
10845
+ return '<table style="border-collapse:collapse;border:none;background:transparent;margin:0.2em 0;font-size:1em;line-height:1.15">'
10846
+ + rows.map(r => '<tr>'
10847
+ + `<td style="${td}padding:0.05em 0.4em 0.05em 0;vertical-align:middle">${swatch(r.rgb)}</td>`
10848
+ // one line: a long label ends in an ellipsis (full text as its title)
10849
+ + `<td style="${td}padding:0.05em 0.8em 0.05em 0;text-align:left"><span title="${escapeHtml(String(r.label).replace(/<[^>]*>/g, ''))}" style="display:inline-block;max-width:13em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;vertical-align:bottom">${r.label}</span></td>`
10850
+ + `<td style="${td}padding:0.05em 0;text-align:right;white-space:nowrap">${r.value}</td></tr>`).join('')
10851
+ + '</table>';
10852
+ }
10764
10853
  function themeIdOf(rt) {
10765
10854
  return (rt.style && rt.style.name) || (rt.meta && rt.meta.name) || rt.name;
10766
10855
  }
@@ -10774,7 +10863,7 @@
10774
10863
  // htmlgui_flat.js). Its methods act on the last built map; called while
10775
10864
  // the map is still building, they run once it is ready.
10776
10865
  const MAP_HANDLE_METHODS = ['replace', 'add', 'remove', 'removeTheme', 'replaceTheme', 'changeThemeStyle', 'setThemeStyle',
10777
- 'refreshTheme', 'setBasemapOpacity', 'setMapType', 'setMapTypeId', 'mapType', 'resize', 'view', 'options', 'layer', 'loadProject', 'require', 'setThemeVisible'];
10866
+ 'refreshTheme', 'setBasemapOpacity', 'setMapType', 'setMapTypeId', 'mapType', 'resize', 'view', 'options', 'layer', 'loadProject', 'require', 'setThemeVisible', 'attribution'];
10778
10867
  const _mapHandle = {};
10779
10868
  for (const m of MAP_HANDLE_METHODS) {
10780
10869
  _mapHandle[m] = (...args) => {
@@ -11375,6 +11464,10 @@
11375
11464
  // handle's methods on the last built map
11376
11465
  getCenter: () => (_lastMapApi && _lastMapApi.map ? _lastMapApi.map.getCenter() : null),
11377
11466
  getMapTypeId: () => (_lastMapApi && _lastMapApi.getMapTypeId ? _lastMapApi.getMapTypeId() : ''),
11467
+ // flat's attribution: ixmaps.setAttribution(text) and its htmlgui_ pair
11468
+ setAttribution: (text) => { if (_lastMapApi && _lastMapApi.setAttribution) _lastMapApi.setAttribution(text); },
11469
+ htmlgui_setAttributionString: (text) => { if (_lastMapApi && _lastMapApi.setAttribution) _lastMapApi.setAttribution(text); },
11470
+ htmlgui_getAttributionString: () => (_lastMapApi && _lastMapApi.getAttribution ? _lastMapApi.getAttribution() : ''),
11378
11471
  changeThemeStyle: (szMap, szId, szStyle, szFlag) => _mapHandle.changeThemeStyle(szId, szStyle, szFlag),
11379
11472
  removeTheme: (szMap, szId) => _mapHandle.remove(szId),
11380
11473
  setBasemapOpacity: (szMap, nOpacity, szMode) => _mapHandle.setBasemapOpacity(nOpacity, szMode),
@@ -11401,7 +11494,7 @@
11401
11494
  // test-only: lets test/unit/*.test.mjs call pure internals directly (the
11402
11495
  // engine runs in a Node vm there); deliberately NOT on the ixmaps object
11403
11496
  global.__ixmapsGlInternals = {
11404
- IXMAPS_GL_VERSION, legendRowLabels, scaleDenom, cssColorAlpha, aggregatedCategoricalClass, dominantDopacityAlpha, isAggregatedCategoricalChoropleth, computeBubblePackLayout, isolatedBubblePackLayout, sequenceLayout,
11497
+ IXMAPS_GL_VERSION, tooltipTable, legendRowLabels, scaleDenom, cssColorAlpha, aggregatedCategoricalClass, dominantDopacityAlpha, isAggregatedCategoricalChoropleth, computeBubblePackLayout, isolatedBubblePackLayout, sequenceLayout,
11405
11498
  normalizeTheme, projectThemeToDefinition, withoutProjectCode, groupRecordCount, resolveBasemapStyleUrl, resolveMapTypeColor, LayerBuilder, LayerRuntime, typeStyleNumbers, styleNum,
11406
11499
  resolveScriptUrl, isTrustedScriptUrl, loadProcessingScript, loadBrokerData, applyBrokerThemePatch, makeBrokerTheme,
11407
11500
  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.10",
3
+ "version": "0.2.11",
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",