Matplotlib: different scale on negative side of the axis - matplotlib

Background
I am trying to show three variables on a single plot. I have connected the three points using lines of different colours based on some other variables. This is shown here
Problem
What I want to do is to have a different scale on the negative x-axis. This would help me in providing positive x_ticks, different axis label and also clear and uncluttered representation of the lines on left side of the image
Question
How to have a different positive x-axis starting from 0 towards negative direction?
Have xticks based on data plotted in that direction
Have a separate xlabel for this new axis
Additional information
I have checked other questions regarding inclusion of multiple axes e.g. this and this. However, these questions did not serve the purpose.
Code Used
font_size = 20
plt.rcParams.update({'font.size': font_size})
fig = plt.figure()
ax = fig.add_subplot(111)
#read my_data from file or create it
for case in my_data:
#Iterating over my_data
if condition1 == True:
local_linestyle = '-'
local_color = 'r'
local_line_alpha = 0.6
elif condition2 == 1:
local_linestyle = '-'
local_color = 'b'
local_line_alpha = 0.6
else:
local_linestyle = '--'
local_color = 'g'
local_line_alpha = 0.6
datapoint = [case[0], case[1], case[2]]
plt.plot(datapoint[0], 0, color=local_color)
plt.plot(-datapoint[2], 0, color=local_color)
plt.plot(0, datapoint[1], color=local_color)
plt.plot([datapoint[0], 0], [0, datapoint[1]], linestyle=local_linestyle, color=local_color)
plt.plot([-datapoint[2], 0], [0, datapoint[1]], linestyle=local_linestyle, color=local_color)
plt.show()
exit()

You can define a custom scale, where values below zero are scaled differently than those above zero.
import numpy as np
from matplotlib import scale as mscale
from matplotlib import transforms as mtransforms
from matplotlib.ticker import FuncFormatter
class AsymScale(mscale.ScaleBase):
name = 'asym'
def __init__(self, axis, **kwargs):
mscale.ScaleBase.__init__(self)
self.a = kwargs.get("a", 1)
def get_transform(self):
return self.AsymTrans(self.a)
def set_default_locators_and_formatters(self, axis):
# possibly, set a different locator and formatter here.
fmt = lambda x,pos: "{}".format(np.abs(x))
axis.set_major_formatter(FuncFormatter(fmt))
class AsymTrans(mtransforms.Transform):
input_dims = 1
output_dims = 1
is_separable = True
def __init__(self, a):
mtransforms.Transform.__init__(self)
self.a = a
def transform_non_affine(self, x):
return (x >= 0)*x + (x < 0)*x*self.a
def inverted(self):
return AsymScale.InvertedAsymTrans(self.a)
class InvertedAsymTrans(AsymTrans):
def transform_non_affine(self, x):
return (x >= 0)*x + (x < 0)*x/self.a
def inverted(self):
return AsymScale.AsymTrans(self.a)
Using this you would provide a scale parameter a that scales the negative part of the axes.
# Now that the Scale class has been defined, it must be registered so
# that ``matplotlib`` can find it.
mscale.register_scale(AsymScale)
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
ax.plot([-2, 0, 5], [0,1,0])
ax.set_xscale("asym", a=2)
ax.annotate("negative axis", xy=(.25,0), xytext=(0,-30),
xycoords = "axes fraction", textcoords="offset points", ha="center")
ax.annotate("positive axis", xy=(.75,0), xytext=(0,-30),
xycoords = "axes fraction", textcoords="offset points", ha="center")
plt.show()
The question is not very clear about what xticks and labels are desired, so I left that out for now.

Here's how to get what you want. This solution uses two twined axes object to get different scaling to the left and right of the origin, and then hides all the evidence:
import matplotlib.pyplot as plt
import matplotlib as mpl
from numbers import Number
tickkwargs = {m+k:False for k in ('bottom','top','left','right') for m in ('','label')}
p = np.zeros((10, 3, 2))
p[:,0,0] -= np.arange(10)*.1 + .5
p[:,1,1] += np.repeat(np.arange(5), 2)*.1 + .3
p[:,2,0] += np.arange(10)*.5 + 2
fig = plt.figure(figsize=(8,6))
host = fig.add_subplot(111)
par = host.twiny()
host.set_xlim(-6, 6)
par.set_xlim(-1, 1)
for ps in p:
# mask the points with negative x values
ppos = ps[ps[:,0] >= 0].T
host.plot(*ppos)
# mask the points with positive x values
pneg = ps[ps[:,0] <= 0].T
par.plot(*pneg)
# hide all possible ticks/notation text that could be set by the second x axis
par.tick_params(axis="both", **tickkwargs)
par.xaxis.get_offset_text().set_visible(False)
# fix the x tick labels so they're all positive
host.set_xticklabels(np.abs(host.get_xticks()))
fig.show()
Output:
Here's what the set of points p I used in the code above look like when plotted normally:
fig = plt.figure(figsize=(8,6))
ax = fig.gca()
for ps in p:
ax.plot(*ps.T)
fig.show()
Output:

The method of deriving a class of mscale.ScaleBase as shown in other answers may be too complicated for your purpose.
You can pass two scale transform functions to set_xscale or set_yscale, something like the following.
def get_scale(a=1): # a is the scale of your negative axis
def forward(x):
x = (x >= 0) * x + (x < 0) * x * a
return x
def inverse(x):
x = (x >= 0) * x + (x < 0) * x / a
return x
return forward, inverse
fig, ax = plt.subplots()
forward, inverse = get_scale(a=3)
ax.set_xscale('function', functions=(forward, inverse)) # this is for setting x axis
# do plotting
More examples can be found in this doc.

Related

Adding secondary axis and making grid lines equal

I am trying to add a secondary axis to a plot and make the grid lines equally spaced along y, but I the code below doesn't do what it is supposed to. y2A,y2B values are not right - they refer to xlim values not ylim. Any ideas?
import numpy as np
import matplotlib.pyplot as plt
def CtoF(y):
return y * 1.8 + 32
def FtoC(y):
return (y - 32) / 1.8
def setAxis2(ax1):
ax2 = ax1.secondary_yaxis('right', functions=(CtoF, FtoC))
ax2.set_ylabel('Fahrenheit')
return ax2
x = np.arange(100)
y = np.random.rand(100)
plt.plot(x,y)
ax1 = plt.gca()
ax1.set_ylabel('Celsius')
ax1.grid()
#Add the 2nd axis for Fahrenheit
ax2 = setAxis2(ax1)
#Get the ylimits and space them equally
[y1A,y1B] = ax1.get_ylim()
[y2A,y2B] = ax2.get_ylim()
ax1.set_yticks(np.linspace(y1A,y1B, 10))
ax2.set_yticks(np.linspace(y2A,y2B, 10)) #Doesn't work
print(y1A,y1B) #
print(y2A,y2B) #Doesn't output the expected values
I tried another method that works well (with the same versions of matplotlib), but the question remains about the issue above. The method that works is below:
ticks1 = ax1.get_yticks()
ticks2 = CtoF(ticks1)
ax2.set_yticks(ticks2)
Instead of getting y2A and y2B from the y-limits of ax2, we can calculate them directly with CtoF:
# Get the y-limits and space them equally.
y1A, y1B = ax1.get_ylim()
y2A, y2B = map(CtoF, (y1A, y1B))
n = 10
ax1.set_yticks(np.linspace(y1A, y1B, n))
ax2.set_yticks(np.linspace(y2A, y2B, n))

getting matplotlib radar plot with pandas

