如何在对数对称刻度上放置次要刻度?

23

我使用matplotlib的symlog比例尺来涵盖参数范围的正负方向。遗憾的是,symlog比例尺不太直观,也不太常用。因此,我想通过在主刻度之间放置次刻度来使所使用的比例更加明显。在对数部分的刻度上,我希望在e附近的主刻度处放置[2,3,...,9]*10^e的刻度。此外,在0到0.1之间的范围应该用均匀分布的次刻度覆盖,每个刻度相隔0.01。我尝试使用matplotlib.ticker API编写以下代码以实现这些刻度:

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.ticker import LogLocator, AutoLocator

x = np.linspace(-5, 5, 100)
y = x

plt.plot(x, y)
plt.yscale('symlog', linthreshy=1e-1)

yaxis = plt.gca().yaxis
yaxis.set_minor_locator(LogLocator(subs=np.arange(2, 10)))

plt.show()

很遗憾,这不能产生我想要的结果:

enter image description here

请注意,0周围有太多的次刻度线,这可能是由于 LogLocator。此外,在负轴上没有小刻度。

如果使用 AutoLocator,则不会出现任何小刻度。 AutoMinorLocator 仅支持均匀缩放的轴。因此,我的问题是如何实现所需的刻度放置?


我做了比被接受的答案更简单的事情,可能适用于我需要的示例。我使用了ax.set_xticks方法。 - Andrew
4个回答

18

深入研究这个问题后,我发现很难找到一个通用解决方案。幸运的是,我可以对我的数据做一些约束并自定义一个类来解决这个问题:

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.ticker import Locator


class MinorSymLogLocator(Locator):
    """
    Dynamically find minor tick positions based on the positions of
    major ticks for a symlog scaling.
    """
    def __init__(self, linthresh):
        """
        Ticks will be placed between the major ticks.
        The placement is linear for x between -linthresh and linthresh,
        otherwise its logarithmically
        """
        self.linthresh = linthresh

    def __call__(self):
        'Return the locations of the ticks'
        majorlocs = self.axis.get_majorticklocs()

        # iterate through minor locs
        minorlocs = []

        # handle the lowest part
        for i in xrange(1, len(majorlocs)):
            majorstep = majorlocs[i] - majorlocs[i-1]
            if abs(majorlocs[i-1] + majorstep/2) < self.linthresh:
                ndivs = 10
            else:
                ndivs = 9
            minorstep = majorstep / ndivs
            locs = np.arange(majorlocs[i-1], majorlocs[i], minorstep)[1:]
            minorlocs.extend(locs)

        return self.raise_if_exceeds(np.array(minorlocs))

    def tick_values(self, vmin, vmax):
        raise NotImplementedError('Cannot get tick locations for a '
                                  '%s type.' % type(self))


x = np.linspace(-5, 5, 100)
y = x

plt.plot(x, y)
plt.yscale('symlog', linthreshy=1e-1)

yaxis = plt.gca().yaxis
yaxis.set_minor_locator(MinorSymLogLocator(1e-1))

plt.show()

这会产生以下结果:

在此输入图片描述

请注意,此方法仅在主刻度之间放置刻度。当您缩放和平移图像时,这将变得明显。此外,线性阈值必须显式提供给类,因为我找不到轴本身轻松而可靠地读取它的方式。


1
你能否向上游开放一个PR,将此添加到mpl代码库中? - tacaswell
为了直接从图形数据中获取阈值,我使用以下代码:LN = matplotlib.lines.Line2D threshold = 10**min([int(round(np.log(np.min(np.abs(line.get_xdata() if isinstance(line,LN) else line.get_offsets())))))-2 for line in ax.lines+ax.collections]) - Devin Charles

6

OP的解决方案很好,但是如果刻度不是线性阈值的倍数,则不会在轴的边缘产生刻度标记。我已经修改了OP的MinorSymLogLocator()类,并给出了以下解决方案(通过在设置次刻度位置时添加临时主刻度位置来填充边缘):

