ixmaps-gl 0.2.20 → 0.2.22

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 +2200 -104
  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.22';
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() : '?';
@@ -1430,6 +1542,16 @@ in vec4 vPieLineColor;
1430
1542
  _filterCache.set(expr, pred);
1431
1543
  return pred;
1432
1544
  }
1545
+ // a load-time .filter() drops records for good, so a later changeThemeStyle
1546
+ // "filter" must re-filter the unfiltered data (flat re-processes the theme
1547
+ // with the new filter) instead of narrowing what the first filter left
1548
+ function refilterFrom(raw, spec, runtimes) {
1549
+ return expr => {
1550
+ const f = applyWhereFilter(raw, expr);
1551
+ return f.type === 'Table' ? joinTableFeatures(spec, f, runtimes) : f;
1552
+ };
1553
+ }
1554
+
1433
1555
  function applyWhereFilter(fc, filterExpr) {
1434
1556
  if (!filterExpr) return fc;
1435
1557
  const pred = flatFilterPredicate(String(filterExpr));
@@ -1547,13 +1669,17 @@ in vec4 vPieLineColor;
1547
1669
  const lookupField = spec.binding.lookup;
1548
1670
  let layers = String(spec.name).split('|');
1549
1671
  if (layers.length > 1 && spec.flags && spec.flags.has('DIFFERENCE')) layers = layers.slice(-1);
1672
+ const lookup2 = spec.binding.lookup2;
1550
1673
  const features = [];
1551
- for (const layer of layers) {
1674
+ const indexes = layers.map(layer => {
1552
1675
  const index = new Map();
1553
1676
  runtimes.filter(r => r.name === layer && (r.flags.has('FEATURE') || r.flags.has('FEATURES'))).forEach(r => {
1554
1677
  // in theme order, as flat's layer holds the themes' shapes
1555
1678
  for (const [k, v] of featurePositionIndex(r)) { if (flatShapeWins(index.get(k), v.area)) index.set(k, v); }
1556
1679
  });
1680
+ return index;
1681
+ });
1682
+ for (const index of indexes) {
1557
1683
  if (!index.size) continue;
1558
1684
  for (const row of table.rows) {
1559
1685
  const v = row[lookupField];
@@ -1563,7 +1689,18 @@ in vec4 vPieLineColor;
1563
1689
  // joined shape, as flat) needs the shape's km² area carried along
1564
1690
  // (featureAreaKm2 prefers this marker), exactly as the choropleth
1565
1691
  // join stamps it
1566
- if (hit) features.push({ type: 'Feature', geometry: { type: 'Point', coordinates: hit.pos }, properties: row, _flatAreaKm2: hit.areaKm2 });
1692
+ if (!hit) continue;
1693
+ const f = { type: 'Feature', geometry: { type: 'Point', coordinates: hit.pos }, properties: row, _flatAreaKm2: hit.areaKm2 };
1694
+ // VECTOR / BEZIER: the second position by its own key (flat looks
1695
+ // it up in every layer of the theme, maptheme.js 10431-10436); a
1696
+ // record without one stays an item that draws no line (null)
1697
+ if (lookup2 && !String(lookup2).includes('|')) {
1698
+ const v2 = row[lookup2];
1699
+ let hit2 = null;
1700
+ if (v2 != null && v2 !== '') for (const idx of indexes) { hit2 = idx.get(flatLookupKey(v2, spec.style)); if (hit2) break; }
1701
+ f._pos2 = hit2 ? hit2.pos : null;
1702
+ }
1703
+ features.push(f);
1567
1704
  }
1568
1705
  }
1569
1706
  return { type: 'FeatureCollection', features };
@@ -2064,6 +2201,26 @@ in vec4 vPieLineColor;
2064
2201
  'POW2', 'POW3', 'NOSORT'];
2065
2202
  // flat's PIE / DONUT variants with no rendering here (see _buildPieLayers)
2066
2203
  const PIE_INERT_FLAGS = [/3D/, /VOLUME/, /HEIGHT/, /STARBURST/, /FLOWER/, /RAYS/, /BOW/, /ZOOM/, /DIRECTION/, /DIRECTED/, /POLAR/];
2204
+ // ... and of AGGREGATE / CATEGORICAL pies: GRIDSIZE's pie the size of its
2205
+ // grid cell (drawn by the SIZE rule instead), AUTOSIZE, DOPACITY, the
2206
+ // chart BOX and TITLE, GAUGE, MULTIPLE / DIFFUSE, the post-aggregation
2207
+ // value arithmetic (DIFFERENCE, FRACTION, PERCENT …, field100)
2208
+ const PIE_AGGREGATE_INERT_FLAGS = [/^GRIDSIZE$/, /AUTOSIZE/, /DOPACITY/, /BOX/, /TITLE/, /^GAUGE$/, /^MULTIPLE$/, /^DIFFUSE$/,
2209
+ /^DIFFERENCE$/, /^FRACTION$/, /^PERCENT$/, /^PERMILLE$/, /^PRODUCT$/, /^CALCVAL$/, /^RELATIVE$/];
2210
+ // flat's BAR / BARS variants with no rendering here (see flatBarLayout):
2211
+ // the bar is drawn plain (SMALLARROW as ARROW) — tested as substrings,
2212
+ // as flat does
2213
+ const BAR_INERT_FLAGS = [/3D/, /VOLUME/, /COLUMN/, /DOTTED/, /SMALLARROW/, /TRENDLINE/, /AXIS/, /^CLIP$/, /MORPH/,
2214
+ /ZOOM/, /NORMSIZE/, /MENUSIZE/, /INVERT/, /DIAGLEFT/, /DIAGRIGHT/, /^WIDTH$/, /^MULTIPLE$/, /^DIFFUSE$/,
2215
+ /^DIFFERENCE$/, /^FRACTION$/, /^PERCENT$/, /^PERMILLE$/, /^PRODUCT$/, /^CALCVAL$/, /^RELATIVE$/];
2216
+ // flat's VECTOR / BEZIER modifiers with no rendering here (see
2217
+ // _buildVectorLayers), the word → what happens instead
2218
+ const VECTOR_INERT_FLAGS = {
2219
+ VALUES: 'the value text along the curve is not drawn yet',
2220
+ DYNAMICWIDTH: 'not drawn yet (flat widens by the log of the length in its own map units) — the plain width instead',
2221
+ LONG: 'not drawn yet (its bow depends on flat\'s own map units) — the default bow instead',
2222
+ DASH: 'drawn as static dashes — flat\'s dash animation is not ported'
2223
+ };
2067
2224
  const _warnedFlags = new Set();
2068
2225
 
2069
2226
  // GL-PORT COMPAT: real ixmaps-flat's global ixmaps.getThemeObj(szId) /
@@ -2079,6 +2236,19 @@ in vec4 vPieLineColor;
2079
2236
  // build()'s main loop and defineLayer(), the two places a runtime is
2080
2237
  // ever created, for where this gets populated.
2081
2238
  const _globalThemeRegistry = new Map();
2239
+ // flat's theme index (maptheme.js getTheme, "theme 0,1,2,..."): a number or
2240
+ // numeric string below the theme count addresses the n-th theme
2241
+ function isThemeIndex(id, count) {
2242
+ return id != null && id !== '' && typeof id !== 'boolean' && !isNaN(Number(id)) && Number.isInteger(Number(id)) && Number(id) >= 0 && Number(id) < count;
2243
+ }
2244
+ // the runtime of a theme id for the module-level globals: by name, else the
2245
+ // n-th theme of the last built map
2246
+ function registryTheme(id) {
2247
+ const rt = _globalThemeRegistry.get(id);
2248
+ if (rt) return rt;
2249
+ const list = _lastMapApi && _lastMapApi.runtimes;
2250
+ return list && isThemeIndex(id, list.length) ? list[Number(id)] : undefined;
2251
+ }
2082
2252
 
2083
2253
  // Same LAST-DEFINITION-WINS rationale as _globalThemeRegistry above, for
2084
2254
  // real ixmaps-flat's map-LEVEL globals (ixmaps.getProjectString/
@@ -2401,6 +2571,9 @@ in vec4 vPieLineColor;
2401
2571
  'style.valuefield': ['style', 'valuefield'],
2402
2572
  'style.timefield': ['binding', 'time'], // setThemeTimeFrame, see LayerRuntime.setTimeFrame
2403
2573
  'style.titlefield': ['binding', 'title'], // {{theme.item.title}}, see _buildTooltipContext
2574
+ // VECTOR / BEZIER's second position: a join key ("a") or lat/lon
2575
+ // fields ("a|b"), see joinChartPositions and _vectorRecords
2576
+ 'style.lookupfield2': ['binding', 'lookup2'],
2404
2577
  // the lookup key's normalization (flatLookupKey): flat's newTheme moves
2405
2578
  // binding.tonumber / digits / … into these style keys (htmlgui.js 1712)
2406
2579
  'style.lookupdigits': ['style', 'lookupdigits'],
@@ -2633,7 +2806,10 @@ in vec4 vPieLineColor;
2633
2806
  // real ixmaps-flat (htmlgui.js newTheme) merges .meta() INTO style first
2634
2807
  // — meta wins — and only then reads type, bindings and style properties
2635
2808
  const style = Object.assign({}, def.style, def.meta);
2636
- const typeStr = style.type != null ? String(style.type) : '';
2809
+ let typeStr = style.type != null ? String(style.type) : '';
2810
+ // flat (htmlgui-side theme parse, maptheme.js 1192): the old type
2811
+ // word EXACT is CATEGORICAL
2812
+ if (/\bEXACT\b/.test(typeStr) && !/\bCATEGORICAL\b/.test(typeStr)) typeStr += '|CATEGORICAL';
2637
2813
  const filter = style.filter;
2638
2814
  const title = style.title;
2639
2815
  delete style.type;
@@ -2664,6 +2840,37 @@ in vec4 vPieLineColor;
2664
2840
  console.info(`[ixmaps-gl] type flag "${flag}" on a PIE chart recognized, not drawn yet — the pie is drawn flat and plain`);
2665
2841
  }
2666
2842
  });
2843
+ // AGGREGATE / CATEGORICAL pies (_isAggregatedPie): what is not ported
2844
+ if (flags.has('AGGREGATE') || flags.has('CATEGORICAL')) {
2845
+ flags.forEach(flag => {
2846
+ const inert = PIE_AGGREGATE_INERT_FLAGS.find(re => re.test(flag));
2847
+ if (inert && !_warnedFlags.has('PIEAGG:' + flag)) {
2848
+ _warnedFlags.add('PIEAGG:' + flag);
2849
+ console.info(`[ixmaps-gl] type flag "${flag}" on an aggregated PIE chart recognized, not ported yet — ignored`);
2850
+ }
2851
+ });
2852
+ }
2853
+ }
2854
+
2855
+ // BAR variants _buildBarLayers doesn't draw (the bars are drawn plain)
2856
+ if (isBarChart(flags)) {
2857
+ flags.forEach(flag => {
2858
+ if (BAR_INERT_FLAGS.some(re => re.test(flag)) && !_warnedFlags.has('BAR:' + flag)) {
2859
+ _warnedFlags.add('BAR:' + flag);
2860
+ console.info(`[ixmaps-gl] type flag "${flag}" on a BAR chart recognized, not ported yet — the bars are drawn plain`);
2861
+ }
2862
+ });
2863
+ }
2864
+
2865
+ // VECTOR / BEZIER modifiers _buildVectorLayers doesn't draw
2866
+ if (isVectorChart(flags)) {
2867
+ flags.forEach(flag => {
2868
+ const why = VECTOR_INERT_FLAGS[flag];
2869
+ if (why && !_warnedFlags.has('VECTOR:' + flag)) {
2870
+ _warnedFlags.add('VECTOR:' + flag);
2871
+ console.info(`[ixmaps-gl] type flag "${flag}" on a VECTOR/BEZIER chart recognized, ${why}`);
2872
+ }
2873
+ });
2667
2874
  }
2668
2875
 
2669
2876
  const rawBinding = Object.assign({}, def.binding);
@@ -2726,6 +2933,9 @@ in vec4 vPieLineColor;
2726
2933
  opacity: 1; transition: opacity 0.4s ease;
2727
2934
  }
2728
2935
  .ixmaps-splash.ixmaps-splash-hidden { opacity: 0; pointer-events: none; }
2936
+ .ixmaps-splash.ixmaps-splash-dark { background: #1a1a1a; }
2937
+ .ixmaps-splash-dark .ixmaps-splash-word { color: #e8e8e8; }
2938
+ .ixmaps-splash-dark .ixmaps-splash-text { color: #9a9a9a; }
2729
2939
  .ixmaps-splash-mark { position: relative; width: 44px; height: 44px; display: flex; align-items: center; justify-content: center; }
2730
2940
  .ixmaps-splash-ping {
2731
2941
  position: absolute; width: 16px; height: 16px; border-radius: 50%;
@@ -2761,15 +2971,21 @@ in vec4 vPieLineColor;
2761
2971
  // flat's Dictionary.getLocalText (mapscript.js 5631-5648) over the page's
2762
2972
  // .local(text, translation) pairs: the whole text, else word by word
2763
2973
  // (a word's entry is written in quotes, 'word')
2974
+ // plus the page-wide entries of ixmaps.setLocalString (flat's
2975
+ // Api.setLocalString → Dictionary.replace, mapapi.js 123), read at call
2976
+ // time so strings set after the map is built count; the later
2977
+ // setLocalString wins over a .local() pair of the same text
2978
+ const _localStrings = Object.create(null);
2764
2979
  function makeLocalText(locals) {
2765
2980
  const dict = {};
2766
2981
  (locals || []).forEach(a => {
2767
2982
  if (a && typeof a[0] === 'object' && a[0]) Object.assign(dict, a[0]);
2768
2983
  else if (a && a.length >= 2) dict[a[0]] = a[1];
2769
2984
  });
2985
+ const entry = k => (_localStrings[k] !== undefined ? _localStrings[k] : dict[k]);
2770
2986
  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(' ');
2987
+ if (entry(text) !== undefined) return String(entry(text));
2988
+ return String(text).split(' ').map(w => (entry("'" + w + "'") !== undefined ? String(entry("'" + w + "'")) : w)).join(' ');
2773
2989
  };
2774
2990
  }
2775
2991
  // make the map container the positioning context of what the engine
@@ -2782,7 +2998,7 @@ in vec4 vPieLineColor;
2782
2998
  function anchorInside(el) {
2783
2999
  if (el && getComputedStyle(el).position === 'static') el.style.position = 'relative';
2784
3000
  }
2785
- function showSplash(el, text) {
3001
+ function showSplash(el, text, dark) {
2786
3002
  ensureSplashStyle();
2787
3003
  // Checks the actual RENDERED position, not el.style (the inline
2788
3004
  // attribute only) — every example page's own #map_div already sets
@@ -2797,7 +3013,7 @@ in vec4 vPieLineColor;
2797
3013
  // this at all.
2798
3014
  anchorInside(el);
2799
3015
  const splash = document.createElement('div');
2800
- splash.className = 'ixmaps-splash';
3016
+ splash.className = 'ixmaps-splash' + (dark ? ' ixmaps-splash-dark' : '');
2801
3017
  splash.innerHTML = '<div class="ixmaps-splash-mark"><span class="ixmaps-splash-ping"></span><span class="ixmaps-splash-dot"></span></div>' +
2802
3018
  '<div class="ixmaps-splash-word">ixmaps<sup>gl</sup></div>' +
2803
3019
  '<div class="ixmaps-splash-text"></div>';
@@ -3569,7 +3785,10 @@ in vec4 vPieLineColor;
3569
3785
  };
3570
3786
  // a plain FEATURE(S) base layer has one flat fill (no classes); FEATURE|CATEGORICAL
3571
3787
  // is coloured per category (see _buildFeaturesLayers), so it gets a legend
3572
- const legendApplies = rt => rt.categoryLabels && rt.categoryLabels.length
3788
+ // a CATEGORICAL theme with no value binding (a count per aggregation cell) has no
3789
+ // category rows: its panel is the text legend (title / snippet / description)
3790
+ const countOnlyLegend = rt => !rt.categoryLabels && rt.flags.has('CATEGORICAL') && !rt.binding.value;
3791
+ const legendApplies = rt => (countOnlyLegend(rt) || (rt.categoryLabels && rt.categoryLabels.length))
3573
3792
  && (!(rt.flags.has('FEATURE') || rt.flags.has('FEATURES')) || rt.flags.has('CATEGORICAL')) && !rt.flags.has('NOLEGEND');
3574
3793
  // One theme's panel — at build time for every theme, and again for a
3575
3794
  // theme defined later (map.layer(...) in a myMap.then(...) chain,
@@ -3663,7 +3882,9 @@ in vec4 vPieLineColor;
3663
3882
  // aggregation" reading (style.valuefield, falling back to
3664
3883
  // the bound size field); anything else (plain CATEGORICAL,
3665
3884
  // no SUM) falls back to a per-category record COUNT.
3666
- const useSum = flatFlag(rt.flags, 'SUM') && rt.style.valuefield;
3885
+ // a VECTOR / BEZIER flow legend sums its size field (flat's rows
3886
+ // show the flows' summed sizes, measured on the twin page)
3887
+ const useSum = flatFlag(rt.flags, 'SUM') && (rt.style.valuefield || ((rt._isVectorChart() || rt._isAggregatedPie()) && rt.binding.size));
3667
3888
  const valueField = rt.style.valuefield || rt.binding.size;
3668
3889
  // legendunits wins over the theme's general-purpose units
3669
3890
  // (used elsewhere for tooltips, e.g. _renderItemChartHtml) —
@@ -3672,13 +3893,18 @@ in vec4 vPieLineColor;
3672
3893
  // space injected), matching how style.units/legendunits are
3673
3894
  // themselves authored with their own leading space (e.g.
3674
3895
  // " MW") in real pages.
3675
- const legendUnit = rt.style.legendunits || rt.style.units || '';
3896
+ // (a VECTOR / BEZIER or aggregated PIE legend: flat's szUnit, the
3897
+ // units after a space unless they start with ".", measured on the
3898
+ // twin pages)
3899
+ const flatUnitLegend = rt._isVectorChart() || rt._isAggregatedPie();
3900
+ const rawLegendUnit = rt.style.legendunits || rt.style.units || '';
3901
+ const legendUnit = flatUnitLegend && rawLegendUnit && rawLegendUnit[0] !== '.' && rawLegendUnit[0] !== ' ' ? ' ' + rawLegendUnit : rawLegendUnit;
3676
3902
  // a per-item chart over several value fields: the row values as
3677
3903
  // flat's legend prints them (legend.js 610, 810: __formatValue
3678
3904
  // with valuedecimals, else 2, "BLANK" — whole numbers without
3679
3905
  // decimals), a colorscheme "none" part as an outline in linecolor
3680
3906
  const multiFieldLegend = rt._isMultiFieldChart();
3681
- const legendTotalText = v => (multiFieldLegend
3907
+ const legendTotalText = v => ((multiFieldLegend || flatUnitLegend)
3682
3908
  ? flatFormatValue(v, legendDecimals(rt.style), 'BLANK')
3683
3909
  : rt._formatTooltipValue(v));
3684
3910
  const outlineRgb = rt.style.linecolor && styleLineColor(rt.style.linecolor) !== 'none' ? hexOrNamedToRgb(styleLineColor(rt.style.linecolor)) : [128, 128, 128];
@@ -3698,7 +3924,7 @@ in vec4 vPieLineColor;
3698
3924
  // dropdown and Chart-size slider stay: neither is a
3699
3925
  // "categorical item" or "colorscheme swatch", both are
3700
3926
  // independent controls unrelated to the row list.
3701
- const isTextOnly = rt.flags.has('TEXTLEGEND');
3927
+ const isTextOnly = rt.flags.has('TEXTLEGEND') || countOnlyLegend(rt);
3702
3928
 
3703
3929
  // Map-view-aware per explicit request: totals reflect only
3704
3930
  // what's CURRENTLY on screen (the map's own bounds, plus the
@@ -4238,6 +4464,11 @@ in vec4 vPieLineColor;
4238
4464
  await null;
4239
4465
  const el = document.getElementById(this.containerId);
4240
4466
  if (!el) throw new Error(`[ixmaps-gl] container #${this.containerId} not found`);
4467
+ // flat's init replaces the container's content with its own map page
4468
+ // (mappage.html via innerHTML), so a page's placeholder inside the
4469
+ // map div ("connecting to Internet ...", the fuel-price page) goes
4470
+ // away; here it would stay on top of the map — remove it the same way
4471
+ while (el.firstChild) el.removeChild(el.firstChild);
4241
4472
  // GL-PORT COMPAT: ixmaps.Map(id, {width, height}) — flat sizes its
4242
4473
  // inner map div with these and gives PIXEL values to the page's own
4243
4474
  // container too (htmlgui_flat.js:866-876, parent().css(...)); here
@@ -4248,6 +4479,11 @@ in vec4 vPieLineColor;
4248
4479
  const v = this.mapOptions && this.mapOptions[dim];
4249
4480
  if (typeof v === 'string' && /^\s*\d+(\.\d+)?\s*px\s*$/i.test(v)) el.style[dim] = v.trim();
4250
4481
  else if (typeof v === 'number' && v > 0) el.style[dim] = v + 'px';
4482
+ // a percentage stays the page's CSS — unless that CSS left the
4483
+ // container collapsed on this side (e.g. an empty flex item, the
4484
+ // UNHCR page): flat's own pixel-sized map div fills it there
4485
+ else if (typeof v === 'string' && /^\s*\d+(\.\d+)?\s*%\s*$/.test(v)
4486
+ && !(dim === 'width' ? el.clientWidth : el.clientHeight)) el.style[dim] = v.trim();
4251
4487
  }
4252
4488
  // without width/height flat's map is "fullscreen" (htmlgui_flat.js
4253
4489
  // 847-876): it fills the window, whatever the page's CSS gives the
@@ -4263,15 +4499,29 @@ in vec4 vPieLineColor;
4263
4499
  // overrides the default message, .options({splash:false}) skips it
4264
4500
  // entirely (e.g. for a map embedded somewhere a full-cover overlay
4265
4501
  // would be wrong, or one that's expected to load near-instantly).
4502
+ // dark on a dark map (no basemap style yet: flatLegendLook's name/
4503
+ // color rule, or a basemap faded to <= 0.5 over a dark page, as
4504
+ // effectiveMapDark) — a light sheet flashed between a dark page and
4505
+ // a dark map (the UNHCR page)
4506
+ const splashOpacity = parseFloat(this._engineOptions.basemapopacity);
4507
+ const splashLegendBg = this.mapOptions.legendBackground || this.mapOptions.legendbackground;
4508
+ const splashDark = flatLegendLook(this.mapOptions.mapType, isNaN(splashOpacity) ? 1 : splashOpacity, splashLegendBg).dark
4509
+ || (splashOpacity <= 0.5 && flatIsDarkColor('rgb(' + pageBackgroundColorOf(el).map(Math.round).join(',') + ')'));
4266
4510
  const splash = this._engineOptions.splash === false ? null
4267
- : showSplash(el, this._engineOptions.splashText || 'loading…');
4511
+ : showSplash(el, this._engineOptions.splashText || 'loading…', splashDark);
4268
4512
 
4269
4513
  // ixmaps.getBoundingBox()/setTitle() for brokers called below, before
4270
4514
  // the MapLibre map exists (same default view as the map gets)
4271
4515
  _titleHost = el;
4272
- const initialZoom = this._viewZoom != null && this._viewZoom !== '' ? flatToMapLibreZoom(Number(this._viewZoom)) : 8;
4516
+ // a flat SVG map in another projection (equalearth.svg): its view
4517
+ // framing and map scale (see FLAT_SVG_PROJECTIONS)
4518
+ const svgProjection = flatSvgProjectionOf(this.mapOptions.map);
4519
+ if (svgProjection) flatSvgProjectionByOptions.set(this._engineOptions, svgProjection);
4520
+ const [vLat, vLng] = this._viewCenter || [45.5, 9.2];
4521
+ const initialZoom = this._viewZoom != null && this._viewZoom !== ''
4522
+ ? flatViewToMapLibreZoom(Number(this._viewZoom), Number(vLat), Number(vLng), el.clientWidth, el.clientHeight, svgProjection)
4523
+ : 8;
4273
4524
  {
4274
- const [vLat, vLng] = this._viewCenter || [45.5, 9.2];
4275
4525
  _boundsSource = () => viewBounds(Number(vLat), Number(vLng), initialZoom, el.clientWidth, el.clientHeight);
4276
4526
  }
4277
4527
 
@@ -4353,6 +4603,7 @@ in vec4 vPieLineColor;
4353
4603
  const filtered = applyWhereFilter(raw, spec.filter);
4354
4604
  const fc = filtered.type === 'Table' ? joinTableFeatures(spec, filtered, runtimes) : filtered;
4355
4605
  const rt = new LayerRuntime(spec, fc, this._engineOptions);
4606
+ if (spec.filter) rt._refilter = refilterFrom(raw, spec, runtimes);
4356
4607
  // tags which underlying data source this runtime came from — see
4357
4608
  // setFacetFilter/clearFacetFilter/clearAllFacetFilters below,
4358
4609
  // which use this to propagate a facet filter to every theme
@@ -4390,7 +4641,7 @@ in vec4 vPieLineColor;
4390
4641
  style: mapTypeColor ? buildBlankBackgroundStyle(mapTypeColor)
4391
4642
  : resolveBasemapStyleUrl(this.mapOptions.mapType),
4392
4643
  center: [lon, lat],
4393
- zoom: this._viewZoom != null && this._viewZoom !== '' ? flatToMapLibreZoom(Number(this._viewZoom)) : 8,
4644
+ zoom: initialZoom,
4394
4645
  // MapLibre's own AttributionControl (bottom right) keeps the
4395
4646
  // basemap's required CARTO/OpenStreetMap credit; the page's own
4396
4647
  // .attribution() goes bottom left, as flat shows it
@@ -4414,6 +4665,7 @@ in vec4 vPieLineColor;
4414
4665
  // world once, at the cost of that zoom limit.
4415
4666
  renderWorldCopies: this._engineOptions.worldcopies !== false
4416
4667
  });
4668
+ engineOptionsByMap.set(map, this._engineOptions);
4417
4669
  // the live-SVG pane USER charts render into (see chartHostFor) —
4418
4670
  // registered here, keyed by this map's options object, so every
4419
4671
  // runtime (including later loadProject/map.layer ones) finds it
@@ -4461,7 +4713,7 @@ in vec4 vPieLineColor;
4461
4713
  if (typeof hook !== 'function' || !prev) return;
4462
4714
  const p = map.project(c), q = map.project(prev.c);
4463
4715
  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); }
4716
+ try { hook.call(global.ixmaps, flatViewZoomOf(map, z)); } catch (e) { console.error('[ixmaps-gl] htmlgui_onZoomAndPan:', e); }
4465
4717
  });
4466
4718
  notifyZoomAndPanMidGesture = () => {
4467
4719
  const hook = pageIxmaps() && pageIxmaps().htmlgui_onZoomAndPan;
@@ -4469,13 +4721,13 @@ in vec4 vPieLineColor;
4469
4721
  const z = map.getZoom();
4470
4722
  if (z === last.z) return; // a pan alone waits for 'moveend' (and its 10 px rule)
4471
4723
  last = { z, c: map.getCenter() };
4472
- try { hook.call(global.ixmaps, mapLibreToFlatZoom(z)); } catch (e) { console.error('[ixmaps-gl] htmlgui_onZoomAndPan:', e); }
4724
+ try { hook.call(global.ixmaps, flatViewZoomOf(map, z)); } catch (e) { console.error('[ixmaps-gl] htmlgui_onZoomAndPan:', e); }
4473
4725
  };
4474
4726
  // flat calls it on the first draw too (its old zoom is unset then)
4475
4727
  map.once('load', () => {
4476
4728
  last = { z: map.getZoom(), c: map.getCenter() };
4477
4729
  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); } }
4730
+ if (typeof hook === 'function') { try { hook.call(global.ixmaps, flatViewZoomOf(map, last.z)); } catch (e) { console.error('[ixmaps-gl] htmlgui_onZoomAndPan:', e); } }
4479
4731
  });
4480
4732
  }
4481
4733
 
@@ -4505,10 +4757,12 @@ in vec4 vPieLineColor;
4505
4757
  // carry the suffix; stripping a pattern that isn't there is a
4506
4758
  // no-op. A theme's single circles (ix-bubbles-<name>-circles-gN,
4507
4759
  // a ScatterplotLayer) route like its bubbles.
4508
- const base = layerId.replace(/-g\d+$/, '').replace(/-circles$/, '');
4760
+ const base = layerId.replace(/-g\d+$/, '').replace(/-circles$/, '').replace(/^(ix-vector-heads-.*)-n\d+$/, '$1');
4509
4761
  return runtimes.find(r => {
4510
4762
  if (base === `ix-bubbles-${r.name}`) return isSymbolChart(r.flags);
4511
4763
  if (base === `ix-pie-${r.name}`) return r._isPieChart();
4764
+ if (base === `ix-bar-${r.name}`) return r._isBarChart();
4765
+ if (base === `ix-vector-${r.name}` || base === `ix-vector-heads-${r.name}`) return r._isVectorChart();
4512
4766
  if (base === `ix-dot-${r.name}`) return r.flags.has('DOT');
4513
4767
  if (base === `ix-choropleth-${r.name}`) return r.flags.has('CHOROPLETH');
4514
4768
  if (base === `ix-features-${r.name}`) return r.flags.has('FEATURE') || r.flags.has('FEATURES');
@@ -4578,6 +4832,9 @@ in vec4 vPieLineColor;
4578
4832
  // verified conventions' own priority (layer name, then style.name).
4579
4833
  function findRuntime(themeId) {
4580
4834
  const matches = runtimes.filter(r => r.name === themeId || (r.style && r.style.name === themeId) || (r.meta && r.meta.name === themeId));
4835
+ // a number (or "1", "2" ...) naming no theme is flat's theme index
4836
+ // (maptheme.js getTheme): 0-based, base layers counted
4837
+ if (!matches.length && isThemeIndex(themeId, runtimes.length)) return runtimes[Number(themeId)];
4581
4838
  if (!matches.length) { console.warn(`[ixmaps-gl] no layer named "${themeId}"`); return undefined; }
4582
4839
  return matches.find(r => !r.flags.has('FEATURE') && !r.flags.has('FEATURES')) || matches[0];
4583
4840
  }
@@ -4665,7 +4922,7 @@ in vec4 vPieLineColor;
4665
4922
  resize: () => { map.resize(); return engineApi; },
4666
4923
  // broker calls in flight (themes whose data comes by name)
4667
4924
  pendingLoads: () => pendingLoads,
4668
- getZoom: () => mapLibreToFlatZoom(map.getZoom()),
4925
+ getZoom: () => flatViewZoomOf(map),
4669
4926
  require: (url) => { loadRequiredScripts([url]); return engineApi; },
4670
4927
  // flat's theme-level calls by theme id (style.name / meta.name, else
4671
4928
  // the layer name): add(theme, flag) — "replace" swaps a theme of the
@@ -4860,7 +5117,13 @@ in vec4 vPieLineColor;
4860
5117
  targets.forEach(rt => {
4861
5118
  if (validation) validation.style(themeIdOf(rt), { [key]: value });
4862
5119
  if (key === 'filter') {
4863
- rt.setRuntimeFilter(action === 'remove' ? '' : value);
5120
+ const expr = action === 'remove' ? '' : value;
5121
+ if (rt._refilter) {
5122
+ // the theme was loaded with a filter: re-filter its full data
5123
+ rt._runtimeFilterExpr = '';
5124
+ rt._filterExpr = expr;
5125
+ rt.replaceFeatures(rt._refilter(expr));
5126
+ } else rt.setRuntimeFilter(expr);
4864
5127
  changed = true;
4865
5128
  return;
4866
5129
  }
@@ -4938,7 +5201,7 @@ in vec4 vPieLineColor;
4938
5201
  const z = isOpts ? latlonOrOpts.zoom : zoom;
4939
5202
  if (c) {
4940
5203
  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)) } : {}));
5204
+ jumpLive(Object.assign({ center: lngLat }, z != null ? { zoom: mapLibreZoomOfFlatView(map, Number(z), lngLat[1], lngLat[0]) } : {}));
4942
5205
  }
4943
5206
  return engineApi;
4944
5207
  },
@@ -5198,6 +5461,7 @@ in vec4 vPieLineColor;
5198
5461
  spec = pspec;
5199
5462
  }
5200
5463
  rt.replaceFeatures(namedThemeFeatures(spec, rows, rt._specBinding));
5464
+ rt._refilter = spec.filter ? expr => namedThemeFeatures(Object.assign({}, spec, { filter: expr }), rows, rt._specBinding) : null;
5201
5465
  if (glDebug()) console.info(`[ixmaps-gl debug] data for ${themeIdOf(rt)}: ${rows.length} rows → ${rt.features.length} features`);
5202
5466
  // the legend is built from the theme's classes: build it again
5203
5467
  if (rt._legendPanel && rt._legendPanel.parentNode) rt._legendPanel.parentNode.removeChild(rt._legendPanel);
@@ -5273,6 +5537,7 @@ in vec4 vPieLineColor;
5273
5537
  const filtered = applyWhereFilter(raw, spec.filter);
5274
5538
  const fc = filtered.type === 'Table' ? joinTableFeatures(spec, filtered, runtimes) : filtered;
5275
5539
  const rt = new LayerRuntime(spec, fc, builder._engineOptions);
5540
+ if (spec.filter) rt._refilter = refilterFrom(raw, spec, runtimes);
5276
5541
  rt._definition = pdef; // for refreshTheme
5277
5542
  rt._deferredLoad = deferred;
5278
5543
  rt._named = named;
@@ -5345,6 +5610,12 @@ in vec4 vPieLineColor;
5345
5610
 
5346
5611
  function applyProjectMap(m, f, report) {
5347
5612
  const svg = String(m.map || '');
5613
+ // its view and map scale follow the project's map file, as on flat
5614
+ if (svg) {
5615
+ const svgProjection = flatSvgProjectionOf(svg);
5616
+ if (svgProjection) flatSvgProjectionByOptions.set(builder._engineOptions, svgProjection);
5617
+ else flatSvgProjectionByOptions.delete(builder._engineOptions);
5618
+ }
5348
5619
  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
5620
  if (typeof map.setProjection === 'function') {
5350
5621
  try {
@@ -5365,8 +5636,8 @@ in vec4 vPieLineColor;
5365
5636
  const c = m.center || {};
5366
5637
  const lat = Number(c.lat), lng = Number(c.lng);
5367
5638
  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) });
5639
+ if (Number.isFinite(lat) && Number.isFinite(lng)) jumpLive(Object.assign({ center: [lng, lat] }, Number.isFinite(z) ? { zoom: mapLibreZoomOfFlatView(map, z, lat, lng) } : {}));
5640
+ else if (Number.isFinite(z)) jumpLive({ zoom: mapLibreZoomOfFlatView(map, z, map.getCenter().lat, map.getCenter().lng) });
5370
5641
  }
5371
5642
  // a project can't authorize its own scripts — trustedscripts is the page's
5372
5643
  if (m.options && typeof m.options === 'object') {
@@ -6040,6 +6311,110 @@ in vec4 vPieLineColor;
6040
6311
  const FLAT_ZOOM_OFFSET = 1;
6041
6312
  const flatToMapLibreZoom = z => z - FLAT_ZOOM_OFFSET;
6042
6313
  const mapLibreToFlatZoom = z => z + FLAT_ZOOM_OFFSET;
6314
+
6315
+ // flat's world SVG maps in a projection other than Mercator
6316
+ // (map:".../generic/equalearth.svg"), which this engine draws in Web
6317
+ // Mercator. Flat syncs such a map to its Leaflet view by fitting the
6318
+ // Leaflet bounds' SW/NE corners, projected, into the map
6319
+ // (htmlgui_sync.js setBoundsLatLon → mapscript2.js doSetMapToGeoBounds /
6320
+ // doZoomMapToArea): both axes fit (min) while the bounds span ≥ 180° of
6321
+ // longitude, else the x axis fits the width on its own (mapapi.js
6322
+ // doSetMapToGeoBounds drops fPreserveMapRatio) — so the same view zoom
6323
+ // frames these maps differently from Mercator (Equal Earth at 2 on
6324
+ // 1024×748: 1.55× the Mercator scale; at 4: 1.09×).
6325
+ // `project` is flat's own forward projection (mapscript.js
6326
+ // _LLtoEqualEarth, _LLtoWinkelTripel — Winkel's cos(50.467) in radians
6327
+ // included), in units of the earth radius.
6328
+ // `scaleConstant`: flat's map scale (nTrueMapScale · nZoomScale, the
6329
+ // scale its dynamic object scaling and gates use) × the screen pixels
6330
+ // per degree of longitude on the equator — constant per map file,
6331
+ // measured on flat (2026-10-09, view zooms 2-5): Equal Earth 170.1e6,
6332
+ // Winkel 195.45e6; for flat's Mercator map 442 913 385 · 256 / 360 =
6333
+ // 314.96e6 (FLAT_OBJECT_SCALE_CONSTANT). These SVG maps carry the
6334
+ // Mercator map's mapscale (177 165 354), so at the same on-screen size
6335
+ // flat calls their scale 0.54× (0.62×) the Mercator one, and its
6336
+ // symbols are drawn larger by that ratio^(1/dynamicScalePow). Framing
6337
+ // matches flat's pixels per degree of longitude at the view's center;
6338
+ // the scale gates (scaleDenominatorAt) still use the Mercator scale.
6339
+ const FLAT_MERCATOR_SCALE_CONSTANT = FLAT_OBJECT_SCALE_CONSTANT * 256 / 360;
6340
+ const FLAT_SVG_PROJECTIONS = {
6341
+ equalearth: {
6342
+ svg: 'maps/svg/maps/generic/equalearth.svg',
6343
+ scaleConstant: 170.1e6,
6344
+ project: (lat, lon) => {
6345
+ const M = Math.sqrt(3) / 2, A1 = 1.340264, A2 = -0.081106, A3 = 0.000893, A4 = 0.003796;
6346
+ const l = Math.asin(M * Math.sin(lat * Math.PI / 180)), l2 = l * l, l6 = l2 * l2 * l2;
6347
+ 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))];
6348
+ }
6349
+ },
6350
+ winkel: {
6351
+ svg: 'maps/svg/maps/generic/winkel.svg',
6352
+ scaleConstant: 195.45e6,
6353
+ project: (lat, lon) => {
6354
+ const p = lat * Math.PI / 180, q = Math.max(-180, Math.min(180, lon)) * Math.PI / 360;
6355
+ const D = Math.acos(Math.cos(p) * Math.cos(q));
6356
+ let x1 = 0, y1 = 0;
6357
+ 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; }
6358
+ return [0.5 * (x1 + 2 * q * Math.cos(50.467)), 0.5 * (y1 + p)];
6359
+ }
6360
+ }
6361
+ };
6362
+ // the flat SVG map projection of a map:/project map URL (generic file
6363
+ // name), null for Mercator and every map drawn as is (orthographic → globe)
6364
+ function flatSvgProjectionOf(svgUrl) {
6365
+ const m = /(?:^|\/)(equalearth|winkel)\.svgz?(?:[?#].*)?$/i.exec(String(svgUrl || ''));
6366
+ return m ? FLAT_SVG_PROJECTIONS[m[1].toLowerCase()] : null;
6367
+ }
6368
+ // the flat SVG projection a map's options object belongs to (set by the
6369
+ // map's init and loadProject; the runtimes only see the options object)
6370
+ const flatSvgProjectionByOptions = new WeakMap();
6371
+ // flat SVG map scale ÷ the Mercator one at the same on-screen size (1
6372
+ // without a flat SVG projection)
6373
+ function flatMapScaleRatio(mapOptions) {
6374
+ const proj = mapOptions && typeof mapOptions === 'object' ? flatSvgProjectionByOptions.get(mapOptions) : null;
6375
+ return proj ? proj.scaleConstant / FLAT_MERCATOR_SCALE_CONSTANT : 1;
6376
+ }
6377
+ // a flat view zoom → the MapLibre zoom showing what flat shows at it:
6378
+ // under a flat SVG projection the same screen pixels per degree of
6379
+ // longitude at the center latitude as flat's fitted map (see
6380
+ // FLAT_SVG_PROJECTIONS), else flatToMapLibreZoom
6381
+ function flatViewToMapLibreZoom(flatZoom, lat, lng, width, height, proj) {
6382
+ if (!proj || !(width > 0) || !(height > 0) || !Number.isFinite(lat) || !Number.isFinite(lng)) return flatToMapLibreZoom(flatZoom);
6383
+ // flat's Leaflet bounds (256 px tiles) at this zoom
6384
+ const [sw, ne] = viewBounds(lat, lng, flatToMapLibreZoom(flatZoom), width, height);
6385
+ const a = proj.project(sw.lat, sw.lng), b = proj.project(ne.lat, ne.lng);
6386
+ const kx = width / Math.abs(b[0] - a[0]), ky = height / Math.abs(b[1] - a[1]);
6387
+ const k = ne.lng - sw.lng < 180 ? kx : Math.min(kx, ky);
6388
+ const pxPerDegree = k * (proj.project(lat, lng + 0.5)[0] - proj.project(lat, lng - 0.5)[0]);
6389
+ return Math.log2(pxPerDegree * 360 / 512);
6390
+ }
6391
+ // the inverse: the flat view zoom of a MapLibre zoom (bisection — the
6392
+ // fit above is monotonic in the zoom)
6393
+ function mapLibreToFlatViewZoom(mapLibreZoom, lat, lng, width, height, proj) {
6394
+ if (!proj || !(width > 0) || !(height > 0) || !Number.isFinite(lat) || !Number.isFinite(lng)) return mapLibreToFlatZoom(mapLibreZoom);
6395
+ let lo = mapLibreToFlatZoom(mapLibreZoom) - 4, hi = mapLibreToFlatZoom(mapLibreZoom) + 4;
6396
+ for (let i = 0; i < 40; i++) {
6397
+ const mid = (lo + hi) / 2;
6398
+ if (flatViewToMapLibreZoom(mid, lat, lng, width, height, proj) < mapLibreZoom) lo = mid; else hi = mid;
6399
+ }
6400
+ return (lo + hi) / 2;
6401
+ }
6402
+ // a live MapLibre map's options object (its runtimes' mapOptions), for
6403
+ // the conversions below
6404
+ const engineOptionsByMap = new WeakMap();
6405
+ function mapSvgProjection(map) {
6406
+ const options = engineOptionsByMap.get(map);
6407
+ return (options && flatSvgProjectionByOptions.get(options)) || null;
6408
+ }
6409
+ // the flat view zoom a live map shows / the MapLibre zoom of a flat view
6410
+ function flatViewZoomOf(map, mapLibreZoom) {
6411
+ const c = map.getCenter(), el = map.getContainer();
6412
+ return mapLibreToFlatViewZoom(mapLibreZoom == null ? map.getZoom() : mapLibreZoom, c.lat, c.lng, el.clientWidth, el.clientHeight, mapSvgProjection(map));
6413
+ }
6414
+ function mapLibreZoomOfFlatView(map, flatZoom, lat, lng) {
6415
+ const el = map.getContainer();
6416
+ return flatViewToMapLibreZoom(flatZoom, lat, lng, el.clientWidth, el.clientHeight, mapSvgProjection(map));
6417
+ }
6043
6418
  // map scale denominator at a MapLibre zoom (null → the default reference):
6044
6419
  // flat's own map scale, the one its scale bar shows and every scale gate
6045
6420
  // (featureupper, chartupper, boxupper, valueupper, aggregation brackets)
@@ -6064,9 +6439,20 @@ in vec4 vPieLineColor;
6064
6439
  // (e.g. "fixed") means they don't — same pixel size at every zoom.
6065
6440
  // the zoom at which flat's object scale equals normalSizeScale (symbols
6066
6441
  // at their normal size) — see FLAT_OBJECT_SCALE_CONSTANT
6442
+ // (under a flat SVG projection, flat's scale at that zoom is
6443
+ // flatMapScaleRatio × the Mercator one)
6067
6444
  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));
6445
+ const scaleDenominator = parseFloat(normalSizeScaleOf(mapOptions)) || FLAT_DEFAULT_NORMAL_SIZE_SCALE;
6446
+ return flatToMapLibreZoom(Math.log2(FLAT_OBJECT_SCALE_CONSTANT * flatMapScaleRatio(mapOptions) / scaleDenominator));
6447
+ }
6448
+ // .options({normalSizeScale}) under any spelling: flat matches the key
6449
+ // case-insensitively (htmlgui.js, /normalSizeScale/i → setScaleParam;
6450
+ // the last matching key wins), e.g. "normalsizescale"
6451
+ function normalSizeScaleOf(mapOptions) {
6452
+ if (!mapOptions || typeof mapOptions !== 'object') return undefined;
6453
+ let value;
6454
+ for (const key of Object.keys(mapOptions)) if (/normalSizeScale/i.test(key)) value = mapOptions[key];
6455
+ return value;
6070
6456
  }
6071
6457
 
6072
6458
  // .options({dynamicScalePow}) — the REAL parameter name (confirmed in
@@ -6139,7 +6525,7 @@ in vec4 vPieLineColor;
6139
6525
  // scale (resolveZoomReference); flat's real rule (maptheme.js 13408,
6140
6526
  // 0.3 + 0.7 / ln(nZoom / dx)) is not ported yet
6141
6527
  function autoOpacityZoomReference(mapOptions) {
6142
- const scaleDenominator = parseFloat(mapOptions && mapOptions.normalSizeScale);
6528
+ const scaleDenominator = parseFloat(normalSizeScaleOf(mapOptions));
6143
6529
  if (!scaleDenominator) return flatToMapLibreZoom(DEFAULT_ZOOM_REFERENCE);
6144
6530
  return flatToMapLibreZoom(Math.log2(WEBMERCATOR_SCALE_CONSTANT / scaleDenominator));
6145
6531
  }
@@ -6474,6 +6860,12 @@ in vec4 vPieLineColor;
6474
6860
 
6475
6861
  // the themes drawn by the symbol chart pipeline (_buildChartLayers): flat's
6476
6862
  // BUBBLE/SQUARE/LABEL branch (maptheme.js 20647-20652), symbols, user charts
6863
+ // CHART|VECTOR / CHART|BEZIER — flat's flow lines (maptheme.js chartMap
6864
+ // 18148-18435, ahead of drawChart: they take precedence over PIE,
6865
+ // BUBBLE, SYMBOL ...; DOT's branch comes first)
6866
+ function isVectorChart(flags) {
6867
+ return flags.has('CHART') && (flags.has('VECTOR') || flags.has('BEZIER')) && !flags.has('DOT');
6868
+ }
6477
6869
  function isSymbolChart(flags) {
6478
6870
  return flags.has('CHART') && (flags.has('SYMBOL') || flags.has('USER') || flags.has('LABEL'));
6479
6871
  }
@@ -6509,6 +6901,17 @@ in vec4 vPieLineColor;
6509
6901
  // the item's radius, SYMBOL: the theme's normal radius)
6510
6902
  // So "left" puts the chart's left edge o to the right of the point (and
6511
6903
  // drops offsetx), "2left" one more h. Returns deck.gl's pixel offset.
6904
+ // flat's maxcharts (maptheme.js 16879-16885): chartMap sorts the charts
6905
+ // ascending (the biggest drawn last, on top) and skips the first
6906
+ // nToDraw − nMaxCharts, so only the biggest maxcharts are drawn — for
6907
+ // every chart type (symbols, pies, bars, VECTOR/BEZIER). In place, on the
6908
+ // already sorted list of charts to draw
6909
+ function applyFlatMaxCharts(list, style) {
6910
+ const n = Math.round(styleNum(style && style.maxcharts)) || 0;
6911
+ if (n > 0 && list.length > n) list.splice(0, list.length - n);
6912
+ return list;
6913
+ }
6914
+
6512
6915
  function flatChartAlignOffset(style, g) {
6513
6916
  const num = v => (isNaN(styleNum(v)) ? 0 : styleNum(v));
6514
6917
  const sym = g.symbolScale || 1;
@@ -6551,6 +6954,53 @@ in vec4 vPieLineColor;
6551
6954
  }
6552
6955
  return [r, g, b].map(c => Math.min(255, Math.floor(c * f)));
6553
6956
  }
