Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JavaFX Double Donut Chart

I am working in an offline environment so I am unable to copy the entire code base, I will try to describe as much as I can.

JavaFX does not provide a donut chart so I have created a custom donut chart by simple extending PieChart and then adding a circle to the center. The pseudocode looks like this:

public class DonutChart extends PieChart{
    private final Circle innerCircle;

    //Initialize PieChart
    //Override "layoutChartChildren()" method to override chart label with value instead of name
    //Calculate chart bounds by looping through each node to find the max/min X/Y bounds.
    //Create a smaller circle and relocate it to the center of the PieChart
}

This gave me a donut chart that i wanted, but now i wish to have a double donut chart, meaning another layer which breaks down the chart further. I did the same thing again but this time instead of just adding the 'innerCircle', i also added another piechart. Now it looks something like this:

public class DonutChart extends PieChart{
    private final Circle innerCircle;
    private PieChart innerChart;

   /*
    * Initialize PieChart
    * Override "layoutChartChildren()" method to override chart label with value instead of name
    * Calculate chart bounds by looping through each node to find the max/min X/Y bounds.
    * Create and add innerChart and innerCircle
    */
    private void addChartAndCircle(){
        if (getData().size > 0){
            Node pie = getData().get(0).getNode();
            if (pie.getParent() instanceof Pane){
                Pane parent = pie.getParent();
                if (!parent.getChildren().contains(innerChart)){
                    parent.getChildren().add(innerChart);
                }
                if (!parent.getChildren().contains(innerCircle)){
                    parent.getChildren().add(innerCircle);
                }
            }
        }
    }
    //Relocate to center
}

Only the outer chart "DonutChart" was displayed along with the inner circle, but the innerChart is nowhere to be seen. I've also observed that the width property of the inner chart is 0, and I have tried binding the property, setting max/min/pref width/height of innerChart but to no avail.

Is anyone able to create a custom donut chart which displayed 2 layers?

Double Donut Chart Illustration

like image 740
Cherple Avatar asked Sep 26 '19 03:09

Cherple


1 Answers

As the question looks intersting, I gave a try for your requirement. Indeed, I too encountered the exact issue of rendering the inner chart. Upon further investigation, looks like the issue lies in layout children of a Region.

As now we are introducing a stranger(aka inner chart), the outer chart's layoutChildren() does not know how to render it. You may ask that how come the circle is rendered. I think things will work differently with Shapes. Not sure ;-)

So to make it work, you need to provide the size of the inner chart in the layoutChartChildren() method of outer chart.

Skipping some stuff of what you provided, below is the quick demo of what I tried to get it work. I hope this can help you.

enter image description here

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.chart.PieChart;
import javafx.scene.layout.Pane;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;

import java.util.Set;
import java.util.stream.Collectors;

public class PieChartSample extends Application {

    @Override
    public void start(Stage stage) {
        Scene scene = new Scene(new Group());
        stage.setTitle("Imported Fruits");
        stage.setWidth(500);
        stage.setHeight(500);

        ObservableList<PieChart.Data> pieChartData =
                FXCollections.observableArrayList(
                        new PieChart.Data("Grapefruit", 45),
                        new PieChart.Data("Oranges", 25),
                        new PieChart.Data("Plums", 30));

        ObservableList<PieChart.Data> innerChartData =
                FXCollections.observableArrayList(
                        new PieChart.Data("G1", 20),
                        new PieChart.Data("G2", 25),
                        new PieChart.Data("O1", 10),
                        new PieChart.Data("O2", 15),
                        new PieChart.Data("P1", 15),
                        new PieChart.Data("P2", 15));
        final DonutChart chart = new DonutChart(pieChartData);
        chart.setTitle("Imported Fruits");
        chart.setInnerChartData(innerChartData);

        ((Group) scene.getRoot()).getChildren().add(chart);
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }

    class DonutChart extends PieChart {
        private final double SPACE = 30; /* Gap between outer and inner pie*/
        private PieChart innerChart;
        private Circle circle;
        private Bounds outerPieBounds;

        public DonutChart(ObservableList<PieChart.Data> data) {
            super(data);
            innerChart = new PieChart() {
                @Override
                protected void layoutChartChildren(double top, double left, double contentWidth, double contentHeight) {
                    super.layoutChartChildren(top, left, contentWidth, contentHeight);
                    final Pane chartContent = (Pane) lookup(".chart-content");
                    chartContent.setPadding(new Insets(0));
                }
            };
            innerChart.setPadding(new Insets(0));
            innerChart.setLabelsVisible(false);
            innerChart.setLegendVisible(false);

            circle = new Circle(40);
            circle.setOpacity(1);
            circle.setStyle("-fx-fill:white;");
        }

        @Override
        protected void layoutChartChildren(double top, double left, double contentWidth, double contentHeight) {
            super.layoutChartChildren(top, left, contentWidth, contentHeight);
            final Pane chartContent = (Pane) lookup(".chart-content");
            if (!chartContent.getChildren().contains(innerChart)) {
                chartContent.getChildren().add(innerChart);
            }
            if (!chartContent.getChildren().contains(circle)) {
                chartContent.getChildren().add(circle);
            }
            updateOuterPieBounds();
            double cX = outerPieBounds.getMinX() + (outerPieBounds.getWidth() / 2);
            double cY = outerPieBounds.getMinY() + (outerPieBounds.getHeight() / 2);
            circle.setCenterX(cX);
            circle.setCenterY(cY);
            double innerSize = outerPieBounds.getWidth() - (2 * SPACE);
            innerChart.resize(innerSize, innerSize); // THIS WHERE YOUR ISSUE LIES. YOU NEED TO PROVIDE THE SIZE TO INNER CHART
            innerChart.setTranslateX(cX - innerChart.getWidth() / 2);
            innerChart.setTranslateY(cY - innerChart.getHeight() / 2);
        }

        public void setInnerChartData(ObservableList<PieChart.Data> data) {
            innerChart.setData(data);
        }

        /**
         * Determining the outer pie visual bounds.
         */
        private void updateOuterPieBounds() {
            Pane chartContent = (Pane) lookup(".chart-content");
            Set<Node> pieNodes = chartContent.getChildren().stream().filter(node -> node.getStyleClass().contains("chart-pie")).collect(Collectors.toSet());
            double minX = getWidth();
            double minY = getHeight();
            double maxX = 0, maxY = 0;
            for (Node pie : pieNodes) {
                Bounds pieBounds = pie.getBoundsInParent();
                minX = Math.min(minX, pieBounds.getMinX());
                minY = Math.min(minY, pieBounds.getMinY());
                maxX = Math.max(maxX, pieBounds.getMaxX());
                maxY = Math.max(maxY, pieBounds.getMaxY());
            }
            outerPieBounds = new BoundingBox(minX, minY, maxX - minX, maxY - minY);
        }
    }
}
like image 98
Sai Dandem Avatar answered Oct 27 '22 17:10

Sai Dandem