0

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:

enter image description here

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:

enter image description here

My attempt

PLEASE NOTE: I have not touched the MarketUpdater, and I am only using ax2 of Thomas' solution:

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=3,figsize=(12, 6)) # swatchai's comment
    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()

Traceback

The error is in this line:

worldmap.plot(color="lightgrey", ax=ax2)

Traceback:

Traceback (most recent call last):
  File "c:\Users\x\OneDrive - Wageningen University & Research\Arjen Daane\Data\tempCodeRunnerFile.python", line 185, in <module>
    worldmap.plot(color="lightgrey", ax=ax2)
  File "C:\Users\x\AppData\Local\Programs\Python\Python310\lib\site-packages\geopandas\geodataframe.py", line 617, in plot
    return plot_dataframe(self, *args, **kwargs)
  File "C:\Users\x\AppData\Local\Programs\Python\Python310\lib\site-packages\geopandas\plotting.py", line 504, in plot_dataframe
    ax.set_aspect("equal")
AttributeError: 'numpy.ndarray' object has no attribute 'set_aspect'

Would anyone be interested in helping me to move forward?

Tom
  • 2,173
  • 1
  • 17
  • 44
  • Need to specify numbers of row and column, `plt.subplots(nrows=2, ncols=3,figsize=(12, 6))` – swatchai May 26 '22 at 23:34
  • @swatchai Thank you for your comment, but it did not get me much further I'm afraid. – Tom May 27 '22 at 05:20
  • Tom - this is not a [cannonical question](//meta.stackoverflow.com/a/291994/3888719). Please always include the full traceback when asking a question about errors, and ask one question at a time. "it did not get me much further" means swatchai solved your original issue, and you have another question. Feel free to ask again if you're stuck again! But currently your question is riddled with issues and isn't reproducible. Can you try harder to produce a [mre]? See also this helpful post on [crafting minimal bug reports](//matthewrocklin.com/blog/work/2018/02/28/minimal-bug-reports). Good luck! – Michael Delgado May 29 '22 at 21:18
  • @MichaelDelgado Thank you for your comment. I will be more careful with the bounty type I use, and I will do my best to improve the question over the coming days. I do not completely agree with the fact that swatchai solved my original question though. My question was how to combine the example and the solution I linked to, not to address the error I got. I will do my best to include a traceback, but the issue is that I do not really get an error. That makes it also harder to include a minimal reproducible example. Nevertheless I will go through the links and improve on the question. Thanks! – Tom May 30 '22 at 04:37
  • @MichaelDelgado Getting an error now, included the traceback. – Tom May 30 '22 at 05:01
  • it looks like part of your issue is that your world map code applies to one subplot, but you're using an example that uses four subplots. see [this guide to subplots](https://matplotlib.org/stable/gallery/subplots_axes_and_figures/subplots_demo.html). Matplotlib has the (unfortunate, imo) behavior of returning a single axis with `plt.subplots()`, a 1-dimensional array with `plot.subplots(cols)`, and a 2-D array with `plt.subplots(rows, cols)`. – Michael Delgado May 30 '22 at 05:23
  • 1
    Because of this, you need to change the way you interact with the axes objects. ax2 is an array. You loop over them e.g. with `for ax in axes.flat: worldmap.plot(color="lightgrey", ax=ax)` to plot the world map on each axis object. – Michael Delgado May 30 '22 at 05:23

1 Answers1

1

The problem is that the MarkerUpdater object also tries to rescale the world map itself. You can avoid this by adding an if statement to the class which avoids the rescaling. Also add an extra import to your import list for this solution:

import matplotlib.pyplot as plt
## UPDATE: Add this import: #########################################
from matplotlib.collections import PatchCollection
import pandas as pd

##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']:
############################### UPDATE: Add this if statement: ########################
                                if not isinstance(path, PatchCollection):   
                                    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 '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

Then your code should look something like this:

...

## UPDATE: construct the object
my_updater = MarkerUpdater()

# 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]')

## UPDATE: Link the scatter plot to the object
my_updater.add_ax(ax, ['size'])  ##line plot, only marker size

...

Best of luck!

Philippe
  • 276
  • 1
  • 3