DEV Community

Cover image for Create Custom Forecast Flower Icons with D3
Jesse Smith Byers
Jesse Smith Byers

Posted on

Create Custom Forecast Flower Icons with D3

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:

Imgur

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:

  1. Fetching data from the OpenWeatherMap API
  2. Calling the DrawFlowers function and passing it the fetched data
  3. 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:

    1. Defining the Petal Path for each type of data (temperature, wind speed, and precipitation
    2. Calculating sizes and scales based on the fetched data
    3. Cleaning the data and setting up a data object
    4. 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.
    Imgur
    const tPetalPath = 'M 0,0 C -30,-30 -30,-30 0,-100 C 30,-30 30,-30 0,0'; //TEMPERATURE

    Imgur
    const wPetalPath = 'M 0,0 C -40,-40 15,-50 50,-100 C 0,-50 0,0 0,0'; //WIND SPEED

    Imgur
    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 used translate(${(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))
    

    Imgur

    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

    Imgur

    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)