Skip to content

figure_resampler

Module withholding wrappers for the plotly go.Figure and go.FigureWidget class which allows bookkeeping and back-end based resampling of high-frequency sequential data.

Tip

The term high-frequency actually refers very large amounts of sequential data.

FigureResampler

Bases: AbstractFigureAggregator, Figure

Data aggregation functionality for go.Figures.

Source code in plotly_resampler/figure_resampler/figure_resampler.py
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
class FigureResampler(AbstractFigureAggregator, go.Figure):
    """Data aggregation functionality for ``go.Figures``."""

    def __init__(
        self,
        figure: BaseFigure | dict = None,
        convert_existing_traces: bool = True,
        default_n_shown_samples: int = 1000,
        default_downsampler: AbstractAggregator = MinMaxLTTB(),
        default_gap_handler: AbstractGapHandler = MedDiffGapHandler(),
        resampled_trace_prefix_suffix: Tuple[str, str] = (
            '<b style="color:sandybrown">[R]</b> ',
            "",
        ),
        show_mean_aggregation_size: bool = True,
        convert_traces_kwargs: dict | None = None,
        create_overview: bool = False,
        overview_row_idxs: list = None,
        overview_kwargs: dict = {},
        verbose: bool = False,
        show_dash_kwargs: dict | None = None,
    ):
        """Initialize a dynamic aggregation data mirror using a dash web app.

        Parameters
        ----------
        figure: BaseFigure
            The figure that will be decorated. Can be either an empty figure
            (e.g., ``go.Figure()``, ``make_subplots()``, ``go.FigureWidget``) or an
            existing figure.
        convert_existing_traces: bool
            A bool indicating whether the high-frequency traces of the passed ``figure``
            should be resampled, by default True. Hence, when set to False, the
            high-frequency traces of the passed ``figure`` will not be resampled.
        default_n_shown_samples: int, optional
            The default number of samples that will be shown for each trace,
            by default 1000.\n
            !!! note
                - This can be overridden within the [`add_trace`][figure_resampler.figure_resampler_interface.AbstractFigureAggregator.add_trace] method.
                - If a trace withholds fewer datapoints than this parameter,
                  the data will *not* be aggregated.
        default_downsampler: AbstractAggregator, optional
            An instance which implements the AbstractAggregator interface and
            will be used as default downsampler, by default ``MinMaxLTTB`` with
            ``MinMaxLTTB`` is a heuristic to the LTTB algorithm that uses pre-selection
            of min-max values (default 4 per bin) to speed up LTTB (as now only 4 values
            per bin are considered by LTTB). This min-max ratio of 4 can be changed by
            initializing ``MinMaxLTTB`` with a different value for the ``minmax_ratio``
            parameter. \n
            !!! note
                This can be overridden within the [`add_trace`][figure_resampler.figure_resampler_interface.AbstractFigureAggregator.add_trace] method.
        default_gap_handler: AbstractGapHandler, optional
            An instance which implements the AbstractGapHandler interface and
            will be used as default gap handler, by default ``MedDiffGapHandler``.
            ``MedDiffGapHandler`` will determine gaps by first calculating the median
            aggregated x difference and then thresholding the aggregated x delta on a
            multiple of this median difference.  \n
            !!! note
                This can be overridden within the [`add_trace`][figure_resampler.figure_resampler_interface.AbstractFigureAggregator.add_trace] method.
        resampled_trace_prefix_suffix: str, optional
            A tuple which contains the ``prefix`` and ``suffix``, respectively, which
            will be added to the trace its legend-name when a resampled version of the
            trace is shown. By default a bold, orange ``[R]`` is shown as prefix
            (no suffix is shown).
        show_mean_aggregation_size: bool, optional
            Whether the mean aggregation bin size will be added as a suffix to the trace
            its legend-name, by default True.
        convert_traces_kwargs: dict, optional
            A dict of kwargs that will be passed to the [`add_trace`][figure_resampler.figure_resampler_interface.AbstractFigureAggregator.add_trace] method and
            will be used to convert the existing traces. \n
            !!! note
                This argument is only used when the passed ``figure`` contains data and
                ``convert_existing_traces`` is set to True.
        create_overview: bool, optional
            Whether an overview will be added to the figure (also known as rangeslider),
            by default False. An overview is a bidirectionally linked figure that is
            placed below the FigureResampler figure and shows a coarse version on which
            the current view of the FigureResampler figure is highlighted. The overview
            can be used to quickly navigate through the data by dragging the selection
            box.
            !!! note
                - In the case of subplots, the overview will be created for each subplot
                  column. Only a single subplot row can be captured in the overview,
                  this is by default the first row. If you want to customize this
                  behavior, you can use the `overview_row_idxs` argument.
                - This functionality is not yet extensively validated. Please report any
                  issues you encounter on GitHub.
        overview_row_idxs: list, optional
            A list of integers corresponding to the row indices (START AT 0) of the
            subplots columns that should be linked with the column its corresponding
            overview. By default None, which will result in the first row being utilized
            for each column.
        overview_kwargs: dict, optional
            A dict of kwargs that will be passed to the `update_layout` method of the
            overview figure, by default {}, which will result in utilizing the
            [`default`][_DEFAULT_OVERVIEW_LAYOUT_KWARGS] overview layout kwargs.
        verbose: bool, optional
            Whether some verbose messages will be printed or not, by default False.
        show_dash_kwargs: dict, optional
            A dict that will be used as default kwargs for the [`show_dash`][figure_resampler.figure_resampler.FigureResampler.show_dash] method.
            !!! note
                The passed kwargs to the [`show_dash`][figure_resampler.figure_resampler.FigureResampler.show_dash] method will take precedence over these defaults.

        """
        # Parse the figure input before calling `super`
        if is_figure(figure) and not is_fr(figure):
            # A go.Figure
            # => base case: the figure does not need to be adjusted
            f = figure
        else:
            # Create a new figure object and make sure that the trace uid will not get
            # adjusted when they are added.
            f = self._get_figure_class(go.Figure)()
            f._data_validator.set_uid = False

            if isinstance(figure, BaseFigure):
                # A base figure object, can be;
                # - a go.FigureWidget
                # - a plotly-resampler figure: subclass of AbstractFigureAggregator
                # => we first copy the layout, grid_str and grid ref
                f.layout = figure.layout
                f._grid_str = figure._grid_str
                f._grid_ref = figure._grid_ref
                f.add_traces(figure.data)
            elif isinstance(figure, dict) and (
                "data" in figure or "layout" in figure  # or "frames" in figure  # TODO
            ):
                # A figure as a dict, can be;
                # - a plotly figure as a dict (after calling `fig.to_dict()`)
                # - a pickled (plotly-resampler) figure (after loading a pickled figure)
                # => we first copy the layout, grid_str and grid ref
                f.layout = figure.get("layout")
                f._grid_str = figure.get("_grid_str")
                f._grid_ref = figure.get("_grid_ref")
                f.add_traces(figure.get("data"))
                # `pr_props` is not None when loading a pickled plotly-resampler figure
                f._pr_props = figure.get("pr_props")
                # `f._pr_props`` is an attribute to store properties of a
                # plotly-resampler figure. This attribute is only used to pass
                # information to the super() constructor. Once the super constructor is
                # called, the attribute is removed.

                # f.add_frames(figure.get("frames")) TODO
            elif isinstance(figure, (dict, list)):
                # A single trace dict or a list of traces
                f.add_traces(figure)

        self._show_dash_kwargs = (
            show_dash_kwargs if show_dash_kwargs is not None else {}
        )

        super().__init__(
            f,
            convert_existing_traces,
            default_n_shown_samples,
            default_downsampler,
            default_gap_handler,
            resampled_trace_prefix_suffix,
            show_mean_aggregation_size,
            convert_traces_kwargs,
            verbose,
        )

        if isinstance(figure, AbstractFigureAggregator):
            # Copy the `_hf_data` if the previous figure was an AbstractFigureAggregator
            # and adjust the default `max_n_samples` and `downsampler`
            self._hf_data.update(
                self._copy_hf_data(figure._hf_data, adjust_default_values=True)
            )

            # Note: This hack ensures that the this figure object initially uses
            # data of the whole view. More concretely; we create a dict
            # serialization figure and adjust the hf-traces to the whole view
            # with the check-update method (by passing no range / filter args)
            with self.batch_update():
                graph_dict: dict = self._get_current_graph()
                update_indices = self._check_update_figure_dict(graph_dict)
                for idx in update_indices:
                    self.data[idx].update(graph_dict["data"][idx])

        self._create_overview = create_overview
        # update the overview layout
        overview_layout_kwargs = _DEFAULT_OVERVIEW_LAYOUT_KWARGS.copy()
        overview_layout_kwargs.update(overview_kwargs)
        self._overview_layout_kwargs = overview_layout_kwargs

        # array representing the row indices per column (START AT 0) of the subplot
        # that should be linked with the columns corresponding overview.
        # By default, the first row (i.e. index 0) will be utilized for each column
        self._overview_row_idxs = self._parse_subplot_row_indices(overview_row_idxs)

        # The FigureResampler needs a dash app
        self._app: dash.Dash | None = None
        self._port: int | None = None
        self._host: str | None = None
        # Certain functions will be different when using persistent inline
        # (namely `show_dash` and `stop_callback`)
        self._is_persistent_inline = False

    def _get_subplot_rows_and_cols_from_grid(self) -> Tuple[int, int]:
        """Get the number of rows and columns of the figure's grid.

        Returns
        -------
        Tuple[int, int]
            The number of rows and columns of the figure's grid, respectively.
        """
        if self._grid_ref is None:  # case: go.Figure (no subplots)
            return (1, 1)
        # TODO: not 100% sure whether this is correct
        return (len(self._grid_ref), len(self._grid_ref[0]))

    def _parse_subplot_row_indices(self, row_indices: list = None) -> List[int]:
        """Verify whether the passed row indices are valid.

        Parameters
        ----------
        row_indices: list, optional
            A list of integers representing the row indices for which the overview
            should be created. The length of the list should be equal to the number of
            columns of the figure. Each element of the list should be smaller than the
            number of rows of the figure (thus note that the row indices start at 0). By
            default None, which will result in the first row being utilized for each
            column.
            !!! note
                When you do not want to use an overview of a certain column (because
                a certain subplot spans more than 1 column), you can specify this by
                setting that respecive row_index value to `None`.

                For instance, the sbuplot on row 2, col 1 spans two coloms. So when you
                intend to utilize that subplot within the overview, you want to specify
                the row_indices as: `[1, None, ...]`

        Returns
        -------
        List[int]
            A list of integers representing the row indices per subplot column.

        """
        n_rows, n_cols = self._get_subplot_rows_and_cols_from_grid()

        # By default, the first row is utilized to set the row indices
        if row_indices is None:
            return [0] * n_cols

        # perform some checks on the row indices
        assert isinstance(row_indices, list), "row indices must be a list"
        assert (
            len(row_indices) == n_cols
        ), "the number of row indices must be equal to the number of columns"
        assert all(
            [(li is None) or (0 <= li < n_rows) for li in row_indices]
        ), "row indices must be smaller than the number of rows"

        return row_indices

    # determines which subplot data to take from main and put into coarse
    def _remove_other_axes_for_coarse(self) -> go.Figure:
        # base case: no rows and cols to filter
        if self._grid_ref is None:  # case: go.Figure (no subplots)
            return self

        # Create the grid specification for the overview figure (in `reduced_grid_ref`)
        # The trace_list and the 2 axis lists are 1D arrays holding track of the traces
        # and axes to track.
        reduced_grid_ref = [[]]

        # Store the xaxis keys (e.g., x2) of the traces to keep
        trace_list = []
        # Store the xaxis and yaxis layout keys of the traces to keep (e.g., xaxis2)
        layout_xaxis_list, layout_yaxis_list = [], []
        for col_idx, row_idx in enumerate(self._overview_row_idxs):
            if row_idx is None:  # skip None value
                continue

            overview_grid_ref = self._grid_ref[row_idx][col_idx]
            reduced_grid_ref[0].append(overview_grid_ref)  # [0] bc 1 row in overview
            for subplot in overview_grid_ref:
                trace_list.append(subplot.trace_kwargs["xaxis"])

                # store the layout keys so that we can retain the exact layout
                xaxis_key, yaxis_key = subplot.layout_keys
                layout_yaxis_list.append(yaxis_key)
                layout_xaxis_list.append(xaxis_key)
        # print("layout_list", l_xaxis_list, l_yaxis_list)
        # print("trace_list", trace_list)

        fig_dict = self._get_current_graph()  # a copy of the current graph

        # copy the data from the relevant overview subplots
        reduced_fig_dict = {
            "data": [],
            "layout": {"template": fig_dict["layout"]["template"]},
        }
        # NOTE: we enumerate over the data of the full figure so that we can utilize the
        # trace index to mimic the colorway.
        for i, trace in enumerate(fig_dict["data"]):
            # NOTE: the interplay between line_color and marker_color seems to work in
            # this implementation - a more thorough investigation might be needed
            if trace.get("xaxis", "x") in trace_list:
                if "line" not in trace:
                    trace["line"] = {}
                # Ensure that the same color is utilized
                trace["line"]["color"] = (
                    self._layout_obj.template.layout.colorway[i]
                    if self.data[i].line.color is None
                    else self.data[i].line.color
                )
                # add the trace to the reduced figure
                reduced_fig_dict["data"].append(trace)

        # Add the relevant layout keys to the reduced figure
        for k, v in fig_dict["layout"].items():
            if k in layout_xaxis_list:
                reduced_fig_dict["layout"][k] = v
            elif k in layout_yaxis_list:
                v = v.copy()
                # set the domain to [0, 1] to ensure that the overview figure has the
                # global y-axis range
                v.update({"domain": [0, 1]})
                reduced_fig_dict["layout"][k] = v

        # Create a figure object using the reduced figure dict
        reduced_fig = go.Figure(layout=reduced_fig_dict["layout"])
        reduced_fig._grid_ref = reduced_grid_ref
        # Ensure that the trace uid is not adjusted, this must be set prior to adding
        # the trace data. Otherwise, data aggregation will not work.
        reduced_fig._data_validator.set_uid = False
        reduced_fig.add_traces(reduced_fig_dict["data"])
        return reduced_fig

    def _create_overview_figure(self) -> go.Figure:
        # create a new coarse fig
        reduced_fig = self._remove_other_axes_for_coarse()

        # Resample the coarse figure using 3x the default aggregation size to ensure
        # that it contains sufficient details
        coarse_fig_hf = FigureResampler(
            reduced_fig,
            default_n_shown_samples=3 * self._global_n_shown_samples,
        )

        # NOTE: this way we can alter props without altering the original hf data
        # NOTE: this also copies the default aggregation functionality to the coarse figure
        coarse_fig_hf._hf_data = {uid: trc.copy() for uid, trc in self._hf_data.items()}
        for trace in coarse_fig_hf.hf_data:
            trace["max_n_samples"] *= 3

        coarse_fig_dict = coarse_fig_hf._get_current_graph()
        # add the 3x max_n_samples coarse figure data to the coarse_fig_dict
        coarse_fig_hf._check_update_figure_dict(coarse_fig_dict)
        del coarse_fig_hf

        coarse_fig = go.Figure(layout=coarse_fig_dict["layout"])
        coarse_fig._grid_ref = reduced_fig._grid_ref
        coarse_fig._data_validator.set_uid = False
        coarse_fig.add_traces(coarse_fig_dict["data"])
        # remove any update menus for the coarse figure
        coarse_fig.layout.pop("updatemenus", None)
        # remove the `rangeselector` options for all 'axis' keys in the layout of the
        # coarse figure
        for k, v in coarse_fig.layout._props.items():
            if "axis" in k:
                v.pop("rangeselector", None)

        # height of the overview scales with the height of the dynamic view
        coarse_fig.update_layout(
            **self._overview_layout_kwargs,
            hovermode=False,
            clickmode="event+select",
            dragmode="select",
        )
        # Hide the grid
        hide_kwrgs = dict(
            showgrid=False,
            showticklabels=False,
            zeroline=False,
            title_text=None,
            mirror=True,
            ticks="",
            showline=False,
            linecolor="black",
        )
        coarse_fig.update_yaxes(**hide_kwrgs)
        coarse_fig.update_xaxes(**hide_kwrgs)

        vrect_props = dict(
            **dict(line_width=0, x0=0, x1=1),
            **dict(fillcolor="lightblue", opacity=0.25, layer="above"),
        )

        if self._grid_ref is None:  # case: go.Figure (no subplots)
            # set the fixed range to True
            coarse_fig["layout"]["xaxis"]["fixedrange"] = True
            coarse_fig["layout"]["yaxis"]["fixedrange"] = True

            # add a shading to the overview
            coarse_fig.add_vrect(xref="x domain", **vrect_props)
            return coarse_fig

        col_idx_overview = 0
        for col_idx, row_idx in enumerate(self._overview_row_idxs):
            if row_idx is None:  # skip the None value
                continue

            # we will only use the first grid-ref (as we will otherwise have multiple
            # overlapping selection boxes)
            for subplot in self._grid_ref[row_idx][col_idx][:1]:
                xaxis_key, yaxis_key = subplot.layout_keys

                # set the fixed range to True
                coarse_fig["layout"][xaxis_key]["fixedrange"] = True
                coarse_fig["layout"][yaxis_key]["fixedrange"] = True

                # add a shading to the overview
                coarse_fig.add_vrect(
                    col=col_idx_overview + 1,
                    xref=f"{subplot.trace_kwargs['xaxis']} domain",
                    **vrect_props,
                )

            col_idx_overview += 1  # only increase the index when not None

        return coarse_fig

    def show_dash(
        self,
        mode=None,
        config: dict | None = None,
        init_dash_kwargs: dict | None = None,
        graph_properties: dict | None = None,
        **kwargs,
    ):
        """Registers the `update_graph` callback & show the figure in a dash app.

        Parameters
        ----------
        mode: str, optional
            Display mode. One of:\n
              * ``"external"``: The URL of the app will be displayed in the notebook
                output cell. Clicking this URL will open the app in the default
                web browser.
              * ``"inline"``: The app will be displayed inline in the notebook output
                cell in an iframe.
              * ``"inline_persistent"``: The app will be displayed inline in the
                notebook output cell in an iframe, if the app is not reachable a static
                image of the figure is shown. Hence this is a persistent version of the
                ``"inline"`` mode, allowing users to see a static figure in other
                environments, browsers, etc.

                !!! note

                    This mode requires the ``kaleido`` and ``flask_cors`` package.
                    Install them : ``pip install plotly_resampler[inline_persistent]``
                    or ``pip install kaleido flask_cors``.

              * ``"jupyterlab"``: The app will be displayed in a dedicated tab in the
                JupyterLab interface. Requires JupyterLab and the ``jupyterlab-dash``
                extension.
            By default None, which will result in the same behavior as ``"external"``.
        config: dict, optional
            The configuration options for displaying this figure, by default None.
            This ``config`` parameter is the same as the dict that you would pass as
            ``config`` argument to the `show` method.
            See more [https://plotly.com/python/configuration-options/](https://plotly.com/python/configuration-options/)
        init_dash_kwargs: dict, optional
            Keyword arguments for the Dash app constructor.
            !!! note
                This variable is of special interest when working in a jupyterhub +
                kubernetes environment. In this case, user notebook servers are spawned
                as separate pods and user access to those servers are proxied via
                jupyterhub. Dash requires the `requests_pathname_prefix` to be set on
                __init__ - which can be done via this `init_dash_kwargs` argument.
                Note that you should also pass the `jupyter_server_url` to the
                `show_dash` method.
                More details: https://github.com/predict-idlab/plotly-resampler/issues/265
        graph_properties: dict, optional
            Dictionary of (keyword, value) for the properties that should be passed to
            the dcc.Graph, by default None.
            e.g.: `{"style": {"width": "50%"}}`
            Note: "config" is not allowed as key in this dict, as there is a distinct
            ``config`` parameter for this property in this method.
            See more [https://dash.plotly.com/dash-core-components/graph](https://dash.plotly.com/dash-core-components/graph)
        **kwargs: dict
            kwargs for the ``app.run_server()`` method, e.g., port=8037.
            !!! note
                These kwargs take precedence over the ones that are passed to the
                constructor via the ``show_dash_kwargs`` argument.

        """
        available_modes = ["external", "inline", "inline_persistent", "jupyterlab"]
        assert (
            mode is None or mode in available_modes
        ), f"mode must be one of {available_modes}"
        graph_properties = {} if graph_properties is None else graph_properties
        assert "config" not in graph_properties  # There is a param for config
        if self["layout"]["autosize"] is True and self["layout"]["height"] is None:
            graph_properties.setdefault("style", {}).update({"height": "100%"})

        # 0. Check if the traces need to be updated when there is a xrange set
        # This will be the case when the users has set a xrange (via the `update_layout`
        # or `update_xaxes` methods`)
        relayout_dict = {}
        for xaxis_str in self._xaxis_list:
            x_range = self.layout[xaxis_str].range
            if x_range:  # when not None
                relayout_dict[f"{xaxis_str}.range[0]"] = x_range[0]
                relayout_dict[f"{xaxis_str}.range[1]"] = x_range[1]
        if relayout_dict:  # when not empty
            update_data = self._construct_update_data(relayout_dict)

            if not self._is_no_update(update_data):  # when there is an update
                with self.batch_update():
                    # First update the layout (first item of update_data)
                    self.layout.update(self._parse_relayout(update_data[0]))

                    # Then update the data
                    for updated_trace in update_data[1:]:
                        trace_idx = updated_trace.pop("index")
                        self.data[trace_idx].update(updated_trace)

        # 1. Construct the Dash app layout
        init_dash_kwargs = {} if init_dash_kwargs is None else init_dash_kwargs
        if self._create_overview:
            # fmt: off
            # Add the assets folder to the init_dash_kwargs
            init_dash_kwargs["assets_folder"] = os.path.relpath(ASSETS_FOLDER, os.getcwd())
            # Also include the lodash script, as the client-side callbacks uses this
            init_dash_kwargs["external_scripts"] = ["https://cdn.jsdelivr.net/npm/lodash/lodash.min.js" ]
            # fmt: on

        if mode == "inline_persistent":
            mode = "inline"
            if _jupyter_dash_installed:
                # Inline persistent mode: we display a static image of the figure when the
                # app is not reachable
                # Note: this is the "inline" behavior of JupyterDashInlinePersistentOutput
                app = JupyterDashPersistentInlineOutput("local_app", **init_dash_kwargs)
                self._is_persistent_inline = True
            else:
                # If Jupyter Dash is not installed, inline persistent won't work and hence
                # we default to normal inline mode with a normal Dash app
                app = dash.Dash("local_app", **init_dash_kwargs)
                warnings.warn(
                    "'jupyter_dash' is not installed. The persistent inline mode will not work. Defaulting to standard inline mode."
                )
        else:
            # jupyter dash uses a normal Dash app as figure
            app = dash.Dash("local_app", **init_dash_kwargs)

        # fmt: off
        div = dash.html.Div(
            children=[
                dash.dcc.Graph(
                    id="resample-figure", figure=self, config=config, **graph_properties
                )
            ],
            style={
                "display": "flex", "flex-flow": "column",
                "height": "95vh", "width": "100%",
            },
        )
        # fmt: on
        if self._create_overview:
            overview_config = config.copy() if config is not None else {}
            overview_config["displayModeBar"] = False
            coarse_fig = self._create_overview_figure()
            div.children += [
                dash.dcc.Graph(
                    id="overview-figure",
                    figure=coarse_fig,
                    config=overview_config,
                    **graph_properties,
                ),
            ]
        app.layout = div

        self.register_update_graph_callback(
            app,
            "resample-figure",
            "overview-figure" if self._create_overview else None,
        )

        height_param = "height" if self._is_persistent_inline else "jupyter_height"

        # 2. Run the app
        if mode == "inline" and height_param not in kwargs:
            # If app height is not specified -> re-use figure height for inline dash app
            #  Note: default layout height is 450 (whereas default app height is 650)
            #  See: https://plotly.com/python/reference/layout/#layout-height
            fig_height = self.layout.height if self.layout.height is not None else 450
            kwargs[height_param] = fig_height + 18

        # kwargs take precedence over the show_dash_kwargs
        kwargs = {**self._show_dash_kwargs, **kwargs}

        # Store the app information, so it can be killed
        self._app = app
        self._host = kwargs.get("host", "127.0.0.1")
        self._port = kwargs.get("port", "8050")

        # function signature is slightly different for the Dash and JupyterDash implementations
        if self._is_persistent_inline:
            app.run(mode=mode, **kwargs)
        else:
            app.run(jupyter_mode=mode, **kwargs)

    def stop_server(self, warn: bool = True):
        """Stop the running dash-app.

        Parameters
        ----------
        warn: bool
            Whether a warning message will be shown or  not, by default True.

        !!! warning

            This only works if the dash-app was started with [`show_dash`][figure_resampler.figure_resampler.FigureResampler.show_dash].
        """
        if self._app is not None:
            servers_dict = (
                self._app._server_threads
                if self._is_persistent_inline
                else dash.jupyter_dash._servers
            )
            old_server = servers_dict.get((self._host, self._port))
            if old_server:
                if self._is_persistent_inline:
                    old_server.kill()
                    old_server.join()
                else:
                    old_server.shutdown()
            del servers_dict[(self._host, self._port)]
        elif warn:
            warnings.warn(
                "Could not stop the server, either the \n"
                + "\t- 'show-dash' method was not called, or \n"
                + "\t- the dash-server wasn't started with 'show_dash'"
            )

    def register_update_graph_callback(
        self,
        app: dash.Dash,
        graph_id: str,
        coarse_graph_id: Optional[str] = None,
    ):
        """Register the [`construct_update_data_patch`][figure_resampler.figure_resampler_interface.AbstractFigureAggregator.construct_update_data_patch]
        method as callback function to the passed dash-app.

        Parameters
        ----------
        app: Union[dash.Dash, JupyterDash]
            The app in which the callback will be registered.
        graph_id:
            The id of the ``dcc.Graph``-component which withholds the to-be resampled
            Figure.
        coarse_graph_id: str, optional
            The id of the ``dcc.Graph``-component which withholds the coarse overview
            Figure, by default None.

        """
        # As we use the figure again as output, we need to set: allow_duplicate=True

        if coarse_graph_id is not None:
            # update pr graph range with overview selection
            app.clientside_callback(
                dash.ClientsideFunction(
                    namespace="clientside", function_name="coarse_to_main"
                ),
                dash.Output(graph_id, "id", allow_duplicate=True),
                dash.Input(coarse_graph_id, "selectedData"),
                dash.State(graph_id, "id"),
                dash.State(coarse_graph_id, "id"),
                prevent_initial_call=True,
            )

            # update selectbox with clientside callback
            app.clientside_callback(
                dash.ClientsideFunction(
                    namespace="clientside", function_name="main_to_coarse"
                ),
                dash.Output(coarse_graph_id, "id", allow_duplicate=True),
                dash.Input(graph_id, "relayoutData"),
                dash.State(coarse_graph_id, "id"),
                dash.State(graph_id, "id"),
                prevent_initial_call=True,
            )

        app.callback(
            dash.Output(graph_id, "figure", allow_duplicate=True),
            dash.Input(graph_id, "relayoutData"),
            prevent_initial_call=True,
        )(self.construct_update_data_patch)

    def _get_pr_props_keys(self) -> List[str]:
        # Add the additional plotly-resampler properties of this class
        return super()._get_pr_props_keys() + ["_show_dash_kwargs"]

    def _ipython_display_(self):
        # To display the figure inline as a dash app
        self.show_dash(mode="inline")

