Creating Matplotlib Graph with common line across subplots in Python - python

For an upcoming assignment I am require to make a series of diagrams that have two graphs that have a line going across from one graph to the other, colouring an area below that line on the other graph.
As shown in this rough drawing:
This is what I currently have:
From this code:
from matplotlib import pyplot as plt
fig = plt.figure()
ax1 = fig.add_subplot(121)
ax1.plot([0,1,2,3,4,5,6,7,8,9,10], [1,1,1,1,1,0,0,0,0,0,0], '-b')
ax1.plot([0,1,2,3,4,5,6,7,8,9,10], [0,0,0,0,0,1,1,1,1,1,1], '-r')
ax1.set_ylim([0, 1.2])
ax2 = fig.add_subplot(122)
ax2.plot([0,5,10,15,20,25,30,35,40], [1,1,1,1,0,0,0,0,0], '-b')
ax2.plot([0,5,10,15,20,25,30,35,40], [0,0,0,0,1,1,1,1,1], '-r')
ax2.set_ylim([0, 1.2])
plt.show()
Obviously this only generates the two graphs and I have yet been unable to add the line across the two graphs.
I really want to be able to do this with Matplotlib in python with the ability to change the value (45 in the example case) and the coloured area change automatically.
Thanks!

There are three steps: 1st, find the intercept point between green and blue lines in the left panel. 2nd, find the intercept point between the red and lines in the right panel. 3rd, fill the area between. These steps involves np.interp scipy.interpolat scipy.optimize and plt.fill_between, which you should look up.
from matplotlib import pyplot as plt
import numpy as np
import scipy.interpolate as spinp
import scipy.optimize as spop
fig = plt.figure(figsize=(16,4))
ax1 = fig.add_subplot(121)
x = [0,10,20,30,40,50,60,70,80,90,100]
yr = [1,1,1,1,1,0,0,0,0,0,0]
yg = [0,0,0,0,0,1,1,1,1,1,1]
turn_pt = np.interp(45, x, yr) #change 45 to whatever.
yb = [0.,turn_pt,turn_pt]
ax1.plot(x, yr, '-r')
ax1.plot(x, yg, '-g')
xb = [45, 45, 200]
ll = plt.plot(xb,yb, '-b')
ll[0].set_clip_on(False)
plt.axis([0,100,0,1.2])
#the above three lines to draw the line out of the box.
ax2 = fig.add_subplot(122)
yr = [1,1,1,1,0,0,0,0,0]
yg = [0,0,0,0,1,1,1,1,1]
x = [0,5,10,15,20,25,30,35,40]
brk_pt_f = lambda X, V: (spinp.interp1d(x, yr)(X)-V)**2
brk_pt = spop.fmin(brk_pt_f, 17., args=(turn_pt,), disp=0) #17. is you inital guess,
#the above two lines solve for the intersection between the blue line and the red line
zero_pt = 20.
start_pt= 0.
xb = np.hstack((start_pt, brk_pt, zero_pt))
yb = [turn_pt,turn_pt,0]
ax2.plot(x, yr, '-r')
ax2.plot(x, yg, '-g')
ax2.plot(xb, yb, '-b')
ax2.hlines(turn_pt,0, 40, 'b', alpha=0.)
ax2.fill_between(xb, yb, 0, alpha=0.4)
ax2.set_ylim([0, 1.2])
ax2.set_xlim([0, 40])
There are a few solutions to get rid of the top x-axis and the right y-axis, please search older SO posts.
And finally, welcome to SO.

Related

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.

Polar plot - Put one grid line in bold

