Transform from data to figure coordinates - python

Similar to this post, I would like to transform my data coordinates to figure coordinates. Unfortunately, the transformation tutorial doesn't seem to talk about it. So I came up with something analogous to the answer by wilywampa, but for some reason, there is something wrong and I can't figure it out:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import ConnectionPatch
t = [
0, 6.297, 39.988, 46.288, 79.989, 86.298, 120.005, 126.314, 159.994,
166.295, 200.012, 206.314, 240.005, 246.301, 280.05, 286.35, 320.032,
326.336, 360.045, 366.345, 480.971, 493.146, 1080.117, 1093.154, 1681.019,
1692.266, 2281.008, 2293.146, 2881.014, 2893.178, 3480.988, 3493.149,
4080.077, 4092.298, 4681.007, 4693.275, 5281.003, 5293.183, 5881.023,
5893.188, 6481.002, 6492.31
]
y = np.zeros(len(t))
fig, (axA, axB) = plt.subplots(2, 1)
fig.tight_layout()
for ax in (axA, axB):
ax.set_frame_on(False)
ax.axes.get_yaxis().set_visible(False)
axA.plot(t[:22], y[:22], c='black')
axA.plot(t[:22], y[:22], 'o', c='#ff4500')
axA.set_ylim((-0.05, 1))
axB.plot(t, y, c='black')
axB.plot(t, y, 'o', c='#ff4500')
axB.set_ylim((-0.05, 1))
pos1 = axB.get_position()
pos2 = [pos1.x0, pos1.y0 + 0.3, pos1.width, pos1.height]
axB.set_position(pos2)
trans = [
# (ax.transAxes + ax.transData.inverted()).inverted().transform for ax in
(fig.transFigure + ax.transData.inverted()).inverted().transform for ax in
(axA, axB)
]
con1 = ConnectionPatch(
xyA=trans[0]((0, 0)), xyB=(0, 0.1), coordsA="figure fraction",
coordsB="data", axesA=axA, axesB=axB, color="black"
)
con2 = ConnectionPatch(
xyA=(500, 0), xyB=(500, 0.1), coordsA="data", coordsB="data",
axesA=axA, axesB=axB, color="black"
)
print(trans[0]((0, 0)))
axB.add_artist(con1)
axB.add_artist(con2)
plt.show()
The line on the left is supposed to go to (0, 0) of the upper axis, but it doesn't. The same happens btw if I try to convert to axes coordinates, so there seems be to something fundamentally wrong.
The reason why I want to use figure coords is because I don't actually want the line to end at (0, 0), but slightly below the '0' tick label. I cannot do that in data coords so I tried to swap to figure coods.

Adapting the second example from this tutorial code, it seems no special combinations of transforms is needed. You can use coordsA=axA.get_xaxis_transform(), if x is in data coordinates and y in figure coordinates. Or coordsA=axA.transData if x and y are both in data coordinates. Note that when using data coordinates you are allowed to give coordinates outside the view window; by default a ConnectionPatch isn't clipped.
The following code uses z-order to put the connection lines behind the rest and adds a semi-transparent background to the tick labels of axA (avoiding that the text gets crossed out by the connection line):
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import ConnectionPatch
t = [0, 6.297, 39.988, 46.288, 79.989, 86.298, 120.005, 126.314, 159.994, 166.295, 200.012, 206.314, 240.005, 246.301, 280.05, 286.35, 320.032, 326.336, 360.045, 366.345, 480.971, 493.146, 1080.117, 1093.154, 1681.019, 1692.266, 2281.008, 2293.146, 2881.014, 2893.178, 3480.988, 3493.149, 4080.077, 4092.298, 4681.007, 4693.275, 5281.003, 5293.183, 5881.023, 5893.188, 6481.002, 6492.31]
y = np.zeros(len(t))
fig, (axA, axB) = plt.subplots(2, 1)
fig.tight_layout()
for ax in (axA, axB):
ax.set_frame_on(False)
ax.axes.get_yaxis().set_visible(False)
axA.plot(t[:22], y[:22], c='black')
axA.plot(t[:22], y[:22], 'o', c='#ff4500')
axA.set_ylim((-0.05, 1))
axB.plot(t, y, c='black')
axB.plot(t, y, 'o', c='#ff4500')
axB.set_ylim((-0.05, 1))
pos1 = axB.get_position()
pos2 = [pos1.x0, pos1.y0 + 0.3, pos1.width, pos1.height]
axB.set_position(pos2)
con1 = ConnectionPatch(xyA=(0, 0.02), coordsA=axA.get_xaxis_transform(),
xyB=(0, 0.05), coordsB=axB.get_xaxis_transform(),
# linestyle='--', color='black', zorder=-1)
linestyle='--', color='darkgrey', zorder=-1)
con2 = ConnectionPatch(xyA=(500, 0.02), coordsA=axA.get_xaxis_transform(),
xyB=(500, 0.05), coordsB=axB.get_xaxis_transform(),
linestyle='--', color='darkgrey', zorder=-1)
fig.add_artist(con1)
fig.add_artist(con2)
for lbl in axA.get_xticklabels():
lbl.set_backgroundcolor((1, 1, 1, 0.8))
plt.show()

