Matplotlib legend makes the image too large - python

I'm plotting this figure with matplotlib, the for loop just color the background:
fig, ax = plt.subplots()
ax.set_ylabel('Number of contacts')
ax.set_xlabel('Time [s]')
for m in range(len(data[node])):
if data[node][m] == -1:
ax.axvline(m,color='r',linewidth=5,alpha=0.2,label="OUT")
if data[node][m] == 0:
ax.axvline(m,color='g',linewidth=5,alpha=0.2,label="RZ0")
if data[node][m] == 1:
ax.axvline(m,color='y',linewidth=5,alpha=0.2,label="RZ1")
ax.plot(x, y, 'b+')
# ax.legend() # HERE is the problem
plt.show()
Which plots the following:
What I want now is a legend to indicate each color of the background meaning, but when I include ax.legend() I get the following error:
ValueError: Image size of 392x648007 pixels is too large. It must be less than 2^16 in each
direction.
<Figure size 432x288 with 1 Axes>
<Figure size 432x288 with 0 Axes>
How am I supposed to name each color of the background, there are 43200 vertical lines but only 3 colors, does it have anything to do with the number of lines?

The trick is to set the label only once. You can add a variable for each label and replace it with None once it's used. Note that using axvline to draw a background has the problem that the line width is measured in pixel space, so neighboring lines will either overlap or have a small white space inbetween. Better to use axvspan. To avoid the white space at the left and at the right, you can explicitly set the x-limits.
The code can be simplified somewhat using a loop.
Updated code:
group consecutive spans together for drawing
precalculate the effect of alpha so the background can be drawn without the need for transparency
from matplotlib import pyplot as plt
from matplotlib import colors as mcolors
import numpy as np
import pandas as pd
import itertools
fig, ax = plt.subplots()
# create some random data
x = np.arange(100)
y = np.sinh(x/20)
indicators = [-1, 0, 1]
node = 0
data = [np.random.choice(indicators, len(x), p=[10/16,1/16,5/16])]
labels = ["OUT", "RZ0", "RZ1"]
colors = ['lime', 'purple', 'gold']
alpha = 0.4
# precalculate the effect of alpha so the colors can be applied with alpha=1
colors = [[1 + (x - 1) * alpha for x in mcolors.to_rgb(c)] for c in colors]
m = 0
for val, group in itertools.groupby(data[node]):
width = len(list(group))
ind = indicators.index(val)
ax.axvspan(m, m + width, color=colors[ind], linewidth=0, alpha=1, label=labels[ind])
labels[ind] = None # reset the label to make sure it is only used once
m += width
ax.plot(x, y, 'b+')
ax.set_xlim(0, len(data[node]))
ax.legend(framealpha=1) # to make the legend background opaque
plt.show()

