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