Initial commit

This commit is contained in:
Andrea Cavalli 2021-12-07 00:46:15 +01:00
commit da25721f52
22 changed files with 1571 additions and 0 deletions

182
.gitignore vendored Normal file
View File

@ -0,0 +1,182 @@
# ---> Maven
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
# https://github.com/takari/maven-wrapper#usage-without-binary-jar
.mvn/wrapper/maven-wrapper.jar
# ---> Java
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
# ---> Linux
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
# ---> Windows
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# ---> macOS
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# ---> JetBrains
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
/.idea/

BIN
.preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

5
README.md Normal file
View File

@ -0,0 +1,5 @@
# JLineGraph
Small library to draw line graphs in java
![.preview.png](.preview.png)

35
pom.xml Normal file
View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>it.cavallium</groupId>
<artifactId>jlinegraph</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<repositories>
<repository>
<id>mchv</id>
<name>MCHV Apache Maven Packages</name>
<url>https://mvn.mchv.eu/repository/mchv/</url>
</repository>
</repositories>
<distributionManagement>
<repository>
<id>mchv-release-distribution</id>
<name>MCHV Release Apache Maven Packages Distribution</name>
<url>https://mvn.mchv.eu/repository/mchv</url>
</repository>
<snapshotRepository>
<id>mchv-snapshot-distribution</id>
<name>MCHV Snapshot Apache Maven Packages Distribution</name>
<url>https://mvn.mchv.eu/repository/mchv-snapshot</url>
</snapshotRepository>
</distributionManagement>
</project>

View File

@ -0,0 +1,19 @@
package it.cavallium.jlinegraph;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
public class AWTBufferedGraphRenderer implements IGraphRenderer<BufferedImage> {
@Override
public BufferedImage renderGraph(Graph graph, RasterSize totalSize) {
BufferedImage image = new BufferedImage((int) totalSize.width(),
(int) totalSize.height(),
BufferedImage.TYPE_INT_ARGB
);
Graphics2D graphics2D = image.createGraphics();
AWTGraphRenderer.renderGraph(graphics2D, graph, totalSize);
return image;
}
}

View File

@ -0,0 +1,108 @@
package it.cavallium.jlinegraph;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.util.List;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.WindowConstants;
public class AWTGraphExample {
private static final GraphColors GRAPH_COLOR = GraphColors.DARK;
public static void main(String[] args) {
var jf = new JFrame("Graph");
jf.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
jf.setLocationByPlatform(true);
var jp = new JComponent() {
@Override
protected void paintComponent(Graphics g) {
generateGraph(this.getWidth(), this.getHeight(), (Graphics2D) g);
}
};
jf.setBackground(GRAPH_COLOR.background().toColor());
jf.add(jp);
jf.setPreferredSize(new Dimension(800, 600));
jf.pack();
jf.setVisible(true);
}
private static void generateGraph(int w, int h, Graphics2D g2d) {
var g = new Graph("Example", new GraphData(List.of(
new SeriesData(List.of(
new Vertex(-3, -3),
new Vertex(0, 0),
new Vertex(3, 1),
new Vertex(3.5, 7),
new Vertex(7, 0.5),
new Vertex(15, 14)
), true, "Data1"),
new SeriesData(List.of(
new Vertex(-3, -2),
new Vertex(0, 3),
new Vertex(1, 1),
new Vertex(2, 5),
new Vertex(3, 6),
new Vertex(4, 7),
new Vertex(5, 4),
new Vertex(6, 3),
new Vertex(7, 1),
new Vertex(8, 10),
new Vertex(9, 12),
new Vertex(10, 13),
new Vertex(11, 15),
new Vertex(12, 11),
new Vertex(13, 15),
new Vertex(14, 10),
new Vertex(15, 4)
), true, "Data2"),
new SeriesData(List.of(
new Vertex(-3, -1),
new Vertex(0, 3),
new Vertex(4, 4),
new Vertex(8, 3),
new Vertex(12, 4),
new Vertex(15, 3)
), true, "Data3"),
new SeriesData(List.of(
new Vertex(4.4, 5.2),
new Vertex(6.4, 7.2),
new Vertex(8.4, 5.2),
new Vertex(6.4, 3.2),
new Vertex(4.4, 5.2)
), false, "full oval"),
new SeriesData(List.of(
new Vertex(-6+4.4, 5.8),
new Vertex(-6+6.4, 7.8),
new Vertex(-6+8.4, 5.8),
new Vertex(-6+6.4, 3.8),
new Vertex(-6+4.4, 5.8)
), false, "oval line"),
new SeriesData(List.of(
new Vertex(3.3+4, 2.3+8),
new Vertex(3.3+5, 2.3+7),
new Vertex(3.3+8, 2.3+5),
new Vertex(3.3+6, 2.3+3)
), false, "open path")
)),
new GraphStyle(List.of(
new SeriesStyle(new Color(0f, 1f, 0f, 1f), 1, 1, 0, 1d),
new SeriesStyle(new Color(1f, 0f, 0f, 1f), 1, 0, 0, 1d),
new SeriesStyle(new Color(0.5f, 1f, 1f, 1f), 0, 1, 0.3, 1d),
new SeriesStyle(new Color(0.5f, 1f, 0.5f, 1f), 0, 0, 1, 1d),
new SeriesStyle(new Color(0.5f, 1f, 0.5f, 1f), 0, 1, 0.3, 1d),
new SeriesStyle(new Color(1f, 1f, 0.7f, 1f), 1.5, 2, 0, 1d)
),
new GraphAxisStyle("X axis", true, "%.2fs"::formatted),
new GraphAxisStyle("Y axis", true, "%.2fm"::formatted),
GRAPH_COLOR,
new GraphFonts(10f, 18f, 12f, 12f),
2f,
true
));
var r = new AWTGraphRenderer();
r.renderGraph(g, new RasterSize(w, h)).drawTo(g2d);
}
}

