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..4fc0ad2d6 --- /dev/null +++ b/macros/graph/StatisticalPlots.pl @@ -0,0 +1,872 @@ + +=head1 NAME + +StatisticalPlots.pl - A macro to create statistical plots to include in PG +problems. + +=head1 DESCRIPTION + +This macro includes a number of methods to include statistical plots in PG +problems. This is based on L which will draw using either C +(default for hardcopy) or C (default for HTML). + +The statistical plots that are available are + +=over + +=item Box Plots + +=item Bar Plots + +=item Histograms + +=item Scatter Plots + +=item Pie Charts + +=back + +=head2 USAGE + +Create a C object with + + loadMacros('StatisticalPlots.pl'); + + $statPlot = 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 C method accepts all of the same options that the C method +from the L macro accepts. + +After a C object is created then specific plots can be added to the +axes. For example, + + $statPlot->add_barplot( + [ 1 .. 6 ], + [3, 6, 7, 8, 4, 1], + fill_color => 'yellow', + stroke_width => 1, + bar_width => 0.9 + ); + +will add a bar plot to the axes. See below for more details about creating a bar +plot. + +=head1 AVAILABLE PLOTS + +The following plots are available to add to a C object. + +=head2 BAR PLOTS + +A bar plot can be added with the C method. The general form is + + $statPlot->add_barplot($xdata, $ydata, %opts); + +This adds a bar plot with bars centered at the coordinates in the array +referenced to by C<$xdata> and heights in the array referenced to by C<$ydata>. + +=head3 OPTIONS + +The following options which are specific to configuring the bar plot can be +passed in addition to any of the dataset options accepted by the +L method which draws the bars (see +L). + +=over + +=item orientation + +This can be either C<'vertical'> (default) or C<'horizontal'> to create vertical +or horizontal bars. An example with horizontal bars was given above, and the +following is an example with with vertical bars + + $statPlot->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 + +This is a number from C<0> to C<1> that specifies the width of the bars as a +fraction of the distance between the bar base coordinates (the x-coordinates for +a vertical bar plot or the y-coordinates for a horizontal bar plot). If +C<< bar_width => 1 >> (the default), then the bar width will be the distance +between the bar base coordinates (and so there is no gap between bars). In the +example above, with C<< bar_width => 0.9 >> the bar width will be C<90%> of the +distance between the bar base coordinates. + +=item stroke_color + +This sets the color of the bar borders. It is an alias for the C +L option of the +L method. See +L for more details on specifying colors. + +=item stroke_width + +This sets the width of the rectangle borders. This is an alias for the C +L. + +=back + +=head2 HISTOGRAMS + +An histogram can be added to a C with the C method. The +general form is + + $statPlot->add_histogram($data, %options); + +where C<$data> is a reference to an array of univariate data. The C<%options> +parameter includes options for the histogram such as the number of bins as well +as options for drawing the bars. + +The following example generates a data set using the L +function which produces a random set of normally distributed data values, and +constructs a histogram for that data set. + + loadMacros('StatisticalPlots.pl', 'PGstatisticsmacros.pl'); + + $statPlot = StatPlot( + xmin => 0, + xmax => 65, + ymin => 0, + ymax => 12, + xtick_delta => 10, + ytick_delta => 2 + ); + $statPlot->add_histogram( + [ urand(30, 9, 50, 6) ], + min => 10, + max => 60, + bins => 10, + fill_color => 'LightGreen', + width => 1 + ); + +=head3 Options + +The following options which are specific to configuring the histogram can be +passed in addition to any of the dataset options accepted by the +L method which draws the bars for the +histogram (see L). Note that if the +C option is set, then C<< fill => 'self' >> is automatically set, +and so you do not need to specify that option. + +=over + +=item orientation + +This can be either C<'vertical'> (default) or C<'horizontal'> to create vertical +or horizontal bars. + +=item min + +The left edge of the leftmost bar. If not defined, the minimum value in the +C<$data> array is used. + +=item max + +The right edge of the rightmost bar. If not defined, the maximum value in +C<$data> is used. + +=item bins + +The number of bins/bars to use for the histogram. This must be an integer +greater than 0. The default value is 10. + +=item normalize + +If this is set to 0 (the default), then the height of each bar is the number of +data values in the bin. If this is 1, then the heights are scaled so the sum of +the heights of the bars is 1. + +=item stroke_color + +This sets the color of the bar borders. It is an alias for the C +L option of the +L method. See +L for more details on specifying colors. + +=item stroke_width + +This sets the width of the rectangle borders. This is an alias for the C +L. + +=back + +=head2 BOX PLOTS + +A box plot (also called a box and whiskers plot) can be created with the +C method. The general form for adding a single box plot is + + $statPlot->add_boxplot($data, %options); + +or to add multiple box plots is + + $statPlot->add_boxplot([$data1, $data2, ...], %options); + +where C<$data> (or C<$data1>, C<$data2>, ...) is a reference to an array of +univariate data or a reference to a hash of the box plot characteristics. Box +plots are created using the five number summary (minimum, first quartile, +median, third quartile, maximum) of the data which is calculated using the +L method. + +An example of creating a box plot with a reference to an array of univariate +data is + + $statPlot = StatPlot( + xmin => 0, + xmax => 200, + ymin => -5, + ymax => 25, + xtick_delta => 25, + yvisible => 0, + show_grid => 0, + aspect_ratio => 4, + rounded_corners => 1 + ); + + $statPlot->add_boxplot( + [ urand(100, 25, 75, 6) ], + fill_color => 'LightBlue', + stroke_width => 1 + ); + +If C<$data> is a hash reference, then it must contains the fields C that form the five number summary. Optionally, the field +C may also be included which is a reference to an array of values +which will be plotted outside of the whiskers. For example, + + $boxplot = StatPlot( + xmin => 100, + xmax => 400, + ymin => -5, + ymax => 25, + xtick_delta => 50, + yvisible => 0, + show_grid => 0, + aspect_ratio => 4 + ); + + $boxplot->add_boxplot({ + min => random(150, 175, 5), + q1 => random(180, 225, 5), + median => random(250, 275, 5), + q3 => random(280, 320, 10), + max => random(325, 350, 5), + outliers => [115, 130] + }); + +=head3 Options + +The following options which are specific to configuring the box plot can be +passed in addition to any of the L. +Note that if the C option is set, then C<< fill => 'self' >> is +automatically set, and so you do not need to specify that option. + +=over + +=item orientation + +This is the direction of the box plot and can be either 'horizontal' (the +default) or 'vertical'. + +=item box_center + +The location of the center of the box. This is optional and if not defined the +box plot will be centered in the range of the minor axis (the y-axis for +horizontal box plots and the x-axis for vertical box plots). + +If multiple box plots are being added and this option is not set, then the box +plots will be equally spaced in the range of the minor axis. + +If included, this option must be a reference to an array of values (in the +x-direction for vertical plots and y-direction for horizontal plots) at which to +center the added box plots. The number of entries in the array must be the same +as the number of box plots that are being added. For example, if three box plots +are being added this could be + + box_center => [3, 6, 9] + +=item box_width + +The width of the box in the direction perpendicular to the orientation. The +default is to use half of the range of the minor axis. + +If multiple box plots are defined, this should only be a single value. + +=item whisker_cap + +This should be set to 0 (default) or 1. If this value is 1, lines that are +perpendicular to the whiskers will be added at the ends of the whiskers of the +box plot. The length of the lines are determined by the C option. + +=item cap_width + +The width of the cap as a fraction of the box width. Default value is 0.2. + +=item outlier_mark + +The shape of the mark to use for outliers. The default is 'plus'. See the +L for other mark options. + +=item stroke_color + +This sets the color of the boundary of the box and the whiskers. It is an alias +for the C L. See +L for more details on specifying colors. + +=item stroke_width + +This sets the width of the boundary of the rectangle and the whiskers. This is +an alias for the C L. + +=back + +=head2 SCATTER PLOTS + +A scatter plot can be created with the C method. The general +form for adding a scatter plot is + + $statPlot->add_scatterplot($data, %options); + +where C<$data> is a reference to an array of array references of C, C +pairs. For example, + + $statPlot = StatPlot( + xmin => -1, + xmax => 15, + ymin => -1, + ymax => 15, + xtick_delta => 5, + ytick_delta => 5, + ); + + $statPlot->add_scatterplot( + [ [1, 1], [2, 3], [3, 4], [5, 5], [7, 8], [10, 9], [12, 10] ], + marks => 'diamond', + mark_size => 5, + color => 'orange' + ); + +This method simply calls the L method with the +following L set. Any of these can be +changed or any of the other data set options set in the C<%options> parameter. + +=over + +=item linestyle + +This sets the style of the lines between the marks is set to 'none' by this +method, so that lines are not drawn between the points. + +=item marks + +This sets the symbol to use for the marks and is set to C<'circle'> by this +method. See the L for other mark +options. + +=item mark_size + +This sets the size of the marks and is set to 3 by this method. + +=item mark_color + +This sets the color of the marks and is an alias for the C +L option. See L +for more details on specifying colors. + +=back + +If more than one dataset is to be plotted, then call the C +method multiple times. This can be done with a single C method +call, but this wrapper is convenient when the default options above are desired. + +=head2 PIE CHARTS + +A pie chart is a circle that divided into sectors whose size is proportional to +an input array. The sectors are each assigned a color and a label. This method +will also produce donut charts (or ring charts), which is a pie chart with a +hole. + +The general form for calling the C method is + + $statPlot->add_piechart($data, %options); + +where C<$data> is a reference to an array of values. + +=head3 Options + +The following options which are specific to configuring the pie chart can be +passed in addition to any of the L. + +=over + +=item center + +The center of the circle as an array reference. The default value is C<[0, 0]>. + +=item radius + +The radius of the circle. The default value of 4 is chosen to fit nicely with +the default values for the bounding box of the C which ranges from -5 +to 5 in both the x and y directions. + +=item inner_radius + +This is the radius of the inner circle of the chart. Set this to a value less +than the C for a donut or ring chart. The default value is 0. + +=item angle_offset + +This is the angle in degrees from the positive horizontal axis at which the +first sector begins. This is 0 by default. + +=item fill_colors + +This is either the name of a color palette, a reference to an array of colors, +or a reference to a hash containing the name of a color palette +(C) and number of colors to generate (C) (see L for more information). If this is a reference to an array and the +length of the array is smaller than the number of data values in the array +referenced to by C<$data>, then the colors will be cycled. The default is to use +the C<'rainbow'> color palette. + +For example, + + fill_colors => 'rainbow' + + fill_colors => ['green', 'OliveGreen', 'DarkGreen', 'ForestGreen', 'PineGreen'] + + fill_colors => { palette_name => 'random', num_colors => 7 } + +=item color_sectors + +If this is 1 (the default), then colors are used for the pie chart. If 0, then +the sectors are not filled. See L for details on selecting +colors. + +=item sector_labels + +The labels for the sector as a array reference of strings or values. The +default is for no labels. If this is used, the length of this must be the same +as the C<$data> array reference. + +=back + +=head2 COLOR PALETTES + +The color palettes for the bar plots and pie charts can be selected with the +C function. This allows a number of built-in or generated color +palettes. To get an array reference of either named or generated colors call + + color_palette($name, $numColors); + +For example, + + color_palette('rainbow'); + +returns the 6 colors of the rainbow. Some of the palettes have fixed numbers of +colors, whereas others have variable numbers. If C<$numColors> is not defined, +then some palettes return a fixed number (like 'rainbow') and if the +C<$numColors> is needed, then the default of 10 is assumed. + +=head3 PALETTE NAMES + +=over + +=item rainbow + +The 6 colors of the rainbow from violet to red. The C<$numColors> value is +ignored for this palette. + +=item reds + +This will return a selection of red colors. The C<$numColors> value is ignored +for this palette. + +=item blues + +This will return a selection of blue colors. The C<$numColors> value is ignored +for this palette. + +=item greens + +This will return a selection of green colors. The C<$numColors> value is ignored +for this palette. + +=item random + +This will return C<$numColors> random colors from the defined SVG colors. + +=back + +=cut + +BEGIN { strict->import; } + +sub _StatisticalPlots_init { + main::PG_restricted_eval('sub StatPlot { Plots::StatPlot->new(@_); }'); +} + +loadMacros('PGstatisticsmacros.pl', 'PGauxiliaryFunctions.pl'); + +package Plots::StatPlot; +our @ISA = qw(Plots::Plot); + +sub add_histogram { + my ($self, $data, %options) = @_; + + $options{orientation} //= 'vertical'; + + my $numBins = delete $options{bins} // 10; + $numBins = 10 unless $numBins =~ /^\d+$/ && $numBins > 0; + + my $min = delete $options{min} // main::min(@$data); + my $max = delete $options{max} // main::max(@$data); + my $binWidth = ($max - $min) / $numBins; + + my @frequencies = (0) x $numBins; + ++$frequencies[ int(($_ - $min) / $binWidth) ] for @$data; + + if (delete $options{normalize}) { + my $total = 0; + $total += $_ for @frequencies; + @frequencies = map { $_ / $total } @frequencies; + } + + my @xdata = map { $min + (0.5 + $_) * $binWidth } (0 .. $#frequencies); + + return $self->add_barplot( + $options{orientation} eq 'vertical' ? (\@xdata, \@frequencies) : (\@frequencies, \@xdata), %options + ), + \@frequencies; +} + +sub add_barplot { + my ($self, $xdata, $ydata, %options) = @_; + + $options{bar_width} //= 1; + $options{orientation} //= 'vertical'; + set_plot_option_aliases(\%options); + + Value::Error('The first two arguments must be references to arrays of the same length.') + unless (ref $xdata eq 'ARRAY' && ref $xdata eq 'ARRAY' && @$xdata == @$ydata); + + # Assume that the values in $xdata are equally spaced. + # TODO: Should arbitrarily spaced bars be handled? + my $bar_width = $options{orientation} eq 'vertical' ? $xdata->[1] - $xdata->[0] : $ydata->[1] - $ydata->[0]; + + return $self->add_rectangle( + map { [ + $options{orientation} eq 'vertical' + ? ( + [ $xdata->[$_] - 0.5 * $bar_width * $options{bar_width}, 0 ], + [ $xdata->[$_] + 0.5 * $bar_width * $options{bar_width}, $ydata->[$_] ] + ) + : ( + [ 0, $ydata->[$_] - 0.5 * $bar_width * $options{bar_width} ], + [ $xdata->[$_], $ydata->[$_] + 0.5 * $bar_width * $options{bar_width} ] + ), + %options + ] } 0 .. $#$xdata + ); +} + +sub add_boxplot { + my ($self, $data, %options) = @_; + + $options{orientation} //= 'horizontal'; + $options{whisker_cap} //= 0; + $options{cap_width} //= 0.2; + $options{outlier_mark} //= 'plus'; + set_plot_option_aliases(\%options); + + if (ref $data eq 'ARRAY' && (ref $data->[0] eq 'ARRAY' || ref $data->[0] eq 'HASH')) { + my $box_centers; + if ($options{box_center}) { + Value::Error(q{The "box_center" option must be a reference to an array } + . 'with the same length as the number of box plots being added.') + unless (ref $options{box_center} eq 'ARRAY' && @{ $options{box_center} } == @$data); + $box_centers = delete $options{box_center}; + } else { + unless ($options{box_width}) { + $options{box_width} = + ($options{orientation} eq 'vertical' ? $self->axes->xaxis('max') : $self->axes->yaxis('max')) / + (2.5 * @$data); + } + $box_centers = [ map { 2 * $options{box_width} * $_ } (1 .. @$data + 1) ]; + } + for (0 .. $#$data) { + $options{box_center} = $box_centers->[$_]; + $self->_add_boxplot($data->[$_], %options); + } + } else { + $self->_add_boxplot($data, %options); + } +} + +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 elements of the five number summary were provided. + my %missing; + for ('min', 'q1', 'median', 'q3', 'max') { + $missing{$_} = 1 unless defined $data->{$_}; + } + for (keys %missing) { + Value::Error(qq{The parameter "$_" is missing from the box plot five number summary.}); + } + + $params = $data; + } + + if ($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 ], + [ [ $params->{q3}, $box_center ], [ $params->{max}, $box_center ], %options ], + [ + [ $params->{median}, $box_center - 0.5 * $box_width ], + [ $params->{median}, $box_center + 0.5 * $box_width ], + %options + ] + ); + + 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 + ], + [ + [ $params->{min}, $box_center - 0.5 * $options{cap_width} * $box_width ], + [ $params->{min}, $box_center + 0.5 * $options{cap_width} * $box_width ], + %options + ] + ); + } + + if (ref $params->{outliers} eq 'ARRAY') { + $self->add_dataset( + (map { [ $_, $box_center ] } @{ $params->{outliers} }), + linestyle => 'none', + marks => $options{outlier_mark}, + marksize => 3 + ); + } + } elsif ($orientation eq 'vertical') { + my $box_center = $options{box_center} // 0.5 * $self->axes->xaxis->{max}; + my $box_width = $options{box_width} // 0.5 * $self->axes->xaxis->{max}; + + $self->add_rectangle([ $box_center - 0.5 * $box_width, $params->{q1} ], + [ $box_center + 0.5 * $box_width, $params->{q3} ], %options); + $self->add_dataset( + [ [ $box_center, $params->{min} ], [ $box_center, $params->{q1} ], %options ], + [ [ $box_center, $params->{q3} ], [ $box_center, $params->{max}, ], %options ], + [ + [ $box_center - 0.5 * $box_width, $params->{median} ], + [ $box_center + 0.5 * $box_width, $params->{median} ], + %options + ] + ); + + 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 + ], + [ + [ $box_center - 0.5 * $options{cap_width} * $box_width, $params->{min} ], + [ $box_center + 0.5 * $options{cap_width} * $box_width, $params->{min} ], + %options + ] + ); + } + + if (ref $params->{outliers} eq 'ARRAY') { + $self->add_dataset( + (map { [ $box_center, $_ ] } @{ $params->{outliers} }), + linestyle => 'none', + marks => $options{outlier_mark}, + marksize => 3 + ); + } + } +} + +sub add_scatterplot { + my ($self, $data, %options) = @_; + + $options{linestyle} //= 'none'; + $options{marks} //= 'circle'; + $options{mark_size} //= 3; + set_plot_option_aliases(\%options); + + $self->add_dataset(@$data, %options); + +} + +sub add_piechart { + my ($self, $data, %options) = @_; + + $options{center} //= [ 0, 0 ]; + $options{radius} //= 4; + $options{angle_offset} //= 0; + $options{inner_radius} //= 0; + set_plot_option_aliases(\%options); + + Value::Error('The number of labels must equal the number of sectors in the pie chart') + unless (defined $options{labels} && @$data == @{ $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->[ $_ % @$fill_colors ], + %options + ); + # add the labels if defined + if ($options{labels}) { + my $alpha = $theta + 0.5 * $delta_theta; + + $self->add_label( + $options{radius} * cos($alpha), $options{radius} * sin($alpha), $options{labels}[$_], + anchor => 180 * (1 + $alpha / $pi), + padding => 15 + ); + } + $theta += $delta_theta; + } + +} + +sub set_plot_option_aliases { + my $options = shift; + my %aliases = (stroke_width => 'width', stroke_color => 'color', mark_color => 'color'); + for (keys %aliases) { + $options->{ $aliases{$_} } = delete $options->{$_} if defined $options->{$_}; + } + return %$options; +} + +sub color_palette { + my ($palette_name, $num_colors) = @_; + + $palette_name //= 'rainbow'; + + 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') { + return [ + main::random_subset( + $num_colors // 10, + ( + 'AliceBlue', 'AntiqueWhite', 'Aqua', 'Aquamarine', + 'Azure', 'Beige', 'Bisque', 'Black', + 'BlanchedAlmond', 'Blue', 'BlueViolet', 'Brown', + 'BurlyWood', 'CadetBlue', 'Chartreuse', 'Chocolate', + 'Coral', 'CornflowerBlue', 'Cornsilk', 'Crimson', + 'Cyan', 'DarkBlue', 'DarkCyan', 'DarkGoldenrod', + 'DarkGray', 'DarkGreen', 'DarkGrey', 'DarkKhaki', + 'DarkMagenta', 'DarkOliveGreen', 'DarkOrange', 'DarkOrchid', + 'DarkRed', 'DarkSalmon', 'DarkSeaGreen', 'DarkSlateBlue', + 'DarkSlateGray', 'DarkSlateGrey', 'DarkTurquoise', 'DarkViolet', + 'DeepPink', 'DeepSkyBlue', 'DimGray', 'DimGrey', + 'DodgerBlue', 'FireBrick', 'FloralWhite', 'ForestGreen', + 'Fuchsia', 'Gainsboro', 'GhostWhite', 'Gold', + 'Goldenrod', 'Gray', 'Green', 'GreenYellow', + 'Grey', 'Honeydew', 'HotPink', 'IndianRed', + 'Indigo', 'Ivory', 'Khaki', 'Lavender', + 'LavenderBlush', 'LawnGreen', 'LemonChiffon', 'LightBlue', + 'LightCoral', 'LightCyan', 'LightGoldenrodYellow', 'LightGray', + 'LightGreen', 'LightGrey', 'LightPink', 'LightSalmon', + 'LightSeaGreen', 'LightSkyBlue', 'LightSlateGray', 'LightSlateGrey', + 'LightSteelBlue', 'LightYellow', 'Lime', 'LimeGreen', + 'Linen', 'Magenta', 'Maroon', 'MediumAquamarine', + 'MediumBlue', 'MediumOrchid', 'MediumPurple', 'MediumSeaGreen', + 'MediumSlateBlue', 'MediumSpringGreen', 'MediumTurquoise', 'MediumVioletRed', + 'MidnightBlue', 'MintCream', 'MistyRose', 'Moccasin', + 'NavajoWhite', 'Navy', 'OldLace', 'Olive', + 'OliveDrab', 'Orange', 'OrangeRed', 'Orchid', + 'PaleGoldenrod', 'PaleGreen', 'PaleTurquoise', 'PaleVioletRed', + 'PapayaWhip', 'PeachPuff', 'Peru', 'Pink', + 'Plum', 'PowderBlue', 'Purple', 'RebeccaPurple', + 'Red', 'RosyBrown', 'RoyalBlue', 'SaddleBrown', + 'Salmon', 'SandyBrown', 'SeaGreen', 'Seashell', + 'Sienna', 'Silver', 'SkyBlue', 'SlateBlue', + 'SlateGray', 'SlateGrey', 'Snow', 'SpringGreen', + 'SteelBlue', 'Tan', 'Teal', 'Thistle', + 'Tomato', 'Turquoise', 'Violet', 'Wheat', + 'White', 'WhiteSmoke', 'Yellow', 'YellowGreen' + ) + ) + ]; + } +} + +1; diff --git a/macros/graph/plots.pl b/macros/graph/plots.pl index 5bb9288d4..7fa04ab0f 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,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 +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], + ... + ); + +Note that if the C option is set, then C<< fill => 'self' >> is +automatically set, and so you do not need to specify that option. + =head2 PLOT VECTOR FIELDS 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],