While perusing popular posts, I was inspired by this COVID-19 map to get into learning Mapbox. The project covers a lot of what I do here and I hope I'm not coming off trying to steal anyone's thunder. This isn't a post about my creativity. I am a beginner/bootcamp student and felt like I could even further simplify the process of just using Mapbox at all, let alone connecting it to interesting COVID data and formatting.
Basic Setup of Mapbox
Mapbox GL JS is a JavaScript library that uses WebGL to render interactive maps from vector tiles and Mapbox styles. This tutorial on basic setup in React is very good and helpful! This post will mostly walk through/combine several already very good tutorials. Once again, not trying to reinvent the wheel here, but hoping to combine some good existing wheels.
Basic React setup:
npx create-react-app your-app-name
cd your-app-name
npm install mapbox-gl
or add mapbox-gl
to package.json
manually and then run npm install
. Both seem to accomplish the same thing - creating package-lock.json
and having a package.json
that contains mapbox-gl
in dependencies
.
Now this is probably a trivial difference, but the Mapbox tutorial includes everything in index.js
, I've been learning React with keeping index.js
short - like this:
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
ReactDOM.render(<App />, document.getElementById("root"));
And then keeping most of my code in App.js
for now.
// src/App.js
import React, { Component } from 'react'
import "./App.css";
import mapboxgl from 'mapbox-gl';
mapboxgl.accessToken = process.env.REACT_APP_MAPBOX_ACCESS_TOKEN;
export class App extends Component {
constructor(props) {
super(props);
this.state = {
lng: -90,
lat: 45,
zoom: 3
};}
componentDidMount() {
const map = new mapboxgl.Map({
container: this.mapContainer,
style: 'mapbox://styles/mapbox/streets-v11',
center: [this.state.lng, this.state.lat],
zoom: this.state.zoom
});}
render() {
return (
<div className="App">
<div ref={element => this.mapContainer = element} className="mapContainer" />
</div>
)}}
export default App
and now we have a basic Mapbox! For the access token, you simply sign up for a free and easy account on Mapbox, and then, small side note that isn't super important since it's unlikely anyone would want to steal your free token, but good practice to use .env
and .gitignore
:
// in project main directory
touch .env
// .env
REACT_APP_MAPBOX_ACCESS_TOKEN=<mytoken>
// App.js
mapboxgl.accessToken = process.env.REACT_APP_MAPBOX_ACCESS_TOKEN;
// .gitignore
.env
Fun note of caution! ⚠️ If you get the error Invalid LngLat latitude value: must be between -90 and 90
- you probably have your longitude and latitude mixed up! If you only knew how many things I tried to fix this without just simply googling the error because I didn't think I could be making such a simple mix up...
Anyway, at this point I have my coords set to SF. You can mess around with console.logs and the React dev tools for state to experiment with different starting coords and zoom.
this.state = {
lat: 37.7524,
lng: -122.4343,
zoom: 11.43
};
}
Still following the Mapbox tutorial - here is how you add a bar that shows your coordinates and zoom as you move around the map.
// added to existing componentDidMount() function
componentDidMount() {
...
map.on('move', () => {
this.setState({
lng: map.getCenter().lng.toFixed(4),
lat: map.getCenter().lat.toFixed(4),
zoom: map.getZoom().toFixed(2)
});
});
}
and in render()
, add the follow <div>
just under <div className="App">
:
// added to existing render()
...
<div className="App">
<div className="sidebarStyle">
Longitude: {this.state.lng} | Latitude: {this.state.lat} | Zoom: {this.state.zoom}
</div>
At this point you should also have something like this in src/App.css
. Note if something isn't working but you aren't getting any errors, it might be a CSS issue - a lot of this involves styling from Mapbox.
.mapContainer {
position: absolute;
top: 0;
right: 0;
left: 0;
bottom: 0;
}
.sidebarStyle {
display: inline-block;
position: absolute;
top: 0;
left: 0;
margin: 12px;
background-color: #404040;
color: #ffffff;
z-index: 1 !important;
padding: 6px;
font-weight: bold;
}
A small tangent I found interesting but easy to look up - if you want to change the icon that appears in the browser tab next to title, save an image to your public folder, and add to index.html
where the default icon link is already set:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/map.png" />
and just change the portion after %PUBLIC_URL%/
. I had saved mine as 'map.png' as you can see here.
This is where the Mapbox tutorial pretty much ends, and then links to examples on how to expand. As with everything in programming, there are so many good options! And different ways of doing every one of those options. For some reason, tool tips stood out to me. I didn't even know 'tool tips' was the official term for these little hover popups until now.
I had also come across this great blog post on React component libraries, and was interested in using react-portal-tooltip
. But, I found the Mapbox official example on tooltips a bit easier to follow directly after this setup. react-portal-tooltip
is more general, and useful for all sorts of apps, which is great, but it helped me to start with the Mapbox specific example to learn what was going on here.
Tooltips
The tooltip (or infotip
, or hint
) is a common graphical user interface element — a small "hover box" with information about the item. Again, pretty basic stuff, but I am a coding bootcamp student, and we just finished vanilla JS/started React, so this seemed like a cool thing that would have been harder without React! I always like to think of a clear example in my mind of why I am learning something, instead of just accepting it because it's a buzzword. Anyway!
This is the repo for the Mapbox specific tooltip example that I'm starting with.
First, create a components
directory within src
and a ToolTipBox.js
Component
(or you could name it anything you want, something shorter like just ToolTip.js
, but if I end up using a tooltip library later, that could be not specific enough). Import the component, as well as ReactDOM
which we now need in App.js
, and add the following code:
...
import ReactDOM from 'react-dom';
import ToolTipBox from './components/ToolTipBox'
...
export class App extends Component {
mapRef = React.createRef();
tooltipContainer;
componentDidMount() {
// Container to put generated content in
this.tooltipContainer = document.createElement('div');
const map = new mapboxgl.Map({
container: this.mapRef.current,
...
});
...
const tooltip = new mapboxgl.Marker(this.tooltipContainer).setLngLat([0,0]).addTo(map);
map.on('mousemove', (e) => {
const features = map.queryRenderedFeatures(e.point);
tooltip.setLngLat(e.lngLat);
map.getCanvas().style.cursor = features.length ? 'pointer' : '';
this.setTooltip(features);
}
);
}
render() {
return (
<div className="App">
...
<div ref={this.mapRef} className="absolute top right left bottom"/>
</div>)}}
...
Notice in map.on('mousemove')
I have this.setTooltip(features)
. I define this outside of componentDidMount()
and it connects to my ToolTipBox
component.
export class App extends Component {
...
setTooltip(features) {
if (features.length) {
ReactDOM.render(
React.createElement(
ToolTipBox, {
features
}
),
this.tooltipContainer
);
} else {
ReactDOM.unmountComponentAtNode(this.tooltipContainer);
}
}
...
}
Important things used here - React.createRef()
, which is good for:
Managing focus, text selection, or media playback.
Triggering imperative animations.
Integrating with third-party DOM libraries.
But should be avoided for anything that can be done declaratively.
queryRenderedFeatures
comes from the Mapbox API and is how we get the 'features' that will gives us the tooltips/popups info!
React.createElement()
- this doesn't seem common/standard and would usually be done with JSX. The React docs recommend using JSX and not React.createElement()
, but it seems fine here.
Now more on the ToolTipBox
component, which uses Static PropTypes
to validate that the 'features' returned from queryRenderedFeatures
is an array.
// src/components/ToolTipBox.js
import React from 'react'
import PropTypes from 'prop-types'
export default class Tooltip extends React.Component {
static propTypes = {
features: PropTypes.array.isRequired
};
render() {
const { features } = this.props;
const renderFeature = (feature, i) => {
return (
<div key={i}>
<strong className='mr3'>{feature.layer['source-layer']}:</strong>
<span className='color-gray-light'>{feature.layer.id}</span>
</div>
)
};
return (
<div className="flex-parent-inline absolute bottom">
<div className="flex-child">
{features.map(renderFeature)}
</div>
</div>
);}}
There's a lot going on with CSS here, and you'll notice the actual example I am copying from had more styling, but I removed it and added some to my own App.css
for simplicity of code blocks here. Here's what I added to my CSS after this step:
.flex-parent {
flex-direction: column;
position: absolute;
}
.flex-child {
color: white;
background: gray;
text-overflow: clip;
padding: 1rem;
}
Pretty simple, just enough styling to see a basic box show up. Not that aesthetic, but I can come back to that later, and so you can you!
Either way, though, unless you want to completely define all your own CSS, which I did not, you should probably have your index.html
looking like the example as well, as they import stylesheets here from mapbox:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/map.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link href='https://api.mapbox.com/mapbox-assembly/mbx/v0.18.0/assembly.min.css' rel='stylesheet'>
<link href='https://api.tiles.mapbox.com/mapbox-gl-js/v0.39.1/mapbox-gl.css' rel='stylesheet' />
<title>MapBox React Example</title>
</head>
<div id="root"></div>
<script src='https://api.mapbox.com/mapbox-assembly/mbx/v0.18.0/assembly.js'></script>
</body>
</html>
React Tooltip Library
This post is already a little long so I won't actually go into react-portal-tooltip
. But one very annoying thing I overcame while exploring it and thought worth sharing - if you get this guy:
There are many solutions on StackOverflow. This one worked for me:
touch src/declare_modules.d.ts
// in declare_modules.d.ts
declare module "react-portal-tooltip";
// if it still doesn't work, add import in `App.js`
// App.js
...
import './declare_modules.d.ts'
Thanks for reading!
Top comments (1)
I just recently started experimenting with Mapbox for a side-project and I've found it to be surprisingly capable, though I did notice that quite a few of their examples are fairly out-dated so this is very helpful.
I've also enjoyed messing around with
mapbox-gl-draw
and their map matching API.