使用自定义刻度

时间:2020-09-01 15:30:40

标签: python matplotlib

我编写了以下自定义Scale类:

import numpy as np
from matplotlib import scale as mscale
from matplotlib import transforms as mtransforms
from matplotlib.ticker import Formatter

class PiecewiseLinearScale(mscale.ScaleBase):
    name = 'piecewise'

    def __init__(self, axis, *, thresholds=None, true_values=None, **kwargs):
        mscale.ScaleBase.__init__(self)
        if thresholds is None:
            thresholds = [0, 1, 3, 5, 10]
        if true_values is None:
            true_values = [0, 1, 2, 3, 4]
        if len(thresholds) != len(true_values):
            raise ValueError("thresholds and true_values should have the same length")
        tp = thresholds[0]
        for t in thresholds[1:]:
            if t <= tp:
                raise ValueError("Values in thresholds array should be ascending")
            t = tp

        tp = true_values[0]
        for t in true_values[1:]:
            if t <= tp:
                raise ValueError("Values in true_values array should be ascending")
            t = tp
        self.thresholds = thresholds
        self.true_values = true_values

    def get_transform(self):
        return self.PiecewiseLinearLatitudeTransform(self.thresholds, self.true_values)

    def set_default_locators_and_formatters(self, axis):
        class DegreeFormatter(Formatter):
            def __call__(self, x, pos=None):
                return "{}".format(x)

        axis.set_major_formatter(DegreeFormatter())
        axis.set_minor_formatter(DegreeFormatter())

    def limit_range_for_scale(self, vmin, vmax, minpos):
        return max(vmin, self.true_values[0]), min(vmax, self.true_values[-1])

    class PiecewiseLinearLatitudeTransform(mtransforms.Transform):
        input_dims = 1
        output_dims = 1
        is_separable = True
        has_inverse = True

        def __init__(self, thresholds = None, true_values = None):
            mtransforms.Transform.__init__(self)
            if thresholds is None:
                thresholds = [0, 1, 3, 5, 10]
            if true_values is None:
                true_values = [0, 1, 2, 3, 4]
            self.true_values = true_values
            self.thresholds = thresholds

        def transform_non_affine(self, a):
            masked = np.ma.masked_where((a < self.true_values[0]) | (a > self.true_values[-1]), a)
            # val = None
            proper_stuff = []
            for val in masked:
                if isinstance(val, np.ma.core.MaskedConstant):
                    proper_stuff.append(np.ma.core.MaskedConstant())
                    continue
                for i, t_v in enumerate(self.true_values[1:]):
                    if t_v >= val:
                        proper_true_ind = i
                        break
                else:
                    proper_true_ind = i
                unbiased = val-self.true_values[proper_true_ind]

                true_values_raise = self.true_values[proper_true_ind+1] - self.true_values[proper_true_ind]
                thresholds_raise = self.thresholds[proper_true_ind+1] - self.thresholds[proper_true_ind]

                first_part = unbiased/true_values_raise
                unbiased_scaled = first_part*thresholds_raise

                proper_stuff.append(self.thresholds[proper_true_ind] + unbiased_scaled)

            return np.ma.array(proper_stuff)

        def inverted(self):
            return PiecewiseLinearScale.InvertedPiecewiseLinearLatitudeTransform(
                self.thresholds, self.true_values)

    class InvertedPiecewiseLinearLatitudeTransform(mtransforms.Transform):
        input_dims = 1
        output_dims = 1
        is_separable = True
        has_inverse = True

        def __init__(self, thresholds = None, true_values = None):
            mtransforms.Transform.__init__(self)
            if thresholds is None:
                thresholds = [0, 1, 3, 5, 10]
            if true_values is None:
                true_values = [0, 1, 2, 3, 4]
            self.true_values = true_values
            self.thresholds = thresholds

        def transform_non_affine(self, a):
            masked = np.ma.masked_where((a < self.thresholds[0]) | (a > self.thresholds[-1]), a)
            proper_stuff = []
            for val in masked:
                if isinstance(val, np.ma.core.MaskedConstant):
                    proper_stuff.append(np.ma.core.MaskedConstant())
                    continue
                for i, t_v in enumerate(self.thresholds[1:]):
                    if t_v >= val:
                        proper_true_ind = i
                        break
                    else:
                        proper_true_ind = i
                true_values_raise = self.true_values[proper_true_ind+1] - self.true_values[proper_true_ind]
                thresholds_raise = self.thresholds[proper_true_ind+1] - self.thresholds[proper_true_ind]

                unbiased = val-self.thresholds[proper_true_ind]
                proper_stuff.append(self.true_values[proper_true_ind] + unbiased*true_values_raise/thresholds_raise)
            return np.ma.array(proper_stuff)

        def inverted(self):
            return PiecewiseLinearScale.PiecewiseLinearLatitudeTransform(self.thresholds, self.true_values)

它正在按照我希望的方式进行转换。问题是,一旦在轴上应用了此比例尺,刻度就会消失。不幸的是,暂时不能使用最新的Matplotlib版本,但是非常相似的代码可以正常使用-区别仅在于调用PiecewiseLinearScale超类的构造函数。由于某些原因,matplotlib page with custom scale中的代码可以正常工作。

预期行为与实际行为的比较:

Current plot

Desired plot(obtained with matplotlib 3.3.1)

我用来生成这些图像的代码:

t = np.arange(0, 4.05, 0.1)

plt.plot(t, t)
plt.gca().set_yscale('piecewise', thresholds = [0, 1, 4, 9, 16], true_values = [0, 1, 2, 3, 4])

plt.xlabel('x')
plt.ylabel('y')
plt.title('Piecewise')
plt.grid(True)
plt.yticks([1, 2, 3, 4])
plt.ylim(0, 4)
plt.xlim(0, 4)
plt.show()

我希望使用解决方案/建议,该解决方案/建议仅更改第一段代码。

1 个答案:

答案 0 :(得分:0)

因此,经过一些调试

我发现,将掩码值放在transform_non_affine的方法InvertedPiecewiseLinearLatitudeTransform返回的数组中是错误的想法-我应该使用np.nan来代替。更改后的代码现在看起来像这样:

def transform_non_affine(self, a):
    masked = a.copy()
    masked[(a < self.thresholds[0]) | (a > self.thresholds[-1])] = np.nan
    proper_stuff = []
    for val in masked:
        if isinstance(val, np.ma.core.MaskedConstant):
            proper_stuff.append(np.ma.core.MaskedConstant())
            continue
        for i, t_v in enumerate(self.thresholds[1:]):
            if t_v >= val:
                proper_true_ind = i
                break
            else:
                proper_true_ind = i
        true_values_raise = self.true_values[proper_true_ind+1] - self.true_values[proper_true_ind]
        thresholds_raise = self.thresholds[proper_true_ind+1] - self.thresholds[proper_true_ind]

        unbiased = val-self.thresholds[proper_true_ind]
        proper_stuff.append(self.true_values[proper_true_ind] + unbiased*true_values_raise/thresholds_raise)
    res = np.array(proper_stuff)
    return res