Writing a custom Scale class

Hi,

I have a time series with two values x(t) and y(t), stored in a Pandas data
frame df with columns DateTime, ValueX, ValueY. I would like to plot ValueY
vs. ValueX. In addition, I would like to see for each data point on the
graph the date when it was measured.

My idea was to plot(ValueX, ValueY) and then somehow set the labels to
DateTime. But not only the visible tick labels. Rather, when I move my
mouse over the plot, I would like to see (DateTime, ValueY) for each
point, rather then (ValueX, ValueY).

Or, another way to see it is, that I would like to plot ValueY vs.
DateTime, but scale the x-axis as ValueX.

My take was to plt.plot(df.index, df.ValueX), and to write a custom Scale
module that receives the data frame upon construction and that scales the
x-axis as ValueX and formats the labels as DateTime.

I managed to have the right scaling of my x-axis but I can't see any ticks
nor tick labels. And when I hover the mouse over the plot, I see (x=nan,
y=<correct value>).

I appreciate any help! :slight_smile: It seems that the documentation for such
low-level functionality is sometimes a bit scarce :slight_smile:

Here is a minimum working example:

import matplotlib.scale
import matplotlib.transforms
import matplotlib.pyplot as plt
from matplotlib.ticker import AutoLocator, FixedLocator, FuncFormatter,
MaxNLocator, ScalarFormatter
import numpy as np
from numpy import ma
import pandas as pd

class Scaler(matplotlib.scale.ScaleBase):

    name = 'scaler'

    def __init__(self, axis, df, **kwargs):
        matplotlib.scale.ScaleBase.__init__(self)
        self.df = df

    def get_transform(self):
        return self.Transform(self.df)

    def limit_range_for_scale(self, vmin, vmax, minpos):
        min_ = max(vmin, self.df.index.min())
        max_ = min(vmax, self.df.index.max())
        return min_, max_

    def set_default_locators_and_formatters(self, axis):
        axis.set_major_locator(AutoLocator())
        axis.set_major_formatter(ScalarFormatter())

    class Transform(matplotlib.transforms.Transform):
        input_dims = 1
        output_dims = 1
        is_separable = True
        has_inverse = True

        def __init__(self, df):
            matplotlib.transforms.Transform.__init__(self)
            self.df = df

        def transform_non_affine(self, x):
            if x.ndim > 1:
                assert x.ndim == 2 and x.shape[1] == 1
            y = ma.masked_array(np.zeros_like(x), mask=[False for _ in x])
            for i in range(x.shape[0]):
                if x.ndim == 1:
                    if (int(x[i]) != x[i]) or (x[i] not in df.index):
                        y.mask[i] = True
                    else:
                        y[i] = self.df.at[int(x[i]), 'x']
                else:
                    if (int(x[i, 0]) != x[i, 0]) or (x[i, 0] not in
df.index):
                        y.mask[i] = True
                    else:
                        y[i, 0] = self.df.at[int(x[i, 0]), 'x']
            return y

        def inverted(self):
            return Scaler.InvertedTransform(self.df)

    class InvertedTransform(matplotlib.transforms.Transform):
        input_dims = 1
        output_dims = 1
        is_separable = True
        has_inverse = True

        def __init__(self, df):
            matplotlib.transforms.Transform.__init__(self)
            self.df = df

        def transform_non_affine(self, x):
            if x.ndim > 1:
                assert x.ndim == 2 and x.shape[1] == 1
            y = ma.masked_array(np.zeros_like(x), mask=[False for _ in x])
            for i in range(x.shape[0]):
                if x.ndim == 1:
                    if x[i] not in df['x']:
                        y.mask[i] = True
                    else:
                        y[i] = self.df.loc[self.df['x'] == x[i], :].index[0]
                else:
                    if x[i, 0] not in df['x']:
                        y.mask[i] = True
                    else:
                        y[i, 0] = self.df.loc[self.df['x'] == x[i, 0],
:].index[0]
            return y

        def inverted(self):
            return Scaler.Transform(self.df)

matplotlib.scale.register_scale(Scaler)

df = pd.DataFrame(index=range(1, 11), data={'x': [1, 1.5, 3, 3.5, 5, 5.5,
7, 7.5, 9, 9.5], 'y': range(1, 11)})
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.plot(df.index, df['y'])
ax.set_xlim([df.index[0], df.index[-1]])
ax.set_xscale('scaler', df=df)

Cheers
Konstantin

···

--
To send me an encrypted email, download my public key from pgp.mit.edu
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.python.org/pipermail/matplotlib-users/attachments/20190210/13ac3c5e/attachment-0001.html>

When the axes limits go beyond the bounds of your dataframe, how should the
ticks be labeled?

···

