-
Notifications
You must be signed in to change notification settings - Fork 0
/
data_vis_gui.py
1750 lines (1374 loc) · 64.1 KB
/
data_vis_gui.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
Introduction
================
This module uses wxPython to create a GUI for visualizing 2D data
in both time domain plots and Bode plots. I am using this GUI to help
guide model refinement for DT-TMM modeling of dynamic systems, but it
should be useful for any situation in which time domain data needs to
be plotted quickly and easily. This GUI should be espeically useful
for dynamic system data where Bode plots and time domain plots are
used together.
Main Class
==============
The main GUI class is :py:class:`MyApp`.
Overview
============
Workflow
++++++++++++
The basic workflow in using the app would most likely be as follows:
- add a data file (txt or csv) to the workspace by pressing the
"Add File" button which calls the method :py:meth:`MyApp.on_add_file`
- assuming the users correctly selects a data file and presses OK,
the method :py:meth:`MyApp.load_data_file` is called. This method
shows a preview of the data in the :py:attr:`MyApp.preview_grid`
and loads the data file parameters into the text controls so that
the user can specifiy which columns to plot. The primary goal of
:py:meth:`MyApp.load_data_file` is to create an instance of the
class :py:class:`plot_description` from the data file. A
:py:class:`MyApp.load_data_file` instace specifies how to turn a
data file into a journal quality
plot. :py:meth:`MyApp.load_data_file` also sets the attribute
:py:attr:`MyApp.cur_plot_description`. Any changes to the various
text controls operate on the properties of
:py:attr:`MyApp.cur_plot_description`
- if a Bode plot will be generated, click the Bode tab of the
notebook control and specify the input and output channels
- note that only one Bode input and Bode pair is allowed for each
:py:class:`plot_description` instance; if you want to create
multiple Bode plots from a single data file, you will need to
duplicate the plot description for each additional input/output
pair
- optionally, at this point you could save the plot description using
the File>Save Plot Description (current) menu option. This menu
choice calls the method :py:meth:`MyApp.on_save_current_pd` which
opens a dialog and then calls the method
:py:meth:`plot_description.create_xml` for the current plot
description (:py:attr:`MyApp.cur_plot_description)`.
- add additional data files to the workspace as needed
- specify different combinations of files to overlay on a plot
- this is done mainly be selecting which plot descriptions to
include on the current plot by selecting or deselecting their
names in the list box :py:attr:`MyApp.plot_name_list_box`
- a set of selected plot descriptions can be set as a
:py:class:`figure` to allow the user to switch back and forth
between figures in a manner that is similar to tabbing between
open figures in pylab.
- Additionall, the active figure can be saved to an XML file.
Main Classes
++++++++++++++++
- :py:class:`MyApp` is the main GUI class
- :py:class:`plot_description` is the class for describing how to plot
a data file; a plot_description includes the path to the datafile,
which columns (labels) to plot on the time domain plot, how to map
the labels to time domain legend entries (using a dictionary
legend_dict), and which labels correspond to the input and output
for a Bode plot
- :py:class:`figure` represents a group of plot descriptions and the
other details needed to recreate one figure
- :py:class:`time_domain_figure` is a child of :py:class:`figure`
that specifically represents a time domain figure
- :py:class:`bode_figure` is another child of :py:class:`figure`
that represents a Bode figure
XML Praser Classes
++++++++++++++++++++++
- :py:class:`plot_description_file_parser` parses the XML of a file
containing one or more plot_descriptions
- :py:class:`figure_parser` parses the XML of a single figure
- :py:class:`gui_state_parser` parses an XML file that restores the
entire GUI state (all plot descriptions and figures, the selected
plot descriptions, ....)
- note that my XML parses will typically have a :py:meth:`parse`
method and :py:meth:`convert` method. The :py:meth:`parse` method
must be called first and will typically either return a list of
dictionaries or set some attributes of the class to either
dictionaries, lists of dictionaries, or text or other constants
Helper Classes
+++++++++++++++++++
- :py:class:`MyGridTable` is used to link the data from the txt data
file with the preview grid display
- :py:class:`figure_dialog` is a good example of using wxPython's xrc
stuff to create custom dialogs; this dialog helps the user
specify the name and figure number of a :py:class:`figure`
instance that is being created
To Do Items
==================
- It is not currently possible to load a data file that contains Bode
columns directly, i.e. you cannot load frequency, dB magnitude and
phase from a data file; it is expected that you load a time domain
file and calculate the Bode parameters from the time domain data.
It should be made possible to load frequency, dB or linear
magnitude, and phase directly from a data file.
- PhaseMassage is not currently supported
Autodoc Class and Method Documentation
==========================================
"""
from __future__ import print_function
# Used to guarantee to use at least Wx2.8
import wxversion
wxversion.ensureMinimal('2.8')
import sys, time, os, gc
import matplotlib
matplotlib.use('WXAgg')
import matplotlib.cm as cm
import matplotlib.cbook as cbook
from matplotlib.backends.backend_wxagg import Toolbar, FigureCanvasWxAgg
from matplotlib.figure import Figure
import numpy as np
import wx
import wx.xrc as xrc
import wx.grid
import wx_mpl_plot_panels as WMPP
import txt_data_processing
import copy
import pdb
data_wildcard = "Text files (*.txt; *.csv)|*.txt;*.csv|" \
"All files (*.*)|*.*"
xml_wildcard = "XML files (*.xml)|*.xml"
png_wildcard = "PNG files (*.png)|*.png"
eps_wildcard = "EPS files (*.eps)|*.eps"
import xml.etree.ElementTree as ET
from xml.dom import minidom
import xml_utils
from xml_utils import prettify
import wx_utils
class MyGridTable(wx.grid.PyGridTableBase):
"""Helper class to link the data from a csv or txt file to the
wxPython GUI wx.grid instance"""
def __init__(self, data):
wx.grid.PyGridTableBase.__init__(self)
self.data = data
def GetNumberRows(self):
"""Return the number of rows in the grid"""
return len(self.data)
def GetNumberCols(self):
"""Return the number of columns in the grid"""
row0 = self.data[0]
return len(row0)
def IsEmptyCell(self, row, col):
"""Return True if the cell is empty"""
return False
def GetTypeName(self, row, col):
"""Return the name of the data type of the value in the cell"""
return type(self.data[row][col])
def GetValue(self, row, col):
"""Return the value of a cell"""
return self.data[row][col]
def SetValue(self, row, col, value):
"""Set the value of a cell"""
self.data[row][col] = value
def parse_one_pd(pd_xml):
"""parse the XML for one :py:class:`plot_description` and return a
dictionary of the coresponding parameters for the class instance"""
assert pd_xml.tag == 'plot_description', \
"Child is not a valid plot_description xml chunk."
string_dict = xml_utils.children_to_dict(pd_xml)
out_dict = copy.copy(string_dict)
if type(out_dict['plot_labels']) in [str, unicode]:
out_dict['plot_labels'] = xml_utils.list_string_to_list(out_dict['plot_labels'])
if type(out_dict['legend_dict']) in [str, unicode]:
out_dict['legend_dict'] = xml_utils.dict_string_to_dict(out_dict['legend_dict'])
return out_dict
class plot_description_file_parser(xml_utils.xml_parser):
"""This parser will parse an XML file that may contain a list of
plot_description items."""
def parse(self):
"""Parse the XML associated with :py:attr:`self.root`. This
method creates a list of dictionaries where each dictionary
corresponds to one :py:class:`plot_description`. The list is
stored in :py:attr:`self.parsed_dicts`."""
assert self.root.tag == 'plot_description_file', \
"This does not appear to be a valide plot_description_file."
self.pd_xml_list = self.root.getchildren()
self.parsed_dicts = [parse_one_pd(item) for item in self.pd_xml_list]
def convert(self):
"""Convert the list of parse dictionaries in
:py:attr:`self.parsed_dicts` to a list of
:py:class:`plot_description` instances and save that list as
:py:attr:`self.pd_list`"""
self.pd_list = [plot_description(**kwargs) for \
kwargs in self.parsed_dicts]
return self.pd_list
## def __init__(self, filename):
## xml_utils.xml_parser.__init__(self, filename)
class figure_parser(plot_description_file_parser):
"""Parse an XML file containing a single figure."""
def validate_and_get_body(self):
"""verify that the XML in :py:attr:`self.root` is either from
an XML file containing a single figure or it is the time
domain or Bode figure portion of a saved full GUI description."""
if self.root.tag in ['time_domain_figure', 'bode_figure']:
body = self.root
return body
elif self.root.tag == 'figure':
children = self.root.getchildren()
#a figure file should have one child that is either a
#bode_figure or a time_domain_figure
assert len(children) == 1, "problem with the children in my xml file"
body = children[0]
return body
else:
raise ValueError("Not sure how to proceed for a figure with tag %s" % self.root.tag)
def parse(self):
"""convert the XML associated with self to a list of parsed
dicts and also find :py:attr:`self.name` and
:py:attr:`self.params`"""
body = self.validate_and_get_body()
self.class_name = body.tag
if self.class_name == 'time_domain_figure':
self.myclass = time_domain_figure
elif self.class_name == 'bode_figure':
self.myclass = bode_figure
else:
raise ValueError("Not sure what to do with figure type %s" % self.class_name)
name_xml = xml_utils.find_child(body, 'name')
self.name = name_xml.text.strip()
self.pd_xml_list = xml_utils.find_child(body, 'plot_description_list')
self.parsed_dicts = [parse_one_pd(item) for item in self.pd_xml_list]
params_xml = xml_utils.find_child(body, 'params')
self.params = xml_utils.children_to_dict(params_xml)
def convert(self):
"""convert the list of parse dictionaries in
:py:attr:`self.parsed_dicts to :py:class:`plot_description`
instances and then also create and return a :py:class:`figure`
instance"""
plot_description_file_parser.convert(self)
fig_instance = self.myclass(self.name, self.pd_list, **self.params)
return fig_instance
class gui_state_parser(plot_description_file_parser):
"""class to parse a GUI state from xml"""
def get_plot_descriptions(self):
self.pd_xml_list = xml_utils.find_child(self.root, 'plot_description_list')
def get_params(self):
self.params_xml = xml_utils.find_child(self.root, 'gui_params')
def get_figures(self):
self.fig_xml_list = xml_utils.find_child_if_it_exists(self.root, \
'figures')
def parse(self):
"""Parse the GUI state"""
assert self.root.tag == 'data_vis_gui_state', \
"This does not appear to be a valid data_vis_gui_state"
self.get_plot_descriptions()
self.parsed_dicts = [parse_one_pd(item) for item in self.pd_xml_list]
self.get_params()
self.params = xml_utils.children_to_dict(self.params_xml)
self.params['selected_inds'] = xml_utils.list_string_to_list(self.params['selected_inds'])
self.has_figs = False
self.get_figures()
if self.fig_xml_list is not None:
self.has_figs = True
fig_parsers = []
for fig_xml in self.fig_xml_list:
cur_parser = figure_parser(filename=None)
cur_parser.set_root(fig_xml)
cur_parser.parse()
fig_parsers.append(cur_parser)
self.fig_parsers = fig_parsers
def convert(self):
"""Convert the GUI state; generates a list of plot
descriptions and a list of figures (:py:meth:`self.fig_list`)"""
plot_description_file_parser.convert(self)
if self.has_figs:
fig_list = []
for fig_parser in self.fig_parsers:
cur_fig = fig_parser.convert()
fig_list.append(cur_fig)
self.fig_list = fig_list
def list_to_str(listin):
str_out = ''
first = True
for item in listin:
if first:
first = False
else:
str_out += ', '
str_out += str(item)
return str_out
class plot_description(xml_utils.xml_writer):
"""This class will be used to determine how to plot a certain data
file and how to save that plot description as an XML file. The
datapath passed into the :py:meth:`plot_description.__init__`
method is used to create a
:py:class:`txt_data_processing.Data_File` instance."""
def remove_n_and_t_from_plot_labels(self):
"""Data files from my PSoC or Arduino tests often have t and n
as the first two columns. It does not typically make sense to
plot these two columns."""
i = 0
while i < len(self.plot_labels):
label = self.plot_labels[i]
if label in ['n','t']:
self.plot_labels.pop(i)
else:
i += 1
def __init__(self, datapath, name='', plot_labels=None, legend_dict={}, \
legloc=1, bode_input_str=None, bode_output_str=None):
self.datapath = datapath
self.name = name
self.plot_labels = plot_labels
self.legend_dict = legend_dict
self.df = txt_data_processing.Data_File(datapath)
self.data = self.df.data
self.labels = self.df.labels
if plot_labels is None:
self.plot_labels = copy.copy(self.labels)
self.remove_n_and_t_from_plot_labels()
self.legend_dict = legend_dict
self.legloc = legloc
self.bode_input_str = bode_input_str
self.bode_output_str = bode_output_str
self.xml_tag_name = 'plot_description'
self.xml_attrs = ['name','datapath','plot_labels','legend_dict', \
'bode_input_str','bode_output_str']
def create_label_str(self):
"""convert the columns labels of the data file into a command
delimitted string that can be place in one text box"""
label_str = ''
first = True
for label in self.plot_labels:
if first:
first = False
else:
label_str += ', '
label_str += label
return label_str
def create_list_str(self, attr):
out_str = ''
if hasattr(self, attr):
mylist = getattr(self, attr)
if mylist:
out_str = list_to_str(mylist)
return out_str
def create_xlim_str(self):
return self.create_list_str('xlim')
def create_ylim_str(self):
return self.create_list_str('ylim')
def plot(self, ax, clear=False):
"""create a time domain plot by calling the
:py:meth:`Time_Plot` method of :py:attr:`self.df`, the
underlying :py:class:`txt_data_processing.Data_File` instance"""
self.df.Time_Plot(labels=self.plot_labels, ax=ax, clear=clear, \
legloc=self.legloc, legend_dict=self.legend_dict, \
)
## ylabel='Voltage (counts)', \
## basename=None, save=False, \
## ext='.png', fig_dir='', title=None, \
## linetypes=None, \
## **plot_opts)
def _create_legend_dict(self):
"""Create the initial legend_dict by setting the keys and
values to :py:attr:`self.plot_labels`"""
self.legend_dict = dict(zip(self.plot_labels, self.plot_labels))
def create_legend_str(self):
"""convert :py:attr:`self.legend_dict` to a comma and colon
delimited representation to place in a text control"""
if not self.legend_dict:
self._create_legend_dict()
leg_str = ''
first = True
for key, val in self.legend_dict.iteritems():
if first:
first = False
else:
leg_str += ', '
leg_str += '%s:%s' % (key, val)
return leg_str
def bode_plot(self, fig, **kwargs):
"""Generate a Bode plot by calling
:py:meth:`plot_description.df.bode_plot`, i.e. the
:py:meth:`bode_plot` method of the underlying
:py:class:`txt_data_processing.Data_File` instance"""
self.df.bode_plot(inlabel=self.bode_input_str, \
outlabel=self.bode_output_str, \
clear=False, \
fig=fig, **kwargs)
class figure(xml_utils.xml_writer):
"""Figures will be used to allow my app to switch quickly between
different sets of plotted data in a way that is analagous to
tabbing between different figures when using pylab/ipython.
Each figure instance needs to contain enough information to plot
one figure."""
def __init__(self, name, plot_descriptions, plot_type, **kwargs):
"""Is this always skipped? - yes"""
self.name = name
self.plot_descriptions = plot_descriptions
self.plot_type = plot_type
for key, val in kwargs.iteritems():
setattr(self, key, val)
def create_xml(self, root):
"""The figure class derives from
:py:class:`xml_utils.xml_writer`. This is the main method for
saving the instance to xml."""
fig_root = ET.SubElement(root, self.xml_tag_name)
dict1 = {'name':self.name}
xml_utils.append_dict_to_xml(fig_root, dict1)
pd_list_xml = ET.SubElement(fig_root, 'plot_description_list')
for pd in self.plot_descriptions:
pd.create_xml(pd_list_xml)
if self.xml_params:
params_xml = ET.SubElement(fig_root, 'params')
for attr in self.xml_params:
cur_xml = ET.SubElement(params_xml, attr)
attr_str = str(getattr(self, attr))
cur_xml.text = attr_str.encode()
class time_domain_figure(figure):
"""This is a sublcass of :py:class:`figure` that reprents a time
domain figure. In some sense, :py:class:`figure` was never meant
to be used directly (it is kind of a an abstract class). So, the
user should create either a :py:class:`time_domain_figure` or a
:py:class:`bode_figure`."""
def __init__(self, name, plot_descriptions, xlim=None, ylim=None, \
ylabel=None, xlabel=None, legloc=None):
figure.__init__(self, name, plot_descriptions, plot_type='time', \
xlim=xlim, ylim=ylim, \
xlabel=xlabel, ylabel=ylabel, legloc=legloc)
self.xml_tag_name = 'time_domain_figure'
self.xml_params = ['xlim','ylim','ylabel','xlabel','legloc']
class bode_figure(figure):
"""This is a subclass of :py:class:`figure` that represents a Bode
figure. Mainly, that means that the parameters that are saved to
XML are:py:attr:`freqlim`, :py:attr:`maglim`, and
:py:attr:`phaselim`."""
def __init__(self, name, plot_descriptions, \
freqlim=None, maglim=None, phaselim=None):
figure.__init__(self, name, plot_descriptions, plot_type='bode', \
freqlim=freqlim, maglim=maglim, phaselim=phaselim)
self.xml_tag_name = 'bode_figure'
self.xml_params = ['freqlim','maglim','phaselim']
class figure_dialog(wx.Dialog):
"""Dialog to set a group of plot descriptions as a
:py:class:`figure` instance. The dialog prompts the user for a
figure name and number. The number sets the hotkey on the figure
menu for switching to that plot. Note that no attempt is made to
check if the user is overwriting an existing figure on the menu.
Note that this class uses wxPython xrc to create a dialog within
an app that is created from a different wxPython xrc file. I am
using the wxPython two stage creation approach (sort of, I guess).
That is what the webpage I found this on said and that is what the
pre and post stuff does."""
def __init__(self, parent):
pre = wx.PreDialog()
self.PostCreate(pre)
res = xrc.XmlResource('figure_name_dialog.xrc')
res.LoadOnDialog(self, None, "main_dialog")
self.Bind(wx.EVT_BUTTON, self.on_ok, xrc.XRCCTRL(self, "ok_button"))
self.Bind(wx.EVT_BUTTON, self.on_cancel, xrc.XRCCTRL(self, "cancel_button"))
self.figure_name_ctrl = xrc.XRCCTRL(self, "figure_name_ctrl")
self.figure_number_ctrl = xrc.XRCCTRL(self, "figure_number_ctrl")
self.figure_number_ctrl.Bind(wx.EVT_TEXT_ENTER, self.on_enter)
def on_ok(self, event):
"""Close and return wx.ID_OK if the user clicks OK"""
self.EndModal(wx.ID_OK)
def on_enter(self, event):
"""Validate the input by looking for an integer in the figure
number text control and a non-empty string in the figure name
text control. If both of these are valid, close the dialog
and return wx.ID_OK."""
name = self.figure_name_ctrl.GetValue()
fig_num_str = self.figure_number_ctrl.GetValue()
try:
fig_num = int(fig_num_str)
valid_num = True
except:
valid_num = False
if valid_num and name:
self.EndModal(wx.ID_OK)
def on_cancel(self, event):
"""Close the dialog and return wx.ID_CANCEL"""
self.EndModal(wx.ID_CANCEL)
class MyApp(wx.App):
"""
Main methods:
- :py:meth:`MyApp.load_data_file` loads a data file, converts it to
a :py:class:`plot_description`, shows a preview of the data in
the wx.grid :py:attr:`MyApp.preview_grid`, adds the file
parameters to the text controls, ...
- :py:meth:`MyApp.on_set_as_fig_button` creates a
:py:class:`figure` instance based on the currently selected plot
description names and the results of the user input into the
:py:class:`figure_dialog`
- :py:meth:`MyApp.set_active_figure` sets the active figure for
the GUI based on an index ind that is passed in
- :py:meth:`MyApp.change_fig` is the method that is called
whenever the user chooses a Figure number to activate (from the
Figure menu or a hotkey). The method determines which Figure
number to go to and then passes the ind to
:py:meth:`MyApp.set_active_figure`
- :py:meth:`MyApp._update_plot` is the background method called
whenever the GUI needs to redraw the plot. It checks to see
whether the time domain or Bode tab of the notebook is selected
and then calls either :py:meth:`MyApp.plot_all_td` or
:py:meth:`MyApp.plot_all_bode`
- Plotting Methods:
- :py:meth:`MyApp.plot_all_td` plots the time domain graph for
all selected plot_descriptions
- :py:meth:`MyApp.plot_td` plots a list of time domain
plot_descriptions given a list of inds (called by
:py:meth:`MyApp.plot_all_td`)
- :py:meth:`MyApp.plot_time_domain` plots one time domain
plot_description (called by :py:meth:`MyApp.plot_td`)
- :py:meth:`MyApp.plot_all_bode` plots the Bode plots for all
selected plot_descriptions
- :py:meth:`MyApp.plot_bodes` plot multiple Bode given a list of
inds
- :py:meth:`MyApp.plot_bode` plots one Bode plot (called by
:py:meth:`MyApp.plot_bodes` indirectly)
- :py:meth:`MyApp.plot_inds` is supposed to be used by both
:py:meth:`MyApp.plot_td` and :py:meth:`MyApp.plot_bodes` to
plot either time domain or Bode based on a list of inds. I
think only :py:meth:`MyApp.plot_bodes` uses it currently.
"""
def get_plot_description_ind(self, pd):
"""Find the ind of plot_description pd by looking for its name
in :py:attr:`self.plot_name_list_box`."""
items = self.plot_name_list_box.GetItems()
key = pd.name
ind = items.index(key)
return ind
def get_plot_description_inds(self, pd_list):
"""Get a list of inds by calling
:py:meth:`MyApp.get_plot_description_ind` for each
plot_description in pd_list."""
ind_list = [self.get_plot_description_ind(pd) for pd in pd_list]
return ind_list
def set_selected_plot_descriptions(self, pd_list):
"""Deselect all the plot_descriptions in
:py:attr:`MyApp.plot_name_list_box` and then select only those
in pd_list."""
self.plot_name_list_box.DeselectAll()
ind_list = self.get_plot_description_inds(pd_list)
for ind in ind_list:
self.plot_name_list_box.Select(ind)
def find_fig_ind(self, fig_name):
"""Search :py:attr:`MyApp.figure_list` to find one whose names
matches fig_name."""
found = 0
for i, fig in enumerate(self.figure_list):
if fig.name == fig_name:
found = 1
return i
if not found:
return None
def set_active_figure(self, ind):
"""Set the active figure to
:py:attr:`MyApp.figure_list[ind]`. This includes specifying
which plot_descriptions are active, setting whether the plot
is time domain or Bode, and then updating the plot."""
self.active_fig = self.figure_list[ind]
self.set_selected_plot_descriptions(self.active_fig.plot_descriptions)
if type(self.active_fig) == time_domain_figure:
sel = 0
elif type(self.active_fig) == bode_figure:
sel = 1
self.td_bode_notebook.SetSelection(sel)
self._update_plot()
def on_switch_to_bode(self,event):
"""Respond to the Switch to Bode hotkey or menu item and
switch to a Bode plot"""
self.td_bode_notebook.SetSelection(1)
#check for Bode readiness:
inds = self.get_selected_plot_inds()
safe_to_plot = True
for ind in inds:
pd = self.get_plot_description_from_ind(ind)
if (not pd.bode_input_str) or (not pd.bode_output_str):
safe_to_plot = False
break
if safe_to_plot:
self._update_plot()
def on_switch_to_time_domain(self,event):
"""Respond to the Switch to Time Domain hotkey or menu item
and switch to a time domain plot"""
self.td_bode_notebook.SetSelection(0)
self._update_plot()
def change_fig(self, event):
"""This is the method called whenever the user selects a
certain Figure number. The method determines which Figure ind
to go to by searching :py:attr:`MyApp.figure_menu_ids` for the
id that matches the id of the event.
:py:meth:`MyApp.set_active_figure` is then called with that ind."""
eid = event.GetId()
#print('in change_fig, Id=%s' % eid)
ind = self.figure_menu_ids.index(eid)
#print('ind = %i' % ind)
self.set_active_figure(ind)
def get_axis(self):
"""Get or create the main axis for time domain plots"""
fig = self.plotpanel.fig
if len(fig.axes) == 0:
ax = fig.add_subplot(111)
else:
ax = fig.axes[0]
return ax
def get_fig(self):
"""Get the figure instance from :py:attr:`MyApp.plotpanel`"""
fig = self.plotpanel.fig
return fig
def plot_time_domain(self, plot_descript, clear=False, draw=True):
"""This is the underlying method for plotting one time domain
plot description."""
fig = self.get_fig()
if clear:
fig.clf()
ax = self.get_axis()
plot_descript.plot(ax)
if draw:
self.plotpanel.canvas.draw()
def get_plot_description_from_ind(self, ind):
"""Given the ind for a plot descrition (such as from a list of
selected plot_descriptions from an XML file), get the
:py:class:`plot_description` instance from
:py:attr:`MyApp.plot_dict`."""
key = self.plot_list[ind]
pd = self.plot_dict[key]
return pd
def plot_inds(self, inds, plot_method):
"""This is the underlying method for plotting either time
domain or Bode plots for the plot descriptions corresponding
to inds (inds refers to the indices in
:py:attr:`MyApp.plot_name_list_box`"""
for ind in inds:
pd = self.get_plot_description_from_ind(ind)
plot_method(pd, clear=False, draw=False)
self.plotpanel.canvas.draw()
def plot_td(self, inds):
"""Given a list of inds, plot the corresponding time domain
graph for each :py:class:`plot_description`."""
self.plot_inds(inds, self.plot_time_domain)
def get_selected_plot_inds(self):
"""Get the indices for the plot descriptions that are
currently selected in :py:attr:`MyApp.plot_name_list_box`"""
all_items = self.plot_name_list_box.GetItems()
inds = self.plot_name_list_box.GetSelections()
return inds
def clear_fig(self):
"""Clear the plot figure"""
fig = self.get_fig()
fig.clf()
def _get_list_from_textctrl(self, widget):
mystr = widget.GetValue()
mylist = xml_utils.list_string_to_list(mystr)
return mylist
def set_td_xlim_ylim(self):
xlim = self._get_list_from_textctrl(self.xlim_textctrl)
ylim = self._get_list_from_textctrl(self.ylim_textctrl)
ax = self.get_axis()
redraw = False
if xlim:
ax.set_xlim(xlim)
redraw = True
print('type(ylim) = ' + str(type(ylim)))
print('ylim = ' + str(ylim))
if ylim:
ax.set_ylim(ylim)
redraw = True
if redraw:
self.plotpanel.canvas.draw()
def plot_all_td(self):
"""Plot the time domain graphs for all selected plot descriptions"""
self.clear_fig()
inds = self.get_selected_plot_inds()
if len(inds) > 0:
self.plot_td(inds)
self.set_td_xlim_ylim()
def plot_all_bode(self):
"""Plot the Bode plots for all selected plot descriptions"""
self.clear_fig()
inds = self.get_selected_plot_inds()
if len(inds) > 0:
self.plot_bodes(inds)
def plot_bodes(self, inds):
"""Plot the Bode plots for the plot descriptions corresponding
to inds"""
self.plot_inds(inds, self.plot_bode)
def plot_bode(self, plot_descript, clear=True, draw=True):
"""This method plots one Bode plot for one plot_description"""
fig = self.get_fig()
if clear:
fig.clf()
plot_descript.bode_plot(fig)
if draw:
self.plotpanel.canvas.draw()
def set_labels_ctrl(self):
"""Set the value of :py:attr:`MyApp.label_text_ctrl` to the
comma delimited string from the labels of
:py:attr:`MyApp.cur_plot_description`"""
label_str = self.cur_plot_description.create_label_str()
self.label_text_ctrl.SetValue(label_str)
def set_legend_str(self):
"""Set the value of :py:attr:`MyApp.legend_dict_ctrl` to the
string corresponing to the legend_dict of
:py:attr:`MyApp.cur_plot_description`"""
legend_str = self.cur_plot_description.create_legend_str()
self.legend_dict_ctrl.SetValue(legend_str)
def get_label_str(self):
"""Pull the label string out of
:py:attr:`MyApp.label_text_ctrl`"""
return self.label_text_ctrl.GetValue()
def get_legend_str(self):
"""Pull the legend string out of
:py:attr:`MyApp.legend_dict_ctrl`"""
return self.legend_dict_ctrl.GetValue()
def parse_label_str(self):
"""Convert a comma delimited label string back into a list of
labels"""
label_list = self.get_label_str().split(',')
plot_labels = [label.strip() for label in label_list]
return plot_labels
def parse_legend_str(self):
"""Convert a legend dictionary string back into a dictionary"""
legend_list = self.get_legend_str().split(',')
legend_dict = {}
for cur_str in legend_list:
key, val = cur_str.split(':',1)
key = key.strip()
val = val.strip()
val = val.replace('\\\\','\\')
legend_dict[key] = val
return legend_dict
def load_data_file(self, datapath, name=''):
"""Load a data file from a txt or csv file and create a
:py:class:`plot_description` instance from it. Set this
:py:attr:`plot_description` as
:py:attr:`MyApp.cur_plot_description.`
This method also puts loads the data from the datafile into
:py:attr:`MyApp.preview_grid.`"""
self.cur_plot_description = plot_description(datapath, name)
cpd = self.cur_plot_description
print('shape = %s, %s' % cpd.data.shape)
self.set_labels_ctrl()
self.set_legend_str()
self.data = [cpd.labels] + cpd.data.tolist()
self.table = MyGridTable(self.data)
self.preview_grid.SetTable(self.table)
def _update_plot(self):
"""Simply plot the time domain or Bode plots without updating
the GUI or checking for changes in labels or legend stuff. To
be used after loading xml files or in other instances where
the GUI was just set programmatically and the plot needs to be
redrawn."""
sel = self.td_bode_notebook.GetSelection()
if sel == 0:
self.plot_all_td()
elif sel == 1:
self.plot_all_bode()
def get_legloc(self):
legloc = None
try:
legloc = int(self.legloc_ctrl.GetValue())
except ValueError:
legstr = self.legloc_ctrl.GetValue()
if legstr.find(',') > -1:
legloc = xml_utils.list_string_to_list(legstr)