canvas-globe 1.1.1 → 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 +44 -0
- package/README.md +113 -5
- package/THIRD_PARTY_NOTICES.md +1 -0
- package/codemeta.json +1 -1
- package/custom-elements.json +2 -2
- package/dist/canvas-globe.umd.js +331 -8
- package/package.json +34 -2
- package/src/charts.js +458 -0
- package/src/controls.js +187 -0
- package/src/data/places.js +5 -0
- package/src/fx/effects/ambient.js +355 -0
- package/src/fx/effects/data.js +438 -0
- package/src/fx/effects/entrances.js +111 -0
- package/src/fx/effects/interaction.js +850 -0
- package/src/fx/effects/scene.js +571 -0
- package/src/fx/effects/transitions.js +209 -0
- package/src/fx/index.js +52 -0
- package/src/fx/pointer.js +94 -0
- package/src/fx/runtime.js +161 -0
- package/src/geo-globe.js +261 -8
- package/src/geo.js +70 -0
- package/src/places.js +97 -0
- package/src/recipes.js +188 -0
- package/src/version.js +1 -1
- package/types/charts.d.ts +82 -0
- package/types/controls.d.ts +54 -0
- package/types/element.d.ts +3 -3
- package/types/fx-interaction.d.ts +16 -0
- package/types/fx.d.ts +313 -0
- package/types/index.d.ts +94 -7
- package/types/jsr-element.d.ts +3 -3
- package/types/places.d.ts +43 -0
- package/types/recipes.d.ts +54 -0
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,
|
|
@@ -1520,13 +1591,28 @@ const DEFAULTS = {
|
|
|
1520
1591
|
onCountryHover: null,
|
|
1521
1592
|
onCountryClick: null,
|
|
1522
1593
|
onRender: null,
|
|
1594
|
+
effects: null,
|
|
1523
1595
|
};
|
|
1524
1596
|
|
|
1525
1597
|
const coord = (v) => (Array.isArray(v) ? [v[0], v[1]] : [v.lon ?? v.lng ?? v.longitude, v.lat ?? v.latitude]);
|
|
1526
1598
|
|
|
1599
|
+
const now = () => (typeof performance !== "undefined" ? performance.now() : Date.now());
|
|
1600
|
+
|
|
1601
|
+
/**
|
|
1602
|
+
* An effect's own 0→1 progress at a point on the shared clock. Derived purely
|
|
1603
|
+
* from `ms`, so the same millisecond always renders the same frame.
|
|
1604
|
+
*/
|
|
1605
|
+
const phaseOf = (fx, ms) => {
|
|
1606
|
+
const duration = Math.max(1, fx.duration ?? 1000);
|
|
1607
|
+
const total = duration + (fx.hold ?? 0);
|
|
1608
|
+
const elapsed = fx.loop === false ? Math.min(ms, total) : ((ms % total) + total) % total;
|
|
1609
|
+
return Math.min(1, elapsed / duration);
|
|
1610
|
+
};
|
|
1611
|
+
|
|
1527
1612
|
const defaultTooltip = (target, kind) => {
|
|
1528
1613
|
if (kind === "country") return target.name || String(target.id ?? "");
|
|
1529
1614
|
if (kind === "cluster") return `${target.count} in this area`;
|
|
1615
|
+
if (kind === "hex-bin") return `${target.markerCount} markers, value ${target.value}`;
|
|
1530
1616
|
const name = target.city || target.name || target.label;
|
|
1531
1617
|
const count = target.count != null ? `: ${target.count}` : "";
|
|
1532
1618
|
return name ? `${name}${count}` : `${target.lat.toFixed(2)}, ${target.lon.toFixed(2)}${count}`;
|
|
@@ -1578,6 +1664,11 @@ class GeoGlobe {
|
|
|
1578
1664
|
this._licenseHits = [];
|
|
1579
1665
|
this._licenseDismissed = false;
|
|
1580
1666
|
this._licenseHovered = false;
|
|
1667
|
+
this._fx = [];
|
|
1668
|
+
this._clock = null;
|
|
1669
|
+
this._t0 = now();
|
|
1670
|
+
this._spinBase = this.lon;
|
|
1671
|
+
this.pointer = { x: 0, y: 0, lon: 0, lat: 0, over: false };
|
|
1581
1672
|
|
|
1582
1673
|
this._applyWorld();
|
|
1583
1674
|
this._applyMarkers(this.o.markers);
|
|
@@ -1589,12 +1680,138 @@ class GeoGlobe {
|
|
|
1589
1680
|
this.resize();
|
|
1590
1681
|
if (this.o.focus) this.focusOn(this.o.focus, { instant: true });
|
|
1591
1682
|
if (this.o.showViewer) this._initViewer();
|
|
1683
|
+
for (const fx of this.o.effects || []) this.use(fx);
|
|
1592
1684
|
this._loop = this._loop.bind(this);
|
|
1593
1685
|
this._raf = requestAnimationFrame(this._loop);
|
|
1594
1686
|
}
|
|
1595
1687
|
|
|
1596
1688
|
/* ------------------------------ public API ------------------------------ */
|
|
1597
1689
|
|
|
1690
|
+
/* --------------------------------- effects ------------------------------- */
|
|
1691
|
+
|
|
1692
|
+
/** Installs an effect. Returns `this`, so calls chain. */
|
|
1693
|
+
use(effect) {
|
|
1694
|
+
if (!effect || typeof effect.frame !== "function") return this;
|
|
1695
|
+
this._fx.push({ fx: effect, state: effect.setup?.(this) ?? {} });
|
|
1696
|
+
this._fx.sort((a, b) => (a.fx.z ?? 0) - (b.fx.z ?? 0));
|
|
1697
|
+
return this.invalidate();
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
/** Removes an effect by reference or by name. */
|
|
1701
|
+
remove(effect) {
|
|
1702
|
+
const i = this._fx.findIndex((e) => e.fx === effect || e.fx.name === effect);
|
|
1703
|
+
if (i < 0) return this;
|
|
1704
|
+
const [entry] = this._fx.splice(i, 1);
|
|
1705
|
+
entry.fx.dispose?.(entry.state, this);
|
|
1706
|
+
return this.invalidate();
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
clearEffects() {
|
|
1710
|
+
while (this._fx.length) this.remove(this._fx[0].fx);
|
|
1711
|
+
return this;
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
get effects() {
|
|
1715
|
+
return this._fx.map((e) => e.fx);
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
/**
|
|
1719
|
+
* Pins the effect clock to `ms`. Rendering becomes reproducible, which is
|
|
1720
|
+
* what frame-exact export and seamless loops need. `play()` releases it.
|
|
1721
|
+
*/
|
|
1722
|
+
seek(ms) {
|
|
1723
|
+
// Capture where auto-rotation started from, so longitude can be derived
|
|
1724
|
+
// from the clock instead of accumulated per frame. Only on the way in:
|
|
1725
|
+
// seeking again while already seeked must not shift the origin.
|
|
1726
|
+
if (this._clock == null) this._spinBase = wrapLon(this.lon - this._spinRate() * this._fxTime());
|
|
1727
|
+
this._clock = Math.max(0, ms);
|
|
1728
|
+
if (this._autoSpin()) this.lon = wrapLon(this._spinBase + this._spinRate() * this._clock);
|
|
1729
|
+
return this.invalidate();
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
play() {
|
|
1733
|
+
// Resume the timeline where it was paused rather than snapping back to 0.
|
|
1734
|
+
if (this._clock != null) this._t0 = now() - this._clock;
|
|
1735
|
+
this._clock = null;
|
|
1736
|
+
return this.invalidate();
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
/** Draws the single frame that belongs at `ms` on the effect clock. */
|
|
1740
|
+
renderFrame(ms) {
|
|
1741
|
+
return this.seek(ms).render();
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
_fxTime() {
|
|
1745
|
+
return this._clock != null ? this._clock : now() - this._t0;
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
/**
|
|
1749
|
+
* Wall clock for looping scene animation. Pinned while seeking, so arcs,
|
|
1750
|
+
* orbits and pulses land on the same phase every time a frame is redrawn.
|
|
1751
|
+
*/
|
|
1752
|
+
_animTime() {
|
|
1753
|
+
return this._clock != null ? this._clock : Date.now();
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
/** Degrees of auto-rotation per millisecond. */
|
|
1757
|
+
_spinRate() {
|
|
1758
|
+
return ((this.o.rotateSpeed || 0) * (this.o.fps || 30)) / 1000;
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
_runPasses(stage) {
|
|
1762
|
+
if (!this._fx.length) return;
|
|
1763
|
+
const ms = this._fxTime();
|
|
1764
|
+
const { ctx } = this;
|
|
1765
|
+
for (const entry of this._fx) {
|
|
1766
|
+
if ((entry.fx.stage || "above") !== stage) continue;
|
|
1767
|
+
ctx.save();
|
|
1768
|
+
try {
|
|
1769
|
+
entry.fx.frame(ctx, this, phaseOf(entry.fx, ms), entry.state);
|
|
1770
|
+
} catch (err) {
|
|
1771
|
+
// One bad effect must not take the render loop down with it.
|
|
1772
|
+
if (!entry.failed) {
|
|
1773
|
+
entry.failed = true;
|
|
1774
|
+
console.error(`canvas-globe: effect "${entry.fx.name || "anonymous"}" failed`, err);
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
ctx.restore();
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
/** Land sample points as `[lon, lat]` pairs — what the dot matrix draws. */
|
|
1782
|
+
landPoints(spacing) {
|
|
1783
|
+
const prev = this.o.dotSpacing;
|
|
1784
|
+
if (spacing != null) this.o.dotSpacing = spacing;
|
|
1785
|
+
const flat = this._dotPoints();
|
|
1786
|
+
this.o.dotSpacing = prev;
|
|
1787
|
+
const out = [];
|
|
1788
|
+
for (let i = 0; i < flat.length; i += 2) out.push([flat[i], flat[i + 1]]);
|
|
1789
|
+
return out;
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
/** Traces a country outline into a path using the current projection. */
|
|
1793
|
+
tracePath(shape, ctx = this.ctx, transform) {
|
|
1794
|
+
const geom = shape?.geometry || shape;
|
|
1795
|
+
if (!geom?.coordinates) return this;
|
|
1796
|
+
const polys = geom.type === "Polygon" ? [geom.coordinates] : geom.coordinates;
|
|
1797
|
+
for (const poly of polys) {
|
|
1798
|
+
for (const ring of poly) {
|
|
1799
|
+
let open = false;
|
|
1800
|
+
for (const c of ring) {
|
|
1801
|
+
const src = transform ? transform(c) : c;
|
|
1802
|
+
const p = this.project(src[0], src[1]);
|
|
1803
|
+
if (!p) {
|
|
1804
|
+
open = false;
|
|
1805
|
+
continue;
|
|
1806
|
+
}
|
|
1807
|
+
open ? ctx.lineTo(p.x, p.y) : (ctx.moveTo(p.x, p.y), (open = true));
|
|
1808
|
+
}
|
|
1809
|
+
if (open) ctx.closePath();
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
return this;
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1598
1815
|
setMarkers(markers = []) {
|
|
1599
1816
|
this._applyMarkers(markers);
|
|
1600
1817
|
this._focus = -1;
|
|
@@ -2151,9 +2368,12 @@ class GeoGlobe {
|
|
|
2151
2368
|
const { w, h } = this._size();
|
|
2152
2369
|
ctx.setTransform(this._dpr, 0, 0, this._dpr, 0, 0);
|
|
2153
2370
|
ctx.clearRect(0, 0, w, h);
|
|
2371
|
+
this._runPasses("beneath");
|
|
2154
2372
|
this.hits = this.o.mode === "map" ? this._paintMap(w, h) : this._paintGlobe(w, h);
|
|
2155
2373
|
this._dirty = false;
|
|
2374
|
+
this._runPasses("above");
|
|
2156
2375
|
this.o.onRender?.(this);
|
|
2376
|
+
this._runPasses("post");
|
|
2157
2377
|
this._paintLicenseNotice(w, h);
|
|
2158
2378
|
return this;
|
|
2159
2379
|
}
|
|
@@ -2171,6 +2391,7 @@ class GeoGlobe {
|
|
|
2171
2391
|
destroy() {
|
|
2172
2392
|
this._destroyed = true;
|
|
2173
2393
|
cancelAnimationFrame(this._raf);
|
|
2394
|
+
this.clearEffects();
|
|
2174
2395
|
this.stopTour();
|
|
2175
2396
|
this.stopStory();
|
|
2176
2397
|
this.stopTimeline();
|
|
@@ -2625,6 +2846,15 @@ class GeoGlobe {
|
|
|
2625
2846
|
this._onMove = (e) => {
|
|
2626
2847
|
const [x, y] = this._local(e);
|
|
2627
2848
|
this._pointer = { x: e.clientX, y: e.clientY };
|
|
2849
|
+
this.pointer.x = x;
|
|
2850
|
+
this.pointer.y = y;
|
|
2851
|
+
this.pointer.over = true;
|
|
2852
|
+
const at = this.unproject(x, y);
|
|
2853
|
+
if (at) {
|
|
2854
|
+
this.pointer.lon = at[0];
|
|
2855
|
+
this.pointer.lat = at[1];
|
|
2856
|
+
}
|
|
2857
|
+
if (this._fx.length) this._dirty = true;
|
|
2628
2858
|
const licenseHovered = this._licenseHitAt(x, y);
|
|
2629
2859
|
if (licenseHovered !== this._licenseHovered) {
|
|
2630
2860
|
this._licenseHovered = licenseHovered;
|
|
@@ -2678,7 +2908,7 @@ class GeoGlobe {
|
|
|
2678
2908
|
this._cursor();
|
|
2679
2909
|
this._dirty = true;
|
|
2680
2910
|
const target = marker || this._hoveredCountry;
|
|
2681
|
-
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);
|
|
2682
2912
|
} else if (this._tipVisible) {
|
|
2683
2913
|
this._placeTip();
|
|
2684
2914
|
}
|
|
@@ -2695,6 +2925,7 @@ class GeoGlobe {
|
|
|
2695
2925
|
this._onLeave = (e) => {
|
|
2696
2926
|
this._onUp(e);
|
|
2697
2927
|
this._pointer = null;
|
|
2928
|
+
this.pointer.over = false;
|
|
2698
2929
|
this._licenseHovered = false;
|
|
2699
2930
|
if (this._hovered || this._hoveredCountry) {
|
|
2700
2931
|
if (this._hovered) this.o.onHover?.(null, null);
|
|
@@ -2875,13 +3106,20 @@ class GeoGlobe {
|
|
|
2875
3106
|
|
|
2876
3107
|
/* --------------------------------- loop --------------------------------- */
|
|
2877
3108
|
|
|
2878
|
-
|
|
3109
|
+
/** Auto-rotation is configured and permitted, regardless of the clock. */
|
|
3110
|
+
_autoSpin() {
|
|
2879
3111
|
return !!this.o.autoRotate && !this._reducedMotion() && !this._drag && !this._vel && this.o.mode === "globe";
|
|
2880
3112
|
}
|
|
2881
3113
|
|
|
3114
|
+
/** Auto-rotation should advance on its own this frame. */
|
|
3115
|
+
_spinning() {
|
|
3116
|
+
return this._autoSpin() && this._clock == null;
|
|
3117
|
+
}
|
|
3118
|
+
|
|
2882
3119
|
/** True while something on screen still needs to move. */
|
|
2883
3120
|
_animating() {
|
|
2884
3121
|
if (this._target || this._drag || this._vel) return true;
|
|
3122
|
+
if (this._fx.length && this._clock == null) return true;
|
|
2885
3123
|
if (this._spinning()) return true;
|
|
2886
3124
|
if (this._reducedMotion()) return false;
|
|
2887
3125
|
if (this._hasLive || this._pings.length) return true;
|
|
@@ -3155,7 +3393,7 @@ class GeoGlobe {
|
|
|
3155
3393
|
const list = this._orbitList();
|
|
3156
3394
|
if (!list.length) return;
|
|
3157
3395
|
const { ctx } = this;
|
|
3158
|
-
const spin = this._reducedMotion() ? 0 : (
|
|
3396
|
+
const spin = this._reducedMotion() ? 0 : (this._animTime() / 1000) % 3600;
|
|
3159
3397
|
ctx.lineCap = "round";
|
|
3160
3398
|
for (const orbit of list) {
|
|
3161
3399
|
const inc = (orbit.inclination ?? 30) * D2R;
|
|
@@ -3709,7 +3947,7 @@ class GeoGlobe {
|
|
|
3709
3947
|
const arcs = this.o.arcs;
|
|
3710
3948
|
if (!arcs || !arcs.length) return;
|
|
3711
3949
|
const { ctx } = this;
|
|
3712
|
-
const now =
|
|
3950
|
+
const now = this._animTime();
|
|
3713
3951
|
const still = this._reducedMotion();
|
|
3714
3952
|
ctx.lineCap = "round";
|
|
3715
3953
|
ctx.lineJoin = "round";
|
|
@@ -3858,6 +4096,87 @@ class GeoGlobe {
|
|
|
3858
4096
|
ctx.restore();
|
|
3859
4097
|
}
|
|
3860
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
|
+
|
|
3861
4180
|
/** Spike height for a marker, as a fraction of the globe radius. */
|
|
3862
4181
|
_spikeLift(m) {
|
|
3863
4182
|
if (!this.o.spikes) return 0;
|
|
@@ -4238,9 +4557,11 @@ class GeoGlobe {
|
|
|
4238
4557
|
pts.push({ m, x: cx + x, y: cy + y, depth: 0.65 + c * 0.35 });
|
|
4239
4558
|
}
|
|
4240
4559
|
if (this.o.heatmap) this._paintHeatmap(pts, t);
|
|
4560
|
+
const binHits = this.o.hexBins ? this._paintHexBins(pts, t, cx, cy, r, w, h) : [];
|
|
4241
4561
|
if (this.o.spikes) this._paintSpikes(t, cx, cy, r, w, h, null);
|
|
4242
4562
|
this._paintViewerAccuracy(t, cx, cy, r, null);
|
|
4243
|
-
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) : [])];
|
|
4244
4565
|
if (this.o.labels) this._paintLabels(pts, t, cx, cy, r, w, h);
|
|
4245
4566
|
this._paintAnnotations(t);
|
|
4246
4567
|
this._paintPings(t);
|
|
@@ -4315,9 +4636,11 @@ class GeoGlobe {
|
|
|
4315
4636
|
pts.push({ m, x, y, depth: 1 });
|
|
4316
4637
|
}
|
|
4317
4638
|
if (this.o.heatmap) this._paintHeatmap(pts, t);
|
|
4639
|
+
const binHits = this.o.hexBins ? this._paintHexBins(pts, t, 0, 0, 0, w, h) : [];
|
|
4318
4640
|
if (this.o.spikes) this._paintSpikes(t, 0, 0, 0, w, h, fwd);
|
|
4319
4641
|
this._paintViewerAccuracy(t, 0, 0, 0, fwd);
|
|
4320
|
-
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) : [])];
|
|
4321
4644
|
if (this.o.labels) this._paintLabels(pts, t, 0, 0, 0, w, h);
|
|
4322
4645
|
this._paintAnnotations(t);
|
|
4323
4646
|
this._paintPings(t);
|
|
@@ -4438,7 +4761,7 @@ class GeoGlobe {
|
|
|
4438
4761
|
}
|
|
4439
4762
|
|
|
4440
4763
|
if (m.live && !this._reducedMotion()) {
|
|
4441
|
-
const pulse = (
|
|
4764
|
+
const pulse = (this._animTime() % 2200) / 2200;
|
|
4442
4765
|
ctx.globalAlpha = 1 - pulse;
|
|
4443
4766
|
ctx.strokeStyle = t.live;
|
|
4444
4767
|
ctx.lineWidth = 1.4 * scale;
|
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": {
|
|
@@ -66,6 +66,36 @@
|
|
|
66
66
|
"import": "./src/react.js",
|
|
67
67
|
"default": "./src/react.js"
|
|
68
68
|
},
|
|
69
|
+
"./fx": {
|
|
70
|
+
"types": "./types/fx.d.ts",
|
|
71
|
+
"import": "./src/fx/index.js",
|
|
72
|
+
"default": "./src/fx/index.js"
|
|
73
|
+
},
|
|
74
|
+
"./fx/interaction": {
|
|
75
|
+
"types": "./types/fx-interaction.d.ts",
|
|
76
|
+
"import": "./src/fx/effects/interaction.js",
|
|
77
|
+
"default": "./src/fx/effects/interaction.js"
|
|
78
|
+
},
|
|
79
|
+
"./recipes": {
|
|
80
|
+
"types": "./types/recipes.d.ts",
|
|
81
|
+
"import": "./src/recipes.js",
|
|
82
|
+
"default": "./src/recipes.js"
|
|
83
|
+
},
|
|
84
|
+
"./charts": {
|
|
85
|
+
"types": "./types/charts.d.ts",
|
|
86
|
+
"import": "./src/charts.js",
|
|
87
|
+
"default": "./src/charts.js"
|
|
88
|
+
},
|
|
89
|
+
"./controls": {
|
|
90
|
+
"types": "./types/controls.d.ts",
|
|
91
|
+
"import": "./src/controls.js",
|
|
92
|
+
"default": "./src/controls.js"
|
|
93
|
+
},
|
|
94
|
+
"./places": {
|
|
95
|
+
"types": "./types/places.d.ts",
|
|
96
|
+
"import": "./src/places.js",
|
|
97
|
+
"default": "./src/places.js"
|
|
98
|
+
},
|
|
69
99
|
"./data/world": {
|
|
70
100
|
"types": "./types/data/world.d.ts",
|
|
71
101
|
"import": "./src/data/world.js",
|
|
@@ -95,7 +125,9 @@
|
|
|
95
125
|
"scripts": {
|
|
96
126
|
"start": "node scripts/serve.mjs",
|
|
97
127
|
"build": "node scripts/build.mjs",
|
|
98
|
-
"data": "node scripts/generate-data.mjs && node scripts/generate-timezones.mjs",
|
|
128
|
+
"data": "node scripts/generate-data.mjs && node scripts/generate-timezones.mjs && node scripts/generate-places.mjs",
|
|
129
|
+
"places": "node scripts/generate-places.mjs",
|
|
130
|
+
"audit:docs": "node scripts/audit-docs.mjs",
|
|
99
131
|
"example": "node scripts/serve.mjs",
|
|
100
132
|
"storybook": "storybook dev -p 6006",
|
|
101
133
|
"build-storybook": "storybook build",
|