On Thu, Mar 21, 2019 at 2:03 PM Konstantin Miller < konstantin.miller at gmail.com> wrote:

Hi,

I have a time series with two values x(t) and y(t), stored in a Pandas
data frame df with columns DateTime, ValueX, ValueY. I would like to plot
ValueY vs. ValueX. In addition, I would like to see for each data point on
the graph the date when it was measured.

My idea was to plot(ValueX, ValueY) and then somehow set the labels to
DateTime. But not only the visible tick labels. Rather, when I move my
mouse over the plot, I would like to see (DateTime, ValueY) for each
point, rather then (ValueX, ValueY).

Or, another way to see it is, that I would like to plot ValueY vs.
DateTime, but scale the x-axis as ValueX.

My take was to plt.plot(df.index, df.ValueX), and to write a custom Scale
module that receives the data frame upon construction and that scales the
x-axis as ValueX and formats the labels as DateTime.

I managed to have the right scaling of my x-axis but I can't see any ticks
nor tick labels. And when I hover the mouse over the plot, I see (x=nan,
y=<correct value>).

I appreciate any help! :slight_smile: It seems that the documentation for such
low-level functionality is sometimes a bit scarce :slight_smile:

Here is a minimum working example:

import matplotlib.scale
import matplotlib.transforms
import matplotlib.pyplot as plt
from matplotlib.ticker import AutoLocator, FixedLocator, FuncFormatter,
MaxNLocator, ScalarFormatter
import numpy as np
from numpy import ma
import pandas as pd

class Scaler(matplotlib.scale.ScaleBase):

    name = 'scaler'

    def __init__(self, axis, df, **kwargs):
        matplotlib.scale.ScaleBase.__init__(self)
        self.df = df

    def get_transform(self):
        return self.Transform(self.df)

    def limit_range_for_scale(self, vmin, vmax, minpos):
        min_ = max(vmin, self.df.index.min())
        max_ = min(vmax, self.df.index.max())
        return min_, max_

    def set_default_locators_and_formatters(self, axis):
        axis.set_major_locator(AutoLocator())
        axis.set_major_formatter(ScalarFormatter())

    class Transform(matplotlib.transforms.Transform):
        input_dims = 1
        output_dims = 1
        is_separable = True
        has_inverse = True

        def __init__(self, df):
            matplotlib.transforms.Transform.__init__(self)
            self.df = df

        def transform_non_affine(self, x):
            if x.ndim > 1:
                assert x.ndim == 2 and x.shape[1] == 1
            y = ma.masked_array(np.zeros_like(x), mask=[False for _ in x])
            for i in range(x.shape[0]):
                if x.ndim == 1:
                    if (int(x[i]) != x[i]) or (x[i] not in df.index):
                        y.mask[i] = True
                    else:
                        y[i] = self.df.at[int(x[i]), 'x']
                else:
                    if (int(x[i, 0]) != x[i, 0]) or (x[i, 0] not in
df.index):
                        y.mask[i] = True
                    else:
                        y[i, 0] = self.df.at[int(x[i, 0]), 'x']
            return y

        def inverted(self):
            return Scaler.InvertedTransform(self.df)

    class InvertedTransform(matplotlib.transforms.Transform):
        input_dims = 1
        output_dims = 1
        is_separable = True
        has_inverse = True

        def __init__(self, df):
            matplotlib.transforms.Transform.__init__(self)
            self.df = df

        def transform_non_affine(self, x):
            if x.ndim > 1:
                assert x.ndim == 2 and x.shape[1] == 1
            y = ma.masked_array(np.zeros_like(x), mask=[False for _ in x])
            for i in range(x.shape[0]):
                if x.ndim == 1:
                    if x[i] not in df['x']:
                        y.mask[i] = True
                    else:
                        y[i] = self.df.loc[self.df['x'] == x[i],
:].index[0]
                else:
                    if x[i, 0] not in df['x']:
                        y.mask[i] = True
                    else:
                        y[i, 0] = self.df.loc[self.df['x'] == x[i, 0],
:].index[0]
            return y

        def inverted(self):
            return Scaler.Transform(self.df)

matplotlib.scale.register_scale(Scaler)

df = pd.DataFrame(index=range(1, 11), data={'x': [1, 1.5, 3, 3.5, 5, 5.5,
7, 7.5, 9, 9.5], 'y': range(1, 11)})
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.plot(df.index, df['y'])
ax.set_xlim([df.index[0], df.index[-1]])
ax.set_xscale('scaler', df=df)

Cheers
Konstantin

--
To send me an encrypted email, download my public key from pgp.mit.edu
_______________________________________________
Matplotlib-users mailing list
Matplotlib-users at python.org
Matplotlib-users Info Page

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.python.org/pipermail/matplotlib-users/attachments/20190321/7f6ed6ee/attachment.html&gt;