
How to make Interactive Python Dashboards! (Reactivity in Shiny)
This is a quick-start guide to Shiny for Python, part 2 of a multi-part series. Data scientists need to quickly build web applications to create and share interactive visualizations, giving others a way to interact with data and analytics. Shiny helps you do this. In this video, we'll build off of the last tutorial where we learned the basics of building, sharing, and deploying a Shiny app in Python. This video specifically focuses on reactivity in Shiny. You can watch this video as a standalone, but it may be helpful to watch the previous video (https://youtu.be/I2W7i7QyJPI). We'll cover: ⬡ Creating toggle options for dynamic visualizations ⬡ Understanding Shiny's reactivity model ⬡ Implementing various input selectors ⬡ Building reactive components and visualizations ⬡ Using reactive calculations and effects ⬡ Adding and formatting plots with Plotly ⬡ Documentation walkthrough to learn more about reactivity (reactivity.effect, reactivity.event, reactivity.isolate, reactivity.invalidate_later, etc…) Video Resources: Video #1: https://youtu.be/I2W7i7QyJPI?si=nx1dk5ovPc91pvlB Starter Code (from end of video #1): https://github.com/KeithGalli/shiny-python-projects/tree/video1 Final App: https://keithgalli.shinyapps.io/final-app/ Shiny Resources: Shiny for Python Homepage: https://shiny.posit.co/py/ Component Gallery: https://shiny.posit.co/py/components/ Express Documentation: https://shiny.posit.co/py/api/express/ Gordon Shotwell’s “How does Shiny Render Things?”: https://youtu.be/jvV4y2xogf8?si=8uGP8ZfboUj1QM4p Joe Cheng’s “Shiny Programming Practices”: https://youtu.be/B2JzHv4FOTU?si=t4Atii-RSc5ojgom Stay tuned for part 3, where we'll explore how to make your dashboard look more professional (layouts in Shiny). Video by @KeithGalli --- Video Timeline! 0:00 - Intro & Overview 1:01 - Getting Started with Code 2:08 - Adding Shiny Components (Inputs, Outputs, & Display Messages) 3:21 - Creating an Additional Visualization (Sales Over Time by City) 7:55 - What are Reactive.Calcs and How Do We Use Them Properly? (DataFrame Best Practices) 10:27 - Creating an Additional Visualization (Sales Over Time by City) — Continued 14:30 - Filtering City Data with Select Inputs (UI.Input_Selectize) 21:15 - Rendering Shiny Inputs Within Text 22:15 - Quick Formatting Adjustments 22:54 - Understanding the Shiny Reactivity Model (How Does Shiny Render Things?) 24:23 - Adding a Checkbox Input to Change Out Bar Chart Marker Colors 28:00 - Deploying Our Updated App! 29:19 - Advanced Concepts in Shiny Reactivity (Reactive.Effect, Reactive.Event, Reactive.Isolate, Reactive.Invalidate_Later) & Other Resources All videos in the series: Part 1 - How to Build, Deploy, & Share a Python Application in 20 minutes! (Using Shiny): https://www.youtube.com/watch?v=I2W7i7QyJPI&t=0s Part 2 - How to make Interactive Python Dashboards! (Reactivity in Shiny): https://www.youtube.com/watch?v=SLkA-Z8HTAE&t=0s Part 3 - How to make your Python Dashboard look Professional! (Layouts in Shiny): https://www.youtube.com/watch?v=jemk7DoN4qk&t=0s Part 4 - How to combine Matplotlib, Plotly, Seaborn, & more in a single Python Dashboard! (Shiny for Python): https://youtu.be/xDgO5hB4-VU?si=kk20yhdpsBqkMYcC Part 5 - How to Perfect Your Python Dashboard with Advanced Styling! (HTML/CSS - Shiny for Python): https://youtu.be/uYZUS-eFbqw
image: thumbnail.jpg
Transcript#
This transcript was generated automatically and may contain errors.
Hey what's up everyone and welcome back to another video. In this video we're going to build off of the last tutorial where we learned the basics of building, sharing, and deploying a Shiny app in Python. This video specifically will focus on reactivity in Shiny and you can totally watch this video as a standalone but also it may be helpful to watch the previous video. So in this video series we're ultimately working our way towards a dashboard that looks like this. Got a bunch of different Python visualizations all nicely put together in a Shiny app.
But in this specific video we'll kind of see how we can create little toggle options like this and play around with these different types of components that can make our visualizations and our Python apps dynamically update based on input variables. Think it's going to be a fun tutorial? Let's get right into it.
Getting started with the app
Okay to start I recommend going to the description to access the finished code from part one of the tutorial and as a reminder the kind of basic app we spun up in that one just kind of looked like this. You could toggle this number of items and it's just displaying some mock sales data from around the United States.
So in this one we're going to kind of see how we can take these toggle inputs like this numeric input that I'm currently playing with and just do more fancy things and also just understand more in depth on how this operates and how Shiny renders different visualizations and items on your screen. So GitHub link in the description you can fork and clone that to get started with the code that you see on the screen.
If you don't know how to fork and clone feel free to leave a comment on the video and I'll do my best to keep an eye out and help out anyone that is struggling with that. A little technical thing that I'm going to do real quick is just create a new branch I'll call it video two.
So what do we want to do here. Well we want to have more numeric inputs more types of input selectors and maybe add another visualization to what we have on the screen now. How do we do that with Shiny. Well I think a good starting point is to go into the Shiny for Python homepage.
And the types of things that we can access are easily visible and there's a lot of good template code. If you go into components and then click on components. So on this screen you see a bunch of different types of inputs. We currently have this numeric input on our screen. But as you can see there's many other types of options that we could use. So let's play around with a couple of them.
But for any of these you can go ahead and click on the reference. It brings you to the documentation and then you can kind of see how it's used in practice and you can even play around with the input. Pretty pretty cool pretty helpful documentation there. So that is kind of what we're playing around with. And again if you are having trouble like rendering the Shiny app. Check out the first video in the series linked in the description that will get you started with everything you see here so you're not lost.
Adding a new visualization
Yeah let's add another graph. I mean right now we're just plotting items in our sales data frame that we have here. That's part of the GitHub repo. So maybe we would want to do some more sophisticated visualizations as part of this app.
I mean we're ultimately building something like this. So maybe we take one of these dashboards. How about the sales by city. And we start creating this with reactive components. So if I toggle this we can see that the different cities update in the amount of sales per month.
So what do we need to do. Well I think like a good starting point with Shiny apps from my perspective is I kind of like to get like the things that I'm building onto the screen. And then I can kind of toy around with them and make it look all pretty kind of after the fact. So that's going to be kind of the focus is let's get the code on the screen and then we can fix things up and make it look good.
So I'm going to temporarily remove this layout columns and just kind of throw everything in a line on the screen. We'll get into kind of the layout columns and some of those components that are used to layout and kind of make your code look formatted nicely in one of the upcoming videos. So how about we render another polygraph. And we'll call this one maybe like have a little bit more description descriptive of a name than plot one. We'll call it sales by city.
I think our sales by city sales over time maybe sales over time. So that gives us a little bit more flexibility with what we actually include in the graph. And what would be in sales over time. Well we want to take that same data frame that we had before and I can print out that data frame real quick just so you can kind of see what it looks like.
I run this. We'll see the data frame pop up. It's got these products quantity order of the product the date that the product was ordered a fake address. This is all mock data a city and kind of the lat longitude coordinates of that purchase. So it's just a bunch of purchases.
And I think one recommendation one little tip I have in shiny land to kind of help you more easily manipulate your data frames quickly instead of having to print everything. Sometimes I like to go ahead and just honestly render a data frame within my dashboard. So that would look like render data frame and we could call a function called sample sales data. And that's just going to return. How about the data frame that's in debt. And we'll just take how about like the first hundred rows. We don't need much of this.
See if this works. We see we got our data frame. One little quality of life thing I may do here. Just kind of separate this from everything else is I might put this in a UI card and all of this is in the documentation and we'll detail it more in future videos. And I might give a UI header. It's kind of a nice little sneak preview of what you'll see in other videos. We'll call the sample data sample sales data and we'll let this re-render.
So I mean it just really all we did is just add this little card around our data and we can see now we have our data frame here. And it's just kind of nice to see it in this view because we can kind of look a little bit more in depth. It's formatted nicely. We see all of the different columns that we have in our data.
Building the sales over time chart
So what do we want to do with this sales over time? Well basically we want to group by the city. And how about like the month. So we don't have a month right now. So that might be the first thing that we do. So I could do something like df. We also want to make sure that this is a date time object. So df order date equals df order date dot. Oh actually I want to make this a date time object in pandas. So we're going to do pd.to date time. And then we'll pass in all of this stuff. So this is just converting it to a nice date time object.
That gives us some nice little items here. Such as we can easily grab the month by doing something like df order date dot date. Because now it's a date time object. Dot month. And that would give us the numeric month. But let's say we wanted the month name. We can call this function that's built right into date time objects in pandas. And now if we look at our data frame we see we get the month here.
Introducing reactive calc
So this leads me to kind of the first little thing that I wanted to mention on Reactivity and Shiny. And that's this reactive calc annotation. So basically what this allows us to do is when you're putting a lot of different visualizations on a screen. You oftentimes want to reuse pieces of code. Maybe you want to reuse variables. You want to reuse data frames, etc. But you don't want to have to like load those in each time that you need them.
So this reactive calc basically will run an operation. And then it will return a value. And then that value is cached so that whenever you call it. So we're calling it here and we're calling it here. It's going to take that cached value and easily retrieve it. You'll get it really very quickly. So it helps with efficiency here.
So this reactive calc basically will run an operation. And then it will return a value. And then that value is cached so that whenever you call it. So we're calling it here and we're calling it here. It's going to take that cached value and easily retrieve it.
But one thing that I'd kind of recommend is I don't think it's great practice to directly modify that same data frame that is being called and created from the dat object. I kind of recommend that you either do everything in this dat function and then like everyone gets the updated data frame. Or if you wanted to just modify a data frame for one function specifically. I'd recommend copying the data frame so that the render plotly plot one up here wouldn't be affected by us adding a new month column to the data frame. That could cause problems in other situations.
I don't think it would affect anything here. But it's just something to look out for that this calculates a value, caches it, and then we use that same value in multiple places. And if we wanted to modify that data frame, let's do it with a copy instead of the original that other properties will see. And we'll get into this in more detail. But we also could pass in like inputs right in here. And when those inputs were updated, such as the number of items, you could also modify the data frame with a reactive calc. And it would propagate the changes to anything that leverages that reactive calc. At the end of this video, I'll link some extra resources where you can learn more about this stuff.
So in this situation, I think honestly it is probably fine. I would think that it's kind of helpful for any potential function that I may have. It probably is helpful to have the order date as a date time object and the month column. So I'm going to actually go ahead and move this into the reactive calc. And we're going to go ahead and return the data frame instead now. Perfect. But we'll see that this will update and we'll have the same as we had before.
And now what would we want to render here? So we're ultimately doing sales over time. At this point, we don't need to modify anything. So I can just retrieve the value directly. And how might we get sales over time? Let's look at our data frame. We have the month. We have the city. How about we do a group by?
So we'll call this sales equals df.groupby. We're going to group by two different properties. So I'm going to group by the city first and then the month for that city. So it's kind of sales over time for each city. So we want to group by the city and the month. And then I think a good aggregation is the quantity ordered, same as we did for this plot one here. So we can do quantity ordered dot sum and we can print out what that gives us.
We see we get something that looks like this. That looks pretty good. But honestly, what I recommend is it doesn't have a name for the quantity ordered here. So we would want to also reset the index to just kind of make it a little bit easier to work with. And we'll see now we have that quantity ordered column back. And this is great. This is already super helpful. We could probably really just go ahead and plot this as a bar chart.
So fig equals px dot bar. We're going to do pretty similar to our last plotly graph. Bar. Sales. X will be the city in this. Oh no, it will be the month in this case. And our y will be the quantity ordered. I guess we didn't return this. So we want to go ahead and return the figure now. And look at that. We got two graphs now. And this is all of the cities.
And so ultimately I think the splits in this is city by city. Unfortunately, it's not telling us that. But I think you could pass in a parameter into Plotly pretty easy and see which city all of these are for. But it doesn't matter too much for us because ultimately we want to do a filter on our data. Call this sales by city. Which is ultimately the sales. And then only wanting to grab a city if it is equal to whatever city that we select.
So how about as a very temporary placeholder. We take my hometown, Boston, Massachusetts. Boston, MA. And why is this yelling at me? Oh, we need equals equals. Alrighty. And that didn't work yet because we kept it still this. Sales by city now. And look at that. Now this is just the sales in Boston.
I don't know if you see this graph, but there's something weird that I'm seeing here that I don't love. And that's that it's not in order month-wise. So a quick little fix we could do for this is we could import a library called calendar. We could get the month orders by doing calendar.monthName. And then this is basically a helper function that lets you index by the month number. So month one gives us January. So if we wanted all of the months, we could do one through the end. I think zero is just like an empty string in this case.
But we have month orders, and then we can pass into this bar chart called like something category orders equals. And then this takes in a dictionary. I typically pass in a dictionary here. I might be able to take in other things. Basically, we want to set the month column that we use as our x value. We want to set that to have the order defined by month orders. And now we see it. Nice. January through December.
Adding reactive inputs
So right now, honestly, we just plotted a graph, but let's now get into the reactivity here. I might isolate some things just to make it a little easier to see what I'm doing. I'm going to just temporarily comment out this. Okay. Let's add another one of our components.
So if we wanted to filter by specific cities and use city as our input, I think a good way to do that might be to do either selectize multiple or select single. I'm going to do the selectize multiple. We'll start with this. Go ahead and copy this code. It's often easy to just do that. And I will go ahead and pass in this code.
This first one is the name of the variable. So in this case, we'll do city. And I'll say for this, like, select a city, how about. And now this is basically label and value. So in this case, I think it's fine if we use a list and just have our labels and values be the same exact thing.
Maybe just to show you what I'm talking about here, I'll let this render real quick. So you see that, like, the value would be 1A, but the choice, the label for it is choice 1A. So if I grabbed and printed, I'll just print it in here. If I printed input.city and we grab this input by doing. variables with reactive components using this input keyword, and we imported this here from Shiny Express. Grab that and then basically just grab whatever we named it in our, when we defined our input method, our input component. And you just end it with parentheses. That's just kind of the format.
So now if we print out this input city, save that, and just look at our terminal because I'm printing it. We haven't selected anything yet, but if I now do 1A, you see that it says choice 1A, but the actual value is just 1A. But we're fine for our case doing just a list of all the different cities that we have access to.
So to figure that out, we might just print out, print df.city.unique. That will give us a list and actually to make it even easier for us, we can get it specifically formatted as a list. Perfect. We can go ahead and just copy this and then paste it into our code.
One little quality of a life improvement I recommend in coding land, this is just basically anything Python. Not a huge deal if you don't do this, but one thing that I like to do sometimes, go into our sales folder, and we have this app.py file. I'm going to just save it real quick. Initial sales by city code. One thing that I like to do, just because this is so long of a list, you could just manually format it, but a good way to standardize things is I use a code formatter called black. For me, I can run black, app.py, and watch what happens to our code. It gets reformatted, and it just adheres to certain Python best practices, and I just think it oftentimes makes things look more clean. So I often will run that.
So now we have our list of cities here, and if we run our app again, now we can select cities, Boston, how about Los Angeles, San Francisco. We want to filter our data by that, and I don't think anything changed. Did we see any updates? I did not see any updates. To make the updates a little bit more clear, I'll also temporarily hide this.
I'm doing a command forward slash. I always forget my slash directions to comment everything at once. I highlight everything and then do command forward slash. Nice little trick. Okay, so yeah, we're not using the city at all. This is not changing anything. So now we need to go into our sales over time plot. We want to edit the code, and this is right now hardcoded to be Boston, Massachusetts, but now we have a list of items.
So how about we do dot is in, and we'll do input dot city. There's nothing there. Now we get Boston. Now look at that. Now we have three cities. So it's a nice little filter here. That looks pretty good. It is working properly. There would be some way to label this. I'm not going to go through it now because I want to focus on the reactivity, but you see that that works.
I think one issue I had, though, with this is that it didn't start with the default value. So I'd also recommend passing in a default value. So because I am extremely biased, I'm going to use Boston, Massachusetts, my hometown, and also posit HQ hometown. So multiple reasons why Boston is great. Is that default? Come on. I'm going to have to go look at the documentation again. What is the default?
And so here we see in this input selectize, there's a selected. By default it is none, but if we change that to be selected equals, let's say, Boston, Massachusetts, it should work for us. Hey, look at that. We see Boston by default. Cool. But we could also add Los Angeles, Seattle.
So that's one way to do it. We could just set this to be false, and then that would force us to have only one city. One issue I'm seeing here, though, is that now we would need to change this syntax. Instead of doing a list here, it would need to be. I want to set this to be equal equal input.city. And now let's see if this works. Cool. Looks good. Make sure it's changing values. Yeah, it's definitely updating.
One recommendation I might have is we want to kind of prove to ourselves what city is being shown. One cool thing you could also do is you can pass in inputs in multiple spots within Shiny. So I could also, how about, pass in a title to this and just do, like, sales over time dash dash, and then pass in, using an f-string syntax in Python, input.city. So you can pass in your input multiple places and rerun this. Did I make a mistake? What did I do? Oh, I need a comma here. It should have automatically regenerated. Look at that. Sales over time Boston. If I switch this to Dallas, sales over time Dallas.
Shiny's smart reactivity
And I think at this point we can put back our original chart. And we can re-add in our sample data. I'm going to open this up in a browser just so we can see everything. And it's doing something weird now where it's squeezing everything in super tightly. So that is because we currently have this fillable equals true set. So how about we set this to false and just kind of let it take whatever space it needs. And I also want to make sure I update the title to show that this is video two of five, not the first video. That looks better in our situation.
So as we can see, we can update our cities. And we can also update the number of items in the total items in the sales data that were sold. One really important shiny detail here with reactivity is this is one of the things that makes shiny special. And oftentimes the framework you might want to use over other popular frameworks. And that is when I update the number of items, nothing is changing in this visualization down here. Only the chart that is affected by this input number of items in the code, you know, this input numeric is updated.
And this is really important because imagine you had a visualization that takes like a minute to update and render. Well, what if you change any times you updated one of these numbers, you had to reload the whole application, which is how some frameworks work like it just would take forever. So it's super nice that inputs are defined in such a way that when you update something, only the components that use that input are re rendered.
So it's super nice that inputs are defined in such a way that when you update something, only the components that use that input are re rendered.
If you want to learn more about this, I recommend checking out Gordon Shotwell's. How does rendering work in Python video? I'll link it in the description. That being said, in addition to having items that only affect one visualization, you could also have inputs that affect multiple visualizations at the same time. And the same principle applies only the visualizations, only the components that depend on a input will be updated and kind of re rendered when you change that input value.
Adding a checkbox input
So going back to our list of possible components. How about we use like a. Checkbox. OK, very easy. Copy this code, pasted it. And how about this checkbox changes the color of our bar charts in our plot figures. So paste in our code at the checkbox and I'll just call this like bar color. And the label will be. How about we make it red. So make bars red question. That'll be set to false. Initially, input checkboxes, I think, either have a value of false or true.
So if we wanted to make it like red or blue, we'd probably want to have a reactive calc that just kind of reads this bar color and returns red or blue based on the truthiness of this. So I'm going to make another reactive calc. I'm going to call this one just simple like color and that will return. How about. Red, if input dot bar color. So if this is true, then we're going to return red else return input. Sorry. Else return blue. Else blue. This is a nice little shorthand syntax to do that.
And then in our figure, what we would want to do and we'll start with the sales over time is we'd want to do fig dot update traces. And we'd want to pass in marker color equals color. OK, so we see that this is now a different blue. If we go ahead, make bars red. It is now red and we can do the same thing for our first plot. So I'm going to move this into a variable. Same thing, update traces, marker color equals color. And then we return fig.
Get both blue and I click this, make bars red. We see that they both re-render and to make it a little easier to see, I'll make this full screen. They're both red, both blue, both red, both blue. If I update this, only this top chart is changing. If I update this, only the bottom is changing. And then if I click this, both change at the same time. So it smartly renders things based on how the inputs are used.
And honestly, like this information here is enough to kind of get off the ground running. When you start building more and more complex applications in Shiny, this same principle applies. And you can kind of follow it, kind of one visualization piece at a time. Just kind of focus on one piece. If you want to learn a little bit more about kind of some of the complexities and how to think about this, I recommend checking out Posit CTO Joe Cheng's video on Shiny programming best practices. I'll link that also in the description.
Deploying and exploring more reactivity
All right. With that, I'm going to commit the code. I'm going to push it to GitHub. So the link will be also in the description to this finish code. And then I also will push this and deploy it so that you can actually access this app easily in the cloud. And I'll do that by doing the following command. And this was shown in the first video if you didn't see this, if you want to be able to do something like this.
I did a command or control R to find my recent commands. Just a nice little shortcut. You do control R and start typing or let me just try this one more time. Control R and then start typing in the command that you know you want to use. And ultimately, it will auto-complete things. And if you keep clicking control R, it will keep searching through, recursively searching through functions you've used recently. So that's a helpful terminal trick.
So this will be sales video 2. I'm going to deploy that. And I will speed this up so you can see the final thing deployed. Awesome. And we see now we have this URL, sales video 2, so you can access this link and play around with the app right there.
All right. So now that we've learned this, the basics of reactivity, what is next? Well, you can keep building on your knowledge of reactivity. There's some really cool stuff that I didn't show here in this video. So first two resources I recommend are Gordon Shotwell's How Does Shiny Render Things and Joe Cheng's Shiny Programming Practices. These have a lot of additional kind of info and they're true experts, you know, building shiny. So they have a lot of great input and advice regarding best practices.
But the second recommendation that I would make is to go to the documentation and explore reactivity there. So, again, so for Shiny Express that we're using in this video, you can go to this URL. Also, we'll link it in the description. And you can check out some of the specific components for reactivity. So look up reactive, reactive programming. Cool. So we touched on reactive calc. We didn't touch on reactive effect, but let me click on that real quick. And reactive effect is basically a reactive calc, but there's no return. So reactive calc has a return, reactive effect, same thing, no return though.
So we can see examples if we scroll down, down here. Other things that I would say are important here that you should check out is how about a reactive event. So imagine that you didn't want something to automatically update. You only wanted it to re-render if a certain event was true. You could use a reactive event for that case. And I always learn the best by going to the examples down at the bottom. This one's a little bit more complex. But basically we see we have some functions here.
And so this first one has a reactive event that is triggered on input.buttonOut. We have another reactive effect that is triggered on, let's see, buttonCalc. And we have a third reactive event that is triggered on input.buttonEffect. And so what we'll basically see is that we have, if we go down to what it's outputting, we have this value that is updating like every 0.5 seconds. But these functions we saw are only triggered when we click on a specific button.
So this first one, input.buttonOut, only will this be called, this string value.get, when I click buttonOut. So buttonOut is defined by an input action button here, the one update number. So watch what happens when I click this. It updates to 980. So it's going to be whatever value we see ticking over here, 785.
This next one, buttonCalc, will only be updated. It will be 1 over the value.get when I call this. This third one will only be triggered when I click button.effect. And one thing that's cool, though, is like you can kind of edit these examples in real time. So what if I wanted to make them all dependent on the first button click? ButtonOut, buttonOut.
So now what we'll see is if I rerun this by clicking the top right, rerun app, and we go down. When I click this, all three of these will update because they're all triggered on the first button. Look at that. Cool. So that's ReactiveEvent. And, you know, look at the documentation. Final one, ReactiveIsolate is nice if you don't want something to be affected by kind of a recalculation. You only want it to happen on certain situations. So in this case, if I change the slider, the histogram down here is not updated. But if I click go, then it is updated. So that's kind of what this ReactiveIsolate is doing. You can read all about it in the description.
I do want to mention Invalidate later. I think this is pretty cool. Imagine you had some sort of app that was running in real time. And every like one second, it had to collect some new data. And I wanted to display a real-time stream of the values of this data. Invalidate later is super helpful for this. It basically tells this render.text function in this situation, in this example, that every 0.5 seconds, this function should be invalidated and you should recalculate a new value for it. And that's why we're seeing these numbers change. So if we need to do something time and time again at a set frequency, look at using this ReactiveInvalidate later.
So if we need to do something time and time again at a set frequency, look at using this ReactiveInvalidate later.
A lot of useful stuff in the docs. The docs are always great. Read the docs. It can definitely help take your skills with reactive components to the next level. But I think that we kind of did the basics. We looked at where we can learn about more advanced things. I think that's a good stopping point for this video. Hopefully you enjoyed this one. It was fun to make. If you did enjoy it, as always, it means a lot of you throw it a thumbs up and subscribe to the channel if you haven't already. We will be back with another, the third video in the series within the next two weeks. So be on the lookout for that. All the videos in the series thus far will be in the description. Check that out. And all the links and everything you saw in this video in the description. Cool. That's all I have for this video. Until next time, everyone. Peace out.

