Skip to content

Commit c5a694a

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

File tree

11 files changed

+362
-36
lines changed

11 files changed

+362
-36
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/GD.pm

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,15 +69,15 @@ sub im_x {
6969
return unless defined($x);
7070
my $pgplot = $self->pgplot;
7171
my ($xmin, $xmax) = ($pgplot->axes->xaxis('min'), $pgplot->axes->xaxis('max'));
72-
return int(($x - $xmin) * ($pgplot->size)[0] / ($xmax - $xmin));
72+
return int(($x - $xmin) * $pgplot->{width} / ($xmax - $xmin));
7373
}
7474

7575
sub im_y {
7676
my ($self, $y) = @_;
7777
return unless defined($y);
7878
my $pgplot = $self->pgplot;
7979
my ($ymin, $ymax) = ($pgplot->axes->yaxis('min'), $pgplot->axes->yaxis('max'));
80-
return int(($ymax - $y) * ($pgplot->size)[1] / ($ymax - $ymin));
80+
return int(($ymax - $y) * $pgplot->{height} / ($ymax - $ymin));
8181
}
8282

8383
sub moveTo {
@@ -235,7 +235,8 @@ sub draw {
235235
my $pgplot = $self->pgplot;
236236
my $axes = $pgplot->axes;
237237
my $grid = $axes->grid;
238-
my $size = $pgplot->size;
238+
my $width = $pgplot->{width};
239+
my $height = $pgplot->{height};
239240

240241
# Initialize image
241242
$self->im->interlaced('true');
@@ -379,7 +380,7 @@ sub draw {
379380
}
380381

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

384385
return $pgplot->ext eq 'gif' ? $self->im->gif : $self->im->png;
385386
}

lib/Plots/JSX.pm

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
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 get_color {
63+
my ($self, $color) = @_;
64+
return sprintf("#%x%x%x", @{ $self->pgplot->colors($color) });
65+
}
66+
67+
sub add_curve {
68+
my ($self, $data) = @_;
69+
my $linestyle = $data->style('linestyle');
70+
return if $linestyle eq 'none';
71+
72+
if ($linestyle eq 'densely dashed') {
73+
$linestyle = ',dash: 4, dashScale: true';
74+
} elsif ($linestyle eq 'loosely dashed') {
75+
$linestyle = ',dash: 3, dashScale: true';
76+
} elsif ($linestyle =~ /dashed/) {
77+
$linestyle = ',dash: 1, dashScale: true';
78+
} elsif ($linestyle =~ /dotted/) {
79+
$linestyle = ',dash: 1';
80+
} else {
81+
$linestyle = '';
82+
}
83+
84+
my $name = $self->{name};
85+
my $color = $self->get_color($data->style('color') || 'default_color');
86+
my $data_points = '[[' . join(',', $data->x) . '],[' . join(',', $data->y) . ']]';
87+
my $line_width = $data->style('width') || 2;
88+
89+
$self->{JS} .= "\n\t\tboard_$name.create('curve', $data_points, "
90+
. "{strokeColor: '$color', strokeWidth: $line_width$linestyle});";
91+
}
92+
93+
sub add_points {
94+
my ($self, $data) = @_;
95+
my $mark = $data->style('marks');
96+
return if !$mark || $mark eq 'none';
97+
98+
if ($mark eq 'plus' || $mark eq 'oplus') {
99+
$mark = ',face: "plus"';
100+
} elsif ($mark eq 'times' || $mark eq 'otimes') {
101+
$mark = ',face: "cross"';
102+
} elsif ($mark eq 'dash') {
103+
$mark = ',face: "minus"';
104+
} elsif ($mark eq 'bar') {
105+
$mark = ',face: "divide"';
106+
} elsif ($mark eq 'diamond') {
107+
$mark = ',face: "diamond"';
108+
} elsif ($mark eq 'open_circle') {
109+
$mark = ',fillColor: "white"';
110+
} else {
111+
$mark = '';
112+
}
113+
114+
my $name = $self->{name};
115+
my $size = $data->style('mark_size') || $data->style('width') || 3;
116+
117+
for my $i (0 .. $data->size - 1) {
118+
$self->{JS} .=
119+
"\n\t\tboard_$name.create('point', ["
120+
. $data->x($i) . ','
121+
. $data->y($i) . '], '
122+
. "{fixed: true, withLabel: false, size: $size$mark});";
123+
}
124+
}
125+
126+
sub init_graph {
127+
my $self = shift;
128+
my $pgplot = $self->pgplot;
129+
my $axes = $pgplot->axes;
130+
my $grid = $axes->grid;
131+
my $name = $self->{name};
132+
my $title = $axes->style('title');
133+
my ($xmin, $ymin, $xmax, $ymax) = $axes->bounds;
134+
my ($height, $width) = $pgplot->size;
135+
my $style = 'display: inline-block; margin: 5px; text-align: center;';
136+
137+
$title = "<strong>$title</strong>" if $title;
138+
$self->{board} = <<END_HTML;
139+
<div style="$style">$title
140+
<div id="board_$name" class="jxgbox" style="width: ${width}px; height: ${height}px;"></div>
141+
</div>
142+
END_HTML
143+
$self->{JS} = <<END_JS;
144+
const board_$name = JXG.JSXGraph.initBoard(
145+
'board_$name',
146+
{
147+
boundingbox: [$xmin, $ymax, $xmax, $ymin],
148+
axis: true,
149+
showNavigation: false,
150+
showCopyright: false,
151+
}
152+
);
153+
END_JS
154+
}
155+
156+
sub draw {
157+
my $self = shift;
158+
my $pgplot = $self->pgplot;
159+
my $name = $pgplot->get_image_name =~ s/-/_/gr;
160+
$self->{name} = $name;
161+
162+
$self->init_graph;
163+
164+
# Plot Data
165+
for my $data ($pgplot->data('function', 'dataset')) {
166+
$data->gen_data;
167+
$self->add_curve($data);
168+
$self->add_points($data);
169+
}
170+
171+
return $self->HTML;
172+
}
173+
174+
1;

lib/Plots/Plot.pm

Lines changed: 49 additions & 14 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 {
@@ -39,8 +41,10 @@ sub new {
3941
pg => $pg,
4042
imageName => {},
4143
type => 'Tikz',
42-
ext => 'svg',
43-
size => [ $size, $size ],
44+
ext => $pg->{displayMode} eq 'TeX' ? 'pdf' : 'svg',
45+
width => $size,
46+
height => $size,
47+
tex_size => 500,
4448
axes => Plots::Axes->new,
4549
colors => {},
4650
data => [],
@@ -51,6 +55,24 @@ sub new {
5155
return $self;
5256
}
5357

58+
# Only insert js file if it isn't already inserted.
59+
sub insert_js {
60+
my ($self, $file) = @_;
61+
for my $obj (@{ $self->{pg}{flags}{extra_js_files} }) {
62+
return if $obj->{file} eq $file;
63+
}
64+
push(@{ $self->{pg}{flags}{extra_js_files} }, { file => $file, external => 0, attributes => { defer => undef } });
65+
}
66+
67+
# Only insert css file if it isn't already inserted.
68+
sub insert_css {
69+
my ($self, $file) = @_;
70+
for my $obj (@{ $self->{pg}{flags}{extra_css_files} }) {
71+
return if $obj->{file} eq $file;
72+
}
73+
push(@{ $self->{pg}{flags}{extra_css_files} }, { file => $file, external => 0 });
74+
}
75+
5476
sub colors {
5577
my ($self, $color) = @_;
5678
return defined($color) ? $self->{colors}{$color} : $self->{colors};
@@ -85,7 +107,7 @@ sub color_init {
85107

86108
sub size {
87109
my $self = shift;
88-
return wantarray ? @{ $self->{size} } : $self->{size};
110+
return wantarray ? ($self->{width}, $self->{height}) : [ $self->{width}, $self->{height} ];
89111
}
90112

91113
sub data {
@@ -131,41 +153,50 @@ sub image_type {
131153
# Check type and extension are valid. The first element of @validExt is used as default.
132154
my @validExt;
133155
$type = lc($type);
134-
if ($type eq 'tikz') {
156+
if ($type eq 'jsx') {
157+
$self->{type} = 'JSX';
158+
@validExt = ('html');
159+
} elsif ($type eq 'plotly') {
160+
$self->{type} = 'Plotly';
161+
@validExt = ('html');
162+
} elsif ($type eq 'tikz') {
135163
$self->{type} = 'Tikz';
136164
@validExt = ('svg', 'png', 'pdf');
137165
} elsif ($type eq 'gd') {
138166
$self->{type} = 'GD';
139167
@validExt = ('png', 'gif');
140168
} else {
141-
warn "PGplot: Invalid image type $type.";
169+
warn "Plots: Invalid image type $type.";
142170
return;
143171
}
144172

145173
if ($ext) {
146174
if (grep(/^$ext$/, @validExt)) {
147175
$self->{ext} = $ext;
148176
} else {
149-
warn "PGplot: Invalid image extension $ext.";
177+
warn "Plots: Invalid image extension $ext.";
150178
}
151179
} else {
152180
$self->{ext} = $validExt[0];
153181
}
182+
183+
# Hardcopy: Tikz needs to use the 'pdf' extension and fallback to Tikz output if ext is 'html'.
184+
if ($self->{pg}{displayMode} eq 'TeX' && ($self->{ext} eq 'html' || $self->{type} eq 'Tikz')) {
185+
$self->{type} = 'Tikz';
186+
$self->{ext} = 'pdf';
187+
}
154188
return;
155189
}
156190

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

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

171202
# Add functions to the graph.
@@ -367,10 +398,14 @@ sub draw {
367398
my $type = $self->{type};
368399

369400
my $image;
370-
if ($type eq 'GD') {
371-
$image = Plots::GD->new($self);
372-
} elsif ($type eq 'Tikz') {
401+
if ($type eq 'Tikz') {
373402
$image = Plots::Tikz->new($self);
403+
} elsif ($type eq 'JSX') {
404+
$image = Plots::JSX->new($self);
405+
} elsif ($type eq 'Plotly') {
406+
$image = Plots::Plotly->new($self);
407+
} elsif ($type eq 'GD') {
408+
$image = Plots::GD->new($self);
374409
} else {
375410
warn "Undefined image type: $type";
376411
return;

0 commit comments

Comments
 (0)