Rocket Globe Pro

Rocket is currently in alpha – available with Datastar Pro.

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": {"lat": 40.7128, "lng": -74.0060},
4        "London": {"lat": 51.5074, "lng": -0.1278}
5    }'
6    data-attr:arcs='[
7        {"startLat": 40.7128, "startLng": -74.0060, "endLat": 51.5074, "endLng": -0.1278}
8    ]'
9></rocket-globe>

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>