Skip to content
Open
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
34 changes: 34 additions & 0 deletions doc/ref/plotting_options/color_colormap.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,40 @@
":::"
]
},
{
"cell_type": "markdown",
"id": "be669a46",
"metadata": {},
"source": [
"::: {admonition} Custom Color Mapping: Best Practice\n",
":class: tip\n",
"\n",
"\n",
"When you want to map categorical values to specific colors, use `color='column_name'` with `cmap={...}` instead of passing a pre-mapped Series:\n",
"\n",
"**✅ Recommended:**\n",
"```python\n",
"df.hvplot.scatter(\n",
" x='x', y='y',\n",
" color='species',\n",
" cmap={'Adelie': 'blue', 'Gentoo': 'yellow', 'Chinstrap': 'red'}\n",
")\n",
"# Hover shows: x, y, species: Adelie\n",
"```\n",
"\n",
"**❌ Not recommended:**\n",
"```python\n",
"df.hvplot.scatter(\n",
" x='x', y='y',\n",
" color=df['species'].map({'Adelie': 'blue', ...})\n",
")\n",
"# Hover shows: x, y only\n",
"```\n",
"\n",
"Using `color='column_name'` with `cmap` keeps your original data visible in hover tooltips and provides a cleaner, more maintainable API.\n",
":::"
]
},
{
"cell_type": "markdown",
"id": "7a03fc23-d338-44df-9777-9f06ad1096eb",
Expand Down
8 changes: 8 additions & 0 deletions hvplot/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2411,6 +2411,14 @@ def single_chart(self, element, x, y, data=None):
ys += [self.kwds['yerr1']]
kdims, vdims = self._get_dimensions([x], ys)

# Automatically exclude internal style columns from hover tooltips
# if hover_tooltips was not explicitly set by the user
internal_cols = {'_color', '_size'}
if 'hover_tooltips' not in self._plot_opts and any(v in internal_cols for v in vdims):
hover_dims = [d for d in kdims + vdims if d not in internal_cols]
if hover_dims:
cur_opts[element.name]['hover_tooltips'] = [(d, f'@{{{d}}}') for d in hover_dims]

if self.by:
if element is Bars and not self.subplots:
if not support_index(data) and any(y in self.indexes for y in ys):
Expand Down
88 changes: 88 additions & 0 deletions hvplot/tests/testcharts.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,81 @@ def test_xarray_dataset_with_attrs(self):

assert render(ndoverlay, 'bokeh').xaxis.axis_label == 'time (s)'

def test_color_series_excluded_from_default_hover(self):
color_series = self.cat_df['category'].map({'A': 'red', 'B': 'blue'})
plot = self.cat_df.hvplot.scatter(x='x', y='y', color=color_series)

# Check hover_tooltips option
opts = Store.lookup_options('bokeh', plot, 'plot')
hover_tooltips = opts.kwargs.get('hover_tooltips')

# hover_tooltips should be set and not include _color
assert hover_tooltips is not None
tooltip_dims = [tt[0] if isinstance(tt, tuple) else tt for tt in hover_tooltips]
assert '_color' not in tooltip_dims
assert 'x' in tooltip_dims
assert 'y' in tooltip_dims

def test_size_series_excluded_from_default_hover(self):
size_series = self.cat_df['y'] / 10
plot = self.cat_df.hvplot.scatter(x='x', y='y', s=size_series)

assert '_size' in plot.data.columns

opts = Store.lookup_options('bokeh', plot, 'plot')
hover_tooltips = opts.kwargs.get('hover_tooltips')

# hover_tooltips should be set and not include _size
assert hover_tooltips is not None
tooltip_dims = [tt[0] if isinstance(tt, tuple) else tt for tt in hover_tooltips]
assert '_size' not in tooltip_dims
assert 'x' in tooltip_dims
assert 'y' in tooltip_dims

def test_explicit_hover_tooltips_respected_with_internal_columns(self):
color_series = self.cat_df['category'].map({'A': 'red', 'B': 'blue'})
plot = self.cat_df.hvplot.scatter(
x='x', y='y', color=color_series, hover_tooltips=[('x', '@x'), ('color', '@_color')]
)

# Explicit hover_tooltips should be used
opts = Store.lookup_options('bokeh', plot, 'plot')
hover_tooltips = opts.kwargs.get('hover_tooltips')
assert hover_tooltips == [('x', '@x'), ('color', '@_color')]

def test_color_column_name_shown_in_hover(self):
plot = self.cat_df.hvplot.scatter(x='x', y='y', color='category')

assert 'category' in [d.name for d in plot.vdims]
assert '_color' not in plot.data.columns

def test_color_with_cmap_dict_shown_in_hover(self):
plot = self.cat_df.hvplot.scatter(
x='x', y='y', color='category', cmap={'A': 'red', 'B': 'blue'}
)

assert 'category' in [d.name for d in plot.vdims]
assert '_color' not in plot.data.columns

def test_both_color_and_size_series_excluded_from_hover(self):
color_series = self.cat_df['category'].map({'A': 'red', 'B': 'blue'})
size_series = self.cat_df['y'] / 10
plot = self.cat_df.hvplot.scatter(x='x', y='y', color=color_series, s=size_series)

# Both should be in data
assert '_color' in plot.data.columns
assert '_size' in plot.data.columns

opts = Store.lookup_options('bokeh', plot, 'plot')
hover_tooltips = opts.kwargs.get('hover_tooltips')

assert hover_tooltips is not None
tooltip_dims = [tt[0] if isinstance(tt, tuple) else tt for tt in hover_tooltips]
assert '_color' not in tooltip_dims
assert '_size' not in tooltip_dims
assert 'x' in tooltip_dims
assert 'y' in tooltip_dims


class TestChart2DDask(TestChart2D):
def setUp(self):
Expand Down Expand Up @@ -123,6 +198,19 @@ def test_2d_set_hover_cols_to_all(self, kind, element):
plot, element(self.cat_df.reset_index(), ['x', 'y'], ['index', 'category'])
)

# Skip Series.map() tests as they don't work with Dask
def test_color_series_excluded_from_default_hover(self):
raise SkipTest('Series.map() not supported with Dask DataFrames')

def test_size_series_excluded_from_default_hover(self):
raise SkipTest('Series.map() not supported with Dask DataFrames')

def test_explicit_hover_tooltips_respected_with_internal_columns(self):
raise SkipTest('Series.map() not supported with Dask DataFrames')

def test_both_color_and_size_series_excluded_from_hover(self):
raise SkipTest('Series.map() not supported with Dask DataFrames')


class TestChart1D(ComparisonTestCase):
def setUp(self):
Expand Down
Loading