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$'}
Related
I want to draw a volume in x1,x2,x3-space. The volume is an isocurve found by the marching cubes algorithm in skimage. The function generating the volume is pdf_grid = f(x1,x2,x3) and
I want to draw the volume where pdf = 60% max(pdf).
My issue is that the marching cubes algorithm generates vertices and faces, but how do I map those back to the x1, x2, x3-space?
My (rather limited) understanding of marching cubes is that "vertices" refer to the indices in the volume (pdf_grid in my case). If "vertices" contained only the exact indices in the grid this would have been easy, but "vertices" contains floats and not integers. It seems like marching cubes do some interpolation between grid points (according to https://www.cs.carleton.edu/cs_comps/0405/shape/marching_cubes.html), so the question is then how to recover exactly the values of x1,x2,x3?
import numpy as np
import scipy.stats
import matplotlib.pyplot as plt
#Make some random data
cov = np.array([[1, .2, -.5],
[.2, 1.2, .1],
[-.5, .1, .8]])
dist = scipy.stats.multivariate_normal(mean = [1., 3., 2], cov = cov)
N = 500
x_samples = dist.rvs(size=N).T
#Create the kernel density estimator - approximation of a pdf
kernel = scipy.stats.gaussian_kde(x_samples)
x_mean = x_samples.mean(axis=1)
#Find the mode
res = scipy.optimize.minimize(lambda x: -kernel.logpdf(x),
x_mean #x0, initial guess
)
x_mode = res["x"]
num_el = 50 #number of elements in the grid
x_min = np.min(x_samples, axis = 1)
x_max = np.max(x_samples, axis = 1)
x1g, x2g, x3g = np.mgrid[x_min[0]:x_max[0]:num_el*1j,
x_min[1]:x_max[1]:num_el*1j,
x_min[2]:x_max[2]:num_el*1j
]
pdf_grid = np.zeros(x1g.shape) #implicit function/grid for the marching cubes
for an in range(x1g.shape[0]):
for b in range(x1g.shape[1]):
for c in range(x1g.shape[2]):
pdf_grid[a,b,c] = kernel(np.array([x1g[a,b,c],
x2g[a,b,c],
x3g[a,b,c]]
))
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
from skimage import measure
iso_level = .6 #draw a volume which contains pdf_val(mode)*60%
verts, faces, normals, values = measure.marching_cubes(pdf_grid, kernel(x_mode)*iso_level)
#How to convert the figure back to x1,x2,x3 space? I just draw the output as it was done in the skimage example here https://scikit-image.org/docs/0.16.x/auto_examples/edges/plot_marching_cubes.html#sphx-glr-auto-examples-edges-plot-marching-cubes-py so you can see the volume
# Fancy indexing: `verts[faces]` to generate a collection of triangles
mesh = Poly3DCollection(verts[faces],
alpha = .5,
label = f"KDE = {iso_level}"+r"$x_{mode}$",
linewidth = .1)
mesh.set_edgecolor('k')
fig, ax = plt.subplots(subplot_kw=dict(projection='3d'))
c1 = ax.add_collection3d(mesh)
c1._facecolors2d=c1._facecolor3d
c1._edgecolors2d=c1._edgecolor3d
#Plot the samples. Marching cubes volume does not capture these samples
pdf_val = kernel(x_samples) #get density value for each point (for color-coding)
x1, x2, x3 = x_samples
scatter_plot = ax.scatter(x1, x2, x3, c=pdf_val, alpha = .2, label = r" samples")
ax.scatter(x_mode[0], x_mode[1], x_mode[2], c = "r", alpha = .2, label = r"$x_{mode}$")
ax.set_xlabel(r"$x_1$")
ax.set_ylabel(r"$x_2$")
ax.set_zlabel(r"$x_3$")
# ax.set_box_aspect([np.ptp(i) for me in x_samples]) # equal aspect ratio
cbar = fig.color bar(scatter_plot, ax=ax)
cbar.set_label(r"$KDE(w) \approx pdf(w)$")
ax.legend()
#Make the axis limit so that the volume and samples are shown.
ax.set_xlim(- 5, np.max(verts, axis=0)[0] + 3)
ax.set_ylim(- 5, np.max(verts, axis=0)[1] + 3)
ax.set_zlim(- 5, np.max(verts, axis=0)[2] + 3)
This is probably way too late of an answer to help OP, but in case anyone else comes across this post looking for a solution to this problem, the issue stems from the marching cubes algorithm outputting the relevant vertices in array space. This space is defined by the number of elements per dimension of the mesh grid and the marching cubes algorithm does indeed do some interpolation in this space (explaining the presence of floats).
Anyways, in order to transform the vertices back into x1,x2,x3 space you just need to scale and shift them by the appropriate quantities. These quantities are defined by the range, number of elements of the mesh grid, and the minimum value in each dimension respectively. So using the variables defined in the OP, the following will provide the actual location of the vertices:
verts_actual = verts*((x_max-x_min)/pdf_grid.shape) + x_min
I am trying to set an interactive notebook up that plots some interpolated GPS data. I have the plotting working by itself, but I am trying to use the ipython widgets to make it more interactive for others.
Currently, my plotting looks like this
def create_grid(array,spacing=.01):
'''
creates evenly spaced grid from the min and max of an array
'''
grid = np.arange(np.amin(array), np.amax(array),spacing)
return grid
def interpolate(x, y, z, grid_spacing = .01, model='spherical',returngrid = False):
'''Interpolates z value and uses create_grid to create a grid of values based on min and max of x and y'''
grid_x = create_grid(x,spacing = grid_spacing)
grid_y = create_grid(y, spacing = grid_spacing)
OK = OrdinaryKriging(x, y, z, variogram_model=model, verbose = False,\
enable_plotting=False, nlags = 20)
z1, ss1 = OK.execute('grid', grid_x,grid_y,mask = False)
print('Interpolation Complete')
vals=np.ma.getdata(z1)
sigma = np.ma.getdata(ss1)
if returngrid == False:
return vals,sigma
else:
return vals, sigma, grid_x, grid_y
mesh_x, mesh_y = np.meshgrid(grid_x,grid_y)
plot = plt.scatter(mesh_x, mesh_y, c = z1, cmap = cm.hsv)
cb = plt.colorbar(plot)
cb.set_label('Northing Change')
plt.show()
'''
This works currently, but I am trying to set up a widget to change the variogram model in the kriging interpolation, as well as change the field to be interpolated.
Currently, to do that I have:
def update_plot(zfield,variogram):
plt.clf()
z1, ss1, grid_x,grid_y =interpolate(lon,lat,zfield,returngrid= True,model=variogram)
mesh_x, mesh_y = np.meshgrid(grid_x,grid_y)
plot = plt.scatter(mesh_x, mesh_y, c = z1, cmap = cm.hsv)
cb = plot.colorbar(plot)
cb.set_label('Interpolated Value')
variogram = widgets.Dropdown(options = ['linear', 'power', 'gaussian', 'spherical', 'exponential', 'hole-effect'],
value = 'spherical', description = "Variogram model for interpolation")
zfield = widgets.Dropdown(options = {'Delta N':delta_n, 'Delta E': delta_e,'Delta V':delta_v},value = 'Delta N',
description = 'Interpolated value')
widgets.interactive(update_plot, variogram = variogram,zfield =zfield)
Which brings up the error
TraitError: Invalid selection: value not found
the values delta_n, delta_e and delta_v are numpy arrays. I have tried looking at documentation but it is not as detailed as something like matplotlibs documentation or something so I feel like I am kind of flying blind here.
Thank you
In this line, you specify the possible values of the Dropdown as:
zfield = widgets.Dropdown(options = {'Delta N':delta_n, 'Delta E': delta_e,'Delta V':delta_v}
When a mapping is used, the values of the dict are interpreted as the possible options. So value = 'Delta N' causes an error as this is not one of the possible values of the Dropdown (although it is one of the keys in the mapping dict). I believe you want value = delta_n instead.
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()
I am trying to plot a graph something similar to this:
For that, I have written the following function in python
def plot_graph_perf(dataset):
#TODO: Give labels as power ranges in spaces of 1000
plotter = ['0',
'1200000-10', '1200000-14', '1200000-18',
'1200000-2', '1200000-22', '1200000-26', '1200000-30',
'1200000-34', '1200000-38', '1200000-42', '1200000-46',
'1200000-6',
'1600000-10', '1600000-14',
'1600000-18', '1600000-2', '1600000-22',
'1600000-26', '1600000-30', '1600000-34',
'1600000-38', '1600000-42', '1600000-46',
'1600000-6',
'2000000-10', '2000000-14',
'2000000-18', '2000000-2', '2000000-22',
'2000000-26', '2000000-30', '2000000-34',
'2000000-38', '2000000-42', '2000000-46',
'2000000-6',
'2400000-10', '2400000-14',
'2400000-18', '2400000-2', '2400000-22',
'2400000-26', '2400000-30', '2400000-34',
'2400000-38', '2400000-42', '2400000-46',
'2400000-6' ,
'800000-10', '800000-14',
'800000-18', '800000-2', '800000-22',
'800000-26', '800000-30', '800000-34',
'800000-38', '800000-42', '800000-46',
'800000-6' ]
x_axis_labels = dataset[1]
x=[a for a in range(len(x_axis_labels))]
y_axis_labels = dataset[0]
y=[a for a in range(len(y_axis_labels))]
width = 0.1
plt.figure
plt.plot(plotter, color = 'g')
plt.tight_layout(pad=1, h_pad=4, w_pad=None)
plt.xticks(x,x_axis_labels, rotation='vertical')
plt.yticks(y,y_axis_labels, rotation='horizontal')
plt.xlabel('Power')
plt.ylabel('perf')
plt.title(file + ' | (Power)')
fig = plt.gcf()
fig.set_size_inches(28.5,10.5)
plt.savefig('watt' + '.png',bbox_inches='tight', pad_inches=0.5,dpi=100)
plt.clf()
Where dataset is two dimensional list something like this
dataset = [[],[]]
each sublist containing same number of elements as plotter.
I plotted dataset[0] and dataset[1] as y and x respectively, but was unable to plot the string values in plotter.
Can you please shed some light and help me plot the plotter values on the graph.
Thanks.
You have to call the text function for each word separately:
words = list("abcdefg")
xs = np.random.randint(0,10,len(words))
ys = np.random.randint(0,10,len(words))
for x, y, s in zip(xs,ys,words):
plt.text(x,y,s)
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