Skip to content

Jupyter Visualization

Jupyter notebook can be used to visualize the logged data. For this purpose, several Jupyter widgets and plotting functions based on plotly and tabulate are provided.

All widgets and functions can be accessed under the module: exputils.gui.jupyter

Please note that the current widgets only work with Jupyter Notebook <= 6.5 and are also not compatible with Jupyter Lab.

General Widgets

ExperimentDataLoaderWidget

Bases: BaseWidget, VBox

Jupyter widget for loading experiment data which can then be used for analysis and visualization.

The widget allows to select which experiments and datasources are loaded. The widget provides basically a GUI for the load_experiment_data function. It is also possible to define callback functions that allow to compute statistics of the loaded data or alter the data. After the user loaded the data through the widget it is available via its experiment_data property.

GUI of the widget:

ExperimentDataLoaderWidget

Functionality:

  • Update Descriptions: Load the descriptions of new experiments. This can be used to update the table after more experiments have been performed.
  • Reset Descriptions: Resets the descriptions of experiments in the table to their default if they had been changed by the user.
  • Up Button: Moves the selected experiments up in the order.
  • Down Button: Moves the selected experiments down in the order.
  • Sort by Experiment ID: Resorts the experiments according to their ID.
  • Load Data: Loads the data of all selected experiments. It is then available via the experiment_data property.
  • Empty Data: Empties the loaded data to free memory.
Example

Execute the following code in a Jupyter notebook located in the experiment campaign directory under a subdirectory, such as ./analysis.

import exputils as eu

experiment_data_loader = eu.gui.jupyter.ExperimentDataLoaderWidget()
display(experiment_data_loader)
To access the experiment data after the user has loaded it through the widget:
experiment_data_loader.experiment_data

