Similarly to a previous question of mine, I'd like to control the capstyle of lines being drawn using matplotlib. However, I have an extremely large number of lines, and drawing with anything other than a line collection takes way too long. Are there any workarounds to control the capstyle of lines in a line collection in a generic way (or alternatively, super fast ways of drawing a large number of Line2D lines). For instance, I've tried using the matplotlib rc settings via:
import matplotlib as mpl
mpl.rcParams['lines.solid_capstyle'] = 'round'
mpl.rcParams['lines.solid_joinstyle'] = 'round'
But this doesn't appear to have any affect. From the docstring for collections.py:
The classes are not meant to be as flexible as their single element counterparts (e.g. you may not be able to select all line styles) but they are meant to be fast for common use cases (e.g. a large set of solid line segemnts)
Which explains why I can't seem to control various parameters, but I still want to do it! I've had a look at the code for the AGG backend (_backend_agg.cpp: not that I really understand it), and it appears that line_cap and line_join are controlled by gc.cap and gc.join, where gc comes from the GCAgg class. Does anyone know how one can control this from Python? Am I asking the right question here? Perhaps that are easier ways to control these parameters?
Any help is greatly appreciated... I'm desperate to get this working, so even crazy hacks are welcome!
Thanks,
Carson
Since you mention in your question that you don't mind "dirty" solutions, one option would as follows.
The "drawing process" of a particular LineCollection is handled by the draw method defined in the Collection class (the base of LineCollection). This method creates an instance of GraphicsContextBase (defined in backend_bases.py) via the statement gc = renderer.new_gc(). It seems to be exactly this object which governs among other things the properties controlling the capstyle (property _capstyle). Therefore, one could subclass GraphicsContextBase, override the _capstyle property, and inject a new new_gc method into the RendererBase class so that consequent calls to new_gc return the customized instance:
Borrowing the example from the answer by #florisvb (assuming Python3):
#!/usr/bin/env python
import types
import numpy as np
from matplotlib.backend_bases import GraphicsContextBase, RendererBase
import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection
class GC(GraphicsContextBase):
def __init__(self):
super().__init__()
self._capstyle = 'round'
def custom_new_gc(self):
return GC()
RendererBase.new_gc = types.MethodType(custom_new_gc, RendererBase)
#----------------------------------------------------------------------
np.random.seed(42)
x = np.random.random(10)
y = np.random.random(10)
points = np.array([x, y]).T.reshape((-1, 1, 2))
segments = np.concatenate([points[:-1], points[1:]], axis=1)
fig = plt.figure()
ax = fig.add_subplot(111)
linewidth = 10
lc = LineCollection(segments, linewidths=linewidth)
ax.add_collection(lc)
fig.savefig('fig.png')
This produces:
To update the answer from #ewcz as this thread still comes up in search results.
One can now use path_effects instead of defining their own GraphicsContextBase.
e.g.
import numpy as np
import matplotlib.patheffects as path_effects
from matplotlib.collections import LineCollection
np.random.seed(42)
x = np.random.random(10)
y = np.random.random(10)
points = np.array([x, y]).T.reshape((-1, 1, 2))
segments = np.concatenate([points[:-1], points[1:]], axis=1)
fig = plt.figure()
ax = fig.add_subplot(111)
linewidth = 10
### Stroke redraws the segment passing kwargs down to the GC renderer
lc = LineCollection(segments, linewidths=linewidth,
path_effects=[path_effects.Stroke(capstyle="round")])
ax.add_collection(lc)
fig.show()
Example png output with smooth lines
and it also seems to work well with pdf output
I was struggling with the same issue. I ended up plotting a scatter plot on top of my line collection. It's not perfect, but it may work for your application. There's a few subtleties - below is a working example.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection
x = np.random.random(10)
y = np.random.random(10)
z = np.arange(0,10)
points = np.array([x, y]).T.reshape(-1, 1, 2)
segments = np.concatenate([points[:-1], points[1:]], axis=1)
fig = plt.figure()
ax = fig.add_subplot(111)
linewidth = 10
cmap = plt.get_cmap('jet')
norm = plt.Normalize(np.min(z), np.max(z))
color = cmap(norm(z))
lc = LineCollection(segments, linewidths=linewidth, cmap=cmap, norm=norm)
lc.set_array(z)
lc.set_zorder(z.tolist())
ax.add_collection(lc)
ax.scatter(x,y,color=color,s=linewidth**2,edgecolor='none', zorder=(z+2).tolist())
Related
I'm just starting using Matplotlib the "right" way. I'm writing various programs that will each give me back a time series, and I'm looking to superimpose the graphs of the various time series, like this:
I think what I want is a single Axes instance defined in the main function, then I call each of my little functions, and they all return a Line2D instance, and then I'll put them all on the Axes object I created.
But I'm having trouble taking an existing Line2D object and adding it to an existing Axes object (like I'd want to do with the output of my function.) I thought of taking a Line2D called a and say ax.add_line(a).
import matplotlib.pyplot as plt
a, = plt.plot([1,2,3], [3,4,5], label = 'a')
fig, ax = plt.subplots()
ax.add_line(a)
Gives me a RuntimeError: "Can not put single artist in more than one figure."
I'm guessing that over time Matplotlib has stopped wanting users to be able to add a given line to any Axes they want. A similar thing is discussed in the comments of this answer, except there they're talking about an Axes object in two different Figure objects.
What's the best way to accomplish what I want? I'd rather keep my main script tidy, and not say ax.plot(some_data) over and over when I want to superimpose these lines.
Indeed, you cannot add the same artist to more than one axes or figure.
But for what I understand from your question, that isn't really necessary.
So let's just do as you propose;
"I thought of taking a Line2D called a and say ax.add_line(a)."
import numpy as np
import matplotlib.pyplot as plt
def get_line(label="a"):
return plt.Line2D(np.linspace(0,1,10), np.random.rand(10), label = label)
fig, ax = plt.subplots()
ax.add_line(get_line(label="a"))
ax.add_line(get_line(label="b"))
ax.add_line(get_line(label="z"))
ax.legend()
plt.show()
The way matplotlib would recommend is to create functions that take an axes as input and plot to that axes.
import numpy as np
import matplotlib.pyplot as plt
def plot_line(ax=None, label="a"):
ax = ax or plt.gca()
line, = ax.plot(np.linspace(0,1,10), np.random.rand(10), label = label)
return line
fig, ax = plt.subplots()
plot_line(ax, label="a")
plot_line(ax, label="b")
plot_line(ax, label="z")
ax.legend()
plt.show()
A possible work around for your problem:
import matplotlib.pyplot as plt
x = np.array([1,2,3])
y = np.array([3,4,5])
label = '1'
def plot(x,y,label):
a, = plt.plot(x,y, label = label)
return a
fig, ax = plt.subplots()
plot(x,y,label)
plot(x,1.5*y,label)
You can put your plot command now in a loop with changing labels. You can still use the ax handle to modify/define the plot parameters.
Is there an easy way to draw a zigzag or wavy line in matplotlib?
I'm aware of the different line styles (http://matplotlib.org/examples/lines_bars_and_markers/line_styles_reference.html), and I'm of course aware that instead of plotting
plt.figure(); plt.plot(n.linspace(0.7,1.42,100),[0.7]*100)
I could plot
plt.figure(); plt.plot(n.linspace(0.7,1.42,100),[0.69,0.71]*50)
for a zigzag-line, but I was wondering whether there was a more straightforward way?
Yes there is, but it comes with a little bit of fallout. The easiest way is to use the xkcd mode in matplotlib.
import numpy as np
import matplotlib.pyplot as plt
plt.xkcd()
plt.figure()
plt.plot(np.linspace(0.7,1.42,100),[0.7]*100)
plt.show()
Which gives you the following:
If you take a look at the code used to achieve this you will find that the xkcd function makes some changes to the rcParams dictionary. Most notably the entry rcParams['path.sketch'] = (scale, length, randomness) which is a path effect that is able to simulate a hand drawn look. The default parameters used by xkcd style are:
# explanation from the docstring of the xkcd function
scale = 1 # amplitude of the wiggle
length = 100 # length of the wiggle along the line
randomness = 2 # scale factor for shrinking and expanding the length
You can change the entries in the rcParams dictionary if you import it from the matplotlib package. In the following example I increased the randomness value from 2 to 100:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import rcParams
rcParams['path.sketch'] = (1, 100, 100)
plt.plot(np.linspace(0.7,1.42,100),[0.7]*100)
plt.show()
Which will result in the following plot:
As you can see, more jiggling and the font used for the ticks is still 'normal'. However, the style is also used to draw the axes and so far I have not found a way around that.
Two workarounds could be:
Work without drawn borders/ spines.
Plot spines and line independently (hard and annoying to automize).
Dig through the documentation of matplotlib and path styles and find out if there is a way to set path styles only for a subset of drawn lines.
Option 1 can be achieved like this:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import rcParams
rcParams['path.sketch'] = (10, 10, 100)
fig = plt.plot(np.linspace(0.7,1.42,100),[0.7]*100)
for pos, spine in fig[0].axes.spines.items():
spine.set_visible(False)
plt.show()
Which, in my opinion look quite ok. borders around plots are highly overrated anyways.
Edit: Less Chaos
To get an evenly waved line, set the randomness parameter to 1 and pick small values for amplitude and length:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import rcParams
rcParams['path.sketch'] = (3, 10, 1)
fig = plt.plot(np.linspace(0.7,1.42,100),[0.7]*100)
for pos, spine in fig[0].axes.spines.items():
spine.set_visible(False)
plt.show()
Bonus image: More Chaos
rcParams['path.sketch'] = (100, 1, 100)
You can apply the change in rcParams['path.sketch'] dictionary only to selected curves using with.
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
# prepare some fancy data
x = np.linspace(0,5,200)
y_0 = 10*x**0.2-x**1.5
y_1 = 20*np.sin(x)
y_2 = x**2
# prepare figure and axis
fig, ax = plt.subplots(nrows=1, ncols = 1, figsize = (5,3), dpi = 128)
# plot with some normal style
ax.plot(x, y_0, color = 'gray', ls='-.', lw = 2, label = 'normal style')
# now plot the wavy-like style!!!!
with mpl.rc_context({'path.sketch': (5, 15, 1)}):
ax.plot(x, y_1, color = 'blue', label = 'wavy style!')
# again plot with some different normal style
ax.plot(x, y_2, color = 'orange', ls = '-', lw = 3, label = 'again normal style')
ax.legend(loc='best') # turn on legend with automatic best location
plt.show()
I have 3 different plots that are currently each saved as separate figures. However, due to space constraints I would like to layer them behind each other and offset like so:
I am trying to convey that a similar pattern exists across each plot and this is a nice and compact way of doing so. I would like to programmatically draw such a figure using matplotlib, but I'm not sure how to layer and offset the graphs using the usual pyplot commands. Any suggestions would be helpful. The following code is a skeleton of what I have currently.
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
window = 100
xs = np.arange(100)
ys = np.zeros(100)
ys[80:90] = 1
y2s = np.random.randn(100)/5.0+0.5
with sns.axes_style("ticks"):
for scenario in ["one", "two", "three"]:
fig = plt.figure()
plt.plot(xs, ys)
plt.plot(xs, y2s)
plt.title(scenario)
sns.despine(offset=10)
You can manually create the axes to plot into and position them as you like.
To highlight this approach modified your example as follows
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
window = 100
xs = np.arange(100)
ys = np.zeros(100)
ys[80:90] = 1
y2s = np.random.randn(100)/5.0+0.5
fig = plt.figure()
with sns.axes_style("ticks"):
for idx,scenario in enumerate(["one", "two", "three"]):
off = idx/10.+0.1
ax=fig.add_axes([off,off,0.65,0.65], axisbg='None')
ax.plot(xs, ys)
ax.plot(xs, y2s)
ax.set_title(scenario)
sns.despine(offset=10)
which gives a plot like
Here, I used fig.add_axes to add manually created axes objects to the predefined figure object. The arguments specify the position and size of the newly created axes, see docs.
Note that I also set the axes background to be transparent (axisbg='None').
Related to this question, I want a 3D scatter plot with prescribed colors for each point. The example posted in the question works on my system, but after the first redraw (for instance after saving or if I rotate the image) the color seems to be lost, i.e. all the points are drawn in blue color with the usual depth information. Please see the modified example below.
My system is Python 2.6.7 with matplotlib 1.1.0 installed from macports on a mac 10.8.0. I use the MacOSX backend.
Does anyone know how to circumvent this problem?
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
# Create Map
cm = plt.get_cmap("RdYlGn")
x = np.random.rand(30)
y = np.random.rand(30)
z = np.random.rand(30)
col = np.arange(30)
fig = plt.figure()
ax3D = fig.add_subplot(111, projection='3d')
ax3D.scatter(x, y, z, s=30, c=col, marker='o', cmap=cm)
plt.savefig('image1.png')
plt.savefig('image2.png')
Here are the two images, I get:
It's not clear why this is happening, and it certainly is a bug. Here I provide a hack to get the result you want, though it is not as automatic as one would want.
For some reason, the Patch3DCollection representing the scatter points is not updated after the first rendering. This update is essential, because it is where unique colors are set for each collection patch. To force it to reinitialize, you can use the changed method on the Patch3DCollection (really a ScalarMappable method), and this just documents that a change happend. When the figure is drawn, it checks if an update happened, and then it redefines the colors. If it didn't, this process is skipped.
To force this update to occur automatically, one would like to do this on every 'draw' event. To do this, one must register a method using the canvas's mpl_connect method (see linked tutorial).
This example shows how saving the figure twice preserves the color mapping, but if you uncomment the plt.show() line, it will still work (on rotation for example).
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
# Create Map
cm = plt.get_cmap("RdYlGn")
# added a seed so consistant plotting of points
np.random.seed(101)
x = np.random.rand(30)
y = np.random.rand(30)
z = np.random.rand(30)
col = np.arange(30)
fig = plt.figure()
#ax = fig.add_subplot(111)
#scatCollection = ax.scatter(x,y,
ax3D = fig.add_subplot(111, projection='3d')
# keep track of the Patch3DCollection:
scatCollection = ax3D.scatter(x, y, z, s=30,
c=col,
marker='o',
cmap=cm
)
def forceUpdate(event):
global scatCollection
scatCollection.changed()
fig.canvas.mpl_connect('draw_event',forceUpdate)
#plt.show()
plt.savefig('image1.png')
plt.savefig('image2.png')
Ideally it should not be required to do this, and the global scatCollection should be accessed using other methods (I'm working on doing this). But this works for now...
I have a very simple basic bar's graphic like this one
but i want to display the bars with some 3d effect, like this
I just want the bars to have that 3d effect...my code is:
fig = Figure(figsize=(4.6,4))
ax1 = fig.add_subplot(111,ylabel="Valeur",xlabel="Code",autoscale_on=True)
width = 0.35
ind = np.arange(len(values))
rects = ax1.bar(ind, values, width, color='#A1B214')
ax1.set_xticks(ind+width)
ax1.set_xticklabels( codes )
ax1.set_ybound(-1,values[0] * 1.1)
canvas = FigureCanvas(fig)
response = HttpResponse(content_type='image/png')
canvas.print_png(response)
i've been looking in the gallery of matplotlib,tried a few things but i wasn't lucky, Any ideas? Thxs
I certainly understand your reason for needing a 3d bar plot; i suspect that's why they were created.
The libraries ('toolkits') in Matplotlib required to create 3D plots are not third-party libraries, etc., rather they are included in the base Matplotlib installation.
(This is true for the current stable version, which is 1.0, though i don't believe it was for 0.98, so the change--from 'add-on' to part of the base install--occurred within the past year, i believe)
So here you are:
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as PLT
import numpy as NP
fig = PLT.figure()
ax1 = fig.add_subplot(111, projection='3d')
xpos = NP.random.randint(1, 10, 10)
ypos = NP.random.randint(1, 10, 10)
num_elements = 10
zpos = NP.zeros(num_elements)
dx = NP.ones(10)
dy = NP.ones(10)
dz = NP.random.randint(1, 5, 10)
ax1.bar3d(xpos, ypos, zpos, dx, dy, dz, color='#8E4585')
PLT.show()
To create 3d bars in Maplotlib, you just need to do three (additional) things:
import Axes3D from mpl_toolkits.mplot3d
call the bar3d method (in my scriptlet, it's called by ax1 an instance of the Axes class). The method signature:
bar3d(x, y, z, dy, dz, color='b', zsort="average", *args, **kwargs)
pass in an additional argument to add_subplot, projection='3d'
As far as I know Matplotlib doesn't by design support features like the "3D" effect you just mentioned. I remember reading about this some time back. I don't know it has changed in the meantime.
See this discussion thread for more details.
Update
Take a look at John Porter's mplot3d module. This is not a part of standard matplotlib but a custom extension. Never used it myself so can't say much about its usefulness.