Autoscale

This commit is contained in:
Patrick Haßel 2025-02-25 11:19:32 +01:00
parent 685436a316
commit 6a25e7501b
3 changed files with 75 additions and 15 deletions

View File

@ -1,6 +1,7 @@
package de.ph87.data.series.graph; package de.ph87.data.series.graph;
import de.ph87.data.series.*; import de.ph87.data.series.*;
import de.ph87.data.value.*;
import jakarta.annotation.*; import jakarta.annotation.*;
import lombok.*; import lombok.*;
@ -59,6 +60,8 @@ public class Graph {
public final int maxLabelWidth; public final int maxLabelWidth;
private final Autoscale autoscale;
public Graph(@NonNull final SeriesDto series, @NonNull final List<GraphPoint> points, @NonNull final Aligned begin, @NonNull final Aligned end, final int width, final int height, final int border) { public Graph(@NonNull final SeriesDto series, @NonNull final List<GraphPoint> points, @NonNull final Aligned begin, @NonNull final Aligned end, final int width, final int height, final int border) {
this.series = series; this.series = series;
this.begin = begin; this.begin = begin;
@ -67,21 +70,31 @@ public class Graph {
this.height = height; this.height = height;
this.border = border; this.border = border;
// find bounds
double vSum = 0; double vSum = 0;
double vMin = Double.MAX_VALUE; double vMin = Double.MAX_VALUE;
double vMax = Double.MIN_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) { for (final GraphPoint point : points) {
vMin = Math.min(vMin, point.getValue()); vMin = Math.min(vMin, point.getValue());
vMax = max(vMax, point.getValue()); vMax = max(vMax, point.getValue());
vSum += 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; heightInner = height - 2 * border;
minuteMin = begin.date.toEpochSecond() / 60; minuteMin = begin.date.toEpochSecond() / 60;
@ -91,12 +104,14 @@ public class Graph {
valueMin = vMin; valueMin = vMin;
valueMax = vMax; valueMax = vMax;
valueRange = vMax - vMin; valueAvg = vSum / points.size();
valueRange = valueMax - valueMin;
valueScale = heightInner / valueRange; 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() { public BufferedImage draw() {
final BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); final BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
final Graphics2D g = (Graphics2D) image.getGraphics(); 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) { 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 offset = maxLabelWidth - g.getFontMetrics().stringWidth(string);
final int y = height - ((int) Math.round((value - valueMin) * valueScale) + border); final int y = height - ((int) Math.round((value - valueMin) * valueScale) + border);
g.setColor(color); g.setColor(color);
@ -142,14 +157,14 @@ public class Graph {
} }
@NonNull @NonNull
private Function<GraphPoint, Point> toPoint(final long minuteMin, final double minuteScale, final double valueMin, final double valueScale) { private Function<GraphPoint, Point> toPoint() {
return point -> { return point -> {
final long minuteEpoch = point.getDate().toEpochSecond() / 60; final long minuteEpoch = point.getDate().toEpochSecond() / 60;
final long minuteRelative = minuteEpoch - minuteMin; final long minuteRelative = minuteEpoch - minuteMin;
final double minuteScaled = minuteRelative * minuteScale; final double minuteScaled = minuteRelative * minuteScale;
final int x = (int) Math.round(minuteScaled); 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 double valueScaled = valueRelative * valueScale;
final int y = (int) Math.round(valueScaled); final int y = (int) Math.round(valueScaled);

View File

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

View File

@ -9,21 +9,34 @@ import java.util.*;
public enum Unit { public enum Unit {
TEMPERATURE_C("°C"), TEMPERATURE_C("°C"),
PRESSURE_HPA("hPa"),
PRESSURE_PA("Pa"),
PRESSURE_HPA("hPa", 100, PRESSURE_PA),
HUMIDITY_RELATIVE_PERCENT("%"), 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"), ILLUMINANCE_LUX("lux"),
RESISTANCE_OHMS("Ω"), RESISTANCE_OHMS("Ω"),
ALTITUDE_M("m"), ALTITUDE_M("m"),
ALTITUDE_KM("km", 1000, ALTITUDE_M),
POWER_W("W"), POWER_W("W"),
POWER_KW("kW", 1000, POWER_W), POWER_KW("kW", 1000, POWER_W),
ENERGY_WH("W"),
ENERGY_WH("Wh"),
ENERGY_KWH("kWh", 1000, ENERGY_WH), ENERGY_KWH("kWh", 1000, ENERGY_WH),
IAQ("IAQ"), IAQ("IAQ"),
IAQ_CO2_EQUIVALENT("ppm"), IAQ_CO2_EQUIVALENT("ppm"),
IAQ_VOC_EQUIVALENT("ppm"), IAQ_VOC_EQUIVALENT("ppm"),
SUN_DC("Δ°C"), SUN_DC("Δ°C"),
UNIT_PERCENT("%"), UNIT_PERCENT("%"),
; ;