Possible answer to your last comment:
As you're dealing with figure coords, these can change depending on your screen resolution. So if your other machine has a different res then this could be why its changing. You'll have to look into using Axes coords instead if you don't want these random changes.

Related

matplotlib fill_between leaving gaps between regions

I'm trying to use fill_between to fill different regions of a plot, but I get gaps between the regions I'm trying to fill.
I've tried using interpolate=True, but this results in non rectangular shapes...
`
import matplotlib.pyplot as plt
import numpy as np
fig, ax = plt.subplots()
x = np.arange(0, 4 * np.pi, 0.01)
y = np.sin(x)
ax.plot(x, y, color='black')
threshold = 0.75
ax.axhline(threshold, color='green', lw=2, alpha=0.7)
ax.fill_between(x, 0, 1, where=y > threshold,
facecolor=(0.5,0,0,0.5), ec=None,transform=ax.get_xaxis_transform())
ax.fill_between(x, 0, 1, where=y <= threshold,
facecolor=(0,0.5,0,0.5), ec=None, transform=ax.get_xaxis_transform())
`
I've attched a zoomed in screenshot of the plot.
You could do one or both of the following:
use finer-grainded x values, e.g.x = np.arange(0, 4 * np.pi, 0.0001). This will remove the white stripes at full view, but if you zoom in they will re-appear at a certain zoom level.
first draw the green background without a where condition over the full x range and then plot the red sections at the required sections. In case of non-opaque colors as in the example you'll need to manually re-calculate the semitransparent color on the default white background to a fully opaque color:
x = np.arange(0, 4 * np.pi, 0.001)
# ...
ax.fill_between(x, 0, 1, facecolor=(0, 0.5, 0, 0.5), ec=None,
transform=ax.get_xaxis_transform())
ax.fill_between(x, 0, 1, where=y>threshold, facecolor=(0.75, 0.5, 0.5),
ec=None, transform=ax.get_xaxis_transform())
I found an alternative way of solving this problem, by using pcolormesh where the color array is 1xn:
C = np.reshape(np.array(trnsys_out["LCG_state"][:-1].values), (-1, 1)).T
x = trnsys_out.index
y = [Pmin, Pmax]
ctrl = ax2.pcolormesh(x, y, C, shading="flat", cmap="binary", alpha=0.5, vmin=0, vmax=5)

How to plot a mean line on a distplot between 0 and the y value of the mean?