I am trying to make use the polar plot projection to make a radar chart. I would like to know how to put only one grid line in bold (while the others should remain standard).
For my specific case, I would like to highlight the gridline associated to the ytick "0".
from matplotlib import pyplot as plt
import pandas as pd
import numpy as np
#Variables
sespi = pd.read_csv("country_progress.csv")
labels = sespi.country
progress = sespi.progress
angles=np.linspace(0, 2*np.pi, len(labels), endpoint=False)
#Concatenation to close the plots
progress=np.concatenate((progress,[progress[0]]))
angles=np.concatenate((angles,[angles[0]]))
#Polar plot
fig=plt.figure()
ax = fig.add_subplot(111, polar=True)
ax.plot(angles, progress, '.--', linewidth=1, c="g")
#ax.fill(angles, progress, alpha=0.25)
ax.set_thetagrids(angles * 180/np.pi, labels)
ax.set_yticklabels([-200,-150,-100,-50,0,50,100,150,200])
#ax.set_title()
ax.grid(True)
plt.show()
The gridlines of a plot are Line2D objects. Therefore you can't make it bold. What you can do (as shown, in part, in the other answer) is to increase the linewidth and change the colour but rather than plot a new line you can do this to the specified gridline.
You first need to find the index of the y tick labels which you want to change:
y_tick_labels = [-100,-10,0,10]
ind = y_tick_labels.index(0) # find index of value 0
You can then get a list of the gridlines using gridlines = ax.yaxis.get_gridlines(). Then use the index you found previously on this list to change the properties of the correct gridline.
Using the example from the gallery as a basis, a full example is shown below:
r = np.arange(0, 2, 0.01)
theta = 2 * np.pi * r
ax = plt.subplot(111, projection='polar')
ax.set_rmax(2)
ax.set_rticks([0.5, 1, 1.5, 2]) # less radial ticks
ax.set_rlabel_position(-22.5) # get radial labels away from plotted line
ax.grid(True)
y_tick_labels = [-100, -10, 0, 10]
ax.set_yticklabels(y_tick_labels)
ind = y_tick_labels.index(0) # find index of value 0
gridlines = ax.yaxis.get_gridlines()
gridlines[ind].set_color("k")
gridlines[ind].set_linewidth(2.5)
plt.show()
Which gives:
It is just a trick, but I guess you could just plot a circle and change its linewidth and color to whatever could be bold for you.
For example:
import matplotlib.pyplot as plt
import numpy as np
Yline = 0
Npoints = 300
angles = np.linspace(0,360,Npoints)*np.pi/180
line = 0*angles + Yline
ax = plt.subplot(111, projection='polar')
plt.plot(angles, line, color = 'k', linewidth = 3)
plt.ylim([-1,1])
plt.grid(True)
plt.show()
In this piece of code, I plot a line using plt.plot between any point of the two vectors angles and line. The former is actually all the angles between 0 and 2*np.pi. The latter is constant, and equal to the 'height' you want to plot that line Yline.
I suggest you try to decrease and increase Npoints while having a look to the documentaion of np.linspace() in order to understand your problem with the roundness of the circle.

How to draw a line through a scatter graph with no overflow

So I am currently plotting a scatter graph with many x and ys in matplotlib:
plt.scatter(x, y)
I want to draw a line on this scatter graph that crosses through the whole graph (i.e hits two 'borders') I know the gradient and the intercept - m and the c in the equation y = mx +c.
I have thought about acquiring the 4 points of the plot (calculating the min and max scatter x and ys) and from that calculating the min and max coords for the line and then plotting but that seems very convoluted. Is there any better way to do this bearing in mind the line may not even be 'within' the 'plot'?
Example of scatter graph:
as identified visually in the plot the four bordering coordinates are ruffly:
bottom left: -1,-2
top left: -1,2
bottom right: 6,-2
top right 6,2
I now have a line that I need to plot that must not exceed these boundaries but if it enters the plot must touch two of the boundary points.
So I could check what y equals when x = -1 and then check if that value is between -1 and 6 and if it is the line must cross the left border, so plot it, and so on and so fourth.
Ideally though I would create a line from -infinity to infinity and then crop it to fit the plot.
The idea here is to draw a line of some equation y=m*x+y0 into the plot. This can be achieved by transforming a horizontal line, originally given in axes coordinates, into data coordinates, applying the Affine2D transform according to the line equation and transforming back to screen coordinates.
The advantage here is that you do not need to know the axes limits at all. You may also freely zoom or pan your plot; the line will always stay within the axes boundaries. It hence effectively implements a line ranging from -infinity to + inifinty.
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.transforms as mtransforms
def axaline(m,y0, ax=None, **kwargs):
if not ax:
ax = plt.gca()
tr = mtransforms.BboxTransformTo(
mtransforms.TransformedBbox(ax.viewLim, ax.transScale)) + \
ax.transScale.inverted()
aff = mtransforms.Affine2D.from_values(1,m,0,0,0,y0)
trinv = ax.transData
line = plt.Line2D([0,1],[0,0],transform=tr+aff+trinv, **kwargs)
ax.add_line(line)
x = np.random.rand(20)*6-0.7
y = (np.random.rand(20)-.5)*4
c = (x > 3).astype(int)
fig, ax = plt.subplots()
ax.scatter(x,y, c=c, cmap="bwr")
# draw y=m*x+y0 into the plot
m = 0.4; y0 = -1
axaline(m,y0, ax=ax, color="limegreen", linewidth=5)
plt.show()
While this solution looks a bit complicated on first sight, one does not need to fully understand it. Just copy the axaline function to your code and use it as it is.
In order to get the automatic updating working without the transforms doing this, one may add callbacks which would reset the transform every time something changes in the plot.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import transforms
class axaline():
def __init__(self, m,y0, ax=None, **kwargs):
if not ax: ax = plt.gca()
self.ax = ax
self.aff = transforms.Affine2D.from_values(1,m,0,0,0,y0)
self.line = plt.Line2D([0,1],[0,0], **kwargs)
self.update()
self.ax.add_line(self.line)
self.ax.callbacks.connect('xlim_changed', self.update)
self.ax.callbacks.connect('ylim_changed', self.update)
def update(self, evt=None):
tr = ax.transAxes - ax.transData
trinv = ax.transData
self.line.set_transform(tr+self.aff+trinv)
x = np.random.rand(20)*6-0.7
y = (np.random.rand(20)-.5)*4
c = (x > 3).astype(int)
fig, ax = plt.subplots()
ax.scatter(x,y, c=c, cmap="bwr")
# draw y=m*x+y0 into the plot
m = 0.4; y0 = -1
al = axaline(m,y0, ax=ax, color="limegreen", linewidth=5)
plt.show()
You may try:
import matplotlib.pyplot as plt
import numpy as np
m=3
c=-2
x1Data= np.random.normal(scale=2, loc=.4, size=25)
y1Data= np.random.normal(scale=3, loc=1.2, size=25)
x2Data= np.random.normal(scale=1, loc=3.4, size=25)
y2Data= np.random.normal(scale=.65, loc=-.2, size=25)
fig = plt.figure()
ax = fig.add_subplot( 1, 1, 1 )
ax.scatter(x1Data, y1Data)
ax.scatter(x2Data, y2Data)
ylim = ax.get_ylim()
xlim = ax.get_xlim()
ax.plot( xlim, [ m * x + c for x in xlim ], 'r:' )
ax.set_ylim( ylim )
ax.set_xlim( xlim )
plt.show()
which gives:

