ixmaps-gl 0.2.19 → 0.2.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -8
- package/ixmaps-gl.js +2986 -74
- package/package.json +1 -1
package/ixmaps-gl.js
CHANGED
|
@@ -49,12 +49,26 @@
|
|
|
49
49
|
// (headTailBreaks); LOG/POW2/POW3 are recognized type flags but not
|
|
50
50
|
// implemented, see KNOWN_INERT_FLAGS. CHART|USER draws a page's own chart
|
|
51
51
|
// function (style.userdraw, _buildUserChartLayers), CHART|LABEL flat's
|
|
52
|
-
// value label, boxed or TEXTONLY (_buildLabelChartLayers)
|
|
52
|
+
// value label, boxed or TEXTONLY (_buildLabelChartLayers), CHART|PIE (and
|
|
53
|
+
// PIE|DONUT) a pie per record, a slice per value field, flat's sizing,
|
|
54
|
+
// CENTER part and VALUES leader labels (_buildPieLayers; 3D/VOLUME/
|
|
55
|
+
// STARBURST/RAYS/BOW variants are noted, drawn flat; AGGREGATE and
|
|
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
|
|
53
67
|
// base types are dispatched by buildDeckLayers in the real engine's own
|
|
54
68
|
// precedence order — DOT is checked first because the real engine's DOT
|
|
55
69
|
// bypasses the whole drawChart/modifier pipeline rather than being
|
|
56
|
-
// "BUBBLE with the size locked." Other real base types (QUAD,
|
|
57
|
-
//
|
|
70
|
+
// "BUBBLE with the size locked." Other real base types (QUAD, WAFFLE,
|
|
71
|
+
// and SYMBOL's own shape variants) are NOT
|
|
58
72
|
// implemented yet — adding one means a new _buildXLayers() method plus a
|
|
59
73
|
// dispatch line, not a rewrite, but each should be added deliberately
|
|
60
74
|
// (checking, per real source, which modifiers it actually shares with
|
|
@@ -82,7 +96,7 @@
|
|
|
82
96
|
// ---------------------------------------------------------------
|
|
83
97
|
// this engine's release (= package.json "version", checked by
|
|
84
98
|
// test/unit/version.test.mjs); ixmaps.glVersion, and logged once at start
|
|
85
|
-
const IXMAPS_GL_VERSION = '0.2.
|
|
99
|
+
const IXMAPS_GL_VERSION = '0.2.21';
|
|
86
100
|
|
|
87
101
|
const LIB_URLS = {
|
|
88
102
|
// Bumped 3.6.2 -> 5.x (2026-09-21, globe-projection compat fix): native
|
|
@@ -199,7 +213,213 @@
|
|
|
199
213
|
// byte-grep of the shipped file (deck.gl's own @deck.gl/maplibre
|
|
200
214
|
// overview page claims ES-modules-only, which the actual UMD bundle
|
|
201
215
|
// contradicts).
|
|
202
|
-
let IconLayer, ScatterplotLayer, GeoJsonLayer, MapLibreOverlay, TextLayer;
|
|
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;
|
|
220
|
+
|
|
221
|
+
// CHART|PIE / DONUT slices (_buildPieLayers): one instance per slice — a
|
|
222
|
+
// ScatterplotLayer circle (billboarded, pixel radius, pixel offset, the
|
|
223
|
+
// same projection path as every other chart, globe included) whose
|
|
224
|
+
// fragment shader keeps only the slice's sector of the circle and the
|
|
225
|
+
// ring outside its inner radius (DONUT, CENTER), and draws flat's slice
|
|
226
|
+
// outline along the sector's edges.
|
|
227
|
+
// getSlice: [start angle, sweep, inner radius / radius] — radians,
|
|
228
|
+
// clockwise from 12 o'clock (flat's DonutChart angles); a
|
|
229
|
+
// sweep of 2π is a full ring (no radial edges)
|
|
230
|
+
// getSliceLineWidth: the outline width in pixels (0 = none), drawn in
|
|
231
|
+
// getLineColor
|
|
232
|
+
// Why not a canvas icon per pie (as the bubbles): every pie has its own
|
|
233
|
+
// slice angles, ~7 900 distinct icons on the comuni page, far beyond
|
|
234
|
+
// what deck.gl's never-evicting icon atlas holds (ICON_ATLAS_RESET_AFTER);
|
|
235
|
+
// and not polygons in map coordinates: ~31 000 slices re-tessellated at
|
|
236
|
+
// every zoom step. Here the GPU draws each slice from 5 numbers, crisp at
|
|
237
|
+
// any device pixel ratio. Neighbouring slices split the circle by angle
|
|
238
|
+
// without anti-aliasing between them (a hard partition: no background
|
|
239
|
+
// seam shows between two slices); the circle's rim keeps
|
|
240
|
+
// ScatterplotLayer's own anti-aliasing.
|
|
241
|
+
function makePieSliceLayer(Base) {
|
|
242
|
+
class Layer extends Base {
|
|
243
|
+
getShaders() {
|
|
244
|
+
const shaders = super.getShaders();
|
|
245
|
+
const inject = Object.assign({}, shaders.inject || {});
|
|
246
|
+
inject['vs:#decl'] = (inject['vs:#decl'] || '') + `
|
|
247
|
+
in vec3 instancePieSlices;
|
|
248
|
+
in float instancePieLineWidths;
|
|
249
|
+
out vec3 vPieSlice;
|
|
250
|
+
out float vPieRadius;
|
|
251
|
+
out float vPieLineWidth;
|
|
252
|
+
out vec4 vPieLineColor;
|
|
253
|
+
`;
|
|
254
|
+
inject['vs:#main-end'] = (inject['vs:#main-end'] || '') + `
|
|
255
|
+
vPieSlice = instancePieSlices;
|
|
256
|
+
vPieRadius = outerRadiusPixels;
|
|
257
|
+
vPieLineWidth = instancePieLineWidths;
|
|
258
|
+
vPieLineColor = vec4(instanceLineColors.rgb, instanceLineColors.a * layer.opacity);
|
|
259
|
+
`;
|
|
260
|
+
inject['fs:#decl'] = (inject['fs:#decl'] || '') + `
|
|
261
|
+
in vec3 vPieSlice;
|
|
262
|
+
in float vPieRadius;
|
|
263
|
+
in float vPieLineWidth;
|
|
264
|
+
in vec4 vPieLineColor;
|
|
265
|
+
`;
|
|
266
|
+
// before the picking module's own filter (it replaces the color
|
|
267
|
+
// when picking): the discards shape the pick area too
|
|
268
|
+
inject['fs:DECKGL_FILTER_COLOR'] = (inject['fs:DECKGL_FILTER_COLOR'] || '') + `
|
|
269
|
+
{
|
|
270
|
+
const float PIE_TAU = 6.283185307179586;
|
|
271
|
+
vec2 pieP = geometry.uv * vPieRadius;
|
|
272
|
+
float pieR = length(pieP);
|
|
273
|
+
float pieInner = vPieSlice.z * vPieRadius;
|
|
274
|
+
if (pieInner > 0.0 && pieR < pieInner - 1.0) discard;
|
|
275
|
+
bool pieFull = vPieSlice.y >= PIE_TAU - 1e-5;
|
|
276
|
+
if (!pieFull) {
|
|
277
|
+
float pieA = atan(pieP.x, pieP.y);
|
|
278
|
+
float pieRel = mod(pieA - vPieSlice.x + 2.0 * PIE_TAU, PIE_TAU);
|
|
279
|
+
if (pieRel >= vPieSlice.y) discard;
|
|
280
|
+
}
|
|
281
|
+
float pieD = vPieRadius - pieR;
|
|
282
|
+
if (pieInner > 0.0) pieD = min(pieD, pieR - pieInner);
|
|
283
|
+
if (!pieFull) {
|
|
284
|
+
vec2 pieE0 = vec2(sin(vPieSlice.x), cos(vPieSlice.x));
|
|
285
|
+
vec2 pieE1 = vec2(sin(vPieSlice.x + vPieSlice.y), cos(vPieSlice.x + vPieSlice.y));
|
|
286
|
+
float pieD0 = dot(pieP, pieE0) > 0.0 ? abs(pieP.x * pieE0.y - pieP.y * pieE0.x) : pieR;
|
|
287
|
+
float pieD1 = dot(pieP, pieE1) > 0.0 ? abs(pieP.x * pieE1.y - pieP.y * pieE1.x) : pieR;
|
|
288
|
+
pieD = min(pieD, min(pieD0, pieD1));
|
|
289
|
+
}
|
|
290
|
+
// in device pixels (fwidth: CSS px per device pixel), so a hairline
|
|
291
|
+
// stays one at any device pixel ratio. Half the outline lies inside
|
|
292
|
+
// each slice (the other half in its neighbour); below a pixel it fades
|
|
293
|
+
// in by its width
|
|
294
|
+
float pieAA = max(fwidth(pieR), 1e-4);
|
|
295
|
+
float pieH = vPieLineWidth * 0.5 / pieAA;
|
|
296
|
+
float pieDD = pieD / pieAA;
|
|
297
|
+
float pieLine = pieH > 0.0 ? clamp(min(pieH + 0.5 - pieDD, 2.0 * pieH), 0.0, 1.0) : 0.0;
|
|
298
|
+
float pieRim = clamp((vPieRadius - pieR) / pieAA + 0.5, 0.0, 1.0);
|
|
299
|
+
color = vec4(mix(color.rgb, vPieLineColor.rgb, pieLine), mix(color.a, vPieLineColor.a * pieRim, pieLine));
|
|
300
|
+
if (pieInner > 0.0) color.a *= clamp((pieR - pieInner) / pieAA + 0.5, 0.0, 1.0);
|
|
301
|
+
}
|
|
302
|
+
`;
|
|
303
|
+
shaders.inject = inject;
|
|
304
|
+
return shaders;
|
|
305
|
+
}
|
|
306
|
+
initializeState() {
|
|
307
|
+
super.initializeState();
|
|
308
|
+
this.getAttributeManager().addInstanced({
|
|
309
|
+
instancePieSlices: { size: 3, accessor: 'getSlice', defaultValue: [0, 2 * Math.PI, 0] },
|
|
310
|
+
instancePieLineWidths: { size: 1, accessor: 'getSliceLineWidth', defaultValue: 0 }
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
Layer.layerName = 'PieSliceLayer';
|
|
315
|
+
// deck.gl merges a subclass's defaultProps over its base class's
|
|
316
|
+
Layer.defaultProps = {
|
|
317
|
+
getSlice: { type: 'accessor', value: [0, 2 * Math.PI, 0] },
|
|
318
|
+
getSliceLineWidth: { type: 'accessor', value: 0 }
|
|
319
|
+
};
|
|
320
|
+
return Layer;
|
|
321
|
+
}
|
|
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
|
+
}
|
|
203
423
|
|
|
204
424
|
// Cached so multiple ixmaps.Map() calls on one page (or a page that
|
|
205
425
|
// still has its own static <script> tags for these libraries) only
|
|
@@ -276,7 +496,9 @@
|
|
|
276
496
|
global.Mustache ? Promise.resolve() : loadScript(LIB_URLS.mustache),
|
|
277
497
|
[...document.styleSheets].some(s => s.href === LIB_URLS.maplibreCss) ? Promise.resolve() : loadStylesheet(LIB_URLS.maplibreCss)
|
|
278
498
|
]).then(() => {
|
|
279
|
-
({ IconLayer, ScatterplotLayer, GeoJsonLayer, MapLibreOverlay, TextLayer } = global.deck);
|
|
499
|
+
({ IconLayer, ScatterplotLayer, GeoJsonLayer, MapLibreOverlay, TextLayer, PathLayer, PathStyleExtension } = global.deck);
|
|
500
|
+
PieSliceLayer = makePieSliceLayer(ScatterplotLayer);
|
|
501
|
+
BarShapeLayer = makeBarShapeLayer(ScatterplotLayer);
|
|
280
502
|
// which engine and libraries this page really runs (a page may
|
|
281
503
|
// bring its own MapLibre/deck.gl — the loaded ones are named)
|
|
282
504
|
const ml = global.maplibregl && typeof global.maplibregl.getVersion === 'function' ? global.maplibregl.getVersion() : '?';
|
|
@@ -1437,13 +1659,17 @@
|
|
|
1437
1659
|
const lookupField = spec.binding.lookup;
|
|
1438
1660
|
let layers = String(spec.name).split('|');
|
|
1439
1661
|
if (layers.length > 1 && spec.flags && spec.flags.has('DIFFERENCE')) layers = layers.slice(-1);
|
|
1662
|
+
const lookup2 = spec.binding.lookup2;
|
|
1440
1663
|
const features = [];
|
|
1441
|
-
|
|
1664
|
+
const indexes = layers.map(layer => {
|
|
1442
1665
|
const index = new Map();
|
|
1443
1666
|
runtimes.filter(r => r.name === layer && (r.flags.has('FEATURE') || r.flags.has('FEATURES'))).forEach(r => {
|
|
1444
1667
|
// in theme order, as flat's layer holds the themes' shapes
|
|
1445
1668
|
for (const [k, v] of featurePositionIndex(r)) { if (flatShapeWins(index.get(k), v.area)) index.set(k, v); }
|
|
1446
1669
|
});
|
|
1670
|
+
return index;
|
|
1671
|
+
});
|
|
1672
|
+
for (const index of indexes) {
|
|
1447
1673
|
if (!index.size) continue;
|
|
1448
1674
|
for (const row of table.rows) {
|
|
1449
1675
|
const v = row[lookupField];
|
|
@@ -1453,7 +1679,18 @@
|
|
|
1453
1679
|
// joined shape, as flat) needs the shape's km² area carried along
|
|
1454
1680
|
// (featureAreaKm2 prefers this marker), exactly as the choropleth
|
|
1455
1681
|
// join stamps it
|
|
1456
|
-
if (hit)
|
|
1682
|
+
if (!hit) continue;
|
|
1683
|
+
const f = { type: 'Feature', geometry: { type: 'Point', coordinates: hit.pos }, properties: row, _flatAreaKm2: hit.areaKm2 };
|
|
1684
|
+
// VECTOR / BEZIER: the second position by its own key (flat looks
|
|
1685
|
+
// it up in every layer of the theme, maptheme.js 10431-10436); a
|
|
1686
|
+
// record without one stays an item that draws no line (null)
|
|
1687
|
+
if (lookup2 && !String(lookup2).includes('|')) {
|
|
1688
|
+
const v2 = row[lookup2];
|
|
1689
|
+
let hit2 = null;
|
|
1690
|
+
if (v2 != null && v2 !== '') for (const idx of indexes) { hit2 = idx.get(flatLookupKey(v2, spec.style)); if (hit2) break; }
|
|
1691
|
+
f._pos2 = hit2 ? hit2.pos : null;
|
|
1692
|
+
}
|
|
1693
|
+
features.push(f);
|
|
1457
1694
|
}
|
|
1458
1695
|
}
|
|
1459
1696
|
return { type: 'FeatureCollection', features };
|
|
@@ -1952,6 +2189,28 @@
|
|
|
1952
2189
|
// outliers" inverse mode) stays unimplemented — no config here uses it.
|
|
1953
2190
|
const KNOWN_INERT_FLAGS = ['SEQUENCE', 'STAR', 'SORT', 'DOWN', 'RECT', 'CLIPTOGEOBOUNDS', 'OUTLIER',
|
|
1954
2191
|
'POW2', 'POW3', 'NOSORT'];
|
|
2192
|
+
// flat's PIE / DONUT variants with no rendering here (see _buildPieLayers)
|
|
2193
|
+
const PIE_INERT_FLAGS = [/3D/, /VOLUME/, /HEIGHT/, /STARBURST/, /FLOWER/, /RAYS/, /BOW/, /ZOOM/, /DIRECTION/, /DIRECTED/, /POLAR/];
|
|
2194
|
+
// ... and of AGGREGATE / CATEGORICAL pies: GRIDSIZE's pie the size of its
|
|
2195
|
+
// grid cell (drawn by the SIZE rule instead), AUTOSIZE, DOPACITY, the
|
|
2196
|
+
// chart BOX and TITLE, GAUGE, MULTIPLE / DIFFUSE, the post-aggregation
|
|
2197
|
+
// value arithmetic (DIFFERENCE, FRACTION, PERCENT …, field100)
|
|
2198
|
+
const PIE_AGGREGATE_INERT_FLAGS = [/^GRIDSIZE$/, /AUTOSIZE/, /DOPACITY/, /BOX/, /TITLE/, /^GAUGE$/, /^MULTIPLE$/, /^DIFFUSE$/,
|
|
2199
|
+
/^DIFFERENCE$/, /^FRACTION$/, /^PERCENT$/, /^PERMILLE$/, /^PRODUCT$/, /^CALCVAL$/, /^RELATIVE$/];
|
|
2200
|
+
// flat's BAR / BARS variants with no rendering here (see flatBarLayout):
|
|
2201
|
+
// the bar is drawn plain (SMALLARROW as ARROW) — tested as substrings,
|
|
2202
|
+
// as flat does
|
|
2203
|
+
const BAR_INERT_FLAGS = [/3D/, /VOLUME/, /COLUMN/, /DOTTED/, /SMALLARROW/, /TRENDLINE/, /AXIS/, /^CLIP$/, /MORPH/,
|
|
2204
|
+
/ZOOM/, /NORMSIZE/, /MENUSIZE/, /INVERT/, /DIAGLEFT/, /DIAGRIGHT/, /^WIDTH$/, /^MULTIPLE$/, /^DIFFUSE$/,
|
|
2205
|
+
/^DIFFERENCE$/, /^FRACTION$/, /^PERCENT$/, /^PERMILLE$/, /^PRODUCT$/, /^CALCVAL$/, /^RELATIVE$/];
|
|
2206
|
+
// flat's VECTOR / BEZIER modifiers with no rendering here (see
|
|
2207
|
+
// _buildVectorLayers), the word → what happens instead
|
|
2208
|
+
const VECTOR_INERT_FLAGS = {
|
|
2209
|
+
VALUES: 'the value text along the curve is not drawn yet',
|
|
2210
|
+
DYNAMICWIDTH: 'not drawn yet (flat widens by the log of the length in its own map units) — the plain width instead',
|
|
2211
|
+
LONG: 'not drawn yet (its bow depends on flat\'s own map units) — the default bow instead',
|
|
2212
|
+
DASH: 'drawn as static dashes — flat\'s dash animation is not ported'
|
|
2213
|
+
};
|
|
1955
2214
|
const _warnedFlags = new Set();
|
|
1956
2215
|
|
|
1957
2216
|
// GL-PORT COMPAT: real ixmaps-flat's global ixmaps.getThemeObj(szId) /
|
|
@@ -2289,6 +2548,9 @@
|
|
|
2289
2548
|
'style.valuefield': ['style', 'valuefield'],
|
|
2290
2549
|
'style.timefield': ['binding', 'time'], // setThemeTimeFrame, see LayerRuntime.setTimeFrame
|
|
2291
2550
|
'style.titlefield': ['binding', 'title'], // {{theme.item.title}}, see _buildTooltipContext
|
|
2551
|
+
// VECTOR / BEZIER's second position: a join key ("a") or lat/lon
|
|
2552
|
+
// fields ("a|b"), see joinChartPositions and _vectorRecords
|
|
2553
|
+
'style.lookupfield2': ['binding', 'lookup2'],
|
|
2292
2554
|
// the lookup key's normalization (flatLookupKey): flat's newTheme moves
|
|
2293
2555
|
// binding.tonumber / digits / … into these style keys (htmlgui.js 1712)
|
|
2294
2556
|
'style.lookupdigits': ['style', 'lookupdigits'],
|
|
@@ -2521,7 +2783,10 @@
|
|
|
2521
2783
|
// real ixmaps-flat (htmlgui.js newTheme) merges .meta() INTO style first
|
|
2522
2784
|
// — meta wins — and only then reads type, bindings and style properties
|
|
2523
2785
|
const style = Object.assign({}, def.style, def.meta);
|
|
2524
|
-
|
|
2786
|
+
let typeStr = style.type != null ? String(style.type) : '';
|
|
2787
|
+
// flat (htmlgui-side theme parse, maptheme.js 1192): the old type
|
|
2788
|
+
// word EXACT is CATEGORICAL
|
|
2789
|
+
if (/\bEXACT\b/.test(typeStr) && !/\bCATEGORICAL\b/.test(typeStr)) typeStr += '|CATEGORICAL';
|
|
2525
2790
|
const filter = style.filter;
|
|
2526
2791
|
const title = style.title;
|
|
2527
2792
|
delete style.type;
|
|
@@ -2542,6 +2807,48 @@
|
|
|
2542
2807
|
console.info(`[ixmaps-gl] type flag "${flag}" recognized, no distinct rendering behavior implemented yet`);
|
|
2543
2808
|
}
|
|
2544
2809
|
});
|
|
2810
|
+
// PIE variants _buildPieLayers doesn't draw (the pie is drawn flat and
|
|
2811
|
+
// plain): flat tests them as substrings, so do these
|
|
2812
|
+
if (flags.has('PIE') && flags.has('CHART')) {
|
|
2813
|
+
flags.forEach(flag => {
|
|
2814
|
+
const inert = PIE_INERT_FLAGS.find(re => re.test(flag));
|
|
2815
|
+
if (inert && !(/HEIGHT/.test(flag) && !flags.has('3D')) && !_warnedFlags.has('PIE:' + flag)) {
|
|
2816
|
+
_warnedFlags.add('PIE:' + flag);
|
|
2817
|
+
console.info(`[ixmaps-gl] type flag "${flag}" on a PIE chart recognized, not drawn yet — the pie is drawn flat and plain`);
|
|
2818
|
+
}
|
|
2819
|
+
});
|
|
2820
|
+
// AGGREGATE / CATEGORICAL pies (_isAggregatedPie): what is not ported
|
|
2821
|
+
if (flags.has('AGGREGATE') || flags.has('CATEGORICAL')) {
|
|
2822
|
+
flags.forEach(flag => {
|
|
2823
|
+
const inert = PIE_AGGREGATE_INERT_FLAGS.find(re => re.test(flag));
|
|
2824
|
+
if (inert && !_warnedFlags.has('PIEAGG:' + flag)) {
|
|
2825
|
+
_warnedFlags.add('PIEAGG:' + flag);
|
|
2826
|
+
console.info(`[ixmaps-gl] type flag "${flag}" on an aggregated PIE chart recognized, not ported yet — ignored`);
|
|
2827
|
+
}
|
|
2828
|
+
});
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2832
|
+
// BAR variants _buildBarLayers doesn't draw (the bars are drawn plain)
|
|
2833
|
+
if (isBarChart(flags)) {
|
|
2834
|
+
flags.forEach(flag => {
|
|
2835
|
+
if (BAR_INERT_FLAGS.some(re => re.test(flag)) && !_warnedFlags.has('BAR:' + flag)) {
|
|
2836
|
+
_warnedFlags.add('BAR:' + flag);
|
|
2837
|
+
console.info(`[ixmaps-gl] type flag "${flag}" on a BAR chart recognized, not ported yet — the bars are drawn plain`);
|
|
2838
|
+
}
|
|
2839
|
+
});
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2842
|
+
// VECTOR / BEZIER modifiers _buildVectorLayers doesn't draw
|
|
2843
|
+
if (isVectorChart(flags)) {
|
|
2844
|
+
flags.forEach(flag => {
|
|
2845
|
+
const why = VECTOR_INERT_FLAGS[flag];
|
|
2846
|
+
if (why && !_warnedFlags.has('VECTOR:' + flag)) {
|
|
2847
|
+
_warnedFlags.add('VECTOR:' + flag);
|
|
2848
|
+
console.info(`[ixmaps-gl] type flag "${flag}" on a VECTOR/BEZIER chart recognized, ${why}`);
|
|
2849
|
+
}
|
|
2850
|
+
});
|
|
2851
|
+
}
|
|
2545
2852
|
|
|
2546
2853
|
const rawBinding = Object.assign({}, def.binding);
|
|
2547
2854
|
const targets = resolveBindingTargets(style, rawBinding, def);
|
|
@@ -2603,6 +2910,9 @@
|
|
|
2603
2910
|
opacity: 1; transition: opacity 0.4s ease;
|
|
2604
2911
|
}
|
|
2605
2912
|
.ixmaps-splash.ixmaps-splash-hidden { opacity: 0; pointer-events: none; }
|
|
2913
|
+
.ixmaps-splash.ixmaps-splash-dark { background: #1a1a1a; }
|
|
2914
|
+
.ixmaps-splash-dark .ixmaps-splash-word { color: #e8e8e8; }
|
|
2915
|
+
.ixmaps-splash-dark .ixmaps-splash-text { color: #9a9a9a; }
|
|
2606
2916
|
.ixmaps-splash-mark { position: relative; width: 44px; height: 44px; display: flex; align-items: center; justify-content: center; }
|
|
2607
2917
|
.ixmaps-splash-ping {
|
|
2608
2918
|
position: absolute; width: 16px; height: 16px; border-radius: 50%;
|
|
@@ -2638,15 +2948,21 @@
|
|
|
2638
2948
|
// flat's Dictionary.getLocalText (mapscript.js 5631-5648) over the page's
|
|
2639
2949
|
// .local(text, translation) pairs: the whole text, else word by word
|
|
2640
2950
|
// (a word's entry is written in quotes, 'word')
|
|
2951
|
+
// plus the page-wide entries of ixmaps.setLocalString (flat's
|
|
2952
|
+
// Api.setLocalString → Dictionary.replace, mapapi.js 123), read at call
|
|
2953
|
+
// time so strings set after the map is built count; the later
|
|
2954
|
+
// setLocalString wins over a .local() pair of the same text
|
|
2955
|
+
const _localStrings = Object.create(null);
|
|
2641
2956
|
function makeLocalText(locals) {
|
|
2642
2957
|
const dict = {};
|
|
2643
2958
|
(locals || []).forEach(a => {
|
|
2644
2959
|
if (a && typeof a[0] === 'object' && a[0]) Object.assign(dict, a[0]);
|
|
2645
2960
|
else if (a && a.length >= 2) dict[a[0]] = a[1];
|
|
2646
2961
|
});
|
|
2962
|
+
const entry = k => (_localStrings[k] !== undefined ? _localStrings[k] : dict[k]);
|
|
2647
2963
|
return text => {
|
|
2648
|
-
if (
|
|
2649
|
-
return String(text).split(' ').map(w => (
|
|
2964
|
+
if (entry(text) !== undefined) return String(entry(text));
|
|
2965
|
+
return String(text).split(' ').map(w => (entry("'" + w + "'") !== undefined ? String(entry("'" + w + "'")) : w)).join(' ');
|
|
2650
2966
|
};
|
|
2651
2967
|
}
|
|
2652
2968
|
// make the map container the positioning context of what the engine
|
|
@@ -2659,7 +2975,7 @@
|
|
|
2659
2975
|
function anchorInside(el) {
|
|
2660
2976
|
if (el && getComputedStyle(el).position === 'static') el.style.position = 'relative';
|
|
2661
2977
|
}
|
|
2662
|
-
function showSplash(el, text) {
|
|
2978
|
+
function showSplash(el, text, dark) {
|
|
2663
2979
|
ensureSplashStyle();
|
|
2664
2980
|
// Checks the actual RENDERED position, not el.style (the inline
|
|
2665
2981
|
// attribute only) — every example page's own #map_div already sets
|
|
@@ -2674,7 +2990,7 @@
|
|
|
2674
2990
|
// this at all.
|
|
2675
2991
|
anchorInside(el);
|
|
2676
2992
|
const splash = document.createElement('div');
|
|
2677
|
-
splash.className = 'ixmaps-splash';
|
|
2993
|
+
splash.className = 'ixmaps-splash' + (dark ? ' ixmaps-splash-dark' : '');
|
|
2678
2994
|
splash.innerHTML = '<div class="ixmaps-splash-mark"><span class="ixmaps-splash-ping"></span><span class="ixmaps-splash-dot"></span></div>' +
|
|
2679
2995
|
'<div class="ixmaps-splash-word">ixmaps<sup>gl</sup></div>' +
|
|
2680
2996
|
'<div class="ixmaps-splash-text"></div>';
|
|
@@ -3540,7 +3856,9 @@
|
|
|
3540
3856
|
// aggregation" reading (style.valuefield, falling back to
|
|
3541
3857
|
// the bound size field); anything else (plain CATEGORICAL,
|
|
3542
3858
|
// no SUM) falls back to a per-category record COUNT.
|
|
3543
|
-
|
|
3859
|
+
// a VECTOR / BEZIER flow legend sums its size field (flat's rows
|
|
3860
|
+
// show the flows' summed sizes, measured on the twin page)
|
|
3861
|
+
const useSum = flatFlag(rt.flags, 'SUM') && (rt.style.valuefield || ((rt._isVectorChart() || rt._isAggregatedPie()) && rt.binding.size));
|
|
3544
3862
|
const valueField = rt.style.valuefield || rt.binding.size;
|
|
3545
3863
|
// legendunits wins over the theme's general-purpose units
|
|
3546
3864
|
// (used elsewhere for tooltips, e.g. _renderItemChartHtml) —
|
|
@@ -3549,13 +3867,18 @@
|
|
|
3549
3867
|
// space injected), matching how style.units/legendunits are
|
|
3550
3868
|
// themselves authored with their own leading space (e.g.
|
|
3551
3869
|
// " MW") in real pages.
|
|
3552
|
-
|
|
3870
|
+
// (a VECTOR / BEZIER or aggregated PIE legend: flat's szUnit, the
|
|
3871
|
+
// units after a space unless they start with ".", measured on the
|
|
3872
|
+
// twin pages)
|
|
3873
|
+
const flatUnitLegend = rt._isVectorChart() || rt._isAggregatedPie();
|
|
3874
|
+
const rawLegendUnit = rt.style.legendunits || rt.style.units || '';
|
|
3875
|
+
const legendUnit = flatUnitLegend && rawLegendUnit && rawLegendUnit[0] !== '.' && rawLegendUnit[0] !== ' ' ? ' ' + rawLegendUnit : rawLegendUnit;
|
|
3553
3876
|
// a per-item chart over several value fields: the row values as
|
|
3554
3877
|
// flat's legend prints them (legend.js 610, 810: __formatValue
|
|
3555
3878
|
// with valuedecimals, else 2, "BLANK" — whole numbers without
|
|
3556
3879
|
// decimals), a colorscheme "none" part as an outline in linecolor
|
|
3557
|
-
const multiFieldLegend =
|
|
3558
|
-
const legendTotalText = v => (multiFieldLegend
|
|
3880
|
+
const multiFieldLegend = rt._isMultiFieldChart();
|
|
3881
|
+
const legendTotalText = v => ((multiFieldLegend || flatUnitLegend)
|
|
3559
3882
|
? flatFormatValue(v, legendDecimals(rt.style), 'BLANK')
|
|
3560
3883
|
: rt._formatTooltipValue(v));
|
|
3561
3884
|
const outlineRgb = rt.style.linecolor && styleLineColor(rt.style.linecolor) !== 'none' ? hexOrNamedToRgb(styleLineColor(rt.style.linecolor)) : [128, 128, 128];
|
|
@@ -3605,7 +3928,7 @@
|
|
|
3605
3928
|
// a per-item chart over several value fields: a row per field,
|
|
3606
3929
|
// the field's sum over the visible items (flat's NYC status
|
|
3607
3930
|
// legend: bikes / empty docks / e-bikes, biggest sum first)
|
|
3608
|
-
if (rt.
|
|
3931
|
+
if (rt._isMultiFieldChart()) {
|
|
3609
3932
|
(rt._activeFeatures || rt.features).forEach(f => {
|
|
3610
3933
|
const c = f.geometry && f.geometry.coordinates;
|
|
3611
3934
|
if (!c || typeof c[0] !== 'number') return;
|
|
@@ -4115,6 +4438,11 @@
|
|
|
4115
4438
|
await null;
|
|
4116
4439
|
const el = document.getElementById(this.containerId);
|
|
4117
4440
|
if (!el) throw new Error(`[ixmaps-gl] container #${this.containerId} not found`);
|
|
4441
|
+
// flat's init replaces the container's content with its own map page
|
|
4442
|
+
// (mappage.html via innerHTML), so a page's placeholder inside the
|
|
4443
|
+
// map div ("connecting to Internet ...", the fuel-price page) goes
|
|
4444
|
+
// away; here it would stay on top of the map — remove it the same way
|
|
4445
|
+
while (el.firstChild) el.removeChild(el.firstChild);
|
|
4118
4446
|
// GL-PORT COMPAT: ixmaps.Map(id, {width, height}) — flat sizes its
|
|
4119
4447
|
// inner map div with these and gives PIXEL values to the page's own
|
|
4120
4448
|
// container too (htmlgui_flat.js:866-876, parent().css(...)); here
|
|
@@ -4125,6 +4453,11 @@
|
|
|
4125
4453
|
const v = this.mapOptions && this.mapOptions[dim];
|
|
4126
4454
|
if (typeof v === 'string' && /^\s*\d+(\.\d+)?\s*px\s*$/i.test(v)) el.style[dim] = v.trim();
|
|
4127
4455
|
else if (typeof v === 'number' && v > 0) el.style[dim] = v + 'px';
|
|
4456
|
+
// a percentage stays the page's CSS — unless that CSS left the
|
|
4457
|
+
// container collapsed on this side (e.g. an empty flex item, the
|
|
4458
|
+
// UNHCR page): flat's own pixel-sized map div fills it there
|
|
4459
|
+
else if (typeof v === 'string' && /^\s*\d+(\.\d+)?\s*%\s*$/.test(v)
|
|
4460
|
+
&& !(dim === 'width' ? el.clientWidth : el.clientHeight)) el.style[dim] = v.trim();
|
|
4128
4461
|
}
|
|
4129
4462
|
// without width/height flat's map is "fullscreen" (htmlgui_flat.js
|
|
4130
4463
|
// 847-876): it fills the window, whatever the page's CSS gives the
|
|
@@ -4140,15 +4473,29 @@
|
|
|
4140
4473
|
// overrides the default message, .options({splash:false}) skips it
|
|
4141
4474
|
// entirely (e.g. for a map embedded somewhere a full-cover overlay
|
|
4142
4475
|
// would be wrong, or one that's expected to load near-instantly).
|
|
4476
|
+
// dark on a dark map (no basemap style yet: flatLegendLook's name/
|
|
4477
|
+
// color rule, or a basemap faded to <= 0.5 over a dark page, as
|
|
4478
|
+
// effectiveMapDark) — a light sheet flashed between a dark page and
|
|
4479
|
+
// a dark map (the UNHCR page)
|
|
4480
|
+
const splashOpacity = parseFloat(this._engineOptions.basemapopacity);
|
|
4481
|
+
const splashLegendBg = this.mapOptions.legendBackground || this.mapOptions.legendbackground;
|
|
4482
|
+
const splashDark = flatLegendLook(this.mapOptions.mapType, isNaN(splashOpacity) ? 1 : splashOpacity, splashLegendBg).dark
|
|
4483
|
+
|| (splashOpacity <= 0.5 && flatIsDarkColor('rgb(' + pageBackgroundColorOf(el).map(Math.round).join(',') + ')'));
|
|
4143
4484
|
const splash = this._engineOptions.splash === false ? null
|
|
4144
|
-
: showSplash(el, this._engineOptions.splashText || 'loading…');
|
|
4485
|
+
: showSplash(el, this._engineOptions.splashText || 'loading…', splashDark);
|
|
4145
4486
|
|
|
4146
4487
|
// ixmaps.getBoundingBox()/setTitle() for brokers called below, before
|
|
4147
4488
|
// the MapLibre map exists (same default view as the map gets)
|
|
4148
4489
|
_titleHost = el;
|
|
4149
|
-
|
|
4490
|
+
// a flat SVG map in another projection (equalearth.svg): its view
|
|
4491
|
+
// framing and map scale (see FLAT_SVG_PROJECTIONS)
|
|
4492
|
+
const svgProjection = flatSvgProjectionOf(this.mapOptions.map);
|
|
4493
|
+
if (svgProjection) flatSvgProjectionByOptions.set(this._engineOptions, svgProjection);
|
|
4494
|
+
const [vLat, vLng] = this._viewCenter || [45.5, 9.2];
|
|
4495
|
+
const initialZoom = this._viewZoom != null && this._viewZoom !== ''
|
|
4496
|
+
? flatViewToMapLibreZoom(Number(this._viewZoom), Number(vLat), Number(vLng), el.clientWidth, el.clientHeight, svgProjection)
|
|
4497
|
+
: 8;
|
|
4150
4498
|
{
|
|
4151
|
-
const [vLat, vLng] = this._viewCenter || [45.5, 9.2];
|
|
4152
4499
|
_boundsSource = () => viewBounds(Number(vLat), Number(vLng), initialZoom, el.clientWidth, el.clientHeight);
|
|
4153
4500
|
}
|
|
4154
4501
|
|
|
@@ -4267,7 +4614,7 @@
|
|
|
4267
4614
|
style: mapTypeColor ? buildBlankBackgroundStyle(mapTypeColor)
|
|
4268
4615
|
: resolveBasemapStyleUrl(this.mapOptions.mapType),
|
|
4269
4616
|
center: [lon, lat],
|
|
4270
|
-
zoom:
|
|
4617
|
+
zoom: initialZoom,
|
|
4271
4618
|
// MapLibre's own AttributionControl (bottom right) keeps the
|
|
4272
4619
|
// basemap's required CARTO/OpenStreetMap credit; the page's own
|
|
4273
4620
|
// .attribution() goes bottom left, as flat shows it
|
|
@@ -4291,6 +4638,7 @@
|
|
|
4291
4638
|
// world once, at the cost of that zoom limit.
|
|
4292
4639
|
renderWorldCopies: this._engineOptions.worldcopies !== false
|
|
4293
4640
|
});
|
|
4641
|
+
engineOptionsByMap.set(map, this._engineOptions);
|
|
4294
4642
|
// the live-SVG pane USER charts render into (see chartHostFor) —
|
|
4295
4643
|
// registered here, keyed by this map's options object, so every
|
|
4296
4644
|
// runtime (including later loadProject/map.layer ones) finds it
|
|
@@ -4338,7 +4686,7 @@
|
|
|
4338
4686
|
if (typeof hook !== 'function' || !prev) return;
|
|
4339
4687
|
const p = map.project(c), q = map.project(prev.c);
|
|
4340
4688
|
if (z === prev.z && Math.abs(p.x - q.x) <= 10 && Math.abs(p.y - q.y) <= 10) return;
|
|
4341
|
-
try { hook.call(global.ixmaps,
|
|
4689
|
+
try { hook.call(global.ixmaps, flatViewZoomOf(map, z)); } catch (e) { console.error('[ixmaps-gl] htmlgui_onZoomAndPan:', e); }
|
|
4342
4690
|
});
|
|
4343
4691
|
notifyZoomAndPanMidGesture = () => {
|
|
4344
4692
|
const hook = pageIxmaps() && pageIxmaps().htmlgui_onZoomAndPan;
|
|
@@ -4346,13 +4694,13 @@
|
|
|
4346
4694
|
const z = map.getZoom();
|
|
4347
4695
|
if (z === last.z) return; // a pan alone waits for 'moveend' (and its 10 px rule)
|
|
4348
4696
|
last = { z, c: map.getCenter() };
|
|
4349
|
-
try { hook.call(global.ixmaps,
|
|
4697
|
+
try { hook.call(global.ixmaps, flatViewZoomOf(map, z)); } catch (e) { console.error('[ixmaps-gl] htmlgui_onZoomAndPan:', e); }
|
|
4350
4698
|
};
|
|
4351
4699
|
// flat calls it on the first draw too (its old zoom is unset then)
|
|
4352
4700
|
map.once('load', () => {
|
|
4353
4701
|
last = { z: map.getZoom(), c: map.getCenter() };
|
|
4354
4702
|
const hook = pageIxmaps() && pageIxmaps().htmlgui_onZoomAndPan;
|
|
4355
|
-
if (typeof hook === 'function') { try { hook.call(global.ixmaps,
|
|
4703
|
+
if (typeof hook === 'function') { try { hook.call(global.ixmaps, flatViewZoomOf(map, last.z)); } catch (e) { console.error('[ixmaps-gl] htmlgui_onZoomAndPan:', e); } }
|
|
4356
4704
|
});
|
|
4357
4705
|
}
|
|
4358
4706
|
|
|
@@ -4380,10 +4728,14 @@
|
|
|
4380
4728
|
// hover/click tooltip lookup keeps working across a rotation.
|
|
4381
4729
|
// Non-atlas layers (ix-dot-/ix-features-/ix-choropleth-) never
|
|
4382
4730
|
// carry the suffix; stripping a pattern that isn't there is a
|
|
4383
|
-
// no-op.
|
|
4384
|
-
|
|
4731
|
+
// no-op. A theme's single circles (ix-bubbles-<name>-circles-gN,
|
|
4732
|
+
// a ScatterplotLayer) route like its bubbles.
|
|
4733
|
+
const base = layerId.replace(/-g\d+$/, '').replace(/-circles$/, '').replace(/^(ix-vector-heads-.*)-n\d+$/, '$1');
|
|
4385
4734
|
return runtimes.find(r => {
|
|
4386
4735
|
if (base === `ix-bubbles-${r.name}`) return isSymbolChart(r.flags);
|
|
4736
|
+
if (base === `ix-pie-${r.name}`) return r._isPieChart();
|
|
4737
|
+
if (base === `ix-bar-${r.name}`) return r._isBarChart();
|
|
4738
|
+
if (base === `ix-vector-${r.name}` || base === `ix-vector-heads-${r.name}`) return r._isVectorChart();
|
|
4387
4739
|
if (base === `ix-dot-${r.name}`) return r.flags.has('DOT');
|
|
4388
4740
|
if (base === `ix-choropleth-${r.name}`) return r.flags.has('CHOROPLETH');
|
|
4389
4741
|
if (base === `ix-features-${r.name}`) return r.flags.has('FEATURE') || r.flags.has('FEATURES');
|
|
@@ -4540,7 +4892,7 @@
|
|
|
4540
4892
|
resize: () => { map.resize(); return engineApi; },
|
|
4541
4893
|
// broker calls in flight (themes whose data comes by name)
|
|
4542
4894
|
pendingLoads: () => pendingLoads,
|
|
4543
|
-
getZoom: () =>
|
|
4895
|
+
getZoom: () => flatViewZoomOf(map),
|
|
4544
4896
|
require: (url) => { loadRequiredScripts([url]); return engineApi; },
|
|
4545
4897
|
// flat's theme-level calls by theme id (style.name / meta.name, else
|
|
4546
4898
|
// the layer name): add(theme, flag) — "replace" swaps a theme of the
|
|
@@ -4813,7 +5165,7 @@
|
|
|
4813
5165
|
const z = isOpts ? latlonOrOpts.zoom : zoom;
|
|
4814
5166
|
if (c) {
|
|
4815
5167
|
const lngLat = Array.isArray(c) ? [c[1], c[0]] : [c.lng, c.lat];
|
|
4816
|
-
jumpLive(Object.assign({ center: lngLat }, z != null ? { zoom:
|
|
5168
|
+
jumpLive(Object.assign({ center: lngLat }, z != null ? { zoom: mapLibreZoomOfFlatView(map, Number(z), lngLat[1], lngLat[0]) } : {}));
|
|
4817
5169
|
}
|
|
4818
5170
|
return engineApi;
|
|
4819
5171
|
},
|
|
@@ -5220,6 +5572,12 @@
|
|
|
5220
5572
|
|
|
5221
5573
|
function applyProjectMap(m, f, report) {
|
|
5222
5574
|
const svg = String(m.map || '');
|
|
5575
|
+
// its view and map scale follow the project's map file, as on flat
|
|
5576
|
+
if (svg) {
|
|
5577
|
+
const svgProjection = flatSvgProjectionOf(svg);
|
|
5578
|
+
if (svgProjection) flatSvgProjectionByOptions.set(builder._engineOptions, svgProjection);
|
|
5579
|
+
else flatSvgProjectionByOptions.delete(builder._engineOptions);
|
|
5580
|
+
}
|
|
5223
5581
|
if (svg && !/generic\/(mercator|orthographic)\.svg$/i.test(svg)) report.notes.push(`flat SVG map "${svg.split('/').pop()}" is not available in ixmaps-gl — mercator used`);
|
|
5224
5582
|
if (typeof map.setProjection === 'function') {
|
|
5225
5583
|
try {
|
|
@@ -5240,8 +5598,8 @@
|
|
|
5240
5598
|
const c = m.center || {};
|
|
5241
5599
|
const lat = Number(c.lat), lng = Number(c.lng);
|
|
5242
5600
|
const z = m.zoom != null && m.zoom !== '' ? Number(m.zoom) : NaN;
|
|
5243
|
-
if (Number.isFinite(lat) && Number.isFinite(lng)) jumpLive(Object.assign({ center: [lng, lat] }, Number.isFinite(z) ? { zoom:
|
|
5244
|
-
else if (Number.isFinite(z)) jumpLive({ zoom:
|
|
5601
|
+
if (Number.isFinite(lat) && Number.isFinite(lng)) jumpLive(Object.assign({ center: [lng, lat] }, Number.isFinite(z) ? { zoom: mapLibreZoomOfFlatView(map, z, lat, lng) } : {}));
|
|
5602
|
+
else if (Number.isFinite(z)) jumpLive({ zoom: mapLibreZoomOfFlatView(map, z, map.getCenter().lat, map.getCenter().lng) });
|
|
5245
5603
|
}
|
|
5246
5604
|
// a project can't authorize its own scripts — trustedscripts is the page's
|
|
5247
5605
|
if (m.options && typeof m.options === 'object') {
|
|
@@ -5915,6 +6273,110 @@
|
|
|
5915
6273
|
const FLAT_ZOOM_OFFSET = 1;
|
|
5916
6274
|
const flatToMapLibreZoom = z => z - FLAT_ZOOM_OFFSET;
|
|
5917
6275
|
const mapLibreToFlatZoom = z => z + FLAT_ZOOM_OFFSET;
|
|
6276
|
+
|
|
6277
|
+
// flat's world SVG maps in a projection other than Mercator
|
|
6278
|
+
// (map:".../generic/equalearth.svg"), which this engine draws in Web
|
|
6279
|
+
// Mercator. Flat syncs such a map to its Leaflet view by fitting the
|
|
6280
|
+
// Leaflet bounds' SW/NE corners, projected, into the map
|
|
6281
|
+
// (htmlgui_sync.js setBoundsLatLon → mapscript2.js doSetMapToGeoBounds /
|
|
6282
|
+
// doZoomMapToArea): both axes fit (min) while the bounds span ≥ 180° of
|
|
6283
|
+
// longitude, else the x axis fits the width on its own (mapapi.js
|
|
6284
|
+
// doSetMapToGeoBounds drops fPreserveMapRatio) — so the same view zoom
|
|
6285
|
+
// frames these maps differently from Mercator (Equal Earth at 2 on
|
|
6286
|
+
// 1024×748: 1.55× the Mercator scale; at 4: 1.09×).
|
|
6287
|
+
// `project` is flat's own forward projection (mapscript.js
|
|
6288
|
+
// _LLtoEqualEarth, _LLtoWinkelTripel — Winkel's cos(50.467) in radians
|
|
6289
|
+
// included), in units of the earth radius.
|
|
6290
|
+
// `scaleConstant`: flat's map scale (nTrueMapScale · nZoomScale, the
|
|
6291
|
+
// scale its dynamic object scaling and gates use) × the screen pixels
|
|
6292
|
+
// per degree of longitude on the equator — constant per map file,
|
|
6293
|
+
// measured on flat (2026-10-09, view zooms 2-5): Equal Earth 170.1e6,
|
|
6294
|
+
// Winkel 195.45e6; for flat's Mercator map 442 913 385 · 256 / 360 =
|
|
6295
|
+
// 314.96e6 (FLAT_OBJECT_SCALE_CONSTANT). These SVG maps carry the
|
|
6296
|
+
// Mercator map's mapscale (177 165 354), so at the same on-screen size
|
|
6297
|
+
// flat calls their scale 0.54× (0.62×) the Mercator one, and its
|
|
6298
|
+
// symbols are drawn larger by that ratio^(1/dynamicScalePow). Framing
|
|
6299
|
+
// matches flat's pixels per degree of longitude at the view's center;
|
|
6300
|
+
// the scale gates (scaleDenominatorAt) still use the Mercator scale.
|
|
6301
|
+
const FLAT_MERCATOR_SCALE_CONSTANT = FLAT_OBJECT_SCALE_CONSTANT * 256 / 360;
|
|
6302
|
+
const FLAT_SVG_PROJECTIONS = {
|
|
6303
|
+
equalearth: {
|
|
6304
|
+
svg: 'maps/svg/maps/generic/equalearth.svg',
|
|
6305
|
+
scaleConstant: 170.1e6,
|
|
6306
|
+
project: (lat, lon) => {
|
|
6307
|
+
const M = Math.sqrt(3) / 2, A1 = 1.340264, A2 = -0.081106, A3 = 0.000893, A4 = 0.003796;
|
|
6308
|
+
const l = Math.asin(M * Math.sin(lat * Math.PI / 180)), l2 = l * l, l6 = l2 * l2 * l2;
|
|
6309
|
+
return [lon * Math.PI / 180 * Math.cos(l) / (M * (A1 + 3 * A2 * l2 + l6 * (7 * A3 + 9 * A4 * l2))), l * (A1 + A2 * l2 + l6 * (A3 + A4 * l2))];
|
|
6310
|
+
}
|
|
6311
|
+
},
|
|
6312
|
+
winkel: {
|
|
6313
|
+
svg: 'maps/svg/maps/generic/winkel.svg',
|
|
6314
|
+
scaleConstant: 195.45e6,
|
|
6315
|
+
project: (lat, lon) => {
|
|
6316
|
+
const p = lat * Math.PI / 180, q = Math.max(-180, Math.min(180, lon)) * Math.PI / 360;
|
|
6317
|
+
const D = Math.acos(Math.cos(p) * Math.cos(q));
|
|
6318
|
+
let x1 = 0, y1 = 0;
|
|
6319
|
+
if (D !== 0) { const C = Math.sin(p) / Math.sin(D); x1 = (q < 0 ? -2 : 2) * D * Math.sqrt(1 - C * C); y1 = D * C; }
|
|
6320
|
+
return [0.5 * (x1 + 2 * q * Math.cos(50.467)), 0.5 * (y1 + p)];
|
|
6321
|
+
}
|
|
6322
|
+
}
|
|
6323
|
+
};
|
|
6324
|
+
// the flat SVG map projection of a map:/project map URL (generic file
|
|
6325
|
+
// name), null for Mercator and every map drawn as is (orthographic → globe)
|
|
6326
|
+
function flatSvgProjectionOf(svgUrl) {
|
|
6327
|
+
const m = /(?:^|\/)(equalearth|winkel)\.svgz?(?:[?#].*)?$/i.exec(String(svgUrl || ''));
|
|
6328
|
+
return m ? FLAT_SVG_PROJECTIONS[m[1].toLowerCase()] : null;
|
|
6329
|
+
}
|
|
6330
|
+
// the flat SVG projection a map's options object belongs to (set by the
|
|
6331
|
+
// map's init and loadProject; the runtimes only see the options object)
|
|
6332
|
+
const flatSvgProjectionByOptions = new WeakMap();
|
|
6333
|
+
// flat SVG map scale ÷ the Mercator one at the same on-screen size (1
|
|
6334
|
+
// without a flat SVG projection)
|
|
6335
|
+
function flatMapScaleRatio(mapOptions) {
|
|
6336
|
+
const proj = mapOptions && typeof mapOptions === 'object' ? flatSvgProjectionByOptions.get(mapOptions) : null;
|
|
6337
|
+
return proj ? proj.scaleConstant / FLAT_MERCATOR_SCALE_CONSTANT : 1;
|
|
6338
|
+
}
|
|
6339
|
+
// a flat view zoom → the MapLibre zoom showing what flat shows at it:
|
|
6340
|
+
// under a flat SVG projection the same screen pixels per degree of
|
|
6341
|
+
// longitude at the center latitude as flat's fitted map (see
|
|
6342
|
+
// FLAT_SVG_PROJECTIONS), else flatToMapLibreZoom
|
|
6343
|
+
function flatViewToMapLibreZoom(flatZoom, lat, lng, width, height, proj) {
|
|
6344
|
+
if (!proj || !(width > 0) || !(height > 0) || !Number.isFinite(lat) || !Number.isFinite(lng)) return flatToMapLibreZoom(flatZoom);
|
|
6345
|
+
// flat's Leaflet bounds (256 px tiles) at this zoom
|
|
6346
|
+
const [sw, ne] = viewBounds(lat, lng, flatToMapLibreZoom(flatZoom), width, height);
|
|
6347
|
+
const a = proj.project(sw.lat, sw.lng), b = proj.project(ne.lat, ne.lng);
|
|
6348
|
+
const kx = width / Math.abs(b[0] - a[0]), ky = height / Math.abs(b[1] - a[1]);
|
|
6349
|
+
const k = ne.lng - sw.lng < 180 ? kx : Math.min(kx, ky);
|
|
6350
|
+
const pxPerDegree = k * (proj.project(lat, lng + 0.5)[0] - proj.project(lat, lng - 0.5)[0]);
|
|
6351
|
+
return Math.log2(pxPerDegree * 360 / 512);
|
|
6352
|
+
}
|
|
6353
|
+
// the inverse: the flat view zoom of a MapLibre zoom (bisection — the
|
|
6354
|
+
// fit above is monotonic in the zoom)
|
|
6355
|
+
function mapLibreToFlatViewZoom(mapLibreZoom, lat, lng, width, height, proj) {
|
|
6356
|
+
if (!proj || !(width > 0) || !(height > 0) || !Number.isFinite(lat) || !Number.isFinite(lng)) return mapLibreToFlatZoom(mapLibreZoom);
|
|
6357
|
+
let lo = mapLibreToFlatZoom(mapLibreZoom) - 4, hi = mapLibreToFlatZoom(mapLibreZoom) + 4;
|
|
6358
|
+
for (let i = 0; i < 40; i++) {
|
|
6359
|
+
const mid = (lo + hi) / 2;
|
|
6360
|
+
if (flatViewToMapLibreZoom(mid, lat, lng, width, height, proj) < mapLibreZoom) lo = mid; else hi = mid;
|
|
6361
|
+
}
|
|
6362
|
+
return (lo + hi) / 2;
|
|
6363
|
+
}
|
|
6364
|
+
// a live MapLibre map's options object (its runtimes' mapOptions), for
|
|
6365
|
+
// the conversions below
|
|
6366
|
+
const engineOptionsByMap = new WeakMap();
|
|
6367
|
+
function mapSvgProjection(map) {
|
|
6368
|
+
const options = engineOptionsByMap.get(map);
|
|
6369
|
+
return (options && flatSvgProjectionByOptions.get(options)) || null;
|
|
6370
|
+
}
|
|
6371
|
+
// the flat view zoom a live map shows / the MapLibre zoom of a flat view
|
|
6372
|
+
function flatViewZoomOf(map, mapLibreZoom) {
|
|
6373
|
+
const c = map.getCenter(), el = map.getContainer();
|
|
6374
|
+
return mapLibreToFlatViewZoom(mapLibreZoom == null ? map.getZoom() : mapLibreZoom, c.lat, c.lng, el.clientWidth, el.clientHeight, mapSvgProjection(map));
|
|
6375
|
+
}
|
|
6376
|
+
function mapLibreZoomOfFlatView(map, flatZoom, lat, lng) {
|
|
6377
|
+
const el = map.getContainer();
|
|
6378
|
+
return flatViewToMapLibreZoom(flatZoom, lat, lng, el.clientWidth, el.clientHeight, mapSvgProjection(map));
|
|
6379
|
+
}
|
|
5918
6380
|
// map scale denominator at a MapLibre zoom (null → the default reference):
|
|
5919
6381
|
// flat's own map scale, the one its scale bar shows and every scale gate
|
|
5920
6382
|
// (featureupper, chartupper, boxupper, valueupper, aggregation brackets)
|
|
@@ -5939,9 +6401,20 @@
|
|
|
5939
6401
|
// (e.g. "fixed") means they don't — same pixel size at every zoom.
|
|
5940
6402
|
// the zoom at which flat's object scale equals normalSizeScale (symbols
|
|
5941
6403
|
// at their normal size) — see FLAT_OBJECT_SCALE_CONSTANT
|
|
6404
|
+
// (under a flat SVG projection, flat's scale at that zoom is
|
|
6405
|
+
// flatMapScaleRatio × the Mercator one)
|
|
5942
6406
|
function resolveZoomReference(mapOptions) {
|
|
5943
|
-
const scaleDenominator = parseFloat(mapOptions
|
|
5944
|
-
return flatToMapLibreZoom(Math.log2(FLAT_OBJECT_SCALE_CONSTANT / scaleDenominator));
|
|
6407
|
+
const scaleDenominator = parseFloat(normalSizeScaleOf(mapOptions)) || FLAT_DEFAULT_NORMAL_SIZE_SCALE;
|
|
6408
|
+
return flatToMapLibreZoom(Math.log2(FLAT_OBJECT_SCALE_CONSTANT * flatMapScaleRatio(mapOptions) / scaleDenominator));
|
|
6409
|
+
}
|
|
6410
|
+
// .options({normalSizeScale}) under any spelling: flat matches the key
|
|
6411
|
+
// case-insensitively (htmlgui.js, /normalSizeScale/i → setScaleParam;
|
|
6412
|
+
// the last matching key wins), e.g. "normalsizescale"
|
|
6413
|
+
function normalSizeScaleOf(mapOptions) {
|
|
6414
|
+
if (!mapOptions || typeof mapOptions !== 'object') return undefined;
|
|
6415
|
+
let value;
|
|
6416
|
+
for (const key of Object.keys(mapOptions)) if (/normalSizeScale/i.test(key)) value = mapOptions[key];
|
|
6417
|
+
return value;
|
|
5945
6418
|
}
|
|
5946
6419
|
|
|
5947
6420
|
// .options({dynamicScalePow}) — the REAL parameter name (confirmed in
|
|
@@ -6014,7 +6487,7 @@
|
|
|
6014
6487
|
// scale (resolveZoomReference); flat's real rule (maptheme.js 13408,
|
|
6015
6488
|
// 0.3 + 0.7 / ln(nZoom / dx)) is not ported yet
|
|
6016
6489
|
function autoOpacityZoomReference(mapOptions) {
|
|
6017
|
-
const scaleDenominator = parseFloat(mapOptions
|
|
6490
|
+
const scaleDenominator = parseFloat(normalSizeScaleOf(mapOptions));
|
|
6018
6491
|
if (!scaleDenominator) return flatToMapLibreZoom(DEFAULT_ZOOM_REFERENCE);
|
|
6019
6492
|
return flatToMapLibreZoom(Math.log2(WEBMERCATOR_SCALE_CONSTANT / scaleDenominator));
|
|
6020
6493
|
}
|
|
@@ -6349,6 +6822,12 @@
|
|
|
6349
6822
|
|
|
6350
6823
|
// the themes drawn by the symbol chart pipeline (_buildChartLayers): flat's
|
|
6351
6824
|
// BUBBLE/SQUARE/LABEL branch (maptheme.js 20647-20652), symbols, user charts
|
|
6825
|
+
// CHART|VECTOR / CHART|BEZIER — flat's flow lines (maptheme.js chartMap
|
|
6826
|
+
// 18148-18435, ahead of drawChart: they take precedence over PIE,
|
|
6827
|
+
// BUBBLE, SYMBOL ...; DOT's branch comes first)
|
|
6828
|
+
function isVectorChart(flags) {
|
|
6829
|
+
return flags.has('CHART') && (flags.has('VECTOR') || flags.has('BEZIER')) && !flags.has('DOT');
|
|
6830
|
+
}
|
|
6352
6831
|
function isSymbolChart(flags) {
|
|
6353
6832
|
return flags.has('CHART') && (flags.has('SYMBOL') || flags.has('USER') || flags.has('LABEL'));
|
|
6354
6833
|
}
|
|
@@ -6384,6 +6863,17 @@
|
|
|
6384
6863
|
// the item's radius, SYMBOL: the theme's normal radius)
|
|
6385
6864
|
// So "left" puts the chart's left edge o to the right of the point (and
|
|
6386
6865
|
// drops offsetx), "2left" one more h. Returns deck.gl's pixel offset.
|
|
6866
|
+
// flat's maxcharts (maptheme.js 16879-16885): chartMap sorts the charts
|
|
6867
|
+
// ascending (the biggest drawn last, on top) and skips the first
|
|
6868
|
+
// nToDraw − nMaxCharts, so only the biggest maxcharts are drawn — for
|
|
6869
|
+
// every chart type (symbols, pies, bars, VECTOR/BEZIER). In place, on the
|
|
6870
|
+
// already sorted list of charts to draw
|
|
6871
|
+
function applyFlatMaxCharts(list, style) {
|
|
6872
|
+
const n = Math.round(styleNum(style && style.maxcharts)) || 0;
|
|
6873
|
+
if (n > 0 && list.length > n) list.splice(0, list.length - n);
|
|
6874
|
+
return list;
|
|
6875
|
+
}
|
|
6876
|
+
|
|
6387
6877
|
function flatChartAlignOffset(style, g) {
|
|
6388
6878
|
const num = v => (isNaN(styleNum(v)) ? 0 : styleNum(v));
|
|
6389
6879
|
const sym = g.symbolScale || 1;
|
|
@@ -6426,6 +6916,53 @@
|
|
|
6426
6916
|
}
|
|
6427
6917
|
return [r, g, b].map(c => Math.min(255, Math.floor(c * f)));
|
|
6428
6918
|
}
|
|
6919
|
+
// A BUBBLE chart's look in flat (maptheme.js, the BUBBLE branch), for a
|
|
6920
|
+
// circle of screen radius r in the class color rgb, `unit` = flat's
|
|
6921
|
+
// normalX(1) on screen (the object zoom factor × style.scale):
|
|
6922
|
+
// - stroke (20740-20780, 20895): style.linecolor, else ChartColors.lowColor
|
|
6923
|
+
// (the color × 0.7, __maptheme_getChartColors) — none with NOLINES or
|
|
6924
|
+
// linecolor "none"
|
|
6925
|
+
// - width (20898-20903): linewidth (0.1) · unit · √(r / normal radius),
|
|
6926
|
+
// the normal radius 15 · unit; OUTLINE: unit · min(1, r / normal radius)
|
|
6927
|
+
// - fill-opacity fillopacity (1); stroke-opacity (21050-21056) 1 with
|
|
6928
|
+
// OUTLINE or a linecolor, else 1 below fillopacity 0.5, 0.3 above
|
|
6929
|
+
// (_buildLabelChartLayers spells the same rule out for CHART|LABEL's box.)
|
|
6930
|
+
function flatBubbleLook(rgb, r, unit, style, flags) {
|
|
6931
|
+
const fillOpacity = styleNum(style.fillopacity) || 1;
|
|
6932
|
+
const outline = flatFlag(flags, 'OUTLINE');
|
|
6933
|
+
const lineColor = style.linecolor ? styleLineColor(style.linecolor) : null;
|
|
6934
|
+
const stroke = lineColor ? (lineColor === 'none' ? null : hexOrNamedToRgb(lineColor))
|
|
6935
|
+
: flatFlag(flags, 'NOLINES') ? null : flatDerivateRgb(rgb, 0.7);
|
|
6936
|
+
const ratio = r / (NORMAL_RADIUS_PX * unit);
|
|
6937
|
+
const width = outline ? unit * Math.min(1, ratio) : unit * (styleNum(style.linewidth) || 0.1) * Math.sqrt(ratio);
|
|
6938
|
+
const strokeOpacity = outline || lineColor ? 1 : (fillOpacity < 0.5 ? 1 : 0.3);
|
|
6939
|
+
return { fillOpacity, stroke, strokeOpacity, width };
|
|
6940
|
+
}
|
|
6941
|
+
|
|
6942
|
+
// flatBubbleLook for a cached icon raster `size` canvas px wide, drawn 2r
|
|
6943
|
+
// screen px wide: the stroke width in canvas px, in quarter-octave steps
|
|
6944
|
+
// (≤ 19 % apart, at most the icon's radius) — every distinct width is
|
|
6945
|
+
// another icon in deck.gl's shared atlas (see ICON_ATLAS_RESET_AFTER), and
|
|
6946
|
+
// a continuous width would make one per bubble. It hardly depends on the
|
|
6947
|
+
// zoom: r and the width both scale with the object zoom factor. `stroke`
|
|
6948
|
+
// only when it is style.linecolor (a packed icon derives each part's own,
|
|
6949
|
+
// see _buildBubbleIcon), noStroke for NOLINES / linecolor "none";
|
|
6950
|
+
// outlineRatio (OUTLINE): r / normal radius, also stepped, as 1 from 1 up
|
|
6951
|
+
// (where flat's OUTLINE width stops growing).
|
|
6952
|
+
function bubbleIconLook(r, unit, style, flags, size) {
|
|
6953
|
+
const look = flatBubbleLook([0, 0, 0], r, unit, style, flags);
|
|
6954
|
+
const step = v => (v > 0 ? Math.round(Math.pow(2, Math.round(Math.log2(v) * 4) / 4) * 1000) / 1000 : 0);
|
|
6955
|
+
const out = {
|
|
6956
|
+
fillOpacity: look.fillOpacity,
|
|
6957
|
+
strokeOpacity: look.strokeOpacity,
|
|
6958
|
+
stroke: style.linecolor ? look.stroke : null,
|
|
6959
|
+
noStroke: !look.stroke,
|
|
6960
|
+
width: r > 0 ? Math.min(size / 2, step(look.width * size / (2 * r))) : 0,
|
|
6961
|
+
};
|
|
6962
|
+
if (flatFlag(flags, 'OUTLINE')) out.outlineRatio = Math.min(1, step(r / (NORMAL_RADIUS_PX * unit)));
|
|
6963
|
+
return out;
|
|
6964
|
+
}
|
|
6965
|
+
|
|
6429
6966
|
// ChartColors.textColor (colorscheme.js 104-116): darker on light colors
|
|
6430
6967
|
function flatChartTextRgb(rgb) {
|
|
6431
6968
|
return flatDerivateRgb(rgb, rgb[0] + rgb[1] + rgb[2] > 450 ? 0.6 : 3);
|
|
@@ -7045,6 +7582,94 @@
|
|
|
7045
7582
|
return { offset, index, extent };
|
|
7046
7583
|
}
|
|
7047
7584
|
|
|
7585
|
+
// Chart texts covered by charts drawn after them. Flat draws each chart
|
|
7586
|
+
// as one SVG group (symbol, then its value text) in the sorted draw
|
|
7587
|
+
// order, so a later chart's symbol paints over the texts of every chart
|
|
7588
|
+
// beneath it; here the texts are separate TextLayers above every icon.
|
|
7589
|
+
// This composites that occlusion on the CPU: for each text, the
|
|
7590
|
+
// transmittance Π(1 − α) of every LATER chart's symbol parts at a few
|
|
7591
|
+
// sample points of the text box, averaged — the fraction of the text
|
|
7592
|
+
// still showing through, its alpha. Partly covered texts fade instead
|
|
7593
|
+
// of being clipped (no per-pixel mask in deck.gl).
|
|
7594
|
+
// charts: in draw order, { x, y, parts: [{ dx, dy, r, shape, alpha }] }
|
|
7595
|
+
// in screen pixels, a part's alpha its opacity
|
|
7596
|
+
// texts: { order (index into charts), x, y, w, h }
|
|
7597
|
+
// Returns each text's transmittance, 0..1.
|
|
7598
|
+
const OCCLUSION_CELL_PX = 32;
|
|
7599
|
+
// below this a text is as good as hidden: left out of its TextLayer
|
|
7600
|
+
const CHART_TEXT_MIN_TRANSMITTANCE = 0.08;
|
|
7601
|
+
function chartTextTransmittance(charts, texts) {
|
|
7602
|
+
// the texts' sample points, then a grid over just their extent holding,
|
|
7603
|
+
// per cell, the charts (in draw order) whose reach overlaps it
|
|
7604
|
+
const SAMPLES = [[0, 0], [-0.35, -0.3], [0.35, -0.3], [-0.35, 0.3], [0.35, 0.3]];
|
|
7605
|
+
const nS = SAMPLES.length, px = new Float64Array(texts.length * nS), py = new Float64Array(texts.length * nS);
|
|
7606
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
7607
|
+
texts.forEach((t, i) => SAMPLES.forEach(([sx, sy], j) => {
|
|
7608
|
+
const x = px[i * nS + j] = t.x + sx * t.w, y = py[i * nS + j] = t.y + sy * t.h;
|
|
7609
|
+
if (x < minX) minX = x; if (x > maxX) maxX = x;
|
|
7610
|
+
if (y < minY) minY = y; if (y > maxY) maxY = y;
|
|
7611
|
+
}));
|
|
7612
|
+
const out = new Array(texts.length).fill(1);
|
|
7613
|
+
if (!texts.length || !charts.length) return out;
|
|
7614
|
+
const C = OCCLUSION_CELL_PX;
|
|
7615
|
+
const W = Math.floor((maxX - minX) / C) + 1, H = Math.floor((maxY - minY) / C) + 1;
|
|
7616
|
+
const reach = charts.map(c => {
|
|
7617
|
+
let ext = 0;
|
|
7618
|
+
for (const p of c.parts) ext = Math.max(ext, Math.sqrt(p.dx * p.dx + p.dy * p.dy) + p.r * Math.SQRT2);
|
|
7619
|
+
return ext;
|
|
7620
|
+
});
|
|
7621
|
+
const range = (c, ext) => [Math.max(0, Math.floor((c.x - ext - minX) / C)), Math.min(W - 1, Math.floor((c.x + ext - minX) / C)),
|
|
7622
|
+
Math.max(0, Math.floor((c.y - ext - minY) / C)), Math.min(H - 1, Math.floor((c.y + ext - minY) / C))];
|
|
7623
|
+
// two passes (count, fill) into one flat array: cells keep draw order
|
|
7624
|
+
const start = new Int32Array(W * H + 1);
|
|
7625
|
+
charts.forEach((c, k) => {
|
|
7626
|
+
const [x0, x1, y0, y1] = range(c, reach[k]);
|
|
7627
|
+
for (let gy = y0; gy <= y1; gy++) for (let gx = x0; gx <= x1; gx++) start[gy * W + gx + 1]++;
|
|
7628
|
+
});
|
|
7629
|
+
for (let i = 0; i < W * H; i++) start[i + 1] += start[i];
|
|
7630
|
+
const fill = start.slice(0, W * H), cells = new Int32Array(start[W * H]);
|
|
7631
|
+
charts.forEach((c, k) => {
|
|
7632
|
+
const [x0, x1, y0, y1] = range(c, reach[k]);
|
|
7633
|
+
for (let gy = y0; gy <= y1; gy++) for (let gx = x0; gx <= x1; gx++) cells[fill[gy * W + gx]++] = k;
|
|
7634
|
+
});
|
|
7635
|
+
// what of (x, y) still shows through chart c's parts
|
|
7636
|
+
const keepAt = (c, x, y) => {
|
|
7637
|
+
let keep = 1;
|
|
7638
|
+
for (const p of c.parts) {
|
|
7639
|
+
if (insideSymbolShape(p.shape, x - c.x - p.dx, y - c.y - p.dy, p.r)) keep *= 1 - p.alpha;
|
|
7640
|
+
}
|
|
7641
|
+
return keep;
|
|
7642
|
+
};
|
|
7643
|
+
texts.forEach((t, i) => {
|
|
7644
|
+
let sum = 0;
|
|
7645
|
+
for (let j = 0; j < nS; j++) {
|
|
7646
|
+
const x = px[i * nS + j], y = py[i * nS + j];
|
|
7647
|
+
const cell = Math.floor((y - minY) / C) * W + Math.floor((x - minX) / C);
|
|
7648
|
+
let keep = 1;
|
|
7649
|
+
// only the charts drawn after the text's own cover it
|
|
7650
|
+
for (let n = start[cell + 1] - 1; n >= start[cell] && cells[n] > t.order && keep > 0.005; n--) {
|
|
7651
|
+
const k = cells[n], c = charts[k], dx = x - c.x, dy = y - c.y;
|
|
7652
|
+
if (dx * dx + dy * dy < reach[k] * reach[k]) keep *= keepAt(c, x, y);
|
|
7653
|
+
}
|
|
7654
|
+
sum += keep;
|
|
7655
|
+
}
|
|
7656
|
+
out[i] = sum / nS;
|
|
7657
|
+
});
|
|
7658
|
+
return out;
|
|
7659
|
+
}
|
|
7660
|
+
// inside drawSymbolPath's shape of radius r, (dx, dy) from its center;
|
|
7661
|
+
// the shapes without their own test as the circle
|
|
7662
|
+
function insideSymbolShape(shape, dx, dy, r) {
|
|
7663
|
+
switch (shape) {
|
|
7664
|
+
case 'squarefull': return Math.abs(dx) <= r && Math.abs(dy) <= r;
|
|
7665
|
+
case 'square': return Math.abs(dx) <= r * Math.SQRT1_2 && Math.abs(dy) <= r * Math.SQRT1_2;
|
|
7666
|
+
case 'roundrect': return Math.abs(dx) <= r * 0.8 && Math.abs(dy) <= r * 0.8;
|
|
7667
|
+
case 'diamond': case 'carot': return Math.abs(dx) + Math.abs(dy) <= r;
|
|
7668
|
+
case 'empty': return false;
|
|
7669
|
+
default: return dx * dx + dy * dy <= r * r;
|
|
7670
|
+
}
|
|
7671
|
+
}
|
|
7672
|
+
|
|
7048
7673
|
// A pixel offset [dx, dy] (y down) from lngLat at MapLibre zoom `zoom`,
|
|
7049
7674
|
// as lng/lat (Web Mercator, 512 px world tiles) — for shapes too large
|
|
7050
7675
|
// for an icon, drawn in map coordinates (rebuilt with every refresh).
|
|
@@ -7056,6 +7681,915 @@
|
|
|
7056
7681
|
return [lngLat[0] + offset[0] * 360 / world, lat];
|
|
7057
7682
|
}
|
|
7058
7683
|
|
|
7684
|
+
// CHART|BAR / BARS: which flat draws as a bar chart — drawChart's BAR
|
|
7685
|
+
// branch (maptheme.js 23283) comes after USER, PIE, WAFFLE, BUBBLE,
|
|
7686
|
+
// SYMBOL and BUFFER; flat tests /BAR/ as a substring (BARS too)
|
|
7687
|
+
function isBarChart(flags) {
|
|
7688
|
+
if (!flags.has('CHART') || isVectorChart(flags) || flags.has('DOT')) return false;
|
|
7689
|
+
const t = flags.typeString != null ? flags.typeString : [...flags].join('|');
|
|
7690
|
+
return /BAR/.test(t) && !/\bUSER\b|PIE|WAFFLE|BUBBLE|SYMBOL|BUFFER/.test(t);
|
|
7691
|
+
}
|
|
7692
|
+
|
|
7693
|
+
// One item's bar chart as real ixmaps-flat draws it — maptheme.js
|
|
7694
|
+
// drawChart's BAR branch (23283-24425) — in flat's normal units (u:
|
|
7695
|
+
// normalX(1), the chart size is 30 u; px = u · the object zoom factor ×
|
|
7696
|
+
// style.scale), y down, relative to the item's position. `values`: the
|
|
7697
|
+
// item's parts (nValuesA, after aggregation); `t`: the type string,
|
|
7698
|
+
// tested as flat does, by substring. `o`:
|
|
7699
|
+
// themeMin / themeMax the theme's nMin / nMax (the items' sizes with a
|
|
7700
|
+
// size field or CATEGORICAL, else their parts)
|
|
7701
|
+
// maxSize, itemSize the theme's nMaxSize, the item's nSize (size field)
|
|
7702
|
+
// sizeField a size field is bound
|
|
7703
|
+
// normalSizeValue, sizePow, rangeScale, valueScale, units,
|
|
7704
|
+
// valueDecimals (explicit, else undefined), noBreaks, gridX, nClasses
|
|
7705
|
+
// (partsA.length), classMax (partsA[k].max), nColors, fillOpacity,
|
|
7706
|
+
// lineWidth, lineColor (linecolor, else undefined), fadeNegative,
|
|
7707
|
+
// fadeValuePow, hideValues (valueupper/lower), means / medians / devs
|
|
7708
|
+
// (per part, OFFSETMEAN / OFFSETMEDIAN / DEVIATION)
|
|
7709
|
+
// measure(text, font) the text's width at that font size (u)
|
|
7710
|
+
// Returns null when flat draws no chart (no size, or a single 0 part),
|
|
7711
|
+
// else { bars, texts, lines, extent: [x0, y0, x1, y1] } —
|
|
7712
|
+
// bars: { part (the value's index), cls (its color index), base
|
|
7713
|
+
// [x, y] (the middle of the shaft's foot), dir (0 up, 1 right,
|
|
7714
|
+
// 2 down, 3 left), w2 (half width), sw (shaft half width), len
|
|
7715
|
+
// (shaft), band, head, value, textValue, fill: { opacity },
|
|
7716
|
+
// line: { rgb: 'color' (the bar's) | null (lineColor or black),
|
|
7717
|
+
// width, opacity }, opacity (FADEIN / FADENEGATIVE) }
|
|
7718
|
+
// texts: { text, x, y (the baseline's start), angle (0 reading right,
|
|
7719
|
+
// 90 up, -90 down), font, opacity, color ('value' — flat's
|
|
7720
|
+
// #798697 values color, 'red', 'contrast' — ChartColors
|
|
7721
|
+
// .textColor of the bar's color —, or a CSS color), bg (null,
|
|
7722
|
+
// 'white' — #fefeff —, or 'bar' — the bar's color at 0.5),
|
|
7723
|
+
// cls, part }
|
|
7724
|
+
// lines: STACKED leader lines { x0, y0, x1, y1 }
|
|
7725
|
+
// Divergences (not drawn here): 3D, VOLUME, COLUMN's cylinders, DOTTED,
|
|
7726
|
+
// SMALLARROW (drawn as ARROW), TRENDLINE, the XAXIS/AXIS labels, the
|
|
7727
|
+
// mean/median marks, CLIP / MORPH frames, showparts / clipparts, ZOOM /
|
|
7728
|
+
// NORMSIZE / MENUSIZE, INVERT, DIAGLEFT / DIAGRIGHT — bars are drawn
|
|
7729
|
+
// plain instead (see BAR_INERT_FLAGS).
|
|
7730
|
+
function flatBarLayout(values, t, o) {
|
|
7731
|
+
t = String(t || '');
|
|
7732
|
+
const has = re => re.test(t);
|
|
7733
|
+
const n = values.length;
|
|
7734
|
+
if (!n) return null;
|
|
7735
|
+
const parts = values.map(v => (isFinite(v) ? v : 0));
|
|
7736
|
+
const sum = parts.reduce((a, v) => a + v, 0);
|
|
7737
|
+
const stacked = has(/STACKED/), pointer = has(/POINTER/), horz = has(/HOR/);
|
|
7738
|
+
const sizeFlag = has(/SIZE/) && !has(/NORMSIZE/);
|
|
7739
|
+
const sizePow = o.sizePow || 2;
|
|
7740
|
+
const CHART = 30;
|
|
7741
|
+
let nMax = Math.max(o.themeMax, Math.abs(o.themeMin));
|
|
7742
|
+
let nRange = o.themeMax - o.themeMin;
|
|
7743
|
+
let maxSize = o.maxSize;
|
|
7744
|
+
if (o.normalSizeValue) nRange = maxSize = o.normalSizeValue;
|
|
7745
|
+
// flat's getChartSize (no chart when 0) and its "a single 0" rule
|
|
7746
|
+
if (sizeFlag && !has(/NOSIZE/)) {
|
|
7747
|
+
const s = o.sizeField ? o.itemSize : Math.abs(sum);
|
|
7748
|
+
const ref = o.sizeField ? maxSize : nRange;
|
|
7749
|
+
const size = has(/SIZELOG/) ? Math.max(1, 15 / Math.log(ref) * Math.log(s)) : 15 / Math.pow(ref, 1 / sizePow) * Math.pow(s, 1 / sizePow);
|
|
7750
|
+
if (isNaN(size) || size === 0) return null;
|
|
7751
|
+
}
|
|
7752
|
+
if (n === 1 && parts[0] === 0 && !has(/ZEROISVALUE|AUTOCOMPLETE|CATEGORICAL/)) return null;
|
|
7753
|
+
|
|
7754
|
+
// the sizer: the chart's size by the size field or by the values' sum
|
|
7755
|
+
let sizer = 1;
|
|
7756
|
+
if (sizeFlag && o.sizeField && o.itemSize != null) {
|
|
7757
|
+
sizer = has(/SIZELOG/) ? 1 / Math.max(1, Math.log(maxSize) * Math.log(o.itemSize))
|
|
7758
|
+
: Math.pow(o.itemSize, 1 / sizePow) / Math.pow(maxSize, 1 / sizePow);
|
|
7759
|
+
} else if (sizeFlag) {
|
|
7760
|
+
const s = pointer ? sum / n : sum;
|
|
7761
|
+
sizer = has(/SIZELOG/) ? 1 / Math.max(1, Math.log(nRange) * Math.log(Math.abs(s)))
|
|
7762
|
+
: Math.pow(Math.abs(s), 1 / sizePow) / Math.pow(nRange, 1 / sizePow);
|
|
7763
|
+
}
|
|
7764
|
+
if (sizer === 0 && sum !== 0) sizer = 1;
|
|
7765
|
+
const chartSize = CHART * sizer;
|
|
7766
|
+
|
|
7767
|
+
// bar width and value text size
|
|
7768
|
+
let W = chartSize / n * 0.66;
|
|
7769
|
+
if (stacked) {
|
|
7770
|
+
W = chartSize / 3;
|
|
7771
|
+
if (o.gridX && !has(/MULTI/)) W /= 2;
|
|
7772
|
+
}
|
|
7773
|
+
let f = W < 5 ? W * 4 / 5 : W;
|
|
7774
|
+
let textOpacity = 1;
|
|
7775
|
+
if (has(/COLUMN/)) W *= has(/THICK/) ? 2 : has(/THIN/) ? 1 : 1.5;
|
|
7776
|
+
if (has(/THIN/)) { W *= 0.6; f *= 0.75; }
|
|
7777
|
+
if (has(/THICK/)) { W *= 1.5; f *= 1.5; }
|
|
7778
|
+
if (stacked) f = 5;
|
|
7779
|
+
if (stacked && !has(/ZOOM/)) f *= sizer;
|
|
7780
|
+
const valueScale = o.valueScale || 1;
|
|
7781
|
+
f *= valueScale;
|
|
7782
|
+
|
|
7783
|
+
// the bar length per value unit (nStep)
|
|
7784
|
+
if (o.normalSizeValue && (!o.field100 || pointer)) nMax = o.normalSizeValue;
|
|
7785
|
+
let step0 = CHART / nMax;
|
|
7786
|
+
if (has(/OFFSETMEAN|OFFSETMEDIAN|DEVIATION/)) step0 = chartSize / 300;
|
|
7787
|
+
if (sizeFlag && has(/SEQUENCE/) && n > 1 && !o.sizeField && !o.field100) step0 = step0 * n / sizer;
|
|
7788
|
+
else if (sizeFlag && n > 1 && !has(/EXPAND/)) {
|
|
7789
|
+
if (o.field100 || pointer) step0 *= sizer;
|
|
7790
|
+
else step0 /= has(/SIZEP4/) ? sizer * sizer : sizer;
|
|
7791
|
+
}
|
|
7792
|
+
if (o.rangeScale) step0 *= o.rangeScale;
|
|
7793
|
+
if (has(/COMPRESSMAX/)) step0 /= 4;
|
|
7794
|
+
else if (has(/COMPRESSMORE/)) step0 /= 3;
|
|
7795
|
+
else if (has(/COMPRESS/)) step0 /= 2;
|
|
7796
|
+
else if (has(/EXPANDMAX/)) step0 *= 4;
|
|
7797
|
+
else if (has(/EXPANDMORE/)) step0 *= 3;
|
|
7798
|
+
else if (has(/EXPAND/)) step0 *= 2;
|
|
7799
|
+
if (pointer && !has(/NORMSIZE/)) step0 *= 1.2;
|
|
7800
|
+
|
|
7801
|
+
// the bar order: SORT descending (STACKED ascending), UP reversed
|
|
7802
|
+
let sorted = null;
|
|
7803
|
+
if (has(/\bSORT\b/)) {
|
|
7804
|
+
sorted = parts.map((v, i) => ({ i, v }));
|
|
7805
|
+
sorted.sort(stacked ? (a, b) => a.v - b.v : (a, b) => b.v - a.v);
|
|
7806
|
+
sorted = sorted.map(s => s.i);
|
|
7807
|
+
}
|
|
7808
|
+
const startI = has(/\bUP\b/) ? n - 1 : 0;
|
|
7809
|
+
const nC = has(/CENTER/) ? 2 : 1;
|
|
7810
|
+
const fmt = (v, d) => flatGroupedValue(v, d, o.noBreaks);
|
|
7811
|
+
const measure = o.measure || ((text, font) => text.length * font * 0.55);
|
|
7812
|
+
|
|
7813
|
+
const bars = [], texts = [], lines = [];
|
|
7814
|
+
let posX = 0, posY = 0, minNext = 0, barsDrawn = 0, step = step0;
|
|
7815
|
+
for (let i = 0; i < n; i++) {
|
|
7816
|
+
step = step0;
|
|
7817
|
+
let idx = Math.abs(startI - i);
|
|
7818
|
+
if (sorted) idx = sorted[idx];
|
|
7819
|
+
let v = parts[idx];
|
|
7820
|
+
let tv = v;
|
|
7821
|
+
if (has(/OFFSETMEAN|OFFSETMEDIAN/)) {
|
|
7822
|
+
const ref = has(/OFFSETMEDIAN/) ? (o.medians || [])[idx] : (o.means || [])[idx];
|
|
7823
|
+
v = 100 / ref * v - 100;
|
|
7824
|
+
v = isFinite(v) ? v : 0;
|
|
7825
|
+
tv = v;
|
|
7826
|
+
nMax = 100;
|
|
7827
|
+
} else if (has(/DEVIATION/)) {
|
|
7828
|
+
v = (v - (o.means || [])[idx]) / (o.devs || [])[idx];
|
|
7829
|
+
v = isFinite(v) ? v : 0;
|
|
7830
|
+
tv = v;
|
|
7831
|
+
v *= 100;
|
|
7832
|
+
}
|
|
7833
|
+
// the color: the part's, or (SEQUENCE, a single part) the class of
|
|
7834
|
+
// the value
|
|
7835
|
+
let cls = o.nColors ? (idx < o.nColors ? idx : idx % (o.gridX || 1000000)) : idx;
|
|
7836
|
+
if ((has(/SEQUENCE|CLIP/) || n === 1) && o.nColors >= 2 && o.classMax && o.classMax.length) {
|
|
7837
|
+
cls = o.classMax.length - 1;
|
|
7838
|
+
for (let k = 0; k < o.classMax.length; k++) if (v < o.classMax[k]) { cls = k; break; }
|
|
7839
|
+
}
|
|
7840
|
+
if (has(/LEFT/) && has(/HORZ/)) v = -v;
|
|
7841
|
+
if (v < 0 && !stacked) { posY = -v * step; v = -v; }
|
|
7842
|
+
if (pointer && sizeFlag) {
|
|
7843
|
+
// flat's sized pointer: its width by the value (the WIDTH variant
|
|
7844
|
+
// by value100 is not ported)
|
|
7845
|
+
W = chartSize * Math.pow(Math.abs(v / nMax), 1 / 3);
|
|
7846
|
+
f = W * 0.6;
|
|
7847
|
+
if (!has(/ZOOM|XAXIS/)) {
|
|
7848
|
+
textOpacity = Math.pow(Math.abs(v), 1 / 2) / Math.pow(o.themeMax, 1 / 2);
|
|
7849
|
+
if (o.fadeValuePow) textOpacity = Math.pow(Math.abs(v), o.fadeValuePow) / Math.pow(nMax, o.fadeValuePow);
|
|
7850
|
+
}
|
|
7851
|
+
}
|
|
7852
|
+
if (has(/DTEXT/) && !has(/ZOOM/)) {
|
|
7853
|
+
f = (chartSize * 2 / 3) * Math.pow(Math.abs(v / nMax), 1 / 2) * 0.75;
|
|
7854
|
+
textOpacity = Math.pow(Math.abs(v), 1 / 3) / Math.pow(nMax, 1 / 3);
|
|
7855
|
+
}
|
|
7856
|
+
v = isFinite(v) ? v : 0;
|
|
7857
|
+
if (v === 0 && has(/NOZERO/)) { posY = 0; continue; }
|
|
7858
|
+
if (tv < 0 && has(/NONEGATIVE/)) { posY = 0; continue; }
|
|
7859
|
+
if (tv > 0 && has(/ONLYNEGATIVE/)) { posY = 0; continue; }
|
|
7860
|
+
|
|
7861
|
+
const oy = posY;
|
|
7862
|
+
const bar = { part: idx, cls, value: v, textValue: tv, dir: 0, w2: W / 2, sw: W / 2, band: 0, head: 0, len: 0,
|
|
7863
|
+
fill: { opacity: o.fillOpacity || 1 }, line: null, opacity: 1 };
|
|
7864
|
+
let drawn = true;
|
|
7865
|
+
if (pointer && v) {
|
|
7866
|
+
if (has(/OFFSETMEAN|OFFSETMEDIAN|DEVIATION/) && Math.abs(v * step) > 50) {
|
|
7867
|
+
const h = Math.abs(v * step);
|
|
7868
|
+
step = (50 + (h - 50) / 10 + h % 50) / Math.abs(v);
|
|
7869
|
+
}
|
|
7870
|
+
const H = v * step;
|
|
7871
|
+
const arrow = has(/ARROW/);
|
|
7872
|
+
const style = { rgb: null, width: o.lineWidth ? o.lineWidth * Math.sqrt(sizer || 1) : 0.05, opacity: 0.5 };
|
|
7873
|
+
if (oy <= 0) {
|
|
7874
|
+
// positive: the shaft from the base up, a band as wide as the bar,
|
|
7875
|
+
// the head on it
|
|
7876
|
+
Object.assign(bar, arrow
|
|
7877
|
+
? { base: [posX + W / 2, oy - 0.9 * H / nC + 0.9 * H], len: 0.9 * H, sw: W / 6, band: W / 15, head: W / 1.5 }
|
|
7878
|
+
: { base: [posX + W / 2, oy - H / nC + H], len: H, sw: W * 5 / 14, band: W / 15, head: W / 2 });
|
|
7879
|
+
bar.line = style;
|
|
7880
|
+
} else if (!has(/NONEGATIVE/)) {
|
|
7881
|
+
// negative: from the zero line down, the head below
|
|
7882
|
+
Object.assign(bar, arrow
|
|
7883
|
+
? { base: [posX + W / 2, oy - H / nC], len: H, sw: W / 6, head: W / 2, dir: 2 }
|
|
7884
|
+
: { base: [posX + W / 2, oy - H / nC], len: H, sw: W * 5 / 14, head: W / 4, dir: 2 });
|
|
7885
|
+
bar.line = style;
|
|
7886
|
+
if (!has(/LEFT/)) {
|
|
7887
|
+
const fadeNeg = o.fadeNegative || 0.1;
|
|
7888
|
+
if (has(/ZOOM|NORMSIZE/)) {
|
|
7889
|
+
bar.fill.opacity = 0.5;
|
|
7890
|
+
bar.line = { rgb: 'color', width: 0.25, opacity: 1 };
|
|
7891
|
+
} else if (o.nClasses > 2) {
|
|
7892
|
+
bar.fill.opacity = fadeNeg;
|
|
7893
|
+
bar.line = { rgb: 'low', width: o.lineWidth ? o.lineWidth * 0.25 * sizer : 0.25, opacity: 0.8 };
|
|
7894
|
+
} else {
|
|
7895
|
+
bar.fill.opacity = fadeNeg;
|
|
7896
|
+
bar.line = { rgb: 'high', width: o.lineWidth ? o.lineWidth * Math.sqrt(sizer || 1) : 0.05, opacity: 1 };
|
|
7897
|
+
}
|
|
7898
|
+
}
|
|
7899
|
+
} else drawn = false;
|
|
7900
|
+
} else {
|
|
7901
|
+
// plain 2D bar (DOTTED, 3D, COLUMN are drawn as this too)
|
|
7902
|
+
if (!has(/NOZERO/)) v = v || 0.00001;
|
|
7903
|
+
const H = v * step;
|
|
7904
|
+
bar.value = v;
|
|
7905
|
+
if (H < 0) drawn = false;
|
|
7906
|
+
bar.base = [posX + W / 2, oy - H / nC + H];
|
|
7907
|
+
bar.len = H;
|
|
7908
|
+
if (oy <= 0 || has(/LEFT/)) {
|
|
7909
|
+
bar.line = { rgb: null, width: o.lineWidth ? o.lineWidth * 0.25 * sizer : 0.2, opacity: o.lineWidth ? 1 : 0.1 };
|
|
7910
|
+
} else {
|
|
7911
|
+
bar.fill.opacity = 0.3;
|
|
7912
|
+
bar.line = { rgb: 'color', width: 0.25, opacity: 1 };
|
|
7913
|
+
}
|
|
7914
|
+
}
|
|
7915
|
+
if (has(/FADENEGATIVE/) && tv < 0) bar.opacity = 0.5;
|
|
7916
|
+
if (has(/FADEIN/)) {
|
|
7917
|
+
if (n === 2 && i === 0) bar.opacity = 0.6;
|
|
7918
|
+
else if (!has(/NORMSIZE/)) {
|
|
7919
|
+
const p = has(/FADEINP4/) ? 10 : 3;
|
|
7920
|
+
bar.opacity = 0.1 + 0.9 * Math.pow(i + 1, p) / Math.pow(n, p);
|
|
7921
|
+
}
|
|
7922
|
+
}
|
|
7923
|
+
if (drawn) bars.push(bar);
|
|
7924
|
+
|
|
7925
|
+
// the value text
|
|
7926
|
+
const H = v * step;
|
|
7927
|
+
if (has(/VALUES/) && !(oy > 0 && has(/NONEGATIVE/)) && !o.hideValues
|
|
7928
|
+
&& f > 0 && !(o.gridX && o.nClasses / o.gridX > 2)) {
|
|
7929
|
+
let text = fmt(tv, o.valueDecimals != null ? o.valueDecimals : (o.themeMax <= 1 ? 1 : 0));
|
|
7930
|
+
if (pointer && tv > 0 && (o.themeMin < 0 || has(/OFFSETMEAN|OFFSETMEDIAN|\bSIGN\b/))) text = '+' + text;
|
|
7931
|
+
if (!has(/VOLUME/)) text += o.units ? ' ' + o.units + ' ' : '';
|
|
7932
|
+
if (stacked && tv !== 0 && !o.field100 && !o.gridX) text += ' (' + Math.round(tv / sum * 100) + '%)';
|
|
7933
|
+
if (stacked) f = (has(/ZOOM/) ? 18 : 5) * valueScale;
|
|
7934
|
+
// SVG collapses the blanks
|
|
7935
|
+
text = text.replace(/\s+/g, ' ').trim();
|
|
7936
|
+
const red = parseFloat(text) < 0;
|
|
7937
|
+
// createTextLabel's box: not with HORZ; with ZOOM … AXIS only
|
|
7938
|
+
// colored (VALUEBACKGROUND / CTEXT); colored for a positive value
|
|
7939
|
+
const colored = tv > 0 && has(/CTEXT|VALUEBACKGROUND/);
|
|
7940
|
+
let bgOn = true;
|
|
7941
|
+
if (has(/ZOOM|MENUSIZE|NORMSIZE|INFOSIZE|AXIS|SELECTION/)) bgOn = colored;
|
|
7942
|
+
if (has(/VALUEBACKGROUND/)) bgOn = true;
|
|
7943
|
+
if (horz) bgOn = false;
|
|
7944
|
+
const bg = bgOn ? (colored ? 'bar' : 'white') : null;
|
|
7945
|
+
// the text color: negative red (valuecolor), else textcolor; on the
|
|
7946
|
+
// bar's color its text color, else flat's values color
|
|
7947
|
+
const color = red ? (o.valueColor || 'red') : (o.textColor || (bg === 'bar' ? 'contrast' : 'value'));
|
|
7948
|
+
const textW = measure(text, f);
|
|
7949
|
+
const tLen = textW + (bgOn ? 0.7 * f : 0);
|
|
7950
|
+
const ptY = W / 2 + f * 0.15;
|
|
7951
|
+
const outside = (!has(/DOINLINETEXT/) || has(/ZOOM/) || has(/NORMSIZE/)) && has(/VALUE/) && !(has(/VOLUME/) && !has(/ZOOM/))
|
|
7952
|
+
&& (has(/THIN|NOINLINETEXT|DTEXT|SIZE|STACKED|NORMSIZE|ZOOM/) || tLen + 1 > H / nC);
|
|
7953
|
+
const base = { text, cls, part: idx, opacity: textOpacity, color, bg };
|
|
7954
|
+
if (outside && stacked) {
|
|
7955
|
+
if (!(v === 0 && !has(/ZEROISVALUE/)) && !(o.gridX && n / o.gridX > 2)) {
|
|
7956
|
+
const topDx = H / 2;
|
|
7957
|
+
minNext = Math.max(0, minNext + (f * 0.75 - topDx));
|
|
7958
|
+
const textPosX = minNext + topDx - f / 3;
|
|
7959
|
+
let indent = 0.5 * sizer;
|
|
7960
|
+
let ptYs = ptY;
|
|
7961
|
+
let x1 = posX + ptY + W / 3, x2 = posX + ptY + W * 7 / 8;
|
|
7962
|
+
const y1 = -topDx, y2 = -textPosX - f / 3;
|
|
7963
|
+
if (o.gridX && barsDrawn < o.gridX) {
|
|
7964
|
+
ptYs -= W * 2 + tLen;
|
|
7965
|
+
const tmp = x2;
|
|
7966
|
+
x2 = x1 - W * 1.4;
|
|
7967
|
+
x1 = tmp - W * 1.4;
|
|
7968
|
+
indent = -indent;
|
|
7969
|
+
}
|
|
7970
|
+
texts.push(Object.assign(base, { x: posX + ptYs + W + indent, y: oy - textPosX, angle: 0, font: f }));
|
|
7971
|
+
[[x1, y1, x1 + indent, y1], [x1 + indent, y1, x2, y2], [x2, y2, x2 + indent, y2]]
|
|
7972
|
+
.forEach(([a, b, c, d]) => lines.push({ x0: a, y0: oy + b, x1: c, y1: oy + d }));
|
|
7973
|
+
minNext = Math.max(0, minNext + (f * 2 / 3 - (H - topDx)));
|
|
7974
|
+
}
|
|
7975
|
+
} else if (outside) {
|
|
7976
|
+
const lift = pointer && tv > 0 ? W * 0.5 : 0;
|
|
7977
|
+
if (n === 1 && !has(/HORZ/) && !has(/VOLUME/)) {
|
|
7978
|
+
texts.push(Object.assign(base, { x: posX - W * 0.8 + f, y: oy - H / nC - f * 0.8 - lift + 0.25 * f, angle: 0, font: f,
|
|
7979
|
+
opacity: tv < 0 ? textOpacity * 0.75 : textOpacity }));
|
|
7980
|
+
} else {
|
|
7981
|
+
// rotate(270) scale(0.9): reading up, 0.9 font
|
|
7982
|
+
texts.push(Object.assign(base, { x: posX + ptY + 0.9 * 0.25 * f, y: oy - 0.5 - W * 0.25 - H / nC - 0.9 * f, angle: 90, font: 0.9 * f }));
|
|
7983
|
+
}
|
|
7984
|
+
} else {
|
|
7985
|
+
// inside the bar, in its text color, shrunk to fit
|
|
7986
|
+
const s = Math.min(1, H / tLen);
|
|
7987
|
+
if (s >= 0.33) {
|
|
7988
|
+
let ptX = -4;
|
|
7989
|
+
if (tv > 0) ptX += Math.max(1, H - (tLen + 3) * s);
|
|
7990
|
+
const dY = 0.5 / s;
|
|
7991
|
+
texts.push(Object.assign(base, { x: posX + ptY - dY + 0.25 * f, y: oy - ptX - f, angle: 90, font: f * s, color: 'contrast', bg: null }));
|
|
7992
|
+
}
|
|
7993
|
+
minNext = Math.max(0, minNext - H);
|
|
7994
|
+
}
|
|
7995
|
+
}
|
|
7996
|
+
|
|
7997
|
+
if (stacked) {
|
|
7998
|
+
if (!has(/CENTER/)) posY -= v * step;
|
|
7999
|
+
} else {
|
|
8000
|
+
posY = 0;
|
|
8001
|
+
posX += W;
|
|
8002
|
+
if (has(/SPACED/)) posX += W / 5;
|
|
8003
|
+
else if (!has(/\bUP\b/)) posX += 0.05;
|
|
8004
|
+
}
|
|
8005
|
+
barsDrawn++;
|
|
8006
|
+
if (stacked && !has(/MULTI/) && o.gridX && barsDrawn % o.gridX === 0) {
|
|
8007
|
+
posY = 0;
|
|
8008
|
+
posX += W + W / 20;
|
|
8009
|
+
}
|
|
8010
|
+
}
|
|
8011
|
+
|
|
8012
|
+
// the chart's position: HOR(Z) turned by 90° (bars to the right, the
|
|
8013
|
+
// first on top, the last centered on the position's height), else
|
|
8014
|
+
// the bars side by side centered on it (STACKED: the first column)
|
|
8015
|
+
const P = posX;
|
|
8016
|
+
let map, dirMap, angleMap;
|
|
8017
|
+
if (horz) {
|
|
8018
|
+
const dy = W / 2 + (has(/3D/) ? W / 3 : 0) - P;
|
|
8019
|
+
map = (x, y) => [-y, x + dy];
|
|
8020
|
+
dirMap = d => (d + 1) % 4;
|
|
8021
|
+
angleMap = a => a - 90;
|
|
8022
|
+
} else {
|
|
8023
|
+
const dx = stacked ? W / 2 : W * barsDrawn / 2;
|
|
8024
|
+
map = (x, y) => [x - dx, y];
|
|
8025
|
+
dirMap = d => d;
|
|
8026
|
+
angleMap = a => a;
|
|
8027
|
+
}
|
|
8028
|
+
const ext = [Infinity, Infinity, -Infinity, -Infinity];
|
|
8029
|
+
const grow = (x, y) => { if (x < ext[0]) ext[0] = x; if (y < ext[1]) ext[1] = y; if (x > ext[2]) ext[2] = x; if (y > ext[3]) ext[3] = y; };
|
|
8030
|
+
for (const b of bars) {
|
|
8031
|
+
b.base = map(b.base[0], b.base[1]);
|
|
8032
|
+
b.dir = dirMap(b.dir);
|
|
8033
|
+
const d = [[0, -1], [1, 0], [0, 1], [-1, 0]][b.dir], a = [-d[1], d[0]];
|
|
8034
|
+
const L = b.len + b.band + b.head;
|
|
8035
|
+
[[-b.w2, 0], [b.w2, 0], [-b.w2, L], [b.w2, L]].forEach(([u, v]) => grow(b.base[0] + a[0] * u + d[0] * v, b.base[1] + a[1] * u + d[1] * v));
|
|
8036
|
+
}
|
|
8037
|
+
for (const tx of texts) {
|
|
8038
|
+
[tx.x, tx.y] = map(tx.x, tx.y);
|
|
8039
|
+
tx.angle = angleMap(tx.angle);
|
|
8040
|
+
// arial: ascent 0.905, descent 0.212 of the font
|
|
8041
|
+
const w = measure(tx.text, tx.font) + (tx.bg ? 0.7 * tx.font : 0);
|
|
8042
|
+
const r = tx.angle * Math.PI / 180, c = Math.cos(r), s = -Math.sin(r);
|
|
8043
|
+
const xo = tx.bg ? -0.3 * tx.font : 0;
|
|
8044
|
+
[[xo, -0.905 * tx.font], [xo + w, -0.905 * tx.font], [xo, 0.212 * tx.font], [xo + w, 0.212 * tx.font]]
|
|
8045
|
+
.forEach(([u, v]) => grow(tx.x + u * c - v * s, tx.y + u * s + v * c));
|
|
8046
|
+
}
|
|
8047
|
+
for (const l of lines) {
|
|
8048
|
+
[l.x0, l.y0] = map(l.x0, l.y0);
|
|
8049
|
+
[l.x1, l.y1] = map(l.x1, l.y1);
|
|
8050
|
+
grow(l.x0, l.y0); grow(l.x1, l.y1);
|
|
8051
|
+
}
|
|
8052
|
+
return { bars, texts, lines, extent: ext[0] <= ext[2] ? ext : [0, 0, 0, 0], width: W, sizer };
|
|
8053
|
+
}
|
|
8054
|
+
|
|
8055
|
+
// flat's BOX and TITLE around a chart (maptheme.js 18758-18905), in the
|
|
8056
|
+
// chart's units: `extent` the chart's [x0, y0, x1, y1]; o: { margin
|
|
8057
|
+
// (boxmargin, 2), titleFont, title (text or ''), bottomTitle, alignRight,
|
|
8058
|
+
// measure }. The margin is min(2 m, m · width / 30); the title's first
|
|
8059
|
+
// baseline 0.7 font above the chart (BOTTOMTITLE: one font below), its
|
|
8060
|
+
// lines wrapped to the chart's width and stacked upward; the box encloses
|
|
8061
|
+
// chart and title, its top trimmed by 0.1 font when titled. Returns
|
|
8062
|
+
// { rect: [x0, y0, x1, y1], title: { lines, x, anchor, baseline, font } | null }
|
|
8063
|
+
function flatChartBox(extent, o) {
|
|
8064
|
+
const e = extent.slice();
|
|
8065
|
+
const w = e[2] - e[0];
|
|
8066
|
+
const margin = Math.min(2 * o.margin, o.margin * w / 30);
|
|
8067
|
+
let title = null;
|
|
8068
|
+
if (o.title) {
|
|
8069
|
+
const f = o.titleFont;
|
|
8070
|
+
const measure = o.measure || ((text, font) => text.length * font * 0.55);
|
|
8071
|
+
const lines = [];
|
|
8072
|
+
for (const word of String(o.title).split(/\s+/).filter(Boolean)) {
|
|
8073
|
+
const last = lines.length ? lines[lines.length - 1] + ' ' + word : null;
|
|
8074
|
+
if (last !== null && measure(last, f) <= w) lines[lines.length - 1] = last;
|
|
8075
|
+
else lines.push(word);
|
|
8076
|
+
}
|
|
8077
|
+
const width = Math.max(...lines.map(l => measure(l, f)));
|
|
8078
|
+
const baseline = o.bottomTitle ? e[3] + f : e[1] - 0.7 * f - (lines.length - 1) * f;
|
|
8079
|
+
const x = o.alignRight ? e[2] : e[0];
|
|
8080
|
+
const x0 = o.alignRight ? x - width : x;
|
|
8081
|
+
title = { lines, x, anchor: o.alignRight ? 'end' : 'start', baseline, font: f };
|
|
8082
|
+
e[0] = Math.min(e[0], x0);
|
|
8083
|
+
e[1] = Math.min(e[1], baseline - 0.905 * f) + 0.1 * f;
|
|
8084
|
+
e[2] = Math.max(e[2], x0 + width);
|
|
8085
|
+
e[3] = Math.max(e[3], baseline + (lines.length - 1) * f + 0.212 * f);
|
|
8086
|
+
}
|
|
8087
|
+
return { rect: [e[0] - margin, e[1] - margin, e[2] + margin, e[3] + margin], title, width: w };
|
|
8088
|
+
}
|
|
8089
|
+
|
|
8090
|
+
// CHART|PIE / DONUT: one pie's slices as real ixmaps-flat lays them out —
|
|
8091
|
+
// maptheme.js drawChart's PIE branch (19939-20330) and piechart.js
|
|
8092
|
+
// DonutChart.realize (181-510). `values`: the item's part values (field
|
|
8093
|
+
// order); `t`: the type string, tested as flat does, by substring (so
|
|
8094
|
+
// CENTERVALUE also is CENTER, XTHIN also THIN). Returns null when flat
|
|
8095
|
+
// draws no pie (no sum, no positive part), else
|
|
8096
|
+
// { slices: [{ i, value, percent, start, sweep, complement }],
|
|
8097
|
+
// inner: inner radius / radius,
|
|
8098
|
+
// center: { i, value, size } | null }
|
|
8099
|
+
// (k on a slice: its index among all of flat's donut parts)
|
|
8100
|
+
// i is the part's field index (-1: AUTOCOMPLETE's no-data complement);
|
|
8101
|
+
// start/sweep in degrees, clockwise from 12 o'clock.
|
|
8102
|
+
// - parts: in field order; SORT biggest first; REVERSE takes flat's
|
|
8103
|
+
// index n − i literally — field n doesn't exist and field 0 is never
|
|
8104
|
+
// reached, so a REVERSE pie loses its first part, as in flat
|
|
8105
|
+
// - a part is a slice when > 0, or = 0 unless ZEROISNOTVALUE (a 0 slice
|
|
8106
|
+
// has no angle and is not drawn); negative parts are left out
|
|
8107
|
+
// - the slices share 100 % (AUTOCOMPLETE below 100: the rest is a
|
|
8108
|
+
// no-data part, the values are taken as percentages)
|
|
8109
|
+
// - CENTER (without DONUT): the center part (style.centerpart first /
|
|
8110
|
+
// last / min / max — default max — or a field number) is no slice but
|
|
8111
|
+
// a disc in the middle, radius √(size / 100) of the pie's, size =
|
|
8112
|
+
// value / sum · 200; the ring around it has that inner radius
|
|
8113
|
+
// - DONUT: inner radius 0.42 (THICK 0.25, THIN 0.66, XTHIN 0.95, 3D 0.5)
|
|
8114
|
+
// - an aggregated 1-record item of fewer than 2 parts has no hole
|
|
8115
|
+
// - angles: from 0° (SYMMETRIC: the first part, BIGTOTOP the biggest,
|
|
8116
|
+
// centered on 12 o'clock), HALF over 180° from 270° (HALFPLUS10 200°
|
|
8117
|
+
// from 260°, HALFMINUS10 160° from 280°, HALFPLUS20 220° from 250°,
|
|
8118
|
+
// HALFMINUS20 140° from 290°)
|
|
8119
|
+
function pieSliceLayout(values, t, opts = {}) {
|
|
8120
|
+
t = String(t || '');
|
|
8121
|
+
const has = re => re.test(t);
|
|
8122
|
+
const parts = values.map(v => (isNaN(v) ? 0 : v));
|
|
8123
|
+
const sum = parts.reduce((a, v) => a + v, 0);
|
|
8124
|
+
const autocomplete = has(/AUTOCOMPLETE/);
|
|
8125
|
+
if (sum === 0 && !autocomplete) return null;
|
|
8126
|
+
const n = parts.length;
|
|
8127
|
+
let sortedIndex = null;
|
|
8128
|
+
if (has(/\bSORT\b/)) sortedIndex = parts.map((value, index) => ({ index, value })).sort((a, b) => b.value - a.value);
|
|
8129
|
+
if (has(/\bREVERSE\b/)) sortedIndex = parts.map((value, i) => ({ index: n - i, value }));
|
|
8130
|
+
const at = p => (sortedIndex ? sortedIndex[p].index : p);
|
|
8131
|
+
const donut = has(/DONUT/), centerFlag = has(/CENTER/);
|
|
8132
|
+
let center = null;
|
|
8133
|
+
if (centerFlag) {
|
|
8134
|
+
let cSum = 0, cMax = -Number.MAX_VALUE, cMin = Number.MAX_VALUE, iMin, iMax;
|
|
8135
|
+
for (let p = 0; p < n; p++) {
|
|
8136
|
+
const i = at(p);
|
|
8137
|
+
cSum += parts[i] || 0;
|
|
8138
|
+
if (parts[i] < cMin) { cMin = parts[i]; iMin = i; }
|
|
8139
|
+
if (parts[i] > cMax) { cMax = parts[i]; iMax = i; }
|
|
8140
|
+
}
|
|
8141
|
+
const cp = opts.centerPart != null && opts.centerPart !== '' ? String(opts.centerPart) : 'max';
|
|
8142
|
+
let ci = 0;
|
|
8143
|
+
if (cp === 'first') ci = 0;
|
|
8144
|
+
else if (cp === 'last') ci = n - 1;
|
|
8145
|
+
else if (cp === 'min') ci = iMin;
|
|
8146
|
+
else if (cp === 'max') ci = iMax;
|
|
8147
|
+
else if (Number(cp)) ci = Number(cp);
|
|
8148
|
+
center = { i: ci, value: parts[ci], size: parts[ci] / cSum * 200 };
|
|
8149
|
+
}
|
|
8150
|
+
const slices = [];
|
|
8151
|
+
for (let p = 0; p < n; p++) {
|
|
8152
|
+
const i = at(p);
|
|
8153
|
+
if (centerFlag && !donut && i === center.i) continue;
|
|
8154
|
+
const v = parts[i];
|
|
8155
|
+
if (v > 0 || (v === 0 && !has(/ZEROISNOTVALUE/))) slices.push({ i, value: v, percent: v });
|
|
8156
|
+
}
|
|
8157
|
+
let inner = 0;
|
|
8158
|
+
if (donut) inner = has(/3D/) ? 0.5 : has(/XTHIN/) ? 0.95 : has(/THIN/) ? 0.66 : has(/THICK/) ? 0.25 : 0.42;
|
|
8159
|
+
else if (centerFlag) inner = Math.sqrt(center.size / 100);
|
|
8160
|
+
// GR 09.11.2019 "donut with count or size == 1 --> bubble": only for an
|
|
8161
|
+
// aggregated item of 1 record (opts.itemCount) — flat's per-record
|
|
8162
|
+
// items have no nCount, so their 1-part donuts keep the hole
|
|
8163
|
+
if (slices.length < 2 && opts.itemCount === 1) inner = 0;
|
|
8164
|
+
// DonutChart.realize
|
|
8165
|
+
let biggest = slices[0], biggestValue = 0, any = false;
|
|
8166
|
+
for (const sl of slices) {
|
|
8167
|
+
if (sl.percent > 0) {
|
|
8168
|
+
if (has(/BIGTOTOP/) && sl.percent > biggestValue) { biggest = sl; biggestValue = sl.percent; }
|
|
8169
|
+
any = true;
|
|
8170
|
+
}
|
|
8171
|
+
}
|
|
8172
|
+
// flat draws its "Pie error: no values !" circle here
|
|
8173
|
+
if (!any && !autocomplete) return null;
|
|
8174
|
+
const pctSum = slices.reduce((a, sl) => a + sl.percent, 0);
|
|
8175
|
+
if (pctSum !== 100) {
|
|
8176
|
+
if (autocomplete && pctSum < 100) slices.push({ i: -1, value: null, percent: Math.floor((100 - pctSum) * 100000) / 100000, complement: true });
|
|
8177
|
+
else slices.forEach(sl => { sl.percent = sl.percent / pctSum * 100; });
|
|
8178
|
+
}
|
|
8179
|
+
let end = 0;
|
|
8180
|
+
if (has(/SYMMETRIC/) && slices.length) {
|
|
8181
|
+
if (has(/BIGTOTOP/)) {
|
|
8182
|
+
end = 360;
|
|
8183
|
+
let x = 0;
|
|
8184
|
+
for (; x < slices.length && slices[x] !== biggest; x++) end -= slices[x].percent < 100 ? Math.floor(360 / 100 * slices[x].percent) : 0;
|
|
8185
|
+
end -= slices[x] && slices[x].percent < 100 ? Math.floor(360 / 100 / 2 * biggest.percent) : 0;
|
|
8186
|
+
} else if (slices[0].percent < 100) {
|
|
8187
|
+
end = 360 - Math.floor(360 / 100 / 2 * slices[0].percent);
|
|
8188
|
+
}
|
|
8189
|
+
}
|
|
8190
|
+
if (has(/HALF/)) end = 270;
|
|
8191
|
+
if (has(/HALFPLUS10/)) end = 260;
|
|
8192
|
+
if (has(/HALFMINUS10/)) end = 280;
|
|
8193
|
+
if (has(/HALFPLUS20/)) end = 250;
|
|
8194
|
+
if (has(/HALFMINUS20/)) end = 290;
|
|
8195
|
+
let maxAngle = has(/HALF/) ? 180 : 360;
|
|
8196
|
+
if (has(/HALFPLUS10/)) maxAngle = 200;
|
|
8197
|
+
if (has(/HALFMINUS10/)) maxAngle = 160;
|
|
8198
|
+
if (has(/HALFPLUS20/)) maxAngle = 220;
|
|
8199
|
+
if (has(/HALFMINUS20/)) maxAngle = 140;
|
|
8200
|
+
const out = [];
|
|
8201
|
+
slices.forEach((sl, k) => {
|
|
8202
|
+
const sweep = sl.percent / 100 * maxAngle;
|
|
8203
|
+
const start = end % 360;
|
|
8204
|
+
end = start + sweep;
|
|
8205
|
+
if (end > 360) end = end % 360;
|
|
8206
|
+
// _crc_drawDonut draws nothing for start == end (a 0 part, or a
|
|
8207
|
+
// full turn that doesn't start at 0°); k: flat's donut part index
|
|
8208
|
+
// (its value texts count them, drawn or not)
|
|
8209
|
+
if (start === end || start < 0 || end < 0) return;
|
|
8210
|
+
out.push(Object.assign(sl, { start, sweep, k }));
|
|
8211
|
+
});
|
|
8212
|
+
return { slices: out, inner, center: centerFlag && !donut ? center : null };
|
|
8213
|
+
}
|
|
8214
|
+
|
|
8215
|
+
// A pie's VALUES as flat draws them for more than one part
|
|
8216
|
+
// (maptheme.js drawDonutText 25072-25155, drawTextforOneQuadrant
|
|
8217
|
+
// 24993-25065, piechart.js getTextPosition 517-555): a label per slice
|
|
8218
|
+
// outside the pie, joined to the slice by a leader line. Per slice: the
|
|
8219
|
+
// point at 1.1 R in the middle of its angle; the labels of each quadrant
|
|
8220
|
+
// stacked by that point's height (line height 1.2 font, kept apart, the
|
|
8221
|
+
// last ones pushed toward the pie's top/bottom), all at x = R + 2u right
|
|
8222
|
+
// or left; the leader line from the slice's rim to its 1.1 R point,
|
|
8223
|
+
// horizontally to R − 3u when short of it, to the label's height at
|
|
8224
|
+
// R + 2u, then 2u straight on; the text 1.5u further. Only flat's first
|
|
8225
|
+
// min(50, maxParts) donut parts get one (maxParts: the theme's class
|
|
8226
|
+
// count). Pixels, y down, relative to the pie center:
|
|
8227
|
+
// [{ slice, segments: [[x0, y0, x1, y1], ...], x, y (text baseline), anchor: 'start' | 'end' }]
|
|
8228
|
+
function pieValueLabelLayout(slices, R, fontPx, unitPx, maxParts) {
|
|
8229
|
+
const limit = Math.min(50, maxParts);
|
|
8230
|
+
const quads = [[], [], [], []];
|
|
8231
|
+
let yMax = 0;
|
|
8232
|
+
for (const sl of slices) {
|
|
8233
|
+
if (sl.k >= limit) continue;
|
|
8234
|
+
let end = sl.start + sl.sweep;
|
|
8235
|
+
if (end > 360) end = end % 360;
|
|
8236
|
+
const mid = sl.start < end ? (sl.start + end) / 2 : Math.min(360, (sl.start + end + 360) / 2) % 360;
|
|
8237
|
+
const sin = Math.sin(Math.PI * mid / 180), cos = Math.cos(Math.PI * mid / 180);
|
|
8238
|
+
const p = { slice: sl, x: sin * R * 1.1, y: -cos * R * 1.1, lx: sin * R, ly: -cos * R };
|
|
8239
|
+
yMax = Math.max(yMax, Math.abs(p.y));
|
|
8240
|
+
// flat's quadrants 1 (upper right) 2 (lower right) 3 (lower left)
|
|
8241
|
+
// 4 (upper left), folded onto positive x/y
|
|
8242
|
+
if (p.x > 0) {
|
|
8243
|
+
if (p.y > 0) quads[1].push(p);
|
|
8244
|
+
else quads[0].push({ slice: sl, x: p.x, y: -p.y, lx: p.lx, ly: -p.ly });
|
|
8245
|
+
} else if (p.y > 0) quads[2].push({ slice: sl, x: -p.x, y: p.y, lx: -p.lx, ly: p.ly });
|
|
8246
|
+
else quads[3].push({ slice: sl, x: -p.x, y: -p.y, lx: -p.lx, ly: -p.ly });
|
|
8247
|
+
}
|
|
8248
|
+
const out = [];
|
|
8249
|
+
const dirs = [[1, -1, 'start'], [1, 1, 'start'], [-1, 1, 'end'], [-1, -1, 'end']];
|
|
8250
|
+
quads.forEach((q, qi) => {
|
|
8251
|
+
q.sort((a, b) => (a.y > b.y ? 1 : -1));
|
|
8252
|
+
const [xDir, yDir, anchor] = dirs[qi];
|
|
8253
|
+
const lh = fontPx * 1.2;
|
|
8254
|
+
let yMin = lh / 2;
|
|
8255
|
+
let ySpace = yMax - lh * (q.length - 2);
|
|
8256
|
+
for (const p of q) {
|
|
8257
|
+
const knee = p.y / p.ly;
|
|
8258
|
+
const yPos = Math.max(yMin, Math.min(p.y, ySpace));
|
|
8259
|
+
yMin = yPos + lh;
|
|
8260
|
+
ySpace += fontPx;
|
|
8261
|
+
const xPos = R + 2 * unitPx;
|
|
8262
|
+
const segments = [];
|
|
8263
|
+
let sx = p.lx * xDir, sy = p.ly * yDir;
|
|
8264
|
+
if (knee) { segments.push([sx, sy, p.x * xDir, p.y * yDir]); sx = p.x * xDir; sy = p.y * yDir; }
|
|
8265
|
+
if (p.x < R - 3 * unitPx) { const x = (R - 3 * unitPx) * xDir; segments.push([sx, sy, x, sy]); sx = x; }
|
|
8266
|
+
let ex = xPos * xDir;
|
|
8267
|
+
const ey = yPos * yDir;
|
|
8268
|
+
segments.push([sx, sy, ex, ey]);
|
|
8269
|
+
segments.push([ex, ey, ex + 2 * unitPx * xDir, ey]);
|
|
8270
|
+
ex += 3.5 * unitPx * xDir;
|
|
8271
|
+
out.push({ slice: p.slice, segments, x: ex, y: ey + fontPx * 0.25, anchor });
|
|
8272
|
+
}
|
|
8273
|
+
});
|
|
8274
|
+
return out;
|
|
8275
|
+
}
|
|
8276
|
+
|
|
8277
|
+
// flat's value rules of a theme (MapTheme constructor, maptheme.js
|
|
8278
|
+
// 7740-7830), by substring of the type string as flat tests them:
|
|
8279
|
+
// nullIsValue (0 is a value: ZEROISNOTVALUE no, ZEROISVALUE yes, a CHART
|
|
8280
|
+
// with CATEGORICAL or several value fields always), negativeOk
|
|
8281
|
+
// (NEGATIVEISNOTVALUE no, NEGATIVEISVALUE yes, CATEGORICAL without
|
|
8282
|
+
// AGGREGATE no), undefinedOk (UNDEFINEDISNOTVALUE no), zeroExcludes
|
|
8283
|
+
// (ZEROISNOTVALUE: a 0 is left out of an aggregation's means)
|
|
8284
|
+
function flatValueRules(t, nFields) {
|
|
8285
|
+
t = String(t || '');
|
|
8286
|
+
let nullIsValue = true, negativeOk = true, undefinedOk = true;
|
|
8287
|
+
if (/ZEROISNOTVALUE/.test(t)) nullIsValue = false;
|
|
8288
|
+
if (/ZEROISVALUE/.test(t)) nullIsValue = true;
|
|
8289
|
+
if (/NEGATIVEISNOTVALUE/.test(t)) negativeOk = false;
|
|
8290
|
+
if (/NEGATIVEISVALUE/.test(t)) negativeOk = true;
|
|
8291
|
+
if (/UNDEFINEDISVALUE/.test(t)) undefinedOk = true;
|
|
8292
|
+
if (/UNDEFINEDISNOTVALUE/.test(t)) undefinedOk = false;
|
|
8293
|
+
if (/CHART/.test(t) && (/CATEGORICAL/.test(t) || nFields > 1)) nullIsValue = true;
|
|
8294
|
+
if (/CATEGORICAL/.test(t) && !/AGGREGATE/.test(t)) negativeOk = false;
|
|
8295
|
+
return { nullIsValue, negativeOk, undefinedOk, zeroExcludes: /ZEROISNOTVALUE/.test(t) };
|
|
8296
|
+
}
|
|
8297
|
+
|
|
8298
|
+
// An AGGREGATE or CATEGORICAL pie: one record's contribution to its item,
|
|
8299
|
+
// as flat's loadAndAggregateValuesOfTheme reads it (maptheme.js
|
|
8300
|
+
// 10465-10571, which every AGGREGATE theme goes through). `o`:
|
|
8301
|
+
// { rules (flatValueRules), sizeField, cat (CATEGORICAL: the record's
|
|
8302
|
+
// category, 0-based, -1 when it is none of them), fields (otherwise:
|
|
8303
|
+
// the value fields) }
|
|
8304
|
+
// Returns null when flat leaves the record out, else
|
|
8305
|
+
// { cat, size } (CATEGORICAL) or { parts, zeroNot, size }
|
|
8306
|
+
// - size: the size field as a number (1 without one); a record whose
|
|
8307
|
+
// size is no number, 0 (unless 0 is a value) or negative (unless
|
|
8308
|
+
// negatives are values) is left out
|
|
8309
|
+
// - CATEGORICAL: a value that is none of the categories (not in
|
|
8310
|
+
// style.values) is part -1 — no slice, but its size still counts in
|
|
8311
|
+
// the item's size (flat: nValuesA[-1]); left out only with
|
|
8312
|
+
// UNDEFINEDISNOTVALUE
|
|
8313
|
+
// - value fields: a value that is no number is 0 (UNDEFINEDISNOTVALUE:
|
|
8314
|
+
// the record is left out); a 0 is left out of the item's MEAN with
|
|
8315
|
+
// ZEROISNOTVALUE (zeroNot), else the record when 0 is no value; a
|
|
8316
|
+
// negative value when negatives are no values
|
|
8317
|
+
function flatPieRecord(raw, o) {
|
|
8318
|
+
const r = o.rules;
|
|
8319
|
+
const size = o.sizeField ? flatScanValue(raw[o.sizeField]) : 1;
|
|
8320
|
+
if (isNaN(size) || (size === 0 && !r.nullIsValue) || (size < 0 && !r.negativeOk)) return null;
|
|
8321
|
+
if (o.cat !== undefined) {
|
|
8322
|
+
if (o.cat < 0 && !r.undefinedOk) return null;
|
|
8323
|
+
return { cat: o.cat, size };
|
|
8324
|
+
}
|
|
8325
|
+
const parts = [], zeroNot = [];
|
|
8326
|
+
for (let k = 0; k < o.fields.length; k++) {
|
|
8327
|
+
let v = parseFloat(raw[o.fields[k]]);
|
|
8328
|
+
zeroNot[k] = false;
|
|
8329
|
+
if (isNaN(v)) {
|
|
8330
|
+
if (!r.undefinedOk) return null;
|
|
8331
|
+
v = 0;
|
|
8332
|
+
}
|
|
8333
|
+
if (v === 0) {
|
|
8334
|
+
if (r.zeroExcludes) { parts[k] = 0; zeroNot[k] = true; continue; }
|
|
8335
|
+
if (!r.nullIsValue) return null;
|
|
8336
|
+
}
|
|
8337
|
+
if (v < 0 && !r.negativeOk) return null;
|
|
8338
|
+
parts[k] = v;
|
|
8339
|
+
}
|
|
8340
|
+
return { parts, zeroNot, size };
|
|
8341
|
+
}
|
|
8342
|
+
|
|
8343
|
+
// Adds a record's flatPieRecord to its item (maptheme.js 10779-10943) —
|
|
8344
|
+
// `item` null for the first record of the item. Returns the item
|
|
8345
|
+
// { parts (sparse for CATEGORICAL: the category sums), counts (the
|
|
8346
|
+
// records per part, for MEAN), size (the item's size, flat's nSize),
|
|
8347
|
+
// n (its record count, flat's nCount) }
|
|
8348
|
+
// SUM (default): the parts and the |sizes| summed; MAX / MIN: their
|
|
8349
|
+
// maximum / minimum (a part missing so far counts 0, as in flat; the
|
|
8350
|
+
// record count stays 1); FIRST: the first record only; LAST: the last.
|
|
8351
|
+
// A CATEGORICAL record of no category (-1) adds its size only.
|
|
8352
|
+
function flatPieAccumulate(item, rec, flags) {
|
|
8353
|
+
const max = flags.has('MAX'), min = flags.has('MIN');
|
|
8354
|
+
if (item && flags.has('FIRST')) return item;
|
|
8355
|
+
if (!item || flags.has('LAST')) {
|
|
8356
|
+
if (rec.cat !== undefined) {
|
|
8357
|
+
const parts = [], counts = [];
|
|
8358
|
+
if (rec.cat >= 0) { parts[rec.cat] = rec.size; counts[rec.cat] = 1; }
|
|
8359
|
+
return { parts, counts, size: Math.abs(rec.size), n: 1 };
|
|
8360
|
+
}
|
|
8361
|
+
return { parts: rec.parts.slice(), counts: rec.zeroNot.map(z => (z ? 0 : 1)), size: Math.abs(rec.size), n: 1 };
|
|
8362
|
+
}
|
|
8363
|
+
const comb = (a, v) => (max ? Math.max(a || 0, v) : min ? Math.min(a || 0, v) : (a || 0) + v);
|
|
8364
|
+
if (rec.cat !== undefined) {
|
|
8365
|
+
if (rec.cat >= 0) {
|
|
8366
|
+
item.parts[rec.cat] = comb(item.parts[rec.cat], rec.size);
|
|
8367
|
+
item.counts[rec.cat] = (item.counts[rec.cat] || 0) + 1;
|
|
8368
|
+
}
|
|
8369
|
+
} else {
|
|
8370
|
+
for (let k = 0; k < rec.parts.length; k++) {
|
|
8371
|
+
item.parts[k] = comb(item.parts[k], rec.parts[k] || 0);
|
|
8372
|
+
if (!rec.zeroNot[k]) item.counts[k] = (item.counts[k] || 0) + 1;
|
|
8373
|
+
}
|
|
8374
|
+
}
|
|
8375
|
+
if (max) item.size = Math.max(item.size, rec.size);
|
|
8376
|
+
else if (min) item.size = Math.min(item.size, rec.size);
|
|
8377
|
+
else { item.size += Math.abs(rec.size); item.n++; }
|
|
8378
|
+
return item;
|
|
8379
|
+
}
|
|
8380
|
+
|
|
8381
|
+
// An aggregated pie item's parts and size after flat's post-aggregation
|
|
8382
|
+
// steps (maptheme.js 11056-11137): MEAN — a part divided by its record
|
|
8383
|
+
// count (CATEGORICAL: the category's records; value fields: the item's
|
|
8384
|
+
// records, or with ZEROISNOTVALUE the records with a value there), the
|
|
8385
|
+
// size by the item's record count unless SIZESUM; AUTO100 — the parts in
|
|
8386
|
+
// % of their sum. `nParts`: the number of parts (CATEGORICAL with
|
|
8387
|
+
// style.values: every category; without: up to the item's last one, as
|
|
8388
|
+
// flat's sparse nValuesA). Returns { parts, size }.
|
|
8389
|
+
function flatPieItemValues(item, t, o = {}) {
|
|
8390
|
+
t = String(t || '');
|
|
8391
|
+
const n = o.nParts != null ? o.nParts : item.parts.length;
|
|
8392
|
+
let parts = [];
|
|
8393
|
+
for (let k = 0; k < n; k++) parts[k] = item.parts[k] || 0;
|
|
8394
|
+
let size = item.size;
|
|
8395
|
+
if (/MEAN/.test(t)) {
|
|
8396
|
+
const perPart = o.categorical || /ZEROISNOTVALUE/.test(t);
|
|
8397
|
+
parts = parts.map((v, k) => (perPart ? (item.counts[k] > 0 ? v / item.counts[k] : 0) : v / item.n));
|
|
8398
|
+
if (!/SIZESUM/.test(t)) size /= item.n;
|
|
8399
|
+
}
|
|
8400
|
+
if (/AUTO100/.test(t)) {
|
|
8401
|
+
const sum = parts.reduce((a, v) => a + v, 0);
|
|
8402
|
+
parts = parts.map(v => v / (sum / 100));
|
|
8403
|
+
}
|
|
8404
|
+
return { parts, size };
|
|
8405
|
+
}
|
|
8406
|
+
|
|
8407
|
+
// CHART|VECTOR / CHART|BEZIER: the theme's items as real ixmaps-flat
|
|
8408
|
+
// builds them (maptheme.js loadAndAggregateValuesOfTheme, which every
|
|
8409
|
+
// VECTOR / BEZIER theme goes through, 9650: 10402-10450 the second
|
|
8410
|
+
// position, 10763 the item id, 10779-10985 the items). `records`:
|
|
8411
|
+
// { p1: [lng, lat], p2: [lng, lat] | null, cat (0-based category, -1
|
|
8412
|
+
// not one of them; null without CATEGORICAL), value (the value
|
|
8413
|
+
// field as a number, without CATEGORICAL), size (the size field as
|
|
8414
|
+
// a number, NaN when not one; 1 without a size field), raw }
|
|
8415
|
+
// `o`: { aggregate, vectorKey (the VECTOR flag: the item id has the
|
|
8416
|
+
// second position too), multiParts (CATEGORICAL: the category is part
|
|
8417
|
+
// of the id), max / min (MAX / MIN aggregation), zeroIsNotValue,
|
|
8418
|
+
// negativeIsNotValue }. Returns items
|
|
8419
|
+
// { p1, p2, cat, nSize, value, count, raw (the first record), raws }
|
|
8420
|
+
// - without AGGREGATE every record is an item, its nSize the size as
|
|
8421
|
+
// it is (negative too, 0 when not a number) — flat checks no value
|
|
8422
|
+
// there
|
|
8423
|
+
// - AGGREGATE: records of one id are one item: the id is the first
|
|
8424
|
+
// position (+ the category with CATEGORICAL, + the second position
|
|
8425
|
+
// with VECTOR — a BEZIER without VECTOR sums every flow of an origin
|
|
8426
|
+
// into its first record's flow, as flat); a record whose size is no
|
|
8427
|
+
// number (or 0 with ZEROISNOTVALUE, negative with
|
|
8428
|
+
// NEGATIVEISNOTVALUE) is left out; nSize the sum of the |sizes|
|
|
8429
|
+
// (MAX / MIN: their maximum / minimum), value the values' sum
|
|
8430
|
+
// - the first record of an item gives its category, second position,
|
|
8431
|
+
// and the record its tooltip reads
|
|
8432
|
+
function flatVectorItems(records, o = {}) {
|
|
8433
|
+
const items = [];
|
|
8434
|
+
if (!o.aggregate) {
|
|
8435
|
+
for (const r of records) {
|
|
8436
|
+
items.push({ p1: r.p1, p2: r.p2, cat: r.cat, nSize: r.size || 0, value: r.value, count: 1, raw: r.raw, raws: [r.raw] });
|
|
8437
|
+
}
|
|
8438
|
+
return items;
|
|
8439
|
+
}
|
|
8440
|
+
const byId = new Map();
|
|
8441
|
+
for (const r of records) {
|
|
8442
|
+
const s = r.size;
|
|
8443
|
+
if (isNaN(s) || (s === 0 && o.zeroIsNotValue) || (s < 0 && o.negativeIsNotValue)) continue;
|
|
8444
|
+
let id = r.p1[0] + ',' + r.p1[1];
|
|
8445
|
+
// flat's nValue for the id: the category number (1-based), 0 for a
|
|
8446
|
+
// value that is none of the categories
|
|
8447
|
+
if (o.multiParts) id += ',' + (r.cat != null && r.cat >= 0 ? r.cat + 1 : 0);
|
|
8448
|
+
if (o.vectorKey && r.p2) id += '&' + r.p2[0] + ',' + r.p2[1];
|
|
8449
|
+
const item = byId.get(id);
|
|
8450
|
+
if (!item) {
|
|
8451
|
+
const it = { p1: r.p1, p2: r.p2, cat: r.cat, nSize: Math.abs(s), value: r.value, count: 1, raw: r.raw, raws: [r.raw] };
|
|
8452
|
+
byId.set(id, it);
|
|
8453
|
+
items.push(it);
|
|
8454
|
+
continue;
|
|
8455
|
+
}
|
|
8456
|
+
if (o.max) {
|
|
8457
|
+
item.nSize = Math.max(item.nSize, s);
|
|
8458
|
+
item.value = Math.max(item.value || 0, r.value || 0);
|
|
8459
|
+
} else if (o.min) {
|
|
8460
|
+
item.nSize = Math.min(item.nSize, s);
|
|
8461
|
+
item.value = Math.min(item.value || 0, r.value || 0);
|
|
8462
|
+
} else {
|
|
8463
|
+
item.nSize += Math.abs(s);
|
|
8464
|
+
item.value = (item.value || 0) + (r.value || 0);
|
|
8465
|
+
}
|
|
8466
|
+
item.count++;
|
|
8467
|
+
item.raws.push(r.raw);
|
|
8468
|
+
}
|
|
8469
|
+
return items;
|
|
8470
|
+
}
|
|
8471
|
+
|
|
8472
|
+
// CHART|BEZIER: one flow's curve as flat draws it (maptheme.js
|
|
8473
|
+
// 18148-18375), in screen pixels relative to its first position (y
|
|
8474
|
+
// down — flat's SVG map coordinates, scaled). dx/dy: the second
|
|
8475
|
+
// position minus the first. o:
|
|
8476
|
+
// w the line width in flat's chart units (linewidth, else
|
|
8477
|
+
// 10 · (|nSize| / max)^(1/sizepow))
|
|
8478
|
+
// ll flat's arrow / gap length in chart units (w + 3, GAP: w +
|
|
8479
|
+
// gapsize — computed by the caller, see _buildVectorLayers)
|
|
8480
|
+
// unit pixels per chart unit (the object zoom × style.scale)
|
|
8481
|
+
// oz pixels per chart unit without style.scale
|
|
8482
|
+
// bow rangescale (flat's nBow; 5 when not given), RANDOM already
|
|
8483
|
+
// applied
|
|
8484
|
+
// t the type string (SHORT, POINTER / ARROW, GAP)
|
|
8485
|
+
// marker markersize (1)
|
|
8486
|
+
// gap gapsize (20)
|
|
8487
|
+
// Returns { start, c1, c2, end, vx, vy } — start/end/c1/c2 [x, y]
|
|
8488
|
+
// pixels; (vx, vy) the end after shortening, which decides the
|
|
8489
|
+
// gradient direction. The control points come from the unshortened
|
|
8490
|
+
// vector: offset by (dy, −dx) / 50 · bow (SHORT: the unit vector · 5
|
|
8491
|
+
// bow chart units), at 1/4 and 1/2 of the (shortened) vector. An arrow (POINTER
|
|
8492
|
+
// or a type word ending in ARROW) ends the curve ll · markersize units
|
|
8493
|
+
// short of the second position, room for the arrow head. GAP: the curve
|
|
8494
|
+
// starts gapsize units off the first position and ends ll / 500 of the
|
|
8495
|
+
// bow further on.
|
|
8496
|
+
function bezierVectorLayout(dx, dy, o) {
|
|
8497
|
+
const t = String(o.t || '');
|
|
8498
|
+
const len = Math.sqrt(dx * dx + dy * dy);
|
|
8499
|
+
const bow = o.bow || 5;
|
|
8500
|
+
let ox, oy;
|
|
8501
|
+
// (LONG's bow depends on flat's own map units — not drawn here, see
|
|
8502
|
+
// VECTOR_INERT_FLAGS: the default bow instead)
|
|
8503
|
+
if (/\bSHORT\b/.test(t)) { ox = dy / len * bow * 5 * o.oz; oy = dx / len * bow * 5 * o.oz; }
|
|
8504
|
+
else { ox = dy / 50 * bow; oy = dx / 50 * bow; }
|
|
8505
|
+
let x = dx, y = dy;
|
|
8506
|
+
// flat's /\bPOINTER|ARROW\b/: POINTER at a word start, or ARROW at a
|
|
8507
|
+
// word end (ARROW, SMALLARROW, LASTARROW ...)
|
|
8508
|
+
if (/\bPOINTER|ARROW\b/.test(t)) {
|
|
8509
|
+
const k = o.ll * o.unit * (o.marker || 1) / len;
|
|
8510
|
+
x -= dx * k; y -= dy * k;
|
|
8511
|
+
}
|
|
8512
|
+
let x0 = 0, y0 = 0;
|
|
8513
|
+
if (/\bGAP\b/.test(t)) {
|
|
8514
|
+
x += ox / 500 * o.ll;
|
|
8515
|
+
y -= oy / 500 * o.ll;
|
|
8516
|
+
x0 = x / len * (o.gap || 20) * o.oz;
|
|
8517
|
+
y0 = y / len * (o.gap || 20) * o.oz;
|
|
8518
|
+
}
|
|
8519
|
+
return { start: [x0, y0], c1: [x / 4 + ox, y / 4 - oy], c2: [x / 2 + ox, y / 2 - oy], end: [x, y], vx: x, vy: y };
|
|
8520
|
+
}
|
|
8521
|
+
|
|
8522
|
+
// flat's Themes.toArray for a style list (maptheme.js 999-1016): a list
|
|
8523
|
+
// as it is; a string split at "|" when it has one or names an RGB(…)
|
|
8524
|
+
// color (flat's /\RGB/: upper case only), else at ","
|
|
8525
|
+
function flatToArray(v) {
|
|
8526
|
+
if (Array.isArray(v)) return v;
|
|
8527
|
+
const str = String(v);
|
|
8528
|
+
return (/\|/.test(str) || /RGB/.test(str)) ? str.split('|') : str.split(',');
|
|
8529
|
+
}
|
|
8530
|
+
// a fixed number in [0, 1) for a string — stands in for flat's
|
|
8531
|
+
// Math.random() where the harness needs the same drawing every time
|
|
8532
|
+
function hashUnit(str) {
|
|
8533
|
+
let h = 2166136261;
|
|
8534
|
+
for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
|
|
8535
|
+
return (h >>> 0) / 4294967296;
|
|
8536
|
+
}
|
|
8537
|
+
|
|
8538
|
+
// points of a cubic Bézier, n segments (n + 1 points)
|
|
8539
|
+
function cubicBezierPoints(p0, c1, c2, p3, n) {
|
|
8540
|
+
const out = [];
|
|
8541
|
+
for (let i = 0; i <= n; i++) {
|
|
8542
|
+
const s = i / n, u = 1 - s;
|
|
8543
|
+
const a = u * u * u, b = 3 * u * u * s, c = 3 * u * s * s, d = s * s * s;
|
|
8544
|
+
out.push([a * p0[0] + b * c1[0] + c * c2[0] + d * p3[0], a * p0[1] + b * c1[1] + c * c2[1] + d * p3[1]]);
|
|
8545
|
+
}
|
|
8546
|
+
return out;
|
|
8547
|
+
}
|
|
8548
|
+
|
|
8549
|
+
// flat's arrow marker (an SVG <marker>, markerUnits strokeWidth, orient
|
|
8550
|
+
// auto) at a line's end: the triangle M0,s L s,s/2 L0,0 in units of the
|
|
8551
|
+
// line width sw, its reference point (refX, refY) on the end point, its
|
|
8552
|
+
// x axis along the end's direction (tx, ty). BEZIER: s = min(5, 2.5 +
|
|
8553
|
+
// 5 / (w · scale)) · markersize, ref (s / 1.7, s / 2) (maptheme.js
|
|
8554
|
+
// 18309-18320); VECTOR: s = 4, ref (4, 2) — the tip on the end point
|
|
8555
|
+
// (18411-18424). A zero direction is orient 0: pointing right.
|
|
8556
|
+
// Returns the three corners [x, y] (pixels, y down).
|
|
8557
|
+
function arrowMarkerTriangle(end, tx, ty, sw, s, refX, refY) {
|
|
8558
|
+
const l = Math.sqrt(tx * tx + ty * ty);
|
|
8559
|
+
const ux = l ? tx / l : 1, uy = l ? ty / l : 0;
|
|
8560
|
+
const at = (mx, my) => {
|
|
8561
|
+
const a = (mx - refX) * sw, b = (my - refY) * sw;
|
|
8562
|
+
return [end[0] + a * ux - b * uy, end[1] + a * uy + b * ux];
|
|
8563
|
+
};
|
|
8564
|
+
return [at(0, s), at(s, s / 2), at(0, 0)];
|
|
8565
|
+
}
|
|
8566
|
+
|
|
8567
|
+
// BEZIER GRADIENT / FADEIN (maptheme.js 18233-18284): a linear gradient
|
|
8568
|
+
// over the curve's bounding box, vertical when the (shortened) vector
|
|
8569
|
+
// is more vertical than horizontal, else horizontal; the stop on the
|
|
8570
|
+
// first position's side has the fade opacity (FADEIN 0.2, GRADIENT 2),
|
|
8571
|
+
// the other one 2 — SVG clamps both to 1 — and the colors run from the
|
|
8572
|
+
// second color (linecolor's first entry) at the start to the line color
|
|
8573
|
+
// at the end. Returns per point { k: 0 … 1 from the start side's stop,
|
|
8574
|
+
// alpha } — the color mix k and opacity of each point.
|
|
8575
|
+
function fadeGradientStops(points, vx, vy, fade) {
|
|
8576
|
+
const vertical = Math.abs(vy) > Math.abs(vx);
|
|
8577
|
+
const axis = vertical ? 1 : 0;
|
|
8578
|
+
let min = Infinity, max = -Infinity;
|
|
8579
|
+
for (const p of points) { if (p[axis] < min) min = p[axis]; if (p[axis] > max) max = p[axis]; }
|
|
8580
|
+
const v = vertical ? vy : vx;
|
|
8581
|
+
const a = Math.min(1, Math.max(0, fade));
|
|
8582
|
+
// offset 0 is the box's left / top side: the start side unless the
|
|
8583
|
+
// vector points left / up
|
|
8584
|
+
const startAtZero = !(v < 0);
|
|
8585
|
+
return points.map(p => {
|
|
8586
|
+
let f = max > min ? (p[axis] - min) / (max - min) : 0;
|
|
8587
|
+
if (!startAtZero) f = 1 - f;
|
|
8588
|
+
// f: 0 on the start side, 1 on the end side
|
|
8589
|
+
return { k: f, alpha: a + (1 - a) * f };
|
|
8590
|
+
});
|
|
8591
|
+
}
|
|
8592
|
+
|
|
7059
8593
|
// CHART|SYMBOL|SEQUENCE (flat maptheme.js 21184-21300, 21790-21925,
|
|
7060
8594
|
// 22296-22395): one symbol per category part of a chart, in category
|
|
7061
8595
|
// order or sorted (SORT: DOWN biggest first, UP smallest first), parts
|
|
@@ -8111,6 +9645,9 @@
|
|
|
8111
9645
|
cell.value += f.properties.value;
|
|
8112
9646
|
if (f.properties.classValue !== undefined) cell.classValue += f.properties.classValue;
|
|
8113
9647
|
if (post) cell.value100 += f.properties.value100 || 0;
|
|
9648
|
+
// an AGGREGATE / CATEGORICAL pie's record (flatPieRecord): its parts
|
|
9649
|
+
// and size added to the cell's item as flat does (flatPieAccumulate)
|
|
9650
|
+
if (f.properties.pie) cell.pie = flatPieAccumulate(cell.pie || null, f.properties.pie, flags);
|
|
8114
9651
|
const series = f.properties.series;
|
|
8115
9652
|
if (series) {
|
|
8116
9653
|
if (!cell.series) cell.series = new Array(series.length).fill(0);
|
|
@@ -8132,6 +9669,7 @@
|
|
|
8132
9669
|
...(cell.raws && cell.raws.length ? { raws: cell.raws } : {}) }
|
|
8133
9670
|
: cell.first.properties;
|
|
8134
9671
|
if (cell.series && cell.n > 1) props = Object.assign({}, props, { series: cell.series });
|
|
9672
|
+
if (cell.pie) props = Object.assign({}, props, { pie: cell.pie });
|
|
8135
9673
|
// a field aggregation's cell key (the field value): the categories of
|
|
8136
9674
|
// one cell are merged by it (groupCoLocated), not by their positions
|
|
8137
9675
|
if (field) props = Object.assign({}, props, { aggKey: cell.key });
|
|
@@ -8309,7 +9847,10 @@
|
|
|
8309
9847
|
this._filterExpr = spec.filter || '';
|
|
8310
9848
|
this.mapOptions = mapOptions || {};
|
|
8311
9849
|
this._specBinding = spec.binding;
|
|
8312
|
-
|
|
9850
|
+
// VECTOR / BEZIER keep every record: flat checks their values only
|
|
9851
|
+
// when it aggregates them (flatVectorItems)
|
|
9852
|
+
const withDensity = this._withDensity(fc.features);
|
|
9853
|
+
this.features = applyField100(isVectorChart(this.flags) ? withDensity : filterFlatValues(withDensity, this.binding, this.flags), this.binding, this.flags, this.style);
|
|
8313
9854
|
this.binding = field100Binding(this.binding, this.flags);
|
|
8314
9855
|
this._iconCache = new Map();
|
|
8315
9856
|
this._glowIconCache = new Map();
|
|
@@ -8404,7 +9945,10 @@
|
|
|
8404
9945
|
this._gridRefLat = (minLat <= maxLat) ? (minLat + maxLat) / 2 : 0;
|
|
8405
9946
|
}
|
|
8406
9947
|
|
|
8407
|
-
|
|
9948
|
+
// a bar chart's parts are a pie's (its value fields or categories)
|
|
9949
|
+
if ((this._isPieChart() || this._isBarChart()) && this.binding.value) {
|
|
9950
|
+
this._preparePie();
|
|
9951
|
+
} else if (this.flags.has('DOMINANT') && this.binding.value && !isAggregatedCategoricalChoropleth(this)) {
|
|
8408
9952
|
// .type("CHOROPLETH|DOMINANT") — a MULTI-field bound value
|
|
8409
9953
|
// (binding.value is a pipe-joined field list, e.g. one CSV column
|
|
8410
9954
|
// per age band for the same year/sex — same pipe convention
|
|
@@ -8554,6 +10098,7 @@
|
|
|
8554
10098
|
if (this._usesAggregationIndex()) {
|
|
8555
10099
|
this._buildAggregationIndex(this.features);
|
|
8556
10100
|
}
|
|
10101
|
+
if (this._isVectorChart()) this._prepareVector();
|
|
8557
10102
|
|
|
8558
10103
|
this._prepareAlphaField();
|
|
8559
10104
|
}
|
|
@@ -8647,6 +10192,36 @@
|
|
|
8647
10192
|
_resolveAggregateValue(props) { return resolveAggregateValue(this.binding, this.flags, props); }
|
|
8648
10193
|
|
|
8649
10194
|
_buildAggregationIndex(sourceFeatures) {
|
|
10195
|
+
// an AGGREGATE / CATEGORICAL pie: one bucket, each record carrying its
|
|
10196
|
+
// contribution to its item (flatPieRecord), added up per position or
|
|
10197
|
+
// grid cell by aggregateOnGrid (flatPieAccumulate)
|
|
10198
|
+
if (this._isAggregatedPie()) {
|
|
10199
|
+
const t = this.flags.typeString != null ? this.flags.typeString : [...this.flags].join('|');
|
|
10200
|
+
const fields = this._pieCategorical ? null : String(this.binding.value || '').split('|');
|
|
10201
|
+
const rules = flatValueRules(t, fields ? fields.length : 1);
|
|
10202
|
+
const sizeField = this.binding.size && this.binding.size !== '$item$' ? this.binding.size : null;
|
|
10203
|
+
const feats = [];
|
|
10204
|
+
for (const f of sourceFeatures) {
|
|
10205
|
+
const c = f.geometry && f.geometry.coordinates;
|
|
10206
|
+
if (!c || typeof c[0] !== 'number' || !isFinite(c[0]) || !isFinite(c[1])) continue;
|
|
10207
|
+
const p = f.properties || {};
|
|
10208
|
+
let cat;
|
|
10209
|
+
if (this._pieCategorical) {
|
|
10210
|
+
const k = this._pieCategoryKey(p[this.binding.value]);
|
|
10211
|
+
const i = k == null ? undefined : this._pieCategoryIndex.get(k);
|
|
10212
|
+
cat = i == null ? -1 : i;
|
|
10213
|
+
}
|
|
10214
|
+
const pie = flatPieRecord(p, { rules, sizeField, cat, fields });
|
|
10215
|
+
if (!pie) continue;
|
|
10216
|
+
feats.push({ type: 'Feature', geometry: f.geometry, properties: { value: pie.size, pie, raw: p } });
|
|
10217
|
+
}
|
|
10218
|
+
this._featuresByCategory = [feats];
|
|
10219
|
+
this._clusterIndices = null;
|
|
10220
|
+
this._clusterRadiusPx = null;
|
|
10221
|
+
this._aggregateStatsCache = null;
|
|
10222
|
+
this._pieStatsCache = null;
|
|
10223
|
+
return;
|
|
10224
|
+
}
|
|
8650
10225
|
// Range-classed (non-CATEGORICAL numeric) coloring: DON'T pre-split
|
|
8651
10226
|
// by class here — the real engine aggregates every record together
|
|
8652
10227
|
// first and classes color from the resulting CELL TOTAL afterward
|
|
@@ -8829,7 +10404,10 @@
|
|
|
8829
10404
|
this._timeMin = undefined;
|
|
8830
10405
|
const facetFilters = this.facetFilters, runtimeFilterExpr = this._runtimeFilterExpr, timeFrame = this._timeFrame;
|
|
8831
10406
|
this.binding = this._specBinding;
|
|
8832
|
-
|
|
10407
|
+
// VECTOR / BEZIER keep every record: flat checks their values only
|
|
10408
|
+
// when it aggregates them (flatVectorItems)
|
|
10409
|
+
const withDensity = this._withDensity(fc.features);
|
|
10410
|
+
this.features = applyField100(isVectorChart(this.flags) ? withDensity : filterFlatValues(withDensity, this.binding, this.flags), this.binding, this.flags, this.style);
|
|
8833
10411
|
this.binding = field100Binding(this.binding, this.flags);
|
|
8834
10412
|
this._iconCache.clear();
|
|
8835
10413
|
this._glowIconCache.clear();
|
|
@@ -8872,10 +10450,14 @@
|
|
|
8872
10450
|
}
|
|
8873
10451
|
|
|
8874
10452
|
_usesAggregationIndex() {
|
|
10453
|
+
// AGGREGATE / CATEGORICAL pies aggregate their records into items
|
|
10454
|
+
// (see _buildAggregationIndex)
|
|
10455
|
+
if (this._isAggregatedPie()) return true;
|
|
8875
10456
|
// GRIDSIZE bins its own grid (_ensureGridIndex) — PLOT curves and the
|
|
8876
10457
|
// plain mesh; a classed GRIDSIZE symbol chart is aggregated like any
|
|
8877
10458
|
// other (see _isPlainGridMesh)
|
|
8878
|
-
|
|
10459
|
+
// VECTOR / BEZIER aggregate by their positions (flatVectorItems)
|
|
10460
|
+
return (this.flags.has('AGGREGATE') || isSymbolChart(this.flags)) && !isVectorChart(this.flags)
|
|
8879
10461
|
&& (!this.flags.has('GRIDSIZE') || (!this.flags.has('PLOT') && !this._isPlainGridMesh()));
|
|
8880
10462
|
}
|
|
8881
10463
|
|
|
@@ -9160,7 +10742,15 @@
|
|
|
9160
10742
|
return this._glowIconCache.get(key);
|
|
9161
10743
|
}
|
|
9162
10744
|
|
|
9163
|
-
|
|
10745
|
+
// look (optional, a CATEGORICAL BUBBLE's flat look — see bubbleIconLook):
|
|
10746
|
+
// every part filled at look.fillOpacity and stroked like its own flat
|
|
10747
|
+
// bubble (look.stroke, else its color × 0.7; look.strokeOpacity), the
|
|
10748
|
+
// width look.width canvas px for a part of the icon's full radius,
|
|
10749
|
+
// √(share of it) for a smaller one (flat's width ∝ √r) — OUTLINE:
|
|
10750
|
+
// ∝ min(1, r / normal radius), look.outlineRatio being the full
|
|
10751
|
+
// radius / normal radius. Without it the older look: 0.9 fill, a 1 px
|
|
10752
|
+
// white stroke (the SYMBOL branch's packed icon).
|
|
10753
|
+
_buildBubbleIcon(counts, colors, marked = null, shapes = null, look = null) {
|
|
9164
10754
|
// The cache key quantizes each count's RATIO to the group's total,
|
|
9165
10755
|
// not the raw count — computeBubblePackLayout's radii are purely
|
|
9166
10756
|
// sqrt(c/total), so two clusters with wildly different absolute
|
|
@@ -9190,7 +10780,8 @@
|
|
|
9190
10780
|
// coarse share buckets that can put a small marked part at 0
|
|
9191
10781
|
const key = counts.map(c => Math.round((c / total) * RATIO_BUCKETS)).join('-') + (marked
|
|
9192
10782
|
? '|iso:' + counts.map((c, i) => (c > 0 && marked.has(i) ? i + ':' + Math.round(Math.sqrt(c / total) * 100) : null)).filter(Boolean).join(',')
|
|
9193
|
-
: '') + (shapes ? '|sym:' + shapes.map(v => String(v).toLowerCase()).join(',') : '')
|
|
10783
|
+
: '') + (shapes ? '|sym:' + shapes.map(v => String(v).toLowerCase()).join(',') : '')
|
|
10784
|
+
+ (look ? `|fb:${look.fillOpacity}:${look.strokeOpacity}:${look.stroke ? look.stroke.join(',') : look.noStroke ? 'none' : 'low'}:${look.width}${look.outlineRatio != null ? ':' + look.outlineRatio : ''}` : '');
|
|
9194
10785
|
if (this._iconCache.has(key)) return this._iconCache.get(key);
|
|
9195
10786
|
const size = BUBBLE_ICON_SIZE;
|
|
9196
10787
|
const canvas = document.createElement('canvas');
|
|
@@ -9205,13 +10796,33 @@
|
|
|
9205
10796
|
// _resolveSymbolShape; circles when the name isn't one GL draws
|
|
9206
10797
|
const shape = shapes ? normalizeSymbolShape(shapes[Math.min(p.i, shapes.length - 1)]) : 'circle';
|
|
9207
10798
|
const px = cx + offsets[i].x * fitScale, py = cy + offsets[i].y * fitScale;
|
|
10799
|
+
let pr = radii[i] * fitScale, lw = 0;
|
|
10800
|
+
if (look && !look.noStroke) {
|
|
10801
|
+
const k = pr / (size / 2);
|
|
10802
|
+
lw = Math.min(pr, look.outlineRatio != null
|
|
10803
|
+
? look.width * Math.min(1, k * look.outlineRatio) / Math.min(1, look.outlineRatio)
|
|
10804
|
+
: look.width * Math.sqrt(k));
|
|
10805
|
+
// inside the part, as _buildSingleIcon: the icon's edge is the bubble's
|
|
10806
|
+
pr = Math.max(0.5, pr - lw / 2);
|
|
10807
|
+
}
|
|
9208
10808
|
ctx.beginPath();
|
|
9209
|
-
if (shape === 'circle') ctx.arc(px, py,
|
|
9210
|
-
else drawSymbolPath(ctx, shape, px, py,
|
|
10809
|
+
if (shape === 'circle') ctx.arc(px, py, pr, 0, Math.PI * 2);
|
|
10810
|
+
else drawSymbolPath(ctx, shape, px, py, pr);
|
|
9211
10811
|
ctx.closePath();
|
|
9212
10812
|
ctx.fillStyle = `rgb(${colors[p.i].join(',')})`;
|
|
9213
|
-
|
|
9214
|
-
|
|
10813
|
+
if (!look) {
|
|
10814
|
+
ctx.globalAlpha = 0.9; ctx.fill(); ctx.globalAlpha = 1;
|
|
10815
|
+
ctx.lineWidth = 1; ctx.strokeStyle = '#fff'; ctx.stroke();
|
|
10816
|
+
return;
|
|
10817
|
+
}
|
|
10818
|
+
ctx.globalAlpha = look.fillOpacity; ctx.fill();
|
|
10819
|
+
if (lw > 0) {
|
|
10820
|
+
ctx.globalAlpha = look.strokeOpacity;
|
|
10821
|
+
ctx.lineWidth = lw;
|
|
10822
|
+
ctx.strokeStyle = `rgb(${(look.stroke || flatDerivateRgb(colors[p.i], 0.7)).join(',')})`;
|
|
10823
|
+
ctx.stroke();
|
|
10824
|
+
}
|
|
10825
|
+
ctx.globalAlpha = 1;
|
|
9215
10826
|
});
|
|
9216
10827
|
const icon = { url: canvas.toDataURL(), width: size, height: size, anchorX: size / 2, anchorY: size / 2, id: key };
|
|
9217
10828
|
return this._cacheIcon(key, icon);
|
|
@@ -9240,10 +10851,12 @@
|
|
|
9240
10851
|
// cached bubble icon here already accepts; the border scales with
|
|
9241
10852
|
// the bubble exactly as the fill already does, which reads as
|
|
9242
10853
|
// reasonable rather than wrong.
|
|
9243
|
-
|
|
10854
|
+
// borderOpacity (default 1): the border's own alpha (flat's
|
|
10855
|
+
// stroke-opacity, see flatBubbleLook)
|
|
10856
|
+
_buildSingleIcon(colorRgb, opacity, shape, borderColorRgb, borderWidth, borderOpacity = 1) {
|
|
9244
10857
|
shape = shape || 'circle';
|
|
9245
10858
|
borderWidth = borderWidth || 0;
|
|
9246
|
-
const key = `single-${shape}-${colorRgb.join(',')}-${opacity}-${borderColorRgb ? borderColorRgb.join(',') : 'none'}-${borderWidth}`;
|
|
10859
|
+
const key = `single-${shape}-${colorRgb.join(',')}-${opacity}-${borderColorRgb ? borderColorRgb.join(',') : 'none'}-${borderWidth}${borderOpacity !== 1 ? `-so${borderOpacity}` : ''}`;
|
|
9247
10860
|
if (this._iconCache.has(key)) return this._iconCache.get(key);
|
|
9248
10861
|
const size = BUBBLE_ICON_SIZE;
|
|
9249
10862
|
const canvas = document.createElement('canvas');
|
|
@@ -9265,7 +10878,7 @@
|
|
|
9265
10878
|
ctx.globalAlpha = shape === 'empty' ? 0 : opacity;
|
|
9266
10879
|
ctx.fill();
|
|
9267
10880
|
if (borderWidth > 0 && borderColorRgb) {
|
|
9268
|
-
ctx.globalAlpha =
|
|
10881
|
+
ctx.globalAlpha = borderOpacity;
|
|
9269
10882
|
ctx.strokeStyle = `rgb(${borderColorRgb.join(',')})`;
|
|
9270
10883
|
ctx.lineWidth = borderWidth;
|
|
9271
10884
|
ctx.stroke();
|
|
@@ -9363,18 +10976,35 @@
|
|
|
9363
10976
|
local: {}
|
|
9364
10977
|
};
|
|
9365
10978
|
|
|
10979
|
+
// a VECTOR / BEZIER flow (measured on the twin page): no item chart,
|
|
10980
|
+
// label or class; its value is flat's nValuesA[0] — with CATEGORICAL
|
|
10981
|
+
// the category's number (1 for the first), else the (summed) value
|
|
10982
|
+
// field; its count the records of the item
|
|
10983
|
+
if (this._isVectorChart() && props.vector) {
|
|
10984
|
+
const v = props.vector.itemValue;
|
|
10985
|
+
dataObj.theme.chart = dataObj.theme.item.chart = '';
|
|
10986
|
+
dataObj.theme.item.value = v == null || isNaN(v) ? '' : flatFormatValue(v, explicitValueDecimals(this.style) ?? (Math.abs(v) < 1 ? 2 : 0), 'ROUND');
|
|
10987
|
+
dataObj.theme.item.label = dataObj.theme.item.class = '';
|
|
10988
|
+
dataObj.theme.item.count = props.count;
|
|
10989
|
+
}
|
|
10990
|
+
|
|
9366
10991
|
// bare {{FIELD}}, {{raw.FIELD}}, {{local.FIELD}} — only meaningful
|
|
9367
|
-
// for a single unaggregated record (see class comment above)
|
|
9368
|
-
|
|
9369
|
-
|
|
10992
|
+
// for a single unaggregated record (see class comment above); an
|
|
10993
|
+
// aggregated pie's item gives its first record's, as flat (data[0])
|
|
10994
|
+
const fieldsRaw = isGroup ? props.firstRaw : props.raw;
|
|
10995
|
+
if (fieldsRaw) {
|
|
10996
|
+
Object.keys(fieldsRaw).forEach(field => {
|
|
9370
10997
|
if (field === 'geometry') return;
|
|
9371
|
-
const v =
|
|
10998
|
+
const v = fieldsRaw[field];
|
|
9372
10999
|
if (this._isNumericValue(v)) {
|
|
9373
11000
|
dataObj[field] = this._formatTooltipValue(v);
|
|
9374
11001
|
dataObj.raw[field] = v;
|
|
9375
11002
|
} else {
|
|
9376
11003
|
dataObj[field] = v;
|
|
9377
|
-
|
|
11004
|
+
// translated, as flat's tooltip (tooltip_mustache.js 446:
|
|
11005
|
+
// ixmaps.getLocalString(value)) — the migration page's ISO
|
|
11006
|
+
// code → country name in {{local.from}} → {{local.to}}
|
|
11007
|
+
dataObj.local[field] = makeLocalText([])(v);
|
|
9378
11008
|
}
|
|
9379
11009
|
});
|
|
9380
11010
|
}
|
|
@@ -9660,8 +11290,21 @@
|
|
|
9660
11290
|
// USER charts (a page's own chart function, style.userdraw) go the
|
|
9661
11291
|
// chart pipeline too, drawn by _buildUserChartLayers (bubbles when the
|
|
9662
11292
|
// page has no such function)
|
|
11293
|
+
// VECTOR / BEZIER (flat's chartMap draws them before it calls
|
|
11294
|
+
// drawChart, maptheme.js 18148, 18376)
|
|
11295
|
+
// PIE / DONUT (flat's PIE branch comes before BUBBLE and SYMBOL in
|
|
11296
|
+
// drawChart, maptheme.js 19939)
|
|
11297
|
+
if (this._isVectorChart()) return this._buildVectorLayers(zoom, bbox, liveZoom, globeCenter);
|
|
11298
|
+
if (this._isPieChart()) return this._buildPieLayers(zoom, bbox, liveZoom, globeCenter);
|
|
11299
|
+
// BAR / BARS (flat's BAR branch, after BUBBLE and SYMBOL: a type
|
|
11300
|
+
// with those is not a bar chart, see isBarChart)
|
|
11301
|
+
if (this._isBarChart()) return this._buildBarLayers(zoom, bbox, liveZoom, globeCenter);
|
|
9663
11302
|
if (isSymbolChart(this.flags)) return this._buildChartLayers(zoom, bbox, liveZoom, globeCenter);
|
|
9664
|
-
|
|
11303
|
+
// once per theme, not on every redraw
|
|
11304
|
+
if (!this._warnedNoRenderer) {
|
|
11305
|
+
this._warnedNoRenderer = true;
|
|
11306
|
+
console.warn(`[ixmaps-gl] layer "${this.name}": type "${[...this.flags].join('|')}" has no implemented renderer`);
|
|
11307
|
+
}
|
|
9665
11308
|
return [];
|
|
9666
11309
|
}
|
|
9667
11310
|
|
|
@@ -10022,6 +11665,118 @@
|
|
|
10022
11665
|
_itemPlotFields() {
|
|
10023
11666
|
return String(this.binding.value || '').split('|');
|
|
10024
11667
|
}
|
|
11668
|
+
// CHART|PIE (and PIE|DONUT): one pie per record, its slices the value
|
|
11669
|
+
// fields; AGGREGATE and CATEGORICAL pies one pie per item — the records
|
|
11670
|
+
// of a position (or grid cell) — its slices the categories or the value
|
|
11671
|
+
// fields (_isAggregatedPie, _buildPieLayers)
|
|
11672
|
+
_isVectorChart() { return isVectorChart(this.flags); }
|
|
11673
|
+
// CHART|BAR / BARS: a bar chart per record or item, its bars a pie's
|
|
11674
|
+
// parts (_buildBarLayers)
|
|
11675
|
+
_isBarChart() { return isBarChart(this.flags); }
|
|
11676
|
+
_isPieChart() {
|
|
11677
|
+
return !isVectorChart(this.flags) && this.flags.has('CHART') && this.flags.has('PIE') && !this.flags.has('DOT');
|
|
11678
|
+
}
|
|
11679
|
+
// a pie whose items flat aggregates (loadAndAggregateValuesOfTheme):
|
|
11680
|
+
// AGGREGATE, or CATEGORICAL (a part per category — flat sums the
|
|
11681
|
+
// records of an item's position by category even without AGGREGATE)
|
|
11682
|
+
// (a bar chart's items too)
|
|
11683
|
+
_isAggregatedPie() {
|
|
11684
|
+
return (this._isPieChart() || this._isBarChart()) && (this.flags.has('AGGREGATE') || this.flags.has('CATEGORICAL'));
|
|
11685
|
+
}
|
|
11686
|
+
// a per-item chart whose parts are its value fields (legend rows, sums)
|
|
11687
|
+
_isMultiFieldChart() {
|
|
11688
|
+
return !!this._multiFields && (this._isMultiFieldSequence() || this._isPieChart() || this._isBarChart());
|
|
11689
|
+
}
|
|
11690
|
+
// flat's theme statistics for a pie (maptheme.js 9119-9130): nMin/nMax
|
|
11691
|
+
// over every part value of every item, the classes (partsA, flat's
|
|
11692
|
+
// distributeValues over that range, as many as the colorscheme has
|
|
11693
|
+
// colors) — flat colors a 1-part pie (or any with CLASSES) by them
|
|
11694
|
+
// (maptheme.js 20330-20341); several parts are colored by field.
|
|
11695
|
+
_preparePie() {
|
|
11696
|
+
if (this.flags.has('CATEGORICAL')) { this._preparePieCategories(); return; }
|
|
11697
|
+
this._pieCategorical = false;
|
|
11698
|
+
const fields = String(this.binding.value).split('|');
|
|
11699
|
+
const pooled = [];
|
|
11700
|
+
for (const f of this.features) {
|
|
11701
|
+
for (const k of fields) { const v = flatNumber(f.properties[k], this.flags); if (isFinite(v)) pooled.push(v); }
|
|
11702
|
+
}
|
|
11703
|
+
let nMin = Infinity, nMax = -Infinity;
|
|
11704
|
+
for (const v of pooled) { if (v < nMin) nMin = v; if (v > nMax) nMax = v; }
|
|
11705
|
+
const nParts = Math.trunc(styleNum(this.style.classes)) || colorSchemeClassCount(this.style.colorscheme) || DEFAULT_RANGE_CLASSES;
|
|
11706
|
+
if (fields.length > 1) {
|
|
11707
|
+
Object.assign(this, computeMultiFieldClasses(this.binding, this.style));
|
|
11708
|
+
this.categoryIndexByLabel = new Map(this.categoryLabels.map((v, i) => [v, i]));
|
|
11709
|
+
const scheme = Array.isArray(this.style.colorscheme) ? this.style.colorscheme : [];
|
|
11710
|
+
this._partNoFill = new Set(fields.map((f, i) => i).filter(i => String(scheme[i]).trim().toLowerCase() === 'none'));
|
|
11711
|
+
} else {
|
|
11712
|
+
const r = computeRangeClasses(this.features, this.binding, this.style, this.flags, v => this._formatTooltipValue(v));
|
|
11713
|
+
if (r) { Object.assign(this, r); this._rangeClassed = false; }
|
|
11714
|
+
this._multiFields = null;
|
|
11715
|
+
this._partNoFill = new Set();
|
|
11716
|
+
}
|
|
11717
|
+
this._pieFields = fields;
|
|
11718
|
+
if (pooled.length) {
|
|
11719
|
+
this._valueMin = nMin;
|
|
11720
|
+
this._valueMax = nMax;
|
|
11721
|
+
this.partsA = flatRangeParts(pooled, nMin, nMax, nParts, this.flags, styleNum(this.style.rangecentervalue));
|
|
11722
|
+
}
|
|
11723
|
+
}
|
|
11724
|
+
|
|
11725
|
+
// a CATEGORICAL pie's categories (flat's getStringValueIndex, maptheme.js
|
|
11726
|
+
// 9204-9243): style.values in their order, else the values in the order
|
|
11727
|
+
// the records bring them; a value is trimmed (IGNORECASE: upper case),
|
|
11728
|
+
// an empty one is none (UNDEFINEDISVALUE: "undefined"). A slice per
|
|
11729
|
+
// category in its colorscheme color, labeled by style.label. flat's
|
|
11730
|
+
// classes are the category numbers (partsA: i + 1, the last one 0.001
|
|
11731
|
+
// wider) — its value range (nMin / nMax) follows the items
|
|
11732
|
+
// (_pieAggregateStats).
|
|
11733
|
+
_preparePieCategories() {
|
|
11734
|
+
const st = this.style;
|
|
11735
|
+
const t = this.flags.typeString != null ? this.flags.typeString : [...this.flags].join('|');
|
|
11736
|
+
const ignoreCase = /IGNORECASE/.test(t), undefinedIsValue = /UNDEFINEDISVALUE/.test(t);
|
|
11737
|
+
const key = v => {
|
|
11738
|
+
let k = v == null ? '' : String(v);
|
|
11739
|
+
if (k.length === 0 || k === ' ') { if (!undefinedIsValue) return null; k = 'undefined'; }
|
|
11740
|
+
if (ignoreCase) k = k.toUpperCase();
|
|
11741
|
+
return k.trim();
|
|
11742
|
+
};
|
|
11743
|
+
const index = new Map();
|
|
11744
|
+
const explicit = Array.isArray(st.values) ? st.values : null;
|
|
11745
|
+
const labels = [];
|
|
11746
|
+
if (explicit) {
|
|
11747
|
+
explicit.forEach(v => { const k = key(v); if (k != null && !index.has(k)) { index.set(k, labels.length); labels.push(String(v).trim()); } });
|
|
11748
|
+
} else {
|
|
11749
|
+
for (const f of this.features) {
|
|
11750
|
+
const k = key(f.properties[this.binding.value]);
|
|
11751
|
+
if (k != null && !index.has(k)) { index.set(k, labels.length); labels.push(k); }
|
|
11752
|
+
}
|
|
11753
|
+
}
|
|
11754
|
+
this._pieCategorical = true;
|
|
11755
|
+
this._pieExplicitValues = !!explicit;
|
|
11756
|
+
this._pieCategoryKey = key;
|
|
11757
|
+
this._pieCategoryIndex = index;
|
|
11758
|
+
this.categoryLabels = labels;
|
|
11759
|
+
const explicitLabel = Array.isArray(st.label) ? st.label.map(String) : null;
|
|
11760
|
+
this.categoryDisplayLabels = explicitLabel && explicitLabel.length === labels.length ? explicitLabel : labels;
|
|
11761
|
+
this.categoryColorsRgb = resolveClassColors(st.colorscheme, this.categoryDisplayLabels, st.classes);
|
|
11762
|
+
// the legend and the facets look a record's raw value up
|
|
11763
|
+
const byLabel = new Map();
|
|
11764
|
+
for (const f of this.features) {
|
|
11765
|
+
const v = f.properties[this.binding.value];
|
|
11766
|
+
if (byLabel.has(v)) continue;
|
|
11767
|
+
const k = key(v);
|
|
11768
|
+
if (k != null && index.has(k)) byLabel.set(v, index.get(k));
|
|
11769
|
+
}
|
|
11770
|
+
labels.forEach((l, i) => { if (!byLabel.has(l)) byLabel.set(l, i); });
|
|
11771
|
+
this.categoryIndexByLabel = byLabel;
|
|
11772
|
+
const n = labels.length;
|
|
11773
|
+
this.partsA = labels.map((l, i) => ({ min: i + 1, max: i + 1 + (i === n - 1 ? 0.001 : 0) + 1e-9 }));
|
|
11774
|
+
this._rangeClassed = false;
|
|
11775
|
+
this._multiFields = null;
|
|
11776
|
+
this._pieFields = null;
|
|
11777
|
+
this._partNoFill = new Set();
|
|
11778
|
+
}
|
|
11779
|
+
|
|
10025
11780
|
// x-axis labels of a per-item PLOT (style.xaxis / label, else the fields)
|
|
10026
11781
|
_itemPlotLabels() {
|
|
10027
11782
|
const fields = this._itemPlotFields();
|
|
@@ -11152,8 +12907,7 @@
|
|
|
11152
12907
|
const combined = individual.concat(groups).sort((a, b) => sizeValueOf(a) - sizeValueOf(b));
|
|
11153
12908
|
// maxcharts: like the flat engine (maptheme.js nMaxCharts), draw only the
|
|
11154
12909
|
// biggest N items — combined is ascending, so drop from the front
|
|
11155
|
-
|
|
11156
|
-
if (maxCharts > 0 && combined.length > maxCharts) combined.splice(0, combined.length - maxCharts);
|
|
12910
|
+
applyFlatMaxCharts(combined, this.style);
|
|
11157
12911
|
if (this.flags.has('USER')) {
|
|
11158
12912
|
const userLayers = this._buildUserChartLayers(combined, liveZoom, sizeValueOf);
|
|
11159
12913
|
if (userLayers) return layers.concat(userLayers);
|
|
@@ -11434,17 +13188,94 @@
|
|
|
11434
13188
|
// fillopacity and linecolor, as a single item (flat draws one
|
|
11435
13189
|
// symbol) — the packed multi-category icon is for CATEGORICAL cells
|
|
11436
13190
|
const singleSymbol = d => !d.properties.counts || (this._rangeClassed && d.properties.cat != null);
|
|
11437
|
-
|
|
13191
|
+
// a CATEGORICAL BUBBLE (flat's BUBBLE branch): every bubble — a single
|
|
13192
|
+
// one and each part of a packed one — filled and stroked as flat's,
|
|
13193
|
+
// see flatBubbleLook; the icon carries both opacities
|
|
13194
|
+
const flatLook = flatChartBranch(this.flags) === 'bubble' && this.flags.has('CATEGORICAL')
|
|
13195
|
+
? d => bubbleIconLook(radiusOf(d), objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(this.style.scale) || 1), this.style, this.flags, BUBBLE_ICON_SIZE)
|
|
13196
|
+
: null;
|
|
13197
|
+
const singleIcon = d => {
|
|
13198
|
+
const rgb = (this.categoryColorsRgb || [])[d.properties.cat];
|
|
13199
|
+
if (!flatLook) return this._buildSingleIcon(rgb, fillOpacity, shapeOf(d.properties), singleBorderColorRgb, singleBorderWidthPx);
|
|
13200
|
+
const look = flatLook(d);
|
|
13201
|
+
return this._buildSingleIcon(rgb, look.fillOpacity, shapeOf(d.properties),
|
|
13202
|
+
look.noStroke || !rgb ? null : (look.stroke || flatDerivateRgb(rgb, 0.7)), look.width, look.strokeOpacity);
|
|
13203
|
+
};
|
|
13204
|
+
// flat's symbol outline (maptheme.js 21595-21610 and 21950-21997,
|
|
13205
|
+
// drawn 21625-21634 / 21995-22035): every SYMBOL shape is stroked,
|
|
13206
|
+
// (linewidth || 1) · normalX(min(2r / rMax, 0.2)) with rMax the
|
|
13207
|
+
// normal radius normalX(30 / 2) — on screen min(2r / 15, 0.2 unit)
|
|
13208
|
+
// × linewidth, centered on the outline like any SVG stroke. Its
|
|
13209
|
+
// color: linecolor; NOLINES or linecolor "none" none; a CATEGORICAL
|
|
13210
|
+
// theme without AGGREGATE/GROUP the symbol's own color (white: gray),
|
|
13211
|
+
// every other symbol chart the darker ChartColors.textColor of its
|
|
13212
|
+
// class color. BUBBLE keeps its linecolor/linewidth border, a width
|
|
13213
|
+
// in icon pixels
|
|
13214
|
+
const symbolOutline = flatChartBranch(this.flags) === 'symbol';
|
|
13215
|
+
const outlineRgb = d => {
|
|
13216
|
+
if (!symbolOutline) return singleBorderColorRgb && singleBorderWidthPx > 0 ? singleBorderColorRgb : null;
|
|
13217
|
+
if (singleBorderColorRgb) return singleBorderColorRgb;
|
|
13218
|
+
if (flatFlag(this.flags, 'NOLINES') || (this.style.linecolor && styleLineColor(this.style.linecolor) === 'none')) return null;
|
|
13219
|
+
const fill = (this.categoryColorsRgb || [])[d.properties.cat] || [128, 128, 128];
|
|
13220
|
+
if (perPartBranch) return flatChartTextRgb(fill);
|
|
13221
|
+
return fill.slice(0, 3).every(c => c >= 255) ? [128, 128, 128] : fill;
|
|
13222
|
+
};
|
|
13223
|
+
const outlinePx = (d, r) => (!outlineRgb(d) ? 0 : symbolOutline
|
|
13224
|
+
? (styleNum(this.style.linewidth) || 1) * Math.min(2 * r / NORMAL_RADIUS_PX, 0.2 * unitPx)
|
|
13225
|
+
: singleBorderWidthPx * r * 2 / BUBBLE_ICON_SIZE);
|
|
13226
|
+
// only single circles (no group icon, no other shape, no pixel
|
|
13227
|
+
// offset — ScatterplotLayer has none): drawn as real circles. A
|
|
13228
|
+
// cached circle icon shrunk to a few device pixels samples a coarse
|
|
13229
|
+
// mip level of the atlas and reads as a square, where flat's SVG
|
|
13230
|
+
// circle of the same radius stays round. Its own id: deck.gl matches
|
|
13231
|
+
// layers by id alone, so a theme switch turning the IconLayer of
|
|
13232
|
+
// this id into a ScatterplotLayer kept the icon layer's state and
|
|
13233
|
+
// drew nothing (the fuel-price page's station points); hover/click
|
|
13234
|
+
// routing goes by the final id (layerRuntimeById). A symbol's outline
|
|
13235
|
+
// is centered on its radius (flat); BUBBLE's border keeps the
|
|
13236
|
+
// icon's look, inside the radius
|
|
13237
|
+
if (combined.length && !alignOf.active && combined.every(d => singleSymbol(d) && shapeOf(d.properties) === 'circle')) {
|
|
13238
|
+
const radiusPx = d => vRadius(sizeValueOf(d));
|
|
13239
|
+
// a CATEGORICAL BUBBLE: flat's look (flatBubbleLook) in screen px,
|
|
13240
|
+
// its stroke centered on the radius as flat's SVG circle
|
|
13241
|
+
const lookUnit = objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(this.style.scale) || 1);
|
|
13242
|
+
const circleLook = flatLook ? d => flatBubbleLook((this.categoryColorsRgb || [])[d.properties.cat] || [128, 128, 128], radiusPx(d), lookUnit, this.style, this.flags) : null;
|
|
13243
|
+
const lineRgb = d => (circleLook ? circleLook(d).stroke : outlineRgb(d)) || [0, 0, 0];
|
|
13244
|
+
layers.push(new ScatterplotLayer({
|
|
13245
|
+
id: `ix-bubbles-${this.name}-circles-g${this._iconGeneration}`,
|
|
13246
|
+
data: combined, pickable: true,
|
|
13247
|
+
getPosition: d => d.geometry.coordinates,
|
|
13248
|
+
getRadius: d => radiusPx(d) - (symbolOutline || circleLook ? 0 : outlinePx(d, radiusPx(d)) / 2),
|
|
13249
|
+
radiusUnits: 'pixels',
|
|
13250
|
+
getFillColor: d => [...((this.categoryColorsRgb || [])[d.properties.cat] || [128, 128, 128]).slice(0, 3), Math.round(this._iconAlpha(d) * (circleLook ? circleLook(d).fillOpacity : fillOpacity))],
|
|
13251
|
+
stroked: circleLook ? combined.some(d => circleLook(d).stroke) : combined.some(d => outlineRgb(d)),
|
|
13252
|
+
getLineColor: d => [...lineRgb(d).slice(0, 3), Math.round(this._iconAlpha(d) * (circleLook ? circleLook(d).strokeOpacity : 1))],
|
|
13253
|
+
getLineWidth: d => (circleLook ? (circleLook(d).stroke ? circleLook(d).width : 0) : outlinePx(d, radiusPx(d))),
|
|
13254
|
+
lineWidthUnits: 'pixels',
|
|
13255
|
+
billboard: true,
|
|
13256
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
13257
|
+
}));
|
|
13258
|
+
} else layers.push(new IconLayer({
|
|
11438
13259
|
id: `ix-bubbles-${this.name}-g${this._iconGeneration}`,
|
|
11439
13260
|
data: combined, pickable: true,
|
|
11440
13261
|
getPosition: d => d.geometry.coordinates,
|
|
11441
|
-
getIcon: d =>
|
|
11442
|
-
|
|
11443
|
-
|
|
13262
|
+
getIcon: d => {
|
|
13263
|
+
if (!singleSymbol(d)) return this._buildBubbleIcon(d.properties.counts, this.categoryColorsRgb, this._groupIsolation(d), Array.isArray(this.style.symbols) ? this.style.symbols : null, flatLook && flatLook(d));
|
|
13264
|
+
if (flatLook) return singleIcon(d);
|
|
13265
|
+
const shape = shapeOf(d.properties), rgb = outlineRgb(d);
|
|
13266
|
+
if (!symbolOutline || !rgb) return this._buildSingleIcon((this.categoryColorsRgb || [])[d.properties.cat], fillOpacity, shape, rgb, rgb ? singleBorderWidthPx : 0);
|
|
13267
|
+
// the outline in the icon's own pixels, inside its edge; quarter
|
|
13268
|
+
// pixel steps keep the icon cache small. flat's "empty" strokes
|
|
13269
|
+
// #888888 whatever the class color
|
|
13270
|
+
const r = vRadius(sizeValueOf(d));
|
|
13271
|
+
const w = Math.round(outlinePx(d, r) * BUBBLE_ICON_SIZE / Math.max(1e-6, 2 * r) * 4) / 4;
|
|
13272
|
+
return this._buildSingleIcon((this.categoryColorsRgb || [])[d.properties.cat], fillOpacity, shape, shape === 'empty' ? [136, 136, 136] : rgb, w);
|
|
13273
|
+
},
|
|
11444
13274
|
getSize: d => vRadius(sizeValueOf(d)) * 2,
|
|
11445
13275
|
// group icons bake a fixed 0.9 alpha; an explicit fillopacity scales it
|
|
11446
13276
|
// so a near-transparent theme doesn't show opaque 1-px-cell clusters
|
|
11447
|
-
|
|
13277
|
+
// (a flat-look icon carries its own fill and stroke opacity)
|
|
13278
|
+
getColor: d => [255, 255, 255, Math.round(this._iconAlpha(d) * (!flatLook && !singleSymbol(d) && this.style.fillopacity != null ? fillOpacity : 1))],
|
|
11448
13279
|
...(alignOf.active ? { getPixelOffset: alignOf } : {}),
|
|
11449
13280
|
sizeUnits: 'pixels',
|
|
11450
13281
|
billboard: true,
|
|
@@ -11478,8 +13309,8 @@
|
|
|
11478
13309
|
// VALUES: bold value label centered on each bubble (see
|
|
11479
13310
|
// flatValueText/valuesFontSizePx above). Labels are their own
|
|
11480
13311
|
// layers, added last in the array so they draw on top of every icon
|
|
11481
|
-
// layer
|
|
11482
|
-
//
|
|
13312
|
+
// layer; a label under later-drawn charts is faded or dropped by
|
|
13313
|
+
// _occludeChartTexts, as flat's later chart groups cover it. Entries whose
|
|
11483
13314
|
// computed font size would be sub-pixel are dropped rather than
|
|
11484
13315
|
// rendered at getSize: 0, matching the real engine's own "too
|
|
11485
13316
|
// small to bother" gate.
|
|
@@ -11488,7 +13319,11 @@
|
|
|
11488
13319
|
const textOpts = this._valueTextOpts();
|
|
11489
13320
|
const pointValueOf = valueTextOf || this._chartValueTextFn();
|
|
11490
13321
|
|
|
13322
|
+
// draw order of each chart (see chartTextTransmittance below); an
|
|
13323
|
+
// item maxcharts left out draws no chart, so no text either
|
|
13324
|
+
const orderOf = new Map(combined.map((d, k) => [d, k]));
|
|
11491
13325
|
const pointLabels = individual.reduce((out, d) => {
|
|
13326
|
+
if (!orderOf.has(d)) return out;
|
|
11492
13327
|
const radius = vRadius(d.properties.value);
|
|
11493
13328
|
// flat's VALUES label prints the value (nValuesA), not the size;
|
|
11494
13329
|
// its font by flat's chart branch (_chartValueTextFn)
|
|
@@ -11496,7 +13331,7 @@
|
|
|
11496
13331
|
if (fontSize > VALUES_MIN_FONT_PX) {
|
|
11497
13332
|
const o = alignOf(d);
|
|
11498
13333
|
out.push({ geometry: d.geometry, text, fontSize, bold, color: resolveTextColor(this.style, contrastTextColor((this.categoryColorsRgb || [])[d.properties.cat])),
|
|
11499
|
-
pixelOffset: baseline ? [o[0], o[1] + baseline - 0.35 * fontSize] : o });
|
|
13334
|
+
pixelOffset: baseline ? [o[0], o[1] + baseline - 0.35 * fontSize] : o, order: orderOf.get(d) });
|
|
11500
13335
|
}
|
|
11501
13336
|
return out;
|
|
11502
13337
|
}, []);
|
|
@@ -11512,6 +13347,7 @@
|
|
|
11512
13347
|
// would misrepresent every category's own count as one summed
|
|
11513
13348
|
// number nobody's individual bubble actually shows.
|
|
11514
13349
|
const groupLabels = groups.reduce((out, d) => {
|
|
13350
|
+
if (!orderOf.has(d)) return out;
|
|
11515
13351
|
const counts = d.properties.counts;
|
|
11516
13352
|
const outerRadiusPx = vRadius(d.properties.total);
|
|
11517
13353
|
const iconSizePx = outerRadiusPx * 2; // matches the cluster IconLayer's own getSize (*2) below
|
|
@@ -11526,7 +13362,7 @@
|
|
|
11526
13362
|
const o = alignOf(d);
|
|
11527
13363
|
out.push({ geometry: d.geometry, text: v.text, fontSize: v.fontSize, bold: v.bold,
|
|
11528
13364
|
color: resolveTextColor(this.style, contrastTextColor((this.categoryColorsRgb || [])[d.properties.cat])),
|
|
11529
|
-
pixelOffset: v.baseline ? [o[0], o[1] + v.baseline - 0.35 * v.fontSize] : o });
|
|
13365
|
+
pixelOffset: v.baseline ? [o[0], o[1] + v.baseline - 0.35 * v.fontSize] : o, order: orderOf.get(d) });
|
|
11530
13366
|
}
|
|
11531
13367
|
return out;
|
|
11532
13368
|
}
|
|
@@ -11541,13 +13377,26 @@
|
|
|
11541
13377
|
out.push({
|
|
11542
13378
|
geometry: d.geometry, text, fontSize,
|
|
11543
13379
|
color: resolveTextColor(this.style, contrastTextColor((this.categoryColorsRgb || [])[p.i])),
|
|
11544
|
-
pixelOffset: [offsets[i].x * fitScale * pxPerCanvasUnit + alignOf(d)[0], offsets[i].y * fitScale * pxPerCanvasUnit + alignOf(d)[1]]
|
|
13380
|
+
pixelOffset: [offsets[i].x * fitScale * pxPerCanvasUnit + alignOf(d)[0], offsets[i].y * fitScale * pxPerCanvasUnit + alignOf(d)[1]],
|
|
13381
|
+
order: orderOf.get(d)
|
|
11545
13382
|
});
|
|
11546
13383
|
}
|
|
11547
13384
|
});
|
|
11548
13385
|
return out;
|
|
11549
13386
|
}, []);
|
|
11550
13387
|
|
|
13388
|
+
// a text under later charts fades by what still shows through them
|
|
13389
|
+
// (chartTextTransmittance), as flat's per-chart SVG groups paint
|
|
13390
|
+
// each later symbol over it; nearly hidden ones are dropped. (Flat's
|
|
13391
|
+
// later GLOW circles veil it too, but they veil the earlier symbols
|
|
13392
|
+
// as well, and here every glow lies under every symbol — a text
|
|
13393
|
+
// fading under a glow its own symbol isn't under would read wrong.)
|
|
13394
|
+
const visible = this._occludeChartTexts(combined, pointLabels.concat(groupLabels), {
|
|
13395
|
+
liveZoom, globeCenter, alignOf, radiusOf, singleSymbol, shapeOf, fillOpacity, singleBorderWidthPx
|
|
13396
|
+
});
|
|
13397
|
+
const keepVisible = labels => labels.filter(l => visible.has(l));
|
|
13398
|
+
const shownPointLabels = keepVisible(pointLabels), shownGroupLabels = keepVisible(groupLabels);
|
|
13399
|
+
|
|
11551
13400
|
const textLayerCommonProps = {
|
|
11552
13401
|
pickable: false,
|
|
11553
13402
|
getPosition: d => d.geometry.coordinates,
|
|
@@ -11565,10 +13414,10 @@
|
|
|
11565
13414
|
getAlignmentBaseline: 'center'
|
|
11566
13415
|
};
|
|
11567
13416
|
// flat's SYMBOL branch writes its values in normal weight
|
|
11568
|
-
if (
|
|
11569
|
-
fontWeight:
|
|
11570
|
-
if (
|
|
11571
|
-
fontWeight:
|
|
13417
|
+
if (shownPointLabels.length) layers.push(new TextLayer({ id: `ix-points-values-${this.name}`, data: shownPointLabels, getPixelOffset: d => d.pixelOffset, ...textLayerCommonProps,
|
|
13418
|
+
fontWeight: shownPointLabels[0].bold === false ? 'normal' : 'bold' }));
|
|
13419
|
+
if (shownGroupLabels.length) layers.push(new TextLayer({ id: `ix-cluster-values-${this.name}`, data: shownGroupLabels, getPixelOffset: d => d.pixelOffset, ...textLayerCommonProps,
|
|
13420
|
+
fontWeight: shownGroupLabels[0].bold === false ? 'normal' : 'bold' }));
|
|
11572
13421
|
}
|
|
11573
13422
|
|
|
11574
13423
|
// LABEL: flat's chart label (maptheme.js 24533-24590) — a small text
|
|
@@ -11792,6 +13641,1007 @@
|
|
|
11792
13641
|
return layers;
|
|
11793
13642
|
}
|
|
11794
13643
|
|
|
13644
|
+
// CHART|PIE / DONUT — real ixmaps-flat's PIE branch (maptheme.js
|
|
13645
|
+
// 19939-20533, piechart.js DonutChart): one pie per record at its
|
|
13646
|
+
// position (joined to its FEATURE shape by binding.lookup), a slice per
|
|
13647
|
+
// value field (pieSliceLayout), drawn by PieSliceLayer (see there why
|
|
13648
|
+
// not as icons).
|
|
13649
|
+
// - radius (flat's nSize in chart units, 15 = NORMAL_RADIUS_PX, × the
|
|
13650
|
+
// object zoom and style.scale): with a type word containing SIZE
|
|
13651
|
+
// (not NORMSIZE) and a size field 15 · (size / max size)^(1/sizepow)
|
|
13652
|
+
// — max size: normalsizevalue, else the field's maximum (flat's
|
|
13653
|
+
// nMaxSize) —, SIZE without a size field 15 · (sum / (nMax − nMin))
|
|
13654
|
+
// ^(1/sizepow) — the pie's sum against the range of the part values
|
|
13655
|
+
// (normalsizevalue replaces that range) —, SIZELOG 15 · log(v) /
|
|
13656
|
+
// log(max), at least 1; otherwise 15. A pie of radius 0 (or no
|
|
13657
|
+
// number) is not drawn.
|
|
13658
|
+
// - colors: a field's colorscheme color; a 1-field pie (or CLASSES)
|
|
13659
|
+
// takes its value's class color (flat's partsA, maptheme.js
|
|
13660
|
+
// 20330-20341); a "none" color fills nothing and strokes the pie in
|
|
13661
|
+
// linecolor, else black; AUTOCOMPLETE's rest in nodatacolor (#eeeeee)
|
|
13662
|
+
// - fill opacity fillopacity (flat: the chart group's fill-opacity,
|
|
13663
|
+
// default 1); opacity sets fill and outline
|
|
13664
|
+
// - outline #555566 (NOLINES none, WHITELINES white, linecolor wins),
|
|
13665
|
+
// (linewidth, default 0.1) · R / 15 px wide — flat's
|
|
13666
|
+
// normalX(linewidth) · R / 300 with normalX(1) = 20 chart units
|
|
13667
|
+
// (measured on the twin page): a hairline at the default
|
|
13668
|
+
// - CENTER (not DONUT): the center part's disc, 0.9 × the inner
|
|
13669
|
+
// radius, in its color, with GLOW two halos (6× at 0.05, 2× at 0.1
|
|
13670
|
+
// fill opacity); its value in bold in the middle with CENTERVALUE or
|
|
13671
|
+
// VALUES: min(0.8 r, 3.3 r / length) · valuescale, valuedecimals
|
|
13672
|
+
// (else 0), the unit up to 5 characters, ChartColors.textColor of
|
|
13673
|
+
// the part color
|
|
13674
|
+
// - DONUT|CENTERVALUE: the item's size value in the middle, min(0.5 r,
|
|
13675
|
+
// 2 r / length), valuecolor / textcolor, else #888888, over a 0.5
|
|
13676
|
+
// black halo
|
|
13677
|
+
// - VALUES (not STACKED, within valueupper/lower): several parts (or
|
|
13678
|
+
// NOINLINETEXT) a label per slice outside the pie on a leader line
|
|
13679
|
+
// (pieValueLabelLayout) — font 5u · valuescale, #999999 (textcolor)
|
|
13680
|
+
// on a white box at 0.5 (VALUEBACKGROUND: the slice color), leader
|
|
13681
|
+
// lines #888888 0.5u over a white 1.5u at 0.2; a single part its
|
|
13682
|
+
// value inside the pie, min(0.8 R, 3.4 R / length), 0.1 R above the
|
|
13683
|
+
// center. Texts: valuedecimals, else 2 below 1 and 0 above, plus the
|
|
13684
|
+
// unit.
|
|
13685
|
+
// - draw order: flat's sort before drawing (by the size value, the
|
|
13686
|
+
// sum or the first value; the biggest on top)
|
|
13687
|
+
// - AGGREGATE / CATEGORICAL (_isAggregatedPie): a pie per item — the
|
|
13688
|
+
// records of a position, or of an aggregation grid cell —
|
|
13689
|
+
// (_pieAggregatedItems; flatPieRecord / flatPieAccumulate /
|
|
13690
|
+
// flatPieItemValues), a slice per category (CATEGORICAL, in its
|
|
13691
|
+
// category color) or per value field; its size the item's summed
|
|
13692
|
+
// size field (flat's nSize, every record of the item counted), the
|
|
13693
|
+
// theme's nMin / nMax / nMaxSize those of the items
|
|
13694
|
+
// (_pieAggregateStats); draw order by the item's size with a size
|
|
13695
|
+
// field or AGGREGATE|SUM; COUNT prints a part's record count;
|
|
13696
|
+
// CENTERVALUE the item's record count
|
|
13697
|
+
// Not drawn here (a console note at type parsing, PIE_INERT_FLAGS,
|
|
13698
|
+
// PIE_AGGREGATE_INERT_FLAGS):
|
|
13699
|
+
// 3D, VOLUME, HEIGHT, STARBURST, FLOWER/RAYS, BOW, ZOOM, DIRECTION /
|
|
13700
|
+
// DIRECTED, POLAR — the pie is drawn flat and plain instead. Nor
|
|
13701
|
+
// style.clipparts, showparts, gridx (flat's several pies per item).
|
|
13702
|
+
// Known divergence: flat draws a chart's value labels in the chart's
|
|
13703
|
+
// own group, so a bigger pie drawn later covers a smaller neighbour's
|
|
13704
|
+
// labels; here every label lies above every pie.
|
|
13705
|
+
// Known divergence: the leader lines are drawn in map coordinates
|
|
13706
|
+
// (pixel offsets converted at the current zoom, Web Mercator), so under
|
|
13707
|
+
// the globe projection they are approximate.
|
|
13708
|
+
_buildPieLayers(zoom, bbox, liveZoom = zoom, globeCenter = null) {
|
|
13709
|
+
const st = this.style;
|
|
13710
|
+
const t = this.flags.typeString != null ? this.flags.typeString : [...this.flags].join('|');
|
|
13711
|
+
const has = re => re.test(t);
|
|
13712
|
+
const agg = this._isAggregatedPie();
|
|
13713
|
+
const cat = !!this._pieCategorical;
|
|
13714
|
+
const fields = cat ? null : (this._pieFields || String(this.binding.value || '').split('|'));
|
|
13715
|
+
const multi = !cat && fields.length > 1;
|
|
13716
|
+
// AGGREGATE / CATEGORICAL: the statistics of the items (sets
|
|
13717
|
+
// _valueMin / _valueMax), their biggest size flat's nMaxSize
|
|
13718
|
+
const stats = agg ? this._pieAggregateStats(zoom) : null;
|
|
13719
|
+
const unit = objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(st.scale) || 1);
|
|
13720
|
+
const sizePow = resolveSizePow(st, this.flags);
|
|
13721
|
+
// flat tests /SIZE/: also NOSIZE and GRIDSIZE
|
|
13722
|
+
const sizeFlag = has(/SIZE/) && !has(/NORMSIZE/);
|
|
13723
|
+
const sizeLog = has(/SIZELOG/);
|
|
13724
|
+
const nsv = styleNum(st.normalsizevalue);
|
|
13725
|
+
const maxSize = nsv || (stats ? stats.maxSize : this._maxSizeValue);
|
|
13726
|
+
const range = nsv || ((this._valueMax || 0) - (this._valueMin || 0));
|
|
13727
|
+
const nSizeOf = (sum, size) => {
|
|
13728
|
+
const n = NORMAL_RADIUS_PX;
|
|
13729
|
+
if (sizeFlag && this.binding.size) {
|
|
13730
|
+
return sizeLog ? Math.max(1, n / Math.log(maxSize) * Math.log(size)) : n / Math.pow(maxSize, 1 / sizePow) * Math.pow(size, 1 / sizePow);
|
|
13731
|
+
}
|
|
13732
|
+
if (sizeFlag) {
|
|
13733
|
+
return sizeLog ? Math.max(1, n / Math.log(range) * Math.log(sum)) : n / Math.pow(range, 1 / sizePow) * Math.pow(sum, 1 / sizePow);
|
|
13734
|
+
}
|
|
13735
|
+
return n;
|
|
13736
|
+
};
|
|
13737
|
+
const colors = this.categoryColorsRgb || [];
|
|
13738
|
+
const scheme = Array.isArray(st.colorscheme) ? st.colorscheme : [];
|
|
13739
|
+
const noFill = this._partNoFill || new Set();
|
|
13740
|
+
const byClass = has(/CLASSES/) || (!cat && !multi);
|
|
13741
|
+
// flat (maptheme.js 20327-20336): a pie of one part (or CLASSES) takes
|
|
13742
|
+
// the color of the class its value falls in — CATEGORICAL: the class
|
|
13743
|
+
// whose number equals the value (sl.single: the item's only part)
|
|
13744
|
+
const classColor = v => {
|
|
13745
|
+
const parts = this.partsA || [];
|
|
13746
|
+
for (let x = 0; x < parts.length; x++) if (cat ? v === parts[x].min : v < parts[x].max) return colors[x];
|
|
13747
|
+
return null;
|
|
13748
|
+
};
|
|
13749
|
+
const sliceRgb = sl => (sl.complement ? (parseCssColor(String(st.nodatacolor || '')) || [0xee, 0xee, 0xee])
|
|
13750
|
+
: ((byClass || sl.single) && classColor(sl.value)) || colors[sl.i] || [128, 128, 128]);
|
|
13751
|
+
const fo = styleNum(st.fillopacity), op = styleNum(st.opacity);
|
|
13752
|
+
// flat sets the group's fill-opacity only for a truthy value
|
|
13753
|
+
const fillAlpha = op > 0 ? op : fo > 0 ? fo : 1;
|
|
13754
|
+
const lineAlpha = op > 0 ? op : 1;
|
|
13755
|
+
let line = '#555566';
|
|
13756
|
+
if (has(/NOLINES/)) line = 'none';
|
|
13757
|
+
if (has(/WHITELINES/)) line = 'white';
|
|
13758
|
+
const lc = st.linecolor != null && st.linecolor !== '' ? styleLineColor(st.linecolor) : null;
|
|
13759
|
+
if (lc) line = lc;
|
|
13760
|
+
if (!cat && scheme.slice(0, fields.length).some(c => String(c).trim().toLowerCase() === 'none')) line = lc || 'black';
|
|
13761
|
+
const lineRgb = line === 'none' ? null : hexOrNamedToRgb(line);
|
|
13762
|
+
const lineWidthOf = r => (lineRgb ? (styleNum(st.linewidth) || 0.1) * r / 15 : 0);
|
|
13763
|
+
const lineRgba = lineRgb ? [...lineRgb.slice(0, 3), Math.round(255 * lineAlpha)] : [0, 0, 0, 0];
|
|
13764
|
+
|
|
13765
|
+
const charts = [];
|
|
13766
|
+
if (agg) {
|
|
13767
|
+
// one pie per item (_pieAggregatedItems), its parts and size as flat
|
|
13768
|
+
// has them after aggregation (flatPieItemValues)
|
|
13769
|
+
for (const f of this._pieAggregatedItems(zoom, bbox)) {
|
|
13770
|
+
const c = f.geometry.coordinates;
|
|
13771
|
+
if (globeCenter && !isOnVisibleHemisphere(c[0], c[1], globeCenter)) continue;
|
|
13772
|
+
const pie = f.properties.pie;
|
|
13773
|
+
const { parts: values, size } = this._pieItemValues(pie);
|
|
13774
|
+
const layout = pieSliceLayout(values, t, { centerPart: st.centerpart, itemCount: pie.n });
|
|
13775
|
+
if (!layout) continue;
|
|
13776
|
+
if (cat) layout.slices.forEach(sl => { sl.single = values.length === 1; });
|
|
13777
|
+
const sum = values.reduce((a, v) => a + (isNaN(v) ? 0 : v), 0);
|
|
13778
|
+
const r = nSizeOf(sum, size) * unit;
|
|
13779
|
+
if (!(r > 0) || !isFinite(r)) continue;
|
|
13780
|
+
// flat's draw-order key (maptheme.js 16812-16860): with a size
|
|
13781
|
+
// field or AGGREGATE|SUM the item's size, else its first part
|
|
13782
|
+
const key = this.binding.size || (has(/AGGREGATE/) && has(/SUM/)) ? size : values[0];
|
|
13783
|
+
// the tooltip: a row per part (counts), its value the parts' sum
|
|
13784
|
+
// (flat: the sum of nValuesA), its records (theme.item.data, the
|
|
13785
|
+
// page's own {{field}}s from the first, as flat's data[0])
|
|
13786
|
+
const raws = f.properties.raws || (f.properties.raw ? [f.properties.raw] : []);
|
|
13787
|
+
const properties = { counts: values, total: sum, recordCounts: [pie.n], raws, firstRaw: raws[0] || null,
|
|
13788
|
+
titleRaw: f.properties.titleRaw || f.properties.raw || null };
|
|
13789
|
+
charts.push({ c, r, layout, size, count: pie.n, partCounts: pie.counts, nParts: values.length, key, properties });
|
|
13790
|
+
}
|
|
13791
|
+
}
|
|
13792
|
+
for (const f of agg ? [] : this._activeFeatures || this.features) {
|
|
13793
|
+
const c = f.geometry && f.geometry.coordinates;
|
|
13794
|
+
if (!c || typeof c[0] !== 'number') continue;
|
|
13795
|
+
const [lng, lat] = c;
|
|
13796
|
+
if (lng < bbox[0] || lng > bbox[2] || lat < bbox[1] || lat > bbox[3]) continue;
|
|
13797
|
+
if (globeCenter && !isOnVisibleHemisphere(lng, lat, globeCenter)) continue;
|
|
13798
|
+
const raw = f.properties || {};
|
|
13799
|
+
const values = fields.map(k => { const v = flatNumber(raw[k], this.flags); return isNaN(v) ? 0 : v; });
|
|
13800
|
+
const layout = pieSliceLayout(values, t, { centerPart: st.centerpart });
|
|
13801
|
+
if (!layout) continue;
|
|
13802
|
+
const sum = values.reduce((a, v) => a + v, 0);
|
|
13803
|
+
const size = this.binding.size ? parseFloat(raw[this.binding.size]) : undefined;
|
|
13804
|
+
const r = nSizeOf(sum, size) * unit;
|
|
13805
|
+
if (!(r > 0) || !isFinite(r)) continue;
|
|
13806
|
+
// flat's draw-order key (maptheme.js 16812-16860): the size value,
|
|
13807
|
+
// else with SIZE the sum, else the first value
|
|
13808
|
+
const key = this.binding.size ? size : has(/SIZE/) ? sum : values[0];
|
|
13809
|
+
// the tooltip's {{theme.item.chart}}: a row per field (parts), a
|
|
13810
|
+
// 1-field pie its class (cat) like a range-classed bubble
|
|
13811
|
+
const properties = multi ? { value: values[0], parts: values, raw }
|
|
13812
|
+
: { value: values[0], cat: this.partsA ? this._resolvePartsClass(values[0]) : 0, raw };
|
|
13813
|
+
charts.push({ c, r, layout, size, nParts: values.length, key, properties });
|
|
13814
|
+
}
|
|
13815
|
+
// flat sorts the charts before drawing (maptheme.js 16774-16870),
|
|
13816
|
+
// ascending — the last drawn on top —, SORT|UP descending; not with
|
|
13817
|
+
// NOSRT, NOSIZE (unless SORT) or when all values are equal, nor a
|
|
13818
|
+
// CATEGORICAL pie without AGGREGATE, size field or SORT
|
|
13819
|
+
if (((this._valueMin !== this._valueMax) || this.binding.size) && !has(/NOSRT/)
|
|
13820
|
+
&& (!has(/NOSIZE/) || has(/\bSORT\b/))
|
|
13821
|
+
&& (!has(/CATEGORICAL/) || has(/AGGREGATE/) || this.binding.size || has(/\bSORT\b/))) {
|
|
13822
|
+
const down = has(/\bSORT\b/) && has(/\bUP\b/);
|
|
13823
|
+
charts.sort((a, b) => (down ? b.key - a.key : a.key - b.key) || 0);
|
|
13824
|
+
}
|
|
13825
|
+
applyFlatMaxCharts(charts, st);
|
|
13826
|
+
// align / offsetx / offsety (flatChartAlignOffset) — flat's PIE
|
|
13827
|
+
// ptNull is (0, r + 5 units); the chart's half size taken as r, as
|
|
13828
|
+
// for BUBBLE (ASSUMPTION: not measured on flat for PIE)
|
|
13829
|
+
const aligned = !!(st.align || styleNum(st.offsetx) || styleNum(st.offsety));
|
|
13830
|
+
charts.forEach(ch => {
|
|
13831
|
+
ch.off = aligned ? flatChartAlignOffset(st, { unit, symbolScale: 1, half: ch.r, orig: ch.r + 5 * unit }) : [0, 0];
|
|
13832
|
+
});
|
|
13833
|
+
|
|
13834
|
+
const TAU = 2 * Math.PI;
|
|
13835
|
+
const slices = [], centers = [], glows = [];
|
|
13836
|
+
const glowOn = has(/GLOW/) && !has(/ZOOM/) && !glowHiddenByScale(st, liveZoom);
|
|
13837
|
+
for (const ch of charts) {
|
|
13838
|
+
for (const sl of ch.layout.slices) {
|
|
13839
|
+
const rgb = sliceRgb(sl);
|
|
13840
|
+
slices.push({
|
|
13841
|
+
position: ch.c, r: ch.r, off: ch.off, properties: ch.properties, chart: ch, part: sl,
|
|
13842
|
+
slice: [sl.start * Math.PI / 180, sl.sweep >= 360 ? TAU : sl.sweep * Math.PI / 180, ch.layout.inner],
|
|
13843
|
+
fill: [...rgb.slice(0, 3), !sl.complement && noFill.has(sl.i) ? 0 : Math.round(255 * fillAlpha)]
|
|
13844
|
+
});
|
|
13845
|
+
}
|
|
13846
|
+
const center = ch.layout.center;
|
|
13847
|
+
if (center) {
|
|
13848
|
+
const cr = ch.r * Math.sqrt(center.size / 100);
|
|
13849
|
+
ch.centerR = cr;
|
|
13850
|
+
const rgb = colors[center.i] || [128, 128, 128];
|
|
13851
|
+
if (cr > 0) centers.push({ position: ch.c, r: cr * 0.9, off: ch.off, properties: ch.properties, chart: ch, fill: [...rgb.slice(0, 3), Math.round(255 * fillAlpha)] });
|
|
13852
|
+
if (glowOn && cr > 0) {
|
|
13853
|
+
glows.push({ position: ch.c, r: cr * 6, off: ch.off, fill: [...rgb.slice(0, 3), Math.round(255 * 0.05)] });
|
|
13854
|
+
glows.push({ position: ch.c, r: cr * 2, off: ch.off, fill: [...rgb.slice(0, 3), Math.round(255 * 0.1)] });
|
|
13855
|
+
}
|
|
13856
|
+
}
|
|
13857
|
+
}
|
|
13858
|
+
const discProps = {
|
|
13859
|
+
getPosition: d => d.position,
|
|
13860
|
+
getRadius: d => d.r,
|
|
13861
|
+
getPixelOffset: d => d.off,
|
|
13862
|
+
getFillColor: d => d.fill,
|
|
13863
|
+
radiusUnits: 'pixels', billboard: true, stroked: false, filled: true, antialiasing: true,
|
|
13864
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
13865
|
+
};
|
|
13866
|
+
const layers = [new PieSliceLayer(Object.assign({}, discProps, {
|
|
13867
|
+
id: `ix-pie-${this.name}`,
|
|
13868
|
+
data: slices, pickable: true,
|
|
13869
|
+
getLineColor: lineRgba,
|
|
13870
|
+
getSlice: d => d.slice,
|
|
13871
|
+
getSliceLineWidth: d => lineWidthOf(d.r)
|
|
13872
|
+
}))];
|
|
13873
|
+
if (centers.length) {
|
|
13874
|
+
layers.push(new PieSliceLayer(Object.assign({}, discProps, { id: `ix-pie-center-${this.name}`, data: centers, pickable: true })));
|
|
13875
|
+
}
|
|
13876
|
+
if (glows.length) {
|
|
13877
|
+
layers.push(new PieSliceLayer(Object.assign({}, discProps, { id: `ix-pie-glow-${this.name}`, data: glows, pickable: false })));
|
|
13878
|
+
}
|
|
13879
|
+
return layers.concat(this._buildPieTextLayers(charts, zoom, liveZoom, unit, sliceRgb, has));
|
|
13880
|
+
}
|
|
13881
|
+
|
|
13882
|
+
// the items of an AGGREGATE / CATEGORICAL pie within bbox: its records
|
|
13883
|
+
// (_buildAggregationIndex) added up per item by aggregateOnGrid — with
|
|
13884
|
+
// AGGREGATE on the theme's aggregation grid (gridwidth, aggregation …,
|
|
13885
|
+
// see _ensureClusterIndices; none: per position), else per position
|
|
13886
|
+
// (flat sums a CATEGORICAL theme's records of one shape by category
|
|
13887
|
+
// without AGGREGATE too). Each item's properties.pie: flatPieAccumulate's.
|
|
13888
|
+
_pieAggregatedItems(zoom, bbox) {
|
|
13889
|
+
if (!this._featuresByCategory) return [];
|
|
13890
|
+
if (this.flags.has('AGGREGATE')) {
|
|
13891
|
+
this._ensureClusterIndices(zoom);
|
|
13892
|
+
return this._clusterIndices[0].getClusters(bbox, this._clusterUsesFixedZoom ? GRIDWIDTH_METERS_REFERENCE_ZOOM : zoom);
|
|
13893
|
+
}
|
|
13894
|
+
const feats = this._featuresByCategory[0];
|
|
13895
|
+
if (!this._pieExactIndex || this._pieExactIndex.features !== feats) {
|
|
13896
|
+
this._pieExactIndex = new GridAggregateIndex(null, this.flags, undefined, null).load(feats);
|
|
13897
|
+
}
|
|
13898
|
+
return this._pieExactIndex.getClusters(bbox, zoom);
|
|
13899
|
+
}
|
|
13900
|
+
|
|
13901
|
+
// an aggregated pie item's parts (a CATEGORICAL pie with style.values:
|
|
13902
|
+
// one per category) and size (flatPieItemValues)
|
|
13903
|
+
_pieItemValues(pie) {
|
|
13904
|
+
const t = this.flags.typeString != null ? this.flags.typeString : [...this.flags].join('|');
|
|
13905
|
+
const nParts = this._pieCategorical ? (this._pieExplicitValues ? this.categoryLabels.length : null) : (this._pieFields || []).length;
|
|
13906
|
+
return flatPieItemValues(pie, t, { categorical: !!this._pieCategorical, nParts });
|
|
13907
|
+
}
|
|
13908
|
+
|
|
13909
|
+
// flat's statistics of an aggregated pie theme (maptheme.js
|
|
13910
|
+
// 11180-11241), over every item (at this zoom when a grid aggregates):
|
|
13911
|
+
// nMin / nMax over the items' sizes (CATEGORICAL or a size field), else
|
|
13912
|
+
// over their parts; nMaxSize the biggest size; the classes of a
|
|
13913
|
+
// value-field pie (partsA) over the items' parts, as for a per-record
|
|
13914
|
+
// pie (_preparePie). Sets the theme's _valueMin / _valueMax / partsA.
|
|
13915
|
+
_pieAggregateStats(zoom) {
|
|
13916
|
+
const feats = this._featuresByCategory && this._featuresByCategory[0];
|
|
13917
|
+
const zkey = this.flags.has('AGGREGATE') ? zoom : null;
|
|
13918
|
+
const cache = this._pieStatsCache;
|
|
13919
|
+
if (cache && cache.feats === feats && cache.zoom === zkey) return cache;
|
|
13920
|
+
let nMin = Infinity, nMax = -Infinity, maxSize = -Infinity;
|
|
13921
|
+
const pooled = [];
|
|
13922
|
+
const bySize = !!this._pieCategorical || !!(this.binding.size && this.binding.size !== '$item$');
|
|
13923
|
+
for (const f of this._pieAggregatedItems(zoom, [-180, -90, 180, 90])) {
|
|
13924
|
+
const { parts, size } = this._pieItemValues(f.properties.pie);
|
|
13925
|
+
if (isFinite(size) && size > maxSize) maxSize = size;
|
|
13926
|
+
for (const v of parts) if (isFinite(v)) pooled.push(v);
|
|
13927
|
+
if (bySize) {
|
|
13928
|
+
if (isFinite(size)) { if (size < nMin) nMin = size; if (size > nMax) nMax = size; }
|
|
13929
|
+
} else {
|
|
13930
|
+
for (const v of parts) if (isFinite(v)) { if (v < nMin) nMin = v; if (v > nMax) nMax = v; }
|
|
13931
|
+
}
|
|
13932
|
+
}
|
|
13933
|
+
if (nMin <= nMax) {
|
|
13934
|
+
this._valueMin = nMin;
|
|
13935
|
+
this._valueMax = nMax;
|
|
13936
|
+
if (!this._pieCategorical && pooled.length) {
|
|
13937
|
+
const nParts = Math.trunc(styleNum(this.style.classes)) || colorSchemeClassCount(this.style.colorscheme) || DEFAULT_RANGE_CLASSES;
|
|
13938
|
+
this.partsA = flatRangeParts(pooled, nMin, nMax, nParts, this.flags, styleNum(this.style.rangecentervalue));
|
|
13939
|
+
// a 1-field pie's legend rows are these classes (in place, as
|
|
13940
|
+
// _ensureAggregateStats: the legend holds the array)
|
|
13941
|
+
if (!this._multiFields && Array.isArray(this.categoryLabels)) {
|
|
13942
|
+
this.categoryLabels.splice(0, this.categoryLabels.length, ...this.partsA.map(p => `${this._formatTooltipValue(p.min)} - ${this._formatTooltipValue(p.max)}`));
|
|
13943
|
+
}
|
|
13944
|
+
}
|
|
13945
|
+
}
|
|
13946
|
+
this._pieStatsCache = { feats, zoom: zkey, maxSize: maxSize > -Infinity ? maxSize : undefined };
|
|
13947
|
+
return this._pieStatsCache;
|
|
13948
|
+
}
|
|
13949
|
+
|
|
13950
|
+
// the value texts of _buildPieLayers (see there)
|
|
13951
|
+
_buildPieTextLayers(charts, zoom, liveZoom, unit, sliceRgb, has) {
|
|
13952
|
+
const st = this.style;
|
|
13953
|
+
const valueScale = styleNum(st.valuescale) || 1;
|
|
13954
|
+
const units = st.units ? String(st.units) : '';
|
|
13955
|
+
const unitText = units ? (units[0] === '.' ? '' : ' ') + units : '';
|
|
13956
|
+
const partUnit = (has(/AUTOCOMPLETE/) && !/%/.test(unitText) ? ' % ' : '') + unitText;
|
|
13957
|
+
const noBreaks = flatNoBreaks(this._valueMin, this._valueMax);
|
|
13958
|
+
const dec = explicitValueDecimals(st);
|
|
13959
|
+
const fmt = (v, d) => flatFormatValue(v, d, noBreaks ? 'ROUND|NOBREAKS' : 'ROUND');
|
|
13960
|
+
// COUNT on an aggregated pie: the part's record count (flat's nCountA)
|
|
13961
|
+
const countText = has(/COUNT/);
|
|
13962
|
+
const partText = (sl, ch) => {
|
|
13963
|
+
if (sl.complement) return Math.round(sl.percent * 10) / 10 + ' % ';
|
|
13964
|
+
const v = countText && ch && ch.partCounts ? ch.partCounts[sl.i] || 0 : sl.value;
|
|
13965
|
+
return fmt(v, dec != null ? dec : (v < 1 ? 2 : 0)) + partUnit;
|
|
13966
|
+
};
|
|
13967
|
+
const valuesOn = has(/VALUES/) && !has(/STACKED/) && !valuesHiddenByScale(st, zoom);
|
|
13968
|
+
const textOverride = st.valuecolor || st.textcolor;
|
|
13969
|
+
const inline = [], boxed = [], centerTexts = [], lines = [];
|
|
13970
|
+
for (const ch of charts) {
|
|
13971
|
+
const o = ch.off;
|
|
13972
|
+
// CENTER: the center part's value (maptheme.js 20402-20410)
|
|
13973
|
+
if (ch.layout.center && (has(/CENTERVALUE/) || (has(/VALUES/) && !valuesHiddenByScale(st, zoom))) && ch.centerR > 0) {
|
|
13974
|
+
const cv = ch.layout.center.value;
|
|
13975
|
+
const text = fmt(cv, dec != null ? dec : 0) + (unitText.length <= 5 ? unitText : '');
|
|
13976
|
+
const fontSize = Math.min(ch.centerR * 0.8, ch.centerR * (3.3 / Math.max(1, text.length))) * valueScale;
|
|
13977
|
+
const rgb = (this.categoryColorsRgb || [])[ch.layout.center.i] || [128, 128, 128];
|
|
13978
|
+
if (fontSize > VALUES_MIN_FONT_PX) centerTexts.push({ position: ch.c, text, fontSize, off: o, color: flatChartTextRgb(rgb), bold: true });
|
|
13979
|
+
} else if (!ch.layout.center && has(/CENTERVALUE/) && (ch.count || (ch.size !== undefined && !isNaN(ch.size)))) {
|
|
13980
|
+
// flat prints String(nCount || nSize): a record's item has no
|
|
13981
|
+
// nCount, so its size value; an aggregated item its record count
|
|
13982
|
+
const text = String(ch.count || ch.size);
|
|
13983
|
+
const fontSize = Math.min(ch.r * 0.5, ch.r * (2 / Math.max(1, text.length)));
|
|
13984
|
+
if (fontSize > VALUES_MIN_FONT_PX) {
|
|
13985
|
+
centerTexts.push({ position: ch.c, text, fontSize, off: o, bold: false,
|
|
13986
|
+
color: textOverride ? hexOrNamedToRgb(textOverride) : [0x88, 0x88, 0x88] });
|
|
13987
|
+
}
|
|
13988
|
+
}
|
|
13989
|
+
if (!valuesOn) continue;
|
|
13990
|
+
if (ch.nParts > 1 || has(/NOINLINETEXT/)) {
|
|
13991
|
+
const fontSize = 5 * unit * valueScale;
|
|
13992
|
+
if (fontSize <= VALUES_MIN_FONT_PX) continue;
|
|
13993
|
+
for (const lab of pieValueLabelLayout(ch.layout.slices, ch.r, fontSize, unit, (this.partsA || []).length)) {
|
|
13994
|
+
const bg = has(/VALUEBACKGROUND/) ? sliceRgb(lab.slice) : [255, 255, 255];
|
|
13995
|
+
const textRgb = textOverride ? hexOrNamedToRgb(textOverride) : flatDerivateRgb(bg, bg[0] + bg[1] + bg[2] > 450 ? 0.6 : 3);
|
|
13996
|
+
boxed.push({ position: ch.c, text: partText(lab.slice, ch), fontSize, anchor: lab.anchor,
|
|
13997
|
+
// the text's baseline → its center (arial: 0.35 font above)
|
|
13998
|
+
off: [o[0] + lab.x, o[1] + lab.y - 0.35 * fontSize], color: textRgb, bg: [...bg.slice(0, 3), 128] });
|
|
13999
|
+
lab.segments.forEach(sg => lines.push({ c: ch.c, o, sg }));
|
|
14000
|
+
}
|
|
14001
|
+
} else if (ch.layout.slices.length) {
|
|
14002
|
+
const sl = ch.layout.slices[ch.layout.slices.length - 1];
|
|
14003
|
+
const text = partText(sl, ch);
|
|
14004
|
+
const fontSize = Math.min(ch.r * 0.8, ch.r * (3.4 / Math.max(1, text.length)));
|
|
14005
|
+
const color = textOverride ? hexOrNamedToRgb(textOverride) : flatChartTextRgb(sliceRgb(sl));
|
|
14006
|
+
// baseline at −0.6 R + R / 2 + 0.45 font (maptheme.js 20434)
|
|
14007
|
+
if (fontSize > VALUES_MIN_FONT_PX) inline.push({ position: ch.c, text, fontSize, off: [o[0], o[1] - 0.1 * ch.r + 0.45 * fontSize - 0.35 * fontSize], color });
|
|
14008
|
+
}
|
|
14009
|
+
}
|
|
14010
|
+
const layers = [];
|
|
14011
|
+
const textBase = {
|
|
14012
|
+
getPosition: d => d.position, getText: d => d.text, getSize: d => d.fontSize,
|
|
14013
|
+
getPixelOffset: d => d.off, getColor: d => d.color,
|
|
14014
|
+
sizeUnits: 'pixels', fontFamily: 'arial', characterSet: 'auto',
|
|
14015
|
+
getAlignmentBaseline: 'center', pickable: false
|
|
14016
|
+
};
|
|
14017
|
+
if (lines.length) {
|
|
14018
|
+
const feature = d => ({ type: 'Feature', properties: {}, geometry: { type: 'LineString', coordinates: [
|
|
14019
|
+
pixelOffsetLngLat(d.c, [d.o[0] + d.sg[0], d.o[1] + d.sg[1]], liveZoom),
|
|
14020
|
+
pixelOffsetLngLat(d.c, [d.o[0] + d.sg[2], d.o[1] + d.sg[3]], liveZoom)] } });
|
|
14021
|
+
const fc = { type: 'FeatureCollection', features: lines.map(feature) };
|
|
14022
|
+
[['bg', [255, 255, 255, Math.round(255 * 0.2)], 1.5], ['fg', [0x88, 0x88, 0x88, 255], 0.5]].forEach(([k, color, w]) => {
|
|
14023
|
+
layers.push(new GeoJsonLayer({
|
|
14024
|
+
id: `ix-pie-lines-${k}-${this.name}`, data: fc, pickable: false,
|
|
14025
|
+
stroked: true, filled: false, getLineColor: color, getLineWidth: w * unit, lineWidthUnits: 'pixels'
|
|
14026
|
+
}));
|
|
14027
|
+
});
|
|
14028
|
+
}
|
|
14029
|
+
if (boxed.length) {
|
|
14030
|
+
layers.push(new TextLayer(Object.assign({}, textBase, {
|
|
14031
|
+
id: `ix-pie-values-${this.name}`, data: boxed, fontWeight: 'normal',
|
|
14032
|
+
getTextAnchor: d => d.anchor,
|
|
14033
|
+
// flat's box: from 0.3 font left of the text to 0.4 font right of
|
|
14034
|
+
// it, 0.1 font taller (createTextLabel)
|
|
14035
|
+
background: true, getBackgroundColor: d => d.bg, backgroundBorderRadius: 0.1 * 5 * unit * valueScale,
|
|
14036
|
+
backgroundPadding: [0.3, 0, 0.4, 0.1].map(k => k * 5 * unit * valueScale)
|
|
14037
|
+
})));
|
|
14038
|
+
}
|
|
14039
|
+
if (inline.length) {
|
|
14040
|
+
layers.push(new TextLayer(Object.assign({}, textBase, { id: `ix-pie-inline-${this.name}`, data: inline, fontWeight: 'normal', getTextAnchor: 'middle' })));
|
|
14041
|
+
}
|
|
14042
|
+
// CENTER's value in bold; DONUT|CENTERVALUE's in normal weight over
|
|
14043
|
+
// flat's thin black halo (stroke-opacity 0.5)
|
|
14044
|
+
const bold = centerTexts.filter(d => d.bold), plain = centerTexts.filter(d => !d.bold);
|
|
14045
|
+
if (bold.length) {
|
|
14046
|
+
layers.push(new TextLayer(Object.assign({}, textBase, { id: `ix-pie-centervalue-${this.name}`, data: bold, getTextAnchor: 'middle', fontWeight: 'bold' })));
|
|
14047
|
+
}
|
|
14048
|
+
if (plain.length) {
|
|
14049
|
+
layers.push(new TextLayer(Object.assign({}, textBase, {
|
|
14050
|
+
id: `ix-pie-centercount-${this.name}`, data: plain, getTextAnchor: 'middle', fontWeight: 'normal',
|
|
14051
|
+
fontSettings: { sdf: true }, outlineWidth: 1, outlineColor: [0, 0, 0, 128]
|
|
14052
|
+
})));
|
|
14053
|
+
}
|
|
14054
|
+
return layers;
|
|
14055
|
+
}
|
|
14056
|
+
|
|
14057
|
+
// CHART|BAR / BARS (_isBarChart) — flat's bar chart per item
|
|
14058
|
+
// (flatBarLayout, maptheme.js drawChart's BAR branch): its items and
|
|
14059
|
+
// theme statistics as a pie's (one per record, or AGGREGATE /
|
|
14060
|
+
// CATEGORICAL items via _pieAggregatedItems / _pieAggregateStats), a bar
|
|
14061
|
+
// per value field or category:
|
|
14062
|
+
// - size: SIZE by the size field (or the values' sum) with sizepow, the
|
|
14063
|
+
// width chart size / parts · 0.66, the length value · 30 u / nMax
|
|
14064
|
+
// (normalsizevalue, else the theme's max) · rangescale, with SIZE
|
|
14065
|
+
// and several parts / sizer (SIZEP4: sizer²), COMPRESS / EXPAND,
|
|
14066
|
+
// POINTER · 1.2
|
|
14067
|
+
// - HORZ: the bars to the right, the first on top; else upward side
|
|
14068
|
+
// by side; STACKED: one column; SORT, UP, CENTER, SPACED, THIN /
|
|
14069
|
+
// THICK, NOZERO, NONEGATIVE, ONLYNEGATIVE, LEFT
|
|
14070
|
+
// - colors: the part's (categories, fields), a single part or
|
|
14071
|
+
// SEQUENCE the value's class; outline linecolor (black) at
|
|
14072
|
+
// linewidth · 0.25 (0.2 u) and opacity 1 (0.1 without linewidth);
|
|
14073
|
+
// a negative bar fill 0.3, outlined in its color; fillopacity
|
|
14074
|
+
// - POINTER / ARROW (SMALLARROW drawn as ARROW): flat's pointer path,
|
|
14075
|
+
// negative values pointing the other way (fadenegative fill, the
|
|
14076
|
+
// outline lighter or darker), SIZE: its width by the value;
|
|
14077
|
+
// OFFSETMEAN / OFFSETMEDIAN / DEVIATION: the value's deviation from
|
|
14078
|
+
// the part's mean / median (in %) or in standard deviations
|
|
14079
|
+
// - VALUES: the value (valuedecimals, else 1 decimal with a max ≤ 1)
|
|
14080
|
+
// and units, POINTER a "+", STACKED its share; beyond the bar's end
|
|
14081
|
+
// (in a white box unless HORZ; VALUEBACKGROUND / CTEXT: the bar's
|
|
14082
|
+
// color), inside it when it fits (without SIZE, THIN, DTEXT …),
|
|
14083
|
+
// STACKED beside the column on leader lines; flat's values color
|
|
14084
|
+
// #798697 (textcolor), negative red (valuecolor); DTEXT and a sized
|
|
14085
|
+
// POINTER fade small values
|
|
14086
|
+
// - BOX / TITLE (flatChartBox): a box around chart and title, the
|
|
14087
|
+
// item's title above it — boxcolor (white) at boxopacity, bordercolor
|
|
14088
|
+
// (#bbbbbb) at 0.3, borderwidth · 0.2 · w / 30; title 5 u ·
|
|
14089
|
+
// textscale in textcolor (#888888) over a white halo
|
|
14090
|
+
// - draw order: flat's (_buildPieLayers); each chart's box under its
|
|
14091
|
+
// bars, a later chart over an earlier one
|
|
14092
|
+
// Not drawn (BAR_INERT_FLAGS, a console note): see flatBarLayout.
|
|
14093
|
+
// Known divergence: every value text lies above every bar (flat draws
|
|
14094
|
+
// them in the chart's own group); the leader lines are drawn in map
|
|
14095
|
+
// coordinates (approximate on the globe).
|
|
14096
|
+
_buildBarLayers(zoom, bbox, liveZoom = zoom, globeCenter = null) {
|
|
14097
|
+
const st = this.style;
|
|
14098
|
+
const t = this.flags.typeString != null ? this.flags.typeString : [...this.flags].join('|');
|
|
14099
|
+
const has = re => re.test(t);
|
|
14100
|
+
const agg = this._isAggregatedPie();
|
|
14101
|
+
const cat = !!this._pieCategorical;
|
|
14102
|
+
const fields = cat ? null : (this._pieFields || String(this.binding.value || '').split('|'));
|
|
14103
|
+
const stats = agg ? this._pieAggregateStats(zoom) : null;
|
|
14104
|
+
const unit = objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(st.scale) || 1);
|
|
14105
|
+
const sizeField = !!(this.binding.size && this.binding.size !== '$item$');
|
|
14106
|
+
const nsv = styleNum(st.normalsizevalue);
|
|
14107
|
+
const colors = this.categoryColorsRgb || [];
|
|
14108
|
+
const num = v => { const x = styleNum(v); return isNaN(x) ? undefined : x; };
|
|
14109
|
+
const measure = (text, font) => titleTextWidth(text, font);
|
|
14110
|
+
// the charts' values (and, for OFFSETMEAN …, every item's)
|
|
14111
|
+
const items = [];
|
|
14112
|
+
const itemValues = f => {
|
|
14113
|
+
if (agg) {
|
|
14114
|
+
const { parts, size } = this._pieItemValues(f.properties.pie);
|
|
14115
|
+
return { values: parts, size };
|
|
14116
|
+
}
|
|
14117
|
+
const raw = f.properties || {};
|
|
14118
|
+
return { values: fields.map(k => flatNumber(raw[k], this.flags)), size: sizeField ? parseFloat(raw[this.binding.size]) : undefined };
|
|
14119
|
+
};
|
|
14120
|
+
const all = agg ? this._pieAggregatedItems(zoom, [-180, -90, 180, 90]) : (this._activeFeatures || this.features);
|
|
14121
|
+
// flat's per-part mean, median (getMeanMedianQuantile) and standard
|
|
14122
|
+
// deviation over every item
|
|
14123
|
+
let means, medians, devs;
|
|
14124
|
+
if (has(/OFFSETMEAN|OFFSETMEDIAN|DEVIATION/)) {
|
|
14125
|
+
const cache = this._barStatsCache;
|
|
14126
|
+
if (cache && cache.all === all && (!agg || cache.zoom === zoom)) ({ means, medians, devs } = cache);
|
|
14127
|
+
else {
|
|
14128
|
+
const cols = [];
|
|
14129
|
+
for (const f of all) itemValues(f).values.forEach((v, k) => { (cols[k] ||= []).push(v); });
|
|
14130
|
+
means = cols.map(c => c.reduce((a, v) => a + v, 0) / c.length);
|
|
14131
|
+
medians = cols.map(c => c.slice().sort((a, b) => a - b)[Math.round(c.length / 2)]);
|
|
14132
|
+
devs = cols.map((c, k) => Math.sqrt(c.reduce((a, v) => a + (v - means[k]) * (v - means[k]), 0) / c.length));
|
|
14133
|
+
this._barStatsCache = { all, zoom, means, medians, devs };
|
|
14134
|
+
}
|
|
14135
|
+
}
|
|
14136
|
+
const lo = {
|
|
14137
|
+
themeMin: this._valueMin, themeMax: this._valueMax,
|
|
14138
|
+
maxSize: stats ? stats.maxSize : this._maxSizeValue, sizeField,
|
|
14139
|
+
normalSizeValue: nsv > 0 ? nsv : undefined, sizePow: resolveSizePow(st, this.flags),
|
|
14140
|
+
rangeScale: num(st.rangescale), valueScale: num(st.valuescale), units: st.units ? String(st.units) : '',
|
|
14141
|
+
valueDecimals: explicitValueDecimals(st), noBreaks: flatNoBreaks(this._valueMin, this._valueMax),
|
|
14142
|
+
gridX: num(st.gridx), nClasses: (this.partsA || []).length, classMax: (this.partsA || []).map(p => p.max),
|
|
14143
|
+
nColors: colors.length, fillOpacity: num(st.fillopacity), lineWidth: num(st.linewidth),
|
|
14144
|
+
fadeNegative: num(st.fadenegative), fadeValuePow: num(st.fadevaluepow),
|
|
14145
|
+
textColor: st.textcolor || undefined, valueColor: st.valuecolor || undefined,
|
|
14146
|
+
hideValues: valuesHiddenByScale(st, zoom), means, medians, devs, measure
|
|
14147
|
+
};
|
|
14148
|
+
const inView = c => c[0] >= bbox[0] && c[0] <= bbox[2] && c[1] >= bbox[1] && c[1] <= bbox[3]
|
|
14149
|
+
&& (!globeCenter || isOnVisibleHemisphere(c[0], c[1], globeCenter));
|
|
14150
|
+
const charts = [];
|
|
14151
|
+
for (const f of agg ? this._pieAggregatedItems(zoom, bbox) : all) {
|
|
14152
|
+
const c = f.geometry && f.geometry.coordinates;
|
|
14153
|
+
if (!c || typeof c[0] !== 'number' || !inView(c)) continue;
|
|
14154
|
+
const { values, size } = itemValues(f);
|
|
14155
|
+
const layout = flatBarLayout(values, t, Object.assign({ itemSize: size }, lo));
|
|
14156
|
+
if (!layout || (!layout.bars.length && !layout.texts.length)) continue;
|
|
14157
|
+
const sum = values.reduce((a, v) => a + (isFinite(v) ? v : 0), 0);
|
|
14158
|
+
let properties;
|
|
14159
|
+
if (agg) {
|
|
14160
|
+
const pie = f.properties.pie;
|
|
14161
|
+
const raws = f.properties.raws || (f.properties.raw ? [f.properties.raw] : []);
|
|
14162
|
+
properties = { counts: values, total: sum, recordCounts: [pie.n], raws, firstRaw: raws[0] || null,
|
|
14163
|
+
titleRaw: f.properties.titleRaw || f.properties.raw || null };
|
|
14164
|
+
} else {
|
|
14165
|
+
const raw = f.properties || {};
|
|
14166
|
+
properties = values.length > 1 ? { value: values[0], parts: values, raw }
|
|
14167
|
+
: { value: values[0], cat: this.partsA ? this._resolvePartsClass(values[0]) : 0, raw };
|
|
14168
|
+
}
|
|
14169
|
+
// flat's draw-order key (as for pies, maptheme.js 16812-16860)
|
|
14170
|
+
const key = agg ? (this.binding.size || (has(/AGGREGATE/) && has(/SUM/)) ? size : values[0])
|
|
14171
|
+
: (this.binding.size ? size : has(/SIZE/) ? sum : values[0]);
|
|
14172
|
+
charts.push({ c, layout, key, properties });
|
|
14173
|
+
}
|
|
14174
|
+
if (((this._valueMin !== this._valueMax) || this.binding.size) && !has(/NOSRT/)
|
|
14175
|
+
&& (!has(/NOSIZE/) || has(/\bSORT\b/))
|
|
14176
|
+
&& (!has(/CATEGORICAL/) || has(/AGGREGATE/) || this.binding.size || has(/\bSORT\b/))) {
|
|
14177
|
+
const down = has(/\bSORT\b/) && has(/\bUP\b/);
|
|
14178
|
+
charts.sort((a, b) => (down ? b.key - a.key : a.key - b.key) || 0);
|
|
14179
|
+
}
|
|
14180
|
+
applyFlatMaxCharts(charts, st);
|
|
14181
|
+
|
|
14182
|
+
const boxShown = flatFlag(this.flags, 'BOX') && !boxHiddenByScale(st, liveZoom);
|
|
14183
|
+
const titleShown = boxShown && flatFlag(this.flags, 'TITLE') && !!this.binding.title;
|
|
14184
|
+
const titleFont = 5 * (num(st.textscale) || num(st.valuescale) || 1);
|
|
14185
|
+
const boxOpacity = isNaN(styleNum(st.boxopacity)) ? 1 : styleNum(st.boxopacity);
|
|
14186
|
+
const borderStyle = String(st.borderstyle || '');
|
|
14187
|
+
const boxFill = [...hexOrNamedToRgb(st.boxcolor || 'white').slice(0, 3), Math.round(255 * boxOpacity)];
|
|
14188
|
+
const boxLine = borderStyle === 'none' ? [0, 0, 0, 0]
|
|
14189
|
+
: [...hexOrNamedToRgb(styleLineColor(st.bordercolor) || '#bbbbbb').slice(0, 3), Math.round(255 * (/^(solid|dotted|dashed)$/.test(borderStyle) ? 1 : 0.3))];
|
|
14190
|
+
const borderWidth = num(st.borderwidth) || 1;
|
|
14191
|
+
const lc = st.linecolor != null && st.linecolor !== '' ? styleLineColor(st.linecolor) : null;
|
|
14192
|
+
const lineRgbOf = (spec, rgb) => (spec.rgb === 'color' ? rgb : spec.rgb === 'low' ? flatDerivateRgb(rgb, 0.7)
|
|
14193
|
+
: spec.rgb === 'high' ? flatDerivateRgb(rgb, 1.3) : (lc && lc !== 'none' ? hexOrNamedToRgb(lc) : [0, 0, 0]));
|
|
14194
|
+
const align = !!(st.align || styleNum(st.offsetx) || styleNum(st.offsety));
|
|
14195
|
+
|
|
14196
|
+
const shapes = [], texts = [], titles = [], lines = [];
|
|
14197
|
+
// a shape in chart units → the instance (px; the circle around it)
|
|
14198
|
+
const shape = (ch, s, fill, line, width, props, noPick) => {
|
|
14199
|
+
const L = s.len + s.band + s.head;
|
|
14200
|
+
const d = [[0, -1], [1, 0], [0, 1], [-1, 0]][s.dir];
|
|
14201
|
+
const cx = (s.base[0] + d[0] * L / 2) * unit + ch.off[0], cy = (s.base[1] + d[1] * L / 2) * unit + ch.off[1];
|
|
14202
|
+
const lw = line[3] > 0 ? width * unit : 0;
|
|
14203
|
+
shapes.push({
|
|
14204
|
+
position: ch.c, off: [cx, -cy], properties: props, fill, line,
|
|
14205
|
+
r: Math.hypot(s.w2 * unit, L * unit / 2) + lw + 2,
|
|
14206
|
+
shape: [s.w2 * unit, s.sw * unit, s.len * unit, s.band * unit],
|
|
14207
|
+
shape2: [s.head * unit, s.dir + (noPick ? 4 : 0), lw, L * unit / 2]
|
|
14208
|
+
});
|
|
14209
|
+
};
|
|
14210
|
+
for (const ch of charts) {
|
|
14211
|
+
const L = ch.layout;
|
|
14212
|
+
let box = null;
|
|
14213
|
+
if (boxShown) {
|
|
14214
|
+
const title = titleShown ? this._itemTitle(ch.properties) : '';
|
|
14215
|
+
box = flatChartBox(L.extent, { margin: num(st.boxmargin) || 2, titleFont, title: title ? title + (st.titleunits || '') : '',
|
|
14216
|
+
bottomTitle: flatFlag(this.flags, 'BOTTOMTITLE'), alignRight: /right/.test(String(st.align || '')), measure });
|
|
14217
|
+
}
|
|
14218
|
+
const ext = box ? box.rect : L.extent;
|
|
14219
|
+
// align / offsetx / offsety (flatChartAlignOffset), the chart's
|
|
14220
|
+
// half height as its size (ASSUMPTION: not measured on flat for BAR)
|
|
14221
|
+
ch.off = align ? flatChartAlignOffset(st, { unit, symbolScale: 1, half: (ext[3] - ext[1]) / 2 * unit }) : [0, 0];
|
|
14222
|
+
if (box) {
|
|
14223
|
+
const r = box.rect;
|
|
14224
|
+
shape(ch, { base: [(r[0] + r[2]) / 2, r[3]], dir: 0, len: r[3] - r[1], w2: (r[2] - r[0]) / 2, sw: (r[2] - r[0]) / 2, band: 0, head: 0 },
|
|
14225
|
+
boxFill, boxLine, borderWidth * 0.2 * box.width / 30, null, true);
|
|
14226
|
+
if (box.title) titles.push({ position: ch.c, text: box.title.lines.join('\n'), font: box.title.font * unit, anchor: box.title.anchor,
|
|
14227
|
+
off: [box.title.x * unit + ch.off[0], (box.title.baseline - 0.8 * box.title.font) * unit + ch.off[1]] });
|
|
14228
|
+
}
|
|
14229
|
+
for (const b of L.bars) {
|
|
14230
|
+
const rgb = colors[b.cls] || [128, 128, 128];
|
|
14231
|
+
const a = Math.round(255 * b.fill.opacity * b.opacity);
|
|
14232
|
+
const line = [...lineRgbOf(b.line, rgb).slice(0, 3), Math.round(255 * b.line.opacity * b.opacity)];
|
|
14233
|
+
shape(ch, b, [...rgb.slice(0, 3), a], line, b.line.width, ch.properties, false);
|
|
14234
|
+
}
|
|
14235
|
+
for (const tx of L.texts) {
|
|
14236
|
+
const font = tx.font * unit;
|
|
14237
|
+
if (font <= VALUES_MIN_FONT_PX) continue;
|
|
14238
|
+
const rgb = colors[tx.cls] || [128, 128, 128];
|
|
14239
|
+
const color = tx.color === 'value' ? [0x79, 0x86, 0x97] : tx.color === 'contrast' ? flatChartTextRgb(rgb) : hexOrNamedToRgb(tx.color);
|
|
14240
|
+
// the baseline's start → the text's middle-left (arial: 0.35 font
|
|
14241
|
+
// above the baseline), perpendicular to its reading direction
|
|
14242
|
+
const r = tx.angle * Math.PI / 180;
|
|
14243
|
+
const mx = tx.x * unit - 0.35 * font * Math.sin(r), my = tx.y * unit - 0.35 * font * Math.cos(r);
|
|
14244
|
+
texts.push({ position: ch.c, text: tx.text, font, angle: tx.angle, off: [mx + ch.off[0], my + ch.off[1]],
|
|
14245
|
+
color: [...color.slice(0, 3), Math.round(255 * Math.max(0, Math.min(1, tx.opacity)))],
|
|
14246
|
+
bg: tx.bg === 'bar' ? [...rgb.slice(0, 3), 128] : tx.bg === 'white' ? [254, 254, 255, 255] : null });
|
|
14247
|
+
}
|
|
14248
|
+
for (const l of L.lines) lines.push({ c: ch.c, o: ch.off, sg: [l.x0 * unit, l.y0 * unit, l.x1 * unit, l.y1 * unit] });
|
|
14249
|
+
}
|
|
14250
|
+
|
|
14251
|
+
const layers = [new BarShapeLayer({
|
|
14252
|
+
id: `ix-bar-${this.name}`,
|
|
14253
|
+
data: shapes, pickable: true,
|
|
14254
|
+
getPosition: d => d.position,
|
|
14255
|
+
getRadius: d => d.r,
|
|
14256
|
+
getPixelOffset: d => d.off,
|
|
14257
|
+
getFillColor: d => d.fill,
|
|
14258
|
+
getLineColor: d => d.line,
|
|
14259
|
+
getBarShape: d => d.shape,
|
|
14260
|
+
getBarShape2: d => d.shape2,
|
|
14261
|
+
radiusUnits: 'pixels', billboard: true, stroked: false, filled: true, antialiasing: true,
|
|
14262
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14263
|
+
})];
|
|
14264
|
+
if (lines.length) {
|
|
14265
|
+
const feature = d => ({ type: 'Feature', properties: {}, geometry: { type: 'LineString', coordinates: [
|
|
14266
|
+
pixelOffsetLngLat(d.c, [d.o[0] + d.sg[0], d.o[1] + d.sg[1]], liveZoom),
|
|
14267
|
+
pixelOffsetLngLat(d.c, [d.o[0] + d.sg[2], d.o[1] + d.sg[3]], liveZoom)] } });
|
|
14268
|
+
const fc = { type: 'FeatureCollection', features: lines.map(feature) };
|
|
14269
|
+
[['bg', [255, 255, 255, Math.round(255 * 0.2)], 0.15], ['fg', [0x88, 0x88, 0x88, 255], 0.05]].forEach(([k, color, w]) => {
|
|
14270
|
+
layers.push(new GeoJsonLayer({
|
|
14271
|
+
id: `ix-bar-lines-${k}-${this.name}`, data: fc, pickable: false,
|
|
14272
|
+
stroked: true, filled: false, getLineColor: color, getLineWidth: w * unit, lineWidthUnits: 'pixels'
|
|
14273
|
+
}));
|
|
14274
|
+
});
|
|
14275
|
+
}
|
|
14276
|
+
const textBase = {
|
|
14277
|
+
getPosition: d => d.position, getText: d => d.text, getSize: d => d.font,
|
|
14278
|
+
getPixelOffset: d => d.off, getColor: d => d.color, getAngle: d => d.angle,
|
|
14279
|
+
sizeUnits: 'pixels', fontFamily: 'arial', characterSet: 'auto', fontWeight: 'normal',
|
|
14280
|
+
getTextAnchor: 'start', getAlignmentBaseline: 'center', pickable: false,
|
|
14281
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14282
|
+
};
|
|
14283
|
+
const boxed = texts.filter(d => d.bg), plain = texts.filter(d => !d.bg);
|
|
14284
|
+
if (plain.length) layers.push(new TextLayer(Object.assign({}, textBase, { id: `ix-bar-values-${this.name}`, data: plain })));
|
|
14285
|
+
if (boxed.length) {
|
|
14286
|
+
// flat's box (createTextLabel): 0.3 font left of the text, 0.4 font
|
|
14287
|
+
// right of it, 0.1 font taller
|
|
14288
|
+
layers.push(new TextLayer(Object.assign({}, textBase, {
|
|
14289
|
+
id: `ix-bar-values-boxed-${this.name}`, data: boxed,
|
|
14290
|
+
background: true, getBackgroundColor: d => d.bg,
|
|
14291
|
+
// one padding per layer: of the texts' mean font
|
|
14292
|
+
backgroundPadding: [0.3, 0, 0.4, 0.1].map(k => k * boxed.reduce((a, d) => a + d.font, 0) / boxed.length)
|
|
14293
|
+
})));
|
|
14294
|
+
}
|
|
14295
|
+
if (titles.length) {
|
|
14296
|
+
layers.push(new TextLayer({
|
|
14297
|
+
id: `ix-bar-titles-${this.name}`, data: titles, pickable: false,
|
|
14298
|
+
getPosition: d => d.position, getText: d => d.text, getSize: d => d.font,
|
|
14299
|
+
sizeUnits: 'pixels', fontFamily: 'arial', characterSet: 'auto', lineHeight: 1,
|
|
14300
|
+
getColor: [...hexOrNamedToRgb(st.textcolor || '#888888').slice(0, 3), 255],
|
|
14301
|
+
fontSettings: { sdf: true }, outlineWidth: 1 / 7, outlineColor: [255, 255, 255, 128],
|
|
14302
|
+
getTextAnchor: d => d.anchor, getAlignmentBaseline: 'top',
|
|
14303
|
+
getPixelOffset: d => d.off,
|
|
14304
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14305
|
+
}));
|
|
14306
|
+
}
|
|
14307
|
+
return layers;
|
|
14308
|
+
}
|
|
14309
|
+
|
|
14310
|
+
// CHART|VECTOR / CHART|BEZIER — the theme's classes as flat has them:
|
|
14311
|
+
// with CATEGORICAL (or EXACT) the categories, flat's string values
|
|
14312
|
+
// trimmed (getStringValueIndex, maptheme.js 9204), its partsA one
|
|
14313
|
+
// [k, k + 1e-9] per category (the last + 0.001) and the colorscheme's
|
|
14314
|
+
// colors per category; the items' statistics follow in _vectorItems
|
|
14315
|
+
_prepareVector() {
|
|
14316
|
+
const st = this.style;
|
|
14317
|
+
this._vectorCategorical = this.flags.has('CATEGORICAL') && !!this.binding.value;
|
|
14318
|
+
if (this._vectorCategorical) {
|
|
14319
|
+
const explicit = Array.isArray(st.values) ? st.values.map(v => String(v).trim()) : null;
|
|
14320
|
+
const index = new Map();
|
|
14321
|
+
if (explicit) explicit.forEach((v, i) => { if (!index.has(v)) index.set(v, i); });
|
|
14322
|
+
else {
|
|
14323
|
+
for (const f of this.features) {
|
|
14324
|
+
const v = f.properties[this.binding.value];
|
|
14325
|
+
if (v == null) continue;
|
|
14326
|
+
const k = String(v).trim();
|
|
14327
|
+
if (k !== '' && !index.has(k)) index.set(k, index.size);
|
|
14328
|
+
}
|
|
14329
|
+
this.categoryLabels = Array.from(index.keys());
|
|
14330
|
+
const explicitLabel = Array.isArray(st.label) ? st.label.map(String) : null;
|
|
14331
|
+
this.categoryDisplayLabels = explicitLabel && explicitLabel.length === this.categoryLabels.length ? explicitLabel : this.categoryLabels;
|
|
14332
|
+
this.categoryColorsRgb = resolveClassColors(st.colorscheme, this.categoryDisplayLabels, st.classes);
|
|
14333
|
+
}
|
|
14334
|
+
this._vectorCategoryIndex = index;
|
|
14335
|
+
// the legend counts records by their raw value
|
|
14336
|
+
const byLabel = new Map(index);
|
|
14337
|
+
for (const f of this.features) {
|
|
14338
|
+
const v = f.properties[this.binding.value];
|
|
14339
|
+
if (v != null && !byLabel.has(v) && index.has(String(v).trim())) byLabel.set(v, index.get(String(v).trim()));
|
|
14340
|
+
}
|
|
14341
|
+
this.categoryIndexByLabel = byLabel;
|
|
14342
|
+
const n = this.categoryLabels.length;
|
|
14343
|
+
this.partsA = this.categoryLabels.map((l, i) => ({ min: i + 1, max: i + 1 + (i === n - 1 ? 0.001 : 0) + 1e-9 }));
|
|
14344
|
+
this._rangeClassed = false;
|
|
14345
|
+
}
|
|
14346
|
+
// colorfield (flat 12529-12597, 12644-12652): an item's color is its
|
|
14347
|
+
// color value's — the colorvalues, else values, else the items'
|
|
14348
|
+
// values in order — n-th color of the colorscheme realized for n
|
|
14349
|
+
const colorField = st.colorfield || (this.binding && this.binding.color);
|
|
14350
|
+
this._vectorColorField = colorField || null;
|
|
14351
|
+
if (colorField) {
|
|
14352
|
+
const list = Array.isArray(st.colorvalues) ? st.colorvalues.map(String)
|
|
14353
|
+
: Array.isArray(st.values) ? st.values.map(String)
|
|
14354
|
+
: [...new Set(this.features.map(f => String(f.properties[colorField])))];
|
|
14355
|
+
const colors = resolveClassColors(st.colorscheme, list, st.classes) || [];
|
|
14356
|
+
this._vectorColorOf = new Map(list.map((v, i) => [v, colors[i]]));
|
|
14357
|
+
}
|
|
14358
|
+
this._vectorItemsOf = null;
|
|
14359
|
+
}
|
|
14360
|
+
|
|
14361
|
+
// the records of flatVectorItems from this theme's (active) features:
|
|
14362
|
+
// the second position joined by key (joinChartPositions: f._pos2) or
|
|
14363
|
+
// read from lat/lon fields ("lat|lon")
|
|
14364
|
+
_vectorRecords(features) {
|
|
14365
|
+
const b = this.binding;
|
|
14366
|
+
const l2 = b.lookup2 != null ? String(b.lookup2) : '';
|
|
14367
|
+
const latlon2 = l2.includes('|') ? l2.split('|') : null;
|
|
14368
|
+
const sizeField = b.size && b.size !== '$item$' ? b.size : null;
|
|
14369
|
+
const records = [];
|
|
14370
|
+
for (const f of features) {
|
|
14371
|
+
const c = f.geometry && f.geometry.coordinates;
|
|
14372
|
+
if (!c || typeof c[0] !== 'number') continue;
|
|
14373
|
+
const p = f.properties || {};
|
|
14374
|
+
let p2 = null;
|
|
14375
|
+
if (latlon2) {
|
|
14376
|
+
const lat = flatScanValue(p[latlon2[0]]), lng = flatScanValue(p[latlon2[1]]);
|
|
14377
|
+
if (isFinite(lat) && isFinite(lng)) p2 = [lng, lat];
|
|
14378
|
+
} else if (f._pos2) p2 = f._pos2;
|
|
14379
|
+
let cat = null, value;
|
|
14380
|
+
if (this._vectorCategorical) {
|
|
14381
|
+
const v = p[b.value];
|
|
14382
|
+
const i = v == null ? undefined : this._vectorCategoryIndex.get(String(v).trim());
|
|
14383
|
+
cat = i == null ? -1 : i;
|
|
14384
|
+
} else if (b.value) {
|
|
14385
|
+
value = flatScanValue(p[b.value]);
|
|
14386
|
+
}
|
|
14387
|
+
records.push({ p1: c, p2, cat, value, size: sizeField ? flatScanValue(p[sizeField]) : 1, raw: p });
|
|
14388
|
+
}
|
|
14389
|
+
return records;
|
|
14390
|
+
}
|
|
14391
|
+
|
|
14392
|
+
// the items (flatVectorItems) of the active features, once per feature
|
|
14393
|
+
// set; also flat's statistics of them (maptheme.js 11180-11240): nMin /
|
|
14394
|
+
// nMax over the items' sizes (CATEGORICAL or a size field, else their
|
|
14395
|
+
// values), nMaxSize the biggest size
|
|
14396
|
+
_vectorItems() {
|
|
14397
|
+
const features = this._activeFeatures || this.features;
|
|
14398
|
+
if (this._vectorItemsOf === features) return this._vectorItemsCache;
|
|
14399
|
+
const t = this.flags.typeString != null ? this.flags.typeString : [...this.flags].join('|');
|
|
14400
|
+
const items = flatVectorItems(this._vectorRecords(features), {
|
|
14401
|
+
aggregate: this.flags.has('AGGREGATE'),
|
|
14402
|
+
vectorKey: /VECTOR/.test(t),
|
|
14403
|
+
multiParts: this.flags.has('CATEGORICAL'),
|
|
14404
|
+
max: /\bMAX\b/.test(t), min: /\bMIN\b/.test(t),
|
|
14405
|
+
zeroIsNotValue: /ZEROISNOTVALUE/.test(t), negativeIsNotValue: /NEGATIVEISNOTVALUE/.test(t)
|
|
14406
|
+
});
|
|
14407
|
+
let min = Infinity, max = -Infinity, maxSize = -Infinity;
|
|
14408
|
+
const bySize = this._vectorCategorical || !!this.binding.size;
|
|
14409
|
+
for (const it of items) {
|
|
14410
|
+
if (isFinite(it.nSize) && it.nSize > maxSize) maxSize = it.nSize;
|
|
14411
|
+
const v = bySize ? it.nSize : it.value;
|
|
14412
|
+
if (isFinite(v)) { if (v < min) min = v; if (v > max) max = v; }
|
|
14413
|
+
}
|
|
14414
|
+
if (min <= max) { this._valueMin = min; this._valueMax = max; }
|
|
14415
|
+
this._vectorMaxSize = maxSize;
|
|
14416
|
+
this._vectorItemsOf = features;
|
|
14417
|
+
this._vectorItemsCache = items;
|
|
14418
|
+
return items;
|
|
14419
|
+
}
|
|
14420
|
+
|
|
14421
|
+
// CHART|VECTOR / CHART|BEZIER — real ixmaps-flat's flow lines
|
|
14422
|
+
// (maptheme.js chartMap 18148-18435): one line per item
|
|
14423
|
+
// (_vectorItems) from its first to its second position — the
|
|
14424
|
+
// positions joined to FEATURE shapes by key (binding position /
|
|
14425
|
+
// position2, style lookupfield / lookupfield2) or read from lat/lon
|
|
14426
|
+
// fields. An item without a second position, or with a size under
|
|
14427
|
+
// style.minvalue, draws nothing; one is drawn when either end lies in
|
|
14428
|
+
// the view (flat 16590-16620).
|
|
14429
|
+
// - BEZIER (also VECTOR|BEZIER — the BEZIER branch comes first): a
|
|
14430
|
+
// cubic curve (bezierVectorLayout), width (linewidth, else 10 ·
|
|
14431
|
+
// (|nSize| / normalsizevalue or the biggest size)^(1/sizepow)) ·
|
|
14432
|
+
// unit px — unit: the object zoom × style.scale; a width ≤ 0 is not
|
|
14433
|
+
// drawn; stroke opacity fillopacity (else 0.3); a negative size or
|
|
14434
|
+
// REVERSE swaps the ends; RANDOM lowers the bow by up to 0.66 ·
|
|
14435
|
+
// rangescale (flat's Math.random, here a fixed hash of the item);
|
|
14436
|
+
// POINTER / ARROW an arrow head (arrowMarkerTriangle) at the
|
|
14437
|
+
// shortened end, its opacity 2 · fillopacity; GRADIENT / FADEIN a
|
|
14438
|
+
// gradient (fadeGradientStops) from the second color (linecolor's
|
|
14439
|
+
// first entry) at the start — FADEIN at 0.2 opacity — to the line
|
|
14440
|
+
// color at the end; DOPACITY (DOPACITYMAX) / DOPACITYMIN the line
|
|
14441
|
+
// and arrow opacity by the size within nMin … nMax (dopacitypow,
|
|
14442
|
+
// dopacityscale); DASH dashes of 50 / 5 + width units, round caps
|
|
14443
|
+
// - VECTOR alone: a straight line, width (linewidth, else 2 · nSize /
|
|
14444
|
+
// normalsizevalue / the biggest size, else 0.1) · the object zoom px
|
|
14445
|
+
// (no style.scale), opacity fillopacity (0.3); POINTER / ARROW a 4 ×
|
|
14446
|
+
// width arrow head with its tip on the second position; DASH dashes
|
|
14447
|
+
// of 50 / 15 units
|
|
14448
|
+
// - color: linecolor (its last entry), else the colorfield's color,
|
|
14449
|
+
// else the category's (CATEGORICAL) or the value's n-th color; a
|
|
14450
|
+
// record whose category is none of them is black (flat: an
|
|
14451
|
+
// "undefined" stroke — szNoDataColor is unset)
|
|
14452
|
+
// - draw order: flat's sort before drawing (16774-16880), the
|
|
14453
|
+
// biggest last (on top) when sized or AGGREGATE|SUM
|
|
14454
|
+
// - lines are densified in Web Mercator pixels (flat draws them on its
|
|
14455
|
+
// Mercator map), so under the globe projection they follow the
|
|
14456
|
+
// surface; their far-side parts are left out
|
|
14457
|
+
// Not drawn here (console note at type parsing, VECTOR_INERT_FLAGS):
|
|
14458
|
+
// VALUES (text along the curve), DYNAMICWIDTH, LONG, the DASH
|
|
14459
|
+
// animation; nor the second position from the category label (flat's
|
|
14460
|
+
// fallback without position2), a grid aggregation (gridwidth), the
|
|
14461
|
+
// VECTOR arrow's thin white outline, the legend's class marking.
|
|
14462
|
+
// Known divergence: all arrow heads lie above all lines (flat draws
|
|
14463
|
+
// each line with its head); a line thinner than a pixel is drawn one
|
|
14464
|
+
// pixel wide at its width as opacity (SVG's coverage).
|
|
14465
|
+
_buildVectorLayers(zoom, bbox, liveZoom = zoom, globeCenter = null) {
|
|
14466
|
+
const st = this.style;
|
|
14467
|
+
const t = this.flags.typeString != null ? this.flags.typeString : [...this.flags].join('|');
|
|
14468
|
+
const has = re => re.test(t);
|
|
14469
|
+
const bezier = has(/\bBEZIER\b/);
|
|
14470
|
+
if (!this.binding.lookup2) {
|
|
14471
|
+
if (!this._warnedNoPos2) {
|
|
14472
|
+
this._warnedNoPos2 = true;
|
|
14473
|
+
console.info(`[ixmaps-gl] layer "${this.name}": a VECTOR/BEZIER chart without position2 (lookupfield2) — flat's second position from the category label is not ported, nothing drawn`);
|
|
14474
|
+
}
|
|
14475
|
+
return [];
|
|
14476
|
+
}
|
|
14477
|
+
const items = this._vectorItems();
|
|
14478
|
+
const oz = objectZoomFactor(liveZoom, this.mapOptions);
|
|
14479
|
+
const scale = styleNum(st.scale) || 1;
|
|
14480
|
+
const unit = oz * scale;
|
|
14481
|
+
const world = 512 * Math.pow(2, liveZoom);
|
|
14482
|
+
const toPx = ([lng, lat]) => {
|
|
14483
|
+
const s = Math.sin(Math.max(-85.0511, Math.min(85.0511, lat)) * Math.PI / 180);
|
|
14484
|
+
return [(lng + 180) / 360 * world, (0.5 - Math.log((1 + s) / (1 - s)) / (4 * Math.PI)) * world];
|
|
14485
|
+
};
|
|
14486
|
+
const toLngLat = ([x, y]) => [x / world * 360 - 180, (2 * Math.atan(Math.exp((0.5 - y / world) * 2 * Math.PI)) - Math.PI / 2) * 180 / Math.PI];
|
|
14487
|
+
const inBox = c => c && c[0] >= bbox[0] && c[0] <= bbox[2] && c[1] >= bbox[1] && c[1] <= bbox[3];
|
|
14488
|
+
|
|
14489
|
+
const sizePow = resolveSizePow(st, this.flags);
|
|
14490
|
+
const nsv = styleNum(st.normalsizevalue);
|
|
14491
|
+
const maxSize = this._vectorMaxSize;
|
|
14492
|
+
// style.linewidth as flat holds it: the raw style value — flat's
|
|
14493
|
+
// `nLineWidth + 3` (the arrow length) joins a string ("0.6" + 3 =
|
|
14494
|
+
// "0.63") but adds a number
|
|
14495
|
+
const rawLw = this._definition && this._definition.style ? this._definition.style.linewidth : st.linewidth;
|
|
14496
|
+
const lwNum = styleNum(st.linewidth);
|
|
14497
|
+
const markerSize = styleNum(st.markersize) || 1;
|
|
14498
|
+
const gapSize = styleNum(st.gapsize) || 20;
|
|
14499
|
+
const fo = styleNum(st.fillopacity);
|
|
14500
|
+
const groupAlpha = styleNum(st.opacity) > 0 ? Math.min(1, styleNum(st.opacity)) : 1;
|
|
14501
|
+
const minValue = styleNum(st.minvalue);
|
|
14502
|
+
const rs = styleNum(st.rangescale);
|
|
14503
|
+
const dopMin = has(/DOPACITYMIN/), dop = !dopMin && has(/DOPACITY/);
|
|
14504
|
+
const dopPow = styleNum(st.dopacitypow) || 1, dopScale = styleNum(st.dopacityscale) || 1;
|
|
14505
|
+
const nMin = this._valueMin, nMax = this._valueMax;
|
|
14506
|
+
const lineColors = st.linecolor != null && st.linecolor !== '' ? flatToArray(st.linecolor) : null;
|
|
14507
|
+
const lineRgb = lineColors ? hexOrNamedToRgb(String(lineColors[lineColors.length - 1]).trim()) : null;
|
|
14508
|
+
const colors = this.categoryColorsRgb || [];
|
|
14509
|
+
const colorOf = it => {
|
|
14510
|
+
if (lineRgb) return lineRgb;
|
|
14511
|
+
if (this._vectorColorField) {
|
|
14512
|
+
const c = this._vectorColorOf.get(String(it.raw[this._vectorColorField]));
|
|
14513
|
+
if (c) return c;
|
|
14514
|
+
}
|
|
14515
|
+
if (it.cat != null) return it.cat >= 0 && colors[it.cat] ? colors[it.cat] : [0, 0, 0];
|
|
14516
|
+
return Number.isInteger(it.value) && colors[it.value - 1] ? colors[it.value - 1] : [0, 0, 0];
|
|
14517
|
+
};
|
|
14518
|
+
const fade = has(/\bFADEIN\b/) ? 0.2 : 2;
|
|
14519
|
+
const gradient = bezier && has(/\bGRADIENT\b|\bFADEIN\b/);
|
|
14520
|
+
const arrow = has(/\bPOINTER|ARROW\b/);
|
|
14521
|
+
const dash = has(/\bDASH\b/);
|
|
14522
|
+
const dopacity = it => {
|
|
14523
|
+
const range = Math.pow(nMax - nMin, 1 / dopPow);
|
|
14524
|
+
return Math.pow(dopMin ? nMax - it.nSize : it.nSize - nMin, 1 / dopPow) / range * (fo || 1) * dopScale;
|
|
14525
|
+
};
|
|
14526
|
+
|
|
14527
|
+
// flat's sort before drawing (maptheme.js 16774-16880)
|
|
14528
|
+
let order = items;
|
|
14529
|
+
const sized = !!this.binding.size;
|
|
14530
|
+
if (((nMin !== nMax) || sized) && !has(/NOSRT/) && (!has(/NOSIZE/) || has(/\bSORT\b/))
|
|
14531
|
+
&& (!this.flags.has('CATEGORICAL') || this.flags.has('AGGREGATE') || sized || has(/\bSORT\b/))) {
|
|
14532
|
+
const key = (sized || (this.flags.has('AGGREGATE') && has(/SUM/))) ? (it => it.nSize)
|
|
14533
|
+
: (this._vectorCategorical ? (it => (it.cat != null && it.cat >= 0 ? it.cat + 1 : 0)) : (it => it.value));
|
|
14534
|
+
const keyed = items.map(it => ({ it, y: key(it) })).filter(d => !isNaN(d.y));
|
|
14535
|
+
const down = has(/\bSORT\b/) && has(/\bUP\b/);
|
|
14536
|
+
keyed.sort((a, b) => (down ? b.y - a.y : a.y - b.y));
|
|
14537
|
+
order = keyed.map(d => d.it);
|
|
14538
|
+
}
|
|
14539
|
+
// the flows that would be drawn (as the loop below draws them), of
|
|
14540
|
+
// which flat keeps the biggest maxcharts
|
|
14541
|
+
if (Math.round(styleNum(st.maxcharts)) > 0) {
|
|
14542
|
+
order = applyFlatMaxCharts(order.filter(it => it.p2 && !(minValue && it.nSize < minValue) && (inBox(it.p1) || inBox(it.p2))), st);
|
|
14543
|
+
}
|
|
14544
|
+
|
|
14545
|
+
const paths = [], heads = [];
|
|
14546
|
+
const N = 32;
|
|
14547
|
+
const visible = c => !globeCenter || isOnVisibleHemisphere(c[0], c[1], globeCenter);
|
|
14548
|
+
for (const it of order) {
|
|
14549
|
+
let p1 = it.p1, p2 = it.p2;
|
|
14550
|
+
if (!p2) continue;
|
|
14551
|
+
if (minValue && it.nSize < minValue) continue;
|
|
14552
|
+
if (!inBox(p1) && !inBox(p2)) continue;
|
|
14553
|
+
const rgb = colorOf(it);
|
|
14554
|
+
let pts, end, tan, sw, headS, refX, refY, headAlpha, lineAlpha, stops = null, dashArr = null;
|
|
14555
|
+
if (bezier) {
|
|
14556
|
+
if (p1[0] === p2[0] && p1[1] === p2[1]) continue;
|
|
14557
|
+
const w = rawLw != null && rawLw !== '' && lwNum ? lwNum
|
|
14558
|
+
: 10 / Math.pow(nsv || maxSize, 1 / sizePow) * Math.pow(Math.abs(it.nSize), 1 / sizePow);
|
|
14559
|
+
sw = w * unit;
|
|
14560
|
+
if (!(sw > 0)) continue;
|
|
14561
|
+
if (has(/\bREVERSE\b/) || it.nSize < 0) [p1, p2] = [p2, p1];
|
|
14562
|
+
const a = toPx(p1), b = toPx(p2);
|
|
14563
|
+
const dx = b[0] - a[0], dy = b[1] - a[1];
|
|
14564
|
+
if (!(dx * dx + dy * dy > 0)) continue;
|
|
14565
|
+
let bow = rs;
|
|
14566
|
+
if (has(/\bRANDOM\b/)) bow -= rs * 0.66 * hashUnit(p1[0] + ',' + p1[1] + '&' + p2[0] + ',' + p2[1]);
|
|
14567
|
+
const gapAdd = has(/\bGAP\b/) ? gapSize : 3;
|
|
14568
|
+
const ll = typeof rawLw === 'string' && lwNum ? Number(rawLw + String(gapAdd)) : w + gapAdd;
|
|
14569
|
+
const lay = bezierVectorLayout(dx, dy, { w, ll, unit, oz, bow, t, marker: markerSize, gap: gapSize });
|
|
14570
|
+
pts = cubicBezierPoints(lay.start, lay.c1, lay.c2, lay.end, N);
|
|
14571
|
+
end = lay.end;
|
|
14572
|
+
tan = [lay.end[0] - lay.c2[0], lay.end[1] - lay.c2[1]];
|
|
14573
|
+
headS = Math.min(5, 2.5 + 5 / (w * scale)) * markerSize;
|
|
14574
|
+
refX = headS / 1.7; refY = headS / 2;
|
|
14575
|
+
lineAlpha = fo || 0.3;
|
|
14576
|
+
headAlpha = Math.min(1, (fo || 0.3) * 2);
|
|
14577
|
+
// (flat sets the computed opacity as is; one that is no number
|
|
14578
|
+
// — every size equal — SVG ignores: the opacity stays)
|
|
14579
|
+
if (dop || dopMin) { const d = dopacity(it); if (isFinite(d)) { lineAlpha = d; headAlpha = d; } }
|
|
14580
|
+
if (gradient) stops = fadeGradientStops(pts, lay.vx, lay.vy, fade);
|
|
14581
|
+
if (dash) dashArr = [50 * oz, 5 * oz + sw];
|
|
14582
|
+
pts = pts.map(p => [p[0] + a[0], p[1] + a[1]]);
|
|
14583
|
+
end = [end[0] + a[0], end[1] + a[1]];
|
|
14584
|
+
} else {
|
|
14585
|
+
const w = (rawLw != null && rawLw !== '' && lwNum) || (2 / (nsv || 1) / maxSize * it.nSize) || 0.1;
|
|
14586
|
+
sw = w * oz;
|
|
14587
|
+
const a = toPx(p1), b = toPx(p2);
|
|
14588
|
+
pts = [];
|
|
14589
|
+
for (let i = 0; i <= N; i++) pts.push([a[0] + (b[0] - a[0]) * i / N, a[1] + (b[1] - a[1]) * i / N]);
|
|
14590
|
+
end = b;
|
|
14591
|
+
tan = [b[0] - a[0], b[1] - a[1]];
|
|
14592
|
+
headS = 4; refX = 4; refY = 2;
|
|
14593
|
+
lineAlpha = fo || 0.3;
|
|
14594
|
+
headAlpha = Math.min(1, (fo || 0.3) * 2);
|
|
14595
|
+
if (dash) dashArr = [50 * oz, 15 * oz];
|
|
14596
|
+
}
|
|
14597
|
+
// a line under a pixel: one pixel at its width as opacity
|
|
14598
|
+
const drawW = Math.max(1, sw);
|
|
14599
|
+
const thin = Math.min(1, sw);
|
|
14600
|
+
const color2 = lineColors && lineColors.length > 1 ? hexOrNamedToRgb(String(lineColors[0]).trim()) : rgb;
|
|
14601
|
+
const vertexColor = i => {
|
|
14602
|
+
const s = stops ? stops[i] : null;
|
|
14603
|
+
const k = s ? s.k : 1;
|
|
14604
|
+
const c = s ? [0, 1, 2].map(j => Math.round(color2[j] + (rgb[j] - color2[j]) * k)) : rgb.slice(0, 3);
|
|
14605
|
+
return [...c, Math.round(255 * Math.min(1, lineAlpha) * (s ? s.alpha : 1) * thin * groupAlpha)];
|
|
14606
|
+
};
|
|
14607
|
+
const properties = { value: it.nSize, cat: it.cat, raw: it.raw, count: it.count,
|
|
14608
|
+
vector: { itemValue: it.cat != null ? (it.cat >= 0 ? it.cat + 1 : 0) : it.value } };
|
|
14609
|
+
// the line in map coordinates; on the globe only its visible runs
|
|
14610
|
+
const ll = pts.map(toLngLat);
|
|
14611
|
+
let run = [], runColors = [];
|
|
14612
|
+
const flush = () => {
|
|
14613
|
+
if (run.length > 1) paths.push({ path: run, colors: runColors, width: drawW, dash: dashArr ? [dashArr[0] / drawW, dashArr[1] / drawW] : [0, 0], properties, item: it });
|
|
14614
|
+
run = []; runColors = [];
|
|
14615
|
+
};
|
|
14616
|
+
ll.forEach((c, i) => { if (visible(c)) { run.push(c); runColors.push(vertexColor(i)); } else flush(); });
|
|
14617
|
+
flush();
|
|
14618
|
+
if (arrow && visible(toLngLat(end))) {
|
|
14619
|
+
const tri = arrowMarkerTriangle(end, tan[0], tan[1], sw, headS, refX, refY).map(toLngLat);
|
|
14620
|
+
heads.push({ type: 'Feature', properties, geometry: { type: 'Polygon', coordinates: [[...tri, tri[0]]] },
|
|
14621
|
+
fill: [...rgb.slice(0, 3), Math.round(255 * Math.min(1, headAlpha) * groupAlpha)] });
|
|
14622
|
+
}
|
|
14623
|
+
}
|
|
14624
|
+
const layers = [new PathLayer({
|
|
14625
|
+
id: `ix-vector-${this.name}`, data: paths, pickable: true,
|
|
14626
|
+
getPath: d => d.path, getColor: d => d.colors, getWidth: d => d.width,
|
|
14627
|
+
widthUnits: 'pixels', capRounded: dash && bezier, jointRounded: false, billboard: false,
|
|
14628
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS,
|
|
14629
|
+
...(dash ? { getDashArray: d => d.dash, extensions: [new PathStyleExtension({ dash: true })] } : {})
|
|
14630
|
+
})];
|
|
14631
|
+
if (heads.length) {
|
|
14632
|
+
// the head count in the id: deck kept a same-id polygon layer's
|
|
14633
|
+
// larger buffers when a filter cut the heads (the Zeit page's "to
|
|
14634
|
+
// Africa": bow-tie triangles from the unfiltered heads' vertices).
|
|
14635
|
+
// Every head has the same vertex count, so an equal count — a pan —
|
|
14636
|
+
// reuses the layer; a changed one gets fresh buffers
|
|
14637
|
+
layers.push(new GeoJsonLayer({
|
|
14638
|
+
id: `ix-vector-heads-${this.name}-n${heads.length}`, data: { type: 'FeatureCollection', features: heads }, pickable: true,
|
|
14639
|
+
filled: true, stroked: false, getFillColor: d => d.fill, parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14640
|
+
}));
|
|
14641
|
+
}
|
|
14642
|
+
return layers;
|
|
14643
|
+
}
|
|
14644
|
+
|
|
11795
14645
|
// flat's chart position for this build (flatChartAlignOffset): a
|
|
11796
14646
|
// function item → pixel offset, the same for every part of one chart
|
|
11797
14647
|
// (symbol, glow, shadow, box, title, value text). unit is flat's
|
|
@@ -11909,6 +14759,60 @@
|
|
|
11909
14759
|
return { noBreaks: flatNoBreaks(vMin, vMax), maxValue: styleNum(this.style.normalsizevalue) || (isFinite(vMax) ? vMax : undefined) };
|
|
11910
14760
|
}
|
|
11911
14761
|
|
|
14762
|
+
// The charts of _buildChartLayers' bubbles layer as
|
|
14763
|
+
// chartTextTransmittance's occluders, in draw order and screen
|
|
14764
|
+
// pixels, and each text's resulting alpha: the text's color gets it,
|
|
14765
|
+
// the returned Set holds the texts still visible. Screen pixels:
|
|
14766
|
+
// Web Mercator world pixels at the live zoom; under globe a local
|
|
14767
|
+
// equal-area frame around the view center at the scale MapLibre's
|
|
14768
|
+
// globe matches Mercator's there (the limb's foreshortening ignored).
|
|
14769
|
+
_occludeChartTexts(combined, texts, o) {
|
|
14770
|
+
const visible = new Set();
|
|
14771
|
+
if (!texts.length) return visible;
|
|
14772
|
+
const toRad = Math.PI / 180;
|
|
14773
|
+
let screenOf;
|
|
14774
|
+
if (o.globeCenter) {
|
|
14775
|
+
const s = 512 * Math.pow(2, o.liveZoom) / 360 / Math.cos(o.globeCenter.lat * toRad);
|
|
14776
|
+
screenOf = c => [(((c[0] - o.globeCenter.lng) % 360 + 540) % 360 - 180) * Math.cos(c[1] * toRad) * s, -c[1] * s];
|
|
14777
|
+
} else {
|
|
14778
|
+
screenOf = c => { const p = lngLatToWorldPixel(c[0], c[1], o.liveZoom); return [p.x, p.y]; };
|
|
14779
|
+
}
|
|
14780
|
+
const symbols = Array.isArray(this.style.symbols) ? this.style.symbols : null;
|
|
14781
|
+
const charts = combined.map(d => {
|
|
14782
|
+
const [x, y] = screenOf(d.geometry.coordinates);
|
|
14783
|
+
const a = o.alignOf(d), r = o.radiusOf(d), iconAlpha = this._iconAlpha(d) / 255;
|
|
14784
|
+
let parts;
|
|
14785
|
+
if (o.singleSymbol(d)) {
|
|
14786
|
+
// the icon's shape is inset by half its border (_buildSingleIcon)
|
|
14787
|
+
const shape = o.shapeOf(d.properties);
|
|
14788
|
+
parts = [{ dx: 0, dy: 0, r: r * (1 - o.singleBorderWidthPx / BUBBLE_ICON_SIZE), shape, alpha: o.fillOpacity * iconAlpha }];
|
|
14789
|
+
} else {
|
|
14790
|
+
// _buildBubbleIcon's packed parts, its baked 0.9 alpha scaled by an
|
|
14791
|
+
// explicit fillopacity like the bubbles layer's getColor
|
|
14792
|
+
const counts = d.properties.counts, iso = this._groupIsolation(d);
|
|
14793
|
+
const { present, radii, offsets, fitScale } = (iso && isolatedBubblePackLayout(counts, BUBBLE_ICON_SIZE, iso)) || computeBubblePackLayout(counts, BUBBLE_ICON_SIZE);
|
|
14794
|
+
const px = 2 * r / BUBBLE_ICON_SIZE * fitScale;
|
|
14795
|
+
const alpha = 0.9 * (this.style.fillopacity != null ? o.fillOpacity : 1) * iconAlpha;
|
|
14796
|
+
parts = present.map((p, i) => ({ dx: offsets[i].x * px, dy: offsets[i].y * px, r: radii[i] * px,
|
|
14797
|
+
shape: symbols ? normalizeSymbolShape(symbols[Math.min(p.i, symbols.length - 1)]) : 'circle', alpha }));
|
|
14798
|
+
}
|
|
14799
|
+
return { x: x + a[0], y: y + a[1], parts };
|
|
14800
|
+
});
|
|
14801
|
+
// the text box: Arial digits are ~0.56 em wide, ~0.7 em high
|
|
14802
|
+
const boxes = texts.map(t => {
|
|
14803
|
+
const [x, y] = screenOf(t.geometry.coordinates);
|
|
14804
|
+
return { order: t.order, x: x + t.pixelOffset[0], y: y + t.pixelOffset[1], w: 0.56 * t.fontSize * String(t.text).length, h: 0.7 * t.fontSize };
|
|
14805
|
+
});
|
|
14806
|
+
const shown = chartTextTransmittance(charts, boxes);
|
|
14807
|
+
texts.forEach((t, i) => {
|
|
14808
|
+
if (shown[i] < CHART_TEXT_MIN_TRANSMITTANCE) return;
|
|
14809
|
+
const c = t.color || [0, 0, 0];
|
|
14810
|
+
if (shown[i] < 1) t.color = [c[0], c[1], c[2], Math.round((c[3] != null ? c[3] : 255) * shown[i])];
|
|
14811
|
+
visible.add(t);
|
|
14812
|
+
});
|
|
14813
|
+
return visible;
|
|
14814
|
+
}
|
|
14815
|
+
|
|
11912
14816
|
// CHART|LABEL (maptheme.js 20647-21037), at the chart radius r of the
|
|
11913
14817
|
// size value:
|
|
11914
14818
|
// - LABEL: a rounded rectangle 2.05r wide and 1.6·f high (f = min(0.8r,
|
|
@@ -12882,11 +15786,12 @@
|
|
|
12882
15786
|
const map = _lastMapApi.map;
|
|
12883
15787
|
const center = map.getCenter();
|
|
12884
15788
|
const proj = (typeof map.getProjection === 'function' && map.getProjection()) || { type: 'mercator' };
|
|
15789
|
+
const svgProjection = mapSvgProjection(map);
|
|
12885
15790
|
return JSON.stringify({
|
|
12886
15791
|
map: {
|
|
12887
|
-
map: proj.type === 'globe' ? 'maps/svg/maps/generic/orthographic.svg' : 'maps/svg/maps/generic/mercator.svg',
|
|
15792
|
+
map: proj.type === 'globe' ? 'maps/svg/maps/generic/orthographic.svg' : svgProjection ? svgProjection.svg : 'maps/svg/maps/generic/mercator.svg',
|
|
12888
15793
|
center: { lat: center.lat, lng: center.lng },
|
|
12889
|
-
zoom:
|
|
15794
|
+
zoom: flatViewZoomOf(map)
|
|
12890
15795
|
}
|
|
12891
15796
|
});
|
|
12892
15797
|
}
|
|
@@ -12957,6 +15862,13 @@
|
|
|
12957
15862
|
htmlgui_getAttributionString: () => (_lastMapApi && _lastMapApi.getAttribution ? _lastMapApi.getAttribution() : ''),
|
|
12958
15863
|
changeThemeStyle: compatChangeThemeStyle,
|
|
12959
15864
|
removeTheme: compatRemoveTheme,
|
|
15865
|
+
// flat's ixmaps.setLocalString(orig, local) (htmlgui.js 3814 →
|
|
15866
|
+
// Api.setLocalString → Dictionary.replace): a page-wide translation,
|
|
15867
|
+
// e.g. the migration page's ISO code → country name; usable before
|
|
15868
|
+
// the map exists. getLocalString: its translation (flat's never
|
|
15869
|
+
// returns it — a missing return there)
|
|
15870
|
+
setLocalString: (szOrig, szLocal) => { _localStrings[szOrig] = szLocal; },
|
|
15871
|
+
getLocalString: (szOrig) => makeLocalText([])(szOrig),
|
|
12960
15872
|
// flat's ixmaps.newTheme(title, theme, flag) — on the last built map
|
|
12961
15873
|
newTheme: (title, theme, flag) => _mapHandle.newTheme(title, theme, flag),
|
|
12962
15874
|
// flat's time slider: show the records of a theme (null = all themes) whose timefield lies in [min, max)
|
|
@@ -12985,16 +15897,16 @@
|
|
|
12985
15897
|
// test-only: lets test/unit/*.test.mjs call pure internals directly (the
|
|
12986
15898
|
// engine runs in a Node vm there); deliberately NOT on the ixmaps object
|
|
12987
15899
|
global.__ixmapsGlInternals = {
|
|
12988
|
-
IXMAPS_GL_VERSION, youtubeClickToPlay, symbolsFlagCompat, tooltipTable, legendRowLabels, scaleDenom, cssColorAlpha, aggregatedCategoricalClass, dominantDopacityAlpha, isAggregatedCategoricalChoropleth, computeBubblePackLayout, isolatedBubblePackLayout, sequenceLayout, ringsLayout, multiQuadOffsets, pixelOffsetLngLat,
|
|
15900
|
+
IXMAPS_GL_VERSION, applyFlatMaxCharts, youtubeClickToPlay, symbolsFlagCompat, tooltipTable, legendRowLabels, scaleDenom, cssColorAlpha, aggregatedCategoricalClass, dominantDopacityAlpha, isAggregatedCategoricalChoropleth, computeBubblePackLayout, isolatedBubblePackLayout, sequenceLayout, ringsLayout, pieSliceLayout, pieValueLabelLayout, isBarChart, flatBarLayout, flatChartBox, flatValueRules, flatPieRecord, flatPieAccumulate, flatPieItemValues, flatVectorItems, bezierVectorLayout, cubicBezierPoints, arrowMarkerTriangle, fadeGradientStops, flatToArray, hashUnit, multiQuadOffsets, pixelOffsetLngLat, chartTextTransmittance, insideSymbolShape,
|
|
12989
15901
|
normalizeTheme, projectThemeToDefinition, withoutProjectCode, groupRecordCount, resolveBasemapStyleUrl, resolveMapTypeColor, LayerBuilder, LayerRuntime, MapBuilder, typeStyleNumbers, styleNum,
|
|
12990
15902
|
resolveScriptUrl, isTrustedScriptUrl, loadProcessingScript, loadBrokerData, applyBrokerThemePatch, makeBrokerTheme,
|
|
12991
15903
|
equalIntervalBreaks, quantileBreaks, naturalBreaks, evenStrideSample, jenksBreakValues, partsFromBreakValues, resolvePartsClass,
|
|
12992
15904
|
computeAlphaStats, computeMultiFieldClasses, computeDominantStats, resolveDominantClass, computeComposeColorStats,
|
|
12993
15905
|
resolveComposedColor, computeRangeClasses, colorSchemeClassCount, resolveDopacityAlpha,
|
|
12994
15906
|
flatColorSweep, applyClassesToColorScheme, resolveClassColors, flatOutlierStats, parseCssColor, flatLegendLook,
|
|
12995
|
-
flatToMapLibreZoom, mapLibreToFlatZoom, scaleDenominatorAt, resolveZoomReference, resolveAggregationPx, valuesHiddenByScale,
|
|
15907
|
+
flatToMapLibreZoom, mapLibreToFlatZoom, flatViewToMapLibreZoom, mapLibreToFlatViewZoom, flatSvgProjectionOf, flatSvgProjectionByOptions, normalSizeScaleOf, scaleDenominatorAt, resolveZoomReference, resolveAggregationPx, valuesHiddenByScale,
|
|
12996
15908
|
fetchLayerData, parseCsvText, dataTableRows, geometryRowsToFeatureCollection, filterFlatValues, applyField100, field100Binding, rangeClassLegendTotals, resolveAggregateValue, classValueSeparate, cellAggregatedValues, oneHot, groupCoLocated, aggregateOnGrid, GridAggregateIndex, flatRangeParts, aggregateField100, valueRadius, itemPlotGeometry, itemAnchor, objectZoomFactor, resolveZoomReference, defaultNormalSizeValue,
|
|
12997
|
-
applyWhereFilter, joinChartPositions, flatLookupKey, flatShapeCenter, chartHiddenByScale, glowHiddenByScale, isSymbolChart, flatChartAlignOffset, flatChartBranch, flatValueText, categoryValueRecord, flatDerivateRgb, flatChartTextRgb, flatGroupedValue, flatNoBreaks, featuresHiddenByScale, boxHiddenByScale, flatShadowOn, snapToAggregationGrid,
|
|
15909
|
+
applyWhereFilter, joinChartPositions, flatLookupKey, flatShapeCenter, chartHiddenByScale, glowHiddenByScale, isSymbolChart, flatChartAlignOffset, flatChartBranch, flatValueText, categoryValueRecord, flatDerivateRgb, flatBubbleLook, bubbleIconLook, flatChartTextRgb, flatGroupedValue, flatNoBreaks, featuresHiddenByScale, boxHiddenByScale, flatShadowOn, snapToAggregationGrid,
|
|
12998
15910
|
drawSymbolPath, normalizeSymbolShape,
|
|
12999
15911
|
flatFormatValue, themeStyleArgs, flatChangedStyleValue, explicitValueDecimals, legendDecimals,
|
|
13000
15912
|
dataCacheKey, dataCacheDisabled, cachedLayerData, featuresBounds, wantsZoomToExtent, legendIsOn,
|