__init__(figure=None, convert_existing_traces=True, default_n_shown_samples=1000, default_downsampler=MinMaxLTTB(), default_gap_handler=MedDiffGapHandler(), resampled_trace_prefix_suffix=('<b style="color:sandybrown">[R]</b> ', ''), show_mean_aggregation_size=True, convert_traces_kwargs=None, create_overview=False, overview_row_idxs=None, overview_kwargs={}, verbose=False, show_dash_kwargs=None)

Initialize a dynamic aggregation data mirror using a dash web app.

Parameters:

Name Type Description Default
figure BaseFigure | dict

The figure that will be decorated. Can be either an empty figure (e.g., go.Figure(), make_subplots(), go.FigureWidget) or an existing figure.

None
convert_existing_traces bool

A bool indicating whether the high-frequency traces of the passed figure should be resampled, by default True. Hence, when set to False, the high-frequency traces of the passed figure will not be resampled.

True
default_n_shown_samples int

The default number of samples that will be shown for each trace, by default 1000.

Note

  • This can be overridden within the add_trace method.
  • If a trace withholds fewer datapoints than this parameter, the data will not be aggregated.
1000
default_downsampler AbstractAggregator

An instance which implements the AbstractAggregator interface and will be used as default downsampler, by default MinMaxLTTB with MinMaxLTTB is a heuristic to the LTTB algorithm that uses pre-selection of min-max values (default 4 per bin) to speed up LTTB (as now only 4 values per bin are considered by LTTB). This min-max ratio of 4 can be changed by initializing MinMaxLTTB with a different value for the minmax_ratio parameter.

