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

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

/*
 * SPDX-License-Identifier: Apache-2.0
 *
 * Copyright 2016-2021 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
 *
 *     https://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.data.DayOfWeekCategory;
import eu.hansolo.fx.charts.data.MonthCategory;
import eu.hansolo.fx.charts.event.SeriesEventListener;
import eu.hansolo.toolboxfx.font.Fonts;
import eu.hansolo.fx.charts.series.ChartItemSeries;
import eu.hansolo.fx.charts.series.Series;
import eu.hansolo.fx.charts.tools.Helper;
import eu.hansolo.fx.charts.tools.InfoPopup;
import eu.hansolo.fx.geometry.Rectangle;
import javafx.beans.DefaultProperty;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.BooleanPropertyBase;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ObjectPropertyBase;
import javafx.beans.property.StringProperty;
import javafx.beans.property.StringPropertyBase;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.text.TextAlignment;

import java.awt.image.BufferedImage;
import java.time.format.TextStyle;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;


/**
 * User: hansolo
 * Date: 14.11.21
 * Time: 13:33
 */
@DefaultProperty("children")
public class PanelBarChart extends Region {
    private static final double                                         PREFERRED_WIDTH  = 600;
    private static final double                                         PREFERRED_HEIGHT = 400;
    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              double                                         width;
    private              double                                         height;
    private              Canvas                                         canvas;
    private              GraphicsContext                                ctx;
    private              Pane                                           pane;
    private              ObservableList>     listOfSeries;
    private              ObservableList>     comparisonListOfSeries;
    private              SeriesEventListener                            seriesEvtListener;
    private              ObservableList             categories;
    private              Locale                                         _locale;
    private              ObjectProperty                         locale;
    private              Paint                                          _chartBackground;
    private              ObjectProperty                          chartBackground;
    private              Color                                          _categoryNameColor;
    private              ObjectProperty                          categoryNameColor;
    private              String                                         _name;
    private              StringProperty                                 name;
    private              Color                                          _nameColor;
    private              ObjectProperty                          nameColor;
    private              Color                                          _seriesNameColor;
    private              ObjectProperty                          seriesNameColor;
    private              Color                                          _categorySumColor;
    private              ObjectProperty                          categorySumColor;
    private              Color                                          _seriesSumColor;
    private              ObjectProperty                          seriesSumColor;
    private              boolean                                        _comparisonEnabled;
    private              BooleanProperty                                comparisonEnabled;
    private              String                                         _comparisonName;
    private              StringProperty                                 comparisonName;
    private              Color                                          _comparisonNameColor;
    private              ObjectProperty                          comparisonNameColor;
    private              Color                                          _comparisonSeriesNameColor;
    private              ObjectProperty                          comparisonSeriesNameColor;
    private              Color                                          _comparisonCategorySumColor;
    private              ObjectProperty                          comparisonCategorySumColor;
    private              Color                                          _comparisonSeriesSumColor;
    private              ObjectProperty                          comparisonSeriesSumColor;
    private              Color                                          _gridColor;
    private              ObjectProperty                          gridColor;
    private              boolean                                        _colorByCategory;
    private              BooleanProperty                                colorByCategory;
    private              Map                          sumsPerCategory;
    private              Map                          comparisonSumsPerCategory;
    private              Map                      itemMap;
    private              Map                      comparisonItemMap;
    private              EventHandler                       mouseHandler;
    private              ListChangeListener                   categoryListener;
    private              ListChangeListener> seriesListener;
    private              InfoPopup                                      popup;


