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