I am trying to go a step further by creating a radar plot like this question states. I using the same source code that the previous question was using, except I'm trying to implement this using pandas dataframe and pivot tables.
import numpy as np
import pandas as pd
from StringIO import StringIO
import matplotlib.pyplot as plt
from matplotlib.projections.polar import PolarAxes
from matplotlib.projections import register_projection
def radar_factory(num_vars, frame='circle'):
"""Create a radar chart with `num_vars` axes."""
# calculate evenly-spaced axis angles
theta = 2 * np.pi * np.linspace(0, 1 - 1. / num_vars, num_vars)
# rotate theta such that the first axis is at the top
theta += np.pi / 2
def draw_poly_frame(self, x0, y0, r):
# TODO: use transforms to convert (x, y) to (r, theta)
verts = [(r * np.cos(t) + x0, r * np.sin(t) + y0) for t in theta]
return plt.Polygon(verts, closed=True, edgecolor='k')
def draw_circle_frame(self, x0, y0, r):
return plt.Circle((x0, y0), r)
frame_dict = {'polygon': draw_poly_frame, 'circle': draw_circle_frame}
if frame not in frame_dict:
raise ValueError, 'unknown value for `frame`: %s' % frame
class RadarAxes(PolarAxes):
"""Class for creating a radar chart (a.k.a. a spider or star chart)
http://en.wikipedia.org/wiki/Radar_chart
"""
name = 'radar'
# use 1 line segment to connect specified points
RESOLUTION = 1
# define draw_frame method
draw_frame = frame_dict[frame]
def fill(self, *args, **kwargs):
"""Override fill so that line is closed by default"""
closed = kwargs.pop('closed', True)
return super(RadarAxes, self).fill(closed=closed, *args, **kwargs)
def plot(self, *args, **kwargs):
"""Override plot so that line is closed by default"""
lines = super(RadarAxes, self).plot(*args, **kwargs)
for line in lines:
self._close_line(line)
def _close_line(self, line):
x, y = line.get_data()
# FIXME: markers at x[0], y[0] get doubled-up
if x[0] != x[-1]:
x = np.concatenate((x, [x[0]]))
y = np.concatenate((y, [y[0]]))
line.set_data(x, y)
def set_varlabels(self, labels):
self.set_thetagrids(theta * 180 / np.pi, labels)
def _gen_axes_patch(self):
x0, y0 = (0.5, 0.5)
r = 0.5
return self.draw_frame(x0, y0, r)
register_projection(RadarAxes)
return theta
def day_radar_plot(df):
fig = plt.figure(figsize=(6,6))
#adjust spacing around the subplots
fig.subplots_adjust(wspace=0.25,hspace=0.20,top=0.85,bottom=0.05)
ldo,rup = 0.1,0.8 #leftdown and right up normalized
ax = fig.add_axes([ldo,ldo,rup,rup],polar=True)
N = len(df['Group1'].unique())
theta = radar_factory(N)
polar_df = pd.DataFrame(df.groupby([df['Group1'],df['Type'],df['Vote']]).size())
polar_df.columns = ['Count']
radii = polar_df['Count'].get_values()
names = polar_df.index.get_values()
#get the number of unique colors needed
num_colors_needed = len(names)
#Create the list of unique colors needed for red and blue shades
Rcolors = []
Gcolors = []
for i in range(num_colors_needed):
ri=1-(float(i)/float(num_colors_needed))
gi=0.
bi=0.
Rcolors.append((ri,gi,bi))
for i in range(num_colors_needed):
ri=0.
gi=1-(float(i)/float(num_colors_needed))
bi=0.
Gcolors.append((ri,gi,bi))
from_x = np.linspace(0,0.95,num_colors_needed)
to_x = from_x + 0.05
i = 0
for d,f,R,G in zip(radii,polar_df.index,Rcolors,Gcolors):
i = i+1
if f[2].lower() == 'no':
ax.plot(theta,d,color=R)
ax.fill(theta,d,facecolor=R,alpha=0.25)
#this is where I think i have the issue
ax.axvspan(from_x[i],to_x[i],color=R)
elif f[2].lower() == 'yes':
ax.plot(theta,d,color=G)
ax.fill(theta,d,facecolor=G,alpha=0.25)
#this is where I think i have the issue
ax.axvspan(from_x[i],to_x[i],color=G)
plt.show()
So, let's say I have this StringIO that has a list of Group1 voting either yes or no and they are from a numbered type..these numbers are arbitrary in labeling but just as an example..
fakefile = StringIO("""\
Group1,Type,Vote
James,7,YES\nRachael,7,YES\nChris,2,YES\nRachael,9,NO
Chris,2,YES\nChris,7,NO\nRachael,9,NO\nJames,2,NO
James,7,NO\nJames,9,YES\nRachael,9,NO
Chris,2,YES\nChris,2,YES\nRachael,7,NO
Rachael,7,YES\nJames,9,YES\nJames,9,NO
Rachael,2,NO\nChris,2,YES\nRachael,7,YES
Rachael,9,NO\nChris,9,NO\nJames,7,NO
James,2,YES\nChris,2,NO\nRachael,9,YES
Rachael,9,YES\nRachael,2,NO\nChris,7,YES
James,7,YES\nChris,9,NO\nRachael,9,NO\n
Chris,9,YES
""")
record = pd.read_csv(fakefile, header=0)
day_radar_plot(record)
The error I get is Value Error: x and y must have same first dimension.
As I indicated in my script, I thought I had a solution for it but apparently I'm going by it the wrong way. Does anyone have any advice or guidance?
Since I'm completely lost in what you are trying to do, I will simply provide a solution on how to draw a radar chart from the given data.
It will answer the question how often have people voted Yes or No.
import pandas as pd
import numpy as np
from StringIO import StringIO
import matplotlib.pyplot as plt
fakefile = StringIO("""\
Group1,Type,Vote
James,7,YES\nRachael,7,YES\nChris,2,YES\nRachael,9,NO
Chris,2,YES\nChris,7,NO\nRachael,9,NO\nJames,2,NO
James,7,NO\nJames,9,YES\nRachael,9,NO
Chris,2,YES\nChris,2,YES\nRachael,7,NO
Rachael,7,YES\nJames,9,YES\nJames,9,NO
Rachael,2,NO\nChris,2,YES\nRachael,7,YES
Rachael,9,NO\nChris,9,NO\nJames,7,NO
James,2,YES\nChris,2,NO\nRachael,9,YES
Rachael,9,YES\nRachael,2,NO\nChris,7,YES
James,7,YES\nChris,9,NO\nRachael,9,NO\n
Chris,9,YES""")
df = pd.read_csv(fakefile, header=0)
df["cnt"] = np.ones(len(df))
pt = pd.pivot_table(df, values='cnt', index=['Group1'],
columns=['Vote'], aggfunc=np.sum)
fig = plt.figure()
ax = fig.add_subplot(111, projection="polar")
theta = np.arange(len(pt))/float(len(pt))*2.*np.pi
l1, = ax.plot(theta, pt["YES"], color="C2", marker="o", label="YES")
l2, = ax.plot(theta, pt["NO"], color="C3", marker="o", label="NO")
def _closeline(line):
x, y = line.get_data()
x = np.concatenate((x, [x[0]]))
y = np.concatenate((y, [y[0]]))
line.set_data(x, y)
[_closeline(l) for l in [l1,l2]]
ax.set_xticks(theta)
ax.set_xticklabels(pt.index)
plt.legend()
plt.title("How often have people votes Yes or No?")
plt.show()