Note

This can be overridden within the add_trace method.

MinMaxLTTB()
default_gap_handler AbstractGapHandler

An instance which implements the AbstractGapHandler interface and will be used as default gap handler, by default MedDiffGapHandler. MedDiffGapHandler will determine gaps by first calculating the median aggregated x difference and then thresholding the aggregated x delta on a multiple of this median difference.

Note

This can be overridden within the add_trace method.

MedDiffGapHandler()
resampled_trace_prefix_suffix Tuple[str, str]

A tuple which contains the prefix and suffix, respectively, which will be added to the trace its legend-name when a resampled version of the trace is shown. By default a bold, orange [R] is shown as prefix (no suffix is shown).

('<b style="color:sandybrown">[R]</b> ', '')
show_mean_aggregation_size bool

Whether the mean aggregation bin size will be added as a suffix to the trace its legend-name, by default True.

True
convert_traces_kwargs dict | None

A dict of kwargs that will be passed to the add_trace method and will be used to convert the existing traces.

Note

This argument is only used when the passed figure contains data and convert_existing_traces is set to True.

None
create_overview bool

Whether an overview will be added to the figure (also known as rangeslider), by default False. An overview is a bidirectionally linked figure that is placed below the FigureResampler figure and shows a coarse version on which the current view of the FigureResampler figure is highlighted. The overview can be used to quickly navigate through the data by dragging the selection box.

