Annotate 3D scatter plot on pick event - python

I want to show annotations on a 3D scatter plot when the user clicks on a point.
The code I have shows the annotation once I move the plot after I click on a point.
from mpl_toolkits.mplot3d import proj3d
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(111, projection = '3d')
x = [1, 2, 3]
y = [1, 2, 3]
z = [1, 2, 3]
scatter = ax.scatter(x,y,z,picker=True)
def annotate_onclick(event):
point_index = int(event.ind)
print(point_index)
proj = ax.get_proj()
x_p, y_p, _ = proj3d.proj_transform(x[point_index], y[point_index], z[point_index], proj)
plt.annotate(str(point_index), xy=(x_p, y_p))
fig.canvas.mpl_connect('pick_event', annotate_onclick)
plt.show()
How can I make the annotation appear as soon as the user clicks on a point, without having to move the plot?

Hmm when using Matplotlib 3.4 the index flips depending on the orientation of the 3dgraph on the screen, and it throws this whole thing off. Run the code rotate graph by 180 degrees and click on the points. It's awful.
#!/usr/bin/env python3
from mpl_toolkits.mplot3d import proj3d
import matplotlib.pyplot as plt
import matplotlib
print('matplotlib: {}'.format(matplotlib.__version__))
fig = plt.figure()
ax = fig.add_subplot(111, projection = '3d')
x = [0, 2, 0,0]
y = [0, 2, 0,2]
z = [0, 2, 2,0]
scatter = ax.scatter(x,y,z,picker=True)
def chaos_onclick(event):
point_index = int(event.ind)
print(point_index)
#proj = ax.get_proj()
#x_p, y_p, _ = proj3d.proj_transform(x[point_index], y[point_index], z[point_index], proj)
#plt.annotate(str(point_index), xy=(x_p, y_p))
print("X=",x[point_index], " Y=",y[point_index], " Z=",z[point_index], " PointIdx=", point_index)
fig.canvas.mpl_connect('pick_event', chaos_onclick)
plt.show()

Add fig.canvas.draw_idle() at the end of your callback function to force the re-drawing of the new annotation.

OK here's a workaround with info gathered from Matplotlib: Annotating a 3D scatter plot .
#!/usr/bin/env python3
from mpl_toolkits.mplot3d import proj3d
import matplotlib.pyplot as plt
import numpy as np
import matplotlib
print('matplotlib: {}'.format(matplotlib.__version__))
fig = plt.figure()
ax = fig.add_subplot(111, projection = '3d')
x = [0, 2, 0,0]
y = [0, 2, 0,2]
z = [0, 2, 2,0]
scatter = ax.scatter(x,y,z,picker=True)
ax.set_xlabel('X axis')
ax.set_ylabel('Y axis')
ax.set_zlabel('Z axis')
def onMouseMotion(event):
print(event)
def chaos_onclick(event):
print(dir(event.mouseevent))
xx=event.mouseevent.x
yy=event.mouseevent.y
#magic from https://stackoverflow.com/questions/10374930/matplotlib-annotating-a-3d-scatter-plot
x2, y2, z2=proj3d.proj_transform(x[0], y[0], z[0], plt.gca().get_proj())
x3, y3 = ax.transData.transform((x2, y2))
#the distance
d=np.sqrt ((x3 - xx)**2 + (y3 - yy)**2)
print ("distance=",d)
#find the closest by searching for min distance.
#All glory to https://stackoverflow.com/questions/10374930/matplotlib-annotating-a-3d-scatter-plot
imin=0
dmin=10000000
for i in range(len(x)):
#magic from https://stackoverflow.com/questions/10374930/matplotlib-annotating-a-3d-scatter-plot
x2, y2, z2=proj3d.proj_transform(x[i], y[i], z[i], plt.gca().get_proj())
x3, y3 = ax.transData.transform((x2, y2))
#the distance magic from https://stackoverflow.com/questions/10374930/matplotlib-annotating-a-3d-scatter-plot
d=np.sqrt ((x3 - xx)**2 + (y3 - yy)**2)
#We find the distance and also the index for the closest datapoint
if d< dmin:
dmin=d
imin=i
#print ("i=",i," d=",d, " imin=",imin, " dmin=",dmin)
# gives the incorrect data point index
point_index = int(event.ind)
print("Xfixed=",x[imin], " Yfixed=",y[imin], " Zfixed=",z[imin], " PointIdxFixed=", imin)
print("Xbroke=",x[point_index], " Ybroke=",y[point_index], " Zbroke=",z[point_index], " PointIdx=", point_index)
fig.canvas.mpl_connect('pick_event', chaos_onclick)
#fig.canvas.mpl_connect('motion_notify_event', onMouseMotion) # on mouse motion
plt.show()

Related

How to change marker size/scale in legend when marker is set to pixel

I am scatter ploting data points with a very small marker (see screengrab below). When I use the very small marker ',' the legend is very hard to read (example code taken from here).
(Python 3, Jupyter lab)
How can I increase the size of the marker in the legend. The two versions shown on the above mentioned site do not work:
legend = ax.legend(frameon=True)
for legend_handle in legend.legendHandles:
legend_handle._legmarker.set_markersize(9)
and
ax.legend(markerscale=6)
The two solutions do however work when the marker is set to '.'.
How can I show bigger makers in the legend?
Sample Code from intoli.com:
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(12)
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
for i in range(5):
mean = [np.random.random()*10, np.random.random()*10]
covariance = [ [1 + np.random.random(), np.random.random() - 1], [0, 1 + np.random.random()], ]
covariance[1][0] = covariance[0][1] # must be symmetric
x, y = np.random.multivariate_normal(mean, covariance, 3000).T
plt.plot(x, y, ',', label=f'Cluster {i + 1}')
ax.legend(markerscale=12)
fig.tight_layout()
plt.show()
You can get 1 pixel sized markers for a plot by setting the markersize to 1 pixel. This would look like
plt.plot(x, y, marker='s', markersize=72./fig.dpi, mec="None", ls="None")
What the above does is set the marker to a square, set the markersize to the ppi (points per inch) divided by dpi (dots per inch) == dots == pixels, and removes lines and edges.
Then the solution you tried using markerscale in the legend works nicely.
Complete example:
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(12)
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
for i in range(5):
mean = [np.random.random()*10, np.random.random()*10]
covariance = [ [1 + np.random.random(), np.random.random() - 1], [0, 1 + np.random.random()], ]
covariance[1][0] = covariance[0][1] # must be symmetric
x, y = np.random.multivariate_normal(mean, covariance, 3000).T
plt.plot(x, y, marker='s', markersize=72./fig.dpi, mec="None", ls="None",
label=f'Cluster {i + 1}')
ax.legend(markerscale=12)
fig.tight_layout()
plt.show()
According to this discussion, the markersize has no effect when using pixels (,) as marker. How about generating a custom legend instead? For example, by adapting the first example in this tutorial, one can get a pretty decent legend:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
np.random.seed(12)
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
for i in range(5):
mean = [np.random.random()*10, np.random.random()*10]
covariance = [ [1 + np.random.random(), np.random.random() - 1], [0, 1 + np.random.random()], ]
covariance[1][0] = covariance[0][1] # must be symmetric
x, y = np.random.multivariate_normal(mean, covariance, 3000).T
plt.plot(x, y, ',', label=f'Cluster {i + 1}')
##generating custom legend
handles, labels = ax.get_legend_handles_labels()
patches = []
for handle, label in zip(handles, labels):
patches.append(mpatches.Patch(color=handle.get_color(), label=label))
legend = ax.legend(handles=patches)
fig.tight_layout()
plt.show()
The output would look like this:

How to draw vertical lines interactively in matplotlib?

I have a time series plot and I need to draw a moving vertical line to show the point of interest.
I am using the following toy example to accomplish the same. However, it prints all the lines at the same time while I wanted to show these vertical line plotting one at a time.
import time
ion() # turn interactive mode on
# initial data
x = arange(-8, 8, 0.1);
y1 = sin(x)
y2 = cos(x)
line1, = plt.plot(x, y1, 'r')
xvals = range(-6, 6, 2);
for i in xvals:
time.sleep(1)
# update data
plt.vlines(i, -1, 1, linestyles = 'solid', color= 'red')
plt.draw()
If I understood well, you want to use the animation tools of matplotlib. An example (adapted from the doc):
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
X_MIN = -6
X_MAX = 6
Y_MIN = -1
Y_MAX = 1
X_VALS = range(X_MIN, X_MAX+1) # possible x values for the line
def update_line(num, line):
i = X_VALS[num]
line.set_data( [i, i], [Y_MIN, Y_MAX])
return line,
fig = plt.figure()
x = np.arange(X_MIN, X_MAX, 0.1);
y = np.sin(x)
plt.scatter(x, y)
l , v = plt.plot(-6, -1, 6, 1, linewidth=2, color= 'red')
plt.xlim(X_MIN, X_MAX)
plt.ylim(Y_MIN, Y_MAX)
plt.xlabel('x')
plt.ylabel('y = sin(x)')
plt.title('Line animation')
line_anim = animation.FuncAnimation(fig, update_line, len(X_VALS), fargs=(l, ))
#line_anim.save('line_animation.gif', writer='imagemagick', fps=4);
plt.show()
Resulting gif looks like this:
Could you try calling plt.draw after plt.vlines? plt.draw is used to interactively redraw the figure after its been modified.

How to make Matplotlib scatterplots transparent as a group?

I'm making some scatterplots using Matplotlib (python 3.4.0, matplotlib 1.4.3, running on Linux Mint 17). It's easy enough to set alpha transparency for each point individually; is there any way to set them as a group, so that two overlapping points from the same group don't change the color?
Example code:
import matplotlib.pyplot as plt
import numpy as np
def points(n=100):
x = np.random.uniform(size=n)
y = np.random.uniform(size=n)
return x, y
x1, y1 = points()
x2, y2 = points()
fig = plt.figure(figsize=(4,4))
ax = fig.add_subplot(111, title="Test scatter")
ax.scatter(x1, y1, s=100, color="blue", alpha=0.5)
ax.scatter(x2, y2, s=100, color="red", alpha=0.5)
fig.savefig("test_scatter.png")
Results in this output:
but I want something more like this one:
I can workaround by saving as SVG and manually grouping then in Inkscape, then setting transparency, but I'd really prefer something I can code. Any suggestions?
Yes, interesting question. You can get this scatterplot with Shapely. Here is the code :
import matplotlib.pyplot as plt
import matplotlib.patches as ptc
import numpy as np
from shapely.geometry import Point
from shapely.ops import cascaded_union
n = 100
size = 0.02
alpha = 0.5
def points():
x = np.random.uniform(size=n)
y = np.random.uniform(size=n)
return x, y
x1, y1 = points()
x2, y2 = points()
polygons1 = [Point(x1[i], y1[i]).buffer(size) for i in range(n)]
polygons2 = [Point(x2[i], y2[i]).buffer(size) for i in range(n)]
polygons1 = cascaded_union(polygons1)
polygons2 = cascaded_union(polygons2)
fig = plt.figure(figsize=(4,4))
ax = fig.add_subplot(111, title="Test scatter")
for polygon1 in polygons1:
polygon1 = ptc.Polygon(np.array(polygon1.exterior), facecolor="red", lw=0, alpha=alpha)
ax.add_patch(polygon1)
for polygon2 in polygons2:
polygon2 = ptc.Polygon(np.array(polygon2.exterior), facecolor="blue", lw=0, alpha=alpha)
ax.add_patch(polygon2)
ax.axis([-0.2, 1.2, -0.2, 1.2])
fig.savefig("test_scatter.png")
and the result is :
Interesting question, I think any use of transparency will result in the stacking effect you want to avoid. You could manually set a transparency type colour to get closer to the results you want,
import matplotlib.pyplot as plt
import numpy as np
def points(n=100):
x = np.random.uniform(size=n)
y = np.random.uniform(size=n)
return x, y
x1, y1 = points()
x2, y2 = points()
fig = plt.figure(figsize=(4,4))
ax = fig.add_subplot(111, title="Test scatter")
alpha = 0.5
ax.scatter(x1, y1, s=100, lw = 0, color=[1., alpha, alpha])
ax.scatter(x2, y2, s=100, lw = 0, color=[alpha, alpha, 1.])
plt.show()
The overlap between the different colours are not included in this way but you get,
This is a terrible, terrible hack, but it works.
You see while Matplotlib plots data points as separate objects that can overlap, it plots the line between them as a single object - even if that line is broken into several pieces by NaNs in the data.
With that in mind, you can do this:
import numpy as np
from matplotlib import pyplot as plt
plt.rcParams['lines.solid_capstyle'] = 'round'
def expand(x, y, gap=1e-4):
add = np.tile([0, gap, np.nan], len(x))
x1 = np.repeat(x, 3) + add
y1 = np.repeat(y, 3) + add
return x1, y1
x1, y1 = points()
x2, y2 = points()
fig = plt.figure(figsize=(4,4))
ax = fig.add_subplot(111, title="Test scatter")
ax.plot(*expand(x1, y1), lw=20, color="blue", alpha=0.5)
ax.plot(*expand(x2, y2), lw=20, color="red", alpha=0.5)
fig.savefig("test_scatter.png")
plt.show()
And each color will overlap with the other color but not with itself.
One caveat is that you have to be careful with the spacing between the two points you use to make each circle. If they're two far apart then the separation will be visible on your plot, but if they're too close together, matplotlib doesn't plot the line at all. That means that the separation needs to be chosen based on the range of your data, and if you plan to make an interactive plot then there's a risk of all the data points suddenly vanishing if you zoom out too much, and stretching if you zoom in too much.
As you can see, I found 1e-5 to be a good separation for data with a range of [0,1].
Just pass an argument saying edgecolors='none' to plt.scatter()
Here's a hack if you have more than just a few points to plot. I had to plot >500000 points, and the shapely solution does not scale well. I also wanted to plot a different shape other than a circle. I opted to instead plot each layer separately with alpha=1 and then read in the resulting image with np.frombuffer (as described here), then add the alpha to the whole image and plot overlays using plt.imshow. Note this solution forfeits access to the original fig object and attributes, so any other modifications to figure should be made before it's drawn.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas
from matplotlib.figure import Figure
def arr_from_fig(fig):
canvas = FigureCanvas(fig)
canvas.draw()
img = np.frombuffer(fig.canvas.tostring_rgb(), dtype=np.uint8)
img = img.reshape(fig.canvas.get_width_height()[::-1] + (3,))
return img
def points(n=100):
x = np.random.uniform(size=n)
y = np.random.uniform(size=n)
return x, y
x1, y1 = points()
x2, y2 = points()
imgs = list()
figsize = (4, 4)
dpi = 200
for x, y, c in zip([x1, x2], [y1, y2], ['blue', 'red']):
fig = plt.figure(figsize=figsize, dpi=dpi, tight_layout={'pad':0})
ax = fig.add_subplot(111)
ax.scatter(x, y, s=100, color=c, alpha=1)
ax.axis([-0.2, 1.2, -0.2, 1.2])
ax.axis('off')
imgs.append(arr_from_fig(fig))
plt.close()
fig = plt.figure(figsize=figsize)
alpha = 0.5
alpha_scaled = 255*alpha
for img in imgs:
img_alpha = np.where((img == 255).all(-1), 0, alpha_scaled).reshape([*img.shape[:2], 1])
img_show = np.concatenate([img, img_alpha], axis=-1).astype(int)
plt.imshow(img_show, origin='lower')
ticklabels = ['{:03.1f}'.format(i) for i in np.linspace(-0.2, 1.2, 8, dtype=np.float16)]
plt.xticks(ticks=np.linspace(0, dpi*figsize[0], 8), labels=ticklabels)
plt.yticks(ticks=np.linspace(0, dpi*figsize[1], 8), labels=ticklabels);
plt.title('Test scatter');
I encountered the save issue recently, my case is there are too many points very close to each other, like 100 points of alpha 0.3 on top of each other, the alpha of the color in the generated image is almost 1. So instead of setting the alpha value in the cmap or scatter. I save it to a Pillow image and set the alpha channel there. My code:
import io
import os
import numpy as np
import numpy.ma as ma
import matplotlib.pyplot as plt
from matplotlib import colors
from PIL import Image
from dhi_base import DHIBase
class HeatMapPlot(DHIBase):
def __init__(self) -> None:
super().__init__()
# these 4 values are precalculated
top=75
left=95
width=1314
height=924
self.crop_box = (left, top, left+width, top+height)
# alpha 0.5, [0-255]
self.alpha = 128
def get_cmap(self):
v = [
...
]
return colors.LinearSegmentedColormap.from_list(
'water_level', v, 512)
def png3857(self):
"""Generate flooding images
"""
muids = np.load(os.path.join(self.npy_dir, 'myfilename.npy'))
cmap = self.get_cmap()
i = 0
for npyf in os.listdir(self.npy_dir):
if not npyf.startswith('flooding'):
continue
flooding_num = np.load(os.path.join(self.npy_dir, npyf))
image_file = os.path.join(self.img_dir, npyf.replace('npy', 'png'))
# if os.path.isfile(image_file):
# continue
# filter the water level value that is less than 0.001
masked_arr = ma.masked_where(flooding_num > 0.001, flooding_num)
flooding_masked = flooding_num[masked_arr.mask]
muids_masked = muids[masked_arr.mask, :]
plt.figure(figsize=(self.grid2D['numJ'] / 500, self.grid2D['numK'] / 500))
plt.axis('off')
plt.tight_layout()
plt.scatter(muids_masked[:, 0], muids_masked[:, 1], s=0.1, c=flooding_masked,
alpha=1, edgecolors='none', linewidths=0,
cmap=cmap,
vmin=0, vmax=1.5)
img_buf = io.BytesIO()
plt.savefig(img_buf, transparent=True, dpi=200, format='png')#, pad_inches=0)
plt.clf()
plt.close()
img_buf.seek(0)
img = Image.open(img_buf)
# Cropped image of above dimension
# (It will not change original image)
img = img.crop(self.crop_box)
alpha_channel = img.getchannel('A')
# Make all opaque pixels into semi-opaque
alpha_channel = alpha_channel.point(lambda i: self.alpha if i>0 else 0)
img.putalpha(alpha_channel)
img.save(image_file)
self.logger.info("PNG saved to {}".format(image_file))
i += 1
# if i > 15:
# break
if __name__ == "__main__":
hp = HeatMapPlot()
hp.png3857()