6957
+ // A BUBBLE chart's look in flat (maptheme.js, the BUBBLE branch), for a
6958
+ // circle of screen radius r in the class color rgb, `unit` = flat's
6959
+ // normalX(1) on screen (the object zoom factor × style.scale):
6960
+ // - stroke (20740-20780, 20895): style.linecolor, else ChartColors.lowColor
6961
+ // (the color × 0.7, __maptheme_getChartColors) — none with NOLINES or
6962
+ // linecolor "none"
6963
+ // - width (20898-20903): linewidth (0.1) · unit · √(r / normal radius),
6964
+ // the normal radius 15 · unit; OUTLINE: unit · min(1, r / normal radius)
6965
+ // - fill-opacity fillopacity (1); stroke-opacity (21050-21056) 1 with
6966
+ // OUTLINE or a linecolor, else 1 below fillopacity 0.5, 0.3 above
6967
+ // (_buildLabelChartLayers spells the same rule out for CHART|LABEL's box.)
6968
+ function flatBubbleLook(rgb, r, unit, style, flags) {
6969
+ const fillOpacity = styleNum(style.fillopacity) || 1;
6970
+ const outline = flatFlag(flags, 'OUTLINE');
6971
+ const lineColor = style.linecolor ? styleLineColor(style.linecolor) : null;
6972
+ const stroke = lineColor ? (lineColor === 'none' ? null : hexOrNamedToRgb(lineColor))
6973
+ : flatFlag(flags, 'NOLINES') ? null : flatDerivateRgb(rgb, 0.7);
6974
+ const ratio = r / (NORMAL_RADIUS_PX * unit);
6975
+ const width = outline ? unit * Math.min(1, ratio) : unit * (styleNum(style.linewidth) || 0.1) * Math.sqrt(ratio);
6976
+ const strokeOpacity = outline || lineColor ? 1 : (fillOpacity < 0.5 ? 1 : 0.3);
6977
+ return { fillOpacity, stroke, strokeOpacity, width };
6978
+ }
6979
+
6980
+ // flatBubbleLook for a cached icon raster `size` canvas px wide, drawn 2r
6981
+ // screen px wide: the stroke width in canvas px, in quarter-octave steps
6982
+ // (≤ 19 % apart, at most the icon's radius) — every distinct width is
6983
+ // another icon in deck.gl's shared atlas (see ICON_ATLAS_RESET_AFTER), and
6984
+ // a continuous width would make one per bubble. It hardly depends on the
6985
+ // zoom: r and the width both scale with the object zoom factor. `stroke`
6986
+ // only when it is style.linecolor (a packed icon derives each part's own,
6987
+ // see _buildBubbleIcon), noStroke for NOLINES / linecolor "none";
6988
+ // outlineRatio (OUTLINE): r / normal radius, also stepped, as 1 from 1 up
6989
+ // (where flat's OUTLINE width stops growing).
6990
+ function bubbleIconLook(r, unit, style, flags, size) {
6991
+ const look = flatBubbleLook([0, 0, 0], r, unit, style, flags);
6992
+ const step = v => (v > 0 ? Math.round(Math.pow(2, Math.round(Math.log2(v) * 4) / 4) * 1000) / 1000 : 0);
6993
+ const out = {
6994
+ fillOpacity: look.fillOpacity,
6995
+ strokeOpacity: look.strokeOpacity,
6996
+ stroke: style.linecolor ? look.stroke : null,
6997
+ noStroke: !look.stroke,
6998
+ width: r > 0 ? Math.min(size / 2, step(look.width * size / (2 * r))) : 0,
6999
+ };
7000
+ if (flatFlag(flags, 'OUTLINE')) out.outlineRatio = Math.min(1, step(r / (NORMAL_RADIUS_PX * unit)));
7001
+ return out;
7002
+ }
7003
+
6554
7004
  // ChartColors.textColor (colorscheme.js 104-116): darker on light colors
6555
7005
  function flatChartTextRgb(rgb) {
6556
7006
  return flatDerivateRgb(rgb, rgb[0] + rgb[1] + rgb[2] > 450 ? 0.6 : 3);
@@ -7269,6 +7719,412 @@ in vec4 vPieLineColor;
7269
7719
  return [lngLat[0] + offset[0] * 360 / world, lat];
7270
7720
  }
7271
7721
 
7722
+ // CHART|BAR / BARS: which flat draws as a bar chart — drawChart's BAR
7723
+ // branch (maptheme.js 23283) comes after USER, PIE, WAFFLE, BUBBLE,
7724
+ // SYMBOL and BUFFER; flat tests /BAR/ as a substring (BARS too)
7725
+ function isBarChart(flags) {
7726
+ if (!flags.has('CHART') || isVectorChart(flags) || flags.has('DOT')) return false;
7727
+ const t = flags.typeString != null ? flags.typeString : [...flags].join('|');
7728
+ return /BAR/.test(t) && !/\bUSER\b|PIE|WAFFLE|BUBBLE|SYMBOL|BUFFER/.test(t);
7729
+ }
7730
+
7731
+ // One item's bar chart as real ixmaps-flat draws it — maptheme.js
7732
+ // drawChart's BAR branch (23283-24425) — in flat's normal units (u:
7733
+ // normalX(1), the chart size is 30 u; px = u · the object zoom factor ×
7734
+ // style.scale), y down, relative to the item's position. `values`: the
7735
+ // item's parts (nValuesA, after aggregation); `t`: the type string,
7736
+ // tested as flat does, by substring. `o`:
7737
+ // themeMin / themeMax the theme's nMin / nMax (the items' sizes with a
7738
+ // size field or CATEGORICAL, else their parts)
7739
+ // maxSize, itemSize the theme's nMaxSize, the item's nSize (size field)
7740
+ // sizeField a size field is bound
7741
+ // normalSizeValue, sizePow, rangeScale, valueScale, units,
7742
+ // valueDecimals (explicit, else undefined), noBreaks, gridX, nClasses
7743
+ // (partsA.length), classMax (partsA[k].max), nColors, fillOpacity,
7744
+ // lineWidth, lineColor (linecolor, else undefined), fadeNegative,
7745
+ // fadeValuePow, hideValues (valueupper/lower), means / medians / devs
7746
+ // (per part, OFFSETMEAN / OFFSETMEDIAN / DEVIATION)
7747
+ // measure(text, font) the text's width at that font size (u)
7748
+ // Returns null when flat draws no chart (no size, or a single 0 part),
7749
+ // else { bars, texts, lines, extent: [x0, y0, x1, y1] } —
7750
+ // bars: { part (the value's index), cls (its color index), base
7751
+ // [x, y] (the middle of the shaft's foot), dir (0 up, 1 right,
7752
+ // 2 down, 3 left), w2 (half width), sw (shaft half width), len
7753
+ // (shaft), band, head, value, textValue, fill: { opacity },
7754
+ // line: { rgb: 'color' (the bar's) | null (lineColor or black),
7755
+ // width, opacity }, opacity (FADEIN / FADENEGATIVE) }
7756
+ // texts: { text, x, y (the baseline's start), angle (0 reading right,
7757
+ // 90 up, -90 down), font, opacity, color ('value' — flat's
7758
+ // #798697 values color, 'red', 'contrast' — ChartColors
7759
+ // .textColor of the bar's color —, or a CSS color), bg (null,
7760
+ // 'white' — #fefeff —, or 'bar' — the bar's color at 0.5),
7761
+ // cls, part }
7762
+ // lines: STACKED leader lines { x0, y0, x1, y1 }
7763
+ // Divergences (not drawn here): 3D, VOLUME, COLUMN's cylinders, DOTTED,
7764
+ // SMALLARROW (drawn as ARROW), TRENDLINE, the XAXIS/AXIS labels, the
7765
+ // mean/median marks, CLIP / MORPH frames, showparts / clipparts, ZOOM /
7766
+ // NORMSIZE / MENUSIZE, INVERT, DIAGLEFT / DIAGRIGHT — bars are drawn
7767
+ // plain instead (see BAR_INERT_FLAGS).
7768
+ function flatBarLayout(values, t, o) {
7769
+ t = String(t || '');
7770
+ const has = re => re.test(t);
7771
+ const n = values.length;
7772
+ if (!n) return null;
7773
+ const parts = values.map(v => (isFinite(v) ? v : 0));
7774
+ const sum = parts.reduce((a, v) => a + v, 0);
7775
+ const stacked = has(/STACKED/), pointer = has(/POINTER/), horz = has(/HOR/);
7776
+ const sizeFlag = has(/SIZE/) && !has(/NORMSIZE/);
7777
+ const sizePow = o.sizePow || 2;
7778
+ const CHART = 30;
7779
+ let nMax = Math.max(o.themeMax, Math.abs(o.themeMin));
7780
+ let nRange = o.themeMax - o.themeMin;
7781
+ let maxSize = o.maxSize;
7782
+ if (o.normalSizeValue) nRange = maxSize = o.normalSizeValue;
7783
+ // flat's getChartSize (no chart when 0) and its "a single 0" rule
7784
+ if (sizeFlag && !has(/NOSIZE/)) {
7785
+ const s = o.sizeField ? o.itemSize : Math.abs(sum);
7786
+ const ref = o.sizeField ? maxSize : nRange;
7787
+ 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);
7788
+ if (isNaN(size) || size === 0) return null;
7789
+ }
7790
+ if (n === 1 && parts[0] === 0 && !has(/ZEROISVALUE|AUTOCOMPLETE|CATEGORICAL/)) return null;
7791
+
7792
+ // the sizer: the chart's size by the size field or by the values' sum
7793
+ let sizer = 1;
7794
+ if (sizeFlag && o.sizeField && o.itemSize != null) {
7795
+ sizer = has(/SIZELOG/) ? 1 / Math.max(1, Math.log(maxSize) * Math.log(o.itemSize))
7796
+ : Math.pow(o.itemSize, 1 / sizePow) / Math.pow(maxSize, 1 / sizePow);
7797
+ } else if (sizeFlag) {
7798
+ const s = pointer ? sum / n : sum;
7799
+ sizer = has(/SIZELOG/) ? 1 / Math.max(1, Math.log(nRange) * Math.log(Math.abs(s)))
7800
+ : Math.pow(Math.abs(s), 1 / sizePow) / Math.pow(nRange, 1 / sizePow);
7801
+ }
7802
+ if (sizer === 0 && sum !== 0) sizer = 1;
7803
+ const chartSize = CHART * sizer;
7804
+
7805
+ // bar width and value text size
7806
+ let W = chartSize / n * 0.66;
7807
+ if (stacked) {
7808
+ W = chartSize / 3;
7809
+ if (o.gridX && !has(/MULTI/)) W /= 2;
7810
+ }
7811
+ let f = W < 5 ? W * 4 / 5 : W;
7812
+ let textOpacity = 1;
7813
+ if (has(/COLUMN/)) W *= has(/THICK/) ? 2 : has(/THIN/) ? 1 : 1.5;
7814
+ if (has(/THIN/)) { W *= 0.6; f *= 0.75; }
7815
+ if (has(/THICK/)) { W *= 1.5; f *= 1.5; }
7816
+ if (stacked) f = 5;
7817
+ if (stacked && !has(/ZOOM/)) f *= sizer;
7818
+ const valueScale = o.valueScale || 1;
7819
+ f *= valueScale;
7820
+
7821
+ // the bar length per value unit (nStep)
7822
+ if (o.normalSizeValue && (!o.field100 || pointer)) nMax = o.normalSizeValue;
7823
+ let step0 = CHART / nMax;
7824
+ if (has(/OFFSETMEAN|OFFSETMEDIAN|DEVIATION/)) step0 = chartSize / 300;
7825
+ if (sizeFlag && has(/SEQUENCE/) && n > 1 && !o.sizeField && !o.field100) step0 = step0 * n / sizer;
7826
+ else if (sizeFlag && n > 1 && !has(/EXPAND/)) {
7827
+ if (o.field100 || pointer) step0 *= sizer;
7828
+ else step0 /= has(/SIZEP4/) ? sizer * sizer : sizer;
7829
+ }
7830
+ if (o.rangeScale) step0 *= o.rangeScale;
7831
+ if (has(/COMPRESSMAX/)) step0 /= 4;
7832
+ else if (has(/COMPRESSMORE/)) step0 /= 3;
7833
+ else if (has(/COMPRESS/)) step0 /= 2;
7834
+ else if (has(/EXPANDMAX/)) step0 *= 4;
7835
+ else if (has(/EXPANDMORE/)) step0 *= 3;
7836
+ else if (has(/EXPAND/)) step0 *= 2;
7837
+ if (pointer && !has(/NORMSIZE/)) step0 *= 1.2;
7838
+
7839
+ // the bar order: SORT descending (STACKED ascending), UP reversed
7840
+ let sorted = null;
7841
+ if (has(/\bSORT\b/)) {
7842
+ sorted = parts.map((v, i) => ({ i, v }));
7843
+ sorted.sort(stacked ? (a, b) => a.v - b.v : (a, b) => b.v - a.v);
7844
+ sorted = sorted.map(s => s.i);
7845
+ }
7846
+ const startI = has(/\bUP\b/) ? n - 1 : 0;
7847
+ const nC = has(/CENTER/) ? 2 : 1;
7848
+ const fmt = (v, d) => flatGroupedValue(v, d, o.noBreaks);
7849
+ const measure = o.measure || ((text, font) => text.length * font * 0.55);
7850
+
7851
+ const bars = [], texts = [], lines = [];
7852
+ let posX = 0, posY = 0, minNext = 0, barsDrawn = 0, step = step0;
7853
+ for (let i = 0; i < n; i++) {
7854
+ step = step0;
7855
+ let idx = Math.abs(startI - i);
7856
+ if (sorted) idx = sorted[idx];
7857
+ let v = parts[idx];
7858
+ let tv = v;
7859
+ if (has(/OFFSETMEAN|OFFSETMEDIAN/)) {
7860
+ const ref = has(/OFFSETMEDIAN/) ? (o.medians || [])[idx] : (o.means || [])[idx];
7861
+ v = 100 / ref * v - 100;
7862
+ v = isFinite(v) ? v : 0;
7863
+ tv = v;
7864
+ nMax = 100;
7865
+ } else if (has(/DEVIATION/)) {
7866
+ v = (v - (o.means || [])[idx]) / (o.devs || [])[idx];
7867
+ v = isFinite(v) ? v : 0;
7868
+ tv = v;
7869
+ v *= 100;
7870
+ }
7871
+ // the color: the part's, or (SEQUENCE, a single part) the class of
7872
+ // the value
7873
+ let cls = o.nColors ? (idx < o.nColors ? idx : idx % (o.gridX || 1000000)) : idx;
7874
+ if ((has(/SEQUENCE|CLIP/) || n === 1) && o.nColors >= 2 && o.classMax && o.classMax.length) {
7875
+ cls = o.classMax.length - 1;
7876
+ for (let k = 0; k < o.classMax.length; k++) if (v < o.classMax[k]) { cls = k; break; }
7877
+ }
7878
+ if (has(/LEFT/) && has(/HORZ/)) v = -v;
7879
+ if (v < 0 && !stacked) { posY = -v * step; v = -v; }
7880
+ if (pointer && sizeFlag) {
7881
+ // flat's sized pointer: its width by the value (the WIDTH variant
7882
+ // by value100 is not ported)
7883
+ W = chartSize * Math.pow(Math.abs(v / nMax), 1 / 3);
7884
+ f = W * 0.6;
7885
+ if (!has(/ZOOM|XAXIS/)) {
7886
+ textOpacity = Math.pow(Math.abs(v), 1 / 2) / Math.pow(o.themeMax, 1 / 2);
7887
+ if (o.fadeValuePow) textOpacity = Math.pow(Math.abs(v), o.fadeValuePow) / Math.pow(nMax, o.fadeValuePow);
7888
+ }
7889
+ }
7890
+ if (has(/DTEXT/) && !has(/ZOOM/)) {
7891
+ f = (chartSize * 2 / 3) * Math.pow(Math.abs(v / nMax), 1 / 2) * 0.75;
7892
+ textOpacity = Math.pow(Math.abs(v), 1 / 3) / Math.pow(nMax, 1 / 3);
7893
+ }
7894
+ v = isFinite(v) ? v : 0;
7895
+ if (v === 0 && has(/NOZERO/)) { posY = 0; continue; }
7896
+ if (tv < 0 && has(/NONEGATIVE/)) { posY = 0; continue; }
7897
+ if (tv > 0 && has(/ONLYNEGATIVE/)) { posY = 0; continue; }
7898
+
7899
+ const oy = posY;
7900
+ const bar = { part: idx, cls, value: v, textValue: tv, dir: 0, w2: W / 2, sw: W / 2, band: 0, head: 0, len: 0,
7901
+ fill: { opacity: o.fillOpacity || 1 }, line: null, opacity: 1 };
7902
+ let drawn = true;
7903
+ if (pointer && v) {
7904
+ if (has(/OFFSETMEAN|OFFSETMEDIAN|DEVIATION/) && Math.abs(v * step) > 50) {
7905
+ const h = Math.abs(v * step);
7906
+ step = (50 + (h - 50) / 10 + h % 50) / Math.abs(v);
7907
+ }
7908
+ const H = v * step;
7909
+ const arrow = has(/ARROW/);
7910
+ const style = { rgb: null, width: o.lineWidth ? o.lineWidth * Math.sqrt(sizer || 1) : 0.05, opacity: 0.5 };
7911
+ if (oy <= 0) {
7912
+ // positive: the shaft from the base up, a band as wide as the bar,
7913
+ // the head on it
7914
+ Object.assign(bar, arrow
7915
+ ? { 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 }
7916
+ : { base: [posX + W / 2, oy - H / nC + H], len: H, sw: W * 5 / 14, band: W / 15, head: W / 2 });
7917
+ bar.line = style;
7918
+ } else if (!has(/NONEGATIVE/)) {
7919
+ // negative: from the zero line down, the head below
7920
+ Object.assign(bar, arrow
7921
+ ? { base: [posX + W / 2, oy - H / nC], len: H, sw: W / 6, head: W / 2, dir: 2 }
7922
+ : { base: [posX + W / 2, oy - H / nC], len: H, sw: W * 5 / 14, head: W / 4, dir: 2 });
7923
+ bar.line = style;
7924
+ if (!has(/LEFT/)) {
7925
+ const fadeNeg = o.fadeNegative || 0.1;
7926
+ if (has(/ZOOM|NORMSIZE/)) {
7927
+ bar.fill.opacity = 0.5;
7928
+ bar.line = { rgb: 'color', width: 0.25, opacity: 1 };
7929
+ } else if (o.nClasses > 2) {
7930
+ bar.fill.opacity = fadeNeg;
7931
+ bar.line = { rgb: 'low', width: o.lineWidth ? o.lineWidth * 0.25 * sizer : 0.25, opacity: 0.8 };
7932
+ } else {
7933
+ bar.fill.opacity = fadeNeg;
7934
+ bar.line = { rgb: 'high', width: o.lineWidth ? o.lineWidth * Math.sqrt(sizer || 1) : 0.05, opacity: 1 };
7935
+ }
7936
+ }
7937
+ } else drawn = false;
7938
+ } else {
7939
+ // plain 2D bar (DOTTED, 3D, COLUMN are drawn as this too)
7940
+ if (!has(/NOZERO/)) v = v || 0.00001;
7941
+ const H = v * step;
7942
+ bar.value = v;
7943
+ if (H < 0) drawn = false;
7944
+ bar.base = [posX + W / 2, oy - H / nC + H];
7945
+ bar.len = H;
7946
+ if (oy <= 0 || has(/LEFT/)) {
7947
+ bar.line = { rgb: null, width: o.lineWidth ? o.lineWidth * 0.25 * sizer : 0.2, opacity: o.lineWidth ? 1 : 0.1 };
7948
+ } else {
7949
+ bar.fill.opacity = 0.3;
7950
+ bar.line = { rgb: 'color', width: 0.25, opacity: 1 };
7951
+ }
7952
+ }
7953
+ if (has(/FADENEGATIVE/) && tv < 0) bar.opacity = 0.5;
7954
+ if (has(/FADEIN/)) {
7955
+ if (n === 2 && i === 0) bar.opacity = 0.6;
7956
+ else if (!has(/NORMSIZE/)) {
7957
+ const p = has(/FADEINP4/) ? 10 : 3;
7958
+ bar.opacity = 0.1 + 0.9 * Math.pow(i + 1, p) / Math.pow(n, p);
7959
+ }
7960
+ }
7961
+ if (drawn) bars.push(bar);
7962
+
7963
+ // the value text
7964
+ const H = v * step;
7965
+ if (has(/VALUES/) && !(oy > 0 && has(/NONEGATIVE/)) && !o.hideValues
7966
+ && f > 0 && !(o.gridX && o.nClasses / o.gridX > 2)) {
7967
+ let text = fmt(tv, o.valueDecimals != null ? o.valueDecimals : (o.themeMax <= 1 ? 1 : 0));
7968
+ if (pointer && tv > 0 && (o.themeMin < 0 || has(/OFFSETMEAN|OFFSETMEDIAN|\bSIGN\b/))) text = '+' + text;
7969
+ if (!has(/VOLUME/)) text += o.units ? ' ' + o.units + ' ' : '';
7970
+ if (stacked && tv !== 0 && !o.field100 && !o.gridX) text += ' (' + Math.round(tv / sum * 100) + '%)';
7971
+ if (stacked) f = (has(/ZOOM/) ? 18 : 5) * valueScale;
7972
+ // SVG collapses the blanks
7973
+ text = text.replace(/\s+/g, ' ').trim();
7974
+ const red = parseFloat(text) < 0;
7975
+ // createTextLabel's box: not with HORZ; with ZOOM … AXIS only
7976
+ // colored (VALUEBACKGROUND / CTEXT); colored for a positive value
7977
+ const colored = tv > 0 && has(/CTEXT|VALUEBACKGROUND/);
7978
+ let bgOn = true;
7979
+ if (has(/ZOOM|MENUSIZE|NORMSIZE|INFOSIZE|AXIS|SELECTION/)) bgOn = colored;
7980
+ if (has(/VALUEBACKGROUND/)) bgOn = true;
7981
+ if (horz) bgOn = false;
7982
+ const bg = bgOn ? (colored ? 'bar' : 'white') : null;
7983
+ // the text color: negative red (valuecolor), else textcolor; on the
7984
+ // bar's color its text color, else flat's values color
7985
+ const color = red ? (o.valueColor || 'red') : (o.textColor || (bg === 'bar' ? 'contrast' : 'value'));
7986
+ const textW = measure(text, f);
7987
+ const tLen = textW + (bgOn ? 0.7 * f : 0);
7988
+ const ptY = W / 2 + f * 0.15;
7989
+ const outside = (!has(/DOINLINETEXT/) || has(/ZOOM/) || has(/NORMSIZE/)) && has(/VALUE/) && !(has(/VOLUME/) && !has(/ZOOM/))
7990
+ && (has(/THIN|NOINLINETEXT|DTEXT|SIZE|STACKED|NORMSIZE|ZOOM/) || tLen + 1 > H / nC);
7991
+ const base = { text, cls, part: idx, opacity: textOpacity, color, bg };
7992
+ if (outside && stacked) {
7993
+ if (!(v === 0 && !has(/ZEROISVALUE/)) && !(o.gridX && n / o.gridX > 2)) {
7994
+ const topDx = H / 2;
7995
+ minNext = Math.max(0, minNext + (f * 0.75 - topDx));
7996
+ const textPosX = minNext + topDx - f / 3;
7997
+ let indent = 0.5 * sizer;
7998
+ let ptYs = ptY;
7999
+ let x1 = posX + ptY + W / 3, x2 = posX + ptY + W * 7 / 8;
8000
+ const y1 = -topDx, y2 = -textPosX - f / 3;
8001
+ if (o.gridX && barsDrawn < o.gridX) {
8002
+ ptYs -= W * 2 + tLen;
8003
+ const tmp = x2;
8004
+ x2 = x1 - W * 1.4;
8005
+ x1 = tmp - W * 1.4;
8006
+ indent = -indent;
8007
+ }
8008
+ texts.push(Object.assign(base, { x: posX + ptYs + W + indent, y: oy - textPosX, angle: 0, font: f }));
8009
+ [[x1, y1, x1 + indent, y1], [x1 + indent, y1, x2, y2], [x2, y2, x2 + indent, y2]]
8010
+ .forEach(([a, b, c, d]) => lines.push({ x0: a, y0: oy + b, x1: c, y1: oy + d }));
8011
+ minNext = Math.max(0, minNext + (f * 2 / 3 - (H - topDx)));
8012
+ }
8013
+ } else if (outside) {
8014
+ const lift = pointer && tv > 0 ? W * 0.5 : 0;
8015
+ if (n === 1 && !has(/HORZ/) && !has(/VOLUME/)) {
8016
+ 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,
8017
+ opacity: tv < 0 ? textOpacity * 0.75 : textOpacity }));
8018
+ } else {
8019
+ // rotate(270) scale(0.9): reading up, 0.9 font
8020
+ 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 }));
8021
+ }
8022
+ } else {
8023
+ // inside the bar, in its text color, shrunk to fit
8024
+ const s = Math.min(1, H / tLen);
8025
+ if (s >= 0.33) {
8026
+ let ptX = -4;
8027
+ if (tv > 0) ptX += Math.max(1, H - (tLen + 3) * s);
8028
+ const dY = 0.5 / s;
8029
+ 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 }));
8030
+ }
8031
+ minNext = Math.max(0, minNext - H);
8032
+ }
8033
+ }
8034
+
8035
+ if (stacked) {
8036
+ if (!has(/CENTER/)) posY -= v * step;
8037
+ } else {
8038
+ posY = 0;
8039
+ posX += W;
8040
+ if (has(/SPACED/)) posX += W / 5;
8041
+ else if (!has(/\bUP\b/)) posX += 0.05;
8042
+ }
8043
+ barsDrawn++;
8044
+ if (stacked && !has(/MULTI/) && o.gridX && barsDrawn % o.gridX === 0) {
8045
+ posY = 0;
8046
+ posX += W + W / 20;
8047
+ }
8048
+ }
8049
+
8050
+ // the chart's position: HOR(Z) turned by 90° (bars to the right, the
8051
+ // first on top, the last centered on the position's height), else
8052
+ // the bars side by side centered on it (STACKED: the first column)
8053
+ const P = posX;
8054
+ let map, dirMap, angleMap;
8055
+ if (horz) {
8056
+ const dy = W / 2 + (has(/3D/) ? W / 3 : 0) - P;
8057
+ map = (x, y) => [-y, x + dy];
8058
+ dirMap = d => (d + 1) % 4;
8059
+ angleMap = a => a - 90;
8060
+ } else {
8061
+ const dx = stacked ? W / 2 : W * barsDrawn / 2;
8062
+ map = (x, y) => [x - dx, y];
8063
+ dirMap = d => d;
8064
+ angleMap = a => a;
8065
+ }
8066
+ const ext = [Infinity, Infinity, -Infinity, -Infinity];
8067
+ 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; };
8068
+ for (const b of bars) {
8069
+ b.base = map(b.base[0], b.base[1]);
8070
+ b.dir = dirMap(b.dir);
8071
+ const d = [[0, -1], [1, 0], [0, 1], [-1, 0]][b.dir], a = [-d[1], d[0]];
8072
+ const L = b.len + b.band + b.head;
8073
+ [[-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));
8074
+ }
8075
+ for (const tx of texts) {
8076
+ [tx.x, tx.y] = map(tx.x, tx.y);
8077
+ tx.angle = angleMap(tx.angle);
8078
+ // arial: ascent 0.905, descent 0.212 of the font
8079
+ const w = measure(tx.text, tx.font) + (tx.bg ? 0.7 * tx.font : 0);
8080
+ const r = tx.angle * Math.PI / 180, c = Math.cos(r), s = -Math.sin(r);
8081
+ const xo = tx.bg ? -0.3 * tx.font : 0;
8082
+ [[xo, -0.905 * tx.font], [xo + w, -0.905 * tx.font], [xo, 0.212 * tx.font], [xo + w, 0.212 * tx.font]]
8083
+ .forEach(([u, v]) => grow(tx.x + u * c - v * s, tx.y + u * s + v * c));
8084
+ }
8085
+ for (const l of lines) {
8086
+ [l.x0, l.y0] = map(l.x0, l.y0);
8087
+ [l.x1, l.y1] = map(l.x1, l.y1);
8088
+ grow(l.x0, l.y0); grow(l.x1, l.y1);
8089
+ }
8090
+ return { bars, texts, lines, extent: ext[0] <= ext[2] ? ext : [0, 0, 0, 0], width: W, sizer };
8091
+ }
8092
+
8093
+ // flat's BOX and TITLE around a chart (maptheme.js 18758-18905), in the
8094
+ // chart's units: `extent` the chart's [x0, y0, x1, y1]; o: { margin
8095
+ // (boxmargin, 2), titleFont, title (text or ''), bottomTitle, alignRight,
8096
+ // measure }. The margin is min(2 m, m · width / 30); the title's first
8097
+ // baseline 0.7 font above the chart (BOTTOMTITLE: one font below), its
8098
+ // lines wrapped to the chart's width and stacked upward; the box encloses
8099
+ // chart and title, its top trimmed by 0.1 font when titled. Returns
8100
+ // { rect: [x0, y0, x1, y1], title: { lines, x, anchor, baseline, font } | null }
8101
+ function flatChartBox(extent, o) {
8102
+ const e = extent.slice();
8103
+ const w = e[2] - e[0];
8104
+ const margin = Math.min(2 * o.margin, o.margin * w / 30);
8105
+ let title = null;
8106
+ if (o.title) {
8107
+ const f = o.titleFont;
8108
+ const measure = o.measure || ((text, font) => text.length * font * 0.55);
8109
+ const lines = [];
8110
+ for (const word of String(o.title).split(/\s+/).filter(Boolean)) {
8111
+ const last = lines.length ? lines[lines.length - 1] + ' ' + word : null;
8112
+ if (last !== null && measure(last, f) <= w) lines[lines.length - 1] = last;
8113
+ else lines.push(word);
8114
+ }
8115
+ const width = Math.max(...lines.map(l => measure(l, f)));
8116
+ const baseline = o.bottomTitle ? e[3] + f : e[1] - 0.7 * f - (lines.length - 1) * f;
8117
+ const x = o.alignRight ? e[2] : e[0];
8118
+ const x0 = o.alignRight ? x - width : x;
8119
+ title = { lines, x, anchor: o.alignRight ? 'end' : 'start', baseline, font: f };
8120
+ e[0] = Math.min(e[0], x0);
8121
+ e[1] = Math.min(e[1], baseline - 0.905 * f) + 0.1 * f;
8122
+ e[2] = Math.max(e[2], x0 + width);
8123
+ e[3] = Math.max(e[3], baseline + (lines.length - 1) * f + 0.212 * f);
8124
+ }
8125
+ return { rect: [e[0] - margin, e[1] - margin, e[2] + margin, e[3] + margin], title, width: w };
8126
+ }
8127
+
7272
8128
  // CHART|PIE / DONUT: one pie's slices as real ixmaps-flat lays them out —
7273
8129
  // maptheme.js drawChart's PIE branch (19939-20330) and piechart.js
7274
8130
  // DonutChart.realize (181-510). `values`: the item's part values (field
@@ -7456,6 +8312,322 @@ in vec4 vPieLineColor;
7456
8312
  return out;
7457
8313
  }
7458
8314
 
8315
+ // flat's value rules of a theme (MapTheme constructor, maptheme.js
8316
+ // 7740-7830), by substring of the type string as flat tests them:
8317
+ // nullIsValue (0 is a value: ZEROISNOTVALUE no, ZEROISVALUE yes, a CHART
8318
+ // with CATEGORICAL or several value fields always), negativeOk
8319
+ // (NEGATIVEISNOTVALUE no, NEGATIVEISVALUE yes, CATEGORICAL without
8320
+ // AGGREGATE no), undefinedOk (UNDEFINEDISNOTVALUE no), zeroExcludes
8321
+ // (ZEROISNOTVALUE: a 0 is left out of an aggregation's means)
8322
+ function flatValueRules(t, nFields) {
8323
+ t = String(t || '');
8324
+ let nullIsValue = true, negativeOk = true, undefinedOk = true;
8325
+ if (/ZEROISNOTVALUE/.test(t)) nullIsValue = false;
8326
+ if (/ZEROISVALUE/.test(t)) nullIsValue = true;
8327
+ if (/NEGATIVEISNOTVALUE/.test(t)) negativeOk = false;
8328
+ if (/NEGATIVEISVALUE/.test(t)) negativeOk = true;
8329
+ if (/UNDEFINEDISVALUE/.test(t)) undefinedOk = true;
8330
+ if (/UNDEFINEDISNOTVALUE/.test(t)) undefinedOk = false;
8331
+ if (/CHART/.test(t) && (/CATEGORICAL/.test(t) || nFields > 1)) nullIsValue = true;
8332
+ if (/CATEGORICAL/.test(t) && !/AGGREGATE/.test(t)) negativeOk = false;
8333
+ return { nullIsValue, negativeOk, undefinedOk, zeroExcludes: /ZEROISNOTVALUE/.test(t) };
8334
+ }
8335
+
8336
+ // An AGGREGATE or CATEGORICAL pie: one record's contribution to its item,
8337
+ // as flat's loadAndAggregateValuesOfTheme reads it (maptheme.js
8338
+ // 10465-10571, which every AGGREGATE theme goes through). `o`:
8339
+ // { rules (flatValueRules), sizeField, cat (CATEGORICAL: the record's
8340
+ // category, 0-based, -1 when it is none of them), fields (otherwise:
8341
+ // the value fields) }
8342
+ // Returns null when flat leaves the record out, else
8343
+ // { cat, size } (CATEGORICAL) or { parts, zeroNot, size }
8344
+ // - size: the size field as a number (1 without one); a record whose
8345
+ // size is no number, 0 (unless 0 is a value) or negative (unless
8346
+ // negatives are values) is left out
8347
+ // - CATEGORICAL: a value that is none of the categories (not in
8348
+ // style.values) is part -1 — no slice, but its size still counts in
8349
+ // the item's size (flat: nValuesA[-1]); left out only with
8350
+ // UNDEFINEDISNOTVALUE
8351
+ // - value fields: a value that is no number is 0 (UNDEFINEDISNOTVALUE:
8352
+ // the record is left out); a 0 is left out of the item's MEAN with
8353
+ // ZEROISNOTVALUE (zeroNot), else the record when 0 is no value; a
8354
+ // negative value when negatives are no values
8355
+ function flatPieRecord(raw, o) {
8356
+ const r = o.rules;
8357
+ const size = o.sizeField ? flatScanValue(raw[o.sizeField]) : 1;
8358
+ if (isNaN(size) || (size === 0 && !r.nullIsValue) || (size < 0 && !r.negativeOk)) return null;
8359
+ if (o.cat !== undefined) {
8360
+ if (o.cat < 0 && !r.undefinedOk) return null;
8361
+ return { cat: o.cat, size };
8362
+ }
8363
+ const parts = [], zeroNot = [];
8364
+ for (let k = 0; k < o.fields.length; k++) {
8365
+ let v = parseFloat(raw[o.fields[k]]);
8366
+ zeroNot[k] = false;
8367
+ if (isNaN(v)) {
8368
+ if (!r.undefinedOk) return null;
8369
+ v = 0;
8370
+ }
8371
+ if (v === 0) {
8372
+ if (r.zeroExcludes) { parts[k] = 0; zeroNot[k] = true; continue; }
8373
+ if (!r.nullIsValue) return null;
8374
+ }
8375
+ if (v < 0 && !r.negativeOk) return null;
8376
+ parts[k] = v;
8377
+ }
8378
+ return { parts, zeroNot, size };
8379
+ }
8380
+
8381
+ // Adds a record's flatPieRecord to its item (maptheme.js 10779-10943) —
8382
+ // `item` null for the first record of the item. Returns the item
8383
+ // { parts (sparse for CATEGORICAL: the category sums), counts (the
8384
+ // records per part, for MEAN), size (the item's size, flat's nSize),
8385
+ // n (its record count, flat's nCount) }
8386
+ // SUM (default): the parts and the |sizes| summed; MAX / MIN: their
8387
+ // maximum / minimum (a part missing so far counts 0, as in flat; the
8388
+ // record count stays 1); FIRST: the first record only; LAST: the last.
8389
+ // A CATEGORICAL record of no category (-1) adds its size only.
8390
+ function flatPieAccumulate(item, rec, flags) {
8391
+ const max = flags.has('MAX'), min = flags.has('MIN');
8392
+ if (item && flags.has('FIRST')) return item;
8393
+ if (!item || flags.has('LAST')) {
8394
+ if (rec.cat !== undefined) {
8395
+ const parts = [], counts = [];
8396
+ if (rec.cat >= 0) { parts[rec.cat] = rec.size; counts[rec.cat] = 1; }
8397
+ return { parts, counts, size: Math.abs(rec.size), n: 1 };
8398
+ }
8399
+ return { parts: rec.parts.slice(), counts: rec.zeroNot.map(z => (z ? 0 : 1)), size: Math.abs(rec.size), n: 1 };
8400
+ }
8401
+ const comb = (a, v) => (max ? Math.max(a || 0, v) : min ? Math.min(a || 0, v) : (a || 0) + v);
8402
+ if (rec.cat !== undefined) {
8403
+ if (rec.cat >= 0) {
8404
+ item.parts[rec.cat] = comb(item.parts[rec.cat], rec.size);
8405
+ item.counts[rec.cat] = (item.counts[rec.cat] || 0) + 1;
8406
+ }
8407
+ } else {
8408
+ for (let k = 0; k < rec.parts.length; k++) {
8409
+ item.parts[k] = comb(item.parts[k], rec.parts[k] || 0);
8410
+ if (!rec.zeroNot[k]) item.counts[k] = (item.counts[k] || 0) + 1;
8411
+ }
8412
+ }
8413
+ if (max) item.size = Math.max(item.size, rec.size);
8414
+ else if (min) item.size = Math.min(item.size, rec.size);
8415
+ else { item.size += Math.abs(rec.size); item.n++; }
8416
+ return item;
8417
+ }
8418
+
8419
+ // An aggregated pie item's parts and size after flat's post-aggregation
8420
+ // steps (maptheme.js 11056-11137): MEAN — a part divided by its record
8421
+ // count (CATEGORICAL: the category's records; value fields: the item's
8422
+ // records, or with ZEROISNOTVALUE the records with a value there), the
8423
+ // size by the item's record count unless SIZESUM; AUTO100 — the parts in
8424
+ // % of their sum. `nParts`: the number of parts (CATEGORICAL with
8425
+ // style.values: every category; without: up to the item's last one, as
8426
+ // flat's sparse nValuesA). Returns { parts, size }.
8427
+ function flatPieItemValues(item, t, o = {}) {
8428
+ t = String(t || '');
8429
+ const n = o.nParts != null ? o.nParts : item.parts.length;
8430
+ let parts = [];
8431
+ for (let k = 0; k < n; k++) parts[k] = item.parts[k] || 0;
8432
+ let size = item.size;
8433
+ if (/MEAN/.test(t)) {
8434
+ const perPart = o.categorical || /ZEROISNOTVALUE/.test(t);
8435
+ parts = parts.map((v, k) => (perPart ? (item.counts[k] > 0 ? v / item.counts[k] : 0) : v / item.n));
8436
+ if (!/SIZESUM/.test(t)) size /= item.n;
8437
+ }
8438
+ if (/AUTO100/.test(t)) {
8439
+ const sum = parts.reduce((a, v) => a + v, 0);
8440
+ parts = parts.map(v => v / (sum / 100));
8441
+ }
8442
+ return { parts, size };
8443
+ }
8444
+
8445
+ // CHART|VECTOR / CHART|BEZIER: the theme's items as real ixmaps-flat
8446
+ // builds them (maptheme.js loadAndAggregateValuesOfTheme, which every
8447
+ // VECTOR / BEZIER theme goes through, 9650: 10402-10450 the second
8448
+ // position, 10763 the item id, 10779-10985 the items). `records`:
8449
+ // { p1: [lng, lat], p2: [lng, lat] | null, cat (0-based category, -1
8450
+ // not one of them; null without CATEGORICAL), value (the value
8451
+ // field as a number, without CATEGORICAL), size (the size field as
8452
+ // a number, NaN when not one; 1 without a size field), raw }
8453
+ // `o`: { aggregate, vectorKey (the VECTOR flag: the item id has the
8454
+ // second position too), multiParts (CATEGORICAL: the category is part
8455
+ // of the id), max / min (MAX / MIN aggregation), zeroIsNotValue,
8456
+ // negativeIsNotValue }. Returns items
8457
+ // { p1, p2, cat, nSize, value, count, raw (the first record), raws }
8458
+ // - without AGGREGATE every record is an item, its nSize the size as
8459
+ // it is (negative too, 0 when not a number) — flat checks no value
8460
+ // there
8461
+ // - AGGREGATE: records of one id are one item: the id is the first
8462
+ // position (+ the category with CATEGORICAL, + the second position
8463
+ // with VECTOR — a BEZIER without VECTOR sums every flow of an origin
8464
+ // into its first record's flow, as flat); a record whose size is no
8465
+ // number (or 0 with ZEROISNOTVALUE, negative with
8466
+ // NEGATIVEISNOTVALUE) is left out; nSize the sum of the |sizes|
8467
+ // (MAX / MIN: their maximum / minimum), value the values' sum
8468
+ // - the first record of an item gives its category, second position,
8469
+ // and the record its tooltip reads
8470
+ function flatVectorItems(records, o = {}) {
8471
+ const items = [];
8472
+ if (!o.aggregate) {
8473
+ for (const r of records) {
8474
+ 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] });
8475
+ }
8476
+ return items;
8477
+ }
8478
+ const byId = new Map();
8479
+ for (const r of records) {
8480
+ const s = r.size;
8481
+ if (isNaN(s) || (s === 0 && o.zeroIsNotValue) || (s < 0 && o.negativeIsNotValue)) continue;
8482
+ let id = r.p1[0] + ',' + r.p1[1];
8483
+ // flat's nValue for the id: the category number (1-based), 0 for a
8484
+ // value that is none of the categories
8485
+ if (o.multiParts) id += ',' + (r.cat != null && r.cat >= 0 ? r.cat + 1 : 0);
8486
+ if (o.vectorKey && r.p2) id += '&' + r.p2[0] + ',' + r.p2[1];
8487
+ const item = byId.get(id);
8488
+ if (!item) {
8489
+ 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] };
8490
+ byId.set(id, it);
8491
+ items.push(it);
8492
+ continue;
8493
+ }
8494
+ if (o.max) {
8495
+ item.nSize = Math.max(item.nSize, s);
8496
+ item.value = Math.max(item.value || 0, r.value || 0);
8497
+ } else if (o.min) {
8498
+ item.nSize = Math.min(item.nSize, s);
8499
+ item.value = Math.min(item.value || 0, r.value || 0);
8500
+ } else {
8501
+ item.nSize += Math.abs(s);
8502
+ item.value = (item.value || 0) + (r.value || 0);
8503
+ }
8504
+ item.count++;
8505
+ item.raws.push(r.raw);
8506
+ }
8507
+ return items;
8508
+ }
8509
+
8510
+ // CHART|BEZIER: one flow's curve as flat draws it (maptheme.js
8511
+ // 18148-18375), in screen pixels relative to its first position (y
8512
+ // down — flat's SVG map coordinates, scaled). dx/dy: the second
8513
+ // position minus the first. o:
8514
+ // w the line width in flat's chart units (linewidth, else
8515
+ // 10 · (|nSize| / max)^(1/sizepow))
8516
+ // ll flat's arrow / gap length in chart units (w + 3, GAP: w +
8517
+ // gapsize — computed by the caller, see _buildVectorLayers)
8518
+ // unit pixels per chart unit (the object zoom × style.scale)
8519
+ // oz pixels per chart unit without style.scale
8520
+ // bow rangescale (flat's nBow; 5 when not given), RANDOM already
8521
+ // applied
8522
+ // t the type string (SHORT, POINTER / ARROW, GAP)
8523
+ // marker markersize (1)
8524
+ // gap gapsize (20)
8525
+ // Returns { start, c1, c2, end, vx, vy } — start/end/c1/c2 [x, y]
8526
+ // pixels; (vx, vy) the end after shortening, which decides the
8527
+ // gradient direction. The control points come from the unshortened
8528
+ // vector: offset by (dy, −dx) / 50 · bow (SHORT: the unit vector · 5
8529
+ // bow chart units), at 1/4 and 1/2 of the (shortened) vector. An arrow (POINTER
8530
+ // or a type word ending in ARROW) ends the curve ll · markersize units
8531
+ // short of the second position, room for the arrow head. GAP: the curve
8532
+ // starts gapsize units off the first position and ends ll / 500 of the
8533
+ // bow further on.
8534
+ function bezierVectorLayout(dx, dy, o) {
8535
+ const t = String(o.t || '');
8536
+ const len = Math.sqrt(dx * dx + dy * dy);
8537
+ const bow = o.bow || 5;
8538
+ let ox, oy;
8539
+ // (LONG's bow depends on flat's own map units — not drawn here, see
8540
+ // VECTOR_INERT_FLAGS: the default bow instead)
8541
+ if (/\bSHORT\b/.test(t)) { ox = dy / len * bow * 5 * o.oz; oy = dx / len * bow * 5 * o.oz; }
8542
+ else { ox = dy / 50 * bow; oy = dx / 50 * bow; }
8543
+ let x = dx, y = dy;
8544
+ // flat's /\bPOINTER|ARROW\b/: POINTER at a word start, or ARROW at a
8545
+ // word end (ARROW, SMALLARROW, LASTARROW ...)
8546
+ if (/\bPOINTER|ARROW\b/.test(t)) {
8547
+ const k = o.ll * o.unit * (o.marker || 1) / len;
8548
+ x -= dx * k; y -= dy * k;
8549
+ }
8550
+ let x0 = 0, y0 = 0;
8551
+ if (/\bGAP\b/.test(t)) {
8552
+ x += ox / 500 * o.ll;
8553
+ y -= oy / 500 * o.ll;
8554
+ x0 = x / len * (o.gap || 20) * o.oz;
8555
+ y0 = y / len * (o.gap || 20) * o.oz;
8556
+ }
8557
+ 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 };
8558
+ }
8559
+
8560
+ // flat's Themes.toArray for a style list (maptheme.js 999-1016): a list
8561
+ // as it is; a string split at "|" when it has one or names an RGB(…)
8562
+ // color (flat's /\RGB/: upper case only), else at ","
8563
+ function flatToArray(v) {
8564
+ if (Array.isArray(v)) return v;
8565
+ const str = String(v);
8566
+ return (/\|/.test(str) || /RGB/.test(str)) ? str.split('|') : str.split(',');
8567
+ }
8568
+ // a fixed number in [0, 1) for a string — stands in for flat's
8569
+ // Math.random() where the harness needs the same drawing every time
8570
+ function hashUnit(str) {
8571
+ let h = 2166136261;
8572
+ for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
8573
+ return (h >>> 0) / 4294967296;
8574
+ }
8575
+
8576
+ // points of a cubic Bézier, n segments (n + 1 points)
8577
+ function cubicBezierPoints(p0, c1, c2, p3, n) {
8578
+ const out = [];
8579
+ for (let i = 0; i <= n; i++) {
8580
+ const s = i / n, u = 1 - s;
8581
+ const a = u * u * u, b = 3 * u * u * s, c = 3 * u * s * s, d = s * s * s;
8582
+ 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]]);
8583
+ }
8584
+ return out;
8585
+ }
8586
+
8587
+ // flat's arrow marker (an SVG <marker>, markerUnits strokeWidth, orient
8588
+ // auto) at a line's end: the triangle M0,s L s,s/2 L0,0 in units of the
8589
+ // line width sw, its reference point (refX, refY) on the end point, its
8590
+ // x axis along the end's direction (tx, ty). BEZIER: s = min(5, 2.5 +
8591
+ // 5 / (w · scale)) · markersize, ref (s / 1.7, s / 2) (maptheme.js
8592
+ // 18309-18320); VECTOR: s = 4, ref (4, 2) — the tip on the end point
8593
+ // (18411-18424). A zero direction is orient 0: pointing right.
8594
+ // Returns the three corners [x, y] (pixels, y down).
8595
+ function arrowMarkerTriangle(end, tx, ty, sw, s, refX, refY) {
8596
+ const l = Math.sqrt(tx * tx + ty * ty);
8597
+ const ux = l ? tx / l : 1, uy = l ? ty / l : 0;
8598
+ const at = (mx, my) => {
8599
+ const a = (mx - refX) * sw, b = (my - refY) * sw;
8600
+ return [end[0] + a * ux - b * uy, end[1] + a * uy + b * ux];
8601
+ };
8602
+ return [at(0, s), at(s, s / 2), at(0, 0)];
8603
+ }
8604
+
8605
+ // BEZIER GRADIENT / FADEIN (maptheme.js 18233-18284): a linear gradient
8606
+ // over the curve's bounding box, vertical when the (shortened) vector
8607
+ // is more vertical than horizontal, else horizontal; the stop on the
8608
+ // first position's side has the fade opacity (FADEIN 0.2, GRADIENT 2),
8609
+ // the other one 2 — SVG clamps both to 1 — and the colors run from the
8610
+ // second color (linecolor's first entry) at the start to the line color
8611
+ // at the end. Returns per point { k: 0 … 1 from the start side's stop,
8612
+ // alpha } — the color mix k and opacity of each point.
8613
+ function fadeGradientStops(points, vx, vy, fade) {
8614
+ const vertical = Math.abs(vy) > Math.abs(vx);
8615
+ const axis = vertical ? 1 : 0;
8616
+ let min = Infinity, max = -Infinity;
8617
+ for (const p of points) { if (p[axis] < min) min = p[axis]; if (p[axis] > max) max = p[axis]; }
8618
+ const v = vertical ? vy : vx;
8619
+ const a = Math.min(1, Math.max(0, fade));
8620
+ // offset 0 is the box's left / top side: the start side unless the
8621
+ // vector points left / up
8622
+ const startAtZero = !(v < 0);
8623
+ return points.map(p => {
8624
+ let f = max > min ? (p[axis] - min) / (max - min) : 0;
8625
+ if (!startAtZero) f = 1 - f;
8626
+ // f: 0 on the start side, 1 on the end side
8627
+ return { k: f, alpha: a + (1 - a) * f };
8628
+ });
8629
+ }
8630
+
7459
8631
  // CHART|SYMBOL|SEQUENCE (flat maptheme.js 21184-21300, 21790-21925,
7460
8632
  // 22296-22395): one symbol per category part of a chart, in category
7461
8633
  // order or sorted (SORT: DOWN biggest first, UP smallest first), parts
@@ -8511,6 +9683,9 @@ in vec4 vPieLineColor;
8511
9683
  cell.value += f.properties.value;
8512
9684
  if (f.properties.classValue !== undefined) cell.classValue += f.properties.classValue;
8513
9685
  if (post) cell.value100 += f.properties.value100 || 0;
9686
+ // an AGGREGATE / CATEGORICAL pie's record (flatPieRecord): its parts
9687
+ // and size added to the cell's item as flat does (flatPieAccumulate)
9688
+ if (f.properties.pie) cell.pie = flatPieAccumulate(cell.pie || null, f.properties.pie, flags);
8514
9689
  const series = f.properties.series;
8515
9690
  if (series) {
8516
9691
  if (!cell.series) cell.series = new Array(series.length).fill(0);
@@ -8532,6 +9707,7 @@ in vec4 vPieLineColor;
8532
9707
  ...(cell.raws && cell.raws.length ? { raws: cell.raws } : {}) }
8533
9708
  : cell.first.properties;