I have a distplot and I would like to plot a mean line that goes from 0 to the y value of the mean frequency. I want to do this, but have the line stop at when the distplot does. Why isn't there a simple parameter that does this? It would be very useful.
I have some code that gets me almost there:
plt.plot([x.mean(),x.mean()], [0, *what here?*])
This code plots a line just as I'd like except for my desired y-value. What would the correct math be to get the y max to stop at the frequency of the mean in the distplot? An example of one of my distplots is below using 0.6 as the y-max. It would be awesome if there was some math to make it stop at the y-value of the mean. I have tried dividing the mean by the count etc.
Update for the latest versions of matplotlib (3.3.4) and seaborn (0.11.1): the kdeplot with shade=True now doesn't create a line object anymore. To get the same outcome as before, setting shade=False will still create the line object. The curve can then be filled with ax.fill_between(). The code below is changed accordingly. (Use the revision history to see the older version.)
ax.lines[0] gets the curve of the kde, of which you can extract the x and y data.
np.interp then can find the height of the curve for a given x-value:
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
x = np.random.normal(np.tile(np.random.uniform(10, 30, 5), 50), 3)
ax = sns.kdeplot(x, shade=False, color='crimson')
kdeline = ax.lines[0]
mean = x.mean()
xs = kdeline.get_xdata()
ys = kdeline.get_ydata()
height = np.interp(mean, xs, ys)
ax.vlines(mean, 0, height, color='crimson', ls=':')
ax.fill_between(xs, 0, ys, facecolor='crimson', alpha=0.2)
plt.show()
The same approach can be extended to show the mean together with the standard deviation, or the median and the quartiles:
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
x = np.random.normal(np.tile(np.random.uniform(10, 30, 5), 50), 3)
fig, axes = plt.subplots(ncols=2, figsize=(12, 4))
for ax in axes:
sns.kdeplot(x, shade=False, color='crimson', ax=ax)
kdeline = ax.lines[0]
xs = kdeline.get_xdata()
ys = kdeline.get_ydata()
if ax == axes[0]:
middle = x.mean()
sdev = x.std()
left = middle - sdev
right = middle + sdev
ax.set_title('Showing mean and sdev')
else:
left, middle, right = np.percentile(x, [25, 50, 75])
ax.set_title('Showing median and quartiles')
ax.vlines(middle, 0, np.interp(middle, xs, ys), color='crimson', ls=':')
ax.fill_between(xs, 0, ys, facecolor='crimson', alpha=0.2)
ax.fill_between(xs, 0, ys, where=(left <= xs) & (xs <= right), interpolate=True, facecolor='crimson', alpha=0.2)
# ax.set_ylim(ymin=0)
plt.show()
PS: for the mode of the kde:
mode_idx = np.argmax(ys)
ax.vlines(xs[mode_idx], 0, ys[mode_idx], color='lime', ls='--')
With plt.get_ylim() you can get the limits of the current plot: [bottom, top].
So, in your case, you can extract the actual limits and save them in ylim, then draw the line:
fig, ax = plt.subplots()
ylim = ax.get_ylim()
ax.plot([x.mean(),x.mean()], ax.get_ylim())
ax.set_ylim(ylim)
As ax.plot changes the ylims afterwards, you have to re-set them with ax.set_ylim as above.

Placing text around the circle

Using pyplot circle function I made a circle, then I have used text function to place the text(parameters) across the circle(PLOT ATTACHED) but the thing is if let's say I want to list out only 6 or say 11 parameters equally spaced across the circle I'll have to chage the coordinates as well as the rotation in text(and the coordinates and rotation value has been manually set). I want something that'll automate these things like given a number of parameter it will place parameter with equal spacing between them around the circle
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon
fig, ax = plt.subplots(figsize=(30, 20))
ax.axis('equal')
ax.set(xlim=(-10, 23), ylim = (-10, 10))
circle = plt.Circle((0, 0), 4.7, fc='#cfe2f3')
ax.add_patch(circle)
ax.text(-0.4, 4.9, 'npxG', fontsize=15)
ax.text(3.35, 3.5, 'xA', rotation=310, fontsize=15)
ax.text(4.8, -0.5, 'Shots on Target', rotation=270, fontsize=15)
ax.text(3.35, -3.55, 'Dribbles', rotation=50, fontsize=15)
ax.text(-1, -5., 'Through Balls', fontsize=15)
ax.text(-4.6, -3.6, 'Passes 1/3', rotation=305, fontsize=15)
ax.text(-5, -0.5, 'Key Passes', rotation=90, fontsize=15)
ax.text(-4., 3.3, 'Crosses', rotation=42, fontsize=15)
ax.axis('off')
Edit:
for i in range(0, len(data)):
a = points[i,2]
x,y = (radius*np.sin(a), radius*np.cos(a))
a = a - 0.5*np.pi
if points[i,1] < 0:
a = a - np.pi
ax.text(x, y, data[i], rotation = np.rad2deg(a), ha="center", va="center", fontsize=15)
On changing order of the array:
On flipping the x and y values:
Using code and inspiration from this question and answer and a bit of coordinate geometry:
import matplotlib.pyplot as plt
import numpy as np
fig, ax = plt.subplots(figsize=(30, 20))
ax.axis('equal')
ax.set(xlim=(-10, 10), ylim=(-10, 10))
circle = plt.Circle((0, 0), 2.7, fc='#cfe2f3')
ax.add_patch(circle)
def kex(N):
alpha=2*np.pi/N
alphas = alpha*np.arange(N)
coordX = np.cos(alphas)
coordY = np.sin(alphas)
return np.c_[coordX, coordY, alphas]
data = ["npxG", "xA", "Shots on Target", "Dribbles", "Through Balls",
"Passes 1/3", "Key Passes", "Crosses"]
radius = 3.2
points = kex(len(data))
for i in range(0, len(data)):
a = points[i,2]
x,y = (radius*np.cos(a), radius*np.sin(a))
if points[i,0] < 0:
a = a - np.pi
ax.text(x, y, data[i], ha="center", va="center", fontsize=15)
ax.axis("off")
plt.show()
Gives this:
If you wish to adapt something like the linked answer and rotate the labels as a perpendicular to the circle, change this line:
ax.text(x, y, data[i], rotation = np.rad2deg(a), ha="center", va="center", fontsize=15)
Note the added roatation parameter. This gives:
To adapt something like the sample image in the question:
for i in range(0, len(data)):
a = points[i,2]
x,y = (radius*np.cos(a), radius*np.sin(a))
a = a - 0.5*np.pi
if points[i,1] < 0:
a = a - np.pi
ax.text(x, y, data[i], rotation = np.rad2deg(a), ha="center", va="center", fontsize=15)
This gives:
The list data can be populated with label text. On changing the number of labels, the plot should adapt accordingly. The parameter radius adjusts the distance of the text from the center of the circle. You can add in extra parameters to the .text() function such as fontsize as required for the labels.
Note: View this answer on the SO white theme to see the labels clearly. I took the liberty to change the plot size to fit it here. Huge thanks to #ImportanceOfBeingErnest for the linked question's answer.

How to create an ax.legend() method for contourf plots that doesn't require passing of legend handles from a user?

Desired feature
I would like to be able to call
ax.legend()
on an axis containing a contourf plot and automatically get the legend (see plot below for an example).
More Detail
I know how to create legend entries for contourf plots using proxies, see code below and which is already discussed in this Q&A. However, I would be interested in a solution where the final call to axes[0][-1].legend() does not require any handles being passed.
The plot generation (more complex plots than in this example) is happening in a package and the user will have access to fig and axes and depending on the plots might prefer some axis over the others to plot the legend in. It would be nice if the call to ax.legend() could be simple and would not require the use of proxies and explicit passing of handles. This works automatically for normal plots, scatter plots, hists, etc., but contourf does not accept label as a kwarg and does not come with its own handle so I need to create a proxy (Rectangle patch in this case).
But how could I attach/attribute/... the proxy alongside a label to the contourf plot or to the axes such that ax.legend() can automatically access them the way it does for other types of plots?
Example Image
Example Code
import numpy as np
from scipy import stats
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
from matplotlib.colors import LinearSegmentedColormap
########################
# not accessed by User #
########################
def basic_cmap(color):
return LinearSegmentedColormap.from_list(color, ['#ffffff', color])
cmap1 = basic_cmap('C0')
cmap2 = basic_cmap('C1')
x = np.linspace(0, 10, 50)
mvn1 = stats.multivariate_normal(mean=[4, 4])
mvn2 = stats.multivariate_normal(mean=[6, 7])
X, Y = np.meshgrid(x, x)
Z1 = [[mvn1.pdf([x1, x2]) for x1 in x] for x2 in x]
Z2 = [[mvn2.pdf([x1, x2]) for x1 in x] for x2 in x]
Z1 = Z1 / np.max(Z1)
Z2 = Z2 / np.max(Z2)
fig, axes = plt.subplots(2, 2, sharex='col', sharey='row')
for i, row in enumerate(axes):
for j, ax in enumerate(row):
cont1 = ax.contourf(X, Y, Z1, [0.05, 0.33, 1], cmap=cmap1, alpha=0.7)
cont2 = ax.contourf(X, Y, Z2, [0.05, 0.33, 1], cmap=cmap2, alpha=0.7)
###################################
# User has access to fig and axes #
###################################
proxy1 = plt.Rectangle((0, 0), 1, 1, fc=cmap1(0.999), ec=cmap1(0.33), alpha=0.7, linewidth=3)
proxy2 = plt.Rectangle((0, 0), 1, 1, fc=cmap2(0.999), ec=cmap2(0.33), alpha=0.7, linewidth=3)
# would like this without passing of handles and labels
axes[0][-1].legend(handles=[proxy1, proxy2], labels=['foo', 'bar'])
plt.savefig("contour_legend.png")
plt.show()
Well, I dappled a bit more and found a solution after all that's surprisingly simple, but I had to dig much deeper into matplotlib.legend to get the right idea. In _get_legend_handles it shows how it collects the handles:
for ax in axs:
handles_original += (ax.lines + ax.patches +
ax.collections + ax.containers)
So all I was lacking was to pass the labels to the proxies and the proxies to ax.patches
Example Code with Solution
changes
# pass labels to proxies and place proxies in loop
proxy1 = plt.Rectangle((0, 0), 1, 1, fc=cmap1(0.999), ec=cmap1(0.33),
alpha=0.7, linewidth=3, label='foo')
proxy2 = plt.Rectangle((0, 0), 1, 1, fc=cmap2(0.999), ec=cmap2(0.33),
alpha=0.7, linewidth=3, label='bar')
# pass proxies to ax.patches
ax.patches += [proxy1, proxy2]
###################################
# User has access to fig and axes #
###################################
# no passing of handles and labels anymore
axes[0][-1].legend()
full code
import numpy as np
from scipy import stats
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
from matplotlib.colors import LinearSegmentedColormap
########################
# not accessed by User #
########################
def basic_cmap(color):
return LinearSegmentedColormap.from_list(color, ['#ffffff', color])
cmap1 = basic_cmap('C0')
cmap2 = basic_cmap('C1')
x = np.linspace(0, 10, 50)
mvn1 = stats.multivariate_normal(mean=[4, 4])
mvn2 = stats.multivariate_normal(mean=[6, 7])
X, Y = np.meshgrid(x, x)
Z1 = [[mvn1.pdf([x1, x2]) for x1 in x] for x2 in x]
Z2 = [[mvn2.pdf([x1, x2]) for x1 in x] for x2 in x]
Z1 = Z1 / np.max(Z1)
Z2 = Z2 / np.max(Z2)
fig, axes = plt.subplots(2, 2, sharex='col', sharey='row')
for i, row in enumerate(axes):
for j, ax in enumerate(row):
cont1 = ax.contourf(X, Y, Z1, [0.05, 0.33, 1], cmap=cmap1, alpha=0.7)
cont2 = ax.contourf(X, Y, Z2, [0.05, 0.33, 1], cmap=cmap2, alpha=0.7)
# pass labels to proxies and place proxies in loop
proxy1 = plt.Rectangle((0, 0), 1, 1, fc=cmap1(0.999), ec=cmap1(0.33),
alpha=0.7, linewidth=3, label='foo')
proxy2 = plt.Rectangle((0, 0), 1, 1, fc=cmap2(0.999), ec=cmap2(0.33),
alpha=0.7, linewidth=3, label='bar')
# pass proxies to ax.patches
ax.patches += [proxy1, proxy2]
###################################
# User has access to fig and axes #
###################################
# no passing of handles and labels anymore
axes[0][-1].legend()
plt.savefig("contour_legend_solved.png")
plt.show()
This produces the same image as shown in the question.
Sorry, was able to come up with a solution on my own after all, but maybe this will be helpful for someone else in the future.

