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 9623ceb72b..8c294c7b05 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,10 +97,6 @@ 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); @@ -277,7 +273,6 @@ public void setLogScale(final boolean logscale) { scale.setLogarithmic(logscale); right_scale.setLogarithmic(logscale); - applyRange(); requestUpdate(); } @@ -446,65 +441,34 @@ public void setPerpendicularTickLabels(final boolean perpendicular) requestUpdate(); } - /** 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 + /** Set value range + * @param low Lower limit + * @param high Upper limit */ public void setRange(final double low, final double 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)) + // Guard against NaN, Infinite, or inverted/flat range + if (!Double.isFinite(low) || !Double.isFinite(high) || low >= high) return; - if (scale.isLogarithmic() && low > high) - { - low = rangeHigh; - high = rangeLow; - } scale.setValueRange(low, high); right_scale.setValueRange(low, high); } - /** @param value Set value; a non-finite value shows an empty tank */ + /** @param value Set value */ public void setValue(final double value) { if (Double.isFinite(value)) this.value = value; else - { - final AxisRange range = scale.getValueRange(); - this.value = Math.min(range.getLow(), range.getHigh()); - } + this.value = scale.getValueRange().getLow(); requestUpdate(); } - /** Map a value to a Y pixel within the plot bounds. + /** Map a value to a Y pixel within the plot bounds (low value at bottom). * 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 boolean normal) + final double v, final boolean logscale) { final double frac; if (logscale) @@ -515,17 +479,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 * (normal ? 1.0 - frac : frac)); + return (int) (pb.y + pb.height * (1.0 - 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 boolean normal, + final double min, final double max, 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(), normal); + final int y = valueToY(pb, min, max, limit, scale.isLogarithmic()); if (y < pb.y || y > pb.y + pb.height) return; gc.setColor(color); @@ -678,18 +642,18 @@ protected Image updateImageBuffer() final double lim_lo = limit_lo; final double lim_hi = limit_hi; final double lim_hihi = limit_hihi; - if (!Double.isNaN(lim_lolo) || !Double.isNaN(lim_lo) || - !Double.isNaN(lim_hi) || !Double.isNaN(lim_hihi)) + if (normal && (!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, 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); + 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); 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 16f50d7481..6703336ff7 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 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. + /** 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. */ private volatile NumberFormat label_fmt_override = null; @@ -93,6 +93,21 @@ 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) { @@ -179,7 +194,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. @@ -265,7 +280,6 @@ 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; } @@ -371,9 +385,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" - 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); + if (Math.abs(num) < zero_threshold) + return num_fmt.format(0.0); + return num_fmt.format(num); } /** {@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 36fae4ff4d..3a76def321 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,6 +136,11 @@ 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 2ad37ffbf3..c9e5177051 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,36 +39,17 @@ public void testConstruction() assertThat(tank, not(nullValue())); } - /** setRange should ignore non-finite and zero-width ranges */ + /** setRange should reject invalid ranges */ @Test - public void testSetRangeIgnoresInvalid() + public void testSetRangeRejectsInvalid() { final RTTank tank = new RTTank(); - tank.setRange(0, 100); + // Should silently ignore these — no exception tank.setRange(Double.NaN, 100); tank.setRange(0, Double.NaN); - tank.setRange(100, 100); + tank.setRange(100, 100); // flat + tank.setRange(100, 0); // inverted 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 25c00171f7..074cb4f8e5 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,11 +9,9 @@ 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 @@ -49,25 +47,6 @@ 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() {