osmgraphing 1.1.1

Playing around with graphs created via parsing OpenStreetMap data
Documentation
import numpy as np
import matplotlib.colors as colors

from matplotlib.colors import Normalize


def _check_inputs(vmin, vcenter, vmax):
    if vmax < vmin:
        raise ValueError('Should not be: vmax < vmin')
    if vcenter is not None:
        if vcenter < vmin:
            raise ValueError('Should not be: vcenter < vmin')
        elif vmax < vcenter:
            raise ValueError('Should not be: vmax < vcenter')
        elif (not vcenter < vmax) or (not vmin < vcenter):
            # vcenter is equal to vmin or vmax
            vcenter = None
    return vmin, vcenter, vmax


class TwoSlopeLoggedNorm(Normalize):
    '''
    Maps values between `[vmin, vmax]` to `[1, base]` before using log_base to map the values to `[0, 1]`.
    The base can be set when initializing this norm, resulting in different contrasts.

    If a `vcenter != vmin, vmax` is provided, values are mapped

    - from `[vmin, vcenter]` to `[-base, -1]` and inverted to `[1, base]`.
    - from `[vcenter, vmax]` to `[1, base]`.

    Using the log_base on both groups results in values between `[0, 1]` for each group.
    Values from the group of values between `[vmin, vcenter]` are inverted back to `[-1, 0]` and merged with the positive values `[0, 1]` from the group `[vcenter, vmax]`.
    A linear interpolation from `[-1, 0, 1]` to `[0, 0.5, 1]` finalizes the normalization, where `0.5` equals `vcenter`.
    '''

    def __init__(self, vcenter=None, base=2.0, **kwargs):
        super().__init__(**kwargs)
        self.base = base
        self.vcenter = vcenter

    def __call__(self, value, clip=None):
        '''
        In the following explanation, vcenter is expected to be zero.
        This helps understanding the comments.

        All positive values are mapped from [0, max] to [1, base] before
        log is taken, resulting in a value in [0, 1].

        Negative values are negated before being treated as a positive
        value. After mapping to [0, 1], the resulting value is negated
        again, resulting in a value in [-1, 0].
        '''

        # set and check data-dependent values

        vmin = self.vmin
        vmax = self.vmax
        if vmin is None:
            vmin = np.min(value)
        if vmax is None:
            vmax = np.max(value)
        vcenter = self.vcenter
        vmin, vcenter, vmax = _check_inputs(
            vmin=vmin,
            vcenter=vcenter,
            vmax=vmax
        )

        base = self.base
        if not base > 1.0:
            raise ValueError('Base should be greater than 1.0')
        log_base = np.log(base)

        # if vcenter is not set, map from [0, 1] to [1, base]
        # to use log for mapping into [0, 1] again.
        if vcenter is None:
            xp, fp = [vmin, vmax], [1.0, base]
            mapped_value = np.interp(x=value, xp=xp, fp=fp)
            mapped_value = np.log(mapped_value) / log_base
        else:
            xp, fp = [vmin, vcenter, vmax], [-1.0, 0.0, 1.0]
            mapped_value = 2.0 * np.interp(x=value, xp=xp, fp=fp)

            for i in range(len(mapped_value)):
                v = mapped_value[i]

                # positive value in (0, 1]
                if v > 0.0:
                    # 1) map to (1, base]
                    # 2) use log to map back to (0, 1]
                    v = np.log((base-1.0)*v + 1.0) / log_base
                # negative value in [1, 0)
                elif v < 0.0:
                    # 1) mirror to (0, 1]
                    # 2) map to (1, base]
                    # 3) use log to map back to (0, 1]
                    # 4) mirror back to [-1, 0)
                    v = -np.log(-(base-1.0)*v + 1.0) / log_base

                # v is in [-1, 1] and should be shifted to [0, 1]
                # -> shift negative values into [0, 0.5)
                # -> shift positive values into (0.5, 1]
                # -> shift zero to 0.5
                mapped_value[i] = v / 2.0 + 0.5
        return np.ma.masked_array(mapped_value)


class SigmoidNorm(Normalize):
    '''
    Maps positive and negative values to[0.0, 1.0]:
    1) Map via linear interpolation
       from [vmin, vcenter, vmax] to[-5, 0, 5]
    1) Map via sigmoid-function
       from [-1, 0.5, 1.0] to[0.0 + eps, 0.5, 1.0 - eps]
    2) Map via linear interpolation to[0.0, 1.0] using TwoSlopeNorm
       from matplotlib

    https: // stackoverflow.com/a/42140710
    https: // de.wikipedia.org/wiki/Sigmoidfunktion
    '''

    @staticmethod
    def exp_sigmoid(x, x_scale):
        '''
        If x_scale < 1.0, the results are less nice.
        '''

        # Input-values are scaled and shifted from [0, 0.5, 1] to [-6, 0.0, 6]
        return 1.0 / (1.0 + np.exp(-12.0 * x_scale * (x-0.5)))

    @staticmethod
    def log_sigmoid(x, x_scale):
        '''
        If x_scale < 1.0, the results are less nice.
        '''

        # Input-values are scaled and shifted from [0, 0.5, 1] to [-6, 0.0, 6]
        return -np.log(1.0/x - 1.0) / (12.0 * x_scale) + 0.5

    def __init__(self, intensity=1.0, vmin=None, vmax=None, vcenter=0.0, **kwargs):
        super().__init__(vmin=vmin, vmax=vmax, **kwargs)
        self.intensity = intensity
        self.vmin = vmin
        self.vmax = vmax
        self.vcenter = vcenter

    def __call__(self, value, clip=None):
        '''
        The clip argument is unused.
        '''

        # set and check data-dependent values

        vmin = self.vmin
        vmax = self.vmax
        if vmin is None:
            vmin = np.min(value)
        if vmax is None:
            vmax = np.max(value)
        vcenter = self.vcenter
        vmin, vcenter, vmax = _check_inputs(
            vmin=vmin,
            vcenter=vcenter,
            vmax=vmax
        )
        intensity = self.intensity

        # normalize values

        if vcenter is None:
            xp, fp = [vmin, vmax], [0.0, 1.0]
        else:
            xp, fp = [vmin, vcenter, vmax], [0.0, 0.5, 1.0]
        values = np.interp(x=value, xp=xp, fp=fp)

        if intensity < 0.0:
            intensity = -intensity
            if intensity < 1.0:
                sigmoid = SigmoidNorm.log_sigmoid(x=values, x_scale=1.0)
                values = values - intensity * (values - sigmoid)
            else:
                sigmoid = SigmoidNorm.log_sigmoid(x=values, x_scale=intensity)
                values = values - 1.0 * (values - sigmoid)
        else:
            if intensity < 1.0:
                sigmoid = SigmoidNorm.exp_sigmoid(x=values, x_scale=1.0)
                values = values - intensity * (values - sigmoid)
            else:
                sigmoid = SigmoidNorm.exp_sigmoid(x=values, x_scale=intensity)
                values = values - 1.0 * (values - sigmoid)

        return np.ma.masked_array(values)