14.1.4 Environmental Data#

Duration:

35-40 minutes

Level:

Intermediate

Prerequisites:

NumPy fundamentals, basic Python, understanding of APIs

Overview#

Environmental data offers a rich source of creative material for generative art. Weather patterns, temperature gradients, wind flows, and atmospheric conditions contain inherent visual rhythms that can be transformed into compelling artwork. In this exercise, you will fetch real-time weather data from a public API and transform it into artistic visualizations using NumPy.

This exercise introduces the fundamental workflow of data-driven generative art: acquiring real-world data, processing it into numerical arrays, and mapping those values to visual properties like color, position, and intensity. The techniques learned here form the foundation for more complex data visualizations and demonstrate how the natural world can become a source of algorithmic creativity.

Learning Objectives#

By the end of this exercise, you will be able to:

  • Fetch real-time environmental data from REST APIs using Python

  • Parse JSON responses and convert them to NumPy arrays for processing

  • Apply data-to-visual mapping strategies (temperature to color, wind to direction)

  • Create multi-layered visualizations combining multiple data dimensions

Quick Start: See It In Action#

Run this code to create your first environmental data visualization:

Generate a temperature heatmap from live weather data#
 1import numpy as np
 2from PIL import Image
 3import requests
 4from scipy.ndimage import zoom
 5
 6# Fetch temperature for a single location
 7url = "https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41&current_weather=true"
 8response = requests.get(url)
 9data = response.json()
10temp = data['current_weather']['temperature']
11print(f"Current temperature in Berlin: {temp}C")
12
13# Create a simple temperature-based color
14normalized = (temp + 10) / 50  # Normalize -10 to 40 range
15r = int(min(255, normalized * 2 * 255))
16b = int(min(255, (1 - normalized) * 2 * 255))
17color = (r, 100, b)
18
19# Generate colored square
20image = np.full((200, 200, 3), color, dtype=np.uint8)
21Image.fromarray(image).save("quick_start.png")
Temperature heatmap showing Europe with blue regions in the north (cold) transitioning to red regions in the south (warm)

A temperature heatmap generated from live Open-Meteo API data. Cooler temperatures appear blue, warmer temperatures appear red, with smooth interpolation between data points.#

The visualization above was created by fetching temperature data for a grid of geographic points across Europe, then mapping each temperature value to a color on a blue-white-red gradient. The smooth appearance comes from interpolating between the discrete data points.

Core Concepts#

Concept 1: Fetching Environmental Data from APIs#

Modern weather services provide free APIs that return real-time measurements in machine-readable formats. An API (Application Programming Interface) allows your Python code to request data from remote servers using HTTP requests [REST2000].

The Open-Meteo API provides global weather data without requiring registration or API keys, making it ideal for educational projects [OpenMeteo2024]. A typical API request looks like this:

Fetching weather data from Open-Meteo#
 1import requests
 2
 3# Construct the API URL with parameters
 4latitude = 48.85    # Paris
 5longitude = 2.35
 6url = f"https://api.open-meteo.com/v1/forecast?latitude={latitude}&longitude={longitude}&current_weather=true"
 7
 8# Send HTTP GET request
 9response = requests.get(url, timeout=5)
10
11# Parse JSON response
12data = response.json()
13weather = data['current_weather']
14
15print(f"Temperature: {weather['temperature']}C")
16print(f"Wind Speed: {weather['windspeed']} km/h")
17print(f"Wind Direction: {weather['winddirection']} degrees")

The response contains structured data including temperature, wind speed, wind direction, and weather codes. For generative art, we typically fetch data for multiple locations to create spatial patterns.

Diagram showing data pipeline from API request through JSON parsing to NumPy array to color mapping to final PNG image

The environmental data art pipeline: HTTP requests fetch JSON data, which is parsed into NumPy arrays, mapped to colors, and rendered as images.#

Did You Know?

The Open-Meteo API aggregates data from national weather services worldwide, including NOAA (USA), DWD (Germany), Meteo France, ECMWF, and many others, providing a unified interface to global weather data without requiring API keys [OpenMeteo2024].

