0

I am trying to produce a 2d plot of a sparse array with imshow() and use plt.text() to overlay it with text boxes. I came up with an additional option, using plt.scatter(). In the second case, the colored tiles and the text boxes are too small and can not be zoomed. In the first case, the size of the colored tiles, produced by imshow() and the text boxes have decoherent size and the plot looks fine only if the zoom function of the dialog window is used. This is illustrated by the code below.

import matplotlib.pyplot as plt
import matplotlib.colors as colors
import numpy as np

#https://matplotlib.org/gallery/images_contours_and_fields/image_annotated_heatmap.html

P=[1, 4, 11, 18, 20, 39, 40, 41, 41, 71, 71, 71, 71, 71, 71, 72, 72, 72, 72, 72, 72, 73, 73, 73, 74, 74, 74, 74, 74, 74, 75, 75, 75, 71]
N=[2, 3, 11, 19, 25, 49, 48, 50, 54, 101, 102, 103, 103, 106, 106, 100, 103, 106, 106, 107, 109, 105, 106, 109, 104, 107, 109, 110, 111, 112, 108, 109, 109, 101]
B=np.random.rand(34)

# crate the array to use with imshow()
A=np.zeros((max(N)+1,max(N)+1))
for i,j,k in zip(N,P,B):
     A[i,j]=k

def plot_map(N,P,A):     
    fig, ax = plt.subplots()     
    plt.imshow(A,norm=colors.LogNorm(),cmap='jet',origin='lower')
    plt.colorbar()
    for n,p in zip(N,P):
            ax.text(p,n, "\n%s\n%s\n%5.1E"%(p,n,A[n,p]),
                ha="center", va="center",
            bbox=dict(fc="none",boxstyle = "square"))
    plt.tight_layout()
    plt.show()

# call the plot function
plot_map(N,P,A)    

# attempt tow using plt.scatter() 
plt.scatter(N,P,c=B,marker='s',s=70,norm=colors.LogNorm(),cmap='jet')
for n,p in zip(N,P):
         plt.text(n,p, "\n%s\n%s"%(p,n), size=3,
             va="center", ha="center", multialignment="left",
             bbox=dict(fc="none",boxstyle = "square"))
plt.colorbar()
plt.show()

I ideally, I would like to produce something like this

What my plot routines produce does not look so good and both the colored tiles and the annotation boxes are discontinuous.
Therefore, I would appreciate your help.

Trenton McKinney
  • 56,955
  • 33
  • 144
  • 158
Alexander Cska
  • 738
  • 1
  • 7
  • 29
  • You have a 110x110 matrix. So text fitting in a box can maximally occupy 0.01 % of the screen, which necessarily is unreadable. – JohanC Jan 27 '20 at 14:54
  • using the first variant I can zoom. But the colored tile and the text box have uncorrelated sizes. The text boxes are actually quite large. – Alexander Cska Jan 27 '20 at 15:02
  • I would like to have the plot readable, when printed on a full A4 page. – Alexander Cska Jan 27 '20 at 15:29
  • The problem is, that I am plotting a lot of empty space. Is it possible, to make `imshow()` split the plot in ranges? Say the first 30 on one plot the second 30 on another etc.. – Alexander Cska Jan 28 '20 at 10:13

1 Answers1

2

The following approach uses mplcursors to display the information on screen, and also saves an image file that could be printed.

When printed on A4 paper, each little square would be about 2x2 mm, so a good printer and a looking glass can be helpful. You might want to experiment with the fontsize.

On screen, mplcursors displays a popup annotation when clicking on a little square. While zoomed in, a double click is needed in order not to interfere with the zooming UI. mplcursors also has a 'hover' mode, but then no information is displayed while zoomed in.

Some code to demonstrate how it might work:

import matplotlib.pyplot as plt
import matplotlib.colors as colors
import mplcursors
import numpy as np

P = [1, 4, 11, 18, 20, 39, 40, 41, 41, 71, 71, 71, 71, 71, 71, 72, 72, 72, 72, 72, 72, 73, 73, 73, 74, 74, 74, 74, 74, 74, 75, 75, 75, 71]
N = [2, 3, 11, 19, 25, 49, 48, 50, 54, 101, 102, 103, 103, 106, 106, 100, 103, 106, 106, 107, 109, 105, 106, 109, 104,  107, 109, 110, 111, 112, 108, 109, 109, 101]
B = np.random.rand(34)

# create the array to use with imshow()
A = np.zeros((max(N) + 1, max(N) + 1))
for i, j, k in zip(N, P, B):
    A[i, j] = k

fig, ax = plt.subplots(figsize=(21, 15))
img = ax.imshow(A, norm=colors.LogNorm(), cmap='jet', origin='lower')
plt.colorbar(img)
for n, p in zip(N, P):
    plt.text(p, n, "%s\n%s\n%5.1E"%(p,n,A[n,p]), size=2,
             va="center", ha="center", multialignment="left")

cursor = mplcursors.cursor(img, hover=False)
@cursor.connect("add")
def on_add(sel):
    i,j = sel.target.index
    if A[i][j] == 0:
        sel.annotation.set_visible(False)
    else:
        sel.annotation.set_text(f'P: {j}\nN: {i}\n{A[i][j]:.3f}')

plt.tight_layout()
plt.savefig('test.png', dpi=300)
plt.show()