View File

@ -0,0 +1,793 @@
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, RasterSize totalSize) {
return graphics2D -> renderGraph(graphics2D, graph, totalSize);
}
public static void renderGraph(Graphics2D graphics2D, Graph graph, RasterSize totalSize) {
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 graphBounds = graph.data().bounds();
var x = graph.style().x();
var y = graph.style().y();
var scaleX = new NiceScale(graphBounds.minX(), graphBounds.maxX());
scaleX.setMaxTicks(20);
var scaleY = new NiceScale(graphBounds.minY(), graphBounds.maxY());
scaleY.setMaxTicks(20);
var topPadding = defaultFontMetrics.getHeight() + valuesFontMetrics.getHeight() / 2d;
var leftPadding = defaultFontMetrics.getHeight();
var rightPadding = defaultFontMetrics.getHeight()
+ valuesFontMetrics.stringWidth(y.valueFormat().apply(graphBounds.maxX())) / 2d;
var bottomPadding = defaultFontMetrics.getHeight();
var xValueLineLength = valuesFontMetrics.getHeight();
var yValueLineLength = valuesFontMetrics.getHeight();
var xValuesHeight = valuesFontMetrics.getHeight();
var xValuesToXAxisNamePadding = (x.show() ? valuesFontMetrics.getHeight() : 0);
var xAxisNameHeight = (x.show() ? axisNameFontMetrics.getHeight() : 0);
var yAxisNameWidth = (y.show() ? axisNameFontMetrics.getHeight() : 0);
var yValuesToYAxisNamePadding = (y.show() ? valuesFontMetrics.getHeight() : 0);
var graphHeight
// Start with total height
= totalSize.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 = topPadding + graphHeight;
var yLabels = getYLabels(graph, graphHeight, valuesFontMetrics, scaleY);
RasterSize yLabelsAreaSize = computeYLabelsAreaSize(graphHeight, valuesFontMetrics, yLabels);
var yValuesWidth = yLabelsAreaSize.width();
var yValueLineOffset = leftPadding + yAxisNameWidth + yValuesToYAxisNamePadding + yValuesWidth;
var graphWidth
// Start with total width
= totalSize.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 / 3d || legendSizeH > graphHeight / 2.5d) {
var newFontSizeW = (float) (seriesNameFont.getSize() * ((graphWidth / 3d) / legendSizeW));
var newFontSizeH = (float) (seriesNameFont.getSize() * ((graphHeight / 2.5d) / legendSizeH));
seriesNameFont = seriesNameFont.deriveFont(Math.min(newFontSizeW, newFontSizeH));
seriesNameFontMetrics = graphics2D.getFontMetrics(seriesNameFont);
}
}
var xLabels = getXLabels(graph, graphWidth, valuesFontMetrics, scaleX);
RasterSize yAxisNameCenterOffset = new RasterSize(valuesFontMetrics.getHeight(), valuesFontMetrics.getHeight()
// Add half of graph height
+ graphHeight / 2);
RasterSize yValuesOffset = new RasterSize(leftPadding
// Add y axis name "90deg height"
+ yAxisNameWidth
// Add the space between the values and the axis name
+ yValuesToYAxisNamePadding, topPadding);
RasterSize graphOffset = new RasterSize(leftPadding
+ yAxisNameWidth
+ yValuesToYAxisNamePadding
+ yValuesWidth
+ yValueLineLength, topPadding);
RasterSize xValuesOffset = new RasterSize(graphOffset.width(), xValueLineOffset + xValueLineLength);
RasterSize xAxisNameCenterOffset = new RasterSize(leftPadding
+ yAxisNameWidth
+ yValuesToYAxisNamePadding
+ yValuesWidth
+ yValueLineLength
// Add half of graph width
+ graphWidth / 2, 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(0, 0, (int) totalSize.width(), (int) totalSize.height());
if (graphHeight < 0) {
return;
}
if (graphWidth < 0) {
return;
}
renderGraphBorders(graphics2D, graph, graphOffset, graphSize, defaultStroke);
if (y.show()) {
renderYAxisName(graphics2D, graph, yAxisNameCenterOffset, axisNameFont, axisNameFontMetrics);
}
if (x.show()) {
renderXAxisName(graphics2D, graph, xAxisNameCenterOffset, axisNameFont, axisNameFontMetrics);
}
renderYAxisValueLabels(graphics2D,
graph,
valuesFont,
valuesFontMetrics,
yValueLineOffset,
yValueLineLength,
yLabels,
yLabelsAreaSize,
yValuesOffset,
defaultStroke
);
renderXAxisValueLabels(graphics2D,
graph,
valuesFont,
valuesFontMetrics,
xValueLineOffset,
xValueLineLength,
xLabels, xValuesOffset,
defaultStroke
);
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()) {
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 = graph.data().series().size();
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()) {
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) {
graphics2D.setFont(valuesFont);
graphics2D.setStroke(defaultStroke);
graphics2D.setColor(graph.style().colors().foreground().toColor());
yLabels.forEach(label -> {
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
));
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) {
graphics2D.setFont(valuesFont);
graphics2D.setStroke(defaultStroke);
graphics2D.setColor(graph.style().colors().foreground().toColor());
xLabels.forEach(label -> {
var lineStartOffsetX = xValuesOffset.width() + label.rasterOffset();
var currentLineLength = label.formattedText().isBlank() ? xValueLineLength / 1.5d : xValueLineLength;
//noinspection SuspiciousNameCombination
graphics2D.draw(new Line2D.Double(lineStartOffsetX, xValueLineOffset, lineStartOffsetX, xValueLineOffset + currentLineLength));
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) {
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(double graphHeight, FontMetrics valuesFontMetrics, List<LabelWithOffset> yLabels) {
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 labelsAreaWidth,
FontMetrics valuesFontMetrics,
NiceScale scaleX) {
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 = 0;
double currentValue = minX;
while (currentValue <= maxX && i < MAX_LABELS && (scaleX.getTickSpacing() > 0)) {
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, ""));
}
i++;
currentValue = minX + i * scaleX.getTickSpacing();
currentRasterOffset = i * singleRasterOffset;
}
return labels;
}
/**
* @return rendered labels
*/
private static List<LabelWithOffset> getYLabels(Graph graph,
double labelsAreaHeight,
FontMetrics valuesFontMetrics,
NiceScale scaleY) {
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 (currentRasterOffset + stringBottom < prevRasterLabelEndOffset) {
labels.add(new LabelWithOffset(currentValue, currentRasterOffset, format.apply(currentValue)));
prevRasterLabelEndOffset = currentRasterOffset - stringTop;
} 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);
}
}

