© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2021
E. ElromIntegrating D3.js with Reacthttps://doi.org/10.1007/978-1-4842-7052-3_6

6. World Chart: Part 1

Elad Elrom1  
(1)
New York, NY, USA
 

A world map chart is a great way to show items globally. Integrating D3 with React and TS can create readable code that uses the best of all the tools. In this chapter, I will show you how to create a rotating map and assign dots based on coordinates.

Specifically, in this chapter, I will show you how to work with a world map using React, D3, and TS as type checkers. I will break down the process into steps. In each step, I will be adding more functionality until we have the rotating world map with dots that represent coordinates.

I have separated the components into five files so it’s easy to see and compare the changes.
  • World map atlas: WorldMapAtlas.tsx

  • Round world map: RoundWorldMap.tsx

  • Rotating round world map: RotatingRoundWorldMap.tsx

  • Rotating round world map with coordinates: RotatingRoundWorldMapWithCoordinates.tsx

  • Refactoring: WorldMap.tsx

The project can be downloaded from here:

https://github.com/Apress/integrating-d3.js-with-react/tree/main/ch06/world-map-chart

Setup

The project setup is simple using CRA with the MHL template project.
$ yarn create react-app world-map-chart --template must-have-libraries
$ cd world-map-chart
$ yarn start
$ open http://localhost:3000

Install Additional Needed Libraries and Types

There are four additional libraries we going to need to get started.
  • d3-geo: We will use d3-geo for geographic projections (drawing the map). See https://github.com/d3/d3-geo.

  • topojson-client: This is a client to manipulate TopoJSON. TopoJSON is the library that provides the map of the world, which I can use to draw the map. See https://github.com/topojson/topojson-client.

  • geojson: This is a format for encoding geographic data. See https://geojson.org/. TopoJSON files are type “Topology” and follow the TopoJSON specification. GeoJSON will then be used to format the encoding of the geographic data structures. See https://geojson.org/.

  • react-uuid: Create a random UUID that we will be using for the list key needed when we map the React component. See https://github.com/uuidjs/uuid.

Go ahead and install these libraries with Yarn:
$yarn add d3-geo @types/d3-geo
$yarn add topojson-client @types/topojson-client
$yarn add geojson @types/geojson
$yarn add react-uuid

Lastly, download the data of the world atlas. The data is provided from TopoJSON that has prebuilt countries data (https://github.com/topojson/world-atlas). Here is the actual JSON I will be using:

https://d3js.org/world-110m.v1.json

Place the file in the public folder for easy access: /public/data/world-110m.json.

World Map Atlas

The first map I will be creating is just a flat world atlas type map that will show the world.

WorldMapAtlas.tsx

Create the files yourself or use generate-react-cli.
$ npx generate-react-cli component WorldMap --type=d3
As I mentioned, I will be creating the components as separate components, so it will be easy to track the work and compare the changes. The first file is WorldMapAtlas.tsx . Here is the complete component code:
// src/components/WorldMap/WorldMapAtlas.tsx
import React, { useState, useEffect } from 'react'
import { geoEqualEarth, geoPath } from 'd3-geo'
import { feature } from 'topojson-client'
import { Feature, FeatureCollection, Geometry } from 'geojson'
import './WorldMap.scss'
const uuid = require('react-uuid')
const scale: number = 200
const cx: number = 400
const cy: number = 150
const WorldMapAtlas = () => {
  const [geographies, setGeographies] = useState<[] | Array<Feature<Geometry | null>>>([])
  useEffect(() => {
    fetch('/data/world-110m.json').then((response) => {
      if (response.status !== 200) {
        // eslint-disable-next-line no-console
        console.log(`Houston we have a problem: ${response.status}`)
        return
      }
      response.json().then((worldData) => {
        const mapFeatures: Array<Feature<Geometry | null>> = ((feature(worldData, worldData.objects.countries) as unknown) as FeatureCollection).features
        setGeographies(mapFeatures)
      })
    })
  }, [])
  const projection = geoEqualEarth().scale(scale).translate([cx, cy]).rotate([0, 0])
  return (
    <>
      <svg width={scale * 3} height={scale * 3} viewBox="0 0 800 450">
        <g>
          {(geographies as []).map((d, i) => (
            <path
              key={`path-${uuid()}`}
              d={geoPath().projection(projection)(d) as string}
              fill={`rgba(38,50,56,${(1 / (geographies ? geographies.length : 0)) * i})`}
              stroke="aliceblue"
              strokeWidth={0.5}
            />
          ))}
        </g>
      </svg>
  )
}
export default WorldMapAtlas

Let’s review.

In the first step, we import React and the libraries we installed. I am also creating WorldMap.scss as a style placeholder for future usage.
import React, { useState, useEffect } from 'react'
import { geoEqualEarth, geoPath } from 'd3-geo'
import { feature } from 'topojson-client'
import { Feature, FeatureCollection, Geometry } from 'geojson'
import './WorldMap.scss'
For the react-uuid library, there is no type for TS, so I will be using require so that ESLint doesn’t complain.
const uuid = require('react-uuid')
Next, we set the attributes such as the map scale and positioning.
const scale: number = 200
const cx: number = 400
const cy: number = 150

WorldMapAtlas is set as function component. This is a matter of preference, and I could have used a class component.

As for the data of the countries, I am setting the client data as a state. Once the data is loaded, I am converting the JSON into a feature geometry array that I can render.
  const [geographies, setGeographies] = useState<[] | Array<Feature<Geometry | null>>>([])

In terms of the type, I had to figure the type by drilling into the actual geojson library.

Next, I am loading the data on the useEffect hook. Later in this chapter I will refactor this code and move it to the parent component, but for now I want the code to be as simple as possible. Here is my working map:
  useEffect(() => {
    fetch('/data/world-110m.json').then((response) => {
      if (response.status !== 200) {
        console.log(`Houston we have a problem: ${response.status}`)
        return
      }
      response.json().then((worldData) => {
Notice that I am using the ‘fetch’, however, another approach is to use d3.json module. D3 already format the object as a JSON so it’s less code.
  useEffect(() => {
    d3.json('/data/world-110m.json').then((d) => { return d }).then((worldData) => {
        // @ts-ignore const mapFeature: Array<Feature<Geometry | null>> = (feature(worldData, worldData.objects.countries) as FeatureCollection).features setGeographies(mapFeature)
    })
  })
Once I get a response, I can convert the JSON to a Geometry feature array and set it as the function state.
        const mapFeatures: Array<Feature<Geometry | null>> = ((feature(worldData, worldData.objects.countries) as unknown) as FeatureCollection).features
        setGeographies(mapFeatures)
      })
    })
  }, [])
The projection, in layman’s terms, is what I want my actual atlas to look like. There are many options to choose from (see https://github.com/d3/d3-geo/blob/master/README.md). Let’s go with geoEqualEarth as a first try.
  const projection = geoEqualEarth().scale(scale).translate([cx, cy]).rotate([0, 0])
To render my atlas, I will be first setting an SVG wrapper that holds a group element and then iterates paths using a map through the geographies data I set as the state to draw each country.
  return (
    <>
      <svg width={scale * 3} height={scale * 3} viewBox="0 0 800 450">
        <g>
          {(geographies as []).map((d, i) => (
            <path
              key={`path-${uuid()}`}
              d={geoPath().projection(projection)(d) as string}
              fill={`rgba(38,50,56,${(1 / (geographies ? geographies.length : 0)) * i})`}
              stroke="aliceblue"
              strokeWidth={0.5}
            />
          ))}
        </g>
      </svg>
      </>
  )
}
export default WorldMapAtlas

Notice that I am using key={`path-${uuid()}`}.

Keys are common practice for identifying unique virtual DOM (VDOM) UI elements with their corresponding data. Without doing this step, React VDOM can get confused when there is a need to refresh the DOM. This is best practice. You can use a random number, but be mindful not to use the map index as the key because the map can change and cause the VDOM to reference the wrong item.

Keys help React identify which items have changed, are added, or are removed. Keys should be given to the elements inside the array to give the elements a stable identity.

https://reactjs.org/docs/lists-and-keys.html

Keys and refs are added as an attribute to a React.createElement() call. They help React optimize the rendering by recycling all the existing elements in the DOM.

App.tsx

Next, let’s add our WorldMapAtlas component to App.tsx as a child.

Notice that the changes are highlighted in bold.
import React from 'react'
import './App.scss'
import WorldMapAtlas from './components/WorldMap/WorldMapAtlas'
function App() {
  return (
    <div className="App">
      <header className="App-header">
        <WorldMapAtlas />
      </header>
    </div>
  )
}
export default App

App.scss

For the App.scss style, I am changing the background color to white.
.App-header {
  background-color: #ffffff;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(10px + 2vmin);
  color: white;
}
Voilà! See Figure 6-1.
../images/510438_1_En_6_Chapter/510438_1_En_6_Fig1_HTML.jpg
Figure 6-1

Simple world map atlas

As I mentioned, for the projections, I used geoEqualEarth; however, I can change the projection easily to other projections. For instance, if I want to change to geoStereographic, my map will change. See Figure 6-2 .

Change the projection from this:
import { geoEqualEarth, geoPath} from 'd3-geo'
const projection = geoEqualEarth().scale(scale).translate([cx, cy]).rotate([0, 0])
to the following:
import { geoPath, geoStereographic } from 'd3-geo'
const projection = geoStereographic().scale(scale).translate([cx, cy]).rotate([0, 0])
../images/510438_1_En_6_Chapter/510438_1_En_6_Fig2_HTML.jpg
Figure 6-2

World atlas map using the geoStereographic projection

Another example is the geoConicConformal projection ( Figure 6-3).
const projection = geoConicConformal().scale(scale).translate([cx, cy]).rotate([0, 0])
../images/510438_1_En_6_Chapter/510438_1_En_6_Fig3_HTML.jpg
Figure 6-3

World atlas map using geoConicConformal projection

Round World Map

Now we know how to draw a world map atlas. Next we will see how to create a round world map.

To change the map to be round, all I have to do is use the geoOrthographic projection.

To make the round map look better, I am also going to draw a round light gray background using a SVG circle element.

RoundWorldMap.tsx

Create a new component called RoundWorldMap.tsx ; see the changes highlighted here.
// src/components/WorldMap/RoundWorldMap.tsx
  const projection = geoOrthographic().scale(scale).translate([cx, cy]).rotate([rotation, 0])
  return (
    <svg width={scale * 3} height={scale * 3} viewBox="0 0 800 450">
      <g>
        <circle
          fill="#0098c8"
          cx={cx}
          cy={cy}
          r={scale}
        />
      </g>
      <g>
        {(geographies as []).map((d, i) => (
          <path
            key={`path-${uuid()}`}
            d={geoPath().projection(projection)(d) as string}
            fill={`rgba(38,50,56,${(1 / (geographies ? geographies.length : 0)) * i})`}
            stroke="aliceblue"
            strokeWidth={0.5}
          />
        ))}
      </g>
    </svg>
  )
}
Remember to update App.tsx.
return (
  <div className="App">
    <header className="App-header">
      <RoundWorldMap />
    </header>
  </div>
)
See Figure 6-4.
../images/510438_1_En_6_Chapter/510438_1_En_6_Fig4_HTML.jpg
Figure 6-4