8534
9709
  if (cell.series && cell.n > 1) props = Object.assign({}, props, { series: cell.series });
9710
+ if (cell.pie) props = Object.assign({}, props, { pie: cell.pie });
8535
9711
  // a field aggregation's cell key (the field value): the categories of
8536
9712
  // one cell are merged by it (groupCoLocated), not by their positions
8537
9713
  if (field) props = Object.assign({}, props, { aggKey: cell.key });
@@ -8709,7 +9885,10 @@ in vec4 vPieLineColor;
8709
9885
  this._filterExpr = spec.filter || '';
8710
9886
  this.mapOptions = mapOptions || {};
8711
9887
  this._specBinding = spec.binding;
8712
- this.features = applyField100(filterFlatValues(this._withDensity(fc.features), this.binding, this.flags), this.binding, this.flags, this.style);
9888
+ // VECTOR / BEZIER keep every record: flat checks their values only
9889
+ // when it aggregates them (flatVectorItems)
9890
+ const withDensity = this._withDensity(fc.features);
9891
+ this.features = applyField100(isVectorChart(this.flags) ? withDensity : filterFlatValues(withDensity, this.binding, this.flags), this.binding, this.flags, this.style);
8713
9892
  this.binding = field100Binding(this.binding, this.flags);
8714
9893
  this._iconCache = new Map();
8715
9894
  this._glowIconCache = new Map();
@@ -8804,7 +9983,8 @@ in vec4 vPieLineColor;
8804
9983
  this._gridRefLat = (minLat <= maxLat) ? (minLat + maxLat) / 2 : 0;
8805
9984
  }
8806
9985
 
8807
- if (this._isPieChart() && this.binding.value) {
9986
+ // a bar chart's parts are a pie's (its value fields or categories)
9987
+ if ((this._isPieChart() || this._isBarChart()) && this.binding.value) {
8808
9988
  this._preparePie();
8809
9989
  } else if (this.flags.has('DOMINANT') && this.binding.value && !isAggregatedCategoricalChoropleth(this)) {
8810
9990
  // .type("CHOROPLETH|DOMINANT") — a MULTI-field bound value
@@ -8956,6 +10136,7 @@ in vec4 vPieLineColor;
8956
10136
  if (this._usesAggregationIndex()) {
8957
10137
  this._buildAggregationIndex(this.features);
8958
10138
  }
10139
+ if (this._isVectorChart()) this._prepareVector();
8959
10140
 
8960
10141
  this._prepareAlphaField();
8961
10142
  }
@@ -9049,6 +10230,36 @@ in vec4 vPieLineColor;
9049
10230
  _resolveAggregateValue(props) { return resolveAggregateValue(this.binding, this.flags, props); }
9050
10231
 
9051
10232
  _buildAggregationIndex(sourceFeatures) {
10233
+ // an AGGREGATE / CATEGORICAL pie: one bucket, each record carrying its
10234
+ // contribution to its item (flatPieRecord), added up per position or
10235
+ // grid cell by aggregateOnGrid (flatPieAccumulate)
10236
+ if (this._isAggregatedPie()) {
10237
+ const t = this.flags.typeString != null ? this.flags.typeString : [...this.flags].join('|');
10238
+ const fields = this._pieCategorical ? null : String(this.binding.value || '').split('|');
10239
+ const rules = flatValueRules(t, fields ? fields.length : 1);
10240
+ const sizeField = this.binding.size && this.binding.size !== '$item$' ? this.binding.size : null;
10241
+ const feats = [];
10242
+ for (const f of sourceFeatures) {
10243
+ const c = f.geometry && f.geometry.coordinates;
10244
+ if (!c || typeof c[0] !== 'number' || !isFinite(c[0]) || !isFinite(c[1])) continue;
10245
+ const p = f.properties || {};
10246
+ let cat;
10247
+ if (this._pieCategorical) {
10248
+ const k = this._pieCategoryKey(p[this.binding.value]);
10249
+ const i = k == null ? undefined : this._pieCategoryIndex.get(k);
10250
+ cat = i == null ? -1 : i;
10251
+ }
10252
+ const pie = flatPieRecord(p, { rules, sizeField, cat, fields });
10253
+ if (!pie) continue;
10254
+ feats.push({ type: 'Feature', geometry: f.geometry, properties: { value: pie.size, pie, raw: p } });
10255
+ }
10256
+ this._featuresByCategory = [feats];
10257
+ this._clusterIndices = null;
10258
+ this._clusterRadiusPx = null;
10259
+ this._aggregateStatsCache = null;
10260
+ this._pieStatsCache = null;
10261
+ return;
10262
+ }
9052
10263
  // Range-classed (non-CATEGORICAL numeric) coloring: DON'T pre-split
9053
10264
  // by class here — the real engine aggregates every record together
9054
10265
  // first and classes color from the resulting CELL TOTAL afterward
@@ -9231,7 +10442,10 @@ in vec4 vPieLineColor;
9231
10442
  this._timeMin = undefined;
9232
10443
  const facetFilters = this.facetFilters, runtimeFilterExpr = this._runtimeFilterExpr, timeFrame = this._timeFrame;
9233
10444
  this.binding = this._specBinding;
9234
- this.features = applyField100(filterFlatValues(this._withDensity(fc.features), this.binding, this.flags), this.binding, this.flags, this.style);
10445
+ // VECTOR / BEZIER keep every record: flat checks their values only
10446
+ // when it aggregates them (flatVectorItems)
10447
+ const withDensity = this._withDensity(fc.features);
10448
+ this.features = applyField100(isVectorChart(this.flags) ? withDensity : filterFlatValues(withDensity, this.binding, this.flags), this.binding, this.flags, this.style);
9235
10449
  this.binding = field100Binding(this.binding, this.flags);
9236
10450
  this._iconCache.clear();
9237
10451
  this._glowIconCache.clear();
@@ -9274,10 +10488,14 @@ in vec4 vPieLineColor;
9274
10488
  }
9275
10489
 
9276
10490
  _usesAggregationIndex() {
10491
+ // AGGREGATE / CATEGORICAL pies aggregate their records into items
10492
+ // (see _buildAggregationIndex)
10493
+ if (this._isAggregatedPie()) return true;
9277
10494
  // GRIDSIZE bins its own grid (_ensureGridIndex) — PLOT curves and the
9278
10495
  // plain mesh; a classed GRIDSIZE symbol chart is aggregated like any
9279
10496
  // other (see _isPlainGridMesh)
9280
- return (this.flags.has('AGGREGATE') || isSymbolChart(this.flags))
10497
+ // VECTOR / BEZIER aggregate by their positions (flatVectorItems)
10498
+ return (this.flags.has('AGGREGATE') || isSymbolChart(this.flags)) && !isVectorChart(this.flags)
9281
10499
  && (!this.flags.has('GRIDSIZE') || (!this.flags.has('PLOT') && !this._isPlainGridMesh()));
9282
10500
  }
9283
10501
 
@@ -9562,7 +10780,15 @@ in vec4 vPieLineColor;
9562
10780
  return this._glowIconCache.get(key);
9563
10781
  }
9564
10782
 
9565
- _buildBubbleIcon(counts, colors, marked = null, shapes = null) {
10783
+ // look (optional, a CATEGORICAL BUBBLE's flat look — see bubbleIconLook):
10784
+ // every part filled at look.fillOpacity and stroked like its own flat
10785
+ // bubble (look.stroke, else its color × 0.7; look.strokeOpacity), the
10786
+ // width look.width canvas px for a part of the icon's full radius,
10787
+ // √(share of it) for a smaller one (flat's width ∝ √r) — OUTLINE:
10788
+ // ∝ min(1, r / normal radius), look.outlineRatio being the full
10789
+ // radius / normal radius. Without it the older look: 0.9 fill, a 1 px
10790
+ // white stroke (the SYMBOL branch's packed icon).
10791
+ _buildBubbleIcon(counts, colors, marked = null, shapes = null, look = null) {
9566
10792
  // The cache key quantizes each count's RATIO to the group's total,
9567
10793
  // not the raw count — computeBubblePackLayout's radii are purely
9568
10794
  // sqrt(c/total), so two clusters with wildly different absolute
@@ -9592,7 +10818,8 @@ in vec4 vPieLineColor;
9592
10818
  // coarse share buckets that can put a small marked part at 0
9593
10819
  const key = counts.map(c => Math.round((c / total) * RATIO_BUCKETS)).join('-') + (marked
9594
10820
  ? '|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(',') : '');
10821
+ : '') + (shapes ? '|sym:' + shapes.map(v => String(v).toLowerCase()).join(',') : '')
10822
+ + (look ? `|fb:${look.fillOpacity}:${look.strokeOpacity}:${look.stroke ? look.stroke.join(',') : look.noStroke ? 'none' : 'low'}:${look.width}${look.outlineRatio != null ? ':' + look.outlineRatio : ''}` : '');
9596
10823
  if (this._iconCache.has(key)) return this._iconCache.get(key);
9597
10824
  const size = BUBBLE_ICON_SIZE;
9598
10825
  const canvas = document.createElement('canvas');
@@ -9607,13 +10834,33 @@ in vec4 vPieLineColor;
9607
10834
  // _resolveSymbolShape; circles when the name isn't one GL draws
9608
10835
  const shape = shapes ? normalizeSymbolShape(shapes[Math.min(p.i, shapes.length - 1)]) : 'circle';
9609
10836
  const px = cx + offsets[i].x * fitScale, py = cy + offsets[i].y * fitScale;
10837
+ let pr = radii[i] * fitScale, lw = 0;
10838
+ if (look && !look.noStroke) {
10839
+ const k = pr / (size / 2);
10840
+ lw = Math.min(pr, look.outlineRatio != null
10841
+ ? look.width * Math.min(1, k * look.outlineRatio) / Math.min(1, look.outlineRatio)
10842
+ : look.width * Math.sqrt(k));
10843
+ // inside the part, as _buildSingleIcon: the icon's edge is the bubble's
10844
+ pr = Math.max(0.5, pr - lw / 2);
10845
+ }
9610
10846
  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);
10847
+ if (shape === 'circle') ctx.arc(px, py, pr, 0, Math.PI * 2);
10848
+ else drawSymbolPath(ctx, shape, px, py, pr);
9613
10849
  ctx.closePath();
9614
10850
  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();
10851
+ if (!look) {
10852
+ ctx.globalAlpha = 0.9; ctx.fill(); ctx.globalAlpha = 1;
10853
+ ctx.lineWidth = 1; ctx.strokeStyle = '#fff'; ctx.stroke();
10854
+ return;
10855
+ }
10856
+ ctx.globalAlpha = look.fillOpacity; ctx.fill();
10857
+ if (lw > 0) {
10858
+ ctx.globalAlpha = look.strokeOpacity;
10859
+ ctx.lineWidth = lw;
10860
+ ctx.strokeStyle = `rgb(${(look.stroke || flatDerivateRgb(colors[p.i], 0.7)).join(',')})`;
10861
+ ctx.stroke();
10862
+ }
10863
+ ctx.globalAlpha = 1;
9617
10864
  });
9618
10865
  const icon = { url: canvas.toDataURL(), width: size, height: size, anchorX: size / 2, anchorY: size / 2, id: key };
9619
10866
  return this._cacheIcon(key, icon);
@@ -9642,10 +10889,19 @@ in vec4 vPieLineColor;
9642
10889
  // cached bubble icon here already accepts; the border scales with
9643
10890
  // the bubble exactly as the fill already does, which reads as
9644
10891
  // reasonable rather than wrong.
9645
- _buildSingleIcon(colorRgb, opacity, shape, borderColorRgb, borderWidth) {
10892
+ // borderOpacity (default 1): the border's own alpha (flat's
10893
+ // stroke-opacity, see flatBubbleLook)
10894
+ // colorscheme ["none"] — flat's explicit no-fill symbol
10895
+ _noFillScheme() {
10896
+ const cs = this.style.colorscheme;
10897
+ return Array.isArray(cs) && cs.length === 1 && String(cs[0]).trim().toLowerCase() === 'none';
10898
+ }
10899
+
10900
+ _buildSingleIcon(colorRgb, opacity, shape, borderColorRgb, borderWidth, borderOpacity = 1) {
10901
+ if (!colorRgb) { colorRgb = [128, 128, 128]; opacity = 0; } // no class colour (["none"]): unfilled
9646
10902
  shape = shape || 'circle';
9647
10903
  borderWidth = borderWidth || 0;
9648
- const key = `single-${shape}-${colorRgb.join(',')}-${opacity}-${borderColorRgb ? borderColorRgb.join(',') : 'none'}-${borderWidth}`;
10904
+ const key = `single-${shape}-${colorRgb.join(',')}-${opacity}-${borderColorRgb ? borderColorRgb.join(',') : 'none'}-${borderWidth}${borderOpacity !== 1 ? `-so${borderOpacity}` : ''}`;
9649
10905
  if (this._iconCache.has(key)) return this._iconCache.get(key);
9650
10906
  const size = BUBBLE_ICON_SIZE;
9651
10907
  const canvas = document.createElement('canvas');
@@ -9667,7 +10923,7 @@ in vec4 vPieLineColor;
9667
10923
  ctx.globalAlpha = shape === 'empty' ? 0 : opacity;
9668
10924
  ctx.fill();
9669
10925
  if (borderWidth > 0 && borderColorRgb) {
9670
- ctx.globalAlpha = 1;
10926
+ ctx.globalAlpha = borderOpacity;
9671
10927
  ctx.strokeStyle = `rgb(${borderColorRgb.join(',')})`;
9672
10928
  ctx.lineWidth = borderWidth;
9673
10929
  ctx.stroke();
@@ -9765,18 +11021,35 @@ in vec4 vPieLineColor;
9765
11021
  local: {}
9766
11022
  };
9767
11023
 
11024
+ // a VECTOR / BEZIER flow (measured on the twin page): no item chart,
11025
+ // label or class; its value is flat's nValuesA[0] — with CATEGORICAL
11026
+ // the category's number (1 for the first), else the (summed) value
11027
+ // field; its count the records of the item
11028
+ if (this._isVectorChart() && props.vector) {
11029
+ const v = props.vector.itemValue;
11030
+ dataObj.theme.chart = dataObj.theme.item.chart = '';
11031
+ dataObj.theme.item.value = v == null || isNaN(v) ? '' : flatFormatValue(v, explicitValueDecimals(this.style) ?? (Math.abs(v) < 1 ? 2 : 0), 'ROUND');
11032
+ dataObj.theme.item.label = dataObj.theme.item.class = '';
11033
+ dataObj.theme.item.count = props.count;
11034
+ }
11035
+
9768
11036
  // 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 => {
11037
+ // for a single unaggregated record (see class comment above); an
11038
+ // aggregated pie's item gives its first record's, as flat (data[0])
11039
+ const fieldsRaw = isGroup ? props.firstRaw : props.raw;
11040
+ if (fieldsRaw) {
11041
+ Object.keys(fieldsRaw).forEach(field => {
9772
11042
  if (field === 'geometry') return;
9773
- const v = props.raw[field];
11043
+ const v = fieldsRaw[field];
9774
11044
  if (this._isNumericValue(v)) {
9775
11045
  dataObj[field] = this._formatTooltipValue(v);
9776
11046
  dataObj.raw[field] = v;
9777
11047
  } else {
9778
11048
  dataObj[field] = v;
9779
- dataObj.local[field] = v;
11049
+ // translated, as flat's tooltip (tooltip_mustache.js 446:
11050
+ // ixmaps.getLocalString(value)) — the migration page's ISO
11051
+ // code → country name in {{local.from}} → {{local.to}}
11052
+ dataObj.local[field] = makeLocalText([])(v);
9780
11053
  }
9781
11054
  });
9782
11055
  }
@@ -10062,11 +11335,21 @@ in vec4 vPieLineColor;
10062
11335
  // USER charts (a page's own chart function, style.userdraw) go the
10063
11336
  // chart pipeline too, drawn by _buildUserChartLayers (bubbles when the
10064
11337
  // page has no such function)
11338
+ // VECTOR / BEZIER (flat's chartMap draws them before it calls
11339
+ // drawChart, maptheme.js 18148, 18376)
10065
11340
  // PIE / DONUT (flat's PIE branch comes before BUBBLE and SYMBOL in
10066
11341
  // drawChart, maptheme.js 19939)
11342
+ if (this._isVectorChart()) return this._buildVectorLayers(zoom, bbox, liveZoom, globeCenter);
10067
11343
  if (this._isPieChart()) return this._buildPieLayers(zoom, bbox, liveZoom, globeCenter);
11344
+ // BAR / BARS (flat's BAR branch, after BUBBLE and SYMBOL: a type
11345
+ // with those is not a bar chart, see isBarChart)
11346
+ if (this._isBarChart()) return this._buildBarLayers(zoom, bbox, liveZoom, globeCenter);
10068
11347
  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`);
11348
+ // once per theme, not on every redraw
11349
+ if (!this._warnedNoRenderer) {
11350
+ this._warnedNoRenderer = true;
11351
+ console.warn(`[ixmaps-gl] layer "${this.name}": type "${[...this.flags].join('|')}" has no implemented renderer`);
11352
+ }
10070
11353
  return [];
10071
11354
  }
10072
11355
 
@@ -10428,16 +11711,26 @@ in vec4 vPieLineColor;
10428
11711
  return String(this.binding.value || '').split('|');
10429
11712
  }
10430
11713
  // 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.
11714
+ // fields; AGGREGATE and CATEGORICAL pies one pie per item — the records
11715
+ // of a position (or grid cell) — its slices the categories or the value
11716
+ // fields (_isAggregatedPie, _buildPieLayers)
11717
+ _isVectorChart() { return isVectorChart(this.flags); }
11718
+ // CHART|BAR / BARS: a bar chart per record or item, its bars a pie's
11719
+ // parts (_buildBarLayers)
11720
+ _isBarChart() { return isBarChart(this.flags); }
10434
11721
  _isPieChart() {
10435
- return this.flags.has('CHART') && this.flags.has('PIE') && !this.flags.has('AGGREGATE')
10436
- && !this.flags.has('CATEGORICAL') && !this.flags.has('DOT');
11722
+ return !isVectorChart(this.flags) && this.flags.has('CHART') && this.flags.has('PIE') && !this.flags.has('DOT');
11723
+ }
11724
+ // a pie whose items flat aggregates (loadAndAggregateValuesOfTheme):
11725
+ // AGGREGATE, or CATEGORICAL (a part per category — flat sums the
11726
+ // records of an item's position by category even without AGGREGATE)
11727
+ // (a bar chart's items too)
11728
+ _isAggregatedPie() {
11729
+ return (this._isPieChart() || this._isBarChart()) && (this.flags.has('AGGREGATE') || this.flags.has('CATEGORICAL'));
10437
11730
  }
10438
11731
  // a per-item chart whose parts are its value fields (legend rows, sums)
10439
11732
  _isMultiFieldChart() {
10440
- return !!this._multiFields && (this._isMultiFieldSequence() || this._isPieChart());
11733
+ return !!this._multiFields && (this._isMultiFieldSequence() || this._isPieChart() || this._isBarChart());
10441
11734
  }
10442
11735
  // flat's theme statistics for a pie (maptheme.js 9119-9130): nMin/nMax
10443
11736
  // over every part value of every item, the classes (partsA, flat's
@@ -10445,6 +11738,8 @@ in vec4 vPieLineColor;
10445
11738
  // colors) — flat colors a 1-part pie (or any with CLASSES) by them
10446
11739
  // (maptheme.js 20330-20341); several parts are colored by field.
10447
11740
  _preparePie() {
11741
+ if (this.flags.has('CATEGORICAL')) { this._preparePieCategories(); return; }
11742
+ this._pieCategorical = false;
10448
11743
  const fields = String(this.binding.value).split('|');
10449
11744
  const pooled = [];
10450
11745
  for (const f of this.features) {
@@ -10472,6 +11767,61 @@ in vec4 vPieLineColor;
10472
11767
  }
10473
11768
  }
10474
11769
 
11770
+ // a CATEGORICAL pie's categories (flat's getStringValueIndex, maptheme.js
11771
+ // 9204-9243): style.values in their order, else the values in the order
11772
+ // the records bring them; a value is trimmed (IGNORECASE: upper case),
11773
+ // an empty one is none (UNDEFINEDISVALUE: "undefined"). A slice per
11774
+ // category in its colorscheme color, labeled by style.label. flat's
11775
+ // classes are the category numbers (partsA: i + 1, the last one 0.001
11776
+ // wider) — its value range (nMin / nMax) follows the items
11777
+ // (_pieAggregateStats).
11778
+ _preparePieCategories() {
11779
+ const st = this.style;
11780
+ const t = this.flags.typeString != null ? this.flags.typeString : [...this.flags].join('|');
11781
+ const ignoreCase = /IGNORECASE/.test(t), undefinedIsValue = /UNDEFINEDISVALUE/.test(t);
11782
+ const key = v => {
11783
+ let k = v == null ? '' : String(v);
11784
+ if (k.length === 0 || k === ' ') { if (!undefinedIsValue) return null; k = 'undefined'; }
11785
+ if (ignoreCase) k = k.toUpperCase();
11786
+ return k.trim();
11787
+ };
11788
+ const index = new Map();
11789
+ const explicit = Array.isArray(st.values) ? st.values : null;
11790
+ const labels = [];
11791
+ if (explicit) {
11792
+ explicit.forEach(v => { const k = key(v); if (k != null && !index.has(k)) { index.set(k, labels.length); labels.push(String(v).trim()); } });
11793
+ } else {
11794
+ for (const f of this.features) {
11795
+ const k = key(f.properties[this.binding.value]);
11796
+ if (k != null && !index.has(k)) { index.set(k, labels.length); labels.push(k); }
11797
+ }
11798
+ }
11799
+ this._pieCategorical = true;
11800
+ this._pieExplicitValues = !!explicit;
11801
+ this._pieCategoryKey = key;
11802
+ this._pieCategoryIndex = index;
11803
+ this.categoryLabels = labels;
11804
+ const explicitLabel = Array.isArray(st.label) ? st.label.map(String) : null;
11805
+ this.categoryDisplayLabels = explicitLabel && explicitLabel.length === labels.length ? explicitLabel : labels;
11806
+ this.categoryColorsRgb = resolveClassColors(st.colorscheme, this.categoryDisplayLabels, st.classes);
11807
+ // the legend and the facets look a record's raw value up
11808
+ const byLabel = new Map();
11809
+ for (const f of this.features) {
11810
+ const v = f.properties[this.binding.value];
11811
+ if (byLabel.has(v)) continue;
11812
+ const k = key(v);
11813
+ if (k != null && index.has(k)) byLabel.set(v, index.get(k));
11814
+ }
11815
+ labels.forEach((l, i) => { if (!byLabel.has(l)) byLabel.set(l, i); });
11816
+ this.categoryIndexByLabel = byLabel;
11817
+ const n = labels.length;
11818
+ this.partsA = labels.map((l, i) => ({ min: i + 1, max: i + 1 + (i === n - 1 ? 0.001 : 0) + 1e-9 }));
11819
+ this._rangeClassed = false;
11820
+ this._multiFields = null;
11821
+ this._pieFields = null;
11822
+ this._partNoFill = new Set();
11823
+ }
11824
+
10475
11825
  // x-axis labels of a per-item PLOT (style.xaxis / label, else the fields)
10476
11826
  _itemPlotLabels() {
10477
11827
  const fields = this._itemPlotFields();
@@ -11600,10 +12950,18 @@ in vec4 vPieLineColor;
11600
12950
  // _buildTooltipContext's own `isGroup` check already uses).
11601
12951
  const sizeValueOf = d => d.properties.counts ? d.properties.total : d.properties.value;
11602
12952
  const combined = individual.concat(groups).sort((a, b) => sizeValueOf(a) - sizeValueOf(b));
12953
+ // .style({minvalue}) on a bubble: flat skips an item whose size value is below it
12954
+ // (maptheme.js 19826 / 20698, `this.nMinValue && nSizeValue < this.nMinValue`).
12955
+ // combined is ascending, so the items below it are a prefix.
12956
+ const bubbleMin = this.flags.has('SYMBOL') ? styleNum(this.style.minvalue) : NaN;
12957
+ if (bubbleMin) {
12958
+ let cut = 0;
12959
+ while (cut < combined.length && sizeValueOf(combined[cut]) < bubbleMin) cut++;
12960
+ combined.splice(0, cut);
12961
+ }
11603
12962
  // maxcharts: like the flat engine (maptheme.js nMaxCharts), draw only the
11604
12963
  // 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);
12964
+ applyFlatMaxCharts(combined, this.style);
11607
12965
  if (this.flags.has('USER')) {
11608
12966
  const userLayers = this._buildUserChartLayers(combined, liveZoom, sizeValueOf);
11609
12967
  if (userLayers) return layers.concat(userLayers);
@@ -11884,6 +13242,19 @@ in vec4 vPieLineColor;
11884
13242
  // fillopacity and linecolor, as a single item (flat draws one
11885
13243
  // symbol) — the packed multi-category icon is for CATEGORICAL cells
11886
13244
  const singleSymbol = d => !d.properties.counts || (this._rangeClassed && d.properties.cat != null);
13245
+ // a CATEGORICAL BUBBLE (flat's BUBBLE branch): every bubble — a single
13246
+ // one and each part of a packed one — filled and stroked as flat's,
13247
+ // see flatBubbleLook; the icon carries both opacities
13248
+ const flatLook = flatChartBranch(this.flags) === 'bubble' && this.flags.has('CATEGORICAL')
13249
+ ? d => bubbleIconLook(radiusOf(d), objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(this.style.scale) || 1), this.style, this.flags, BUBBLE_ICON_SIZE)
13250
+ : null;
13251
+ const singleIcon = d => {
13252
+ const rgb = (this.categoryColorsRgb || [])[d.properties.cat];
13253
+ if (!flatLook) return this._buildSingleIcon(rgb, fillOpacity, shapeOf(d.properties), singleBorderColorRgb, singleBorderWidthPx);
13254
+ const look = flatLook(d);
13255
+ return this._buildSingleIcon(rgb, look.fillOpacity, shapeOf(d.properties),
13256
+ look.noStroke || !rgb ? null : (look.stroke || flatDerivateRgb(rgb, 0.7)), look.width, look.strokeOpacity);
13257
+ };
11887
13258
  // flat's symbol outline (maptheme.js 21595-21610 and 21950-21997,
11888
13259
  // drawn 21625-21634 / 21995-22035): every SYMBOL shape is stroked,
11889
13260
  // (linewidth || 1) · normalX(min(2r / rMax, 0.2)) with rMax the
@@ -11919,17 +13290,23 @@ in vec4 vPieLineColor;
11919
13290
  // icon's look, inside the radius
11920
13291
  if (combined.length && !alignOf.active && combined.every(d => singleSymbol(d) && shapeOf(d.properties) === 'circle')) {
11921
13292
  const radiusPx = d => vRadius(sizeValueOf(d));
11922
- const lineRgb = d => outlineRgb(d) || [0, 0, 0];
13293
+ // a CATEGORICAL BUBBLE: flat's look (flatBubbleLook) in screen px,
13294
+ // its stroke centered on the radius as flat's SVG circle
13295
+ const lookUnit = objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(this.style.scale) || 1);
13296
+ // colorscheme ["none"]: flat draws the symbol unfilled (only its values / box / title)
13297
+ const noFill = this._noFillScheme();
13298
+ const circleLook = flatLook ? d => flatBubbleLook((this.categoryColorsRgb || [])[d.properties.cat] || [128, 128, 128], radiusPx(d), lookUnit, this.style, this.flags) : null;
13299
+ const lineRgb = d => (circleLook ? circleLook(d).stroke : outlineRgb(d)) || [0, 0, 0];
11923
13300
  layers.push(new ScatterplotLayer({
11924
13301
  id: `ix-bubbles-${this.name}-circles-g${this._iconGeneration}`,
11925
13302
  data: combined, pickable: true,
11926
13303
  getPosition: d => d.geometry.coordinates,
11927
- getRadius: d => radiusPx(d) - (symbolOutline ? 0 : outlinePx(d, radiusPx(d)) / 2),
13304
+ getRadius: d => radiusPx(d) - (symbolOutline || circleLook ? 0 : outlinePx(d, radiusPx(d)) / 2),
11928
13305
  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)),
13306
+ getFillColor: d => [...((this.categoryColorsRgb || [])[d.properties.cat] || [128, 128, 128]).slice(0, 3), noFill ? 0 : Math.round(this._iconAlpha(d) * (circleLook ? circleLook(d).fillOpacity : fillOpacity))],
13307
+ stroked: circleLook ? combined.some(d => circleLook(d).stroke) : combined.some(d => outlineRgb(d)),
13308
+ getLineColor: d => [...lineRgb(d).slice(0, 3), Math.round(this._iconAlpha(d) * (circleLook ? circleLook(d).strokeOpacity : 1))],
13309
+ getLineWidth: d => (circleLook ? (circleLook(d).stroke ? circleLook(d).width : 0) : outlinePx(d, radiusPx(d))),
11933
13310
  lineWidthUnits: 'pixels',
11934
13311
  billboard: true,
11935
13312
  parameters: ICON_LAYER_GLOBE_PARAMETERS
@@ -11939,7 +13316,8 @@ in vec4 vPieLineColor;
11939
13316
  data: combined, pickable: true,
11940
13317
  getPosition: d => d.geometry.coordinates,
11941
13318
  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);
13319
+ 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));
13320
+ if (flatLook) return singleIcon(d);
11943
13321
  const shape = shapeOf(d.properties), rgb = outlineRgb(d);
11944
13322
  if (!symbolOutline || !rgb) return this._buildSingleIcon((this.categoryColorsRgb || [])[d.properties.cat], fillOpacity, shape, rgb, rgb ? singleBorderWidthPx : 0);
11945
13323
  // the outline in the icon's own pixels, inside its edge; quarter
@@ -11952,7 +13330,8 @@ in vec4 vPieLineColor;
11952
13330
  getSize: d => vRadius(sizeValueOf(d)) * 2,
11953
13331
  // group icons bake a fixed 0.9 alpha; an explicit fillopacity scales it
11954
13332
  // 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))],
13333
+ // (a flat-look icon carries its own fill and stroke opacity)
13334
+ getColor: d => [255, 255, 255, Math.round(this._iconAlpha(d) * (!flatLook && !singleSymbol(d) && this.style.fillopacity != null ? fillOpacity : 1))],
11956
13335
  ...(alignOf.active ? { getPixelOffset: alignOf } : {}),
11957
13336
  sizeUnits: 'pixels',
11958
13337
  billboard: true,
@@ -12361,7 +13740,18 @@ in vec4 vPieLineColor;
12361
13740
  // unit.
12362
13741
  // - draw order: flat's sort before drawing (by the size value, the
12363
13742
  // sum or the first value; the biggest on top)
12364
- // Not drawn here (a console note at type parsing, PIE_INERT_FLAGS):
13743
+ // - AGGREGATE / CATEGORICAL (_isAggregatedPie): a pie per item — the
13744
+ // records of a position, or of an aggregation grid cell —
13745
+ // (_pieAggregatedItems; flatPieRecord / flatPieAccumulate /
13746
+ // flatPieItemValues), a slice per category (CATEGORICAL, in its
13747
+ // category color) or per value field; its size the item's summed
13748
+ // size field (flat's nSize, every record of the item counted), the
13749
+ // theme's nMin / nMax / nMaxSize those of the items
13750
+ // (_pieAggregateStats); draw order by the item's size with a size
13751
+ // field or AGGREGATE|SUM; COUNT prints a part's record count;
13752
+ // CENTERVALUE the item's record count
13753
+ // Not drawn here (a console note at type parsing, PIE_INERT_FLAGS,
13754
+ // PIE_AGGREGATE_INERT_FLAGS):
12365
13755
  // 3D, VOLUME, HEIGHT, STARBURST, FLOWER/RAYS, BOW, ZOOM, DIRECTION /
12366
13756
  // DIRECTED, POLAR — the pie is drawn flat and plain instead. Nor
12367
13757
  // style.clipparts, showparts, gridx (flat's several pies per item).
@@ -12375,15 +13765,20 @@ in vec4 vPieLineColor;
12375
13765
  const st = this.style;
12376
13766
  const t = this.flags.typeString != null ? this.flags.typeString : [...this.flags].join('|');
12377
13767
  const has = re => re.test(t);
12378
- const fields = this._pieFields || String(this.binding.value || '').split('|');
12379
- const multi = fields.length > 1;
13768
+ const agg = this._isAggregatedPie();
13769
+ const cat = !!this._pieCategorical;
13770
+ const fields = cat ? null : (this._pieFields || String(this.binding.value || '').split('|'));
13771
+ const multi = !cat && fields.length > 1;
13772
+ // AGGREGATE / CATEGORICAL: the statistics of the items (sets
13773
+ // _valueMin / _valueMax), their biggest size flat's nMaxSize
13774
+ const stats = agg ? this._pieAggregateStats(zoom) : null;
12380
13775
  const unit = objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(st.scale) || 1);
12381
13776
  const sizePow = resolveSizePow(st, this.flags);
12382
13777
  // flat tests /SIZE/: also NOSIZE and GRIDSIZE
12383
13778
  const sizeFlag = has(/SIZE/) && !has(/NORMSIZE/);
12384
13779
  const sizeLog = has(/SIZELOG/);
12385
13780
  const nsv = styleNum(st.normalsizevalue);
12386
- const maxSize = nsv || this._maxSizeValue;
13781
+ const maxSize = nsv || (stats ? stats.maxSize : this._maxSizeValue);
12387
13782
  const range = nsv || ((this._valueMax || 0) - (this._valueMin || 0));
12388
13783
  const nSizeOf = (sum, size) => {
12389
13784
  const n = NORMAL_RADIUS_PX;
@@ -12398,14 +13793,17 @@ in vec4 vPieLineColor;
12398
13793
  const colors = this.categoryColorsRgb || [];
12399
13794
  const scheme = Array.isArray(st.colorscheme) ? st.colorscheme : [];
12400
13795
  const noFill = this._partNoFill || new Set();
12401
- const byClass = has(/CLASSES/) || !multi;
13796
+ const byClass = has(/CLASSES/) || (!cat && !multi);
13797
+ // flat (maptheme.js 20327-20336): a pie of one part (or CLASSES) takes
13798
+ // the color of the class its value falls in — CATEGORICAL: the class
13799
+ // whose number equals the value (sl.single: the item's only part)
12402
13800
  const classColor = v => {
12403
13801
  const parts = this.partsA || [];
12404
- for (let x = 0; x < parts.length; x++) if (v < parts[x].max) return colors[x];
13802
+ for (let x = 0; x < parts.length; x++) if (cat ? v === parts[x].min : v < parts[x].max) return colors[x];
12405
13803
  return null;
12406
13804
  };
12407
13805
  const sliceRgb = sl => (sl.complement ? (parseCssColor(String(st.nodatacolor || '')) || [0xee, 0xee, 0xee])
12408
- : (byClass && classColor(sl.value)) || colors[sl.i] || [128, 128, 128]);
13806
+ : ((byClass || sl.single) && classColor(sl.value)) || colors[sl.i] || [128, 128, 128]);
12409
13807
  const fo = styleNum(st.fillopacity), op = styleNum(st.opacity);
12410
13808
  // flat sets the group's fill-opacity only for a truthy value
12411
13809
  const fillAlpha = op > 0 ? op : fo > 0 ? fo : 1;
@@ -12415,13 +13813,39 @@ in vec4 vPieLineColor;
12415
13813
  if (has(/WHITELINES/)) line = 'white';
12416
13814
  const lc = st.linecolor != null && st.linecolor !== '' ? styleLineColor(st.linecolor) : null;
12417
13815
  if (lc) line = lc;
12418
- if (scheme.slice(0, fields.length).some(c => String(c).trim().toLowerCase() === 'none')) line = lc || 'black';
13816
+ if (!cat && scheme.slice(0, fields.length).some(c => String(c).trim().toLowerCase() === 'none')) line = lc || 'black';
12419
13817
  const lineRgb = line === 'none' ? null : hexOrNamedToRgb(line);
12420
13818
  const lineWidthOf = r => (lineRgb ? (styleNum(st.linewidth) || 0.1) * r / 15 : 0);
12421
13819
  const lineRgba = lineRgb ? [...lineRgb.slice(0, 3), Math.round(255 * lineAlpha)] : [0, 0, 0, 0];
12422
13820
 
12423
13821
  const charts = [];
12424
- for (const f of this._activeFeatures || this.features) {
13822
+ if (agg) {
13823
+ // one pie per item (_pieAggregatedItems), its parts and size as flat
13824
+ // has them after aggregation (flatPieItemValues)
13825
+ for (const f of this._pieAggregatedItems(zoom, bbox)) {
13826
+ const c = f.geometry.coordinates;
13827
+ if (globeCenter && !isOnVisibleHemisphere(c[0], c[1], globeCenter)) continue;
13828
+ const pie = f.properties.pie;
13829
+ const { parts: values, size } = this._pieItemValues(pie);
13830
+ const layout = pieSliceLayout(values, t, { centerPart: st.centerpart, itemCount: pie.n });
13831
+ if (!layout) continue;
13832
+ if (cat) layout.slices.forEach(sl => { sl.single = values.length === 1; });
13833
+ const sum = values.reduce((a, v) => a + (isNaN(v) ? 0 : v), 0);
13834
+ const r = nSizeOf(sum, size) * unit;
13835
+ if (!(r > 0) || !isFinite(r)) continue;
13836
+ // flat's draw-order key (maptheme.js 16812-16860): with a size
13837
+ // field or AGGREGATE|SUM the item's size, else its first part
13838
+ const key = this.binding.size || (has(/AGGREGATE/) && has(/SUM/)) ? size : values[0];
13839
+ // the tooltip: a row per part (counts), its value the parts' sum
13840
+ // (flat: the sum of nValuesA), its records (theme.item.data, the
13841
+ // page's own {{field}}s from the first, as flat's data[0])
13842
+ const raws = f.properties.raws || (f.properties.raw ? [f.properties.raw] : []);
13843
+ const properties = { counts: values, total: sum, recordCounts: [pie.n], raws, firstRaw: raws[0] || null,
13844
+ titleRaw: f.properties.titleRaw || f.properties.raw || null };
13845
+ charts.push({ c, r, layout, size, count: pie.n, partCounts: pie.counts, nParts: values.length, key, properties });
13846
+ }
13847
+ }
13848
+ for (const f of agg ? [] : this._activeFeatures || this.features) {
12425
13849
  const c = f.geometry && f.geometry.coordinates;
12426
13850
  if (!c || typeof c[0] !== 'number') continue;
12427
13851
  const [lng, lat] = c;
@@ -12442,16 +13866,19 @@ in vec4 vPieLineColor;
12442
13866
  // 1-field pie its class (cat) like a range-classed bubble
12443
13867
  const properties = multi ? { value: values[0], parts: values, raw }
12444
13868
  : { value: values[0], cat: this.partsA ? this._resolvePartsClass(values[0]) : 0, raw };
12445
- charts.push({ c, r, layout, size, key, properties });
13869
+ charts.push({ c, r, layout, size, nParts: values.length, key, properties });
12446
13870
  }
12447
13871
  // flat sorts the charts before drawing (maptheme.js 16774-16870),
12448
13872
  // ascending — the last drawn on top —, SORT|UP descending; not with
12449
- // NOSRT, NOSIZE (unless SORT) or when all values are equal
13873
+ // NOSRT, NOSIZE (unless SORT) or when all values are equal, nor a
13874
+ // CATEGORICAL pie without AGGREGATE, size field or SORT
12450
13875
  if (((this._valueMin !== this._valueMax) || this.binding.size) && !has(/NOSRT/)
12451
- && (!has(/NOSIZE/) || has(/\bSORT\b/))) {
13876
+ && (!has(/NOSIZE/) || has(/\bSORT\b/))
13877
+ && (!has(/CATEGORICAL/) || has(/AGGREGATE/) || this.binding.size || has(/\bSORT\b/))) {
12452
13878
  const down = has(/\bSORT\b/) && has(/\bUP\b/);
12453
13879
  charts.sort((a, b) => (down ? b.key - a.key : a.key - b.key) || 0);
12454
13880
  }
13881
+ applyFlatMaxCharts(charts, st);
12455
13882
  // align / offsetx / offsety (flatChartAlignOffset) — flat's PIE
12456
13883
  // ptNull is (0, r + 5 units); the chart's half size taken as r, as
12457
13884
  // for BUBBLE (ASSUMPTION: not measured on flat for PIE)
@@ -12508,6 +13935,74 @@ in vec4 vPieLineColor;
12508
13935
  return layers.concat(this._buildPieTextLayers(charts, zoom, liveZoom, unit, sliceRgb, has));
12509
13936
  }
12510
13937
 
13938
+ // the items of an AGGREGATE / CATEGORICAL pie within bbox: its records
13939
+ // (_buildAggregationIndex) added up per item by aggregateOnGrid — with
13940
+ // AGGREGATE on the theme's aggregation grid (gridwidth, aggregation …,
13941
+ // see _ensureClusterIndices; none: per position), else per position
13942
+ // (flat sums a CATEGORICAL theme's records of one shape by category
13943
+ // without AGGREGATE too). Each item's properties.pie: flatPieAccumulate's.
13944
+ _pieAggregatedItems(zoom, bbox) {
13945
+ if (!this._featuresByCategory) return [];
13946
+ if (this.flags.has('AGGREGATE')) {
13947
+ this._ensureClusterIndices(zoom);
13948
+ return this._clusterIndices[0].getClusters(bbox, this._clusterUsesFixedZoom ? GRIDWIDTH_METERS_REFERENCE_ZOOM : zoom);
13949
+ }
13950
+ const feats = this._featuresByCategory[0];
13951
+ if (!this._pieExactIndex || this._pieExactIndex.features !== feats) {
13952
+ this._pieExactIndex = new GridAggregateIndex(null, this.flags, undefined, null).load(feats);
13953
+ }
13954
+ return this._pieExactIndex.getClusters(bbox, zoom);
13955
+ }
13956
+
13957
+ // an aggregated pie item's parts (a CATEGORICAL pie with style.values:
13958
+ // one per category) and size (flatPieItemValues)
13959
+ _pieItemValues(pie) {
13960
+ const t = this.flags.typeString != null ? this.flags.typeString : [...this.flags].join('|');
13961
+ const nParts = this._pieCategorical ? (this._pieExplicitValues ? this.categoryLabels.length : null) : (this._pieFields || []).length;
13962
+ return flatPieItemValues(pie, t, { categorical: !!this._pieCategorical, nParts });
13963
+ }
13964
+
13965
+ // flat's statistics of an aggregated pie theme (maptheme.js
13966
+ // 11180-11241), over every item (at this zoom when a grid aggregates):
13967
+ // nMin / nMax over the items' sizes (CATEGORICAL or a size field), else
13968
+ // over their parts; nMaxSize the biggest size; the classes of a
13969
+ // value-field pie (partsA) over the items' parts, as for a per-record
13970
+ // pie (_preparePie). Sets the theme's _valueMin / _valueMax / partsA.
13971
+ _pieAggregateStats(zoom) {
13972
+ const feats = this._featuresByCategory && this._featuresByCategory[0];
13973
+ const zkey = this.flags.has('AGGREGATE') ? zoom : null;
13974
+ const cache = this._pieStatsCache;
13975
+ if (cache && cache.feats === feats && cache.zoom === zkey) return cache;
13976
+ let nMin = Infinity, nMax = -Infinity, maxSize = -Infinity;
13977
+ const pooled = [];
13978
+ const bySize = !!this._pieCategorical || !!(this.binding.size && this.binding.size !== '$item$');
13979
+ for (const f of this._pieAggregatedItems(zoom, [-180, -90, 180, 90])) {
13980
+ const { parts, size } = this._pieItemValues(f.properties.pie);
13981
+ if (isFinite(size) && size > maxSize) maxSize = size;
13982
+ for (const v of parts) if (isFinite(v)) pooled.push(v);
13983
+ if (bySize) {
13984
+ if (isFinite(size)) { if (size < nMin) nMin = size; if (size > nMax) nMax = size; }
13985
+ } else {
13986
+ for (const v of parts) if (isFinite(v)) { if (v < nMin) nMin = v; if (v > nMax) nMax = v; }
13987
+ }
13988
+ }
13989
+ if (nMin <= nMax) {
13990
+ this._valueMin = nMin;
13991
+ this._valueMax = nMax;
13992
+ if (!this._pieCategorical && pooled.length) {
13993
+ const nParts = Math.trunc(styleNum(this.style.classes)) || colorSchemeClassCount(this.style.colorscheme) || DEFAULT_RANGE_CLASSES;
13994
+ this.partsA = flatRangeParts(pooled, nMin, nMax, nParts, this.flags, styleNum(this.style.rangecentervalue));
13995
+ // a 1-field pie's legend rows are these classes (in place, as
13996
+ // _ensureAggregateStats: the legend holds the array)
13997
+ if (!this._multiFields && Array.isArray(this.categoryLabels)) {
13998
+ this.categoryLabels.splice(0, this.categoryLabels.length, ...this.partsA.map(p => `${this._formatTooltipValue(p.min)} - ${this._formatTooltipValue(p.max)}`));
13999
+ }
14000
+ }
14001
+ }
14002
+ this._pieStatsCache = { feats, zoom: zkey, maxSize: maxSize > -Infinity ? maxSize : undefined };
14003
+ return this._pieStatsCache;
14004
+ }
14005
+
12511
14006
  // the value texts of _buildPieLayers (see there)
12512
14007
  _buildPieTextLayers(charts, zoom, liveZoom, unit, sliceRgb, has) {
12513
14008
  const st = this.style;
@@ -12518,12 +14013,16 @@ in vec4 vPieLineColor;
12518
14013
  const noBreaks = flatNoBreaks(this._valueMin, this._valueMax);
12519
14014
  const dec = explicitValueDecimals(st);
12520
14015
  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);
14016
+ // COUNT on an aggregated pie: the part's record count (flat's nCountA)
14017
+ const countText = has(/COUNT/);
14018
+ const partText = (sl, ch) => {
14019
+ if (sl.complement) return Math.round(sl.percent * 10) / 10 + ' % ';
14020
+ const v = countText && ch && ch.partCounts ? ch.partCounts[sl.i] || 0 : sl.value;
14021
+ return fmt(v, dec != null ? dec : (v < 1 ? 2 : 0)) + partUnit;
14022
+ };
12523
14023
  const valuesOn = has(/VALUES/) && !has(/STACKED/) && !valuesHiddenByScale(st, zoom);
12524
14024
  const textOverride = st.valuecolor || st.textcolor;
12525
14025
  const inline = [], boxed = [], centerTexts = [], lines = [];
12526
- const nFields = (this._pieFields || []).length;
12527
14026
  for (const ch of charts) {
12528
14027
  const o = ch.off;
12529
14028
  // CENTER: the center part's value (maptheme.js 20402-20410)
@@ -12533,10 +14032,10 @@ in vec4 vPieLineColor;
12533
14032
  const fontSize = Math.min(ch.centerR * 0.8, ch.centerR * (3.3 / Math.max(1, text.length))) * valueScale;
12534
14033
  const rgb = (this.categoryColorsRgb || [])[ch.layout.center.i] || [128, 128, 128];
12535
14034
  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)) {
14035
+ } else if (!ch.layout.center && has(/CENTERVALUE/) && (ch.count || (ch.size !== undefined && !isNaN(ch.size)))) {
12537
14036
  // flat prints String(nCount || nSize): a record's item has no
12538
- // nCount, so its size value
12539
- const text = String(ch.size);
14037
+ // nCount, so its size value; an aggregated item its record count
14038
+ const text = String(ch.count || ch.size);
12540
14039
  const fontSize = Math.min(ch.r * 0.5, ch.r * (2 / Math.max(1, text.length)));
12541
14040
  if (fontSize > VALUES_MIN_FONT_PX) {
12542
14041
  centerTexts.push({ position: ch.c, text, fontSize, off: o, bold: false,
@@ -12544,20 +14043,20 @@ in vec4 vPieLineColor;
12544
14043
  }
12545
14044
  }
12546
14045
  if (!valuesOn) continue;
12547
- if (nFields > 1 || has(/NOINLINETEXT/)) {
14046
+ if (ch.nParts > 1 || has(/NOINLINETEXT/)) {
12548
14047
  const fontSize = 5 * unit * valueScale;
12549
14048
  if (fontSize <= VALUES_MIN_FONT_PX) continue;
12550
14049
  for (const lab of pieValueLabelLayout(ch.layout.slices, ch.r, fontSize, unit, (this.partsA || []).length)) {
12551
14050
  const bg = has(/VALUEBACKGROUND/) ? sliceRgb(lab.slice) : [255, 255, 255];
12552
14051
  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,
14052
+ boxed.push({ position: ch.c, text: partText(lab.slice, ch), fontSize, anchor: lab.anchor,
12554
14053
  // the text's baseline → its center (arial: 0.35 font above)
12555
14054
  off: [o[0] + lab.x, o[1] + lab.y - 0.35 * fontSize], color: textRgb, bg: [...bg.slice(0, 3), 128] });
12556
14055
  lab.segments.forEach(sg => lines.push({ c: ch.c, o, sg }));
12557
14056
  }
12558
14057
  } else if (ch.layout.slices.length) {
12559
14058
  const sl = ch.layout.slices[ch.layout.slices.length - 1];
12560
- const text = partText(sl);
14059
+ const text = partText(sl, ch);
12561
14060
  const fontSize = Math.min(ch.r * 0.8, ch.r * (3.4 / Math.max(1, text.length)));
12562
14061
  const color = textOverride ? hexOrNamedToRgb(textOverride) : flatChartTextRgb(sliceRgb(sl));
12563
14062
  // baseline at −0.6 R + R / 2 + 0.45 font (maptheme.js 20434)
@@ -12611,6 +14110,594 @@ in vec4 vPieLineColor;
12611
14110
  return layers;
12612
14111
  }
12613
14112
 
14113
+ // CHART|BAR / BARS (_isBarChart) — flat's bar chart per item
14114
+ // (flatBarLayout, maptheme.js drawChart's BAR branch): its items and
14115
+ // theme statistics as a pie's (one per record, or AGGREGATE /
14116
+ // CATEGORICAL items via _pieAggregatedItems / _pieAggregateStats), a bar
14117
+ // per value field or category:
14118
+ // - size: SIZE by the size field (or the values' sum) with sizepow, the
14119
+ // width chart size / parts · 0.66, the length value · 30 u / nMax
14120
+ // (normalsizevalue, else the theme's max) · rangescale, with SIZE
14121
+ // and several parts / sizer (SIZEP4: sizer²), COMPRESS / EXPAND,
14122
+ // POINTER · 1.2
14123
+ // - HORZ: the bars to the right, the first on top; else upward side
14124
+ // by side; STACKED: one column; SORT, UP, CENTER, SPACED, THIN /
14125
+ // THICK, NOZERO, NONEGATIVE, ONLYNEGATIVE, LEFT
14126
+ // - colors: the part's (categories, fields), a single part or
14127
+ // SEQUENCE the value's class; outline linecolor (black) at
14128
+ // linewidth · 0.25 (0.2 u) and opacity 1 (0.1 without linewidth);
14129
+ // a negative bar fill 0.3, outlined in its color; fillopacity
14130
+ // - POINTER / ARROW (SMALLARROW drawn as ARROW): flat's pointer path,
14131
+ // negative values pointing the other way (fadenegative fill, the
14132
+ // outline lighter or darker), SIZE: its width by the value;
14133
+ // OFFSETMEAN / OFFSETMEDIAN / DEVIATION: the value's deviation from
14134
+ // the part's mean / median (in %) or in standard deviations
14135
+ // - VALUES: the value (valuedecimals, else 1 decimal with a max ≤ 1)
14136
+ // and units, POINTER a "+", STACKED its share; beyond the bar's end
14137
+ // (in a white box unless HORZ; VALUEBACKGROUND / CTEXT: the bar's
14138
+ // color), inside it when it fits (without SIZE, THIN, DTEXT …),
14139
+ // STACKED beside the column on leader lines; flat's values color
14140
+ // #798697 (textcolor), negative red (valuecolor); DTEXT and a sized
14141
+ // POINTER fade small values
14142
+ // - BOX / TITLE (flatChartBox): a box around chart and title, the
14143
+ // item's title above it — boxcolor (white) at boxopacity, bordercolor
14144
+ // (#bbbbbb) at 0.3, borderwidth · 0.2 · w / 30; title 5 u ·
14145
+ // textscale in textcolor (#888888) over a white halo
14146
+ // - draw order: flat's (_buildPieLayers); each chart's box under its
14147
+ // bars, a later chart over an earlier one
14148
+ // Not drawn (BAR_INERT_FLAGS, a console note): see flatBarLayout.
14149
+ // Known divergence: every value text lies above every bar (flat draws
14150
+ // them in the chart's own group); the leader lines are drawn in map
14151
+ // coordinates (approximate on the globe).
14152
+ _buildBarLayers(zoom, bbox, liveZoom = zoom, globeCenter = null) {
14153
+ const st = this.style;
14154
+ const t = this.flags.typeString != null ? this.flags.typeString : [...this.flags].join('|');
14155
+ const has = re => re.test(t);
14156
+ const agg = this._isAggregatedPie();
14157
+ const cat = !!this._pieCategorical;
14158
+ const fields = cat ? null : (this._pieFields || String(this.binding.value || '').split('|'));
14159
+ const stats = agg ? this._pieAggregateStats(zoom) : null;
14160
+ const unit = objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(st.scale) || 1);
14161
+ const sizeField = !!(this.binding.size && this.binding.size !== '$item$');
14162
+ const nsv = styleNum(st.normalsizevalue);
14163
+ const colors = this.categoryColorsRgb || [];
14164
+ const num = v => { const x = styleNum(v); return isNaN(x) ? undefined : x; };
14165
+ const measure = (text, font) => titleTextWidth(text, font);
14166
+ // the charts' values (and, for OFFSETMEAN …, every item's)
14167
+ const items = [];
14168
+ const itemValues = f => {
14169
+ if (agg) {
14170
+ const { parts, size } = this._pieItemValues(f.properties.pie);
14171
+ return { values: parts, size };
14172
+ }
14173
+ const raw = f.properties || {};
14174
+ return { values: fields.map(k => flatNumber(raw[k], this.flags)), size: sizeField ? parseFloat(raw[this.binding.size]) : undefined };
14175
+ };
14176
+ const all = agg ? this._pieAggregatedItems(zoom, [-180, -90, 180, 90]) : (this._activeFeatures || this.features);
14177
+ // flat's per-part mean, median (getMeanMedianQuantile) and standard
14178
+ // deviation over every item
14179
+ let means, medians, devs;
14180
+ if (has(/OFFSETMEAN|OFFSETMEDIAN|DEVIATION/)) {
14181
+ const cache = this._barStatsCache;
14182
+ if (cache && cache.all === all && (!agg || cache.zoom === zoom)) ({ means, medians, devs } = cache);
14183
+ else {
14184
+ const cols = [];
14185
+ for (const f of all) itemValues(f).values.forEach((v, k) => { (cols[k] ||= []).push(v); });
14186
+ means = cols.map(c => c.reduce((a, v) => a + v, 0) / c.length);
14187
+ medians = cols.map(c => c.slice().sort((a, b) => a - b)[Math.round(c.length / 2)]);
14188
+ devs = cols.map((c, k) => Math.sqrt(c.reduce((a, v) => a + (v - means[k]) * (v - means[k]), 0) / c.length));
14189
+ this._barStatsCache = { all, zoom, means, medians, devs };
14190
+ }
14191
+ }
14192
+ const lo = {
14193
+ themeMin: this._valueMin, themeMax: this._valueMax,
14194
+ maxSize: stats ? stats.maxSize : this._maxSizeValue, sizeField,
14195
+ normalSizeValue: nsv > 0 ? nsv : undefined, sizePow: resolveSizePow(st, this.flags),
14196
+ rangeScale: num(st.rangescale), valueScale: num(st.valuescale), units: st.units ? String(st.units) : '',
14197
+ valueDecimals: explicitValueDecimals(st), noBreaks: flatNoBreaks(this._valueMin, this._valueMax),
14198
+ gridX: num(st.gridx), nClasses: (this.partsA || []).length, classMax: (this.partsA || []).map(p => p.max),
14199
+ nColors: colors.length, fillOpacity: num(st.fillopacity), lineWidth: num(st.linewidth),
14200
+ fadeNegative: num(st.fadenegative), fadeValuePow: num(st.fadevaluepow),
14201
+ textColor: st.textcolor || undefined, valueColor: st.valuecolor || undefined,
14202
+ hideValues: valuesHiddenByScale(st, zoom), means, medians, devs, measure
14203
+ };
14204
+ const inView = c => c[0] >= bbox[0] && c[0] <= bbox[2] && c[1] >= bbox[1] && c[1] <= bbox[3]
14205
+ && (!globeCenter || isOnVisibleHemisphere(c[0], c[1], globeCenter));
14206
+ const charts = [];
14207
+ for (const f of agg ? this._pieAggregatedItems(zoom, bbox) : all) {
14208
+ const c = f.geometry && f.geometry.coordinates;
14209
+ if (!c || typeof c[0] !== 'number' || !inView(c)) continue;
14210
+ const { values, size } = itemValues(f);
14211
+ const layout = flatBarLayout(values, t, Object.assign({ itemSize: size }, lo));
14212
+ if (!layout || (!layout.bars.length && !layout.texts.length)) continue;
14213
+ const sum = values.reduce((a, v) => a + (isFinite(v) ? v : 0), 0);
14214
+ let properties;
14215
+ if (agg) {
14216
+ const pie = f.properties.pie;
14217
+ const raws = f.properties.raws || (f.properties.raw ? [f.properties.raw] : []);
14218
+ properties = { counts: values, total: sum, recordCounts: [pie.n], raws, firstRaw: raws[0] || null,
14219
+ titleRaw: f.properties.titleRaw || f.properties.raw || null };
14220
+ } else {
14221
+ const raw = f.properties || {};
14222
+ properties = values.length > 1 ? { value: values[0], parts: values, raw }
14223
+ : { value: values[0], cat: this.partsA ? this._resolvePartsClass(values[0]) : 0, raw };
14224
+ }
14225
+ // flat's draw-order key (as for pies, maptheme.js 16812-16860)
14226
+ const key = agg ? (this.binding.size || (has(/AGGREGATE/) && has(/SUM/)) ? size : values[0])
14227
+ : (this.binding.size ? size : has(/SIZE/) ? sum : values[0]);
14228
+ charts.push({ c, layout, key, properties });
14229
+ }
14230
+ if (((this._valueMin !== this._valueMax) || this.binding.size) && !has(/NOSRT/)
14231
+ && (!has(/NOSIZE/) || has(/\bSORT\b/))
14232
+ && (!has(/CATEGORICAL/) || has(/AGGREGATE/) || this.binding.size || has(/\bSORT\b/))) {
14233
+ const down = has(/\bSORT\b/) && has(/\bUP\b/);
14234
+ charts.sort((a, b) => (down ? b.key - a.key : a.key - b.key) || 0);
14235
+ }
14236
+ applyFlatMaxCharts(charts, st);
14237
+
14238
+ const boxShown = flatFlag(this.flags, 'BOX') && !boxHiddenByScale(st, liveZoom);
14239
+ const titleShown = boxShown && flatFlag(this.flags, 'TITLE') && !!this.binding.title;
14240
+ const titleFont = 5 * (num(st.textscale) || num(st.valuescale) || 1);
14241
+ const boxOpacity = isNaN(styleNum(st.boxopacity)) ? 1 : styleNum(st.boxopacity);
14242
+ const borderStyle = String(st.borderstyle || '');
14243
+ const boxFill = [...hexOrNamedToRgb(st.boxcolor || 'white').slice(0, 3), Math.round(255 * boxOpacity)];
14244
+ const boxLine = borderStyle === 'none' ? [0, 0, 0, 0]
14245
+ : [...hexOrNamedToRgb(styleLineColor(st.bordercolor) || '#bbbbbb').slice(0, 3), Math.round(255 * (/^(solid|dotted|dashed)$/.test(borderStyle) ? 1 : 0.3))];
14246
+ const borderWidth = num(st.borderwidth) || 1;
14247
+ const lc = st.linecolor != null && st.linecolor !== '' ? styleLineColor(st.linecolor) : null;
14248
+ const lineRgbOf = (spec, rgb) => (spec.rgb === 'color' ? rgb : spec.rgb === 'low' ? flatDerivateRgb(rgb, 0.7)
14249
+ : spec.rgb === 'high' ? flatDerivateRgb(rgb, 1.3) : (lc && lc !== 'none' ? hexOrNamedToRgb(lc) : [0, 0, 0]));
14250
+ const align = !!(st.align || styleNum(st.offsetx) || styleNum(st.offsety));
14251
+
14252
+ const shapes = [], texts = [], titles = [], lines = [];
14253
+ // a shape in chart units → the instance (px; the circle around it)
14254
+ const shape = (ch, s, fill, line, width, props, noPick) => {
14255
+ const L = s.len + s.band + s.head;
14256
+ const d = [[0, -1], [1, 0], [0, 1], [-1, 0]][s.dir];
14257
+ 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];
14258
+ const lw = line[3] > 0 ? width * unit : 0;
14259
+ shapes.push({
14260
+ position: ch.c, off: [cx, -cy], properties: props, fill, line,
14261
+ r: Math.hypot(s.w2 * unit, L * unit / 2) + lw + 2,
14262
+ shape: [s.w2 * unit, s.sw * unit, s.len * unit, s.band * unit],
14263
+ shape2: [s.head * unit, s.dir + (noPick ? 4 : 0), lw, L * unit / 2]
14264
+ });
14265
+ };
14266
+ for (const ch of charts) {
14267
+ const L = ch.layout;
14268
+ let box = null;
14269
+ if (boxShown) {
14270
+ const title = titleShown ? this._itemTitle(ch.properties) : '';
14271
+ box = flatChartBox(L.extent, { margin: num(st.boxmargin) || 2, titleFont, title: title ? title + (st.titleunits || '') : '',
14272
+ bottomTitle: flatFlag(this.flags, 'BOTTOMTITLE'), alignRight: /right/.test(String(st.align || '')), measure });
14273
+ }
14274
+ const ext = box ? box.rect : L.extent;
14275
+ // align / offsetx / offsety (flatChartAlignOffset), the chart's
14276
+ // half height as its size (ASSUMPTION: not measured on flat for BAR)
14277
+ ch.off = align ? flatChartAlignOffset(st, { unit, symbolScale: 1, half: (ext[3] - ext[1]) / 2 * unit }) : [0, 0];
14278
+ if (box) {
14279
+ const r = box.rect;
14280
+ 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 },
14281
+ boxFill, boxLine, borderWidth * 0.2 * box.width / 30, null, true);
14282
+ if (box.title) titles.push({ position: ch.c, text: box.title.lines.join('\n'), font: box.title.font * unit, anchor: box.title.anchor,
14283
+ off: [box.title.x * unit + ch.off[0], (box.title.baseline - 0.8 * box.title.font) * unit + ch.off[1]] });
14284
+ }
14285
+ for (const b of L.bars) {
14286
+ const rgb = colors[b.cls] || [128, 128, 128];
14287
+ const a = Math.round(255 * b.fill.opacity * b.opacity);
14288
+ const line = [...lineRgbOf(b.line, rgb).slice(0, 3), Math.round(255 * b.line.opacity * b.opacity)];
14289
+ shape(ch, b, [...rgb.slice(0, 3), a], line, b.line.width, ch.properties, false);
14290
+ }
14291
+ for (const tx of L.texts) {
14292
+ const font = tx.font * unit;
14293
+ if (font <= VALUES_MIN_FONT_PX) continue;
14294
+ const rgb = colors[tx.cls] || [128, 128, 128];
14295
+ const color = tx.color === 'value' ? [0x79, 0x86, 0x97] : tx.color === 'contrast' ? flatChartTextRgb(rgb) : hexOrNamedToRgb(tx.color);
14296
+ // the baseline's start → the text's middle-left (arial: 0.35 font
14297
+ // above the baseline), perpendicular to its reading direction
14298
+ const r = tx.angle * Math.PI / 180;
14299
+ const mx = tx.x * unit - 0.35 * font * Math.sin(r), my = tx.y * unit - 0.35 * font * Math.cos(r);
14300
+ texts.push({ position: ch.c, text: tx.text, font, angle: tx.angle, off: [mx + ch.off[0], my + ch.off[1]],
14301
+ color: [...color.slice(0, 3), Math.round(255 * Math.max(0, Math.min(1, tx.opacity)))],
14302
+ bg: tx.bg === 'bar' ? [...rgb.slice(0, 3), 128] : tx.bg === 'white' ? [254, 254, 255, 255] : null });
14303
+ }
14304
+ 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] });
14305
+ }
14306
+
14307
+ const layers = [new BarShapeLayer({
14308
+ id: `ix-bar-${this.name}`,
14309
+ data: shapes, pickable: true,
14310
+ getPosition: d => d.position,
14311
+ getRadius: d => d.r,
14312
+ getPixelOffset: d => d.off,
14313
+ getFillColor: d => d.fill,
14314
+ getLineColor: d => d.line,
14315
+ getBarShape: d => d.shape,
14316
+ getBarShape2: d => d.shape2,
14317
+ radiusUnits: 'pixels', billboard: true, stroked: false, filled: true, antialiasing: true,
14318
+ parameters: ICON_LAYER_GLOBE_PARAMETERS
14319
+ })];
14320
+ if (lines.length) {
14321
+ const feature = d => ({ type: 'Feature', properties: {}, geometry: { type: 'LineString', coordinates: [
14322
+ pixelOffsetLngLat(d.c, [d.o[0] + d.sg[0], d.o[1] + d.sg[1]], liveZoom),
14323
+ pixelOffsetLngLat(d.c, [d.o[0] + d.sg[2], d.o[1] + d.sg[3]], liveZoom)] } });
14324
+ const fc = { type: 'FeatureCollection', features: lines.map(feature) };
14325
+ [['bg', [255, 255, 255, Math.round(255 * 0.2)], 0.15], ['fg', [0x88, 0x88, 0x88, 255], 0.05]].forEach(([k, color, w]) => {
14326
+ layers.push(new GeoJsonLayer({
14327
+ id: `ix-bar-lines-${k}-${this.name}`, data: fc, pickable: false,
14328
+ stroked: true, filled: false, getLineColor: color, getLineWidth: w * unit, lineWidthUnits: 'pixels'
14329
+ }));
14330
+ });
14331
+ }
14332
+ const textBase = {
14333
+ getPosition: d => d.position, getText: d => d.text, getSize: d => d.font,
14334
+ getPixelOffset: d => d.off, getColor: d => d.color, getAngle: d => d.angle,
14335
+ sizeUnits: 'pixels', fontFamily: 'arial', characterSet: 'auto', fontWeight: 'normal',
14336
+ getTextAnchor: 'start', getAlignmentBaseline: 'center', pickable: false,
14337
+ parameters: ICON_LAYER_GLOBE_PARAMETERS
14338
+ };
14339
+ const boxed = texts.filter(d => d.bg), plain = texts.filter(d => !d.bg);
14340
+ if (plain.length) layers.push(new TextLayer(Object.assign({}, textBase, { id: `ix-bar-values-${this.name}`, data: plain })));
14341
+ if (boxed.length) {
14342
+ // flat's box (createTextLabel): 0.3 font left of the text, 0.4 font
14343
+ // right of it, 0.1 font taller
14344
+ layers.push(new TextLayer(Object.assign({}, textBase, {
14345
+ id: `ix-bar-values-boxed-${this.name}`, data: boxed,
14346
+ background: true, getBackgroundColor: d => d.bg,
14347
+ // one padding per layer: of the texts' mean font
14348
+ backgroundPadding: [0.3, 0, 0.4, 0.1].map(k => k * boxed.reduce((a, d) => a + d.font, 0) / boxed.length)
14349
+ })));
14350
+ }
14351
+ if (titles.length) {
14352
+ layers.push(new TextLayer({
14353
+ id: `ix-bar-titles-${this.name}`, data: titles, pickable: false,
14354
+ getPosition: d => d.position, getText: d => d.text, getSize: d => d.font,
14355
+ sizeUnits: 'pixels', fontFamily: 'arial', characterSet: 'auto', lineHeight: 1,
14356
+ getColor: [...hexOrNamedToRgb(st.textcolor || '#888888').slice(0, 3), 255],
14357
+ fontSettings: { sdf: true }, outlineWidth: 1 / 7, outlineColor: [255, 255, 255, 128],
14358
+ getTextAnchor: d => d.anchor, getAlignmentBaseline: 'top',
14359
+ getPixelOffset: d => d.off,
14360
+ parameters: ICON_LAYER_GLOBE_PARAMETERS
14361
+ }));
14362
+ }
14363
+ return layers;
14364
+ }
14365
+
14366
+ // CHART|VECTOR / CHART|BEZIER — the theme's classes as flat has them:
14367
+ // with CATEGORICAL (or EXACT) the categories, flat's string values
14368
+ // trimmed (getStringValueIndex, maptheme.js 9204), its partsA one
14369
+ // [k, k + 1e-9] per category (the last + 0.001) and the colorscheme's
14370
+ // colors per category; the items' statistics follow in _vectorItems
14371
+ _prepareVector() {
14372
+ const st = this.style;
14373
+ this._vectorCategorical = this.flags.has('CATEGORICAL') && !!this.binding.value;
14374
+ if (this._vectorCategorical) {
14375
+ const explicit = Array.isArray(st.values) ? st.values.map(v => String(v).trim()) : null;
14376
+ const index = new Map();
14377
+ if (explicit) explicit.forEach((v, i) => { if (!index.has(v)) index.set(v, i); });
14378
+ else {
14379
+ for (const f of this.features) {
14380
+ const v = f.properties[this.binding.value];
14381
+ if (v == null) continue;
14382
+ const k = String(v).trim();
14383
+ if (k !== '' && !index.has(k)) index.set(k, index.size);
14384
+ }
14385
+ this.categoryLabels = Array.from(index.keys());
14386
+ const explicitLabel = Array.isArray(st.label) ? st.label.map(String) : null;
14387
+ this.categoryDisplayLabels = explicitLabel && explicitLabel.length === this.categoryLabels.length ? explicitLabel : this.categoryLabels;
14388
+ this.categoryColorsRgb = resolveClassColors(st.colorscheme, this.categoryDisplayLabels, st.classes);
14389
+ }
14390
+ this._vectorCategoryIndex = index;
14391
+ // the legend counts records by their raw value
14392
+ const byLabel = new Map(index);
14393
+ for (const f of this.features) {
14394
+ const v = f.properties[this.binding.value];
14395
+ if (v != null && !byLabel.has(v) && index.has(String(v).trim())) byLabel.set(v, index.get(String(v).trim()));
14396
+ }
14397
+ this.categoryIndexByLabel = byLabel;
14398
+ const n = this.categoryLabels.length;
14399
+ this.partsA = this.categoryLabels.map((l, i) => ({ min: i + 1, max: i + 1 + (i === n - 1 ? 0.001 : 0) + 1e-9 }));
14400
+ this._rangeClassed = false;
14401
+ }
14402
+ // colorfield (flat 12529-12597, 12644-12652): an item's color is its
14403
+ // color value's — the colorvalues, else values, else the items'
14404
+ // values in order — n-th color of the colorscheme realized for n
14405
+ const colorField = st.colorfield || (this.binding && this.binding.color);
14406
+ this._vectorColorField = colorField || null;
14407
+ if (colorField) {
14408
+ const list = Array.isArray(st.colorvalues) ? st.colorvalues.map(String)
14409
+ : Array.isArray(st.values) ? st.values.map(String)
14410
+ : [...new Set(this.features.map(f => String(f.properties[colorField])))];
14411
+ const colors = resolveClassColors(st.colorscheme, list, st.classes) || [];
14412
+ this._vectorColorOf = new Map(list.map((v, i) => [v, colors[i]]));
14413
+ }
14414
+ this._vectorItemsOf = null;
14415
+ }
14416
+
14417
+ // the records of flatVectorItems from this theme's (active) features:
14418
+ // the second position joined by key (joinChartPositions: f._pos2) or
14419
+ // read from lat/lon fields ("lat|lon")
14420
+ _vectorRecords(features) {
14421
+ const b = this.binding;
14422
+ const l2 = b.lookup2 != null ? String(b.lookup2) : '';
14423
+ const latlon2 = l2.includes('|') ? l2.split('|') : null;
14424
+ const sizeField = b.size && b.size !== '$item$' ? b.size : null;
14425
+ const records = [];
14426
+ for (const f of features) {
14427
+ const c = f.geometry && f.geometry.coordinates;
14428
+ if (!c || typeof c[0] !== 'number') continue;
14429
+ const p = f.properties || {};
14430
+ let p2 = null;
14431
+ if (latlon2) {
14432
+ const lat = flatScanValue(p[latlon2[0]]), lng = flatScanValue(p[latlon2[1]]);
14433
+ if (isFinite(lat) && isFinite(lng)) p2 = [lng, lat];
14434
+ } else if (f._pos2) p2 = f._pos2;
14435
+ let cat = null, value;
14436
+ if (this._vectorCategorical) {
14437
+ const v = p[b.value];
14438
+ const i = v == null ? undefined : this._vectorCategoryIndex.get(String(v).trim());
14439
+ cat = i == null ? -1 : i;
14440
+ } else if (b.value) {
14441
+ value = flatScanValue(p[b.value]);
14442
+ }
14443
+ records.push({ p1: c, p2, cat, value, size: sizeField ? flatScanValue(p[sizeField]) : 1, raw: p });
14444
+ }
14445
+ return records;
14446
+ }
14447
+
14448
+ // the items (flatVectorItems) of the active features, once per feature
14449
+ // set; also flat's statistics of them (maptheme.js 11180-11240): nMin /
14450
+ // nMax over the items' sizes (CATEGORICAL or a size field, else their
14451
+ // values), nMaxSize the biggest size
14452
+ _vectorItems() {
14453
+ const features = this._activeFeatures || this.features;
14454
+ if (this._vectorItemsOf === features) return this._vectorItemsCache;
14455
+ const t = this.flags.typeString != null ? this.flags.typeString : [...this.flags].join('|');
14456
+ const items = flatVectorItems(this._vectorRecords(features), {
14457
+ aggregate: this.flags.has('AGGREGATE'),
14458
+ vectorKey: /VECTOR/.test(t),
14459
+ multiParts: this.flags.has('CATEGORICAL'),
14460
+ max: /\bMAX\b/.test(t), min: /\bMIN\b/.test(t),
14461
+ zeroIsNotValue: /ZEROISNOTVALUE/.test(t), negativeIsNotValue: /NEGATIVEISNOTVALUE/.test(t)
14462
+ });
14463
+ let min = Infinity, max = -Infinity, maxSize = -Infinity;
14464
+ const bySize = this._vectorCategorical || !!this.binding.size;
14465
+ for (const it of items) {
14466
+ if (isFinite(it.nSize) && it.nSize > maxSize) maxSize = it.nSize;
14467
+ const v = bySize ? it.nSize : it.value;
14468
+ if (isFinite(v)) { if (v < min) min = v; if (v > max) max = v; }
14469
+ }
14470
+ if (min <= max) { this._valueMin = min; this._valueMax = max; }
14471
+ this._vectorMaxSize = maxSize;
14472
+ this._vectorItemsOf = features;
14473
+ this._vectorItemsCache = items;
14474
+ return items;
14475
+ }
14476
+
14477
+ // CHART|VECTOR / CHART|BEZIER — real ixmaps-flat's flow lines
14478
+ // (maptheme.js chartMap 18148-18435): one line per item
14479
+ // (_vectorItems) from its first to its second position — the
14480
+ // positions joined to FEATURE shapes by key (binding position /
14481
+ // position2, style lookupfield / lookupfield2) or read from lat/lon
14482
+ // fields. An item without a second position, or with a size under
14483
+ // style.minvalue, draws nothing; one is drawn when either end lies in
14484
+ // the view (flat 16590-16620).
14485
+ // - BEZIER (also VECTOR|BEZIER — the BEZIER branch comes first): a
14486
+ // cubic curve (bezierVectorLayout), width (linewidth, else 10 ·
14487
+ // (|nSize| / normalsizevalue or the biggest size)^(1/sizepow)) ·
14488
+ // unit px — unit: the object zoom × style.scale; a width ≤ 0 is not
14489
+ // drawn; stroke opacity fillopacity (else 0.3); a negative size or
14490
+ // REVERSE swaps the ends; RANDOM lowers the bow by up to 0.66 ·
14491
+ // rangescale (flat's Math.random, here a fixed hash of the item);
14492
+ // POINTER / ARROW an arrow head (arrowMarkerTriangle) at the
14493
+ // shortened end, its opacity 2 · fillopacity; GRADIENT / FADEIN a
14494
+ // gradient (fadeGradientStops) from the second color (linecolor's
14495
+ // first entry) at the start — FADEIN at 0.2 opacity — to the line
14496
+ // color at the end; DOPACITY (DOPACITYMAX) / DOPACITYMIN the line
14497
+ // and arrow opacity by the size within nMin … nMax (dopacitypow,
14498
+ // dopacityscale); DASH dashes of 50 / 5 + width units, round caps
14499
+ // - VECTOR alone: a straight line, width (linewidth, else 2 · nSize /
14500
+ // normalsizevalue / the biggest size, else 0.1) · the object zoom px
14501
+ // (no style.scale), opacity fillopacity (0.3); POINTER / ARROW a 4 ×
14502
+ // width arrow head with its tip on the second position; DASH dashes
14503
+ // of 50 / 15 units
14504
+ // - color: linecolor (its last entry), else the colorfield's color,
14505
+ // else the category's (CATEGORICAL) or the value's n-th color; a
14506
+ // record whose category is none of them is black (flat: an
14507
+ // "undefined" stroke — szNoDataColor is unset)
14508
+ // - draw order: flat's sort before drawing (16774-16880), the
14509
+ // biggest last (on top) when sized or AGGREGATE|SUM
14510
+ // - lines are densified in Web Mercator pixels (flat draws them on its
14511
+ // Mercator map), so under the globe projection they follow the
14512
+ // surface; their far-side parts are left out
14513
+ // Not drawn here (console note at type parsing, VECTOR_INERT_FLAGS):
14514
+ // VALUES (text along the curve), DYNAMICWIDTH, LONG, the DASH
14515
+ // animation; nor the second position from the category label (flat's
14516
+ // fallback without position2), a grid aggregation (gridwidth), the
14517
+ // VECTOR arrow's thin white outline, the legend's class marking.
14518
+ // Known divergence: all arrow heads lie above all lines (flat draws
14519
+ // each line with its head); a line thinner than a pixel is drawn one
14520
+ // pixel wide at its width as opacity (SVG's coverage).
14521
+ _buildVectorLayers(zoom, bbox, liveZoom = zoom, globeCenter = null) {
14522
+ const st = this.style;
14523
+ const t = this.flags.typeString != null ? this.flags.typeString : [...this.flags].join('|');
14524
+ const has = re => re.test(t);
14525
+ const bezier = has(/\bBEZIER\b/);
14526
+ if (!this.binding.lookup2) {
14527
+ if (!this._warnedNoPos2) {
14528
+ this._warnedNoPos2 = true;
14529
+ 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`);
14530
+ }
14531
+ return [];
14532
+ }
14533
+ const items = this._vectorItems();
14534
+ const oz = objectZoomFactor(liveZoom, this.mapOptions);
14535
+ const scale = styleNum(st.scale) || 1;
14536
+ const unit = oz * scale;
14537
+ const world = 512 * Math.pow(2, liveZoom);
14538
+ const toPx = ([lng, lat]) => {
14539
+ const s = Math.sin(Math.max(-85.0511, Math.min(85.0511, lat)) * Math.PI / 180);
14540
+ return [(lng + 180) / 360 * world, (0.5 - Math.log((1 + s) / (1 - s)) / (4 * Math.PI)) * world];
14541
+ };
14542
+ 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];
14543
+ const inBox = c => c && c[0] >= bbox[0] && c[0] <= bbox[2] && c[1] >= bbox[1] && c[1] <= bbox[3];
14544
+
14545
+ const sizePow = resolveSizePow(st, this.flags);
14546
+ const nsv = styleNum(st.normalsizevalue);
14547
+ const maxSize = this._vectorMaxSize;
14548
+ // style.linewidth as flat holds it: the raw style value — flat's
14549
+ // `nLineWidth + 3` (the arrow length) joins a string ("0.6" + 3 =
14550
+ // "0.63") but adds a number
14551
+ const rawLw = this._definition && this._definition.style ? this._definition.style.linewidth : st.linewidth;
14552
+ const lwNum = styleNum(st.linewidth);
14553
+ const markerSize = styleNum(st.markersize) || 1;
14554
+ const gapSize = styleNum(st.gapsize) || 20;
14555
+ const fo = styleNum(st.fillopacity);
14556
+ const groupAlpha = styleNum(st.opacity) > 0 ? Math.min(1, styleNum(st.opacity)) : 1;
14557
+ const minValue = styleNum(st.minvalue);
14558
+ const rs = styleNum(st.rangescale);
14559
+ const dopMin = has(/DOPACITYMIN/), dop = !dopMin && has(/DOPACITY/);
14560
+ const dopPow = styleNum(st.dopacitypow) || 1, dopScale = styleNum(st.dopacityscale) || 1;
14561
+ const nMin = this._valueMin, nMax = this._valueMax;
14562
+ const lineColors = st.linecolor != null && st.linecolor !== '' ? flatToArray(st.linecolor) : null;
14563
+ const lineRgb = lineColors ? hexOrNamedToRgb(String(lineColors[lineColors.length - 1]).trim()) : null;
14564
+ const colors = this.categoryColorsRgb || [];
14565
+ const colorOf = it => {
14566
+ if (lineRgb) return lineRgb;
14567
+ if (this._vectorColorField) {
14568
+ const c = this._vectorColorOf.get(String(it.raw[this._vectorColorField]));
14569
+ if (c) return c;
14570
+ }
14571
+ if (it.cat != null) return it.cat >= 0 && colors[it.cat] ? colors[it.cat] : [0, 0, 0];
14572
+ return Number.isInteger(it.value) && colors[it.value - 1] ? colors[it.value - 1] : [0, 0, 0];
14573
+ };
14574
+ const fade = has(/\bFADEIN\b/) ? 0.2 : 2;
14575
+ const gradient = bezier && has(/\bGRADIENT\b|\bFADEIN\b/);
14576
+ const arrow = has(/\bPOINTER|ARROW\b/);
14577
+ const dash = has(/\bDASH\b/);
14578
+ const dopacity = it => {
14579
+ const range = Math.pow(nMax - nMin, 1 / dopPow);
14580
+ return Math.pow(dopMin ? nMax - it.nSize : it.nSize - nMin, 1 / dopPow) / range * (fo || 1) * dopScale;
14581
+ };
14582
+
14583
+ // flat's sort before drawing (maptheme.js 16774-16880)
14584
+ let order = items;
14585
+ const sized = !!this.binding.size;
14586
+ if (((nMin !== nMax) || sized) && !has(/NOSRT/) && (!has(/NOSIZE/) || has(/\bSORT\b/))
14587
+ && (!this.flags.has('CATEGORICAL') || this.flags.has('AGGREGATE') || sized || has(/\bSORT\b/))) {
14588
+ const key = (sized || (this.flags.has('AGGREGATE') && has(/SUM/))) ? (it => it.nSize)
14589
+ : (this._vectorCategorical ? (it => (it.cat != null && it.cat >= 0 ? it.cat + 1 : 0)) : (it => it.value));
14590
+ const keyed = items.map(it => ({ it, y: key(it) })).filter(d => !isNaN(d.y));
14591
+ const down = has(/\bSORT\b/) && has(/\bUP\b/);
14592
+ keyed.sort((a, b) => (down ? b.y - a.y : a.y - b.y));
14593
+ order = keyed.map(d => d.it);
14594
+ }
14595
+ // the flows that would be drawn (as the loop below draws them), of
14596
+ // which flat keeps the biggest maxcharts
14597
+ if (Math.round(styleNum(st.maxcharts)) > 0) {
14598
+ order = applyFlatMaxCharts(order.filter(it => it.p2 && !(minValue && it.nSize < minValue) && (inBox(it.p1) || inBox(it.p2))), st);
14599
+ }
14600
+
14601
+ const paths = [], heads = [];
14602
+ const N = 32;
14603
+ const visible = c => !globeCenter || isOnVisibleHemisphere(c[0], c[1], globeCenter);
14604
+ for (const it of order) {
14605
+ let p1 = it.p1, p2 = it.p2;
14606
+ if (!p2) continue;
14607
+ if (minValue && it.nSize < minValue) continue;
14608
+ if (!inBox(p1) && !inBox(p2)) continue;
14609
+ const rgb = colorOf(it);
14610
+ let pts, end, tan, sw, headS, refX, refY, headAlpha, lineAlpha, stops = null, dashArr = null;
14611
+ if (bezier) {
14612
+ if (p1[0] === p2[0] && p1[1] === p2[1]) continue;
14613
+ const w = rawLw != null && rawLw !== '' && lwNum ? lwNum
14614
+ : 10 / Math.pow(nsv || maxSize, 1 / sizePow) * Math.pow(Math.abs(it.nSize), 1 / sizePow);
14615
+ sw = w * unit;
14616
+ if (!(sw > 0)) continue;
14617
+ if (has(/\bREVERSE\b/) || it.nSize < 0) [p1, p2] = [p2, p1];
14618
+ const a = toPx(p1), b = toPx(p2);
14619
+ const dx = b[0] - a[0], dy = b[1] - a[1];
14620
+ if (!(dx * dx + dy * dy > 0)) continue;
14621
+ let bow = rs;
14622
+ if (has(/\bRANDOM\b/)) bow -= rs * 0.66 * hashUnit(p1[0] + ',' + p1[1] + '&' + p2[0] + ',' + p2[1]);
14623
+ const gapAdd = has(/\bGAP\b/) ? gapSize : 3;
14624
+ const ll = typeof rawLw === 'string' && lwNum ? Number(rawLw + String(gapAdd)) : w + gapAdd;
14625
+ const lay = bezierVectorLayout(dx, dy, { w, ll, unit, oz, bow, t, marker: markerSize, gap: gapSize });
14626
+ pts = cubicBezierPoints(lay.start, lay.c1, lay.c2, lay.end, N);
14627
+ end = lay.end;
14628
+ tan = [lay.end[0] - lay.c2[0], lay.end[1] - lay.c2[1]];
14629
+ headS = Math.min(5, 2.5 + 5 / (w * scale)) * markerSize;
14630
+ refX = headS / 1.7; refY = headS / 2;
14631
+ lineAlpha = fo || 0.3;
14632
+ headAlpha = Math.min(1, (fo || 0.3) * 2);
14633
+ // (flat sets the computed opacity as is; one that is no number
14634
+ // — every size equal — SVG ignores: the opacity stays)
14635
+ if (dop || dopMin) { const d = dopacity(it); if (isFinite(d)) { lineAlpha = d; headAlpha = d; } }
14636
+ if (gradient) stops = fadeGradientStops(pts, lay.vx, lay.vy, fade);
14637
+ if (dash) dashArr = [50 * oz, 5 * oz + sw];
14638
+ pts = pts.map(p => [p[0] + a[0], p[1] + a[1]]);
14639
+ end = [end[0] + a[0], end[1] + a[1]];
14640
+ } else {
14641
+ const w = (rawLw != null && rawLw !== '' && lwNum) || (2 / (nsv || 1) / maxSize * it.nSize) || 0.1;
14642
+ sw = w * oz;
14643
+ const a = toPx(p1), b = toPx(p2);
14644
+ pts = [];
14645
+ 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]);
14646
+ end = b;
14647
+ tan = [b[0] - a[0], b[1] - a[1]];
14648
+ headS = 4; refX = 4; refY = 2;
14649
+ lineAlpha = fo || 0.3;
14650
+ headAlpha = Math.min(1, (fo || 0.3) * 2);
14651
+ if (dash) dashArr = [50 * oz, 15 * oz];
14652
+ }
14653
+ // a line under a pixel: one pixel at its width as opacity
14654
+ const drawW = Math.max(1, sw);
14655
+ const thin = Math.min(1, sw);
14656
+ const color2 = lineColors && lineColors.length > 1 ? hexOrNamedToRgb(String(lineColors[0]).trim()) : rgb;
14657
+ const vertexColor = i => {
14658
+ const s = stops ? stops[i] : null;
14659
+ const k = s ? s.k : 1;
14660
+ const c = s ? [0, 1, 2].map(j => Math.round(color2[j] + (rgb[j] - color2[j]) * k)) : rgb.slice(0, 3);
14661
+ return [...c, Math.round(255 * Math.min(1, lineAlpha) * (s ? s.alpha : 1) * thin * groupAlpha)];
14662
+ };
14663
+ const properties = { value: it.nSize, cat: it.cat, raw: it.raw, count: it.count,
14664
+ vector: { itemValue: it.cat != null ? (it.cat >= 0 ? it.cat + 1 : 0) : it.value } };
14665
+ // the line in map coordinates; on the globe only its visible runs
14666
+ const ll = pts.map(toLngLat);
14667
+ let run = [], runColors = [];
14668
+ const flush = () => {
14669
+ 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 });
14670
+ run = []; runColors = [];
14671
+ };
14672
+ ll.forEach((c, i) => { if (visible(c)) { run.push(c); runColors.push(vertexColor(i)); } else flush(); });
14673
+ flush();
14674
+ if (arrow && visible(toLngLat(end))) {
14675
+ const tri = arrowMarkerTriangle(end, tan[0], tan[1], sw, headS, refX, refY).map(toLngLat);
14676
+ heads.push({ type: 'Feature', properties, geometry: { type: 'Polygon', coordinates: [[...tri, tri[0]]] },
14677
+ fill: [...rgb.slice(0, 3), Math.round(255 * Math.min(1, headAlpha) * groupAlpha)] });
14678
+ }
14679
+ }
14680
+ const layers = [new PathLayer({
14681
+ id: `ix-vector-${this.name}`, data: paths, pickable: true,
14682
+ getPath: d => d.path, getColor: d => d.colors, getWidth: d => d.width,
14683
+ widthUnits: 'pixels', capRounded: dash && bezier, jointRounded: false, billboard: false,
14684
+ parameters: ICON_LAYER_GLOBE_PARAMETERS,
14685
+ ...(dash ? { getDashArray: d => d.dash, extensions: [new PathStyleExtension({ dash: true })] } : {})
14686
+ })];
14687
+ if (heads.length) {
14688
+ // the head count in the id: deck kept a same-id polygon layer's
14689
+ // larger buffers when a filter cut the heads (the Zeit page's "to
14690
+ // Africa": bow-tie triangles from the unfiltered heads' vertices).
14691
+ // Every head has the same vertex count, so an equal count — a pan —
14692
+ // reuses the layer; a changed one gets fresh buffers
14693
+ layers.push(new GeoJsonLayer({
14694
+ id: `ix-vector-heads-${this.name}-n${heads.length}`, data: { type: 'FeatureCollection', features: heads }, pickable: true,
14695
+ filled: true, stroked: false, getFillColor: d => d.fill, parameters: ICON_LAYER_GLOBE_PARAMETERS
14696
+ }));
14697
+ }
14698
+ return layers;
14699
+ }
14700
+
12614
14701
  // flat's chart position for this build (flatChartAlignOffset): a
12615
14702
  // function item → pixel offset, the same for every part of one chart
12616
14703
  // (symbol, glow, shadow, box, title, value text). unit is flat's
@@ -12926,7 +15013,8 @@ in vec4 vPieLineColor;
12926
15013
  }
12927
15014
 
12928
15015
  // pure: oneHot → see the pure-function sections above the class
12929
- _oneHot(cat, value) { return oneHot(cat, value, this.categoryLabels.length); }
15016
+ // CATEGORICAL without a value binding (count per aggregation cell) has no categoryLabels: one bucket
15017
+ _oneHot(cat, value) { return oneHot(cat, value, this.categoryLabels ? this.categoryLabels.length : 1); }
12930
15018
 
12931
15019
  // pure: groupCoLocated → see the pure-function sections above the class
12932
15020
  _groupCoLocated(clusterFeatures, zoom) { return groupCoLocated(clusterFeatures, zoom, this._clusterRadiusPx, this.categoryLabels ? this.categoryLabels.length : 1, this.flags); }
@@ -13152,7 +15240,7 @@ in vec4 vPieLineColor;
13152
15240
  show() { mapHandle(this.szMap).setThemeVisible(this.szTheme, true); return this; },
13153
15241
  hide() { mapHandle(this.szMap).setThemeVisible(this.szTheme, false); return this; },
