I'm plotting 3 things at once: a multicolored line via a LineCollection (following this) and a scatter (for the markers to "cover" where the lines are joining) for an average value, and a fill_between for min/max. I get all the legend returns to plot a single legend handle. The graph looks like this:
As one can note, the circle marker is not aligned with the line. How can I adjust this?
The piece of the code that is plotting them and the legend looks like:
lc = LineCollection(segments, cmap='turbo',zorder=3)
p1 = ax.add_collection(lc)
p2 = ax.fill_between(x, errmin,errmax, color=colors[1],zorder=2)
ps = ax.scatter(x,y,marker='o',s=1,c=y,cmap='turbo',zorder=4)
ax.legend([(p2, p1, ps)], ["(min/avg/max)"],fontsize=tinyfont, facecolor='white', loc='lower right')
The legend has a parameter scatteryoffsets= which defaults to [0.375, 0.5, 0.3125]. As only one point is shown, setting it to [0.5] should show the dot in the center of the legend marker.
To change the color of the line in the legend, one could create a copy of the existing line, change its color and create the legend with that copy.
import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection
import numpy as np
from copy import copy
x = np.arange(150)
y = np.random.randn(150).cumsum()
y -= y.min()
y /= y.max()
errmin = 0
errmax = 1
segments = np.array([x[:-1], y[:-1], x[1:], y[1:]]).T.reshape(-1, 2, 2)
fig, ax = plt.subplots(figsize=(12, 3))
lc = LineCollection(segments, cmap='turbo', zorder=3)
lc.set_array(y[:-1])
p1 = ax.add_collection(lc)
p2 = ax.fill_between(x, errmin, errmax, color='lightblue', zorder=2, alpha=0.4)
ps = ax.scatter(x, y, marker='o', s=5, color='black', zorder=4)
p1copy = copy(p1)
p1copy.set_color('crimson')
leg = ax.legend([(p2, p1copy, ps)], ["(min/avg/max)"], fontsize=10, facecolor='white', loc='lower right',
scatteryoffsets=[0.5])
ax.margins(x=0.02)
plt.show()
Related
I can create a scatter plot as follows:
fig, ax = plt.subplots()
x1 = [1, 1, 2]
y1 = [1, 2, 1]
x2 = [2]
y2 = [2]
ax.scatter(x1, y1, color="red", s=500)
ax.scatter(x2, y2, color="blue", s=500)
which gives
What I would like is something like the following (apologies for poor paint work):
I am plotting data that is all integer values, so they're all on a grid. I would like to be able to control the size of the scatter marker so that I could have white space around the points, or I could make the points large enough such that there would be no white space around them (as I have done in the above paint image).
Note - ideally the solution will be in pure matplotlib, using the OOP interface as they suggest in the documentation.
import matplotlib.pyplot as plt
import matplotlib as mpl
# X and Y coordinates for red circles
red_xs = [1,2,3,4,1,2,3,4,1,2,1,2]
red_ys = [1,1,1,1,2,2,2,2,3,3,4,4]
# X and Y coordinates for blue circles
blu_xs = [3,4,3,4]
blu_ys = [3,3,4,4]
# Plot with a small markersize
markersize = 5
fig, ax = plt.subplots(figsize=(3,3))
ax.plot(red_xs, red_ys, marker="o", color="r", linestyle="", markersize=markersize)
ax.plot(blu_xs, blu_ys, marker="o", color="b", linestyle="", markersize=markersize)
plt.show()
# Plot with a large markersize
markersize = 50
fig, ax = plt.subplots(figsize=(3,3))
ax.plot(red_xs, red_ys, marker="o", color="r", linestyle="", markersize=markersize)
ax.plot(blu_xs, blu_ys, marker="o", color="b", linestyle="", markersize=markersize)
plt.show()
# Plot with using patches and radius
r = 0.5
fig, ax = plt.subplots(figsize=(3,3))
for x, y in zip(red_xs, red_ys):
ax.add_patch(mpl.patches.Circle((x,y), radius=r, color="r"))
for x, y in zip(blu_xs, blu_ys):
ax.add_patch(mpl.patches.Circle((x,y), radius=r, color="b"))
ax.autoscale()
plt.show()
Usually two y-axes are kept apart with different colors, as shown in the example below.
For publications it's often necessary to keep it distinguishable, even when it's printed in black and white.
This is usually done by plotting circles around a line, which have an arrow in the direction of the corresponding axis attached.
How can this be achieved with matplotlib? Or is there a better way to accomplish black and white readability without those circles?
Code from matplotlib.org:
import numpy as np
import matplotlib.pyplot as plt
# Create some mock data
t = np.arange(0.01, 10.0, 0.01)
data1 = np.exp(t)
data2 = np.sin(2 * np.pi * t)
fig, ax1 = plt.subplots()
color = 'tab:red'
ax1.set_xlabel('time (s)')
ax1.set_ylabel('exp', color=color)
ax1.plot(t, data1, color=color)
ax1.tick_params(axis='y', labelcolor=color)
ax2 = ax1.twinx() # instantiate a second axes that shares the same x-axis
color = 'tab:blue'
ax2.set_ylabel('sin', color=color) # we already handled the x-label with ax1
ax2.plot(t, data2, color=color)
ax2.tick_params(axis='y', labelcolor=color)
fig.tight_layout() # otherwise the right y-label is slightly clipped
plt.show()
This approach is based on this answer. It uses arc, which can be configured as follows:
import matplotlib.pyplot as plt
from matplotlib.patches import Arc
# Generate example graph
fig = plt.figure(figsize=(5, 5))
ax = fig.add_subplot(1, 1, 1)
ax.plot([1,2,3,4,5,6], [2,4,6,8,10,12])
# Configure arc
center_x = 2 # x coordinate
center_y = 3.8 # y coordinate
radius_1 = 0.25 # radius 1
radius_2 = 1 # radius 2 >> for cicle: radius_2 = 2 x radius_1
angle = 180 # orientation
theta_1 = 70 # arc starts at this angle
theta_2 = 290 # arc finishes at this angle
arc = Arc([center_x, center_y],
radius_1,
radius_2,
angle = angle,
theta1 = theta_1,
theta2=theta_2,
capstyle = 'round',
linestyle='-',
lw=1,
color = 'black')
# Add arc
ax.add_patch(arc)
# Add arrow
x1 = 1.9 # x coordinate
y1 = 4 # y coordinate
length_x = -0.5 # length on the x axis (negative so the arrow points to the left)
length_y = 0 # length on the y axis
ax.arrow(x1,
y1,
length_x,
length_y,
head_width=0.1,
head_length=0.05,
fc='k',
ec='k',
linewidth = 0.6)
The result is shown below:
You can use matplotlib's axes annotate to draw arrows to the y-axes. You will need to find the points in the plot where the arrows should start. However, this does not plot circles around lines. If you really want to plot a circle, you could use plt.scatter or plt.Circle to plot an appropriate circle covering the relevant area.
import numpy as np
import matplotlib.pyplot as plt
# Create some mock data
t = np.arange(0.01, 10.0, 0.01)
data1 = np.exp(t)
data2 = np.sin(2 * np.pi * t)
fig, ax1 = plt.subplots()
color = 'tab:red'
ax1.set_xlabel('time (s)')
ax1.set_ylabel('exp', color=color)
ax1.plot(t, data1, color=color)
ax1.tick_params(axis='y', labelcolor=color)
ax1.annotate('', xy=(7, 1096), xytext=(-0.5, 1096), # start the arrow from x=7 and draw towards primary y-axis
arrowprops=dict(arrowstyle="<-", color=color))
ax2 = ax1.twinx() # instantiate a second axes that shares the same x-axis
color = 'tab:blue'
ax2.set_ylabel('sin', color=color) # we already handled the x-label with ax1
ax2.plot(t, data2, color=color)
ax2.tick_params(axis='y', labelcolor=color)
# plt.arrow()
ax2.annotate('', xy=(6,0), xytext=(10.4, 0), # start the arrow from x=6 and draw towards secondary y-axis
arrowprops=dict(arrowstyle="<-", color=color))
fig.tight_layout() # otherwise the right y-label is slightly clipped
plt.show()
Following is the sample output figure.
EDIT: Following is the snippet with the circles you've requested. I have used plt.scatter.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Circle
# Create some mock data
t = np.arange(0.01, 10.0, 0.01)
data1 = np.exp(t)
data2 = np.sin(2 * np.pi * t)
fig, ax1 = plt.subplots()
color = 'tab:red'
ax1.set_xlabel('time (s)')
ax1.set_ylabel('exp', color=color)
ax1.plot(t, data1, color=color)
ax1.tick_params(axis='y', labelcolor=color)
ax1.annotate('', xy=(7, 1096), xytext=(-0.5, 1096), # start the arrow from x=7 and draw towards primary y-axis
arrowprops=dict(arrowstyle="<-", color=color))
# circle1 = Circle((5, 3000), color='r')
# ax1.add_artist(circle1)
plt.scatter(7, 1096, s=100, facecolors='none', edgecolors='r')
ax2 = ax1.twinx() # instantiate a second axes that shares the same x-axis
color = 'tab:blue'
ax2.set_ylabel('sin', color=color) # we already handled the x-label with ax1
ax2.plot(t, data2, color=color)
ax2.tick_params(axis='y', labelcolor=color)
# plt.arrow()
ax2.annotate('', xy=(6.7,0), xytext=(10.5, 0), # start the arrow from x=6.7 and draw towards secondary y-axis
arrowprops=dict(arrowstyle="<-", color=color))
plt.scatter(6,0, s=2000, facecolors='none', edgecolors=color)
fig.tight_layout() # otherwise the right y-label is slightly clipped
plt.savefig('fig')
plt.show()
Here is the sample output.
In pyplot, you can change the order of different graphs using the zorder option or by changing the order of the plot() commands. However, when you add an alternative axis via ax2 = twinx(), the new axis will always overlay the old axis (as described in the documentation).
Is it possible to change the order of the axis to move the alternative (twinned) y-axis to background?
In the example below, I would like to display the blue line on top of the histogram:
import numpy as np
import matplotlib.pyplot as plt
import random
# Data
x = np.arange(-3.0, 3.01, 0.1)
y = np.power(x,2)
y2 = 1/np.sqrt(2*np.pi) * np.exp(-y/2)
data = [random.gauss(0.0, 1.0) for i in range(1000)]
# Plot figure
fig = plt.figure()
ax1 = fig.add_subplot(111)
ax2 = ax1.twinx()
ax2.hist(data, bins=40, normed=True, color='g',zorder=0)
ax2.plot(x, y2, color='r', linewidth=2, zorder=2)
ax1.plot(x, y, color='b', linewidth=2, zorder=5)
ax1.set_ylabel("Parabola")
ax2.set_ylabel("Normal distribution")
ax1.yaxis.label.set_color('b')
ax2.yaxis.label.set_color('r')
plt.show()
Edit: For some reason, I am unable to upload the image generated by this code. I will try again later.
You can set the zorder of an axes, ax.set_zorder(). One would then need to remove the background of that axes, such that the axes below is still visible.
ax2 = ax1.twinx()
ax1.set_zorder(10)
ax1.patch.set_visible(False)
Is it possible to color axis spine with multiple colors using matplotlib in python?
Desired output style:
You can use a LineCollection to create a multicolored line. You can then use the xaxis-transform to keep it fixed to the xaxis, independent of the y-limits. Setting the actual spine invisible and turning clip_on off makes the LineCollection look like the axis spine.
import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection
import numpy as np
fig, ax = plt.subplots()
colors=["b","r","lightgreen","gold"]
x=[0,.25,.5,.75,1]
y=[0,0,0,0,0]
points = np.array([x, y]).T.reshape(-1, 1, 2)
segments = np.concatenate([points[:-1], points[1:]], axis=1)
lc = LineCollection(segments,colors=colors, linewidth=2,
transform=ax.get_xaxis_transform(), clip_on=False )
ax.add_collection(lc)
ax.spines["bottom"].set_visible(False)
ax.set_xticks(x)
plt.show()
Here is a slightly different solution. If you don't want to recolor the complete axis, you can use zorder to make sure the colored line segments are visible on top of the original axis.
After drawing the main plot:
save the x and y limits
draw a horizontal line at ylims[0] between the chosen x-values with the desired color
clipping should be switched off to allow the line to be visible outside the strict plot area
zorder should be high enough to put the new line in front of the axes
the saved x and y limits need to be put back, because drawing extra lines moved them (alternatively, you might have turned off autoscaling the axes limits by calling plt.autoscale(False) before drawing the colored axes)
from matplotlib import pyplot as plt
import numpy as np
x = np.linspace(0, 20, 100)
for i in range(10):
plt.plot(x, np.sin(x*(1-i/50)), c=plt.cm.plasma(i/12))
xlims = plt.xlim()
ylims = plt.ylim()
plt.hlines(ylims[0], 0, 10, color='limegreen', lw=1, zorder=4, clip_on=False)
plt.hlines(ylims[0], 10, 20, color='crimson', lw=1, zorder=4, clip_on=False)
plt.vlines(xlims[0], -1, 0, color='limegreen', lw=1, zorder=4, clip_on=False)
plt.vlines(xlims[0], 0, 1, color='crimson', lw=1, zorder=4, clip_on=False)
plt.xlim(xlims)
plt.ylim(ylims)
plt.show()
To highlight an area on the x-axis, also axvline or axvspan can be interesting. An example:
from matplotlib import pyplot as plt
import numpy as np
x = np.linspace(0, 25, 100)
for i in range(10):
plt.plot(x, np.sin(x)*(1-i/20), c=plt.cm.plasma(i/12))
plt.axvspan(10, 20, color='paleturquoise', alpha=0.5)
plt.show()
In Jfreechart there is a method called setQuadrantPaint which let's you set the background colour of a given quandrant in a plot.
How would you achieve the equivalent in matplotlib?
E.g.
You can plot a 2x2 array with imshow in the background. Giving it an extent will make the center of it always at 0,0.
import numpy as np
import matplotlib.pyplot as plt
x1, y1 = np.random.randint(-8,8,5), np.random.randint(-8,8,5)
x2, y2 = np.random.randint(-8,8,5), np.random.randint(-8,8,5)
vmax = np.abs(np.concatenate([x1,x2,y1,y2])).max() + 5
extent = [vmax*-1,vmax, vmax*-1,vmax]
arr = np.array([[1,0],[0,1]])
fig, ax = plt.subplots(1,1)
ax.scatter(x1,y1, marker='s', s=30, c='r', edgecolors='red', lw=1)
ax.scatter(x2,y2, marker='s', s=30, c='none', edgecolors='red', lw=1)
ax.autoscale(False)
ax.imshow(arr, extent=extent, cmap=plt.cm.Greys, interpolation='none', alpha=.1)
ax.axhline(0, color='grey')
ax.grid(True)
Setting the autoscale to False after the data points are plotted, but before the image is, makes sure that the axes scales only to the data points.