Recreating Shopify’s BFCM Globe Using react-globe.gl
Did you see this year’s BFCM (Black Friday Cyber Monday) Globe from Shopify? I did, and loved it. Every year the Shopify team knocks it out of the park and this year was no exception! However, unless you possess NASA level WebGL engineering skills, creating a 3D globe is hard. Fortunately react-globe.gl by Vasco Asturiano is here to help. I’ve used this library on a number of occasions, but this time I wanted to see how close I could get to Shopify’s BFCM ‘23 globe.
You can see a preview and all the code referenced in this post on the links below.
- 🚀 Preview: https://tns-react-3d-globe.netlify.app
- ⚙️ Code: https://github.com/PaulieScanlon/tns-react-3d-globe
But before I explain how I created the Globe, I’ll talk you through the fundamentals of using react-globe-gl.
Installation
To use react-globe.gl, you’ll first need to install it.
1 |
npm install react-globe.gl |
Once the package has been installed, you’ll need to import it.
1 2 3 4 5 6 7 8 |
import Globe from 'react-globe.gl'; const Page = () => { return null } export default Page |
Basic Image
In this example, I’ll cover the basics of creating the Globe, adding an image so the globe looks like earth, and how you can plot points around the globe.
- 🚀 Preview: https://tns-react-3d-globe.netlify.app/basic-image
- ⚙️ Code: src/routes/basic-image.jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
// src/routes/basic-image.jsx import Globe from 'react-globe.gl'; import globeImage from '../assets/earth-dark.jpg'; const Page = () => { const myData = [ { lat: 29.953204744601763, lng: -90.08925929478903, altitude: 0.4, color: '#00ff33', }, { lat: 28.621322361013092, lng: 77.20347613099612, altitude: 0.4, color: '#ff0000', }, { lat: -43.1571459086602, lng: 172.72338919659848, altitude: 0.4, color: '#ffff00', }, ]; return ( <div className='cursor-move'> <Globe globeImageUrl={globeImage} pointsData={myData} pointAltitude='altitude' pointColor='color' /> </div> ); }; export default Page; |
globeImageUrl
Using this prop you can pass a real earth image to be rendered on the globe. To ensure real latitude and longitude points line up with the countries, the earth image needs to be prepared correctly. At the following link, you’ll find a number of variations to choose from; all will work with react-globe.gl: https://unpkg.com/browse/world-atlas@2.0.2/
pointsData
This array of data holds four key value pairs. Each should be self explanatory enough and when passed on to the globe using pointsData prop, will appear in their correction geographical locations.
pointAltitude
This prop allows you to set the “size” of each of the points. The higher the number, the “taller” the point. The string passed to this prop is a key name from the data array.
pointColor
This prop allows you to set the color for each of the points. The string passed to this prop is a key name from the data array.
GeoJson Hexagon
In this example, I use geojson data to display the countries of the world as hexagons, instead of using an image of earth.
- 🚀 Preview: https://tns-react-3d-globe.netlify.app/geojson-hexagon
- ⚙️ Code: src/routes/geojson-hexagon.jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
// src/routes/geojson-hexagon.jsx import Globe from 'react-globe.gl'; import globeJson from '../assets/countries_110m.json'; const Page = () => { const myData = [ { lat: 29.953204744601763, lng: -90.08925929478903, altitude: 0.4, color: '#00ff33', }, { lat: 28.621322361013092, lng: 77.20347613099612, altitude: 0.4, color: '#ff0000', }, { lat: -43.1571459086602, lng: 172.72338919659848, altitude: 0.4, color: '#ffff00', }, ]; return ( <div className='cursor-move'> <Globe hexPolygonsData={globeJson.features} hexPolygonColor={(geometry) => { return ['#0000ff', '#0000cc', '#000099', '#000066'][geometry.properties.abbrev_len % 4]; }} pointsData={myData} pointAltitude='altitude' pointColor='color' /> </div> ); }; export default Page; |
hexPolygonsData
Using this prop you can pass real world geojson data to be rendered on the globe, instead of an image. On the following link you’ll find a number of data sets that can be used: https://unpkg.com/browse/world-atlas@2.0.2/
hexPolygonColor
This prop can be used to change the color of each country. To determine which color from the first array to use, I use JavaScript’s Remainder, or modulo operator to create an index based on the length of the countries’ abbreviation codes that are returned from the geometry parameter.
Geojson Polygon
In this example, I use geojson data to display the countries of the world as polygons, instead of using an image of Earth.
- 🚀 Preview: https://tns-react-3d-globe.netlify.app/geojson-polygon
- ⚙️ Code: src/routes/geojson-polygon.jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
// src/routes/geojson-polygon.jsx import Globe from 'react-globe.gl'; import globeJson from '../assets/countries_110m.json'; const Page = () => { const myData = [ { lat: 29.953204744601763, lng: -90.08925929478903, altitude: 0.4, color: '#00ff33', }, { lat: 28.621322361013092, lng: 77.20347613099612, altitude: 0.4, color: '#ff0000', }, { lat: -43.1571459086602, lng: 172.72338919659848, altitude: 0.4, color: '#ffff00', }, ]; return ( <div className='cursor-move'> <Globe polygonsData={globeJson.features} polygonCapColor={(geometry) => { return ['#0000ff', '#0000cc', '#000099', '#000066'][geometry.properties.abbrev_len % 4]; }} polygonSideColor={(geometry) => { return ['#0000ff', '#0000cc', '#000099', '#000066'][geometry.properties.abbrev_len % 4]; }} polygonAltitude={0.08} pointsData={myData} pointAltitude='altitude' pointColor='color' /> </div> ); }; export default Page; |
polygonsData
Using this prop you can pass real world geojson data to be rendered on the globe, instead of an image. On the following link you’ll find a number of data sets that can be used: https://unpkg.com/browse/world-atlas@2.0.2/
polygonCapColor
This prop can be used to change the color of each country. To determine which color from the first array to use I use JavaScript’s Remainder, or modulo operator to create an index based on the length of the countries’ abbreviation codes that are returned from the geometry parameter.
polygonSideColor
Similar to above, this prop controls the color of the “edges” of each country.
polygonAltitude
This prop raises the level of the polygons (you’ll notice the “edge” colors more when using this prop).
Arcs
In this example, I connect the points to one another using arcs. There are a few more configuration options for using arcs, but the same method I mentioned earlier about providing a key name from the data object still applies.
- 🚀 Preview: https://tns-react-3d-globe.netlify.app/arcs-data
- ⚙️ Code: src/routes/arcs-data.jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
// src/routes/arcs-data.jsx import Globe from 'react-globe.gl'; import globeImage from '../assets/earth-dark.jpg'; const Page = () => { const myData = [ { startLat: 29.953204744601763, startLng: -90.08925929478903, endLat: 28.621322361013092, endLng: 77.20347613099612, color: ['#00ff33', '#ff0000'], stroke: 1, gap: 0.02, dash: 0.02, scale: 0.3, time: 2000, }, { startLat: 28.621322361013092, startLng: 77.20347613099612, endLat: -43.1571459086602, endLng: 172.72338919659848, color: ['#ff0000', '#ffff00'], stroke: 3, gap: 0.05, dash: 0.3, scale: 0.5, time: 8000, }, ]; return ( <div className='cursor-move'> <Globe globeImageUrl={globeImage} arcsData={myData} arcColor='color' arcStroke='stroke' arcDashGap='gap' arcDashLength='dash' arcAltitudeAutoScale='scale' arcDashAnimateTime='time' /> </div> ); }; export default Page; |
arcsData
This is the data array used to position the arcs.
arcColor
A key identifier from the data array is used to change the color of the arcs. The color values can be an array, meaning you can create a “gradient” effect where the arc starts and finishes using different colors.
arcStroke
This is the thickness of the arc.
arcDashGap
This is the gap between each “dash” on the stroke.
arcDashLength
This is the size of the “dash” on the stroke.
arcAltitudeAutoScale
This prop determines how far from the globe surface the arc should be positioned.
arcDashAnimateTime
This is the speed of the arc animation.
Rings
In this example, I add pulsating rings to the globe. There are a few more configuration options for using rings, but the same method I mentioned earlier about providing a key name from the data object still applies.
- 🚀 Preview: https://tns-react-3d-globe.netlify.app/rings-data
- ⚙️ Code: src/routes/rings-data.jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
// src/routes/rings-data.jsx import Globe from 'react-globe.gl'; import hexRgb from 'hex-rgb'; import globeImage from '../assets/earth-dark.jpg'; const Page = () => { const myData = [ { lat: 29.953204744601763, lng: -90.08925929478903, radius: 20, color: '#00ff33', speed: 10, repeat: 500, }, { lat: 28.621322361013092, lng: 77.20347613099612, radius: 40, color: '#ffff00', speed: 20, repeat: 500, }, { lat: -43.1571459086602, lng: 172.72338919659848, radius: 5, color: '#ff0000', speed: 2, repeat: 1000, }, ]; return ( <div className='cursor-move'> <Globe globeImageUrl={globeImage} ringsData={myData} ringMaxRadius='radius' ringColor={(ring) => (t) => { const { red, green, blue } = hexRgb(ring.color); return `rgba(${red},${green},${blue},${Math.sqrt(1 - t)})`; }} ringPropagationSpeed='speed' ringRepeatPeriod='repeat' /> </div> ); }; export default Page; |
ringsData
This is the data array used to position the rings.
ringMaxRadius
This determines how large each ring should be.
ringColor
This is slightly different than before with the arcs color as I curry the time parameter and use it as the alpha value of an rgba color reference. This allows the ring to fade out as it grows.
ringPropagationSpeed
The speed the rings animate.
ringRepeatPeriod
The speed the rings are generated.
HTML Marker
In this example, I use a Svg icon to act as a “marker” for each of the positions in the data array. You can use any HTML element(s) you like to create a marker and any of the values from the data array can be abstracted from the data prop.
- 🚀 Preview: https://tns-react-3d-globe.netlify.app/html-marker
- ⚙️ Code: src/routes/html-marker.jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
// src/routes/html-marker.jsx import Globe from 'react-globe.gl'; import globeImage from '../assets/earth-dark.jpg'; const Page = () => { const myData = [ { city: 'New Orleans', lat: 29.953204744601763, lng: -90.08925929478903, altitude: 0.1, color: '#00ff33', }, { city: 'New Delhi', lat: 28.621322361013092, lng: 77.20347613099612, altitude: 0.1, color: '#ff0000', }, { city: 'New Zealand', lat: -43.1571459086602, lng: 172.72338919659848, altitude: 0.1, color: '#ffff00', }, ]; const icon = ` `; return ( <div className='cursor-move'> <Globe globeImageUrl={globeImage} htmlElementsData={myData} htmlAltitude='altitude' htmlElement={(data) => { const { city, color } = data; const element = document.createElement('div'); element.style.color = color; element.innerHTML = ` <div> <svg viewBox="0 0 24 24" style="width:24px;margin:0 auto;"> <path fill="currentColor" fill-rule="evenodd" d="M11.54 22.351l.07.04.028.016a.76.76 0 00.723 0l.028-.015.071-.041a16.975 16.975 0 001.144-.742 19.58 19.58 0 002.683-2.282c1.944-1.99 3.963-4.98 3.963-8.827a8.25 8.25 0 00-16.5 0c0 3.846 2.02 6.837 3.963 8.827a19.58 19.58 0 002.682 2.282 16.975 16.975 0 001.145.742zM12 13.5a3 3 0 100-6 3 3 0 000 6z" clip-rule="evenodd"/> </svg> <strong style="font-size:10px;text-align:center">${city}</strong> </div>`; return element; }} /> </div> ); }; export default Page; |
htmlElementsData
The data used to position the points/marker.
htmlAltitude
This is the distance above the globe’s surface where the marker should be positioned.
htmlElement
The HTML Element is to be displayed as a marker. The data prop can be used to access any of the data from the data array.
Custom Layer
This example is slightly different as it’s not used to add elements to the globe itself but rather, to add elements to the atmosphere that surrounds the globe; or in this case, suns/stars in the universe.
- 🚀 Preview: https://tns-react-3d-globe.netlify.app/custom-layer
- ⚙️ Code: src/routes/custom-layer.jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
// src/routes/custom-layer import { useRef } from 'react'; import Globe from 'react-globe.gl'; import * as THREE from 'three'; import globeImage from '../assets/earth-dark.jpg'; const Page = () => { const globeEl = useRef(null); const myData = [ { lat: 29.953204744601763, lng: -90.08925929478903, altitude: 0.4, color: '#00ff33', }, { lat: 28.621322361013092, lng: 77.20347613099612, altitude: 0.4, color: '#ff0000', }, { lat: -43.1571459086602, lng: 172.72338919659848, altitude: 0.4, color: '#ffff00', }, ]; return ( <div className='cursor-move'> <Globe ref={globeEl} globeImageUrl={globeImage} pointsData={myData} pointAltitude='altitude' pointColor='color' customLayerData={[...Array(500).keys()].map(() => ({ lat: (Math.random() - 1) * 360, lng: (Math.random() - 1) * 360, altitude: Math.random() * 2, size: Math.random() * 1, color: '#9999cc', }))} customThreeObject={(data) => { const { size, color } = data; return new THREE.Mesh(new THREE.SphereGeometry(size), new THREE.MeshBasicMaterial({ color })); }} customThreeObjectUpdate={(obj, data) => { const { lat, lng, altitude } = data; return Object.assign(obj.position, globeEl.current?.getCoords(lat, lng, altitude)); }} /> </div> ); }; export default Page; |
customLayerData
The key value pairs I’ve used here are similar to the values used in all the other example data arrays, but this time I’m creating 500 new “points” and randomly setting their lat/lng positions, altitude, color and size.
customThreeObject
This is the three.js geometry and material used to create a new three.js “shape”. I can access the size and color by destructuring their values from the data parameter.
customThreeObjectUpdate
In order for the custom layer to move with the globe when it’s rotated or zoomed, I grab a reference to the globe using a React ref and set the position of each of the “points” so they are relative to the globe’s current rotation or position.
… that’s the basics done and I’ve used some of the above methods to create the finished globe.
Finished
Now for a couple of things I’ve not already covered in this example; namely, how to auto-rotate the globe and the use of a texture.
- 🚀 Preview: https://tns-react-3d-globe.netlify.app
- ⚙️ Code: src/routes/finished.jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
// src/routes/finished.jsx import { useRef } from 'react'; import Globe from 'react-globe.gl'; import * as THREE from 'three'; import * as topojson from 'topojson-client'; import landTopology from '../assets/land_10m.json'; import pointsData from '../assets/random-locations.json'; import texture from '../assets/texture.jpg'; const min = 1000; const max = 4000; const sliceData = pointsData.sort(() => (Math.random() > 0.5 ? 1 : -1)).slice(20, 90); const arcsData = sliceData.map(() => { const randStart = Math.floor(Math.random() * sliceData.length); const randEnd = Math.floor(Math.random() * sliceData.length); const randTime = Math.floor(Math.random() * (max - min + 1) + min); return { startLat: sliceData[randStart].lat, startLng: sliceData[randStart].lng, endLat: sliceData[randEnd].lat, endLng: sliceData[randEnd].lng, time: randTime, color: ['#ffffff00', '#faf7e6', '#ffffff00'], }; }); const Page = () => { const globeRef = useRef(null); const globeReady = () => { if (globeRef.current) { globeRef.current.controls().autoRotate = true; globeRef.current.controls().enableZoom = false; globeRef.current.pointOfView({ lat: 19.054339351561637, lng: -50.421161072148465, altitude: 1.8, }); } }; return ( <div className='cursor-move'> <Globe ref={globeRef} onGlobeReady={globeReady} backgroundColor='#08070e' rendererConfig={{ antialias: true, alpha: true }} globeMaterial={ new THREE.MeshPhongMaterial({ color: '#1a2033', opacity: 0.95, transparent: true, }) } atmosphereColor='#5784a7' atmosphereAltitude={0.5} pointsMerge={true} pointsData={pointsData} pointAltitude={0.01} pointRadius={0.2} pointResolution={5} pointColor={() => '#eed31f'} arcsData={arcsData} arcAltitudeAutoScale={0.3} arcColor='color' arcStroke={0.5} arcDashGap={2} arcDashAnimateTime='time' polygonsData={topojson.feature(landTopology, landTopology.objects.land).features} polygonSideColor={() => '#00000000'} polygonCapMaterial={ new THREE.MeshPhongMaterial({ color: '#49ac8f', side: THREE.DoubleSide, map: new THREE.TextureLoader().load(texture), }) } polygonAltitude={0.01} customLayerData={[...Array(500).keys()].map(() => ({ lat: (Math.random() - 1) * 360, lng: (Math.random() - 1) * 360, altitude: Math.random() * 2, size: Math.random() * 0.4, color: '#faadfd', }))} customThreeObject={(sliceData) => { const { size, color } = sliceData; return new THREE.Mesh(new THREE.SphereGeometry(size), new THREE.MeshBasicMaterial({ color })); }} customThreeObjectUpdate={(obj, sliceData) => { const { lat, lng, altitude } = sliceData; return Object.assign(obj.position, globeRef.current?.getCoords(lat, lng, altitude)); }} /> </div> ); }; export default Page; |
onGlobeReady
This prop can be used to call a function when the globe has fully loaded. The globeReady function can then access the globe via the React ref and there are a number of functions that can be used to control the globe. To make the globe rotate, you can use the autoRotate function.
polygonsData
This method uses a slightly different approach. To ensure the countries were flat (with no depth) I’ve used a different geojson file. To translate the geojson into data that react-globe.gl can use, I’ve used topojson-client.
polygonCapMaterial
This is where I apply a texture to each of the countries, instead of the method I explained earlier which simply sets each country to a different color.
And that’s it. It’s far from a “recreation” of the Shopify team’s impressive work, but it’s close enough for me.
If you have any questions about the methods I’ve used in this post, feel free to come and find me on Twitter/X: PaulieScanlon.