13154
15242
  toggle() {
13155
- const rt = _globalThemeRegistry.get(this.szTheme);
15243
+ const rt = registryTheme(this.szTheme);
13156
15244
  mapHandle(this.szMap).setThemeVisible(this.szTheme, rt ? !!rt._hidden : false);
13157
15245
  return this;
13158
15246
  },
@@ -13319,7 +15407,7 @@ in vec4 vPieLineColor;
13319
15407
  // no id: flat's current theme — here the one a page's facets were
13320
15408
  // built for (its own facet script sets ixmaps.filterThemeId), else the
13321
15409
  // last chart / choropleth theme
13322
- let rt = szId != null ? _globalThemeRegistry.get(szId) : null;
15410
+ let rt = szId != null ? registryTheme(szId) : null;
13323
15411
  if (szId == null) {
13324
15412
  const pid = pageIxmaps() && pageIxmaps().filterThemeId;
13325
15413
  rt = (pid != null && _globalThemeRegistry.get(pid))
@@ -13410,7 +15498,7 @@ in vec4 vPieLineColor;
13410
15498
  // flat's ixmaps.getThemeDefinitionObj(szId): the theme's definition —
13411
15499
  // type, style (sizefield/valuefield as flat names them), meta
13412
15500
  function getThemeDefinitionObj(szId) {
13413
- const rt = _globalThemeRegistry.get(szId);
15501
+ const rt = registryTheme(szId);
13414
15502
  if (!rt) return null;
13415
15503
  const style = Object.assign({}, rt.style);
13416
15504
  if (rt.binding.size && !style.sizefield) style.sizefield = rt.binding.size;
@@ -13456,11 +15544,11 @@ in vec4 vPieLineColor;
13456
15544
  // before that block ran (there is none, currently) would just no-op
13457
15545
  // here instead of redrawing.
13458
15546
  function markThemeClass(szId, index) {
13459
- const rt = _globalThemeRegistry.get(szId);
15547
+ const rt = registryTheme(szId);
13460
15548
  if (rt) markRuntimeClass(rt, index);
13461
15549
  }
13462
15550
  function unmarkThemeClass(szId, index) {
13463
- const rt = _globalThemeRegistry.get(szId);
15551
+ const rt = registryTheme(szId);
13464
15552
  if (rt) unmarkRuntimeClass(rt, index);
13465
15553
  }
13466
15554
  // the legend's own rows mark their theme directly: its .layer() name
@@ -13755,11 +15843,12 @@ in vec4 vPieLineColor;
13755
15843
  const map = _lastMapApi.map;
13756
15844
  const center = map.getCenter();
13757
15845
  const proj = (typeof map.getProjection === 'function' && map.getProjection()) || { type: 'mercator' };
15846
+ const svgProjection = mapSvgProjection(map);
13758
15847
  return JSON.stringify({
13759
15848
  map: {
13760
- map: proj.type === 'globe' ? 'maps/svg/maps/generic/orthographic.svg' : 'maps/svg/maps/generic/mercator.svg',
15849
+ map: proj.type === 'globe' ? 'maps/svg/maps/generic/orthographic.svg' : svgProjection ? svgProjection.svg : 'maps/svg/maps/generic/mercator.svg',
13761
15850
  center: { lat: center.lat, lng: center.lng },
13762
- zoom: mapLibreToFlatZoom(map.getZoom())
15851
+ zoom: flatViewZoomOf(map)
13763
15852
  }
13764
15853
  });
13765
15854
  }
@@ -13830,6 +15919,13 @@ in vec4 vPieLineColor;
13830
15919
  htmlgui_getAttributionString: () => (_lastMapApi && _lastMapApi.getAttribution ? _lastMapApi.getAttribution() : ''),
13831
15920
  changeThemeStyle: compatChangeThemeStyle,
13832
15921
  removeTheme: compatRemoveTheme,
15922
+ // flat's ixmaps.setLocalString(orig, local) (htmlgui.js 3814 →
15923
+ // Api.setLocalString → Dictionary.replace): a page-wide translation,
15924
+ // e.g. the migration page's ISO code → country name; usable before
15925
+ // the map exists. getLocalString: its translation (flat's never
15926
+ // returns it — a missing return there)
15927
+ setLocalString: (szOrig, szLocal) => { _localStrings[szOrig] = szLocal; },
15928
+ getLocalString: (szOrig) => makeLocalText([])(szOrig),
13833
15929
  // flat's ixmaps.newTheme(title, theme, flag) — on the last built map
13834
15930
  newTheme: (title, theme, flag) => _mapHandle.newTheme(title, theme, flag),
13835
15931
  // flat's time slider: show the records of a theme (null = all themes) whose timefield lies in [min, max)
@@ -13858,16 +15954,16 @@ in vec4 vPieLineColor;
13858
15954
  // test-only: lets test/unit/*.test.mjs call pure internals directly (the
13859
15955
  // engine runs in a Node vm there); deliberately NOT on the ixmaps object
13860
15956
  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,
15957
+ 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
15958
  normalizeTheme, projectThemeToDefinition, withoutProjectCode, groupRecordCount, resolveBasemapStyleUrl, resolveMapTypeColor, LayerBuilder, LayerRuntime, MapBuilder, typeStyleNumbers, styleNum,
13863
15959
  resolveScriptUrl, isTrustedScriptUrl, loadProcessingScript, loadBrokerData, applyBrokerThemePatch, makeBrokerTheme,
13864
15960
  equalIntervalBreaks, quantileBreaks, naturalBreaks, evenStrideSample, jenksBreakValues, partsFromBreakValues, resolvePartsClass,
13865
15961
  computeAlphaStats, computeMultiFieldClasses, computeDominantStats, resolveDominantClass, computeComposeColorStats,
13866
15962
  resolveComposedColor, computeRangeClasses, colorSchemeClassCount, resolveDopacityAlpha,
13867
15963
  flatColorSweep, applyClassesToColorScheme, resolveClassColors, flatOutlierStats, parseCssColor, flatLegendLook,
13868
- flatToMapLibreZoom, mapLibreToFlatZoom, scaleDenominatorAt, resolveZoomReference, resolveAggregationPx, valuesHiddenByScale,
15964
+ flatToMapLibreZoom, mapLibreToFlatZoom, flatViewToMapLibreZoom, mapLibreToFlatViewZoom, flatSvgProjectionOf, flatSvgProjectionByOptions, normalSizeScaleOf, scaleDenominatorAt, resolveZoomReference, resolveAggregationPx, valuesHiddenByScale,
13869
15965
  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,
15966
+ applyWhereFilter, joinChartPositions, flatLookupKey, flatShapeCenter, chartHiddenByScale, glowHiddenByScale, isSymbolChart, flatChartAlignOffset, flatChartBranch, flatValueText, categoryValueRecord, flatDerivateRgb, flatBubbleLook, bubbleIconLook, flatChartTextRgb, flatGroupedValue, flatNoBreaks, featuresHiddenByScale, boxHiddenByScale, flatShadowOn, snapToAggregationGrid,
13871
15967
  drawSymbolPath, normalizeSymbolShape,
13872
15968
  flatFormatValue, themeStyleArgs, flatChangedStyleValue, explicitValueDecimals, legendDecimals,
13873
15969
  dataCacheKey, dataCacheDisabled, cachedLayerData, featuresBounds, wantsZoomToExtent, legendIsOn,