jupyter-widgets / ipywidgets

Interactive Widgets for the Jupyter Notebook
https://ipywidgets.readthedocs.io
BSD 3-Clause "New" or "Revised" License
3.13k stars 950 forks source link

Showcase/example page widget-based apps #3348

Open swelborn opened 2 years ago

swelborn commented 2 years ago

Background

This is meant to be a discussion. I'm not sure where to post this. I'm new here.

I am an experimental scientist that currently works at Stanford Synchrotron Radiation Lightsource. I started learning python, along with your widgets, around 2 months ago to create a user interface for a data analysis tool. I believe that I exemplify your average user. Maybe I'm wrong, just a feeling I have.

As I made progress on my app, I have connected with scientists at other national labs to showcase my work. As it turns out, nearly everyone is using ipywidgets to enable at least some amount of interactivity within their data analysis notebooks. This should come as no surprise, considering many people in science use python/Jupyter Notebooks -- it logically follows that they would eventually want some interactivity to inspect their data, and ipywidgets is a great way to do that.

That being said...

Problem

There are 1,000,000 hackers like me out there who want to learn how to use ipywidgets, but reading documentation and browsing github/stackexchange to find answers to problems can be a big time suck. The notebooks I have seen are relatively simple - plotting here and there, few sliders, many cells with a lot of code, etc.

When I was starting out, I kept wondering to myself "where can I find applications built on it? What can be done with it?" While I fell in love with widgets (I did find out what could be done, eventually), many scientists simply do not have the time to learn it, and really do not know the power of it.

Suggested Improvement

A central location for open-source ipywidgets-based applications/github repositories. This could be on an "example applications" or "showcase" page. I think that if people saw more examples (with code) for full applications built on ipywidgets, they would be much more inclined to use them.

While it may seem like I am just trying to shill my app (I am, a little), I think that this page would attract many more users, and eventually more contributors.

Thanks for all your hard work !

swelborn commented 2 years ago

It seems like bqplot has just added something like this, coincidentally.

martinRenou commented 2 years ago

Ahah! The bqplot gallery is is still under development, nice that you noticed it. We'll add some more examples in the next coming days.

You can also look at the voila-gallery https://voila-gallery.org that contains some examples using ipywidgets.

maartenbreddels commented 2 years ago

Hi Samuel,

We already discussed this on gitter, but let me repeat/elaborate on it here. Over the years, @mariobuikhuizen and I have built quite a few things with ipywidgets, and looking back, let me give you my biased view.

