2

I have a binary numpy array that contains filling data, and two grids that define the bounding box for the data:

data = np.random.choice([0, 1], size=12).reshape((3, 4))
xGrid = np.linspace(1, 4, 4)
yGrid = np.linspace(0.1, 0.3, 3)

I would like to plot a particular color, with a particular alpha, on any grid point where data is 1, and nothing when data is 0. The two closest matplotlib functions are

  • fill, which requires (x, y) coordinates and can't work with this data
  • imshow or matshow, which unfortunately will plot some color everywhere. That is, it will also plot some color drawn from the color map wherever data == 0. Now, I could fiddle around to make that color be the background color of the ax, but that's rather nasty.

The bounding boxes are expected to behave as follows: xGrid contains three values, and there are three data points on the x-dimension. Each value in xGrid denotes the location of the center point for each of the data points, and similar for yGrid. "Filling a data point" then corresponds to filling the rectangle defined by the center coordinates (x, y).

What's the best way of achieving this?

4
  • have you looked at matshow? matplotlib.org/2.0.2/examples/pylab_examples/matshow.html Commented Dec 19, 2018 at 7:30
  • @Andrew I seem to recall that it functions similar to imshow. At least in that example, it's drawing the whole matrix, and zeros are black -- exactly what I don't want :/ Commented Dec 19, 2018 at 7:32
  • You have 12 data points and a grid of 6 rectangles, do you want the filled areas to be drawn around the grid nodes or we can change xGrid=np.linspace(1,5,5) etc? Commented Dec 19, 2018 at 7:58
  • @gboffi I've updated the answer to be clearer on that: In essence, each data point represents a rectangle as defined by xGrid and yGrid., and I would like that rectangle to be filled. Commented Dec 19, 2018 at 8:04

4 Answers 4

2

With the understanding that the filled areas are to be drawn using the grid intersections as central points, we have

In [27]: import numpy as np 
    ...: import matplotlib.pyplot as plt 
    ...: np.random.seed(2018) 
    ...: data = np.random.choice([0, 1], size=12).reshape((3, 4)) 
    ...: xGrid = np.linspace(1, 4, 4) 
    ...: yGrid = np.linspace(0.1, 0.3, 3)                                                 

In [28]: print(data)                                                                      
[[0 0 0 1]
 [1 0 0 0]
 [1 1 1 1]]

In [29]: dx, dy = (xGrid[1]-xGrid[0])/2, (yGrid[1]-yGrid[0])/2                                    

In [30]: xli, yli = [], [] 
    ...: for y in yGrid: 
    ...:     for x in xGrid: # the x's in data are changing faster, so inner loop
    ...:         xli.append([x-dx, x+dx, x+dx, x-dx, x-dx]) 
    ...:         yli.append([y-dy, y-dy, y+dy, y+dy, y-dy]) 

In [31]: for xs, ys, Bool in zip(xli, yli, data.flatten()): 
    ...:     if Bool : plt.fill(xs, ys, color='red')
    ...: plt.gca().set_facecolor('yellow')                                      

Executing the code above gives me enter image description here

It's worth mentioning that only the filled rectangles are drawn, as shown by filling the background of the plotting area with a different color.

plt.fill is documented here and the lists created in the first for loop are simply the x, y coordinates of the corners of a rectangle that could possibly be drawn by plt.fill.


A Note on Efficiency

If one has to draw a few hundreds of rectangles the simple approach above is OK, if we go into the tens of thousands maybe we want to loop over the data points with enumerate, if it's needed construct the x, y lists and draw the rectangle on the fly or, even better for performance, create a Rectangle patch, put it in a PatchCollection and use the ax.add_collection method when we have finished the loop on dataan example is available in the Matplotlib docs that can be easily adapted to the scope and another example is this new answer of mine.

Sign up to request clarification or add additional context in comments.

4 Comments

I actually figured it out on the way, but that would indeed make the answer more complete :)
Oh, you can't actually swap the order in for x... for y...: Later on, when iterating through the zip, the order of the xs, ys, won't be in line with .flatten()
Your comment seems to say that you've swapped the order of the loops for performance reasons. I'm saying that the outcome of the plot is different depending on the order of the loops.
I wasn't thinking of efficiency but of correctness, I have however edited the comment to stress that the aim of the swap is to respect the order of points in data.
1

In another answer I mentioned that possible efficiency issues can be solved using Rectangle patches and a PatchCollection — here it is an implementation of this approach. First the initialization, note the imports of Rectangle and PatchCollection

In [99]: import numpy as np 
    ...: import matplotlib.pyplot as plt 
    ...: from matplotlib.collections import PatchCollection 
    ...: from matplotlib.patches import Rectangle 
    ...:  
    ...: np.random.seed(2018) 
    ...: data = np.random.choice([0, 1], size=12).reshape((3, 4)) 
    ...: xGrid = np.linspace(1, 4, 4) 
    ...: yGrid = np.linspace(0.1, 0.3, 3) 
    ...: dx, dy = (xGrid[1]-xGrid[0])/2, (yGrid[1]-yGrid[0])/2 
    ...: print(data)                                                                             
[[0 0 0 1]
 [1 0 0 0]
 [1 1 1 1]]

Next we construct the PatchCollection: we need a provisional list of patches, we loop on the rows of data AND the y coordinates and on the columns in each row AND the x coordinates, if we have to we add a Rectangle to the list of patches and finally we instantiate it

In [100]: patches = [] 
     ...: for y, row in zip(yGrid, data): 
     ...:     for x, col in zip(xGrid, row): 
     ...:         if col: patches.append(Rectangle((x-dx, y-dy), 2*dx, 2*dy)) 
     ...: pc = PatchCollection(patches) 

And in the end the plotting, we need two axis' methods so plt.gca() is needed, we modify the rectangles using methods of the path collection, committ the collection to the ax and finally we call explicitly the autoscale_view method that is required to have the correct axes limits.

In [101]: ax = plt.gca() 
     ...: pc.set_facecolor('yellow') 
     ...: pc.set_edgecolor('black') 
     ...: ax.add_collection(pc) 
     ...: ax.autoscale_view()                                                                    

And this is the result

enter image description here

Comments

1

Using imshow() based on this example for using the alpha.

I am using set_ticks code given by @B. M.

def make_rgb_transparent(rgb, bg_rgb, alpha):
    return [alpha * c1 + (1 - alpha) * c2
            for (c1, c2) in zip(rgb, bg_rgb)]

import matplotlib
from matplotlib import colors

alpha =1.0
white = np.ones((1,3))
rgb = colors.colorConverter.to_rgb('red')
rgb_new = make_rgb_transparent(rgb, (1, 1, 1), alpha)
red_white_map = colors.LinearSegmentedColormap.from_list('map_white', np.vstack((white, rgb_new)),2)

ax=plt.imshow(data,cmap=red_white_map)
ax.axes.set_xticks(np.arange(len(xGrid)))
ax.axes.set_xticklabels([str(a) for a in xGrid])
ax.axes.set_yticks(np.arange(len(yGrid)))
ax.axes.set_yticklabels([str(a) for a in yGrid])

enter image description here

3 Comments

That seems to be almost there. I suppose marker='s' will make them squares, but is there a way to make them exactly as large as the grid point? I.e. several points next to each other would appear as a continuous rectangle, instead of a collection of points?
You can play around with square size , if you want even a continuous flow of color
The axis labels show a different grid w.r.t. what the OP asked for. All the surface of the drawing area is covered in color but the OP said "I would like to plot a particular color, with a particular alpha, on any grid point where data is 1, and nothing when data is 0."
1

You can manage color with the colormap parameter. Here a fast solution using imshow, with total control on all parameters, in particular custom colors:

from pylab import imshow,show,cm
from matplotlib.colors import LinearSegmentedColormap
alpha=.7
cdict = {'blue': ((0.0, 0.0, 0.0), (1.0, 0.0, 0.0)),
 'green': ((0.0, 0.0, 0.0), (1.0, 0.0, 0.0)),
 'red': ((0.0, 0.0, 0.0), (1.0, alpha,alpha))} 
mycolors = LinearSegmentedColormap("my_colors",cdict,N=2)

ax=imshow(data,cmap=mycolors)
ax.axes.set_xticks(np.arange(len(xGrid)))
ax.axes.set_xticklabels([str(a) for a in xGrid])
ax.axes.set_yticks(np.arange(len(yGrid)))
ax.axes.set_yticklabels([str(a) for a in yGrid])
ax.axes.set_xbound(-.5,3.5)
ax.axes.set_ybound(-.5,2.5)
ax.axes.set_aspect(.2/3)

For : enter image description here

3 Comments

Won't that draw white for zeros? I'm looking for approaches that don't draw anything on the zeros.
it's white on my screen; some noise on the way probably.
When I use your code I do see the x and y axes with ticks and tick labels and the tick labels are different from what the OP asked for. Further all the surface of the drawing area is covered, again different from what the OP asked for.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.