feat: expose datashader categorical blending on hvPlot views - #2035
Merged
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #2035 +/- ##
==========================================
+ Coverage 75.27% 75.50% +0.22%
==========================================
Files 212 214 +2
Lines 39238 39556 +318
==========================================
+ Hits 29538 29865 +327
+ Misses 9700 9691 -9 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Coloring a dense scatter by a category is a datashader job: aggregate per pixel with count_cat and blend the categories present in each one. hvPlot derives that aggregator from by= whenever datashade is set, so the whole plot is expressible declaratively, but the keywords were only reachable as loose kwargs and so invisible to anything reading the params. Promote datashade, dynspread and color_key to params. View.__init__ strips params out of kwargs, so each consumer now forwards them explicitly: get_plot puts them back for hvPlot, _get_args lets the nested explorer controls claim them, and the render-size cap reads the param rather than the kwarg it used to find there.
The agent derives its output schema from the view's params, so the datashade, dynspread and color_key params are now fields the model can fill; the prompt says when to reach for them. Above the row threshold the agent forced rasterize, which reduces each pixel to a single number and so discards the category it was asked to color by. Pick datashade instead whenever by is set, and leave the scalar path alone otherwise.
Asking for one view's schema converted the entire component tree and then failed on the first param type it could not map, so hvPlotAgent could not produce a response model at all. Three things were wrong. Exclusions stopped at the class they were given for, but the generated models inherit, so an excluded name still arrived through its parent's model and, worse, the parent kept recursing into whatever that param pointed at. Subclass expansion is for callers that want a union over a taxonomy, not for describing a single view, and it reached down into Panel and Bokeh objects that have no JSON schema. Field dispatch matched the exact parameter class, so any subclass of a mapped type aborted the whole model rather than the one field. Propagate exclusions to base classes, describe the view on its own, and resolve unmapped parameter types through the MRO.
The constructor already coerced a single column name into a list, but spec validation runs first and the ListSelector rejected the string before that coercion could run, so a spec saying `by: family` failed while the equivalent Python call worked. Coerce in the validation hook as well, sharing one helper so the two paths cannot drift.
Three things the model does that the plot cannot survive. It repeats a column across the axis roles despite being told not to. A groupby naming x or y raises while the plot is built; one naming the same column as by is worse, because it pages every category into its own frame and the datashaded plot then renders successfully having blended nothing. Both are dropped before the spec is validated. It sets z on kinds that have no z, which hvPlot answers with a warning nobody reads. It names colors for the categories the user mentioned and leaves the rest out, which datashader rejects outright since it needs one per category. Fill the remainder from a palette of distinct hues, keeping the colors that were asked for.
_get_args forwards a param only when one of the explorer's controls claims it, but the color_key completion was written outside that gate. hvPlot gained the control after this was written, so on an older hvPlot the keyword reached hvPlotExplorer.__init__ and was rejected outright rather than falling back to the default palette.
Two assumptions held on my machine and not in CI. The color_key forwarding test asserted the keyword is always passed on, but the view deliberately withholds it when the installed hvPlot has no such control, which is the case CI runs. And building the explorer builds an hvPlot converter, which asks for datashader straight away; datashader is not a Lumen dependency. Split the version-dependent assertion into its own test, add one that pins the withholding behaviour either way, and gate the explorer tests on datashader being present. hvPlotView still covers the same forwarding without it.
tests/utils.py already exists to hold an optional dependency guard once rather than per file, so requires_datashader belongs there too. Also hoists a set out of a 256 iteration comprehension and a repeated spec lookup out of its condition.
ghostiee-11
force-pushed
the
feat/hvplot-categorical-datashade
branch
from
August 19, 2026 15:45
1cb9846 to
012d315
Compare
ahuang11
reviewed
Aug 19, 2026
Comment on lines
+976
to
+979
| datashade = param.Boolean(default=False, doc=""" | ||
| Aggregate the data server-side with datashader and send an image | ||
| instead of one glyph per row. Combined with `by` this blends the | ||
| categories present in each pixel, rather than overplotting them.""") |
Contributor
There was a problem hiding this comment.
Should we introduce aggregator too https://hvplot.holoviz.org/en/docs/latest/ref/plotting_options/resampling.html#aggregator
Collaborator
Author
There was a problem hiding this comment.
Yup.. i think it will be good addition :)
Thanks for pointing it out...
Datashading reduced each pixel to a row count with no way to ask for anything else, so shading by an average or a sum was out of reach. The reductions offered are the ones hvPlot's own explorer names. count_cat is left out: hvPlot builds it from by, and naming it here only gets as far as a bare string that never becomes a categorical reduction. Everything except count and any reduces a value column, which hvPlot takes as color; asking for one of those without it is answered from inside the datashader operation with a message that never mentions the spec, so it is caught up front instead. The agent drops such an aggregator, having no field to name the column with.
ahuang11
approved these changes
Aug 24, 2026
…al-datashade # Conflicts: # lumen/views/base.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Coloring a dense scatter by a category is a datashader job, and hvPlot derives the count_cat aggregator from
bywhenever datashade is set, but the keywords were only reachable as loose kwargs and so invisible to the agent's schema; this promotes datashade, dynspread and color_key to params and points the large-frame default at datashade rather than rasterize whenbyis set. It also carries theparam_to_pydanticfix needed to build the agent's response model at all, which overlaps #1935 and #1931 by a different route (fixing the recursion rather than replacing it with an explicitcreate_model), so those should be reconciled before either lands.Colors are best with holoviz/hvplot#1752, but degrade to the default palette without it.