I think ipywidgets are easy to get started with (in the sense of https://www.infoq.com/presentations/Simple-Made-Easy/ where easy != simple, and simple being the opposite of complex). That makes it great to get people on board and put together a simple interactive notebook.

The next step is to build a more extensive application with it. This can introduce (accidental) complexity, such as many callbacks scattered around that manage state in the application, resource leaks due to forgetting detaching callbacks, mixing UI code and logic (although there is an example in the tutorial on how to avoid that https://github.com/jupyter-widgets/tutorial ).

Another issue is that ipywidgets are not straightforward to compose. For instance, given this solution for rendering Markdown, there is no obvious solution to making a 'Markdown' widget that others can reuse. Or imagine making a markdown editor on top of that etc. etc. I am not saying it is impossible, but there is no straightforward way, and three people will probably come up with three different solutions.

In terms of building applications, I think using ipywidgets is similar to building a Web-app using DOM manipulation in the browser. One reaches for the easiest-to-understand parts and tries to create something large with it. One should, however, reach for a framework that helps you focus on building the app, helps you manage state etc, like React/Vue/Angular. Similarly, for for ipywidgets, it makes sense to build a framework on top of ipywidgets, to avoid/conquer the (accidental) complexity when using 'raw' ipywidgets.

One such framework I've recently put together is https://github.com/widgetti/react-ipywidgets It has a bit of a learning curve but solves all the problems I discussed.

I didn't just write this to scratch an itch. I love ipywidgets and want them to thrive and build larger applications using them. @mariobuikhuizen and I are doing that right now, and I can say that the dog food tastes great. I finally have the feeling that all things I write using react-ipywidgets are re-usable and simple to understand/reason about. That being said, the library is still young. The implementation currently has a few flaws, but the technology (React) has been proven for many years.

Note that I don't think this problem is unique to ipywidgets. Also, I don't believe ipywidgets has the wrong design. It simply means that the API of ipywidgets is not the layer upon which you should try to build a larger application. A larger application simply requires an extra layer (like react-ipywidgets) to manage complexity and keep your code simple.

I think also a good analogy is mathematics. Complex problems can become much simpler using advanced math, e.g., calculating the area of a curved surface curve using integrals is simpler than using arithmetic (oh, imagine the pain), but it does require learning a new framework (integrals).

Regards,

Maarten

maartenbreddels commented 2 years ago

As an example, I translated the password generator from https://github.com/jupyter-widgets/tutorial/tree/scipy2018/notebooks/password_generator_example to react-ipywidgets (including an extra seed option):

import react_ipywidgets as react
from react_ipywidgets import ipywidgets as widgets
import random
import string

def generate_password(seed: int, password_length: int, special_character_groups: str, include_numbers: bool):
    random.seed(seed)
    new_pass = []
    for _ in range(password_length):
        new_pass.append(random.choice(string.ascii_letters))

    # Generate a list of indices for choosing which characters in the
    # initial password to replace, then shuffle it. We'll pop
    # elements off the list as we need them.
    order_for_replacements = list(range(password_length))
    random.shuffle(order_for_replacements)

    # Replace some of the letters with special characters
    n_special = random.randint(1, 3)
    for _ in range(n_special):
        loc = order_for_replacements.pop(0)
        new_pass[loc] = random.choice(special_character_groups)

    if include_numbers:
        # Number of digits to include.
        n_digits = random.randint(1, 3)
        for _ in range(n_digits):
            loc = order_for_replacements.pop(0)
            new_pass[loc] = random.choice(string.digits)

    return "".join(new_pass)

@react.component
def PasswordGenerator(on_password=None):
    password_length, set_password_length = react.use_state(8)
    seed, set_seed = react.use_state(random.randint(0, 2**63))

    special_character_groups_index, set_special_character_groups_index = react.use_state(0)
    options = [",./;[", "!@#~%", "^&*()"]
    special_character_groups = options[special_character_groups_index]

    include_numbers, set_include_numbers = react.use_state(False)

    password = generate_password(seed, password_length, special_character_groups, include_numbers)
    if on_password:  # optional callback to bubble data up
        on_password(password)

    with widgets.VBox() as main:
        widgets.HTML("Generated password is:")
        widgets.HTML(password, placeholder="No password generated yet", layout={"margin": "0 0 0 20px"})

        widgets.IntSlider(
            value=password_length, on_value=set_password_length, description="Length of password", min=8, max=20, style={"description_width": "initial"}
        )

        widgets.Label("Choose special characters to include")
        widgets.ToggleButtons(
            index=special_character_groups_index,
            description="",
            options=options,
            style={"description_width": "initial"},
            on_index=set_special_character_groups_index,
            layout={"margin": "0 0 0 20px"},
        )

        widgets.Checkbox(value=include_numbers, description="Include numbers in password", style={"description_width": "initial"}, on_value=set_include_numbers)
        widgets.Button(description="New seed", on_click=lambda: set_seed(random.randint(0, 2**63)))

    return main
display(PasswordGenerator())

Which can now easily be embedded in a larger application, e.g.:


@react.component
def CreateAccount(on_create_account=None):
    # optional callback
    on_create_account = on_create_account or (lambda *ignore: None)
    username, set_username = react.use_state("")
    password, set_password = react.use_state("")
    generated_password, set_generated_password = react.use_state("")
    with widgets.HBox() as main:
        with widgets.VBox():
            widgets.Text(description="Username", value=username, on_value=set_username)
            widgets.Text(description="Password", value=password, on_value=set_password)

            def copy_over_password():
                set_password(generated_password)

            widgets.Button(description="Copy generated password", on_click=copy_over_password)
            ok = True
            if len(username) < 3:
                widgets.Label(value="Username is too short (at least 3 characters)")
                ok = False
            if len(password) < 8:
                widgets.Label(value="Password is too short (at least 8 characters)")
                ok = False
            widgets.Button(description="Create account", disabled=not ok, on_click=on_create_account(username, password))
        with widgets.VBox():

            PasswordGenerator(on_password=set_generated_password)

    return main
display(CreateAccount(on_create_account=print))
swelborn commented 2 years ago

Dear Maarten,

Thank you for the thoughtful reply. I agree with you: working with raw ipywidgets when attempting to build a large, complicated application is tricky for all of the reasons you listed. I have rewritten my code about 5 times to reduce the complexity, but it is still a bit unmanageable. The way I have "reused" my widgets is storing them within attributes of custom widget classes that contain specific states (i.e. on/off states for buttons). I have also tried to maneuver away from mixing backend stuff with frontend stuff, but some backend methods inevitably creep into the frontend as you mentioned.

What you are arguing for here is extremely valuable. If I am to build another application in the future, I will definitely learn and use your package. I would like some of your dogfood. If I find some time (highly unlikely within 6 months) to eventually migrate tomopyui over, I will attempt to and ask you questions. At the moment, however, I have 1,000 things going on outside of development (i.e. searching for a gig........).

Are any of your react-ipywidgets applications public? The password generator example is useful, but like I mentioned in the OP I think it is very educational to have examples of a full application for people to play around with and get a sense of what is going on in the code.

Best, Sam

jmk89 commented 1 year ago

@swelborn I haven't played around with these myself, however the calculator and todo app might be useful for you to see how react widgets can be reused.

Reusing components is a good step towards building applications a bit larger in size. Certainly it will be easier to keep your sanity, at least 🙃 . Once applications get large enough (you'll know when), you'll want to take a look at state management.