Extending a line segment in matplotlib

Is there a function in matplotlib similar to MATLAB's line extensions?
I am basically looking for a way to extend a line segment to a plot. My current plot looks like this.
After looking at another question and applying the formula, I was able to get it to here, but it still looks messy.
Does anyone have the magic formula here?
Have a go to write your own as I don't think this exists in matplotlib. This is a start, you could improve by adding the semiinfinite etc
import matplotlib.pylab as plt
import numpy as np
def extended(ax, x, y, **args):
xlim = ax.get_xlim()
ylim = ax.get_ylim()
x_ext = np.linspace(xlim[0], xlim[1], 100)
p = np.polyfit(x, y , deg=1)
y_ext = np.poly1d(p)(x_ext)
ax.plot(x_ext, y_ext, **args)
ax.set_xlim(xlim)
ax.set_ylim(ylim)
return ax
ax = plt.subplot(111)
ax.scatter(np.linspace(0, 1, 100), np.random.random(100))
x_short = np.linspace(0.2, 0.7)
y_short = 0.2* x_short
ax = extended(ax, x_short, y_short, color="r", lw=2, label="extended")
ax.plot(x_short, y_short, color="g", lw=4, label="short")
ax.legend()
plt.show()
I just realised you have some red dots on your plots, are those important? Anyway the main point I think you solution so far is missing is to set the plot limits to those that existed before otherwise, as you have found, they get extended.
New in matplotlib 3.3
There is now an axline method to easily extend arbitrary lines:
Adds an infinitely long straight line. The line can be defined either by two points xy1 and xy2
plt.axline(xy1=(0, 1), xy2=(1, 0.5), color='r')
or defined by one point xy1 and a slope.
plt.axline(xy1=(0, 1), slope=-0.5, color='r')
Sample data for reference:
import numpy as np
import matplotlib.pyplot as plt
x, y = np.random.default_rng(123).random((2, 100)) * 2 - 1
m, b = -0.5, 1
plt.scatter(x, y, c=np.where(y > m*x + b, 'r', 'k'))

Categories