View File

@ -0,0 +1,118 @@
package it.cavallium.jlinegraph;
/*
* Copyright (c) 2005 David Benson
*
* See LICENSE file in distribution for licensing details of this source file
*/
import java.awt.Point;
import java.awt.geom.Point2D;
/**
* Interpolates given points by a bezier curve. The first
* and the last two points are interpolated by a quadratic
* bezier curve; the other points by a cubic bezier curve.
*
* Let p a list of given points and b the calculated bezier points,
* then one get the whole curve by:
*
* sharedPath.moveTo(p[0])
* sharedPath.quadTo(b[0].x, b[0].getY(), p[1].x, p[1].getY());
*
* for(int i = 2; i < p.length - 1; i++ ) {
* Point b0 = b[2*i-3];
* Point b1 = b[2*i-2];
* sharedPath.curveTo(b0.x, b0.getY(), b1.x, b1.getY(), p[i].x, p[i].getY());
* }
*
* sharedPath.quadTo(b[b.length-1].x, b[b.length-1].getY(), p[n - 1].x, p[n - 1].getY());
*
* @author krueger
*/
public class Bezier {
private static final float AP = 0.5f;
private Point2D[] bPoints;
/**
* Creates a new Bezier curve.
* @param points
*/
public Bezier(Point2D[] points) {
int n = points.length;
if (n < 3) {
// Cannot create bezier with less than 3 points
return;
}
bPoints = new Point[2 * (n - 2)];
double paX, paY;
double pbX = points[0].getX();
double pbY = points[0].getY();
double pcX = points[1].getX();
double pcY = points[1].getY();
for (int i = 0; i < n - 2; i++) {
paX = pbX;
paY = pbY;
pbX = pcX;
pbY = pcY;
pcX = points[i + 2].getX();
pcY = points[i + 2].getY();
double abX = pbX - paX;
double abY = pbY - paY;
double acX = pcX - paX;
double acY = pcY - paY;
double lac = Math.sqrt(acX * acX + acY * acY);
acX = acX /lac;
acY = acY /lac;
double proj = abX * acX + abY * acY;
proj = proj < 0 ? -proj : proj;
double apX = proj * acX;
double apY = proj * acY;
double p1X = pbX - AP * apX;
double p1Y = pbY - AP * apY;
bPoints[2 * i] = new Point((int) p1X, (int) p1Y);
acX = -acX;
acY = -acY;
double cbX = pbX - pcX;
double cbY = pbY - pcY;
proj = cbX * acX + cbY * acY;
proj = proj < 0 ? -proj : proj;
apX = proj * acX;
apY = proj * acY;
double p2X = pbX - AP * apX;
double p2Y = pbY - AP * apY;
bPoints[2 * i + 1] = new Point((int) p2X, (int) p2Y);
}
}
/**
* Returns the calculated bezier points.
* @return the calculated bezier points
*/
public Point2D[] getPoints() {
return bPoints;
}
/**
* Returns the number of bezier points.
* @return number of bezier points
*/
public int getPointCount() {
return bPoints.length;
}
/**
* Returns the bezier points at position i.
* @param i
* @return the bezier point at position i
*/
public Point2D getPoint(int i) {
return bPoints[i];
}
}

