I have this pie chart. It encodes data points in three dimensions, which are encoded via
- sector radius,
- sector angle,
- color intensity of the sector.
How it looks like?
(See here.)
Code
com.github.coderodde.javafx.PieChart3D.java:
package com.github.coderodde.javafx;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;
import javafx.scene.shape.ArcType;
/**
* This class implements a pie chart that can communicate data points in three
* dimensions:
* <ol>
* <li>the radius of a sector,</li>
* <li>the angle of a sector,</li>
* <li>color intensity of a sector.</li>
* </ol>
*
* @author Rodion "rodde" Efremov
* @version 1.6 (Dec 18, 2023)
* @since 1.6 (Dec 18, 2023)
*/
public final class PieChart3D extends Canvas {
private static final Color DEFAULT_BOX_COLOR = Color.WHITE;
private static final Color DEFAULT_CHART_BACKGROUND_COLOR = Color.WHITE;
private static final Color DEFAULT_ORIGINAL_INTENSITY_COLOR = Color.BLACK;
private Color boxColor = DEFAULT_BOX_COLOR;
private Color chartBackgroundColor = DEFAULT_CHART_BACKGROUND_COLOR;
private Color originalIntensityColor = DEFAULT_ORIGINAL_INTENSITY_COLOR;
private double angleOffset = 0.0;
private final List<PieChart3DEntry> entries = new ArrayList<>();
public PieChart3D(double dimension) {
checkDimension(dimension);
super.setWidth(dimension);
super.setHeight(dimension);
}
public Color getBoxBackgroundColor() {
return boxColor;
}
public Color getChartBackgroundColor() {
return chartBackgroundColor;
}
public Color getOriginalIntensityColor() {
return originalIntensityColor;
}
public double getAngleOffset() {
return angleOffset;
}
public void setBoxBackgroundColor(Color boxColor) {
this.boxColor =
Objects.requireNonNull(boxColor, "The input color is null.");
}
public void setChartBackgroundColor(Color chartBackgroundColor) {
this.chartBackgroundColor =
Objects.requireNonNull(
chartBackgroundColor,
"The input color is null.");
}
public void setOriginalIntensityColor(Color originalIntensityColor) {
this.originalIntensityColor =
Objects.requireNonNull(
originalIntensityColor,
"The input color is null.");
}
public void setAngleOffset(double angleOffset) {
checkAngleOffset(angleOffset);
angleOffset %= 360.0;
if (angleOffset < 0.0) {
angleOffset += 360.0;
}
this.angleOffset = angleOffset;
}
public PieChart3DEntry get(int index) {
return entries.get(index);
}
public void set(int index, PieChart3DEntry entry) {
entries.set(index, Objects.requireNonNull(entry, "The entry is null."));
}
public int size() {
return entries.size();
}
public void add(PieChart3DEntry entry) {
entries.add(Objects.requireNonNull(entry, "The entry is null."));
}
public void add(int index, PieChart3DEntry entry) {
entries.add(index, Objects.requireNonNull(entry, "The entry is null."));
}
public void remove(int index) {
entries.remove(index);
}
public void draw() {
GraphicsContext gc = getGraphicsContext2D();
drawBoundingBox(gc);
drawEntirePieChart(gc);
if (!entries.isEmpty()) {
// Once here, we have entries to draw:
drawChart(gc);
}
}
private void drawBoundingBox(GraphicsContext gc) {
gc.setFill(getBoxBackgroundColor());
gc.fillRect(0.0,
0.0,
getWidth(),
getHeight());
}
private void drawEntirePieChart(GraphicsContext gc) {
gc.setFill(getChartBackgroundColor());
gc.fillOval(0.0,
0.0,
getHeight(),
getWidth());
}
private void drawChart(GraphicsContext gc) {
double sumOfRelativeAngles = computeSumOfRelativeAngles();
double maximumRadiusValue = getMaximumRadiusValue();
double maximumColorIntensityValue = getMaximumColorIntensity();
double startAngle = 90.0 - angleOffset;
for (PieChart3DEntry entry : entries) {
double actualAngle = 360.0 * entry.getSectorAngleValue()
/ sumOfRelativeAngles;
double actualRadius =
(getHeight() / 2.0) * (entry.getSectorRadiusValue()
/ maximumRadiusValue);
Color actualColor =
obtainColor(entry.getSectorColorIntensityValue() /
maximumColorIntensityValue);
double sectorStartAngle = startAngle - actualAngle;
startAngle -= actualAngle;
drawSector(gc,
sectorStartAngle,
actualAngle,
actualRadius,
actualColor);
}
}
/**
* Draws a single sector.
*
* @param gc the graphics context.
* @param startAngle the start angle.
* @param angle the end angle.
* @param actualRadius the radius of the sector.
* @param color the color of the sector.
*/
private void drawSector(GraphicsContext gc,
double startAngle,
double angle,
double actualRadius,
Color color) {
double canvasDimension = getHeight();
double centerX = canvasDimension / 2.0;
double centerY = canvasDimension / 2.0;
gc.setFill(color);
gc.fillArc(centerX - actualRadius,
centerY - actualRadius,
2.0 * actualRadius,
2.0 * actualRadius,
startAngle,
angle,
ArcType.ROUND);
}
private double computeSumOfRelativeAngles() {
double angleSum = 0.0;
for (PieChart3DEntry entry : entries) {
angleSum += entry.getSectorAngleValue();
}
return angleSum;
}
private static void checkDimension(double dimension) {
checkIsNotNaN(dimension, "The dimension is NaN.");
checkIsNotInfinite(dimension,
"The dimention is infinite in absolute value.");
if (dimension <= 0.0) {
throw new IllegalArgumentException(
"The dimension is non-positive.");
}
}
private static void checkAngleOffset(double angleOffset) {
checkIsNotNaN(angleOffset, "The angle offset is NaN.");
checkIsNotInfinite(angleOffset,
"The angle offset is infinite in absolute value.");
}
private static void checkIsNotNaN(double value, String exceptionMessage) {
if (Double.isNaN(value)) {
throw new IllegalArgumentException(exceptionMessage);
}
}
private static void checkIsNotInfinite(double value,
String exceptionMessage) {
if (Double.isInfinite(value)) {
throw new IllegalArgumentException(exceptionMessage);
}
}
private double getMaximumRadiusValue() {
Optional<PieChart3DEntry> optional =
entries.stream().max((e1, e2) -> {
return Double.compare(e1.getSectorRadiusValue(),
e2.getSectorRadiusValue());
});
if (optional.isEmpty()) {
throw new IllegalStateException("No entries in this chart.");
}
return optional.get().getSectorRadiusValue();
}
private double getMaximumColorIntensity() {
Optional<PieChart3DEntry> optional =
entries.stream().max((e1, e2) -> {
return Double.compare(e1.getSectorColorIntensityValue(),
e2.getSectorColorIntensityValue());
});
if (optional.isEmpty()) {
throw new IllegalStateException("No entries in this chart.");
}
return optional.get().getSectorColorIntensityValue();
}
private double getSumOfEntrySectorAngles() {
double sum = 0.0;
for (PieChart3DEntry entry : entries) {
sum += entry.getSectorAngleValue();
}
return sum;
}
private double normalizeSectorAngle(PieChart3DEntry entry) {
return 360.0 *
(entry.getSectorAngleValue() /
getSumOfEntrySectorAngles());
}
private Color obtainColor(double intensity) {
double r = originalIntensityColor.getRed();
double g = originalIntensityColor.getGreen();
double b = originalIntensityColor.getBlue();
r += (1.0 - r) * (1.0 - intensity);
g += (1.0 - g) * (1.0 - intensity);
b += (1.0 - b) * (1.0 - intensity);
return new Color(r, g, b, 1.0);
}
}
com.github.coderodde.javafx.PieChart3DEntry.java:
package com.github.coderodde.javafx;
/**
* This class implements the pie chart entry.
*
* @author Rodion "rodde" Efremov
* @version 1.6 (Dec 18, 2023)
* @since 1.6 (Dec 18, 2023)
*/
public final class PieChart3DEntry {
/**
* Holds the sector radius of this entry.
*/
private double sectorRadiusValue;
/**
* Holds the sector angle of this entry.
*/
private double sectorAngleValue;
/**
* Holds the sector color intensity of this entry.
*/
private double sectorColorIntensityValue;
public double getSectorRadiusValue() {
return sectorRadiusValue;
}
public double getSectorAngleValue() {
return sectorAngleValue;
}
public double getSectorColorIntensityValue() {
return sectorColorIntensityValue;
}
public void setSectorRadiusValue(double sectorRadiusValue) {
checkValue(sectorRadiusValue);
this.sectorRadiusValue = sectorRadiusValue;
}
public void setSectorAngleValue(double sectorAngleValue) {
checkValue(sectorAngleValue);
this.sectorAngleValue = sectorAngleValue;
}
public void setSectorColorIntensityValue(double sectorColorIntensityValue) {
checkValue(sectorColorIntensityValue);
this.sectorColorIntensityValue = sectorColorIntensityValue;
}
public PieChart3DEntry withSectorRadiusValue(double sectorRadiusValue) {
setSectorRadiusValue(sectorRadiusValue);
return this;
}
public PieChart3DEntry withSectorAngleValue(double sectorAngleValue) {
setSectorAngleValue(sectorAngleValue);
return this;
}
public PieChart3DEntry withSectorColorIntensityValue(
double sectorColorIntensityValue) {
setSectorColorIntensityValue(sectorColorIntensityValue);
return this;
}
@Override
public String toString() {
return "[sectorRadiusValue = "
+ sectorRadiusValue
+ ", sectorAngleValue = "
+ sectorAngleValue
+ ", sectorColorIntensityValue = "
+ sectorColorIntensityValue
+ "]";
}
private static void checkValue(double value) {
if (Double.isNaN(value)) {
throw new IllegalArgumentException("The input value is NaN.");
}
if (Double.isInfinite(value)) {
throw new IllegalArgumentException("The input value is infinite.");
}
if (value < 0.0) {
throw new IllegalArgumentException("The input value is negative.");
}
}
}
com.github.coderodde.javafx.PieChart3DDemo.java:
import java.util.Random;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
public class PieChart3DDemo extends Application {
private static final double DIMENSION = 500.0;
private static final double CANVAS_DIMENSION = 400.0;
private static final int MAXIMUM_NUMBER_OF_SECTORS = 20;
private static final double MAXIMUM_VALUE = 200.0;
private static final double FULL_ANGLE = 360.0;
private static final Color CHART_BACKGROUND_COLOR = new Color(0.9,
0.9,
0.9,
1.0);
private static final DemoTask DEMO_TASK = new DemoTask();
public static void main(String[] args) {
Runtime.getRuntime()
.addShutdownHook(
new Thread(() -> {
DEMO_TASK.stop();
}));
launch(args);
}
@Override
public void start(Stage primaryStage) {
primaryStage.setTitle("PieChart3D demo");
StackPane root = new StackPane();
primaryStage.setScene(
new Scene(root,
DIMENSION,
DIMENSION));
DEMO_TASK.setRoot(root);
DEMO_TASK.setStage(primaryStage);
new Thread(DEMO_TASK).start();
primaryStage.show();
}
private static Color getRandomColor(Random random) {
return new Color(random.nextDouble(),
random.nextDouble(),
random.nextDouble(),
1.0);
}
private static int getRandomNumberOfSectors(Random random) {
return random.nextInt(MAXIMUM_NUMBER_OF_SECTORS + 1);
}
private static double getRandomValue(Random random) {
return MAXIMUM_VALUE * random.nextDouble();
}
private static double getRandomAngleOffset(Random random) {
return FULL_ANGLE * random.nextDouble();
}
static PieChart3D getRandomChart(Random random) {
double angleOffset = getRandomAngleOffset(random);
int numberOfSectors = getRandomNumberOfSectors(random);
PieChart3D chart = new PieChart3D(CANVAS_DIMENSION);
chart.setChartBackgroundColor(CHART_BACKGROUND_COLOR);
chart.setAngleOffset(angleOffset);
chart.setOriginalIntensityColor(getRandomColor(random));
for (int i = 0; i < numberOfSectors; i++) {
PieChart3DEntry entry =
new PieChart3DEntry()
.withSectorAngleValue(getRandomValue(random))
.withSectorColorIntensityValue(getRandomValue(random))
.withSectorRadiusValue(getRandomValue(random));
chart.add(entry);
}
return chart;
}
}
final class DemoTask extends Task<Void> {
private static final long SLEEP_DURATION_IN_MILLISECONDS = 1L;
private static final int FRAMES_PER_CHART = 2_000;
private volatile boolean doRun = true;
private final Random random = new Random();
private StackPane root;
private Stage stage;
void stop() {
doRun = false;
}
void setRoot(StackPane root) {
this.root = root;
}
void setStage(Stage stage) {
this.stage = stage;
}
@Override
protected Void call() throws Exception {
stage.setOnCloseRequest(new EventHandler<WindowEvent>() {
@Override
public void handle(WindowEvent t) {
System.out.println("Exiting...");
Platform.exit();
System.exit(0);
}
});
int iterations = 0;
while (doRun) {
PieChart3D chart = PieChart3DDemo.getRandomChart(random);
for (int i = 0; i < FRAMES_PER_CHART; i++) {
Platform.runLater(() -> {
root.getChildren().clear();
root.getChildren().add(chart);
chart.setAngleOffset(chart.getAngleOffset() + 0.1);
chart.draw();
});
try {
Thread.sleep(SLEEP_DURATION_IN_MILLISECONDS);
} catch (InterruptedException ex) {}
}
System.out.println("Iterated: " + ++iterations);
}
return null;
}
}
Critique request
As always, I would like to hear any comment about improving the code.
-
1\$\begingroup\$ @Mast Added a link to imgur. \$\endgroup\$coderodde– coderodde2023年12月19日 12:20:46 +00:00Commented Dec 19, 2023 at 12:20
1 Answer 1
Small issue, I don't think private static final DemoTask DEMO_TASK = new DemoTask();
needs static
.
My other issue is with DemoTask
extending Task<Void>
. Most time I see Thread.sleep(SLEEP_DURATION_IN_MILLISECONDS);
in a JavaFX app, I think something is wrong. In your case, getRandomChart(random)
does not appear to do a large amount of work. I think you should implement this using TimeLine
. The way your Task
is designed goes against how it was designed to be used. You should not be dependent on a boolean to stop and start a Task
. Moreover, you should not try to stop a Task
to begin with. Task
should do some work and return some state they will allow you to get a value, update the GUI, or move forward.