Matplotlib coordinates tranformation - python

I am trying to understand this code snippet:
def add_inset(ax, rect, *args, **kwargs):
box = ax.get_position()
inax_position = ax.transAxes.transform(rect[0:2])
infig_position = ax.figure.transFigure.inverted().transform(inax_position)
new_rect = list(infig_position) + [box.width * rect[2], box.height * rect[3]]
return fig.add_axes(new_rect, *args, **kwargs)
This code adds an inset to an existing figure. It looks like this:
The original code is from this notebook file.
I don't understand why two coordinates transformation are needed:
inax_position = ax.transAxes.transform(rect[0:2])
infig_position = ax.figure.transFigure.inverted().transform(inax_position)

Explanation
In the method add_inset(ax, rect), rect is a rectangle in axes coordinates. That makes sense because you often want to specify the location of the inset relavtive to the axes in which it lives.
However in order to later be able to create a new axes, the axes position needs to be known in figure coordinates, which can then be given to fig.add_axes(figurecoordinates).
So what is needed is a coordinate transform from axes coordinates to figure coordinates. This is performed here in a two-step process:
Transform from axes coords to display coords using transAxes.
Transform from display coords to figure coords using the inverse of transFigure.
This two step procedure could be further condensed in a single transform like
mytrans = ax.transAxes + ax.figure.transFigure.inverted()
infig_position = mytrans.transform(rect[0:2])
It may be of interest to read the matplotlib transformation tutorial on how transformations work.
Alternatives
The above might not be the most obvious method to place an inset. Matplotlib provides some tools itself. A convenient method is the mpl_toolkits.axes_grid1.inset_locator. Below are two ways to use its inset_axes method when creating insets in axes coordinates.
import matplotlib.pyplot as plt
import mpl_toolkits.axes_grid1.inset_locator as il
fig, (ax1, ax2) = plt.subplots(nrows=2, figsize=(4,4))
ax1.plot([1,2,3],[2.2,2,3])
# set the inset at upper left (loc=2) with width, height=0.5,0.4
axins = il.inset_axes(ax1, "50%", "40%", loc=2, borderpad=1)
axins.scatter([1,2,3],[3,2,3])
# set the inset at 0.2,0.5, with width, height=0.8,0.4
# in parent axes coordinates
axins2 = il.inset_axes(ax2, "100%", "100%", loc=3, borderpad=0,
bbox_to_anchor=(0.2,0.5,0.7,0.4),bbox_transform=ax2.transAxes,)
axins2.scatter([1,2,3],[3,2,3])
plt.show()

Related

Draw more axes than default in python matplotlib 3d [duplicate]

