Instantly share code, notes, and snippets.

Embed
What would you like to do?
Code to reproduce the "Temperature Circle" visualization.
#
# Hi all,
# this is the Python code I used to make the visualization "Temperature circle"
# (https://twitter.com/anttilip/status/892318734244884480).
# Please be aware that originally I wrote this for my tests only so the
# code was not ment to be published and is a mess and has no comments.
# Feel free to improve, modify, do whatever you want with it. If you decide
# to use the code, make an improved version of it, or it is useful for you
# in some another way I would be happy to know about it. You can contact me
# for example in Twitter (@anttilip). Unchecked demo data (no quarantees)
# for year 2017 Jan-Jul is included here and this code draws only a single image.
# The animation code is basically just a loop through the years. To keep
# it simple, I only included one year here.
#
# Thanks and have fun!
# Antti
#
# ---------
#
# Copyright 2017 Antti Lipponen
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
from matplotlib.patches import Circle
backgroundcolor = '#faf2eb'
fontname = 'Lato'
yearname = '2017'
data2017 = {
'AMERICA': [
['Antigua and Barbuda', 0.68],
['Argentina', 0.89],
['Bahamas', 0.65],
['Barbados', 0.68],
['Belize', 1.22],
['Bolivia', 1.22],
['Brazil', 1.23],
['Canada', 1.72],
['Chile', 0.93],
['Colombia', 0.88],
['Costa Rica', 0.76],
['Cuba', 0.78],
['Dominica', 0.64],
['Dominican Republic', 0.82],
['Ecuador', 1.16],
['El Salvador', 0.66],
['Grenada', 0.75],
['Guatemala', 1.25],
['Guyana', 0.65],
['Haiti', 0.56],
['Honduras', 1.1],
['Jamaica', 0.51],
['Mexico', 1.75],
['Nicaragua', 0.96],
['Panama', 0.65],
['Paraguay', 1.02],
['Peru', 1.25],
['Saint Kitts and Nevis', 0.68],
['Saint Lucia', 0.73],
['Saint Vincent and the Grenadines', 0.75],
['Suriname', 0.62],
['Trinidad and Tobago', 0.73],
['United States', 1.92],
['Uruguay', 1.02],
['Venezuela', 0.86],
],
'OCEANIA': [
['Australia', 0.77],
['Fiji', 0.64],
['Kiribati', 0.21],
['Marshall Islands', 0.66],
['Micronesia', 0.9],
['Nauru', 0.82],
['New Zealand', 0.47],
['Palau', 0.94],
['Papua New Guinea', 0.92],
['Samoa', 0.77],
['Solomon Island', 1.0],
['Tonga', 0.86],
['Vanuatu', 1.17],
],
'EUROPE': [
['Albania', 1.07],
['Andorra', 1.88],
['Armenia', 0.38],
['Austria', 1.66],
['Azerbaijan', 0.51],
['Belarus', 1.58],
['Belgium', 1.79],
['Bosnia and Herzegovina', 1.4],
['Bulgaria', 0.89],
['Croatia', 1.5],
['Cyprus', 0.38],
['Czech Republic', 1.68],
['Denmark', 1.73],
['Estonia', 1.67],
['Finland', 1.48],
['France', 1.62],
['Georgia', 0.44],
['Germany', 1.76],
['Greece', 0.77],
['Hungary', 1.49],
['Iceland', 1.66],
['Ireland', 1.57],
['Italy', 1.57],
['Latvia', 1.70],
['Liechtenstein', 1.74],
['Lithuania', 1.70],
['Luxembourg', 1.79],
['Macedonia', 0.99],
['Malta', 1.03],
['Moldova', 1.12],
['Montenegro', 1.25],
['Netherlands', 1.77],
['Norway', 1.63],
['Poland', 1.67],
['Portugal', 1.71],
['Romania', 1.14],
['San Marino', 1.59],
['Serbia', 1.23],
['Slovakia', 1.56],
['Slovenia', 1.59],
['Spain', 1.89],
['Sweden', 1.69],
['Switzerland', 1.76],
['Ukraine', 1.23],
['United Kingdom', 1.68],
],
'AFRICA': [
['Algeria', 1.79],
['Angola', 0.70],
['Benin', 1.13],
['Botswana', 0.65],
['Burkina Faso', 1.20],
['Burundi', 1.20],
['Cameroon', 1.05],
['Cape Verde', 0.72],
['Central African Republic', 1.06],
['Chad', 1.04],
['Comoros', 0.90],
['Congo', 0.88],
['Democratic Republic of Congo', 0.97],
['Djibouti', 1.2],
['Egypt', 0.7],
['Equatorial Guinea', 0.92],
['Eritrea', 1.22],
['Ethiopia', 1.35],
['Gabon', 0.86],
['Gambia', 1.43],
['Ghana', 1.08],
['Guinea', 1.34],
['Guinea-Bissau', 1.39],
['Ivory Coast', 1.22],
['Kenya', 1.14],
['Lesotho', 0.84],
['Liberia', 1.21],
['Libya', 0.94],
['Madagascar', 1.16],
['Malawi', 0.89],
['Mali', 1.32],
['Mauritania', 1.56],
['Mauritius', 1.16],
['Morocco', 1.86],
['Mozambique', 0.90],
['Namibia', 0.94],
['Niger', 0.90],
['Nigeria', 1.10],
['Rwanda', 1.23],
['Sao Tome and Principe', 0.86],
['Senegal', 1.41],
['Seychelles', 0.99],
['Sierra Leone', 1.29],
['Somalia', 1.19],
['South Africa', 0.91],
['South Sudan', 1.27],
['Sudan', 1.17],
['Swaziland', 0.69],
['Tanzania', 1.01],
['Togo', 1.20],
['Tunisia', 1.81],
['Uganda', 1.26],
['Zambia', 0.59],
['Zimbabwe', 0.58],
],
'ASIA': [
['Afghanistan', 1.78],
['Bahrain', 1.48],
['Bangladesh', 0.52],
['Bhutan', 0.61],
['Brunei', 0.77],
['Burma (Myanmar)', 0.65],
['Cambodia', 0.84],
['China', 1.80],
['East Timor', 0.34],
['India', 0.96],
['Indonesia', 0.67],
['Iran', 1.48],
['Iraq', 0.68],
['Israel', 0.52],
['Japan', 1.03],
['Jordan', 0.56],
['Kazakhstan', 1.91],
['Kuwait', 1.24],
['Kyrgyzstan', 1.57],
['Laos', 0.87],
['Lebanon', 0.42],
['Malaysia', 0.79],
['Maldives', 0.70],
['Mongolia', 3.05],
['Nepal', 0.71],
['North Korea', 2.01],
['Oman', 1.53],
['Pakistan', 1.76],
['Philippines', 0.81],
['Qatar', 1.86],
['Russian Federation', 3.01],
['Saudi Arabia', 1.46],
['Singapore', 0.51],
['South Korea', 1.65],
['Sri Lanka', 0.90],
['Syria', 0.40],
['Tajikistan', 1.39],
['Thailand', 0.85],
['Turkey', 0.39],
['Turkmenistan', 1.50],
['United Arab Emirates', 2.08],
['Uzbekistan', 1.54],
['Vietnam', 0.72],
['Yemen', 1.37],
]
}
def rotText(areaText, defaultspacing, rotangleoffset, rText, fontname):
angle = areaText[0][1]
for ii, l in enumerate(areaText):
if ii > 0:
angle += defaultspacing + l[1]
plt.text(
(rText) * np.sin(np.deg2rad(angle)),
(rText) * np.cos(np.deg2rad(angle)),
'{}'.format(l[0]),
{'ha': 'center', 'va': 'center'},
rotation=-angle + rotangleoffset,
fontsize=15,
fontname=fontname,
)
plt.rcParams['axes.facecolor'] = backgroundcolor
mpl.rcParams.update({'font.size': 22})
cmap = plt.get_cmap('RdYlBu_r')
norm = mpl.colors.Normalize(vmin=-2.0, vmax=2.0)
Ncountries = 0
Ncontinents = 0
for countrylist in data2017.items():
Ncountries += len(countrylist[1])
Ncontinents += 1
spaceBetweenContinents = 3.0 # degrees
Nspaces = Ncontinents - 1
anglePerCountry = (345.0 - Nspaces * spaceBetweenContinents) / (Ncountries - 1)
fig, ax = plt.subplots(figsize=(12, 12))
renderer = fig.canvas.get_renderer()
transf = ax.transData.inverted()
limitangles = np.linspace(np.deg2rad(5.0), np.deg2rad(355.0), 500)
scaleRs = [
[1.5, '-2.0', True, 0.25],
[0.5 * (1.5 + 2.25), '-1.0', True, 0.25],
[2.25, '0.0', True, 1.0],
[0.5 * (3.0 + 2.25), '+1.0', True, 0.25],
[3.0, '+2.0', True, 0.25],
[3.3, '$^\\circ$C', False, 0.0]
]
for r in scaleRs:
if r[2]:
ax.plot(r[0] * np.sin(limitangles), r[0] * np.cos(limitangles), linewidth=r[3], color='#888888', linestyle='-')
plt.text(
0.0,
r[0],
'{}'.format(r[1]),
{'ha': 'center', 'va': 'center'},
fontsize=12,
fontname=fontname,
)
angle = 7.5
rText = 3.96
for continent in ['AFRICA', 'ASIA', 'EUROPE', 'AMERICA', 'OCEANIA']:
for country in data2017[continent]:
if angle < 185.0:
rotangle = -angle + 90.0
else:
rotangle = -angle - 90.0
plt.text(
(rText) * np.sin(np.deg2rad(angle)),
(rText) * np.cos(np.deg2rad(angle)),
'{}'.format(country[0]),
{'ha': 'center', 'va': 'center'},
rotation=rotangle,
fontsize=8,
fontname=fontname,
bbox={
'facecolor': backgroundcolor,
'linestyle': 'solid',
'linewidth': 0.0,
'boxstyle': 'square,pad=0.0'
}
)
ax.plot(
[1.3 * np.sin(np.deg2rad(angle)), 3.8 * np.sin(np.deg2rad(angle))],
[1.3 * np.cos(np.deg2rad(angle)), 3.8 * np.cos(np.deg2rad(angle))],
linewidth=0.6,
linestyle='--',
color='#DEDEDE'
)
lowerRoffset = 0.015
temperatureAnomaly = country[1]
rValue = 1.5 + (temperatureAnomaly + 2.0) / 4.0 * 1.5 # a lot more clever way for computing the radius should be used here...
ax.plot(
[(1.3 + lowerRoffset) * np.sin(np.deg2rad(angle)), rValue * np.sin(np.deg2rad(angle))],
[(1.3 + lowerRoffset) * np.cos(np.deg2rad(angle)), rValue * np.cos(np.deg2rad(angle))],
linewidth=4.3,
linestyle='-',
color='#202020'
)
ax.plot(
[(1.3 + lowerRoffset) * np.sin(np.deg2rad(angle)), rValue * np.sin(np.deg2rad(angle))],
[(1.3 + lowerRoffset) * np.cos(np.deg2rad(angle)), rValue * np.cos(np.deg2rad(angle))],
linewidth=4.0,
linestyle='-',
color=cmap(norm(temperatureAnomaly))
)
angle += anglePerCountry
angle += spaceBetweenContinents
c = Circle((0.0, 0.0), radius=1.0, fill=True, color='#fff9f5')
ax.add_patch(c)
plt.text(
0.0,
-0.52,
yearname,
{'ha': 'center', 'va': 'bottom'},
fontsize=40,
fontname=fontname,
)
plt.text(
0.0,
0.27,
'Year',
{'ha': 'center', 'va': 'center'},
fontsize=26,
fontname=fontname,
)
angles = np.linspace(np.deg2rad(0.0), np.deg2rad(360.0), 1000)
rs = [1.0, 1.3]
for r in rs:
ax.plot(r * np.sin(angles), r * np.cos(angles), linewidth=1.0, color='#666666', linestyle='-')
plt.text(
5.87,
-4.67,
'Antti Lipponen (@anttilip)',
{'ha': 'right', 'va': 'center'},
fontsize=10,
fontname=fontname,
)
plt.text(
-6.3 + 0.015,
4.385 - 0.015,
'Temperature anomalies',
{'ha': 'left', 'va': 'center'},
fontsize=27,
fontname=fontname,
color='#909090'
)
plt.text(
-6.3,
4.385,
'Temperature anomalies',
{'ha': 'left', 'va': 'center'},
fontsize=27,
fontname=fontname,
color='#0D0D0D'
)
plt.text(
-6.35,
-4.35,
'Data source:\nNASA GISS Surface Temperature Analysis (GISTEMP)\nLand-Ocean Temperature Index, ERSSTv4, 1200km smoothing\nhttps://data.giss.nasa.gov/gistemp/\nAverage of monthly temperature anomalies. GISTEMP base period 1951-1980.',
{'ha': 'left', 'va': 'center'},
fontsize=10,
fontname=fontname,
)
areaText = [
['A', 46.0],
['f', 0.3],
['r', -0.05],
['i', -0.15],
['c', -0.15],
['a', 0.2],
]
rText, defaultspacing, rotangleoffset = 1.13, 4.4, 0.0
rotText(areaText, defaultspacing, rotangleoffset, rText, fontname)
areaText = [
['E', 236.0],
['u', 0.0],
['r', 0.3],
['o', 0.7],
['p', 0.0],
['e', 0.0],
]
rText, defaultspacing, rotangleoffset = 1.155, -5.5, 180.0
rotText(areaText, defaultspacing, rotangleoffset, rText, fontname)
areaText = [
['A', 147.0],
['s', -0.8],
['i', 0.0],
['a', 0.0],
]
rText, defaultspacing, rotangleoffset = 1.155, -4.7, 180.0
rotText(areaText, defaultspacing, rotangleoffset, rText, fontname)
areaText = [
['A', 276.0],
['m', 2.5],
['e', 0.6],
['r', -0.15],
['i', -2.0],
['c', -2.0],
['a', -0.15],
]
rText, defaultspacing, rotangleoffset = 1.13, 5.85, 0.0
rotText(areaText, defaultspacing, rotangleoffset, rText, fontname)
areaText = [
['O', 328.5],
['c', 1.0],
['e', 0.0],
['a', 0.2],
['n', 0.2],
['i', -0.3],
['a', -0.3],
]
rText, defaultspacing, rotangleoffset = 1.125, 4.8, 0.0
rotText(areaText, defaultspacing, rotangleoffset, rText, fontname)
ax.set_xlim([-5.0, 5.0])
ax.set_ylim([-5.0, 5.0])
plt.axis('off')
plt.savefig('temperatureCircle.png', facecolor=backgroundcolor, edgecolor='none', dpi=160)
plt.close()
# and finally I used imageMagick to crop the image for animation
@ramonskovitch

This comment has been minimized.

ramonskovitch commented Aug 3, 2017

Hello Antti;

Being new to python how would one "loop through the years" with additional data? Could I possibly get an example of the above with say 2 years to see how to do it?

Thank you

Dan Raymond

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment