From 90653360d62b965d093c85a77248a3e759eaecc6 Mon Sep 17 00:00:00 2001 From: Jaimos Skriletz Date: Thu, 8 Feb 2024 23:09:59 -0700 Subject: [PATCH] Combine PGplot files into single macro file. --- macros/graph/PGplot.pl | 1158 ++++++++++++++++++++++++++++++++++- macros/graph/PGplot/Axes.pl | 342 ----------- macros/graph/PGplot/Data.pl | 230 ------- macros/graph/PGplot/GD.pl | 383 ------------ macros/graph/PGplot/Tikz.pl | 289 --------- 5 files changed, 1153 insertions(+), 1249 deletions(-) delete mode 100644 macros/graph/PGplot/Axes.pl delete mode 100644 macros/graph/PGplot/Data.pl delete mode 100644 macros/graph/PGplot/GD.pl delete mode 100644 macros/graph/PGplot/Tikz.pl diff --git a/macros/graph/PGplot.pl b/macros/graph/PGplot.pl index 2602f86f54..d824b9d2ca 100644 --- a/macros/graph/PGplot.pl +++ b/macros/graph/PGplot.pl @@ -31,7 +31,7 @@ =head1 USAGE loadMacros('PGplot.pl'); $plot = PGplot(); -Configure the L: +Configure the L: $plot->axes->xaxis( min => 0, @@ -59,7 +59,7 @@ =head1 USAGE =head1 PLOT ELEMENTS -A plot consists of multiple L objects, which define datasets, functions, +A plot consists of multiple L objects, which define datasets, functions, and labels to add to the graph. Data objects should be created though the PGplot object, but can be access directly if needed @@ -90,7 +90,7 @@ =head2 DATASETS [[0, 0], [4, -1], color => 'red', end_mark => 'arrow'], ); -If needed, the C<$plot-Eadd_dataset> method returns the L object +If needed, the C<$plot-Eadd_dataset> method returns the L object (or array of Data objects) which can be manipulated directly. $data = $plot->add_dataset(...); @@ -408,8 +408,6 @@ BEGIN strict->import; } -loadMacros('MathObjects.pl', 'PGplot/Axes.pl', 'PGplot/Data.pl', 'PGplot/GD.pl', 'PGplot/Tikz.pl'); - sub _PGplot_init { } sub PGplot { PGplot->new(@_); } @@ -763,4 +761,1154 @@ sub draw { return $image->draw; } +# Tikz/PGFPlots output +package PGplot::Tikz; + +sub new { + my ($class, $pgplot) = @_; + my $image = new LaTeXImage; + $image->environment('tikzpicture'); + $image->svgMethod($main::envir{latexImageSVGMethod} // 'pdf2svg'); + $image->convertOptions($main::envir{latexImageConvertOptions} // { input => {}, output => {} }); + $image->ext($pgplot->ext); + $image->tikzLibraries('arrows.meta'); + $image->texPackages(['pgfplots']); + $image->addToPreamble('\pgfplotsset{compat=1.18}\usepgfplotslibrary{fillbetween}'); + + my $self = { + image => $image, + pgplot => $pgplot, + colors => {}, + }; + bless $self, $class; + + return $self; +} + +sub pgplot { + my $self = shift; + return $self->{pgplot}; +} + +sub im { + my $self = shift; + return $self->{image}; +} + +sub get_color { + my ($self, $color) = @_; + return '' if $self->{colors}{$color}; + my ($r, $g, $b) = @{ $self->pgplot->colors($color) }; + $self->{colors}{$color} = 1; + return "\\definecolor{$color}{RGB}{$r,$g,$b}\n"; +} + +sub configure_axes { + my $self = shift; + my $pgplot = $self->pgplot; + my $axes = $pgplot->axes; + my $grid = $axes->grid; + my ($xmin, $ymin, $xmax, $ymax) = $axes->bounds; + my ($axes_height, $axes_width) = $pgplot->size; + my $show_grid = $axes->style('show_grid'); + my $xmajor = $show_grid && $grid->{xmajor} ? 'true' : 'false'; + my $xminor_num = $show_grid && $grid->{xmajor} ? $grid->{xminor} : 0; + my $xminor = $xminor_num > 0 ? 'true' : 'false'; + my $ymajor = $show_grid && $grid->{ymajor} ? 'true' : 'false'; + my $yminor_num = $show_grid && $grid->{ymajor} ? $grid->{yminor} : 0; + my $yminor = $yminor_num > 0 ? 'true' : 'false'; + my $xticks = join(',', @{ $grid->{xticks} }); + my $yticks = join(',', @{ $grid->{yticks} }); + my $grid_color = $axes->style('grid_color'); + my $grid_color2 = $self->get_color($grid_color); + my $grid_alpha = $axes->style('grid_alpha'); + my $grid_style = $axes->style('grid_style'); + my $xlabel = $axes->xaxis('label'); + my $axis_x_line = $axes->xaxis('location'); + my $ylabel = $axes->yaxis('label'); + my $axis_y_line = $axes->yaxis('location'); + my $title = $axes->style('title'); + my $axis_on_top = $axes->style('axis_on_top') ? "axis on top,\n\t\t\t" : ''; + my $hide_x_axis = ''; + my $hide_y_axis = ''; + my $xaxis_plot = ($xmin <= 0 && $xmax >= 0) ? "\\path[name path=xaxis] ($xmin, 0) -- ($xmax,0);\n" : ''; + + unless ($axes->xaxis('visible')) { + $xlabel = ''; + $hide_x_axis = + "\n\t\t\tx axis line style={draw=none},\n" + . "\t\t\tx tick style={draw=none},\n" + . "\t\t\txticklabel=\\empty,"; + } + unless ($axes->yaxis('visible')) { + $ylabel = ''; + $hide_y_axis = + "\n\t\t\ty axis line style={draw=none},\n" + . "\t\t\ty tick style={draw=none},\n" + . "\t\t\tyticklabel=\\empty,"; + } + my $tikzCode = <style('color') || 'default_color'; + my $width = $data->style('width') || 1; + my $linestyle = $data->style('linestyle') || 'solid'; + my $marks = $data->style('marks') || 'none'; + my $mark_size = $data->style('mark_size') || 0; + my $start = $data->style('start_mark') || 'none'; + my $end = $data->style('end_mark') || 'none'; + my $name = $data->style('name') || ''; + my $fill = $data->style('fill') || 'none'; + my $fill_color = $data->style('fill_color') || 'default_color'; + my $fill_opacity = $data->style('fill_opacity') || 0.5; + my $tikzOpts = $data->style('tikzOpts') || ''; + + if ($start =~ /circle/) { + $start = '{Circle[sep=-1.196825pt -1.595769' . ($start eq 'open_circle' ? ', open' : '') . ']}'; + } elsif ($start eq 'arrow') { + $start = '{Latex}'; + } else { + $start = ''; + } + if ($end =~ /circle/) { + $end = '{Circle[sep=-1.196825pt -1.595769' . ($end eq 'open_circle' ? ', open' : '') . ']}'; + } elsif ($end eq 'arrow') { + $end = '{Latex}'; + } else { + $end = ''; + } + my $end_markers = ($start || $end) ? ", $start-$end" : ''; + $marks = { + closed_circle => '*', + open_circle => 'o', + plus => '+', + times => 'x', + bar => '|', + dash => '-', + asterisk => 'asterisk', + star => 'star', + oplus => 'oplus', + otimes => 'otimes', + diamond => 'diamond', + none => '', + }->{$marks}; + $marks = $marks ? $mark_size ? ", mark=$marks, mark size=${mark_size}px" : ", mark=$marks" : ''; + $linestyle = $linestyle eq 'none' ? ', only marks' : ', ' . ($linestyle =~ s/_/ /gr); + if ($fill eq 'self') { + $fill = ", fill=$fill_color, fill opacity=$fill_opacity"; + } else { + $fill = ''; + } + $name = ", name path=$name" if $name; + $tikzOpts = ", $tikzOpts" if $tikzOpts; + + return "color=$color, line width=${width}pt$marks$linestyle$end_markers$fill$name$tikzOpts"; +} + +sub draw { + my $self = shift; + my $pgplot = $self->pgplot; + + # Reset colors just in case. + $self->{colors} = {}; + + # Add Axes + my $tikzCode = $self->configure_axes; + + # Plot Data + for my $data ($pgplot->data('function', 'dataset')) { + $data->gen_data; + my $n = $data->size; + my $color = $data->style('color') || 'default_color'; + my $fill = $data->style('fill') || 'none'; + my $fill_color = $data->style('fill_color') || 'default_color'; + my $tikzData = join(' ', map { '(' . $data->x($_) . ',' . $data->y($_) . ')'; } (0 .. $n - 1)); + my $tikzOpts = $self->get_plot_opts($data); + $tikzCode .= $self->get_color($fill_color) unless $fill eq 'none'; + $tikzCode .= $self->get_color($color) . "\\addplot[$tikzOpts] coordinates {$tikzData};\n"; + + unless ($fill eq 'none' || $fill eq 'self') { + my $opacity = $data->style('fill_opacity') || 0.5; + my $fill_range = $data->style('fill_range') || ''; + my $name = $data->style('name') || ''; + $opacity *= 100; + if ($fill_range) { + my ($min_fill, $max_fill) = split(',', $fill_range); + $fill_range = ", soft clip={domain=$min_fill:$max_fill}"; + } + $tikzCode .= "\\addplot[$fill_color!$opacity] fill between[of=$name and $fill$fill_range];\n"; + } + } + + # Stamps + for my $stamp ($pgplot->data('stamp')) { + my $mark = { + closed_circle => '*', + open_circle => 'o', + plus => '+', + times => 'x', + bar => '|', + dash => '-', + asterisk => 'asterisk', + star => 'star', + oplus => 'oplus', + otimes => 'otimes', + diamond => 'diamond', + none => '', + }->{ $stamp->style('symbol') }; + my $color = $stamp->style('color') || 'default_color'; + my $x = $stamp->x(0); + my $y = $stamp->y(0); + my $r = $stamp->style('radius') || 4; + $tikzCode .= $self->get_color($color) + . "\\addplot[$color, mark=$mark, mark size=${r}pt, only marks] coordinates {($x,$y)};\n"; + } + + # Labels + for my $label ($pgplot->data('label')) { + my $str = $label->style('label'); + my $x = $label->x(0); + my $y = $label->y(0); + my $color = $label->style('color') || 'default_color'; + my $fontsize = $label->style('fontsize') || 'medium'; + my $orientation = $label->style('orientation') || 'horizontal'; + my $tikzOpts = $label->style('tikzOpts') || ''; + my $h_align = $label->style('h_align') || 'center'; + my $v_align = $label->style('v_align') || 'middle'; + my $anchor = $v_align eq 'top' ? 'north' : $v_align eq 'bottom' ? 'south' : ''; + $str = { + tiny => '\tiny ', + small => '\small ', + medium => '', + large => '\large ', + giant => '\Large ', + }->{$fontsize} + . $str; + $anchor .= $h_align eq 'left' ? ' west' : $h_align eq 'right' ? ' east' : ''; + $tikzOpts = $tikzOpts ? "$color, $tikzOpts" : $color; + $tikzOpts = "anchor=$anchor, $tikzOpts" if $anchor; + $tikzOpts = "rotate=90, $tikzOpts" if $orientation eq 'vertical'; + $tikzCode .= $self->get_color($color) . "\\node[$tikzOpts] at (axis cs: $x,$y) {$str};\n"; + } + $tikzCode .= '\end{axis}' . "\n"; + + $pgplot->{tikzCode} = $tikzCode; + $self->im->tex($tikzCode); + return $pgplot->{tikzDebug} ? '' : $self->im->draw; +} + +# GD Output +package PGplot::GD; + +sub new { + my ($class, $pgplot) = @_; + my $self = { + image => '', + pgplot => $pgplot, + position => [ 0, 0 ], + colors => {}, + }; + bless $self, $class; + + $self->{image} = new GD::Image($pgplot->size); + return $self; +} + +sub pgplot { + my $self = shift; + return $self->{pgplot}; +} + +sub im { + my $self = shift; + return $self->{image}; +} + +sub position { + my ($self, $x, $y) = @_; + return wantarray ? @{ $self->{position} } : $self->{position} unless (defined($x) && defined($y)); + $self->{position} = [ $x, $y ]; + return; +} + +sub color { + my ($self, $color) = @_; + $self->{colors}{$color} = $self->im->colorAllocate(@{ $self->pgplot->colors($color) }) + unless $self->{colors}{$color}; + return $self->{colors}{$color}; +} + +# Translate x and y coordinates to pixels on the graph. +sub im_x { + my ($self, $x) = @_; + return unless defined($x); + my $pgplot = $self->pgplot; + my ($xmin, $xmax) = ($pgplot->axes->xaxis('min'), $pgplot->axes->xaxis('max')); + return int(($x - $xmin) * ($pgplot->size)[0] / ($xmax - $xmin)); +} + +sub im_y { + my ($self, $y) = @_; + return unless defined($y); + my $pgplot = $self->pgplot; + my ($ymin, $ymax) = ($pgplot->axes->yaxis('min'), $pgplot->axes->yaxis('max')); + return int(($ymax - $y) * ($pgplot->size)[1] / ($ymax - $ymin)); +} + +sub moveTo { + my ($self, $x, $y) = @_; + $x = $self->im_x($x); + $y = $self->im_y($y); + $self->position($x, $y); + return; +} + +sub lineTo { + my ($self, $x, $y, $color, $width, $dashed) = @_; + $color = 'default_color' unless defined($color); + $color = $self->color($color); + $width = 1 unless defined($width); + $dashed = 0 unless defined($dashed); + $x = $self->im_x($x); + $y = $self->im_y($y); + + $self->im->setThickness($width); + if ($dashed =~ /dash/) { + my @dashing = ($color) x (4 * $width * $width); + my @spacing = (GD::gdTransparent) x (3 * $width * $width); + $self->im->setStyle(@dashing, @spacing); + $self->im->line($self->position, $x, $y, GD::gdStyled); + } elsif ($dashed =~ /dot/) { + my @dashing = ($color) x (1 * $width * $width); + my @spacing = (GD::gdTransparent) x (2 * $width * $width); + $self->im->setStyle(@dashing, @spacing); + $self->im->line($self->position, $x, $y, GD::gdStyled); + } else { + $self->im->line($self->position, $x, $y, $color); + } + $self->im->setThickness(1); + $self->position($x, $y); + return; +} + +# Draw functions / lines / arrows +sub draw_data { + my ($self, $pass) = @_; + my $pgplot = $self->pgplot; + $pass = 0 unless $pass; + for my $data ($pgplot->data('function', 'dataset')) { + $data->gen_data; + my $n = $data->size - 1; + my $x = $data->x; + my $y = $data->y; + my $color = $data->style('color'); + my $width = $data->style('width'); + $self->moveTo($x->[0], $y->[0]); + for (1 .. $n) { + $self->lineTo($x->[$_], $y->[$_], $color, $width, $data->style('linestyle')); + } + + if ($pass == 2) { + my $r = int(3 + $width); + my $start = $data->style('start_mark') || 'none'; + if ($start eq 'closed_circle') { + $self->draw_circle_stamp($data->x(0), $data->y(0), $r, $color, 1); + } elsif ($start eq 'open_circle') { + $self->draw_circle_stamp($data->x(0), $data->y(0), $r, $color); + } elsif ($start eq 'arrow') { + $self->draw_arrow_head($data->x(1), $data->y(1), $data->x(0), $data->y(0), $color, $width); + } + + my $end = $data->style('end_mark') || 'none'; + if ($end eq 'closed_circle') { + $self->draw_circle_stamp($data->x($n), $data->y($n), $r, $color, 1); + } elsif ($end eq 'open_circle') { + $self->draw_circle_stamp($data->x($n), $data->y($n), $r, $color); + } elsif ($end eq 'arrow') { + $self->draw_arrow_head($data->x($n - 1), $data->y($n - 1), $data->x($n), $data->y($n), $color, $width); + } + } + } + return; +} + +# Label helpers +sub get_gd_font { + my ($self, $font) = @_; + if ($font eq 'tiny') { return GD::gdTinyFont; } + elsif ($font eq 'small') { return GD::gdSmallFont; } + elsif ($font eq 'large') { return GD::gdLargeFont; } + elsif ($font eq 'giant') { return GD::gdGiantFont; } + return GD::gdMediumBoldFont; +} + +sub label_offset { + my ($self, $loc, $str, $fontsize) = @_; + my $offset = 0; + # Add an additional 2px offset for the edges 'right', 'bottom', 'left', and 'top'. + if ($loc eq 'right') { $offset -= length($str) * $fontsize + 2; } + elsif ($loc eq 'bottom') { $offset -= $fontsize + 2; } + elsif ($loc eq 'center') { $offset -= length($str) * $fontsize / 2; } + elsif ($loc eq 'middle') { $offset -= $fontsize / 2; } + else { $offset = 2; } # Both 'left' and 'top'. + return $offset; +} + +sub draw_label { + my ($self, $str, $x, $y, %options) = @_; + my $font = $self->get_gd_font($options{fontsize} || 'medium'); + my $color = $self->color($options{color} || 'default_color'); + my $xoff = $self->label_offset($options{h_align} || 'center', $str, $font->width); + my $yoff = $self->label_offset($options{v_align} || 'middle', $str, $font->height); + + if ($options{orientation} && $options{orientation} eq 'vertical') { + $self->im->stringUp($font, $self->im_x($x) + $xoff, $self->im_y($y) + $yoff, $str, $color); + } else { + $self->im->string($font, $self->im_x($x) + $xoff, $self->im_y($y) + $yoff, $str, $color); + } + return; +} + +sub draw_arrow_head { + my ($self, $x1, $y1, $x2, $y2, $color, $w) = @_; + return unless scalar(@_) > 4; + $color = $self->color($color || 'default_color'); + $w = 1 unless $w; + ($x1, $y1) = ($self->im_x($x1), $self->im_y($y1)); + ($x2, $y2) = ($self->im_x($x2), $self->im_y($y2)); + + my $dx = $x2 - $x1; + my $dy = $y2 - $y1; + my $len = sqrt($dx * $dx + $dy * $dy); + my $ux = $dx / $len; # Unit vector in direction of arrow. + my $uy = $dy / $len; + my $px = -1 * $uy; # Unit vector perpendicular to arrow. + my $py = $ux; + my $hbx = $x2 - 7 * $w * $ux; + my $hby = $y2 - 7 * $w * $uy; + my $head = new GD::Polygon; + $head->addPt($x2, $y2); + $head->addPt($hbx + 3 * $w * $px, $hby + 3 * $w * $py); + $head->addPt($hbx - 3 * $w * $px, $hby - 3 * $w * $py); + $self->im->setThickness($w); + $self->im->filledPolygon($head, $color); + $self->im->setThickness(1); + return; +} + +sub draw_circle_stamp { + my ($self, $x, $y, $r, $color, $filled) = @_; + my $d = $r ? 2 * $r : 8; + $color = $self->color($color || 'default_color'); + $self->im->filledArc($self->im_x($x), $self->im_y($y), $d, $d, 0, 360, $self->color('nearwhite')); + $self->im->filledArc($self->im_x($x), $self->im_y($y), $d, $d, 0, 360, $color, $filled ? () : GD::gdNoFill); + return; +} + +sub draw { + my $self = shift; + my $pgplot = $self->pgplot; + my $axes = $pgplot->axes; + my $grid = $axes->grid; + my $size = $pgplot->size; + + # Initialize image + $self->im->interlaced('true'); + $self->im->fill(1, 1, $self->color('background_color')); + + # Plot data first, then fill in regions before adding axes, grid, etc. + $self->draw_data(1); + + # Fill regions + for my $region ($pgplot->data('fill_region')) { + $self->im->fill($self->im_x($region->x(0)), $self->im_y($region->y(0)), $self->color($region->style('color'))); + } + + # Gridlines + my ($xmin, $ymin, $xmax, $ymax) = $axes->bounds; + my $grid_color = $axes->style('grid_color'); + my $grid_style = $axes->style('grid_style'); + my $show_grid = $axes->style('show_grid'); + if ($show_grid && $grid->{xmajor}) { + my $xminor = $grid->{xminor} || 0; + my $prevx = $xmin; + my $dx = 0; + my $first = 1; + for my $x (@{ $grid->{xticks} }) { + # Number comparison of $dx and $x - $prevx failed in some tests, so using string comparison. + $xminor = 0 unless ($first || $dx == 0 || $dx eq $x - $prevx); + $dx = $x - $prevx unless $first; + $prevx = $x; + $first = 0; + $self->moveTo($x, $ymin); + $self->lineTo($x, $ymax, $grid_color, 0.5, 1); + } + if ($xminor) { + $dx /= ($xminor + 1); + for my $x (@{ $grid->{xticks} }) { + last if $x == $prevx; + for (1 .. $xminor) { + my $x2 = $x + $dx * $_; + $self->moveTo($x2, $ymin); + $self->lineTo($x2, $ymax, $grid_color, 0.5, 1); + } + } + } + } + if ($show_grid && $grid->{ymajor}) { + my $yminor = $grid->{yminor} || 0; + my $prevy; + my $dy = 0; + my $first = 1; + for my $y (@{ $grid->{yticks} }) { + # Number comparison of $dy and $y - $prevy failed in some tests, so using string comparison. + $yminor = 0 unless ($first || $dy == 0 || $dy eq $y - $prevy); + $dy = $y - $prevy unless $first; + $prevy = $y; + $first = 0; + $self->moveTo($xmin, $y); + $self->lineTo($xmax, $y, $grid_color, 0.5, 1); + } + if ($yminor) { + $dy /= ($yminor + 1); + for my $y (@{ $grid->{yticks} }) { + last if $y == $prevy; + for (1 .. $yminor) { + my $y2 = $y + $dy * $_; + $self->moveTo($xmin, $y2); + $self->lineTo($xmax, $y2, $grid_color, 0.5, 1); + } + } + } + } + + # Plot axes + my $show_x = $axes->xaxis('visible'); + my $show_y = $axes->yaxis('visible'); + my $xloc = $axes->xaxis('location') || 'middle'; + my $yloc = $axes->yaxis('location') || 'center'; + my $xpos = ($yloc eq 'box' || $yloc eq 'left') ? $xmin : $yloc eq 'right' ? $xmax : $axes->yaxis('position'); + my $ypos = ($xloc eq 'box' || $xloc eq 'bottom') ? $ymin : $xloc eq 'top' ? $ymax : $axes->xaxis('position'); + $xpos = $xmin if $xpos < $xmin; + $xpos = $xmax if $xpos > $xmax; + $ypos = $ymin if $ypos < $ymin; + $ypos = $ymax if $ypos > $ymax; + + if ($show_x) { + my $xlabel = $axes->xaxis('label') =~ s/\\[\(\[\)\]]//gr; + my $tick_align = ($self->im_y($ymin) - $self->im_y($ypos) < 5) ? 'bottom' : 'top'; + my $label_align = ($self->im_y($ypos) - $self->im_y($ymax) < 5) ? 'top' : 'bottom'; + my $label_loc = $yloc eq 'right' && ($xloc eq 'top' || $xloc eq 'bottom') ? $xmin : $xmax; + + $self->moveTo($xmin, $ypos); + $self->lineTo($xmax, $ypos, 'black', 1.5, 0); + $self->draw_label( + $xlabel, $label_loc, $ypos, + fontsize => 'large', + v_align => $label_align, + h_align => $label_loc == $xmin ? 'left' : 'right' + ); + for my $x (@{ $grid->{xticks} }) { + $self->draw_label($x, $x, $ypos, font => 'large', v_align => $tick_align, h_align => 'center') + unless ($x == $xpos && $show_y); + } + } + if ($axes->yaxis('visible')) { + my $ylabel = $axes->yaxis('label') =~ s/\\[\(\[\)\]]//gr; + my $tick_align = ($self->im_x($xpos) - $self->im_x($xmin) < 5) ? 'left' : 'right'; + my $label_align = ($self->im_x($xmax) - $self->im_x($xpos) < 5) ? 'right' : 'left'; + my $label_loc = ($yloc eq 'left' && $xloc eq 'top') || ($yloc eq 'right' && $xloc eq 'top') ? $ymin : $ymax; + + $self->moveTo($xpos, $ymin); + $self->lineTo($xpos, $ymax, 'black', 1.5, 0); + $self->draw_label( + $ylabel, $xpos, $label_loc, + fontsize => 'large', + v_align => $label_loc == $ymin ? 'bottom' : 'top', + h_align => $label_align + ); + for my $y (@{ $grid->{yticks} }) { + $self->draw_label($y, $xpos, $y, font => 'large', v_align => 'middle', h_align => $tick_align) + unless ($y == $ypos && $show_x); + } + } + + # Draw data a second time to cleanup any issues with the grid and axes. + $self->draw_data(2); + + # Print Labels + for my $label ($pgplot->data('label')) { + $self->draw_label($label->style('label'), $label->x(0), $label->y(0), %{ $label->style }); + } + + # Draw stamps + for my $stamp ($pgplot->data('stamp')) { + my $symbol = $stamp->style('symbol'); + my $color = $stamp->style('color'); + my $r = $stamp->style('radius') || 4; + if ($symbol eq 'closed_circle') { + $self->draw_circle_stamp($stamp->x(0), $stamp->y(0), $r, $color, 1); + } elsif ($symbol eq 'open_circle') { + $self->draw_circle_stamp($stamp->x(0), $stamp->y(0), $r, $color); + } + } + + # Put a black frame around the picture + $self->im->rectangle(0, 0, $size->[0] - 1, $size->[1] - 1, $self->color('black')); + + return $pgplot->ext eq 'gif' ? $self->im->gif : $self->im->png; +} + +=head1 AXES OBJECT + +This is a hash to store information about the axes (ticks, range, grid, etc) +with some helper methods. The hash is further split into three smaller hashes: + +=over 5 + +=item xaxis + +Hash of data for the horizontal axis. + +=item yaxis + +Hash of data for the vertical axis. + +=item styles + +Hash of data for options for the general axis. + +=back + +=head1 USAGE + +The axes object should be accessed through a PGplot object using C<$plot-Eaxes>. +The axes object is used to configure and retrieve information about the axes, +as in the following examples. + +Each axis can be configured individually, such as: + + $plot->axes->xaxis(min => -10, max => 10, ticks => [-12, -8, -4, 0, 4, 8, 12]); + $plot->axes->yaxis(min => 0, max => 100, ticks => [20, 40, 60, 80, 100]); + +This can also be combined using the set method, such as: + + $plot->axes->set( + xmin => -10, + xmax => 10, + xticks => [-12, -8, -4, 0, 4, 8, 12], + ymin => 0, + ymax => 100, + yticks => [20, 40, 60, 80, 100] + ); + +In addition to the configuration each axis, there is a set of styles that apply to both axes. +These are access via the style method. To set one or more styles use: + + $plot->axes->style(title => '\(y = f(x)\)', show_grid => 0); + +The same methods also get the value of a single option, such as: + + $xmin = $plot->axes->xaxis('min'); + $yticks = $plot->axes->yaxis('ticks'); + $title = $plot->axes->style('title'); + +The methods without any inputs return a reference to the full hash, such as: + + $xaxis = $plot->axes->xaxis; + $styles = $plot->axes->style; + +It is also possible to get multiple options for both axes using the get method, which returns +a reference to a hash of requested keys, such as: + + $bounds = $plot->axes->get('xmin', 'xmax', 'ymin', 'ymax'); + # The following is equivlant to $plot->axes->grid + $grid = $plot->axes->get('xmajor', 'xminor', 'xticks', 'ymajor', 'yminor', 'yticks'); + +It is also possible to get the bounds as an array in the order xmin, ymin, xmax, ymax +using the C<$plot-Eaxes-Ebounds> method. + +=head1 AXIS CONFIGURATION OPTIONS + +Each axis (the xaxis and yaxis) has the following configuration options: + +=over 5 + +=item min + +The minimum value the axis shows. Default is -5. + +=item max + +The maximum value the axis shows. Default is 5. + +=item ticks + +An array which lists the major tick marks. If this array is empty, the ticks are +generated using either C or C. Default is C<[]>. + +=item tick_delta + +This is the distance between each major tick mark, starting from the origin. +This distance is then used to generate the tick marks if the ticks array is empty. +If this is set to 0, this distance is set by using the number of ticks, C. +Default is 0. + +=item tick_num + +This is the number of major tick marks to include on the axis. This number is used +to compute the C as the difference between the C and C values +and the number of ticks. Default: 5. + +=item label + +The axis label. Defaults are C<\(x\)> and C<\(y\)>. + +=item major + +Show (1) or don't show (0) grid lines at the tick marks. Default is 1. + +=item minor + +This sets the number of minor grid lines per major grid line. If this is +set to 0, no minor grid lines are shown. Default is 3. + +=item visible + +This sets if the axis is shown (1) or not (0) on the plot. Default is 1. + +=item location + +This sets the location of the axes relative to the graph. The possible options +for each axis are: + + xaxis => 'box', 'top', 'middle', 'bottom' + yaxis => 'box', 'left', 'center', 'right' + +This places the axis at the appropriate edge of the graph. If 'center' or 'middle' +are used, the axes appear on the inside of the graph at the appropriate position. +Setting the location to 'box' creates a box or framed pot. Default 'middle' or 'center'. + +=item position + +The position in terms of the appropriate variable to draw the axis if the location is +set to 'middle' or 'center'. Default is 0. + +=back + +=head1 STYLES + +The following styles configure aspects about the axes: + +=over 5 + +=item title + +The title of the graph. Default is ''. + +=item show_grid + +Either draw (1) or don't draw (0) the grid lines for the axis. Default is 1. + +=item grid_color + +The color of the grid lines. Default is 'gray'. + +=item grid_style + +The line style of grid lines. This can be 'dashed', 'dotted', 'solid', etc. +Default is 'solid'. + +=item grid_alpha + +The alpha value to use to draw the grid lines in Tikz. This is a number from +0 (fully transparent) to 100 (fully solid). Default is 40. + +=item axis_on_top + +Configures if the axis should be drawn on top of the graph (1) or below the graph (0). +Useful when filling a region that covers an axis, if the axis are on top they will still +be visible after the fill, otherwise the fill will cover the axis. Default: 0 + +=back + +=cut + +package PGplot::Axes; + +sub new { + my $class = shift; + my $self = { + xaxis => {}, + yaxis => {}, + styles => { + title => '', + grid_color => 'gray', + grid_style => 'solid', + grid_alpha => 40, + show_grid => 1, + }, + @_ + }; + + bless $self, $class; + $self->xaxis($self->axis_defaults('x')); + $self->yaxis($self->axis_defaults('y')); + return $self; +} + +sub axis_defaults { + my ($self, $axis) = @_; + return ( + visible => 1, + min => -5, + max => 5, + label => $axis eq 'y' ? '\(y\)' : '\(x\)', + location => $axis eq 'y' ? 'center' : 'middle', + position => 0, + ticks => undef, + tick_delta => 0, + tick_num => 5, + major => 1, + minor => 3, + ); +} + +sub axis { + my ($self, $axis, @items) = @_; + return $self->{$axis} unless @items; + if (scalar(@items) > 1) { + my %item_hash = @items; + map { $self->{$axis}{$_} = $item_hash{$_}; } (keys %item_hash); + return; + } + my $item = $items[0]; + if (ref($item) eq 'HASH') { + map { $self->{$axis}{$_} = $item->{$_}; } (keys %$item); + return; + } + # Deal with ticks individually since they may need to be generated. + return $item eq 'ticks' ? $self->{$axis}{ticks} || $self->gen_ticks($self->axis($axis)) : $self->{$axis}{$item}; +} + +sub xaxis { + my $self = shift; + return $self->axis('xaxis', @_); +} + +sub yaxis { + my $self = shift; + return $self->axis('yaxis', @_); +} + +sub set { + my ($self, %options) = @_; + my (%xopts, %yopts); + for (keys %options) { + if ($_ =~ s/^x//) { + $xopts{$_} = $options{"x$_"}; + } elsif ($_ =~ s/^y//) { + $yopts{$_} = $options{"y$_"}; + } + } + $self->xaxis(%xopts) if %xopts; + $self->yaxis(%yopts) if %yopts; + return; +} + +sub get { + my ($self, @keys) = @_; + my %options; + for (@keys) { + if ($_ =~ s/^x//) { + $options{"x$_"} = $self->xaxis($_); + } elsif ($_ =~ s/^y//) { + $options{"y$_"} = $self->yaxis($_); + } + } + return \%options; +} + +sub style { + my ($self, @styles) = @_; + return $self->{styles} unless @styles; + if (scalar(@styles) > 1) { + my %style_hash = @styles; + map { $self->{styles}{$_} = $style_hash{$_}; } (keys %style_hash); + return; + } + my $style = $styles[0]; + if (ref($style) eq 'HASH') { + map { $self->{styles}{$_} = $style->{$_}; } (keys %$style); + return; + } + return $self->{styles}{$style}; +} + +sub gen_ticks { + my ($self, $axis) = @_; + my $min = $axis->{min}; + my $max = $axis->{max}; + my $delta = $axis->{tick_delta}; + $delta = ($max - $min) / $axis->{tick_num} unless $delta; + + my @ticks = $min <= 0 && $max >= 0 ? (0) : (); + my $point = $delta; + # Adjust min/max to place one more tick beyond the graph's edge. + $min -= $delta; + $max += $delta; + do { + push(@ticks, $point) unless $point < $min || $point > $max; + unshift(@ticks, -$point) unless -$point < $min || -$point > $max; + $point += $delta; + } until (-$point < $min && $point > $max); + return \@ticks; +} + +sub grid { + my $self = shift; + return $self->get('xmajor', 'xminor', 'xticks', 'ymajor', 'yminor', 'yticks'); +} + +sub bounds { + my $self = shift; + return $self->{xaxis}{min}, $self->{yaxis}{min}, $self->{xaxis}{max}, $self->{yaxis}{max}; +} + +=head1 DATA OBJECT + +This object holds data about the different types of elements that can be added +to a PGplot graph. This is a hash with some helper methods. Data objects are created +and modified using the PGplot methods, and do not need to generally be +modified in a PG problem. Each PG add method returns the related data object which +can be used if needed. + +Each data object contains the following: + +=over 5 + +=item name + +The name is used to identify what type of data is being stored, +such as a function, dataset, label, etc. + +=item x + +The array of the data points x-value. + +=item y + +The array of the data points y-value. + +=item function + +A function (stored as a hash) to generate the x and y data points. + +=item styles + +An hash of different style options and values that can be used +to store additional data for things like color, width, etc. + +=back + +=head1 USAGE + +The main methods for adding data and accessing the data are: + +=over 5 + +=item C<$data-Ename> + +Sets, C<$data-Ename($string)>, or gets C<$data-Ename> the name of the data object. + +=item C<$data-Eadd> + +Adds a single data point, C<$data-Eadd($x, $y)>, or adds multiple data points, +C<$data-Eadd([$x1, $y1], [$x2, $y2], ..., [$xn, $yn])>. + +=item C<$data-Eset_function> + +Configures a function to generate data points. C and C are are perl subroutines. + + $data->set_function( + sub_x => sub { return $_[0]; }, + sub_y => sub { return $_[0]**2; }, + min => -5, + max => 5, + ); + +The number of steps used to generate the data is a style and needs to be set separately. + + $data->style(steps => 50); + +=item C<$data-Egen_data> + +Generate the data points from a function. This can only be done when there is no data, so +once the data has been generated this will do nothing (to avoid generating data again). + +=item C<$data-Esize> + +Returns the current number of points being stored. + +=item C<$data-Ex> and C<$data-Ey> + +Without any inputs, these return either the x array or y array of data points being stored. +A single input can be used to return only the n-th data point, C<$data-Ex($n)>. + +=item C<$data-Estyle> + +Sets or gets style information. Use C<$data-Estyle($name)> to get the style value of a single +style name. C<$data-Estyle> will returns a reference to the full style hash. Last, input a hash +to add / change the styles. + + $data->style(color => 'blue', width => 3); + +=back + +=cut + +package PGplot::Data; + +sub new { + my $class = shift; + my $self = { + name => '', + x => [], + y => [], + function => {}, + styles => {}, + @_ + }; + + bless $self, $class; + return $self; +} + +sub name { + my ($self, $name) = @_; + return $self->{name} unless $name; + $self->{name} = $name; + return; +} + +sub size { + my $self = shift; + return scalar(@{ $self->{x} }); +} + +sub x { + my ($self, $n) = @_; + return $self->{x}->[$n] if (defined($n) && defined($self->{x}->[$n])); + return wantarray ? @{ $self->{x} } : $self->{x}; +} + +sub y { + my ($self, $n) = @_; + return $self->{y}[$n] if (defined($n) && defined($self->{y}[$n])); + return wantarray ? @{ $self->{y} } : $self->{y}; +} + +sub style { + my ($self, @styles) = @_; + return $self->{styles} unless @styles; + if (scalar(@styles) > 1) { + my %style_hash = @styles; + map { $self->{styles}{$_} = $style_hash{$_}; } (keys %style_hash); + return; + } + my $style = $styles[0]; + if (ref($style) eq 'HASH') { + map { $self->{styles}{$_} = $style->{$_}; } (keys %$style); + return; + } + return $self->{styles}{$style}; +} + +sub set_function { + my $self = shift; + $self->{function} = { + sub_x => sub { return $_[0]; }, + sub_y => sub { return $_[0]; }, + min => -5, + max => 5, + @_ + }; + $self->style(steps => $self->{function}{steps}) if $self->{funciton}{steps}; + return; +} + +sub _stepsize { + my $self = shift; + my $f = $self->{function}; + my $steps = $self->style('steps') || 20; + # Using MathObjects allows bounds like 2pi/3, e^2, et, etc. + $f->{min} = &main::Real($f->{min})->value if ($f->{min} =~ /[^\d\-\.]/); + $f->{max} = &main::Real($f->{max})->value if ($f->{max} =~ /[^\d\-\.]/); + return ($f->{max} - $f->{min}) / $steps; +} + +sub gen_data { + my $self = shift; + my $f = $self->{function}; + return if !$f || $self->size; + my $steps = $self->style('steps') || 20; + my $dt = $self->_stepsize; + my $t = $f->{min}; + for (0 .. $steps) { + $self->add(&{ $f->{sub_x} }($t), &{ $f->{sub_y} }($t)); + $t += $dt; + } + return; +} + +sub _add { + my ($self, $x, $y) = @_; + return unless defined($x) && defined($y); + push(@{ $self->{x} }, $x); + push(@{ $self->{y} }, $y); + return; +} + +sub add { + my $self = shift; + if (ref($_[0]) eq 'ARRAY') { + for (@_) { $self->_add(@$_); } + } else { + $self->_add(@_); + } + return; +} + 1; diff --git a/macros/graph/PGplot/Axes.pl b/macros/graph/PGplot/Axes.pl deleted file mode 100644 index 07c7b5ae90..0000000000 --- a/macros/graph/PGplot/Axes.pl +++ /dev/null @@ -1,342 +0,0 @@ -################################################################################ -# WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork -# -# This program is free software; you can redistribute it and/or modify it under -# the terms of either: (a) the GNU General Public License as published by the -# Free Software Foundation; either version 2, or (at your option) any later -# version, or (b) the "Artistic License" which comes with this package. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the -# Artistic License for more details. -################################################################################ - -=head1 NAME - -PGplot/Axes.pl - Object used with PGplot to store data about a plot's title and axes. - -=head1 DESCRIPTION - -This is a hash to store information about the axes (ticks, range, grid, etc) -with some helper methods. The hash is further split into three smaller hashes: - -=over 5 - -=item xaxis - -Hash of data for the horizontal axis. - -=item yaxis - -Hash of data for the vertical axis. - -=item styles - -Hash of data for options for the general axis. - -=back - -=head1 USAGE - -The axes object should be accessed through a L object using C<$plot-Eaxes>. -The axes object is used to configure and retrieve information about the axes, -as in the following examples. - -Each axis can be configured individually, such as: - - $plot->axes->xaxis(min => -10, max => 10, ticks => [-12, -8, -4, 0, 4, 8, 12]); - $plot->axes->yaxis(min => 0, max => 100, ticks => [20, 40, 60, 80, 100]); - -This can also be combined using the set method, such as: - - $plot->axes->set( - xmin => -10, - xmax => 10, - xticks => [-12, -8, -4, 0, 4, 8, 12], - ymin => 0, - ymax => 100, - yticks => [20, 40, 60, 80, 100] - ); - -In addition to the configuration each axis, there is a set of styles that apply to both axes. -These are access via the style method. To set one or more styles use: - - $plot->axes->style(title => '\(y = f(x)\)', show_grid => 0); - -The same methods also get the value of a single option, such as: - - $xmin = $plot->axes->xaxis('min'); - $yticks = $plot->axes->yaxis('ticks'); - $title = $plot->axes->style('title'); - -The methods without any inputs return a reference to the full hash, such as: - - $xaxis = $plot->axes->xaxis; - $styles = $plot->axes->style; - -It is also possible to get multiple options for both axes using the get method, which returns -a reference to a hash of requested keys, such as: - - $bounds = $plot->axes->get('xmin', 'xmax', 'ymin', 'ymax'); - # The following is equivlant to $plot->axes->grid - $grid = $plot->axes->get('xmajor', 'xminor', 'xticks', 'ymajor', 'yminor', 'yticks'); - -It is also possible to get the bounds as an array in the order xmin, ymin, xmax, ymax -using the C<$plot-Eaxes-Ebounds> method. - -=head1 AXIS CONFIGURATION OPTIONS - -Each axis (the xaxis and yaxis) has the following configuration options: - -=over 5 - -=item min - -The minimum value the axis shows. Default is -5. - -=item max - -The maximum value the axis shows. Default is 5. - -=item ticks - -An array which lists the major tick marks. If this array is empty, the ticks are -generated using either C or C. Default is C<[]>. - -=item tick_delta - -This is the distance between each major tick mark, starting from the origin. -This distance is then used to generate the tick marks if the ticks array is empty. -If this is set to 0, this distance is set by using the number of ticks, C. -Default is 0. - -=item tick_num - -This is the number of major tick marks to include on the axis. This number is used -to compute the C as the difference between the C and C values -and the number of ticks. Default: 5. - -=item label - -The axis label. Defaults are C<\(x\)> and C<\(y\)>. - -=item major - -Show (1) or don't show (0) grid lines at the tick marks. Default is 1. - -=item minor - -This sets the number of minor grid lines per major grid line. If this is -set to 0, no minor grid lines are shown. Default is 3. - -=item visible - -This sets if the axis is shown (1) or not (0) on the plot. Default is 1. - -=item location - -This sets the location of the axes relative to the graph. The possible options -for each axis are: - - xaxis => 'box', 'top', 'middle', 'bottom' - yaxis => 'box', 'left', 'center', 'right' - -This places the axis at the appropriate edge of the graph. If 'center' or 'middle' -are used, the axes appear on the inside of the graph at the appropriate position. -Setting the location to 'box' creates a box or framed pot. Default 'middle' or 'center'. - -=item position - -The position in terms of the appropriate variable to draw the axis if the location is -set to 'middle' or 'center'. Default is 0. - -=back - -=head1 STYLES - -The following styles configure aspects about the axes: - -=over 5 - -=item title - -The title of the graph. Default is ''. - -=item show_grid - -Either draw (1) or don't draw (0) the grid lines for the axis. Default is 1. - -=item grid_color - -The color of the grid lines. Default is 'gray'. - -=item grid_style - -The line style of grid lines. This can be 'dashed', 'dotted', 'solid', etc. -Default is 'solid'. - -=item grid_alpha - -The alpha value to use to draw the grid lines in Tikz. This is a number from -0 (fully transparent) to 100 (fully solid). Default is 40. - -=item axis_on_top - -Configures if the axis should be drawn on top of the graph (1) or below the graph (0). -Useful when filling a region that covers an axis, if the axis are on top they will still -be visible after the fill, otherwise the fill will cover the axis. Default: 0 - -=back - -=cut - -BEGIN { - strict->import; -} - -sub _Axes_init { } - -package PGplot::Axes; - -sub new { - my $class = shift; - my $self = { - xaxis => {}, - yaxis => {}, - styles => { - title => '', - grid_color => 'gray', - grid_style => 'solid', - grid_alpha => 40, - show_grid => 1, - }, - @_ - }; - - bless $self, $class; - $self->xaxis($self->axis_defaults('x')); - $self->yaxis($self->axis_defaults('y')); - return $self; -} - -sub axis_defaults { - my ($self, $axis) = @_; - return ( - visible => 1, - min => -5, - max => 5, - label => $axis eq 'y' ? '\(y\)' : '\(x\)', - location => $axis eq 'y' ? 'center' : 'middle', - position => 0, - ticks => undef, - tick_delta => 0, - tick_num => 5, - major => 1, - minor => 3, - ); -} - -sub axis { - my ($self, $axis, @items) = @_; - return $self->{$axis} unless @items; - if (scalar(@items) > 1) { - my %item_hash = @items; - map { $self->{$axis}{$_} = $item_hash{$_}; } (keys %item_hash); - return; - } - my $item = $items[0]; - if (ref($item) eq 'HASH') { - map { $self->{$axis}{$_} = $item->{$_}; } (keys %$item); - return; - } - # Deal with ticks individually since they may need to be generated. - return $item eq 'ticks' ? $self->{$axis}{ticks} || $self->gen_ticks($self->axis($axis)) : $self->{$axis}{$item}; -} - -sub xaxis { - my $self = shift; - return $self->axis('xaxis', @_); -} - -sub yaxis { - my $self = shift; - return $self->axis('yaxis', @_); -} - -sub set { - my ($self, %options) = @_; - my (%xopts, %yopts); - for (keys %options) { - if ($_ =~ s/^x//) { - $xopts{$_} = $options{"x$_"}; - } elsif ($_ =~ s/^y//) { - $yopts{$_} = $options{"y$_"}; - } - } - $self->xaxis(%xopts) if %xopts; - $self->yaxis(%yopts) if %yopts; - return; -} - -sub get { - my ($self, @keys) = @_; - my %options; - for (@keys) { - if ($_ =~ s/^x//) { - $options{"x$_"} = $self->xaxis($_); - } elsif ($_ =~ s/^y//) { - $options{"y$_"} = $self->yaxis($_); - } - } - return \%options; -} - -sub style { - my ($self, @styles) = @_; - return $self->{styles} unless @styles; - if (scalar(@styles) > 1) { - my %style_hash = @styles; - map { $self->{styles}{$_} = $style_hash{$_}; } (keys %style_hash); - return; - } - my $style = $styles[0]; - if (ref($style) eq 'HASH') { - map { $self->{styles}{$_} = $style->{$_}; } (keys %$style); - return; - } - return $self->{styles}{$style}; -} - -sub gen_ticks { - my ($self, $axis) = @_; - my $min = $axis->{min}; - my $max = $axis->{max}; - my $delta = $axis->{tick_delta}; - $delta = ($max - $min) / $axis->{tick_num} unless $delta; - - my @ticks = $min <= 0 && $max >= 0 ? (0) : (); - my $point = $delta; - # Adjust min/max to place one more tick beyond the graph's edge. - $min -= $delta; - $max += $delta; - do { - push(@ticks, $point) unless $point < $min || $point > $max; - unshift(@ticks, -$point) unless -$point < $min || -$point > $max; - $point += $delta; - } until (-$point < $min && $point > $max); - return \@ticks; -} - -sub grid { - my $self = shift; - return $self->get('xmajor', 'xminor', 'xticks', 'ymajor', 'yminor', 'yticks'); -} - -sub bounds { - my $self = shift; - return $self->{xaxis}{min}, $self->{yaxis}{min}, $self->{xaxis}{max}, $self->{yaxis}{max}; -} - -1; diff --git a/macros/graph/PGplot/Data.pl b/macros/graph/PGplot/Data.pl deleted file mode 100644 index f261748e7f..0000000000 --- a/macros/graph/PGplot/Data.pl +++ /dev/null @@ -1,230 +0,0 @@ -################################################################################ -# WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork -# -# This program is free software; you can redistribute it and/or modify it under -# the terms of either: (a) the GNU General Public License as published by the -# Free Software Foundation; either version 2, or (at your option) any later -# version, or (b) the "Artistic License" which comes with this package. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the -# Artistic License for more details. -################################################################################ - -=head1 NAME - -Data.pl - Base data class for PGplot elements (functions, labels, etc). - -=head1 DESCRIPTION - -This is a data class to hold data about the different types of elements -that can be added to a PGplot graph. This is a hash with some helper methods. -Data objects are created and modified using the L methods, -and do not need to generally be modified in a PG problem. Each PG add method -returns the related data object which can be used if needed. - -Each data object contains the following: - -=over 5 - -=item name - -The name is used to identify what type of data is being stored, -such as a function, dataset, label, etc. - -=item x - -The array of the data points x-value. - -=item y - -The array of the data points y-value. - -=item function - -A function (stored as a hash) to generate the x and y data points. - -=item styles - -An hash of different style options and values that can be used -to store additional data for things like color, width, etc. - -=back - -=head1 USAGE - -The main methods for adding data and accessing the data are: - -=over 5 - -=item C<$data-Ename> - -Sets, C<$data-Ename($string)>, or gets C<$data-Ename> the name of the data object. - -=item C<$data-Eadd> - -Adds a single data point, C<$data-Eadd($x, $y)>, or adds multiple data points, -C<$data-Eadd([$x1, $y1], [$x2, $y2], ..., [$xn, $yn])>. - -=item C<$data-Eset_function> - -Configures a function to generate data points. C and C are are perl subroutines. - - $data->set_function( - sub_x => sub { return $_[0]; }, - sub_y => sub { return $_[0]**2; }, - min => -5, - max => 5, - ); - -The number of steps used to generate the data is a style and needs to be set separately. - - $data->style(steps => 50); - -=item C<$data-Egen_data> - -Generate the data points from a function. This can only be done when there is no data, so -once the data has been generated this will do nothing (to avoid generating data again). - -=item C<$data-Esize> - -Returns the current number of points being stored. - -=item C<$data-Ex> and C<$data-Ey> - -Without any inputs, these return either the x array or y array of data points being stored. -A single input can be used to return only the n-th data point, C<$data-Ex($n)>. - -=item C<$data-Estyle> - -Sets or gets style information. Use C<$data-Estyle($name)> to get the style value of a single -style name. C<$data-Estyle> will returns a reference to the full style hash. Last, input a hash -to add / change the styles. - - $data->style(color => 'blue', width => 3); - -=back - -=cut - -BEGIN { - strict->import; -} - -sub _Data_init { } - -package PGplot::Data; - -sub new { - my $class = shift; - my $self = { - name => '', - x => [], - y => [], - function => {}, - styles => {}, - @_ - }; - - bless $self, $class; - return $self; -} - -sub name { - my ($self, $name) = @_; - return $self->{name} unless $name; - $self->{name} = $name; - return; -} - -sub size { - my $self = shift; - return scalar(@{ $self->{x} }); -} - -sub x { - my ($self, $n) = @_; - return $self->{x}->[$n] if (defined($n) && defined($self->{x}->[$n])); - return wantarray ? @{ $self->{x} } : $self->{x}; -} - -sub y { - my ($self, $n) = @_; - return $self->{y}[$n] if (defined($n) && defined($self->{y}[$n])); - return wantarray ? @{ $self->{y} } : $self->{y}; -} - -sub style { - my ($self, @styles) = @_; - return $self->{styles} unless @styles; - if (scalar(@styles) > 1) { - my %style_hash = @styles; - map { $self->{styles}{$_} = $style_hash{$_}; } (keys %style_hash); - return; - } - my $style = $styles[0]; - if (ref($style) eq 'HASH') { - map { $self->{styles}{$_} = $style->{$_}; } (keys %$style); - return; - } - return $self->{styles}{$style}; -} - -sub set_function { - my $self = shift; - $self->{function} = { - sub_x => sub { return $_[0]; }, - sub_y => sub { return $_[0]; }, - min => -5, - max => 5, - @_ - }; - $self->style(steps => $self->{function}{steps}) if $self->{funciton}{steps}; - return; -} - -sub _stepsize { - my $self = shift; - my $f = $self->{function}; - my $steps = $self->style('steps') || 20; - # Using MathObjects allows bounds like 2pi/3, e^2, et, etc. - $f->{min} = &main::Real($f->{min})->value if ($f->{min} =~ /[^\d\-\.]/); - $f->{max} = &main::Real($f->{max})->value if ($f->{max} =~ /[^\d\-\.]/); - return ($f->{max} - $f->{min}) / $steps; -} - -sub gen_data { - my $self = shift; - my $f = $self->{function}; - return if !$f || $self->size; - my $steps = $self->style('steps') || 20; - my $dt = $self->_stepsize; - my $t = $f->{min}; - for (0 .. $steps) { - $self->add(&{ $f->{sub_x} }($t), &{ $f->{sub_y} }($t)); - $t += $dt; - } - return; -} - -sub _add { - my ($self, $x, $y) = @_; - return unless defined($x) && defined($y); - push(@{ $self->{x} }, $x); - push(@{ $self->{y} }, $y); - return; -} - -sub add { - my $self = shift; - if (ref($_[0]) eq 'ARRAY') { - for (@_) { $self->_add(@$_); } - } else { - $self->_add(@_); - } - return; -} - -1; diff --git a/macros/graph/PGplot/GD.pl b/macros/graph/PGplot/GD.pl deleted file mode 100644 index c6b6a2bc2d..0000000000 --- a/macros/graph/PGplot/GD.pl +++ /dev/null @@ -1,383 +0,0 @@ -################################################################################ -# WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork -# -# This program is free software; you can redistribute it and/or modify it under -# the terms of either: (a) the GNU General Public License as published by the -# Free Software Foundation; either version 2, or (at your option) any later -# version, or (b) the "Artistic License" which comes with this package. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the -# Artistic License for more details. -################################################################################ - -BEGIN { - strict->import; -} - -sub _GD_init { } - -package PGplot::GD; - -sub new { - my ($class, $pgplot) = @_; - my $self = { - image => '', - pgplot => $pgplot, - position => [ 0, 0 ], - colors => {}, - }; - bless $self, $class; - - $self->{image} = new GD::Image($pgplot->size); - return $self; -} - -sub pgplot { - my $self = shift; - return $self->{pgplot}; -} - -sub im { - my $self = shift; - return $self->{image}; -} - -sub position { - my ($self, $x, $y) = @_; - return wantarray ? @{ $self->{position} } : $self->{position} unless (defined($x) && defined($y)); - $self->{position} = [ $x, $y ]; - return; -} - -sub color { - my ($self, $color) = @_; - $self->{colors}{$color} = $self->im->colorAllocate(@{ $self->pgplot->colors($color) }) - unless $self->{colors}{$color}; - return $self->{colors}{$color}; -} - -# Translate x and y coordinates to pixels on the graph. -sub im_x { - my ($self, $x) = @_; - return unless defined($x); - my $pgplot = $self->pgplot; - my ($xmin, $xmax) = ($pgplot->axes->xaxis('min'), $pgplot->axes->xaxis('max')); - return int(($x - $xmin) * ($pgplot->size)[0] / ($xmax - $xmin)); -} - -sub im_y { - my ($self, $y) = @_; - return unless defined($y); - my $pgplot = $self->pgplot; - my ($ymin, $ymax) = ($pgplot->axes->yaxis('min'), $pgplot->axes->yaxis('max')); - return int(($ymax - $y) * ($pgplot->size)[1] / ($ymax - $ymin)); -} - -sub moveTo { - my ($self, $x, $y) = @_; - $x = $self->im_x($x); - $y = $self->im_y($y); - $self->position($x, $y); - return; -} - -sub lineTo { - my ($self, $x, $y, $color, $width, $dashed) = @_; - $color = 'default_color' unless defined($color); - $color = $self->color($color); - $width = 1 unless defined($width); - $dashed = 0 unless defined($dashed); - $x = $self->im_x($x); - $y = $self->im_y($y); - - $self->im->setThickness($width); - if ($dashed =~ /dash/) { - my @dashing = ($color) x (4 * $width * $width); - my @spacing = (GD::gdTransparent) x (3 * $width * $width); - $self->im->setStyle(@dashing, @spacing); - $self->im->line($self->position, $x, $y, GD::gdStyled); - } elsif ($dashed =~ /dot/) { - my @dashing = ($color) x (1 * $width * $width); - my @spacing = (GD::gdTransparent) x (2 * $width * $width); - $self->im->setStyle(@dashing, @spacing); - $self->im->line($self->position, $x, $y, GD::gdStyled); - } else { - $self->im->line($self->position, $x, $y, $color); - } - $self->im->setThickness(1); - $self->position($x, $y); - return; -} - -# Draw functions / lines / arrows -sub draw_data { - my ($self, $pass) = @_; - my $pgplot = $self->pgplot; - $pass = 0 unless $pass; - for my $data ($pgplot->data('function', 'dataset')) { - $data->gen_data; - my $n = $data->size - 1; - my $x = $data->x; - my $y = $data->y; - my $color = $data->style('color'); - my $width = $data->style('width'); - $self->moveTo($x->[0], $y->[0]); - for (1 .. $n) { - $self->lineTo($x->[$_], $y->[$_], $color, $width, $data->style('linestyle')); - } - - if ($pass == 2) { - my $r = int(3 + $width); - my $start = $data->style('start_mark') || 'none'; - if ($start eq 'closed_circle') { - $self->draw_circle_stamp($data->x(0), $data->y(0), $r, $color, 1); - } elsif ($start eq 'open_circle') { - $self->draw_circle_stamp($data->x(0), $data->y(0), $r, $color); - } elsif ($start eq 'arrow') { - $self->draw_arrow_head($data->x(1), $data->y(1), $data->x(0), $data->y(0), $color, $width); - } - - my $end = $data->style('end_mark') || 'none'; - if ($end eq 'closed_circle') { - $self->draw_circle_stamp($data->x($n), $data->y($n), $r, $color, 1); - } elsif ($end eq 'open_circle') { - $self->draw_circle_stamp($data->x($n), $data->y($n), $r, $color); - } elsif ($end eq 'arrow') { - $self->draw_arrow_head($data->x($n - 1), $data->y($n - 1), $data->x($n), $data->y($n), $color, $width); - } - } - } - return; -} - -# Label helpers -sub get_gd_font { - my ($self, $font) = @_; - if ($font eq 'tiny') { return GD::gdTinyFont; } - elsif ($font eq 'small') { return GD::gdSmallFont; } - elsif ($font eq 'large') { return GD::gdLargeFont; } - elsif ($font eq 'giant') { return GD::gdGiantFont; } - return GD::gdMediumBoldFont; -} - -sub label_offset { - my ($self, $loc, $str, $fontsize) = @_; - my $offset = 0; - # Add an additional 2px offset for the edges 'right', 'bottom', 'left', and 'top'. - if ($loc eq 'right') { $offset -= length($str) * $fontsize + 2; } - elsif ($loc eq 'bottom') { $offset -= $fontsize + 2; } - elsif ($loc eq 'center') { $offset -= length($str) * $fontsize / 2; } - elsif ($loc eq 'middle') { $offset -= $fontsize / 2; } - else { $offset = 2; } # Both 'left' and 'top'. - return $offset; -} - -sub draw_label { - my ($self, $str, $x, $y, %options) = @_; - my $font = $self->get_gd_font($options{fontsize} || 'medium'); - my $color = $self->color($options{color} || 'default_color'); - my $xoff = $self->label_offset($options{h_align} || 'center', $str, $font->width); - my $yoff = $self->label_offset($options{v_align} || 'middle', $str, $font->height); - - if ($options{orientation} && $options{orientation} eq 'vertical') { - $self->im->stringUp($font, $self->im_x($x) + $xoff, $self->im_y($y) + $yoff, $str, $color); - } else { - $self->im->string($font, $self->im_x($x) + $xoff, $self->im_y($y) + $yoff, $str, $color); - } - return; -} - -sub draw_arrow_head { - my ($self, $x1, $y1, $x2, $y2, $color, $w) = @_; - return unless scalar(@_) > 4; - $color = $self->color($color || 'default_color'); - $w = 1 unless $w; - ($x1, $y1) = ($self->im_x($x1), $self->im_y($y1)); - ($x2, $y2) = ($self->im_x($x2), $self->im_y($y2)); - - my $dx = $x2 - $x1; - my $dy = $y2 - $y1; - my $len = sqrt($dx * $dx + $dy * $dy); - my $ux = $dx / $len; # Unit vector in direction of arrow. - my $uy = $dy / $len; - my $px = -1 * $uy; # Unit vector perpendicular to arrow. - my $py = $ux; - my $hbx = $x2 - 7 * $w * $ux; - my $hby = $y2 - 7 * $w * $uy; - my $head = new GD::Polygon; - $head->addPt($x2, $y2); - $head->addPt($hbx + 3 * $w * $px, $hby + 3 * $w * $py); - $head->addPt($hbx - 3 * $w * $px, $hby - 3 * $w * $py); - $self->im->setThickness($w); - $self->im->filledPolygon($head, $color); - $self->im->setThickness(1); - return; -} - -sub draw_circle_stamp { - my ($self, $x, $y, $r, $color, $filled) = @_; - my $d = $r ? 2 * $r : 8; - $color = $self->color($color || 'default_color'); - $self->im->filledArc($self->im_x($x), $self->im_y($y), $d, $d, 0, 360, $self->color('nearwhite')); - $self->im->filledArc($self->im_x($x), $self->im_y($y), $d, $d, 0, 360, $color, $filled ? () : GD::gdNoFill); - return; -} - -sub draw { - my $self = shift; - my $pgplot = $self->pgplot; - my $axes = $pgplot->axes; - my $grid = $axes->grid; - my $size = $pgplot->size; - - # Initialize image - $self->im->interlaced('true'); - $self->im->fill(1, 1, $self->color('background_color')); - - # Plot data first, then fill in regions before adding axes, grid, etc. - $self->draw_data(1); - - # Fill regions - for my $region ($pgplot->data('fill_region')) { - $self->im->fill($self->im_x($region->x(0)), $self->im_y($region->y(0)), $self->color($region->style('color'))); - } - - # Gridlines - my ($xmin, $ymin, $xmax, $ymax) = $axes->bounds; - my $grid_color = $axes->style('grid_color'); - my $grid_style = $axes->style('grid_style'); - my $show_grid = $axes->style('show_grid'); - if ($show_grid && $grid->{xmajor}) { - my $xminor = $grid->{xminor} || 0; - my $prevx = $xmin; - my $dx = 0; - my $first = 1; - for my $x (@{ $grid->{xticks} }) { - # Number comparison of $dx and $x - $prevx failed in some tests, so using string comparison. - $xminor = 0 unless ($first || $dx == 0 || $dx eq $x - $prevx); - $dx = $x - $prevx unless $first; - $prevx = $x; - $first = 0; - $self->moveTo($x, $ymin); - $self->lineTo($x, $ymax, $grid_color, 0.5, 1); - } - if ($xminor) { - $dx /= ($xminor + 1); - for my $x (@{ $grid->{xticks} }) { - last if $x == $prevx; - for (1 .. $xminor) { - my $x2 = $x + $dx * $_; - $self->moveTo($x2, $ymin); - $self->lineTo($x2, $ymax, $grid_color, 0.5, 1); - } - } - } - } - if ($show_grid && $grid->{ymajor}) { - my $yminor = $grid->{yminor} || 0; - my $prevy; - my $dy = 0; - my $first = 1; - for my $y (@{ $grid->{yticks} }) { - # Number comparison of $dy and $y - $prevy failed in some tests, so using string comparison. - $yminor = 0 unless ($first || $dy == 0 || $dy eq $y - $prevy); - $dy = $y - $prevy unless $first; - $prevy = $y; - $first = 0; - $self->moveTo($xmin, $y); - $self->lineTo($xmax, $y, $grid_color, 0.5, 1); - } - if ($yminor) { - $dy /= ($yminor + 1); - for my $y (@{ $grid->{yticks} }) { - last if $y == $prevy; - for (1 .. $yminor) { - my $y2 = $y + $dy * $_; - $self->moveTo($xmin, $y2); - $self->lineTo($xmax, $y2, $grid_color, 0.5, 1); - } - } - } - } - - # Plot axes - my $show_x = $axes->xaxis('visible'); - my $show_y = $axes->yaxis('visible'); - my $xloc = $axes->xaxis('location') || 'middle'; - my $yloc = $axes->yaxis('location') || 'center'; - my $xpos = ($yloc eq 'box' || $yloc eq 'left') ? $xmin : $yloc eq 'right' ? $xmax : $axes->yaxis('position'); - my $ypos = ($xloc eq 'box' || $xloc eq 'bottom') ? $ymin : $xloc eq 'top' ? $ymax : $axes->xaxis('position'); - $xpos = $xmin if $xpos < $xmin; - $xpos = $xmax if $xpos > $xmax; - $ypos = $ymin if $ypos < $ymin; - $ypos = $ymax if $ypos > $ymax; - - if ($show_x) { - my $xlabel = $axes->xaxis('label') =~ s/\\[\(\[\)\]]//gr; - my $tick_align = ($self->im_y($ymin) - $self->im_y($ypos) < 5) ? 'bottom' : 'top'; - my $label_align = ($self->im_y($ypos) - $self->im_y($ymax) < 5) ? 'top' : 'bottom'; - my $label_loc = $yloc eq 'right' && ($xloc eq 'top' || $xloc eq 'bottom') ? $xmin : $xmax; - - $self->moveTo($xmin, $ypos); - $self->lineTo($xmax, $ypos, 'black', 1.5, 0); - $self->draw_label( - $xlabel, $label_loc, $ypos, - fontsize => 'large', - v_align => $label_align, - h_align => $label_loc == $xmin ? 'left' : 'right' - ); - for my $x (@{ $grid->{xticks} }) { - $self->draw_label($x, $x, $ypos, font => 'large', v_align => $tick_align, h_align => 'center') - unless ($x == $xpos && $show_y); - } - } - if ($axes->yaxis('visible')) { - my $ylabel = $axes->yaxis('label') =~ s/\\[\(\[\)\]]//gr; - my $tick_align = ($self->im_x($xpos) - $self->im_x($xmin) < 5) ? 'left' : 'right'; - my $label_align = ($self->im_x($xmax) - $self->im_x($xpos) < 5) ? 'right' : 'left'; - my $label_loc = ($yloc eq 'left' && $xloc eq 'top') || ($yloc eq 'right' && $xloc eq 'top') ? $ymin : $ymax; - - $self->moveTo($xpos, $ymin); - $self->lineTo($xpos, $ymax, 'black', 1.5, 0); - $self->draw_label( - $ylabel, $xpos, $label_loc, - fontsize => 'large', - v_align => $label_loc == $ymin ? 'bottom' : 'top', - h_align => $label_align - ); - for my $y (@{ $grid->{yticks} }) { - $self->draw_label($y, $xpos, $y, font => 'large', v_align => 'middle', h_align => $tick_align) - unless ($y == $ypos && $show_x); - } - } - - # Draw data a second time to cleanup any issues with the grid and axes. - $self->draw_data(2); - - # Print Labels - for my $label ($pgplot->data('label')) { - $self->draw_label($label->style('label'), $label->x(0), $label->y(0), %{ $label->style }); - } - - # Draw stamps - for my $stamp ($pgplot->data('stamp')) { - my $symbol = $stamp->style('symbol'); - my $color = $stamp->style('color'); - my $r = $stamp->style('radius') || 4; - if ($symbol eq 'closed_circle') { - $self->draw_circle_stamp($stamp->x(0), $stamp->y(0), $r, $color, 1); - } elsif ($symbol eq 'open_circle') { - $self->draw_circle_stamp($stamp->x(0), $stamp->y(0), $r, $color); - } - } - - # Put a black frame around the picture - $self->im->rectangle(0, 0, $size->[0] - 1, $size->[1] - 1, $self->color('black')); - - return $pgplot->ext eq 'gif' ? $self->im->gif : $self->im->png; -} - -1; diff --git a/macros/graph/PGplot/Tikz.pl b/macros/graph/PGplot/Tikz.pl deleted file mode 100644 index 7952b43b20..0000000000 --- a/macros/graph/PGplot/Tikz.pl +++ /dev/null @@ -1,289 +0,0 @@ -################################################################################ -# WeBWorK Online Homework Delivery System -# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork -# -# This program is free software; you can redistribute it and/or modify it under -# the terms of either: (a) the GNU General Public License as published by the -# Free Software Foundation; either version 2, or (at your option) any later -# version, or (b) the "Artistic License" which comes with this package. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the -# Artistic License for more details. -################################################################################ - -BEGIN { - strict->import; -} - -sub _Tikz_init { } - -package PGplot::Tikz; - -sub new { - my ($class, $pgplot) = @_; - my $image = new LaTeXImage; - $image->environment('tikzpicture'); - $image->svgMethod($main::envir{latexImageSVGMethod} // 'pdf2svg'); - $image->convertOptions($main::envir{latexImageConvertOptions} // { input => {}, output => {} }); - $image->ext($pgplot->ext); - $image->tikzLibraries('arrows.meta'); - $image->texPackages(['pgfplots']); - $image->addToPreamble('\pgfplotsset{compat=1.18}\usepgfplotslibrary{fillbetween}'); - - my $self = { - image => $image, - pgplot => $pgplot, - colors => {}, - }; - bless $self, $class; - - return $self; -} - -sub pgplot { - my $self = shift; - return $self->{pgplot}; -} - -sub im { - my $self = shift; - return $self->{image}; -} - -sub get_color { - my ($self, $color) = @_; - return '' if $self->{colors}{$color}; - my ($r, $g, $b) = @{ $self->pgplot->colors($color) }; - $self->{colors}{$color} = 1; - return "\\definecolor{$color}{RGB}{$r,$g,$b}\n"; -} - -sub configure_axes { - my $self = shift; - my $pgplot = $self->pgplot; - my $axes = $pgplot->axes; - my $grid = $axes->grid; - my ($xmin, $ymin, $xmax, $ymax) = $axes->bounds; - my ($axes_height, $axes_width) = $pgplot->size; - my $show_grid = $axes->style('show_grid'); - my $xmajor = $show_grid && $grid->{xmajor} ? 'true' : 'false'; - my $xminor_num = $show_grid && $grid->{xmajor} ? $grid->{xminor} : 0; - my $xminor = $xminor_num > 0 ? 'true' : 'false'; - my $ymajor = $show_grid && $grid->{ymajor} ? 'true' : 'false'; - my $yminor_num = $show_grid && $grid->{ymajor} ? $grid->{yminor} : 0; - my $yminor = $yminor_num > 0 ? 'true' : 'false'; - my $xticks = join(',', @{ $grid->{xticks} }); - my $yticks = join(',', @{ $grid->{yticks} }); - my $grid_color = $axes->style('grid_color'); - my $grid_color2 = $self->get_color($grid_color); - my $grid_alpha = $axes->style('grid_alpha'); - my $grid_style = $axes->style('grid_style'); - my $xlabel = $axes->xaxis('label'); - my $axis_x_line = $axes->xaxis('location'); - my $ylabel = $axes->yaxis('label'); - my $axis_y_line = $axes->yaxis('location'); - my $title = $axes->style('title'); - my $axis_on_top = $axes->style('axis_on_top') ? "axis on top,\n\t\t\t" : ''; - my $hide_x_axis = ''; - my $hide_y_axis = ''; - my $xaxis_plot = ($xmin <= 0 && $xmax >= 0) ? "\\path[name path=xaxis] ($xmin, 0) -- ($xmax,0);\n" : ''; - - unless ($axes->xaxis('visible')) { - $xlabel = ''; - $hide_x_axis = - "\n\t\t\tx axis line style={draw=none},\n" - . "\t\t\tx tick style={draw=none},\n" - . "\t\t\txticklabel=\\empty,"; - } - unless ($axes->yaxis('visible')) { - $ylabel = ''; - $hide_y_axis = - "\n\t\t\ty axis line style={draw=none},\n" - . "\t\t\ty tick style={draw=none},\n" - . "\t\t\tyticklabel=\\empty,"; - } - my $tikzCode = <style('color') || 'default_color'; - my $width = $data->style('width') || 1; - my $linestyle = $data->style('linestyle') || 'solid'; - my $marks = $data->style('marks') || 'none'; - my $mark_size = $data->style('mark_size') || 0; - my $start = $data->style('start_mark') || 'none'; - my $end = $data->style('end_mark') || 'none'; - my $name = $data->style('name') || ''; - my $fill = $data->style('fill') || 'none'; - my $fill_color = $data->style('fill_color') || 'default_color'; - my $fill_opacity = $data->style('fill_opacity') || 0.5; - my $tikzOpts = $data->style('tikzOpts') || ''; - - if ($start =~ /circle/) { - $start = '{Circle[sep=-1.196825pt -1.595769' . ($start eq 'open_circle' ? ', open' : '') . ']}'; - } elsif ($start eq 'arrow') { - $start = '{Latex}'; - } else { - $start = ''; - } - if ($end =~ /circle/) { - $end = '{Circle[sep=-1.196825pt -1.595769' . ($end eq 'open_circle' ? ', open' : '') . ']}'; - } elsif ($end eq 'arrow') { - $end = '{Latex}'; - } else { - $end = ''; - } - my $end_markers = ($start || $end) ? ", $start-$end" : ''; - $marks = { - closed_circle => '*', - open_circle => 'o', - plus => '+', - times => 'x', - bar => '|', - dash => '-', - asterisk => 'asterisk', - star => 'star', - oplus => 'oplus', - otimes => 'otimes', - diamond => 'diamond', - none => '', - }->{$marks}; - $marks = $marks ? $mark_size ? ", mark=$marks, mark size=${mark_size}px" : ", mark=$marks" : ''; - $linestyle = $linestyle eq 'none' ? ', only marks' : ', ' . ($linestyle =~ s/_/ /gr); - if ($fill eq 'self') { - $fill = ", fill=$fill_color, fill opacity=$fill_opacity"; - } else { - $fill = ''; - } - $name = ", name path=$name" if $name; - $tikzOpts = ", $tikzOpts" if $tikzOpts; - - return "color=$color, line width=${width}pt$marks$linestyle$end_markers$fill$name$tikzOpts"; -} - -sub draw { - my $self = shift; - my $pgplot = $self->pgplot; - - # Reset colors just in case. - $self->{colors} = {}; - - # Add Axes - my $tikzCode = $self->configure_axes; - - # Plot Data - for my $data ($pgplot->data('function', 'dataset')) { - $data->gen_data; - my $n = $data->size; - my $color = $data->style('color') || 'default_color'; - my $fill = $data->style('fill') || 'none'; - my $fill_color = $data->style('fill_color') || 'default_color'; - my $tikzData = join(' ', map { '(' . $data->x($_) . ',' . $data->y($_) . ')'; } (0 .. $n - 1)); - my $tikzOpts = $self->get_plot_opts($data); - $tikzCode .= $self->get_color($fill_color) unless $fill eq 'none'; - $tikzCode .= $self->get_color($color) . "\\addplot[$tikzOpts] coordinates {$tikzData};\n"; - - unless ($fill eq 'none' || $fill eq 'self') { - my $opacity = $data->style('fill_opacity') || 0.5; - my $fill_range = $data->style('fill_range') || ''; - my $name = $data->style('name') || ''; - $opacity *= 100; - if ($fill_range) { - my ($min_fill, $max_fill) = split(',', $fill_range); - $fill_range = ", soft clip={domain=$min_fill:$max_fill}"; - } - $tikzCode .= "\\addplot[$fill_color!$opacity] fill between[of=$name and $fill$fill_range];\n"; - } - } - - # Stamps - for my $stamp ($pgplot->data('stamp')) { - my $mark = { - closed_circle => '*', - open_circle => 'o', - plus => '+', - times => 'x', - bar => '|', - dash => '-', - asterisk => 'asterisk', - star => 'star', - oplus => 'oplus', - otimes => 'otimes', - diamond => 'diamond', - none => '', - }->{ $stamp->style('symbol') }; - my $color = $stamp->style('color') || 'default_color'; - my $x = $stamp->x(0); - my $y = $stamp->y(0); - my $r = $stamp->style('radius') || 4; - $tikzCode .= $self->get_color($color) - . "\\addplot[$color, mark=$mark, mark size=${r}pt, only marks] coordinates {($x,$y)};\n"; - } - - # Labels - for my $label ($pgplot->data('label')) { - my $str = $label->style('label'); - my $x = $label->x(0); - my $y = $label->y(0); - my $color = $label->style('color') || 'default_color'; - my $fontsize = $label->style('fontsize') || 'medium'; - my $orientation = $label->style('orientation') || 'horizontal'; - my $tikzOpts = $label->style('tikzOpts') || ''; - my $h_align = $label->style('h_align') || 'center'; - my $v_align = $label->style('v_align') || 'middle'; - my $anchor = $v_align eq 'top' ? 'north' : $v_align eq 'bottom' ? 'south' : ''; - $str = { - tiny => '\tiny ', - small => '\small ', - medium => '', - large => '\large ', - giant => '\Large ', - }->{$fontsize} - . $str; - $anchor .= $h_align eq 'left' ? ' west' : $h_align eq 'right' ? ' east' : ''; - $tikzOpts = $tikzOpts ? "$color, $tikzOpts" : $color; - $tikzOpts = "anchor=$anchor, $tikzOpts" if $anchor; - $tikzOpts = "rotate=90, $tikzOpts" if $orientation eq 'vertical'; - $tikzCode .= $self->get_color($color) . "\\node[$tikzOpts] at (axis cs: $x,$y) {$str};\n"; - } - $tikzCode .= '\end{axis}' . "\n"; - - $pgplot->{tikzCode} = $tikzCode; - $self->im->tex($tikzCode); - return $pgplot->{tikzDebug} ? '' : $self->im->draw; -} - -1;