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 11, 2024
1 parent 8477137 commit 3a971b8
Show file tree
Hide file tree
Showing 11 changed files with 362 additions and 36 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
9 changes: 5 additions & 4 deletions lib/Plots/GD.pm
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,15 @@ sub im_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));
return int(($x - $xmin) * $pgplot->{width} / ($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));
return int(($ymax - $y) * $pgplot->{height} / ($ymax - $ymin));
}

sub moveTo {
Expand Down Expand Up @@ -235,7 +235,8 @@ sub draw {
my $pgplot = $self->pgplot;
my $axes = $pgplot->axes;
my $grid = $axes->grid;
my $size = $pgplot->size;
my $width = $pgplot->{width};
my $height = $pgplot->{height};

# Initialize image
$self->im->interlaced('true');
Expand Down Expand Up @@ -379,7 +380,7 @@ sub draw {
}

# Put a black frame around the picture
$self->im->rectangle(0, 0, $size->[0] - 1, $size->[1] - 1, $self->color('black'));
$self->im->rectangle(0, 0, $width - 1, $height - 1, $self->color('black'));

return $pgplot->ext eq 'gif' ? $self->im->gif : $self->im->png;
}
Expand Down
174 changes: 174 additions & 0 deletions lib/Plots/JSX.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
################################################################################
# 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 get_color {
my ($self, $color) = @_;
return sprintf("#%x%x%x", @{ $self->pgplot->colors($color) });
}

sub add_curve {
my ($self, $data) = @_;
my $linestyle = $data->style('linestyle');
return if $linestyle eq 'none';

if ($linestyle eq 'densely dashed') {
$linestyle = ',dash: 4, dashScale: true';
} elsif ($linestyle eq 'loosely dashed') {
$linestyle = ',dash: 3, dashScale: true';
} elsif ($linestyle =~ /dashed/) {
$linestyle = ',dash: 1, dashScale: true';
} elsif ($linestyle =~ /dotted/) {
$linestyle = ',dash: 1';
} else {
$linestyle = '';
}

my $name = $self->{name};
my $color = $self->get_color($data->style('color') || 'default_color');
my $data_points = '[[' . join(',', $data->x) . '],[' . join(',', $data->y) . ']]';
my $line_width = $data->style('width') || 2;

$self->{JS} .= "\n\t\tboard_$name.create('curve', $data_points, "
. "{strokeColor: '$color', strokeWidth: $line_width$linestyle});";
}

sub add_points {
my ($self, $data) = @_;
my $mark = $data->style('marks');
return if !$mark || $mark eq 'none';

if ($mark eq 'plus' || $mark eq 'oplus') {
$mark = ',face: "plus"';
} elsif ($mark eq 'times' || $mark eq 'otimes') {
$mark = ',face: "cross"';
} elsif ($mark eq 'dash') {
$mark = ',face: "minus"';
} elsif ($mark eq 'bar') {
$mark = ',face: "divide"';
} elsif ($mark eq 'diamond') {
$mark = ',face: "diamond"';
} elsif ($mark eq 'open_circle') {
$mark = ',fillColor: "white"';
} else {
$mark = '';
}

my $name = $self->{name};
my $size = $data->style('mark_size') || $data->style('width') || 3;

for my $i (0 .. $data->size - 1) {
$self->{JS} .=
"\n\t\tboard_$name.create('point', ["
. $data->x($i) . ','
. $data->y($i) . '], '
. "{fixed: true, withLabel: false, size: $size$mark});";
}
}

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->add_curve($data);
$self->add_points($data);
}

return $self->HTML;
}

1;
63 changes: 49 additions & 14 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 @@ -39,8 +41,10 @@ sub new {
pg => $pg,
imageName => {},
type => 'Tikz',
ext => 'svg',
size => [ $size, $size ],
ext => $pg->{displayMode} eq 'TeX' ? 'pdf' : 'svg',
width => $size,
height => $size,
tex_size => 500,
axes => Plots::Axes->new,
colors => {},
data => [],
Expand All @@ -51,6 +55,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 @@ -85,7 +107,7 @@ sub color_init {

sub size {
my $self = shift;
return wantarray ? @{ $self->{size} } : $self->{size};
return wantarray ? ($self->{width}, $self->{height}) : [ $self->{width}, $self->{height} ];
}

sub data {
Expand Down Expand Up @@ -131,41 +153,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 +398,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
Loading

0 comments on commit 3a971b8

Please sign in to comment.