Matplotlib/pyplot: Auto adjust unit of y Axis

I would like to modify the Y axis unit of the plot indicated below. Preferable would be the use of units like M (Million), k (Thousand) for large numbers. For example, the y Axis should look like: 50k, 100k, 150k, etc.
The plot below is generated by the following code snippet:
plt.autoscale(enable=True, axis='both')
plt.title("TTL Distribution")
plt.xlabel('TTL Value')
plt.ylabel('Number of Packets')
y = graphy # data from a sqlite query
x = graphx # data from a sqlite query
width = 0.5
plt.bar(x, y, width, align='center', linewidth=2, color='red', edgecolor='red')
fig = plt.gcf()
plt.show()
I saw this post and thought I could write my own formatting function:
def y_fmt(x, y):
if max_y > 1000000:
val = int(y)/1000000
return '{:d} M'.format(val)
elif max_y > 1000:
val = int(y) / 1000
return '{:d} k'.format(val)
else:
return y
But I missed that there is no plt.yaxis.set_major_formatter(tick.FuncFormatter(y_fmt)) function available for the bar plot I am using.
How I can achieve a better formatting of the Y axis?
[]
In principle there is always the option to set custom labels via plt.gca().yaxis.set_xticklabels().
However, I'm not sure why there shouldn't be the possibility to use matplotlib.ticker.FuncFormatter here. The FuncFormatter is designed for exactly the purpose of providing custom ticklabels depending on the ticklabel's position and value.
There is actually a nice example in the matplotlib example collection.
In this case we can use the FuncFormatter as desired to provide unit prefixes as suffixes on the axes of a matplotlib plot. To this end, we iterate over the multiples of 1000 and check if the value to be formatted exceeds it. If the value is then a whole number, we can format it as integer with the respective unit symbol as suffix. On the other hand, if there is a remainder behind the decimal point, we check how many decimal places are needed to format this number.
Here is a complete example:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.ticker import FuncFormatter
def y_fmt(y, pos):
decades = [1e9, 1e6, 1e3, 1e0, 1e-3, 1e-6, 1e-9 ]
suffix = ["G", "M", "k", "" , "m" , "u", "n" ]
if y == 0:
return str(0)
for i, d in enumerate(decades):
if np.abs(y) >=d:
val = y/float(d)
signf = len(str(val).split(".")[1])
if signf == 0:
return '{val:d} {suffix}'.format(val=int(val), suffix=suffix[i])
else:
if signf == 1:
print val, signf
if str(val).split(".")[1] == "0":
return '{val:d} {suffix}'.format(val=int(round(val)), suffix=suffix[i])
tx = "{"+"val:.{signf}f".format(signf = signf) +"} {suffix}"
return tx.format(val=val, suffix=suffix[i])
#return y
return y
fig, ax = plt.subplots(ncols=3, figsize=(10,5))
x = np.linspace(0,349,num=350)
y = np.sinc((x-66.)/10.3)**2*1.5e6+np.sinc((x-164.)/8.7)**2*660000.+np.random.rand(len(x))*76000.
width = 1
ax[0].bar(x, y, width, align='center', linewidth=2, color='red', edgecolor='red')
ax[0].yaxis.set_major_formatter(FuncFormatter(y_fmt))
ax[1].bar(x[::-1], y*(-0.8e-9), width, align='center', linewidth=2, color='orange', edgecolor='orange')
ax[1].yaxis.set_major_formatter(FuncFormatter(y_fmt))
ax[2].fill_between(x, np.sin(x/100.)*1.7+100010, np.cos(x/100.)*1.7+100010, linewidth=2, color='#a80975', edgecolor='#a80975')
ax[2].yaxis.set_major_formatter(FuncFormatter(y_fmt))
for axes in ax:
axes.set_title("TTL Distribution")
axes.set_xlabel('TTL Value')
axes.set_ylabel('Number of Packets')
axes.set_xlim([x[0], x[-1]+1])
plt.show()
which provides the following plot:
You were pretty close; one (possibly) confusing thing about FuncFormatter is that the first argument is the tick value, and the second the tick position , which (when named x,y) can be confusing for the y-axis. For clarity, I renamed them in the example below.
The function should take in two inputs (tick value x and position pos) and return a string
(http://matplotlib.org/api/ticker_api.html#matplotlib.ticker.FuncFormatter)
Working example:
import numpy as np
import matplotlib.pylab as pl
import matplotlib.ticker as tick
def y_fmt(tick_val, pos):
if tick_val > 1000000:
val = int(tick_val)/1000000
return '{:d} M'.format(val)
elif tick_val > 1000:
val = int(tick_val) / 1000
return '{:d} k'.format(val)
else:
return tick_val
x = np.arange(300)
y = np.random.randint(0,2000000,x.size)
width = 0.5
pl.bar(x, y, width, align='center', linewidth=2, color='red', edgecolor='red')
pl.xlim(0,300)
ax = pl.gca()
ax.yaxis.set_major_formatter(tick.FuncFormatter(y_fmt))

Matplotlib darker hsv colormap

I'm using the HSV colormap from matplotlib to plot some vector fields. Is there a way to darken or make smoother the HSV colours so they look more like this
than my original plot colours, which are too bright:
Introduction
Assuming you're trying to plot a pcolor image like this:
import numpy as np
import matplotlib.pyplot as plt
y, x = np.mgrid[slice(-3, 3 + 0.05, 0.05),
slice(-3, 3 + 0.15, 0.15)]
z = (1 - x / 2. + x ** 5 + y ** 3) * np.exp(-x ** 2 - y ** 2)
# x and y are bounds, so z should be the value *inside* those bounds.
# Therefore, remove the last value from the z array.
z = z[:-1, :-1]
fig = plt.figure(1)
fig.clf()
ax = plt.gca()
pcol = ax.pcolormesh(x, y, z, cmap=plt.get_cmap('hsv'), )
plt.colorbar(pcol)
ax.set_xlim([-3, 3])
ax.set_ylim([-3, 3])
Your image will be:
Methods
I've written an alternate implementation of the MPL cookbook cmap_map function that modifies colormaps. In addition to support for kwargs and pep8 compliance, this version handles discontinuities in a colormap:
import numpy as np
from matplotlib.colors import LinearSegmentedColormap as lsc
def cmap_map(function, cmap, name='colormap_mod', N=None, gamma=None):
"""
Modify a colormap using `function` which must operate on 3-element
arrays of [r, g, b] values.
You may specify the number of colors, `N`, and the opacity, `gamma`,
value of the returned colormap. These values default to the ones in
the input `cmap`.
You may also specify a `name` for the colormap, so that it can be
loaded using plt.get_cmap(name).
"""
if N is None:
N = cmap.N
if gamma is None:
gamma = cmap._gamma
cdict = cmap._segmentdata
# Cast the steps into lists:
step_dict = {key: map(lambda x: x[0], cdict[key]) for key in cdict}
# Now get the unique steps (first column of the arrays):
step_list = np.unique(sum(step_dict.values(), []))
# 'y0', 'y1' are as defined in LinearSegmentedColormap docstring:
y0 = cmap(step_list)[:, :3]
y1 = y0.copy()[:, :3]
# Go back to catch the discontinuities, and place them into y0, y1
for iclr, key in enumerate(['red', 'green', 'blue']):
for istp, step in enumerate(step_list):
try:
ind = step_dict[key].index(step)
except ValueError:
# This step is not in this color
continue
y0[istp, iclr] = cdict[key][ind][1]
y1[istp, iclr] = cdict[key][ind][2]
# Map the colors to their new values:
y0 = np.array(map(function, y0))
y1 = np.array(map(function, y1))
# Build the new colormap (overwriting step_dict):
for iclr, clr in enumerate(['red', 'green', 'blue']):
step_dict[clr] = np.vstack((step_list, y0[:, iclr], y1[:, iclr])).T
return lsc(name, step_dict, N=N, gamma=gamma)
Implementation
To use it, simply define a function that will modify your RGB colors as you like (values from 0 to 1) and supply it as input to cmap_map. To get colors close to the ones in the images you provided, for example, you could define:
def darken(x, ):
return x * 0.8
dark_hsv = cmap_map(darken, plt.get_cmap('hsv'))
And then modify the call to pcolormesh:
pcol = ax.pcolormesh(x, y, z, cmap=dark_hsv)
If you only wanted to darken the greens in the image, you could do (now all in one line):
pcol = ax.pcolormesh(x, y, z,
cmap=cmap_map(lambda x: x * [1, 0.7, 1],
plt.get_cmap('hsv'))
)

Matplotlib bad ticks/labels for loglog (twin axis)

I'm creating loglog plots with matplotlib. As can be seen in the figure below, the default ticks are chosen badly (at best); the right y-axis doesn't even have any at all (it does in the linear equivalent) and both x-axis have only one.
Is there a way to get a reasonable number of ticks with labels, without specifying them by hand for every plot?
EDIT: the exact code is too long, but here's a short example of the problem:
x = linspace(4, 18, 20)
y = 1 / (x ** 4)
fig = figure()
ax = fig.add_axes([.1, .1, .8, .8])
ax.loglog(x, y)
ax.set_xlim([4, 18])
ax2 = ax.twiny()
ax2.set_xlim([4 / 3., 18 / 3.])
ax2.set_xscale('log')
show()
I've been fighting with something like what you show (only one major tick in the axis range). None of the matplotlib tick formatter satisfied me, so I use matplotlib.ticker.FuncFormatter to achieve what I wanted. I haven't tested with twin axes, but my feeling is that it should work anyway.
import matplotlib.pyplot as plt
from matplotlib import ticker
import numpy as np
##Mark: thanks for the suggestion :D
mi, ma, conv = 4, 8, 1./3.
x = np.linspace(mi, ma, 20)
y = 1 / (x ** 4)
fig, ax = plt.subplots()
ax.plot(x, y) # plot the lines
ax.set_xscale('log') #convert to log
ax.set_yscale('log')
ax.set_xlim([0.2, 1.8]) #large enough, but should show only 1 tick
def ticks_format(value, index):
"""
This function decompose value in base*10^{exp} and return a latex string.
If 0<=value<99: return the value as it is.
if 0.1<value<0: returns as it is rounded to the first decimal
otherwise returns $base*10^{exp}$
I've designed the function to be use with values for which the decomposition
returns integers
"""
exp = np.floor(np.log10(value))
base = value/10**exp
if exp == 0 or exp == 1:
return '${0:d}$'.format(int(value))
if exp == -1:
return '${0:.1f}$'.format(value)
else:
return '${0:d}\\times10^{{{1:d}}}$'.format(int(base), int(exp))
# here specify which minor ticks per decate you want
# likely all of them give you a too crowed axis
subs = [1., 3., 6.]
# set the minor locators
ax.xaxis.set_minor_locator(ticker.LogLocator(subs=subs))
ax.yaxis.set_minor_locator(ticker.LogLocator(subs=subs))
# remove the tick labels for the major ticks:
# if not done they will be printed with the custom ones (you don't want it)
# plus you want to remove them to avoid font missmatch: the above function
# returns latex string, and I don't know how matplotlib does exponents in labels
ax.xaxis.set_major_formatter(ticker.NullFormatter())
ax.yaxis.set_major_formatter(ticker.NullFormatter())
# set the desired minor tick labels using the above function
ax.xaxis.set_minor_formatter(ticker.FuncFormatter(ticks_format))
ax.yaxis.set_minor_formatter(ticker.FuncFormatter(ticks_format))
The figure that I get is the following :
Of course you can set different minor locators for x and y axis and you can wrap everything from ticks_format to the end into a function that accepts an axes instance ax and subs or subsx and subsy as input parameters.
I hope that this helps you
In the end, this is the best I could come up with with the help of other answers here and elsewere is this:
On the left, x and y vary over only a part of an order of magnitude, with labels working out fairly well. On the left, x varies between 1 and 2 orders of magnitude. It works okay, but the method is reaching it's limit. The y values vary many orders of magnitude and the standard labels are used automatically.
from matplotlib import ticker
from numpy import linspace, logspace, log10, floor
from warnings import warn
def round_to_n(x, n):
''' http://stackoverflow.com/questions/3410976/how-to-round-a-number-to-significant-figures-in-python '''
return round(x, -int(floor(log10(abs(x)))) + (n - 1))
def ticks_log_format(value, index):
''' http://stackoverflow.com/questions/19239297/matplotlib-bad-ticks-labels-for-loglog-twin-axis '''
pwr = floor(log10(value))
base = value / (10 ** pwr)
if pwr == 0 or pwr == 1:
return '${0:d}$'.format(int(value))
if -3 <= pwr < 0:
return '${0:.3g}$'.format(value)
if 0 < pwr <= 3:
return '${0:d}$'.format(int(value))
else:
return '${0:d}\\times10^{{{1:d}}}$'.format(int(base), int(pwr))
def calc_ticks(domain, tick_count, equidistant):
if equidistant:
ticks = logspace(log10(domain[0]), log10(domain[1]), num = tick_count, base = 10)
else:
ticks = linspace(domain[0], domain[1], num = tick_count)
for n in range(1, 6):
if len(set(round_to_n(tick, n) for tick in ticks)) == tick_count:
break
return list(round_to_n(tick, n) for tick in ticks)
''' small domain log ticks '''
def sdlt_x(ax, domain, tick_count = 4, equidistant = True):
''' http://stackoverflow.com/questions/3410976/how-to-round-a-number-to-significant-figures-in-python '''
if min(domain) <= 0:
warn('domain %g-%g contains values lower than 0' % (domain[0], domain[1]))
domain = [max(value, 0.) for value in domain]
ax.set_xscale('log')
ax.set_xlim(domain)
ax.xaxis.set_major_formatter(ticker.FuncFormatter(ticks_log_format))
if log10(max(domain) / min(domain)) > 1.7:
return
ticks = calc_ticks(domain, tick_count = tick_count, equidistant = equidistant)
ax.set_xticks(ticks)
''' any way to prevent this code duplication? '''
def sdlt_y(ax, domain, tick_count = 5, equidistant = True):
''' http://stackoverflow.com/questions/3410976/how-to-round-a-number-to-significant-figures-in-python '''
if min(domain) <= 0:
warn('domain %g-%g contains values lower than 0' % (domain[0], domain[1]))
domain = [max(value, 1e-8) for value in domain]
ax.set_yscale('log')
ax.set_ylim(domain)
ax.yaxis.set_major_formatter(ticker.FuncFormatter(ticks_log_format))
if log10(max(domain) / min(domain)) > 1.7:
return
ticks = calc_ticks(domain, tick_count = tick_count, equidistant = equidistant)
ax.set_yticks(ticks)
''' demo '''
fig, (ax1, ax2,) = plt.subplots(1, 2)
for mi, ma, ax in ((100, 130, ax1,), (10, 400, ax2,), ):
x = np.linspace(mi, ma, 50)
y = 1 / ((x + random(50) * 0.1 * (ma - mi)) ** 4)
ax.scatter(x, y)
sdlt_x(ax, (mi, ma, ))
sdlt_y(ax, (min(y), max(y), ))
show()
EDIT: updated with an option to make labels equidistant (so the values are logarithmic, but the visible positions are equidistant).