ixmaps-gl 0.2.20 → 0.2.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +45 -8
  2. package/ixmaps-gl.js +2134 -95
  3. package/package.json +1 -1
package/ixmaps-gl.js CHANGED
@@ -53,12 +53,22 @@
53
53
  // PIE|DONUT) a pie per record, a slice per value field, flat's sizing,
54
54
  // CENTER part and VALUES leader labels (_buildPieLayers; 3D/VOLUME/
55
55
  // STARBURST/RAYS/BOW variants are noted, drawn flat; AGGREGATE and
56
- // CATEGORICAL pies not yet). These
56
+ // CATEGORICAL pies a pie per item, a slice per category or field, flat's
57
+ // aggregation), CHART|VECTOR and CHART|BEZIER a flow line
58
+ // per item from its position to its second one (binding position2 /
59
+ // style lookupfield2), per record or AGGREGATE'd per origin–destination
60
+ // pair, with flat's widths, colors, arrow heads and GRADIENT / FADEIN
61
+ // (_buildVectorLayers; VALUES, DYNAMICWIDTH, LONG and the DASH animation
62
+ // are noted, see VECTOR_INERT_FLAGS), CHART|BAR (and BARS) a bar chart
63
+ // per record or aggregated item, a bar per value field or category, flat's
64
+ // sizing, HORZ / STACKED / SORT, POINTER and ARROW pointers, VALUES texts,
65
+ // BOX and TITLE (_buildBarLayers; 3D, VOLUME, DOTTED, TRENDLINE, the axis
66
+ // labels … are noted, drawn plain, see BAR_INERT_FLAGS). These
57
67
  // base types are dispatched by buildDeckLayers in the real engine's own
58
68
  // precedence order — DOT is checked first because the real engine's DOT
59
69
  // bypasses the whole drawChart/modifier pipeline rather than being
60
- // "BUBBLE with the size locked." Other real base types (QUAD, BEZIER,
61
- // VECTOR, WAFFLE, BAR, and SYMBOL's own shape variants) are NOT
70
+ // "BUBBLE with the size locked." Other real base types (QUAD, WAFFLE,
71
+ // and SYMBOL's own shape variants) are NOT
62
72
  // implemented yet — adding one means a new _buildXLayers() method plus a
63
73
  // dispatch line, not a rewrite, but each should be added deliberately
64
74
  // (checking, per real source, which modifiers it actually shares with
@@ -86,7 +96,7 @@
86
96
  // ---------------------------------------------------------------
87
97
  // this engine's release (= package.json "version", checked by
88
98
  // test/unit/version.test.mjs); ixmaps.glVersion, and logged once at start
89
- const IXMAPS_GL_VERSION = '0.2.20';
99
+ const IXMAPS_GL_VERSION = '0.2.21';
90
100
 
91
101
  const LIB_URLS = {
92
102
  // Bumped 3.6.2 -> 5.x (2026-09-21, globe-projection compat fix): native
@@ -203,10 +213,10 @@
203
213
  // byte-grep of the shipped file (deck.gl's own @deck.gl/maplibre
204
214
  // overview page claims ES-modules-only, which the actual UMD bundle
205
215
  // contradicts).
206
- let IconLayer, ScatterplotLayer, GeoJsonLayer, MapLibreOverlay, TextLayer;
207
- // PieSliceLayer (see makePieSliceLayer) is derived from ScatterplotLayer
208
- // once deck.gl is there
209
- let PieSliceLayer;
216
+ let IconLayer, ScatterplotLayer, GeoJsonLayer, MapLibreOverlay, TextLayer, PathLayer, PathStyleExtension;
217
+ // PieSliceLayer (see makePieSliceLayer) and BarShapeLayer
218
+ // (makeBarShapeLayer) are derived from ScatterplotLayer once deck.gl is there
219
+ let PieSliceLayer, BarShapeLayer;
210
220
 
211
221
  // CHART|PIE / DONUT slices (_buildPieLayers): one instance per slice — a
212
222
  // ScatterplotLayer circle (billboarded, pixel radius, pixel offset, the
@@ -310,6 +320,107 @@ in vec4 vPieLineColor;
310
320
  return Layer;
311
321
  }
312
322
 
323
+ // CHART|BAR / BARS bars (_buildBarLayers): one instance per bar — a
324
+ // ScatterplotLayer circle (billboarded, pixel radius, pixel offset, the
325
+ // same projection path as every other chart, globe included) whose
326
+ // fragment shader keeps only the bar's shape inside the circle and draws
327
+ // flat's outline along its edge, centered on it (an SVG stroke).
328
+ // getBarShape: [half width, shaft half width, shaft length, band] px
329
+ // getBarShape2: [head length, direction (0 up, 1 right, 2 down,
330
+ // 3 left; + 4: never picked), outline width px, the base's distance from
331
+ // the circle's center along the direction] — the circle
332
+ // centered on the shape, its radius covering it
333
+ // A plain bar is a rectangle (shaft = half width, no band, no head); a
334
+ // POINTER / ARROW its shaft, a band as wide as the bar and a triangular
335
+ // head (flat's pointer path). Why not a canvas icon per bar: every bar
336
+ // has its own length (~1 000 distinct icons per view on a comuni page,
337
+ // beyond deck.gl's never-evicting icon atlas); nor polygons in map
338
+ // coordinates, re-tessellated at every zoom step and wrong on the globe.
339
+ function makeBarShapeLayer(Base) {
340
+ class Layer extends Base {
341
+ getShaders() {
342
+ const shaders = super.getShaders();
343
+ const inject = Object.assign({}, shaders.inject || {});
344
+ inject['vs:#decl'] = (inject['vs:#decl'] || '') + `
345
+ in vec4 instanceBarShapes;
346
+ in vec4 instanceBarShapes2;
347
+ out vec4 vBarShape;
348
+ out vec4 vBarShape2;
349
+ out float vBarRadius;
350
+ out vec4 vBarLineColor;
351
+ `;
352
+ inject['vs:#main-end'] = (inject['vs:#main-end'] || '') + `
353
+ vBarShape = instanceBarShapes;
354
+ vBarShape2 = instanceBarShapes2;
355
+ vBarRadius = outerRadiusPixels;
356
+ vBarLineColor = vec4(instanceLineColors.rgb, instanceLineColors.a * layer.opacity);
357
+ `;
358
+ inject['fs:#decl'] = (inject['fs:#decl'] || '') + `
359
+ in vec4 vBarShape;
360
+ in vec4 vBarShape2;
361
+ in float vBarRadius;
362
+ in vec4 vBarLineColor;
363
+ float barSegDist(vec2 p, vec2 a, vec2 b) {
364
+ vec2 ab = b - a;
365
+ float l2 = dot(ab, ab);
366
+ float t = l2 > 0.0 ? clamp(dot(p - a, ab) / l2, 0.0, 1.0) : 0.0;
367
+ return length(p - a - t * ab);
368
+ }
369
+ `;
370
+ // before the picking module's own filter (it replaces the color
371
+ // when picking): the discards shape the pick area too
372
+ inject['fs:DECKGL_FILTER_COLOR'] = (inject['fs:DECKGL_FILTER_COLOR'] || '') + `
373
+ {
374
+ vec2 barS = geometry.uv * vBarRadius;
375
+ barS.y = -barS.y;
376
+ // a direction beyond 3: drawn, never picked (a chart's BOX)
377
+ float barDir = vBarShape2.y;
378
+ bool barNoPick = barDir > 3.5;
379
+ if (barNoPick) barDir -= 4.0;
380
+ if (barNoPick && picking.isActive > 0.5) discard;
381
+ vec2 barD = barDir < 0.5 ? vec2(0.0, -1.0) : barDir < 1.5 ? vec2(1.0, 0.0) : barDir < 2.5 ? vec2(0.0, 1.0) : vec2(-1.0, 0.0);
382
+ vec2 barP = vec2(abs(dot(barS, vec2(-barD.y, barD.x))), dot(barS, barD) + vBarShape2.w);
383
+ float barW2 = vBarShape.x, barSW = vBarShape.y, barL = vBarShape.z, barB = vBarShape.w, barH = vBarShape2.x;
384
+ bool barIn = (barP.x <= barSW && barP.y >= 0.0 && barP.y <= barL)
385
+ || (barP.x <= barW2 && barP.y >= barL && barP.y <= barL + barB)
386
+ || (barH > 0.0 && barP.y >= barL + barB && barP.y <= barL + barB + barH && barP.x <= barW2 * (1.0 - (barP.y - barL - barB) / barH));
387
+ float barD0 = barSegDist(barP, vec2(0.0, 0.0), vec2(barSW, 0.0));
388
+ barD0 = min(barD0, barSegDist(barP, vec2(barSW, 0.0), vec2(barSW, barL)));
389
+ barD0 = min(barD0, barSegDist(barP, vec2(barSW, barL), vec2(barW2, barL)));
390
+ barD0 = min(barD0, barSegDist(barP, vec2(barW2, barL), vec2(barW2, barL + barB)));
391
+ barD0 = min(barD0, barSegDist(barP, vec2(barW2, barL + barB), vec2(0.0, barL + barB + barH)));
392
+ // in device pixels (fwidth: CSS px per device pixel): edges anti-aliased
393
+ // over one device pixel, a hairline outline fades in by its width
394
+ float barAA = max(fwidth(barS.x), 1e-4);
395
+ float barFill = barIn ? clamp(barD0 / barAA + 0.5, 0.0, 1.0) : clamp(0.5 - barD0 / barAA, 0.0, 1.0);
396
+ // a bar thinner than a device pixel covers only its share of it (a 0
397
+ // value's bar is no line)
398
+ barFill = min(barFill, clamp(min(2.0 * barW2, barL + barB + barH) / barAA, 0.0, 1.0));
399
+ float barLW = vBarShape2.z;
400
+ float barLine = barLW > 0.0 ? clamp(min(barLW * 0.5 / barAA + 0.5 - barD0 / barAA, barLW / barAA), 0.0, 1.0) : 0.0;
401
+ color = vec4(mix(color.rgb, vBarLineColor.rgb, barLine), mix(color.a * barFill, vBarLineColor.a, barLine));
402
+ if (color.a <= 0.0) discard;
403
+ }
404
+ `;
405
+ shaders.inject = inject;
406
+ return shaders;
407
+ }
408
+ initializeState() {
409
+ super.initializeState();
410
+ this.getAttributeManager().addInstanced({
411
+ instanceBarShapes: { size: 4, accessor: 'getBarShape', defaultValue: [0, 0, 0, 0] },
412
+ instanceBarShapes2: { size: 4, accessor: 'getBarShape2', defaultValue: [0, 0, 0, 0] }
413
+ });
414
+ }
415
+ }
416
+ Layer.layerName = 'BarShapeLayer';
417
+ Layer.defaultProps = {
418
+ getBarShape: { type: 'accessor', value: [0, 0, 0, 0] },
419
+ getBarShape2: { type: 'accessor', value: [0, 0, 0, 0] }
420
+ };
421
+ return Layer;
422
+ }
423
+
313
424
  // Cached so multiple ixmaps.Map() calls on one page (or a page that
314
425
  // still has its own static <script> tags for these libraries) only
315
426
  // ever fetch once: a library a page already loaded is reused when it is
@@ -385,8 +496,9 @@ in vec4 vPieLineColor;
385
496
  global.Mustache ? Promise.resolve() : loadScript(LIB_URLS.mustache),
386
497
  [...document.styleSheets].some(s => s.href === LIB_URLS.maplibreCss) ? Promise.resolve() : loadStylesheet(LIB_URLS.maplibreCss)
387
498
  ]).then(() => {
388
- ({ IconLayer, ScatterplotLayer, GeoJsonLayer, MapLibreOverlay, TextLayer } = global.deck);
499
+ ({ IconLayer, ScatterplotLayer, GeoJsonLayer, MapLibreOverlay, TextLayer, PathLayer, PathStyleExtension } = global.deck);
389
500
  PieSliceLayer = makePieSliceLayer(ScatterplotLayer);
501
+ BarShapeLayer = makeBarShapeLayer(ScatterplotLayer);
390
502
  // which engine and libraries this page really runs (a page may
391
503
  // bring its own MapLibre/deck.gl — the loaded ones are named)
392
504
  const ml = global.maplibregl && typeof global.maplibregl.getVersion === 'function' ? global.maplibregl.getVersion() : '?';
@@ -1547,13 +1659,17 @@ in vec4 vPieLineColor;
1547
1659
  const lookupField = spec.binding.lookup;
1548
1660
  let layers = String(spec.name).split('|');
1549
1661
  if (layers.length > 1 && spec.flags && spec.flags.has('DIFFERENCE')) layers = layers.slice(-1);
1662
+ const lookup2 = spec.binding.lookup2;
1550
1663
  const features = [];
1551
- for (const layer of layers) {
1664
+ const indexes = layers.map(layer => {
1552
1665
  const index = new Map();
1553
1666
  runtimes.filter(r => r.name === layer && (r.flags.has('FEATURE') || r.flags.has('FEATURES'))).forEach(r => {
1554
1667
  // in theme order, as flat's layer holds the themes' shapes
1555
1668
  for (const [k, v] of featurePositionIndex(r)) { if (flatShapeWins(index.get(k), v.area)) index.set(k, v); }
1556
1669
  });
1670
+ return index;
1671
+ });
1672
+ for (const index of indexes) {
1557
1673
  if (!index.size) continue;
1558
1674
  for (const row of table.rows) {
1559
1675
  const v = row[lookupField];
@@ -1563,7 +1679,18 @@ in vec4 vPieLineColor;
1563
1679
  // joined shape, as flat) needs the shape's km² area carried along
1564
1680
  // (featureAreaKm2 prefers this marker), exactly as the choropleth
1565
1681
  // join stamps it
1566
- if (hit) features.push({ type: 'Feature', geometry: { type: 'Point', coordinates: hit.pos }, properties: row, _flatAreaKm2: hit.areaKm2 });
1682
+ if (!hit) continue;
1683
+ const f = { type: 'Feature', geometry: { type: 'Point', coordinates: hit.pos }, properties: row, _flatAreaKm2: hit.areaKm2 };
1684
+ // VECTOR / BEZIER: the second position by its own key (flat looks
1685
+ // it up in every layer of the theme, maptheme.js 10431-10436); a
1686
+ // record without one stays an item that draws no line (null)
1687
+ if (lookup2 && !String(lookup2).includes('|')) {
1688
+ const v2 = row[lookup2];
1689
+ let hit2 = null;
1690
+ if (v2 != null && v2 !== '') for (const idx of indexes) { hit2 = idx.get(flatLookupKey(v2, spec.style)); if (hit2) break; }
1691
+ f._pos2 = hit2 ? hit2.pos : null;
1692
+ }
1693
+ features.push(f);
1567
1694
  }
1568
1695
  }
1569
1696
  return { type: 'FeatureCollection', features };
@@ -2064,6 +2191,26 @@ in vec4 vPieLineColor;
2064
2191
  'POW2', 'POW3', 'NOSORT'];
2065
2192
  // flat's PIE / DONUT variants with no rendering here (see _buildPieLayers)
2066
2193
  const PIE_INERT_FLAGS = [/3D/, /VOLUME/, /HEIGHT/, /STARBURST/, /FLOWER/, /RAYS/, /BOW/, /ZOOM/, /DIRECTION/, /DIRECTED/, /POLAR/];
2194
+ // ... and of AGGREGATE / CATEGORICAL pies: GRIDSIZE's pie the size of its
2195
+ // grid cell (drawn by the SIZE rule instead), AUTOSIZE, DOPACITY, the
2196
+ // chart BOX and TITLE, GAUGE, MULTIPLE / DIFFUSE, the post-aggregation
2197
+ // value arithmetic (DIFFERENCE, FRACTION, PERCENT …, field100)
2198
+ const PIE_AGGREGATE_INERT_FLAGS = [/^GRIDSIZE$/, /AUTOSIZE/, /DOPACITY/, /BOX/, /TITLE/, /^GAUGE$/, /^MULTIPLE$/, /^DIFFUSE$/,
2199
+ /^DIFFERENCE$/, /^FRACTION$/, /^PERCENT$/, /^PERMILLE$/, /^PRODUCT$/, /^CALCVAL$/, /^RELATIVE$/];
2200
+ // flat's BAR / BARS variants with no rendering here (see flatBarLayout):
2201
+ // the bar is drawn plain (SMALLARROW as ARROW) — tested as substrings,
2202
+ // as flat does
2203
+ const BAR_INERT_FLAGS = [/3D/, /VOLUME/, /COLUMN/, /DOTTED/, /SMALLARROW/, /TRENDLINE/, /AXIS/, /^CLIP$/, /MORPH/,
2204
+ /ZOOM/, /NORMSIZE/, /MENUSIZE/, /INVERT/, /DIAGLEFT/, /DIAGRIGHT/, /^WIDTH$/, /^MULTIPLE$/, /^DIFFUSE$/,
2205
+ /^DIFFERENCE$/, /^FRACTION$/, /^PERCENT$/, /^PERMILLE$/, /^PRODUCT$/, /^CALCVAL$/, /^RELATIVE$/];
2206
+ // flat's VECTOR / BEZIER modifiers with no rendering here (see
2207
+ // _buildVectorLayers), the word → what happens instead
2208
+ const VECTOR_INERT_FLAGS = {
2209
+ VALUES: 'the value text along the curve is not drawn yet',
2210
+ DYNAMICWIDTH: 'not drawn yet (flat widens by the log of the length in its own map units) — the plain width instead',
2211
+ LONG: 'not drawn yet (its bow depends on flat\'s own map units) — the default bow instead',
2212
+ DASH: 'drawn as static dashes — flat\'s dash animation is not ported'
2213
+ };
2067
2214
  const _warnedFlags = new Set();
2068
2215
 
2069
2216
  // GL-PORT COMPAT: real ixmaps-flat's global ixmaps.getThemeObj(szId) /
@@ -2401,6 +2548,9 @@ in vec4 vPieLineColor;
2401
2548
  'style.valuefield': ['style', 'valuefield'],
2402
2549
  'style.timefield': ['binding', 'time'], // setThemeTimeFrame, see LayerRuntime.setTimeFrame
2403
2550
  'style.titlefield': ['binding', 'title'], // {{theme.item.title}}, see _buildTooltipContext
2551
+ // VECTOR / BEZIER's second position: a join key ("a") or lat/lon
2552
+ // fields ("a|b"), see joinChartPositions and _vectorRecords
2553
+ 'style.lookupfield2': ['binding', 'lookup2'],
2404
2554
  // the lookup key's normalization (flatLookupKey): flat's newTheme moves
2405
2555
  // binding.tonumber / digits / … into these style keys (htmlgui.js 1712)
2406
2556
  'style.lookupdigits': ['style', 'lookupdigits'],
@@ -2633,7 +2783,10 @@ in vec4 vPieLineColor;
2633
2783
  // real ixmaps-flat (htmlgui.js newTheme) merges .meta() INTO style first
2634
2784
  // — meta wins — and only then reads type, bindings and style properties
2635
2785
  const style = Object.assign({}, def.style, def.meta);
2636
- const typeStr = style.type != null ? String(style.type) : '';
2786
+ let typeStr = style.type != null ? String(style.type) : '';
2787
+ // flat (htmlgui-side theme parse, maptheme.js 1192): the old type
2788
+ // word EXACT is CATEGORICAL
2789
+ if (/\bEXACT\b/.test(typeStr) && !/\bCATEGORICAL\b/.test(typeStr)) typeStr += '|CATEGORICAL';
2637
2790
  const filter = style.filter;
2638
2791
  const title = style.title;
2639
2792
  delete style.type;
@@ -2664,6 +2817,37 @@ in vec4 vPieLineColor;
2664
2817
  console.info(`[ixmaps-gl] type flag "${flag}" on a PIE chart recognized, not drawn yet — the pie is drawn flat and plain`);
2665
2818
  }
2666
2819
  });
2820
+ // AGGREGATE / CATEGORICAL pies (_isAggregatedPie): what is not ported
2821
+ if (flags.has('AGGREGATE') || flags.has('CATEGORICAL')) {
2822
+ flags.forEach(flag => {
2823
+ const inert = PIE_AGGREGATE_INERT_FLAGS.find(re => re.test(flag));
2824
+ if (inert && !_warnedFlags.has('PIEAGG:' + flag)) {
2825
+ _warnedFlags.add('PIEAGG:' + flag);
2826
+ console.info(`[ixmaps-gl] type flag "${flag}" on an aggregated PIE chart recognized, not ported yet — ignored`);
2827
+ }
2828
+ });
2829
+ }
2830
+ }
2831
+
2832
+ // BAR variants _buildBarLayers doesn't draw (the bars are drawn plain)
2833
+ if (isBarChart(flags)) {
2834
+ flags.forEach(flag => {
2835
+ if (BAR_INERT_FLAGS.some(re => re.test(flag)) && !_warnedFlags.has('BAR:' + flag)) {
2836
+ _warnedFlags.add('BAR:' + flag);
2837
+ console.info(`[ixmaps-gl] type flag "${flag}" on a BAR chart recognized, not ported yet — the bars are drawn plain`);
2838
+ }
2839
+ });
2840
+ }
2841
+
2842
+ // VECTOR / BEZIER modifiers _buildVectorLayers doesn't draw
2843
+ if (isVectorChart(flags)) {
2844
+ flags.forEach(flag => {
2845
+ const why = VECTOR_INERT_FLAGS[flag];
2846
+ if (why && !_warnedFlags.has('VECTOR:' + flag)) {
2847
+ _warnedFlags.add('VECTOR:' + flag);
2848
+ console.info(`[ixmaps-gl] type flag "${flag}" on a VECTOR/BEZIER chart recognized, ${why}`);
2849
+ }
2850
+ });
2667
2851
  }
2668
2852
 
2669
2853
  const rawBinding = Object.assign({}, def.binding);
@@ -2726,6 +2910,9 @@ in vec4 vPieLineColor;
2726
2910
  opacity: 1; transition: opacity 0.4s ease;
2727
2911
  }
2728
2912
  .ixmaps-splash.ixmaps-splash-hidden { opacity: 0; pointer-events: none; }
