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?

FooBar
  • 15,724
  • 19
  • 82
  • 171
  • have you looked at `matshow`? https://matplotlib.org/2.0.2/examples/pylab_examples/matshow.html – Andrew Dec 19 '18 at 07: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 :/ – FooBar Dec 19 '18 at 07: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? – gboffi Dec 19 '18 at 07: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. – FooBar Dec 19 '18 at 08:04

4 Answers4

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.

gboffi
  • 22,939
  • 8
  • 54
  • 85
  • I actually figured it out on the way, but that would indeed make the answer more complete :) – FooBar Dec 19 '18 at 09:05
  • 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()` – FooBar Dec 19 '18 at 09:14
  • 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. – FooBar Dec 19 '18 at 09:23
  • 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`. – gboffi Dec 19 '18 at 09:27
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

Venkatachalam
  • 16,288
  • 9
  • 49
  • 77
  • 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? – FooBar Dec 19 '18 at 07:58
  • You can play around with square size , if you want even a continuous flow of color – Venkatachalam Dec 19 '18 at 08:13
  • 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."_ – gboffi Dec 19 '18 at 09:00
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

B. M.
  • 18,243
  • 2
  • 35
  • 54
  • Won't that draw *white* for zeros? I'm looking for approaches that don't draw anything on the zeros. – FooBar Dec 19 '18 at 08:06
  • it's white on my screen; some noise on the way probably. – B. M. Dec 19 '18 at 08:21
  • 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. – gboffi Dec 19 '18 at 08:55
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

gboffi
  • 22,939
  • 8
  • 54
  • 85