How to add hierarchical axis across subplots in order to label groups?

I am having a set of different times series which can be grouped. E.g. the plot below shows series A, B, C and D. However, A and B are in group G1 and C and D are in group G2.
I would like to reflect that in the plot by adding another axis on the left which goes across groups of turbines and label thes axis accordingly.
I've tried a few thing so far but apparently that one's not so easy.
Does some body know how I can do that?
PS: Since I am using panda's plot(subplots=True) on a data frame which has already columns
| G1 | G2 |
|-------|------|
index | A B | C D |
------|-------|------|
it might be that pandas can do that already for me. That's why I am using the pandas tag.
You can create additional axes in the plot, which span each two plots but only have a left y-axis, no ticks and other decorations. Only a ylabel is set. This will make the whole thing look well aligned.
The good thing is that you can work with your existing pandas plot. The drawback is that is more than 15 lines of code.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
df = pd.DataFrame(np.random.rand(26,4), columns=list("ABCD"))
axes = df.plot(subplots=True)
fig = axes[0].figure
gs = gridspec.GridSpec(4,2)
gs.update(left=0.1, right=0.48, wspace=0.05)
fig.subplots_adjust(left=.2)
for i, ax in enumerate(axes):
ax.set_subplotspec(gs[i,1])
aux1 = fig.add_subplot(gs[:2,0])
aux2 = fig.add_subplot(gs[2:,0])
aux1.set_ylabel("G1")
aux2.set_ylabel("G2")
for ax in [aux1, aux2]:
ax.tick_params(size=0)
ax.set_xticklabels([])
ax.set_yticklabels([])
ax.set_facecolor("none")
for pos in ["right", "top", "bottom"]:
ax.spines[pos].set_visible(False)
ax.spines["left"].set_linewidth(3)
ax.spines["left"].set_color("crimson")
plt.show()
Here is an example I came up with. Since you did not provide your code, I did it without pandas, because I am not proficient with it.
You basically plot as one would and then create another axis around all your previous ones, remove its axis with ax5.axis('off') and plot the 2 lines and text on it.
from matplotlib import lines
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 4*np.pi, 100)
y1 = np.sin(x)
y2 = np.cos(x)
y3 = np.tan(x)
y4 = np.cos(x)/(x+1)
fig = plt.figure()
fig.subplots_adjust(hspace=.5)
ax1 = plt.subplot(411)
ax1.plot(x, y1)
ax2 = plt.subplot(412)
ax2.plot(x, y2)
ax3 = plt.subplot(413)
ax3.plot(x, y3)
ax4 = plt.subplot(414)
ax4.plot(x, y4)
# new axis around the others with 0-1 limits
ax5 = plt.axes([0, 0, 1, 1])
ax5.axis('off')
line_x1, line_y1 = np.array([[0.05, 0.05], [0.05, 0.5]])
line1 = lines.Line2D(line_x1, line_y1, lw=2., color='k')
ax5.add_line(line1)
line_x2, line_y2 = np.array([[0.05, 0.05], [0.55, 0.9]])
line2 = lines.Line2D(line_x2, line_y2, lw=2., color='k')
ax5.add_line(line2)
ax5.text(0.0, 0.75, "G1")
ax5.text(0.0, 0.25, "G2")
plt.show()
Inspired by How to draw a line outside of an axis in matplotlib (in figure coordinates)?

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