From c87c377e016970b4d908c454be0134a3f231c052 Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Thu, 26 Mar 2026 22:19:26 -0500 Subject: [PATCH] Clean up and fix issues in the `StatisticalPlots.pl` macro. The POD is rather heftily rewritten, and there were a lot of issues in the code that were fixed. I don't even remember all of them, since I did this several months ago. One issue was with the `set_plot_option_aliases` method. It defined the aliases with a hash with duplicate keys. That means only the last one of the duplicates actually worked. So the definition needed to be inverted to function right. Another option was with incorrectly names svg colors. Also, switch to using the new plots `padding` option instead of using a multiplier. The `padding` options scales better. Don't blindly add the `fill => 'self'` option to everything. Just add it to things that can be possible filled. In fact, since the `add_rectangle` method handles this, the `StatisticalPlots.pl` macro does not need to. Also, there was quite a bit of general code clean up. --- macros/graph/StatisticalPlots.pl | 862 ++++++++++++++++--------------- macros/graph/plots.pl | 2 + 2 files changed, 438 insertions(+), 426 deletions(-) diff --git a/macros/graph/StatisticalPlots.pl b/macros/graph/StatisticalPlots.pl index 6239b667e..4fc0ad2d6 100644 --- a/macros/graph/StatisticalPlots.pl +++ b/macros/graph/StatisticalPlots.pl @@ -1,15 +1,16 @@ =head1 NAME -StatisticalPlots.pl - A macro to create dynamic statistics plots to include in PG problems. +StatisticalPlots.pl - A macro to create statistical plots to include in PG +problems. =head1 DESCRIPTION -This macro includes a number of methods to include statistical plots in PG problems. This is based -on L which will draw using either C (default for hardcopy) or C (default -for HTML). +This macro includes a number of methods to include statistical plots in PG +problems. This is based on L which will draw using either C +(default for hardcopy) or C (default for HTML). -The statistical plots available are +The statistical plots that are available are =over @@ -27,26 +28,27 @@ =head1 DESCRIPTION =head2 USAGE -First, start with a C object as in +Create a C object with loadMacros('StatisticalPlots.pl'); - $stat_plot = StatPlot( + + $statPlot = StatPlot( xmin => -1, - xmax => 8, + xmax => 8, ymin => -1.5, - ymax => 10, - xtick_delta => 1, - ytick_delta => 4, + ymax => 10, + xtick_delta => 1, + ytick_delta => 4, aria_label => 'Bar plot of a set of data' ); -The options for C are identical to that of a C object and all options are in the -L. Note that each of the x- and y-axes have separate options and each -option is preceded with an C or C. +The C method accepts all of the same options that the C method +from the L macro accepts. -After a C object is created then specific plots are added to the axes. For example: +After a C object is created then specific plots can be added to the +axes. For example, - $hist->add_barplot( + $statPlot->add_barplot( [ 1 .. 6 ], [3, 6, 7, 8, 4, 1], fill_color => 'yellow', @@ -54,43 +56,38 @@ =head2 USAGE bar_width => 0.9 ); -will add a barplot to the axes with heights defined in the second argument at the x-locations -C<(1..6)>. - -See below for more details about creating a barplot and its options. +will add a bar plot to the axes. See below for more details about creating a bar +plot. -=head1 PLOT ELEMENTS +=head1 AVAILABLE PLOTS -A statistical plot is a set of axes with one or more plot objects such as bar plots, box plots or -scatter plots. A C must be created first and then one or more of the following can be -added. +The following plots are available to add to a C object. =head2 BAR PLOTS -A bar plot can be added using the C<< $stat_plot->add_barplot >> method. +A bar plot can be added with the C method. The general form is - $stat_plot->add_barplot($xdata, $ydata, %opts); + $statPlot->add_barplot($xdata, $ydata, %opts); -This adds vertical bars (as the default) centered at the values in the array reference C<$xdata> -with heights from C<$ydata>, also an array reference. +This adds a bar plot with bars centered at the coordinates in the array +referenced to by C<$xdata> and heights in the array referenced to by C<$ydata>. =head3 OPTIONS -There are two types of options for the C method. The following are specific to -changing the barplot, and any more are passed along to C, which is a wrapper function -for C which draws the bars. - -The following are options for the barplot itself: +The following options which are specific to configuring the bar plot can be +passed in addition to any of the dataset options accepted by the +L method which draws the bars (see +L). =over =item orientation -The C option can take values C<'vertical'> (default) or C<'horizontal'> to create -vertical or horizontal bars. Above was an example with vertical bars, and an example with -horizontal bars is +This can be either C<'vertical'> (default) or C<'horizontal'> to create vertical +or horizontal bars. An example with horizontal bars was given above, and the +following is an example with with vertical bars - $hist->add_barplot( + $statPlot->add_barplot( [3, 6, 7, 8, 4, 1], [ 1 .. 6 ], orientation => 'horizontal', @@ -101,43 +98,46 @@ =head3 OPTIONS =item bar_width -The option C is a number in the range [0,1] to give the relative width of the bar. If -C<< bar_width => 1 >> (default), then there is no gap between bars. +This is a number from C<0> to C<1> that specifies the width of the bars as a +fraction of the distance between the bar base coordinates (the x-coordinates for +a vertical bar plot or the y-coordinates for a horizontal bar plot). If +C<< bar_width => 1 >> (the default), then the bar width will be the distance +between the bar base coordinates (and so there is no gap between bars). In the +example above, with C<< bar_width => 0.9 >> the bar width will be C<90%> of the +distance between the bar base coordinates. -=item fill_color +=item stroke_color -This is the color of the bars, which is passed to the C method. If this is not -included, then C is set to C<'self'>, the natural way to fill a rectangle. +This sets the color of the bar borders. It is an alias for the C +L option of the +L method. See +L for more details on specifying colors. -See L for more details on specifying colors. +=item stroke_width -=item stroke_color - -This is an alias for the C option of the C method. This specifies the color of -the boundary of the rectangle. See L for more details on specifying -colors. +This sets the width of the rectangle borders. This is an alias for the C +L. =back -Any remaining options are passed to C which has the same options as C. -See L for other options. - =head2 HISTOGRAMS -A L is added to a C with the -`add_histogram` method. The general form is +An histogram can be added to a C with the C method. The +general form is + + $statPlot->add_histogram($data, %options); - $stat_plot->add_histogram($data, %options); +where C<$data> is a reference to an array of univariate data. The C<%options> +parameter includes options for the histogram such as the number of bins as well +as options for drawing the bars. -where C<$data> is an array ref of univariate data. The C<%options> include both options for the -histogram (e.g. number of bins) as well as options for the bars. +The following example generates a data set using the L +function which produces a random set of normally distributed data values, and +constructs a histogram for that data set. -Here is an example using the C function from C which produces -normally distributed random variables. + loadMacros('StatisticalPlots.pl', 'PGstatisticsmacros.pl'); - macros('StatisticalPlots.pl', 'PGstatisticsmacros.pl'); - @data = urand(30, 9, 50, 6); # create 50 random variables with mean 30 and std. dev of 9. - $stat_plot = StatPlot( + $statPlot = StatPlot( xmin => 0, xmax => 65, ymin => 0, @@ -145,325 +145,342 @@ =head2 HISTOGRAMS xtick_delta => 10, ytick_delta => 2 ); - $stat_plot->add_histogram( - ~~@data, + $statPlot->add_histogram( + [ urand(30, 9, 50, 6) ], min => 10, max => 60, bins => 10, - fill_color => 'lightgreen', + fill_color => 'LightGreen', width => 1 ); -The first argument to C is an array ref of univariate data. - =head3 Options -The following are options specific to histograms. +The following options which are specific to configuring the histogram can be +passed in addition to any of the dataset options accepted by the +L method which draws the bars for the +histogram (see L). Note that if the +C option is set, then C<< fill => 'self' >> is automatically set, +and so you do not need to specify that option. =over +=item orientation + +This can be either C<'vertical'> (default) or C<'horizontal'> to create vertical +or horizontal bars. + =item min -The left edge of the leftmost box. If not defined, the minimum of C<$data> is used. +The left edge of the leftmost bar. If not defined, the minimum value in the +C<$data> array is used. =item max -The right edge of the rightmost box. If not defined, the maximum of C<$data> is used. +The right edge of the rightmost bar. If not defined, the maximum value in +C<$data> is used. =item bins -The number of bins/boxes to use for the histogram. This must be an integer greater than 0. If not -defined, the default value of 10 is used. +The number of bins/bars to use for the histogram. This must be an integer +greater than 0. The default value is 10. =item normalize -If 0 is used (the default), the height of the bars is the count of the number of points within each -bin. If the value is 1, then the heights are scaled so the total height of the bars is 1. - -=item fill_color - -This is the color of the bars, which is passed to the C method. If this is not -included, then C is set to C<'self'>, the natural way to fill a rectangle. - -See L for more details on specifying colors. +If this is set to 0 (the default), then the height of each bar is the number of +data values in the bin. If this is 1, then the heights are scaled so the sum of +the heights of the bars is 1. =item stroke_color -This sets the color of the boundary of the rectangle and the whiskers. It is an alias for the -C option of L. See L for options to -change the color. +This sets the color of the bar borders. It is an alias for the C +L option of the +L method. See +L for more details on specifying colors. =item stroke_width -This sets the width of the boundary of the rectangle and the whiskers. This is an alias for the -C option of L. +This sets the width of the rectangle borders. This is an alias for the C +L. =back -The rest of the options are passed through to the L method in which the fill -color and opacity as well as the stroke color and width. See both L and -L for more details. - =head2 BOX PLOTS -A box plot (also called a box and whiskers plot) can be created with the C method. If -one performs +A box plot (also called a box and whiskers plot) can be created with the +C method. The general form for adding a single box plot is - $stat_plot->add_boxplot($data, %options); + $statPlot->add_boxplot($data, %options); -or if one has multiple box plots +or to add multiple box plots is - $stat_plot->add_boxplot([$data1, $data2, ...], %options); + $statPlot->add_boxplot([$data1, $data2, ...], %options); -where C<$data> (or C<$data1>, C<$data2>, ...) is an array ref of univariate data or a hash ref of -the boxplot characteristics, then a box plot is created using the five number summary (minimum, -first quartile, median, third quartile, maximum) of the data. These values are calculated using -the C function from C. An example of creating a boxplot -with an array reference of univariate data is +where C<$data> (or C<$data1>, C<$data2>, ...) is a reference to an array of +univariate data or a reference to a hash of the box plot characteristics. Box +plots are created using the five number summary (minimum, first quartile, +median, third quartile, maximum) of the data which is calculated using the +L method. - @data = urand(100, 25, 75, 6); +An example of creating a box plot with a reference to an array of univariate +data is - $boxplot = StatPlot( + $statPlot = StatPlot( xmin => 0, xmax => 200, - xtick_delta => 25, - show_grid => 0, ymin => -5, - ymax => 25, - yvisible => 0, + ymax => 25, + xtick_delta => 25, + yvisible => 0, + show_grid => 0, aspect_ratio => 4, rounded_corners => 1 ); - $boxplot->add_boxplot(~~@data, fill_color => 'LightBlue', stroke_width => 1); - -and as with other methods, one can pass options for the box plot (like fill color, stroke color, -and width) within the C method. - -If C<$data> is a hash reference, it must contains the fields C that are -used to define the boxplot. Optionally, one may also include the field C which is an -array ref of values which will be plotted beyond the whiskers. - -An example of this is + $statPlot->add_boxplot( + [ urand(100, 25, 75, 6) ], + fill_color => 'LightBlue', + stroke_width => 1 + ); - $params = { - min => random(150, 175, 5), - q1 => random(180, 225, 5), - median => random(250, 275, 5), - q3 => random(280, 320, 10), - max => random(325, 350, 5), - outliers => [115,130] - }; +If C<$data> is a hash reference, then it must contains the fields C that form the five number summary. Optionally, the field +C may also be included which is a reference to an array of values +which will be plotted outside of the whiskers. For example, $boxplot = StatPlot( xmin => 100, xmax => 400, - xtick_delta => 50, - show_grid => 0, ymin => -5, - ymax => 25, - yvisible => 0, + ymax => 25, + xtick_delta => 50, + yvisible => 0, + show_grid => 0, aspect_ratio => 4 ); - $boxplot->add_boxplot($params); + $boxplot->add_boxplot({ + min => random(150, 175, 5), + q1 => random(180, 225, 5), + median => random(250, 275, 5), + q3 => random(280, 320, 10), + max => random(325, 350, 5), + outliers => [115, 130] + }); =head3 Options -The following are options to the C method. +The following options which are specific to configuring the box plot can be +passed in addition to any of the L. +Note that if the C option is set, then C<< fill => 'self' >> is +automatically set, and so you do not need to specify that option. =over =item orientation -This is the direction of the box plot and can take on values 'horizontal' (default) or 'vertical'. +This is the direction of the box plot and can be either 'horizontal' (the +default) or 'vertical'. =item box_center -The location of the center of the box. This is optional and if not defined will center the box -between the axis and the edge of the plot. +The location of the center of the box. This is optional and if not defined the +box plot will be centered in the range of the minor axis (the y-axis for +horizontal box plots and the x-axis for vertical box plots). -If multiple box plots are included, this option will be created to equally space the box plots -between the axis and the edge of the plot. If included, this option must be an array reference of -values (in the x-direction for vertical plots and y-direction for horizontal). +If multiple box plots are being added and this option is not set, then the box +plots will be equally spaced in the range of the minor axis. - box_center => [3, 6, 9] +If included, this option must be a reference to an array of values (in the +x-direction for vertical plots and y-direction for horizontal plots) at which to +center the added box plots. The number of entries in the array must be the same +as the number of box plots that are being added. For example, if three box plots +are being added this could be -as an example. + box_center => [3, 6, 9] =item box_width -The width of the box in the direction perpendicular to the orientation. If not defined, it will -take the value of 0.5 times the space between the axis and the edge of the plot. +The width of the box in the direction perpendicular to the orientation. The +default is to use half of the range of the minor axis. If multiple box plots are defined, this should only be a single value. =item whisker_cap -Value of 0 (default) or 1. If this value is 1, a short line will be added that is perpendicular to -the whiskers on the boxplot with relative size C. +This should be set to 0 (default) or 1. If this value is 1, lines that are +perpendicular to the whiskers will be added at the ends of the whiskers of the +box plot. The length of the lines are determined by the C option. =item cap_width -The width of the cap as a fraction of the box width. Default value is 0.2. +The width of the cap as a fraction of the box width. Default value is 0.2. =item outlier_mark -The shape of the mark to use for outliers. Default is 'plus'. -See L for other mark options. - -=item fill_color - -This is the color of the bars, which is passed to the C method. If this is not -included, then C is set to C<'self'>, the natural way to fill a rectangle. - -See L for more details on specifying colors. +The shape of the mark to use for outliers. The default is 'plus'. See the +L for other mark options. =item stroke_color -This sets the color of the boundary of the rectangle and the whiskers. It is an alias for the -C option of L. See L for options to -change the color. +This sets the color of the boundary of the box and the whiskers. It is an alias +for the C L. See +L for more details on specifying colors. =item stroke_width -This sets the width of the boundary of the rectangle and the whiskers. This is an alias for the -C option of L. +This sets the width of the boundary of the rectangle and the whiskers. This is +an alias for the C L. =back -As with other methods, other options can be passed to L and -C. - - =head2 SCATTER PLOTS -To produce a scatter plot, use the C method to a C. The general form is +A scatter plot can be created with the C method. The general +form for adding a scatter plot is - $stat_plot->add_scatterplot($data, %options); + $statPlot->add_scatterplot($data, %options); -where the dataset in C<$data> is an array reference of C pairs as an array reference. For -example, +where C<$data> is a reference to an array of array references of C, C +pairs. For example, - $stat_plot = StatPlot( + $statPlot = StatPlot( xmin => -1, xmax => 15, - xtick_delta => 5, ymin => -1, ymax => 15, + xtick_delta => 5, ytick_delta => 5, ); - $data = [ [1,1], [2,3], [3,4], [5,5], [7,8], [10,9], [12,10]]; - - $stat_plot->add_scatterplot($data, marks => 'diamond', mark_size => 5, color => 'orange'); + $statPlot->add_scatterplot( + [ [1, 1], [2, 3], [3, 4], [5, 5], [7, 8], [10, 9], [12, 10] ], + marks => 'diamond', + mark_size => 5, + color => 'orange' + ); -This method is simply a wrapper for the C method where the defaults are different. +This method simply calls the L method with the +following L set. Any of these can be +changed or any of the other data set options set in the C<%options> parameter. =over =item linestyle -The C option is set to 'none', so that lines are not drawn between the points. +This sets the style of the lines between the marks is set to 'none' by this +method, so that lines are not drawn between the points. =item marks -The C is default to 'circle'. See L for -other mark options. +This sets the symbol to use for the marks and is set to C<'circle'> by this +method. See the L for other mark +options. =item mark_size -The C is default to 3. +This sets the size of the marks and is set to 3 by this method. =item mark_color -This changes the mark color and is an alias for the C option. See L for -options to change the color. +This sets the color of the marks and is an alias for the C +L option. See L +for more details on specifying colors. =back -If more that one dataset is to be plotted, simply call the C method multiple times. -This can be done with a single C method call, but this wrapper makes it easier to set -different options. +If more than one dataset is to be plotted, then call the C +method multiple times. This can be done with a single C method +call, but this wrapper is convenient when the default options above are desired. =head2 PIE CHARTS -A pie chart is a circle that divided in to sectors whose size is proportional to an input array. -The sectors are generally each given a color and a label. This method can also produce donut -charts (or ring charts), which is a pie chart with a hole. +A pie chart is a circle that divided into sectors whose size is proportional to +an input array. The sectors are each assigned a color and a label. This method +will also produce donut charts (or ring charts), which is a pie chart with a +hole. + +The general form for calling the C method is -The general form is + $statPlot->add_piechart($data, %options); - $stat_plot->add_piechart($data, %options); +where C<$data> is a reference to an array of values. -where $data is an array reference of values. +=head3 Options -The following are the options: +The following options which are specific to configuring the pie chart can be +passed in addition to any of the L. =over =item center -The center of the circle as an array reference. The default value is C<[0,0]>. +The center of the circle as an array reference. The default value is C<[0, 0]>. =item radius -The radius of the circle. The default value of C<4> is chosen to fit nicely with the default values -of the bounding box of the C which ranges from -5 to 5 in both the x- and y-directions. +The radius of the circle. The default value of 4 is chosen to fit nicely with +the default values for the bounding box of the C which ranges from -5 +to 5 in both the x and y directions. =item inner_radius -If you desire a donut chart or ring chart, set this to a value less than the radius. The default -value is 0. +This is the radius of the inner circle of the chart. Set this to a value less +than the C for a donut or ring chart. The default value is 0. =item angle_offset -The first sector by default starts at angle 0 (from the positive horizontal axis) in degrees, but -can be changed with this option. +This is the angle in degrees from the positive horizontal axis at which the +first sector begins. This is 0 by default. =item fill_colors -This is either the name of a color palette (as a string), an array reference of colors, or a hash -reference for the name of the color palette and number of colors to generate (not available for all -palettes). If the length of this array reference is smaller than the C<$data> array reference, then -the colors will be cycled. The default is to use the 'default' color palette. See -L for more information. +This is either the name of a color palette, a reference to an array of colors, +or a reference to a hash containing the name of a color palette +(C) and number of colors to generate (C) (see L for more information). If this is a reference to an array and the +length of the array is smaller than the number of data values in the array +referenced to by C<$data>, then the colors will be cycled. The default is to use +the C<'rainbow'> color palette. -The following are possible options. +For example, - fill_colors => 'rainbow' # generates the rainbow palette + fill_colors => 'rainbow' fill_colors => ['green', 'OliveGreen', 'DarkGreen', 'ForestGreen', 'PineGreen'] - fill_colors => {palette_name => 'random', num_colors => 7} + fill_colors => { palette_name => 'random', num_colors => 7 } =item color_sectors -If this is 1 (default), then colors are used for the pie chart. If 0, then the sectors are not -filled. See C for selecting colors. +If this is 1 (the default), then colors are used for the pie chart. If 0, then +the sectors are not filled. See L for details on selecting +colors. =item sector_labels -The labels for the sector as a array reference of strings or values. The default is for no labels. -If this is used, the length of this must be the same as the C<$data> array reference. +The labels for the sector as a array reference of strings or values. The +default is for no labels. If this is used, the length of this must be the same +as the C<$data> array reference. =back =head2 COLOR PALETTES -The color palettes for the bar plots and pie charts can be select from the C -function. This allows a number of built-in/generated color palettes. To get an array reference -of either named or generated colors: +The color palettes for the bar plots and pie charts can be selected with the +C function. This allows a number of built-in or generated color +palettes. To get an array reference of either named or generated colors call - color_palette($name, $n); + color_palette($name, $numColors); For example, color_palette('rainbow'); -returns the 6 colors of the rainbow. Some of the palettes have fixed numbers of colors, whereas -others have variable numbers. If C is not defined, then some palettes return a fixed -number (like 'rainbow') and if the C is needed, then the default of 10 is assumed. +returns the 6 colors of the rainbow. Some of the palettes have fixed numbers of +colors, whereas others have variable numbers. If C<$numColors> is not defined, +then some palettes return a fixed number (like 'rainbow') and if the +C<$numColors> is needed, then the default of 10 is assumed. =head3 PALETTE NAMES @@ -471,29 +488,29 @@ =head3 PALETTE NAMES =item rainbow -The colors of the rainbow from violet to red. The C options is ignored. - -=item random - -This will return C random colors from the defined SVG colors. +The 6 colors of the rainbow from violet to red. The C<$numColors> value is +ignored for this palette. =item reds -This will return a selection of red colors. If C is passed in, the number is ignored. +This will return a selection of red colors. The C<$numColors> value is ignored +for this palette. =item blues -This will return a selection of blue colors. If C is passed in, the number is ignored. +This will return a selection of blue colors. The C<$numColors> value is ignored +for this palette. =item greens -This will return a selection of green colors. If C is passed in, the number is ignored. +This will return a selection of green colors. The C<$numColors> value is ignored +for this palette. -=back +=item random -=head2 LEGENDS +This will return C<$numColors> random colors from the defined SVG colors. -TODO: A legend is helpful for some plots. +=back =cut @@ -503,116 +520,98 @@ sub _StatisticalPlots_init { main::PG_restricted_eval('sub StatPlot { Plots::StatPlot->new(@_); }'); } -loadMacros('PGstatisticsmacros.pl'); +loadMacros('PGstatisticsmacros.pl', 'PGauxiliaryFunctions.pl'); package Plots::StatPlot; our @ISA = qw(Plots::Plot); sub add_histogram { - my ($self, $data, %opts) = @_; + my ($self, $data, %options) = @_; - my %options = ( - bins => 10, - normalize => 0, - orientation => 'vertical', - %opts - ); + $options{orientation} //= 'vertical'; - Value::Error("The option 'bins' must be a positive integer") - unless $options{bins} =~ /^\d+$/ && $options{bins} > 0; + my $numBins = delete $options{bins} // 10; + $numBins = 10 unless $numBins =~ /^\d+$/ && $numBins > 0; - my @counts = (0) x $options{bins}; - my $min = $options{min} // main::min(@$data); - my $max = $options{max} // main::max(@$data); - my $bin_width = ($max - $min) / $options{bins}; + my $min = delete $options{min} // main::min(@$data); + my $max = delete $options{max} // main::max(@$data); + my $binWidth = ($max - $min) / $numBins; - # TODO: if the bin_width is 0, set the num_bins to 1 and give a non-zero bin_width. + my @frequencies = (0) x $numBins; + ++$frequencies[ int(($_ - $min) / $binWidth) ] for @$data; - $counts[ int(($_ - $min) / $bin_width) ]++ for (@$data); - if ($options{normalize}) { + if (delete $options{normalize}) { my $total = 0; - $total += $_ for (@counts); - @counts = map { $_ / $total } @counts; + $total += $_ for @frequencies; + @frequencies = map { $_ / $total } @frequencies; } - my @xdata = map { $min + (0.5 + $_) * $bin_width } (0 .. $#counts); - # Remove these options and pass the rest to add_barplot - delete $options{$_} for ('min', 'max', 'bins', 'normalize'); - - if ($options{orientation} eq 'vertical') { - $self->add_barplot(\@xdata, \@counts, %options); - } else { - $self->add_barplot(\@counts, \@xdata, %options); - } + my @xdata = map { $min + (0.5 + $_) * $binWidth } (0 .. $#frequencies); - return \@counts; + return $self->add_barplot( + $options{orientation} eq 'vertical' ? (\@xdata, \@frequencies) : (\@frequencies, \@xdata), %options + ), + \@frequencies; } -# Create a barplot where for each x in xdata, create a bar of height y in ydata. - sub add_barplot { - my ($self, $xdata, $ydata, %opts) = @_; + my ($self, $xdata, $ydata, %options) = @_; - my %options = ( - bar_width => 1, - orientation => 'vertical', - plot_option_aliases(%opts) - ); + $options{bar_width} //= 1; + $options{orientation} //= 'vertical'; + set_plot_option_aliases(\%options); - Value::Error('The lengths of the data in the first two arguments must be array references of the same length') - unless ref $xdata eq 'ARRAY' && ref $xdata eq 'ARRAY' && scalar(@$xdata) == scalar(@$ydata); + Value::Error('The first two arguments must be references to arrays of the same length.') + unless (ref $xdata eq 'ARRAY' && ref $xdata eq 'ARRAY' && @$xdata == @$ydata); - # assume that the $xdata is equally spaced. TODO: should we handle arbitrary spaced bars? + # Assume that the values in $xdata are equally spaced. + # TODO: Should arbitrarily spaced bars be handled? my $bar_width = $options{orientation} eq 'vertical' ? $xdata->[1] - $xdata->[0] : $ydata->[1] - $ydata->[0]; - # if fill_color is passed as an option, set the 'fill' to 'self'. - $options{fill} = 'self' if $options{fill_color}; - - for my $j (0 .. scalar(@$xdata) - 1) { - if ($options{orientation} eq 'vertical') { - $self->SUPER::add_rectangle([ $xdata->[$j] - 0.5 * $bar_width * $options{bar_width}, 0 ], - [ $xdata->[$j] + 0.5 * $bar_width * $options{bar_width}, $ydata->[$j] ], %options); - } else { - $self->SUPER::add_rectangle([ 0, $ydata->[$j] - 0.5 * $bar_width * $options{bar_width} ], - [ $xdata->[$j], $ydata->[$j] + 0.5 * $bar_width * $options{bar_width} ], %options); - } - } + return $self->add_rectangle( + map { [ + $options{orientation} eq 'vertical' + ? ( + [ $xdata->[$_] - 0.5 * $bar_width * $options{bar_width}, 0 ], + [ $xdata->[$_] + 0.5 * $bar_width * $options{bar_width}, $ydata->[$_] ] + ) + : ( + [ 0, $ydata->[$_] - 0.5 * $bar_width * $options{bar_width} ], + [ $xdata->[$_], $ydata->[$_] + 0.5 * $bar_width * $options{bar_width} ] + ), + %options + ] } 0 .. $#$xdata + ); } sub add_boxplot { - my ($self, $data, %opts) = @_; - - my %options = ( - orientation => 'horizontal', - whisker_cap => 0, - cap_width => 0.2, - outlier_mark => 'plus', - plot_option_aliases(%opts) - ); + my ($self, $data, %options) = @_; + + $options{orientation} //= 'horizontal'; + $options{whisker_cap} //= 0; + $options{cap_width} //= 0.2; + $options{outlier_mark} //= 'plus'; + set_plot_option_aliases(\%options); - # Placeholder for boxplot implementation. if (ref $data eq 'ARRAY' && (ref $data->[0] eq 'ARRAY' || ref $data->[0] eq 'HASH')) { - my ($box_centers, $box_width); + my $box_centers; if ($options{box_center}) { - Value::Error( - "The option 'box_center' must be an array ref with the same length as the box plots to produce.") - unless ref $options{box_center} eq 'ARRAY' && scalar(@{ $options{box_center} }) == scalar(@$data); - $box_centers = $options{box_center}; - delete $options{box_center}; + Value::Error(q{The "box_center" option must be a reference to an array } + . 'with the same length as the number of box plots being added.') + unless (ref $options{box_center} eq 'ARRAY' && @{ $options{box_center} } == @$data); + $box_centers = delete $options{box_center}; } else { - my $n = scalar(@$data); unless ($options{box_width}) { $options{box_width} = ($options{orientation} eq 'vertical' ? $self->axes->xaxis('max') : $self->axes->yaxis('max')) / - (2.5 * $n); + (2.5 * @$data); } - $box_centers = [ map { 2 * $options{box_width} * $_ } (1 .. $n + 1) ]; + $box_centers = [ map { 2 * $options{box_width} * $_ } (1 .. @$data + 1) ]; } for (0 .. $#$data) { $options{box_center} = $box_centers->[$_]; $self->_add_boxplot($data->[$_], %options); } - } else { $self->_add_boxplot($data, %options); } @@ -633,96 +632,122 @@ sub _add_boxplot { max => $five_point[4] }; } elsif (ref $data eq 'HASH') { - # check that all aspects of the boxplot are passed in. - my %count; - $count{$_}++ for ('min', 'q1', 'median', 'q3', 'max'); - $count{$_}-- for (keys %$data); - for (keys %count) { - Value::Error("The parameter $_ is missing from the boxplot attributes.") if $count{$_} > 0; + # Check that all elements of the five number summary were provided. + my %missing; + for ('min', 'q1', 'median', 'q3', 'max') { + $missing{$_} = 1 unless defined $data->{$_}; + } + for (keys %missing) { + Value::Error(qq{The parameter "$_" is missing from the box plot five number summary.}); } + $params = $data; } - # if fill_color is passed as an option, set the 'fill' to 'self'. - $options{fill} = 'self' if $options{fill_color}; - if ($orientation eq 'horizontal') { my $box_center = $options{box_center} // 0.5 * $self->axes->yaxis->{max}; my $box_width = $options{box_width} // 0.5 * $self->axes->yaxis->{max}; $self->add_rectangle([ $params->{q1}, $box_center - 0.5 * $box_width ], [ $params->{q3}, $box_center + 0.5 * $box_width ], %options); - $self->add_dataset([ $params->{min}, $box_center ], [ $params->{q1}, $box_center ], %options); - $self->add_dataset([ $params->{q3}, $box_center ], [ $params->{max}, $box_center ], %options); - $self->add_dataset([ $params->{median}, $box_center - 0.5 * $box_width ], - [ $params->{median}, $box_center + 0.5 * $box_width ], %options); + $self->add_dataset( + [ [ $params->{min}, $box_center ], [ $params->{q1}, $box_center ], %options ], + [ [ $params->{q3}, $box_center ], [ $params->{max}, $box_center ], %options ], + [ + [ $params->{median}, $box_center - 0.5 * $box_width ], + [ $params->{median}, $box_center + 0.5 * $box_width ], + %options + ] + ); - # add whisker caps if ($options{whisker_cap}) { - $self->add_dataset([ $params->{max}, $box_center - 0.5 * $options{cap_width} * $box_width ], - [ $params->{max}, $box_center + 0.5 * $options{cap_width} * $box_width ], %options); - $self->add_dataset([ $params->{min}, $box_center - 0.5 * $options{cap_width} * $box_width ], - [ $params->{min}, $box_center + 0.5 * $options{cap_width} * $box_width ], %options); + $self->add_dataset( + [ + [ $params->{max}, $box_center - 0.5 * $options{cap_width} * $box_width ], + [ $params->{max}, $box_center + 0.5 * $options{cap_width} * $box_width ], + %options + ], + [ + [ $params->{min}, $box_center - 0.5 * $options{cap_width} * $box_width ], + [ $params->{min}, $box_center + 0.5 * $options{cap_width} * $box_width ], + %options + ] + ); } - if ($params->{outliers}) { - my @points = map { [ $_, $box_center ] } @{ $params->{outliers} }; - $self->add_dataset(@points, linestyle => 'none', marks => $options{outlier_mark}, marksize => 3); + if (ref $params->{outliers} eq 'ARRAY') { + $self->add_dataset( + (map { [ $_, $box_center ] } @{ $params->{outliers} }), + linestyle => 'none', + marks => $options{outlier_mark}, + marksize => 3 + ); } } elsif ($orientation eq 'vertical') { - my $box_center = $options{box_center} // 0.5 * $self->axes->xaxis->{max}; my $box_width = $options{box_width} // 0.5 * $self->axes->xaxis->{max}; $self->add_rectangle([ $box_center - 0.5 * $box_width, $params->{q1} ], [ $box_center + 0.5 * $box_width, $params->{q3} ], %options); - $self->add_dataset([ $box_center, $params->{min} ], [ $box_center, $params->{q1} ], %options); - $self->add_dataset([ $box_center, $params->{q3} ], [ $box_center, $params->{max}, ], %options); - $self->add_dataset([ $box_center - 0.5 * $box_width, $params->{median} ], - [ $box_center + 0.5 * $box_width, $params->{median} ], %options); - - if ($params->{outliers}) { - my @points = map { [ $box_center, $_ ] } @{ $params->{outliers} }; - $self->add_dataset(@points, linestyle => 'none', marks => $options{outlier_mark}, marksize => 3); - } + $self->add_dataset( + [ [ $box_center, $params->{min} ], [ $box_center, $params->{q1} ], %options ], + [ [ $box_center, $params->{q3} ], [ $box_center, $params->{max}, ], %options ], + [ + [ $box_center - 0.5 * $box_width, $params->{median} ], + [ $box_center + 0.5 * $box_width, $params->{median} ], + %options + ] + ); - # add whisker caps if ($options{whisker_cap}) { - $self->add_dataset([ $box_center - 0.5 * $options{cap_width} * $box_width, $params->{max} ], - [ $box_center + 0.5 * $options{cap_width} * $box_width, $params->{max}, ], %options); - $self->add_dataset([ $box_center - 0.5 * $options{cap_width} * $box_width, $params->{min} ], - [ $box_center + 0.5 * $options{cap_width} * $box_width, $params->{min} ], %options); + $self->add_dataset( + [ + [ $box_center - 0.5 * $options{cap_width} * $box_width, $params->{max} ], + [ $box_center + 0.5 * $options{cap_width} * $box_width, $params->{max}, ], + %options + ], + [ + [ $box_center - 0.5 * $options{cap_width} * $box_width, $params->{min} ], + [ $box_center + 0.5 * $options{cap_width} * $box_width, $params->{min} ], + %options + ] + ); + } + + if (ref $params->{outliers} eq 'ARRAY') { + $self->add_dataset( + (map { [ $box_center, $_ ] } @{ $params->{outliers} }), + linestyle => 'none', + marks => $options{outlier_mark}, + marksize => 3 + ); } } } sub add_scatterplot { - my ($self, $data, %opts) = @_; + my ($self, $data, %options) = @_; - my %options = ( - linestyle => 'none', - marks => 'circle', - mark_size => 3, - plot_option_aliases(%opts) - ); + $options{linestyle} //= 'none'; + $options{marks} //= 'circle'; + $options{mark_size} //= 3; + set_plot_option_aliases(\%options); $self->add_dataset(@$data, %options); } sub add_piechart { - my ($self, $data, %opts) = @_; - - my %options = ( - center => [ 0, 0 ], - radius => 4, - angle_offset => 0, - inner_radius => 0, - plot_option_aliases(%opts) - ); + my ($self, $data, %options) = @_; + + $options{center} //= [ 0, 0 ]; + $options{radius} //= 4; + $options{angle_offset} //= 0; + $options{inner_radius} //= 0; + set_plot_option_aliases(\%options); Value::Error('The number of labels must equal the number of sectors in the pie chart') - unless defined($options{labels}) && scalar(@$data) == scalar(@{ $options{labels} }); + unless (defined $options{labels} && @$data == @{ $options{labels} }); my $fill_colors = ref $options{fill_colors} eq 'HASH' @@ -733,7 +758,7 @@ sub add_piechart { my $pi = 4 * atan2(1, 1); my $total = 0; - $total += $_ for (@$data); + $total += $_ for @$data; my $theta = $options{angle_offset} * $pi / 180; # first angle of the sector for (0 .. $#$data) { @@ -741,14 +766,14 @@ sub add_piechart { $self->add_multipath( [ [ - "$options{center}->[0] + $options{radius} * cos(t)", - "$options{center}->[1] + $options{radius} * sin(t)", + "$options{center}[0] + $options{radius} * cos(t)", + "$options{center}[1] + $options{radius} * sin(t)", $theta, $theta + $delta_theta ], [ - "$options{center}->[0] + $options{inner_radius} * cos(t)", - "$options{center}->[1] + $options{inner_radius} * sin(t)", + "$options{center}[0] + $options{inner_radius} * cos(t)", + "$options{center}[1] + $options{inner_radius} * sin(t)", $theta + $delta_theta, $theta ], @@ -756,23 +781,17 @@ sub add_piechart { 't', cycle => 1, fill => 'self', - fill_color => $fill_colors->[ $_ % scalar(@$fill_colors) ], + fill_color => $fill_colors->[ $_ % @$fill_colors ], %options ); # add the labels if defined if ($options{labels}) { my $alpha = $theta + 0.5 * $delta_theta; - # take $alpha mod 2pi - $alpha = $alpha - (2 * $pi * int($alpha / (2 * $pi))); $self->add_label( - 1.1 * $options{radius} * cos($alpha), - 1.1 * $options{radius} * sin($alpha), - $options{labels}->[$_], - (0 <= $alpha && $alpha < $pi / 4) - || (7 * $pi / 4 < $alpha && $alpha < 2 * $pi) ? (h_align => 'left') : $pi / 4 <= $alpha - && $alpha < 3 * $pi / 4 ? (v_align => 'bottom') : 3 * $pi / 4 <= $alpha - && $alpha < 5 * $pi / 4 ? (h_align => 'right') : (v_align => 'top') + $options{radius} * cos($alpha), $options{radius} * sin($alpha), $options{labels}[$_], + anchor => 180 * (1 + $alpha / $pi), + padding => 15 ); } $theta += $delta_theta; @@ -780,83 +799,74 @@ sub add_piechart { } -# This provides some alias for options. -# For additional aliases, add to the %aliases hash below. - -sub plot_option_aliases { - my (%options) = @_; - - my %aliases = ( - width => 'stroke_width', - color => 'stroke_color', - color => 'mark_color' - ); - +sub set_plot_option_aliases { + my $options = shift; + my %aliases = (stroke_width => 'width', stroke_color => 'color', mark_color => 'color'); for (keys %aliases) { - $options{$_} = $options{ $aliases{$_} } if $options{ $aliases{$_} }; - delete $options{ $aliases{$_} }; + $options->{ $aliases{$_} } = delete $options->{$_} if defined $options->{$_}; } - return %options; + return %$options; } sub color_palette { my ($palette_name, $num_colors) = @_; - $palette_name = 'rainbow' unless defined($palette_name); + $palette_name //= 'rainbow'; if ($palette_name eq 'rainbow') { - return [ 'violet', 'blue', 'green', 'yellow', 'orange', 'red' ]; + return [ 'Violet', 'blue', 'green', 'yellow', 'orange', 'red' ]; } elsif ($palette_name eq 'greens') { - return [ 'green', 'Olive', 'DarkGreen', 'LawnGreen', 'MediumAquaMarine', 'LimeGreen' ]; + return [ 'Green', 'Olive', 'DarkGreen', 'LawnGreen', 'MediumAquaMarine', 'LimeGreen' ]; } elsif ($palette_name eq 'blues') { - return [ 'blue', 'MidnightBlue', 'MediumBlue', 'LightSkyBlue', 'DodgerBlue', 'DarkBlue', 'CornflowerBlue' ]; + return [ 'Blue', 'MidnightBlue', 'MediumBlue', 'LightSkyBlue', 'DodgerBlue', 'DarkBlue', 'CornflowerBlue' ]; } elsif ($palette_name eq 'reds') { - return [ 'red', 'Crimson', 'DarkRed', 'FireBrick', 'IndianRed', 'Maroon', 'Tomato' ]; + return [ 'Red', 'Crimson', 'DarkRed', 'FireBrick', 'IndianRed', 'Maroon', 'Tomato' ]; } elsif ($palette_name eq 'random') { - my @all_colors = ( - 'AliceBlue', 'AntiqueWhite', 'Aqua', 'Aquamarine', - 'Azure', 'Beige', 'Bisque', 'Black', - 'BlanchedAlmond', 'Blue', 'BlueViolet', 'Brown', - 'BurlyWood', 'CadetBlue', 'Chartreuse', 'Chocolate', - 'Coral', 'CornflowerBlue', 'Cornsilk', 'Crimson', - 'Cyan', 'DarkBlue', 'DarkCyan', 'DarkGoldenrod', - 'DarkGray', 'DarkGreen', 'DarkGrey', 'DarkKhaki', - 'DarkMagenta', 'DarkOliveGreen', 'DarkOrange', 'DarkOrchid', - 'DarkRed', 'DarkSalmon', 'DarkSeaGreen', 'DarkSlateBlue', - 'DarkSlateGray', 'DarkSlateGrey', 'DarkTurquoise', 'DarkViolet', - 'DeepPink', 'DeepSkyBlue', 'DimGray', 'DimGrey', - 'DodgerBlue', 'FireBrick', 'FloralWhite', 'ForestGreen', - 'Fuchsia', 'Gainsboro', 'GhostWhite', 'Gold', - 'Goldenrod', 'Gray', 'Green', 'GreenYellow', - 'Grey', 'Honeydew', 'HotPink', 'IndianRed', - 'Indigo', 'Ivory', 'Khaki', 'Lavender', - 'LavenderBlush', 'LawnGreen', 'LemonChiffon', 'LightBlue', - 'LightCoral', 'LightCyan', 'LightGoldenrodYellow', 'LightGray', - 'LightGreen', 'LightGrey', 'LightPink', 'LightSalmon', - 'LightSeaGreen', 'LightSkyBlue', 'LightSlateGray', 'LightSlateGrey', - 'LightSteelBlue', 'LightYellow', 'Lime', 'LimeGreen', - 'Linen', 'Magenta', 'Maroon', 'MediumAquamarine', - 'MediumBlue', 'MediumOrchid', 'MediumPurple', 'MediumSeaGreen', - 'MediumSlateBlue', 'MediumSpringGreen', 'MediumTurquoise', 'MediumVioletRed', - 'MidnightBlue', 'MintCream', 'MistyRose', 'Moccasin', - 'NavajoWhite', 'Navy', 'OldLace', 'Olive', - 'OliveDrab', 'Orange', 'OrangeRed', 'Orchid', - 'PaleGoldenrod', 'PaleGreen', 'PaleTurquoise', 'PaleVioletRed', - 'PapayaWhip', 'PeachPuff', 'Peru', 'Pink', - 'Plum', 'PowderBlue', 'Purple', 'RebeccaPurple', - 'Red', 'RosyBrown', 'RoyalBlue', 'SaddleBrown', - 'Salmon', 'SandyBrown', 'SeaGreen', 'Seashell', - 'Sienna', 'Silver', 'SkyBlue', 'SlateBlue', - 'SlateGray', 'SlateGrey', 'Snow', 'SpringGreen', - 'SteelBlue', 'Tan', 'Teal', 'Thistle', - 'Tomato', 'Turquoise', 'Violet', 'Wheat', - 'White', 'WhiteSmoke', 'Yellow', 'YellowGreen' - ); - - $num_colors = 10 unless defined($num_colors); - return [ map { $all_colors[$_] } main::random_subset($num_colors, 0 .. $#all_colors) ]; + return [ + main::random_subset( + $num_colors // 10, + ( + 'AliceBlue', 'AntiqueWhite', 'Aqua', 'Aquamarine', + 'Azure', 'Beige', 'Bisque', 'Black', + 'BlanchedAlmond', 'Blue', 'BlueViolet', 'Brown', + 'BurlyWood', 'CadetBlue', 'Chartreuse', 'Chocolate', + 'Coral', 'CornflowerBlue', 'Cornsilk', 'Crimson', + 'Cyan', 'DarkBlue', 'DarkCyan', 'DarkGoldenrod', + 'DarkGray', 'DarkGreen', 'DarkGrey', 'DarkKhaki', + 'DarkMagenta', 'DarkOliveGreen', 'DarkOrange', 'DarkOrchid', + 'DarkRed', 'DarkSalmon', 'DarkSeaGreen', 'DarkSlateBlue', + 'DarkSlateGray', 'DarkSlateGrey', 'DarkTurquoise', 'DarkViolet', + 'DeepPink', 'DeepSkyBlue', 'DimGray', 'DimGrey', + 'DodgerBlue', 'FireBrick', 'FloralWhite', 'ForestGreen', + 'Fuchsia', 'Gainsboro', 'GhostWhite', 'Gold', + 'Goldenrod', 'Gray', 'Green', 'GreenYellow', + 'Grey', 'Honeydew', 'HotPink', 'IndianRed', + 'Indigo', 'Ivory', 'Khaki', 'Lavender', + 'LavenderBlush', 'LawnGreen', 'LemonChiffon', 'LightBlue', + 'LightCoral', 'LightCyan', 'LightGoldenrodYellow', 'LightGray', + 'LightGreen', 'LightGrey', 'LightPink', 'LightSalmon', + 'LightSeaGreen', 'LightSkyBlue', 'LightSlateGray', 'LightSlateGrey', + 'LightSteelBlue', 'LightYellow', 'Lime', 'LimeGreen', + 'Linen', 'Magenta', 'Maroon', 'MediumAquamarine', + 'MediumBlue', 'MediumOrchid', 'MediumPurple', 'MediumSeaGreen', + 'MediumSlateBlue', 'MediumSpringGreen', 'MediumTurquoise', 'MediumVioletRed', + 'MidnightBlue', 'MintCream', 'MistyRose', 'Moccasin', + 'NavajoWhite', 'Navy', 'OldLace', 'Olive', + 'OliveDrab', 'Orange', 'OrangeRed', 'Orchid', + 'PaleGoldenrod', 'PaleGreen', 'PaleTurquoise', 'PaleVioletRed', + 'PapayaWhip', 'PeachPuff', 'Peru', 'Pink', + 'Plum', 'PowderBlue', 'Purple', 'RebeccaPurple', + 'Red', 'RosyBrown', 'RoyalBlue', 'SaddleBrown', + 'Salmon', 'SandyBrown', 'SeaGreen', 'Seashell', + 'Sienna', 'Silver', 'SkyBlue', 'SlateBlue', + 'SlateGray', 'SlateGrey', 'Snow', 'SpringGreen', + 'SteelBlue', 'Tan', 'Teal', 'Thistle', + 'Tomato', 'Turquoise', 'Violet', 'Wheat', + 'White', 'WhiteSmoke', 'Yellow', 'YellowGreen' + ) + ) + ]; } - } 1; diff --git a/macros/graph/plots.pl b/macros/graph/plots.pl index 0d55b9491..7fa04ab0f 100644 --- a/macros/graph/plots.pl +++ b/macros/graph/plots.pl @@ -325,6 +325,8 @@ =head2 PLOT RECTANGLES ... ); +Note that if the C option is set, then C<< fill => 'self' >> is +automatically set, and so you do not need to specify that option. =head2 PLOT VECTOR FIELDS