ixmaps-gl 0.2.24 → 0.2.25
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 +3 -3
- package/ixmaps-gl.js +1343 -665
- package/package.json +1 -1
package/ixmaps-gl.js
CHANGED
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
// ---------------------------------------------------------------
|
|
101
101
|
// this engine's release (= package.json "version", checked by
|
|
102
102
|
// test/unit/version.test.mjs); ixmaps.glVersion, and logged once at start
|
|
103
|
-
const IXMAPS_GL_VERSION = '0.2.
|
|
103
|
+
const IXMAPS_GL_VERSION = '0.2.25';
|
|
104
104
|
|
|
105
105
|
const LIB_URLS = {
|
|
106
106
|
// Bumped 3.6.2 -> 5.x (2026-09-21, globe-projection compat fix): native
|
|
@@ -220,7 +220,7 @@
|
|
|
220
220
|
let IconLayer, ScatterplotLayer, GeoJsonLayer, MapLibreOverlay, TextLayer, PathLayer, PathStyleExtension;
|
|
221
221
|
// PieSliceLayer (see makePieSliceLayer) and BarShapeLayer
|
|
222
222
|
// (makeBarShapeLayer) are derived from ScatterplotLayer once deck.gl is there
|
|
223
|
-
let PieSliceLayer, BarShapeLayer;
|
|
223
|
+
let PieSliceLayer, BarShapeLayer, FlowPathLayer;
|
|
224
224
|
|
|
225
225
|
// CHART|PIE / DONUT slices (_buildPieLayers): one instance per slice — a
|
|
226
226
|
// ScatterplotLayer circle (billboarded, pixel radius, pixel offset, the
|
|
@@ -242,6 +242,111 @@
|
|
|
242
242
|
// without anti-aliasing between them (a hard partition: no background
|
|
243
243
|
// seam shows between two slices); the circle's rim keeps
|
|
244
244
|
// ScatterplotLayer's own anti-aliasing.
|
|
245
|
+
// flat's VECTOR|DASH (maptheme.js 18240-18256, the bezier branch):
|
|
246
|
+
// the flow lines are dashed — one long dash, a small gap — and the
|
|
247
|
+
// dash pattern MOVES toward the target: flat animates each SVG line's
|
|
248
|
+
// stroke-dashoffset from d1+d2+random down to random (1-2s, infinite),
|
|
249
|
+
// the offset decreasing walking the pattern along the path direction.
|
|
250
|
+
// Here the same in the fragment shader — but deck.gl tessellates a
|
|
251
|
+
// path into per-SEGMENT quads whose vPathPosition.y restarts at 0 at
|
|
252
|
+
// every segment vertex (and is in half-stroke-width units, not
|
|
253
|
+
// pixels), so the pattern is NOT continuous from the segment
|
|
254
|
+
// coordinate alone. deck's own PathStyleExtension with dashMode
|
|
255
|
+
// 'path' (HIGH_PRECISION_DASH) supplies the missing cumulative term:
|
|
256
|
+
// its calculateDashMetrics fills per-segment [distance so far, path
|
|
257
|
+
// length] and its vertex shader turns it into vDashOffset, making
|
|
258
|
+
// alongPath = vPathPosition.y + vDashOffset
|
|
259
|
+
// continuous across segments (deck's own dash shader uses exactly
|
|
260
|
+
// this; the pattern is periodic, so the mod() in vDashOffset is
|
|
261
|
+
// seamless). The extension's own gap stays 0 (getDashArray's second
|
|
262
|
+
// component — it renders the line solid); THIS layer's
|
|
263
|
+
// DECKGL_FILTER_COLOR discards the moving gap: the solid length and
|
|
264
|
+
// flat's random per-arc phase as an instanced attribute, a time
|
|
265
|
+
// uniform advanced by the wall clock (one full dash cycle every 2s),
|
|
266
|
+
// and fragments outside the moving dash window discarded — the layers
|
|
267
|
+
// themselves are NOT rebuilt per frame. The discard skips the picking
|
|
268
|
+
// pass (picking.isActive): flat's dashed SVG line is hover-tested
|
|
269
|
+
// across its gaps too, so the arcs keep their tooltips while the
|
|
270
|
+
// dash runs
|
|
271
|
+
function makeFlowPathLayer(Base) {
|
|
272
|
+
// deck.gl 9: a layer's custom uniforms live in shader MODULES — an
|
|
273
|
+
// std140 block named <module>Uniforms, its layout declared by the
|
|
274
|
+
// module's own GLSL and uniformTypes, values pushed per draw via
|
|
275
|
+
// setShaderModuleProps (the picking module's picking.isActive works
|
|
276
|
+
// exactly the same way). A plain GLSL uniform would have no backing
|
|
277
|
+
// buffer, and deck 8's state.model.setUniforms() no longer exists.
|
|
278
|
+
const FLOW_MODULE = {
|
|
279
|
+
name: 'flow',
|
|
280
|
+
uniformTypes: { uFlowTime: 'f32' },
|
|
281
|
+
defaultUniforms: { uFlowTime: 0 },
|
|
282
|
+
vs: 'layout(std140) uniform flowUniforms { float uFlowTime; } flow;',
|
|
283
|
+
fs: 'layout(std140) uniform flowUniforms { float uFlowTime; } flow;'
|
|
284
|
+
};
|
|
285
|
+
class Layer extends Base {
|
|
286
|
+
getShaders() {
|
|
287
|
+
const shaders = super.getShaders();
|
|
288
|
+
shaders.modules = (shaders.modules || []).concat(FLOW_MODULE);
|
|
289
|
+
const inject = Object.assign({}, shaders.inject || {});
|
|
290
|
+
inject['vs:#decl'] = (inject['vs:#decl'] || '') + `
|
|
291
|
+
in vec2 instanceFlowPhase;
|
|
292
|
+
out vec2 vFlowPhase;
|
|
293
|
+
`;
|
|
294
|
+
inject['vs:#main-end'] = (inject['vs:#main-end'] || '') + `
|
|
295
|
+
vFlowPhase = instanceFlowPhase;
|
|
296
|
+
`;
|
|
297
|
+
inject['fs:#decl'] = (inject['fs:#decl'] || '') + `
|
|
298
|
+
in vec2 vFlowPhase;
|
|
299
|
+
`;
|
|
300
|
+
// before the picking module's own filter; the dash is skipped
|
|
301
|
+
// there (see the class comment). vDashArray/vDashOffset come
|
|
302
|
+
// from the PathStyleExtension (see the layer's extensions),
|
|
303
|
+
// the position along the path is geometry.uv (the path
|
|
304
|
+
// layer's main sets it to vPathPosition before the filter
|
|
305
|
+
// runs; the varying itself is NOT redeclared here — the base
|
|
306
|
+
// shader already declares it, and the injected block lands
|
|
307
|
+
// before the base source, so a redeclaration is a
|
|
308
|
+
// redefinition error). uFlowTime comes from the flow module's
|
|
309
|
+
// block (see FLOW_MODULE)
|
|
310
|
+
inject['fs:DECKGL_FILTER_COLOR'] = (inject['fs:DECKGL_FILTER_COLOR'] || '') + `
|
|
311
|
+
{
|
|
312
|
+
float period = vDashArray.x;
|
|
313
|
+
float pickingActive = 0.0;
|
|
314
|
+
#ifdef MODULE_PICKING
|
|
315
|
+
pickingActive = picking.isActive;
|
|
316
|
+
#endif
|
|
317
|
+
if (period > 0.0 && pickingActive < 0.5) {
|
|
318
|
+
float alongPath = geometry.uv.y + vDashOffset;
|
|
319
|
+
float pos = fract((alongPath - flow.uFlowTime * period + vFlowPhase.y) / period);
|
|
320
|
+
if (pos * period > vFlowPhase.x) discard;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
`;
|
|
324
|
+
shaders.inject = inject;
|
|
325
|
+
return shaders;
|
|
326
|
+
}
|
|
327
|
+
initializeState() {
|
|
328
|
+
super.initializeState();
|
|
329
|
+
this.getAttributeManager().addInstanced({
|
|
330
|
+
instanceFlowPhase: { size: 2, accessor: 'getFlowPhase', defaultValue: [0, 0] }
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
draw(opts) {
|
|
334
|
+
// one full dash cycle every 2s — flat's animate dur (the bezier
|
|
335
|
+
// branch); the offset decreases, so the pattern walks toward
|
|
336
|
+
// the target (alongPath grows along the path). setProps on
|
|
337
|
+
// the shader inputs; the model's predraw flushes the uniform
|
|
338
|
+
// buffer when super.draw renders
|
|
339
|
+
this.setShaderModuleProps({ flow: { uFlowTime: (Date.now() % 2000) / 2000 } });
|
|
340
|
+
super.draw(opts);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
Layer.layerName = 'FlowPathLayer';
|
|
344
|
+
Layer.defaultProps = Object.assign({}, Base.defaultProps, {
|
|
345
|
+
getFlowPhase: { type: 'accessor', value: [0, 0] }
|
|
346
|
+
});
|
|
347
|
+
return Layer;
|
|
348
|
+
}
|
|
349
|
+
|
|
245
350
|
function makePieSliceLayer(Base) {
|
|
246
351
|
class Layer extends Base {
|
|
247
352
|
getShaders() {
|
|
@@ -293,12 +398,15 @@ in vec4 vPieLineColor;
|
|
|
293
398
|
}
|
|
294
399
|
// in device pixels (fwidth: CSS px per device pixel), so a hairline
|
|
295
400
|
// stays one at any device pixel ratio. Half the outline lies inside
|
|
296
|
-
// each slice (the other half in its neighbour)
|
|
297
|
-
//
|
|
401
|
+
// each slice (the other half in its neighbour). No fade-in by the
|
|
402
|
+
// width: the browser rasterizes flat's sub-pixel SVG strokes as
|
|
403
|
+
// hairlines — one device pixel at FULL opacity (Skia's stroke floor,
|
|
404
|
+
// measured on the ISTAT story: 0.17 CSS px strokes render gray 32) —
|
|
405
|
+
// so the line keeps full alpha and only its extent grows past 1px
|
|
298
406
|
float pieAA = max(fwidth(pieR), 1e-4);
|
|
299
407
|
float pieH = vPieLineWidth * 0.5 / pieAA;
|
|
300
408
|
float pieDD = pieD / pieAA;
|
|
301
|
-
float pieLine = pieH > 0.0 ? clamp(
|
|
409
|
+
float pieLine = pieH > 0.0 ? clamp(pieH + 0.5 - pieDD, 0.0, 1.0) : 0.0;
|
|
302
410
|
float pieRim = clamp((vPieRadius - pieR) / pieAA + 0.5, 0.0, 1.0);
|
|
303
411
|
color = vec4(mix(color.rgb, vPieLineColor.rgb, pieLine), mix(color.a, vPieLineColor.a * pieRim, pieLine));
|
|
304
412
|
if (pieInner > 0.0) color.a *= clamp((pieR - pieInner) / pieAA + 0.5, 0.0, 1.0);
|
|
@@ -503,6 +611,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
503
611
|
({ IconLayer, ScatterplotLayer, GeoJsonLayer, MapLibreOverlay, TextLayer, PathLayer, PathStyleExtension } = global.deck);
|
|
504
612
|
PieSliceLayer = makePieSliceLayer(ScatterplotLayer);
|
|
505
613
|
BarShapeLayer = makeBarShapeLayer(ScatterplotLayer);
|
|
614
|
+
FlowPathLayer = makeFlowPathLayer(PathLayer);
|
|
506
615
|
// which engine and libraries this page really runs (a page may
|
|
507
616
|
// bring its own MapLibre/deck.gl — the loaded ones are named)
|
|
508
617
|
const ml = global.maplibregl && typeof global.maplibregl.getVersion === 'function' ? global.maplibregl.getVersion() : '?';
|
|
@@ -662,10 +771,17 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
662
771
|
return ring;
|
|
663
772
|
}
|
|
664
773
|
|
|
774
|
+
// a quantized topology's point positions are absolute (not delta-encoded
|
|
775
|
+
// like arcs) quantized integers: x * scale + translate
|
|
776
|
+
function decodePoint(p, transform) {
|
|
777
|
+
if (!transform) return p;
|
|
778
|
+
return [p[0] * transform.scale[0] + transform.translate[0], p[1] * transform.scale[1] + transform.translate[1]];
|
|
779
|
+
}
|
|
780
|
+
|
|
665
781
|
function topoGeometryToGeoJSON(geom, arcs, transform) {
|
|
666
782
|
switch (geom.type) {
|
|
667
|
-
case 'Point': return { type: 'Point', coordinates: geom.coordinates };
|
|
668
|
-
case 'MultiPoint': return { type: 'MultiPoint', coordinates: geom.coordinates };
|
|
783
|
+
case 'Point': return { type: 'Point', coordinates: decodePoint(geom.coordinates, transform) };
|
|
784
|
+
case 'MultiPoint': return { type: 'MultiPoint', coordinates: geom.coordinates.map(p => decodePoint(p, transform)) };
|
|
669
785
|
case 'LineString': return { type: 'LineString', coordinates: ringCoords(geom.arcs, arcs, transform) };
|
|
670
786
|
case 'MultiLineString': return { type: 'MultiLineString', coordinates: geom.arcs.map(a => ringCoords(a, arcs, transform)) };
|
|
671
787
|
case 'Polygon': return { type: 'Polygon', coordinates: geom.arcs.map(ring => ringCoords(ring, arcs, transform)) };
|
|
@@ -941,11 +1057,40 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
941
1057
|
// ixmaps-flat dev tree, Themes.prototype.setExternalData). This engine
|
|
942
1058
|
// has no such pipeline to resume, so the bridge only needs to do the
|
|
943
1059
|
// ONE thing fetchLayerData actually needs: resolve the promise a
|
|
944
|
-
// query()-mode fetch is awaiting.
|
|
945
|
-
//
|
|
946
|
-
//
|
|
947
|
-
//
|
|
948
|
-
|
|
1060
|
+
// query()-mode fetch is awaiting. Queries run in parallel (several
|
|
1061
|
+
// layers, a later map.layer()), so every pending one has its own entry,
|
|
1062
|
+
// keyed by the options object its query function received — real pages
|
|
1063
|
+
// hand that same object back: query(themeObj, options) …
|
|
1064
|
+
// ixmaps.setExternalData(data, options). In data order: oldest first.
|
|
1065
|
+
const _pendingQueries = new Map(); // options object → { name, resolve, reject, timer }
|
|
1066
|
+
// a pending query's timeout (0: DATAJS_LOAD_TIMEOUT_MS) — a field so the
|
|
1067
|
+
// unit tests can shorten it
|
|
1068
|
+
const QUERY_HANDOFF = { timeoutMs: 0 };
|
|
1069
|
+
function settleQuery(opts, err, data) {
|
|
1070
|
+
const q = _pendingQueries.get(opts);
|
|
1071
|
+
if (!q) return;
|
|
1072
|
+
_pendingQueries.delete(opts);
|
|
1073
|
+
clearTimeout(q.timer);
|
|
1074
|
+
if (err) q.reject(err); else q.resolve(data);
|
|
1075
|
+
}
|
|
1076
|
+
// the pending query a setExternalData(data, opt) belongs to: the one
|
|
1077
|
+
// given that very options object, else the one of that data name, else —
|
|
1078
|
+
// no name — the oldest (exact while only one is pending)
|
|
1079
|
+
function pendingQueryOf(opt) {
|
|
1080
|
+
if (opt && _pendingQueries.has(opt)) return opt;
|
|
1081
|
+
const name = opt && opt.name;
|
|
1082
|
+
if (name != null && name !== '') {
|
|
1083
|
+
for (const [o, q] of _pendingQueries) if (q.name === String(name)) return o;
|
|
1084
|
+
return null;
|
|
1085
|
+
}
|
|
1086
|
+
const first = _pendingQueries.keys().next();
|
|
1087
|
+
if (first.done) return null;
|
|
1088
|
+
if (_pendingQueries.size > 1) {
|
|
1089
|
+
console.warn('[ixmaps-gl] ixmaps.setExternalData without its query\'s options object or a name, while ' + _pendingQueries.size +
|
|
1090
|
+
' .data({query}) fetches are pending — handed to the oldest; pass the options the query function received');
|
|
1091
|
+
}
|
|
1092
|
+
return first.value;
|
|
1093
|
+
}
|
|
949
1094
|
// Data by name, as flat (maptheme.js 2570-2593, 8286-8310):
|
|
950
1095
|
// setExternalData(data, {type, name}) makes a data.js Table of the data
|
|
951
1096
|
// and keeps it as window[name]; whoever waits for that name gets it, and
|
|
@@ -991,6 +1136,12 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
991
1136
|
// globals a data name must not overwrite
|
|
992
1137
|
const _engineGlobalsGuard = { ixmaps: 1, Data: 1, deck: 1, maplibregl: 1, window: 1, document: 1, $: 1, jQuery: 1 };
|
|
993
1138
|
function setExternalDataBridge(dataObj, opt) {
|
|
1139
|
+
const query = pendingQueryOf(opt);
|
|
1140
|
+
if (query) {
|
|
1141
|
+
if (dataObj == null) settleQuery(query, new Error('[ixmaps-gl] .data({query}): the query handed over no data (ixmaps.setExternalData(null, …))'));
|
|
1142
|
+
else settleQuery(query, null, dataObj);
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
994
1145
|
const name = opt && opt.name;
|
|
995
1146
|
if (name != null && name !== '') {
|
|
996
1147
|
(dataObj == null ? Promise.resolve(null) : externalDataToTable(dataObj, opt))
|
|
@@ -998,13 +1149,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
998
1149
|
.catch(err => { console.error(err); publishNamedData(String(name), null); });
|
|
999
1150
|
return;
|
|
1000
1151
|
}
|
|
1001
|
-
|
|
1002
|
-
console.warn('[ixmaps-gl] ixmaps.setExternalData called with no pending .data({query}) fetch — ignored');
|
|
1003
|
-
return;
|
|
1004
|
-
}
|
|
1005
|
-
const resolve = _pendingQueryResolve;
|
|
1006
|
-
_pendingQueryResolve = null;
|
|
1007
|
-
resolve(dataObj);
|
|
1152
|
+
console.warn('[ixmaps-gl] ixmaps.setExternalData called with no pending .data({query}) fetch — ignored');
|
|
1008
1153
|
}
|
|
1009
1154
|
|
|
1010
1155
|
// ---------------------------------------------------------------
|
|
@@ -1402,7 +1547,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1402
1547
|
}
|
|
1403
1548
|
obj = runDataProcess(obj, dataConfig);
|
|
1404
1549
|
}
|
|
1405
|
-
|
|
1550
|
+
// a raw jsonDB object ({fields, records}) → rows, with or without
|
|
1551
|
+
// process (and without needing data.js)
|
|
1552
|
+
const isJsonDb = !Array.isArray(obj) && Array.isArray(obj.fields) && Array.isArray(obj.records);
|
|
1553
|
+
const rows = typeof obj.json === 'function' ? obj.json() : isJsonDb ? dataTableRows(obj) : obj;
|
|
1406
1554
|
return rowsResult(rows, binding);
|
|
1407
1555
|
}
|
|
1408
1556
|
|
|
@@ -1412,9 +1560,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1412
1560
|
'<script src="https://cdn.jsdelivr.net/gh/gjrichter/data.js@master/data.js"> before ixmaps-gl.js');
|
|
1413
1561
|
}
|
|
1414
1562
|
const queryFn = typeof dataConfig.query === 'function' ? dataConfig.query : new Function(`return (${dataConfig.query});`)();
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1563
|
+
// its own options object: the query hands it back with the data
|
|
1564
|
+
const opts = { type: dataConfig.type, name: dataConfig.name };
|
|
1565
|
+
const timeoutMs = QUERY_HANDOFF.timeoutMs || DATAJS_LOAD_TIMEOUT_MS;
|
|
1566
|
+
const dataObj = await new Promise((resolve, reject) => {
|
|
1567
|
+
const timer = setTimeout(() => settleQuery(opts, new Error(`[ixmaps-gl] .data({query}): no data handed over (ixmaps.setExternalData) within ${timeoutMs / 1000}s`)), timeoutMs);
|
|
1568
|
+
_pendingQueries.set(opts, { name: dataConfig.name != null ? String(dataConfig.name) : null, resolve, reject, timer });
|
|
1569
|
+
try { queryFn({}, opts); } catch (err) { settleQuery(opts, err); }
|
|
1418
1570
|
});
|
|
1419
1571
|
return rowsResult((dataConfig.process ? runDataProcess(dataObj, dataConfig) : dataObj).json(), binding);
|
|
1420
1572
|
}
|
|
@@ -1436,8 +1588,21 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1436
1588
|
// parser (PapaParse), so the Table arrives via the callback
|
|
1437
1589
|
const tables = await Promise.all(texts.map(dataJsCsvTable));
|
|
1438
1590
|
let table = tables[0];
|
|
1439
|
-
|
|
1440
|
-
|
|
1591
|
+
// records are positional: align each later file's columns to the
|
|
1592
|
+
// first file's by name (a column only a later file has is added),
|
|
1593
|
+
// as the no-data.js path below gets from each file's own header
|
|
1594
|
+
for (const t of tables.slice(1)) {
|
|
1595
|
+
const ids = table.fields.map(f => f.id);
|
|
1596
|
+
const tIds = t.fields.map(f => f.id);
|
|
1597
|
+
if (tIds.length === ids.length && tIds.every((id, c) => id === ids[c])) {
|
|
1598
|
+
table.records = table.records.concat(t.records);
|
|
1599
|
+
continue;
|
|
1600
|
+
}
|
|
1601
|
+
t.fields.forEach(f => { if (!ids.includes(f.id)) { ids.push(f.id); table.fields.push(f); } });
|
|
1602
|
+
const src = ids.map(id => tIds.indexOf(id));
|
|
1603
|
+
table.records = table.records.concat(t.records.map(rec => src.map(c => (c < 0 ? undefined : rec[c]))));
|
|
1604
|
+
}
|
|
1605
|
+
if (table.table) { table.table.records = table.records.length; table.table.fields = table.fields.length; }
|
|
1441
1606
|
if (scriptProcess) table = scriptProcess(table);
|
|
1442
1607
|
else if (dataConfig.process) table = runDataProcess(table, dataConfig);
|
|
1443
1608
|
rows = dataTableRows(table);
|
|
@@ -1858,21 +2023,25 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1858
2023
|
if (isAggregatedCategoricalChoropleth(spec)) {
|
|
1859
2024
|
const groups = new Map();
|
|
1860
2025
|
for (const row of table.rows) {
|
|
1861
|
-
|
|
2026
|
+
if (row[lookupField] == null) continue;
|
|
2027
|
+
const key = flatLookupKey(row[lookupField], spec.style);
|
|
1862
2028
|
if (!groups.has(key)) groups.set(key, []);
|
|
1863
2029
|
groups.get(key).push(row);
|
|
1864
2030
|
}
|
|
1865
2031
|
return {
|
|
1866
2032
|
type: 'FeatureCollection',
|
|
1867
2033
|
features: geomRt.features.map(f => {
|
|
1868
|
-
const rows = groups.get(String(f.properties[idField]));
|
|
2034
|
+
const rows = f.properties[idField] == null ? null : groups.get(String(f.properties[idField]));
|
|
1869
2035
|
const properties = rows ? Object.assign({}, rows[0]) : {};
|
|
1870
2036
|
if (rows) Object.defineProperty(properties, AGGREGATED_ROWS, { value: rows });
|
|
1871
2037
|
return { type: 'Feature', geometry: f.geometry, properties, _flatAreaKm2: flatShapeAreaKm2(geomRt, f) };
|
|
1872
2038
|
})
|
|
1873
2039
|
};
|
|
1874
2040
|
}
|
|
1875
|
-
|
|
2041
|
+
// the data side's key as flat makes it (lookupdigits/toupper/tonumber),
|
|
2042
|
+
// the geometry id as is — the same rule as joinChartPositions
|
|
2043
|
+
// a missing key on either side joins nothing (not "undefined" to "undefined")
|
|
2044
|
+
const rowsByKey = new Map(table.rows.filter(row => row[lookupField] != null).map(row => [flatLookupKey(row[lookupField], spec.style), row]));
|
|
1876
2045
|
// Unmatched polygons (no CSV row for that id) keep their geometry with
|
|
1877
2046
|
// empty properties — real-world data: not every comune necessarily has
|
|
1878
2047
|
// a row in every demographic source. Renders as "no data" (see
|
|
@@ -1881,7 +2050,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
1881
2050
|
const features = geomRt.features.map(f => ({
|
|
1882
2051
|
type: 'Feature',
|
|
1883
2052
|
geometry: f.geometry,
|
|
1884
|
-
properties: rowsByKey.get(String(f.properties[idField])) || {},
|
|
2053
|
+
properties: (f.properties[idField] != null && rowsByKey.get(String(f.properties[idField]))) || {},
|
|
1885
2054
|
_flatAreaKm2: flatShapeAreaKm2(geomRt, f)
|
|
1886
2055
|
}));
|
|
1887
2056
|
return { type: 'FeatureCollection', features };
|
|
@@ -2294,13 +2463,17 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
2294
2463
|
// this list; the rest is still recognized-but-inert
|
|
2295
2464
|
const KNOWN_INERT_FLAGS = ['CLIPTOGEOBOUNDS', 'OUTLIER',
|
|
2296
2465
|
'POW2', 'POW3', 'NOSORT'];
|
|
2297
|
-
// flat's PIE / DONUT variants with no rendering here (see _buildPieLayers)
|
|
2298
|
-
|
|
2466
|
+
// flat's PIE / DONUT variants with no rendering here (see _buildPieLayers).
|
|
2467
|
+
// STARBURST / FLOWER / RAYS (the starburst rays, with the XLRAYS & kin
|
|
2468
|
+
// angle/gap variants) and POLAR (its reference circles) ARE drawn — see
|
|
2469
|
+
// pieSliceLayout; the rest is still recognized-but-inert
|
|
2470
|
+
const PIE_INERT_FLAGS = [/3D/, /VOLUME/, /HEIGHT/, /BOW/, /ZOOM/, /DIRECTION/, /DIRECTED/];
|
|
2299
2471
|
// ... and of AGGREGATE / CATEGORICAL pies: GRIDSIZE's pie the size of its
|
|
2300
|
-
// grid cell (drawn by the SIZE rule instead), AUTOSIZE, DOPACITY,
|
|
2301
|
-
//
|
|
2302
|
-
// value arithmetic (DIFFERENCE, FRACTION, PERCENT …, field100)
|
|
2303
|
-
|
|
2472
|
+
// grid cell (drawn by the SIZE rule instead), AUTOSIZE, DOPACITY,
|
|
2473
|
+
// GAUGE, MULTIPLE / DIFFUSE, the post-aggregation
|
|
2474
|
+
// value arithmetic (DIFFERENCE, FRACTION, PERCENT …, field100).
|
|
2475
|
+
// The chart BOX and TITLE ARE drawn (see _buildPieLayers)
|
|
2476
|
+
const PIE_AGGREGATE_INERT_FLAGS = [/^GRIDSIZE$/, /AUTOSIZE/, /DOPACITY/, /^GAUGE$/, /^MULTIPLE$/, /^DIFFUSE$/,
|
|
2304
2477
|
/^DIFFERENCE$/, /^FRACTION$/, /^PERCENT$/, /^PERMILLE$/, /^PRODUCT$/, /^CALCVAL$/, /^RELATIVE$/];
|
|
2305
2478
|
// flat's BAR / BARS variants with no rendering here (see flatBarLayout):
|
|
2306
2479
|
// the bar is drawn plain (SMALLARROW as ARROW) — tested as substrings,
|
|
@@ -2310,11 +2483,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
2310
2483
|
/^DIFFERENCE$/, /^FRACTION$/, /^PERCENT$/, /^PERMILLE$/, /^PRODUCT$/, /^CALCVAL$/, /^RELATIVE$/];
|
|
2311
2484
|
// flat's VECTOR / BEZIER modifiers with no rendering here (see
|
|
2312
2485
|
// _buildVectorLayers), the word → what happens instead
|
|
2486
|
+
// (DASH IS drawn — the animated flow, see makeFlowPathLayer)
|
|
2313
2487
|
const VECTOR_INERT_FLAGS = {
|
|
2314
2488
|
VALUES: 'the value text along the curve is not drawn yet',
|
|
2315
2489
|
DYNAMICWIDTH: 'not drawn yet (flat widens by the log of the length in its own map units) — the plain width instead',
|
|
2316
|
-
LONG: 'not drawn yet (its bow depends on flat\'s own map units) — the default bow instead'
|
|
2317
|
-
DASH: 'drawn as static dashes — flat\'s dash animation is not ported'
|
|
2490
|
+
LONG: 'not drawn yet (its bow depends on flat\'s own map units) — the default bow instead'
|
|
2318
2491
|
};
|
|
2319
2492
|
const _warnedFlags = new Set();
|
|
2320
2493
|
|
|
@@ -2359,6 +2532,18 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
2359
2532
|
// makes while the map is still building wait for it
|
|
2360
2533
|
let _resolveMapReady;
|
|
2361
2534
|
const _mapReady = new Promise(r => { _resolveMapReady = r; });
|
|
2535
|
+
// a map declared with an options name (Map(id, {name})) → the deferred of
|
|
2536
|
+
// ITS build: ixmaps.map("name") calls made while it is still building wait
|
|
2537
|
+
// for that map, not whichever map is built first
|
|
2538
|
+
const _namedMapReady = Object.create(null);
|
|
2539
|
+
function namedMapReady(name) {
|
|
2540
|
+
if (!_namedMapReady[name]) {
|
|
2541
|
+
let resolve;
|
|
2542
|
+
const promise = new Promise(r => { resolve = r; });
|
|
2543
|
+
_namedMapReady[name] = { promise, resolve };
|
|
2544
|
+
}
|
|
2545
|
+
return _namedMapReady[name];
|
|
2546
|
+
}
|
|
2362
2547
|
|
|
2363
2548
|
// flat's ixmaps.require(url) fetches the script and evals it
|
|
2364
2549
|
// (htmlgui.js 3911-3934); here each one runs globally, in order, before
|
|
@@ -2478,7 +2663,16 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
2478
2663
|
// key wins. Always into a new object — the page's own objects are never
|
|
2479
2664
|
// mutated. Aliases (geo → position, ...) are resolved in normalizeTheme.
|
|
2480
2665
|
binding(b) {
|
|
2481
|
-
|
|
2666
|
+
// flat's toString joins an array with '|' (maptheme.js 1014): a
|
|
2667
|
+
// binding given as an ARRAY — .binding({value: ["out","in"]}, the
|
|
2668
|
+
// pendolari page's saldo pie) is a MULTI-FIELD binding, not one
|
|
2669
|
+
// field named "out,in" (which would find no column and draw
|
|
2670
|
+
// nothing). Normalized once, here, for every consumer that
|
|
2671
|
+
// splits binding.value on '|'
|
|
2672
|
+
const norm = {};
|
|
2673
|
+
// (an empty or missing argument keeps what earlier calls set)
|
|
2674
|
+
for (const [k, v] of Object.entries(b || {})) norm[k] = Array.isArray(v) ? v.join('|') : v;
|
|
2675
|
+
this._binding = Object.assign({}, this._binding, norm);
|
|
2482
2676
|
return this;
|
|
2483
2677
|
}
|
|
2484
2678
|
filter(expr) { this._filterExpr = expr; return this; }
|
|
@@ -2768,27 +2962,75 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
2768
2962
|
// that kind: the data processing function (data.process /
|
|
2769
2963
|
// style.dbtableProcess), the broker query (data.query /
|
|
2770
2964
|
// style.dbtableQuery — without it a data.name still reaches a
|
|
2771
|
-
// provider the PAGE defined as ixmaps[name]) and a colorscheme
|
|
2772
|
-
//
|
|
2773
|
-
// (
|
|
2965
|
+
// provider the PAGE defined as ixmaps[name]) and a colorscheme
|
|
2966
|
+
// resolveColorScheme would evaluate: a string, or ["byvalue", "<fn>"]
|
|
2967
|
+
// (any other array colorscheme is data and stays). normalizeTheme
|
|
2968
|
+
// merges meta INTO style, so every style key is checked under meta too.
|
|
2969
|
+
const isColorSchemeCode = v => typeof v === 'string'
|
|
2970
|
+
|| (Array.isArray(v) && v[0] === 'byvalue' && typeof v[1] === 'string');
|
|
2774
2971
|
const PROJECT_CODE_KEYS = [
|
|
2775
2972
|
['data', 'process', 'data.process function'],
|
|
2776
2973
|
['style', 'dbtableProcess', 'data.process function'],
|
|
2777
2974
|
['data', 'query', 'data.query function'],
|
|
2778
2975
|
['style', 'dbtableQuery', 'data.query function'],
|
|
2779
|
-
['style', 'colorscheme', 'colorscheme function',
|
|
2780
|
-
];
|
|
2976
|
+
['style', 'colorscheme', 'colorscheme function', isColorSchemeCode],
|
|
2977
|
+
].flatMap(k => (k[0] === 'style' ? [k, ['meta', ...k.slice(1)]] : [k]));
|
|
2978
|
+
// A project's legend texts (title, snippet, description — in style or
|
|
2979
|
+
// meta, normalizeTheme fills meta from either) are shown as HTML, as on
|
|
2980
|
+
// flat: citation links in a description are real. A project's markup is
|
|
2981
|
+
// cleaned to formatting tags and plain attributes so nothing in it runs;
|
|
2982
|
+
// a page's own .meta()/.style() HTML stays as given.
|
|
2983
|
+
const PROJECT_HTML_KEYS = ['title', 'snippet', 'description'];
|
|
2984
|
+
const PROJECT_HTML_DROP = new Set(['SCRIPT', 'STYLE', 'IFRAME', 'FRAME', 'FRAMESET', 'OBJECT', 'EMBED', 'SVG', 'MATH',
|
|
2985
|
+
'TEMPLATE', 'NOSCRIPT', 'LINK', 'META', 'BASE', 'FORM', 'INPUT', 'BUTTON', 'SELECT', 'TEXTAREA', 'AUDIO', 'VIDEO', 'SOURCE']);
|
|
2986
|
+
const PROJECT_HTML_TAGS = new Set(['A', 'B', 'I', 'EM', 'STRONG', 'U', 'S', 'SMALL', 'SUB', 'SUP', 'BR', 'P', 'DIV', 'SPAN',
|
|
2987
|
+
'UL', 'OL', 'LI', 'HR', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'IMG', 'TABLE', 'THEAD', 'TBODY', 'TR', 'TD', 'TH', 'CODE', 'BLOCKQUOTE']);
|
|
2988
|
+
const PROJECT_HTML_ATTRS = new Set(['href', 'src', 'alt', 'title', 'target', 'rel', 'class', 'style', 'width', 'height', 'colspan', 'rowspan']);
|
|
2989
|
+
function sanitizeProjectHtml(html) {
|
|
2990
|
+
const s = String(html);
|
|
2991
|
+
if (!/[<&]/.test(s)) return s;
|
|
2992
|
+
// no DOM to parse with (Node): the text, escaped
|
|
2993
|
+
if (typeof global.DOMParser !== 'function') return escapeHtml(s);
|
|
2994
|
+
// an inert document: nothing in it runs or loads while it is cleaned
|
|
2995
|
+
const body = new global.DOMParser().parseFromString('<!doctype html><body>' + s, 'text/html').body;
|
|
2996
|
+
for (const el of Array.from(body.querySelectorAll('*'))) {
|
|
2997
|
+
if (PROJECT_HTML_DROP.has(el.tagName.toUpperCase())) { el.remove(); continue; }
|
|
2998
|
+
if (!PROJECT_HTML_TAGS.has(el.tagName.toUpperCase())) { el.replaceWith(...el.childNodes); continue; }
|
|
2999
|
+
for (const a of Array.from(el.attributes)) {
|
|
3000
|
+
const name = a.name.toLowerCase();
|
|
3001
|
+
if (!PROJECT_HTML_ATTRS.has(name)
|
|
3002
|
+
|| ((name === 'href' || name === 'src') && /^(javascript|vbscript|data):/i.test(a.value.replace(/[\s\u0000-\u001f]/g, '')))) {
|
|
3003
|
+
el.removeAttribute(a.name);
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
return body.innerHTML;
|
|
3008
|
+
}
|
|
3009
|
+
|
|
2781
3010
|
function withoutProjectCode(t, report) {
|
|
2782
3011
|
const found = PROJECT_CODE_KEYS.filter(([part, key, , isCode]) =>
|
|
2783
3012
|
t[part] && t[part][key] != null && (!isCode || isCode(t[part][key])));
|
|
2784
|
-
|
|
3013
|
+
const html = [];
|
|
3014
|
+
for (const part of ['style', 'meta']) {
|
|
3015
|
+
for (const key of PROJECT_HTML_KEYS) {
|
|
3016
|
+
const v = t[part] && t[part][key];
|
|
3017
|
+
if (typeof v !== 'string') continue;
|
|
3018
|
+
const clean = sanitizeProjectHtml(v);
|
|
3019
|
+
if (clean !== v) html.push([part, key, clean]);
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
if (!found.length && !html.length) return t;
|
|
2785
3023
|
const copy = Object.assign({}, t);
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
3024
|
+
const own = part => { if (copy[part] === t[part]) copy[part] = Object.assign({}, t[part]); return copy[part]; };
|
|
3025
|
+
found.forEach(([part, key]) => { delete own(part)[key]; });
|
|
3026
|
+
html.forEach(([part, key, clean]) => { own(part)[key] = clean; });
|
|
3027
|
+
if (found.length) {
|
|
3028
|
+
const what = [...new Set(found.map(f => f[2]))].join(', ');
|
|
3029
|
+
report.notes.push(`theme "${t.layer}": the project's ${what} is never run by ixmaps-gl`);
|
|
3030
|
+
}
|
|
3031
|
+
if (html.length) {
|
|
3032
|
+
report.notes.push(`theme "${t.layer}": the project's ${[...new Set(html.map(h => h[1]))].join(', ')} HTML is cleaned to formatting markup by ixmaps-gl`);
|
|
3033
|
+
}
|
|
2792
3034
|
return copy;
|
|
2793
3035
|
}
|
|
2794
3036
|
|
|
@@ -3367,7 +3609,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3367
3609
|
map.setPaintProperty(id, 'background-opacity', opacity);
|
|
3368
3610
|
} catch (e) { console.warn('[ixmaps-gl] the {type:"Sphere"} ocean backdrop could not be set:', e); }
|
|
3369
3611
|
};
|
|
3370
|
-
|
|
3612
|
+
// isStyleLoaded() is also false AFTER the style loaded while tiles or
|
|
3613
|
+
// images load (every pan/zoom) — 'style.load' never fires again then, so
|
|
3614
|
+
// a Sphere theme defined at such a moment would never get its color
|
|
3615
|
+
if (map.isStyleLoaded() || (map.style && map.style._loaded)) paint(); else map.once('style.load', paint);
|
|
3371
3616
|
}
|
|
3372
3617
|
|
|
3373
3618
|
function buildBlankBackgroundStyle(color) {
|
|
@@ -3516,7 +3761,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
3516
3761
|
// visible (an element larger than the viewport ends up at its edge)
|
|
3517
3762
|
function keepTooltipVisible(tipEl, x, y) {
|
|
3518
3763
|
const w = tipEl.offsetWidth, h = tipEl.offsetHeight;
|
|
3519
|
-
|
|
3764
|
+
// x/y and the translate are relative to the map container (it clips,
|
|
3765
|
+
// overflow:hidden): its size is the room, not the window's — they
|
|
3766
|
+
// differ on an embedded map
|
|
3767
|
+
const vw = el.clientWidth || global.innerWidth || 1200, vh = el.clientHeight || global.innerHeight || 800;
|
|
3520
3768
|
let tx = x > vw / 2 ? x - w - 30 : x + 30;
|
|
3521
3769
|
let ty = y > vh / 2 ? y - h - 20 : y + 20;
|
|
3522
3770
|
tx = Math.min(Math.max(8, tx), Math.max(8, vw - w - 8));
|
|
@@ -4116,13 +4364,17 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4116
4364
|
// twin pages)
|
|
4117
4365
|
const flatUnitLegend = rt._isVectorChart() || rt._isAggregatedPie();
|
|
4118
4366
|
const rawLegendUnit = rt.style.legendunits || rt.style.units || '';
|
|
4119
|
-
|
|
4367
|
+
// COUNT: the rows are record counts — flat prints them as whole numbers
|
|
4368
|
+
// with no unit (legend.js 705, 810: __formatValue(count, nDecimals,
|
|
4369
|
+
// "BLANK") + (COUNT ? "" : " " + unit))
|
|
4370
|
+
const countLegend = flatFlag(rt.flags, 'COUNT') && !useSum;
|
|
4371
|
+
const legendUnit = countLegend ? '' : flatUnitLegend && rawLegendUnit && rawLegendUnit[0] !== '.' && rawLegendUnit[0] !== ' ' ? ' ' + rawLegendUnit : rawLegendUnit;
|
|
4120
4372
|
// a per-item chart over several value fields: the row values as
|
|
4121
4373
|
// flat's legend prints them (legend.js 610, 810: __formatValue
|
|
4122
4374
|
// with valuedecimals, else 2, "BLANK" — whole numbers without
|
|
4123
4375
|
// decimals), a colorscheme "none" part as an outline in linecolor
|
|
4124
4376
|
const multiFieldLegend = rt._isMultiFieldChart();
|
|
4125
|
-
const legendTotalText = v => ((multiFieldLegend || flatUnitLegend)
|
|
4377
|
+
const legendTotalText = v => ((multiFieldLegend || flatUnitLegend || countLegend)
|
|
4126
4378
|
? flatFormatValue(v, legendDecimals(rt.style), 'BLANK')
|
|
4127
4379
|
: rt._formatTooltipValue(v));
|
|
4128
4380
|
const outlineRgb = rt.style.linecolor && styleLineColor(rt.style.linecolor) !== 'none' ? hexOrNamedToRgb(styleLineColor(rt.style.linecolor)) : [128, 128, 128];
|
|
@@ -4425,7 +4677,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4425
4677
|
// same 400ms as the facets sidebar's own onRedraw use), same
|
|
4426
4678
|
// signal already used elsewhere in this file for "the visible
|
|
4427
4679
|
// data just changed, recompute".
|
|
4428
|
-
|
|
4680
|
+
// a panel replaced (rebuild, legend off) or a removed theme: the
|
|
4681
|
+
// listener goes with it — else one per rebuild piles up, each
|
|
4682
|
+
// recomputing a stale runtime on every redraw
|
|
4683
|
+
const offRedraw = engineApi.onRedraw(() => {
|
|
4684
|
+
if (rt._legendPanel !== panel || !runtimes.includes(rt)) { offRedraw(); return; }
|
|
4685
|
+
computeTotals(); renderRows();
|
|
4686
|
+
});
|
|
4429
4687
|
}
|
|
4430
4688
|
|
|
4431
4689
|
// rt._triggerRedraw: see its own doc comment on LayerRuntime
|
|
@@ -4740,7 +4998,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4740
4998
|
}
|
|
4741
4999
|
return this;
|
|
4742
5000
|
}
|
|
4743
|
-
options(
|
|
5001
|
+
// merged, as flat's options() sets each key on its own (ixmaps.setOptions)
|
|
5002
|
+
// and the live map's options() below does — .options({a}).options({b})
|
|
5003
|
+
// keeps a
|
|
5004
|
+
options(o) { if (o && typeof o === 'object') Object.assign(this._engineOptions, o); return this; }
|
|
4744
5005
|
local(...args) { this._locals.push(args); return this; }
|
|
4745
5006
|
// flat's .require(url) (htmlgui.js ixmaps.require): a script the page
|
|
4746
5007
|
// needs (a chart library, a user chart) — page code, like a <script>
|
|
@@ -4793,6 +5054,12 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4793
5054
|
// the Map(id, opts, map => map.options(...)) callback form worked).
|
|
4794
5055
|
// One microtask lets the caller's whole synchronous chain land first.
|
|
4795
5056
|
await null;
|
|
5057
|
+
// the DASH flow animation's repaint loop (see refreshLayers): MapLibre
|
|
5058
|
+
// renders on demand only, so the shader's wall-clock dash (see
|
|
5059
|
+
// makeFlowPathLayer) needs a continuous frame request while any flow
|
|
5060
|
+
// layer is on screen — one handle per map, or another map's refresh
|
|
5061
|
+
// cancels (or blocks) this map's loop
|
|
5062
|
+
let _flowRepaintRAF = null;
|
|
4796
5063
|
let el = document.getElementById(this.containerId);
|
|
4797
5064
|
if (!el) {
|
|
4798
5065
|
// flat's own fallback for a missing target div (htmlgui_flat.js
|
|
@@ -4812,7 +5079,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4812
5079
|
el = document.createElement('div');
|
|
4813
5080
|
el.id = this.containerId;
|
|
4814
5081
|
el.style.width = (this.mapOptions && this.mapOptions.width) ? String(this.mapOptions.width) : '100%';
|
|
4815
|
-
|
|
5082
|
+
// a percentage height would collapse against a body without a
|
|
5083
|
+
// height of its own (the blank-screen case): the window's
|
|
5084
|
+
// innerHeight instead, like flat's fullscreen fallback
|
|
5085
|
+
el.style.height = (this.mapOptions && this.mapOptions.height) ? String(this.mapOptions.height)
|
|
5086
|
+
: (global.innerHeight || 800) + 'px';
|
|
4816
5087
|
document.body.appendChild(el);
|
|
4817
5088
|
}
|
|
4818
5089
|
// flat's init replaces the container's content with its own map page
|
|
@@ -4836,13 +5107,22 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4836
5107
|
else if (typeof v === 'string' && /^\s*\d+(\.\d+)?\s*%\s*$/.test(v)
|
|
4837
5108
|
&& !(dim === 'width' ? el.clientWidth : el.clientHeight)) el.style[dim] = v.trim();
|
|
4838
5109
|
}
|
|
4839
|
-
// without
|
|
5110
|
+
// without height flat's map is "fullscreen" (htmlgui_flat.js
|
|
4840
5111
|
// 847-876): it fills the window, whatever the page's CSS gives the
|
|
4841
|
-
// container —
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
5112
|
+
// container. PER SIDE — also when only a width is given (the World
|
|
5113
|
+
// Dataviz Prize pages: width 100%, no height), where the old
|
|
5114
|
+
// both-sides gate left the container collapsed at 0 (a blank
|
|
5115
|
+
// screen): no height option and no effective CSS height gets the
|
|
5116
|
+
// window's innerHeight, kept current on window resizes (MapLibre
|
|
5117
|
+
// follows its container's own size)
|
|
5118
|
+
if (!(this.mapOptions && this.mapOptions.height) && !el.clientHeight) {
|
|
5119
|
+
this._heightFallbackToWindow = true;
|
|
5120
|
+
el.style.height = (global.innerHeight || 800) + 'px';
|
|
5121
|
+
global.addEventListener('resize', () => {
|
|
5122
|
+
if (this._heightFallbackToWindow) el.style.height = (global.innerHeight || 800) + 'px';
|
|
5123
|
+
});
|
|
4845
5124
|
}
|
|
5125
|
+
if (!(this.mapOptions && this.mapOptions.width) && !el.clientWidth) el.style.width = '100%';
|
|
4846
5126
|
|
|
4847
5127
|
// shown for the whole build() (library lazy-load + every layer's own
|
|
4848
5128
|
// data fetch, real-world verified as the two slowest phases) through
|
|
@@ -4869,8 +5149,6 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
4869
5149
|
const svgProjection = flatSvgProjectionOf(this.mapOptions.map)
|
|
4870
5150
|
|| (isFlatOrthographicName(this.mapOptions.mapProjection) ? FLAT_ORTHOGRAPHIC_PROJECTION : null);
|
|
4871
5151
|
if (svgProjection) flatSvgProjectionByOptions.set(this._engineOptions, svgProjection);
|
|
4872
|
-
const planeProjection = flatPlaneProjectionOf(this.mapOptions);
|
|
4873
|
-
if (planeProjection) flatPlaneProjectionByOptions.set(this._engineOptions, planeProjection);
|
|
4874
5152
|
if (isFlatOrthographicName(this.mapOptions.mapProjection)) {
|
|
4875
5153
|
flatOrthographicByOptions.set(this._engineOptions, true);
|
|
4876
5154
|
if (this._viewCenter) flatViewCenterByOptions.set(this._engineOptions, { lat: this._viewCenter[0], lng: this._viewCenter[1] });
|
|
@@ -5175,7 +5453,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5175
5453
|
// a page legend (map.legend(html)) is positioned by the map's
|
|
5176
5454
|
// own align option — the same rule that places the native stack
|
|
5177
5455
|
legendEl.style.cssText = 'position:absolute;' + legendAlignCssFor(this.mapOptions) + 'z-index:5;pointer-events:none';
|
|
5178
|
-
|
|
5456
|
+
// flat's legend takes the pane's html OR a URL to it (htmlgui):
|
|
5457
|
+
// a value without a tag is fetched, its html shown
|
|
5458
|
+
if (/<[a-z!]/i.test(this._legendHtml)) legendEl.innerHTML = this._legendHtml;
|
|
5459
|
+
else fetch(this._legendHtml).then(r => r.ok ? r.text() : '').then(t => { legendEl.innerHTML = t; }).catch(() => {});
|
|
5179
5460
|
// inside the map container, never the page's body (see the pinned
|
|
5180
5461
|
// tooltip mount) — styling body breaks page-positioned elements
|
|
5181
5462
|
anchorInside(el);
|
|
@@ -5550,7 +5831,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5550
5831
|
// called after every redraw (pan/zoom, or any of the setters above)
|
|
5551
5832
|
// so a page can keep a facets sidebar in sync, mirroring the real
|
|
5552
5833
|
// engine's htmlgui_onDrawTheme hook
|
|
5553
|
-
|
|
5834
|
+
// returns an unsubscribe (the legend drops its listener with its panel)
|
|
5835
|
+
onRedraw: (cb) => {
|
|
5836
|
+
redrawListeners.push(cb);
|
|
5837
|
+
return () => { const i = redrawListeners.indexOf(cb); if (i >= 0) redrawListeners.splice(i, 1); };
|
|
5838
|
+
},
|
|
5554
5839
|
refresh: () => refresh(),
|
|
5555
5840
|
// Adds ONE new layer/theme to an ALREADY-BUILT map — build()'s own
|
|
5556
5841
|
// loop above only ever runs once, at construction, so a caller
|
|
@@ -5700,7 +5985,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
5700
5985
|
// layers — leaving its groups mounted keeps the charts on the
|
|
5701
5986
|
// map after map.remove()/replace() (censimenti's sidebar)
|
|
5702
5987
|
if (rt._clearUserChartPane) rt._clearUserChartPane();
|
|
5988
|
+
if (rt._chartPaneDefs) { rt._chartPaneDefs.remove(); rt._chartPaneDefs = null; }
|
|
5703
5989
|
if (rt._chartHost && rt._chartHost.runtimes) rt._chartHost.runtimes.delete(rt);
|
|
5990
|
+
// and its hidden <name>_init group (userChartHost, on the body)
|
|
5991
|
+
if (rt._userChartGroup) { rt._userChartGroup.remove(); rt._userChartGroup = null; }
|
|
5704
5992
|
});
|
|
5705
5993
|
const removed = removedRuntimes.length > 0;
|
|
5706
5994
|
// stale-registry guard: _globalThemeRegistry is a bare name->
|
|
@@ -6002,8 +6290,6 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6002
6290
|
const svgProjection = flatSvgProjectionOf(svg) || (/orthographic\.svg$/i.test(svg) ? FLAT_ORTHOGRAPHIC_PROJECTION : null);
|
|
6003
6291
|
if (svgProjection) flatSvgProjectionByOptions.set(builder._engineOptions, svgProjection);
|
|
6004
6292
|
else flatSvgProjectionByOptions.delete(builder._engineOptions);
|
|
6005
|
-
if (svgProjection && svgProjection.invert) flatPlaneProjectionByOptions.set(builder._engineOptions, svgProjection);
|
|
6006
|
-
else flatPlaneProjectionByOptions.delete(builder._engineOptions);
|
|
6007
6293
|
if (/orthographic\.svg$/i.test(svg)) flatOrthographicByOptions.set(builder._engineOptions, true);
|
|
6008
6294
|
else { flatOrthographicByOptions.delete(builder._engineOptions); flatViewCenterByOptions.delete(builder._engineOptions); }
|
|
6009
6295
|
}
|
|
@@ -6174,6 +6460,22 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6174
6460
|
if (order !== refreshLayers._lastOrder) { refreshLayers._lastOrder = order; console.info('[ixmaps-gl debug] draw order (bottom → top): ' + order); }
|
|
6175
6461
|
}
|
|
6176
6462
|
overlay.setProps({ layers });
|
|
6463
|
+
// start/stop the flow repaint loop: any layer whose paths carry
|
|
6464
|
+
// dash offsets animates, and only then (a static map renders
|
|
6465
|
+
// nothing new; the loop is one rAF, one triggerRepaint each)
|
|
6466
|
+
const flowOn = layers.some(l => l.constructor.layerName === 'FlowPathLayer'
|
|
6467
|
+
&& l.props.data && l.props.data.some(d => d.dash && d.dash[0] + d.dash[1] > 0));
|
|
6468
|
+
if (glDebug()) console.info('[ixmaps-gl debug] flow loop on: ' + flowOn + ' (' + layers.filter(l => l.constructor.layerName === 'FlowPathLayer').length + ' flow layers, '
|
|
6469
|
+
+ (layers.find(l => l.constructor.layerName === 'FlowPathLayer') || {props: {data: []}}).props.data.length + ' paths)');
|
|
6470
|
+
if (flowOn) {
|
|
6471
|
+
if (_flowRepaintRAF == null) {
|
|
6472
|
+
const loop = () => { if (map.triggerRepaint) map.triggerRepaint(); _flowRepaintRAF = global.requestAnimationFrame(loop); };
|
|
6473
|
+
_flowRepaintRAF = global.requestAnimationFrame(loop);
|
|
6474
|
+
}
|
|
6475
|
+
} else if (_flowRepaintRAF != null) {
|
|
6476
|
+
global.cancelAnimationFrame(_flowRepaintRAF);
|
|
6477
|
+
_flowRepaintRAF = null;
|
|
6478
|
+
}
|
|
6177
6479
|
// flat's WMS themes render as native raster layers, not deck.gl —
|
|
6178
6480
|
// reconcile them with the current runtimes on every rebuild
|
|
6179
6481
|
syncWmsLayers();
|
|
@@ -6334,7 +6636,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6334
6636
|
} finally { inDrawThemeHook = false; }
|
|
6335
6637
|
}
|
|
6336
6638
|
function notifyRedraw() {
|
|
6337
|
-
redrawListeners.forEach(cb => { try { cb(); } catch (err) { console.error('[ixmaps-gl] onRedraw callback failed:', err); } });
|
|
6639
|
+
redrawListeners.slice().forEach(cb => { try { cb(); } catch (err) { console.error('[ixmaps-gl] onRedraw callback failed:', err); } });
|
|
6338
6640
|
callDrawThemeHooks();
|
|
6339
6641
|
}
|
|
6340
6642
|
|
|
@@ -6516,6 +6818,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6516
6818
|
};
|
|
6517
6819
|
}
|
|
6518
6820
|
_resolveMapReady(engineApi);
|
|
6821
|
+
if (this.mapOptions && this.mapOptions.name) namedMapReady(this.mapOptions.name).resolve(engineApi);
|
|
6519
6822
|
resolveBuilt();
|
|
6520
6823
|
namedLoads.forEach(rt => { listenNamedData(rt); if (!rt._deferredLoad) loadNamedTheme(rt, false); });
|
|
6521
6824
|
return engineApi;
|
|
@@ -6547,6 +6850,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6547
6850
|
// class used throughout this file — a real bug caught during testing
|
|
6548
6851
|
// (every `new Map()` below silently resolved to *this* function instead)
|
|
6549
6852
|
function createMap(containerId, options, mapFn) {
|
|
6853
|
+
if (options && options.name) namedMapReady(options.name);
|
|
6550
6854
|
const builder = new MapBuilder(containerId, options);
|
|
6551
6855
|
if (mapFn) mapFn(builder);
|
|
6552
6856
|
// If build() rejects (bad data URL, misconfigured CHOROPLETH join,
|
|
@@ -6614,7 +6918,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6614
6918
|
// flat's mapApi.setView(center, zoom) (htmlgui_flat.js 1327) — the
|
|
6615
6919
|
// same live-or-build-time split as .view() above
|
|
6616
6920
|
promise.setView = (...args) => promise.view(...args);
|
|
6617
|
-
|
|
6921
|
+
// post-build: the live map's options() (merged into the options
|
|
6922
|
+
// object the running themes hold), as .view() above — the builder's
|
|
6923
|
+
// own is read once, by build()
|
|
6924
|
+
promise.options = (...args) => { if (resolvedApi) resolvedApi.options(...args); else builder.options(...args); return promise; };
|
|
6618
6925
|
promise.attribution = (...args) => { builder.attribution(...args); return promise; };
|
|
6619
6926
|
promise.legend = (...args) => { builder.legend(...args); return promise; };
|
|
6620
6927
|
promise.local = (...args) => { builder.local(...args); return promise; };
|
|
@@ -6838,11 +7145,6 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6838
7145
|
// the flat SVG projection a map's options object belongs to (set by the
|
|
6839
7146
|
// map's init and loadProject; the runtimes only see the options object)
|
|
6840
7147
|
const flatSvgProjectionByOptions = new WeakMap();
|
|
6841
|
-
// the projection plane a map's VECTOR / BEZIER flows are drawn in: flat's
|
|
6842
|
-
// map file, or the Map() option mapProjection ("equalearth") — flat computes
|
|
6843
|
-
// the curves in its map's own coordinates, so the same flow bows around the
|
|
6844
|
-
// pole there. (Unlike flatSvgProjectionByOptions it leaves the view framing alone.)
|
|
6845
|
-
const flatPlaneProjectionByOptions = new WeakMap();
|
|
6846
7148
|
// a map that is flat's orthographic one (drawn as MapLibre's globe): its object scale is
|
|
6847
7149
|
// flat's (scale × the globe's pixels per degree at its center, measured on flat 2026-10-10
|
|
6848
7150
|
// on the mechanical-goods globe page, the same at view zooms 2.5 and 3.5: 197.44e6; the
|
|
@@ -6885,12 +7187,6 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
6885
7187
|
const radius = 512 * Math.pow(2, zoom) / (2 * Math.PI * c);
|
|
6886
7188
|
return Math.log2(globeDiscRadius(radius, height > 0 ? height : 768) * 2 * Math.PI / 512);
|
|
6887
7189
|
}
|
|
6888
|
-
function flatPlaneProjectionOf(mapOptions) {
|
|
6889
|
-
const byUrl = flatSvgProjectionOf(mapOptions.map);
|
|
6890
|
-
if (byUrl) return byUrl;
|
|
6891
|
-
const named = FLAT_SVG_PROJECTIONS[String(mapOptions.mapProjection || '').trim().toLowerCase()];
|
|
6892
|
-
return named || null;
|
|
6893
|
-
}
|
|
6894
7190
|
// flat SVG map scale ÷ the Mercator one at the same on-screen size (1
|
|
6895
7191
|
// without a flat SVG projection)
|
|
6896
7192
|
function flatMapScaleRatio(mapOptions) {
|
|
@@ -7678,8 +7974,8 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
7678
7974
|
_measureCtx.font = fontPx + 'px arial';
|
|
7679
7975
|
return _measureCtx.measureText(text).width;
|
|
7680
7976
|
}
|
|
7681
|
-
function wrapTitleLines(text, fontPx, widthPx) {
|
|
7682
|
-
const width = w =>
|
|
7977
|
+
function wrapTitleLines(text, fontPx, widthPx, measure = titleTextWidth) {
|
|
7978
|
+
const width = w => measure(w, fontPx);
|
|
7683
7979
|
const lines = [];
|
|
7684
7980
|
for (const word of String(text).split(/\s+/).filter(Boolean)) {
|
|
7685
7981
|
const last = lines.length ? lines[lines.length - 1] + ' ' + word : null;
|
|
@@ -8629,39 +8925,55 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
8629
8925
|
return { bars, texts, lines, extent: ext[0] <= ext[2] ? ext : [0, 0, 0, 0], width: W, sizer };
|
|
8630
8926
|
}
|
|
8631
8927
|
|
|
8632
|
-
//
|
|
8633
|
-
//
|
|
8634
|
-
//
|
|
8635
|
-
//
|
|
8636
|
-
//
|
|
8637
|
-
//
|
|
8638
|
-
//
|
|
8639
|
-
//
|
|
8640
|
-
|
|
8928
|
+
// THE box-and-title layout of every chart type (flat's BOX block,
|
|
8929
|
+
// maptheme.js 18757-18925): single symbol, SEQUENCE, PIE and BAR all call
|
|
8930
|
+
// this. `extent`: the chart's drawn [x0, y0, x1, y1] around its anchor (px;
|
|
8931
|
+
// chart units with unit 1). o: { unit (px per flat unit, default 1), margin
|
|
8932
|
+
// (boxmargin, flat's 2), circular (CIRCULARBOX), radius / center (the
|
|
8933
|
+
// circle's base radius and center; default the extent's half longer side
|
|
8934
|
+
// and its center), titleText (with the titleunits suffix, '' / null: no
|
|
8935
|
+
// title), titleFont (px, or w => px for DTEXT), bottomTitle, alignRight,
|
|
8936
|
+
// wrap, measure }. Returns { rect (rectangular box), boxR / cx / cy (the
|
|
8937
|
+
// circular box: radius, center), w (the extent's width), title { text,
|
|
8938
|
+
// lines, x, anchor, top, baseline, f, extent } }.
|
|
8939
|
+
// The margin is min(2 m u, m w / 30); the title's first baseline 0.7 font
|
|
8940
|
+
// above the box (BOTTOMTITLE: one font below), wrapped to the chart's width
|
|
8941
|
+
// (the circle's diameter); a rectangular box grows to enclose the title,
|
|
8942
|
+
// its top trimmed by 0.1 font. arial: ascent 0.905, descent 0.212.
|
|
8943
|
+
function chartBoxLayout(extent, o) {
|
|
8641
8944
|
const e = extent.slice();
|
|
8642
|
-
const w = e[2] - e[0];
|
|
8643
|
-
const
|
|
8945
|
+
const w = e[2] - e[0], h = e[3] - e[1];
|
|
8946
|
+
const unit = o.unit == null ? 1 : o.unit;
|
|
8947
|
+
const circular = !!o.circular;
|
|
8948
|
+
const margin = Math.min(2 * o.margin * unit, o.margin * w / 30);
|
|
8949
|
+
const center = o.center || [(e[0] + e[2]) / 2, (e[1] + e[3]) / 2];
|
|
8950
|
+
const boxR = circular ? (o.radius != null ? o.radius : Math.max(w, h) / 2) * 1.1 + margin : 0;
|
|
8644
8951
|
let title = null;
|
|
8645
|
-
if (o.
|
|
8646
|
-
const f = o.titleFont;
|
|
8647
|
-
const measure = o.measure ||
|
|
8648
|
-
const lines =
|
|
8649
|
-
for (const word of String(o.title).split(/\s+/).filter(Boolean)) {
|
|
8650
|
-
const last = lines.length ? lines[lines.length - 1] + ' ' + word : null;
|
|
8651
|
-
if (last !== null && measure(last, f) <= w) lines[lines.length - 1] = last;
|
|
8652
|
-
else lines.push(word);
|
|
8653
|
-
}
|
|
8952
|
+
if (o.titleText) {
|
|
8953
|
+
const f = typeof o.titleFont === 'function' ? o.titleFont(w) : o.titleFont;
|
|
8954
|
+
const measure = o.measure || titleTextWidth;
|
|
8955
|
+
const lines = wrapTitleLines(o.titleText, f, circular ? boxR * 2 : w, measure).split('\n');
|
|
8654
8956
|
const width = Math.max(...lines.map(l => measure(l, f)));
|
|
8655
|
-
|
|
8656
|
-
const
|
|
8657
|
-
|
|
8658
|
-
|
|
8659
|
-
|
|
8660
|
-
|
|
8661
|
-
|
|
8662
|
-
|
|
8957
|
+
// first baseline; lines go down one font each
|
|
8958
|
+
const base = circular
|
|
8959
|
+
? (o.bottomTitle ? center[1] + boxR + f : center[1] - boxR - 0.7 * f - (lines.length - 1) * f)
|
|
8960
|
+
: (o.bottomTitle ? e[3] + f : e[1] - 0.7 * f - (lines.length - 1) * f);
|
|
8961
|
+
const x = circular ? center[0] + unit / 2 : (o.alignRight ? e[2] : e[0]);
|
|
8962
|
+
const anchor = circular ? 'middle' : (o.alignRight ? 'end' : 'start');
|
|
8963
|
+
const x0 = anchor === 'middle' ? x - width / 2 : anchor === 'end' ? x - width : x;
|
|
8964
|
+
title = { text: lines.join('\n'), lines, x, anchor, top: base - 0.8 * f, baseline: base, f,
|
|
8965
|
+
extent: [x0, base - 0.905 * f, x0 + width, base + (lines.length - 1) * f + 0.212 * f] };
|
|
8663
8966
|
}
|
|
8664
|
-
|
|
8967
|
+
let rect = null;
|
|
8968
|
+
if (!circular) {
|
|
8969
|
+
const b = e.slice();
|
|
8970
|
+
if (title) {
|
|
8971
|
+
b[0] = Math.min(b[0], title.extent[0]); b[1] = Math.min(b[1], title.extent[1]) + 0.1 * title.f;
|
|
8972
|
+
b[2] = Math.max(b[2], title.extent[2]); b[3] = Math.max(b[3], title.extent[3]);
|
|
8973
|
+
}
|
|
8974
|
+
rect = [b[0] - margin, b[1] - margin, b[2] + margin, b[3] + margin];
|
|
8975
|
+
}
|
|
8976
|
+
return { rect, boxR, cx: center[0], cy: center[1], w, margin, title };
|
|
8665
8977
|
}
|
|
8666
8978
|
|
|
8667
8979
|
// CHART|PIE / DONUT: one pie's slices as real ixmaps-flat lays them out —
|
|
@@ -8693,10 +9005,38 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
8693
9005
|
// centered on 12 o'clock), HALF over 180° from 270° (HALFPLUS10 200°
|
|
8694
9006
|
// from 260°, HALFMINUS10 160° from 280°, HALFPLUS20 220° from 250°,
|
|
8695
9007
|
// HALFMINUS20 140° from 290°)
|
|
9008
|
+
// - STARBURST (piechart.js 246, 324-355, 415-420): every part >= 0, the
|
|
9009
|
+
// angles EQUAL (the value is the ray's radius, not its sweep) and the
|
|
9010
|
+
// percents NOT normalized — raw values. The FLOWER/RAYS variants
|
|
9011
|
+
// (XLFLOWER/XLRAYS, LFLOWER/LRAYS, XSFLOWER/XSRAYS, SFLOWER/SRAYS,
|
|
9012
|
+
// FLOWER/RAYS) cap the ray angle (maxAngle / 4 / 12 / 10 / none) and
|
|
9013
|
+
// inset a gap (10 / 20 / 40 / 33 / 35 % of it) on both sides of each
|
|
9014
|
+
// ray. The ray RADIUS is
|
|
9015
|
+
// computed in _buildPieLayers (it needs the theme's maxValue)
|
|
8696
9016
|
function pieSliceLayout(values, t, opts = {}) {
|
|
8697
9017
|
t = String(t || '');
|
|
8698
9018
|
const has = re => re.test(t);
|
|
8699
|
-
|
|
9019
|
+
// flat's STARBURST (piechart.js realize 246, 324-355, 415-420): the
|
|
9020
|
+
// parts get EQUAL angles — the value is the ray's radius, not its
|
|
9021
|
+
// sweep — and every part is clamped to >= 0. The FLOWER/RAYS
|
|
9022
|
+
// variants (flat maps XLFLOWER to XLRAYS & kin, maptheme.js
|
|
9023
|
+
// 20169-20186) cap the ray angle and leave a gap between the rays.
|
|
9024
|
+
// flat's `nRadInner = max(5, nRadInner)` for the ray variants is 5
|
|
9025
|
+
// units of the chart's INTERNAL coordinate system — measured on the
|
|
9026
|
+
// ISTAT story: 5 of the disc's 197 internal units, 0.28 screen px,
|
|
9027
|
+
// sub-pixel and invisible (the petals go to the center; an inner
|
|
9028
|
+
// radius shows only with DONUT) — so starInner here is 0 for the
|
|
9029
|
+
// ray variants too, and only DONUT+STARBURST starts its rays at
|
|
9030
|
+
// the donut's inner radius (see _buildPieLayers)
|
|
9031
|
+
const star = has(/STARBURST/);
|
|
9032
|
+
const starRay = !star ? null
|
|
9033
|
+
: /XLFLOWER|XLRAYS/.test(t) ? { cap: 4, gap: 0.10 }
|
|
9034
|
+
: /LFLOWER|LRAYS/.test(t) ? { cap: 4, gap: 0.20 }
|
|
9035
|
+
: /XSFLOWER|XSRAYS/.test(t) ? { cap: 12, gap: 0.40 }
|
|
9036
|
+
: /SFLOWER|SRAYS/.test(t) ? { cap: 10, gap: 0.33 }
|
|
9037
|
+
: /FLOWER|RAYS/.test(t) ? { cap: 1, gap: 0.35 }
|
|
9038
|
+
: { cap: 1, gap: 0 };
|
|
9039
|
+
const parts = values.map(v => (isNaN(v) ? 0 : star ? Math.max(0, v) : v));
|
|
8700
9040
|
const sum = parts.reduce((a, v) => a + v, 0);
|
|
8701
9041
|
const autocomplete = has(/AUTOCOMPLETE/);
|
|
8702
9042
|
if (sum === 0 && !autocomplete) return null;
|
|
@@ -8749,13 +9089,18 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
8749
9089
|
// flat draws its "Pie error: no values !" circle here
|
|
8750
9090
|
if (!any && !autocomplete) return null;
|
|
8751
9091
|
const pctSum = slices.reduce((a, sl) => a + sl.percent, 0);
|
|
8752
|
-
|
|
9092
|
+
// flat's STARBURST keeps the parts' raw values (piechart.js 246-258:
|
|
9093
|
+
// no 100% normalization, no AUTOCOMPLETE) — the value is the radius
|
|
9094
|
+
if (!star && pctSum !== 100) {
|
|
8753
9095
|
if (autocomplete && pctSum < 100) slices.push({ i: -1, value: null, percent: Math.floor((100 - pctSum) * 100000) / 100000, complement: true });
|
|
8754
9096
|
else slices.forEach(sl => { sl.percent = sl.percent / pctSum * 100; });
|
|
8755
9097
|
}
|
|
8756
9098
|
let end = 0;
|
|
8757
9099
|
if (has(/SYMMETRIC/) && slices.length) {
|
|
8758
|
-
if (
|
|
9100
|
+
if (star) {
|
|
9101
|
+
// flat piechart.js 296-299: the first ray centered on 12 o'clock
|
|
9102
|
+
end = 360 - Math.floor(360 / parts.length / 2);
|
|
9103
|
+
} else if (has(/BIGTOTOP/)) {
|
|
8759
9104
|
end = 360;
|
|
8760
9105
|
let x = 0;
|
|
8761
9106
|
for (; x < slices.length && slices[x] !== biggest; x++) end -= slices[x].percent < 100 ? Math.floor(360 / 100 * slices[x].percent) : 0;
|
|
@@ -8776,6 +9121,20 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
8776
9121
|
if (has(/HALFMINUS20/)) maxAngle = 140;
|
|
8777
9122
|
const out = [];
|
|
8778
9123
|
slices.forEach((sl, k) => {
|
|
9124
|
+
if (star) {
|
|
9125
|
+
// flat piechart.js 324-355, 395-403: the angles advance by the
|
|
9126
|
+
// FULL equal angle (the ray variants capped), the gap inset on
|
|
9127
|
+
// both sides of a ray; a ray's own sweep is that reduced angle
|
|
9128
|
+
const angle = Math.min(maxAngle / starRay.cap, maxAngle / parts.length);
|
|
9129
|
+
const base = end % 360;
|
|
9130
|
+
end = base + angle;
|
|
9131
|
+
if (end > 360) end = end % 360;
|
|
9132
|
+
const sweep = angle * (1 - 2 * starRay.gap);
|
|
9133
|
+
const start = base + angle * starRay.gap;
|
|
9134
|
+
if (sweep <= 0 || start < 0) return;
|
|
9135
|
+
out.push(Object.assign(sl, { start, sweep, k }));
|
|
9136
|
+
return;
|
|
9137
|
+
}
|
|
8779
9138
|
const sweep = sl.percent / 100 * maxAngle;
|
|
8780
9139
|
const start = end % 360;
|
|
8781
9140
|
end = start + sweep;
|
|
@@ -8902,6 +9261,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
8902
9261
|
const parts = [], zeroNot = [];
|
|
8903
9262
|
for (let k = 0; k < o.fields.length; k++) {
|
|
8904
9263
|
let v = parseFloat(raw[o.fields[k]]);
|
|
9264
|
+
// flat's weights multiply every record's per-field value as it is
|
|
9265
|
+
// accumulated (maptheme.js 10506-10508) — the SIZE (nValueSize)
|
|
9266
|
+
// stays raw, exactly as there
|
|
9267
|
+
if (o.weights) v = v * o.weights(k);
|
|
8905
9268
|
zeroNot[k] = false;
|
|
8906
9269
|
if (isNaN(v)) {
|
|
8907
9270
|
if (!r.undefinedOk) return null;
|
|
@@ -9111,6 +9474,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9111
9474
|
for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
|
|
9112
9475
|
return (h >>> 0) / 4294967296;
|
|
9113
9476
|
}
|
|
9477
|
+
// a flow's dash phase in [0, 1): flat's random one, the same for the
|
|
9478
|
+
// same arc on every rebuild
|
|
9479
|
+
function flowDashPhase(p1, p2) {
|
|
9480
|
+
return hashUnit('dash:' + p1[0] + ',' + p1[1] + '&' + p2[0] + ',' + p2[1]);
|
|
9481
|
+
}
|
|
9114
9482
|
|
|
9115
9483
|
// points of a cubic Bézier, n segments (n + 1 points)
|
|
9116
9484
|
function cubicBezierPoints(p0, c1, c2, p3, n) {
|
|
@@ -9273,14 +9641,21 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9273
9641
|
const up = fl.up;
|
|
9274
9642
|
const maxR = opts.maxRadius || 0;
|
|
9275
9643
|
const nStarParts = opts.nParts || counts.length;
|
|
9276
|
-
|
|
9644
|
+
// flat's star radius (nStarRadius, 0 per chart) is set ONLY by the FIRST
|
|
9645
|
+
// part of the sorted order (maptheme.js 22301-22316: `if (i === 0)`), and a
|
|
9646
|
+
// part of value 0 is skipped before it (22222): a chart whose first part
|
|
9647
|
+
// (with SORT|UP the smallest) is 0 keeps star radius 0, so with UP every
|
|
9648
|
+
// part sits on the center — only a chart with ALL parts non-zero spreads
|
|
9649
|
+
// into its star. (A cell of a CATEGORICAL AGGREGATE theme has a zero part
|
|
9650
|
+
// for every category it lacks.)
|
|
9651
|
+
let angle = 0, starR = 0, x = 0;
|
|
9277
9652
|
order.forEach((p, k) => {
|
|
9278
9653
|
if (!p.v) return;
|
|
9279
9654
|
const r = radiusOf(p.v);
|
|
9280
9655
|
if (!(r > 0)) return;
|
|
9281
9656
|
let px = 0, py = 0;
|
|
9282
9657
|
if (star) {
|
|
9283
|
-
if (
|
|
9658
|
+
if (k === 0) {
|
|
9284
9659
|
starR = r;
|
|
9285
9660
|
if (fl.expand && fl.sort && up) {
|
|
9286
9661
|
const big = order[order.length - 1].v, mv = opts.maxValue || big || 1;
|
|
@@ -9288,7 +9663,8 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9288
9663
|
}
|
|
9289
9664
|
if (fl.compress) starR *= fl.compressmax ? 0.25 : 0.5;
|
|
9290
9665
|
}
|
|
9291
|
-
|
|
9666
|
+
// flat: `(i > 0 || UP) && nRadius` — i the part's index in the sorted order
|
|
9667
|
+
if (k > 0 || up) {
|
|
9292
9668
|
const c = starR + r;
|
|
9293
9669
|
const b = Math.sqrt(Math.max(0, c * c - r * r));
|
|
9294
9670
|
let f = 90 - Math.asin(Math.min(1, b / c)) / Math.PI * 180;
|
|
@@ -9837,7 +10213,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9837
10213
|
// 7740-7801): a value that is no number counts as 0 — unless
|
|
9838
10214
|
// UNDEFINEDISNOTVALUE —, and 0 is a value — unless ZEROISNOTVALUE
|
|
9839
10215
|
function flatNumber(v, flags) {
|
|
9840
|
-
|
|
10216
|
+
// flat's own value scan (maptheme.js __scanValue 9265-9272): a
|
|
10217
|
+
// value with a comma is EUROPEAN format — dots stripped as
|
|
10218
|
+
// thousands separators, the comma is the decimal ("78,8" -> 78.8);
|
|
10219
|
+
// a value without one is parsed plain, blanks stripped. Applied
|
|
10220
|
+
// to every theme value in flat, so also here
|
|
10221
|
+
const s = String(v);
|
|
10222
|
+
let n = s.includes(',') ? parseFloat(s.replace(/\./g, '').replace(/,/g, '.')) : parseFloat(s.replace(/ /g, ''));
|
|
9841
10223
|
if (isNaN(n)) n = flags && flags.has('UNDEFINEDISNOTVALUE') ? NaN : 0;
|
|
9842
10224
|
if (n === 0 && flags && flags.has('ZEROISNOTVALUE')) return NaN;
|
|
9843
10225
|
return n;
|
|
@@ -9848,6 +10230,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9848
10230
|
const bins = [arr.reduce((a, v) => (v < a ? v : a), Infinity), m];
|
|
9849
10231
|
while (arr.length > 1) {
|
|
9850
10232
|
arr = arr.filter(d => d > m);
|
|
10233
|
+
// every value left equals the last mean (a tie at the top): there is
|
|
10234
|
+
// no head above it — mean([]) would add a NaN break (flat's own bug)
|
|
10235
|
+
if (!arr.length) break;
|
|
9851
10236
|
m = mean(arr);
|
|
9852
10237
|
bins.push(m);
|
|
9853
10238
|
}
|
|
@@ -9898,6 +10283,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
9898
10283
|
// values above the last mean, while more than one is left
|
|
9899
10284
|
if (flags.has('HEADTAIL')) {
|
|
9900
10285
|
const b = headTailBreaks(values);
|
|
10286
|
+
// fewer breaks than classes (small or low-variance data): the surplus
|
|
10287
|
+
// classes padded at the top with the last break, as naturalBreaks
|
|
10288
|
+
// does — flat reads past the end (undefined bounds, records in no class)
|
|
10289
|
+
while (b.length < nParts + 1) b.push(b[b.length - 1]);
|
|
9901
10290
|
parts = new Array(nParts).fill(null).map((_, i) => ({ min: b[i], max: b[i + 1] - 0.000001 }));
|
|
9902
10291
|
} else if (flags.has('QUANTILE')) parts = quantileBreaks(values, nParts);
|
|
9903
10292
|
else if (flags.has('NATURAL')) parts = naturalBreaks(values, nParts);
|
|
@@ -10010,7 +10399,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10010
10399
|
function fieldWeightOf(style, nFields, repeatLast) {
|
|
10011
10400
|
const w = style && style.weights;
|
|
10012
10401
|
if (w == null || w === '') return null;
|
|
10013
|
-
|
|
10402
|
+
// flat's toArray (maptheme.js 999-1014): split on '|' when the
|
|
10403
|
+
// string has one, else on ',' — "1,10,10" is a THREE-weight list
|
|
10404
|
+
// (the Happy Planet page), not one weight of 1
|
|
10405
|
+
const s = Array.isArray(w) ? w.join('|') : String(w);
|
|
10406
|
+
const list = (s.includes('|') ? s.split('|') : s.split(',')).map(v => parseFloat(v));
|
|
10014
10407
|
if (!list.some(v => !isNaN(v))) return null;
|
|
10015
10408
|
if (repeatLast) { const last = list[list.length - 1]; while (list.length < nFields) list.push(last); }
|
|
10016
10409
|
return i => list[i] || list[0] || 1;
|
|
@@ -10019,9 +10412,16 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10019
10412
|
// the value fields' own values weighted — the class value and the field100
|
|
10020
10413
|
// value stay raw, exactly as flat reads them
|
|
10021
10414
|
function weightedRecord(f, weightOf) {
|
|
10415
|
+
// a pie record's parts were weighted in flatPieRecord, and its size
|
|
10416
|
+
// (nValueSize) stays raw — nothing to do here
|
|
10417
|
+
if (f.properties && f.properties.pie) return f;
|
|
10022
10418
|
const p = Object.assign({}, f.properties);
|
|
10023
10419
|
if (p.value !== undefined && !isNaN(p.value)) p.value = p.value * weightOf(0);
|
|
10024
10420
|
if (Array.isArray(p.series)) p.series = p.series.map((v, i) => (isNaN(v) ? v : v * weightOf(i)));
|
|
10421
|
+
// the multi-field pie's parts — the SAME per-field values as series,
|
|
10422
|
+
// but the grid cell's pie accumulates THESE (flatPieAccumulate's
|
|
10423
|
+
// rec.parts), so they carry the weights too
|
|
10424
|
+
if (Array.isArray(p.parts)) p.parts = p.parts.map((v, i) => (isNaN(v) ? v : v * weightOf(i)));
|
|
10025
10425
|
return Object.assign({}, f, { properties: p });
|
|
10026
10426
|
}
|
|
10027
10427
|
function applyField100(features, binding, flags, style) {
|
|
@@ -10044,7 +10444,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10044
10444
|
return features.map(f => {
|
|
10045
10445
|
if (!f || !f.properties) return f;
|
|
10046
10446
|
const props = Object.assign({}, f.properties);
|
|
10047
|
-
let values = names.map(name =>
|
|
10447
|
+
let values = names.map(name => flatScanValue(f.properties[name]));
|
|
10048
10448
|
// the weights run BEFORE the field100 arithmetic, as in flat's
|
|
10049
10449
|
// addItemValues — the FRACTION of a weighted value is the weighted
|
|
10050
10450
|
// per-unit value
|
|
@@ -10053,7 +10453,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10053
10453
|
const sum = auto100 ? values.reduce((a, v) => a + (isNaN(v) ? 0 : v), 0) : 0;
|
|
10054
10454
|
values = values.map((v, i) => {
|
|
10055
10455
|
if (isNaN(v)) return v;
|
|
10056
|
-
const v100 = auto100 ? sum :
|
|
10456
|
+
const v100 = auto100 ? sum : flatScanValue(f.properties[f100A[Math.min(i, f100A.length - 1)]]);
|
|
10057
10457
|
if (v === 0 && !flags.has('ZEROISVALUE')) return 0;
|
|
10058
10458
|
if (fields[i][0] === '!') return (isNaN(v100) ? 0 : v100) - v;
|
|
10059
10459
|
if (flags.has('CALCVAL') || flags.has('CALC100')) return v100 ? Math.round(v * v100 / 100) : v;
|
|
@@ -10099,7 +10499,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10099
10499
|
// DIFFERENCE over several value fields: the difference is the value
|
|
10100
10500
|
const fields = String(binding.value).split('|');
|
|
10101
10501
|
const valueOf = flags && flags.has('DIFFERENCE') && fields.length > 1
|
|
10102
|
-
|
|
10502
|
+
// without AGGREGATE applyField100 already differenced every record
|
|
10503
|
+
// (flat's nValuesA, popped): the first field IS the value
|
|
10504
|
+
? (!flags.has('AGGREGATE') ? p => flatNumber(p[fields[0]], flags)
|
|
10505
|
+
: p => cellAggregatedValues({ sums: fields.map(k => parseFloat(p[k]) || 0), counts: fields.map(() => 1) }, flags, style)[0])
|
|
10103
10506
|
// "$item$" is flat's record count: 1 per record
|
|
10104
10507
|
: binding.value === '$item$' ? () => 1
|
|
10105
10508
|
: p => (p && Object.keys(p).length ? flatNumber(p[binding.value], flags) : NaN);
|
|
@@ -10395,13 +10798,18 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10395
10798
|
if (cell.titleMax === undefined || (maxAgg ? m > cell.titleMax : m < cell.titleMax)) {
|
|
10396
10799
|
cell.titleMax = m;
|
|
10397
10800
|
cell.titleFrom = f;
|
|
10801
|
+
cell.titleX = p.x; cell.titleY = p.y;
|
|
10398
10802
|
}
|
|
10399
10803
|
}
|
|
10400
10804
|
}
|
|
10401
10805
|
const out = [];
|
|
10402
10806
|
for (const cell of cells.values()) {
|
|
10403
|
-
|
|
10404
|
-
|
|
10807
|
+
// RELOCATE: at the records' mean position — with MAX / MIN at the
|
|
10808
|
+
// record holding the max (min), the one the cell IS (and its title's);
|
|
10809
|
+
// only the position follows it, never a value
|
|
10810
|
+
const atTitle = relocate && cell.titleFrom;
|
|
10811
|
+
const x = atTitle ? cell.titleX : relocate ? cell.sumX / cell.n : cell.snapped.x;
|
|
10812
|
+
const y = atTitle ? cell.titleY : relocate ? cell.sumY / cell.n : cell.snapped.y;
|
|
10405
10813
|
const ll = worldPixelToLngLat(x, y, zoom);
|
|
10406
10814
|
const geometry = { type: 'Point', coordinates: [ll.lng, ll.lat] };
|
|
10407
10815
|
const hasClass = cell.first.properties.classValue !== undefined;
|
|
@@ -10702,17 +11110,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10702
11110
|
// could ever need it (a plain "px" gridwidth, or an aggregation
|
|
10703
11111
|
// array with only "px" entries, never reads this field).
|
|
10704
11112
|
const gridwidthIsMeters = typeof this.style.gridwidth === 'string' ? !/px\s*$/i.test(this.style.gridwidth) : this.style.gridwidth != null;
|
|
10705
|
-
|
|
10706
|
-
|
|
10707
|
-
|
|
10708
|
-
const lat = f.geometry && f.geometry.coordinates && f.geometry.coordinates[1];
|
|
10709
|
-
if (typeof lat === 'number' && !isNaN(lat)) {
|
|
10710
|
-
if (lat < minLat) minLat = lat;
|
|
10711
|
-
if (lat > maxLat) maxLat = lat;
|
|
10712
|
-
}
|
|
10713
|
-
});
|
|
10714
|
-
this._gridRefLat = (minLat <= maxLat) ? (minLat + maxLat) / 2 : 0;
|
|
10715
|
-
}
|
|
11113
|
+
// aggregationscale: flat's alias of aggregation, read the same way in
|
|
11114
|
+
// _ensureClusterIndices
|
|
11115
|
+
if (gridwidthIsMeters || aggregationHasMetersEntry(this.style.aggregation || this.style.aggregationscale)) this._scanGridRefLat();
|
|
10716
11116
|
|
|
10717
11117
|
// a bar chart's parts are a pie's (its value fields or categories)
|
|
10718
11118
|
if ((this._isPieChart() || this._isBarChart()) && this.binding.value) {
|
|
@@ -10799,6 +11199,8 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10799
11199
|
let max = 0;
|
|
10800
11200
|
for (const f of this.features) for (const field of this._multiFields) { const v = Math.abs(parseFloat(f.properties[field])); if (v > max) max = v; }
|
|
10801
11201
|
this._multiFieldPartMax = max || 1;
|
|
11202
|
+
// AUTO100's scale, measured on these features by _buildSequenceChartLayers
|
|
11203
|
+
this._seqAuto100Max = null;
|
|
10802
11204
|
} else if (this.binding.value) {
|
|
10803
11205
|
// real engine's OTHER coloring mode (maptheme.js distributeValues,
|
|
10804
11206
|
// partsA): a NUMERIC bound value, not CATEGORICAL, is classed into
|
|
@@ -10988,6 +11390,14 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10988
11390
|
_resolveAggregateValue(props) { return resolveAggregateValue(this.binding, this.flags, props); }
|
|
10989
11391
|
|
|
10990
11392
|
_buildAggregationIndex(sourceFeatures) {
|
|
11393
|
+
// only records with a point position are drawn: a GeoJSON feature may
|
|
11394
|
+
// have geometry null (RFC 7946 — open-data exports of unlocated
|
|
11395
|
+
// records), or a shape chartAnchoredFeatures could not anchor; they
|
|
11396
|
+
// stay in the data (counts, facets), as on flat, but have no chart
|
|
11397
|
+
sourceFeatures = sourceFeatures.filter(f => {
|
|
11398
|
+
const c = f.geometry && f.geometry.coordinates;
|
|
11399
|
+
return !!c && typeof c[0] === 'number' && isFinite(c[0]) && isFinite(c[1]);
|
|
11400
|
+
});
|
|
10991
11401
|
// an AGGREGATE / CATEGORICAL pie: one bucket, each record carrying its
|
|
10992
11402
|
// contribution to its item (flatPieRecord), added up per position or
|
|
10993
11403
|
// grid cell by aggregateOnGrid (flatPieAccumulate)
|
|
@@ -10997,9 +11407,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
10997
11407
|
const rules = flatValueRules(t, fields ? fields.length : 1);
|
|
10998
11408
|
const sizeField = this.binding.size && this.binding.size !== '$item$' ? this.binding.size : null;
|
|
10999
11409
|
const feats = [];
|
|
11410
|
+
// flat's per-field weights (maptheme.js 10506-10508), applied to
|
|
11411
|
+
// every record's parts inside flatPieRecord
|
|
11412
|
+
const pieWeightOf = fieldWeightOf(this.style, fields ? fields.length : 1, true);
|
|
11000
11413
|
for (const f of sourceFeatures) {
|
|
11001
|
-
const c = f.geometry && f.geometry.coordinates;
|
|
11002
|
-
if (!c || typeof c[0] !== 'number' || !isFinite(c[0]) || !isFinite(c[1])) continue;
|
|
11003
11414
|
const p = f.properties || {};
|
|
11004
11415
|
let cat;
|
|
11005
11416
|
if (this._pieCategorical) {
|
|
@@ -11007,7 +11418,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11007
11418
|
const i = k == null ? undefined : this._pieCategoryIndex.get(k);
|
|
11008
11419
|
cat = i == null ? -1 : i;
|
|
11009
11420
|
}
|
|
11010
|
-
const pie = flatPieRecord(p, { rules, sizeField, cat, fields });
|
|
11421
|
+
const pie = flatPieRecord(p, { rules, sizeField, cat, fields, weights: pieWeightOf });
|
|
11011
11422
|
if (!pie) continue;
|
|
11012
11423
|
feats.push({ type: 'Feature', geometry: f.geometry, properties: { value: pie.size, pie, raw: p } });
|
|
11013
11424
|
}
|
|
@@ -11066,7 +11477,8 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11066
11477
|
const seriesFields = String(this.binding.value || '').split('|');
|
|
11067
11478
|
this._seriesDifference = this.flags.has('DIFFERENCE') && seriesFields.length > 1 && !this._aggregateField100;
|
|
11068
11479
|
const seriesOf = props => seriesFields.map(k => parseFloat(props[k]) || 0);
|
|
11069
|
-
|
|
11480
|
+
// without AGGREGATE the records are already differenced (applyField100)
|
|
11481
|
+
const seriesValue = !this.flags.has('AGGREGATE') ? props => parseFloat(props[seriesFields[0]]) || 0 : props => cellAggregatedValues({ sums: seriesOf(props), counts: seriesFields.map(() => 1) }, this.flags, this.style)[0];
|
|
11070
11482
|
this._featuresByCategory = [sourceFeatures.map(f => ({
|
|
11071
11483
|
type: 'Feature',
|
|
11072
11484
|
geometry: f.geometry,
|
|
@@ -11424,6 +11836,20 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11424
11836
|
});
|
|
11425
11837
|
}
|
|
11426
11838
|
|
|
11839
|
+
// the meters grid's reference latitude: the middle of the data's own
|
|
11840
|
+
// latitude extent (see _prepare)
|
|
11841
|
+
_scanGridRefLat() {
|
|
11842
|
+
let minLat = Infinity, maxLat = -Infinity;
|
|
11843
|
+
this.features.forEach(f => {
|
|
11844
|
+
const lat = f.geometry && f.geometry.coordinates && f.geometry.coordinates[1];
|
|
11845
|
+
if (typeof lat === 'number' && !isNaN(lat)) {
|
|
11846
|
+
if (lat < minLat) minLat = lat;
|
|
11847
|
+
if (lat > maxLat) maxLat = lat;
|
|
11848
|
+
}
|
|
11849
|
+
});
|
|
11850
|
+
this._gridRefLat = (minLat <= maxLat) ? (minLat + maxLat) / 2 : 0;
|
|
11851
|
+
}
|
|
11852
|
+
|
|
11427
11853
|
// (re)builds the per-category GridAggregateIndex for the grid width
|
|
11428
11854
|
// resolved at the given zoom, only when that width changed. Grid width,
|
|
11429
11855
|
// as flat reads it (maptheme.js:1892-1908, 7990-8008): gridwidthpx and
|
|
@@ -11434,6 +11860,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11434
11860
|
// style.aggregation bracket; none → no grid (exact positions).
|
|
11435
11861
|
_ensureClusterIndices(zoom) {
|
|
11436
11862
|
let radiusPx;
|
|
11863
|
+
// a meters grid set later by setStyle (gridwidth / aggregation) never
|
|
11864
|
+
// went through _prepare's scan — not the equator, the data's latitude
|
|
11865
|
+
if (this._gridRefLat === undefined) this._scanGridRefLat();
|
|
11437
11866
|
// GL-PORT COMPAT: a real-ixmaps-flat page's own gridwidth is
|
|
11438
11867
|
// sometimes a fixed SCREEN-PIXEL size (e.g. style.gridwidth:"3px",
|
|
11439
11868
|
// confirmed against a real ported page) rather than this engine's
|
|
@@ -11480,6 +11909,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
11480
11909
|
this._clusterUsesFixedZoom = false;
|
|
11481
11910
|
}
|
|
11482
11911
|
if (this._clusterIndices && this._clusterRadiusPx === radiusPx && this._clusterField === aggField) return;
|
|
11912
|
+
// new cells (e.g. a runtime gridwidth): the zoom-keyed stats of the
|
|
11913
|
+
// old ones (breaks, NORMALIZE, NOOUTLIER, sizeMax) no longer apply
|
|
11914
|
+
this._aggregateStatsCache = null;
|
|
11483
11915
|
this._clusterRadiusPx = radiusPx;
|
|
11484
11916
|
this._clusterField = aggField;
|
|
11485
11917
|
// after aggregation, on the cell sums: field100 of a value-based
|
|
@@ -12080,7 +12512,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12080
12512
|
// companion layer) takes the uniform mesh shortcut.
|
|
12081
12513
|
if (this.flags.has('GRIDSIZE') && this.flags.has('PLOT')) return this._buildPlotLayers(zoom, bbox);
|
|
12082
12514
|
// PLOT without GRIDSIZE: one line chart per item over its value fields
|
|
12083
|
-
if (this.flags.has('PLOT') && this._isItemPlot()) return this._buildItemPlotLayers(zoom);
|
|
12515
|
+
if (this.flags.has('PLOT') && this._isItemPlot()) return this._buildItemPlotLayers(zoom, liveZoom);
|
|
12084
12516
|
// a plain grid (one color, no values or glow — the Germany page's
|
|
12085
12517
|
// companion "grid" layer) as one square per cell; a classed GRIDSIZE
|
|
12086
12518
|
// chart (the fuel-price page) is a symbol chart sized to the grid,
|
|
@@ -12666,16 +13098,20 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12666
13098
|
return explicit ? fields.map((f, i) => String(explicit[i] != null ? explicit[i] : f)) : fields;
|
|
12667
13099
|
}
|
|
12668
13100
|
|
|
12669
|
-
_buildItemPlotLayers(zoom) {
|
|
13101
|
+
_buildItemPlotLayers(zoom, liveZoom = zoom) {
|
|
12670
13102
|
const fields = this._itemPlotFields();
|
|
12671
13103
|
const items = [];
|
|
12672
13104
|
let dMin = Infinity, dMax = -Infinity;
|
|
13105
|
+
// facet / runtime WHERE / time frame narrow the drawn charts, as in
|
|
13106
|
+
// every other renderer; the value range stays the full data's (as
|
|
13107
|
+
// the bubbles' _maxSizeValue), so a filter does not rescale them
|
|
13108
|
+
const active = this._activeFeatures ? new Set(this._activeFeatures) : null;
|
|
12673
13109
|
for (const f of this.features) {
|
|
12674
13110
|
const pos = itemAnchor(f.geometry);
|
|
12675
13111
|
if (!pos) continue;
|
|
12676
13112
|
const values = fields.map(k => parseFloat(f.properties[k]));
|
|
12677
13113
|
for (const v of values) if (!isNaN(v)) { dMin = Math.min(dMin, v); dMax = Math.max(dMax, v); }
|
|
12678
|
-
items.push({ geometry: { type: 'Point', coordinates: pos }, properties: { values, raw: f.properties } });
|
|
13114
|
+
if (!active || active.has(f)) items.push({ geometry: { type: 'Point', coordinates: pos }, properties: { values, raw: f.properties } });
|
|
12679
13115
|
}
|
|
12680
13116
|
if (!items.length) return [];
|
|
12681
13117
|
const sMin = styleNum(this.style.minvalue), sMaxRaw = this.style.maxvalue;
|
|
@@ -12694,7 +13130,18 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12694
13130
|
const S = 2 * NORMAL_RADIUS_PX * (styleNum(this.style.scale) || 1) * objectZoomFactor(zoom, this.mapOptions);
|
|
12695
13131
|
const color = parseCssColor(Array.isArray(this.style.colorscheme) ? this.style.colorscheme[0] : this.style.colorscheme) || [0, 102, 204];
|
|
12696
13132
|
const fillOpacity = styleNum(this.style.fillopacity);
|
|
12697
|
-
|
|
13133
|
+
// BOX / TITLE: flat's chart-group box around every chart; the chart's
|
|
13134
|
+
// drawn extent is its plot geometry's bounding box (chart units × S px,
|
|
13135
|
+
// y up in the geometry, down on screen)
|
|
13136
|
+
const boxRows = this._boxRows(items, { unit: S / (2 * NORMAL_RADIUS_PX), liveZoom, extentOf: d => {
|
|
13137
|
+
const b = itemPlotGeometry(d.properties.values, rangeFor(d.properties.values), this.style, this.flags, sizeMax).bbox;
|
|
13138
|
+
return [b.x0 * S, -b.y1 * S, b.x1 * S, -b.y0 * S];
|
|
13139
|
+
} });
|
|
13140
|
+
const boxLayers = boxRows ? this._boxTitleLayers(boxRows, {
|
|
13141
|
+
unit: S / (2 * NORMAL_RADIUS_PX), liveZoom, positionOf: b => b.d.geometry.coordinates, offsetOf: () => [0, 0],
|
|
13142
|
+
ids: { circle: `ix-itemplot-box-${this.name}`, icon: `ix-itemplot-box-${this.name}`, polygons: `ix-itemplot-box-polygons-${this.name}`, titles: `ix-itemplot-titles-${this.name}` }
|
|
13143
|
+
}) : { box: [], titles: [] };
|
|
13144
|
+
return [...boxLayers.box, new IconLayer({
|
|
12698
13145
|
id: `ix-itemplot-${this.name}-g${this._iconGeneration}`,
|
|
12699
13146
|
data: items, pickable: true,
|
|
12700
13147
|
getPosition: d => d.geometry.coordinates,
|
|
@@ -12704,7 +13151,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12704
13151
|
getSize: d => this._buildItemPlotIcon(d.properties.values, rangeFor(d.properties.values), sizeMax, color, fillOpacity).heightUnits * S,
|
|
12705
13152
|
sizeUnits: 'pixels',
|
|
12706
13153
|
updateTriggers: { getSize: [S] }
|
|
12707
|
-
})];
|
|
13154
|
+
}), ...boxLayers.titles];
|
|
12708
13155
|
}
|
|
12709
13156
|
|
|
12710
13157
|
// canvas for one per-item PLOT chart (itemPlotGeometry), RASTER px per
|
|
@@ -12842,14 +13289,22 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12842
13289
|
datasetMax = datasetMax || 1;
|
|
12843
13290
|
if (isNaN(normalSizeValue)) normalSizeValue = datasetMax;
|
|
12844
13291
|
|
|
12845
|
-
|
|
13292
|
+
// BOX / TITLE: flat's chart-group box around every chart; the chart is
|
|
13293
|
+
// the iconSize square on the cell's center (flat's chart size is 30 units)
|
|
13294
|
+
const boxRows = this._boxRows(data, { unit: iconSize / 30, liveZoom: zoom,
|
|
13295
|
+
extentOf: () => [-iconSize / 2, -iconSize / 2, iconSize / 2, iconSize / 2] });
|
|
13296
|
+
const boxLayers = boxRows ? this._boxTitleLayers(boxRows, {
|
|
13297
|
+
unit: iconSize / 30, liveZoom: zoom, positionOf: b => b.d.geometry.coordinates, offsetOf: () => [0, 0],
|
|
13298
|
+
ids: { circle: `ix-plot-box-${this.name}`, icon: `ix-plot-box-${this.name}`, polygons: `ix-plot-box-polygons-${this.name}`, titles: `ix-plot-titles-${this.name}` }
|
|
13299
|
+
}) : { box: [], titles: [] };
|
|
13300
|
+
return [...boxLayers.box, new IconLayer({
|
|
12846
13301
|
id: `ix-plot-${this.name}-g${this._iconGeneration}`,
|
|
12847
13302
|
data, pickable: true,
|
|
12848
13303
|
getPosition: d => d.geometry.coordinates,
|
|
12849
13304
|
getIcon: d => this._buildPlotIcon(d.properties.values, categories, labels, normalSizeValue, datasetMax, datasetMin),
|
|
12850
13305
|
getSize: iconSize,
|
|
12851
13306
|
sizeUnits: 'pixels'
|
|
12852
|
-
})];
|
|
13307
|
+
}), ...boxLayers.titles];
|
|
12853
13308
|
}
|
|
12854
13309
|
|
|
12855
13310
|
// Canvas rendering for one cell's mini-chart — BOX background, AREA
|
|
@@ -12967,9 +13422,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
12967
13422
|
canvas.width = W; canvas.height = H;
|
|
12968
13423
|
const ctx = canvas.getContext('2d');
|
|
12969
13424
|
|
|
12970
|
-
// BOX
|
|
13425
|
+
// BOX (a plot without the BOX flag keeps its own canvas box; with it the
|
|
13426
|
+
// shared box layer draws it — see _buildPlotLayers)
|
|
12971
13427
|
const boxOpacity = styleNum(this.style.boxopacity);
|
|
12972
|
-
if (!isNaN(boxOpacity) && boxOpacity > 0) {
|
|
13428
|
+
if (!flatFlag(this.flags, 'BOX') && !isNaN(boxOpacity) && boxOpacity > 0) {
|
|
12973
13429
|
ctx.fillStyle = `rgba(255,255,255,${boxOpacity})`;
|
|
12974
13430
|
ctx.fillRect(0, 0, W, H);
|
|
12975
13431
|
}
|
|
@@ -13215,6 +13671,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13215
13671
|
// async image decode, the whole drawing is synchronous
|
|
13216
13672
|
const items = [];
|
|
13217
13673
|
const defsSeen = new Set();
|
|
13674
|
+
// this drawing's defs, swapped in for the previous drawing's below —
|
|
13675
|
+
// appended to the shared pane <defs> on every draw they grew without
|
|
13676
|
+
// bound (and getElementById then found a stale earlier copy)
|
|
13677
|
+
const paneDefs = document.createElementNS(SVG_NS, 'g');
|
|
13218
13678
|
let html = '';
|
|
13219
13679
|
for (const d of combined) {
|
|
13220
13680
|
const value = valueOf(d);
|
|
@@ -13231,11 +13691,17 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13231
13691
|
for (const id of chart.ids) {
|
|
13232
13692
|
if (defsSeen.has(id)) continue;
|
|
13233
13693
|
defsSeen.add(id);
|
|
13234
|
-
|
|
13235
|
-
|
|
13694
|
+
// drawn now (fresh), else a cached chart's: the previous drawing's copy
|
|
13695
|
+
const def = this._userChartGroup.querySelector('#' + CSS.escape(id)) || this._userChartGroup.ownerDocument.getElementById(id);
|
|
13696
|
+
if (def) paneDefs.appendChild(def.cloneNode(true));
|
|
13236
13697
|
}
|
|
13237
13698
|
items.push({ d, coords: d.geometry.coordinates, ox: chart.ox, oy: chart.oy, s: pxPerUnit,
|
|
13238
|
-
r: Math.max(chart.vw, chart.vh) * pxPerUnit / 2
|
|
13699
|
+
r: Math.max(chart.vw, chart.vh) * pxPerUnit / 2,
|
|
13700
|
+
// the chart's measured box (flat's getBox of the chart group) in px
|
|
13701
|
+
// around the map position: the pane draws a chart point at
|
|
13702
|
+
// p + s · (point − origin)
|
|
13703
|
+
ext: [(chart.bb.x - chart.ox) * pxPerUnit, (chart.bb.y - chart.oy) * pxPerUnit,
|
|
13704
|
+
(chart.bb.x + chart.bb.width - chart.ox) * pxPerUnit, (chart.bb.y + chart.bb.height - chart.oy) * pxPerUnit] });
|
|
13239
13705
|
}
|
|
13240
13706
|
if (glDebug()) console.info(`[ixmaps-gl debug] ${themeIdOf(this)}: zoom ${mapLibreToFlatZoom(liveZoom).toFixed(2)}, ${combined.length} charts, ${items.length} drawn${this._viewMoving ? ' (moving)' : ''}`);
|
|
13241
13707
|
if (!items.length) {
|
|
@@ -13244,6 +13710,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13244
13710
|
this._chartItems = null;
|
|
13245
13711
|
return (this._userChartLastLayers = []);
|
|
13246
13712
|
}
|
|
13713
|
+
chartHost.defs.appendChild(paneDefs);
|
|
13714
|
+
if (this._chartPaneDefs) this._chartPaneDefs.remove();
|
|
13715
|
+
this._chartPaneDefs = paneDefs;
|
|
13247
13716
|
if (this._chartPaneGroup) this._chartPaneGroup.remove();
|
|
13248
13717
|
this._chartPaneGroup = document.createElementNS(SVG_NS, 'g');
|
|
13249
13718
|
chartHost.pane.appendChild(this._chartPaneGroup);
|
|
@@ -13252,11 +13721,24 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13252
13721
|
for (let i = 0; i < items.length; i++) items[i].el = els[i];
|
|
13253
13722
|
this._chartItems = items;
|
|
13254
13723
|
chartHost.runtimes.add(this);
|
|
13724
|
+
this._chartHost = chartHost; // removeRuntimes takes it out again
|
|
13255
13725
|
chartHost.update();
|
|
13256
13726
|
// picking: a transparent circle per chart — the VISIBLE chart is the
|
|
13257
13727
|
// live SVG in the pane; this deck layer only serves hover, click and
|
|
13258
13728
|
// the tooltips (pickedFeature unwraps the {d, icon} entry shape)
|
|
13259
|
-
|
|
13729
|
+
// BOX / TITLE: flat's chart-group box around the chart as the page's
|
|
13730
|
+
// function drew it (its measured box). The chart itself is a live SVG in
|
|
13731
|
+
// the pane above the deck canvas, so the box lies under it and the title
|
|
13732
|
+
// (outside the box) beside it
|
|
13733
|
+
const extOf = new Map(items.map(it => [it.d, it.ext]));
|
|
13734
|
+
const boxRows = this._boxRows(items.map(it => it.d), {
|
|
13735
|
+
unit: objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(this.style.scale) || 1), liveZoom, extentOf: d => extOf.get(d) });
|
|
13736
|
+
const boxLayers = boxRows ? this._boxTitleLayers(boxRows, {
|
|
13737
|
+
unit: objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(this.style.scale) || 1), liveZoom,
|
|
13738
|
+
positionOf: b => b.d.geometry.coordinates, offsetOf: () => [0, 0],
|
|
13739
|
+
ids: { circle: `ix-user-box-${this.name}`, icon: `ix-user-box-${this.name}`, polygons: `ix-user-box-polygons-${this.name}`, titles: `ix-user-titles-${this.name}` }
|
|
13740
|
+
}) : { box: [], titles: [] };
|
|
13741
|
+
this._userChartLastLayers = [...boxLayers.box, new ScatterplotLayer({
|
|
13260
13742
|
id: `ix-bubbles-${this.name}`,
|
|
13261
13743
|
data: items.map(it => ({ d: it.d, icon: true, r: it.r })),
|
|
13262
13744
|
pickable: true,
|
|
@@ -13265,7 +13747,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13265
13747
|
radiusUnits: 'pixels', radiusMinPixels: 1,
|
|
13266
13748
|
stroked: false, filled: true, getFillColor: [0, 0, 0, 0],
|
|
13267
13749
|
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
13268
|
-
})];
|
|
13750
|
+
}), ...boxLayers.titles];
|
|
13269
13751
|
return this._userChartLastLayers;
|
|
13270
13752
|
}
|
|
13271
13753
|
// the chart's SVG, cached by its draw parameters — a failing draw
|
|
@@ -13320,7 +13802,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13320
13802
|
const vx = bb.x - pad, vy = bb.y - pad, vw = bb.width + 2 * pad, vh = bb.height + 2 * pad;
|
|
13321
13803
|
// the chart's origin (0,0 — or the point flat moves it to, ret) sits at the map position
|
|
13322
13804
|
const ox = (ret && ret.x) || 0, oy = (ret && ret.y) || 0;
|
|
13323
|
-
chart = { vx, vy, vw, vh, ox, oy, markup, ids: [...ids],
|
|
13805
|
+
chart = { vx, vy, vw, vh, ox, oy, markup, ids: [...ids], bb: { x: bb.x, y: bb.y, width: bb.width, height: bb.height },
|
|
13324
13806
|
svg: `<svg xmlns="${SVG_NS}" width="__W__" height="__H__" viewBox="${vx} ${vy} ${vw} ${vh}"><defs>${defs}</defs>${markup}</svg>` };
|
|
13325
13807
|
}
|
|
13326
13808
|
} catch (e) {
|
|
@@ -13333,6 +13815,187 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13333
13815
|
return chart;
|
|
13334
13816
|
}
|
|
13335
13817
|
|
|
13818
|
+
// flat's BOX block belongs to the box: the box owns its TITLE — no BOX, no
|
|
13819
|
+
// title — so everything about both is decided here and in _boxRows, for
|
|
13820
|
+
// every chart type (single symbol, SEQUENCE, PIE, BAR): the gates (BOX /
|
|
13821
|
+
// TITLE flags, boxupper / boxlower / titleupper scale), the margin, the
|
|
13822
|
+
// title font (DTEXT follows the chart), BOTTOMTITLE, align, CIRCULARBOX.
|
|
13823
|
+
// null when there is no box at this zoom.
|
|
13824
|
+
_boxSpec(liveZoom) {
|
|
13825
|
+
const st = this.style;
|
|
13826
|
+
if (!(flatFlag(this.flags, 'BOX') && !boxHiddenByScale(st, liveZoom))) return null;
|
|
13827
|
+
const titleUpper = scaleDenom(st.titleupper);
|
|
13828
|
+
const titleScale = styleNum(st.textscale) || styleNum(st.valuescale) || 1;
|
|
13829
|
+
return {
|
|
13830
|
+
circular: flatFlag(this.flags, 'CIRCULAR'),
|
|
13831
|
+
margin: styleNum(st.boxmargin) || 2,
|
|
13832
|
+
bottomTitle: flatFlag(this.flags, 'BOTTOMTITLE'),
|
|
13833
|
+
alignRight: /right/.test(String(st.align || '')),
|
|
13834
|
+
titleShown: flatFlag(this.flags, 'TITLE') && !!this.binding.title
|
|
13835
|
+
&& !(titleUpper && scaleDenominatorAt(liveZoom) > titleUpper),
|
|
13836
|
+
// DTEXT: the title font follows the chart, max(0.9u, min(W/5, 14u /
|
|
13837
|
+
// scale)) (18815) — measured on the fire areas page's SIF chart
|
|
13838
|
+
// (W 362 units → font 72.35)
|
|
13839
|
+
titleFontOf: (unit, w) => (flatFlag(this.flags, 'DTEXT')
|
|
13840
|
+
? Math.max(0.9 * unit, Math.min(w / 5, 14 * unit / (styleNum(st.scale) || 1)))
|
|
13841
|
+
: 5 * unit) * titleScale
|
|
13842
|
+
};
|
|
13843
|
+
}
|
|
13844
|
+
|
|
13845
|
+
// the layout row of every item's box and title (chartBoxLayout + the spec
|
|
13846
|
+
// above); null without a box (items: the array, or a function building it
|
|
13847
|
+
// only when there is a box). o: { unit (px per flat unit), liveZoom,
|
|
13848
|
+
// extentOf (item -> its drawn [x0, y0, x1, y1], px around the anchor),
|
|
13849
|
+
// radiusOf / centerOf (the circular box's base, default the extent's) }.
|
|
13850
|
+
// A row: { d: item, geometry, properties, w (the chart's width — flat's
|
|
13851
|
+
// size measure W, the stroke and corner basis), boxR / cx / cy (circle),
|
|
13852
|
+
// rect (rectangle), title }
|
|
13853
|
+
_boxRows(items, o) {
|
|
13854
|
+
const spec = this._boxSpec(o.liveZoom);
|
|
13855
|
+
if (!spec) return null;
|
|
13856
|
+
// flat appends style.titleunits (its parse adds the leading space,
|
|
13857
|
+
// maptheme.js 1470; szTitle += at 18827)
|
|
13858
|
+
const units = this.style.titleunits ? ' ' + this.style.titleunits : '';
|
|
13859
|
+
return (typeof items === 'function' ? items() : items).map(d => {
|
|
13860
|
+
const itemTitle = spec.titleShown ? this._itemTitle(d.properties) : null;
|
|
13861
|
+
const L = chartBoxLayout(o.extentOf(d), { unit: o.unit, margin: spec.margin, circular: spec.circular,
|
|
13862
|
+
radius: o.radiusOf ? o.radiusOf(d) : undefined, center: o.centerOf ? o.centerOf(d) : undefined,
|
|
13863
|
+
titleText: itemTitle ? itemTitle + units : null, titleFont: w => spec.titleFontOf(o.unit, w),
|
|
13864
|
+
bottomTitle: spec.bottomTitle, alignRight: spec.alignRight });
|
|
13865
|
+
// geometry/properties of the chart's feature on the row: the circular
|
|
13866
|
+
// box is pickable, and the tooltip (buildTooltipHtml, the click-pin)
|
|
13867
|
+
// works on the feature's own properties
|
|
13868
|
+
return { d, geometry: d.geometry, properties: d.properties, w: L.w, boxR: L.boxR, cx: L.cx, cy: L.cy,
|
|
13869
|
+
rect: L.rect, title: L.title };
|
|
13870
|
+
});
|
|
13871
|
+
}
|
|
13872
|
+
|
|
13873
|
+
// THE box-and-title layers of every chart type: rows are _boxRows results.
|
|
13874
|
+
// o: { unit, liveZoom, positionOf (row -> [lng, lat]), offsetOf (row ->
|
|
13875
|
+
// the chart's [dx, dy] px move: align / offsetx / offsety), box (false:
|
|
13876
|
+
// the title layer only — a BAR draws its box as a shader shape), ids:
|
|
13877
|
+
// { circle, icon, polygons, titles } } — the layer ids stay each chart
|
|
13878
|
+
// type's own. Returns { box: [layers], titles: [layers] }: the caller
|
|
13879
|
+
// pushes them where its draw order wants them.
|
|
13880
|
+
_boxTitleLayers(rows, o) {
|
|
13881
|
+
const st = this.style;
|
|
13882
|
+
const { unit, liveZoom, positionOf, offsetOf, ids } = o;
|
|
13883
|
+
const circular = flatFlag(this.flags, 'CIRCULAR');
|
|
13884
|
+
const box = [], titles = [];
|
|
13885
|
+
// flat: fill-opacity (boxopacity || 1) on the raw style value — the usual
|
|
13886
|
+
// string "0" is transparent; typed here, a given 0 is too
|
|
13887
|
+
const boxOpacity = isNaN(styleNum(st.boxopacity)) ? 1 : styleNum(st.boxopacity);
|
|
13888
|
+
if (o.box === false) {
|
|
13889
|
+
// the title layer only
|
|
13890
|
+
} else if (circular) {
|
|
13891
|
+
const boxNone = isNoneColor(st.boxcolor), borderNone = isNoneColor(st.bordercolor);
|
|
13892
|
+
const boxRgb = hexOrNamedToRgb(boxNone ? '#eeeeee' : st.boxcolor || '#eeeeee');
|
|
13893
|
+
const boxAlpha = boxNone ? 0 : Math.round(255 * boxOpacity);
|
|
13894
|
+
const borderRgb = borderNone ? null : hexOrNamedToRgb(st.bordercolor || '#dddddd');
|
|
13895
|
+
// Always a ScatterplotLayer: its shader circle is smooth at any
|
|
13896
|
+
// size, where an icon — a 48px canvas texture deck.gl scales up —
|
|
13897
|
+
// pixelated with large boxes. ScatterplotLayer has no pixel offset,
|
|
13898
|
+
// so the chart's align/offsetx/offsety move and flat's circular box
|
|
13899
|
+
// position (the chart's bounding-box center, 18793) are baked into
|
|
13900
|
+
// map coordinates (pixelOffsetLngLat). Pickable: flat's box is part
|
|
13901
|
+
// of the chart group, so hovering it fires the chart's tooltip — the
|
|
13902
|
+
// symbols sit above it, so they keep their own
|
|
13903
|
+
box.push(new ScatterplotLayer({
|
|
13904
|
+
id: ids.circle,
|
|
13905
|
+
data: rows, pickable: true,
|
|
13906
|
+
getPosition: b => { const f = offsetOf(b); return pixelOffsetLngLat(positionOf(b), [f[0] + b.cx, f[1] + b.cy], liveZoom); },
|
|
13907
|
+
getRadius: b => b.boxR,
|
|
13908
|
+
radiusUnits: 'pixels',
|
|
13909
|
+
stroked: !borderNone,
|
|
13910
|
+
getFillColor: [...boxRgb.slice(0, 3), boxAlpha],
|
|
13911
|
+
getLineColor: [...(borderRgb || boxRgb).slice(0, 3), 255],
|
|
13912
|
+
getLineWidth: unit / 20,
|
|
13913
|
+
lineWidthUnits: 'pixels',
|
|
13914
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
13915
|
+
}));
|
|
13916
|
+
} else {
|
|
13917
|
+
// flat's rectangular box (the chart and its title plus the margin)
|
|
13918
|
+
const borderStyle = String(st.borderstyle || '');
|
|
13919
|
+
const boxLook = {
|
|
13920
|
+
fill: st.boxcolor || 'white',
|
|
13921
|
+
fillOpacity: boxOpacity,
|
|
13922
|
+
stroke: borderStyle === 'none' || isNoneColor(styleLineColor(st.bordercolor)) ? null : styleLineColor(st.bordercolor) || '#bbbbbb',
|
|
13923
|
+
strokeOpacity: /^(solid|dotted|dashed)$/.test(borderStyle) ? 1 : 0.3,
|
|
13924
|
+
};
|
|
13925
|
+
const borderWidth = styleNum(st.borderwidth) || 1;
|
|
13926
|
+
const borderRadius = styleNum(st.borderradius);
|
|
13927
|
+
const strokeOf = b => borderWidth * 0.2 * b.w / 30;
|
|
13928
|
+
// a box larger than BOX_ICON_MAX_PX (a MULTIQUAD grid of thousands
|
|
13929
|
+
// of items) would need a huge canvas: it is drawn as a polygon in
|
|
13930
|
+
// map coordinates instead (square corners)
|
|
13931
|
+
const bigBox = b => Math.max(b.rect[2] - b.rect[0], b.rect[3] - b.rect[1]) > BOX_ICON_MAX_PX;
|
|
13932
|
+
const iconBoxes = rows.filter(b => !bigBox(b)), polygonBoxes = rows.filter(bigBox);
|
|
13933
|
+
iconBoxes.forEach(b => {
|
|
13934
|
+
b.icon = this._buildBoxIcon(b.rect[2] - b.rect[0], b.rect[3] - b.rect[1],
|
|
13935
|
+
isNaN(borderRadius) ? 0 : borderRadius * Math.min(unit, b.w / 30),
|
|
13936
|
+
strokeOf(b), boxLook);
|
|
13937
|
+
});
|
|
13938
|
+
if (polygonBoxes.length) {
|
|
13939
|
+
const fillRgb = hexOrNamedToRgb(boxLook.fill);
|
|
13940
|
+
const strokeRgb = boxLook.stroke ? hexOrNamedToRgb(boxLook.stroke) : null;
|
|
13941
|
+
box.push(new GeoJsonLayer({
|
|
13942
|
+
id: ids.polygons,
|
|
13943
|
+
data: polygonBoxes.map(b => {
|
|
13944
|
+
const f = offsetOf(b), c = positionOf(b), r = b.rect;
|
|
13945
|
+
const at = (x, y) => pixelOffsetLngLat(c, [x + f[0], y + f[1]], liveZoom);
|
|
13946
|
+
return { type: 'Feature', properties: { stroke: strokeOf(b) },
|
|
13947
|
+
geometry: { type: 'Polygon', coordinates: [[at(r[0], r[1]), at(r[2], r[1]), at(r[2], r[3]), at(r[0], r[3]), at(r[0], r[1])]] } };
|
|
13948
|
+
}),
|
|
13949
|
+
pickable: false,
|
|
13950
|
+
filled: boxLook.fillOpacity > 0,
|
|
13951
|
+
stroked: !!strokeRgb,
|
|
13952
|
+
getFillColor: [...fillRgb.slice(0, 3), Math.round(255 * boxLook.fillOpacity)],
|
|
13953
|
+
getLineColor: strokeRgb ? [...strokeRgb.slice(0, 3), Math.round(255 * boxLook.strokeOpacity)] : [0, 0, 0, 0],
|
|
13954
|
+
getLineWidth: f => f.properties.stroke,
|
|
13955
|
+
lineWidthUnits: 'pixels',
|
|
13956
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
13957
|
+
}));
|
|
13958
|
+
}
|
|
13959
|
+
if (iconBoxes.length) box.push(new IconLayer({
|
|
13960
|
+
// the generation suffix (like every icon-atlas layer id, see
|
|
13961
|
+
// ICON_ATLAS_RESET_AFTER): every zoom creates a new set of box
|
|
13962
|
+
// rasters, and deck.gl's atlas never evicts
|
|
13963
|
+
id: `${ids.icon}-g${this._iconGeneration}`,
|
|
13964
|
+
data: iconBoxes, pickable: false,
|
|
13965
|
+
getPosition: positionOf,
|
|
13966
|
+
getIcon: b => b.icon,
|
|
13967
|
+
// the icon is drawn at 2× with a stroke padding around the box
|
|
13968
|
+
getSize: b => b.icon.height / 2,
|
|
13969
|
+
getPixelOffset: b => { const f = offsetOf(b); return [(b.rect[0] + b.rect[2]) / 2 + f[0], (b.rect[1] + b.rect[3]) / 2 + f[1]]; },
|
|
13970
|
+
sizeUnits: 'pixels',
|
|
13971
|
+
billboard: true,
|
|
13972
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
13973
|
+
}));
|
|
13974
|
+
}
|
|
13975
|
+
const titleData = rows.filter(b => b.title);
|
|
13976
|
+
if (titleData.length) titles.push(new TextLayer({
|
|
13977
|
+
id: ids.titles,
|
|
13978
|
+
data: titleData, pickable: false,
|
|
13979
|
+
getPosition: positionOf,
|
|
13980
|
+
getText: b => b.title.text,
|
|
13981
|
+
getSize: b => b.title.f,
|
|
13982
|
+
sizeUnits: 'pixels',
|
|
13983
|
+
fontFamily: 'arial',
|
|
13984
|
+
characterSet: 'auto',
|
|
13985
|
+
lineHeight: 1,
|
|
13986
|
+
getColor: [...hexOrNamedToRgb(st.textcolor || '#888888').slice(0, 3), 255],
|
|
13987
|
+
fontSettings: { sdf: true },
|
|
13988
|
+
outlineWidth: 1 / 7,
|
|
13989
|
+
outlineColor: [255, 255, 255, 128],
|
|
13990
|
+
getTextAnchor: b => b.title.anchor,
|
|
13991
|
+
// top of the text ≈ first baseline − 0.8 font
|
|
13992
|
+
getAlignmentBaseline: 'top',
|
|
13993
|
+
getPixelOffset: b => { const f = offsetOf(b); return [b.title.x + f[0], b.title.top + f[1]]; },
|
|
13994
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
13995
|
+
}));
|
|
13996
|
+
return { box, titles };
|
|
13997
|
+
}
|
|
13998
|
+
|
|
13336
13999
|
// BOX (see the BOX block in _buildChartLayers): a rectangle icon
|
|
13337
14000
|
// w×h px, rounded corners rx, stroke sw px; drawn at twice the size
|
|
13338
14001
|
_buildBoxIcon(w, h, rx, sw, look) {
|
|
@@ -13503,7 +14166,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13503
14166
|
const groups = doRelocate ? this._groupCoLocated(clusterFeatures, effectiveZoom) : clusterFeatures.map(f => {
|
|
13504
14167
|
return {
|
|
13505
14168
|
geometry: f.geometry,
|
|
13506
|
-
|
|
14169
|
+
// counts is the summed bound VALUE and recordCounts the record
|
|
14170
|
+
// count, as groupCoLocated builds them (RELOCATE) — RELOCATE
|
|
14171
|
+
// moves the item, it never changes a value
|
|
14172
|
+
properties: { counts: this._oneHot(f.properties.cat, f.properties.value),
|
|
14173
|
+
recordCounts: this._oneHot(f.properties.cat, f.properties.point_count), total: f.properties.value,
|
|
13507
14174
|
...(f.properties.classValue !== undefined ? { classTotal: f.properties.classValue } : {}),
|
|
13508
14175
|
...(f.properties.titleRaw ? { titleRaw: f.properties.titleRaw } : {}) }
|
|
13509
14176
|
};
|
|
@@ -13922,15 +14589,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13922
14589
|
// DTEXT: the title font follows the chart, max(0.9u, min(W/5,
|
|
13923
14590
|
// 14u / scale)) (18815) — measured on the fire areas page's SIF chart
|
|
13924
14591
|
// (W 362 units → font 72.35).
|
|
13925
|
-
const boxShown = flatFlag(this.flags, 'BOX') && !boxHiddenByScale(this.style, liveZoom);
|
|
13926
|
-
const circular = flatFlag(this.flags, 'CIRCULAR');
|
|
13927
14592
|
const unitPx = objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(this.style.scale) || 1);
|
|
13928
|
-
const boxMarginU = styleNum(this.style.boxmargin) || 2;
|
|
13929
|
-
const titleShown = boxShown && flatFlag(this.flags, 'TITLE') && !!this.binding.title;
|
|
13930
|
-
const titleScale = styleNum(this.style.textscale) || styleNum(this.style.valuescale) || 1;
|
|
13931
|
-
const titleFontOf = W => (flatFlag(this.flags, 'DTEXT')
|
|
13932
|
-
? Math.max(0.9 * unitPx, Math.min(W / 5, 14 * unitPx / (styleNum(this.style.scale) || 1)))
|
|
13933
|
-
: 5 * unitPx) * titleScale;
|
|
13934
14593
|
// the chart's value text and label (see VALUES and LABEL below), shared
|
|
13935
14594
|
// with the box: flat's box encloses whatever the chart group draws
|
|
13936
14595
|
const valuesShown = this.flags.has('VALUES') && !valuesHiddenByScale(this.style, zoom);
|
|
@@ -13938,141 +14597,28 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
13938
14597
|
const labelShown = !!this.style.labelfield && !flatFlag(this.flags, 'NORMSIZE') && !flatFlag(this.flags, 'ZOOM')
|
|
13939
14598
|
&& !labelHiddenByScale(this.style, zoom);
|
|
13940
14599
|
const labelOf = labelShown ? this._chartLabelFn(unitPx) : null;
|
|
13941
|
-
//
|
|
13942
|
-
//
|
|
13943
|
-
const
|
|
13944
|
-
|
|
13945
|
-
|
|
13946
|
-
|
|
13947
|
-
|
|
13948
|
-
|
|
13949
|
-
|
|
13950
|
-
|
|
13951
|
-
|
|
13952
|
-
|
|
13953
|
-
|
|
13954
|
-
const w = chart[2] - chart[0];
|
|
13955
|
-
const margin = Math.min(2 * boxMarginU * unitPx, boxMarginU * w / 30);
|
|
13956
|
-
const boxR = circular ? r * 1.1 + margin : r;
|
|
13957
|
-
const titleFontPx = titleFontOf(w);
|
|
13958
|
-
let title = null;
|
|
13959
|
-
if (titleShown) {
|
|
13960
|
-
// flat appends style.titleunits (its parse adds the leading
|
|
13961
|
-
// space, maptheme.js 1470; szTitle += at 18827)
|
|
13962
|
-
const units = this.style.titleunits ? ' ' + this.style.titleunits : '';
|
|
13963
|
-
const text = this._itemTitle(d.properties) ? this._itemTitle(d.properties) + units : null;
|
|
13964
|
-
if (text) {
|
|
13965
|
-
const f = titleFontPx;
|
|
13966
|
-
const lines = wrapTitleLines(text, f, circular ? boxR * 2 : w).split('\n');
|
|
13967
|
-
const width = Math.max(...lines.map(l => titleTextWidth(l, f)));
|
|
13968
|
-
// first baseline; lines go down one font each
|
|
13969
|
-
const base = circular
|
|
13970
|
-
? (bottomTitle ? boxR + f : -boxR - 0.7 * f - (lines.length - 1) * f)
|
|
13971
|
-
: (bottomTitle ? chart[3] + f : chart[1] - 0.7 * f - (lines.length - 1) * f);
|
|
13972
|
-
const x = circular ? unitPx / 2 : (alignRight ? chart[2] : chart[0]);
|
|
13973
|
-
const anchor = circular ? 'middle' : (alignRight ? 'end' : 'start');
|
|
13974
|
-
const x0 = anchor === 'middle' ? x - width / 2 : anchor === 'end' ? x - width : x;
|
|
13975
|
-
// arial: ascent 0.905, descent 0.212 of the font size
|
|
13976
|
-
title = { text: lines.join('\n'), x, anchor, top: base - 0.8 * f, f,
|
|
13977
|
-
extent: [x0, base - 0.905 * f, x0 + width, base + (lines.length - 1) * f + 0.212 * f] };
|
|
13978
|
-
}
|
|
13979
|
-
}
|
|
13980
|
-
let rect = null;
|
|
13981
|
-
if (!circular) {
|
|
13982
|
-
const e = chart.slice();
|
|
13983
|
-
// with a title flat trims 0.1 font off the top of chart + title
|
|
13984
|
-
if (title) { e[0] = Math.min(e[0], title.extent[0]); e[1] = Math.min(e[1], title.extent[1]) + 0.1 * titleFontPx; e[2] = Math.max(e[2], title.extent[2]); e[3] = Math.max(e[3], title.extent[3]); }
|
|
13985
|
-
rect = [e[0] - margin, e[1] - margin, e[2] + margin, e[3] + margin];
|
|
14600
|
+
// with MULTIQUAD only the first item of a position gets its box/title;
|
|
14601
|
+
// the circle sits on the anchor (0, 0), its base radius the symbol's
|
|
14602
|
+
const boxLayout = this._boxRows(() => combined.filter(d => !multi || multi.index.get(d) === 0), {
|
|
14603
|
+
unit: unitPx, liveZoom,
|
|
14604
|
+
radiusOf: d => vRadius(sizeValueOf(d)), centerOf: () => [0, 0],
|
|
14605
|
+
extentOf: d => {
|
|
14606
|
+
const r = vRadius(sizeValueOf(d));
|
|
14607
|
+
const ext = multi ? multi.extent.get(d) : [0, 0, 0, 0];
|
|
14608
|
+
const chart = [ext[0] - r, ext[1] - r, ext[2] + r, ext[3] + r];
|
|
14609
|
+
const grow = e => { if (!e) return; chart[0] = Math.min(chart[0], e[0]); chart[1] = Math.min(chart[1], e[1]); chart[2] = Math.max(chart[2], e[2]); chart[3] = Math.max(chart[3], e[3]); };
|
|
14610
|
+
if (valueTextOf && !d.properties.counts) { const v = valueTextOf(d, r); if (v) grow(v.extent); }
|
|
14611
|
+
if (labelOf) { const l = labelOf(d, r); if (l) grow(l.extent); }
|
|
14612
|
+
return chart;
|
|
13986
14613
|
}
|
|
13987
|
-
|
|
14614
|
+
});
|
|
14615
|
+
// (the title layers are pushed after the symbols, below)
|
|
14616
|
+
const boxLayers = boxLayout ? this._boxTitleLayers(boxLayout, {
|
|
14617
|
+
unit: unitPx, liveZoom,
|
|
14618
|
+
positionOf: b => b.d.geometry.coordinates, offsetOf: b => alignOf(b.d),
|
|
14619
|
+
ids: { circle: `ix-box-${this.name}`, icon: `ix-box-${this.name}`, polygons: `ix-box-polygons-${this.name}`, titles: `ix-titles-${this.name}` }
|
|
13988
14620
|
}) : null;
|
|
13989
|
-
if (
|
|
13990
|
-
const boxNone = isNoneColor(this.style.boxcolor), borderNone = isNoneColor(this.style.bordercolor);
|
|
13991
|
-
const boxRgb = hexOrNamedToRgb(boxNone ? '#eeeeee' : this.style.boxcolor || '#eeeeee');
|
|
13992
|
-
const boxAlpha = boxNone ? 0 : Math.round(255 * boxOpacity);
|
|
13993
|
-
const borderRgb = borderNone ? null : hexOrNamedToRgb(this.style.bordercolor || '#dddddd');
|
|
13994
|
-
// ScatterplotLayer has no pixel offset: a moved chart (align,
|
|
13995
|
-
// offsetx/offsety) gets its circle as an icon
|
|
13996
|
-
if (alignOf.active) layers.push(new IconLayer({
|
|
13997
|
-
id: `ix-box-${this.name}-g${this._iconGeneration}`,
|
|
13998
|
-
data: boxLayout, pickable: false,
|
|
13999
|
-
getPosition: b => b.d.geometry.coordinates,
|
|
14000
|
-
getIcon: b => this._buildSingleIcon(boxRgb, boxAlpha / 255, 'circle', borderRgb,
|
|
14001
|
-
borderNone ? 0 : Math.round(unitPx / 20 * BUBBLE_ICON_SIZE / Math.max(1, 2 * b.boxR) * 4) / 4),
|
|
14002
|
-
getSize: b => b.boxR * 2,
|
|
14003
|
-
getPixelOffset: b => alignOf(b.d),
|
|
14004
|
-
sizeUnits: 'pixels',
|
|
14005
|
-
billboard: true,
|
|
14006
|
-
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14007
|
-
}));
|
|
14008
|
-
else layers.push(new ScatterplotLayer({
|
|
14009
|
-
id: `ix-box-${this.name}`,
|
|
14010
|
-
data: boxLayout, pickable: false,
|
|
14011
|
-
getPosition: b => b.d.geometry.coordinates,
|
|
14012
|
-
getRadius: b => b.boxR,
|
|
14013
|
-
radiusUnits: 'pixels',
|
|
14014
|
-
stroked: !borderNone,
|
|
14015
|
-
getFillColor: [...boxRgb.slice(0, 3), boxAlpha],
|
|
14016
|
-
getLineColor: [...(borderRgb || boxRgb).slice(0, 3), 255],
|
|
14017
|
-
getLineWidth: unitPx / 20,
|
|
14018
|
-
lineWidthUnits: 'pixels',
|
|
14019
|
-
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14020
|
-
}));
|
|
14021
|
-
} else if (boxShown) {
|
|
14022
|
-
const borderStyle = String(this.style.borderstyle || '');
|
|
14023
|
-
const boxLook = {
|
|
14024
|
-
fill: this.style.boxcolor || 'white',
|
|
14025
|
-
fillOpacity: boxOpacity,
|
|
14026
|
-
stroke: borderStyle === 'none' || isNoneColor(styleLineColor(this.style.bordercolor)) ? null : styleLineColor(this.style.bordercolor) || '#bbbbbb',
|
|
14027
|
-
strokeOpacity: /^(solid|dotted|dashed)$/.test(borderStyle) ? 1 : 0.3,
|
|
14028
|
-
};
|
|
14029
|
-
const borderWidth = styleNum(this.style.borderwidth) || 1;
|
|
14030
|
-
const borderRadius = styleNum(this.style.borderradius);
|
|
14031
|
-
const strokeOf = b => borderWidth * 0.2 * b.w / 30;
|
|
14032
|
-
// a box larger than BOX_ICON_MAX_PX (a MULTIQUAD grid of thousands
|
|
14033
|
-
// of items) would need a huge canvas: it is drawn as a polygon in
|
|
14034
|
-
// map coordinates instead (square corners)
|
|
14035
|
-
const bigBox = b => Math.max(b.rect[2] - b.rect[0], b.rect[3] - b.rect[1]) > BOX_ICON_MAX_PX;
|
|
14036
|
-
const iconBoxes = boxLayout.filter(b => !bigBox(b)), polygonBoxes = boxLayout.filter(bigBox);
|
|
14037
|
-
iconBoxes.forEach(b => {
|
|
14038
|
-
b.icon = this._buildBoxIcon(b.rect[2] - b.rect[0], b.rect[3] - b.rect[1],
|
|
14039
|
-
isNaN(borderRadius) ? 0 : borderRadius * Math.min(unitPx, b.w / 30),
|
|
14040
|
-
strokeOf(b), boxLook);
|
|
14041
|
-
});
|
|
14042
|
-
if (polygonBoxes.length) {
|
|
14043
|
-
const fillRgb = hexOrNamedToRgb(boxLook.fill);
|
|
14044
|
-
const strokeRgb = boxLook.stroke ? hexOrNamedToRgb(boxLook.stroke) : null;
|
|
14045
|
-
layers.push(new GeoJsonLayer({
|
|
14046
|
-
id: `ix-box-polygons-${this.name}`,
|
|
14047
|
-
data: polygonBoxes.map(b => {
|
|
14048
|
-
const o = alignOf(b.d), c = b.d.geometry.coordinates, r = b.rect;
|
|
14049
|
-
const at = (x, y) => pixelOffsetLngLat(c, [x + o[0], y + o[1]], liveZoom);
|
|
14050
|
-
return { type: 'Feature', properties: { stroke: strokeOf(b) },
|
|
14051
|
-
geometry: { type: 'Polygon', coordinates: [[at(r[0], r[1]), at(r[2], r[1]), at(r[2], r[3]), at(r[0], r[3]), at(r[0], r[1])]] } };
|
|
14052
|
-
}),
|
|
14053
|
-
pickable: false,
|
|
14054
|
-
filled: boxLook.fillOpacity > 0,
|
|
14055
|
-
stroked: !!strokeRgb,
|
|
14056
|
-
getFillColor: [...fillRgb.slice(0, 3), Math.round(255 * boxLook.fillOpacity)],
|
|
14057
|
-
getLineColor: strokeRgb ? [...strokeRgb.slice(0, 3), Math.round(255 * boxLook.strokeOpacity)] : [0, 0, 0, 0],
|
|
14058
|
-
getLineWidth: f => f.properties.stroke,
|
|
14059
|
-
lineWidthUnits: 'pixels',
|
|
14060
|
-
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14061
|
-
}));
|
|
14062
|
-
}
|
|
14063
|
-
if (iconBoxes.length) layers.push(new IconLayer({
|
|
14064
|
-
id: `ix-box-${this.name}-g${this._iconGeneration}`,
|
|
14065
|
-
data: iconBoxes, pickable: false,
|
|
14066
|
-
getPosition: b => b.d.geometry.coordinates,
|
|
14067
|
-
getIcon: b => b.icon,
|
|
14068
|
-
// the icon is drawn at 2× with a stroke padding around the box
|
|
14069
|
-
getSize: b => b.icon.height / 2,
|
|
14070
|
-
getPixelOffset: b => { const o = alignOf(b.d); return [(b.rect[0] + b.rect[2]) / 2 + o[0], (b.rect[1] + b.rect[3]) / 2 + o[1]]; },
|
|
14071
|
-
sizeUnits: 'pixels',
|
|
14072
|
-
billboard: true,
|
|
14073
|
-
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14074
|
-
}));
|
|
14075
|
-
}
|
|
14621
|
+
if (boxLayers) layers.push(...boxLayers.box);
|
|
14076
14622
|
|
|
14077
14623
|
// flat's per-part SYMBOL branch (everything but CATEGORICAL without
|
|
14078
14624
|
// AGGREGATE/GROUP) draws a square or roundrect of side 2r, the other
|
|
@@ -14180,29 +14726,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14180
14726
|
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14181
14727
|
}));
|
|
14182
14728
|
|
|
14183
|
-
|
|
14184
|
-
if (titleData.length) {
|
|
14185
|
-
layers.push(new TextLayer({
|
|
14186
|
-
id: `ix-titles-${this.name}`,
|
|
14187
|
-
data: titleData, pickable: false,
|
|
14188
|
-
getPosition: b => b.d.geometry.coordinates,
|
|
14189
|
-
getText: b => b.title.text,
|
|
14190
|
-
getSize: b => b.title.f,
|
|
14191
|
-
sizeUnits: 'pixels',
|
|
14192
|
-
fontFamily: 'arial',
|
|
14193
|
-
characterSet: 'auto',
|
|
14194
|
-
lineHeight: 1,
|
|
14195
|
-
getColor: [...hexOrNamedToRgb(this.style.textcolor || '#888888').slice(0, 3), 255],
|
|
14196
|
-
fontSettings: { sdf: true },
|
|
14197
|
-
outlineWidth: 1 / 7,
|
|
14198
|
-
outlineColor: [255, 255, 255, 128],
|
|
14199
|
-
getTextAnchor: b => b.title.anchor,
|
|
14200
|
-
// top of the text ≈ first baseline − 0.8 font
|
|
14201
|
-
getAlignmentBaseline: 'top',
|
|
14202
|
-
getPixelOffset: b => { const o = alignOf(b.d); return [b.title.x + o[0], b.title.top + o[1]]; },
|
|
14203
|
-
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14204
|
-
}));
|
|
14205
|
-
}
|
|
14729
|
+
if (boxLayers) layers.push(...boxLayers.titles);
|
|
14206
14730
|
|
|
14207
14731
|
// VALUES: bold value label centered on each bubble (see
|
|
14208
14732
|
// flatValueText/valuesFontSizePx above). Labels are their own
|
|
@@ -14526,7 +15050,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14526
15050
|
};
|
|
14527
15051
|
const shapeOf = i => { const sh = symbols && (symbols[i] || symbols[0]); return normalizeSymbolShape(sh); };
|
|
14528
15052
|
const colorOf = i => this.classRgb(i);
|
|
14529
|
-
const
|
|
15053
|
+
const allParts = [], allGlows = [];
|
|
14530
15054
|
for (const d of charts) {
|
|
14531
15055
|
// parts: the record's own field values (a per-item chart, or the
|
|
14532
15056
|
// single-record cell of an aggregated one); series: a multi-record
|
|
@@ -14538,6 +15062,18 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14538
15062
|
const sum = counts.reduce((a, v) => a + (v || 0), 0);
|
|
14539
15063
|
counts = counts.map(v => sum > 0 ? v / (sum / 100) : 0);
|
|
14540
15064
|
}
|
|
15065
|
+
// flat's aggregated CATEGORICAL item holds a value per category only up
|
|
15066
|
+
// to the highest category it has (measured on flat: the chart's star and
|
|
15067
|
+
// glow follow the categories present as 0..k, 985 of 988 charts of the
|
|
15068
|
+
// concessioni page): trailing zeros are no parts, so with SORT|UP the
|
|
15069
|
+
// smallest part — order index 0, the one that sets the star radius and
|
|
15070
|
+
// carries the glow — is a zero only when a category BELOW the highest
|
|
15071
|
+
// one is missing
|
|
15072
|
+
if (!d.properties.parts && !d.properties.series) {
|
|
15073
|
+
let n = counts.length;
|
|
15074
|
+
while (n > 0 && !counts[n - 1]) n--;
|
|
15075
|
+
if (n < counts.length) counts = counts.slice(0, n);
|
|
15076
|
+
}
|
|
14541
15077
|
const sizeFactor = itemSizeFactor(d);
|
|
14542
15078
|
const partRadius = multi
|
|
14543
15079
|
? v => Math.max(VALUE_RADIUS_MIN, normalR * Math.pow(Math.abs(v) / seqMax, 1 / sizePow) * sizeFactor)
|
|
@@ -14547,7 +15083,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14547
15083
|
if (!layout.length) continue;
|
|
14548
15084
|
// GLOW belongs to the first part (sorted) or to every part, and
|
|
14549
15085
|
// hides with it when its class is not marked
|
|
14550
|
-
|
|
15086
|
+
// every chart glows (deliberately NOT flat's rule, which gives the glow to
|
|
15087
|
+
// the part at sorted order index 0 — a zero, skipped, whenever a category
|
|
15088
|
+
// below the highest is missing — so only about a quarter of flat's charts
|
|
15089
|
+
// glow): a sorted chart's glow belongs to its biggest part, an unsorted
|
|
15090
|
+
// one glows with every part as in flat
|
|
15091
|
+
const biggest = layout.reduce((m, p) => (p.v > m.v ? p : m), layout[0]);
|
|
15092
|
+
const glowParts = new Set(sorted ? [biggest.i] : layout.map(p => p.i));
|
|
14551
15093
|
// the chart's drawn extent (flat: map.Dom.getBox of its shape
|
|
14552
15094
|
// group) — the parts' star cluster — for BOX / CIRCULARBOX sizing
|
|
14553
15095
|
let ex0 = Infinity, ey0 = Infinity, ex1 = -Infinity, ey1 = -Infinity;
|
|
@@ -14564,12 +15106,12 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14564
15106
|
d._seqLast = layout[layout.length - 1].r;
|
|
14565
15107
|
const items = layout.map(p => {
|
|
14566
15108
|
const item = { geometry: d.geometry, properties: d.properties, chart: d, part: p };
|
|
14567
|
-
if (glowParts.has(p.i))
|
|
15109
|
+
if (glowParts.has(p.i)) allGlows.push(item);
|
|
14568
15110
|
return item;
|
|
14569
15111
|
});
|
|
14570
15112
|
// STAR: the first part (its center) is put on top at the end (23145)
|
|
14571
15113
|
if (star && items.length > 1) items.push(items.shift());
|
|
14572
|
-
|
|
15114
|
+
allParts.push(...items);
|
|
14573
15115
|
}
|
|
14574
15116
|
const alignOf = this._chartAlignFn(liveZoom, d => (d.chart || d)._seqLast || 0);
|
|
14575
15117
|
const offsetOf = item => { const o = alignOf(item.chart); return [o[0] + item.part.x, o[1] + item.part.y]; };
|
|
@@ -14587,287 +15129,227 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14587
15129
|
// max(0.9u, min(w/5, 14u/scale)) · textscale, wrapped to the
|
|
14588
15130
|
// circle's width — gated by titleupper (shown while the map
|
|
14589
15131
|
// scale is that bound or coarser, 18810) and boxupper/boxlower
|
|
14590
|
-
|
|
15132
|
+
// (flat centers the circular box and its title on the chart's
|
|
15133
|
+
// bounding-box CENTER — maptheme.js 18793-18798 — not on its anchor: a
|
|
15134
|
+
// star cluster's satellites spread asymmetrically around it; the
|
|
15135
|
+
// circle's title baselines are measured on the resized box, 18798-18830)
|
|
14591
15136
|
const circular = flatFlag(this.flags, 'CIRCULAR');
|
|
14592
|
-
const
|
|
14593
|
-
|
|
14594
|
-
|
|
14595
|
-
|
|
14596
|
-
|
|
14597
|
-
|
|
14598
|
-
|
|
14599
|
-
|
|
14600
|
-
|
|
14601
|
-
|
|
14602
|
-
|
|
14603
|
-
|
|
14604
|
-
|
|
14605
|
-
|
|
14606
|
-
|
|
14607
|
-
|
|
14608
|
-
|
|
14609
|
-
|
|
14610
|
-
|
|
14611
|
-
|
|
14612
|
-
|
|
14613
|
-
|
|
14614
|
-
|
|
14615
|
-
|
|
14616
|
-
|
|
14617
|
-
|
|
14618
|
-
|
|
14619
|
-
|
|
14620
|
-
|
|
14621
|
-
|
|
14622
|
-
|
|
14623
|
-
|
|
14624
|
-
|
|
14625
|
-
|
|
14626
|
-
|
|
14627
|
-
|
|
14628
|
-
|
|
14629
|
-
|
|
14630
|
-
|
|
14631
|
-
|
|
14632
|
-
|
|
14633
|
-
|
|
14634
|
-
|
|
14635
|
-
|
|
14636
|
-
|
|
14637
|
-
|
|
14638
|
-
|
|
14639
|
-
|
|
14640
|
-
|
|
14641
|
-
|
|
14642
|
-
|
|
14643
|
-
|
|
14644
|
-
|
|
14645
|
-
|
|
14646
|
-
|
|
14647
|
-
|
|
14648
|
-
|
|
14649
|
-
|
|
14650
|
-
const
|
|
14651
|
-
|
|
14652
|
-
|
|
14653
|
-
|
|
14654
|
-
|
|
14655
|
-
id: `ix-seqbox-${this.name}-g${this._iconGeneration}`,
|
|
14656
|
-
data: boxLayout, pickable: false,
|
|
14657
|
-
getPosition: b => b.d.geometry.coordinates,
|
|
14658
|
-
getIcon: b => this._buildSingleIcon(boxRgb, b.boxOpacity, 'circle', borderRgb,
|
|
14659
|
-
borderNone ? 0 : Math.round(unit / 20 * BUBBLE_ICON_SIZE / Math.max(1, 2 * b.boxR) * 4) / 4),
|
|
14660
|
-
getSize: b => b.boxR * 2,
|
|
14661
|
-
getPixelOffset: b => { const o = alignOf(b.d); return [o[0] + b.cx, o[1] + b.cy]; },
|
|
14662
|
-
sizeUnits: 'pixels', billboard: true,
|
|
14663
|
-
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14664
|
-
}));
|
|
14665
|
-
else layers.push(new ScatterplotLayer({
|
|
14666
|
-
id: `ix-seqbox-${this.name}`,
|
|
14667
|
-
data: boxLayout, pickable: false,
|
|
14668
|
-
getPosition: b => {
|
|
14669
|
-
// the circle at the chart's bbox center: with no align
|
|
14670
|
-
// offset it moves in map coordinates
|
|
14671
|
-
const o = pixelOffsetLngLat(b.d.geometry.coordinates, [b.cx, b.cy], liveZoom);
|
|
14672
|
-
return o;
|
|
14673
|
-
},
|
|
14674
|
-
getRadius: b => b.boxR,
|
|
14675
|
-
radiusUnits: 'pixels',
|
|
14676
|
-
stroked: !borderNone,
|
|
14677
|
-
getFillColor: b => [...boxRgb.slice(0, 3), Math.round(255 * b.boxOpacity)],
|
|
14678
|
-
getLineColor: [...(borderRgb || boxRgb).slice(0, 3), 255],
|
|
14679
|
-
getLineWidth: unit / 20,
|
|
14680
|
-
lineWidthUnits: 'pixels',
|
|
14681
|
-
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14682
|
-
}));
|
|
14683
|
-
} else {
|
|
14684
|
-
// flat's rectangular box (the chart and its title plus the
|
|
14685
|
-
// margin), the single-symbol path's own _buildBoxIcon drawing
|
|
14686
|
-
const borderStyle = String(st.borderstyle || '');
|
|
14687
|
-
const boxLook = {
|
|
14688
|
-
fill: st.boxcolor || 'white',
|
|
14689
|
-
fillOpacity: isNaN(styleNum(st.boxopacity)) ? 1 : styleNum(st.boxopacity),
|
|
14690
|
-
stroke: borderStyle === 'none' || isNoneColor(styleLineColor(st.bordercolor)) ? null : styleLineColor(st.bordercolor) || '#bbbbbb',
|
|
14691
|
-
strokeOpacity: /^(solid|dotted|dashed)$/.test(borderStyle) ? 1 : 0.3
|
|
14692
|
-
};
|
|
14693
|
-
const borderWidth = styleNum(st.borderwidth) || 1;
|
|
14694
|
-
const borderRadius = styleNum(st.borderradius);
|
|
14695
|
-
const strokeOf = b => borderWidth * 0.2 * b.w / 30;
|
|
14696
|
-
boxLayout.forEach(b => {
|
|
14697
|
-
b.icon = this._buildBoxIcon(b.rect[2] - b.rect[0], b.rect[3] - b.rect[1],
|
|
14698
|
-
isNaN(borderRadius) ? 0 : borderRadius * Math.min(unit, b.w / 30),
|
|
14699
|
-
strokeOf(b), boxLook);
|
|
15137
|
+
const boxLayoutAll = this._boxRows(() => charts.filter(d => d._seqExtent), { unit, liveZoom, extentOf: d => d._seqExtent });
|
|
15138
|
+
// per-chart overlap bands (see the band computation in the
|
|
15139
|
+
// box block): a Map chart → band index, filled only when
|
|
15140
|
+
// boxes are drawn; empty (single band) otherwise
|
|
15141
|
+
const bandOfChart = new Map();
|
|
15142
|
+
let nBands = 1;
|
|
15143
|
+
if (boxLayoutAll) {
|
|
15144
|
+
// flat draws every chart as ONE group — its box, its symbols,
|
|
15145
|
+
// its title, its values together — and the groups are sorted by
|
|
15146
|
+
// size, so a bigger chart covers a smaller one ENTIRELY
|
|
15147
|
+
// (chartMap's ascending sort, the biggest drawn last). deck.gl
|
|
15148
|
+
// stacks whole layers, so with every box in one layer below
|
|
15149
|
+
// every symbol, a bigger chart's box covered a smaller chart's
|
|
15150
|
+
// box while that chart's symbols floated on top of it (the
|
|
15151
|
+
// City-moving page's overlapping circles). Charts are grouped
|
|
15152
|
+
// into overlap "bands" instead — an interval coloring assigned
|
|
15153
|
+
// biggest-chart-first: within a band no two charts overlap
|
|
15154
|
+
// (their box + title extents, in map pixel space, disjoint),
|
|
15155
|
+
// and a chart is always in a band above every smaller chart it
|
|
15156
|
+
// overlaps. Each band then draws its charts' complete group
|
|
15157
|
+
// (box, title, glow, symbols, values), the smallest band first
|
|
15158
|
+
// — deck.gl draws the first layer at the bottom — so between
|
|
15159
|
+
// two overlapping charts the bigger one is wholly on top, as
|
|
15160
|
+
// flat draws it. Overlap-free themes keep one band and today's
|
|
15161
|
+
// exact single layers
|
|
15162
|
+
const world = 512 * Math.pow(2, liveZoom);
|
|
15163
|
+
const anchorPx = d => {
|
|
15164
|
+
const o = alignOf(d), c = d.geometry.coordinates;
|
|
15165
|
+
const lat = c[1] * Math.PI / 180;
|
|
15166
|
+
return [(c[0] + 180) / 360 * world + o[0],
|
|
15167
|
+
(0.5 - Math.log(Math.tan(Math.PI / 4 + lat / 2)) / (2 * Math.PI)) * world + o[1]];
|
|
15168
|
+
};
|
|
15169
|
+
const sizeOfBand = b => circular ? b.boxR
|
|
15170
|
+
: b.rect ? Math.max(b.rect[2] - b.rect[0], b.rect[3] - b.rect[1]) / 2 : 0;
|
|
15171
|
+
const bandRects = [];
|
|
15172
|
+
for (const b of boxLayoutAll.slice().sort((a, c) => sizeOfBand(c) - sizeOfBand(a))) {
|
|
15173
|
+
// the chart's whole drawn extent: parts cluster, box and
|
|
15174
|
+
// title, as px around the (align-moved) anchor
|
|
15175
|
+
const e = b.d._seqExtent || [0, 0, 0, 0];
|
|
15176
|
+
let x0 = e[0], y0 = e[1], x1 = e[2], y1 = e[3];
|
|
15177
|
+
if (circular) { x0 = Math.min(x0, b.cx - b.boxR); y0 = Math.min(y0, b.cy - b.boxR); x1 = Math.max(x1, b.cx + b.boxR); y1 = Math.max(y1, b.cy + b.boxR); }
|
|
15178
|
+
else if (b.rect) { x0 = Math.min(x0, b.rect[0]); y0 = Math.min(y0, b.rect[1]); x1 = Math.max(x1, b.rect[2]); y1 = Math.max(y1, b.rect[3]); }
|
|
15179
|
+
if (b.title) { x0 = Math.min(x0, b.title.extent[0]); y0 = Math.min(y0, b.title.extent[1]); x1 = Math.max(x1, b.title.extent[2]); y1 = Math.max(y1, b.title.extent[3]); }
|
|
15180
|
+
const p = anchorPx(b.d);
|
|
15181
|
+
const r = [p[0] + x0, p[1] + y0, p[0] + x1, p[1] + y1];
|
|
15182
|
+
let k = 0;
|
|
15183
|
+
while (bandRects[k] && bandRects[k].some(q => r[0] < q[2] && q[0] < r[2] && r[1] < q[3] && q[1] < r[3])) k++;
|
|
15184
|
+
(bandRects[k] = bandRects[k] || []).push(r);
|
|
15185
|
+
bandOfChart.set(b.d, k);
|
|
15186
|
+
}
|
|
15187
|
+
nBands = bandRects.length;
|
|
15188
|
+
}
|
|
15189
|
+
// one band's whole chart group — box, title, glow, symbols and
|
|
15190
|
+
// values of ITS charts only (the closure's boxLayout, glows and
|
|
15191
|
+
// parts are the band's slices) — pushed as one deck.gl layer
|
|
15192
|
+
// sequence, in today's order. bs: the band's id suffix
|
|
15193
|
+
const drawBand = (boxLayout, glows, parts, bs) => {
|
|
15194
|
+
if (boxLayout) {
|
|
15195
|
+
const L = this._boxTitleLayers(boxLayout, {
|
|
15196
|
+
unit, liveZoom,
|
|
15197
|
+
positionOf: b => b.d.geometry.coordinates, offsetOf: b => alignOf(b.d),
|
|
15198
|
+
ids: { circle: `ix-seqbox-${this.name}${bs}`, icon: `ix-seqbox-${this.name}${bs}`,
|
|
15199
|
+
polygons: `ix-seqbox-polygons-${this.name}${bs}`, titles: `ix-seqtitles-${this.name}${bs}` }
|
|
14700
15200
|
});
|
|
14701
|
-
|
|
14702
|
-
|
|
14703
|
-
|
|
14704
|
-
|
|
14705
|
-
|
|
14706
|
-
|
|
14707
|
-
|
|
15201
|
+
layers.push(...L.box, ...L.titles);
|
|
15202
|
+
}
|
|
15203
|
+
if (glows.length && this.flags.has('GLOW') && !glowHiddenByScale(st, liveZoom)) {
|
|
15204
|
+
layers.push(new IconLayer({
|
|
15205
|
+
id: `ix-glow-${this.name}${bs}-g${this._iconGeneration}`,
|
|
15206
|
+
data: glows, pickable: false,
|
|
15207
|
+
getPosition: d => d.geometry.coordinates,
|
|
15208
|
+
getIcon: d => this._getGlowIcon(colorOf(d.part.i)),
|
|
15209
|
+
getSize: d => d.part.r * 11,
|
|
15210
|
+
getPixelOffset: offsetOf,
|
|
15211
|
+
getColor: d => [255, 255, 255, alphaOf(d)],
|
|
14708
15212
|
sizeUnits: 'pixels', billboard: true,
|
|
14709
15213
|
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14710
15214
|
}));
|
|
14711
|
-
}
|
|
14712
|
-
|
|
14713
|
-
|
|
14714
|
-
|
|
14715
|
-
|
|
14716
|
-
|
|
14717
|
-
|
|
14718
|
-
|
|
14719
|
-
|
|
14720
|
-
sizeUnits: 'pixels',
|
|
14721
|
-
characterSet: 'auto',
|
|
14722
|
-
lineHeight: 1,
|
|
14723
|
-
getColor: [...hexOrNamedToRgb(st.textcolor || '#888888').slice(0, 3), 255],
|
|
14724
|
-
fontSettings: { sdf: true },
|
|
14725
|
-
outlineWidth: 1 / 7,
|
|
14726
|
-
outlineColor: [255, 255, 255, 128],
|
|
14727
|
-
getTextAnchor: b => b.title.anchor,
|
|
14728
|
-
getAlignmentBaseline: 'top',
|
|
14729
|
-
getPixelOffset: b => { const o = alignOf(b.d); return [b.title.x + o[0], b.title.top + o[1]]; },
|
|
15215
|
+
} else if (glows.length && this.flags.has('AURA') && !glowHiddenByScale(st, liveZoom)) {
|
|
15216
|
+
layers.push(new IconLayer({
|
|
15217
|
+
id: `ix-aura-${this.name}${bs}-g${this._iconGeneration}`,
|
|
15218
|
+
data: glows, pickable: false,
|
|
15219
|
+
getPosition: d => d.geometry.coordinates,
|
|
15220
|
+
getIcon: d => this._buildSingleIcon(borderRgb || colorOf(d.part.i), 0.3, 'circle', null, 0),
|
|
15221
|
+
getSize: d => d.part.r * 2 * 1.3,
|
|
15222
|
+
getPixelOffset: offsetOf,
|
|
15223
|
+
getColor: d => [255, 255, 255, alphaOf(d)],
|
|
15224
|
+
sizeUnits: 'pixels', billboard: true,
|
|
14730
15225
|
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14731
15226
|
}));
|
|
14732
15227
|
}
|
|
14733
|
-
}
|
|
14734
|
-
if (glows.length && this.flags.has('GLOW') && !glowHiddenByScale(st, liveZoom)) {
|
|
14735
|
-
layers.push(new IconLayer({
|
|
14736
|
-
id: `ix-glow-${this.name}-g${this._iconGeneration}`,
|
|
14737
|
-
data: glows, pickable: false,
|
|
14738
|
-
getPosition: d => d.geometry.coordinates,
|
|
14739
|
-
getIcon: d => this._getGlowIcon(colorOf(d.part.i)),
|
|
14740
|
-
getSize: d => d.part.r * 11,
|
|
14741
|
-
getPixelOffset: offsetOf,
|
|
14742
|
-
getColor: d => [255, 255, 255, alphaOf(d)],
|
|
14743
|
-
sizeUnits: 'pixels', billboard: true,
|
|
14744
|
-
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14745
|
-
}));
|
|
14746
|
-
} else if (glows.length && this.flags.has('AURA') && !glowHiddenByScale(st, liveZoom)) {
|
|
14747
15228
|
layers.push(new IconLayer({
|
|
14748
|
-
id: `ix-
|
|
14749
|
-
data:
|
|
15229
|
+
id: `ix-bubbles-${this.name}${bs}-g${this._iconGeneration}`,
|
|
15230
|
+
data: parts, pickable: true,
|
|
14750
15231
|
getPosition: d => d.geometry.coordinates,
|
|
14751
|
-
|
|
14752
|
-
|
|
15232
|
+
// the stroke, in the icon raster's pixels (the icon is drawn at 2r)
|
|
15233
|
+
getIcon: d => {
|
|
15234
|
+
// an external symbol file: the URL icon as flat's <image> (see
|
|
15235
|
+
// urlSymbolOf above), scaled to the part's diameter like flat's
|
|
15236
|
+
// fu.scale(nRadius / nMaxRadius) on a nChartSize image
|
|
15237
|
+
const u = urlSymbolOf(d.part.i);
|
|
15238
|
+
if (u) return urlIcon(u);
|
|
15239
|
+
return this._buildSingleIcon(colorOf(d.part.i), noFill.has(d.part.i) ? 0 : fillOpacity, shapeOf(d.part.i), borderRgb,
|
|
15240
|
+
borderRgb ? Math.round(lineWidth * unit * Math.min(2 * d.part.r / normalR, 0.2) * BUBBLE_ICON_SIZE / Math.max(1, 2 * d.part.r) * 4) / 4 : 0);
|
|
15241
|
+
},
|
|
15242
|
+
getSize: d => d.part.r * 2,
|
|
14753
15243
|
getPixelOffset: offsetOf,
|
|
14754
15244
|
getColor: d => [255, 255, 255, alphaOf(d)],
|
|
14755
15245
|
sizeUnits: 'pixels', billboard: true,
|
|
14756
15246
|
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
14757
15247
|
}));
|
|
14758
|
-
|
|
14759
|
-
|
|
14760
|
-
|
|
14761
|
-
|
|
14762
|
-
|
|
14763
|
-
//
|
|
14764
|
-
|
|
14765
|
-
|
|
14766
|
-
|
|
14767
|
-
|
|
14768
|
-
|
|
14769
|
-
|
|
14770
|
-
|
|
14771
|
-
|
|
14772
|
-
|
|
14773
|
-
|
|
14774
|
-
|
|
14775
|
-
|
|
14776
|
-
|
|
14777
|
-
|
|
14778
|
-
|
|
14779
|
-
|
|
14780
|
-
|
|
14781
|
-
|
|
14782
|
-
|
|
14783
|
-
|
|
14784
|
-
|
|
14785
|
-
|
|
14786
|
-
|
|
14787
|
-
|
|
14788
|
-
|
|
14789
|
-
|
|
14790
|
-
|
|
14791
|
-
|
|
14792
|
-
|
|
14793
|
-
|
|
14794
|
-
|
|
14795
|
-
|
|
14796
|
-
|
|
14797
|
-
|
|
14798
|
-
|
|
14799
|
-
|
|
14800
|
-
|
|
14801
|
-
|
|
14802
|
-
|
|
14803
|
-
|
|
14804
|
-
|
|
14805
|
-
|
|
14806
|
-
|
|
14807
|
-
|
|
14808
|
-
|
|
14809
|
-
|
|
14810
|
-
|
|
14811
|
-
|
|
14812
|
-
|
|
14813
|
-
|
|
14814
|
-
|
|
14815
|
-
|
|
14816
|
-
|
|
14817
|
-
|
|
14818
|
-
|
|
14819
|
-
|
|
14820
|
-
|
|
14821
|
-
|
|
14822
|
-
|
|
14823
|
-
|
|
14824
|
-
|
|
14825
|
-
|
|
14826
|
-
|
|
14827
|
-
|
|
14828
|
-
|
|
14829
|
-
|
|
14830
|
-
|
|
14831
|
-
|
|
14832
|
-
|
|
14833
|
-
const plainLabels = labels.filter(l => !l.image);
|
|
14834
|
-
if (haloLabels.length) {
|
|
14835
|
-
// a real outline around the characters: the label's own
|
|
14836
|
-
// glyphs re-drawn in white on rings of pixel offsets — a
|
|
14837
|
-
// stroke of copies that hugs the character shapes (deck's
|
|
14838
|
-
// SDF outline is only a faint, buffer-clipped rim). The
|
|
14839
|
-
// base radius is a small fraction of the font size (the
|
|
14840
|
-
// 1px floor keeps it tiny until labels get big), and the
|
|
14841
|
-
// ring alphas fall off, so the border fades to nothing
|
|
14842
|
-
const DIRS = 16, RINGS = [[1, 210], [2, 120], [3.2, 65]];
|
|
14843
|
-
const haloCopies = [];
|
|
14844
|
-
for (const l of haloLabels) {
|
|
14845
|
-
const rb = Math.max(0.75, l.fontSize * 0.015);
|
|
14846
|
-
for (let ring = 0; ring < RINGS.length; ring++) {
|
|
14847
|
-
for (let dir = 0; dir < DIRS; dir++) {
|
|
14848
|
-
const a = (dir / DIRS) * 2 * Math.PI;
|
|
14849
|
-
haloCopies.push({ ...l,
|
|
14850
|
-
dx: Math.cos(a), dy: Math.sin(a),
|
|
14851
|
-
radius: rb * RINGS[ring][0],
|
|
14852
|
-
haloAlpha: Math.round(RINGS[ring][1] * l.alpha / 255) });
|
|
15248
|
+
// VALUES on each part (maptheme.js 22188-22260): the part's value — with
|
|
15249
|
+
// COUNT its record count, with valuefield on the categorical field the
|
|
15250
|
+
// class name — formatted with valuedecimals (else none) plus the unit.
|
|
15251
|
+
// - a SHAPE part (circle & kin): font min(1, 3.2 / L) · r · valuescale,
|
|
15252
|
+
// L the length of the theme's biggest value text (szMaxText), normal
|
|
15253
|
+
// weight, in valuecolor / textcolor, else ChartColors.textColor of
|
|
15254
|
+
// the part color
|
|
15255
|
+
// - an IMAGE-symbol part (an external .svg, the TELRAAM pictograms):
|
|
15256
|
+
// flat's own other branch (22253-22256) — font max(r · 0.8, 1) ·
|
|
15257
|
+
// valuescale, the text over createTextLabel's background box (white,
|
|
15258
|
+
// its rgba(…,0.3) fill × the 0.5 fill-opacity) and in
|
|
15259
|
+
// ChartColors.lowColor — the 0.7 derivate of the part color —
|
|
15260
|
+
// regardless of valuecolor/textcolor (flat passes cColor.lowColor
|
|
15261
|
+
// straight into createTextLabel)
|
|
15262
|
+
// CENTERVALUES: only on the first part (class 0), its own value
|
|
15263
|
+
if ((this.flags.has('VALUES') || centerValues) && !valuesHiddenByScale(st, zoom)) {
|
|
15264
|
+
const valueScale = styleNum(st.valuescale) || 1;
|
|
15265
|
+
const { noBreaks } = this._valueTextOpts();
|
|
15266
|
+
const units = st.units ? String(st.units) : '';
|
|
15267
|
+
const unit = units ? (units[0] === '.' ? '' : ' ') + units : '';
|
|
15268
|
+
const unitText = unit.length <= 5 ? unit : '';
|
|
15269
|
+
const dec = explicitValueDecimals(st) || 0;
|
|
15270
|
+
const fmt = v => flatGroupedValue(v, dec, noBreaks) + unitText;
|
|
15271
|
+
const maxLen = fmt(seqMax).length;
|
|
15272
|
+
const textOverride = st.valuecolor || st.textcolor;
|
|
15273
|
+
const counting = flatFlag(this.flags, 'COUNT');
|
|
15274
|
+
const labels = [];
|
|
15275
|
+
parts.forEach(d => {
|
|
15276
|
+
if (centerValues && d.part.i !== 0) return;
|
|
15277
|
+
const rec = this._categoryValueRecord(d.part.i);
|
|
15278
|
+
const name = rec ? String(rec[st.valuefield]) : null;
|
|
15279
|
+
const t = counting && d.properties.recordCounts ? d.properties.recordCounts[d.part.i] : d.part.v;
|
|
15280
|
+
const text = name != null ? name : fmt(t);
|
|
15281
|
+
const image = !!urlSymbolOf(d.part.i);
|
|
15282
|
+
const L = Math.max((name != null ? name.length : 0) + 1, maxLen);
|
|
15283
|
+
const fontSize = image
|
|
15284
|
+
? Math.max(d.part.r * 0.8, 1) * valueScale
|
|
15285
|
+
: Math.min(1, 3.2 / L) * d.part.r * valueScale;
|
|
15286
|
+
if (fontSize > VALUES_MIN_FONT_PX) {
|
|
15287
|
+
labels.push({ geometry: d.geometry, text, fontSize, pixelOffset: offsetOf(d), alpha: alphaOf(d),
|
|
15288
|
+
image,
|
|
15289
|
+
color: image ? flatDerivateRgb(colorOf(d.part.i), 0.7)
|
|
15290
|
+
: textOverride ? hexOrNamedToRgb(textOverride) : flatChartTextRgb(colorOf(d.part.i)) });
|
|
15291
|
+
}
|
|
15292
|
+
});
|
|
15293
|
+
if (labels.length) {
|
|
15294
|
+
// VALUEBACKGROUND on URL-symbol parts: a fading white border
|
|
15295
|
+
// around the value's own CHARACTER SHAPES — the glyphs
|
|
15296
|
+
// re-drawn in white on rings of pixel offsets (a stroke of
|
|
15297
|
+
// copies, no box): the width is a fraction of the font size,
|
|
15298
|
+
// so the halo scales with every label; the shape-part values
|
|
15299
|
+
// keep flat's plain text (no outline)
|
|
15300
|
+
if (flatFlag(this.flags, 'VALUEBACKGROUND')) {
|
|
15301
|
+
const haloLabels = labels.filter(l => l.image);
|
|
15302
|
+
const plainLabels = labels.filter(l => !l.image);
|
|
15303
|
+
if (haloLabels.length) {
|
|
15304
|
+
// a real outline around the characters: the label's own
|
|
15305
|
+
// glyphs re-drawn in white on rings of pixel offsets — a
|
|
15306
|
+
// stroke of copies that hugs the character shapes (deck's
|
|
15307
|
+
// SDF outline is only a faint, buffer-clipped rim). The
|
|
15308
|
+
// base radius is a small fraction of the font size (the
|
|
15309
|
+
// 1px floor keeps it tiny until labels get big), and the
|
|
15310
|
+
// ring alphas fall off, so the border fades to nothing
|
|
15311
|
+
const DIRS = 16, RINGS = [[1, 210], [2, 120], [3.2, 65]];
|
|
15312
|
+
const haloCopies = [];
|
|
15313
|
+
for (const l of haloLabels) {
|
|
15314
|
+
const rb = Math.max(0.75, l.fontSize * 0.015);
|
|
15315
|
+
for (let ring = 0; ring < RINGS.length; ring++) {
|
|
15316
|
+
for (let dir = 0; dir < DIRS; dir++) {
|
|
15317
|
+
const a = (dir / DIRS) * 2 * Math.PI;
|
|
15318
|
+
haloCopies.push({ ...l,
|
|
15319
|
+
dx: Math.cos(a), dy: Math.sin(a),
|
|
15320
|
+
radius: rb * RINGS[ring][0],
|
|
15321
|
+
haloAlpha: Math.round(RINGS[ring][1] * l.alpha / 255) });
|
|
15322
|
+
}
|
|
14853
15323
|
}
|
|
14854
15324
|
}
|
|
15325
|
+
layers.push(new TextLayer({
|
|
15326
|
+
id: `ix-cluster-values-halo-${this.name}${bs}`,
|
|
15327
|
+
data: haloCopies, pickable: false,
|
|
15328
|
+
getPosition: d => d.geometry.coordinates,
|
|
15329
|
+
getText: d => d.text,
|
|
15330
|
+
getSize: d => d.fontSize,
|
|
15331
|
+
getColor: d => [255, 255, 255, d.haloAlpha],
|
|
15332
|
+
getPixelOffset: d => [d.pixelOffset[0] + d.dx * d.radius, d.pixelOffset[1] + d.dy * d.radius],
|
|
15333
|
+
sizeUnits: 'pixels', fontFamily: 'arial', fontWeight: 'normal',
|
|
15334
|
+
characterSet: 'auto',
|
|
15335
|
+
getTextAnchor: 'middle', getAlignmentBaseline: 'center'
|
|
15336
|
+
}));
|
|
15337
|
+
layers.push(new TextLayer({
|
|
15338
|
+
id: `ix-cluster-values-${this.name}${bs}`,
|
|
15339
|
+
data: haloLabels, pickable: false,
|
|
15340
|
+
getPosition: d => d.geometry.coordinates,
|
|
15341
|
+
getText: d => d.text,
|
|
15342
|
+
getSize: d => d.fontSize,
|
|
15343
|
+
getColor: d => [...d.color.slice(0, 3), d.alpha],
|
|
15344
|
+
getPixelOffset: d => d.pixelOffset,
|
|
15345
|
+
sizeUnits: 'pixels', fontFamily: 'arial', fontWeight: 'normal',
|
|
15346
|
+
characterSet: 'auto',
|
|
15347
|
+
getTextAnchor: 'middle', getAlignmentBaseline: 'center'
|
|
15348
|
+
}));
|
|
14855
15349
|
}
|
|
14856
|
-
layers.push(new TextLayer({
|
|
14857
|
-
id: `ix-cluster-values-
|
|
14858
|
-
data:
|
|
14859
|
-
getPosition: d => d.geometry.coordinates,
|
|
14860
|
-
getText: d => d.text,
|
|
14861
|
-
getSize: d => d.fontSize,
|
|
14862
|
-
getColor: d => [255, 255, 255, d.haloAlpha],
|
|
14863
|
-
getPixelOffset: d => [d.pixelOffset[0] + d.dx * d.radius, d.pixelOffset[1] + d.dy * d.radius],
|
|
14864
|
-
sizeUnits: 'pixels', fontFamily: 'arial', fontWeight: 'normal',
|
|
14865
|
-
characterSet: 'auto',
|
|
14866
|
-
getTextAnchor: 'middle', getAlignmentBaseline: 'center'
|
|
14867
|
-
}));
|
|
14868
|
-
layers.push(new TextLayer({
|
|
14869
|
-
id: `ix-cluster-values-${this.name}`,
|
|
14870
|
-
data: haloLabels, pickable: false,
|
|
15350
|
+
if (plainLabels.length) layers.push(new TextLayer({
|
|
15351
|
+
id: `ix-cluster-values-plain-${this.name}${bs}`,
|
|
15352
|
+
data: plainLabels, pickable: false,
|
|
14871
15353
|
getPosition: d => d.geometry.coordinates,
|
|
14872
15354
|
getText: d => d.text,
|
|
14873
15355
|
getSize: d => d.fontSize,
|
|
@@ -14877,10 +15359,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14877
15359
|
characterSet: 'auto',
|
|
14878
15360
|
getTextAnchor: 'middle', getAlignmentBaseline: 'center'
|
|
14879
15361
|
}));
|
|
14880
|
-
}
|
|
14881
|
-
|
|
14882
|
-
id: `ix-cluster-values
|
|
14883
|
-
data:
|
|
15362
|
+
} else {
|
|
15363
|
+
layers.push(new TextLayer({
|
|
15364
|
+
id: `ix-cluster-values-${this.name}${bs}`,
|
|
15365
|
+
data: labels, pickable: false,
|
|
14884
15366
|
getPosition: d => d.geometry.coordinates,
|
|
14885
15367
|
getText: d => d.text,
|
|
14886
15368
|
getSize: d => d.fontSize,
|
|
@@ -14890,21 +15372,20 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14890
15372
|
characterSet: 'auto',
|
|
14891
15373
|
getTextAnchor: 'middle', getAlignmentBaseline: 'center'
|
|
14892
15374
|
}));
|
|
14893
|
-
|
|
14894
|
-
layers.push(new TextLayer({
|
|
14895
|
-
id: `ix-cluster-values-${this.name}`,
|
|
14896
|
-
data: labels, pickable: false,
|
|
14897
|
-
getPosition: d => d.geometry.coordinates,
|
|
14898
|
-
getText: d => d.text,
|
|
14899
|
-
getSize: d => d.fontSize,
|
|
14900
|
-
getColor: d => [...d.color.slice(0, 3), d.alpha],
|
|
14901
|
-
getPixelOffset: d => d.pixelOffset,
|
|
14902
|
-
sizeUnits: 'pixels', fontFamily: 'arial', fontWeight: 'normal',
|
|
14903
|
-
characterSet: 'auto',
|
|
14904
|
-
getTextAnchor: 'middle', getAlignmentBaseline: 'center'
|
|
14905
|
-
}));
|
|
15375
|
+
}
|
|
14906
15376
|
}
|
|
14907
15377
|
}
|
|
15378
|
+
};
|
|
15379
|
+
const chartBand = c => bandOfChart.get(c) || 0;
|
|
15380
|
+
if (nBands > 1) {
|
|
15381
|
+
// deck.gl draws the FIRST layer at the bottom: the smallest
|
|
15382
|
+
// charts' band first, the biggest (band 0) last, on top
|
|
15383
|
+
for (let k = nBands - 1; k >= 0; k--) drawBand(
|
|
15384
|
+
boxLayoutAll ? boxLayoutAll.filter(b => chartBand(b.d) === k) : null,
|
|
15385
|
+
allGlows.filter(g => chartBand(g.chart) === k),
|
|
15386
|
+
allParts.filter(p => chartBand(p.chart) === k), '-b' + k);
|
|
15387
|
+
} else {
|
|
15388
|
+
drawBand(boxLayoutAll, allGlows, allParts, '');
|
|
14908
15389
|
}
|
|
14909
15390
|
return layers;
|
|
14910
15391
|
}
|
|
@@ -14964,8 +15445,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
14964
15445
|
// CENTERVALUE the item's record count
|
|
14965
15446
|
// Not drawn here (a console note at type parsing, PIE_INERT_FLAGS,
|
|
14966
15447
|
// PIE_AGGREGATE_INERT_FLAGS):
|
|
14967
|
-
// 3D, VOLUME, HEIGHT,
|
|
14968
|
-
//
|
|
15448
|
+
// 3D, VOLUME, HEIGHT, BOW, ZOOM, DIRECTION / DIRECTED — the pie is
|
|
15449
|
+
// drawn flat and plain instead; POLAR without STARBURST has no
|
|
15450
|
+
// effect in flat either. STARBURST (with the FLOWER/RAYS angle/gap
|
|
15451
|
+
// variants) and its POLAR circles ARE drawn (pieSliceLayout). Nor
|
|
14969
15452
|
// style.clipparts, showparts, gridx (flat's several pies per item).
|
|
14970
15453
|
// Known divergence: flat draws a chart's value labels in the chart's
|
|
14971
15454
|
// own group, so a bigger pie drawn later covers a smaller neighbour's
|
|
@@ -15029,6 +15512,17 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15029
15512
|
const lineRgb = line === 'none' ? null : hexOrNamedToRgb(line);
|
|
15030
15513
|
const lineWidthOf = r => (lineRgb ? (styleNum(st.linewidth) || 0.1) * r / 15 : 0);
|
|
15031
15514
|
const lineRgba = lineRgb ? [...lineRgb.slice(0, 3), Math.round(255 * lineAlpha)] : [0, 0, 0, 0];
|
|
15515
|
+
// flat's STARBURST (maptheme.js 20262-20275, piechart.js 281-287,
|
|
15516
|
+
// 415-420): the parts get equal angles and a ray's radius carries
|
|
15517
|
+
// its value — inner + span · sqrt(v / maxValue) · scale, scale =
|
|
15518
|
+
// maxValue / (normalsizevalue || maxValue) · 2.14 (with SIZE flat
|
|
15519
|
+
// sets only maxValue, so a part at the reference value reaches
|
|
15520
|
+
// 2.14 R). maxValue: style.maxvalue (the reference — the ISTAT
|
|
15521
|
+
// story's national mean), else the biggest part value over the
|
|
15522
|
+
// items (flat's max of the per-part maxima nMaxA). The star's
|
|
15523
|
+
// inner px (the ray variants' 5px) comes off the layout
|
|
15524
|
+
const star = has(/STARBURST/);
|
|
15525
|
+
let starMaxPart = 0;
|
|
15032
15526
|
|
|
15033
15527
|
const charts = [];
|
|
15034
15528
|
if (agg) {
|
|
@@ -15043,6 +15537,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15043
15537
|
if (!layout) continue;
|
|
15044
15538
|
if (cat) layout.slices.forEach(sl => { sl.single = values.length === 1; });
|
|
15045
15539
|
const sum = values.reduce((a, v) => a + (isNaN(v) ? 0 : v), 0);
|
|
15540
|
+
if (star) for (const v of values) if (v > starMaxPart) starMaxPart = v;
|
|
15046
15541
|
const r = nSizeOf(sum, size) * unit;
|
|
15047
15542
|
if (!(r > 0) || !isFinite(r)) continue;
|
|
15048
15543
|
// flat's draw-order key (maptheme.js 16812-16860): with a size
|
|
@@ -15067,6 +15562,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15067
15562
|
const layout = pieSliceLayout(values, t, { centerPart: st.centerpart });
|
|
15068
15563
|
if (!layout) continue;
|
|
15069
15564
|
const sum = values.reduce((a, v) => a + v, 0);
|
|
15565
|
+
if (star) for (const v of values) if (v > starMaxPart) starMaxPart = v;
|
|
15070
15566
|
const size = this.binding.size ? parseFloat(raw[this.binding.size]) : undefined;
|
|
15071
15567
|
const r = nSizeOf(sum, size) * unit;
|
|
15072
15568
|
if (!(r > 0) || !isFinite(r)) continue;
|
|
@@ -15099,14 +15595,80 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15099
15595
|
});
|
|
15100
15596
|
|
|
15101
15597
|
const TAU = 2 * Math.PI;
|
|
15102
|
-
const slices = [], centers = [], glows = [];
|
|
15598
|
+
const slices = [], centers = [], glows = [], polar = [], polarDashes = [];
|
|
15103
15599
|
const glowOn = has(/GLOW/) && !has(/ZOOM/) && !glowHiddenByScale(st, liveZoom);
|
|
15600
|
+
// the STARBURST ray scale (see the star comment above): style
|
|
15601
|
+
// maxvalue, else the biggest part value over the items — flat's
|
|
15602
|
+
// `this.nMaxValue || max(nMaxA)`. Without SIZE flat also sets the
|
|
15603
|
+
// donut's normalSizeValue (normalsizevalue, else maxValue), which
|
|
15604
|
+
// dampens the scale
|
|
15605
|
+
const starMax = styleNum(st.maxvalue);
|
|
15606
|
+
const starRef = !isNaN(starMax) && starMax > 0 ? starMax : (starMaxPart > 0 ? starMaxPart : 1);
|
|
15607
|
+
const starNsv = has(/\bSIZE\b/) ? starRef : (nsv || starRef);
|
|
15608
|
+
const starScale = starRef / starNsv * 2.14;
|
|
15609
|
+
// a chart's rays start at the CENTER — flat's `max(5, nRadInner)`
|
|
15610
|
+
// for the ray variants is 5 units of the chart's internal
|
|
15611
|
+
// coordinate system, sub-pixel and invisible on screen (see
|
|
15612
|
+
// pieSliceLayout) — and only with DONUT at the donut's inner
|
|
15613
|
+
// radius (flat piechart.js 326-327: nRadOuter += nRadInner, the
|
|
15614
|
+
// span stays R)
|
|
15615
|
+
const starInnerOf = ch => (ch.layout.inner || 0) * ch.r;
|
|
15616
|
+
const starRadius = (v, R, inner) => Math.max(1, inner + (R - inner) * Math.sqrt(Math.max(0, v) / starRef) * starScale);
|
|
15104
15617
|
for (const ch of charts) {
|
|
15618
|
+
const starInner = star ? starInnerOf(ch) : 0;
|
|
15619
|
+
if (star && has(/POLAR/)) {
|
|
15620
|
+
// flat piechart.js 435-452: the STARBURST's reference circles —
|
|
15621
|
+
// a white disc at the reference value (fill-opacity 0.25,
|
|
15622
|
+
// #444444 border, solid) and range rings at 75 / 50 / 25 % of
|
|
15623
|
+
// it, the 75% and 25% rings DASHED (stroke-dasharray 50 50 in
|
|
15624
|
+
// the chart group's units, on for half of each cycle). All of
|
|
15625
|
+
// them under every ray. The stroke widths are flat's 1px /
|
|
15626
|
+
// 3px — in the CHART GROUP's coordinate system, whose scale
|
|
15627
|
+
// is ~0.017 of the screen, so on screen they are sub-pixel
|
|
15628
|
+
// hairlines (measured on the ISTAT story: the disc's 197
|
|
15629
|
+
// group-units radius is 11.2px on screen); 1px ≈ 1.1% and
|
|
15630
|
+
// 3px ≈ 3.2% of the base radius R, taken here as fractions
|
|
15631
|
+
// of ch.r so they stay hairlines at every zoom, as in flat
|
|
15632
|
+
const lw1 = ch.r * 0.0108, lw3 = ch.r * 0.0325;
|
|
15633
|
+
const w = ch.r - starInner;
|
|
15634
|
+
polar.push({ position: ch.c, r: starInner + w * starScale, off: ch.off,
|
|
15635
|
+
fill: [255, 255, 255, 64], line: [68, 68, 68, 255], lw: lw1 });
|
|
15636
|
+
polar.push({ position: ch.c, r: starInner + w * Math.sqrt(0.5) * starScale, off: ch.off,
|
|
15637
|
+
fill: [0, 0, 0, 0], line: [68, 68, 68, 255], lw: lw1 });
|
|
15638
|
+
// the dashed rings: one chord per dash — flat's own arcs are
|
|
15639
|
+
// octant polylines, so straight chords are as faithful. The
|
|
15640
|
+
// dash count is flat's geometry: a 50-unit dash on the ring's
|
|
15641
|
+
// group radius — the ring in group units is ~17.7 × its
|
|
15642
|
+
// screen radius (the chart group scale measured on the
|
|
15643
|
+
// story), so a cycle is 100 group units = 100/(17.7 · 2π ·
|
|
15644
|
+
// ringR_screen) of the circle: ~11 dashes around the 75%,
|
|
15645
|
+
// ~6 around the 25% ring, at every zoom
|
|
15646
|
+
for (const q of [0.75, 0.25]) {
|
|
15647
|
+
const rr = starInner + w * Math.sqrt(q) * starScale;
|
|
15648
|
+
const cycles = Math.max(q > 0.5 ? 4 : 3, Math.round((q > 0.5 ? 2.06 : 1.19) * ch.r));
|
|
15649
|
+
const seg = 2 * Math.PI / cycles;
|
|
15650
|
+
for (let i = 0; i < cycles; i++) {
|
|
15651
|
+
const a0 = i * seg, a1 = a0 + seg / 2;
|
|
15652
|
+
// 0 at 12 o'clock, clockwise (pieSliceLayout's convention);
|
|
15653
|
+
// pixelOffsetLngLat's y grows down
|
|
15654
|
+
polarDashes.push({
|
|
15655
|
+
path: [
|
|
15656
|
+
pixelOffsetLngLat(ch.c, [Math.sin(a0) * rr, -Math.cos(a0) * rr], liveZoom),
|
|
15657
|
+
pixelOffsetLngLat(ch.c, [Math.sin(a1) * rr, -Math.cos(a1) * rr], liveZoom)
|
|
15658
|
+
], lw: lw3
|
|
15659
|
+
});
|
|
15660
|
+
}
|
|
15661
|
+
}
|
|
15662
|
+
}
|
|
15105
15663
|
for (const sl of ch.layout.slices) {
|
|
15106
15664
|
const rgb = sliceRgb(sl);
|
|
15665
|
+
// STARBURST: this ray's own radius (the value), its inner the
|
|
15666
|
+
// px starInner as a fraction of that radius — the shader takes
|
|
15667
|
+
// a fraction of the instance radius, which varies per ray here
|
|
15668
|
+
const rayR = star ? starRadius(sl.value, ch.r, starInner) : ch.r;
|
|
15107
15669
|
slices.push({
|
|
15108
|
-
position: ch.c, r:
|
|
15109
|
-
slice: [sl.start * Math.PI / 180, sl.sweep >= 360 ? TAU : sl.sweep * Math.PI / 180, ch.layout.inner],
|
|
15670
|
+
position: ch.c, r: rayR, off: ch.off, properties: ch.properties, chart: ch, part: sl,
|
|
15671
|
+
slice: [sl.start * Math.PI / 180, sl.sweep >= 360 ? TAU : sl.sweep * Math.PI / 180, star ? starInner / rayR : ch.layout.inner],
|
|
15110
15672
|
fill: [...rgb.slice(0, 3), !sl.complement && noFill.has(sl.i) ? 0 : Math.round(255 * fillAlpha)]
|
|
15111
15673
|
});
|
|
15112
15674
|
}
|
|
@@ -15130,13 +15692,80 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15130
15692
|
radiusUnits: 'pixels', billboard: true, stroked: false, filled: true, antialiasing: true,
|
|
15131
15693
|
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
15132
15694
|
};
|
|
15133
|
-
|
|
15695
|
+
// flat's BOX / TITLE for a pie (maptheme.js 18446-18925, the same
|
|
15696
|
+
// block the sequence builder draws): a rectangle around the
|
|
15697
|
+
// chart's drawn extent — on a STARBURST the longest ray, the
|
|
15698
|
+
// disc otherwise — plus, with TITLE, the item's title (an
|
|
15699
|
+
// aggregated grid cell of more than 3 records without its own:
|
|
15700
|
+
// "(n)", see _itemTitle) at the chart's left edge, wrapped to
|
|
15701
|
+
// the chart's width, in textcolor (#888888) over a half-opaque
|
|
15702
|
+
// white halo, its first baseline 0.7 font above the box — the
|
|
15703
|
+
// box grows to enclose it, its top trimmed by 0.1 font. The
|
|
15704
|
+
// box: white at boxopacity, #bbbbbb border at 0.3 opacity
|
|
15705
|
+
// (borderstyle solid/dotted/dashed: 1, none: no border),
|
|
15706
|
+
// borderwidth · 0.2 · width/chartSize px wide. Both are removed
|
|
15707
|
+
// outside boxupper / titleupper (maptheme.js 18758-18760, the
|
|
15708
|
+
// TITLE gate 18809-18811)
|
|
15709
|
+
// the chart's drawn extent, px around the anchor (flat's getBox of the
|
|
15710
|
+
// shape group — the longest ray on a STARBURST)
|
|
15711
|
+
const boxRows = this._boxRows(charts, { unit, liveZoom, extentOf: ch => {
|
|
15712
|
+
let e = ch.r;
|
|
15713
|
+
if (star) { const in0 = starInnerOf(ch); for (const sl of ch.layout.slices) e = Math.max(e, starRadius(sl.value, ch.r, in0)); }
|
|
15714
|
+
return [-e, -e, e, e];
|
|
15715
|
+
} });
|
|
15716
|
+
const layers = [];
|
|
15717
|
+
if (boxRows && boxRows.length) {
|
|
15718
|
+
const L = this._boxTitleLayers(boxRows, {
|
|
15719
|
+
unit, liveZoom,
|
|
15720
|
+
positionOf: b => b.d.c, offsetOf: b => b.d.off,
|
|
15721
|
+
ids: { circle: `ix-pie-box-${this.name}`, icon: `ix-pie-box-${this.name}`, polygons: `ix-pie-box-polygons-${this.name}`, titles: `ix-pie-titles-${this.name}` }
|
|
15722
|
+
});
|
|
15723
|
+
layers.push(...L.box, ...L.titles);
|
|
15724
|
+
}
|
|
15725
|
+
if (polar.length) {
|
|
15726
|
+
layers.push(new ScatterplotLayer({
|
|
15727
|
+
id: `ix-pie-polar-${this.name}`,
|
|
15728
|
+
data: polar, pickable: false,
|
|
15729
|
+
getPosition: d => d.position,
|
|
15730
|
+
getRadius: d => d.r,
|
|
15731
|
+
getPixelOffset: d => d.off,
|
|
15732
|
+
radiusUnits: 'pixels', billboard: true,
|
|
15733
|
+
filled: true, stroked: true,
|
|
15734
|
+
getFillColor: d => d.fill,
|
|
15735
|
+
getLineColor: d => d.line,
|
|
15736
|
+
getLineWidth: d => d.lw,
|
|
15737
|
+
lineWidthUnits: 'pixels',
|
|
15738
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
15739
|
+
}));
|
|
15740
|
+
// flat's dashed range rings (see the polar block): one chord per
|
|
15741
|
+
// dash, drawn in map coordinates at the current zoom — the
|
|
15742
|
+
// chart anchors move on zoom, and the layers rebuild with them
|
|
15743
|
+
if (polarDashes.length) {
|
|
15744
|
+
layers.push(new PathLayer({
|
|
15745
|
+
id: `ix-pie-polar-dashes-${this.name}`,
|
|
15746
|
+
data: polarDashes, pickable: false,
|
|
15747
|
+
getPath: d => d.path,
|
|
15748
|
+
getColor: [68, 68, 68, 255],
|
|
15749
|
+
getWidth: d => d.lw,
|
|
15750
|
+
widthUnits: 'pixels',
|
|
15751
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
15752
|
+
}));
|
|
15753
|
+
}
|
|
15754
|
+
}
|
|
15755
|
+
layers.push(new PieSliceLayer(Object.assign({}, discProps, {
|
|
15134
15756
|
id: `ix-pie-${this.name}`,
|
|
15135
15757
|
data: slices, pickable: true,
|
|
15136
15758
|
getLineColor: lineRgba,
|
|
15137
15759
|
getSlice: d => d.slice,
|
|
15138
|
-
|
|
15139
|
-
|
|
15760
|
+
// flat's pie outline (piechart.js: normalX(linewidth) · R / 300)
|
|
15761
|
+
// is 2% of the radius — sub-pixel for every small chart, and the
|
|
15762
|
+
// browser rasterizes it as a HAIRLINE: one device pixel at full
|
|
15763
|
+
// opacity (Skia's stroke floor). The 1 device-px floor here
|
|
15764
|
+
// matches that: the width is in the shader's device-pixel
|
|
15765
|
+
// space (pieAA = fwidth), so the CSS width is scaled by the
|
|
15766
|
+
// device pixel ratio first
|
|
15767
|
+
getSliceLineWidth: d => (lineWidthOf(d.r) > 0 ? Math.max(lineWidthOf(d.r) * (global.devicePixelRatio || 1), 1) : 0)
|
|
15768
|
+
})));
|
|
15140
15769
|
if (centers.length) {
|
|
15141
15770
|
layers.push(new PieSliceLayer(Object.assign({}, discProps, { id: `ix-pie-center-${this.name}`, data: centers, pickable: true })));
|
|
15142
15771
|
}
|
|
@@ -15350,7 +15979,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15350
15979
|
// STACKED beside the column on leader lines; flat's values color
|
|
15351
15980
|
// #798697 (textcolor), negative red (valuecolor); DTEXT and a sized
|
|
15352
15981
|
// POINTER fade small values
|
|
15353
|
-
// - BOX / TITLE (
|
|
15982
|
+
// - BOX / TITLE (chartBoxLayout): a box around chart and title, the
|
|
15354
15983
|
// item's title above it — boxcolor (white) at boxopacity, bordercolor
|
|
15355
15984
|
// (#bbbbbb) at 0.3, borderwidth · 0.2 · w / 30; title 5 u ·
|
|
15356
15985
|
// textscale in textcolor (#888888) over a white halo
|
|
@@ -15446,9 +16075,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15446
16075
|
}
|
|
15447
16076
|
applyFlatMaxCharts(charts, st);
|
|
15448
16077
|
|
|
15449
|
-
|
|
15450
|
-
|
|
15451
|
-
|
|
16078
|
+
// a BAR draws its box as a shader shape (rectangle only, no CIRCULARBOX):
|
|
16079
|
+
// the gates, margin, title font and placement are the box's, shared with
|
|
16080
|
+
// every chart type (_boxSpec / chartBoxLayout, here in chart units)
|
|
16081
|
+
const boxSpec = this._boxSpec(liveZoom);
|
|
16082
|
+
const boxUnits = st.titleunits ? ' ' + st.titleunits : '';
|
|
15452
16083
|
const boxOpacity = isNaN(styleNum(st.boxopacity)) ? 1 : styleNum(st.boxopacity);
|
|
15453
16084
|
const borderStyle = String(st.borderstyle || '');
|
|
15454
16085
|
const boxFill = [...hexOrNamedToRgb(isNoneColor(st.boxcolor) ? 'white' : st.boxcolor || 'white').slice(0, 3), isNoneColor(st.boxcolor) ? 0 : Math.round(255 * boxOpacity)];
|
|
@@ -15477,10 +16108,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15477
16108
|
for (const ch of charts) {
|
|
15478
16109
|
const L = ch.layout;
|
|
15479
16110
|
let box = null;
|
|
15480
|
-
if (
|
|
15481
|
-
const title = titleShown ? this._itemTitle(ch.properties) : '';
|
|
15482
|
-
box =
|
|
15483
|
-
|
|
16111
|
+
if (boxSpec) {
|
|
16112
|
+
const title = boxSpec.titleShown ? this._itemTitle(ch.properties) : '';
|
|
16113
|
+
box = chartBoxLayout(L.extent, { unit: 1, margin: boxSpec.margin, titleText: title ? title + boxUnits : '',
|
|
16114
|
+
titleFont: w => boxSpec.titleFontOf(1, w), bottomTitle: boxSpec.bottomTitle, alignRight: boxSpec.alignRight, measure });
|
|
15484
16115
|
}
|
|
15485
16116
|
const ext = box ? box.rect : L.extent;
|
|
15486
16117
|
// align / offsetx / offsety (flatChartAlignOffset), the chart's
|
|
@@ -15489,9 +16120,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15489
16120
|
if (box) {
|
|
15490
16121
|
const r = box.rect;
|
|
15491
16122
|
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 },
|
|
15492
|
-
boxFill, boxLine, borderWidth * 0.2 * box.
|
|
15493
|
-
|
|
15494
|
-
|
|
16123
|
+
boxFill, boxLine, borderWidth * 0.2 * box.w / 30, null, true);
|
|
16124
|
+
// the title in px, rendered by the shared title layer
|
|
16125
|
+
if (box.title) titles.push({ d: ch, title: { text: box.title.text, x: box.title.x * unit, anchor: box.title.anchor,
|
|
16126
|
+
top: box.title.top * unit, f: box.title.f * unit } });
|
|
15495
16127
|
}
|
|
15496
16128
|
for (const b of L.bars) {
|
|
15497
16129
|
const rgb = this.classRgb(b.cls);
|
|
@@ -15559,18 +16191,10 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15559
16191
|
backgroundPadding: [0.3, 0, 0.4, 0.1].map(k => k * boxed.reduce((a, d) => a + d.font, 0) / boxed.length)
|
|
15560
16192
|
})));
|
|
15561
16193
|
}
|
|
15562
|
-
if (titles.length) {
|
|
15563
|
-
|
|
15564
|
-
|
|
15565
|
-
|
|
15566
|
-
sizeUnits: 'pixels', fontFamily: 'arial', characterSet: 'auto', lineHeight: 1,
|
|
15567
|
-
getColor: [...hexOrNamedToRgb(st.textcolor || '#888888').slice(0, 3), 255],
|
|
15568
|
-
fontSettings: { sdf: true }, outlineWidth: 1 / 7, outlineColor: [255, 255, 255, 128],
|
|
15569
|
-
getTextAnchor: d => d.anchor, getAlignmentBaseline: 'top',
|
|
15570
|
-
getPixelOffset: d => d.off,
|
|
15571
|
-
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
15572
|
-
}));
|
|
15573
|
-
}
|
|
16194
|
+
if (titles.length) layers.push(...this._boxTitleLayers(titles, {
|
|
16195
|
+
box: false, unit, liveZoom, positionOf: b => b.d.c, offsetOf: b => b.d.off,
|
|
16196
|
+
ids: { titles: `ix-bar-titles-${this.name}` }
|
|
16197
|
+
}).titles);
|
|
15574
16198
|
return layers;
|
|
15575
16199
|
}
|
|
15576
16200
|
|
|
@@ -15757,22 +16381,28 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15757
16381
|
return [(lng + 180) / 360 * world, (0.5 - Math.log((1 + s) / (1 - s)) / (4 * Math.PI)) * world];
|
|
15758
16382
|
};
|
|
15759
16383
|
const mercToLngLat = ([x, y]) => [x / world * 360 - 180, (2 * Math.atan(Math.exp((0.5 - y / world) * 2 * Math.PI)) - Math.PI / 2) * 180 / Math.PI];
|
|
15760
|
-
// the curves are computed in the plane
|
|
15761
|
-
//
|
|
15762
|
-
//
|
|
15763
|
-
//
|
|
15764
|
-
//
|
|
16384
|
+
// the curves are computed in the plane the map is DISPLAYED in: Web
|
|
16385
|
+
// Mercator under MapLibre's mercator projection; on the globe (flat's
|
|
16386
|
+
// orthographic map) the orthographic plane of the view center, for
|
|
16387
|
+
// the flows whose two ends face the viewer (the others keep
|
|
16388
|
+
// Mercator's). flat's own Equal Earth / Winkel plane (e1cb264) is NOT
|
|
16389
|
+
// used: this engine displays those maps in Web Mercator too (a flat
|
|
16390
|
+
// SVG map in another projection is reported "not available — mercator
|
|
16391
|
+
// used"), and a curve bowed in the Equal Earth plane swings far
|
|
16392
|
+
// toward the pole once Mercator renders it — measured on
|
|
16393
|
+
// mechanical_goods_flows.html: Japan→USA at 82°N, its bow almost half
|
|
16394
|
+
// the chord, where the Mercator curve stays at 63°N — the "deviated
|
|
16395
|
+
// near the north pole" vectors. Flat bows its curves in the plane of
|
|
16396
|
+
// ITS map; here the plane of the display.
|
|
15765
16397
|
const planeFns = (proj, k) => ({
|
|
15766
16398
|
toPx: ([lng, lat]) => { const q = proj.project(lat, lng); return [q[0] * k, -q[1] * k]; },
|
|
15767
16399
|
toLngLat: ([x, y]) => { const q = proj.invert(x / k, -y / k); return [q[1], q[0]]; }
|
|
15768
16400
|
});
|
|
15769
16401
|
const mercFns = { toPx: mercToPx, toLngLat: mercToLngLat };
|
|
15770
|
-
const fixedPlane = flatPlaneProjectionByOptions.get(this.mapOptions);
|
|
15771
|
-
const fixedFns = fixedPlane && fixedPlane.invert ? planeFns(fixedPlane, (world / 360) / fixedPlane.project(0, 1)[0]) : null;
|
|
15772
16402
|
const planeCenter = flatViewCenterByOptions.get(this.mapOptions) || globeCenter;
|
|
15773
|
-
const ortho =
|
|
16403
|
+
const ortho = globeCenter ? orthographicPlane(planeCenter.lat, planeCenter.lng) : null;
|
|
15774
16404
|
const orthoFns = ortho ? planeFns(ortho, world / (2 * Math.PI)) : null;
|
|
15775
|
-
const fnsOf = it => (
|
|
16405
|
+
const fnsOf = it => (orthoFns && ortho.inFront(it.p1) && ortho.inFront(it.p2) ? orthoFns : mercFns);
|
|
15776
16406
|
const inBox = c => c && c[0] >= bbox[0] && c[0] <= bbox[2] && c[1] >= bbox[1] && c[1] <= bbox[3];
|
|
15777
16407
|
|
|
15778
16408
|
const sizePow = resolveSizePow(st, this.flags);
|
|
@@ -15871,7 +16501,6 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15871
16501
|
// — every size equal — SVG ignores: the opacity stays)
|
|
15872
16502
|
if (dop || dopMin) { const d = dopacity(it); if (isFinite(d)) { lineAlpha = d; headAlpha = d; } }
|
|
15873
16503
|
if (gradient) stops = fadeGradientStops(pts, lay.vx, lay.vy, fade);
|
|
15874
|
-
if (dash) dashArr = [50 * oz, 5 * oz + sw];
|
|
15875
16504
|
pts = pts.map(p => [p[0] + a[0], p[1] + a[1]]);
|
|
15876
16505
|
end = [end[0] + a[0], end[1] + a[1]];
|
|
15877
16506
|
} else {
|
|
@@ -15885,7 +16514,25 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15885
16514
|
headS = 4; refX = 4; refY = 2;
|
|
15886
16515
|
lineAlpha = fo || 0.3;
|
|
15887
16516
|
headAlpha = Math.min(1, (fo || 0.3) * 2);
|
|
15888
|
-
|
|
16517
|
+
}
|
|
16518
|
+
if (dash) {
|
|
16519
|
+
// flat's dash pattern: one long dash (50 px at the object scale)
|
|
16520
|
+
// and a small gap, its phase walking toward the target — a flow
|
|
16521
|
+
// of any length gets its own count of dashes that way: long ones
|
|
16522
|
+
// many, short ones none at all (a solid line). style.dashcount
|
|
16523
|
+
// instead divides the flow into that many dash+gap periods:
|
|
16524
|
+
// every flow, long or short, shows the same pattern and moves at
|
|
16525
|
+
// the same relative speed (one full dash cycle every 2s), the
|
|
16526
|
+
// arrows as on the region-migration page
|
|
16527
|
+
const nDashes = Math.round(styleNum(st.dashcount) || 0);
|
|
16528
|
+
if (nDashes >= 1) {
|
|
16529
|
+
let flowPx = 0;
|
|
16530
|
+
for (let i = 1; i < pts.length; i++) flowPx += Math.hypot(pts[i][0] - pts[i - 1][0], pts[i][1] - pts[i - 1][1]);
|
|
16531
|
+
const period = flowPx / nDashes;
|
|
16532
|
+
dashArr = [period * 0.85, period * 0.15];
|
|
16533
|
+
} else {
|
|
16534
|
+
dashArr = bezier ? [50 * oz, 5 * oz + sw] : [50 * oz, 15 * oz];
|
|
16535
|
+
}
|
|
15889
16536
|
}
|
|
15890
16537
|
// a line under a pixel: one pixel at its width as opacity
|
|
15891
16538
|
const drawW = Math.max(1, sw);
|
|
@@ -15911,7 +16558,11 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15911
16558
|
let run = [], runColors = [];
|
|
15912
16559
|
const flush = () => {
|
|
15913
16560
|
if (run.length > 1) {
|
|
15914
|
-
|
|
16561
|
+
// the flow triple: [dash, gap] in px (the shader's path
|
|
16562
|
+
// distance unit) + flat's random per-arc dashoffset phase
|
|
16563
|
+
// (maptheme.js 18249), fixed per arc: the layers are rebuilt on
|
|
16564
|
+
// every pan/zoom, a new random phase each time made dashes jump
|
|
16565
|
+
const dash = dashArr ? [dashArr[0], dashArr[1], flowDashPhase(p1, p2)] : [0, 0, 0];
|
|
15915
16566
|
if (globeCenter && !dashArr) {
|
|
15916
16567
|
// on the globe deck.gl draws only the first three quarters of a path that has a
|
|
15917
16568
|
// color per vertex (also an equal one): one color per path, a gradient as one
|
|
@@ -15946,12 +16597,22 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
15946
16597
|
fill: [...rgb.slice(0, 3), Math.round(255 * Math.min(1, headAlpha) * groupAlpha)] });
|
|
15947
16598
|
}
|
|
15948
16599
|
}
|
|
15949
|
-
|
|
16600
|
+
// always FlowPathLayer: without dash offsets its flow hook no-ops
|
|
16601
|
+
// (solid lines), with them the pattern walks toward the target
|
|
16602
|
+
// (see makeFlowPathLayer). The dash triple is px; deck's path
|
|
16603
|
+
// shader measures along-path positions in HALF-STROKE-WIDTH
|
|
16604
|
+
// units (see makeFlowPathLayer), so the accessors convert:
|
|
16605
|
+
// getDashArray carries the pattern period (its gap stays 0 — the
|
|
16606
|
+
// extension renders solid, the moving gap is the flow hook),
|
|
16607
|
+
// getFlowPhase the solid length and flat's random per-arc phase
|
|
16608
|
+
const layers = [new FlowPathLayer({
|
|
15950
16609
|
id: `ix-vector-${this.name}`, data: paths, pickable: true,
|
|
15951
16610
|
getPath: d => d.path, getColor: d => d.colors, getWidth: d => d.width,
|
|
16611
|
+
getDashArray: d => [(d.dash[0] + d.dash[1]) * 2 / Math.max(d.width, 1), 0],
|
|
16612
|
+
getFlowPhase: d => [d.dash[0] * 2 / Math.max(d.width, 1), d.dash[2]],
|
|
15952
16613
|
widthUnits: 'pixels', capRounded: dash && bezier, jointRounded: false, billboard: false,
|
|
15953
|
-
|
|
15954
|
-
|
|
16614
|
+
extensions: [new PathStyleExtension({ dash: true, dashMode: 'path' })],
|
|
16615
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
15955
16616
|
})];
|
|
15956
16617
|
if (heads.length) {
|
|
15957
16618
|
// the head count in the id: deck kept a same-id polygon layer's
|
|
@@ -16051,7 +16712,8 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
16051
16712
|
const v = raw && raw[this.style.labelfield];
|
|
16052
16713
|
let text = v == null || v === '' ? '' : String(v);
|
|
16053
16714
|
if (!text) return null;
|
|
16054
|
-
|
|
16715
|
+
// flat reads it with __scanValue: "1.234,5", "12 500"
|
|
16716
|
+
const num = flatScanValue(text);
|
|
16055
16717
|
text = (!isNaN(num) ? flatGroupedValue(num, dec != null ? dec : 2, noBreaks) : text) + units;
|
|
16056
16718
|
const width = Math.max(titleTextWidth(text, f), (text.length + 2) * f * 0.5);
|
|
16057
16719
|
const x = width / 3, y = f / 3 + r + 5 * unitPx;
|
|
@@ -16223,6 +16885,9 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
16223
16885
|
// the box is sized by the value's own text, not the valuefield's
|
|
16224
16886
|
const boxStyle = Object.assign({}, this.style, { valuefield: undefined });
|
|
16225
16887
|
const boxes = [], texts = [];
|
|
16888
|
+
// the label's drawn extent for the chart-group BOX (below): x from -r
|
|
16889
|
+
// to 1.05r, y from -0.83f to 0.77f around the chart origin
|
|
16890
|
+
const labelExtent = new Map();
|
|
16226
16891
|
for (const d of combined) {
|
|
16227
16892
|
const r = radiusOf(d);
|
|
16228
16893
|
if (!(r > 0)) continue;
|
|
@@ -16237,6 +16902,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
16237
16902
|
const o = alignOf(d);
|
|
16238
16903
|
// the box's center relative to the chart's: x from −r to 1.05r, y from −0.83f to 0.77f
|
|
16239
16904
|
boxes.push({ ...d, icon, pixelOffset: [o[0] + 0.025 * r, o[1] - 0.03 * f] });
|
|
16905
|
+
labelExtent.set(d, [-r, -0.83 * f, 1.05 * r, 0.77 * f]);
|
|
16240
16906
|
if (showValues) {
|
|
16241
16907
|
const text = textOf(props);
|
|
16242
16908
|
const fontSize = Math.min(r * 0.8, r * 3.3 / Math.max(1, text.length)) * valueScale;
|
|
@@ -16244,7 +16910,13 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
16244
16910
|
if (fontSize > 0) texts.push({ ...d, text, fontSize, color, pixelOffset: o });
|
|
16245
16911
|
}
|
|
16246
16912
|
}
|
|
16247
|
-
|
|
16913
|
+
// BOX / TITLE: flat's chart-group box around the label chart
|
|
16914
|
+
const boxRows = this._boxRows(() => combined.filter(d => labelExtent.has(d)), { unit, liveZoom, extentOf: d => labelExtent.get(d) });
|
|
16915
|
+
const boxLayers = boxRows ? this._boxTitleLayers(boxRows, {
|
|
16916
|
+
unit, liveZoom, positionOf: b => b.d.geometry.coordinates, offsetOf: b => alignOf(b.d),
|
|
16917
|
+
ids: { circle: `ix-label-box-${this.name}`, icon: `ix-label-box-${this.name}`, polygons: `ix-label-box-polygons-${this.name}`, titles: `ix-label-titles-${this.name}` }
|
|
16918
|
+
}) : null;
|
|
16919
|
+
const layers = boxLayers ? [...boxLayers.box] : [];
|
|
16248
16920
|
if (boxes.length) {
|
|
16249
16921
|
layers.push(new IconLayer({
|
|
16250
16922
|
id: `ix-bubbles-${this.name}-g${this._iconGeneration}`,
|
|
@@ -16278,6 +16950,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
16278
16950
|
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
16279
16951
|
}));
|
|
16280
16952
|
}
|
|
16953
|
+
if (boxLayers) layers.push(...boxLayers.titles);
|
|
16281
16954
|
return layers;
|
|
16282
16955
|
}
|
|
16283
16956
|
|
|
@@ -16535,12 +17208,16 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
16535
17208
|
const _mapHandles = Object.create(null);
|
|
16536
17209
|
function makeMapHandle(szMap) {
|
|
16537
17210
|
const h = {};
|
|
16538
|
-
|
|
17211
|
+
// a name a Map() declared waits for that map; any other name (or none)
|
|
17212
|
+
// is the last built map, as before
|
|
17213
|
+
const declared = () => szMap && _namedMapReady[szMap];
|
|
17214
|
+
const api = () => (szMap && _mapApis[szMap]) || (declared() ? null : _lastMapApi);
|
|
16539
17215
|
for (const m of MAP_HANDLE_METHODS) {
|
|
16540
17216
|
h[m] = (...args) => {
|
|
16541
17217
|
const a = api();
|
|
16542
17218
|
if (a) { const r = a[m](...args); return r === a ? h : r; }
|
|
16543
|
-
_mapReady.then(ready => ready[m](...args))
|
|
17219
|
+
(declared() ? declared().promise : _mapReady).then(ready => ready[m](...args))
|
|
17220
|
+
.catch(err => console.error(`[ixmaps-gl] map(${szMap ? '"' + szMap + '"' : ''}).${m}:`, err));
|
|
16544
17221
|
return h;
|
|
16545
17222
|
};
|
|
16546
17223
|
}
|
|
@@ -17005,9 +17682,14 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
17005
17682
|
// their own comments) are still real, directly-callable globals a
|
|
17006
17683
|
// page's OWN custom UI can use too — only THIS generated markup
|
|
17007
17684
|
// happens to reach them via delegation instead of inline HTML.
|
|
17685
|
+
// the handlers below outlive this call: they read the theme id of
|
|
17686
|
+
// the LATEST call into this container (a theme switch re-renders
|
|
17687
|
+
// the same div for another theme)
|
|
17688
|
+
container._ixFacetsId = szId;
|
|
17008
17689
|
if (!container.dataset.ixFacetsWired) {
|
|
17009
17690
|
container.dataset.ixFacetsWired = '1';
|
|
17010
17691
|
container.addEventListener('click', e => {
|
|
17692
|
+
const szId = container._ixFacetsId;
|
|
17011
17693
|
const moreBtn = e.target.closest('[data-role="more"]');
|
|
17012
17694
|
if (moreBtn) { moreBtn.hidden = true; moreBtn.nextElementSibling.hidden = false; return; }
|
|
17013
17695
|
|
|
@@ -17033,7 +17715,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
17033
17715
|
container.addEventListener('keyup', e => {
|
|
17034
17716
|
if (e.key !== 'Enter') return;
|
|
17035
17717
|
const input = e.target.closest('[data-role="search"]');
|
|
17036
|
-
if (input) global.__setFilter(
|
|
17718
|
+
if (input) global.__setFilter(container._ixFacetsId, input.closest('[data-field]').dataset.field, input.value);
|
|
17037
17719
|
});
|
|
17038
17720
|
}
|
|
17039
17721
|
}
|
|
@@ -17123,11 +17805,7 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
17123
17805
|
// known real page's globe toggle actually uses (see
|
|
17124
17806
|
// example-world-bubble-projection-toggle.md) — so `ixmaps.szResourceBase`
|
|
17125
17807
|
// only needs to be SOME string, not a real, working resource base URL.
|
|
17126
|
-
|
|
17127
|
-
// page (tooltip_mustache.js) resolve their dependencies through it —
|
|
17128
|
-
// $.getScript(ixmaps.szResourceBase + "ui/js/tools/format.js") — which
|
|
17129
|
-
// with the empty string 404s against the page's own directory
|
|
17130
|
-
const ixmapsSzResourceBase = 'https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-flat@1/';
|
|
17808
|
+
const ixmapsSzResourceBase = '';
|
|
17131
17809
|
function getProjectString() {
|
|
17132
17810
|
if (!_lastMapApi || !_lastMapApi.map) return JSON.stringify({ map: {} });
|
|
17133
17811
|
const map = _lastMapApi.map;
|
|
@@ -17247,19 +17925,19 @@ float barSegDist(vec2 p, vec2 a, vec2 b) {
|
|
|
17247
17925
|
// test-only: lets test/unit/*.test.mjs call pure internals directly (the
|
|
17248
17926
|
// engine runs in a Node vm there); deliberately NOT on the ixmaps object
|
|
17249
17927
|
global.__ixmapsGlInternals = {
|
|
17250
|
-
IXMAPS_GL_VERSION, applyFlatMaxCharts, youtubeClickToPlay, symbolsFlagCompat, tooltipTable, legendRowLabels, scaleDenom, cssColorAlpha, aggregatedCategoricalClass, dominantDopacityAlpha, isAggregatedCategoricalChoropleth, computeBubblePackLayout, isolatedBubblePackLayout, sequenceLayout, ringsLayout, pieSliceLayout, pieValueLabelLayout, isBarChart, flatBarLayout,
|
|
17251
|
-
choroplethOutlineColor, normalizeTheme, projectThemeToDefinition, withoutProjectCode, groupRecordCount, resolveBasemapStyleUrl, resolveMapTypeColor, LayerBuilder, LayerRuntime, MapBuilder, typeStyleNumbers, styleNum,
|
|
17928
|
+
IXMAPS_GL_VERSION, applyFlatMaxCharts, youtubeClickToPlay, symbolsFlagCompat, tooltipTable, legendRowLabels, scaleDenom, cssColorAlpha, aggregatedCategoricalClass, dominantDopacityAlpha, isAggregatedCategoricalChoropleth, computeBubblePackLayout, isolatedBubblePackLayout, sequenceLayout, ringsLayout, pieSliceLayout, pieValueLabelLayout, isBarChart, flatBarLayout, flatValueRules, flatPieRecord, flatPieAccumulate, flatPieItemValues, flatVectorItems, orthographicPlane, vectorArcHeight, pathBackIndex, greatCircleMeters, cameraOptions, bezierVectorLayout, cubicBezierPoints, arrowMarkerTriangle, fadeGradientStops, flatToArray, hashUnit, flowDashPhase, multiQuadOffsets, pixelOffsetLngLat, chartTextTransmittance, insideSymbolShape,
|
|
17929
|
+
chartBoxLayout, choroplethOutlineColor, normalizeTheme, projectThemeToDefinition, withoutProjectCode, groupRecordCount, resolveBasemapStyleUrl, resolveMapTypeColor, LayerBuilder, LayerRuntime, MapBuilder, typeStyleNumbers, styleNum,
|
|
17252
17930
|
resolveScriptUrl, isTrustedScriptUrl, loadProcessingScript, loadBrokerData, applyBrokerThemePatch, makeBrokerTheme,
|
|
17253
17931
|
equalIntervalBreaks, quantileBreaks, naturalBreaks, evenStrideSample, jenksBreakValues, partsFromBreakValues, resolvePartsClass,
|
|
17254
17932
|
computeAlphaStats, computeMultiFieldClasses, computeDominantStats, resolveDominantClass, computeComposeColorStats,
|
|
17255
17933
|
resolveComposedColor, computeRangeClasses, colorSchemeClassCount, resolveDopacityAlpha,
|
|
17256
17934
|
flatColorSweep, applyClassesToColorScheme, resolveClassColors, flatOutlierStats, parseCssColor, flatLegendLook,
|
|
17257
17935
|
flatToMapLibreZoom, mapLibreToFlatZoom, flatViewToMapLibreZoom, mapLibreToFlatViewZoom, flatSvgProjectionOf, flatSvgProjectionByOptions, normalSizeScaleOf, scaleDenominatorAt, resolveZoomReference, resolveAggregationPx, valuesHiddenByScale,
|
|
17258
|
-
fetchLayerData, parseCsvText, dataTableRows, geometryRowsToFeatureCollection, filterFlatValues, applyField100, field100Binding, rangeClassLegendTotals, resolveAggregateValue, classValueSeparate, cellAggregatedValues, oneHot, groupCoLocated, aggregateOnGrid, GridAggregateIndex, flatRangeParts, explicitRangeBreaks, explicitRangeParts, resolvePartsClass, isNoneColor, aggregateField100, valueRadius, itemPlotGeometry, itemAnchor, objectZoomFactor, resolveZoomReference, defaultNormalSizeValue,
|
|
17259
|
-
applyWhereFilter, joinChartPositions, flatLookupKey, flatShapeCenter, chartHiddenByScale, glowHiddenByScale, isSymbolChart, flatChartAlignOffset, flatChartBranch, flatValueText, categoryValueRecord, flatDerivateRgb, flatBubbleLook, bubbleIconLook, flatChartTextRgb, flatGroupedValue, flatNoBreaks, featuresHiddenByScale, boxHiddenByScale, flatShadowOn, snapToAggregationGrid,
|
|
17936
|
+
fetchLayerData, QUERY_HANDOFF, parseCsvText, dataTableRows, geometryRowsToFeatureCollection, filterFlatValues, applyField100, field100Binding, rangeClassLegendTotals, resolveAggregateValue, classValueSeparate, cellAggregatedValues, oneHot, groupCoLocated, aggregateOnGrid, GridAggregateIndex, flatRangeParts, explicitRangeBreaks, explicitRangeParts, resolvePartsClass, isNoneColor, aggregateField100, valueRadius, itemPlotGeometry, itemAnchor, objectZoomFactor, resolveZoomReference, defaultNormalSizeValue,
|
|
17937
|
+
applyWhereFilter, joinChartPositions, joinChoroplethFeatures, flatLookupKey, flatShapeCenter, chartHiddenByScale, glowHiddenByScale, isSymbolChart, flatChartAlignOffset, flatChartBranch, flatValueText, categoryValueRecord, flatDerivateRgb, flatBubbleLook, bubbleIconLook, flatChartTextRgb, flatGroupedValue, flatNoBreaks, featuresHiddenByScale, boxHiddenByScale, flatShadowOn, snapToAggregationGrid,
|
|
17260
17938
|
drawSymbolPath, normalizeSymbolShape,
|
|
17261
17939
|
flatFormatValue, themeStyleArgs, flatChangedStyleValue, explicitValueDecimals, legendDecimals,
|
|
17262
|
-
dataCacheKey, dataCacheDisabled, cachedLayerData, featuresBounds, wantsZoomToExtent, legendIsOn,
|
|
17940
|
+
dataCacheKey, dataCacheDisabled, cachedLayerData, featuresBounds, wantsZoomToExtent, legendIsOn, applySphereBackdrop,
|
|
17263
17941
|
};
|
|
17264
17942
|
global.__setFilter = __setFilter;
|
|
17265
17943
|
global.__removeFacets = __removeFacets;
|