187 lines
5.7 KiB
Java
187 lines
5.7 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.isGraphZero() ? 0.0 : Double.MAX_VALUE;
|
|
double vMax = series.isGraphZero() ? 0.0 : Double.MIN_VALUE;
|
|
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 = 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;
|
|
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();
|
|
final int fontH3_4 = (int) Math.round(g.getFontMetrics().getHeight() * 0.75);
|
|
|
|
// g.setColor(Color.gray);
|
|
// final String string = "%s [%s]".formatted(series.getTitle(), autoscale.unit);
|
|
// g.drawString(string, border, border + fontH3_4);
|
|
|
|
yLabel(g, valueMax, DASHED, Color.red.darker().darker());
|
|
// yLabel(g, valueAvg, DASHED, new Color(0, 127, 0));
|
|
yLabel(g, 0, NORMAL, Color.BLACK);
|
|
yLabel(g, valueMin, DASHED, new Color(64, 128, 255).darker().darker());
|
|
|
|
g.translate(border, height - border);
|
|
g.scale(1, -1);
|
|
|
|
g.setStroke(NORMAL);
|
|
g.setColor(Color.BLACK);
|
|
g.drawLine(widthInner, 0, widthInner, heightInner); // y-axis
|
|
|
|
if (series.type == SeriesType.METER) {
|
|
g.setColor(Color.PINK);
|
|
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 {
|
|
g.setColor(Color.RED);
|
|
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 Stroke stroke, @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 (stroke != null && color != null) {
|
|
g.setStroke(stroke);
|
|
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);
|
|
};
|
|
}
|
|
|
|
}
|