    // ******************** Constructors **************************************
    public PanelBarChart(final List categories) {
        this(categories, new ArrayList<>());
    }
    public PanelBarChart(final List categories, final ChartItemSeries... series) {
        this(categories, Arrays.asList(series));
    }
    public PanelBarChart(final List categories, final List> series) {
        this.listOfSeries                = FXCollections.observableArrayList(series);
        this.comparisonListOfSeries      = FXCollections.observableArrayList();
        this.seriesEvtListener           = evt -> redraw();
        this.categories                  = FXCollections.observableArrayList(categories);
        this._locale                     = Locale.getDefault();
        this._chartBackground            = Color.TRANSPARENT;
        this._categoryNameColor          = Color.BLACK;
        this._seriesNameColor            = Color.BLACK;
        this._categorySumColor           = Color.BLACK;
        this._seriesSumColor             = Color.BLACK;
        this._comparisonSeriesNameColor  = Color.BLACK;
        this._comparisonCategorySumColor = Color.BLACK;
        this._comparisonSeriesSumColor   = Color.BLACK;
        this._gridColor                  = Color.LIGHTGRAY;
        this._colorByCategory            = false;
        this._comparisonEnabled          = false;
        this._name                       = "";
        this._nameColor                  = Color.BLACK;
        this._comparisonName             = "";
        this._comparisonNameColor        = Color.BLACK;
        this.sumsPerCategory             = new ConcurrentHashMap<>();
        this.comparisonSumsPerCategory   = new ConcurrentHashMap<>();
        this.itemMap                     = new ConcurrentHashMap<>();
        this.comparisonItemMap           = new ConcurrentHashMap<>();
        this.mouseHandler                = e -> handleMouseEvent(e);
        this.categoryListener            = c -> redraw();
        this.seriesListener              = c -> {
            while (c.next()) {
                if (c.wasAdded()) {
                    c.getAddedSubList().forEach(series1 -> series1.addSeriesEventListener(seriesEvtListener));
                } else if (c.wasRemoved()) {
                    c.getRemoved().forEach(series1 -> series1.removeSeriesEventListener(seriesEvtListener));
                }
            }
            recalc();
            redraw();
        };
        this.popup                       = new InfoPopup();
        recalc();
        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);
            }
        }

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

        pane = new Pane(canvas);

        getChildren().setAll(pane);
    }

    private void registerListeners() {
        widthProperty().addListener(o -> resize());
        heightProperty().addListener(o -> resize());
        categories.addListener(categoryListener);
        listOfSeries.addListener(seriesListener);
        comparisonListOfSeries.addListener(seriesListener);
        canvas.addEventHandler(MouseEvent.MOUSE_PRESSED, mouseHandler);
    }


    // ******************** 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 ObservableList getCategories() { return categories; }

    public ObservableList> getListOfSeries() { return listOfSeries; }
    public void setListOfSeries(final ChartItemSeries... arrayOfSeries) {
        setListOfSeries(Arrays.asList(arrayOfSeries));
    }
    public void setListOfSeries(final List> listOfSeries) { this.listOfSeries.setAll(listOfSeries); }

    public void addSeries(final ChartItemSeries series) {
        if (listOfSeries.contains(series)) { return; }
        listOfSeries.add(series);
    }

    public void removeSeries(final Series series) {
        if (!listOfSeries.contains(series)) { return; }
        listOfSeries.remove(series);
    }

    public ObservableList> getComparisonListOfSeries() { return comparisonListOfSeries; }
    public void setComparisonListOfSeries(final ChartItemSeries... arrayOfSeries) { setComparisonListOfSeries(Arrays.asList(arrayOfSeries)); }
    public void setComparisonListOfSeries(final List> listOfOfSeries) { this.comparisonListOfSeries.setAll(listOfOfSeries); }

    public void addComparisonSeries(final ChartItemSeries series) {
        if (comparisonListOfSeries.contains(series)) { return; }
        comparisonListOfSeries.add(series);
    }

    public void removeComparisonSeries(final ChartItemSeries series) {
        if (comparisonListOfSeries.contains(series)) {
            comparisonListOfSeries.remove(series);
        }
    }

    public Locale getLocale() { return null == locale ? _locale : locale.get(); }
    public void setLocale(final Locale locale) {
        if (null == this.locale) {
            _locale = locale;
            redraw();
        } else {
            this.locale.set(locale);
        }
    }
    public ObjectProperty localeProperty() {
        if (null == locale) {
            locale = new ObjectPropertyBase<>(_locale) {
                @Override protected void invalidated() { redraw(); }
                @Override public Object getBean() { return PanelBarChart.this; }
                @Override public String getName() { return "locale"; }
            };
            _locale = null;
        }
        return locale;
    }

    public Paint getChartBackground() { return null == chartBackground ? _chartBackground : chartBackground.get(); }
    public void setChartBackground(final Paint chartBackground) {
        if (null == this.chartBackground) {
            _chartBackground = chartBackground;
            redraw();
        } else {
            this.chartBackground.set(chartBackground);
        }
    }
    public ObjectProperty chartBackgroundProperty() {
        if (null == chartBackground) {
            chartBackground = new ObjectPropertyBase<>(_chartBackground) {
                @Override protected void invalidated() { redraw(); }
                @Override public Object getBean() { return PanelBarChart.this; }
                @Override public String getName() { return "chartBackground"; }
            };
            _chartBackground = null;
        }
        return chartBackground;
    }

    public String getName() { return null == name ? _name : name.get(); }
    public void setName(final String name) {
        if (null == this.name) {
            _name = name;
            redraw();
        } else {
            this.name.set(name);
        }
    }
    public StringProperty nameProperty() {
        if (null == name) {
            name = new StringPropertyBase(_name) {
                @Override protected void invalidated() { redraw(); }
                @Override public Object getBean() { return PanelBarChart.this; }
                @Override public String getName() { return "name"; }
            };
            _name = null;
        }
        return name;
    }

    public Color getNameColor() { return null == nameColor ? _nameColor : nameColor.get(); }
    public void setNameColor(final Color color) {
        if (null == this.nameColor) {
            _nameColor = color;
            redraw();
        } else {
            this.nameColor.set(color);
        }
    }
    public ObjectProperty nameColorProperty() {
        if (null == nameColor) {
            nameColor = new ObjectPropertyBase<>(_nameColor) {
                @Override protected void invalidated() { redraw(); }
                @Override public Object getBean() { return PanelBarChart.this; }
                @Override public String getName() { return "nameColor"; }
            };
            _nameColor = null;
        }
        return nameColor;
    }

    public Color getCategoryNameColor() { return null == categoryNameColor ? _categoryNameColor : categoryNameColor.get(); }
    public void setCategoryNameColor(final Color color) {
        if (null == this.categoryNameColor) {
            _categoryNameColor = color;
            redraw();
        } else {
            this.categoryNameColor.set(color);
        }
    }
    public ObjectProperty categoryNameColorProperty() {
        if (null == categoryNameColor) {
            categoryNameColor = new ObjectPropertyBase<>(_categoryNameColor) {
                @Override protected void invalidated() { redraw(); }
                @Override public Object getBean() { return PanelBarChart.this; }
                @Override public String getName() { return "categoryNameColor"; }
            };
            _categoryNameColor = null;
        }
        return categoryNameColor;
    }

    public Color getSeriesNameColor() { return null == seriesNameColor ? _seriesNameColor : seriesNameColor.get(); }
    public void setSeriesNameColor(final Color color) {
        if (null == this.seriesNameColor) {
            _seriesNameColor = color;
            redraw();
        } else {
            this.seriesNameColor.set(color);
        }
    }
    public ObjectProperty seriesNameColorProperty() {
        if (null == seriesNameColor) {
            seriesNameColor = new ObjectPropertyBase<>(_seriesNameColor) {
                @Override protected void invalidated() { redraw(); }
                @Override public Object getBean() { return PanelBarChart.this; }
                @Override public String getName() { return "seriesNameColor"; }
            };
            _seriesNameColor = null;
        }
        return seriesNameColor;
    }

    public Color getCategorySumColor() { return null == categorySumColor ? _categorySumColor : categorySumColor.get(); }
    public void setCategorySumColor(final Color color) {
        if (null == categorySumColor) {
            _categorySumColor = color;
            redraw();
        } else {
            categorySumColor.set(color);
        }
    }
    public ObjectProperty categorySumColorProperty() {
        if (null == categorySumColor) {
            categorySumColor = new ObjectPropertyBase<>(_categorySumColor) {
                @Override protected void invalidated() { redraw(); }
                @Override public Object getBean() { return PanelBarChart.this; }
                @Override public String getName() { return "categorySumColor"; }
            };
            _categorySumColor = null;
        }
        return categorySumColor;
    }

    public Color getSeriesSumColor() { return null == seriesSumColor ? _seriesSumColor : seriesSumColor.get(); }
    public void setSeriesSumColor(final Color color) {
        if (null == seriesSumColor) {
            _seriesSumColor = color;
            redraw();
        } else {
            seriesSumColor.set(color);
        }
    }
    public ObjectProperty seriesSumColorProperty() {
        if (null == seriesSumColor) {
            seriesSumColor = new ObjectPropertyBase<>(_seriesSumColor) {
                @Override protected void invalidated() { redraw(); }
                @Override public Object getBean() { return PanelBarChart.this; }
                @Override public String getName() { return "seriesSumColor"; }
            };
            _seriesSumColor = null;
        }
        return seriesSumColor;
    }

    public Color getGridColor() { return null == gridColor ? _gridColor : gridColor.get(); }
    public void setGridColor(final Color color) {
        if (null == gridColor) {
            _gridColor = color;
            redraw();
        } else {
            gridColor.set(color);
        }
    }
    public ObjectProperty gridColorProperty() {
        if (null == gridColor) {
            gridColor = new ObjectPropertyBase<>() {
                @Override protected void invalidated() { redraw(); }
                @Override public Object getBean() { return PanelBarChart.this; }
                @Override public String getName() { return "gridColor"; }
            };
            _gridColor = null;
        }
        return gridColor;
    }

    public boolean getColorByCategory() { return null == colorByCategory ? _colorByCategory : colorByCategory.get(); }
    public void setColorByCategory(final boolean colorByCategory) {
        if (null == this.colorByCategory) {
            _colorByCategory = colorByCategory;
            redraw();
        } else {
            this.colorByCategory.set(colorByCategory);
        }
    }
    public BooleanProperty colorByCategoryProperty() {
        if (null == colorByCategory) {
            colorByCategory = new BooleanPropertyBase(_colorByCategory) {
                @Override protected void invalidated() { redraw(); }
                @Override public Object getBean() { return PanelBarChart.this; }
                @Override public String getName() { return "colorByCategory"; }
            };
        }
        return colorByCategory;
    }

    public boolean getComparisonEnabled() { return null == comparisonEnabled ? _comparisonEnabled : comparisonEnabled.get(); }
    public void setComparisonEnabled(final boolean comparisonEnabled) {
        if (null == this.comparisonEnabled) {
            _comparisonEnabled = comparisonEnabled;
            redraw();
        } else {
            this.comparisonEnabled.set(comparisonEnabled);
        }
    }
    public BooleanProperty comparisonEnabledProperty() {
        if (null == comparisonEnabled) {
            comparisonEnabled = new BooleanPropertyBase(_comparisonEnabled) {
                @Override protected void invalidated() { redraw(); }
                @Override public Object getBean() { return PanelBarChart.this; }
                @Override public String getName() { return "comparisonEnabled"; }
            };
        }
        return comparisonEnabled;
    }

    public String getComparisonName() { return null == comparisonName ? _comparisonName : comparisonName.get(); }
    public void setComparisonName(final String name) {
        if (null == this.comparisonName) {
            _comparisonName = name;
            redraw();
        } else {
            comparisonName.set(name);
        }
    }
    public StringProperty comparisonNameProperty() {
        if (null == comparisonName) {
            comparisonName = new StringPropertyBase(_comparisonName) {
                @Override protected void invalidated() { redraw(); }
                @Override public Object getBean() { return PanelBarChart.this; }
                @Override public String getName() { return "comparisonName"; }
            };
            _comparisonName = null;
        }
        return comparisonName;
    }

    public Color getComparisonNameColor() { return null == comparisonNameColor ? _comparisonNameColor : comparisonNameColor.get(); }
    public void setComparisonNameColor(final Color color) {
        if (null == this.comparisonNameColor) {
            _comparisonNameColor = color;
            redraw();
        } else {
            this.comparisonNameColor.set(color);
        }
    }
    public ObjectProperty comparisonNameColorProperty() {
        if (null == comparisonNameColor) {
            comparisonNameColor = new ObjectPropertyBase<>(_comparisonNameColor) {
                @Override protected void invalidated() { redraw(); }
                @Override public Object getBean() { return PanelBarChart.this; }
                @Override public String getName() { return "comparisonNameColor"; }
            };
            _comparisonNameColor = null;
        }
        return comparisonNameColor;
    }

    public Color getComparisonCategorySumColor() { return null == comparisonCategorySumColor ? _comparisonCategorySumColor : comparisonCategorySumColor.get(); }
    public void setComparisonCategorySumColor(final Color color) {
        if (null == comparisonCategorySumColor) {
            _comparisonCategorySumColor = color;
            redraw();
        } else {
            comparisonCategorySumColor.set(color);
        }
    }
    public ObjectProperty comparisonCategorySumColorProperty() {
        if (null == comparisonCategorySumColor) {
            comparisonCategorySumColor = new ObjectPropertyBase<>(_comparisonCategorySumColor) {
                @Override protected void invalidated() { redraw(); }
                @Override public Object getBean() { return PanelBarChart.this; }
                @Override public String getName() { return "comparisonCategorySumColor"; }
            };
            _comparisonCategorySumColor = null;
        }
        return comparisonCategorySumColor;
    }

    public Color getComparisonSeriesNameColor() { return null == comparisonSeriesNameColor ? _comparisonSeriesNameColor : comparisonSeriesNameColor.get(); }
    public void setComparisonSeriesNameColor(final Color color) {
        if (null == this.comparisonSeriesNameColor) {
            _comparisonSeriesNameColor = color;
            redraw();
        } else {
            this.comparisonSeriesNameColor.set(color);
        }
    }
    public ObjectProperty comparisonSeriesNameColorProperty() {
        if (null == comparisonSeriesNameColor) {
            comparisonSeriesNameColor = new ObjectPropertyBase<>(_comparisonSeriesNameColor) {
                @Override protected void invalidated() { redraw(); }
                @Override public Object getBean() { return PanelBarChart.this; }
                @Override public String getName() { return "comparisonSeriesNameColor"; }
            };
            _comparisonSeriesNameColor = null;
        }
        return comparisonSeriesNameColor;
    }

    public Color getComparisonSeriesSumColor() { return null == comparisonSeriesSumColor ? _comparisonSeriesSumColor : comparisonSeriesSumColor.get(); }
    public void setComparisonSeriesSumColor(final Color color) {
        if (null == comparisonSeriesSumColor) {
            _comparisonSeriesSumColor = color;
            redraw();
        } else {
            comparisonSeriesSumColor.set(color);
        }
    }
    public ObjectProperty comparisonSeriesSumColorProperty() {
        if (null == comparisonSeriesSumColor) {
            comparisonSeriesSumColor = new ObjectPropertyBase<>(_comparisonSeriesSumColor) {
                @Override protected void invalidated() { redraw(); }
                @Override public Object getBean() { return PanelBarChart.this; }
                @Override public String getName() { return "comparisonSeriesSumColor"; }
            };
            _comparisonSeriesSumColor = null;
        }
        return comparisonSeriesSumColor;
    }

    /**
     * Calling this method will render this chart/plot to a png given of the given width and height
     * @param filename The path and name of the file  /Users/hansolo/Desktop/plot.png
     * @param width The width of the final image in pixels (if < 0 then 400 and if > 4096 then 4096)
     * @param height The height of the final image in pixels (if < 0 then 400 and if > 4096 then 4096)
     * @return True if the procedure was successful, otherwise false
     */
    public boolean renderToImage(final String filename, final int width, final int height) {
        return Helper.renderToImage(PanelBarChart.this, width, height, filename);
    }

    /**
     * Calling this method will render this chart/plot to a png given of the given width and height
     * @param width The width of the final image in pixels (if < 0 then 400 and if > 4096 then 4096)
     * @param height The height of the final image in pixels (if < 0 then 400 and if > 4096 then 4096)
     * @return A BufferedImage of this chart in the given dimension
     */
    public BufferedImage renderToImage(final int width, final int height) {
        return Helper.renderToImage(PanelBarChart.this, width, height);
    }

    public void removeAllData() {
        listOfSeries.clear();
        comparisonListOfSeries.clear();
        redraw();
    }

    public void dispose() {
        canvas.removeEventHandler(MouseEvent.MOUSE_PRESSED, mouseHandler);
        categories.removeListener(categoryListener);
        listOfSeries.forEach(series -> series.removeSeriesEventListener(seriesEvtListener));
        listOfSeries.removeListener(seriesListener);
        comparisonListOfSeries.forEach(series -> series.removeSeriesEventListener(seriesEvtListener));
        comparisonListOfSeries.removeListener(seriesListener);
    }

    private void recalc() {
        sumsPerCategory.clear();
        comparisonSumsPerCategory.clear();
        // Calculate sums per category
        for (Category category : categories) {
            listOfSeries.stream()
                        .mapToDouble(series -> series.getItems().parallelStream().filter(item -> item.getCategory().getName().equals(category.getName())).map(ChartItem::getValue).reduce(0.0, Double::sum))
                        .forEach(seriesSum -> {
                            double sum = sumsPerCategory.getOrDefault(category, 0.0);
                            sumsPerCategory.put(category, sum + seriesSum);
                        });
            comparisonListOfSeries.stream()
                                  .mapToDouble(comparisonSeries -> comparisonSeries.getItems().parallelStream().filter(item -> item.getCategory().getName().equals(category.getName())).map(ChartItem::getValue).reduce(0.0, Double::sum))
                                  .forEach(comparisonSeriesSum -> {
                                      double comparisonSum = comparisonSumsPerCategory.getOrDefault(category, 0.0);
                                      comparisonSumsPerCategory.put(category, comparisonSum + comparisonSeriesSum);
                                  });
        }
    }

    private void handleMouseEvent(final MouseEvent evt) {
        final EventType type = evt.getEventType();
        final double                     x    = evt.getX();
        final double                     y    = evt.getY();

        if (type.equals(MouseEvent.MOUSE_PRESSED)) {
            Optional> optionalItem = itemMap.entrySet().parallelStream().filter(entry -> entry.getKey().contains(x, y)).findFirst();
            if (optionalItem.isPresent()) {
                popup.setX(evt.getScreenX());
                popup.setY(evt.getScreenY() - popup.getHeight());
                if (optionalItem.get().getValue().getDescription().isEmpty()) {
                    popup.update(optionalItem.get().getValue());
                } else {
                    popup.update(optionalItem.get().getValue(), true);
                }
                popup.animatedShow(getScene().getWindow());
            } else {
                Optional> optionalComparisonItem = comparisonItemMap.entrySet().parallelStream().filter(entry -> entry.getKey().contains(x, y)).findFirst();
                if (optionalComparisonItem.isPresent()) {
                    popup.setX(evt.getScreenX());
                    popup.setY(evt.getScreenY() - popup.getHeight());
                    if (optionalComparisonItem.get().getValue().getDescription().isEmpty()) {
                        popup.update(optionalComparisonItem.get().getValue());
                    } else {
                        popup.update(optionalComparisonItem.get().getValue(), true);
                    }
                    popup.animatedShow(getScene().getWindow());
                }
            }
        }
    }


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

    private void resize() {
        width  = getWidth() - getInsets().getLeft() - getInsets().getRight();
        height = getHeight() - getInsets().getTop() - getInsets().getBottom();

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

            canvas.setWidth(width);
            canvas.setHeight(height);

            redraw();
        }
    }

    private void redraw() {
        ctx.clearRect(0, 0, width, height);
        ctx.setFill(getChartBackground());
        ctx.fillRect(0, 0, width, height);

        ctx.setTextAlign(TextAlignment.LEFT);
        ctx.setTextBaseline(VPos.CENTER);
        if (!listOfSeries.isEmpty()) {
            if (getComparisonEnabled() && !comparisonListOfSeries.isEmpty() && listOfSeries.size() == comparisonListOfSeries.size()) {
                // Draw items of both series
                final int     noOfSeries             = listOfSeries.size();
                final int     noOfCategories         = categories.size();
                final double  nameColumnWidth        = width * 0.125;
                final double  sumColumnWidth         = width * 0.06125;
                final double  spaceBetweenCategories = width * 0.00625;
                final double  spaceBetweenSeries     = height * 0.0075;
                final double  cellWidth              = (width - ((noOfCategories - 1) * spaceBetweenCategories) - nameColumnWidth - sumColumnWidth) / (noOfCategories);
                final double  cellHeight             = (height - ((noOfSeries + 1) * spaceBetweenSeries)) / (noOfSeries + 2);
                final double  maxCellValue           = Math.max(listOfSeries.parallelStream().max(Comparator.comparingDouble(ChartItemSeries::getMaxValue)).get().getMaxValue(), comparisonListOfSeries.parallelStream().max(Comparator.comparingDouble(ChartItemSeries::getMaxValue)).get().getMaxValue());
                final double  scaleFactorX           = cellWidth / maxCellValue;
                final double  itemHeight             = cellHeight * 0.4;
                final double  itemOffsetY            = cellHeight * 0.1;
                final double  spaceBetweenItems      = cellHeight * 0.05;
                final boolean useCategoryColor       = getColorByCategory();
                final double  completeSum            = sumsPerCategory.values().stream().reduce(0.0, (a, b) -> a + b);
                final double  comparisonCompleteSum  = comparisonSumsPerCategory.values().stream().reduce(0.0, (a, b) -> a + b);

                // Draw name and comparisonName
                ctx.setFont(Fonts.opensansSemibold(cellHeight * 0.25));
                ctx.setFill(getNameColor());
                ctx.fillText(getName(), 0, cellHeight * 0.25, cellWidth);
                ctx.setFill(getComparisonNameColor());
                ctx.fillText(getComparisonName(), 0, cellHeight * 0.75, cellWidth * 0.95);

                // Draw complete sum
                ctx.setTextAlign(TextAlignment.RIGHT);
                ctx.setFill(getCategorySumColor());
                ctx.fillText(Helper.shortenNumber((long) completeSum), width, cellHeight * 0.25, sumColumnWidth);
                ctx.setFill(getComparisonCategorySumColor());
                ctx.fillText(Helper.shortenNumber((long) comparisonCompleteSum), width, cellHeight * 0.75, sumColumnWidth * 0.95);

                // Draw series names and their sums
                for (int y = 0; y < noOfSeries; y++) {
                    final ChartItemSeries series      = listOfSeries.get(y);
                    final String                     seriesName  = series.getName();
                    final String                     sumOfSeries = Helper.shortenNumber((long) series.getSumOfAllItems());
                    final double                     posY        = y * (cellHeight + spaceBetweenSeries) + cellHeight + cellHeight * 0.1;
                    ctx.setFont(Fonts.opensansRegular(cellHeight * 0.5));
                    ctx.setTextAlign(TextAlignment.LEFT);
                    ctx.setFill(getSeriesNameColor());
                    ctx.fillText(seriesName, 0, posY + (cellHeight * 0.5), nameColumnWidth * 0.95);

                    ctx.setFont(Fonts.opensansRegular(cellHeight * 0.25));
                    ctx.setTextAlign(TextAlignment.RIGHT);
                    ctx.setFill(getSeriesSumColor());
                    ctx.fillText(sumOfSeries, width, posY + (itemHeight * 0.5), sumColumnWidth * 0.95);

                    final ChartItemSeries comparisonSeries      = comparisonListOfSeries.get(y);
                    final String                     comparisonSumOfSeries = Helper.shortenNumber((long) comparisonSeries.getSumOfAllItems());
                    final double                     comparisonPosY        = y * (cellHeight + spaceBetweenSeries) + cellHeight + itemHeight + spaceBetweenItems + cellHeight * 0.1;
                    ctx.setFill(getComparisonSeriesSumColor());
                    ctx.fillText(comparisonSumOfSeries, width, comparisonPosY + (itemHeight * 0.5), sumColumnWidth * 0.95);
                }

                // Draw category names and their sums
                ctx.setTextAlign(TextAlignment.LEFT);
                final Locale locale = getLocale();
                for (int x = 0; x < noOfCategories; x++) {
                    ctx.setFont(Fonts.opensansRegular(cellHeight * 0.25));
                    final double   posX          = nameColumnWidth + x * (cellWidth + spaceBetweenCategories);
                    final Category category      = categories.get(x);
                    final double   sum           = sumsPerCategory.get(category);
                    final double   comparisonSum = comparisonSumsPerCategory.get(category);
                    final String   categoryName;
                    if (category instanceof MonthCategory) {
                        MonthCategory monthCategory = (MonthCategory) category;
                        categoryName = monthCategory.getName(TextStyle.SHORT, locale);
                    } else if (category instanceof DayOfWeekCategory) {
                        DayOfWeekCategory dayOfWeekCategory = (DayOfWeekCategory) category;
                        categoryName = dayOfWeekCategory.getName(TextStyle.SHORT, locale);
                    } else {
                        categoryName = category.getName();
                    }
                    ctx.setFill(getCategorySumColor());
                    ctx.fillText(Helper.shortenNumber((long) sum), posX, cellHeight * 0.25, cellWidth * 0.95);
                    ctx.setFill(getComparisonCategorySumColor());
                    ctx.fillText(Helper.shortenNumber((long) comparisonSum), posX, cellHeight * 0.75, cellWidth * 0.95);
                    ctx.setFont(Fonts.opensansRegular(cellHeight * 0.5));
                    ctx.setFill(getCategoryNameColor());
                    ctx.fillText(categoryName, posX, height - cellHeight * 0.5, cellWidth * 0.95);
                }

                // Draw items
                ctx.setFont(Fonts.opensansRegular(cellHeight * 0.25));
                itemMap.clear();
                ctx.setStroke(getGridColor());
                for (int y = 0; y < noOfSeries; y++) {
                    final ChartItemSeries series = listOfSeries.get(y);
                    final double                     posY   = y * (cellHeight + spaceBetweenSeries) + cellHeight;
                    for (int x = 0; x < noOfCategories; x++) {
                        final double posX = nameColumnWidth + x * (cellWidth + spaceBetweenCategories);
                        ctx.strokeLine(posX, cellHeight, posX, ((noOfSeries) * (cellHeight + spaceBetweenSeries) + cellHeight));
                        final Category            category     = categories.get(x);
                        final String              categoryName = category.getName();
                        final Optional chartItem    = series.getItems().stream().filter(item -> item.getCategory().getName().equals(categoryName)).findFirst();
                        if (chartItem.isPresent()) {
                            final ChartItem item      = chartItem.get();
                            final double    itemWidth = item.getValue() * scaleFactorX;
                            ctx.setFill(useCategoryColor ? category.getFill() : item.getFill());
                            ctx.fillRect(posX, posY + itemOffsetY, itemWidth, itemHeight);
                            itemMap.put(new Rectangle(posX, posY + itemOffsetY, itemWidth, itemHeight), item);
                        }
                    }

                    final ChartItemSeries comparisonSeries = comparisonListOfSeries.get(y);
                    final double                     comparisonPosY   = y * (cellHeight + spaceBetweenSeries) + cellHeight + itemHeight + spaceBetweenItems;
                    for (int x = 0; x < noOfCategories; x++) {
                        final double posX = nameColumnWidth + x * (cellWidth + spaceBetweenCategories);
                        ctx.strokeLine(posX, cellHeight, posX, ((noOfSeries) * (cellHeight + spaceBetweenSeries) + cellHeight));
                        final Category            category     = categories.get(x);
                        final String              categoryName = category.getName();
                        final Optional chartItem    = comparisonSeries.getItems().stream().filter(item -> item.getCategory().getName().equals(categoryName)).findFirst();
                        if (chartItem.isPresent()) {
                            final ChartItem item      = chartItem.get();
                            final double    itemWidth = item.getValue() * scaleFactorX;
                            ctx.setFill(useCategoryColor ? category.getFill() : item.getFill());
                            ctx.fillRect(posX, comparisonPosY + itemOffsetY, itemWidth, itemHeight);
                            itemMap.put(new Rectangle(posX, comparisonPosY + itemOffsetY, itemWidth, itemHeight), item);
                        }
                    }
                }
            } else {
                // Draw items of standard series
                final int     noOfSeries             = listOfSeries.size();
                final int     noOfCategories         = categories.size();
                final double  nameColumnWidth        = width * 0.125;
                final double  sumColumnWidth         = width * 0.06125;
                final double  spaceBetweenCategories = width * 0.00625;
                final double  spaceBetweenSeries     = height * 0.0075;
                final double  cellWidth              = (width - ((noOfCategories - 1) * spaceBetweenCategories) - nameColumnWidth - sumColumnWidth) / (noOfCategories);
                final double  cellHeight             = (height - ((noOfSeries + 1) * spaceBetweenSeries)) / (noOfSeries + 2);
                final double  maxCellValue           = listOfSeries.parallelStream().max(Comparator.comparingDouble(ChartItemSeries::getMaxValue)).get().getMaxValue();
                final double  scaleFactorX           = cellWidth / maxCellValue;
                final double  itemHeight             = cellHeight * 0.8;
                final double  itemOffsetY            = cellHeight * 0.1;
                final boolean useCategoryColor       = getColorByCategory();
                final double  completeSum            = sumsPerCategory.values().stream().reduce(0.0, (a,b) -> a + b);

                // Draw name
                ctx.setFont(Fonts.opensansSemibold(cellHeight * 0.5));
                ctx.setFill(getNameColor());
                ctx.fillText(getName(), 0, cellHeight * 0.5, cellWidth * 0.95);

                // Draw complete sum
                ctx.setTextAlign(TextAlignment.RIGHT);
                ctx.setFill(getCategoryNameColor());
                ctx.fillText(Helper.shortenNumber((long) completeSum), width, cellHeight * 0.5, sumColumnWidth * 0.95);

                ctx.setFont(Fonts.opensansRegular(cellHeight * 0.5));

                // Draw series names and their sums
                for (int y = 0; y < noOfSeries; y++) {
                    final ChartItemSeries series      = listOfSeries.get(y);
                    final String                     seriesName  = series.getName();
                    final String                     sumOfSeries = Helper.shortenNumber((long) series.getSumOfAllItems());
                    final double                     posY        = y * (cellHeight + spaceBetweenSeries) + cellHeight;
                    ctx.setTextAlign(TextAlignment.LEFT);
                    ctx.setFill(getSeriesNameColor());
                    ctx.fillText(seriesName, 0, posY + (cellHeight * 0.5), nameColumnWidth * 0.95);
                    ctx.setTextAlign(TextAlignment.RIGHT);
                    ctx.setFill(getSeriesSumColor());
                    ctx.fillText(sumOfSeries, width, posY + (cellHeight * 0.5), sumColumnWidth * 0.95);
                }

                // Draw category names and their sums
                ctx.setTextAlign(TextAlignment.LEFT);
                final Locale locale = getLocale();
                for (int x = 0; x < noOfCategories; x++) {
                    final double   posX     = nameColumnWidth + x * (cellWidth + spaceBetweenCategories);
                    final Category category = categories.get(x);
                    final double   sum      = sumsPerCategory.get(category);
                    final String   categoryName;
                    if (category instanceof MonthCategory) {
                        MonthCategory monthCategory = (MonthCategory) category;
                        categoryName = monthCategory.getName(TextStyle.SHORT, locale);
                    } else if (category instanceof DayOfWeekCategory) {
                        DayOfWeekCategory dayOfWeekCategory = (DayOfWeekCategory) category;
                        categoryName = dayOfWeekCategory.getName(TextStyle.SHORT, locale);
                    } else {
                        categoryName = category.getName();
                    }
                    ctx.setFill(getCategorySumColor());
                    ctx.fillText(Helper.shortenNumber((long) sum), posX, cellHeight * 0.5, cellWidth * 0.95);
                    ctx.setFill(getCategoryNameColor());
                    ctx.fillText(categoryName, posX, height - cellHeight * 0.5, cellWidth * 0.95);
                }

                // Draw items
                itemMap.clear();
                ctx.setStroke(getGridColor());
                for (int y = 0; y < noOfSeries; y++) {
                    final ChartItemSeries series = listOfSeries.get(y);
                    final double                     posY   = y * (cellHeight + spaceBetweenSeries) + cellHeight;
                    for (int x = 0; x < noOfCategories; x++) {
                        final double posX = nameColumnWidth + x * (cellWidth + spaceBetweenCategories);
                        ctx.strokeLine(posX, cellHeight, posX, ((noOfSeries) * (cellHeight + spaceBetweenSeries) + cellHeight));
                        final Category            category     = categories.get(x);
                        final String              categoryName = category.getName();
                        final Optional chartItem    = series.getItems().stream().filter(item -> item.getCategory().getName().equals(categoryName)).findFirst();
                        if (chartItem.isPresent()) {
                            final ChartItem item      = chartItem.get();
                            final double    itemWidth = item.getValue() * scaleFactorX;
                            ctx.setFill(useCategoryColor ? category.getFill() : item.getFill());
                            ctx.fillRect(posX, posY + itemOffsetY, itemWidth, itemHeight);
                            itemMap.put(new Rectangle(posX, posY + itemOffsetY, itemWidth, itemHeight), item);
                        }
                    }
                }
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy