Matplotlib 3D scatter color lost after redraw - python

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...

Related

Why doesn't blitting work with animation of a scatter plot in matplotlib?

I am trying to create an animation containing a fixed sphere and a trajectory on the surface of the sphere, with a trail of the trajectory containing the last "windowSize" points in the trajectory.
Now, for the purposes of the code I will show here, I won't have an actual such trajectory, but rather just some random points changing each frame.
I am using matplotlib.animation.FuncAnimation. When I use the option blit=False, the animation works as expected. However, I would like to use blit=True to optimize performance.
When I do that, though, what happens is that nothing seems to happen in the animation, except that when I rotate the figure, then it shows an updated version of the figure (some number of frames ahead) and then freezes again.
The code below is based on this similar question.
Let me show the code I am using
import numpy as np
from matplotlib import pyplot as plt
import matplotlib.animation
import pandas as pd
Np = 5000
windowSize = 1000
m = np.random.rand(Np, 3)
df = pd.DataFrame({ "x" : m[0:Np,0], "y" : m[0:Np,1], "z" : m[0:Np,2]})
def init_graph():
u, v = np.mgrid[0:2*np.pi:50j, 0:np.pi:50j]
x = np.cos(u)*np.sin(v)
y = np.sin(u)*np.sin(v)
z = np.cos(v)
ax.plot_surface(x, y, z, color="bisque", alpha=0.3)
return graph,
def update_graph(num):
if (num<windowSize):
graph._offsets3d = (df.x[0:num], df.y[0:num], df.z[0:num])
else:
graph._offsets3d = (df.x[(num-windowSize):num], df.y[(num-windowSize):num], df.z[(num-windowSize):num])
title.set_text('3D Test, time={}'.format(num))
return graph,
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
ax.set_box_aspect((1,1,1))
title = ax.set_title('3D Test')
graph = ax.scatter(0, 0, 0)
ani = matplotlib.animation.FuncAnimation(fig, update_graph, frames=Np, init_func=init_graph, interval=200, blit=True, repeat=False)
plt.show()
m is an Np by 3 matrix, and each row represents a 3d point (in my real use case, each row is a point in a trajectory on the sphere surface, but for this demo I created m as random numbers).
I create a variable graph that contains a scatter plot, which I believe is an Artist. This is what I return from both the init_func and the updating func which are passed to FuncAnimation (as per the docs).
From what I read, you return an iterable of the Artists which will be updated in the animation. Thus I return a tuple of one element, graph,.
Now, in update_graph, the updating function for the animation, I am updating the scatter plot using graph._offsets3d, which I read in another question here on StackOverflow. I am not totally sure if this is the way to do it and I didn't find much information in the docs about whether to use this or one of the setting methods on the scatter plot.
Why doesn't blitting work with scatter plots?

Matplotlib: make objects ignored by axis autoscaling

Is it possible to create a plot object that is ignored by the Axes autoscaler?
I often need to add vertical lines, or shade a region of a plot to show the desired range of data (as a frame of reference for the viewer), but then I have to set the axes auto-scales x/ylimits back to where they were before - or truncate the lines/shading to the current axis limits, or various other fandangos.
It would be much easier if these shader/vertical lines acted as "background" objects on the plot, ignored by the autoscaler, so only my real data affected the autoscale.
Here's an example:
This plot is of real-world data, and I want to see if the data is within desired limits from day to day.
I want to shade the 3rd axis plot from -50 nm ≤ Y ≤ +50 nm.
I'd love to simply add a giant translucent rectangle from -50 --> +50nm, but have the autoscale ignore it.
Eg. like this (I manually added the red shading in a drawing prog.):
Also, you can see I've manually added vertical lines using code like this (I should really just use the vertical gridline locations...):
ax1.set_ylim(ymin, ymax)
ax1.vlines( self.Dates , color="grey", alpha=0.05, ymin=ax1.get_ylim()[0], ymax=ax1.get_ylim()[1] )
You can see in the 2nd & 3rd axes, that the VLines pushed the AutoScaling outwards, so now there's a gap between the VLine and Axis. Currently I'd need to finagle the order of calling fig.tight_layout() and ax2/ax3.plot(), or convert to manually setting the X-Tick locations/gridlines etc. - but it would be even easier if these VLines were not even treated as data, so the autoscale ignored them.
Is this possible, to have autoscale "ignore" certain objects?
autoscale_view predominantly uses the dataLim attribute of the axis to figure out the axis limits. In turn, the data limits are set by axis methods such as _update_image_limits, _update_line_limits, or _update_patch_limits. These methods all use essential attributes of those artists to figure out the new data limits (e.g. the path), so overriding them for "background" artists won't work. So no, strictly speaking, I don't think it is possible for autoscale to ignore certain objects, as long as they are visible.
However, there are other options to retain a data view apart from the ones mentioned so far.
Use artists that don't affect the data limits, e.g. axhline and axvline or add patches (and derived classes) using add_artist.
#!/usr/bin/env python
import numpy as np
import matplotlib.pyplot as plt
x, y = np.random.randn(2, 1000)
fig, ax = plt.subplots()
ax.scatter(x, y, zorder=2)
ax.add_artist(plt.Rectangle((0,0), 6, 6, alpha=0.1, zorder=1))
ax.axhline(0)
ax.axvline(0)
You can plot your foreground objects, and then turn autoscale off.
#!/usr/bin/env python
import numpy as np
import matplotlib.pyplot as plt
x, y = np.random.randn(2, 1000)
fig, ax = plt.subplots()
ax.scatter(x, y, zorder=2)
ax.autoscale_view() # force auto-scale to update data limits based on scatter
ax.set_autoscale_on(False)
ax.add_patch(plt.Rectangle((0,0), 6, 6, alpha=0.1, zorder=1))
The only other idea I have is to monkey patch Axes.relim() to check for a background attribute (which is probably the closest to what you are imagining):
import numpy as np
import matplotlib.axes
import matplotlib.transforms as mtransforms
import matplotlib.image as mimage
import matplotlib.lines as mlines
import matplotlib.patches as mpatches
class PatchedAxis(matplotlib.axes.Axes):
def relim(self, visible_only=False):
"""
Recompute the data limits based on current artists.
At present, `.Collection` instances are not supported.
Parameters
----------
visible_only : bool, default: False
Whether to exclude invisible artists.
"""
# Collections are deliberately not supported (yet); see
# the TODO note in artists.py.
self.dataLim.ignore(True)
self.dataLim.set_points(mtransforms.Bbox.null().get_points())
self.ignore_existing_data_limits = True
for artist in self._children:
if not visible_only or artist.get_visible():
if not hasattr(artist, "background"):
if isinstance(artist, mlines.Line2D):
self._update_line_limits(artist)
elif isinstance(artist, mpatches.Patch):
self._update_patch_limits(artist)
elif isinstance(artist, mimage.AxesImage):
self._update_image_limits(artist)
matplotlib.axes.Axes = PatchedAxis
import matplotlib.pyplot as plt
x, y = np.random.randn(2, 1000)
fig, ax = plt.subplots()
ax.scatter(x, y, zorder=2)
rect = plt.Rectangle((0,0), 6, 6, alpha=0.1, zorder=1)
rect.background = True
ax.add_patch(rect)
ax.relim()
ax.autoscale_view()
However, for some reason ax._children is not populated when calling relim. Maybe someone else can figure out under what conditions ax._children attribute is created.

matplotlib 3: 3D scatter plots with tight_layout

I have some code which produces a 3D scatter plot using matplotlib's scatter in conjunction with tight_layout, see the simplified code below:
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import proj3d
fig = plt.figure()
ax = fig.gca(projection='3d')
N = 100
x = np.random.random(N)
y = np.random.random(N)
z = np.random.random(N)
ax.scatter(x, y, z)
plt.tight_layout() # <-- Without this, everything is fine
plt.savefig('scatter.png')
In matplotlib 2.2.3 this makes a figure like so:
Similar output is generated by older versions, at least back to 1.5.1. When using the new version 3.0.0, something goes wrong at plt.tight_layout() and I get the following output:
Accompanying this is the warning
.../matplotlib/tight_layout.py:177: UserWarning: The left and right margins cannot be made large enough to accommodate all axes decorations
One may argue that using tight_layout with no arguments as here does not (on older matplotlibs) consistently lead to the expected tightened margins anyway, and so one should refrain from using tight_layout with 3D plots in the first place. However, by manually tweaking the arguments to tight_layout it is (used to be) a decent way to trim the margins even on 3D plots.
My guess is that this is a bug in matplotlib, but maybe they've made some deliberate change I havn't picked up on. Any pointers about a fix is appreciated.
Thanks to the comment by ImportanceOfBeingErnest, it now works:
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import proj3d
fig = plt.figure()
ax = fig.gca(projection='3d')
N = 100
x = np.random.random(N)
y = np.random.random(N)
z = np.random.random(N)
ax.scatter(x, y, z)
# The fix
for spine in ax.spines.values():
spine.set_visible(False)
plt.tight_layout()
plt.savefig('scatter.png')
From the links in the comment, it seems that this will be fixed in matplotlib 3.0.x. For now, the above may be used.
plt.tight_layout()
plt.show()
Right below your main body code of plotting.

Python: Why do plots of functions with two variables look spurious?

I am using the following code to plot a function of two variables
import numpy as np
from mpl_toolkits.mplot3d import Axes3D
from pylab import meshgrid
import matplotlib.pyplot as plt
x = np.arange(0,1.0,0.01)
y = np.arange(0,1.0,0.01)
X,Y = meshgrid(x, y)
Z = np.sin(2*np.abs(X-0.3)+2*np.sin(5*Y))
fig = plt.figure()
ax = fig.gca(projection='3d')
ax.plot_surface(X, Y, Z)
plt.show()
The result looks like this:
What are those lines that bump out of the surface coming from?
They are not in my data. Changing the resolution to 0.001 fixes them, but this makes the plotting really slow.
By default, ax.plot_surface, ignores some of the data. The problem is that it does not ignore this data to draw the black lines. Therefore, the black lines are based on different data than the connecting blue patches.
This can be turned by passing optional arguments:
ax.plot_surface(X, Y, Z,cstride=1,rstride=1)
It is not clear to me what the idea behind the default settings is. I would be happy to be illuminated.

how to make a 3d effect on bars in matplotlib?

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.

Categories

Resources