Concept 2: Data-to-Visual Mapping Strategies#

The core creative challenge in data visualization is choosing how to map data values to visual properties. Edward Tufte, a pioneer in data visualization, emphasizes that effective mappings should reveal patterns while maintaining data integrity [Tufte2001].

Temperature to Color: The most intuitive mapping for temperature uses a diverging color scale with blue for cold, white for neutral, and red for hot. This leverages our cultural associations with color and follows perceptual guidelines for effective map design [Brewer2003] [Harrower2003].

Temperature to color mapping function#
 1def temperature_to_color(temp, temp_min=-10, temp_max=35):
 2    """Map temperature to blue-white-red gradient."""
 3    # Normalize to 0-1 range
 4    normalized = (temp - temp_min) / (temp_max - temp_min)
 5    normalized = max(0, min(1, normalized))  # Clamp to valid range
 6
 7    if normalized < 0.5:
 8        # Cold: Blue to White
 9        t = normalized * 2
10        r, g, b = int(t * 255), int(t * 255), 255
11    else:
12        # Hot: White to Red
13        t = (normalized - 0.5) * 2
14        r, g, b = 255, int((1 - t) * 255), int((1 - t) * 255)
15
16    return (r, g, b)

Wind to Direction: Wind data includes both speed (scalar) and direction (angle). Direction maps naturally to arrow orientation, while speed can control arrow length or color intensity.

Color gradient bar showing temperature scale from -10C (blue) through 12C (white) to 35C (red)

The temperature color scale used in our visualizations. Cold temperatures appear blue, mild temperatures white, and hot temperatures red.#

Important

Always normalize your data before mapping to visual properties. Raw temperature values might range from -30 to 45 degrees, but color channels require 0-255. Normalization ensures consistent visual output regardless of the actual data range.

Concept 3: Compositing Multi-Dimensional Weather Art#

Real weather systems involve multiple interacting variables: temperature, humidity, wind, pressure, and precipitation. Compelling data art often layers multiple dimensions to create rich, informative visualizations [Cleveland1994].

The layering approach builds images from bottom to top:

  1. Base layer: Temperature gradient provides the background color field

  2. Overlay layer: Wind arrows or flow lines show atmospheric movement

  3. Effect layer: Humidity affects color saturation or adds cloud-like effects

  4. Detail layer: Precipitation adds particle effects or texture

Multi-layer composition approach#
 1# Layer 1: Temperature background
 2base_image = create_temperature_heatmap(temperatures)
 3
 4# Layer 2: Wind visualization
 5base_image = draw_wind_arrows(base_image, wind_speeds, wind_directions)
 6
 7# Layer 3: Humidity effect (affects saturation)
 8base_image = apply_humidity_saturation(base_image, humidity)
 9
10# Layer 4: Cloud overlay for high humidity
11final_image = add_cloud_effect(base_image, humidity)

Data artists like Jer Thorp and Nicholas Felton have pioneered approaches to transforming personal and environmental data into art, demonstrating that data visualization can be both informative and aesthetically compelling [Thorp2012] [Felton2014].

Multi-layered weather visualization showing temperature colors with flowing white wind lines and subtle cloud effects

A multi-dimensional weather visualization combining temperature (color), wind (flowing lines), humidity (saturation), and clouds (white overlay).#

Hands-On Exercises#

These exercises follow the PACL scaffolding model: first Execute existing code to understand the concepts, then Modify parameters to explore variations, and finally Re-code from scratch to demonstrate mastery.

Exercise 1: Execute and Explore#

Run the complete environmental art generator script:

environmental_art.py#
 1import numpy as np
 2from PIL import Image
 3import requests
 4from scipy.ndimage import zoom
 5
 6# Configuration
 7GRID_SIZE = 10       # 10x10 grid of data points
 8IMAGE_SIZE = 512     # Output image size
 9
