Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add PGplot, a method for generating graphs. #966

Draft
wants to merge 5 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions conf/pg_config.dist.yml
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ modules:
- [Multiple]
- [PGrandom]
- [Regression]
- ['Plots::Plot', 'Plots::Axes', 'Plots::Data', 'Plots::Tikz', 'Plots::JSX', 'Plots::Plotly', 'Plots::GD']
- [Select]
- [Units]
- [VectorField]
Expand Down
334 changes: 334 additions & 0 deletions lib/Plots/Axes.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,334 @@
################################################################################
# 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 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 Plots object using C<< $plot->axes >>.
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->axes->bounds >> 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<tick_delta> or C<tick_num>. 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<tick_num>.
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<tick_delta> as the difference between the C<max> and C<min> 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 Plots::Axes;

use strict;
use warnings;

sub new {
my $class = shift;
my $self = bless {
xaxis => {},
yaxis => {},
styles => {
title => '',
grid_color => 'gray',
grid_style => 'solid',
grid_alpha => 40,
show_grid => 1,
},
@_
}, $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, @items) = @_;
return $self->axis('xaxis', @items);
}

sub yaxis {
my ($self, @items) = @_;
return $self->axis('yaxis', @items);
}

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;
Loading