File Coverage

blib/lib/Matplotlib/Simple.pm
Criterion Covered Total %
statement 1489 2004 74.3
branch 364 650 56.0
condition 176 319 55.1
subroutine 37 38 97.3
pod 0 18 0.0
total 2066 3029 68.2


line stmt bran cond sub pod time code
1             # ABSTRACT: Access Matplotlib from Perl; providing consistent user interface between different plot types
2 6     6   362130 use strict;
  6         10  
  6         196  
3 6     6   22 use feature 'say';
  6         8  
  6         3434  
4 6     6   28 use warnings FATAL => 'all';
  6         8  
  6         250  
5 6     6   1588 use autodie ':all';
  6         49274  
  6         28  
6              
7             package Matplotlib::Simple;
8             require 5.010;
9             our $VERSION = 0.301;
10 6     6   86592 use Scalar::Util 'looks_like_number';
  6         10  
  6         256  
11 6     6   22 use List::Util qw(max sum min);
  6         6  
  6         380  
12 6     6   3620 use Term::ANSIColor;
  6         43070  
  6         472  
13 6     6   40 use Cwd 'getcwd';
  6         12  
  6         240  
14 6     6   26 use File::Temp;
  6         6  
  6         336  
15 6     6   2524 use DDP { output => 'STDOUT', array_max => 10, show_memsize => 1 };
  6         171908  
  6         60  
16 6     6   3692 use Devel::Confess 'color';
  6         37194  
  6         24  
17 6     6   3746 use FindBin '$RealScript';
  6         5486  
  6         778  
18 6     6   30 use Exporter 'import';
  6         8  
  6         136  
19 6     6   2124 use Capture::Tiny 'capture';
  6         23274  
  6         260  
20 6     6   2168 use JSON::MaybeXS;
  6         57004  
  6         362  
21 6     6   2410 use MIME::Base64;
  6         3896  
  6         160478  
