I am just starting out with Python and probably in a little bit over my head here, but I would like to combine this example, by Zack Fizell, with this solution, by Thomas Kühn.
The data below creates a world map with "fire balls". The issue is that when zooming in, the size of the circles remains the same (in absolute terms) and they become smaller relative to the background. I would like to apply the solution by Thomas Kühn to this example by Zack Fizell (not really for the fireballs, but for my own data using a world map).
The problem is that when fiddling around with the code, I hardly ever get a useful error message. Which makes it hard to figure out where I'm going wrong.
I have included all the code, so that it is still an interesting question when the link dies at some point (I am just not sure what to do with the csv-file needed to run the example).
It became quite a long question but I hope it is at least an interesting one.
Please feel free to give me tips on how to improve this question
The example data is as follows:
# Importing libraries
import matplotlib.pyplot as plt
import pandas as pd
import geopandas as gpd
# Reading cvs file using pandas
df = pd.read_csv('cneos_fireball_data.csv',
usecols=["Peak Brightness Date/Time (UT)",
"Calculated Total Impact Energy (kt)",
"Latitude (deg.)", "Longitude (deg.)"])
df = df.rename(columns={"Peak Brightness Date/Time (UT)":
'Datetime',
"Calculated Total Impact Energy (kt)":
'Impact Energy [kt]',
"Latitude (deg.)": 'Latitude',
"Longitude (deg.)": 'Longitude'})
# Converting to a datetime datatype
df['Datetime'] = pd.to_datetime(df['Datetime'], errors='coerce')
# Applying +/- based on direction and converting to numeric datatype
for x in range(len(df['Longitude'])):
if str(df.loc[x, 'Longitude'])[-1] == 'E':
df.loc[x, 'Longitude'] = str(df.loc[x, 'Longitude'])[:-1]
if str(df.loc[x, 'Longitude'])[-1] == 'W':
df.loc[x, 'Longitude'] = \
'-' + str(df.loc[x, 'Longitude'])[:-1]
for x in range(len(df['Latitude'])):
if str(df.loc[x, 'Latitude'])[-1] == 'N':
df.loc[x, 'Latitude'] = str(df.loc[x, 'Latitude'])[:-1]
if str(df.loc[x, 'Latitude'])[-1] == 'S':
df.loc[x, 'Latitude'] = \
'-' + str(df.loc[x, 'Latitude'])[:-1]
df['Longitude'] = pd.to_numeric(df['Longitude'], errors='coerce')
df['Latitude'] = pd.to_numeric(df['Latitude'], errors='coerce')
# Converting to numeric datatype
threshold = 20
df = df[df['Impact Energy [kt]'] < threshold]
df['Impact Energy [kt]'] = pd.to_numeric(df['Impact Energy [kt]'],
errors='coerce')
# Dropping the errors from data conversions and resetting index
df.dropna()
df = df.reset_index(drop=True)
# From GeoPandas, our world map data
worldmap = gpd.read_file(gpd.datasets.get_path("naturalearth_lowres"))
# Creating axes and plotting world map
fig, ax = plt.subplots(figsize=(12, 6))
worldmap.plot(color="lightgrey", ax=ax)
# Plotting our Impact Energy data with a color map
x = df['Longitude']
y = df['Latitude']
z = df['Impact Energy [kt]']
plt.scatter(x, y, s=20*z, c=z, alpha=0.6, vmin=0, vmax=threshold,
cmap='autumn')
plt.colorbar(label='Impact Energy [kt]')
# Creating axis limits and title
plt.xlim([-180, 180])
plt.ylim([-90, 90])
first_year = df["Datetime"].min().strftime("%Y")
last_year = df["Datetime"].max().strftime("%Y")
plt.title("NASA: Fireballs Reported by Government Sensors\n" +
str(first_year) + " - " + str(last_year))
plt.xlabel("Longitude")
plt.ylabel("Latitude")
plt.show()
Result:
The point is that I am having trouble adapting the solution. Mostly because I do not understand the MarkerUpdater
class very well (see below).
Solution by Thomas Kühn
from matplotlib import pyplot as plt
import numpy as np
##plt.switch_backend('TkAgg')
class MarkerUpdater:
def __init__(self):
##for storing information about Figures and Axes
self.figs = {}
##for storing timers
self.timer_dict = {}
def add_ax(self, ax, features=[]):
ax_dict = self.figs.setdefault(ax.figure,dict())
ax_dict[ax] = {
'xlim' : ax.get_xlim(),
'ylim' : ax.get_ylim(),
'figw' : ax.figure.get_figwidth(),
'figh' : ax.figure.get_figheight(),
'scale_s' : 1.0,
'scale_a' : 1.0,
'features' : [features] if isinstance(features,str) else features,
}
ax.figure.canvas.mpl_connect('draw_event', self.update_axes)
def update_axes(self, event):
for fig,axes in self.figs.items():
if fig is event.canvas.figure:
for ax, args in axes.items():
##make sure the figure is re-drawn
update = True
fw = fig.get_figwidth()
fh = fig.get_figheight()
fac1 = min(fw/args['figw'], fh/args['figh'])
xl = ax.get_xlim()
yl = ax.get_ylim()
fac2 = min(
abs(args['xlim'][1]-args['xlim'][0])/abs(xl[1]-xl[0]),
abs(args['ylim'][1]-args['ylim'][0])/abs(yl[1]-yl[0])
)
##factor for marker size
facS = (fac1*fac2)/args['scale_s']
##factor for alpha -- limited to values smaller 1.0
facA = min(1.0,fac1*fac2)/args['scale_a']
##updating the artists
if facS != 1.0:
for line in ax.lines:
if 'size' in args['features']:
line.set_markersize(line.get_markersize()*facS)
if 'alpha' in args['features']:
alpha = line.get_alpha()
if alpha is not None:
line.set_alpha(alpha*facA)
for path in ax.collections:
if 'size' in args['features']:
path.set_sizes([s*facS**2 for s in path.get_sizes()])
if 'alpha' in args['features']:
alpha = path.get_alpha()
if alpha is not None:
path.set_alpha(alpha*facA)
args['scale_s'] *= facS
args['scale_a'] *= facA
self._redraw_later(fig)
def _redraw_later(self, fig):
timer = fig.canvas.new_timer(interval=10)
timer.single_shot = True
timer.add_callback(lambda : fig.canvas.draw_idle())
timer.start()
##stopping previous timer
if fig in self.timer_dict:
self.timer_dict[fig].stop()
##storing a reference to prevent garbage collection
self.timer_dict[fig] = timer
if __name__ == '__main__':
my_updater = MarkerUpdater()
##setting up the figure
fig, axes = plt.subplots(nrows = 2, ncols =2)#, figsize=(1,1))
ax1,ax2,ax3,ax4 = axes.flatten()
## a line plot
x1 = np.linspace(0,np.pi,30)
y1 = np.sin(x1)
ax1.plot(x1, y1, 'ro', markersize = 10, alpha = 0.8)
ax3.plot(x1, y1, 'ro', markersize = 10, alpha = 1)
## a scatter plot
x2 = np.random.normal(1,1,30)
y2 = np.random.normal(1,1,30)
ax2.scatter(x2,y2, c = 'b', s = 100, alpha = 0.6)
## scatter and line plot
ax4.scatter(x2,y2, c = 'b', s = 100, alpha = 0.6)
ax4.plot([0,0.5,1],[0,0.5,1],'ro', markersize = 10) ##note: no alpha value!
##setting up the updater
my_updater.add_ax(ax1, ['size']) ##line plot, only marker size
my_updater.add_ax(ax2, ['size']) ##scatter plot, only marker size
my_updater.add_ax(ax3, ['alpha']) ##line plot, only alpha
my_updater.add_ax(ax4, ['size', 'alpha']) ##scatter plot, marker size and alpha
plt.show()
Result:
My attempt (I have not touched the MarketUpdater):
from matplotlib import pyplot as plt
import numpy as np
##plt.switch_backend('TkAgg')
class MarkerUpdater:
def __init__(self):
##for storing information about Figures and Axes
self.figs = {}
##for storing timers
self.timer_dict = {}
def add_ax(self, ax, features=[]):
ax_dict = self.figs.setdefault(ax.figure,dict())
ax_dict[ax] = {
'xlim' : ax.get_xlim(),
'ylim' : ax.get_ylim(),
'figw' : ax.figure.get_figwidth(),
'figh' : ax.figure.get_figheight(),
'scale_s' : 1.0,
'scale_a' : 1.0,
'features' : [features] if isinstance(features,str) else features,
}
ax.figure.canvas.mpl_connect('draw_event', self.update_axes)
def update_axes(self, event):
for fig,axes in self.figs.items():
if fig is event.canvas.figure:
for ax, args in axes.items():
##make sure the figure is re-drawn
update = True
fw = fig.get_figwidth()
fh = fig.get_figheight()
fac1 = min(fw/args['figw'], fh/args['figh'])
xl = ax.get_xlim()
yl = ax.get_ylim()
fac2 = min(
abs(args['xlim'][1]-args['xlim'][0])/abs(xl[1]-xl[0]),
abs(args['ylim'][1]-args['ylim'][0])/abs(yl[1]-yl[0])
)
##factor for marker size
facS = (fac1*fac2)/args['scale_s']
##factor for alpha -- limited to values smaller 1.0
facA = min(1.0,fac1*fac2)/args['scale_a']
##updating the artists
if facS != 1.0:
for line in ax.lines:
if 'size' in args['features']:
line.set_markersize(line.get_markersize()*facS)
if 'alpha' in args['features']:
alpha = line.get_alpha()
if alpha is not None:
line.set_alpha(alpha*facA)
for path in ax.collections:
if 'size' in args['features']:
path.set_sizes([s*facS**2 for s in path.get_sizes()])
if 'alpha' in args['features']:
alpha = path.get_alpha()
if alpha is not None:
path.set_alpha(alpha*facA)
args['scale_s'] *= facS
args['scale_a'] *= facA
self._redraw_later(fig)
def _redraw_later(self, fig):
timer = fig.canvas.new_timer(interval=10)
timer.single_shot = True
timer.add_callback(lambda : fig.canvas.draw_idle())
timer.start()
##stopping previous timer
if fig in self.timer_dict:
self.timer_dict[fig].stop()
##storing a reference to prevent garbage collection
self.timer_dict[fig] = timer
if __name__ == '__main__':
my_updater = MarkerUpdater()
##setting up the figure
fig, axes = plt.subplots(figsize=(12, 6))
ax2 = axes.flatten()
## a line plot
# x1 = np.linspace(0,np.pi,30)
# y1 = np.sin(x1)
# ax1.plot(x1, y1, 'ro', markersize = 10, alpha = 0.8)
# ax3.plot(x1, y1, 'ro', markersize = 10, alpha = 1)
## a scatter plot
worldmap.plot(color="lightgrey", ax=ax2)
# Plotting our Impact Energy data with a color map
x = df['Longitude']
y = df['Latitude']
z = df['Impact Energy [kt]']
# x = np.random.normal(1,1,30)
# y = np.random.normal(1,1,30)
ax2.scatter(x, y, s=20*z, c=z, alpha=0.6, vmin=0, vmax=threshold,
cmap='autumn')
## scatter and line plot
# ax4.scatter(x2,y2, c = 'b', s = 100, alpha = 0.6)
# ax4.plot([0,0.5,1],[0,0.5,1],'ro', markersize = 10) ##note: no alpha value!
##setting up the updater
# my_updater.add_ax(ax1, ['size']) ##line plot, only marker size
my_updater.add_ax(ax2, ['size']) ##scatter plot, only marker size
# my_updater.add_ax(ax3, ['alpha']) ##line plot, only alpha
# my_updater.add_ax(ax4, ['size', 'alpha']) ##scatter plot, marker size and alpha
plt.show()
However I just get an error:
AttributeError: 'AxesSubplot' object has no attribute 'flatten'
And nothing else happens.. So it's little bit hard to figure out what is going wrong.
If I remove ax2 = axes.flatten()
, I just end up with the first map, nothing more.
Would anyone be interested in helping me to move forward?
from
Keeping the marker size to scale when zooming in on a world map