diff --git a/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/RTTank.java b/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/RTTank.java index 8c294c7b05..9623ceb72b 100644 --- a/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/RTTank.java +++ b/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/RTTank.java @@ -97,6 +97,10 @@ public class RTTank extends Canvas /** Current value, i.e. fill level */ private volatile double value = 5.0; + /** Requested value range, see {@link #setRange}; NaN until set */ + private volatile double rangeLow = Double.NaN; + private volatile double rangeHigh = Double.NaN; + /** Does layout need to be re-computed? */ protected final AtomicBoolean need_layout = new AtomicBoolean(true); @@ -273,6 +277,7 @@ public void setLogScale(final boolean logscale) { scale.setLogarithmic(logscale); right_scale.setLogarithmic(logscale); + applyRange(); requestUpdate(); } @@ -441,34 +446,65 @@ public void setPerpendicularTickLabels(final boolean perpendicular) requestUpdate(); } - /** Set value range - * @param low Lower limit - * @param high Upper limit + /** Set value range. + * + *

An inverted range ({@code low > high}) runs the scale top-down + * and fills the tank from the top. A logarithmic scale always runs + * bottom-up. Non-finite and zero-width ranges are ignored. + * + * @param low Value at the bottom of the tank + * @param high Value at the top of the tank */ public void setRange(final double low, final double high) { - // Guard against NaN, Infinite, or inverted/flat range - if (!Double.isFinite(low) || !Double.isFinite(high) || low >= high) + if (!Double.isFinite(low) || !Double.isFinite(high) || low == high) + return; + rangeLow = low; + rangeHigh = high; + applyRange(); + } + + /** @return Current value range of the scale */ + public AxisRange getValueRange() + { + return scale.getValueRange(); + } + + /** Push the requested range to both scales, ascending for a log scale */ + private void applyRange() + { + double low = rangeLow; + double high = rangeHigh; + if (Double.isNaN(low)) return; + if (scale.isLogarithmic() && low > high) + { + low = rangeHigh; + high = rangeLow; + } scale.setValueRange(low, high); right_scale.setValueRange(low, high); } - /** @param value Set value */ + /** @param value Set value; a non-finite value shows an empty tank */ public void setValue(final double value) { if (Double.isFinite(value)) this.value = value; else - this.value = scale.getValueRange().getLow(); + { + final AxisRange range = scale.getValueRange(); + this.value = Math.min(range.getLow(), range.getHigh()); + } requestUpdate(); } - /** Map a value to a Y pixel within the plot bounds (low value at bottom). + /** Map a value to a Y pixel within the plot bounds. * Returns -1 when the mapping is undefined (e.g. log scale with non-positive inputs). + * @param normal Range runs bottom-up? Otherwise the low value is at the top */ private int valueToY(final Rectangle pb, final double min, final double max, - final double v, final boolean logscale) + final double v, final boolean logscale, final boolean normal) { final double frac; if (logscale) @@ -479,17 +515,17 @@ private int valueToY(final Rectangle pb, final double min, final double max, } else frac = (v - min) / (max - min); - return (int) (pb.y + pb.height * (1.0 - frac)); + return (int) (pb.y + pb.height * (normal ? 1.0 - frac : frac)); } /** Draw a single horizontal limit line across the tank area at the given value. */ private void drawLimitLineAt(final Graphics2D gc, final Rectangle pb, - final double min, final double max, + final double min, final double max, final boolean normal, final double limit, final Color color) { if (!Double.isFinite(limit) || limit <= min || limit >= max) return; - final int y = valueToY(pb, min, max, limit, scale.isLogarithmic()); + final int y = valueToY(pb, min, max, limit, scale.isLogarithmic(), normal); if (y < pb.y || y > pb.y + pb.height) return; gc.setColor(color); @@ -642,18 +678,18 @@ protected Image updateImageBuffer() final double lim_lo = limit_lo; final double lim_hi = limit_hi; final double lim_hihi = limit_hihi; - if (normal && (!Double.isNaN(lim_lolo) || !Double.isNaN(lim_lo) || - !Double.isNaN(lim_hi) || !Double.isNaN(lim_hihi))) + if (!Double.isNaN(lim_lolo) || !Double.isNaN(lim_lo) || + !Double.isNaN(lim_hi) || !Double.isNaN(lim_hihi)) { if (limits_from_pv) gc.setStroke(new BasicStroke(2f)); else gc.setStroke(new BasicStroke(2f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10f, new float[]{6f, 4f}, 0f)); - drawLimitLineAt(gc, plot_bounds, min, max, lim_lolo, limit_major_color); - drawLimitLineAt(gc, plot_bounds, min, max, lim_lo, limit_minor_color); - drawLimitLineAt(gc, plot_bounds, min, max, lim_hi, limit_minor_color); - drawLimitLineAt(gc, plot_bounds, min, max, lim_hihi, limit_major_color); + drawLimitLineAt(gc, plot_bounds, min, max, normal, lim_lolo, limit_major_color); + drawLimitLineAt(gc, plot_bounds, min, max, normal, lim_lo, limit_minor_color); + drawLimitLineAt(gc, plot_bounds, min, max, normal, lim_hi, limit_minor_color); + drawLimitLineAt(gc, plot_bounds, min, max, normal, lim_hihi, limit_major_color); gc.setStroke(new BasicStroke(1f)); } diff --git a/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/internal/LinearTicks.java b/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/internal/LinearTicks.java index 6703336ff7..16f50d7481 100644 --- a/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/internal/LinearTicks.java +++ b/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/internal/LinearTicks.java @@ -53,9 +53,9 @@ public class LinearTicks extends Ticks /** Threshold for order-of-magnitude to use exponential notation */ private long exponential_threshold = 4; - /** User-specified override format, or {@code null} for automatic selection. - * When set, it is applied to all non-empty major tick labels after - * {@code compute()} finishes its internal layout. + /** User-specified format for the tick labels, or {@code null} for the + * automatic format. Applied by {@link #format(Double)}, so it covers + * every label that {@code compute()} creates. */ private volatile NumberFormat label_fmt_override = null; @@ -93,21 +93,6 @@ public boolean isPerpendicularTickLabels() return perpendicular_tick_labels; } - /** Re-apply {@code fmt} to every non-empty major tick label in {@code ticks}. - * @param ticks List to mutate in-place - * @param fmt Format to use - */ - protected static void relabelTicks(final List> ticks, - final NumberFormat fmt) - { - for (int i = 0; i < ticks.size(); i++) - { - final MajorTick t = ticks.get(i); - if (!t.getLabel().isEmpty()) - ticks.set(i, new MajorTick<>(t.getValue(), fmt.format(t.getValue()))); - } - } - /** @param order_of_magnitude determines when to use exponential notation */ public void setExponentialThreshold(long order_of_magnitude) { @@ -194,7 +179,7 @@ public void compute(Double low, Double high, final Graphics2D gc, final int scre double distance = selectNiceStep(min_distance); if (distance == 0.0) throw new Error("Broken tickmark computation"); - + // Update num_fmt based on distance between major tick labels. // For example, an axis with range 0 .. 10 would ordinarily use precision 0 // and axis markers like 0, 2, 4, 6, 8, 10. @@ -280,6 +265,7 @@ public void compute(Double low, Double high, final Graphics2D gc, final int scre major_ticks.add(0, new MajorTick<>(low, format(low))); major_ticks.add(new MajorTick<>(high, format(high))); } + this.major_ticks = major_ticks; this.minor_ticks = minor_ticks; } @@ -385,9 +371,9 @@ public String format(final Double num) return "Inf"; // Patch numbers that are "very close to zero" // to avoid "-0.00" or "0.0e-22" - if (Math.abs(num) < zero_threshold) - return num_fmt.format(0.0); - return num_fmt.format(num); + final double val = Math.abs(num) < zero_threshold ? 0.0 : num; + final NumberFormat override = getLabelFormatOverride(); + return (override != null) ? override.format(val) : num_fmt.format(val); } /** {@inheritDoc} */ diff --git a/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/internal/LogTicks.java b/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/internal/LogTicks.java index 3a76def321..36fae4ff4d 100644 --- a/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/internal/LogTicks.java +++ b/app/rtplot/src/main/java/org/csstudio/javafx/rtplot/internal/LogTicks.java @@ -136,11 +136,6 @@ else if (decadeExps.size() >= 2) major_ticks.add( new MajorTick<>(high, format(high))); } - // Apply user-specified label format override if set. - final NumberFormat override = getLabelFormatOverride(); - if (override != null) - relabelTicks(major_ticks, override); - this.major_ticks = major_ticks; this.minor_ticks = minor_ticks; } diff --git a/app/rtplot/src/test/java/org/csstudio/javafx/rtplot/RTTankTest.java b/app/rtplot/src/test/java/org/csstudio/javafx/rtplot/RTTankTest.java index c9e5177051..2ad37ffbf3 100644 --- a/app/rtplot/src/test/java/org/csstudio/javafx/rtplot/RTTankTest.java +++ b/app/rtplot/src/test/java/org/csstudio/javafx/rtplot/RTTankTest.java @@ -39,17 +39,36 @@ public void testConstruction() assertThat(tank, not(nullValue())); } - /** setRange should reject invalid ranges */ + /** setRange should ignore non-finite and zero-width ranges */ @Test - public void testSetRangeRejectsInvalid() + public void testSetRangeIgnoresInvalid() { final RTTank tank = new RTTank(); - // Should silently ignore these — no exception + tank.setRange(0, 100); tank.setRange(Double.NaN, 100); tank.setRange(0, Double.NaN); - tank.setRange(100, 100); // flat - tank.setRange(100, 0); // inverted + tank.setRange(100, 100); tank.setRange(Double.POSITIVE_INFINITY, 100); + assertThat(tank.getValueRange().getLow(), equalTo(0.0)); + assertThat(tank.getValueRange().getHigh(), equalTo(100.0)); + } + + /** An inverted range is kept, except on a log scale which is always ascending */ + @Test + public void testInvertedRange() + { + final RTTank tank = new RTTank(); + tank.setRange(100, 1); + assertThat(tank.getValueRange().getLow(), equalTo(100.0)); + assertThat(tank.getValueRange().getHigh(), equalTo(1.0)); + + tank.setLogScale(true); + assertThat(tank.getValueRange().getLow(), equalTo(1.0)); + assertThat(tank.getValueRange().getHigh(), equalTo(100.0)); + + tank.setLogScale(false); + assertThat(tank.getValueRange().getLow(), equalTo(100.0)); + assertThat(tank.getValueRange().getHigh(), equalTo(1.0)); } /** setValue should handle NaN and Infinity */ diff --git a/app/rtplot/src/test/java/org/csstudio/javafx/rtplot/internal/LinearTicksTest.java b/app/rtplot/src/test/java/org/csstudio/javafx/rtplot/internal/LinearTicksTest.java index 074cb4f8e5..25c00171f7 100644 --- a/app/rtplot/src/test/java/org/csstudio/javafx/rtplot/internal/LinearTicksTest.java +++ b/app/rtplot/src/test/java/org/csstudio/javafx/rtplot/internal/LinearTicksTest.java @@ -9,9 +9,11 @@ import org.csstudio.javafx.rtplot.TicksTestBase; import org.csstudio.javafx.rtplot.internal.LinearTicks; +import org.csstudio.javafx.rtplot.internal.MajorTick; import org.junit.jupiter.api.Test; import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; /** JUnit test @@ -47,6 +49,25 @@ public void testNiceDistance() } } + /** A label format set by the user applies to every label, and a value + * that is numerically almost zero still reads as zero, not "-0.00" */ + @Test + public void testLabelFormat() + { + final LinearTicks ticks = new LinearTicks(); + ticks.setLabelFormat(LinearTicks.createDecimalFormat(2)); + ticks.compute(-0.7, 0.7, gc, buf.getWidth()); + for (MajorTick tick : ticks.getMajorTicks()) + { + final String label = tick.getLabel(); + if (label.isEmpty()) + continue; + assertThat(label, not(equalTo("-0.00"))); + assertThat(label + " has two decimals", label.matches("-?\\d+\\.\\d\\d"), equalTo(true)); + } + assertThat(ticks.format(-1e-17), equalTo("0.00")); + } + @Test public void testNormalTicks() {