ixmaps-gl 0.2.5 → 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 +14 -4
- package/ixmaps-gl.js +584 -57
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,9 +42,19 @@ page. It currently implements:
|
|
|
42
42
|
- **CHART\|SYMBOL\|GLOW\|CATEGORICAL\|AGGREGATE\|COUNT\|RELOCATE\|VALUES** — the
|
|
43
43
|
bubble-map pipeline: AGGREGATE on ixmaps-flat's grid (hexagonal, or square with
|
|
44
44
|
`RECT`; cell width from `aggregation`/`gridwidth`/`gridwidthpx`, values summed per
|
|
45
|
-
cell, class breaks and legend from the aggregated cells), dynamic sizing, glow
|
|
45
|
+
cell, class breaks and legend from the aggregated cells), dynamic sizing, glow (between
|
|
46
|
+
`glowlower` and `glowupper`),
|
|
46
47
|
multi-point grouping, on-bubble value labels, `NORMALIZE`, chart boxes (`BOX`, `CIRCULARBOX`)
|
|
47
48
|
with the item title above or below (`TITLE`, `BOTTOMTITLE`), between `boxlower` and `boxupper`
|
|
49
|
+
- **CHART\|LABEL** — flat's value label: a rounded box sized by the bubble radius of the size value
|
|
50
|
+
in the item color, with `VALUES` the value in bold; with `TEXTONLY` only the text, over a halo of
|
|
51
|
+
the theme color
|
|
52
|
+
- Chart values (`VALUES`, labels) as flat prints them: the `valuefield` value, flat's number format
|
|
53
|
+
(thousands with spaces, `valuedecimals`, years without breaks), `units` after a space
|
|
54
|
+
- `align` (`left`, `2left`, `right`, `above`, `below`, …) and `offsetx`/`offsety` move a whole
|
|
55
|
+
chart — symbol, glow, shadow, box, title and value — as flat does
|
|
56
|
+
- A range theme of at most two classes gets flat's one-row legend (the value range, or for a
|
|
57
|
+
`$item$` count the label or field name)
|
|
48
58
|
- **CHART\|USER** — a page's own chart function (`style.userdraw`: `ixmaps.<name>(SVGDocument, opt)`
|
|
49
59
|
and its `_init`, as flat calls them), drawn in flat's chart units and shown as an icon at flat's size
|
|
50
60
|
- **DOT** — the simplest base symbol (fixed-radius, unclustered points)
|
|
@@ -193,7 +203,7 @@ of your own project files gl can render.
|
|
|
193
203
|
Load the engine from jsDelivr, pinned to a release tag (or `@main` for the latest commit):
|
|
194
204
|
|
|
195
205
|
```html
|
|
196
|
-
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.
|
|
206
|
+
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.8/ixmaps-gl.js"></script>
|
|
197
207
|
```
|
|
198
208
|
|
|
199
209
|
or from unpkg, the npm package [`ixmaps-gl`](https://www.npmjs.com/package/ixmaps-gl)
|
|
@@ -201,13 +211,13 @@ or from unpkg, the npm package [`ixmaps-gl`](https://www.npmjs.com/package/ixmap
|
|
|
201
211
|
browser script, which defines the global `ixmaps`):
|
|
202
212
|
|
|
203
213
|
```html
|
|
204
|
-
<script src="https://unpkg.com/ixmaps-gl@0.2.
|
|
214
|
+
<script src="https://unpkg.com/ixmaps-gl@0.2.8/ixmaps-gl.js"></script>
|
|
205
215
|
```
|
|
206
216
|
|
|
207
217
|
or use a local copy (`<script src="ixmaps-gl.js"></script>`). A minimal page:
|
|
208
218
|
|
|
209
219
|
```html
|
|
210
|
-
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.
|
|
220
|
+
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.8/ixmaps-gl.js"></script>
|
|
211
221
|
<div id="map_div" style="position:absolute;inset:0;"></div>
|
|
212
222
|
|
|
213
223
|
<script>
|
package/ixmaps-gl.js
CHANGED
|
@@ -47,7 +47,8 @@
|
|
|
47
47
|
// binding.lookup — see joinChoroplethFeatures. HEADTAIL breaks are flat's
|
|
48
48
|
// (headTailBreaks); LOG/POW2/POW3 are recognized type flags but not
|
|
49
49
|
// implemented, see KNOWN_INERT_FLAGS. CHART|USER draws a page's own chart
|
|
50
|
-
// function (style.userdraw, _buildUserChartLayers)
|
|
50
|
+
// function (style.userdraw, _buildUserChartLayers), CHART|LABEL flat's
|
|
51
|
+
// value label, boxed or TEXTONLY (_buildLabelChartLayers). These
|
|
51
52
|
// base types are dispatched by buildDeckLayers in the real engine's own
|
|
52
53
|
// precedence order — DOT is checked first because the real engine's DOT
|
|
53
54
|
// bypasses the whole drawChart/modifier pipeline rather than being
|
|
@@ -80,7 +81,7 @@
|
|
|
80
81
|
// ---------------------------------------------------------------
|
|
81
82
|
// this engine's release (= package.json "version", checked by
|
|
82
83
|
// test/unit/version.test.mjs); ixmaps.glVersion, and logged once at start
|
|
83
|
-
const IXMAPS_GL_VERSION = '0.2.
|
|
84
|
+
const IXMAPS_GL_VERSION = '0.2.8';
|
|
84
85
|
|
|
85
86
|
const LIB_URLS = {
|
|
86
87
|
// Bumped 3.6.2 -> 5.x (2026-09-21, globe-projection compat fix): native
|
|
@@ -306,7 +307,7 @@
|
|
|
306
307
|
// crafted link must not be able to load arbitrary code into the page.
|
|
307
308
|
// ---------------------------------------------------------------
|
|
308
309
|
// v0.1.6: knows this engine's aliases, lookupfield rule, meta↔style, builder methods, project loading
|
|
309
|
-
const VALIDATOR_URL_DEFAULT = 'https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-grammar@v0.1.
|
|
310
|
+
const VALIDATOR_URL_DEFAULT = 'https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-grammar@v0.1.23/dist/validate.mjs';
|
|
310
311
|
// a page may set ixmaps.validate BEFORE loading this script — the
|
|
311
312
|
// `global.ixmaps = {...}` export below would otherwise overwrite it
|
|
312
313
|
const _preloadValidate = global.ixmaps && global.ixmaps.validate;
|
|
@@ -1533,11 +1534,137 @@
|
|
|
1533
1534
|
// - '2narrow'/'3narrow', '2wide'/'3wide': cumulative-step variants.
|
|
1534
1535
|
// Channels are FLOORED, as the source's own hex encoding does
|
|
1535
1536
|
// (hh.charAt(Math.floor(v / 16)) + hh.charAt(v % 16) truncates the
|
|
1536
|
-
// fraction), and clamped to 0-255.
|
|
1537
|
-
//
|
|
1537
|
+
// fraction), and clamped to 0-255. The named palettes are
|
|
1538
|
+
// flatPaletteScheme's; spectrum/spectral (flat's colour-wheel generator)
|
|
1539
|
+
// is not ported: null, the caller's fallback.
|
|
1538
1540
|
const FLAT_PALETTE_SCHEMES = /^(spectrum|spectral|office|mineral|minaral|pastel|harvest|fruit|kmeansp?|pimp|intense|fluo|tableau(10|20)?|viridis|plasma|magma)$/i;
|
|
1539
1541
|
const FLAT_SWEEP_MODES = ['auto', 'linear', 'dynamic', '2colors', '2wide', '2narrow', '2low', '2high', '3colors', '3wide', '3narrow', '3low', '3high'];
|
|
1540
1542
|
|
|
1543
|
+
// flat's named palettes (colorscheme.js 832-1177, 1197-1268), the tables
|
|
1544
|
+
// copied from flat's source: ["N", "tableau", offset] takes N colors of the
|
|
1545
|
+
// palette from offset on, repeating it cyclically
|
|
1546
|
+
// (_circ_createPaletteColorScheme); viridis / plasma / magma are 32-stop
|
|
1547
|
+
// sequential maps resampled to N stops with linear interpolation
|
|
1548
|
+
// (_circ_createSequentialPaletteColorScheme). flat matches each name in
|
|
1549
|
+
// three spellings only — lower, UPPER, Title ("Minaral" for mineral).
|
|
1550
|
+
const FLAT_QUALITATIVE_PALETTES = {
|
|
1551
|
+
office: [
|
|
1552
|
+
'#9999FF', '#993366', '#FFFFCC', '#CCFFFF', '#660066', '#FF8080', '#0066CC', '#CCCCFF',
|
|
1553
|
+
'#000080', '#FF00FF', '#FFFF00', '#00FFFF', '#800080', '#800000', '#008080', '#0000FF',
|
|
1554
|
+
'#00CCFF', '#CCFFFF', '#CCFFCC', '#FFFF99', '#99CCFF', '#FF99CC', '#CC99FF', '#FFCC99'
|
|
1555
|
+
],
|
|
1556
|
+
mineral: [
|
|
1557
|
+
'#F3898B', '#7BFECD', '#B3B07B', '#49BA85', '#FEDBFE', '#847FBA', '#FEA869', '#17BCC4',
|
|
1558
|
+
'#DC686D', '#28803C', '#FFFF00', '#C09B43', '#746FC0', '#9C9C9C', '#EDFEA5', '#0000FF',
|
|
1559
|
+
'#00E04D', '#86A9CE', '#B37B9D', '#9FD8B3', '#FEB676', '#C09671', '#87CFFE', '#00A7C7'
|
|
1560
|
+
],
|
|
1561
|
+
pastel: [
|
|
1562
|
+
'#D2D2D2', '#9DC0C0', '#DFC7AA', '#A1D197', '#E2A6A6', '#CBA6CB', '#FEA4A4', '#A8ACD1',
|
|
1563
|
+
'#C8D89A', '#F3C4D8', '#E9E15E', '#EEEEEE', '#C0AB79', '#E2E17F', '#B4E1FE', '#E8DDFE',
|
|
1564
|
+
'#E1FEEB', '#FEF782', '#C3FFC3', '#CEFE87', '#8CFEB3', '#D2D2D2', '#9DC0C0', '#DFC7AA'
|
|
1565
|
+
],
|
|
1566
|
+
harvest: [
|
|
1567
|
+
'#C06549', '#FFD700', '#BDB76B', '#F7B567', '#CEC395', '#CD9B1D', '#F0E68C', '#A7AF5E',
|
|
1568
|
+
'#C09058', '#8B4513', '#AC96AC', '#698B69', '#8B6914', '#8B8B00', '#FFFBC3', '#BDB056',
|
|
1569
|
+
'#DCCEDB', '#FEF782', '#FEEAC6', '#FFC7AE', '#A6B655', '#DB6700', '#E5A100', '#F7D3B3'
|
|
1570
|
+
],
|
|
1571
|
+
fruit: [
|
|
1572
|
+
'#1F77B4', '#AEC7E8', '#FF7F0E', '#FFBB78', '#2CA02C', '#99DF8B', '#D62728', '#FF9896',
|
|
1573
|
+
'#966ABE', '#C5B0D5', '#8C564B', '#C49C94', '#E377C2', '#F7B6D2', '#7E7E7E', '#C7C7C7',
|
|
1574
|
+
'#BCBD22', '#DBDB8D', '#18BECF', '#9EDAE5', '#1F77B4', '#AEC7E8'
|
|
1575
|
+
],
|
|
1576
|
+
kmeans: [
|
|
1577
|
+
'#c17cd3', '#91c15d', '#6a70d7', '#bab440', '#513688', '#5dc67f', '#993888', '#37d8b0',
|
|
1578
|
+
'#e3586f', '#36dee6', '#d66044', '#47b795', '#ab396c', '#568429', '#e07db5', '#3c7c3d',
|
|
1579
|
+
'#628bd5', '#cb8832', '#ad4258', '#a2863e', '#ad4248', '#9c4629'
|
|
1580
|
+
],
|
|
1581
|
+
kmeansp: [
|
|
1582
|
+
'#ffd1b2', '#a5b2e9', '#effcc2', '#e1c3f8', '#acc692', '#d899b7', '#bbfdd9', '#e8a197',
|
|
1583
|
+
'#8ceceb', '#fab9a0', '#6dc5b7', '#ffc0bc', '#66b8bd', '#dcc992', '#7db3c8', '#fffedb',
|
|
1584
|
+
'#ffdfff', '#84b5ac', '#ffebf2', '#a2aead', '#e1f3ff', '#b5e6ff'
|
|
1585
|
+
],
|
|
1586
|
+
pimp: [
|
|
1587
|
+
'#b09234', '#5f3dc1', '#4ca735', '#b54ade', '#7e9a36', '#cb43b3', '#4a9f61', '#d93f76',
|
|
1588
|
+
'#3a9e88', '#da4631', '#7876dc', '#d57a29', '#5e90cd', '#814b1b', '#554f95', '#396829',
|
|
1589
|
+
'#c275ba', '#75702e', '#89376c', '#bf7f51', '#923432', '#d37075'
|
|
1590
|
+
],
|
|
1591
|
+
intense: [
|
|
1592
|
+
'#c98ab6', '#5bbb42', '#6035bd', '#9dac3c', '#c450da', '#66b888', '#d84fa9', '#44733a',
|
|
1593
|
+
'#7370d7', '#d19231', '#443672', '#db4d32', '#4ca5b0', '#d5466d', '#829fdb', '#8b3b26',
|
|
1594
|
+
'#506793', '#a69358', '#873987', '#4f4b21', '#d58873', '#79354c'
|
|
1595
|
+
],
|
|
1596
|
+
fluo: [
|
|
1597
|
+
'#ecd730', '#4ddded', '#c0ee32', '#f6ac8d', '#64ea51', '#eebd5e', '#5be9c8', '#d9db55',
|
|
1598
|
+
'#77e7a1', '#c6e552', '#8ac793', '#95e354', '#a5e3ad', '#dff782', '#67ee8b', '#e2d680',
|
|
1599
|
+
'#a5e47d', '#b0d490', '#9fc658', '#d1f5a5', '#b4db6c', '#c1d271'
|
|
1600
|
+
],
|
|
1601
|
+
tableau: [
|
|
1602
|
+
'#4e79a7', '#a0cbe8', '#f28e2b', '#ffbe7d', '#59a14f', '#8cd17d', '#b6992d', '#f1ce63',
|
|
1603
|
+
'#499894', '#86bcb6', '#e15759', '#ff9d9a', '#79706e', '#bab0ac', '#d37295', '#fabfd2',
|
|
1604
|
+
'#b07aa1', '#d4a6c8', '#9d7660', '#d7b5a6'
|
|
1605
|
+
],
|
|
1606
|
+
tableau10: [
|
|
1607
|
+
'#1F77B4', '#FF7F0E', '#2CA02C', '#D62728', '#9467BD', '#8C564B', '#E377C2', '#7F7F7F',
|
|
1608
|
+
'#BCBD22', '#17BECF', '#1F77B4', '#FF7F0E', '#2CA02C', '#D62728', '#9467BD', '#8C564B',
|
|
1609
|
+
'#E377C2', '#7F7F7F', '#BCBD22', '#17BECF'
|
|
1610
|
+
],
|
|
1611
|
+
tableau20: [
|
|
1612
|
+
'#1F77B4', '#AEC7E8', '#FF7F0E', '#FFBB78', '#2CA02C', '#98DF8A', '#D62728', '#FF9896',
|
|
1613
|
+
'#9467BD', '#C5B0D5', '#8C564B', '#C49C94', '#E377C2', '#F7B6D2', '#7F7F7F', '#C7C7C7',
|
|
1614
|
+
'#BCBD22', '#DBDB8D', '#17BECF', '#9EDAE5'
|
|
1615
|
+
]
|
|
1616
|
+
};
|
|
1617
|
+
const FLAT_SEQUENTIAL_PALETTES = {
|
|
1618
|
+
viridis: [
|
|
1619
|
+
'#440154', '#470D60', '#48186A', '#482475', '#472E7C', '#453882', '#424186', '#3E4C8A',
|
|
1620
|
+
'#3A548C', '#365D8D', '#32658E', '#2E6D8E', '#2B758E', '#287D8E', '#25848E', '#228C8D',
|
|
1621
|
+
'#1F948C', '#1E9C89', '#20A386', '#25AB82', '#2EB37C', '#3ABA76', '#48C16E', '#58C765',
|
|
1622
|
+
'#69CD5B', '#7FD34E', '#93D741', '#A8DB34', '#BDDF26', '#D5E21A', '#EAE51A', '#FDE725'
|
|
1623
|
+
],
|
|
1624
|
+
plasma: [
|
|
1625
|
+
'#0D0887', '#220690', '#310597', '#41049D', '#4E02A2', '#5B01A5', '#6700A8', '#7501A8',
|
|
1626
|
+
'#8104A7', '#8D0BA5', '#9814A0', '#A21D9A', '#AD2793', '#B6308B', '#BF3984', '#C7427C',
|
|
1627
|
+
'#CF4C74', '#D6556D', '#DD5E66', '#E3685F', '#E97257', '#EF7C51', '#F3874A', '#F79143',
|
|
1628
|
+
'#FA9C3C', '#FCA934', '#FDB52E', '#FDC229', '#FCCE25', '#F9DD25', '#F5EB27', '#F0F921'
|
|
1629
|
+
],
|
|
1630
|
+
magma: [
|
|
1631
|
+
'#000004', '#030312', '#0A0822', '#140E36', '#1E1149', '#2A115C', '#38106C', '#471078',
|
|
1632
|
+
'#54137D', '#601880', '#6D1D81', '#792282', '#882781', '#942C80', '#A1307E', '#AE347B',
|
|
1633
|
+
'#BD3977', '#CA3E72', '#D6456C', '#E24D66', '#EC5860', '#F3655C', '#F8745C', '#FB835F',
|
|
1634
|
+
'#FD9266', '#FEA36F', '#FEB27A', '#FEC185', '#FECF92', '#FDE0A1', '#FCEEB0', '#FCFDBF'
|
|
1635
|
+
]
|
|
1636
|
+
};
|
|
1637
|
+
const FLAT_PALETTE_NAMES = (() => {
|
|
1638
|
+
const names = {};
|
|
1639
|
+
for (const key of [...Object.keys(FLAT_QUALITATIVE_PALETTES), ...Object.keys(FLAT_SEQUENTIAL_PALETTES)]) {
|
|
1640
|
+
names[key] = key; names[key.toUpperCase()] = key;
|
|
1641
|
+
names[key === 'mineral' ? 'Minaral' : key[0].toUpperCase() + key.slice(1)] = key;
|
|
1642
|
+
}
|
|
1643
|
+
return names;
|
|
1644
|
+
})();
|
|
1645
|
+
const hexRgb = h => [parseInt(h.substr(1, 2), 16), parseInt(h.substr(3, 2), 16), parseInt(h.substr(5, 2), 16)];
|
|
1646
|
+
function flatPaletteScheme(name, nColors, offset) {
|
|
1647
|
+
const key = FLAT_PALETTE_NAMES[name];
|
|
1648
|
+
if (!key) return null;
|
|
1649
|
+
nColors = Math.max(1, Number(nColors) || 1);
|
|
1650
|
+
const seq = FLAT_SEQUENTIAL_PALETTES[key];
|
|
1651
|
+
if (seq) {
|
|
1652
|
+
const out = [];
|
|
1653
|
+
for (let i = 0; i < nColors; i++) {
|
|
1654
|
+
const pos = (nColors < 2 ? 0 : i / (nColors - 1)) * (seq.length - 1);
|
|
1655
|
+
const i0 = Math.floor(pos), i1 = Math.min(seq.length - 1, i0 + 1), f = pos - i0;
|
|
1656
|
+
const a = hexRgb(seq[i0]), b = hexRgb(seq[i1]);
|
|
1657
|
+
out.push(a.map((v, k) => Math.round(v + (b[k] - v) * f)));
|
|
1658
|
+
}
|
|
1659
|
+
return out;
|
|
1660
|
+
}
|
|
1661
|
+
const colors = FLAT_QUALITATIVE_PALETTES[key];
|
|
1662
|
+
const off = Number(offset) || 0;
|
|
1663
|
+
const out = [];
|
|
1664
|
+
for (let i = 0; i < nColors; i++) out.push(hexRgb(colors[(off + i) % colors.length]));
|
|
1665
|
+
return out;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1541
1668
|
function isGeneratedColorScheme(colorscheme) {
|
|
1542
1669
|
return Array.isArray(colorscheme) && colorscheme.length > 0 && !isNaN(Number(colorscheme[0])) && String(colorscheme[0]).trim() !== '';
|
|
1543
1670
|
}
|
|
@@ -1549,6 +1676,7 @@
|
|
|
1549
1676
|
}
|
|
1550
1677
|
|
|
1551
1678
|
function flatColorSweep(cc1, cc2, nSteps, nParam1, nParam2) {
|
|
1679
|
+
if (typeof cc1 === 'string' && FLAT_PALETTE_NAMES[cc1]) return flatPaletteScheme(cc1, nSteps, cc2);
|
|
1552
1680
|
if (typeof cc1 === 'string' && FLAT_PALETTE_SCHEMES.test(cc1)) return null;
|
|
1553
1681
|
nSteps = Number(nSteps);
|
|
1554
1682
|
const [r1, g1, b1] = flatColorRgb(cc1, [255, 255, 255]);
|
|
@@ -2066,7 +2194,7 @@
|
|
|
2066
2194
|
// edit it by hand; `npm run unit` fails when it drifts.
|
|
2067
2195
|
// ---------------------------------------------------------------
|
|
2068
2196
|
// <grammar:binding-aliases>
|
|
2069
|
-
// generated from ixmaps-grammar 0.1.
|
|
2197
|
+
// generated from ixmaps-grammar 0.1.23 (ixmaps-flat 1.0.42, 2b978d5 2026-10-02) — 52 aliases
|
|
2070
2198
|
const FLAT_BINDING_ALIASES = {
|
|
2071
2199
|
"aggregation": "style.aggregationfield",
|
|
2072
2200
|
"aggregationfield": "style.aggregationfield",
|
|
@@ -2128,7 +2256,7 @@
|
|
|
2128
2256
|
// both — so .style({tooltip}) is a tooltip and .meta({fillopacity}) a style
|
|
2129
2257
|
// property. GENERATED by test/sync-grammar.mjs — do not edit.
|
|
2130
2258
|
// <grammar:meta-keys>
|
|
2131
|
-
// generated from ixmaps-grammar 0.1.
|
|
2259
|
+
// generated from ixmaps-grammar 0.1.23 — 5 meta keys
|
|
2132
2260
|
const FLAT_META_KEYS = ["description","name","snippet","title","tooltip"];
|
|
2133
2261
|
// </grammar:meta-keys>
|
|
2134
2262
|
|
|
@@ -2312,7 +2440,7 @@
|
|
|
2312
2440
|
const STYLE_NUMBER_KEYS = ['linewidth', 'fillopacity', 'scale', 'classes', 'valuedecimals', 'normalsizevalue',
|
|
2313
2441
|
'sizepow', 'rangescale', 'minvalue', 'maxvalue', 'markersize', 'boxopacity', 'outlierscale', 'valuescale',
|
|
2314
2442
|
'brightness', 'fractionscale', 'dopacityscale', 'dopacitypow', 'gridwidthpx', 'textscale', 'rangecentervalue',
|
|
2315
|
-
'shadowblur', 'shadowdx', 'shadowdy', 'maxshadow'];
|
|
2443
|
+
'shadowblur', 'shadowdx', 'shadowdy', 'maxshadow', 'offsetx', 'offsety'];
|
|
2316
2444
|
function toNumberIfNumeric(v) {
|
|
2317
2445
|
if (typeof v !== 'string' || !v.trim()) return v;
|
|
2318
2446
|
const n = Number(v);
|
|
@@ -2914,7 +3042,7 @@
|
|
|
2914
3042
|
// no-op.
|
|
2915
3043
|
const base = layerId.replace(/-g\d+$/, '');
|
|
2916
3044
|
return runtimes.find(r => {
|
|
2917
|
-
if (base === `ix-bubbles-${r.name}`) return
|
|
3045
|
+
if (base === `ix-bubbles-${r.name}`) return isSymbolChart(r.flags);
|
|
2918
3046
|
if (base === `ix-dot-${r.name}`) return r.flags.has('DOT');
|
|
2919
3047
|
if (base === `ix-choropleth-${r.name}`) return r.flags.has('CHOROPLETH');
|
|
2920
3048
|
if (base === `ix-features-${r.name}`) return r.flags.has('FEATURE') || r.flags.has('FEATURES');
|
|
@@ -4509,6 +4637,36 @@
|
|
|
4509
4637
|
// form: SPACE there is flat's plain "." thousands.
|
|
4510
4638
|
const flatCompactLegend = rt._rangeClassed && !flatFlag(rt.flags, 'CATEGORICAL') && !flatFlag(rt.flags, 'PLOT')
|
|
4511
4639
|
&& (rt.partsA || []).length >= 5 && !(Array.isArray(rt.style.label) && rt.style.label.length);
|
|
4640
|
+
// flat's single-color legend (legend.js 765-769, 1039-1046): a range
|
|
4641
|
+
// theme of at most 2 classes without label/ranges, not
|
|
4642
|
+
// CATEGORICAL, is one row — the swatch and "min ... max unit",
|
|
4643
|
+
// or, when every value is 1 (a "$item$" count), its label or
|
|
4644
|
+
// value field
|
|
4645
|
+
const flatSingleRowLegend = rt._rangeClassed && !flatFlag(rt.flags, 'CATEGORICAL') && !flatFlag(rt.flags, 'PLOT')
|
|
4646
|
+
&& (rt.partsA || []).length <= 2 && !(Array.isArray(rt.style.label) && rt.style.label.length)
|
|
4647
|
+
&& !(Array.isArray(rt.style.ranges) && rt.style.ranges.length);
|
|
4648
|
+
if (flatSingleRowLegend) {
|
|
4649
|
+
renderRows = function() {
|
|
4650
|
+
const rgb = rt.categoryColorsRgb[0] || [128, 128, 128];
|
|
4651
|
+
const unit = String(rt.style.legendunits || rt.style.units || '').replace(/ /g, ' ');
|
|
4652
|
+
const vMin = styleNum(rt.style.minvalue) || rt._valueMin, vMax = styleNum(rt.style.maxvalue) || rt._valueMax;
|
|
4653
|
+
const label = rt.style.label != null && !Array.isArray(rt.style.label) ? String(rt.style.label) : String(rt.binding.value || '');
|
|
4654
|
+
const text = vMin !== 1 || vMax !== 1
|
|
4655
|
+
? flatFormatValue(vMin, 2, 'BLANK') + ' ... ' + flatFormatValue(vMax, 2, 'BLANK') + ' ' + unit
|
|
4656
|
+
: label;
|
|
4657
|
+
const marked = rt._markedClasses.has(0);
|
|
4658
|
+
rowsEl.innerHTML = '<div class="ix-legend-row" data-idx="0" style="display:flex;align-items:center;gap:5px;padding:4px 3px;cursor:pointer;border-radius:4px;'
|
|
4659
|
+
+ 'background:' + (marked ? legendColors.rowMarked : 'transparent') + ';">'
|
|
4660
|
+
+ '<span style="flex:0 0 auto;width:1.6em;height:0.8em;background:rgb(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ');"></span>'
|
|
4661
|
+
+ '<span>' + text + '</span></div>';
|
|
4662
|
+
rowsEl.querySelectorAll('.ix-legend-row').forEach(row => {
|
|
4663
|
+
row.addEventListener('click', () => {
|
|
4664
|
+
if (rt._markedClasses.has(0)) window.ixmaps.unmarkThemeClass(rt.name, 0);
|
|
4665
|
+
else window.ixmaps.markThemeClass(rt.name, 0);
|
|
4666
|
+
});
|
|
4667
|
+
});
|
|
4668
|
+
};
|
|
4669
|
+
} else
|
|
4512
4670
|
if (flatCompactLegend) {
|
|
4513
4671
|
renderRows = function() {
|
|
4514
4672
|
const n = rt.categoryColorsRgb.length;
|
|
@@ -5432,26 +5590,6 @@
|
|
|
5432
5590
|
}, 0);
|
|
5433
5591
|
}
|
|
5434
5592
|
|
|
5435
|
-
function roundBubbleValue(value, style) {
|
|
5436
|
-
const parsedDecimals = styleNum(style.valuedecimals);
|
|
5437
|
-
const decimals = !isNaN(parsedDecimals) ? parsedDecimals : (value < 1 ? 1 : 0);
|
|
5438
|
-
return decimals > 0 ? value.toFixed(decimals) : String(Math.round(value));
|
|
5439
|
-
}
|
|
5440
|
-
|
|
5441
|
-
// DIFFERENCE, RELATIVE and SIGN: a positive value gets "+", zero "+/-"
|
|
5442
|
-
// (maptheme.js 20990-21017, "make a positive sign, if positive value is
|
|
5443
|
-
// result of a diff operation")
|
|
5444
|
-
function formatBubbleValue(value, style, flags) {
|
|
5445
|
-
const rounded = roundBubbleValue(value, style);
|
|
5446
|
-
const unit = style.units || '';
|
|
5447
|
-
const text = unit.length && unit.length <= 5 ? rounded + unit : rounded;
|
|
5448
|
-
if (flags && (flags.has('DIFFERENCE') || flags.has('RELATIVE') || flags.has('SIGN'))) {
|
|
5449
|
-
if (value === 0) return '+/-' + text;
|
|
5450
|
-
if (value > 0) return '+' + text;
|
|
5451
|
-
}
|
|
5452
|
-
return text;
|
|
5453
|
-
}
|
|
5454
|
-
|
|
5455
5593
|
// Records behind one drawn chart item: a group's per-category record
|
|
5456
5594
|
// counts summed, else a Supercluster cluster's point_count, else 1.
|
|
5457
5595
|
// recordCounts only exists on RELOCATE groups (groupCoLocated); a plain
|
|
@@ -5466,6 +5604,130 @@
|
|
|
5466
5604
|
return Math.min(radiusPx * 0.8, radiusPx * (3.3 / Math.max(1, text.length))) * (valueScale || 1);
|
|
5467
5605
|
}
|
|
5468
5606
|
|
|
5607
|
+
// the themes drawn by the symbol chart pipeline (_buildChartLayers): flat's
|
|
5608
|
+
// BUBBLE/SQUARE/LABEL branch (maptheme.js 20647-20652), symbols, user charts
|
|
5609
|
+
function isSymbolChart(flags) {
|
|
5610
|
+
return flags.has('CHART') && (flags.has('SYMBOL') || flags.has('USER') || flags.has('LABEL'));
|
|
5611
|
+
}
|
|
5612
|
+
|
|
5613
|
+
// flat's __formatValue (mapscript.js 6268-6365) as maptheme's formatValue
|
|
5614
|
+
// calls it: 0 and values beyond ±10^12 as they are, else rounded twice
|
|
5615
|
+
// (one decimal more, then the precision — 25.446 → 25.45 → 25.5),
|
|
5616
|
+
// thousands grouped with spaces unless noBreaks (formatValue's NOBREAKS
|
|
5617
|
+
// for a theme of values within 1000-3000, i.e. years: maptheme.js 26364)
|
|
5618
|
+
function flatGroupedValue(value, decimals, noBreaks) {
|
|
5619
|
+
if (!isFinite(value)) return String(value);
|
|
5620
|
+
if (value === 0) return '0';
|
|
5621
|
+
if (value > 1e12 || value < -1e12) return String(value);
|
|
5622
|
+
const clip = Math.pow(10, decimals);
|
|
5623
|
+
const flatRounded = Math.round(Number(value.toFixed(decimals + 1)) * clip) / clip;
|
|
5624
|
+
const [intPart, dec] = flatRounded.toFixed(decimals).split('.');
|
|
5625
|
+
const grouped = noBreaks ? intPart : intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
|
|
5626
|
+
return dec ? `${grouped}.${dec}` : grouped;
|
|
5627
|
+
}
|
|
5628
|
+
// maptheme.js formatValue's NOBREAKS switch (26368): theme min > 999 and max < 3000
|
|
5629
|
+
function flatNoBreaks(vMin, vMax) {
|
|
5630
|
+
return vMin > 999 && vMax < 3000;
|
|
5631
|
+
}
|
|
5632
|
+
|
|
5633
|
+
// flat's chart position (maptheme.js 24441-24530 and 24603, the non-PLOT,
|
|
5634
|
+
// non-grid case), measured on flat: the chart group is moved by −ptNull.
|
|
5635
|
+
// ptNull = (offsetx, −offsety) · unit · symbolScale — unit is flat's
|
|
5636
|
+
// normalX(1) on screen (object zoom × style.scale), symbolScale the
|
|
5637
|
+
// SYMBOL branch's r / chart size (21993), 1 for BUBBLE/LABEL
|
|
5638
|
+
// then align overwrites/adds: o is the chart's own ptNull.y as its
|
|
5639
|
+
// branch left it (ptNullOrig — BUBBLE/LABEL and SYMBOL: r + 5 units,
|
|
5640
|
+
// TEXTONLY: r; 21111, 23204), h half the chart size (BUBBLE/LABEL:
|
|
5641
|
+
// the item's radius, SYMBOL: the theme's normal radius)
|
|
5642
|
+
// So "left" puts the chart's left edge o to the right of the point (and
|
|
5643
|
+
// drops offsetx), "2left" one more h. Returns deck.gl's pixel offset.
|
|
5644
|
+
function flatChartAlignOffset(style, g) {
|
|
5645
|
+
const num = v => (isNaN(styleNum(v)) ? 0 : styleNum(v));
|
|
5646
|
+
const sym = g.symbolScale || 1;
|
|
5647
|
+
let x = num(style.offsetx) * g.unit * sym, y = -num(style.offsety) * g.unit * sym;
|
|
5648
|
+
const a = String(style.align || '');
|
|
5649
|
+
if (a) {
|
|
5650
|
+
const h = g.half, o = g.orig;
|
|
5651
|
+
if (/center/.test(a)) y = 0;
|
|
5652
|
+
if (/bottom/.test(a)) y = o || h * 2 / 5;
|
|
5653
|
+
if (/above/.test(a)) y = o || h;
|
|
5654
|
+
if (/2above/.test(a)) y += h;
|
|
5655
|
+
if (/below/.test(a)) y = -(o || h);
|
|
5656
|
+
if (/2below/.test(a)) y -= h;
|
|
5657
|
+
if (/top/.test(a)) y = -(o || h);
|
|
5658
|
+
if (/left/.test(a)) x = -(o || h);
|
|
5659
|
+
if (/2left/.test(a)) x -= h;
|
|
5660
|
+
if (/right/.test(a)) x = o || h;
|
|
5661
|
+
if (/2right/.test(a)) x += h;
|
|
5662
|
+
if (/23right/.test(a)) x = h * 2 / 3;
|
|
5663
|
+
if (/baseline/.test(a)) y += h * 2 / 5;
|
|
5664
|
+
}
|
|
5665
|
+
return [-x || 0, -y || 0];
|
|
5666
|
+
}
|
|
5667
|
+
// which of flat's chart branches draws a symbol-pipeline theme: BUBBLE /
|
|
5668
|
+
// SQUARE / LABEL (maptheme.js 20647) come before SYMBOL (21122)
|
|
5669
|
+
function flatChartBranch(flags) {
|
|
5670
|
+
const t = flags.typeString != null ? flags.typeString : [...flags].join('|');
|
|
5671
|
+
return /BUBBLE|SQUARE|LABEL/.test(t) ? 'bubble' : 'symbol';
|
|
5672
|
+
}
|
|
5673
|
+
|
|
5674
|
+
// flat's ColorScheme.getDerivateColor (colorscheme.js 46-75): each
|
|
5675
|
+
// channel × f, floored, capped at 255; brightening (f > 1) first lifts
|
|
5676
|
+
// every channel to at least 90 and, with one at 250 or more, uses
|
|
5677
|
+
// max(0.9 f, 1.1)
|
|
5678
|
+
function flatDerivateRgb(rgb, f) {
|
|
5679
|
+
let [r, g, b] = rgb;
|
|
5680
|
+
if (f > 1) {
|
|
5681
|
+
r = Math.max(r, 90); g = Math.max(g, 90); b = Math.max(b, 90);
|
|
5682
|
+
if (r >= 250 || g >= 250 || b >= 250) f = Math.max(f * 0.9, 1.1);
|
|
5683
|
+
}
|
|
5684
|
+
return [r, g, b].map(c => Math.min(255, Math.floor(c * f)));
|
|
5685
|
+
}
|
|
5686
|
+
// ChartColors.textColor (colorscheme.js 104-116): darker on light colors
|
|
5687
|
+
function flatChartTextRgb(rgb) {
|
|
5688
|
+
return flatDerivateRgb(rgb, rgb[0] + rgb[1] + rgb[2] > 450 ? 0.6 : 3);
|
|
5689
|
+
}
|
|
5690
|
+
|
|
5691
|
+
// a chart's VALUES text (maptheme.js 20979-21017): an explicit valuefield
|
|
5692
|
+
// prints that field of the record ($title$: the title; a non-number as
|
|
5693
|
+
// is), else the value; numbers formatted with valuedecimals (flat's `||`:
|
|
5694
|
+
// 0 or unset → 1 decimal below 1 — or when the max value is at most 1 —
|
|
5695
|
+
// else none) plus the unit, which flat stores with a leading space unless
|
|
5696
|
+
// it starts with "." (9675) and appends only up to 5 characters.
|
|
5697
|
+
// A CATEGORICAL bubble prints its size value when size is bound to a
|
|
5698
|
+
// field (its value, see resolveAggregateValue), as flat does; with
|
|
5699
|
+
// valuefield set to its categorical field, the class name
|
|
5700
|
+
// (_categoryValueRecord for an aggregated part).
|
|
5701
|
+
// pure: the record an aggregated part of category i stands for in its
|
|
5702
|
+
// value text — with style.valuefield the theme's CATEGORICAL field, flat
|
|
5703
|
+
// prints the class name (the records' own valuefield value); else none
|
|
5704
|
+
function categoryValueRecord(style, binding, flags, labels, i) {
|
|
5705
|
+
const field = style.valuefield;
|
|
5706
|
+
if (!field || field !== binding.value || !flags.has('CATEGORICAL') || !labels || labels[i] == null) return null;
|
|
5707
|
+
return { [field]: labels[i] };
|
|
5708
|
+
}
|
|
5709
|
+
function flatValueText(raw, title, value, style, flags, opts = {}) {
|
|
5710
|
+
const units = style.units ? String(style.units) : '';
|
|
5711
|
+
const unit = units ? (units[0] === '.' ? '' : ' ') + units : '';
|
|
5712
|
+
const unitText = unit.length <= 5 ? unit : '';
|
|
5713
|
+
const dec = v => styleNum(style.valuedecimals) || ((v < 1 || (opts.maxValue != null && opts.maxValue <= 1)) ? 1 : 0);
|
|
5714
|
+
const signed = (v, text) => {
|
|
5715
|
+
if (flags && (flags.has('DIFFERENCE') || flags.has('RELATIVE') || flags.has('SIGN'))) {
|
|
5716
|
+
if (v === 0) return '+/-' + text;
|
|
5717
|
+
if (v > 0) return '+' + text;
|
|
5718
|
+
}
|
|
5719
|
+
return text;
|
|
5720
|
+
};
|
|
5721
|
+
const field = style.valuefield;
|
|
5722
|
+
if (field === '$title$') return title != null ? String(title) : '';
|
|
5723
|
+
if (field && raw && raw[field] != null && raw[field] !== '') {
|
|
5724
|
+
if (isNaN(raw[field])) return String(raw[field]);
|
|
5725
|
+
const v = Number(raw[field]);
|
|
5726
|
+
return signed(v, flatGroupedValue(v, styleNum(style.valuedecimals) || (v < 1 ? 1 : 0), opts.noBreaks) + unitText);
|
|
5727
|
+
}
|
|
5728
|
+
return signed(value, flatGroupedValue(value, dec(value), opts.noBreaks) + unitText);
|
|
5729
|
+
}
|
|
5730
|
+
|
|
5469
5731
|
// .style({valueupper: "1:200000"}) — the real engine's fHideValues gate
|
|
5470
5732
|
// (maptheme.js ~line 16969/19542): value LABELS (not the whole layer,
|
|
5471
5733
|
// that's clipupper, out of scope) hide once the map is zoomed out past
|
|
@@ -5495,6 +5757,16 @@
|
|
|
5495
5757
|
const scale = scaleDenominatorAt(zoom);
|
|
5496
5758
|
return !!((upper && scale > upper) || (lower && scale <= lower));
|
|
5497
5759
|
}
|
|
5760
|
+
// .style({glowupper, glowlower}) — flat's glow gate (maptheme.js
|
|
5761
|
+
// 1726-1734, 16986-16990): the GLOW halo is drawn at scales at or below
|
|
5762
|
+
// glowupper and at or above glowlower (both inclusive)
|
|
5763
|
+
function glowHiddenByScale(style, zoom) {
|
|
5764
|
+
const denom = v => (v == null || v === '' ? 0 : Number(String(v).includes(':') ? String(v).split(':')[1] : v)) || 0;
|
|
5765
|
+
const upper = denom(style.glowupper), lower = denom(style.glowlower);
|
|
5766
|
+
if (!upper && !lower) return false;
|
|
5767
|
+
const scale = scaleDenominatorAt(zoom);
|
|
5768
|
+
return !!((upper && scale > upper) || (lower && scale < lower));
|
|
5769
|
+
}
|
|
5498
5770
|
function themeHiddenByScale(flags, style, zoom) {
|
|
5499
5771
|
return (flags.has('FEATURE') || flags.has('FEATURES')) ? featuresHiddenByScale(style, zoom) : chartHiddenByScale(style, zoom);
|
|
5500
5772
|
}
|
|
@@ -5935,6 +6207,25 @@
|
|
|
5935
6207
|
return { present, radii, offsets, fitScale, maxR };
|
|
5936
6208
|
}
|
|
5937
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
|
+
|
|
5938
6229
|
// ---------------------------------------------------------------
|
|
5939
6230
|
// Classification — pure functions (no LayerRuntime state): class
|
|
5940
6231
|
// breaks for a numeric field, and which class a value falls in.
|
|
@@ -6646,6 +6937,8 @@
|
|
|
6646
6937
|
const fields = String(binding.value).split('|');
|
|
6647
6938
|
const valueOf = flags && flags.has('DIFFERENCE') && fields.length > 1
|
|
6648
6939
|
? p => cellAggregatedValues({ sums: fields.map(k => parseFloat(p[k]) || 0), counts: fields.map(() => 1) }, flags, style)[0]
|
|
6940
|
+
// "$item$" is flat's record count: 1 per record
|
|
6941
|
+
: binding.value === '$item$' ? () => 1
|
|
6649
6942
|
: p => (p && Object.keys(p).length ? flatNumber(p[binding.value], flags) : NaN);
|
|
6650
6943
|
const values = features
|
|
6651
6944
|
.map(f => valueOf(f.properties))
|
|
@@ -7361,7 +7654,8 @@
|
|
|
7361
7654
|
properties: {
|
|
7362
7655
|
value: this._seriesDifference ? seriesValue(f.properties) : this._resolveAggregateValue(f.properties),
|
|
7363
7656
|
...(this._seriesDifference ? { series: seriesOf(f.properties) } : {}),
|
|
7364
|
-
|
|
7657
|
+
// "$item$": flat's record count, 1 per record
|
|
7658
|
+
...(this._classSeparate ? { classValue: this.binding.value === '$item$' ? 1 : parseFloat(f.properties[this.binding.value]) || 0 } : {}),
|
|
7365
7659
|
...(this._aggregateField100 ? { value100: parseFloat(f.properties[f100]) || 0 } : {}),
|
|
7366
7660
|
raw: f.properties
|
|
7367
7661
|
}
|
|
@@ -7476,7 +7770,7 @@
|
|
|
7476
7770
|
_dataLayerId(base) { return this._dataGen ? `${base}-d${this._dataGen}` : base; }
|
|
7477
7771
|
|
|
7478
7772
|
_usesAggregationIndex() {
|
|
7479
|
-
return (this.flags.has('AGGREGATE') || (this.flags
|
|
7773
|
+
return (this.flags.has('AGGREGATE') || isSymbolChart(this.flags)) && !this.flags.has('GRIDSIZE');
|
|
7480
7774
|
}
|
|
7481
7775
|
|
|
7482
7776
|
_rebuildActiveFeatures() {
|
|
@@ -7736,7 +8030,7 @@
|
|
|
7736
8030
|
return this._glowIconCache.get(key);
|
|
7737
8031
|
}
|
|
7738
8032
|
|
|
7739
|
-
_buildBubbleIcon(counts, colors) {
|
|
8033
|
+
_buildBubbleIcon(counts, colors, marked = null) {
|
|
7740
8034
|
// The cache key quantizes each count's RATIO to the group's total,
|
|
7741
8035
|
// not the raw count — computeBubblePackLayout's radii are purely
|
|
7742
8036
|
// sqrt(c/total), so two clusters with wildly different absolute
|
|
@@ -7761,13 +8055,18 @@
|
|
|
7761
8055
|
// smooth.
|
|
7762
8056
|
const total = counts.reduce((a, b) => a + b, 0) || 1;
|
|
7763
8057
|
const RATIO_BUCKETS = 15;
|
|
7764
|
-
|
|
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
|
+
: '');
|
|
7765
8064
|
if (this._iconCache.has(key)) return this._iconCache.get(key);
|
|
7766
8065
|
const size = BUBBLE_ICON_SIZE;
|
|
7767
8066
|
const canvas = document.createElement('canvas');
|
|
7768
8067
|
canvas.width = size; canvas.height = size;
|
|
7769
8068
|
const ctx = canvas.getContext('2d');
|
|
7770
|
-
const { present, radii, offsets, fitScale } = computeBubblePackLayout(counts, size);
|
|
8069
|
+
const { present, radii, offsets, fitScale } = (marked && isolatedBubblePackLayout(counts, size, marked)) || computeBubblePackLayout(counts, size);
|
|
7771
8070
|
const cx = size / 2, cy = size / 2;
|
|
7772
8071
|
present.forEach((p, i) => {
|
|
7773
8072
|
ctx.beginPath();
|
|
@@ -8051,11 +8350,10 @@
|
|
|
8051
8350
|
// flat's __formatValue (mapscript.js 6268ff) rounds twice: first to
|
|
8052
8351
|
// one decimal more (toFixed), then to the precision (Math.round) —
|
|
8053
8352
|
// 25.446 → "25.45" → 25.5, where a single toFixed(1) gives 25.4
|
|
8054
|
-
|
|
8055
|
-
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
return dec ? `${grouped}.${dec}` : grouped;
|
|
8353
|
+
// 0 keeps its decimals here (the class labels follow flat's legend
|
|
8354
|
+
// toFixed(2), legend.js 636), unlike the chart values' formatValue
|
|
8355
|
+
if (num === 0) return num.toFixed(decimals);
|
|
8356
|
+
return flatGroupedValue(num, decimals);
|
|
8059
8357
|
}
|
|
8060
8358
|
|
|
8061
8359
|
// Per-icon alpha for the legend's isolate-on-click behavior (see
|
|
@@ -8067,6 +8365,15 @@
|
|
|
8067
8365
|
// counts as marked if ANY of its constituent categories is marked —
|
|
8068
8366
|
// a mixed cluster shouldn't dim just because one of several
|
|
8069
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
|
+
|
|
8070
8377
|
_iconAlpha(d) {
|
|
8071
8378
|
if (!this._markedClasses.size) return 255;
|
|
8072
8379
|
if (d.properties.counts) {
|
|
@@ -8140,7 +8447,7 @@
|
|
|
8140
8447
|
// USER charts (a page's own chart function, style.userdraw) go the
|
|
8141
8448
|
// chart pipeline too, drawn by _buildUserChartLayers (bubbles when the
|
|
8142
8449
|
// page has no such function)
|
|
8143
|
-
if (
|
|
8450
|
+
if (isSymbolChart(this.flags)) return this._buildChartLayers(zoom, bbox, liveZoom, globeCenter);
|
|
8144
8451
|
console.warn(`[ixmaps-gl] layer "${this.name}": type "${[...this.flags].join('|')}" has no implemented renderer`);
|
|
8145
8452
|
return [];
|
|
8146
8453
|
}
|
|
@@ -9524,6 +9831,7 @@
|
|
|
9524
9831
|
const userLayers = this._buildUserChartLayers(combined, liveZoom, sizeValueOf);
|
|
9525
9832
|
if (userLayers) return layers.concat(userLayers);
|
|
9526
9833
|
}
|
|
9834
|
+
if (this.flags.has('LABEL')) return layers.concat(this._buildLabelChartLayers(combined, zoom, liveZoom, sizeValueOf));
|
|
9527
9835
|
const fillOpacity = styleNum(this.style.fillopacity) || 0.85;
|
|
9528
9836
|
// .style({linecolor, linewidth}) — an individual icon's own border,
|
|
9529
9837
|
// see _buildSingleIcon's own comment. Real ixmaps-flat symbol
|
|
@@ -9537,6 +9845,8 @@
|
|
|
9537
9845
|
|
|
9538
9846
|
const itemRgb = d => this.categoryColorsRgb[d.properties.counts ? dominant(d.properties.counts) : d.properties.cat] || [128, 128, 128];
|
|
9539
9847
|
const radiusOf = d => valueRadius(sizeValueOf(d), liveZoom, this.style, this.mapOptions, this.flags, this._maxSizeValue);
|
|
9848
|
+
// align / offsetx / offsety: the whole chart moves (flatChartAlignOffset)
|
|
9849
|
+
const alignOf = this._chartAlignFn(liveZoom, radiusOf);
|
|
9540
9850
|
|
|
9541
9851
|
// SHADOW (style.shadow, flat's gate: maxshadow, shadowupper/lower — flatShadowOn): a soft
|
|
9542
9852
|
// drop shadow under each symbol, see _getShadowIcon
|
|
@@ -9550,7 +9860,7 @@
|
|
|
9550
9860
|
getPosition: d => d.geometry.coordinates,
|
|
9551
9861
|
getIcon: d => this._getShadowIcon(radiusOf(d), blurPx, d.properties.counts ? 'circle' : this._resolveSymbolShape(d.properties)),
|
|
9552
9862
|
getSize: d => radiusOf(d) * 3, // disc = 2/3 of the icon → its diameter is 2r
|
|
9553
|
-
getPixelOffset: [dx, dy],
|
|
9863
|
+
getPixelOffset: alignOf.active ? d => { const o = alignOf(d); return [o[0] + dx, o[1] + dy]; } : [dx, dy],
|
|
9554
9864
|
getColor: d => {
|
|
9555
9865
|
const rgb = itemRgb(d);
|
|
9556
9866
|
const gray = Math.round(Math.min(255, 0.1 * (rgb[0] + rgb[1] + rgb[2])));
|
|
@@ -9568,14 +9878,15 @@
|
|
|
9568
9878
|
// and cluster glows keep their own size multiplier (11 vs 9 — a lone
|
|
9569
9879
|
// point and a packed cluster read differently at the same radius)
|
|
9570
9880
|
// but share one sorted layer, same as the main bubbles below.
|
|
9571
|
-
if (this.flags.has('GLOW')) {
|
|
9881
|
+
if (this.flags.has('GLOW') && !glowHiddenByScale(this.style, liveZoom)) {
|
|
9572
9882
|
layers.push(new IconLayer({
|
|
9573
9883
|
id: `ix-glow-${this.name}-g${this._iconGeneration}`,
|
|
9574
9884
|
data: combined, pickable: false,
|
|
9575
9885
|
getPosition: d => d.geometry.coordinates,
|
|
9576
|
-
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]),
|
|
9577
9887
|
getSize: d => valueRadius(sizeValueOf(d), liveZoom, this.style, this.mapOptions, this.flags, this._maxSizeValue) * (d.properties.counts ? 9 : 11),
|
|
9578
9888
|
getColor: d => [255, 255, 255, this._iconAlpha(d)],
|
|
9889
|
+
...(alignOf.active ? { getPixelOffset: alignOf } : {}),
|
|
9579
9890
|
sizeUnits: 'pixels',
|
|
9580
9891
|
billboard: true,
|
|
9581
9892
|
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
@@ -9590,6 +9901,7 @@
|
|
|
9590
9901
|
getIcon: d => this._buildSingleIcon(singleBorderColorRgb || itemRgb(d), 0.3, 'circle', null, 0),
|
|
9591
9902
|
getSize: d => radiusOf(d) * 2 * 1.3,
|
|
9592
9903
|
getColor: d => [255, 255, 255, this._iconAlpha(d)],
|
|
9904
|
+
...(alignOf.active ? { getPixelOffset: alignOf } : {}),
|
|
9593
9905
|
sizeUnits: 'pixels',
|
|
9594
9906
|
billboard: true,
|
|
9595
9907
|
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
@@ -9660,7 +9972,21 @@
|
|
|
9660
9972
|
const boxRgb = hexOrNamedToRgb(this.style.boxcolor || '#eeeeee');
|
|
9661
9973
|
const boxAlpha = Math.round(255 * (styleNum(this.style.boxopacity) || 1));
|
|
9662
9974
|
const borderRgb = hexOrNamedToRgb(this.style.bordercolor || '#dddddd');
|
|
9663
|
-
|
|
9975
|
+
// ScatterplotLayer has no pixel offset: a moved chart (align,
|
|
9976
|
+
// offsetx/offsety) gets its circle as an icon
|
|
9977
|
+
if (alignOf.active) layers.push(new IconLayer({
|
|
9978
|
+
id: `ix-box-${this.name}-g${this._iconGeneration}`,
|
|
9979
|
+
data: boxLayout, pickable: false,
|
|
9980
|
+
getPosition: b => b.d.geometry.coordinates,
|
|
9981
|
+
getIcon: b => this._buildSingleIcon(boxRgb, boxAlpha / 255, 'circle', borderRgb,
|
|
9982
|
+
Math.round(unitPx / 20 * BUBBLE_ICON_SIZE / Math.max(1, 2 * b.boxR) * 4) / 4),
|
|
9983
|
+
getSize: b => b.boxR * 2,
|
|
9984
|
+
getPixelOffset: b => alignOf(b.d),
|
|
9985
|
+
sizeUnits: 'pixels',
|
|
9986
|
+
billboard: true,
|
|
9987
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
9988
|
+
}));
|
|
9989
|
+
else layers.push(new ScatterplotLayer({
|
|
9664
9990
|
id: `ix-box-${this.name}`,
|
|
9665
9991
|
data: boxLayout, pickable: false,
|
|
9666
9992
|
getPosition: b => b.d.geometry.coordinates,
|
|
@@ -9695,7 +10021,7 @@
|
|
|
9695
10021
|
getIcon: b => b.icon,
|
|
9696
10022
|
// the icon is drawn at 2× with a stroke padding around the box
|
|
9697
10023
|
getSize: b => b.icon.height / 2,
|
|
9698
|
-
getPixelOffset: b => [(b.rect[0] + b.rect[2]) / 2, (b.rect[1] + b.rect[3]) / 2],
|
|
10024
|
+
getPixelOffset: b => { const o = alignOf(b.d); return [(b.rect[0] + b.rect[2]) / 2 + o[0], (b.rect[1] + b.rect[3]) / 2 + o[1]]; },
|
|
9699
10025
|
sizeUnits: 'pixels',
|
|
9700
10026
|
billboard: true,
|
|
9701
10027
|
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
@@ -9707,10 +10033,11 @@
|
|
|
9707
10033
|
data: combined, pickable: true,
|
|
9708
10034
|
getPosition: d => d.geometry.coordinates,
|
|
9709
10035
|
getIcon: d => d.properties.counts
|
|
9710
|
-
? this._buildBubbleIcon(d.properties.counts, this.categoryColorsRgb)
|
|
10036
|
+
? this._buildBubbleIcon(d.properties.counts, this.categoryColorsRgb, this._groupIsolation(d))
|
|
9711
10037
|
: this._buildSingleIcon(this.categoryColorsRgb[d.properties.cat], fillOpacity, this._resolveSymbolShape(d.properties), singleBorderColorRgb, singleBorderWidthPx),
|
|
9712
10038
|
getSize: d => valueRadius(sizeValueOf(d), liveZoom, this.style, this.mapOptions, this.flags, this._maxSizeValue) * 2,
|
|
9713
10039
|
getColor: d => [255, 255, 255, this._iconAlpha(d)],
|
|
10040
|
+
...(alignOf.active ? { getPixelOffset: alignOf } : {}),
|
|
9714
10041
|
sizeUnits: 'pixels',
|
|
9715
10042
|
billboard: true,
|
|
9716
10043
|
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
@@ -9735,13 +10062,13 @@
|
|
|
9735
10062
|
getTextAnchor: b => b.title.anchor,
|
|
9736
10063
|
// top of the text ≈ first baseline − 0.8 font
|
|
9737
10064
|
getAlignmentBaseline: 'top',
|
|
9738
|
-
getPixelOffset: b => [b.title.x, b.title.top],
|
|
10065
|
+
getPixelOffset: b => { const o = alignOf(b.d); return [b.title.x + o[0], b.title.top + o[1]]; },
|
|
9739
10066
|
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
9740
10067
|
}));
|
|
9741
10068
|
}
|
|
9742
10069
|
|
|
9743
10070
|
// VALUES: bold value label centered on each bubble (see
|
|
9744
|
-
//
|
|
10071
|
+
// flatValueText/valuesFontSizePx above). Labels are their own
|
|
9745
10072
|
// layers, added last in the array so they draw on top of every icon
|
|
9746
10073
|
// layer (a small bubble's label can still show over a bigger
|
|
9747
10074
|
// neighboring bubble's icon — a known, accepted gap). Entries whose
|
|
@@ -9750,14 +10077,16 @@
|
|
|
9750
10077
|
// small to bother" gate.
|
|
9751
10078
|
if (this.flags.has('VALUES') && !valuesHiddenByScale(this.style, zoom)) {
|
|
9752
10079
|
const valueScale = styleNum(this.style.valuescale) || 1;
|
|
10080
|
+
const textOpts = this._valueTextOpts();
|
|
9753
10081
|
|
|
9754
10082
|
const pointLabels = individual.reduce((out, d) => {
|
|
9755
10083
|
const radius = valueRadius(d.properties.value, liveZoom, this.style, this.mapOptions, this.flags, this._maxSizeValue);
|
|
9756
10084
|
// flat's VALUES label prints the value (nValuesA), not the size
|
|
9757
|
-
const text =
|
|
10085
|
+
const text = flatValueText(d.properties.raw, this.binding.title && d.properties.raw ? d.properties.raw[this.binding.title] : undefined,
|
|
10086
|
+
d.properties.classValue !== undefined ? d.properties.classValue : d.properties.value, this.style, this.flags, textOpts);
|
|
9758
10087
|
const fontSize = valuesFontSizePx(radius, text, valueScale);
|
|
9759
10088
|
if (fontSize > VALUES_MIN_FONT_PX) {
|
|
9760
|
-
out.push({ geometry: d.geometry, text, fontSize, color: resolveTextColor(this.style, contrastTextColor(this.categoryColorsRgb[d.properties.cat])) });
|
|
10089
|
+
out.push({ geometry: d.geometry, text, fontSize, color: resolveTextColor(this.style, contrastTextColor(this.categoryColorsRgb[d.properties.cat])), pixelOffset: alignOf(d) });
|
|
9761
10090
|
}
|
|
9762
10091
|
return out;
|
|
9763
10092
|
}, []);
|
|
@@ -9777,18 +10106,19 @@
|
|
|
9777
10106
|
const outerRadiusPx = valueRadius(d.properties.total, liveZoom, this.style, this.mapOptions, this.flags, this._maxSizeValue);
|
|
9778
10107
|
const iconSizePx = outerRadiusPx * 2; // matches the cluster IconLayer's own getSize (*2) below
|
|
9779
10108
|
const pxPerCanvasUnit = iconSizePx / BUBBLE_ICON_SIZE;
|
|
9780
|
-
const
|
|
10109
|
+
const iso = this._groupIsolation(d);
|
|
10110
|
+
const { present, radii, offsets, fitScale } = (iso && isolatedBubblePackLayout(counts, BUBBLE_ICON_SIZE, iso)) || computeBubblePackLayout(counts, BUBBLE_ICON_SIZE);
|
|
9781
10111
|
|
|
9782
10112
|
present.forEach((p, i) => {
|
|
9783
10113
|
// one part for a range-classed cell: its class value, if separate
|
|
9784
|
-
const text =
|
|
10114
|
+
const text = flatValueText(this._categoryValueRecord(p.i), null, d.properties.classTotal !== undefined && present.length === 1 ? d.properties.classTotal : p.c, this.style, this.flags, textOpts);
|
|
9785
10115
|
const subRadiusPx = radii[i] * fitScale * pxPerCanvasUnit;
|
|
9786
10116
|
const fontSize = valuesFontSizePx(subRadiusPx, text, valueScale);
|
|
9787
10117
|
if (fontSize > VALUES_MIN_FONT_PX) {
|
|
9788
10118
|
out.push({
|
|
9789
10119
|
geometry: d.geometry, text, fontSize,
|
|
9790
10120
|
color: resolveTextColor(this.style, contrastTextColor(this.categoryColorsRgb[p.i])),
|
|
9791
|
-
pixelOffset: [offsets[i].x * fitScale * pxPerCanvasUnit, offsets[i].y * fitScale * pxPerCanvasUnit]
|
|
10121
|
+
pixelOffset: [offsets[i].x * fitScale * pxPerCanvasUnit + alignOf(d)[0], offsets[i].y * fitScale * pxPerCanvasUnit + alignOf(d)[1]]
|
|
9792
10122
|
});
|
|
9793
10123
|
}
|
|
9794
10124
|
});
|
|
@@ -9807,13 +10137,210 @@
|
|
|
9807
10137
|
getTextAnchor: 'middle',
|
|
9808
10138
|
getAlignmentBaseline: 'center'
|
|
9809
10139
|
};
|
|
9810
|
-
if (pointLabels.length) layers.push(new TextLayer({ id: `ix-points-values-${this.name}`, data: pointLabels, ...textLayerCommonProps }));
|
|
10140
|
+
if (pointLabels.length) layers.push(new TextLayer({ id: `ix-points-values-${this.name}`, data: pointLabels, ...(alignOf.active ? { getPixelOffset: d => d.pixelOffset } : {}), ...textLayerCommonProps }));
|
|
9811
10141
|
if (groupLabels.length) layers.push(new TextLayer({ id: `ix-cluster-values-${this.name}`, data: groupLabels, getPixelOffset: d => d.pixelOffset, ...textLayerCommonProps }));
|
|
9812
10142
|
}
|
|
9813
10143
|
|
|
9814
10144
|
return layers;
|
|
9815
10145
|
}
|
|
9816
10146
|
|
|
10147
|
+
// flat's chart position for this build (flatChartAlignOffset): a
|
|
10148
|
+
// function item → pixel offset, the same for every part of one chart
|
|
10149
|
+
// (symbol, glow, shadow, box, title, value text). unit is flat's
|
|
10150
|
+
// normalX(1) on screen, the object zoom × style.scale.
|
|
10151
|
+
_chartAlignFn(liveZoom, radiusOf, textOnly = false) {
|
|
10152
|
+
const st = this.style;
|
|
10153
|
+
const none = () => [0, 0];
|
|
10154
|
+
none.active = false;
|
|
10155
|
+
if (!st.align && !styleNum(st.offsetx) && !styleNum(st.offsety)) return none;
|
|
10156
|
+
const unit = objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(st.scale) || 1);
|
|
10157
|
+
const symbol = flatChartBranch(this.flags) === 'symbol';
|
|
10158
|
+
const normalR = NORMAL_RADIUS_PX * unit; // flat's normalX(chart size / 2)
|
|
10159
|
+
const cache = new Map();
|
|
10160
|
+
const fn = d => {
|
|
10161
|
+
let v = cache.get(d);
|
|
10162
|
+
if (v) return v;
|
|
10163
|
+
const r = radiusOf(d);
|
|
10164
|
+
v = flatChartAlignOffset(st, { unit, symbolScale: symbol ? r / (2 * normalR) : 1, half: symbol ? normalR : r, orig: r + (textOnly ? 0 : 5 * unit) });
|
|
10165
|
+
cache.set(d, v);
|
|
10166
|
+
return v;
|
|
10167
|
+
};
|
|
10168
|
+
fn.active = true;
|
|
10169
|
+
return fn;
|
|
10170
|
+
}
|
|
10171
|
+
|
|
10172
|
+
// the record an aggregated part of category i stands for in its value
|
|
10173
|
+
// text: with style.valuefield the theme's categorical field, flat prints
|
|
10174
|
+
// the class name (the records' own valuefield value) — otherwise none
|
|
10175
|
+
_categoryValueRecord(i) { return categoryValueRecord(this.style, this.binding, this.flags, this.categoryLabels, i); }
|
|
10176
|
+
|
|
10177
|
+
// value-text options (flatValueText): flat's formatValue NOBREAKS for a
|
|
10178
|
+
// theme of values within 1000-3000, the max value for the decimals
|
|
10179
|
+
_valueTextOpts() {
|
|
10180
|
+
// the range stats (_valueMin/_valueMax, per zoom for AGGREGATE) when
|
|
10181
|
+
// there are any, else the value field's range over the data (cached)
|
|
10182
|
+
let vMin = this._valueMin, vMax = this._valueMax;
|
|
10183
|
+
if (vMin === undefined) {
|
|
10184
|
+
if (this._valueTextRangeFor !== this.features) {
|
|
10185
|
+
let lo = Infinity, hi = -Infinity;
|
|
10186
|
+
const field = this.binding.value;
|
|
10187
|
+
for (const f of this.features || []) {
|
|
10188
|
+
const v = field === '$item$' ? 1 : parseFloat(f.properties && f.properties[field]);
|
|
10189
|
+
if (isNaN(v)) continue;
|
|
10190
|
+
if (v < lo) lo = v;
|
|
10191
|
+
if (v > hi) hi = v;
|
|
10192
|
+
}
|
|
10193
|
+
this._valueTextRangeFor = this.features;
|
|
10194
|
+
this._valueTextRange = [lo, hi];
|
|
10195
|
+
}
|
|
10196
|
+
[vMin, vMax] = this._valueTextRange;
|
|
10197
|
+
}
|
|
10198
|
+
return { noBreaks: flatNoBreaks(vMin, vMax), maxValue: styleNum(this.style.normalsizevalue) || (isFinite(vMax) ? vMax : undefined) };
|
|
10199
|
+
}
|
|
10200
|
+
|
|
10201
|
+
// CHART|LABEL (maptheme.js 20647-21037), at the chart radius r of the
|
|
10202
|
+
// size value:
|
|
10203
|
+
// - LABEL: a rounded rectangle 2.05r wide and 1.6·f high (f = min(0.8r,
|
|
10204
|
+
// 3.4r / the value's text length)) from −r and −0.83f, corners 2r/15
|
|
10205
|
+
// (normalX(2r / normal radius)), in the item color at fillopacity
|
|
10206
|
+
// (default 1), stroked in linecolor or the color × 0.7 (NOLINES: none)
|
|
10207
|
+
// linewidth (0.1) · unit · √(r / normal radius) wide (OUTLINE:
|
|
10208
|
+
// unit · min(1, r / normal radius)), opaque with OUTLINE or a
|
|
10209
|
+
// linecolor, else 1 below fillopacity 0.5, 0.3 above; with VALUES the
|
|
10210
|
+
// text in bold, min(0.8r, 3.3r / its length) · valuescale, in
|
|
10211
|
+
// valuecolor / textcolor, else ChartColors.textColor of a class
|
|
10212
|
+
// color (white for a single-color theme). Flat's drop shadow rects
|
|
10213
|
+
// (20950) never draw: they need fOrigShadow without fShadow, which
|
|
10214
|
+
// the shadow gate (16740-16745) leaves equal.
|
|
10215
|
+
// - LABEL|TEXTONLY: no box, only an invisible hit rect (the
|
|
10216
|
+
// hover/tooltip target) and, with VALUES, the text at r·0.8·valuescale
|
|
10217
|
+
// in normal weight, valuecolor/textcolor (else black), over a halo in
|
|
10218
|
+
// the item color (stroke font/7, opacity 0.5).
|
|
10219
|
+
// Both moved by the chart position (flatChartAlignOffset). The text is
|
|
10220
|
+
// the hover target of TEXTONLY, the box of LABEL.
|
|
10221
|
+
_buildLabelChartLayers(combined, zoom, liveZoom, sizeValueOf) {
|
|
10222
|
+
const textOnly = this.flags.has('TEXTONLY');
|
|
10223
|
+
const showValues = this.flags.has('VALUES') && !valuesHiddenByScale(this.style, zoom);
|
|
10224
|
+
if (textOnly && !showValues) return [];
|
|
10225
|
+
const radiusOf = d => valueRadius(sizeValueOf(d), liveZoom, this.style, this.mapOptions, this.flags, this._maxSizeValue);
|
|
10226
|
+
const alignOf = this._chartAlignFn(liveZoom, radiusOf, textOnly);
|
|
10227
|
+
const valueScale = styleNum(this.style.valuescale) || 1;
|
|
10228
|
+
const textOverride = this.style.valuecolor || this.style.textcolor;
|
|
10229
|
+
const opts = this._valueTextOpts();
|
|
10230
|
+
const itemRgb = props => this.categoryColorsRgb[props.counts ? dominant(props.counts) : props.cat] || [128, 128, 128];
|
|
10231
|
+
const valueOf = props => (props.counts ? props.total : (props.classValue !== undefined ? props.classValue : props.value));
|
|
10232
|
+
const textOf = props => flatValueText(props.counts ? this._categoryValueRecord(dominant(props.counts)) : props.raw,
|
|
10233
|
+
this.binding.title && props.raw ? props.raw[this.binding.title] : undefined, valueOf(props), this.style, this.flags, opts);
|
|
10234
|
+
|
|
10235
|
+
if (textOnly) {
|
|
10236
|
+
const textRgb = textOverride ? hexOrNamedToRgb(textOverride) : [0, 0, 0];
|
|
10237
|
+
const labels = [];
|
|
10238
|
+
for (const d of combined) {
|
|
10239
|
+
const r = radiusOf(d);
|
|
10240
|
+
const fontSize = r * 0.8 * valueScale;
|
|
10241
|
+
if (!(fontSize > 0)) continue;
|
|
10242
|
+
labels.push({ ...d, text: textOf(d.properties), fontSize, halo: itemRgb(d.properties), pixelOffset: alignOf(d) });
|
|
10243
|
+
}
|
|
10244
|
+
// TextLayer's outlineColor is one per layer: a layer per halo color
|
|
10245
|
+
const byHalo = new Map();
|
|
10246
|
+
for (const l of labels) {
|
|
10247
|
+
const key = l.halo.slice(0, 3).join(',');
|
|
10248
|
+
if (!byHalo.has(key)) byHalo.set(key, []);
|
|
10249
|
+
byHalo.get(key).push(l);
|
|
10250
|
+
}
|
|
10251
|
+
return [...byHalo.values()].map((data, i) => new TextLayer({
|
|
10252
|
+
// the ix-bubbles- id (generation-like suffix) routes hover/click to this theme
|
|
10253
|
+
id: `ix-bubbles-${this.name}-g${i}`,
|
|
10254
|
+
data, pickable: true,
|
|
10255
|
+
getPosition: d => d.geometry.coordinates,
|
|
10256
|
+
getText: d => d.text,
|
|
10257
|
+
getSize: d => d.fontSize,
|
|
10258
|
+
getColor: d => [...textRgb.slice(0, 3), this._iconAlpha(d)],
|
|
10259
|
+
getPixelOffset: d => d.pixelOffset,
|
|
10260
|
+
sizeUnits: 'pixels',
|
|
10261
|
+
fontFamily: 'arial',
|
|
10262
|
+
fontWeight: 'normal',
|
|
10263
|
+
characterSet: 'auto',
|
|
10264
|
+
fontSettings: { sdf: true },
|
|
10265
|
+
outlineWidth: 1 / 7,
|
|
10266
|
+
outlineColor: [...data[0].halo.slice(0, 3), 128],
|
|
10267
|
+
getTextAnchor: 'middle',
|
|
10268
|
+
getAlignmentBaseline: 'center',
|
|
10269
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
10270
|
+
}));
|
|
10271
|
+
}
|
|
10272
|
+
|
|
10273
|
+
const unit = objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(this.style.scale) || 1);
|
|
10274
|
+
const normalR = NORMAL_RADIUS_PX * unit;
|
|
10275
|
+
const fillOpacity = styleNum(this.style.fillopacity) || 1;
|
|
10276
|
+
const lineWidth = styleNum(this.style.linewidth) || 0.1;
|
|
10277
|
+
const outline = flatFlag(this.flags, 'OUTLINE');
|
|
10278
|
+
const lineColor = this.style.linecolor && styleLineColor(this.style.linecolor) !== 'none' ? styleLineColor(this.style.linecolor) : null;
|
|
10279
|
+
const noLines = flatFlag(this.flags, 'NOLINES');
|
|
10280
|
+
const strokeOpacity = outline || lineColor ? 1 : (fillOpacity < 0.5 ? 1 : 0.3);
|
|
10281
|
+
const multiColor = (this.categoryColorsRgb || []).length > 1;
|
|
10282
|
+
const css = rgb => `rgb(${rgb[0]},${rgb[1]},${rgb[2]})`;
|
|
10283
|
+
// the box is sized by the value's own text, not the valuefield's
|
|
10284
|
+
const boxStyle = Object.assign({}, this.style, { valuefield: undefined });
|
|
10285
|
+
const boxes = [], texts = [];
|
|
10286
|
+
for (const d of combined) {
|
|
10287
|
+
const r = radiusOf(d);
|
|
10288
|
+
if (!(r > 0)) continue;
|
|
10289
|
+
const props = d.properties;
|
|
10290
|
+
const rgb = itemRgb(props);
|
|
10291
|
+
const boxText = flatValueText(null, null, valueOf(props), boxStyle, this.flags, opts);
|
|
10292
|
+
const f = Math.min(r * 0.8, r * 3.4 / Math.max(1, boxText.length));
|
|
10293
|
+
const ratio = r / normalR;
|
|
10294
|
+
const icon = this._buildBoxIcon(2.05 * r, 1.6 * f, 2 * r / 15,
|
|
10295
|
+
outline ? unit * Math.min(1, ratio) : unit * lineWidth * Math.sqrt(ratio),
|
|
10296
|
+
{ fill: css(rgb), fillOpacity, stroke: noLines ? null : (lineColor || css(flatDerivateRgb(rgb, 0.7))), strokeOpacity });
|
|
10297
|
+
const o = alignOf(d);
|
|
10298
|
+
// the box's center relative to the chart's: x from −r to 1.05r, y from −0.83f to 0.77f
|
|
10299
|
+
boxes.push({ ...d, icon, pixelOffset: [o[0] + 0.025 * r, o[1] - 0.03 * f] });
|
|
10300
|
+
if (showValues) {
|
|
10301
|
+
const text = textOf(props);
|
|
10302
|
+
const fontSize = Math.min(r * 0.8, r * 3.3 / Math.max(1, text.length)) * valueScale;
|
|
10303
|
+
const color = textOverride ? hexOrNamedToRgb(textOverride) : (multiColor ? flatChartTextRgb(rgb) : [255, 255, 255]);
|
|
10304
|
+
if (fontSize > 0) texts.push({ ...d, text, fontSize, color, pixelOffset: o });
|
|
10305
|
+
}
|
|
10306
|
+
}
|
|
10307
|
+
const layers = [];
|
|
10308
|
+
if (boxes.length) {
|
|
10309
|
+
layers.push(new IconLayer({
|
|
10310
|
+
id: `ix-bubbles-${this.name}-g${this._iconGeneration}`,
|
|
10311
|
+
data: boxes, pickable: true,
|
|
10312
|
+
getPosition: d => d.geometry.coordinates,
|
|
10313
|
+
getIcon: d => d.icon,
|
|
10314
|
+
// the icon is drawn at 2× with a stroke padding around the box
|
|
10315
|
+
getSize: d => d.icon.height / 2,
|
|
10316
|
+
getPixelOffset: d => d.pixelOffset,
|
|
10317
|
+
getColor: d => [255, 255, 255, this._iconAlpha(d)],
|
|
10318
|
+
sizeUnits: 'pixels',
|
|
10319
|
+
billboard: true,
|
|
10320
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
10321
|
+
}));
|
|
10322
|
+
}
|
|
10323
|
+
if (texts.length) {
|
|
10324
|
+
layers.push(new TextLayer({
|
|
10325
|
+
id: `ix-labels-${this.name}`,
|
|
10326
|
+
data: texts, pickable: false,
|
|
10327
|
+
getPosition: d => d.geometry.coordinates,
|
|
10328
|
+
getText: d => d.text,
|
|
10329
|
+
getSize: d => d.fontSize,
|
|
10330
|
+
getColor: d => [...d.color.slice(0, 3), this._iconAlpha(d)],
|
|
10331
|
+
getPixelOffset: d => d.pixelOffset,
|
|
10332
|
+
sizeUnits: 'pixels',
|
|
10333
|
+
fontFamily: 'arial',
|
|
10334
|
+
fontWeight: 'bold',
|
|
10335
|
+
characterSet: 'auto',
|
|
10336
|
+
getTextAnchor: 'middle',
|
|
10337
|
+
getAlignmentBaseline: 'center',
|
|
10338
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
10339
|
+
}));
|
|
10340
|
+
}
|
|
10341
|
+
return layers;
|
|
10342
|
+
}
|
|
10343
|
+
|
|
9817
10344
|
// pure: oneHot → see the pure-function sections above the class
|
|
9818
10345
|
_oneHot(cat, value) { return oneHot(cat, value, this.categoryLabels.length); }
|
|
9819
10346
|
|
|
@@ -10253,7 +10780,7 @@
|
|
|
10253
10780
|
// test-only: lets test/unit/*.test.mjs call pure internals directly (the
|
|
10254
10781
|
// engine runs in a Node vm there); deliberately NOT on the ixmaps object
|
|
10255
10782
|
global.__ixmapsGlInternals = {
|
|
10256
|
-
IXMAPS_GL_VERSION,
|
|
10783
|
+
IXMAPS_GL_VERSION, computeBubblePackLayout, isolatedBubblePackLayout,
|
|
10257
10784
|
normalizeTheme, projectThemeToDefinition, withoutProjectCode, groupRecordCount, resolveBasemapStyleUrl, resolveMapTypeColor, LayerBuilder, LayerRuntime, typeStyleNumbers, styleNum,
|
|
10258
10785
|
resolveScriptUrl, isTrustedScriptUrl, loadProcessingScript, loadBrokerData, applyBrokerThemePatch, makeBrokerTheme,
|
|
10259
10786
|
equalIntervalBreaks, quantileBreaks, naturalBreaks, evenStrideSample, jenksBreakValues, partsFromBreakValues, resolvePartsClass,
|
|
@@ -10261,8 +10788,8 @@
|
|
|
10261
10788
|
resolveComposedColor, computeRangeClasses, colorSchemeClassCount, resolveDopacityAlpha,
|
|
10262
10789
|
flatColorSweep, applyClassesToColorScheme, resolveClassColors, flatOutlierStats, parseCssColor, flatLegendLook,
|
|
10263
10790
|
flatToMapLibreZoom, mapLibreToFlatZoom, scaleDenominatorAt, resolveZoomReference, resolveAggregationPx, valuesHiddenByScale,
|
|
10264
|
-
fetchLayerData, parseCsvText, dataTableRows, geometryRowsToFeatureCollection, filterFlatValues, applyField100, field100Binding, rangeClassLegendTotals, resolveAggregateValue, classValueSeparate, cellAggregatedValues, oneHot, groupCoLocated, aggregateOnGrid, GridAggregateIndex, flatRangeParts, aggregateField100,
|
|
10265
|
-
applyWhereFilter, joinChartPositions, flatLookupKey, flatShapeCenter, chartHiddenByScale, featuresHiddenByScale, boxHiddenByScale, flatShadowOn, snapToAggregationGrid,
|
|
10791
|
+
fetchLayerData, parseCsvText, dataTableRows, geometryRowsToFeatureCollection, filterFlatValues, applyField100, field100Binding, rangeClassLegendTotals, resolveAggregateValue, classValueSeparate, cellAggregatedValues, oneHot, groupCoLocated, aggregateOnGrid, GridAggregateIndex, flatRangeParts, aggregateField100, valueRadius, itemPlotGeometry, itemAnchor, objectZoomFactor, resolveZoomReference, defaultNormalSizeValue,
|
|
10792
|
+
applyWhereFilter, joinChartPositions, flatLookupKey, flatShapeCenter, chartHiddenByScale, glowHiddenByScale, isSymbolChart, flatChartAlignOffset, flatChartBranch, flatValueText, categoryValueRecord, flatDerivateRgb, flatChartTextRgb, flatGroupedValue, flatNoBreaks, featuresHiddenByScale, boxHiddenByScale, flatShadowOn, snapToAggregationGrid,
|
|
10266
10793
|
flatFormatValue,
|
|
10267
10794
|
dataCacheKey, dataCacheDisabled, cachedLayerData, featuresBounds, wantsZoomToExtent, legendIsOn,
|
|
10268
10795
|
};
|