ckjellson / textalloc

Allocates text labels in matplotlib
MIT License
67 stars 6 forks source link

labels not clearing #24

Closed nebukadnezar closed 4 months ago

nebukadnezar commented 4 months ago

First of all, many thanks for implementing the cartopy compatibility. This works beautifully on static plots now!

But now I have new problem: I'm plotting live plots that update asynchronously. Turns out the textalloc labels are not cleared between redraws.

I've tried numerous methods to remove all artists between redraws, without success.

Any suggestions on how to remove the lines/labels from textalloc between redraws?

Here's an example code illustrating the issue:

#!/usr/bin/env python3

import json
import time
import sys
import os
import numpy as np
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import cartopy.feature as cfeature
import cartopy.io.img_tiles as cimgt
from cartopy.mpl.ticker import LongitudeFormatter, LatitudeFormatter
import textalloc as ta
from math import acos, cos, sin, radians, sqrt, atan2, degrees

#######################################################################################################
# live plot
class LivePlot:
    def __init__(self):
        self.fig, self.ax = plt.subplots(figsize=(12, 12), subplot_kw={'projection': ccrs.PlateCarree()})
        self.sc = None
        self.ax.add_feature(cfeature.LAND)
        self.ax.add_feature(cfeature.OCEAN)
        self.ax.add_feature(cfeature.COASTLINE)
        self.ax.add_feature(cfeature.BORDERS, linestyle=':')
        self.ax.gridlines(draw_labels=True)

    def update_plot(self, data, map_extent):
        lats = data[0]
        lons = data[1]
        labels = data[2]

        self.ax.set_extent(map_extent)

        if self.sc:
            self.sc.remove()
        self.sc = self.ax.scatter(lons, lats, c='red', s=5, transform=ccrs.PlateCarree())

        # Plot labels
        ta.allocate(self.ax, lons, lats,
                     labels,
                     x_scatter=lons, y_scatter=lats,
                     textsize=6,
                     draw_lines=True,
                     linewidth=0.5,
                     draw_all=False,
                     transform=ccrs.PlateCarree(),
                     avoid_label_lines_overlap=True)

        self.ax.set_title('Live Positions')
        plt.draw()

def plot_positions(data, live_plot, map_extent):
    live_plot.update_plot(data, map_extent)

if __name__ == '__main__':

    live_plot = None
    live_plot = LivePlot()
    plt.ion()  # Turn on interactive mode

    lon = 151
    lat = -34
    radius = 100  #km

    while True:

        # calculate the box size
        left = lon - 1/cos(radians(lat)) * radius / 111
        right = lon + 1/cos(radians(lat)) * radius / 111
        top = lat + radius / 111
        bottom = lat - radius / 111

        n = 100
        lons = np.random.uniform(left, right, size=n)
        lats = np.random.uniform(bottom, top, size=n)
        labels = [f"Label {i}" for i in range(0, n)]
        thisdata = [ lats, lons, labels ]
        plot_positions(thisdata, live_plot, [left, right, bottom, top])
        plt.pause(0.01)  # Shorter pause for more frequent updates

        # sleep
        time.sleep(2)
ckjellson commented 4 months ago

Hi, great!

I tried out your example and I seem to get rid of the lines and texts by adding the following lines before the ta.allocate call:

for artist in self.ax.get_children():
    if isinstance(artist, plt.Text):
        artist.set_visible(False)
    if isinstance(artist, plt.Line2D):
        artist.remove()

HOWEVER, there seems to be no way to call .remove() on plt.Text artists, so I think these will still be kept in memory, and might become a lot after many iterations. Therefore I think it would make more sense if the ta.allocate call returned all the plt.Text and plt.Line2D objects so that you could call .remove() on these ones instead of the artists, which seemed to work when I tried that.

Long story short, I made updates so that text and line objects are returned and can then be removed outside of textalloc, try this with the latest version of textalloc (1.1.2):

#!/usr/bin/env python3

import json
import time
import sys
import os
import numpy as np
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import cartopy.feature as cfeature
import cartopy.io.img_tiles as cimgt
from cartopy.mpl.ticker import LongitudeFormatter, LatitudeFormatter
import textalloc as ta
from math import acos, cos, sin, radians, sqrt, atan2, degrees

#######################################################################################################
# live plot
class LivePlot:
    def __init__(self):
        self.fig, self.ax = plt.subplots(
            figsize=(12, 12), subplot_kw={"projection": ccrs.PlateCarree()}
        )
        self.sc = None
        self.lines = None
        self.texts = None
        self.ax.add_feature(cfeature.LAND)
        self.ax.add_feature(cfeature.OCEAN)
        self.ax.add_feature(cfeature.COASTLINE)
        self.ax.add_feature(cfeature.BORDERS, linestyle=":")
        self.ax.gridlines(draw_labels=True)

    def update_plot(self, data, map_extent):
        lats = data[0]
        lons = data[1]
        labels = data[2]

        self.ax.set_extent(map_extent)

        if self.sc:
            self.sc.remove()
        if self.texts:
            [t.remove() for t in self.texts]
        if self.lines:
            [l.remove() for l in self.lines]
        self.sc = self.ax.scatter(
            lons, lats, c="red", s=5, transform=ccrs.PlateCarree()
        )

        # Plot labels
        _, _, self.texts, self.lines = ta.allocate(
            self.ax,
            lons,
            lats,
            labels,
            x_scatter=lons,
            y_scatter=lats,
            textsize=6,
            draw_lines=True,
            linewidth=0.5,
            draw_all=False,
            transform=ccrs.PlateCarree(),
            avoid_label_lines_overlap=True,
        )

        self.ax.set_title("Live Positions")
        plt.draw()

def plot_positions(data, live_plot, map_extent):
    live_plot.update_plot(data, map_extent)

if __name__ == "__main__":

    live_plot = None
    live_plot = LivePlot()
    plt.ion()  # Turn on interactive mode

    lon = 151
    lat = -34
    radius = 100  # km

    while True:

        # calculate the box size
        left = lon - 1 / cos(radians(lat)) * radius / 111
        right = lon + 1 / cos(radians(lat)) * radius / 111
        top = lat + radius / 111
        bottom = lat - radius / 111

        n = 100
        lons = np.random.uniform(left, right, size=n)
        lats = np.random.uniform(bottom, top, size=n)
        labels = [f"Label {i}" for i in range(0, n)]
        thisdata = [lats, lons, labels]
        plot_positions(thisdata, live_plot, [left, right, bottom, top])
        plt.pause(0.01)  # Shorter pause for more frequent updates

        # sleep
        time.sleep(2)