Search code examples
pythonbokehtimedeltaholoviewshvplot

hvplot / holoviews (bokeh) timedelta axis with negative values formatter


Issue:

I am trying to plot data with a pandas time delta index with negative time delta values on the x-axis using hvplot or holoviews (bokeh backend).

The labels are just integers, and seem to be in milliseconds.
I want them to be formatted in a friendlier format such as HH:MM:SS

EXAMPLE

import pandas as pd
import numpy as np
import hvplot.pandas

x = pd.timedelta_range(start=0, freq='S', periods=11) - pd.Timedelta('5S')
y = np.arange(len(x))
df = pd.DataFrame({'y': y}, index=x)
df.hvplot.line(rot=20)

Output: enter image description here I expected the x axis to be -00:00:04 -00:00:04 00:00:00 00:00:02 00:00:04
or at least in seconds, this seems to be milliseconds.

What I tried

using the df create above:

from bokeh.models.formatters import NumeralTickFormatter
df.hvplot.line(xformatter=NumeralTickFormatter(format="00:00:00"), rot=20)

Output: enter image description here Idk what happened here with the xlabels but they dont really make any sense.

Using DatetimeTickFormatter:

from bokeh.models.formatters import DatetimeTickFormatter
df.hvplot.line(rot=20, xformatter=DatetimeTickFormatter())

enter image description here

Does not work unfortunatelt: No negative values: - 00:00:02 becomes 58s


Solution

  • I found a solution, it is not pretty but it works
    (Using the earlier created dataframe):

    def timedelta_formatter(x):
        x/=1000                       # ms -> seconds
    
        # extract seconds, minutes, hours, days from time
        m, s = divmod(abs(x), 60)
        h, m = divmod(m, 60)
        d, h = divmod(h, 24)
        
        # create output string with time format
        out = f"{d}d. {h:02}:{m:02}:{s:02}"
    
        if x < 0:
            # add - if negative value
            return "- " + out
        else:
            return out
    df.hvplot.line(xformatter=timedelta_formatter, rot=20)
    

    Output: enter image description here

    This looks better!

    Lets try it with a timedelta range in days:

    x = pd.timedelta_range(start=0, freq='D', periods=11) - pd.Timedelta('5D')
    y = np.arange(len(x))
    df = pd.DataFrame({'y': y}, index=x)
    df.hvplot.line(xformatter=timedelta_formatter, rot=20)
    

    Output: enter image description here

    Works too

    I still think this is not the most elegant solution,
    so open to hear better solutions