diff --git a/htdocs/js/Plots/plots.js b/htdocs/js/Plots/plots.js index 7e1c0416a..837e5fbe4 100644 --- a/htdocs/js/Plots/plots.js +++ b/htdocs/js/Plots/plots.js @@ -375,9 +375,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 5141a255e..990908f7a 100644 --- a/lib/Plots/Axes.pm +++ b/lib/Plots/Axes.pm @@ -313,6 +313,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/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/lib/Plots/JSXGraph.pm b/lib/Plots/JSXGraph.pm index cc65c8d42..d81deb2cb 100644 --- a/lib/Plots/JSXGraph.pm +++ b/lib/Plots/JSXGraph.pm @@ -122,21 +122,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 b03262739..40b94bdaf 100644 --- a/lib/Plots/Plot.pm +++ b/lib/Plots/Plot.pm @@ -392,6 +392,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; + # 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_vectorfield { my ($self, @options) = @_; my $data = Plots::Data->new(name => 'vectorfield'); diff --git a/macros/core/PGbasicmacros.pl b/macros/core/PGbasicmacros.pl index 6437d71e9..6ea5862b6 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}; @@ -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 new file mode 100644 index 000000000..253d45159 --- /dev/null +++ b/macros/graph/StatisticalPlots.pl @@ -0,0 +1,773 @@ + +=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 plots available are + +=over + +=item Box Plots + +=item Bar Plots + +=item Histograms + +=item Scatter Plots + +=item Pie Charts + +=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 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>. + +=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 + + @x = (3, 6, 7, 8, 4, 1); + $hist->add_barplot( + ~~@x, [ 1 .. 6 ], + orientation => 'horizontal', + fill_color => 'yellow', + width => 1, + 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. + +=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. + +=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 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 + + $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 array reference +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 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 + + $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 +array reference 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. + +=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. + +=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 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); + +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. + +=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. + +=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; } + +sub _StatisticalPlots_init { + main::PG_restricted_eval('sub StatPlot { Plots::StatPlot->new(@_); }'); +} + +loadMacros('PGstatisticsmacros.pl'); + +package Plots::StatPlot; +our @ISA = qw(Plots::Plot); + +sub add_histogram { + my ($self, $data, %opts) = @_; + + my %options = ( + bins => 10, + normalize => 0, + orientation => 'vertical', + %opts + ); + + Value::Error("The option 'bins' must be a positive integer") + unless $options{bins} =~ /^\d+$/ && $options{bins} > 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}; + + # 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', 'normalize'); + + 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', + plot_option_aliases(%opts) + ); + + 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? + 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', + whisker_cap => 0, + cap_width => 0.2, + outlier_mark => 'plus', + plot_option_aliases(%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) { + Value::Error("The parameter $_ is missing from the boxplot attributes.") if $count{$_} > 0; + } + $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); + + # 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 => $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); + } + + # 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); + } + } +} + +sub add_scatterplot { + my ($self, $data, %opts) = @_; + + my %options = ( + linestyle => 'none', + marks => 'circle', + mark_size => 3, + 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; diff --git a/macros/graph/plots.pl b/macros/graph/plots.pl index de245bd66..241759806 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 @@ -16,7 +16,7 @@ =head1 DESCRIPTION =head1 USAGE -First create a Plots object: +First create a Plot object: loadMacros('plots.pl'); $plot = Plot( @@ -291,13 +291,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. @@ -579,7 +596,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 @@ -696,7 +713,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],