10# Geographic bounds (Europe)
11LAT_MIN, LAT_MAX = 35.0, 60.0
12LON_MIN, LON_MAX = -10.0, 25.0
13
14def fetch_temperature_grid(lat_min, lat_max, lon_min, lon_max, grid_size):
15    """Fetch temperature data for a grid of geographic points."""
16    temperatures = np.zeros((grid_size, grid_size), dtype=np.float32)
17    latitudes = np.linspace(lat_max, lat_min, grid_size)
18    longitudes = np.linspace(lon_min, lon_max, grid_size)
19
20    for i, lat in enumerate(latitudes):
21        for j, lon in enumerate(longitudes):
22            url = f"https://api.open-meteo.com/v1/forecast?latitude={lat:.2f}&longitude={lon:.2f}&current_weather=true"
23            try:
24                response = requests.get(url, timeout=5)
25                data = response.json()
26                temperatures[i, j] = data['current_weather']['temperature']
27            except:
28                temperatures[i, j] = 10  # Fallback value
29
30    return temperatures
31
32def temperature_to_color(temp, temp_min=-10, temp_max=35):
33    """Map temperature to blue-white-red gradient."""
34    normalized = np.clip((temp - temp_min) / (temp_max - temp_min), 0, 1)
35    if normalized < 0.5:
36        t = normalized * 2
37        return (int(t * 255), int(t * 255), 255)
38    else:
39        t = (normalized - 0.5) * 2
40        return (255, int((1-t) * 255), int((1-t) * 255))
41
42def create_heatmap(temperatures, image_size):
43    """Create heatmap from temperature grid."""
44    scale = image_size / temperatures.shape[0]
45    upscaled = zoom(temperatures, scale, order=1)[:image_size, :image_size]
46
47    image = np.zeros((image_size, image_size, 3), dtype=np.uint8)
48    temp_min, temp_max = upscaled.min() - 5, upscaled.max() + 5
49
50    for y in range(image_size):
51        for x in range(image_size):
52            image[y, x] = temperature_to_color(upscaled[y, x], temp_min, temp_max)
53
54    return image
55
56# Main execution
57print("Fetching weather data...")
58temperatures = fetch_temperature_grid(LAT_MIN, LAT_MAX, LON_MIN, LON_MAX, GRID_SIZE)
59print(f"Temperature range: {temperatures.min():.1f}C to {temperatures.max():.1f}C")
60
61image = create_heatmap(temperatures, IMAGE_SIZE)
62Image.fromarray(image).save("environmental_art.png")
63print("Saved environmental_art.png")

After running the code, answer these reflection questions:

  1. How does the temperature distribution reflect the geography of Europe (latitude, coastlines)?

  2. Why do we use scipy.ndimage.zoom instead of simply repeating pixels?

  3. What would happen if we changed the color gradient to green-yellow-red instead?

Answers and Explanation
  1. Geographic patterns: The visualization typically shows cooler temperatures in northern Europe (Scandinavia) and warmer temperatures in southern Europe (Mediterranean). Coastal areas often show more moderate temperatures than continental interiors due to ocean thermal regulation.

  2. Smooth interpolation: scipy.ndimage.zoom with order=1 performs bilinear interpolation, creating smooth gradients between data points. Simple pixel repetition would create blocky, pixelated output that doesn’t represent the continuous nature of temperature fields.

  3. Green-yellow-red: This palette would create a different aesthetic, often used for vegetation health or air quality indices. The visual impact depends on cultural associations; blue-red feels more intuitive for temperature because we associate blue with cold and red with heat.

Exercise 2: Modify Parameters#

Experiment with the visualization by modifying these parameters:

Goal 1: Change the geographic region to view different areas

Try different regions#
# North America
LAT_MIN, LAT_MAX = 25.0, 50.0
LON_MIN, LON_MAX = -125.0, -70.0

# Asia
LAT_MIN, LAT_MAX = 20.0, 50.0
LON_MIN, LON_MAX = 70.0, 140.0

# Your home region
# LAT_MIN, LAT_MAX = ?, ?
# LON_MIN, LON_MAX = ?, ?

Goal 2: Add wind visualization as an overlay

Modify the script to fetch wind data and display arrows:

Fetch wind data from API#
# In the fetch function, also get wind:
weather = data['current_weather']
wind_speed = weather['windspeed']
wind_direction = weather['winddirection']
Hint: Drawing wind arrows

Use the PIL ImageDraw module to draw arrows:

from PIL import ImageDraw
import math

def draw_arrow(draw, cx, cy, speed, direction):
    length = 10 + speed * 0.5
    angle = math.radians(direction + 180)
    dx = length * math.sin(angle)
    dy = -length * math.cos(angle)
    draw.line([(cx, cy), (cx + dx, cy + dy)], fill=(50, 50, 50), width=2)

Goal 3: Change the color palette for different data types

Alternative color palettes#
# Precipitation: Blue gradient
def precipitation_to_color(mm, max_mm=50):
    t = min(1, mm / max_mm)
    return (int((1-t) * 255), int((1-t) * 255), int(200 + t * 55))

# Humidity: Green gradient
def humidity_to_color(percent):
    t = percent / 100
    return (int((1-t) * 200), int(100 + t * 155), int((1-t) * 200))
Complete Exercise 2 Solution

See the wind_overlay.py script in this directory for a complete implementation that combines temperature heatmap with wind direction arrows.

Temperature heatmap with wind direction arrows overlaid

Temperature heatmap with wind direction arrows showing atmospheric flow patterns.#

Exercise 3: Create Weather Dashboard Art#

Create your own multi-layered weather visualization from scratch. Your artwork should combine at least two weather variables in a visually cohesive composition.

Requirements:

  • Fetch at least 2 different weather variables (temperature, wind, humidity, etc.)

  • Apply appropriate normalization to each variable

  • Use layered composition to combine the data dimensions

  • Create a visually appealing output (consider color harmony, visual balance)

Starter Code:

weather_dashboard_starter.py#
 1import numpy as np
 2from PIL import Image
 3import requests
 4from scipy.ndimage import zoom
 5
 6GRID_SIZE = 8
 7IMAGE_SIZE = 512
 8
 9def fetch_weather_data(lat_min, lat_max, lon_min, lon_max, grid_size):
10    """
11    TODO: Fetch temperature AND one other variable (wind, humidity, etc.)
12    Return a dictionary with arrays for each variable.
13    """
14    data = {
15        'temperature': np.zeros((grid_size, grid_size)),
16        # TODO: Add another variable
17    }
18
19    # TODO: Implement API fetching loop
20    # Hint: Add &hourly=relativehumidity_2m to get humidity
21
22    return data
23
24def create_visualization(data, image_size):
25    """
26    TODO: Create a multi-layered visualization.
27    Layer 1: Temperature background
28    Layer 2: Your chosen overlay (wind arrows, humidity effect, etc.)
29    """
30    image = np.zeros((image_size, image_size, 3), dtype=np.uint8)
31
32    # TODO: Implement your visualization
33
34    return image
35
36# Main
37weather = fetch_weather_data(40, 55, -5, 20, GRID_SIZE)
38result = create_visualization(weather, IMAGE_SIZE)
39Image.fromarray(result).save("my_weather_art.png")
Hint 1: Fetching humidity data

Add &hourly=relativehumidity_2m to your API URL:

url = f"...&current_weather=true&hourly=relativehumidity_2m"
data = response.json()
humidity = data['hourly']['relativehumidity_2m'][0]
Hint 2: Layering approach

Build your image layer by layer:

# Start with temperature background
image = create_temperature_layer(temps)

# Modify saturation based on humidity
image = adjust_saturation(image, humidity)

# Add wind flow lines on top
image = draw_wind_lines(image, winds)
Complete Solution

See weather_dashboard_art.py for a complete implementation that combines:

  • Temperature gradient background (sunset color palette)

  • Humidity-based saturation adjustment

  • Flowing wind lines

  • Cloud overlay effect for high humidity areas

 1# Key components of the solution:
 2
 3def temperature_to_color(temp, temp_min=-5, temp_max=20):
 4    """Sunset palette: blue -> purple -> orange -> red"""
 5    normalized = np.clip((temp - temp_min) / (temp_max - temp_min), 0, 1)
 6    if normalized < 0.33:
 7        t = normalized * 3
 8        return (int(50 + t * 100), int(50 + t * 50), int(200 - t * 80))
 9    elif normalized < 0.66:
10        t = (normalized - 0.33) * 3
11        return (int(150 + t * 80), int(100 + t * 50), int(120 - t * 80))
12    else:
13        t = (normalized - 0.66) * 3
14        return (int(230 + t * 25), int(150 - t * 100), int(40 - t * 40))
15
16def add_humidity_effect(image, humidity):
17    """High humidity = more saturated colors"""
18    saturation = 0.5 + (humidity / 100) * 0.5
19    gray = np.mean(image, axis=2, keepdims=True)
20    return gray + saturation * (image - gray)

Challenge Extension: Add temporal animation by fetching the 24-hour forecast and creating a GIF that shows how weather patterns evolve over time.

Summary#

In 35 minutes, you have learned how to transform real-world environmental data into generative art.

Key Takeaways#

  • REST APIs provide access to real-time environmental data in JSON format

  • Data normalization is essential before mapping values to visual properties

  • Diverging color scales (blue-white-red) effectively communicate temperature ranges

  • Multi-layer composition allows combining multiple data dimensions artistically

  • Fallback strategies ensure code works even when APIs are unavailable

  • Environmental data contains inherent visual patterns that can inspire generative art

Common Pitfalls#

  • Rate limiting: Making too many API requests too quickly can result in blocked requests

  • Timeout handling: Always use timeouts when making network requests

  • Data range assumptions: Temperature ranges vary by season and location; use dynamic normalization

  • Color overflow: Ensure RGB values stay within 0-255 range using np.clip

  • Missing data: APIs may return incomplete data; always have fallback values

Next Steps#

Continue to 14.2.1 - Network Graphs to learn about visualizing relationships and connections in data, or explore 14.2.4 - Time Series Art to create temporal visualizations from sequential data.

References#

[Tufte2001]

Tufte, E. R. (2001). The Visual Display of Quantitative Information (2nd ed.). Graphics Press. ISBN: 978-0-9613921-4-7 [Classic reference for data visualization principles and best practices]

[Brewer2003]

Brewer, C. A. (2003). ColorBrewer: Color advice for cartography. Pennsylvania State University. https://colorbrewer2.org/ [Tool for selecting effective color schemes for maps and data visualization]

[OpenMeteo2024] (1,2)

Open-Meteo. (2024). Open-Meteo Free Weather API Documentation. https://open-meteo.com/en/docs [Free, open-source weather API used in this exercise]

[Cleveland1994]

Cleveland, W. S. (1994). The Elements of Graphing Data (Revised ed.). Hobart Press. ISBN: 978-0963488411 [Scientific visualization principles and perceptual guidelines]

[Harrower2003]

Harrower, M., & Brewer, C. A. (2003). ColorBrewer.org: An online tool for selecting colour schemes for maps. The Cartographic Journal, 40(1), 27-37. https://doi.org/10.1179/000870403235002042 [Research on perceptually-based color selection]

[Thorp2012]

Thorp, J. (2012). Make data more human. TED Talk. https://www.ted.com/talks/jer_thorp_make_data_more_human [Data artist discussing humanizing data visualization]

[Felton2014]

Felton, N. (2014). Feltron Annual Report. http://feltron.com/ [Pioneer of personal data visualization as art form]

[REST2000]

Fielding, R. T. (2000). Architectural Styles and the Design of Network-based Software Architectures. Doctoral dissertation, University of California, Irvine. https://ics.uci.edu/~fielding/pubs/dissertation/fielding_dissertation.pdf [Original REST architecture dissertation]

[NumPyDocs]

NumPy Developers. (2024). NumPy User Guide. NumPy Documentation. https://numpy.org/doc/stable/user/index.html

[PillowDocs]

Clark, A., et al. (2024). Pillow: Python Imaging Library (Version 10.x). https://pillow.readthedocs.io/