class MinorSymLogLocator(Locator):
    """
    Dynamically find minor tick positions based on the positions of
    major ticks for a symlog scaling.
    """
    def __init__(self, linthresh, nints=10):
        """
        Ticks will be placed between the major ticks.
        The placement is linear for x between -linthresh and linthresh,
        otherwise its logarithmically. nints gives the number of
        intervals that will be bounded by the minor ticks.
        """
        self.linthresh = linthresh
        self.nintervals = nints

    def __call__(self):
        # Return the locations of the ticks
        majorlocs = self.axis.get_majorticklocs()

        if len(majorlocs) == 1:
            return self.raise_if_exceeds(np.array([]))

        # add temporary major tick locs at either end of the current range
        # to fill in minor tick gaps
        dmlower = majorlocs[1] - majorlocs[0]    # major tick difference at lower end
        dmupper = majorlocs[-1] - majorlocs[-2]  # major tick difference at upper end

        # add temporary major tick location at the lower end
        if majorlocs[0] != 0. and ((majorlocs[0] != self.linthresh and dmlower > self.linthresh) or (dmlower == self.linthresh and majorlocs[0] < 0)):
            majorlocs = np.insert(majorlocs, 0, majorlocs[0]*10.)
        else:
            majorlocs = np.insert(majorlocs, 0, majorlocs[0]-self.linthresh)

        # add temporary major tick location at the upper end
        if majorlocs[-1] != 0. and ((np.abs(majorlocs[-1]) != self.linthresh and dmupper > self.linthresh) or (dmupper == self.linthresh and majorlocs[-1] > 0)):
            majorlocs = np.append(majorlocs, majorlocs[-1]*10.)
        else:
            majorlocs = np.append(majorlocs, majorlocs[-1]+self.linthresh)

        # iterate through minor locs
        minorlocs = []

        # handle the lowest part
        for i in xrange(1, len(majorlocs)):
            majorstep = majorlocs[i] - majorlocs[i-1]
            if abs(majorlocs[i-1] + majorstep/2) < self.linthresh:
                ndivs = self.nintervals
            else:
                ndivs = self.nintervals - 1.

            minorstep = majorstep / ndivs
            locs = np.arange(majorlocs[i-1], majorlocs[i], minorstep)[1:]
            minorlocs.extend(locs)

        return self.raise_if_exceeds(np.array(minorlocs))

    def tick_values(self, vmin, vmax):
        raise NotImplementedError('Cannot get tick locations for a '
                          '%s type.' % type(self))

2
我找到了一种更简单的方法,可能适用于此问题:
我使用了ax.set_xticks方法,并将下面函数的输出作为参数传入:
def gen_tick_positions(scale_start=100, scale_max=10000):

    start, finish = np.floor(np.log10((scale_start, scale_max)))
    finish += 1
    majors = [10 ** x for x in np.arange(start, finish)]
    minors = []
    for idx, major in enumerate(majors[:-1]):
        minor_list = np.arange(majors[idx], majors[idx+1], major)
        minors.extend(minor_list[1:])
    return minors, majors

对于这个例子,您可以从ax.get_yticks()推断出线性区域(即围绕零且与0-1/10不相差10倍的值)。
y_ticks = ax.get_yticks()
total_scale = list(y_ticks)

zero_point = total_scale.index(0.0)
post_zeroes = np.log10(total_scale[zero_point+1:])
first_log = []
for idx, value in enumerate(post_zeroes[:-1]):
    if 1.005 > post_zeroes[idx+1] - value > 0.995:
        first_log = total_scale[idx + zero_point]

这样,您就可以得到一个起始值,将其放入上面的函数中,其中scale_max可以是任何您喜欢的值,例如total_scale[-1]

您可以使用first_log的正区域和负区域生成线性刻度,然后将列表合并。

lin_ticks = list(np.linspace(first_log * -1, first_log, 21))
pos_log_ticks_minors, pos_log_ticks_majors = gen_tick_positions(first_log, scale_max)
neg_log_ticks_minors = [x * -1 for x in pos_log_ticks_minors]
neg_log_ticks_majors = [x * -1 for x in pos_log_ticks_majors]

final_scale_minors = neg_log_ticks_minors + lin_ticks + pos_log_ticks_minors

The merged list can then be passed into e.g.

ax.set_yticks(final_scale_minors, minor=True)

虽然我想到了您不需要从图表或轴上读取线性阈值,因为在应用“symlog”时已指定为参数。


0
仅对David的回答进行一些补充,同时也解决了Matt提出的问题。 请注意,现在matplotlib.scale.SymmetricalLogScale类也有一个子参数来处理这个问题,尽管它并没有真正处理阈值内外的情况。如果您有建议和更正,请在评论中指出。

        majorlocs = self.axis.get_majorticklocs()
        # my changes to previous solution
        # this adds one majortickloc below and above the axis range
        # to extend the minor ticks outside the range of majorticklocs
        # bottom of the axis (low values)
        first_major = majorlocs[0]
        if first_major == 0:
            outrange_first = -self.linthresh
        else:
            outrange_first = first_major * float(10) ** (- np.sign(first_major))
        # top of the axis (high values)
        last_major = majorlocs[-1]
        if last_major == 0:
            outrange_last = self.linthresh
        else:
            outrange_last = last_major * float(10) ** (np.sign(last_major))
        majorlocs = np.concatenate(([outrange_first], majorlocs, [outrange_last]))

然后继续阅读大卫的回答...

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接