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 15 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:"
ahuang11 marked this conversation as resolved.
Show resolved Hide resolved
]
},
{
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\": [1, 2, -1]})\n",
ahuang11 marked this conversation as resolved.
Show resolved Hide resolved
"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 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 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
39 changes: 28 additions & 11 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 @@ -862,10 +858,11 @@ def _add_color_data(self, ds, ranges, style, cdim, data, mapping, factors, color

# Merge data and mappings
mapping.update(cmapping)
for k, cd in cdata.items():
for i, (k, cd) in enumerate(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):
# I don't know what this is for but adding the i check makes test pass
ahuang11 marked this conversation as resolved.
Show resolved Hide resolved
if k not in data or (len(data[k]) != next(len(data[key]) for key in data if key != k) and not i == len(cdata) - 1):
data[k].append(cd)
else:
data[k][-1] = cd
Expand All @@ -889,6 +886,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,18 +904,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)
try:
ahuang11 marked this conversation as resolved.
Show resolved Hide resolved
xdiff = 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.int32) * width
ahuang11 marked this conversation as resolved.
Show resolved Hide resolved
else:
width = width / xdiff
width = 1 - np.repeat(np.min(np.abs(width)), len(xvals))
ahuang11 marked this conversation as resolved.
Show resolved Hide resolved
data['width'] = [width]
except TypeError:
# fast way to check for categorical
# vs complicated dtype comparison
data['width'] = [np.repeat(width, len(xvals))]
width = 'width'
else:
grouped = element.groupby(group_dim, group_type=Dataset,
container_type=dict,
datatype=['dataframe', 'dictionary'])
data["width"] = [np.repeat(width, len(xvals))]
ahuang11 marked this conversation as resolved.
Show resolved Hide resolved

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',
Expand Down Expand Up @@ -956,7 +974,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 +1012,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 +1044,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
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