Data/src/main/java/de/ph87/data/series/graph/Graph.java

181 lines
5.4 KiB
Java

package de.ph87.data.series.graph;
import de.ph87.data.series.Aligned;
import de.ph87.data.series.SeriesDto;
import de.ph87.data.series.SeriesType;
import de.ph87.data.value.Autoscale;
import jakarta.annotation.Nullable;
import lombok.NonNull;
import java.awt.*;
import java.awt.geom.Line2D;
import java.awt.image.BufferedImage;
import java.util.List;
import java.util.function.Function;
import static java.lang.Math.max;
public class Graph {
public static final Stroke NORMAL = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 1);
public static final Stroke DASHED = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 1, new float[]{2, 2}, 0);
@NonNull
public final SeriesDto series;
@NonNull
public final Aligned begin;
@NonNull
public final Aligned end;
public final int width;
public final int height;
public final int border;
public final List<Point> points;
public final long minuteMin;
public final long minuteMax;
public final long minuteRange;
public final double minuteScale;
public final double valueMin;
public final double valueMax;
public final double valueAvg;
public final double valueRange;
public final double valueScale;
public final int heightInner;
public final int widthInner;
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) {
this.series = series;
this.begin = begin;
this.end = end;
this.width = width;
this.height = height;
this.border = border;
// find bounds
double vSum = 0;
double vMin = series.getYMin() == null || Double.isNaN(series.getYMin()) ? Double.MIN_VALUE : series.getYMin();
double vMax = series.getYMax() == null || Double.isNaN(series.getYMax()) ? Double.MAX_VALUE : series.getYMax();
for (final GraphPoint point : points) {
vMin = Math.min(vMin, point.getValue());
vMax = max(vMax, point.getValue());
vSum += point.getValue();
}
// auto scale
autoscale = new Autoscale(series, vMin, vMax);
vMin *= autoscale.factor;
vMax *= autoscale.factor;
vSum *= autoscale.factor;
// find max label width
int __maxLabelWidth = 0;
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;
minuteMax = end.date.toEpochSecond() / 60;
minuteRange = minuteMax - minuteMin;
minuteScale = (double) widthInner / (minuteRange + begin.alignment.maxDuration.toMinutes());
valueMin = vMin;
valueMax = vMax;
valueAvg = vSum / points.size();
valueRange = valueMax - valueMin;
valueScale = heightInner / valueRange;
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();
yLabel(g, valueMax, Color.red);
yLabel(g, valueAvg, new Color(0, 255, 0));
yLabel(g, valueMin, new Color(64, 128, 255));
g.translate(border, height - border);
g.scale(1, -1);
// y-axis
g.setStroke(NORMAL);
g.setColor(Color.GRAY);
g.drawLine(widthInner, 0, widthInner, heightInner);
g.setColor(Color.WHITE);
if (series.type == SeriesType.METER) {
final int space = (int) (minuteScale * begin.alignment.maxDuration.toMinutes());
final int width = (int) (space * 0.95);
for (final Point point : points) {
g.fillRect(point.x + (space - width), 0, width, point.y);
}
} else {
Point last = null;
for (final Point current : points) {
if (last != null) {
g.drawLine(last.x, last.y, current.x, current.y);
}
last = current;
}
}
return image;
}
private void yLabel(@NonNull final Graphics2D g, final double value, @Nullable final Color color) {
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);
g.drawString(string, widthInner + 2 * border + offset, y + (int) Math.round(g.getFontMetrics().getHeight() * 0.25));
if (color != null) {
g.setStroke(Graph.DASHED);
g.draw(new Line2D.Double(border, y, width - maxLabelWidth - border * 1.5, y));
}
}
@NonNull
private Function<GraphPoint, Point> 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() * autoscale.factor - valueMin;
final double valueScaled = valueRelative * valueScale;
final int y = (int) Math.round(valueScaled);
return new Point(x, y);
};
}
}