Initial commit
This commit is contained in:
commit
da25721f52
182
.gitignore
vendored
Normal file
182
.gitignore
vendored
Normal 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
BIN
.preview.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 142 KiB |
5
README.md
Normal file
5
README.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# JLineGraph
|
||||||
|
|
||||||
|
Small library to draw line graphs in java
|
||||||
|
|
||||||
|
![.preview.png](.preview.png)
|
35
pom.xml
Normal file
35
pom.xml
Normal 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>
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
108
src/main/java/it/cavallium/jlinegraph/AWTGraphExample.java
Normal file
108
src/main/java/it/cavallium/jlinegraph/AWTGraphExample.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
793
src/main/java/it/cavallium/jlinegraph/AWTGraphRenderer.java
Normal file
793
src/main/java/it/cavallium/jlinegraph/AWTGraphRenderer.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
118
src/main/java/it/cavallium/jlinegraph/Bezier.java
Normal file
118
src/main/java/it/cavallium/jlinegraph/Bezier.java
Normal 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];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
36
src/main/java/it/cavallium/jlinegraph/Color.java
Normal file
36
src/main/java/it/cavallium/jlinegraph/Color.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
3
src/main/java/it/cavallium/jlinegraph/Graph.java
Normal file
3
src/main/java/it/cavallium/jlinegraph/Graph.java
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
package it.cavallium.jlinegraph;
|
||||||
|
|
||||||
|
public record Graph(String name, GraphData data, GraphStyle style) {}
|
@ -0,0 +1,5 @@
|
|||||||
|
package it.cavallium.jlinegraph;
|
||||||
|
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
public record GraphAxisStyle(String title, boolean show, Function<Number, String> valueFormat) {}
|
94
src/main/java/it/cavallium/jlinegraph/GraphBounds.java
Normal file
94
src/main/java/it/cavallium/jlinegraph/GraphBounds.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7
src/main/java/it/cavallium/jlinegraph/GraphColors.java
Normal file
7
src/main/java/it/cavallium/jlinegraph/GraphColors.java
Normal 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));
|
||||||
|
}
|
10
src/main/java/it/cavallium/jlinegraph/GraphData.java
Normal file
10
src/main/java/it/cavallium/jlinegraph/GraphData.java
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
3
src/main/java/it/cavallium/jlinegraph/GraphFonts.java
Normal file
3
src/main/java/it/cavallium/jlinegraph/GraphFonts.java
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
package it.cavallium.jlinegraph;
|
||||||
|
|
||||||
|
public record GraphFonts(double global, double axisName, double seriesName, double valueLabel) {}
|
6
src/main/java/it/cavallium/jlinegraph/GraphStyle.java
Normal file
6
src/main/java/it/cavallium/jlinegraph/GraphStyle.java
Normal 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) {}
|
@ -0,0 +1,6 @@
|
|||||||
|
package it.cavallium.jlinegraph;
|
||||||
|
|
||||||
|
public interface IGraphRenderer<T> {
|
||||||
|
|
||||||
|
T renderGraph(Graph graph, RasterSize size);
|
||||||
|
}
|
111
src/main/java/it/cavallium/jlinegraph/NiceScale.java
Normal file
111
src/main/java/it/cavallium/jlinegraph/NiceScale.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
3
src/main/java/it/cavallium/jlinegraph/RasterSize.java
Normal file
3
src/main/java/it/cavallium/jlinegraph/RasterSize.java
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
package it.cavallium.jlinegraph;
|
||||||
|
|
||||||
|
public record RasterSize(double width, double height) {}
|
5
src/main/java/it/cavallium/jlinegraph/SeriesData.java
Normal file
5
src/main/java/it/cavallium/jlinegraph/SeriesData.java
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
package it.cavallium.jlinegraph;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record SeriesData(List<Vertex> vertices, boolean isFunction, String name) {}
|
19
src/main/java/it/cavallium/jlinegraph/SeriesStyle.java
Normal file
19
src/main/java/it/cavallium/jlinegraph/SeriesStyle.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
src/main/java/it/cavallium/jlinegraph/Vertex.java
Normal file
3
src/main/java/it/cavallium/jlinegraph/Vertex.java
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
package it.cavallium.jlinegraph;
|
||||||
|
|
||||||
|
public record Vertex(double x, double y) {}
|
Loading…
Reference in New Issue
Block a user