Skip to content

Commit 96735e5

Browse files
committed
Optimize the TikZ format when a draw and fill occur separately.
This is accomplished using the spath3 TikZ library. To make this work all paths need to be named, and if a draw and fill are done separately, then the fill just uses the path from the draw so it does not need to be recomputed. Additionally refactor multipaths to make the TikZ format much more efficient, as well as to make multipaths more versatile. Both the TikZ and JSXGraph formats are done differently now. They both join the paths in a completely different way that does not use the x transform (so the `function_string` x transform code is now never used in fact). Instead in TikZ the spath3 TikZ library is used and the paths are drawn individually, and then concatenated. For the JSXGraph format the paths are created individually and their data points concatenated to form a single curve. The result allows for more versatility since now paths do not need to end where the next path starts. The paths are connected by a line segment if needed. For JSXGraph this just works with the concatenation approach. For TikZ this has to be added. Note this actually happened before with the previous TikZ implementation, but not for the JSXGraph implementation. The most important thing is that with this implementation the time that it takes for TeX to run for multipaths is greatly reduced. For the example in the POD and the current TikZ code it takes about 3 seconds for TikZ to run, and the CPU usage is quite high. If the fill and draw are on different layers so that the fill and draw occur separately, it takes even longer. With the new code it takes about 1 second for either case and the CPU usage is much less. One reason for this is that the number of steps needed with the new approach (which is now per curve) is much less. Previously 500 steps were used (by default) for the entire curve. Now 30 (by default) are used for each curve. Note that this can now be optimized and steps set per curve. There is also a new `cycle` option for multipaths. If `cycly => 1` is set for a `multipath`, then a line segment will be inserted from the end of the last path to the start of the first in the case that the last path does not end at the start of the first, thus closing the path.
1 parent 7e9d44e commit 96735e5

File tree

4 files changed

+180
-80
lines changed

4 files changed

+180
-80
lines changed

lib/Plots/JSXGraph.pm

