Print string over plotted line (mimic contour plot labels) - python

The contour plot demo shows how you can plot the curves with the level value plotted over them, see below.
Is there a way to do this same thing for a simple line plot like the one obtained with the code below?
import matplotlib.pyplot as plt
x = [1.81,1.715,1.78,1.613,1.629,1.714,1.62,1.738,1.495,1.669,1.57,1.877,1.385]
y = [0.924,0.915,0.914,0.91,0.909,0.905,0.905,0.893,0.886,0.881,0.873,0.873,0.844]
# This is the string that should show somewhere over the plotted line.
line_string = 'name of line'
# plotting
plt.plot(x,y)
plt.show()

You could simply add some text (MPL Gallery) like
import matplotlib.pyplot as plt
import numpy as np
x = [1.81,1.715,1.78,1.613,1.629,1.714,1.62,1.738,1.495,1.669,1.57,1.877,1.385]
y = [0.924,0.915,0.914,0.91,0.909,0.905,0.905,0.893,0.886,0.881,0.873,0.873,0.844]
# This is the string that should show somewhere over the plotted line.
line_string = 'name of line'
# plotting
fig, ax = plt.subplots(1,1)
l, = ax.plot(x,y)
pos = [(x[-2]+x[-1])/2., (y[-2]+y[-1])/2.]
# transform data points to screen space
xscreen = ax.transData.transform(zip(x[-2::],y[-2::]))
rot = np.rad2deg(np.arctan2(*np.abs(np.gradient(xscreen)[0][0][::-1])))
ltex = plt.text(pos[0], pos[1], line_string, size=9, rotation=rot, color = l.get_color(),
ha="center", va="center",bbox = dict(ec='1',fc='1'))
def updaterot(event):
"""Event to update the rotation of the labels"""
xs = ax.transData.transform(zip(x[-2::],y[-2::]))
rot = np.rad2deg(np.arctan2(*np.abs(np.gradient(xs)[0][0][::-1])))
ltex.set_rotation(rot)
fig.canvas.mpl_connect('button_release_event', updaterot)
plt.show()
which gives
This way you have maximum control.
Note, the rotation is in degrees and in screen not data space.
Update:
As I recently needed automatic label rotations which update on zooming and panning, thus I updated my answer to account for these needs. Now the label rotation is updated on every mouse button release (the draw_event alone was not triggered when zooming). This approach uses matplotlib transformations to link the data and screen space as discussed in this tutorial.

Based on Jakob's code, here is a function that rotates the text in data space, puts labels near a given x or y data coordinate, and works also with log plots.
def label_line(line, label_text, near_i=None, near_x=None, near_y=None, rotation_offset=0, offset=(0,0)):
"""call
l, = plt.loglog(x, y)
label_line(l, "text", near_x=0.32)
"""
def put_label(i):
"""put label at given index"""
i = min(i, len(x)-2)
dx = sx[i+1] - sx[i]
dy = sy[i+1] - sy[i]
rotation = np.rad2deg(math.atan2(dy, dx)) + rotation_offset
pos = [(x[i] + x[i+1])/2. + offset[0], (y[i] + y[i+1])/2 + offset[1]]
plt.text(pos[0], pos[1], label_text, size=9, rotation=rotation, color = line.get_color(),
ha="center", va="center", bbox = dict(ec='1',fc='1'))
x = line.get_xdata()
y = line.get_ydata()
ax = line.get_axes()
if ax.get_xscale() == 'log':
sx = np.log10(x) # screen space
else:
sx = x
if ax.get_yscale() == 'log':
sy = np.log10(y)
else:
sy = y
# find index
if near_i is not None:
i = near_i
if i < 0: # sanitize negative i
i = len(x) + i
put_label(i)
elif near_x is not None:
for i in range(len(x)-2):
if (x[i] < near_x and x[i+1] >= near_x) or (x[i+1] < near_x and x[i] >= near_x):
put_label(i)
elif near_y is not None:
for i in range(len(y)-2):
if (y[i] < near_y and y[i+1] >= near_y) or (y[i+1] < near_y and y[i] >= near_y):
put_label(i)
else:
raise ValueError("Need one of near_i, near_x, near_y")

Related

Matplotlib: How to get size of a figure within a window

Inputted x, y, and z coordinates will output three graphs: an x-z graph sliding along the y axis, an x-y graph sliding along the z axis, and a y-z graph sliding along the x axis. I position the lines based on the percent by which the user has slid its corresponding coordinate on the slider tool. See below (don't be alarmed by how the coronal and transverse views are switched):
However, as you can see, the y-coordinate is low, so the line is out of the bounds of the figure. The issue is the lines are positioned relative to the window rather than relative to the plot. Therefore, I would like to get the size of the figure within the window to correct for this issue. I have not found any documentation on how to find the dimensions of a figure within a window as opposed to the whole window—how would I approach this? Thank you! See my code below to see how each individual plot is visualized:
fig = plt.figure(d+1, figsize = (maxX, maxY))
xCoord = -1
yCoord = -1
if d == 0:
thisSlice = fData[newVoxel.s, :, :, 0]
plt.title("Saggital")
xCoord = newVoxel.t / maxT
yCoord = newVoxel.c / maxC
elif d == 1:
thisSlice = fData[:, newVoxel.t, :, 0]
plt.title("Transverse")
xCoord = newVoxel.s / maxS
yCoord = newVoxel.c / maxC
elif d == 2:
thisSlice = fData[:, :, newVoxel.c, 0]
plt.title("Coronal")
xCoord = newVoxel.s / maxS
yCoord = newVoxel.t / maxT
artists.append(fig.add_artist(lines.Line2D([0, 1], [yCoord, yCoord])))
artists.append(fig.add_artist(lines.Line2D([xCoord, xCoord], [0, 1])))
plt.axis('off')
plt.imshow(thisSlice.T, cmap = 'inferno', origin = 'lower')
It is possible to transform figure points into axis data; matplotlib has an entire framework dedicated to this. However, why make your life more difficult? I suggest structuring the figures differently around the axis limits of the plotted images:
import matplotlib.pyplot as plt
#random data
import numpy as np
rng = np.random.default_rng(123)
maxX = rng.integers(10, 20)
maxY = rng.integers(5, 10)
thisSlice = rng.random((maxX, maxY))
def define_figure(d, thisSlice):
fig = plt.figure(d+1, figsize=(maxX, maxY))
ax = fig.add_subplot()
artists = []
my_title = ["Saggital", "Transverse", "Coronal"][d]
ax.set_title(my_title)
ax.axis('off')
img = ax.imshow(thisSlice.T, cmap = 'inferno', origin = 'lower')
view_x_max, view_y_max = thisSlice.shape
viewlimits = [view_x_max, view_y_max]
artists.append(ax.axhline(y=view_y_max//2, xmin=-10, xmax=10, clip_on = False))
artists.append(ax.axvline(x=view_x_max//2, ymin=-10, ymax=10, clip_on = False))
return fig, ax, img, artists, viewlimits
#create figure and capture essential figure elements you will need for later updates
current_figure, current_axis, current_image, current_lineartists, current_viewlimits = define_figure(1, thisSlice)
#now limit the slider range to integer values not exceeding current_viewlimits
#or update the image in axis if the corresponding slider is moved
def update_image(img_object, newimage):
img_object.set_data(newimage.T)
plt.pause(5)
update_image(current_image, rng.random(thisSlice.shape))
plt.pause(5)
plt.show()

Matplotlib clearing old axis labels when re-plotting data

I've got a script wherein I have two functions, makeplots() which makes a figure of blank subplots arranged in a particular way (depending on the number of subplots to be drawn), and drawplots() which is called later, drawing the plots (obviously). The functions are posted below.
The script does some analysis of data for a given number of 'targets' (which can number anywhere from one to nine) and creates plots of the linear regression for each target. When there are multiple targets, this works great. But when there's a single target (i.e. a single 'subplot' in the figure), the Y-axis label overlaps the axis itself (this does not happen when there are multiple targets).
Ideally, each subplot would be square, no labels would overlap, and it would work the same for one target as for multiple targets. But when I tried to decrease the size of the y-axis label and shift it over a bit, it appears that the actual axes object was drawn over the previously blank, square plot (whose axes ranged from 0 to 1), and the old tick mark labels are still visible. I'd like to have those old tick marks removed when calling drawplots(). I've tried changing the subplot_kw={} arguments in makeplots, as well as removing ax.set_aspect('auto') from drawplots, both to no avail. Note that there are also screenshots of various behaviors at the end, also.
def makeplots(targets, active=actwindow):
def rowcnt(y):
rownumb = y//3 if (y%3 == 0) else y//3+1
return rownumb
def colcnt(x):
if x <= 3: colnumb = x
elif x == 4: colnumb = 2
else: colnumb = 3
return colnumb
numsubs = len(targets)
numrow, numcol = rowcnt(numsubs), colcnt(numsubs)
if numsubs >= 1:
if numsubs == 1:
fig, axs = plt.subplots(num='LOD-95 Plots', nrows=1, ncols=1, figsize = [8,6], subplot_kw={'adjustable': 'box', 'aspect': 1})
# changed 'box' to 'datalim'
fig, axs = plt.subplots(num='LOD-95 Plots', nrows=numrow, ncols=numcol, figsize = [numcol*6,numrow*6], subplot_kw={'adjustable': 'box', 'aspect': 1})
fig.text(0.02, 0.5, 'Probit score\n $(\sigma + 5)$', va='center', rotation='vertical', size='16')
else:
raise ValueError(f'Error generating plots [call: makeplots({targets},{active}) - invalid numsubs value]')
axs = np.ravel(axs)
for i, ax in enumerate(axs):
ax.set_title(f'Limit of Detection: {targets[i]}', size=11)
ax.grid()
return fig, axs
and
def drawplots(ax, dftables, color1, color2):
y = dftables.probit
y95 = 6.6448536269514722
logreg = False
regfun = lambda m, x, b : (m*x) + b
regq = scipy.stats.linregress(dftables.qty,y)
regl = scipy.stats.linregress(dftables.log_qty,y)
if regq.rvalue**2 >= regl.rvalue**2:
regression = regq
x_label = 'input quantity'
x = dftables.qty
elif regq.rvalue**2 < regl.rvalue**2:
regression = regl
x_label = '$log_{10}$(input quantity)'
x = dftables.log_qty
logreg = True
slope, intercept, r = regression.slope, regression.intercept, regression.rvalue
r2 = r**2
lod = (y95-intercept)/slope
xr = [0, lod*1.2]
yr = [intercept, regfun(slope, xr[1], intercept)]
regeqn = "y = "+str(f"{slope:.2e}")+"x + "+str(f"{intercept:.3f}")
if logreg:
lodstr = f'log(LOD) = {lod:.2f}' if lod <= 100 else f'log(LOD) = {lod:.2e}'
elif not logreg:
lodstr = f'LOD = {lod:.2f}' if lod <= 100 else f'LOD = {lod:.2e}'
# raise ValueError(f'Error raised calling drawplots()')
ax.set_xlabel(x_label, fontweight='bold')
ax.plot(xr, yr, color=color1, linestyle='--') # plot regression line
ax.plot(lod, y95, marker='D', color=color2, markersize=7) # plot point for LoD
ax.plot(xr, [y95,y95], color=color2, linestyle=':') # horizontal crosshair
ax.plot([lod,lod],[0, 7.1], color=color2, linestyle=':') # vertical crosshair
ax.scatter(x, y, s=81, color=color1, marker='.') # actual data points
ax.annotate(f"{lodstr}", xy=(lod,0.1),
xytext=(0.9*lod,0.5), fontsize=8, arrowprops = dict(facecolor='black', headlength=5, width=2, headwidth=5))
ax.set_aspect('auto')
ax.set_xlim(left=0)
ax.set_ylim(bottom=0)
ax.plot()
if logreg: lod = 10 ** lod
return r2, lod, regeqn, logreg
The context they're called in:
fig, axs = makeplots(targets)
wg.SetForegroundWindow(actwindow)
with open(outName, 'a+') as f:
print(f"Lower Limit of Detection Analysis on {dt} at {tm}\n", file=f)
for i, tars in enumerate(targets):
data[tars] = stripThousands(data[tars])
# logans = checkyn(f"Analyze {tars} using log10(concentration/quantity)? (y/n): ")
for idx, val in enumerate(qtys):
tables[i,idx,2] = hitrate(val,data,tars)
tables[i,idx,3] = norm.ppf(tables[i,idx,2])+5
printtables[tars] = pd.DataFrame(tables[i,:,:], columns=["qty","log_qty","probability","probit"])
# construct dataframes from np.arrays and drop
# rows with infinite probit values:
dftables[tars] = pd.DataFrame(tables[i,:,:], columns=["qty","log_qty","probability","probit"])
dftables[tars].probit.replace([np.inf,-np.inf],np.nan, inplace=True)
dftables[tars].dropna(inplace=True)
r2, lod, eqn, logreg = drawplots(axs[i], dftables[tars], cbcolors[i], cbcolors[i+5])
You should clear the axes in each iteration using pyplot.cla().
You posted a lot of code, so I'm not 100% sure of the best location to place it in your code, but the general idea is to clear the axes before each new plot.
Here is a minimal demo without cla():
x = [[1,2,3], [3,2,1]]
fig, ax = plt.subplots()
for index, data in enumerate(x):
ax.plot(data)
And with cla():
for index, data in enumerate(x):
ax.cla()
ax.plot(data)

Updating items on a grid dynamically within a for loop (python)

I’m trying to update a plot dynamically within a for loop and I can’t get it to work. I wonder if anyone can help?
I get a bit confused between passing the figure vs axes and how to update. I’ve been trying to use
display.clear_output(wait=True)
display.display(plt.gcf())
time.sleep(2)
but it’s not doing what I want it to.
I'm trying to:
1. add objects to a grid (setupGrid2)
2. at a timestep - move each object in random direction (makeMove2)
3. update the position of each object visually on the grid (updateGrid2)
My problem is with 3. I'd like to clear the previous step, so that just the new location for each object is displayed. The goal to show the objects dynamically moving around the grid.
I'd also like to work with the ax object created in setupGrid2, so that I can set the plot variables (title, legend etc.) in one place and update that chart.
Grateful for any help.
Sample code below (for running in jupyter notebook):
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
import time
import pylab as pl
from IPython import display
def setupGrid2(norows,nocols,noobjects):
#each object needs current grid position (x and y coordinate)
objects = np.zeros(noobjects)
ObjectPos = np.zeros(shape=(noobjects,2))
#put objects randomly on grid
for i in range (noobjects):
ObjectPos[i][0] = np.random.uniform(0,norows)
ObjectPos[i][1] = np.random.uniform(0,nocols)
#plot objects on grid
fig = plt.figure(1,figsize=(15,5))
ax = fig.add_subplot(1,1,1)
x,y = zip(*ObjectPos)
ax.scatter(x, y,c="b", label='Initial positions')
ax.grid()
plt.show()
return ax,ObjectPos
def updateGrid2(ax,ObjPos):
x,y = zip(*ObjPos)
plt.scatter(x, y)
display.clear_output(wait=True)
display.display(plt.gcf())
time.sleep(0.1)
#move object in a random direction
def makeMove2(object,xpos,ypos):
#gets a number: 1,2,3 or 4
direction = int(np.random.uniform(1,4))
if (direction == 1):
ypos = ypos+1
if (direction == 2):
ypos = ypos - 1
if (direction == 3):
xpos = xpos+1
if (direction == 4):
xpos = xpos-1
return xpos,ypos
def Simulation2(rows,cols,objects,steps):
ax,ObjPos = setupGrid2(rows,cols,objects)
for i in range(steps):
for j in range (objects):
xpos = ObjPos[j][0]
ypos = ObjPos[j][1]
newxpos,newypos = makeMove2(j,xpos,ypos)
ObjPos[j][0] = newxpos
ObjPos[j][1] = newypos
updateGrid2(ax,ObjPos)
Simulation2(20,20,2,20)
It seems you want to update the scatter, instead of producing a new scatter for each frame. That would be shown in this question. Of course you can still use display when running this in jupyter instead of the shown solutions with ion or FuncAnimation.
Leaving the code from the question mostly intact this might look as follows.
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
import time
import pylab as pl
from IPython import display
def setupGrid2(norows,nocols,noobjects):
#each object needs current grid position (x and y coordinate)
objects = np.zeros(noobjects)
ObjectPos = np.zeros(shape=(noobjects,2))
#put objects randomly on grid
for i in range (noobjects):
ObjectPos[i,0] = np.random.uniform(0,norows)
ObjectPos[i,1] = np.random.uniform(0,nocols)
#plot objects on grid
fig = plt.figure(1,figsize=(15,5))
ax = fig.add_subplot(1,1,1)
ax.axis([0,nocols+1,0,norows+1])
x,y = zip(*ObjectPos)
scatter = ax.scatter(x, y,c="b", label='Initial positions')
ax.grid()
return ax,scatter,ObjectPos
def updateGrid2(ax,sc,ObjPos):
sc.set_offsets(ObjPos)
display.clear_output(wait=True)
display.display(plt.gcf())
time.sleep(0.1)
#move object in a random direction
def makeMove2(object,xpos,ypos):
#gets a number: 1,2,3 or 4
direction = int(np.random.uniform(1,4))
if (direction == 1):
ypos = ypos+1
if (direction == 2):
ypos = ypos - 1
if (direction == 3):
xpos = xpos+1
if (direction == 4):
xpos = xpos-1
return xpos,ypos
def Simulation2(rows,cols,objects,steps):
ax,scatter,ObjPos = setupGrid2(rows,cols,objects)
for i in range(steps):
for j in range (objects):
xpos = ObjPos[j,0]
ypos = ObjPos[j,1]
newxpos,newypos = makeMove2(j,xpos,ypos)
ObjPos[j,0] = newxpos
ObjPos[j,1] = newypos
updateGrid2(ax,scatter,ObjPos)
Simulation2(20,20,3,20)

matplotlib LogFormatterExponent -- 'e' in the exponent labels of cbar

So I'm modifying someone else's library to setup a cbar with log (values). I thought I could use LogFormatterExponent() ... But it seemingly randomly adds and 'e' to the exponents that it uses for the cbar. What's going on? How can I suppress/fix this?
if show_cbar:
if log:
l_f = LogFormatterExponent()
else:
l_f = ScalarFormatter()
if qtytitle is not None:
plt.colorbar(ims,format=l_f).set_label(qtytitle)
else:
plt.colorbar(ims,format=l_f).set_label(units)
Here's what I'm seeing for log=True:
And another plot where log = False:
At first, I thought the 'e's were being cut-off by the label at right... but over several plots this doesn't appear to be the case. I usually get 1-2 'e's ... But on a plot with only 3 color bar ticks, I see none!
A minimal example is
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colors as cm
import matplotlib.ticker as ct
data = np.exp(np.random.rand(20, 20) * 100)
fig, ax = plt.subplots()
log_norm = cm.LogNorm()
im = ax.imshow(data, interpolation='nearest', cmap='viridis', norm=log_norm)
fig.colorbar(im, format=ct.LogFormatterExponent())
This looks like a bug in mpl. If you already have a large library, I would just include a fixed version of the formatter.
class LogFormatterExponentFixed(LogFormatter):
"""
Format values for log axis; using ``exponent = log_base(value)``
"""
def __call__(self, x, pos=None):
"""Return the format for tick val *x* at position *pos*"""
vmin, vmax = self.axis.get_view_interval()
vmin, vmax = mtransforms.nonsingular(vmin, vmax, expander=0.05)
d = abs(vmax - vmin)
b = self._base
if x == 0:
return '0'
sign = np.sign(x)
# only label the decades
fx = math.log(abs(x)) / math.log(b)
isDecade = is_close_to_int(fx)
if not isDecade and self.labelOnlyBase:
s = ''
elif abs(fx) > 10000:
s = '%1.0g' % fx
elif abs(fx) < 1:
s = '%1.0g' % fx
else:
# this is the added line
fd = math.log(abs(d)) / math.log(b)
s = self.pprint_val(fx, fd)
if sign == -1:
s = '-%s' % s
return self.fix_minus(s)
Working on a fix for upstream.

Plot dashed line interrupted with data (similar to contour plot)

I am stuck with a (hopefully) simple problem.
My aim is to plot a dashed line interrupted with data (not only text).
As I only found out to create a dashed line via linestyle = 'dashed', any help is appreciated to put the data between the dashes.
Something similar, regarding the labeling, is already existing with Matplotlib - as I saw in the contour line demo.
Update:
The question link mentioned by Richard in comments was very helpful, but not the 100% like I mentioned via comment.
Currently, I do it this way:
line_string2 = '-10 ' + u"\u00b0" +"C"
l, = ax1.plot(T_m10_X_Values,T_m10_Y_Values)
pos = [(T_m10_X_Values[-2]+T_m10_X_Values[-1])/2., (T_m10_Y_Values[-2]+T_m10_Y_Values[-1])/2.]
# transform data points to screen space
xscreen = ax1.transData.transform(zip(T_m10_Y_Values[-2::],T_m10_Y_Values[-2::]))
rot = np.rad2deg(np.arctan2(*np.abs(np.gradient(xscreen)[0][0][::-1])))
ltex = plt.text(pos[0], pos[1], line_string2, size=9, rotation=rot, color='b',ha="center", va="bottom",bbox = dict(ec='1',fc='1', alpha=0.5))
Here you can see a snapshot of the result. The minus 20°C is without BBox.
Quick and dirty answer using annotate:
import matplotlib.pyplot as plt
import numpy as np
x = list(reversed([1.81,1.715,1.78,1.613,1.629,1.714,1.62,1.738,1.495,1.669,1.57,1.877,1.385]))
y = [0.924,0.915,0.914,0.91,0.909,0.905,0.905,0.893,0.886,0.881,0.873,0.873,0.844]
def plot_with_text(x, y, text, text_count=None):
text_count = (2 * (len(x) / len(text))) if text_count is None else text_count
fig, ax = plt.subplots(1,1)
l, = ax.plot(x,y)
text_size = len(text) * 10
idx_step = len(x) / text_count
for idx_num in range(text_count):
idx = int(idx_num * idx_step)
text_pos = [x[idx], y[idx]]
xscreen = ax.transData.transform(zip(x[max(0, idx-1):min(len(x), idx+2)], y[max(0, idx-1):min(len(y), idx+2)]))
a = np.abs(np.gradient(xscreen)[0][0])
rot = np.rad2deg(np.arctan2(*a)) - 90
ax.annotate(text, xy=text_pos, color="r", bbox=dict(ec="1", fc="1", alpha=0.9), rotation=rot, ha="center", va="center")
plot_with_text(x, y, "test")
Yields:
You can play with the offsets for more pleasing results.

Categories