View File

@ -0,0 +1,36 @@
package it.cavallium.jlinegraph;
public record Color(float red, float green, float blue, float alpha) {
public Color {
if (red < 0d || red > 1.0d) {
throw new IndexOutOfBoundsException();
}
if (green < 0d || green > 1.0d) {
throw new IndexOutOfBoundsException();
}
if (blue < 0d || blue > 1.0d) {
throw new IndexOutOfBoundsException();
}
if (alpha < 0d || alpha > 1.0d) {
throw new IndexOutOfBoundsException();
}
}
public static Color fromRGB(int rgb) {
var col = new java.awt.Color(rgb);
return new Color(col.getRed() / 255f, col.getGreen() / 255f, col.getBlue() / 255f, 1);
}
public java.awt.Color toColor() {
return new java.awt.Color(red, green, blue, alpha);
}
public Color multiplyOpacity(float alpha) {
return new Color(red, green, blue, this.alpha * alpha);
}
public Color overrideOpacity(float alpha) {
return new Color(red, green, blue, alpha);
}
}

View File

@ -0,0 +1,3 @@
package it.cavallium.jlinegraph;
public record Graph(String name, GraphData data, GraphStyle style) {}

View File

@ -0,0 +1,5 @@
package it.cavallium.jlinegraph;
import java.util.function.Function;
public record GraphAxisStyle(String title, boolean show, Function<Number, String> valueFormat) {}

View File

@ -0,0 +1,94 @@
package it.cavallium.jlinegraph;
import java.util.List;
public record GraphBounds(double minX, double minY, double maxX, double maxY) {
private static final GraphBounds EMPTY = new GraphBounds(0, 0, 0, 0);
public static GraphBounds fromSeriesData(List<SeriesData> seriesDataList,
boolean includeOriginX,
boolean includeOriginY) {
var merged = merge(seriesDataList
.stream()
.map(bound -> fromSeriesData(bound, includeOriginX, includeOriginY))
.toList());
return adjustZero(merged, includeOriginX, includeOriginY);
}
private static GraphBounds adjustZero(GraphBounds bounds, boolean showZeroX, boolean showZeroY) {
double minX = bounds.minX();
double minY = bounds.minY();
double maxX = bounds.maxX();
double maxY = bounds.maxY();
if (showZeroY) {
minY = Math.min(0, bounds.minY());
maxY = Math.max(0, bounds.maxY());
}
if (showZeroX) {
minX = Math.min(0, bounds.minX());
maxX = Math.max(0, bounds.maxX());
}
return new GraphBounds(minX, minY, maxX, maxY);
}
public static GraphBounds merge(List<GraphBounds> list) {
double minX = Double.MAX_VALUE;
double minY = Double.MAX_VALUE;
double maxX = -Double.MAX_VALUE;
double maxY = -Double.MAX_VALUE;
boolean empty = true;
for (GraphBounds graphBounds : list) {
if (empty) {
empty = false;
}
if (minX > graphBounds.minX()) {
minX = graphBounds.minX();
}
if (maxX < graphBounds.maxX()) {
maxX = graphBounds.maxX();
}
if (minY > graphBounds.minY()) {
minY = graphBounds.minY();
}
if (maxY < graphBounds.maxY()) {
maxY = graphBounds.maxY();
}
}
if (empty) {
return EMPTY;
} else {
return new GraphBounds(minX, minY, maxX, maxY);
}
}
public static GraphBounds fromSeriesData(SeriesData seriesData, boolean showZeroX, boolean showZeroY) {
double minX = Double.MAX_VALUE;
double minY = Double.MAX_VALUE;
double maxX = -Double.MAX_VALUE;
double maxY = -Double.MAX_VALUE;
boolean empty = true;
for (Vertex vertex : seriesData.vertices()) {
if (empty) {
empty = false;
}
if (minX > vertex.x()) {
minX = vertex.x();
}
if (maxX < vertex.x()) {
maxX = vertex.x();
}
if (minY > vertex.y()) {
minY = vertex.y();
}
if (maxY < vertex.y()) {
maxY = vertex.y();
}
}
if (empty) {
return EMPTY;
} else {
return adjustZero(new GraphBounds(minX, minY, maxX, maxY), showZeroX, showZeroY);
}
}
}

View File

@ -0,0 +1,7 @@
package it.cavallium.jlinegraph;
public record GraphColors(Color background, Color foreground) {
public static GraphColors DARK = new GraphColors(new Color(0.1f, 0.1f, 0.1f, 1f), new Color(0.9f, 0.9f, 0.9f, 1f));
public static GraphColors LIGHT = new GraphColors(new Color(1.0f, 1.0f, 1.0f, 1f), new Color(0.0f, 0.0f, 0.0f, 1f));
}

View File

@ -0,0 +1,10 @@
package it.cavallium.jlinegraph;
import java.util.List;
public record GraphData(List<SeriesData> series, GraphBounds bounds) {
public GraphData(List<SeriesData> series) {
this(series, GraphBounds.fromSeriesData(series, false, false));
}
}

View File

@ -0,0 +1,3 @@
package it.cavallium.jlinegraph;
public record GraphFonts(double global, double axisName, double seriesName, double valueLabel) {}

View File

