Writing a custom Scale class

classic Classic list List threaded Threaded
1 message Options
Reply | Threaded
Open this post in threaded view
|

Writing a custom Scale class

Konstantin Miller
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! :) It seems that the documentation for such low-level functionality is sometimes a bit scarce :)

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
[hidden email]
https://mail.python.org/mailman/listinfo/matplotlib-users