Zach Steiner
Nov 15, 2018

Weather

  • JavaScript
  • UX
  • React

Main Forecast

I built this application while on the bench between client engagements to get better at interaction with REST APIs and put React Context through its paces. I also used it as an opportunity to experiment with some data driven CSS layouts.

Data comes from the Dark Sky API. The API request gets pushed into a context that can be accessed from children components, as an alternative to Redux. To ration API requests, I push the forecast object into local storage. On load there is a check that location hasn't changed since last pull and that last forecast update is less than 60 minutes ago. A manual refresh or location change will force an update of the forecast.

The temperature dial, inspired by a visualization on Apple Watch, changes color gradient end points (with some HSL computation) depending on the temperature of high and low. The position of the circle required some high school geometry that took way too long to retrieve.... The end result pushes the position and color to CSS custom properties:

import colorValue from './colorValue';

function hueCalc(temp) {
  return temp > 28 ? '50%' : '80%';
}

export default function temperatureColors(
  temperature,
  temperatureHigh,
  temperatureLow,
) {
  const hueLow = colorValue(temperatureLow);
  const colorLow = `hsl(${hueLow}, 85%, ${hueCalc(temperatureLow)})`;
  const hueHigh = colorValue(temperatureHigh);
  const colorHigh = `hsl(${hueHigh}, 85%, ${hueCalc(temperatureHigh)})`;

  let markerColor = 'transparent';

  if (temperature >= temperatureHigh - 0.5) {
    markerColor = colorHigh;
  } else if (temperature <= temperatureLow + 0.5) {
    markerColor = colorLow;
  }

  let root = document.documentElement;
  root.style.setProperty('--temp-low', colorLow);
  root.style.setProperty('--marker-color', markerColor);
  root.style.setProperty('--temp-high', colorHigh);
}
export default function temperaturePosition(
  temperature,
  temperatureHigh,
  temperatureLow,
) {
  if (temperature < temperatureLow) {
    temperature = temperatureLow;
  } else if (temperature > temperatureHigh) {
    temperature = temperatureHigh;
  }

  const currentRange = temperatureHigh - temperatureLow;
  const currentDifference = temperature - temperatureLow;

  let currentPercentage = currentDifference / currentRange;
  let currentAngle = currentPercentage * 180 - 180;

  let root = document.documentElement;
  root.style.setProperty('--current-position', `${currentAngle}deg`);
}

Background images are based on location search using Unsplash's API to retrieve a random photo from the location. I also integrated Mapbox's geocoding API to power the location search bar.

Hourly Forecast

The expanded hourly forecast required a fair amount of math to dynamically size and place them. The hourly forecasts take into account the total range for the 7 day forecast to position the bars on relative to the overall high and low for the week. Then the length of each bar is computed based on the range within the day. A similar method is used for the temperature dot graph on the time machine feature.

Hourly Forecast

Time Machine

The time machine data from Dark Sky allows you to pull historic data or future forecast based on past averages. Since the entire day is known, I experimented with React graphing libraries to display the data. The temperature graph is a custom CSS driven visualization, using similar layout techniques to the hourly graphs.

Time Machine

The line charts use Nivo Line after having tried Victory Charts. I found Nivo easier to work with and style, though both are robust enough to use in production environments. Massaging the API data into chart form did require some data manipulation:

import getDate from './getDate';

export default function buildConditionData(
  conditionList,
  hourlyData,
  timezone,
) {
  let hourlyConditions = {};

  conditionList.map(item => {
    let specificConditions = [];
    const conditionKey = item;

    hourlyData.map(item => {
      const conditionValue = item[conditionKey];
      const timeValue = getDate(item['time'], timezone);

      specificConditions.push({
        x: timeValue,
        y: conditionValue,
      });
      return specificConditions;
    });

    hourlyConditions[conditionKey] = [
      {
        id: conditionKey,
        data: specificConditions,
      },
    ];
    return hourlyConditions;
  });

  return hourlyConditions;
}

Graphs

Next steps for this project are to tailor the UI and feature set for hiking and outdoor activities, particularly ones in remote areas. I'm going to collaborate with Jackie Glimp on branding and design for the refreshed app. I will be swapping Dark Sky for NOAA data, providing a mobile friendly view into their fantastic hourly forecast data, while also making the forecasts more discoverable for outdoors types.