-
Notifications
You must be signed in to change notification settings - Fork 795
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
Add online JupyterChart widget based on AnyWidget #3119
Conversation
cc @philippjfr. I know you've put a lot of thought into how Panel's Vega Pane handles Altair chart selections, so if you happen to have a chance to take a look at this design I'd definitely appreciate your input. It might also be nice to see if we can work toward a future where this Jupyter widget and Panel's Vega Pane share a similar API for dealing with selections/params. |
altair/widget/js/index.js
Outdated
@@ -0,0 +1,70 @@ | |||
import embed from "https://cdn.jsdelivr.net/npm/vega-embed@6/+esm"; | |||
import { debounce } from "https://cdn.jsdelivr.net/npm/[email protected]/lodash.js" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Know it’s not bundled, but lodash-es is a pretty large dep (98kb minified, https://bundlephobia.com/package/[email protected] ) for just one import.
The modern alternative I’ve been using is just-debounce-it from https://github.com/angus-c/just
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome, thanks for the recommendation! Switched in ff29ea1
I haven't reviewed the code, but all the high level design decisions here make sense to me! I really like the examples and think they will be a great addition to the docs. I also really like the future vision of sharing a similar API as Panel, so that it is easy to transition between the two. One comment about the name. To me, a widget traditionally is a component like a dropdown or slider. I know that this is not technically true and a JupyterWidget can be almost anything, but I wonder if some users might find the naming confusing or if it is mostly me. I don't have a better suggestion for a name, but we might want to do some rewording in the docs if we call this |
Thanks for that feedback @joelostblom. You make a good point about the potential confusion that could result from overloading the term "Widget" like this. Another possibility that just came to mind would be to use the term "Jupyter" in the name instead of "Widget". e.g. An argument against calling it How does that sound to you? |
I do quite like the explicit of using the word "Jupyter" in the name, but I agree with you that it might not be obvious that it then also works in eg VSCode... although maybe it would be clear enough? We can be explicit in the docs that the I do like the idea of aiming for |
I agree with @joelostblom on the potential confusion on the name. Given this spec: import altair as alt
bind = alt.binding_range(min=0, max=100, step=1, name="my label😀")
param = alt.param(name="my_param", bind=bind, value=50)
chart = alt.Chart().mark_point().add_params(param)
widget = alt.ChartWidget(chart)
widget.params It returns a dict:
Dare to dream: could this also be returned as an attribute so we can set it pythonic using a property setter? As such: widget.params.my_param = 50 Another thing. If I make a JavaScript error. Like adding a space in the param
Where the
With this spec: import altair as alt
bind = alt.binding_range(min=0, max=100, step=1, name="my label😀")
param = alt.param(name="my param", bind=bind, value=50)
chart = alt.Chart().mark_point().encode(
color=alt.condition(param,alt.value('red'), alt.value('blue'))
).add_params(param)
widget = alt.ChartWidget(chart)
widget Would this type of trace back also be possible on the AnyWidget model? |
I'll look into implementing a similar traceback
@manzt, do you have any ideas on this? Right now One option I can think of is that I could replace |
Also @mattijn, how do you feel about |
|
@mattijn, thanks to your dream I worked on it some more, and I figured out how to do it! I updated the PR description above with the new syntax. And it's now possible to use the Jupyter Widget's Screen.Recording.2023-07-28.at.10.35.58.AM.movI also renamed it to |
Very great (and inspiring!). I find the interplay very intuitive, also the natural linkage with other components of ipywidgets is 👍👍 |
This looks amazing @jonmmease, really great work! On aligning the selection I would indeed love to work on that although I do envision some difficulty. Panel leans very heavily on Param, so it dynamically creates parameters corresponding to Vega selection On the naming I'll throw out one more suggestion: |
ChartWidget -> JupyterChart Co-authored-by: Mattijn van Hoek <[email protected]>
ChartWidget -> JupyterChart Co-authored-by: Mattijn van Hoek <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is such an awesome PR!! 🔥 Thank you @jonmmease for this great contribution and
@manzt for the anywidget project! I'm very excited to see what people will come up with. Personally, I'll definitely use this to create interactive reports using widgets linked to charts with jslink
and then convert with Quarto to standalone html 😍
I added a few smaller comments but afterwards this looks good to me.
Thanks for the helpful review @binste, I'll address your comments shortly.
I didn't test this explicitly, but I'm afraid this might not work with |
No worries at all! I can also use the normal Vega input widgets for this. The use cases with a running Python kernel are more exciting anyway :) |
Ok, I think I addressed your comments @binste, thanks again for taking a look! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
:) Thanks!
I did a little refactoring to move the widget independent data classes and construction logic to My hope is that other dashboard toolkits will be able to present selections to end users using these same dataclasses. That way users will have a partially consistent experience when moving between |
I'm going to revert back to using Here is how it works with Screen.Recording.2023-08-01.at.6.26.40.PM.movYou can see that while continually dragging the selection region around, no updates are made to the Python side. Updates aren't sent until 500ms after dragging has completed. lodash's debounce wrapper has an optional Screen.Recording.2023-08-01.at.6.36.17.PM.movYou can see that updates are sent to Python every 500ms while the selection region is continually dragged around. This is the more desirable behavior for our use case. In the future we can look for a smaller implementation of this functionality. |
Ok, going to merge this and start working on documentation. Thanks again everyone! Let me know if anyone has thoughts on where this should be documented as I haven't started thinking about that yet. |
@@ -0,0 +1,80 @@ | |||
import embed from "https://cdn.jsdelivr.net/npm/vega-embed@6/+esm"; | |||
import { debounce } from "https://cdn.jsdelivr.net/npm/[email protected]/lodash.js" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can import various lodash functions independently from lodash-es
:
import { debounce } from "https://cdn.jsdelivr.net/npm/[email protected]/lodash.js" | |
import debounce from "https://cdn.jsdelivr.net/npm/[email protected]/debounce/+esm"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh cool. Does this have an impact on bundle size?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes (and eliminates unnecessary imports). Should have suggested this originally (sorry!), see #3135
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks great to me – excellent work @jonmmease! Really excited to see this come together. Just a minor comment about lodash.
Oops, missed submitting – no worries! |
This overview has been updated after the rename to JupyterChart and the change to using traitlets for params and selections.
Background
This PR is a follow on to #3108. After a lot of good discussion (thanks @manzt, @binste , @mattijn, @domoritz, and @joelostblom), we decided that the best first step is for Altair to bundle an AnyWidget-based Jupyter Widget that loads it's JavaScript dependencies from a CDN. This approach requires an internet connection (like the default html renderer) but it doesn't result in an increase in Altair's wheel size, and it doesn't introduce a development dependency on npm. Furthermore, AnyWidget's architecture leaves open the future possibility of providing an optional "altair-offline" Python package that would include an offline bundle of the same JavaScript logic included in this PR.
Now that we have the distribution decision out of the way, this PR focuses on the widget's design and logic.
Basic Usage
To use the widget, simply wrap an Altair chart in
alt.JupyterChart
.Here's the simplest example of using the widget:
Updating charts in-place
The
JupyterChart
'schart
property can be assigned to a new Altair chart, and the new chart will immediately be displayed in place of the old one. And the update looks really smooth, parts of the chart that don't change don't seem to flash at all.Screen.Recording.2023-07-28.at.10.39.35.AM.mov
Params and Selections
The JupyterChart includes special handling for Altair params (both regular and selection params). The current state of regular non-selection params are stored in the
.params
property, and the current state of selection params are stored in the.selections
property.Observe changes to a regular param in Python
Here's what this looks like for the Slider Cutoff gallery example:
Screen.Recording.2023-07-28.at.10.26.04.AM.mov
The
.params
property is a traitlet class with dynamic properties with one attribute for each regular parameter (onlycutoff
in this case). The property is updated whenever a regular param's value changes. The.params
property is a traitlet, so it's possible to [set up callbacks] to listen for changes to individual param values. See https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20Events.html#registering-callbacks-to-trait-changes-in-the-kernelScreen.Recording.2023-07-28.at.10.31.00.AM.mov
Set the value of a regular param from Python
It's also possible to set the value of a regular param from Python by simply assigning to them. For the example above:
Screen.Recording.2023-07-28.at.10.33.43.AM.mov
Linking params with ipywidgets
Because
params
is a traitlet object, it's possible to use the ipywidgetslink
anddlink
functions to bind params to other ipywidgets.Screen.Recording.2023-07-28.at.10.35.58.AM.mov
In fact, we can update the Altair example to remove the slider binding, and just drive the interaction with ipywidgets:
Screen.Recording.2023-07-28.at.10.37.55.AM.mov
Observing selection in Python: Selection types
After a bit of contemplation, I settled on distinguishing three kinds of selections for the purpose of presenting them to the user, each with a dedicated dataclass:
PointSelection
,IndexSelection
, andIntervalSelection
.PointSelection
The
PointSelection
dataclass is used to store the current state of an Altair point selection (as created byalt.selection_point()
) when either afields
orencodings
specification is provided. One common example is a point selection withencodings=["color"]
that is bound to the legend.Here's an example:
Screen.Recording.2023-07-28.at.10.42.03.AM.mov
The
jchart.selections
property is a traitlet class with properties for each selection ("point" in this case) to one of the three selection dataclasses. Each of these selection dataclasses havevalue
andstore
properties. Thevalue
property is designed to be the easiest to use. Thestore
is Vega-Lite's internal representation of the selection that is used to apply filtering. I wanted to include it here because I'm working towards using it to automatically filter the input dataset based on the selection, and thisstore
value has the info needed to do that.IndexSelection
What I'm calling an "Index Selection" is an Altair point selection (as created by
alt.selection_point()
) when neither theencodings
norfields
properties are specified. In this situation, Vega-Lite generates a special 1-indexed column (using theidentifier
transform) named_vgsid_
, and builds a point selection referencing this column.We could use
PointSelection
above to represent these selections, but the selection specification would contain references to this internal_vgsid_
column. In this case it's a lot more useful to convert the selection state into a list of zero-based indices that are compatible with pandasiloc
indexing.Here's an example:
Screen.Recording.2023-07-28.at.10.45.29.AM.mov
Looking at the
store
we can see what the actual selection is referencing:IntervalSelection
The
IntervalSelection
dataclass is used to store the current state of an Altair interval selection (as created byalt.selection_interval()
). One common example is a box selection. For example:Screen.Recording.2023-07-28.at.10.47.53.AM.mov
Coverage
I tried all of the Interactive Charts gallery examples, and they all fall nicely into these four concepts (regular params, point selections, index selections, and interval selections).
A dashboard
Just for run, here's a mini Jupyter Widgets dashboard that displays the selected rows in a pandas HTML table:
Screen.Recording.2023-07-22.at.7.36.29.PM.mov
Follow-on work
Binary serialization
This PR doesn't tackle binary serialization, but the foundation is here since the Jupyter Widget protocol supports raw binary buffers without base64 encoding.
Listen to and update arbitrary Vega signals and datasets
In order to migrate VegaFusion widget to use this ChartWidget, I'll need to add support for updating arbitrary Vega signals and datasets (not only the explicit params declared in the Vega-Lite spec). I'll also need to be able to register callbacks to run in response to changes to arbitrary signals and datasets.
Since I'm not ready to use this yet, and it would add a bit more complexity, I decided not to include this functionality in the initial version.
Setting selections
In the Vega that Vega-Lite produces, it's not always straightforward to set selections from the outside. There may be more we can do here, but for you get an error if you try to set a selection property to a new value in Python.