Skip to content

Commit

Permalink
Allow Bars to be plotted on continuous axes (#6145)
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr authored May 17, 2024
1 parent a566b78 commit ee20d23
Show file tree
Hide file tree
Showing 12 changed files with 460 additions and 33 deletions.
38 changes: 36 additions & 2 deletions examples/reference/elements/bokeh/Bars.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"A ``Bars`` element can be sliced and selecting on like any other element:"
"A `Bars` element can be sliced and selected on like any other element:"
]
},
{
Expand Down Expand Up @@ -88,7 +88,41 @@
"\n",
"# or using .redim.values(**{'Car Occupants': ['three', 'two', 'four', 'one', 'five', 'six']})\n",
"\n",
"hv.Bars(data, occupants, 'Count') "
"hv.Bars(data, occupants, 'Count')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"`Bars` also supports continuous data and x-axis."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"data = pd.DataFrame({\"x\": [0, 1, 5], \"y\": [0, 2, 10]})\n",
"hv.Bars(data, [\"x\"], [\"y\"])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"And datetime data and x-axis."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"data = pd.DataFrame({\"x\": pd.date_range(\"2017-01-01\", \"2017-01-03\"), \"y\": [0, 2, -1]})\n",
"hv.Bars(data, [\"x\"], [\"y\"])"
]
},
{
Expand Down
37 changes: 36 additions & 1 deletion examples/reference/elements/matplotlib/Bars.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"metadata": {},
"outputs": [],
"source": [
"import pandas as pd\n",
"import numpy as np\n",
"import holoviews as hv\n",
"hv.extension('matplotlib')"
Expand Down Expand Up @@ -80,6 +81,40 @@
"hv.Bars(data, occupants, 'Count') "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"`Bars` also supports continuous data and x-axis."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"data = pd.DataFrame({\"x\": [0, 1, 5], \"y\": [0, 2, 10]})\n",
"hv.Bars(data, [\"x\"], [\"y\"])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"And datetime data and x-axis."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"data = pd.DataFrame({\"x\": pd.date_range(\"2017-01-01\", \"2017-01-03\"), \"y\": [0, 2, -1]})\n",
"hv.Bars(data, [\"x\"], [\"y\"])"
]
},
{
"cell_type": "markdown",
"metadata": {},
Expand Down Expand Up @@ -169,5 +204,5 @@
}
},
"nbformat": 4,
"nbformat_minor": 2
"nbformat_minor": 4
}
35 changes: 35 additions & 0 deletions examples/reference/elements/plotly/Bars.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"metadata": {},
"outputs": [],
"source": [
"import pandas as pd\n",
"import numpy as np\n",
"import holoviews as hv\n",
"hv.extension('plotly')"
Expand Down Expand Up @@ -80,6 +81,40 @@
"hv.Bars(data, occupants, 'Count')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"`Bars` also support continuous data and x-axis."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"data = pd.DataFrame({\"x\": [0, 1, 5], \"y\": [0, 2, 10]})\n",
"hv.Bars(data, [\"x\"], [\"y\"])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"And datetime data and x-axis."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"data = pd.DataFrame({\"x\": pd.date_range(\"2017-01-01\", \"2017-01-03\"), \"y\": [0, 2, -1]})\n",
"hv.Bars(data, [\"x\"], [\"y\"])"
]
},
{
"cell_type": "markdown",
"metadata": {},
Expand Down
33 changes: 21 additions & 12 deletions holoviews/plotting/bokeh/chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

import numpy as np
import param
from bokeh.models import CategoricalColorMapper, CustomJS, FactorRange, Range1d, Whisker
from bokeh.models import CategoricalColorMapper, CustomJS, Whisker
from bokeh.models.tools import BoxSelectTool
from bokeh.transform import jitter

from ...core.data import Dataset
from ...core.dimension import dimension_name
from ...core.util import dimension_sanitizer, isfinite
from ...core.util import dimension_sanitizer, isdatetime, isfinite
from ...operation import interpolate_curve
from ...util.transform import dim
from ..mixins import AreaMixin, BarsMixin, SpikesMixin
Expand Down Expand Up @@ -780,10 +780,6 @@ class BarPlot(BarsMixin, ColorbarPlot, LegendPlot):
_nonvectorized_styles = base_properties + ['bar_width', 'cmap']
_plot_methods = dict(single=('vbar', 'hbar'))

# Declare that y-range should auto-range if not bounded
_x_range_type = FactorRange
_y_range_type = Range1d

def _axis_properties(self, axis, key, plot, dimension=None,
ax_mapping=None):
if ax_mapping is None:
Expand Down Expand Up @@ -865,7 +861,7 @@ def _add_color_data(self, ds, ranges, style, cdim, data, mapping, factors, color
for k, cd in cdata.items():
if isinstance(cmapper, CategoricalColorMapper) and cd.dtype.kind in 'uif':
cd = categorize_array(cd, cdim)
if k not in data or len(data[k]) != next(len(data[key]) for key in data if key != k):
if k not in data or (len(data[k]) != next(len(data[key]) for key in data if key != k)):
data[k].append(cd)
else:
data[k][-1] = cd
Expand All @@ -889,6 +885,7 @@ def get_data(self, element, ranges, style):
grouping = 'grouped'
group_dim = element.get_dimension(1)

data = defaultdict(list)
xdim = element.get_dimension(0)
ydim = element.vdims[0]
no_cidx = self.color_index is None
Expand All @@ -906,25 +903,38 @@ def get_data(self, element, ranges, style):
hover = 'hover' in self.handles

# Group by stack or group dim if necessary
xdiff = None
xvals = element.dimension_values(xdim)
if group_dim is None:
grouped = {0: element}
is_dt = isdatetime(xvals)
if is_dt or xvals.dtype.kind not in 'OU':
xdiff = np.abs(np.diff(xvals))
if len(np.unique(xdiff)) == 1 and xdiff[0] == 0:
xdiff = 1
if is_dt:
width *= xdiff.astype('timedelta64[ms]').astype(np.int64)
else:
width /= xdiff
width = np.min(width)
else:
grouped = element.groupby(group_dim, group_type=Dataset,
container_type=dict,
datatype=['dataframe', 'dictionary'])

width = abs(width)
y0, y1 = ranges.get(ydim.name, {'combined': (None, None)})['combined']
if self.logy:
bottom = (ydim.range[0] or (0.01 if y1 > 0.01 else 10**(np.log10(y1)-2)))
else:
bottom = 0

# Map attributes to data
if grouping == 'stacked':
mapping = {'x': xdim.name, 'top': 'top',
'bottom': 'bottom', 'width': width}
elif grouping == 'grouped':
mapping = {'x': 'xoffsets', 'top': ydim.name, 'bottom': bottom,
'width': width}
mapping = {'x': 'xoffsets', 'top': ydim.name, 'bottom': bottom, 'width': width}
else:
mapping = {'x': xdim.name, 'top': ydim.name, 'bottom': bottom, 'width': width}

Expand Down Expand Up @@ -955,7 +965,6 @@ def get_data(self, element, ranges, style):
factors, colors = None, None

# Iterate over stacks and groups and accumulate data
data = defaultdict(list)
baselines = defaultdict(lambda: {'positive': bottom, 'negative': 0})
for k, ds in grouped.items():
k = k[0] if isinstance(k, tuple) else k
Expand Down Expand Up @@ -994,7 +1003,7 @@ def get_data(self, element, ranges, style):
ds = ds.add_dimension(group_dim, ds.ndims, gval)
data[group_dim.name].append(ds.dimension_values(group_dim))
else:
data[xdim.name].append(ds.dimension_values(xdim))
data[xdim.name].append(xvals)
data[ydim.name].append(ds.dimension_values(ydim))

if hover and grouping != 'stacked':
Expand Down Expand Up @@ -1026,7 +1035,7 @@ def get_data(self, element, ranges, style):

# Ensure x-values are categorical
xname = dimension_sanitizer(xdim.name)
if xname in sanitized_data:
if xname in sanitized_data and isinstance(sanitized_data[xname], np.ndarray) and sanitized_data[xname].dtype.kind not in 'uifM' and not isdatetime(sanitized_data[xname]):
sanitized_data[xname] = categorize_array(sanitized_data[xname], xdim)

# If axes inverted change mapping to match hbar signature
Expand Down
1 change: 0 additions & 1 deletion holoviews/plotting/bokeh/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,6 @@ def _postprocess_data(self, data):
new_data = {}
for k, values in data.items():
values = decode_bytes(values) # Bytes need decoding to strings

# Certain datetime types need to be converted
if len(values) and isinstance(values[0], cftime_types):
if any(v.calendar not in _STANDARD_CALENDARS for v in values):
Expand Down
8 changes: 5 additions & 3 deletions holoviews/plotting/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,9 @@ def get_extents(self, element, ranges, range_type='combined', **kwargs):
s0 = min(s0, 0) if util.isfinite(s0) else 0
s1 = max(s1, 0) if util.isfinite(s1) else 0
ranges[vdim]['soft'] = (s0, s1)
l, b, r, t = super().get_extents(element, ranges, range_type, ydim=element.vdims[0])
if range_type not in ('combined', 'data'):
return super().get_extents(element, ranges, range_type, ydim=element.vdims[0])
return l, b, r, t

# Compute stack heights
xdim = element.kdims[0]
Expand All @@ -173,14 +174,15 @@ 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 ('', '')
if range_type == 'data':
return ('', y0, '', y1)
return (x0, y0, x1, y1)

padding = 0 if self.overlaid else self.padding
_, ypad, _ = get_axis_padding(padding)
y0, y1 = util.dimension_range(y0, y1, ranges[vdim]['hard'], ranges[vdim]['soft'], ypad, self.logy)
y0, y1 = util.dimension_range(y0, y1, self.ylim, (None, None))
return ('', y0, '', y1)
return (x0, y0, x1, y1)

def _get_coords(self, element, ranges, as_string=True):
"""
Expand Down
Loading

0 comments on commit ee20d23

Please sign in to comment.