I'm doing some 3D surface plots using Matplotlib in Python and have noticed an annoying phenomenon. Depending on how I set the viewpoint (camera location), the vertical (z) axis moves between the left and right side. Here are two examples: Example 1, Axis left, Example 2, Axis right. The first example has ax.view_init(25,-135) while the second has ax.view_init(25,-45).
I would like to keep the viewpoints the same (best way to view the data). Is there any way to force the axis to one side or the other?
I needed something similar: drawing the zaxis on both sides. Thanks to the answer by #crayzeewulf I came to following workaround (for left, righ, or both sides):
First plot your 3d as you need, then before you call show() wrap the Axes3D with a Wrapper class that simply overrides the draw() method.
The Wrapper Class calls simply sets the visibility of some features to False, it draws itself and finally draws the zaxis with modified PLANES. This Wrapper Class allows you to draw the zaxis on the left, on the rigth or on both sides.
import matplotlib
matplotlib.use('QT4Agg')
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import axes3d
class MyAxes3D(axes3d.Axes3D):
def __init__(self, baseObject, sides_to_draw):
self.__class__ = type(baseObject.__class__.__name__,
(self.__class__, baseObject.__class__),
{})
self.__dict__ = baseObject.__dict__
self.sides_to_draw = list(sides_to_draw)
self.mouse_init()
def set_some_features_visibility(self, visible):
for t in self.w_zaxis.get_ticklines() + self.w_zaxis.get_ticklabels():
t.set_visible(visible)
self.w_zaxis.line.set_visible(visible)
self.w_zaxis.pane.set_visible(visible)
self.w_zaxis.label.set_visible(visible)
def draw(self, renderer):
# set visibility of some features False
self.set_some_features_visibility(False)
# draw the axes
super(MyAxes3D, self).draw(renderer)
# set visibility of some features True.
# This could be adapted to set your features to desired visibility,
# e.g. storing the previous values and restoring the values
self.set_some_features_visibility(True)
zaxis = self.zaxis
draw_grid_old = zaxis.axes._draw_grid
# disable draw grid
zaxis.axes._draw_grid = False
tmp_planes = zaxis._PLANES
if 'l' in self.sides_to_draw :
# draw zaxis on the left side
zaxis._PLANES = (tmp_planes[2], tmp_planes[3],
tmp_planes[0], tmp_planes[1],
tmp_planes[4], tmp_planes[5])
zaxis.draw(renderer)
if 'r' in self.sides_to_draw :
# draw zaxis on the right side
zaxis._PLANES = (tmp_planes[3], tmp_planes[2],
tmp_planes[1], tmp_planes[0],
tmp_planes[4], tmp_planes[5])
zaxis.draw(renderer)
zaxis._PLANES = tmp_planes
# disable draw grid
zaxis.axes._draw_grid = draw_grid_old
def example_surface(ax):
""" draw an example surface. code borrowed from http://matplotlib.org/examples/mplot3d/surface3d_demo.html """
from matplotlib import cm
import numpy as np
X = np.arange(-5, 5, 0.25)
Y = np.arange(-5, 5, 0.25)
X, Y = np.meshgrid(X, Y)
R = np.sqrt(X**2 + Y**2)
Z = np.sin(R)
surf = ax.plot_surface(X, Y, Z, rstride=1, cstride=1, cmap=cm.coolwarm, linewidth=0, antialiased=False)
if __name__ == '__main__':
fig = plt.figure(figsize=(15, 5))
ax = fig.add_subplot(131, projection='3d')
ax.set_title('z-axis left side')
ax = fig.add_axes(MyAxes3D(ax, 'l'))
example_surface(ax) # draw an example surface
ax = fig.add_subplot(132, projection='3d')
ax.set_title('z-axis both sides')
ax = fig.add_axes(MyAxes3D(ax, 'lr'))
example_surface(ax) # draw an example surface
ax = fig.add_subplot(133, projection='3d')
ax.set_title('z-axis right side')
ax = fig.add_axes(MyAxes3D(ax, 'r'))
example_surface(ax) # draw an example surface
plt.show()
As pointed out in a comment below by OP, the method suggested below did not provide adequate answer to the original question.
As mentioned in this note, there are lots of hard-coded values in axis3d that make it difficult to customize its behavior. So, I do not think there is a good way to do this in the current API. You can "hack" it by modifying the _PLANES parameter of the zaxis as shown below:
tmp_planes = ax.zaxis._PLANES
ax.zaxis._PLANES = ( tmp_planes[2], tmp_planes[3],
tmp_planes[0], tmp_planes[1],
tmp_planes[4], tmp_planes[5])
view_1 = (25, -135)
view_2 = (25, -45)
init_view = view_2
ax.view_init(*init_view)
Now the z-axis will always be on the left side of the figure no matter how you rotate the figure (as long as positive-z direction is pointing up). The x-axis and y-axis will keep flipping though. You can play with _PLANES and might be able to get the desired behavior for all axes but this is likely to break in future versions of matplotlib.

matplotlib mark_inset with different data in inset plot

This is a slightly tricky one to explain. Basically, I want to make an inset plot and then utilize the convenience of mpl_toolkits.axes_grid1.inset_locator.mark_inset, but I want the data in the inset plot to be completely independent of the data in the parent axes.
Example code with the functions I'd like to use:
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1.inset_locator import inset_axes
from mpl_toolkits.axes_grid1.inset_locator import mark_inset
from mpl_toolkits.axes_grid1.inset_locator import InsetPosition
data = np.random.normal(size=(2000,2000))
plt.imshow(data, origin='lower')
parent_axes = plt.gca()
ax2 = inset_axes(parent_axes, 1, 1)
ax2.plot([900,1100],[900,1100])
# I need more control over the position of the inset axes than is given by the inset_axes function
ip = InsetPosition(parent_axes,[0.7,0.7,0.3,0.3])
ax2.set_axes_locator(ip)
# I want to be able to control where the mark is connected to, independently of the data in the ax2.plot call
mark_inset(parent_axes, ax2, 2,4)
# plt.savefig('./inset_example.png')
plt.show()
The example code produces the following image:
So to sum up: The location of the blue box is entire controlled by the input data to ax2.plot(). I would like to manually place the blue box and enter whatever I want into ax2. Is this possible?
quick edit: to be clear, I understand why inset plots would have the data linked, as that's the most likely usage. So if there's a completely different way in matplotlib to accomplish this, do feel free to reply with that. However, I am trying to avoid manually placing boxes and lines to all of the axes I would place, as I need quite a few insets into a large image.
If I understand correctly, you want an arbitrarily scaled axis at a given position that looks like a zoomed inset, but has no connection to the inset marker's position.
Following your approach you can simply add another axes to the plot and position it at the same spot of the true inset, using the set_axes_locator(ip) function. Since this axis is drawn after the original inset, it will be on top of it and you'll only need to hide the tickmarks of the original plot to let it disappear completely (set_visible(False) does not work here, as it would hide the lines between the inset and the marker position).
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1.inset_locator import inset_axes, mark_inset, InsetPosition
data = np.random.normal(size=(200,200))
plt.imshow(data, origin='lower')
parent_axes = plt.gca()
ax2 = inset_axes(parent_axes, 1, 1)
ax2.plot([60,75],[90,110])
# hide the ticks of the linked axes
ax2.set_xticks([])
ax2.set_yticks([])
#add a new axes to the plot and plot whatever you like
ax3 = plt.gcf().add_axes([0,0,1,1])
ax3.plot([0,3,4], [2,3,1], marker=ur'$\u266B$' , markersize=30, linestyle="")
ax3.set_xlim([-1,5])
ax3.set_ylim([-1,5])
ip = InsetPosition(parent_axes,[0.7,0.7,0.3,0.3])
ax2.set_axes_locator(ip)
# set the new axes (ax3) to the position of the linked axes
ax3.set_axes_locator(ip)
# I want to be able to control where the mark is connected to, independently of the data in the ax2.plot call
mark_inset(parent_axes, ax2, 2,4)
plt.show()
FWIW, I came up with a hack that works.
In the source code for inset_locator, I added a version of mark_inset that takes another set of axes used to define the TransformedBbox:
def mark_inset_hack(parent_axes, inset_axes, hack_axes, loc1, loc2, **kwargs):
rect = TransformedBbox(hack_axes.viewLim, parent_axes.transData)
pp = BboxPatch(rect, **kwargs)
parent_axes.add_patch(pp)
p1 = BboxConnector(inset_axes.bbox, rect, loc1=loc1, **kwargs)
inset_axes.add_patch(p1)
p1.set_clip_on(False)
p2 = BboxConnector(inset_axes.bbox, rect, loc1=loc2, **kwargs)
inset_axes.add_patch(p2)
p2.set_clip_on(False)
return pp, p1, p2
Then in my original-post code I make an inset axis where I want the box to be, pass it to my hacked function, and make it invisible:
# location of desired axes
axdesire = inset_axes(parent_axes,1,1)
axdesire.plot([100,200],[100,200])
mark_inset_hack(parent_axes, ax2, axdesire, 2,4)
axdesire.set_visible(False)
Now I have a marked box at a different location in data units than the inset that I'm marking:
It is certainly a total hack, and at this point I'm not sure it's cleaner than simply drawing lines manually, but I think for a lot of insets this will keep things conceptually cleaner.
Other ideas are still welcome.

Zoomed inset in matplotlib without re-plotting data

