Skip to content

Commit

Permalink
Merge branch 'main' into inspect_where
Browse files Browse the repository at this point in the history
  • Loading branch information
hoxbro committed Jul 21, 2023
2 parents 47b8932 + d48950a commit cab0cc9
Show file tree
Hide file tree
Showing 28 changed files with 869 additions and 36 deletions.
5 changes: 1 addition & 4 deletions doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
.. raw:: html

<div style="display: flex">
<div style="width: 70%">
<div style="width: 95%">

HoloViews is an `open-source <https://github.com/holoviz/holoviews/blob/main/LICENSE.txt>`_ Python library designed to make data analysis and visualization seamless and simple. With HoloViews, you can usually express what you want to do in very few lines of code, letting you focus on what you are trying to explore and convey, not on the process of plotting.

Expand All @@ -27,9 +27,6 @@ If you have any `issues <https://github.com/holoviz/holoviews/issues>`_ or wish

</div>

.. raw:: html
:file: latest_news.html

.. raw:: html

</div>
Expand Down
4 changes: 0 additions & 4 deletions doc/latest_news.html

This file was deleted.

59 changes: 58 additions & 1 deletion examples/user_guide/Plotting_with_Bokeh.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"Not also that most of these options support vectorized style mapping as described in the [Style Mapping user guide](04-Style_Mapping.ipynb). Here's an example of HoloViews Elements using a Bokeh backend, with bokeh's style options:"
"Note also that most of these options support vectorized style mapping as described in the [Style Mapping user guide](04-Style_Mapping.ipynb). Here's an example of HoloViews Elements using a Bokeh backend, with bokeh's style options:"
]
},
{
Expand All @@ -149,6 +149,63 @@
"Notice that because the first two plots use the same underlying data, they become linked, such that zooming or panning one of the plots makes the corresponding change on the other."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Setting Backend Opts\n",
"\n",
"HoloViews does not expose every single option from Bokeh.\n",
"\n",
"Instead, HoloViews allow users to attach [hooks](Customizing_Plots.ipynb#plot-hooks) to modify the plot object directly--but writing these hooks could be cumbersome, especially if it's only used for a single line of update.\n",
"\n",
"Fortunately, HoloViews allows `backend_opts` for the Bokeh backend to configure options by declaring a dictionary with accessor specification for updating the plot components.\n",
"\n",
"For example, here's how to make the toolbar auto-hide."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"hv.Curve(data).opts(\n",
" backend_opts={\"plot.toolbar.autohide\": False}\n",
")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The following is the equivalent, as a hook."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def hook(hv_plot, element):\n",
" toolbar = hv_plot.handles['plot'].toolbar\n",
" toolbar.autohide = True\n",
"\n",
"hv.Curve(data).opts(hooks=[hook])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Notice how much more concise it is with `backend_opts`!\n",
"\n",
"With knowledge of the attributes of Bokeh, it's possible to configure many other plot components besides `toolbar`. Some examples include `legend`, `colorbar`, `xaxis`, `yaxis`, and much, much more.\n",
"\n",
"If you're unsure, simply input your best guess and it'll try to provide a list of suggestions if there's an issue."
]
},
{
"cell_type": "markdown",
"metadata": {},
Expand Down
85 changes: 85 additions & 0 deletions examples/user_guide/Plotting_with_Matplotlib.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,91 @@
"print('Axes: ', fig.axes)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Setting Backend Opts\n",
"\n",
"HoloViews does not expose every single option from matplotlib.\n",
"\n",
"Instead, HoloViews allow users to attach [hooks](Customizing_Plots.ipynb#plot-hooks) to modify the plot object directly--but writing these hooks could be cumbersome, especially if it's only used for a single line of update.\n",
"\n",
"Fortunately, HoloViews allows `backend_opts` for the Bokeh backend to configure options by declaring a dictionary with accessor specification for updating the plot components.\n",
"\n",
"For example, here's how to remove the frame on the legend."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"plot = hv.Curve([1, 2, 3], label=\"a\") * hv.Curve([1, 4, 9], label=\"b\")\n",
"plot.opts(\n",
" show_legend=True,\n",
" backend_opts={\n",
" \"legend.frame_on\": False,\n",
" }\n",
")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The following is the equivalent, as a hook."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def hook(plot, element):\n",
" legend = plot.handles[\"legend\"]\n",
" legend.set_frame_on(False)\n",
"\n",
"plot = hv.Curve([1, 2, 3], label=\"a\") * hv.Curve([1, 4, 9], label=\"b\")\n",
"plot.opts(\n",
" show_legend=True,\n",
" hooks=[hook]\n",
")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Notice how much more concise it is with `backend_opts`, and it's even possible to update items in a list.\n",
"\n",
"For example you can set the first legend label's `fontsize`!"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"plot = hv.Curve([1, 2, 3], label=\"a\") * hv.Curve([1, 4, 9], label=\"b\")\n",
"plot.opts(\n",
" show_legend=True,\n",
" backend_opts={\"legend.get_texts()[0:2].fontsize\": 18}\n",
")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"With knowledge of the methods in matplotlib, it's possible to configure many other plot components besides `legend`. Some examples include `colorbar`, `xaxis`, `yaxis`, and much, much more.\n",
"\n",
"If you're unsure, simply input your best guess and it'll try to provide a list of suggestions if there's an issue."
]
},
{
"cell_type": "markdown",
"metadata": {},
Expand Down
2 changes: 1 addition & 1 deletion holoviews/core/data/xarray.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ def retrieve_unit_and_label(dim):
packed = True
else:
data = {d: v for d, v in zip(dimensions, data)}
elif isinstance(data, list) and data == []:
elif isinstance(data, (list, np.ndarray)) and len(data) == 0:
dimensions = [d.name for d in kdims + vdims]
data = {d: np.array([]) for d in dimensions[:ndims]}
data.update({d: np.empty((0,) * ndims) for d in dimensions[ndims:]})
Expand Down
1 change: 0 additions & 1 deletion holoviews/core/sheetcoords.py
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,6 @@ def _boundsspec2slicespec(boundsspec,scs):

l_idx = int(np.ceil(l_m-0.5))
t_idx = int(np.ceil(t_m-0.5))
# CBENHANCEMENT: Python 2.6's math.trunc()?
r_idx = int(np.floor(r_m+0.5))
b_idx = int(np.floor(b_m+0.5))

Expand Down
29 changes: 29 additions & 0 deletions holoviews/element/geom.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,35 @@ class VectorField(Selection2DExpr, Geometry):
vdims = param.List(default=[Dimension('Angle', cyclic=True, range=(0,2*np.pi)),
Dimension('Magnitude')], bounds=(1, None))

@classmethod
def from_uv(cls, data, kdims=None, vdims=None, **params):
if kdims is None:
kdims = ['x', 'y']
if vdims is None:
vdims = ['u', 'v']
dataset = Dataset(data, kdims=kdims, vdims=vdims, **params)
us, vs = (dataset.dimension_values(i) for i in range(2, 4))

uv_magnitudes = np.hypot(us, vs) # unscaled
# this follows mathematical conventions,
# unlike WindBarbs which follows meteorological conventions
radians = np.arctan2(vs, us)

# calculations on this data could mutate the original data
# here we do not do any calculations; we only store the data
repackaged_dataset = {}
for kdim in kdims:
repackaged_dataset[kdim] = dataset[kdim]
repackaged_dataset["Angle"] = radians
repackaged_dataset["Magnitude"] = uv_magnitudes
for vdim in vdims[2:]:
repackaged_dataset[vdim] = dataset[vdim]
vdims = [
Dimension('Angle', cyclic=True, range=(0, 2 * np.pi)),
Dimension('Magnitude')
] + vdims[2:]
return cls(repackaged_dataset, kdims=kdims, vdims=vdims, **params)


class Segments(SelectionGeomExpr, Geometry):
"""
Expand Down
15 changes: 10 additions & 5 deletions holoviews/operation/datashader.py
Original file line number Diff line number Diff line change
Expand Up @@ -1438,20 +1438,25 @@ class rasterize(AggregationOperation):
]

__instance_params = set()
__instance_kwargs = {}

@bothmethod
def instance(self_or_cls, **params):
inst = super().instance(**params)
inst.__instance_params = set(params)
kwargs = set(params) - set(self_or_cls.param)
inst_params = {k: v for k, v in params.items() if k in self_or_cls.param}
inst = super().instance(**inst_params)
inst.__instance_params = set(inst_params)
inst.__instance_kwargs = {k: v for k, v in params.items() if k in kwargs}
return inst

def _process(self, element, key=None):
# Potentially needs traverse to find element types first?
all_allowed_kws = set()
all_supplied_kws = set()
instance_params = {
k: getattr(self, k) for k in self.__instance_params
}
instance_params = dict(
self.__instance_kwargs,
**{k: getattr(self, k) for k in self.__instance_params}
)
for predicate, transform in self._transforms:
merged_param_values = dict(instance_params, **self.p)

Expand Down
5 changes: 3 additions & 2 deletions holoviews/operation/resample.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,13 @@ class ResampleOperation1D(LinkableOperation):
height = param.Integer(default=400, doc="""
The height of the output image in pixels.""")

pixel_ratio = param.Number(default=1, bounds=(1,None), doc="""
pixel_ratio = param.Number(default=1, bounds=(0,None),
inclusive_bounds=(False,False), doc="""
Pixel ratio applied to the height and width. Useful for higher
resolution screens where the PlotSize stream reports 'nominal'
dimensions in pixels that do not match the physical pixels. For
instance, setting pixel_ratio=2 can give better results on Retina
displays.""")
displays. Also useful for using lower resolution for speed.""")

class ResampleOperation2D(ResampleOperation1D):
"""
Expand Down
58 changes: 56 additions & 2 deletions holoviews/plotting/bokeh/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
from packaging.version import Version

from ...core import DynamicMap, CompositeOverlay, Element, Dimension, Dataset
from ...core.options import abbreviated_exception, SkipRendering
from ...core.options import abbreviated_exception, Keywords, SkipRendering
from ...core import util
from ...element import (
Annotation, Contours, Graph, Path, Tiles, VectorField
Expand Down Expand Up @@ -97,6 +97,13 @@ class ElementPlot(BokehPlot, GenericElementPlot):
ratio between the axis scales use the data_aspect option
instead.""")

backend_opts = param.Dict(default={}, doc="""
A dictionary of custom options to apply to the plot or
subcomponents of the plot. The keys in the dictionary mirror
attribute access on the underlying models stored in the plot's
handles, e.g. {'colorbar.margin': 10} will index the colorbar
in the Plot.handles and then set the margin to 10.""")

data_aspect = param.Number(default=None, doc="""
Defines the aspect of the axis scaling, i.e. the ratio of
y-unit to x-unit.""")
Expand Down Expand Up @@ -772,6 +779,47 @@ def _update_title(self, key, plot, element):
plot.title = Title(**self._title_properties(key, plot, element))


def _update_backend_opts(self):
plot = self.handles["plot"]
model_accessor_aliases = {
"cbar": "colorbar",
"p": "plot",
"xaxes": "xaxis",
"yaxes": "yaxis",
}

for opt, val in self.backend_opts.items():
parsed_opt = self._parse_backend_opt(
opt, plot, model_accessor_aliases)
if parsed_opt is None:
continue
model, attr_accessor = parsed_opt

# not using isinstance because some models inherit from list
if not isinstance(model, list):
# to reduce the need for many if/else; cast to list
# to do the same thing for both single and multiple models
models = [model]
else:
models = model

valid_options = models[0].properties()
if attr_accessor not in valid_options:
kws = Keywords(values=valid_options)
matches = sorted(kws.fuzzy_match(attr_accessor))
self.param.warning(
f"Could not find {attr_accessor!r} property on {type(models[0]).__name__!r} "
f"model. Ensure the custom option spec {opt!r} you provided references a "
f"valid attribute on the specified model. "
f"Similar options include {matches!r}"
)

continue

for m in models:
setattr(m, attr_accessor, val)


def _update_grid(self, plot):
if not self.show_grid:
plot.xgrid.grid_line_color = None
Expand Down Expand Up @@ -848,8 +896,11 @@ def _update_ranges(self, element, ranges):
frame_aspect = 1
elif self.aspect and self.aspect != 'equal':
frame_aspect = self.aspect
else:
elif plot.frame_height and plot.frame_width:
frame_aspect = plot.frame_height/plot.frame_width
else:
# Skip if aspect can't be determined
return

if self.drawn:
current_l, current_r = plot.x_range.start, plot.x_range.end
Expand Down Expand Up @@ -1170,6 +1221,8 @@ def _init_glyph(self, plot, mapping, properties):
plot_method = plot_method[int(self.invert_axes)]
if 'legend_field' in properties and 'legend_label' in properties:
del properties['legend_label']
if "name" not in properties:
properties["name"] = properties.get("legend_label") or properties.get("legend_field")
renderer = getattr(plot, plot_method)(**dict(properties, **mapping))
return renderer, renderer.glyph

Expand Down Expand Up @@ -1681,6 +1734,7 @@ def update_frame(self, key, ranges=None, plot=None, element=None):
def _execute_hooks(self, element):
dtype_fix_hook(self, element)
super()._execute_hooks(element)
self._update_backend_opts()


def model_changed(self, model):
Expand Down
Loading

0 comments on commit cab0cc9

Please sign in to comment.