Legend for Lines with Different Endpoint Markers - python

I need to create a legend for a line segment that has a different marker at the bottom and the top of the line. I am able to create a legend with 1 of the marker symbols repeated but not the two different markers on each end.
Here is a reproducible example.
from matplotlib import pyplot as plt
import numpy as np
#Create some data
x = np.arange(0,11)
y1 = np.sqrt(x/2.)
y2 = x
plt.figure(figsize=(8,8))
ax = plt.subplot(111)
#Plot the lines
for i,x_ in zip(range(11),x):
ax.plot([x_,x_],[y1[i],y2[i]],c='k')
#Plot the end points
ax.scatter(x,y1,marker='s',c='r',s=100,zorder=10)
ax.scatter(x,y2,marker='o',c='r',s=100,zorder=10)
ax.plot([],[],c='k',marker='o',mfc='r',label='Test Range') #Create a single line for the label
ax.legend(loc=2,numpoints=2,prop={'size':16}) # How can I add a label with different symbols the line segments?
plt.show()
The end product should have a legend with a symbol showing a line connecting a circle and a square.

I'm afraid you have to combine different patches of mpatches, I'm not sure whether there is a better solution
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.patches as mpatches
from matplotlib.legend_handler import HandlerPatch
from matplotlib.legend_handler import HandlerLine2D
class HandlerCircle(HandlerPatch):
def create_artists(self,legend,orig_handle,
xdescent,ydescent,width,height,fontsize,trans):
center = 0.5 * width, 0.5 * height
p = mpatches.Circle(xy=center,radius=width*0.3)
self.update_prop(p,orig_handle,legend)
p.set_transform(trans)
return [p]
class HandlerRectangle(HandlerPatch):
def create_artists(self, legend, orig_handle,
xdescent, ydescent, width, height, fontsize, trans):
center = 0,height/2-width*0.5/2
width,height = width*0.5,width*0.5
p = mpatches.Rectangle(xy=center,width=width,height=width)
self.update_prop(p, orig_handle, legend)
p.set_transform(trans)
return [p]
fig,ax = plt.subplots(figsize=(12,8))
texts = ['','','Test Range']
line, = ax.plot([],[],c='k')
c = [mpatches.Circle((0.,0.,),facecolor='r',linewidth=.5),
line,
mpatches.Rectangle((0.,0.),5,5,facecolor='r',linewidth=.5)]
ax.legend(c,texts,bbox_to_anchor=(.25,.95),loc='center',ncol=3,prop={'size':20},
columnspacing=-1,handletextpad=.6,
handler_map={mpatches.Circle: HandlerCircle(),
line: HandlerLine2D(numpoints=0,),
mpatches.Rectangle: HandlerRectangle()}).get_frame().set_facecolor('w')
plt.show()
running this script, you will get
If you use a different figure size or a different legend size, the settings in my script above may not be optimal. In that case, you can adjust the following parameters:
The centers and the sizes of Circle and Rectangle
columnspacing and handletextpad in ax.legend(...)

Related

Placement of latitude labels in cartopy with polar stereographic projection

