Skip to main content

Displaying a map

Because our data represents countries, lets display those countries on a map.

There are many available map making solutions out there. Here, we will use Plotly because of its simplicity and rich stock features.

Install the dependencies

To add the required dependencies:

  1. Head back to your terminal and navigate to your project folder.
  2. Run
npm add react-plotly.js plotly.js @rehooks/component-size @types/react-plotly.js
  1. Run
npm build

Get the countries and their values

Believe it or not, we are almost ready to display a fully functioning map. This is the last step before we render it!

We just have to extract the countries and their GDP per capita for the year 2019 from data.

Add this function above the Map Component in Map.tsx

const getCountriesAndValues = (data?: CellSet): [string[], number[]] => {
if (!data) {
return [[], []];
}

const [columnsAxis, rowsAxis] = data.axes;
const numberOfYears = columnsAxis.positions.length;
const numberOfCountries = rowsAxis.positions.length;

const valuesForSelectedYear = new Array(numberOfCountries).fill(null);
data.cells.forEach((cell) => {
const rowIndex = Math.floor(cell.ordinal / numberOfYears);
const columnIndex = cell.ordinal % numberOfYears;
const year = columnsAxis.positions[columnIndex][0].captionPath[0];
// Only display the 2019 values for now.
if (year === "2019") {
valuesForSelectedYear[rowIndex] = cell.value;
}
});

return [
rowsAxis.positions.map((position) => position[0].captionPath[2]),
valuesForSelectedYear,
];
};

And also make these changes inside Map.tsx

import {
+ CellSet,
useQueryResult,
WidgetPluginProps,
} from "@activeviam/atoti-ui-sdk";

...

export const Map: FC<WidgetPluginProps> = (props) => {

...

+ // countries and values only depend on `data`.
+ const [countries, values] = getCountriesAndValues(data)
+ console.log(isLoading, data, countries, values);
- console.log(isLoading, data);

...
}

Display the map 🌎

Now the real fun begins!

We will provide all the Plotly setup code for you here. However, to do this on your own you only need the React-Plotly quick start and the Plotly chloropleth map configuration.

note

To push you along the road toward mastery, we recommend copy/pasting the below code first, observing the output, deleting the code, and then trying to reproduce it using the above links.

+ import Plot from "react-plotly.js";
import React from "react";

...

export const Map: FC<WidgetPluginProps> = (props) => {
...

- return <div style={props.style}>Hello World!</div>;
+ return (
+ <div
+ style={{
+ ...props.style,
+ height: "100%",
+ }}
+ >
+ <Plot
+ data={[
+ {
+ type: "choropleth",
+ locationmode: "country names",
+ locations: countries,
+ z: values,
+ text: countries,
+ // @ts-ignore
+ autocolorscale: true,
+ },
+ ]}
+ />
+ </div>
+ );
}

Meet your new widget! 🎉

map not centered

You might shake your head at the sight of this little map randomly positioned in a big widget and think How come it never works out on the first try! But with just a little more code, it will all fall into place!

Make it fit

One suboptimal way to make the map fit better is to tweak layout with a hardcoded height and width.

On a full HD screen, you will end up with something like:

...

<Plot
data={

...

}
+ layout={
+ {
+ height: 750,
+ width: 1300,
+ margin: {
+ l: 20,
+ t: 30,
+ r: 20,
+ b: 20,
+ }
+ }
+ }
/>


...

And it seems to be working pretty well:

hardcoded size

Is this it, did we make it? Can we party now?

Unfortunately, not quite. The problem with hardcoding the size is that it won't necessarily work in a couple situations:

  • On other devices with a different screen size.
  • If more widgets are added to the dashboard, thus giving your widget a different amount of space. Like this:

hardcoded size error

In order to dynamically adapt the map's size to the available room in the widget, we can use a hook called useComponentSize.

But first we have to reorganize the code a little bit.

This is because useComponentSize requires that the element in question always exists in the DOM. Right now, our "early returns" violate this condition by adding and removing several different <div>'s in the DOM as the data loads.

Let's fix it:

- if (isLoading) {
- return <Spin />
- }
-
- if (error) {
- return <div>{error.stackTrace}</div>
- }


return (
<div
style={{
...props.style,
height: "100%",
}}
>
+ {
+ error ? (
+ <div>{error.stackTrace}</div>
+ ) : isLoading ? (
+ <Spin />
+ ) : (
<Plot
...
/>
+ // Don't forget to close your new brackets!!!
+ )
+ }
</div>
);

It might look like we haven't changed much at all, and the difference is subtle indeed.

Notice that the root div element of Map now survives any update of our Component, no matter what the values of isLoading and error are. That root div will always be in the DOM until the Component is unmounted.

Time for the final touch. Thanks to useComponentSize, we can make the map dynamically fit in the available room:

+ import useComponentSize from "@rehooks/component-size";
import Plot from "react-plotly.js";
- import React, { FC } from "react";
+ import React, { FC, useRef } from "react";


...

export const Map: FC<WidgetPluginProps> = (props) => {
+ const container = useRef<HTMLDivElement>(null);
+ const { height, width } = useComponentSize(container);

...

return (
<div
+ ref={container}
style={{
...props.style,
height: "100%",
}}
>
...

(
<Plot

data={

...

}
layout={{
- height: 750,
+ height,
- width: 1300,
+ width,
margin: {
l: 20,
t: 30,
r: 20,
b: 20,
}
}}
/>
)}
</div>
);
}

This time, we nailed it:

dynamic size

Just in case you got a bit lost, you can click here to download Map.tsx as it should be by now.

In the next page, we will add some interactivity and allow the user to select what measure to represent on the map.

Reduce the size of the bundle

The bundle is all the JavaScript assets downloaded to a user's machine whenever they connect to our application. The bigger the bundle, the longer the load time. To give users a fast, smooth experience, we want to keep our bundle as small as possible!

For this reason, we recommend paying close attention to your bundle size as you add features and new dependencies to your application.

Accidentally and unnecessarily increasing the bundle size can happen sooner than you think. In fact, it just happened to us! 😨

The way we imported Plot unintentionally wrapped all Plotly features and charts into our bundle. This added an additional 3.7MB! This has the potential to add several seconds of load time! The worst part is, that additional load time is for resources we don't even use!

In the case of Plotly, we can get around this issue and achieve a lightweight bundle by importing Plot like this:

// @ts-expect-error
import Plotly from "plotly.js/dist/plotly-geo";
import createPlotlyComponent from "react-plotly.js/factory";
const Plot = createPlotlyComponent(Plotly);