ixmaps-gl 0.2.33 → 0.2.34
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 +145 -5
- package/ixmaps-gl.js +26 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -114,8 +114,8 @@ page. It currently implements:
|
|
|
114
114
|
`window.__setFacetFilter`) for building filterable sidebars
|
|
115
115
|
- Globe (orthographic) projection alongside flat Mercator
|
|
116
116
|
|
|
117
|
-
Not yet implemented: the `QUAD`/`WAFFLE` base types.
|
|
118
|
-
|
|
117
|
+
Not yet implemented: the `QUAD`/`WAFFLE` base types. SYMBOL shapes: the built-in ones and image-URL
|
|
118
|
+
symbols are described under [Symbols](#symbols).
|
|
119
119
|
The top-of-file comment in [`ixmaps-gl.js`](./ixmaps-gl.js) is the single source of truth
|
|
120
120
|
for scope; this list only summarises it.
|
|
121
121
|
|
|
@@ -329,12 +329,152 @@ so zoom-dependent page state (e.g. a basemap opacity ramp) changes in step with
|
|
|
329
329
|
hooks) aren't supported. `test/project-report.mjs` shows which themes
|
|
330
330
|
of your own project files gl can render.
|
|
331
331
|
|
|
332
|
+
### Symbols
|
|
333
|
+
|
|
334
|
+
`CHART|SYMBOL` draws one symbol per item (per category with `CATEGORICAL`, per part with `SEQUENCE`).
|
|
335
|
+
`style.symbols` names them, by color class: class `i` (the `i`-th entry of `values`, else the range
|
|
336
|
+
class) takes `symbols[i]`, and the last entry repeats when the array is shorter than the classes.
|
|
337
|
+
|
|
338
|
+
- **Built-in shapes:** `circle` (default), `square`, `diamond` (= `carot`), `triangle`, `roundrect`,
|
|
339
|
+
`hexagon`, `cross`, `label`, `empty`. An unknown name draws a circle.
|
|
340
|
+
- **Image symbols:** a URL ending in `.svg`, `.png`, `.jpg` or `.jpeg` (a query or `#` may follow), or a
|
|
341
|
+
`data:image/svg+xml|png|jpeg` URI. The image is drawn as is, not tinted by the class color. An SVG is
|
|
342
|
+
fetched and rasterized once per URL (a `viewBox` is enough, it needs no width/height); a PNG/JPEG is
|
|
343
|
+
loaded by deck.gl. A page opened from `file://` cannot fetch symbol files: serve it over http(s) or use
|
|
344
|
+
`data:` URIs.
|
|
345
|
+
- **By record field:** `symbolfield: "type"` with `symbolvalues: ["a", "b"]` maps the value of that field
|
|
346
|
+
to `symbols[i]` by position, independent of the color class (for individual items, not aggregated cells).
|
|
347
|
+
|
|
348
|
+
```js
|
|
349
|
+
.type("CHART|SYMBOL|CATEGORICAL")
|
|
350
|
+
.style({ values: ["school", "shop"], colorscheme: ["#d9534f", "#428bca"],
|
|
351
|
+
symbols: ["icons/school.svg", "icons/shop.svg"] })
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### Zoom gates: `chartupper` and `chartlower`
|
|
355
|
+
|
|
356
|
+
`style.chartupper` / `style.chartlower` (`layerupper` / `layerlower` when unset) take a scale `"1:N"`. The
|
|
357
|
+
theme is **hidden when zoomed out beyond `chartupper`** (the scale denominator is above N) and **hidden when
|
|
358
|
+
zoomed in to `chartlower` or closer** (the scale denominator is at or below N). So `chartlower: "1:20000"`
|
|
359
|
+
shows the layer only while the view is zoomed out past 1:20000, and `chartupper: "1:20000"` only from there on
|
|
360
|
+
in. The scale is flat's, about `442913385 / 2^z` for the zoom `z` you give `.view()` on a Mercator map:
|
|
361
|
+
|
|
362
|
+
| zoom | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
|
|
363
|
+
|---|---|---|---|---|---|---|---|---|
|
|
364
|
+
| scale 1: | 432533 | 216266 | 108133 | 54067 | 27033 | 13517 | 6758 | 3379 |
|
|
365
|
+
|
|
366
|
+
`featureupper`/`featurelower` gate FEATURE themes the same way, `glowupper`/`glowlower` the glow halo.
|
|
367
|
+
|
|
368
|
+
### Aggregation grid and symbol size
|
|
369
|
+
|
|
370
|
+
`AGGREGATE` groups items into grid cells. The cell size is read, first match wins, from:
|
|
371
|
+
|
|
372
|
+
1. `aggregation` (flat's alias `aggregationscale`): scale/width pairs, e.g. `["1:1", "3px", "1:500000", "1px"]`,
|
|
373
|
+
the width of the last pair for which the view is zoomed out past `1:N` (the example gives 3 px, and 1 px once
|
|
374
|
+
the view is zoomed out past 1:500000); `"Npx"` is screen pixels, a bare number meters
|
|
375
|
+
2. `gridwidthpx`: screen pixels, constant over zoom (the recommended spelling for a fixed cell)
|
|
376
|
+
3. `gridwidth: "Npx"`: the same, as a string
|
|
377
|
+
4. `gridwidth: N` (a bare number): meters, the same cells at every zoom
|
|
378
|
+
5. none of them: no grid, items stay at their positions
|
|
379
|
+
|
|
380
|
+
With `CATEGORICAL|COUNT` and no size field the size of a symbol follows the number of records in its cell:
|
|
381
|
+
radius = `15 px × scale × (count / normalsizevalue)^(1/sizepow)`, scaled with the zoom (`objectscaling`) and
|
|
382
|
+
clamped to a minimum and maximum radius.
|
|
383
|
+
`normalsizevalue` is the count drawn at normal size, the dataset's maximum when unset. A `normalsizevalue` of
|
|
384
|
+
`30` therefore draws a single-record cell at 1/30 of the normal radius: pick it near the biggest cell you expect.
|
|
385
|
+
|
|
386
|
+
### Basemaps (`mapType`)
|
|
387
|
+
|
|
388
|
+
`mapType` is resolved in this order; a name that matches nothing falls back to the last two rules, so an
|
|
389
|
+
unknown flat name such as `VT_TONER_LITE` draws CARTO Positron:
|
|
390
|
+
|
|
391
|
+
| `mapType` | style |
|
|
392
|
+
|---|---|
|
|
393
|
+
| `ArcGIS - Topo` (`terrain`), `ArcGIS - Light Gray Base`, `ArcGIS - Ocean Basemap`, `ArcGIS - Hillshade` | Esri raster tiles |
|
|
394
|
+
| `CARTO - Voyager` (`VT_VOYAGER`), `CARTO - Voyager (no labels)` (`VT_VOYAGER_LIGHT`), `CARTO - Positron (no labels)`, `CARTO - Dark Matter (no labels)` | CARTO vector styles |
|
|
395
|
+
| `OpenFreeMap - Liberty`, `OpenFreeMap - Bright`, `OpenFreeMap - Positron` | OpenFreeMap vector styles |
|
|
396
|
+
| any name containing `dark`, `black`, `night` or `matter` | CARTO Dark Matter |
|
|
397
|
+
| everything else | CARTO Positron |
|
|
398
|
+
|
|
399
|
+
### Adding and replacing themes at runtime
|
|
400
|
+
|
|
401
|
+
`ixmaps.map().add(theme, "replace")` adds the theme the first time and swaps it on later calls, matched by
|
|
402
|
+
the theme id, `style.name` or `meta.name`. Give the theme a `name` in `.meta()`: the name passed to
|
|
403
|
+
`ixmaps.layer("name", …)` is not the id, and without one `"replace"` cannot find the old theme and adds a second. `ixmaps.map().remove(id)` removes it.
|
|
404
|
+
|
|
405
|
+
### Text on labels
|
|
406
|
+
|
|
407
|
+
`CHART|LABEL` (with `TEXTONLY` for the text alone) writes the item's value. A text field bound as the *value*
|
|
408
|
+
reads as 0, as in flat, and shows nothing visible; the console says so once. To show a text, as in flat, keep a
|
|
409
|
+
numeric `value` (or `size`) binding and name the text field in `style.valuefield`, or `"$title$"` for the
|
|
410
|
+
`title` binding:
|
|
411
|
+
|
|
412
|
+
```js
|
|
413
|
+
.binding({ geo: "geometry", value: "count" })
|
|
414
|
+
.type("CHART|LABEL|TEXTONLY|VALUES")
|
|
415
|
+
.style({ valuefield: "name" })
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
With `CATEGORICAL` the value field holds the categories and `valuefield` names the text. The text is drawn as a
|
|
419
|
+
single-color glyph with a halo, so an emoji comes out as a monochrome silhouette (seen in a headless Chromium
|
|
420
|
+
test): use image symbols (see Symbols) for colored icons.
|
|
421
|
+
|
|
422
|
+
### Runtime filtering
|
|
423
|
+
|
|
424
|
+
A theme's records can be filtered in place, without redefining it, with flat's `changeThemeStyle`:
|
|
425
|
+
|
|
426
|
+
```js
|
|
427
|
+
ixmaps.changeThemeStyle("mytheme", 'filter:WHERE "type" = "school"', "set");
|
|
428
|
+
ixmaps.changeThemeStyle("mytheme", "filter", "remove");
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
The same through the handles, `ixmaps.map().changeThemeStyle(id, style, flag)` and
|
|
432
|
+
`ixmaps.map().theme(id).changeStyle(style, flag)`; with `null` as the id every theme but the FEATURE ones is
|
|
433
|
+
changed. The expression is flat's filter grammar: `WHERE field op value` clauses joined by `AND` with `=`, `!=`,
|
|
434
|
+
`<>`, `<`, `>`, `<=`, `>=`, `BETWEEN … AND …`, `IN (…)`, `LIKE` and `NOT` (see `test/unit/filter.test.mjs`).
|
|
435
|
+
(`npm run runtime` in `test/` checks these calls in a browser.)
|
|
436
|
+
|
|
437
|
+
### Porting a flat page
|
|
438
|
+
|
|
439
|
+
How the calls of a flat page map to gl; "tested" means `test/runtime.mjs` runs it in a browser.
|
|
440
|
+
|
|
441
|
+
| flat | gl |
|
|
442
|
+
|---|---|
|
|
443
|
+
| `ixmaps.Map(div, opts, function (map) { … })` | the same; the callback gets the map builder (`view`, `options`, `layer`, `on`, …) |
|
|
444
|
+
| `var myMap = ixmaps.Map(div, opts).view(…)`, `myMap.then(map => …)` | the same, `myMap` is the builder and a thenable |
|
|
445
|
+
| `myMap.layer("name").data(…)…define()` | the same, before and after the map is built (tested) |
|
|
446
|
+
| `myMap.layer(theme)`, `myMap.layer(theme, "direct")` after the map is built | the same (tested); up to v0.2.33 these silently added nothing |
|
|
447
|
+
| `ixmaps.map().layer(theme)`, `ixmaps.map().add(theme, "replace")` | the same (tested) |
|
|
448
|
+
| `ixmaps.changeThemeStyle(id, 'filter:…', "set")` | the same (tested), see Runtime filtering |
|
|
449
|
+
| `map.getBounds()` | `ixmaps.map().getBounds()`, from v0.2.33 |
|
|
450
|
+
| `map.on(event, fn)` | `map.on` / `map.off`, see Map events |
|
|
451
|
+
| `ixmaps.hideTheme(id)` / `ixmaps.showTheme(id)` | not provided: `ixmaps.map().theme(id).hide()` / `.show()` or `ixmaps.map().setThemeVisible(id, bool)` |
|
|
452
|
+
| `ixmaps.flyTo(…)` | not provided: `ixmaps.map().view([lat, lng], zoom)` |
|
|
453
|
+
|
|
454
|
+
### `CHART|USER` draw function
|
|
455
|
+
|
|
456
|
+
`style.userdraw` names a function (`ixmaps[name]`, or a dotted path on `window`). It is called as
|
|
457
|
+
`fn(document, opt)` for each chart, after an optional `name_init(document, {target, theme})` once per draw.
|
|
458
|
+
`opt` holds `target` (the SVG group to draw into), `theme`, `value`, `values`, `size`, `maxSize`, `color`,
|
|
459
|
+
`class`, `flag`, `ccolor`, `item` (`{szColor, szLabel, szTitle, nCount, nSize}`) and `dbRecord` (the item's data
|
|
460
|
+
record, also the place to read the category). `size` is in flat's chart units, `maxSize` (300) being the
|
|
461
|
+
normal radius: one unit is `15 px × scale × zoom factor / 300`. The chart is drawn around 0,0.
|
|
462
|
+
|
|
463
|
+
### Headless, CI and offline
|
|
464
|
+
|
|
465
|
+
A headless Chromium has no GPU: start it with `--use-gl=swiftshader --enable-unsafe-swiftshader
|
|
466
|
+
--ignore-gpu-blocklist`. deck.gl then logs "GPU stall due to ReadPixels", which is harmless. The engine loads
|
|
467
|
+
`maplibre-gl@6.11.2` (module and css) and `deck.gl@9.4.0` and `mustache@4.2.0` from unpkg, `data.js` from
|
|
468
|
+
jsDelivr (`gh/gjrichter/data.js`), and on demand `d3-array@3.2.4` and `d3-contour@4.0.2` (jsDelivr) and the
|
|
469
|
+
`ixmaps-grammar` validator; mirror these to run offline. Page-level script and data URLs are not
|
|
470
|
+
configurable yet.
|
|
471
|
+
|
|
332
472
|
## Quick start
|
|
333
473
|
|
|
334
474
|
Load the engine from jsDelivr, pinned to a release tag (or `@main` for the latest commit):
|
|
335
475
|
|
|
336
476
|
```html
|
|
337
|
-
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.
|
|
477
|
+
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.34/ixmaps-gl.js"></script>
|
|
338
478
|
```
|
|
339
479
|
|
|
340
480
|
or from unpkg, the npm package [`ixmaps-gl`](https://www.npmjs.com/package/ixmaps-gl)
|
|
@@ -342,13 +482,13 @@ or from unpkg, the npm package [`ixmaps-gl`](https://www.npmjs.com/package/ixmap
|
|
|
342
482
|
browser script, which defines the global `ixmaps`):
|
|
343
483
|
|
|
344
484
|
```html
|
|
345
|
-
<script src="https://unpkg.com/ixmaps-gl@0.2.
|
|
485
|
+
<script src="https://unpkg.com/ixmaps-gl@0.2.34/ixmaps-gl.js"></script>
|
|
346
486
|
```
|
|
347
487
|
|
|
348
488
|
or use a local copy (`<script src="ixmaps-gl.js"></script>`). A minimal page:
|
|
349
489
|
|
|
350
490
|
```html
|
|
351
|
-
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.
|
|
491
|
+
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.34/ixmaps-gl.js"></script>
|
|
352
492
|
<div id="map_div" style="position:absolute;inset:0;"></div>
|
|
353
493
|
|
|
354
494
|
<script>
|
package/ixmaps-gl.js
CHANGED
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
// ---------------------------------------------------------------
|
|
101
101
|
// this engine's release (= package.json "version", checked by
|
|
102
102
|
// test/unit/version.test.mjs); ixmaps.glVersion, and logged once at start
|
|
103
|
-
const IXMAPS_GL_VERSION = '0.2.
|
|
103
|
+
const IXMAPS_GL_VERSION = '0.2.34';
|
|
104
104
|
|
|
105
105
|
const LIB_URLS = {
|
|
106
106
|
// Bumped 3.6.2 -> 5.x (2026-09-21, globe-projection compat fix): native
|
|
@@ -7628,7 +7628,12 @@ self.onmessage = e => {
|
|
|
7628
7628
|
// object form (a pre-built LayerBuilder, e.g. from the global
|
|
7629
7629
|
// ixmaps.layer(name, cb) factory): existing semantics, chain
|
|
7630
7630
|
// back on the MAP for further .layer(a).layer(b) calls.
|
|
7631
|
-
|
|
7631
|
+
// Once the map is built build() has consumed its queue, so queuing
|
|
7632
|
+
// here would add nothing, silently: define the theme on the live
|
|
7633
|
+
// map instead (flat's runtime overlay: myMap.layer(theme, "direct"),
|
|
7634
|
+
// the second argument is accepted and not needed)
|
|
7635
|
+
if (resolvedApi) resolvedApi.layer(nameOrBuilder);
|
|
7636
|
+
else builder.layer(nameOrBuilder);
|
|
7632
7637
|
return promise;
|
|
7633
7638
|
}
|
|
7634
7639
|
const lb = new LayerBuilder(nameOrBuilder);
|
|
@@ -17958,6 +17963,24 @@ self.onmessage = e => {
|
|
|
17958
17963
|
// the item color (stroke font/7, opacity 0.5).
|
|
17959
17964
|
// Both moved by the chart position (flatChartAlignOffset). The text is
|
|
17960
17965
|
// the hover target of TEXTONLY, the box of LABEL.
|
|
17966
|
+
// A text field bound as the LABEL's value scans to 0 (flat's __scanValue), so the labels are drawn at the smallest size
|
|
17967
|
+
// with the text "0" — as flat skips such items, nothing visible, and no message. Say how to show a text: flat's
|
|
17968
|
+
// style.valuefield (or "$title$"), with a numeric value or size binding. CATEGORICAL values are categories, not numbers.
|
|
17969
|
+
_warnTextValue(combined) {
|
|
17970
|
+
// looked at once per theme (this runs on every redraw): the first record with a value
|
|
17971
|
+
if (this._checkedTextValue || this.style.valuefield || this.flags.has('CATEGORICAL') || !this.binding.value) return;
|
|
17972
|
+
let sample;
|
|
17973
|
+
for (const d of combined) {
|
|
17974
|
+
const v = d.properties.raw && d.properties.raw[this.binding.value];
|
|
17975
|
+
if (v != null && String(v).trim() !== '') { sample = v; break; }
|
|
17976
|
+
}
|
|
17977
|
+
if (sample === undefined) return;
|
|
17978
|
+
this._checkedTextValue = true;
|
|
17979
|
+
if (!isNaN(scanValue(sample))) return;
|
|
17980
|
+
console.warn(`[ixmaps-gl] theme "${this.name}": LABEL value field "${this.binding.value}" holds text ("${String(sample).slice(0, 20)}"), ` +
|
|
17981
|
+
'which reads as 0 — as in flat the labels are not visible. To show a text keep a numeric value (or size) binding ' +
|
|
17982
|
+
'and name the text field in style.valuefield (or "$title$" for the title).');
|
|
17983
|
+
}
|
|
17961
17984
|
_buildLabelChartLayers(combined, zoom, liveZoom, sizeValueOf) {
|
|
17962
17985
|
const textOnly = this.flags.has('TEXTONLY');
|
|
17963
17986
|
const showValues = this.flags.has('VALUES') && !valuesHiddenByScale(this.style, zoom);
|
|
@@ -17972,6 +17995,7 @@ self.onmessage = e => {
|
|
|
17972
17995
|
const textOf = props => flatValueText(props.counts ? this._categoryValueRecord(dominant(props.counts)) : props.raw,
|
|
17973
17996
|
this.binding.title && props.raw ? props.raw[this.binding.title] : undefined, valueOf(props), this.style, this.flags, opts);
|
|
17974
17997
|
|
|
17998
|
+
this._warnTextValue(combined);
|
|
17975
17999
|
if (textOnly) {
|
|
17976
18000
|
const textRgb = textOverride ? hexOrNamedToRgb(textOverride) : [0, 0, 0];
|
|
17977
18001
|
const labels = [];
|
|
@@ -17981,12 +18005,6 @@ self.onmessage = e => {
|
|
|
17981
18005
|
if (!(fontSize > 0)) continue;
|
|
17982
18006
|
labels.push({ ...d, text: textOf(d.properties), fontSize, halo: itemRgb(d.properties), pixelOffset: alignOf(d) });
|
|
17983
18007
|
}
|
|
17984
|
-
// a text field read as the value/size scans to 0 (flat's __scanValue), so every label has size 0 and nothing is drawn
|
|
17985
|
-
if (!labels.length && combined.length && !this._warnedNoLabels) {
|
|
17986
|
-
this._warnedNoLabels = true;
|
|
17987
|
-
console.warn(`[ixmaps-gl] theme "${this.name}": LABEL|TEXTONLY drew no labels — the size of all ${combined.length} items is 0. ` +
|
|
17988
|
-
'A non-numeric (text) field bound as value/size reads as 0; bind a numeric size field.');
|
|
17989
|
-
}
|
|
17990
18008
|
// TextLayer's outlineColor is one per layer: a layer per halo color
|
|
17991
18009
|
const byHalo = new Map();
|
|
17992
18010
|
for (const l of labels) {
|