Source code in exputils/gui/jupyter/experiment_data_loader_widget.py
 25
 26
 27
 28
 29
 30
 31
 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
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
class ExperimentDataLoaderWidget(BaseWidget, ipywidgets.VBox):
    """
        Jupyter widget for loading experiment data which can then be used for analysis and visualization.

        The widget allows to select which experiments and datasources are loaded.
        The widget provides basically a GUI for the [load_experiment_data][exputils.data.load_experiment_data] function.
        It is also possible to define callback functions that allow to compute statistics of the
        loaded data or alter the data.
        After the user loaded the data through the widget it is available via its `experiment_data` property.

        GUI of the widget:
        <figure markdown="span">
          ![ExperimentDataLoaderWidget](../assets/images/experiment_data_loader_widget.png)
        </figure>

        Functionality:

        - _Update Descriptions_: Load the descriptions of new experiments.
        This can be used to update the table after more experiments have been performed.
        - _Reset Descriptions_: Resets the descriptions of experiments in the table to their default if they had been changed by the user.
        - _Up Button_: Moves the selected experiments up in the order.
        - _Down Button_: Moves the selected experiments down in the order.
        - _Sort by Experiment ID_: Resorts the experiments according to their ID.
        - _Load Data_: Loads the data of all selected experiments. It is then available via the `experiment_data` property.
        - _Empty Data_: Empties the loaded data to free memory.

        Example:
            Execute the following code in a Jupyter notebook located in the experiment campaign directory under a subdirectory, such as `./analysis`.
            ```python
            import exputils as eu

            experiment_data_loader = eu.gui.jupyter.ExperimentDataLoaderWidget()
            display(experiment_data_loader)
            ```
            To access the experiment data after the user has loaded it through the widget:
            ```python
            experiment_data_loader.experiment_data
            ```
    """

    @staticmethod
    def default_config():
        """Generates the default configuration for the widget.

            Returns:
                dict: A dictionary containing default configurations for various components of the widget.
        """

        dc = BaseWidget.default_config()

        dc.load_experiment_descriptions_function = eu.AttrDict(
            func=eu.data.load_experiment_descriptions
        )
        dc.load_experiment_data_function = eu.AttrDict(
            func=eu.data.load_experiment_data
        )
        dc.experiments_directory = os.path.join('..', eu.DEFAULT_EXPERIMENTS_DIRECTORY)

        dc.main_box = eu.AttrDict(
            layout=eu.AttrDict(
                width='99%',
                display='flex',
                flex_flow='column',
                align_items='stretch'))

        dc.top_button_box = eu.AttrDict(
            layout=eu.AttrDict(
                width='100%',
                display='flex',
                flex_flow='row',
                align_items='stretch'))

        dc.load_descr_button = eu.AttrDict(
            layout=eu.AttrDict(
                width = '75%',
                height = 'auto'),
            description = 'Update Descriptions',
            disabled = False,
            button_style = '',  # 'success', 'info', 'warning', 'danger' or ''
            tooltip = 'Update for the selected experiment and repetition.')

        dc.reset_descr_button = eu.AttrDict(
            layout=eu.AttrDict(
                width = '25%',
                height = 'auto'),
            description = 'Reset Descriptions',
            disabled = False,
            button_style = '',  # 'success', 'info', 'warning', 'danger' or ''
            tooltip = 'Reset all experiment descriptions.')

        dc.move_buttons_box = eu.AttrDict(
            layout=eu.AttrDict(
                width='100%',
                display='flex',
                flex_flow='row',
                align_items='stretch'))

        dc.move_up_button = eu.AttrDict(
            layout=eu.AttrDict(
                width = '25%',
                height = 'auto'),
            description = u'\u02C5', # 'down',
            disabled = False,
            button_style = '',  # 'success', 'info', 'warning', 'danger' or ''
            tooltip = 'Moves the selected experiments up in the order.  (Only works if data is not filtered.)')

        dc.move_down_button = eu.AttrDict(
            layout=eu.AttrDict(
                width = '25%',
                height = 'auto'),
            description = u'\u02C4', #'up',
            disabled = False,
            button_style = '',  # 'success', 'info', 'warning', 'danger' or ''
            tooltip = 'Moves the selected experiments down in the order.  (Only works if data is not filtered.)')

        dc.sort_by_id_button = eu.AttrDict(
            layout=eu.AttrDict(
                width='50%',
                height='auto'),
            description='Sort by Experiment ID',  # 'up',
            disabled=False,
            button_style='',  # 'success', 'info', 'warning', 'danger' or ''
            tooltip='Resorts the experiments according to their ID.')

        dc.data_buttons_box = eu.AttrDict(
            layout=eu.AttrDict(
                width='100%',
                display='flex',
                flex_flow='row',
                align_items='stretch'))

        dc.load_data_button = eu.AttrDict(
            layout=eu.AttrDict(
                width = '75%',
                height = 'auto'),
            description = 'Load Data',
            disabled = False,
            button_style = '',  # 'success', 'info', 'warning', 'danger' or ''
            tooltip = 'Load experimental data.')

        dc.empty_data_button = eu.AttrDict(
            layout=eu.AttrDict(
                width = '25%',
                height = 'auto'),
            description = 'Empty Data',
            disabled = False,
            button_style = '',  # 'success', 'info', 'warning', 'danger' or ''
            tooltip = 'Empties the loaded experimental data to free memory.')


        # naming of columns in the dataframe (key: name in experiment_description dict, value: name in dataframe)
        dc.dataframe_column_names = {'id': 'experiment id',
                                     'order': 'order',
                                     'is_load_data': 'load data',
                                     'short_name': 'short name',
                                     'name': 'name',
                                     'description': 'description',
                                     'directory': 'directory'}

        dc.qgrid_widget = eu.AttrDict(
            show_toolbar = True,
            grid_options = {'autoEdit': True,
                            'sortable': False},
            column_options = {'editable': False},
            column_definitions = {
                'load data': {'editable': True},
                'short name': {'editable': True},
                'name': {'editable': True},
                'description': {'editable': True}})

        dc.output_widget = eu.AttrDict()

        return dc


    def __init__(self, config=None, **kwargs):
        # constructor of BaseWidget
        super().__init__(config=config, **kwargs)
        # constructor of GridspecLayout
        super(BaseWidget, self).__init__(
            **self.config.main_box)

        self.experiment_descriptions = None
        self.experiment_data = None

        # list with registered event handlers for the data collected event
        self._on_experiment_data_loaded_event_handlers = []
        self._on_experiment_descriptions_updated_event_handlers = []

        self.load_state_backup()

        self.update_experiment_descriptions()

        # create gui elements
        self.load_descr_btn = ipywidgets.Button(**self.config.load_descr_button)
        self.reset_descr_btn = ipywidgets.Button(**self.config.reset_descr_button)
        self.top_button_box = ipywidgets.Box(
            children=[self.load_descr_btn, self.reset_descr_btn],
            **self.config.top_button_box)

        self.qgrid_widget = ipywidgets.Box()  # initialize with dummy, will be overridden by update function

        self.move_up_btn = ipywidgets.Button(**self.config.move_up_button)
        self.move_down_btn = ipywidgets.Button(**self.config.move_down_button)
        self.sort_by_id_button = ipywidgets.Button(**self.config.sort_by_id_button)
        self.move_buttons_box = ipywidgets.Box(
            children=[self.move_down_btn, self.move_up_btn, self.sort_by_id_button],
            **self.config.move_buttons_box)

        self.load_data_btn = ipywidgets.Button(**self.config.load_data_button)
        self.empty_data_btn = ipywidgets.Button(**self.config.empty_data_button)
        self.data_buttons_box = ipywidgets.Box(
            children=[self.load_data_btn, self.empty_data_btn],
            **self.config.data_buttons_box)

        eu.gui.jupyter.add_children_to_widget(
            self,
            [self.top_button_box, self.qgrid_widget, self.move_buttons_box, self.data_buttons_box])

        # create an output widget
        self._output_widget = None

        self._update_qgrid()

        # register events
        self.load_descr_btn.on_click(self._handle_load_descr_button_on_click)
        self.reset_descr_btn.on_click(self._handle_reset_descr_button_on_click)
        self.load_data_btn.on_click(self._handle_load_data_button_on_click)
        self.empty_data_btn.on_click(self._handle_empty_data_button_on_click)
        self.move_up_btn.on_click(self._handle_move_up_button_on_click)
        self.move_down_btn.on_click(self._handle_move_down_button_on_click)
        self.sort_by_id_button.on_click(self._handle_sort_by_id_button_on_click)

        self._handle_qgrid_cell_edited_is_active = True

    def _prepare_output_widget(self):

        if self._output_widget is None:
            self._output_widget = ipywidgets.Output(**self.config.output_widget)
            IPython.display.display(self._output_widget)
        else:
            warnings.resetwarnings()
            self._output_widget.clear_output(wait=False)

        return self._output_widget


    def _handle_load_descr_button_on_click(self, btn):
        # errors are plotted in output widget and it will be cleaned after next button press
        with self._prepare_output_widget():
            self.update_experiment_descriptions(is_reset=False)
            self._update_qgrid()


    def _handle_reset_descr_button_on_click(self, btn):
        # errors are plotted in output widget and it will be cleaned after next button press
        with self._prepare_output_widget():
            self.update_experiment_descriptions(is_reset=True)
            self._update_qgrid()


    def _handle_load_data_button_on_click(self, btn):
        # errors are plotted in output widget and it will be cleaned after next button press
        with self._prepare_output_widget():
            # load data and save widget state
            print('Load data ...')
            self.load_data()
            self.backup_state()
            print('Data successfully loaded.')


    def _handle_empty_data_button_on_click(self, btn):
        # errors are plotted in output widget and it will be cleaned after next button press
        with self._prepare_output_widget():
            # empty data and save widget state
            self.empty_data()
            self.backup_state()
            print('Emptied data.')


    def _handle_move_up_button_on_click(self, btn):
        # errors are plotted in output widget and it will be cleaned after next button press
        with self._prepare_output_widget():
            try:
                self.move_up_btn.disabled = True
                self.move_down_btn.disabled = True

                self.move_experiments_up()
            finally:
                self.move_up_btn.disabled = False
                self.move_down_btn.disabled = False

    def _handle_move_down_button_on_click(self, btn):
        # errors are plotted in output widget and it will be cleaned after next button press
        with self._prepare_output_widget():
            try:
                self.move_up_btn.disabled = True
                self.move_down_btn.disabled = True

                self.move_experiments_down()
            finally:
                self.move_up_btn.disabled = False
                self.move_down_btn.disabled = False

    def _handle_sort_by_id_button_on_click(self, btn):
        # errors are plotted in output widget and it will be cleaned after next button press
        with self._prepare_output_widget():
            self.resort_experiments_by_id()

    def _handle_qgrid_cell_edited(self, event, widget):
        with self._prepare_output_widget():

            if self._handle_qgrid_cell_edited_is_active:

                # update the experiment_description
                if event['name'] == 'cell_edited':

                    for expdescr_prop_name, df_col_name in self.config.dataframe_column_names.items():
                        if df_col_name == event['column']:
                            self.experiment_descriptions[event['index']][expdescr_prop_name] = event['new']
                            break

                    self.backup_state()

                    self._call_experiment_descriptions_updated_event()


    def _handle_qgrid_filter_changed(self, event, widget):
        with self._prepare_output_widget():

            # identify if a filter is active or not
            is_filter_active = len(self.qgrid_widget.df) != len(self.qgrid_widget.get_changed_df())

            if is_filter_active:
                # do not allow to change order
                self.move_up_btn.disabled = True
                self.move_down_btn.disabled = True

            else:
                # allow to change order
                self.move_up_btn.disabled = False
                self.move_down_btn.disabled = False

    def _update_qgrid(self):

        # convert experiment description to the dataframe
        df = pd.DataFrame()
        for exp_descr_field_name, df_column_name in self.config.dataframe_column_names.items():
            df[df_column_name] = [descr[exp_descr_field_name] for descr in self.experiment_descriptions.values()]

        df = df.set_index(self.config.dataframe_column_names['id'])

        # create a new qgrid widget with the dataframe
        for opt_name, opt_value in self.config.qgrid_widget.grid_options.items():
            qgrid.set_grid_option(opt_name, opt_value)

        self.qgrid_widget = qgrid.show_grid(df,
                                            column_options=self.config.qgrid_widget.column_options,
                                            column_definitions=self.config.qgrid_widget.column_definitions,
                                            show_toolbar=self.config.qgrid_widget.show_toolbar)

        eu.gui.jupyter.remove_children_from_widget(self, 1)
        eu.gui.jupyter.add_children_to_widget(self, self.qgrid_widget, idx=1)

        self.qgrid_widget.on('cell_edited', self._handle_qgrid_cell_edited)

        self.qgrid_widget.on('filter_changed', self._handle_qgrid_filter_changed)

        self.sort_grid_by_order()


    def sort_grid_by_order(self):
        # hack to resort the experiments in the grid according to the order field
        content = dict(
            type='change_sort',
            sort_field=self.config.dataframe_column_names['order'],
            sort_ascending=True)
        self.qgrid_widget._handle_qgrid_msg_helper(content)


    def on_experiment_descriptions_updated(self, handler):
        """
        Register an event handler for the case that the experiment descriptions was changed.
        Please note, that this does not mean that the data was loaded according to the new experiment descriptions.
        Use the on_experiment_data_loaded for this purpose.
        The handler receives a dict with information about the event.
        """
        self._on_experiment_descriptions_updated_event_handlers.append(handler)


    def _call_experiment_descriptions_updated_event(self):
        for handler in self._on_experiment_descriptions_updated_event_handlers:
            handler(eu.AttrDict(
                name='experiment_descriptions_updated',
                new=self.experiment_descriptions,
                owner=self,
                type='change'))


    def on_experiment_data_loaded(self, handler):
        """
        Register an event handler for the case new data was loaded.
        The handler receives a dict with information about the event.
        """
        self._on_experiment_data_loaded_event_handlers.append(handler)


    def _call_experiment_data_loaded_event(self):
        for handler in self._on_experiment_data_loaded_event_handlers:
            handler(eu.AttrDict(
                name='data_loaded',
                new=self.experiment_data,
                owner=self,
                type='change'))


    def move_experiments_up(self, selected_items=None, is_select_changed_items=True):

        try:
            self._handle_qgrid_cell_edited_is_active = False

            if selected_items is None:
                selected_items = self.qgrid_widget.get_selected_df()

            if len(selected_items) > 0:

                order_col_name = self.config.dataframe_column_names['order']
                experiment_id_col_name = self.config.dataframe_column_names['id']

                # select according to the order
                selected_items = selected_items.sort_values(by=order_col_name)
                qgrid_df = self.qgrid_widget.get_changed_df()

                # make order the index to allow to seach by it
                all_items_sorted_by_order = qgrid_df.sort_values(by=order_col_name).reset_index().set_index(order_col_name)

                selected_idx = len(selected_items) - 1
                while selected_idx >= 0:

                    # identify current block of consequetively selected items
                    current_block_exp_ids = []

                    is_block = True
                    last_item_order = None
                    while is_block and selected_idx >= 0:
                        cur_item_order = selected_items.iloc[selected_idx][order_col_name]

                        if last_item_order is None or last_item_order - cur_item_order == 1:
                            current_block_exp_ids.append(selected_items.index[selected_idx])
                            selected_idx -= 1
                            last_item_order = cur_item_order
                        else:
                            is_block = False

                    order_of_highest_block_item = qgrid_df.loc[current_block_exp_ids[0]][order_col_name]

                    # only change orders if the block is not at the end of the list
                    if order_of_highest_block_item < len(qgrid_df) - 1:

                        # change the order of item below the block
                        exp_id_of_current_item_above_block = all_items_sorted_by_order.loc[order_of_highest_block_item + 1][experiment_id_col_name]

                        new_order = order_of_highest_block_item - len(current_block_exp_ids) + 1
                        self.qgrid_widget.edit_cell(
                            exp_id_of_current_item_above_block,
                            order_col_name,
                            new_order)
                        self.experiment_descriptions[exp_id_of_current_item_above_block]['order'] = new_order

                        # subtract 1 to all selected items in the block
                        for block_item_exp_id in current_block_exp_ids:
                            new_order = qgrid_df.loc[block_item_exp_id]['order'] + 1

                            self.qgrid_widget.edit_cell(
                                block_item_exp_id,
                                order_col_name,
                                new_order)
                            self.experiment_descriptions[block_item_exp_id]['order'] = new_order

                        #resort the experiments in the grid according to the order field
                        self.sort_grid_by_order()

                        # reslect the old elements
                        if is_select_changed_items:
                            self.qgrid_widget.change_selection(selected_items.index)

                        self.backup_state()

                        self._call_experiment_descriptions_updated_event()
        finally:
            self._handle_qgrid_cell_edited_is_active = True


    def move_experiments_down(self, selected_items=None, is_select_changed_items=True):

        try:
            self._handle_qgrid_cell_edited_is_active = False

            if selected_items is None:
                selected_items = self.qgrid_widget.get_selected_df()

            if len(selected_items) > 0:

                order_col_name = self.config.dataframe_column_names['order']
                experiment_id_col_name = self.config.dataframe_column_names['id']

                # select according to the order
                selected_items = selected_items.sort_values(by=order_col_name)
                qgrid_df = self.qgrid_widget.get_changed_df()

                # make order the index to allow to seach by it
                all_items_sorted_by_order = qgrid_df.sort_values(by=order_col_name).reset_index().set_index(order_col_name)

                selected_idx = 0
                while selected_idx < len(selected_items):

                    # identify current block of consequetively selected items
                    current_block_exp_ids = []

                    is_block = True
                    last_item_order = None
                    while is_block and selected_idx < len(selected_items):
                        cur_item_order = selected_items.iloc[selected_idx][order_col_name]

                        if last_item_order is None or cur_item_order - last_item_order == 1:
                            current_block_exp_ids.append(selected_items.index[selected_idx])
                            selected_idx += 1
                            last_item_order = cur_item_order
                        else:
                            is_block = False

                    order_of_lowest_block_item = qgrid_df.loc[current_block_exp_ids[0]][order_col_name]

                    # only change orders if the block is not at the end of the list
                    if order_of_lowest_block_item > 0:

                        # change the order of item below the block
                        exp_id_of_current_item_below_block = all_items_sorted_by_order.loc[order_of_lowest_block_item - 1][experiment_id_col_name]

                        new_order = order_of_lowest_block_item + len(current_block_exp_ids) - 1
                        self.qgrid_widget.edit_cell(
                            exp_id_of_current_item_below_block,
                            order_col_name,
                            new_order)
                        self.experiment_descriptions[exp_id_of_current_item_below_block]['order'] = new_order

                        # subtract 1 to all selected items in the block
                        for block_item_exp_id in current_block_exp_ids:
                            new_order = qgrid_df.loc[block_item_exp_id]['order'] - 1

                            self.qgrid_widget.edit_cell(
                                block_item_exp_id,
                                order_col_name,
                                new_order)
                            self.experiment_descriptions[block_item_exp_id]['order'] = new_order

                        # resort the experiments in the grid according to the order field
                        self.sort_grid_by_order()

                        # reselect the old elements
                        if is_select_changed_items:
                            self.qgrid_widget.change_selection(selected_items.index)

                        self.backup_state()

                        self._call_experiment_descriptions_updated_event()
        finally:
            self._handle_qgrid_cell_edited_is_active = True


    def resort_experiments_by_id(self):
        """
        Resets the order of the experiments according to their IDs.
        """
        try:
            # don't allow to change the qgrid during the operation
            self._handle_qgrid_cell_edited_is_active = False

            # sort experiments according to ids
            # get list of experiment ids
            sorted_existing_experiment_ids = sorted(list(self.experiment_descriptions.keys()))

            # update the order of the experiments according to the sorted list
            for order, exp_id in enumerate(sorted_existing_experiment_ids):
                self.experiment_descriptions[exp_id].order = order

            # update the gui according to the new order
            self._update_qgrid()

            # inform others the descriptions are changed
            self._call_experiment_descriptions_updated_event()

        finally:
            # qgrid can be changed again
            self._handle_qgrid_cell_edited_is_active = True


    def update_experiment_descriptions(self, is_reset=False):
        """Updates the experiment descriptions by adding new experiments and removing old experiments."""

        # load experiment descriptions
        new_exp_descr = eu.misc.call_function_from_config(
            self.config.load_experiment_descriptions_function,
            self.config.experiments_directory)

        if not self.experiment_descriptions or is_reset:
            self.experiment_descriptions = new_exp_descr
        else:
            # combine existing descriptions and new list

            # remove non-existing elements from exisiting descriptions
            deleted_experiments = set(self.experiment_descriptions.keys()).difference(set(new_exp_descr.keys()))
            for deleted_exp in deleted_experiments:
                del self.experiment_descriptions[deleted_exp]

            # kepp current order and add new experiments at the end of the list
            # get current order of experiment
            sorted_existing_experiment_ids = eu.data.get_ordered_experiment_ids_from_descriptions(self.experiment_descriptions)
            sorted_new_experiment_ids = eu.data.get_ordered_experiment_ids_from_descriptions(new_exp_descr)

            # remove existing experiment ids from the sorted list of new experiment ids
            for existing_exp_id in sorted_existing_experiment_ids:
                if existing_exp_id in sorted_new_experiment_ids:
                    sorted_new_experiment_ids.remove(existing_exp_id)

            # add new elements
            self.experiment_descriptions = eu.combine_dicts(self.experiment_descriptions, new_exp_descr)

            # update the order of the experiments according to the sorted lists
            for order, exp_id in enumerate(sorted_existing_experiment_ids + sorted_new_experiment_ids):
                self.experiment_descriptions[exp_id].order = order

            # do not keep the repetition ids from existing ones, but use the ones from the new discriptions
            # otherwise, if new repetitions are added, they will not be used
            for new_descr in new_exp_descr.values():
                self.experiment_descriptions[new_descr.id].repetition_ids = new_descr.repetition_ids

        self._call_experiment_descriptions_updated_event()


    def get_widget_state(self):
        state = super().get_widget_state()
        state.experiment_descriptions = self.experiment_descriptions
        return state


    def set_widget_state(self, state):
        if 'experiment_descriptions' in state: self.experiment_descriptions  = state.experiment_descriptions
        return super().set_widget_state(state)

    def empty_data(self):
        # Delete experiment_data to free memory
        if self.experiment_data:
            keys = list(self.experiment_data.keys())
            for key in keys:
                del self.experiment_data[key]

    def load_data(self):
        """Loads the experiment data.

        Can be called directly after initialization of the widget to preload the data without needing a user input.
        """

        # delete old data to free memory
        self.empty_data()

        experiment_data = eu.misc.call_function_from_config(
            self.config.load_experiment_data_function,
            self.experiment_descriptions)

        # some data loader functions give as extra argument the experiment descriptions
        if isinstance(experiment_data, tuple):
            experiment_data = experiment_data[0]

        self.experiment_data = experiment_data

        self._call_experiment_data_loaded_event()

ExperimentDataPlotSelectionWidget

Bases: ExperimentDataSelectionWidget

Jupyter widget for plotting experiment data and creating Jupyter cells for dedicated plotting.

The widget allows to select the datasource that should be plotted and the plotting function. It also allows to select which experiments should be plotted and to create dedicated Jupyter cells to plot specific datasources.

GUI of the widget:

ExperimentDataPlotSelectionWidget

Functionality:

  • Data Sources: Allows to define the datasource or datasources that should be plotted. The datasource names correspond to the filenames under the data folder of repetitions and correspond to the names that were used by the logging functions. A comma-seperated list of datasources can be provided for table plots (tabulate_meanstd). It is also possible to extract single elements from data arrays using bracket operation after the name. For example loss[-1] will access the final loss value.
  • Experiments: Selection of experiments from which data was loaded that should be plotted.
  • Repetitions: Selection of repetitions from which data was loaded that should be plotted.
  • Plot Function: Plotting function that should be used. See the Plotting Functions section for a list of exisiting plotting functions.
  • Plot Configuration: Configuration of the plotting function. See the Plotting Functions section for details.
  • Plot Data: Plots the data below the widget.
  • Code Production: Creates a new Jupyter notebook cell below the current one that contains the code to plot the data again with all the configuration that was set in the GUI. The code also allows to change the configuration.
