First post. Bokeh <3.
Having datetime axes formatted without missing data linearly interpolated (i.e. with missing entries omitted from the chart completely) is not an uncommon request.
Finding a solution that:
- works for me,
- doesn’t use extension models end-users would need NodeJS for, and
- has the labels formatted in a resolution-adapted manner instead of a fixed string manner as with solutions involving p.xaxis.major_label_overrides,
I’d like to share it with the community. The code works in evergreen browsers by using a FuncTickFormatter with JavaScript code to switch out label formatting function for one that first maps indexes to their corresponding dates, then uses DatetimeTickFormatter to format the labels
concisely, regardless of their datetime resolution. Please find comments interspersed.
import pandas as pd
from bokeh.io import show
from bokeh.plotting import figure
from bokeh.transform import factor_cmap
from bokeh.models import ColumnDataSource,
FuncTickFormatter,
DatetimeTickFormatter
from bokeh.sampledata.stocks import GOOG
df = pd.DataFrame(GOOG)
df[‘inc’] = (df.open > df.close).astype(int).astype(str)
df[‘date’] = pd.to_datetime(df[‘date’]) # Have real dates in ‘date’ column
df.reset_index(drop=True, inplace=True) # And a simple range(0,n) index
source = ColumnDataSource(df)
Axis type must be linear and df.index a simple range index
p = figure(x_axis_type=‘linear’,
plot_width=1000,
tools=‘pan,wheel_zoom’,
active_scroll=‘wheel_zoom’,
active_drag=‘pan’)
Plot high-low segment and candles, colored appropriately
p.segment(‘index’, ‘high’, ‘index’, ‘low’, source=source, color=“black”)
p.vbar(‘index’, .9, ‘open’, ‘close’, source=source, line_color=‘black’,
fill_color=factor_cmap(‘inc’, [‘tomato’, ‘lime’], [‘0’, ‘1’]))
Override x axis formatter with a custom JS function formatter
Could avoid using FuncTickFormatter if GH-4272 were available
p.xaxis.formatter = FuncTickFormatter(
args=dict(
# We pass in the x axis itself, so we can access its
# ticks values
axis=p.xaxis[0],
# An instance of DatetimeTickFormatter to nicely format
# arbitrary precision datetimes
formatter=DatetimeTickFormatter(days=['%d %b', '%a %d'],
months=['%m/%Y', "%b %y"]),
# Our column data source with 'date' column we will
# map indexes through
source=source,
),
code="""
// We override this axis' formatter's `doFormat` method
// with one that maps index ticks to dates. Some of those dates
// are undefined (e.g. those whose ticks fall out of defined data
// range) and we must filter out and account for those, otherwise
// the formatter computes invalid visible span and returns some
// labels as 'ERR'.
// Note, after this assignment statement, on next plot redrawing,
// our override `doFormat` will be called directly
// -- FunctionTickFormatter.doFormat(), i.e. _this_ code, no longer
// executes.
axis.formatter.doFormat = function (ticks) {
const dates = ticks.map(i => source.data.date[i]),
valid = t => t !== undefined,
labels = formatter.doFormat(dates.filter(valid));
let i = 0;
return dates.map(t => valid(t) ? labels[i++] : '');
};
// Before the second redrawing when above doFormat will be called,
// we are still within this current labels formatting.
// FuncTickFormatter gets passed a single `tick` at a time, but
// DatetimeTickFormatter requires all ticks at once to work.
// We handle that by formatting all axis' ticks with the function
// we constructed above and then just taking out the current tick.
// Note: .tick_coords probably not public API
const ticks = axis.tick_coords.major[0],
labels = axis.formatter.doFormat(ticks);
return labels[ticks.indexOf(tick)];
“”")
show(p)
``
The code exhibits some drawbacks:
- ticks are not positioned as informedly rounded as would be if DatetimeTicker were used,
- ticks outside of defined range are not labeled, and
- when a single major tick is visible, it is formatted as %Y, because there is no range span for DatetimeTickFormatter to compute.
The approach would benefit somewhat if FuncTickFormatter could be configured to accept the whole array of ticks at once instead of a single tick at a time. In particular, this would let us avoid the use of non-public-API axis.formatter.doFormat() and axis.tick_coords.major.
I’d like some input:
- Could the above code or the approach be improved?
- Might a PR extending FuncTickFormatter with something like returns_array = Bool(False, help=…) be reviewed favorably?
- How do we proceed to such datetime axes stop being of workarounds and becoming API?
Thanks!
datetime-skip-missing.py (3.26 KB)