I'm working on some matplotlib plots and need to have a zoomed inset. This is possible with the zoomed_inset_axes from the axes_grid1 toolkit. See the example here:
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1.inset_locator import zoomed_inset_axes
from mpl_toolkits.axes_grid1.inset_locator import mark_inset
import numpy as np
def get_demo_image():
from matplotlib.cbook import get_sample_data
import numpy as np
f = get_sample_data("axes_grid/bivariate_normal.npy", asfileobj=False)
z = np.load(f)
# z is a numpy array of 15x15
return z, (-3,4,-4,3)
fig, ax = plt.subplots(figsize=[5,4])
# prepare the demo image
Z, extent = get_demo_image()
Z2 = np.zeros([150, 150], dtype="d")
ny, nx = Z.shape
Z2[30:30+ny, 30:30+nx] = Z
# extent = [-3, 4, -4, 3]
ax.imshow(Z2, extent=extent, interpolation="nearest",
origin="lower")
axins = zoomed_inset_axes(ax, 6, loc=1) # zoom = 6
axins.imshow(Z2, extent=extent, interpolation="nearest",
origin="lower")
# sub region of the original image
x1, x2, y1, y2 = -1.5, -0.9, -2.5, -1.9
axins.set_xlim(x1, x2)
axins.set_ylim(y1, y2)
plt.xticks(visible=False)
plt.yticks(visible=False)
# draw a bbox of the region of the inset axes in the parent axes and
# connecting lines between the bbox and the inset axes area
mark_inset(ax, axins, loc1=2, loc2=4, fc="none", ec="0.5")
plt.draw()
plt.show()
This will give the desired result:
http://matplotlib.org/1.3.1/_images/inset_locator_demo21.png
But as you can see in the code, the data has to be plotted twice - once for the main axis (ax.imshow...) and once for the inset axis (axins.imshow...).
My question is:
Is there a way to add a zoomed inset after the main plot is completed, without the need to plot everything again on the new axis?
Please note: I am not looking for a solution which wraps the plot call with a function and let the function plot ax and axins (see example below), but (if this exists) a native solution that makes use of the existing data in ax. Anybody knows if such a solution exists?
This is the wrapper-solution:
def plot_with_zoom(*args, **kwargs):
ax.imshow(*args, **kwargs)
axins.imshow(*args, **kwargs)
It works, but it feels a bit like a hack, since why should I need to plot all data again if I just want to zoom into a region of my existing plot.
Some additional clarification after the answer by ed-smith:
The example above is of course only the minimal example. There could be many different sets of data in the plot (and with sets of data I mean things plotted via imshow or plot etc). Imagine for example a scatter plot with 10 arrays of points, all plotted vs. common x.
As I wrote above, the most direct way to do that is just have a wrapper to plot the data in all instances. But what I'm looking for is a way (if it exists) to start with the final ax object (not the individual plotting commands) and somehow create the zoomed inset.
I think the following does what you want. Note that you use the returned handle to the first imshow and add it to the axis for the insert. You need to make a copy so you have a separate handle for each figure,
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1.inset_locator import zoomed_inset_axes
from mpl_toolkits.axes_grid1.inset_locator import mark_inset
import numpy as np
import copy
def get_demo_image():
from matplotlib.cbook import get_sample_data
import numpy as np
f = get_sample_data("axes_grid/bivariate_normal.npy", asfileobj=False)
z = np.load(f)
# z is a numpy array of 15x15
return z, (-3,4,-4,3)
fig, ax = plt.subplots(figsize=[5,4])
# prepare the demo image
Z, extent = get_demo_image()
Z2 = np.zeros([150, 150], dtype="d")
ny, nx = Z.shape
Z2[30:30+ny, 30:30+nx] = Z
# extent = [-3, 4, -4, 3]
im = ax.imshow(Z2, extent=extent, interpolation="nearest",
origin="lower")
#Without copy, image is shown in insert only
imcopy = copy.copy(im)
axins = zoomed_inset_axes(ax, 6, loc=1) # zoom = 6
axins.add_artist(imcopy)
# sub region of the original image
x1, x2, y1, y2 = -1.5, -0.9, -2.5, -1.9
axins.set_xlim(x1, x2)
axins.set_ylim(y1, y2)
plt.xticks(visible=False)
plt.yticks(visible=False)
# draw a bbox of the region of the inset axes in the parent axes and
# connecting lines between the bbox and the inset axes area
mark_inset(ax, axins, loc1=2, loc2=4, fc="none", ec="0.5")
plt.draw()
plt.show()
For your wrapper function, this would be something like,
def plot_with_zoom(*args, **kwargs):
im = ax.imshow(*args, **kwargs)
imcopy = copy.copy(im)
axins.add_artist(imcopy)
However, as imshow just displays the data stored in array Z as an image, I would think this solution would actually be slower than two separate calls to imshow. For plots which take more time, e.g. a contour plot or pcolormesh, this approach may be sensible...
EDIT:
Beyond a single imshow, and for multiple plots of different types. Plotting functions all return different handles (e.g. plot returns a list of lines, imshow returns a matplotlib.image.AxesImage, etc). You could keep adding these handles to a list (or dict) as you plot (or use a collection if they are similar enough). Then you could write a general function which adds them to an axis using add_artist or add_patch methods from the zoomed axis, probably with if type checking to deal with the various types used in the plot. A simpler method may be to loop over ax.get_children() and reuse anything which isn't an element of the axis itself.
Another option may be to look into blitting techniques, rasterization or other techniques used to speed up animation, for example using fig.canvas.copy_from_bbox or fig.canvas.tostring_rgb to copy the entire figure as an image (see why is plotting with Matplotlib so slow?‌​low). You could also draw the figure, save it to a non-vector graphic (with savefig or to a StringIO buffer), read back in and plot a zoomed in version.
Update: The solution below doesn't work in newer versions of matplotlib because some of the internal APIs have changed. For newer versions of matplotlib you can use https://github.com/matplotlib/matplotview, which provides the same functionality as this answer and some additional functionality.
I recently worked on a solution to this problem in a piece of software I am writing, and decided to share it here in case anyone is still dealing with this issue. This solution requires no replotting, simply the use of a custom zoom axes class instead of the default one. It works using a custom Renderer, which acts as a middle-man between the matplotlib Artists and the actual Renderer. Artists are then simply drawn using the custom Renderer instead of the original Renderer provided. Below is the implementation:
from matplotlib.path import Path
from matplotlib.axes import Axes
from matplotlib.axes._axes import _make_inset_locator
from matplotlib.transforms import Bbox, Transform, IdentityTransform, Affine2D
from matplotlib.backend_bases import RendererBase
import matplotlib._image as _image
import numpy as np
class TransformRenderer(RendererBase):
"""
A matplotlib renderer which performs transforms to change the final location of plotted
elements, and then defers drawing work to the original renderer.
"""
def __init__(self, base_renderer: RendererBase, mock_transform: Transform, transform: Transform,
bounding_axes: Axes):
"""
Constructs a new TransformRender.
:param base_renderer: The renderer to use for finally drawing objects.
:param mock_transform: The transform or coordinate space which all passed paths/triangles/images will be
converted to before being placed back into display coordinates by the main transform.
For example if the parent axes transData is passed, all objects will be converted to
the parent axes data coordinate space before being transformed via the main transform
back into coordinate space.
:param transform: The main transform to be used for plotting all objects once converted into the mock_transform
coordinate space. Typically this is the child axes data coordinate space (transData).
:param bounding_axes: The axes to plot everything within. Everything outside of this axes will be clipped.
"""
super().__init__()
self.__renderer = base_renderer
self.__mock_trans = mock_transform
self.__core_trans = transform
self.__bounding_axes = bounding_axes
def _get_axes_display_box(self) -> Bbox:
"""
Private method, get the bounding box of the child axes in display coordinates.
"""
return self.__bounding_axes.patch.get_bbox().transformed(self.__bounding_axes.transAxes)
def _get_transfer_transform(self, orig_transform):
"""
Private method, returns the transform which translates and scales coordinates as if they were originally
plotted on the child axes instead of the parent axes.
:param orig_transform: The transform that was going to be originally used by the object/path/text/image.
:return: A matplotlib transform which goes from original point data -> display coordinates if the data was
originally plotted on the child axes instead of the parent axes.
"""
# We apply the original transform to go to display coordinates, then apply the parent data transform inverted
# to go to the parent axes coordinate space (data space), then apply the child axes data transform to
# go back into display space, but as if we originally plotted the artist on the child axes....
return orig_transform + self.__mock_trans.inverted() + self.__core_trans
# We copy all of the properties of the renderer we are mocking, so that artists plot themselves as if they were
# placed on the original renderer.
#property
def height(self):
return self.__renderer.get_canvas_width_height()[1]
#property
def width(self):
return self.__renderer.get_canvas_width_height()[0]
def get_text_width_height_descent(self, s, prop, ismath):
return self.__renderer.get_text_width_height_descent(s, prop, ismath)
def get_canvas_width_height(self):
return self.__renderer.get_canvas_width_height()
def get_texmanager(self):
return self.__renderer.get_texmanager()
def get_image_magnification(self):
return self.__renderer.get_image_magnification()
def _get_text_path_transform(self, x, y, s, prop, angle, ismath):
return self.__renderer._get_text_path_transform(x, y, s, prop, angle, ismath)
def option_scale_image(self):
return False
def points_to_pixels(self, points):
return self.__renderer.points_to_pixels(points)
def flipy(self):
return self.__renderer.flipy()
# Actual drawing methods below:
def draw_path(self, gc, path: Path, transform: Transform, rgbFace=None):
# Convert the path to display coordinates, but if it was originally drawn on the child axes.
path = path.deepcopy()
path.vertices = self._get_transfer_transform(transform).transform(path.vertices)
bbox = self._get_axes_display_box()
# We check if the path intersects the axes box at all, if not don't waste time drawing it.
if(not path.intersects_bbox(bbox, True)):
return
# Change the clip to the sub-axes box
gc.set_clip_rectangle(bbox)
self.__renderer.draw_path(gc, path, IdentityTransform(), rgbFace)
def _draw_text_as_path(self, gc, x, y, s: str, prop, angle, ismath):
# If the text field is empty, don't even try rendering it...
if((s is None) or (s.strip() == "")):
return
# Call the super class instance, which works for all cases except one checked above... (Above case causes error)
super()._draw_text_as_path(gc, x, y, s, prop, angle, ismath)
def draw_gouraud_triangle(self, gc, points, colors, transform):
# Pretty much identical to draw_path, transform the points and adjust clip to the child axes bounding box.
points = self._get_transfer_transform(transform).transform(points)
path = Path(points, closed=True)
bbox = self._get_axes_display_box()
if(not path.intersects_bbox(bbox, True)):
return
gc.set_clip_rectangle(bbox)
self.__renderer.draw_gouraud_triangle(gc, path.vertices, colors, IdentityTransform())
# Images prove to be especially messy to deal with...
def draw_image(self, gc, x, y, im, transform=None):
mag = self.get_image_magnification()
shift_data_transform = self._get_transfer_transform(IdentityTransform())
axes_bbox = self._get_axes_display_box()
# Compute the image bounding box in display coordinates.... Image arrives pre-magnified.
img_bbox_disp = Bbox.from_bounds(x, y, im.shape[1], im.shape[0])
# Now compute the output location, clipping it with the final axes patch.
out_box = img_bbox_disp.transformed(shift_data_transform)
clipped_out_box = Bbox.intersection(out_box, axes_bbox)
if(clipped_out_box is None):
return
# We compute what the dimensions of the final output image within the sub-axes are going to be.
x, y, out_w, out_h = clipped_out_box.bounds
out_w, out_h = int(np.ceil(out_w * mag)), int(np.ceil(out_h * mag))
if((out_w <= 0) or (out_h <= 0)):
return
# We can now construct the transform which converts between the original image (a 2D numpy array which starts
# at the origin) to the final zoomed image (also a 2D numpy array which starts at the origin).
img_trans = (
Affine2D().scale(1/mag, 1/mag).translate(img_bbox_disp.x0, img_bbox_disp.y0)
+ shift_data_transform
+ Affine2D().translate(-clipped_out_box.x0, -clipped_out_box.y0).scale(mag, mag)
)
# We resize and zoom the original image onto the out_arr.
out_arr = np.zeros((out_h, out_w, im.shape[2]), dtype=im.dtype)
_image.resample(im, out_arr, img_trans, _image.NEAREST, alpha=1)
_image.resample(im[:, :, 3], out_arr[:, :, 3], img_trans, _image.NEAREST, alpha=1)
gc.set_clip_rectangle(clipped_out_box)
x, y = clipped_out_box.x0, clipped_out_box.y0
if(self.option_scale_image()):
self.__renderer.draw_image(gc, x, y, out_arr, None)
else:
self.__renderer.draw_image(gc, x, y, out_arr)
class ZoomViewAxes(Axes):
"""
A zoom axes which automatically displays all of the elements it is currently zoomed in on. Does not require
Artists to be plotted twice.
"""
def __init__(self, axes_of_zoom: Axes, rect: Bbox, transform = None, zorder = 5, **kwargs):
"""
Construct a new zoom axes.
:param axes_of_zoom: The axes to zoom in on which this axes will be nested inside.
:param rect: The bounding box to place this axes in, within the parent axes.
:param transform: The transform to use when placing this axes in the parent axes. Defaults to
'axes_of_zoom.transData'.
:param zorder: An integer, the z-order of the axes. Defaults to 5, which means it is drawn on top of most
object in the plot.
:param kwargs: Any other keyword arguments which the Axes class accepts.
"""
if(transform is None):
transform = axes_of_zoom.transData
inset_loc = _make_inset_locator(rect.bounds, transform, axes_of_zoom)
bb = inset_loc(None, None)
super().__init__(axes_of_zoom.figure, bb.bounds, zorder=zorder, **kwargs)
self.__zoom_axes = axes_of_zoom
self.set_axes_locator(inset_loc)
axes_of_zoom.add_child_axes(self)
def draw(self, renderer=None, inframe=False):
super().draw(renderer, inframe)
if(not self.get_visible()):
return
axes_children = [
*self.__zoom_axes.collections,
*self.__zoom_axes.patches,
*self.__zoom_axes.lines,
*self.__zoom_axes.texts,
*self.__zoom_axes.artists,
*self.__zoom_axes.images
]
img_boxes = []
# We need to temporarily disable the clip boxes of all of the images, in order to allow us to continue
# rendering them it even if it is outside of the parent axes (they might still be visible in this zoom axes).
for img in self.__zoom_axes.images:
img_boxes.append(img.get_clip_box())
img.set_clip_box(img.get_window_extent(renderer))
# Sort all rendered item by their z-order so the render in layers correctly...
axes_children.sort(key=lambda obj: obj.get_zorder())
# Construct mock renderer and draw all artists to it.
mock_renderer = TransformRenderer(renderer, self.__zoom_axes.transData, self.transData, self)
for artist in axes_children:
if(artist is not self):
artist.draw(mock_renderer)
# Reset all of the image clip boxes...
for img, box in zip(self.__zoom_axes.images, img_boxes):
img.set_clip_box(box)
# We need to redraw the splines if enabled, as we have finally drawn everything... This avoids other objects
# being drawn over the splines
if(self.axison and self._frameon):
for spine in self.spines.values():
spine.draw(renderer)
The example done using the custom zoom axes:
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1.inset_locator import mark_inset
from zoomaxes import ZoomViewAxes
from matplotlib.transforms import Bbox
import numpy as np
def get_demo_image():
from matplotlib.cbook import get_sample_data
import numpy as np
f = get_sample_data("axes_grid/bivariate_normal.npy", asfileobj=False)
z = np.load(f)
# z is a numpy array of 15x15
return z, (-3, 4, -4, 3)
fig, ax = plt.subplots(figsize=[5, 4])
# prepare the demo image
Z, extent = get_demo_image()
Z2 = np.zeros([150, 150], dtype="d")
ny, nx = Z.shape
Z2[30:30 + ny, 30:30 + nx] = Z
# extent = [-3, 4, -4, 3]
ax.imshow(Z2, extent=extent, interpolation="nearest",
origin="lower")
axins = ZoomViewAxes(ax, Bbox.from_bounds(0.6, 0.6, 0.35, 0.35), ax.transAxes) # Use the new zoom axes...
# sub region of the original image
x1, x2, y1, y2 = -1.5, -0.9, -2.5, -1.9
axins.set_xlim(x1, x2)
axins.set_ylim(y1, y2)
plt.xticks(visible=False)
plt.yticks(visible=False)
# draw a bbox of the region of the inset axes in the parent axes and
# connecting lines between the bbox and the inset axes area
mark_inset(ax, axins, loc1=2, loc2=4, fc="none", ec="0.5")
plt.draw()
plt.show()
Result
Nearly identical image of plot shown in question:
Advantages:
Fully automatic, and will redraw when changes are made to the plot.
Works on pretty much all artists. (I have personally tested lines, boxes, arrows, text, and images)
Avoids using blitting techniques, meaning zoom quality/depth is infinitely high. (Exception would be zooming on images obviously)
Disadvantages:
Not as fast as bliting techniques. All artists of the parent axes have to be looped over and drawn for every ZoomViewAxes instance.