22             our @EXPORT = qw(plt bar barh boxplot colored_table hexbin hist hist2d imshow pie plot scatter violinplot violin wide);
23             our @EXPORT_OK = @EXPORT;
24              
25             my @ax_methods = (
26             'ArtistList', 'add_child_axes', 'add_collection', 'add_container',
27             'add_image', 'add_line', 'add_patch', 'add_table', 'apply_aspect',
28             'autoscale_view', 'axison', 'bxp', 'callbacks', 'can_pan', 'can_zoom',
29             'child_axes', 'collections', 'containers', 'contains_point', 'dataLim',
30             'drag_pan', 'end_pan', 'fmt_xdata', 'fmt_ydata', 'format_coord',
31             'format_xdata', 'format_ydata','hexbin', 'hist', 'hist2d', 'hlines',
32             'ignore_existing_data_limits', 'in_axes', 'indicate_inset',
33             'indicate_inset_zoom', 'inset_axes', 'invert_xaxis', 'invert_yaxis',
34             'label_outer', 'legend_', 'name', 'pcolorfast', 'redraw_in_frame', 'relim',
35             'reset_position', 'scatter', 'secondary_xaxis', 'secondary_yaxis', 'set_adjustable',
36             'set_anchor', 'set_aspect', 'set_autoscale_on', 'set_autoscalex_on', 'set_autoscaley_on',
37             'set_axes_locator', 'set_axis_off', 'set_axis_on', 'set_axisbelow',
38             'set_box_aspect', 'set_fc', 'set_forward_navigation_events',
39             'set_frame_on', 'set_navigate', 'set_navigate_mode',
40             'set_position', 'set_prop_cycle', 'set_rasterization_zorder',
41             'set_subplotspec', 'set_title', 'set_xbound', 'set_xlabel',
42             'set_xlim', # ax.set_xlim(left, right), or ax.set_xlim(right = 180)
43             'set_xmargin', 'set_xscale', 'set_xticklabels', 'set_xticks', 'set_ybound',
44             'set_ylabel', 'set_ylim', 'set_ymargin', 'set_yscale', 'set_yticklabels',
45             'set_yticks', 'sharex', 'sharey', 'spines', 'start_pan', 'tables',
46             'text', 'tick_params', 'ticklabel_format', 'titleOffsetTrans', 'transAxes', 'transData', 'transLimits',
47             'transScale', 'update_datalim', 'use_sticky_edges', 'viewLim', 'vlines', 'violin',
48             'xaxis', 'xaxis_date', 'xaxis_inverted', 'yaxis', 'yaxis_date',
49             'yaxis_inverted'
50             );
51             my @fig_methods = (
52             'add_artist', 'add_axes', 'add_axobserver', 'add_callback', 'add_gridspec',
53             'add_subfigure', 'add_subplot', 'align_labels', 'align_titles',
54             'align_xlabels', 'align_ylabels', 'artists', 'autofmt_xdate', #'axes', # same as plt
55             'bbox', 'bbox_inches', 'canvas', 'clear', 'clf', 'clipbox',
56             'colorbar', # same name as in plt, have to use on case-by-case
57             'contains', 'convert_xunits', 'convert_yunits', 'delaxes', 'dpi',
58             'dpi_scale_trans', 'draw', 'draw_artist', 'draw_without_rendering',
59             'figbbox', 'figimage', # 'figure', 'findobj',
60             'format_cursor_data', 'frameon', 'have_units', 'images', 'is_transform_set', # 'legend', legends',
61             'lines', 'mouseover', 'number', 'patch', 'patches', 'pchanged', 'pick',
62             'pickable', 'properties', 'remove',
63             'remove_callback', #'savefig', keeping plt instead
64             'sca', 'set', 'set_agg_filter', 'set_alpha', 'set_animated', 'set_canvas',
65             'set_clip_box', 'set_clip_on', 'set_clip_path', 'set_constrained_layout',
66             'set_constrained_layout_pads', 'set_dpi', 'set_edgecolor', 'set_facecolor',
67             'set_figheight'
68             , # default 4.8 # 'set_figure', # deprecated as of matplotlib 3.10.0
69             'set_figwidth', # default 6.4
70             'set_frameon', 'set_gid', 'set_in_layout', 'set_label',
71             'set_layout_engine', 'set_linewidth', 'set_mouseover', 'set_path_effects',
72             'set_picker', 'set_rasterized', 'set_size_inches', 'set_sketch_params',
73             'set_snap', 'set_tight_layout', 'set_transform', 'set_url', 'set_visible',
74             'set_zorder', # 'show', # keeping plt instead
75             'stale', 'stale_callback', 'sticky_edges', 'subfigs',
76             'subfigures', # subplot_mosaic',
77             'subplotpars', # subplots','subplots_adjust',
78             'suppressComposite', # 'suptitle', # keeping plt instead
79             'supxlabel', 'supylabel', #'text',
80             'texts', #'tight_layout',
81             'transFigure', 'transSubfigure', 'update',
82             'update_from', #'waitforbuttonpress',
83             'zorder'
84             );
85             my @plt_methods = (
86             'AbstractContextManager', 'Annotation', 'Arrow', 'Artist', 'AutoLocator',
87             'AxLine', 'Axes', 'BackendFilter', 'Button', 'Circle', 'Colorizer',
88             'ColorizingArtist', 'Colormap', 'Enum', 'ExitStack', 'Figure',
89             'FigureBase', 'FigureCanvasBase', 'FigureManagerBase', 'FixedFormatter',
90             'FixedLocator', 'FormatStrFormatter', 'Formatter', 'FuncFormatter',
91             'GridSpec', 'IndexLocator', 'Line2D', 'LinearLocator', 'Locator',
92             'LogFormatter', 'LogFormatterExponent', 'LogFormatterMathtext',
93             'LogLocator', 'MaxNLocator', 'MouseButton', 'MultipleLocator', 'Normalize',
94             'NullFormatter', 'NullLocator', 'PolarAxes', 'Polygon', 'Rectangle',
95             'ScalarFormatter', 'Slider', 'Subplot', 'SubplotSpec', 'TYPE_CHECKING',
96             'Text', 'TickHelper', 'Widget', 'acorr', 'angle_spectrum', 'annotate',
97             'annotations', 'arrow', 'autoscale', 'autumn', 'axes', 'axhline',
98             'axhspan', 'axis', 'axline', 'axvline', 'axvspan', 'backend_registry',
99             'bar_label', 'barbs', 'bone', 'box', 'boxplot',
100             'broken_barh', 'cast', 'cbook', 'cla', 'clabel'
101             , #'clf', # I don't think you'd ever do that, also redundant with fig
102             'clim', 'close', 'cm', 'cohere', 'color_sequences', 'colorbar',
103             'colormaps', 'connect', 'contour', 'contourf', 'cool', 'copper', 'csd',
104             'cycler', 'delaxes', 'disconnect', 'draw', 'draw_all',
105             'draw_if_interactive', 'ecdf', 'errorbar', 'eventplot', 'figaspect',
106             'figimage', 'figlegend', 'fignum_exists', 'figtext', 'figure', 'fill',
107             'fill_between', 'fill_betweenx', 'findobj', 'flag', 'functools', 'gca',
108             'gcf', 'gci', 'get', 'get_backend', 'get_cmap', 'get_current_fig_manager',
109             'get_figlabels', 'get_fignums', 'get_plot_commands', 'get_scale_names',
110             'getp', 'ginput', 'gray', 'grid',
111             'hot', 'hsv', 'importlib', 'imread', 'imsave', 'inferno',
112             'inspect', 'install_repl_displayhook', 'interactive', 'ioff', 'ion',
113             'isinteractive', 'jet', 'legend', 'locator_params', 'logging', 'loglog',
114             'magma', 'magnitude_spectrum', 'margins', 'matplotlib', 'matshow',
115             'minorticks_off', 'minorticks_on', 'mlab', 'new_figure_manager',
116             'nipy_spectral', 'np', 'overload', 'pause', 'pcolor', 'pcolormesh',
117             'phase_spectrum', 'pie', 'pink', 'plasma', 'plot', 'plot_date', 'polar',
118             'prism', 'psd', 'quiver', 'quiverkey', 'rc', 'rcParams', 'rcParamsDefault',
119             'rcParamsOrig', 'rc_context', 'rcdefaults', 'rcsetup', 'rgrids', 'savefig',
120             'sca', #'scatter', # taken by "ax"
121             'sci', 'semilogx', 'semilogy', 'set_cmap', 'set_loglevel', 'setp', 'show',
122             'specgram', 'spring', 'spy', 'stackplot', 'stairs', 'stem', 'step',
123             'streamplot', 'style',
124             'subplot', # nrows, ncols : int, default: 1
125             'subplot2grid', 'subplot_mosaic', 'subplot_tool', 'subplots',
126             'subplots_adjust', 'summer', 'suptitle', 'switch_backend', 'sys', 'table',
127             # 'text'
128             , # text(x: 'float', y: 'float', s: 'str', fontdict: 'dict[str, Any] | None' = None, **kwargs) -> 'Text'
129             'thetagrids', 'threading',# 'tick_params',
130             'tight_layout', 'time', 'title', 'tricontour', 'tricontourf', 'tripcolor',
131             'triplot',
132             #'twinx', 'twiny',
133             'uninstall_repl_displayhook', 'violinplot',
134             'viridis', 'waitforbuttonpress', 'winter', 'xcorr', 'xkcd',# 'vlines'
135             'xlabel','xscale','ylabel', 'ylim', 'yscale',
136             # 'xlim','xticks','yticks'
137             );
138              
139             my @arg = ('add', 'cmap', 'data', 'execute', 'fh', 'ncol', 'ncols', 'nrow', 'nrows', 'p', 'plot.type', 'plots', 'plot', 'output.file', 'scale', 'scalex', 'scaley', 'shared.colorbar', 'show', 'twinx');
140             my @cb_arg = (
141             'cbdrawedges', # for colarbar: Whether to draw lines at color boundaries
142             'cblabel', # The label on the colorbar's long axis
143             'cblocation', # of the colorbar None or {'left', 'right', 'top', 'bottom'}
144             'cb_min',
145             'cb_max',
146             'cborientation', # None or {'vertical', 'horizontal'}
147             'cbpad', # pad : float, default: 0.05 if vertical, 0.15 if horizontal; Fraction of original Axes between colorbar and new image Axes
148             'cb_logscale');
149             my %opt = (
150             barplot_helper => [
151             'color'
152             , # :mpltype:`color` or list of :mpltype:`color`, optional; The colors of the bar faces. This is an alias for *facecolor*. If both are given, *facecolor* takes precedence # if entering multiple colors, quoting isn't needed
153             'edgecolor' , # optional; The colors of the bar edges.
154             'key.order', # define the keys in an order (an array reference)
155             'label', # an array of labels for grouped bar plots
156             'linewidth', # float or array, optional; Width of the bar edge(s). If 0, don't draw edges
157             'log' , # bool, default: False; If *True*, set the y-axis to be log scale.
158             'logscale', # equivalent to "log"
159             'stacked', # stack the groups on top of one another; default 0 = off
160             'width', # float or array, default: 0.8; The width(s) of the bars.
161             'xerr', # float or array-like of shape(N,) or shape(2, N), optional. If not *None*, add horizontal / vertical errorbars to the bar tips. The values are +/- sizes relative to the data: - scalar: symmetric +/- values for all bars # - shape(N,): symmetric +/- values for each bar # - shape(2, N): Separate - and + values for each bar. First row # contains the lower errors, the second row contains the upper # errors. # - *None*: No errorbar. (Default)
162             'yerr', # same as xerr, but better with bar
163             ],
164             boxplot_helper => [
165             'color', # a hash, where keys are the keys in data, and values are colors, e.g. X => 'blue'
166             'colors', 'key.order',
167             'logscale', # array of "x" and/or "y"
168             'notch', # Whether to draw a notched boxplot (`True`), or a rectangular boxplot (`False`)
169             'orientation',# {'vertical', 'horizontal'}, default: 'vertical'
170             'showcaps', # bool: Show the caps on the ends of whiskers; default "True"
171             'showfliers', #bool, default: :rc:`boxplot.showfliers`; Show the outliers beyond the caps.
172             'showmeans' #bool, default: :rc:`boxplot.showmeans`
173             ],
174             colored_table_helper => [@cb_arg,
175             'col.labels',
176             'cmap', # the cmap used for coloring
177             'colorbar.on', # only draw if colorbar is on
178             'default_undefined', # what value should undefined values be assigned to?
179             'mirror', # $data{A}{B} = $data{B}{A}
180             'row.labels', # row labels
181             'shared.colorbar', # array of 0-based indices for sharing a colorbar
182             'show.numbers',# show the numbers or not, by default off. 0 = "off"; "show.numbers" > 0 => "on"
183             'undef.color', # what color will undefined points be
184             ],
185             hexbin_helper => [ @cb_arg,
186             'cb_logscale',
187             'cmap', # "gist_rainbow" by default
188             'colorbar.on', # only draw colorbar if colorbar is on
189             'key.order', # define the keys in an order (an array reference)
190             'marginals', # If marginals is *True*, plot the marginal density as colormapped rectangles along the bottom of the x-axis and left of the y-axis.
191             'mincnt'
192             , # int >= 0, default: 0 If > 0, only display cells with at least *mincnt* number of points in the cell.
193             'shared.colorbar', # array of 0-based indices for sharing a colorbar
194             'vmax'
195             , # When using scalar data and no explicit *norm*, *vmin* and *vmax* define the data range that the colormap cover
196             'vmin'
197             , # When using scalar data and no explicit *norm*, *vmin* and *vmax* define the data range that the colormap cover
198             'xbins', # default 15
199             'xscale.hexbin', # 'linear', 'log'}, default: 'linear': Use a linear or log10 scale on the horizontal axis.
200             'ybins', # default 15
201             'yscale.hexbin', # 'linear', 'log'}, default: 'linear': Use a linear or log10 scale on the vertical axis.
202             ],
203             hist_helper => [
204             'alpha', # default 0.5; same for all sets
205             'bins'
206             , # nt or sequence or str, default: :rc:`hist.bins`If *bins* is an integer, it defines the number of equal-width bins in the range. If *bins* is a sequence, it defines the bin edges, including the left edge of the first bin and the right edge of the last bin; in this case, bins may be unequally spaced. All but the last (righthand-most) bin is half-open
207             'color', # a hash, where keys are the keys in data, and values are colors, e.g. X => 'blue'
208             'colorbar.on', # only draw colorbar if colorbar is on
209             'logscale', # if set to > 1, the y-axis will be logarithmic
210             'orientation', # {'vertical', 'horizontal'}, default: 'vertical'
211             'shared.colorbar', # array of 0-based indices for sharing a colorbar
212             'show.legend'
213             ],
214             hist2d_helper => [@cb_arg,
215             'cb_logscale',
216             'cmap', # "gist_rainbow" by default
217             'cmax', # All bins that has count < *cmin* or > *cmax* will not be displayed
218             'cmin', # color min
219             'colorbar.on', # only draw colorbar if colorbar is on
220             'density', # density : bool, default: False
221             'key.order', # define the keys in an order (an array reference)
222             'logscale', # logscale, an array of axes that will get log scale
223             'shared.colorbar', # array of 0-based indices for sharing a colorbar
224             'show.colorbar',
225             'vmax'
226             , # When using scalar data and no explicit *norm*, *vmin* and *vmax* define the data range that the colormap cover
227             'vmin'
228             , # When using scalar data and no explicit *norm*, *vmin* and *vmax* define the data range that the colormap cover
229             'xbins', # default 15
230             'xmin', 'xmax', 'ymin', 'ymax',
231             'ybins', # default 15
232             ],
233             imshow_helper => [@cb_arg,
234             'cblabel', # colorbar label
235             'cbdrawedges', # for colorbar
236             'cblocation', # of the colorbar None or {'left', 'right', 'top', 'bottom'}
237             'cborientation', # None or {'vertical', 'horizontal'}
238             'cmap', # The Colormap instance or registered colormap name used to map scalar data to colors
239             'colorbar.on', # only draw if colorbar is on
240             'shared.colorbar', # array of 0-based indices for sharing a colorbar
241             'stringmap', # 'H' => 'Alpha helix'
242             'vmax', # float
243             'vmin', # flat
244             ],
245             pie_helper => [
246             'autopct', # percent wise
247             #labeldistance and pctdistance are ratios of the radius; therefore they vary between 0 for the center of the pie and 1 for the edge of the pie, and can be set to greater than 1 to place text outside the pie https://matplotlib.org/stable/gallery/pie_and_polar_charts/pie_features.html
248             'labeldistance',
249             'pctdistance',
250             ],
251             plot_helper => [
252             'key.order', # an array of key strings (which are defined in data)
253             'logscale', # an array of "x" and/or "y"
254             'show.legend', # be default on; should be 0 if off
255             'set.options',
256             'twinx.args'
257             ],
258             scatter_helper => [
259             'color_key', # which of data keys is the color key
260             'cmap', # for 3-set scatterplots; default "gist_rainbow"
261             'colorbar.on', # only draw colorbar if colorbar is on
262             'keys', # specify the order, otherwise alphabetical #'log', # if set to > 1, the y-axis will be logarithmic # 's', # float or array-like, shape (n, ), optional. The marker size in points**2 (typographic points are 1/72 in.).
263             'logscale', # "x" and/or "y" as an aray
264             'shared.colorbar', # array of 0-based indices for sharing a colorbar
265             'show.legend',
266             'set.options' # color = 'red', marker = 'v', etc.
267             ],
268             violin_helper => [
269             'color', # a hash, where keys are the keys in data, and values are colors, e.g. X => 'blue'
270             'colorbar.on',# only draw colorbar if colorbar is on
271             'colors',
272             'key.order',
273             'logscale', # array: "x" and/or "y"
274             'orientation',# {'vertical', 'horizontal'}, default: 'vertical'
275             'whiskers'
276             ],
277             wide_helper => [
278             'color', # a hash, with each key assigned to a color "blue" or something
279             'show.legend', # be default on; should be 0 if off
280             ],
281             );
282             sub write_data {
283 69     69 0 113 my ($args) = @_;
284 69         671 my $current_sub = ( split( /::/, ( caller(0) )[3] ) )[-1];
285 69 50       251 if ( ref $args ne 'HASH' ) {
286 0         0 die "args must be given as a hash ref, e.g. \"$current_sub({ data => ... })\"";
287             }
288 69         134 my @reqd_args = (
289             'data', # args to original function (scalar, hashref, or arrayref)
290             'fh', # file handle
291             'name' # python variable name
292             );
293 69         92 my @undef_args = grep { not defined $args->{$_} } @reqd_args;
  207         289  
294 69 50       111 if (scalar @undef_args > 0) {
295 0         0 p @undef_args;
296 0         0 die "the above args are required for $current_sub, but weren't defined";
297             }
298             # 1. Create the JSON Encoder; allow_nonref: allows scalars (strings/numbers) to be encoded
299 69         482 my $json_encoder = JSON::MaybeXS->new->utf8->allow_nonref;
300             # 2. Serialize Perl Data -> JSON String; Passing data directly. JSON::MaybeXS handles refs + scalars automatically.
301 69         1246 my $json_string = $json_encoder->encode($args->{data});
302             # 3. Base64 Encode the JSON String, not the reference
303 69         224 my $b64_data = encode_base64($json_string, '');
304             # Assign the b64 string to a temp python variable
305 69         90 say {$args->{fh}} "$args->{name}_b64 = '$b64_data'";
  69         396  
306             # Decode b64 -> bytes -> utf8 string -> json load -> python object
307 69         82 say {$args->{fh}} "$args->{name} = json.loads(base64.b64decode($args->{name}_b64).decode('utf-8'))";
  69         688  
308             }
309             sub plot_args { # this is a helper function to other matplotlib subroutines
310 79     79 0 134 my ($args) = @_;
311 79         672 my $current_sub = ( split( /::/, ( caller(0) )[3] ) )[-1];
312 79 50       578 if ( ref $args ne 'HASH' ) {
313 0         0 die "args must be given as a hash ref, e.g. \"$current_sub({ data => \@blah })\"";
314             }
315 79         163 my @reqd_args = (
316             'ax', # ax1, ax2, etc. when there are multiple plots
317             'fh', # e.g. $py, $fh, which will be passed by the subroutine
318             'args', # args to original function
319             );
320 79         103 my @undef_args = grep { !defined $args->{$_} } @reqd_args;
  237         347  
321 79 50       126 if ( scalar @undef_args > 0 ) {
322 0         0 p @undef_args;
323 0         0 die 'the above args are necessary, but were not defined.';
324             }
325 79         2940 my @defined_args = ( @reqd_args, @ax_methods, @fig_methods, @plt_methods, @arg, @cb_arg );
326             my @bad_args = grep {
327 237         252 my $key = $_;
328 237         613 not grep { $_ eq $key } @defined_args
  108783         111506  
329 79         109 } keys %{$args};
  79         161  
330 79 50       199 if ( scalar @bad_args > 0 ) {
331 0         0 p @bad_args, array_max => scalar @bad_args;
332 0         0 say 'the above arguments are not recognized.';
333 0         0 p @defined_args, array_max => scalar @defined_args;
334 0         0 die 'The above args are accepted.';
335             }
336 79   50     201 $args->{ax} = $args->{ax} // 'ax';
337 79         129 foreach my $item (
338 553         800 grep { defined $args->{args}{$_} } ( # no quotes!
339             'set_title', 'set_xlabel', 'set_ylabel', 'suptitle',
340             'xlabel', 'ylabel', 'title'
341             )
342             )
343             {
344 41 100       231 if ( $args->{args}{$item} =~ m/^([^\"\',]+)$/ ) {
345 39         95 $args->{args}{$item} = "'$args->{args}{$item}'";
346             }
347             }
348 79         179 my @obj = ( $args->{ax}, 'fig', 'plt' );
349 79         141 my @args = ( \@ax_methods, \@fig_methods, \@plt_methods );
350 79         222 foreach my $i ( 0 .. $#args ) {
351 237         240 foreach my $method ( grep { defined $args->{args}{$_} } @{ $args[$i] } ) {
  33812         39356  
  237         410  
352 44         62 my $ref = ref $args->{args}{$method};
353 44 50 66     151 if ( ( $ref ne 'ARRAY' ) && ( $ref ne '' ) ) {
354 0         0 die "$current_sub only accepts scalar or array types, but \"$ref\" was entered.";
355             }
356 44 100       88 if ( $ref eq '' ) {
357 43 50       72 if ($method eq 'show') {
358 0         0 next; # plt.show() is emitted by plt() after plt.savefig()
359             } else {
360 43         45 say {$args->{fh}} "$obj[$i].$method($args->{args}{$method}) #line" . __LINE__;
  43         329  
361             }
362 43         79 next;
363             }
364             # can only be ARRAY
365 1         2 foreach my $j ( @{ $args->{args}{$method} } ) {
  1         3  
366 1         1 say { $args->{fh} } "$obj[$i].$method($j) #line" . __LINE__;
  1         4  
367             }
368             }
369             }
370 79 50       162 return unless defined $args->{ax};
371 79   50     258 my $legend = $args->{args}{legend} // '';
372 79         90 my $pie_plot = 0;
373 79 100 100     255 if ( ( defined $args->{args}{'plot.type'} )
374             && ( $args->{args}{'plot.type'} eq 'pie' ) ) {
375 2         4 $pie_plot = 1;
376             }
377 79 100       152 return 1 if $pie_plot == 1;
378             # pie charts don't get legends
379 77         92 say { $args->{fh} }
  77         295  
380             "handles, labels = $args->{ax}.get_legend_handles_labels()";
381 77         91 say { $args->{fh} } 'if len(labels) > 0:';
  77         120  
382 77         81 say { $args->{fh} } "\t$args->{ax}.legend($legend)";
  77         1369  
383             }
384             sub barplot_helper { # this is a helper function to other matplotlib subroutines
385 7     7 0 21 my ($args) = @_;
386 7         126 my $current_sub = ( split( /::/, ( caller(0) )[3] ) )[-1]
387             ; # https://stackoverflow.com/questions/2559792/how-can-i-get-the-name-of-the-current-subroutine-in-perl
388 7 50       45 if ( ref $args ne 'HASH' ) {
389 0         0 die
390             "args must be given as a hash ref, e.g. \"$current_sub({ data => \@blah })\"";
391             }
392 7         21 my @reqd_args = (
393             'fh', # e.g. $py, $fh, which will be passed by the subroutine
394             'plot', # args to original function
395             );
396 7         43 my @undef_args = grep { !defined $args->{$_} } @reqd_args;
  14         43  
397 7 50       22 if ( scalar @undef_args > 0 ) {
398 0         0 p @undef_args;
399 0         0 die 'the above args are necessary, but were not defined.';
400             }
401 7         29 my @opt = ('ax', @reqd_args, @ax_methods, @plt_methods, @fig_methods, @arg, @{ $opt{$current_sub} } );
  7         407  
402 7         19 my $plot = $args->{plot};
403             my @bad_opt = grep {
404 44         56 my $key = $_;
405 44         120 not grep { $_ eq $key } @opt
  20328         20895  
406 7         10 } keys %{$plot};
  7         26  
407 7   50     34 my $ax = $args->{ax} // '';
408 7 100       29 if ( scalar @bad_opt > 0 ) {
409 1         7 p @bad_opt;
410 1         3444 die
411             "The above arguments aren't defined for $plot->{'plot.type'} at plot position $ax";
412             }
413 6         10 my ( %ref_counts, $plot_type );
414 6         10 foreach my $set ( keys %{ $plot->{data} } ) {
  6         24  
415 14         31 $ref_counts{ ref $plot->{data}{$set} }++;
416             }
417 6 50       15 if ( scalar keys %ref_counts > 1 ) {
418 0         0 p $plot->{data};
419 0         0 p %ref_counts;
420 0         0 die
421             "different kinds of data were entered to plot $ax which should be simple hash or hash of arrays.";
422             }
423 6 100       21 if ( defined $ref_counts{''} ) {
    50          
    50          
424 4         8 $plot_type = 'simple';
425             } elsif ( defined $ref_counts{'ARRAY'} ) {
426 0         0 $plot_type = 'grouped';
427             } elsif ( defined $ref_counts{'HASH'} ) {
428 2         3 $plot_type = 'grouped'; # now make the hash of hash into a ARRAY structure
429 2         4 my %key2;
430 2         4 foreach my $key1 ( keys %{ $plot->{data} } ) {
  2         4  
431 4         15 foreach my $key2 ( keys %{ $plot->{data}{$key1} } ) {
  4         9  
432 8         13 $key2{$key2}++;
433             }
434             }
435 2         13 my @key2 = sort { lc $a cmp lc $b } keys %key2;
  2         9  
436 2         3 my %new_structure;
437 2         5 foreach my $k1 ( keys %{ $plot->{data} } ) {
  2         4  
438 4         4 @{ $new_structure{$k1} } = @{ $plot->{data}{$k1} }{@key2};
  4         10  
  4         6  
439             }
440 2         4 @{ $plot->{label} } = @key2;
  2         6  
441 2         6 $plot->{data} = \%new_structure;
442             } else {
443 0         0 p %ref_counts;
444 0         0 p $plot->{data};
445 0         0 die 'the above plot type is not yet programmed in to bar/barh';
446             }
447 6   100     28 $plot->{stacked} = $plot->{stacked} // 0;
448 6 50 66     34 if ( ( $plot_type eq 'grouped' )
      33        
449             && ( defined $plot->{width} )
450             && ( $plot->{stacked} == 0 ) )
451             {
452 0         0 say STDERR 'grouped, non-stacked barplots ignore width settings';
453 0         0 delete $plot->{width};
454             }
455 6         22 my @key_order;
456 6 50       13 if ( defined $plot->{'key.order'} ) {
457 0         0 @key_order = @{ $plot->{'key.order'} };
  0         0  
458             } else {
459 6         10 @key_order = sort keys %{ $plot->{data} };
  6         25  
460             }
461 6         10 my $options = ''; # these args go to the plt.bar call
462 6 50 33     26 if ( $plot->{'log'} || $plot->{logscale}) {
463 0         0 $options .= ', log = True';
464             } # args that can be either arrays or strings below; STRINGS:
465 6         13 foreach my $c ( grep { defined $plot->{$_} } ( 'color', 'edgecolor' ) ) {
  12         24  
466 1 50 33     9 next if ( ( $c eq 'color' ) && ( $plot_type eq 'grouped' ) && ( ref $plot->{$c} ne '' ) );
      33        
467 1         2 my $ref = ref $plot->{$c};
468 1 50       3 if ( $ref eq '' ) { # single color
    0          
    0          
469 1         4 $options .= ", $c = '$plot->{$c}'";
470             } elsif ( $ref eq 'ARRAY' ) {
471 0         0 $options .= ", $c = [\"" . join( '","', @{ $plot->{$c} } ) . '"]';
  0         0  
472             } elsif ( $ref eq 'HASH') {
473            
474             } else {
475 0         0 die "ref \"$ref\" isn't defined";
476             }
477             } # args that can be either arrays or strings below; NUMERIC:
478 6         9 foreach my $c ( grep { defined $plot->{$_} } ('linewidth') ) {
  6         14  
479 0         0 my $ref = ref $plot->{$c};
480 0 0       0 if ( $ref eq '' ) { # single color
    0          
481 0         0 $options .= ", $c = $plot->{$c}";
482             } elsif ( $ref eq 'ARRAY' ) {
483 0         0 $options .= ", $c = [" . join( ',', @{ $plot->{$c} } ) . ']';
  0         0  
484             } else {
485 0         0 p $args;
486 0         0 die "$ref for $c isn't acceptable";
487             }
488             }
489 6         11 foreach my $err ( grep { defined $plot->{$_} } ( 'xerr', 'yerr' ) ) {
  12         19  
490 0         0 my $ref = ref $plot->{$err};
491 0 0       0 if ( $ref eq '' ) {
    0          
492 0         0 $options .= ", $err = $plot->{$err}";
493             } elsif ( $ref eq 'HASH' ) { # I assume that it's all defined
494 0         0 my ( @low, @high );
495 0         0 foreach my $i (@key_order) {
496 0 0       0 if ( scalar @{ $plot->{$err}{$i} } != 2 ) {
  0         0  
497 0         0 p $plot->{$err}{$i};
498 0         0 die "$err/$i should have exactly 2 items: low and high error bars";
499             }
500 0         0 push @low, $plot->{$err}{$i}[0];
501 0         0 push @high, $plot->{$err}{$i}[1];
502             }
503             $options .=
504 0         0 ", $err = [["
505             . join( ',', @low ) . '],['
506             . join( ',', @high ) . ']]';
507             } else {
508 0         0 p $args;
509 0         0 die "$ref for $err isn't acceptable";
510             }
511             }
512 6 100       21 if ( $plot_type eq 'simple' ) { # a simple hash -> simple bar plot
    50          
513             write_data({
514             data => \@key_order,
515             fh => $args->{fh},
516 4         26 name => 'labels'
517             });
518 4         11 say { $args->{fh} } 'vals = [' . join( ',', @{ $plot->{data} }{@key_order} ) . ']';
  4         9  
  4         35  
519 4 50 33     20 if ((defined $plot->{color}) && (ref $plot->{color} eq 'HASH')) {
520 0         0 @undef_args = grep {not defined $plot->{color}{$_}} @key_order;
  0         0  
521 0 0       0 if (scalar @undef_args > 0) {
522 0         0 p @undef_args;
523 0         0 die 'the above keys were not defined in the colors hash';
524             }
525 0         0 $options .= ',color = ["' . join ('","', @{ $plot->{color} }{@key_order} ) . '"]';
  0         0  
526             }
527 4         7 say { $args->{fh} } "ax$ax.$plot->{'plot.type'}(labels, vals $options)";
  4         83  
528             } elsif ( $plot_type eq 'grouped' ) { # grouped bar plot; hash of array
529 2         5 my @val;
530 2         12 foreach my $k (@key_order) {
531 4         5 foreach my $i ( 0 .. scalar @{ $plot->{data}{$k} } - 1 ) {
  4         11  
532 8   50     16 $plot->{data}{$k}[$i] = $plot->{data}{$k}[$i] // 0; # must match in both sets
533 8         8 push @{ $val[$i] }, $plot->{data}{$k}[$i];
  8         13  
534             }
535             }
536 2   50     9 my $barwidth = $plot->{width} // 0.8;
537 2   50     6 $plot->{stacked} = $plot->{stacked} // 0;
538 2 100       6 if ( $plot->{stacked} == 0 ) {
539 1         2 $barwidth /= ( scalar @val + 1 ); # @val holds one array per bar series
540             }
541 2         4 my @xticks = 0 .. scalar @{ $val[0] } - 1;
  2         6  
542 2         5 my @mean_pos = map { 0 } 0 .. scalar @{ $val[0] } - 1; # initialize
  4         7  
  2         3  
543 2         4 my $hw = 'height';
544 2 100       8 $hw = 'width' if $plot->{'plot.type'} eq 'bar';
545 2         4 my @bottom = map { 0 } 0 .. scalar @{ $val[0] } - 1; # initialize
  4         6  
  2         4  
546 2         3 my $i = 0;
547 2         4 foreach my $arr (@val) {
548 4         14 my $x = '[' . join( ',', @xticks ) . ']';
549 4         5 foreach my $p ( 0 .. $#mean_pos ) {
550 8         11 $mean_pos[$p] += $xticks[$p];
551             }
552 4         6 my $set_options = '';
553 4         5 foreach
554 8 100       40 my $f ( grep { ( ref $plot->{$_} eq 'ARRAY' ) && defined $plot->{$_}[$i] } ( 'color', 'label' ) )
555             {
556 4         7 $set_options .= ", $f = '$plot->{$f}[$i]'";
557             }
558 4 100       7 if ( $plot->{stacked} > 0 ) {
559 2 50       7 my $stack_kwarg = $plot->{'plot.type'} eq 'barh' ? 'left' : 'bottom';
560 2         7 $set_options .= ", $stack_kwarg = [" . join( ',', @bottom ) . ']';
561             }
562 4         10 say { $args->{fh} } "ax$ax.$plot->{'plot.type'}($x, ["
563 4         5 . join( ',', @{$arr} )
  4         24  
564             . "], $hw = $barwidth $options $set_options)";
565             @bottom =
566 4         8 map { $bottom[$_] + $arr->[$_] } 0 .. scalar @{ $val[0] } - 1
  2         4  
567 4 100       10 if $plot->{stacked} > 0;
568 4         8 @xticks = map { $_ + $barwidth } @xticks
569 4 100       8 if $plot->{stacked} <= 0; # for next iteration
570 4         5 $i++;
571             }
572 2         4 my $xticks = '["' . join( '","', @key_order ) . '"]';
573 2         3 my $ticks = 'yticks';
574 2 100       7 $ticks = 'xticks' if $plot->{'plot.type'} eq 'bar';
575 2         6 $_ /= scalar @val for @mean_pos;
576 2         2 say { $args->{fh} } "ax$ax.set_$ticks(["
  2         47  
577             . join( ',', @mean_pos )
578             . "], $xticks)";
579             } else {
580 0         0 die
581             "\$plot_type = $plot_type & stacked = $plot->{stacked}and isn't defined.";
582             }
583             }
584              
585             sub boxplot_helper {
586 4     4 0 14 my ($args) = @_;
587 4         44 my $current_sub = ( split( /::/, ( caller(0) )[3] ) )[-1]
588             ; # https://stackoverflow.com/questions/2559792/how-can-i-get-the-name-of-the-current-subroutine-in-perl
589 4 50       17 if ( ref $args ne 'HASH' ) {
590 0         0 die "args must be given as a hash ref, e.g. \"$current_sub({ data => \@blah })\"";
591             }
592 4         12 my @reqd_args = (
593             'fh', # e.g. $py, $fh, which will be passed by the subroutine
594             'plot', # args to original function
595             );
596 4         7 my @undef_args = grep { !defined $args->{$_} } @reqd_args;
  8         18  
597 4 50       10 if ( scalar @undef_args > 0 ) {
598 0         0 p @undef_args;
599 0         0 die 'the above args are necessary, but were not defined.';
600             }
601             my @opt = (
602 4         15 @ax_methods, @plt_methods, @fig_methods, @arg, 'ax', @{ $opt{$current_sub} }
  4         148  
603             );
604 4         11 my $plot = $args->{plot};
605             my @bad_opt = grep {
606 22         27 my $key = $_;
607 22         60 not grep { $_ eq $key } @opt
  10076         10472  
608 4         6 } keys %{$plot};
  4         10  
609 4 50       17 if ( scalar @bad_opt > 0 ) {
610 0         0 p @bad_opt;
611 0         0 die "The above arguments aren't defined for $plot->{'plot.type'} using $current_sub";
612             }
613 4   50     22 $plot->{orientation} = $plot->{orientation} // 'vertical';
614 4 50       36 if ( $plot->{orientation} !~ m/^(?:horizontal|vertical)$/ ) {
615 0         0 die
616             "$current_sub needs either \"horizontal\" or \"vertical\", not \"$plot->{orientation}\"";
617             }
618 4 100       12 if (ref $plot->{data} eq 'ARRAY') {
619 1         3 my $tmp = delete $plot->{data};
620 1         4 $plot->{data}{''} = $tmp;
621             }
622 4         8 my ( @xticks, @key_order );
623 4 50       10 if ( defined $plot->{'key.order'} ) {
624 0         0 @key_order = @{ $plot->{'key.order'} };
  0         0  
625             } else {
626 4         6 @key_order = sort keys %{ $plot->{data} };
  4         17  
627             }
628 4   50     10 my $ax = $args->{ax} // '';
629             # $plot->{medians} = $plot->{medians} // 1; # by default, show median values
630 4   50     20 $plot->{notch} = $plot->{notch} // 'False';
631 4   50     14 $plot->{showcaps} = $plot->{showcaps} // 'True';
632 4   50     14 $plot->{showfliers} = $plot->{showfliers} // 'True';
633 4   50     43 $plot->{showmeans} = $plot->{showmeans} // 'True';
634 4         10 my $options = "orientation = '$plot->{orientation}'";
635 4         13 foreach my $arg ( 'showcaps', 'showfliers', 'showmeans', 'notch') {
636 16         27 $options .= ", $arg = $plot->{$arg}";
637             }
638 4         8 foreach my $axis (@{ $plot->{logscale} }) { # x, y
  4         13  
639 0 0       0 if ($axis =~ m/^([^xy])$/) {
640 0         0 p $plot->{logscale};
641 0         0 die "only \"x\" and \"y\" are allowed in boxplot, not \"$axis\"";
642             }
643 0         0 say {$args->{fh}} "ax$ax.set_$axis" . 'scale("log")';
  0         0  
644             }
645 4         9 say { $args->{fh} } 'd = []';
  4         12  
646 4         9 foreach my $key (@key_order) {
647 5         7 @{ $plot->{data}{$key} } = grep { defined } @{ $plot->{data}{$key} };
  5         11  
  61         65  
  5         10  
648 5         8 my @non_numeric = grep {!looks_like_number($_)} @{ $plot->{data}{$key} };
  61         76  
  5         8  
649 5 50       10 if (scalar @non_numeric > 0) {
650 0         0 p @non_numeric;
651 0         0 die "$key has non-numeric values";
652             }
653 5         6 say { $args->{fh} } 'd.append([' . join( ',', @{ $plot->{data}{$key} } ) . '])';
  5         8  
  5         29  
654             }
655 4         5 say { $args->{fh} } "bp = ax$ax.boxplot(d, patch_artist = True, $options)";
  4         13  
656 4 50       12 if ( defined $plot->{colors} ){ # every hash key should have its own color defined
657             # the below code helps to provide better error messages in case I make an error in calling the sub
658             my @bad_keys =
659 0         0 grep { not defined $plot->{colors}{$_} } keys %{ $plot->{data} };
  0         0  
  0         0  
660 0 0       0 if ( scalar @bad_keys > 0 ) {
661 0         0 p @bad_keys;
662 0         0 die 'the above data keys have no defined color';
663             }
664              
665             # list of pre-defined colors: https://matplotlib.org/stable/gallery/color/named_colors.html
666 0         0 print { $args->{fh} } 'colors = ["'
667 0         0 . join( '","', @{ $plot->{colors} }{@key_order} ) . '"]' . "\n";
  0         0  
668              
669             # the above color list will have the same order, via the above hash slice
670 0         0 say { $args->{fh} } 'for patch, color in zip(bp["boxes"], colors):';
  0         0  
671 0         0 say { $args->{fh} } "\tpatch.set_facecolor(color)";
  0         0  
672 0         0 say { $args->{fh} } "\tpatch.set_edgecolor('black')";
  0         0  
673             } else {
674 4         6 say { $args->{fh} } 'for pc in bp["boxes"]:';
  4         7  
675 4 50       8 if ( defined $plot->{color} ) {
676 0         0 say { $args->{fh} } "\tpc.set_facecolor('$plot->{color}')";
  0         0  
677             }
678 4         7 say { $args->{fh} } "\tpc.set_edgecolor('black')";
  4         7  
679             }
680 4         10 foreach my $key (@key_order) {
681             push @xticks, "$key ("
682 5         10 . format_commas( scalar @{ $plot->{data}{$key} }, '%.0u' ) . ')';
  5         16  
683             }
684 4 50       12 if ( $plot->{orientation} eq 'vertical' ) {
685 4         4 say { $args->{fh} } "ax$ax.set_xticks(["
  4         76  
686             . join( ',', 1 .. scalar @key_order ) . '], ["'
687             . join( '","', @xticks ) . '"])';
688             } else {
689 0         0 say { $args->{fh} } "ax$ax.set_yticks(["
  0         0  
690             . join( ',', 1 .. scalar @key_order ) . '], ["'
691             . join( '","', @xticks ) . '"])';
692             }
693             }
694              
695             sub colored_table_helper {
696 3     3 0 10 my ($args) = @_;
697 3         41 my $current_sub = (split(/::/,(caller(0))[3]))[-1]; # https://stackoverflow.com/questions/2559792/how-can-i-get-the-name-of-the-current-subroutine-in-perl
698 3 50       20 if (ref $args ne 'HASH') {
699 0         0 die "args must be given as a hash ref, e.g. \"$current_sub({ data => \@blah })\"";
700             }
701 3         17 my @reqd_args = (
702             'fh', # e.g. $py, $fh, which will be passed by the subroutine
703             'plot', # args to original function
704             );
705 3         8 my @undef_args = grep {!defined $args->{$_}} @reqd_args;
  6         15  
706 3 50       9 if (scalar @undef_args > 0) {
707 0         0 p @undef_args;
708 0         0 die "The arguments above are necessary for proper function of $current_sub and weren't defined.";
709             }
710             # optional args are below
711             my @defined_args = (@reqd_args, @ax_methods, @plt_methods, @fig_methods, @arg,
712 3         13 'ax', @{ $opt{$current_sub} });
  3         150  
713 3         6 my @bad_args = grep { my $key = $_; not grep {$_ eq $key} @defined_args} keys %{ $args };
  9         11  
  9         26  
  4212         4344  
  3         11  
714 3 50       15 if (scalar @bad_args > 0) {
715 0         0 p @bad_args;
716 0         0 say 'the above arguments are not recognized.';
717 0         0 p @defined_args;
718 0         0 die 'The above args are accepted.'
719             }
720 3         8 my $plot = $args->{plot};
721 3   50     20 $plot->{default_undefined} = $plot->{default_undefined} // 0;
722 3   100     13 $plot->{mirror} = $plot->{mirror} // 0;
723             # my @data;
724 3         7 my (@cols, @rows, %data);
725 3 100       9 if (defined $plot->{'col.labels'}) {
726 1         3 @cols = @{ $plot->{'col.labels'} };
  1         4  
727             } else {
728 2         4 @cols = sort keys %{ $plot->{data} };
  2         13  
729             }
730 3         12 foreach my $k1 (@cols) {
731 9         8 foreach my $k2 (keys %{ $plot->{data}{$k1} }) {
  9         22  
732 27         36 $data{$k1}{$k2} = $plot->{data}{$k1}{$k2};
733 27 100       46 $data{$k2}{$k1} = $data{$k1}{$k2} if $plot->{mirror} > 0;
734             }
735             }
736 3 100       7 if (defined $plot->{'row.labels'}) {
737 1         3 @rows = @{ $plot->{'row.labels'} };
  1         3  
738             } else {
739 2         4 @rows = @cols; # the matrix "d" is built with one row per entry of @cols
740             }
741 3         6 my ($min, $max) = ('inf', '-inf');
742 3         4 say {$args->{fh}} 'd = []';
  3         10  
743 3         5 say {$args->{fh}} 'import numpy as np';
  3         6  
744 3         13 foreach my $k1 (@cols) {
745 9         12 foreach my $k2 (grep {!defined $data{$k1}{$_}} @cols) {
  27         38  
746 0         0 $data{$k1}{$k2} = 'np.nan';#$plot->{default_undefined};
747             }
748 9         11 foreach my $k2 (grep {looks_like_number($data{$k1}{$_})} @cols) {
  27         42  
749 27         43 $min = min($min, $data{$k1}{$k2});
750 27         38 $max = max($max, $data{$k1}{$k2});
751             }
752 9         12 say {$args->{fh}} 'd.append([' . join (',', @{ $data{$k1} }{@cols}) . '])';
  9         12  
  9         53  
753             }
754 3   33     14 $min = $plot->{cb_min} // $min;
755 3   33     9 $max = $plot->{cb_max} // $max;
756 3   50     13 $plot->{cmap} = $plot->{cmap} // 'gist_rainbow';
757 3   50     22 $plot->{cb_logscale} = $plot->{cb_logscale} // 0;
758 3   50     8 my $ax = $args->{ax} // '';
759 3 50       6 say {$args->{fh}} 'from matplotlib import colors' if $plot->{cb_logscale} > 0;
  0         0  
760 3   100     9 $plot->{'undef.color'} = $plot->{'undef.color'} // 'gray';
761 3         6 say {$args->{fh}} 'import matplotlib';
  3         5  
762 3         5 say {$args->{fh}} "table_cmap = matplotlib.colormaps['$plot->{cmap}'].copy()";
  3         6  
763 3         5 say {$args->{fh}} 'table_cmap.set_bad("' . $plot->{'undef.color'} . '")';
  3         7  
764 3         3 say {$args->{fh}} "norm = plt.Normalize($min, $max)";
  3         7  
765 3         5 say {$args->{fh}} 'datacolors = table_cmap(norm(d))';
  3         5  
766 3         4 my @options;
767 3         10 my %translate = (cb_min => 'vmin', cb_max => 'vmax');
768 3         19 foreach my $opt (grep {defined $plot->{$_}} 'cb_min', 'cb_max'){
  6         10  
769 0 0       0 unless (looks_like_number( $plot->{$opt} )) {
770 0         0 die "\"$opt\" = $plot->{$opt} must be a number";
771             }
772 0         0 push @options, "$translate{$opt} = $plot->{$opt}";
773             }
774 3         7 my $opt = join (',', @options);
775 3 50       11 if (scalar @options > 0) {
776 0         0 $opt = ", $opt";
777             }
778 3 50       8 if ($plot->{cb_logscale}) {
779 0         0 say {$args->{fh}} 'img = ax' . $ax . '.imshow(d, cmap=table_cmap, norm=colors.LogNorm(' . join (',', @options) . '))';
  0         0  
780             } else {
781 3         5 say {$args->{fh}} "img = ax$ax.imshow(d, cmap=table_cmap $opt)";
  3         7  
782             }
783 3   50     15 $plot->{'colorbar.on'} = $plot->{'colorbar.on'} // 1;
784 3 100       8 if (defined $plot->{cblabel}) {
785 1         1 say {$args->{fh}} "fig.colorbar(img, label = '$plot->{cblabel}')";
  1         3  
786             } else {
787 2 50       16 say {$args->{fh}} "fig.colorbar(img)" if $plot->{'colorbar.on'};
  2         7  
788             }
789 3         6 say {$args->{fh}} 'img.set_visible(False)';
  3         5  
790 3   100     14 $plot->{'show.numbers'} = $plot->{'show.numbers'} // 0;
791 3         5 say {$args->{fh}} 'for ri, row in enumerate(d):';
  3         5  
792 3         6 say {$args->{fh}} ' for ii, item in enumerate(row):';
  3         5  
793 3         6 say {$args->{fh}} ' if np.isnan(item):';
  3         5  
794 3         4 say {$args->{fh}} ' d[ri][ii] = ""';
  3         7  
795 3 100       9 if ($plot->{'show.numbers'}) {
796 1         2 say {$args->{fh}} "table = ax$ax" . '.table(cellText=d, rowLabels=["' . join ('","', @rows) . '"], colLabels = ["' . join ('","', @cols) . '"], cellColours = datacolors, loc = "center", bbox=[0,0,1,1])';
  1         5  
797             } else {
798 2         3 say {$args->{fh}} "table = ax$ax" . '.table(rowLabels=["' . join ('","', @rows) . '"], colLabels = ["' . join ('","', @cols) . '"], cellColours = datacolors, loc = "center", bbox=[0,0,1,1])';
  2         9  
799             }
800 3         6 foreach my $arg (grep {defined $plot->{$_}} ('title')) {
  3         9  
801 0         0 say {$args->{fh}} "ax$ax.$arg('$plot->{$arg}')";
  0         0  
802             }
803 3         4 foreach my $axis (@{ $plot->{logscale} }) { # x, y
  3         8  
804 0 0       0 if ($axis =~ m/^([^xy])$/) {
805 0         0 p $plot->{logscale};
806 0         0 die "only \"x\" and \"y\" are allowed in boxplot, not \"$axis\"";
807             }
808 0         0 say {$args->{fh}} "ax$ax.set_$axis" . 'scale("log")';
  0         0  
809             }
810             # say {$args->{fh}} "fig.clim(vmin = $plot->{cb_min})" if defined $plot->{cb_min};
811             # say {$args->{fh}} "fig.clim(vmax = $plot->{cb_max})" if defined $plot->{cb_max};
812 3         6 foreach my $axis ('x','y') {
813 6         7 say {$args->{fh}} "ax$ax.set_${axis}ticks" . '([])';
  6         12  
814 6         6 say {$args->{fh}} "ax$ax.set_${axis}ticklabels" . '([])';
  6         64  
815             }
816             }
817              
818             sub hexbin_helper {
819 3     3 0 11 my ($args) = @_;
820 3         36 my $current_sub = ( split( /::/, ( caller(0) )[3] ) )[-1]
821             ; # https://stackoverflow.com/questions/2559792/how-can-i-get-the-name-of-the-current-subroutine-in-perl
822 3 50       30 if ( ref $args ne 'HASH' ) {
823 0         0 die "args must be given as a hash ref, e.g. \"$current_sub({ data => \@blah })\"";
824             }
825 3         14 my @reqd_args = (
826             'fh', # e.g. $py, $fh, which will be passed by the subroutine
827             'plot', # args to original function
828             );
829 3         7 my @undef_args = grep { !defined $args->{$_} } @reqd_args;
  6         12  
830 3 50       8 if ( scalar @undef_args > 0 ) {
831 0         0 p @undef_args;
832 0         0 die 'the above args are necessary, but were not defined.';
833             }
834             my @opt = (
835             @ax_methods, @fig_methods, @arg, @plt_methods,
836 3         11 'ax', @{ $opt{$current_sub} }
  3         200  
837             );
838 3         6 my $plot = $args->{plot};
839             @undef_args = grep {
840 19         22 my $key = $_;
841 19         55 not grep { $_ eq $key } @opt
  8930         9295  
842 3         6 } keys %{$plot};
  3         8  
843 3 50       14 if ( scalar @undef_args > 0 ) {
844 0         0 p @undef_args;
845 0         0 die "The above arguments aren't defined for $plot->{'plot.type'} in $current_sub";
846             }
847 3   50     17 $plot->{cb_logscale} = $plot->{cb_logscale} // 0;
848 3   50     12 $plot->{marginals} = $plot->{marginals} // 0;
849 3   50     16 $plot->{xbins} = $plot->{xbins} // 15;
850 3   50     19 $plot->{ybins} = $plot->{ybins} // 15;
851 3         7 $plot->{xbins} = int $plot->{xbins};
852 3         3 $plot->{ybins} = int $plot->{ybins};
853 3 50 33     20 if ( ( $plot->{xbins} == 0 ) || ( $plot->{ybins} == 0 ) ) {
854 0         0 p $plot;
855 0         0 die "# of bins cannot be 0 in $current_sub";
856             }
857 3         4 my @keys;
858 3 100       10 if ( defined $plot->{'key.order'} ) {
859 1         2 @keys = @{ $plot->{'key.order'} };
  1         4  
860             } else {
861 2         2 @keys = sort keys %{ $plot->{data} };
  2         11  
862             }
863 3 50       9 if ( scalar @keys != 2 ) {
864 0         0 p @keys;
865 0         0 die "There must be exactly 2 keys for $current_sub";
866             }
867 3         4 my $n_points = scalar @{ $plot->{data}{ $keys[0] } };
  3         6  
868 3 50       5 if ( scalar @{ $plot->{data}{ $keys[1] } } != $n_points ) {
  3         8  
869 0         0 say "\"$keys[0]\" has $n_points points.";
870             say "\"$keys[1]\" has "
871 0         0 . scalar @{ $plot->{data}{ $keys[1] } }
  0         0  
872             . " points.";
873 0         0 die 'The length of both keys must be equal.';
874             }
875 3   33     13 $plot->{xlabel} = $plot->{xlabel} // $keys[0];
876 3   33     10 $plot->{ylabel} = $plot->{ylabel} // $keys[1];
877 3   50     22 $plot->{cmap} = $plot->{cmap} // 'gist_rainbow';
878 3         11 my $options =
879             ", gridsize = ($plot->{xbins}, $plot->{ybins}), cmap = '$plot->{cmap}'"
880             ; # these args go to the plt.hist call
881 3 50       6 if ( $plot->{cb_logscale} ) {
882 0         0 say { $args->{fh} } 'from matplotlib.colors import LogNorm';
  0         0  
883 0         0 $options .= ', norm = LogNorm()';
884             }
885 3         6 foreach my $opt (
886 15         25 grep { defined $plot->{$_} } ('xrange', 'yrange', 'vmin', 'vmax', 'mincnt')
887             )
888             {
889 0         0 $options .= ", $opt = $plot->{$opt}";
890             }
891 3         4 foreach my $opt (grep {defined $plot->{$_} } ('xscale.hexbin', 'yscale.hexbin')) {
  6         11  
892 0 0 0     0 if (($plot->{$opt} ne 'log') && ($plot->{$opt} ne 'linear')) {
893 0         0 die "\"$opt\" is neither \"log\" nor \"linear\"";
894             }
895 0         0 my $opth = $opt;
896 0         0 $opth =~ s/\.\w+$//;
897 0         0 $options .= ", $opth = '$plot->{$opt}'";
898             }
899 3 50 33     13 if ((defined $plot->{marginals}) && ($plot->{marginals} > 0)) {
900 0         0 $options .= ', marginals = True';
901             }
902 3         6 say { $args->{fh} } 'x = ['
903 3         4 . join( ',', @{ $plot->{data}{ $keys[0] } } ) . ']';
  3         91  
904 3         5 say { $args->{fh} } 'y = ['
905 3         6 . join( ',', @{ $plot->{data}{ $keys[1] } } ) . ']';
  3         83  
906 3   50     10 my $ax = $args->{ax} // '';
907 3         3 say { $args->{fh} } "im$ax = ax$ax.hexbin(x, y $options)\n";
  3         9  
908 3         52 my $opts = '';
909 3         6 foreach my $o (grep {defined $plot->{$_}} ('cblocation', 'cborientation')) { #str; cblabel is handled separately below
  6         12  
910 0         0 my $mpl_opt = $o;
911 0         0 $mpl_opt =~ s/^cb//;
912 0         0 $opts .= ", $mpl_opt = '$plot->{$o}'";
913             }
914 3         5 foreach my $o (grep {defined $plot->{$_}} ('cbdrawedges', 'cbpad')) { # numeric
  6         10  
915 0 0       0 die "$o = $plot->{$o} must be numeric" unless (looks_like_number($plot->{$o}));
916 0         0 my $mpl_opt = $o;
917 0         0 $mpl_opt =~ s/^cb//;
918 0         0 $opts .= ", $mpl_opt = $plot->{$o}";
919             }
920 3   50     9 $plot->{'colorbar.on'} = $plot->{'colorbar.on'} // 1;
921 3 50 33     16 if (($plot->{'colorbar.on'}) && (defined $plot->{'shared.colorbar'})) {
922 0         0 my @ax = map {"ax$_"} @{ $plot->{'shared.colorbar'} };
  0         0  
  0         0  
923 0         0 $opts .= ', ax = [' . join (',', @ax) . '] ';
924             }
925 3 50       7 if ( defined $plot->{cblabel} ) {
926             write_data({
927             data => $plot->{cblabel},
928             fh => $args->{fh},
929 0         0 name => 'cblabel',
930             });
931 0         0 say { $args->{fh} } "plt.colorbar(im$ax, label = cblabel $opts)";
  0         0  
932             } else {
933 3         3 say { $args->{fh} } "plt.colorbar(im$ax, label = 'Density' $opts)";
  3         52  
934             }
935             }
936              
937             sub format_commas
938             { #($n, $format = '.%02d') { # https://stackoverflow.com/questions/33442240/perl-printf-to-use-commas-as-thousands-separator
939             # $format should be '%.0u' for integers
940 10     10 0 20 my ( $n, $format ) = @_;
941 10 50       36 $format = '.%02d' if not defined $format;
942             return
943 10         122 reverse( join( ",", unpack( "(A3)*", reverse int($n) ) ) )
944             . sprintf( $format, int( 100 * ( .005 + ( $n - int($n) ) ) ) );
945             }
946              
947             sub hist_helper {
948 25     25 0 49 my ($args) = @_;
949 25         176 my $current_sub = ( split( /::/, ( caller(0) )[3] ) )[-1]
950             ; # https://stackoverflow.com/questions/2559792/how-can-i-get-the-name-of-the-current-subroutine-in-perl
951 25 50       521 if ( ref $args ne 'HASH' ) {
952 0         0 die
953             "args must be given as a hash ref, e.g. \"$current_sub({ data => \@blah })\"";
954             }
955 25         117 my @reqd_args = (
956             'ax', # used for multiple plots
957             'fh', # e.g. $py, $fh, which will be passed by the subroutine
958             'plot', # args to original function
959             );
960 25         43 my @undef = grep { !defined $args->{$_} } @reqd_args;
  75         122  
961 25 50       48 if ( scalar @undef > 0 ) {
962 0         0 p @undef;
963 0         0 die 'the above args are necessary, but were not defined.';
964             }
965 25         81 my @opt = (@ax_methods, @plt_methods, @fig_methods, @arg, 'ax', @{ $opt{$current_sub} });
  25         1033  
966 25         51 my $plot = $args->{plot};
967             @undef = grep {
968 90         99 my $key = $_;
969 90         232 not grep { $_ eq $key } @opt
  41130         41784  
970 25         33 } keys %{$plot};
  25         60  
971 25 50       81 if ( scalar @undef > 0 ) {
972 0         0 p @undef;
973 0         0 die "The above arguments aren't defined for $plot->{'plot.type'}";
974             }
975 25         42 my $options = ''; # these args go to the plt.hist call
976 25   50     121 $plot->{alpha} = $plot->{alpha} // 0.5;
977 25         46 foreach my $arg ( grep { defined $plot->{$_} } ( 'bins', 'orientation' ) ) {
  50         98  
978 1 50       6 next if ref $plot->{$arg} eq 'HASH'; # set-specific setting exists
979 1         2 my $ref = ref $plot->{$arg};
980 1 50       5 if ( $ref eq '' ) { # single color
    50          
981 0 0       0 if ( $plot->{$arg} =~ m/^[A-Za-z]+$/ ) { # "Red" needs quotes
982 0         0 $options .= ", $arg = '$plot->{$arg}'";
983             } else { # I'm assuming numeric
984 0         0 $options .= ", $arg = $plot->{$arg}";
985             }
986             } elsif ( $ref eq 'ARRAY' ) {
987 1         3 $options .= ", $arg = [" . join( ',', @{ $plot->{$arg} } ) . ']';
  1         6  
988             } else {
989 0         0 p $plot;
990 0         0 die "$ref for $arg isn't acceptable";
991             }
992             }
993 25         38 foreach my $axis (@{ $plot->{logscale} }) { # x, y
  25         55  
994 0 0       0 if ($axis =~ m/^([^xy])$/) {
995 0         0 p $plot->{logscale};
996 0         0 die "only \"x\" and \"y\" are allowed in boxplot, not \"$axis\"";
997             }
998 0         0 say {$args->{fh}} "ax$args->{ax}.set_$axis" . 'scale("log")';
  0         0  
999             }
1000 25 100       58 if (ref $plot->{data} eq 'ARRAY') {
1001 1         3 my $tmp = delete $plot->{data};
1002 1         3 $plot->{data}{A} = $tmp;
1003             }
1004 25 100       28 if (scalar keys %{ $plot->{data} } > 1) {
  25         56  
1005 1   50     5 $plot->{'show.legend'} = $plot->{'show.legend'} // 1;
1006             } else {
1007 24   100     86 $plot->{'show.legend'} = $plot->{'show.legend'} // 0;
1008             }
1009 25         30 foreach my $set ( sort keys %{ $plot->{data} } ) {
  25         61  
1010 26         49 my @non_numeric = grep {not looks_like_number($_)} @{ $plot->{data}{$set} };
  165         229  
  26         41  
1011 26 50       45 if (scalar @non_numeric > 0) {
1012 0         0 p @non_numeric;
1013 0         0 die "$set has non-numeric values; which for hist must be numeric";
1014             }
1015 26         37 my $set_options = '';
1016 26         32 foreach
1017 52         77 my $arg ( grep { ref $plot->{$_} eq 'HASH' } ( 'bins', 'color' ) )
1018             {
1019 0 0       0 next unless defined $plot->{$arg}{$set};
1020 0 0       0 if ( $plot->{$arg}{$set} =~ m/^[A-Za-z]+$/ ) { # "Red" needs quotes
1021 0         0 $set_options .= ", $arg = '$plot->{$arg}{$set}'";
1022             } else { # I'm assuming numeric; "10" doesn't need quotes
1023 0         0 $set_options .= ", $arg = $plot->{$arg}{$set}";
1024             }
1025             }
1026 26         43 say {$args->{fh}} 'd = [' . join (',', @{ $plot->{data}{$set} }) . ']';
  26         45  
  26         128  
1027 26 100       49 if ($plot->{'show.legend'}) {
1028 2         3 say { $args->{fh} } "ax$args->{ax}.hist(d, alpha = $plot->{alpha}, label = '$set' $options $set_options)";
  2         24  
1029             } else {
1030 24         25 say { $args->{fh} } "ax$args->{ax}.hist(d, alpha = $plot->{alpha} $options $set_options)";
  24         490  
1031             }
1032             }
1033             }
1034              
1035             sub hist2d_helper {
1036 7     7 0 13 my ($args) = @_;
1037 7         81 my $current_sub = ( split( /::/, ( caller(0) )[3] ) )[-1]
1038             ; # https://stackoverflow.com/questions/2559792/how-can-i-get-the-name-of-the-current-subroutine-in-perl
1039 7 50       39 if ( ref $args ne 'HASH' ) {
1040 0         0 die "args must be given as a hash ref, e.g. \"$current_sub({ data => \@blah })\"";
1041             }
1042 7         22 my @reqd_args = (
1043             'fh', # e.g. $py, $fh, which will be passed by the subroutine
1044             'plot', # args to original function
1045             );
1046 7         14 my @undef_args = grep { !defined $args->{$_} } @reqd_args;
  14         29  
1047 7 50       16 if ( scalar @undef_args > 0 ) {
1048 0         0 p @undef_args;
1049 0         0 die 'the above args are necessary, but were not defined.';
1050             }
1051 7         25 my @opt = (@ax_methods, @plt_methods, @fig_methods, @arg, 'ax', @{ $opt{$current_sub} });
  7         291  
1052 7         18 my $plot = $args->{plot};
1053             @undef_args = grep {
1054 49         70 my $key = $_;
1055 49         136 not grep { $_ eq $key } @opt
  23275         24067  
1056 7         9 } keys %{$plot};
  7         38  
1057 7 50       32 if ( scalar @undef_args > 0 ) {
1058 0         0 p @undef_args;
1059 0         0 die
1060             "The above arguments aren't defined for $plot->{'plot.type'} in $current_sub";
1061             }
1062 7   100     35 $plot->{cb_logscale} = $plot->{cb_logscale} // 0;
1063 7   100     31 $plot->{'show.colorbar'} = $plot->{'show.colorbar'} // 1;
1064 7   50     36 $plot->{xbins} = int( $plot->{xbins} // 15 );
1065 7   50     26 $plot->{ybins} = int( $plot->{ybins} // 15 );
1066 7 50 33     26 if ( ( $plot->{xbins} == 0 ) || ( $plot->{ybins} == 0 ) ) {
1067 0         0 p $plot;
1068 0         0 die "# of bins cannot be 0 in $current_sub";
1069             }
1070 7         12 my @keys;
1071 7 50       15 if ( defined $plot->{'key.order'} ) {
1072 0         0 @keys = @{ $plot->{'key.order'} };
  0         0  
1073             } else {
1074 7         8 @keys = sort keys %{ $plot->{data} };
  7         58  
1075             }
1076 7 50       16 if ( scalar @keys != 2 ) {
1077 0         0 p @keys;
1078 0         0 die "There must be exactly 2 keys for $current_sub";
1079             }
1080 7         9 my $n_points = scalar @{ $plot->{data}{ $keys[0] } };
  7         17  
1081 7 50       9 if ( scalar @{ $plot->{data}{ $keys[1] } } != $n_points ) {
  7         15  
1082 0         0 say "$keys[0] has $n_points points.";
1083             say "$keys[1] has "
1084 0         0 . scalar @{ $plot->{data}{ $keys[1] } }
  0         0  
1085             . " points.";
1086 0         0 die 'The length of both keys must be equal.';
1087             }
1088 7   33     33 $plot->{xlabel} = $plot->{xlabel} // $keys[0];
1089 7   33     31 $plot->{ylabel} = $plot->{ylabel} // $keys[1];
1090 7   100     23 $plot->{cmap} = $plot->{cmap} // 'gist_rainbow';
1091 7         18 my $options = ", cmap = '$plot->{cmap}'"; # these args go to the plt.hist call
1092 7 100       17 if ( $plot->{cb_logscale} ) {
1093 1         2 say {$args->{fh}} 'from matplotlib.colors import LogNorm';
  1         4  
1094             # prevents "ValueError: Passing a Normalize instance simultaneously with vmin/vmax is not supported. Please pass vmin/vmax directly to the norm when creating it"
1095 1         3 my @logNorm_opt;
1096 1         2 foreach my $arg (grep {defined $plot->{$_}} ('vmin', 'vmax')) {
  2         4  
1097 0 0       0 if (not looks_like_number($plot->{$arg})) {
1098 0         0 die "$arg must be numeric for $current_sub, but was given \"$plot->{$arg}\"";
1099             }
1100 0         0 push @logNorm_opt, "$arg = $plot->{$arg}";
1101 0         0 delete $plot->{$arg};
1102             }
1103 1         3 $options .= ', norm = LogNorm(' . join (',', @logNorm_opt) . ')';
1104             }
1105 7         12 foreach my $opt ( grep { defined $plot->{$_} } ( 'cmin', 'cmax', 'density', 'vmin', 'vmax' ) )
  35         50  
1106             {
1107 2         5 $options .= ", $opt = $plot->{$opt}";
1108             }
1109 7         9 my @bad_indices;
1110 7         11 my $bad_pts = 0;
1111 7         12 foreach my $i (0,1) {
1112 14         29 @{ $bad_indices[$i] } = grep {not defined $plot->{data}{$keys[$i]}[$_]} 0..$n_points-1;
  14         26  
  420         490  
1113 14         22 $bad_pts += scalar @{ $bad_indices[$i] };
  14         24  
1114             }
1115 7 50       15 if ($bad_pts > 0) {
1116 0         0 say STDERR "the above args have the following indices undefined ($n_points total)";
1117 0         0 p @bad_indices;
1118 0         0 die "Cannot proceed as there are $bad_pts undefined points.";
1119             }
1120 7         15 foreach my $i (0,1) {
1121 14         22 @{ $bad_indices[$i] } = grep {not looks_like_number($plot->{data}{$keys[$i]}[$_])} 0..$n_points-1;
  14         16  
  420         602  
1122 14         22 $bad_pts += scalar @{ $bad_indices[$i] };
  14         16  
1123             }
1124 7 50       17 if ($bad_pts > 0) {
1125 0         0 p $args;
1126 0         0 say STDERR "the above args have the following indices non-numeric ($n_points total)";
1127 0         0 p @bad_indices;
1128 0         0 die "Cannot proceed as there are $bad_pts non-numeric points.";
1129             }
1130 7         14 say { $args->{fh} } 'x = ['
1131 7         8 . join( ',', @{ $plot->{data}{ $keys[0] } } ) . ']';
  7         181  
1132 7   33     21 $plot->{xmin} = $plot->{xmin} // min( @{ $plot->{data}{ $keys[0] } } );
  7         33  
1133 7   33     17 $plot->{xmax} = $plot->{xmax} // max( @{ $plot->{data}{ $keys[0] } } );
  7         26  
1134 7         10 say { $args->{fh} } 'y = ['
1135 7         10 . join( ',', @{ $plot->{data}{ $keys[1] } } ) . ']';
  7         178  
1136 7   33     17 $plot->{ymin} = $plot->{ymin} // min( @{ $plot->{data}{ $keys[1] } } );
  7         28  
1137 7   33     15 $plot->{ymax} = $plot->{ymax} // max( @{ $plot->{data}{ $keys[1] } } );
  7         23  
1138 7   50     21 my $ax = $args->{ax} // '';
1139             # the range argument ensures that there are no empty parts of the plot
1140 7         27 my $range = ", range = [($plot->{xmin}, $plot->{xmax}), ($plot->{ymin}, $plot->{ymax})]";
1141             # logscale complications
1142 7         9 say {$args->{fh}} 'import numpy as np';
  7         18  
1143 7 50       14 if ($plot->{logscale}) {
1144 0         0 my %linear_axes = ('x' => 1, 'y' => 1);
1145 0         0 foreach my $axis (@{ $plot->{logscale} }) { # x, y
  0         0  
1146 0 0       0 if ($axis =~ m/^([^xy])$/) {
1147 0         0 p $plot->{logscale};
1148 0         0 die "only \"x\" and \"y\" are allowed in boxplot, not \"$axis\"";
1149             }
1150 0         0 say {$args->{fh}} "ax$ax.set_$axis" . 'scale("log")';
  0         0  
1151 0         0 my $min = $plot->{$axis . 'min'};
1152 0         0 my $max = $plot->{$axis . 'max'};
1153 0         0 my $key = "${axis}bins";
1154 0         0 say {$args->{fh}} "${axis}bins = np.logspace(np.log10($min), np.log10($max), $plot->{$key})";
  0         0  
1155 0         0 delete $linear_axes{$axis}; # so that I don't make a linear space for it
1156             }
1157 0         0 foreach my $axis ( keys %linear_axes ) {
1158 0         0 my $min = $plot->{$axis . 'min'};
1159 0         0 my $max = $plot->{$axis . 'max'};
1160 0         0 my $key = "${axis}bins";
1161 0         0 say {$args->{fh}} "${axis}bins = np.linspace($min, $max, $plot->{$key})";
  0         0  
1162             }
1163 0         0 $options = ", bins = [xbins, ybins] $options";
1164             } else {
1165 7         20 $options = ", ($plot->{xbins}, $plot->{ybins}) $options";
1166             }
1167 7         9 say {$args->{fh}} "hist2d_n, hist2d_xedges, hist2d_yedges, im$ax = ax$ax.hist2d(x, y $options $range)";
  7         15  
1168 7         11 say {$args->{fh}} 'max_hist2d_box = np.max(hist2d_n)';
  7         10  
1169 7         9 say {$args->{fh}} 'min_hist2d_box = np.min(hist2d_n)';
  7         10  
1170 7         9 say {$args->{fh}} "print(f'plot $ax hist2d density range = [{min_hist2d_box}, {max_hist2d_box}]')";
  7         12  
1171 7 100       34 return 0 if $plot->{'show.colorbar'} == 0;
1172 6         11 my $opts = '';
1173 6         6 foreach my $o (grep {defined $plot->{$_}} ('cblocation', 'cborientation')) { #str; cblabel is handled separately below
  12         25  
1174 1         1 my $mpl_opt = $o;
1175 1         4 $mpl_opt =~ s/^cb//;
1176 1         4 $opts .= ", $mpl_opt = '$plot->{$o}'";
1177             }
1178 6         8 foreach my $o (grep {defined $plot->{$_}} ('cbdrawedges', 'cbpad')) { # numeric
  12         20  
1179 2 50       6 die "$o = $plot->{$o} must be numeric" unless (looks_like_number($plot->{$o}));
1180 2         9 my $mpl_opt = $o;
1181 2         6 $mpl_opt =~ s/^cb//;
1182 2         6 $opts .= ", $mpl_opt = $plot->{$o}";
1183             }
1184 6   100     25 $plot->{'colorbar.on'} = $plot->{'colorbar.on'} // 1;
1185 6 100 100     24 if (($plot->{'colorbar.on'}) && (defined $plot->{'shared.colorbar'})) {
1186 1         2 my @ax = map {"ax$_"} @{ $plot->{'shared.colorbar'} };
  2         6  
  1         3  
1187 1         3 $opts .= ', ax = [' . join (',', @ax) . '] ';
1188             }
1189             # say { $args->{fh} } "cbar = fig.colorbar(im$ax $opts)" if $plot->{'colorbar.on'};
1190 6 100       12 if ( defined $plot->{cblabel} ) {
1191 1         2 say { $args->{fh} } "plt.colorbar(im$ax, label = '$plot->{cblabel}' $opts)";
  1         61  
1192             } else {
1193 5         7 say { $args->{fh} } "plt.colorbar(im$ax, label = 'Density' $opts)";
  5         157  
1194             }
1195             }
1196              
1197             sub imshow_helper {
1198 5     5 0 12 my ($args) = @_;
1199 5         69 my $current_sub = ( split( /::/, ( caller(0) )[3] ) )[-1]
1200             ; # https://stackoverflow.com/questions/2559792/how-can-i-get-the-name-of-the-current-subroutine-in-perl
1201 5 50       30 if ( ref $args ne 'HASH' ) {
1202 0         0 die
1203             "args must be given as a hash ref, e.g. \"$current_sub({ data => \@blah })\"";
1204             }
1205 5         28 my @reqd_args = (
1206             'ax',
1207             'fh', # e.g. $py, $fh, which will be passed by the subroutine
1208             'plot', # args to original function
1209             );
1210 5         13 my @undef_args = grep { !defined $args->{$_} } @reqd_args;
  15         34  
1211 5 50       17 if ( scalar @undef_args > 0 ) {
1212 0         0 p @undef_args;
1213 0         0 die 'the above args are necessary, but were not defined.';
1214             }
1215 5         23 my @opt = (@ax_methods, @plt_methods, @fig_methods, @arg, 'ax', @{ $opt{$current_sub} });
  5         312  
1216 5         14 my $plot = $args->{plot};
1217             @undef_args = grep {
1218 34         40 my $key = $_;
1219 34         97 not grep { $_ eq $key } @opt
  15878         16578  
1220 5         30 } keys %{$plot};
  5         20  
1221 5 50       21 if ( scalar @undef_args > 0 ) {
1222 0         0 p @undef_args;
1223 0         0 die
1224             "The above arguments aren't defined for $plot->{'plot.type'} in $current_sub";
1225             }
1226 5         15 my $data_ref = ref $plot->{data};
1227 5 100       13 if ($data_ref ne 'ARRAY') {
1228 1         6 p $args;
1229 1         7595 die "$current_sub can only accept 2-D arrays as input in \"data\", but received $data_ref";
1230             }
1231 4         6 my $non_numeric_data = 0;
1232 4         6 foreach my $row (@{ $plot->{data} }) {
  4         16  
1233 7 100       12 if (grep {not looks_like_number($_)} @{ $row }) {
  18         46  
  7         19  
1234 1         4 $non_numeric_data = 1;
1235 1         4 last;
1236             }
1237             }
1238 4 50 66     27 if (($non_numeric_data) && (not defined $plot->{stringmap})) {
1239 0         0 p $args;
1240 0         0 die "$current_sub needs a map to translate strings to integers";
1241             }
1242 4         10 my (@ytick_labels, %intmap);
1243             # assign integers for each key in stringmap
1244 4         7 foreach my $string (sort keys %{ $plot->{stringmap} }) {
  4         19  
1245 3         6 $intmap{$string} = scalar @ytick_labels;
1246 3         7 push @ytick_labels, $plot->{stringmap}{$string};
1247             }
1248 4 100       11 if ($non_numeric_data) {
1249 1         2 foreach my $row (@{ $plot->{data} }) {
  1         4  
1250 2         3 @{ $row } = map { $intmap{$_} } @{ $row };
  2         5  
  4         7  
  2         2  
1251             }
1252             }
1253 4         11 my ($min_val, $max_val) = ('inf', '-inf');
1254 4         8 my $opts = '';
1255 4 100       10 if ($non_numeric_data) {
1256 1         3 say { $args->{fh} } 'from matplotlib.colors import ListedColormap';
  1         4  
1257 1         7 my @prop_cycle = ('#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2',
1258             '#7f7f7f', '#bcbd22', '#17becf'); #plt.rcParams['axes.prop_cycle']
1259 1         2 say { $args->{fh} } 'colors = ["' . join ('","', @prop_cycle[0..(scalar keys %intmap) - 1]) . '"]';
  1         7  
1260 1         1 say { $args->{fh} } 'this_cmap = ListedColormap(colors)';
  1         3  
1261 1 50       4 warn "deleting \"$plot->{cmap}\" because string data is present" if defined $plot->{cmap};
1262 1         2 delete $plot->{cmap};
1263 1         2 $min_val = 0;
1264 1         5 $max_val = max(values %intmap);
1265 1         3 $opts .= ', cmap = this_cmap';
1266             }
1267             write_data({
1268             data => $plot->{data},
1269             fh => $args->{fh},
1270 4         22 name => 'd',
1271             });
1272 4         10 foreach my $row (@{ $plot->{data} }) { # write data to the python file
  4         9  
1273 8 100       14 next if $non_numeric_data; # strings don't have max and min
1274 6         8 $min_val = min(@{ $row }, $min_val);
  6         18  
1275 6         6 $max_val = max(@{ $row }, $max_val);
  6         13  
1276             }
1277 4   50     12 my $ax = $args->{ax} // '';
1278 4   33     19 $plot->{vmax} = $plot->{vmax} // $max_val;
1279 4   66     22 $plot->{vmin} = $plot->{vmin} // $min_val;
1280 4         8 foreach my $opt (grep {defined $plot->{$_}} ('cmap')) { # strings
  4         10  
1281 0         0 $opts .= ", $opt = '$plot->{$opt}'";
1282             }
1283 4         8 foreach my $opt (grep {defined $plot->{$_}} ('vmax', 'vmin')) { # numeric
  8         13  
1284 8         17 $opts .= ", $opt = $plot->{$opt}";
1285             }
1286 4         5 say { $args->{fh} } "im$ax = ax$ax.imshow(d, aspect = 'auto' $opts)";
  4         10  
1287 4         11 $opts = '';
1288 4 100       9 if ($non_numeric_data) {
1289 1         3 $opts .= ', ticks = [' . join (',', map {$_ + 0.5 - $_/$max_val} 0..$max_val) . ']';
  3         13  
1290             }
1291 4         8 foreach my $o (grep {defined $plot->{$_}} ('cblabel', 'cblocation', 'cborientation')) { #str
  12         22  
1292 0         0 my $mpl_opt = $o;
1293 0         0 $mpl_opt =~ s/^cb//;
1294 0         0 $opts .= ", $mpl_opt = '$plot->{$o}'";
1295             }
1296 4         7 foreach my $o (grep {defined $plot->{$_}} ('cbdrawedges', 'cbpad')) { # numeric
  8         13  
1297 1 50       4 die "$o = $plot->{$o} must be numeric" unless (looks_like_number($plot->{$o}));
1298 1         2 my $mpl_opt = $o;
1299 1         4 $mpl_opt =~ s/^cb//;
1300 1         7 $opts .= ", $mpl_opt = $plot->{$o}";
1301             }
1302 4   50     26 $plot->{'colorbar.on'} = $plot->{'colorbar.on'} // 1;
1303 4 50 33     22 if (($plot->{'colorbar.on'}) && (defined $plot->{'shared.colorbar'})) {
1304 0         0 my @ax = map {"ax$_"} @{ $plot->{'shared.colorbar'} };
  0         0  
  0         0  
1305 0         0 $opts .= ', ax = [' . join (',', @ax) . '] ';
1306             }
1307 4 50       9 say { $args->{fh} } "cbar = fig.colorbar(im$ax $opts)" if $plot->{'colorbar.on'};
  4         10  
1308 4 50 66     57 if (($non_numeric_data) && ($plot->{'colorbar.on'})) {
1309 1         2 say { $args->{fh} } 'cbar.ax.set_yticklabels(["' . join ('","', @ytick_labels) . '"])';
  1         21  
1310             }
1311             }
1312              
1313             sub pie_helper {
1314 2     2 0 5 my ($args) = @_;
1315 2         24 my $current_sub = ( split( /::/, ( caller(0) )[3] ) )[-1]
1316             ; # https://stackoverflow.com/questions/2559792/how-can-i-get-the-name-of-the-current-subroutine-in-perl
1317 2 50       11 if ( ref $args ne 'HASH' ) {
1318 0         0 die
1319             "args must be given as a hash ref, e.g. \"$current_sub({ data => \@blah })\"";
1320             }
1321 2         13 my @reqd_args = (
1322             'ax',
1323             'fh', # e.g. $py, $fh, which will be passed by the subroutine
1324             'plot', # args to original function
1325             );
1326 2         4 my @undef_args = grep { !defined $args->{$_} } @reqd_args;
  6         13  
1327 2 50       4 if ( scalar @undef_args > 0 ) {
1328 0         0 p @undef_args;
1329 0         0 die 'the above args are necessary, but were not defined.';
1330             }
1331 2         8 my @opt = (@ax_methods, @plt_methods, @fig_methods, @arg, 'ax', @{ $opt{$current_sub} });
  2         74  
1332 2         4 my $plot = $args->{plot};
1333             my @undef_opt = grep {
1334 12         14 my $key = $_;
1335 12         30 not grep { $_ eq $key } @opt
  5424         5702  
1336 2         4 } keys %{$plot};
  2         6  
1337 2 50       10 if ( scalar @undef_opt > 0 ) {
1338 0         0 p @undef_opt;
1339 0         0 die
1340             "The above arguments aren't defined for $plot->{'plot.type'} in $current_sub";
1341             }
1342 2         4 my @key_order;
1343 2 50       11 if ( defined $plot->{'key.order'} ) {
1344 0         0 @key_order = @{ $plot->{'key.order'} };
  0         0  
1345             } else {
1346 2         6 @key_order = sort keys %{ $plot->{data} };
  2         14  
1347             }
1348 2   50     22 $plot->{autopct} = $plot->{autopct} // '';
1349 2         4 my $opt = '';
1350 2 50       8 if ( $plot->{autopct} ne '' ) {
1351 0         0 $opt .= ", autopct = '$plot->{autopct}'";
1352             }
1353 2         4 foreach my $arg ( grep { defined $plot->{$_} } 'labeldistance', 'pctdistance' ) {
  4         16  
1354 0         0 $opt .= ", $arg = $plot->{$arg}";
1355             }
1356             write_data({
1357             data => \@key_order,
1358             fh => $args->{fh},
1359 2         16 name => 'labels',
1360             });
1361 2         5 say { $args->{fh} } 'vals = ['
1362 2         4 . join( ',', @{ $plot->{data} }{@key_order} ) . ']';
  2         59  
1363 2   50     8 my $ax = $args->{ax} // '';
1364 2         3 say { $args->{fh} } "ax$ax.pie(vals, labels = labels $opt)";
  2         34  
1365             }
1366              
1367             sub plot_helper {
1368 13     13 0 33 my ($args) = @_;
1369 13         167 my $current_sub = ( split( /::/, ( caller(0) )[3] ) )[-1]
1370             ; # https://stackoverflow.com/questions/2559792/how-can-i-get-the-name-of-the-current-subroutine-in-perl
1371 13 50       70 if ( ref $args ne 'HASH' ) {
1372 0         0 die "args must be given as a hash ref, e.g. \"$current_sub({ data => \@blah })\"";
1373             }
1374 13         40 my @reqd_args = (
1375             'ax', 'fh',# e.g. $py, $fh, which will be passed by the subroutine
1376             'plot', # args to original function
1377             );
1378 13         29 my @undef_args = grep { !defined $args->{$_} } @reqd_args;
  39         80  
1379 13 50       36 if ( scalar @undef_args > 0 ) {
1380 0         0 p @undef_args;
1381 0         0 die 'the above args are necessary, but were not defined.';
1382             }
1383 13         46 my @opt = (@ax_methods, @fig_methods, @arg, @plt_methods, 'ax', @{ $opt{$current_sub} });
  13         616  
1384 13         30 my $plot = $args->{plot};
1385             my @bad_opt = grep {
1386 92         98 my $key = $_;
1387 92         283 not grep { $_ eq $key } @opt
  41768         43199  
1388 13         22 } keys %{$plot};
  13         45  
1389 13 50       46 if ( scalar @bad_opt > 0 ) {
1390 0         0 p $args;
1391 0         0 p @bad_opt;
1392 0         0 die "The above arguments aren't defined for $plot->{'plot.type'} in $current_sub";
1393             }
1394 13   100     68 $plot->{'show.legend'} = $plot->{'show.legend'} // 1;
1395 13         18 foreach my $axis (@{ $plot->{logscale} }) { # x, y
  13         45  
1396 0 0       0 if ($axis =~ m/^([^xy])$/) {
1397 0         0 p $plot->{logscale};
1398 0         0 die "only \"x\" and \"y\" are allowed in boxplot, not \"$axis\"";
1399             }
1400 0         0 say {$args->{fh}} "ax$args->{ax}.set_$axis" . 'scale("log")';
  0         0  
1401             }
1402 13         21 my @twinx;
1403             # Allow a single line entered as two bare array refs, with no enclosing
1404             # pair-array and no hash key:
1405             # data => [ \@x, \@y ]
1406             # Promote it to the canonical array-of-pairs form so the array path below
1407             # handles it unchanged:
1408             # data => [ [ \@x, \@y ] ]
1409             # The discriminator vs. the multi-line form (data => [ [\@x,\@y], ... ]) is
1410             # data->[0][0]: a number here, an ARRAY ref in the multi-line form.
1411 13 100 66     79 if ( ( ref $plot->{data} eq 'ARRAY' )
      66        
      33        
      66        
1412 6         61 && ( scalar @{ $plot->{data} } == 2 )
1413             && ( ref $plot->{data}[0] eq 'ARRAY' )
1414             && ( ref $plot->{data}[1] eq 'ARRAY' )
1415             && ( ref $plot->{data}[0][0] ne 'ARRAY' ) )
1416             {
1417 2         6 $plot->{data} = [ $plot->{data} ];
1418             }
1419 13 100       33 if (ref $plot->{data} eq 'ARRAY') {
1420 6 100       16 if (defined $plot->{'set.options'}) {
1421 4         7 my $ref_type = ref $plot->{'set.options'};
1422             # a scalar applies to every line (handy for the single-line form);
1423             # an array gives one option string per line (positional)
1424 4 100       14 if ($ref_type eq 'ARRAY') {
    50          
1425 2         4 my $n_set_opt = scalar @{ $plot->{'set.options'} };
  2         3  
1426 2         5 my $n_data = scalar @{ $plot->{data} };
  2         4  
1427 2 50       7 if ($n_set_opt > $n_data) {
1428 0         0 p $args;
1429 0         0 die "there are $n_set_opt sets for options, but only $n_data data points.";
1430             }
1431             } elsif ($ref_type ne '') {
1432 0         0 p $args;
1433 0         0 die "\"set.options\" must be a scalar (applied to every line) or an array (one per line) when data is an array, but \"$ref_type\" was given.";
1434             }
1435             }
1436 6 50       15 if (defined $plot->{twinx}) {
1437 0 0       0 if (ref $plot->{twinx} eq '') {
    0          
1438 0 0       0 die "twinx must be an array index, not \"$plot->{twinx}\"" unless $plot->{twinx} =~ m/^\d+$/;
1439 0         0 @twinx = $plot->{twinx};
1440             } elsif (ref $plot->{twinx} eq 'ARRAY') {
1441 0         0 @bad_opt = grep {$_ !~ m/^\d+$/} @{ $plot->{twinx} };
  0         0  
  0         0  
1442 0 0       0 if (scalar @bad_opt > 0) {
1443 0         0 p @bad_opt;
1444 0         0 die 'data that could not possibly be an array index for twinx is shown above';
1445             }
1446 0         0 @twinx = @{ $plot->{twinx} };
  0         0  
1447             }
1448             }
1449 6 100       11 if (defined $plot->{'twinx.args'}) {
1450 1         3 my $ref = ref $plot->{'twinx.args'};
1451 1 50       3 die "\"twinx.args\" must be a hash, but $ref was entered" unless $ref eq 'HASH';
1452 1         2 @bad_opt = grep {$_ !~ m/^\d+$/} keys %{ $plot->{'twinx.args'} };
  1         6  
  1         3  
1453 1 50       3 if (scalar @bad_opt > 0) {
1454 0         0 p @bad_opt;
1455 0         0 die 'the above keys are not hash indices';
1456             }
1457 1         2 foreach my $idx (keys %{ $plot->{'twinx.args'} }) {
  1         3  
1458 1 50       4 next if grep {$idx == $_} @twinx;
  0         0  
1459 1         2 push @twinx, $idx;
1460             }
1461             }
1462 6         9 my $arr_i = 0;
1463 6         7 foreach my $arr (@{ $plot->{data} }) {
  6         12  
1464 10         15 my $ref = ref $arr;
1465 10 50       18 if ($ref ne 'ARRAY') {
1466 0         0 p $plot->{data}[$arr_i];
1467 0         0 die "index $arr_i is \"$ref\" but must be \"ARRAY\"";
1468             }
1469 10         10 my $n_elem = scalar @{ $arr };
  10         14  
1470 10 50       16 if ($n_elem != 2) {
1471 0         0 p $arr;
1472 0         0 die "there must be 2 array references (x, y) but index $arr_i has $n_elem";
1473             }
1474 10         16 @bad_opt = map { scalar @{ $arr->[$_] } } (0,1);
  20         19  
  20         35  
1475 10 50       19 if ($bad_opt[0] != $bad_opt[1]) {
1476 0         0 say STDERR "index $arr_i must have arrays/array refs of equal length, but these lengths were given:";
1477 0         0 p @bad_opt;
1478 0         0 die 'the array/array ref lengths must be equal';
1479             }
1480 10         12 foreach my $i (0,1) {
1481 20         22 my $max_i = scalar @{ $arr->[$i] } - 1;
  20         40  
1482 20         30 @bad_opt = grep {not looks_like_number($arr->[$i][$_])} 0..$max_i;
  156         208  
1483 20 50       53 next if scalar @bad_opt == 0; # it's fine, don't worry
1484 0         0 p $plot->{data}[$arr_i];
1485 0         0 p @bad_opt;
1486 0         0 @bad_opt = @{ $arr->[$i] }[@bad_opt];
  0         0  
1487 0         0 say STDERR 'have these non-numeric values:';
1488 0         0 p @bad_opt;
1489 0         0 die "Array index $arr_i/axis $i has non-numeric values (above)";
1490             }
1491 10         16 my $options = '';
1492 10         13 say { $args->{fh} } 'x = [' . join( ',', @{ $arr->[0] } ) . ']';
  10         19  
  10         92  
1493 10         15 say { $args->{fh} } 'y = [' . join( ',', @{ $arr->[1] } ) . ']';
  10         33  
  10         149  
1494 10 100       28 if ( defined $plot->{'set.options'} ) {
1495 7         13 my $so_ref = ref $plot->{'set.options'};
1496 7 100 33     42 if ( $so_ref eq '' ) { # scalar: same options for every line
    50          
1497 3         24 $options = ", $plot->{'set.options'}";
1498             } elsif ( ( $so_ref eq 'ARRAY' )
1499             && ( defined $plot->{'set.options'}[$arr_i] ) ) {
1500 4         9 $options = ", $plot->{'set.options'}[$arr_i]"; # one per line
1501             }
1502             }
1503 10         19 my $ax = "ax$args->{ax}";
1504 10 100       21 if (grep {$arr_i == $_} @twinx) {
  2         6  
1505 1         3 say { $args->{fh} } "twinx_$ax = $ax.twinx()# " . __LINE__;
  1         4  
1506 1         2 say { $args->{fh} } "twinx_$ax.plot(x, y $options) # " . __LINE__;
  1         3  
1507 1 50       4 if (defined $plot->{'twinx.args'}{$arr_i}) {
1508             plot_args({
1509             fh => $args->{fh},
1510 1         7 args => $plot->{'twinx.args'}{$arr_i},
1511             ax => "twinx_$ax"
1512             });
1513             }
1514             } else {
1515 9         11 say { $args->{fh} } "ax$args->{ax}.plot(x, y $options) # " . __LINE__;
  9         26  
1516             }
1517 10         19 $arr_i++;
1518             }
1519 6         110 return 1; # the rest only applies if $plot->{data} is a hash
1520             }
1521 7         10 my @key_order;
1522 7 50       20 if ( defined $plot->{'key.order'} ) {
1523 0         0 @key_order = @{ $plot->{'key.order'} };
  0         0  
1524             } else {
1525 7         9 @key_order = sort keys %{ $plot->{data} };
  7         33  
1526             }
1527 7 100 66     37 if ((defined $plot->{'set.options'}) && (ref $plot->{'set.options'} eq 'HASH')) {
1528 2         4 @bad_opt = sort grep {!defined $plot->{data}{$_}} keys %{ $plot->{'set.options'} };
  3         10  
  2         8  
1529 2 50       7 if (scalar @bad_opt > 0) {
1530 0         0 p @bad_opt;
1531 0         0 die "the above options are defined for undefined data sets in $current_sub.";
1532             }
1533             }
1534 7 50       36 if (defined $plot->{twinx}) {
1535 0 0       0 if (ref $plot->{twinx} eq '') {
    0          
1536 0 0       0 die "twinx \"$plot->{twinx}\" is not a key in data" unless defined $plot->{data}{ $plot->{twinx} };
1537 0         0 @twinx = ($plot->{twinx});
1538             } elsif (ref $plot->{twinx} eq 'HASH') {
1539 0         0 @bad_opt = sort grep {!defined $plot->{data}{$_}} keys %{ $plot->{twinx} };
  0         0  
  0         0  
1540 0 0       0 if (scalar @bad_opt > 0) {
1541 0         0 p @bad_opt;
1542 0         0 die 'data for undefined data keys is shown above';
1543             }
1544 0         0 @twinx = sort keys %{ $plot->{twinx} };
  0         0  
1545             }
1546             }
1547 7 100       16 if (defined $plot->{'twinx.args'}) {
1548 1         156 my $ref = ref $plot->{'twinx.args'};
1549 1 50       7 die "\"twinx.args\" must be a hash, but $ref was entered" unless $ref eq 'HASH';
1550 1         2 @bad_opt = sort grep {!defined $plot->{data}{$_}} keys %{ $plot->{'twinx.args'} };
  1         3  
  1         3  
1551 1 50       3 if (scalar @bad_opt > 0) {
1552 0         0 p @bad_opt;
1553 0         0 die 'the above keys are not present in data keys';
1554             }
1555 1         2 foreach my $set (keys %{ $plot->{'twinx.args'} }) {
  1         3  
1556 1 50       3 next if grep {$set eq $_} @twinx;
  0         0  
1557 1         2 push @twinx, $set;
1558             }
1559             }
1560 7         12 my $set_i = 0;
1561 7         12 foreach my $set (@key_order) {
1562 8         19 my $set_ref = ref $plot->{data}{$set};
1563 8 50       16 if ( $set_ref ne 'ARRAY' ) {
1564 0         0 p $plot->{data}{$set};
1565 0         0 die "$set must have two arrays, x and y coordinates, but instead has a $set_ref";
1566             }
1567 8         10 my $n_arrays = scalar @{ $plot->{data}{$set} };
  8         14  
1568 8 50       18 if ( $n_arrays != 2 ) {
1569 0         0 p $plot->{data}{$set};
1570 0         0 die "$n_arrays were entered for $set, but there must be exactly 2";
1571             }
1572 8         17 my @n_elem = map { scalar @{ $plot->{data}{$set}[$_] }} (0,1);
  16         16  
  16         48  
1573 8 50       17 if ( $n_elem[0] != $n_elem[1] ) {
1574 0         0 p $plot->{data}{$set};
1575 0         0 p @n_elem;
1576 0         0 die "$set has length = $n_elem[0] for x; length = $n_elem[1] for y: x & y must be of equal length";
1577             }
1578 8         15 foreach my $ax (0,1) {
1579 16         14 my $n = scalar @{ $plot->{data}{$set}[$ax] };
  16         22  
1580 16         38 my @undef_i = grep {not defined $plot->{data}{$set}[$ax][$_]} 0..$n-1;
  450         529  
1581 16 50       34 if (scalar @undef_i > 0) {
1582 0         0 p $plot;
1583 0         0 p @undef_i;
1584 0         0 my $max_i = scalar @{ $plot->{data}{$set}[$ax] };
  0         0  
1585 0         0 die "set $set axis $ax has undefined indices, of max index $max_i in $current_sub";
1586             }
1587             }
1588 8         13 my $options = '';
1589 8         9 say { $args->{fh} } 'x = [' . join( ',', @{ $plot->{data}{$set}[0] } ) . ']';
  8         16  
  8         232  
1590 8         14 say { $args->{fh} } 'y = [' . join( ',', @{ $plot->{data}{$set}[1] } ) . ']';
  8         14  
  8         244  
1591 8 50 66     33 if ( ( defined $plot->{'set.options'} )
1592             && ( ref $plot->{'set.options'} eq '' ) )
1593             {
1594 0         0 $options = ", $plot->{'set.options'}";
1595             }
1596 8 100 66     31 if ( ( ref $plot->{'set.options'} eq 'HASH' ) && ( defined $plot->{'set.options'}{$set} ) ) {
1597 3         7 $options = ", $plot->{'set.options'}{$set}";
1598             }
1599 8         11 my $label = '';
1600 8 100       21 if ( $plot->{'show.legend'} ) {
1601 4         8 $label = ",label = '$set'";
1602             }
1603 8         15 my $ax = "ax$args->{ax}";
1604 8 100       19 if (grep {$set eq $_} @twinx) { # this set has
  2         8  
1605 1         3 say { $args->{fh} } "twinx_${ax}_$set_i = $ax.twinx()# " . __LINE__;
  1         4  
1606 1         3 say { $args->{fh} } "twinx_${ax}_$set_i.plot(x, y $label $options) # " . __LINE__;
  1         5  
1607 1 50       5 if (defined $plot->{'twinx.args'}{$set}) {
1608             plot_args({
1609             fh => $args->{fh},
1610 1         9 args => $plot->{'twinx.args'}{$set},
1611             ax => "twinx_${ax}_$set_i"
1612             });
1613             }
1614             } else {
1615 7         12 say { $args->{fh} } "ax$args->{ax}.plot(x, y $label $options) # " . __LINE__;
  7         23  
1616             }
1617 8         19 $set_i++;
1618             }
1619 7         135 return 1;
1620             }
1621              
1622             sub scatter_helper {
1623 6     6 0 18 my ($args) = @_;
1624 6         72 my $current_sub = ( split( /::/, ( caller(0) )[3] ) )[-1]
1625             ; # https://stackoverflow.com/questions/2559792/how-can-i-get-the-name-of-the-current-subroutine-in-perl
1626 6 50       35 if ( ref $args ne 'HASH' ) {
1627 0         0 die "args must be given as a hash ref, e.g. \"$current_sub({ data => \@blah })\"";
1628             }
1629 6         24 my @reqd_args = (
1630             'ax', 'fh', # e.g. $py, $fh, which will be passed by the subroutine
1631             'plot', # args to original function
1632             );
1633 6         29 my @undef_args = grep { !defined $args->{$_} } @reqd_args;
  18         49  
1634 6 50       17 if ( scalar @undef_args > 0 ) {
1635 0         0 p @undef_args;
1636 0         0 die 'the above args are necessary, but were not defined.';
1637             }
1638 6         24 my @opt = (@ax_methods, @cb_arg, @plt_methods, @fig_methods, @arg, 'ax', @{ $opt{$current_sub} });
  6         284  
1639 6         15 my $plot = $args->{plot};
1640             @undef_args = grep {
1641 43         51 my $key = $_;
1642 43         112 not grep { $_ eq $key } @opt
  19995         20807  
1643 6         11 } keys %{$plot};
  6         22  
1644 6 50       25 if ( scalar @undef_args > 0 ) {
1645 0         0 p @undef_args;
1646 0         0 die "The above arguments aren't defined for $plot->{'plot.type'} in $current_sub";
1647             }
1648 6         19 my $overall_ref = ref $plot->{data};
1649 6 50       18 if ( $overall_ref ne 'HASH' ) {
1650 0         0 die
1651             "scatter only takes 1) hashes of arrays (single or 2) hash of hash of arrays; but $overall_ref was entered";
1652             }
1653 6         14 my ( %ref_counts, $plot_type );
1654 6         11 foreach my $set ( keys %{ $plot->{data} } ) {
  6         49  
1655 16         44 $ref_counts{ ref $plot->{data}{$set} }++;
1656             }
1657 6         13 my $ax = $args->{ax};
1658 6 50       15 if ( (scalar keys %ref_counts) > 1 ) {
1659 0         0 p $plot->{data};
1660 0         0 die "different kinds of data were entered to plot $ax; should be simple hash or hash of arrays.";
1661             }
1662 6 100       22 if ( defined $ref_counts{ARRAY} ) {
    50          
1663 4         7 $plot_type = 'single';
1664             } elsif ( defined $ref_counts{HASH} ) {
1665 2         4 $plot_type = 'multiple';
1666             } else {
1667 0         0 p $plot->{data};
1668 0         0 p %ref_counts;
1669 0         0 die 'Could not determine scatter type for the above data.';
1670             }
1671 6   50     32 $plot->{cmap} = $plot->{cmap} // 'gist_rainbow';
1672 6         9 foreach my $axis (@{ $plot->{logscale} }) { # x, y
  6         14  
1673 2         4 say {$args->{fh}} "ax$ax.set_$axis" . 'scale("log")';
  2         7  
1674             }
1675 6 100       21 if ( $plot_type eq 'single' ) { # only a single set of data
    50          
1676 4         5 my $options = '';
1677 4         7 my ( $color_key, @keys );
1678 4 50       10 if ( defined $plot->{'keys'} ) {
1679 0         0 @keys = @{ $plot->{'keys'} };
  0         0  
1680             } else {
1681 4         5 @keys = sort { lc $a cmp lc $b } keys %{ $plot->{data} };
  10         26  
  4         24  
1682             }
1683 4         4 my $n_keys = scalar keys %{ $plot->{data} };
  4         9  
1684 4 50 33     21 if ( ( $n_keys != 2 ) && ( $n_keys != 3 ) ) {
1685 0         0 p $plot->{data};
1686 0         0 die "scatterplots can only take 2 or 3 keys as data, but $current_sub received $n_keys";
1687             }
1688 4 50       19 if ( defined $plot->{color_key} ) {
    0          
1689 4         7 $color_key = $plot->{color_key};
1690 4         7 @keys = grep {$_ ne $plot->{color_key}} @keys;
  12         20  
1691             } elsif ( scalar @keys == 3 ) {
1692 0         0 $color_key = pop @keys;
1693             }
1694 4         8 foreach my $i (0,1) {
1695 8         11 my @undef_i = grep {not defined $plot->{data}{$keys[$i]}[$_]} 0..scalar @{ $plot->{data}{ $keys[$i] } } - 1;
  240         278  
  8         27  
1696 8 50       19 if (scalar @undef_i > 0) {
1697 0         0 p @undef_i;
1698 0         0 die "the above indices for $current_sub group $i are undefined.";
1699             }
1700             }
1701 4         5 say { $args->{fh} } 'x = [' . join( ',', @{ $plot->{data}{ $keys[0] } } ) . ']';
  4         7  
  4         109  
1702 4         5 say { $args->{fh} } 'y = [' . join( ',', @{ $plot->{data}{ $keys[1] } } ) . ']';
  4         7  
  4         159  
1703 4 50 33     12 if ( ( defined $plot->{'set.options'} )
1704             && ( ref $plot->{'set.options'} eq '' ) )
1705             {
1706 0         0 $options = ", $plot->{'set.options'}";
1707             }
1708 4         9 my $cb_opts = '';
1709 4         10 foreach my $o (grep {defined $plot->{$_}} ('cbdrawedges', 'cbpad')) { # numeric
  8         16  
1710 0 0       0 die "$o = $plot->{$o} must be numeric" unless (looks_like_number($plot->{$o}));
1711 0         0 my $mpl_opt = $o;
1712 0         0 $mpl_opt =~ s/^cb//;
1713 0         0 $cb_opts .= ", $mpl_opt = $plot->{$o}";
1714             }
1715 4 50       9 if ( defined $color_key ) {
1716 4 50       10 if (not defined $plot->{data}{$color_key}) {
1717 0         0 die "\"$color_key\" isn't defined for this scatter";
1718             }
1719 4         6 say { $args->{fh} } 'z = [' . join( ',', @{ $plot->{data}{$color_key} } ) . ']';
  4         7  
  4         33  
1720 4         6 say { $args->{fh} }
  4         10  
1721             "im = ax$ax.scatter(x, y, c = z, cmap = '$plot->{cmap}' $options)";
1722 4         5 say { $args->{fh} } "fig.colorbar(im, label = '$color_key' $cb_opts)";
  4         9  
1723             } else {
1724 0         0 say { $args->{fh} } "ax$ax.scatter(x, y $options)";
  0         0  
1725             }
1726 4   33     23 $plot->{xlabel} = $plot->{xlabel} // $keys[0];
1727 4   33     83 $plot->{ylabel} = $plot->{ylabel} // $keys[1];
1728             } elsif ( $plot_type eq 'multiple' ) { # multiple sets
1729 2         5 my @undefined_opts;
1730 2         3 foreach my $set ( sort keys %{ $plot->{'set.options'} } ) {
  2         18  
1731 2 50       3 next if grep { $set eq $_ } keys %{ $plot->{data} };
  4         8  
  2         5  
1732 0         0 push @undefined_opts, $set;
1733             }
1734 2 50       6 if ( scalar @undefined_opts > 0 ) {
1735 0         0 p $plot->{data};
1736 0         0 p $plot;
1737 0         0 say 'The data and options are above, but the following sets have options without data:';
1738 0         0 p @undefined_opts;
1739 0         0 die 'no data was defined for the above options';
1740             }
1741 2         3 my $color_key;
1742 2         4 foreach my $set ( sort keys %{ $plot->{data} } ) {
  2         7  
1743 4         9 my $options = '';
1744 4         6 my @keys;
1745 4 50       15 if ( defined $plot->{'keys'} ) {
1746 0         0 @keys = @{ $plot->{'keys'} };
  0         0  
1747             } else { # automatically take the key from the first; further sets should have the same labels
1748 4         8 @keys = sort { lc $a cmp lc $b } keys %{ $plot->{data}{$set} };
  4         17  
  4         24  
1749             }
1750 4         7 my $n_keys = scalar keys %{ $plot->{data}{$set} };
  4         8  
1751 4 50 33     12 if ( ( $n_keys != 2 ) && ( $n_keys != 3 ) ) {
1752 0         0 p $plot->{data}{$set};
1753 0         0 die "scatterplots can only take 2 or 3 keys as data, but $current_sub received $n_keys";
1754             }
1755 4         7 foreach my $key (@keys) {
1756 8         12 @undef_args = grep {!defined $plot->{data}{$set}{$key}[$_]} 0..scalar @{ $plot->{data}{$set}{$key} } - 1;
  240         317  
  8         22  
1757 8 50       20 if (scalar @undef_args > 0) {
1758 0         0 p @undef_args;
1759 0         0 die "the above indices for \"$key\" are undefined in $current_sub";
1760             }
1761             }
1762 4 50       13 if ( defined $plot->{color_key} ) {
    50          
1763 0         0 $color_key = $plot->{color_key};
1764 0         0 @keys = grep {$_ ne $plot->{color_key}} @keys;
  0         0  
1765             } elsif ( scalar @keys == 3 ) {
1766 0         0 $color_key = pop @keys;
1767             }
1768 4 50 33     18 if ( ( not defined $color_key ) && ( $n_keys == 3 ) ) {
1769 0         0 $color_key = pop @keys;
1770             }
1771 4 100       12 if ( defined $plot->{'set.options'}{$set} ) {
1772 2         4 $options = ", $plot->{'set.options'}{$set}";
1773             }
1774 4         7 say { $args->{fh} } 'x = [' . join( ',', @{ $plot->{data}{$set}{ $keys[0] } } ) . ']';
  4         8  
  4         181  
1775 4         9 say { $args->{fh} } 'y = [' . join( ',', @{ $plot->{data}{$set}{ $keys[1] } } ) . ']';
  4         8  
  4         208  
1776 4 50       12 if ( defined $color_key ) {
1777 0 0       0 if (not defined $plot->{data}{$set}{$color_key}) {
1778 0         0 die "\"$color_key\" isn't defined for set \"$set\"";
1779             }
1780 0         0 say { $args->{fh} } 'z = [' . join( ',', @{ $plot->{data}{$set}{$color_key} } ) . ']';
  0         0  
  0         0  
1781 0 0       0 unless ( $options =~ m/label\s*=/ ) {
1782 0         0 $options .= ", label = '$set'";
1783             }
1784 0         0 say { $args->{fh} }
  0         0  
1785             "im = ax$ax.scatter(x, y, c = z, cmap = '$plot->{cmap}' $options)";
1786             } else {
1787 4         5 say { $args->{fh} } "ax$ax.scatter(x, y, label = '$set' $options)";
  4         15  
1788             }
1789 4   66     20 $plot->{xlabel} = $plot->{xlabel} // $keys[0];
1790 4   66     18 $plot->{ylabel} = $plot->{ylabel} // $keys[1];
1791             }
1792 2 50       54 say { $args->{fh} } "plt.colorbar(im, label = '$color_key')" if defined $color_key;
  0         0  
1793             }
1794             }
1795              
1796             sub violin_helper {
1797 4     4 0 8 my ($args) = @_;
1798 4         72 my $current_sub = ( split( /::/, ( caller(0) )[3] ) )[-1]
1799             ; # https://stackoverflow.com/questions/2559792/how-can-i-get-the-name-of-the-current-subroutine-in-perl
1800 4 50       22 if ( ref $args ne 'HASH' ) {
1801 0         0 die "args must be given as a hash ref, e.g. \"$current_sub({ data => \@blah })\"";
1802             }
1803 4         15 my @reqd_args = (
1804             'fh', # e.g. $py, $fh, which will be passed by the subroutine
1805             'plot', # args to original function
1806             );
1807 4         7 my @undef_args = grep { !defined $args->{$_} } @reqd_args;
  8         17  
1808 4 50       14 if ( scalar @undef_args > 0 ) {
1809 0         0 p @undef_args;
1810 0         0 die 'the above args are necessary, but were not defined.';
1811             }
1812 4         12 my @opt = (@ax_methods, @plt_methods, @fig_methods, @arg, 'ax', @{ $opt{$current_sub} });
  4         163  
1813 4         10 my $plot = $args->{plot};
1814             my @undef_opt = grep {
1815 24         27 my $key = $_;
1816 24         66 not grep { $_ eq $key } @opt
  10944         11454  
1817 4         6 } keys %{$plot};
  4         11  
1818 4 50       14 if ( scalar @undef_opt > 0 ) {
1819 0         0 p @undef_opt;
1820 0         0 die "The above arguments aren't defined for $plot->{'plot.type'} using $current_sub";
1821             }
1822 4   50     28 $plot->{orientation} = $plot->{orientation} // 'vertical';
1823 4 50       28 if ( $plot->{orientation} !~ m/^(?:horizontal|vertical)$/ ) {
1824 0         0 die "$current_sub needs either \"horizontal\" or \"vertical\", not \"$plot->{orientation}\"";
1825             }
1826 4 100       23 if (ref $plot->{data} eq 'ARRAY') {
1827 2         5 my $tmp = delete $plot->{data};
1828 2         7 $plot->{data}{''} = $tmp;
1829             }
1830 4         11 my ( @xticks, @key_order );
1831 4 50       10 if ( defined $plot->{'key.order'} ) {
1832 0         0 @key_order = @{ $plot->{'key.order'} };
  0         0  
1833             } else {
1834 4         6 @key_order = sort keys %{ $plot->{data} };
  4         19  
1835             }
1836 4   50     11 my $ax = $args->{ax} // '';
1837 4   50     13 $plot->{medians} = $plot->{medians} // 1; # by default, show median values
1838 4   50     18 $plot->{whiskers} = $plot->{whiskers} // 1; # by default, make whiskers
1839 4   50     13 $plot->{edgecolor} = $plot->{edgecolor} // 'black';
1840 4         7 my $options = ''; # these args go to the plt.hist call
1841 4         5 say { $args->{fh} } 'd = []';
  4         13  
1842 4         6 my $min_n_points = 'inf';
1843 4         9 foreach my $key (@key_order) {
1844 5 50       7 @{ $plot->{data}{$key} } = grep { defined && looks_like_number($_) } @{ $plot->{data}{$key} };
  5         12  
  63         120  
  5         11  
1845 5         10 say { $args->{fh} } 'd.append(['
1846 5         7 . join( ',', @{ $plot->{data}{$key} } ) . '])';
  5         28  
1847 5         6 $min_n_points = min( scalar @{ $plot->{data}{$key} }, $min_n_points );
  5         22  
1848             }
1849 4         6 foreach my $axis (@{ $plot->{logscale} }) { # x, y
  4         16  
1850 0 0       0 if ($axis =~ m/^([^xy])$/) {
1851 0         0 p $plot->{logscale};
1852 0         0 die "only \"x\" and \"y\" are allowed in boxplot, not \"$axis\"";
1853             }
1854 0         0 say {$args->{fh}} "ax$ax.set_$axis" . 'scale("log")';
  0         0  
1855             }
1856 4         5 say { $args->{fh} } "vp = ax$ax.violinplot(d, showmeans=False, points = $min_n_points, orientation = '$plot->{orientation}', showmedians = $plot->{medians})";
  4         15  
1857 4 50       12 if ( defined $plot->{colors} ) { # every hash key should have its own color defined
1858             # the below code helps to provide better error messages in case I make an error in calling the sub
1859 0         0 my @wrong_keys = grep { not defined $plot->{colors}{$_} } keys %{ $plot->{data} };
  0         0  
  0         0  
1860 0 0       0 if ( scalar @wrong_keys > 0 ) {
1861 0         0 p $plot;
1862 0         0 p @wrong_keys;
1863 0         0 die 'the above data keys have no defined color';
1864             }
1865             # list of pre-defined colors: https://matplotlib.org/stable/gallery/color/named_colors.html
1866 0         0 say { $args->{fh} } 'colors = ["'
1867 0         0 . join( '","', @{ $plot->{colors} }{@key_order} ) . '"]';
  0         0  
1868              
1869             # the above color list will have the same order, via the above hash slice
1870 0         0 say { $args->{fh} } 'for i, pc in enumerate(vp["bodies"], 1):';
  0         0  
1871 0         0 say { $args->{fh} } "\tpc.set_facecolor(colors[i-1])";
  0         0  
1872 0         0 say { $args->{fh} } "\tpc.set_edgecolor('black')";
  0         0  
1873             } else {
1874 4         4 say { $args->{fh} } 'for pc in vp["bodies"]:';
  4         7  
1875 4 50       10 if ( defined $plot->{color} ) {
1876 0         0 say { $args->{fh} } "\tpc.set_facecolor('$plot->{color}')";
  0         0  
1877             }
1878 4         4 say { $args->{fh} } "\tpc.set_edgecolor('black')";
  4         7  
1879              
1880             # say {$args->{fh}} "\tpc.set_alpha(1)";
1881             }
1882 4 50       15 if ( $plot->{whiskers} ) {
1883             # https://matplotlib.org/stable/gallery/statistics/customized_violin.html
1884 4         5 say {$args->{fh} } 'import numpy as np';
  4         8  
1885 4         7 say {$args->{fh} } 'def adjacent_values(vals, q1, q3):';
  4         7  
1886 4         5 say {$args->{fh} } ' upper_adjacent_value = q3 + (q3 - q1) * 1.5';
  4         7  
1887 4         4 say {$args->{fh} }
  4         16  
1888             ' upper_adjacent_value = np.clip(upper_adjacent_value, q3, vals[-1])';
1889 4         4 say {$args->{fh} } ' lower_adjacent_value = q1 - (q3 - q1) * 1.5';
  4         6  
1890 4         4 say {$args->{fh} }
  4         8  
1891             ' lower_adjacent_value = np.clip(lower_adjacent_value, vals[0], q1)';
1892 4         4 say {$args->{fh} }
  4         7  
1893             ' return lower_adjacent_value, upper_adjacent_value';
1894 4         5 say {$args->{fh} } 'np_data = np.array(d, dtype = object)';
  4         6  
1895 4         6 say {$args->{fh} } 'quartile1 = []';
  4         6  
1896 4         4 say {$args->{fh} } 'medians = []';
  4         6  
1897 4         4 say {$args->{fh} } 'quartile3 = []';
  4         6  
1898 4         4 say {$args->{fh} } 'for subset in list(range(0, len(np_data))):';
  4         7  
1899 4         4 say {$args->{fh}} ' local_quartile1, local_medians, local_quartile3 = np.percentile(d[subset], [25, 50, 75])' . "\n";
  4         9  
1900 4         5 say {$args->{fh}} ' quartile1.append(local_quartile1)';
  4         6  
1901 4         4 say {$args->{fh}} ' medians.append(local_medians)';
  4         5  
1902 4         5 say {$args->{fh}} ' quartile3.append(local_quartile3)';
  4         18  
1903 4         6 say {$args->{fh}} 'whiskers = np.array([';
  4         4  
1904 4         5 say {$args->{fh}} ' adjacent_values(sorted_array, q1, q3)';
  4         48  
1905 4         7 say {$args->{fh}} ' for sorted_array, q1, q3 in zip(d, quartile1, quartile3)])';
  4         152  
1906 4         9 say {$args->{fh}} 'whiskers_min, whiskers_max = whiskers[:, 0], whiskers[:, 1]';
  4         9  
1907 4         6 say {$args->{fh}} 'inds = np.arange(1, len(medians) + 1)';
  4         7  
1908 4 50       10 if ( $plot->{orientation} eq 'vertical' ) {
1909 4         12 say { $args->{fh} } "ax$ax"
  4         12  
1910             . '.vlines(inds, quartile1, quartile3, color="k", linestyle="-", lw=5)';
1911 4         6 say { $args->{fh} } "ax$ax"
  4         11  
1912             . '.vlines(inds, whiskers_min, whiskers_max, color="k", linestyle="-", lw=1)';
1913             } else {
1914 0         0 say { $args->{fh} } "ax$ax"
  0         0  
1915             . '.hlines(inds, quartile1, quartile3, color="k", linestyle="-", lw=5)';
1916 0         0 say { $args->{fh} } "ax$ax"
  0         0  
1917             . '.hlines(inds, whiskers_min, whiskers_max, color="k", linestyle="-", lw=1)';
1918             }
1919             }
1920 4         7 foreach my $key (@key_order) {
1921             push @xticks, "$key ("
1922 5         11 . format_commas( scalar @{ $plot->{data}{$key} }, '%.0u' ) . ')';
  5         19  
1923 5 50       12 if ( $plot->{orientation} eq 'vertical' ) {
1924 5         17 say { $args->{fh} } "ax$ax.plot("
1925             . scalar @xticks . ', '
1926 5         7 . ( sum( @{ $plot->{data}{$key} } ) / scalar @{ $plot->{data}{$key} } )
  5         31  
  5         66  
1927             . ', "ro")'; # plot mean point, which is red
1928             } else { # orientation = horizontal
1929 0         0 say { $args->{fh} } "ax$ax.plot("
1930 0         0 . ( sum( @{ $plot->{data}{$key} } ) / scalar @{ $plot->{data}{$key} } )
  0         0  
  0         0  
1931             . ', ' . scalar @xticks . ', "ro")'; # plot mean point, which is red
1932             }
1933             }
1934 4 50       11 if ( $plot->{orientation} eq 'vertical' ) {
1935 4         4 say { $args->{fh} } "ax$ax.set_xticks(["
  4         82  
1936             . join( ',', 1 .. scalar @key_order ) . '], ["'
1937             . join( '","', @xticks ) . '"])';
1938             } else {
1939 0         0 say { $args->{fh} } "ax$ax.set_yticks(["
  0         0  
1940             . join( ',', 1 .. scalar @key_order ) . '], ["'
1941             . join( '","', @xticks ) . '"])';
1942             }
1943             }
1944              
1945             sub wide_helper {
1946 3     3 0 8 my ($args) = @_;
1947 3         46 my $current_sub = ( split( /::/, ( caller(0) )[3] ) )[-1]
1948             ; # https://stackoverflow.com/questions/2559792/how-can-i-get-the-name-of-the-current-subroutine-in-perl
1949 3 50       24 if ( ref $args ne 'HASH' ) {
1950 0         0 die "args must be given as a hash ref, e.g. \"$current_sub({ data => \@blah })\"";
1951             }
1952 3         19 my @reqd_args = (
1953             'fh', # e.g. $py, $fh, which will be passed by the subroutine
1954             'plot', # args to original function
1955             );
1956 3         9 my @undef_args = grep { !defined $args->{$_} } @reqd_args;
  6         18  
1957 3 50       11 if ( scalar @undef_args > 0 ) {
1958 0         0 p @undef_args;
1959 0         0 die "the above args are necessary for $current_sub, but were not defined.";
1960             }
1961 3         15 my @opt = (@ax_methods, @plt_methods, @fig_methods, @arg, 'ax', @{ $opt{$current_sub} });
  3         144  
1962 3         7 my $plot = $args->{plot};
1963 3   50     21 $plot->{'show.legend'} = $plot->{'show.legend'} // 1;
1964             my @undef_opt = grep {
1965 24         30 my $key = $_;
1966 24         70 not grep { $_ eq $key } @opt
  10824         11389  
1967 3         7 } keys %{$plot};
  3         13  
1968 3 50       14 if ( scalar @undef_opt > 0 ) {
1969 0         0 p @undef_opt;
1970 0         0 die
1971             "The above arguments aren't defined for $plot->{'plot.type'} using $current_sub";
1972             }
1973 3         5 say { $args->{fh} } 'import numpy as np';
  3         13  
1974 3   50     9 my $ax = $args->{ax} // '';
1975 3         9 my $ref_type = ref $plot->{data};
1976 3 100       13 if ( $ref_type eq 'HASH' ) { # multiple groups, no label
    50          
1977 1         3 foreach my $group ( keys %{ $plot->{data} } ) {
  1         4  
1978 2   50     8 my $color = $plot->{color}{$group} // 'b';
1979 2         3 say { $args->{fh} } 'ys = []';
  2         94  
1980 2         4 my ( $min_x, $max_x ) = ( 'inf', '-inf' );
1981 2         5 foreach my $run ( 0 .. scalar @{ $plot->{data}{$group} } - 1 ) {
  2         6  
1982 6         7 $min_x = min( $min_x, @{ $plot->{data}{$group}[$run][0] } );
  6         21  
1983 6         8 $max_x = max( $max_x, @{ $plot->{data}{$group}[$run][0] } );
  6         17  
1984             }
1985 2         2 say { $args->{fh} } "base_y = np.linspace($max_x, $min_x, 101)";
  2         13  
1986 2         2 foreach my $run ( 0 .. scalar @{ $plot->{data}{$group} } - 1 ) {
  2         5  
1987 6         7 say { $args->{fh} } 'x = ['
1988 6         7 . join( ',', @{ $plot->{data}{$group}[$run][0] } ) . ']';
  6         134  
1989 6         6 say { $args->{fh} } 'y = ['
1990 6         7 . join( ',', @{ $plot->{data}{$group}[$run][1] } ) . ']';
  6         236  
1991 6         10 say { $args->{fh} } "ax$ax.plot(x, y, '$color', alpha=0.15)";
  6         10  
1992 6         7 say { $args->{fh} } 'y = np.interp(base_y, x, y)';
  6         8  
1993 6         7 say { $args->{fh} } 'ys.append(y)';
  6         9  
1994             }
1995 2         3 say { $args->{fh} } 'ys = np.array(ys)';
  2         3  
1996 2         4 say { $args->{fh} } 'mean_ys = ys.mean(axis=0)';
  2         3  
1997 2         3 say { $args->{fh} } 'std = ys.std(axis=0)';
  2         3  
1998 2         3 say { $args->{fh} } 'ys_upper = mean_ys + std';
  2         2  
1999 2         2 say { $args->{fh} } 'ys_lower = mean_ys - std';
  2         3  
2000 2 50       6 if ( $plot->{'show.legend'} > 0 ) {
2001 2         2 say { $args->{fh} } "ax$ax.plot(base_y, mean_ys, '$color', label = '$group')";
  2         4  
2002             } else {
2003 0         0 say { $args->{fh} } "ax$ax.plot(base_y, mean_ys, '$color')";
  0         0  
2004             }
2005 2         3 say { $args->{fh} }
  2         21  
2006             "ax$ax.fill_between(base_y, ys_lower, ys_upper, color='$color', alpha=0.3)";
2007             }
2008             } elsif ( $ref_type eq 'ARRAY' ) {
2009 2   50     6 my $color = $plot->{color} // 'b';
2010 2         3 say { $args->{fh} } 'ys = []';
  2         5  
2011 2         5 my ( $min_x, $max_x ) = ( 'inf', '-inf' );
2012 2         4 foreach my $run ( 0 .. scalar @{ $plot->{data} } - 1 ) {
  2         13  
2013 6         9 $min_x = min( $min_x, @{ $plot->{data}[$run][0] } );
  6         22  
2014 6         6 $max_x = max( $max_x, @{ $plot->{data}[$run][0] } );
  6         15  
2015             }
2016 2         4 say { $args->{fh} } "base_y = np.linspace($max_x, $min_x, 101)";
  2         14  
2017 2         4 foreach my $run ( 0 .. scalar @{ $plot->{data} } - 1 ) {
  2         5  
2018 6         11 say { $args->{fh} } 'x = ['
2019 6         9 . join( ',', @{ $plot->{data}[$run][0] } ) . ']';
  6         156  
2020 6         8 say { $args->{fh} } 'y = ['
2021 6         10 . join( ',', @{ $plot->{data}[$run][1] } ) . ']';
  6         359  
2022 6         14 say { $args->{fh} } "ax$ax.plot(x, y, '$color', alpha=0.15)";
  6         16  
2023 6         8 say { $args->{fh} } 'y = np.interp(base_y, x, y)';
  6         9  
2024 6         9 say { $args->{fh} } 'ys.append(y)';
  6         12  
2025             }
2026 2         3 say { $args->{fh} } 'ys = np.array(ys)';
  2         5  
2027 2         2 say { $args->{fh} } 'mean_ys = ys.mean(axis=0)';
  2         3  
2028 2         3 say { $args->{fh} } 'std = ys.std(axis=0)';
  2         4  
2029 2         3 say { $args->{fh} } 'ys_upper = mean_ys + std';
  2         4  
2030 2         2 say { $args->{fh} } 'ys_lower = mean_ys - std';
  2         5  
2031 2         3 say { $args->{fh} } "ax$ax.plot(base_y, mean_ys, '$color')";
  2         5  
2032 2         2 say { $args->{fh} }
  2         38  
2033             "ax$ax.fill_between(base_y, ys_lower, ys_upper, color='$color', alpha=0.3)";
2034             } else {
2035 0         0 die "$current_sub cannot take ref type \"$ref_type\" for \"data\"";
2036             }
2037             }
2038              
2039             sub print_type {
2040 7     7 0 13 my $str = shift;
2041 7         12 my $type = 'no quotes';
2042 7 100       21 if (looks_like_number($str)) { # numbers (e.g. 0.8) must not be quoted
2043 6         15 return 'no quotes';
2044             }
2045 1 50       4 if ($str =~ m/\w+\h*=\h*["'\(]/) {
2046 0         0 return 'no quotes';
2047             }
2048 1 50 0     14 if ($str =~ m/^\w+$/) {
    0          
    0          
    0          
2049 1         3 return 'single quotes';
2050             } elsif ($str =~ m/^\[\h*\-?\d.+\d\h*\]$/) {
2051 0         0 return 'no quotes';
2052             } elsif ($str =~ m/[!@#\$\%^&*\(\)\{\}\[\]\<\>,\/\-\h:;\+=\w]+$/) {
2053 0         0 return 'single quotes';
2054             } elsif (($str =~ m/,/) && ($str !~ m/[\]\[]/)) {
2055 0         0 return 'single quotes';
2056             }
2057 0         0 return $type;
2058             }
2059             my @all_opt;
2060             foreach my $type (keys %opt) {
2061             push @all_opt, @{ $opt{$type} };
2062             }
2063             sub normalise_p {
2064             # "p" is a flat list of subplots: ONE array element == ONE subplot.
2065             # p => [ \%h, \%h, ... ] each hash is its own subplot
2066             # p => [ \%h, [ \%h, \%h ], \%h, ... ] an inner array of hashes is a
2067             # single subplot holding several
2068             # plots on the same axes (the 1st
2069             # hash is the base plot, the rest
2070             # are "add"itions/overlays)
2071             # The plain-hash and inner-array forms may be mixed freely in one "p".
2072             # Using "p" means top-level "plot.type"/"data" are not used. If no grid is
2073             # given, the subplots are laid out on an automatically-sized near-square
2074             # grid; pass nrow(s)/ncol(s) to override.
2075 17     17 0 35 my ( $args, $current_sub ) = @_;
2076 17   50     43 $current_sub = $current_sub // 'plt';
2077 17         27 my $p = delete $args->{p};
2078 17 100       50 if ( ref $p ne 'ARRAY' ) {
2079 1         31 die "$current_sub: \"p\" must be an ARRAY reference (one element per subplot)";
2080             }
2081 16 100       22 if ( scalar @{$p} == 0 ) {
  16         43  
2082 1         10 die "$current_sub: \"p\" is empty";
2083             }
2084             # "p" supersedes the older interface; forbid mixing to avoid ambiguity
2085 15         45 foreach my $clash ( grep { defined $args->{$_} } ( 'plot.type', 'data', 'plots', 'add' ) ) {
  60         122  
2086 4         40 die "$current_sub: \"$clash\" cannot be combined with \"p\"";
2087             }
2088 11         16 my @plots;
2089 11         19 my $i = 0;
2090 11         16 foreach my $subplot ( @{$p} ) {
  11         97  
2091 21         34 my $ref = ref $subplot;
2092 21 100       62 if ( $ref eq 'HASH' ) { # one subplot, one plot
    100          
2093 16         23 push @plots, { %{$subplot} }; # shallow copy; don't mutate caller
  16         53  
2094             } elsif ( $ref eq 'ARRAY' ) { # one subplot, several overlaid plots
2095 4 100       9 if ( scalar @{$subplot} == 0 ) {
  4         12  
2096 1         29 die "$current_sub: subplot $i in \"p\" is an empty array";
2097             }
2098 3         5 my @g = @{$subplot};
  3         7  
2099 3         6 my $main = shift @g; # 1st hash is the base plot
2100 3         8 foreach my $plot ( $main, @g ) {
2101 5 100       13 if ( ref $plot ne 'HASH' ) {
2102 1         12 die "$current_sub: subplot $i in \"p\": every plot must be a HASH reference";
2103             }
2104             }
2105 2         5 my %plot = %{$main}; # shallow copy of the base plot
  2         8  
2106 2 50       8 push @{ $plot{add} }, @g if scalar @g; # the rest overlay on the same axes
  2         6  
2107 2         5 push @plots, \%plot;
2108             } else {
2109 1         29 die "$current_sub: subplot $i in \"p\" must be a HASH reference (one plot) or an ARRAY of HASH references (overlaid plots), not a \"$ref\" reference";
2110             }
2111 18         25 $i++;
2112             }
2113 8         21 $args->{plots} = \@plots;
2114             # convenience: choose a subplot grid. With neither dimension given, use a
2115             # near-square grid; with exactly one given (and positive), derive the other
2116             # so e.g. "ncols => 1" stacks the subplots in a single column. Empty
2117             # trailing cells are pruned later by the engine.
2118 8         13 my $n = scalar @plots;
2119 8   66     62 my $ncols = $args->{ncols} // $args->{ncol};
2120 8   66     34 my $nrows = $args->{nrows} // $args->{nrow};
2121 8 100 100     69 if ( ( not defined $ncols ) && ( not defined $nrows ) ) {
    100 66        
    100 66        
2122 4         8 $ncols = 1;
2123 4         12 $ncols++ while ( $ncols * $ncols ) < $n; # ceil(sqrt $n), no floats
2124 4         12 $args->{ncols} = $ncols;
2125 4         16 $args->{nrows} = int( ( $n + $ncols - 1 ) / $ncols ); # ceil($n / $ncols)
2126             } elsif ( ( not defined $ncols ) && ( $nrows > 0 ) ) { # only rows given
2127 1         23 $args->{ncols} = int( ( $n + $nrows - 1 ) / $nrows );
2128             } elsif ( ( not defined $nrows ) && ( $ncols > 0 ) ) { # only cols given
2129 2         11 $args->{nrows} = int( ( $n + $ncols - 1 ) / $ncols );
2130             }
2131 8         18 return $args;
2132             }
2133             sub plt {
2134 78     78 0 3318095 my $current_sub = ( split( /::/, ( caller(0) )[3] ) )[-1]
2135             ; # https://stackoverflow.com/questions/2559792/how-can-i-get-the-name-of-the-current-subroutine-in-perl
2136             # Accept BOTH calling conventions:
2137             # new style: plt( key => value, ... )
2138             # back-compatible: plt({ key => value, ... })
2139 78         673 my $args;
2140 78 100 66     498 if ( ( scalar @_ == 1 ) && ( ref $_[0] eq 'HASH' ) ) {
    50          
2141 29         45 $args = $_[0]; # plt({ ... })
2142             } elsif ( ( scalar @_ % 2 ) == 0 ) {
2143 49         297 $args = { @_ }; # plt( ... )
2144             } else {
2145 0         0 die "$current_sub: odd number of arguments; call as $current_sub( key => value, ... ) or $current_sub({ key => value, ... })";
2146             }
2147             # fold the "p" interface into the engine's internal plot model
2148 78 100       244 normalise_p( $args, $current_sub ) if defined $args->{p};
2149 69 50       149 if ((scalar grep {$args->{$_}} ('output.file', 'show')) == 0) {
  138         341  
2150 0         0 p $args;
2151 0         0 die 'either "show" or "output.file" must be defined';
2152             }
2153 69 100       185 my @reqd_args = $args->{show} ? () : ('output.file'); # e.g. "my_image.svg"; optional if "show" is set
2154 69         110 my $single_example = 'plt({
2155             \'output.file\' => \'/tmp/gospel.word.counts.svg\',
2156             \'plot.type\' => \'bar\',
2157             data => {
2158             \'Matthew\' => 18345,
2159             \'Mark\' => 11304,
2160             \'Luke\' => 19482,
2161             \'John\' => 15635,
2162             }
2163             });';
2164 69         135 my $multi_example = 'plt({
2165             \'output.file\' => \'svg/pie.svg\',
2166             plots => [
2167             {
2168             data => {
2169             Russian => 106_000_000, # Primarily European Russia
2170             German => 95_000_000, # Germany, Austria, Switzerland, etc.
2171             },
2172             \'plot.type\' => \'pie\',
2173             title => \'Top Languages in Europe\',
2174             suptitle => \'Pie in subplots\',
2175             },
2176             {
2177             data => {
2178             Russian => 106_000_000, # Primarily European Russia
2179             German => 95_000_000, # Germany, Austria, Switzerland, etc.
2180             },
2181             \'plot.type\' => \'pie\',
2182             title => \'Top Languages in Europe\',
2183             },
2184             ncols => 3,
2185             });';
2186 69         114 my @undef_args = grep { !defined $args->{$_} } @reqd_args;
  63         176  
2187 69 50       157 if ( scalar @undef_args > 0 ) {
2188 0         0 p @undef_args;
2189 0         0 die 'the above args are necessary, but were not defined.';
2190             }
2191 69 50 66     186 if ( ( not defined $args->{'plot.type'} )
2192             && ( not defined $args->{plots} ) )
2193             {
2194 0         0 p $args;
2195 0         0 die 'either "plot.type" or "plots" must be defined, but neither were';
2196             }
2197 69 50       154 if (ref $args->{'output.file'} ne '') {
2198 0         0 p $args;
2199 0         0 die '"output.file" must be a SCALAR or string, but was given a ' . ref $args->{'output.file'};
2200             }
2201 69         4307 my @defined_args = (@reqd_args, @ax_methods, @fig_methods, @plt_methods, @arg, @all_opt);
2202             my @bad_args = grep {
2203 337         388 my $key = $_;
2204 337         1087 not grep { $_ eq $key } @defined_args
  196778         207261  
2205 69         124 } keys %{$args};
  69         217  
2206 69 50       206 if ( scalar @bad_args > 0 ) {
2207 0         0 p @defined_args, array_max => scalar @defined_args;
2208 0         0 p @bad_args, array_max => scalar @bad_args;
2209 0         0 say STDERR 'the 2nd group of arguments are not recognized, while the 1st is the defined list';
2210 0         0 die "The above args are accepted by \"$current_sub\"";
2211             }
2212 69 50       211 $args->{nrows} = $args->{nrow} if defined $args->{nrow}; # allow synonyms
2213 69 50       121 $args->{ncols} = $args->{ncol} if defined $args->{ncol}; # allow synonyms
2214 69   100     312 $args->{nrows} = $args->{nrows} // 1;
2215 69   100     192 $args->{ncols} = $args->{ncols} // 1;
2216 69         82 my $single_plot = 0; # false
2217 69 100 100     234 if ( ( defined $args->{'plot.type'} ) && ( defined $args->{data} ) ) {
2218 58         81 $single_plot = 1; # true
2219             }
2220 69 50 66     213 if (($single_plot == 1) && (not defined $args->{'plot.type'})) {
2221 0         0 p $args;
2222 0         0 say $single_example;
2223 0         0 die "\"plot.type\" was not defined for a single plot in $current_sub";
2224             }
2225 69 100 100     171 if ( ( $single_plot == 0 ) && (not defined $args->{plots} )) {
2226 1         20 say $multi_example;
2227 1         23 die "$current_sub: single plots need \"data\" and \"plot.type\", see example above";
2228             }
2229 68 50 66     173 if ( ( $single_plot == 0 ) && ( ref $args->{plots} ne 'ARRAY' ) ) {
2230 0         0 p $args;
2231 0         0 die "$current_sub \"plots\" must have an array entered into it";
2232             }
2233 68 50 66     138 if ( ( $single_plot == 0 ) && ( scalar @{ $args->{plots} } == 0 ) ) {
  10         31  
2234 0         0 p $args;
2235 0         0 die "$current_sub \"plots\" has 0 plots entered.";
2236             }
2237 68 100       109 if ($single_plot == 1) {
2238 58 50       103 foreach my $arg (grep {defined $args->{$_} && $args->{$_} > 1} ('ncols', 'nrows')) {
  116         361  
2239 0         0 warn "\"$arg\" is set to >1, but there is only 1 plot: resetting $arg to 1.";
2240 0         0 $args->{$arg} = 1;
2241             }
2242             }
2243 68 50 66     172 if ( ( $single_plot == 0 )
2244 10         27 && ( ( $args->{nrows} * $args->{ncols} ) < scalar @{ $args->{plots} } )
2245             )
2246             {
2247 0         0 p $args;
2248 0         0 my $n_plots = scalar @{ $args->{plots} };
  0         0  
2249 0         0 say
2250             "ncols = $args->{ncols}; nrows = $args->{nrows}, but there are $n_plots plots.\n";
2251 0         0 die 'There are not enough subplots for the data';
2252             }
2253 68 100       151 if ($single_plot == 0) { # multiple plots
2254 10         31 my $max_i = scalar @{ $args->{plots} } - 1;
  10         34  
2255 10         27 my @hash_ref_i = grep { ref $args->{plots}[$_]{data} eq 'HASH' } 0..$max_i;
  22         50  
2256 10         21 @bad_args = grep { scalar keys %{ $args->{plots}[$_]{data} } == 0} @hash_ref_i;
  22         20  
  22         53  
2257 10 50       20 if (scalar @bad_args > 0) {
2258 0         0 foreach my $i (@bad_args) {
2259 0         0 say STDERR "plot index $i:";
2260 0         0 p $args->{plots}[$i];
2261             }
2262 0         0 die "the above hash ref indices have empty data hashes for $current_sub";
2263             }
2264 10         16 my @output_file = grep {defined $args->{plots}[$_]{'output.file'}} 0..$max_i;
  22         52  
2265 10 50       24 if (scalar @output_file > 0) {
2266 0         0 p $args;
2267 0         0 p @output_file;
2268 0         0 die '"output.file" was defined at subplots indices above, which does not make sense';
2269             }
2270             }
2271 68 50 100     244 if (($single_plot == 1) && (ref $args->{data} eq 'HASH') && (scalar keys %{ $args->{data}} == 0 )) {
  42   66     131  
2272 0         0 p $args;
2273 0         0 die '"data" is an empty hash';
2274             }
2275 68 100       97 @bad_args = grep {defined $args->{$_} && (not looks_like_number($args->{$_}))} ('cbpad', 'ncols', 'nrows', 'scale', 'scalex', 'scaley');
  408         898  
2276 68 50       125 if (scalar @bad_args > 0) {
2277 0         0 p $args;
2278 0         0 p @bad_args;
2279 0         0 die 'the above args must be numeric';
2280             }
2281 68         192 my @ax = map { "ax$_" } 0 .. $args->{nrows} * $args->{ncols} - 1;
  87         283  
2282 68         125 my ( @py, @y, $fh);
2283 68         79 my $i = 0;
2284 68         109 foreach my $ax (@ax) {
2285 87         184 my $a1i = int $i / $args->{ncols}; # 1st index
2286 87         117 my $a2i = $i % $args->{ncols}; # 2nd index
2287 87         201 $y[$a1i][$a2i] = $ax;
2288 87         145 $i++;
2289             }
2290 68         83 foreach my $y (@y) {
2291 74         84 push @py, '(' . join( ',', @{$y} ) . ')';
  74         230  
2292             }
2293 68 50 66     148 if ((defined $args->{'shared.colorbar'}) && ($single_plot == 1)) {
2294 0         0 warn 'There is only 1 plot/subplot, shared colorbars make no sense... deleting';
2295 0         0 delete $args->{'shared.colorbar'};
2296             }
2297 68 100       138 if (defined $args->{'shared.colorbar'}) {
2298 1         5 my $ref = ref $args->{'shared.colorbar'};
2299 1 50       6 if ($ref ne 'ARRAY') {
2300 0         0 p $args;
2301 0         0 die '"shared.colobar" must be an array reference';
2302             }
2303 1         2 my $max_subplot_idx = max(@{ $args->{'shared.colorbar'} });
  1         5  
2304 1 50       6 if ($max_subplot_idx > ($args->{nrows} * $args->{ncols} - 1)) {
2305 0         0 p $args;
2306 0         0 die "the max \"shared.colorbar\" index $max_subplot_idx > than the max index of plots";
2307             }
2308             }
2309 68 100       132 if (defined $args->{add}) {
2310 1         3 my $ref = ref $args->{add};
2311 1 50       3 if ($ref ne 'ARRAY') {
2312 0         0 die "\"add\" must be an array (of anonymous hashes), but you entered a $ref reference";
2313             }
2314             }
2315 68 50       127 if ( defined $args->{fh} ) {
2316 0         0 my $ref = ref $args->{fh};
2317 0 0       0 if ($ref ne 'File::Temp') {
2318 0         0 p $args;
2319 0         0 die "$current_sub received a \"$ref\" for \"fh\", which isn't a \"File::Temp\" object.";
2320             }
2321 0         0 $fh = $args->{fh};# open $fh, '>>', $args->{fh};
2322             } else {
2323 68         401 $fh = File::Temp->new(DIR => '/tmp', SUFFIX => '.py', UNLINK => 0);
2324             }
2325             # Non-ASCII key names (e.g. Greek letters like ρ, τ) arrive as wide
2326             # characters when the caller has "use utf8", so give the output filehandle
2327             # a UTF-8 encoding layer. Without it, "say $fh" dies with
2328             # "Wide character in say" under this module's "warnings FATAL => 'all'".
2329             # Only add the layer if it isn't already present, to avoid double-encoding
2330             # a filehandle that was passed in.
2331 68 50       31133 unless ( grep { /utf-?8/i } PerlIO::get_layers($fh) ) {
  136         439  
2332 68     2   884 binmode $fh, ':encoding(UTF-8)';
  2         60  
  2         4  
  2         21  
2333             }
2334 68         6424 say 'temp file is ' . $fh->filename;
2335 68         2246 say $fh 'import matplotlib.pyplot as plt';
2336 68         202 say $fh 'import json, base64';
2337 68 100       249 if ( $single_plot == 0 ) {
    50          
2338 10   100     74 $args->{sharex} = $args->{sharex} // 0;
2339 10   100     47 $args->{sharey} = $args->{sharey} // 0;
2340 10         78 say $fh 'fig, ('
2341             . join( ',', @py )
2342             . ") = plt.subplots($args->{nrows}, $args->{ncols}, sharex = $args->{sharex}, sharey = $args->{sharey}, layout = 'constrained') #" . __LINE__;
2343             } elsif ( $single_plot == 1 ) {
2344 58         108 say $fh 'fig, ax0 = plt.subplots(1,1, layout = "constrained")';
2345             } else {
2346 0         0 die "\$single_plot = $single_plot breaks pigeonholes";
2347             }
2348 68 100       213 if ( defined $args->{plots} ) {
2349 10         17 my @undef_plot_types;
2350 10         19 my $j = 0;
2351 10         14 foreach my $plot (@{ $args->{plots} }) {
  10         32  
2352 22 50       56 next if defined $plot->{'plot.type'};
2353 0         0 push @undef_plot_types, $j;
2354 0         0 $j++;
2355             }
2356 10 50       34 if ( scalar @undef_plot_types > 0 ) {
2357 0         0 p $args;
2358 0         0 p @undef_plot_types;
2359 0         0 die 'The above subplot indices are missing "plot.type"';
2360             }
2361             }
2362 68         902 my %dispatch = (
2363             bar => \&barplot_helper, barh => \&barplot_helper,
2364             boxplot => \&boxplot_helper, colored_table=> \&colored_table_helper,
2365             hexbin => \&hexbin_helper, hist => \&hist_helper,
2366             hist2d => \&hist2d_helper, imshow => \&imshow_helper,
2367             pie => \&pie_helper, plot => \&plot_helper,
2368             scatter => \&scatter_helper, violin => \&violin_helper,
2369             violinplot => \&violin_helper,
2370             wide => \&wide_helper
2371             );
2372 68 100       203 if ($single_plot == 1) {
2373 58         80 foreach my $graph (@{ $args->{add} }) {
  58         252  
2374 1   33     7 $graph->{'plot.type'} = $graph->{'plot.type'} // $args->{'plot.type'};
2375 1         3 my $type = $graph->{'plot.type'};
2376 1 50       4 die 'plot.type not defined for "add" graph' unless defined $type;
2377 1 50       4 die "\"$type\" isn't a known plot.type" unless defined $dispatch{$type};
2378 1         8 $dispatch{$type}->({
2379             fh => $fh,
2380             ax => 0,
2381             plot => $graph
2382             });
2383             }
2384 58         124 delete $args->{add};
2385 58         113 my $type = $args->{'plot.type'};
2386 58 100       159 unless (defined $dispatch{$type}) {
2387 1         26 p $args;
2388 1         11448 die "$type isn't defined";
2389             }
2390 57         388 $dispatch{$type}->({
2391             fh => $fh,
2392             ax => 0,
2393             plot => $args
2394             });
2395 55         334 my %rename = (
2396             xlabel => 'set_xlabel',
2397             title => 'set_title',
2398             ylabel => 'set_ylabel',
2399             legend => 'legend',
2400             xlim => 'set_xlim',
2401             );
2402 55         69 foreach my $opt ( grep { defined $rename{$_} } keys %{$args} ) {
  603         742  
  55         174  
2403 31         74 $args->{ $rename{$opt} } = delete $args->{$opt};
2404             }
2405             plot_args({
2406 55         247 fh => $fh,
2407             args => $args,
2408             ax => 'ax0'
2409             });
2410             }
2411 65         168 my $ax = 0;
2412 65         73 foreach my $plot (@{ $args->{plots} } ) {
  65         172  
2413 22 100 66     66 if (
2414             (defined $args->{'shared.colorbar'}) && # shared.colorbar is defined
2415 4         13 (grep {$_ == $ax} @{ $args->{'shared.colorbar'} }) # subplot's colorbar is shared w other plots
  2         7  
2416             ) {
2417 2 100       4 if ($ax == max( @{ $args->{'shared.colorbar'} } )) { # this is the max
  2         10  
2418 1         2 $plot->{'colorbar.on'} = 1; # turn on if this is the max plot
2419 1         3 $plot->{'shared.colorbar'} = $args->{'shared.colorbar'};
2420 1         4 $plot->{cbpad} = $args->{cbpad};
2421             } else {
2422 1         4 $plot->{'colorbar.on'} = 0; # turn off, its colorbar will be shared later
2423             }
2424             }
2425 22 100       42 if (defined $plot->{add}) {
2426 2         5 my $ref = ref $plot->{add};
2427 2 50       7 if ($ref ne 'ARRAY') {
2428 0         0 die "\"add\" must be an array (of anonymous hashes), but you entered a $ref reference at ax = $ax";
2429             }
2430             }
2431 22         28 foreach my $graph (@{ $plot->{add} }) {
  22         55  
2432 2   33     8 $graph->{'plot.type'} = $graph->{'plot.type'} // $plot->{'plot.type'};
2433 2         5 my $type = $graph->{'plot.type'};
2434 2 50       6 die "plot.type not defined for \"add\" graph at ax = $ax" unless defined $type;
2435 2 50       6 die "\"$type\" isn't a known plot.type at ax = $ax" unless defined $dispatch{$type};
2436 2         8 $dispatch{$type}->({
2437             fh => $fh,
2438             ax => $ax,
2439             plot => $graph
2440             });
2441             }
2442 22         52 my @reqd_keys = (
2443             'data', # data type, of which several are available
2444             'plot.type', # "bar", "barh", "hist", etc.
2445             );
2446 22         39 my @undef_keys = grep { !defined $plot->{$_} } @reqd_keys;
  44         106  
2447 22 50       39 if ( scalar @undef_keys > 0 ) {
2448 0         0 p $plot;
2449 0         0 p @undef_keys;
2450 0         0 die "Above args are necessary, but were not defined for plot $ax.";
2451             }
2452 22         108 $dispatch{$plot->{'plot.type'}}->({
2453             fh => $fh,
2454             ax => $ax,
2455             plot => $plot
2456             });
2457 22         103 my %rename = (
2458             xlabel => 'set_xlabel', title => 'set_title',
2459             ylabel => 'set_ylabel', legend => 'legend',
2460             # xlim => 'set_xlim',
2461             );
2462 22         27 foreach my $opt ( grep { defined $rename{$_} } keys %{$plot} ) {
  159         187  
  22         46  
2463 8         23 $plot->{ $rename{$opt} } = delete $plot->{$opt};
2464             }
2465             plot_args({
2466 22         108 fh => $fh,
2467             args => $plot,
2468             ax => "ax$ax"
2469             });
2470 22         90 $ax++;
2471             }
2472 65         138 foreach my $ax (@ax) {
2473 84         150 say $fh "if $ax.has_data() == False:"; # remove empty plots
2474 84         213 say $fh "\t$ax.remove()"; # remove empty plots
2475             }
2476 65         139 my %methods = map { $_ => 1 } @plt_methods;
  14625         24598  
2477 65         531 foreach my $plt_method ( grep { defined $methods{$_} } keys %{$args} ) {
  730         883  
  65         236  
2478 7         11 my $ref = ref $args->{$plt_method};
2479 7 50       17 if ( $ref eq '' ) {
    0          
2480 7         18 my $type = print_type($args->{$plt_method});
2481 7 100       15 if ($plt_method eq 'show') {
    50          
    0          
2482 6         11 next; # plt.show() is emitted after plt.savefig() below
2483             } elsif ($type eq 'single quotes') {
2484 1         5 say $fh "plt.$plt_method('$args->{$plt_method}')#" . __LINE__;
2485             } elsif ($type eq 'no quotes') {
2486 0         0 say $fh "plt.$plt_method($args->{$plt_method})#" . __LINE__;
2487             }
2488             } elsif ( $ref eq 'ARRAY' ) {
2489 0         0 foreach my $j ( @{ $args->{$plt_method} } ) {
  0         0  
2490 0         0 my $type = print_type($j);
2491 0 0       0 if ($type eq 'single quotes') {
    0          
2492 0         0 say $fh "plt.$plt_method('$j')#" . __LINE__;
2493             } elsif ($type eq 'no quotes') {
2494 0         0 say $fh "plt.$plt_method($j)#" . __LINE__;
2495             }
2496             }
2497             } else {
2498 0         0 p $args;
2499 0         0 die "$plt_method = \"$ref\" only accepts scalar or array types";
2500             }
2501             }
2502 65         145 %methods = map { $_ => 1 } @fig_methods;
  6240         9216  
2503 65         289 foreach my $fig_method ( grep { defined $methods{$_} } keys %{$args} ) {
  730         856  
  65         175  
2504 0         0 my $ref = ref $args->{$fig_method};
2505 0 0       0 if ( $ref eq '' ) {
    0          
2506 0         0 say $fh "fig.$fig_method($args->{$fig_method})#" . __LINE__;
2507             } elsif ( $ref eq 'ARRAY' ) {
2508 0         0 foreach my $j ( @{ $args->{$fig_method} } ) { # say $fh "plt.$method($plt)";
  0         0  
2509 0         0 say $fh "fig.$fig_method($j)";
2510             }
2511             } else {
2512 0         0 p $args;
2513 0         0 die "$fig_method = \"$ref\" only accepts scalar or array types";
2514             }
2515             }
2516 65 100       182 if (defined $args->{scale}) {
2517 1         6 say $fh "fig.set_figheight(plt.rcParams['figure.figsize'][1] * $args->{scale}) #" . __LINE__;
2518 1         4 say $fh "fig.set_figwidth(plt.rcParams['figure.figsize'][0] * $args->{scale}) #" . __LINE__;
2519             }
2520 65 100       117 if (defined $args->{scalex}) {
2521 1         4 say $fh "fig.set_figwidth(plt.rcParams['figure.figsize'][0] * $args->{scalex}) #" . __LINE__;
2522             }
2523 65 100       109 if (defined $args->{scaley}) {
2524 1         3 say $fh "fig.set_figheight(plt.rcParams['figure.figsize'][1] * $args->{scaley}) #" . __LINE__;
2525             }
2526 65 100       112 if (defined $args->{'output.file'}) {
2527             write_data({
2528 59         291 data => $args->{'output.file'},
2529             fh => $fh,
2530             name => 'output_file'
2531             });
2532 59         1363 say $fh "plt.savefig(output_file, bbox_inches = 'tight', metadata={'Creator': 'made/written by "
2533             . getcwd()
2534             . "/$RealScript called using \"$current_sub\" in " . __FILE__ . " version $VERSION'})";
2535             }
2536 65 100       182 say $fh 'plt.show()' if $args->{show}; # after savefig, so the file is written even if the window is never closed
2537 65   50     185 $args->{execute} = $args->{execute} // 1;
2538 65 50       188 say $fh 'plt.close()' if $args->{execute} == 0;
2539 65 50       126 if ( $args->{execute} ) {
2540             my ($stdout, $stderr, $exit) = capture {
2541 0     0   0 system( 'python3 ' . $fh->filename )
2542 0         0 };
2543 0 0       0 if ($exit != 0) {
2544 0         0 say STDERR "STDOUT = $stdout";
2545 0         0 say STDERR "STDERR = $stderr";
2546 0         0 die 'python3 ' . $fh->filename . ' failed';
2547             }
2548             say 'wrote '
2549 0 0       0 . colored( ['cyan on_bright_yellow'], "$args->{'output.file'}" ) if defined $args->{'output.file'};
2550             } else { # not running yet
2551             say 'will write '
2552 65 100       374 . colored( ['cyan on_bright_yellow'], "$args->{'output.file'}" ) if defined $args->{'output.file'};
2553             }
2554 65         4012 return $fh->filename;
2555             }
2556             # Generate wrappers dynamically
2557             my @wrappers = qw(bar barh boxplot colored_table hexbin hist hist2d imshow pie plot scatter violin violinplot wide);
2558              
2559             foreach my $sub_name (@wrappers) {
2560 6     6   62 no strict 'refs'; # Gemini helped
  6         10  
  6         1924  
2561             *$sub_name = sub {
2562             # accept both bar({ ... }) and bar( ... )
2563 14     14   7406 my $args;
2564 14 50 33     88 if ( ( scalar @_ == 1 ) && ( ref $_[0] eq 'HASH' ) ) {
    50          
2565 0         0 $args = $_[0];
2566             } elsif ( ( scalar @_ % 2 ) == 0 ) {
2567 14         42 $args = { @_ };
2568             } else {
2569 0         0 die "$sub_name: odd number of arguments; call as $sub_name( key => value, ... ) or $sub_name({ key => value, ... })";
2570             }
2571             # Check for conflicts
2572 14 50 33     39 if ((defined $args->{'plot.type'}) && ($args->{'plot.type'} ne $sub_name)) {
2573 0         0 warn "$args->{'plot.type'} will be ignored for $sub_name";
2574             }
2575 14 50       25 if (defined $args->{plots}) {
2576 0         0 die "\"plots\" is meant for the subroutine \"plt\"; $sub_name is single-only";
2577             }
2578 14 50       29 if (defined $args->{p}) {
2579 0         0 die "\"p\" is meant for the subroutine \"plt\"; $sub_name is single-only";
2580             }
2581             # Call plt
2582 14         15 plt({ %{ $args }, 'plot.type' => $sub_name });
  14         55  
2583             };
2584             }
2585             1;
2586             __END__