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

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

The 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.YItem;
import eu.hansolo.fx.charts.font.Fonts;
import eu.hansolo.fx.charts.series.YSeries;
import eu.hansolo.fx.charts.tools.Helper;
import eu.hansolo.fx.charts.tools.Point;
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.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.Paint;
import javafx.scene.paint.RadialGradient;
import javafx.scene.shape.ArcType;
import javafx.scene.shape.StrokeLineCap;
import javafx.scene.text.Font;
import javafx.scene.text.TextAlignment;

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

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


public class YPane extends Region implements ChartArea {
    private static final double                PREFERRED_WIDTH  = 250;
    private static final double                PREFERRED_HEIGHT = 250;
    private static final double                MINIMUM_WIDTH    = 0;
    private static final double                MINIMUM_HEIGHT   = 0;
    private static final double                MAXIMUM_WIDTH    = 4096;
    private static final double                MAXIMUM_HEIGHT   = 4096;
    private static       double                aspectRatio;
    private              boolean               keepAspect;
    private              double                size;
    private              double                width;
    private              double                height;
    private              Paint                 _chartBackground;
    private              ObjectProperty chartBackground;
    private              List>      listOfSeries;
    private              Canvas                canvas;
    private              GraphicsContext       ctx;
    private              double                _thresholdY;
    private              DoubleProperty        thresholdY;
    private              boolean               _thresholdYVisible;
    private              BooleanProperty       thresholdYVisible;
    private              Color                 _thresholdYColor;
    private              ObjectProperty thresholdYColor;
    private              boolean               valid;
    private              double                _lowerBoundY;
    private              DoubleProperty        lowerBoundY;
    private              double                _upperBoundY;
    private              DoubleProperty        upperBoundY;


    // ******************** Constructors **************************************
    public YPane(final YSeries... SERIES) {
        this(Color.TRANSPARENT, SERIES);
    }
    public YPane(final Paint BACKGROUND, final YSeries... SERIES) {
        getStylesheets().add(YPane.class.getResource("chart.css").toExternalForm());
        aspectRatio        = PREFERRED_HEIGHT / PREFERRED_WIDTH;
        keepAspect         = false;
        _chartBackground   = BACKGROUND;
        listOfSeries       = FXCollections.observableArrayList(SERIES);
        _thresholdY        = 100;
        _thresholdYVisible = false;
        _thresholdYColor   = Color.RED;
        _lowerBoundY       = 0;
        _upperBoundY       = 100;
        valid              = isChartTypeValid();
        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);
            }
        }

        getStyleClass().setAll("chart", "xy-chart");

        canvas = new Canvas(PREFERRED_WIDTH, PREFERRED_HEIGHT);
        ctx    = canvas.getGraphicsContext2D();

        getChildren().setAll(canvas);
    }

    private void registerListeners() {
        widthProperty().addListener(o -> resize());
        heightProperty().addListener(o -> resize());

        listOfSeries.forEach(series -> series.setOnSeriesEvent(seriesEvent -> redraw()));
    }


    // ******************** Methods *******************************************
    @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 Paint getChartBackground() { return null == chartBackground ? _chartBackground : chartBackground.get(); }
    public void setChartBackground(final Paint PAINT) {
        if (null == chartBackground) {
            _chartBackground = PAINT;
            redraw();
        } else {
            chartBackground.set(PAINT);
        }
    }
    public ObjectProperty chartBackgroundProperty() {
        if (null == chartBackground) {
            chartBackground = new ObjectPropertyBase(_chartBackground) {
                @Override protected void invalidated() { redraw(); }
                @Override public Object getBean() { return YPane.this; }
                @Override public String getName() { return "chartBackground"; }
            };
            _chartBackground = null;
        }
        return chartBackground;
    }

    public double getThresholdY() { return null == thresholdY ? _thresholdY : thresholdY.get(); }
    public void setThresholdY(final double THRESHOLD) {
        if (null == thresholdY) {
            _thresholdY = THRESHOLD;
            redraw();
        } else {
            thresholdY.set(THRESHOLD);
        }
    }
    public DoubleProperty thresholdYProperty() {
        if (null == thresholdY) {
            thresholdY = new DoublePropertyBase(_thresholdY) {
                @Override protected void invalidated() { redraw(); }
                @Override public Object getBean() { return YPane.this; }
                @Override public String getName() { return "thresholdY"; }
            };
        }
        return thresholdY;
    }

    public boolean isThresholdYVisible() { return null == thresholdYVisible ? _thresholdYVisible : thresholdYVisible.get(); }
    public void setThresholdYVisible(final boolean VISIBLE) {
        if (null == thresholdYVisible) {
            _thresholdYVisible = VISIBLE;
            redraw();
        } else {
            thresholdYVisible.set(VISIBLE);
        }
    }
    public BooleanProperty thresholdYVisibleProperty() {
        if (null == thresholdYVisible) {
            thresholdYVisible = new BooleanPropertyBase(_thresholdYVisible) {
                @Override protected void invalidated() { redraw(); }
                @Override public Object getBean() { return YPane.this; }
                @Override public String getName() { return "thresholdYVisible"; }
            };
        }
        return thresholdYVisible;
    }

    public Color getThresholdYColor() { return null == thresholdYColor ? _thresholdYColor : thresholdYColor.get(); }
    public void setThresholdYColor(final Color COLOR) {
        if (null == thresholdYColor) {
            _thresholdYColor = COLOR;
            redraw();
        } else {
            thresholdYColor.set(COLOR);
        }
    }
    public ObjectProperty thresholdYColorProperty() {
        if (null == thresholdYColor) {
            thresholdYColor = new ObjectPropertyBase(_thresholdYColor) {
                @Override protected void invalidated() { redraw(); }
                @Override public Object getBean() { return YPane.this; }
                @Override public String getName() { return "thresholdYColor"; }
            };
            _thresholdYColor = null;
        }
        return thresholdYColor;
    }

    public double getLowerBoundY() { return null == lowerBoundY ? _lowerBoundY : lowerBoundY.get(); }
    public void setLowerBoundY(final double VALUE) {
        if (null == lowerBoundY) {
            _lowerBoundY = VALUE;
            redraw();
        } else {
            lowerBoundY.set(VALUE);
        }
    }
    public DoubleProperty lowerBoundYProperty() {
        if (null == lowerBoundY) {
            lowerBoundY = new DoublePropertyBase(_lowerBoundY) {
                @Override protected void invalidated() { redraw(); }
                @Override public Object getBean() { return YPane.this; }
                @Override public String getName() { return "lowerBoundY"; }
            };
        }
        return lowerBoundY;
    }

    public double getUpperBoundY() { return null == upperBoundY ? _upperBoundY : upperBoundY.get(); }
    public void setUpperBoundY(final double VALUE) {
        if (null == upperBoundY) {
            _upperBoundY = VALUE;
            redraw();
        } else {
            upperBoundY.set(VALUE);
        }
    }
    public DoubleProperty upperBoundYProperty() {
        if (null == upperBoundY) {
            upperBoundY = new DoublePropertyBase(_upperBoundY) {
                @Override protected void invalidated() { redraw(); }
                @Override public Object getBean() { return YPane.this; }
                @Override public String getName() { return "upperBoundY"; }
            };
        }
        return upperBoundY;
    }

    public double getRangeY() { return getUpperBoundY() - getLowerBoundY(); }

    public double getDataMinY() { return listOfSeries.stream().mapToDouble(YSeries::getMinY).min().getAsDouble(); }
    public double getDataMaxY() { return listOfSeries.stream().mapToDouble(YSeries::getMaxY).max().getAsDouble(); }

    public double getDataRangeY() { return getDataMaxY() - getDataMinY(); }

    public List> getListOfSeries() { return listOfSeries; }

    public boolean containsRadarChart() {
        for(YSeries series : listOfSeries) {
            ChartType type = series.getChartType();
            if (ChartType.RADAR_POLYGON == type ||
                ChartType.RADAR_SECTOR == type  ||
                ChartType.SMOOTH_RADAR_POLYGON == type) { return true; }
        }
        return false;
    }


    // ******************** Draw Chart ****************************************
    private void drawChart() {
        if (null == listOfSeries || listOfSeries.isEmpty()) return;

        ctx.clearRect(0, 0, width, height);
        ctx.setFill(getChartBackground());
        ctx.fillRect(0, 0, width, height);

        if (containsRadarChart()) {
            drawRadarOverlay(listOfSeries.get(0).getItems().size(), listOfSeries.get(0).getChartType());
        }
        for(YSeries series : listOfSeries) {
            final ChartType TYPE = series.getChartType();
            switch(TYPE) {
                case DONUT               : drawDonut(series); break;
                case RADAR_POLYGON       :
                case SMOOTH_RADAR_POLYGON:
                case RADAR_SECTOR        : drawRadar(series); break;
            }
        }
    }

    private void drawDonut(final YSeries SERIES) {
        if (null == SERIES) return;
        List items       = SERIES.getItems();
        int     noOfItems   = items.size();
        double  center      = size * 0.5;
        double  innerRadius = size * 0.275;
        double  outerRadius = size * 0.4;
        double  barWidth    = size * 0.1;
        double  sum         = items.stream().mapToDouble(T::getY).sum();
        double  stepSize    = 360.0 / sum;
        double  angle       = 0;
        double  startAngle  = 90;
        double  xy          = size * 0.1;
        double  wh          = size * 0.8;
        double  x;
        double  y;
        
        ctx.setLineCap(StrokeLineCap.BUTT);
        ctx.setTextAlign(TextAlignment.CENTER);
        ctx.setTextBaseline(VPos.CENTER);

        for (T item : items) {
            double value = item.getY();
            startAngle -= angle;
            angle = value * stepSize;

            // Segment
            ctx.setLineWidth(barWidth);
            ctx.setStroke(item.getFill());
            ctx.strokeArc(xy, xy, wh, wh, startAngle, -angle, ArcType.OPEN);

            // Percentage
            //if (angle > 8) {
            //    x = innerRadius * Math.cos(Math.toRadians(startAngle - (angle * 0.5)));
            //    y = -innerRadius * Math.sin(Math.toRadians(startAngle - (angle * 0.5)));
            //    ctx.setFill(Color.BLACK);
            //    ctx.fillText(String.format(Locale.US, "%.0f%%", (value / sum * 100.0)), center + x, center + y, barWidth);
            //}

            // Value
            if (angle > 8) {
                x = outerRadius * Math.cos(Math.toRadians(startAngle - (angle * 0.5)));
                y = -outerRadius * Math.sin(Math.toRadians(startAngle - (angle * 0.5)));
                ctx.setFill(Color.WHITE);
                ctx.fillText(String.format(Locale.US, "%.0f", value), center + x, center + y, barWidth);
            }
        }
    }

    private void drawRadar(final YSeries SERIES) {
        final double CENTER_X      = 0.5 * size;
        final double CENTER_Y      = CENTER_X;
        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 int    NO_OF_SECTORS = SERIES.getItems().size();
        final double angleStep     = 360.0 / NO_OF_SECTORS;

        // draw the chart data
        ctx.save();
        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 {
            ctx.setFill(SERIES.getFill());
        }
        ctx.setLineWidth(SERIES.getStrokeWidth() > -1 ? SERIES.getStrokeWidth() : size * 0.0025);
        ctx.setStroke(SERIES.getStroke());

        switch(SERIES.getChartType()) {
            case RADAR_POLYGON:
                ctx.save();
                ctx.beginPath();
                ctx.moveTo(CENTER_X, 0.36239 * size);
                SERIES.getItems().forEach(item -> {
                    double r1 = (item.getY() - LOWER_BOUND_Y) / DATA_RANGE;
                    ctx.lineTo(CENTER_X, CENTER_Y - OFFSET - r1 * RANGE);
                    Helper.rotateCtx(ctx, CENTER_X, CENTER_Y, angleStep);
                });
                double r2 = ((SERIES.getItems().get(NO_OF_SECTORS - 1).getY() - LOWER_BOUND_Y) / DATA_RANGE);
                ctx.lineTo(CENTER_X, CENTER_Y - OFFSET - r2 * RANGE);
                ctx.closePath();
                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)));
                points.add(new Point(x, y));

                for (T item : SERIES.getItems()) {
                    double r1 = (CENTER_Y - (CENTER_Y - OFFSET - ((item.getY() - LOWER_BOUND_Y) / DATA_RANGE) * 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 = (CENTER_Y - (CENTER_Y - OFFSET - ((SERIES.getItems().get(NO_OF_SECTORS - 1).getY() - LOWER_BOUND_Y) / DATA_RANGE) * RANGE));
                x = CENTER_X + (-Math.sin(radAngle) * r3);
                y = CENTER_Y + (+Math.cos(radAngle) * r3);
                points.add(new Point(x, y));

                Point[] interpolatedPoints = 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++) {
                    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:
                Helper.rotateCtx(ctx, CENTER_X, CENTER_Y, -90);
                SERIES.getItems().forEach(item -> {
                    double r1 = (item.getY() - LOWER_BOUND_Y) / DATA_RANGE;
                    ctx.beginPath();
                    ctx.moveTo(CENTER_X, CENTER_Y);
                    ctx.arc(CENTER_X, CENTER_Y, r1 * RANGE + OFFSET, r1 * RANGE + OFFSET, 0, -angleStep);
                    ctx.closePath();
                    ctx.fill();
                    ctx.stroke();

                    Helper.rotateCtx(ctx, CENTER_X, CENTER_Y, angleStep);
                });
                break;
        }
        ctx.restore();
    }

    private void drawRadarOverlay(final int NO_OF_SECTORS, final ChartType TYPE) {
        final double CENTER_X    = 0.5 * size;
        final double CENTER_Y    = CENTER_X;
        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 angleStep   = 360.0 / NO_OF_SECTORS;

        // draw concentric rings
        ctx.setLineWidth(1);
        ctx.setStroke(Color.GRAY);
        double ringStepSize = size / 20.0;
        double pos          = 0.5 * (size - CIRCLE_SIZE);
        double ringSize     = CIRCLE_SIZE;
        for (int i = 0 ; i < 11 ; i++) {
            ctx.strokeOval(pos, pos, ringSize, ringSize);
            pos      += ringStepSize;
            ringSize -= 2 * ringStepSize;
        }

        // draw star lines
        ctx.save();
        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);
        }
        ctx.restore();

        // draw threshold line
        if (isThresholdYVisible()) {
            double r = ((getThresholdY() - MIN_VALUE) / DATA_RANGE);
            ctx.setLineWidth(clamp(1d, 3d, size * 0.005));
            ctx.setStroke(getThresholdYColor());
            ctx.strokeOval(0.5 * size - OFFSET - r * RANGE, 0.5 * size - OFFSET - r * RANGE,
                                  2 * (r * RANGE + OFFSET), 2 * (r * RANGE + OFFSET));
        }

        // prerotate if sectormode
        ctx.save();

        if (ChartType.RADAR_SECTOR == TYPE) {
            Helper.rotateCtx(ctx, CENTER_X, CENTER_Y, angleStep * 0.5);
        }

        // draw text
        ctx.save();
        ctx.setFont(Fonts.latoRegular(0.04 * size));
        ctx.setTextAlign(TextAlignment.CENTER);
        ctx.setTextBaseline(VPos.CENTER);
        ctx.setFill(Color.BLACK);
        for (int i = 0 ; i < NO_OF_SECTORS ; i++) {
            //ctx.fillText(data.get(i).ID, CENTER_X, size * 0.02);
            Helper.rotateCtx(ctx, CENTER_X, CENTER_Y, angleStep);
        }
        ctx.restore();

        ctx.restore();

        // draw min and max Text
        Font   font         = Fonts.latoRegular(0.025 * size);
        String minValueText = String.format(Locale.US, "%.0f", getLowerBoundY());
        String maxValueText = String.format(Locale.US, "%.0f", getUpperBoundY());
        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);
        ctx.restore();
    }

    private boolean isChartTypeValid() {
        boolean containsDonut              = false;
        boolean containsRadarSector        = false;
        boolean containsRadarPolygon       = false;
        boolean containsSmoothRadarPolygon = false;
        for(YSeries series : getListOfSeries()) {
            final ChartType TYPE = series.getChartType();
            containsDonut              = ChartType.DONUT                == TYPE && !containsDonut;
            containsRadarSector        = ChartType.RADAR_SECTOR         == TYPE && !containsRadarSector;
            containsRadarPolygon       = ChartType.RADAR_POLYGON        == TYPE && !containsRadarPolygon;
            containsSmoothRadarPolygon = ChartType.SMOOTH_RADAR_POLYGON == TYPE && !containsSmoothRadarPolygon;
        }
        boolean valid = false;
        if (containsDonut && !containsRadarSector && !containsRadarPolygon && !containsSmoothRadarPolygon) {
            valid = true;
        } else if (containsRadarSector && !containsDonut && !containsRadarPolygon && !containsSmoothRadarPolygon) {
            valid = true;
        } else if (containsRadarPolygon | containsSmoothRadarPolygon && !containsDonut && !containsRadarSector) {
            valid = true;
        }
        return valid;
    }


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

        if (keepAspect) {
            if (aspectRatio * width > height) {
                width = 1 / (aspectRatio / height);
            } else if (1 / (aspectRatio / height) > width) {
                height = aspectRatio * width;
            }
        }

        if (width > 0 && height > 0) {
            canvas.setWidth(size);
            canvas.setHeight(size);
            //canvas.relocate((getWidth() - width) * 0.5, (getHeight() - height) * 0.5);
            canvas.relocate((width - size) * 0.5, (height - size) * 0.5);
            
            redraw();
        }
    }

    protected void redraw() {
        drawChart();
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy