ixmaps-gl 0.1.0

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 (4) hide show
  1. package/LICENSE.txt +28 -0
  2. package/README.md +291 -0
  3. package/ixmaps-gl.js +9921 -0
  4. package/package.json +34 -0
package/LICENSE.txt ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Guenter Richter
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,291 @@
1
+ # ixmaps-gl
2
+
3
+ A from-scratch, config-driven map/theme engine that exposes the **same declarative
4
+ builder API** as the original [ixmaps](https://ixmaps.com) framework —
5
+
6
+ ```js
7
+ ixmaps.Map(id, options)
8
+ ixmaps.layer(name, layer => layer.data().binding().filter().type().style().meta())
9
+ ```
10
+
11
+ — but renders through **MapLibre GL JS + deck.gl** instead of ixmaps' own SVG/Leaflet
12
+ engine. The goal is drop-in compatibility: a real, unmodified ixmaps page should be
13
+ able to run against this engine by swapping only the `<script>` tag that loads it.
14
+
15
+ ## Why
16
+
17
+ ixmaps' declarative model (a page describes *what* to show — a data source, a field
18
+ binding, a chart type, a style — not *how* to draw it) is a good fit for a modern
19
+ WebGL rendering stack: MapLibre for basemap/vector-tile rendering and camera control,
20
+ deck.gl for large-scale, GPU-instanced point/polygon layers and clustering. This
21
+ project re-implements the ixmaps theme grammar on top of that stack rather than
22
+ patching the original SVG engine.
23
+
24
+ ## Status
25
+
26
+ This engine is under active, compatibility-driven development: every gap found by
27
+ pointing a real ixmaps page at it gets fixed **in the engine**, never by rewriting the
28
+ page. It currently implements:
29
+
30
+ - **FEATURE / FEATURES** — polygon/line rendering from GeoJSON/TopoJSON and the data.js geo
31
+ formats; `featureupper`/`featurelower` hide the layer outside their scales, and, as in flat, a
32
+ FEATURE theme out of scale loads its data only once it comes into scale (for that view);
33
+ flat's drop `shadow` (`shadowblur`/`shadowdx`/`shadowdy`, `maxshadow`, `shadowupper`/`shadowlower`),
34
+ approximated with translucent outlines since deck.gl has no blur
35
+ - **CHOROPLETH** — polygon fill from a bound value: single-field numeric range
36
+ (equal-interval, QUANTILE, NATURAL/Jenks breaks) or multi-field DOMINANT
37
+ (per-polygon argmax, plain/`PERCENTOFMEAN`/`DEVIATION`; with a `value100` the field means are
38
+ flat's pooled Σ field ÷ Σ value100) or COMPOSECOLOR (additive or
39
+ `SUBTRACTIVE` color blend); `HEADTAIL` breaks, `DENSITY` (value per km²), flat's
40
+ `ZEROISNOTVALUE`/`UNDEFINEDISNOTVALUE`; plus DOPACITY/DOPACITYMIN/DOPACITYMAX/DOPACITYMINMAX for
41
+ value- or density-driven fill opacity
42
+ - **CHART\|SYMBOL\|GLOW\|CATEGORICAL\|AGGREGATE\|COUNT\|RELOCATE\|VALUES** — the
43
+ bubble-map pipeline: AGGREGATE on ixmaps-flat's grid (hexagonal, or square with
44
+ `RECT`; cell width from `aggregation`/`gridwidth`/`gridwidthpx`, values summed per
45
+ cell, class breaks and legend from the aggregated cells), dynamic sizing, glow,
46
+ multi-point grouping, on-bubble value labels, `NORMALIZE`, chart boxes (`BOX`, `CIRCULARBOX`)
47
+ with the item title above or below (`TITLE`, `BOTTOMTITLE`), between `boxlower` and `boxupper`
48
+ - **CHART\|USER** — a page's own chart function (`style.userdraw`: `ixmaps.<name>(SVGDocument, opt)`
49
+ and its `_init`, as flat calls them), drawn in flat's chart units and shown as an icon at flat's size
50
+ - **DOT** — the simplest base symbol (fixed-radius, unclustered points)
51
+ - **PLOT** — a small line/area chart per item over its value fields (flat's per-item PLOT
52
+ geometry: first point at the item, `scale`/`rangescale`, FIXSIZE markers), or one per grid
53
+ cell with `GRIDSIZE` (a categorical field's series, e.g. one value per year)
54
+ - A native interactive legend (default bars, `SIMPLELEGEND`, `COMPACTLEGEND`,
55
+ `NOLEGEND`, `TEXTLEGEND`; light/dark color themes; corner or flat `align` option;
56
+ collapsible, collapsed by default on narrow/mobile screens). As flat's `#map-legend` it is one box:
57
+ the themes' legends one after the other in theme order, leaving out a theme hidden or out of scale;
58
+ a range theme of 5 classes or more without `label` gets flat's one-line color bar (with DOPACITY and an
59
+ alpha field, flat's three-row opacity grid); `style.label` names the classes; the opacity slider comes
60
+ with the type word `CHOROPLETH`, the size slider with `CHART`/`BUBBLE`/`DOT`, as in flat
61
+ - A standard facets API (`ixmaps.data.getFacets`/`showFacets`,
62
+ `window.__setFacetFilter`) for building filterable sidebars
63
+ - Globe (orthographic) projection alongside flat Mercator
64
+
65
+ Not yet implemented: `CATEGORICAL` choropleths, and the `QUAD`/`BEZIER`/`VECTOR`/
66
+ `PIE`/`DONUT`/`WAFFLE`/`BAR` base types and SYMBOL shape variants beyond
67
+ circle/square/diamond/triangle. See the top-of-file comment in
68
+ [`ixmaps-gl.js`](./ixmaps-gl.js) for the exact, currently-accurate scope note.
69
+
70
+ ### Page scripts, named data and the runtime API
71
+
72
+ A page's `.require(url)` scripts (page code, e.g. a user chart function) load in order before the
73
+ layers are built. `ixmaps.map()` is the map handle as in flat — its calls wait until the map is ready —
74
+ with `add(theme, flags)` / `replace` / `replaceTheme` / `remove`, `changeThemeStyle` for any style key,
75
+ `getZoom()` (flat's zoom), `resize()` and `setBasemapOpacity`; the page's `htmlgui_onNewTheme(id)` and
76
+ `htmlgui_onZoomAndPan()` (also once on load) are called (as in flat, a theme swapped in with `replace` comes last
77
+ in the legend, while FEATURE and CHOROPLETH shapes always draw under the charts), and `map`, `getZoom` and flat's
78
+ `formatValue` exist as globals. `.data({name})` without a URL waits for data of that name: from
79
+ `ixmaps.setExternalData(data, {name})`, or from a broker — `.data({query, name, type: "ext"})`
80
+ registers the query function as `ixmaps[name]` and calls it with flat's options (`ext`, `theme`,
81
+ `setData`); a theme reloads when new data of its name arrives. Charts on a FEATURE layer are placed
82
+ as in flat: joined by `lookup` (`lookupdigits`, `lookuptonumber`, `lookuptoupper`), the first shape of
83
+ an id wins, a polygon's position is flat's shape center (the vertex mean in Mercator), a theme on
84
+ `"a|b"` is placed on each layer (with `DIFFERENCE`, on the last). `aggregationscale` (alias of
85
+ `aggregation`) picks px, meters or a grouping field by scale on flat's grid origin; `chartupper`/
86
+ `chartlower` (else `layerupper`/`layerlower`) hide a theme outside their scales; `.filter()` takes
87
+ flat's grammar (`WHERE a > 5 AND b NOT x`, `LIKE`, `IN`, `BETWEEN`, `$field$`, or a plain regex).
88
+
89
+ ### Zoom levels
90
+
91
+ Zoom numbers in the ixmaps API are ixmaps-flat's, i.e. Leaflet's (256px tiles):
92
+ `.view({center, zoom})`, `view([lat, lng], zoom)`, the `zoom` of a project map
93
+ (`loadProject`/`setProjectJSON`) and the `zoom` `getProjectString()` returns. MapLibre
94
+ works on 512px tiles, so ixmaps-gl shows ixmaps zoom *z* at MapLibre zoom *z* − 1 — the
95
+ same area a flat page shows. Map scales (`aggregation`, `valueupper`, `featureupper`/`featurelower`
96
+ and `boxupper`/`boxlower` thresholds, `normalSizeScale`) are computed from the zoom that is
97
+ actually displayed. Code that talks
98
+ to the MapLibre map directly (`api.map.getZoom()`, `jumpTo`) sees MapLibre zooms.
99
+
100
+ ### Theme normalization
101
+
102
+ Every layer definition passes through one pure function, `normalizeTheme()`, before any
103
+ renderer sees it. Its input has real ixmaps-flat's theme-definition shape (`{layer, data,
104
+ binding, style: {type, filter, title, …}, meta}` — the same shape as a theme in a flat
105
+ project JSON), and every alias rule lives there: `BUBBLE` implies `SYMBOL`, `geo` is read
106
+ as `position`, `.title()` is the legend-title fallback, and all of real ixmaps-flat's
107
+ binding aliases resolve to their targets (`values`/`fields`/`field` → `value`,
108
+ `sizefield` → `size`, `itemfield` → `id`, `text`/`valuefield` → the value-label field, …),
109
+ also when given as a style key (`.style({sizefield})` ≡ `.binding({size})`, as in flat).
110
+ flat's single lookup field (`geo`, `position`, `lookup`, `georef`, `lookupfield`, …) is read
111
+ by flat's own rule: `"lat|lon"` → points; `"geometry"` or GeoJSON/TopoJSON data → the
112
+ features' own geometry; any other single field on tabular data → a join key against the
113
+ same-named FEATURE layer — so a flat-style choropleth `.binding({geo: "code"})` works.
114
+ And as in flat, `.meta()` is merged into style first (meta wins): a style property given in
115
+ `.meta()` applies, and the meta keys (`title`, `tooltip`, `name`, `snippet`, `description`)
116
+ work when given in `.style()` too. The layer builder has all of flat's methods, including
117
+ `.field()`, `.field100()`, `.geo()`, `.lookup()`, `.encoding()`, `.query()`, `.process()` and
118
+ `.json()`; each writes the slot flat writes. Repeated `.binding()`, `.style()` and `.meta()` calls merge, as in flat
119
+ (a later value for the same key wins). The style keys read as numbers (`fillopacity`, `linewidth`, `scale`, `classes`,
120
+ …) are typed here once: `"0.8"` becomes `0.8`, also in runtime style changes (`setThemeStyle`, the legend sliders),
121
+ while anything that isn't a plain number (`"auto"`, `"12px"`) stays as given.
122
+ A page's own data processing function (`.data({process})` or `.process()`, as a function or
123
+ its `toString()`) runs like in flat: after loading, it gets the data as a data.js Table
124
+ (`column()`, `addColumn()`, …) and returns it, or nothing to keep the changed table.
125
+ The alias table is generated from the shared grammar (`test/sync-grammar.mjs`); targets
126
+ this engine doesn't implement yet (`colorfield`, `timefield`, `titlefield`, …) are resolved
127
+ but unused. It never mutates the page's own objects. Unit tests: `cd test && npm run unit`.
128
+
129
+ ### World copies
130
+
131
+ At an extreme zoom-out MapLibre repeats the world side by side, and the themes are repeated on
132
+ every copy (flat repeats only its basemap tiles). `.options({worldcopies: false})` draws the
133
+ world once, but MapLibre then also stops the zoom-out where the world gets narrower than the
134
+ window.
135
+
136
+ ### Data formats and data.js
137
+
138
+ Like flat, ixmaps-gl loads the real [data.js](https://github.com/gjrichter/data.js) by default
139
+ (from flat's CDN URL, alongside MapLibre and deck.gl), unless the page has it already;
140
+ `.options({datajs: url})` loads another build, `.options({datajs: false})` none. It reads every
141
+ `.data({url, type})` format data.js knows — `csv`, `json`, `jsondb`, `jsonstat`, `ndjson`/`jsonl`,
142
+ `rss`, `kml`, `gml`, `geobuf`, `pbf`, `flatgeobuf`/`fgb`, `geopackage`/`gpkg`, `parquet` — and hands
143
+ the table to `.data({process})`, `.data({query})` and project scripts, as flat does. A data.js geo
144
+ format comes as a table with a `geometry` column; `.binding({geo: "geometry"})` turns it into
145
+ points, lines or polygons. GeoJSON and TopoJSON are read by ixmaps-gl itself (features directly),
146
+ and without data.js CSV falls back to ixmaps-gl's own parser (same rows, ~10 % faster on 1.2M rows).
147
+
148
+ ### Loading ixmaps-flat project files
149
+
150
+ `map.loadProject(src, flags)` (and the global `ixmaps.loadProject` / `ixmaps.setProjectJSON`)
151
+ loads a real ixmaps-flat project — an object, a JSON string or a URL — the way flat's own
152
+ `setProjectJSON` does: the map part (projection from flat's map file, `center`/`zoom`,
153
+ `options`) and then the themes, in order; without `add` the first theme clears the existing
154
+ ones, `replace` swaps themes by `style.name`/`meta.name`, `themeonly`/`maponly`/`keepview`
155
+ work as in flat. Themes in flat's older shape (data source in `style.dbtable*`, value field at
156
+ the top level) are translated first. It resolves to `{ themes, skipped, notes }`: a theme
157
+ that can't load is skipped with a warning, the rest still load. **Code a project names is not
158
+ run by default** — `required` scripts, `ext` data scripts and a project's `process` functions are
159
+ reported, not executed (only a page's own `.data({process})` runs) — and the basemap isn't switched.
160
+
161
+ A page can opt in to a project's **scripts** (`data.ext`) for script URLs under prefixes it lists —
162
+ no built-in prefixes, and a project file's own `options.trustedscripts` is ignored. Both of flat's
163
+ script contracts work: a **processing script** on a loaded file (`ixmaps.<data.name>.after` /
164
+ `.process(table, options)`), and a **broker** (`data.type: "ext"`), which loads the data itself in
165
+ `ixmaps.<data.name>(theme, options)` and hands it over with `ixmaps.setExternalData(data, {type, name})`:
166
+
167
+ ```js
168
+ ixmaps.Map("map_div", {...}).options({
169
+ trustedscripts: ["https://gjrichter.github.io/viz/", "https://raw.githubusercontent.com/gjrichter/"]
170
+ })
171
+ ```
172
+
173
+ Prefixes match whole path segments; relative script paths resolve against the page. Data is parsed
174
+ by data.js, and a script runs right before its own theme loads. The
175
+ theme properties a broker sets from its data (flat's `szFields`, `szField100`, `szSnippet`,
176
+ `szTitle`, `szLabelA`, `setProperties()`, …) are applied to the theme, where flat's own style keys
177
+ would put them. As in flat, a broker whose function the page itself already defines
178
+ (`ixmaps.<name>`, page code — no opt-in needed) is called directly, and gets `data.ext` (or the
179
+ `data.url`) as `options.ext`, its data URL; only otherwise is `data.ext` loaded as the broker's
180
+ script. For brokers that query by view, like flat's bbox data providers, ixmaps-gl has flat's
181
+ `ixmaps.getBoundingBox()`, `ixmaps.refreshTheme(id)` (loads the theme's data again, in place),
182
+ `ixmaps.setTitle(html)`/`setTitleBox(text, color)`, `ixmaps.getThemeObj(id).fVisible` (false for a
183
+ FEATURE theme out of scale) and calls a page's `ixmaps.htmlgui_onZoomAndPan(zoom)` after each zoom
184
+ or pan. Scripts that reach further into flat's internals (`ixmaps.parentApi`, other `htmlgui_*`
185
+ hooks) aren't supported. `test/project-report.mjs` shows which themes
186
+ of your own project files gl can render.
187
+
188
+ ## Quick start
189
+
190
+ Load the engine from jsDelivr, pinned to a release tag (or `@main` for the latest commit):
191
+
192
+ ```html
193
+ <script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.1.0/ixmaps-gl.js"></script>
194
+ ```
195
+
196
+ or use a local copy (`<script src="ixmaps-gl.js"></script>`). A minimal page:
197
+
198
+ ```html
199
+ <script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.1.0/ixmaps-gl.js"></script>
200
+ <div id="map_div" style="position:absolute;inset:0;"></div>
201
+
202
+ <script>
203
+ var __theme = ixmaps.layer("my_theme", layer => layer
204
+ .data({ url: "data.topojson.gz", type: "topojson" })
205
+ .binding({ position: "geometry", value: "CATEGORY_FIELD", size: "SIZE_FIELD" })
206
+ .type("CHART|SYMBOL|GLOW|CATEGORICAL|AGGREGATE|COUNT|RELOCATE|VALUES")
207
+ .style({ colorscheme: ["#ddbb22", "#0066cc", "#ff0088"] })
208
+ .meta({ title: "My theme", tooltip: "<b>{{theme.title}}</b><br>{{theme.item.chart}}" })
209
+ );
210
+
211
+ ixmaps.Map("map_div", { mapType: "VT_BRIGHT_LIGHT", mode: "pan", legend: "open" })
212
+ .view({ center: { lat: 45.47, lng: 9.19 }, zoom: 10 })
213
+ .layer(__theme);
214
+ </script>
215
+ ```
216
+
217
+ See [`demo_accidents.html`](./stage/demo_accidents.html) for a minimal working page copied
218
+ verbatim (config included) from a real ixmaps page — the only change is which script
219
+ provides `ixmaps.layer`/`ixmaps.Map`.
220
+
221
+ ## Grammar validation (opt-in)
222
+
223
+ ixmaps-gl can check every theme definition against the shared ixmaps grammar
224
+ ([ixmaps-grammar](https://github.com/gjrichter/ixmaps-grammar), extracted from the real
225
+ ixmaps-flat sources) and report — once per keyword, in the browser console — typos and
226
+ features this engine doesn't implement:
227
+
228
+ ```text
229
+ [ixmaps-gl validate] error unknown-style-key — layer "points": unknown style key "fillOpacity" — did you mean "fillopacity"?
230
+ [ixmaps-gl validate] warning gl-unsupported — layer "pie": type flag "PIE" is not implemented by ixmaps-gl
231
+ ```
232
+
233
+ It is **off by default** — nothing is fetched and nothing changes. Turn it on with any of:
234
+
235
+ ```js
236
+ ixmaps.Map("map_div", {...}).options({ validate: true }) // or a validator module URL
237
+ ixmaps.validate = true // before or after loading ixmaps-gl.js
238
+ ```
239
+
240
+ or by adding `?ixmaps-validate` to the page URL (this only switches validation on; the
241
+ validator URL can only be set from page code).
242
+
243
+ Checked: Map() options, `.options()`, each layer's `.type()` flags, `.style()`, `.meta()`,
244
+ `.binding()`, `.data()`; layers added later (`defineLayer`); runtime `setThemeStyle` /
245
+ `changeThemeStyle` patches; and reads of `ixmaps.*` functions this engine lacks (the
246
+ `ixmaps` global becomes a Proxy in validation mode only; missing properties still read as
247
+ `undefined`). `map.getValidationReport()` returns the findings as data (`null` when off).
248
+ If the validator can't be loaded, the map loads normally without it. See
249
+ [`examples/validate_demo.html`](./examples/validate_demo.html).
250
+
251
+ For checking page files before they run, use the static checker in ixmaps-grammar
252
+ (`ixmaps-check --engine gl page.html`).
253
+
254
+ ## Example / demo pages
255
+
256
+ | Page | What it shows |
257
+ |---|---|
258
+ | [`demo_accidents.html`](./stage/demo_accidents.html) | Minimal quick-start example |
259
+ | [`accidents_app.html`](./stage/accidents_app.html) / [`germany_accidents_app.html`](./stage/germany_accidents_app.html) | Full app with a facets sidebar (standard facets API) |
260
+ | [`global_power_plants_world_map.html`](./stage/global_power_plants_world_map.html) | Native legend, globe projection toggle |
261
+ | [`global_power_plants_sidebar.html`](./stage/global_power_plants_sidebar.html) | Same dataset with a facets sidebar instead of the native legend |
262
+ | [`mappa_stranieri_30.html`](./stage/mappa_stranieri_30.html) | Symbol shapes, city picker, runtime `changeThemeStyle` filtering, light legend theme |
263
+ | [`roma_incidenti_pericolosita_sidebar_gl.html`](./stage/roma_incidenti_pericolosita_sidebar_gl.html) | AGGREGATE on a gridwidth grid, facets sidebar |
264
+ | [`roma_incidenti_pericolosita_sidebar.html`](./stage/roma_incidenti_pericolosita_sidebar.html) | The same page on the **real** ixmaps-flat engine, kept for side-by-side reference |
265
+ | [`uk_collisions_2023.html`](./stage/uk_collisions_2023.html), [`ixmaps-loader_70.html`](./stage/ixmaps-loader_70.html) | Additional real-page compatibility tests |
266
+ | [`examples/`](./examples) | Smaller feature-by-feature test pages (choropleth classing modes, bubble ranges, etc.) |
267
+
268
+ Serve any of these with a static file server (they fetch remote data over HTTPS, so
269
+ `file://` won't work for most):
270
+
271
+ ```bash
272
+ python3 -m http.server 8000
273
+ ```
274
+
275
+ ## Testing
276
+
277
+ [`test/`](./test) holds an output-regression harness: it loads every example and real-page
278
+ test in headless Chromium, records what the engine produces (every deck.gl layer's
279
+ per-item accessor output, each theme's class breaks and colors, legend and tooltips) and
280
+ compares it against committed baselines, so a refactor that changes any output is caught
281
+ with the exact layer and field.
282
+
283
+ ```bash
284
+ cd test && npm install && npm test
285
+ ```
286
+
287
+ See [`test/README.md`](./test/README.md).
288
+
289
+ ## License
290
+
291
+ BSD 3-Clause — see [`LICENSE.txt`](./LICENSE.txt).