changing size of a plot in a subplot figure

i create a figure with 4 subplots (2 x 2), where 3 of them are of the type imshow and the other is errorbar. Each imshow plots have in addition a colorbar at the right side of them. I would like to resize my 3rd plot, that the area of the graph would be exactly under the one above it (with out colorbar)
as example (this is what i now have):
How could i resize the 3rd plot?
Regards
To adjust the dimensions of an axes instance, you need to use the set_position() method. This applies to subplotAxes as well. To get the current position/dimensions of the axis, use the get_position() method, which returns a Bbox instance. For me, it's conceptually easier to just interact with the position, ie [left,bottom,right,top] limits. To access this information from a Bbox, the bounds property.
Here I apply these methods to something similar to your example above:
import matplotlib.pyplot as plt
import numpy as np
x,y = np.random.rand(2,10)
img = np.random.rand(10,10)
fig = plt.figure()
ax1 = fig.add_subplot(221)
im = ax1.imshow(img,extent=[0,1,0,1])
plt.colorbar(im)
ax2 = fig.add_subplot(222)
im = ax2.imshow(img,extent=[0,1,0,1])
plt.colorbar(im)
ax3 = fig.add_subplot(223)
ax3.plot(x,y)
ax3.axis([0,1,0,1])
ax4 = fig.add_subplot(224)
im = ax4.imshow(img,extent=[0,1,0,1])
plt.colorbar(im)
pos4 = ax4.get_position().bounds
pos1 = ax1.get_position().bounds
# set the x limits (left and right) to first axes limits
# set the y limits (bottom and top) to the last axes limits
newpos = [pos1[0],pos4[1],pos1[2],pos4[3]]
ax3.set_position(newpos)
plt.show()
You may feel that the two plots do not exactly look the same (in my rendering, the left or xmin position is not quite right), so feel free to adjust the position until you get the desired effect.

