While working on the Spirograph series of posts, I went through a few phases of development and/or artistic endeavour. At first I was just happy to generate a plot on a white background minus all the axis labels, borders, ticks, etc. Though I was using Matplotlib, that was pretty straightforward.

But when I decided I’d like coloured backgrounds, my initial approach had some issues. So, I had to change things a bit. Then it occurred to me that I’d like to get some of the images Giclée printed and framed. Now I had to consider not just the final size of the canvas, but I had to control the size of the image so that the frame matting wouldn’t cover any of it. But, I also had to make sure the background ran under the matting. I sorted an approach, but it never really appealed to me. A little to convoluted and unpredictable.

Then there was the aspect ratio. I wanted for the most part to generate a square (aspect='equal') image. But I didn’t really understand what adjustable='datalim' did versus what adjustable='box' did. And no easy way to see two images side by each. Then there was the sizing thing. Very difficult to do for some of the image types if autoscale was disabled. And, if autoscale was enabled when I tried setting the larger background ‘image’ things often didn’t go the way I wanted or expected.

And, of course, with all those changes I couldn’t replicate older images I really liked. In one particular case, that really bothers me. I may waste a bunch of time trying to sort that out. And, because so many values used in generating the images were intialized randomly, it really is/was difficult to reproduce the more complex images. Another thing I may yet waste a bunch of time on.

I have recently settled on a, perhaps, final approach to generating the desired images. Don’t yet know if it will make reproducing images more feasible or not. But, for those of you who might want to generate digital images using Matplotlib, I thought I would document some of what I went through. Well, assuming I can recreate or recall the various steps. Hopefully git will help. But I often committed rather large changes which might inhibit the recovery of past techniques.

Start Simple

So I instantiate figure and axes objects with the subplots method. I originally started with 8 in by 8 in images. Enough for my purposes at the time. Then I turn off all the axis stuff. Then I make sure the image is using all the avaialble space. Finally set the aspect ratio to square. And there you go, pretty much what I wanted.

  # Set up plot/figure
  fig, ax = plt.subplots(figsize=(8,8))  
  plt.axis('off')
  # use all available space
  ax.set_position([0, 0, 1, 1], which='both')
  # set the aspect ratio to square
  ax.set_aspect('equal')

Examples

Examples of the minimalist style of the generated images.

Simple beginnings…
simple spirograph image using a minimal matplotlib setup
Variation on the basic spirograph curve
simple gnarly spirograph image using a minimal matplotlib setup

Coloured Backgrounds

Gradient

I borrowed a function that generated a gradient background using imshow(). I continued using the code from above to set up my plotting/image figure and axes. Then after plotting the curve, I called the function to paint the background.

  x_mn, x_mx = ax.get_xlim()
  y_mn, y_mx = ax.get_ylim()
  ax = gradient_image(ax, extent=(x_mn, x_mx, y_mn, y_mx), direction=0.3, cmap_range=(0, 1), cmap=clr_nm[rcm], alpha=bg_lpha)

All too often the result was not what I was expecting. Almost every image had at least a thin white border on the left/right or top/bottom. No idea why. For example!

Background not so good
simple gnarly spirograph image with gradient background

I verified that the axes limits and plot limits were the same.

I first tried changing the call to set_aspect to use adjustable='datalim'. No change. So I tried, adjustable='box'.

That seems to fix things
simple gnarly spirograph image with gradient background

Until I tried generating images a number of more times. So, afterall, no success!

So, I decided to go back to basics and just use a single background colour implemented with Matplotlib’s set_facecolor() method.

Single Colour

Okay, always go back to basics when troubleshooting a problem. Use a single colour for the background, taken from the current colour map in the cycler. Needless to say, there is some code you are not seeing.

I added this code after I plotted the curve. The order really doesn’t matter. It was just as good a spot as any.

# for some reason I chose only use the first 25% of the available colours in the current colour map
n_clr = int(lc_frq / 4)
c_bg = cycle[np.random.randint(0, lc_frq)]
# select opacity between 0.35 and 0.66 (inclusive?)
bg_lpha = np.random.randint(35, 66) / 100
ax.set_facecolor(c_bg)
ax.patch.set_alpha(bg_lpha)
Where's the background?
simple spirograph image with an attempted background colour

For the images I have been generating I really didn’t want all the axes stuff included. So, as shown above, I turned them off with plt.axis('off'). Well, that can be problematic.

The background patch is part of the axes. So if the axes is turned off, so will the background patch.

  • Turning axes off and setting facecolor at the same time not possible?

That article provided 3 potential solutions. After playing with the one that added a new patch, I decided to use the one that removed the axis spines and ticks from the plot. So, I replaced plt.axis('off') with the suggested code.

  # Set up plot/figure
  fig, ax = plt.subplots(figsize=(8,8))  
  for spine in ax.spines.values():
    spine.set_visible(False)
  ax.tick_params(bottom=False, labelbottom=False, left=False, labelleft=False)  if do_ttl:
    ax.set_position([0, 0, .9, 1], which='both')
  else:
    ax.set_position([0, 0, 1, 1], which='both')
  ax.set_aspect('equal', adjustable='box')

With adjustable='box' I was still getting white borders of various sizes on either the sides or the top and bottom. Changing that to adjustable='datalim' seemed to fix things. Though I sure don’t understand why!! But at least I was getting a background colour.

Ah! a background colour!
simple spirograph image with an attempted background colour

Did That Fix Gradient/Blotchty Background

Multiple code executions with a single background colour, did not show the white border issue. As soon as I went to the gradient or blotchy background functions, the borders re-appeared on many images. Those two functions use imshow(). Wonder if that has something to do with it.

So, I decided to mess with extent= in the call to the gradient or blotchy background functions. Which is passed directly to imshow(). I decided on a simple algorithm to get the extents for the background.

First of all I set the plot aspect to auto before plotting the curve and the background. I am plotting the background after the curve so that I have some meaningful data limits to use for the background. I then replot the curve after the background is drawn so that it is drawn on top of the background. And, finally I set the aspect to equal.

  plt.plot(np.real(r_pts[-1]), np.imag(r_pts[-1]), lw=ln_w, ls=ln_s, alpha=c_lpha)
  x_mn, x_mx = ax.get_xlim()
  y_mn, y_mx = ax.get_ylim()
  if y_mn < x_mn:
    l_mn = y_mn
  else:
    l_mn = x_mn
  if y_mx > x_mx:
    l_mx = y_mx
  else:
    l_mx = x_mx
  bax = gradient_image(ax, extent=(l_mn, l_mx, l_mn, l_mx), direction=0.3, cmap_range=(0, 1), cmap=clr_nm[rcm], alpha=bg_lpha)  
  plt.plot(np.real(r_pts[-1]), np.imag(r_pts[-1]), lw=ln_w, ls=ln_s, alpha=c_lpha)
  ax.set_aspect('equal', adjustable='datalim')

After a number of repeated executions with gradient and blotchy backgrounds, I was no longer getting any unwanted white space in the images.

Unwanted white space no longer present
simple spirograph image with background colour gradient

And, it seems to work for the gnarly plot style as well. Sorry about the colour similarity in the two images.

Gnarly and blotchy
gnarly spirograph image with blotchy background

Generate Images for Framing

At the moment I must admit I am not sufficiently informed about getting images Giclée printed and framed. I am looking at finished frame sizes of 25x25 cm to 36x36 cm. Since, matplotlib by default sets image sizes in inches that would be 10x10 to 14x14 inches. I wrote a bit of code to figure out the size of the matting and consequently the size of the area that will be visible (my curve plotting region). I also figured I might save a few pennies by not printing the whole of the frame size. Figured I just needed enough background to make sure it extended under tha mat.

I have been setting the figure size to 8x8 inches. I will for now work with 14x14 inches.

fig_sz = 14
  ...
  # sort axes position
  wdv = .715    # golden ratio
  # allow 5% all sides for background
  wdv2 = wdv * 1.10
  llv2 = (1 - wdv2) / 2

  # Set up plot/figure
  fig, ax = plt.subplots(figsize=(fig_sz,fig_sz))
  for spine in ax.spines.values():
    spine.set_visible(False)
  ax.tick_params(bottom=False, labelbottom=False, left=False, labelleft=False)
  if do_ttl:
    ax.set_position([0, 0, .9, 1], which='both')
  else:
    ax.set_position([llv2, llv2, wdv2, wdv2], which='both')
  ...

And, based on some testing, that seems to work reasonably well. But, when it came to images where the x and y limits were significantly different, the image gets squashed in one direction or the other.

Not square
plain spirograph image with blotchy background compressed in y direction

I really would like the curves to fill as much of the visible drawing area as possible.

Multiple Artists

I won’t go through all the other minor variations on the above that I attempted. None were particularly successful at resolving all my concerns.

I am not sure that I am using the Matplotlib terminology correctly. But I recalled reading that one could create multiple artists on a Matplotlib figure. So, I figured what if create one axes for the background, of whatever size I wished. And, a second, smaller one for the curve. I would also have to make sure that they are generated in the correct order. I definitely want the curve drawn on top of the background.

And, in this case, I didn’t think I would need to concern my self with the data limits. As I would not be mixing the background and curve on the same axis. Only the size and placement of each axes would matter. But one never knows…

I do not yet know for certain that this approach will resolve all my issues. But it does seem to work reasonably well. And, it makes things much easier with the curves built using markers, as it pretty much takes care of fitting the curve to the available space. Something that with the above approach I had to account for when the marker sizes were large.

I start by creating a figure, then manually add two axes and finally draw the background in one axes and the curve in the other.

  fig = plt.figure(figsize=(fig_sz,fig_sz), frameon=False)  
  
  wdv = .715
  llv = (1 - wdv) / 2
  # make background axes 10% larger
  wdv2 = wdv * 1.10
  llv2 = (1 - wdv2) / 2

  # background axes
  # set zorder so it is lower in the drawing order
  ax2 = fig.add_axes([llv2, llv2, wdv2, wdv2], zorder=1)
  ax2.axis('off')
  ax2.autoscale(False)

  xn3, xx3 = ax2.
  # curve axes
  # set zorder so it is higher in the drawing order
  ax = fig.add_axes([llv, llv, wdv, wdv], zorder=10)
  # need set background patch transparency so that we can see the background colours
  ax.patch.set_alpha(0)
  ax.axis('off')

  # want to make sure the curve fills the axes' drawing area as best as possible
  ax.autoscale(True)

For the background, I need to mess a bit with the axes limits. extent= in imshow() works with data coordinates. So, I get those limits before calling the background plotting function. (Looks like the defaults are (0, 0, 1, 1), so could perhaps skip this step, but…)

  xmin, xmax = ax2.get_xlim()
  ymin, ymax = ax2.get_ylim()

  bax = gradient_image(ax2, extent=(xmin, xmax, ymin, ymax), direction=g_dir, cmap_range=(v_min, v_max), transform=ax.transAxes, cmap=cmap, alpha=bg_lpha)

I then go ahead and plot the curve. I no longer adjust any of the data limits nor set the aspect ratio.

Examples

These examples show reduced versions of the generated images. So, there is a solid colour background for the full figure (should be white). Which in these samples was originally 14x14 inches. Reduced to 800x800 pixels for the post. Then there is the background axes and the curve plotting axes, each at there own reduced size. The latter originally being around 9.9x9.9 inches. The former approximately 10.9x10.9 inches (originally).

Looking good! But forgot to specify a background colour when saving the plot.
plain spirograph image with blotchy background using 2 axes

With the way I am initializing the figure I need to tell savefig to use the figure’s facecolour. And, make sure it is not transparent.

plt.savefig(f_pth, facecolor=fig.get_facecolor(), transparent=False)

And, that didn’t work when I opened images in my image application. Seems it is something in the app and not in Matplotlib or Python. So manually editing the background colour in the app.

A curve variation.
variation on spirograph image with blotchy background using 2 axes
Another curve variation.
variation on spirograph image with blotchy background using 2 axes

Done

Think that’s it for this one. Hopefully, I will eventually sort the change from a white to a grey background for the figure area outside the two axes.

Enjoy your coding time!

And, yes it was my software. The main window colour was set to that grey colour. Changed it to white and all now good.

Resources