diff --git a/demo/work_pretty_trio.py b/demo/work_pretty_trio.py index 2323b7b..6e75104 100755 --- a/demo/work_pretty_trio.py +++ b/demo/work_pretty_trio.py @@ -169,20 +169,20 @@ def saveone(i, pname=None): # make single image file (for QA) saveone(16*60, (odir / oname).with_suffix('.png')) -### -### run_parallel = True -### if run_parallel: -### # parallel processing -### # save all frames in parallel -### # 68 for stampede, 24 for ls5 -### nthreads = 24 # ls5 -### with Pool(nthreads) as pool: -### pool.map(saveone, range(len(tstamps))) -### else: -### # serial processing -### for i in range(len(tstamps)): -### saveone(i) -### -### # make mpeg file -### cmd = f'ffmpeg -i "{wdir / "%04d.png"}" -vf scale=1920:-2 -vframes 2880 -crf 3 -vcodec libx264 -pix_fmt yuv420p -f mp4 -y "{odir / oname}"' -### subprocess.run(shlex.split(cmd), check=True) + +run_parallel = True +if run_parallel: + # parallel processing + # save all frames in parallel + # 68 for stampede, 24 for ls5 + nthreads = 24 # ls5 + with Pool(nthreads) as pool: + pool.map(saveone, range(len(tstamps))) +else: + # serial processing + for i in range(len(tstamps)): + saveone(i) + +# make mpeg file +cmd = f'ffmpeg -i "{wdir / "%04d.png"}" -vf scale=1920:-2 -vframes 2880 -crf 3 -vcodec libx264 -pix_fmt yuv420p -f mp4 -y "{odir / oname}"' +subprocess.run(shlex.split(cmd), check=True) diff --git a/demo/work_pretty_trio_added.py b/demo/work_pretty_trio_added.py index 99327c6..7b5ee44 100755 --- a/demo/work_pretty_trio_added.py +++ b/demo/work_pretty_trio_added.py @@ -65,8 +65,10 @@ df_shp = df_shp.to_crs('EPSG:3857') # title to use for each input -titles = ['Regular Only', f'Regular +\nUnintended,\nContinous {site}', - f'Regular + \nUnintended,\nPulsated {site}'] +titles = ['routine emissions', + f'routine +\nunintended continous\n emissions from {site}', + f'routine +\nunintended pulse\n emissions from {site}', + ] # read the data data = [] @@ -110,6 +112,7 @@ bndry = [1, 10, 50, 100, 200, 500, 1000, 2000] norm = colors.BoundaryNorm(bndry, len(bndry)) + plotter_options = { 'background_manager': BackgroundManager(bgfile=bgfile,), 'contour_options': { @@ -119,15 +122,38 @@ 'alpha': .5, 'extend': 'max', }, - 'title_options': {'fontsize': 'medium'}, + # TODO instead of setting fontsize evrywhere, change across everywhere like this + # https://stackoverflow.com/questions/3899980/how-to-change-the-font-size-on-a-matplotlib-plot + 'title_options': {'fontsize': 'small'}, 'colorbar_options': None, - 'customize_once': [ + 'plot_customizers': [ # emission points - lambda p: df_shp.plot(ax=p.ax, column='kls', categorical=True, legend=False, zorder=10, + lambda p: df_shp.plot(ax=p.ax, column='kls', categorical=True, + legend=False, zorder=10, markersize=2, # got red/blue/yellow from colorbrewer's Set1 cmap=colors.ListedColormap(['#e41a1c', '#377eb8', '#ffff33']) ), + #lambda p: p.ax.scatter(df_shp.geometry.x, df_shp.geometry.y, + # c=df_shp['kls'].apply(lambda x: {'flare':'#e41a1c', + # 'tank':'#377eb8', 'well':'#ffff33'}[x]), + # zorder=10, #markersize=2, + # ), + # emission point annotations + lambda p: + # adjust_text() repels labels from each other + adjust_text( + # make list of annotation + list( + # this part creates annotation for each point + p.ax.annotate(_.Site_Label, (_.geometry.x, _.geometry.y,), + zorder=11, + fontsize=4, + ) + # goes across all points but filter by Site_Label + for _ in df_shp.itertuples() if _.Site_Label in ('S2',) + ), + ), # Shannon's "original" box lambda p: p.ax.add_geometries( [Polygon([(-101.8834373, 31.71350603), @@ -149,44 +175,65 @@ # clone the options and let each has own title plotter_options = [{**plotter_options, 'title': title} for title in titles] +def legend_manager(fig): + ax = fig.get_axes() + #print(ax) + #print(dir(ax[0])) + ll = fig.get_axes()[0].get_legend_handles_labels() + #print(ll) + fig.legend(*ll, loc='lower right') + + # colorbar goes to entire figure figure_options = { 'colorbar_options': { - 'label': r'$CH_4$ (ppbV)', - } + 'label': r'$CH_4$ (ppbV)', + 'colorbar_customizers': [ + lambda cb: cb.ax.tick_params(labelsize = 8), + lambda cb: cb.set_label( r'$CH_4$ (ppbV)', fontsize='small') + ] + }, + 'footnote_options': {'fontsize': 'small'}, + 'figure_customizers': [ + # neither worked... + #lambda fig: fig.legend(*fig.get_axes()[0].get_legend_handles_labels()) + #legend_manager, + ] } # make a plot template -p = plotter_multi.Plotter(arrays=arrays, tstamps=tstamps, +pp = plotter_multi.Plotter(arrays=arrays, tstamps=tstamps, x=x, y=y, projection=LambertConformalTCEQ(), plotter_options=plotter_options, figure_options=figure_options) + # function to save one time frame def saveone(i, pname=None): if pname is None: pname = wdir / f'{i:04}.png' ts = tstamps[i] - footnote = str(ts) - p(pname, tidx=i, footnote=footnote) + footnote = str(ts).replace(':00-06:00', ' (UTC-06:00)') + pp(pname, tidx=i, footnote=footnote) # make single image file (for QA) saveone(16*60, (odir / oname).with_suffix('.png')) -### -### run_parallel = True -### if run_parallel: -### # parallel processing -### # save all frames in parallel -### # 68 for stampede, 24 for ls5 -### nthreads = 24 # ls5 -### with Pool(nthreads) as pool: -### pool.map(saveone, range(len(tstamps))) -### else: -### # serial processing -### for i in range(len(tstamps)): -### saveone(i) -### -### # make mpeg file -### cmd = f'ffmpeg -i "{wdir / "%04d.png"}" -vf scale=1920:-2 -vframes 2880 -crf 3 -vcodec libx264 -pix_fmt yuv420p -f mp4 -y "{odir / oname}"' -### subprocess.run(shlex.split(cmd), check=True) + + +run_parallel = True +if run_parallel: + # parallel processing + # save all frames in parallel + # 68 for stampede, 24 for ls5 + nthreads = 24 # ls5 + with Pool(nthreads) as pool: + pool.map(saveone, range(len(tstamps))) +else: + # serial processing + for i in range(len(tstamps)): + saveone(i) + +# make mpeg file +cmd = f'ffmpeg -i "{wdir / "%04d.png"}" -vf scale=1920:-2 -vframes 2880 -crf 3 -vcodec libx264 -pix_fmt yuv420p -f mp4 -y "{odir / oname}"' +subprocess.run(shlex.split(cmd), check=True) diff --git a/plotter/plotter_core.py b/plotter/plotter_core.py index 719dacf..dac84f8 100644 --- a/plotter/plotter_core.py +++ b/plotter/plotter_core.py @@ -42,7 +42,7 @@ def __init__(self, array, tstamps, projection=None, extent=None, x=None, y=None, self.colorbar_options = plotter_options.get('colorbar_options', {}) - self.footnote_options = plotter_options.get('footnote_options', {}) + self.footnote_options = plotter_options.get('footnote_options', None) self.title = plotter_options.get('title', None) self.title_options = plotter_options.get('title_options', None) @@ -108,7 +108,11 @@ def __init__(self, array, tstamps, projection=None, extent=None, x=None, y=None, self.ax.set_title(**ttlopt) # other customizations + if 'plot_customizers' in plotter_options: + self.customize(plotter_options['plot_customizers']) if 'customize_once' in plotter_options: + warnings.warn("'customize_once' renamed to 'plot_customizer'", + DeprecationWarning) self.customize(plotter_options['customize_once']) self.hasdata = False @@ -175,15 +179,27 @@ def __call__(self, tidx=None, footnote='', title=None): warnings.warn('No data to show, Colorbar turned off', pu.PlotterWarning) - if footnote is not None: + if footnote is not None or self.footnote_options: + # default set of options + my_footnote_options = { + 'xy': (0.5, 0), # bottom center + 'xytext': (0, -6), # drop 6 ponts below (works if there is no x axis label) + #'xytext': (0,-18), # drop 18 ponts below (works with x-small fontsize axis label) + 'xycoords': 'axes fraction', + 'textcoords': 'offset points', + 'ha': 'center', 'va':'top'} + + if self.footnote_options: + + # user overrides options + my_footnote_options.update(self.footnote_options) + if footnote is not None: + my_footnote_options['text'] = footnote + + self.footnote = self.ax.annotate(footnote, - xy=(0.5, 0), # bottom center - xytext=(0, -6), - # drop 6 ponts below (works if there is no x axis label) - # xytext=(0,-18), # drop 18 ponts below (works with x-small fontsize axis label) - xycoords='axes fraction', - textcoords='offset points', - ha='center', va='top') + **my_footnote_options) + self.hasdata = True diff --git a/plotter/plotter_multi.py b/plotter/plotter_multi.py index d8c9837..7ecc45e 100644 --- a/plotter/plotter_multi.py +++ b/plotter/plotter_multi.py @@ -65,21 +65,35 @@ def __init__(self, arrays, tstamps, projection=None, extent=None, x=None, y=None x=x, y=y, plotter_options=po) for arr, po in zip(arrays, plotter_options)] self.axes = [p.ax for p in self.plotters] - def __call__(self, oname, tidx=None, footnote='', suptitle=None, titles=None, footnotes=''): + figure_customizers = figure_options.pop('figure_customizers', None) + + if figure_customizers: + for fc in figure_customizers: + fc(self.fig) + + # TODO rename footnotes to subplot_footnote or something like that... + def __call__(self, oname, tidx=None, footnote='', suptitle=None, + titles=None):#, footnotes=''): # remember if plots were blank haddata = self.plotters[0].hasdata - if isinstance(footnotes, str) or len(footnotes) != len(self.plotters): - footnotes = [footnotes] * len(self.plotters) + #if isinstance(footnotes, str) or len(footnotes) != len(self.plotters): + # footnotes = [footnotes] * len(self.plotters) - for p,fn in zip(self.plotters, footnotes): + #print(footnotes) + # if footnotes: + #for p,fn in zip(self.plotters, footnotes): - p(tidx, footnote=fn) + # p(tidx, footnote=fn) + for p in self.plotters: + + p(tidx, footnote=None) # if it was blank, need some initalization if not haddata: cbopt = self.figure_options.get('colorbar_options', None) + fnopt = self.figure_options.get('footnote_options', None) if cbopt is not None: self.fig.subplots_adjust(wspace=.1) @@ -90,20 +104,39 @@ def __call__(self, oname, tidx=None, footnote='', suptitle=None, titles=None, fo elif self.nplot >= 3: my_shrink = .5 - self.fig.colorbar( + # TODO do this on plotter_core too + cb_customizers = cbopt.pop('colorbar_customizers', None) + + cb = self.fig.colorbar( mappable=self.plotters[0].mappable, ax=self.axes, use_gridspec=True, **{'shrink': my_shrink, **cbopt}) + for cbc in cb_customizers: + cbc(cb) - if footnote is not None: + + if footnote is not None or fnopt is not None: # no clue why, but y=0.2 puts text nicely below the plots, for pair case... if self.nplot <= 2: my_ypos = .2 elif self.nplot >=3: my_ypos = .3 - self.footnote = self.fig.text(0.5, my_ypos, footnote, - ha='center', va='top') + + + # default options + my_footnote_options = { + 'x': 0.5, + 'y': my_ypos, + 's': '', + 'ha': 'center', + 'va': 'top', + } + if footnote is not None: + my_footnote_options['s'] = footnote + if fnopt is not None: + my_footnote_options.update(fnopt) + self.footnote = self.fig.text(**my_footnote_options) else: if footnote is not None: self.footnote.set_text(footnote)