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

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.

Related

How to link more than 2 artists with mplcursors (across multi-axis plot)

I was wondering whether there is a way to link more than 2 artists with mplcursors?
All the working examples I find include only 2 artists.
Example (taken from here):
import numpy as np
import matplotlib.pyplot as plt
import mplcursors
fig, axes = plt.subplots(ncols=2)
num = 5
xy = np.random.random((num, 2))
lines = []
for i in range(num):
line, = axes[0].plot((i + 1) * np.arange(10))
lines.append(line)
points = []
for x, y in xy:
point, = axes[1].plot([x], [y], linestyle="none", marker="o")
points.append(point)
cursor = mplcursors.cursor(points + lines, highlight=True)
pairs = dict(zip(points, lines))
pairs.update(zip(lines, points))
#cursor.connect("add")
def on_add(sel):
sel.extras.append(cursor.add_highlight(pairs[sel.artist]))
plt.show()
I managed to get a work around, but it only works if the artist is selected in the top graph.
cursor = mplcursors.cursor(points + lines + lines2, highlight=True)
pairs = dict(zip(points, lines))
pairs.update(zip(lines, points))
pairs2 = dict(zip(lines2, lines))
pairs2.update(zip(lines, lines2))
#cursor.connect("add")
def on_add(sel):
sel.extras.append(cursor.add_highlight(pairs[sel.artist]))
sel.extras.append(cursor.add_highlight(pairs2[sel.artist]))
plt.show()
There must be a proper way to achieve this (without getting errors), but I cannot figure it out.
PS: I also tried zipping together all 3:
pairs = dict(zip(zip(points, lines), lines2))
pairs.update(zip(lines2, zip(lines, points)))
This also didn't work.

Comparison arrow object between two bar containers

So I have the following two arrays:
base = np.arange(2)
y_axis = [32.59, 28.096]
And the following code
base = np.arange(2)
fig,ax = plt.subplots()
fig.set_figheight(10)
fig.set_figwidth(15)
bars = ax.bar(base, y_axis, width = 0.3)
bars[0].set_color('g')
ax.bar_label(bars,[f'{i}%' for i in y_axis])
ax.set_xticks(base, labels = ['Simplificado','Não simplificados'])
ax.arrow(base[0],y5,dx = base[1], dy = x5-y5)
That results in the following image
What I want to do is a comparison, arrow something kinda like this. Any ideas on a way to build up such arrow?
Sorry for bad image.
You could use matplotlib.path.
That can be used to draw polygons or also just a polyline following a specific path as used for this case.
This plot isn't optimized to look pretty (see notes at the end for potential improvement), but to show the concept:
Code:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.path as mpath
base = np.arange(2)
y_axis = [32.59, 28.096]
fig, ax = plt.subplots()
fig.set_figheight(10)
fig.set_figwidth(15)
path_y_gap = 5
delta_value = y_axis[1] - y_axis[0]
Path = mpath.Path
path_data = [
(Path.MOVETO, (base[0],y_axis[0])),
(Path.MOVETO, (base[0],y_axis[0]+path_y_gap)),
(Path.MOVETO, (base[1],y_axis[0]+path_y_gap)),
#(Path.MOVETO, (base[1],y_axis[1])), # alternative to the arrow
]
codes, verts = zip(*path_data)
path = mpath.Path(verts, codes)
x, y = zip(*path.vertices)
line, = ax.plot(x, y, 'k-')
ax.text( 0.5 , y_axis[0] + path_y_gap + 0.5, round(delta_value,2))
ax.arrow(base[1], y_axis[0]+path_y_gap, 0, -(-delta_value + path_y_gap),
head_width = 0.02 , head_length = 0.8, length_includes_head = True)
bars = ax.bar(base, y_axis, width = 0.3)
bars[0].set_color('g')
ax.bar_label(bars,[f'{i}%' for i in y_axis])
ax.set_xticks(base, labels = ['Simplificado','Não simplificados'])
Notes:
path doesn't offer arrow shaped ends, as a workaround the last section is done by a normal matplotlib arrow
Check the alternative in the path_data to the arrow for the last section
I haven't dealt with overlay of the bar % text and the path / arrow, but you could e.g. easily put a y-offset variable to start/end above that text
Check Bézier example in the matplotlib path tutorial if you prefer a 'rounded' line
You may for sure adapt the float digits another way than the used round()
The first MOVETO sets the starting point, an explicit endpoint isn't required.

Conditionally moving the position of a single data label in a pie chart