2913
+ .ixmaps-splash.ixmaps-splash-dark { background: #1a1a1a; }
2914
+ .ixmaps-splash-dark .ixmaps-splash-word { color: #e8e8e8; }
2915
+ .ixmaps-splash-dark .ixmaps-splash-text { color: #9a9a9a; }
2729
2916
  .ixmaps-splash-mark { position: relative; width: 44px; height: 44px; display: flex; align-items: center; justify-content: center; }
2730
2917
  .ixmaps-splash-ping {
2731
2918
  position: absolute; width: 16px; height: 16px; border-radius: 50%;
@@ -2761,15 +2948,21 @@ in vec4 vPieLineColor;
2761
2948
  // flat's Dictionary.getLocalText (mapscript.js 5631-5648) over the page's
2762
2949
  // .local(text, translation) pairs: the whole text, else word by word
2763
2950
  // (a word's entry is written in quotes, 'word')
2951
+ // plus the page-wide entries of ixmaps.setLocalString (flat's
2952
+ // Api.setLocalString → Dictionary.replace, mapapi.js 123), read at call
2953
+ // time so strings set after the map is built count; the later
2954
+ // setLocalString wins over a .local() pair of the same text
2955
+ const _localStrings = Object.create(null);
2764
2956
  function makeLocalText(locals) {
2765
2957
  const dict = {};
2766
2958
  (locals || []).forEach(a => {
2767
2959
  if (a && typeof a[0] === 'object' && a[0]) Object.assign(dict, a[0]);
2768
2960
  else if (a && a.length >= 2) dict[a[0]] = a[1];
2769
2961
  });
2962
+ const entry = k => (_localStrings[k] !== undefined ? _localStrings[k] : dict[k]);
2770
2963
  return text => {
2771
- if (dict[text] !== undefined) return String(dict[text]);
2772
- return String(text).split(' ').map(w => (dict["'" + w + "'"] !== undefined ? String(dict["'" + w + "'"]) : w)).join(' ');
2964
+ if (entry(text) !== undefined) return String(entry(text));
2965
+ return String(text).split(' ').map(w => (entry("'" + w + "'") !== undefined ? String(entry("'" + w + "'")) : w)).join(' ');
2773
2966
  };
2774
2967
  }
2775
2968
  // make the map container the positioning context of what the engine
@@ -2782,7 +2975,7 @@ in vec4 vPieLineColor;
2782
2975
  function anchorInside(el) {
2783
2976
  if (el && getComputedStyle(el).position === 'static') el.style.position = 'relative';
2784
2977
  }
2785
- function showSplash(el, text) {
2978
+ function showSplash(el, text, dark) {
2786
2979
  ensureSplashStyle();
2787
2980
  // Checks the actual RENDERED position, not el.style (the inline
2788
2981
  // attribute only) — every example page's own #map_div already sets
@@ -2797,7 +2990,7 @@ in vec4 vPieLineColor;
2797
2990
  // this at all.
2798
2991
  anchorInside(el);
2799
2992
  const splash = document.createElement('div');
2800
- splash.className = 'ixmaps-splash';
2993
+ splash.className = 'ixmaps-splash' + (dark ? ' ixmaps-splash-dark' : '');
2801
2994
  splash.innerHTML = '<div class="ixmaps-splash-mark"><span class="ixmaps-splash-ping"></span><span class="ixmaps-splash-dot"></span></div>' +
2802
2995
  '<div class="ixmaps-splash-word">ixmaps<sup>gl</sup></div>' +
2803
2996
  '<div class="ixmaps-splash-text"></div>';
@@ -3663,7 +3856,9 @@ in vec4 vPieLineColor;
3663
3856
  // aggregation" reading (style.valuefield, falling back to
3664
3857
  // the bound size field); anything else (plain CATEGORICAL,
3665
3858
  // no SUM) falls back to a per-category record COUNT.
3666
- const useSum = flatFlag(rt.flags, 'SUM') && rt.style.valuefield;
3859
+ // a VECTOR / BEZIER flow legend sums its size field (flat's rows
3860
+ // show the flows' summed sizes, measured on the twin page)
3861
+ const useSum = flatFlag(rt.flags, 'SUM') && (rt.style.valuefield || ((rt._isVectorChart() || rt._isAggregatedPie()) && rt.binding.size));
3667
3862
  const valueField = rt.style.valuefield || rt.binding.size;
3668
3863
  // legendunits wins over the theme's general-purpose units
3669
3864
  // (used elsewhere for tooltips, e.g. _renderItemChartHtml) —
@@ -3672,13 +3867,18 @@ in vec4 vPieLineColor;
3672
3867
  // space injected), matching how style.units/legendunits are
3673
3868
  // themselves authored with their own leading space (e.g.
3674
3869
  // " MW") in real pages.
3675
- const legendUnit = rt.style.legendunits || rt.style.units || '';
3870
+ // (a VECTOR / BEZIER or aggregated PIE legend: flat's szUnit, the
3871
+ // units after a space unless they start with ".", measured on the
3872
+ // twin pages)
3873
+ const flatUnitLegend = rt._isVectorChart() || rt._isAggregatedPie();
3874
+ const rawLegendUnit = rt.style.legendunits || rt.style.units || '';
3875
+ const legendUnit = flatUnitLegend && rawLegendUnit && rawLegendUnit[0] !== '.' && rawLegendUnit[0] !== ' ' ? ' ' + rawLegendUnit : rawLegendUnit;
3676
3876
  // a per-item chart over several value fields: the row values as
3677
3877
  // flat's legend prints them (legend.js 610, 810: __formatValue
3678
3878
  // with valuedecimals, else 2, "BLANK" — whole numbers without
3679
3879
  // decimals), a colorscheme "none" part as an outline in linecolor
3680
3880
  const multiFieldLegend = rt._isMultiFieldChart();
3681
- const legendTotalText = v => (multiFieldLegend
3881
+ const legendTotalText = v => ((multiFieldLegend || flatUnitLegend)
3682
3882
  ? flatFormatValue(v, legendDecimals(rt.style), 'BLANK')
3683
3883
  : rt._formatTooltipValue(v));
3684
3884
  const outlineRgb = rt.style.linecolor && styleLineColor(rt.style.linecolor) !== 'none' ? hexOrNamedToRgb(styleLineColor(rt.style.linecolor)) : [128, 128, 128];
@@ -4238,6 +4438,11 @@ in vec4 vPieLineColor;
4238
4438
  await null;
4239
4439
  const el = document.getElementById(this.containerId);
4240
4440
  if (!el) throw new Error(`[ixmaps-gl] container #${this.containerId} not found`);
4441
+ // flat's init replaces the container's content with its own map page
4442
+ // (mappage.html via innerHTML), so a page's placeholder inside the
4443
+ // map div ("connecting to Internet ...", the fuel-price page) goes
4444
+ // away; here it would stay on top of the map — remove it the same way
4445
+ while (el.firstChild) el.removeChild(el.firstChild);
4241
4446
  // GL-PORT COMPAT: ixmaps.Map(id, {width, height}) — flat sizes its
4242
4447
  // inner map div with these and gives PIXEL values to the page's own
4243
4448
  // container too (htmlgui_flat.js:866-876, parent().css(...)); here
@@ -4248,6 +4453,11 @@ in vec4 vPieLineColor;
4248
4453
  const v = this.mapOptions && this.mapOptions[dim];
4249
4454
  if (typeof v === 'string' && /^\s*\d+(\.\d+)?\s*px\s*$/i.test(v)) el.style[dim] = v.trim();
4250
4455
  else if (typeof v === 'number' && v > 0) el.style[dim] = v + 'px';
4456
+ // a percentage stays the page's CSS — unless that CSS left the
4457
+ // container collapsed on this side (e.g. an empty flex item, the
4458
+ // UNHCR page): flat's own pixel-sized map div fills it there
4459
+ else if (typeof v === 'string' && /^\s*\d+(\.\d+)?\s*%\s*$/.test(v)
4460
+ && !(dim === 'width' ? el.clientWidth : el.clientHeight)) el.style[dim] = v.trim();
4251
4461
  }
4252
4462
  // without width/height flat's map is "fullscreen" (htmlgui_flat.js
4253
4463
  // 847-876): it fills the window, whatever the page's CSS gives the
@@ -4263,15 +4473,29 @@ in vec4 vPieLineColor;
4263
4473
  // overrides the default message, .options({splash:false}) skips it
4264
4474
  // entirely (e.g. for a map embedded somewhere a full-cover overlay
4265
4475
  // would be wrong, or one that's expected to load near-instantly).
4476
+ // dark on a dark map (no basemap style yet: flatLegendLook's name/
4477
+ // color rule, or a basemap faded to <= 0.5 over a dark page, as
4478
+ // effectiveMapDark) — a light sheet flashed between a dark page and
4479
+ // a dark map (the UNHCR page)
4480
+ const splashOpacity = parseFloat(this._engineOptions.basemapopacity);
4481
+ const splashLegendBg = this.mapOptions.legendBackground || this.mapOptions.legendbackground;
4482
+ const splashDark = flatLegendLook(this.mapOptions.mapType, isNaN(splashOpacity) ? 1 : splashOpacity, splashLegendBg).dark
4483
+ || (splashOpacity <= 0.5 && flatIsDarkColor('rgb(' + pageBackgroundColorOf(el).map(Math.round).join(',') + ')'));
4266
4484
  const splash = this._engineOptions.splash === false ? null
4267
- : showSplash(el, this._engineOptions.splashText || 'loading…');
4485
+ : showSplash(el, this._engineOptions.splashText || 'loading…', splashDark);
4268
4486
 
4269
4487
  // ixmaps.getBoundingBox()/setTitle() for brokers called below, before
4270
4488
  // the MapLibre map exists (same default view as the map gets)
4271
4489
  _titleHost = el;
4272
- const initialZoom = this._viewZoom != null && this._viewZoom !== '' ? flatToMapLibreZoom(Number(this._viewZoom)) : 8;
4490
+ // a flat SVG map in another projection (equalearth.svg): its view
4491
+ // framing and map scale (see FLAT_SVG_PROJECTIONS)
4492
+ const svgProjection = flatSvgProjectionOf(this.mapOptions.map);
4493
+ if (svgProjection) flatSvgProjectionByOptions.set(this._engineOptions, svgProjection);
4494
+ const [vLat, vLng] = this._viewCenter || [45.5, 9.2];
4495
+ const initialZoom = this._viewZoom != null && this._viewZoom !== ''
4496
+ ? flatViewToMapLibreZoom(Number(this._viewZoom), Number(vLat), Number(vLng), el.clientWidth, el.clientHeight, svgProjection)
4497
+ : 8;
4273
4498
  {
4274
- const [vLat, vLng] = this._viewCenter || [45.5, 9.2];
4275
4499
  _boundsSource = () => viewBounds(Number(vLat), Number(vLng), initialZoom, el.clientWidth, el.clientHeight);
4276
4500
  }
4277
4501
 
@@ -4390,7 +4614,7 @@ in vec4 vPieLineColor;
4390
4614
  style: mapTypeColor ? buildBlankBackgroundStyle(mapTypeColor)
4391
4615
  : resolveBasemapStyleUrl(this.mapOptions.mapType),
4392
4616
  center: [lon, lat],
4393
- zoom: this._viewZoom != null && this._viewZoom !== '' ? flatToMapLibreZoom(Number(this._viewZoom)) : 8,
4617
+ zoom: initialZoom,
4394
4618
  // MapLibre's own AttributionControl (bottom right) keeps the
4395
4619
  // basemap's required CARTO/OpenStreetMap credit; the page's own
4396
4620
  // .attribution() goes bottom left, as flat shows it
@@ -4414,6 +4638,7 @@ in vec4 vPieLineColor;
4414
4638
  // world once, at the cost of that zoom limit.
4415
4639
  renderWorldCopies: this._engineOptions.worldcopies !== false
4416
4640
  });
4641
+ engineOptionsByMap.set(map, this._engineOptions);
4417
4642
  // the live-SVG pane USER charts render into (see chartHostFor) —
4418
4643
  // registered here, keyed by this map's options object, so every
4419
4644
  // runtime (including later loadProject/map.layer ones) finds it
@@ -4461,7 +4686,7 @@ in vec4 vPieLineColor;
4461
4686
  if (typeof hook !== 'function' || !prev) return;
4462
4687
  const p = map.project(c), q = map.project(prev.c);
4463
4688
  if (z === prev.z && Math.abs(p.x - q.x) <= 10 && Math.abs(p.y - q.y) <= 10) return;
4464
- try { hook.call(global.ixmaps, mapLibreToFlatZoom(z)); } catch (e) { console.error('[ixmaps-gl] htmlgui_onZoomAndPan:', e); }
4689
+ try { hook.call(global.ixmaps, flatViewZoomOf(map, z)); } catch (e) { console.error('[ixmaps-gl] htmlgui_onZoomAndPan:', e); }
4465
4690
  });
4466
4691
  notifyZoomAndPanMidGesture = () => {
4467
4692
  const hook = pageIxmaps() && pageIxmaps().htmlgui_onZoomAndPan;
@@ -4469,13 +4694,13 @@ in vec4 vPieLineColor;
4469
4694
  const z = map.getZoom();
4470
4695
  if (z === last.z) return; // a pan alone waits for 'moveend' (and its 10 px rule)
4471
4696
  last = { z, c: map.getCenter() };
4472
- try { hook.call(global.ixmaps, mapLibreToFlatZoom(z)); } catch (e) { console.error('[ixmaps-gl] htmlgui_onZoomAndPan:', e); }
4697
+ try { hook.call(global.ixmaps, flatViewZoomOf(map, z)); } catch (e) { console.error('[ixmaps-gl] htmlgui_onZoomAndPan:', e); }
4473
4698
  };
4474
4699
  // flat calls it on the first draw too (its old zoom is unset then)
4475
4700
  map.once('load', () => {
4476
4701
  last = { z: map.getZoom(), c: map.getCenter() };
4477
4702
  const hook = pageIxmaps() && pageIxmaps().htmlgui_onZoomAndPan;
4478
- if (typeof hook === 'function') { try { hook.call(global.ixmaps, mapLibreToFlatZoom(last.z)); } catch (e) { console.error('[ixmaps-gl] htmlgui_onZoomAndPan:', e); } }
4703
+ if (typeof hook === 'function') { try { hook.call(global.ixmaps, flatViewZoomOf(map, last.z)); } catch (e) { console.error('[ixmaps-gl] htmlgui_onZoomAndPan:', e); } }
4479
4704
  });
4480
4705
  }
4481
4706
 
@@ -4505,10 +4730,12 @@ in vec4 vPieLineColor;
4505
4730
  // carry the suffix; stripping a pattern that isn't there is a
4506
4731
  // no-op. A theme's single circles (ix-bubbles-<name>-circles-gN,
4507
4732
  // a ScatterplotLayer) route like its bubbles.
4508
- const base = layerId.replace(/-g\d+$/, '').replace(/-circles$/, '');
4733
+ const base = layerId.replace(/-g\d+$/, '').replace(/-circles$/, '').replace(/^(ix-vector-heads-.*)-n\d+$/, '$1');
4509
4734
  return runtimes.find(r => {
4510
4735
  if (base === `ix-bubbles-${r.name}`) return isSymbolChart(r.flags);
4511
4736
  if (base === `ix-pie-${r.name}`) return r._isPieChart();
4737
+ if (base === `ix-bar-${r.name}`) return r._isBarChart();
4738
+ if (base === `ix-vector-${r.name}` || base === `ix-vector-heads-${r.name}`) return r._isVectorChart();
4512
4739
  if (base === `ix-dot-${r.name}`) return r.flags.has('DOT');
4513
4740
  if (base === `ix-choropleth-${r.name}`) return r.flags.has('CHOROPLETH');
4514
4741
  if (base === `ix-features-${r.name}`) return r.flags.has('FEATURE') || r.flags.has('FEATURES');
@@ -4665,7 +4892,7 @@ in vec4 vPieLineColor;
4665
4892
  resize: () => { map.resize(); return engineApi; },
4666
4893
  // broker calls in flight (themes whose data comes by name)
4667
4894
  pendingLoads: () => pendingLoads,
4668
- getZoom: () => mapLibreToFlatZoom(map.getZoom()),
4895
+ getZoom: () => flatViewZoomOf(map),
4669
4896
  require: (url) => { loadRequiredScripts([url]); return engineApi; },
4670
4897
  // flat's theme-level calls by theme id (style.name / meta.name, else
4671
4898
  // the layer name): add(theme, flag) — "replace" swaps a theme of the
@@ -4938,7 +5165,7 @@ in vec4 vPieLineColor;
4938
5165
  const z = isOpts ? latlonOrOpts.zoom : zoom;
4939
5166
  if (c) {
4940
5167
  const lngLat = Array.isArray(c) ? [c[1], c[0]] : [c.lng, c.lat];
4941
- jumpLive(Object.assign({ center: lngLat }, z != null ? { zoom: flatToMapLibreZoom(Number(z)) } : {}));
5168
+ jumpLive(Object.assign({ center: lngLat }, z != null ? { zoom: mapLibreZoomOfFlatView(map, Number(z), lngLat[1], lngLat[0]) } : {}));
4942
5169
  }
4943
5170
  return engineApi;
4944
5171
  },
@@ -5345,6 +5572,12 @@ in vec4 vPieLineColor;
5345
5572
 
5346
5573
  function applyProjectMap(m, f, report) {
5347
5574
  const svg = String(m.map || '');
5575
+ // its view and map scale follow the project's map file, as on flat
5576
+ if (svg) {
5577
+ const svgProjection = flatSvgProjectionOf(svg);
5578
+ if (svgProjection) flatSvgProjectionByOptions.set(builder._engineOptions, svgProjection);
5579
+ else flatSvgProjectionByOptions.delete(builder._engineOptions);
5580
+ }
5348
5581
  if (svg && !/generic\/(mercator|orthographic)\.svg$/i.test(svg)) report.notes.push(`flat SVG map "${svg.split('/').pop()}" is not available in ixmaps-gl — mercator used`);
5349
5582
  if (typeof map.setProjection === 'function') {
5350
5583
  try {
@@ -5365,8 +5598,8 @@ in vec4 vPieLineColor;
5365
5598
  const c = m.center || {};
5366
5599
  const lat = Number(c.lat), lng = Number(c.lng);
5367
5600
  const z = m.zoom != null && m.zoom !== '' ? Number(m.zoom) : NaN;
5368
- if (Number.isFinite(lat) && Number.isFinite(lng)) jumpLive(Object.assign({ center: [lng, lat] }, Number.isFinite(z) ? { zoom: flatToMapLibreZoom(z) } : {}));
5369
- else if (Number.isFinite(z)) jumpLive({ zoom: flatToMapLibreZoom(z) });
5601
+ if (Number.isFinite(lat) && Number.isFinite(lng)) jumpLive(Object.assign({ center: [lng, lat] }, Number.isFinite(z) ? { zoom: mapLibreZoomOfFlatView(map, z, lat, lng) } : {}));
5602
+ else if (Number.isFinite(z)) jumpLive({ zoom: mapLibreZoomOfFlatView(map, z, map.getCenter().lat, map.getCenter().lng) });
5370
5603
  }
5371
5604
  // a project can't authorize its own scripts — trustedscripts is the page's
5372
5605
  if (m.options && typeof m.options === 'object') {
@@ -6040,6 +6273,110 @@ in vec4 vPieLineColor;
6040
6273
  const FLAT_ZOOM_OFFSET = 1;
6041
6274
  const flatToMapLibreZoom = z => z - FLAT_ZOOM_OFFSET;
6042
6275
  const mapLibreToFlatZoom = z => z + FLAT_ZOOM_OFFSET;
6276
+
6277
+ // flat's world SVG maps in a projection other than Mercator
6278
+ // (map:".../generic/equalearth.svg"), which this engine draws in Web
6279
+ // Mercator. Flat syncs such a map to its Leaflet view by fitting the
6280
+ // Leaflet bounds' SW/NE corners, projected, into the map
6281
+ // (htmlgui_sync.js setBoundsLatLon → mapscript2.js doSetMapToGeoBounds /
6282
+ // doZoomMapToArea): both axes fit (min) while the bounds span ≥ 180° of
6283
+ // longitude, else the x axis fits the width on its own (mapapi.js
6284
+ // doSetMapToGeoBounds drops fPreserveMapRatio) — so the same view zoom
6285
+ // frames these maps differently from Mercator (Equal Earth at 2 on
6286
+ // 1024×748: 1.55× the Mercator scale; at 4: 1.09×).
6287
+ // `project` is flat's own forward projection (mapscript.js
6288
+ // _LLtoEqualEarth, _LLtoWinkelTripel — Winkel's cos(50.467) in radians
6289
+ // included), in units of the earth radius.
6290
+ // `scaleConstant`: flat's map scale (nTrueMapScale · nZoomScale, the
6291
+ // scale its dynamic object scaling and gates use) × the screen pixels
6292
+ // per degree of longitude on the equator — constant per map file,
6293
+ // measured on flat (2026-10-09, view zooms 2-5): Equal Earth 170.1e6,
6294
+ // Winkel 195.45e6; for flat's Mercator map 442 913 385 · 256 / 360 =
6295
+ // 314.96e6 (FLAT_OBJECT_SCALE_CONSTANT). These SVG maps carry the
6296
+ // Mercator map's mapscale (177 165 354), so at the same on-screen size
6297
+ // flat calls their scale 0.54× (0.62×) the Mercator one, and its
6298
+ // symbols are drawn larger by that ratio^(1/dynamicScalePow). Framing
6299
+ // matches flat's pixels per degree of longitude at the view's center;
6300
+ // the scale gates (scaleDenominatorAt) still use the Mercator scale.
6301
+ const FLAT_MERCATOR_SCALE_CONSTANT = FLAT_OBJECT_SCALE_CONSTANT * 256 / 360;
6302
+ const FLAT_SVG_PROJECTIONS = {
6303
+ equalearth: {
6304
+ svg: 'maps/svg/maps/generic/equalearth.svg',
6305
+ scaleConstant: 170.1e6,
6306
+ project: (lat, lon) => {
6307
+ const M = Math.sqrt(3) / 2, A1 = 1.340264, A2 = -0.081106, A3 = 0.000893, A4 = 0.003796;
6308
+ const l = Math.asin(M * Math.sin(lat * Math.PI / 180)), l2 = l * l, l6 = l2 * l2 * l2;
6309
+ return [lon * Math.PI / 180 * Math.cos(l) / (M * (A1 + 3 * A2 * l2 + l6 * (7 * A3 + 9 * A4 * l2))), l * (A1 + A2 * l2 + l6 * (A3 + A4 * l2))];
6310
+ }
6311
+ },
6312
+ winkel: {
6313
+ svg: 'maps/svg/maps/generic/winkel.svg',
6314
+ scaleConstant: 195.45e6,
6315
+ project: (lat, lon) => {
6316
+ const p = lat * Math.PI / 180, q = Math.max(-180, Math.min(180, lon)) * Math.PI / 360;
6317
+ const D = Math.acos(Math.cos(p) * Math.cos(q));
6318
+ let x1 = 0, y1 = 0;
6319
+ if (D !== 0) { const C = Math.sin(p) / Math.sin(D); x1 = (q < 0 ? -2 : 2) * D * Math.sqrt(1 - C * C); y1 = D * C; }
6320
+ return [0.5 * (x1 + 2 * q * Math.cos(50.467)), 0.5 * (y1 + p)];
6321
+ }
6322
+ }
6323
+ };
6324
+ // the flat SVG map projection of a map:/project map URL (generic file
6325
+ // name), null for Mercator and every map drawn as is (orthographic → globe)
6326
+ function flatSvgProjectionOf(svgUrl) {
6327
+ const m = /(?:^|\/)(equalearth|winkel)\.svgz?(?:[?#].*)?$/i.exec(String(svgUrl || ''));
6328
+ return m ? FLAT_SVG_PROJECTIONS[m[1].toLowerCase()] : null;
6329
+ }
6330
+ // the flat SVG projection a map's options object belongs to (set by the
6331
+ // map's init and loadProject; the runtimes only see the options object)
6332
+ const flatSvgProjectionByOptions = new WeakMap();
6333
+ // flat SVG map scale ÷ the Mercator one at the same on-screen size (1
6334
+ // without a flat SVG projection)
6335
+ function flatMapScaleRatio(mapOptions) {
6336
+ const proj = mapOptions && typeof mapOptions === 'object' ? flatSvgProjectionByOptions.get(mapOptions) : null;
6337
+ return proj ? proj.scaleConstant / FLAT_MERCATOR_SCALE_CONSTANT : 1;
6338
+ }
6339
+ // a flat view zoom → the MapLibre zoom showing what flat shows at it:
6340
+ // under a flat SVG projection the same screen pixels per degree of
6341
+ // longitude at the center latitude as flat's fitted map (see
6342
+ // FLAT_SVG_PROJECTIONS), else flatToMapLibreZoom
6343
+ function flatViewToMapLibreZoom(flatZoom, lat, lng, width, height, proj) {
6344
+ if (!proj || !(width > 0) || !(height > 0) || !Number.isFinite(lat) || !Number.isFinite(lng)) return flatToMapLibreZoom(flatZoom);
6345
+ // flat's Leaflet bounds (256 px tiles) at this zoom
6346
+ const [sw, ne] = viewBounds(lat, lng, flatToMapLibreZoom(flatZoom), width, height);
6347
+ const a = proj.project(sw.lat, sw.lng), b = proj.project(ne.lat, ne.lng);
6348
+ const kx = width / Math.abs(b[0] - a[0]), ky = height / Math.abs(b[1] - a[1]);
6349
+ const k = ne.lng - sw.lng < 180 ? kx : Math.min(kx, ky);
6350
+ const pxPerDegree = k * (proj.project(lat, lng + 0.5)[0] - proj.project(lat, lng - 0.5)[0]);
6351
+ return Math.log2(pxPerDegree * 360 / 512);
6352
+ }
6353
+ // the inverse: the flat view zoom of a MapLibre zoom (bisection — the
6354
+ // fit above is monotonic in the zoom)
6355
+ function mapLibreToFlatViewZoom(mapLibreZoom, lat, lng, width, height, proj) {
6356
+ if (!proj || !(width > 0) || !(height > 0) || !Number.isFinite(lat) || !Number.isFinite(lng)) return mapLibreToFlatZoom(mapLibreZoom);
6357
+ let lo = mapLibreToFlatZoom(mapLibreZoom) - 4, hi = mapLibreToFlatZoom(mapLibreZoom) + 4;
6358
+ for (let i = 0; i < 40; i++) {
6359
+ const mid = (lo + hi) / 2;
6360
+ if (flatViewToMapLibreZoom(mid, lat, lng, width, height, proj) < mapLibreZoom) lo = mid; else hi = mid;
6361
+ }
6362
+ return (lo + hi) / 2;
6363
+ }
6364
+ // a live MapLibre map's options object (its runtimes' mapOptions), for
6365
+ // the conversions below
6366
+ const engineOptionsByMap = new WeakMap();
6367
+ function mapSvgProjection(map) {
6368
+ const options = engineOptionsByMap.get(map);
6369
+ return (options && flatSvgProjectionByOptions.get(options)) || null;
6370
+ }
6371
+ // the flat view zoom a live map shows / the MapLibre zoom of a flat view
6372
+ function flatViewZoomOf(map, mapLibreZoom) {
6373
+ const c = map.getCenter(), el = map.getContainer();
6374
+ return mapLibreToFlatViewZoom(mapLibreZoom == null ? map.getZoom() : mapLibreZoom, c.lat, c.lng, el.clientWidth, el.clientHeight, mapSvgProjection(map));
6375
+ }
6376
+ function mapLibreZoomOfFlatView(map, flatZoom, lat, lng) {
6377
+ const el = map.getContainer();
6378
+ return flatViewToMapLibreZoom(flatZoom, lat, lng, el.clientWidth, el.clientHeight, mapSvgProjection(map));
6379
+ }
6043
6380
  // map scale denominator at a MapLibre zoom (null → the default reference):
6044
6381
  // flat's own map scale, the one its scale bar shows and every scale gate
6045
6382
  // (featureupper, chartupper, boxupper, valueupper, aggregation brackets)
@@ -6064,9 +6401,20 @@ in vec4 vPieLineColor;
6064
6401
  // (e.g. "fixed") means they don't — same pixel size at every zoom.
6065
6402
  // the zoom at which flat's object scale equals normalSizeScale (symbols
6066
6403
  // at their normal size) — see FLAT_OBJECT_SCALE_CONSTANT
6404
+ // (under a flat SVG projection, flat's scale at that zoom is
6405
+ // flatMapScaleRatio × the Mercator one)
6067
6406
  function resolveZoomReference(mapOptions) {
6068
- const scaleDenominator = parseFloat(mapOptions && mapOptions.normalSizeScale) || FLAT_DEFAULT_NORMAL_SIZE_SCALE;
6069
- return flatToMapLibreZoom(Math.log2(FLAT_OBJECT_SCALE_CONSTANT / scaleDenominator));
6407
+ const scaleDenominator = parseFloat(normalSizeScaleOf(mapOptions)) || FLAT_DEFAULT_NORMAL_SIZE_SCALE;
6408
+ return flatToMapLibreZoom(Math.log2(FLAT_OBJECT_SCALE_CONSTANT * flatMapScaleRatio(mapOptions) / scaleDenominator));
6409
+ }
6410
+ // .options({normalSizeScale}) under any spelling: flat matches the key
6411
+ // case-insensitively (htmlgui.js, /normalSizeScale/i → setScaleParam;
6412
+ // the last matching key wins), e.g. "normalsizescale"
6413
+ function normalSizeScaleOf(mapOptions) {
6414
+ if (!mapOptions || typeof mapOptions !== 'object') return undefined;
6415
+ let value;
6416
+ for (const key of Object.keys(mapOptions)) if (/normalSizeScale/i.test(key)) value = mapOptions[key];
6417
+ return value;
6070
6418
  }
6071
6419
 
6072
6420
  // .options({dynamicScalePow}) — the REAL parameter name (confirmed in
@@ -6139,7 +6487,7 @@ in vec4 vPieLineColor;
6139
6487
  // scale (resolveZoomReference); flat's real rule (maptheme.js 13408,
6140
6488
  // 0.3 + 0.7 / ln(nZoom / dx)) is not ported yet
6141
6489
  function autoOpacityZoomReference(mapOptions) {
6142
- const scaleDenominator = parseFloat(mapOptions && mapOptions.normalSizeScale);
6490
+ const scaleDenominator = parseFloat(normalSizeScaleOf(mapOptions));
6143
6491
  if (!scaleDenominator) return flatToMapLibreZoom(DEFAULT_ZOOM_REFERENCE);
6144
6492
  return flatToMapLibreZoom(Math.log2(WEBMERCATOR_SCALE_CONSTANT / scaleDenominator));
6145
6493
  }
@@ -6474,6 +6822,12 @@ in vec4 vPieLineColor;
6474
6822
 
6475
6823
  // the themes drawn by the symbol chart pipeline (_buildChartLayers): flat's
6476
6824
  // BUBBLE/SQUARE/LABEL branch (maptheme.js 20647-20652), symbols, user charts
6825
+ // CHART|VECTOR / CHART|BEZIER — flat's flow lines (maptheme.js chartMap
6826
+ // 18148-18435, ahead of drawChart: they take precedence over PIE,
6827
+ // BUBBLE, SYMBOL ...; DOT's branch comes first)
6828
+ function isVectorChart(flags) {
6829
+ return flags.has('CHART') && (flags.has('VECTOR') || flags.has('BEZIER')) && !flags.has('DOT');
6830
+ }
6477
6831
  function isSymbolChart(flags) {
6478
6832
  return flags.has('CHART') && (flags.has('SYMBOL') || flags.has('USER') || flags.has('LABEL'));
6479
6833
  }
@@ -6509,6 +6863,17 @@ in vec4 vPieLineColor;
6509
6863
  // the item's radius, SYMBOL: the theme's normal radius)
6510
6864
  // So "left" puts the chart's left edge o to the right of the point (and
6511
6865
  // drops offsetx), "2left" one more h. Returns deck.gl's pixel offset.
6866
+ // flat's maxcharts (maptheme.js 16879-16885): chartMap sorts the charts
6867
+ // ascending (the biggest drawn last, on top) and skips the first
6868
+ // nToDraw − nMaxCharts, so only the biggest maxcharts are drawn — for
6869
+ // every chart type (symbols, pies, bars, VECTOR/BEZIER). In place, on the
6870
+ // already sorted list of charts to draw
6871
+ function applyFlatMaxCharts(list, style) {
6872
+ const n = Math.round(styleNum(style && style.maxcharts)) || 0;
6873
+ if (n > 0 && list.length > n) list.splice(0, list.length - n);
6874
+ return list;
6875
+ }
6876
+
6512
6877
  function flatChartAlignOffset(style, g) {
6513
6878
  const num = v => (isNaN(styleNum(v)) ? 0 : styleNum(v));
6514
6879
  const sym = g.symbolScale || 1;
@@ -6551,6 +6916,53 @@ in vec4 vPieLineColor;
6551
6916
  }
6552
6917
  return [r, g, b].map(c => Math.min(255, Math.floor(c * f)));
6553
6918
  }
6919
+ // A BUBBLE chart's look in flat (maptheme.js, the BUBBLE branch), for a
6920
+ // circle of screen radius r in the class color rgb, `unit` = flat's
6921
+ // normalX(1) on screen (the object zoom factor × style.scale):
6922
+ // - stroke (20740-20780, 20895): style.linecolor, else ChartColors.lowColor
6923
+ // (the color × 0.7, __maptheme_getChartColors) — none with NOLINES or
6924
+ // linecolor "none"
6925
+ // - width (20898-20903): linewidth (0.1) · unit · √(r / normal radius),
6926
+ // the normal radius 15 · unit; OUTLINE: unit · min(1, r / normal radius)
6927
+ // - fill-opacity fillopacity (1); stroke-opacity (21050-21056) 1 with
6928
+ // OUTLINE or a linecolor, else 1 below fillopacity 0.5, 0.3 above
6929
+ // (_buildLabelChartLayers spells the same rule out for CHART|LABEL's box.)
6930
+ function flatBubbleLook(rgb, r, unit, style, flags) {
6931
+ const fillOpacity = styleNum(style.fillopacity) || 1;
6932
+ const outline = flatFlag(flags, 'OUTLINE');
6933
+ const lineColor = style.linecolor ? styleLineColor(style.linecolor) : null;
6934
+ const stroke = lineColor ? (lineColor === 'none' ? null : hexOrNamedToRgb(lineColor))
6935
+ : flatFlag(flags, 'NOLINES') ? null : flatDerivateRgb(rgb, 0.7);
6936
+ const ratio = r / (NORMAL_RADIUS_PX * unit);
6937
+ const width = outline ? unit * Math.min(1, ratio) : unit * (styleNum(style.linewidth) || 0.1) * Math.sqrt(ratio);
6938
+ const strokeOpacity = outline || lineColor ? 1 : (fillOpacity < 0.5 ? 1 : 0.3);
6939
+ return { fillOpacity, stroke, strokeOpacity, width };
6940
+ }
6941
+
6942
+ // flatBubbleLook for a cached icon raster `size` canvas px wide, drawn 2r
6943
+ // screen px wide: the stroke width in canvas px, in quarter-octave steps
6944
+ // (≤ 19 % apart, at most the icon's radius) — every distinct width is
6945
+ // another icon in deck.gl's shared atlas (see ICON_ATLAS_RESET_AFTER), and
6946
+ // a continuous width would make one per bubble. It hardly depends on the
6947
+ // zoom: r and the width both scale with the object zoom factor. `stroke`
6948
+ // only when it is style.linecolor (a packed icon derives each part's own,
6949
+ // see _buildBubbleIcon), noStroke for NOLINES / linecolor "none";
6950
+ // outlineRatio (OUTLINE): r / normal radius, also stepped, as 1 from 1 up
6951
+ // (where flat's OUTLINE width stops growing).
6952
+ function bubbleIconLook(r, unit, style, flags, size) {
6953
+ const look = flatBubbleLook([0, 0, 0], r, unit, style, flags);
6954
+ const step = v => (v > 0 ? Math.round(Math.pow(2, Math.round(Math.log2(v) * 4) / 4) * 1000) / 1000 : 0);
6955
+ const out = {
6956
+ fillOpacity: look.fillOpacity,
6957
+ strokeOpacity: look.strokeOpacity,
6958
+ stroke: style.linecolor ? look.stroke : null,
6959
+ noStroke: !look.stroke,
6960
+ width: r > 0 ? Math.min(size / 2, step(look.width * size / (2 * r))) : 0,
6961
+ };
6962
+ if (flatFlag(flags, 'OUTLINE')) out.outlineRatio = Math.min(1, step(r / (NORMAL_RADIUS_PX * unit)));
6963
+ return out;
6964
+ }
6965
+
6554
6966
  // ChartColors.textColor (colorscheme.js 104-116): darker on light colors
6555
6967
  function flatChartTextRgb(rgb) {
6556
6968
  return flatDerivateRgb(rgb, rgb[0] + rgb[1] + rgb[2] > 450 ? 0.6 : 3);
@@ -7269,6 +7681,412 @@ in vec4 vPieLineColor;
7269
7681
  return [lngLat[0] + offset[0] * 360 / world, lat];
7270
7682
  }
7271
7683
 
7684
+ // CHART|BAR / BARS: which flat draws as a bar chart — drawChart's BAR
7685
+ // branch (maptheme.js 23283) comes after USER, PIE, WAFFLE, BUBBLE,
7686
+ // SYMBOL and BUFFER; flat tests /BAR/ as a substring (BARS too)
7687
+ function isBarChart(flags) {
7688
+ if (!flags.has('CHART') || isVectorChart(flags) || flags.has('DOT')) return false;
7689
+ const t = flags.typeString != null ? flags.typeString : [...flags].join('|');
7690
+ return /BAR/.test(t) && !/\bUSER\b|PIE|WAFFLE|BUBBLE|SYMBOL|BUFFER/.test(t);
7691
+ }
7692
+
7693
+ // One item's bar chart as real ixmaps-flat draws it — maptheme.js
7694
+ // drawChart's BAR branch (23283-24425) — in flat's normal units (u:
7695
+ // normalX(1), the chart size is 30 u; px = u · the object zoom factor ×
7696
+ // style.scale), y down, relative to the item's position. `values`: the
7697
+ // item's parts (nValuesA, after aggregation); `t`: the type string,
7698
+ // tested as flat does, by substring. `o`:
7699
+ // themeMin / themeMax the theme's nMin / nMax (the items' sizes with a
7700
+ // size field or CATEGORICAL, else their parts)
7701
+ // maxSize, itemSize the theme's nMaxSize, the item's nSize (size field)
7702
+ // sizeField a size field is bound
7703
+ // normalSizeValue, sizePow, rangeScale, valueScale, units,
7704
+ // valueDecimals (explicit, else undefined), noBreaks, gridX, nClasses
7705
+ // (partsA.length), classMax (partsA[k].max), nColors, fillOpacity,
7706
+ // lineWidth, lineColor (linecolor, else undefined), fadeNegative,
7707
+ // fadeValuePow, hideValues (valueupper/lower), means / medians / devs
7708
+ // (per part, OFFSETMEAN / OFFSETMEDIAN / DEVIATION)
7709
+ // measure(text, font) the text's width at that font size (u)
7710
+ // Returns null when flat draws no chart (no size, or a single 0 part),
7711
+ // else { bars, texts, lines, extent: [x0, y0, x1, y1] } —
7712
+ // bars: { part (the value's index), cls (its color index), base
7713
+ // [x, y] (the middle of the shaft's foot), dir (0 up, 1 right,
7714
+ // 2 down, 3 left), w2 (half width), sw (shaft half width), len
7715
+ // (shaft), band, head, value, textValue, fill: { opacity },
7716
+ // line: { rgb: 'color' (the bar's) | null (lineColor or black),
7717
+ // width, opacity }, opacity (FADEIN / FADENEGATIVE) }
7718
+ // texts: { text, x, y (the baseline's start), angle (0 reading right,
7719
+ // 90 up, -90 down), font, opacity, color ('value' — flat's
7720
+ // #798697 values color, 'red', 'contrast' — ChartColors
7721
+ // .textColor of the bar's color —, or a CSS color), bg (null,
7722
+ // 'white' — #fefeff —, or 'bar' — the bar's color at 0.5),
7723
+ // cls, part }
7724
+ // lines: STACKED leader lines { x0, y0, x1, y1 }
7725
+ // Divergences (not drawn here): 3D, VOLUME, COLUMN's cylinders, DOTTED,
7726
+ // SMALLARROW (drawn as ARROW), TRENDLINE, the XAXIS/AXIS labels, the
7727
+ // mean/median marks, CLIP / MORPH frames, showparts / clipparts, ZOOM /
7728
+ // NORMSIZE / MENUSIZE, INVERT, DIAGLEFT / DIAGRIGHT — bars are drawn
7729
+ // plain instead (see BAR_INERT_FLAGS).
7730
+ function flatBarLayout(values, t, o) {
7731
+ t = String(t || '');
7732
+ const has = re => re.test(t);
7733
+ const n = values.length;
7734
+ if (!n) return null;
7735
+ const parts = values.map(v => (isFinite(v) ? v : 0));
7736
+ const sum = parts.reduce((a, v) => a + v, 0);
7737
+ const stacked = has(/STACKED/), pointer = has(/POINTER/), horz = has(/HOR/);
7738
+ const sizeFlag = has(/SIZE/) && !has(/NORMSIZE/);
7739
+ const sizePow = o.sizePow || 2;
7740
+ const CHART = 30;
7741
+ let nMax = Math.max(o.themeMax, Math.abs(o.themeMin));
7742
+ let nRange = o.themeMax - o.themeMin;
7743
+ let maxSize = o.maxSize;
7744
+ if (o.normalSizeValue) nRange = maxSize = o.normalSizeValue;
7745
+ // flat's getChartSize (no chart when 0) and its "a single 0" rule
7746
+ if (sizeFlag && !has(/NOSIZE/)) {
7747
+ const s = o.sizeField ? o.itemSize : Math.abs(sum);
7748
+ const ref = o.sizeField ? maxSize : nRange;
7749
+ const size = has(/SIZELOG/) ? Math.max(1, 15 / Math.log(ref) * Math.log(s)) : 15 / Math.pow(ref, 1 / sizePow) * Math.pow(s, 1 / sizePow);
7750
+ if (isNaN(size) || size === 0) return null;
7751
+ }
7752
+ if (n === 1 && parts[0] === 0 && !has(/ZEROISVALUE|AUTOCOMPLETE|CATEGORICAL/)) return null;
7753
+
7754
+ // the sizer: the chart's size by the size field or by the values' sum
7755
+ let sizer = 1;
7756
+ if (sizeFlag && o.sizeField && o.itemSize != null) {
7757
+ sizer = has(/SIZELOG/) ? 1 / Math.max(1, Math.log(maxSize) * Math.log(o.itemSize))
7758
+ : Math.pow(o.itemSize, 1 / sizePow) / Math.pow(maxSize, 1 / sizePow);
7759
+ } else if (sizeFlag) {
7760
+ const s = pointer ? sum / n : sum;
7761
+ sizer = has(/SIZELOG/) ? 1 / Math.max(1, Math.log(nRange) * Math.log(Math.abs(s)))
7762
+ : Math.pow(Math.abs(s), 1 / sizePow) / Math.pow(nRange, 1 / sizePow);
7763
+ }
7764
+ if (sizer === 0 && sum !== 0) sizer = 1;
7765
+ const chartSize = CHART * sizer;
7766
+
7767
+ // bar width and value text size
7768
+ let W = chartSize / n * 0.66;
7769
+ if (stacked) {
7770
+ W = chartSize / 3;
7771
+ if (o.gridX && !has(/MULTI/)) W /= 2;
7772
+ }
7773
+ let f = W < 5 ? W * 4 / 5 : W;
7774
+ let textOpacity = 1;
7775
+ if (has(/COLUMN/)) W *= has(/THICK/) ? 2 : has(/THIN/) ? 1 : 1.5;
7776
+ if (has(/THIN/)) { W *= 0.6; f *= 0.75; }
7777
+ if (has(/THICK/)) { W *= 1.5; f *= 1.5; }
7778
+ if (stacked) f = 5;
7779
+ if (stacked && !has(/ZOOM/)) f *= sizer;
7780
+ const valueScale = o.valueScale || 1;
7781
+ f *= valueScale;
7782
+
7783
+ // the bar length per value unit (nStep)
7784
+ if (o.normalSizeValue && (!o.field100 || pointer)) nMax = o.normalSizeValue;
7785
+ let step0 = CHART / nMax;
7786
+ if (has(/OFFSETMEAN|OFFSETMEDIAN|DEVIATION/)) step0 = chartSize / 300;
7787
+ if (sizeFlag && has(/SEQUENCE/) && n > 1 && !o.sizeField && !o.field100) step0 = step0 * n / sizer;
7788
+ else if (sizeFlag && n > 1 && !has(/EXPAND/)) {
7789
+ if (o.field100 || pointer) step0 *= sizer;
7790
+ else step0 /= has(/SIZEP4/) ? sizer * sizer : sizer;
7791
+ }
7792
+ if (o.rangeScale) step0 *= o.rangeScale;
7793
+ if (has(/COMPRESSMAX/)) step0 /= 4;
7794
+ else if (has(/COMPRESSMORE/)) step0 /= 3;
7795
+ else if (has(/COMPRESS/)) step0 /= 2;
7796
+ else if (has(/EXPANDMAX/)) step0 *= 4;
7797
+ else if (has(/EXPANDMORE/)) step0 *= 3;
7798
+ else if (has(/EXPAND/)) step0 *= 2;
7799
+ if (pointer && !has(/NORMSIZE/)) step0 *= 1.2;
7800
+
7801
+ // the bar order: SORT descending (STACKED ascending), UP reversed
7802
+ let sorted = null;
7803
+ if (has(/\bSORT\b/)) {
7804
+ sorted = parts.map((v, i) => ({ i, v }));
7805
+ sorted.sort(stacked ? (a, b) => a.v - b.v : (a, b) => b.v - a.v);
7806
+ sorted = sorted.map(s => s.i);
7807
+ }
7808
+ const startI = has(/\bUP\b/) ? n - 1 : 0;
7809
+ const nC = has(/CENTER/) ? 2 : 1;
7810
+ const fmt = (v, d) => flatGroupedValue(v, d, o.noBreaks);
7811
+ const measure = o.measure || ((text, font) => text.length * font * 0.55);
7812
+
7813
+ const bars = [], texts = [], lines = [];
7814
+ let posX = 0, posY = 0, minNext = 0, barsDrawn = 0, step = step0;
7815
+ for (let i = 0; i < n; i++) {
7816
+ step = step0;
7817
+ let idx = Math.abs(startI - i);
7818
+ if (sorted) idx = sorted[idx];
7819
+ let v = parts[idx];
7820
+ let tv = v;
7821
+ if (has(/OFFSETMEAN|OFFSETMEDIAN/)) {
7822
+ const ref = has(/OFFSETMEDIAN/) ? (o.medians || [])[idx] : (o.means || [])[idx];
7823
+ v = 100 / ref * v - 100;
7824
+ v = isFinite(v) ? v : 0;
7825
+ tv = v;
7826
+ nMax = 100;
7827
+ } else if (has(/DEVIATION/)) {
7828
+ v = (v - (o.means || [])[idx]) / (o.devs || [])[idx];
7829
+ v = isFinite(v) ? v : 0;
7830
+ tv = v;
7831
+ v *= 100;
7832
+ }
7833
+ // the color: the part's, or (SEQUENCE, a single part) the class of
7834
+ // the value
7835
+ let cls = o.nColors ? (idx < o.nColors ? idx : idx % (o.gridX || 1000000)) : idx;
7836
+ if ((has(/SEQUENCE|CLIP/) || n === 1) && o.nColors >= 2 && o.classMax && o.classMax.length) {
7837
+ cls = o.classMax.length - 1;
7838
+ for (let k = 0; k < o.classMax.length; k++) if (v < o.classMax[k]) { cls = k; break; }
7839
+ }
7840
+ if (has(/LEFT/) && has(/HORZ/)) v = -v;
7841
+ if (v < 0 && !stacked) { posY = -v * step; v = -v; }
7842
+ if (pointer && sizeFlag) {
7843
+ // flat's sized pointer: its width by the value (the WIDTH variant
7844
+ // by value100 is not ported)
7845
+ W = chartSize * Math.pow(Math.abs(v / nMax), 1 / 3);
7846
+ f = W * 0.6;
7847
+ if (!has(/ZOOM|XAXIS/)) {
7848
+ textOpacity = Math.pow(Math.abs(v), 1 / 2) / Math.pow(o.themeMax, 1 / 2);
7849
+ if (o.fadeValuePow) textOpacity = Math.pow(Math.abs(v), o.fadeValuePow) / Math.pow(nMax, o.fadeValuePow);
7850
+ }
7851
+ }
7852
+ if (has(/DTEXT/) && !has(/ZOOM/)) {
7853
+ f = (chartSize * 2 / 3) * Math.pow(Math.abs(v / nMax), 1 / 2) * 0.75;
7854
+ textOpacity = Math.pow(Math.abs(v), 1 / 3) / Math.pow(nMax, 1 / 3);
7855
+ }
7856
+ v = isFinite(v) ? v : 0;
7857
+ if (v === 0 && has(/NOZERO/)) { posY = 0; continue; }
7858
+ if (tv < 0 && has(/NONEGATIVE/)) { posY = 0; continue; }
7859
+ if (tv > 0 && has(/ONLYNEGATIVE/)) { posY = 0; continue; }
7860
+
7861
+ const oy = posY;
7862
+ const bar = { part: idx, cls, value: v, textValue: tv, dir: 0, w2: W / 2, sw: W / 2, band: 0, head: 0, len: 0,
7863
+ fill: { opacity: o.fillOpacity || 1 }, line: null, opacity: 1 };
7864
+ let drawn = true;
7865
+ if (pointer && v) {
7866
+ if (has(/OFFSETMEAN|OFFSETMEDIAN|DEVIATION/) && Math.abs(v * step) > 50) {
7867
+ const h = Math.abs(v * step);
7868
+ step = (50 + (h - 50) / 10 + h % 50) / Math.abs(v);
7869
+ }
7870
+ const H = v * step;
7871
+ const arrow = has(/ARROW/);
7872
+ const style = { rgb: null, width: o.lineWidth ? o.lineWidth * Math.sqrt(sizer || 1) : 0.05, opacity: 0.5 };
7873
+ if (oy <= 0) {
7874
+ // positive: the shaft from the base up, a band as wide as the bar,
7875
+ // the head on it
7876
+ Object.assign(bar, arrow
7877
+ ? { base: [posX + W / 2, oy - 0.9 * H / nC + 0.9 * H], len: 0.9 * H, sw: W / 6, band: W / 15, head: W / 1.5 }
7878
+ : { base: [posX + W / 2, oy - H / nC + H], len: H, sw: W * 5 / 14, band: W / 15, head: W / 2 });
7879
+ bar.line = style;
7880
+ } else if (!has(/NONEGATIVE/)) {
7881
+ // negative: from the zero line down, the head below
7882
+ Object.assign(bar, arrow
7883
+ ? { base: [posX + W / 2, oy - H / nC], len: H, sw: W / 6, head: W / 2, dir: 2 }
7884
+ : { base: [posX + W / 2, oy - H / nC], len: H, sw: W * 5 / 14, head: W / 4, dir: 2 });
7885
+ bar.line = style;
7886
+ if (!has(/LEFT/)) {
7887
+ const fadeNeg = o.fadeNegative || 0.1;
7888
+ if (has(/ZOOM|NORMSIZE/)) {
7889
+ bar.fill.opacity = 0.5;
7890
+ bar.line = { rgb: 'color', width: 0.25, opacity: 1 };
7891
+ } else if (o.nClasses > 2) {
7892
+ bar.fill.opacity = fadeNeg;
7893
+ bar.line = { rgb: 'low', width: o.lineWidth ? o.lineWidth * 0.25 * sizer : 0.25, opacity: 0.8 };
7894
+ } else {
7895
+ bar.fill.opacity = fadeNeg;
7896
+ bar.line = { rgb: 'high', width: o.lineWidth ? o.lineWidth * Math.sqrt(sizer || 1) : 0.05, opacity: 1 };
7897
+ }
7898
+ }
7899
+ } else drawn = false;
7900
+ } else {
7901
+ // plain 2D bar (DOTTED, 3D, COLUMN are drawn as this too)
7902
+ if (!has(/NOZERO/)) v = v || 0.00001;
7903
+ const H = v * step;
7904
+ bar.value = v;
7905
+ if (H < 0) drawn = false;
7906
+ bar.base = [posX + W / 2, oy - H / nC + H];
7907
+ bar.len = H;
7908
+ if (oy <= 0 || has(/LEFT/)) {
7909
+ bar.line = { rgb: null, width: o.lineWidth ? o.lineWidth * 0.25 * sizer : 0.2, opacity: o.lineWidth ? 1 : 0.1 };
7910
+ } else {
7911
+ bar.fill.opacity = 0.3;
7912
+ bar.line = { rgb: 'color', width: 0.25, opacity: 1 };
7913
+ }
7914
+ }
7915
+ if (has(/FADENEGATIVE/) && tv < 0) bar.opacity = 0.5;
7916
+ if (has(/FADEIN/)) {
7917
+ if (n === 2 && i === 0) bar.opacity = 0.6;
7918
+ else if (!has(/NORMSIZE/)) {
7919
+ const p = has(/FADEINP4/) ? 10 : 3;
7920
+ bar.opacity = 0.1 + 0.9 * Math.pow(i + 1, p) / Math.pow(n, p);
7921
+ }
7922
+ }
7923
+ if (drawn) bars.push(bar);
7924
+
7925
+ // the value text
7926
+ const H = v * step;
7927
+ if (has(/VALUES/) && !(oy > 0 && has(/NONEGATIVE/)) && !o.hideValues
7928
+ && f > 0 && !(o.gridX && o.nClasses / o.gridX > 2)) {
7929
+ let text = fmt(tv, o.valueDecimals != null ? o.valueDecimals : (o.themeMax <= 1 ? 1 : 0));
7930
+ if (pointer && tv > 0 && (o.themeMin < 0 || has(/OFFSETMEAN|OFFSETMEDIAN|\bSIGN\b/))) text = '+' + text;
7931
+ if (!has(/VOLUME/)) text += o.units ? ' ' + o.units + ' ' : '';
7932
+ if (stacked && tv !== 0 && !o.field100 && !o.gridX) text += ' (' + Math.round(tv / sum * 100) + '%)';
7933
+ if (stacked) f = (has(/ZOOM/) ? 18 : 5) * valueScale;
7934
+ // SVG collapses the blanks
7935
+ text = text.replace(/\s+/g, ' ').trim();
7936
+ const red = parseFloat(text) < 0;
7937
+ // createTextLabel's box: not with HORZ; with ZOOM … AXIS only
7938
+ // colored (VALUEBACKGROUND / CTEXT); colored for a positive value
7939
+ const colored = tv > 0 && has(/CTEXT|VALUEBACKGROUND/);
7940
+ let bgOn = true;
7941
+ if (has(/ZOOM|MENUSIZE|NORMSIZE|INFOSIZE|AXIS|SELECTION/)) bgOn = colored;
7942
+ if (has(/VALUEBACKGROUND/)) bgOn = true;
7943
+ if (horz) bgOn = false;
7944
+ const bg = bgOn ? (colored ? 'bar' : 'white') : null;
7945
+ // the text color: negative red (valuecolor), else textcolor; on the
7946
+ // bar's color its text color, else flat's values color
7947
+ const color = red ? (o.valueColor || 'red') : (o.textColor || (bg === 'bar' ? 'contrast' : 'value'));
7948
+ const textW = measure(text, f);
7949
+ const tLen = textW + (bgOn ? 0.7 * f : 0);
7950
+ const ptY = W / 2 + f * 0.15;
7951
+ const outside = (!has(/DOINLINETEXT/) || has(/ZOOM/) || has(/NORMSIZE/)) && has(/VALUE/) && !(has(/VOLUME/) && !has(/ZOOM/))
7952
+ && (has(/THIN|NOINLINETEXT|DTEXT|SIZE|STACKED|NORMSIZE|ZOOM/) || tLen + 1 > H / nC);
7953
+ const base = { text, cls, part: idx, opacity: textOpacity, color, bg };
7954
+ if (outside && stacked) {
7955
+ if (!(v === 0 && !has(/ZEROISVALUE/)) && !(o.gridX && n / o.gridX > 2)) {
7956
+ const topDx = H / 2;
7957
+ minNext = Math.max(0, minNext + (f * 0.75 - topDx));
7958
+ const textPosX = minNext + topDx - f / 3;
7959
+ let indent = 0.5 * sizer;
7960
+ let ptYs = ptY;
7961
+ let x1 = posX + ptY + W / 3, x2 = posX + ptY + W * 7 / 8;
7962
+ const y1 = -topDx, y2 = -textPosX - f / 3;
7963
+ if (o.gridX && barsDrawn < o.gridX) {
7964
+ ptYs -= W * 2 + tLen;
7965
+ const tmp = x2;
7966
+ x2 = x1 - W * 1.4;
7967
+ x1 = tmp - W * 1.4;
7968
+ indent = -indent;
7969
+ }
7970
+ texts.push(Object.assign(base, { x: posX + ptYs + W + indent, y: oy - textPosX, angle: 0, font: f }));
7971
+ [[x1, y1, x1 + indent, y1], [x1 + indent, y1, x2, y2], [x2, y2, x2 + indent, y2]]
7972
+ .forEach(([a, b, c, d]) => lines.push({ x0: a, y0: oy + b, x1: c, y1: oy + d }));
7973
+ minNext = Math.max(0, minNext + (f * 2 / 3 - (H - topDx)));
7974
+ }
7975
+ } else if (outside) {
7976
+ const lift = pointer && tv > 0 ? W * 0.5 : 0;
7977
+ if (n === 1 && !has(/HORZ/) && !has(/VOLUME/)) {
7978
+ texts.push(Object.assign(base, { x: posX - W * 0.8 + f, y: oy - H / nC - f * 0.8 - lift + 0.25 * f, angle: 0, font: f,
7979
+ opacity: tv < 0 ? textOpacity * 0.75 : textOpacity }));
7980
+ } else {
7981
+ // rotate(270) scale(0.9): reading up, 0.9 font
7982
+ texts.push(Object.assign(base, { x: posX + ptY + 0.9 * 0.25 * f, y: oy - 0.5 - W * 0.25 - H / nC - 0.9 * f, angle: 90, font: 0.9 * f }));
7983
+ }
7984
+ } else {
7985
+ // inside the bar, in its text color, shrunk to fit
7986
+ const s = Math.min(1, H / tLen);
7987
+ if (s >= 0.33) {
7988
+ let ptX = -4;
7989
+ if (tv > 0) ptX += Math.max(1, H - (tLen + 3) * s);
7990
+ const dY = 0.5 / s;
7991
+ texts.push(Object.assign(base, { x: posX + ptY - dY + 0.25 * f, y: oy - ptX - f, angle: 90, font: f * s, color: 'contrast', bg: null }));
7992
+ }
7993
+ minNext = Math.max(0, minNext - H);
7994
+ }
7995
+ }
7996
+
7997
+ if (stacked) {
7998
+ if (!has(/CENTER/)) posY -= v * step;
7999
+ } else {
8000
+ posY = 0;
8001
+ posX += W;
8002
+ if (has(/SPACED/)) posX += W / 5;
8003
+ else if (!has(/\bUP\b/)) posX += 0.05;
8004
+ }
8005
+ barsDrawn++;
8006
+ if (stacked && !has(/MULTI/) && o.gridX && barsDrawn % o.gridX === 0) {
8007
+ posY = 0;
8008
+ posX += W + W / 20;
8009
+ }
8010
+ }
8011
+
8012
+ // the chart's position: HOR(Z) turned by 90° (bars to the right, the
8013
+ // first on top, the last centered on the position's height), else
8014
+ // the bars side by side centered on it (STACKED: the first column)
8015
+ const P = posX;
8016
+ let map, dirMap, angleMap;
8017
+ if (horz) {
8018
+ const dy = W / 2 + (has(/3D/) ? W / 3 : 0) - P;
8019
+ map = (x, y) => [-y, x + dy];
8020
+ dirMap = d => (d + 1) % 4;
8021
+ angleMap = a => a - 90;
8022
+ } else {
8023
+ const dx = stacked ? W / 2 : W * barsDrawn / 2;
8024
+ map = (x, y) => [x - dx, y];
8025
+ dirMap = d => d;
8026
+ angleMap = a => a;
8027
+ }
8028
+ const ext = [Infinity, Infinity, -Infinity, -Infinity];
8029
+ const grow = (x, y) => { if (x < ext[0]) ext[0] = x; if (y < ext[1]) ext[1] = y; if (x > ext[2]) ext[2] = x; if (y > ext[3]) ext[3] = y; };
8030
+ for (const b of bars) {
8031
+ b.base = map(b.base[0], b.base[1]);
8032
+ b.dir = dirMap(b.dir);
8033
+ const d = [[0, -1], [1, 0], [0, 1], [-1, 0]][b.dir], a = [-d[1], d[0]];
8034
+ const L = b.len + b.band + b.head;
8035
+ [[-b.w2, 0], [b.w2, 0], [-b.w2, L], [b.w2, L]].forEach(([u, v]) => grow(b.base[0] + a[0] * u + d[0] * v, b.base[1] + a[1] * u + d[1] * v));
8036
+ }
8037
+ for (const tx of texts) {
8038
+ [tx.x, tx.y] = map(tx.x, tx.y);
8039
+ tx.angle = angleMap(tx.angle);
8040
+ // arial: ascent 0.905, descent 0.212 of the font
8041
+ const w = measure(tx.text, tx.font) + (tx.bg ? 0.7 * tx.font : 0);
8042
+ const r = tx.angle * Math.PI / 180, c = Math.cos(r), s = -Math.sin(r);
8043
+ const xo = tx.bg ? -0.3 * tx.font : 0;
8044
+ [[xo, -0.905 * tx.font], [xo + w, -0.905 * tx.font], [xo, 0.212 * tx.font], [xo + w, 0.212 * tx.font]]
8045
+ .forEach(([u, v]) => grow(tx.x + u * c - v * s, tx.y + u * s + v * c));
8046
+ }
8047
+ for (const l of lines) {
8048
+ [l.x0, l.y0] = map(l.x0, l.y0);
8049
+ [l.x1, l.y1] = map(l.x1, l.y1);
8050
+ grow(l.x0, l.y0); grow(l.x1, l.y1);
8051
+ }
8052
+ return { bars, texts, lines, extent: ext[0] <= ext[2] ? ext : [0, 0, 0, 0], width: W, sizer };
8053
+ }
8054
+
8055
+ // flat's BOX and TITLE around a chart (maptheme.js 18758-18905), in the
8056
+ // chart's units: `extent` the chart's [x0, y0, x1, y1]; o: { margin
8057
+ // (boxmargin, 2), titleFont, title (text or ''), bottomTitle, alignRight,
8058
+ // measure }. The margin is min(2 m, m · width / 30); the title's first
8059
+ // baseline 0.7 font above the chart (BOTTOMTITLE: one font below), its
8060
+ // lines wrapped to the chart's width and stacked upward; the box encloses
8061
+ // chart and title, its top trimmed by 0.1 font when titled. Returns
8062
+ // { rect: [x0, y0, x1, y1], title: { lines, x, anchor, baseline, font } | null }
8063
+ function flatChartBox(extent, o) {
8064
+ const e = extent.slice();
8065
+ const w = e[2] - e[0];
8066
+ const margin = Math.min(2 * o.margin, o.margin * w / 30);
8067
+ let title = null;
8068
+ if (o.title) {
8069
+ const f = o.titleFont;
8070
+ const measure = o.measure || ((text, font) => text.length * font * 0.55);
8071
+ const lines = [];
8072
+ for (const word of String(o.title).split(/\s+/).filter(Boolean)) {
8073
+ const last = lines.length ? lines[lines.length - 1] + ' ' + word : null;
8074
+ if (last !== null && measure(last, f) <= w) lines[lines.length - 1] = last;
8075
+ else lines.push(word);
8076
+ }
8077
+ const width = Math.max(...lines.map(l => measure(l, f)));
8078
+ const baseline = o.bottomTitle ? e[3] + f : e[1] - 0.7 * f - (lines.length - 1) * f;
8079
+ const x = o.alignRight ? e[2] : e[0];
8080
+ const x0 = o.alignRight ? x - width : x;
8081
+ title = { lines, x, anchor: o.alignRight ? 'end' : 'start', baseline, font: f };
8082
+ e[0] = Math.min(e[0], x0);
8083
+ e[1] = Math.min(e[1], baseline - 0.905 * f) + 0.1 * f;
8084
+ e[2] = Math.max(e[2], x0 + width);
8085
+ e[3] = Math.max(e[3], baseline + (lines.length - 1) * f + 0.212 * f);
8086
+ }
8087
+ return { rect: [e[0] - margin, e[1] - margin, e[2] + margin, e[3] + margin], title, width: w };
8088
+ }
8089
+
7272
8090
  // CHART|PIE / DONUT: one pie's slices as real ixmaps-flat lays them out —
7273
8091
  // maptheme.js drawChart's PIE branch (19939-20330) and piechart.js
7274
8092
  // DonutChart.realize (181-510). `values`: the item's part values (field
@@ -7456,6 +8274,322 @@ in vec4 vPieLineColor;
7456
8274
  return out;
7457
8275
  }
7458
8276
 
8277
+ // flat's value rules of a theme (MapTheme constructor, maptheme.js
8278
+ // 7740-7830), by substring of the type string as flat tests them:
8279
+ // nullIsValue (0 is a value: ZEROISNOTVALUE no, ZEROISVALUE yes, a CHART
8280
+ // with CATEGORICAL or several value fields always), negativeOk
8281
+ // (NEGATIVEISNOTVALUE no, NEGATIVEISVALUE yes, CATEGORICAL without
8282
+ // AGGREGATE no), undefinedOk (UNDEFINEDISNOTVALUE no), zeroExcludes
8283
+ // (ZEROISNOTVALUE: a 0 is left out of an aggregation's means)
8284
+ function flatValueRules(t, nFields) {
8285
+ t = String(t || '');
8286
+ let nullIsValue = true, negativeOk = true, undefinedOk = true;
8287
+ if (/ZEROISNOTVALUE/.test(t)) nullIsValue = false;
8288
+ if (/ZEROISVALUE/.test(t)) nullIsValue = true;
8289
+ if (/NEGATIVEISNOTVALUE/.test(t)) negativeOk = false;
8290
+ if (/NEGATIVEISVALUE/.test(t)) negativeOk = true;
8291
+ if (/UNDEFINEDISVALUE/.test(t)) undefinedOk = true;
8292
+ if (/UNDEFINEDISNOTVALUE/.test(t)) undefinedOk = false;
8293
+ if (/CHART/.test(t) && (/CATEGORICAL/.test(t) || nFields > 1)) nullIsValue = true;
8294
+ if (/CATEGORICAL/.test(t) && !/AGGREGATE/.test(t)) negativeOk = false;
8295
+ return { nullIsValue, negativeOk, undefinedOk, zeroExcludes: /ZEROISNOTVALUE/.test(t) };
8296
+ }
8297
+
8298
+ // An AGGREGATE or CATEGORICAL pie: one record's contribution to its item,
8299
+ // as flat's loadAndAggregateValuesOfTheme reads it (maptheme.js
8300
+ // 10465-10571, which every AGGREGATE theme goes through). `o`:
8301
+ // { rules (flatValueRules), sizeField, cat (CATEGORICAL: the record's
8302
+ // category, 0-based, -1 when it is none of them), fields (otherwise:
8303
+ // the value fields) }
8304
+ // Returns null when flat leaves the record out, else
8305
+ // { cat, size } (CATEGORICAL) or { parts, zeroNot, size }
8306
+ // - size: the size field as a number (1 without one); a record whose
8307
+ // size is no number, 0 (unless 0 is a value) or negative (unless
8308
+ // negatives are values) is left out
8309
+ // - CATEGORICAL: a value that is none of the categories (not in
8310
+ // style.values) is part -1 — no slice, but its size still counts in
8311
+ // the item's size (flat: nValuesA[-1]); left out only with
8312
+ // UNDEFINEDISNOTVALUE
8313
+ // - value fields: a value that is no number is 0 (UNDEFINEDISNOTVALUE:
8314
+ // the record is left out); a 0 is left out of the item's MEAN with
8315
+ // ZEROISNOTVALUE (zeroNot), else the record when 0 is no value; a
8316
+ // negative value when negatives are no values
8317
+ function flatPieRecord(raw, o) {
8318
+ const r = o.rules;
8319
+ const size = o.sizeField ? flatScanValue(raw[o.sizeField]) : 1;
8320
+ if (isNaN(size) || (size === 0 && !r.nullIsValue) || (size < 0 && !r.negativeOk)) return null;
8321
+ if (o.cat !== undefined) {
8322
+ if (o.cat < 0 && !r.undefinedOk) return null;
8323
+ return { cat: o.cat, size };
8324
+ }
8325
+ const parts = [], zeroNot = [];
8326
+ for (let k = 0; k < o.fields.length; k++) {
8327
+ let v = parseFloat(raw[o.fields[k]]);
8328
+ zeroNot[k] = false;
8329
+ if (isNaN(v)) {
8330
+ if (!r.undefinedOk) return null;
8331
+ v = 0;
8332
+ }
8333
+ if (v === 0) {
8334
+ if (r.zeroExcludes) { parts[k] = 0; zeroNot[k] = true; continue; }
8335
+ if (!r.nullIsValue) return null;
8336
+ }
8337
+ if (v < 0 && !r.negativeOk) return null;
8338
+ parts[k] = v;
8339
+ }
8340
+ return { parts, zeroNot, size };
8341
+ }
8342
+
8343
+ // Adds a record's flatPieRecord to its item (maptheme.js 10779-10943) —
8344
+ // `item` null for the first record of the item. Returns the item
8345
+ // { parts (sparse for CATEGORICAL: the category sums), counts (the
8346
+ // records per part, for MEAN), size (the item's size, flat's nSize),
8347
+ // n (its record count, flat's nCount) }
8348
+ // SUM (default): the parts and the |sizes| summed; MAX / MIN: their
8349
+ // maximum / minimum (a part missing so far counts 0, as in flat; the
8350
+ // record count stays 1); FIRST: the first record only; LAST: the last.
8351
+ // A CATEGORICAL record of no category (-1) adds its size only.
8352
+ function flatPieAccumulate(item, rec, flags) {
8353
+ const max = flags.has('MAX'), min = flags.has('MIN');
8354
+ if (item && flags.has('FIRST')) return item;
8355
+ if (!item || flags.has('LAST')) {
8356
+ if (rec.cat !== undefined) {
8357
+ const parts = [], counts = [];
8358
+ if (rec.cat >= 0) { parts[rec.cat] = rec.size; counts[rec.cat] = 1; }
8359
+ return { parts, counts, size: Math.abs(rec.size), n: 1 };
8360
+ }
8361
+ return { parts: rec.parts.slice(), counts: rec.zeroNot.map(z => (z ? 0 : 1)), size: Math.abs(rec.size), n: 1 };
8362
+ }
8363
+ const comb = (a, v) => (max ? Math.max(a || 0, v) : min ? Math.min(a || 0, v) : (a || 0) + v);
8364
+ if (rec.cat !== undefined) {
8365
+ if (rec.cat >= 0) {
8366
+ item.parts[rec.cat] = comb(item.parts[rec.cat], rec.size);
8367
+ item.counts[rec.cat] = (item.counts[rec.cat] || 0) + 1;
8368
+ }
8369
+ } else {
8370
+ for (let k = 0; k < rec.parts.length; k++) {
8371
+ item.parts[k] = comb(item.parts[k], rec.parts[k] || 0);
8372
+ if (!rec.zeroNot[k]) item.counts[k] = (item.counts[k] || 0) + 1;
8373
+ }
8374
+ }
8375
+ if (max) item.size = Math.max(item.size, rec.size);
8376
+ else if (min) item.size = Math.min(item.size, rec.size);
8377
+ else { item.size += Math.abs(rec.size); item.n++; }
8378
+ return item;
8379
+ }
8380
+
8381
+ // An aggregated pie item's parts and size after flat's post-aggregation
8382
+ // steps (maptheme.js 11056-11137): MEAN — a part divided by its record
8383
+ // count (CATEGORICAL: the category's records; value fields: the item's
8384
+ // records, or with ZEROISNOTVALUE the records with a value there), the
8385
+ // size by the item's record count unless SIZESUM; AUTO100 — the parts in
8386
+ // % of their sum. `nParts`: the number of parts (CATEGORICAL with
8387
+ // style.values: every category; without: up to the item's last one, as
8388
+ // flat's sparse nValuesA). Returns { parts, size }.
8389
+ function flatPieItemValues(item, t, o = {}) {
8390
+ t = String(t || '');
8391
+ const n = o.nParts != null ? o.nParts : item.parts.length;
8392
+ let parts = [];
8393
+ for (let k = 0; k < n; k++) parts[k] = item.parts[k] || 0;
8394
+ let size = item.size;
8395
+ if (/MEAN/.test(t)) {
8396
+ const perPart = o.categorical || /ZEROISNOTVALUE/.test(t);
8397
+ parts = parts.map((v, k) => (perPart ? (item.counts[k] > 0 ? v / item.counts[k] : 0) : v / item.n));
8398
+ if (!/SIZESUM/.test(t)) size /= item.n;
8399
+ }
8400
+ if (/AUTO100/.test(t)) {
8401
+ const sum = parts.reduce((a, v) => a + v, 0);
8402
+ parts = parts.map(v => v / (sum / 100));
8403
+ }
8404
+ return { parts, size };
8405
+ }
8406
+
8407
+ // CHART|VECTOR / CHART|BEZIER: the theme's items as real ixmaps-flat
8408
+ // builds them (maptheme.js loadAndAggregateValuesOfTheme, which every
8409
+ // VECTOR / BEZIER theme goes through, 9650: 10402-10450 the second
8410
+ // position, 10763 the item id, 10779-10985 the items). `records`:
8411
+ // { p1: [lng, lat], p2: [lng, lat] | null, cat (0-based category, -1
8412
+ // not one of them; null without CATEGORICAL), value (the value
8413
+ // field as a number, without CATEGORICAL), size (the size field as
8414
+ // a number, NaN when not one; 1 without a size field), raw }
8415
+ // `o`: { aggregate, vectorKey (the VECTOR flag: the item id has the
8416
+ // second position too), multiParts (CATEGORICAL: the category is part
8417
+ // of the id), max / min (MAX / MIN aggregation), zeroIsNotValue,
8418
+ // negativeIsNotValue }. Returns items
8419
+ // { p1, p2, cat, nSize, value, count, raw (the first record), raws }
8420
+ // - without AGGREGATE every record is an item, its nSize the size as
8421
+ // it is (negative too, 0 when not a number) — flat checks no value
8422
+ // there
8423
+ // - AGGREGATE: records of one id are one item: the id is the first
8424
+ // position (+ the category with CATEGORICAL, + the second position
8425
+ // with VECTOR — a BEZIER without VECTOR sums every flow of an origin
8426
+ // into its first record's flow, as flat); a record whose size is no
8427
+ // number (or 0 with ZEROISNOTVALUE, negative with
8428
+ // NEGATIVEISNOTVALUE) is left out; nSize the sum of the |sizes|
8429
+ // (MAX / MIN: their maximum / minimum), value the values' sum
8430
+ // - the first record of an item gives its category, second position,
8431
+ // and the record its tooltip reads
8432
+ function flatVectorItems(records, o = {}) {
8433
+ const items = [];
8434
+ if (!o.aggregate) {
8435
+ for (const r of records) {
8436
+ items.push({ p1: r.p1, p2: r.p2, cat: r.cat, nSize: r.size || 0, value: r.value, count: 1, raw: r.raw, raws: [r.raw] });
8437
+ }
8438
+ return items;
8439
+ }
8440
+ const byId = new Map();
8441
+ for (const r of records) {
8442
+ const s = r.size;
8443
+ if (isNaN(s) || (s === 0 && o.zeroIsNotValue) || (s < 0 && o.negativeIsNotValue)) continue;
8444
+ let id = r.p1[0] + ',' + r.p1[1];
8445
+ // flat's nValue for the id: the category number (1-based), 0 for a
8446
+ // value that is none of the categories
8447
+ if (o.multiParts) id += ',' + (r.cat != null && r.cat >= 0 ? r.cat + 1 : 0);
8448
+ if (o.vectorKey && r.p2) id += '&' + r.p2[0] + ',' + r.p2[1];
8449
+ const item = byId.get(id);
8450
+ if (!item) {
8451
+ const it = { p1: r.p1, p2: r.p2, cat: r.cat, nSize: Math.abs(s), value: r.value, count: 1, raw: r.raw, raws: [r.raw] };
8452
+ byId.set(id, it);
8453
+ items.push(it);
8454
+ continue;
8455
+ }
8456
+ if (o.max) {
8457
+ item.nSize = Math.max(item.nSize, s);
8458
+ item.value = Math.max(item.value || 0, r.value || 0);
8459
+ } else if (o.min) {
8460
+ item.nSize = Math.min(item.nSize, s);
8461
+ item.value = Math.min(item.value || 0, r.value || 0);
8462
+ } else {
8463
+ item.nSize += Math.abs(s);
8464
+ item.value = (item.value || 0) + (r.value || 0);
8465
+ }
8466
+ item.count++;
8467
+ item.raws.push(r.raw);
8468
+ }
8469
+ return items;
8470
+ }
8471
+
8472
+ // CHART|BEZIER: one flow's curve as flat draws it (maptheme.js
8473
+ // 18148-18375), in screen pixels relative to its first position (y
8474
+ // down — flat's SVG map coordinates, scaled). dx/dy: the second
8475
+ // position minus the first. o:
8476
+ // w the line width in flat's chart units (linewidth, else
8477
+ // 10 · (|nSize| / max)^(1/sizepow))
8478
+ // ll flat's arrow / gap length in chart units (w + 3, GAP: w +
8479
+ // gapsize — computed by the caller, see _buildVectorLayers)
8480
+ // unit pixels per chart unit (the object zoom × style.scale)
8481
+ // oz pixels per chart unit without style.scale
8482
+ // bow rangescale (flat's nBow; 5 when not given), RANDOM already
8483
+ // applied
8484
+ // t the type string (SHORT, POINTER / ARROW, GAP)
8485
+ // marker markersize (1)
8486
+ // gap gapsize (20)
8487
+ // Returns { start, c1, c2, end, vx, vy } — start/end/c1/c2 [x, y]
8488
+ // pixels; (vx, vy) the end after shortening, which decides the
8489
+ // gradient direction. The control points come from the unshortened
8490
+ // vector: offset by (dy, −dx) / 50 · bow (SHORT: the unit vector · 5
8491
+ // bow chart units), at 1/4 and 1/2 of the (shortened) vector. An arrow (POINTER
8492
+ // or a type word ending in ARROW) ends the curve ll · markersize units
8493
+ // short of the second position, room for the arrow head. GAP: the curve
8494
+ // starts gapsize units off the first position and ends ll / 500 of the
8495
+ // bow further on.
8496
+ function bezierVectorLayout(dx, dy, o) {
8497
+ const t = String(o.t || '');
8498
+ const len = Math.sqrt(dx * dx + dy * dy);
8499
+ const bow = o.bow || 5;
8500
+ let ox, oy;
8501
+ // (LONG's bow depends on flat's own map units — not drawn here, see
8502
+ // VECTOR_INERT_FLAGS: the default bow instead)
8503
+ if (/\bSHORT\b/.test(t)) { ox = dy / len * bow * 5 * o.oz; oy = dx / len * bow * 5 * o.oz; }
8504
+ else { ox = dy / 50 * bow; oy = dx / 50 * bow; }
8505
+ let x = dx, y = dy;
8506
+ // flat's /\bPOINTER|ARROW\b/: POINTER at a word start, or ARROW at a
8507
+ // word end (ARROW, SMALLARROW, LASTARROW ...)
8508
+ if (/\bPOINTER|ARROW\b/.test(t)) {
8509
+ const k = o.ll * o.unit * (o.marker || 1) / len;
8510
+ x -= dx * k; y -= dy * k;
8511
+ }
8512
+ let x0 = 0, y0 = 0;
8513
+ if (/\bGAP\b/.test(t)) {
8514
+ x += ox / 500 * o.ll;
8515
+ y -= oy / 500 * o.ll;
8516
+ x0 = x / len * (o.gap || 20) * o.oz;
8517
+ y0 = y / len * (o.gap || 20) * o.oz;
8518
+ }
8519
+ return { start: [x0, y0], c1: [x / 4 + ox, y / 4 - oy], c2: [x / 2 + ox, y / 2 - oy], end: [x, y], vx: x, vy: y };
8520
+ }
8521
+
8522
+ // flat's Themes.toArray for a style list (maptheme.js 999-1016): a list
8523
+ // as it is; a string split at "|" when it has one or names an RGB(…)
8524
+ // color (flat's /\RGB/: upper case only), else at ","
8525
+ function flatToArray(v) {
8526
+ if (Array.isArray(v)) return v;
8527
+ const str = String(v);
8528
+ return (/\|/.test(str) || /RGB/.test(str)) ? str.split('|') : str.split(',');
8529
+ }
8530
+ // a fixed number in [0, 1) for a string — stands in for flat's
8531
+ // Math.random() where the harness needs the same drawing every time
8532
+ function hashUnit(str) {
8533
+ let h = 2166136261;
8534
+ for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
8535
+ return (h >>> 0) / 4294967296;
8536
+ }
8537
+
8538
+ // points of a cubic Bézier, n segments (n + 1 points)
8539
+ function cubicBezierPoints(p0, c1, c2, p3, n) {
8540
+ const out = [];
8541
+ for (let i = 0; i <= n; i++) {
8542
+ const s = i / n, u = 1 - s;
8543
+ const a = u * u * u, b = 3 * u * u * s, c = 3 * u * s * s, d = s * s * s;
8544
+ out.push([a * p0[0] + b * c1[0] + c * c2[0] + d * p3[0], a * p0[1] + b * c1[1] + c * c2[1] + d * p3[1]]);
8545
+ }
8546
+ return out;
8547
+ }
8548
+
8549
+ // flat's arrow marker (an SVG <marker>, markerUnits strokeWidth, orient
8550
+ // auto) at a line's end: the triangle M0,s L s,s/2 L0,0 in units of the
8551
+ // line width sw, its reference point (refX, refY) on the end point, its
8552
+ // x axis along the end's direction (tx, ty). BEZIER: s = min(5, 2.5 +
8553
+ // 5 / (w · scale)) · markersize, ref (s / 1.7, s / 2) (maptheme.js
8554
+ // 18309-18320); VECTOR: s = 4, ref (4, 2) — the tip on the end point
8555
+ // (18411-18424). A zero direction is orient 0: pointing right.
8556
+ // Returns the three corners [x, y] (pixels, y down).
8557
+ function arrowMarkerTriangle(end, tx, ty, sw, s, refX, refY) {
8558
+ const l = Math.sqrt(tx * tx + ty * ty);
8559
+ const ux = l ? tx / l : 1, uy = l ? ty / l : 0;
8560
+ const at = (mx, my) => {
8561
+ const a = (mx - refX) * sw, b = (my - refY) * sw;
8562
+ return [end[0] + a * ux - b * uy, end[1] + a * uy + b * ux];
8563
+ };
8564
+ return [at(0, s), at(s, s / 2), at(0, 0)];
8565
+ }
8566
+
8567
+ // BEZIER GRADIENT / FADEIN (maptheme.js 18233-18284): a linear gradient
8568
+ // over the curve's bounding box, vertical when the (shortened) vector
8569
+ // is more vertical than horizontal, else horizontal; the stop on the
8570
+ // first position's side has the fade opacity (FADEIN 0.2, GRADIENT 2),
8571
+ // the other one 2 — SVG clamps both to 1 — and the colors run from the
8572
+ // second color (linecolor's first entry) at the start to the line color
8573
+ // at the end. Returns per point { k: 0 … 1 from the start side's stop,
8574
+ // alpha } — the color mix k and opacity of each point.
8575
+ function fadeGradientStops(points, vx, vy, fade) {
8576
+ const vertical = Math.abs(vy) > Math.abs(vx);
8577
+ const axis = vertical ? 1 : 0;
8578
+ let min = Infinity, max = -Infinity;
8579
+ for (const p of points) { if (p[axis] < min) min = p[axis]; if (p[axis] > max) max = p[axis]; }
8580
+ const v = vertical ? vy : vx;
8581
+ const a = Math.min(1, Math.max(0, fade));
8582
+ // offset 0 is the box's left / top side: the start side unless the
8583
+ // vector points left / up
8584
+ const startAtZero = !(v < 0);
8585
+ return points.map(p => {
8586
+ let f = max > min ? (p[axis] - min) / (max - min) : 0;
8587
+ if (!startAtZero) f = 1 - f;
8588
+ // f: 0 on the start side, 1 on the end side
8589
+ return { k: f, alpha: a + (1 - a) * f };
8590
+ });
8591
+ }
8592
+
7459
8593
  // CHART|SYMBOL|SEQUENCE (flat maptheme.js 21184-21300, 21790-21925,
7460
8594
  // 22296-22395): one symbol per category part of a chart, in category
7461
8595
  // order or sorted (SORT: DOWN biggest first, UP smallest first), parts
@@ -8511,6 +9645,9 @@ in vec4 vPieLineColor;
8511
9645
  cell.value += f.properties.value;
8512
9646
  if (f.properties.classValue !== undefined) cell.classValue += f.properties.classValue;
8513
9647
  if (post) cell.value100 += f.properties.value100 || 0;
9648
+ // an AGGREGATE / CATEGORICAL pie's record (flatPieRecord): its parts
9649
+ // and size added to the cell's item as flat does (flatPieAccumulate)
9650
+ if (f.properties.pie) cell.pie = flatPieAccumulate(cell.pie || null, f.properties.pie, flags);
8514
9651
  const series = f.properties.series;
8515
9652
  if (series) {
8516
9653
  if (!cell.series) cell.series = new Array(series.length).fill(0);
@@ -8532,6 +9669,7 @@ in vec4 vPieLineColor;
8532
9669
  ...(cell.raws && cell.raws.length ? { raws: cell.raws } : {}) }
8533
9670
  : cell.first.properties;
8534
9671
  if (cell.series && cell.n > 1) props = Object.assign({}, props, { series: cell.series });
9672
+ if (cell.pie) props = Object.assign({}, props, { pie: cell.pie });
8535
9673
  // a field aggregation's cell key (the field value): the categories of
8536
9674
  // one cell are merged by it (groupCoLocated), not by their positions
8537
9675
  if (field) props = Object.assign({}, props, { aggKey: cell.key });
@@ -8709,7 +9847,10 @@ in vec4 vPieLineColor;
8709
9847
  this._filterExpr = spec.filter || '';
8710
9848
  this.mapOptions = mapOptions || {};
8711
9849
  this._specBinding = spec.binding;
8712
- this.features = applyField100(filterFlatValues(this._withDensity(fc.features), this.binding, this.flags), this.binding, this.flags, this.style);
9850
+ // VECTOR / BEZIER keep every record: flat checks their values only
9851
+ // when it aggregates them (flatVectorItems)
9852
+ const withDensity = this._withDensity(fc.features);
9853
+ this.features = applyField100(isVectorChart(this.flags) ? withDensity : filterFlatValues(withDensity, this.binding, this.flags), this.binding, this.flags, this.style);
8713
9854
  this.binding = field100Binding(this.binding, this.flags);
8714
9855
  this._iconCache = new Map();
8715
9856
  this._glowIconCache = new Map();
@@ -8804,7 +9945,8 @@ in vec4 vPieLineColor;
8804
9945
  this._gridRefLat = (minLat <= maxLat) ? (minLat + maxLat) / 2 : 0;
8805
9946
  }
8806
9947
 
8807
- if (this._isPieChart() && this.binding.value) {
9948
+ // a bar chart's parts are a pie's (its value fields or categories)
9949
+ if ((this._isPieChart() || this._isBarChart()) && this.binding.value) {
8808
9950
  this._preparePie();
8809
9951
  } else if (this.flags.has('DOMINANT') && this.binding.value && !isAggregatedCategoricalChoropleth(this)) {
8810
9952
  // .type("CHOROPLETH|DOMINANT") — a MULTI-field bound value
@@ -8956,6 +10098,7 @@ in vec4 vPieLineColor;
8956
10098
  if (this._usesAggregationIndex()) {
8957
10099
  this._buildAggregationIndex(this.features);
8958
10100
  }
10101
+ if (this._isVectorChart()) this._prepareVector();
8959
10102
 
8960
10103
  this._prepareAlphaField();
8961
10104
  }
@@ -9049,6 +10192,36 @@ in vec4 vPieLineColor;
9049
10192
  _resolveAggregateValue(props) { return resolveAggregateValue(this.binding, this.flags, props); }
9050
10193
 
9051
10194
  _buildAggregationIndex(sourceFeatures) {
10195
+ // an AGGREGATE / CATEGORICAL pie: one bucket, each record carrying its
10196
+ // contribution to its item (flatPieRecord), added up per position or
10197
+ // grid cell by aggregateOnGrid (flatPieAccumulate)
10198
+ if (this._isAggregatedPie()) {
10199
+ const t = this.flags.typeString != null ? this.flags.typeString : [...this.flags].join('|');
10200
+ const fields = this._pieCategorical ? null : String(this.binding.value || '').split('|');
10201
+ const rules = flatValueRules(t, fields ? fields.length : 1);
10202
+ const sizeField = this.binding.size && this.binding.size !== '$item$' ? this.binding.size : null;
10203
+ const feats = [];
10204
+ for (const f of sourceFeatures) {
10205
+ const c = f.geometry && f.geometry.coordinates;
10206
+ if (!c || typeof c[0] !== 'number' || !isFinite(c[0]) || !isFinite(c[1])) continue;
10207
+ const p = f.properties || {};
10208
+ let cat;
10209
+ if (this._pieCategorical) {
10210
+ const k = this._pieCategoryKey(p[this.binding.value]);
10211
+ const i = k == null ? undefined : this._pieCategoryIndex.get(k);
10212
+ cat = i == null ? -1 : i;
10213
+ }
10214
+ const pie = flatPieRecord(p, { rules, sizeField, cat, fields });
10215
+ if (!pie) continue;
10216
+ feats.push({ type: 'Feature', geometry: f.geometry, properties: { value: pie.size, pie, raw: p } });
10217
+ }
10218
+ this._featuresByCategory = [feats];
10219
+ this._clusterIndices = null;
10220
+ this._clusterRadiusPx = null;
10221
+ this._aggregateStatsCache = null;
10222
+ this._pieStatsCache = null;
10223
+ return;
10224
+ }
9052
10225
  // Range-classed (non-CATEGORICAL numeric) coloring: DON'T pre-split
9053
10226
  // by class here — the real engine aggregates every record together
9054
10227
  // first and classes color from the resulting CELL TOTAL afterward
@@ -9231,7 +10404,10 @@ in vec4 vPieLineColor;
9231
10404
  this._timeMin = undefined;
9232
10405
  const facetFilters = this.facetFilters, runtimeFilterExpr = this._runtimeFilterExpr, timeFrame = this._timeFrame;
9233
10406
  this.binding = this._specBinding;
9234
- this.features = applyField100(filterFlatValues(this._withDensity(fc.features), this.binding, this.flags), this.binding, this.flags, this.style);
10407
+ // VECTOR / BEZIER keep every record: flat checks their values only
10408
+ // when it aggregates them (flatVectorItems)
10409
+ const withDensity = this._withDensity(fc.features);
10410
+ this.features = applyField100(isVectorChart(this.flags) ? withDensity : filterFlatValues(withDensity, this.binding, this.flags), this.binding, this.flags, this.style);
9235
10411
  this.binding = field100Binding(this.binding, this.flags);
9236
10412
  this._iconCache.clear();
9237
10413
  this._glowIconCache.clear();
@@ -9274,10 +10450,14 @@ in vec4 vPieLineColor;
9274
10450
  }
9275
10451
 
9276
10452
  _usesAggregationIndex() {
10453
+ // AGGREGATE / CATEGORICAL pies aggregate their records into items
10454
+ // (see _buildAggregationIndex)
10455
+ if (this._isAggregatedPie()) return true;
9277
10456
  // GRIDSIZE bins its own grid (_ensureGridIndex) — PLOT curves and the
9278
10457
  // plain mesh; a classed GRIDSIZE symbol chart is aggregated like any
9279
10458
  // other (see _isPlainGridMesh)
9280
- return (this.flags.has('AGGREGATE') || isSymbolChart(this.flags))
10459
+ // VECTOR / BEZIER aggregate by their positions (flatVectorItems)
10460
+ return (this.flags.has('AGGREGATE') || isSymbolChart(this.flags)) && !isVectorChart(this.flags)
9281
10461
  && (!this.flags.has('GRIDSIZE') || (!this.flags.has('PLOT') && !this._isPlainGridMesh()));
9282
10462
  }
9283
10463
 
@@ -9562,7 +10742,15 @@ in vec4 vPieLineColor;
9562
10742
  return this._glowIconCache.get(key);
9563
10743
  }
9564
10744
 
9565
- _buildBubbleIcon(counts, colors, marked = null, shapes = null) {
10745
+ // look (optional, a CATEGORICAL BUBBLE's flat look — see bubbleIconLook):
10746
+ // every part filled at look.fillOpacity and stroked like its own flat
10747
+ // bubble (look.stroke, else its color × 0.7; look.strokeOpacity), the
10748
+ // width look.width canvas px for a part of the icon's full radius,
10749
+ // √(share of it) for a smaller one (flat's width ∝ √r) — OUTLINE:
10750
+ // ∝ min(1, r / normal radius), look.outlineRatio being the full
10751
+ // radius / normal radius. Without it the older look: 0.9 fill, a 1 px
10752
+ // white stroke (the SYMBOL branch's packed icon).
10753
+ _buildBubbleIcon(counts, colors, marked = null, shapes = null, look = null) {
9566
10754
  // The cache key quantizes each count's RATIO to the group's total,
9567
10755
  // not the raw count — computeBubblePackLayout's radii are purely
9568
10756
  // sqrt(c/total), so two clusters with wildly different absolute
@@ -9592,7 +10780,8 @@ in vec4 vPieLineColor;
9592
10780
  // coarse share buckets that can put a small marked part at 0
9593
10781
  const key = counts.map(c => Math.round((c / total) * RATIO_BUCKETS)).join('-') + (marked
9594
10782
  ? '|iso:' + counts.map((c, i) => (c > 0 && marked.has(i) ? i + ':' + Math.round(Math.sqrt(c / total) * 100) : null)).filter(Boolean).join(',')
9595
- : '') + (shapes ? '|sym:' + shapes.map(v => String(v).toLowerCase()).join(',') : '');
10783
+ : '') + (shapes ? '|sym:' + shapes.map(v => String(v).toLowerCase()).join(',') : '')
10784
+ + (look ? `|fb:${look.fillOpacity}:${look.strokeOpacity}:${look.stroke ? look.stroke.join(',') : look.noStroke ? 'none' : 'low'}:${look.width}${look.outlineRatio != null ? ':' + look.outlineRatio : ''}` : '');
9596
10785
  if (this._iconCache.has(key)) return this._iconCache.get(key);
9597
10786
  const size = BUBBLE_ICON_SIZE;
9598
10787
  const canvas = document.createElement('canvas');
@@ -9607,13 +10796,33 @@ in vec4 vPieLineColor;
9607
10796
  // _resolveSymbolShape; circles when the name isn't one GL draws
9608
10797
  const shape = shapes ? normalizeSymbolShape(shapes[Math.min(p.i, shapes.length - 1)]) : 'circle';
9609
10798
  const px = cx + offsets[i].x * fitScale, py = cy + offsets[i].y * fitScale;
10799
+ let pr = radii[i] * fitScale, lw = 0;
10800
+ if (look && !look.noStroke) {
10801
+ const k = pr / (size / 2);
10802
+ lw = Math.min(pr, look.outlineRatio != null
10803
+ ? look.width * Math.min(1, k * look.outlineRatio) / Math.min(1, look.outlineRatio)
10804
+ : look.width * Math.sqrt(k));
10805
+ // inside the part, as _buildSingleIcon: the icon's edge is the bubble's
10806
+ pr = Math.max(0.5, pr - lw / 2);
10807
+ }
9610
10808
  ctx.beginPath();
9611
- if (shape === 'circle') ctx.arc(px, py, radii[i] * fitScale, 0, Math.PI * 2);
9612
- else drawSymbolPath(ctx, shape, px, py, radii[i] * fitScale);
10809
+ if (shape === 'circle') ctx.arc(px, py, pr, 0, Math.PI * 2);
10810
+ else drawSymbolPath(ctx, shape, px, py, pr);
9613
10811
  ctx.closePath();
9614
10812
  ctx.fillStyle = `rgb(${colors[p.i].join(',')})`;
9615
- ctx.globalAlpha = 0.9; ctx.fill(); ctx.globalAlpha = 1;
9616
- ctx.lineWidth = 1; ctx.strokeStyle = '#fff'; ctx.stroke();
10813
+ if (!look) {
10814
+ ctx.globalAlpha = 0.9; ctx.fill(); ctx.globalAlpha = 1;
10815
+ ctx.lineWidth = 1; ctx.strokeStyle = '#fff'; ctx.stroke();
10816
+ return;
10817
+ }
10818
+ ctx.globalAlpha = look.fillOpacity; ctx.fill();
10819
+ if (lw > 0) {
10820
+ ctx.globalAlpha = look.strokeOpacity;
10821
+ ctx.lineWidth = lw;
10822
+ ctx.strokeStyle = `rgb(${(look.stroke || flatDerivateRgb(colors[p.i], 0.7)).join(',')})`;
10823
+ ctx.stroke();
10824
+ }
10825
+ ctx.globalAlpha = 1;
9617
10826
  });
9618
10827
  const icon = { url: canvas.toDataURL(), width: size, height: size, anchorX: size / 2, anchorY: size / 2, id: key };
9619
10828
  return this._cacheIcon(key, icon);
@@ -9642,10 +10851,12 @@ in vec4 vPieLineColor;
9642
10851
  // cached bubble icon here already accepts; the border scales with
9643
10852
  // the bubble exactly as the fill already does, which reads as
9644
10853
  // reasonable rather than wrong.
9645
- _buildSingleIcon(colorRgb, opacity, shape, borderColorRgb, borderWidth) {
10854
+ // borderOpacity (default 1): the border's own alpha (flat's
10855
+ // stroke-opacity, see flatBubbleLook)
10856
+ _buildSingleIcon(colorRgb, opacity, shape, borderColorRgb, borderWidth, borderOpacity = 1) {
9646
10857
  shape = shape || 'circle';
9647
10858
  borderWidth = borderWidth || 0;
9648
- const key = `single-${shape}-${colorRgb.join(',')}-${opacity}-${borderColorRgb ? borderColorRgb.join(',') : 'none'}-${borderWidth}`;
10859
+ const key = `single-${shape}-${colorRgb.join(',')}-${opacity}-${borderColorRgb ? borderColorRgb.join(',') : 'none'}-${borderWidth}${borderOpacity !== 1 ? `-so${borderOpacity}` : ''}`;
9649
10860
  if (this._iconCache.has(key)) return this._iconCache.get(key);
9650
10861
  const size = BUBBLE_ICON_SIZE;
9651
10862
  const canvas = document.createElement('canvas');
@@ -9667,7 +10878,7 @@ in vec4 vPieLineColor;
9667
10878
  ctx.globalAlpha = shape === 'empty' ? 0 : opacity;
9668
10879
  ctx.fill();
9669
10880
  if (borderWidth > 0 && borderColorRgb) {
9670
- ctx.globalAlpha = 1;
10881
+ ctx.globalAlpha = borderOpacity;
9671
10882
  ctx.strokeStyle = `rgb(${borderColorRgb.join(',')})`;
9672
10883
  ctx.lineWidth = borderWidth;
9673
10884
  ctx.stroke();
@@ -9765,18 +10976,35 @@ in vec4 vPieLineColor;
9765
10976
  local: {}
9766
10977
  };
9767
10978
 
10979
+ // a VECTOR / BEZIER flow (measured on the twin page): no item chart,
10980
+ // label or class; its value is flat's nValuesA[0] — with CATEGORICAL
10981
+ // the category's number (1 for the first), else the (summed) value
10982
+ // field; its count the records of the item
10983
+ if (this._isVectorChart() && props.vector) {
10984
+ const v = props.vector.itemValue;
10985
+ dataObj.theme.chart = dataObj.theme.item.chart = '';
10986
+ dataObj.theme.item.value = v == null || isNaN(v) ? '' : flatFormatValue(v, explicitValueDecimals(this.style) ?? (Math.abs(v) < 1 ? 2 : 0), 'ROUND');
10987
+ dataObj.theme.item.label = dataObj.theme.item.class = '';
10988
+ dataObj.theme.item.count = props.count;
10989
+ }
10990
+
9768
10991
  // bare {{FIELD}}, {{raw.FIELD}}, {{local.FIELD}} — only meaningful
9769
- // for a single unaggregated record (see class comment above)
9770
- if (!isGroup && props.raw) {
9771
- Object.keys(props.raw).forEach(field => {
10992
+ // for a single unaggregated record (see class comment above); an
10993
+ // aggregated pie's item gives its first record's, as flat (data[0])
10994
+ const fieldsRaw = isGroup ? props.firstRaw : props.raw;
10995
+ if (fieldsRaw) {
10996
+ Object.keys(fieldsRaw).forEach(field => {
9772
10997
  if (field === 'geometry') return;
9773
- const v = props.raw[field];
10998
+ const v = fieldsRaw[field];
9774
10999
  if (this._isNumericValue(v)) {
9775
11000
  dataObj[field] = this._formatTooltipValue(v);
9776
11001
  dataObj.raw[field] = v;
9777
11002
  } else {
9778
11003
  dataObj[field] = v;
9779
- dataObj.local[field] = v;
11004
+ // translated, as flat's tooltip (tooltip_mustache.js 446:
11005
+ // ixmaps.getLocalString(value)) — the migration page's ISO
11006
+ // code → country name in {{local.from}} → {{local.to}}
11007
+ dataObj.local[field] = makeLocalText([])(v);
9780
11008
  }
9781
11009
  });
9782
11010
  }
@@ -10062,11 +11290,21 @@ in vec4 vPieLineColor;
10062
11290
  // USER charts (a page's own chart function, style.userdraw) go the
10063
11291
  // chart pipeline too, drawn by _buildUserChartLayers (bubbles when the
10064
11292
  // page has no such function)
11293
+ // VECTOR / BEZIER (flat's chartMap draws them before it calls
11294
+ // drawChart, maptheme.js 18148, 18376)
10065
11295
  // PIE / DONUT (flat's PIE branch comes before BUBBLE and SYMBOL in
10066
11296
  // drawChart, maptheme.js 19939)
11297
+ if (this._isVectorChart()) return this._buildVectorLayers(zoom, bbox, liveZoom, globeCenter);
10067
11298
  if (this._isPieChart()) return this._buildPieLayers(zoom, bbox, liveZoom, globeCenter);
11299
+ // BAR / BARS (flat's BAR branch, after BUBBLE and SYMBOL: a type
11300
+ // with those is not a bar chart, see isBarChart)
11301
+ if (this._isBarChart()) return this._buildBarLayers(zoom, bbox, liveZoom, globeCenter);
10068
11302
  if (isSymbolChart(this.flags)) return this._buildChartLayers(zoom, bbox, liveZoom, globeCenter);
10069
- console.warn(`[ixmaps-gl] layer "${this.name}": type "${[...this.flags].join('|')}" has no implemented renderer`);
11303
+ // once per theme, not on every redraw
11304
+ if (!this._warnedNoRenderer) {
11305
+ this._warnedNoRenderer = true;
11306
+ console.warn(`[ixmaps-gl] layer "${this.name}": type "${[...this.flags].join('|')}" has no implemented renderer`);
11307
+ }
10070
11308
  return [];
10071
11309
  }
10072
11310
 
@@ -10428,16 +11666,26 @@ in vec4 vPieLineColor;
10428
11666
  return String(this.binding.value || '').split('|');
10429
11667
  }
10430
11668
  // CHART|PIE (and PIE|DONUT): one pie per record, its slices the value
10431
- // fields (_buildPieLayers). An AGGREGATE pie (flat sums the records of
10432
- // a grid cell or category into one pie) and a CATEGORICAL one (a part
10433
- // per category) have no renderer here yet.
11669
+ // fields; AGGREGATE and CATEGORICAL pies one pie per item — the records
11670
+ // of a position (or grid cell) — its slices the categories or the value
11671
+ // fields (_isAggregatedPie, _buildPieLayers)
11672
+ _isVectorChart() { return isVectorChart(this.flags); }
11673
+ // CHART|BAR / BARS: a bar chart per record or item, its bars a pie's
11674
+ // parts (_buildBarLayers)
11675
+ _isBarChart() { return isBarChart(this.flags); }
10434
11676
  _isPieChart() {
10435
- return this.flags.has('CHART') && this.flags.has('PIE') && !this.flags.has('AGGREGATE')
10436
- && !this.flags.has('CATEGORICAL') && !this.flags.has('DOT');
11677
+ return !isVectorChart(this.flags) && this.flags.has('CHART') && this.flags.has('PIE') && !this.flags.has('DOT');
11678
+ }
11679
+ // a pie whose items flat aggregates (loadAndAggregateValuesOfTheme):
11680
+ // AGGREGATE, or CATEGORICAL (a part per category — flat sums the
11681
+ // records of an item's position by category even without AGGREGATE)
11682
+ // (a bar chart's items too)
11683
+ _isAggregatedPie() {
11684
+ return (this._isPieChart() || this._isBarChart()) && (this.flags.has('AGGREGATE') || this.flags.has('CATEGORICAL'));
10437
11685
  }
10438
11686
  // a per-item chart whose parts are its value fields (legend rows, sums)
10439
11687
  _isMultiFieldChart() {
10440
- return !!this._multiFields && (this._isMultiFieldSequence() || this._isPieChart());
11688
+ return !!this._multiFields && (this._isMultiFieldSequence() || this._isPieChart() || this._isBarChart());
10441
11689
  }
10442
11690
  // flat's theme statistics for a pie (maptheme.js 9119-9130): nMin/nMax
10443
11691
  // over every part value of every item, the classes (partsA, flat's
@@ -10445,6 +11693,8 @@ in vec4 vPieLineColor;
10445
11693
  // colors) — flat colors a 1-part pie (or any with CLASSES) by them
10446
11694
  // (maptheme.js 20330-20341); several parts are colored by field.
10447
11695
  _preparePie() {
11696
+ if (this.flags.has('CATEGORICAL')) { this._preparePieCategories(); return; }
11697
+ this._pieCategorical = false;
10448
11698
  const fields = String(this.binding.value).split('|');
10449
11699
  const pooled = [];
10450
11700
  for (const f of this.features) {
@@ -10472,6 +11722,61 @@ in vec4 vPieLineColor;
10472
11722
  }
10473
11723
  }
10474
11724
 
11725
+ // a CATEGORICAL pie's categories (flat's getStringValueIndex, maptheme.js
11726
+ // 9204-9243): style.values in their order, else the values in the order
11727
+ // the records bring them; a value is trimmed (IGNORECASE: upper case),
11728
+ // an empty one is none (UNDEFINEDISVALUE: "undefined"). A slice per
11729
+ // category in its colorscheme color, labeled by style.label. flat's
11730
+ // classes are the category numbers (partsA: i + 1, the last one 0.001
11731
+ // wider) — its value range (nMin / nMax) follows the items
11732
+ // (_pieAggregateStats).
11733
+ _preparePieCategories() {
11734
+ const st = this.style;
11735
+ const t = this.flags.typeString != null ? this.flags.typeString : [...this.flags].join('|');
11736
+ const ignoreCase = /IGNORECASE/.test(t), undefinedIsValue = /UNDEFINEDISVALUE/.test(t);
11737
+ const key = v => {
11738
+ let k = v == null ? '' : String(v);
11739
+ if (k.length === 0 || k === ' ') { if (!undefinedIsValue) return null; k = 'undefined'; }
11740
+ if (ignoreCase) k = k.toUpperCase();
11741
+ return k.trim();
11742
+ };
11743
+ const index = new Map();
11744
+ const explicit = Array.isArray(st.values) ? st.values : null;
11745
+ const labels = [];
11746
+ if (explicit) {
11747
+ explicit.forEach(v => { const k = key(v); if (k != null && !index.has(k)) { index.set(k, labels.length); labels.push(String(v).trim()); } });
11748
+ } else {
11749
+ for (const f of this.features) {
11750
+ const k = key(f.properties[this.binding.value]);
11751
+ if (k != null && !index.has(k)) { index.set(k, labels.length); labels.push(k); }
11752
+ }
11753
+ }
11754
+ this._pieCategorical = true;
11755
+ this._pieExplicitValues = !!explicit;
11756
+ this._pieCategoryKey = key;
11757
+ this._pieCategoryIndex = index;
11758
+ this.categoryLabels = labels;
11759
+ const explicitLabel = Array.isArray(st.label) ? st.label.map(String) : null;
11760
+ this.categoryDisplayLabels = explicitLabel && explicitLabel.length === labels.length ? explicitLabel : labels;
11761
+ this.categoryColorsRgb = resolveClassColors(st.colorscheme, this.categoryDisplayLabels, st.classes);
11762
+ // the legend and the facets look a record's raw value up
11763
+ const byLabel = new Map();
11764
+ for (const f of this.features) {
11765
+ const v = f.properties[this.binding.value];
11766
+ if (byLabel.has(v)) continue;
11767
+ const k = key(v);
11768
+ if (k != null && index.has(k)) byLabel.set(v, index.get(k));
11769
+ }
11770
+ labels.forEach((l, i) => { if (!byLabel.has(l)) byLabel.set(l, i); });
11771
+ this.categoryIndexByLabel = byLabel;
11772
+ const n = labels.length;
11773
+ this.partsA = labels.map((l, i) => ({ min: i + 1, max: i + 1 + (i === n - 1 ? 0.001 : 0) + 1e-9 }));
11774
+ this._rangeClassed = false;
11775
+ this._multiFields = null;
11776
+ this._pieFields = null;
11777
+ this._partNoFill = new Set();
11778
+ }
11779
+
10475
11780
  // x-axis labels of a per-item PLOT (style.xaxis / label, else the fields)
10476
11781
  _itemPlotLabels() {
10477
11782
  const fields = this._itemPlotFields();
@@ -11602,8 +12907,7 @@ in vec4 vPieLineColor;
11602
12907
  const combined = individual.concat(groups).sort((a, b) => sizeValueOf(a) - sizeValueOf(b));
11603
12908
  // maxcharts: like the flat engine (maptheme.js nMaxCharts), draw only the
11604
12909
  // biggest N items — combined is ascending, so drop from the front
11605
- const maxCharts = Math.round(styleNum(this.style.maxcharts)) || 0;
11606
- if (maxCharts > 0 && combined.length > maxCharts) combined.splice(0, combined.length - maxCharts);
12910
+ applyFlatMaxCharts(combined, this.style);
11607
12911
  if (this.flags.has('USER')) {
11608
12912
  const userLayers = this._buildUserChartLayers(combined, liveZoom, sizeValueOf);
11609
12913
  if (userLayers) return layers.concat(userLayers);
@@ -11884,6 +13188,19 @@ in vec4 vPieLineColor;
11884
13188
  // fillopacity and linecolor, as a single item (flat draws one
11885
13189
  // symbol) — the packed multi-category icon is for CATEGORICAL cells
11886
13190
  const singleSymbol = d => !d.properties.counts || (this._rangeClassed && d.properties.cat != null);
13191
+ // a CATEGORICAL BUBBLE (flat's BUBBLE branch): every bubble — a single
13192
+ // one and each part of a packed one — filled and stroked as flat's,
13193
+ // see flatBubbleLook; the icon carries both opacities
13194
+ const flatLook = flatChartBranch(this.flags) === 'bubble' && this.flags.has('CATEGORICAL')
13195
+ ? d => bubbleIconLook(radiusOf(d), objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(this.style.scale) || 1), this.style, this.flags, BUBBLE_ICON_SIZE)
13196
+ : null;
13197
+ const singleIcon = d => {
13198
+ const rgb = (this.categoryColorsRgb || [])[d.properties.cat];
13199
+ if (!flatLook) return this._buildSingleIcon(rgb, fillOpacity, shapeOf(d.properties), singleBorderColorRgb, singleBorderWidthPx);
13200
+ const look = flatLook(d);
13201
+ return this._buildSingleIcon(rgb, look.fillOpacity, shapeOf(d.properties),
13202
+ look.noStroke || !rgb ? null : (look.stroke || flatDerivateRgb(rgb, 0.7)), look.width, look.strokeOpacity);
13203
+ };
11887
13204
  // flat's symbol outline (maptheme.js 21595-21610 and 21950-21997,
11888
13205
  // drawn 21625-21634 / 21995-22035): every SYMBOL shape is stroked,
11889
13206
  // (linewidth || 1) · normalX(min(2r / rMax, 0.2)) with rMax the
@@ -11919,17 +13236,21 @@ in vec4 vPieLineColor;
11919
13236
  // icon's look, inside the radius
11920
13237
  if (combined.length && !alignOf.active && combined.every(d => singleSymbol(d) && shapeOf(d.properties) === 'circle')) {
11921
13238
  const radiusPx = d => vRadius(sizeValueOf(d));
11922
- const lineRgb = d => outlineRgb(d) || [0, 0, 0];
13239
+ // a CATEGORICAL BUBBLE: flat's look (flatBubbleLook) in screen px,
13240
+ // its stroke centered on the radius as flat's SVG circle
13241
+ const lookUnit = objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(this.style.scale) || 1);
13242
+ const circleLook = flatLook ? d => flatBubbleLook((this.categoryColorsRgb || [])[d.properties.cat] || [128, 128, 128], radiusPx(d), lookUnit, this.style, this.flags) : null;
13243
+ const lineRgb = d => (circleLook ? circleLook(d).stroke : outlineRgb(d)) || [0, 0, 0];
11923
13244
  layers.push(new ScatterplotLayer({
11924
13245
  id: `ix-bubbles-${this.name}-circles-g${this._iconGeneration}`,
11925
13246
  data: combined, pickable: true,
11926
13247
  getPosition: d => d.geometry.coordinates,
11927
- getRadius: d => radiusPx(d) - (symbolOutline ? 0 : outlinePx(d, radiusPx(d)) / 2),
13248
+ getRadius: d => radiusPx(d) - (symbolOutline || circleLook ? 0 : outlinePx(d, radiusPx(d)) / 2),
11928
13249
  radiusUnits: 'pixels',
11929
- getFillColor: d => [...((this.categoryColorsRgb || [])[d.properties.cat] || [128, 128, 128]).slice(0, 3), Math.round(this._iconAlpha(d) * fillOpacity)],
11930
- stroked: combined.some(d => outlineRgb(d)),
11931
- getLineColor: d => [...lineRgb(d).slice(0, 3), this._iconAlpha(d)],
11932
- getLineWidth: d => outlinePx(d, radiusPx(d)),
13250
+ getFillColor: d => [...((this.categoryColorsRgb || [])[d.properties.cat] || [128, 128, 128]).slice(0, 3), Math.round(this._iconAlpha(d) * (circleLook ? circleLook(d).fillOpacity : fillOpacity))],
13251
+ stroked: circleLook ? combined.some(d => circleLook(d).stroke) : combined.some(d => outlineRgb(d)),
13252
+ getLineColor: d => [...lineRgb(d).slice(0, 3), Math.round(this._iconAlpha(d) * (circleLook ? circleLook(d).strokeOpacity : 1))],
13253
+ getLineWidth: d => (circleLook ? (circleLook(d).stroke ? circleLook(d).width : 0) : outlinePx(d, radiusPx(d))),
11933
13254
  lineWidthUnits: 'pixels',
11934
13255
  billboard: true,
11935
13256
  parameters: ICON_LAYER_GLOBE_PARAMETERS
@@ -11939,7 +13260,8 @@ in vec4 vPieLineColor;
11939
13260
  data: combined, pickable: true,
11940
13261
  getPosition: d => d.geometry.coordinates,
11941
13262
  getIcon: d => {
11942
- if (!singleSymbol(d)) return this._buildBubbleIcon(d.properties.counts, this.categoryColorsRgb, this._groupIsolation(d), Array.isArray(this.style.symbols) ? this.style.symbols : null);
13263
+ if (!singleSymbol(d)) return this._buildBubbleIcon(d.properties.counts, this.categoryColorsRgb, this._groupIsolation(d), Array.isArray(this.style.symbols) ? this.style.symbols : null, flatLook && flatLook(d));
13264
+ if (flatLook) return singleIcon(d);
11943
13265
  const shape = shapeOf(d.properties), rgb = outlineRgb(d);
11944
13266
  if (!symbolOutline || !rgb) return this._buildSingleIcon((this.categoryColorsRgb || [])[d.properties.cat], fillOpacity, shape, rgb, rgb ? singleBorderWidthPx : 0);
11945
13267
  // the outline in the icon's own pixels, inside its edge; quarter
@@ -11952,7 +13274,8 @@ in vec4 vPieLineColor;
11952
13274
  getSize: d => vRadius(sizeValueOf(d)) * 2,
11953
13275
  // group icons bake a fixed 0.9 alpha; an explicit fillopacity scales it
11954
13276
  // so a near-transparent theme doesn't show opaque 1-px-cell clusters
11955
- getColor: d => [255, 255, 255, Math.round(this._iconAlpha(d) * (!singleSymbol(d) && this.style.fillopacity != null ? fillOpacity : 1))],
13277
+ // (a flat-look icon carries its own fill and stroke opacity)
13278
+ getColor: d => [255, 255, 255, Math.round(this._iconAlpha(d) * (!flatLook && !singleSymbol(d) && this.style.fillopacity != null ? fillOpacity : 1))],
11956
13279
  ...(alignOf.active ? { getPixelOffset: alignOf } : {}),
11957
13280
  sizeUnits: 'pixels',
11958
13281
  billboard: true,
@@ -12361,7 +13684,18 @@ in vec4 vPieLineColor;
12361
13684
  // unit.
12362
13685
  // - draw order: flat's sort before drawing (by the size value, the
12363
13686
  // sum or the first value; the biggest on top)
12364
- // Not drawn here (a console note at type parsing, PIE_INERT_FLAGS):
13687
+ // - AGGREGATE / CATEGORICAL (_isAggregatedPie): a pie per item — the
13688
+ // records of a position, or of an aggregation grid cell —
13689
+ // (_pieAggregatedItems; flatPieRecord / flatPieAccumulate /
13690
+ // flatPieItemValues), a slice per category (CATEGORICAL, in its
13691
+ // category color) or per value field; its size the item's summed
13692
+ // size field (flat's nSize, every record of the item counted), the
13693
+ // theme's nMin / nMax / nMaxSize those of the items
13694
+ // (_pieAggregateStats); draw order by the item's size with a size
13695
+ // field or AGGREGATE|SUM; COUNT prints a part's record count;
13696
+ // CENTERVALUE the item's record count
13697
+ // Not drawn here (a console note at type parsing, PIE_INERT_FLAGS,
13698
+ // PIE_AGGREGATE_INERT_FLAGS):
12365
13699
  // 3D, VOLUME, HEIGHT, STARBURST, FLOWER/RAYS, BOW, ZOOM, DIRECTION /
12366
13700
  // DIRECTED, POLAR — the pie is drawn flat and plain instead. Nor
12367
13701
  // style.clipparts, showparts, gridx (flat's several pies per item).
@@ -12375,15 +13709,20 @@ in vec4 vPieLineColor;
12375
13709
  const st = this.style;
12376
13710
  const t = this.flags.typeString != null ? this.flags.typeString : [...this.flags].join('|');
12377
13711
  const has = re => re.test(t);
12378
- const fields = this._pieFields || String(this.binding.value || '').split('|');
12379
- const multi = fields.length > 1;
13712
+ const agg = this._isAggregatedPie();
13713
+ const cat = !!this._pieCategorical;
13714
+ const fields = cat ? null : (this._pieFields || String(this.binding.value || '').split('|'));
13715
+ const multi = !cat && fields.length > 1;
13716
+ // AGGREGATE / CATEGORICAL: the statistics of the items (sets
13717
+ // _valueMin / _valueMax), their biggest size flat's nMaxSize
13718
+ const stats = agg ? this._pieAggregateStats(zoom) : null;
12380
13719
  const unit = objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(st.scale) || 1);
12381
13720
  const sizePow = resolveSizePow(st, this.flags);
12382
13721
  // flat tests /SIZE/: also NOSIZE and GRIDSIZE
12383
13722
  const sizeFlag = has(/SIZE/) && !has(/NORMSIZE/);
12384
13723
  const sizeLog = has(/SIZELOG/);
12385
13724
  const nsv = styleNum(st.normalsizevalue);
12386
- const maxSize = nsv || this._maxSizeValue;
13725
+ const maxSize = nsv || (stats ? stats.maxSize : this._maxSizeValue);
12387
13726
  const range = nsv || ((this._valueMax || 0) - (this._valueMin || 0));
12388
13727
  const nSizeOf = (sum, size) => {
12389
13728
  const n = NORMAL_RADIUS_PX;
@@ -12398,14 +13737,17 @@ in vec4 vPieLineColor;
12398
13737
  const colors = this.categoryColorsRgb || [];
12399
13738
  const scheme = Array.isArray(st.colorscheme) ? st.colorscheme : [];
12400
13739
  const noFill = this._partNoFill || new Set();
12401
- const byClass = has(/CLASSES/) || !multi;
13740
+ const byClass = has(/CLASSES/) || (!cat && !multi);
13741
+ // flat (maptheme.js 20327-20336): a pie of one part (or CLASSES) takes
13742
+ // the color of the class its value falls in — CATEGORICAL: the class
13743
+ // whose number equals the value (sl.single: the item's only part)
12402
13744
  const classColor = v => {
12403
13745
  const parts = this.partsA || [];
12404
- for (let x = 0; x < parts.length; x++) if (v < parts[x].max) return colors[x];
13746
+ for (let x = 0; x < parts.length; x++) if (cat ? v === parts[x].min : v < parts[x].max) return colors[x];
12405
13747
  return null;
12406
13748
  };
12407
13749
  const sliceRgb = sl => (sl.complement ? (parseCssColor(String(st.nodatacolor || '')) || [0xee, 0xee, 0xee])
12408
- : (byClass && classColor(sl.value)) || colors[sl.i] || [128, 128, 128]);
13750
+ : ((byClass || sl.single) && classColor(sl.value)) || colors[sl.i] || [128, 128, 128]);
12409
13751
  const fo = styleNum(st.fillopacity), op = styleNum(st.opacity);
12410
13752
  // flat sets the group's fill-opacity only for a truthy value
12411
13753
  const fillAlpha = op > 0 ? op : fo > 0 ? fo : 1;
@@ -12415,13 +13757,39 @@ in vec4 vPieLineColor;
12415
13757
  if (has(/WHITELINES/)) line = 'white';
12416
13758
  const lc = st.linecolor != null && st.linecolor !== '' ? styleLineColor(st.linecolor) : null;
12417
13759
  if (lc) line = lc;
12418
- if (scheme.slice(0, fields.length).some(c => String(c).trim().toLowerCase() === 'none')) line = lc || 'black';
13760
+ if (!cat && scheme.slice(0, fields.length).some(c => String(c).trim().toLowerCase() === 'none')) line = lc || 'black';
12419
13761
  const lineRgb = line === 'none' ? null : hexOrNamedToRgb(line);
12420
13762
  const lineWidthOf = r => (lineRgb ? (styleNum(st.linewidth) || 0.1) * r / 15 : 0);
12421
13763
  const lineRgba = lineRgb ? [...lineRgb.slice(0, 3), Math.round(255 * lineAlpha)] : [0, 0, 0, 0];
12422
13764
 
12423
13765
  const charts = [];
12424
- for (const f of this._activeFeatures || this.features) {
13766
+ if (agg) {
13767
+ // one pie per item (_pieAggregatedItems), its parts and size as flat
13768
+ // has them after aggregation (flatPieItemValues)
13769
+ for (const f of this._pieAggregatedItems(zoom, bbox)) {
13770
+ const c = f.geometry.coordinates;
13771
+ if (globeCenter && !isOnVisibleHemisphere(c[0], c[1], globeCenter)) continue;
13772
+ const pie = f.properties.pie;
13773
+ const { parts: values, size } = this._pieItemValues(pie);
13774
+ const layout = pieSliceLayout(values, t, { centerPart: st.centerpart, itemCount: pie.n });
13775
+ if (!layout) continue;
13776
+ if (cat) layout.slices.forEach(sl => { sl.single = values.length === 1; });
13777
+ const sum = values.reduce((a, v) => a + (isNaN(v) ? 0 : v), 0);
13778
+ const r = nSizeOf(sum, size) * unit;
13779
+ if (!(r > 0) || !isFinite(r)) continue;
13780
+ // flat's draw-order key (maptheme.js 16812-16860): with a size
13781
+ // field or AGGREGATE|SUM the item's size, else its first part
13782
+ const key = this.binding.size || (has(/AGGREGATE/) && has(/SUM/)) ? size : values[0];
13783
+ // the tooltip: a row per part (counts), its value the parts' sum
13784
+ // (flat: the sum of nValuesA), its records (theme.item.data, the
13785
+ // page's own {{field}}s from the first, as flat's data[0])
13786
+ const raws = f.properties.raws || (f.properties.raw ? [f.properties.raw] : []);
13787
+ const properties = { counts: values, total: sum, recordCounts: [pie.n], raws, firstRaw: raws[0] || null,
13788
+ titleRaw: f.properties.titleRaw || f.properties.raw || null };
13789
+ charts.push({ c, r, layout, size, count: pie.n, partCounts: pie.counts, nParts: values.length, key, properties });
13790
+ }
13791
+ }
13792
+ for (const f of agg ? [] : this._activeFeatures || this.features) {
12425
13793
  const c = f.geometry && f.geometry.coordinates;
12426
13794
  if (!c || typeof c[0] !== 'number') continue;
12427
13795
  const [lng, lat] = c;
@@ -12442,16 +13810,19 @@ in vec4 vPieLineColor;
12442
13810
  // 1-field pie its class (cat) like a range-classed bubble
12443
13811
  const properties = multi ? { value: values[0], parts: values, raw }
12444
13812
  : { value: values[0], cat: this.partsA ? this._resolvePartsClass(values[0]) : 0, raw };
12445
- charts.push({ c, r, layout, size, key, properties });
13813
+ charts.push({ c, r, layout, size, nParts: values.length, key, properties });
12446
13814
  }
12447
13815
  // flat sorts the charts before drawing (maptheme.js 16774-16870),
12448
13816
  // ascending — the last drawn on top —, SORT|UP descending; not with
12449
- // NOSRT, NOSIZE (unless SORT) or when all values are equal
13817
+ // NOSRT, NOSIZE (unless SORT) or when all values are equal, nor a
13818
+ // CATEGORICAL pie without AGGREGATE, size field or SORT
12450
13819
  if (((this._valueMin !== this._valueMax) || this.binding.size) && !has(/NOSRT/)
12451
- && (!has(/NOSIZE/) || has(/\bSORT\b/))) {
13820
+ && (!has(/NOSIZE/) || has(/\bSORT\b/))
13821
+ && (!has(/CATEGORICAL/) || has(/AGGREGATE/) || this.binding.size || has(/\bSORT\b/))) {
12452
13822
  const down = has(/\bSORT\b/) && has(/\bUP\b/);
12453
13823
  charts.sort((a, b) => (down ? b.key - a.key : a.key - b.key) || 0);
12454
13824
  }
13825
+ applyFlatMaxCharts(charts, st);
12455
13826
  // align / offsetx / offsety (flatChartAlignOffset) — flat's PIE
12456
13827
  // ptNull is (0, r + 5 units); the chart's half size taken as r, as
12457
13828
  // for BUBBLE (ASSUMPTION: not measured on flat for PIE)
@@ -12508,6 +13879,74 @@ in vec4 vPieLineColor;
12508
13879
  return layers.concat(this._buildPieTextLayers(charts, zoom, liveZoom, unit, sliceRgb, has));
12509
13880
  }
12510
13881
 
13882
+ // the items of an AGGREGATE / CATEGORICAL pie within bbox: its records
13883
+ // (_buildAggregationIndex) added up per item by aggregateOnGrid — with
13884
+ // AGGREGATE on the theme's aggregation grid (gridwidth, aggregation …,
13885
+ // see _ensureClusterIndices; none: per position), else per position
13886
+ // (flat sums a CATEGORICAL theme's records of one shape by category
13887
+ // without AGGREGATE too). Each item's properties.pie: flatPieAccumulate's.
13888
+ _pieAggregatedItems(zoom, bbox) {
13889
+ if (!this._featuresByCategory) return [];
13890
+ if (this.flags.has('AGGREGATE')) {
13891
+ this._ensureClusterIndices(zoom);
13892
+ return this._clusterIndices[0].getClusters(bbox, this._clusterUsesFixedZoom ? GRIDWIDTH_METERS_REFERENCE_ZOOM : zoom);
13893
+ }
13894
+ const feats = this._featuresByCategory[0];
13895
+ if (!this._pieExactIndex || this._pieExactIndex.features !== feats) {
13896
+ this._pieExactIndex = new GridAggregateIndex(null, this.flags, undefined, null).load(feats);
13897
+ }
13898
+ return this._pieExactIndex.getClusters(bbox, zoom);
13899
+ }
13900
+
13901
+ // an aggregated pie item's parts (a CATEGORICAL pie with style.values:
13902
+ // one per category) and size (flatPieItemValues)
13903
+ _pieItemValues(pie) {
13904
+ const t = this.flags.typeString != null ? this.flags.typeString : [...this.flags].join('|');
13905
+ const nParts = this._pieCategorical ? (this._pieExplicitValues ? this.categoryLabels.length : null) : (this._pieFields || []).length;
13906
+ return flatPieItemValues(pie, t, { categorical: !!this._pieCategorical, nParts });
13907
+ }
13908
+
13909
+ // flat's statistics of an aggregated pie theme (maptheme.js
13910
+ // 11180-11241), over every item (at this zoom when a grid aggregates):
13911
+ // nMin / nMax over the items' sizes (CATEGORICAL or a size field), else
13912
+ // over their parts; nMaxSize the biggest size; the classes of a
13913
+ // value-field pie (partsA) over the items' parts, as for a per-record
13914
+ // pie (_preparePie). Sets the theme's _valueMin / _valueMax / partsA.
13915
+ _pieAggregateStats(zoom) {
13916
+ const feats = this._featuresByCategory && this._featuresByCategory[0];
13917
+ const zkey = this.flags.has('AGGREGATE') ? zoom : null;
13918
+ const cache = this._pieStatsCache;
13919
+ if (cache && cache.feats === feats && cache.zoom === zkey) return cache;
13920
+ let nMin = Infinity, nMax = -Infinity, maxSize = -Infinity;
13921
+ const pooled = [];
13922
+ const bySize = !!this._pieCategorical || !!(this.binding.size && this.binding.size !== '$item$');
13923
+ for (const f of this._pieAggregatedItems(zoom, [-180, -90, 180, 90])) {
13924
+ const { parts, size } = this._pieItemValues(f.properties.pie);
13925
+ if (isFinite(size) && size > maxSize) maxSize = size;
13926
+ for (const v of parts) if (isFinite(v)) pooled.push(v);
13927
+ if (bySize) {
13928
+ if (isFinite(size)) { if (size < nMin) nMin = size; if (size > nMax) nMax = size; }
13929
+ } else {
13930
+ for (const v of parts) if (isFinite(v)) { if (v < nMin) nMin = v; if (v > nMax) nMax = v; }
13931
+ }
13932
+ }
13933
+ if (nMin <= nMax) {
13934
+ this._valueMin = nMin;
13935
+ this._valueMax = nMax;
13936
+ if (!this._pieCategorical && pooled.length) {
13937
+ const nParts = Math.trunc(styleNum(this.style.classes)) || colorSchemeClassCount(this.style.colorscheme) || DEFAULT_RANGE_CLASSES;
13938
+ this.partsA = flatRangeParts(pooled, nMin, nMax, nParts, this.flags, styleNum(this.style.rangecentervalue));
13939
+ // a 1-field pie's legend rows are these classes (in place, as
13940
+ // _ensureAggregateStats: the legend holds the array)
13941
+ if (!this._multiFields && Array.isArray(this.categoryLabels)) {
13942
+ this.categoryLabels.splice(0, this.categoryLabels.length, ...this.partsA.map(p => `${this._formatTooltipValue(p.min)} - ${this._formatTooltipValue(p.max)}`));
13943
+ }
13944
+ }
13945
+ }
13946
+ this._pieStatsCache = { feats, zoom: zkey, maxSize: maxSize > -Infinity ? maxSize : undefined };
13947
+ return this._pieStatsCache;
13948
+ }
13949
+
12511
13950
  // the value texts of _buildPieLayers (see there)
12512
13951
  _buildPieTextLayers(charts, zoom, liveZoom, unit, sliceRgb, has) {
12513
13952
  const st = this.style;
@@ -12518,12 +13957,16 @@ in vec4 vPieLineColor;
12518
13957
  const noBreaks = flatNoBreaks(this._valueMin, this._valueMax);
12519
13958
  const dec = explicitValueDecimals(st);
12520
13959
  const fmt = (v, d) => flatFormatValue(v, d, noBreaks ? 'ROUND|NOBREAKS' : 'ROUND');
12521
- const partText = sl => (sl.complement ? Math.round(sl.percent * 10) / 10 + ' % '
12522
- : fmt(sl.value, dec != null ? dec : (sl.value < 1 ? 2 : 0)) + partUnit);
13960
+ // COUNT on an aggregated pie: the part's record count (flat's nCountA)
13961
+ const countText = has(/COUNT/);
13962
+ const partText = (sl, ch) => {
13963
+ if (sl.complement) return Math.round(sl.percent * 10) / 10 + ' % ';
13964
+ const v = countText && ch && ch.partCounts ? ch.partCounts[sl.i] || 0 : sl.value;
13965
+ return fmt(v, dec != null ? dec : (v < 1 ? 2 : 0)) + partUnit;
13966
+ };
12523
13967
  const valuesOn = has(/VALUES/) && !has(/STACKED/) && !valuesHiddenByScale(st, zoom);
12524
13968
  const textOverride = st.valuecolor || st.textcolor;
12525
13969
  const inline = [], boxed = [], centerTexts = [], lines = [];
12526
- const nFields = (this._pieFields || []).length;
12527
13970
  for (const ch of charts) {
12528
13971
  const o = ch.off;
12529
13972
  // CENTER: the center part's value (maptheme.js 20402-20410)
@@ -12533,10 +13976,10 @@ in vec4 vPieLineColor;
12533
13976
  const fontSize = Math.min(ch.centerR * 0.8, ch.centerR * (3.3 / Math.max(1, text.length))) * valueScale;
12534
13977
  const rgb = (this.categoryColorsRgb || [])[ch.layout.center.i] || [128, 128, 128];
12535
13978
  if (fontSize > VALUES_MIN_FONT_PX) centerTexts.push({ position: ch.c, text, fontSize, off: o, color: flatChartTextRgb(rgb), bold: true });
12536
- } else if (!ch.layout.center && has(/CENTERVALUE/) && ch.size !== undefined && !isNaN(ch.size)) {
13979
+ } else if (!ch.layout.center && has(/CENTERVALUE/) && (ch.count || (ch.size !== undefined && !isNaN(ch.size)))) {
12537
13980
  // flat prints String(nCount || nSize): a record's item has no
12538
- // nCount, so its size value
12539
- const text = String(ch.size);
13981
+ // nCount, so its size value; an aggregated item its record count
13982
+ const text = String(ch.count || ch.size);
12540
13983
  const fontSize = Math.min(ch.r * 0.5, ch.r * (2 / Math.max(1, text.length)));
12541
13984
  if (fontSize > VALUES_MIN_FONT_PX) {
12542
13985
  centerTexts.push({ position: ch.c, text, fontSize, off: o, bold: false,
@@ -12544,20 +13987,20 @@ in vec4 vPieLineColor;
12544
13987
  }
12545
13988
  }
12546
13989
  if (!valuesOn) continue;
12547
- if (nFields > 1 || has(/NOINLINETEXT/)) {
13990
+ if (ch.nParts > 1 || has(/NOINLINETEXT/)) {
12548
13991
  const fontSize = 5 * unit * valueScale;
12549
13992
  if (fontSize <= VALUES_MIN_FONT_PX) continue;
12550
13993
  for (const lab of pieValueLabelLayout(ch.layout.slices, ch.r, fontSize, unit, (this.partsA || []).length)) {
12551
13994
  const bg = has(/VALUEBACKGROUND/) ? sliceRgb(lab.slice) : [255, 255, 255];
12552
13995
  const textRgb = textOverride ? hexOrNamedToRgb(textOverride) : flatDerivateRgb(bg, bg[0] + bg[1] + bg[2] > 450 ? 0.6 : 3);
12553
- boxed.push({ position: ch.c, text: partText(lab.slice), fontSize, anchor: lab.anchor,
13996
+ boxed.push({ position: ch.c, text: partText(lab.slice, ch), fontSize, anchor: lab.anchor,
12554
13997
  // the text's baseline → its center (arial: 0.35 font above)
12555
13998
  off: [o[0] + lab.x, o[1] + lab.y - 0.35 * fontSize], color: textRgb, bg: [...bg.slice(0, 3), 128] });
12556
13999
  lab.segments.forEach(sg => lines.push({ c: ch.c, o, sg }));
12557
14000
  }
12558
14001
  } else if (ch.layout.slices.length) {
12559
14002
  const sl = ch.layout.slices[ch.layout.slices.length - 1];
12560
- const text = partText(sl);
14003
+ const text = partText(sl, ch);
12561
14004
  const fontSize = Math.min(ch.r * 0.8, ch.r * (3.4 / Math.max(1, text.length)));
12562
14005
  const color = textOverride ? hexOrNamedToRgb(textOverride) : flatChartTextRgb(sliceRgb(sl));
12563
14006
  // baseline at −0.6 R + R / 2 + 0.45 font (maptheme.js 20434)
@@ -12611,6 +14054,594 @@ in vec4 vPieLineColor;
12611
14054
  return layers;
12612
14055
  }
12613
14056
 
14057
+ // CHART|BAR / BARS (_isBarChart) — flat's bar chart per item
14058
+ // (flatBarLayout, maptheme.js drawChart's BAR branch): its items and
14059
+ // theme statistics as a pie's (one per record, or AGGREGATE /
14060
+ // CATEGORICAL items via _pieAggregatedItems / _pieAggregateStats), a bar
14061
+ // per value field or category:
14062
+ // - size: SIZE by the size field (or the values' sum) with sizepow, the
14063
+ // width chart size / parts · 0.66, the length value · 30 u / nMax
14064
+ // (normalsizevalue, else the theme's max) · rangescale, with SIZE
14065
+ // and several parts / sizer (SIZEP4: sizer²), COMPRESS / EXPAND,
14066
+ // POINTER · 1.2
14067
+ // - HORZ: the bars to the right, the first on top; else upward side
14068
+ // by side; STACKED: one column; SORT, UP, CENTER, SPACED, THIN /
14069
+ // THICK, NOZERO, NONEGATIVE, ONLYNEGATIVE, LEFT
14070
+ // - colors: the part's (categories, fields), a single part or
14071
+ // SEQUENCE the value's class; outline linecolor (black) at
14072
+ // linewidth · 0.25 (0.2 u) and opacity 1 (0.1 without linewidth);
14073
+ // a negative bar fill 0.3, outlined in its color; fillopacity
14074
+ // - POINTER / ARROW (SMALLARROW drawn as ARROW): flat's pointer path,
14075
+ // negative values pointing the other way (fadenegative fill, the
14076
+ // outline lighter or darker), SIZE: its width by the value;
14077
+ // OFFSETMEAN / OFFSETMEDIAN / DEVIATION: the value's deviation from
14078
+ // the part's mean / median (in %) or in standard deviations
14079
+ // - VALUES: the value (valuedecimals, else 1 decimal with a max ≤ 1)
14080
+ // and units, POINTER a "+", STACKED its share; beyond the bar's end
14081
+ // (in a white box unless HORZ; VALUEBACKGROUND / CTEXT: the bar's
14082
+ // color), inside it when it fits (without SIZE, THIN, DTEXT …),
14083
+ // STACKED beside the column on leader lines; flat's values color
14084
+ // #798697 (textcolor), negative red (valuecolor); DTEXT and a sized
14085
+ // POINTER fade small values
14086
+ // - BOX / TITLE (flatChartBox): a box around chart and title, the
14087
+ // item's title above it — boxcolor (white) at boxopacity, bordercolor
14088
+ // (#bbbbbb) at 0.3, borderwidth · 0.2 · w / 30; title 5 u ·
14089
+ // textscale in textcolor (#888888) over a white halo
14090
+ // - draw order: flat's (_buildPieLayers); each chart's box under its
14091
+ // bars, a later chart over an earlier one
14092
+ // Not drawn (BAR_INERT_FLAGS, a console note): see flatBarLayout.
14093
+ // Known divergence: every value text lies above every bar (flat draws
14094
+ // them in the chart's own group); the leader lines are drawn in map
14095
+ // coordinates (approximate on the globe).
14096
+ _buildBarLayers(zoom, bbox, liveZoom = zoom, globeCenter = null) {
14097
+ const st = this.style;
14098
+ const t = this.flags.typeString != null ? this.flags.typeString : [...this.flags].join('|');
14099
+ const has = re => re.test(t);
14100
+ const agg = this._isAggregatedPie();
14101
+ const cat = !!this._pieCategorical;
14102
+ const fields = cat ? null : (this._pieFields || String(this.binding.value || '').split('|'));
14103
+ const stats = agg ? this._pieAggregateStats(zoom) : null;
14104
+ const unit = objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(st.scale) || 1);
14105
+ const sizeField = !!(this.binding.size && this.binding.size !== '$item$');
14106
+ const nsv = styleNum(st.normalsizevalue);
14107
+ const colors = this.categoryColorsRgb || [];
14108
+ const num = v => { const x = styleNum(v); return isNaN(x) ? undefined : x; };
14109
+ const measure = (text, font) => titleTextWidth(text, font);
14110
+ // the charts' values (and, for OFFSETMEAN …, every item's)
14111
+ const items = [];
14112
+ const itemValues = f => {
14113
+ if (agg) {
14114
+ const { parts, size } = this._pieItemValues(f.properties.pie);
14115
+ return { values: parts, size };
14116
+ }
14117
+ const raw = f.properties || {};
14118
+ return { values: fields.map(k => flatNumber(raw[k], this.flags)), size: sizeField ? parseFloat(raw[this.binding.size]) : undefined };
14119
+ };
14120
+ const all = agg ? this._pieAggregatedItems(zoom, [-180, -90, 180, 90]) : (this._activeFeatures || this.features);
14121
+ // flat's per-part mean, median (getMeanMedianQuantile) and standard
14122
+ // deviation over every item
14123
+ let means, medians, devs;
14124
+ if (has(/OFFSETMEAN|OFFSETMEDIAN|DEVIATION/)) {
14125
+ const cache = this._barStatsCache;
14126
+ if (cache && cache.all === all && (!agg || cache.zoom === zoom)) ({ means, medians, devs } = cache);
14127
+ else {
14128
+ const cols = [];
14129
+ for (const f of all) itemValues(f).values.forEach((v, k) => { (cols[k] ||= []).push(v); });
14130
+ means = cols.map(c => c.reduce((a, v) => a + v, 0) / c.length);
14131
+ medians = cols.map(c => c.slice().sort((a, b) => a - b)[Math.round(c.length / 2)]);
14132
+ devs = cols.map((c, k) => Math.sqrt(c.reduce((a, v) => a + (v - means[k]) * (v - means[k]), 0) / c.length));
14133
+ this._barStatsCache = { all, zoom, means, medians, devs };
14134
+ }
14135
+ }
14136
+ const lo = {
14137
+ themeMin: this._valueMin, themeMax: this._valueMax,
14138
+ maxSize: stats ? stats.maxSize : this._maxSizeValue, sizeField,
14139
+ normalSizeValue: nsv > 0 ? nsv : undefined, sizePow: resolveSizePow(st, this.flags),
14140
+ rangeScale: num(st.rangescale), valueScale: num(st.valuescale), units: st.units ? String(st.units) : '',
14141
+ valueDecimals: explicitValueDecimals(st), noBreaks: flatNoBreaks(this._valueMin, this._valueMax),
14142
+ gridX: num(st.gridx), nClasses: (this.partsA || []).length, classMax: (this.partsA || []).map(p => p.max),
14143
+ nColors: colors.length, fillOpacity: num(st.fillopacity), lineWidth: num(st.linewidth),
14144
+ fadeNegative: num(st.fadenegative), fadeValuePow: num(st.fadevaluepow),
14145
+ textColor: st.textcolor || undefined, valueColor: st.valuecolor || undefined,
14146
+ hideValues: valuesHiddenByScale(st, zoom), means, medians, devs, measure
14147
+ };
14148
+ const inView = c => c[0] >= bbox[0] && c[0] <= bbox[2] && c[1] >= bbox[1] && c[1] <= bbox[3]
14149
+ && (!globeCenter || isOnVisibleHemisphere(c[0], c[1], globeCenter));
14150
+ const charts = [];
14151
+ for (const f of agg ? this._pieAggregatedItems(zoom, bbox) : all) {
14152
+ const c = f.geometry && f.geometry.coordinates;
14153
+ if (!c || typeof c[0] !== 'number' || !inView(c)) continue;
14154
+ const { values, size } = itemValues(f);
14155
+ const layout = flatBarLayout(values, t, Object.assign({ itemSize: size }, lo));
14156
+ if (!layout || (!layout.bars.length && !layout.texts.length)) continue;
14157
+ const sum = values.reduce((a, v) => a + (isFinite(v) ? v : 0), 0);
14158
+ let properties;
14159
+ if (agg) {
14160
+ const pie = f.properties.pie;
14161
+ const raws = f.properties.raws || (f.properties.raw ? [f.properties.raw] : []);
14162
+ properties = { counts: values, total: sum, recordCounts: [pie.n], raws, firstRaw: raws[0] || null,
14163
+ titleRaw: f.properties.titleRaw || f.properties.raw || null };
14164
+ } else {
14165
+ const raw = f.properties || {};
14166
+ properties = values.length > 1 ? { value: values[0], parts: values, raw }
14167
+ : { value: values[0], cat: this.partsA ? this._resolvePartsClass(values[0]) : 0, raw };
14168
+ }
14169
+ // flat's draw-order key (as for pies, maptheme.js 16812-16860)
14170
+ const key = agg ? (this.binding.size || (has(/AGGREGATE/) && has(/SUM/)) ? size : values[0])
14171
+ : (this.binding.size ? size : has(/SIZE/) ? sum : values[0]);
14172
+ charts.push({ c, layout, key, properties });
14173
+ }
14174
+ if (((this._valueMin !== this._valueMax) || this.binding.size) && !has(/NOSRT/)
14175
+ && (!has(/NOSIZE/) || has(/\bSORT\b/))
14176
+ && (!has(/CATEGORICAL/) || has(/AGGREGATE/) || this.binding.size || has(/\bSORT\b/))) {
14177
+ const down = has(/\bSORT\b/) && has(/\bUP\b/);
14178
+ charts.sort((a, b) => (down ? b.key - a.key : a.key - b.key) || 0);
14179
+ }
14180
+ applyFlatMaxCharts(charts, st);
14181
+
14182
+ const boxShown = flatFlag(this.flags, 'BOX') && !boxHiddenByScale(st, liveZoom);
14183
+ const titleShown = boxShown && flatFlag(this.flags, 'TITLE') && !!this.binding.title;
14184
+ const titleFont = 5 * (num(st.textscale) || num(st.valuescale) || 1);
14185
+ const boxOpacity = isNaN(styleNum(st.boxopacity)) ? 1 : styleNum(st.boxopacity);
14186
+ const borderStyle = String(st.borderstyle || '');
14187
+ const boxFill = [...hexOrNamedToRgb(st.boxcolor || 'white').slice(0, 3), Math.round(255 * boxOpacity)];
14188
+ const boxLine = borderStyle === 'none' ? [0, 0, 0, 0]
14189
+ : [...hexOrNamedToRgb(styleLineColor(st.bordercolor) || '#bbbbbb').slice(0, 3), Math.round(255 * (/^(solid|dotted|dashed)$/.test(borderStyle) ? 1 : 0.3))];
14190
+ const borderWidth = num(st.borderwidth) || 1;
14191
+ const lc = st.linecolor != null && st.linecolor !== '' ? styleLineColor(st.linecolor) : null;
14192
+ const lineRgbOf = (spec, rgb) => (spec.rgb === 'color' ? rgb : spec.rgb === 'low' ? flatDerivateRgb(rgb, 0.7)
14193
+ : spec.rgb === 'high' ? flatDerivateRgb(rgb, 1.3) : (lc && lc !== 'none' ? hexOrNamedToRgb(lc) : [0, 0, 0]));
14194
+ const align = !!(st.align || styleNum(st.offsetx) || styleNum(st.offsety));
14195
+
14196
+ const shapes = [], texts = [], titles = [], lines = [];
14197
+ // a shape in chart units → the instance (px; the circle around it)
14198
+ const shape = (ch, s, fill, line, width, props, noPick) => {
14199
+ const L = s.len + s.band + s.head;
14200
+ const d = [[0, -1], [1, 0], [0, 1], [-1, 0]][s.dir];
14201
+ const cx = (s.base[0] + d[0] * L / 2) * unit + ch.off[0], cy = (s.base[1] + d[1] * L / 2) * unit + ch.off[1];
14202
+ const lw = line[3] > 0 ? width * unit : 0;
14203
+ shapes.push({
14204
+ position: ch.c, off: [cx, -cy], properties: props, fill, line,
14205
+ r: Math.hypot(s.w2 * unit, L * unit / 2) + lw + 2,
14206
+ shape: [s.w2 * unit, s.sw * unit, s.len * unit, s.band * unit],
14207
+ shape2: [s.head * unit, s.dir + (noPick ? 4 : 0), lw, L * unit / 2]
14208
+ });
14209
+ };
14210
+ for (const ch of charts) {
14211
+ const L = ch.layout;
14212
+ let box = null;
14213
+ if (boxShown) {
14214
+ const title = titleShown ? this._itemTitle(ch.properties) : '';
14215
+ box = flatChartBox(L.extent, { margin: num(st.boxmargin) || 2, titleFont, title: title ? title + (st.titleunits || '') : '',
14216
+ bottomTitle: flatFlag(this.flags, 'BOTTOMTITLE'), alignRight: /right/.test(String(st.align || '')), measure });
14217
+ }
14218
+ const ext = box ? box.rect : L.extent;
14219
+ // align / offsetx / offsety (flatChartAlignOffset), the chart's
14220
+ // half height as its size (ASSUMPTION: not measured on flat for BAR)
14221
+ ch.off = align ? flatChartAlignOffset(st, { unit, symbolScale: 1, half: (ext[3] - ext[1]) / 2 * unit }) : [0, 0];
14222
+ if (box) {
14223
+ const r = box.rect;
14224
+ shape(ch, { base: [(r[0] + r[2]) / 2, r[3]], dir: 0, len: r[3] - r[1], w2: (r[2] - r[0]) / 2, sw: (r[2] - r[0]) / 2, band: 0, head: 0 },
14225
+ boxFill, boxLine, borderWidth * 0.2 * box.width / 30, null, true);
14226
+ if (box.title) titles.push({ position: ch.c, text: box.title.lines.join('\n'), font: box.title.font * unit, anchor: box.title.anchor,
14227
+ off: [box.title.x * unit + ch.off[0], (box.title.baseline - 0.8 * box.title.font) * unit + ch.off[1]] });
14228
+ }
14229
+ for (const b of L.bars) {
14230
+ const rgb = colors[b.cls] || [128, 128, 128];
14231
+ const a = Math.round(255 * b.fill.opacity * b.opacity);
14232
+ const line = [...lineRgbOf(b.line, rgb).slice(0, 3), Math.round(255 * b.line.opacity * b.opacity)];
14233
+ shape(ch, b, [...rgb.slice(0, 3), a], line, b.line.width, ch.properties, false);
14234
+ }
14235
+ for (const tx of L.texts) {
14236
+ const font = tx.font * unit;
14237
+ if (font <= VALUES_MIN_FONT_PX) continue;
14238
+ const rgb = colors[tx.cls] || [128, 128, 128];
14239
+ const color = tx.color === 'value' ? [0x79, 0x86, 0x97] : tx.color === 'contrast' ? flatChartTextRgb(rgb) : hexOrNamedToRgb(tx.color);
14240
+ // the baseline's start → the text's middle-left (arial: 0.35 font
14241
+ // above the baseline), perpendicular to its reading direction
14242
+ const r = tx.angle * Math.PI / 180;
14243
+ const mx = tx.x * unit - 0.35 * font * Math.sin(r), my = tx.y * unit - 0.35 * font * Math.cos(r);
14244
+ texts.push({ position: ch.c, text: tx.text, font, angle: tx.angle, off: [mx + ch.off[0], my + ch.off[1]],
14245
+ color: [...color.slice(0, 3), Math.round(255 * Math.max(0, Math.min(1, tx.opacity)))],
14246
+ bg: tx.bg === 'bar' ? [...rgb.slice(0, 3), 128] : tx.bg === 'white' ? [254, 254, 255, 255] : null });
14247
+ }
14248
+ for (const l of L.lines) lines.push({ c: ch.c, o: ch.off, sg: [l.x0 * unit, l.y0 * unit, l.x1 * unit, l.y1 * unit] });
14249
+ }
14250
+
14251
+ const layers = [new BarShapeLayer({
14252
+ id: `ix-bar-${this.name}`,
14253
+ data: shapes, pickable: true,
14254
+ getPosition: d => d.position,
14255
+ getRadius: d => d.r,
14256
+ getPixelOffset: d => d.off,
14257
+ getFillColor: d => d.fill,
14258
+ getLineColor: d => d.line,
14259
+ getBarShape: d => d.shape,
14260
+ getBarShape2: d => d.shape2,
14261
+ radiusUnits: 'pixels', billboard: true, stroked: false, filled: true, antialiasing: true,
14262
+ parameters: ICON_LAYER_GLOBE_PARAMETERS
14263
+ })];
14264
+ if (lines.length) {
14265
+ const feature = d => ({ type: 'Feature', properties: {}, geometry: { type: 'LineString', coordinates: [
14266
+ pixelOffsetLngLat(d.c, [d.o[0] + d.sg[0], d.o[1] + d.sg[1]], liveZoom),
14267
+ pixelOffsetLngLat(d.c, [d.o[0] + d.sg[2], d.o[1] + d.sg[3]], liveZoom)] } });
14268
+ const fc = { type: 'FeatureCollection', features: lines.map(feature) };
14269
+ [['bg', [255, 255, 255, Math.round(255 * 0.2)], 0.15], ['fg', [0x88, 0x88, 0x88, 255], 0.05]].forEach(([k, color, w]) => {
14270
+ layers.push(new GeoJsonLayer({
14271
+ id: `ix-bar-lines-${k}-${this.name}`, data: fc, pickable: false,
14272
+ stroked: true, filled: false, getLineColor: color, getLineWidth: w * unit, lineWidthUnits: 'pixels'
14273
+ }));
14274
+ });
14275
+ }
14276
+ const textBase = {
14277
+ getPosition: d => d.position, getText: d => d.text, getSize: d => d.font,
14278
+ getPixelOffset: d => d.off, getColor: d => d.color, getAngle: d => d.angle,
14279
+ sizeUnits: 'pixels', fontFamily: 'arial', characterSet: 'auto', fontWeight: 'normal',
14280
+ getTextAnchor: 'start', getAlignmentBaseline: 'center', pickable: false,
14281
+ parameters: ICON_LAYER_GLOBE_PARAMETERS
14282
+ };
14283
+ const boxed = texts.filter(d => d.bg), plain = texts.filter(d => !d.bg);
14284
+ if (plain.length) layers.push(new TextLayer(Object.assign({}, textBase, { id: `ix-bar-values-${this.name}`, data: plain })));
14285
+ if (boxed.length) {
14286
+ // flat's box (createTextLabel): 0.3 font left of the text, 0.4 font
14287
+ // right of it, 0.1 font taller
14288
+ layers.push(new TextLayer(Object.assign({}, textBase, {
14289
+ id: `ix-bar-values-boxed-${this.name}`, data: boxed,
14290
+ background: true, getBackgroundColor: d => d.bg,
14291
+ // one padding per layer: of the texts' mean font
14292
+ backgroundPadding: [0.3, 0, 0.4, 0.1].map(k => k * boxed.reduce((a, d) => a + d.font, 0) / boxed.length)
14293
+ })));
14294
+ }
14295
+ if (titles.length) {
14296
+ layers.push(new TextLayer({
14297
+ id: `ix-bar-titles-${this.name}`, data: titles, pickable: false,
14298
+ getPosition: d => d.position, getText: d => d.text, getSize: d => d.font,
14299
+ sizeUnits: 'pixels', fontFamily: 'arial', characterSet: 'auto', lineHeight: 1,
14300
+ getColor: [...hexOrNamedToRgb(st.textcolor || '#888888').slice(0, 3), 255],
14301
+ fontSettings: { sdf: true }, outlineWidth: 1 / 7, outlineColor: [255, 255, 255, 128],
14302
+ getTextAnchor: d => d.anchor, getAlignmentBaseline: 'top',
14303
+ getPixelOffset: d => d.off,
14304
+ parameters: ICON_LAYER_GLOBE_PARAMETERS
14305
+ }));
14306
+ }
14307
+ return layers;
14308
+ }
14309
+
14310
+ // CHART|VECTOR / CHART|BEZIER — the theme's classes as flat has them:
14311
+ // with CATEGORICAL (or EXACT) the categories, flat's string values
14312
+ // trimmed (getStringValueIndex, maptheme.js 9204), its partsA one
14313
+ // [k, k + 1e-9] per category (the last + 0.001) and the colorscheme's
14314
+ // colors per category; the items' statistics follow in _vectorItems
14315
+ _prepareVector() {
14316
+ const st = this.style;
14317
+ this._vectorCategorical = this.flags.has('CATEGORICAL') && !!this.binding.value;
14318
+ if (this._vectorCategorical) {
14319
+ const explicit = Array.isArray(st.values) ? st.values.map(v => String(v).trim()) : null;
14320
+ const index = new Map();
14321
+ if (explicit) explicit.forEach((v, i) => { if (!index.has(v)) index.set(v, i); });
14322
+ else {
14323
+ for (const f of this.features) {
14324
+ const v = f.properties[this.binding.value];
14325
+ if (v == null) continue;
14326
+ const k = String(v).trim();
14327
+ if (k !== '' && !index.has(k)) index.set(k, index.size);
14328
+ }
14329
+ this.categoryLabels = Array.from(index.keys());
14330
+ const explicitLabel = Array.isArray(st.label) ? st.label.map(String) : null;
14331
+ this.categoryDisplayLabels = explicitLabel && explicitLabel.length === this.categoryLabels.length ? explicitLabel : this.categoryLabels;
14332
+ this.categoryColorsRgb = resolveClassColors(st.colorscheme, this.categoryDisplayLabels, st.classes);
14333
+ }
14334
+ this._vectorCategoryIndex = index;
14335
+ // the legend counts records by their raw value
14336
+ const byLabel = new Map(index);
14337
+ for (const f of this.features) {
14338
+ const v = f.properties[this.binding.value];
14339
+ if (v != null && !byLabel.has(v) && index.has(String(v).trim())) byLabel.set(v, index.get(String(v).trim()));
14340
+ }
14341
+ this.categoryIndexByLabel = byLabel;
14342
+ const n = this.categoryLabels.length;
14343
+ this.partsA = this.categoryLabels.map((l, i) => ({ min: i + 1, max: i + 1 + (i === n - 1 ? 0.001 : 0) + 1e-9 }));
14344
+ this._rangeClassed = false;
14345
+ }
14346
+ // colorfield (flat 12529-12597, 12644-12652): an item's color is its
14347
+ // color value's — the colorvalues, else values, else the items'
14348
+ // values in order — n-th color of the colorscheme realized for n
14349
+ const colorField = st.colorfield || (this.binding && this.binding.color);
14350
+ this._vectorColorField = colorField || null;
14351
+ if (colorField) {
14352
+ const list = Array.isArray(st.colorvalues) ? st.colorvalues.map(String)
14353
+ : Array.isArray(st.values) ? st.values.map(String)
14354
+ : [...new Set(this.features.map(f => String(f.properties[colorField])))];
14355
+ const colors = resolveClassColors(st.colorscheme, list, st.classes) || [];
14356
+ this._vectorColorOf = new Map(list.map((v, i) => [v, colors[i]]));
14357
+ }
14358
+ this._vectorItemsOf = null;
14359
+ }
14360
+
14361
+ // the records of flatVectorItems from this theme's (active) features:
14362
+ // the second position joined by key (joinChartPositions: f._pos2) or
14363
+ // read from lat/lon fields ("lat|lon")
14364
+ _vectorRecords(features) {
14365
+ const b = this.binding;
14366
+ const l2 = b.lookup2 != null ? String(b.lookup2) : '';
14367
+ const latlon2 = l2.includes('|') ? l2.split('|') : null;
14368
+ const sizeField = b.size && b.size !== '$item$' ? b.size : null;
14369
+ const records = [];
14370
+ for (const f of features) {
14371
+ const c = f.geometry && f.geometry.coordinates;
14372
+ if (!c || typeof c[0] !== 'number') continue;
14373
+ const p = f.properties || {};
14374
+ let p2 = null;
14375
+ if (latlon2) {
14376
+ const lat = flatScanValue(p[latlon2[0]]), lng = flatScanValue(p[latlon2[1]]);
14377
+ if (isFinite(lat) && isFinite(lng)) p2 = [lng, lat];
14378
+ } else if (f._pos2) p2 = f._pos2;
14379
+ let cat = null, value;
14380
+ if (this._vectorCategorical) {
14381
+ const v = p[b.value];
14382
+ const i = v == null ? undefined : this._vectorCategoryIndex.get(String(v).trim());
14383
+ cat = i == null ? -1 : i;
14384
+ } else if (b.value) {
14385
+ value = flatScanValue(p[b.value]);
14386
+ }
14387
+ records.push({ p1: c, p2, cat, value, size: sizeField ? flatScanValue(p[sizeField]) : 1, raw: p });
14388
+ }
14389
+ return records;
14390
+ }
14391
+
14392
+ // the items (flatVectorItems) of the active features, once per feature
14393
+ // set; also flat's statistics of them (maptheme.js 11180-11240): nMin /
14394
+ // nMax over the items' sizes (CATEGORICAL or a size field, else their
14395
+ // values), nMaxSize the biggest size
14396
+ _vectorItems() {
14397
+ const features = this._activeFeatures || this.features;
14398
+ if (this._vectorItemsOf === features) return this._vectorItemsCache;
14399
+ const t = this.flags.typeString != null ? this.flags.typeString : [...this.flags].join('|');
14400
+ const items = flatVectorItems(this._vectorRecords(features), {
14401
+ aggregate: this.flags.has('AGGREGATE'),
14402
+ vectorKey: /VECTOR/.test(t),
14403
+ multiParts: this.flags.has('CATEGORICAL'),
14404
+ max: /\bMAX\b/.test(t), min: /\bMIN\b/.test(t),
14405
+ zeroIsNotValue: /ZEROISNOTVALUE/.test(t), negativeIsNotValue: /NEGATIVEISNOTVALUE/.test(t)
14406
+ });
14407
+ let min = Infinity, max = -Infinity, maxSize = -Infinity;
14408
+ const bySize = this._vectorCategorical || !!this.binding.size;
14409
+ for (const it of items) {
14410
+ if (isFinite(it.nSize) && it.nSize > maxSize) maxSize = it.nSize;
14411
+ const v = bySize ? it.nSize : it.value;
14412
+ if (isFinite(v)) { if (v < min) min = v; if (v > max) max = v; }
14413
+ }
14414
+ if (min <= max) { this._valueMin = min; this._valueMax = max; }
14415
+ this._vectorMaxSize = maxSize;
14416
+ this._vectorItemsOf = features;
14417
+ this._vectorItemsCache = items;
14418
+ return items;
14419
+ }
14420
+
14421
+ // CHART|VECTOR / CHART|BEZIER — real ixmaps-flat's flow lines
14422
+ // (maptheme.js chartMap 18148-18435): one line per item
14423
+ // (_vectorItems) from its first to its second position — the
14424
+ // positions joined to FEATURE shapes by key (binding position /
14425
+ // position2, style lookupfield / lookupfield2) or read from lat/lon
14426
+ // fields. An item without a second position, or with a size under
14427
+ // style.minvalue, draws nothing; one is drawn when either end lies in
14428
+ // the view (flat 16590-16620).
14429
+ // - BEZIER (also VECTOR|BEZIER — the BEZIER branch comes first): a
14430
+ // cubic curve (bezierVectorLayout), width (linewidth, else 10 ·
14431
+ // (|nSize| / normalsizevalue or the biggest size)^(1/sizepow)) ·
14432
+ // unit px — unit: the object zoom × style.scale; a width ≤ 0 is not
14433
+ // drawn; stroke opacity fillopacity (else 0.3); a negative size or
14434
+ // REVERSE swaps the ends; RANDOM lowers the bow by up to 0.66 ·
14435
+ // rangescale (flat's Math.random, here a fixed hash of the item);
14436
+ // POINTER / ARROW an arrow head (arrowMarkerTriangle) at the
14437
+ // shortened end, its opacity 2 · fillopacity; GRADIENT / FADEIN a
14438
+ // gradient (fadeGradientStops) from the second color (linecolor's
14439
+ // first entry) at the start — FADEIN at 0.2 opacity — to the line
14440
+ // color at the end; DOPACITY (DOPACITYMAX) / DOPACITYMIN the line
14441
+ // and arrow opacity by the size within nMin … nMax (dopacitypow,
14442
+ // dopacityscale); DASH dashes of 50 / 5 + width units, round caps
14443
+ // - VECTOR alone: a straight line, width (linewidth, else 2 · nSize /
14444
+ // normalsizevalue / the biggest size, else 0.1) · the object zoom px
14445
+ // (no style.scale), opacity fillopacity (0.3); POINTER / ARROW a 4 ×
14446
+ // width arrow head with its tip on the second position; DASH dashes
14447
+ // of 50 / 15 units
14448
+ // - color: linecolor (its last entry), else the colorfield's color,
14449
+ // else the category's (CATEGORICAL) or the value's n-th color; a
14450
+ // record whose category is none of them is black (flat: an
14451
+ // "undefined" stroke — szNoDataColor is unset)
14452
+ // - draw order: flat's sort before drawing (16774-16880), the
14453
+ // biggest last (on top) when sized or AGGREGATE|SUM
14454
+ // - lines are densified in Web Mercator pixels (flat draws them on its
14455
+ // Mercator map), so under the globe projection they follow the
14456
+ // surface; their far-side parts are left out
14457
+ // Not drawn here (console note at type parsing, VECTOR_INERT_FLAGS):
14458
+ // VALUES (text along the curve), DYNAMICWIDTH, LONG, the DASH
14459
+ // animation; nor the second position from the category label (flat's
14460
+ // fallback without position2), a grid aggregation (gridwidth), the
14461
+ // VECTOR arrow's thin white outline, the legend's class marking.
14462
+ // Known divergence: all arrow heads lie above all lines (flat draws
14463
+ // each line with its head); a line thinner than a pixel is drawn one
14464
+ // pixel wide at its width as opacity (SVG's coverage).
14465
+ _buildVectorLayers(zoom, bbox, liveZoom = zoom, globeCenter = null) {
14466
+ const st = this.style;
14467
+ const t = this.flags.typeString != null ? this.flags.typeString : [...this.flags].join('|');
14468
+ const has = re => re.test(t);
14469
+ const bezier = has(/\bBEZIER\b/);
14470
+ if (!this.binding.lookup2) {
14471
+ if (!this._warnedNoPos2) {
14472
+ this._warnedNoPos2 = true;
14473
+ console.info(`[ixmaps-gl] layer "${this.name}": a VECTOR/BEZIER chart without position2 (lookupfield2) — flat's second position from the category label is not ported, nothing drawn`);
14474
+ }
14475
+ return [];
14476
+ }
14477
+ const items = this._vectorItems();
14478
+ const oz = objectZoomFactor(liveZoom, this.mapOptions);
14479
+ const scale = styleNum(st.scale) || 1;
14480
+ const unit = oz * scale;
14481
+ const world = 512 * Math.pow(2, liveZoom);
14482
+ const toPx = ([lng, lat]) => {
14483
+ const s = Math.sin(Math.max(-85.0511, Math.min(85.0511, lat)) * Math.PI / 180);
14484
+ return [(lng + 180) / 360 * world, (0.5 - Math.log((1 + s) / (1 - s)) / (4 * Math.PI)) * world];
14485
+ };
14486
+ const toLngLat = ([x, y]) => [x / world * 360 - 180, (2 * Math.atan(Math.exp((0.5 - y / world) * 2 * Math.PI)) - Math.PI / 2) * 180 / Math.PI];
14487
+ const inBox = c => c && c[0] >= bbox[0] && c[0] <= bbox[2] && c[1] >= bbox[1] && c[1] <= bbox[3];
14488
+
14489
+ const sizePow = resolveSizePow(st, this.flags);
14490
+ const nsv = styleNum(st.normalsizevalue);
14491
+ const maxSize = this._vectorMaxSize;
14492
+ // style.linewidth as flat holds it: the raw style value — flat's
14493
+ // `nLineWidth + 3` (the arrow length) joins a string ("0.6" + 3 =
14494
+ // "0.63") but adds a number
14495
+ const rawLw = this._definition && this._definition.style ? this._definition.style.linewidth : st.linewidth;
14496
+ const lwNum = styleNum(st.linewidth);
14497
+ const markerSize = styleNum(st.markersize) || 1;
14498
+ const gapSize = styleNum(st.gapsize) || 20;
14499
+ const fo = styleNum(st.fillopacity);
14500
+ const groupAlpha = styleNum(st.opacity) > 0 ? Math.min(1, styleNum(st.opacity)) : 1;
14501
+ const minValue = styleNum(st.minvalue);
14502
+ const rs = styleNum(st.rangescale);
14503
+ const dopMin = has(/DOPACITYMIN/), dop = !dopMin && has(/DOPACITY/);
14504
+ const dopPow = styleNum(st.dopacitypow) || 1, dopScale = styleNum(st.dopacityscale) || 1;
14505
+ const nMin = this._valueMin, nMax = this._valueMax;
14506
+ const lineColors = st.linecolor != null && st.linecolor !== '' ? flatToArray(st.linecolor) : null;
14507
+ const lineRgb = lineColors ? hexOrNamedToRgb(String(lineColors[lineColors.length - 1]).trim()) : null;
14508
+ const colors = this.categoryColorsRgb || [];
14509
+ const colorOf = it => {
14510
+ if (lineRgb) return lineRgb;
14511
+ if (this._vectorColorField) {
14512
+ const c = this._vectorColorOf.get(String(it.raw[this._vectorColorField]));
14513
+ if (c) return c;
14514
+ }
14515
+ if (it.cat != null) return it.cat >= 0 && colors[it.cat] ? colors[it.cat] : [0, 0, 0];
14516
+ return Number.isInteger(it.value) && colors[it.value - 1] ? colors[it.value - 1] : [0, 0, 0];
14517
+ };
14518
+ const fade = has(/\bFADEIN\b/) ? 0.2 : 2;
14519
+ const gradient = bezier && has(/\bGRADIENT\b|\bFADEIN\b/);
14520
+ const arrow = has(/\bPOINTER|ARROW\b/);
14521
+ const dash = has(/\bDASH\b/);
14522
+ const dopacity = it => {
14523
+ const range = Math.pow(nMax - nMin, 1 / dopPow);
14524
+ return Math.pow(dopMin ? nMax - it.nSize : it.nSize - nMin, 1 / dopPow) / range * (fo || 1) * dopScale;
14525
+ };
14526
+
14527
+ // flat's sort before drawing (maptheme.js 16774-16880)
14528
+ let order = items;
14529
+ const sized = !!this.binding.size;
14530
+ if (((nMin !== nMax) || sized) && !has(/NOSRT/) && (!has(/NOSIZE/) || has(/\bSORT\b/))
14531
+ && (!this.flags.has('CATEGORICAL') || this.flags.has('AGGREGATE') || sized || has(/\bSORT\b/))) {
14532
+ const key = (sized || (this.flags.has('AGGREGATE') && has(/SUM/))) ? (it => it.nSize)
14533
+ : (this._vectorCategorical ? (it => (it.cat != null && it.cat >= 0 ? it.cat + 1 : 0)) : (it => it.value));
14534
+ const keyed = items.map(it => ({ it, y: key(it) })).filter(d => !isNaN(d.y));
14535
+ const down = has(/\bSORT\b/) && has(/\bUP\b/);
14536
+ keyed.sort((a, b) => (down ? b.y - a.y : a.y - b.y));
14537
+ order = keyed.map(d => d.it);
14538
+ }
14539
+ // the flows that would be drawn (as the loop below draws them), of
14540
+ // which flat keeps the biggest maxcharts
14541
+ if (Math.round(styleNum(st.maxcharts)) > 0) {
14542
+ order = applyFlatMaxCharts(order.filter(it => it.p2 && !(minValue && it.nSize < minValue) && (inBox(it.p1) || inBox(it.p2))), st);
14543
+ }
14544
+
14545
+ const paths = [], heads = [];
14546
+ const N = 32;
14547
+ const visible = c => !globeCenter || isOnVisibleHemisphere(c[0], c[1], globeCenter);
14548
+ for (const it of order) {
14549
+ let p1 = it.p1, p2 = it.p2;
14550
+ if (!p2) continue;
14551
+ if (minValue && it.nSize < minValue) continue;
14552
+ if (!inBox(p1) && !inBox(p2)) continue;
14553
+ const rgb = colorOf(it);
14554
+ let pts, end, tan, sw, headS, refX, refY, headAlpha, lineAlpha, stops = null, dashArr = null;
14555
+ if (bezier) {
14556
+ if (p1[0] === p2[0] && p1[1] === p2[1]) continue;
14557
+ const w = rawLw != null && rawLw !== '' && lwNum ? lwNum
14558
+ : 10 / Math.pow(nsv || maxSize, 1 / sizePow) * Math.pow(Math.abs(it.nSize), 1 / sizePow);
14559
+ sw = w * unit;
14560
+ if (!(sw > 0)) continue;
14561
+ if (has(/\bREVERSE\b/) || it.nSize < 0) [p1, p2] = [p2, p1];
14562
+ const a = toPx(p1), b = toPx(p2);
14563
+ const dx = b[0] - a[0], dy = b[1] - a[1];
14564
+ if (!(dx * dx + dy * dy > 0)) continue;
14565
+ let bow = rs;
14566
+ if (has(/\bRANDOM\b/)) bow -= rs * 0.66 * hashUnit(p1[0] + ',' + p1[1] + '&' + p2[0] + ',' + p2[1]);
14567
+ const gapAdd = has(/\bGAP\b/) ? gapSize : 3;
14568
+ const ll = typeof rawLw === 'string' && lwNum ? Number(rawLw + String(gapAdd)) : w + gapAdd;
14569
+ const lay = bezierVectorLayout(dx, dy, { w, ll, unit, oz, bow, t, marker: markerSize, gap: gapSize });
14570
+ pts = cubicBezierPoints(lay.start, lay.c1, lay.c2, lay.end, N);
14571
+ end = lay.end;
14572
+ tan = [lay.end[0] - lay.c2[0], lay.end[1] - lay.c2[1]];
14573
+ headS = Math.min(5, 2.5 + 5 / (w * scale)) * markerSize;
14574
+ refX = headS / 1.7; refY = headS / 2;
14575
+ lineAlpha = fo || 0.3;
14576
+ headAlpha = Math.min(1, (fo || 0.3) * 2);
14577
+ // (flat sets the computed opacity as is; one that is no number
14578
+ // — every size equal — SVG ignores: the opacity stays)
14579
+ if (dop || dopMin) { const d = dopacity(it); if (isFinite(d)) { lineAlpha = d; headAlpha = d; } }
14580
+ if (gradient) stops = fadeGradientStops(pts, lay.vx, lay.vy, fade);
14581
+ if (dash) dashArr = [50 * oz, 5 * oz + sw];
14582
+ pts = pts.map(p => [p[0] + a[0], p[1] + a[1]]);
14583
+ end = [end[0] + a[0], end[1] + a[1]];
14584
+ } else {
14585
+ const w = (rawLw != null && rawLw !== '' && lwNum) || (2 / (nsv || 1) / maxSize * it.nSize) || 0.1;
14586
+ sw = w * oz;
14587
+ const a = toPx(p1), b = toPx(p2);
14588
+ pts = [];
14589
+ for (let i = 0; i <= N; i++) pts.push([a[0] + (b[0] - a[0]) * i / N, a[1] + (b[1] - a[1]) * i / N]);
14590
+ end = b;
14591
+ tan = [b[0] - a[0], b[1] - a[1]];
14592
+ headS = 4; refX = 4; refY = 2;
14593
+ lineAlpha = fo || 0.3;
14594
+ headAlpha = Math.min(1, (fo || 0.3) * 2);
14595
+ if (dash) dashArr = [50 * oz, 15 * oz];
14596
+ }
14597
+ // a line under a pixel: one pixel at its width as opacity
14598
+ const drawW = Math.max(1, sw);
14599
+ const thin = Math.min(1, sw);
14600
+ const color2 = lineColors && lineColors.length > 1 ? hexOrNamedToRgb(String(lineColors[0]).trim()) : rgb;
14601
+ const vertexColor = i => {
14602
+ const s = stops ? stops[i] : null;
14603
+ const k = s ? s.k : 1;
14604
+ const c = s ? [0, 1, 2].map(j => Math.round(color2[j] + (rgb[j] - color2[j]) * k)) : rgb.slice(0, 3);
14605
+ return [...c, Math.round(255 * Math.min(1, lineAlpha) * (s ? s.alpha : 1) * thin * groupAlpha)];
14606
+ };
14607
+ const properties = { value: it.nSize, cat: it.cat, raw: it.raw, count: it.count,
14608
+ vector: { itemValue: it.cat != null ? (it.cat >= 0 ? it.cat + 1 : 0) : it.value } };
14609
+ // the line in map coordinates; on the globe only its visible runs
14610
+ const ll = pts.map(toLngLat);
14611
+ let run = [], runColors = [];
14612
+ const flush = () => {
14613
+ if (run.length > 1) paths.push({ path: run, colors: runColors, width: drawW, dash: dashArr ? [dashArr[0] / drawW, dashArr[1] / drawW] : [0, 0], properties, item: it });
14614
+ run = []; runColors = [];
14615
+ };
14616
+ ll.forEach((c, i) => { if (visible(c)) { run.push(c); runColors.push(vertexColor(i)); } else flush(); });
14617
+ flush();
14618
+ if (arrow && visible(toLngLat(end))) {
14619
+ const tri = arrowMarkerTriangle(end, tan[0], tan[1], sw, headS, refX, refY).map(toLngLat);
14620
+ heads.push({ type: 'Feature', properties, geometry: { type: 'Polygon', coordinates: [[...tri, tri[0]]] },
14621
+ fill: [...rgb.slice(0, 3), Math.round(255 * Math.min(1, headAlpha) * groupAlpha)] });
14622
+ }
14623
+ }
14624
+ const layers = [new PathLayer({
14625
+ id: `ix-vector-${this.name}`, data: paths, pickable: true,
14626
+ getPath: d => d.path, getColor: d => d.colors, getWidth: d => d.width,
14627
+ widthUnits: 'pixels', capRounded: dash && bezier, jointRounded: false, billboard: false,
14628
+ parameters: ICON_LAYER_GLOBE_PARAMETERS,
14629
+ ...(dash ? { getDashArray: d => d.dash, extensions: [new PathStyleExtension({ dash: true })] } : {})
14630
+ })];
14631
+ if (heads.length) {
14632
+ // the head count in the id: deck kept a same-id polygon layer's
14633
+ // larger buffers when a filter cut the heads (the Zeit page's "to
14634
+ // Africa": bow-tie triangles from the unfiltered heads' vertices).
14635
+ // Every head has the same vertex count, so an equal count — a pan —
14636
+ // reuses the layer; a changed one gets fresh buffers
14637
+ layers.push(new GeoJsonLayer({
14638
+ id: `ix-vector-heads-${this.name}-n${heads.length}`, data: { type: 'FeatureCollection', features: heads }, pickable: true,
14639
+ filled: true, stroked: false, getFillColor: d => d.fill, parameters: ICON_LAYER_GLOBE_PARAMETERS
14640
+ }));
14641
+ }
14642
+ return layers;
14643
+ }
14644
+
12614
14645
  // flat's chart position for this build (flatChartAlignOffset): a
12615
14646
  // function item → pixel offset, the same for every part of one chart
12616
14647
  // (symbol, glow, shadow, box, title, value text). unit is flat's
@@ -13755,11 +15786,12 @@ in vec4 vPieLineColor;
13755
15786
  const map = _lastMapApi.map;
13756
15787
  const center = map.getCenter();
13757
15788
  const proj = (typeof map.getProjection === 'function' && map.getProjection()) || { type: 'mercator' };
15789
+ const svgProjection = mapSvgProjection(map);
13758
15790
  return JSON.stringify({
13759
15791
  map: {
13760
- map: proj.type === 'globe' ? 'maps/svg/maps/generic/orthographic.svg' : 'maps/svg/maps/generic/mercator.svg',
15792
+ map: proj.type === 'globe' ? 'maps/svg/maps/generic/orthographic.svg' : svgProjection ? svgProjection.svg : 'maps/svg/maps/generic/mercator.svg',
13761
15793
  center: { lat: center.lat, lng: center.lng },
13762
- zoom: mapLibreToFlatZoom(map.getZoom())
15794
+ zoom: flatViewZoomOf(map)
13763
15795
  }
13764
15796
  });
13765
15797
  }
@@ -13830,6 +15862,13 @@ in vec4 vPieLineColor;
13830
15862
  htmlgui_getAttributionString: () => (_lastMapApi && _lastMapApi.getAttribution ? _lastMapApi.getAttribution() : ''),
13831
15863
  changeThemeStyle: compatChangeThemeStyle,
13832
15864
  removeTheme: compatRemoveTheme,
15865
+ // flat's ixmaps.setLocalString(orig, local) (htmlgui.js 3814 →
15866
+ // Api.setLocalString → Dictionary.replace): a page-wide translation,
15867
+ // e.g. the migration page's ISO code → country name; usable before
15868
+ // the map exists. getLocalString: its translation (flat's never
15869
+ // returns it — a missing return there)
15870
+ setLocalString: (szOrig, szLocal) => { _localStrings[szOrig] = szLocal; },
15871
+ getLocalString: (szOrig) => makeLocalText([])(szOrig),
13833
15872
  // flat's ixmaps.newTheme(title, theme, flag) — on the last built map
13834
15873
  newTheme: (title, theme, flag) => _mapHandle.newTheme(title, theme, flag),
13835
15874
  // flat's time slider: show the records of a theme (null = all themes) whose timefield lies in [min, max)
@@ -13858,16 +15897,16 @@ in vec4 vPieLineColor;
13858
15897
  // test-only: lets test/unit/*.test.mjs call pure internals directly (the
13859
15898
  // engine runs in a Node vm there); deliberately NOT on the ixmaps object
13860
15899
  global.__ixmapsGlInternals = {
13861
- IXMAPS_GL_VERSION, youtubeClickToPlay, symbolsFlagCompat, tooltipTable, legendRowLabels, scaleDenom, cssColorAlpha, aggregatedCategoricalClass, dominantDopacityAlpha, isAggregatedCategoricalChoropleth, computeBubblePackLayout, isolatedBubblePackLayout, chartTextTransmittance, insideSymbolShape, sequenceLayout, ringsLayout, pieSliceLayout, pieValueLabelLayout, multiQuadOffsets, pixelOffsetLngLat,
15900
+ IXMAPS_GL_VERSION, applyFlatMaxCharts, youtubeClickToPlay, symbolsFlagCompat, tooltipTable, legendRowLabels, scaleDenom, cssColorAlpha, aggregatedCategoricalClass, dominantDopacityAlpha, isAggregatedCategoricalChoropleth, computeBubblePackLayout, isolatedBubblePackLayout, sequenceLayout, ringsLayout, pieSliceLayout, pieValueLabelLayout, isBarChart, flatBarLayout, flatChartBox, flatValueRules, flatPieRecord, flatPieAccumulate, flatPieItemValues, flatVectorItems, bezierVectorLayout, cubicBezierPoints, arrowMarkerTriangle, fadeGradientStops, flatToArray, hashUnit, multiQuadOffsets, pixelOffsetLngLat, chartTextTransmittance, insideSymbolShape,
13862
15901
  normalizeTheme, projectThemeToDefinition, withoutProjectCode, groupRecordCount, resolveBasemapStyleUrl, resolveMapTypeColor, LayerBuilder, LayerRuntime, MapBuilder, typeStyleNumbers, styleNum,
13863
15902
  resolveScriptUrl, isTrustedScriptUrl, loadProcessingScript, loadBrokerData, applyBrokerThemePatch, makeBrokerTheme,
13864
15903
  equalIntervalBreaks, quantileBreaks, naturalBreaks, evenStrideSample, jenksBreakValues, partsFromBreakValues, resolvePartsClass,
13865
15904
  computeAlphaStats, computeMultiFieldClasses, computeDominantStats, resolveDominantClass, computeComposeColorStats,
13866
15905
  resolveComposedColor, computeRangeClasses, colorSchemeClassCount, resolveDopacityAlpha,
13867
15906
  flatColorSweep, applyClassesToColorScheme, resolveClassColors, flatOutlierStats, parseCssColor, flatLegendLook,
13868
- flatToMapLibreZoom, mapLibreToFlatZoom, scaleDenominatorAt, resolveZoomReference, resolveAggregationPx, valuesHiddenByScale,
15907
+ flatToMapLibreZoom, mapLibreToFlatZoom, flatViewToMapLibreZoom, mapLibreToFlatViewZoom, flatSvgProjectionOf, flatSvgProjectionByOptions, normalSizeScaleOf, scaleDenominatorAt, resolveZoomReference, resolveAggregationPx, valuesHiddenByScale,
13869
15908
  fetchLayerData, parseCsvText, dataTableRows, geometryRowsToFeatureCollection, filterFlatValues, applyField100, field100Binding, rangeClassLegendTotals, resolveAggregateValue, classValueSeparate, cellAggregatedValues, oneHot, groupCoLocated, aggregateOnGrid, GridAggregateIndex, flatRangeParts, aggregateField100, valueRadius, itemPlotGeometry, itemAnchor, objectZoomFactor, resolveZoomReference, defaultNormalSizeValue,
13870
- applyWhereFilter, joinChartPositions, flatLookupKey, flatShapeCenter, chartHiddenByScale, glowHiddenByScale, isSymbolChart, flatChartAlignOffset, flatChartBranch, flatValueText, categoryValueRecord, flatDerivateRgb, flatChartTextRgb, flatGroupedValue, flatNoBreaks, featuresHiddenByScale, boxHiddenByScale, flatShadowOn, snapToAggregationGrid,
15909
+ applyWhereFilter, joinChartPositions, flatLookupKey, flatShapeCenter, chartHiddenByScale, glowHiddenByScale, isSymbolChart, flatChartAlignOffset, flatChartBranch, flatValueText, categoryValueRecord, flatDerivateRgb, flatBubbleLook, bubbleIconLook, flatChartTextRgb, flatGroupedValue, flatNoBreaks, featuresHiddenByScale, boxHiddenByScale, flatShadowOn, snapToAggregationGrid,
13871
15910
  drawSymbolPath, normalizeSymbolShape,
13872
15911
  flatFormatValue, themeStyleArgs, flatChangedStyleValue, explicitValueDecimals, legendDecimals,
13873
15912
  dataCacheKey, dataCacheDisabled, cachedLayerData, featuresBounds, wantsZoomToExtent, legendIsOn,