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);