Do something like hrz1 = ax.axvline(m,color='y',linewidth=5,alpha=0.2) for each of your classes, and then ax.legend((hrz1, hrz0, hout), ('RZ1', 'RZ0', 'OUT'). The hrz1 pointer will be rewritten for each line you make, and then legend will only make one label for each of the handles.

Related

Python matplotlib intersection between 2 plots to get the colour of only one of them

I'm plotting more than 10,000 lines with the same colour on a single matplotlib plot. I use alpha=0.1 for transparency.
for i in range(len(x)):
plt.plt(x[i], y[i], color='b', alpha=0.1)
In the intersections between the lines, the colour becomes darker as the colours of the lines "add up".
How can I make the intersections the same colour of a single line?
(I prefer not to find the intersections as there are so many).
You could create a lighter color in place of using alpha for transparency. You can do that as explained in this answer: you can define a function to which to pass two parameters:
the name of the color you want to use, 'blue' for example
a value which indicates the lightness of the color;
0 total black
1 natural color ('blue' as is)
2 total white
In your case you can use a value of 1.9 in order to keep the color very light.
Complete Code
import matplotlib.pyplot as plt
import numpy as np
N = 10
x = np.linspace(0, 1, N)
def adjust_lightness(color, amount=0.5):
import matplotlib.colors as mc
import colorsys
try:
c = mc.cnames[color]
except:
c = color
c = colorsys.rgb_to_hls(*mc.to_rgb(c))
return colorsys.hls_to_rgb(c[0], max(0, min(1, amount * c[1])), c[2])
fig, ax = plt.subplots()
for i in range(20):
y = np.random.randn(N)
ax.plot(x, y, color = adjust_lightness('blue', 1.9), linewidth = 3)
plt.show()

draw a color grid based on points density using python matplotlib

The question is to read 10,000 coordinate points from a file and create a colored grid based on the density of each block on the grid. The range of x-axis is [-73.59, -73.55] and the y-axis is [45.49,45.530]. My code will plot a grid with many different colors, now I need a feature to only color the grid that has a specific density n, for example, The n = 100, only the grid with 100 points or higher will be colored to yellow, and other grids will be black.
I just added a link to my shapefile
https://drive.google.com/open?id=1H-8FhfonnPrYW9y7RQZDtiNLxVEiC6R8
import numpy as np
import matplotlib.pyplot as plt
import shapefile
grid_size = 0.002
x1 = np.arange(-73.59,-73.55,grid_size)
y1 = np.arange(45.49,45.530,grid_size)
shape = shapefile.Reader("Shape/crime_dt.shp",encoding='ISO-8859-1')
shapeRecords = shape.shapeRecords()
x_coordinates=[]
y_coordinates=[]
# read all points in .shp file, and store them in 2 lists.
for k in range(len(shapeRecords)):
x = float(shapeRecords[k].shape.__geo_interface__["coordinates"][0])
y = float(shapeRecords[k].shape.__geo_interface__["coordinates"][1])
x_coordinates.append(x)
y_coordinates.append(y)
plt.hist2d(x_coordinates,y_coordinates,bins=[x1,y1])
plt.show()
You can create a colormap with just two colors, and set vmin and vmax to be symmetrical around your desired pivot value.
Optionally you put the value of each bin inside the cells, while the pivot value decides the text color.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
grid_size = 0.002
x1 = np.arange(-73.59, -73.55, grid_size)
y1 = np.arange(45.49, 45.530, grid_size)
# read coordinates from file and put them into two lists, similar to this
x_coordinates = np.random.uniform(x1.min(), x1.max(), size=40000)
y_coordinates = np.random.uniform(y1.min(), y1.max(), size=40000)
pivot_value = 100
# create a colormap with two colors, vmin and vmax are chosen so that their center is the pivot value
cmap = ListedColormap(['indigo', 'gold'])
# create a 2d histogram with xs and ys as bin boundaries
binvalues, _, _, _ = plt.hist2d(x_coordinates, y_coordinates, bins=[x1, y1], cmap=cmap, vmin=0, vmax=2*pivot_value)
binvalues = binvalues.astype(np.int)
for i in range(len(x1) - 1):
for j in range(len(y1) - 1):
plt.text((x1[i] + x1[i + 1]) / 2, (y1[j] + y1[j + 1]) / 2, binvalues[i, j],
color='white' if binvalues[i, j] < pivot_value else 'black',
ha='center', va='center', size=8)
plt.show()
PS: If the bin values are very important, you can add them all as ticks. Then, their positions can also be used to draw gridlines as a division between the cells.
plt.yticks(y1)
plt.xticks(x1, rotation=90)
plt.grid(True, ls='-', lw=1, color='black')
To obtain contours based on these data, you could plt.contourf with the generated matrix. (You might want to use np.histogram2d to directly create the matrix.)
plt.contourf((x1[1:]+x1[:-1])/2, (y1[1:]+y1[:-1])/2, binvalues.T, levels=[0,100,1000], cmap=cmap)

Add legend of whole numbers instead of gradient in matplotlib

I'd like to add a legend that is only whole numbers, i.e. 0,1,2,3...14, instead of the gradient color bar. Basically, I want the array values to have a unique color and label in the legend, so that you can clearly distinguish each value in the array.
fileloc=os.path.join(basepath, infile)
data=np.loadtxt(fileloc)
fig = plt.figure(figsize=(20,10))
plt.imshow(data)
plt.colorbar()
If I understand the question correctly, the data is given as an integer numpy array which results in an image with exactly N different colors.
To get a colormap with exactly N colors from the viridis colormap, use plt.cm.get_cmap('viridis', N). This will result in a colorbar with exactly N regions.
To get ticks nicely in the center of each region, divide the space into 2N+1 pieces, and then take all the odd positions. (So, if there are 5 colors, the colorbar will go from 0 to 4, which would get 11 marks of which mark 0 is skipped and marks 1, 3, 5, 7 and 9 are used). Next to each tick a label with the number can be placed.
import numpy as np
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(6, 4))
# create some random test data
data = np.random.normal(0, 0.05, size=(150, 150)).cumsum(axis=0).cumsum(axis=1)
data = data.astype(np.int) # convert to integers
data -= data.min() # let the numbers start at zero
num_colors = data.max() + 1
cmap = plt.cm.get_cmap('viridis', num_colors)
plt.imshow(data, cmap=cmap)
cbar = plt.colorbar(ticks=np.linspace(0, num_colors - 1, num_colors * 2 + 1)[1::2])
cbar.ax.set_yticklabels(range(num_colors))
plt.show()
You can use matplotlib.colors.ListedColormap as follows:
custom_cmap = colors.ListedColormap(['purple','blue','green','yellow']) #... and so on until you have 15 colours specified
Then just pass that as the cmap argument to imshow and colorbar:
plt.imshow(data, cmap=custom_cmap)
plt.colorbar(cmap=custom_cmap)