Mixing Matplotlib patches with polar plot?

I'm trying to plot some data in polar coordinates, but I don't want the standard ticks, labels, axes, etc. that you get with the Matplotlib polar() function. All I want is the raw plot and nothing else, as I'm handling everything with manually drawn patches and lines.
Here are the options I've considered:
1) Drawing the data with polar(), hiding the superfluous stuff (with ax.axes.get_xaxis().set_visible(False), etc.) and then drawing my own axes (with Line2D, Circle, etc.). The problem is when I call polar() and subsequently add a Circle patch, it's drawn in polar coordinates and ends up looking like an infinity symbol. Also zooming doesn't seem to work with the polar() function.
2) Skip the polar() function and somehow make my own polar plot manually using Line2D. The problem is I don't know how to make Line2D draw in polar coordinates and haven't figured out how to use a transform to do that.
Any idea how I should proceed?
Your option #2 is probably the simplest, given what you want to do. You would thus stay in rectangular coordinates, modify your function from polar to rectangular coordinates, and plot it with plot() (which is easier than using `Line2D').
The transformation of your polar function into a rectangular one can be done with:
def polar_to_rect(theta, r):
return (r*cos(theta), r*sin(theta))
and the plotting can be done with:
def my_polar(theta, r, *args, **kwargs):
"""
theta, r -- NumPy arrays with polar coordinates.
"""
rect_coords = polar_to_rect(theta, r)
pyplot.plot(rect_coords[0], rect_coords[1], *args, **kwargs)
# You can customize the plot with additional arguments, or use `Line2D` on the points in rect_coords.
To remove the ticks and the labels, try using
`matplotlib.pyplot.tick_params(axis='both', which='both', length=0, width=0, labelbottom = False, labeltop = False, labelleft = False, labelright = False)`
From http://matplotlib.sourceforge.net/api/pyplot_api.html#matplotlib.pyplot.polar
Regarding your comment about using the matplotlib transforms...I used the following method to translate a polar plot into a polygon that I could draw on my cartesian/rectangular axes.
import matplotlib.pyplot as plt
polarPlot = plt.subplot(111, polar = True)
# Create some dummy polar plot data
polarData = np.ones((360,2))
polarData[:,0] = np.arange(0, np.pi, np.pi/360) * polarData[:,0]
# Use the polar plot axes transformation into cartesian coordinates
cartesianData = polarPlot.transProjection.transform(polarData)

Categories

Resources