How To Draw A Spiral In Python
Generating Climate Temperature Spirals in Python
Published: May 21, 2022
Ed Hawkins, a climate scientist, tweeted the post-obit animated visualization in 2022 and captivated the globe:
This visualization shows the deviations from the average temperature between 1850 and 1900. Information technology was reshared millions of times over Twitter and Facebook and a version of it was even shown at the opening ceremony for the Rio Olympics. The visualization is compelling, because it helps viewers empathise both the varying fluctuations in temperatures, and the sharp overall increases in average temperatures in the last thirty years. You lot can read more almost the motivation behind this visualization on Ed Hawkins' website. In this blog mail, we'll walk through how to recreate this animated visualization in Python. We'll specifically be working with pandas (for representing and munging the data) and matplotlib (for visualizing the data). If you're unfamiliar with matplotlib, we recommend going through the Exploratory Data Visualization and Storytelling Through Data Visualization courses. We'll utilise the following libraries in this post:
- Python iii.6
- Pandas 0.22
- Matplotlib ii.2.2
Data cleaning
The underlying data was released by the Met Role in the United Kingdon, which does excellent work on weather and climate forecasting. The dataset tin exist downloaded directly
hither. The openclimatedata repo on Github contains some helpful data-cleaning code in this notebook. You'll need to scroll down to the section titled Global Temperatures. The following code reads the text file into a pandas information frame:
hadcrut = pd.read_csv( "HadCRUT.4.v.0.0.monthly_ns_avg.txt", delim_whitespace=True, usecols=[0, 1], header=None) Then, we need to:
- carve up the first column into
calendar monthandtwelvemonthcolumns - rename the
icolumn tovalue - select and save all but the outset column (
0)
hadcrut['year'] = hadcrut.iloc[:, 0].employ(lambda 10: x.divide("/")[0]).astype(int) hadcrut['month'] = hadcrut.iloc[:, 0].apply(lambda x: x.split("/")[1]).astype(int) hadcrut = hadcrut.rename(columns={ane: "value"})hadcrut = hadcrut.iloc[:, i:] hadcrut.head() | value | year | month | |
|---|---|---|---|
| 0 | -0.700 | 1850 | 1 |
| ane | -0.286 | 1850 | 2 |
| 2 | -0.732 | 1850 | three |
| iii | -0.563 | 1850 | 4 |
| 4 | -0.327 | 1850 | 5 |
To continue our data
tidy, permit'southward remove rows containing data from 2022 (since it'south the simply year with data on iii months, not all 12 months).
hadcrut = hadcrut.drop(hadcrut[hadcrut['year'] == 2022].index) Lastly, let's compute the mean of the global temperatures from 1850 to 1900 and subtract that value from the entire dataset. To make this easier, we'll create a
multiindex using the twelvemonth and month columns:
hadcrut = hadcrut.set_index(['year', 'calendar month']) This way, we are merely modifying values in the
value cavalcade (the bodily temperature values). Finally, calculate and subtract the hateful temperature from 1850 to 1900 and reset the index dorsum to the way it was before.
hadcrut -= hadcrut.loc[1850:1900].hateful() hadcrut = hadcrut.reset_index() hadcrut.head() | year | month | value | |
|---|---|---|---|
| 0 | 1850 | 1 | -0.386559 |
| 1 | 1850 | 2 | 0.027441 |
| ii | 1850 | 3 | -0.418559 |
| 3 | 1850 | four | -0.249559 |
| iv | 1850 | five | -0.013559 |
Cartesian versus polar coordinate system
There are a few key phases to recreating Ed'southward GIF:
- learning how to plot on a polar cooridnate system
- transforming the information for polar visualization
- customizing the aesthetics of the plot
- stepping through the visualization twelvemonth-by-yr and turning the plot into a GIF
Nosotros'll showtime past diving into plotting in a
polar coordinate system. Most of the plots you've probably seen (bar plots, box plots, scatter plots, etc.) live in the cartesian coordinate arrangement. In this organisation:
-
xandy(andz) tin range from negative infinity to positive infinity (if nosotros're sticking with real numbers) - the center coordinate is (0,0)
- nosotros can recollect of this arrangement as being rectangular
In contrast, the polar coordinate system is round and uses
r and theta. The r coordinate specifies the distance from the eye and can range from 0 to infinity. The theta coordinate specifies the angle from the origin and can range from 0 to 2*pi. To learn more almost the polar coordinate system, I suggest diving into the following links:
- Wikipedia: Polar coordinate system
- NRICH: An Introduction to Polar Coordinates
Preparing information for polar plotting
Permit'due south kickoff understand how the data was plotted in Ed Hawkins' original climate spirals plot. The temperature values for a single year span virtually an entire spiral / circle. You'll notice how the line spans from Jan to December but doesn't connect to Jan again. Hither's but the 1850 frame from the GIF:
This ways that we need to subset the data by year and use the post-obit coordinates:
-
r: temperature value for a given calendar month, adapted to contain no negative values.- Matplotlib supports plotting negative values, simply not in the way you lot think. We want -0.one to be closer to the centre than 0.one, which isn't the default matplotlib behavior.
- We besides want to get out some space around the origin of the plot for displaying the twelvemonth as text.
-
theta: generate 12 equally spaced bending values that span from 0 to 2*pi.
Let'south dive into how to plot just the data for the yr 1850 in matplotlib, and so scale upwardly to all years. If you're unfamiliar with creating Figure and Axes objects in matplotlib, I recommend our
Exploratory Data Visualization course. To generate a matplotlib Axes object that uses the polar organisation, we need to gear up the project parameter to "polar" when creating it.
fig = plt.figure(figsize=(8,8)) ax1 = plt.subplot(111, projection='polar') Here's what the default polar plot looks like:
To suit the data to contain no negative temperature values, we need to first calculate the minimum temperature value:
hadcrut['value'].min() -0.66055882352941175 Let's add
ane to all temperature values, so they'll be positive just there's still some infinite reserved around the origin for displaying text:
Let's too generate 12 evenly spaced values from 0 to 2*pi and use the starting time 12 every bit the theta values:
import numpy as np hc_1850 = hadcrut[hadcrut['year'] == 1850] fig = plt.figure(figsize=(8,8)) ax1 = plt.subplot(111, projection='polar') r = hc_1850['value'] + ane theta = np.linspace(0, ii*np.pi, 12) To plot data on a polar projection, we nonetheless use the
Axes.plot() method but now the kickoff value corresponds to the list of theta values and the second value corresponds to the listing of r values.
ax1.plot(theta, r) Here's what this plot looks similar:
Tweaking the aesthetics
To make our plot close to Ed Hawkins', permit's tweak the aesthetics. Most of the other matplotlib methods we're used to having when plotting unremarkably on a cartesian coordinate system acquit over. Internally, matplotlib considers
theta to be 10 and r to be y. To see this in action, we can hide all of the tick labels for both axes using:
ax1.axes.get_yaxis().set_ticklabels([]) ax1.axes.get_xaxis().set_ticklabels([]) Now, let'southward tweak the colors. We need the background colour inside the polar plot to be black, and the color surrounding the polar plot to be grey. We actually used an paradigm editing tool to find the exact black and gray color values, as
hex values:
- Gray: #323331
- Blackness: #000100
Nosotros can employ
fig.set_facecolor() to set the foreground color and Axes.set_axis_bgcolor() to set the background color of the plot:
fig.set_facecolor("#323331") ax1.set_axis_bgcolor('#000100') Adjacent, let'south add the championship using
Axes.set_title():
ax1.set_title("Global Temperature Change (1850-2017)", color='white', fontdict={'fontsize': xxx}) Lastly, allow's add together the text in the middle that specifies the current yr that's being visualized. We want this text to be at the origin
(0,0), we want the text to be white, have a large font size, and exist horizontally center-aligned.
ax1.text(0,0,"1850", color='white', size=thirty, ha='center') Here's what the plot looks like now (recollect that this is just for the year 1850).
Plotting the remaining years
To plot the spirals for the remaining years, we need to repeat what we but did simply for all of the years in the dataset. The i tweak nosotros should brand here is to manually set the axis limit for
r (or y in matplotlib). This is because matplotlib scales the size of the plot automatically based on the data that'due south used. This is why, in the last step, we observed that the data for just 1850 was displayed at the edge of the plotting area. Let's summate the maximum temperature value in the entire dataset and add a generous amount of padding (to friction match what Ed did).
hadcrut['value'].max() ane.4244411764705882 We can manually set the y-centrality limit using
Axes.set_ylim()
ax1.set_ylim(0, 3.25) Now, we can utilize a for loop to generate the rest of the data. Let'south exit out the code that generates the heart text for now (otherwise each year will generate text at the same point and it'll exist very messy):
fig = plt.figure(figsize=(14,14)) ax1 = plt.subplot(111, projection='polar') ax1.axes.get_yaxis().set_ticklabels([]) ax1.axes.get_xaxis().set_ticklabels([]) fig.set_facecolor("#323331") ax1.set_ylim(0, 3.25) theta = np.linspace(0, two*np.pi, 12) ax1.set_title("Global Temperature Change (1850-2017)", color='white', fontdict={'fontsize': 20}) ax1.set_axis_bgcolor('#000100') years = hadcrut['yr'].unique() for year in years: r = hadcrut[hadcrut['year'] == year]['value'] + one # ax1.text(0,0, str(year), color='white', size=30, ha='center') ax1.plot(theta, r) Here's what that plot looks like:
Customizing the colors
Right at present, the colors feel a bit random and don't represent to the gradual heating of the climate that the original visualization conveys well. In the original visualiation, the colors transition from blue / royal, to green, to yellowish.
This color scheme is known as a sequential colormap, because the progression of colors reflects some pregnant from the data. While it's like shooting fish in a barrel to specify a color map when creating a scatter plot in matplotlib (using the cm parameter from Axes.scatter(), there's no direct parameter to specify a colormap when creating a line plot. Tony Yu has an excellent brusque postal service on how to use a colormap when generating besprinkle plots, which we'll use here. Essentially, we utilise the colour (or c) parameter when calling the Axes.plot() method and draw colors from plt.cm.<colormap_name>(alphabetize). Here's how nosotros'd utilize the viridis colormap:
ax1.plot(theta, r, c=plt.cm.viridis(index)) # Index is a counter variable This will result in the plot having sequential colors from bluish to green, but to get to yellowish we can actually multiply the counter variable past
ii:
ax1.plot(theta, r, c=plt.cm.viridis(index*2)) Permit's reformat our code to incorporate this sequential colormap.
fig = plt.figure(figsize=(fourteen,14)) ax1 = plt.subplot(111, projection='polar') ax1.axes.get_yaxis().set_ticklabels([]) ax1.axes.get_xaxis().set_ticklabels([]) fig.set_facecolor("#323331") for index, year in enumerate(years): r = hadcrut[hadcrut['twelvemonth'] == twelvemonth]['value'] + 1 theta = np.linspace(0, ii*np.pi, 12) ax1.grid(False) ax1.set_title("Global Temperature Change (1850-2017)", color='white', fontdict={'fontsize': 20}) ax1.set_ylim(0, 3.25) ax1.set_axis_bgcolor('#000100') # ax1.text(0,0, str(year), colour='white', size=30, ha='center') ax1.plot(theta, r, c=plt.cm.viridis(index*2)) Here's what the resulting plot looks like:
Calculation temperature rings
While the plot we take correct now is pretty, a viewer can't actually understand the underlying data at all. There's no indication of the underlying temperature values anywhere in the visulaization. The original visualization had full, uniform rings at 0.0, 1.5, and 2.0 degrees Celsius to help with this. Because we added
1 to every temperature value, nosotros demand to practise the same matter hither when plotting these uniform rings also. The blueish ring was originally at 0.0 degrees Celsius, then we need to generate a ring where r=1. The first cherry ring was originally at 1.5, then we demand to plot information technology at 2.5. The concluding one at 2.0, so that needs to be 3.0.
full_circle_thetas = np.linspace(0, 2*np.pi, m) blue_line_one_radii = [one.0]*thousand red_line_one_radii = [2.5]*thou red_line_two_radii = [3.0]*thou ax1.plot(full_circle_thetas, blue_line_one_radii, c='blue') ax1.plot(full_circle_thetas, red_line_one_radii, c='reddish') ax1.plot(full_circle_thetas, red_line_two_radii, c='ruby') Lastly, nosotros can add the text specifying the ring's temperature values. All three of these text values are at the 0.v*pi angle, at varying distance values:
ax1.text(np.pi/two, 1.0, "0.0 C", color="bluish", ha='middle', fontdict={'fontsize': twenty}) ax1.text(np.pi/2, 2.5, "i.5 C", color="red", ha='center', fontdict={'fontsize': 20}) ax1.text(np.pi/ii, 3.0, "2.0 C", colour="red", ha='eye', fontdict={'fontsize': twenty}) Because the text for "0.5 C" gets obscured by the data, we may want to consider hiding it for the static plot version.
Generating The GIF Animation
Now we're set up to generate a GIF animation from the plot. An animation is a series of images that are displayed in rapid succession. We'll use the
matplotlib.animation.FuncAnimation role to aid u.s. with this. To take advantage of this role, nosotros need to write lawmaking that:
- defines the base plot advent and properties
- updates the plot between each frames with new data
We'll use the following required parameters when calling
FuncAnimation():
-
fig: the matplotlib Effigy object -
func: the update office that'south chosen between each frame -
frames: the number of frames (nosotros want one for each year) -
interval: the numer of milliseconds each frame is displayed (there are grand milliseconds in a 2d)
This function will return a
matplotlib.animation.FuncAnimation object, which has a save() method we can employ to write the blitheness to a GIF file. Hither's some skeleton lawmaking that reflects the workflow nosotros'll employ:
# To exist able to write out the animation as a GIF file import sysfrom matplotlib.blitheness import FuncAnimation # Create the base plot fig = plt.figure(figsize=(eight,8)) ax1 = plt.subplot(111, projection='polar') def update(i): # Specify how nosotros desire the plot to change in each frame. # We need to unravel the for loop nosotros had earlier. yr = years[i] r = hadcrut[hadcrut['year'] == year]['value'] + one ax1.plot(theta, r, c=plt.cm.viridis(i*2)) render ax1 anim = FuncAnimation(fig, update, frames=len(years), interval=50) anim.save('climate_spiral.gif', dpi=120, writer='imagemagick', savefig_kwargs={'facecolor': '#323331'}) All that's left now is to re-format our previous code and add it to the skeleton to a higher place. Nosotros encourage yous to practice this on your own, to practice programming using matplotlib.
Hither'southward what the final animation looks like in lower resolution (to decrease loading fourth dimension).
Side by side Steps
In this post, we explored:
- how to plot on a polar coordinate system
- how to customize text in a polar plot
- how to generate GIF animations by interpolating multiple plots
You're able to get most of the way to recreating the excellent climate spiral GIF Ed Hawkins originally released. Here are the few key things that are that nosotros didn't explore, but nosotros strongly encourage you to exercise so on your own:
- Adding month values to the outer rim of the polar plot/
- Adding the current year value in the center of the plot equally the blitheness is created.
- If yous try to practice this using the
FuncAcnimation()method, you'll detect that the twelvemonth values are stacked on elevation of each other (instead of clearing out the previous twelvemonth value and displaying a new year value).
- If yous try to practice this using the
- Adding a text signature to the bottom left and bottom right corners of the effigy.
- Tweaking how the text for 0.0 C, one.5 C, and 2.0 C intersect the static temperature rings.
Tags
Source: https://www.dataquest.io/blog/climate-temperature-spirals-python/
Posted by: cainwhiseve.blogspot.com

0 Response to "How To Draw A Spiral In Python"
Post a Comment