The following sample code will generate the donut chart I'll use as my example:
import matplotlib.pyplot as plt
%matplotlib inline
# Following should supposedly set the font correctly:
plt.rcParams['font.family'] = 'sans-serif'
plt.rcParams['font.sans-serif'] = ['Muli'] + plt.rcParams['font.sans-serif']
plt.rcParams['font.weight'] = 'extra bold'
size_of_groups=[12,11,30,0.3]
colors = ['#a1daaa','#bbbbb4','#444511','#1afff2']
import matplotlib as mpl
mpl.rcParams['text.color'] = '#273859'
# Create a pieplot
my_pie,texts,_ = plt.pie(size_of_groups,radius = 1.2,colors=colors,autopct="%.1f%%",
textprops = {'color':'w',
'size':15 #, 'weight':"extra bold"
}, pctdistance=0.75, labeldistance=0.7) #pctdistance and labeldistance change label positions.
labels=['High','Low','Normal','NA']
plt.legend(my_pie,labels,loc='lower center',ncol=2,bbox_to_anchor=(0.5, -0.2))
plt.setp(my_pie, width=0.6, edgecolor='white')
fig1 = plt.gcf()
fig1.show()
The above outputs this:
Mostly, this is great. Finally I got a nice looking donut chart!
But there is just one last thing to finesse - when the portion of the donut chart is very small (like the 0.6%), I need the labels to be moved out of the chart, and possibly colored black instead.
I managed to do something similar for bar charts using plt.text, but I don't think that will be feasible with pie charts at all. I figure someone has definitely solved a similar problem before, but I can't readily fine any decent solutions.
Here is a way to move all percent-texts for patches smaller than some given amount (5 degrees in the code example). Note that this will also fail when there would be multiple small pieces close to each other.
import matplotlib.pyplot as plt
import matplotlib as mpl
import numpy as np
size_of_groups = [12, 11, 30, 0.3]
colors = ['#a1daaa', '#bbbbb4', '#444511', '#1afff2']
my_pie, texts, pct_txts = plt.pie(size_of_groups, radius=1.2, colors=colors, autopct="%.1f%%",
textprops={'color': 'w', 'size': 15}, pctdistance=0.75,
labeldistance=0.7)
labels = ['High', 'Low', 'Normal', 'NA']
plt.legend(my_pie, labels, loc='lower center', ncol=2, bbox_to_anchor=(0.5, -0.2))
plt.setp(my_pie, width=0.6, edgecolor='white')
for patch, txt in zip(my_pie, pct_txts):
if (patch.theta2 - patch.theta1) <= 5:
# the angle at which the text is normally located
angle = (patch.theta2 + patch.theta1) / 2.
# new distance to the pie center
x = patch.r * 1.2 * np.cos(angle * np.pi / 180)
y = patch.r * 1.2 * np.sin(angle * np.pi / 180)
# move text to new position
txt.set_position((x, y))
txt.set_color('black')
plt.tight_layout()
plt.show()
I attempted a solution by tweaking the solution of ImportanceOfBeingErnest on a different problem given here. For some reason, the percentage sign is not being displayed in my system but you can figure that out
rad = 1.2 # Define a radius variable for later use
my_pie, texts, autotexts = plt.pie(size_of_groups, radius=rad, colors=colors, autopct="%.1f%%",
pctdistance=0.75, labeldistance=0.7, textprops={'color':'white', 'size':20})
# Rest of the code
cx, cy = 0, 0 # Center of the pie chart
for t in autotexts:
x, y = t.get_position()
text = t.get_text()
if float(text.strip('%')) < 1: # Here 1 is the target threshold percentage
angle = np.arctan2(y-cy, x-cx)
xt, yt = 1.1*rad*np.cos(angle)+cx, 1.1*rad*np.sin(angle)+cy
t.set_color("k")
t.set_position((xt,yt))

How to unstacked yticklabel in matplotlib.pcolor

The following code is the minimum amount of code needed for reproducing the my heat map problem (full code in comments):
import numpy as np
import string
from matplotlib import pylab as plt
def random_letter(chars=string.ascii_uppercase, size=2):
char_arr = np.array(list(chars))
if size > 27:
size = 27
np.random.shuffle(char_arr)
return char_arr[:size]
y_labels = [', '.join(x for x in random_letter()) for _ in range(174)]
fig.set_size_inches(11.7, 16.5)
fig, ax = plt.subplots()
data = np.random.poisson(1, (174, 40))
heatmap = ax.pcolor(data,
cmap=plt.cm.Blues,
vmin=data.min(),
vmax=data.max(),
edgecolors='white')
ax.set_xticks(np.arange(data.shape[1])+.5, minor=False);
ax.set_yticks(np.arange(data.shape[0])+.5, minor=False);
ax.set_xticklabels(np.arange(40),
minor=False,
rotation=90,
fontsize='x-small',
weight='bold');
ax.set_yticklabels(y_labels,
minor=False,
fontsize='x-small',
weight='bold');
cb = fig.colorbar(heatmap, shrink=0.33, aspect=10)
My question is: Is there a way of increase the distance between the y tick labels? When I print this in A3 (good size) it still almost unreadable because the letters are stacking in each other. I'd tried to fix the yticklabelpads without success.
Thank you all in advance

Print string over plotted line (mimic contour plot labels)

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")

Categories