How to put circles on top of a polygon?

I use matplotlib to generate an image in the following way:
fig = plt.figure()
ax = fig.add_subplot(111)
ax.fill(border[0],border[1], color='g', linewidth=1, fill=True, alpha = 0.5)
patches = []
for x1,y1,r in zip(x, y, radii):
circle = Circle((x1,y1), r)
patches.append(circle)
p = PatchCollection(patches, cmap='cool', alpha=1.0)
p.set_array(c)
ax.add_collection(p)
plt.colorbar(p)
plt.savefig(fig_name)
What I want to have is a polygon (given by its border) and colored circles on the top of this polygon. However, I get the polygon on the top of the circles.
This is strange because I plot the polygon first and then I add circles to the plot.
Does anybody know why it happens and how this problem can be resolved?
ADDED
As requested, here is fully working example:
import pandas
import matplotlib
import matplotlib.pyplot as plt
from matplotlib.collections import PatchCollection
from matplotlib.patches import Circle, Polygon
import numpy as np
def plot_xyc(df, x_col, y_col, c_col, radius, fig_name, title, zrange):
resolution = 50
x = df[x_col]
y = df[y_col]
c = df[c_col]
x0 = (max(x) + min(x))/2.0
y0 = (max(y) + min(y))/2.0
dx = (max(x) - min(x))
dy = (max(y) - min(y))
delta = max(dx, dy)
radii = [delta*radius for i in range(len(x))]
fig = plt.figure()
plt.title(title)
ax = fig.add_subplot(111)
border = ([-3, 3, 3, -3], [-3, -3, 3, 3])
ax.fill(border[0],border[1], color='g', linewidth=1, fill=True, alpha = 1.0)
patches = []
for x1,y1,r in zip(x, y, radii):
circle = Circle((x1,y1), r)
patches.append(circle)
patches.append(Circle((-100,-100), r))
patches.append(Circle((-100,-100), r))
p = PatchCollection(patches, cmap='cool', alpha=1.0)
p.set_array(c)
max_ind = max(c.index)
c.set_value(max_ind + 1, min(zrange))
c.set_value(max_ind + 2, max(zrange))
plt.xlim([x0 - delta/2.0 - 0.05*delta, x0 + delta/2.0 + 0.05*delta])
plt.ylim([y0 - delta/2.0 - 0.05*delta, y0 + delta/2.0 + 0.05*delta])
ax.add_collection(p)
plt.colorbar(p)
plt.savefig(fig_name)
if __name__ == '__main__':
df = pandas.DataFrame({'x':[1,2,3,4], 'y':[4,3,2,1], 'z':[1,1,2,2]})
plot_xyc(df, 'x', 'y', 'z', 0.1, 'test2.png', 'My Titlle', (0.0, 3.0))
You're looking for zorder.
In matplotlib, all additional arguments are just passed up the class heirarchy. zorder is a kwarg of the Artist class, so you just need to make sure that at some point it gets zorder.
You can do it two ways in your example;
either add it in here:
ax.fill(border[0],border[1], color='g', linewidth=1, fill=True, alpha = 1.0, zorder=1)
or here:
p = PatchCollection(patches, cmap='cool', alpha=1.0, zorder=2)
or if you want, both. Objects with a higher zorder sit on top of those with lower values.

