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

Add online JupyterChart widget based on AnyWidget #3119

Merged
merged 24 commits into from
Aug 2, 2023
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
775995a
Add ChartWidget based on AnyWidget
jonmmease Jul 15, 2023
ff29ea1
lodash to just-debounce-it for size reduction
jonmmease Jul 23, 2023
cf80f25
Rename ChartWidget to JupyterChart
jonmmease Jul 28, 2023
cce1579
Get rid of _param_watches (just use param keys)
jonmmease Jul 28, 2023
fa629d2
remove set_params method
jonmmease Jul 28, 2023
7a6ef41
rename params Dict traitlet to _params
jonmmease Jul 28, 2023
329860b
Store params in a traitlet object
jonmmease Jul 28, 2023
bf2e26d
Make selections prop a dynamic traitlet class
jonmmease Jul 28, 2023
d726038
Remove selection watches traitlet
jonmmease Jul 28, 2023
84a5503
black
jonmmease Jul 28, 2023
7418f46
Update tests
jonmmease Jul 28, 2023
31abc8d
Use runAsync to avoid race condition
jonmmease Jul 28, 2023
f2d7575
Skip vegafusion test when not installed
jonmmease Jul 28, 2023
fbb2932
Empty-Commit
jonmmease Jul 28, 2023
037b84f
Show errors the same was as the HTML renderer
jonmmease Jul 28, 2023
e87a3ee
mypy / ruff
jonmmease Jul 28, 2023
477b0c5
Update altair/jupyter/js/README.md
jonmmease Jul 29, 2023
cc13950
Update altair/jupyter/js/README.md
jonmmease Jul 29, 2023
9df3d52
Add kernel restart to message
jonmmease Jul 31, 2023
948a23d
Import JupyterChart in else
jonmmease Jul 31, 2023
d27e458
import from top-level
jonmmease Jul 31, 2023
0e574fa
mypy
jonmmease Jul 31, 2023
2e4a48e
Move non-widget selection logic to `util.selection`
jonmmease Aug 1, 2023
0fa4723
Use lodash's debounce for maxWait functionality
jonmmease Aug 1, 2023
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,4 @@ Untitled*.ipynb
.vscode

# hatch, doc generation
data.json
data.json
3 changes: 3 additions & 0 deletions altair/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@
"JoinAggregateFieldDef",
"JoinAggregateTransform",
"JsonDataFormat",
"JupyterChart",
"Key",
"LabelOverlap",
"LatLongDef",
Expand Down Expand Up @@ -569,6 +570,7 @@
"expr",
"graticule",
"hconcat",
"jupyter",
"layer",
"limit_rows",
"load_ipython_extension",
Expand Down Expand Up @@ -607,6 +609,7 @@ def __dir__():


from .vegalite import *
from .jupyter import JupyterChart


def load_ipython_extension(ipython):
Expand Down
20 changes: 20 additions & 0 deletions altair/jupyter/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
try:
import anywidget # noqa: F401
except ImportError:
# When anywidget isn't available, create stand-in JupyterChart class
# that raises an informative import error on construction. This
# way we can make JupyterChart available in the altair namespace
# when anywidget is not installed
class JupyterChart:
def __init__(self, *args, **kwargs):
raise ImportError(
"The Altair JupyterChart requires the anywidget \n"
"Python package which may be installed using pip with\n"
" pip install anywidget\n"
"or using conda with\n"
" conda install -c conda-forge anywidget\n"
"Afterwards, you will need to restart your Python kernel."
)

else:
from .jupyter_chart import JupyterChart # noqa: F401
2 changes: 2 additions & 0 deletions altair/jupyter/js/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# JupyterChart
This directory contains the JavaScript portion of the Altair `JupyterChart`. The `JupyterChart` is based on the [AnyWidget](https://anywidget.dev/) project.
79 changes: 79 additions & 0 deletions altair/jupyter/js/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import embed from "https://cdn.jsdelivr.net/npm/vega-embed@6/+esm";
import debounce from 'https://cdn.jsdelivr.net/npm/just-debounce-it@3.2.0/+esm'

export async function render({ model, el }) {
let finalize;

function showError(error){
el.innerHTML = (
'<div style="color:red;">'
+ '<p>JavaScript Error: ' + error.message + '</p>'
+ "<p>This usually means there's a typo in your chart specification. "
+ "See the javascript console for the full traceback.</p>"
+ '</div>'
);
}

const reembed = async () => {
if (finalize != null) {
finalize();
}

let spec = model.get("spec");
let api;
try {
api = await embed(el, spec);
} catch (error) {
showError(error)
return;
}

finalize = api.finalize;

// Debounce config
const wait = model.get("debounce_wait") ?? 10;

const initialSelections = {};
for (const selectionName of Object.keys(model.get("_vl_selections"))) {
const selectionHandler = (_, value) => {
const newSelections = JSON.parse(JSON.stringify(model.get("_vl_selections"))) || {};
const store = JSON.parse(JSON.stringify(api.view.data(`${selectionName}_store`)));

newSelections[selectionName] = {value, store};
model.set("_vl_selections", newSelections);
model.save_changes();
};
api.view.addSignalListener(selectionName, debounce(selectionHandler, wait, true));

initialSelections[selectionName] = {value: {}, store: []}
}
model.set("_vl_selections", initialSelections);

const initialParams = {};
for (const paramName of Object.keys(model.get("_params"))) {
const paramHandler = (_, value) => {
const newParams = JSON.parse(JSON.stringify(model.get("_params"))) || {};
newParams[paramName] = value;
model.set("_params", newParams);
model.save_changes();
};
api.view.addSignalListener(paramName, debounce(paramHandler, wait, true));

initialParams[paramName] = api.view.signal(paramName) ?? null
}
model.set("_params", initialParams);
model.save_changes();

// Param change callback
model.on('change:_params', async (new_params) => {
for (const [param, value] of Object.entries(new_params.changed._params)) {
api.view.signal(param, value);
}
await api.view.runAsync();
});
}

model.on('change:spec', reembed);
model.on('change:debounce_wait', reembed);
await reembed();
}
Loading