From 6a25e7501b04388a318ee71fbc64d804ea36bdfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Ha=C3=9Fel?= Date: Tue, 25 Feb 2025 11:19:32 +0100 Subject: [PATCH] Autoscale --- .../java/de/ph87/data/series/graph/Graph.java | 37 +++++++++++++------ .../java/de/ph87/data/value/Autoscale.java | 32 ++++++++++++++++ src/main/java/de/ph87/data/value/Unit.java | 21 +++++++++-- 3 files changed, 75 insertions(+), 15 deletions(-) create mode 100644 src/main/java/de/ph87/data/value/Autoscale.java diff --git a/src/main/java/de/ph87/data/series/graph/Graph.java b/src/main/java/de/ph87/data/series/graph/Graph.java index 56d3b26..3f4e51d 100644 --- a/src/main/java/de/ph87/data/series/graph/Graph.java +++ b/src/main/java/de/ph87/data/series/graph/Graph.java @@ -1,6 +1,7 @@ package de.ph87.data.series.graph; import de.ph87.data.series.*; +import de.ph87.data.value.*; import jakarta.annotation.*; import lombok.*; @@ -59,6 +60,8 @@ public class Graph { public final int maxLabelWidth; + private final Autoscale autoscale; + public Graph(@NonNull final SeriesDto series, @NonNull final List points, @NonNull final Aligned begin, @NonNull final Aligned end, final int width, final int height, final int border) { this.series = series; this.begin = begin; @@ -67,21 +70,31 @@ public class Graph { this.height = height; this.border = border; + // find bounds double vSum = 0; double vMin = Double.MAX_VALUE; double vMax = Double.MIN_VALUE; - int maxLabelWidth = 80; - final FontMetrics fontMetrics = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB).getGraphics().getFontMetrics(); for (final GraphPoint point : points) { vMin = Math.min(vMin, point.getValue()); vMax = max(vMax, point.getValue()); vSum += point.getValue(); - maxLabelWidth = max(maxLabelWidth, fontMetrics.stringWidth(series.format(point.getValue()))); } - this.valueAvg = vSum / points.size(); - this.maxLabelWidth = maxLabelWidth; - widthInner = width - 3 * border - maxLabelWidth; + // auto scale + autoscale = new Autoscale(series.unit, vMin, vMax); + vMin *= autoscale.factor; + vMax *= autoscale.factor; + vSum *= autoscale.factor; + + // find max label width + int __maxLabelWidth = 80; + final FontMetrics fontMetrics = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB).getGraphics().getFontMetrics(); + for (final GraphPoint point : points) { + __maxLabelWidth = max(__maxLabelWidth, fontMetrics.stringWidth(autoscale.format(point.getValue() * autoscale.factor))); + } + this.maxLabelWidth = __maxLabelWidth; + + widthInner = width - 3 * border - this.maxLabelWidth; heightInner = height - 2 * border; minuteMin = begin.date.toEpochSecond() / 60; @@ -91,12 +104,14 @@ public class Graph { valueMin = vMin; valueMax = vMax; - valueRange = vMax - vMin; + valueAvg = vSum / points.size(); + valueRange = valueMax - valueMin; valueScale = heightInner / valueRange; - this.points = points.stream().map(toPoint(minuteMin, minuteScale, vMin, valueScale)).toList(); + this.points = points.stream().map(toPoint()).toList(); } + @NonNull public BufferedImage draw() { final BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); final Graphics2D g = (Graphics2D) image.getGraphics(); @@ -130,7 +145,7 @@ public class Graph { } private void yLabel(@NonNull final Graphics2D g, final double value, @Nullable final Stroke stroke, @Nullable final Color color) { - final String string = series.format(value); + final String string = autoscale.format(value); final int offset = maxLabelWidth - g.getFontMetrics().stringWidth(string); final int y = height - ((int) Math.round((value - valueMin) * valueScale) + border); g.setColor(color); @@ -142,14 +157,14 @@ public class Graph { } @NonNull - private Function toPoint(final long minuteMin, final double minuteScale, final double valueMin, final double valueScale) { + private Function toPoint() { return point -> { final long minuteEpoch = point.getDate().toEpochSecond() / 60; final long minuteRelative = minuteEpoch - minuteMin; final double minuteScaled = minuteRelative * minuteScale; final int x = (int) Math.round(minuteScaled); - final double valueRelative = point.getValue() - valueMin; + final double valueRelative = point.getValue() * autoscale.factor - valueMin; final double valueScaled = valueRelative * valueScale; final int y = (int) Math.round(valueScaled); diff --git a/src/main/java/de/ph87/data/value/Autoscale.java b/src/main/java/de/ph87/data/value/Autoscale.java new file mode 100644 index 0000000..5f14b4d --- /dev/null +++ b/src/main/java/de/ph87/data/value/Autoscale.java @@ -0,0 +1,32 @@ +package de.ph87.data.value; + +import lombok.*; + +import java.util.*; + +@Data +public class Autoscale { + + public static final String[] SI_PREFIX = new String[]{"f", "n,", "µ", "m", "", "k", "M", "G", "T"}; + + public final double factor; + + @NonNull + public final String unit; + + public Autoscale(@NonNull final Unit sourceUnit, final double... values) { + final double abs = sourceUnit.factor * Arrays.stream(values).map(Math::abs).max().orElse(0); + final double exp = Math.max(0, Math.log10(abs)); + final int group = (int) Math.floor(exp / 3); + this.factor = sourceUnit.factor * Math.pow(10, group * 3); + + final int index = (SI_PREFIX.length - 1) / 2 + group; + this.unit = SI_PREFIX[index] + sourceUnit.base.unit; + } + + @NonNull + public String format(final double value) { + return "%.1f %s".formatted(value, unit); + } + +} diff --git a/src/main/java/de/ph87/data/value/Unit.java b/src/main/java/de/ph87/data/value/Unit.java index 410f6bc..2fbee31 100644 --- a/src/main/java/de/ph87/data/value/Unit.java +++ b/src/main/java/de/ph87/data/value/Unit.java @@ -9,21 +9,34 @@ import java.util.*; public enum Unit { TEMPERATURE_C("°C"), - PRESSURE_HPA("hPa"), + + PRESSURE_PA("Pa"), + PRESSURE_HPA("hPa", 100, PRESSURE_PA), + HUMIDITY_RELATIVE_PERCENT("%"), - HUMIDITY_ABSOLUTE_MGL("mg/L"), - HUMIDITY_ABSOLUTE_GM3("g/m³", 1, HUMIDITY_ABSOLUTE_MGL), + + HUMIDITY_ABSOLUTE_GM3("g/m³"), + HUMIDITY_ABSOLUTE_MGL("mg/L", 1, HUMIDITY_ABSOLUTE_GM3), + ILLUMINANCE_LUX("lux"), + RESISTANCE_OHMS("Ω"), + ALTITUDE_M("m"), + ALTITUDE_KM("km", 1000, ALTITUDE_M), + POWER_W("W"), POWER_KW("kW", 1000, POWER_W), - ENERGY_WH("W"), + + ENERGY_WH("Wh"), ENERGY_KWH("kWh", 1000, ENERGY_WH), + IAQ("IAQ"), IAQ_CO2_EQUIVALENT("ppm"), IAQ_VOC_EQUIVALENT("ppm"), + SUN_DC("Δ°C"), + UNIT_PERCENT("%"), ;