-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlplot.py
More file actions
2059 lines (1691 loc) · 74.4 KB
/
lplot.py
File metadata and controls
2059 lines (1691 loc) · 74.4 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
"""PYTHON PLOTTING UTILITIES
Logan Halstrom
CREATED: 07 OCT 2015
MODIFIED: 16 MAR 2021
DESCRIPTION: File manipulation, matplotlib plotting and saving. A subset of
lutil.py simply for plotting.
TO IMPORT:
import sys
import os
HOME = os.path.expanduser('~')
sys.path.append('{}/lib/python'.format(HOME))
import lplot
To Do:
Potentially navigate rcparams to matplotlibrc file?
"""
import numpy as np
import pandas as pd
import os
import sys
import re
import matplotlib
if sys.platform != 'darwin': #not an issue on mac
if 'DISPLAY' not in os.environ:
#Compatiblity mode for plotting on non-X11 server (also need to call this in your local script)
matplotlib.use('Agg')
elif 'pfe' in os.environ['DISPLAY']:
#for some reason, pfe sets display but hangs up, so treat as no X11
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib.cm import get_cmap
from matplotlib.transforms import Bbox #for getting plot bounding boxes
from scipy.interpolate import interp1d
from functools import partial
#path to directory containing "lplot.py", so local files can be sourced regardless of the location where lplot is being imported
sourcepath = os.path.dirname(os.path.abspath(__file__))
########################################################################
### UTILITIES
########################################################################
def MakeOutputDir(filename):
""" Makes output directories in filename that do not already exisi
filename --> save file path, used to determine parent directories
NOTE: If specifying an empty directory name, 'filename' must end in '/'
e.g. To make the directory 'test', specify either:
path/to/test/filename.dat
paht/to/test/
"""
# #split individual directories
# splitstring = savedir.split('/')
# prestring = ''
# for string in splitstring:
# prestring += string + '/'
# try:
# os.mkdir(prestring)
# except Exception:
# pass
#below is equivalent of 'GetRootDir'
rootpath = os.path.dirname(filename)
if rootpath == '': rootpath=None
if rootpath is not None and not os.path.exists(rootpath):
#there are parent dirs and they don't exist, so make them
try:
os.makedirs(rootpath)
except OSError as exc: # Guard against race condition
if exc.errno != errno.EEXIST:
raise
# if not os.path.exists(os.path.dirname(filename)):
# try:
# os.makedirs(os.path.dirname(filename))
# except OSError as exc: # Guard against race condition
# if exc.errno != errno.EEXIST:
# raise
def GetParentDir(savename):
"""Get parent directory from path of file"""
#split individual directories
splitstring = savename.split('/')
parent = ''
#concatenate all dirs except bottommost
for string in splitstring[:-1]:
parent += string + '/'
return parent
def GetFilename(path):
"""Get filename from path of file"""
parent = GetParentDir(path)
filename = FindBetween(path, parent)
return filename
def NoWhitespace(str):
"""Return given string with all whitespace removed"""
return str.replace(' ', '')
def FindBetween(string, before='^', after=None):
"""Search 'string' for characters between 'before' and 'after' characters
If after=None, return everything after 'before'
Default before is beginning of line
"""
if after == None and before != None:
match = re.search('{}(.*)$'.format(before), string)
if match != None:
return match.group(1)
else:
return None
else:
match = re.search('(?<={})(?P<value>.*?)(?={})'.format(before, after), string)
if match != None:
return match.group('value')
else:
return None
########################################################################
### PLOTTING DEFAULTS
########################################################################
def get_palette(colors, colorkind=None):
""" Convert a list of colors into strings that matplotlib understands
colors: list of color names to set the cycle
colorkind: type of color specificer (e.g. 'xkcd')
"""
if colorkind is not None:
#this text gets prepended to color name so mpl can recognize it
# e.g. 'xkcd:color name'
cycle = ['{}:{}'.format(colorkind, c) for c in colors]
else:
cycle = colors
return cycle
def set_palette(colors, colorkind=None):
""" Set matplotlib default color cycle
colors: list of color names to set the cycle
colorkind: type of color specificer (e.g. 'xkcd')
"""
# if colorkind is not None:
# #this text gets prepended to color name so mpl can recognize it
# # e.g. 'xkcd:color name'
# cycle = ['{}:{}'.format(colorkind, c) for c in colors]
# else:
# cycle = colors
cycle = get_palette(colors, colorkind)
matplotlib.rcParams.update({'axes.prop_cycle' : matplotlib.cycler(color=cycle)})
return cycle
#new matplotlib default color cycle (use to reset seaborn default)
# dark blue, orange, green, red, purple, brown, pink, gray, yellow, light blue
mplcolors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf']
#custom color cycle I like make with xkcd colors
xkcdcolors = ["windows blue", "tangerine", "dusty purple", "leaf green", "cherry" , 'light brown', "salmon pink", "greyish", "puke yellow", "sky blue", "aqua" ]
xkcdhex = ['#3778bf', "#ff9408" , '#825f87', '#5ca904', '#cf0234', '#ad8150', "#fe7b7c" , '#a8a495', '#c2be0e', "#75bbfd" , "#13eac9" ]
colorxkcd = get_palette(xkcdcolors, colorkind='xkcd') #actual rgbs that matplotlib likes
#easy names for xkcd colors in case you need to pick and choose:
colordictxkcd = {k:colorxkcd[i] for i,k in enumerate(["blue", "orange", "purple", "green", "red" , 'brown', "pink", "gray", "yellow", "light blue", "aqua" ])}
#corresponding dark/light color pairs
xkcddark = ["windows blue", "tangerine", "dusty purple", "viridian", "cherry" , "black" ]
xkcdhex = ['#3778bf', "#ff9408" , '#825f87', '#5ca904', '#cf0234', ]
xkcdlight =[ "sky blue", "sunflower", "lightish purple", "leaf green", "cherry red" , "gray" ]
xkcdhex = [ "#75bbfd" , "#??????" , '#a552e6', '#??????', '#??????', ]
colordark = get_palette(xkcddark, colorkind='xkcd') #actual rgbs that matplotlib likes
colorlight = get_palette(xkcdlight, colorkind='xkcd') #actual rgbs that matplotlib likes
colordarklight = [x for x in zip(colordark, colorlight)]
colordictdark = {k:colordark[i] for i,k in enumerate(["blue", "orange", "purple", "green", "red", "black" ])}
colordictlight = {k:colorlight[i] for i,k in enumerate(["blue", "orange", "purple", "green", "red", "black" ])}
xkcdrainbow = ["cherry" , "tangerine", "puke yellow", "leaf green", "windows blue", "dusty purple", 'light brown', "greyish", "salmon pink", "sky blue", "aqua" ]
xkcdrainbowhex = ['#cf0234', "#ff9408" , '#c2be0e', '#5ca904', '#3778bf', '#825f87', '#ad8150', '#a8a495', "#fe7b7c" , "#75bbfd" , "#13eac9" ]
colorrainbow = get_palette(xkcdrainbow, colorkind='xkcd') #actual rgbs that matplotlib like
bigrainbowxkcd = ["cherry" , "tangerine", "puke yellow", "leaf green", "windows blue", "dusty purple", 'light brown', "greyish", "cherry red", "sunflower", "leaf green", "sky blue", "lightish purple", "salmon pink", "aqua", ]
bigrainbowhex = ['#cf0234', "#ff9408" , '#c2be0e', '#5ca904', '#3778bf', '#825f87', '#ad8150', '#a8a495', '#??????', '#??????', '#??????', "#75bbfd", '#a552e6', "#fe7b7c" , "#13eac9", ]
colorrainbowbig = get_palette(bigrainbowxkcd, colorkind='xkcd')
#Line Styles
mark = 5
minimark = 0.75
line = 1.5
#dot, start, x, tri-line, plus
smallmarkers = ['.', '*', 'd', '1', '+']
bigmarkers = ['o', 'v', 'd', 's', '*', 'D', 'p', '>', 'H', '8']
bigdotmarkers = ['$\\odot$', 'v', 'd', '$\\boxdot$', '*', 'D', 'p', '>', 'H', '8']
scattermarkers = ['o', 'v', 'd', 's', 'p']
#linestyle sequencer
# linestyles = ['-', '--', '-.', '.'] #old
linestyles = [
(0, ()) , #'solid
(0, (5, 5)) , #'dashed
(0, (3, 5, 1, 5)) , #'dashdot
(0, (1, 1)) , #'dotted
(0, (1, 10)) ,
(0, (5, 10)) ,
(0, (5, 1)) ,
(0, (3, 10, 1, 10)) ,
(0, (3, 1, 1, 1)) ,
(0, (3, 5, 1, 5, 1, 5)) ,
(0, (3, 10, 1, 10, 1, 10)) ,
(0, (3, 1, 1, 1, 1, 1)) ,
]
#more named linestyles that must be input as a tuple
linestyle_names = {
'solid' : 'solid' ,
'dotted' : 'dotted' ,
'dashed' : 'dashed' ,
'dashdot' : 'dashdot' ,
'loosely dotted' : (0, (1, 10)) ,
'dotted' : (0, (1, 1)) ,
'densely dotted' : (0, (1, 1)) ,
'loosely dashed' : (0, (5, 10)) ,
# 'dashed' : (0, (5, 5)) ,
'densely dashed' : (0, (5, 1)) ,
'loosely dashdotted' : (0, (3, 10, 1, 10)) ,
'dashdotted' : (0, (3, 5, 1, 5)) ,
'densely dashdotted' : (0, (3, 1, 1, 1)) ,
'dashdotdotted' : (0, (3, 5, 1, 5, 1, 5)) ,
'loosely dashdotdotted' : (0, (3, 10, 1, 10, 1, 10)) ,
'densely dashdotdotted' : (0, (3, 1, 1, 1, 1, 1)) ,
}
linestyle_name_cycle = ['solid']
for mod in ["", "loosely ", "densely "]:
for s in ['dashed', 'dashdotted', 'dotted', 'dashdotdotted']:
linestyle_name_cycle.append("{}{}".format(mod, s))
#GLOBAL INITIAL FONT SIZES
#default font sizes
Ttl = 24
Lbl = 24
Box = 20+2
Leg = 20+2
Tck = 18+2+2
# #small font sizes
# Ttl = 24-4
# Lbl = 24-4
# Box = 20-4
# Leg = 20-4
# Tck = 13 #13: max size that allows dense tick spacing (smaller doesnt help if whole numbers are weird)
# Tck = 15 #larger font is acceptable for Noto fonts
#big font sizes
Lbl_big = 32
Leg_big = 24
Box_big = 28
Tck_big = 22
# print("{}/fonts".format(os.path.dirname(os.path.abspath(__file__))) )
# sys.exit()
#ADD ALTERNATIVE FONTS
# import matplotlib.font_manager
from matplotlib.font_manager import fontManager
from matplotlib import font_manager
# ogfonts = font_manager.get_font_names() #debug new fonts
for f in font_manager.findSystemFonts(fontpaths="{}/fonts".format(sourcepath)):
fontManager.addfont(f)
# #how to figure out the correct name of fonts:
# print([f for f in font_manager.get_font_names() if f not in ogfonts]) #new fonts
# font_manager.findfont("CMU Sans Serif")
# sys.exit()
#MATPLOTLIB DEFAULTS
mpl_params = {
#FONT SIZES
'axes.labelsize' : Lbl, #Axis Labels
'axes.titlesize' : Ttl, #Title
'font.size' : Box, #Textbox
'xtick.labelsize': Tck, #Axis tick labels [default: ?, OG: 20]
'ytick.labelsize': Tck, #Axis tick labels [default: ?, OG: 20]
'legend.fontsize': Leg, #Legend font size
#FONT.FAMILY sets the font for the entire figure
#use specific font names, e.g. generic 'monospace' selects DejaVu, not the specified `font.monospace`
'font.family' : 'sans',
# 'font.family' : 'monospace',
# 'font.family': 'helvetica' #Font family
'font.serif' : ['Noto Serif', 'DejaVu Serif'], #list is priority order to cycle through if a font is not found on local system
'font.sans-serif': ['Noto Sans', 'CMU Sans Serif', 'DejaVu Sans'], #Noto is sleek and modern, CMU (Computer Modern) is a classic 70's look, DejaVu ships with python
'font.monospace' : ['Noto Sans Mono Condensed', 'CMU Typewriter Text', 'DejaVu Sans Mono'],
'font.fantasy' : 'xkcd',
#Font used for LaTeX math:
'mathtext.fontset' : 'cm', #Computer Modern instead of DejaVu
#I cant get custom math fonts to work becuase bold text is same as normal (Noto is not good for math)
# 'mathtext.fontset' : 'custom',
# 'mathtext.bf' : "CMU Sans Serif Bold",
# 'mathtext.cal' : 'CMU Classical Serif',
# 'mathtext.it' : 'CMU Sans Serif Oblique',
# 'mathtext.rm' : 'CMU Sans Serif',
# 'mathtext.sf' : 'CMU Sans Serif',
# 'mathtext.tt' : 'CMU Typewriter Text',
# 'mathtext.fallback' : 'cm',
#AXIS PROPERTIES
'axes.titlepad' : 2*6.0, #title spacing from axis
'axes.linewidth' : 1.1, #thickness of axes
'xtick.major.width' : 1.1, #thickness of major tick
'ytick.major.width' : 1.1, #thickness of major tic
'axes.grid' : True, #grid on plot
'grid.color' : 'b0b0b0', #transparency of grid?
# 'grid.linestyle' : '--', #for dashed grid
'xtick.direction': 'in', #ticks inward
'ytick.direction': 'in', #ticks inward
#FIGURE PROPERTIES
'figure.figsize' : (7,6), #square plots
'savefig.bbox' : 'tight', #reduce whitespace in saved figures
'savefig.format' : 'png', #default figure filetype
#LEGEND PROPERTIES
'legend.framealpha' : 0.75,
# 'legend.fancybox' : True,
# 'legend.frameon' : True,
# 'legend.numpoints' : 1,
# 'legend.scatterpoints' : 1,
'legend.borderpad' : 0.1,
'legend.borderaxespad' : 0.1,
'legend.handletextpad' : 0.2,
'legend.handlelength' : 1.0,
'legend.labelspacing' : 0.001,
}
# tickparams = {
# 'xtick.labelsize': Tck,
# 'ytick.labelsize': Tck,
# }
#UPDATE MATPLOTLIB DEFAULT PREFERENCES
#These commands are called again in UseSeaborn since Seaborn resets defaults
#If you want tight tick spacing, don't update tick size default, just do manually
matplotlib.rcParams.update(mpl_params)
# matplotlib.rcParams.update(tickparams)
global sns
#USE SEABORN SETTINGS WITH SOME CUSTOMIZATION
def UseSeaborn(palette=None, ncycle=6):
"""Call to use seaborn plotting package defaults, then customize
palette --> keyword for default color palette
ncycle --> number of colors in color palette cycle
"""
global sns
import seaborn as sns
# global colors
#No Background fill, legend font scale, frame on legend
sns.set(style='whitegrid', font_scale=1, rc={'legend.frameon': True})
#Mark ticks with border on all four sides (overrides 'whitegrid')
sns.set_style('ticks')
# #ticks point in
# sns.set_style({"xtick.direction": "in","ytick.direction": "in"})
# sns.choose_colorbrewer_palette('q')
#Nice Blue,green,Red
# sns.set_palette('colorblind')
if palette == 'xkcd':
#Nice blue, purple, green
sns.set_palette(sns.xkcd_palette(xkcdcolors))
elif palette == 'xkcdrainbow':
#my colors in rainbow cycle
sns.set_palette(sns.xkcd_palette(xkcdrainbow))
elif palette is not None:
#set specified color palette
sns.set_palette(palette, ncycle)
#Nice blue, green red
# sns.set_palette('deep')
# sns.set_palette('Accent_r')
# sns.set_palette('Set2')
# sns.set_palette('Spectral_r')
# sns.set_palette('spectral')
else:
#Reset color cycle back to matplotlib defaults
sns.set_palette(mplcolors)
#FIX INVISIBLE MARKER BUG
sns.set_context(rc={'lines.markeredgewidth': 0.1})
colors = sns.color_palette() #Save new color palette to variable
#CALL MATPLOTLIB DEFAULTS AGAIN, AFTER SEABORN CHANGED THEM
matplotlib.rcParams.update(mpl_params)
# matplotlib.rcParams.update(tickparams) #DONT CALL THIS IF YOU WANT TIGHT TICK SPACING
#return color cycle
return colors
#################
#PLOT FORMATTING
def LaTeXPlotSize():
# Get figure size for figures for production in LaTeX documents
WIDTH = 495.0 # width of one column
FACTOR = 1.0 # the fraction of the width the figure should occupy
fig_width_pt = WIDTH * FACTOR
inches_per_pt = 1.0 / 72.27
golden_ratio = (np.sqrt(5) - 1.0) / 2.0 # because it looks good
fig_width_in = fig_width_pt * inches_per_pt # figure width in inches
fig_height_in = fig_width_in * golden_ratio # figure height in inches
fig_dims = [fig_width_in, fig_height_in] # fig dims as a list
return fig_dims
########################################################################
### PLOTTING UTILITIES
########################################################################
def MakeFakeFigure(num=None):
""" Start a dummy figure for prototyping that wont interact with your current plot
Use to make dummy lines, format axis labels, etc
IMPORTANT: Call `KillFakeFigure()` once done, otherwise this will mess up your current figure
"""
if num is None: num = 999999
#fake plot to format axis labels
curfig = plt.gcf()
curax = plt.gca()
fakefig = plt.figure(num)
fakeax = fakefig.add_subplot()
return curfig, curax, fakefig, fakeax
def DrawFakeFigure(fakefig):
""" Fake draw the fake prototyping figure (will not show).
Allows settings to manifest, like tick label formatting
"""
fakefig.canvas.draw() #axis ticks dont get set until the figure is shown
def KillFakeFigure(curfig, curax, fakefig):
""" Close fake prototyping figure and restore previous current figure
"""
#close fake figure and reset old figure to current
plt.close(fakefig)
plt.figure(curfig.number)
plt.sca(curax)
def PlotStart(xlabel=None, ylabel=None, nrow=1, ncol=1, width_factor=None, height_factor=None, minoraxisgrid=None, wf=None, hf=None, title=None):
""" Start a plot for a single figure or a variable layout for subplots.
Default: 1 subplot, show minor axis grid, square aspect ratio
Args:
nrow: number of subplots horizontally [1]
ncol: number of subplots vertically [1]
width_factor: float: multiplier for plot width [7] (for a square plot)
height_factor: float: multiplier for plot height [6] (for a square plot)
wf: float: alias for width_factor
hf: float: alias for height_factor
minoraxisgrid: int: show secondary axis grid with `subgrid` lines [5]
TODO:
- HANDLE MULTIPLE SUBPLOTS
- PLOT TITLE
"""
values = [width_factor, wf]
if all(v is None for v in values):
width_factor=7
elif all(v is not None for v in values):
raise ValueError("`width_factor` and `wf` double defined")
else:
width_factor=[v for v in values if v is not None][0]
values = [height_factor, hf]
if all(v is None for v in values):
height_factor=6
elif all(v is not None for v in values):
raise ValueError("`height_factor` and `hf` double defined")
else:
height_factor=[v for v in values if v is not None][0]
fig, ax = plt.subplots(nrow,ncol, figsize=[width_factor*ncol, height_factor*nrow])
if xlabel is not None: ax.set_xlabel(xlabel)
if ylabel is not None: ax.set_ylabel(ylabel)
if minoraxisgrid is None: minoraxisgrid=5
#supertitle
if title is not None: fig.suptitle(title, y=1)
#minor axis grid
if isinstance(minoraxisgrid,(int,np.int64)): Grid_Minor(ax, nx=minoraxisgrid, ny=minoraxisgrid)
return fig, ax
def PlotFinish(fig, ax, savename=None, xlim=None, ylim=None, noautolegend=False, legloc=None, pad=0.0):
""" Finish up a plot: set axis limits nicely, add legend, save to file, close plot.
TODO: HANDLE MULTIPLE SUBPLOTS
Args:
savename: path to save figure to (file extension optional [png]) [None] (do not save, return figure objects)
legloc: str: location of plot legend. Standard `loc` options and `lplot` 'outside' options (e.g. 'outsideright') ['best']
"""
if xlim is not None: ax.set_xlim(xlim)
if ylim is not None:
ax.set_ylim(ylim)
elif xlim is not None:
autoscale_axis(ax, whichax='y', pad=pad, relative=False, inplace=True)
if not noautolegend:
if legloc is not None:
if 'outside' in legloc:
Legend(ax, outside=legloc.replace('outside',''))
else:
Legend(ax, loc=legloc)
else:
Legend(ax)
if savename is not None:
SavePlot(savename)
plt.close()
return fig, ax
def PlotStartOld(title, xlbl, ylbl, horzy='vertical', figsize='square',
ttl=None, lbl=None, tck=None, leg=None, box=None,
grid=True):
"""Begin plot with title and axis labels. Space title above plot.
DEPRICATED: 7/31/2020
horzy --> vertical or horizontal y axis label
figsize --> set figure size. None for autosizing, 'tex' for latex
formatting, or 2D list for user specification.
ttl,lbl,tck --> title, label, and axis font sizes
grid --> show grid
"""
#SET FIGURE SIZE
if figsize == None:
#Plot with automatic figure sizing
fig = plt.figure()
else:
if figsize == 'tex':
#Plot with latex 2-column figure sizing
fig_dims = LaTeXPlotSize()
figsize = fig_dims
elif figsize == 'square':
figsize = [6, 6]
#Otherwise, plot with user-specificed dimensions (i.e. [width, height])
fig = plt.figure(figsize=figsize)
#PLOT FIGURE
ax = fig.add_subplot(1, 1, 1)
#USING MATPLOTLIB RC PARAMS SETTINGS
if title != None:
plt.title(title)
plt.xlabel(xlbl)
plt.ylabel(ylbl)
# #INCREASE TITLE SPACING
# if title != None:
# ttl = ax.title
# ttl.set_position([.5, 1.025])
# ax.xaxis.set_label_coords( .5, -1.025*10 )
# ax.yaxis.labelpad = 20
#TURN GRID ON
if grid:
ax.grid(True)
return fig, ax
def MoveSubplot(ax, xoffset=0, yoffset=0):
""" Offset a subplot relative to its original position.
offset values are absolute(?).
(NOTE: must be done AFTER second y-axis is created).
"""
bbox=ax.get_position()
ax.set_position([bbox.x0+xoffset, bbox.y0+yoffset, bbox.x1-bbox.x0, bbox.y1 - bbox.y0])
return ax
# def SubPlotStart(shape, figsize='square',
# sharex=False, sharey=False,
# ttl=None, lbl=None, tck=None, leg=None, box=None,
# grid=True, ):
# """Just like PlotStart, but for subplots. Enter subplot layout in `shape
# figsize --> set figure size. None for autosizing, 'tex' for latex
# formatting, or 2D list for user specification.
# ttl,lbl,tck --> title, label, and axis font sizes
# grid --> show grid
# rc --> use matplotlib rc params default
# """
# #SET FIGURE SIZE
# if figsize == None:
# #Plot with automatic figure sizing
# fig, ax = plt.subplots(shape, sharex=sharex, sharey=sharey)
# else:
# if figsize == 'tex':
# #Plot with latex 2-column figure sizing
# figsize = fig_dims
# elif figsize == 'square':
# figsize = [6, 6]
# #Otherwise, plot with user-specificed dimensions (i.e. [width, height])
# fig, ax = plt.subplots(shape, sharex=sharex, sharey=sharey, figsize=figsize)
# else:
# #USE FONT DICT SETTINGS
# #Set font sizes
# if ttl != None or lbl != None or tck != None or leg != None or box != None:
# #Set any given font sizes
# SetFontDictSize(ttl=ttl, lbl=lbl, tck=tck, leg=leg, box=box)
# else:
# #Reset default font dictionaries
# SetFontDictSize()
# if title != None:
# plt.title(title, fontdict=font_ttl)
# # plt.xlabel(xlbl, fontdict=font_lbl)
# plt.xticks(fontsize=font_tck)
# # plt.ylabel(ylbl, fontdict=font_lbl, rotation=horzy)
# plt.yticks(fontsize=font_tck)
# # #Return Matplotlib Defaults
# # matplotlib.rcParams.update({'xtick.labelsize': Tck,
# # 'ytick.labelsize': Tck,})
# # #INCREASE TITLE SPACING
# # if title != None:
# # ttl = ax.title
# # ttl.set_position([.5, 1.025])
# # # ax.xaxis.set_label_coords( .5, -1.025*10 )
# # # ax.yaxis.labelpad = 20
# # #TURN GRID ON
# # if grid:
# # ax.grid(True)
# return fig, ax
def MakeTwinx(ax, ylbl='', horzy='vertical'):
"""Make separate, secondary y-axis for new variable with label.
(must later plot data on ax2 for axis ticks an bounds to be set)
ax --> original axes object
ylbl --> text for new y-axis label
horzy --> set orientation of y-axis label
"""
ax2 = ax.twinx()
ax2.set_ylabel(ylbl)
return ax2
def MakeTwiny(ax, xlbl=''):
"""Make separate, secondary x-axis for new variable with label.
(must later plot data on ax2 for axis ticks an bounds to be set)
ax --> original axes object for plot
xlbl --> new x-axis label
"""
ax2 = ax.twiny() #get separte x-axis for labeling trajectory Mach
ax2.set_xlabel(xlbl) #label new x-axis
return ax2
def YlabelOnTop(ax, ylbl, x=0.0, y=1.01):
"""Place horizontally oriented y-label on top of y-axis.
x --> relative x coordinate of text label center (0: right of fig, goes <0)
y --> relative y coordinate of text label center (1: top of fig, goes <0)
"""
#rotate ylabel
ax.set_ylabel(ylbl, rotation=0)
#set new center coordinates of label
ax.yaxis.set_label_coords(x, y)
return ax
def RemoveAxisTicks(ax, axis='both'):
"""Remove numbers and grid lines for axis ticks
Use to declassify sensitive info (e.g. ITAR, SBU)
axis --> Which axis to remove ticks ('x', 'y', 'both')
"""
if axis == 'both' or axis == 'x':
ax.get_xaxis().set_ticks([])
if axis == 'both' or axis == 'y':
ax.get_yaxis().set_ticks([])
return ax
def RemoveAxisTickLabels(ax, axis='both', prettygrid=True):
"""Remove numbers from axis tick labels, keep ticks and gridlines
Use to declassify sensitive info (e.g. ITAR, SBU)
NOTE: Call after setting axis limits
axis --> Which axis to remove ticks ('x', 'y', 'both')
prettygrid --> set axis ticks to square grid if True
"""
if axis == 'both' or axis == 'x':
#remove axis tick labels
ax.set_xticklabels([])
#space grid lines nicely
if prettygrid:
#get current axis limits
lim = ax.get_xlim()
#space grid lines with desired number
ticks = np.linspace(lim[0], lim[1], 6)
ax.set_xticks(ticks)
if axis == 'both' or axis == 'y':
#remove axis tick labels
ax.set_yticklabels([])
#space grid lines nicely
if prettygrid:
#get current y limits
lim = ax.get_ylim()
#space grid lines with desired number
ticks = np.linspace(lim[0], lim[1], 6)
ax.set_yticks(ticks)
return ax
def RotateTicks(ax, rot=0, whichax='xy',):
""" Rotate axis tick labels (makes them fit better)
Args:
rot --> angle to rotate new tick labels, default none
whichax --> which axes to rotate labels for ['xy'] xy: both axes, x: x-axis, y: y-axis
"""
if 'x' in whichax.lower():
for tk in ax.get_xticklabels():
tk.set_rotation(rot)
if 'y' in whichax.lower():
for tk in ax.get_yticklabels():
tk.set_rotation(rot)
return ax
def MoreTicks(ax, ndouble=1, whichax='y'):
"""Increase the number of ticks on an axis to a multiple of the original amount
(Multiple so that original tick positions are preserved)
"""
if whichax == None or (whichax.lower() != 'x' and whichax.lower() != 'y'):
raise IOError("Must choose 'x' or 'y' axes to sync ticks")
xy = whichax.lower()
def DoubleTicks(vals, ndoubl=1):
for k in range(ndouble):
newvals = list([ vals[0] ])
for i in range(1, len(vals)):
#get mid-point preceding current value
newvals.append( (vals[i] + vals[i-1])/2 )
#get current value
newvals.append( vals[i] )
vals = list(newvals)
return vals
vals = getattr(ax, "get_{}ticks".format(xy))()
vals = DoubleTicks(vals, ndouble)
getattr(ax, "set_{}ticks".format(xy))(vals)
return ax
def GetRelativeTicks(ax, whichax='y'):
"""Get relative tick locations for a specified axis, use to match shared axes.
(Generalized `GetRelativeTicksX`).
Use linear interpolation, leave out endpoints if they exceede the data bounds
Return relative tick locations and corresponding tick values
"""
if whichax == None or (whichax.lower() != 'x' and whichax.lower() != 'y'):
raise IOError("Must choose 'x' or 'y' axes to sync ticks")
xy = whichax.lower()
#Get bounds of axis values
axmin, axmax = getattr(ax, "get_{}lim".format(xy))()
#Get values at each tick
tickvals = getattr(ax, "get_{}ticks".format(xy))()
#if exterior ticks are outside bounds of data, drop them
if tickvals[0] < axmin: tickvals = tickvals[1:]
if tickvals[-1] > axmax: tickvals = tickvals[:-1]
#Interpolate relative tick locations for bounds 0 to 1
relticks = np.interp(tickvals, np.linspace(axmin, axmax), np.linspace(0, 1))
return relticks, tickvals
GetRelativeTicksX = partial(GetRelativeTicks, whichax='x')
def SyncDualAxisTicks(ax1, ax2, whichax=None):
""" Sync secondary axis ticks to align with primary (align grid)
ASSUMES: linear relationship between both x-axis parameters (for non-linear parameters, use `MakeSyncedDualAxis`)
Required: data must already be plotted on both axes
(Functionality demonstrated by plotting in GUI and hovering cursor next to
tick to compare interpolated value to GUI value)
Args:
ax1: primary axis
ax2: secondary axis
whichax: 'x' or 'y', which dual axis pair to sync
Returns:
updated `ax2`
"""
if whichax == None or (whichax.lower() != 'x' and whichax.lower() != 'y'):
raise IOError("Must choose 'x' or 'y' axes to sync ticks")
xy = whichax.lower()
#get location of primary axis ticks relative to position on plot
ax1reltcks, ax1vals = GetRelativeTicks(ax1, xy)
#get axis bounds of second axis
ax2min, ax2max = getattr(ax2, 'get_{}lim'.format(xy))()
#linear relationship between dual-axis parameters (standard functionality)
ax2vals = np.interp(ax1reltcks, np.linspace(0, 1), np.linspace(ax2min, ax2max), )
#eliminate negative zeros
ax2vals = ax2vals + 0.0
# ### DEBUG
# print(" LPLOT DEBUG: SyncDualAxisTicks sometimes dont line up if a slight round makes the numbers better. could fix this by changing axis number formatter")
# print(" LPLOT DEBUG: old ticks", getattr(ax2, "get_{}ticks".format(xy))() )
# print(" LPLOT DEBUG: new ticks", ax2vals )
#set new tick locations on second axis
getattr(ax2, "set_{}ticks".format(xy))(ax2vals)
return ax2
#partials
SyncDualAxisTicksY = partial(SyncDualAxisTicks, whichax='y')
SyncDualAxisTicksX = partial(SyncDualAxisTicks, whichax='x')
#backwards compatibility
SyncTicks_DualAxisY = SyncDualAxisTicksY
SyncTicks_DualAxisX = SyncDualAxisTicksX
def MakeSyncedDualAxis(ax1, x2, whichax=None, linear=None):
""" Create a second axis stacked on the first, which shows equivalent values of a second parameter at each tick
Use the plot data for interpolation to handle non-linear relationships (DATA MUST BE POINT-MATCHED)
Use `SyncDualAxisTicks` for linear, non-point-matched dual axis.
Strategy: plot the exact same data invisibly, then rename each tick with the interplated value for the second xaxis parameter
Args:
ax1: primary axis
x2: data for secondary axis
whichax: 'x' or 'y', which dual axis pair to sync
linear: use a linear interpolation to determine the relative tick locations [True]. Otherwise, interpolate with actual plot data (MUST BE POINT-MATCHED)
"""
if whichax == None or (whichax.lower() != 'x' and whichax.lower() != 'y'):
raise IOError("Must choose 'x' or 'y' axes to sync ticks")
xy = whichax.lower()
xyopposite = 'x' if xy == 'y' else 'y'
if linear is None: linear = True
#ax2 is just an empty twin axis since it will never actually show any plotted data
ax2 = getattr(ax1, 'twin{}'.format(xyopposite))()
if not linear:
#get original axis data for interpolation with `x2`
ax1data = {}
ax1data['x'], ax1data['y'] = ax1.lines[0].get_data() #original x-data (must be point-matched to second x axis)
#get original and secondary axis bounds
ax1minmax = getattr(ax1, 'get_{}lim'.format(xy))() #original axis bounds
ax2minmax = np.interp(ax1minmax, ax1data[xy], x2 ) + 0.0 #equivalent axis bounts on 2nd axis
#get original and secondary axis tick locations
ax1vals = getattr(ax1, "get_{}ticks".format(xy))() #original tick values (locations)
ax1vals = ax1vals[(ax1vals<ax1minmax[1]) & (ax1vals>ax1minmax[0])] #exclude out of bound end ticks, they'll mess up the second axis ***(THIS MIGHT STILL CREATE PROBLEMS WITH SOME TICKS, COULD JUST FAKE-PLOT THE FIRST AXIS TO GET FORMATTED TICK LABELS) ***
ax2vals = np.interp(ax1vals, ax1data[xy], x2 ) + 0.0 #eqivalent tick locations on 2nd axis (plus zero eliminates negative zeros)
#SET UP SECOND AXIS
# #ax2 is just an empty twin axis since it will never actually show any plotted data
# ax2 = getattr(ax1, 'twin{}'.format(xyopposite))()
#Set second axis to equivalent bounds as first
ax2.set_xlim(ax1minmax)
#make the ticks on the second axis at the same location as the first
ax2.set_xticks(ax1vals)
#FORMAT SECOND AXIS TICK VALUES WITH MATPLOTLIB TICK FORMATER (USING DUMMY PLOT)
#fake plot with second axis data so the tick labels get formatted, make sure ticks are in appropriate locations
curfig, curax, fakefig, fakeax = MakeFakeFigure()
getattr( fakeax, "set_{}lim".format(xy))(ax2minmax)
fakeax.plot(x2, x2)
getattr( fakeax, "set_{}ticks".format(xy))(ax2vals)
#axis ticks dont get formatted until the figure is shown
DrawFakeFigure(fakefig)
#get formatted tick labels and unpack just the strings
ax2ticklabels = [xx.get_text() for xx in getattr(fakeax, "get_{}ticklabels".format(xy))() ]
#close fake figure and reset old figure to current
KillFakeFigure(curfig, curax, fakefig)
#RENAME SECONDARY AXIS TICKS WITH EQUIVALENT VALUES
#label second axis ticks with corresponding, interpolated values
getattr( ax2, "set_{}ticklabels".format(xy))(ax2ticklabels)
# print("\n\nLPLOT DEBUG: MAKESYNCEDDUALAXIS")
# print("1st axis tick values: ", ax1vals)
# print("1st axis bounds: ", ax1minmax)
# print("2nd axis tick labels:", ax2ticklabels)
# print("2nd axis bounds: ", ax2minmax)
# # print(ax2vals, ax2ticklabels)
else:
#SET UP SECOND AXIS
#invisible throwaway plot to set up the second x-axis
ax2.plot(x2, ax1.lines[0].get_ydata(), color='k', alpha=0 )
#sync ticks assuming linear relationship (no need for point-matched data)
ax2 = SyncDualAxisTicks(ax1, ax2, xy)
return ax2
def SecondaryXaxis(ax, x2, bot=True, xlbl=None, xlblcombine=True, offset=None, linear=None):
"""Make a second x-axis below the first, mapping a second independent parameter
from the dataset to y-data ALREADY PLOTTED
Args:
ax --> original axes object for plot
x2 --> independed x-data to map to on second x-axis
bottom --> second x-axis on bottom of plot (otherwise top) [True]
xlbl --> new x-axis label [None]
xlblcombine --> combine primary and secondary x-axis labels with a "/" on one line below both axes [True]
offset --> axis-relative distance to offset 2nd x-axis from first [0.15]
linear --> use a linear interpolation to determine the relative tick locations [True]. Otherwise, interpolate with actual plot data (MUST BE POINT-MATCHED)
"""
if offset is None: offset = 0.15
if linear is None: linear = True
#
ax2 = MakeSyncedDualAxis(ax, x2, 'x', linear=linear)
#SHOW SECOND X-AXIS ON SAME SIDE AS FIRST (BOTTOM)
if bot:
#offset the 2nd xaxis from the first (spines are the lines that tick lines are connected to)
ax2.spines["bottom"].set_position(("axes", 0.0-offset))
#activate ax2 spines, but keep them invisible so that they dont appear over ax1
# make_patch_spines_invisible(ax2)
ax2.set_frame_on(True)
ax2.patch.set_visible(False)
[sp.set_visible(False) for sp in ax.spines.values()]
#now make the x-axis spine visible
ax2.spines["bottom"].set_visible(True)
#and put the labels on the correct side
ax2.xaxis.set_label_position('bottom')
ax2.xaxis.set_ticks_position('bottom')
#LABEL NEW X-AXIS
if xlbl is not None:
if xlblcombine:
xlbl1 = ax.get_xlabel()
lbl = "{} / {}".format(xlbl1, xlbl)
ax.set_xlabel(None) #hide original axis label
else:
lbl = xlbl
ax2.set_xlabel(lbl)
return ax2
def MakeSecondaryXaxis(ax, xlbl, tickfunc, locs=5):
"""*** SEE `SecondaryXaxis` FOR SECOND X-AXIS WITH VALUES SYNCED TO DATA ***
Make a second x-axis with tick values interpolated from user-provided function