Need help formatting axis for duration

Hi,
I’m trying to format x-axis for duration/elapsed time. Depending on the data chosen by the user the data displayed can vary a lot with respect to duration so I need to be able to cover from less than a minute to days (measured in hours, hence if duration is 1 day and 7 hours, the axis should show in hh:mm).
I have tried using NumeralTickFormatter with format="00:00:00". It works to some extent in the sence it show days in hours, but it is always on the format of hh:mm:ss. I had hoped it would automatically change between hh:mm or mm:ss depending on the data being displayed.
I have tried formatting the axis as datetime, but here the problem is that duration extending for more than a day, I’m not able to control have days in hours; when duration is 0 or 24 it is shown as a day/date.

Any help/hints with respect to formatting duration appreciated.
Cheers,
Jonas

from bokeh.io import output_file, save
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource, NumeralTickFormatter, DatetimeTickFormatter
from bokeh.models import AdaptiveTicker
from bokeh.layouts import row

output_file('duration_axis.html')

data = {
    'test': ['Test A', 'Test B', 'Test C'],
    'duration_s': [3600, 90000, 72000],
}
data['duration_ms'] = [item*1000 for item in data['duration_s']]

src = ColumnDataSource(data = data)

plot1 = figure(
    plot_height = 400,
    plot_width = 400,
    y_range = data['test'],
)

plot1.hbar(
    right = 'duration_s',
    left = 0,
    y = 'test',
    height = 0.8,
    source = src
)

plot1.x_range.start = 0
axis_format = '00:00:00'
axis_mantissas = [3.6, 7.2, 18]
plot1.xaxis[0].formatter = NumeralTickFormatter(format = axis_format)
plot1.xaxis.ticker = AdaptiveTicker(base = 10, mantissas = axis_mantissas)
plot1.xgrid.ticker = AdaptiveTicker(base = 10, mantissas = axis_mantissas)

plot1.title.text = 'Duration using NumeralTickFormatter ("{}")'.format(axis_format)
plot1.xaxis[0].axis_label = 'Duration'

plot2 = figure(
    plot_height = 400,
    plot_width = 400,
    y_range = data['test'],
    x_axis_type = 'datetime'
)

plot2.hbar(
    right = 'duration_ms',
    left = 0,
    y = 'test',
    height = 0.8,
    source = src
)

plot2.x_range.start = 0

plot2.title.text = 'Duration using datetime axis'
plot2.xaxis[0].axis_label = 'Duration'

save(row(plot1, plot2))

Take a look at how DatetimeTicker is implemented. It’s built on top of CompositeTicker and just specifies a bunch of other tickers for different scales.
It seems like you should be able to create your own version of CompositeTicker that does what you want.

@p-himik thanks for a quick reply! I do struggle to understand how to get something like hh:mm or mm:ss for the labels… I’m not able to get DateTimeTicker to show days in hours, hence 1 day and 7 hours should be 31:00

There is no ticker that is denominated in only in perpetually accumulating hours. You can probably use a SingleIntervalTicker with an interval of 1hr (in milliseconds) and then a FuncTickFormatter to format those values as “total hours” (assuming here that your range starts at 0 for 0 hours)

@p-himik So I have managed to make two FuncTickFormatter's, one for hh:mm and one for mm:ss. However I struggle to combine those into CompositeTicker since I made formatters and not ticker (or maybe I misunderstood)?

from bokeh.io import output_file, save
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource, NumeralTickFormatter, DatetimeTickFormatter
from bokeh.models import AdaptiveTicker
from bokeh.layouts import row
from bokeh.models import FuncTickFormatter, CompositeTicker

output_file('duration_axis.html')

data = {
    'test': ['Test A', 'Test B', 'Test C'],
    'duration_lng': [3600, 90000, 72000],
    'duration_shrt': [5, 133, 1000]
}
src = ColumnDataSource(data = data)

hr_min_formatter = FuncTickFormatter(code="""
    let hours = Math.floor(tick / 3600);
    let minutes = Math.floor(tick / 60) % 60;
    let seconds = tick % 60; 
   
    if (tick == 0) {
        return `0:00`
    }    
    return `${hours}:${(minutes < 10) ? "0" : ""}${minutes}`;
""")

min_sec_formatter = FuncTickFormatter(code="""
    let hours = Math.floor(tick / 3600);
    let minutes = Math.floor(tick / 60) % 60;
    let seconds = tick % 60;    

    if (tick == 0) {
        return `0:00`
    }    
    return `${minutes}:${(seconds < 10) ? "0" : ""}${seconds}`;
""")

plot1 = figure(
    plot_height = 400,
    plot_width = 400,
    y_range = data['test'],
)
plot1.hbar(
    right = 'duration_lng',
    left = 0,
    y = 'test',
    height = 0.8,
    source = src
)
plot1.x_range.start = 0
plot1.xaxis.ticker = AdaptiveTicker(base = 60)
plot1.xgrid.ticker = AdaptiveTicker(base = 60)
plot1.xaxis[0].formatter = hr_min_formatter
plot1.title.text = 'Duration setup hh:mm'
plot1.xaxis[0].axis_label = 'Duration (hh:ss)'

plot2 = figure(
    plot_height = 400,
    plot_width = 400,
    y_range = data['test'],
)
plot2.hbar(
    right = 'duration_shrt',
    left = 0,
    y = 'test',
    height = 0.8,
    source = src
)
plot2.x_range.start = 0
plot2.xaxis.ticker = AdaptiveTicker(base = 60)
plot2.xgrid.ticker = AdaptiveTicker(base = 60)
plot2.xaxis[0].formatter = min_sec_formatter
plot2.title.text = 'Duration setup mm:ss'
plot2.xaxis[0].axis_label = 'Duration (mm:ss)'

save(row(plot1, plot2))

Hmm, yeah, you might need to write a custom formatter that has access to the full set of ticks so it could at least guess what range it should use - hours or minutes.
Sorry, I can’t really add anything else without spending significant amount of time on this.

@Jonas_Grave_Kristens are you can’t just change the default formats on the built-in DatetimeTickFormatter? It has properties for minutes, hourmin, and hours scales. If you don’t like the default format strings for those scales you can provide your own.

@Bryan the issue with DatetimeTickFormatter is that I have not been able to figure out to have the duration in hours as a running total, eg 24 hours should be 24:00. If I have plot.axis[0].formatter.days = ['%H:%M'] then I get 0:00 @ 24:00. I would like the ticker to continue from 23:00, 24:00, 25:00 etc.

Oh right, I forgot that was also a requirement. This is a fairly specialized ask, I am afraid I don’t think there are any simple solutions available. Offhand, my best suggestion is to configure a CompositeTicker with two AdaptiveTickers similar to what the existing DatetimeTicker does:

You’d want to configure the mantissas for whatever “nice” multiples you want to be avaialble for selection as tick locations. You may also need to set the max_interval higher on the “hours” ticker. Then you will need to combine your two FuncTickFormatters above into a single one, that checks to see if the tick value is an even hour or not, and applies different formats appropriately.