manzt / anywidget

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

widget renders upon first cell execution but not upon second execution #531

Closed jakobtroidl closed 7 months ago

jakobtroidl commented 7 months ago

I am running into issues with my widget rendering only upon the first cell execution. For the second and following executions, the widget does not show up (in particular, the SVG component that I use to draw a D3 chart -- see the repo link below). However, if I modify the javscript code while npm run dev is running and the esm code rebuilds, my widget reappears.

I think it has to do with how often the js render function is called. For some reason, the render function gets called three times on the first run after building the JS code. Then, my widget renders correctly. For all other later cell executions, the widget only renders once, and then my widget doesn't display.

Here are the core components of my widget:

widget.js

import * as d3 from "https://cdn.jsdelivr.net/npm/d3@6/+esm";
import { BarChart } from "./barchart.js";
import "./widget.css";

function render({ model, el }) {
  console.log("rendering the widget"); // prints multiple times for the first run after the build, and only once for subsequent runs
  // more code here that renders a D3 bar chart 
  // ***
  // (see more in the example repository below)
}
export default { render };

__init__.py

import importlib.metadata
import pathlib
from io import StringIO

import anywidget
import traitlets

try:
    __version__ = importlib.metadata.version("jbar")
except importlib.metadata.PackageNotFoundError:
    __version__ = "unknown"

class BarChart(anywidget.AnyWidget):
    _esm = pathlib.Path(__file__).parent / "static" / "widget.js"
    _css = pathlib.Path(__file__).parent / "static" / "widget.css"
    data = traitlets.Unicode().tag(sync=True)

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

    def update(self, data):
        output = StringIO()
        data.to_csv(output, index=False)
        self.data = output.getvalue()
        output.close()
        return self

example.ipynb

%load_ext autoreload
%autoreload 2
%env ANYWIDGET_HMR=1
import pandas as pd
df = pd.read_csv('data/test_features.csv')
df.head()
from jbar import BarChart
barchart = BarChart()
barchart.update(df)

Here's a relatively minimal repository to reproduce the problem. The issue is consistent across VS Code (v1.87.2) and JupyterLab. I am using anywidget version 0.9.6.

manzt commented 7 months ago

I think this could be related to https://github.com/flekschas/jupyter-scatter/issues/37. Specifically, I think there is some race condition with dynamically using clientWidth from the element given to then render function. In JupyterLab, things work just fine. I think you could use a setTimeout trick to append elements to the DOM, and wait a tick to inspect clientWidth:

import * as d3 from "https://cdn.jsdelivr.net/npm/d3@6/+esm";
import { BarChart } from "./barchart.js";
import "./widget.css";

function render({ model, el }) {
   /** ... */ 
   setTimeout(() => {
    barchart.init();
    barchart.update("outgoing_syn");
  })
}

export default { render };
jakobtroidl commented 7 months ago

Adding a setTimeout before initializing the bar chart works. However, I don't think it relates to dynamically using clientWidth. Even if I hardcode my widget's width, this problem happens. Anyway, the above method solves my problem. Thanks so much for your help, @manzt!

manzt commented 7 months ago

Sorry, I think it's related to whether the el provided is already inserted in the DOM. In jupyter it seems like it is, but in VS Code is seems like it may not be (which is why you need to wait a tick). If i had time to dig in, i'd maybe open an issue in vscode-jupyter.