Skip to content

Commit 98eb31e

Browse files
committed
WIP: Add JSXGraph and Plotly.js graph output to Plots.
1 parent 8477137 commit 98eb31e

File tree

10 files changed

+282
-29
lines changed

10 files changed

+282
-29
lines changed

conf/pg_config.dist.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ modules:
230230
- [Multiple]
231231
- [PGrandom]
232232
- [Regression]
233-
- ['Plots::Plot', 'Plots::Tikz', 'Plots::GD', 'Plots::Data', 'Plots::Axes']
233+
- ['Plots::Plot', 'Plots::Axes', 'Plots::Data', 'Plots::Tikz', 'Plots::JSX', 'Plots::Plotly', 'Plots::GD']
234234
- [Select]
235235
- [Units]
236236
- [VectorField]

lib/Plots/Axes.pm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ Hash of data for options for the general axis.
3636
3737
=head1 USAGE
3838
39-
The axes object should be accessed through a PGplot object using C<< $plot->axes >>.
39+
The axes object should be accessed through a Plots object using C<< $plot->axes >>.
4040
The axes object is used to configure and retrieve information about the axes,
4141
as in the following examples.
4242

lib/Plots/Data.pm

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,10 @@
1515

1616
=head1 DATA OBJECT
1717
18-
This object holds data about the different types of elements that can be added
19-
to a PGplot graph. This is a hash with some helper methods. Data objects are created
20-
and modified using the PGplot methods, and do not need to generally be
21-
modified in a PG problem. Each PG add method returns the related data object which
22-
can be used if needed.
18+
This object holds data about the different types of elements that can be added to a
19+
Plots graph. This is a hash with some helper methods. Data objects are created and
20+
modified using the Plots methods, and do not need to generally be modified in a PG
21+
problem. Each PG add method returns the related data object which can be used if needed.
2322
2423
Each data object contains the following:
2524

lib/Plots/JSX.pm

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
################################################################################
2+
# WeBWorK Online Homework Delivery System
3+
# Copyright &copy; 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 DESCRIPTION
17+
18+
This is the code that takes a C<Plots::Plot> and creates a jsxgraph graph of the plot.
19+
20+
See L<plots.pl> for more details.
21+
22+
=cut
23+
24+
package Plots::JSX;
25+
26+
use strict;
27+
use warnings;
28+
29+
sub new {
30+
my ($class, $pgplot) = @_;
31+
32+
$pgplot->insert_css('node_modules/jsxgraph/distrib/jsxgraph.css');
33+
$pgplot->insert_js('node_modules/jsxgraph/distrib/jsxgraphcore.js');
34+
35+
return bless { pgplot => $pgplot }, $class;
36+
}
37+
38+
sub pgplot {
39+
my $self = shift;
40+
return $self->{pgplot};
41+
}
42+
43+
sub HTML {
44+
my $self = shift;
45+
my $board = $self->{board};
46+
my $JS = $self->{JS};
47+
48+
return <<END_HTML;
49+
$board
50+
<script>
51+
(() => {
52+
const draw_board = () => {
53+
$JS
54+
}
55+
if (document.readyState === 'loading') window.addEventListener('DOMContentLoaded', draw_board);
56+
else draw_board();
57+
})();
58+
</script>
59+
END_HTML
60+
}
61+
62+
sub init_graph {
63+
my $self = shift;
64+
my $pgplot = $self->pgplot;
65+
my $axes = $pgplot->axes;
66+
my $grid = $axes->grid;
67+
my $name = $self->{name};
68+
my $title = $axes->style('title');
69+
my ($xmin, $ymin, $xmax, $ymax) = $axes->bounds;
70+
my ($height, $width) = $pgplot->size;
71+
my $style = 'display: inline-block; margin: 5px; text-align: center;';
72+
73+
$title = "<strong>$title</strong>" if $title;
74+
$self->{board} = <<END_HTML;
75+
<div style="$style">$title
76+
<div id="board_$name" class="jxgbox" style="width: ${width}px; height: ${height}px;"></div>
77+
</div>
78+
END_HTML
79+
$self->{JS} = <<END_JS;
80+
const board_$name = JXG.JSXGraph.initBoard(
81+
'board_$name',
82+
{
83+
boundingbox: [$xmin, $ymax, $xmax, $ymin],
84+
axis: true,
85+
showNavigation: false,
86+
showCopyright: false,
87+
}
88+
);
89+
END_JS
90+
}
91+
92+
sub draw {
93+
my $self = shift;
94+
my $pgplot = $self->pgplot;
95+
my $name = $pgplot->get_image_name =~ s/-/_/gr;
96+
$self->{name} = $name;
97+
98+
$self->init_graph;
99+
100+
# Plot Data
101+
for my $data ($pgplot->data('function', 'dataset')) {
102+
$data->gen_data;
103+
$self->{JS} .=
104+
"\n\t\tboard_$name.create('curve', [[" . (join(',', $data->x)) . "],[" . (join(',', $data->y)) . "]]);";
105+
}
106+
107+
return $self->HTML;
108+
}
109+
110+
1;

lib/Plots/Plot.pm

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ use warnings;
2929
use Plots::Axes;
3030
use Plots::Data;
3131
use Plots::Tikz;
32+
use Plots::Plotly;
33+
use Plots::JSX;
3234
use Plots::GD;
3335

3436
sub new {
@@ -41,6 +43,7 @@ sub new {
4143
type => 'Tikz',
4244
ext => 'svg',
4345
size => [ $size, $size ],
46+
tex_size => 500,
4447
axes => Plots::Axes->new,
4548
colors => {},
4649
data => [],
@@ -51,6 +54,24 @@ sub new {
5154
return $self;
5255
}
5356

57+
# Only insert js file if it isn't already inserted.
58+
sub insert_js {
59+
my ($self, $file) = @_;
60+
for my $obj (@{ $self->{pg}{flags}{extra_js_files} }) {
61+
return if $obj->{file} eq $file;
62+
}
63+
push(@{ $self->{pg}{flags}{extra_js_files} }, { file => $file, external => 0, attributes => { defer => undef } });
64+
}
65+
66+
# Only insert css file if it isn't already inserted.
67+
sub insert_css {
68+
my ($self, $file) = @_;
69+
for my $obj (@{ $self->{pg}{flags}{extra_css_files} }) {
70+
return if $obj->{file} eq $file;
71+
}
72+
push(@{ $self->{pg}{flags}{extra_css_files} }, { file => $file, external => 0 });
73+
}
74+
5475
sub colors {
5576
my ($self, $color) = @_;
5677
return defined($color) ? $self->{colors}{$color} : $self->{colors};
@@ -131,41 +152,50 @@ sub image_type {
131152
# Check type and extension are valid. The first element of @validExt is used as default.
132153
my @validExt;
133154
$type = lc($type);
134-
if ($type eq 'tikz') {
155+
if ($type eq 'jsx') {
156+
$self->{type} = 'JSX';
157+
@validExt = ('html');
158+
} elsif ($type eq 'plotly') {
159+
$self->{type} = 'Plotly';
160+
@validExt = ('html');
161+
} elsif ($type eq 'tikz') {
135162
$self->{type} = 'Tikz';
136163
@validExt = ('svg', 'png', 'pdf');
137164
} elsif ($type eq 'gd') {
138165
$self->{type} = 'GD';
139166
@validExt = ('png', 'gif');
140167
} else {
141-
warn "PGplot: Invalid image type $type.";
168+
warn "Plots: Invalid image type $type.";
142169
return;
143170
}
144171

145172
if ($ext) {
146173
if (grep(/^$ext$/, @validExt)) {
147174
$self->{ext} = $ext;
148175
} else {
149-
warn "PGplot: Invalid image extension $ext.";
176+
warn "Plots: Invalid image extension $ext.";
150177
}
151178
} else {
152179
$self->{ext} = $validExt[0];
153180
}
181+
182+
# Hardcopy: Tikz needs to use the 'pdf' extension and fallback to Tikz output if ext is 'html'.
183+
if ($self->{pg}{displayMode} eq 'TeX' && ($self->{ext} eq 'html' || $self->{type} eq 'Tikz')) {
184+
$self->{type} = 'Tikz';
185+
$self->{ext} = 'pdf';
186+
}
154187
return;
155188
}
156189

157-
# Tikz needs to use pdf for hardcopy generation.
158190
sub ext {
159-
my $self = shift;
160-
return 'pdf' if ($self->{type} eq 'Tikz' && eval('$main::displayMode') eq 'TeX');
161-
return $self->{ext};
191+
return (shift)->{ext};
162192
}
163193

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

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

369399
my $image;
370-
if ($type eq 'GD') {
371-
$image = Plots::GD->new($self);
372-
} elsif ($type eq 'Tikz') {
400+
if ($type eq 'Tikz') {
373401
$image = Plots::Tikz->new($self);
402+
} elsif ($type eq 'JSX') {
403+
$image = Plots::JSX->new($self);
404+
} elsif ($type eq 'Plotly') {
405+
$image = Plots::Plotly->new($self);
406+
} elsif ($type eq 'GD') {
407+
$image = Plots::GD->new($self);
374408
} else {
375409
warn "Undefined image type: $type";
376410
return;

lib/Plots/Plotly.pm

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
################################################################################
2+
# WeBWorK Online Homework Delivery System
3+
# Copyright &copy; 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 DESCRIPTION
17+
18+
This is the code that takes a C<Plots::Plot> and creates a Plotly.js graph of the plot.
19+
20+
See L<plots.pl> for more details.
21+
22+
=cut
23+
24+
package Plots::Plotly;
25+
26+
use strict;
27+
use warnings;
28+
29+
sub new {
30+
my ($class, $pgplot) = @_;
31+
32+
$pgplot->insert_js('node_modules/plotly.js-dist-min/plotly.min.js');
33+
34+
return bless { pgplot => $pgplot, plots => [] }, $class;
35+
}
36+
37+
sub pgplot {
38+
my $self = shift;
39+
return $self->{pgplot};
40+
}
41+
42+
sub HTML {
43+
my $self = shift;
44+
my $pgplot = $self->pgplot;
45+
my $axes = $pgplot->axes;
46+
my $grid = $axes->grid;
47+
my $name = $pgplot->get_image_name =~ s/-/_/gr;
48+
my $title = $axes->style('title');
49+
my $plots = '';
50+
my ($xmin, $ymin, $xmax, $ymax) = $axes->bounds;
51+
my ($height, $width) = $pgplot->size;
52+
my $style = 'border: solid 2px; display: inline-block; margin: 5px; text-align: center;';
53+
54+
$title = "<strong>$title</strong>" if $title;
55+
for (@{ $self->{plots} }) {
56+
$plots .= $_;
57+
}
58+
59+
return <<END_HTML;
60+
<div style="$style">$title
61+
<div id="plotlyDiv_$name" style="width: ${width}px; height: ${height}px;"></div>
62+
</div>
63+
<script>
64+
(() => {
65+
const draw_graph = () => {
66+
const plotlyData = [];
67+
$plots
68+
Plotly.newPlot('plotlyDiv_$name', plotlyData);
69+
}
70+
if (document.readyState === 'loading') window.addEventListener('DOMContentLoaded', draw_graph);
71+
else draw_graph();
72+
})();
73+
</script>
74+
END_HTML
75+
}
76+
77+
sub draw {
78+
my $self = shift;
79+
my $pgplot = $self->pgplot;
80+
81+
# Plot Data
82+
for my $data ($pgplot->data('function', 'dataset')) {
83+
$data->gen_data;
84+
85+
my $x_points = join(',', $data->x);
86+
my $y_points = join(',', $data->y);
87+
my $plot = <<END_JS;
88+
plotlyData.push({
89+
x: [$x_points],
90+
y: [$y_points],
91+
mode: 'lines'
92+
});
93+
END_JS
94+
push(@{ $self->{plots} }, $plot);
95+
}
96+
97+
return $self->HTML;
98+
}
99+
100+
1;

macros/core/PGbasicmacros.pl

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2909,9 +2909,19 @@ sub image {
29092909
);
29102910
next;
29112911
}
2912+
if (ref $image_item eq 'Plots::Plot') {
2913+
# Update image size as needed.
2914+
$image_item->{size}->[0] = $width if $out_options{width};
2915+
$image_item->{size}->[1] = $height if $out_options{height};
2916+
2917+
if ($image_item->ext eq 'html') {
2918+
push(@output_list, $image_item->draw);
2919+
next;
2920+
}
2921+
}
29122922
$image_item = insertGraph($image_item)
29132923
if (ref $image_item eq 'WWPlot'
2914-
|| ref $image_item eq 'PGplot'
2924+
|| ref $image_item eq 'Plots::Plot'
29152925
|| ref $image_item eq 'PGlateximage'
29162926
|| ref $image_item eq 'PGtikz');
29172927
my $imageURL = alias($image_item) // '';

0 commit comments

Comments
 (0)