From f3122f791bebc0575900b92d20fd692b9e1656b4 Mon Sep 17 00:00:00 2001 From: spinner Date: Sun, 19 Mar 2023 22:58:17 +0300 Subject: [PATCH 1/2] Fixes and new features for YPane 1) Offset is now a property, it can be changed or disabled by setting to 0.0 2) Overlay rendering fixed, it can be used to estimate values now. 3) Mid-point label added for easier estimation of plotted values. 4) All values below low bound limit are "grounded" to zero now, the plot is not turning inside-out. --- src/main/java/eu/hansolo/fx/charts/YPane.java | 93 +++++++++++++------ 1 file changed, 64 insertions(+), 29 deletions(-) diff --git a/src/main/java/eu/hansolo/fx/charts/YPane.java b/src/main/java/eu/hansolo/fx/charts/YPane.java index 399ec10f..375b9c66 100644 --- a/src/main/java/eu/hansolo/fx/charts/YPane.java +++ b/src/main/java/eu/hansolo/fx/charts/YPane.java @@ -21,12 +21,15 @@ import eu.hansolo.fx.charts.series.YSeries; import eu.hansolo.fx.charts.tools.Helper; import eu.hansolo.fx.charts.tools.Point; +import javafx.beans.InvalidationListener; import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanPropertyBase; import javafx.beans.property.DoubleProperty; import javafx.beans.property.DoublePropertyBase; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectPropertyBase; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; @@ -58,6 +61,9 @@ public class YPane extends Region implements ChartArea { private static final double MINIMUM_HEIGHT = 0; private static final double MAXIMUM_WIDTH = 4096; private static final double MAXIMUM_HEIGHT = 4096; + + private static final double FONTSCALE = 0.67; + private static final double MAXIMUM_OFFSET = 0.3; private static double aspectRatio; private boolean keepAspect; private double size; @@ -83,6 +89,8 @@ public class YPane extends Region implements ChartArea { private DoubleProperty upperBoundY; private ObservableList categories; + private double _offset; + private DoubleProperty offset; // ******************** Constructors ************************************** public YPane(final YSeries... SERIES) { @@ -162,6 +170,24 @@ private void registerListeners() { @Override public ObservableList getChildren() { return super.getChildren(); } + public double getOffset() { return _offset; } + + public void setOffset(final double offset) { + _offset = (offset < 0.0) ? 0.0 : Math.min(offset, MAXIMUM_OFFSET); + redraw(); + } + + public DoubleProperty offsetProperty() { + if (null == offset) { + offset = new DoublePropertyBase(_offset) { + @Override protected void invalidated() { redraw(); } + @Override public Object getBean() { return YPane.this; } + @Override public String getName() { return "thresholdY"; } + }; + } + return offset; + } + public Paint getChartBackground() { return null == chartBackground ? _chartBackground : chartBackground.get(); } public void setChartBackground(final Paint PAINT) { if (null == chartBackground) { @@ -405,8 +431,8 @@ private void drawRadar(final YSeries SERIES) { final double CIRCLE_SIZE = 0.9 * size; final double LOWER_BOUND_Y = getLowerBoundY(); final double DATA_RANGE = getRangeY(); - final double RANGE = 0.35714 * CIRCLE_SIZE; - final double OFFSET = 0.14286 * CIRCLE_SIZE; + final double RANGE = (0.5 - _offset) * CIRCLE_SIZE; + final double OFFSET = _offset * CIRCLE_SIZE; final int NO_OF_SECTORS = SERIES.getItems().size(); final double angleStep = 360.0 / NO_OF_SECTORS; @@ -420,13 +446,14 @@ private void drawRadar(final YSeries SERIES) { ctx.setLineWidth(SERIES.getStrokeWidth() > -1 ? SERIES.getStrokeWidth() : size * 0.0025); ctx.setStroke(SERIES.getStroke()); - switch(SERIES.getChartType()) { - case RADAR_POLYGON: + switch (SERIES.getChartType()) { + case RADAR_POLYGON -> { ctx.save(); ctx.beginPath(); ctx.moveTo(CENTER_X, 0.36239 * size); SERIES.getItems().forEach(item -> { double r1 = (item.getValue() - LOWER_BOUND_Y) / DATA_RANGE; + r1 = Math.max(0.0, r1); ctx.lineTo(CENTER_X, CENTER_Y - OFFSET - r1 * RANGE); Helper.rotateCtx(ctx, CENTER_X, CENTER_Y, angleStep); }); @@ -437,45 +464,49 @@ private void drawRadar(final YSeries SERIES) { ctx.fill(); ctx.stroke(); ctx.restore(); - break; - case SMOOTH_RADAR_POLYGON: - double radAngle = Math.toRadians(180); - double radAngleStep = Math.toRadians(angleStep); - List points = new ArrayList<>(); - - double x = CENTER_X + (-Math.sin(radAngle) * (CENTER_Y - (0.36239 * size))); - double y = CENTER_Y + (+Math.cos(radAngle) * (CENTER_Y - (0.36239 * size))); - if(!SERIES.isWithWrapping()){ + } + case SMOOTH_RADAR_POLYGON -> { + double radAngle = Math.toRadians(180); + double radAngleStep = Math.toRadians(angleStep); + List points = new ArrayList<>(); + double x = CENTER_X + (-Math.sin(radAngle) * (CENTER_Y - (0.5 - _offset) * size)); + double y = CENTER_Y + (+Math.cos(radAngle) * (CENTER_Y - (0.5 - _offset) * size)); + if (!SERIES.isWithWrapping()) { points.add(new Point(x, y)); } - for (T item : SERIES.getItems()) { - double r1 = (CENTER_Y - (CENTER_Y - OFFSET - ((item.getValue() - LOWER_BOUND_Y) / DATA_RANGE) * RANGE)); + double r1 = (item.getValue() - LOWER_BOUND_Y) / DATA_RANGE; + r1 = Math.max(0.0, r1); + r1 = (CENTER_Y - (CENTER_Y - OFFSET - r1 * RANGE)); x = CENTER_X + (-Math.sin(radAngle) * r1); y = CENTER_Y + (+Math.cos(radAngle) * r1); points.add(new Point(x, y)); radAngle += radAngleStep; } - double r3 = (SERIES.isWithWrapping()) ? (CENTER_Y - (CENTER_Y - OFFSET - ((SERIES.getItems().get(0).getValue() - LOWER_BOUND_Y) / DATA_RANGE) * RANGE)) : (CENTER_Y - (CENTER_Y - OFFSET - ((SERIES.getItems().get(NO_OF_SECTORS - 1).getValue() - LOWER_BOUND_Y) / DATA_RANGE) * RANGE)); + double r3 = (SERIES.getItems().get(NO_OF_SECTORS - 1).getValue() - LOWER_BOUND_Y) / DATA_RANGE; + r3 = Math.max(0.0, r3); + double r4 = (SERIES.getItems().get(0).getValue() - LOWER_BOUND_Y) / DATA_RANGE; + r4 = Math.max(0.0, r4); + r3 = (SERIES.isWithWrapping()) ? (CENTER_Y - (CENTER_Y - OFFSET - r4 * RANGE)) : (CENTER_Y - (CENTER_Y - OFFSET - r3 * RANGE)); x = CENTER_X + (-Math.sin(radAngle) * r3); y = CENTER_Y + (+Math.cos(radAngle) * r3); points.add(new Point(x, y)); - Point[] interpolatedPoints = (SERIES.isWithWrapping())?Helper.subdividePointsRadial(points.toArray(new Point[0]), 16):Helper.subdividePoints(points.toArray(new Point[0]), 16); - + Point[] interpolatedPoints = (SERIES.isWithWrapping()) ? + Helper.subdividePointsRadial(points.toArray(new Point[0]), 16) : + Helper.subdividePoints(points.toArray(new Point[0]), 16); ctx.beginPath(); ctx.moveTo(interpolatedPoints[0].getX(), interpolatedPoints[0].getY()); - for (int i = 0 ; i < interpolatedPoints.length - 1 ; i++) { + for (int i = 0; i < interpolatedPoints.length - 1; i++) { Point point = interpolatedPoints[i]; ctx.lineTo(point.getX(), point.getY()); } ctx.lineTo(interpolatedPoints[interpolatedPoints.length - 1].getX(), interpolatedPoints[interpolatedPoints.length - 1].getY()); ctx.closePath(); - ctx.fill(); ctx.stroke(); - break; - case RADAR_SECTOR: + } + case RADAR_SECTOR -> { Helper.rotateCtx(ctx, CENTER_X, CENTER_Y, -90); SERIES.getItems().forEach(item -> { double r1 = (item.getValue() - LOWER_BOUND_Y) / DATA_RANGE; @@ -488,7 +519,7 @@ private void drawRadar(final YSeries SERIES) { Helper.rotateCtx(ctx, CENTER_X, CENTER_Y, angleStep); }); - break; + } } ctx.restore(); } @@ -499,14 +530,15 @@ private void drawRadarOverlay(final int NO_OF_SECTORS, final ChartType TYPE) { final double CIRCLE_SIZE = 0.90 * size; final double DATA_RANGE = getRangeY(); final double MIN_VALUE = listOfSeries.stream().mapToDouble(YSeries::getMinY).min().getAsDouble(); - final double RANGE = 0.35714 * CIRCLE_SIZE; - final double OFFSET = 0.14286 * CIRCLE_SIZE; + final double RANGE = (0.5 - _offset) * CIRCLE_SIZE; + final double OFFSET = _offset * CIRCLE_SIZE; final double angleStep = 360.0 / NO_OF_SECTORS; // draw concentric rings ctx.setLineWidth(1); ctx.setStroke(Color.GRAY); - double ringStepSize = size / 20.0; + double ringRange = CIRCLE_SIZE - (OFFSET * 2); + double ringStepSize = ringRange / 20.0; double pos = 0.5 * (size - CIRCLE_SIZE); double ringSize = CIRCLE_SIZE; for (int i = 0 ; i < 11 ; i++) { @@ -560,13 +592,16 @@ private void drawRadarOverlay(final int NO_OF_SECTORS, final ChartType TYPE) { ctx.restore(); // draw min and max Text - Font font = Fonts.latoRegular(0.025 * size); + double fSize = 0.025 * size; + Font font = Fonts.latoRegular(fSize); String minValueText = String.format(Locale.US, "%.0f", getLowerBoundY()); String maxValueText = String.format(Locale.US, "%.0f", getUpperBoundY()); + String midValueText = String.format(Locale.US, "%.0f", getLowerBoundY() + (getUpperBoundY()-getLowerBoundY()) * 0.5); ctx.save(); ctx.setFont(font); - Helper.drawTextWithBackground(ctx, minValueText, font, Color.WHITE, Color.BLACK, CENTER_X, CENTER_Y - size * 0.018); - Helper.drawTextWithBackground(ctx, maxValueText, font, Color.WHITE, Color.BLACK, CENTER_X, CENTER_Y - CIRCLE_SIZE * 0.48); + Helper.drawTextWithBackground(ctx, minValueText, font, Color.WHITE, Color.BLACK, CENTER_X, (CENTER_Y - OFFSET) - (ringRange * 0.0) + (fSize * FONTSCALE)); + Helper.drawTextWithBackground(ctx, midValueText, font, Color.WHITE, Color.BLACK, CENTER_X, (CENTER_Y - OFFSET) - (ringRange * 0.25) + (fSize * FONTSCALE)); + Helper.drawTextWithBackground(ctx, maxValueText, font, Color.WHITE, Color.BLACK, CENTER_X, (CENTER_Y - OFFSET) - (ringRange * 0.5) + (fSize * FONTSCALE)); ctx.restore(); } From fbc42b2b7c87e58fafd3e27310de838c7d0f47b3 Mon Sep 17 00:00:00 2001 From: spinner Date: Mon, 10 Apr 2023 14:33:20 +0300 Subject: [PATCH 2/2] Origin of RadarChart now can be set to an arbitrary angle --- src/main/java/eu/hansolo/fx/charts/YPane.java | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/hansolo/fx/charts/YPane.java b/src/main/java/eu/hansolo/fx/charts/YPane.java index 375b9c66..68fc3ed8 100644 --- a/src/main/java/eu/hansolo/fx/charts/YPane.java +++ b/src/main/java/eu/hansolo/fx/charts/YPane.java @@ -92,6 +92,10 @@ public class YPane extends Region implements ChartArea { private double _offset; private DoubleProperty offset; + private double _originAngle; + private DoubleProperty originAngle; + + // ******************** Constructors ************************************** public YPane(final YSeries... SERIES) { this(Color.TRANSPARENT, new ArrayList<>(), SERIES); @@ -114,6 +118,8 @@ public YPane(final Paint BACKGROUND, final List CATEGORIES, final YSer _categoryColor = Color.BLACK; _lowerBoundY = 0; _upperBoundY = 100; + _offset = 10; + _originAngle = 0; categories = FXCollections.observableArrayList(CATEGORIES); valid = isChartTypeValid(); initGraphics(); @@ -182,12 +188,34 @@ public DoubleProperty offsetProperty() { offset = new DoublePropertyBase(_offset) { @Override protected void invalidated() { redraw(); } @Override public Object getBean() { return YPane.this; } - @Override public String getName() { return "thresholdY"; } + @Override public String getName() { return "offset"; } }; } return offset; } + public double getOriginAngle() { return _originAngle; } + + public void setOriginAngle(final double originAngle) { + _originAngle = originAngle; + redraw(); + } + + public DoubleProperty originAngleProperty() { + if (null == originAngle) { + originAngle = new DoublePropertyBase(_originAngle) { + + @Override protected void invalidated() { redraw(); } + @Override + public Object getBean() { return YPane.this; } + + @Override + public String getName() { return "originAngle"; } + }; + } + return originAngle; + } + public Paint getChartBackground() { return null == chartBackground ? _chartBackground : chartBackground.get(); } public void setChartBackground(final Paint PAINT) { if (null == chartBackground) { @@ -438,6 +466,7 @@ private void drawRadar(final YSeries SERIES) { // draw the chart data ctx.save(); + Helper.rotateCtx(ctx, CENTER_X, CENTER_Y, _originAngle); if (SERIES.getFill() instanceof RadialGradient) { ctx.setFill(new RadialGradient(0, 0, size * 0.5, size * 0.5, size * 0.45, false, CycleMethod.NO_CYCLE, ((RadialGradient) SERIES.getFill()).getStops())); } else { @@ -521,6 +550,7 @@ private void drawRadar(final YSeries SERIES) { }); } } + Helper.rotateCtx(ctx, CENTER_X, CENTER_Y, -_originAngle); ctx.restore(); } @@ -549,10 +579,12 @@ private void drawRadarOverlay(final int NO_OF_SECTORS, final ChartType TYPE) { // draw star lines ctx.save(); + Helper.rotateCtx(ctx, CENTER_X, CENTER_Y, _originAngle); for (int i = 0 ; i < NO_OF_SECTORS ; i++) { ctx.strokeLine(CENTER_X, 0.05 * size, CENTER_X, 0.5 * size); Helper.rotateCtx(ctx, CENTER_X, CENTER_Y, angleStep); } + Helper.rotateCtx(ctx, CENTER_X, CENTER_Y, -_originAngle); ctx.restore(); // draw threshold line @@ -567,6 +599,8 @@ private void drawRadarOverlay(final int NO_OF_SECTORS, final ChartType TYPE) { // prerotate if sectormode ctx.save(); + Helper.rotateCtx(ctx, CENTER_X, CENTER_Y, _originAngle); + if (ChartType.RADAR_SECTOR == TYPE) { Helper.rotateCtx(ctx, CENTER_X, CENTER_Y, angleStep * 0.5); } @@ -583,10 +617,13 @@ private void drawRadarOverlay(final int NO_OF_SECTORS, final ChartType TYPE) { index = NO_OF_SECTORS; } + for (int i = 0 ; i < index ; i++) { ctx.fillText(categories.get(i).getName(), CENTER_X, size * 0.03); Helper.rotateCtx(ctx, CENTER_X, CENTER_Y, angleStep); } + Helper.rotateCtx(ctx, CENTER_X, CENTER_Y, -_originAngle); + ctx.restore(); ctx.restore();