I want to generate an azimuth-elevation plot depicting the motion of a body over a range of dates. In the example figure below, I made a polar plot of the data using standard matplotlib calls. However, I want to programmatically add tic marks and text labels to some of the points to annotate the date associated with the data. These were added to the figure below using Gimp.
Ideally, I'd like for the tic marks to be drawn normal to the curve location where they're plotted but I could live with having them vertical or horizontal if I can have them with an existing matplotlib function call. However, I haven't been able to find a set of functions that do this. Are there such things or do I need to write my own?
from matplotlib.pyplot import *
azel = [(0.13464431952125472,294.0475121469728,41761.31282856121),
(1.0847050101323694, 294.07215546949817, 41762.31339111264),
(2.0568066678342625, 294.08166309282285, 41763.3139526239),
(3.0241776724839813, 294.07688196040385, 41764.3145128838),
(3.9693016837899107, 294.05876829872494, 41765.315071676574),
(4.880729810599228, 294.0283602965401, 41766.315628782446),
(5.750685455655052, 293.98677810487305, 41767.31618397785),
(6.573500415719916, 293.93516919550444, 41768.316737037436),
(7.344961744736395, 293.8748176439982, 41769.31728773474),
(8.061838763227069, 293.80692556364824, 41770.317835842376),
(8.7216239379929, 293.732913633802, 41771.31838113272),
(9.322354443421153, 293.65412057153674, 41772.31892337514),
(9.862485802985763, 293.57204901846984, 41773.319462333326),
(10.340798827919878, 293.48820161621876, 41774.31999776034),
(10.756318508719481, 293.40413564791425, 41775.32052939467),
(11.108256812309081, 293.3215176797138, 41776.32105695697),
(11.395961455622944, 293.2420689192882, 41777.321580147836),
(11.618873216922772, 293.16759253657835, 41778.32209864656),
(11.776501176361972, 293.09997366379525, 41779.322612110234),
(11.868395395228971, 293.04117939541976, 41780.32312017416),
(11.894134963116164, 292.9932041466896, 41781.32362245329),
(11.853317752636167, 292.95820625738247, 41782.324118542434),
(11.745559565648168, 292.9383167465194, 41783.32460801967),
(11.57050010967345, 292.9358305576615, 41784.325090448285),
(11.327811535631849, 292.95306995512664, 41785.32556538106),
(11.01721978218292, 292.9923298824759, 41786.32603236289),
(10.638530188935318, 293.0560692078104, 41787.32649093496),
(10.191665062487234, 293.1466921577181, 41788.32694063783),
(9.676711487750586, 293.26663027954356, 41789.32738101277),
(9.093982799653546, 293.4182877998745, 41790.32781160377),
(8.444096276542357, 293.6040416245422, 41791.328231959254),
(7.728075593018872, 293.82621401786434, 41792.328641632426),
(6.947485289289849, 294.08704528188883, 41793.32904018317),
(6.104631834860636, 294.3886391148799, 41794.329427178716),
(5.202865010632301, 294.7329352905619, 41795.3298021964),
(4.247126458469349, 295.12173697887573, 41796.3301648264),
(3.2448043086196177, 295.55651950068204, 41797.3305146729),
(2.2076748744292662, 296.0385122900315, 41798.3308513581),
(1.1552935211005704, 296.56867157340787, 41799.33117452266),
(0.12014145335536401, 297.1474344829181, 41800.33148382535)]
fig = figure()
ax = fig.add_subplot(1,1,1,projection='polar')
ax.plot([az for el,az,_ in azel], [el for el,az,_ in azel])
ylim([0,25])
show()
I'm going to assume that you know which tick marks you want to annotate. The tricky part is telling if the text should be to the left or right of the point to be annotated. If you are interested in a solution to that part, let me know, I'll assume that's not the main point here. Insert the following before the show()
ax.plot(azel[20][1],azel[20][0], marker=(2,0,0), mew=3, markersize=7)
ax.annotate('Nav 20', xy=(azel[20][1], azel[20][0]), xytext=(azel[20][1], azel[20][0]+2), horizontalalignment='left', verticalalignment='top')
ax.plot(azel[10][1],azel[10][0], marker=(2,0,20), mew=3, markersize=7)
ax.annotate('Nav 10', xy=(azel[10][1], azel[10][0]), xytext=(azel[20][1], azel[20][0]+2), horizontalalignment='left', verticalalignment='top')
The parameters for marker are (number of polygon sides, 0 for polygon, angle in degrees), I chose the number of sides to be two to make a tick mark. mew controls the thickness of the walls of the mark, and markersize controls the size of the polygon (in this case the length).
Calculating angle from the differential to place the markers tangent to the curve. You could probably use the them to place the text more intelligently also. Borrowed some code of the previous answer from Troy Rockwood
def getAngles(theta, r):
x = r * np.cos(theta)
y = r * np.sin(theta)
dx = np.diff(x)
dy = np.diff(y)
normalY = -dx
normalX = dy
return np.degrees(np.arctan(normalY/normalX)) + 90
azel = np.asarray(azel)
theta = azel[:,1]
r = azel[:,0]
angles = getAngles(theta, r)
markers_on = [5,10,15,20] #indicies of azel to mark.
fig = figure()
ax = fig.add_subplot(1,1,1,projection='polar')
ax.plot(theta, r)
for i in markers_on:
ax.plot(theta[i], r[i], marker=(2,0,angles[i-1]), mew=3, markersize=7)
ax.annotate('Point: %i'%i, xy=(theta[i], r[i]), xytext=(theta[i], r[i]+2), horizontalalignment='left', verticalalignment='top')
ylim([0,25])
show()
Related
I want to fill a bunch of polygons with line hatch. The lines must have a specific angle with respect to x-axis. I found that matplotlib already suppots some hatch classes and one can define a custom class (like How to fill a polygon with a custom hatch in matplotlib?). I tried to generate a custom hatch but when I append it to the list of hatches the init function doesn't know the angle. I tried with the following class:
class AngularHatch(HatchPatternBase):
def __init__(self, hatch, density, angle):
self.num_lines = int((hatch.count('{'))*density*3)
self.num_vertices = self.num_lines * 2
self.R = np.array([[np.cos(angle), -np.sin(angle)],
[np.sin(angle), np.cos(angle)]])
def set_vertices_and_codes(self, vertices, codes):
steps, stepsize = np.linspace(0.0, 1.0, self.num_lines, False,
retstep=True)
steps += stepsize / 2.
vertices[0::2, 0] = 0
vertices[0::2, 1] = steps
vertices[1::2, 0] = 1
vertices[1::2, 1] = steps
for i, v in enumerate(vertices):
vertices[i] = self.R.dot(v)
codes[0::2] = Path.MOVETO
codes[1::2] = Path.LINETO
Then I add this class to the list of available classes for hatching. However this will not generate the correct lines since the code is modified from the HorizontalHatch source code here and I think this generates lines in the unit square. Moreover I need to generate this patch for a specific angle for each polygon I want to render. ¿Any ideas on how to give the correct angle to this class per polygon?
The following does not solve this issue. It just solves part of the problem and shows at which point the approach fails. I am currently convinced that hatching with arbitrary angles is not possible with matplotlib, because the size of the unit cell is fixed.
To overcome the problem of setting the angle, one may define a custom format from which to take the angle information. E.g. "{angle}{factor}", such that "{45}{2}" would produce a hatching with an angle of 45° and a density factor of 2.
I then do not completely understand the attempt of calculating the vertices. To replicate the behaviour of the hatches which are built-in, one may rotate them directly.
The problem is that this way the line hatches work only for angles of 45°. This is because the lines at the edges of the unit cell do not align well. See the following:
import numpy as np
import matplotlib.hatch
import matplotlib.path
import matplotlib.pyplot as plt
from matplotlib.patches import Ellipse, Rectangle
class AngularHatch(matplotlib.hatch.HatchPatternBase):
def __init__(self, hatch, density):
self.num_lines=0
self.num_vertices=0
if hatch[0] == "{":
h = hatch.strip("{}").split("}{")
angle = np.deg2rad(float(h[0])-45)
d = float(h[1])
self.num_lines = int(density*d)
self.num_vertices = (self.num_lines + 1) * 2
self.R = np.array([[np.cos(angle), -np.sin(angle)],
[np.sin(angle), np.cos(angle)]])
def set_vertices_and_codes(self, vertices, codes):
steps = np.linspace(-0.5, 0.5, self.num_lines + 1, True)
vertices[0::2, 0] = 0.0 + steps
vertices[0::2, 1] = 0.0 - steps
vertices[1::2, 0] = 1.0 + steps
vertices[1::2, 1] = 1.0 - steps
codes[0::2] = matplotlib.path.Path.MOVETO
codes[1::2] = matplotlib.path.Path.LINETO
vertices[:,:] = np.dot((vertices-0.5),self.R)+0.5
matplotlib.hatch._hatch_types.append(AngularHatch)
fig = plt.figure()
ax = fig.add_subplot(111)
ellipse = ax.add_patch(Rectangle((0.1, 0.1), 0.4, 0.8, fill=False))
ellipse.set_hatch('{45}{1}')
ellipse.set_color('red')
ellipse = ax.add_patch(Rectangle((0.55, 0.1), 0.4, 0.8, fill=False))
ellipse.set_hatch('{22}{1}')
ellipse.set_color('blue')
plt.show()
I would like to plot parallel lines with different colors. E.g. rather than a single red line of thickness 6, I would like to have two parallel lines of thickness 3, with one red and one blue.
Any thoughts would be appreciated.
Merci
Even with the smart offsetting (s. below), there is still an issue in a view that has sharp angles between consecutive points.
Zoomed view of smart offsetting:
Overlaying lines of varying thickness:
Plotting parallel lines is not an easy task. Using a simple uniform offset will of course not show the desired result. This is shown in the left picture below.
Such a simple offset can be produced in matplotlib as shown in the transformation tutorial.
Method1
A better solution may be to use the idea sketched on the right side. To calculate the offset of the nth point we can use the normal vector to the line between the n-1st and the n+1st point and use the same distance along this normal vector to calculate the offset point.
The advantage of this method is that we have the same number of points in the original line as in the offset line. The disadvantage is that it is not completely accurate, as can be see in the picture.
This method is implemented in the function offset in the code below.
In order to make this useful for a matplotlib plot, we need to consider that the linewidth should be independent of the data units. Linewidth is usually given in units of points, and the offset would best be given in the same unit, such that e.g. the requirement from the question ("two parallel lines of width 3") can be met.
The idea is therefore to transform the coordinates from data to display coordinates, using ax.transData.transform. Also the offset in points o can be transformed to the same units: Using the dpi and the standard of ppi=72, the offset in display coordinates is o*dpi/ppi. After the offset in display coordinates has been applied, the inverse transform (ax.transData.inverted().transform) allows a backtransformation.
Now there is another dimension of the problem: How to assure that the offset remains the same independent of the zoom and size of the figure?
This last point can be addressed by recalculating the offset each time a zooming of resizing event has taken place.
Here is how a rainbow curve would look like produced by this method.
And here is the code to produce the image.
import numpy as np
import matplotlib.pyplot as plt
dpi = 100
def offset(x,y, o):
""" Offset coordinates given by array x,y by o """
X = np.c_[x,y].T
m = np.array([[0,-1],[1,0]])
R = np.zeros_like(X)
S = X[:,2:]-X[:,:-2]
R[:,1:-1] = np.dot(m, S)
R[:,0] = np.dot(m, X[:,1]-X[:,0])
R[:,-1] = np.dot(m, X[:,-1]-X[:,-2])
On = R/np.sqrt(R[0,:]**2+R[1,:]**2)*o
Out = On+X
return Out[0,:], Out[1,:]
def offset_curve(ax, x,y, o):
""" Offset array x,y in data coordinates
by o in points """
trans = ax.transData.transform
inv = ax.transData.inverted().transform
X = np.c_[x,y]
Xt = trans(X)
xto, yto = offset(Xt[:,0],Xt[:,1],o*dpi/72. )
Xto = np.c_[xto, yto]
Xo = inv(Xto)
return Xo[:,0], Xo[:,1]
# some single points
y = np.array([1,2,2,3,3,0])
x = np.arange(len(y))
#or try a sinus
x = np.linspace(0,9)
y=np.sin(x)*x/3.
fig, ax=plt.subplots(figsize=(4,2.5), dpi=dpi)
cols = ["#fff40b", "#00e103", "#ff9921", "#3a00ef", "#ff2121", "#af00e7"]
lw = 2.
lines = []
for i in range(len(cols)):
l, = plt.plot(x,y, lw=lw, color=cols[i])
lines.append(l)
def plot_rainbow(event=None):
xr = range(6); yr = range(6);
xr[0],yr[0] = offset_curve(ax, x,y, lw/2.)
xr[1],yr[1] = offset_curve(ax, x,y, -lw/2.)
xr[2],yr[2] = offset_curve(ax, xr[0],yr[0], lw)
xr[3],yr[3] = offset_curve(ax, xr[1],yr[1], -lw)
xr[4],yr[4] = offset_curve(ax, xr[2],yr[2], lw)
xr[5],yr[5] = offset_curve(ax, xr[3],yr[3], -lw)
for i in range(6):
lines[i].set_data(xr[i], yr[i])
plot_rainbow()
fig.canvas.mpl_connect("resize_event", plot_rainbow)
fig.canvas.mpl_connect("button_release_event", plot_rainbow)
plt.savefig(__file__+".png", dpi=dpi)
plt.show()
Method2
To avoid overlapping lines, one has to use a more complicated solution.
One could first offset every point normal to the two line segments it is part of (green points in the picture below). Then calculate the line through those offset points and find their intersection.
A particular case would be when the slopes of two subsequent line segments equal. This has to be taken care of (eps in the code below).
from __future__ import division
import numpy as np
import matplotlib.pyplot as plt
dpi = 100
def intersect(p1, p2, q1, q2, eps=1.e-10):
""" given two lines, first through points pn, second through qn,
find the intersection """
x1 = p1[0]; y1 = p1[1]; x2 = p2[0]; y2 = p2[1]
x3 = q1[0]; y3 = q1[1]; x4 = q2[0]; y4 = q2[1]
nomX = ((x1*y2-y1*x2)*(x3-x4)- (x1-x2)*(x3*y4-y3*x4))
denom = float( (x1-x2)*(y3-y4) - (y1-y2)*(x3-x4) )
nomY = (x1*y2-y1*x2)*(y3-y4) - (y1-y2)*(x3*y4-y3*x4)
if np.abs(denom) < eps:
#print "intersection undefined", p1
return np.array( p1 )
else:
return np.array( [ nomX/denom , nomY/denom ])
def offset(x,y, o, eps=1.e-10):
""" Offset coordinates given by array x,y by o """
X = np.c_[x,y].T
m = np.array([[0,-1],[1,0]])
S = X[:,1:]-X[:,:-1]
R = np.dot(m, S)
norm = np.sqrt(R[0,:]**2+R[1,:]**2) / o
On = R/norm
Outa = On+X[:,1:]
Outb = On+X[:,:-1]
G = np.zeros_like(X)
for i in xrange(0, len(X[0,:])-2):
p = intersect(Outa[:,i], Outb[:,i], Outa[:,i+1], Outb[:,i+1], eps=eps)
G[:,i+1] = p
G[:,0] = Outb[:,0]
G[:,-1] = Outa[:,-1]
return G[0,:], G[1,:]
def offset_curve(ax, x,y, o, eps=1.e-10):
""" Offset array x,y in data coordinates
by o in points """
trans = ax.transData.transform
inv = ax.transData.inverted().transform
X = np.c_[x,y]
Xt = trans(X)
xto, yto = offset(Xt[:,0],Xt[:,1],o*dpi/72., eps=eps )
Xto = np.c_[xto, yto]
Xo = inv(Xto)
return Xo[:,0], Xo[:,1]
# some single points
y = np.array([1,1,2,0,3,2,1.,4,3]) *1.e9
x = np.arange(len(y))
x[3]=x[4]
#or try a sinus
#x = np.linspace(0,9)
#y=np.sin(x)*x/3.
fig, ax=plt.subplots(figsize=(4,2.5), dpi=dpi)
cols = ["r", "b"]
lw = 11.
lines = []
for i in range(len(cols)):
l, = plt.plot(x,y, lw=lw, color=cols[i], solid_joinstyle="miter")
lines.append(l)
def plot_rainbow(event=None):
xr = range(2); yr = range(2);
xr[0],yr[0] = offset_curve(ax, x,y, lw/2.)
xr[1],yr[1] = offset_curve(ax, x,y, -lw/2.)
for i in range(2):
lines[i].set_data(xr[i], yr[i])
plot_rainbow()
fig.canvas.mpl_connect("resize_event", plot_rainbow)
fig.canvas.mpl_connect("button_release_event", plot_rainbow)
plt.show()
Note that this method should work well as long as the offset between the lines is smaller then the distance between subsequent points on the line. Otherwise method 1 may be better suited.
The best that I can think of is to take your data, generate a series of small offsets, and use fill_between to make bands of whatever color you like.
I wrote a function to do this. I don't know what shape you're trying to plot, so this may or may not work for you. I tested it on a parabola and got decent results. You can also play around with the list of colors.
def rainbow_plot(x, y, spacing=0.1):
fig, ax = plt.subplots()
colors = ['red', 'yellow', 'green', 'cyan','blue']
top = max(y)
lines = []
for i in range(len(colors)+1):
newline_data = y - top*spacing*i
lines.append(newline_data)
for i, c in enumerate(colors):
ax.fill_between(x, lines[i], lines[i+1], facecolor=c)
return fig, ax
x = np.linspace(0,1,51)
y = 1-(x-0.5)**2
rainbow_plot(x,y)
I am trying to overlay a contour plot on an astronomical image. The following code shows how I generate my contours:
print "contour map "
ny, nx = 50, 50
level=np.array([0.683,0.866,0.954,0.990, 0.997])
print "limits:"
print
print level
print
bins_x=np.linspace(min(xp),max(xp),nx)
bins_y=np.linspace(min(yp),max(yp),ny)
H, yedges, xedges = np.histogram2d(xp, yp, (bins_x,bins_y),weights=zp)
smooth=0.8
Hsmooth = scipy.ndimage.filters.gaussian_filter(H.T, smooth)
xcenters = (xedges[1:] + xedges[:-1])/2.
ycenters = (yedges[1:] + yedges[:-1])/2.
Xgrid, Ygrid = np.meshgrid(ycenters, xcenters)
extent = [xedges[0], xedges[-1], yedges[0], yedges[-1] ]
print len(ra),len(Gcl_1_ra),len(Gcl_2_ra)
# Now we add contours
CS = plt.contour(Xgrid, Ygrid, Hsmooth, levels=level, extent=extent, linewidths=0.4, cmap=cm.Pastel1)
print CS.levels
plt.clabel(CS, CS.levels, colors='red',inline=True, inline_spacing=0.02,fontsize=7, fmt="%0.3f")
print "label coordinate :"
print CS.cl_xy
plt.show()
running the code gives me information about were contour labels are placed:
label coordinate :
[(479.11978392445798, 152.0), (183.33333333333337, 234.5705217606079), (394.86796408013157, 336.0), (462.33333333333337, 156.69957238363236), (183.33333333333337, 232.80998335706244), (399.34062255451977, 296.0), (462.33333333333337, 155.83083286816793), (183.33333333333337, 231.97448760480535), (402.06057288711821, 320.00000000000006), (452.00000000000006, 152.37778562776006), (183.33333333333337, 231.73316562697329), (399.50827125157235, 328.0), (452.00000000000006, 152.25467967915702), (183.33333333333337, 231.68624190906149), (399.44091390280244, 328.0)]
My questions are:
Since all the time the contour's labels are overlap with each other, how could I displace the labels in order that they don't get mixed up?
I would like to change the labels for clabel to label=[r'1$\sigma$',r'1.5$\sigma$',r'2$\sigma$',r'2.6$\sigma$',r'3$\sigma$']. How could I do that?
Thanks in advance.
For 1), you can pass a set of x,y values to clabel, where you would like the labels placed with
manual=[(x1,y1),(x2,y2)...
For 2), you can pass fmt as a function to be called with each numerical value, which returns a string for labelling; or a dictionary. For you, it could be
fmt={0.683:r'1$sigma$', 0.866:r'1.5$sigma$', 0.954:r'2$sigma$',
0.990:r'2.6$sigma$', 0.997:r'3$sigma$'}
In a project I'm doing, I have to take in a user input from a structured file (xml). The file contains road data of an area, which I have to plot on to the matplotlib canvas. The problem is that along with the road, I also have to render the road name, and most of the roads are curved. I know how to render text in an angle. But I was wondering whether it is possible to change the text angle midway through the string?
Something like this : Draw rotated text on curved path
But using matplotlib.
Here is my take on the problem:
In order to make the text robust to figure adjustments after drawing, I derive a child class, CurvedText, from matplotlib.text. The CurvedText object takes a string and a curve in the form of x- and y-value arrays. The text to be displayed itself is cut into separate characters, which each are added to the plot at the appropriate position. As matplotlib.text draws nothing if the string is empty, I replace all spaces by invisible 'a's. Upon figure adjustment, the overloaded draw() calls the update_positions() function, which takes care that the character positions and orientations stay correct. To assure the calling order (each character's draw() function will be called as well) the CurvedText object also takes care that the zorder of each character is higher than its own zorder. Following my example here, the text can have any alignment. If the text cannot be fit to the curve at the current resolution, the rest will be hidden, but will appear upon resizing. Below is the code with an example of application.
from matplotlib import pyplot as plt
from matplotlib import patches
from matplotlib import text as mtext
import numpy as np
import math
class CurvedText(mtext.Text):
"""
A text object that follows an arbitrary curve.
"""
def __init__(self, x, y, text, axes, **kwargs):
super(CurvedText, self).__init__(x[0],y[0],' ', **kwargs)
axes.add_artist(self)
##saving the curve:
self.__x = x
self.__y = y
self.__zorder = self.get_zorder()
##creating the text objects
self.__Characters = []
for c in text:
if c == ' ':
##make this an invisible 'a':
t = mtext.Text(0,0,'a')
t.set_alpha(0.0)
else:
t = mtext.Text(0,0,c, **kwargs)
#resetting unnecessary arguments
t.set_ha('center')
t.set_rotation(0)
t.set_zorder(self.__zorder +1)
self.__Characters.append((c,t))
axes.add_artist(t)
##overloading some member functions, to assure correct functionality
##on update
def set_zorder(self, zorder):
super(CurvedText, self).set_zorder(zorder)
self.__zorder = self.get_zorder()
for c,t in self.__Characters:
t.set_zorder(self.__zorder+1)
def draw(self, renderer, *args, **kwargs):
"""
Overload of the Text.draw() function. Do not do
do any drawing, but update the positions and rotation
angles of self.__Characters.
"""
self.update_positions(renderer)
def update_positions(self,renderer):
"""
Update positions and rotations of the individual text elements.
"""
#preparations
##determining the aspect ratio:
##from https://stackoverflow.com/a/42014041/2454357
##data limits
xlim = self.axes.get_xlim()
ylim = self.axes.get_ylim()
## Axis size on figure
figW, figH = self.axes.get_figure().get_size_inches()
## Ratio of display units
_, _, w, h = self.axes.get_position().bounds
##final aspect ratio
aspect = ((figW * w)/(figH * h))*(ylim[1]-ylim[0])/(xlim[1]-xlim[0])
#points of the curve in figure coordinates:
x_fig,y_fig = (
np.array(l) for l in zip(*self.axes.transData.transform([
(i,j) for i,j in zip(self.__x,self.__y)
]))
)
#point distances in figure coordinates
x_fig_dist = (x_fig[1:]-x_fig[:-1])
y_fig_dist = (y_fig[1:]-y_fig[:-1])
r_fig_dist = np.sqrt(x_fig_dist**2+y_fig_dist**2)
#arc length in figure coordinates
l_fig = np.insert(np.cumsum(r_fig_dist),0,0)
#angles in figure coordinates
rads = np.arctan2((y_fig[1:] - y_fig[:-1]),(x_fig[1:] - x_fig[:-1]))
degs = np.rad2deg(rads)
rel_pos = 10
for c,t in self.__Characters:
#finding the width of c:
t.set_rotation(0)
t.set_va('center')
bbox1 = t.get_window_extent(renderer=renderer)
w = bbox1.width
h = bbox1.height
#ignore all letters that don't fit:
if rel_pos+w/2 > l_fig[-1]:
t.set_alpha(0.0)
rel_pos += w
continue
elif c != ' ':
t.set_alpha(1.0)
#finding the two data points between which the horizontal
#center point of the character will be situated
#left and right indices:
il = np.where(rel_pos+w/2 >= l_fig)[0][-1]
ir = np.where(rel_pos+w/2 <= l_fig)[0][0]
#if we exactly hit a data point:
if ir == il:
ir += 1
#how much of the letter width was needed to find il:
used = l_fig[il]-rel_pos
rel_pos = l_fig[il]
#relative distance between il and ir where the center
#of the character will be
fraction = (w/2-used)/r_fig_dist[il]
##setting the character position in data coordinates:
##interpolate between the two points:
x = self.__x[il]+fraction*(self.__x[ir]-self.__x[il])
y = self.__y[il]+fraction*(self.__y[ir]-self.__y[il])
#getting the offset when setting correct vertical alignment
#in data coordinates
t.set_va(self.get_va())
bbox2 = t.get_window_extent(renderer=renderer)
bbox1d = self.axes.transData.inverted().transform(bbox1)
bbox2d = self.axes.transData.inverted().transform(bbox2)
dr = np.array(bbox2d[0]-bbox1d[0])
#the rotation/stretch matrix
rad = rads[il]
rot_mat = np.array([
[math.cos(rad), math.sin(rad)*aspect],
[-math.sin(rad)/aspect, math.cos(rad)]
])
##computing the offset vector of the rotated character
drp = np.dot(dr,rot_mat)
#setting final position and rotation:
t.set_position(np.array([x,y])+drp)
t.set_rotation(degs[il])
t.set_va('center')
t.set_ha('center')
#updating rel_pos to right edge of character
rel_pos += w-used
if __name__ == '__main__':
Figure, Axes = plt.subplots(2,2, figsize=(7,7), dpi=100)
N = 100
curves = [
[
np.linspace(0,1,N),
np.linspace(0,1,N),
],
[
np.linspace(0,2*np.pi,N),
np.sin(np.linspace(0,2*np.pi,N)),
],
[
-np.cos(np.linspace(0,2*np.pi,N)),
np.sin(np.linspace(0,2*np.pi,N)),
],
[
np.cos(np.linspace(0,2*np.pi,N)),
np.sin(np.linspace(0,2*np.pi,N)),
],
]
texts = [
'straight lines work the same as rotated text',
'wavy curves work well on the convex side',
'you even can annotate parametric curves',
'changing the plotting direction also changes text orientation',
]
for ax, curve, text in zip(Axes.reshape(-1), curves, texts):
#plotting the curve
ax.plot(*curve, color='b')
#adjusting plot limits
stretch = 0.2
xlim = ax.get_xlim()
w = xlim[1] - xlim[0]
ax.set_xlim([xlim[0]-stretch*w, xlim[1]+stretch*w])
ylim = ax.get_ylim()
h = ylim[1] - ylim[0]
ax.set_ylim([ylim[0]-stretch*h, ylim[1]+stretch*h])
#adding the text
text = CurvedText(
x = curve[0],
y = curve[1],
text=text,#'this this is a very, very long text',
va = 'bottom',
axes = ax, ##calls ax.add_artist in __init__
)
plt.show()
The result looks like this:
There are still some problems, when the text follows the concave side of a sharply bending curve. This is because the characters are 'stitched together' along the curve without accounting for overlap. If I have time, I'll try to improve on that. Any comments are very welcome.
Tested on python 3.5 and 2.7
I found your problem quite interesting, so I made something which comes pretty close using the matplotlib text tool:
from __future__ import division
import itertools
import matplotlib.pyplot as plt
import numpy as np
%matplotlib inline
# define figure and axes properties
fig, ax = plt.subplots(figsize=(8,6))
ax.set_xlim(left=0, right=10)
ax.set_ylim(bottom=-1.5, top=1.5)
(xmin, xmax), (ymin, ymax) = ax.get_xlim(), ax.get_ylim()
# calculate a shape factor, more explanation on usage further
# it is a representation of the distortion of the actual image compared to a
# cartesian space:
fshape = abs(fig.get_figwidth()*(xmax - xmin)/(ymax - ymin)/fig.get_figheight())
# the text you want to plot along your line
thetext = 'the text is flowing '
# generate a cycler, so that the string is cycled through
lettercycler = itertools.cycle(tuple(thetext))
# generate dummy river coordinates
xvals = np.linspace(1, 10, 300)
yvals = np.sin(xvals)**3
# every XX datapoints, a character is printed
markerevery = 10
# calculate the rotation angle for the labels (in degrees)
# the angle is calculated as the slope between two datapoints.
# it is then multiplied by a shape factor to get from the angles in a
# cartesian space to the angles in this figure
# first calculate the slope between two consecutive points, multiply with the
# shape factor, get the angle in radians with the arctangens functions, and
# convert to degrees
angles = np.rad2deg(np.arctan((yvals[1:]-yvals[:-1])/(xvals[1:]-xvals[:-1])*fshape))
# plot the 'river'
ax.plot(xvals, yvals, 'b', linewidth=3)
# loop over the data points, but only plot a character every XX steps
for counter in np.arange(0, len(xvals)-1, step=markerevery):
# plot the character in between two datapoints
xcoord = (xvals[counter] + xvals[counter+1])/2.
ycoord = (yvals[counter] + yvals[counter+1])/2.
# plot using the text method, set the rotation so it follows the line,
# aling in the center for a nicer look, optionally, a box can be drawn
# around the letter
ax.text(xcoord, ycoord, lettercycler.next(),
fontsize=25, rotation=angles[counter],
horizontalalignment='center', verticalalignment='center',
bbox=dict(facecolor='white', edgecolor='white', alpha=0.5))
The implementation is far from perfect, but it is a good starting point in my opinion.
Further, it seems that there is some development in matplotlib on having a scatterplot with rotation of the markers, which would be ideal for this case. However, my programming skills are nearly not as hardcore as they need to be to tackle this issue, so I cannot help here.
matplotlib on github: pull request
matplotlib on github: issue
I have about 50,000 data points in 3D on which I have run scipy.spatial.Delaunay from the new scipy (I'm using 0.10) which gives me a very useful triangulation.
Based on: http://en.wikipedia.org/wiki/Delaunay_triangulation (section "Relationship with the Voronoi diagram")
...I was wondering if there is an easy way to get to the "dual graph" of this triangulation, which is the Voronoi Tesselation.
Any clues? My searching around on this seems to show no pre-built in scipy functions, which I find almost strange!
Thanks,
Edward
The adjacency information can be found in the neighbors attribute of the Delaunay object. Unfortunately, the code does not expose the circumcenters to the user at the moment, so you'll have to recompute those yourself.
Also, the Voronoi edges that extend to infinity are not directly obtained in this way. It's still probably possible, but needs some more thinking.
import numpy as np
from scipy.spatial import Delaunay
points = np.random.rand(30, 2)
tri = Delaunay(points)
p = tri.points[tri.vertices]
# Triangle vertices
A = p[:,0,:].T
B = p[:,1,:].T
C = p[:,2,:].T
# See http://en.wikipedia.org/wiki/Circumscribed_circle#Circumscribed_circles_of_triangles
# The following is just a direct transcription of the formula there
a = A - C
b = B - C
def dot2(u, v):
return u[0]*v[0] + u[1]*v[1]
def cross2(u, v, w):
"""u x (v x w)"""
return dot2(u, w)*v - dot2(u, v)*w
def ncross2(u, v):
"""|| u x v ||^2"""
return sq2(u)*sq2(v) - dot2(u, v)**2
def sq2(u):
return dot2(u, u)
cc = cross2(sq2(a) * b - sq2(b) * a, a, b) / (2*ncross2(a, b)) + C
# Grab the Voronoi edges
vc = cc[:,tri.neighbors]
vc[:,tri.neighbors == -1] = np.nan # edges at infinity, plotting those would need more work...
lines = []
lines.extend(zip(cc.T, vc[:,:,0].T))
lines.extend(zip(cc.T, vc[:,:,1].T))
lines.extend(zip(cc.T, vc[:,:,2].T))
# Plot it
import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection
lines = LineCollection(lines, edgecolor='k')
plt.hold(1)
plt.plot(points[:,0], points[:,1], '.')
plt.plot(cc[0], cc[1], '*')
plt.gca().add_collection(lines)
plt.axis('equal')
plt.xlim(-0.1, 1.1)
plt.ylim(-0.1, 1.1)
plt.show()
As I spent a considerable amount of time on this, I'd like to share my solution on how to get the Voronoi polygons instead of just the edges.
The code is at https://gist.github.com/letmaik/8803860 and extends on the solution of tauran.
First, I changed the code to give me vertices and (pairs of) indices (=edges) separately, as many calculations can be simplified when working on indices instead of point coordinates.
Then, in the voronoi_cell_lines method I determine which edges belong to which cells. For that I use the proposed solution of Alink from a related question. That is, for each edge find the two nearest input points (=cells) and create a mapping from that.
The last step is to create the actual polygons (see voronoi_polygons method). First, the outer cells which have dangling edges need to be closed. This is as simple as looking through all edges and checking which ones have only one neighboring edge. There can be either zero or two such edges. In case of two, I then connect these by introducing an additional edge.
Finally, the unordered edges in each cell need to be put into the right order to derive a polygon from them.
The usage is:
P = np.random.random((100,2))
fig = plt.figure(figsize=(4.5,4.5))
axes = plt.subplot(1,1,1)
plt.axis([-0.05,1.05,-0.05,1.05])
vertices, lineIndices = voronoi(P)
cells = voronoi_cell_lines(P, vertices, lineIndices)
polys = voronoi_polygons(cells)
for pIdx, polyIndices in polys.items():
poly = vertices[np.asarray(polyIndices)]
p = matplotlib.patches.Polygon(poly, facecolor=np.random.rand(3,1))
axes.add_patch(p)
X,Y = P[:,0],P[:,1]
plt.scatter(X, Y, marker='.', zorder=2)
plt.axis([-0.05,1.05,-0.05,1.05])
plt.show()
which outputs:
The code is probably not suitable for large numbers of input points and can be improved in some areas. Nevertheless, it may be helpful to others who have similar problems.
I came across the same problem and built a solution out of pv.'s answer and other code snippets I found across the web. The solution returns a complete Voronoi diagram, including the outer lines where no triangle neighbours are present.
#!/usr/bin/env python
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from scipy.spatial import Delaunay
def voronoi(P):
delauny = Delaunay(P)
triangles = delauny.points[delauny.vertices]
lines = []
# Triangle vertices
A = triangles[:, 0]
B = triangles[:, 1]
C = triangles[:, 2]
lines.extend(zip(A, B))
lines.extend(zip(B, C))
lines.extend(zip(C, A))
lines = matplotlib.collections.LineCollection(lines, color='r')
plt.gca().add_collection(lines)
circum_centers = np.array([triangle_csc(tri) for tri in triangles])
segments = []
for i, triangle in enumerate(triangles):
circum_center = circum_centers[i]
for j, neighbor in enumerate(delauny.neighbors[i]):
if neighbor != -1:
segments.append((circum_center, circum_centers[neighbor]))
else:
ps = triangle[(j+1)%3] - triangle[(j-1)%3]
ps = np.array((ps[1], -ps[0]))
middle = (triangle[(j+1)%3] + triangle[(j-1)%3]) * 0.5
di = middle - triangle[j]
ps /= np.linalg.norm(ps)
di /= np.linalg.norm(di)
if np.dot(di, ps) < 0.0:
ps *= -1000.0
else:
ps *= 1000.0
segments.append((circum_center, circum_center + ps))
return segments
def triangle_csc(pts):
rows, cols = pts.shape
A = np.bmat([[2 * np.dot(pts, pts.T), np.ones((rows, 1))],
[np.ones((1, rows)), np.zeros((1, 1))]])
b = np.hstack((np.sum(pts * pts, axis=1), np.ones((1))))
x = np.linalg.solve(A,b)
bary_coords = x[:-1]
return np.sum(pts * np.tile(bary_coords.reshape((pts.shape[0], 1)), (1, pts.shape[1])), axis=0)
if __name__ == '__main__':
P = np.random.random((300,2))
X,Y = P[:,0],P[:,1]
fig = plt.figure(figsize=(4.5,4.5))
axes = plt.subplot(1,1,1)
plt.scatter(X, Y, marker='.')
plt.axis([-0.05,1.05,-0.05,1.05])
segments = voronoi(P)
lines = matplotlib.collections.LineCollection(segments, color='k')
axes.add_collection(lines)
plt.axis([-0.05,1.05,-0.05,1.05])
plt.show()
Black lines = Voronoi diagram, Red lines = Delauny triangles
I do not know of a function to do this, but it does not seem like an overly complicated task.
The Voronoi graph is the junction of the circumcircles, as described in the wikipedia article.
So you could start with a function that finds the center of the circumcircles of a triangle, which is basic mathematics (http://en.wikipedia.org/wiki/Circumscribed_circle).
Then, just join centers of adjacent triangles.