@ -0,0 +1,6 @@
package it.cavallium.jlinegraph;
import java.util.List;
public record GraphStyle(List<SeriesStyle> seriesStyles, GraphAxisStyle x, GraphAxisStyle y, GraphColors colors,
GraphFonts fonts, double strokeWidth, boolean showLegend) {}

View File

@ -0,0 +1,6 @@
package it.cavallium.jlinegraph;
public interface IGraphRenderer<T> {
T renderGraph(Graph graph, RasterSize size);
}

View File

@ -0,0 +1,111 @@
package it.cavallium.jlinegraph;
@SuppressWarnings("FieldCanBeLocal")
public class NiceScale {
private double minPoint;
private double maxPoint;
private double maxTicks = 10;
private double tickSpacing;
private double range;
private double niceMin;
private double niceMax;
/**
* Instantiates a new instance of the NiceScale class.
*
* @param min the minimum data point on the axis
* @param max the maximum data point on the axis
*/
public NiceScale(double min, double max) {
this.minPoint = min;
this.maxPoint = max;
calculate();
}
/**
* Calculate and update values for tick spacing and nice
* minimum and maximum data points on the axis.
*/
private void calculate() {
this.range = niceNum(maxPoint - minPoint, false);
this.tickSpacing = niceNum(range / (maxTicks - 1), true);
this.niceMin =
Math.floor(minPoint / tickSpacing) * tickSpacing;
this.niceMax =
Math.ceil(maxPoint / tickSpacing) * tickSpacing;
}
/**
* Returns a "nice" number approximately equal to range Rounds
* the number if round = true Takes the ceiling if round = false.
*
* @param range the data range
* @param round whether to round the result
* @return a "nice" number to be used for the data range
*/
private double niceNum(double range, boolean round) {
double exponent; /** exponent of range */
double fraction; /** fractional part of range */
double niceFraction; /** nice, rounded fraction */
exponent = Math.floor(Math.log10(range));
fraction = range / Math.pow(10, exponent);
if (round) {
if (fraction < 1.5)
niceFraction = 1;
else if (fraction < 3)
niceFraction = 2;
else if (fraction < 7)
niceFraction = 5;
else
niceFraction = 10;
} else {
if (fraction <= 1)
niceFraction = 1;
else if (fraction <= 2)
niceFraction = 2;
else if (fraction <= 5)
niceFraction = 5;
else
niceFraction = 10;
}
return niceFraction * Math.pow(10, exponent);
}
/**
* Sets the minimum and maximum data points for the axis.
*
* @param minPoint the minimum data point on the axis
* @param maxPoint the maximum data point on the axis
*/
public void setMinMaxPoints(double minPoint, double maxPoint) {
this.minPoint = minPoint;
this.maxPoint = maxPoint;
calculate();
}
/**
* Sets maximum number of tick marks we're comfortable with
*
* @param maxTicks the maximum number of tick marks for the axis
*/
public void setMaxTicks(double maxTicks) {
this.maxTicks = maxTicks;
calculate();
}
public double getNiceMin() {
return niceMin;
}
public double getNiceMax() {
return niceMax;
}
public double getTickSpacing() {
return tickSpacing;
}
}

View File

@ -0,0 +1,3 @@
package it.cavallium.jlinegraph;
public record RasterSize(double width, double height) {}

View File

@ -0,0 +1,5 @@
package it.cavallium.jlinegraph;
import java.util.List;
public record SeriesData(List<Vertex> vertices, boolean isFunction, String name) {}

View File

@ -0,0 +1,19 @@
package it.cavallium.jlinegraph;
public record SeriesStyle(Color color, double pointsWeight, double lineWeight, double areaOpacity, double smoothness) {
public SeriesStyle {
if (pointsWeight != 0 && (pointsWeight < 1d || pointsWeight > 4.0d)) {
throw new IndexOutOfBoundsException();
}
if (lineWeight != 0 && (lineWeight < 1d || lineWeight > 4.0d)) {
throw new IndexOutOfBoundsException();
}
if (areaOpacity < 0d || areaOpacity > 1.0d) {
throw new IndexOutOfBoundsException();
}
if (smoothness < 0d || smoothness > 1.0d) {
throw new IndexOutOfBoundsException();
}
}
}

View File

@ -0,0 +1,3 @@
package it.cavallium.jlinegraph;
public record Vertex(double x, double y) {}