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 4716b4eee..0392cd655 100644 --- a/lib/Plots/JSXGraph.pm +++ b/lib/Plots/JSXGraph.pm @@ -691,7 +691,7 @@ sub draw { $self->{JS} .= "plot.createLabel($x, $y, '$str', $textOptions);"; } - # JSXGraph only produces HTML graphs and uses TikZ for hadrcopy. + # JSXGraph only produces HTML graphs and uses TikZ for hardcopy. return $self->HTML; } diff --git a/lib/Plots/Plot.pm b/lib/Plots/Plot.pm index 43f2f6455..844b653f0 100644 --- a/lib/Plots/Plot.pm +++ b/lib/Plots/Plot.pm @@ -394,6 +394,24 @@ sub add_arc { return $self->_add_arc(@data); } +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); +} + +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 { 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..6239b667e --- /dev/null +++ b/macros/graph/StatisticalPlots.pl @@ -0,0 +1,862 @@ + +=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 using either C (default for hardcopy) or C (default +for HTML). + +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('StatisticalPlots.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 an C or C. + +After a C object is created then specific plots are added to the axes. For example: + + $hist->add_barplot( + [ 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 defined in the second argument at the x-locations +C<(1..6)>. + +See below for more details about creating a barplot and its options. + +=head1 PLOT ELEMENTS + +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 can be added using the C<< $stat_plot->add_barplot >> method. + + $stat_plot->add_barplot($xdata, $ydata, %opts); + +This adds vertical bars (as the default) centered at the values in the array reference C<$xdata> +with heights from C<$ydata>, also an array reference. + +=head3 OPTIONS + +There are two types of options for the C method. The following are specific to +changing the barplot, and any more are passed along to C, which is a wrapper function +for C which draws the bars. + +The following are options for the barplot itself: + +=over + +=item orientation + +The C option can take values C<'vertical'> (default) or C<'horizontal'> to create +vertical or horizontal bars. Above was an example with vertical bars, and an example with +horizontal bars is + + $hist->add_barplot( + [3, 6, 7, 8, 4, 1], + [ 1 .. 6 ], + orientation => 'horizontal', + fill_color => 'yellow', + stroke_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. + +=item fill_color + +This is the color of the bars, which is passed to the C method. If this is not +included, then C is set to C<'self'>, the natural way to fill a rectangle. + +See L for more details on specifying colors. + +=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. +See L for other options. + +=head2 HISTOGRAMS + +A L is added to a C with the +`add_histogram` method. 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 (e.g. number of bins) as well as options for the bars. + +Here is an example 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 0 is used (the default), the height of the bars is the count of the number of points within each +bin. If the value is 1, then the heights are scaled so the total height of the bars is 1. + +=item fill_color + +This is the color of the bars, which is passed to the C method. If this is not +included, then C is set to C<'self'>, the natural way to fill a rectangle. + +See L for more details on specifying colors. + +=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> (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); + + $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', stroke_width => 1); + +and as with other methods, one can pass options for the box plot (like fill color, stroke color, +and width) within the C method. + +If C<$data> is a hash reference, it must contains the fields C that are +used to define the boxplot. Optionally, one may also include the field C which is an +array ref of values which will be plotted beyond the whiskers. + +An example of this is + + $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 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 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 width. Default value is 0.2. + +=item outlier_mark + +The shape of the mark to use for outliers. Default is 'plus'. +See L for other mark options. + +=item fill_color + +This is the color of the bars, which is passed to the C method. If this is not +included, then C is set to C<'self'>, the natural way to fill a rectangle. + +See L for more details on specifying colors. + +=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, other options can be passed to L and +C. + + +=head2 SCATTER PLOTS + +To produce a scatter plot, use the C method to a C. The general form is + + $stat_plot->add_scatterplot($data, %options); + +where the dataset in C<$data> is an array reference of C pairs as an array reference. 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 each given a color and a label. This method can also produce donut +charts (or ring charts), which is a pie chart with a hole. + +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, but +can be changed with this option. + +=item fill_colors + +This is either the name of a color palette (as a string), an array reference of colors, or a hash +reference for the name of the color palette and number of colors to generate (not available for all +palettes). If the length of this array reference is smaller than the C<$data> array reference, then +the colors will be cycled. The default is to use the 'default' color palette. See +L for more information. + +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 + +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, $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. + +=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 + +TODO: 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 = + 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}; + + 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 + && $alpha < 3 * $pi / 4 ? (v_align => 'bottom') : 3 * $pi / 4 <= $alpha + && $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' ]; + } 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) ]; + } + +} + +1; diff --git a/macros/graph/plots.pl b/macros/graph/plots.pl index 5bb9288d4..0d55b9491 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( @@ -304,13 +304,28 @@ =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 +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. @@ -592,7 +607,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 +724,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],