canvas-globe 0.1.0 โ 0.1.2
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 +150 -137
- package/README.md +665 -638
- package/codemeta.json +1 -1
- package/dist/canvas-globe.umd.js +4280 -4265
- package/package.json +56 -56
- package/src/csv.js +151 -151
- package/src/element.js +142 -142
- package/src/export.js +19 -19
- package/src/geo-globe.js +2846 -2846
- package/src/geo.js +286 -286
- package/src/index.js +10 -10
- package/src/media.js +160 -160
- package/src/presets.js +21 -21
- package/src/react.js +60 -60
- package/src/recorder.js +70 -70
- package/src/scenes.js +80 -80
- package/src/texture.js +129 -129
- package/src/themes.js +241 -241
- package/src/viewer.js +130 -130
- package/types/element.d.ts +41 -41
- package/types/index.d.ts +674 -674
- package/types/react.d.ts +15 -15
package/README.md
CHANGED
|
@@ -1,638 +1,665 @@
|
|
|
1
|
-
# CanvasGlobe
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
- **
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
[
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
##
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
|
170
|
-
|
|
|
171
|
-
| `
|
|
172
|
-
| `
|
|
173
|
-
| `
|
|
174
|
-
| `
|
|
175
|
-
| `
|
|
176
|
-
| `
|
|
177
|
-
| `
|
|
178
|
-
| `
|
|
179
|
-
| `
|
|
180
|
-
| `
|
|
181
|
-
| `
|
|
182
|
-
| `
|
|
183
|
-
| `
|
|
184
|
-
| `
|
|
185
|
-
| `
|
|
186
|
-
| `
|
|
187
|
-
| `
|
|
188
|
-
| `
|
|
189
|
-
| `
|
|
190
|
-
| `
|
|
191
|
-
| `
|
|
192
|
-
| `
|
|
193
|
-
| `
|
|
194
|
-
| `
|
|
195
|
-
| `
|
|
196
|
-
| `
|
|
197
|
-
| `
|
|
198
|
-
| `
|
|
199
|
-
| `
|
|
200
|
-
| `
|
|
201
|
-
| `
|
|
202
|
-
| `
|
|
203
|
-
| `
|
|
204
|
-
| `
|
|
205
|
-
| `
|
|
206
|
-
| `
|
|
207
|
-
| `
|
|
208
|
-
| `
|
|
209
|
-
| `
|
|
210
|
-
| `
|
|
211
|
-
| `
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
`
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
globe.
|
|
304
|
-
globe.
|
|
305
|
-
globe.
|
|
306
|
-
globe.
|
|
307
|
-
globe.
|
|
308
|
-
globe.
|
|
309
|
-
globe.
|
|
310
|
-
globe.
|
|
311
|
-
globe.
|
|
312
|
-
globe.
|
|
313
|
-
globe.
|
|
314
|
-
globe.
|
|
315
|
-
globe.
|
|
316
|
-
globe.
|
|
317
|
-
|
|
318
|
-
globe.
|
|
319
|
-
globe.
|
|
320
|
-
globe.
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
`
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
##
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
1
|
+
# CanvasGlobe
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/canvas-globe)
|
|
4
|
+
[](https://www.npmjs.com/package/canvas-globe)
|
|
5
|
+
[](https://github.com/Shree-hari/canvas-globe/actions/workflows/ci.yml)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](types/index.d.ts)
|
|
8
|
+
[](package.json)
|
|
9
|
+
|
|
10
|
+
[](https://canvasglobe.swiftools.com/playground)
|
|
11
|
+
|
|
12
|
+
[**Open the live playground**](https://canvasglobe.swiftools.com/playground) ยท
|
|
13
|
+
[Examples](https://canvasglobe.swiftools.com/examples) ยท
|
|
14
|
+
[Documentation](https://canvasglobe.swiftools.com/intro) ยท
|
|
15
|
+
[React guide](https://canvasglobe.swiftools.com/react-globe) ยท
|
|
16
|
+
[Showcase](https://canvasglobe.swiftools.com/showcase) ยท
|
|
17
|
+
[Commercial pricing](https://canvasglobe.swiftools.com/pricing)
|
|
18
|
+
|
|
19
|
+
`canvas-globe` is a zero-dependency JavaScript library for an interactive
|
|
20
|
+
**3D globe** and **flat world map** on Canvas 2D. It works with
|
|
21
|
+
vanilla JavaScript, React, or a Web Component and requires no WebGL, map API
|
|
22
|
+
key, tile service, or runtime network request.
|
|
23
|
+
|
|
24
|
+
- **Zero dependencies:** no WebGL, D3, map tiles, or API keys
|
|
25
|
+
- **Zero network calls:** country geometry ships inside the package
|
|
26
|
+
- **Interactive:** drag, zoom, pinch, hover, and click
|
|
27
|
+
- **Marker support:** weighted markers, avatars, pulse rings, and clustering
|
|
28
|
+
- **Great-circle arcs:** animated routes clipped at the horizon
|
|
29
|
+
- **Choropleths:** colour countries by ISO code, numeric ID, or name
|
|
30
|
+
- **CSV input:** resolve cities and countries without a geocoding API
|
|
31
|
+
- **Image export:** square, story, LinkedIn, Open Graph, and transparent PNG presets
|
|
32
|
+
- **Country media:** clip images, GIFs, or video to a country's outline
|
|
33
|
+
- **Viewer location:** estimate a region from the browser time zone without a permission prompt
|
|
34
|
+
- **Live pings:** display recent activity without requiring a CanvasGlobe backend
|
|
35
|
+
- **Recording:** export a WebM clip in the browser
|
|
36
|
+
- **Presets:** ten included visual styles
|
|
37
|
+
- **Day and night:** calculate the solar terminator for a given time
|
|
38
|
+
- **Four projections:** orthographic, equirectangular, Mercator, and Natural Earth
|
|
39
|
+
- **Accessibility:** keyboard controls, a live region, and reduced-motion support
|
|
40
|
+
- **Bindings:** vanilla JavaScript, a custom element, and React
|
|
41
|
+
|
|
42
|
+
Common uses include audience dashboards, launch pages, status boards, and share graphics.
|
|
43
|
+
|
|
44
|
+
[](https://canvasglobe.swiftools.com/examples)
|
|
45
|
+
|
|
46
|
+
The images above are generated from real CanvasGlobe renders. Open the
|
|
47
|
+
[interactive playground](https://canvasglobe.swiftools.com/playground) to drag,
|
|
48
|
+
zoom, change projections, switch presets, toggle data layers, and export the result.
|
|
49
|
+
|
|
50
|
+
## When to choose CanvasGlobe
|
|
51
|
+
|
|
52
|
+
Choose CanvasGlobe when you need a JavaScript or React globe with markers,
|
|
53
|
+
great-circle arcs, choropleths, keyboard interaction, and image/video export,
|
|
54
|
+
especially when WebGL or external map services are not acceptable. Use a 3D
|
|
55
|
+
engine such as globe.gl or Cesium instead when you need terrain, perspective
|
|
56
|
+
cameras, custom shaders, or thousands of independent 3D objects. See the
|
|
57
|
+
[globe-library comparison](https://canvasglobe.swiftools.com/compare/javascript-globe-libraries).
|
|
58
|
+
|
|
59
|
+
## Licensing
|
|
60
|
+
|
|
61
|
+
CanvasGlobe is dual-licensed:
|
|
62
|
+
|
|
63
|
+
- **GPL-3.0-only** for projects that can comply with GNU GPLv3; or
|
|
64
|
+
- a **paid commercial license** for proprietary products.
|
|
65
|
+
|
|
66
|
+
The full package and feature set are the same on both paths. GPL permits
|
|
67
|
+
commercial activity; whether a particular distribution can comply is
|
|
68
|
+
fact-specific. See [LICENSING.md](LICENSING.md) and the
|
|
69
|
+
[commercial plans](https://canvasglobe.swiftools.com/pricing).
|
|
70
|
+
|
|
71
|
+
Pass the license key supplied with a commercial order. GPLv3-compatible
|
|
72
|
+
projects can request a complimentary key through the licensing page:
|
|
73
|
+
|
|
74
|
+
```js
|
|
75
|
+
createGlobe(canvas, { licenseKey: "your_license_key" });
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The default `0000-0000-000-0000` value is for evaluation only and produces a
|
|
79
|
+
console warning in browser builds.
|
|
80
|
+
|
|
81
|
+
## Install
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
npm install canvas-globe
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Or drop it on a page with no build step at all:
|
|
88
|
+
|
|
89
|
+
```html
|
|
90
|
+
<script src="https://cdn.jsdelivr.net/npm/canvas-globe/dist/canvas-globe.umd.js"></script>
|
|
91
|
+
<canvas id="globe" style="width:520px;aspect-ratio:1"></canvas>
|
|
92
|
+
<script>
|
|
93
|
+
CanvasGlobe.createGlobe(document.getElementById("globe"), {
|
|
94
|
+
markers: [{ lat: 23.03, lon: 72.58, count: 12, emoji: "๐งโ๐จ", live: true }],
|
|
95
|
+
});
|
|
96
|
+
</script>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Usage
|
|
100
|
+
|
|
101
|
+
```js
|
|
102
|
+
import { createGlobe } from "canvas-globe";
|
|
103
|
+
|
|
104
|
+
const globe = createGlobe(document.querySelector("#globe"), {
|
|
105
|
+
markers: [
|
|
106
|
+
{ lat: 23.03, lon: 72.58, count: 12, emoji: "๐งโ๐จ", live: true, city: "Ahmedabad" },
|
|
107
|
+
{ lat: 51.5, lon: -0.12, count: 8, emoji: "๐ฉโ๐ป", city: "London" },
|
|
108
|
+
],
|
|
109
|
+
theme: "atlas",
|
|
110
|
+
tooltip: (m) => `${m.city}: ${m.count} visitors`,
|
|
111
|
+
onClick: (marker) => globe.flyTo(marker.lon, marker.lat, { zoom: 2.5 }),
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The canvas is sized from CSS: give it a width and an aspect ratio:
|
|
116
|
+
|
|
117
|
+
```css
|
|
118
|
+
#globe { width: 100%; aspect-ratio: 1; } /* globe mode */
|
|
119
|
+
#map { width: 100%; aspect-ratio: 360 / 139; } /* map mode */
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`mapAspect(latRange, projection)` returns the height/width ratio for any map setup.
|
|
123
|
+
|
|
124
|
+
### Custom element
|
|
125
|
+
|
|
126
|
+
```html
|
|
127
|
+
<script type="module">
|
|
128
|
+
import "canvas-globe/element";
|
|
129
|
+
</script>
|
|
130
|
+
|
|
131
|
+
<geo-globe mode="map" theme="midnight" tooltip cluster style="display:block;width:100%"></geo-globe>
|
|
132
|
+
|
|
133
|
+
<script>
|
|
134
|
+
const el = document.querySelector("geo-globe");
|
|
135
|
+
el.markers = [{ lat: 23.03, lon: 72.58, count: 12 }];
|
|
136
|
+
el.addEventListener("geo-click", (e) => console.log(e.detail.marker));
|
|
137
|
+
</script>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Every scalar option is available as a dash-cased attribute (`auto-rotate`, `marker-scale`,
|
|
141
|
+
`radius-ratio`โฆ). `markers`, `arcs`, `country-colors` and `lat-range` accept JSON. Objects and
|
|
142
|
+
callbacks go through the `markers`, `arcs` and `options` properties. Events: `geo-hover`,
|
|
143
|
+
`geo-click`, `geo-country-hover`, `geo-country-click`, `geo-render`.
|
|
144
|
+
|
|
145
|
+
### React
|
|
146
|
+
|
|
147
|
+
```jsx
|
|
148
|
+
import { useRef } from "react";
|
|
149
|
+
import { Globe } from "canvas-globe/react";
|
|
150
|
+
|
|
151
|
+
export function Visitors({ markers }) {
|
|
152
|
+
const globe = useRef(null);
|
|
153
|
+
return (
|
|
154
|
+
<Globe
|
|
155
|
+
ref={globe}
|
|
156
|
+
markers={markers}
|
|
157
|
+
theme="midnight"
|
|
158
|
+
tooltip
|
|
159
|
+
onClick={(m) => globe.current.flyTo(m.lon, m.lat, { zoom: 3 })}
|
|
160
|
+
/>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
React is an optional peer dependency: only the `/react` entry point needs it.
|
|
166
|
+
|
|
167
|
+
## Options
|
|
168
|
+
|
|
169
|
+
| Option | Default | Description |
|
|
170
|
+
| --- | --- | --- |
|
|
171
|
+
| `licenseKey` | `"0000-0000-000-0000"` | The key supplied for a GPLv3-compatible project or with a commercial order |
|
|
172
|
+
| `mode` | `"globe"` | `"globe"` (orthographic, spinnable) or `"map"` (flat) |
|
|
173
|
+
| `projection` | `"equirectangular"` | Flat-map projection: also `"mercator"`, `"naturalEarth"` |
|
|
174
|
+
| `preset` | Not set | Named bundle of theme + render style, applied under your options |
|
|
175
|
+
| `theme` | `"atlas"` | Theme name or a partial theme object |
|
|
176
|
+
| `landStyle` | `"fill"` | `"fill"` \| `"dots"` \| `"outline"` \| `"glow"` |
|
|
177
|
+
| `dotSpacing` | `2` | Dot grid spacing in degrees |
|
|
178
|
+
| `dotSize` | `1.15` | Dot radius in px |
|
|
179
|
+
| `orbits` | `0` | Decorative rings: a count (0-6) or explicit specs |
|
|
180
|
+
| `texture` | Not set | Equirectangular image painted onto the sphere |
|
|
181
|
+
| `textureQuality` | `"auto"` | Pixel step for the texture pass; higher is faster |
|
|
182
|
+
| `focus` | Not set | Frame one country: `"IN"` or `{ country, isolate, dim, outlineWidth }` |
|
|
183
|
+
| `countryMedia` | Not set | Media clipped to each country, keyed by ISO, id or name |
|
|
184
|
+
| `scene` | Not set | Whole composition: preset plus the layers a job needs |
|
|
185
|
+
| `counter` | Not set | `{ value, label, format, position }` rolling headline number |
|
|
186
|
+
| `title` | Not set | `{ text, subtitle, position }` headline painted onto the canvas |
|
|
187
|
+
| `watermark` | Not set | `{ image, text, position, opacity }` logo baked into every export |
|
|
188
|
+
| `annotations` | Not set | `[{ lat, lon, text, dx, dy }]` leader-line callouts |
|
|
189
|
+
| `timeline` | Not set | `{ at }`: hides markers whose `date` has not arrived |
|
|
190
|
+
| `transparentBackground` | `false` | Skip the ocean fill so exports keep an alpha channel |
|
|
191
|
+
| `heatmap` | `false` | Additive density blobs: `{ radius, intensity, color }` |
|
|
192
|
+
| `spikes` | `false` | Bars off the surface, sized by `count`: `{ height, width }` |
|
|
193
|
+
| `labels` | `false` | `"markers"` \| `"countries"` \| `"both"`, with collision avoidance |
|
|
194
|
+
| `legend` | Not set | `{ title, items }` or `{ title, scale, position }` |
|
|
195
|
+
| `showViewer` | `false` | Pin the current viewer from their time zone |
|
|
196
|
+
| `momentum` | `true` | Coast after a drag instead of stopping dead |
|
|
197
|
+
| `countryPalette` | Not set | Fills used by `countryColors: "auto"` |
|
|
198
|
+
| `markers` | `[]` | See [Markers](#markers) |
|
|
199
|
+
| `arcs` | `[]` | See [Arcs](#arcs) |
|
|
200
|
+
| `center` | `{ lon: 10, lat: 20 }` | Initial view centre |
|
|
201
|
+
| `zoom` / `minZoom` / `maxZoom` | `1` / `1` / `8` | Zoom level and bounds |
|
|
202
|
+
| `zoomable` | `true` | Wheel and pinch zoom |
|
|
203
|
+
| `autoRotate` | `true` | Spin when idle |
|
|
204
|
+
| `rotateSpeed` | `0.09` | Degrees per frame |
|
|
205
|
+
| `interactive` | `true` | Drag, zoom, hover and click |
|
|
206
|
+
| `keyboard` | `true` | Arrow keys, `+`/`-`, `0`, `PageUp`/`PageDown` |
|
|
207
|
+
| `graticule` | `true` | Latitude/longitude grid |
|
|
208
|
+
| `stars` | `true` | Starfield around the globe |
|
|
209
|
+
| `shade` | `true` | Lit-from-upper-left shading |
|
|
210
|
+
| `terminator` | `false` | Shade the night side using the real solar position |
|
|
211
|
+
| `time` | `null` | Clock for the terminator; `null` tracks now |
|
|
212
|
+
| `markerStyle` | `"auto"` | `"auto"` \| `"bubble"` \| `"dot"` |
|
|
213
|
+
| `markerScale` | `1` | Scales every marker |
|
|
214
|
+
| `renderMarker` | Not set | `(ctx, marker, info) => radius`: draw markers yourself |
|
|
215
|
+
| `cluster` | `false` | Merge nearby markers into count bubbles |
|
|
216
|
+
| `clusterRadius` | `42` | Cluster grid size in px |
|
|
217
|
+
| `countryColors` | Not set | `{ IN: "#f00" }` keyed by ISO code, id or name, or `"auto"` |
|
|
218
|
+
| `countryColor` | Not set | `(shape) => color`: wins over `countryColors` |
|
|
219
|
+
| `countryKey` | Not set | `(shape) => key` used against `countryColors` |
|
|
220
|
+
| `arcLift` | `0.28` | Default arc height, as a fraction of the radius |
|
|
221
|
+
| `arcSpeed` | `1` | Multiplies every arc's travel speed |
|
|
222
|
+
| `radiusRatio` | `0.4` | Globe radius vs the smaller canvas side |
|
|
223
|
+
| `latRange` | `[83, -56]` | Map mode north/south bounds |
|
|
224
|
+
| `world` | bundled | Your own GeoJSON |
|
|
225
|
+
| `fps` | `30` | Frame cap |
|
|
226
|
+
| `tooltip` | `false` | `true`, or `(target, kind) => string` |
|
|
227
|
+
| `respectReducedMotion` | `true` | Honour `prefers-reduced-motion` |
|
|
228
|
+
| `ariaLabel` | `"Interactive world map"` | Accessible name for the canvas |
|
|
229
|
+
| `onHover` | Not set | `(marker \| null, { x, y } \| null) => void` |
|
|
230
|
+
| `onClick` | Not set | `(marker, { x, y }) => void` |
|
|
231
|
+
| `onCountryHover` | Not set | `(country \| null, { x, y } \| null) => void` |
|
|
232
|
+
| `onCountryClick` | Not set | `(country, { x, y }) => void` |
|
|
233
|
+
| `onRender` | Not set | Called after every frame |
|
|
234
|
+
|
|
235
|
+
## Markers
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
{
|
|
239
|
+
lat: number; // required
|
|
240
|
+
lon: number; // required
|
|
241
|
+
count?: number; // relative weight: bigger count, bigger marker
|
|
242
|
+
emoji?: string; // drawn inside a bubble marker
|
|
243
|
+
live?: boolean; // pulsing ring
|
|
244
|
+
color?: string; // overrides the theme colour
|
|
245
|
+
size?: number; // base radius, default 3.4
|
|
246
|
+
...anything // passed straight back to onHover / onClick
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
With `cluster: true`, dense areas collapse into a single bubble and your callbacks receive
|
|
251
|
+
`{ cluster: true, count, markers, lat, lon }` instead. Clustering happens in screen space, so it
|
|
252
|
+
re-balances automatically as you zoom.
|
|
253
|
+
|
|
254
|
+
### How accurate is marker placement?
|
|
255
|
+
|
|
256
|
+
The projection maths is exact: a marker's pixel position matches the closed-form projection to
|
|
257
|
+
floating-point precision, and the bubble is centred on the coordinate. Two things are worth knowing:
|
|
258
|
+
|
|
259
|
+
- **The coastlines are approximate, not the markers.** The bundled geometry is Natural Earth 1:110m,
|
|
260
|
+
decimated to a ~0.14ยฐ tolerance and rounded to two decimals, so the drawn shoreline can sit a few
|
|
261
|
+
kilometres from the real one. A coastal marker may look slightly offshore even though it is exactly
|
|
262
|
+
where you put it. Natural Earth 1:110m also omits microstates such as Singapore, Malta and Monaco: a marker there lands on open water or a neighbour. Pass higher-detail GeoJSON via `world` if that
|
|
263
|
+
matters.
|
|
264
|
+
- **`latRange` defaults to `[83, -56]`,** which trims the polar caps. Markers south of โ56ยฐ or north
|
|
265
|
+
of 83ยฐ project outside the drawn map. Use `latRange: [90, -90]` for a full-height map.
|
|
266
|
+
|
|
267
|
+
The antimeridian is handled: the map pans freely across ยฑ180ยฐ once zoomed, and coordinates are
|
|
268
|
+
wrapped around the view centre, so a marker at 179ยฐE and one at 179ยฐW render side by side.
|
|
269
|
+
|
|
270
|
+
## Arcs
|
|
271
|
+
|
|
272
|
+
```js
|
|
273
|
+
globe.setArcs([
|
|
274
|
+
{ from: { lat: 23.03, lon: 72.58 }, to: [-0.12, 51.5] },
|
|
275
|
+
{ from: [72.58, 23.03], to: [139.69, 35.68], color: "#f97316", duration: 3200 },
|
|
276
|
+
]);
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Arcs follow the great circle, bow above the surface by `lift`, animate a travelling head, and are
|
|
280
|
+
clipped at the horizon as the globe turns. Set `animate: false` for a static line.
|
|
281
|
+
|
|
282
|
+
## Choropleth
|
|
283
|
+
|
|
284
|
+
```js
|
|
285
|
+
import { createGlobe, colorScale } from "canvas-globe";
|
|
286
|
+
|
|
287
|
+
const visits = { IN: 940, US: 720, GB: 480, JP: 300 };
|
|
288
|
+
const scale = colorScale([0, 1000], ["#e0f2fe", "#0369a1"]);
|
|
289
|
+
|
|
290
|
+
createGlobe(canvas, {
|
|
291
|
+
mode: "map",
|
|
292
|
+
countryColors: Object.fromEntries(Object.entries(visits).map(([k, v]) => [k, scale(v)])),
|
|
293
|
+
onCountryClick: (shape) => console.log(shape.iso, shape.name),
|
|
294
|
+
});
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Keys are matched case-insensitively against the ISO alpha-2 code, then the numeric id, then the
|
|
298
|
+
country name. Pass `countryKey` if your data uses something else.
|
|
299
|
+
|
|
300
|
+
## Methods
|
|
301
|
+
|
|
302
|
+
```js
|
|
303
|
+
globe.setMarkers([...]); // swap the marker set
|
|
304
|
+
globe.setArcs([...]); // swap the arcs
|
|
305
|
+
globe.setMode("map"); // switch projection family
|
|
306
|
+
globe.setProjection("naturalEarth"); // switch flat projection
|
|
307
|
+
globe.setPreset("hologram"); // swap the whole look
|
|
308
|
+
globe.setLandStyle("dots"); // fill | dots | outline | glow
|
|
309
|
+
globe.setTheme("midnight"); // switch palette
|
|
310
|
+
globe.setTime(Date.UTC(2024, 5, 21)); // terminator clock; null tracks now
|
|
311
|
+
globe.setOptions({ autoRotate: false });
|
|
312
|
+
globe.setZoom(3);
|
|
313
|
+
globe.zoomBy(1.4);
|
|
314
|
+
globe.flyTo(139.69, 35.68); // ease to Tokyo
|
|
315
|
+
globe.flyTo(139.69, 35.68, { instant: true, zoom: 4 });
|
|
316
|
+
globe.fitTo([68, 6, 98, 36]); // frame [west, south, east, north]
|
|
317
|
+
globe.fitToMarkers(); // frame every marker
|
|
318
|
+
globe.focusOn("India", { isolate: true });
|
|
319
|
+
globe.setCountryMedia("India", "/reel.mp4");
|
|
320
|
+
globe.countryAspect("India"); // โ height / width ratio for the canvas
|
|
321
|
+
globe.clearFocus();
|
|
322
|
+
globe.setScene("logos"); // whole composition
|
|
323
|
+
globe.exportImage({ preset: "story", transparent: true });
|
|
324
|
+
await globe.exportBlob({ preset: "og" });
|
|
325
|
+
globe.setTimelineAt("2024-06-01");
|
|
326
|
+
globe.playTimeline({ duration: 6000 }); // โ { stop() }
|
|
327
|
+
globe.ping({ lat, lon, label }); // one-shot expanding ring
|
|
328
|
+
globe.pingFeed(events, { interval }); // โ { stop() }
|
|
329
|
+
globe.tour(points, { dwell }); // โ { stop() }
|
|
330
|
+
globe.story(el, steps); // scroll-linked view
|
|
331
|
+
globe.record({ duration, filename }); // โ { promise, stop() }
|
|
332
|
+
globe.locateViewer(); // โ { lat, lon, timeZone, country, source, accuracy }
|
|
333
|
+
globe.setTexture(imageOrUrl);
|
|
334
|
+
globe.getCenter(); // โ { lon, lat }
|
|
335
|
+
globe.project(lon, lat); // โ { x, y } or null if behind the globe
|
|
336
|
+
globe.unproject(x, y); // โ [lon, lat] or null
|
|
337
|
+
globe.countryAt(x, y); // โ country shape or null
|
|
338
|
+
globe.snapshot(); // โ PNG data URL, great for share images
|
|
339
|
+
await globe.toBlob(); // โ Blob
|
|
340
|
+
globe.invalidate(); // request one more frame
|
|
341
|
+
globe.resize(); // usually automatic via ResizeObserver
|
|
342
|
+
globe.destroy(); // stop the loop and remove listeners
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
Helpers are exported too: `mapAspect`, `colorScale`, `greatCircle`, `angularDistance`,
|
|
346
|
+
`subsolarPoint`, `pointInGeometry`, `geometryBounds`, `projections`, `themes`, `presets`, `scenes`,
|
|
347
|
+
`exportPresets`, `fromCSV`, `parseCSV`, `geocode`, `countryPoint`, `placeLocation`.
|
|
348
|
+
|
|
349
|
+
## Accessibility
|
|
350
|
+
|
|
351
|
+
The canvas gets `role="img"`, an `aria-label`, and, when `keyboard` is on, a tab stop plus a
|
|
352
|
+
polite live region that announces the view as it changes.
|
|
353
|
+
|
|
354
|
+
| Key | Action |
|
|
355
|
+
| --- | --- |
|
|
356
|
+
| `โ` `โ` `โ` `โ` | Rotate or pan (hold <kbd>Shift</kbd> for bigger steps) |
|
|
357
|
+
| `+` / `-` | Zoom in / out |
|
|
358
|
+
| `0` | Reset to the initial view |
|
|
359
|
+
| `PageDown` / `PageUp` | Cycle through markers, flying to each |
|
|
360
|
+
| `Enter` / `Space` | Activate the focused marker |
|
|
361
|
+
|
|
362
|
+
When the user prefers reduced motion, auto-rotation stops, `flyTo` jumps instead of easing, and
|
|
363
|
+
pulse rings and arc animations hold still. Set `respectReducedMotion: false` to opt out.
|
|
364
|
+
|
|
365
|
+
## Data in, assets out
|
|
366
|
+
|
|
367
|
+
Marketing data arrives as a spreadsheet, so `fromCSV` resolves rows itself: explicit `lat`/`lon`
|
|
368
|
+
columns first, then a city name, then a country code or name:
|
|
369
|
+
|
|
370
|
+
```js
|
|
371
|
+
import { fromCSV } from "canvas-globe";
|
|
372
|
+
|
|
373
|
+
const markers = fromCSV(`city,count,image
|
|
374
|
+
London,8,/logos/acme.png
|
|
375
|
+
Tokyo,4,/logos/globex.png`);
|
|
376
|
+
|
|
377
|
+
markers.skipped; // rows that could not be placed, so you can report them
|
|
378
|
+
globe.setMarkers(markers).fitToMarkers();
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
City lookup covers roughly 300 major cities that already ship with the package. Pass
|
|
382
|
+
`{ gazetteer: { Ahmedabad: [72.58, 23.03] } }` for anything else: no geocoding service, no key.
|
|
383
|
+
|
|
384
|
+
Render at whatever size the destination wants, without touching the live canvas:
|
|
385
|
+
|
|
386
|
+
```js
|
|
387
|
+
globe.exportImage({ preset: "story" }); // 1080ร1920 data URL
|
|
388
|
+
globe.exportImage({ preset: "linkedin", transparent: true });
|
|
389
|
+
await globe.exportBlob({ width: 2400, height: 1260 });
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Presets: `square`, `story`, `portrait`, `wide`, `linkedin`, `og`, `twitter`, `thumbnail`.
|
|
393
|
+
|
|
394
|
+
## Scenes
|
|
395
|
+
|
|
396
|
+
Presets decide how it looks; **scenes** decide what you are making. Each one bundles a preset with
|
|
397
|
+
the layers and overlays that job needs.
|
|
398
|
+
|
|
399
|
+
```js
|
|
400
|
+
createGlobe(canvas, { scene: "signups" });
|
|
401
|
+
globe.setScene("coverage");
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
| Scene | For |
|
|
405
|
+
| --- | --- |
|
|
406
|
+
| `signups` | Live activity on a pricing or landing page |
|
|
407
|
+
| `launch` | A regional announcement, ready for country media |
|
|
408
|
+
| `logos` | "Trusted in N countries" with customer logos |
|
|
409
|
+
| `team` | Where the team is, on a careers page |
|
|
410
|
+
| `coverage` | Campaign or revenue by country, with a legend |
|
|
411
|
+
| `review` | Scroll-linked year in review |
|
|
412
|
+
| `routes` | Traffic between regions |
|
|
413
|
+
|
|
414
|
+
Switching scenes resets every key the new scene does not set, so nothing leaks between them.
|
|
415
|
+
|
|
416
|
+
## Overlays
|
|
417
|
+
|
|
418
|
+
```js
|
|
419
|
+
createGlobe(canvas, {
|
|
420
|
+
counter: { value: 21947, label: "customers worldwide" }, // rolls when it changes
|
|
421
|
+
title: { text: "Trusted in 68 countries", subtitle: "Join 21,947 teams" },
|
|
422
|
+
watermark: { image: "/logo.svg", text: "acme.com" }, // baked into every export
|
|
423
|
+
annotations: [{ lat: 23.03, lon: 72.58, text: "HQ: Ahmedabad" }],
|
|
424
|
+
timeline: { at: "2024-06-01" }, // hides later markers
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
globe.playTimeline({ duration: 6000, loop: true }); // "our growth, animated"
|
|
428
|
+
globe.ping({ lat, lon, label: "10,000 users ๐", burst: 18 });
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
Markers take `image` for a circular logo or avatar crop, and arcs take `icon` for a travelling
|
|
432
|
+
glyph. Country media accepts `{ text }` to cut type out of a country's outline.
|
|
433
|
+
|
|
434
|
+
## Country canvas
|
|
435
|
+
|
|
436
|
+
Frame one country and paint media inside its outline: a still, an animated GIF, a video, another
|
|
437
|
+
canvas, or a live `MediaStream`:
|
|
438
|
+
|
|
439
|
+
```js
|
|
440
|
+
globe.focusOn("India", { isolate: true });
|
|
441
|
+
globe.setCountryMedia("India", "/launch-reel.mp4");
|
|
442
|
+
|
|
443
|
+
// or declaratively
|
|
444
|
+
createGlobe(canvas, {
|
|
445
|
+
mode: "map",
|
|
446
|
+
focus: { country: "IN", isolate: true, outlineWidth: 2 },
|
|
447
|
+
countryMedia: {
|
|
448
|
+
IN: { src: "/reel.mp4", fit: "cover" },
|
|
449
|
+
BR: "/photo.jpg",
|
|
450
|
+
},
|
|
451
|
+
});
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
The media is clipped to the bundled country outline, including islands. Sources resolve
|
|
455
|
+
automatically: `.mp4`/`.webm` become looping muted video, `.gif` keeps animating, and anything
|
|
456
|
+
`drawImage` accepts can be passed directly. `fit` mirrors CSS `object-fit`, and `opacity`, `blend`,
|
|
457
|
+
`scale` and `offset` are available per country.
|
|
458
|
+
|
|
459
|
+
`focusOn` zooms past `maxZoom` when it has to, since framing a country is an explicit request.
|
|
460
|
+
`countryAspect("India")` returns the height/width ratio to size the canvas with, so the shape is not
|
|
461
|
+
letterboxed:
|
|
462
|
+
|
|
463
|
+
```css
|
|
464
|
+
#map { width: 100%; aspect-ratio: var(--country-aspect, 1); }
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
Without `isolate`, neighbours stay visible at `dim` opacity, which reads well for "our market" maps.
|
|
468
|
+
|
|
469
|
+
## Where is the viewer?
|
|
470
|
+
|
|
471
|
+
```js
|
|
472
|
+
createGlobe(canvas, { showViewer: true });
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
That pins the person looking at the page: **no permission prompt, no network call, no API key,
|
|
476
|
+
instantly**. It reads `Intl.DateTimeFormat().resolvedOptions().timeZone`, which every browser
|
|
477
|
+
exposes, and maps it to the coordinate the IANA database publishes for that zone. Legacy aliases
|
|
478
|
+
resolve too (Chrome often reports `Asia/Calcutta`, not `Asia/Kolkata`).
|
|
479
|
+
|
|
480
|
+
```js
|
|
481
|
+
const found = globe.locateViewer();
|
|
482
|
+
// { lat: 23.29, lon: 82.52, timeZone: "Asia/Kolkata", country: "IN",
|
|
483
|
+
// source: "timezone", accuracy: "region", accuracyMeters: 2242000 }
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
**It shows a region, not a pinpoint, and represents that uncertainty.** A time zone only narrows you to
|
|
487
|
+
its area, and the tz database publishes one representative city per zone. `Asia/Kolkata` covers all
|
|
488
|
+
of India, so a naive pin would sit confidently on Kolkata even for someone in Ahmedabad, 1,600 km
|
|
489
|
+
away. Two things prevent that:
|
|
490
|
+
|
|
491
|
+
- **Anchoring.** For countries wider than 8ยฐ the pin goes on the country centroid instead of the
|
|
492
|
+
zone's city, which roughly halves the average error. Smaller countries keep their real city.
|
|
493
|
+
Override with `anchor: "country" | "timezone"`.
|
|
494
|
+
- **An uncertainty circle.** The pin is surrounded by a dashed circle sized to the actual radius, so
|
|
495
|
+
the graphic says "somewhere in here" rather than "exactly here". Turn it off with
|
|
496
|
+
`accuracyCircle: false`.
|
|
497
|
+
|
|
498
|
+
When you need a real position, ask for one:
|
|
499
|
+
|
|
500
|
+
```js
|
|
501
|
+
const found = await globe.locateViewer({ precise: true });
|
|
502
|
+
globe.setViewerLocation(found);
|
|
503
|
+
// source: "geolocation", accuracyMeters: 24; the circle shrinks to match
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
That requests the high-accuracy provider and reports the device's own `accuracyMeters`, so you can
|
|
507
|
+
tell a 20 m GPS fix from a 40 km Wi-Fi one. On a desktop with no GPS the browser falls back to
|
|
508
|
+
network positioning, which often lands on your ISP's city: the radius will say so. If the viewer
|
|
509
|
+
declines, it resolves to the time-zone estimate and never rejects.
|
|
510
|
+
|
|
511
|
+
| | Prompt | Network | Always works | Typical radius |
|
|
512
|
+
| --- | --- | --- | --- | --- |
|
|
513
|
+
| Time zone (default) | No | No | Yes | Country-sized |
|
|
514
|
+
| Locale fallback | No | No | Yes | Country-sized |
|
|
515
|
+
| `precise: true`, GPS | Yes | No | Only if allowed | 5-50 m |
|
|
516
|
+
| `precise: true`, Wi-Fi/IP | Yes | Yes (by the browser) | Only if allowed | 1-50 km |
|
|
517
|
+
|
|
518
|
+
Options: `showViewer: { emoji, label, color, live, anchor, accuracyCircle, accuracyColor, flyTo,
|
|
519
|
+
ping, precise, onLocate }`. The pin lives outside `markers`, so `setMarkers()` never wipes it.
|
|
520
|
+
|
|
521
|
+
## Live pings
|
|
522
|
+
|
|
523
|
+
A one-shot expanding ring: the social-proof moment, with no backend:
|
|
524
|
+
|
|
525
|
+
```js
|
|
526
|
+
globe.ping({ lat: 52.52, lon: 13.4, emoji: "โจ", label: "Someone in Berlin just signed up" });
|
|
527
|
+
|
|
528
|
+
const feed = globe.pingFeed(events, { interval: 1800 });
|
|
529
|
+
feed.stop();
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
## Tour, story and recording
|
|
533
|
+
|
|
534
|
+
```js
|
|
535
|
+
globe.tour(cities, { dwell: 2600, zoom: 2.2 }); // cinematic auto-fly, returns { stop() }
|
|
536
|
+
|
|
537
|
+
globe.story(section, [ // scroll-linked rotation
|
|
538
|
+
{ at: 0, center: [0, 20], zoom: 1 },
|
|
539
|
+
{ at: 0.5, center: [72, 23], zoom: 3, markers: indiaMarkers, preset: "hologram" },
|
|
540
|
+
]);
|
|
541
|
+
|
|
542
|
+
await globe.record({ duration: 6000, filename: "globe.webm" }).promise;
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
`record()` uses `MediaRecorder` on the canvas stream: the clip is encoded in the tab and never
|
|
546
|
+
leaves the device. Check the exported `canRecord()` helper first.
|
|
547
|
+
|
|
548
|
+
## Looks
|
|
549
|
+
A **preset** bundles a theme with a render style. Your own options always win over it.
|
|
550
|
+
|
|
551
|
+
```js
|
|
552
|
+
createGlobe(canvas, { preset: "hologram" });
|
|
553
|
+
globe.setPreset("neon");
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
| Preset | Look |
|
|
557
|
+
| --- | --- |
|
|
558
|
+
| `atlas` | Bright cartographic globe (the default) |
|
|
559
|
+
| `midnight` | Dark space theme |
|
|
560
|
+
| `mono` | Neutral greyscale |
|
|
561
|
+
| `political` | Printed atlas: a distinct colour per country |
|
|
562
|
+
| `hologram` | Cyan dot-matrix earth on deep navy |
|
|
563
|
+
| `neon` | Glowing magenta continents with a cyan rim |
|
|
564
|
+
| `blueprint` | Technical line art with orbit rings |
|
|
565
|
+
| `aurora` | Green dot matrix with violet rings |
|
|
566
|
+
| `noir` | High-contrast black and white |
|
|
567
|
+
| `constellation` | Sparse dots, stars and three orbits |
|
|
568
|
+
|
|
569
|
+
The pieces compose independently, so any theme mixes with any style:
|
|
570
|
+
|
|
571
|
+
| Option | Values |
|
|
572
|
+
| --- | --- |
|
|
573
|
+
| `landStyle` | `"fill"` ยท `"dots"` (halftone) ยท `"outline"` (line art) ยท `"glow"` (neon) ยท `"none"` |
|
|
574
|
+
| `dotSpacing` / `dotSize` | Grid spacing in degrees and dot radius in px |
|
|
575
|
+
| `orbits` | A count (0-6), or `{ inclination, phase, radius, speed, color, width }` rings |
|
|
576
|
+
| `countryColors: "auto"` | A distinct fill per country; `countryPalette` supplies your own |
|
|
577
|
+
|
|
578
|
+
```js
|
|
579
|
+
createGlobe(canvas, { theme: "midnight", landStyle: "dots", dotSpacing: 2.4, orbits: 3 });
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
Dots come from rasterising the land once into an off-screen bitmap and sampling a grid, so the whole
|
|
583
|
+
matrix draws in a single fill and re-spacing is cheap. Orbit rings are real great circles, so they
|
|
584
|
+
pass behind the globe as it turns. `countryColors: "auto"` runs a greedy graph colouring over country
|
|
585
|
+
adjacency, so neighbours never share a fill.
|
|
586
|
+
|
|
587
|
+
## Themes
|
|
588
|
+
|
|
589
|
+
`atlas`, `midnight`, `mono`, `hologram`, `neon`, `blueprint`, `aurora`, `noir` and `political`.
|
|
590
|
+
Use `theme: "auto"` to follow the OS colour scheme, or `theme: "css"` to read `--geo-*` custom
|
|
591
|
+
properties off the canvas so the globe inherits your design tokens:
|
|
592
|
+
|
|
593
|
+
```css
|
|
594
|
+
#globe { --geo-land: #334155; --geo-ocean-from: #0f172a; --geo-ocean-to: #020617; }
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
Override any subset directly too:
|
|
598
|
+
|
|
599
|
+
```js
|
|
600
|
+
createGlobe(canvas, {
|
|
601
|
+
theme: { ocean: ["#0f172a", "#020617"], land: "#334155", marker: "#f97316", arc: "#22d3ee" },
|
|
602
|
+
});
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
## Custom geometry
|
|
606
|
+
|
|
607
|
+
The bundled data is Natural Earth 1:110m, simplified for smooth animation. Swap in anything GeoJSON:
|
|
608
|
+
|
|
609
|
+
```js
|
|
610
|
+
const world = await fetch("/my-countries.geojson").then((r) => r.json());
|
|
611
|
+
createGlobe(canvas, { world });
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
## Performance
|
|
615
|
+
|
|
616
|
+
Rendering is capped at 30 fps, and frames are skipped entirely when nothing is moving: a static
|
|
617
|
+
chart costs nothing after the first paint. Geometry behind the horizon is clipped away rather than
|
|
618
|
+
drawn, so a typical globe frame skips 20-60% of the world. On a laptop a 560 px canvas costs roughly
|
|
619
|
+
4 ms per frame, or 25 ms with 5,000 clustered markers.
|
|
620
|
+
|
|
621
|
+
## Data & licences
|
|
622
|
+
|
|
623
|
+
- Country geometry: [Natural Earth](https://www.naturalearthdata.com/) 1:110m via `world-atlas`, **public domain**
|
|
624
|
+
- ISO codes: [natural-earth-vector](https://github.com/nvkelso/natural-earth-vector), **public domain**
|
|
625
|
+
- Supplemental geometry: [Datameet maps](https://github.com/datameet/maps), **CC-0**
|
|
626
|
+
- This package: **GPL-3.0-only or a commercial license**
|
|
627
|
+
|
|
628
|
+
See [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) for source links and
|
|
629
|
+
provenance.
|
|
630
|
+
|
|
631
|
+
Regenerate the bundled data any time with `npm run data`.
|
|
632
|
+
|
|
633
|
+
## Development
|
|
634
|
+
|
|
635
|
+
```bash
|
|
636
|
+
npm test # node --test, no test framework to install
|
|
637
|
+
npm run build # dist/canvas-globe.umd.js, with a gzipped size budget
|
|
638
|
+
npm run example # demo at http://localhost:8099
|
|
639
|
+
npm run release:check # tests, types, build and packed-artifact validation
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
The product website and documentation are maintained separately at
|
|
643
|
+
[canvasglobe.swiftools.com](https://canvasglobe.swiftools.com/).
|
|
644
|
+
|
|
645
|
+
## Support
|
|
646
|
+
|
|
647
|
+
For installation help, licensing questions, commercial inquiries, or general
|
|
648
|
+
support, email [globe@swiftools.com](mailto:globe@swiftools.com).
|
|
649
|
+
|
|
650
|
+
Found a bug or have an idea? Use the repository's guided issue forms. Built
|
|
651
|
+
something with CanvasGlobe? Submit it to the
|
|
652
|
+
[community showcase](https://canvasglobe.swiftools.com/showcase); projects are
|
|
653
|
+
only displayed after the owner grants permission.
|
|
654
|
+
|
|
655
|
+
## Author
|
|
656
|
+
|
|
657
|
+
Harsh Jhunjhunuwala
|
|
658
|
+
|
|
659
|
+
Copyright (C) 2026 Harsh Jhunjhunuwala. CanvasGlobe is published under the
|
|
660
|
+
Swiftools brand.
|
|
661
|
+
|
|
662
|
+
## Browser support
|
|
663
|
+
|
|
664
|
+
Any browser with `<canvas>` and `ResizeObserver`: Chrome, Edge, Firefox, Safari 13.1+. No polyfills
|
|
665
|
+
required. The modules load safely during SSR; nothing touches the DOM until you construct an instance.
|