-
Notifications
You must be signed in to change notification settings - Fork 13
Expand file tree
/
Copy pathplot_view.py
More file actions
3131 lines (2237 loc) · 127 KB
/
plot_view.py
File metadata and controls
3131 lines (2237 loc) · 127 KB
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
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import os
import re
import copy
import platform
import functools
import itertools
from fractions import Fraction
import numpy as np
import matplotlib as mpl
from matplotlib.figure import Figure
from . import label_view, cache
from .core import DataViewWindow, MessageWindow, Window
from .mpl import (FigureCanvas, MPLCompat, get_mpl_linestyles, get_mpl_markerstyles,
mpl_color_to_hex, mpl_color_to_inverted_rgb)
from .widgets.notebook import TabBar, Tab
from ..reader.data_types import is_pds_integer_data, data_type_convert_dates
from ..reader.table_objects import Meta_Field
from ..utils.compat import OrderedDict
from ..extern import six
from ..extern.six.moves import tkinter_colorchooser
from ..extern.six.moves.tkinter import (Menu, Frame, Scrollbar, Listbox, Label, Entry, Button, Checkbutton,
OptionMenu, DoubleVar, IntVar, BooleanVar, StringVar)
from ..extern.six.moves.tkinter_tkfiledialog import asksaveasfilename
#################################
class PlotViewWindow(DataViewWindow):
""" Window that displays PDS4 data as plots.
This window will display plots. After creating it, you should use the load_table() method to load
the table that it needs to display.
"""
def __init__(self, viewer):
# Create basic data view window
super(PlotViewWindow, self).__init__(viewer)
# Pack the display frame, which contains the scrollbars and the scrollable canvas
self._display_frame.pack(side='left', anchor='nw', expand=1, fill='both')
# Control variable for `freeze_display` and `thaw_display`. Image will not be updated on screen
# when counter is greater than 1.
self._freeze_display_counter = 0
# Will be set to an instance of FigureCanvas (containing the main image/slice being displayed)
self._figure_canvas = None
# Will be set to an instance of MPL's Axes
self._plot = None
# Will be set to an instance of MPL's toolbar for TK
self._toolbar = None
# Will contain _Series objects, which describe each line/point series added to the plot
self.series = []
# Contains sub-widgets of the header
self._header_widgets = {'x': None, 'y': None}
# Menu option variables. These are TKinter type wrappers around standard Python variables. The
# advantage is that you can use trace(func) on them, to automatically call func whenever one of
# these variables is changed
menu_options = [
{'name': 'axis_limits', 'type': StringVar(), 'default': 'intelligent', 'trace': self._update_axis_limits},
{'name': 'axis_scaling', 'type': StringVar(), 'default': 'linear-linear', 'trace': self._update_axis_scaling},
{'name': 'show_title', 'type': BooleanVar(), 'default': False, 'trace': self._update_labels},
{'name': 'show_labels', 'type': BooleanVar(), 'default': True, 'trace': self._update_labels},
{'name': 'show_border', 'type': BooleanVar(), 'default': True, 'trace': self._update_border},
{'name': 'tick_direction', 'type': StringVar(), 'default': 'in', 'trace': self._update_ticks},
{'name': 'show_major_ticks', 'type': BooleanVar(), 'default': True, 'trace': self._update_ticks},
{'name': 'show_minor_ticks', 'type': BooleanVar(), 'default': True, 'trace': self._update_ticks},
{'name': 'show_tick_labels', 'type': StringVar(), 'default': 'both', 'trace': self._update_ticks},
{'name': 'show_major_grid', 'type': BooleanVar(), 'default': False, 'trace': self._update_grid},
{'name': 'show_minor_grid', 'type': BooleanVar(), 'default': False, 'trace': self._update_grid},
{'name': 'grid_linestyle', 'type': StringVar(), 'default': 'dotted', 'trace': self._update_grid},
{'name': 'invert_axis', 'type': StringVar(), 'default': 'none', 'trace': self._update_invert_axis},
{'name': 'enlarge_level', 'type': DoubleVar(), 'default': 1, 'trace': self._update_enlarge_level},
{'name': 'pan', 'type': BooleanVar(), 'default': False, 'trace': self._pan},
{'name': 'axis_limits_to_rectangle', 'type': BooleanVar(), 'default': False, 'trace': self._axis_limits_to_rectangle}
]
for option in menu_options:
var = option['type']
self._menu_options[option['name']] = var
self._add_trace(var, 'write', option['trace'], option['default'])
@property
def settings(self):
settings = super(PlotViewWindow, self).settings
settings['labels'] = copy.deepcopy(settings['labels'])
return settings
# Loads the table structure into this window, displays a plot for the selected axes. axis_selections
# is a list of field indexes in the table to use for the plot in the order x,y,x_err,y_err; to have one
# of the indexes be row number, the string 'row' may be used for a field index. Set `lines` to have lines
# connecting the data points; set `points` to have markers shown on data points. Set `mask_special` to
# ignore special values (such as nulls and special constants) in plot.
def load_table(self, table_structure, axis_selections, lines=True, points=False, mask_special=False):
# Add series to storage
series = _Series(table_structure, axis_selections)
series_idx = len(self.series)
self.series.append(series)
# Add series to plot, do not continue with initialization if it has been done before
if self._data_open:
self._draw_series(series_idx, lines=lines, points=points, mask_special=mask_special)
return
# Set necessary instance variables for this DataViewWindow
self._settings = {'title': None,
'labels': {'x': {'visible': self.menu_option('show_labels'), 'name': None},
'y': {'visible': self.menu_option('show_labels'), 'name': None}},
'pixel_init_dimensions': (0, 0), 'dpi': 80.}
# Set a title for the window
self.set_window_title("{0} - Plot from '{1}'".format(self.get_window_title(), table_structure.id))
# Create the header
self._draw_header()
# Add vertical scrollbar for the plot
self._vert_scrollbar = Scrollbar(self._display_frame, orient='vertical', command=self._scrollable_canvas.yview)
self._scrollable_canvas.config(yscrollcommand=self._vert_scrollbar.set)
self._vert_scrollbar.pack(side='right', fill='y')
# Add horizontal scrollbar for the plot
self._horz_scrollbar = Scrollbar(self._display_frame, orient='horizontal', command=self._scrollable_canvas.xview)
self._scrollable_canvas.config(xscrollcommand=self._horz_scrollbar.set)
self._horz_scrollbar.pack(side='bottom', fill='x')
# Pack the static canvas, which contains the scrollable canvas
self._static_canvas.config(background='white')
self._static_canvas.pack(side='left', anchor='nw', expand=1, fill='both')
# Place the scrollable canvas, which contains the plot itself
self._scrollable_canvas.config(background='white')
self._scrollable_canvas.place(relx=0.5, rely=0.5, anchor='center')
# Update idletasks such that the window takes on its real size
self._widget.update_idletasks()
# Draw plot / series to the screen
self._draw_series(series_idx, lines=lines, points=points, mask_special=mask_special)
# Add notify event for window resizing
self._display_frame.bind('<Configure>', self._window_resize)
# Add notify event for scroll wheel (used to change plot size via scroll wheel)
self._bind_scroll_event(self._mousewheel_scroll)
# Add notify event for mouse pointer exiting _scrollable_canvas (used to display locations
# under the mouse pointer)
self._figure_canvas.tk_widget.bind('<Leave>', self._update_mouse_pixel_value)
# Add notify event for mouse motion (used to display pixel location under pointer)
self._figure_canvas.mpl_canvas.mpl_connect('motion_notify_event', self._update_mouse_pixel_value)
self._add_menus()
self._data_open = True
# Current enlarge level. E.g., a value of 2 means double the dimensions of the original plot.
def get_enlarge_level(self):
return self.menu_option('enlarge_level')
# Current title. By default a string is returned; if mpl_text is True, an MPL text object for the title
# label is returned instead. If the if_visible parameter is true, the title is returned only if it
# is visible on the plot, otherwise None is returned.
def get_title(self, mpl_text=False, if_visible=False):
if if_visible and (not self.is_title_shown()):
title = None
elif mpl_text:
title = self._plot.axes.title
else:
title = self._settings['title']
return title
# Current axis labels. Valid options for axis are x|y. By default a string is returned; if mpl_text is
# True, an MPL text object for the axis label is returned instead. If the if_visible parameter is true,
# the label is returned only if it is visible on the plot, otherwise None is returned.
def get_axis_label(self, axis, mpl_text=False, if_visible=False):
if axis not in ('x', 'y'):
raise ValueError('Unknown label type: {0}'.format(axis))
if if_visible and (not self.is_label_shown(axis)):
axis_label = None
elif mpl_text:
ax = self._plot.axes
axis = ax.xaxis if axis == 'x' else ax.yaxis
axis_label = axis.get_label()
else:
axis_label = self._settings['labels'][axis]['name']
return axis_label
# Current tick labels. Valid options for axis are x|y, and for which are major|minor|both. By default a
# string is returned; if mpl_text is True, an MPL text object for the axis label is returned instead. If
# the if_visible parameter is true, the labels are returned only if visible on the plot, otherwise
# None is returned.
def get_tick_labels(self, axis, which='both', mpl_text=False, if_visible=False):
# Ensure proper axis and which options specified
if axis not in ('x', 'y'):
raise ValueError('Unknown tick label type: {0}'.format(axis))
elif which not in ('major', 'minor', 'both'):
raise ValueError('Unknown tick label type: {0}'.format(which))
# Gather proper tick labels
tick_labels = []
if (not if_visible) or self.is_tick_labels_shown(axis):
ax = self._plot.axes
axis = ax.xaxis if axis == 'x' else ax.yaxis
tick_types = ['minor', 'major'] if which == 'both' else [which]
for tick_type in tick_types:
mpl_labels = axis.get_ticklabels(minor=(tick_type == 'minor'))
if mpl_text:
tick_labels += mpl_labels
else:
tick_labels += [label.get_text() for label in mpl_labels]
return tick_labels
# Current axis limits
def get_axis_limits(self):
ax = self._plot.axes
return list(ax.get_xlim()), list(ax.get_ylim())
# Current line options for the specified series
def get_line_options(self, series):
line = self.series[series].plot_line
line_options = {
'visible': self.is_series_shown(series, which='line'),
'style': get_mpl_linestyles()[line.get_linestyle()],
'width': line.get_linewidth(),
'color': mpl_color_to_hex(line.get_color())
}
return line_options
# Current point options for the specified series
def get_point_options(self, series):
line = self.series[series].plot_line
point_options = {
'visible': self.is_series_shown(series, which='points'),
'style': get_mpl_markerstyles()[line.get_marker()],
'width': line.get_markersize(),
'color': mpl_color_to_hex(line.get_markerfacecolor()),
'frequency': line.get_markevery()
}
return point_options
# Current error bar options for the specified series and direction (vertical or horizontal)
def get_error_bar_options(self, series, which):
if which not in ('vertical', 'horizontal'):
raise ValueError('Unknown error bar type: {0}'.format(which))
error_line = self.series[series].error_lines[which]
error_caps = self.series[series].error_caps[which]
if (error_line is None) or (not error_caps):
return None
# Calling MPL's `get_linestyle` on collections does not actually return the same value as passed in
# when doing `set_linestyle`. E.g., it does not return 'solid', 'dashed', etc but rather a different
# metric for the same thing. To obtain the name, we use two different methods below.
try:
# For MPL 1.x
dash_dict = mpl.backend_bases.GraphicsContextBase.dashd
except AttributeError:
# For MPL 2.x
from matplotlib.lines import _get_dash_pattern
dash_dict = {}
for key in ('solid', 'dashed', 'dotted', 'dashdot'):
value = _get_dash_pattern(key)
dash_dict[key] = value if not isinstance(value[1], (tuple, list)) else (value[0], list(value[1]))
line_style = [key for key, value in six.iteritems(dash_dict)
if error_line.get_linestyle()[0] == value]
error_bar_options = {
'visible': self.is_error_bar_shown(series, which=which),
'style': line_style[0],
'width': error_line.get_linewidth()[0],
'color': mpl_color_to_hex(error_line.get_color()[0])
}
return error_bar_options
# Get colors, as hex strings, of certain plot elements. Includes the plot and axes backgrounds,
# the border, the ticks, the title and axis labels. All other colors can be obtained in other more
# specific methods.
def get_colors(self):
colors = {}
ax = self._plot.axes
# Background color of middle portion of the plot (where the data lines are)
try:
colors['plot_background'] = ax.get_facecolor()
except AttributeError:
colors['plot_background'] = ax.get_axis_bgcolor()
# Background color for outside portion of the plot (where axes are)
colors['axes_background'] = self._figure_canvas.mpl_figure.get_facecolor()
# Color of border
colors['border'] = ax.spines['bottom'].get_edgecolor()
# Color of ticks
inverted_plot_bg = mpl_color_to_inverted_rgb(colors['plot_background'])
ticks = ax.xaxis.get_ticklines()
colors['ticks'] = ticks[0].get_color() if ticks else inverted_plot_bg
# Color of tick labels
inverted_axes_bg = mpl_color_to_inverted_rgb(colors['axes_background'])
tick_labels = ax.xaxis.get_ticklabels()
colors['tick_labels'] = tick_labels[0].get_color() if tick_labels else inverted_axes_bg
# Color of title
colors['title'] = ax.title.get_color()
# Color of X-label
colors['x_label'] = ax.xaxis.get_label().get_color()
# Color of Y-label
colors['y_label'] = ax.yaxis.get_label().get_color()
# Convert to MPL colors to a hex string
for name, mpl_color in six.iteritems(colors):
colors[name] = mpl_color_to_hex(mpl_color)
return colors
# Current grid options for the specified grid lines, either major|minor
def get_grid_options(self, which):
if which not in ('major', 'minor'):
raise ValueError('Unknown grid type: {0}'.format(which))
axis = self._plot.axes.xaxis
ticks = axis.majorTicks if which == 'major' else axis.minorTicks
grid_line = ticks[0].gridline
grid_options = {
'visible': self._menu_options['show_{0}_grid'.format(which)],
'style': get_mpl_linestyles()[grid_line.get_linestyle()],
'color': mpl_color_to_hex(grid_line.get_color())
}
return grid_options
# Current tick options for the specified type of tick marks, either major|minor
def get_tick_options(self, which):
if which not in ('major', 'minor'):
raise ValueError('Unknown tick type: {0}'.format(which))
tick_options = {
'visible': self.menu_option('show_{0}_ticks'.format(which)),
'direction': self.menu_option('tick_direction'),
'labels': self._menu_options('show_tick_labels'),
}
return tick_options
# Determine if the title is visible
def is_title_shown(self):
return self.menu_option('show_title')
# Determine if the x and/or y axis labels are visible. Valid options for axis are x|y|both|any.
def is_label_shown(self, axis):
x_shown = self._settings['labels']['x']['visible']
y_shown = self._settings['labels']['y']['visible']
if axis == 'x':
return x_shown
elif axis == 'y':
return y_shown
elif axis == 'both':
return x_shown and y_shown
elif axis == 'any':
return x_shown or y_shown
else:
raise ValueError('Unknown label type: {0}'.format(axis))
# Determines if the tick labels are shown. Valid options for axis are x|y|both|any.
def is_tick_labels_shown(self, axis):
show_tick_labels = self.menu_option('show_tick_labels')
if axis in ('x', 'y', 'both'):
return show_tick_labels == axis
elif axis == 'any':
return show_tick_labels in ('x', 'y', 'both')
else:
raise ValueError('Unknown tick label type: {0}'.format(axis))
# Determine if series is shown, as either line|points|both|any.
def is_series_shown(self, series, which='any'):
line = self.series[series].plot_line
line_shown = get_mpl_linestyles()[line.get_linestyle()] != 'nothing'
points_shown = get_mpl_markerstyles()[line.get_marker()] != 'nothing'
if which == 'line':
return line_shown
elif which == 'points':
return points_shown
elif which == 'both':
return line_shown and points_shown
elif which == 'any':
return line_shown or points_shown
elif which not in('any', 'both', 'line', 'points'):
raise ValueError('Unknown series type: {0}'.format(which))
# Determine if error bars for a series are shown. Valid options for which are vertical|horizontal|both|any.
# To determine whether error bars exist at all, set exist_only to True.
def is_error_bar_shown(self, series, which='any', exist_only=False):
vertical_error_lines = self.series[series].error_lines['vertical']
horizontal_error_lines = self.series[series].error_lines['horizontal']
vertical_shown = (vertical_error_lines is not None) and (exist_only or vertical_error_lines.get_visible())
horizontal_shown = (horizontal_error_lines is not None) and (exist_only or horizontal_error_lines.get_visible())
if (which == 'vertical') and vertical_shown:
return True
elif (which == 'horizontal') and horizontal_shown:
return True
elif (which == 'any') and (vertical_shown or horizontal_shown):
return True
elif (which == 'both') and (vertical_shown and horizontal_shown):
return True
elif which not in('vertical', 'horizontal', 'both', 'any'):
raise ValueError('Unknown error bar type: {0}'.format(which))
return False
# Enlarges the initial plot dimensions by enlarge_level (e.g. enlarge_level of 2 will double the
# dimensions of the original plot, a level of 0.5 will shrink it to half the original size.)
def set_enlarge_level(self, enlarge_level):
self._menu_options['enlarge_level'].set(enlarge_level)
# Sets the axis title, and controls whether it is shown. If keyword is set to None, the current option
# for that setting will be kept.
def set_title(self, title=None, show=None, **kwargs):
ax = self._plot.axes
# Save the new title settings
if title is not None:
self._settings['title'] = title
if show is not None:
# We only adjust show_title in menu_options if it is not already set, otherwise there
# is effectively an infinite loop since it calls this method again
if self.is_title_shown() != show:
self._menu_options['show_title'].set(show)
# Set new title settings. We set an empty title if the title is not currently being shown
# such that we can save font settings.
if self.is_title_shown():
title = self.get_title()
else:
title = ''
# On set_title, MPL will override the below default_kwargs unless they are specified. To preserve
# existing options, we obtain them and pass them on again unless they are overridden by *kwargs*.
old_title = self.get_title(mpl_text=True)
default_kwargs = {
'fontsize': old_title.get_fontsize(),
'fontweight': old_title.get_fontweight(),
'verticalalignment': old_title.get_verticalalignment(),
'horizontalalignment': old_title.get_horizontalalignment()}
default_kwargs.update(kwargs)
final_kwargs = default_kwargs
ax.set_title(title, **final_kwargs)
self._figure_canvas.draw()
# Sets the axis labels, and controls whether they are shown. If keyword is set to None, the current option
# for that setting will be kept.
def set_axis_label(self, axis, label=None, show=None, **kwargs):
ax = self._plot.axes
if axis in ('x', 'y'):
# Save new axis label settings
settings = self._settings['labels'][axis]
if label is not None:
settings['name'] = label
if show is not None:
settings['visible'] = show
# Set show_labels in current menu_options to True if at least one label is being shown,
# We only adjust it if it does not already match, otherwise there is effectively an infinite
# loop since it calls this method again
any_label_shown = self.is_label_shown('any')
show_labels = self._menu_options['show_labels']
if any_label_shown != show_labels.get():
show_labels.set(any_label_shown)
# Set a new axis label if necessary. We set an empty label if the label is not currently
# being shown such that we can save font settings.
if self.is_label_shown(axis):
label = self.get_axis_label(axis)
else:
label = ''
if axis == 'x':
ax.set_xlabel(label, **kwargs)
else:
ax.set_ylabel(label, **kwargs)
else:
raise ValueError('Unknown axis for axis labels: {0}'.format(axis))
self._figure_canvas.draw()
# Sets the axis limits. You may manually set axis limits, or use auto limits with the available options
# being intelligent|auto|tight|manual. If set to None, the current axis limits will be kept.
def set_axis_limits(self, x=None, y=None, auto=None):
# Set auto limits
if auto is not None:
self._menu_options['axis_limits'].set(auto)
# Set manual limits
if (x is not None) or (y is not None):
ax = self._plot.axes
self._menu_options['axis_limits'].set('manual')
if x is not None:
ax.set_xlim(x)
if y is not None:
ax.set_ylim(y)
self._figure_canvas.draw()
# Sets axis scaling. Available options for each are linear|log|symlog. If set to None, the current axis
# scaling will be kept for that axis.
def set_axis_scaling(self, x=None, y=None):
# Obtain the current axis scaling setting
axis_scaling = self.menu_option('axis_scaling')
x_scale, y_scale = axis_scaling.split('-')
if x is None:
x = x_scale
if y is None:
y = y_scale
# Set new axis scaling
axis_scaling = '{0}-{1}'.format(x, y)
self._menu_options['axis_scaling'].set(axis_scaling)
# Set plot and axes background colors, as well as colors of border and ticks. Other colors can be set
# in other more specific methods. Each color must be an MPL acceptable color.
def set_colors(self, plot_background=None, axes_background=None, border=None, ticks=None):
ax = self._plot.axes
# Set background color of middle portion of the plot (where the data lines are)
MPLCompat.axis_set_facecolor(ax, plot_background)
# Set background color for outside portion of the plot (where axes are)
self._figure_canvas.mpl_figure.set_facecolor(axes_background)
self._static_canvas.config(bg=mpl_color_to_hex(axes_background))
# Set color of border
for spine in ('top', 'bottom', 'left', 'right'):
ax.spines[spine].set_color(border)
# Set color of ticks
tick_lines = (ax.xaxis.get_majorticklines() + ax.xaxis.get_minorticklines() +
ax.yaxis.get_majorticklines() + ax.yaxis.get_minorticklines())
for tick_line in tick_lines:
tick_line.set_color(ticks)
self._figure_canvas.draw()
def invert_colors(self):
# Freeze the plot display temporarily so it is not needlessly re-drawn multiple times
self.freeze_display()
colors = self.get_colors()
# Set most colors
kwargs = {}
for option in ('plot_background', 'axes_background', 'border', 'ticks'):
kwargs[option] = mpl_color_to_inverted_rgb(colors[option])
self.set_colors(**kwargs)
# Set grid color
for grid_type in ('major', 'minor'):
grid_color_hex = self.get_grid_options(grid_type)['color']
grid_color = mpl_color_to_inverted_rgb(grid_color_hex)
self.set_grid_options(grid_type, color=grid_color)
# Set label and title colors
title_color = mpl_color_to_inverted_rgb(colors['title'])
x_label_color = mpl_color_to_inverted_rgb(colors['x_label'])
y_label_color = mpl_color_to_inverted_rgb(colors['y_label'])
tick_label_color = mpl_color_to_inverted_rgb(colors['tick_labels'])
self.set_title(color=title_color)
self.set_axis_label('x', color=x_label_color)
self.set_axis_label('y', color=y_label_color)
self.set_tick_label_options('both', which='both', color=tick_label_color)
# Thaw the plot display since it was frozen above
self.thaw_display()
# Sets line options for each data series on the currently displayed plot. series specifies the index
# for the data series on which the line options are to be adjusted. style is an MPL line style for the
# error bar, color is an MPL color for the line, width is an integer controlling the width of the
# line. The boolean show controls whether the line is visible on the plot.
def set_line_options(self, series, style=None, width=None, color=None, show=None):
line = self.series[series].plot_line
# Show or hide line
if (not show) or ((show is None) and (not self.is_series_shown(series, which='line'))):
line.set_linestyle('none')
elif style is not None:
if style not in get_mpl_linestyles():
style = get_mpl_linestyles(inverse=style)
line.set_linestyle(style)
# Set line options (can be set even if line is not shown)
if width is not None:
line.set_linewidth(width)
if color is not None:
line.set_color(color)
self._figure_canvas.draw()
# Sets point options for each data series on the currently displayed plot. series specifies the index
# for the data series on which the point options are to be adjusted. style is an MPL marker name for the
# error bar, color is an MPL color for the points, width is an integer controlling the width of the
# points. The boolean show controls whether the points are visible on the plot.
def set_point_options(self, series, style=None, width=None, color=None, frequency=None, show=None):
line = self.series[series].plot_line
# Show or hide points
if (not show) or ((show is None) and (not self.is_series_shown(series, which='points'))):
line.set_marker('None')
elif style is not None:
if style not in get_mpl_markerstyles():
style = get_mpl_markerstyles(inverse=style)
line.set_marker(style)
# Set point options (can be set even if points are not shown)
if width is not None:
line.set_markersize(width)
if frequency is not None:
line.set_markevery(frequency)
if color is not None:
line.set_markerfacecolor(color)
line.set_markeredgecolor(color)
self._figure_canvas.draw()
# Sets error bar options for each data series on the currently displayed plot. series specifies the index
# for the data series on which the error bar options are to be adjusted. which can have values
# vertical|horizontal|both, specifying for which error bar to set the selected options. style is an MPL
# line style for the error bar, color is an MPL color for the error line, width is an integer controlling
# the width of the error line. The boolean show controls whether the error bars are visible on the plot.
def set_error_bar_options(self, series, which='both', style=None, width=None, color=None, show=None):
_series = self.series[series]
directions = ('vertical', 'horizontal') if which == 'both' else (which,)
for which in directions:
# Check if error bars exist for this direction prior to setting options
if not self.is_error_bar_shown(series, which=which, exist_only=True):
continue
# Set error cap options
for cap in _series.error_caps[which]:
if show is not None:
cap.set_visible(show)
if color is not None:
cap.set_markeredgecolor(color)
# Error cap widths are set to preserve initial MPL proportions to error bar line width
if width is not None:
cap.set_markersize(4+width)
cap.set_markeredgewidth(width/2)
# Set error line options
error_line = _series.error_lines[which]
if show is not None:
error_line.set_visible(show)
if color is not None:
error_line.set_color(color)
if width is not None:
error_line.set_linewidth(width)
if style is not None:
if style not in get_mpl_linestyles():
style = get_mpl_linestyles(inverse=style)
error_line.set_linestyle(style)
self._figure_canvas.draw()
# Sets grid options on the currently displayed plot. which can have values major|minor|both, specifying
# to which grid lines the rest of the options apply. style is a MPL line style for the grid line,
# color is an MPL line color and the show boolean specifies whether the grid lines will be visible. To
# keep existing value for any variable use None.
def set_grid_options(self, which='both', style=None, color=None, show=None):
if which in ('major', 'minor', 'both'):
# Show or hide grid
if show is not None:
if which in ('major', 'both'):
self._menu_options['show_major_grid'].set(show)
if which in ('minor', 'both'):
self._menu_options['show_minor_grid'].set(show)
# Set grid style
if style is not None:
self.menu_option('grid_linestyle').set(style)
# Set grid color
if color is not None:
self._plot.axes.grid(which=which, color=color)
# Setting the grid color above automatically displays grid even if it was not suppose to be
# displayed. We run `_update_grid` to ensure it is hidden if it should be, while preserving
# the linestyle and color changes.
self._update_grid()
else:
raise ValueError('Unknown grid type: {0}'.format(which))
self._figure_canvas.draw()
# Sets tick options on the currently displayed plot. which can have values major|minor|both, specifying
# which ticks to show or hide via the show boolean. direction can have values in|out, specifying in which
# direction the ticks are facing in the plot if they are shown. To keep existing value for any variable
# use None.
def set_tick_options(self, which='both', direction=None, show=None):
if which in ('major', 'minor', 'both'):
# Show or hide ticks
if show is not None:
if which in ('major', 'both'):
self._menu_options['show_major_ticks'].set(show)
if which in ('minor', 'both'):
self._menu_options['show_minor_ticks'].set(show)
# Set tick direction
if direction is not None:
self._menu_options['tick_direction'].set(direction)
else:
raise ValueError('Unknown tick type: {0}'.format(which))
# Set tick label options on the currently displayed plot. axis can have values x|y|both, and which
# can have values major|minor|both, specifying which tick labels to adjust. show can have values
# x|y|both|none, indicating which tick labels to show on the plot.
def set_tick_label_options(self, axis, which='major', show=None, **kwargs):
# Find the right axes to set tick labels
if axis == 'both':
axes = ('x', 'y')
else:
axes = (axis,)
# Update whether tick labels for an axis are shown or not
if show is not None:
self._menu_options['show_tick_labels'].set(show)
# Update tick label options for selected axes (must be done once they are shown)
for axis in axes:
tick_labels = self.get_tick_labels(axis=axis, which=which, mpl_text=True)
for label in tick_labels:
label.update(kwargs)
self._figure_canvas.draw()
# Controls certain axes display options for current plot. invert_axis can have values x|y|both|none.
# show_border is a boolean that controls whether a border is displayed around image. To keep existing
# values use None.
def set_axes_display(self, invert_axis=None, show_border=None):
if invert_axis is not None:
self._menu_options['invert_axis'].set(invert_axis)
if show_border is not None:
self._menu_options['show_border'].set(show_border)
# Save the plot image to disk (as currently displayed). The extension in filename controls the image
# format unless *format* is set; likely supported formats are support png, pdf, ps, eps and svg.
def save_image(self, filename, format=None):
# Save figure, ensure facecolor is not ignored
mpl_canvas = self._figure_canvas.mpl_canvas
mpl_canvas.print_figure(filename,
dpi=self._settings['dpi'],
facecolor=self._figure_canvas.mpl_figure.get_facecolor(),
format=format)
# Freezes redrawing of image/plot on the screen, such that any updates to the plot are not reflected
# until it is thawed via `thaw_display`. This can be helpful, both for performance reasons and to hide
# ugly redrawing, if the plot would otherwise be redrawn a number of intermediate times.
def freeze_display(self):
# Increment counter that stores number of times freeze_display/thaw_display has been called
self._freeze_display_counter += 1
# If display is not already frozen, freeze it (by unpacking display_frame and freezing the canvas)
if self._freeze_display_counter == 1:
if self._figure_canvas is not None:
self._figure_canvas.freeze()
# Thaws a frozen display (see `freeze_display`)
def thaw_display(self):
# Decrement counter that stores number of times freeze_display/thaw_display has been called
if self._freeze_display_counter > 0:
self._freeze_display_counter -= 1
# If display should be thawed, then do so
if self._freeze_display_counter == 0:
# Thaw the image now that its being displayed again
if self._figure_canvas is not None:
self._figure_canvas.thaw()
# Update scrollable canvas to account for new scrollregion needed
self._update_scrollable_canvas_dimensions()
# Adds menu options used for manipulating the data display
def _add_menus(self):
# Add Save Plot option to the File menu
file_menu = self._menu('File', in_menu='main')
file_menu.insert_command(0, label='Save Plot', command=self._save_file_box)
file_menu.insert_separator(1)
# Add a Data menu
data_menu = self._add_menu('Data', in_menu='main')
data_menu.add_command(label='Lines / Points',
command=lambda: self._open_plot_options('Lines / Points'))
data_menu.add_separator()
data_menu.add_command(label='Error Bars',
command=lambda: self._open_plot_options('Error Bars'))
# Add an Axes menu
axes_menu = self._add_menu('Axes', in_menu='main')
axes_menu.add_checkbutton(label='Tight Limits', onvalue='tight', offvalue='manual',
variable=self._menu_options['axis_limits'])
axes_menu.add_checkbutton(label='Auto Limits', onvalue='auto', offvalue='manual',
variable=self._menu_options['axis_limits'])
axes_menu.add_command(label='Manual Limits',
command=lambda: self._open_plot_options('Axes'))
axes_menu.add_separator()
axes_menu.add_checkbutton(label='Pan', onvalue=True, offvalue=False, variable=self._menu_options['pan'])
axes_menu.add_checkbutton(label='Axis-Limits to Rectangle', onvalue=True, offvalue=False,
variable=self._menu_options['axis_limits_to_rectangle'])
axes_menu.add_separator()
invert_options = OrderedDict([('X', 'x'), ('Y', 'y'), ('XY', 'both')])
for axis, option in six.iteritems(invert_options):
label = 'Invert {0}'.format(axis)
axes_menu.add_checkbutton(label=label, onvalue=option, offvalue='none',
variable=self._menu_options['invert_axis'])
axes_menu.add_separator()
# Add an Axis Scaling sub-menu to the Axes menu
axis_scaling_menu = self._add_menu('Axis Scaling', in_menu='Axes')
axis_scaling = OrderedDict([('Linear-Linear', 'linear-linear'), ('Log-Log', 'log-log'),
('SymLog-SymLog', 'symlog-symlog'),
('X-Log', 'log-linear'), ('Y-Log', 'linear-log'),
('X-SymLog', 'symlog-linear'), ('Y-SymLog', 'linear-symlog')])
for label, scale in six.iteritems(axis_scaling):
if 'X' in label:
axis_scaling_menu.add_separator()
axis_scaling_menu.add_checkbutton(label=label, onvalue=scale, offvalue=scale,
variable=self._menu_options['axis_scaling'])