All Downloads are FREE. Search and download functionalities are using the official Maven repository.

eu.hansolo.fx.charts.SunburstChart Maven / Gradle / Ivy

There is a newer version: 1.0.5
Show newest version
/*
 * Copyright (c) 2017 by Gerrit Grunwald
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package eu.hansolo.fx.charts;


import eu.hansolo.fx.charts.data.ChartItem;
import eu.hansolo.fx.charts.event.TreeNodeEvent;
import eu.hansolo.fx.charts.event.TreeNodeEvent.EventType;
import eu.hansolo.fx.charts.font.Fonts;
import eu.hansolo.fx.charts.tools.Helper;
import eu.hansolo.fx.charts.tree.TreeNode;
import javafx.beans.DefaultProperty;
import javafx.beans.InvalidationListener;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.BooleanPropertyBase;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.IntegerPropertyBase;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ObjectPropertyBase;
import javafx.collections.ObservableList;
import javafx.event.WeakEventHandler;
import javafx.geometry.Insets;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.Border;
import javafx.scene.layout.BorderStroke;
import javafx.scene.layout.BorderStrokeStyle;
import javafx.scene.layout.BorderWidths;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.ArcTo;
import javafx.scene.shape.ArcType;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.shape.StrokeLineCap;
import javafx.scene.text.TextAlignment;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import static eu.hansolo.fx.charts.tools.Helper.clamp;


@DefaultProperty("children")
public class SunburstChart extends Region {
    public enum TextOrientation {
        HORIZONTAL(12),
        TANGENT(8),
        ORTHOGONAL(12);

        private double maxAngle;
        TextOrientation(final double MAX_ANGLE) {
            maxAngle = MAX_ANGLE;
        }

        public double getMaxAngle() { return maxAngle; }
    }
    public enum VisibleData {
        NONE, NAME, VALUE, NAME_VALUE
    }

    private static final double PREFERRED_WIDTH   = 250;
    private static final double PREFERRED_HEIGHT  = 250;
    private static final double MINIMUM_WIDTH     = 50;
    private static final double MINIMUM_HEIGHT    = 50;
    private static final double MAXIMUM_WIDTH     = 2048;
    private static final double MAXIMUM_HEIGHT    = 2048;
    private static final Color  BRIGHT_TEXT_COLOR = Color.WHITE;
    private static final Color  DARK_TEXT_COLOR   = Color.BLACK;
    private              double                          size;
    private              double                          width;
    private              double                          height;
    private              double                          centerX;
    private              double                          centerY;
    private              Pane                            segmentPane;
    private              Canvas                          chartCanvas;
    private              GraphicsContext                 chartCtx;
    private              Pane                            pane;
    private              Paint                           backgroundPaint;
    private              Paint                           borderPaint;
    private              double                          borderWidth;
    private              List                      segments;
    private              VisibleData                     _visibleData;
    private              ObjectProperty     visibleData;
    private              TextOrientation                 _textOrientation;
    private              ObjectProperty textOrientation;
    private              Color                           _backgroundColor;
    private              ObjectProperty           backgroundColor;
    private              Color                           _textColor;
    private              ObjectProperty           textColor;
    private              boolean                         _useColorFromParent;
    private              BooleanProperty                 useColorFromParent;
    private              int                             _decimals;
    private              IntegerProperty                 decimals;
    private              boolean                         _interactive;
    private              BooleanProperty                 interactive;
    private              boolean                         _autoTextColor;
    private              BooleanProperty                 autoTextColor;
    private              Color                           _brightTextColor;
    private              ObjectProperty           brightTextColor;
    private              Color                           _darkTextColor;
    private              ObjectProperty           darkTextColor;
    private              boolean                         _useChartItemTextColor;
    private              BooleanProperty                 useChartItemTextColor;
    private              String                          formatString;
    private              TreeNode                        tree;
    private              TreeNode                        root;
    private              int                             maxLevel;
    private              Map>    levelMap;
    private              InvalidationListener            sizeListener;



    // ******************** Constructors **************************************
    public SunburstChart() {
        this(new TreeNode(new ChartItem()));
    }
    public SunburstChart(final TreeNode TREE) {
        backgroundPaint        = Color.TRANSPARENT;
        borderPaint            = Color.TRANSPARENT;
        borderWidth            = 0d;
        segments               = new ArrayList<>(64);
        _visibleData           = VisibleData.NAME;
        _textOrientation       = TextOrientation.TANGENT;
        _backgroundColor       = Color.WHITE;
        _textColor             = Color.BLACK;
        _useColorFromParent    = false;
        _decimals              = 0;
        _interactive           = false;
        _autoTextColor         = true;
        _brightTextColor       = BRIGHT_TEXT_COLOR;
        _darkTextColor         = DARK_TEXT_COLOR;
        _useChartItemTextColor = false;
        formatString           = "%.0f";
        tree                   = TREE;
        levelMap               = new HashMap<>(8);
        sizeListener           = o -> resize();
        initGraphics();
        registerListeners();
    }


    // ******************** Initialization ************************************
    private void initGraphics() {
        if (Double.compare(getPrefWidth(), 0.0) <= 0 || Double.compare(getPrefHeight(), 0.0) <= 0 || Double.compare(getWidth(), 0.0) <= 0 ||
            Double.compare(getHeight(), 0.0) <= 0) {
            if (getPrefWidth() > 0 && getPrefHeight() > 0) {
                setPrefSize(getPrefWidth(), getPrefHeight());
            } else {
                setPrefSize(PREFERRED_WIDTH, PREFERRED_HEIGHT);
            }
        }

        segmentPane = new Pane();

        chartCanvas = new Canvas(PREFERRED_WIDTH, PREFERRED_HEIGHT);
        chartCanvas.setMouseTransparent(true);

        chartCtx    = chartCanvas.getGraphicsContext2D();

        pane = new Pane(segmentPane, chartCanvas);
        pane.setBackground(new Background(new BackgroundFill(backgroundPaint, CornerRadii.EMPTY, Insets.EMPTY)));
        pane.setBorder(new Border(new BorderStroke(borderPaint, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, new BorderWidths(borderWidth))));

        getChildren().setAll(pane);

        prepareData();
    }

    private void registerListeners() {
        widthProperty().addListener(sizeListener);
        heightProperty().addListener(sizeListener);
        tree.setOnTreeNodeEvent(e -> redraw());
    }


    // ******************** Methods *******************************************
    @Override public void layoutChildren() {
        super.layoutChildren();
    }

    @Override protected double computeMinWidth(final double HEIGHT) { return MINIMUM_WIDTH; }
    @Override protected double computeMinHeight(final double WIDTH) { return MINIMUM_HEIGHT; }
    @Override protected double computePrefWidth(final double HEIGHT) { return super.computePrefWidth(HEIGHT); }
    @Override protected double computePrefHeight(final double WIDTH) { return super.computePrefHeight(WIDTH); }
    @Override protected double computeMaxWidth(final double HEIGHT) { return MAXIMUM_WIDTH; }
    @Override protected double computeMaxHeight(final double WIDTH) { return MAXIMUM_HEIGHT; }

    @Override public ObservableList getChildren() { return super.getChildren(); }

    public void dispose() {
        widthProperty().removeListener(sizeListener);
        heightProperty().removeListener(sizeListener);
        tree.removeAllTreeNodeEventListeners();
    }

    /**
     * Returns the data that should be visualized in the chart segments
     * @return the data that should be visualized in the chart segments
     */
    public VisibleData getVisibleData() { return null == visibleData ? _visibleData : visibleData.get(); }
    /**
     * Defines the data that should be visualized in the chart segments
     * @param VISIBLE_DATA
     */
    public void setVisibleData(final VisibleData VISIBLE_DATA) {
        if (null == visibleData) {
            _visibleData = VISIBLE_DATA;
            redraw();
        } else {
            visibleData.set(VISIBLE_DATA);
        }
    }
    public ObjectProperty visibleDataProperty() {
        if (null == visibleData) {
            visibleData = new ObjectPropertyBase(_visibleData) {
                @Override protected void invalidated() { redraw(); }
                @Override public Object getBean() { return SunburstChart.this; }
                @Override public String getName() { return "visibleData"; }
            };
            _visibleData = null;
        }
        return visibleData;
    }

    /**
     * Returns the orientation the text will be drawn in the segments
     * @return the orientation the text will be drawn in the segments
     */
    public TextOrientation getTextOrientation() { return null == textOrientation ? _textOrientation : textOrientation.get(); }
    /**
     * Defines the orientation the text will be drawn in the segments
     * @param ORIENTATION
     */
    public void setTextOrientation(final TextOrientation ORIENTATION) {
        if (null == textOrientation) {
            _textOrientation = ORIENTATION;
            redraw();
        } else {
            textOrientation.set(ORIENTATION);
        }
    }
    public ObjectProperty textOrientationProperty() {
        if (null == textOrientation) {
            textOrientation = new ObjectPropertyBase(_textOrientation) {
                @Override protected void invalidated() { redraw(); }
                @Override public Object getBean() { return SunburstChart.this; }
                @Override public String getName() { return "textOrientation"; }
            };
            _textOrientation = null;
        }
        return textOrientation;
    }

    /**
     * Returns the color that will be used to fill the background of the chart
     * @return the color that will be used to fill the background of the chart
     */
    public Color getBackgroundColor() { return null == backgroundColor ? _backgroundColor : backgroundColor.get(); }
    /**
     * Defines the color that will be used to fill the background of the chart
     * @param COLOR
     */
    public void setBackgroundColor(final Color COLOR) {
        if (null == backgroundColor) {
            _backgroundColor = COLOR;
            redraw();
        } else {
            backgroundColor.set(COLOR);
        }
    }
    public ObjectProperty backgroundColorProperty() {
        if (null == backgroundColor) {
            backgroundColor = new ObjectPropertyBase(_backgroundColor) {
                @Override protected void invalidated() { redraw(); }
                @Override public Object getBean() { return SunburstChart.this; }
                @Override public String getName() { return "backgroundColor"; }
            };
            _backgroundColor = null;
        }
        return backgroundColor;
    }

    /**
     * Returns the color that will be used to draw text in segments if useChartItemTextColor == false
     * @return the color that will be used to draw text in segments if useChartItemTextColor == false
     */
    public Color getTextColor() { return null == textColor ? _textColor : textColor.get(); }
    /**
     * Defines the color that will be used to draw text in segments if useChartItemTextColor == false
     * @param COLOR
     */
    public void setTextColor(final Color COLOR) {
        if (null == textColor) {
            _textColor = COLOR;
            redraw();
        } else {
            textColor.set(COLOR);
        }
    }
    public ObjectProperty textColorProperty() {
        if (null == textColor) {
            textColor = new ObjectPropertyBase(_textColor) {
                @Override protected void invalidated() { redraw(); }
                @Override public Object getBean() { return SunburstChart.this; }
                @Override public String getName() { return "textColor"; }
            };
            _textColor = null;
        }
        return textColor;
    }

    /**
     * Returns true if the color of all chart segments in one group should be filled with the color
     * of the groups root node or by the color defined in the chart data elements
     * @return
     */
    public boolean getUseColorFromParent() { return null == useColorFromParent ? _useColorFromParent : useColorFromParent.get(); }
    /**
     * Defines if tthe color of all chart segments in one group should be filled with the color
     * of the groups root node or by the color defined in the chart data elements
     * @param USE
     */
    public void setUseColorFromParent(final boolean USE) {
        if (null == useColorFromParent) {
            _useColorFromParent = USE;
            redraw();
        } else {
            useColorFromParent.set(USE);
        }
    }
    public BooleanProperty useColorFromParentProperty() {
        if (null == useColorFromParent) {
            useColorFromParent = new BooleanPropertyBase(_useColorFromParent) {
                @Override protected void invalidated() { redraw(); }
                @Override public Object getBean() { return SunburstChart.this; }
                @Override public String getName() { return "useColorFromParent"; }
            };
        }
        return useColorFromParent;
    }

    /**
     * Returns the number of decimals that will be used to format the values in the tooltip
     * @return
     */
    public int getDecimals() { return null == decimals ? _decimals : decimals.get(); }
    /**
     * Defines the number of decimals that will be used to format the values in the tooltip
     * @param DECIMALS
     */
    public void setDecimals(final int DECIMALS) {
        if (null == decimals) {
            _decimals    = clamp(0, 5, DECIMALS);
            formatString = new StringBuilder("%.").append(_decimals).append("f").toString();
            redraw();
        } else {
            decimals.set(DECIMALS);
        }
    }
    public IntegerProperty decimalsProperty() {
        if (null == decimals) {
            decimals = new IntegerPropertyBase(_decimals) {
                @Override protected void invalidated() {
                    set(clamp(0, 5, get()));
                    formatString = new StringBuilder("%.").append(get()).append("f").toString();
                    redraw();
                }
                @Override public Object getBean() { return SunburstChart.this; }
                @Override public String getName() { return "decimals"; }
            };
        }
        return decimals;
    }

    /**
     * Returns true if the chart is drawn using Path elements, fire ChartItemEvents and show tooltips on segments.
     * @return
     */
    public boolean isInteractive() { return null == interactive ? _interactive : interactive.get(); }
    /**
     * Defines if the chart should be drawn using Path elements, fire ChartItemEvents and shows tooltips on segments or
     * if the the chart should be drawn using one Canvas node.
     * @param INTERACTIVE
     */
    public void setInteractive(final boolean INTERACTIVE) {
        if (null == interactive) {
            _interactive = INTERACTIVE;
            redraw();
        } else {
            interactive.set(INTERACTIVE);
        }
    }
    public BooleanProperty interactiveProperty() {
        if (null == interactive) {
            interactive = new BooleanPropertyBase(_interactive) {
                @Override protected void invalidated() { redraw(); }
                @Override public Object getBean() { return SunburstChart.this; }
                @Override public String getName() { return "interactive"; }
            };
        }
        return interactive;
    }

    /**
     * Returns true if the text color of the chart data should be adjusted according to the chart data fill color.
     * e.g. if the fill color is dark the text will be set to the defined brightTextColor and vice versa.
     * @return true if the text color of the chart data should be adjusted according to the chart data fill color
     */
    public boolean isAutoTextColor() { return null == autoTextColor ? _autoTextColor : autoTextColor.get(); }
    /**
     * Defines if the text color of the chart data should be adjusted according to the chart data fill color
     * @param AUTOMATIC
     */
    public void setAutoTextColor(final boolean AUTOMATIC) {
        if (null == autoTextColor) {
            _autoTextColor = AUTOMATIC;
            adjustTextColors();
            redraw();
        } else {
            autoTextColor.set(AUTOMATIC);
        }
    }
    public BooleanProperty autoTextColorProperty() {
        if (null == autoTextColor) {
            autoTextColor = new BooleanPropertyBase(_autoTextColor) {
                @Override protected void invalidated() {
                    adjustTextColors();
                    redraw();
                }
                @Override public Object getBean() { return SunburstChart.this; }
                @Override public String getName() { return "autoTextColor"; }
            };
        }
        return autoTextColor;
    }

    /**
     * Returns the color that will be used by the autoTextColor feature as the bright text on dark segment fill colors
     * @return the color that will be used by the autoTextColor feature as the bright text on dark segment fill colors
     */
    public Color getBrightTextColor() { return null == brightTextColor ? _brightTextColor : brightTextColor.get(); }

    /**
     * Defines the color that will be used by the autoTextColor feature as the bright text on dark segment fill colors
     * @param COLOR
     */
    public void setBrightTextColor(final Color COLOR) {
        if (null == brightTextColor) {
            _brightTextColor = COLOR;
            if (isAutoTextColor()) {
                adjustTextColors();
                redraw();
            }
        } else {
            brightTextColor.set(COLOR);
        }
    }
    public ObjectProperty brightTextColorProperty() {
        if (null == brightTextColor) {
            brightTextColor = new ObjectPropertyBase(_brightTextColor) {
                @Override protected void invalidated() {
                    if (isAutoTextColor()) {
                        adjustTextColors();
                        redraw();
                    }
                }
                @Override public Object getBean() { return SunburstChart.this; }
                @Override public String getName() { return "brightTextColor"; }
            };
            _brightTextColor = null;
        }
        return brightTextColor;
    }

    /**
     * Returns the color that will be used by the autoTextColor feature as the dark text on bright segment fill colors
     * @return the color that will be used by the autoTextColor feature as the dark text on bright segment fill colors
     */
    public Color getDarkTextColor() { return null == darkTextColor ? _darkTextColor : darkTextColor.get(); }
    /**
     * Defines the color that will be used by the autoTextColor feature as the dark text on bright segment fill colors
     * @param COLOR
     */
    public void setDarkTextColor(final Color COLOR) {
        if (null == darkTextColor) {
            _darkTextColor = COLOR;
            if (isAutoTextColor()) {
                adjustTextColors();
                redraw();
            }
        } else {
            darkTextColor.set(COLOR);
        }
    }
    public ObjectProperty darkTextColorProperty() {
        if (null == darkTextColor) {
            darkTextColor = new ObjectPropertyBase(_darkTextColor) {
                @Override protected void invalidated() {
                    if (isAutoTextColor()) {
                        adjustTextColors();
                        redraw();
                    }
                }
                @Override public Object getBean() { return SunburstChart.this; }
                @Override public String getName() { return "darkTextColor"; }
            };
            _darkTextColor = null;
        }
        return darkTextColor;
    }

    /**
     * Returns true if the text color of the ChartItem elements should be used to
     * fill the text in the segments
     * @return true if the text color of the segments will be taken from the ChartItem elements
     */
    public boolean getUseChartItemTextColor() { return null == useChartItemTextColor ? _useChartItemTextColor : useChartItemTextColor.get(); }
    /**
     * Defines if the text color of the segments should be taken from the ChartItem elements
     * @param USE
     */
    public void setUseChartItemTextColor(final boolean USE) {
        if (null == useChartItemTextColor) {
            _useChartItemTextColor = USE;
            redraw();
        } else {
            useChartItemTextColor.set(USE);
        }
    }
    public BooleanProperty useChartItemTextColor() {
        if (null == useChartItemTextColor) {
            useChartItemTextColor = new BooleanPropertyBase(_useChartItemTextColor) {
                @Override protected void invalidated() { redraw(); }
                @Override public Object getBean() { return SunburstChart.this; }
                @Override public String getName() { return "useChartItemTextColor"; }
            };
        }
        return useChartItemTextColor;
    }

    /**
     * Defines the root element of the tree
     * @param TREE
     */
    public void setTree(final TreeNode TREE) {
        if (null != tree) { tree.flattened().forEach(node -> node.removeAllTreeNodeEventListeners()); }
        tree = TREE;
        tree.flattened().forEach(node -> node.setOnTreeNodeEvent(e -> redraw()));
        prepareData();
        if (isAutoTextColor()) { adjustTextColors(); }
        drawChart();
    }

    private void adjustTextColors() {
        Color brightColor = getBrightTextColor();
        Color darkColor   = getDarkTextColor();
        root.stream().forEach(node -> {
            ChartItem data          = node.getData();
            boolean   darkFillColor = Helper.isDark(data.getFillColor());
            boolean   darkTextColor = Helper.isDark(data.getTextColor());
            if (darkFillColor && darkTextColor) { data.setTextColor(brightColor); }
            if (!darkFillColor && !darkTextColor) { data.setTextColor(darkColor); }
        });
    }

    private void prepareData() {
        root     = tree.getTreeRoot();
        maxLevel = root.getMaxLevel();

        // Create map of all nodes per level
        levelMap.clear();
        for (int i = 0 ; i <= maxLevel ; i++) { levelMap.put(i, new ArrayList<>()); }
        root.stream().forEach(node -> levelMap.get(node.getDepth()).add(node));

        for (int level = 1 ; level < maxLevel ; level++) {
            List treeNodeList = levelMap.get(level);
            treeNodeList.stream()
                        .filter(node -> node.getChildren().isEmpty())
                        .forEach(node ->node.addNode(new TreeNode(new ChartItem("", 0, Color.TRANSPARENT, Color.TRANSPARENT), node)));
        }
    }

    private void drawChart() {
        levelMap.clear();
        for (int i = 0 ; i <= maxLevel ; i++) { levelMap.put(i, new ArrayList<>()); }
        root.stream().forEach(node -> levelMap.get(node.getDepth()).add(node));
        boolean         isInteractive      = isInteractive();
        double          ringStepSize       = size * 0.8 / maxLevel;
        double          ringRadiusStep     = ringStepSize * 0.5;
        double          barWidth           = isInteractive ? ringStepSize * 0.5 : ringStepSize * 0.49;
        double          textRadiusStep     = size * 0.4 / maxLevel;
        double          segmentStrokeWidth = ringStepSize * 0.01;
        Color           bkgColor           = getBackgroundColor();
        Color           textColor          = getTextColor();
        TextOrientation textOrientation    = getTextOrientation();
        double          maxTextWidth       = barWidth * 0.9;

        chartCtx.clearRect(0, 0, size, size);
        chartCtx.setFill(isInteractive ? Color.TRANSPARENT : bkgColor);
        chartCtx.fillRect(0, 0, size, size);

        chartCtx.setFont(Fonts.latoRegular(barWidth * 0.2));
        chartCtx.setTextBaseline(VPos.CENTER);
        chartCtx.setTextAlign(TextAlignment.CENTER);
        chartCtx.setLineCap(StrokeLineCap.BUTT);

        segments.clear();

        for (int level = 1 ; level <= maxLevel ; level++) {
            List nodesAtLevel = levelMap.get(level);
            double         xy           = centerX - ringStepSize * level * 0.5;
            double         wh           = ringStepSize * level;
            double         outerRadius  = ringRadiusStep * level + barWidth * 0.5;
            double         innerRadius  = outerRadius - barWidth;

            double segmentStartAngle;
            double segmentEndAngle = 0;
            for (TreeNode node : nodesAtLevel) {
                ChartItem segmentData  = node.getData();
                double    segmentAngle = node.getParentAngle() * node.getPercentage();
                Color     segmentColor = getUseColorFromParent() ? node.getMyRoot().getData().getFillColor() : segmentData.getFillColor();

                segmentStartAngle = 90 + segmentEndAngle;
                segmentEndAngle  -= segmentAngle;

                // Only draw if segment fill color is not TRANSPARENT
                if (!Color.TRANSPARENT.equals(segmentData.getFillColor())) {
                    double value = segmentData.getValue();

                    if (isInteractive) {
                        segments.add(createSegment(-segmentStartAngle, -segmentStartAngle + segmentAngle, innerRadius, outerRadius, segmentColor, bkgColor, node));
                    } else {
                        // Segment Fill
                        chartCtx.setLineWidth(barWidth);
                        chartCtx.setStroke(segmentColor);
                        chartCtx.strokeArc(xy, xy, wh, wh, segmentStartAngle, -segmentAngle, ArcType.OPEN);

                        // Segment Stroke
                        double radStart = Math.toRadians(segmentStartAngle);
                        double cosStart = Math.cos(radStart);
                        double sinStart = Math.sin(radStart);
                        double x1       = centerX + innerRadius * cosStart;
                        double y1       = centerY - innerRadius * sinStart;
                        double x2       = centerX + outerRadius * cosStart;
                        double y2       = centerY - outerRadius * sinStart;

                        chartCtx.setLineWidth(segmentStrokeWidth);
                        chartCtx.setStroke(bkgColor);
                        chartCtx.strokeLine(x1, y1, x2, y2);
                    }

                    // Visible Data
                    if (getVisibleData() != VisibleData.NONE && segmentAngle > textOrientation.getMaxAngle()) {
                        double radText    = Math.toRadians(segmentStartAngle - (segmentAngle * 0.5));
                        double cosText    = Math.cos(radText);
                        double sinText    = Math.sin(radText);
                        double textRadius = textRadiusStep * level;
                        double textX      = centerX + textRadius * cosText;
                        double textY      = centerY - textRadius * sinText;

                        chartCtx.setFill(getUseChartItemTextColor() ? segmentData.getTextColor() : textColor);

                        chartCtx.save();
                        chartCtx.translate(textX, textY);

                        rotateContextForText(chartCtx, segmentStartAngle, -(segmentAngle * 0.5), textOrientation);

                        switch (getVisibleData()) {
                            case VALUE:
                                chartCtx.fillText(String.format(Locale.US, formatString, value), 0, 0, maxTextWidth);
                                break;
                            case NAME:
                                chartCtx.fillText(segmentData.getName(), 0, 0, maxTextWidth);
                                break;
                            case NAME_VALUE:
                                chartCtx.fillText(String.join("", segmentData.getName(), " (", String.format(Locale.US, formatString, value),")"), 0, 0, maxTextWidth);
                                break;
                        }
                        chartCtx.restore();
                    }
                }
            }
        }

        segmentPane.getChildren().setAll(segments);
    }

    private Path createSegment(final double START_ANGLE, final double END_ANGLE, final double INNER_RADIUS, final double OUTER_RADIUS, final Color FILL, final Color STROKE, final TreeNode NODE) {
        double  startAngleRad = Math.toRadians(START_ANGLE + 90);
        double  endAngleRad   = Math.toRadians(END_ANGLE + 90);
        boolean largeAngle    = Math.abs(END_ANGLE - START_ANGLE) > 180.0;

        double x1 = centerX + INNER_RADIUS * Math.sin(startAngleRad);
        double y1 = centerY - INNER_RADIUS * Math.cos(startAngleRad);

        double x2 = centerX + OUTER_RADIUS * Math.sin(startAngleRad);
        double y2 = centerY - OUTER_RADIUS * Math.cos(startAngleRad);

        double x3 = centerX + OUTER_RADIUS * Math.sin(endAngleRad);
        double y3 = centerY - OUTER_RADIUS * Math.cos(endAngleRad);

        double x4 = centerX + INNER_RADIUS * Math.sin(endAngleRad);
        double y4 = centerY - INNER_RADIUS * Math.cos(endAngleRad);

        MoveTo moveTo1 = new MoveTo(x1, y1);
        LineTo lineTo2 = new LineTo(x2, y2);
        ArcTo  arcTo3  = new ArcTo(OUTER_RADIUS, OUTER_RADIUS, 0, x3, y3, largeAngle, true);
        LineTo lineTo4 = new LineTo(x4, y4);
        ArcTo  arcTo1  = new ArcTo(INNER_RADIUS, INNER_RADIUS, 0, x1, y1, largeAngle, false);

        Path path = new Path(moveTo1, lineTo2, arcTo3, lineTo4, arcTo1);

        path.setFill(FILL);
        path.setStroke(STROKE);

        String tooltipText = new StringBuilder(NODE.getData().getName()).append("\n").append(String.format(Locale.US, formatString, NODE.getData().getValue())).toString();
        Tooltip.install(path, new Tooltip(tooltipText));

        path.setOnMousePressed(new WeakEventHandler<>(e -> NODE.getTreeRoot().fireTreeNodeEvent(new TreeNodeEvent(NODE, EventType.NODE_SELECTED))));

        return path;
    }

    private static void rotateContextForText(final GraphicsContext CTX, final double START_ANGLE, final double ANGLE, final TextOrientation ORIENTATION) {
        switch (ORIENTATION) {
            case TANGENT:
                if ((360 - START_ANGLE - ANGLE) % 360 > 90 && (360 - START_ANGLE - ANGLE) % 360 < 270) {
                    CTX.rotate((180 - START_ANGLE - ANGLE) % 360);
                } else {
                    CTX.rotate((360 - START_ANGLE - ANGLE) % 360);
                }
                break;
            case ORTHOGONAL:
                if ((360 - START_ANGLE - ANGLE - 90) % 360 > 90 && (360 - START_ANGLE - ANGLE - 90) % 360 < 270) {
                    CTX.rotate((90 - START_ANGLE - ANGLE) % 360);
                } else {
                    CTX.rotate((270 - START_ANGLE - ANGLE) % 360);
                }
                break;
            case HORIZONTAL:
            default:
                break;
        }
    }


    // ******************** Resizing ******************************************
    private void resize() {
        width  = getWidth() - getInsets().getLeft() - getInsets().getRight();
        height = getHeight() - getInsets().getTop() - getInsets().getBottom();
        size   = width < height ? width : height;

        if (width > 0 && height > 0) {
            pane.setMaxSize(size, size);
            pane.setPrefSize(size, size);
            pane.relocate((getWidth() - size) * 0.5, (getHeight() - size) * 0.5);

            segmentPane.setPrefSize(size, size);

            chartCanvas.setWidth(size);
            chartCanvas.setHeight(size);

            centerX = size * 0.5;
            centerY = centerX;

            redraw();
        }
    }

    private void redraw() {
        pane.setBackground(new Background(new BackgroundFill(backgroundPaint, CornerRadii.EMPTY, Insets.EMPTY)));
        pane.setBorder(new Border(new BorderStroke(borderPaint, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, new BorderWidths(borderWidth / PREFERRED_WIDTH * size))));

        segmentPane.setBackground(new Background(new BackgroundFill(getBackgroundColor(), CornerRadii.EMPTY, Insets.EMPTY)));
        segmentPane.setManaged(isInteractive());
        segmentPane.setVisible(isInteractive());

        drawChart();
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy