在 JavaFX 中制作半圆形图表的最佳方法是什么?

What's the best way to make a half circle chart in JavaFX?

我想创建一个类似 this in JavaFX, but the PieChart class 的图表似乎无法实现。有什么我想念的吗?如果没有,即使我需要安装库,在 Java 中更简单的方法是什么?

我一直在尝试 JFreeChart 和 GRAL 等几个库,但它们在 JavaFX 中的效果并不理想,所以我尝试创建自己的 SemiCircleChart class.这是非常基本的,因为它在应该扩展 Chart 时扩展了 Parent(我猜)。我对 Java 和编程还很陌生,所以请多多包涵,因为我想还有很多地方可以改进(如果您有任何想法,请编辑我的答案!)。所以这就是我所做的:

SemiCircleChart.java

import java.util.List;
import javafx.event.EventHandler;
import javafx.scene.Parent;
import javafx.scene.control.Tooltip;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.shape.Arc;
import javafx.scene.shape.ArcType;

public class SemiCircleChart extends Parent{ //TODO: this should extend Chart in the future
    private List<Data> dataList;
    private double centerX;
    private double centerY;
    private double radius;
    private double innerHoleRadius;
    private double separatorLength;
    private Color holeColor = Color.web("#f4f4f4");
    private Color separatorColor = holeColor;

    public SemiCircleChart(List<Data> data){
        this(data, 0, 0,100);
    }

    public SemiCircleChart(List<Data> dataList, double centerX, double centerY, double radius){
        this(dataList,centerX,centerY,radius,0,0);
    }

    public SemiCircleChart(List<Data> dataList, double centerX, double centerY, double radius, double innerHoleRadius, double separatorLength){
        this.dataList = dataList;
        this.centerX = centerX;
        this.centerY = centerY;
        this.radius = radius;
        this.innerHoleRadius = innerHoleRadius;
        this.separatorLength = separatorLength;
        makeChart();
    }

    public void setHoleColor(Color holeColor){
        this.holeColor = holeColor;

        //reset the chart
        this.getChildren().clear();
        makeChart();
    }

    public void setSeparatorColor(Color separatorColor){
        this.separatorColor = separatorColor;

        //reset the chart
        this.getChildren().clear();
        makeChart();
    }
    private void makeChart(){
        double totalValues=0;

        for(Data data:dataList){
            totalValues += data.getValue();
        }

        double ratio = totalValues/180;
        double totalAngle=180;

        for(int i=0;i<dataList.size();i++){
            Data data = dataList.get(i);
            Arc slice = new Arc();
            slice.setCenterX(centerX);
            slice.setCenterY(centerY);
            slice.setRadiusX(radius);
            slice.setRadiusY(radius);
            slice.setType(ArcType.ROUND);
            slice.setFill(data.getColor());
            slice.setStartAngle(totalAngle);

            double length = data.getValue()/ratio;
            slice.setLength(-length);
            totalAngle -= length;

            //adding a tooltip
             Tooltip tooltip = new Tooltip(data.getName());


                slice.setOnMouseEntered(new EventHandler<MouseEvent>() {
                    @Override
                    public void handle(MouseEvent event) {
                        slice.setFill(data.getColor().brighter());
                        tooltip.show(slice, event.getScreenX()+10, event.getScreenY()+10);
                    }
                });

                slice.setOnMouseExited(new EventHandler<MouseEvent>() {
                    @Override
                    public void handle(MouseEvent event) {
                        slice.setFill(data.getColor());
                        tooltip.hide();
                    }
                });
            this.getChildren().add(slice);

            if(separatorLength<0){
                throw new IllegalArgumentException("separatorLength can't be negative");
            }
            else if(separatorLength>0&&i<dataList.size()-1){
                Arc sep = new Arc();
                sep.setCenterX(centerX);
                sep.setCenterY(centerY);
                sep.setRadiusX(radius+1);//enough to cover the border of the slice
                sep.setRadiusY(radius+1);
                sep.setType(ArcType.ROUND);
                sep.setFill(separatorColor);
                sep.setLength(-separatorLength);
                //we calculate the start angle so half of the separator overlaps one slice and the other half the next slice.
                double halfLength = separatorLength/2;
                sep.setStartAngle(totalAngle+halfLength);
                this.getChildren().add(sep);
            }
        }

        if(innerHoleRadius<0){
            throw new IllegalArgumentException("innerHoleRadius can't be negative");
        }
        else{
            Arc hole = new Arc();
            hole.setCenterX(centerX);
            hole.setCenterY(centerY);
            hole.setRadiusX(innerHoleRadius);
            hole.setRadiusY(innerHoleRadius);
            hole.setType(ArcType.ROUND);
            hole.setFill(holeColor);
            hole.setStartAngle(0);
            hole.setLength(180);
            this.getChildren().add(hole);
        }
    }


    public static final class Data{
        private String name;
        private double value;
        private Color color;

        public Data(String name, double value) {
            this(name,value,Color.color(Math.random(), Math.random(), Math.random()));
        }

        public Data(String name, double value, Color color) {
            this.name = name;
            this.value = value;
            this.color = color;
        }

        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public double getValue() {
            return value;
        }
        public void setValue(double value) {
            this.value = value;
        }
        public Color getColor() {
            return color;
        }
        public void setColor(Color color) {
            this.color = color;
        }

    }
}

Test.java

import java.util.ArrayList;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

public class Test extends Application{
    @Override
    public void start(Stage primaryStage) {
        ArrayList<SemiCircleChart.Data> dataList = new ArrayList<SemiCircleChart.Data>();

        dataList.add(new SemiCircleChart.Data("data1", 1, Color.RED));
        dataList.add(new SemiCircleChart.Data("data2", 2, Color.GREEN));
        dataList.add(new SemiCircleChart.Data("data3", 3, Color.BLUE));

        SemiCircleChart chart = new SemiCircleChart(dataList, 500, 500, 500,100,2);

        Pane pane = new Pane(chart);
        Scene scene = new Scene(pane);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

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