I am trying to figure out how to change the placement of gridline labels (more specifically, latitude labels) in cartopy when using a polar stereographic projection (NorthPolarStereo). My axis currently looks like this:
import matplotlib.pyplot as plt
import matplotlib.path as mpath
import numpy as np
import cartopy.crs as ccrs
# Helper function
# from https://nordicesmhub.github.io/NEGI-Abisko-2019/training/example_NorthPolarStereo_projection.html
def polarCentral_set_latlim(lat_lims, ax):
ax.set_extent([-180, 180, lat_lims[0], lat_lims[1]], ccrs.PlateCarree())
theta = np.linspace(0, 2*np.pi, 100)
center, radius = [0.5, 0.5], 0.5
verts = np.vstack([np.sin(theta), np.cos(theta)]).T
circle = mpath.Path(verts * radius + center)
ax.set_boundary(circle, transform=ax.transAxes)
fig = plt.figure(figsize=(6,6))
ax = fig.add_subplot(1,1,1,projection=ccrs.NorthPolarStereo(central_longitude=0))
ax.coastlines(linewidth=0.5,color='k')
ax.gridlines(color='C7',lw=1,ls=':',draw_labels=True,rotate_labels=False,ylocs=[60,70,80])
polarCentral_set_latlim((50,90),ax)
Oddly, the latitude labels are always plotted at about 150E even if the central_longitude is set to a different value. Preferably, I'd like to align them with the 180th meridian (similar to the labels in this plot) but I cannot find an option in the documentation of the gridlines function to set their position. Did I overlook something or would I have to place them manually with plt.text()
After the gridlines is created, some labels can be accessed and moved to new positions.
I use alternate method to define the circular boundary of the plot in order not to interfere with gridlines' labels.
import cartopy.crs as ccrs
import matplotlib.pyplot as plt
import matplotlib.path as mpath
import numpy as np
# Value for r_extent is obtained by trial and error
# get it with `ax.get_ylim()` after running this code
r_extent = 4651194.319
r_extent *= 1.005 #increase a bit for better result
# Projection settings
lonlat_proj = ccrs.PlateCarree()
use_proj = ccrs.NorthPolarStereo(central_longitude=0)
fig = plt.figure(figsize=[7, 7])
ax = plt.subplot(1, 1, 1, projection=use_proj)
ax.set_extent([-180, 180, 50, 90], lonlat_proj)
#ax.stock_img() # add bluemarble image
ax.coastlines(lw=0.5, color="black", zorder=20) # add coastlines
# draw graticule (meridian and parallel lines)
gls = ax.gridlines(draw_labels=True, crs=lonlat_proj, lw=1, color="gray",
y_inline=True, xlocs=range(-180,180,30), ylocs=range(0,90,10))
# set the plot limits
ax.set_xlim(-r_extent, r_extent)
ax.set_ylim(-r_extent, r_extent)
# Prep circular boundary
circle_path = mpath.Path.unit_circle()
circle_path = mpath.Path(circle_path.vertices.copy() * r_extent,
circle_path.codes.copy())
#set circular boundary
#this method will not interfere with the gridlines' labels
ax.set_boundary(circle_path)
ax.set_frame_on(False) #hide the boundary frame
plt.draw() # Enable the use of `gl._labels`
# Reposition the tick labels
# Labels at 150d meridian will be moved to 180d
for ea in gls._labels:
# No _labels if not run `plt.draw()`
pos = ea[2].get_position()
#print("Position:", pos[0], pos[1])
if (pos[0]==150):
ea[2].set_position([180, pos[1]])
plt.show()
Might because of the cartopy version (0.21.1) I used but the the answer by #swatchai didn't work for me. Yet a slight change can be done and will work fine.
plt.draw()
for ea in gls.label_artists:
# return ea of mpl.text type, e.g. Text(135, 30, '30°N')
pos = ea.get_position()
if pos[0] == 135:
ea.set_position([180, pos[1]])

Matplotlib customize the legend to show squares instead of rectangles

Here is my attempt to change the legend of a barplot from rectangle to square:
import matplotlib.patches as patches
rect1 = patches.Rectangle((0,0),1,1,facecolor='#FF605E')
rect2 = patches.Rectangle((0,0),1,1,facecolor='#64B2DF')
plt.legend((rect1, rect2), ('2016', '2015'))
But when I plot this, I still see rectangles instead of squares:
Any suggestions on how can I do this?
I tried both solutions provided by #ImportanceOfBeingErnest and #furas, here are the results:
#ImportanceOfBeingErnest's solution is the easiest to do:
plt.rcParams['legend.handlelength'] = 1
plt.rcParams['legend.handleheight'] = 1.125
Here is the result:
My final code looks like this:
plt.legend((df.columns[1], df.columns[0]), handlelength=1, handleheight=1) # the df.columns = the legend text
#furas's solution produces this, I don't know why the texts are further away from the rectangles, but I am sure the gap can be changed somehow:
Matplotlib provides the rcParams
legend.handlelength : 2. # the length of the legend lines in fraction of fontsize
legend.handleheight : 0.7 # the height of the legend handle in fraction of fontsize
You can set those within the call to plt.legend()
plt.legend(handlelength=1, handleheight=1)
or using the rcParams at the beginning of your script
import matplotlib
matplotlib.rcParams['legend.handlelength'] = 1
matplotlib.rcParams['legend.handleheight'] = 1
Unfortunately providing equal handlelength=1, handleheight=1 will not give a perfect rectange. It seems handlelength=1, handleheight=1.125 will do the job, but this may depend on the font being used.
An alternative, if you want to use proxy artists may be to use the square markers from the plot/scatter methods.
bar1 = plt.plot([], marker="s", markersize=15, linestyle="", label="2015")
and supply it to the legend, legend(handles=[bar1]). Using this approach needs to have set matplotlib.rcParams['legend.numpoints'] = 1, otherwise two markers would appear in the legend.
Here is a full example of both methods
import matplotlib.pyplot as plt
plt.rcParams['legend.handlelength'] = 1
plt.rcParams['legend.handleheight'] = 1.125
plt.rcParams['legend.numpoints'] = 1
fig, ax = plt.subplots(ncols=2, figsize=(5,2.5))
# Method 1: Set the handlesizes already in the rcParams
ax[0].set_title("Setting handlesize")
ax[0].bar([0,2], [6,3], width=0.7, color="#a30e73", label="2015", align="center")
ax[0].bar([1,3], [3,2], width=0.7, color="#0943a8", label="2016", align="center" )
ax[0].legend()
# Method 2: use proxy markers. (Needs legend.numpoints to be 1)
ax[1].set_title("Proxy markers")
ax[1].bar([0,2], [6,3], width=0.7, color="#a30e73", align="center" )
ax[1].bar([1,3], [3,2], width=0.7, color="#0943a8", align="center" )
b1, =ax[1].plot([], marker="s", markersize=15, linestyle="", color="#a30e73", label="2015")
b2, =ax[1].plot([], marker="s", markersize=15, linestyle="", color="#0943a8", label="2016")
ax[1].legend(handles=[b1, b2])
[a.set_xticks([0,1,2,3]) for a in ax]
plt.show()
producing
It seems they change it long time ago - and now some elements can't be used directly in legend.
Now it needs handler:
Implementing a custom legend handler
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from matplotlib.legend_handler import HandlerPatch
# --- handlers ---
class HandlerRect(HandlerPatch):
def create_artists(self, legend, orig_handle,
xdescent, ydescent, width, height,
fontsize, trans):
x = width//2
y = 0
w = h = 10
# create
p = patches.Rectangle(xy=(x, y), width=w, height=h)
# update with data from oryginal object
self.update_prop(p, orig_handle, legend)
# move xy to legend
p.set_transform(trans)
return [p]
class HandlerCircle(HandlerPatch):
def create_artists(self, legend, orig_handle,
xdescent, ydescent, width, height,
fontsize, trans):
r = 5
x = r + width//2
y = height//2
# create
p = patches.Circle(xy=(x, y), radius=r)
# update with data from oryginal object
self.update_prop(p, orig_handle, legend)
# move xy to legend
p.set_transform(trans)
return [p]
# --- main ---
rect = patches.Rectangle((0,0), 1, 1, facecolor='#FF605E')
circ = patches.Circle((0,0), 1, facecolor='#64B2DF')
plt.legend((rect, circ), ('2016', '2015'),
handler_map={
patches.Rectangle: HandlerRect(),
patches.Circle: HandlerCircle(),
})
plt.show()
Legend reserves place for rectangle and this method doesn't change it so there is so many empty space.

Dynamically updating a stacked bar plot in matplotlib

