Skip to content

Commit 8d197b1

Browse files
committed
Add PGplot, a method for generating graphs.
PGplot is a new method for generating dynamic graphs and can be used as a replacement for WWPlot and PGgraphmacros.pl. PGplot is a method to create a plot object and store information about a plot in multiple data objects. Since PGplot just stores data about the plot, the data can then be used to create multiple different outputs. Currently only TikZ pgfplots and GD (for testing) are supported. In addition a new macro PGplotmacros.pl is added to be a replacement of PGgraphmacros.pl. This is meant to allow moving old problems to PGplot and create nicer TikZ graphs without much work. Replacing PGgraphmacros.pl with PGplotmacros.pl will work provided the problem doesn't call GD object calls directly (only uses the macros) and doesn't use fill_region (as this isn't supported by TikZ).
1 parent 10d1930 commit 8d197b1

File tree

8 files changed

+2374
-4
lines changed

8 files changed

+2374
-4
lines changed

macros/graph/PGplot.pl

Lines changed: 766 additions & 0 deletions
Large diffs are not rendered by default.

macros/graph/PGplot/Axes.pl

Lines changed: 344 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,344 @@
1+
################################################################################
2+
# WeBWorK Online Homework Delivery System
3+
# Copyright © 2000-2023 The WeBWorK Project, https://github.com/openwebwork
4+
#
5+
# This program is free software; you can redistribute it and/or modify it under
6+
# the terms of either: (a) the GNU General Public License as published by the
7+
# Free Software Foundation; either version 2, or (at your option) any later
8+
# version, or (b) the "Artistic License" which comes with this package.
9+
#
10+
# This program is distributed in the hope that it will be useful, but WITHOUT
11+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12+
# FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the
13+
# Artistic License for more details.
14+
################################################################################
15+
16+
=head1 NAME
17+
18+
PGplot/Axes.pl - Object used with PGplot to store data about a plot's title and axes.
19+
20+
=head1 DESCRIPTION
21+
22+
This is a hash to store information about the axes (ticks, range, grid, etc)
23+
with some helper methods. The hash is further split into three smaller hashes:
24+
25+
=over 5
26+
27+
=item xaxis
28+
29+
Hash of data for the horizontal axis.
30+
31+
=item yaxis
32+
33+
Hash of data for the vertical axis.
34+
35+
=item styles
36+
37+
Hash of data for options for the general axis.
38+
39+
=back
40+
41+
=head1 USAGE
42+
43+
The axes object should be accessed through a L<PGplot|PGplot.pl> object using C<$plot-E<gt>axes>.
44+
The axes object is used to configure and retrieve information about the axes,
45+
as in the following examples.
46+
47+
Each axis can be configured individually, such as:
48+
49+
$plot->axes->xaxis(min => -10, max => 10, ticks => [-12, -8, -4, 0, 4, 8, 12]);
50+
$plot->axes->yaxis(min => 0, max => 100, ticks => [20, 40, 60, 80, 100]);
51+
52+
This can also be combined using the set method, such as:
53+
54+
$plot->axes->set(
55+
xmin => -10,
56+
xmax => 10,
57+
xticks => [-12, -8, -4, 0, 4, 8, 12],
58+
ymin => 0,
59+
ymax => 100,
60+
yticks => [20, 40, 60, 80, 100]
61+
);
62+
63+
In addition to the configuration each axis, there is a set of styles that apply to both axes.
64+
These are access via the style method. To set one or more styles use:
65+
66+
$plot->axes->style(title => '\(y = f(x)\)', show_grid => 0);
67+
68+
The same methods also get the value of a single option, such as:
69+
70+
$xmin = $plot->axes->xaxis('min');
71+
$yticks = $plot->axes->yaxis('ticks');
72+
$title = $plot->axes->style('title');
73+
74+
The methods without any inputs return a reference to the full hash, such as:
75+
76+
$xaxis = $plot->axes->xaxis;
77+
$styles = $plot->axes->style;
78+
79+
It is also possible to get multiple options for both axes using the get method, which returns
80+
a reference to a hash of requested keys, such as:
81+
82+
$bounds = $plot->axes->get('xmin', 'xmax', 'ymin', 'ymax');
83+
# The following is equivlant to $plot->axes->grid
84+
$grid = $plot->axes->get('xmajor', 'xminor', 'xticks', 'ymajor', 'yminor', 'yticks');
85+
86+
It is also possible to get the bounds as an array in the order xmin, ymin, xmax, ymax
87+
using the C<$plot-E<gt>axes-E<gt>bounds> method.
88+
89+
=head1 AXIS CONFIGURATION OPTIONS
90+
91+
Each axis (the xaxis and yaxis) has the following configuration options:
92+
93+
=over 5
94+
95+
=item min
96+
97+
The minimum value the axis shows. Default is -5.
98+
99+
=item max
100+
101+
The maximum value the axis shows. Default is 5.
102+
103+
=item ticks
104+
105+
An array which lists the major tick marks. If this array is empty, the ticks are
106+
generated using either C<tick_delta> or C<tick_num>. Default is C<[]>.
107+
108+
=item tick_delta
109+
110+
This is the distance between each major tick mark, starting from the origin.
111+
This distance is then used to generate the tick marks if the ticks array is empty.
112+
If this is set to 0, this distance is set by using the number of ticks, C<tick_num>.
113+
Default is 0.
114+
115+
=item tick_num
116+
117+
This is the number of major tick marks to include on the axis. This number is used
118+
to compute the C<tick_delta> as the difference between the C<max> and C<min> values
119+
and the number of ticks. Default: 5.
120+
121+
=item label
122+
123+
The axis label. Defaults are C<\(x\)> and C<\(y\)>.
124+
125+
=item major
126+
127+
Show (1) or don't show (0) grid lines at the tick marks. Default is 1.
128+
129+
=item minor
130+
131+
This sets the number of minor grid lines per major grid line. If this is
132+
set to 0, no minor grid lines are shown. Default is 3.
133+
134+
=item visible
135+
136+
This sets if the axis is shown (1) or not (0) on the plot. Default is 1.
137+
138+
=item location
139+
140+
This sets the location of the axes relative to the graph. The possible options
141+
for each axis are:
142+
143+
xaxis => 'box', 'top', 'middle', 'bottom'
144+
yaxis => 'box', 'left', 'center', 'right'
145+
146+
This places the axis at the appropriate edge of the graph. If 'center' or 'middle'
147+
are used, the axes appear on the inside of the graph at the appropriate position.
148+
Setting the location to 'box' creates a box or framed pot. Default 'middle' or 'center'.
149+
150+
=item position
151+
152+
The position in terms of the appropriate variable to draw the axis if the location is
153+
set to 'middle' or 'center'. Default is 0.
154+
155+
=back
156+
157+
=head1 STYLES
158+
159+
The following styles configure aspects about the axes:
160+
161+
=over 5
162+
163+
=item title
164+
165+
The title of the graph. Default is ''.
166+
167+
=item show_grid
168+
169+
Either draw (1) or don't draw (0) the grid lines for the axis. Default is 1.
170+
171+
=item grid_color
172+
173+
The color of the grid lines. Default is 'gray'.
174+
175+
=item grid_style
176+
177+
The line style of grid lines. This can be 'dashed', 'dotted', 'solid', etc.
178+
Default is 'solid'.
179+
180+
=item grid_alpha
181+
182+
The alpha value to use to draw the grid lines in Tikz. This is a number from
183+
0 (fully transparent) to 100 (fully solid). Default is 40.
184+
185+
=item axis_on_top
186+
187+
Configures if the axis should be drawn on top of the graph (1) or below the graph (0).
188+
Useful when filling a region that covers an axis, if the axis are on top they will still
189+
be visible after the fill, otherwise the fill will cover the axis. Default: 0
190+
191+
=back
192+
193+
=cut
194+
195+
BEGIN {
196+
be_strict();
197+
}
198+
199+
sub _Axes_init {
200+
return;
201+
}
202+
203+
package PGplot::Axes;
204+
205+
sub new {
206+
my $class = shift;
207+
my $self = {
208+
xaxis => {},
209+
yaxis => {},
210+
styles => {
211+
title => '',
212+
grid_color => 'gray',
213+
grid_style => 'solid',
214+
grid_alpha => 40,
215+
show_grid => 1,
216+
},
217+
@_
218+
};
219+
220+
bless $self, $class;
221+
$self->xaxis($self->axis_defaults('x'));
222+
$self->yaxis($self->axis_defaults('y'));
223+
return $self;
224+
}
225+
226+
sub axis_defaults {
227+
my ($self, $axis) = @_;
228+
return (
229+
visible => 1,
230+
min => -5,
231+
max => 5,
232+
label => $axis eq 'y' ? '\(y\)' : '\(x\)',
233+
location => $axis eq 'y' ? 'center' : 'middle',
234+
position => 0,
235+
ticks => undef,
236+
tick_delta => 0,
237+
tick_num => 5,
238+
major => 1,
239+
minor => 3,
240+
);
241+
}
242+
243+
sub axis {
244+
my ($self, $axis, @items) = @_;
245+
return $self->{$axis} unless @items;
246+
if (scalar(@items) > 1) {
247+
my %item_hash = @items;
248+
map { $self->{$axis}{$_} = $item_hash{$_}; } (keys %item_hash);
249+
return;
250+
}
251+
my $item = $items[0];
252+
if (ref($item) eq 'HASH') {
253+
map { $self->{$axis}{$_} = $item->{$_}; } (keys %$item);
254+
return;
255+
}
256+
# Deal with ticks individually since they may need to be generated.
257+
return $item eq 'ticks' ? $self->{$axis}{ticks} || $self->gen_ticks($self->axis($axis)) : $self->{$axis}{$item};
258+
}
259+
260+
sub xaxis {
261+
my $self = shift;
262+
return $self->axis('xaxis', @_);
263+
}
264+
265+
sub yaxis {
266+
my $self = shift;
267+
return $self->axis('yaxis', @_);
268+
}
269+
270+
sub set {
271+
my ($self, %options) = @_;
272+
my (%xopts, %yopts);
273+
for (keys %options) {
274+
if ($_ =~ s/^x//) {
275+
$xopts{$_} = $options{"x$_"};
276+
} elsif ($_ =~ s/^y//) {
277+
$yopts{$_} = $options{"y$_"};
278+
}
279+
}
280+
$self->xaxis(%xopts) if %xopts;
281+
$self->yaxis(%yopts) if %yopts;
282+
return;
283+
}
284+
285+
sub get {
286+
my ($self, @keys) = @_;
287+
my %options;
288+
for (@keys) {
289+
if ($_ =~ s/^x//) {
290+
$options{"x$_"} = $self->xaxis($_);
291+
} elsif ($_ =~ s/^y//) {
292+
$options{"y$_"} = $self->yaxis($_);
293+
}
294+
}
295+
return \%options;
296+
}
297+
298+
sub style {
299+
my ($self, @styles) = @_;
300+
return $self->{styles} unless @styles;
301+
if (scalar(@styles) > 1) {
302+
my %style_hash = @styles;
303+
map { $self->{styles}{$_} = $style_hash{$_}; } (keys %style_hash);
304+
return;
305+
}
306+
my $style = $styles[0];
307+
if (ref($style) eq 'HASH') {
308+
map { $self->{styles}{$_} = $style->{$_}; } (keys %$style);
309+
return;
310+
}
311+
return $self->{styles}{$style};
312+
}
313+
314+
sub gen_ticks {
315+
my ($self, $axis) = @_;
316+
my $min = $axis->{min};
317+
my $max = $axis->{max};
318+
my $delta = $axis->{tick_delta};
319+
$delta = ($max - $min) / $axis->{tick_num} unless $delta;
320+
321+
my @ticks = $min <= 0 && $max >= 0 ? (0) : ();
322+
my $point = $delta;
323+
# Adjust min/max to place one more tick beyond the graph's edge.
324+
$min -= $delta;
325+
$max += $delta;
326+
do {
327+
push(@ticks, $point) unless $point < $min || $point > $max;
328+
unshift(@ticks, -$point) unless -$point < $min || -$point > $max;
329+
$point += $delta;
330+
} until (-$point < $min && $point > $max);
331+
return \@ticks;
332+
}
333+
334+
sub grid {
335+
my $self = shift;
336+
return $self->get('xmajor', 'xminor', 'xticks', 'ymajor', 'yminor', 'yticks');
337+
}
338+
339+
sub bounds {
340+
my $self = shift;
341+
return $self->{xaxis}{min}, $self->{yaxis}{min}, $self->{xaxis}{max}, $self->{yaxis}{max};
342+
}
343+
344+
1;

0 commit comments

Comments
 (0)