Skip to content

Commit

Permalink
WIP: Add JSXGraph and Plotly.js graph output to Plots.
Browse files Browse the repository at this point in the history
  • Loading branch information
somiaj committed Nov 10, 2024
1 parent 8477137 commit 98eb31e
Show file tree
Hide file tree
Showing 10 changed files with 282 additions and 29 deletions.
2 changes: 1 addition & 1 deletion conf/pg_config.dist.yml
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ modules:
- [Multiple]
- [PGrandom]
- [Regression]
- ['Plots::Plot', 'Plots::Tikz', 'Plots::GD', 'Plots::Data', 'Plots::Axes']
- ['Plots::Plot', 'Plots::Axes', 'Plots::Data', 'Plots::Tikz', 'Plots::JSX', 'Plots::Plotly', 'Plots::GD']
- [Select]
- [Units]
- [VectorField]
Expand Down
2 changes: 1 addition & 1 deletion lib/Plots/Axes.pm
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Hash of data for options for the general axis.
=head1 USAGE
The axes object should be accessed through a PGplot object using C<< $plot->axes >>.
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.
Expand Down
9 changes: 4 additions & 5 deletions lib/Plots/Data.pm
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,10 @@

=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.
This object holds data about the different types of elements that can be added to a
Plots graph. This is a hash with some helper methods. Data objects are created and
modified using the Plots 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:
Expand Down
110 changes: 110 additions & 0 deletions lib/Plots/JSX.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
################################################################################
# WeBWorK Online Homework Delivery System
# Copyright &copy; 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 DESCRIPTION
This is the code that takes a C<Plots::Plot> and creates a jsxgraph graph of the plot.
See L<plots.pl> for more details.
=cut

package Plots::JSX;

use strict;
use warnings;

sub new {
my ($class, $pgplot) = @_;

$pgplot->insert_css('node_modules/jsxgraph/distrib/jsxgraph.css');
$pgplot->insert_js('node_modules/jsxgraph/distrib/jsxgraphcore.js');

return bless { pgplot => $pgplot }, $class;
}

sub pgplot {
my $self = shift;
return $self->{pgplot};
}

sub HTML {
my $self = shift;
my $board = $self->{board};
my $JS = $self->{JS};

return <<END_HTML;
$board
<script>
(() => {
const draw_board = () => {
$JS
}
if (document.readyState === 'loading') window.addEventListener('DOMContentLoaded', draw_board);
else draw_board();
})();
</script>
END_HTML
}

sub init_graph {
my $self = shift;
my $pgplot = $self->pgplot;
my $axes = $pgplot->axes;
my $grid = $axes->grid;
my $name = $self->{name};
my $title = $axes->style('title');
my ($xmin, $ymin, $xmax, $ymax) = $axes->bounds;
my ($height, $width) = $pgplot->size;
my $style = 'display: inline-block; margin: 5px; text-align: center;';

$title = "<strong>$title</strong>" if $title;
$self->{board} = <<END_HTML;
<div style="$style">$title
<div id="board_$name" class="jxgbox" style="width: ${width}px; height: ${height}px;"></div>
</div>
END_HTML
$self->{JS} = <<END_JS;
const board_$name = JXG.JSXGraph.initBoard(
'board_$name',
{
boundingbox: [$xmin, $ymax, $xmax, $ymin],
axis: true,
showNavigation: false,
showCopyright: false,
}
);
END_JS
}

sub draw {
my $self = shift;
my $pgplot = $self->pgplot;
my $name = $pgplot->get_image_name =~ s/-/_/gr;
$self->{name} = $name;

$self->init_graph;

# Plot Data
for my $data ($pgplot->data('function', 'dataset')) {
$data->gen_data;
$self->{JS} .=
"\n\t\tboard_$name.create('curve', [[" . (join(',', $data->x)) . "],[" . (join(',', $data->y)) . "]]);";
}
return $self->HTML;
}
1;
56 changes: 45 additions & 11 deletions lib/Plots/Plot.pm
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ use warnings;
use Plots::Axes;
use Plots::Data;
use Plots::Tikz;
use Plots::Plotly;
use Plots::JSX;
use Plots::GD;

sub new {
Expand All @@ -41,6 +43,7 @@ sub new {
type => 'Tikz',
ext => 'svg',
size => [ $size, $size ],
tex_size => 500,
axes => Plots::Axes->new,
colors => {},
data => [],
Expand All @@ -51,6 +54,24 @@ sub new {
return $self;
}

# Only insert js file if it isn't already inserted.
sub insert_js {
my ($self, $file) = @_;
for my $obj (@{ $self->{pg}{flags}{extra_js_files} }) {
return if $obj->{file} eq $file;
}
push(@{ $self->{pg}{flags}{extra_js_files} }, { file => $file, external => 0, attributes => { defer => undef } });
}

# Only insert css file if it isn't already inserted.
sub insert_css {
my ($self, $file) = @_;
for my $obj (@{ $self->{pg}{flags}{extra_css_files} }) {
return if $obj->{file} eq $file;
}
push(@{ $self->{pg}{flags}{extra_css_files} }, { file => $file, external => 0 });
}

sub colors {
my ($self, $color) = @_;
return defined($color) ? $self->{colors}{$color} : $self->{colors};
Expand Down Expand Up @@ -131,41 +152,50 @@ sub image_type {
# Check type and extension are valid. The first element of @validExt is used as default.
my @validExt;
$type = lc($type);
if ($type eq 'tikz') {
if ($type eq 'jsx') {
$self->{type} = 'JSX';
@validExt = ('html');
} elsif ($type eq 'plotly') {
$self->{type} = 'Plotly';
@validExt = ('html');
} elsif ($type eq 'tikz') {
$self->{type} = 'Tikz';
@validExt = ('svg', 'png', 'pdf');
} elsif ($type eq 'gd') {
$self->{type} = 'GD';
@validExt = ('png', 'gif');
} else {
warn "PGplot: Invalid image type $type.";
warn "Plots: Invalid image type $type.";
return;
}

if ($ext) {
if (grep(/^$ext$/, @validExt)) {
$self->{ext} = $ext;
} else {
warn "PGplot: Invalid image extension $ext.";
warn "Plots: Invalid image extension $ext.";
}
} else {
$self->{ext} = $validExt[0];
}

# Hardcopy: Tikz needs to use the 'pdf' extension and fallback to Tikz output if ext is 'html'.
if ($self->{pg}{displayMode} eq 'TeX' && ($self->{ext} eq 'html' || $self->{type} eq 'Tikz')) {
$self->{type} = 'Tikz';
$self->{ext} = 'pdf';
}
return;
}

# Tikz needs to use pdf for hardcopy generation.
sub ext {
my $self = shift;
return 'pdf' if ($self->{type} eq 'Tikz' && eval('$main::displayMode') eq 'TeX');
return $self->{ext};
return (shift)->{ext};
}

# Return a copy of the tikz code (available after the image has been drawn).
# Set $plot->{tikzDebug} to 1 to just generate the tikzCode, and not create a graph.
sub tikz_code {
my $self = shift;
return ($self->{tikzCode} && eval('$main::displayMode') =~ /HTML/) ? '<pre>' . $self->{tikzCode} . '</pre>' : '';
return $self->{tikzCode} && $self->{pg}{displayMode} =~ /HTML/ ? '<pre>' . $self->{tikzCode} . '</pre>' : '';
}

# Add functions to the graph.
Expand Down Expand Up @@ -367,10 +397,14 @@ sub draw {
my $type = $self->{type};

my $image;
if ($type eq 'GD') {
$image = Plots::GD->new($self);
} elsif ($type eq 'Tikz') {
if ($type eq 'Tikz') {
$image = Plots::Tikz->new($self);
} elsif ($type eq 'JSX') {
$image = Plots::JSX->new($self);
} elsif ($type eq 'Plotly') {
$image = Plots::Plotly->new($self);
} elsif ($type eq 'GD') {
$image = Plots::GD->new($self);
} else {
warn "Undefined image type: $type";
return;
Expand Down
100 changes: 100 additions & 0 deletions lib/Plots/Plotly.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
################################################################################
# WeBWorK Online Homework Delivery System
# Copyright &copy; 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 DESCRIPTION
This is the code that takes a C<Plots::Plot> and creates a Plotly.js graph of the plot.
See L<plots.pl> for more details.
=cut

package Plots::Plotly;

use strict;
use warnings;

sub new {
my ($class, $pgplot) = @_;

$pgplot->insert_js('node_modules/plotly.js-dist-min/plotly.min.js');

return bless { pgplot => $pgplot, plots => [] }, $class;
}

sub pgplot {
my $self = shift;
return $self->{pgplot};
}

sub HTML {
my $self = shift;
my $pgplot = $self->pgplot;
my $axes = $pgplot->axes;
my $grid = $axes->grid;
my $name = $pgplot->get_image_name =~ s/-/_/gr;
my $title = $axes->style('title');
my $plots = '';
my ($xmin, $ymin, $xmax, $ymax) = $axes->bounds;
my ($height, $width) = $pgplot->size;
my $style = 'border: solid 2px; display: inline-block; margin: 5px; text-align: center;';

$title = "<strong>$title</strong>" if $title;
for (@{ $self->{plots} }) {
$plots .= $_;
}

return <<END_HTML;
<div style="$style">$title
<div id="plotlyDiv_$name" style="width: ${width}px; height: ${height}px;"></div>
</div>
<script>
(() => {
const draw_graph = () => {
const plotlyData = [];
$plots
Plotly.newPlot('plotlyDiv_$name', plotlyData);
}
if (document.readyState === 'loading') window.addEventListener('DOMContentLoaded', draw_graph);
else draw_graph();
})();
</script>
END_HTML
}

sub draw {
my $self = shift;
my $pgplot = $self->pgplot;

# Plot Data
for my $data ($pgplot->data('function', 'dataset')) {
$data->gen_data;

my $x_points = join(',', $data->x);
my $y_points = join(',', $data->y);
my $plot = <<END_JS;
plotlyData.push({
x: [$x_points],
y: [$y_points],
mode: 'lines'
});
END_JS
push(@{ $self->{plots} }, $plot);
}

return $self->HTML;
}

1;
12 changes: 11 additions & 1 deletion macros/core/PGbasicmacros.pl
Original file line number Diff line number Diff line change
Expand Up @@ -2909,9 +2909,19 @@ sub image {
);
next;
}
if (ref $image_item eq 'Plots::Plot') {
# Update image size as needed.
$image_item->{size}->[0] = $width if $out_options{width};
$image_item->{size}->[1] = $height if $out_options{height};

if ($image_item->ext eq 'html') {
push(@output_list, $image_item->draw);
next;
}
}
$image_item = insertGraph($image_item)
if (ref $image_item eq 'WWPlot'
|| ref $image_item eq 'PGplot'
|| ref $image_item eq 'Plots::Plot'
|| ref $image_item eq 'PGlateximage'
|| ref $image_item eq 'PGtikz');
my $imageURL = alias($image_item) // '';
Expand Down
Loading

0 comments on commit 98eb31e

Please sign in to comment.