Skip to content

Commit b8078d0

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

File tree

6 files changed

+265
-13
lines changed

6 files changed

+265
-13
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/JSX.pm

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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 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 ($xmin, $ymin, $xmax, $ymax) = $axes->bounds;
69+
my ($height, $width) = $pgplot->size;
70+
71+
$self->{board} =
72+
"<div id=\"board_$name\" class=\"jxgbox\" style=\"width: ${width}px; height: ${height}px;\"></div>";
73+
$self->{JS} = <<END_JS;
74+
const board_$name = JXG.JSXGraph.initBoard(
75+
'board_$name',
76+
{
77+
boundingbox: [$xmin, $ymax, $xmax, $ymin],
78+
axis: true,
79+
showNavigation: false,
80+
showCopyright: false,
81+
}
82+
);
83+
END_JS
84+
}
85+
86+
sub draw {
87+
my $self = shift;
88+
my $pgplot = $self->pgplot;
89+
my $name = $pgplot->get_image_name =~ s/-/_/gr;
90+
$self->{name} = $name;
91+
92+
$self->init_graph;
93+
94+
# Plot Data
95+
for my $data ($pgplot->data('function', 'dataset')) {
96+
$data->gen_data;
97+
$self->{JS} .=
98+
"\n\t\tboard_$name.create('curve', [[" . (join(',', $data->x)) . "],[" . (join(',', $data->y)) . "]]);";
99+
}
100+
101+
return $self->HTML;
102+
}
103+
104+
1;

lib/Plots/Plot.pm

Lines changed: 49 additions & 6 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,22 +152,40 @@ 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+
# Only use this type if using HTML output, fall back to Tikz otherwise.
157+
if (eval('$main::displayMode') =~ /HTML/) {
158+
$self->{type} = 'JSX';
159+
@validExt = ('html');
160+
} else {
161+
$self->{type} = 'Tikz';
162+
@validExt = ('svg', 'png', 'pdf');
163+
}
164+
} elsif ($type eq 'plotly') {
165+
# Only use this type if using HTML output, fall back to Tikz otherwise.
166+
if (eval('$main::displayMode') =~ /HTML/) {
167+
$self->{type} = 'Plotly';
168+
@validExt = ('html');
169+
} else {
170+
$self->{type} = 'Tikz';
171+
@validExt = ('svg', 'png', 'pdf');
172+
}
173+
} elsif ($type eq 'tikz') {
135174
$self->{type} = 'Tikz';
136175
@validExt = ('svg', 'png', 'pdf');
137176
} elsif ($type eq 'gd') {
138177
$self->{type} = 'GD';
139178
@validExt = ('png', 'gif');
140179
} else {
141-
warn "PGplot: Invalid image type $type.";
180+
warn "Plots: Invalid image type $type.";
142181
return;
143182
}
144183

145184
if ($ext) {
146185
if (grep(/^$ext$/, @validExt)) {
147186
$self->{ext} = $ext;
148187
} else {
149-
warn "PGplot: Invalid image extension $ext.";
188+
warn "Plots: Invalid image extension $ext.";
150189
}
151190
} else {
152191
$self->{ext} = $validExt[0];
@@ -367,10 +406,14 @@ sub draw {
367406
my $type = $self->{type};
368407

369408
my $image;
370-
if ($type eq 'GD') {
371-
$image = Plots::GD->new($self);
372-
} elsif ($type eq 'Tikz') {
409+
if ($type eq 'Tikz') {
373410
$image = Plots::Tikz->new($self);
411+
} elsif ($type eq 'JSX') {
412+
$image = Plots::JSX->new($self);
413+
} elsif ($type eq 'Plotly') {
414+
$image = Plots::Plotly->new($self);
415+
} elsif ($type eq 'GD') {
416+
$image = Plots::GD->new($self);
374417
} else {
375418
warn "Undefined image type: $type";
376419
return;

lib/Plots/Plotly.pm

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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 $plots = '';
49+
my ($xmin, $ymin, $xmax, $ymax) = $axes->bounds;
50+
my ($height, $width) = $pgplot->size;
51+
52+
for (@{ $self->{plots} }) {
53+
$plots .= $_;
54+
}
55+
56+
return <<END_HTML;
57+
<div id="plotlyDiv_$name" style="width: ${width}px; height: ${height}px;"></div>
58+
<script>
59+
(() => {
60+
const draw_graph = () => {
61+
const plotlyData = [];
62+
$plots
63+
Plotly.newPlot('plotlyDiv_$name', plotlyData);
64+
}
65+
if (document.readyState === 'loading') window.addEventListener('DOMContentLoaded', draw_graph);
66+
else draw_graph();
67+
})();
68+
</script>
69+
END_HTML
70+
}
71+
72+
sub draw {
73+
my $self = shift;
74+
my $pgplot = $self->pgplot;
75+
76+
# Plot Data
77+
for my $data ($pgplot->data('function', 'dataset')) {
78+
$data->gen_data;
79+
80+
my $x_points = join(',', $data->x);
81+
my $y_points = join(',', $data->y);
82+
my $plot = <<END_JS;
83+
plotlyData.push({
84+
x: [$x_points],
85+
y: [$y_points],
86+
mode: 'lines'
87+
});
88+
END_JS
89+
push(@{ $self->{plots} }, $plot);
90+
}
91+
92+
return $self->HTML;
93+
}
94+
95+
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) // '';

macros/graph/plots.pl

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,14 @@ =head1 NAME
1919
2020
=head1 DESCRIPTION
2121
22-
This macro creates a Plot object that is used to add data of different
22+
This macro creates a Plots object that is used to add data of different
2323
elements of a 2D plot, then draw the plot. The plots can be drawn using different
24-
formats. Currently the legacy GD graphics format and TikZ (using pgfplots)
25-
are available.
24+
formats. Currently C<TikZ> (using pgfplots), C<JSX> (using jsxgraph), C<Plotly>
25+
(using Plotly.js), and the legacy C<GD> graphics format are available.
2626
2727
=head1 USAGE
2828
29-
First create a PGplot object:
29+
First create a Plots object:
3030
3131
loadMacros('plots.pl');
3232
$plot = Plot();
@@ -60,7 +60,7 @@ =head1 USAGE
6060
=head1 PLOT ELEMENTS
6161
6262
A plot consists of multiple L<Data|/"DATA OBJECT"> objects, which define datasets, functions,
63-
and labels to add to the graph. Data objects should be created though the PGplot object,
63+
and labels to add to the graph. Data objects should be created though the Plots object,
6464
but can be access directly if needed
6565
6666
=head2 DATASETS

0 commit comments

Comments
 (0)