Inspired by Shirley Wu's Film Flowers and Annie Liao's Baby Names, I decided to try representing weather data with flowers. When I look at a weather forecast, I generally want quick answers to these 3 questions:
- Will it be getting warmer or cooler throughout the week?
- How will tomorrow's wind compare to today?
- Are we expecting any rain this week?
By fetching forecast data from an external API, and using flowers to visualize temperature, wind speed, and precipitation amount, I was able to create custom icons that give us a relative sense of how the weather will change from day to day across the week.
Resources
I used the following resources to research and plan out my design:
- Coding Train tutorial with Shirley Wu
- Observable sandbox for testing out my petal paths and flower designs
- OpenWeatherMap One Call API
- D3 documentation
Step 1: Set up a Basic React App
I started by setting up a very simple react app to house my project. The App.js component is responsible for:
- Fetching data from the OpenWeatherMap API
- Calling the DrawFlowers function and passing it the fetched data
- Setting up a placeholder that we will be attaching our element to later
App.js
import React, { useEffect } from 'react'; import { drawFlowers } from './d3/DrawFlowers' import './App.css'; function App() { useEffect( () => { async function fetchData() { let requestOptions = { method: 'GET', redirect: 'follow' }; let apiKey = process.env.REACT_APP_API_KEY let lat = "44.811345" let lon = "-73.149572" let apiUrl = "https://api.openweathermap.org/data/2.5/onecall?lat=" + lat + "&lon=" + lon + "&units=imperial&exclude=current,minutely,hourly&appid=" + apiKey const response = await fetch(apiUrl, requestOptions) const data = await response.json() drawFlowers(data) } fetchData(); }, []); return ( <div className="viz"> </div> ); } export default App;
Step 2: Set up the D3 File
Following the advice of Leigh Steiner in React + D3 : The Macaroni and Cheese of the Data Visualization World, I decided to keep all of my D3 logic confined to a D3 file, separate from my react components. This file includes one function,
DrawFlowers(data)
, which is passed the data fetched from the API.This function is responsible for the following 4 tasks, which will be broken down in the next sections:
- Defining the Petal Path for each type of data (temperature, wind speed, and precipitation
- Calculating sizes and scales based on the fetched data
- Cleaning the data and setting up a data object
- Creating and appending the element, the flower groups, and the text elements on the DOM.
DrawFlowers.js
import * as d3 from 'd3' let _ = require('lodash') export const drawFlowers = (days) => { // DEFINE THE PETAL PATH FOR EACH TYPE OF DATA // CALCULATE SIZES AND SCALES FROM FETCHED DATA // CLEANING THE DATA AND SETTING UP DATA OBJECT // APPENDING SVG ELEMENT, FLOWER GROUPS, AND TEXT ELEMENTS TO THE DOM }
Step 3: Build the D3 Logic to Create Flower Icons
The majority of this project involved working with D3 to create the petal shapes, assemble petals into flowers based on data, and append everything to the DOM.
Defining Petal Paths
The first challenge was designing petals. I wanted to design a slightly different petal shape for temperature, wind speed, and precipitation, and I wanted the design to resemble the data type. I ended up drawing petals that resembled rays of sunlight for temperature, wispy petals for wind speed, and droplet-shaped petals for precipitation.
const tPetalPath = 'M 0,0 C -30,-30 -30,-30 0,-100 C 30,-30 30,-30 0,0'; //TEMPERATURE
const wPetalPath = 'M 0,0 C -40,-40 15,-50 50,-100 C 0,-50 0,0 0,0'; //WIND SPEED
const pPetalPath = 'M 0,0 C -60,-30 0,-40 0,-100 C 0,-40 60,-30 0,0'; //PRECIPITATION
I used Observable as a sandbox to test out these shapes as I was designing.
Calculate Sizes and Scales
I set up a number of size constants to help manage re-sizing elements as I worked.
const petalSize = 150 const height = 1500 const width = 1200 const sideMargin = 300 const topMargin = 200
I then used D3 methods to set up the scale and number of petals based on the data. The extent method was used to find the minimum and maximum values in the data, and the results were used to set the domain for the petal scales. The scaleQuantize method allows us to take the data and break it into discrete chunks, which allow us to represent values by numbers of petals, which are defined in the range arrays. I decided to give each petal scale a different range so that they would be more visually interesting when looking across the different data types.
// FINDING DOMAIN OF DATA FOR TEMPERATURE, PRECIPITATION, AND WIND SPEED const tempMinmax = d3.extent(data, d => d.temp.day); const windMinmax = d3.extent(data, d => d.wind_speed); const precipMinmax = d3.extent(data, d => d.rain); // DEFINING THE PETAL SCALES const tPetalScAle = d3.scaleQuantize().domain(tempMinmax).range([3, 5, 7, 9, 11, 13]); const wPetalScale = d3.scaleQuantize().domain(windMinmax).range([3, 6, 9, 12, 15, 18]); const pPetalScale = d3.scaleQuantize().domain(precipMinmax).range([3, 4, 5, 6, 7, 8]);
Clean the Data and Set Up Data Object
Next, we can use the fetched data and the scales we have already defined to build a data object. This object holds all of the forecast and scale data that is needed to build each flower, as well as label each flower with text and data values.
const flowersData = _.map(data, d => { const tempPetals = tPetalScale(d.temp.day); const windPetals = wPetalScale(d.wind_speed); const precipPetals = pPetalScale(d.rain); const petSize = 1 const date = new Date(d.dt * 1000).toLocaleDateString("en") const temperature = d.temp.day const windSpeed = d.wind_speed const precip = d.rain return { petSize, tPetals: _.times(tempPetals, i => { return { angle: 360 * i / tempPetals, tPetalPath } }), wPetals: _.times(windPetals, i => { return { angle: 360 * i / windPetals, wPetalPath } }), pPetals: _.times(precipPetals, i => { return { angle: 360 * i / precipPetals, pPetalPath } }), tempPetals, windPetals, precipPetals, date, temperature, windSpeed, precip } })
Append the svg element, flower groups, and text elements to the DOM
Now that the
flowersData
object is set up, we are ready to start building the visualization on the DOM. First, we will set up an<svg>
element and attach it to the placeholder<div className="viz">
element that we set up in the React App.js component.
const svg = d3.select('.viz') .append('svg') .attr('height', height) .attr('width', width)
Next, we will start creating individual flowers by binding the flowersData to each flower
<g>
element. The transform, translate attribute is used to position the flowers. I chose to arrange the 8 day forecast vertically (one in each row), so I usedtranslate(${(i % 1) * petalSize + sideMargin}, ${Math.floor(i / 1) * petalSize + topMargin})scale(${d.petSize})
. To display the flower icons horizontally (8 in a row), we can alter our modulo and division expressions like so:translate(${(i % 8) * petalSize + sideMargin}, ${Math.floor(i / 8) * petalSize + topMargin})scale(${d.petSize})
const flowers = d3.select('svg') .selectAll('g') .data(flowersData) .enter() .append('g') .attr('transform', (d, i) => `translate(${(i % 1) * petalSize + sideMargin}, ${Math.floor(i / 1) * petalSize + topMargin})scale(${d.petSize})`)
We can use similar logic to create flowers for each day representing temperature, wind speed, and precipitation. In this code, the transform, translate attribute is used to position each flower in rows and columns. Each data type receives a different color scale and petal shape.
// ADDING TEMPERATURE FLOWERS flowers.selectAll('path') .data(d => d.tPetals) .enter() .append('path') .attr('d', d => d.tPetalPath) .attr('transform', d => `rotate(${d.angle})`) .attr('fill', (d, i) => d3.interpolateYlOrRd(d.angle / 360)) // ADDING WIND FLOWERS flowers.append('g') .attr("transform", "translate(200, 0)") .selectAll('path') .data(d => d.wPetals) .enter() .append('path') .attr('d', d => d.wPetalPath) .attr('transform', d => `rotate(${d.angle})`) .attr('fill', (d, i) => d3.interpolateBuGn(d.angle / 360)) // ADDING PRECIPITATION FLOWERS flowers.append('g') .attr("transform", "translate(400, 0)") .selectAll('path') .data(d => d.pPetals) .enter() .append('path') .attr('d', d => d.pPetalPath) .attr('transform', d => `rotate(${d.angle})`) .attr('fill', (d, i) => d3.interpolateYlGnBu(d.angle / 360))
Finally, we can finish this off by adding text labels for each day's data, as well as headers for each column, or data type. We can use the x and y attributes to position each label.
// ADDING TEXT FOR EACH FLOWER flowers.append('text') .text(d => `${d.date}` ) .attr('text-anchor', 'middle') .attr('y', -20) .attr('x', -200) flowers.append('text') .text(d => `Temperature: ${d.temperature} F` ) .attr('text-anchor', 'middle') .attr('y', 0) .attr('x', -200) flowers.append('text') .text(d => `Wind Speed: ${d.windSpeed} MPH` ) .attr('text-anchor', 'middle') .attr('y', 20) .attr('x', -200) flowers.append('text') .text(d => d.precip ? `Precipitation: ${d.precip} mm` : `Precipitation: 0 mm`) .attr('text-anchor', 'middle') .attr('y', 40) .attr('x', -200) // ADDING HEADER TEXT TO THE SVG svg.append('text') .text("Temperature (degrees F)") .attr('text-anchor', 'middle') .attr('y', 75) .attr('x', 300) svg.append('text') .text("Wind Speed (MPH)") .attr('text-anchor', 'middle') .attr('y', 75) .attr('x', 500) svg.append('text') .text("Precipitation (mm)") .attr('text-anchor', 'middle') .attr('y', 75) .attr('x', 700)
Final Thoughts, Next Steps
Although I am pleased with how this project turned out, there is quite a bit more I would like to do. My next steps include:
- Experimenting with color scales and size scales to better represent the data.
- Adding more data points, such as humidity and precipitation type.
- Integrating React forms field and buttons to allow users to display weather forecast data for anywhere in the world.
If you want to check out the repo, click here. Feel free to fork and clone it to play around with the icons...but you will need to get your own Open Weather API key in order to play around with it in the browser.
Top comments (0)