Note

  • In the case of subplots, the overview will be created for each subplot column. Only a single subplot row can be captured in the overview, this is by default the first row. If you want to customize this behavior, you can use the overview_row_idxs argument.
  • This functionality is not yet extensively validated. Please report any issues you encounter on GitHub.
False
overview_row_idxs list

A list of integers corresponding to the row indices (START AT 0) of the subplots columns that should be linked with the column its corresponding overview. By default None, which will result in the first row being utilized for each column.

None
overview_kwargs dict

A dict of kwargs that will be passed to the update_layout method of the overview figure, by default {}, which will result in utilizing the [default][_DEFAULT_OVERVIEW_LAYOUT_KWARGS] overview layout kwargs.

{}
verbose bool

Whether some verbose messages will be printed or not, by default False.

False
show_dash_kwargs dict | None

A dict that will be used as default kwargs for the show_dash method.

Note

The passed kwargs to the show_dash method will take precedence over these defaults.

None
Source code in plotly_resampler/figure_resampler/figure_resampler.py
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
def __init__(
    self,
    figure: BaseFigure | dict = None,
    convert_existing_traces: bool = True,
    default_n_shown_samples: int = 1000,
    default_downsampler: AbstractAggregator = MinMaxLTTB(),
    default_gap_handler: AbstractGapHandler = MedDiffGapHandler(),
    resampled_trace_prefix_suffix: Tuple[str, str] = (
        '<b style="color:sandybrown">[R]</b> ',
        "",
    ),
    show_mean_aggregation_size: bool = True,
    convert_traces_kwargs: dict | None = None,
    create_overview: bool = False,
    overview_row_idxs: list = None,
    overview_kwargs: dict = {},
    verbose: bool = False,
    show_dash_kwargs: dict | None = None,
):
    """Initialize a dynamic aggregation data mirror using a dash web app.

    Parameters
    ----------
    figure: BaseFigure
        The figure that will be decorated. Can be either an empty figure
        (e.g., ``go.Figure()``, ``make_subplots()``, ``go.FigureWidget``) or an
        existing figure.
    convert_existing_traces: bool
        A bool indicating whether the high-frequency traces of the passed ``figure``
        should be resampled, by default True. Hence, when set to False, the
        high-frequency traces of the passed ``figure`` will not be resampled.
    default_n_shown_samples: int, optional
        The default number of samples that will be shown for each trace,
        by default 1000.\n
        !!! note
            - This can be overridden within the [`add_trace`][figure_resampler.figure_resampler_interface.AbstractFigureAggregator.add_trace] method.
            - If a trace withholds fewer datapoints than this parameter,
              the data will *not* be aggregated.
    default_downsampler: AbstractAggregator, optional
        An instance which implements the AbstractAggregator interface and
        will be used as default downsampler, by default ``MinMaxLTTB`` with
        ``MinMaxLTTB`` is a heuristic to the LTTB algorithm that uses pre-selection
        of min-max values (default 4 per bin) to speed up LTTB (as now only 4 values
        per bin are considered by LTTB). This min-max ratio of 4 can be changed by
        initializing ``MinMaxLTTB`` with a different value for the ``minmax_ratio``
        parameter. \n
        !!! note
            This can be overridden within the [`add_trace`][figure_resampler.figure_resampler_interface.AbstractFigureAggregator.add_trace] method.
    default_gap_handler: AbstractGapHandler, optional
        An instance which implements the AbstractGapHandler interface and
        will be used as default gap handler, by default ``MedDiffGapHandler``.
        ``MedDiffGapHandler`` will determine gaps by first calculating the median
        aggregated x difference and then thresholding the aggregated x delta on a
        multiple of this median difference.  \n
        !!! note
            This can be overridden within the [`add_trace`][figure_resampler.figure_resampler_interface.AbstractFigureAggregator.add_trace] method.
    resampled_trace_prefix_suffix: str, optional
        A tuple which contains the ``prefix`` and ``suffix``, respectively, which
        will be added to the trace its legend-name when a resampled version of the
        trace is shown. By default a bold, orange ``[R]`` is shown as prefix
        (no suffix is shown).
    show_mean_aggregation_size: bool, optional
        Whether the mean aggregation bin size will be added as a suffix to the trace
        its legend-name, by default True.
    convert_traces_kwargs: dict, optional
        A dict of kwargs that will be passed to the [`add_trace`][figure_resampler.figure_resampler_interface.AbstractFigureAggregator.add_trace] method and
        will be used to convert the existing traces. \n
        !!! note
            This argument is only used when the passed ``figure`` contains data and
            ``convert_existing_traces`` is set to True.
    create_overview: bool, optional
        Whether an overview will be added to the figure (also known as rangeslider),
        by default False. An overview is a bidirectionally linked figure that is
        placed below the FigureResampler figure and shows a coarse version on which
        the current view of the FigureResampler figure is highlighted. The overview
        can be used to quickly navigate through the data by dragging the selection
        box.
        !!! note
            - In the case of subplots, the overview will be created for each subplot
              column. Only a single subplot row can be captured in the overview,
              this is by default the first row. If you want to customize this
              behavior, you can use the `overview_row_idxs` argument.
            - This functionality is not yet extensively validated. Please report any
              issues you encounter on GitHub.
    overview_row_idxs: list, optional
        A list of integers corresponding to the row indices (START AT 0) of the
        subplots columns that should be linked with the column its corresponding
        overview. By default None, which will result in the first row being utilized
        for each column.
    overview_kwargs: dict, optional
        A dict of kwargs that will be passed to the `update_layout` method of the
        overview figure, by default {}, which will result in utilizing the
        [`default`][_DEFAULT_OVERVIEW_LAYOUT_KWARGS] overview layout kwargs.
    verbose: bool, optional
        Whether some verbose messages will be printed or not, by default False.
    show_dash_kwargs: dict, optional
        A dict that will be used as default kwargs for the [`show_dash`][figure_resampler.figure_resampler.FigureResampler.show_dash] method.
        !!! note
            The passed kwargs to the [`show_dash`][figure_resampler.figure_resampler.FigureResampler.show_dash] method will take precedence over these defaults.

    """
    # Parse the figure input before calling `super`
    if is_figure(figure) and not is_fr(figure):
        # A go.Figure
        # => base case: the figure does not need to be adjusted
        f = figure
    else:
        # Create a new figure object and make sure that the trace uid will not get
        # adjusted when they are added.
        f = self._get_figure_class(go.Figure)()
        f._data_validator.set_uid = False

        if isinstance(figure, BaseFigure):
            # A base figure object, can be;
            # - a go.FigureWidget
            # - a plotly-resampler figure: subclass of AbstractFigureAggregator
            # => we first copy the layout, grid_str and grid ref
            f.layout = figure.layout
            f._grid_str = figure._grid_str
            f._grid_ref = figure._grid_ref
            f.add_traces(figure.data)
        elif isinstance(figure, dict) and (
            "data" in figure or "layout" in figure  # or "frames" in figure  # TODO
        ):
            # A figure as a dict, can be;
            # - a plotly figure as a dict (after calling `fig.to_dict()`)
            # - a pickled (plotly-resampler) figure (after loading a pickled figure)
            # => we first copy the layout, grid_str and grid ref
            f.layout = figure.get("layout")
            f._grid_str = figure.get("_grid_str")
            f._grid_ref = figure.get("_grid_ref")
            f.add_traces(figure.get("data"))
            # `pr_props` is not None when loading a pickled plotly-resampler figure
            f._pr_props = figure.get("pr_props")
            # `f._pr_props`` is an attribute to store properties of a
            # plotly-resampler figure. This attribute is only used to pass
            # information to the super() constructor. Once the super constructor is
            # called, the attribute is removed.

            # f.add_frames(figure.get("frames")) TODO
        elif isinstance(figure, (dict, list)):
            # A single trace dict or a list of traces
            f.add_traces(figure)

    self._show_dash_kwargs = (
        show_dash_kwargs if show_dash_kwargs is not None else {}
    )

    super().__init__(
        f,
        convert_existing_traces,
        default_n_shown_samples,
        default_downsampler,
        default_gap_handler,
        resampled_trace_prefix_suffix,
        show_mean_aggregation_size,
        convert_traces_kwargs,
        verbose,
    )

    if isinstance(figure, AbstractFigureAggregator):
        # Copy the `_hf_data` if the previous figure was an AbstractFigureAggregator
        # and adjust the default `max_n_samples` and `downsampler`
        self._hf_data.update(
            self._copy_hf_data(figure._hf_data, adjust_default_values=True)
        )

        # Note: This hack ensures that the this figure object initially uses
        # data of the whole view. More concretely; we create a dict
        # serialization figure and adjust the hf-traces to the whole view
        # with the check-update method (by passing no range / filter args)
        with self.batch_update():
            graph_dict: dict = self._get_current_graph()
            update_indices = self._check_update_figure_dict(graph_dict)
            for idx in update_indices:
                self.data[idx].update(graph_dict["data"][idx])

    self._create_overview = create_overview
    # update the overview layout
    overview_layout_kwargs = _DEFAULT_OVERVIEW_LAYOUT_KWARGS.copy()
    overview_layout_kwargs.update(overview_kwargs)
    self._overview_layout_kwargs = overview_layout_kwargs

    # array representing the row indices per column (START AT 0) of the subplot
    # that should be linked with the columns corresponding overview.
    # By default, the first row (i.e. index 0) will be utilized for each column
    self._overview_row_idxs = self._parse_subplot_row_indices(overview_row_idxs)

    # The FigureResampler needs a dash app
    self._app: dash.Dash | None = None
    self._port: int | None = None
    self._host: str | None = None
    # Certain functions will be different when using persistent inline
    # (namely `show_dash` and `stop_callback`)
    self._is_persistent_inline = False

