manzt / anywidget

reusable widgets made easy
https://anywidget.dev
MIT License
503 stars 39 forks source link

Enabling rendering on nbviewer by persisting state of anywidget in Jupyter Notebook #343

Open andersy005 opened 1 year ago

andersy005 commented 1 year ago

@manzt and i have been experimenting with anywidget + @carbonplan/maps in https://github.com/manzt/carbonplan. The notebook works perfectly fine when connected to a live kernel. however, i am facing an issue with persisting the widget's state within the notebook and having external services like nbviewer render the initial state of the widget.

to illustrate the problem, i have shared a sample notebook at https://nbviewer.org/gist/andersy005/f8bca6c542135a75ab3e11203eada3a1. as you can observe, the last cell of the notebook is not being rendered.

any guidance on how to resolve this issue?

https://github.com/manzt/anywidget/assets/13301940/d4902396-7795-4de8-b37b-5a2a784b75bd

Cc @katamartin

rgbkrk commented 1 year ago

I'm a bit new to how widgets persist state in the notebook so for my own sake (and maybe others) I'm going to document what I see in the notebook. I can see the widget model in the output:

image

There is no other reference to the widget model in the source of the notebook and nothing for nbviewer to render (other than that text/plain entry).

When I try the simplest widget in JupyterLab, the IntSlider I'm also not seeing the persistence.

Cell:

from ipywidgets import IntSlider
islide = IntSlider()
islide

Notebook:

{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "088fffff-0cb1-4055-a722-2635371d3be1",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "9480df1fa4f04356873a4969ef4038c4",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "IntSlider(value=0)"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from ipywidgets import IntSlider\n",
    "islide = IntSlider()\n",
    "islide"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.1"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}

If I recall correctly, the classic notebook had a "Save Widget State" button. I don't have that in JupyterLab.

rgbkrk commented 1 year ago

It looks like there's a setting that has to be enabled in JupyterLab to automatically save widget state

image

After doing that and also making sure to update my jupyterlab and ipywidgets installation, I restarted JupyterLab and see a big pile of state:

{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "088fffff-0cb1-4055-a722-2635371d3be1",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "03a522b9129949d8ae7a802b0f344980",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "IntSlider(value=0)"
      ]
     },
     "execution_count": 1,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from ipywidgets import IntSlider\n",
    "islide = IntSlider()\n",
    "islide"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.1"
  },
  "widgets": {
   "application/vnd.jupyter.widget-state+json": {
    "state": {
     "03a522b9129949d8ae7a802b0f344980": {
      "model_module": "@jupyter-widgets/controls",
      "model_module_version": "2.0.0",
      "model_name": "IntSliderModel",
      "state": {
       "behavior": "drag-tap",
       "layout": "IPY_MODEL_1db186f70ff646aea16422745dea7a8b",
       "style": "IPY_MODEL_30cb35a482a54c17a2173a45376f43ea",
       "value": 85
      }
     },
     "1db186f70ff646aea16422745dea7a8b": {
      "model_module": "@jupyter-widgets/base",
      "model_module_version": "2.0.0",
      "model_name": "LayoutModel",
      "state": {}
     },
     "30cb35a482a54c17a2173a45376f43ea": {
      "model_module": "@jupyter-widgets/controls",
      "model_module_version": "2.0.0",
      "model_name": "SliderStyleModel",
      "state": {
       "description_width": ""
      }
     }
    },
    "version_major": 2,
    "version_minor": 0
   }
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}

However, that doesn't render in notebook viewer. https://nbviewer.org/gist/rgbkrk/d11992979381414b32d18b16be2d64cf

I've been away from working on nbviewer and nbconvert so I don't know how it takes the widget state into account.

manzt commented 1 year ago

However, that doesn't render in notebook viewer. nbviewer.org/gist/rgbkrk/d11992979381414b32d18b16be2d64cf

I'm not sure how nbviewer is implemented, but nbconvert works for rendering widgets to HTML. https://github.com/flekschas/jupyter-scatter/issues/81#issuecomment-1638951995

If you nbconvert --execute the notebook, it will embed the widget model state, otherwise you'll need to make sure the model state is embedded (i.e., "Save Widget State Automatically" in JupyterLab).

For clarity, @andersy005 this is not an issue with anywidget, but Jupyter Widgets (standard ipywidgets do not render in nbviewer either, thanks to @rgbkrk example). But thank you @rgbkrk for the exploration, and maybe we can at least figure out if this is unexpected or expected behavior!

andersy005 commented 1 year ago

@rgbkrk / @manzt, thank you for looking into this and suggesting workarounds. i was able to convert the executed notebook to HTML and then served it from an s3 bucket: https://carbonplan-share.s3.us-west-2.amazonaws.com/leap-demo.html

and it appears to be working: initial states of the widgets are captured as expected

manzt commented 1 year ago

Awesome! @andersy005 if you use ipywidgets.jslink instead of ipywidgets.link you can also link the inputs on the client side (i.e., sliders and dropdowns will still work in the HTML-only version).

import ipywidgets

colormap = ipywidgets.Dropdown(options=["warm", "fire", "water"])
clim = ipywidgets.FloatRangeSlider(min=-20, max=30)
opacity = ipywidgets.FloatSlider(min=0, max=1, step=0.001)
region = ipywidgets.Checkbox(description="Region")

-- ipywidgets.link((map_widget, "colormap"), (colormap, "value"))
-- ipywidgets.link((map_widget, "clim"), (clim, "value"))
-- ipywidgets.link((map_widget, "opacity"), (opacity, "value"))
-- ipywidgets.link((map_widget, "region"), (region, "value"))
++ ipywidgets.jslink((map_widget, "colormap"), (colormap, "value"))
++ ipywidgets.jslink((map_widget, "clim"), (clim, "value"))
++ ipywidgets.jslink((map_widget, "opacity"), (opacity, "value"))
++ ipywidgets.jslink((map_widget, "region"), (region, "value"))

ipywidgets.VBox([
    ipywidgets.HBox([colormap, opacity, clim]),
    region,
    map_widget,
])
andersy005 commented 1 year ago

thank you very much, @manzt! the jslink changes work :) with the exception of any selection widgets (dropdown, radiobuttons, select, etc), and this appears to be a well known issue upstream:

however, i'm satisfied with dropping the dropdown widget for the time being: https://carbonplan-share.s3.us-west-2.amazonaws.com/leap-demo.html

manzt commented 1 year ago

Great! I'm going to mark this issue as resolved!

manzt commented 1 year ago

Ah, actually realizing this doesn't address that things don't seem to be working in nbviewer. Still need to look into that, but it doesn't seem like any widgets render with nbviewer.

manzt commented 1 year ago

Seems related: