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()
{