|
27 | 27 | "## Special events"
|
28 | 28 | ]
|
29 | 29 | },
|
30 |
| - { |
31 |
| - "cell_type": "code", |
32 |
| - "execution_count": null, |
33 |
| - "metadata": {}, |
34 |
| - "outputs": [], |
35 |
| - "source": [ |
36 |
| - "from __future__ import print_function" |
37 |
| - ] |
38 |
| - }, |
39 | 30 | {
|
40 | 31 | "cell_type": "markdown",
|
41 | 32 | "metadata": {},
|
|
363 | 354 | "Sliders, `Text`, and `Textarea` controls default to `continuous_update=True`. `IntText` and other text boxes for entering integer or float numbers default to `continuous_update=False` (since often you'll want to type an entire number before submitting the value by pressing enter or navigating out of the box)."
|
364 | 355 | ]
|
365 | 356 | },
|
| 357 | + { |
| 358 | + "cell_type": "markdown", |
| 359 | + "metadata": {}, |
| 360 | + "source": [ |
| 361 | + "## Debouncing\n", |
| 362 | + "\n", |
| 363 | + "When trait changes trigger a callback that performs a heavy computation, you may want to not do the computation as often as the value is updated. For instance, if the trait is driven by a slider which has its `continuous_update` set to `True`, the user will trigger a bunch of computations in rapid succession.\n", |
| 364 | + "\n", |
| 365 | + "Debouncing solves this problem by delaying callback execution until the value has not changed for a certain time, after which the callback is called with the latest value. The effect is that the callback is only called when the trait pauses changing for a certain amount of time.\n", |
| 366 | + "\n", |
| 367 | + "Debouncing can be implemented using an asynchronous loop or threads. We show an asynchronous solution below, which is more suited for ipywidgets. If you would like to instead use threads to do the debouncing, replace the `Timer` class with `from threading import Timer`." |
| 368 | + ] |
| 369 | + }, |
| 370 | + { |
| 371 | + "cell_type": "code", |
| 372 | + "execution_count": null, |
| 373 | + "metadata": {}, |
| 374 | + "outputs": [], |
| 375 | + "source": [ |
| 376 | + "import asyncio\n", |
| 377 | + "\n", |
| 378 | + "class Timer:\n", |
| 379 | + " def __init__(self, timeout, callback):\n", |
| 380 | + " self._timeout = timeout\n", |
| 381 | + " self._callback = callback\n", |
| 382 | + " self._task = asyncio.ensure_future(self._job())\n", |
| 383 | + "\n", |
| 384 | + " async def _job(self):\n", |
| 385 | + " await asyncio.sleep(self._timeout)\n", |
| 386 | + " self._callback()\n", |
| 387 | + "\n", |
| 388 | + " def cancel(self):\n", |
| 389 | + " self._task.cancel()\n", |
| 390 | + "\n", |
| 391 | + "def debounce(wait):\n", |
| 392 | + " \"\"\" Decorator that will postpone a function's\n", |
| 393 | + " execution until after `wait` seconds\n", |
| 394 | + " have elapsed since the last time it was invoked. \"\"\"\n", |
| 395 | + " def decorator(fn):\n", |
| 396 | + " timer = None\n", |
| 397 | + " def debounced(*args, **kwargs):\n", |
| 398 | + " nonlocal timer\n", |
| 399 | + " def call_it():\n", |
| 400 | + " fn(*args, **kwargs)\n", |
| 401 | + " if timer is not None:\n", |
| 402 | + " timer.cancel()\n", |
| 403 | + " timer = Timer(wait, call_it)\n", |
| 404 | + " return debounced\n", |
| 405 | + " return decorator" |
| 406 | + ] |
| 407 | + }, |
| 408 | + { |
| 409 | + "cell_type": "markdown", |
| 410 | + "metadata": {}, |
| 411 | + "source": [ |
| 412 | + "Here is how we use the `debounce` function as a decorator. Try changing the value of the slider. The text box will only update after the slider has paused for about 0.2 seconds." |
| 413 | + ] |
| 414 | + }, |
| 415 | + { |
| 416 | + "cell_type": "code", |
| 417 | + "execution_count": null, |
| 418 | + "metadata": {}, |
| 419 | + "outputs": [], |
| 420 | + "source": [ |
| 421 | + "slider = widgets.IntSlider()\n", |
| 422 | + "text = widgets.IntText()\n", |
| 423 | + "\n", |
| 424 | + "@debounce(0.2)\n", |
| 425 | + "def value_changed(change):\n", |
| 426 | + " text.value = change.new\n", |
| 427 | + "slider.observe(value_changed, 'value')\n", |
| 428 | + "\n", |
| 429 | + "widgets.VBox([slider, text])" |
| 430 | + ] |
| 431 | + }, |
| 432 | + { |
| 433 | + "cell_type": "markdown", |
| 434 | + "metadata": {}, |
| 435 | + "source": [ |
| 436 | + "## Throttling\n", |
| 437 | + "\n", |
| 438 | + "Throttling is another technique that can be used to limit callbacks. Whereas debouncing ignores calls to a function if a certain amount of time has not passed since the last (attempt of) call to the function, throttling will just limit the rate of calls. This ensures that the function is regularly called.\n", |
| 439 | + "\n", |
| 440 | + "We show an synchronous solution below. Likewise, you can replace the `Timer` class with `from threading import Timer` if you want to use threads instead of asynchronous programming." |
| 441 | + ] |
| 442 | + }, |
| 443 | + { |
| 444 | + "cell_type": "code", |
| 445 | + "execution_count": null, |
| 446 | + "metadata": {}, |
| 447 | + "outputs": [], |
| 448 | + "source": [ |
| 449 | + "import asyncio\n", |
| 450 | + "from time import time\n", |
| 451 | + "\n", |
| 452 | + "def throttle(wait):\n", |
| 453 | + " \"\"\" Decorator that prevents a function from being called\n", |
| 454 | + " more than once every wait period. \"\"\"\n", |
| 455 | + " def decorator(fn):\n", |
| 456 | + " time_of_last_call = 0\n", |
| 457 | + " scheduled = False\n", |
| 458 | + " new_args, new_kwargs = None, None\n", |
| 459 | + " def throttled(*args, **kwargs):\n", |
| 460 | + " nonlocal new_args, new_kwargs, time_of_last_call, scheduled\n", |
| 461 | + " def call_it():\n", |
| 462 | + " nonlocal new_args, new_kwargs, time_of_last_call, scheduled\n", |
| 463 | + " time_of_last_call = time()\n", |
| 464 | + " fn(*new_args, **new_kwargs)\n", |
| 465 | + " scheduled = False\n", |
| 466 | + " time_since_last_call = time() - time_of_last_call\n", |
| 467 | + " new_args = args\n", |
| 468 | + " new_kwargs = kwargs\n", |
| 469 | + " if not scheduled:\n", |
| 470 | + " new_wait = max(0, wait - time_since_last_call)\n", |
| 471 | + " Timer(new_wait, call_it)\n", |
| 472 | + " scheduled = True\n", |
| 473 | + " return throttled\n", |
| 474 | + " return decorator" |
| 475 | + ] |
| 476 | + }, |
| 477 | + { |
| 478 | + "cell_type": "markdown", |
| 479 | + "metadata": {}, |
| 480 | + "source": [ |
| 481 | + "To see how different it behaves compared to the debouncer, here is the same slider example with its throttled value displayed in the text box. Notice how much more interactive it is, while still limiting the callback rate." |
| 482 | + ] |
| 483 | + }, |
| 484 | + { |
| 485 | + "cell_type": "code", |
| 486 | + "execution_count": null, |
| 487 | + "metadata": {}, |
| 488 | + "outputs": [], |
| 489 | + "source": [ |
| 490 | + "slider = widgets.IntSlider()\n", |
| 491 | + "text = widgets.IntText()\n", |
| 492 | + "\n", |
| 493 | + "@throttle(0.2)\n", |
| 494 | + "def value_changed(change):\n", |
| 495 | + " text.value = change.new\n", |
| 496 | + "slider.observe(value_changed, 'value')\n", |
| 497 | + "\n", |
| 498 | + "widgets.VBox([slider, text])" |
| 499 | + ] |
| 500 | + }, |
366 | 501 | {
|
367 | 502 | "cell_type": "markdown",
|
368 | 503 | "metadata": {
|
|
395 | 530 | "name": "python",
|
396 | 531 | "nbconvert_exporter": "python",
|
397 | 532 | "pygments_lexer": "ipython3",
|
398 |
| - "version": "3.6.4" |
| 533 | + "version": "3.7.3" |
399 | 534 | }
|
400 | 535 | },
|
401 | 536 | "nbformat": 4,
|
|
0 commit comments