I would like to know how I can dynamically update a stacked bar plot in matplotlib.
This question Dynamically updating a bar plot in matplotlib describes how it can be done for a normal bar chart, but not a stacked bar chart.
In a normal bar chart the update can be done via rect.set_height(h) assuming that rects = plt.bar(range(N), x, align='center')
But in a stacked bar chart we also need to set the bottom.
p2 = plt.bar(ind, womenMeans, width, color='y',
bottom=menMeans, yerr=menStd)
How can I dynamically set the bottom? Unfortunately it seems that the 'Rectangle' object has no attribute 'set_bottom'. Is there any alternative way to handle this?
For some reason, the set_bottom() function you want is set_y under patches in the return object from bar. The minimal example, based on the link you suggest would look like,
import numpy as np
import matplotlib.pyplot as plt
def setup_backend(backend='TkAgg'):
import sys
del sys.modules['matplotlib.backends']
del sys.modules['matplotlib.pyplot']
import matplotlib as mpl
mpl.use(backend) # do this before importing pyplot
import matplotlib.pyplot as plt
return plt
N = 5
width = 0.35 # the width of the bars: can also be len(x) sequence
def animate():
# http://www.scipy.org/Cookbook/Matplotlib/Animations
mu, sigma = 100, 15
h = mu + sigma * np.random.randn((N*2))
p1 = plt.bar(np.arange(N), h[:N], width, color='r')
p2 = plt.bar(np.arange(N), h[N:], width, color='b', bottom=h[:N])
assert len(p1) == len(p2)
maxh = 0.
for i in range(50):
for rect1, rect2 in zip(p1.patches, p2.patches):
h = mu + sigma * np.random.randn(2)
#Keep a record of maximum value of h
maxh = max(h[0]+h[1],maxh)
rect1.set_height(h[0])
rect2.set_y(rect1.get_height())
rect2.set_height(h[1])
#Set y limits to maximum value
ax.set_ylim((0,maxh))
fig.canvas.draw()
plt = setup_backend()
fig, ax = plt.subplots(1,1)
win = fig.canvas.manager.window
win.after(10, animate)
plt.show()
Note, I change the height generation using random numbers each iteration so the two arrays of patches can be zipped instead (would get a bit messy otherwise).

How to plot one line in different colors

