From 7e786fd84b1432ab47f0936ad80292583207540a Mon Sep 17 00:00:00 2001 From: Peter Staab Date: Wed, 28 Jan 2026 20:19:02 -0500 Subject: [PATCH 1/6] The initial code for the beginning of the StatisticalPlots macro. This contains the methods add_barplot, add_histogram, add_boxplot and add_scatterplot. There are many options for each and there is documentation as well. This also includes the add_rectangle method to the plot.pl macro which is a wrapper for the add_dataset for creating rectangles. --- lib/Plots/Plot.pm | 12 + lib/Plots/StatPlot.pm | 38 +++ macros/core/PGbasicmacros.pl | 2 +- macros/graph/StatisticalPlots.pl | 507 +++++++++++++++++++++++++++++++ macros/graph/plots.pl | 25 +- 5 files changed, 579 insertions(+), 5 deletions(-) create mode 100644 lib/Plots/StatPlot.pm create mode 100644 macros/graph/StatisticalPlots.pl diff --git a/lib/Plots/Plot.pm b/lib/Plots/Plot.pm index 43f2f6455..2cf1b9cac 100644 --- a/lib/Plots/Plot.pm +++ b/lib/Plots/Plot.pm @@ -394,6 +394,18 @@ sub add_arc { return $self->_add_arc(@data); } +sub add_rectangle { + my ($self, $pt0, $pt2, %options) = @_; + + Value::Error('The first point must be an array ref of length 2') + unless ref($pt0) eq 'ARRAY' && scalar(@$pt0) == 2; + Value::Error('The second point must be an array ref of length 2') + unless ref($pt2) eq 'ARRAY' && scalar(@$pt2) == 2; + my $pt1 = [ $pt2->[0], $pt0->[1] ]; + my $pt3 = [ $pt0->[0], $pt2->[1] ]; + return $self->add_dataset($pt0, $pt1, $pt2, $pt3, $pt0, %options); +} + sub add_vectorfield { my ($self, @options) = @_; my $data = Plots::Data->new(name => 'vectorfield'); diff --git a/lib/Plots/StatPlot.pm b/lib/Plots/StatPlot.pm new file mode 100644 index 000000000..d211fd9e2 --- /dev/null +++ b/lib/Plots/StatPlot.pm @@ -0,0 +1,38 @@ + +=head1 DESCRIPTION + +This is the main C code for creating statistical plots. + +See L for more details. +=cut + +package Plots::StatPlot; + +use strict; +use warnings; + +use WeBWorK::Utils qw(min max); + +sub new { + my ($class, %options) = @_; + return Plots::Plot->new(%options); +} + +sub add_histogram { + my ($self, $data, %opts) = @_; + + my %options = ( + bins => 10, + %opts + ); + + my $min = min(@$data); + my $max = max(@$data); + my $bin_size = ($max - $min) / $options{bins}; + + my @counts; + $counts[ int(($_ - $min) / $bin_size) ]++ for (@$data); + +} + +1; diff --git a/macros/core/PGbasicmacros.pl b/macros/core/PGbasicmacros.pl index 6437d71e9..d1a44db70 100644 --- a/macros/core/PGbasicmacros.pl +++ b/macros/core/PGbasicmacros.pl @@ -2923,7 +2923,7 @@ sub image { ); next; } - if (ref $image_item eq 'Plots::Plot') { + if (ref $image_item eq 'Plots::Plot' || ref $image_item eq 'Plots::StatPlot') { # Update image attributes as needed. $image_item->{width} = $width if $out_options{width}; $image_item->{height} = $height if $out_options{height}; diff --git a/macros/graph/StatisticalPlots.pl b/macros/graph/StatisticalPlots.pl new file mode 100644 index 000000000..5f3ec1ac0 --- /dev/null +++ b/macros/graph/StatisticalPlots.pl @@ -0,0 +1,507 @@ + +=head1 NAME + +StatisticalPlots.pl - A macro to create dynamic statistics 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 in either C or C format with the +default for the former to be used for hardcopy and the latter for HTML output. + +The statistical plot available are + +=over + +=item Box Plots + +=item Bar Plots + +=item Histograms + +=item Scatter Plots + +=back + +=head2 USAGE + +First, start with a C object as in + + loadMacros('StatisticsPlots.pl'); + $stat_plot = StatPlot( + xmin => -1, + xmax => 8, + ymin => -1.5, + 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 a C or C. + +After the C is created then specific plots are added to the axes. For example: + + @y = (3, 6, 7, 8, 4, 1); + $hist->add_barplot( + [ 1 .. 6 ], ~~@y, + fill_color => 'yellow', + width => 1, + bar_width => 0.9 + ); + +will add a barplot to the axes with heights in the C<@y> variable at the x-locations C<(1..6)>. + +See below for more details about creating a barplot and its options. + +=head1 PLOT ELEMENTS + +As mentioned above, 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. + +=head2 BAR PLOTS + +A bar plot is added with the C method to a C. The general form for a +bar plot with vertical bars (the default) is + + $stat_plot->add_barplot($xdata, $ydata, %opts); + +where C<$xdata> is an ARRAYREF of x-values where the bars will be centered and C<$ydata> is an +ARRAY of heights of the bars. Note: if the option C<< orientation => 'horizontal' >> is included +then the bar lengths are the values in C<$xdata> and locations in C<$ydata>. + +The options for the C method are two fold. The following are specific to changing +the barplot, and the rest are passed along to C, which is a wrapper function for +C. + +The C option can take on C (default) or C to make vertical +or horizontal bars. Above was an example with vertical bars and an example with horizontal bars is + + @x = (3, 6, 7, 8, 4, 1); + $hist->add_barplot( + ~~@x, [ 1 .. 6 ], + orientation => 'horizontal', + fill_color => 'yellow', + width => 1, + bar_width => 0.9 + ); + +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. In the example above, with +C<< bar_width => 0.9 >>, there is a small gap between bars. + +Any remaining options are passed to C which has the same options as C, +however, if C is passed to C, then the C<< fill => 'self' >> is also +passed along. + +See L for specifics about other options to both changing fill and stroke +color. + +=head2 HISTOGRAMS + +A L is added with the `add_histogram` method to a C. The general form +is + + $stat_plot->add_histogram($data, %options); + +where C<$data> is an array ref of univariate data. The C<%options> include both options +for the histogram like number of bins as well as options for the bars. + +An example is performed using the C function from C which +produces normally distributed random variables. + + 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( + xmin => 0, + xmax => 65, + ymin => 0, + ymax => 12, + xtick_delta => 10, + ytick_delta => 2 + ); + $stat_plot->add_histogram( + ~~@data, + min => 10, + max => 60, + bins => 10, + 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. + +=over + +=item min + +The left edge of the leftmost box. If not defined, the minimum of C<$data> is used. + +=item max + +The right edge of the rightmost box. If not defined, the maximum of 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. + +=item normalize + +If the value of 0 (default) is used, the height of the bars is the count of the number +of points. If the value is 1, then the heights are scaled so the total height of the +bars is 1. + +=back + +The rest of the options are passed through to the C 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 + + $stat_plot->add_boxplot($data, %options); + +or if one has multiple box plots + + $stat_plot->add_boxplot([$data1, $data2, ...], %options); + +where C<$data> 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 arrayref of +univariate data is + + @data = urand(100,25,75,6); + + $boxplot = StatPlot( + xmin => 0, + xmax => 200, + xtick_delta => 25, + show_grid => 0, + ymin => -5, + ymax => 25, + yvisible => 0, + aspect_ratio => 4, + rounded_corners => 1 + ); + + $boxplot->add_boxplot(~~@data, fill_color => 'lightblue', width => 1); + +and as with other methods in this macro, one can pass options to the characteristic of the +box plot (like fill color or stroke color and width) within the C method. + +If C<$data> is a hashref, 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 + + $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] + }; + + $boxplot = StatPlot( + xmin => 100, + xmax => 400, + xtick_delta => 50, + show_grid => 0, + ymin => -5, + ymax => 25, + yvisible => 0, + aspect_ratio => 4 + ); + + $boxplot->add_boxplot($params); + +=head3 Options + +The following are options to the C method. + +=over + +=item orientation + +This is the direction of the box plot and can take on values 'horizontal' (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. + +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 +arrayref of values (in the x-direction for vertical plots and y-direction for horizontal). + + box_center => [3,6,9] + +as an example. + +=item box_width + +The width of the box in the direction perpendicular to the orientation. If not define, it +will take the value of 0.5 times the space between the axis and the edge of the plot. + +If multiple box plots are defined, this should only be a single value. + +=back + +As with other methods in the macro, other options can be passed along to C +and C which are used in the macro. + +Also, if C is included, then C<< fill => 'self' >> is automatically added on the +box. + +=head2 SCATTER PLOTS + +To produce a scatter plot, use the C method to a C. The general +form is + + $plot->add_scatterplot($data, %options); + +where the dataset in C<$data> is an array ref of C pairs as an array ref. For example, + + $stat_plot = StatPlot( + xmin => -1, + xmax => 15, + xtick_delta => 5, + ymin => -1, + ymax => 15, + 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'); + +This method is simply a wrapper for the C method where the defaults are different. Specifically + +=over + +=item linestyle + +The C option is set to 'none', so that lines are not drawn between the points. + +=item marks + +The C is default to 'circle'. See L for other mark options. + +=item mark_size + +The C is default to 3. + +=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 + +=cut + +BEGIN { strict->import; } + +sub _StatisticalPlots_init { + main::PG_restricted_eval('sub StatPlot { Plots::StatPlot->new(@_); }'); +} + +loadMacros('PGstatisticsmacros.pl'); + +package Plots::StatPlot; +our @ISA = qw(Plots::Plot); + +sub new { + my $self = shift; + my $class = ref($self) || $self; + + return $class->SUPER::new(@_); +} + +sub add_histogram { + my ($self, $data, %opts) = @_; + + my %options = ( + bins => 10, + %opts + ); + + Value::Error("The option 'bins' must be a positive integer") + unless $options{bins} =~ /^\d+$/ && $options{bins} > 0; + + # if the bin_width is 0, set the num_bins to 1 and give a non-zero bin_width. + + my @counts; + my $min = $options{min} // main::min(@$data); + my $max = $options{max} // main::max(@$data); + my $bin_width = ($max - $min) / $options{bins}; + + $counts[ int(($_ - $min) / $bin_width) ]++ for (@$data); + 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'); + + if ($options{orientation} eq 'vertical') { + $self->add_barplot(\@xdata, \@counts, %options); + } else { + $self->add_barplot(\@counts, \@xdata, %options); + } + + return \@counts; +} + +# 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 %options = ( + bar_width => 1, + orientation => 'vertical', + %opts + ); + + Value::Error('The lengths of the data in the first two arguments must be arrayrefs of the same length') + unless ref $xdata eq 'ARRAY' && ref $xdata eq 'ARRAY' && scalar(@$xdata) == scalar(@$ydata); + + # assume that the $xdata is equally spaced. TODO: should we handle arbitrary spaced bars? + 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); + } + } +} + +sub add_boxplot { + my ($self, $data, %opts) = @_; + + my %options = ( + orientation => 'horizontal', + %opts + ); + + # Placeholder for boxplot implementation. + if (ref $data eq 'ARRAY' && (ref $data->[0] eq 'ARRAY' || ref $data->[0] eq 'HASH')) { + my ($box_centers, $box_width); + 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}; + } 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); + } + $box_centers = [ map { 2 * $options{box_width} * $_ } (1 .. $n + 1) ]; + } + for (0 .. $#$data) { + $options{box_center} = $box_centers->[$_]; + $self->_add_boxplot($data->[$_], %options); + } + + } else { + $self->_add_boxplot($data, %options); + } +} + +sub _add_boxplot { + my ($self, $data, %options) = @_; + + my $orientation = $options{orientation} // 'horizontal'; + my $params; + if (ref $data eq 'ARRAY') { + my @five_point = main::five_point_summary(@$data); + $params = { + min => $five_point[0], + q1 => $five_point[1], + median => $five_point[2], + q3 => $five_point[3], + 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) { + # warn "$_: $count{$_}"; + Value::Error("The parameter $_ is missing from the boxplot attributes.") if $count{$_} > 0; + } + $params = $data; + } + # warn "$_: $options{$_}" for (keys %options);s + + # 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); + + if ($params->{outliers}) { + my @points = map { [ $_, $box_center ] } @{ $params->{outliers} }; + $self->add_dataset(@points, linestyle => 'none', marks => 'plus', 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); + } +} + +sub add_scatterplot { + my ($self, $data, %opts) = @_; + + my %options = ( + linestyle => 'none', + marks => 'circle', + mark_size => 3, + %opts + ); + + $self->add_dataset(@$data, %options); + +} + +1; diff --git a/macros/graph/plots.pl b/macros/graph/plots.pl index 5bb9288d4..91d980980 100644 --- a/macros/graph/plots.pl +++ b/macros/graph/plots.pl @@ -16,7 +16,7 @@ =head1 DESCRIPTION =head1 USAGE -First create a Plots object: +First create a Plot object: loadMacros('plots.pl'); $plot = Plot( @@ -304,13 +304,30 @@ =head2 PLOT ARCS the direction of the third point. Arcs always go in the counter clockwise direction. - $plot->add_arc([$start_x, $start_y], [$center_x, $center_y], [$end_x, $end_y], %options); + $plot->add_arc([$center_x, $center_y], [$start_x, $start_y], [$end_x, $end_y], %options); $plot->add_arc( [[$center_x1, $center_y1], [$start_x1, $start_y1], [$end_x1, $end_y1], %options1], [[$center_x2, $center_y2], [$start_x2, $start_y2], [$end_x2, $end_y2], %options2], ... ); +=head2 PLOT RECTANGLES + +A rectangle can be plotted with the C<< $plot->add_rectangle >> method. This is a +convenience method as a shortcut for the C<< $plot->add_dataset >> method. The first +two arguments are opposite corners of the rectangle. All other arguments are passed +as options to the C<< add_dataset >> method. + +The following makes a filled rectangle with a thicker blue border. + + $plot->add_rectangle([2,1], [6,3], + color => 'blue', + width => 1.5, + fill => 'self', + fill_color => 'yellow', + fill_opacity => 0.1, + ); + =head2 PLOT VECTOR FIELDS Vector fields and slope fields can be plotted using the C<< $plot->add_vectorfield >> method. @@ -592,7 +609,7 @@ =head2 DATASET OPTIONS =item tikz_options -Additional pgfplots C<\addplot> options to be appeneded to the tikz output. +Additional pgfplots C<\addplot> options to be appended to the tikz output. =back @@ -709,7 +726,7 @@ =head2 STAMPS # Add a single stamp. $plot->add_stamp($x1, $y1, symbol => $symbol, color => $color, radius => $radius); - # Add Multple stamps. + # Add Multiple stamps. $plot->add_stamp( [$x1, $y1, symbol => $symbol1, color => $color1, radius => $radius1], [$x2, $y2, symbol => $symbol2, color => $color2, radius => $radius2], From 85ebaebb0f4ee8279fc47dc37e81243b40549af1 Mon Sep 17 00:00:00 2001 From: Peter Staab Date: Sat, 31 Jan 2026 08:16:47 -0500 Subject: [PATCH 2/6] Add normalize functionality to add_histogram. Cleanup of the POD. Removal of Plots::StatsPlot->new function. It wasn't needed. Make sure Plots::StatsPlot objects are rendered in PGbasicmacros.pl. --- lib/Plots/Plot.pm | 4 +-- lib/Plots/StatPlot.pm | 38 ---------------------- macros/core/PGbasicmacros.pl | 5 +-- macros/graph/StatisticalPlots.pl | 56 +++++++++++++++++++------------- macros/graph/plots.pl | 2 +- 5 files changed, 37 insertions(+), 68 deletions(-) delete mode 100644 lib/Plots/StatPlot.pm diff --git a/lib/Plots/Plot.pm b/lib/Plots/Plot.pm index 2cf1b9cac..3c18a01ab 100644 --- a/lib/Plots/Plot.pm +++ b/lib/Plots/Plot.pm @@ -401,9 +401,7 @@ sub add_rectangle { unless ref($pt0) eq 'ARRAY' && scalar(@$pt0) == 2; Value::Error('The second point must be an array ref of length 2') unless ref($pt2) eq 'ARRAY' && scalar(@$pt2) == 2; - my $pt1 = [ $pt2->[0], $pt0->[1] ]; - my $pt3 = [ $pt0->[0], $pt2->[1] ]; - return $self->add_dataset($pt0, $pt1, $pt2, $pt3, $pt0, %options); + return $self->add_dataset($pt0, [ $pt2->[0], $pt0->[1] ], $pt2, [ $pt0->[0], $pt2->[1] ], $pt0, %options); } sub add_vectorfield { diff --git a/lib/Plots/StatPlot.pm b/lib/Plots/StatPlot.pm deleted file mode 100644 index d211fd9e2..000000000 --- a/lib/Plots/StatPlot.pm +++ /dev/null @@ -1,38 +0,0 @@ - -=head1 DESCRIPTION - -This is the main C code for creating statistical plots. - -See L for more details. -=cut - -package Plots::StatPlot; - -use strict; -use warnings; - -use WeBWorK::Utils qw(min max); - -sub new { - my ($class, %options) = @_; - return Plots::Plot->new(%options); -} - -sub add_histogram { - my ($self, $data, %opts) = @_; - - my %options = ( - bins => 10, - %opts - ); - - my $min = min(@$data); - my $max = max(@$data); - my $bin_size = ($max - $min) / $options{bins}; - - my @counts; - $counts[ int(($_ - $min) / $bin_size) ]++ for (@$data); - -} - -1; diff --git a/macros/core/PGbasicmacros.pl b/macros/core/PGbasicmacros.pl index d1a44db70..6ea5862b6 100644 --- a/macros/core/PGbasicmacros.pl +++ b/macros/core/PGbasicmacros.pl @@ -2942,10 +2942,7 @@ sub image { $width_ratio = 0.001 * $image_item->{tex_size}; } $image_item = insertGraph($image_item) - if (ref $image_item eq 'WWPlot' - || ref $image_item eq 'Plots::Plot' - || ref $image_item eq 'PGlateximage' - || ref $image_item eq 'PGtikz'); + if (grep { ref $image_item eq $_ } ('WWPlot', 'Plots::Plot', 'Plots::StatPlot', 'PGlateximage', 'PGtikz')); my $imageURL = alias($image_item) // ''; $imageURL = ($envir{use_site_prefix}) ? $envir{use_site_prefix} . $imageURL : $imageURL; my $id = $main::PG->getUniqueName('img'); diff --git a/macros/graph/StatisticalPlots.pl b/macros/graph/StatisticalPlots.pl index 5f3ec1ac0..89b78b1cb 100644 --- a/macros/graph/StatisticalPlots.pl +++ b/macros/graph/StatisticalPlots.pl @@ -9,7 +9,7 @@ =head1 DESCRIPTION This is based on L which will draw in either C or C format with the default for the former to be used for hardcopy and the latter for HTML output. -The statistical plot available are +The statistical plots available are =over @@ -73,10 +73,16 @@ =head2 BAR PLOTS ARRAY of heights of the bars. Note: if the option C<< orientation => 'horizontal' >> is included then the bar lengths are the values in C<$xdata> and locations in C<$ydata>. +=head3 OPTIONS + The options for the C method are two fold. The following are specific to changing the barplot, and the rest are passed along to C, which is a wrapper function for C. +=over + +=item orientation + The C option can take on C (default) or C to make vertical or horizontal bars. Above was an example with vertical bars and an example with horizontal bars is @@ -89,21 +95,25 @@ =head2 BAR PLOTS bar_width => 0.9 ); +=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. In the example above, with C<< bar_width => 0.9 >>, there is a small gap between bars. +=back + Any remaining options are passed to C which has the same options as C, however, if C is passed to C, then the C<< fill => 'self' >> is also passed along. -See L for specifics about other options to both changing fill and stroke -color. +See L for specifics about other options to +both changing fill and stroke color. =head2 HISTOGRAMS -A L is added with the `add_histogram` method to a C. The general form -is +A L is added with the `add_histogram` method +to a C. The general form is $stat_plot->add_histogram($data, %options); @@ -167,7 +177,8 @@ =head3 Options =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. +If one performs $stat_plot->add_boxplot($data, %options); @@ -201,8 +212,8 @@ =head2 BOX PLOTS box plot (like fill color or stroke color and width) within the C method. If C<$data> is a hashref, 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. +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 @@ -289,7 +300,7 @@ =head2 SCATTER PLOTS $stat_plot->add_scatterplot($data, marks => 'diamond', mark_size => 5, color => 'orange'); -This method is simply a wrapper for the C method where the defaults are different. Specifically +This method is simply a wrapper for the C method where the defaults are different. =over @@ -299,7 +310,8 @@ =head2 SCATTER PLOTS =item marks -The C is default to 'circle'. See L for other mark options. +The C is default to 'circle'. See L +for other mark options. =item mark_size @@ -324,36 +336,36 @@ sub _StatisticalPlots_init { package Plots::StatPlot; our @ISA = qw(Plots::Plot); -sub new { - my $self = shift; - my $class = ref($self) || $self; - - return $class->SUPER::new(@_); -} - sub add_histogram { my ($self, $data, %opts) = @_; my %options = ( - bins => 10, + bins => 10, + normalize => 0, + orientation => 'vertical', %opts ); Value::Error("The option 'bins' must be a positive integer") unless $options{bins} =~ /^\d+$/ && $options{bins} > 0; - # if the bin_width is 0, set the num_bins to 1 and give a non-zero bin_width. - - my @counts; + 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}; + # TODO: if the bin_width is 0, set the num_bins to 1 and give a non-zero bin_width. + $counts[ int(($_ - $min) / $bin_width) ]++ for (@$data); + if ($options{normalize}) { + my $total = 0; + $total += $_ for (@counts); + @counts = map { $_ / $total } @counts; + } 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'); + delete $options{$_} for ('min', 'max', 'bins', 'normalize'); if ($options{orientation} eq 'vertical') { $self->add_barplot(\@xdata, \@counts, %options); diff --git a/macros/graph/plots.pl b/macros/graph/plots.pl index 91d980980..1d4e6ec42 100644 --- a/macros/graph/plots.pl +++ b/macros/graph/plots.pl @@ -5,7 +5,7 @@ =head1 NAME =head1 DESCRIPTION -This macro creates a Plots object that is used to add data of different +This macro creates a Plot object that is used to add data of different elements of a 2D plot, then draw the plot. The plots can be drawn using different formats. Currently C (using PGFplots) and C graphics format are available. The default is to use C for HTML output and C for From 063c60dc4d61e68877e686e350532aea5381a83b Mon Sep 17 00:00:00 2001 From: Peter Staab Date: Mon, 2 Feb 2026 17:34:29 -0500 Subject: [PATCH 3/6] Adds some other functionality to StatisticalPlots.pl - Ability to change the outlier marks in a boxplot - Adds a whisker cap option to a boxplot. - Adds a cap_width option for these whiskers. - Adds custom tick labels for the x-axis. --- htdocs/js/Plots/plots.js | 10 ++++++- lib/Plots/Axes.pm | 1 + lib/Plots/JSXGraph.pm | 28 +++++++++++++------- lib/Plots/Plot.pm | 2 ++ macros/graph/StatisticalPlots.pl | 45 +++++++++++++++++++++++++++++--- 5 files changed, 71 insertions(+), 15 deletions(-) diff --git a/htdocs/js/Plots/plots.js b/htdocs/js/Plots/plots.js index 8dbe389ae..048871458 100644 --- a/htdocs/js/Plots/plots.js +++ b/htdocs/js/Plots/plots.js @@ -387,9 +387,17 @@ const PGplots = { options.xAxis.overrideOptions ?? {} ) )); - xAxis.defaultTicks.generateLabelText = plot.generateLabelText; + xAxis.defaultTicks.formatLabelText = plot.formatLabelText; + if (options.xAxis.ticks?.customLabels) { + xAxis.defaultTicks.generateLabelText = function (tick) { + return options.xAxis.ticks.customLabels[tick.usrCoords[1]/options.xAxis.ticks.distance-1]; + } + } else { + xAxis.defaultTicks.generateLabelText = plot.generateLabelText; + } + if (options.xAxis.location !== 'middle' && options.xAxis.name !== '') { plot.xLabel = board.create( 'text', diff --git a/lib/Plots/Axes.pm b/lib/Plots/Axes.pm index 40d10573c..2d9de6be9 100644 --- a/lib/Plots/Axes.pm +++ b/lib/Plots/Axes.pm @@ -350,6 +350,7 @@ sub axis_defaults { tick_labels => 1, tick_label_format => 'decimal', tick_label_digits => 2, + tick_label_custom => undef, # NEW: Array of custom labels tick_distance => 0, tick_scale => 1, tick_scale_symbol => '', diff --git a/lib/Plots/JSXGraph.pm b/lib/Plots/JSXGraph.pm index 4716b4eee..6b194350a 100644 --- a/lib/Plots/JSXGraph.pm +++ b/lib/Plots/JSXGraph.pm @@ -128,21 +128,29 @@ sub HTML { $options->{mathJaxTickLabels} = $axes->style('mathjax_tick_labels') if $xvisible || $yvisible; if ($xvisible) { - $options->{xAxis}{name} = $axes->xaxis('label'); - $options->{xAxis}{ticks}{show} = $axes->xaxis('show_ticks'); - $options->{xAxis}{ticks}{labels} = $axes->xaxis('tick_labels'); - $options->{xAxis}{ticks}{labelFormat} = $axes->xaxis('tick_label_format'); - $options->{xAxis}{ticks}{labelDigits} = $axes->xaxis('tick_label_digits'); + $options->{xAxis}{name} = $axes->xaxis('label'); + $options->{xAxis}{ticks}{show} = $axes->xaxis('show_ticks'); + $options->{xAxis}{ticks}{labels} = $axes->xaxis('tick_labels'); + if ($axes->xaxis('tick_label_custom')) { + $options->{xAxis}{ticks}{customLabels} = $axes->xaxis('tick_label_custom'); + } else { + $options->{xAxis}{ticks}{labelFormat} = $axes->xaxis('tick_label_format'); + $options->{xAxis}{ticks}{labelDigits} = $axes->xaxis('tick_label_digits'); + } $options->{xAxis}{ticks}{scaleSymbol} = $axes->xaxis('tick_scale_symbol'); $options->{xAxis}{arrowsBoth} = $axes->xaxis('arrows_both'); $options->{xAxis}{overrideOptions} = $axes->xaxis('jsx_options') if $axes->xaxis('jsx_options'); } if ($yvisible) { - $options->{yAxis}{name} = $axes->yaxis('label'); - $options->{yAxis}{ticks}{show} = $axes->yaxis('show_ticks'); - $options->{yAxis}{ticks}{labels} = $axes->yaxis('tick_labels'); - $options->{yAxis}{ticks}{labelFormat} = $axes->yaxis('tick_label_format'); - $options->{yAxis}{ticks}{labelDigits} = $axes->yaxis('tick_label_digits'); + $options->{yAxis}{name} = $axes->yaxis('label'); + $options->{yAxis}{ticks}{show} = $axes->yaxis('show_ticks'); + $options->{yAxis}{ticks}{labels} = $axes->yaxis('tick_labels'); + if ($axes->yaxis('tick_label_custom')) { + $options->{yAxis}{ticks}{customLabels} = $axes->yaxis('tick_label_custom'); + } else { + $options->{yAxis}{ticks}{labelFormat} = $axes->yaxis('tick_label_format'); + $options->{yAxis}{ticks}{labelDigits} = $axes->yaxis('tick_label_digits'); + } $options->{yAxis}{ticks}{scaleSymbol} = $axes->yaxis('tick_scale_symbol'); $options->{yAxis}{arrowsBoth} = $axes->yaxis('arrows_both'); $options->{yAxis}{overrideOptions} = $axes->yaxis('jsx_options') if $axes->yaxis('jsx_options'); diff --git a/lib/Plots/Plot.pm b/lib/Plots/Plot.pm index 3c18a01ab..01ef797e8 100644 --- a/lib/Plots/Plot.pm +++ b/lib/Plots/Plot.pm @@ -401,6 +401,8 @@ sub add_rectangle { unless ref($pt0) eq 'ARRAY' && scalar(@$pt0) == 2; Value::Error('The second point must be an array ref of length 2') unless ref($pt2) eq 'ARRAY' && scalar(@$pt2) == 2; + # If the fill_color option is set, set the fill to 'self'. + $options{fill} = 'self' if $options{fill_color} && !defined($options{fill}); return $self->add_dataset($pt0, [ $pt2->[0], $pt0->[1] ], $pt2, [ $pt0->[0], $pt2->[1] ], $pt0, %options); } diff --git a/macros/graph/StatisticalPlots.pl b/macros/graph/StatisticalPlots.pl index 89b78b1cb..146c2f7ef 100644 --- a/macros/graph/StatisticalPlots.pl +++ b/macros/graph/StatisticalPlots.pl @@ -270,6 +270,21 @@ =head3 Options If multiple box plots are defined, this should only be a single value. +=item whisker_cap + +Value of 0 (default) or 1. If 1, his will add a short line perpendicular to the whiskers +on the boxplot with relative size C + +=item cap_width + +The width of the cap as a fraction of the box height (if C<< orientation => 'vertical' >>) +or box width (if C<< orientation => 'horizontal' >>). 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. + =back As with other methods in the macro, other options can be passed along to C @@ -411,7 +426,10 @@ sub add_boxplot { my ($self, $data, %opts) = @_; my %options = ( - orientation => 'horizontal', + orientation => 'horizontal', + whisker_cap => 0, + cap_width => 0.2, + outlier_mark => 'plus', %opts ); @@ -463,12 +481,10 @@ sub _add_boxplot { $count{$_}++ for ('min', 'q1', 'median', 'q3', 'max'); $count{$_}-- for (keys %$data); for (keys %count) { - # warn "$_: $count{$_}"; Value::Error("The parameter $_ is missing from the boxplot attributes.") if $count{$_} > 0; } $params = $data; } - # warn "$_: $options{$_}" for (keys %options);s # if fill_color is passed as an option, set the 'fill' to 'self'. $options{fill} = 'self' if $options{fill_color}; @@ -484,9 +500,17 @@ sub _add_boxplot { $self->add_dataset([ $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); + } + if ($params->{outliers}) { my @points = map { [ $_, $box_center ] } @{ $params->{outliers} }; - $self->add_dataset(@points, linestyle => 'none', marks => 'plus', marksize => 3); + $self->add_dataset(@points, linestyle => 'none', marks => $options{outlier_mark}, marksize => 3); } } elsif ($orientation eq 'vertical') { @@ -499,6 +523,19 @@ sub _add_boxplot { $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); + } + + # 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); + } } } From b6648746930718bfe7e09b02be0aed8ac9042841 Mon Sep 17 00:00:00 2001 From: Peter Staab Date: Fri, 6 Feb 2026 10:53:38 -0500 Subject: [PATCH 4/6] Add pie chart. Add beginnings of some color palettes. Fix a typo in Data.pm --- htdocs/js/Plots/plots.js | 6 +- lib/Plots/Data.pm | 4 +- macros/graph/StatisticalPlots.pl | 239 +++++++++++++++++++++++++++++-- 3 files changed, 233 insertions(+), 16 deletions(-) diff --git a/htdocs/js/Plots/plots.js b/htdocs/js/Plots/plots.js index 048871458..e2f2105c2 100644 --- a/htdocs/js/Plots/plots.js +++ b/htdocs/js/Plots/plots.js @@ -387,13 +387,13 @@ const PGplots = { options.xAxis.overrideOptions ?? {} ) )); - + xAxis.defaultTicks.formatLabelText = plot.formatLabelText; if (options.xAxis.ticks?.customLabels) { xAxis.defaultTicks.generateLabelText = function (tick) { - return options.xAxis.ticks.customLabels[tick.usrCoords[1]/options.xAxis.ticks.distance-1]; - } + return options.xAxis.ticks.customLabels[tick.usrCoords[1] / options.xAxis.ticks.distance - 1]; + }; } else { xAxis.defaultTicks.generateLabelText = plot.generateLabelText; } diff --git a/lib/Plots/Data.pm b/lib/Plots/Data.pm index cdd1840d5..d40afdc50 100644 --- a/lib/Plots/Data.pm +++ b/lib/Plots/Data.pm @@ -66,7 +66,7 @@ stored in the C<< $data->{function} >> hash, though other data is stored as a st ); Note, the first argument must be $self->context when called from C -to use a single context for all C objects. +to use a single context for all C objects. This is also used to set a two variable function (used for slope or vector fields): @@ -116,7 +116,7 @@ Takes a MathObject C<$formula> and replaces the function with either a JavaScript or PGF function string. If the function contains any function tokens not supported, a warning and empty string is returned. - $formula The mathobject formula object, either $self->{function}{Fx} or $self->{function}{Fy}. + $formula The MathObject formula object, either $self->{function}{Fx} or $self->{function}{Fy}. $type 'js' or 'PGF' (falls back to js for any input except 'PGF'). $xvar The x-variable name, $self->{function}{xvar}. $yvar The y-variable name, $self->{function}{yvar}, for vector fields. diff --git a/macros/graph/StatisticalPlots.pl b/macros/graph/StatisticalPlots.pl index 146c2f7ef..253d45159 100644 --- a/macros/graph/StatisticalPlots.pl +++ b/macros/graph/StatisticalPlots.pl @@ -21,6 +21,8 @@ =head1 DESCRIPTION =item Scatter Plots +=item Pie Charts + =back =head2 USAGE @@ -69,7 +71,7 @@ =head2 BAR PLOTS $stat_plot->add_barplot($xdata, $ydata, %opts); -where C<$xdata> is an ARRAYREF of x-values where the bars will be centered and C<$ydata> is an +where C<$xdata> is an array reference of x-values where the bars will be centered and C<$ydata> is an ARRAY of heights of the bars. Note: if the option C<< orientation => 'horizontal' >> is included then the bar lengths are the values in C<$xdata> and locations in C<$ydata>. @@ -169,9 +171,19 @@ =head3 Options of points. If the value is 1, then the heights are scaled so the total height 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. + +=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. + =back -The rest of the options are passed through to the C method in which the +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. @@ -189,8 +201,8 @@ =head2 BOX PLOTS where C<$data> 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 arrayref of -univariate data is +function from C. An example of creating a boxplot with an array reference +of univariate data is @data = urand(100,25,75,6); @@ -211,7 +223,7 @@ =head2 BOX PLOTS and as with other methods in this macro, one can pass options to the characteristic of the box plot (like fill color or stroke color and width) within the C method. -If C<$data> is a hashref, it must contains the fields C that are used to +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. @@ -257,7 +269,7 @@ =head3 Options 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 -arrayref of values (in the x-direction for vertical plots and y-direction for horizontal). +array reference of values (in the x-direction for vertical plots and y-direction for horizontal). box_center => [3,6,9] @@ -285,9 +297,19 @@ =head3 Options The shape of the mark to use for outliers. Default is 'plus'. See 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. + +=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. + =back -As with other methods in the macro, other options can be passed along to C +As with other methods in the macro, other options can be passed along to L and C which are used in the macro. Also, if C is included, then C<< fill => 'self' >> is automatically added on the @@ -332,12 +354,108 @@ =head2 SCATTER PLOTS The C is default to 3. +=item mark_color + +This changes the mark color and is an alias for the C option. See L +for options to change the color. + =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 +=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 given each 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 is + + $stat_plot->add_piechart($data, %options); + +where $data is an array reference of values. + +The following are the options: + +=over + +=item center + +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. + +=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. + +=item angle_offset + +The first sector by default starts at angle 0 (from the positive horizontal axis) in degrees. Use +this to change this. + +=item color_palette + +This is either the name of a color palette or an array reference of colors for each of the +sectors in the pie chart. 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. + +=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. + +=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. + +=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: + + color_palette($name, num_colors => $n); + +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. + +=head3 PALETTE NAMES + +=over + +=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. + +=back + +=head2 LEGENDS + +A legend is helpful for some plots. + =cut BEGIN { strict->import; } @@ -399,10 +517,10 @@ sub add_barplot { my %options = ( bar_width => 1, orientation => 'vertical', - %opts + plot_option_aliases(%opts) ); - Value::Error('The lengths of the data in the first two arguments must be arrayrefs of the same length') + 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); # assume that the $xdata is equally spaced. TODO: should we handle arbitrary spaced bars? @@ -430,7 +548,7 @@ sub add_boxplot { whisker_cap => 0, cap_width => 0.2, outlier_mark => 'plus', - %opts + plot_option_aliases(%opts) ); # Placeholder for boxplot implementation. @@ -546,11 +664,110 @@ sub add_scatterplot { linestyle => 'none', marks => 'circle', mark_size => 3, - %opts + plot_option_aliases(%opts) ); $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) + ); + + 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} }); + + my $fill_colors = + (!defined $options{fill_colors} || ref $options{fill_colors} ne 'ARRAY') + ? color_palette($options{fill_colors}) + : $options{fill_colors}; + + my $pi = 4 * atan2(1, 1); + my $total = 0; + $total += $_ for (@$data); + + my $theta = $options{angle_offset} * $pi / 180; # first angle of the sector + for (0 .. $#$data) { + my $delta_theta = 2 * $pi * $data->[$_] / $total; + $self->add_multipath( + [ + [ + "$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)", + $theta + $delta_theta, + $theta + ], + ], + 't', + cycle => 1, + fill => 'self', + fill_color => $fill_colors->[ $_ % scalar(@$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 < 3 * $pi / 4 ? (v_align => 'bottom') + : 3 * $pi / 4 <= $alpha < 5 * $pi / 4 ? (h_align => 'right') + : (v_align => 'top') + ); + } + $theta += $delta_theta; + } + +} + +# 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' + ); + + for (keys %aliases) { + $options{$_} = $options{ $aliases{$_} } if $options{ $aliases{$_} }; + delete $options{ $aliases{$_} }; + } + return %options; +} + +sub color_palette { + my ($palette_name, $num_colors) = @_; + + $palette_name = 'rainbow' unless defined($palette_name); + + if ($palette_name eq 'rainbow') { + return [ 'violet', 'blue', 'green', 'yellow', 'orange', 'red' ]; + } + +} + 1; From 6b6d9d4166e312947a47e8a23b6ff783fe2086eb Mon Sep 17 00:00:00 2001 From: Peter Staab Date: Tue, 19 May 2026 10:24:05 -0400 Subject: [PATCH 5/6] Update some documentation --- macros/graph/StatisticalPlots.pl | 227 ++++++++++++++++++++++--------- 1 file changed, 165 insertions(+), 62 deletions(-) diff --git a/macros/graph/StatisticalPlots.pl b/macros/graph/StatisticalPlots.pl index 253d45159..bc7c3c6bd 100644 --- a/macros/graph/StatisticalPlots.pl +++ b/macros/graph/StatisticalPlots.pl @@ -44,17 +44,18 @@ =head2 USAGE L. Note that each of the x- and y-axes have separate options and each option is preceded with a C or C. -After the C is created then specific plots are added to the axes. For example: +After a C object is created then specific plots are added to the axes. For example: - @y = (3, 6, 7, 8, 4, 1); $hist->add_barplot( - [ 1 .. 6 ], ~~@y, - fill_color => 'yellow', - width => 1, - bar_width => 0.9 + [ 1 .. 6 ], + [3, 6, 7, 8, 4, 1], + fill_color => 'yellow', + stroke_width => 1, + bar_width => 0.9 ); -will add a barplot to the axes with heights in the C<@y> variable at the x-locations C<(1..6)>. +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. @@ -66,34 +67,35 @@ =head1 PLOT ELEMENTS =head2 BAR PLOTS -A bar plot is added with the C method to a C. The general form for a -bar plot with vertical bars (the default) is +A bar plot can be added using the C<< $stat_plot->add_barplot >> method. $stat_plot->add_barplot($xdata, $ydata, %opts); -where C<$xdata> is an array reference of x-values where the bars will be centered and C<$ydata> is an -ARRAY of heights of the bars. Note: if the option C<< orientation => 'horizontal' >> is included -then the bar lengths are the values in C<$xdata> and locations in C<$ydata>. +This adds vertical bars (as the default) centered at the array reference C<$xdata> +with heights C<$ydata>, an array reference. =head3 OPTIONS The options for the C method are two fold. The following are specific to changing the barplot, and the rest are passed along to C, which is a wrapper function for -C. +C which draws the bars. + +The following are options for the barplot itself: =over =item orientation -The C option can take on C (default) or C to make vertical -or horizontal bars. Above was an example with vertical bars and an example with horizontal bars is +The C option can take on 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 - @x = (3, 6, 7, 8, 4, 1); $hist->add_barplot( - ~~@x, [ 1 .. 6 ], + [3, 6, 7, 8, 4, 1], + [ 1 .. 6 ], orientation => 'horizontal', fill_color => 'yellow', - width => 1, + stroke_width => 1, bar_width => 0.9 ); @@ -101,21 +103,30 @@ =head3 OPTIONS 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. In the example above, with -C<< bar_width => 0.9 >>, there is a small gap between bars. +C<< bar_width => 0 >>. + +=item fill_color + +This is the color of the bars, which is passed to the C method. If this is included +then C is set to C<'self'>, the natural way to fill a rectangle. + +See L for more details on specifying colors. + +=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. =back Any remaining options are passed to C which has the same options as C, -however, if C is passed to C, then the C<< fill => 'self' >> is also -passed along. - -See L for specifics about other options to -both changing fill and stroke color. +however. See L for other options. =head2 HISTOGRAMS -A L is added with the `add_histogram` method -to a C. The general form is +A L is added to a C +with the `add_histogram` method. The general form is $stat_plot->add_histogram($data, %options); @@ -168,13 +179,21 @@ =head3 Options =item normalize If the value of 0 (default) is used, the height of the bars is the count of the number -of points. If the value is 1, then the heights are scaled so the total height of the -bars is 1. +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 included +then C is set to C<'self'>, the natural way to fill a rectangle. + +See L for more details on specifying colors. =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. +the C option of L. See L +for options to change the color. =item stroke_width @@ -198,11 +217,12 @@ =head2 BOX PLOTS $stat_plot->add_boxplot([$data1, $data2, ...], %options); -where C<$data> 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 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 @data = urand(100,25,75,6); @@ -218,7 +238,7 @@ =head2 BOX PLOTS rounded_corners => 1 ); - $boxplot->add_boxplot(~~@data, fill_color => 'lightblue', width => 1); + $boxplot->add_boxplot(~~@data, fill_color => 'LightBlue', stroke_width => 1); and as with other methods in this macro, one can pass options to the characteristic of the box plot (like fill color or stroke color and width) within the C method. @@ -277,52 +297,59 @@ =head3 Options =item box_width -The width of the box in the direction perpendicular to the orientation. If not define, it +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. If multiple box plots are defined, this should only be a single value. =item whisker_cap -Value of 0 (default) or 1. If 1, his will add a short line perpendicular to the whiskers -on the boxplot with relative size C +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. =item cap_width -The width of the cap as a fraction of the box height (if C<< orientation => 'vertical' >>) -or box width (if C<< orientation => 'horizontal' >>). 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. +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 included then C is set to C<'self'>, the natural way to +fill a rectangle. + +See L for more details on specifying colors. =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 rectangle and the whiskers. It is an +alias for the C option of L. +See L for options to change the color. =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 option of L. =back -As with other methods in the macro, other options can be passed along to L -and C which are used in the macro. +As with other methods in the macro, other options can be passed along to +L and C which are used in the macro. -Also, if C is included, then C<< fill => 'self' >> is automatically added on the -box. =head2 SCATTER PLOTS To produce a scatter plot, use the C method to a C. The general form is - $plot->add_scatterplot($data, %options); + $stat_plot->add_scatterplot($data, %options); -where the dataset in C<$data> is an array ref of C pairs as an array ref. For example, +where the dataset in C<$data> is an array reference of C pairs as an array +reference. For example, $stat_plot = StatPlot( xmin => -1, @@ -398,15 +425,25 @@ =head2 PIE CHARTS =item angle_offset -The first sector by default starts at angle 0 (from the positive horizontal axis) in degrees. Use -this to change this. +The first sector by default starts at angle 0 (from the positive horizontal axis) +in degrees. Use this to change this. + +=item fill_colors -=item color_palette +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 or an array reference of colors for each of the -sectors in the pie chart. 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. +Usage: the following are possible options. + + fill_colors => 'rainbow' # generates the rainbow palette + + fill_colors => ['green', 'OliveGreen', 'DarkGreen', 'ForestGreen', 'PineGreen'] + + fill_colors => {palette_name => 'random', num_colors => 7} =item color_sectors @@ -427,7 +464,7 @@ =head2 COLOR PALETTES function. This allows a number of built-in/generated color palettes. To get an array reference of either named or generated colors: - color_palette($name, num_colors => $n); + color_palette($name, $n); For example, @@ -450,11 +487,26 @@ =head3 PALETTE NAMES This will return C random colors from the defined SVG colors. +=item reds + +This will return a selection of red colors. If C is passed in, +the number is ignored. + +=item blues + +This will return a selection of blue colors. If C is passed in, +the number is ignored. + +=item greens + +This will return a selection of green colors. If C is passed in, +the number is ignored. + =back =head2 LEGENDS -A legend is helpful for some plots. +TODO: A legend is helpful for some plots. =cut @@ -686,7 +738,9 @@ sub add_piechart { unless defined($options{labels}) && scalar(@$data) == scalar(@{ $options{labels} }); my $fill_colors = - (!defined $options{fill_colors} || ref $options{fill_colors} ne 'ARRAY') + ref $options{fill_colors} eq 'HASH' + ? color_palette($options{fill_colors}{palette_name}, $options{fill_colors}{num_colors}) + : (!defined $options{fill_colors} || ref $options{fill_colors} ne 'ARRAY') ? color_palette($options{fill_colors}) : $options{fill_colors}; @@ -766,6 +820,55 @@ sub color_palette { if ($palette_name eq 'rainbow') { return [ 'violet', 'blue', 'green', 'yellow', 'orange', 'red' ]; + } elsif ($palette_name eq 'greens') { + return [ 'green', 'Olive', 'DarkGreen', 'LawnGreen', 'MediumAquaMarine', 'LimeGreen' ]; + } elsif ($palette_name eq 'blues') { + return [ 'blue', 'MidnightBlue', 'MediumBlue', 'LightSkyBlue', 'DodgerBlue', 'DarkBlue', 'CornflowerBlue' ]; + } elsif ($palette_name eq 'reds') { + 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) ]; } } From aa36cee065787139c45fbd5802c05df34f1b629d Mon Sep 17 00:00:00 2001 From: Peter Staab Date: Tue, 19 May 2026 13:19:12 -0400 Subject: [PATCH 6/6] Updates to Plots subroutines suggested by @drgrice1 --- lib/Plots/Plot.pm | 22 ++++++++++++++-------- macros/graph/plots.pl | 24 +++++++++++------------- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/lib/Plots/Plot.pm b/lib/Plots/Plot.pm index 01ef797e8..844b653f0 100644 --- a/lib/Plots/Plot.pm +++ b/lib/Plots/Plot.pm @@ -394,16 +394,22 @@ sub add_arc { return $self->_add_arc(@data); } -sub add_rectangle { +sub _add_rectangle { my ($self, $pt0, $pt2, %options) = @_; + unless (ref($pt0) eq 'ARRAY' && @$pt0 == 2 && ref($pt2) eq 'ARRAY' && @$pt2 == 2) { + warn 'A rectangle requires two points defined by length two array references.'; + return; + } + $options{fill} = 'self' if $options{fill_color} && !defined $options{fill}; + return $self->_add_dataset($pt0, [ $pt2->[0], $pt0->[1] ], $pt2, [ $pt0->[0], $pt2->[1] ], $pt0, %options); +} - Value::Error('The first point must be an array ref of length 2') - unless ref($pt0) eq 'ARRAY' && scalar(@$pt0) == 2; - Value::Error('The second point must be an array ref of length 2') - unless ref($pt2) eq 'ARRAY' && scalar(@$pt2) == 2; - # If the fill_color option is set, set the fill to 'self'. - $options{fill} = 'self' if $options{fill_color} && !defined($options{fill}); - return $self->add_dataset($pt0, [ $pt2->[0], $pt0->[1] ], $pt2, [ $pt0->[0], $pt2->[1] ], $pt0, %options); +sub add_rectangle { + my ($self, @data) = @_; + if (ref($data[0]) eq 'ARRAY' && ref($data[0][0]) eq 'ARRAY') { + return [ map { $self->_add_rectangle(@$_) } @data ]; + } + return $self->_add_rectangle(@data); } sub add_vectorfield { diff --git a/macros/graph/plots.pl b/macros/graph/plots.pl index 1d4e6ec42..0d55b9491 100644 --- a/macros/graph/plots.pl +++ b/macros/graph/plots.pl @@ -313,21 +313,19 @@ =head2 PLOT ARCS =head2 PLOT RECTANGLES -A rectangle can be plotted with the C<< $plot->add_rectangle >> method. This is a -convenience method as a shortcut for the C<< $plot->add_dataset >> method. The first -two arguments are opposite corners of the rectangle. All other arguments are passed -as options to the C<< add_dataset >> method. - -The following makes a filled rectangle with a thicker blue border. - - $plot->add_rectangle([2,1], [6,3], - color => 'blue', - width => 1.5, - fill => 'self', - fill_color => 'yellow', - fill_opacity => 0.1, +A rectangle can be plotted with the C<< $plot->add_rectangle >> method. This +method takes two points which are opposite corners of the rectangle. Multiple +rectangles can be plotted at once by passing references to arrays of data for +each rectangle. + + $plot->add_rectangle([$lower_left_x, $lower_left_y], [$upper_right_x, $upper_right_y], %options); + $plot->add_rectangle( + [[$lower_left_x1, $lower_left_y1], [$upper_right_x1, $upper_right_y1], %options1], + [[$lower_left_x2, $lower_left_y2], [$upper_right_x2, $upper_right_y2], %options2], + ... ); + =head2 PLOT VECTOR FIELDS Vector fields and slope fields can be plotted using the C<< $plot->add_vectorfield >> method.