ixmaps-gl 0.2.19 → 0.2.20
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 +11 -5
- package/ixmaps-gl.js +895 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -61,6 +61,12 @@ page. It currently implements:
|
|
|
61
61
|
flat's part sizes, glow and value labels
|
|
62
62
|
- **CHART\|USER** — a page's own chart function (`style.userdraw`: `ixmaps.<name>(SVGDocument, opt)`
|
|
63
63
|
and its `_init`, as flat calls them), drawn in flat's chart units and shown as an icon at flat's size
|
|
64
|
+
- **CHART\|PIE** (and `PIE|DONUT`) — a pie per record (placed on its FEATURE shape by
|
|
65
|
+
`binding.lookup`), a slice per value field, as flat lays them out: `SIZE` by the size field (or
|
|
66
|
+
the parts' sum), `SORT`, `SYMMETRIC`/`BIGTOTOP`, `HALF*`, `AUTOCOMPLETE`, `CENTER` (the center
|
|
67
|
+
part as a disc, `GLOW`), `CENTERVALUE`, `VALUES` (flat's leader-line labels; one part: inside),
|
|
68
|
+
`THIN`/`XTHIN`/`THICK` donuts. `3D`, `VOLUME`, `STARBURST`, `RAYS`/`FLOWER`, `BOW`, `ZOOM` are
|
|
69
|
+
noted in the console and drawn as a flat pie; `AGGREGATE` and `CATEGORICAL` pies are not drawn yet
|
|
64
70
|
- **DOT** — the simplest base symbol (fixed-radius, unclustered points)
|
|
65
71
|
- **PLOT** — a small line/area chart per item over its value fields (flat's per-item PLOT
|
|
66
72
|
geometry: first point at the item, `scale`/`rangescale`, FIXSIZE markers), or one per grid
|
|
@@ -77,7 +83,7 @@ page. It currently implements:
|
|
|
77
83
|
- Globe (orthographic) projection alongside flat Mercator
|
|
78
84
|
|
|
79
85
|
Not yet implemented: plain (non-AGGREGATE) `CATEGORICAL` choropleths, and the `QUAD`/`BEZIER`/`VECTOR`/
|
|
80
|
-
`
|
|
86
|
+
`WAFFLE`/`BAR` base types, `AGGREGATE`/`CATEGORICAL` pies, and SYMBOL shape variants beyond
|
|
81
87
|
circle/square/diamond/triangle. See the top-of-file comment in
|
|
82
88
|
[`ixmaps-gl.js`](./ixmaps-gl.js) for the exact, currently-accurate scope note.
|
|
83
89
|
|
|
@@ -243,7 +249,7 @@ of your own project files gl can render.
|
|
|
243
249
|
Load the engine from jsDelivr, pinned to a release tag (or `@main` for the latest commit):
|
|
244
250
|
|
|
245
251
|
```html
|
|
246
|
-
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.
|
|
252
|
+
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.20/ixmaps-gl.js"></script>
|
|
247
253
|
```
|
|
248
254
|
|
|
249
255
|
or from unpkg, the npm package [`ixmaps-gl`](https://www.npmjs.com/package/ixmaps-gl)
|
|
@@ -251,13 +257,13 @@ or from unpkg, the npm package [`ixmaps-gl`](https://www.npmjs.com/package/ixmap
|
|
|
251
257
|
browser script, which defines the global `ixmaps`):
|
|
252
258
|
|
|
253
259
|
```html
|
|
254
|
-
<script src="https://unpkg.com/ixmaps-gl@0.2.
|
|
260
|
+
<script src="https://unpkg.com/ixmaps-gl@0.2.20/ixmaps-gl.js"></script>
|
|
255
261
|
```
|
|
256
262
|
|
|
257
263
|
or use a local copy (`<script src="ixmaps-gl.js"></script>`). A minimal page:
|
|
258
264
|
|
|
259
265
|
```html
|
|
260
|
-
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.
|
|
266
|
+
<script src="https://cdn.jsdelivr.net/gh/gjrichter/ixmaps-gl@v0.2.20/ixmaps-gl.js"></script>
|
|
261
267
|
<div id="map_div" style="position:absolute;inset:0;"></div>
|
|
262
268
|
|
|
263
269
|
<script>
|
|
@@ -288,7 +294,7 @@ features this engine doesn't implement:
|
|
|
288
294
|
|
|
289
295
|
```text
|
|
290
296
|
[ixmaps-gl validate] error unknown-style-key — layer "points": unknown style key "fillOpacity" — did you mean "fillopacity"?
|
|
291
|
-
[ixmaps-gl validate] warning gl-unsupported — layer "
|
|
297
|
+
[ixmaps-gl validate] warning gl-unsupported — layer "waffle": type flag "WAFFLE" is not implemented by ixmaps-gl
|
|
292
298
|
```
|
|
293
299
|
|
|
294
300
|
It is **off by default** — nothing is fetched and nothing changes. Turn it on with any of:
|
package/ixmaps-gl.js
CHANGED
|
@@ -49,12 +49,16 @@
|
|
|
49
49
|
// (headTailBreaks); LOG/POW2/POW3 are recognized type flags but not
|
|
50
50
|
// implemented, see KNOWN_INERT_FLAGS. CHART|USER draws a page's own chart
|
|
51
51
|
// function (style.userdraw, _buildUserChartLayers), CHART|LABEL flat's
|
|
52
|
-
// value label, boxed or TEXTONLY (_buildLabelChartLayers)
|
|
52
|
+
// value label, boxed or TEXTONLY (_buildLabelChartLayers), CHART|PIE (and
|
|
53
|
+
// PIE|DONUT) a pie per record, a slice per value field, flat's sizing,
|
|
54
|
+
// CENTER part and VALUES leader labels (_buildPieLayers; 3D/VOLUME/
|
|
55
|
+
// STARBURST/RAYS/BOW variants are noted, drawn flat; AGGREGATE and
|
|
56
|
+
// CATEGORICAL pies not yet). These
|
|
53
57
|
// base types are dispatched by buildDeckLayers in the real engine's own
|
|
54
58
|
// precedence order — DOT is checked first because the real engine's DOT
|
|
55
59
|
// bypasses the whole drawChart/modifier pipeline rather than being
|
|
56
60
|
// "BUBBLE with the size locked." Other real base types (QUAD, BEZIER,
|
|
57
|
-
// VECTOR,
|
|
61
|
+
// VECTOR, WAFFLE, BAR, and SYMBOL's own shape variants) are NOT
|
|
58
62
|
// implemented yet — adding one means a new _buildXLayers() method plus a
|
|
59
63
|
// dispatch line, not a rewrite, but each should be added deliberately
|
|
60
64
|
// (checking, per real source, which modifiers it actually shares with
|
|
@@ -82,7 +86,7 @@
|
|
|
82
86
|
// ---------------------------------------------------------------
|
|
83
87
|
// this engine's release (= package.json "version", checked by
|
|
84
88
|
// test/unit/version.test.mjs); ixmaps.glVersion, and logged once at start
|
|
85
|
-
const IXMAPS_GL_VERSION = '0.2.
|
|
89
|
+
const IXMAPS_GL_VERSION = '0.2.20';
|
|
86
90
|
|
|
87
91
|
const LIB_URLS = {
|
|
88
92
|
// Bumped 3.6.2 -> 5.x (2026-09-21, globe-projection compat fix): native
|
|
@@ -200,6 +204,111 @@
|
|
|
200
204
|
// overview page claims ES-modules-only, which the actual UMD bundle
|
|
201
205
|
// contradicts).
|
|
202
206
|
let IconLayer, ScatterplotLayer, GeoJsonLayer, MapLibreOverlay, TextLayer;
|
|
207
|
+
// PieSliceLayer (see makePieSliceLayer) is derived from ScatterplotLayer
|
|
208
|
+
// once deck.gl is there
|
|
209
|
+
let PieSliceLayer;
|
|
210
|
+
|
|
211
|
+
// CHART|PIE / DONUT slices (_buildPieLayers): one instance per slice — a
|
|
212
|
+
// ScatterplotLayer circle (billboarded, pixel radius, pixel offset, the
|
|
213
|
+
// same projection path as every other chart, globe included) whose
|
|
214
|
+
// fragment shader keeps only the slice's sector of the circle and the
|
|
215
|
+
// ring outside its inner radius (DONUT, CENTER), and draws flat's slice
|
|
216
|
+
// outline along the sector's edges.
|
|
217
|
+
// getSlice: [start angle, sweep, inner radius / radius] — radians,
|
|
218
|
+
// clockwise from 12 o'clock (flat's DonutChart angles); a
|
|
219
|
+
// sweep of 2π is a full ring (no radial edges)
|
|
220
|
+
// getSliceLineWidth: the outline width in pixels (0 = none), drawn in
|
|
221
|
+
// getLineColor
|
|
222
|
+
// Why not a canvas icon per pie (as the bubbles): every pie has its own
|
|
223
|
+
// slice angles, ~7 900 distinct icons on the comuni page, far beyond
|
|
224
|
+
// what deck.gl's never-evicting icon atlas holds (ICON_ATLAS_RESET_AFTER);
|
|
225
|
+
// and not polygons in map coordinates: ~31 000 slices re-tessellated at
|
|
226
|
+
// every zoom step. Here the GPU draws each slice from 5 numbers, crisp at
|
|
227
|
+
// any device pixel ratio. Neighbouring slices split the circle by angle
|
|
228
|
+
// without anti-aliasing between them (a hard partition: no background
|
|
229
|
+
// seam shows between two slices); the circle's rim keeps
|
|
230
|
+
// ScatterplotLayer's own anti-aliasing.
|
|
231
|
+
function makePieSliceLayer(Base) {
|
|
232
|
+
class Layer extends Base {
|
|
233
|
+
getShaders() {
|
|
234
|
+
const shaders = super.getShaders();
|
|
235
|
+
const inject = Object.assign({}, shaders.inject || {});
|
|
236
|
+
inject['vs:#decl'] = (inject['vs:#decl'] || '') + `
|
|
237
|
+
in vec3 instancePieSlices;
|
|
238
|
+
in float instancePieLineWidths;
|
|
239
|
+
out vec3 vPieSlice;
|
|
240
|
+
out float vPieRadius;
|
|
241
|
+
out float vPieLineWidth;
|
|
242
|
+
out vec4 vPieLineColor;
|
|
243
|
+
`;
|
|
244
|
+
inject['vs:#main-end'] = (inject['vs:#main-end'] || '') + `
|
|
245
|
+
vPieSlice = instancePieSlices;
|
|
246
|
+
vPieRadius = outerRadiusPixels;
|
|
247
|
+
vPieLineWidth = instancePieLineWidths;
|
|
248
|
+
vPieLineColor = vec4(instanceLineColors.rgb, instanceLineColors.a * layer.opacity);
|
|
249
|
+
`;
|
|
250
|
+
inject['fs:#decl'] = (inject['fs:#decl'] || '') + `
|
|
251
|
+
in vec3 vPieSlice;
|
|
252
|
+
in float vPieRadius;
|
|
253
|
+
in float vPieLineWidth;
|
|
254
|
+
in vec4 vPieLineColor;
|
|
255
|
+
`;
|
|
256
|
+
// before the picking module's own filter (it replaces the color
|
|
257
|
+
// when picking): the discards shape the pick area too
|
|
258
|
+
inject['fs:DECKGL_FILTER_COLOR'] = (inject['fs:DECKGL_FILTER_COLOR'] || '') + `
|
|
259
|
+
{
|
|
260
|
+
const float PIE_TAU = 6.283185307179586;
|
|
261
|
+
vec2 pieP = geometry.uv * vPieRadius;
|
|
262
|
+
float pieR = length(pieP);
|
|
263
|
+
float pieInner = vPieSlice.z * vPieRadius;
|
|
264
|
+
if (pieInner > 0.0 && pieR < pieInner - 1.0) discard;
|
|
265
|
+
bool pieFull = vPieSlice.y >= PIE_TAU - 1e-5;
|
|
266
|
+
if (!pieFull) {
|
|
267
|
+
float pieA = atan(pieP.x, pieP.y);
|
|
268
|
+
float pieRel = mod(pieA - vPieSlice.x + 2.0 * PIE_TAU, PIE_TAU);
|
|
269
|
+
if (pieRel >= vPieSlice.y) discard;
|
|
270
|
+
}
|
|
271
|
+
float pieD = vPieRadius - pieR;
|
|
272
|
+
if (pieInner > 0.0) pieD = min(pieD, pieR - pieInner);
|
|
273
|
+
if (!pieFull) {
|
|
274
|
+
vec2 pieE0 = vec2(sin(vPieSlice.x), cos(vPieSlice.x));
|
|
275
|
+
vec2 pieE1 = vec2(sin(vPieSlice.x + vPieSlice.y), cos(vPieSlice.x + vPieSlice.y));
|
|
276
|
+
float pieD0 = dot(pieP, pieE0) > 0.0 ? abs(pieP.x * pieE0.y - pieP.y * pieE0.x) : pieR;
|
|
277
|
+
float pieD1 = dot(pieP, pieE1) > 0.0 ? abs(pieP.x * pieE1.y - pieP.y * pieE1.x) : pieR;
|
|
278
|
+
pieD = min(pieD, min(pieD0, pieD1));
|
|
279
|
+
}
|
|
280
|
+
// in device pixels (fwidth: CSS px per device pixel), so a hairline
|
|
281
|
+
// stays one at any device pixel ratio. Half the outline lies inside
|
|
282
|
+
// each slice (the other half in its neighbour); below a pixel it fades
|
|
283
|
+
// in by its width
|
|
284
|
+
float pieAA = max(fwidth(pieR), 1e-4);
|
|
285
|
+
float pieH = vPieLineWidth * 0.5 / pieAA;
|
|
286
|
+
float pieDD = pieD / pieAA;
|
|
287
|
+
float pieLine = pieH > 0.0 ? clamp(min(pieH + 0.5 - pieDD, 2.0 * pieH), 0.0, 1.0) : 0.0;
|
|
288
|
+
float pieRim = clamp((vPieRadius - pieR) / pieAA + 0.5, 0.0, 1.0);
|
|
289
|
+
color = vec4(mix(color.rgb, vPieLineColor.rgb, pieLine), mix(color.a, vPieLineColor.a * pieRim, pieLine));
|
|
290
|
+
if (pieInner > 0.0) color.a *= clamp((pieR - pieInner) / pieAA + 0.5, 0.0, 1.0);
|
|
291
|
+
}
|
|
292
|
+
`;
|
|
293
|
+
shaders.inject = inject;
|
|
294
|
+
return shaders;
|
|
295
|
+
}
|
|
296
|
+
initializeState() {
|
|
297
|
+
super.initializeState();
|
|
298
|
+
this.getAttributeManager().addInstanced({
|
|
299
|
+
instancePieSlices: { size: 3, accessor: 'getSlice', defaultValue: [0, 2 * Math.PI, 0] },
|
|
300
|
+
instancePieLineWidths: { size: 1, accessor: 'getSliceLineWidth', defaultValue: 0 }
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
Layer.layerName = 'PieSliceLayer';
|
|
305
|
+
// deck.gl merges a subclass's defaultProps over its base class's
|
|
306
|
+
Layer.defaultProps = {
|
|
307
|
+
getSlice: { type: 'accessor', value: [0, 2 * Math.PI, 0] },
|
|
308
|
+
getSliceLineWidth: { type: 'accessor', value: 0 }
|
|
309
|
+
};
|
|
310
|
+
return Layer;
|
|
311
|
+
}
|
|
203
312
|
|
|
204
313
|
// Cached so multiple ixmaps.Map() calls on one page (or a page that
|
|
205
314
|
// still has its own static <script> tags for these libraries) only
|
|
@@ -277,6 +386,7 @@
|
|
|
277
386
|
[...document.styleSheets].some(s => s.href === LIB_URLS.maplibreCss) ? Promise.resolve() : loadStylesheet(LIB_URLS.maplibreCss)
|
|
278
387
|
]).then(() => {
|
|
279
388
|
({ IconLayer, ScatterplotLayer, GeoJsonLayer, MapLibreOverlay, TextLayer } = global.deck);
|
|
389
|
+
PieSliceLayer = makePieSliceLayer(ScatterplotLayer);
|
|
280
390
|
// which engine and libraries this page really runs (a page may
|
|
281
391
|
// bring its own MapLibre/deck.gl — the loaded ones are named)
|
|
282
392
|
const ml = global.maplibregl && typeof global.maplibregl.getVersion === 'function' ? global.maplibregl.getVersion() : '?';
|
|
@@ -1952,6 +2062,8 @@
|
|
|
1952
2062
|
// outliers" inverse mode) stays unimplemented — no config here uses it.
|
|
1953
2063
|
const KNOWN_INERT_FLAGS = ['SEQUENCE', 'STAR', 'SORT', 'DOWN', 'RECT', 'CLIPTOGEOBOUNDS', 'OUTLIER',
|
|
1954
2064
|
'POW2', 'POW3', 'NOSORT'];
|
|
2065
|
+
// flat's PIE / DONUT variants with no rendering here (see _buildPieLayers)
|
|
2066
|
+
const PIE_INERT_FLAGS = [/3D/, /VOLUME/, /HEIGHT/, /STARBURST/, /FLOWER/, /RAYS/, /BOW/, /ZOOM/, /DIRECTION/, /DIRECTED/, /POLAR/];
|
|
1955
2067
|
const _warnedFlags = new Set();
|
|
1956
2068
|
|
|
1957
2069
|
// GL-PORT COMPAT: real ixmaps-flat's global ixmaps.getThemeObj(szId) /
|
|
@@ -2542,6 +2654,17 @@
|
|
|
2542
2654
|
console.info(`[ixmaps-gl] type flag "${flag}" recognized, no distinct rendering behavior implemented yet`);
|
|
2543
2655
|
}
|
|
2544
2656
|
});
|
|
2657
|
+
// PIE variants _buildPieLayers doesn't draw (the pie is drawn flat and
|
|
2658
|
+
// plain): flat tests them as substrings, so do these
|
|
2659
|
+
if (flags.has('PIE') && flags.has('CHART')) {
|
|
2660
|
+
flags.forEach(flag => {
|
|
2661
|
+
const inert = PIE_INERT_FLAGS.find(re => re.test(flag));
|
|
2662
|
+
if (inert && !(/HEIGHT/.test(flag) && !flags.has('3D')) && !_warnedFlags.has('PIE:' + flag)) {
|
|
2663
|
+
_warnedFlags.add('PIE:' + flag);
|
|
2664
|
+
console.info(`[ixmaps-gl] type flag "${flag}" on a PIE chart recognized, not drawn yet — the pie is drawn flat and plain`);
|
|
2665
|
+
}
|
|
2666
|
+
});
|
|
2667
|
+
}
|
|
2545
2668
|
|
|
2546
2669
|
const rawBinding = Object.assign({}, def.binding);
|
|
2547
2670
|
const targets = resolveBindingTargets(style, rawBinding, def);
|
|
@@ -3554,7 +3677,7 @@
|
|
|
3554
3677
|
// flat's legend prints them (legend.js 610, 810: __formatValue
|
|
3555
3678
|
// with valuedecimals, else 2, "BLANK" — whole numbers without
|
|
3556
3679
|
// decimals), a colorscheme "none" part as an outline in linecolor
|
|
3557
|
-
const multiFieldLegend =
|
|
3680
|
+
const multiFieldLegend = rt._isMultiFieldChart();
|
|
3558
3681
|
const legendTotalText = v => (multiFieldLegend
|
|
3559
3682
|
? flatFormatValue(v, legendDecimals(rt.style), 'BLANK')
|
|
3560
3683
|
: rt._formatTooltipValue(v));
|
|
@@ -3605,7 +3728,7 @@
|
|
|
3605
3728
|
// a per-item chart over several value fields: a row per field,
|
|
3606
3729
|
// the field's sum over the visible items (flat's NYC status
|
|
3607
3730
|
// legend: bikes / empty docks / e-bikes, biggest sum first)
|
|
3608
|
-
if (rt.
|
|
3731
|
+
if (rt._isMultiFieldChart()) {
|
|
3609
3732
|
(rt._activeFeatures || rt.features).forEach(f => {
|
|
3610
3733
|
const c = f.geometry && f.geometry.coordinates;
|
|
3611
3734
|
if (!c || typeof c[0] !== 'number') return;
|
|
@@ -4380,10 +4503,12 @@
|
|
|
4380
4503
|
// hover/click tooltip lookup keeps working across a rotation.
|
|
4381
4504
|
// Non-atlas layers (ix-dot-/ix-features-/ix-choropleth-) never
|
|
4382
4505
|
// carry the suffix; stripping a pattern that isn't there is a
|
|
4383
|
-
// no-op.
|
|
4384
|
-
|
|
4506
|
+
// no-op. A theme's single circles (ix-bubbles-<name>-circles-gN,
|
|
4507
|
+
// a ScatterplotLayer) route like its bubbles.
|
|
4508
|
+
const base = layerId.replace(/-g\d+$/, '').replace(/-circles$/, '');
|
|
4385
4509
|
return runtimes.find(r => {
|
|
4386
4510
|
if (base === `ix-bubbles-${r.name}`) return isSymbolChart(r.flags);
|
|
4511
|
+
if (base === `ix-pie-${r.name}`) return r._isPieChart();
|
|
4387
4512
|
if (base === `ix-dot-${r.name}`) return r.flags.has('DOT');
|
|
4388
4513
|
if (base === `ix-choropleth-${r.name}`) return r.flags.has('CHOROPLETH');
|
|
4389
4514
|
if (base === `ix-features-${r.name}`) return r.flags.has('FEATURE') || r.flags.has('FEATURES');
|
|
@@ -7045,6 +7170,94 @@
|
|
|
7045
7170
|
return { offset, index, extent };
|
|
7046
7171
|
}
|
|
7047
7172
|
|
|
7173
|
+
// Chart texts covered by charts drawn after them. Flat draws each chart
|
|
7174
|
+
// as one SVG group (symbol, then its value text) in the sorted draw
|
|
7175
|
+
// order, so a later chart's symbol paints over the texts of every chart
|
|
7176
|
+
// beneath it; here the texts are separate TextLayers above every icon.
|
|
7177
|
+
// This composites that occlusion on the CPU: for each text, the
|
|
7178
|
+
// transmittance Π(1 − α) of every LATER chart's symbol parts at a few
|
|
7179
|
+
// sample points of the text box, averaged — the fraction of the text
|
|
7180
|
+
// still showing through, its alpha. Partly covered texts fade instead
|
|
7181
|
+
// of being clipped (no per-pixel mask in deck.gl).
|
|
7182
|
+
// charts: in draw order, { x, y, parts: [{ dx, dy, r, shape, alpha }] }
|
|
7183
|
+
// in screen pixels, a part's alpha its opacity
|
|
7184
|
+
// texts: { order (index into charts), x, y, w, h }
|
|
7185
|
+
// Returns each text's transmittance, 0..1.
|
|
7186
|
+
const OCCLUSION_CELL_PX = 32;
|
|
7187
|
+
// below this a text is as good as hidden: left out of its TextLayer
|
|
7188
|
+
const CHART_TEXT_MIN_TRANSMITTANCE = 0.08;
|
|
7189
|
+
function chartTextTransmittance(charts, texts) {
|
|
7190
|
+
// the texts' sample points, then a grid over just their extent holding,
|
|
7191
|
+
// per cell, the charts (in draw order) whose reach overlaps it
|
|
7192
|
+
const SAMPLES = [[0, 0], [-0.35, -0.3], [0.35, -0.3], [-0.35, 0.3], [0.35, 0.3]];
|
|
7193
|
+
const nS = SAMPLES.length, px = new Float64Array(texts.length * nS), py = new Float64Array(texts.length * nS);
|
|
7194
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
7195
|
+
texts.forEach((t, i) => SAMPLES.forEach(([sx, sy], j) => {
|
|
7196
|
+
const x = px[i * nS + j] = t.x + sx * t.w, y = py[i * nS + j] = t.y + sy * t.h;
|
|
7197
|
+
if (x < minX) minX = x; if (x > maxX) maxX = x;
|
|
7198
|
+
if (y < minY) minY = y; if (y > maxY) maxY = y;
|
|
7199
|
+
}));
|
|
7200
|
+
const out = new Array(texts.length).fill(1);
|
|
7201
|
+
if (!texts.length || !charts.length) return out;
|
|
7202
|
+
const C = OCCLUSION_CELL_PX;
|
|
7203
|
+
const W = Math.floor((maxX - minX) / C) + 1, H = Math.floor((maxY - minY) / C) + 1;
|
|
7204
|
+
const reach = charts.map(c => {
|
|
7205
|
+
let ext = 0;
|
|
7206
|
+
for (const p of c.parts) ext = Math.max(ext, Math.sqrt(p.dx * p.dx + p.dy * p.dy) + p.r * Math.SQRT2);
|
|
7207
|
+
return ext;
|
|
7208
|
+
});
|
|
7209
|
+
const range = (c, ext) => [Math.max(0, Math.floor((c.x - ext - minX) / C)), Math.min(W - 1, Math.floor((c.x + ext - minX) / C)),
|
|
7210
|
+
Math.max(0, Math.floor((c.y - ext - minY) / C)), Math.min(H - 1, Math.floor((c.y + ext - minY) / C))];
|
|
7211
|
+
// two passes (count, fill) into one flat array: cells keep draw order
|
|
7212
|
+
const start = new Int32Array(W * H + 1);
|
|
7213
|
+
charts.forEach((c, k) => {
|
|
7214
|
+
const [x0, x1, y0, y1] = range(c, reach[k]);
|
|
7215
|
+
for (let gy = y0; gy <= y1; gy++) for (let gx = x0; gx <= x1; gx++) start[gy * W + gx + 1]++;
|
|
7216
|
+
});
|
|
7217
|
+
for (let i = 0; i < W * H; i++) start[i + 1] += start[i];
|
|
7218
|
+
const fill = start.slice(0, W * H), cells = new Int32Array(start[W * H]);
|
|
7219
|
+
charts.forEach((c, k) => {
|
|
7220
|
+
const [x0, x1, y0, y1] = range(c, reach[k]);
|
|
7221
|
+
for (let gy = y0; gy <= y1; gy++) for (let gx = x0; gx <= x1; gx++) cells[fill[gy * W + gx]++] = k;
|
|
7222
|
+
});
|
|
7223
|
+
// what of (x, y) still shows through chart c's parts
|
|
7224
|
+
const keepAt = (c, x, y) => {
|
|
7225
|
+
let keep = 1;
|
|
7226
|
+
for (const p of c.parts) {
|
|
7227
|
+
if (insideSymbolShape(p.shape, x - c.x - p.dx, y - c.y - p.dy, p.r)) keep *= 1 - p.alpha;
|
|
7228
|
+
}
|
|
7229
|
+
return keep;
|
|
7230
|
+
};
|
|
7231
|
+
texts.forEach((t, i) => {
|
|
7232
|
+
let sum = 0;
|
|
7233
|
+
for (let j = 0; j < nS; j++) {
|
|
7234
|
+
const x = px[i * nS + j], y = py[i * nS + j];
|
|
7235
|
+
const cell = Math.floor((y - minY) / C) * W + Math.floor((x - minX) / C);
|
|
7236
|
+
let keep = 1;
|
|
7237
|
+
// only the charts drawn after the text's own cover it
|
|
7238
|
+
for (let n = start[cell + 1] - 1; n >= start[cell] && cells[n] > t.order && keep > 0.005; n--) {
|
|
7239
|
+
const k = cells[n], c = charts[k], dx = x - c.x, dy = y - c.y;
|
|
7240
|
+
if (dx * dx + dy * dy < reach[k] * reach[k]) keep *= keepAt(c, x, y);
|
|
7241
|
+
}
|
|
7242
|
+
sum += keep;
|
|
7243
|
+
}
|
|
7244
|
+
out[i] = sum / nS;
|
|
7245
|
+
});
|
|
7246
|
+
return out;
|
|
7247
|
+
}
|
|
7248
|
+
// inside drawSymbolPath's shape of radius r, (dx, dy) from its center;
|
|
7249
|
+
// the shapes without their own test as the circle
|
|
7250
|
+
function insideSymbolShape(shape, dx, dy, r) {
|
|
7251
|
+
switch (shape) {
|
|
7252
|
+
case 'squarefull': return Math.abs(dx) <= r && Math.abs(dy) <= r;
|
|
7253
|
+
case 'square': return Math.abs(dx) <= r * Math.SQRT1_2 && Math.abs(dy) <= r * Math.SQRT1_2;
|
|
7254
|
+
case 'roundrect': return Math.abs(dx) <= r * 0.8 && Math.abs(dy) <= r * 0.8;
|
|
7255
|
+
case 'diamond': case 'carot': return Math.abs(dx) + Math.abs(dy) <= r;
|
|
7256
|
+
case 'empty': return false;
|
|
7257
|
+
default: return dx * dx + dy * dy <= r * r;
|
|
7258
|
+
}
|
|
7259
|
+
}
|
|
7260
|
+
|
|
7048
7261
|
// A pixel offset [dx, dy] (y down) from lngLat at MapLibre zoom `zoom`,
|
|
7049
7262
|
// as lng/lat (Web Mercator, 512 px world tiles) — for shapes too large
|
|
7050
7263
|
// for an icon, drawn in map coordinates (rebuilt with every refresh).
|
|
@@ -7056,6 +7269,193 @@
|
|
|
7056
7269
|
return [lngLat[0] + offset[0] * 360 / world, lat];
|
|
7057
7270
|
}
|
|
7058
7271
|
|
|
7272
|
+
// CHART|PIE / DONUT: one pie's slices as real ixmaps-flat lays them out —
|
|
7273
|
+
// maptheme.js drawChart's PIE branch (19939-20330) and piechart.js
|
|
7274
|
+
// DonutChart.realize (181-510). `values`: the item's part values (field
|
|
7275
|
+
// order); `t`: the type string, tested as flat does, by substring (so
|
|
7276
|
+
// CENTERVALUE also is CENTER, XTHIN also THIN). Returns null when flat
|
|
7277
|
+
// draws no pie (no sum, no positive part), else
|
|
7278
|
+
// { slices: [{ i, value, percent, start, sweep, complement }],
|
|
7279
|
+
// inner: inner radius / radius,
|
|
7280
|
+
// center: { i, value, size } | null }
|
|
7281
|
+
// (k on a slice: its index among all of flat's donut parts)
|
|
7282
|
+
// i is the part's field index (-1: AUTOCOMPLETE's no-data complement);
|
|
7283
|
+
// start/sweep in degrees, clockwise from 12 o'clock.
|
|
7284
|
+
// - parts: in field order; SORT biggest first; REVERSE takes flat's
|
|
7285
|
+
// index n − i literally — field n doesn't exist and field 0 is never
|
|
7286
|
+
// reached, so a REVERSE pie loses its first part, as in flat
|
|
7287
|
+
// - a part is a slice when > 0, or = 0 unless ZEROISNOTVALUE (a 0 slice
|
|
7288
|
+
// has no angle and is not drawn); negative parts are left out
|
|
7289
|
+
// - the slices share 100 % (AUTOCOMPLETE below 100: the rest is a
|
|
7290
|
+
// no-data part, the values are taken as percentages)
|
|
7291
|
+
// - CENTER (without DONUT): the center part (style.centerpart first /
|
|
7292
|
+
// last / min / max — default max — or a field number) is no slice but
|
|
7293
|
+
// a disc in the middle, radius √(size / 100) of the pie's, size =
|
|
7294
|
+
// value / sum · 200; the ring around it has that inner radius
|
|
7295
|
+
// - DONUT: inner radius 0.42 (THICK 0.25, THIN 0.66, XTHIN 0.95, 3D 0.5)
|
|
7296
|
+
// - an aggregated 1-record item of fewer than 2 parts has no hole
|
|
7297
|
+
// - angles: from 0° (SYMMETRIC: the first part, BIGTOTOP the biggest,
|
|
7298
|
+
// centered on 12 o'clock), HALF over 180° from 270° (HALFPLUS10 200°
|
|
7299
|
+
// from 260°, HALFMINUS10 160° from 280°, HALFPLUS20 220° from 250°,
|
|
7300
|
+
// HALFMINUS20 140° from 290°)
|
|
7301
|
+
function pieSliceLayout(values, t, opts = {}) {
|
|
7302
|
+
t = String(t || '');
|
|
7303
|
+
const has = re => re.test(t);
|
|
7304
|
+
const parts = values.map(v => (isNaN(v) ? 0 : v));
|
|
7305
|
+
const sum = parts.reduce((a, v) => a + v, 0);
|
|
7306
|
+
const autocomplete = has(/AUTOCOMPLETE/);
|
|
7307
|
+
if (sum === 0 && !autocomplete) return null;
|
|
7308
|
+
const n = parts.length;
|
|
7309
|
+
let sortedIndex = null;
|
|
7310
|
+
if (has(/\bSORT\b/)) sortedIndex = parts.map((value, index) => ({ index, value })).sort((a, b) => b.value - a.value);
|
|
7311
|
+
if (has(/\bREVERSE\b/)) sortedIndex = parts.map((value, i) => ({ index: n - i, value }));
|
|
7312
|
+
const at = p => (sortedIndex ? sortedIndex[p].index : p);
|
|
7313
|
+
const donut = has(/DONUT/), centerFlag = has(/CENTER/);
|
|
7314
|
+
let center = null;
|
|
7315
|
+
if (centerFlag) {
|
|
7316
|
+
let cSum = 0, cMax = -Number.MAX_VALUE, cMin = Number.MAX_VALUE, iMin, iMax;
|
|
7317
|
+
for (let p = 0; p < n; p++) {
|
|
7318
|
+
const i = at(p);
|
|
7319
|
+
cSum += parts[i] || 0;
|
|
7320
|
+
if (parts[i] < cMin) { cMin = parts[i]; iMin = i; }
|
|
7321
|
+
if (parts[i] > cMax) { cMax = parts[i]; iMax = i; }
|
|
7322
|
+
}
|
|
7323
|
+
const cp = opts.centerPart != null && opts.centerPart !== '' ? String(opts.centerPart) : 'max';
|
|
7324
|
+
let ci = 0;
|
|
7325
|
+
if (cp === 'first') ci = 0;
|
|
7326
|
+
else if (cp === 'last') ci = n - 1;
|
|
7327
|
+
else if (cp === 'min') ci = iMin;
|
|
7328
|
+
else if (cp === 'max') ci = iMax;
|
|
7329
|
+
else if (Number(cp)) ci = Number(cp);
|
|
7330
|
+
center = { i: ci, value: parts[ci], size: parts[ci] / cSum * 200 };
|
|
7331
|
+
}
|
|
7332
|
+
const slices = [];
|
|
7333
|
+
for (let p = 0; p < n; p++) {
|
|
7334
|
+
const i = at(p);
|
|
7335
|
+
if (centerFlag && !donut && i === center.i) continue;
|
|
7336
|
+
const v = parts[i];
|
|
7337
|
+
if (v > 0 || (v === 0 && !has(/ZEROISNOTVALUE/))) slices.push({ i, value: v, percent: v });
|
|
7338
|
+
}
|
|
7339
|
+
let inner = 0;
|
|
7340
|
+
if (donut) inner = has(/3D/) ? 0.5 : has(/XTHIN/) ? 0.95 : has(/THIN/) ? 0.66 : has(/THICK/) ? 0.25 : 0.42;
|
|
7341
|
+
else if (centerFlag) inner = Math.sqrt(center.size / 100);
|
|
7342
|
+
// GR 09.11.2019 "donut with count or size == 1 --> bubble": only for an
|
|
7343
|
+
// aggregated item of 1 record (opts.itemCount) — flat's per-record
|
|
7344
|
+
// items have no nCount, so their 1-part donuts keep the hole
|
|
7345
|
+
if (slices.length < 2 && opts.itemCount === 1) inner = 0;
|
|
7346
|
+
// DonutChart.realize
|
|
7347
|
+
let biggest = slices[0], biggestValue = 0, any = false;
|
|
7348
|
+
for (const sl of slices) {
|
|
7349
|
+
if (sl.percent > 0) {
|
|
7350
|
+
if (has(/BIGTOTOP/) && sl.percent > biggestValue) { biggest = sl; biggestValue = sl.percent; }
|
|
7351
|
+
any = true;
|
|
7352
|
+
}
|
|
7353
|
+
}
|
|
7354
|
+
// flat draws its "Pie error: no values !" circle here
|
|
7355
|
+
if (!any && !autocomplete) return null;
|
|
7356
|
+
const pctSum = slices.reduce((a, sl) => a + sl.percent, 0);
|
|
7357
|
+
if (pctSum !== 100) {
|
|
7358
|
+
if (autocomplete && pctSum < 100) slices.push({ i: -1, value: null, percent: Math.floor((100 - pctSum) * 100000) / 100000, complement: true });
|
|
7359
|
+
else slices.forEach(sl => { sl.percent = sl.percent / pctSum * 100; });
|
|
7360
|
+
}
|
|
7361
|
+
let end = 0;
|
|
7362
|
+
if (has(/SYMMETRIC/) && slices.length) {
|
|
7363
|
+
if (has(/BIGTOTOP/)) {
|
|
7364
|
+
end = 360;
|
|
7365
|
+
let x = 0;
|
|
7366
|
+
for (; x < slices.length && slices[x] !== biggest; x++) end -= slices[x].percent < 100 ? Math.floor(360 / 100 * slices[x].percent) : 0;
|
|
7367
|
+
end -= slices[x] && slices[x].percent < 100 ? Math.floor(360 / 100 / 2 * biggest.percent) : 0;
|
|
7368
|
+
} else if (slices[0].percent < 100) {
|
|
7369
|
+
end = 360 - Math.floor(360 / 100 / 2 * slices[0].percent);
|
|
7370
|
+
}
|
|
7371
|
+
}
|
|
7372
|
+
if (has(/HALF/)) end = 270;
|
|
7373
|
+
if (has(/HALFPLUS10/)) end = 260;
|
|
7374
|
+
if (has(/HALFMINUS10/)) end = 280;
|
|
7375
|
+
if (has(/HALFPLUS20/)) end = 250;
|
|
7376
|
+
if (has(/HALFMINUS20/)) end = 290;
|
|
7377
|
+
let maxAngle = has(/HALF/) ? 180 : 360;
|
|
7378
|
+
if (has(/HALFPLUS10/)) maxAngle = 200;
|
|
7379
|
+
if (has(/HALFMINUS10/)) maxAngle = 160;
|
|
7380
|
+
if (has(/HALFPLUS20/)) maxAngle = 220;
|
|
7381
|
+
if (has(/HALFMINUS20/)) maxAngle = 140;
|
|
7382
|
+
const out = [];
|
|
7383
|
+
slices.forEach((sl, k) => {
|
|
7384
|
+
const sweep = sl.percent / 100 * maxAngle;
|
|
7385
|
+
const start = end % 360;
|
|
7386
|
+
end = start + sweep;
|
|
7387
|
+
if (end > 360) end = end % 360;
|
|
7388
|
+
// _crc_drawDonut draws nothing for start == end (a 0 part, or a
|
|
7389
|
+
// full turn that doesn't start at 0°); k: flat's donut part index
|
|
7390
|
+
// (its value texts count them, drawn or not)
|
|
7391
|
+
if (start === end || start < 0 || end < 0) return;
|
|
7392
|
+
out.push(Object.assign(sl, { start, sweep, k }));
|
|
7393
|
+
});
|
|
7394
|
+
return { slices: out, inner, center: centerFlag && !donut ? center : null };
|
|
7395
|
+
}
|
|
7396
|
+
|
|
7397
|
+
// A pie's VALUES as flat draws them for more than one part
|
|
7398
|
+
// (maptheme.js drawDonutText 25072-25155, drawTextforOneQuadrant
|
|
7399
|
+
// 24993-25065, piechart.js getTextPosition 517-555): a label per slice
|
|
7400
|
+
// outside the pie, joined to the slice by a leader line. Per slice: the
|
|
7401
|
+
// point at 1.1 R in the middle of its angle; the labels of each quadrant
|
|
7402
|
+
// stacked by that point's height (line height 1.2 font, kept apart, the
|
|
7403
|
+
// last ones pushed toward the pie's top/bottom), all at x = R + 2u right
|
|
7404
|
+
// or left; the leader line from the slice's rim to its 1.1 R point,
|
|
7405
|
+
// horizontally to R − 3u when short of it, to the label's height at
|
|
7406
|
+
// R + 2u, then 2u straight on; the text 1.5u further. Only flat's first
|
|
7407
|
+
// min(50, maxParts) donut parts get one (maxParts: the theme's class
|
|
7408
|
+
// count). Pixels, y down, relative to the pie center:
|
|
7409
|
+
// [{ slice, segments: [[x0, y0, x1, y1], ...], x, y (text baseline), anchor: 'start' | 'end' }]
|
|
7410
|
+
function pieValueLabelLayout(slices, R, fontPx, unitPx, maxParts) {
|
|
7411
|
+
const limit = Math.min(50, maxParts);
|
|
7412
|
+
const quads = [[], [], [], []];
|
|
7413
|
+
let yMax = 0;
|
|
7414
|
+
for (const sl of slices) {
|
|
7415
|
+
if (sl.k >= limit) continue;
|
|
7416
|
+
let end = sl.start + sl.sweep;
|
|
7417
|
+
if (end > 360) end = end % 360;
|
|
7418
|
+
const mid = sl.start < end ? (sl.start + end) / 2 : Math.min(360, (sl.start + end + 360) / 2) % 360;
|
|
7419
|
+
const sin = Math.sin(Math.PI * mid / 180), cos = Math.cos(Math.PI * mid / 180);
|
|
7420
|
+
const p = { slice: sl, x: sin * R * 1.1, y: -cos * R * 1.1, lx: sin * R, ly: -cos * R };
|
|
7421
|
+
yMax = Math.max(yMax, Math.abs(p.y));
|
|
7422
|
+
// flat's quadrants 1 (upper right) 2 (lower right) 3 (lower left)
|
|
7423
|
+
// 4 (upper left), folded onto positive x/y
|
|
7424
|
+
if (p.x > 0) {
|
|
7425
|
+
if (p.y > 0) quads[1].push(p);
|
|
7426
|
+
else quads[0].push({ slice: sl, x: p.x, y: -p.y, lx: p.lx, ly: -p.ly });
|
|
7427
|
+
} else if (p.y > 0) quads[2].push({ slice: sl, x: -p.x, y: p.y, lx: -p.lx, ly: p.ly });
|
|
7428
|
+
else quads[3].push({ slice: sl, x: -p.x, y: -p.y, lx: -p.lx, ly: -p.ly });
|
|
7429
|
+
}
|
|
7430
|
+
const out = [];
|
|
7431
|
+
const dirs = [[1, -1, 'start'], [1, 1, 'start'], [-1, 1, 'end'], [-1, -1, 'end']];
|
|
7432
|
+
quads.forEach((q, qi) => {
|
|
7433
|
+
q.sort((a, b) => (a.y > b.y ? 1 : -1));
|
|
7434
|
+
const [xDir, yDir, anchor] = dirs[qi];
|
|
7435
|
+
const lh = fontPx * 1.2;
|
|
7436
|
+
let yMin = lh / 2;
|
|
7437
|
+
let ySpace = yMax - lh * (q.length - 2);
|
|
7438
|
+
for (const p of q) {
|
|
7439
|
+
const knee = p.y / p.ly;
|
|
7440
|
+
const yPos = Math.max(yMin, Math.min(p.y, ySpace));
|
|
7441
|
+
yMin = yPos + lh;
|
|
7442
|
+
ySpace += fontPx;
|
|
7443
|
+
const xPos = R + 2 * unitPx;
|
|
7444
|
+
const segments = [];
|
|
7445
|
+
let sx = p.lx * xDir, sy = p.ly * yDir;
|
|
7446
|
+
if (knee) { segments.push([sx, sy, p.x * xDir, p.y * yDir]); sx = p.x * xDir; sy = p.y * yDir; }
|
|
7447
|
+
if (p.x < R - 3 * unitPx) { const x = (R - 3 * unitPx) * xDir; segments.push([sx, sy, x, sy]); sx = x; }
|
|
7448
|
+
let ex = xPos * xDir;
|
|
7449
|
+
const ey = yPos * yDir;
|
|
7450
|
+
segments.push([sx, sy, ex, ey]);
|
|
7451
|
+
segments.push([ex, ey, ex + 2 * unitPx * xDir, ey]);
|
|
7452
|
+
ex += 3.5 * unitPx * xDir;
|
|
7453
|
+
out.push({ slice: p.slice, segments, x: ex, y: ey + fontPx * 0.25, anchor });
|
|
7454
|
+
}
|
|
7455
|
+
});
|
|
7456
|
+
return out;
|
|
7457
|
+
}
|
|
7458
|
+
|
|
7059
7459
|
// CHART|SYMBOL|SEQUENCE (flat maptheme.js 21184-21300, 21790-21925,
|
|
7060
7460
|
// 22296-22395): one symbol per category part of a chart, in category
|
|
7061
7461
|
// order or sorted (SORT: DOWN biggest first, UP smallest first), parts
|
|
@@ -8404,7 +8804,9 @@
|
|
|
8404
8804
|
this._gridRefLat = (minLat <= maxLat) ? (minLat + maxLat) / 2 : 0;
|
|
8405
8805
|
}
|
|
8406
8806
|
|
|
8407
|
-
if (this.
|
|
8807
|
+
if (this._isPieChart() && this.binding.value) {
|
|
8808
|
+
this._preparePie();
|
|
8809
|
+
} else if (this.flags.has('DOMINANT') && this.binding.value && !isAggregatedCategoricalChoropleth(this)) {
|
|
8408
8810
|
// .type("CHOROPLETH|DOMINANT") — a MULTI-field bound value
|
|
8409
8811
|
// (binding.value is a pipe-joined field list, e.g. one CSV column
|
|
8410
8812
|
// per age band for the same year/sex — same pipe convention
|
|
@@ -9660,6 +10062,9 @@
|
|
|
9660
10062
|
// USER charts (a page's own chart function, style.userdraw) go the
|
|
9661
10063
|
// chart pipeline too, drawn by _buildUserChartLayers (bubbles when the
|
|
9662
10064
|
// page has no such function)
|
|
10065
|
+
// PIE / DONUT (flat's PIE branch comes before BUBBLE and SYMBOL in
|
|
10066
|
+
// drawChart, maptheme.js 19939)
|
|
10067
|
+
if (this._isPieChart()) return this._buildPieLayers(zoom, bbox, liveZoom, globeCenter);
|
|
9663
10068
|
if (isSymbolChart(this.flags)) return this._buildChartLayers(zoom, bbox, liveZoom, globeCenter);
|
|
9664
10069
|
console.warn(`[ixmaps-gl] layer "${this.name}": type "${[...this.flags].join('|')}" has no implemented renderer`);
|
|
9665
10070
|
return [];
|
|
@@ -10022,6 +10427,51 @@
|
|
|
10022
10427
|
_itemPlotFields() {
|
|
10023
10428
|
return String(this.binding.value || '').split('|');
|
|
10024
10429
|
}
|
|
10430
|
+
// CHART|PIE (and PIE|DONUT): one pie per record, its slices the value
|
|
10431
|
+
// fields (_buildPieLayers). An AGGREGATE pie (flat sums the records of
|
|
10432
|
+
// a grid cell or category into one pie) and a CATEGORICAL one (a part
|
|
10433
|
+
// per category) have no renderer here yet.
|
|
10434
|
+
_isPieChart() {
|
|
10435
|
+
return this.flags.has('CHART') && this.flags.has('PIE') && !this.flags.has('AGGREGATE')
|
|
10436
|
+
&& !this.flags.has('CATEGORICAL') && !this.flags.has('DOT');
|
|
10437
|
+
}
|
|
10438
|
+
// a per-item chart whose parts are its value fields (legend rows, sums)
|
|
10439
|
+
_isMultiFieldChart() {
|
|
10440
|
+
return !!this._multiFields && (this._isMultiFieldSequence() || this._isPieChart());
|
|
10441
|
+
}
|
|
10442
|
+
// flat's theme statistics for a pie (maptheme.js 9119-9130): nMin/nMax
|
|
10443
|
+
// over every part value of every item, the classes (partsA, flat's
|
|
10444
|
+
// distributeValues over that range, as many as the colorscheme has
|
|
10445
|
+
// colors) — flat colors a 1-part pie (or any with CLASSES) by them
|
|
10446
|
+
// (maptheme.js 20330-20341); several parts are colored by field.
|
|
10447
|
+
_preparePie() {
|
|
10448
|
+
const fields = String(this.binding.value).split('|');
|
|
10449
|
+
const pooled = [];
|
|
10450
|
+
for (const f of this.features) {
|
|
10451
|
+
for (const k of fields) { const v = flatNumber(f.properties[k], this.flags); if (isFinite(v)) pooled.push(v); }
|
|
10452
|
+
}
|
|
10453
|
+
let nMin = Infinity, nMax = -Infinity;
|
|
10454
|
+
for (const v of pooled) { if (v < nMin) nMin = v; if (v > nMax) nMax = v; }
|
|
10455
|
+
const nParts = Math.trunc(styleNum(this.style.classes)) || colorSchemeClassCount(this.style.colorscheme) || DEFAULT_RANGE_CLASSES;
|
|
10456
|
+
if (fields.length > 1) {
|
|
10457
|
+
Object.assign(this, computeMultiFieldClasses(this.binding, this.style));
|
|
10458
|
+
this.categoryIndexByLabel = new Map(this.categoryLabels.map((v, i) => [v, i]));
|
|
10459
|
+
const scheme = Array.isArray(this.style.colorscheme) ? this.style.colorscheme : [];
|
|
10460
|
+
this._partNoFill = new Set(fields.map((f, i) => i).filter(i => String(scheme[i]).trim().toLowerCase() === 'none'));
|
|
10461
|
+
} else {
|
|
10462
|
+
const r = computeRangeClasses(this.features, this.binding, this.style, this.flags, v => this._formatTooltipValue(v));
|
|
10463
|
+
if (r) { Object.assign(this, r); this._rangeClassed = false; }
|
|
10464
|
+
this._multiFields = null;
|
|
10465
|
+
this._partNoFill = new Set();
|
|
10466
|
+
}
|
|
10467
|
+
this._pieFields = fields;
|
|
10468
|
+
if (pooled.length) {
|
|
10469
|
+
this._valueMin = nMin;
|
|
10470
|
+
this._valueMax = nMax;
|
|
10471
|
+
this.partsA = flatRangeParts(pooled, nMin, nMax, nParts, this.flags, styleNum(this.style.rangecentervalue));
|
|
10472
|
+
}
|
|
10473
|
+
}
|
|
10474
|
+
|
|
10025
10475
|
// x-axis labels of a per-item PLOT (style.xaxis / label, else the fields)
|
|
10026
10476
|
_itemPlotLabels() {
|
|
10027
10477
|
const fields = this._itemPlotFields();
|
|
@@ -11434,13 +11884,71 @@
|
|
|
11434
11884
|
// fillopacity and linecolor, as a single item (flat draws one
|
|
11435
11885
|
// symbol) — the packed multi-category icon is for CATEGORICAL cells
|
|
11436
11886
|
const singleSymbol = d => !d.properties.counts || (this._rangeClassed && d.properties.cat != null);
|
|
11437
|
-
|
|
11887
|
+
// flat's symbol outline (maptheme.js 21595-21610 and 21950-21997,
|
|
11888
|
+
// drawn 21625-21634 / 21995-22035): every SYMBOL shape is stroked,
|
|
11889
|
+
// (linewidth || 1) · normalX(min(2r / rMax, 0.2)) with rMax the
|
|
11890
|
+
// normal radius normalX(30 / 2) — on screen min(2r / 15, 0.2 unit)
|
|
11891
|
+
// × linewidth, centered on the outline like any SVG stroke. Its
|
|
11892
|
+
// color: linecolor; NOLINES or linecolor "none" none; a CATEGORICAL
|
|
11893
|
+
// theme without AGGREGATE/GROUP the symbol's own color (white: gray),
|
|
11894
|
+
// every other symbol chart the darker ChartColors.textColor of its
|
|
11895
|
+
// class color. BUBBLE keeps its linecolor/linewidth border, a width
|
|
11896
|
+
// in icon pixels
|
|
11897
|
+
const symbolOutline = flatChartBranch(this.flags) === 'symbol';
|
|
11898
|
+
const outlineRgb = d => {
|
|
11899
|
+
if (!symbolOutline) return singleBorderColorRgb && singleBorderWidthPx > 0 ? singleBorderColorRgb : null;
|
|
11900
|
+
if (singleBorderColorRgb) return singleBorderColorRgb;
|
|
11901
|
+
if (flatFlag(this.flags, 'NOLINES') || (this.style.linecolor && styleLineColor(this.style.linecolor) === 'none')) return null;
|
|
11902
|
+
const fill = (this.categoryColorsRgb || [])[d.properties.cat] || [128, 128, 128];
|
|
11903
|
+
if (perPartBranch) return flatChartTextRgb(fill);
|
|
11904
|
+
return fill.slice(0, 3).every(c => c >= 255) ? [128, 128, 128] : fill;
|
|
11905
|
+
};
|
|
11906
|
+
const outlinePx = (d, r) => (!outlineRgb(d) ? 0 : symbolOutline
|
|
11907
|
+
? (styleNum(this.style.linewidth) || 1) * Math.min(2 * r / NORMAL_RADIUS_PX, 0.2 * unitPx)
|
|
11908
|
+
: singleBorderWidthPx * r * 2 / BUBBLE_ICON_SIZE);
|
|
11909
|
+
// only single circles (no group icon, no other shape, no pixel
|
|
11910
|
+
// offset — ScatterplotLayer has none): drawn as real circles. A
|
|
11911
|
+
// cached circle icon shrunk to a few device pixels samples a coarse
|
|
11912
|
+
// mip level of the atlas and reads as a square, where flat's SVG
|
|
11913
|
+
// circle of the same radius stays round. Its own id: deck.gl matches
|
|
11914
|
+
// layers by id alone, so a theme switch turning the IconLayer of
|
|
11915
|
+
// this id into a ScatterplotLayer kept the icon layer's state and
|
|
11916
|
+
// drew nothing (the fuel-price page's station points); hover/click
|
|
11917
|
+
// routing goes by the final id (layerRuntimeById). A symbol's outline
|
|
11918
|
+
// is centered on its radius (flat); BUBBLE's border keeps the
|
|
11919
|
+
// icon's look, inside the radius
|
|
11920
|
+
if (combined.length && !alignOf.active && combined.every(d => singleSymbol(d) && shapeOf(d.properties) === 'circle')) {
|
|
11921
|
+
const radiusPx = d => vRadius(sizeValueOf(d));
|
|
11922
|
+
const lineRgb = d => outlineRgb(d) || [0, 0, 0];
|
|
11923
|
+
layers.push(new ScatterplotLayer({
|
|
11924
|
+
id: `ix-bubbles-${this.name}-circles-g${this._iconGeneration}`,
|
|
11925
|
+
data: combined, pickable: true,
|
|
11926
|
+
getPosition: d => d.geometry.coordinates,
|
|
11927
|
+
getRadius: d => radiusPx(d) - (symbolOutline ? 0 : outlinePx(d, radiusPx(d)) / 2),
|
|
11928
|
+
radiusUnits: 'pixels',
|
|
11929
|
+
getFillColor: d => [...((this.categoryColorsRgb || [])[d.properties.cat] || [128, 128, 128]).slice(0, 3), Math.round(this._iconAlpha(d) * fillOpacity)],
|
|
11930
|
+
stroked: combined.some(d => outlineRgb(d)),
|
|
11931
|
+
getLineColor: d => [...lineRgb(d).slice(0, 3), this._iconAlpha(d)],
|
|
11932
|
+
getLineWidth: d => outlinePx(d, radiusPx(d)),
|
|
11933
|
+
lineWidthUnits: 'pixels',
|
|
11934
|
+
billboard: true,
|
|
11935
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
11936
|
+
}));
|
|
11937
|
+
} else layers.push(new IconLayer({
|
|
11438
11938
|
id: `ix-bubbles-${this.name}-g${this._iconGeneration}`,
|
|
11439
11939
|
data: combined, pickable: true,
|
|
11440
11940
|
getPosition: d => d.geometry.coordinates,
|
|
11441
|
-
getIcon: d =>
|
|
11442
|
-
|
|
11443
|
-
|
|
11941
|
+
getIcon: d => {
|
|
11942
|
+
if (!singleSymbol(d)) return this._buildBubbleIcon(d.properties.counts, this.categoryColorsRgb, this._groupIsolation(d), Array.isArray(this.style.symbols) ? this.style.symbols : null);
|
|
11943
|
+
const shape = shapeOf(d.properties), rgb = outlineRgb(d);
|
|
11944
|
+
if (!symbolOutline || !rgb) return this._buildSingleIcon((this.categoryColorsRgb || [])[d.properties.cat], fillOpacity, shape, rgb, rgb ? singleBorderWidthPx : 0);
|
|
11945
|
+
// the outline in the icon's own pixels, inside its edge; quarter
|
|
11946
|
+
// pixel steps keep the icon cache small. flat's "empty" strokes
|
|
11947
|
+
// #888888 whatever the class color
|
|
11948
|
+
const r = vRadius(sizeValueOf(d));
|
|
11949
|
+
const w = Math.round(outlinePx(d, r) * BUBBLE_ICON_SIZE / Math.max(1e-6, 2 * r) * 4) / 4;
|
|
11950
|
+
return this._buildSingleIcon((this.categoryColorsRgb || [])[d.properties.cat], fillOpacity, shape, shape === 'empty' ? [136, 136, 136] : rgb, w);
|
|
11951
|
+
},
|
|
11444
11952
|
getSize: d => vRadius(sizeValueOf(d)) * 2,
|
|
11445
11953
|
// group icons bake a fixed 0.9 alpha; an explicit fillopacity scales it
|
|
11446
11954
|
// so a near-transparent theme doesn't show opaque 1-px-cell clusters
|
|
@@ -11478,8 +11986,8 @@
|
|
|
11478
11986
|
// VALUES: bold value label centered on each bubble (see
|
|
11479
11987
|
// flatValueText/valuesFontSizePx above). Labels are their own
|
|
11480
11988
|
// layers, added last in the array so they draw on top of every icon
|
|
11481
|
-
// layer
|
|
11482
|
-
//
|
|
11989
|
+
// layer; a label under later-drawn charts is faded or dropped by
|
|
11990
|
+
// _occludeChartTexts, as flat's later chart groups cover it. Entries whose
|
|
11483
11991
|
// computed font size would be sub-pixel are dropped rather than
|
|
11484
11992
|
// rendered at getSize: 0, matching the real engine's own "too
|
|
11485
11993
|
// small to bother" gate.
|
|
@@ -11488,7 +11996,11 @@
|
|
|
11488
11996
|
const textOpts = this._valueTextOpts();
|
|
11489
11997
|
const pointValueOf = valueTextOf || this._chartValueTextFn();
|
|
11490
11998
|
|
|
11999
|
+
// draw order of each chart (see chartTextTransmittance below); an
|
|
12000
|
+
// item maxcharts left out draws no chart, so no text either
|
|
12001
|
+
const orderOf = new Map(combined.map((d, k) => [d, k]));
|
|
11491
12002
|
const pointLabels = individual.reduce((out, d) => {
|
|
12003
|
+
if (!orderOf.has(d)) return out;
|
|
11492
12004
|
const radius = vRadius(d.properties.value);
|
|
11493
12005
|
// flat's VALUES label prints the value (nValuesA), not the size;
|
|
11494
12006
|
// its font by flat's chart branch (_chartValueTextFn)
|
|
@@ -11496,7 +12008,7 @@
|
|
|
11496
12008
|
if (fontSize > VALUES_MIN_FONT_PX) {
|
|
11497
12009
|
const o = alignOf(d);
|
|
11498
12010
|
out.push({ geometry: d.geometry, text, fontSize, bold, color: resolveTextColor(this.style, contrastTextColor((this.categoryColorsRgb || [])[d.properties.cat])),
|
|
11499
|
-
pixelOffset: baseline ? [o[0], o[1] + baseline - 0.35 * fontSize] : o });
|
|
12011
|
+
pixelOffset: baseline ? [o[0], o[1] + baseline - 0.35 * fontSize] : o, order: orderOf.get(d) });
|
|
11500
12012
|
}
|
|
11501
12013
|
return out;
|
|
11502
12014
|
}, []);
|
|
@@ -11512,6 +12024,7 @@
|
|
|
11512
12024
|
// would misrepresent every category's own count as one summed
|
|
11513
12025
|
// number nobody's individual bubble actually shows.
|
|
11514
12026
|
const groupLabels = groups.reduce((out, d) => {
|
|
12027
|
+
if (!orderOf.has(d)) return out;
|
|
11515
12028
|
const counts = d.properties.counts;
|
|
11516
12029
|
const outerRadiusPx = vRadius(d.properties.total);
|
|
11517
12030
|
const iconSizePx = outerRadiusPx * 2; // matches the cluster IconLayer's own getSize (*2) below
|
|
@@ -11526,7 +12039,7 @@
|
|
|
11526
12039
|
const o = alignOf(d);
|
|
11527
12040
|
out.push({ geometry: d.geometry, text: v.text, fontSize: v.fontSize, bold: v.bold,
|
|
11528
12041
|
color: resolveTextColor(this.style, contrastTextColor((this.categoryColorsRgb || [])[d.properties.cat])),
|
|
11529
|
-
pixelOffset: v.baseline ? [o[0], o[1] + v.baseline - 0.35 * v.fontSize] : o });
|
|
12042
|
+
pixelOffset: v.baseline ? [o[0], o[1] + v.baseline - 0.35 * v.fontSize] : o, order: orderOf.get(d) });
|
|
11530
12043
|
}
|
|
11531
12044
|
return out;
|
|
11532
12045
|
}
|
|
@@ -11541,13 +12054,26 @@
|
|
|
11541
12054
|
out.push({
|
|
11542
12055
|
geometry: d.geometry, text, fontSize,
|
|
11543
12056
|
color: resolveTextColor(this.style, contrastTextColor((this.categoryColorsRgb || [])[p.i])),
|
|
11544
|
-
pixelOffset: [offsets[i].x * fitScale * pxPerCanvasUnit + alignOf(d)[0], offsets[i].y * fitScale * pxPerCanvasUnit + alignOf(d)[1]]
|
|
12057
|
+
pixelOffset: [offsets[i].x * fitScale * pxPerCanvasUnit + alignOf(d)[0], offsets[i].y * fitScale * pxPerCanvasUnit + alignOf(d)[1]],
|
|
12058
|
+
order: orderOf.get(d)
|
|
11545
12059
|
});
|
|
11546
12060
|
}
|
|
11547
12061
|
});
|
|
11548
12062
|
return out;
|
|
11549
12063
|
}, []);
|
|
11550
12064
|
|
|
12065
|
+
// a text under later charts fades by what still shows through them
|
|
12066
|
+
// (chartTextTransmittance), as flat's per-chart SVG groups paint
|
|
12067
|
+
// each later symbol over it; nearly hidden ones are dropped. (Flat's
|
|
12068
|
+
// later GLOW circles veil it too, but they veil the earlier symbols
|
|
12069
|
+
// as well, and here every glow lies under every symbol — a text
|
|
12070
|
+
// fading under a glow its own symbol isn't under would read wrong.)
|
|
12071
|
+
const visible = this._occludeChartTexts(combined, pointLabels.concat(groupLabels), {
|
|
12072
|
+
liveZoom, globeCenter, alignOf, radiusOf, singleSymbol, shapeOf, fillOpacity, singleBorderWidthPx
|
|
12073
|
+
});
|
|
12074
|
+
const keepVisible = labels => labels.filter(l => visible.has(l));
|
|
12075
|
+
const shownPointLabels = keepVisible(pointLabels), shownGroupLabels = keepVisible(groupLabels);
|
|
12076
|
+
|
|
11551
12077
|
const textLayerCommonProps = {
|
|
11552
12078
|
pickable: false,
|
|
11553
12079
|
getPosition: d => d.geometry.coordinates,
|
|
@@ -11565,10 +12091,10 @@
|
|
|
11565
12091
|
getAlignmentBaseline: 'center'
|
|
11566
12092
|
};
|
|
11567
12093
|
// flat's SYMBOL branch writes its values in normal weight
|
|
11568
|
-
if (
|
|
11569
|
-
fontWeight:
|
|
11570
|
-
if (
|
|
11571
|
-
fontWeight:
|
|
12094
|
+
if (shownPointLabels.length) layers.push(new TextLayer({ id: `ix-points-values-${this.name}`, data: shownPointLabels, getPixelOffset: d => d.pixelOffset, ...textLayerCommonProps,
|
|
12095
|
+
fontWeight: shownPointLabels[0].bold === false ? 'normal' : 'bold' }));
|
|
12096
|
+
if (shownGroupLabels.length) layers.push(new TextLayer({ id: `ix-cluster-values-${this.name}`, data: shownGroupLabels, getPixelOffset: d => d.pixelOffset, ...textLayerCommonProps,
|
|
12097
|
+
fontWeight: shownGroupLabels[0].bold === false ? 'normal' : 'bold' }));
|
|
11572
12098
|
}
|
|
11573
12099
|
|
|
11574
12100
|
// LABEL: flat's chart label (maptheme.js 24533-24590) — a small text
|
|
@@ -11792,6 +12318,299 @@
|
|
|
11792
12318
|
return layers;
|
|
11793
12319
|
}
|
|
11794
12320
|
|
|
12321
|
+
// CHART|PIE / DONUT — real ixmaps-flat's PIE branch (maptheme.js
|
|
12322
|
+
// 19939-20533, piechart.js DonutChart): one pie per record at its
|
|
12323
|
+
// position (joined to its FEATURE shape by binding.lookup), a slice per
|
|
12324
|
+
// value field (pieSliceLayout), drawn by PieSliceLayer (see there why
|
|
12325
|
+
// not as icons).
|
|
12326
|
+
// - radius (flat's nSize in chart units, 15 = NORMAL_RADIUS_PX, × the
|
|
12327
|
+
// object zoom and style.scale): with a type word containing SIZE
|
|
12328
|
+
// (not NORMSIZE) and a size field 15 · (size / max size)^(1/sizepow)
|
|
12329
|
+
// — max size: normalsizevalue, else the field's maximum (flat's
|
|
12330
|
+
// nMaxSize) —, SIZE without a size field 15 · (sum / (nMax − nMin))
|
|
12331
|
+
// ^(1/sizepow) — the pie's sum against the range of the part values
|
|
12332
|
+
// (normalsizevalue replaces that range) —, SIZELOG 15 · log(v) /
|
|
12333
|
+
// log(max), at least 1; otherwise 15. A pie of radius 0 (or no
|
|
12334
|
+
// number) is not drawn.
|
|
12335
|
+
// - colors: a field's colorscheme color; a 1-field pie (or CLASSES)
|
|
12336
|
+
// takes its value's class color (flat's partsA, maptheme.js
|
|
12337
|
+
// 20330-20341); a "none" color fills nothing and strokes the pie in
|
|
12338
|
+
// linecolor, else black; AUTOCOMPLETE's rest in nodatacolor (#eeeeee)
|
|
12339
|
+
// - fill opacity fillopacity (flat: the chart group's fill-opacity,
|
|
12340
|
+
// default 1); opacity sets fill and outline
|
|
12341
|
+
// - outline #555566 (NOLINES none, WHITELINES white, linecolor wins),
|
|
12342
|
+
// (linewidth, default 0.1) · R / 15 px wide — flat's
|
|
12343
|
+
// normalX(linewidth) · R / 300 with normalX(1) = 20 chart units
|
|
12344
|
+
// (measured on the twin page): a hairline at the default
|
|
12345
|
+
// - CENTER (not DONUT): the center part's disc, 0.9 × the inner
|
|
12346
|
+
// radius, in its color, with GLOW two halos (6× at 0.05, 2× at 0.1
|
|
12347
|
+
// fill opacity); its value in bold in the middle with CENTERVALUE or
|
|
12348
|
+
// VALUES: min(0.8 r, 3.3 r / length) · valuescale, valuedecimals
|
|
12349
|
+
// (else 0), the unit up to 5 characters, ChartColors.textColor of
|
|
12350
|
+
// the part color
|
|
12351
|
+
// - DONUT|CENTERVALUE: the item's size value in the middle, min(0.5 r,
|
|
12352
|
+
// 2 r / length), valuecolor / textcolor, else #888888, over a 0.5
|
|
12353
|
+
// black halo
|
|
12354
|
+
// - VALUES (not STACKED, within valueupper/lower): several parts (or
|
|
12355
|
+
// NOINLINETEXT) a label per slice outside the pie on a leader line
|
|
12356
|
+
// (pieValueLabelLayout) — font 5u · valuescale, #999999 (textcolor)
|
|
12357
|
+
// on a white box at 0.5 (VALUEBACKGROUND: the slice color), leader
|
|
12358
|
+
// lines #888888 0.5u over a white 1.5u at 0.2; a single part its
|
|
12359
|
+
// value inside the pie, min(0.8 R, 3.4 R / length), 0.1 R above the
|
|
12360
|
+
// center. Texts: valuedecimals, else 2 below 1 and 0 above, plus the
|
|
12361
|
+
// unit.
|
|
12362
|
+
// - draw order: flat's sort before drawing (by the size value, the
|
|
12363
|
+
// sum or the first value; the biggest on top)
|
|
12364
|
+
// Not drawn here (a console note at type parsing, PIE_INERT_FLAGS):
|
|
12365
|
+
// 3D, VOLUME, HEIGHT, STARBURST, FLOWER/RAYS, BOW, ZOOM, DIRECTION /
|
|
12366
|
+
// DIRECTED, POLAR — the pie is drawn flat and plain instead. Nor
|
|
12367
|
+
// style.clipparts, showparts, gridx (flat's several pies per item).
|
|
12368
|
+
// Known divergence: flat draws a chart's value labels in the chart's
|
|
12369
|
+
// own group, so a bigger pie drawn later covers a smaller neighbour's
|
|
12370
|
+
// labels; here every label lies above every pie.
|
|
12371
|
+
// Known divergence: the leader lines are drawn in map coordinates
|
|
12372
|
+
// (pixel offsets converted at the current zoom, Web Mercator), so under
|
|
12373
|
+
// the globe projection they are approximate.
|
|
12374
|
+
_buildPieLayers(zoom, bbox, liveZoom = zoom, globeCenter = null) {
|
|
12375
|
+
const st = this.style;
|
|
12376
|
+
const t = this.flags.typeString != null ? this.flags.typeString : [...this.flags].join('|');
|
|
12377
|
+
const has = re => re.test(t);
|
|
12378
|
+
const fields = this._pieFields || String(this.binding.value || '').split('|');
|
|
12379
|
+
const multi = fields.length > 1;
|
|
12380
|
+
const unit = objectZoomFactor(liveZoom, this.mapOptions) * (styleNum(st.scale) || 1);
|
|
12381
|
+
const sizePow = resolveSizePow(st, this.flags);
|
|
12382
|
+
// flat tests /SIZE/: also NOSIZE and GRIDSIZE
|
|
12383
|
+
const sizeFlag = has(/SIZE/) && !has(/NORMSIZE/);
|
|
12384
|
+
const sizeLog = has(/SIZELOG/);
|
|
12385
|
+
const nsv = styleNum(st.normalsizevalue);
|
|
12386
|
+
const maxSize = nsv || this._maxSizeValue;
|
|
12387
|
+
const range = nsv || ((this._valueMax || 0) - (this._valueMin || 0));
|
|
12388
|
+
const nSizeOf = (sum, size) => {
|
|
12389
|
+
const n = NORMAL_RADIUS_PX;
|
|
12390
|
+
if (sizeFlag && this.binding.size) {
|
|
12391
|
+
return sizeLog ? Math.max(1, n / Math.log(maxSize) * Math.log(size)) : n / Math.pow(maxSize, 1 / sizePow) * Math.pow(size, 1 / sizePow);
|
|
12392
|
+
}
|
|
12393
|
+
if (sizeFlag) {
|
|
12394
|
+
return sizeLog ? Math.max(1, n / Math.log(range) * Math.log(sum)) : n / Math.pow(range, 1 / sizePow) * Math.pow(sum, 1 / sizePow);
|
|
12395
|
+
}
|
|
12396
|
+
return n;
|
|
12397
|
+
};
|
|
12398
|
+
const colors = this.categoryColorsRgb || [];
|
|
12399
|
+
const scheme = Array.isArray(st.colorscheme) ? st.colorscheme : [];
|
|
12400
|
+
const noFill = this._partNoFill || new Set();
|
|
12401
|
+
const byClass = has(/CLASSES/) || !multi;
|
|
12402
|
+
const classColor = v => {
|
|
12403
|
+
const parts = this.partsA || [];
|
|
12404
|
+
for (let x = 0; x < parts.length; x++) if (v < parts[x].max) return colors[x];
|
|
12405
|
+
return null;
|
|
12406
|
+
};
|
|
12407
|
+
const sliceRgb = sl => (sl.complement ? (parseCssColor(String(st.nodatacolor || '')) || [0xee, 0xee, 0xee])
|
|
12408
|
+
: (byClass && classColor(sl.value)) || colors[sl.i] || [128, 128, 128]);
|
|
12409
|
+
const fo = styleNum(st.fillopacity), op = styleNum(st.opacity);
|
|
12410
|
+
// flat sets the group's fill-opacity only for a truthy value
|
|
12411
|
+
const fillAlpha = op > 0 ? op : fo > 0 ? fo : 1;
|
|
12412
|
+
const lineAlpha = op > 0 ? op : 1;
|
|
12413
|
+
let line = '#555566';
|
|
12414
|
+
if (has(/NOLINES/)) line = 'none';
|
|
12415
|
+
if (has(/WHITELINES/)) line = 'white';
|
|
12416
|
+
const lc = st.linecolor != null && st.linecolor !== '' ? styleLineColor(st.linecolor) : null;
|
|
12417
|
+
if (lc) line = lc;
|
|
12418
|
+
if (scheme.slice(0, fields.length).some(c => String(c).trim().toLowerCase() === 'none')) line = lc || 'black';
|
|
12419
|
+
const lineRgb = line === 'none' ? null : hexOrNamedToRgb(line);
|
|
12420
|
+
const lineWidthOf = r => (lineRgb ? (styleNum(st.linewidth) || 0.1) * r / 15 : 0);
|
|
12421
|
+
const lineRgba = lineRgb ? [...lineRgb.slice(0, 3), Math.round(255 * lineAlpha)] : [0, 0, 0, 0];
|
|
12422
|
+
|
|
12423
|
+
const charts = [];
|
|
12424
|
+
for (const f of this._activeFeatures || this.features) {
|
|
12425
|
+
const c = f.geometry && f.geometry.coordinates;
|
|
12426
|
+
if (!c || typeof c[0] !== 'number') continue;
|
|
12427
|
+
const [lng, lat] = c;
|
|
12428
|
+
if (lng < bbox[0] || lng > bbox[2] || lat < bbox[1] || lat > bbox[3]) continue;
|
|
12429
|
+
if (globeCenter && !isOnVisibleHemisphere(lng, lat, globeCenter)) continue;
|
|
12430
|
+
const raw = f.properties || {};
|
|
12431
|
+
const values = fields.map(k => { const v = flatNumber(raw[k], this.flags); return isNaN(v) ? 0 : v; });
|
|
12432
|
+
const layout = pieSliceLayout(values, t, { centerPart: st.centerpart });
|
|
12433
|
+
if (!layout) continue;
|
|
12434
|
+
const sum = values.reduce((a, v) => a + v, 0);
|
|
12435
|
+
const size = this.binding.size ? parseFloat(raw[this.binding.size]) : undefined;
|
|
12436
|
+
const r = nSizeOf(sum, size) * unit;
|
|
12437
|
+
if (!(r > 0) || !isFinite(r)) continue;
|
|
12438
|
+
// flat's draw-order key (maptheme.js 16812-16860): the size value,
|
|
12439
|
+
// else with SIZE the sum, else the first value
|
|
12440
|
+
const key = this.binding.size ? size : has(/SIZE/) ? sum : values[0];
|
|
12441
|
+
// the tooltip's {{theme.item.chart}}: a row per field (parts), a
|
|
12442
|
+
// 1-field pie its class (cat) like a range-classed bubble
|
|
12443
|
+
const properties = multi ? { value: values[0], parts: values, raw }
|
|
12444
|
+
: { value: values[0], cat: this.partsA ? this._resolvePartsClass(values[0]) : 0, raw };
|
|
12445
|
+
charts.push({ c, r, layout, size, key, properties });
|
|
12446
|
+
}
|
|
12447
|
+
// flat sorts the charts before drawing (maptheme.js 16774-16870),
|
|
12448
|
+
// ascending — the last drawn on top —, SORT|UP descending; not with
|
|
12449
|
+
// NOSRT, NOSIZE (unless SORT) or when all values are equal
|
|
12450
|
+
if (((this._valueMin !== this._valueMax) || this.binding.size) && !has(/NOSRT/)
|
|
12451
|
+
&& (!has(/NOSIZE/) || has(/\bSORT\b/))) {
|
|
12452
|
+
const down = has(/\bSORT\b/) && has(/\bUP\b/);
|
|
12453
|
+
charts.sort((a, b) => (down ? b.key - a.key : a.key - b.key) || 0);
|
|
12454
|
+
}
|
|
12455
|
+
// align / offsetx / offsety (flatChartAlignOffset) — flat's PIE
|
|
12456
|
+
// ptNull is (0, r + 5 units); the chart's half size taken as r, as
|
|
12457
|
+
// for BUBBLE (ASSUMPTION: not measured on flat for PIE)
|
|
12458
|
+
const aligned = !!(st.align || styleNum(st.offsetx) || styleNum(st.offsety));
|
|
12459
|
+
charts.forEach(ch => {
|
|
12460
|
+
ch.off = aligned ? flatChartAlignOffset(st, { unit, symbolScale: 1, half: ch.r, orig: ch.r + 5 * unit }) : [0, 0];
|
|
12461
|
+
});
|
|
12462
|
+
|
|
12463
|
+
const TAU = 2 * Math.PI;
|
|
12464
|
+
const slices = [], centers = [], glows = [];
|
|
12465
|
+
const glowOn = has(/GLOW/) && !has(/ZOOM/) && !glowHiddenByScale(st, liveZoom);
|
|
12466
|
+
for (const ch of charts) {
|
|
12467
|
+
for (const sl of ch.layout.slices) {
|
|
12468
|
+
const rgb = sliceRgb(sl);
|
|
12469
|
+
slices.push({
|
|
12470
|
+
position: ch.c, r: ch.r, off: ch.off, properties: ch.properties, chart: ch, part: sl,
|
|
12471
|
+
slice: [sl.start * Math.PI / 180, sl.sweep >= 360 ? TAU : sl.sweep * Math.PI / 180, ch.layout.inner],
|
|
12472
|
+
fill: [...rgb.slice(0, 3), !sl.complement && noFill.has(sl.i) ? 0 : Math.round(255 * fillAlpha)]
|
|
12473
|
+
});
|
|
12474
|
+
}
|
|
12475
|
+
const center = ch.layout.center;
|
|
12476
|
+
if (center) {
|
|
12477
|
+
const cr = ch.r * Math.sqrt(center.size / 100);
|
|
12478
|
+
ch.centerR = cr;
|
|
12479
|
+
const rgb = colors[center.i] || [128, 128, 128];
|
|
12480
|
+
if (cr > 0) centers.push({ position: ch.c, r: cr * 0.9, off: ch.off, properties: ch.properties, chart: ch, fill: [...rgb.slice(0, 3), Math.round(255 * fillAlpha)] });
|
|
12481
|
+
if (glowOn && cr > 0) {
|
|
12482
|
+
glows.push({ position: ch.c, r: cr * 6, off: ch.off, fill: [...rgb.slice(0, 3), Math.round(255 * 0.05)] });
|
|
12483
|
+
glows.push({ position: ch.c, r: cr * 2, off: ch.off, fill: [...rgb.slice(0, 3), Math.round(255 * 0.1)] });
|
|
12484
|
+
}
|
|
12485
|
+
}
|
|
12486
|
+
}
|
|
12487
|
+
const discProps = {
|
|
12488
|
+
getPosition: d => d.position,
|
|
12489
|
+
getRadius: d => d.r,
|
|
12490
|
+
getPixelOffset: d => d.off,
|
|
12491
|
+
getFillColor: d => d.fill,
|
|
12492
|
+
radiusUnits: 'pixels', billboard: true, stroked: false, filled: true, antialiasing: true,
|
|
12493
|
+
parameters: ICON_LAYER_GLOBE_PARAMETERS
|
|
12494
|
+
};
|
|
12495
|
+
const layers = [new PieSliceLayer(Object.assign({}, discProps, {
|
|
12496
|
+
id: `ix-pie-${this.name}`,
|
|
12497
|
+
data: slices, pickable: true,
|
|
12498
|
+
getLineColor: lineRgba,
|
|
12499
|
+
getSlice: d => d.slice,
|
|
12500
|
+
getSliceLineWidth: d => lineWidthOf(d.r)
|
|
12501
|
+
}))];
|
|
12502
|
+
if (centers.length) {
|
|
12503
|
+
layers.push(new PieSliceLayer(Object.assign({}, discProps, { id: `ix-pie-center-${this.name}`, data: centers, pickable: true })));
|
|
12504
|
+
}
|
|
12505
|
+
if (glows.length) {
|
|
12506
|
+
layers.push(new PieSliceLayer(Object.assign({}, discProps, { id: `ix-pie-glow-${this.name}`, data: glows, pickable: false })));
|
|
12507
|
+
}
|
|
12508
|
+
return layers.concat(this._buildPieTextLayers(charts, zoom, liveZoom, unit, sliceRgb, has));
|
|
12509
|
+
}
|
|
12510
|
+
|
|
12511
|
+
// the value texts of _buildPieLayers (see there)
|
|
12512
|
+
_buildPieTextLayers(charts, zoom, liveZoom, unit, sliceRgb, has) {
|
|
12513
|
+
const st = this.style;
|
|
12514
|
+
const valueScale = styleNum(st.valuescale) || 1;
|
|
12515
|
+
const units = st.units ? String(st.units) : '';
|
|
12516
|
+
const unitText = units ? (units[0] === '.' ? '' : ' ') + units : '';
|
|
12517
|
+
const partUnit = (has(/AUTOCOMPLETE/) && !/%/.test(unitText) ? ' % ' : '') + unitText;
|
|
12518
|
+
const noBreaks = flatNoBreaks(this._valueMin, this._valueMax);
|
|
12519
|
+
const dec = explicitValueDecimals(st);
|
|
12520
|
+
const fmt = (v, d) => flatFormatValue(v, d, noBreaks ? 'ROUND|NOBREAKS' : 'ROUND');
|
|
12521
|
+
const partText = sl => (sl.complement ? Math.round(sl.percent * 10) / 10 + ' % '
|
|
12522
|
+
: fmt(sl.value, dec != null ? dec : (sl.value < 1 ? 2 : 0)) + partUnit);
|
|
12523
|
+
const valuesOn = has(/VALUES/) && !has(/STACKED/) && !valuesHiddenByScale(st, zoom);
|
|
12524
|
+
const textOverride = st.valuecolor || st.textcolor;
|
|
12525
|
+
const inline = [], boxed = [], centerTexts = [], lines = [];
|
|
12526
|
+
const nFields = (this._pieFields || []).length;
|
|
12527
|
+
for (const ch of charts) {
|
|
12528
|
+
const o = ch.off;
|
|
12529
|
+
// CENTER: the center part's value (maptheme.js 20402-20410)
|
|
12530
|
+
if (ch.layout.center && (has(/CENTERVALUE/) || (has(/VALUES/) && !valuesHiddenByScale(st, zoom))) && ch.centerR > 0) {
|
|
12531
|
+
const cv = ch.layout.center.value;
|
|
12532
|
+
const text = fmt(cv, dec != null ? dec : 0) + (unitText.length <= 5 ? unitText : '');
|
|
12533
|
+
const fontSize = Math.min(ch.centerR * 0.8, ch.centerR * (3.3 / Math.max(1, text.length))) * valueScale;
|
|
12534
|
+
const rgb = (this.categoryColorsRgb || [])[ch.layout.center.i] || [128, 128, 128];
|
|
12535
|
+
if (fontSize > VALUES_MIN_FONT_PX) centerTexts.push({ position: ch.c, text, fontSize, off: o, color: flatChartTextRgb(rgb), bold: true });
|
|
12536
|
+
} else if (!ch.layout.center && has(/CENTERVALUE/) && ch.size !== undefined && !isNaN(ch.size)) {
|
|
12537
|
+
// flat prints String(nCount || nSize): a record's item has no
|
|
12538
|
+
// nCount, so its size value
|
|
12539
|
+
const text = String(ch.size);
|
|
12540
|
+
const fontSize = Math.min(ch.r * 0.5, ch.r * (2 / Math.max(1, text.length)));
|
|
12541
|
+
if (fontSize > VALUES_MIN_FONT_PX) {
|
|
12542
|
+
centerTexts.push({ position: ch.c, text, fontSize, off: o, bold: false,
|
|
12543
|
+
color: textOverride ? hexOrNamedToRgb(textOverride) : [0x88, 0x88, 0x88] });
|
|
12544
|
+
}
|
|
12545
|
+
}
|
|
12546
|
+
if (!valuesOn) continue;
|
|
12547
|
+
if (nFields > 1 || has(/NOINLINETEXT/)) {
|
|
12548
|
+
const fontSize = 5 * unit * valueScale;
|
|
12549
|
+
if (fontSize <= VALUES_MIN_FONT_PX) continue;
|
|
12550
|
+
for (const lab of pieValueLabelLayout(ch.layout.slices, ch.r, fontSize, unit, (this.partsA || []).length)) {
|
|
12551
|
+
const bg = has(/VALUEBACKGROUND/) ? sliceRgb(lab.slice) : [255, 255, 255];
|
|
12552
|
+
const textRgb = textOverride ? hexOrNamedToRgb(textOverride) : flatDerivateRgb(bg, bg[0] + bg[1] + bg[2] > 450 ? 0.6 : 3);
|
|
12553
|
+
boxed.push({ position: ch.c, text: partText(lab.slice), fontSize, anchor: lab.anchor,
|
|
12554
|
+
// the text's baseline → its center (arial: 0.35 font above)
|
|
12555
|
+
off: [o[0] + lab.x, o[1] + lab.y - 0.35 * fontSize], color: textRgb, bg: [...bg.slice(0, 3), 128] });
|
|
12556
|
+
lab.segments.forEach(sg => lines.push({ c: ch.c, o, sg }));
|
|
12557
|
+
}
|
|
12558
|
+
} else if (ch.layout.slices.length) {
|
|
12559
|
+
const sl = ch.layout.slices[ch.layout.slices.length - 1];
|
|
12560
|
+
const text = partText(sl);
|
|
12561
|
+
const fontSize = Math.min(ch.r * 0.8, ch.r * (3.4 / Math.max(1, text.length)));
|
|
12562
|
+
const color = textOverride ? hexOrNamedToRgb(textOverride) : flatChartTextRgb(sliceRgb(sl));
|
|
12563
|
+
// baseline at −0.6 R + R / 2 + 0.45 font (maptheme.js 20434)
|
|
12564
|
+
if (fontSize > VALUES_MIN_FONT_PX) inline.push({ position: ch.c, text, fontSize, off: [o[0], o[1] - 0.1 * ch.r + 0.45 * fontSize - 0.35 * fontSize], color });
|
|
12565
|
+
}
|
|
12566
|
+
}
|
|
12567
|
+
const layers = [];
|
|
12568
|
+
const textBase = {
|
|
12569
|
+
getPosition: d => d.position, getText: d => d.text, getSize: d => d.fontSize,
|
|
12570
|
+
getPixelOffset: d => d.off, getColor: d => d.color,
|
|
12571
|
+
sizeUnits: 'pixels', fontFamily: 'arial', characterSet: 'auto',
|
|
12572
|
+
getAlignmentBaseline: 'center', pickable: false
|
|
12573
|
+
};
|
|
12574
|
+
if (lines.length) {
|
|
12575
|
+
const feature = d => ({ type: 'Feature', properties: {}, geometry: { type: 'LineString', coordinates: [
|
|
12576
|
+
pixelOffsetLngLat(d.c, [d.o[0] + d.sg[0], d.o[1] + d.sg[1]], liveZoom),
|
|
12577
|
+
pixelOffsetLngLat(d.c, [d.o[0] + d.sg[2], d.o[1] + d.sg[3]], liveZoom)] } });
|
|
12578
|
+
const fc = { type: 'FeatureCollection', features: lines.map(feature) };
|
|
12579
|
+
[['bg', [255, 255, 255, Math.round(255 * 0.2)], 1.5], ['fg', [0x88, 0x88, 0x88, 255], 0.5]].forEach(([k, color, w]) => {
|
|
12580
|
+
layers.push(new GeoJsonLayer({
|
|
12581
|
+
id: `ix-pie-lines-${k}-${this.name}`, data: fc, pickable: false,
|
|
12582
|
+
stroked: true, filled: false, getLineColor: color, getLineWidth: w * unit, lineWidthUnits: 'pixels'
|
|
12583
|
+
}));
|
|
12584
|
+
});
|
|
12585
|
+
}
|
|
12586
|
+
if (boxed.length) {
|
|
12587
|
+
layers.push(new TextLayer(Object.assign({}, textBase, {
|
|
12588
|
+
id: `ix-pie-values-${this.name}`, data: boxed, fontWeight: 'normal',
|
|
12589
|
+
getTextAnchor: d => d.anchor,
|
|
12590
|
+
// flat's box: from 0.3 font left of the text to 0.4 font right of
|
|
12591
|
+
// it, 0.1 font taller (createTextLabel)
|
|
12592
|
+
background: true, getBackgroundColor: d => d.bg, backgroundBorderRadius: 0.1 * 5 * unit * valueScale,
|
|
12593
|
+
backgroundPadding: [0.3, 0, 0.4, 0.1].map(k => k * 5 * unit * valueScale)
|
|
12594
|
+
})));
|
|
12595
|
+
}
|
|
12596
|
+
if (inline.length) {
|
|
12597
|
+
layers.push(new TextLayer(Object.assign({}, textBase, { id: `ix-pie-inline-${this.name}`, data: inline, fontWeight: 'normal', getTextAnchor: 'middle' })));
|
|
12598
|
+
}
|
|
12599
|
+
// CENTER's value in bold; DONUT|CENTERVALUE's in normal weight over
|
|
12600
|
+
// flat's thin black halo (stroke-opacity 0.5)
|
|
12601
|
+
const bold = centerTexts.filter(d => d.bold), plain = centerTexts.filter(d => !d.bold);
|
|
12602
|
+
if (bold.length) {
|
|
12603
|
+
layers.push(new TextLayer(Object.assign({}, textBase, { id: `ix-pie-centervalue-${this.name}`, data: bold, getTextAnchor: 'middle', fontWeight: 'bold' })));
|
|
12604
|
+
}
|
|
12605
|
+
if (plain.length) {
|
|
12606
|
+
layers.push(new TextLayer(Object.assign({}, textBase, {
|
|
12607
|
+
id: `ix-pie-centercount-${this.name}`, data: plain, getTextAnchor: 'middle', fontWeight: 'normal',
|
|
12608
|
+
fontSettings: { sdf: true }, outlineWidth: 1, outlineColor: [0, 0, 0, 128]
|
|
12609
|
+
})));
|
|
12610
|
+
}
|
|
12611
|
+
return layers;
|
|
12612
|
+
}
|
|
12613
|
+
|
|
11795
12614
|
// flat's chart position for this build (flatChartAlignOffset): a
|
|
11796
12615
|
// function item → pixel offset, the same for every part of one chart
|
|
11797
12616
|
// (symbol, glow, shadow, box, title, value text). unit is flat's
|
|
@@ -11909,6 +12728,60 @@
|
|
|
11909
12728
|
return { noBreaks: flatNoBreaks(vMin, vMax), maxValue: styleNum(this.style.normalsizevalue) || (isFinite(vMax) ? vMax : undefined) };
|
|
11910
12729
|
}
|
|
11911
12730
|
|
|
12731
|
+
// The charts of _buildChartLayers' bubbles layer as
|
|
12732
|
+
// chartTextTransmittance's occluders, in draw order and screen
|
|
12733
|
+
// pixels, and each text's resulting alpha: the text's color gets it,
|
|
12734
|
+
// the returned Set holds the texts still visible. Screen pixels:
|
|
12735
|
+
// Web Mercator world pixels at the live zoom; under globe a local
|
|
12736
|
+
// equal-area frame around the view center at the scale MapLibre's
|
|
12737
|
+
// globe matches Mercator's there (the limb's foreshortening ignored).
|
|
12738
|
+
_occludeChartTexts(combined, texts, o) {
|
|
12739
|
+
const visible = new Set();
|
|
12740
|
+
if (!texts.length) return visible;
|
|
12741
|
+
const toRad = Math.PI / 180;
|
|
12742
|
+
let screenOf;
|
|
12743
|
+
if (o.globeCenter) {
|
|
12744
|
+
const s = 512 * Math.pow(2, o.liveZoom) / 360 / Math.cos(o.globeCenter.lat * toRad);
|
|
12745
|
+
screenOf = c => [(((c[0] - o.globeCenter.lng) % 360 + 540) % 360 - 180) * Math.cos(c[1] * toRad) * s, -c[1] * s];
|
|
12746
|
+
} else {
|
|
12747
|
+
screenOf = c => { const p = lngLatToWorldPixel(c[0], c[1], o.liveZoom); return [p.x, p.y]; };
|
|
12748
|
+
}
|
|
12749
|
+
const symbols = Array.isArray(this.style.symbols) ? this.style.symbols : null;
|
|
12750
|
+
const charts = combined.map(d => {
|
|
12751
|
+
const [x, y] = screenOf(d.geometry.coordinates);
|
|
12752
|
+
const a = o.alignOf(d), r = o.radiusOf(d), iconAlpha = this._iconAlpha(d) / 255;
|
|
12753
|
+
let parts;
|
|
12754
|
+
if (o.singleSymbol(d)) {
|
|
12755
|
+
// the icon's shape is inset by half its border (_buildSingleIcon)
|
|
12756
|
+
const shape = o.shapeOf(d.properties);
|
|
12757
|
+
parts = [{ dx: 0, dy: 0, r: r * (1 - o.singleBorderWidthPx / BUBBLE_ICON_SIZE), shape, alpha: o.fillOpacity * iconAlpha }];
|
|
12758
|
+
} else {
|
|
12759
|
+
// _buildBubbleIcon's packed parts, its baked 0.9 alpha scaled by an
|
|
12760
|
+
// explicit fillopacity like the bubbles layer's getColor
|
|
12761
|
+
const counts = d.properties.counts, iso = this._groupIsolation(d);
|
|
12762
|
+
const { present, radii, offsets, fitScale } = (iso && isolatedBubblePackLayout(counts, BUBBLE_ICON_SIZE, iso)) || computeBubblePackLayout(counts, BUBBLE_ICON_SIZE);
|
|
12763
|
+
const px = 2 * r / BUBBLE_ICON_SIZE * fitScale;
|
|
12764
|
+
const alpha = 0.9 * (this.style.fillopacity != null ? o.fillOpacity : 1) * iconAlpha;
|
|
12765
|
+
parts = present.map((p, i) => ({ dx: offsets[i].x * px, dy: offsets[i].y * px, r: radii[i] * px,
|
|
12766
|
+
shape: symbols ? normalizeSymbolShape(symbols[Math.min(p.i, symbols.length - 1)]) : 'circle', alpha }));
|
|
12767
|
+
}
|
|
12768
|
+
return { x: x + a[0], y: y + a[1], parts };
|
|
12769
|
+
});
|
|
12770
|
+
// the text box: Arial digits are ~0.56 em wide, ~0.7 em high
|
|
12771
|
+
const boxes = texts.map(t => {
|
|
12772
|
+
const [x, y] = screenOf(t.geometry.coordinates);
|
|
12773
|
+
return { order: t.order, x: x + t.pixelOffset[0], y: y + t.pixelOffset[1], w: 0.56 * t.fontSize * String(t.text).length, h: 0.7 * t.fontSize };
|
|
12774
|
+
});
|
|
12775
|
+
const shown = chartTextTransmittance(charts, boxes);
|
|
12776
|
+
texts.forEach((t, i) => {
|
|
12777
|
+
if (shown[i] < CHART_TEXT_MIN_TRANSMITTANCE) return;
|
|
12778
|
+
const c = t.color || [0, 0, 0];
|
|
12779
|
+
if (shown[i] < 1) t.color = [c[0], c[1], c[2], Math.round((c[3] != null ? c[3] : 255) * shown[i])];
|
|
12780
|
+
visible.add(t);
|
|
12781
|
+
});
|
|
12782
|
+
return visible;
|
|
12783
|
+
}
|
|
12784
|
+
|
|
11912
12785
|
// CHART|LABEL (maptheme.js 20647-21037), at the chart radius r of the
|
|
11913
12786
|
// size value:
|
|
11914
12787
|
// - LABEL: a rounded rectangle 2.05r wide and 1.6·f high (f = min(0.8r,
|
|
@@ -12985,7 +13858,7 @@
|
|
|
12985
13858
|
// test-only: lets test/unit/*.test.mjs call pure internals directly (the
|
|
12986
13859
|
// engine runs in a Node vm there); deliberately NOT on the ixmaps object
|
|
12987
13860
|
global.__ixmapsGlInternals = {
|
|
12988
|
-
IXMAPS_GL_VERSION, youtubeClickToPlay, symbolsFlagCompat, tooltipTable, legendRowLabels, scaleDenom, cssColorAlpha, aggregatedCategoricalClass, dominantDopacityAlpha, isAggregatedCategoricalChoropleth, computeBubblePackLayout, isolatedBubblePackLayout, sequenceLayout, ringsLayout, multiQuadOffsets, pixelOffsetLngLat,
|
|
13861
|
+
IXMAPS_GL_VERSION, youtubeClickToPlay, symbolsFlagCompat, tooltipTable, legendRowLabels, scaleDenom, cssColorAlpha, aggregatedCategoricalClass, dominantDopacityAlpha, isAggregatedCategoricalChoropleth, computeBubblePackLayout, isolatedBubblePackLayout, chartTextTransmittance, insideSymbolShape, sequenceLayout, ringsLayout, pieSliceLayout, pieValueLabelLayout, multiQuadOffsets, pixelOffsetLngLat,
|
|
12989
13862
|
normalizeTheme, projectThemeToDefinition, withoutProjectCode, groupRecordCount, resolveBasemapStyleUrl, resolveMapTypeColor, LayerBuilder, LayerRuntime, MapBuilder, typeStyleNumbers, styleNum,
|
|
12990
13863
|
resolveScriptUrl, isTrustedScriptUrl, loadProcessingScript, loadBrokerData, applyBrokerThemePatch, makeBrokerTheme,
|
|
12991
13864
|
equalIntervalBreaks, quantileBreaks, naturalBreaks, evenStrideSample, jenksBreakValues, partsFromBreakValues, resolvePartsClass,
|