register_update_graph_callback(app, graph_id, coarse_graph_id=None)

Register the construct_update_data_patch method as callback function to the passed dash-app.

Parameters:

Name Type Description Default
app Dash

The app in which the callback will be registered.

required
graph_id str

The id of the dcc.Graph-component which withholds the to-be resampled Figure.

required
coarse_graph_id Optional[str]

The id of the dcc.Graph-component which withholds the coarse overview Figure, by default None.

None
Source code in plotly_resampler/figure_resampler/figure_resampler.py
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
def register_update_graph_callback(
    self,
    app: dash.Dash,
    graph_id: str,
    coarse_graph_id: Optional[str] = None,
):
    """Register the [`construct_update_data_patch`][figure_resampler.figure_resampler_interface.AbstractFigureAggregator.construct_update_data_patch]
    method as callback function to the passed dash-app.

    Parameters
    ----------
    app: Union[dash.Dash, JupyterDash]
        The app in which the callback will be registered.
    graph_id:
        The id of the ``dcc.Graph``-component which withholds the to-be resampled
        Figure.
    coarse_graph_id: str, optional
        The id of the ``dcc.Graph``-component which withholds the coarse overview
        Figure, by default None.

    """
    # As we use the figure again as output, we need to set: allow_duplicate=True

    if coarse_graph_id is not None:
        # update pr graph range with overview selection
        app.clientside_callback(
            dash.ClientsideFunction(
                namespace="clientside", function_name="coarse_to_main"
            ),
            dash.Output(graph_id, "id", allow_duplicate=True),
            dash.Input(coarse_graph_id, "selectedData"),
            dash.State(graph_id, "id"),
            dash.State(coarse_graph_id, "id"),
            prevent_initial_call=True,
        )

        # update selectbox with clientside callback
        app.clientside_callback(
            dash.ClientsideFunction(
                namespace="clientside", function_name="main_to_coarse"
            ),
            dash.Output(coarse_graph_id, "id", allow_duplicate=True),
            dash.Input(graph_id, "relayoutData"),
            dash.State(coarse_graph_id, "id"),
            dash.State(graph_id, "id"),
            prevent_initial_call=True,
        )

    app.callback(
        dash.Output(graph_id, "figure", allow_duplicate=True),
        dash.Input(graph_id, "relayoutData"),
        prevent_initial_call=True,
    )(self.construct_update_data_patch)

