Weather

dataviz
chart-challenge
TIL
#30DayChartChallenge Day 16
Author
Published

April 16, 2024

Weather

Today’s theme is Weather and I while searching for inspiration I came across these beautiful wind rose charts from the Lets-Plot gallery.opted for a simple interpretation of showing the changing monthly temperature distribution over a year. Data is taken from a Python version of Forecasting: Principles and Practise.

As always, I like to adapt the examples to my home country of Singapore and data for Singapore’s wind readings was provided by Iowa State University’s Iowa Environmental Mesonet site.

Code
import polars as pl
from lets_plot import *
from lets_plot.mapping import as_discrete
LetsPlot.setup_html()

raw = pl.read_csv('./sg_wind.csv')

CALM = 15.59

df = (raw
    .with_columns(
        pl.Series(name="Direction",values=list(range(0,360,10)))
    )
    .drop('Calm     ')
    .rename({
        ' 2.0  4.9': '2.0 - 4.9',
        ' 5.0  6.9': '5.0 - 6.9',
        ' 7.0  9.9': '7.0 - 9.9',
        '10.0 14.9': '10.0 - 14.9',
        '15.0 19.9': '15.0 - 19.9',
        '20.0+    ': '20.0+'
    })
    .melt(id_vars='Direction')
    .with_columns(pl.col('variable').replace(
        old=['2.0 - 4.9','5.0 - 6.9','7.0 - 9.9','10.0 - 14.9','15.0 - 19.9','20.0+'],
        new = [0,1,2,3,4,5]).cast(pl.Int32).alias('Speed Group')
    )
)

Creativity on display

One thing that really fascinated me was how easily different component geoms came together to create the final chart. A true testament to the power of The Grammer of Graphics.

We start first with a simple stacked bar chart of wind readings for every direction (0 to 360 degrees).

Code
(ggplot(df)
    + geom_bar(
        aes('Direction', 'value', fill=as_discrete('Speed Group')), 
        size=0, width=.8,
        stat='identity',
        tooltips=layer_tooltips().format('^y', '{.2g}%').format('^x', '{}°')
    ) 
)

Then we shift the chart up by inserting a blank rectangle underneath the chart. At this point, a seemingly strange decision.

Code
(ggplot(df)
    + geom_bar(
        aes('Direction', 'value', fill=as_discrete('Speed Group')), 
        size=0, width=.8,
        stat='identity',
        tooltips=layer_tooltips().format('^y', '{.2g}%').format('^x', '{}°')
    ) 
    + geom_rect(
        # Visually align the width of the rectangle with the bars - widen it by 5 (half a bar width)
        xmin=5, xmax=365, 
        ymin=-1, ymax=0, fill='white', size=0
    )
)

But it all comes together when we convert the y axis to polar coordinates. Our bar chart is now a compass. And that blank rectangle is wrapped into a perfect blank circle, for us to add annotations!

Code
(ggplot(df)
    + geom_bar(
        aes('Direction', 'value', fill=as_discrete('Speed Group')), 
        size=0, width=.8,
        stat='identity',
        tooltips=layer_tooltips().format('^y', '{.2g}%').format('^x', '{}°')
    ) 
    + geom_rect(
        # Visually align the width of the rectangle with the bars - widen it by 5 (half a bar width)
        xmin=5, xmax=365, 
        ymin=-1, ymax=0, fill='white', size=0
    )+ coord_polar(
        ylim=[-1, None], # -1 is to make inner circle
        start=(3.14 * 2) / 36 / 2 # Divide by 2 (i.e. rotate by half a bar width) to make the N-S axis perpendicular
    )
)

Final Chart

And with some finishing touches, we get a very professional looking final product.

Code
(ggplot(df)
    + geom_bar(
        aes('Direction', 'value', fill=as_discrete('Speed Group')), 
        size=0, width=.8,
        stat='identity',
        tooltips=layer_tooltips().format('^y', '{.2g}%').format('^x', '{}°')
    ) 
    + geom_rect(
        # Visually align the width of the rectangle with the bars - widen it by 5 (half a bar width)
        xmin=5, xmax=365, 
        ymin=-1, ymax=0, fill='white', size=0
    )
    + coord_polar(
        ylim=[-1, None], # -1 is to make inner circle
        start=(3.14 * 2) / 36 / 2 # Divide by 2 (i.e. rotate by half a bar width) to make the N-S axis perpendicular
    )
    + geom_hline(yintercept=0, size=2) 
    + geom_text(x=180, y=-1, label=f'Calm\n{CALM:.1f}%', 
        hjust='middle', vjust='center', size=6)
    + scale_fill_manual(
        name='Wind Speed (mph):', 
        values=['#002bff', '#03d3f8', '#7afe81', '#fde609', '#ff4404', '#780200'], 
        labels={
            0: '2 - 4.9', 
            1: '5 - 6.9', 
            2: '7 - 9.9', 
            3: '10 - 14.9', 
            4: '15 - 19.9', 
            5: '20+'
        }
    )
    + scale_y_continuous(
        breaks=[0, 1, 2, 3, 4, 5], # To not add automatically generated ticks for values outside of the data range
        format='{}%'
    )
    + scale_x_continuous(
        labels={
            45:  'NE', 
            90:  'E', 
            135: 'SE', 
            180: 'S', 
            225: 'SW',
            270: 'W',
            315: 'NW',
            360: 'N', 
        },
    ) 
    + labs(
        title = 'Wind Rose for Singapore',
        subtitle = 'Observations from 2001 to 2023',
        caption = '#30DayChartChallenge #Day16 Weather\nData: Iowa Environmental Mesonet\nMade by: www.ddanieltan.com'
    )
    + ggsize(800,800)
    + theme_minimal2()
    + theme(
        plot_title=element_text(size=24, face='bold'),
        plot_subtitle=element_text(size=18),
        plot_caption=element_text(size=12, color='grey'),
        panel_grid_minor_x=element_line(),
        panel_grid=element_line(color='#A0A0A0'),
        axis_ticks_y=element_line(),
        axis_text_x=element_text(size=18),
        axis_title=element_blank(),
    )
)

TIL

  1. To add a list as a column to a Polars DataFrame use `df.with_columns(pl.Series(…)). Akin to df[] = list(…) in Pandas.

  2. There’s a treasure chest of learnings to be gathered from the reference Lets-Plot notebook. So many little tweaks to add more polish to the final chart.

Reuse

Citation

BibTeX citation:
@online{tan2024,
  author = {Tan, Daniel},
  title = {Weather},
  date = {2024-04-16},
  url = {https://www.ddanieltan.com/posts/30-day-chart-16},
  langid = {en}
}
For attribution, please cite this work as:
Tan, Daniel. 2024. “Weather.” April 16, 2024. https://www.ddanieltan.com/posts/30-day-chart-16.