Rocket Globe Pro
Rocket is currently in beta.
An interactive 3D globe visualization using the Globe.GL library.
Explanation #
The page-level controls own the target, places, and arcs, while the component mounts Globe.GL from a rendered ref in onFirstRender(). After that first mount, prop observers keep the globe instance in sync without rerendering the container.
Performance Tips for Complex Props #
When working with complex reactive objects or arrays that update frequently, you may encounter performance issues where effects fire repeatedly even when values haven’t changed. This happens because reactive proxies can trigger updates on every reactive read.
Solution:
Use the “stringify and compare” pattern to prevent unnecessary updates.
This pattern ensures the effect only runs when the actual data changes, not on every reactive read.
Usage Example #
Rocket Component #
1import { rocket } from 'datastar'
2
3const { default: Globe } = await import(
4 'https://cdn.jsdelivr.net/npm/[email protected]/+esm'
5)
6
7rocket('globe-view', {
8 props: ({ json }) => ({
9 paths: json.default([]),
10 target: json.default([36, -115]),
11 places: json.default([]),
12 arcs: json.default([]),
13 }),
14 onFirstRender: ({ refs, cleanup, host, observeProps, props }) => {
15 let globe
16
17 const target = () => ({
18 lat: props.target?.[0] ?? 36,
19 lng: props.target?.[1] ?? -115,
20 altitude:
21 (refs.container?.offsetWidth ?? 1) /
22 (refs.container?.offsetHeight ?? 1) >
23 1.5
24 ? 0.8
25 : (refs.container?.offsetWidth ?? 1) /
26 (refs.container?.offsetHeight ?? 1) >
27 1
28 ? 1.2
29 : 1.5,
30 })
31 const syncTarget = () => {
32 if (!globe) return
33 globe.pointOfView(target(), 2000)
34 }
35 const syncPaths = () => {
36 if (!globe) return
37 if (props.paths?.length) {
38 globe
39 .pathsData(props.paths)
40 .pathPointLat((point) => point[0])
41 .pathPointLng((point) => point[1])
42 .pathColor(() => '#ff0000')
43 .pathStroke(0.5)
44 return
45 }
46 globe.pathsData([])
47 }
48 const syncArcs = () => {
49 if (!globe) return
50 if (props.arcs?.length) {
51 globe
52 .arcsData(props.arcs)
53 .arcStartLat((arc) => arc[0][0])
54 .arcStartLng((arc) => arc[0][1])
55 .arcEndLat((arc) => arc[1][0])
56 .arcEndLng((arc) => arc[1][1])
57 .arcDashLength(0.5)
58 .arcDashGap(0.5)
59 .arcDashAnimateTime(1000)
60 return
61 }
62 globe.arcsData([])
63 }
64 const syncPlaces = () => {
65 if (!globe) return
66 if (!props.places?.length) {
67 globe.htmlElementsData([])
68 return
69 }
70 globe
71 .htmlElementsData(
72 props.places.map(([name, [lat, lng]]) => ({ name, lat, lng })),
73 )
74 .htmlLat((entry) => entry.lat)
75 .htmlLng((entry) => entry.lng)
76 .htmlElement((entry) => {
77 const wrapper = document.createElement('div')
78 wrapper.className = 'marker-wrapper'
79 wrapper.style.cursor = 'pointer'
80 wrapper.style.pointerEvents = 'auto'
81
82 const icon = document.createElement('div')
83 icon.className = 'marker-icon'
84 icon.style.width = '32px'
85 icon.style.height = '32px'
86 icon.innerHTML = `<svg viewBox="-4 0 36 36" width="32" height="32"><path fill="currentColor" d="M14,0 C21.732,0 28,5.641 28,12.6 C28,23.963 14,36 14,36 C14,36 0,24.064 0,12.6 C0,5.641 6.268,0 14,0 Z"/><circle fill="black" cx="14" cy="14" r="7"/></svg>`
87
88 const label = document.createElement('div')
89 label.className = 'marker-label'
90 label.textContent = entry.name
91
92 wrapper.append(icon, label)
93 wrapper.addEventListener('click', (evt) => {
94 evt.preventDefault()
95 evt.stopPropagation()
96 host.dispatchEvent(
97 new CustomEvent('marker-click', {
98 detail: { name: entry.name, lat: entry.lat, lng: entry.lng },
99 bubbles: true,
100 composed: true,
101 }),
102 )
103 })
104 return wrapper
105 })
106 }
107
108 if (refs.container instanceof HTMLElement) {
109 globe = Globe()(refs.container)
110 .width(refs.container.offsetWidth)
111 .height(refs.container.offsetHeight)
112 .globeImageUrl(
113 'https://unpkg.com/three-globe/example/img/earth-blue-marble.jpg',
114 )
115 .bumpImageUrl(
116 'https://unpkg.com/three-globe/example/img/earth-topology.png',
117 )
118 .pointOfView(target(), 0)
119 syncPaths()
120 syncTarget()
121 syncArcs()
122 syncPlaces()
123 }
124
125 observeProps(syncPaths, 'paths')
126 observeProps(syncTarget, 'target')
127 observeProps(syncArcs, 'arcs')
128 observeProps(syncPlaces, 'places')
129
130 cleanup(() => globe?._destructor?.())
131 },
132 render: ({ html }) => html`
133 <style>
134 :host {
135 display: block;
136 width: 100%;
137 height: 100%;
138 position: relative;
139 overflow: hidden;
140 }
141
142 .globe-container {
143 position: absolute;
144 inset: 0;
145 overflow: hidden;
146 }
147
148 .marker-wrapper {
149 display: flex;
150 flex-direction: column;
151 align-items: center;
152 transform: translate(-50%, -100%);
153 position: relative;
154 z-index: 1;
155
156 .marker-icon {
157 width: 32px;
158 height: 32px;
159 color: #9333ea;
160 filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));
161
162 svg {
163 width: 100%;
164 height: 100%;
165 display: block;
166 }
167 }
168
169 .marker-label {
170 margin-top: 4px;
171 padding: 4px 8px;
172 background: linear-gradient(135deg, #9333ea, #7c3aed);
173 color: white;
174 border-radius: 4px;
175 font-size: 12px;
176 white-space: nowrap;
177 cursor: pointer;
178 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
179 font-weight: 600;
180
181 &:hover {
182 background: linear-gradient(
183 135deg,
184 var(--purple-10),
185 var(--purple-11)
186 );
187 transform: scale(1.05);
188 box-shadow: var(--shadow-4);
189 }
190 }
191 }
192 </style>
193 <div class="globe-container" data-ref:container data-ignore-morph></div>
194 `,
195})