Related
I have a collection of measuring points and I want to interpolate between them, for which I use SciPy's griddata():
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from scipy.interpolate import griddata
data = pd.DataFrame({
'time': [0, 1, 2, 3, 4, 0.1, 0.9, 2, 3.05, 4, 0, 1, 2.2, 3, 3.95],
'force': [1, 2, 4, 9, 16, 0, 0, 0, 0, 0, -1, -2, -4, -9, -16]
})
Times, Forces = np.meshgrid(
np.linspace(0, 4, 100),
np.linspace(-16, 16, 100)
)
data['work'] = data['time'] * data['force']
interpolation = griddata(
(data['time'], data['force']),
data['work'],
(Times, Forces),
method= 'linear'
)
fig, ax = plt.subplots()
contour = ax.contourf(
Times, Forces, interpolation
)
ax.scatter(data['time'], data['force'])
fig.show()
My problem is, that my measuring points already follow the borders of the physical possibilities, but the interpolation will nontheless interpolate for every spannable area, including those which aren't reachable to measure.
How can I limit the interpolation or at least the plot of the interpolation to 'within' the shape of the outer points? Unfortunately the 'time' measurement has small deviations.
Any advise would be greatly appreciated!
Firstly, you need to get the concave polygon for all the points. Secondly, Using the polygon to clip the contour fill. Although, this may be a bit complicated, some useful packages can help these tasks.
Below is the code.
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from scipy.interpolate import griddata
import matplotlib.path as mpath
import alphashape
from descartes import PolygonPatch
Path = mpath.Path
data = pd.DataFrame({
'time': [0, 1, 2, 3, 4, 0.1, 0.9, 2, 3.05, 4, 0, 1, 2.2, 3, 3.95],
'force': [1, 2, 4, 9, 16, 0, 0, 0, 0, 0, -1, -2, -4, -9, -16]
})
Times, Forces = np.meshgrid(
np.linspace(0, 4, 100),
np.linspace(-16, 16, 100)
)
data['work'] = data['time'] * data['force']
interpolation = griddata(
(data['time'], data['force']),
data['work'],
(Times, Forces),
method= 'linear')
fig, ax = plt.subplots()
plt.xlim(-.3,4.3)
plt.ylim(-18,18)
#contour = ax.contourf( Times, Forces, interpolation)
contour = ax.tricontourf( data['time'], data['force'], data['work'])
ax.scatter(data['time'], data['force'])
x = data['time']
y = data['force']
points = np.vstack([x, y]).T
alpha = 0.95 * alphashape.optimizealpha(points)
hull = alphashape.alphashape(points, alpha)
hull_pts = hull.exterior.coords.xy
ax.scatter(hull_pts[0], hull_pts[1], color='red')
ax.add_patch(PolygonPatch(hull, fill=False, color='red'))
plt.savefig("clip_before.png")
#make clip path
vertices = []
codes = []
xpts,ypts = hull_pts
## convert polygon to path for cliping contour fill
for ix,iy in zip(xpts,ypts):
vertices.append((ix,iy))
codes += [Path.MOVETO]
codes += [Path.LINETO] * (len(xpts) -2)
codes += [Path.CLOSEPOLY]
clip = Path(vertices, codes)
for collection in contour.collections:
collection.set_clip_path(clip,transform=ax.transData)
plt.savefig("clip_after.png")
plt.show()
And here is the output figures.
Befor clipping.
After clipping.
I have a matplotlib plot where certain points get annotated. I have worked out how to do the annotations themselves, including arrows and everything. However, I need to add a line to each annotation, next to the text of the annotation. It should run in parallel to the text, with a certain offset from the text in points. The length of the line is based on a percentage value, that each annotated point has. Ideally I would like a line that's always the same length (roughly 15 text characters, which is the max length of the text in the annotations) but has a let's say red and grey portion, based on the percentage value mentioned.
Any help or suggestions is greatly appreciated.
Edit:
Here is a minimum example of some mock data points:
import numpy as np
import matplotlib.pyplot as plt
x=[2, 3, 4, 6, 7, 8, 10, 11]
y=[1, 3, 4, 2, 3, 1, 5, 2]
tx=[3, 4, 5, 6, 7, 8, 9, 10]
yd=dict(zip(x, y))
plt.scatter(x, y)
plt.xlim(0, 14)
plt.ylim(0, 8)
tspace=list(np.linspace(.05, .95, len(tx)))
tsd=dict(zip(tx, tspace))
arpr = {"arrowstyle": "-",
"connectionstyle": "arc,angleA=-90,armA=20,angleB=90,armB=20,rad=10"}
for i, j in zip(x, tx):
plt.annotate("foo bar baz", (i, yd[i]), (tsd[j], .75),
textcoords="axes fraction", arrowprops=arpr,
annotation_clip=False, rotation="vertical")
And here is a comparison of current vs. desired output:
You can use plt.Rectangle to draw the bars — first a grey one that is the height of the entire bar, and then the red bar that is a percentage of the height of the entire bar.
However, since the width and length parameters of the rectangle are in units of the x- and y-coordinates on the plot, we need to be able to access the coordinates of the text annotations you made.
You set the annotation coordinates using textcoords="axes fraction" which makes it difficult to access the starting and ending coordinates for the rectangle in x- and y-coordinates, so instead I defined some constants x_min, x_max, y_min, y_max for the limits of the plot, and then calculated the coordinates for your text annotations directly from the tspace list as well as the bar annotation.
The percentage of red space for each bar can be set in a list so that's it's generalizable.
import numpy as np
import matplotlib.pyplot as plt
x=[2, 3, 4, 6, 7, 8, 10, 11]
y=[1, 3, 4, 2, 3, 1, 5, 2]
tx=[3, 4, 5, 6, 7, 8, 9, 10]
yd=dict(zip(x, y))
fig,ax = plt.subplots(1,1)
plt.scatter(x, y)
x_min, x_max = 0, 14
y_min, y_max = 0, 8
y_text_end = 0.75*(y_max-y_min)
plt.xlim(0, 14)
plt.ylim(0, 8)
tspace=list(np.linspace(.05, .95, len(tx)))
# tsd=dict(zip(tx, tspace))
# random percentage values to demonstrate the bar functionality
bar_percentages = [0.95, 0.9, 0.8, 0.6, 0.4, 0.2, 0.1, 0.05]
bar_width = 0.2
bar_height = 1.9
arpr = {"arrowstyle": "-",
"connectionstyle": "arc,angleA=-90,armA=20,angleB=90,armB=20,rad=10"}
## axes fraction is convenient but it's important to be able to access the exact coordinates for the Rectangle function
for i, x_val in enumerate(x):
plt.annotate("foo bar baz", (x_val, yd[x_val]), (tspace[i]*(x_max-x_min), y_text_end),
arrowprops=arpr, annotation_clip=False, rotation="vertical")
bar_grey = plt.Rectangle((tspace[i]*(x_max-x_min)+0.4, y_text_end-0.1), bar_width, bar_height, fc='#cccccc')
bar_red = plt.Rectangle((tspace[i]*(x_max-x_min)+0.4, y_text_end-0.1), bar_width, bar_percentages[i]*bar_height, fc='r')
plt.gca().add_patch(bar_grey)
plt.gca().add_patch(bar_red)
plt.show()
I have since found a solution, albeit a hacky one, and without the ideal "grey boxes", but it's fine for my purposes and I'll share it here if it might help someone. If anyone knows an improvement, please feel free to contribute. Thanks to #DerekO for providing a useful input, which I incorporated into my solution.
This is adapted from this matplotlib demo. I simply shifted the custom box to outside of the text and modified width and height with an additional parameter for the percentage. I had to split it into two actual annotations, because the arrow would not start at the correct location using the custom box, but this way it works fine. The scaling/zooming now behaves well and follows the text.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.path import Path
from matplotlib.patches import BoxStyle
class MyStyle(BoxStyle._Base):
def __init__(self, pad, per=1.):
self.pad = pad
self.per = per
super().__init__()
def transmute(self, x0, y0, width, height, mutation_size):
# padding
pad = mutation_size * self.pad
# width and height with padding added.
width = width + 2.*pad
width *= self.per
height = 8.
# boundary of the padded box
x0, y0 = x0-pad, y0-pad,
x1, y1 = x0+width, y0-height
cp = [(x0, y0),
(x1, y0),
(x1, y1),
(x0, y1),
(x0, y0)]
com = [Path.MOVETO,
Path.LINETO,
Path.LINETO,
Path.LINETO,
Path.CLOSEPOLY]
path = Path(cp, com)
return path
# register the custom style
BoxStyle._style_list["percent"] = MyStyle
x=[2, 3, 4, 6, 7, 8, 10, 11]
y=[1, 3, 4, 2, 3, 1, 5, 2]
tx=[3, 4, 5, 6, 7, 8, 9, 10]
yd=dict(zip(x, y))
fig,ax = plt.subplots(1,1)
plt.scatter(x, y)
x_min, x_max = 0, 14
y_min, y_max = 0, 8
y_text_end = 0.75*(y_max-y_min)
plt.xlim(0, 14)
plt.ylim(0, 8)
tspace=list(np.linspace(.05, .95, len(tx)))
# tsd=dict(zip(tx, tspace))
# random percentage values to demonstrate the bar functionality
bar_percentages = [0.95, 0.9, 0.8, 0.6, 0.4, 0.2, 0.1, 0.05]
arpr = {"arrowstyle": "-",
"connectionstyle": "arc,angleA=-90,armA=20,angleB=90,armB=20,rad=10"}
## axes fraction is convenient but it's important to be able to access the exact coordinates for the Rectangle function
for i, x_val in enumerate(x):
plt.annotate("", (x_val, yd[x_val]), (tspace[i]*(x_max-x_min), y_text_end),
arrowprops=arpr, annotation_clip=False, rotation="vertical",)
plt.annotate("foo bar baz", (x_val, yd[x_val]), (tspace[i]*(x_max-x_min), y_text_end),
annotation_clip=False, rotation="vertical",
va="bottom", ha="right",
bbox=dict(boxstyle=f"percent,pad=.2,per={bar_percentages[i]}",
fc="red",
ec="none"))
del BoxStyle._style_list["percent"]
plt.show()
Does anybody have a suggestion on what's the best way to present overlapping lines on a plot? I have a lot of them, and I had the idea of having full lines of different colors where they don't overlap, and having dashed lines where they do overlap so that all colors are visible and overlapping colors are seen.
But still, how do I that.
I have the same issue on a plot with a high degree of discretization.
Here the starting situation:
import matplotlib.pyplot as plt
grid=[x for x in range(10)]
graphs=[
[1,1,1,4,4,4,3,5,6,0],
[1,1,1,5,5,5,3,5,6,0],
[1,1,1,0,0,3,3,2,4,0],
[1,2,4,4,3,2,3,2,4,0],
[1,2,3,3,4,4,3,2,6,0],
[1,1,3,3,0,3,3,5,4,3],
]
for gg,graph in enumerate(graphs):
plt.plot(grid,graph,label='g'+str(gg))
plt.legend(loc=3,bbox_to_anchor=(1,0))
plt.show()
No one can say where the green and blue lines run exactly
and my "solution"
import matplotlib.pyplot as plt
grid=[x for x in range(10)]
graphs=[
[1,1,1,4,4,4,3,5,6,0],
[1,1,1,5,5,5,3,5,6,0],
[1,1,1,0,0,3,3,2,4,0],
[1,2,4,4,3,2,3,2,4,0],
[1,2,3,3,4,4,3,2,6,0],
[1,1,3,3,0,3,3,5,4,3],
]
for gg,graph in enumerate(graphs):
lw=10-8*gg/len(graphs)
ls=['-','--','-.',':'][gg%4]
plt.plot(grid,graph,label='g'+str(gg), linestyle=ls, linewidth=lw)
plt.legend(loc=3,bbox_to_anchor=(1,0))
plt.show()
I am grateful for suggestions on improvement!
Just decrease the opacity of the lines so that they are see-through. You can achieve that using the alpha variable. Example:
plt.plot(x, y, alpha=0.7)
Where alpha ranging from 0-1, with 0 being invisible.
imagine your panda data frame is called respone_times, then you can use alpha to set different opacity for your graphs. Check the picture before and after using alpha.
plt.figure(figsize=(15, 7))
plt.plot(respone_times,alpha=0.5)
plt.title('a sample title')
plt.grid(True)
plt.show()
Depending on your data and use case, it might be OK to add a bit of random jitter to artificially separate the lines.
from numpy.random import default_rng
import pandas as pd
rng = default_rng()
def jitter_df(df: pd.DataFrame, std_ratio: float) -> pd.DataFrame:
"""
Add jitter to a DataFrame.
Adds normal distributed jitter with mean 0 to each of the
DataFrame's columns. The jitter's std is the column's std times
`std_ratio`.
Returns the jittered DataFrame.
"""
std = df.std().values * std_ratio
jitter = pd.DataFrame(
std * rng.standard_normal(df.shape),
index=df.index,
columns=df.columns,
)
return df + jitter
Here's a plot of the original data from Markus Dutschke's example:
And here's the jittered version, with std_ratio set to 0.1:
Replacing solid lines by dots or dashes works too
g = sns.FacetGrid(data, col='config', row='outputs', sharex=False)
g.map_dataframe(sns.lineplot, x='lag',y='correlation',hue='card', linestyle='dotted')
Instead of random jitter, the lines can be offset just a little bit, creating a layered appearance:
import matplotlib.pyplot as plt
from matplotlib.transforms import offset_copy
grid = list(range(10))
graphs = [[1, 1, 1, 4, 4, 4, 3, 5, 6, 0],
[1, 1, 1, 5, 5, 5, 3, 5, 6, 0],
[1, 1, 1, 0, 0, 3, 3, 2, 4, 0],
[1, 2, 4, 4, 3, 2, 3, 2, 4, 0],
[1, 2, 3, 3, 4, 4, 3, 2, 6, 0],
[1, 1, 3, 3, 0, 3, 3, 5, 4, 3]]
fig, ax = plt.subplots()
lw = 1
for gg, graph in enumerate(graphs):
trans_offset = offset_copy(ax.transData, fig=fig, x=lw * gg, y=lw * gg, units='dots')
ax.plot(grid, graph, lw=lw, transform=trans_offset, label='g' + str(gg))
ax.legend(loc='upper left', bbox_to_anchor=(1.01, 1.01))
# manually set the axes limits, because the transform doesn't set them automatically
ax.set_xlim(grid[0] - .5, grid[-1] + .5)
ax.set_ylim(min([min(g) for g in graphs]) - .5, max([max(g) for g in graphs]) + .5)
plt.tight_layout()
plt.show()
I am displaying information with two y-axes and a common x-axis using the following script.
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import host_subplot
import mpl_toolkits.axisartist as AA
#creating a host plot with x and y axis
hostplot = host_subplot(111, axes_class=AA.Axes)
#creating a second y axis
extra_y_axis = hostplot.twinx()
extra_y_axis.set_navigate_mode(True)
extra_y_axis.set_navigate(True)
print extra_y_axis.can_zoom() #prints true on output
hostplot.set_xlabel("host_x")
hostplot.set_ylabel("host_y")
extra_y_axis.set_ylabel("extra_y")
hostplot.plot([0, 1, 2], [0, 1, 2])
extra_y_axis.plot([0, 1, 2], [0, 3, 2])
plt.draw()
plt.show()
After this I used the 'Zoom to Rectangle' tool from the tray in the bottom-left as shown below:
.
And I got the following output:
.
Please notice the y-axis scales in both the images. While the zoom functionality is working correctly for the host plot, I am unable to get the extra_y_axis to rescale and it just maintains a constant scale throughout (so I can't really zoom in on plots using the second axis).
How can I make it so that all the axes are rescaled on zooming in a small portion?
Thanks
I've traced down your problem to the sue of the axes_grid1 toolkit. If you don't require the use of this toolkit you can easily fix your issue by initialising your figure in the usual manner:
import matplotlib.pyplot as plt
#creating a host plot with x and y axis
fig, hostplot = plt.subplots()
#creating a second y axis
extra_y_axis = hostplot.twinx()
hostplot.set_xlabel("host_x")
hostplot.set_ylabel("host_y")
extra_y_axis.set_ylabel("extra_y")
hostplot.plot([0, 1, 2], [0, 1, 2])
extra_y_axis.plot([0, 1, 2], [0, 3, 2])
plt.show()
If you do want to use the toolkit then you have to add a couple of lines to get the two y axes to scale and transform together:
import matplotlib.transforms as mtransforms
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1.parasite_axes import SubplotHost
fig = plt.figure()
ax1 = SubplotHost(fig, 1, 1, 1)
#set the scale difference between the two y axes
aux_trans = mtransforms.Affine2D().scale(sx = 1.,sy= 1.5)
ax2 = ax1.twin(aux_trans)
fig.add_subplot(ax1)
ax1.plot([0, 1, 2], [0, 1, 2])
ax2.plot([0, 1, 2], [0, 3, 2])
ax1.set_ylim(0,3)
plt.show()
I would like to draw some text behind a Poly3DCollection object, unfortunately using the zorder keyword doesn't seem to produce the effect requested.
from mpl_toolkits.mplot3d import Axes3D
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
from matplotlib.pyplot import figure, show
canvas = figure()
axes = Axes3D(canvas)
axes.text(0.5, 0.5, 0.5, 'I would like to be behind!', 'x', zorder=0)
x = [0, 1, 1, 0]
y = [0, 0, 0.5, 0.5]
z = [0, 0, 1, 1]
axes.add_collection3d(Poly3DCollection([zip(x, y, z)], zorder=1))
show()