From d536a47835ba0ecc14e3efa70f24f9a16ffaab73 Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Wed, 25 Mar 2026 17:06:49 -0500 Subject: [PATCH] Make it possible to set custom tick positions and labels with the `plots.pl` macro. To set custom tick positions use the `tick_positions` axis option. Set that to a reference to an array containing the positions on the axis that ticks are desired. For example, `tick_positions => [ 2, 5, 9 ]` will place ticks at positions 2, 5, and 9 on the axis. Note that when this option is used the `tick_delta`, `tick_scale`, and `tick_distance` options are not used. So only the given tick positions will appear in the graph. To set custom tick labels use the `tick_labels` option. Note that this is not a new option, but now it accepts a new type of value. Previously this was purely boolean (0 or 1), and it only determined if tick labels would be shown or not. Now it can take a value that is a reference to a hash. The keys of the hash are tick positions, and the values are the labels to be placed at those positons. Note that formatting of the label must be done by the auther, and the `tick_label_format` option is ignored for any label provided in this hash. If a major tick is not listed in the hash, then the position will be used for the label and it will be formatted according to the `tick_label_format` option. This is intended to replace what is done in #1374 and is a more flexible approach than what is done there. In that pull request the capability for custom tick labels only is added, and it is extremely restrictive in what it can do. Only positive tick labels can be customized, and it requires that the problem author label all major ticks (there is no fallback and a tick is labeled "undefined" if one is missing). --- htdocs/js/Plots/plots.js | 21 ++++++++--- lib/Plots/Axes.pm | 40 +++++++++++++++++++- lib/Plots/JSXGraph.pm | 6 +++ lib/Plots/Tikz.pm | 80 ++++++++++++++++++++++++++++------------ 4 files changed, 116 insertions(+), 31 deletions(-) diff --git a/htdocs/js/Plots/plots.js b/htdocs/js/Plots/plots.js index 7e1c0416a..2534226de 100644 --- a/htdocs/js/Plots/plots.js +++ b/htdocs/js/Plots/plots.js @@ -86,9 +86,18 @@ const PGplots = { } }, - // Override the default axis generateLabelText method so that 0 is displayed - // using MathJax if the axis is configured to show tick labels using MathJax. + // Override the default axis generateLabelText method to show custom tick labels if they are set, and so + // that 0 is displayed using MathJax if the axis is configured to show tick labels using MathJax. generateLabelText(tick, zero, value) { + for (const axis of ['xAxis', 'yAxis']) { + if ( + this === plot[axis]?.defaultTicks && + typeof options[axis]?.ticks?.labels === 'object' && + tick.usrCoords[1] in options[axis].ticks.labels + ) { + return options[axis].ticks.labels[tick.usrCoords[1]]; + } + } if (JXG.exists(value)) return this.formatLabelText(value); const distance = this.getDistanceFromZero(zero, tick); return this.formatLabelText(Math.abs(distance) < JXG.Math.eps ? 0 : distance / this.visProp.scale); @@ -216,7 +225,7 @@ const PGplots = { strokeColor: options.grid.color ?? '#808080', strokeOpacity: options.grid.opacity ?? 0.2, insertTicks: false, - ticksDistance: options.xAxis?.ticks?.distance ?? 2, + ticksDistance: options.xAxis.ticks?.positions ?? options.xAxis.ticks?.distance ?? 2, scale: options.xAxis?.ticks?.scale ?? 1, minorTicks: options.grid.x.minorGrids ? (options.xAxis?.ticks?.minorTicks ?? 3) : 0, ignoreInfiniteTickEndings: false, @@ -280,7 +289,7 @@ const PGplots = { strokeColor: options.grid.color ?? '#808080', strokeOpacity: options.grid.opacity ?? 0.2, insertTicks: false, - ticksDistance: options.yAxis?.ticks?.distance ?? 2, + ticksDistance: options.yAxis.ticks?.positions ?? options.yAxis.ticks?.distance ?? 2, scale: options.yAxis?.ticks?.scale ?? 1, minorTicks: options.grid.y.minorGrids ? (options.yAxis?.ticks?.minorTicks ?? 3) : 0, ignoreInfiniteTickEndings: false, @@ -352,7 +361,7 @@ const PGplots = { ? true : false, insertTicks: false, - ticksDistance: options.xAxis.ticks?.distance ?? 2, + ticksDistance: options.xAxis.ticks?.positions ?? options.xAxis.ticks?.distance ?? 2, scale: options.xAxis.ticks?.scale ?? 1, scaleSymbol: options.xAxis.ticks?.scaleSymbol ?? '', minorTicks: options.xAxis.ticks?.minorTicks ?? 3, @@ -451,7 +460,7 @@ const PGplots = { ? true : false, insertTicks: false, - ticksDistance: options.yAxis.ticks?.distance ?? 2, + ticksDistance: options.yAxis.ticks?.positions ?? options.yAxis.ticks?.distance ?? 2, scale: options.yAxis.ticks?.scale ?? 1, scaleSymbol: options.yAxis.ticks?.scaleSymbol ?? '', minorTicks: options.yAxis.ticks?.minorTicks ?? 3, diff --git a/lib/Plots/Axes.pm b/lib/Plots/Axes.pm index 5141a255e..40d10573c 100644 --- a/lib/Plots/Axes.pm +++ b/lib/Plots/Axes.pm @@ -100,9 +100,37 @@ difference between the C and C divided by the C. Default: 0 =item tick_labels -This can be either 1 (show) or 0 (don't show) the labels for the major ticks. +This can be set to 1 to show the labels for the major ticks, 0 to not show the +labels for the major ticks, or to a reference to a hash whose keys are tick +positions, and whose values are tick labels to be shown at those positions. Default: 1 +The following is an example of passing a reference to a hash for this option. + + tick_labels => { 5 => '\(a\)' } + +In this case if there is a major tick at 5, then the label that will be shown +there is 'a' and the label will be rendered via MathJax. Note that if there is +not a major tick at 5, then the label will be unused. At any other major tick +that is shown, the position will be shown for the label and will be formatted +according to the C option. + +This option is most useful in combination with the C option +below. With that option the precise list of major ticks to be shown can be +specified, and the labels for those ticks specified with this option. For +example, + + tick_positions => [2, 4], + tick_labels => { 2 => 'a', 4 => 'b' }, + +would place a tick at 2 labeled 'a', and a tick at 4 labeled 'b'. No other ticks +would be shown on the axis. + +Note that if the hash reference value is used, the C does not +apply. You are responsible for formatting the labels as you would like them to +appear. If you want the labels rendered via MathJax, then wrap the labels in +C<\(> and C<\)>. + =item tick_label_format This can be one of "decimal", "fraction", "multiple", or "scinot". If this is @@ -133,6 +161,15 @@ C. Default: 1 This is appended to major tick labels. Default: '' +=item tick_positions + +Set this to a reference to an array of values to be used for the positions of +ticks that will be shown on the axis. In this case the C, +C, and C options will not be used to generate tick +positions. If this is set to 0 (or is not an array reference), then the tick +positions will be computed using the values of the C, +C, and C options. Default: 0 + =item show_ticks This can be either 1 (show) or 0 (don't show) the tick lines. If ticks are @@ -316,6 +353,7 @@ sub axis_defaults { tick_distance => 0, tick_scale => 1, tick_scale_symbol => '', + tick_positions => 0, show_ticks => 1, tick_delta => 0, tick_num => 5, diff --git a/lib/Plots/JSXGraph.pm b/lib/Plots/JSXGraph.pm index cc65c8d42..4716b4eee 100644 --- a/lib/Plots/JSXGraph.pm +++ b/lib/Plots/JSXGraph.pm @@ -90,6 +90,9 @@ sub HTML { $options->{xAxis}{ticks}{scale} = $axes->xaxis('tick_scale'); $options->{xAxis}{ticks}{distance} = $axes->xaxis('tick_distance'); $options->{xAxis}{ticks}{minorTicks} = $grid->{xminor}; + + my $xticks = $axes->xaxis('tick_positions'); + $options->{xAxis}{ticks}{positions} = $xticks if ref $xticks eq 'ARRAY'; } $options->{yAxis}{visible} = $yvisible; @@ -100,6 +103,9 @@ sub HTML { $options->{yAxis}{ticks}{scale} = $axes->yaxis('tick_scale'); $options->{yAxis}{ticks}{distance} = $axes->yaxis('tick_distance'); $options->{yAxis}{ticks}{minorTicks} = $grid->{yminor}; + + my $yticks = $axes->yaxis('tick_positions'); + $options->{yAxis}{ticks}{positions} = $yticks if ref $yticks eq 'ARRAY'; } if ($show_grid) { diff --git a/lib/Plots/Tikz.pm b/lib/Plots/Tikz.pm index 611273f29..048ae0f51 100644 --- a/lib/Plots/Tikz.pm +++ b/lib/Plots/Tikz.pm @@ -213,25 +213,41 @@ sub generate_axes { my $x_tick_distance = $axes->xaxis('tick_distance'); my $x_tick_scale = $axes->xaxis('tick_scale') || 1; - my @xticks = - grep { $_ > $xmin && $_ < $xmax } - map { -$_ * $x_tick_distance * $x_tick_scale } - reverse(1 .. -$xmin / ($x_tick_distance * $x_tick_scale)); - push(@xticks, 0) if $xmin < 0 && $xmax > 0; - push(@xticks, - grep { $_ > $xmin && $_ < $xmax } - map { $_ * $x_tick_distance * $x_tick_scale } (1 .. $xmax / ($x_tick_distance * $x_tick_scale))); + my $xtick_positions = $axes->xaxis('tick_positions'); + my @xticks; + if (ref $xtick_positions eq 'ARRAY') { + @xticks = @$xtick_positions; + } else { + @xticks = + grep { $_ > $xmin && $_ < $xmax } + map { -$_ * $x_tick_distance * $x_tick_scale } + reverse(1 .. -$xmin / ($x_tick_distance * $x_tick_scale)); + push(@xticks, 0) if $xmin < 0 && $xmax > 0; + push(@xticks, + grep { $_ > $xmin && $_ < $xmax } + map { $_ * $x_tick_distance * $x_tick_scale } (1 .. $xmax / ($x_tick_distance * $x_tick_scale))); + } + my $xtick_labels_value = $axes->xaxis('tick_labels'); my $xtick_labels = $xvisible && $axes->xaxis('show_ticks') - && $axes->xaxis('tick_labels') - ? (",\nxticklabel shift=9pt,\nxticklabel style={anchor=center},\nxticklabels={" - . join(',', map { $self->formatTickLabelText($_ / $x_tick_scale, 'xaxis') } @xticks) . '}') + && $xtick_labels_value + ? ( + ",\nxticklabel shift=9pt,\nxticklabel style={anchor=center},\nxticklabels={" . (join( + ',', + map { + ref $xtick_labels_value eq 'HASH' && defined $xtick_labels_value->{$_} + ? $xtick_labels_value->{$_} + : $self->formatTickLabelText($_ / $x_tick_scale, 'xaxis') + } @xticks + )) + . '}' + ) : ",\nxticklabel=\\empty"; my @xminor_ticks; - if ($grid->{xminor} > 0) { + if ($grid->{xminor} > 0 && ref $xtick_positions ne 'ARRAY') { my @majorTicks = @xticks; unshift(@majorTicks, ($majorTicks[0] // $xmin) - $x_tick_distance * $x_tick_scale); push(@majorTicks, ($majorTicks[-1] // $xmax) + $x_tick_distance * $x_tick_scale); @@ -246,25 +262,41 @@ sub generate_axes { my $y_tick_distance = $axes->yaxis('tick_distance'); my $y_tick_scale = $axes->yaxis('tick_scale') || 1; - my @yticks = - grep { $_ > $ymin && $_ < $ymax } - map { -$_ * $y_tick_distance * $y_tick_scale } - reverse(1 .. -$ymin / ($y_tick_distance * $y_tick_scale)); - push(@yticks, 0) if $ymin < 0 && $ymax > 0; - push(@yticks, - grep { $_ > $ymin && $_ < $ymax } - map { $_ * $y_tick_distance * $y_tick_scale } (1 .. $ymax / ($y_tick_distance * $y_tick_scale))); + my $ytick_positions = $axes->yaxis('tick_positions'); + my @yticks; + if (ref $ytick_positions eq 'ARRAY') { + @yticks = @$ytick_positions; + } else { + @yticks = + grep { $_ > $ymin && $_ < $ymax } + map { -$_ * $y_tick_distance * $y_tick_scale } + reverse(1 .. -$ymin / ($y_tick_distance * $y_tick_scale)); + push(@yticks, 0) if $ymin < 0 && $ymax > 0; + push(@yticks, + grep { $_ > $ymin && $_ < $ymax } + map { $_ * $y_tick_distance * $y_tick_scale } (1 .. $ymax / ($y_tick_distance * $y_tick_scale))); + } + my $ytick_labels_value = $axes->yaxis('tick_labels'); my $ytick_labels = $yvisible && $axes->yaxis('show_ticks') - && $axes->yaxis('tick_labels') - ? (",\nyticklabel shift=-3pt,\nyticklabels={" - . join(',', map { $self->formatTickLabelText($_ / $y_tick_scale, 'yaxis') } @yticks) . '}') + && $ytick_labels_value + ? ( + ",\nyticklabel shift=-3pt,\nyticklabel style={anchor=left},\nyticklabels={" . (join( + ',', + map { + ref $ytick_labels_value eq 'HASH' && defined $ytick_labels_value->{$_} + ? $ytick_labels_value->{$_} + : $self->formatTickLabelText($_ / $y_tick_scale, 'yaxis') + } @yticks + )) + . '}' + ) : ",\nyticklabel=\\empty"; my @yminor_ticks; - if ($grid->{yminor} > 0) { + if ($grid->{yminor} > 0 && ref $ytick_positions ne 'ARRAY') { my @majorTicks = @yticks; unshift(@majorTicks, ($majorTicks[0] // $ymin) - $y_tick_distance * $y_tick_scale); push(@majorTicks, ($majorTicks[-1] // $ymax) + $y_tick_distance * $y_tick_scale);