canvas-globe 1.2.0 → 1.3.0
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/CHANGELOG.md +20 -0
- package/README.md +41 -5
- package/codemeta.json +1 -1
- package/custom-elements.json +2 -2
- package/dist/canvas-globe.umd.js +161 -4
- package/package.json +1 -1
- package/src/geo-globe.js +91 -4
- package/src/geo.js +70 -0
- package/src/version.js +1 -1
- package/types/element.d.ts +3 -3
- package/types/index.d.ts +46 -7
- package/types/jsr-element.d.ts +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,26 @@ All notable changes to this package are documented here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.3.0] - 2026-09-19
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Added an interactive screen-space hex-bin density layer for globe and map
|
|
14
|
+
modes, including weighted aggregation, colour ramps, source-marker hit
|
|
15
|
+
payloads, framework event types, and 5,000-marker performance coverage.
|
|
16
|
+
- Added an Angular SSR and hydration starter with prerendered HTML, a stable
|
|
17
|
+
canvas shell, client hydration, and project-creator support.
|
|
18
|
+
- Added a reproducible nine-library globe comparison harness for timing,
|
|
19
|
+
bundle size, package metadata, server-import, and accessibility-surface
|
|
20
|
+
measurements.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- Prevented the Angular component from creating a CanvasGlobe instance during
|
|
25
|
+
server rendering.
|
|
26
|
+
- Corrected public TypeScript declarations for story-step overrides and theme
|
|
27
|
+
options.
|
|
28
|
+
|
|
9
29
|
## [1.2.0] - 2026-09-18
|
|
10
30
|
|
|
11
31
|
### Added
|
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
|
|
36
36
|
[](https://stackblitz.com/github/Shree-hari/canvas-globe?startScript=start)
|
|
37
37
|
|
|
38
|
-
**Start with your stack:** [Vanilla JS](starters/vanilla-vite) | [React + Vite](starters/react-vite) | [Next.js](starters/nextjs-app-router) | [Vue](starters/vue-vite) | [SvelteKit](starters/sveltekit) | [Web Component](starters/web-component-vite)
|
|
38
|
+
**Start with your stack:** [Vanilla JS](starters/vanilla-vite) | [React + Vite](starters/react-vite) | [Next.js](starters/nextjs-app-router) | [Nuxt](starters/nuxt-ssr) | [Vue](starters/vue-vite) | [SvelteKit](starters/sveltekit) | [Angular SSR](starters/angular-ssr) | [Web Component](starters/web-component-vite)
|
|
39
39
|
|
|
40
40
|
**Install with shadcn:** `npx shadcn@latest add https://canvasglobe.swiftools.com/r/canvas-globe.json`
|
|
41
41
|
|
|
@@ -139,19 +139,19 @@ your existing application rather than installing a second React copy.
|
|
|
139
139
|
For a plain `<script>` installation, use the versioned UMD build:
|
|
140
140
|
|
|
141
141
|
```html
|
|
142
|
-
<script src="https://cdn.jsdelivr.net/npm/canvas-globe@1.
|
|
142
|
+
<script src="https://cdn.jsdelivr.net/npm/canvas-globe@1.3.0/dist/canvas-globe.umd.js"></script>
|
|
143
143
|
```
|
|
144
144
|
|
|
145
145
|
The same npm release is also available from UNPKG:
|
|
146
146
|
|
|
147
147
|
```html
|
|
148
|
-
<script src="https://unpkg.com/canvas-globe@1.
|
|
148
|
+
<script src="https://unpkg.com/canvas-globe@1.3.0/dist/canvas-globe.umd.js"></script>
|
|
149
149
|
```
|
|
150
150
|
|
|
151
151
|
Modern browsers can import the package through an ESM CDN:
|
|
152
152
|
|
|
153
153
|
```js
|
|
154
|
-
import { createGlobe } from "https://esm.sh/canvas-globe@1.
|
|
154
|
+
import { createGlobe } from "https://esm.sh/canvas-globe@1.3.0";
|
|
155
155
|
```
|
|
156
156
|
|
|
157
157
|
Pin an exact version in production so a future release cannot change a deployed page unexpectedly.
|
|
@@ -171,7 +171,7 @@ import { createGlobe } from "canvas-globe";
|
|
|
171
171
|
Or drop the UMD build on a page with no build step at all:
|
|
172
172
|
|
|
173
173
|
```html
|
|
174
|
-
<script src="https://cdn.jsdelivr.net/npm/canvas-globe@1.
|
|
174
|
+
<script src="https://cdn.jsdelivr.net/npm/canvas-globe@1.3.0/dist/canvas-globe.umd.js"></script>
|
|
175
175
|
<canvas id="globe" style="width:520px;aspect-ratio:1"></canvas>
|
|
176
176
|
<script>
|
|
177
177
|
CanvasGlobe.createGlobe(document.getElementById("globe"), {
|
|
@@ -381,6 +381,7 @@ included effects. See `types/fx.d.ts` for every option and callback signature.
|
|
|
381
381
|
| `timeline` | Not set | `{ at }`: hides markers whose `date` has not arrived |
|
|
382
382
|
| `transparentBackground` | `false` | Skip the ocean fill so exports keep an alpha channel |
|
|
383
383
|
| `heatmap` | `false` | Additive density blobs: `{ radius, intensity, color }` |
|
|
384
|
+
| `hexBins` | `false` | Interactive screen-space density cells; see [Hex bins](#hex-bins) |
|
|
384
385
|
| `spikes` | `false` | Bars off the surface, sized by `count`: `{ height, width }` |
|
|
385
386
|
| `labels` | `false` | `"markers"` \| `"countries"` \| `"both"`, with collision avoidance |
|
|
386
387
|
| `legend` | Not set | `{ title, items }` or `{ title, scale, position }` |
|
|
@@ -443,6 +444,41 @@ With `cluster: true`, dense areas collapse into a single bubble and your callbac
|
|
|
443
444
|
`{ cluster: true, count, markers, lat, lon }` instead. Clustering happens in screen space, so it
|
|
444
445
|
re-balances automatically as you zoom.
|
|
445
446
|
|
|
447
|
+
## Hex bins
|
|
448
|
+
|
|
449
|
+
Use hex bins when individual markers are too dense to read. CanvasGlobe aggregates the visible,
|
|
450
|
+
projected markers into a pointy-top hexagonal grid, so the density view updates naturally as the
|
|
451
|
+
globe rotates, the map pans, or the user zooms.
|
|
452
|
+
|
|
453
|
+
```js
|
|
454
|
+
const globe = createGlobe(canvas, {
|
|
455
|
+
markers: demandPoints,
|
|
456
|
+
hexBins: {
|
|
457
|
+
radius: 19,
|
|
458
|
+
value: "sum",
|
|
459
|
+
colorRange: ["#dbeafe", "#2563eb", "#172554"],
|
|
460
|
+
padding: 1.5,
|
|
461
|
+
showCount: true,
|
|
462
|
+
},
|
|
463
|
+
tooltip: (target, kind) => kind === "hex-bin"
|
|
464
|
+
? `${target.markerCount} locations, ${target.value} total requests`
|
|
465
|
+
: target.name,
|
|
466
|
+
onClick: (target) => {
|
|
467
|
+
if (target.hexBin) console.log(target.markers);
|
|
468
|
+
},
|
|
469
|
+
});
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
Each interactive bin returned to `tooltip`, `onHover`, and `onClick` contains
|
|
473
|
+
`{ hexBin: true, markerCount, count, value, markers, lat, lon }`. The default `value: "sum"` adds
|
|
474
|
+
each marker's `count`; use `value: "count"` to colour by the number of markers. Individual markers
|
|
475
|
+
are hidden by default while the layer is active. Set `hideMarkers: false` to keep them visible.
|
|
476
|
+
|
|
477
|
+
Available options are `radius`, `minValue`, `value`, `color`, `colorRange`, `opacity`, `stroke`,
|
|
478
|
+
`strokeWidth`, `padding`, `showCount`, `labelColor`, and `hideMarkers`. The same configuration works
|
|
479
|
+
in globe and flat-map modes. Since binning happens after projection, `radius` is measured in screen
|
|
480
|
+
pixels rather than geographic degrees.
|
|
481
|
+
|
|
446
482
|
### How accurate is marker placement?
|
|
447
483
|
|
|
448
484
|
The projection maths is exact: a marker's pixel position matches the closed-form projection to
|
package/codemeta.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"identifier": "canvas-globe",
|
|
6
6
|
"description": "CanvasGlobe helps developers add interactive 3D globes and flat world maps to JavaScript and React apps with Canvas 2D, without WebGL, map tiles, API keys, or runtime network calls.",
|
|
7
7
|
"url": "https://canvasglobe.swiftools.com/",
|
|
8
|
-
"version": "1.
|
|
8
|
+
"version": "1.3.0",
|
|
9
9
|
"codeRepository": "https://github.com/Shree-hari/canvas-globe",
|
|
10
10
|
"issueTracker": "https://github.com/Shree-hari/canvas-globe/issues",
|
|
11
11
|
"downloadUrl": "https://www.npmjs.com/package/canvas-globe",
|
package/custom-elements.json
CHANGED
|
@@ -112,8 +112,8 @@
|
|
|
112
112
|
{ "name": "lat-range", "description": "Serialized latitude range.", "type": { "text": "[number, number]" } }
|
|
113
113
|
],
|
|
114
114
|
"events": [
|
|
115
|
-
{ "name": "geo-hover", "description": "Fires when the hovered marker changes.", "type": { "text": "CustomEvent<{ marker: Marker | ClusterMarker | null; pos: { x: number; y: number } | null }>" } },
|
|
116
|
-
{ "name": "geo-click", "description": "Fires when a marker is clicked.", "type": { "text": "CustomEvent<{ marker: Marker | ClusterMarker; pos: { x: number; y: number } }>" } },
|
|
115
|
+
{ "name": "geo-hover", "description": "Fires when the hovered marker changes.", "type": { "text": "CustomEvent<{ marker: Marker | ClusterMarker | HexBinMarker | null; pos: { x: number; y: number } | null }>" } },
|
|
116
|
+
{ "name": "geo-click", "description": "Fires when a marker is clicked.", "type": { "text": "CustomEvent<{ marker: Marker | ClusterMarker | HexBinMarker; pos: { x: number; y: number } }>" } },
|
|
117
117
|
{ "name": "geo-country-hover", "description": "Fires when the hovered country changes.", "type": { "text": "CustomEvent<{ country: CountryShape | null; pos: { x: number; y: number } | null }>" } },
|
|
118
118
|
{ "name": "geo-country-click", "description": "Fires when a country is clicked.", "type": { "text": "CustomEvent<{ country: CountryShape; pos: { x: number; y: number } }>" } },
|
|
119
119
|
{ "name": "geo-render", "description": "Fires after CanvasGlobe renders a frame.", "type": { "text": "CustomEvent<{ globe: GeoGlobe }>" } }
|
package/dist/canvas-globe.umd.js
CHANGED
|
@@ -640,6 +640,76 @@ const withAlpha = (color, a) => {
|
|
|
640
640
|
return color;
|
|
641
641
|
};
|
|
642
642
|
|
|
643
|
+
/**
|
|
644
|
+
* Aggregates projected points into a pointy-top hexagonal grid.
|
|
645
|
+
*
|
|
646
|
+
* The input is deliberately renderer-shaped (`{ x, y, depth, m }`) so the
|
|
647
|
+
* same helper works after either globe or flat-map projection. Each returned
|
|
648
|
+
* bin preserves its source markers for tooltips, clicks, and custom details.
|
|
649
|
+
*/
|
|
650
|
+
const hexBinPoints = (points = [], radius = 18) => {
|
|
651
|
+
const size = Math.max(1, Number(radius) || 18);
|
|
652
|
+
const sqrt3 = Math.sqrt(3);
|
|
653
|
+
const cells = new Map();
|
|
654
|
+
|
|
655
|
+
const roundAxial = (q, r) => {
|
|
656
|
+
let x = q;
|
|
657
|
+
let z = r;
|
|
658
|
+
let y = -x - z;
|
|
659
|
+
let rx = Math.round(x);
|
|
660
|
+
let ry = Math.round(y);
|
|
661
|
+
let rz = Math.round(z);
|
|
662
|
+
const dx = Math.abs(rx - x);
|
|
663
|
+
const dy = Math.abs(ry - y);
|
|
664
|
+
const dz = Math.abs(rz - z);
|
|
665
|
+
if (dx > dy && dx > dz) rx = -ry - rz;
|
|
666
|
+
else if (dy > dz) ry = -rx - rz;
|
|
667
|
+
else rz = -rx - ry;
|
|
668
|
+
return [rx, rz];
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
for (const point of points) {
|
|
672
|
+
if (!Number.isFinite(point?.x) || !Number.isFinite(point?.y)) continue;
|
|
673
|
+
const q = (sqrt3 / 3 * point.x - point.y / 3) / size;
|
|
674
|
+
const r = (2 * point.y / 3) / size;
|
|
675
|
+
const [hq, hr] = roundAxial(q, r);
|
|
676
|
+
const key = `${hq}:${hr}`;
|
|
677
|
+
let cell = cells.get(key);
|
|
678
|
+
if (!cell) {
|
|
679
|
+
cell = {
|
|
680
|
+
q: hq,
|
|
681
|
+
r: hr,
|
|
682
|
+
x: size * sqrt3 * (hq + hr / 2),
|
|
683
|
+
y: size * 1.5 * hr,
|
|
684
|
+
count: 0,
|
|
685
|
+
value: 0,
|
|
686
|
+
depth: 0,
|
|
687
|
+
lon: 0,
|
|
688
|
+
lat: 0,
|
|
689
|
+
weight: 0,
|
|
690
|
+
markers: [],
|
|
691
|
+
};
|
|
692
|
+
cells.set(key, cell);
|
|
693
|
+
}
|
|
694
|
+
const marker = point.m || {};
|
|
695
|
+
const value = Number(marker.count);
|
|
696
|
+
const weight = Number.isFinite(value) && value > 0 ? value : 1;
|
|
697
|
+
cell.count += 1;
|
|
698
|
+
cell.value += weight;
|
|
699
|
+
cell.depth = Math.max(cell.depth, Number(point.depth) || 0);
|
|
700
|
+
cell.lon += (Number(marker.lon) || 0) * weight;
|
|
701
|
+
cell.lat += (Number(marker.lat) || 0) * weight;
|
|
702
|
+
cell.weight += weight;
|
|
703
|
+
cell.markers.push(marker);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
return [...cells.values()].map((cell) => ({
|
|
707
|
+
...cell,
|
|
708
|
+
lon: cell.weight ? cell.lon / cell.weight : 0,
|
|
709
|
+
lat: cell.weight ? cell.lat / cell.weight : 0,
|
|
710
|
+
}));
|
|
711
|
+
};
|
|
712
|
+
|
|
643
713
|
const parseRGB = (color) => {
|
|
644
714
|
if (color.startsWith("#")) {
|
|
645
715
|
const hex = color.length === 4 ? color.replace(/#(.)(.)(.)/, "#$1$1$2$2$3$3") : color;
|
|
@@ -1314,7 +1384,7 @@ function drawFitted(ctx, media, box) {
|
|
|
1314
1384
|
}
|
|
1315
1385
|
|
|
1316
1386
|
// Keep in sync with package.json. Release checks enforce this value.
|
|
1317
|
-
const CANVAS_GLOBE_VERSION = "1.
|
|
1387
|
+
const CANVAS_GLOBE_VERSION = "1.3.0";
|
|
1318
1388
|
|
|
1319
1389
|
/** Local license-key checks and production-use presentation helpers. */
|
|
1320
1390
|
|
|
@@ -1477,6 +1547,7 @@ const DEFAULTS = {
|
|
|
1477
1547
|
timeline: null,
|
|
1478
1548
|
transparentBackground: false,
|
|
1479
1549
|
heatmap: false,
|
|
1550
|
+
hexBins: false,
|
|
1480
1551
|
spikes: false,
|
|
1481
1552
|
labels: false,
|
|
1482
1553
|
legend: null,
|
|
@@ -1541,6 +1612,7 @@ const phaseOf = (fx, ms) => {
|
|
|
1541
1612
|
const defaultTooltip = (target, kind) => {
|
|
1542
1613
|
if (kind === "country") return target.name || String(target.id ?? "");
|
|
1543
1614
|
if (kind === "cluster") return `${target.count} in this area`;
|
|
1615
|
+
if (kind === "hex-bin") return `${target.markerCount} markers, value ${target.value}`;
|
|
1544
1616
|
const name = target.city || target.name || target.label;
|
|
1545
1617
|
const count = target.count != null ? `: ${target.count}` : "";
|
|
1546
1618
|
return name ? `${name}${count}` : `${target.lat.toFixed(2)}, ${target.lon.toFixed(2)}${count}`;
|
|
@@ -2836,7 +2908,7 @@ class GeoGlobe {
|
|
|
2836
2908
|
this._cursor();
|
|
2837
2909
|
this._dirty = true;
|
|
2838
2910
|
const target = marker || this._hoveredCountry;
|
|
2839
|
-
this._showTip(marker ? (marker.cluster ? "cluster" : "marker") : this._hoveredCountry ? "country" : null, target);
|
|
2911
|
+
this._showTip(marker ? (marker.hexBin ? "hex-bin" : marker.cluster ? "cluster" : "marker") : this._hoveredCountry ? "country" : null, target);
|
|
2840
2912
|
} else if (this._tipVisible) {
|
|
2841
2913
|
this._placeTip();
|
|
2842
2914
|
}
|
|
@@ -4024,6 +4096,87 @@ class GeoGlobe {
|
|
|
4024
4096
|
ctx.restore();
|
|
4025
4097
|
}
|
|
4026
4098
|
|
|
4099
|
+
/** Aggregate projected markers into an interactive hexagonal density layer. */
|
|
4100
|
+
_paintHexBins(pts, t, cx, cy, globeRadius, w, h) {
|
|
4101
|
+
const o = this.o.hexBins === true ? {} : this.o.hexBins;
|
|
4102
|
+
const { ctx } = this;
|
|
4103
|
+
const radius = Math.max(4, o.radius ?? 18);
|
|
4104
|
+
const padding = clamp(o.padding ?? 1.5, 0, radius * 0.45);
|
|
4105
|
+
const drawRadius = radius - padding;
|
|
4106
|
+
const minValue = Math.max(0, o.minValue ?? 1);
|
|
4107
|
+
const metric = o.value === "count" ? "count" : "value";
|
|
4108
|
+
const bins = hexBinPoints(pts, radius).filter((bin) => {
|
|
4109
|
+
if (bin[metric] < minValue) return false;
|
|
4110
|
+
if (this.o.mode === "globe") return Math.hypot(bin.x - cx, bin.y - cy) <= globeRadius + drawRadius;
|
|
4111
|
+
return bin.x >= -drawRadius && bin.x <= w + drawRadius && bin.y >= -drawRadius && bin.y <= h + drawRadius;
|
|
4112
|
+
});
|
|
4113
|
+
const max = Math.max(1, ...bins.map((bin) => bin[metric]));
|
|
4114
|
+
const range = Array.isArray(o.colorRange) && o.colorRange.length > 1
|
|
4115
|
+
? o.colorRange
|
|
4116
|
+
: [t.ocean[0], o.color || t.marker];
|
|
4117
|
+
const domain = range.map((_, index) => max * index / (range.length - 1));
|
|
4118
|
+
const scale = colorScale(domain, range);
|
|
4119
|
+
const hits = [];
|
|
4120
|
+
|
|
4121
|
+
ctx.save();
|
|
4122
|
+
if (this.o.mode === "globe") {
|
|
4123
|
+
ctx.beginPath();
|
|
4124
|
+
ctx.arc(cx, cy, globeRadius, 0, TAU);
|
|
4125
|
+
ctx.clip();
|
|
4126
|
+
} else {
|
|
4127
|
+
ctx.beginPath();
|
|
4128
|
+
ctx.rect(0, 0, w, h);
|
|
4129
|
+
ctx.clip();
|
|
4130
|
+
}
|
|
4131
|
+
|
|
4132
|
+
ctx.lineJoin = "round";
|
|
4133
|
+
for (const bin of bins) {
|
|
4134
|
+
const value = bin[metric];
|
|
4135
|
+
ctx.beginPath();
|
|
4136
|
+
for (let i = 0; i < 6; i++) {
|
|
4137
|
+
const angle = (60 * i - 30) * D2R;
|
|
4138
|
+
const x = bin.x + drawRadius * Math.cos(angle);
|
|
4139
|
+
const y = bin.y + drawRadius * Math.sin(angle);
|
|
4140
|
+
i ? ctx.lineTo(x, y) : ctx.moveTo(x, y);
|
|
4141
|
+
}
|
|
4142
|
+
ctx.closePath();
|
|
4143
|
+
ctx.globalAlpha = clamp(o.opacity ?? 0.82, 0, 1);
|
|
4144
|
+
ctx.fillStyle = scale(value) || o.color || t.marker;
|
|
4145
|
+
ctx.fill();
|
|
4146
|
+
if ((o.strokeWidth ?? 0.8) > 0) {
|
|
4147
|
+
ctx.globalAlpha = 1;
|
|
4148
|
+
ctx.strokeStyle = o.stroke || withAlpha(t.label, 0.3);
|
|
4149
|
+
ctx.lineWidth = o.strokeWidth ?? 0.8;
|
|
4150
|
+
ctx.stroke();
|
|
4151
|
+
}
|
|
4152
|
+
if (o.showCount && drawRadius >= 10) {
|
|
4153
|
+
ctx.globalAlpha = 1;
|
|
4154
|
+
ctx.fillStyle = o.labelColor || t.label;
|
|
4155
|
+
ctx.font = `600 ${Math.max(9, Math.min(13, drawRadius * 0.7))}px Inter,system-ui,sans-serif`;
|
|
4156
|
+
ctx.textAlign = "center";
|
|
4157
|
+
ctx.textBaseline = "middle";
|
|
4158
|
+
ctx.fillText(String(value), bin.x, bin.y);
|
|
4159
|
+
}
|
|
4160
|
+
hits.push({
|
|
4161
|
+
marker: {
|
|
4162
|
+
hexBin: true,
|
|
4163
|
+
count: bin.count,
|
|
4164
|
+
value: bin.value,
|
|
4165
|
+
markerCount: bin.count,
|
|
4166
|
+
markers: bin.markers,
|
|
4167
|
+
lon: bin.lon,
|
|
4168
|
+
lat: bin.lat,
|
|
4169
|
+
},
|
|
4170
|
+
x: bin.x,
|
|
4171
|
+
y: bin.y,
|
|
4172
|
+
r: drawRadius,
|
|
4173
|
+
});
|
|
4174
|
+
}
|
|
4175
|
+
ctx.restore();
|
|
4176
|
+
this._lastHexBins = bins;
|
|
4177
|
+
return hits;
|
|
4178
|
+
}
|
|
4179
|
+
|
|
4027
4180
|
/** Spike height for a marker, as a fraction of the globe radius. */
|
|
4028
4181
|
_spikeLift(m) {
|
|
4029
4182
|
if (!this.o.spikes) return 0;
|
|
@@ -4404,9 +4557,11 @@ class GeoGlobe {
|
|
|
4404
4557
|
pts.push({ m, x: cx + x, y: cy + y, depth: 0.65 + c * 0.35 });
|
|
4405
4558
|
}
|
|
4406
4559
|
if (this.o.heatmap) this._paintHeatmap(pts, t);
|
|
4560
|
+
const binHits = this.o.hexBins ? this._paintHexBins(pts, t, cx, cy, r, w, h) : [];
|
|
4407
4561
|
if (this.o.spikes) this._paintSpikes(t, cx, cy, r, w, h, null);
|
|
4408
4562
|
this._paintViewerAccuracy(t, cx, cy, r, null);
|
|
4409
|
-
const
|
|
4563
|
+
const showMarkers = !this.o.hexBins || (this.o.hexBins !== true && this.o.hexBins.hideMarkers === false);
|
|
4564
|
+
const hits = [...binHits, ...(showMarkers ? this._paintMarkers(pts, t) : [])];
|
|
4410
4565
|
if (this.o.labels) this._paintLabels(pts, t, cx, cy, r, w, h);
|
|
4411
4566
|
this._paintAnnotations(t);
|
|
4412
4567
|
this._paintPings(t);
|
|
@@ -4481,9 +4636,11 @@ class GeoGlobe {
|
|
|
4481
4636
|
pts.push({ m, x, y, depth: 1 });
|
|
4482
4637
|
}
|
|
4483
4638
|
if (this.o.heatmap) this._paintHeatmap(pts, t);
|
|
4639
|
+
const binHits = this.o.hexBins ? this._paintHexBins(pts, t, 0, 0, 0, w, h) : [];
|
|
4484
4640
|
if (this.o.spikes) this._paintSpikes(t, 0, 0, 0, w, h, fwd);
|
|
4485
4641
|
this._paintViewerAccuracy(t, 0, 0, 0, fwd);
|
|
4486
|
-
const
|
|
4642
|
+
const showMarkers = !this.o.hexBins || (this.o.hexBins !== true && this.o.hexBins.hideMarkers === false);
|
|
4643
|
+
const hits = [...binHits, ...(showMarkers ? this._paintMarkers(pts, t) : [])];
|
|
4487
4644
|
if (this.o.labels) this._paintLabels(pts, t, 0, 0, 0, w, h);
|
|
4488
4645
|
this._paintAnnotations(t);
|
|
4489
4646
|
this._paintPings(t);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "canvas-globe",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Build interactive 3D globes and flat world maps in JavaScript, React, Vue, Angular, or Svelte with Canvas 2D and no WebGL.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"author": {
|
package/src/geo-globe.js
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
getLicensePresentation,
|
|
16
16
|
reportLicenseStatus,
|
|
17
17
|
} from "./license.js";
|
|
18
|
-
import { D2R, R2D, TAU, clamp, wrapLon, resolveProjection, projectionBounds, ortho, orthoInverse, greatCircle, circleAround, distanceMeters, subsolarPoint, pointInGeometry, geometryBounds, normalizeShapes, withAlpha } from "./geo.js";
|
|
18
|
+
import { D2R, R2D, TAU, clamp, wrapLon, resolveProjection, projectionBounds, ortho, orthoInverse, greatCircle, circleAround, distanceMeters, subsolarPoint, pointInGeometry, geometryBounds, normalizeShapes, withAlpha, colorScale, hexBinPoints } from "./geo.js";
|
|
19
19
|
|
|
20
20
|
const DEFAULTS = {
|
|
21
21
|
licenseKey: null,
|
|
@@ -40,6 +40,7 @@ const DEFAULTS = {
|
|
|
40
40
|
timeline: null,
|
|
41
41
|
transparentBackground: false,
|
|
42
42
|
heatmap: false,
|
|
43
|
+
hexBins: false,
|
|
43
44
|
spikes: false,
|
|
44
45
|
labels: false,
|
|
45
46
|
legend: null,
|
|
@@ -104,6 +105,7 @@ const phaseOf = (fx, ms) => {
|
|
|
104
105
|
const defaultTooltip = (target, kind) => {
|
|
105
106
|
if (kind === "country") return target.name || String(target.id ?? "");
|
|
106
107
|
if (kind === "cluster") return `${target.count} in this area`;
|
|
108
|
+
if (kind === "hex-bin") return `${target.markerCount} markers, value ${target.value}`;
|
|
107
109
|
const name = target.city || target.name || target.label;
|
|
108
110
|
const count = target.count != null ? `: ${target.count}` : "";
|
|
109
111
|
return name ? `${name}${count}` : `${target.lat.toFixed(2)}, ${target.lon.toFixed(2)}${count}`;
|
|
@@ -1399,7 +1401,7 @@ export class GeoGlobe {
|
|
|
1399
1401
|
this._cursor();
|
|
1400
1402
|
this._dirty = true;
|
|
1401
1403
|
const target = marker || this._hoveredCountry;
|
|
1402
|
-
this._showTip(marker ? (marker.cluster ? "cluster" : "marker") : this._hoveredCountry ? "country" : null, target);
|
|
1404
|
+
this._showTip(marker ? (marker.hexBin ? "hex-bin" : marker.cluster ? "cluster" : "marker") : this._hoveredCountry ? "country" : null, target);
|
|
1403
1405
|
} else if (this._tipVisible) {
|
|
1404
1406
|
this._placeTip();
|
|
1405
1407
|
}
|
|
@@ -2587,6 +2589,87 @@ export class GeoGlobe {
|
|
|
2587
2589
|
ctx.restore();
|
|
2588
2590
|
}
|
|
2589
2591
|
|
|
2592
|
+
/** Aggregate projected markers into an interactive hexagonal density layer. */
|
|
2593
|
+
_paintHexBins(pts, t, cx, cy, globeRadius, w, h) {
|
|
2594
|
+
const o = this.o.hexBins === true ? {} : this.o.hexBins;
|
|
2595
|
+
const { ctx } = this;
|
|
2596
|
+
const radius = Math.max(4, o.radius ?? 18);
|
|
2597
|
+
const padding = clamp(o.padding ?? 1.5, 0, radius * 0.45);
|
|
2598
|
+
const drawRadius = radius - padding;
|
|
2599
|
+
const minValue = Math.max(0, o.minValue ?? 1);
|
|
2600
|
+
const metric = o.value === "count" ? "count" : "value";
|
|
2601
|
+
const bins = hexBinPoints(pts, radius).filter((bin) => {
|
|
2602
|
+
if (bin[metric] < minValue) return false;
|
|
2603
|
+
if (this.o.mode === "globe") return Math.hypot(bin.x - cx, bin.y - cy) <= globeRadius + drawRadius;
|
|
2604
|
+
return bin.x >= -drawRadius && bin.x <= w + drawRadius && bin.y >= -drawRadius && bin.y <= h + drawRadius;
|
|
2605
|
+
});
|
|
2606
|
+
const max = Math.max(1, ...bins.map((bin) => bin[metric]));
|
|
2607
|
+
const range = Array.isArray(o.colorRange) && o.colorRange.length > 1
|
|
2608
|
+
? o.colorRange
|
|
2609
|
+
: [t.ocean[0], o.color || t.marker];
|
|
2610
|
+
const domain = range.map((_, index) => max * index / (range.length - 1));
|
|
2611
|
+
const scale = colorScale(domain, range);
|
|
2612
|
+
const hits = [];
|
|
2613
|
+
|
|
2614
|
+
ctx.save();
|
|
2615
|
+
if (this.o.mode === "globe") {
|
|
2616
|
+
ctx.beginPath();
|
|
2617
|
+
ctx.arc(cx, cy, globeRadius, 0, TAU);
|
|
2618
|
+
ctx.clip();
|
|
2619
|
+
} else {
|
|
2620
|
+
ctx.beginPath();
|
|
2621
|
+
ctx.rect(0, 0, w, h);
|
|
2622
|
+
ctx.clip();
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
ctx.lineJoin = "round";
|
|
2626
|
+
for (const bin of bins) {
|
|
2627
|
+
const value = bin[metric];
|
|
2628
|
+
ctx.beginPath();
|
|
2629
|
+
for (let i = 0; i < 6; i++) {
|
|
2630
|
+
const angle = (60 * i - 30) * D2R;
|
|
2631
|
+
const x = bin.x + drawRadius * Math.cos(angle);
|
|
2632
|
+
const y = bin.y + drawRadius * Math.sin(angle);
|
|
2633
|
+
i ? ctx.lineTo(x, y) : ctx.moveTo(x, y);
|
|
2634
|
+
}
|
|
2635
|
+
ctx.closePath();
|
|
2636
|
+
ctx.globalAlpha = clamp(o.opacity ?? 0.82, 0, 1);
|
|
2637
|
+
ctx.fillStyle = scale(value) || o.color || t.marker;
|
|
2638
|
+
ctx.fill();
|
|
2639
|
+
if ((o.strokeWidth ?? 0.8) > 0) {
|
|
2640
|
+
ctx.globalAlpha = 1;
|
|
2641
|
+
ctx.strokeStyle = o.stroke || withAlpha(t.label, 0.3);
|
|
2642
|
+
ctx.lineWidth = o.strokeWidth ?? 0.8;
|
|
2643
|
+
ctx.stroke();
|
|
2644
|
+
}
|
|
2645
|
+
if (o.showCount && drawRadius >= 10) {
|
|
2646
|
+
ctx.globalAlpha = 1;
|
|
2647
|
+
ctx.fillStyle = o.labelColor || t.label;
|
|
2648
|
+
ctx.font = `600 ${Math.max(9, Math.min(13, drawRadius * 0.7))}px Inter,system-ui,sans-serif`;
|
|
2649
|
+
ctx.textAlign = "center";
|
|
2650
|
+
ctx.textBaseline = "middle";
|
|
2651
|
+
ctx.fillText(String(value), bin.x, bin.y);
|
|
2652
|
+
}
|
|
2653
|
+
hits.push({
|
|
2654
|
+
marker: {
|
|
2655
|
+
hexBin: true,
|
|
2656
|
+
count: bin.count,
|
|
2657
|
+
value: bin.value,
|
|
2658
|
+
markerCount: bin.count,
|
|
2659
|
+
markers: bin.markers,
|
|
2660
|
+
lon: bin.lon,
|
|
2661
|
+
lat: bin.lat,
|
|
2662
|
+
},
|
|
2663
|
+
x: bin.x,
|
|
2664
|
+
y: bin.y,
|
|
2665
|
+
r: drawRadius,
|
|
2666
|
+
});
|
|
2667
|
+
}
|
|
2668
|
+
ctx.restore();
|
|
2669
|
+
this._lastHexBins = bins;
|
|
2670
|
+
return hits;
|
|
2671
|
+
}
|
|
2672
|
+
|
|
2590
2673
|
/** Spike height for a marker, as a fraction of the globe radius. */
|
|
2591
2674
|
_spikeLift(m) {
|
|
2592
2675
|
if (!this.o.spikes) return 0;
|
|
@@ -2967,9 +3050,11 @@ export class GeoGlobe {
|
|
|
2967
3050
|
pts.push({ m, x: cx + x, y: cy + y, depth: 0.65 + c * 0.35 });
|
|
2968
3051
|
}
|
|
2969
3052
|
if (this.o.heatmap) this._paintHeatmap(pts, t);
|
|
3053
|
+
const binHits = this.o.hexBins ? this._paintHexBins(pts, t, cx, cy, r, w, h) : [];
|
|
2970
3054
|
if (this.o.spikes) this._paintSpikes(t, cx, cy, r, w, h, null);
|
|
2971
3055
|
this._paintViewerAccuracy(t, cx, cy, r, null);
|
|
2972
|
-
const
|
|
3056
|
+
const showMarkers = !this.o.hexBins || (this.o.hexBins !== true && this.o.hexBins.hideMarkers === false);
|
|
3057
|
+
const hits = [...binHits, ...(showMarkers ? this._paintMarkers(pts, t) : [])];
|
|
2973
3058
|
if (this.o.labels) this._paintLabels(pts, t, cx, cy, r, w, h);
|
|
2974
3059
|
this._paintAnnotations(t);
|
|
2975
3060
|
this._paintPings(t);
|
|
@@ -3044,9 +3129,11 @@ export class GeoGlobe {
|
|
|
3044
3129
|
pts.push({ m, x, y, depth: 1 });
|
|
3045
3130
|
}
|
|
3046
3131
|
if (this.o.heatmap) this._paintHeatmap(pts, t);
|
|
3132
|
+
const binHits = this.o.hexBins ? this._paintHexBins(pts, t, 0, 0, 0, w, h) : [];
|
|
3047
3133
|
if (this.o.spikes) this._paintSpikes(t, 0, 0, 0, w, h, fwd);
|
|
3048
3134
|
this._paintViewerAccuracy(t, 0, 0, 0, fwd);
|
|
3049
|
-
const
|
|
3135
|
+
const showMarkers = !this.o.hexBins || (this.o.hexBins !== true && this.o.hexBins.hideMarkers === false);
|
|
3136
|
+
const hits = [...binHits, ...(showMarkers ? this._paintMarkers(pts, t) : [])];
|
|
3050
3137
|
if (this.o.labels) this._paintLabels(pts, t, 0, 0, 0, w, h);
|
|
3051
3138
|
this._paintAnnotations(t);
|
|
3052
3139
|
this._paintPings(t);
|
package/src/geo.js
CHANGED
|
@@ -254,6 +254,76 @@ export const withAlpha = (color, a) => {
|
|
|
254
254
|
return color;
|
|
255
255
|
};
|
|
256
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Aggregates projected points into a pointy-top hexagonal grid.
|
|
259
|
+
*
|
|
260
|
+
* The input is deliberately renderer-shaped (`{ x, y, depth, m }`) so the
|
|
261
|
+
* same helper works after either globe or flat-map projection. Each returned
|
|
262
|
+
* bin preserves its source markers for tooltips, clicks, and custom details.
|
|
263
|
+
*/
|
|
264
|
+
export const hexBinPoints = (points = [], radius = 18) => {
|
|
265
|
+
const size = Math.max(1, Number(radius) || 18);
|
|
266
|
+
const sqrt3 = Math.sqrt(3);
|
|
267
|
+
const cells = new Map();
|
|
268
|
+
|
|
269
|
+
const roundAxial = (q, r) => {
|
|
270
|
+
let x = q;
|
|
271
|
+
let z = r;
|
|
272
|
+
let y = -x - z;
|
|
273
|
+
let rx = Math.round(x);
|
|
274
|
+
let ry = Math.round(y);
|
|
275
|
+
let rz = Math.round(z);
|
|
276
|
+
const dx = Math.abs(rx - x);
|
|
277
|
+
const dy = Math.abs(ry - y);
|
|
278
|
+
const dz = Math.abs(rz - z);
|
|
279
|
+
if (dx > dy && dx > dz) rx = -ry - rz;
|
|
280
|
+
else if (dy > dz) ry = -rx - rz;
|
|
281
|
+
else rz = -rx - ry;
|
|
282
|
+
return [rx, rz];
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
for (const point of points) {
|
|
286
|
+
if (!Number.isFinite(point?.x) || !Number.isFinite(point?.y)) continue;
|
|
287
|
+
const q = (sqrt3 / 3 * point.x - point.y / 3) / size;
|
|
288
|
+
const r = (2 * point.y / 3) / size;
|
|
289
|
+
const [hq, hr] = roundAxial(q, r);
|
|
290
|
+
const key = `${hq}:${hr}`;
|
|
291
|
+
let cell = cells.get(key);
|
|
292
|
+
if (!cell) {
|
|
293
|
+
cell = {
|
|
294
|
+
q: hq,
|
|
295
|
+
r: hr,
|
|
296
|
+
x: size * sqrt3 * (hq + hr / 2),
|
|
297
|
+
y: size * 1.5 * hr,
|
|
298
|
+
count: 0,
|
|
299
|
+
value: 0,
|
|
300
|
+
depth: 0,
|
|
301
|
+
lon: 0,
|
|
302
|
+
lat: 0,
|
|
303
|
+
weight: 0,
|
|
304
|
+
markers: [],
|
|
305
|
+
};
|
|
306
|
+
cells.set(key, cell);
|
|
307
|
+
}
|
|
308
|
+
const marker = point.m || {};
|
|
309
|
+
const value = Number(marker.count);
|
|
310
|
+
const weight = Number.isFinite(value) && value > 0 ? value : 1;
|
|
311
|
+
cell.count += 1;
|
|
312
|
+
cell.value += weight;
|
|
313
|
+
cell.depth = Math.max(cell.depth, Number(point.depth) || 0);
|
|
314
|
+
cell.lon += (Number(marker.lon) || 0) * weight;
|
|
315
|
+
cell.lat += (Number(marker.lat) || 0) * weight;
|
|
316
|
+
cell.weight += weight;
|
|
317
|
+
cell.markers.push(marker);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return [...cells.values()].map((cell) => ({
|
|
321
|
+
...cell,
|
|
322
|
+
lon: cell.weight ? cell.lon / cell.weight : 0,
|
|
323
|
+
lat: cell.weight ? cell.lat / cell.weight : 0,
|
|
324
|
+
}));
|
|
325
|
+
};
|
|
326
|
+
|
|
257
327
|
const parseRGB = (color) => {
|
|
258
328
|
if (color.startsWith("#")) {
|
|
259
329
|
const hex = color.length === 4 ? color.replace(/#(.)(.)(.)/, "#$1$1$2$2$3$3") : color;
|
package/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Keep in sync with package.json. Release checks enforce this value.
|
|
2
|
-
export const CANVAS_GLOBE_VERSION = "1.
|
|
2
|
+
export const CANVAS_GLOBE_VERSION = "1.3.0";
|
package/types/element.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { Arc, CountryShape, GeoGlobe, GeoGlobeOptions, Marker, ClusterMarker, FlyToOptions } from "./index.js";
|
|
1
|
+
import type { Arc, CountryShape, GeoGlobe, GeoGlobeOptions, Marker, ClusterMarker, HexBinMarker, FlyToOptions } from "./index.js";
|
|
2
2
|
|
|
3
3
|
export interface GeoGlobeEventMap {
|
|
4
|
-
"geo-hover": CustomEvent<{ marker: Marker | ClusterMarker | null; pos: { x: number; y: number } | null }>;
|
|
5
|
-
"geo-click": CustomEvent<{ marker: Marker | ClusterMarker; pos: { x: number; y: number } }>;
|
|
4
|
+
"geo-hover": CustomEvent<{ marker: Marker | ClusterMarker | HexBinMarker | null; pos: { x: number; y: number } | null }>;
|
|
5
|
+
"geo-click": CustomEvent<{ marker: Marker | ClusterMarker | HexBinMarker; pos: { x: number; y: number } }>;
|
|
6
6
|
"geo-country-hover": CustomEvent<{ country: CountryShape | null; pos: { x: number; y: number } | null }>;
|
|
7
7
|
"geo-country-click": CustomEvent<{ country: CountryShape; pos: { x: number; y: number } }>;
|
|
8
8
|
"geo-render": CustomEvent<{ globe: GeoGlobe }>;
|
package/types/index.d.ts
CHANGED
|
@@ -32,6 +32,20 @@ export interface ClusterMarker {
|
|
|
32
32
|
lon: number;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/** Synthetic marker produced by the `hexBins` density layer. */
|
|
36
|
+
export interface HexBinMarker {
|
|
37
|
+
hexBin: true;
|
|
38
|
+
/** Number of source markers in the cell. */
|
|
39
|
+
markerCount: number;
|
|
40
|
+
/** Number of source markers in the cell. */
|
|
41
|
+
count: number;
|
|
42
|
+
/** Sum of source marker `count` values, with missing values treated as 1. */
|
|
43
|
+
value: number;
|
|
44
|
+
markers: Marker[];
|
|
45
|
+
lat: number;
|
|
46
|
+
lon: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
35
49
|
export type Coordinate = { lat: number; lon: number } | [lon: number, lat: number];
|
|
36
50
|
|
|
37
51
|
export interface Arc {
|
|
@@ -178,7 +192,7 @@ export interface Handle {
|
|
|
178
192
|
stop(): void;
|
|
179
193
|
}
|
|
180
194
|
|
|
181
|
-
export interface StoryStep extends Partial<GeoGlobeOptions> {
|
|
195
|
+
export interface StoryStep extends Omit<Partial<GeoGlobeOptions>, "center" | "zoom"> {
|
|
182
196
|
/** Scroll progress, 0-1. */
|
|
183
197
|
at: number;
|
|
184
198
|
center?: [lon: number, lat: number];
|
|
@@ -193,6 +207,31 @@ export interface HeatmapOptions {
|
|
|
193
207
|
color?: string;
|
|
194
208
|
}
|
|
195
209
|
|
|
210
|
+
export interface HexBinOptions {
|
|
211
|
+
/** Hexagon radius in screen pixels. Default 18. */
|
|
212
|
+
radius?: number;
|
|
213
|
+
/** Ignore cells below this count or summed value. Default 1. */
|
|
214
|
+
minValue?: number;
|
|
215
|
+
/** Colour cells by summed marker values or marker count. Default "sum". */
|
|
216
|
+
value?: "sum" | "count";
|
|
217
|
+
/** Single base colour used when `colorRange` is omitted. */
|
|
218
|
+
color?: string;
|
|
219
|
+
/** Low-to-high colour ramp. */
|
|
220
|
+
colorRange?: string[];
|
|
221
|
+
/** Fill opacity, 0-1. Default 0.82. */
|
|
222
|
+
opacity?: number;
|
|
223
|
+
stroke?: string;
|
|
224
|
+
/** Cell outline width in px. Default 0.8. */
|
|
225
|
+
strokeWidth?: number;
|
|
226
|
+
/** Gap inside each cell in px. Default 1.5. */
|
|
227
|
+
padding?: number;
|
|
228
|
+
/** Draw the selected count/value inside each cell. Default false. */
|
|
229
|
+
showCount?: boolean;
|
|
230
|
+
labelColor?: string;
|
|
231
|
+
/** Hide individual markers beneath the bins. Default true. */
|
|
232
|
+
hideMarkers?: boolean;
|
|
233
|
+
}
|
|
234
|
+
|
|
196
235
|
export interface SpikeOptions {
|
|
197
236
|
/** Tallest spike as a fraction of the globe radius. Default 0.28. */
|
|
198
237
|
height?: number;
|
|
@@ -378,7 +417,7 @@ export interface RenderMarkerContext {
|
|
|
378
417
|
globe: GeoGlobe;
|
|
379
418
|
}
|
|
380
419
|
|
|
381
|
-
export type TooltipKind = "marker" | "cluster" | "country";
|
|
420
|
+
export type TooltipKind = "marker" | "cluster" | "hex-bin" | "country";
|
|
382
421
|
|
|
383
422
|
export interface GeoGlobeOptions {
|
|
384
423
|
/** Commercial license key supplied after purchase. */
|
|
@@ -387,8 +426,6 @@ export interface GeoGlobeOptions {
|
|
|
387
426
|
mode?: "globe" | "map";
|
|
388
427
|
/** Flat-map projection. Default "equirectangular". */
|
|
389
428
|
projection?: MapProjection;
|
|
390
|
-
/** Built-in theme name or a partial theme object. Default "atlas". */
|
|
391
|
-
theme?: ThemeName | Partial<Theme>;
|
|
392
429
|
/** Named bundle of theme + render style, applied under your own options. */
|
|
393
430
|
preset?: PresetName;
|
|
394
431
|
/** Theme name, a partial theme, "auto" for the OS colour scheme, or "css"
|
|
@@ -428,6 +465,8 @@ export interface GeoGlobeOptions {
|
|
|
428
465
|
transparentBackground?: boolean;
|
|
429
466
|
/** Additive density blobs instead of, or under, markers. */
|
|
430
467
|
heatmap?: boolean | HeatmapOptions;
|
|
468
|
+
/** Screen-space hexagonal aggregation for dense marker datasets. */
|
|
469
|
+
hexBins?: boolean | HexBinOptions;
|
|
431
470
|
/** Bars standing off the surface, scaled by each marker's `count`. */
|
|
432
471
|
spikes?: boolean | SpikeOptions;
|
|
433
472
|
/** Text labels with collision avoidance. */
|
|
@@ -496,13 +535,13 @@ export interface GeoGlobeOptions {
|
|
|
496
535
|
/** Frame cap. Default 30. */
|
|
497
536
|
fps?: number;
|
|
498
537
|
/** Built-in tooltip. `true` uses the default text, or pass a formatter. */
|
|
499
|
-
tooltip?: boolean | ((target: Marker | ClusterMarker | CountryShape, kind: TooltipKind) => string);
|
|
538
|
+
tooltip?: boolean | ((target: Marker | ClusterMarker | HexBinMarker | CountryShape, kind: TooltipKind) => string);
|
|
500
539
|
/** Honour `prefers-reduced-motion`. Default true. */
|
|
501
540
|
respectReducedMotion?: boolean;
|
|
502
541
|
/** Accessible name for the canvas. */
|
|
503
542
|
ariaLabel?: string;
|
|
504
|
-
onHover?: (marker: Marker | ClusterMarker | null, position: { x: number; y: number } | null) => void;
|
|
505
|
-
onClick?: (marker: Marker | ClusterMarker, position: { x: number; y: number }) => void;
|
|
543
|
+
onHover?: (marker: Marker | ClusterMarker | HexBinMarker | null, position: { x: number; y: number } | null) => void;
|
|
544
|
+
onClick?: (marker: Marker | ClusterMarker | HexBinMarker, position: { x: number; y: number }) => void;
|
|
506
545
|
onCountryHover?: (country: CountryShape | null, position: { x: number; y: number } | null) => void;
|
|
507
546
|
onCountryClick?: (country: CountryShape, position: { x: number; y: number }) => void;
|
|
508
547
|
onRender?: (instance: GeoGlobe) => void;
|
package/types/jsr-element.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { Arc, CountryShape, GeoGlobe, GeoGlobeOptions, Marker, ClusterMarker, FlyToOptions } from "./index.js";
|
|
1
|
+
import type { Arc, CountryShape, GeoGlobe, GeoGlobeOptions, Marker, ClusterMarker, HexBinMarker, FlyToOptions } from "./index.js";
|
|
2
2
|
|
|
3
3
|
export interface GeoGlobeEventMap {
|
|
4
|
-
"geo-hover": CustomEvent<{ marker: Marker | ClusterMarker | null; pos: { x: number; y: number } | null }>;
|
|
5
|
-
"geo-click": CustomEvent<{ marker: Marker | ClusterMarker; pos: { x: number; y: number } }>;
|
|
4
|
+
"geo-hover": CustomEvent<{ marker: Marker | ClusterMarker | HexBinMarker | null; pos: { x: number; y: number } | null }>;
|
|
5
|
+
"geo-click": CustomEvent<{ marker: Marker | ClusterMarker | HexBinMarker; pos: { x: number; y: number } }>;
|
|
6
6
|
"geo-country-hover": CustomEvent<{ country: CountryShape | null; pos: { x: number; y: number } | null }>;
|
|
7
7
|
"geo-country-click": CustomEvent<{ country: CountryShape; pos: { x: number; y: number } }>;
|
|
8
8
|
"geo-render": CustomEvent<{ globe: GeoGlobe }>;
|