Rocket Globe Pro

Rocket is currently in alpha – available in the Datastar Pro repo.

An interactive 3D globe visualization using the Globe.GL library.

Demo

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.

 1let prevArcsStr = ''
 2
 3effect(() => {
 4    const arcsStr = JSON.stringify($$arcs || [])
 5    if (arcsStr !== prevArcsStr) {
 6    prevArcsStr = arcsStr
 7    const arcs = JSON.parse(arcsStr)
 8    // Only update when value actually changed
 9    g.arcsData(arcs)
10    }
11})

This pattern ensures the effect only runs when the actual data changes, not on every reactive read.

Usage Example #

1<rocket-globe
2    data-attr:places='[
3        ["New York", [40.7128, -74.0060]],
4        ["London", [51.5074, -0.1278]]
5    ]'
6    data-attr:arcs='[
7        [[40.7128, -74.0060], [51.5074, -0.1278]]
8    ]'
9></rocket-globe>

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>