Point and figure chart with matplotlib - python

I'm trying to make a point and figure chart. I can get it to work printing out on the terminal but I want to graph it with matplotlib. What would be the best way of doing something like this? I was thinking scatter, but when I do this the columns are spread out too far. I would like to get something much like the chart from the link I provided where the columns are as close to one another as possible. Is there a parameter I can overwrite to force this? First time using matplotlib so please excuse me if this is trivial. Thanks.

You can adjust the size of the symbols used in a scatter plot by choosing the s parameter. You also will likely need to adjust the size of your figure (with figsize) or the dimensions of your axes (with add_axes). This is because the symbols for scatter are square, in display units, and the x and y axis are not automatically adjusted so that width-of-one-change = height-of-one-box.
In other words, the example you provided is a rectangular plot with the height > width, and the height and width are chosen to make the width-of-one-change == height-of-one-box.
Here's an example of apply these techniques:
import matplotlib.pyplot as plt
BOX = 5
START = 365
changes = (8, -3, 4, -4, 12, -3, 7, -3, 5, -9, 3)
# one way to force dimensions is to set the figure size:
fig = plt.figure(figsize=(5, 10))
# another way is to control the axes dimensions
# for axes to have specific dimensions:
# [ x0, y0, w, h] in figure units, from 0 to 1
#ax = fig.add_axes([.15, .15, .7*.5, .7])
ax = fig.add_axes([.15, .15, .7, .7])
def sign(val):
return val / abs(val)
pointChanges = []
for chg in changes:
pointChanges += [sign(chg)] * abs(chg)
symbol = {-1:'o',
1:'x'}
chgStart = START
for ichg, chg in enumerate(changes):
x = [ichg+1] * abs(chg)
y = [chgStart + i * BOX * sign(chg) for i in range(abs(chg))]
chgStart += BOX * sign(chg) * (abs(chg)-2)
ax.scatter(x, y,
marker=symbol[sign(chg)],
s=175) #<----- control size of scatter symbol
ax.set_xlim(0, len(changes)+1)
fig.savefig('pointandfigure.png')
plt.show()
The method developed for each scatter plot is very hackish, but the key point is that I needed to play with the scatter s parameter and the figure size to get something of the desired effect.
The resulting plot is:
Ideally, one would make a custom method modeled after the scatter method. It would create a custom Collection instance that would include the x's, o's and month labels. It would also a) automatically adjust the axes/figure aspect or b) make asymmetric symbols. This is obviously an advanced option, intended for someone wishing to contribute, as a developer, to the Matplotlib project.

No personal experience, but maybe set_view_interval() or set_data_interval() from here? I have used Matplotlib for a project, but didn't have to play with fixing the x-axis width.

Editing the colors was the best I could.
chgStart = START
colors=['red','black','green','blue']
for ichg, chg in enumerate(changes):
x = [ichg+1] * abs(chg)
y = [chgStart + i * BOX * sign(chg) for i in range(abs(chg))]
chgStart += BOX * sign(chg) * (abs(chg)-2)
ax.scatter(x, y,
marker=symbol[sign(chg)],
s=175, color = colors[int(sign(chg))] ) #<----- control size of scatter symbol

Related

Setting the same x-scale but different x-limits for adjacent subplots matplotlib

I am trying to create a figure with three bar plots side by side. These bar plots have different yscales, but the data is fundamentally similar so I'd like all the bars to have the same width.
The only way I was able to get the bars to have the exact same width was by using sharex when creating the subplots, in order to keep the same x scale.
import matplotlib.pyplot as plt
BigData = [[100,300],[400,200]]
MediumData = [[40, 30],[50,20],[60,50],[30,30]]
SmallData = [[3,2],[11,3],[7,5]]
data = [BigData, MediumData, SmallData]
colors = ['#FC766A','#5B84B1']
fig, axs = plt.subplots(1, 3, figsize=(30,5), sharex=True)
subplot = 0
for scale in data:
for type in range(2):
bar_x = [x + type*0.2 for x in range(len(scale))]
bar_y = [d[type] for d in scale]
axs[subplot].bar(bar_x,bar_y, width = 0.2, color = colors[type])
subplot += 1
plt.show()
This creates this figure:
The problem with this is that the x-limits of the plot are also shared, leading to unwanted whitespace. I've tried setting the x-bounds after the fact, but it doesn't seem to override sharex. Is there a way to make the bars have the same width, without each subplot also being the same width?
Additionally, is there a way to create such a plot (one with different y scales to depending on the size of the data) without having to sort the data manually beforehand, like shown in my code?
Thanks!
Thanks to Jody Klymak for help finding this solution! I thought I should document it for future users.
We can make use of the 'width_ratios' GridSpec parameter. Unfortunately there's no way to specify these ratios after we've already drawn a graph, so the best way I found to implement this is to write a function that creates a dummy graph, and measures the x-limits from that graph:
def getXRatios(data, size):
phig, aks = plt.subplots(1, 3, figsize=size)
subplot = 0
for scale in data:
for type in range(2):
bar_x = [x + type*0.2 for x in range(len(scale))]
bar_y = [d[type] for d in scale]
aks[subplot].bar(bar_x,bar_y, width = 0.2)
subplot += 1
ratios = [aks[i].get_xlim()[1] for i in range(3)]
plt.close(phig)
return ratios
This is essentially identical to the code that creates the actual figure, with the cosmetic aspects removed, as all we want from this dummy figure is the x-limits of the graph (something we can't get from our actual figure as we need to define those limits before we start in order to solve the problem).
Now all you need to do is call this function when you're creating your subplots:
fig, axs = plt.subplots(1, 3, figsize=(40,5), gridspec_kw = {'width_ratios':getXRatios(data,(40,5))})
As long as your XRatio function creates your graph in the same way your actual graph does, everything should work! Here's my output using this solution.
To save space you could re-purpose the getXRatios function to also construct your final graph, by calling itself in the arguments and giving an option to return either the ratios or the final figure. I couldn't be bothered.

How to precisely control axis scale function in matplotlib

I want to plot points on the interval x in [0, 4]. My function performs something interesting for very small values, so I would like to create a non-linear scale that would use more space for smaller values of x. Logarithmic scale would be a great solution, but the problem is that my x-axis must include 0, which is not part of logarithmic axis.
So I considered using a power scale. After some googling I came across the following solution.
def stratify(ax, power=2):
f = lambda x: (x + 1)**(1 / power)
f_inv = lambda y: y**power - 1
ax.set_xscale('function', functions=(f, f_inv))
x = np.linspace(0, 4, 100)
y = np.sqrt(x)
fig, ax = plt.subplots()
ax.plot(x, y)
stratify(ax, 2)
plt.show()
The function stratify changes the x-scale of the plot to the square root function. This looks kind of correct. Below is a minimal example plot corresponding to the above code (not actual data).
I would like to have control over the nonlinearity in the x-scale, that is why I have introduced the power parameter. However, when I change the power parameter to value different from 2, the plot does not change at all. This is very surprising for me. I would appreciate if somebody could advise me how I can control the extent of non-linearity in x-axis. If possible, I would like it even more non-linear, making the interval [0, 0.5] take half of the plot.
EDIT While the current solution by #Thomas works as intended, the plotting routine throws a lot of errors when one attempts to do anything with it. For example, I would like to insert extra ticks, as the original only has integer ticks for whatever reason. Inserting extra ticks via ax.set_xticks(ax.get_xticks() + [0.5]) results in an error posx and posy should be finite values. What is this error, and how can it be bypassed?
For me, there's a change when switching from power=2 to power=10. Just be careful to edit it at the right position, i.e. when calling stratify=X.
Here's power=2:
Here's power=10:
However, here's another suggestion that is slightly more aggressive:
import numpy as np
import matplotlib.pyplot as plt
def stratify(ax, scale=1):
f = lambda x: np.log(x / scale + 1)
f_inv = lambda y: scale * (np.exp(y) - 1)
ax.set_xscale('function', functions=(f, f_inv))
x = np.linspace(0, 4, 100)
y = np.sqrt(x)
fig, axs = plt.subplots(1, 3)
for i, scale in enumerate([10,1,0.1]):
ax = axs[i]
ax.set_title(f'Scale={scale}')
ax.plot(x, y)
stratify(ax, scale=scale)
plt.show()
Resulting in
Another option are zoom regions.

How to match top and bottom x-axes in Python with Matplotlib?

I am trying to plot the energy consumption profile of an electric vehicle. I am using the elevation profile vs the horizontal distance the vehicle runs along a path. I want to add a second x-axis on top of the plot to represent by each chunk of distance, what the energy consumption value was at that precise location.
This is what I have so far, but it's not precisely what I need:
I know this should be fairly simple as it is only adding a second x-axis that matches with the primary x-axis, but I have wasted an entire day trying to figure out unsuccessfully :(
Any insights will be greatly appreciated.
Code:
fig, ax1 = plt.subplots()
elevation_distance_np = elevation_distance.to_numpy()
plt.plot(elevation_distance_np[:,0], elevation_distance_np[:,1], color = 'blue')
plt.grid(True)
plt.xlabel("Distancia recorrida")
plt.ylabel("Elevación de distancia recorrrida")
axes2 = ax1.twiny()
axes2.set_xticks(suma_kWh_np[::mth.ceil(len(suma_kWh_np)/8)])
plt.title("Elevación vs Distancia Recorrida")
plt.show()
This is a not so trivial endeavor, as these questions show, so don't feel frustrated for not getting this on your own.
Disclaimer: this is not the most elegant solution, but it works. I made a toy example where the conversion from one axis to the other is obtained by dividing the main by 8.5. Also, I replotted your data on this secondary axis, to set the values of its own X axis to something sensible, then removed this extra line.
x = np.linspace(0, 140) # Some x values, similar to your range
# Caps them to a minimum of 0
y = np.clip(x * (-1) + 100, a_min=0, a_max=100)
# Creates something similar to your data
elevation_distance_np = np.hstack((x[:, np.newaxis], y[:, np.newaxis]))
# I guessed some transform. If you don't have a formula,
# you'll need to interpolate between known values, probably.
suma_kWh_np = x / 8.5
fig, ax1 = plt.subplots()
# Changed to explicit notation, so we don't go back and forth between them
ax1.plot(elevation_distance_np[:,0], elevation_distance_np[:,1], color = 'blue')
ax1.grid(True)
ax1.set_xlabel("Distancia recorrida")
ax1.set_ylabel("Elevación de distancia recorrrida")
ax2 = ax1.twiny()
# Added a copy of your line, but which will be removed later
extra_line = ax2.plot(suma_kWh_np, elevation_distance_np[:,1], color = 'r')
# Now, we get the x ticks and transform them to kWh.
# Here, I had to remove the first and last points ([1:-1])
# because ax1.get_xticks() returned a range from -20 to 160,
ax2.set_xticks(ax1.get_xticks()[1:-1] / 8.5)
ax1.set_title("Elevación vs Distancia Recorrida")
ax2.lines.pop() # We remove the temporary line right before plotting
plt.show()
Here's the result.

How to put multiple colormap patches in a matplotlib legend?

Situation at hand:
I have multiple groups of lines, where the lines within the same group vary according to some group specific parameter. I assign each of these lines within the same group a color from a colormap according to this parameter using a different colormap for each group.
Now, I would like to add a legend to the plot with one entry per group of lines.
Solution for only one set of lines:
If I had only one group of lines the best way of labelling would be to add a colorbar as suggested in the answer to: Matplotlib: Add colorbar to non-mappable object.
How best to do this for multiple sets of lines?
As I have multiple such groups of lines, I do not want to add a colorbar for each new parameter. Instead, I would rather put patches filled with the corresponding colormaps in the legend (as a sort of mini colorbar).
Minimal working example:
In the following you can find a minimal working example of the situation at hand. Note, though, that I heavily simplified the calculation of the lines which hides the parameter dependence. Thus, my "parameter" param here is just the index I am iterating over. My actual code calculates the x and y values depending on a model parameter with more complicated functions. Accordingly the maximum param_max here is the same for each group of lines, though actually it would not be.
import numpy as np
import matplotlib.pyplot as plt
x_array = np.linspace(1, 10, 10)
y_array = x_array
param_max = x_array.size
cmaps = [plt.cm.spring, plt.cm.winter] # set of colormaps
# (as many as there are groups of lines)
plt.figure()
for param, (x, y) in enumerate(zip(x_array, y_array)):
x_line1 = np.linspace(x, 1.5 * x, 10)
y_line1 = np.linspace(y**2, y**2 - x, 10)
x_line2 = np.linspace(1.2 * x, 1.5 * x, 10)
y_line2 = np.linspace(2 * y, 2 * y - x, 10)
# plot lines with color depending on param using different colormaps:
plt.plot(x_line1, y_line1, c=cmaps[0](param / param_max))
plt.plot(x_line2, y_line2, c=cmaps[1](param / param_max))
plt.show()
This produces the plot shown above.
Since I could not find anything directly answering this on stackoverflow, I tried finding a solution myself which you can find in the answers section. If there is a more direct/proper way of doing this I would be happy to know.
I adapted the solution of the answer by ImportanceOfBeingErnest to "Create a matplotlib mpatches with a rectangle bi-colored for figure legend" to this case. As linked there, the instructions in the section on Implementing a custom legend handler in the matplotlib legend guide were particularly helpful.
Result:
Solution:
I created the class HandlerColormap derived from matplotlib's base class for legend handlers HandlerBase. HandlerColormap takes a colormap and a number of stripes as arguments.
For the argument cmap a matplotlib colormap instance should be given.
The argument num_stripes determines how (non-)continuous the color gradient in the legend patch will be.
As instructed in HandlerBase I override its create_artist method using the given dimension such that the code should be (automatically) scaled by fontsize. In this new create_artist method I create multiple stripes (slim matplotlib Rectangles) colored according to the input colormap.
Code:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
from matplotlib.legend_handler import HandlerBase
class HandlerColormap(HandlerBase):
def __init__(self, cmap, num_stripes=8, **kw):
HandlerBase.__init__(self, **kw)
self.cmap = cmap
self.num_stripes = num_stripes
def create_artists(self, legend, orig_handle,
xdescent, ydescent, width, height, fontsize, trans):
stripes = []
for i in range(self.num_stripes):
s = Rectangle([xdescent + i * width / self.num_stripes, ydescent],
width / self.num_stripes,
height,
fc=self.cmap((2 * i + 1) / (2 * self.num_stripes)),
transform=trans)
stripes.append(s)
return stripes
x_array = np.linspace(1, 10, 10)
y_array = x_array
param_max = x_array.size
cmaps = [plt.cm.spring, plt.cm.winter] # set of colormaps
# (as many as there are groups of lines)
plt.figure()
for param, (x, y) in enumerate(zip(x_array, y_array)):
x_line1 = np.linspace(x, 1.5 * x, 10)
y_line1 = np.linspace(y**2, y**2 - x, 10)
x_line2 = np.linspace(1.2 * x, 1.5 * x, 10)
y_line2 = np.linspace(2 * y, 2 * y - x, 10)
# plot lines with color depending on param using different colormaps:
plt.plot(x_line1, y_line1, c=cmaps[0](param / param_max))
plt.plot(x_line2, y_line2, c=cmaps[1](param / param_max))
cmap_labels = ["parameter 1 $\in$ [0, 10]", "parameter 2 $\in$ [-1, 1]"]
# create proxy artists as handles:
cmap_handles = [Rectangle((0, 0), 1, 1) for _ in cmaps]
handler_map = dict(zip(cmap_handles,
[HandlerColormap(cm, num_stripes=8) for cm in cmaps]))
plt.legend(handles=cmap_handles,
labels=cmap_labels,
handler_map=handler_map,
fontsize=12)
plt.show()

Add colorbar to scatter plot or change the plot type

I am plotting some data that includes spatial (x, y) components as well as a z component, which is the value of the measurement at that point in space. I was looking at the gallery, and I'm just not getting it. I think that what I want is a pcolormesh, but I don't understand what I need to put in for arguments. I finally had success getting a scatter plot to do basically what I want, but it's less pretty than I want. If I could figure out a way to make the points in the scatter plot bigger, I would be a lot happier with my plot. Furthermore, I am stuck on trying to add a legend - I only need the colorbar portion, since the end user doesn't really care about the X and Y dimensions. Looking at the colorbar example, it seems that I need to add an axis, but I don't understand how I'm telling it that the axis I need is the Z axis.
x_vals = list(first_array[data_loc_dictionary['x_coord_index']][:])
y_vals = list(first_array[data_loc_dictionary['y_coord_index']][:])
y_vals = [-i for i in y_vals]
z_vals = list(first_array[data_loc_dictionary['value_index']][:])
plt.scatter(x_vals, y_vals, s = len(x_vals)^2, c = z_vals, cmap = 'rainbow')
plt.show()
Here is an example of what I am trying to duplicate:
And here is what the code above produces:
I would like the second to look a little more like the first, i.e., if there were a way to adjust the markers to be large enough to approximate that look, that would be ideal
I am struggling with creating a legend. Colorbar seems to be the way to go, but I am not comprehending how to specify that it needs to be based on the Z values.
Good catch with the ^2 -
What about this basic example:
# generate random data
In [63]: x = np.random.rand(20)
In [64]: y = np.random.rand(20)
In [65]: z = np.random.rand(20)
# plot it with square markers: marker='s'
In [66]: plt.scatter(x, y, s=len(x)**2, c=z, cmap='rainbow', marker='s')
Out[66]: <matplotlib.collections.PathCollection at 0x39e6c90>
# colorbar
In [67]: c = plt.colorbar(orientation='horizontal')
In [68]: c.set_label('This is a colorbar')
In [69]: plt.show()
The Size of the points is given by
s : scalar or array_like, shape (n, ), optional, default: 20
size in points^2.
I see no reason why s=len(x)**2 is a good choice by default. I would play around with it according to your preference.
In case you want to know how to replicate your initial example image with pcolormesh, I would do:
import numpy as np
import matplotlib.pyplot as plt
f, ax = plt.subplots(figsize=(6, 5))
grid = np.arange(-5, 6)
x, y = np.meshgrid(grid, grid)
z = np.random.randn(len(x), len(y))
mask = (np.abs(x) + np.abs(y)) > 4
z = np.ma.masked_array(z, mask)
mesh = ax.pcolormesh(x - .5, y - .5, z, cmap="coolwarm", vmin=-3, vmax=3)
plt.colorbar(mesh)
To produce:

Categories

Resources