Round world map atlas

Rotating Round World Map Chart

Now that we have a round world map atlas, wouldn’t it be neat to add animation and interactions? We can rotate the atlas and add a button to start the animation.

AnimationFrame.tsx

To add animations, we can call JavaScript window requestAnimationFrame API (https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame).

The requestAnimationFrame method tells the browser that I want to perform an animation, and the browser will call my callback function so that I can update my animation before the next redraw.

To use requestAnimationFrame , I can just place the following code in my React component:
window.requestAnimationFrame(() => {
  // TODO
})
However, a better architectural design is to create a hook function component using useRef and wrap my requestAnimationFrame. Take a look:
// src/hooks/WindowDimensions.tsx
import { useEffect, useRef } from 'react'
export default (callback: (arg0: ICallback) => void) => {
  const frame = useRef()
  const last = useRef(performance.now())
  const init = useRef(performance.now())
  const animate = () => {
    const now = performance.now()
    const time = (now - init.current) / 1000
    const delta = (now - last.current) / 1000
    callback({ time, delta })
    last.current = now
    ;((frame as unknown) as IFrame).current = requestAnimationFrame(animate)
  }
  useEffect(() => {
    ((frame as unknown) as IFrame).current = requestAnimationFrame(animate)
    return () => cancelAnimationFrame(((frame as unknown) as IFrame).current)
  })
}
interface ICallback {
  time: number
  delta: number
}

Let’s review the code.

I am passing the callback as an argument.
export default (callback: (arg0: ICallback) => void) => {

Next, I will keep track of the frame using performance.now() (https://developer.mozilla.org/en-US/docs/Web/API/Performance/now). That feature brings a timestamp in the one-millisecond resolution that I can use to figure out the time delta in case I need it.

Note

Time deltas are the differences in times.

  const frame = useRef()
  const last = useRef(performance.now())
  const init = useRef(performance.now())
On every requestAnimationFrame call, animate will return the current timestamp.
  const animate = () => {
    const now = performance.now()
    const time = (now - init.current) / 1000
    const delta = (now - last.current) / 1000
    callback({ time, delta })
    last.current = now;
    (frame as unknown as IFrame).current = requestAnimationFrame(animate)
  }

I can then use the useEffect hook to tie the animate method.

My effects will need to be cleaned up before the component leaves the screen. To do this, the function passed to useEffect needs to return a cleanup function. In my case, cancelAnimationFrame needs to be called on the useEffect return callback. You can learn more about React effects and clean up here: https://reactjs.org/docs/hooks-reference.html.
  useEffect(() => {
    (frame as unknown as IFrame).current = requestAnimationFrame(animate)
    return () => cancelAnimationFrame((frame as unknown as IFrame).current)
  })
}

RotatingRoundWorldMap.tsx

Now that we have the AnimationFrame hook ready to be used for our animation, I can add my rotating animation. Additionally, I will be adding a user gesture in the form of an icon button from Material-UI to start the animation.

Copy the RoundWorldMap.tsx file from our previous example and save it as a new file called RotatingRoundWorldMap.tsx. Take a look at these changes from RoundWorldMap.tsx:
// src/components/WorldMap/RotatingRoundWorldMap.tsx
import PlayCircleFilledWhiteIcon from '@material-ui/icons/PlayCircleFilledWhite'
import { Button } from '@material-ui/core'
The animation logic checks whether a 360-degree rotation ended to reset the rotation variable on each completion of 360 degrees. The animation checks if the isRotate state is set to true so that my map will only start rotating when I click the start button.
  AnimationFrame(() => {
    if (isRotate) {
      let newRotation = rotation
      if (rotation >= 360) {
        newRotation = rotation - 360
      }
      setRotation(newRotation + 0.2)
      // console.log(`rotation: ${  rotation}`)
    }
  })
I am adding a button to start the animation. This is done by setting the isRotate state to true using the fat arrow inline function.
  return (
    <>
      <Button
        size="medium"
        color="primary"
        startIcon={<PlayCircleFilledWhiteIcon />}
        onClick={() => {
          setIsRotate(true)
        }}
      />
      <svg width={scale * 3} height={scale * 3} viewBox="0 0 800 450">
        <g>
          <circle
            fill="#0098c8"
            cx={cx}
            cy={cy}
            r={scale}
          />
        </g>
        <g>
          {(geographies as []).map((d, i) => (
            <path
              key={`path-${uuid()}`}
              d={geoPath().projection(projection)(d) as string}
              fill={`rgba(38,50,56,${(1 / (geographies ? geographies.length : 0)) * i})`}
              stroke="aliceblue"
              strokeWidth={0.5}
            />
          ))}
        </g>
      </svg>
      </>
  )
}
Remember to update App.tsx to include the RotatingRoundWorldMap component .
return (
  <div className="App">
    <header className="App-header">
      <RotatingRoundWorldMap />
    </header>
  </div>
)
Figure 6-5 shows the final results.
../images/510438_1_En_6_Chapter/510438_1_En_6_Fig5_HTML.jpg
Figure 6-5

Rotating round world map atlas

One more change we need to make is to place the loading of the map data inside of a statement that checks if the data was loaded.

The reason is that since using the animation hook, the useEffect will get called all the time.