show_dash(mode=None, config=None, init_dash_kwargs=None, graph_properties=None, **kwargs)

Registers the update_graph callback & show the figure in a dash app.

Parameters:

Name Type Description Default
mode

Display mode. One of:

  • "external": The URL of the app will be displayed in the notebook output cell. Clicking this URL will open the app in the default web browser.
  • "inline": The app will be displayed inline in the notebook output cell in an iframe.
  • "inline_persistent": The app will be displayed inline in the notebook output cell in an iframe, if the app is not reachable a static image of the figure is shown. Hence this is a persistent version of the "inline" mode, allowing users to see a static figure in other environments, browsers, etc.

    Note

    This mode requires the kaleido and flask_cors package. Install them : pip install plotly_resampler[inline_persistent] or pip install kaleido flask_cors.

  • "jupyterlab": The app will be displayed in a dedicated tab in the JupyterLab interface. Requires JupyterLab and the jupyterlab-dash extension. By default None, which will result in the same behavior as "external".

None
config dict | None

The configuration options for displaying this figure, by default None. This config parameter is the same as the dict that you would pass as config argument to the show method. See more https://plotly.com/python/configuration-options/

None
init_dash_kwargs dict | None

Keyword arguments for the Dash app constructor.

Note

This variable is of special interest when working in a jupyterhub + kubernetes environment. In this case, user notebook servers are spawned as separate pods and user access to those servers are proxied via jupyterhub. Dash requires the requests_pathname_prefix to be set on init - which can be done via this init_dash_kwargs argument. Note that you should also pass the jupyter_server_url to the show_dash method. More details: https://github.com/predict-idlab/plotly-resampler/issues/265

None
graph_properties dict | None

Dictionary of (keyword, value) for the properties that should be passed to the dcc.Graph, by default None. e.g.: {"style": {"width": "50%"}} Note: “config” is not allowed as key in this dict, as there is a distinct config parameter for this property in this method. See more https://dash.plotly.com/dash-core-components/graph

None
**kwargs

kwargs for the app.run_server() method, e.g., port=8037.

Note

These kwargs take precedence over the ones that are passed to the constructor via the show_dash_kwargs argument.

{}
Source code in plotly_resampler/figure_resampler/figure_resampler.py
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
def show_dash(
    self,
    mode=None,
    config: dict | None = None,
    init_dash_kwargs: dict | None = None,
    graph_properties: dict | None = None,
    **kwargs,
):
    """Registers the `update_graph` callback & show the figure in a dash app.

    Parameters
    ----------
    mode: str, optional
        Display mode. One of:\n
          * ``"external"``: The URL of the app will be displayed in the notebook
            output cell. Clicking this URL will open the app in the default
            web browser.
          * ``"inline"``: The app will be displayed inline in the notebook output
            cell in an iframe.
          * ``"inline_persistent"``: The app will be displayed inline in the
            notebook output cell in an iframe, if the app is not reachable a static
            image of the figure is shown. Hence this is a persistent version of the
            ``"inline"`` mode, allowing users to see a static figure in other
            environments, browsers, etc.

            !!! note

                This mode requires the ``kaleido`` and ``flask_cors`` package.
                Install them : ``pip install plotly_resampler[inline_persistent]``
                or ``pip install kaleido flask_cors``.

          * ``"jupyterlab"``: The app will be displayed in a dedicated tab in the
            JupyterLab interface. Requires JupyterLab and the ``jupyterlab-dash``
            extension.
        By default None, which will result in the same behavior as ``"external"``.
    config: dict, optional
        The configuration options for displaying this figure, by default None.
        This ``config`` parameter is the same as the dict that you would pass as
        ``config`` argument to the `show` method.
        See more [https://plotly.com/python/configuration-options/](https://plotly.com/python/configuration-options/)
    init_dash_kwargs: dict, optional
        Keyword arguments for the Dash app constructor.
        !!! note
            This variable is of special interest when working in a jupyterhub +
            kubernetes environment. In this case, user notebook servers are spawned
            as separate pods and user access to those servers are proxied via
            jupyterhub. Dash requires the `requests_pathname_prefix` to be set on
            __init__ - which can be done via this `init_dash_kwargs` argument.
            Note that you should also pass the `jupyter_server_url` to the
            `show_dash` method.
            More details: https://github.com/predict-idlab/plotly-resampler/issues/265
    graph_properties: dict, optional
        Dictionary of (keyword, value) for the properties that should be passed to
        the dcc.Graph, by default None.
        e.g.: `{"style": {"width": "50%"}}`
        Note: "config" is not allowed as key in this dict, as there is a distinct
        ``config`` parameter for this property in this method.
        See more [https://dash.plotly.com/dash-core-components/graph](https://dash.plotly.com/dash-core-components/graph)
    **kwargs: dict
        kwargs for the ``app.run_server()`` method, e.g., port=8037.
        !!! note
            These kwargs take precedence over the ones that are passed to the
            constructor via the ``show_dash_kwargs`` argument.

    """
    available_modes = ["external", "inline", "inline_persistent", "jupyterlab"]
    assert (
        mode is None or mode in available_modes
    ), f"mode must be one of {available_modes}"
    graph_properties = {} if graph_properties is None else graph_properties
    assert "config" not in graph_properties  # There is a param for config
    if self["layout"]["autosize"] is True and self["layout"]["height"] is None:
        graph_properties.setdefault("style", {}).update({"height": "100%"})

    # 0. Check if the traces need to be updated when there is a xrange set
    # This will be the case when the users has set a xrange (via the `update_layout`
    # or `update_xaxes` methods`)
    relayout_dict = {}
    for xaxis_str in self._xaxis_list:
        x_range = self.layout[xaxis_str].range
        if x_range:  # when not None
            relayout_dict[f"{xaxis_str}.range[0]"] = x_range[0]
            relayout_dict[f"{xaxis_str}.range[1]"] = x_range[1]
    if relayout_dict:  # when not empty
        update_data = self._construct_update_data(relayout_dict)

        if not self._is_no_update(update_data):  # when there is an update
            with self.batch_update():
                # First update the layout (first item of update_data)
                self.layout.update(self._parse_relayout(update_data[0]))

                # Then update the data
                for updated_trace in update_data[1:]:
                    trace_idx = updated_trace.pop("index")
                    self.data[trace_idx].update(updated_trace)

    # 1. Construct the Dash app layout
    init_dash_kwargs = {} if init_dash_kwargs is None else init_dash_kwargs
    if self._create_overview:
        # fmt: off
        # Add the assets folder to the init_dash_kwargs
        init_dash_kwargs["assets_folder"] = os.path.relpath(ASSETS_FOLDER, os.getcwd())
        # Also include the lodash script, as the client-side callbacks uses this
        init_dash_kwargs["external_scripts"] = ["https://cdn.jsdelivr.net/npm/lodash/lodash.min.js" ]
        # fmt: on

    if mode == "inline_persistent":
        mode = "inline"
        if _jupyter_dash_installed:
            # Inline persistent mode: we display a static image of the figure when the
            # app is not reachable
            # Note: this is the "inline" behavior of JupyterDashInlinePersistentOutput
            app = JupyterDashPersistentInlineOutput("local_app", **init_dash_kwargs)
            self._is_persistent_inline = True
        else:
            # If Jupyter Dash is not installed, inline persistent won't work and hence
            # we default to normal inline mode with a normal Dash app
            app = dash.Dash("local_app", **init_dash_kwargs)
            warnings.warn(
                "'jupyter_dash' is not installed. The persistent inline mode will not work. Defaulting to standard inline mode."
            )
    else:
        # jupyter dash uses a normal Dash app as figure
        app = dash.Dash("local_app", **init_dash_kwargs)

    # fmt: off
    div = dash.html.Div(
        children=[
            dash.dcc.Graph(
                id="resample-figure", figure=self, config=config, **graph_properties
            )
        ],
        style={
            "display": "flex", "flex-flow": "column",
            "height": "95vh", "width": "100%",
        },
    )
    # fmt: on
    if self._create_overview:
        overview_config = config.copy() if config is not None else {}
        overview_config["displayModeBar"] = False
        coarse_fig = self._create_overview_figure()
        div.children += [
            dash.dcc.Graph(
                id="overview-figure",
                figure=coarse_fig,
                config=overview_config,
                **graph_properties,
            ),
        ]
    app.layout = div

    self.register_update_graph_callback(
        app,
        "resample-figure",
        "overview-figure" if self._create_overview else None,
    )

    height_param = "height" if self._is_persistent_inline else "jupyter_height"

    # 2. Run the app
    if mode == "inline" and height_param not in kwargs:
        # If app height is not specified -> re-use figure height for inline dash app
        #  Note: default layout height is 450 (whereas default app height is 650)
        #  See: https://plotly.com/python/reference/layout/#layout-height
        fig_height = self.layout.height if self.layout.height is not None else 450
        kwargs[height_param] = fig_height + 18

    # kwargs take precedence over the show_dash_kwargs
    kwargs = {**self._show_dash_kwargs, **kwargs}

    # Store the app information, so it can be killed
    self._app = app
    self._host = kwargs.get("host", "127.0.0.1")
    self._port = kwargs.get("port", "8050")

    # function signature is slightly different for the Dash and JupyterDash implementations
    if self._is_persistent_inline:
        app.run(mode=mode, **kwargs)
    else:
        app.run(jupyter_mode=mode, **kwargs)