How to align labels to the inside edge of polar bar chart

I am stuck making the visualization I want. I cannot yet put images so the link is below. I almost have what I want. The issue is the labels are not correctly placed.
inverted-polar-bar-demo
I would like to have the labels be rotated like they are, but have the labels' right edges aligned to just inside the outer edge of the circle.
EDIT To clarify:
The labels I used for this example are all 'testing'. With actual data, these labels will be of different length. I want to have the end of the labels moved so that they always have their last letter next to the outside edge of the circle. So in this case, all the 'g's would be next to the outside edge.
import matplotlib.pyplot as mpl
import numpy as np
import random
bgcolor = '#222222'
barcolor = '#6699cc'
bottom = 15
N = 32
Values = np.random.random(N)*10
MetricLabels = ['testing' for _ in range(1, N+1)]
# Select the radii, thetas, and widths.
Radii = -5*np.ones(N)-Values
Theta = np.linspace(0.0, 2 * np.pi, N, endpoint=False)
width = 2*np.pi/N
# Make a list of shifted thetas to place the labels at.
ThetaShifted = np.copy(Theta)
for i in range(N-1):
ThetaShifted[i] = (Theta[i] + Theta[i+1])/2.0
ThetaShifted[-1] = (Theta[-1] + 2.0*np.pi)/2.0
# Make the figure
fig = mpl.figure()
ax = fig.add_subplot(111, projection='polar')
bars = ax.bar(Theta, Radii, width=width, bottom=bottom)
# Set the outer ring to be invisible.
ax.spines["polar"].set_visible(False)
# Set the grid line locations but set the labels to be invisible.
ax.grid(False)
ax.set_thetagrids([], visible=False)
ax.set_rgrids([3], visible=False)
# Apply colors to bars based on the settings above.
for v, bar in zip(Values, bars):
bar.set_facecolor(barcolor)
bar.set_edgecolor(bar.get_facecolor())
# Show the metric and value labels
for counter in range(N):
ax.text(ThetaShifted[counter], bottom-3, MetricLabels[counter],
horizontalalignment='center', verticalalignment='baseline',
rotation=(counter+.5)*360/N, color=bgcolor)
ax.text(ThetaShifted[counter], bottom+0.75, np.round(Values[counter],2),
horizontalalignment='center', verticalalignment='center',
color=bars[counter].get_facecolor())
# Set the background color to be a dark grey,
ax.set_axis_bgcolor(bgcolor)
fig.set_facecolor(bgcolor)
# Show the figure.
mpl.show()
I actually solved my issue. See image and code below. The main thing to solve it was to use the monospace font family and to use rjust to create the label strings to be fixed length and right justified from the beginning. After that, it is just a matter of choosing the correct radial location for each label which should be much easier when they are all the same number of characters.
import matplotlib.pyplot as mpl
import numpy as np
import random
bgcolor = '#222222'
barcolor = '#6699cc'
bottom = 15
N = 32
Values = np.random.random(N)*10
MetricLabels = [('A'*(4+int(8*random.random()))).rjust(10) for _ in range(1, N+1)]
# Select the radii, thetas, and widths.
Radii = -5*np.ones(N)-Values
Theta = np.linspace(0.0, 2 * np.pi, N, endpoint=False)
width = 2*np.pi/N
# Make a list of shifted thetas to place the labels at.
ThetaShifted = np.copy(Theta)
for i in range(N-1):
ThetaShifted[i] = (Theta[i] + Theta[i+1])/2.0
ThetaShifted[-1] = (Theta[-1] + 2.0*np.pi)/2.0
# Make the figure
fig = mpl.figure()
ax = fig.add_subplot(111, projection='polar')
bars = ax.bar(Theta, Radii, width=width, bottom=bottom)
# Set the outer ring to be invisible.
ax.spines["polar"].set_visible(False)
# Set the grid line locations but set the labels to be invisible.
ax.grid(False)
ax.set_thetagrids([], visible=False)
ax.set_rgrids([3], visible=False)
# Apply colors to bars based on the settings above.
for v, bar in zip(Values, bars):
bar.set_facecolor(barcolor)
bar.set_edgecolor(bar.get_facecolor())
# Show the metric and value labels
for counter in range(N):
ax.text(ThetaShifted[counter], bottom-.075*(10+len(MetricLabels[counter])), MetricLabels[counter]+' '*5,
horizontalalignment='center', verticalalignment='center',
rotation=(counter+.5)*360/N, color=bgcolor,
family='monospace')
ax.text(ThetaShifted[counter], bottom+1, np.round(Values[counter],2),
horizontalalignment='center', verticalalignment='center',
rotation=(counter+.5)*360/N, color=bars[counter].get_facecolor(),
family='monospace')
# Set the background color to be a dark grey,
ax.set_axis_bgcolor(bgcolor)
fig.set_facecolor(bgcolor)
# Show the figure.
mpl.show()
If I correct understand what you want you have to add rotation property to the second call of counter cycle and align the text like here:
...
# Show the metric and value labels
for counter in range(N):
ax.text(ThetaShifted[counter], bottom-3, MetricLabels[counter],
horizontalalignment='center', verticalalignment='baseline',
rotation=(counter+.5)*360/N, color=bgcolor)
ax.text(ThetaShifted[counter], bottom+2.5, np.round(Values[counter],2),
horizontalalignment='center', verticalalignment='center',
rotation=(counter+.5)*360/N,
color=bars[counter].get_facecolor())
...