I have two list as below:
latt=[42.0,41.978567980875397,41.96622693388357,41.963791391892457,...,41.972407378075879]
lont=[-66.706920989908909,-66.703116557977069,-66.707351643324543,...-66.718218142021925]
now I want to plot this as a line, separate each 10 of those 'latt' and 'lont' records as a period and give it a unique color.
what should I do?
There are several different ways to do this. The "best" approach will depend mostly on how many line segments you want to plot.
If you're just going to be plotting a handful (e.g. 10) line segments, then just do something like:
import numpy as np
import matplotlib.pyplot as plt
def uniqueish_color():
"""There're better ways to generate unique colors, but this isn't awful."""
return plt.cm.gist_ncar(np.random.random())
xy = (np.random.random((10, 2)) - 0.5).cumsum(axis=0)
fig, ax = plt.subplots()
for start, stop in zip(xy[:-1], xy[1:]):
x, y = zip(start, stop)
ax.plot(x, y, color=uniqueish_color())
plt.show()
If you're plotting something with a million line segments, though, this will be terribly slow to draw. In that case, use a LineCollection. E.g.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection
xy = (np.random.random((1000, 2)) - 0.5).cumsum(axis=0)
# Reshape things so that we have a sequence of:
# [[(x0,y0),(x1,y1)],[(x0,y0),(x1,y1)],...]
xy = xy.reshape(-1, 1, 2)
segments = np.hstack([xy[:-1], xy[1:]])
fig, ax = plt.subplots()
coll = LineCollection(segments, cmap=plt.cm.gist_ncar)
coll.set_array(np.random.random(xy.shape[0]))
ax.add_collection(coll)
ax.autoscale_view()
plt.show()
For both of these cases, we're just drawing random colors from the "gist_ncar" coloramp. Have a look at the colormaps here (gist_ncar is about 2/3 of the way down): http://matplotlib.org/examples/color/colormaps_reference.html
Copied from this example:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection
from matplotlib.colors import ListedColormap, BoundaryNorm
x = np.linspace(0, 3 * np.pi, 500)
y = np.sin(x)
z = np.cos(0.5 * (x[:-1] + x[1:])) # first derivative
# Create a colormap for red, green and blue and a norm to color
# f' < -0.5 red, f' > 0.5 blue, and the rest green
cmap = ListedColormap(['r', 'g', 'b'])
norm = BoundaryNorm([-1, -0.5, 0.5, 1], cmap.N)
# Create a set of line segments so that we can color them individually
# This creates the points as a N x 1 x 2 array so that we can stack points
# together easily to get the segments. The segments array for line collection
# needs to be numlines x points per line x 2 (x and y)
points = np.array([x, y]).T.reshape(-1, 1, 2)
segments = np.concatenate([points[:-1], points[1:]], axis=1)
# Create the line collection object, setting the colormapping parameters.
# Have to set the actual values used for colormapping separately.
lc = LineCollection(segments, cmap=cmap, norm=norm)
lc.set_array(z)
lc.set_linewidth(3)
fig1 = plt.figure()
plt.gca().add_collection(lc)
plt.xlim(x.min(), x.max())
plt.ylim(-1.1, 1.1)
plt.show()
See the answer here to generate the "periods" and then use the matplotlib scatter function as #tcaswell mentioned. Using the plot.hold function you can plot each period, colors will increment automatically.
Cribbing the color choice off of #JoeKington,
import numpy as np
import matplotlib.pyplot as plt
def uniqueish_color(n):
"""There're better ways to generate unique colors, but this isn't awful."""
return plt.cm.gist_ncar(np.random.random(n))
plt.scatter(latt, lont, c=uniqueish_color(len(latt)))
You can do this with scatter.
I have been searching for a short solution how to use pyplots line plot to show a time series coloured by a label feature without using scatter due to the amount of data points.
I came up with the following workaround:
plt.plot(np.where(df["label"]==1, df["myvalue"], None), color="red", label="1")
plt.plot(np.where(df["label"]==0, df["myvalue"], None), color="blue", label="0")
plt.legend()
The drawback is you are creating two different line plots so the connection between the different classes is not shown. For my purposes it is not a big deal. It may help someone.

Updating a matplotlib bar graph?

I have a bar graph which retrieves its y values from a dict. Instead of showing several graphs with all the different values and me having to close every single one, I need it to update values on the same graph. Is there a solution for this?
Here is an example of how you can animate a bar plot.
You call plt.bar only once, save the return value rects, and then call rect.set_height to modify the bar plot.
Calling fig.canvas.draw() updates the figure.
import matplotlib
matplotlib.use('TKAgg')
import matplotlib.pyplot as plt
import numpy as np
def animated_barplot():
# http://www.scipy.org/Cookbook/Matplotlib/Animations
mu, sigma = 100, 15
N = 4
x = mu + sigma*np.random.randn(N)
rects = plt.bar(range(N), x, align = 'center')
for i in range(50):
x = mu + sigma*np.random.randn(N)
for rect, h in zip(rects, x):
rect.set_height(h)
fig.canvas.draw()
fig = plt.figure()
win = fig.canvas.manager.window
win.after(100, animated_barplot)
plt.show()
I've simplified the above excellent solution to its essentials, with more details at my blogpost:
import numpy as np
import matplotlib.pyplot as plt
numBins = 100
numEvents = 100000
file = 'datafile_100bins_100000events.histogram'
histogramSeries = np.loadtext(file)
fig, ax = plt.subplots()
rects = ax.bar(range(numBins), np.ones(numBins)*40) # 40 is upper bound of y-axis
for i in range(numEvents):
for rect,h in zip(rects,histogramSeries[i,:]):
rect.set_height(h)
fig.canvas.draw()
plt.pause(0.001)

Categories

Resources