21

I am trying to create a color wheel in Python, preferably using Matplotlib. The following works OK:

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

xval = np.arange(0, 2*pi, 0.01)
yval = np.ones_like(xval)

colormap = plt.get_cmap('hsv')
norm = mpl.colors.Normalize(0.0, 2*np.pi)

ax = plt.subplot(1, 1, 1, polar=True)
ax.scatter(xval, yval, c=xval, s=300, cmap=colormap, norm=norm, linewidths=0)
ax.set_yticks([])

Own attempt at creating a color wheel

However, this attempt has two serious drawbacks.

First, when saving the resulting figure as a vector (figure_1.svg), the color wheel consists (as expected) of 621 different shapes, corresponding to the different (x,y) values being plotted. Although the result looks like a circle, it isn't really. I would greatly prefer to use an actual circle, defined by a few path points and Bezier curves between them, as in e.g. matplotlib.patches.Circle. This seems to me the 'proper' way of doing it, and the result would look nicer (no banding, better gradient, better anti-aliasing).

Second (relatedly), the final plotted markers (the last few before 2*pi) overlap the first few. It's very hard to see in the pixel rendering, but if you zoom in on the vector-based rendering you can clearly see the last disc overlap the first few.

I tried using different markers (. or |), but none of them go around the second issue.

Bottom line: can I draw a circle in Python/Matplotlib which is defined in the proper vector/Bezier curve way, and which has an edge color defined according to a colormap (or, failing that, an arbitrary color gradient)?

EelkeSpaak
  • 2,757
  • 2
  • 17
  • 37

3 Answers3

20

One way I have found is to produce a colormap and then project it onto a polar axis. Here is a working example - it includes a nasty hack, though (clearly commented). I'm sure there's a way to either adjust limits or (harder) write your own Transform to get around it, but I haven't quite managed that yet. I thought the bounds on the call to Normalize would do that, but apparently not.

import matplotlib.pyplot as plt
import numpy as np
from matplotlib import cm
import matplotlib as mpl

fig = plt.figure()

display_axes = fig.add_axes([0.1,0.1,0.8,0.8], projection='polar')
display_axes._direction = 2*np.pi ## This is a nasty hack - using the hidden field to 
                                  ## multiply the values such that 1 become 2*pi
                                  ## this field is supposed to take values 1 or -1 only!!

norm = mpl.colors.Normalize(0.0, 2*np.pi)

# Plot the colorbar onto the polar axis
# note - use orientation horizontal so that the gradient goes around
# the wheel rather than centre out
quant_steps = 2056
cb = mpl.colorbar.ColorbarBase(display_axes, cmap=cm.get_cmap('hsv',quant_steps),
                                   norm=norm,
                                   orientation='horizontal')

# aesthetics - get rid of border and axis labels                                   
cb.outline.set_visible(False)                                 
display_axes.set_axis_off()
plt.show() # Replace with plt.savefig if you want to save a file

This produces

colorwheel direct from matplotlib

If you want a ring rather than a wheel, use this before plt.show() or plt.savefig

display_axes.set_rlim([-1,1])

This gives

color ring


As per @EelkeSpaak in comments - if you save the graphic as an SVG as per the OP, here is a tip for working with the resulting graphic: The little elements of the resulting SVG image are touching and non-overlapping. This leads to faint grey lines in some renderers (Inkscape, Adobe Reader, probably not in print). A simple solution to this is to apply a small (e.g. 120%) scaling to each of the individual gradient elements, using e.g. Inkscape or Illustrator. Note you'll have to apply the transform to each element separately (the mentioned software provides functionality to do this automatically), rather than to the whole drawing, otherwise it has no effect.

Community
  • 1
  • 1
J Richard Snape
  • 20,116
  • 5
  • 51
  • 79
  • Excellent, I attempted some stuff using `ColorbarBase` as well, but missed the `orientation` keyword. This solution is definitely better than my own (since the 'spokes' on the wheel now don't overlap anymore) but it's still not quite the solution I wanted, since the resulting vector art still contains many separate path elements rather than a single Bezier-curved circle. Thanks a lot anyway; if no one comes along with the *true* solution, I will mark your answer as accepted :) – EelkeSpaak Aug 11 '15 at 13:27
  • Ahh - I see what you mean. I'll have a think about that - sorry for not reading the question quite carefully enough ;) – J Richard Snape Aug 11 '15 at 13:42
  • A couple of thoughts I recalled that bezier curves can't actually produce a perfect circle: [this answer](http://stackoverflow.com/a/13338311/838992) seems to back that up. I also think that for matplotlib, 1 element = 1 color. So where we see gradients, we're actually seeing lots of tightly packed small elements. [Another answer about colouring lines with gradients](http://stackoverflow.com/a/8505774/838992) would seem to support this view. The elements could be many small bezier segments, but I think you'll still get some visible quantization. Maybe someone will come and prove me wrong :) – J Richard Snape Aug 11 '15 at 13:53
  • @EelkeSpaak I [introduced a variable `quant_steps`](http://stackoverflow.com/posts/31942626/revisions) to get a smoother gradient. Set to 2056, I can't detect "spokes" at 300dpi, 400% magnification. It still doesn't give you a single element for the circle, but as per my comment above - I'm not sure that's going to be possible. Thanks for the fascinating question! – J Richard Snape Aug 11 '15 at 14:26
  • 1
    Another tip. The little elements of the resulting SVG image are touching and non-overlapping. This leads to faint grey lines in some renderers (Inkscape, Adobe Reader, probably not in print). A simple solution to this is to apply a small (e.g. 120%) scaling to each of the individual gradient elements, using e.g. Inkscape or Illustrator. Note you'll have to apply the transform to each element separately (the mentioned software provides functionality to do this automatically), rather than to the whole drawing, otherwise it has no effect. – EelkeSpaak Sep 23 '15 at 15:18
  • Hi, I'm getting some errors when running this code straight out of the box (as posted, no modifications). Specifically the line containing `orientation='horizontal'`. The final message is `File "[...]matplotlib\projections\polar.py", line 1090, in get_theta_direction return self._direction.get_matrix()[0, 0] AttributeError: 'float' object has no attribute 'get_matrix' `. Do you have any idea what might be causing this? I don't have enough debugging experience to figure it out, perhaps I am missing a necessary package? – Greedo Oct 23 '17 at 17:29
  • @Greedo Not sure off the top of my head. The raw error is telling you that within matplotlib's internals, it is expecting direction to be a matrix-like object and it's actually a float. It's possible this is a change to matplotlib in the two years since I wrote the answer. Can you tell me which version of python and matplotlib you are using? – J Richard Snape Oct 24 '17 at 09:05
  • 1
    @Greedo Looking more closely at my code - it is probably the commented "Nasty hack" that no longer works. I have changed an internal variable there - something you shouldn't do, and although it's worked for 2 years it looks like my luck has run out. This is the problem with such hacks - the behaviour of the library "under the hood" can change without warning. If I get some time, I'll update the answer. – J Richard Snape Oct 24 '17 at 09:07
  • @JRichardSnape Yes, if you get a moment that would be good, as some more thorough searching has not turned up anything new! – Greedo Oct 29 '17 at 14:43
10

I just needed to make a color wheel and decided to update rsnape's solution to be compatible with matplotlib 2.1. Rather than place a colorbar object on an axis, you can instead plot a polar colored mesh on a polar plot.

import matplotlib.pyplot as plt
import numpy as np
from matplotlib import cm
import matplotlib as mpl

# If displaying in a Jupyter notebook:
# %matplotlib inline 

# Generate a figure with a polar projection
fg = plt.figure(figsize=(8,8))
ax = fg.add_axes([0.1,0.1,0.8,0.8], projection='polar')

# Define colormap normalization for 0 to 2*pi
norm = mpl.colors.Normalize(0, 2*np.pi) 

# Plot a color mesh on the polar plot
# with the color set by the angle

n = 200  #the number of secants for the mesh
t = np.linspace(0,2*np.pi,n)   #theta values
r = np.linspace(.6,1,2)        #radius values change 0.6 to 0 for full circle
rg, tg = np.meshgrid(r,t)      #create a r,theta meshgrid
c = tg                         #define color values as theta value
im = ax.pcolormesh(t, r, c.T,norm=norm)  #plot the colormesh on axis with colormap
ax.set_yticklabels([])                   #turn of radial tick labels (yticks)
ax.tick_params(pad=15,labelsize=24)      #cosmetic changes to tick labels
ax.spines['polar'].set_visible(False)    #turn off the axis spine.

It gives this:

A color wheel for the viridis colormap. Made with matplotlib 2.1.

icemtel
  • 412
  • 5
  • 12
toconnor
  • 101
  • 1
  • 2
0

Based on 2nd answer draw hsv wheel, and change red position in the wheel

import matplotlib.pyplot as plt
import numpy as np
from matplotlib import cm
import matplotlib as mpl

# If displaying in a Jupyter notebook:
# %matplotlib inline 

# Generate a figure with a polar projection
fg = plt.figure(figsize=(8,8))
ax = fg.add_axes([0.1,0.1,0.8,0.8], projection='polar')

# Define colormap normalization for 0 to 2*pi
norm = mpl.colors.Normalize(0, 2*np.pi) 

# Plot a color mesh on the polar plot
# with the color set by the angle
colormap = plt.get_cmap('hsv')
norm = mpl.colors.Normalize(start, end)

s = np.pi*65/180
e = np.pi*65/180+2*np.pi

n = 1000  #the number of secants for the mesh
t = np.linspace(s,e,n)   #theta values
r = np.linspace(0,1,2)        #radius values change 0.6 to 0 for full circle
rg, tg = np.meshgrid(r,t)      #create a r,theta meshgrid
c = tg                         #define color values as theta value

im = ax.pcolormesh(t, r, c.T,norm=norm, cmap=colormap)  #plot the colormesh on axis with colormap
ax.set_yticklabels([])                   #turn of radial tick labels (yticks)
# ax.tick_params(pad=15,labelsize=24)      #cosmetic changes to tick labels
# ax.spines['polar'].set_visible(False)    #turn off the axis spine.
ax.set_thetagrids(np.linspace(0, 360, 12, endpoint=False), 
                  ['0','30', '60', '90', '120', '150','180','210','240','270', '300','330'])
ax.grid(False)

enter image description here

Xavier
  • 1
  • 1