Lines changed: 51 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -390,37 +390,65 @@ sub add_multipath {
390390
my ($self, $data) = @_;
391391

392392
my @paths = @{ $data->{paths} };
393-
my $n = scalar(@paths);
394393
my $var = $data->{function}{var};
395394
my $curve_name = $data->style('name');
396395
warn 'Duplicate plot name detected. This will most likely cause issues. Make sure that all names used are unique.'
397396
if $curve_name && $self->{names}{$curve_name};
398397
$self->{names}{$curve_name} = 1 if $curve_name;
399398
my ($plotOptions, $fillOptions) = $self->get_options($data);
400-
my $jsFunctionx = 'function (x){';
401-
my $jsFunctiony = 'function (x){';
399+
400+
my $count = 0;
401+
unless ($curve_name) {
402+
++$count while ($self->{names}{"_plots_internal_$count"});
403+
$curve_name = "_plots_internal_$count";
404+
$self->{names}{$curve_name} = 1;
405+
}
406+
407+
$count = 0;
408+
++$count while ($self->{names}{"${curve_name}_$count"});
409+
my $curve_parts_name = "${curve_name}_$count";
410+
$self->{names}{$curve_parts_name} = 1;
411+
412+
$self->{JS} .= "const $curve_parts_name = [\n";
413+
414+
my $cycle = $data->style('cycle');
415+
my ($start_x, $start_y) = ('', '');
402416

403417
for (0 .. $#paths) {
404418
my $path = $paths[$_];
405-
my $a = $_ / $n;
406-
my $b = ($_ + 1) / $n;
407-
my $tmin = $path->{tmin};
408-
my $tmax = $path->{tmax};
409-
my $m = ($tmax - $tmin) / ($b - $a);
410-
my $tmp = $a < 0 ? 'x+' . (-$a) : "x-$a";
411-
my $t = $m < 0 ? "($tmin$m*($tmp))" : "($tmin+$m*($tmp))";
412-
413-
my $xfunction = $data->function_string($path->{Fx}, 'js', $var, undef, $t);
414-
my $yfunction = $data->function_string($path->{Fy}, 'js', $var, undef, $t);
415-
$jsFunctionx .= "if(x<=$b){return $xfunction;}";
416-
$jsFunctiony .= "if(x<=$b){return $yfunction;}";
419+
420+
($start_x, $start_y) =
421+
(', ' . $path->{Fx}->eval($var => $path->{tmin}), ', ' . $path->{Fy}->eval($var => $path->{tmin}))
422+
if $cycle && $_ == 0;
423+
424+
my $xfunction = $data->function_string($path->{Fx}, 'js', $var);
425+
my $yfunction = $data->function_string($path->{Fy}, 'js', $var);
426+
427+
$self->{JS} .=
428+
"board.create('curve', "
429+
. "[(x) => $xfunction, (x) => $yfunction, $path->{tmin}, $path->{tmax}], { visible: false }),\n";
417430
}
418-
$jsFunctionx .= 'return 0;}';
419-
$jsFunctiony .= 'return 0;}';
420431

421-
$self->{JS} .= "const curve_${curve_name} = " if $curve_name;
422-
$self->{JS} .= "board.create('curve', [$jsFunctionx, $jsFunctiony, 0, 1], $plotOptions);" if $plotOptions;
423-
$self->{JS} .= "board.create('curve', [$jsFunctionx, $jsFunctiony, 0, 1], $fillOptions);" if $fillOptions;
432+
$self->{JS} .= "];\n";
433+
434+
if ($plotOptions) {
435+
$self->{JS} .= <<~ "END_JS";
436+
const curve_$curve_name = board.create('curve', [[], []], $plotOptions);
437+
curve_$curve_name.updateDataArray = function () {
438+
this.dataX = [].concat(...$curve_parts_name.map((c) => c.points.map((p) => p.usrCoords[1]))$start_x);
439+
this.dataY = [].concat(...$curve_parts_name.map((c) => c.points.map((p) => p.usrCoords[2]))$start_y);
440+
};
441+
END_JS
442+
}
443+
if ($fillOptions) {
444+
$self->{JS} .= <<~ "END_JS";
445+
const fill_$curve_name = board.create('curve', [[], []], $fillOptions);
446+
fill_$curve_name.updateDataArray = function () {
447+
this.dataX = [].concat(...$curve_parts_name.map((c) => c.points.map((p) => p.usrCoords[1])));
448+
this.dataY = [].concat(...$curve_parts_name.map((c) => c.points.map((p) => p.usrCoords[2])));
449+
};
450+
END_JS
451+
}
424452
return;
425453
}
426454

@@ -530,8 +558,8 @@ sub add_circle {
530558
my $r = $data->style('radius');
531559
my ($circleOptions, $fillOptions) = $self->get_options($data);
532560

533-
$self->{JS} .= "board.create('circle', [[$x, $y], $r], $circleOptions);";
534-
$self->{JS} .= "board.create('circle', [[$x, $y], $r], $fillOptions);" if $fillOptions;
561+
$self->{JS} .= "board.create('circle', [[$x, $y], $r], $circleOptions);" if $circleOptions;
562+
$self->{JS} .= "board.create('circle', [[$x, $y], $r], $fillOptions);" if $fillOptions;
535563
return;
536564
}
537565

@@ -547,7 +575,7 @@ sub add_arc {
547575
radiusPoint => { visible => 0 },
548576
);
549577

550-
$self->{JS} .= "board.create('arc', [[$x1, $y1], [$x2, $y2], [$x3, $y3]], $arcOptions);";
578+
$self->{JS} .= "board.create('arc', [[$x1, $y1], [$x2, $y2], [$x3, $y3]], $arcOptions);" if $arcOptions;
551579
$self->{JS} .= "board.create('arc', [[$x1, $y1], [$x2, $y2], [$x3, $y3]], $fillOptions);" if $fillOptions;
552580
return;
553581
}

