Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow Bars to be plotted on continuous axes #6145

Merged
merged 26 commits into from
May 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -956,7 +966,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 @@ -995,7 +1004,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 @@ -1027,7 +1036,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