How to plot confusion matrix with gridding without color using matplotlib?

I found a similar quesion on How to plot confusion matrix with string axis rather than integer in python. But the answer is not exact what I want. Because it doesn't contain gridding (e.g., the numbers are not in little squares) and there is background color to show the number which is not what I want.
import numpy as np
import matplotlib.pyplot as plt
conf_arr = [[33,2,0,0,0,0,0,0,0,1,3],
[3,31,0,0,0,0,0,0,0,0,0],
[0,4,41,0,0,0,0,0,0,0,1],
[0,1,0,30,0,6,0,0,0,0,1],
[0,0,0,0,38,10,0,0,0,0,0],
[0,0,0,3,1,39,0,0,0,0,4],
[0,2,2,0,4,1,31,0,0,0,2],
[0,1,0,0,0,0,0,36,0,2,0],
[0,0,0,0,0,0,1,5,37,5,1],
[3,0,0,0,0,0,0,0,0,39,0],
[0,0,0,0,0,0,0,0,0,0,38]]
norm_conf = []
for i in conf_arr:
a = 0
tmp_arr = []
a = sum(i, 0)
for j in i:
tmp_arr.append(float(j)/float(a))
norm_conf.append(tmp_arr)
fig = plt.figure()
plt.clf()
ax = fig.add_subplot(111)
ax.set_aspect(1)
res = ax.imshow(np.array(norm_conf), cmap=plt.cm.jet,
interpolation='nearest')
width, height = conf_arr.shape
for x in xrange(width):
for y in xrange(height):
ax.annotate(str(conf_arr[x][y]), xy=(y, x),
horizontalalignment='center',
verticalalignment='center')
cb = fig.colorbar(res)
alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
plt.xticks(range(width), alphabet[:width])
plt.yticks(range(height), alphabet[:height])
plt.savefig('confusion_matrix.png', format='png')
By making only a few changes to that rather excellent code proposal (I upvoted it, consider doing that too), you can get the figure you're describing.
You'll get gridding by calling the hlines and vlines methods of the ax object, which will add horizontal and vertical lines respectively.
When you then also remove the call to imshow, the colors are gone. Like this:
import numpy as np
import matplotlib.pyplot as plt
conf_arr = np.array([[33,2,0,0,0,0,0,0,0,1,3],
[3,31,0,0,0,0,0,0,0,0,0],
[0,4,41,0,0,0,0,0,0,0,1],
[0,1,0,30,0,6,0,0,0,0,1],
[0,0,0,0,38,10,0,0,0,0,0],
[0,0,0,3,1,39,0,0,0,0,4],
[0,2,2,0,4,1,31,0,0,0,2],
[0,1,0,0,0,0,0,36,0,2,0],
[0,0,0,0,0,0,1,5,37,5,1],
[3,0,0,0,0,0,0,0,0,39,0],
[0,0,0,0,0,0,0,0,0,0,38]])
height, width = conf_arr.shape
fig = plt.figure('confusion matrix')
ax = fig.add_subplot(111, aspect='equal')
for x in range(width):
for y in range(height):
ax.annotate(str(conf_arr[x][y]), xy=(y, x), ha='center', va='center')
offset = .5
ax.set_xlim(-offset, width - offset)
ax.set_ylim(-offset, height - offset)
ax.hlines(y=np.arange(height+1)- offset, xmin=-offset, xmax=width-offset)
ax.vlines(x=np.arange(width+1) - offset, ymin=-offset, ymax=height-offset)
alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
plt.xticks(range(width), alphabet[:width])
plt.yticks(range(height), alphabet[:height])
plt.savefig('confusion_matrix.png', format='png')
Remark that when you remove the call to imshow, you'll need to set the x- and y-limits explicitly, as shown above, otherwise you'll only see the lower left region (imshow updates the limits automatically depending on what you pass to it).

Categories

Resources