|
| 1 | +--- |
| 2 | +jupytext: |
| 3 | + custom_cell_magics: kql |
| 4 | + formats: ipynb,md:myst |
| 5 | + text_representation: |
| 6 | + extension: .md |
| 7 | + format_name: myst |
| 8 | +kernelspec: |
| 9 | + name: python3 |
| 10 | + display_name: Python 3 (ipykernel) |
| 11 | + language: python |
| 12 | +language_info: |
| 13 | + name: python |
| 14 | + nbconvert_exporter: python |
| 15 | + pygments_lexer: ipython3 |
| 16 | +--- |
| 17 | + |
| 18 | +# altair |
| 19 | + |
| 20 | +```{admonition} download the zip |
| 21 | +:class: warning |
| 22 | +
|
| 23 | +like many similar systems, altair-made dashboards might not run well in Jupyter notebooks, |
| 24 | +so you can {download}`download the companion zip<./ARTEFACTS-altair.zip>` |
| 25 | +to run our samples locally |
| 26 | +``` |
| 27 | + |
| 28 | +this notebook shows a very basic dashboard made in altair / vega-lite from dynamic data (fetched at a web service) |
| 29 | +what we will demonstrate is |
| 30 | + |
| 31 | +- how to write a simple web server that generates random data - as an anticipation of the S2 courses |
| 32 | +- how to create an altair chart from that data, as an example of using altair's "graphic grammar" approach |
| 33 | +- how to transform the result into a standalone web app |
| 34 | + |
| 35 | + |
| 36 | +one of the pros of using altair is the ability to produce an interactive visualisation as a self-contained HTML file |
| 37 | + |
| 38 | ++++ |
| 39 | + |
| 40 | +## dependencies |
| 41 | + |
| 42 | +you may need to |
| 43 | + |
| 44 | +```bash |
| 45 | +pip install "fastapi[standard]" altair |
| 46 | +``` |
| 47 | + |
| 48 | ++++ |
| 49 | + |
| 50 | +## imports |
| 51 | + |
| 52 | +```{code-cell} ipython3 |
| 53 | +import pandas as pd |
| 54 | +
|
| 55 | +# here is how to import altair |
| 56 | +import altair as alt |
| 57 | +``` |
| 58 | + |
| 59 | +## the fastapi web service |
| 60 | + |
| 61 | +we provide you with the source code for a small web service that is able to produce random data for the leases, in a format compatible with the one in `leases.csv` |
| 62 | +this is the purpose of `fastapi_random_yields.py` |
| 63 | + |
| 64 | ++++ |
| 65 | + |
| 66 | +### how to start |
| 67 | + |
| 68 | +assuming you have installed fastapi as above, you can start the web service by typing in your terminal |
| 69 | + |
| 70 | +```bash |
| 71 | +fastapi dev fastapi_random_yields.py |
| 72 | +``` |
| 73 | + |
| 74 | +this will behave a bit like `jupyter lab`, in that it will |
| 75 | + |
| 76 | +- start a web server |
| 77 | +- display the URL to use to join it |
| 78 | +- and it will *block*, meaning the terminal is busy, and no longer responding to your commands |
| 79 | + |
| 80 | +so first off, leave this terminal running, and create another one if needed |
| 81 | + |
| 82 | ++++ |
| 83 | + |
| 84 | +### how to get the docs |
| 85 | + |
| 86 | +inside the terminal where you triggered the server, you'll see a URL |
| 87 | +something like <http://localhost:8000/docs/> |
| 88 | +just open a web browser and cut-n-paste that URL |
| 89 | + |
| 90 | +```{admonition} link maybe clickable ? |
| 91 | +:class: dropdown tip |
| 92 | +
|
| 93 | +in some terminal setups, you may be able to e.g. Command-click or Alt-click on the link in the terminal to open it in your browser (this is rather likely on linux and MacOS, not sure wbout Windows) |
| 94 | +``` |
| 95 | + |
| 96 | +as you might have guessed now, this means |
| 97 | + |
| 98 | +- use the http protocol |
| 99 | +- to reach a service running on the computer named `localhost` (your own laptop, that is) |
| 100 | +- on port 8000 |
| 101 | +- and on the `/docs/` route |
| 102 | + |
| 103 | +```{admonition} what are ports ? |
| 104 | +ports is a very simple mechanism to allow your computer to run many different services (in real life, you would typically have ssh on port 22, web on port 443, dns on port 53, and so on..) |
| 105 | +``` |
| 106 | + |
| 107 | +the `/docs/` route is a predefined route, auto-generated by FastAPI, that gives us.. the doc of how to use this web service |
| 108 | + |
| 109 | ++++ |
| 110 | + |
| 111 | +### how to use |
| 112 | + |
| 113 | +and so, from this doc, you can see that you can send requests with this pattern |
| 114 | + |
| 115 | +``` |
| 116 | +/api/yields/1931/1933 |
| 117 | +``` |
| 118 | + |
| 119 | +so, try it out from your browser and enter this URL: <http://localhost:8000/api/yields/1931/1933> |
| 120 | + |
| 121 | +```{admonition} also from the terminal |
| 122 | +:class:dropdown |
| 123 | +
|
| 124 | +you can achieve the same result from the terminal if you prefer |
| 125 | +first you need to |
| 126 | +`pip install httpie` |
| 127 | +and then you just do |
| 128 | +`http :8000/api/yields/1931/1933` |
| 129 | +
|
| 130 | +then [take a look at `jq`](https://jqlang.org/manual/) that can filter this JSON stream for you |
| 131 | +``` |
| 132 | + |
| 133 | ++++ |
| 134 | + |
| 135 | +## the API outcome |
| 136 | + |
| 137 | +either way - from the browser or from the terminal - you should see a JSON stream with the random data generated |
| 138 | +it's actually a list of dicts, and its structure mimicks this example taken from altair's documentation |
| 139 | + |
| 140 | +```{code-cell} ipython3 |
| 141 | +import altair as alt |
| 142 | +from vega_datasets import data |
| 143 | +
|
| 144 | +source = data.barley() |
| 145 | +source.head(10) |
| 146 | +``` |
| 147 | + |
| 148 | +so our API call above would issue a variation around this predefined data, over 3 years 1931 .. 1933 |
| 149 | +and there's one generated entry for each combination of site x variety x year |
| 150 | + |
| 151 | +```{admonition} exercice |
| 152 | +how many different sites and varieties are there in the source ? |
| 153 | +``` |
| 154 | + |
| 155 | ++++ |
| 156 | + |
| 157 | +## altair visualisations |
| 158 | + |
| 159 | +altair offers a "grammar-oriented" visualisation paradigm where the visualisation is defined in a **declarative** way |
| 160 | + |
| 161 | ++++ |
| 162 | + |
| 163 | +### a stacked bar from altair's doc |
| 164 | + |
| 165 | +here's an example taken from the altair documentation |
| 166 | + |
| 167 | +```{code-cell} ipython3 |
| 168 | +# stolen from https://altair-viz.github.io/gallery/stacked_bar_chart.html |
| 169 | +
|
| 170 | +( |
| 171 | + alt.Chart(source) |
| 172 | + .mark_bar() |
| 173 | + .encode( |
| 174 | + x='variety', |
| 175 | + y='sum(yield)', |
| 176 | + color='site' |
| 177 | + ) |
| 178 | +) |
| 179 | +``` |
| 180 | + |
| 181 | +### **exo**: inspect the data |
| 182 | + |
| 183 | +take some time to get a glimpse at what the data looks like... |
| 184 | + |
| 185 | +```{code-cell} ipython3 |
| 186 | +import itables |
| 187 | +itables.init_notebook_mode() |
| 188 | +
|
| 189 | +source |
| 190 | +``` |
| 191 | + |
| 192 | +```{code-cell} ipython3 |
| 193 | +# your code here |
| 194 | +# feel free to create extra cells if needed |
| 195 | +``` |
| 196 | + |
| 197 | +### **exo**: write your own pivot |
| 198 | + |
| 199 | +you should be able to see a resemblance with some sort of *pivot table* here |
| 200 | +would you be able to compute a pivot table that resonates with this visualisation ? |
| 201 | + |
| 202 | +````{admonition} solution |
| 203 | +:class: dropdown |
| 204 | +
|
| 205 | +```{code-cell} python |
| 206 | +source.pivot_table( |
| 207 | + values='yield', |
| 208 | + aggfunc="sum", |
| 209 | + index="site", |
| 210 | + columns="variety", |
| 211 | +) |
| 212 | +``` |
| 213 | +```` |
| 214 | + |
| 215 | +```{code-cell} ipython3 |
| 216 | +# your code |
| 217 | +``` |
| 218 | + |
| 219 | +### a few useful additions |
| 220 | + |
| 221 | +here's a few additions to that sample chart, that will make our life easier: |
| 222 | + |
| 223 | +- we set a *width* and *height* |
| 224 | +- as well as a *title* |
| 225 | +- and make it interactive: try to scroll up or down in the figure with 2 fingers |
| 226 | + |
| 227 | +````{admonition} not interactive ? |
| 228 | +:class: dropdown |
| 229 | +
|
| 230 | +Oh but no, the interactive thing is not working for us here; it is because the X axis does not have numeric values ! |
| 231 | +but let's keep this trick in mind for later, it will come in handy at some point |
| 232 | +```` |
| 233 | + |
| 234 | +```{code-cell} ipython3 |
| 235 | +# once and for good |
| 236 | +alt.renderers.enable("mimetype") |
| 237 | +
|
| 238 | +# same beginning |
| 239 | +( |
| 240 | + alt.Chart(source) |
| 241 | + .mark_bar() |
| 242 | + .encode( |
| 243 | + x='variety', |
| 244 | + y='sum(yield)', |
| 245 | + color='site' |
| 246 | + ).properties( |
| 247 | + height=300, |
| 248 | + width=800, |
| 249 | + title=f"Barley yields", |
| 250 | + ) |
| 251 | + .interactive() |
| 252 | +) |
| 253 | +``` |
| 254 | + |
| 255 | +## dynamic data |
| 256 | + |
| 257 | +ofcourse, we can easily switch from the (static) data source to our web service |
| 258 | + |
| 259 | +```{code-cell} ipython3 |
| 260 | +# just replace the source with this |
| 261 | +URL = "http://localhost:8000/api/yields/1931/1933" |
| 262 | +
|
| 263 | +chart = ( |
| 264 | + alt.Chart(URL) |
| 265 | + .mark_bar() |
| 266 | + .encode( |
| 267 | + # here we need to help altair (actually vega-lite) |
| 268 | + # and be explicit on the types of the various fields |
| 269 | + x='variety:N', |
| 270 | + y='sum(yield):Q', |
| 271 | + color='site:N' |
| 272 | + ).properties( |
| 273 | + height=300, |
| 274 | + width=800, |
| 275 | + title=f"Barley yields", |
| 276 | + ) |
| 277 | + .interactive() |
| 278 | +) |
| 279 | +``` |
| 280 | + |
| 281 | +## save as an HTML standalone app |
| 282 | + |
| 283 | +```{code-cell} ipython3 |
| 284 | +import altair as alt |
| 285 | +
|
| 286 | +# Convert to HTML, adding a custom button and reload script |
| 287 | +html = f""" |
| 288 | +<!DOCTYPE html> |
| 289 | +<html> |
| 290 | +<head> |
| 291 | + <meta charset="utf-8"> |
| 292 | + <title>Altair Chart with Refresh</title> |
| 293 | + <script src="https://cdn.jsdelivr.net/npm/vega@5"></script> |
| 294 | + <script src="https://cdn.jsdelivr.net/npm/vega-lite@5"></script> |
| 295 | + <script src="https://cdn.jsdelivr.net/npm/vega-embed@6"></script> |
| 296 | +</head> |
| 297 | +<body style="font-family:sans-serif"> |
| 298 | + <h2>Yield Chart (with Refresh)</h2> |
| 299 | + <button id="refresh">🔄 Refresh</button> |
| 300 | + <div id="visual"></div> |
| 301 | +
|
| 302 | + <script type="text/javascript"> |
| 303 | + const spec = {chart.to_json(indent=None)}; |
| 304 | + const container = document.getElementById('visual'); |
| 305 | +
|
| 306 | + function render() {{ |
| 307 | + vegaEmbed(container, spec, {{ actions: false }}); |
| 308 | + }} |
| 309 | +
|
| 310 | + // Initial render |
| 311 | + render(); |
| 312 | +
|
| 313 | + // Reload button handler |
| 314 | + document.getElementById('refresh').addEventListener('click', () => {{ |
| 315 | + // Force Vega-Lite to reload the URL by tweaking it with a cache-busting parameter |
| 316 | + const url = new URL(spec.data.url); |
| 317 | + url.searchParams.set('_t', Date.now()); |
| 318 | + spec.data.url = url.toString(); |
| 319 | + render(); |
| 320 | + }}); |
| 321 | + </script> |
| 322 | +</body> |
| 323 | +</html> |
| 324 | +""" |
| 325 | +
|
| 326 | +# Save to a standalone file |
| 327 | +with open("yield-chart.html", "w", encoding="utf-8") as f: |
| 328 | + f.write(html) |
| 329 | +
|
| 330 | +print("✅ Saved to yield-chart.html") |
| 331 | +``` |
| 332 | + |
| 333 | +## try it out |
| 334 | + |
| 335 | +the previous cell should have created a local file named `yield-chart.html` |
| 336 | +the simplest way to try it is to open it in your browser (e.g. double-click it from your file explorer) |
| 337 | + |
| 338 | +of course this can also be inserted into another app, etc... |
| 339 | + |
| 340 | ++++ |
| 341 | + |
| 342 | +## conclusion |
| 343 | + |
| 344 | +with altair: |
| 345 | +- the way the diagram is built, using this so-called *graphic grammar*, reminds a bit of the way seaborn lets us use several dimensions to outline various aspects of the data |
| 346 | +- you have a rather easy way to write nice web apps with mostly Python, and a limited knowledge of the web technos like HTML and the like |
| 347 | +- you can easily use dynamic data sources |
| 348 | +- it can come in handy sometimes to come up with great demos in a reasonable time |
0 commit comments