lib/Plots/Plot.pm

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -294,15 +294,15 @@ sub add_function {
294294
sub add_multipath {
295295
my ($self, $paths, $var, %options) = @_;
296296
my $data = Plots::Data->new(name => 'multipath');
297-
my $steps = 100 * @$paths; # Steps set high to help Tikz deal with boundaries of paths.
298-
$steps = delete $options{steps} if $options{steps};
297+
my $steps = (delete $options{steps}) || 30;
299298
$data->{context} = $self->context;
300299
$data->{paths} = [
301300
map { {
302301
Fx => $data->get_math_object($_->[0], $var),
303302
Fy => $data->get_math_object($_->[1], $var),
304303
tmin => $data->str_to_real($_->[2]),
305-
tmax => $data->str_to_real($_->[3])
304+
tmax => $data->str_to_real($_->[3]),
305+
@$_[ 4 .. $#$_ ]
306306
} } @$paths
307307
];
308308
$data->{function} = { var => $var, steps => $steps };

lib/Plots/Tikz.pm

Lines changed: 82 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ sub new {
1919
$image->svgMethod(eval('$main::envir{latexImageSVGMethod}') // 'dvisvgm');
2020
$image->convertOptions(eval('$main::envir{latexImageConvertOptions}') // { input => {}, output => {} });
2121
$image->ext($plots->ext);
22-
$image->tikzLibraries('arrows.meta,plotmarks,calc');
22+
$image->tikzLibraries('arrows.meta,plotmarks,calc,spath3');
2323
$image->texPackages(['pgfplots']);
2424

2525
# Set the pgfplots compatibility, add the pgfplots fillbetween library, define a save
@@ -458,18 +458,8 @@ sub get_options {
458458
my $fill_color = $data->style('fill_color') || $data->style('color') || 'default_color';
459459
my $fill_opacity = $data->style('fill_opacity') || 0.5;
460460
push(@drawOptions, "fill=$fill_color", "fill opacity=$fill_opacity");
461-
}
462-
463-
my $name = $data->style('name');
464-
if ($name) {
465-
warn 'Duplicate plot name detected. This will most likely cause issues. '
466-
. 'Make sure that all names used are unique.'
467-
if $self->{names}{$name};
468-
$self->{names}{$name} = 1;
469-
# This forces the curve to be inserted invisibly if it has been named,
470-
# but the curve would otherwise not be drawn.
471-
push(@drawOptions, 'draw=none') if !@drawOptions;
472-
push(@drawOptions, "name path=$name");
461+
} elsif (!@drawOptions) {
462+
push(@drawOptions, 'draw=none');
473463
}
474464

475465
push(@drawOptions, $tikz_options) if $tikz_options;
@@ -539,6 +529,19 @@ sub draw {
539529
next;
540530
}
541531

532+
my $curve_name = $data->style('name');
533+
warn 'Duplicate plot name detected. This will most likely cause issues. '
534+
. 'Make sure that all names used are unique.'
535+
if $curve_name && $self->{names}{$curve_name};
536+
$self->{names}{$curve_name} = 1 if $curve_name;
537+
538+
my $count = 0;
539+
unless ($curve_name) {
540+
++$count while ($self->{names}{"_plots_internal_$count"});
541+
$curve_name = "_plots_internal_$count";
542+
$self->{names}{$curve_name} = 1;
543+
}
544+
542545
my $fill = $data->style('fill') || 'none';
543546
my $fill_color = $data->style('fill_color') || $data->style('color') || 'default_color';
544547
$tikzCode .= $self->get_color($fill_color) unless $fill eq 'none';
@@ -549,11 +552,12 @@ sub draw {
549552
my $x = $data->x(0);
550553
my $y = $data->y(0);
551554
my $r = $data->style('radius');
552-
$tikzCode .= $self->draw_on_layer("\\fill[$fill_options->[0]] (axis cs:$x,$y) circle[radius=$r];\n",
553-
$fill_options->[1])
554-
if $fill_options;
555-
$tikzCode .= $self->draw_on_layer("\\draw[$draw_options->[0]] (axis cs:$x,$y) circle[radius=$r];\n",
555+
$tikzCode .= $self->draw_on_layer(
556+
"\\draw[name path=$curve_name, $draw_options->[0]] (axis cs:$x,$y) circle[radius=$r];\n",
556557
$draw_options->[1]);
558+
$tikzCode .=
559+
$self->draw_on_layer("\\fill[$fill_options->[0]] [spath/use=$curve_name];\n", $fill_options->[1])
560+
if $fill_options;
557561
next;
558562
}
559563
if ($data->name eq 'arc') {
@@ -566,20 +570,19 @@ sub draw {
566570
$theta1 += 360 if $theta1 < 0;
567571
$theta2 += 360 if $theta2 < 0;
568572
$tikzCode .= $self->draw_on_layer(
569-
"\\fill[$fill_options->[0]] (axis cs:$x2,$y2) "
570-
. "arc[start angle=$theta1, end angle=$theta2, radius = $r];\n",
571-
$fill_options->[1]
572-
) if $fill_options;
573-
$tikzCode .= $self->draw_on_layer(
574-
"\\draw[$draw_options->[0]] (axis cs:$x2,$y2) "
573+
"\\draw[name path=$curve_name, $draw_options->[0]] (axis cs:$x2,$y2) "
575574
. "arc[start angle=$theta1, end angle=$theta2, radius = $r];\n",
576575
$draw_options->[1]
577576
);
577+
$tikzCode .=
578+
$self->draw_on_layer("\\fill[$fill_options->[0]] [spath/use=$curve_name];\n", $fill_options->[1])
579+
if $fill_options;
578580
next;
579581
}
580582

581583
my $plot;
582584
my $plot_options = '';
585+
583586
if ($data->name eq 'function') {
584587
my $f = $data->{function};
585588
if (ref($f->{Fx}) ne 'CODE' && $f->{xvar} eq $f->{Fx}->string) {
@@ -599,43 +602,79 @@ sub draw {
599602
$plot = "({$xfunction}, {$yfunction})";
600603
}
601604
}
602-
}
603-
if ($data->name eq 'multipath') {
605+
} elsif ($data->name eq 'multipath') {
604606
my $var = $data->{function}{var};
605607
my @paths = @{ $data->{paths} };
606-
my $n = scalar(@paths);
607608
my @tikzFunctionx;
608609
my @tikzFunctiony;
610+
611+
# This saves the internal path names and the endpoints of the paths. The endpoints are used to determine if
612+
# the paths meet at the endpoints. If the end of one path is not at the same place that the next path
613+
# starts, then the line segment from the first path end to the next path start is inserted.
614+
my @pathData;
615+
616+
my $count = 0;
617+
609618
for (0 .. $#paths) {
610619
my $path = $paths[$_];
611-
my $a = $_ / $n;
612-
my $b = ($_ + 1) / $n;
613-
my $tmin = $path->{tmin};
614-
my $tmax = $path->{tmax};
615-
my $m = ($tmax - $tmin) / ($b - $a);
616-
my $tmp = $a < 0 ? 'x+' . (-$a) : "x-$a";
617-
my $t = $m < 0 ? "($tmin$m*($tmp))" : "($tmin+$m*($tmp))";
618-
619-
my $xfunction = $data->function_string($path->{Fx}, 'PGF', $var, undef, $t);
620-
my $yfunction = $data->function_string($path->{Fy}, 'PGF', $var, undef, $t);
621-
my $last = $_ == $#paths ? '=' : '';
622-
push(@tikzFunctionx, "(x>=$a)*(x<$last$b)*($xfunction)");
623-
push(@tikzFunctiony, "(x>=$a)*(x<$last$b)*($yfunction)");
620+
621+
my $xfunction = $data->function_string($path->{Fx}, 'PGF', $var);
622+
my $yfunction = $data->function_string($path->{Fy}, 'PGF', $var);
623+
624+
++$count while $self->{names}{"${curve_name}_$count"};
625+
push(
626+
@pathData,
627+
[
628+
"${curve_name}_$count",
629+
$path->{Fx}->eval($var => $path->{tmin}),
630+
$path->{Fy}->eval($var => $path->{tmin}),
631+
$path->{Fx}->eval($var => $path->{tmax}),
632+
$path->{Fy}->eval($var => $path->{tmax})
633+
]
634+
);
635+
$self->{names}{ $pathData[-1][0] } = 1;
636+
637+
my $steps = $path->{steps} // $data->{function}{steps};
638+
639+
$tikzCode .=
640+
"\\addplot[name path=$pathData[-1][0], draw=none, domain=$path->{tmin}:$path->{tmax}, "
641+
. "samples=$steps] ({$xfunction}, {$yfunction});\n";
624642
}
625-
$plot_options .= ", mark=none, domain=0:1, samples=$data->{function}{steps}";
626-
$plot = "\n({" . join("\n+", @tikzFunctionx) . "},\n{" . join("\n+", @tikzFunctiony) . '})';
643+
644+
$tikzCode .= "\\path[name path=$curve_name] " . join(
645+
' ',
646+
map {
647+
(
648+
$_ == 0 || ($pathData[ $_ - 1 ][3] == $pathData[$_][1]
649+
&& $pathData[ $_ - 1 ][4] == $pathData[$_][2])
650+
? ''
651+
: "-- (spath cs:$pathData[$_ - 1][0] 1) -- (spath cs:$pathData[$_][0] 0) "
652+
)
653+
. "[spath/append no move=$pathData[$_][0]]"
654+
} 0 .. $#pathData
655+
) . ($data->style('cycle') ? '-- cycle' : '') . ";\n";
656+
657+
$plot = 'skip';
658+
$tikzCode .=
659+
$self->draw_on_layer("\\draw[$draw_options->[0], spath/use=$curve_name];\n", $draw_options->[1]);
627660
}
661+
628662
unless ($plot) {
629663
$data->gen_data;
630664
$plot = 'coordinates {'
631665
. join(' ', map { '(' . $data->x($_) . ',' . $data->y($_) . ')'; } (0 .. $data->size - 1)) . '}';
632666
}
633-
$tikzCode .= $self->draw_on_layer("\\addplot[$fill_options->[0]$plot_options] $plot;\n", $fill_options->[1])
667+
668+
# 'skip' is a special value of $plot for a multipath which has already been drawn.
669+
$tikzCode .= $self->draw_on_layer("\\addplot[name path=$curve_name, $draw_options->[0]$plot_options] $plot;\n",
670+
$draw_options->[1])
671+
unless $plot eq 'skip';
672+
$tikzCode .= $self->draw_on_layer("\\fill[$fill_options->[0]] [spath/use=$curve_name];\n", $fill_options->[1])
634673
if $fill_options;
635-
$tikzCode .= $self->draw_on_layer("\\addplot[$draw_options->[0]$plot_options] $plot;\n", $draw_options->[1]);
636674

637675
unless ($fill eq 'none' || $fill eq 'self') {
638676
if ($self->{names}{$fill}) {
677+
# Make sure this is the name from the data style attribute, and not an internal name.
639678
my $name = $data->style('name');
640679
if ($name) {
641680
my $opacity = $data->style('fill_opacity') || 0.5;

macros/graph/plots.pl

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -197,26 +197,34 @@ =head2 PLOT FUNCTIONS
197197
198198
=head2 PLOT MULTIPATH FUNCTIONS
199199
200-
A multipath function is defined using multiple parametric paths pieced together into into a single
201-
curve, whose primary use is to create a closed region to be filled using multiple boundaries.
202-
This is done by providing a list of parametric functions, the name of the parameter, and a list
203-
of options.
200+
A multipath function is defined using multiple parametric paths pieced together
201+
into into a single curve, whose primary use is to create a closed region to be
202+
filled using multiple boundaries. This is done by providing a list of
203+
parametric functions, the name of the parameter, and a list of options.
204204
205205
$plot->add_multipath(
206206
[
207-
[ $function_x1, $function_y1, $min1, $max1 ],
208-
[ $function_x2, $function_y2, $min2, $max2 ],
207+
[ $function_x1, $function_y1, $min1, $max1, %path_options ],
208+
[ $function_x2, $function_y2, $min2, $max2, %path_options ],
209209
...
210210
],
211211
$variable,
212212
%options
213213
);
214214
215-
The paths have to be listed in the order they are followed, but the minimum/maximum values
216-
of the parameter can match the parametrization. The following example creates a sector of
217-
radius 5 between pi/4 and 3pi/4, by first drawing the line (0,0) to (5sqrt(2),5/sqrt(2)),
218-
then the arc of the circle of radius 5 from pi/4 to 3pi/4, followed by the final line from
219-
(-5sqrt(2), 5sqrt(2)) back to the origin.
215+
Note that C<%path_options> can be specified for each path. At this point, the
216+
only supported individual path option is C<steps>, if specified, then that
217+
number of steps will be used for that path in the TikZ format. If not specified
218+
the number of steps for the multipath will be used. That defaults to 30, but can
219+
be changed by passing the C<steps> option in the general C<%options> for the
220+
multipath.
221+
222+
The paths have to be listed in the order they are followed, but the
223+
minimum/maximum values of the parameter can match the parametrization. The
224+
following example creates a sector of radius 5 between pi/4 and 3pi/4, by first
225+
drawing the line from (0,0) to (5sqrt(2),5/sqrt(2)), then the arc of the circle
226+
of radius 5 from pi/4 to 3pi/4, followed by the final line from (-5sqrt(2),
227+
5sqrt(2)) back to the origin.
220228
221229
$plot->add_multipath(
222230
[
@@ -229,6 +237,31 @@ =head2 PLOT MULTIPATH FUNCTIONS
229237
fill => 'self',
230238
);
231239
240+
Note that the ending point of one path does not need to be the same as the
241+
starting point of the next. In this case a line segment will connect the end of
242+
the first path to the start of the next. Additionally, if C<< cycle => 1 >> is
243+
added to the C<%options> for the multipath, and the last path does not end where
244+
the first path starts, then a line segment will connect the end of the last path
245+
to the start of the first path. For example, the following path draws the top
246+
half of a circle of radius two centered at the point (0, 2), followed by the
247+
line segment from (-2, 0) to (2, 0). The line segment from (-2, 2) to (-2, 0) is
248+
implicitly added to connect the end of the first path to the beginning of the
249+
second path. The cycle option is added to close the path with the line segment
250+
from (2, 0) to (2, 2). Note that drawing of the line is optimized by using only
251+
2 steps, and the fill region is drawn on the "axis background" layer.
252+
253+
$plot->add_multipath(
254+
[
255+
[ '2 cos(t) + 5', '2 sin(t) - 5', '0', 'pi' ],
256+
[ 't', '-8', '3', '7', steps => 2 ]
257+
],
258+
't',
259+
color => 'green',
260+
fill => 'self',
261+
fill_layer => 'axis background',
262+
cycle => 1
263+
);
264+
232265
=head2 PLOT CIRCLES
233266
234267
Circles can be added to the plot by specifying its center and radius using the

0 commit comments

Comments
 (0)