Take a look;
useEffect(() => { if (geographies.length === 0) {
    // load map
}

Rotating Round World Map Chart with Coordinates

In this section, I will show you how to add coordinate dots to our map.

RotatingRoundWorldMapWithCoordinates.tsx

Copy the RotatingRoundWorldMap.tsx file from the previous example and name it RotatingRoundWorldMapWIthCoordinates.tsx .

Yes, I know it’s a long name, but using Shakespeare’s method names, it’s easy to tell what this component is doing.

To create the coordinate dots, I will be adding a new data array feed that includes the coordinates’ longitude and latitude. Take a look at the changes from the previous RotatingRoundWorldMap.tsx component:
// src/components/WorldMap/RotatingRoundWorldMapWIthCoordinates.tsx
import React, { useState, useEffect } from 'react'
import { geoOrthographic, geoPath } from 'd3-geo'
import { feature } from 'topojson-client'
import { Feature, FeatureCollection, Geometry } from 'geojson'
import './WorldMap.scss'
import PlayCircleFilledWhiteIcon from '@material-ui/icons/PlayCircleFilledWhite'
import { Button } from '@material-ui/core'
import AnimationFrame from '../../hooks/AnimationFrame'
const uuid = require('react-uuid')
const data: { name: string; coordinates: [number, number] }[] = [
  { name: '1', coordinates: [-73.9919, 40.7529] },
  { name: '2', coordinates: [-70.0007884457405, 40.75509010847814] },
]
const scale: number = 200
const cx: number = 400
const cy: number = 150
const initRotation: number = 50
const RotatingRoundWorldMapWithCoordinates = () => {
  const [geographies, setGeographies] = useState<[] | Array<Feature<Geometry | null>>>([])
  const [rotation, setRotation] = useState<number>(initRotation)
  const [isRotate, setIsRotate] = useState<Boolean>(false)
  useEffect(() => {
    if (geographies.length === 0) {
        fetch('/data/world-110m.json').then((response) => {
          if (response.status !== 200) {
            // eslint-disable-next-line no-console
            console.log(`Houston we have a problem: ${response.status}`)
          return
        }
        response.json().then((worldData) => {
          const mapFeatures: Array<Feature<Geometry | null>> = ((feature(worldData, worldData.objects.countries) as unknown) as FeatureCollection).features
          setGeographies(mapFeatures)
        })
      })
    }
  }, [])
  // geoEqualEarth
  // geoOrthographic
  const projection = geoOrthographic().scale(scale).translate([cx, cy]).rotate([rotation, 0])
  AnimationFrame(() => {
    if (isRotate) {
      let newRotation = rotation
      if (rotation >= 360) {
        newRotation = rotation - 360
      }
      setRotation(newRotation + 0.2)
      // console.log(`rotation: ${  rotation}`)
    }
  })
  function returnProjectionValueWhenValid(point: [number, number], index: number) {
    const retVal: [number, number] | null = projection(point)
    if (retVal?.length) {
      return retVal[index]
    }
    return 0
  }
  const handleMarkerClick = (i: number) => {
    // eslint-disable-next-line no-alert
    alert(`Marker: ${JSON.stringify(data[i])}`)
  }
  return (
  <>
      <Button
        size="medium"
        color="primary"
        startIcon={<PlayCircleFilledWhiteIcon />}
        onClick={() => {
          setIsRotate(true)
        }}
      />
      <svg width={scale * 3} height={scale * 3} viewBox="0 0 800 450">
        <g>
          <circle fill="#f2f2f2" cx={cx} cy={cy} r={scale} />
        </g>
        <g>
          {(geographies as []).map((d, i) => (
            <path
              key={`path-${uuid()}`}
              d={geoPath().projection(projection)(d) as string}
              fill={`rgba(38,50,56,${(1 / (geographies ? geographies.length : 0)) * i})`}
              stroke="aliceblue"
              strokeWidth={0.5}
            />
          ))}
        </g>
        <g>
          {data.map((d, i) => (
            <circle
              key={`marker-${uuid()}`}
              cx={returnProjectionValueWhenValid(d.coordinates, 0)}
              cy={returnProjectionValueWhenValid(d.coordinates, 1)}
              r={5}
              fill="#E91E63"
              stroke="#FFFFFF"
              onClick={() => handleMarkerClick(i)}
              onMouseEnter={() => setIsRotate(false)}
            />
          ))}
        </g>
      </svg>
  </>
  )
}
export default RotatingRoundWorldMapWithCoordinates
Let’s review the changes in RotatingRoundWorldMapWIthCoordinates from RoundWorldMapAtlas. I am setting a data object that includes the names and coordinates.
const data: { name: string; coordinates: [number, number] }[] = [
  { name: '1', coordinates: [-73.9919, 40.7529] },
  { name: '2', coordinates: [-70.0007884457405, 40.75509010847814] },
]
For the initial world map rotation location, I can set that in a constant with where I want the world map to start rotating in degrees.
const initRotation: number = 50
Next, I am adding a returnProjectionValueWhenValid method to adjust the locations of the dots. That is needed because the world map is animating and the location on the projection map is going to change.
function returnProjectionValueWhenValid(point: [number, number], index: number) {
    const retVal: [number, number] | null = projection(point)
    if (retVal?.length) {
      return retVal[index]
    }
    return 0
  }
I am going to set a handler once the user clicks the dots. This can be used to open a detailed information window or anything you want to do once the user is clicked.
  const handleMarkerClick = (i: number) => {
    alert(`Marker: ${  JSON.stringify( data[i])}` )
  }
On the rendering, I will iterate through the array of coordinates using the array map attribute and set the onClick handler as well as mouse enter event to stop the animation so it’s easier to click the marker.
        <g>
          {data.map((d, i) => (
            <circle
              key={`marker-${uuid()}`}
              cx={returnProjectionValueWhenValid(d.coordinates, 0)}
              cy={returnProjectionValueWhenValid(d.coordinates, 1)}
              r={5}
              fill="#E91E63"
              stroke="#FFFFFF"
              onClick={() => handleMarkerClick(i)}
              onMouseEnter={() => setIsRotate(false)}
            />
          ))}
        </g>

Notice that just like the previous step, every time that I am using the map in React I am adding a unique key to each item.

Lastly, remember to update App.tsx.
return (
  <div className="App">
    <header className="App-header">
<RotatingRoundRotatingRoundWorldMapWithCoordinatesWIthCoordinates />
    </header>
  </div>
)
Figure 6-6 shows the final results.
../images/510438_1_En_6_Chapter/510438_1_En_6_Fig6_HTML.jpg
Figure 6-6

Rotating the round world map atlas with coordinates

Refactoring

In the last step for this chapter, I will be doing some neat refactoring effort. I will be doing the following:
  • Props: Extract the props attributes so the parent component can adjust the attributes and data.

  • Coordinates: Set the coordinate data as a second data feed in the form of CSV.

  • Loader: Load the data from the parent component and pass the data to the map chart using asynchronous tasks.

  • Types: Add the TypeScript types to make the code more readable, avoid errors, and help with testing.

coordinates.csv

It’s better to extract the coordinate data to a separate CSV data file. That’s not just to clean the code. The data may grow, and I may need to get this data from an external source. I am breaking down the coordinates into latitude and longitude in case I ever need to use that information.
id,latitude,longitude
1,-73.9919,40.7529
2,-70.0007884457405,40.75509010847814

Place the file here for easy access: world-map-chart/public/data/coordinates.csv.

types.tsx

A common practice in TS, as you will seen throughout this book, is to create types for TypeScript. That’s not only good practice, but it will clean the code and make it more readable. In my case, there are two types I will be setting for the two data feeds: CoordinatesData and MapObject.
// src/component/BasicScatterChart/types.ts
import { Feature, Geometry } from 'geojson'
export namespace Types {
  export type CoordinatesData = {
    id: number
    latitude: number
    longitude: number
  }
  export type MapObject = {
    mapFeatures: Array<Feature<Geometry | null>>
  }
}
When referencing a type, it’s common to use a lowercase first letter. We are creating the reference, so either way is fine. In other words, you can decide what you prefer, but just stay consistent. I wanted you to be aware of both options.
export type coordinatesData
Notice that if you start with a lowercase letter, you will get a lint error since our lint rules are set to complain on any export types that are not starting with uppercase. You can disable that, just for this time or globally.
// eslint-disable-next-line @typescript-eslint/naming-convention
export type coordinatesData

WorldMap.tsx

For our refactoring effort, copy the RotatingRoundWorldMapWIthCoordinates.tsx file from the previous example and name it WorldMap.tsx .

Most of the code stays the same.

For the props interface, I will be adding the data feeds and the alignment attributes, so I need to add the props interface and change the function signature.

Most of the change to WorldMap.tsx is just adding these props instead of the data. The change is highlighted in bold.
// src/components/WorldMap/WorldMap.tsx
import React, { useState } from 'react'
import { geoOrthographic, geoPath } from 'd3-geo'
import './WorldMap.scss'
import PlayCircleFilledWhiteIcon from '@material-ui/icons/PlayCircleFilledWhite'
import { Button } from '@material-ui/core'
import AnimationFrame from '../../hooks/AnimationFrame'
import { Types } from './types'
const uuid = require('react-uuid')
const WorldMap = (props: IWorldMapProps) => {
  const [rotation, setRotation] = useState<number>(props.initRotation)
  const [isRotate, setIsRotate] = useState<Boolean>(false)
  const projection = geoOrthographic().scale(props.scale).translate([props.cx, props.cy]).rotate([rotation, 0])
  AnimationFrame(() => {
    if (isRotate) {
      let newRotation = rotation
      if (rotation >= 360) {
        newRotation = rotation - 360
      }
      setRotation(newRotation + 0.2)
    }
  })
  function returnProjectionValueWhenValid(point: [number, number], index: number ) {
    const retVal: [number, number] | null = projection(point)
    if (retVal?.length) {
      return retVal[index]
    }
    return 0
  }
  const handleMarkerClick = (i: number) => {
    alert(`Marker: ${  JSON.stringify( props.coordinatesData[i].id)}` )
  }
  return (
  <>
      <Button
        size="medium"
        color="primary"
        startIcon={<PlayCircleFilledWhiteIcon />}
        onClick={() => {
          setIsRotate(true)
        }}
      />
      <svg width={props.scale * 3} height={props.scale * 3} viewBox="0 0 800 450">
        <g>
          <circle
            fill="#f2f2f2"
            cx={props.cx}
            cy={props.cy}
            r={props.scale}
          />
        </g>
        <g>
          {(props.mapData.mapFeatures as ).map((d, i) => (
            <path
              key={`path-${uuid()}`}
              d={geoPath().projection(projection)(d) as string}
              fill={`rgba(30,50,50,${(1 / (props.mapData.mapFeatures ? props.mapData.mapFeatures.length : 0)) * i})`}
              stroke="aliceblue"
              strokeWidth={props.rotationSpeed}
            />
          ))}
        </g>
        <g>
          {props.coordinatesData?.map((d, i) => (
            <circle
              key={`marker-${uuid()}`}
              cx={returnProjectionValueWhenValid([d.latitude, d.longitude], 0)}
              cy={returnProjectionValueWhenValid([d.latitude, d.longitude], 1)}
              r={5}
              fill="#E91E63"
              stroke="#FFFFFF"
              onClick={() => handleMarkerClick(i)}
              onMouseEnter={() => setIsRotate(false)}
            />
          ))}
        </g>
      </svg>
  </>
  )
}
export default WorldMapinterface IWorldMapProps {
  mapData: Types.MapObject
  coordinatesData: Types.CoordinatesData[]
  scale: number
  cx: number
  cy: number
  initRotation: number
  rotationSpeed: number
}

As you can see, our code in WorldMap.tsx is now cleaner and more readable compared to the RotatingRoundWorldMapWIthCoordinates.tsx file from the previous example.

App.tsx

For the parent component, I will be extracting the data, placing it inside the effect hook, and using the D3 to load both the JSON and the CSV.

Since I want to load both data sets before I draw the map, a good approach is to use the asynchronous calls to both data sets before passing the data to the chart component.

We can use D3’s queue(). See https://github.com/d3/d3-queue. D3’s queue() to do asynchronous tasks. Add these D3 modules and TS types.

The first step is to add these libraries to our project;
$ yarn add d3-queue d3-request @types/d3-queue @types/d3-request
Next, let’s refactor our App.tsx.
// src/App.tsx
import React, { useEffect, useState } from 'react'
import './App.scss'
import { queue } from 'd3-queue'
import { csv, json } from 'd3-request'
import { FeatureCollection } from 'geojson'
import { feature } from 'topojson-client'
import WorldMap from './components/WorldMap/WorldMap'
import { Types } from './components/WorldMap/types'
function App() {
  const [mapData, setMapData] = useState<Types.MapObject>({ mapFeatures: [] })
  const [coordinatesData, setCoordinatesData] = useState<Types.CoordinatesData[]>([])
  useEffect(() => {
    if (coordinatesData.length === 0) {
      const fileNames = ['./data/world-110m.json', './data/coordinates.csv']
      queue()
        .defer(json, fileNames[0])
        .defer(csv, fileNames[1])
        .await((error, d1, d2: Types.CoordinatesData[]) => {
          if (error) {
            // eslint-disable-next-line no-console
            console.log(`Houston we have a problem:${error}`)
          }
          setMapData({ mapFeatures: ((feature(d1, d1.objects.countries) as unknown) as FeatureCollection).features })
          setCoordinatesData(d2)
        })
    }
  })
  return (
    <div className="App">
      <header className="App-header">
        <WorldMap mapData={mapData} coordinatesData={coordinatesData} scale={200} cx={400} cy={150} initRotation={50} rotationSpeed={0.5} />
      </header>
    </div>
  )
}
export default App

Let’s review the code.

We add imports for geojson and topojson-client since we will be uploading the data here. I am also using d3-request to load the data instead of the fetch I used previously.
import { queue } from 'd3-queue'
import { csv, json } from 'd3-request'
import { FeatureCollection } from 'geojson'
import { feature } from 'topojson-client'
import WorldMap from './components/WorldMap/WorldMap'
import { Types } from './components/WorldMap/types'
I am setting the data feeds as the state; this will allow me to assign the props and make sure React refreshes the props once the data is loaded.
  const [mapData, setMapData] = useState<Types.MapObject>({ 'mapFeatures': [] })
  const [coordinatesData, setCoordinatesData] = useState<Types.CoordinatesData[]>([])

The useEffect hook will be doing the heavy lifting. The if statement ensures I am not loading my data multiple times.

The D3 queue will load the two data feeds and set the state.
  useEffect(() => {
    if ( coordinatesData.length === 0 ) {
      const fileNames = ['./data/world-110m.json', './data/coordinates.csv']
      queue()
        .defer(json, fileNames[0])
        .defer(csv, fileNames[1])
        .await((error, d1, d2: Types.CoordinatesData[]) => {
          if (error) {
            console.log(`Houston we have a problem:${  error}`)
          }
          setMapData({ mapFeatures: ((feature(d1, d1.objects.countries) as unknown) as FeatureCollection).features })
          setCoordinatesData(d2)
        })
    }
  })
Lastly, I need to set the WorldMap with the data and attribute props to match the props interface we set for the WorldMap component.
        <WorldMap mapData={mapData} coordinatesData={coordinatesData} scale={200} cx={400} cy={150} initRotation={50} rotationSpeed={0.5} />

Nothing really changes, from the user view perspective, once you check port 3000: http://localhost:3000. However, my code is more organized and easier to read as well as ready to implement state management such as Recoil or Redux since the data is extracted from the actual component and can be shared with multiple components.

Summary

In this chapter, we created a world map with the help of D3, TopoJSON, and React. The ability to draw maps as a background and add dots, animations, and interaction can help create a compelling chart that can be used for many things to tell your story.

In this chapter, I broke down the steps into five parts and created five components.
  • World map atlas: WorldMapAtlas.tsx

  • Round world map: RoundWorldMap.tsx

  • Rotating round world map : RotatingRoundWorldMap.tsx

  • Rotating round world map with coordinates: RotatingRoundWorldMapWithCoordinates.tsx

  • Refactoring: WorldMap.tsx

As you can see from this chapter, integrating the world map atlas using D3 with the help of topojson and geojson is straightforward.

Having React involved makes adding animations and interaction intuitive. TS helps ensure we understand what we are doing and that we avoid potential errors, and after doing some refactoring, you can see that our component is not just reusable but ready for state management.

In the next chapter, I will show you how to use the map we created here and implement Recoil state management and a list to create a widget that displays a résumé in an interactive way.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.157.45