How to make four-way logarithmic plot in Matplotlib?

Four-way logarithmic plot is a very often used graph for vibration control and earthquake protection. I am quite interesting in how this plot can be plotted in Matplotlib instead of adding axes in Inkscape. A sample of Four-way logarithmic plot is here.
A quick and dirty Python code can generate main part of the figure, but I cannot add the two axes onto the figure. http://matplotlib.org/examples/axes_grid/demo_curvelinear_grid.html provides an example of adding axes, but I fails to make it working. Anyone has similar experience on adding axes to Matplotlib figure?
from pylab import *
from mpl_toolkits.axisartist.grid_helper_curvelinear import GridHelperCurveLinear
from mpl_toolkits.axisartist import Subplot
beta=logspace(-1,1,500)
Rd={}
for zeta in [0.01,0.1,0.2,0.7,1]:
Rd[zeta]=beta/sqrt((1-beta*beta)**2+(2*beta*zeta)**2)
loglog(beta,Rd[zeta])
ylim([0.1,10])
xlim([0.1,10])
grid('on',which='minor')
Update: Thank you all! I use Inkscape to modify the figure above. I think the result is just fine. However, I am still looking for methods to draw this figure in Matplotlib.
Here is a partial solution. I am still working on how to do all of this in a natural loglog() plot rather than scaling the data. (To complete this example you would have to define custom tick-lables so that they display 10**x rather than x.)
%matplotlib inline # I am doing this in an IPython notebook.
from matplotlib import pyplot as plt
import numpy as np
from numpy import log10
# Generate the data
beta = np.logspace(-1, 1, 500)[:, None]
zeta = np.array([0.01,0.1,0.2,0.7,1])[None, :]
Rd = beta/np.sqrt((1 - beta*beta)**2 + (2*beta*zeta)**2)
def draw(beta=beta, Rd=Rd):
plt.plot(log10(beta), log10(Rd))
plt.ylim([log10(0.1), log10(10)])
plt.xlim([log10(0.1), log10(10)])
plt.grid('on',which='minor')
ax = plt.gca()
ax.set_aspect(1)
from mpl_toolkits.axisartist import GridHelperCurveLinear
from matplotlib.transforms import Affine2D
from mpl_toolkits.axisartist import SubplotHost
from mpl_toolkits.axisartist import Subplot
#tr = Affine2D().rotate(-np.pi/2)
#inv_tr = Affine2D().rotate(np.pi/2)
class Transform(object):
"""Provides transforms to go to and from rotated grid.
Parameters
----------
ilim : (xmin, xmax, ymin, ymax)
The limits of the displayed axes (in physical units)
olim : (xmin, xmax, ymin, ymax)
The limits of the rotated axes (in physical units)
"""
def __init__(self, ilim, olim):
# Convert each to a 3x3 matrix and compute the transform
# [x1, y1, 1] = A*[x0, y0, 1]
x0, x1, y0, y1 = np.log10(ilim)
I = np.array([[x0, x0, x1],
[y0, y1, y1],
[ 1, 1, 1]])
x0, x1, y0, y1 = np.log10(olim)
x_mid = (x0 + x1)/2
y_mid = (y0 + y1)/2
O = np.array([[ x0, x_mid, x1],
[y_mid, y1, y_mid],
[ 1, 1, 1]])
self.A = np.dot(O, np.linalg.inv(I))
self.Ainv = np.linalg.inv(self.A)
def tr(self, x, y):
"""From "curved" (rotated) coords to rectlinear coords"""
x, y = map(np.asarray, (x, y))
return np.dot(self.A, np.asarray([x, y, 1]))[:2]
def inv_tr(self, x, y):
"""From rectlinear coords to "curved" (rotated) coords"""
x, y = map(np.asarray, (x, y))
return np.dot(self.Ainv, np.asarray([x, y, 1]))[:2]
ilim = (0.1, 10)
olim = (0.01, 100)
tr = Transform(ilim + ilim, olim + olim)
grid_helper = GridHelperCurveLinear((tr.tr, tr.inv_tr))
fig = plt.gcf()
ax0 = Subplot(fig, 1, 1, 1)
ax1 = Subplot(fig, 1, 1, 1, grid_helper=grid_helper, frameon=False)
ax1.set_xlim(*np.log10(olim))
ax1.set_ylim(*np.log10(olim))
ax1.axis["left"] = ax1.new_floating_axis(0, 0.)
ax1.axis["bottom"] = ax1.new_floating_axis(1, 0.0)
fig.add_subplot(ax0)
fig.add_subplot(ax1)
ax0.grid('on', which='both')
ax1.grid('on', which='both')
plt.plot(log10(beta), log10(Rd))
plt.ylim(np.log10(ilim))
plt.xlim(np.log10(ilim))
This seems to be a bit tricker than it should. There are ways to center the spines (axis lines), and ways to rotate them, but those do not work together. Adding a normal axis on a line (a la mpl demos) results in a curved axis (because it is logarithmic). Here is a [poor] example of how to draw -- as in, like you would with Inkscape something to look like an additional pair of axis spines with the example data.
import matplotlib.pyplot as plt
import numpy as np
#data
b = np.logspace(-1, 1, 500)
Rd = {}
for zeta in [0.01, 0.1, 0.2, 0.7, 1]:
Rd[zeta] = b / np.sqrt((1 - b * b) ** 2 + (2 * b * zeta) ** 2)
#plot
fig = plt.figure()
ax1 = fig.add_subplot(111)
for z in Rd:
ax1.loglog(b, Rd[z])
ax1.set_xlim([0.1, 10])
ax1.set_ylim([0.1, 10])
ax1.set_aspect(1.)
#draw lines to look like diagonal spines (axes)
xmin, xmax = ax1.get_xlim() # xlim == ylim
a = np.log10(xmin)
b = np.log10(xmax)
span = b - a
period_points = 3 # number of points/ticks per decade
npts = (span * period_points) + 1 # +1 for even powers of 10
x1 = np.logspace(a, b, num=npts)
x2 = np.logspace(b, a, num=npts)
ax1.plot(x1, x1, color='k', marker='x', ms='9')
ax1.plot(x1, x2, color='k', marker='x', ms='9')
#NOTE: v1.2.1 lacks 'TICKUP' and similar - these may be
# a better choice in v1.3x and beyond
ax1.text(0.97, 0.9,
"axis label: A",
size='large',
horizontalalignment='right',
verticalalignment='top',
rotation=45,
transform=ax1.transAxes,
#bbox={'facecolor': 'white', 'alpha': 0.5, 'pad': 10},
)
ax1.text(0.03, 0.9,
"axis label: B",
size='large',
horizontalalignment='left',
verticalalignment='top',
rotation=-45,
transform=ax1.transAxes,
#bbox={'facecolor': 'white', 'alpha': 0.5, 'pad': 10},
)
plt.savefig("example.pdf")

Categories