|
9 | 9 | "\n",
|
10 | 10 | "## TODO LIST\n",
|
11 | 11 | "- [ ] Add a better pandas dataframe display using `ipydatagrid`\n",
|
12 |
| - "- [ ] Need to fix `%%exception` magic as it is not working.\n", |
13 |
| - "- [ ] Get to the point where a first version of the dashboard package is created, but note the complexity in the handling of all the settings since we require observer functions to handle all the individual settings (since they are separate widgets). Do we mention the need to code up validation of the settings (e.g. - We need to ensure the degree of the fit was reasonable given the number of data points, etc.)?\n", |
14 |
| - "- [x] Revise all references to answers to the new cell numbers in the final version of this notebook.\n" |
| 12 | + "- [x] Need to fix `%%exception` magic as it is not working.\n", |
| 13 | + "- [x] Revise all references to answers to the new cell numbers in the final version of this notebook.\n", |
| 14 | + "- [ ] Remove all the exported answers from the notebook\n", |
| 15 | + "- [ ] Once main has been rebuilt, uncomment the code to display it.\n", |
| 16 | + "- [ ] Get to the point where a first version of the dashboard package is created, but note the complexity in the handling of all the settings since we require observer functions to handle all the individual settings (since they are separate widgets). Do we mention the need to code up validation of the settings (e.g. - We need to ensure the degree of the fit was reasonable given the number of data points, etc.)?\n" |
15 | 17 | ]
|
16 | 18 | },
|
17 | 19 | {
|
|
56 | 58 | "id": "8bbdd068",
|
57 | 59 | "metadata": {},
|
58 | 60 | "source": [
|
59 |
| - "We will be developing the `widgets.py` module in this notebook, so we start by declaring that module to be our intended export target in `nbdev`." |
| 61 | + "## Building the Widgets Module of our Package\n", |
| 62 | + "\n", |
| 63 | + "We will be developing the `widgets.py` module in this notebook, so we start by declaring that module to be our intended export target in `nbdev`. Notice that as we work through this notebook, we will export only those cells necessary to create the `widgets.py` module, leaving other cells which provide examples our testing or development process intact." |
60 | 64 | ]
|
61 | 65 | },
|
62 | 66 | {
|
|
84 | 88 | "metadata": {},
|
85 | 89 | "outputs": [],
|
86 | 90 | "source": [
|
87 |
| - "from key.dashboard.main import main_widget\n", |
| 91 | + "# from key.dashboard.main import main_widget\n", |
88 | 92 | "\n",
|
89 |
| - "main_widget" |
| 93 | + "# main_widget" |
90 | 94 | ]
|
91 | 95 | },
|
92 | 96 | {
|
|
109 | 113 | "outputs": [],
|
110 | 114 | "source": [
|
111 | 115 | "#| export\n",
|
112 |
| - "# %answer key/dashboard/widgets.py 6\n", |
| 116 | + "\n", |
| 117 | + "# %answer key/dashboard/widgets.py 8\n", |
113 | 118 | "\n",
|
114 | 119 | "import pandas as pd\n",
|
115 | 120 | "import os\n",
|
116 | 121 | "from matplotlib import pyplot as plt\n",
|
117 | 122 | "from scipy.signal import savgol_filter\n",
|
118 |
| - "# add import statement for Jupyter widgets\n", |
119 |
| - "import ipywidgets as widgets" |
| 123 | + "import ipywidgets as widgets # add import statement for Jupyter widgets" |
120 | 124 | ]
|
121 | 125 | },
|
122 | 126 | {
|
|
131 | 135 | "\n",
|
132 | 136 | "Execute the next cell create a browser of the available elements. To see some example code for implementing any of the elements click on its title. It will be easier to view both this overview and [the code examples](reference/complete-ipywidgets-widget-list.ipynb) if you have them open in separate tabs.\n",
|
133 | 137 | "\n",
|
134 |
| - "Additional Resource: [Jupyter widgets documentation site](https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20List.html)." |
| 138 | + "Additional Resources:\n", |
| 139 | + "- [nicole-brewer/awesome-jupyter-widgets](https://github.com/nicole-brewer/awesome-jupyter-widgets) lists many packages taking advantage of Jupyter widgets\n", |
| 140 | + "- [Jupyter widgets documentation site](https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20List.html)." |
135 | 141 | ]
|
136 | 142 | },
|
137 | 143 | {
|
|
275 | 281 | "id": "a838e4c9-0ab3-4bd5-abe7-02215b6a4488",
|
276 | 282 | "metadata": {},
|
277 | 283 | "source": [
|
| 284 | + "Now go up and look at the slider widget...\n", |
| 285 | + "\n", |
278 | 286 | "Okay that looks a lot better, but how to we actually get the value of the range from the widget? Generally, most widgets have a `value` attribute, but since there are actually two values in a range, I'm not sure what to expect. Let's see what happens when we try and poll the value attribute. What data type is it?"
|
279 | 287 | ]
|
280 | 288 | },
|
|
333 | 341 | "#| export\n",
|
334 | 342 | "# %answer key/dashboard/widgets.py 27\n",
|
335 | 343 | "\n",
|
336 |
| - "selected_df = original_df[(original_df['Year'] >= year_range.value[0]) & (original_df['Year'] <= year_range.value[1])] # selected_df = original_df[(original_df['Year'] >= from_year) & (original_df['Year'] <= to_year)]\n" |
| 344 | + "selected_df = original_df[(original_df['Year'] >= year_range.value[0]) & (original_df['Year'] <= year_range.value[1])] # selected_df = original_df[(original_df['Year'] >= from_year) & (original_df['Year'] <= to_year)]" |
337 | 345 | ]
|
338 | 346 | },
|
339 | 347 | {
|
|
367 | 375 | "\n",
|
368 | 376 | "### Observing trait changes\n",
|
369 | 377 | "\n",
|
370 |
| - "Widgets are built on the `traitlets` library, which we will learn more about later on. Attributes of objects that inherit the `HasTraits` class, such as widgets do, are called **traits**. Any trait can be \"observed\" for changes. This means we can observe the \"value\" trait of the slider for changes." |
| 378 | + "Widgets are built on the `traitlets` library, which we will learn more about later on. Attributes of objects that inherit the `HasTraits` class, such as widgets do, are called **traits**. Jupyter widgets have an `observe` method that lets you register a handler function that is called when a trait changes." |
371 | 379 | ]
|
372 | 380 | },
|
373 | 381 | {
|
|
389 | 397 | "metadata": {},
|
390 | 398 | "outputs": [],
|
391 | 399 | "source": [
|
392 |
| - "# Attach the above function to the 'value' attribute of the widget so that\n", |
393 |
| - "# it gets called whenever a trait of the widget changes.\n", |
394 |
| - "year_range.observe(on_range_change, 'value')" |
| 400 | + "# Attach the above 'handler' function to the widget so that it gets called whenever a\n", |
| 401 | + "# trait of the widget changes.\n", |
| 402 | + "year_range.observe(on_range_change)" |
395 | 403 | ]
|
396 | 404 | },
|
397 | 405 | {
|
398 | 406 | "cell_type": "markdown",
|
399 | 407 | "id": "82011a05-8e12-4150-a0f0-fa90fe1fd7bd",
|
400 | 408 | "metadata": {},
|
401 | 409 | "source": [
|
402 |
| - "So if we change the value of the slider, our observe function will print the `change`. But what type of data is `change`? Let's find out by changing the value of the slider programmatically." |
| 410 | + "So if we change the value of the slider, our handler function (`on_range_change`) will print the variable `change`. Apparently the `observe` method of a widget passes some variable to the handler function (the variable we are assigning the name `change`).\n", |
| 411 | + "\n", |
| 412 | + "But what is contained in `change`? Let's find out by changing the value of the slider programmatically and thus triggering the handler function.." |
403 | 413 | ]
|
404 | 414 | },
|
405 | 415 | {
|
|
409 | 419 | "metadata": {},
|
410 | 420 | "outputs": [],
|
411 | 421 | "source": [
|
412 |
| - "year_range.value = (1880, 2000)" |
| 422 | + "year_range.value = (1880, 2002)" |
413 | 423 | ]
|
414 | 424 | },
|
415 | 425 | {
|
416 | 426 | "cell_type": "markdown",
|
417 | 427 | "id": "a4ee768e-6d90-4444-a325-1f4276fda768",
|
418 | 428 | "metadata": {},
|
419 | 429 | "source": [
|
420 |
| - "So there are a lot of things going on here. Now we know that `change` is a dictionary. There are a lot of key/value pairs here, but the most important one's for us right now are the \"old\" and \"new\" keys. We can use them to see what the widget's traits used to be, and what they were updated to." |
| 430 | + "It's clear that `change` is a dictionary. There are a lot of key/value pairs here, but the most important ones for us right now are the `old` and `new` keys. We can use their associated values to see what the widget's traits used to be, and what they were updated to." |
421 | 431 | ]
|
422 | 432 | },
|
423 | 433 | {
|
|
442 | 452 | "# years selected by the user using the year_range widget\n",
|
443 | 453 | "def on_range_change(change):\n",
|
444 | 454 | " global selected_df\n",
|
445 |
| - " selected_df = original_df[(original_df['Year'] >= year_range.value[0]) & (original_df['Year'] <= year_range.value[1])] \n", |
446 |
| - "\n", |
447 |
| - "# Attach the above function to the 'value' attribute of the widget so that\n", |
448 |
| - "# it gets called whenever a trait of the widget changes.\n", |
449 |
| - "year_range.observe(on_range_change, 'value')" |
| 455 | + " selected_df = original_df[(original_df['Year'] >= change['new'][0])\n", |
| 456 | + " & (original_df['Year'] <= change['new'][1])]\n", |
| 457 | + " # NOTE: You could also use:\n", |
| 458 | + " # selected_df = original_df[(original_df['Year'] >= year_range.value[0])\n", |
| 459 | + " # & (original_df['Year'] <= year_range.value[1])]\n", |
| 460 | + " # but it is better to use the 'change' object passed to the function because it is\n", |
| 461 | + " # not dependent on the name of the widget." |
450 | 462 | ]
|
451 | 463 | },
|
452 | 464 | {
|
|
456 | 468 | "metadata": {},
|
457 | 469 | "outputs": [],
|
458 | 470 | "source": [
|
459 |
| - "year_range.value = (1990, 1993)" |
| 471 | + "# The second (optional) argument to the observe method is the name of the trait\n", |
| 472 | + "# to watch for changes. The default is 'All' which watches all traits.\n", |
| 473 | + "# This particular widget only has a value trait, so asking to observe the 'value'\n", |
| 474 | + "# trait is redundant... however, it is good practice to be explicit.\n", |
| 475 | + "year_range.observe(on_range_change, 'value')" |
460 | 476 | ]
|
461 | 477 | },
|
462 | 478 | {
|
463 | 479 | "cell_type": "code",
|
464 | 480 | "execution_count": null,
|
465 |
| - "id": "bcf1ccfe-704d-44fc-915d-837214bb7053", |
| 481 | + "id": "4f6be451", |
466 | 482 | "metadata": {},
|
467 | 483 | "outputs": [],
|
468 | 484 | "source": [
|
| 485 | + "# Programmatically set the value of the widget\n", |
| 486 | + "year_range.value = (1990, 1993)\n", |
| 487 | + "\n", |
| 488 | + "# Confirm the selected dataframe changed\n", |
469 | 489 | "selected_df"
|
470 | 490 | ]
|
471 | 491 | },
|
|
476 | 496 | "source": [
|
477 | 497 | "That worked really well! But we are still printing the value of `change`. I thought we redefined the function!\n",
|
478 | 498 | "\n",
|
479 |
| - "As it turns out, both versions of the function have copies that are called when there is a change." |
| 499 | + "As it turns out, both versions of the function have copies that are called when there is a change in `year_range`. This is an illustration that we can attach MULTIPLE handler functions to a single widget. That said, it is a bit confusing to have two functions with the same name attached to this widget.\n", |
| 500 | + "\n", |
| 501 | + "**Note**: This happened because functions are treated by Python as immutable objects, so when we redefined the function, we were actually creating a new function object and assigning it to the same name. The old function object was still attached to the widget." |
480 | 502 | ]
|
481 | 503 | },
|
482 | 504 | {
|
|
508 | 530 | "source": [
|
509 | 531 | "%%exception\n",
|
510 | 532 | "\n",
|
511 |
| - "year_range.observe(on_range_change)\n", |
512 |
| - "year_range.value = (1993, 1996)" |
| 533 | + "year_range.observe()\n", |
| 534 | + "year_range.value = (1987, 1991)" |
513 | 535 | ]
|
514 | 536 | },
|
515 | 537 | {
|
|
540 | 562 | "#| export\n",
|
541 | 563 | "# %answer key/dashboard/widgets.py 46\n",
|
542 | 564 | "\n",
|
543 |
| - "year_range.observe(on_range_change, 'value')" |
| 565 | + "year_range.observe(on_range_change, 'value') # year_range.observe()" |
544 | 566 | ]
|
545 | 567 | },
|
546 | 568 | {
|
|
631 | 653 | "metadata": {},
|
632 | 654 | "outputs": [],
|
633 | 655 | "source": [
|
634 |
| - "#| export\n", |
635 |
| - "\n", |
636 |
| - "# Create a function that will display the selected dataframe\n", |
| 656 | + "# Create a handler function that will display the selected dataframe\n", |
637 | 657 | "def display_selected_dataframe(change):\n",
|
638 | 658 | " selected_dataframe_output.clear_output(wait=True)\n",
|
639 | 659 | " with selected_dataframe_output:\n",
|
640 | 660 | " display(selected_df)\n",
|
641 | 661 | "\n",
|
642 |
| - "# Start by displaying nothing\n", |
| 662 | + "# Call the function (since called outside observe method, use None as argument)\n", |
643 | 663 | "display_selected_dataframe(None)"
|
644 | 664 | ]
|
645 | 665 | },
|
|
808 | 828 | "def on_window_size_change(change):\n",
|
809 | 829 | " # Remember to access the global variables\n",
|
810 | 830 | " # original_df['Savitzky-Golay'] = savgol_filter(original_df['Temperature'], window_size, poly_order)\n",
|
811 |
| - " selected_df = original_df[(original_df['Year'] >= year_range.value[0]) & (original_df['Year'] <= year_range.value[1])]" |
| 831 | + " selected_df = original_df[(original_df['Year'] >= change['new'][0])\n", |
| 832 | + " & (original_df['Year'] <= change['new'][1])]" |
812 | 833 | ]
|
813 | 834 | },
|
814 | 835 | {
|
|
860 | 881 | "\n",
|
861 | 882 | "def on_poly_order_change(change):\n",
|
862 | 883 | " global original_df, selected_df\n",
|
863 |
| - " # original_df['Savitzky-Golay'] = savgol_filter(original_df['Temperature'], window_size, poly_order)\n", |
864 |
| - " selected_df = original_df[(original_df['Year'] >= year_range.value[0]) & (original_df['Year'] <= year_range.value[1])]\n" |
| 884 | + " original_df['Savitzky-Golay'] = savgol_filter(original_df['Temperature'], window_size.value, change['new']) # original_df['Savitzky-Golay'] = savgol_filter(original_df['Temperature'], window_size, poly_order)\n", |
| 885 | + " selected_df = original_df[(original_df['Year'] >= year_range.value[0]) & (original_df['Year'] <= year_range.value[1])]" |
865 | 886 | ]
|
866 | 887 | },
|
867 | 888 | {
|
|
0 commit comments