stop_server(warn=True)

Stop the running dash-app.

Parameters:

Name Type Description Default
warn bool

Whether a warning message will be shown or not, by default True.

True

Warning

This only works if the dash-app was started with show_dash.

Source code in plotly_resampler/figure_resampler/figure_resampler.py
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
def stop_server(self, warn: bool = True):
    """Stop the running dash-app.

    Parameters
    ----------
    warn: bool
        Whether a warning message will be shown or  not, by default True.

    !!! warning

        This only works if the dash-app was started with [`show_dash`][figure_resampler.figure_resampler.FigureResampler.show_dash].
    """
    if self._app is not None:
        servers_dict = (
            self._app._server_threads
            if self._is_persistent_inline
            else dash.jupyter_dash._servers
        )
        old_server = servers_dict.get((self._host, self._port))
        if old_server:
            if self._is_persistent_inline:
                old_server.kill()
                old_server.join()
            else:
                old_server.shutdown()
        del servers_dict[(self._host, self._port)]
    elif warn:
        warnings.warn(
            "Could not stop the server, either the \n"
            + "\t- 'show-dash' method was not called, or \n"
            + "\t- the dash-server wasn't started with 'show_dash'"
        )

FigureWidgetResampler

Bases: AbstractFigureAggregator, FigureWidget

Data aggregation functionality wrapper for go.FigureWidgets.

Warning

  • This wrapper only works within jupyter-based environments.
  • The .show() method returns a static figure on which the dynamic resampling cannot be performed. To allow dynamic resampling, you should just output the FigureWidgetResampler object in a cell.