Example

Execute the following code in a Jupyter notebook located in the experiment campaign directory under a subdirectory, such as ./analysis. This code should be executed after data has been loaded, for example via the ExperimentDataLoaderWidget.

# allow plotting of data loaded by the experiment_data_loader (ExperimentDataLoaderWidget)
experiment_data_plotter = eu.gui.jupyter.ExperimentDataPlotSelectionWidget(experiment_data_loader)
display(experiment_data_plotter)

Source code in exputils/gui/jupyter/experiment_data_plot_selection_widget.py
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
class ExperimentDataPlotSelectionWidget(ExperimentDataSelectionWidget):
    """
        Jupyter widget for plotting experiment data and creating Jupyter cells for dedicated plotting.

        The widget allows to select the datasource that should be plotted and the plotting function.
        It also allows to select which experiments should be plotted and to create dedicated Jupyter
        cells to plot specific datasources.

        GUI of the widget:
        <figure markdown="span">
          ![ExperimentDataPlotSelectionWidget](../assets/images/experiment_data_plot_selection_widget.png)
        </figure>

        Functionality:

        - _Data Sources_: Allows to define the datasource or datasources that should be plotted.
            The datasource names correspond to the filenames under the data folder of repetitions and
            correspond to the names that were used by the [logging](logging.md#writting) functions.
            A comma-seperated list of datasources can be provided for table plots ([tabulate_meanstd][exputils.gui.jupyter.tabulate_meanstd.tabulate_meanstd]).
            It is also possible to extract single elements from data arrays using bracket operation after the name.
            For example `loss[-1]` will access the final loss value.
        - _Experiments_: Selection of experiments from which data was loaded that should be plotted.
        - _Repetitions_: Selection of repetitions from which data was loaded that should be plotted.
        - _Plot Function_: Plotting function that should be used.
            See the [Plotting Functions](./#plotting-functions) section for a list of exisiting
            plotting functions.
        - _Plot Configuration_: Configuration of the plotting function.
            See the [Plotting Functions](./#plotting-functions) section for details.
        - _Plot Data_: Plots the data below the widget.
        - _Code Production_: Creates a new Jupyter notebook cell below the current one that contains
            the code to plot the data again with all the configuration that was set in the GUI.
            The code also allows to change the configuration.

        Example:
            Execute the following code in a Jupyter notebook located in the experiment campaign directory under a subdirectory, such as `./analysis`.
            This code should be executed after data has been loaded, for example via the [ExperimentDataLoaderWidget][exputils.gui.jupyter.ExperimentDataLoaderWidget].
            ```python
            # allow plotting of data loaded by the experiment_data_loader (ExperimentDataLoaderWidget)
            experiment_data_plotter = eu.gui.jupyter.ExperimentDataPlotSelectionWidget(experiment_data_loader)
            display(experiment_data_plotter)
            ```
    """

    @staticmethod
    def default_config():
        dc = ExperimentDataSelectionWidget.default_config()

        # do not show the get data button, this widget will create its own plotting button
        dc.is_get_experiment_data_button = False

        # dictionary with possible plotting function
        dc.plot_functions = {'plotly_meanstd_scatter': eu.gui.jupyter.plotly_meanstd_scatter,
                             'plotly_box': eu.gui.jupyter.plotly_box,
                             'plotly_meanstd_bar': eu.gui.jupyter.plotly_meanstd_bar,
                             'tabulate_meanstd': eu.gui.jupyter.tabulate_meanstd,
                             'tabulate_pairwise': eu.gui.jupyter.tabulate_pairwise,

        }

        # dictionary with plot_function_configs for each
        dc.plot_function_configs = {'plotly_meanstd_scatter': DEFAULT_PLOTLY_MEANSTD_SCATTER_CONFIG,
                                    'plotly_box': DEFAULT_PLOTLY_BOX_CONFIG,
                                    'plotly_meanstd_bar': DEFAULT_PLOTLY_MEANSTD_BAR_CONFIG,
                                    'tabulate_meanstd': DEFAULT_TABULATE_MEANSTD_CONFIG,
                                    'tabulate_pairwise': DEFAULT_TABULATE_PAIRWISE_CONFIG,
        }

        dc.is_plot_function_selection = True
        dc.plot_function = list(dc.plot_functions.keys())[0]
        dc.plot_function_selection = eu.AttrDict(
            hbox=eu.AttrDict(  # ipywidgets.HBox parameters
                layout=eu.AttrDict(
                    width='100%')),
            label=eu.AttrDict(  # ipywidgets.Label parameters
                value='Plot Function:',
                layout=eu.AttrDict(min_width='100px')),
            dropdown=eu.AttrDict(  # ipywidgets.Text parameters
                description='',
                layout=eu.AttrDict(width='100%')))

        dc.is_plot_function_config_editor = True
        dc.plot_function_config = None
        dc.plot_function_config_editor = eu.AttrDict(
            title = 'Plot Configuration',
            accordion=eu.AttrDict(  # ipywidgets.HBox parameters
                selected_index=None,  # collapse accordion at init
                layout=eu.AttrDict(
                    width='100%')),
            textarea=eu.AttrDict(  # ipywidgets.Label parameters
                placeholder='Provide the configuration of the plot function in form of a python dictionary ...',
                layout=eu.AttrDict(
                    min_width='100%'))) # TODO: start with a larger height at the beginning

        dc.is_plot_button = True
        dc.plot_button = eu.AttrDict(
            description='Plot Data',
            tooltip='Plots the data according to the selection.',
            layout=eu.AttrDict(width='100%', height='95%'))

        dc.code_producer.code_templates = [dict(name='Multi Line', code_template=CODE_TEMPLATE_MULTILINE),
                                           dict(name='Single Line', code_template=CODE_TEMPLATE_SINGLELINE)]

        dc.figure_output = eu.AttrDict()  # ipywidgets.Output parameters

        return dc


    def __init__(self, experiment_data, experiment_descriptions=None, config=None, **kwargs):

        super().__init__(experiment_data,
                         experiment_descriptions=experiment_descriptions,
                         config=config,
                         **kwargs)

        # add a figure output below the selection
        self.figure_output = None

        #########################
        # handle config input if not a state backup was loaded
        if not hasattr(self, '_plot_function_key') and not hasattr(self, '_plot_function') and not hasattr(self, '_plot_function_configs'):

            # use first plot function in plot_fuctions dictionary if non is provided
            if self.config.plot_function == '' or self.config.plot_function is None:
                plot_function = list(self.config.plot_functions.keys())[0]
            else:
                plot_function = self.config.plot_function

            if isinstance(plot_function, str):
                # config.plot_function is a key for the config.plot_functions dictionary

                if self.config.plot_function not in self.config.plot_functions:
                    raise ValueError('If config.plot_function is a string then it must be a key for the config.plot_functions dictionary!')

                self._plot_function_key = plot_function
                self._plot_function = self.config.plot_functions[self._plot_function_key]

            elif callable(plot_function):
                # if the config.plotfunction is a function handle
                self._plot_function_key = 'NONE'
                self._plot_function = plot_function

            else:
                raise TypeError('Unsupported type for config.plot_fuction! Only strings or function handles are valid.')

            # handle self._plot_function_configs
            self._plot_function_configs = self.config.plot_function_configs.copy()
            plot_function_config = self.config.plot_function_config
            if plot_function_config == '' or plot_function_config is None:
                # if no config.plot_function_config is given, then use the one from the config.plot_function_configs dictionary
                # create a new entry in _plot_function_configs if non exists for this plot_function_key
                if self._plot_function_key not in self._plot_function_configs:
                    self._plot_function_configs[self._plot_function_key] = None
            else:
                # if a config is given, then override the _plot_functions_config with it
                self._plot_function_configs[self._plot_function_key] = _config_obj_to_dict(plot_function_config)

            # if the config is invalid and the user can not change it anymore, then raise an exception
            if not self.config.is_plot_function_config_editor \
                    and isinstance(self._plot_function_configs[self._plot_function_key], BaseException):
                raise self._plot_function_config[self._plot_function_key]

            if self._plot_function_key == 'NONE' and self.config.is_plot_function_selection:
                raise ValueError('If plot_function_selection is active, then the config.plot_function must be a key for the config.plot_functions dictionary!')

        ########################
        # add gui components
        selection_children = []

        # only allow selection of a plot function if the defined intial plot_function is not a function handle
        if self.config.is_plot_function_selection and callable(self.config.plot_function):
            self.config.is_plot_function_selection = False

        # selection of plot function
        if self.config.is_plot_function_selection:

            self.plot_function_selection_label_widget = ipywidgets.Label(**self.config.plot_function_selection.label)

            self.plot_function_selection_dropdown_widget = ipywidgets.Dropdown(
                options=list(self.config.plot_functions.keys()),
                value=self._plot_function_key,
                **self.config.plot_function_selection.dropdown)

            self.plot_function_selection_hbox_widget = ipywidgets.HBox(
                [self.plot_function_selection_label_widget, self.plot_function_selection_dropdown_widget],
                **self.config.plot_function_selection.hbox)

            selection_children.append(self.plot_function_selection_hbox_widget)

            # register event to know when a new plot function was selected
            self.plot_function_selection_dropdown_widget.observe(
                self._on_plot_function_selection_dropdown_widget_value_change,
                names='value')

        if self.config.is_plot_function_config_editor:

            self.plot_function_config_editor_textarea = ipywidgets.Textarea(
                value=self._plot_function_configs[self._plot_function_key],
                **self.config.plot_function_config_editor.textarea)

            self.plot_function_config_editor_accordion = ipywidgets.Accordion(
                children=[self.plot_function_config_editor_textarea],
                **self.config.plot_function_config_editor.accordion)
            self.plot_function_config_editor_accordion.set_title(0, self.config.plot_function_config_editor.title)

            selection_children.append(self.plot_function_config_editor_accordion)

            # register event to know when a new config was edited
            self.plot_function_config_editor_textarea.observe(
                self._on_plot_function_config_editor_value_change,
                names='value')

        # add selection items befor the box for the buttons
        eu.gui.jupyter.add_children_to_widget(self, selection_children, idx=-1)

        # add plotting button
        if self.config.is_plot_button:
            self.plot_button = ipywidgets.Button(**self.config.plot_button)
            self.plot_button.on_click(self._plot_button_on_click_handler)

            # append button to start of button box
            eu.gui.jupyter.add_children_to_widget(self.activity_hbox, self.plot_button, idx=0)


    def _on_plot_function_selection_dropdown_widget_value_change(self, event_descr):
        self._plot_function_key = event_descr['new']
        self._plot_function = self.config.plot_functions[self._plot_function_key]
        self.plot_function_config = self._plot_function_configs.get(self._plot_function_key, None)


    def _on_plot_function_config_editor_value_change(self, event_descr):
        self._plot_function_configs[self._plot_function_key] = event_descr['new']


    @property
    def plot_function(self):
        return self._plot_function


    @plot_function.setter
    def plot_function(self, plot_function):

        if isinstance(plot_function, str):
            # config.plot_function is a key for the config.plot_functions dictionary

            if plot_function not in self.config.plot_functions:
                raise ValueError(
                    'If plot_function is a string then it must be a key for the config.plot_functions dictionary!')

            self._plot_function_key = plot_function
            self._plot_function = self.config.plot_functions[self._plot_function_key]

            # also the config.plot_function_configs dict will be used
            self.plot_function_config = self._plot_function_configs[self._plot_function_key]

        elif callable(plot_function):
            # if the config.plotfunction is a function handle

            # try to find function in configured functions
            plot_function_key = 'NONE'
            for func_str, func in self.config.plot_functions.items():
                if func == plot_function:
                    plot_function_key = func_str
                    break

            self._plot_function_key = plot_function_key
            self._plot_function = plot_function
            if self._plot_function_key not in self._plot_function_configs:
                self._plot_function_configs[self._plot_function_key] = None

    @property
    def plot_function_config(self):
        return _config_obj_to_dict(self._plot_function_configs[self._plot_function_key])


    @plot_function_config.setter
    def plot_function_config(self, plot_function_config):

        if self.config.is_plot_function_config_editor:

            if not isinstance(plot_function_config, str):
                raise ValueError('If the plot_function_config_editor is activated, then the given plot_function_config must be a string!')

            # was the gui element already created?
            if hasattr(self, 'plot_function_config_editor_textarea'):
                self.plot_function_config_editor_textarea.value = plot_function_config

        self._plot_function_configs[self._plot_function_key] = plot_function_config


    def _plot_button_on_click_handler(self, _):
        self.plot_data()


    @property
    def selection(self):
        '''AttrDict (dict) with the selected experiment data options.'''
        selection = super().selection
        selection.plot_function = self.plot_function
        selection.plot_function_config = self.plot_function_config
        return selection


    @selection.setter
    def selection(self, selection):
        super(self.__class__, self.__class__).selection.fset(self, selection)
        if 'plot_function' in selection: self.plot_function = selection.plot_function
        if 'plot_function_config' in selection: self.plot_function_config = selection.plot_function_config


    def get_widget_state(self):
        state = super().get_widget_state()

        if self._plot_function_key == 'NONE':
            state.plot_function = self.plot_function
        else:
            state.plot_function = self._plot_function_key
        state._plot_function_configs = self._plot_function_configs
        return state


    def set_widget_state(self, state):

        if '_plot_function_configs' in state:
            self._plot_function_configs = state._plot_function_configs  # set all gui configs

        if 'plot_function' in state:
            self.plot_function = state.plot_function

        if '_plot_function_configs' in state:
            self.plot_function_config = self._plot_function_configs[self._plot_function_key]  # update the view

        return super().set_widget_state(state)


    def plot_data(self):

        # display the figure
        # must be created here so that the display is created below the selection gui
        if self.figure_output is None:
            self.figure_output = ipywidgets.Output(**self.config.figure_output)
            IPython.display.display(self.figure_output)
        else:
            self.figure_output.clear_output(wait=True)

        with self.figure_output:
            print('Plotting ...')

            # load experimental data before plotting
            self.select_experiment_data()

            plot_config = self.plot_function_config
            if isinstance(plot_config, BaseException):
                raise plot_config
            else:
                display_obiect = self.plot_function(
                    self.selected_data,
                    labels=self.selected_data_labels,
                    config=plot_config)

                self.figure_output.clear_output(wait=True)
                IPython.display.display(display_obiect)


    def get_code_producer_variables(self):

        variables = super().get_code_producer_variables()

        plot_function_handle = self.plot_function
        variables.plot_function = plot_function_handle.__name__

        if plot_function_handle.__module__ == '__main__' or plot_function_handle.__module__ == 'builtins':
            # no extra imports
            variables.import_statements = '\n'
        else:
            variables.import_statements = 'from {} import {}'.format(plot_function_handle.__module__, variables.plot_function)

        # plot configuration as a dictionary
        if self.config.is_plot_function_config_editor:
            plot_function_config_str = 'eu.AttrDict(\n{})'.format(self.plot_function_config_editor_textarea.value)
        else:
            plot_function_config_dict = self.plot_function_config
            plot_function_config_str = 'eu.' + str(eu.AttrDict(plot_function_config_dict))

        variables.plot_function_config = plot_function_config_str

        return variables

Plotting Functions

plotly_meanstd_scatter

Interactive line plot that shows the mean and std over all repetitions of each experiment or to show the individual repetitions.

plotly_meanstd_scatter

Parameters:

Name Type Description Default
data list

Data to plot. Should be in the following forms:

  • [subplot_idx:list][trace_idx:list][elems_per_trace:numpy.ndarray]
  • [trace_idx:list][elems_per_trace:numpy.ndarray]
  • [elems_per_trace:numpy.ndarray]
None
config dict

Dictionary with configuration of plot.

None

Configuration:

  • layout (dict): See Plotly layout for all possible options.
    • xaxis (dict):
      • title (str): Title of the x-axis.
      • range (tuple): Tuple with min and max values of x-axis. Default is [None, None].
    • yaxis (dict)
      • title (str): Title of the y-axis.
      • range (tuple): Tuple with min and max values of y-axis. Default is [None, None].
  • moving_average (dict):
    • n (int): Number of elements (over the x-axis) over which a moving average should be computed. Default is 1.
  • data_filter (dict):
    • every_nth_step (int, dict): Either an integer with the number of steps or a dictionary. In case of a dictionary:
      • step (int): Number of steps between taken values. Default is None.
      • include_final_step (bool): Should the final step (the final value) also be included even if outside the stepping. Default is False.

Returns:

Name Type Description
fig figure

Plotly figure object that can be displayed using display(fig).

The plot is based on Plotly scatter.

Source code in exputils/gui/jupyter/plotly_meanstd_scatter.py
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 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
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
def plotly_meanstd_scatter(data: Optional[list] = None,
                           config: Optional[dict] = None,
                           **kwargs):
    """
    Interactive line plot that shows the mean and std over all repetitions of each experiment or to
    show the individual repetitions.

    <figure markdown="span">
          ![plotly_meanstd_scatter](../assets/images/plotly_meanstd_scatter_2.png)
    </figure>

    Parameters:
        data (list): Data to plot. Should be in the following forms:

            - [subplot_idx:list][trace_idx:list][elems_per_trace:numpy.ndarray]
            - [trace_idx:list][elems_per_trace:numpy.ndarray]
            - [elems_per_trace:numpy.ndarray]
        config (dict): Dictionary with configuration of plot.

    __Configuration__:

    - `layout` (`dict`): See [Plotly layout](https://plotly.com/python/reference/layout/) for all possible options.
        - `xaxis` (`dict`):
            - `title` (`str`): Title of the x-axis.
            - `range` (`tuple`): Tuple with min and max values of x-axis. Default is `[None, None]`.
        - `yaxis` (`dict`)
            - `title` (`str`): Title of the y-axis.
            - `range` (`tuple`): Tuple with min and max values of y-axis. Default is `[None, None]`.
    -  `moving_average` (`dict`):
        - `n` (`int`): Number of elements (over the x-axis) over which a moving average should be
            computed. Default is `1`.
    - `data_filter` (`dict`):
        - `every_nth_step` (`int`, `dict`):
            Either an integer with the number of steps or a dictionary.
            In case of a dictionary:
            - `step` (`int`): Number of steps between taken values. Default is `None`.
            - `include_final_step` (`bool`):
                Should the final step (the final value) also be included even if outside the stepping.
                Default is `False`.

    Returns:
        fig (figure): Plotly figure object that can be displayed using `display(fig)`.

    The plot is based on [Plotly scatter](https://plotly.com/python/line-and-scatter/).
    """
    default_config = eu.AttrDict(

        # allows to display the moving average per element over n steps
        moving_average=eu.AttrDict(
            n=1,
            mode='fill_start'
        ),

        data_filter=eu.AttrDict(
            every_nth_step=eu.AttrDict(
                step=None,
                include_final_step=False),
        ),

        subplots=eu.AttrDict(  # paramters for the 'plotly.subplots.make_subplots' function
            rows=None,
            cols=None,
            print_grid=False
        ),
        std=eu.AttrDict(
            style='shaded',  # or errorbar
            steps=1,
            visible=True
        ),
        init_mode='mean_std',  # mean_std, mean, elements
        plotly_format='webgl',  # webgl or svg
        error_type='std', # std or sem (standard error of the mean)
        layout=eu.AttrDict(

            default_xaxis=eu.AttrDict(),
            # if several subplots, then these are the default values for all xaxis config in fig.layout
            default_yaxis=eu.AttrDict(),
            # if several subplots, then these are the default values for all yaxis config in fig.layout

            xaxis=eu.AttrDict(),
            yaxis=eu.AttrDict(),

            updatemenus=[
                eu.AttrDict(type="buttons",
                         active=0,
                         buttons=[
                             eu.AttrDict(label='mean + std',
                                      method='restyle',
                                      args=[{'visible': []}]),
                             eu.AttrDict(label='mean',
                                      method='restyle',
                                      args=[{'visible': []}]),
                             eu.AttrDict(label='elements',
                                      method='restyle',
                                      args=[{'visible': []}]),
                         ],
                         direction='right',
                         pad={'t': 70},
                         x=1,
                         xanchor='right',
                         y=0,
                         yanchor='top'
                         ),
            ]
        ),

        default_trace=eu.AttrDict(),

        default_mean_trace=eu.AttrDict(
            legendgroup='<subplot_idx>-<trace_idx>',  # subplot_idx, trace_idx
            hoverinfo='text+x',
        ),
        default_subplot_mean_traces=[],  # default config of traces per subplot
        mean_traces=[],

        default_std_trace=eu.AttrDict(
            legendgroup='<mean_trace_legendgroup>',  # subplot_idx, trace_idx, mean_trace_legendgroup
            hoverinfo='none',
            showlegend=False,
        ),
        default_subplot_std_traces=[],  # default config of traces per subplot
        std_traces=[],

        default_element_trace=eu.AttrDict(  # overall default
            legendgroup=None,
            # subplot_idx, trace_idx, elem_idx, subelem_idx, mean_trace_legendgroup, std_trace_legendgroup
        ),
        default_subplot_element_traces=[],  # default per subplot
        default_data_element_traces=[],  # default per data item
        element_traces=[],  # individual items

        # label configurations

        labels=[],  # holds all labels in a specific structure

        default_mean_label='<trace_idx>',
        mean_labels=[],

        default_element_label='<mean_label> - <subelem_idx>',
        # possible replacements: <mean_label>, <subelem_idx>, <elem_idx>, <trace_idx>
        element_labels=[],

        default_colors=plotly.colors.DEFAULT_PLOTLY_COLORS,
    )
    config = eu.combine_dicts(kwargs, config, default_config)

    if data is None:
        data = np.array([])

    # format data in form [subplot_idx:list][trace_idx:list][elems_per_trace:numpy.ndarray]
    if isinstance(data, np.ndarray):
        data = [[data]]
    elif isinstance(data, list) and isinstance(data[0], np.ndarray):
        data = [data]
    elif not isinstance(data, list) and not isinstance(data[0], list) and not isinstance(data[0][0], np.ndarray):
        raise ValueError('Unsupported type of data!')

    # handle different input formats of labels
    if config.labels:
        # if only labels for mean-traces are given, then add an empty label for the sub figure
        if isinstance(config.labels, list) and not isinstance(config.labels[0], tuple):
            config.labels = [('', config.labels)]

        # if no labels are given for elements, then create an empty list for element labels
        for ds_idx in range(len(config.labels)):
            for trace_idx in range(len(config.labels[ds_idx][1])):
                if not isinstance(config.labels[ds_idx][1][trace_idx], tuple):
                    config.labels[ds_idx][1][trace_idx] = (config.labels[ds_idx][1][trace_idx], [])

    # subplot_titles
    if config.labels and ('subplot_titles' not in config.subplots or config.subplots.subplot_titles == []):
        config.subplots.subplot_titles = [subplot_labels[0] for subplot_labels in config.labels]

    # identify the number of subplots
    n_subplots = len(data)

    # if not defined, set rows and cols of subplots
    if config.subplots.rows is None and config.subplots.cols is None:
        config.subplots.rows = n_subplots
        config.subplots.cols = 1
    elif config.subplots.rows is not None and config.subplots.cols is None:
        config.subplots.cols = int(np.ceil(n_subplots / config.subplots.rows))
    elif config.subplots.rows is None and config.subplots.cols is not None:
        config.subplots.rows = int(np.ceil(n_subplots / config.subplots.cols))

    if config.plotly_format.lower() == 'webgl':
        plotly_scatter_plotter = plotly.graph_objs.Scattergl
    elif config.plotly_format.lower() == 'svg':
        plotly_scatter_plotter = plotly.graph_objs.Scatter
    else:
        raise ValueError('Unknown config {!r} for plotly_format! Allowed values: \'webgl\', \'svg\'.')

    # make figure with subplots
    fig = plotly.subplots.make_subplots(**config.subplots)

    mean_traces = []
    elem_traces = []

    elem_idx = 0

    # interate over subplots
    for subplot_idx, subplot_data in enumerate(data):

        subplot_mean_traces = []
        subplot_elem_traces = []

        # iterate over traces
        for trace_idx, cur_data in enumerate(subplot_data):

            # do not plot data, if it does not exits
            if cur_data is not None:

                # in case the data is only 1 point, add an extra dimension
                if np.ndim(cur_data) == 1:
                    cur_data = np.array([cur_data]).transpose()

                # create a moving average over the data if requested
                if config.moving_average is not None and config.moving_average.n != 1:
                    cur_data = eu.misc.moving_average(
                        cur_data,
                        config.moving_average.n,
                        config.moving_average.mode)

                # define standard x_values
                x_values = list(range(cur_data.shape[1]))

                # filter the data if requested
                if config.data_filter.every_nth_step is not None:

                    if isinstance(config.data_filter.every_nth_step, dict):
                        step = config.data_filter.every_nth_step.step
                        is_include_final_step = config.data_filter.every_nth_step.include_final_step
                    else:
                        step = config.data_filter.every_nth_step
                        is_include_final_step = False

                    # detect if the final step was not in the selection
                    if is_include_final_step and cur_data.shape[1] % step == 0:
                        inds = np.zeros(cur_data.shape[1], dtype=bool)
                        inds[::step] = True
                        inds[-1] = True

                        cur_data = cur_data[:, inds]
                        x_values = x_values[::step] + [x_values[-1]]
                    else:
                        cur_data = cur_data[:, ::step]
                        x_values = x_values[::step]

                mean_data = np.nanmean(cur_data, axis=0)

                if config.error_type == 'std':
                    std_data = np.nanstd(cur_data, axis=0)
                elif config.error_type == 'sem':
                    std_data = np.nanstd(cur_data, axis=0, ddof=1) / np.sqrt(np.shape(cur_data)[0])
                else:
                    raise ValueError('Unknown error_type!')

                info_text = ['{} ± {}'.format(mean_data[idx], std_data[idx]) for idx in range(len(mean_data))]

                # define label of the trace
                if config.labels:
                    mean_label = config.labels[subplot_idx][1][trace_idx][0]
                else:
                    mean_label = config.default_mean_label
                    if len(config.mean_labels) > trace_idx:
                        mean_label = config.mean_labels[trace_idx]
                mean_label = eu.misc.replace_str_from_dict(str(mean_label), {'<trace_idx>': trace_idx})

                mean_trace_params = dict(
                    x=x_values,
                    y=mean_data,
                    line=dict(color=config.default_colors[trace_idx % len(config.default_colors)]),
                    name=mean_label,
                    text=info_text,
                )

                mean_trace_config = eu.combine_dicts(config.default_mean_trace, config.default_trace)
                if len(config.default_subplot_mean_traces) > subplot_idx:
                    mean_trace_config = eu.combine_dicts(config.default_subplot_mean_traces[subplot_idx], mean_trace_config)
                if len(config.mean_traces) > trace_idx:
                    mean_trace_config = eu.combine_dicts(config.mean_traces[trace_idx], mean_trace_config)

                mean_trace_params = eu.combine_dicts(mean_trace_config, mean_trace_params)

                # handle legendgroup
                mean_trace_legendgroup = mean_trace_params.legendgroup
                if isinstance(mean_trace_legendgroup, str):
                    mean_trace_legendgroup = eu.misc.replace_str_from_dict(mean_trace_legendgroup,
                                                                           {'<trace_idx>': trace_idx,
                                                                            '<subplot_idx>': subplot_idx})
                mean_trace_params.legendgroup = mean_trace_legendgroup

                cur_mean_trace = plotly_scatter_plotter(**mean_trace_params)
                subplot_mean_traces.append(cur_mean_trace)

                # handle trace for std values

                if config.std.style.lower() == 'shaded':

                    fill_color = config.default_colors[trace_idx % len(config.default_colors)]
                    fill_color = fill_color.replace('rgb', 'rgba')
                    fill_color = fill_color.replace(')', ', 0.2)')

                    std_trace_params = dict(
                        x=x_values + x_values[::-1],
                        y=np.hstack((mean_data + std_data, mean_data[::-1] - std_data[::-1])),
                        fill='tozerox',
                        line=dict(color='rgba(255,255,255,0)'),
                        fillcolor=fill_color,
                    )

                elif config.std.style.lower() == 'errorbar':

                    std_trace_params = dict(
                        x=x_values[::config.std.steps],
                        y=mean_data[::config.std.steps],
                        error_y=dict(type='data', array=std_data, visible=True),
                        mode='markers',
                        line=dict(color=config.default_colors[trace_idx % len(config.default_colors)]),
                        marker=dict(size=0, opacity=0),
                    )

                else:
                    raise ValueError(
                        'Unknown config.std.style ({!r})! Options: \'shaded\', \'errorbar\''.format(config.std.type))

                std_trace_config = eu.combine_dicts(config.default_std_trace, config.default_trace)
                if len(config.default_subplot_std_traces) > subplot_idx:
                    std_trace_config = eu.combine_dicts(config.default_subplot_std_traces[subplot_idx], std_trace_config)
                if len(config.std_traces) > trace_idx:
                    std_trace_config = eu.combine_dicts(config.std_traces[trace_idx], std_trace_config)
                std_trace_params = eu.combine_dicts(std_trace_config, std_trace_params)

                # handle legendgroup
                std_trace_legendgroup = std_trace_params.legendgroup
                if isinstance(std_trace_legendgroup, str):
                    std_trace_legendgroup = eu.misc.replace_str_from_dict(std_trace_legendgroup,
                                                                          {'<trace_idx>': trace_idx,
                                                                           '<subplot_idx>': subplot_idx,
                                                                           '<mean_trace_legendgroup>': mean_trace_legendgroup})
                std_trace_params.legendgroup = std_trace_legendgroup

                cur_std_trace = plotly_scatter_plotter(**std_trace_params)
                subplot_mean_traces.append(cur_std_trace)

                # traces for each data element
                n_elems = cur_data.shape[0]
                color_coeff_step = 1 / n_elems
                cur_color_coeff = 0 + color_coeff_step
                for cur_elem_idx in range(n_elems):

                    if config.labels and len(config.labels[subplot_idx][1][trace_idx][1]) > cur_elem_idx:
                        element_label = config.labels[subplot_idx][1][trace_idx][1][cur_elem_idx]
                    else:
                        element_label = config.default_element_label
                    if len(config.element_labels) > trace_idx:
                        element_label = config.element_labels[trace_idx]
                    element_label = eu.misc.replace_str_from_dict(str(element_label),
                                                                  {'<trace_idx>': trace_idx,
                                                                   '<subelem_idx>': cur_elem_idx,
                                                                   '<elem_idx>': elem_idx,
                                                                   '<mean_label>': mean_label})

                    color = eu.gui.misc.transform_color_str_to_tuple(
                        config.default_colors[trace_idx % len(config.default_colors)])
                    color = (color[0],
                             int(color[1] * cur_color_coeff),
                             int(color[2] * cur_color_coeff),
                             int(color[3] * cur_color_coeff))
                    color = eu.gui.misc.transform_color_tuple_to_str(color)
                    cur_color_coeff += color_coeff_step

                    element_trace_params = dict(
                        x=x_values,
                        y=cur_data[cur_elem_idx, :],
                        line=dict(color=color),
                        name=element_label,
                        visible=True,
                    )

                    element_trace_config = eu.combine_dicts(config.default_element_trace, config.default_trace)
                    if len(config.default_subplot_element_traces) > subplot_idx:
                        element_trace_config = eu.combine_dicts(config.default_subplot_element_traces[subplot_idx],
                                                                element_trace_config)
                    if len(config.default_data_element_traces) > cur_elem_idx:
                        element_trace_config = eu.combine_dicts(config.default_data_element_traces[cur_elem_idx],
                                                                element_trace_config)
                    if len(config.element_traces) > elem_idx:
                        element_trace_config = eu.combine_dicts(config.element_traces[elem_idx], element_trace_config)

                    element_trace_params = eu.combine_dicts(element_trace_config, element_trace_params)

                    # handle legendgroup
                    element_trace_legendgroup = element_trace_params.legendgroup
                    if isinstance(element_trace_legendgroup, str):
                        element_trace_legendgroup = eu.misc.replace_str_from_dict(
                            element_trace_legendgroup,
                            {'<subelem_idx>': cur_elem_idx,
                             '<elem_idx>': elem_idx,
                             '<trace_idx>': trace_idx,
                             '<subplot_idx>': subplot_idx,
                             '<mean_trace_legendgroup>': mean_trace_legendgroup,
                             '<std_trace_legendgroup>': std_trace_legendgroup})
                    element_trace_params.legendgroup = element_trace_legendgroup

                    cur_elem_trace = plotly_scatter_plotter(**element_trace_params)
                    subplot_elem_traces.append(cur_elem_trace)

                    elem_idx += 1

        mean_traces.append(subplot_mean_traces)
        elem_traces.append(subplot_elem_traces)

    # set for the std toggle buttons which traces should be hidden and which ones should be shown
    layout = config.layout

    # set default values for all layouts
    def set_axis_properties_by_default(axis_name, fig_layout, config_layout):
        # sets the axis properties to default values

        def set_single_axis_property_default(cur_axis_name, default_name):
            if cur_axis_name in fig_layout or cur_axis_name in config_layout:
                cur_config = config_layout[cur_axis_name] if cur_axis_name in config_layout else dict()
                config_layout[cur_axis_name] = eu.combine_dicts(cur_config, config_layout[default_name])

        default_name = 'default_' + axis_name

        set_single_axis_property_default(axis_name, default_name)
        set_single_axis_property_default(axis_name + '1', default_name)
        axis_idx = 2
        while True:
            cur_axis_name = axis_name + str(axis_idx)

            if cur_axis_name not in fig_layout and cur_axis_name not in config_layout:
                break

            set_single_axis_property_default(cur_axis_name, default_name)
            axis_idx += 1

    set_axis_properties_by_default('xaxis', fig['layout'], layout)
    set_axis_properties_by_default('yaxis', fig['layout'], layout)

    # remove default fields, because they are not true proerties of the plotly layout
    del (layout['default_xaxis'])
    del (layout['default_yaxis'])

    update_menus_visible_meanstd = []
    update_menus_visible_mean = []
    update_menus_visible_elements = []

    for subplot_idx in range(len(mean_traces)):
        update_menus_visible_meanstd.extend(
            [True, True] * int(len(mean_traces[subplot_idx]) / 2) + [False] * int(len(elem_traces[subplot_idx])))
        update_menus_visible_mean.extend(
            [True, False] * int(len(mean_traces[subplot_idx]) / 2) + [False] * int(len(elem_traces[subplot_idx])))

        element_default_visibility = [elem_trace['visible'] for elem_trace in elem_traces[subplot_idx]]
        update_menus_visible_elements.extend(
            [False, False] * int(len(mean_traces[subplot_idx]) / 2) + element_default_visibility)

    if layout.updatemenus:

        layout.updatemenus[0]['buttons'][0]['args'][0]['visible'] = update_menus_visible_meanstd
        layout.updatemenus[0]['buttons'][1]['args'][0]['visible'] = update_menus_visible_mean
        layout.updatemenus[0]['buttons'][2]['args'][0]['visible'] = update_menus_visible_elements

        if config.init_mode == 'mean_std':
            config.layout.updatemenus[0]['active'] = 0
        elif config.init_mode == 'mean':
            config.layout.updatemenus[0]['active'] = 1
        elif config.init_mode == 'elements':
            config.layout.updatemenus[0]['active'] = 2
        else:
            raise ValueError(
                'Value {!r} for \'config.init_mode\' is not supported! Only \'mean_std\',\'mean\',\'elements\'.'.format(
                    config.init_mode))

    if config.init_mode == 'mean_std':
        trace_visibility = update_menus_visible_meanstd
    elif config.init_mode == 'mean':
        trace_visibility = update_menus_visible_mean
    elif config.init_mode == 'elements':
        trace_visibility = update_menus_visible_elements
    else:
        raise ValueError(
            'Value {!r} for \'config.init_mode\' is not supported! Only \'mean_std\',\'mean\',\'elements\'.'.format(
                config.init_mode))

    cur_row = 1
    cur_col = 1
    for subplot_idx in range(n_subplots):

        n_traces = len(mean_traces[subplot_idx]) + len(elem_traces[subplot_idx])

        fig.add_traces(mean_traces[subplot_idx] + elem_traces[subplot_idx],
                       rows=[cur_row] * n_traces,
                       cols=[cur_col] * n_traces)

        if cur_col < config.subplots.cols:
            cur_col += 1
        else:
            cur_col = 1
            cur_row += 1

    for trace_idx in range(len(fig['data'])):
        fig['data'][trace_idx]['visible'] = trace_visibility[trace_idx]

    fig['layout'].update(layout)

    return fig

plotly_box

Interactive box plot that shows several statistics such as the mean, std and range of scalars over all repetitions of each experiment.

plotly_box

Parameters: data (list): Data to plot. config (dict): Dictionary with configuration of plot.

Configuration:

  • layout (dict): See Plotly layout for all possible options.
    • xaxis (dict):
      • title (str): Title of the x-axis.
      • range (tuple): Tuple with min and max values of x-axis. Default is [None, None].
    • yaxis (dict)
      • title (str): Title of the y-axis.
      • range (tuple): Tuple with min and max values of y-axis. Default is [None, None].

Returns: fig (figure): Plotly figure object that can be displayed using display(fig).

The plot is based on Plotly box plots.

Source code in exputils/gui/jupyter/plotly_box.py
def plotly_box(data: Optional[list] = None,
               config: Optional[dict] = None,
               **kwargs):
    """
     Interactive box plot that shows several statistics such as the mean, std and range of scalars
     over all repetitions of each experiment.

    <figure markdown="span">
          ![plotly_box](../assets/images/plotly_box.png)
    </figure>

     Parameters:
         data (list): Data to plot.
         config (dict): Dictionary with configuration of plot.

     __Configuration__:

     - `layout` (`dict`): See [Plotly layout](https://plotly.com/python/reference/layout/) for all possible options.
         - `xaxis` (`dict`):
             - `title` (`str`): Title of the x-axis.
             - `range` (`tuple`): Tuple with min and max values of x-axis. Default is `[None, None]`.
         - `yaxis` (`dict`)
             - `title` (`str`): Title of the y-axis.
             - `range` (`tuple`): Tuple with min and max values of y-axis. Default is `[None, None]`.

     Returns:
         fig (figure): Plotly figure object that can be displayed using `display(fig)`.

     The plot is based on [Plotly box plots](https://plotly.com/python/box-plots/).
     """

    default_config = eu.AttrDict(
        subplots=eu.AttrDict(
            rows=None,
            cols=None,
            print_grid=False
        ),
        init_mode='mean_std',  # mean_std, mean, elements
        layout=eu.AttrDict(

            default_xaxis=eu.AttrDict(),  # if several subplots, then these are the default values for all xaxis config in fig.layout
            default_yaxis=eu.AttrDict(),  # if several subplots, then these are the default values for all yaxis config in fig.layout

            xaxis=eu.AttrDict(),
            yaxis=eu.AttrDict(),

            boxmode='group',

            updatemenus=[
                eu.AttrDict(
                    type="buttons",
                    active=0,
                    buttons=[
                         eu.AttrDict(label='normal',
                                     method='restyle',
                                     args=[{'boxpoints': False}]),
                         eu.AttrDict(label='all',
                              method='restyle',
                              args=[{'boxpoints': 'all'}]),
                         eu.AttrDict(label='suspectedoutliers',
                              method='restyle',
                              args=[{'boxpoints': 'suspectedoutliers'}]),
                         eu.AttrDict(label='outliers',
                              method='restyle',
                              args=[{'boxpoints': 'outliers'}]),
                     ],
                     direction='right',
                     pad={'t': 70},
                     x=1,
                     xanchor='right',
                     y=0,
                     yanchor='top'
                     ),
            ]
        ),

        default_trace=eu.AttrDict(
            legendgroup=None,
            boxmean='sd',
        ),
        default_subplot_traces=[],
        traces=[],

        labels=[],  # holds all labels in a specific structure

        default_trace_label='<trace_idx>',
        trace_labels=[],

        default_group_label='<group_idx>',
        group_labels=[],

        default_colors=plotly.colors.DEFAULT_PLOTLY_COLORS,
    )
    config = eu.combine_dicts(kwargs, config, default_config)

    if data is None:
        data = np.array([])

    # format data in form [subplot_idx:list][trace_idx:list][elems_per_trace:numpy.ndarray]
    if isinstance(data, np.ndarray):
        data = [[data]]
    elif isinstance(data, list) and isinstance(data[0], np.ndarray):
        data = [data]
    elif not isinstance(data, list) and not isinstance(data[0], list) and not isinstance(data[0][0], np.ndarray):
        raise ValueError('Unsupported type of data!')

    # handle different input formats of labels
    if config.labels:
        # if only labels for mean-traces are given, then add an empty label for the sub figure
        if isinstance(config.labels, list) and not isinstance(config.labels[0], tuple):
            config.labels = [('', config.labels)]
        # if no labels are given for elements, then create an empty list for element labels
        for ds_idx in range(len(config.labels)):
            for trace_idx in range(len(config.labels[ds_idx][1])):
                if not isinstance(config.labels[ds_idx][1][trace_idx], tuple):
                    config.labels[ds_idx][1][trace_idx] = (config.labels[ds_idx][1][trace_idx], [])

    # subplot_titles
    if config.labels and ('subplot_titles' not in config.subplots or config.subplots.subplot_titles == []):
        config.subplots.subplot_titles = [subplot_labels[0] for subplot_labels in config.labels]

    # identify the number of subplots
    n_subplots = len(data)

    # if not defined, set rows and cols of subplots
    if config.subplots.rows is None and config.subplots.cols is None:
        config.subplots.rows = n_subplots
        config.subplots.cols = 1
    elif config.subplots.rows is not None and config.subplots.cols is None:
        config.subplots.cols = int(np.ceil(n_subplots / config.subplots.rows))
    elif config.subplots.rows is None and config.subplots.cols is not None:
        config.subplots.rows = int(np.ceil(n_subplots / config.subplots.cols))

    # handle init mode
    if config.init_mode == 'normal':
        if len(config.layout.updatemenus) > 0:
            config.layout.updatemenus[0]['active'] = 0
        config.default_trace.boxpoints = False
    elif config.init_mode == 'all':
        if len(config.layout.updatemenus) > 0:
            config.layout.updatemenus[0]['active'] = 1
        config.default_trace.boxpoints = 'all'
    elif config.init_mode == 'suspectedoutliers':
        if len(config.layout.updatemenus) > 0:
            config.layout.updatemenus[0]['active'] = 2
        config.default_trace.boxpoints = 'suspectedoutliers'
    elif config.init_mode == 'outliers':
        if len(config.layout.updatemenus) > 0:
            config.layout.updatemenus[0]['active'] = 3
        config.default_trace.boxpoints = 'outliers'

    # make figure with subplots
    fig = plotly.subplots.make_subplots(**config.subplots)

    traces = []

    # interate over subplots
    for subplot_idx, subplot_data in enumerate(data):

        subplot_traces = []

        # create for each experiment a trace
        for trace_idx, cur_data in enumerate(subplot_data):

            data_points = np.array([])
            elem_labels = []

            if np.ndim(cur_data) == 0:
                data_points = np.array([cur_data])
                elem_labels = ['']
            elif np.ndim(cur_data) == 1:
                data_points = cur_data
                elem_labels = [''] * len(data_points)
            else:
                # collect data over elements
                for elem_idx, elem_data in enumerate(cur_data):  # data elements

                    # get element data which could be in matrix format or array format
                    if np.ndim(elem_data) == 0:
                        cur_elem_data = np.array([elem_data])
                    elif np.ndim(elem_data) == 1:
                        cur_elem_data = elem_data
                    elif np.ndim(elem_data) == 2:
                        if elem_data.shape[0] == 1:
                            cur_elem_data = elem_data[0, :]
                        elif elem_data.shape[1] == 1:
                            cur_elem_data = elem_data[1, 0]
                        else:
                            raise ValueError('Invalid data format!')
                    else:
                        raise ValueError('Invalid data format!')

                    data_points = np.hstack((data_points, cur_elem_data))

                    # handle trace for mean values
                    group_label = config.default_group_label
                    if len(config.group_labels) > elem_idx:
                        group_label = config.group_labels[elem_idx]
                    group_label = eu.misc.replace_str_from_dict(str(group_label), {'<group_idx>': elem_idx})

                    elem_labels.extend([group_label] * len(cur_elem_data))

            # handle trace for mean values
            if config.labels:
                trace_label = config.labels[subplot_idx][1][trace_idx][0]
            else:
                trace_label = config.default_trace_label
                if len(config.trace_labels) > trace_idx:
                    trace_label = config.trace_labels[trace_idx]
            trace_label = eu.misc.replace_str_from_dict(str(trace_label), {'<trace_idx>': trace_idx})

            trace_params = eu.AttrDict(
                x=elem_labels,
                y=data_points,
                name=trace_label,
                marker_color=config.default_colors[trace_idx % len(config.default_colors)])

            trace_config = config.default_trace.copy()
            if len(config.default_subplot_traces) > subplot_idx:
                trace_config = eu.combine_dicts(config.default_subplot_traces[subplot_idx], trace_config)
            if len(config.traces) > trace_idx:
                trace_config = eu.combine_dicts(config.traces[trace_idx], trace_config)

            trace_params = eu.combine_dicts(trace_config, trace_params)

            # handle legendgroup
            trace_legendgroup = trace_params.legendgroup
            if isinstance(trace_legendgroup, str):
                trace_legendgroup = eu.misc.replace_str_from_dict(trace_legendgroup,
                                                                  {'<trace_idx>': trace_idx,
                                                                   '<subplot_idx>': subplot_idx})
            trace_params.legendgroup = trace_legendgroup

            cur_trace = plotly.graph_objs.Box(**trace_params)
            subplot_traces.append(cur_trace)

        traces.append(subplot_traces)

    # set for the std toggle buttons which traces should be hidden and which ones should be shown
    layout = config.layout

    # set default values for all layouts
    def set_axis_properties_by_default(axis_name, fig_layout, config_layout):
        # sets the axis properties to default values

        def set_single_axis_property_default(cur_axis_name, default_name):
            if cur_axis_name in fig_layout or cur_axis_name in config_layout:
                cur_config = config_layout[cur_axis_name] if cur_axis_name in config_layout else dict()
                config_layout[cur_axis_name] = eu.combine_dicts(cur_config, config_layout[default_name])

        default_name = 'default_' + axis_name

        set_single_axis_property_default(axis_name, default_name)
        set_single_axis_property_default(axis_name + '1', default_name)
        axis_idx = 2
        while True:
            cur_axis_name = axis_name + str(axis_idx)

            if cur_axis_name not in fig_layout and cur_axis_name not in config_layout:
                break

            set_single_axis_property_default(cur_axis_name, default_name)
            axis_idx += 1

    set_axis_properties_by_default('xaxis', fig['layout'], layout)
    set_axis_properties_by_default('yaxis', fig['layout'], layout)

    # remove default fields, because they are not true proerties of the plotly layout
    del (layout['default_xaxis'])
    del (layout['default_yaxis'])

    cur_row = 1
    cur_col = 1
    for subplot_idx in range(n_subplots):
        n_traces = len(traces[subplot_idx])

        fig.add_traces(traces[subplot_idx],
                       rows=[cur_row] * n_traces,
                       cols=[cur_col] * n_traces)

        if cur_col < config.subplots.cols:
            cur_col += 1
        else:
            cur_col = 1
            cur_row += 1

    fig['layout'].update(layout)

    return fig

plotly_meanstd_bar

Interactive bar plot that shows the mean and std of scalars over all repetitions of each experiment.

plotly_meanstd_bar

Parameters: data (list): Data to plot. config (dict): Dictionary with configuration of plot.

Configuration:

  • layout (dict): See Plotly layout for all possible options.
    • xaxis (dict):
      • title (str): Title of the x-axis.
      • range (tuple): Tuple with min and max values of x-axis. Default is [None, None].
    • yaxis (dict)
      • title (str): Title of the y-axis.
      • range (tuple): Tuple with min and max values of y-axis. Default is [None, None].

Returns: fig (figure): Plotly figure object that can be displayed using display(fig).

The plot is based on Plotly bar charts.

Source code in exputils/gui/jupyter/plotly_meanstd_bar.py
def plotly_meanstd_bar(data: Optional[list] = None,
                       config: Optional[dict] = None,
                       **kwargs):
    """
     Interactive bar plot that shows the mean and std of scalars over all repetitions of each experiment.

    <figure markdown="span">
          ![plotly_meanstd_bar](../assets/images/plotly_meanstd_bar.png)
    </figure>

     Parameters:
         data (list): Data to plot.
         config (dict): Dictionary with configuration of plot.

     __Configuration__:

     - `layout` (`dict`): See [Plotly layout](https://plotly.com/python/reference/layout/) for all possible options.
         - `xaxis` (`dict`):
             - `title` (`str`): Title of the x-axis.
             - `range` (`tuple`): Tuple with min and max values of x-axis. Default is `[None, None]`.
         - `yaxis` (`dict`)
             - `title` (`str`): Title of the y-axis.
             - `range` (`tuple`): Tuple with min and max values of y-axis. Default is `[None, None]`.

     Returns:
         fig (figure): Plotly figure object that can be displayed using `display(fig)`.

     The plot is based on [Plotly bar charts](https://plotly.com/python/bar-charts/).
     """
    default_config = eu.AttrDict(
        subplots=eu.AttrDict(
            rows=None,
            cols=None,
            print_grid=False
        ),
        init_mode='mean_std',  # mean_std, mean, elements
        layout=eu.AttrDict(

            default_xaxis=eu.AttrDict(),  # if several subplots, then these are the default values for all xaxis config in fig.layout
            default_yaxis=eu.AttrDict(),  # if several subplots, then these are the default values for all yaxis config in fig.layout

            xaxis=eu.AttrDict(),
            yaxis=eu.AttrDict(),

            boxmode='group',

            updatemenus=[
                dict(type="buttons",
                     active=0,
                     buttons=[
                         eu.AttrDict(
                             label='with std',
                             method='restyle',
                             args=[{'error_y.visible': True}]),
                         eu.AttrDict(
                             label='without std',
                             method='restyle',
                             args=[{'error_y.visible': False}]),
                     ],
                     direction='right',
                     pad={'t': 70},
                     x=1,
                     xanchor='right',
                     y=0,
                     yanchor='top'
                     ),
            ]
        ),

        default_trace=eu.AttrDict(
            legendgroup=None,
            error_y=eu.AttrDict(visible=True),
        ),
        default_subplot_traces=[],
        traces=[],

        labels=[],  # holds all labels in a specific structure

        default_trace_label='<trace_idx>',
        trace_labels=[],

        default_group_label='<group_idx>',
        group_labels=[],

        default_colors=plotly.colors.DEFAULT_PLOTLY_COLORS,
    )
    config = eu.combine_dicts(kwargs, config, default_config)

    default_string_replace_pattern = '<{}>'

    if data is None:
        data = np.array([])

    # format data in form [subplot_idx:list][trace_idx:list][elems_per_trace:numpy.ndarray]
    if isinstance(data, np.ndarray):
        data = [[data]]
    elif isinstance(data, list) and isinstance(data[0], np.ndarray):
        data = [data]
    elif not isinstance(data, list) and not isinstance(data[0], list) and not isinstance(data[0][0], np.ndarray):
        raise ValueError('Unsupported type of data!')

    # handle different input formats of labels
    if config.labels:
        # if only labels for mean-traces are given, then add an empty label for the sub figure
        if isinstance(config.labels, list) and not isinstance(config.labels[0], tuple):
            config.labels = [('', config.labels)]
        # if no labels are given for elements, then create an empty list for element labels
        for ds_idx in range(len(config.labels)):
            for trace_idx in range(len(config.labels[ds_idx][1])):
                if not isinstance(config.labels[ds_idx][1][trace_idx], tuple):
                    config.labels[ds_idx][1][trace_idx] = (config.labels[ds_idx][1][trace_idx], [])

    # subplot_titles
    if config.labels and ('subplot_titles' not in config.subplots or config.subplots.subplot_titles == []):
        config.subplots.subplot_titles = [subplot_labels[0] for subplot_labels in config.labels]

    # identify the number of subplots
    n_subplots = len(data)

    # if not defined, set rows and cols of subplots
    if config.subplots.rows is None and config.subplots.cols is None:
        config.subplots.rows = n_subplots
        config.subplots.cols = 1
    elif config.subplots.rows is not None and config.subplots.cols is None:
        config.subplots.cols = int(np.ceil(n_subplots / config.subplots.rows))
    elif config.subplots.rows is None and config.subplots.cols is not None:
        config.subplots.rows = int(np.ceil(n_subplots / config.subplots.cols))

    # handle init mode
    if config.init_mode == 'mean_std':
        config.layout.updatemenus[0]['active'] = 0
        config.default_trace.error_y.visible = True
    elif config.init_mode == 'mean':
        config.layout.updatemenus[0]['active'] = 1
        config.default_trace.error_y.visible = False

    # make figure with subplots
    fig = plotly.subplots.make_subplots(**config.subplots)

    traces = []

    # interate over subplots
    for subplot_idx, subplot_data in enumerate(data):

        subplot_traces = []

        # create for each experiment a trace
        for trace_idx, cur_data in enumerate(subplot_data):  # data source

            means = []
            stds = []
            group_labels = []

            if np.ndim(cur_data) == 0 or np.ndim(cur_data) == 1:
                means.append(np.nanmean(cur_data))
                stds.append(np.nanstd(cur_data))
                group_labels.append('')
            else:
                # collect data over elements
                for elem_idx, elem_data in enumerate(cur_data):  # data elements

                    # get element data which could be in matrix format or array format
                    if np.ndim(elem_data) == 1:
                        cur_elem_data = elem_data
                    elif np.ndim(elem_data) == 2:
                        if elem_data.shape[0] == 1:
                            cur_elem_data = elem_data[0, :]
                        elif elem_data.shape[1] == 1:
                            cur_elem_data = elem_data[1, 0]
                        else:
                            raise ValueError('Invalid data format!')
                    else:
                        raise ValueError('Invalid data format!')

                    means.append(np.nanmean(cur_elem_data))
                    stds.append(np.nanstd(cur_elem_data))

                    # handle trace for mean values
                    group_label = config.default_group_label
                    if len(config.group_labels) > elem_idx:
                        group_label = config.group_labels[elem_idx]
                    group_label = eu.misc.replace_str_from_dict(str(group_label), {'<group_idx>': elem_idx})

                    group_labels.append(group_label)

            # handle trace for mean values
            if config.labels:
                trace_label = config.labels[subplot_idx][1][trace_idx][0]
            else:
                trace_label = config.default_trace_label
                if len(config.trace_labels) > trace_idx:
                    trace_label = config.trace_labels[trace_idx]
            trace_label = eu.misc.replace_str_from_dict(str(trace_label), {'<trace_idx>': trace_idx})

            trace_params = dict(
                x=group_labels,
                y=means,
                error_y=dict(type='data', array=stds),
                name=trace_label,
                marker_color=config.default_colors[trace_idx % len(config.default_colors)])

            trace_config = config.default_trace.copy()
            if len(config.default_subplot_traces) > subplot_idx:
                trace_config = eu.combine_dicts(config.default_subplot_traces[subplot_idx], trace_config)
            if len(config.traces) > trace_idx:
                trace_config = eu.combine_dicts(config.traces[trace_idx], trace_config)

            trace_params = eu.combine_dicts(trace_config, trace_params)

            # handle legendgroup
            trace_legendgroup = trace_params.legendgroup
            if isinstance(trace_legendgroup, str):
                trace_legendgroup = eu.misc.replace_str_from_dict(
                    trace_legendgroup,
                    {'<trace_idx>': trace_idx,
                     '<subplot_idx>': subplot_idx})
            trace_params.legendgroup = trace_legendgroup

            cur_trace = plotly.graph_objs.Bar(**trace_params)
            subplot_traces.append(cur_trace)

        traces.append(subplot_traces)

    # set for the std toggle buttons which traces should be hidden and which ones should be shown
    layout = config.layout

    # set default values for all layouts
    def set_axis_properties_by_default(axis_name, fig_layout, config_layout):
        # sets the axis properties to default values

        def set_single_axis_property_default(cur_axis_name, default_name):
            if cur_axis_name in fig_layout or cur_axis_name in config_layout:
                cur_config = config_layout[cur_axis_name] if cur_axis_name in config_layout else dict()
                config_layout[cur_axis_name] = eu.combine_dicts(cur_config, config_layout[default_name])

        default_name = 'default_' + axis_name

        set_single_axis_property_default(axis_name, default_name)
        set_single_axis_property_default(axis_name + '1', default_name)
        axis_idx = 2
        while True:
            cur_axis_name = axis_name + str(axis_idx)

            if cur_axis_name not in fig_layout and cur_axis_name not in config_layout:
                break

            set_single_axis_property_default(cur_axis_name, default_name)
            axis_idx += 1

    set_axis_properties_by_default('xaxis', fig['layout'], layout)
    set_axis_properties_by_default('yaxis', fig['layout'], layout)

    # remove default fields, because they are not true proerties of the plotly layout
    del (layout['default_xaxis'])
    del (layout['default_yaxis'])

    cur_row = 1
    cur_col = 1
    for subplot_idx in range(n_subplots):
        n_traces = len(traces[subplot_idx])

        fig.add_traces(traces[subplot_idx],
                       rows=[cur_row] * n_traces,
                       cols=[cur_col] * n_traces)

        if cur_col < config.subplots.cols:
            cur_col += 1
        else:
            cur_col = 1
            cur_row += 1

    fig['layout'].update(layout)

    return fig

tabulate_meanstd

Table that shows the mean and std of scalars over all repetitions of each experiment. Can be used to display several datasources.

tabulate_meanstd

Parameters:

Name Type Description Default
data list

Data to plot.

None
config dict

Dictionary with configuration of plot.

None

Configuration:

  • primary_content_function (function): Handle to function that computes the first value of a cell. Function format: func(data: nparray) -> scalar. Default is numpy.nanmean.

  • secondary_content_function (function): Handle to function that computes the first value of a cell. Function format: func(data: nparray) -> scalar. Default is numpy.nanstd.

  • tabulate (dict): Parameters for the tabulate function that plots the table. See tabulate for all possible parameters. Some important ones:

    • tablefmt (str): Format of the table such as 'html', 'latex', or 'simple'. Default is 'html'.
    • numalign (str): Alignment of numbers in the table ('right', 'center', or 'left'). Default is 'right'.

    • cell_format (str): Format of the cell content. The format can take up to 2 numbers which are by default the mean and the std. Default is '{:.3f} ({:.3f})'.

  • flip_rows_and_cols (bool): Should the content of rows and columns be flipped. Default is False.

  • top_left_cell_content (str): Content of the top left cell which can be used as a label for the table. Default is ''.

Returns:

Name Type Description
fig figure

Plotly figure object that can be displayed using display(fig).

The plot is based on tabulate.

Source code in exputils/gui/jupyter/tabulate_meanstd.py
def tabulate_meanstd(data: Optional[list] = None,
                     config: Optional[dict] = None,
                     **kwargs):
    """
    Table that shows the mean and std of scalars over all repetitions of each experiment.
    Can be used to display several datasources.

    <figure markdown="span">
          ![tabulate_meanstd](../assets/images/tabulate_meanstd.png){width="500"}
    </figure>

    Parameters:
        data (list): Data to plot.
        config (dict): Dictionary with configuration of plot.

    __Configuration__:

    - `primary_content_function` (`function`):
            Handle to function that computes the first value of a cell.
            Function format: `func(data: nparray) -> scalar`.
            Default is [`numpy.nanmean`](https://numpy.org/doc/stable/reference/generated/numpy.nanmean.html).

    - `secondary_content_function` (`function`):
            Handle to function that computes the first value of a cell.
            Function format: `func(data: nparray) -> scalar`.
            Default is [`numpy.nanstd`](https://numpy.org/doc/stable/reference/generated/numpy.nanstd.html).

    - `tabulate` (`dict`): Parameters for the tabulate function that plots the table.
            See [tabulate](https://pypi.org/project/tabulate/) for all possible parameters.
            Some important ones:
        -  `tablefmt` (`str`):
            Format of the table such as `'html'`, `'latex'`, or `'simple'`.
            Default is `'html'`.
        - `numalign` (`str`): Alignment of numbers in the table (`'right'`, `'center'`, or `'left'`).
            Default is `'right'`.

        - `cell_format` (`str`):
            Format of the cell content. The format can take up to 2 numbers which are by default the
            mean and the std.
            Default is `'{:.3f} ({:.3f})'`.

    - `flip_rows_and_cols` (`bool`): Should the content of rows and columns be flipped.
            Default is `False`.

    - `top_left_cell_content` (`str`): Content of the top left cell which can be used as a label for the table.
            Default is `''`.

    Returns:
        fig (figure): Plotly figure object that can be displayed using `display(fig)`.

    The plot is based on [tabulate](https://pypi.org/project/tabulate/).
    """

    default_config = eu.AttrDict(

        primary_content_function = np.nanmean,

        secondary_content_function = np.nanstd,

        flip_rows_and_cols = False,

        tabulate=eu.AttrDict(
            tablefmt='html', #
            numalign='right',
        ),

        cell_format = '{:.3f} ({:.3f})',

        top_left_cell_content = '',

        default_row_label = '<row_idx>',
        default_column_label = '<column_idx>',

        labels=[],  # holds all labels in a specific structure

    )
    config = eu.combine_dicts(kwargs, config, default_config)

    # remove the secondary information from the cell_format if the secondary information is set to None and the cell_format was not changed
    if config.secondary_content_function is None and config.cell_format == default_config.cell_format:
        config.cell_format = '{}'

    if data is None:
        data = np.array([])

    # format data in form [rows][columns][elems_per_trace:numpy.ndarray]
    # subplot is a single table
    if isinstance(data, np.ndarray):
        data = [[data]]
    elif isinstance(data, list) and isinstance(data[0], np.ndarray):
        data = [data]
    elif not isinstance(data, list) and not isinstance(data[0], list) and not isinstance(data[0][0], np.ndarray):
        raise ValueError('Unsupported type of data!')

    # handle different input formats of labels
    if config.labels:
        # if only labels for columns are given, then add an empty label for the sub figure
        if isinstance(config.labels, list) and not isinstance(config.labels[0], tuple):
            config.labels = [('', config.labels)]

    row_headers = []
    column_headers = []

    primary_data = []
    secondary_data = []

    # interate over rows (subplots)
    for row_idx, row_data in enumerate(data):

        primary_data.append([])
        secondary_data.append([])

        # define label of the row
        if config.labels:
            row_label = config.labels[row_idx][0]
        else:
            row_label = config.default_row_label
        row_label = eu.misc.replace_str_from_dict(str(row_label), {'<row_idx>': row_idx})
        row_headers.append(row_label)

        # collect the data and labels for each trace
        for column_idx, cur_data in enumerate(row_data):

            # get column_header from labels of first column
            if row_idx == 0:

                # define label of the row
                if config.labels:
                    column_label = config.labels[row_idx][1][column_idx]
                    if isinstance(column_label, tuple):
                        column_label = column_label[0]
                else:
                    column_label = config.default_column_label
                column_label = eu.misc.replace_str_from_dict(str(column_label), {'<column_idx>': column_idx})
                column_headers.append(column_label)

            data_points = np.array([])

            if np.ndim(cur_data) == 0:
                data_points = np.array([cur_data])
            elif np.ndim(cur_data) == 1:
                data_points = cur_data
            else:
                # collect data over elements
                for elem_idx, elem_data in enumerate(cur_data):  # data elements

                    # get element data which could be in matrix format or array format
                    if np.ndim(elem_data) == 0:
                        cur_elem_data = np.array([elem_data])
                    elif np.ndim(elem_data) == 1:
                        cur_elem_data = elem_data
                    elif np.ndim(elem_data) == 2:
                        if elem_data.shape[0] == 1:
                            cur_elem_data = elem_data[0, :]
                        elif elem_data.shape[1] == 1:
                            cur_elem_data = elem_data[1, 0]
                        else:
                            raise ValueError('Invalid data format!')
                    else:
                        raise ValueError('Invalid data format!')

                    data_points = np.hstack((data_points, cur_elem_data))

            # try:
            if data_points[0] is not None:
                primary_data[row_idx].append(config.primary_content_function(data_points))

                if config.secondary_content_function:
                    secondary_data[row_idx].append(config.secondary_content_function(data_points))
                else:
                    secondary_data[row_idx].append(None)
            else:
                primary_data[row_idx].append(None)
                secondary_data[row_idx].append(None)



    # plot the results
    n_rows = len(primary_data)
    n_columns = len(primary_data[0])
    if config.flip_rows_and_cols:
        n_rows = len(primary_data[0])
        n_columns = len(primary_data)
        tmp = row_headers
        row_headers = column_headers
        column_headers = tmp

    table_content = [[None] * (n_columns + 1) for _ in range(n_rows + 1)]
    table_content[0][0] = config.top_left_cell_content


    # set row and column headers
    for row_idx in range(n_rows):
        table_content[row_idx + 1][0] = row_headers[row_idx]
    for column_idx in range(n_columns):
        table_content[0][column_idx + 1] = column_headers[column_idx]

    # fill table
    for row_idx in range(n_rows):
        for column_idx in range(n_columns):

            data_1_idx = row_idx
            data_2_idx = column_idx
            if config.flip_rows_and_cols:
                data_1_idx = column_idx
                data_2_idx = row_idx

            if primary_data[data_1_idx][data_2_idx] is None:
                cell_data = ''
            else:
                if isinstance(config.cell_format, str):
                    cell_data = config.cell_format.format(
                        primary_data[data_1_idx][data_2_idx],
                        secondary_data[data_1_idx][data_2_idx])
                else:
                    cell_data = eu.misc.call_function_from_config(
                        config.cell_format,
                        primary_data[data_1_idx][data_2_idx],
                        secondary_data[data_1_idx][data_2_idx])

            table_content[row_idx + 1][column_idx + 1] = cell_data

    table = original_tabulate(
        table_content,
        headers='firstrow',
        **config.tabulate)

    return table

