Rocket Globe Pro
Rocket is currently in alpha – available in the Datastar Pro repo.
An interactive 3D globe visualization using the Globe.GL library.
Explanation #
The Globe component showcases Rocket’s ability to integrate complex 3D visualization libraries. It uses Globe.GL to render an interactive 3D Earth with markers, arcs, and paths.
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 #
1<template data-rocket:rocket-globe
2 data-schema:lat-lng="arr (num (clamp -90 90)) (num (clamp -180 180))"
3 data-prop:paths="arr ((arr latLng))"
4 data-prop:target="latLng (= [36,-115])"
5 data-prop:places="arr ((arr str latLng))"
6 data-prop:arcs="arr ((arr latLng latLng))"
7 data-import:globe="https://cdn.jsdelivr.net/npm/[email protected]/+esm"
8>
9 <script data-static>
10 const markerSvg = `<svg viewBox="-4 0 36 36" width="32" height="32">
11 <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"/>
12 <circle fill="black" cx="14" cy="14" r="7"/>
13 </svg>`
14 </script>
15
16 <script>
17
18 // Initialize globe - DOM is ready when setup runs
19 const w = $$container.offsetWidth
20 const h = $$container.offsetHeight
21 const alt = w / h > 1.5 ? 0.8 : w / h > 1 ? 1.2 : 1.5
22
23 const readTarget = () => [$$target[0], $$target[1]]
24
25 const initialTarget = readTarget()
26
27 // Globe.gl needs double call: globe() returns factory, factory(container) creates instance
28 const g = globe()($$container)
29 .width(w).height(h)
30 .globeImageUrl('https://unpkg.com/three-globe/example/img/earth-blue-marble.jpg')
31 .bumpImageUrl('https://unpkg.com/three-globe/example/img/earth-topology.png')
32 .pointOfView({ lat: initialTarget[0], lng: initialTarget[1], altitude: alt })
33
34 // Use watch pattern - create computed signals that only fire when value actually changes
35 let prevPathsStr = ''
36 let prevTargetStr = ''
37 let prevArcsStr = ''
38 let prevPlacesStr = ''
39
40 // Watch paths
41 effect(() => {
42 const pathsStr = JSON.stringify($$paths)
43 if (pathsStr !== prevPathsStr) {
44 prevPathsStr = pathsStr
45 const paths = JSON.parse(pathsStr)
46 if (paths.length > 0) {
47 g.pathsData(paths)
48 .pathPointLat((point) => point[0])
49 .pathPointLng((point) => point[1])
50 .pathColor(() => '#ff0000')
51 .pathStroke(0.5)
52 } else {
53 g.pathsData([])
54 }
55 }
56 })
57
58 // Watch target
59 effect(() => {
60 const target = readTarget()
61 const targetStr = JSON.stringify(target)
62 if (targetStr !== prevTargetStr) {
63 prevTargetStr = targetStr
64 const w = $$container.offsetWidth
65 const h = $$container.offsetHeight
66 const alt = w / h > 1.5 ? 0.8 : w / h > 1 ? 1.2 : 1.5
67 g.pointOfView({ lat: target[0], lng: target[1], altitude: alt }, 2000)
68 }
69 })
70
71 // Watch arcs
72 effect(() => {
73 const arcsStr = JSON.stringify($$arcs)
74 if (arcsStr !== prevArcsStr) {
75 prevArcsStr = arcsStr
76 const arcs = JSON.parse(arcsStr)
77 if (arcs.length > 0) {
78 g.arcsData(arcs)
79 .arcStartLat((arc) => arc[0][0])
80 .arcStartLng((arc) => arc[0][1])
81 .arcEndLat((arc) => arc[1][0])
82 .arcEndLng((arc) => arc[1][1])
83 .arcDashLength(0.5)
84 .arcDashGap(0.5)
85 .arcDashAnimateTime(1000)
86 } else {
87 g.arcsData([])
88 }
89 }
90 })
91
92 // Watch places
93 effect(() => {
94 const placesStr = JSON.stringify($$places)
95 if (placesStr !== prevPlacesStr) {
96 prevPlacesStr = placesStr
97 const places = JSON.parse(placesStr)
98 if (!places.length) {
99 g.htmlElementsData([])
100 return
101 }
102 g.htmlElementsData(
103 places
104 .map(([name, [lat, lng]]) => ({
105 name,
106 lat,
107 lng,
108 }))
109 )
110 .htmlLat(d => d.lat)
111 .htmlLng(d => d.lng)
112 .htmlElement(d => {
113 const wrapper = document.createElement('div')
114 wrapper.className = 'marker-wrapper'
115 wrapper.style.cursor = 'pointer'
116 wrapper.style.pointerEvents = 'auto'
117
118 const icon = document.createElement('div')
119 icon.className = 'marker-icon'
120 icon.style.width = '32px'
121 icon.style.height = '32px'
122 icon.innerHTML = markerSvg
123
124 const label = document.createElement('div')
125 label.className = 'marker-label'
126 label.textContent = d.name
127
128 wrapper.appendChild(icon)
129 wrapper.appendChild(label)
130
131 // Handle marker click
132 wrapper.addEventListener('click', (e) => {
133 e.preventDefault()
134 e.stopPropagation()
135 el.dispatchEvent(new CustomEvent('marker-click', {
136 detail: {
137 name: d.name,
138 lat: d.lat,
139 lng: d.lng
140 },
141 bubbles: true,
142 composed: true
143 }))
144 })
145
146 return wrapper
147 })
148 }
149 })
150
151 // Cleanup on disconnect
152 onCleanup(() => g?._destructor?.())
153 </script>
154
155 <style>
156 :host {
157 display: block;
158 width: 100%;
159 height: 100%;
160 position: relative;
161 overflow: hidden;
162 }
163
164 .globe-container {
165 position: absolute;
166 inset: 0;
167 overflow: hidden;
168 }
169
170 .marker-wrapper {
171 display: flex;
172 flex-direction: column;
173 align-items: center;
174 transform: translate(-50%, -100%);
175 position: relative;
176 z-index: 1;
177
178 .marker-icon {
179 width: 32px;
180 height: 32px;
181 color: #9333ea;
182 filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));
183
184 svg {
185 width: 100%;
186 height: 100%;
187 display: block;
188 }
189 }
190
191 .marker-label {
192 margin-top: 4px;
193 padding: 4px 8px;
194 background: linear-gradient(135deg, #9333ea, #7c3aed);
195 color: white;
196 border-radius: 4px;
197 font-size: 12px;
198 white-space: nowrap;
199 cursor: pointer;
200 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
201 font-weight: 600;
202
203 &:hover {
204 background: linear-gradient(135deg, var(--purple-10), var(--purple-11));
205 transform: scale(1.05);
206 box-shadow: var(--shadow-4);
207 }
208 }
209 }
210 </style>
211
212 <div class="globe-container" data-ref="container"></div>
213</template>