Resources

Shinywidgets - An Overview || Carson Sievert || RStudio

Shiny makes it easy to build interactive web applications with the power of Python’s data and scientific stack. Shinywidgets lets you use ipywidgets in Shiny for Python applications. We called it ipyShiny during development, but we're launching as Shinywidgets! Learn more about how to integrate them into your Shiny for Python apps. . Learn more about Shiny for Python: https://shiny.rstudio.com/py/ Check out our interactive Shiny for Python examples: https://shinylive.io/py/examples/ Content: Carson Sievert (@cpsievert) Producer: Jesse Mostipak (@kierisi) Editing and Motion Design: Tony Pelleriti (@TonyPelleriti)

image: thumbnail.jpg

Transcript#

This transcript was generated automatically and may contain errors.

All right, so let me start by motivating iPyShiny with this interactive web application. So in Core Shiny itself, you know, we have the ability to only render like a few different things like images and plots and text, like some very basic sort of core utilities for rendering Python objects.

Let me mention this is also very experimental at this point, and partially the reason why it's in a separate package at this point is we've been experimenting with the ability to render iPyWidgets with Shiny.

So if you're not already familiar with iPyWidgets, it kind of provides this foundation for Python developers to like create a Python package that wraps a JavaScript library, like a library like Leaflet for creating interactive maps and provide an interface in Python that people can then use that Python package to create these interactive data visualizations inside of Jupyter.

So traditionally, the main sort of platform for using iPyWidgets is Jupyter, but we've kind of provided this kind of wrapper to allow you to write the same iPyWidgets code and just kind of get that into Shiny in the way that makes the most sense to.

Demo: super zip scores map

So this is kind of a motivating example where I have some data here. This is showing a super zip score, and these super zip scores are really like some combination of percent college educated, median income, and I think population for each zip code in the US.

So you can go to this dropdown and instead of like looking at some combination of income and education, essentially, you can look at just education. So you can see compared to this overall score, percent college educated is a lot more highly dense than the Northeast in the US.

And then you can see median income is actually maybe more highly correlated with this overall score. It's at least more widespread out. Basically three different main measures that we're looking at here.

And then this is a density plot of the overall score for all of the US, essentially. So you can see this overall score, there's a pretty high concentration, you know, around zero to 20.

And you know, same similar for college, but it's more pronounced and pretty same similar distribution for income. And then this is the log of the population. Just if I didn't take the log here, it'd be super, you know, you basically just see a straight line up here, and then a very long tail.

And the point of having these density plots here is this is programmed in such a way that I can zoom in on this application. And these density plots will update whenever I change the bounding box of this map, I will recompute the density of these things conditional on just the zip codes within this area.

So as I zoom in here to like the DC area, you can see like the inbounds density here, there's a much higher density of very high overall scores in the DC area. And that's really a combination of the college percent college educated and the income both being, you know, much higher in these areas, as well as a higher population, I guess, population density, I should say.

And then if you want to get more detailed into like, what specifically is in this view, there's kind of this details tab over here. That tells me there's 123 zip codes currently in that bounding box. And amongst those 123 zips, 23% are considered super zips, which just means their score is like above a certain threshold, and their mean income is nearly 100,000. The mean population is a little over 10,000. And the mean college educated is nearly 40%.

And then if I wanted, like if I had other data about these zip codes, you know, I can see other information here, like their relative rank amongst all of the zip codes and stuff like that.

And this is also hooked up to another map where if I want to click on a particular county and see exactly where that county is, I can zoom in on this map. And I can see, you know, where this, sorry, zip code is actually located. And this is currently a bug when I click on this pop up, the table goes away, but hopefully that will get fixed sometime soon, where, you know, I could click on this marker as well to see basically that same information displayed again.

Using shinywidgets: register widget and side effects

So the repo's name is IPyShiny. If you go to the GitHub page, it will have some information on how to install it and a quick overview on how to use it. There's a couple different ways you can actually render an IPyWidget with IPyShiny.

And the first use that it goes over is, should feel really intuitive if you are already an IPyWidget user instead of like a Shiny user. So hopefully most of the audience is in that former group where, you know, you want to, like always with any Shiny app, create your user interface. And in that user interface, use this output widget, which comes from IPyShiny. And like any other sort of UI component, you want to give it an ID that you'll then reference on the server.

And inside of the server function, the kind of recommended way to do this is to first initialize the widget object when the server function first executes. So this will only run once. We'll be initializing the map just once with a center location and a zoom level.

And then once you're ready, you want to call register widget with the ID that should match this ID on the user interface to basically say, I have my widget object here that I want you to perform like an initial draw of for the application.

And then for all of the other updates, you're actually performing side effects. You're not like redrawn or like recreating the widget from scratch. You're essentially just mutating properties on that widget.

So it's a little bit different if you're coming from a Shiny world where we essentially re-initialize and redraw a widget every time we want to update it using our code. But here we will register essentially side effects.

So we haven't seen this yet from Shiny, but this is a decorator that comes from the Shiny reactive sub package. So we saw reactive calc before where this is a calc, you might remember, wants you to return a value. So it reads some other reactive values and then returns a reactive value.

But with side effects, this also can read reactive values, but you're not going to be calling this function for a return value. You're going to be calling this function for its side effects.

So when I say reactive effect, I don't really need to give this a name because Shiny isn't going to like put this anywhere. So I can just use this underscore and define a function. And then I will read the zoom input value, which is coming from our Shiny input slider up here at the zoom.

What this is essentially saying is whenever the slider changes, update the maps zoom attribute. So map.zoom is like an IPyWidget property that will be mutating every time the value of this zoom input value changes.

What this is essentially saying is whenever the slider changes, update the maps zoom attribute. So map.zoom is like an IPyWidget property that will be mutating every time the value of this zoom input value changes.

Bidirectional communication between widget and Shiny inputs

So we just went over like the side effect that allows me to change this input slider to change the zoom level on the map. But what if I wanted to go the other way in the sense of, you know, I can also use this control that's directly attached to the widget instance on the IPyWidget. If I wanted to zoom in this way, what if I wanted to essentially have that bidirectional communication of changing the zoom on the map, update the slider as well.

So in order to do something like this, where you're kind of treating the IPyWidget like an input control to then update some other thing on your user interface, then you want to use this helper function that again comes from IPyShiny to this reactive read essentially allows me to read this zoom property on the map object in the same way that I would read like in a true Shiny input value like this input.zoom.

By using this reactive read here inside of this reactive side effect, that will ensure whenever the zoom property changes on this map, this function is going to re-execute and perform side effects.

So I'll get the zoom level from the map here and use that to inform this function that's coming from Shiny to update the slider. So this is coming from the UI sub package to say, update this slider with an ID of zoom and update its value to whatever the zoom level is on the map, right? So this is like, you know, changes from the slider to the map. And if you want to go the other way from the map to the slider, you could do something like this.

And then this last part here is basically to get this text up here showing the current latitude and longitude on the bounding box of this map, which will change whenever this zoom level changes, but I can also pan on the map to get, you know, a different bounding box with different latitude and longitude. If I wanted to do something like this, this is available on the maps bounds attribute.

So normally if you wanted to read this property in like a Jupyter context, you would just type out map dot or map dot bounds, and then enter on your console and that would give you the current bounds. But we want to reactively read this. Whenever the bounds change, we want this output to get invalidated and basically re-execute. So that's why we use the reactive read here.

And then from this bounds, I'm just getting the latitude and longitude in a more usable way. And then returning a string with the current latitude and longitude and rendering that as text.

Yeah, that basically kind of covers the core of what you would need to do something like, you know, coordinate other controls with a widget in terms of like keeping its state in sync with other things in your application.

Render widget vs. register widget: when to use each

And also keep in mind that like if you are coming from like an R Shiny world and this doesn't make a whole lot of sense to you, or it's like, you know, kind of a foreign programming paradigm that you're not so keen on using. We do also in IPyShiny provide like a more render text like interface. You can initialize widgets inside of a reactive context.

So there might be situations where you really do want the, you know, some parameter that goes into initializing a widget to come from a reactive value before we just kind of like had a hard coded value of four. But if you just needed like, you know, your slider to redraw the map, for instance, every time you change the slider, you could do something like this, which would reinitialize and redraw from scratch that map object, which if that redrawing is relatively cheap, that can be a totally fine way to approach how to render widgets with Shiny.

And again, this is going to feel like a little bit more natural in some cases for Shiny users. Because you're not having to deal with side effects and we generally kind of recommend avoiding side effects as much as possible because it can make your application logic harder to reason about.

Yeah, this other option is available to you, but to really highlight the situation where you do really want the side effects is maybe an example like this, where I'm using Plotly to render a scatterplot with, say, 10,000 or maybe more points. Don't worry too much about like this code. This code is just like fitting a linear regression to like 10,000 different random X and Y coordinates.

And then I'm going to create a scatterplot down here using Plotly. But really the key part here is, you know, I'm using this register widget way of like basically doing all of the work ahead of time to say, here's my scatterplot. I know that this thing isn't going to need to be redrawn. So I'm going to initialize with the scatterplot, register it to do that initial draw.

And then I will basically register the side effect to happen whenever I click this show fit button here, which is tied to this checkbox. Then I just want to basically toggle the visibility of this fitted line that I've added here inside of the figure widget object.

Right. So the scatterplot is actually two different pieces. One, the actual markers, like the black dots here for the scatterplot. But then I've also layered on a fitted line that thankfully with like the way Plotly is set up, you can then mutate like the visibility of that specific line trace is what Plotly would call this.

So if you tried to do something like this, where instead imagine you were creating this figure widget object inside of a render widget context, every time you, every time the show fit value changed, you would have to re-initialize and redraw, you know, a ton of different points and you would get like a bit of lag because every time you click, it has to redo all of that work.

So, yeah, it's kind of a line that people will have to walk in the sense of like, this is really easy and quick to get started with something displaying inside of Shiny. But if you're in a situation where you really need performant updates, other outputs don't depend on your widget state. And it's really convenient to initialize like this is a handy way to go. But in the case where you need performance, this is this is going to be really useful.

So, yeah, it's kind of a line that people will have to walk in the sense of like, this is really easy and quick to get started with something displaying inside of Shiny. But if you're in a situation where you really need performant updates, other outputs don't depend on your widget state. And it's really convenient to initialize like this is a handy way to go.