At the left is how it would look on screen when zoomed in and double-clicking on a square. At the right how the image-to-be-printed would look when zoomed in.

resulting plots

To get text that gets bigger when zoomed in, TextPath is needed, as explained in this post. As TextPath doesn't really deal with multiple lines and alignments, the code calculates the positions. Also, depending on the color of the box, the text is easier to read when white. You'll need to test which values are good cut-offs in your situation and colormap.

To cope with the empty space, you could zoom in to the 3 places with data. The code below creates a subplot for each of these areas.

import matplotlib.pyplot as plt
import matplotlib.colors as colors
from matplotlib.textpath import TextPath
from matplotlib.patches import PathPatch
from matplotlib.ticker import MaxNLocator
import numpy as np

P = [1, 4, 11, 18, 20, 39, 40, 41, 41, 71, 71, 71, 71, 71, 71, 72, 72, 72, 72, 72, 72, 73, 73, 73, 74, 74, 74, 74, 74, 74, 75, 75, 75, 71]
N = [2, 3, 11, 19, 25, 49, 48, 50, 54, 101, 102, 103, 103, 106, 106, 100, 103, 106, 106, 107, 109, 105, 106, 109, 104,  107, 109, 110, 111, 112, 108, 109, 109, 101]
B = np.random.rand(34)

# create the array to use with imshow()
A = np.zeros((max(N) + 1, max(N) + 1))
for i, j, k in zip(N, P, B):
    A[i, j] = k

plot_limits = [[[0, 19], [1, 20]],
               [[38, 42], [47 - 1, 55 + 2]],  # second subplot with higher y-range to better fit with the rest
               [[70, 76], [99, 113]],
               [[0, 0.05], [0, 1]]]  # separate subplot for the colorbar

width_ratios = [(lim[0][1] - lim[0][0] ) / (lim[1][1] - lim[1][0]) for lim in plot_limits]

fig, ax = plt.subplots(figsize=(16, 8), ncols=4, gridspec_kw={'width_ratios': width_ratios})
for i in range(3):
    img = ax[i].imshow(A, norm=colors.LogNorm(), cmap='jet', origin='lower')
    for n, p in zip(N, P):
        textsize = 0.3
        for line, label in zip((n + 0.2, n - 0.1, n - 0.4), (f"{p}", f"{n}", f"{A[n, p]:.3f}")):
            tp = TextPath((p - 0.4, line), label, size=0.3)
            ax[i].add_patch(PathPatch(tp, color="black" if 0.08 < A[n, p] < 0.7 else "white"))
    ax[i].xaxis.set_major_locator(MaxNLocator(integer=True))
    ax[i].yaxis.set_major_locator(MaxNLocator(integer=True))
    ax[i].set_xlim(plot_limits[i][0])
    ax[i].set_ylim(plot_limits[i][1])

plt.colorbar(img, cax=ax[3])
plt.tight_layout()
plt.show()

This is how it looks like:

triple plot version

JohanC
  • 71,591
  • 8
  • 33
  • 66
  • Isn't it possible to increase the size of the tiles? I think that I would have to slice the picture in parts. And print each part on A4. If I would zoom the interactive plot. The text remains small and only the tiles grow. – Alexander Cska Jan 27 '20 at 16:53
  • 1
    If you increase the size of the tiles, they will either overlap or won't be on the correct position anymore. To have text that grows when you zoom, you need the approach from [this post](https://stackoverflow.com/questions/57235339/pyplot-keep-text-size-while-zooming) as mentioned in my other comment. – JohanC Jan 27 '20 at 17:02
  • Could you please explain what is done with the decorator exactly. You are decorating on_add() but how is this used in the code. I would like to understand that part. – Alexander Cska Jan 27 '20 at 17:17
  • 1
    the @cursor.connect("add") decorator is the same as calling `cursor.connect("add", myfunc)`. In this case it connects the mouse click to calling the `on_add` as a callback function. See the [docs](https://mplcursors.readthedocs.io/en/stable/#customization). – JohanC Jan 27 '20 at 17:23
  • I would like to test it. Anyway, It looks that if I would use another matrix `A`, some of the values won't be plotted. This is due to hard coding the limits. I think that the limits have to be defined according to where the data patch is located and limit the printing of white space. i.e. automatize what you did by hand. – Alexander Cska Jan 28 '20 at 16:44
  • I really appreciate your help. My problem was regarding the visibility of the plot. Your solution partially solves this. The hard coded limits, however, are a problem. If most of the data won't be plotted. Properly selecting the split limits is difficult, I agree on that one. Anyway, if nobody would come up with a better idea, I will accept your solution. Thank you. – Alexander Cska Jan 28 '20 at 17:26
  • Well, I programmed 3 solutions for you. It works for the example data. It will work for other data provided you give it the limits as input. You didn't define what type of data are to be expected. What if there are no separations possible? Maybe you want to skip some outliers? You still can generate the large png and manually zoom and cut the parts you need. – JohanC Jan 28 '20 at 17:34
  • I don't want to bother you further. You gave me quite a lot of valuable advice. concerning the boundaries, that is a bad idea. One option is to use non-linear scale for the patches but still label the thicks using the N & P arrays. Wit the non-linear scale, I can group the data. Something similar to a sparse matrix format. – Alexander Cska Jan 28 '20 at 17:46