Source code in plotly_resampler/figure_resampler/figurewidget_resampler.py
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
class FigureWidgetResampler(
    AbstractFigureAggregator, go.FigureWidget, metaclass=_FigureWidgetResamplerM
):
    """Data aggregation functionality wrapper for ``go.FigureWidgets``.

    !!! warning

        * This wrapper only works within ``jupyter``-based environments.
        * The ``.show()`` method returns a **static figure** on which the
          **dynamic resampling cannot be performed**. To allow dynamic resampling,
          you should just output the ``FigureWidgetResampler`` object in a cell.

    """

    def __init__(
        self,
        figure: BaseFigure | dict = None,
        convert_existing_traces: bool = True,
        default_n_shown_samples: int = 1000,
        default_downsampler: AbstractAggregator = MinMaxLTTB(),
        default_gap_handler: AbstractGapHandler = MedDiffGapHandler(),
        resampled_trace_prefix_suffix: Tuple[str, str] = (
            '<b style="color:sandybrown">[R]</b> ',
            "",
        ),
        show_mean_aggregation_size: bool = True,
        convert_traces_kwargs: dict | None = None,
        verbose: bool = False,
    ):
        # Parse the figure input before calling `super`
        f = self._get_figure_class(go.FigureWidget)()
        f._data_validator.set_uid = False

        if isinstance(figure, BaseFigure):
            # A base figure object, can be;
            # - a base plotly figure: go.Figure or go.FigureWidget
            # - a plotly-resampler figure: subclass of AbstractFigureAggregator
            # => we first copy the layout, grid_str and grid ref
            f.layout = figure.layout
            f._grid_str = figure._grid_str
            f._grid_ref = figure._grid_ref
            f.add_traces(figure.data)
        elif isinstance(figure, dict) and (
            "data" in figure or "layout" in figure  # or "frames" in figure  # TODO
        ):
            # A figure as a dict, can be;
            # - a plotly figure as a dict (after calling `fig.to_dict()`)
            # - a pickled (plotly-resampler) figure (after loading a pickled figure)
            f.layout = figure.get("layout")
            f._grid_str = figure.get("_grid_str")
            f._grid_ref = figure.get("_grid_ref")
            f.add_traces(figure.get("data"))
            # `pr_props` is not None when loading a pickled plotly-resampler figure
            f._pr_props = figure.get("pr_props")
            # `f._pr_props`` is an attribute to store properties of a plotly-resampler
            # figure. This attribute is only used to pass information to the super()
            # constructor. Once the super constructor is called, the attribute is
            # removed.

            # f.add_frames(figure.get("frames")) TODO
        elif isinstance(figure, (dict, list)):
            # A single trace dict or a list of traces
            f.add_traces(figure)

        super().__init__(
            f,
            convert_existing_traces,
            default_n_shown_samples,
            default_downsampler,
            default_gap_handler,
            resampled_trace_prefix_suffix,
            show_mean_aggregation_size,
            convert_traces_kwargs,
            verbose,
        )

        if isinstance(figure, AbstractFigureAggregator):
            # Copy the `_hf_data` if the previous figure was an AbstractFigureAggregator
            # And adjust the default max_n_samples and
            self._hf_data.update(
                self._copy_hf_data(figure._hf_data, adjust_default_values=True)
            )

            # Note: This hack ensures that the this figure object initially uses
            # data of the whole view. More concretely; we create a dict
            # serialization figure and adjust the hf-traces to the whole view
            # with the check-update method (by passing no range / filter args)
            with self.batch_update():
                graph_dict: dict = self._get_current_graph()
                update_indices = self._check_update_figure_dict(graph_dict)
                for idx in update_indices:
                    self.data[idx].update(graph_dict["data"][idx])

        self._prev_layout = None  # Contains the previous xaxis layout configuration

        # used for logging purposes to save a history of layout changes
        self._relayout_hist = []

        # Assign the the update-methods to the corresponding classes
        showspike_keys = [f"{xaxis}.showspikes" for xaxis in self._xaxis_list]
        self.layout.on_change(self._update_spike_ranges, *showspike_keys)

        x_relayout_keys = [f"{xaxis}.range" for xaxis in self._xaxis_list]
        self.layout.on_change(self._update_x_ranges, *x_relayout_keys)

    def _update_x_ranges(self, layout, *x_ranges, force_update: bool = False):
        """Update the the go.Figure data based on changed x-ranges.

        Parameters
        ----------
        layout : go.Layout
            The figure's (i.e, self) layout object. Remark that this is a reference,
            so if we change self.layout (same object reference), this object will
            change.
        *x_ranges: iterable
            A iterable list of current x-ranges, where each x-range is a tuple of two
            items, indicating the current/new (if changed) left-right x-range,
            respectively.
        fore_update: bool
            Whether an update of all traces will be forced, by default False.
        """
        relayout_dict = {}  # variable in which we aim to reconstruct the relayout
        # serialize the layout in a new dict object
        layout = {
            xaxis_str: layout[xaxis_str].to_plotly_json()
            for xaxis_str in self._xaxis_list
        }
        if self._prev_layout is None:
            self._prev_layout = layout

        for xaxis_str, x_range in zip(self._xaxis_list, x_ranges):
            # We also check whether "range" is within the xaxis its layout otherwise
            # It is most-likely an autorange check
            if (
                "range" in layout[xaxis_str]
                and self._prev_layout[xaxis_str].get("range", []) != x_range
                or (force_update and x_range is not None)
            ):
                # a change took place -> add to the relayout dict
                relayout_dict[f"{xaxis_str}.range[0]"] = x_range[0]
                relayout_dict[f"{xaxis_str}.range[1]"] = x_range[1]

                # An update will take place for that trace
                # -> save current xaxis range to _prev_layout
                self._prev_layout[xaxis_str]["range"] = x_range

        if relayout_dict:  # when not empty
            # Construct the update data
            update_data = self._construct_update_data(relayout_dict)

            if self._is_no_update(update_data):
                # Return when no data update
                return

            if self._print_verbose:
                self._relayout_hist.append(dict(zip(self._xaxis_list, x_ranges)))
                self._relayout_hist.append(layout)
                self._relayout_hist.append(["xaxis-range-update", len(update_data) - 1])
                self._relayout_hist.append("-" * 30)

            with self.batch_update():
                # First update the layout (first item of update_data)
                self.layout.update(self._parse_relayout(update_data[0]))

                for xaxis_str in self._xaxis_list:
                    if "showspikes" in layout[xaxis_str]:
                        self.layout[xaxis_str].pop("showspikes")

                # Then update the data
                for updated_trace in update_data[1:]:
                    trace_idx = updated_trace.pop("index")
                    self.data[trace_idx].update(updated_trace)

    def _update_spike_ranges(self, layout, *showspikes, force_update=False):
        """Update the go.Figure based on the changed spike-ranges.

        Parameters
        ----------
        layout : go.Layout
            The figure's (i.e, self) layout object. Remark that this is a reference,
            so if we change self.layout (same object reference), this object will
            change.
        *showspikes: iterable
            A iterable where each item is a bool, indicating  whether showspikes is set
            to true/false for the corresponding xaxis in ``self._xaxis_list``.
        force_update: bool
            Bool indicating whether the range updates need to take place. This is
            especially useful when you have recently updated the figure its data (with
            the hf_data property) and want to perform an autoscale, independent from
            the current figure-layout.
        """
        relayout_dict = {}  # variable in which we aim to reconstruct the relayout
        # serialize the layout in a new dict object
        layout = {
            xaxis_str: layout[xaxis_str].to_plotly_json()
            for xaxis_str in self._xaxis_list
        }

        if self._prev_layout is None:
            self._prev_layout = layout

        for xaxis_str, showspike in zip(self._xaxis_list, showspikes):
            if (
                force_update
                or
                # autorange key must be set to True
                (
                    layout[xaxis_str].get("autorange", False)
                    # we only perform updates for traces which have 'range' property,
                    # as we do need to reconstruct the update-data for these traces
                    and self._prev_layout[xaxis_str].get("range", None) is not None
                )
            ):
                relayout_dict[f"{xaxis_str}.autorange"] = True
                relayout_dict[f"{xaxis_str}.showspikes"] = showspike
                # autorange -> we pop the xaxis range
                if "range" in layout[xaxis_str]:
                    del layout[xaxis_str]["range"]

        if len(relayout_dict):
            # An update will take place, save current layout to _prev_layout
            self._prev_layout = layout

            # Construct the update data
            update_data = self._construct_update_data(relayout_dict)
            if self._print_verbose:
                self._relayout_hist.append(layout)
                self._relayout_hist.append(["showspikes-update", len(update_data) - 1])
                self._relayout_hist.append("-" * 30)

            with self.batch_update():
                # First update the layout (first item of update_data)
                if not force_update:
                    self.layout.update(self._parse_relayout(update_data[0]))

                # Also:  Remove the showspikes from the layout, otherwise the autorange
                # will not work as intended (it will not be triggered again)
                # Note: this removal causes a second trigger of this method
                # which will go in the "else" part below.
                for xaxis_str in self._xaxis_list:
                    self.layout[xaxis_str].pop("showspikes")

                # Then, update the data
                for updated_trace in update_data[1:]:
                    trace_idx = updated_trace.pop("index")
                    self.data[trace_idx].update(updated_trace)
        elif self._print_verbose:
            self._relayout_hist.append(["showspikes", "initial call or showspikes"])
            self._relayout_hist.append("-" * 40)

    def reset_axes(self):
        """Reset the axes of the FigureWidgetResampler.

        This is useful when adjusting the `hf_data` properties of the
        ``FigureWidgetResampler``.
        """
        self._update_spike_ranges(
            self.layout, *[False] * len(self._xaxis_list), force_update=True
        )
        # Reset the layout
        self.update_layout(
            {
                axis: {"autorange": None, "range": None}
                for axis in self._xaxis_list + self._yaxis_list
            }
        )

    def reload_data(self):
        """Reload all the data of FigureWidgetResampler for the current range-view.

        This is useful when adjusting the `hf_data` properties of the
        ``FigureWidgetResampler``.
        """
        if all(
            self.layout[xaxis].autorange
            or (
                self.layout[xaxis].autorange is None
                and self.layout[xaxis].range is None
            )
            for xaxis in self._xaxis_list
        ):
            self._update_spike_ranges(
                self.layout, *[False] * len(self._xaxis_list), force_update=True
            )
        else:
            # Resample the data for the current range-view
            self._update_x_ranges(
                self.layout,
                # Pass the current view to trigger a resample operation
                *[self.layout[xaxis_str]["range"] for xaxis_str in self._xaxis_list],
                force_update=True,
            )

reload_data()

Reload all the data of FigureWidgetResampler for the current range-view.

This is useful when adjusting the hf_data properties of the FigureWidgetResampler.

Source code in plotly_resampler/figure_resampler/figurewidget_resampler.py
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
def reload_data(self):
    """Reload all the data of FigureWidgetResampler for the current range-view.

    This is useful when adjusting the `hf_data` properties of the
    ``FigureWidgetResampler``.
    """
    if all(
        self.layout[xaxis].autorange
        or (
            self.layout[xaxis].autorange is None
            and self.layout[xaxis].range is None
        )
        for xaxis in self._xaxis_list
    ):
        self._update_spike_ranges(
            self.layout, *[False] * len(self._xaxis_list), force_update=True
        )
    else:
        # Resample the data for the current range-view
        self._update_x_ranges(
            self.layout,
            # Pass the current view to trigger a resample operation
            *[self.layout[xaxis_str]["range"] for xaxis_str in self._xaxis_list],
            force_update=True,
        )

reset_axes()

Reset the axes of the FigureWidgetResampler.

This is useful when adjusting the hf_data properties of the FigureWidgetResampler.

Source code in plotly_resampler/figure_resampler/figurewidget_resampler.py
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
def reset_axes(self):
    """Reset the axes of the FigureWidgetResampler.

    This is useful when adjusting the `hf_data` properties of the
    ``FigureWidgetResampler``.
    """
    self._update_spike_ranges(
        self.layout, *[False] * len(self._xaxis_list), force_update=True
    )
    # Reset the layout
    self.update_layout(
        {
            axis: {"autorange": None, "range": None}
            for axis in self._xaxis_list + self._yaxis_list
        }
    )