tabulate_pairwise

Plots a pairwise comparison between data from experiments based on a pairwise function d = f(exp_a, exp_b). By default it performs a Mann-WhitneyU P-Value test to identify significant differences between experiments.

tabulate_pairwise

Parameters:

Name Type Description Default
data list

Data to plot.

None
config dict

Dictionary with configuration of plot.

None

Configuration:

  • pairwise_function (function): Handle to function that computes the difference between the data of two experiments. Function format: func(exp1_data: nparray, exp2_data: nparray) -> scalar. Default is eu.misc.mannwhitneyu_pvalue.

  • pairwise_mode (str): Which pairs of experiments are compared? Possible values are 'full', 'full_not_identity', 'upper_triangle', 'upper_triangle_not_identiy', 'lower_triangle', 'lower_triangle_not_identiy'. Default is 'full'.

  • tabulate (dict): Parameters for the tabulate function that plots the table. See tabulate for all possible parameters. Some important ones:

    • tablefmt (str): Format of the table such as 'html', 'latex', or 'simple'. Default is 'html'.
    • numalign (str): Alignment of numbers in the table ('right', 'center', or 'left'). Default is 'right'.

    • cell_format (str): Format of the cell content. The format can take 1 number. Default is '{}'.

  • top_left_cell_content (str): Content of the top left cell which can be used as a label for the table. Default is ''.

Returns:

Name Type Description
fig figure

Plotly figure object that can be displayed using display(fig).

The plot is based on tabulate.

Source code in exputils/gui/jupyter/tabulate_pairwise.py
def tabulate_pairwise(data: Optional[list] = None,
                     config: Optional[dict] = None,
                     **kwargs):
    """
    Plots a pairwise comparison between data from experiments based on a pairwise function d = f(exp_a, exp_b).
    By default it performs a Mann-WhitneyU P-Value test to identify significant differences between experiments.

    <figure markdown="span">
          ![tabulate_pairwise](../assets/images/tabulate_pairwise.png){width="550"}
    </figure>

    Parameters:
        data (list): Data to plot.
        config (dict): Dictionary with configuration of plot.

    __Configuration__:

    - `pairwise_function` (`function`):
            Handle to function that computes the difference between the data of two experiments.
            Function format: `func(exp1_data: nparray, exp2_data: nparray) -> scalar`.
            Default is `eu.misc.mannwhitneyu_pvalue`.

    - `pairwise_mode` (`str`):
            Which pairs of experiments are compared?
            Possible values are `'full'`, `'full_not_identity'`, `'upper_triangle'`, `'upper_triangle_not_identiy'`, `'lower_triangle'`, `'lower_triangle_not_identiy'`.
            Default is `'full'`.

    - `tabulate` (`dict`): Parameters for the tabulate function that plots the table.
            See [tabulate](https://pypi.org/project/tabulate/) for all possible parameters.
            Some important ones:
        -  `tablefmt` (`str`):
            Format of the table such as `'html'`, `'latex'`, or `'simple'`.
            Default is `'html'`.
        - `numalign` (`str`): Alignment of numbers in the table (`'right'`, `'center'`, or `'left'`).
            Default is `'right'`.

        - `cell_format` (`str`):
            Format of the cell content. The format can take 1 number.
            Default is `'{}'`.

    - `top_left_cell_content` (`str`): Content of the top left cell which can be used as a label for the table.
            Default is `''`.

    Returns:
        fig (figure): Plotly figure object that can be displayed using `display(fig)`.

    The plot is based on [tabulate](https://pypi.org/project/tabulate/).
    """

    default_config = eu.AttrDict(

        pairwise_function = eu.misc.mannwhitneyu_pvalue,

        pairwise_mode = 'full',    # which pairs are compared? 'full', 'full_not_identity', 'upper_triangle', 'upper_triangle_not_identiy', 'lower_triangle', 'lower_triangle_not_identiy'

        tabulate=eu.AttrDict(
            tablefmt='html', #
            numalign='right',
        ),

        cell_format = '{}',

        top_left_cell_content = '',

        labels=[],  # holds all labels in a specific structure

    )
    config = eu.combine_dicts(kwargs, config, default_config)

    allowed_pairwise_modes = ['full', 'full_not_identity', 'upper_triangle', 'upper_triangle_not_identity', 'lower_triangle', 'lower_triangle_not_identity']
    if config.pairwise_mode not in allowed_pairwise_modes:
        raise ValueError('Unknown configuration {!r} for pairwise_mode! Allowed values: {}'.format(config.pairwise_mode, allowed_pairwise_modes))

    if data is None:
        data = np.array([])

    # format data in form [subplot_idx:list][trace_idx:list][elems_per_trace:numpy.ndarray]
    # subplot is a single table
    if isinstance(data, np.ndarray):
        data = [[data]]
    elif isinstance(data, list) and isinstance(data[0], np.ndarray):
        data = [data]
    elif not isinstance(data, list) and not isinstance(data[0], list) and not isinstance(data[0][0], np.ndarray):
        raise ValueError('Unsupported type of data!')

    # handle different input formats of labels
    if config.labels:
        # if only labels for mean-traces are given, then add an empty label for the sub figure
        if isinstance(config.labels, list) and not isinstance(config.labels[0], tuple):
            config.labels = [('', config.labels)]
        # if no labels are given for elements, then create an empty list for element labels
        for ds_idx in range(len(config.labels)):
            for trace_idx in range(len(config.labels[ds_idx][1])):
                if not isinstance(config.labels[ds_idx][1][trace_idx], tuple):
                    config.labels[ds_idx][1][trace_idx] = (config.labels[ds_idx][1][trace_idx], [])

    # identify the number of subplots
    n_subplots = len(data)

    if n_subplots > 1:
        raise NotImplementedError('Only supports 1 subplot!')

    # interate over subplots
    for subplot_idx, subplot_data in enumerate(data):

        trace_labels = []
        data_per_trace = []

        # collect the data and labels for each trace
        for trace_idx, cur_data in enumerate(subplot_data):

            # handle trace for mean values
            if config.labels:
                trace_label = config.labels[subplot_idx][1][trace_idx][0]
            else:
                trace_label = config.default_trace_label
                if len(config.trace_labels) > trace_idx:
                    trace_label = config.trace_labels[trace_idx]
            trace_label = eu.misc.replace_str_from_dict(str(trace_label), {'<trace_idx>': trace_idx})
            trace_labels.append(trace_label)

            data_points = np.array([])

            if np.ndim(cur_data) == 0:
                data_points = np.array([cur_data])
            elif np.ndim(cur_data) == 1:
                data_points = cur_data
            else:
                # collect data over elements
                for elem_idx, elem_data in enumerate(cur_data):  # data elements

                    # get element data which could be in matrix format or array format
                    if np.ndim(elem_data) == 0:
                        cur_elem_data = np.array([elem_data])
                    elif np.ndim(elem_data) == 1:
                        cur_elem_data = elem_data
                    elif np.ndim(elem_data) == 2:
                        if elem_data.shape[0] == 1:
                            cur_elem_data = elem_data[0, :]
                        elif elem_data.shape[1] == 1:
                            cur_elem_data = elem_data[1, 0]
                        else:
                            raise ValueError('Invalid data format!')
                    else:
                        raise ValueError('Invalid data format!')

                    data_points = np.hstack((data_points, cur_elem_data))

            data_per_trace.append(data_points)


        n_traces = len(data_per_trace)

        # compute the pairwise function of all needed combinations
        pairwise_data = np.full((n_traces,n_traces), np.nan)
        for first_trace_idx in range(n_traces):
            for second_trace_idx in range(n_traces):
                # decide if data has to be compared based on the config.pairwise_mode
                if _is_needed_pairwise_combination(first_trace_idx, second_trace_idx, config.pairwise_mode):
                    pairwise_data[first_trace_idx, second_trace_idx] = eu.misc.call_function_from_config(
                        config.pairwise_function,
                        data_per_trace[first_trace_idx],
                        data_per_trace[second_trace_idx],
                    )

        # plot the results
        row_shift = 1
        col_shift = 1
        n_rows_and_cols = n_traces + 1
        # we leave some header out for these two cases
        if config.pairwise_mode == 'upper_triangle_not_identity':
            col_shift = 0
            n_rows_and_cols = n_traces
        elif config.pairwise_mode == 'lower_triangle_not_identity':
            row_shift = 0
            n_rows_and_cols = n_traces

        table_content = [[None] * (n_rows_and_cols) for _ in range(n_rows_and_cols)]

        table_content[0][0] = config.top_left_cell_content

        # set top and side header
        for trace_idx in range(n_traces):
            if config.pairwise_mode == 'upper_triangle_not_identity':
                # top header
                if trace_idx > 0:
                    table_content[0][trace_idx + col_shift] = trace_labels[trace_idx]
                # side header
                if trace_idx < n_traces - 1:
                    table_content[trace_idx + row_shift][0] = trace_labels[trace_idx]

            elif config.pairwise_mode == 'lower_triangle_not_identity':
                # top header
                if trace_idx < n_traces - 1:
                    table_content[0][trace_idx + col_shift] = trace_labels[trace_idx]
                # side header
                if trace_idx > 0:
                    table_content[trace_idx + row_shift][0] = trace_labels[trace_idx]

            else:
                # top header
                table_content[0][trace_idx + col_shift] = trace_labels[trace_idx]
                # side header
                table_content[trace_idx + row_shift][0] = trace_labels[trace_idx]

        # fill table
        for first_trace_idx in range(n_traces):
            for second_trace_idx in range(n_traces):
                if _is_needed_pairwise_combination(first_trace_idx, second_trace_idx, config.pairwise_mode):

                    if isinstance(config.cell_format, str):
                        cell_data = config.cell_format.format(pairwise_data[first_trace_idx, second_trace_idx])
                    else:
                        cell_data = eu.misc.call_function_from_config(
                            config.cell_format,
                            pairwise_data[first_trace_idx, second_trace_idx])

                    table_content[first_trace_idx + row_shift][second_trace_idx + col_shift] = cell_data

        table = original_tabulate(
            table_content,
            headers='firstrow',
            **config.tabulate)

        return table