captest 0.13.3rc1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
captest/plotting.py ADDED
@@ -0,0 +1,505 @@
1
+ from pathlib import Path
2
+ import copy
3
+ import json
4
+ import warnings
5
+ import itertools
6
+ from functools import partial
7
+ import importlib
8
+
9
+ import numpy as np
10
+ import pandas as pd
11
+ from bokeh.models import NumeralTickFormatter
12
+
13
+ from .util import tags_by_regex, read_json
14
+
15
+ pn_spec = importlib.util.find_spec("panel")
16
+ if pn_spec is not None:
17
+ import panel as pn
18
+
19
+ pn.extension()
20
+ # disable error messages for panel dashboard
21
+ pn.config.console_output = "disable"
22
+ else:
23
+ warnings.warn(
24
+ "The ReportingIrradiance.dashboard method will not work without "
25
+ "the panel package."
26
+ )
27
+
28
+ hv_spec = importlib.util.find_spec("holoviews")
29
+ if hv_spec is not None:
30
+ import holoviews as hv
31
+ from holoviews import opts
32
+ else:
33
+ warnings.warn("The plotting methods will not work without the holoviews package.")
34
+
35
+ COMBINE = {
36
+ "poa_ghi": "irr.*(poa|ghi)$",
37
+ "poa_csky": "(?=.*poa)(?=.*irr)",
38
+ "ghi_csky": "(?=.*ghi)(?=.*irr)",
39
+ "temp_amb_bom": "(?=.*temp)((?=.*amb)|(?=.*bom))",
40
+ "inv_sum_mtr_pwr": ["(?=.*real)(?=.*pwr)(?=.*mtr)", "(?=.*pwr)(?=.*agg)"],
41
+ }
42
+
43
+ DEFAULT_GROUPS = [
44
+ "inv_sum_mtr_pwr",
45
+ "(?=.*real)(?=.*pwr)(?=.*inv)",
46
+ "(?=.*real)(?=.*pwr)(?=.*mtr)",
47
+ "poa_ghi",
48
+ "poa_csky",
49
+ "ghi_csky",
50
+ "temp_amb_bom",
51
+ ]
52
+
53
+
54
+ def find_default_groups(groups, default_groups):
55
+ """
56
+ Find the default groups in the list of groups.
57
+
58
+ Parameters
59
+ ----------
60
+ groups : list of str
61
+ The list of groups to search for the default groups.
62
+ default_groups : list of str
63
+ List of regex strings to use to identify default groups.
64
+
65
+ Returns
66
+ -------
67
+ list of str
68
+ The default groups found in the list of groups.
69
+ """
70
+ found_groups = []
71
+ for re_str in default_groups:
72
+ found_grp = tags_by_regex(groups, re_str)
73
+ if len(found_grp) == 1:
74
+ found_groups.append(found_grp[0])
75
+ elif len(found_grp) > 1:
76
+ warnings.warn(
77
+ f"More than one group found for regex string {re_str}. "
78
+ "Refine regex string to find only one group. "
79
+ f"Groups found: {found_grp}"
80
+ )
81
+ return found_groups
82
+
83
+
84
+ def parse_combine(combine, column_groups=None, data=None, cd=None):
85
+ """
86
+ Parse regex strings for identifying groups of columns or tags to combine.
87
+
88
+ Parameters
89
+ ----------
90
+ combine : dict
91
+ Dictionary of group names and regex strings to use to identify groups from
92
+ column groups and individual tags (columns) to combine into new groups.
93
+ Keys should be strings for names of new groups. Values should be either a string
94
+ or a list of two strings. If a string, the string is used as a regex to identify
95
+ groups to combine. If a list, the first string is used to identify groups to
96
+ combine and the second is used to identify individual tags (columns) to combine.
97
+ column_groups : ColumnGroups, optional
98
+ The column groups object to add new groups to. Required if `cd` is not provided.
99
+ data : pd.DataFrame, optional
100
+ The data to use to identify groups and columns to combine. Required if `cd` is
101
+ not provided.
102
+ cd : captest.CapData, optional
103
+ The captest.CapData object with the `data` and `column_groups` attributes set.
104
+ Required if `columng_groups` and `data` are not provided.
105
+
106
+ Returns
107
+ -------
108
+ ColumnGroups
109
+ New column groups object with new groups added.
110
+ """
111
+ if cd is not None:
112
+ data = cd.data
113
+ column_groups = cd.column_groups
114
+ cg_out = copy.deepcopy(column_groups)
115
+ orig_groups = list(cg_out.keys())
116
+
117
+ tags = list(data.columns)
118
+
119
+ for grp_name, re_str in combine.items():
120
+ group_re = None
121
+ tag_re = None
122
+ tags_in_matched_groups = []
123
+ matched_tags = []
124
+ if isinstance(re_str, str):
125
+ group_re = re_str
126
+ elif isinstance(re_str, list):
127
+ if len(re_str) != 2:
128
+ warnings.warn(
129
+ "When passing a list of regex. There should be two strings. One for "
130
+ "identifying groups and one for identifying individual tags (columns)."
131
+ )
132
+ return None
133
+ else:
134
+ group_re = re_str[0]
135
+ tag_re = re_str[1]
136
+ if group_re is not None:
137
+ matched_groups = tags_by_regex(orig_groups, group_re)
138
+ if len(matched_groups) >= 1:
139
+ tags_in_matched_groups = list(
140
+ itertools.chain(*[cg_out[grp] for grp in matched_groups])
141
+ )
142
+ if tag_re is not None:
143
+ matched_tags = tags_by_regex(tags, tag_re)
144
+ cg_out[grp_name] = tags_in_matched_groups + matched_tags
145
+ return cg_out
146
+
147
+
148
+ def msel_from_column_groups(column_groups, groups=True):
149
+ """
150
+ Create a multi-select widget from a column groups object.
151
+
152
+ Parameters
153
+ ----------
154
+ column_groups : ColumnGroups
155
+ The column groups object.
156
+ groups : bool, default True
157
+ By default creates list of groups i.e. the keys of `column_groups`,
158
+ otherwise creates list of individual columns i.e. the values of `column_groups`
159
+ concatenated together.
160
+ """
161
+ if groups:
162
+ keys = list(column_groups.data.keys())
163
+ keys.sort()
164
+ options = {k: column_groups.data[k] for k in keys}
165
+ name = "Groups"
166
+ value = column_groups.data[list(column_groups.keys())[0]]
167
+ else:
168
+ options = []
169
+ for columns in column_groups.values():
170
+ options += columns
171
+ options.sort()
172
+ name = "Columns"
173
+ value = [options[0]]
174
+ return pn.widgets.MultiSelect(
175
+ name=name, value=value, options=options, size=8, height=400, width=400
176
+ )
177
+
178
+
179
+ def plot_tag(data, tag, width=1500, height=250):
180
+ if len(tag) == 1:
181
+ plot = hv.Curve(data[tag])
182
+ elif len(tag) > 1:
183
+ curves = {}
184
+ for column in tag:
185
+ try:
186
+ curves[column] = hv.Curve(data[column])
187
+ except KeyError:
188
+ continue
189
+ plot = hv.NdOverlay(curves)
190
+ elif len(tag) == 0:
191
+ plot = hv.Curve(
192
+ pd.DataFrame({"no_data": [np.nan] * data.shape[0]}, index=data.index)
193
+ )
194
+ plot.opts(
195
+ opts.Curve(
196
+ line_width=1,
197
+ width=width,
198
+ height=height,
199
+ muted_alpha=0,
200
+ tools=["hover"],
201
+ yformatter=NumeralTickFormatter(format="0,0"),
202
+ ),
203
+ opts.NdOverlay(
204
+ width=width, height=height, legend_position="right", batched=False
205
+ ),
206
+ )
207
+ return plot
208
+
209
+
210
+ def group_tag_overlay(group_tags, column_tags):
211
+ """
212
+ Overlay curves of groups and individually selected columns.
213
+
214
+ Parameters
215
+ ----------
216
+ group_tags : list of str
217
+ The tags to plot from the groups selected.
218
+ column_tags : list of str
219
+ The tags to plot from the individually selected columns.
220
+ """
221
+ joined_tags = [t for tag_list in group_tags for t in tag_list] + column_tags
222
+ return joined_tags
223
+
224
+
225
+ def plot_group_tag_overlay(data, group_tags, column_tags, width=1500, height=400):
226
+ """
227
+ Overlay curves of groups and individually selected columns.
228
+
229
+ Parameters
230
+ ----------
231
+ data : pd.DataFrame
232
+ The data to plot.
233
+ group_tags : list of str
234
+ The tags to plot from the groups selected.
235
+ column_tags : list of str
236
+ The tags to plot from the individually selected columns.
237
+ """
238
+ joined_tags = group_tag_overlay(group_tags, column_tags)
239
+ return plot_tag(data, joined_tags, width=width, height=height)
240
+
241
+
242
+ def plot_tag_groups(data, tags_to_plot, width=1500, height=250):
243
+ """
244
+ Plot groups of tags, one of overlayed curves per group.
245
+
246
+ Parameters
247
+ ----------
248
+ data : pd.DataFrame
249
+ The data to plot.
250
+ tags_to_plot : list
251
+ List of lists of strings. One plot for each inner list.
252
+ """
253
+ group_plots = []
254
+ if len(tags_to_plot) == 0:
255
+ tags_to_plot = [[]]
256
+ for group in tags_to_plot:
257
+ plot = plot_tag(data, group, width=width, height=height)
258
+ group_plots.append(plot)
259
+ return hv.Layout(group_plots).cols(1)
260
+
261
+
262
+ def filter_list(text_input, ms_to_filter, names, event=None):
263
+ """
264
+ Filter a multi-select widget by a regex string.
265
+
266
+ Parameters
267
+ ----------
268
+ text_input : pn.widgets.TextInput
269
+ The text input widget to get the regex string from.
270
+ ms_to_filter : pn.widgets.MultiSelect
271
+ The multi-select widget to update.
272
+ names : list of str
273
+ The list of names to filter.
274
+ event : pn.widgets.event, optional
275
+ Passed by the `param.watch` method. Not used.
276
+
277
+ Returns
278
+ -------
279
+ None
280
+ """
281
+ if text_input.value == "":
282
+ re_value = ".*"
283
+ else:
284
+ re_value = text_input.value
285
+ names_ = copy.deepcopy(names)
286
+ if isinstance(names_, dict):
287
+ selected_groups = tags_by_regex(list(names_.keys()), re_value)
288
+ selected_groups.sort()
289
+ options = {k: names_[k] for k in selected_groups}
290
+ else:
291
+ options = tags_by_regex(names_, re_value)
292
+ options.sort()
293
+ ms_to_filter.param.update(options=options)
294
+
295
+
296
+ def scatter_dboard(data, **kwargs):
297
+ """
298
+ Create a dashboard to plot any two columns of data against each other.
299
+
300
+ Parameters
301
+ ----------
302
+ data : pd.DataFrame
303
+ The data to plot.
304
+ **kwargs : optional
305
+ Pass additional keyword arguments to the holoviews options of the scatter plot.
306
+
307
+ Returns
308
+ -------
309
+ pn.Column
310
+ The dashboard with a scatter plot of the data.
311
+ """
312
+ cols = list(data.columns)
313
+ cols.sort()
314
+ x = pn.widgets.Select(name="x", value=cols[0], options=cols)
315
+ y = pn.widgets.Select(name="y", value=cols[1], options=cols)
316
+ # slope = pn.widgets.Checkbox(name='Slope', value=False)
317
+
318
+ defaults = {
319
+ "width": 500,
320
+ "height": 500,
321
+ "fill_alpha": 0.4,
322
+ "line_alpha": 0,
323
+ "size": 4,
324
+ "yformatter": NumeralTickFormatter(format="0,0"),
325
+ "xformatter": NumeralTickFormatter(format="0,0"),
326
+ }
327
+ for opt, value in defaults.items():
328
+ kwargs.setdefault(opt, value)
329
+
330
+ def scatter(data, x, y, slope=True, **kwargs):
331
+ scatter_plot = hv.Scatter(data, x, y).opts(**kwargs)
332
+ # if slope:
333
+ # slope_line = hv.Slope.from_scatter(scatter_plot).opts(
334
+ # line_color='red',
335
+ # line_width=1,
336
+ # line_alpha=0.4,
337
+ # line_dash=(5,3)
338
+ # )
339
+ # if slope:
340
+ # return scatter_plot * slope_line
341
+ # else:
342
+ return scatter_plot
343
+
344
+ # dboard = pn.Column(
345
+ # pn.Row(x, y, slope),
346
+ # pn.bind(scatter, data, x, y, slope=slope, **kwargs)
347
+ # )
348
+ dboard = pn.Column(pn.Row(x, y), pn.bind(scatter, data, x, y, **kwargs))
349
+ return dboard
350
+
351
+
352
+ def plot(
353
+ cd=None,
354
+ cg=None,
355
+ data=None,
356
+ combine=COMBINE,
357
+ default_groups=DEFAULT_GROUPS,
358
+ group_width=1500,
359
+ group_height=250,
360
+ **kwargs,
361
+ ):
362
+ """
363
+ Create plotting dashboard.
364
+
365
+ NOTE: If a 'plot_defaults.json' file exists in the same directory as the file this
366
+ function is called from called, then the default groups will be read from that file
367
+ instead of using the `default_groups` argument. Delete or manually edit the file to
368
+ change the default groups. Use the `default_groups` or manually edit the file to
369
+ control the order of the plots.
370
+
371
+ Parameters
372
+ ----------
373
+ cd : captest.CapData, optional
374
+ The captest.CapData object.
375
+ cg : captest.ColumnGroups, optional
376
+ The captest.ColumnGroups object. `data` must also be provided.
377
+ data : pd.DataFrame, optional
378
+ The data to plot. `cg` must also be provided.
379
+ combine : dict, optional
380
+ Dictionary of group names and regex strings to use to identify groups from
381
+ column groups and individual tags (columns) to combine into new groups. See the
382
+ `parse_combine` function for more details.
383
+ default_groups : list of str, optional
384
+ List of regex strings to use to identify default groups to plot. See the
385
+ `find_default_groups` function for more details.
386
+ group_width : int, optional
387
+ The width of the plots on the Groups tab.
388
+ group_height : int, optional
389
+ The height of the plots on the Groups tab.
390
+ **kwargs : optional
391
+ Pass additional keyword arguments to the holoviews options of the scatter plot
392
+ on the 'Scatter' tab.
393
+ """
394
+ if cd is not None:
395
+ data = cd.data
396
+ cg = cd.column_groups
397
+ # make sure data is numeric
398
+ data = data.apply(pd.to_numeric, errors="coerce")
399
+ bool_columns = data.select_dtypes(include="bool").columns
400
+ data.loc[:, bool_columns] = data.loc[:, bool_columns].astype(int)
401
+ # setup custom plot for 'Custom' tab
402
+ groups = msel_from_column_groups(cg)
403
+ tags = msel_from_column_groups({"all_tags": list(data.columns)}, groups=False)
404
+ columns_re_input = pn.widgets.TextInput(name="Input regex to filter columns list")
405
+ groups_re_input = pn.widgets.TextInput(name="Input regex to filter groups list")
406
+
407
+ columns_re_input.param.watch(
408
+ partial(filter_list, columns_re_input, tags, tags.options), "value"
409
+ )
410
+ groups_re_input.param.watch(
411
+ partial(filter_list, groups_re_input, groups, groups.options), "value"
412
+ )
413
+
414
+ custom_plot_name = pn.widgets.TextInput()
415
+ update = pn.widgets.Button(name="Update")
416
+ width_custom = pn.widgets.IntInput(
417
+ name="Plot Width", value=1500, start=200, end=2800, step=100, width=200
418
+ )
419
+ height_custom = pn.widgets.IntInput(
420
+ name="Plot height", value=400, start=150, end=800, step=50, width=200
421
+ )
422
+ custom_plot = pn.Column(
423
+ pn.Row(custom_plot_name, update, width_custom, height_custom),
424
+ pn.Row(
425
+ pn.WidgetBox(groups_re_input, groups),
426
+ pn.WidgetBox(columns_re_input, tags),
427
+ ),
428
+ pn.Row(
429
+ pn.bind(
430
+ plot_group_tag_overlay,
431
+ data,
432
+ groups,
433
+ tags,
434
+ width=width_custom,
435
+ height=height_custom,
436
+ )
437
+ ),
438
+ )
439
+
440
+ # setup group plotter for 'Main' tab
441
+ cg_layout = parse_combine(combine, column_groups=cg, data=data)
442
+ main_ms = msel_from_column_groups(cg_layout)
443
+
444
+ def add_custom_plot_group(event):
445
+ column_groups_ = copy.deepcopy(main_ms.options)
446
+ column_groups_ = add_custom_plot(
447
+ custom_plot_name.value,
448
+ column_groups_,
449
+ groups.value,
450
+ tags.value,
451
+ )
452
+ main_ms.options = column_groups_
453
+
454
+ update.on_click(add_custom_plot_group)
455
+ plots_to_layout = pn.widgets.Button(name="Set plots to current layout")
456
+ width_main = pn.widgets.IntInput(
457
+ name="Plot Width", value=1500, start=200, end=2800, step=100, width=200
458
+ )
459
+ height_main = pn.widgets.IntInput(
460
+ name="Plot height", value=250, start=150, end=800, step=50, width=200
461
+ )
462
+ main_plot = pn.Column(
463
+ pn.Row(pn.WidgetBox(plots_to_layout, main_ms, pn.Row(width_main, height_main))),
464
+ pn.Row(
465
+ pn.bind(
466
+ plot_tag_groups, data, main_ms, width=width_main, height=height_main
467
+ )
468
+ ),
469
+ )
470
+
471
+ def set_defaults(event):
472
+ with open("./plot_defaults.json", "w") as file:
473
+ json.dump(main_ms.value, file)
474
+
475
+ plots_to_layout.on_click(set_defaults)
476
+
477
+ # setup default groups
478
+ if Path("./plot_defaults.json").exists():
479
+ default_tags = read_json("./plot_defaults.json")
480
+ else:
481
+ default_groups = find_default_groups(list(cg_layout.keys()), default_groups)
482
+ default_tags = [cg_layout.get(grp, []) for grp in default_groups]
483
+
484
+ # layout dashboard
485
+ plotter = pn.Tabs(
486
+ (
487
+ "Groups",
488
+ plot_tag_groups(data, default_tags, width=group_width, height=group_height),
489
+ ),
490
+ ("Layout", main_plot),
491
+ ("Overlay", custom_plot),
492
+ ("Scatter", scatter_dboard(data, **kwargs)),
493
+ )
494
+ return plotter
495
+
496
+
497
+ def add_custom_plot(name, column_groups, group_tags, column_tags):
498
+ """
499
+ Append a new custom group to column groups for plotting.
500
+
501
+ Parameters
502
+ ----------
503
+ """
504
+ column_groups[name] = group_tag_overlay(group_tags, column_tags)
505
+ return column_groups