Paul is a Senior Software Engineer, Independent Developer Advocate and Technical Writer. More from Paul can be found on his site, paulie.dev.
Read more from Paul Scanlon
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.
But before I explain how I created the Globe, I’ll talk you through the fundamentals of using react-globe-gl.
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
|
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.
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;
|
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/
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.
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.
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.
In this example, I use geojson data to display the countries of the world as hexagons, instead of using an image of earth.
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;
|
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/
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.
In this example, I use geojson data to display the countries of the world as polygons, instead of using an image of Earth.
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;
|
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/
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.
Similar to above, this prop controls the color of the “edges” of each country.
This prop raises the level of the polygons (you’ll notice the “edge” colors more when using this prop).
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.
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;
|
This is the data array used to position the arcs.
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.
This is the thickness of the arc.
This is the gap between each “dash” on the stroke.
This is the size of the “dash” on the stroke.
This prop determines how far from the globe surface the arc should be positioned.
This is the speed of the arc animation.
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.
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;
|
This is the data array used to position the rings.
This determines how large each ring should be.
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.
The speed the rings animate.
The speed the rings are generated.
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.
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;
|
The data used to position the points/marker.
This is the distance above the globe’s surface where the marker should be positioned.
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.
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.
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;
|
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.
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.
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.
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.
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;
|
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.
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.
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.