matplotlib secondary axis is reversed when using log scale - matplotlib

A matplotlib secondary axis is reversed when plotting on a log scale.
In the attached example, I use magnitude as the primary (left) axis, luminosity as the secondary (right) axis. Higher luminosity should correspond to a smaller magnitude, as it does in the first two plots. However, in the third plot, when I use a log scale for luminosity, luminosity increases with magnitude, which is incorrect. Is this a bug, or am I doing something wrong?
# Test secondary axis
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
print(matplotlib.__version__)
# Mag to luminosity (solar units) conversion
M_sun = 4.83
def mag2lum(mag):
return 10**(0.4*(M_sun - mag))
def lum2mag(L):
return M_sun - 2.5*np.log10(L)
def mag2lgl(mag):
return 0.4*(M_sun - mag)
def lgl2mag(lgl):
return M_sun - 2.5*lgl
# log luminosity as second axis - correct behaviour
fig, ax = plt.subplots(constrained_layout=True)
plt.ylabel(r'$M_G$ [mag]')
plt.xlim(-1, 5)
plt.ylim(10, 0)
secax = ax.secondary_yaxis('right', functions=(mag2lgl, lgl2mag))
secax.set_ylabel(r'$\log\ L_G\ [L_\odot]$')
plt.show()
# luminosity as second axis - correct behaviour, but labelling is horrible
fig, ax = plt.subplots(constrained_layout=True)
plt.ylabel(r'$M_G$ [mag]')
plt.xlim(-1, 5)
plt.ylim(10, 0)
secax = ax.secondary_yaxis('right', functions=(mag2lum, lum2mag))
secax.set_ylabel(r'$L_G\ [L_\odot]$')
plt.show()
# luminosity as second axis on log scale: axis is reversed
fig, ax = plt.subplots(constrained_layout=True)
plt.ylabel(r'$M_G$ [mag]')
plt.xlim(-1, 5)
plt.ylim(10, 0)
secax = ax.secondary_yaxis('right', functions=(mag2lum, lum2mag))
secax.set_ylabel(r'$L_G\ [L_\odot]$')
secax.set_yscale('log')
plt.show()
log luminosity as second axis - correct behaviour
luminosity as second axis - correct behaviour, but labelling is horrible
luminosity as second axis on log scale: axis is reversed
In third plot, luminosity should increase upwards.

In the simple case of one y axis, you can indeed set the scale. However, note what it is happening here, in your third case. The "scale" is not a decision of the plot drawing, but it is a consequence of the functions that translate between the main y axis and the secondary axis. What I mean is that the values and their positions in the secondary axis are "locked", as they need to match the corresponding ones in the main axis. So what you need, is not to try to change the secondary axis, but its ticks and labels. For that, you can use custom tick locations labels, or, as done below, import pre-defined locators and formatters.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.ticker import LogLocator, LogFormatterMathtext
M_sun = 4.83
def mag2lum(mag):
return 10**(0.4*(M_sun - mag))
def lum2mag(L):
return M_sun - 2.5*np.log10(L)
fig, ax = plt.subplots(constrained_layout=True)
ax.set_ylabel(r'$M_G$ [mag]')
ax.set_xlim(-1, 5)
ax.set_ylim(10, 0)
secax = ax.secondary_yaxis('right', functions=(mag2lum, lum2mag))
secax.set_ylabel(r'$L_G\ [L_\odot]$')
# do this instead of trying to set scale
secax.yaxis.set_major_locator(LogLocator())
secax.yaxis.set_major_formatter(LogFormatterMathtext())
plt.show()

Related

Pyplot axis limits within boundaries

Is there an easy way to avoid pyplot zooming far into noisy data?
Something like a lower boundary for the axis limits.
I am not trying to set a fix boundary to my axis, as this will fully disable automatic scaling.
Maybe a "minimum tick distance" would also work.
Right now I am using an additional 'invisible' plot in my graph that will define the maximum zoom.
Some example that illustrates what I want to achieve:
import numpy as np
import matplotlib.pyplot as plt
x = np.arange(0, 100, 1)
noise = np.random.randn(len(x))*0.1
y = 10+noise
y_dummy_low = [0]*len(x)
y_dummy_high = [20]*len(x)
plt.figure()
plt.plot(x, y) # noise data i actually want to plot
plt.plot(x, y_dummy_low, y_dummy_high, marker="None", linestyle="None") # this will avoid zooming too much
plt.show()
Zooming too far
Zooming OK

Plotting fuzzy data with matplotlib

I don't know where to start, as I think it is a new approach for me. Using matplotlib with python, I would like to plot a set of fuzzy numbers (for instance a set of triangular or bell curve fuzzy numbers) as in the picture below:
You can plot the curves recurrently. My try at reproducing your example (including the superposition of labels 1 and 6):
import matplotlib.pyplot as plt
import numpy as np
# creating the figure and axis
fig, ax = plt.subplots(1,1,constrained_layout=True)
# generic gaussian
y = np.linspace(-1,1,100)
x = np.exp(-5*y**2)
center_x = (0,2,4,1,3,0,5)
center_y = (6,2,3,4,5,6,7)
# loop for all the values
for i in range(len(center_x)):
x_c, y_c = center_x[i], center_y[i]
# plotting the several bells, relocated to (x_c, y_c)
ax.plot(x + x_c,y + y_c,
color='red',linewidth=2.0)
ax.plot(x_c,y_c,
'o',color='blue',markersize=3)
# adding label
ax.annotate(
str(i+1),
(x_c - 0.1,y_c), # slight shift in x
horizontalalignment='right',
verticalalignment='center',
color='blue',
)
ax.grid()
Every call to ax.plot() is adding points or curves (to be more precise, Artists) to the same axis. The same for ax.annotate() to create the labels.

Add a secondary label to a plot x-axis for events

I have an ax.stackplot showing population of different groups over time. The x-axis is time and the y-axis is population. I am showing time at major labels 1 year and minor labels 1 month, however, changes in the data occur more frequently at "events". I'd like to show labels for these events along the x-axis, kind of how I have it sketched out in the image here:
I've attempted adding a second axis with plt.axes(), but this second axis is overwriting the ticks of my first axis for some reason. Does anyone have any suggestions for how to accomplish this?
Thank you!
If you don't have too many points, I think the best way to do this is adding text to your axes using ax.text:
from matplotlib import pyplot
import matplotlib
import numpy as np
# Random plot
t = np.arange(0.0, 2.0, 0.01)
s = 1 + np.sin(2 * np.pi * t)
fig, ax = pyplot.subplots()
ax.plot(t, s)
# ax.text(x, y, text, rotation)
ax.text(0, -0.35, "Event 1", rotation=90) # rotation=90 is easier to read, for me
ax.text(0.5, -0.35, "Event 2", rotation=-90) # opposite rotation
ax.text(0.75, -0.35, "Event 3", rotation=-90)
# This gives some space at the bottom of the figure
# so that the text is visible
fig.subplots_adjust(bottom=0.2)
pyplot.show()
Result:
Check the Axes.text documentation for more info.
Thank you for the responses, I was able to come up with a solution based on your suggestions. The solution involves using ax.twiny() to create a second axes object, and then specifying the second x-axis data points and labels. Below is a simple example for those interested:
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
# Create some meaningless data for testing.
x = np.arange(0, 10)
y = np.full(10, len(x))
# Set up figure and set axes parameters.
fig = plt.figure(num=None, figsize=(10, 8), dpi=80, facecolor='w', edgecolor='k')
ax = plt.axes()
ax.xaxis.set_minor_locator(ticker.FixedLocator([1, 3, 5, 7, 9]))
# Get a second axes (for secondary labels) and set parameters.
axl = ax.twiny()
axl.tick_params(axis='x', bottom=True, labelbottom=True, labeltop=False, top=False, length=15, colors=[.5,.5,.5])
# Plot data on primary axes
ax.bar(x, y)
interval = ax.xaxis.get_view_interval()
# Set label properties on secondary axes (for secondary labels)
axl.xaxis.set_view_interval(*interval)
axl.xaxis.set_ticklabels(['a', 'b'])
axl_loc = ticker.FixedLocator([0.5, 4.75])
axl.xaxis.set_major_locator(axl_loc)
plt.show()

