JetBrains / lets-plot

Multiplatform plotting library based on the Grammar of Graphics
https://lets-plot.org
MIT License
1.55k stars 49 forks source link

Better Marimo Support? #1018

Closed jsnelgro closed 3 weeks ago

jsnelgro commented 7 months ago

Let-plot is my favorite plotting library and for the past few weeks I've been happily experimenting with Marimo, a reactive Jupyter alternative. Unfortunately though, Lets-plot doesn't play very nicely with it. The big issues are:

  1. Charts keep getting appended to the cell output instead of modifying the chart in place (see screenshot)
  2. The output is awkwardly sized and not responsive, so you need to scroll within the iframe
  3. it's not clear how the imperative call LetsPlot.setup_html() works in a reactive environment like this (including it doesn't seem to make any difference)
  4. (less important) Marimo doesn't allow star imports, so chart declarations can be a bit harder to read

Here's an example screenshot: Screenshot 2024-02-15 at 16 44 28

And here's a minimal example Marimo notebook to reproduce the issue:

import marimo

__generated_with = "0.1.86"
app = marimo.App()

@app.cell
def __():
    import numpy as np
    import marimo as mo
    import lets_plot as lp

    lp.LetsPlot.setup_html()
    np.random.seed(42)
    return lp, mo, np

@app.cell
def __(mo):
    stddev = mo.ui.slider(1, 10, step=0.001, label="std dev")
    stddev
    return stddev,

@app.cell
def __(np, stddev):
    data = dict(
        cond=np.repeat(['A', 'B'], 200),
        rating=np.concatenate(
            (np.random.normal(0, 1, 200), np.random.normal(1, stddev.value, 200))
        )
    )
    return data,

@app.cell
def __(data, lp, mo):
    # necessary, otherwise charts keep getting appended to the frame.
    # However, this makes the cell output redraw everything on changes, causing flashes
    mo.output.clear()

    chart = lp.ggplot(data, lp.aes(x='rating', fill='cond')) + lp.ggsize(700, 300) + \
        lp.geom_density(color='dark_green', alpha=.7) + lp.scale_fill_brewer(type='seq') + \
        lp.theme(panel_grid_major_x='blank')
    chart
    return chart,

@app.cell
def __():
    return

if __name__ == "__main__":
    app.run()

I'm not sure where the easiest integration point would be (with Lets plot or Marimo?), but I figured I'd at least get it on your radar. Honestly, a quick and simple QoL improvement would just be make to_html return the html as a string if no filepath was provided. I'm happy to take a stab at patching the to_html, to_svg, etc methods if it seems like a reasonable feature to add.

alshan commented 7 months ago

Hi, by contract, IPython notebook calls our magic _repr_html_() method which indeed returns a string.

What happens with this string in the notebook is beyond our scope.

Does Marimo provide some kind of API alternative to IPython?

jsnelgro commented 7 months ago

interesting, yes it looks like they provide some APIs that might do the trick (for example, rendering arbitrary html). Although, based on their interactive Altair, Plotly, and matplotlib helper methods, I'm wondering if interactive charts would be more of an integration on their end 🤔

jsnelgro commented 6 months ago

So I think if LetsPlot.setup_html(isolated_frame=False) didn't have a hard dependency on IPython's display_html method here, it could possibly solve the issue. Right now, setting isolated_frame=False just throws an error unfortunately, due to the display_html method being None. Calling mo.Html(_repr_html()) almost does the trick but the method returns a whole html page with javascript to attach to a newly created DOM node. And it seems isolated_frame=False is needed to change this behavior?

alshan commented 6 months ago

isolated_frame=True is used when notebook wraps any output in iframe. In this case the output should be self sufficient and includes both the library script and plot data and JS that calls the library.

When isolated_frame=False the library is added to the output of LetsPlot.setup_html() and plot cells only contain JS calling the library.

If display_html is None then there is no IPython in the env and I can't say how anything is shown at all :)

Calling mo.Html(_repr_html()) almost does the trick but the method returns a whole html page with javascript to attach to a newly created DOM node. And it seems isolated_frame=False is needed to change this behavior?

If this is not what you need then absolutely try isolated_frame=False. But in absence of display_html the LetsPlot.setup_html() will not be able to inject the library JS into the notebook :( As a workaround you can try to call _configure_connected_script and then pass the result to mo.Html().

alshan commented 3 weeks ago

@jsnelgro , @signup2k, @VivaldoMendes Hi guys, good new: my PR-2084 was accepted and just made it to the latest marimo 0.8.1 release.

Now Marimo knows how to display lets-plot charts. (also, LetsPlot.setup_html() is no longer required). p.show() however won't work.

Try this simple demo:

import numpy as np
from lets_plot import ggplot, geom_point, aes

x = np.random.rand(100)
y = 2 * x + 1 + np.random.normal(0, 0.1, 100)

data = dict(x = x, y = y)

p = ggplot(data) + geom_point(aes('x', 'y'))
p