jlinegraph/src/main/java/it/cavallium/jlinegraph/AWTGraphRenderer.java

871 lines
31 KiB
Java

package it.cavallium.jlinegraph;
import it.cavallium.jlinegraph.AWTGraphRenderer.AWTDrawer;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.font.GlyphVector;
import java.awt.geom.Ellipse2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
public class AWTGraphRenderer implements IGraphRenderer<AWTDrawer> {
private static final int MAX_LABELS = 1000;
@Override
public AWTDrawer renderGraph(Graph graph, GraphBounds bounds) {
return graphics2D -> renderGraph(graphics2D, graph, bounds);
}
public static void renderGraph(Graphics2D g2d, Graph graph, GraphBounds bounds) {
var graphics2D = (Graphics2D) g2d.create();
graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
graphics2D.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
graphics2D.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
graphics2D.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_GASP);
graphics2D.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
graphics2D.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
graphics2D.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
graphics2D.setRenderingHint(RenderingHints.KEY_RESOLUTION_VARIANT, RenderingHints.VALUE_RESOLUTION_VARIANT_DPI_FIT);
Font defaultFont = graphics2D.getFont().deriveFont((float) graph.style().fonts().global());
Font valuesFont = defaultFont.deriveFont((float) graph.style().fonts().valueLabel());
Font axisNameFont = defaultFont.deriveFont((float) graph.style().fonts().axisName());
var defaultFontMetrics = graphics2D.getFontMetrics(defaultFont);
var valuesFontMetrics = graphics2D.getFontMetrics(valuesFont);
var axisNameFontMetrics = graphics2D.getFontMetrics(axisNameFont);
var paddingMultiplier = graph.style().paddingMultiplier();
var graphBounds = graph.data().bounds();
var x = graph.style().x();
var y = graph.style().y();
var padding = defaultFontMetrics.getHeight() * paddingMultiplier;
var scaleX = new NiceScale(graphBounds.minX(), graphBounds.maxX());
scaleX.setMaxTicks(20);
var scaleY = new NiceScale(graphBounds.minY(), graphBounds.maxY());
scaleY.setMaxTicks(20);
var halfMaxXLabelWidth = x.mode().showLabels()
? (valuesFontMetrics.stringWidth(y.valueFormat().apply(graphBounds.maxX())) / 2d) : 0;
var halfMaxYLabelHeight = (y.mode().showLabels() ? valuesFontMetrics.getHeight() / 2d : 0);
var topPadding = padding + halfMaxYLabelHeight;
var leftPadding = padding
+ ((y.mode() == AxisMode.HIDE && !y.showName()) ? halfMaxXLabelWidth : 0);
var rightPadding = padding + (x.mode().showLabels() ? halfMaxXLabelWidth : 0);
var bottomPadding = padding
+ ((x.mode() == AxisMode.HIDE && !x.showName()) ? halfMaxYLabelHeight : 0);
var xValueLineLength = x.mode().showRuler() ? valuesFontMetrics.getHeight() : 0;
var yValueLineLength = y.mode().showRuler() ? valuesFontMetrics.getHeight() : 0;
var xValuesHeight = x.mode().showLabels() ? valuesFontMetrics.getHeight() : 0;
var xValuesToXAxisNamePadding = (x.showName() ? valuesFontMetrics.getHeight() / 2 : 0);
var xAxisNameHeight = (x.showName() ? axisNameFontMetrics.getHeight() : 0);
var yAxisNameWidth = (y.showName() ? axisNameFontMetrics.getHeight() : 0);
var yValuesToYAxisNamePadding = (y.showName() ? valuesFontMetrics.getHeight() / 2 : 0);
var graphHeight
// Start with total height
= bounds.height()
// Remove the padding on top
- topPadding
// Remove the x value lines length
- xValueLineLength
// Remove the values height
- xValuesHeight
// Remove the space between the values and the axis name
- xValuesToXAxisNamePadding
// Remove x-axis name height
- xAxisNameHeight
// Remove the padding on bottom
- bottomPadding;
var xValueLineOffset = bounds.minY() + topPadding + graphHeight;
var yLabels = getYLabels(graph, bounds.minY(), graphHeight, valuesFontMetrics, scaleY, y.mode());
RasterSize yLabelsAreaSize = computeYLabelsAreaSize(y.mode(), graphHeight, valuesFontMetrics, yLabels);
var yValuesWidth = yLabelsAreaSize.width();
var yValueLineOffset = bounds.minX() + leftPadding + yAxisNameWidth + yValuesToYAxisNamePadding + yValuesWidth;
var graphWidth
// Start with total width
= bounds.width()
// Remove the padding on left
- leftPadding
// Remove y-axis name "90deg height"
- yAxisNameWidth
// Remove the space between the values and the axis name
- yValuesToYAxisNamePadding
// Remove the y values width
- yValuesWidth
// Remove the y value lines length
- yValueLineLength
// Remove the padding on right
- rightPadding;
Font seriesNameFont = null;
FontMetrics seriesNameFontMetrics = null;
if (graph.style().showLegend()) {
double legendSizeW;
double legendSizeH;
seriesNameFont = defaultFont.deriveFont((float) graph.style().fonts().seriesName());
seriesNameFontMetrics = graphics2D.getFontMetrics(seriesNameFont);
legendSizeW = getLegendSizeW(graph, seriesNameFontMetrics);
legendSizeH = getLegendSizeH(graph, seriesNameFontMetrics);
if (legendSizeW > graphWidth / 1.2d || legendSizeH > graphHeight / 1.5d) {
var newFontSizeW = (float) (seriesNameFont.getSize() * ((graphWidth / 1.2d) / legendSizeW));
var newFontSizeH = (float) (seriesNameFont.getSize() * ((graphHeight / 1.5d) / legendSizeH));
seriesNameFont = seriesNameFont.deriveFont(Math.min(newFontSizeW, newFontSizeH));
seriesNameFontMetrics = graphics2D.getFontMetrics(seriesNameFont);
}
}
var xLabels = getXLabels(graph, bounds.minX(), graphWidth, valuesFontMetrics, scaleX, x.mode());
RasterSize yAxisNameCenterOffset = new RasterSize(bounds.minX()
+ leftPadding
+ valuesFontMetrics.getHeight() / 2d, bounds.minY()
+ valuesFontMetrics.getHeight()
// Add half of graph height
+ graphHeight / 2 + topPadding);
RasterSize yValuesOffset = new RasterSize(bounds.minX()
+ leftPadding
// Add y axis name "90deg height"
+ yAxisNameWidth
// Add the space between the values and the axis name
+ yValuesToYAxisNamePadding, bounds.minY() + topPadding);
RasterSize graphOffset = new RasterSize(bounds.minX()
+ leftPadding
+ yAxisNameWidth
+ yValuesToYAxisNamePadding
+ yValuesWidth
+ yValueLineLength, bounds.minY() + topPadding);
RasterSize xValuesOffset = new RasterSize(graphOffset.width(), xValueLineOffset + xValueLineLength);
RasterSize xAxisNameCenterOffset = new RasterSize(bounds.minX()
+ leftPadding
+ yAxisNameWidth
+ yValuesToYAxisNamePadding
+ yValuesWidth
+ yValueLineLength
// Add half of graph width
+ graphWidth / 2, bounds.minY()
+ topPadding
// Add graph height
+ graphHeight
// Add the x value lines length
+ xValueLineLength
// Add the x values height
+ xValuesHeight
// Add the space between the values and the axis name
+ xValuesToXAxisNamePadding
// Add x-axis half name height
+ axisNameFontMetrics.getHeight() / 2d);
RasterSize graphSize = new RasterSize(graphWidth, graphHeight);
var bgColor = graph.style().colors().background().toColor();
var fgColor = graph.style().colors().foreground().toColor();
var strokeWidth = graph.style().strokeWidth();
var defaultStroke = new BasicStroke((float) strokeWidth, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND, BasicStroke.JOIN_MITER);
try {
graphics2D.setBackground(bgColor);
graphics2D.clearRect((int) Math.floor(bounds.minX()),
(int) Math.floor(bounds.minY()),
(int) Math.ceil(bounds.width()),
(int) Math.ceil(bounds.height())
);
if (graphHeight < 0) {
return;
}
if (graphWidth < 0) {
return;
}
renderGraphBorders(graphics2D, graph, graphOffset, graphSize, defaultStroke, bounds);
if (y.showName()) {
renderYAxisName(graphics2D, graph, yAxisNameCenterOffset, axisNameFont, axisNameFontMetrics);
}
if (x.showName()) {
renderXAxisName(graphics2D, graph, xAxisNameCenterOffset, axisNameFont, axisNameFontMetrics);
}
renderYAxisValueLabels(graphics2D,
graph,
valuesFont,
valuesFontMetrics,
yValueLineOffset,
yValueLineLength,
yLabels,
yLabelsAreaSize,
yValuesOffset,
defaultStroke,
y.mode().showRuler(),
y.mode().showLabels()
);
renderXAxisValueLabels(graphics2D,
graph,
valuesFont,
valuesFontMetrics,
xValueLineOffset,
xValueLineLength,
xLabels, xValuesOffset,
defaultStroke,
x.mode().showRuler(),
x.mode().showLabels()
);
var seriesGraphics2D = (Graphics2D) graphics2D.create();
seriesGraphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
try {
seriesGraphics2D.setClip(new Rectangle2D.Double(graphOffset.width() - defaultStroke.getLineWidth(),
graphOffset.height() - defaultStroke.getLineWidth(),
graphSize.width() + defaultStroke.getLineWidth() * 2d,
graphSize.height() + defaultStroke.getLineWidth() * 2d
));
var zeroLineStroke = new BasicStroke((float) strokeWidth,
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND,
10.0f,
new float[]{2.0f, 3.0f},
0.0f
);
var zeroLineColor = graph.style().colors().foreground().multiplyOpacity(0.5f).toColor();
if ((graphBounds.minY() < 0 && graphBounds.maxY() > 0)
|| (graphBounds.minY() > 0 && graphBounds.maxY() < 0)) {
double rasterZeroY = graphOffset.height() + graphSize.height()
- ((-graphBounds.minY()) / (graphBounds.maxY() - graphBounds.minY())) * graphSize.height();
seriesGraphics2D.setColor(zeroLineColor);
seriesGraphics2D.setStroke(zeroLineStroke);
seriesGraphics2D.draw(new Line2D.Double(graphOffset.width(),
rasterZeroY,
graphOffset.width() + graphWidth,
rasterZeroY
));
}
if ((graphBounds.minX() < 0 && graphBounds.maxX() > 0)
|| (graphBounds.minX() > 0 && graphBounds.maxX() < 0)) {
double rasterZeroX = graphOffset.width()
+ ((-graphBounds.minX()) / (graphBounds.maxX() - graphBounds.minX())) * graphSize.width();
seriesGraphics2D.setColor(zeroLineColor);
seriesGraphics2D.setStroke(zeroLineStroke);
seriesGraphics2D.draw(new Line2D.Double(
rasterZeroX,
graphOffset.height(),
rasterZeroX,
graphOffset.height() + graphHeight
));
}
int i = 0;
for (SeriesData series : graph.data().series()) {
var seriesStyleSize = graph.style().seriesStyles().size();
if (graph.style().seriesStyles().isEmpty()) {
throw new IllegalArgumentException("No styles found");
}
SeriesStyle style = graph.style().seriesStyles().get(i % seriesStyleSize);
BasicStroke seriesStroke = getSeriesStroke(style, strokeWidth);
BasicStroke seriesPointsStroke = getSeriesPointsStroke(style, strokeWidth);
drawSeries(seriesGraphics2D, graphBounds,
graphOffset,
graphSize,
series,
style,
seriesStroke,
seriesPointsStroke
);
i++;
}
} finally {
seriesGraphics2D.dispose();
}
if (graph.style().showLegend()) {
drawSeriesLegend(graphics2D,
graph,
graphOffset,
graphSize,
seriesNameFont,
seriesNameFontMetrics,
defaultStroke,
fgColor,
strokeWidth
);
}
} finally {
graphics2D.dispose();
}
}
private static void drawSeriesLegend(Graphics2D graphics2D,
Graph graph,
RasterSize graphOffset,
RasterSize graphSize,
Font seriesNameFont,
FontMetrics seriesNameFontMetrics,
BasicStroke defaultStroke,
Color fgColor,
double strokeWidth) {
double seriesPadding = getSeriesPadding(seriesNameFontMetrics);
double seriesMargin = getSeriesMargin(seriesNameFontMetrics);
double seriesPreviewLineWidth = seriesNameFontMetrics.getHeight() * 2;
double singleSeriesHeight = seriesNameFontMetrics.getHeight();
double legendSizeW = getLegendSizeW(graph, seriesNameFontMetrics);
double legendSizeH = getLegendSizeH(graph, seriesNameFontMetrics);
double legendOffsetX = graphOffset.width()
+ graphSize.width()
- seriesMargin
- legendSizeW;
double legendOffsetY = graphOffset.height()
+ seriesMargin;
var legendRect = new Rectangle2D.Double(legendOffsetX, legendOffsetY, legendSizeW, legendSizeH);
graphics2D.setStroke(defaultStroke);
graphics2D.setColor(graph.style().colors().background().multiplyOpacity(0.75f).toColor());
graphics2D.fill(legendRect);
graphics2D.setColor(fgColor);
graphics2D.draw(legendRect);
int i = 0;
for (SeriesData series : graph.data().series()) {
if (series.showInLegend()) {
var seriesStyleSize = graph.style().seriesStyles().size();
if (graph.style().seriesStyles().isEmpty()) {
throw new IllegalArgumentException("No styles found");
}
SeriesStyle style = graph.style().seriesStyles().get(i % seriesStyleSize);
var seriesName = series.name();
var stroke = new BasicStroke((float) (strokeWidth * 2f), BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
graphics2D.setColor(style.color().overrideOpacity(1.0f).toColor());
graphics2D.setStroke(stroke);
var lineOffsetX = legendOffsetX + seriesPadding;
var currentOffsetY = legendOffsetY + seriesPadding / 2d
+ i * (seriesPadding / 2d + singleSeriesHeight + seriesPadding / 2d)
+ seriesPadding / 2d;
var lineOffsetY = currentOffsetY + singleSeriesHeight / 2d;
graphics2D.draw(new Line2D.Double(lineOffsetX, lineOffsetY, lineOffsetX + seriesPreviewLineWidth, lineOffsetY));
var textOffsetX = lineOffsetX + seriesPreviewLineWidth + seriesPadding;
var textOffsetY = currentOffsetY + seriesNameFontMetrics.getAscent();
graphics2D.setColor(fgColor);
graphics2D.setFont(seriesNameFont);
graphics2D.fill(generateShapeFromText(graphics2D, seriesName, textOffsetX, textOffsetY));
i++;
}
}
}
private static double getSeriesMargin(FontMetrics seriesNameFontMetrics) {
return seriesNameFontMetrics.getHeight() * 2d / 3d;
}
private static double getLegendSizeW(Graph graph, FontMetrics seriesNameFontMetrics) {
double seriesPadding = getSeriesPadding(seriesNameFontMetrics);
double seriesTextMaxWidth = getSeriesTextMaxWidth(graph, seriesNameFontMetrics);
double seriesPreviewLineWidth = seriesNameFontMetrics.getHeight() * 2;
return seriesPadding
+ seriesTextMaxWidth
+ seriesPadding
+ seriesPreviewLineWidth
+ seriesPadding;
}
private static double getSeriesPadding(FontMetrics seriesNameFontMetrics) {
return seriesNameFontMetrics.getHeight() / 3d;
}
private static double getLegendSizeH(Graph graph, FontMetrics seriesNameFontMetrics) {
int seriesCount = 0;
for (SeriesData series : graph.data().series()) {
if (series.showInLegend()) {
seriesCount++;
}
}
double seriesPadding = getSeriesPadding(seriesNameFontMetrics);
double singleSeriesHeight = seriesNameFontMetrics.getHeight();
return seriesPadding / 2d
+ seriesCount * (seriesPadding / 2d + singleSeriesHeight + seriesPadding / 2d)
+ seriesPadding / 2d;
}
private static double getSeriesTextMaxWidth(Graph graph, FontMetrics seriesNameFontMetrics) {
double seriesTextMaxWidth = 0;
for (SeriesData series : graph.data().series()) {
if (series.showInLegend()) {
var seriesName = series.name();
var seriesNameRasterWidth = seriesNameFontMetrics.stringWidth(seriesName);
if (seriesTextMaxWidth < seriesNameRasterWidth) {
seriesTextMaxWidth = seriesNameRasterWidth;
}
}
}
return seriesTextMaxWidth;
}
private static void drawSeries(Graphics2D seriesGraphics2D,
GraphBounds graphBounds,
RasterSize graphOffset,
RasterSize graphSize,
SeriesData series,
SeriesStyle style,
BasicStroke seriesStroke,
BasicStroke seriesPointsStroke) {
var lineColor = style.color().toColor();
var areaColor = style.color().multiplyOpacity((float) style.areaOpacity()).toColor();
var points = new java.awt.geom.Point2D.Double[series.vertices().size()];
double rasterMinY = graphOffset.height() + graphSize.height()
- ((0 - graphBounds.minY()) / (graphBounds.maxY() - graphBounds.minY())) * graphSize.height();
int i = 0;
for (Vertex vertex : series.vertices()) {
double rasterX = graphOffset.width()
+ ((vertex.x() - graphBounds.minX()) / (graphBounds.maxX() - graphBounds.minX())) * graphSize.width();
double rasterY = graphOffset.height() + graphSize.height()
- ((vertex.y() - graphBounds.minY()) / (graphBounds.maxY() - graphBounds.minY())) * graphSize.height();
points[i] = new java.awt.geom.Point2D.Double(rasterX, rasterY);
i++;
}
// Sort points if it's a function
if (series.isFunction()) {
Arrays.sort(points, Comparator.comparingDouble(Point2D.Double::getX));
}
seriesGraphics2D.setStroke(seriesPointsStroke);
if (style.pointsWeight() != 0) {
seriesGraphics2D.setColor(lineColor);
for (var point : points) {
seriesGraphics2D.fill(new Ellipse2D.Double(point.getX() - seriesPointsStroke.getLineWidth(),
point.getY() - seriesPointsStroke.getLineWidth(),
seriesPointsStroke.getLineWidth() * 2f,
seriesPointsStroke.getLineWidth() * 2f
) {});
}
}
if (style.lineWeight() != 0 || style.areaOpacity() > 0d) {
if (style.smoothness() > 0d && points.length >= 3) {
var mPath = new GeneralPath(Path2D.WIND_NON_ZERO, points.length);
var areaPath = new GeneralPath(Path2D.WIND_NON_ZERO, points.length);
if (series.isFunction()) {
areaPath.moveTo(points[0].x, rasterMinY);
mPath.moveTo(points[0].x, points[0].y);
areaPath.lineTo(points[0].x, points[0].y);
} else {
areaPath.moveTo(points[0].x, points[0].y);
mPath.moveTo(points[0].x, points[0].y);
}
double SMOOTHNESS = style.smoothness() / 2d; // higher is smoother, but don't go over 0.5
if (!series.isFunction()) {
Point2D.Double[] bezierPoints;
final boolean closedPath = Objects.equals(points[points.length - 1], points[0]);
if (closedPath) {
bezierPoints = new Point2D.Double[points.length + 2];
bezierPoints[0] = points[points.length - 2];
System.arraycopy(points, 0, bezierPoints, 1, points.length);
bezierPoints[bezierPoints.length - 1] = points[1];
} else {
bezierPoints = points;
}
var bez = new Bezier(bezierPoints);
Point2D[] b = bez.getPoints();
if (!closedPath) {
mPath.quadTo(b[0].getX(), b[0].getY(), bezierPoints[1].x, bezierPoints[1].getY());
areaPath.quadTo(b[0].getX(), b[0].getY(), bezierPoints[1].x, bezierPoints[1].getY());
}
for (int w = 2; w < bezierPoints.length - 1; w++) {
Point2D b0 = b[2 * w - 3];
Point2D b1 = b[2 * w - 2];
double cp1X = b0.getX();
double cp1Y = b0.getY();
double cp2X = b1.getX();
double cp2Y = b1.getY();
double endPointX = bezierPoints[w].getX();
double endPointY = bezierPoints[w].getY();
mPath.curveTo(cp1X, cp1Y, cp2X, cp2Y, endPointX, endPointY);
areaPath.curveTo(cp1X, cp1Y, cp2X, cp2Y, endPointX, endPointY);
}
if (!closedPath) {
mPath.quadTo(b[b.length - 1].getX(),
b[b.length - 1].getY(),
bezierPoints[bezierPoints.length - 1].x,
bezierPoints[bezierPoints.length - 1].getY()
);
areaPath.quadTo(b[b.length - 1].getX(),
b[b.length - 1].getY(),
bezierPoints[bezierPoints.length - 1].x,
bezierPoints[bezierPoints.length - 1].getY()
);
}
} else {
// calculate smooth path
double lX = 0, lY = 0;
int size = points.length;
for (int pointIndex=1; pointIndex<size; pointIndex++) {
java.awt.geom.Point2D.Double p = points[pointIndex]; // current point
// first control point
var p0 = points[pointIndex-1]; // previous point
double d0 = Math.sqrt(Math.pow(p.x - p0.x, 2)+Math.pow(p.y-p0.y, 2)); // distance between p and p0
double x1 = Math.min(p0.x + lX*d0, (p0.x + p.x)/2); // min is used to avoid going too much right
double y1 = p0.y + lY*d0;
// second control point
var p1 = points[pointIndex+1 < size ? pointIndex+1 : pointIndex]; // next point
double d1 = Math.sqrt(Math.pow(p1.x - p0.x, 2)+Math.pow(p1.y-p0.y, 2)); // distance between p1 and p0 (length of reference line)
lX = (p1.x-p0.x)/d1*SMOOTHNESS; // (lX,lY) is the slope of the reference line
lY = (p1.y-p0.y)/d1*SMOOTHNESS;
double x2 = Math.max(p.x - lX*d0, (p0.x + p.x)/2); // max is used to avoid going too much left
double y2 = p.y - lY*d0;
// add line
mPath.curveTo(x1,y1,x2, y2, p.x, p.y);
areaPath.curveTo(x1, y1, x2, y2, p.x, p.y);
}
}
if (series.isFunction()) {
areaPath.lineTo(points[points.length - 1].x, rasterMinY);
}
areaPath.closePath();
if (style.areaOpacity() > 0d) {
seriesGraphics2D.setColor(areaColor);
seriesGraphics2D.fill(areaPath);
}
if (style.lineWeight() != 0) {
seriesGraphics2D.setStroke(seriesStroke);
seriesGraphics2D.setColor(lineColor);
seriesGraphics2D.draw(mPath);
}
} else {
var areaPath = new Path2D.Double();
var path = new Path2D.Double();
if (points.length > 0) {
areaPath.moveTo(points[0].x, rasterMinY);
}
boolean first = true;
for (Point2D.Double point : points) {
if (first) {
path.moveTo(point.getX(), point.getY());
first = false;
} else {
path.lineTo(point.getX(), point.getY());
}
areaPath.lineTo(point.getX(), point.getY());
}
if (points.length > 0) {
areaPath.moveTo(points[points.length - 1].x, rasterMinY);
areaPath.closePath();
}
if (style.areaOpacity() > 0d) {
seriesGraphics2D.setStroke(seriesStroke);
seriesGraphics2D.setColor(areaColor);
seriesGraphics2D.fill(areaPath);
}
if (style.lineWeight() != 0) {
seriesGraphics2D.setStroke(seriesStroke);
seriesGraphics2D.setColor(lineColor);
seriesGraphics2D.draw(path);
}
}
}
}
private static BasicStroke getSeriesStroke(SeriesStyle seriesStyle, double defaultStrokeWidth) {
return new BasicStroke((float) (defaultStrokeWidth * seriesStyle.lineWeight()),
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND
);
}
private static BasicStroke getSeriesPointsStroke(SeriesStyle seriesStyle, double defaultStrokeWidth) {
return new BasicStroke((float) (defaultStrokeWidth * 2d * seriesStyle.pointsWeight()),
BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND
);
}
private static void renderYAxisName(Graphics2D graphics2D,
Graph graph,
RasterSize yAxisNameCenterOffset,
Font axisNameFont,
FontMetrics axisNameFontMetrics) {
var fgColor = graph.style().colors().foreground().toColor();
graphics2D.setColor(fgColor);
graphics2D.setFont(axisNameFont);
var title = graph.style().y().title();
var previousTransform = graphics2D.getTransform();
graphics2D.rotate(Math.toRadians(-90),
yAxisNameCenterOffset.width(),
yAxisNameCenterOffset.height()
);
graphics2D.fill(generateShapeFromText(graphics2D,
title,
yAxisNameCenterOffset.width() - axisNameFontMetrics.stringWidth(title) / 2d,
yAxisNameCenterOffset.height() + axisNameFontMetrics.getHeight() / 2d - axisNameFontMetrics.getDescent()
));
graphics2D.setTransform(previousTransform);
}
private static void renderYAxisValueLabels(Graphics2D graphics2D,
Graph graph,
Font valuesFont,
FontMetrics valuesFontMetrics,
double yValueLineOffset,
int yValueLineLength,
List<LabelWithOffset> yLabels,
RasterSize yLabelsAreaSize,
RasterSize yValuesOffset,
BasicStroke defaultStroke,
boolean showRulerTicks,
boolean showRulerLabels) {
if ((yValueLineLength > 0 && showRulerTicks) || showRulerLabels) {
graphics2D.setFont(valuesFont);
graphics2D.setStroke(defaultStroke);
graphics2D.setColor(graph.style().colors().foreground().toColor());
yLabels.forEach(label -> {
if (showRulerTicks) {
var lineStartOffsetY = yValuesOffset.height() + label.rasterOffset();
var currentLineOffsetX = label.formattedText().isBlank() ? yValueLineLength / 3d : 0;
var currentLineLength = yValueLineLength + (label.formattedText().isBlank() ? -yValueLineLength / 3d : 0);
graphics2D.draw(new Line2D.Double(yValueLineOffset + currentLineOffsetX,
lineStartOffsetY,
yValueLineOffset + currentLineOffsetX + currentLineLength,
lineStartOffsetY
));
}
if (showRulerLabels) {
graphics2D.fill(generateShapeFromText(graphics2D,
label.formattedText(),
yValuesOffset.width() + yLabelsAreaSize.width() - valuesFontMetrics.stringWidth(label.formattedText()),
yValuesOffset.height() + label.rasterOffset() + valuesFontMetrics.getHeight() / 2d - valuesFontMetrics.getDescent()
));
}
});
}
}
private static void renderXAxisName(Graphics2D graphics2D,
Graph graph,
RasterSize xAxisNameCenterOffset,
Font axisNameFont,
FontMetrics axisNameFontMetrics) {
var fgColor = graph.style().colors().foreground().toColor();
graphics2D.setColor(fgColor);
graphics2D.setFont(axisNameFont);
var title = graph.style().x().title();
graphics2D.fill(generateShapeFromText(graphics2D,
title,
xAxisNameCenterOffset.width() - axisNameFontMetrics.stringWidth(title) / 2d,
xAxisNameCenterOffset.height() + axisNameFontMetrics.getHeight() / 2d - axisNameFontMetrics.getDescent()
));
}
private static void renderXAxisValueLabels(Graphics2D graphics2D,
Graph graph,
Font valuesFont,
FontMetrics valuesFontMetrics,
double xValueLineOffset,
int xValueLineLength,
List<LabelWithOffset> xLabels,
RasterSize xValuesOffset,
BasicStroke defaultStroke,
boolean showRulerTicks,
boolean showRulerLabels) {
if ((xValueLineLength > 0 && showRulerTicks) || showRulerLabels) {
graphics2D.setFont(valuesFont);
graphics2D.setStroke(defaultStroke);
graphics2D.setColor(graph.style().colors().foreground().toColor());
xLabels.forEach(label -> {
var lineStartOffsetX = xValuesOffset.width() + label.rasterOffset();
if (showRulerTicks) {
var currentLineLength = label.formattedText().isBlank() ? xValueLineLength / 1.5d : xValueLineLength;
//noinspection SuspiciousNameCombination
graphics2D.draw(new Line2D.Double(lineStartOffsetX, xValueLineOffset, lineStartOffsetX, xValueLineOffset + currentLineLength));
}
if (showRulerLabels) {
graphics2D.fill(generateShapeFromText(graphics2D,
label.formattedText(),
xValuesOffset.width() + label.rasterOffset() - valuesFontMetrics.stringWidth(label.formattedText()) / 2d,
xValuesOffset.height() + valuesFontMetrics.getHeight()
));
}
});
}
}
private static void renderGraphBorders(Graphics2D graphics2D,
Graph graph,
RasterSize graphOffset,
RasterSize graphSize,
BasicStroke defaultStroke,
GraphBounds bounds) {
// Do not draw the border if the graph is fullscreen
if (graphOffset.width() <= bounds.minX()
&& graphOffset.height() <= bounds.minY()
&& graphSize.width() >= bounds.width()
&& graphSize.height() >= bounds.height()) {
return;
}
var fgColor = graph.style().colors().foreground().toColor();
graphics2D.setColor(fgColor);
graphics2D.setStroke(defaultStroke);
graphics2D.draw(new Rectangle2D.Double(graphOffset.width(),
graphOffset.height(),
graphSize.width(),
graphSize.height()
));
}
private static RasterSize computeYLabelsAreaSize(AxisMode axisMode, double graphHeight, FontMetrics valuesFontMetrics,
List<LabelWithOffset> yLabels) {
if (!axisMode.showLabels()) {
return RasterSize.EMPTY;
}
double maxLabelWidth = 0d;
for (LabelWithOffset yLabel : yLabels) {
var currentMaxLabelWidth = valuesFontMetrics.stringWidth(yLabel.formattedText);
if (currentMaxLabelWidth > maxLabelWidth) {
maxLabelWidth = currentMaxLabelWidth;
}
}
return new RasterSize(maxLabelWidth, graphHeight);
}
record LabelWithOffset(double value, double rasterOffset, String formattedText) {}
/**
* @return rendered labels
*/
private static List<LabelWithOffset> getXLabels(Graph graph,
double labelsAreaOffset,
double labelsAreaWidth,
FontMetrics valuesFontMetrics,
NiceScale scaleX,
AxisMode mode) {
if (mode == AxisMode.HIDE) {
return List.of();
}
var bounds = graph.data().bounds();
var minX = bounds.minX();
var maxX = bounds.maxX();
var format = graph.style().x().valueFormat();
double singleRasterOffset = labelsAreaWidth / ((maxX - minX) / scaleX.getTickSpacing());
ArrayList<LabelWithOffset> labels = new ArrayList<>();
int i = 0;
double prevRasterLabelEndOffset = -Double.MAX_VALUE;
double currentRasterOffset = labelsAreaOffset;
double currentValue = minX;
while (currentValue <= maxX && i < MAX_LABELS && (scaleX.getTickSpacing() > 0)) {
if (mode.showLabels()) {
var formatted = format.apply(currentValue);
var stringWidth = valuesFontMetrics.stringWidth(formatted);
if (currentRasterOffset - stringWidth / 2d > prevRasterLabelEndOffset) {
labels.add(new LabelWithOffset(currentValue, currentRasterOffset, formatted));
prevRasterLabelEndOffset = currentRasterOffset + stringWidth / 2d;
} else {
labels.add(new LabelWithOffset(currentValue, currentRasterOffset, ""));
}
} else {
labels.add(new LabelWithOffset(currentValue, currentRasterOffset, ""));
}
i++;
currentValue = minX + i * scaleX.getTickSpacing();
currentRasterOffset = i * singleRasterOffset;
}
return labels;
}
/**
* @return rendered labels
*/
private static List<LabelWithOffset> getYLabels(Graph graph,
double labelsAreaOffset,
double labelsAreaHeight,
FontMetrics valuesFontMetrics,
NiceScale scaleY,
AxisMode mode) {
if (mode == AxisMode.HIDE) {
return List.of();
}
var bounds = graph.data().bounds();
var minY = bounds.minY();
var maxY = bounds.maxY();
var format = graph.style().y().valueFormat();
double singleRasterOffset = labelsAreaHeight / ((maxY - minY) / scaleY.getTickSpacing());
double stringTop = valuesFontMetrics.getAscent();
double stringBottom = valuesFontMetrics.getDescent();
ArrayList<LabelWithOffset> labels = new ArrayList<>();
int i = 0;
double prevRasterLabelEndOffset = Double.MAX_VALUE;
double currentRasterOffset = labelsAreaHeight;
double currentValue = minY;
while (currentValue <= maxY && i < MAX_LABELS && (scaleY.getTickSpacing() > 0)) {
if (mode.showLabels()) {
if (currentRasterOffset + stringBottom < prevRasterLabelEndOffset) {
labels.add(new LabelWithOffset(currentValue, currentRasterOffset, format.apply(currentValue)));
prevRasterLabelEndOffset = currentRasterOffset - stringTop;
} else {
labels.add(new LabelWithOffset(currentValue, currentRasterOffset, ""));
}
} else {
labels.add(new LabelWithOffset(currentValue, currentRasterOffset, ""));
}
i++;
currentValue = minY + i * scaleY.getTickSpacing();
currentRasterOffset = labelsAreaHeight - i * singleRasterOffset;
}
return labels;
}
public static Shape generateShapeFromText(Graphics2D graphics2D, String string, double x, double y) {
GlyphVector vector = graphics2D.getFont().createGlyphVector(graphics2D.getFontRenderContext(), string);
return vector.getOutline((float) x, (float) y);// - (float) vector.getVisualBounds().getY());
}
public interface AWTDrawer {
void drawTo(Graphics2D graphics2D);
}
}