heatmap for positive and negative values [duplicate]

I am trying to make a filled contour for a dataset. It should be fairly straightforward:
plt.contourf(x, y, z, label = 'blah', cm = matplotlib.cm.RdBu)
However, what do I do if my dataset is not symmetric about 0? Let's say I want to go from blue (negative values) to 0 (white), to red (positive values). If my dataset goes from -8 to 3, then the white part of the color bar, which should be at 0, is in fact slightly negative. Is there some way to shift the color bar?
First off, there's more than one way to do this.
Pass an instance of DivergingNorm as the norm kwarg.
Use the colors kwarg to contourf and manually specify the colors
Use a discrete colormap constructed with matplotlib.colors.from_levels_and_colors.
The simplest way is the first option. It is also the only option that allows you to use a continuous colormap.
The reason to use the first or third options is that they will work for any type of matplotlib plot that uses a colormap (e.g. imshow, scatter, etc).
The third option constructs a discrete colormap and normalization object from specific colors. It's basically identical to the second option, but it will a) work with other types of plots than contour plots, and b) avoids having to manually specify the number of contours.
As an example of the first option (I'll use imshow here because it makes more sense than contourf for random data, but contourf would have identical usage other than the interpolation option.):
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import DivergingNorm
data = np.random.random((10,10))
data = 10 * (data - 0.8)
fig, ax = plt.subplots()
im = ax.imshow(data, norm=DivergingNorm(0), cmap=plt.cm.seismic, interpolation='none')
fig.colorbar(im)
plt.show()
As an example of the third option (notice that this gives a discrete colormap instead of a continuous colormap):
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import from_levels_and_colors
data = np.random.random((10,10))
data = 10 * (data - 0.8)
num_levels = 20
vmin, vmax = data.min(), data.max()
midpoint = 0
levels = np.linspace(vmin, vmax, num_levels)
midp = np.mean(np.c_[levels[:-1], levels[1:]], axis=1)
vals = np.interp(midp, [vmin, midpoint, vmax], [0, 0.5, 1])
colors = plt.cm.seismic(vals)
cmap, norm = from_levels_and_colors(levels, colors)
fig, ax = plt.subplots()
im = ax.imshow(data, cmap=cmap, norm=norm, interpolation='none')
fig.colorbar(im)
plt.show()

Why does logarithmic scatter axis not start at zero?

I'm generating a scatter plot in matplotlib. Everything works fine if I use linear scales.
But since I'm mainly interested in the lower values, I thought I'd use logarithmic scaling. However, even though I have set my x-axis limits explicitly to (0,1), the axis starts at 0.1, so i miss everything below that!
Why does the logarithmic axis not start at zero, and how can I force it to?
import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(0,1,100)
y = np.random.randint(1000, size=100)
fig = plt.figure()
ax = fig.add_subplot(1,1,1)
ax.scatter(x, y)
ax.set_xlim(0,1.2)
ax.set_ylim(0,1000)
ax.set_yscale('log')
ax.set_xscale('log')
ax.yaxis.set_major_formatter(plt.FormatStrFormatter('%1.0f'))
ax.xaxis.set_major_formatter(plt.FormatStrFormatter('%.1f'))
ax.xaxis.set_minor_formatter(plt.FormatStrFormatter('%.1f'))
# this red line at x = 0.1
ax.axvline(x=0.1,color='r')
plt.show()
Any help is greatly appreciated!
Usually logarithmic axes never start at zero because there is no "good" value for log(0) on the x-axis, because log(0)==x only for x->-infinity.