ixmaps-gl 0.2.18 → 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 +809 -90
- 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');
|
|
@@ -7046,68 +7171,79 @@
|
|
|
7046
7171
|
}
|
|
7047
7172
|
|
|
7048
7173
|
// Chart texts covered by charts drawn after them. Flat draws each chart
|
|
7049
|
-
// as one SVG group (
|
|
7050
|
-
//
|
|
7051
|
-
//
|
|
7052
|
-
//
|
|
7053
|
-
//
|
|
7054
|
-
//
|
|
7055
|
-
//
|
|
7056
|
-
//
|
|
7057
|
-
// charts: in draw order, { x, y, parts: [{ dx, dy, r, shape, alpha }]
|
|
7058
|
-
//
|
|
7059
|
-
// glowR 0 for none (glowAlpha scales glowProfileAlpha)
|
|
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
|
|
7060
7184
|
// texts: { order (index into charts), x, y, w, h }
|
|
7061
7185
|
// Returns each text's transmittance, 0..1.
|
|
7062
|
-
const OCCLUSION_CELL_PX =
|
|
7186
|
+
const OCCLUSION_CELL_PX = 32;
|
|
7063
7187
|
// below this a text is as good as hidden: left out of its TextLayer
|
|
7064
7188
|
const CHART_TEXT_MIN_TRANSMITTANCE = 0.08;
|
|
7065
7189
|
function chartTextTransmittance(charts, texts) {
|
|
7066
|
-
|
|
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);
|
|
7067
7213
|
charts.forEach((c, k) => {
|
|
7068
|
-
|
|
7069
|
-
for (
|
|
7070
|
-
|
|
7071
|
-
|
|
7072
|
-
|
|
7073
|
-
|
|
7074
|
-
|
|
7075
|
-
|
|
7076
|
-
if (!cell) grid.set(key, cell = []);
|
|
7077
|
-
cell.push(k);
|
|
7078
|
-
}
|
|
7079
|
-
}
|
|
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;
|
|
7080
7222
|
});
|
|
7081
|
-
|
|
7223
|
+
// what of (x, y) still shows through chart c's parts
|
|
7224
|
+
const keepAt = (c, x, y) => {
|
|
7082
7225
|
let keep = 1;
|
|
7083
|
-
if (c.glowR) {
|
|
7084
|
-
const d = Math.hypot(x - c.x, y - c.y) / c.glowR;
|
|
7085
|
-
if (d < 1) keep *= 1 - c.glowAlpha * glowProfileAlpha(d);
|
|
7086
|
-
}
|
|
7087
7226
|
for (const p of c.parts) {
|
|
7088
7227
|
if (insideSymbolShape(p.shape, x - c.x - p.dx, y - c.y - p.dy, p.r)) keep *= 1 - p.alpha;
|
|
7089
7228
|
}
|
|
7090
7229
|
return keep;
|
|
7091
7230
|
};
|
|
7092
|
-
|
|
7093
|
-
const SAMPLES = [[0, 0], [-0.35, -0.3], [0.35, -0.3], [-0.35, 0.3], [0.35, 0.3]];
|
|
7094
|
-
return texts.map(t => {
|
|
7231
|
+
texts.forEach((t, i) => {
|
|
7095
7232
|
let sum = 0;
|
|
7096
|
-
for (
|
|
7097
|
-
const x =
|
|
7098
|
-
const cell =
|
|
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);
|
|
7099
7236
|
let keep = 1;
|
|
7100
|
-
//
|
|
7101
|
-
|
|
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
|
+
}
|
|
7102
7242
|
sum += keep;
|
|
7103
7243
|
}
|
|
7104
|
-
|
|
7244
|
+
out[i] = sum / nS;
|
|
7105
7245
|
});
|
|
7106
|
-
|
|
7107
|
-
// _getGlowIcon's radial gradient: 0.28 at the center, 0.09 at half the
|
|
7108
|
-
// radius, 0 at the edge (d = distance / radius)
|
|
7109
|
-
function glowProfileAlpha(d) {
|
|
7110
|
-
return d < 0.5 ? 0.28 - 0.19 * d / 0.5 : d < 1 ? 0.09 * (1 - (d - 0.5) / 0.5) : 0;
|
|
7246
|
+
return out;
|
|
7111
7247
|
}
|
|
7112
7248
|
// inside drawSymbolPath's shape of radius r, (dx, dy) from its center;
|
|
7113
7249
|
// the shapes without their own test as the circle
|
|
@@ -7133,6 +7269,193 @@
|
|
|
7133
7269
|
return [lngLat[0] + offset[0] * 360 / world, lat];
|
|
7134
7270
|
}
|
|
7135
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
|
+
|
|
7136
7459
|
// CHART|SYMBOL|SEQUENCE (flat maptheme.js 21184-21300, 21790-21925,
|
|
7137
7460
|
// 22296-22395): one symbol per category part of a chart, in category
|
|
7138
7461
|
// order or sorted (SORT: DOWN biggest first, UP smallest first), parts
|
|
@@ -8481,7 +8804,9 @@
|
|
|
8481
8804
|
this._gridRefLat = (minLat <= maxLat) ? (minLat + maxLat) / 2 : 0;
|
|
8482
8805
|
}
|
|
8483
8806
|
|
|
8484
|
-
if (this.
|
|
8807
|
+
if (this._isPieChart() && this.binding.value) {
|
|
8808
|
+
this._preparePie();
|
|
8809
|
+
} else if (this.flags.has('DOMINANT') && this.binding.value && !isAggregatedCategoricalChoropleth(this)) {
|
|
8485
8810
|
// .type("CHOROPLETH|DOMINANT") — a MULTI-field bound value
|
|
8486
8811
|
// (binding.value is a pipe-joined field list, e.g. one CSV column
|
|
8487
8812
|
// per age band for the same year/sex — same pipe convention
|
|
@@ -9737,6 +10062,9 @@
|
|
|
9737
10062
|
// USER charts (a page's own chart function, style.userdraw) go the
|
|
9738
10063
|
// chart pipeline too, drawn by _buildUserChartLayers (bubbles when the
|
|
9739
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);
|
|
9740
10068
|
if (isSymbolChart(this.flags)) return this._buildChartLayers(zoom, bbox, liveZoom, globeCenter);
|
|
9741
10069
|
console.warn(`[ixmaps-gl] layer "${this.name}": type "${[...this.flags].join('|')}" has no implemented renderer`);
|
|
9742
10070
|
return [];
|
|
@@ -10099,6 +10427,51 @@
|
|
|
10099
10427
|
_itemPlotFields() {
|
|
10100
10428
|
return String(this.binding.value || '').split('|');
|
|
10101
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
|
+
|
|
10102
10475
|
// x-axis labels of a per-item PLOT (style.xaxis / label, else the fields)
|
|
10103
10476
|
_itemPlotLabels() {
|
|
10104
10477
|
const fields = this._itemPlotFields();
|
|
@@ -11511,13 +11884,71 @@
|
|
|
11511
11884
|
// fillopacity and linecolor, as a single item (flat draws one
|
|
11512
11885
|
// symbol) — the packed multi-category icon is for CATEGORICAL cells
|
|
11513
11886
|
const singleSymbol = d => !d.properties.counts || (this._rangeClassed && d.properties.cat != null);
|
|
11514
|
-
|
|
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({
|
|
11515
11938
|
id: `ix-bubbles-${this.name}-g${this._iconGeneration}`,
|
|
11516
11939
|
data: combined, pickable: true,
|
|
11517
11940
|
getPosition: d => d.geometry.coordinates,
|
|
11518
|
-
getIcon: d =>
|
|
11519
|
-
|
|
11520
|
-
|
|
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
|
+
},
|
|
11521
11952
|
getSize: d => vRadius(sizeValueOf(d)) * 2,
|
|
11522
11953
|
// group icons bake a fixed 0.9 alpha; an explicit fillopacity scales it
|
|
11523
11954
|
// so a near-transparent theme doesn't show opaque 1-px-cell clusters
|
|
@@ -11633,10 +12064,12 @@
|
|
|
11633
12064
|
|
|
11634
12065
|
// a text under later charts fades by what still shows through them
|
|
11635
12066
|
// (chartTextTransmittance), as flat's per-chart SVG groups paint
|
|
11636
|
-
// each later symbol
|
|
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.)
|
|
11637
12071
|
const visible = this._occludeChartTexts(combined, pointLabels.concat(groupLabels), {
|
|
11638
|
-
liveZoom, globeCenter, alignOf, radiusOf,
|
|
11639
|
-
singleBorderWidthPx
|
|
12072
|
+
liveZoom, globeCenter, alignOf, radiusOf, singleSymbol, shapeOf, fillOpacity, singleBorderWidthPx
|
|
11640
12073
|
});
|
|
11641
12074
|
const keepVisible = labels => labels.filter(l => visible.has(l));
|
|
11642
12075
|
const shownPointLabels = keepVisible(pointLabels), shownGroupLabels = keepVisible(groupLabels);
|
|
@@ -11885,6 +12318,299 @@
|
|
|
11885
12318
|
return layers;
|
|
11886
12319
|
}
|
|
11887
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
|
+
|
|
11888
12614
|
// flat's chart position for this build (flatChartAlignOffset): a
|
|
11889
12615
|
// function item → pixel offset, the same for every part of one chart
|
|
11890
12616
|
// (symbol, glow, shadow, box, title, value text). unit is flat's
|
|
@@ -12002,28 +12728,8 @@
|
|
|
12002
12728
|
return { noBreaks: flatNoBreaks(vMin, vMax), maxValue: styleNum(this.style.normalsizevalue) || (isFinite(vMax) ? vMax : undefined) };
|
|
12003
12729
|
}
|
|
12004
12730
|
|
|
12005
|
-
//
|
|
12006
|
-
//
|
|
12007
|
-
// - LABEL: a rounded rectangle 2.05r wide and 1.6·f high (f = min(0.8r,
|
|
12008
|
-
// 3.4r / the value's text length)) from −r and −0.83f, corners 2r/15
|
|
12009
|
-
// (normalX(2r / normal radius)), in the item color at fillopacity
|
|
12010
|
-
// (default 1), stroked in linecolor or the color × 0.7 (NOLINES: none)
|
|
12011
|
-
// linewidth (0.1) · unit · √(r / normal radius) wide (OUTLINE:
|
|
12012
|
-
// unit · min(1, r / normal radius)), opaque with OUTLINE or a
|
|
12013
|
-
// linecolor, else 1 below fillopacity 0.5, 0.3 above; with VALUES the
|
|
12014
|
-
// text in bold, min(0.8r, 3.3r / its length) · valuescale, in
|
|
12015
|
-
// valuecolor / textcolor, else ChartColors.textColor of a class
|
|
12016
|
-
// color (white for a single-color theme). Flat's drop shadow rects
|
|
12017
|
-
// (20950) never draw: they need fOrigShadow without fShadow, which
|
|
12018
|
-
// the shadow gate (16740-16745) leaves equal.
|
|
12019
|
-
// - LABEL|TEXTONLY: no box, only an invisible hit rect (the
|
|
12020
|
-
// hover/tooltip target) and, with VALUES, the text at r·0.8·valuescale
|
|
12021
|
-
// in normal weight, valuecolor/textcolor (else black), over a halo in
|
|
12022
|
-
// the item color (stroke font/7, opacity 0.5).
|
|
12023
|
-
// Both moved by the chart position (flatChartAlignOffset). The text is
|
|
12024
|
-
// the hover target of TEXTONLY, the box of LABEL.
|
|
12025
|
-
// The charts of _buildChartLayers' bubbles layer (and its GLOW / AURA)
|
|
12026
|
-
// as chartTextTransmittance's occluders, in draw order and screen
|
|
12731
|
+
// The charts of _buildChartLayers' bubbles layer as
|
|
12732
|
+
// chartTextTransmittance's occluders, in draw order and screen
|
|
12027
12733
|
// pixels, and each text's resulting alpha: the text's color gets it,
|
|
12028
12734
|
// the returned Set holds the texts still visible. Screen pixels:
|
|
12029
12735
|
// Web Mercator world pixels at the live zoom; under globe a local
|
|
@@ -12040,8 +12746,6 @@
|
|
|
12040
12746
|
} else {
|
|
12041
12747
|
screenOf = c => { const p = lngLatToWorldPixel(c[0], c[1], o.liveZoom); return [p.x, p.y]; };
|
|
12042
12748
|
}
|
|
12043
|
-
const glowOn = this.flags.has('GLOW') && !glowHiddenByScale(this.style, o.liveZoom);
|
|
12044
|
-
const auraOn = !glowOn && this.flags.has('AURA') && !this.flags.has('ZOOM');
|
|
12045
12749
|
const symbols = Array.isArray(this.style.symbols) ? this.style.symbols : null;
|
|
12046
12750
|
const charts = combined.map(d => {
|
|
12047
12751
|
const [x, y] = screenOf(d.geometry.coordinates);
|
|
@@ -12061,12 +12765,7 @@
|
|
|
12061
12765
|
parts = present.map((p, i) => ({ dx: offsets[i].x * px, dy: offsets[i].y * px, r: radii[i] * px,
|
|
12062
12766
|
shape: symbols ? normalizeSymbolShape(symbols[Math.min(p.i, symbols.length - 1)]) : 'circle', alpha }));
|
|
12063
12767
|
}
|
|
12064
|
-
|
|
12065
|
-
return {
|
|
12066
|
-
x: x + a[0], y: y + a[1], parts,
|
|
12067
|
-
glowR: glowOn ? o.vRadius(o.sizeValueOf(d)) * (d.properties.counts ? 9 : 11) / 2 : 0,
|
|
12068
|
-
glowAlpha: iconAlpha
|
|
12069
|
-
};
|
|
12768
|
+
return { x: x + a[0], y: y + a[1], parts };
|
|
12070
12769
|
});
|
|
12071
12770
|
// the text box: Arial digits are ~0.56 em wide, ~0.7 em high
|
|
12072
12771
|
const boxes = texts.map(t => {
|
|
@@ -12083,6 +12782,26 @@
|
|
|
12083
12782
|
return visible;
|
|
12084
12783
|
}
|
|
12085
12784
|
|
|
12785
|
+
// CHART|LABEL (maptheme.js 20647-21037), at the chart radius r of the
|
|
12786
|
+
// size value:
|
|
12787
|
+
// - LABEL: a rounded rectangle 2.05r wide and 1.6·f high (f = min(0.8r,
|
|
12788
|
+
// 3.4r / the value's text length)) from −r and −0.83f, corners 2r/15
|
|
12789
|
+
// (normalX(2r / normal radius)), in the item color at fillopacity
|
|
12790
|
+
// (default 1), stroked in linecolor or the color × 0.7 (NOLINES: none)
|
|
12791
|
+
// linewidth (0.1) · unit · √(r / normal radius) wide (OUTLINE:
|
|
12792
|
+
// unit · min(1, r / normal radius)), opaque with OUTLINE or a
|
|
12793
|
+
// linecolor, else 1 below fillopacity 0.5, 0.3 above; with VALUES the
|
|
12794
|
+
// text in bold, min(0.8r, 3.3r / its length) · valuescale, in
|
|
12795
|
+
// valuecolor / textcolor, else ChartColors.textColor of a class
|
|
12796
|
+
// color (white for a single-color theme). Flat's drop shadow rects
|
|
12797
|
+
// (20950) never draw: they need fOrigShadow without fShadow, which
|
|
12798
|
+
// the shadow gate (16740-16745) leaves equal.
|
|
12799
|
+
// - LABEL|TEXTONLY: no box, only an invisible hit rect (the
|
|
12800
|
+
// hover/tooltip target) and, with VALUES, the text at r·0.8·valuescale
|
|
12801
|
+
// in normal weight, valuecolor/textcolor (else black), over a halo in
|
|
12802
|
+
// the item color (stroke font/7, opacity 0.5).
|
|
12803
|
+
// Both moved by the chart position (flatChartAlignOffset). The text is
|
|
12804
|
+
// the hover target of TEXTONLY, the box of LABEL.
|
|
12086
12805
|
_buildLabelChartLayers(combined, zoom, liveZoom, sizeValueOf) {
|
|
12087
12806
|
const textOnly = this.flags.has('TEXTONLY');
|
|
12088
12807
|
const showValues = this.flags.has('VALUES') && !valuesHiddenByScale(this.style, zoom);
|
|
@@ -13139,7 +13858,7 @@
|
|
|
13139
13858
|
// test-only: lets test/unit/*.test.mjs call pure internals directly (the
|
|
13140
13859
|
// engine runs in a Node vm there); deliberately NOT on the ixmaps object
|
|
13141
13860
|
global.__ixmapsGlInternals = {
|
|
13142
|
-
IXMAPS_GL_VERSION, youtubeClickToPlay, symbolsFlagCompat, tooltipTable, legendRowLabels, scaleDenom, cssColorAlpha, aggregatedCategoricalClass, dominantDopacityAlpha, isAggregatedCategoricalChoropleth, computeBubblePackLayout, isolatedBubblePackLayout, chartTextTransmittance,
|
|
13861
|
+
IXMAPS_GL_VERSION, youtubeClickToPlay, symbolsFlagCompat, tooltipTable, legendRowLabels, scaleDenom, cssColorAlpha, aggregatedCategoricalClass, dominantDopacityAlpha, isAggregatedCategoricalChoropleth, computeBubblePackLayout, isolatedBubblePackLayout, chartTextTransmittance, insideSymbolShape, sequenceLayout, ringsLayout, pieSliceLayout, pieValueLabelLayout, multiQuadOffsets, pixelOffsetLngLat,
|
|
13143
13862
|
normalizeTheme, projectThemeToDefinition, withoutProjectCode, groupRecordCount, resolveBasemapStyleUrl, resolveMapTypeColor, LayerBuilder, LayerRuntime, MapBuilder, typeStyleNumbers, styleNum,
|
|
13144
13863
|
resolveScriptUrl, isTrustedScriptUrl, loadProcessingScript, loadBrokerData, applyBrokerThemePatch, makeBrokerTheme,
|
|
13145
13864
|
equalIntervalBreaks, quantileBreaks, naturalBreaks, evenStrideSample, jenksBreakValues, partsFromBreakValues, resolvePartsClass,
|