Skip to content

Commit

Permalink
Improve datetime support for hv.Bars (#6365)
Browse files Browse the repository at this point in the history
  • Loading branch information
hoxbro authored Sep 24, 2024
1 parent 3b3f10f commit 1de5249
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 22 deletions.
7 changes: 5 additions & 2 deletions holoviews/plotting/bokeh/chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -880,7 +880,7 @@ def _add_color_data(self, ds, ranges, style, cdim, data, mapping, factors, color

def get_data(self, element, ranges, style):
# Get x, y, group, stack and color dimensions
group_dim, stack_dim = None, None
group_dim, stack_dim, stack_order = None, None, None
if element.ndims == 1:
grouping = None
elif self.stacked:
Expand All @@ -893,6 +893,8 @@ def get_data(self, element, ranges, style):
else:
stack_order = element.dimension_values(1, False)
stack_order = list(stack_order)
stack_data = element.dimension_values(stack_dim)
stack_idx = stack_data == stack_data[0]
else:
grouping = 'grouped'
group_dim = element.get_dimension(1)
Expand Down Expand Up @@ -921,7 +923,8 @@ def get_data(self, element, ranges, style):
grouped = {0: element}
is_dt = isdatetime(xvals)
if is_dt or xvals.dtype.kind not in 'OU':
xdiff = np.abs(np.diff(xvals))
xslice = stack_idx if stack_order else slice(None)
xdiff = np.abs(np.diff(xvals[xslice]))
diff_size = len(np.unique(xdiff))
if diff_size == 0 or (diff_size == 1 and xdiff[0] == 0):
xdiff = 1
Expand Down
2 changes: 1 addition & 1 deletion holoviews/plotting/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ def get_extents(self, element, ranges, range_type='combined', **kwargs):
else:
y0, y1 = ranges[vdim]['combined']

x0, x1 = (l, r) if util.isnumeric(l) and len(element.kdims) == 1 else ('', '')
x0, x1 = (l, r) if (util.isnumeric(l) or isinstance(l, util.datetime_types)) else ('', '')
if range_type == 'data':
return (x0, y0, x1, y1)

Expand Down
39 changes: 25 additions & 14 deletions holoviews/plotting/mpl/chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -936,9 +936,12 @@ def _create_bars(self, axis, element, ranges, style):

cats = None
style_dim = None
xslice = slice(None)
if sdim:
cats = values['stack']
style_dim = sdim
stack_data = element.dimension_values(sdim)
xslice = stack_data == stack_data[0]
elif cdim:
cats = values['category']
style_dim = cdim
Expand All @@ -954,14 +957,15 @@ def _create_bars(self, axis, element, ranges, style):
is_dt = isdatetime(xvals)
continuous = True
if is_dt or xvals.dtype.kind not in 'OU' and not (cdim or len(element.kdims) > 1):
xdiff_vals = date2num(xvals) if is_dt else xvals
xdiff = np.abs(np.diff(xdiff_vals))
if len(np.unique(xdiff)) == 1:
# if all are same
xvals = xvals[xslice]
xdiff = np.abs(np.diff(xvals))
diff_size = len(np.unique(xdiff))
if diff_size == 0 or (diff_size == 1 and xdiff[0] == 0):
xdiff = 1
else:
xdiff = np.min(xdiff)
width = (1 - self.bar_padding) * xdiff
width = (1 - self.bar_padding) * (date2num(xdiff) / 1000 if is_dt else xdiff)
data_width = (1 - self.bar_padding) * xdiff
else:
xdiff = len(values.get('category', [None]))
width = (1 - self.bar_padding) / xdiff
Expand Down Expand Up @@ -1050,16 +1054,23 @@ def _create_bars(self, axis, element, ranges, style):
axis.legend(title=title, **legend_opts)

x_range = ranges[gdim.label]["data"]
if continuous and not is_dt:
if style.get('align', 'center') == 'center':
left_multiplier = 0.5
right_multiplier = 0.5
else:
left_multiplier = 0
right_multiplier = 1
if style.get('align', 'center') == 'center':
left_multiplier = 0.5
right_multiplier = 0.5
else:
left_multiplier = 0
right_multiplier = 1
if continuous:
ranges[gdim.label]["data"] = (
x_range[0] - data_width * left_multiplier,
x_range[1] + data_width * right_multiplier,
)
else:
locs = [item[0] for item in xticks]
xmin, xmax = min(locs), max(locs)
ranges[gdim.label]["data"] = (
x_range[0] - width * left_multiplier,
x_range[1] + width * right_multiplier
- width * left_multiplier + xmin,
width * right_multiplier + xmax
)
return bars, xticks if not continuous else None, ax_dims

Expand Down
37 changes: 36 additions & 1 deletion holoviews/tests/plotting/bokeh/test_barplot.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import numpy as np
import pandas as pd
from bokeh.models import CategoricalColorMapper, LinearAxis, LinearColorMapper
from bokeh.models import (
CategoricalColorMapper,
DatetimeAxis,
LinearAxis,
LinearColorMapper,
)

from holoviews.core.overlay import NdOverlay, Overlay
from holoviews.element import Bars
Expand Down Expand Up @@ -313,6 +318,29 @@ def test_bars_continuous_datetime(self):
plot = bokeh_renderer.get_plot(bars)
np.testing.assert_almost_equal(plot.handles["glyph"].width, 69120000.0)

def test_bars_continuous_datetime_timezone_in_overlay(self):
# See: https://github.com/holoviz/holoviews/issues/6364
bars = Bars((pd.date_range("1/1/2000", periods=10, tz="UTC"), np.random.rand(10)))
overlay = Overlay([bars])
plot = bokeh_renderer.get_plot(overlay)
assert isinstance(plot.handles["xaxis"], DatetimeAxis)

def test_bars_continuous_datetime_stacked(self):
# See: https://github.com/holoviz/holoviews/issues/6288
data = pd.DataFrame({
"x": pd.to_datetime([
"2017-01-01T00:00:00",
"2017-01-01T00:00:00",
"2017-01-01T01:00:00",
"2017-01-01T01:00:00",
]),
"cat": ["A", "B", "A", "B"],
"y": [1, 2, 3, 4],
})
bars = Bars(data, ["x", "cat"], ["y"]).opts(stacked=True)
plot = bokeh_renderer.get_plot(bars)
assert isinstance(plot.handles["xaxis"], DatetimeAxis)

def test_bars_not_continuous_data_list(self):
bars = Bars([("A", 1), ("B", 2), ("C", 3)])
plot = bokeh_renderer.get_plot(bars)
Expand Down Expand Up @@ -356,3 +384,10 @@ def test_bar_group_stacked(self):
)
plot = bokeh_renderer.get_plot(bars)
assert plot.handles["glyph"].width == 0.8

def test_bar_stacked_stack_variable_sorted(self):
# Check that if the stack dim is ordered
df = pd.DataFrame({"a": [*range(50), *range(50)], "b": sorted("ab" * 50), "c": range(100)})
bars = Bars(df, kdims=["a", "b"], vdims=["c"]).opts(stacked=True)
plot = bokeh_renderer.get_plot(bars)
assert plot.handles["glyph"].width == 0.8
8 changes: 4 additions & 4 deletions holoviews/tests/plotting/matplotlib/test_barplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def test_bars_not_continuous_data_list(self):
bars = Bars([("A", 1), ("B", 2), ("C", 3)])
plot = mpl_renderer.get_plot(bars)
ax = plot.handles["axis"]
np.testing.assert_almost_equal(ax.get_xlim(), (-0.54, 2.54))
np.testing.assert_almost_equal(ax.get_xlim(), (-0.4, 2.4))
assert ax.patches[0].get_width() == 0.8
np.testing.assert_equal(ax.get_xticks(), [0, 1, 2])
np.testing.assert_equal(
Expand All @@ -69,7 +69,7 @@ def test_bars_group(self):
plot = mpl_renderer.get_plot(bars)
ax = plot.handles["axis"]

np.testing.assert_almost_equal(ax.get_xlim(), (-0.3233333, 3.8566667))
np.testing.assert_almost_equal(ax.get_xlim(), (-0.1333333, 3.6666667))
assert ax.patches[0].get_width() == 0.26666666666666666
ticklabels = ax.get_xticklabels()
expected = [
Expand Down Expand Up @@ -113,7 +113,7 @@ def test_bar_group_stacked(self):
plot = mpl_renderer.get_plot(bars)
ax = plot.handles["axis"]

np.testing.assert_almost_equal(ax.get_xlim(), (-0.59, 3.59))
np.testing.assert_almost_equal(ax.get_xlim(), (-0.4, 3.4))
assert ax.patches[0].get_width() == 0.8
ticklabels = ax.get_xticklabels()
expected = [
Expand All @@ -136,7 +136,7 @@ def test_group_dim(self):
plot = mpl_renderer.get_plot(bars)
ax = plot.handles["axis"]

np.testing.assert_almost_equal(ax.get_xlim(), (-0.34, 2.74))
np.testing.assert_almost_equal(ax.get_xlim(), (-0.2, 2.6))
assert ax.patches[0].get_width() == 0.4
assert len(ax.get_xticks()) > 3

Expand Down

0 comments on commit 1de5249

Please sign in to comment.