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

jdplus.toolkit.desktop.plugin.ui.JDistributionView Maven / Gradle / Ivy

/*
 * Copyright 2013 National Bank of Belgium
 *
 * Licensed under the EUPL, Version 1.1 or – as soon they will be approved 
 * by the European Commission - subsequent versions of the EUPL (the "Licence");
 * You may not use this work except in compliance with the Licence.
 * You may obtain a copy of the Licence at:
 *
 * http://ec.europa.eu/idabc/eupl
 *
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the Licence is distributed on an "AS IS" basis,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the Licence for the specific language governing permissions and 
 * limitations under the Licence.
 */
package jdplus.toolkit.desktop.plugin.ui;

import jdplus.toolkit.base.api.data.DoubleSeq;
import jdplus.toolkit.desktop.plugin.components.parts.HasColorScheme;
import jdplus.toolkit.desktop.plugin.jfreechart.TsCharts;
import jdplus.toolkit.desktop.plugin.components.parts.HasChart.LinesThickness;
import jdplus.toolkit.desktop.plugin.components.TimeSeriesComponent;
import jdplus.toolkit.desktop.plugin.components.parts.HasColorSchemeResolver;
import jdplus.toolkit.desktop.plugin.components.parts.HasColorSchemeSupport;
import jdplus.main.desktop.design.SwingComponent;
import jdplus.main.desktop.design.SwingProperty;
import jdplus.toolkit.desktop.plugin.interfaces.DoubleSeqView;
import ec.util.chart.ColorScheme.KnownColor;
import ec.util.chart.swing.ChartCommand;
import ec.util.chart.swing.Charts;
import ec.util.chart.swing.SwingColorSchemeSupport;
import jdplus.toolkit.desktop.plugin.jfreechart.MatrixChartCommand;
import jdplus.toolkit.base.api.dstats.BoundaryType;
import jdplus.toolkit.base.api.dstats.ContinuousDistribution;
import jdplus.toolkit.base.api.util.Arrays2;
import java.awt.BorderLayout;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Locale;
import javax.swing.JComponent;
import javax.swing.JMenu;
import javax.swing.JPopupMenu;

import jdplus.toolkit.base.core.stats.DescriptiveStatistics;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.NumberTickUnit;
import org.jfree.chart.block.BlockBorder;
import org.jfree.chart.labels.XYSeriesLabelGenerator;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYBarRenderer;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.chart.renderer.xy.XYSplineRenderer;
import org.jfree.data.function.Function2D;
import org.jfree.data.general.DatasetUtilities;
import org.jfree.data.xy.XYBarDataset;
import org.jfree.data.xy.XYDataset;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;

/**
 *
 * @author Demortier & BAYENSK
 */
@SwingComponent
public final class JDistributionView extends JComponent implements TimeSeriesComponent, DoubleSeqView, HasColorScheme {

    // CONSTANTS
    private static final int HISTOGRAM_INDEX = 1;
    private static final int DISTRIBUTION_INDEX = 0;
    private static final KnownColor HISTOGRAM_COLOR = KnownColor.BLUE;
    private static final KnownColor DISTRIBUTION_COLOR = KnownColor.RED;

    // PROPERTIES DEFINITION
    @SwingProperty
    public static final String L_BOUND_PROPERTY = "lBound";

    @SwingProperty
    public static final String R_BOUND_PROPERTY = "rBound";

    @SwingProperty
    public static final String DISTRIBUTION_PROPERTY = "distribution";

    @SwingProperty
    public static final String ADJUST_DISTRIBUTION_PROPERTY = "adjustDistribution";

    @SwingProperty
    public static final String H_COUNT_PROPERTY = "hCount";

    @SwingProperty
    public static final String DATA_PROPERTY = "data";

    // DEFAULT PROPERTIES
    private static final double DEFAULT_L_BOUND = 0;
    private static final double DEFAULT_R_BOUND = 0;
    private static final ContinuousDistribution DEFAULT_DISTRIBUTION = null;
    private static final boolean DEFAULT_ADJUST_DISTRIBUTION = true;
    private static final int DEFAULT_H_COUNT = 0;
    private static final double[] DEFAULT_DATA = null;

    // PROPERTIES
    private double lBound;
    private double rBound;
    private ContinuousDistribution distribution;
    private boolean adjustDistribution;
    private int hCount;
    private double[] data;

    // OTHER
    private final ChartPanel chartPanel;

    @lombok.experimental.Delegate
    private final HasColorScheme colorScheme = HasColorSchemeSupport.of(this::firePropertyChange);

    private final HasColorSchemeResolver colorSchemeResolver = new HasColorSchemeResolver(colorScheme, this::onColorSchemeChange);

    public JDistributionView() {
        this.lBound = DEFAULT_L_BOUND;
        this.rBound = DEFAULT_R_BOUND;
        this.distribution = DEFAULT_DISTRIBUTION;
        this.adjustDistribution = DEFAULT_ADJUST_DISTRIBUTION;
        this.hCount = DEFAULT_H_COUNT;
        this.data = DEFAULT_DATA;

        this.chartPanel = Charts.newChartPanel(createDistributionViewChart());

        onColorSchemeChange();
        onComponentPopupMenuChange();
        enableProperties();

        setLayout(new BorderLayout());
        add(chartPanel, BorderLayout.CENTER);
    }

    private void enableProperties() {
        addPropertyChangeListener(evt -> {
            switch (evt.getPropertyName()) {
                case L_BOUND_PROPERTY:
                    onDataChange();
                    break;
                case R_BOUND_PROPERTY:
                    onDataChange();
                    break;
                case DISTRIBUTION_PROPERTY:
                    onDataChange();
                    break;
                case ADJUST_DISTRIBUTION_PROPERTY:
                    onDataChange();
                    break;
                case H_COUNT_PROPERTY:
                    onDataChange();
                    break;
                case DATA_PROPERTY:
                    onDataChange();
                    break;
                case "componentPopupMenu":
                    onComponentPopupMenuChange();
                    break;
            }
        });
    }

    //
    private void onColorSchemeChange() {
        SwingColorSchemeSupport themeSupport = colorSchemeResolver.resolve();

        XYPlot plot = chartPanel.getChart().getXYPlot();
        plot.setBackgroundPaint(themeSupport.getPlotColor());
        plot.setDomainGridlinePaint(themeSupport.getGridColor());
        plot.setRangeGridlinePaint(themeSupport.getGridColor());
        chartPanel.getChart().setBackgroundPaint(themeSupport.getBackColor());

        plot.getRenderer(HISTOGRAM_INDEX).setBasePaint(themeSupport.getAreaColor(HISTOGRAM_COLOR));
        plot.getRenderer(HISTOGRAM_INDEX).setBaseOutlinePaint(themeSupport.getLineColor(HISTOGRAM_COLOR));
        plot.getRenderer(DISTRIBUTION_INDEX).setBasePaint(themeSupport.getLineColor(DISTRIBUTION_COLOR));
    }

    protected void onDataChange() {
        XYPlot plot = chartPanel.getChart().getXYPlot();

        if (data != DEFAULT_DATA) {
            DescriptiveStatistics stats = DescriptiveStatistics.ofInternal(data);

            double m = 0, M = 0, dv = 1;
            if (adjustDistribution && distribution != DEFAULT_DISTRIBUTION) {
                m = stats.getAverage();
                M = distribution.getExpectation();
                double v = stats.getVar();
                double V = distribution.getVariance();
                dv = Math.sqrt(v / V);
            }

            final double xmin = stats.getMin() < lBound ? stats.getMin() : lBound;
            final double xmax = stats.getMax() > rBound ? stats.getMax() : rBound;
            final int n = hCount != 0 ? hCount : (int) Math.ceil(Math.sqrt(stats.getObservationsCount()));
            final double xstep = (xmax - xmin) / n;

            // distribution >
            if (distribution != DEFAULT_DISTRIBUTION) {
                Function2D density = distribution::getDensity;

                final double zmin = distribution.hasLeftBound() != BoundaryType.None ? distribution.getLeftBound() : ((xmin - xstep - m) / dv + M);
                final double zmax = distribution.hasRightBound() != BoundaryType.None ? distribution.getRightBound() : ((xmax + xstep - m) / dv + M);

                // TODO: create IDistribution#getName() method
                String name = distribution.getClass().getSimpleName();

                ((XYLineAndShapeRenderer) plot.getRenderer(DISTRIBUTION_INDEX)).setLegendItemToolTipGenerator(getTooltipGenerator(distribution));
                plot.setDataset(DISTRIBUTION_INDEX, DatasetUtilities.sampleFunction2D(density, zmin, zmax, n, name));
            } else {
                plot.setDataset(DISTRIBUTION_INDEX, Charts.emptyXYDataset());
            }
            // < distribution

            // histogram >
            XYSeries hSeries = new XYSeries("");
            double nobs = stats.getObservationsCount();
            for (int i = 0; i <= n; ++i) {
                double x0 = xmin + i * xstep;
                double x1 = x0 + xstep;
                double y = stats.countBetween(x0, x1) / (nobs * xstep / dv);
                hSeries.add(((x0 + x1) / 2 - m) / dv + M, y);
            }

            plot.setDataset(HISTOGRAM_INDEX, new XYBarDataset(new XYSeriesCollection(hSeries), xstep / dv + M));
            // < histogram
        } else {
            plot.setDataset(HISTOGRAM_INDEX, Charts.emptyXYDataset());
            plot.setDataset(DISTRIBUTION_INDEX, Charts.emptyXYDataset());
        }

        onColorSchemeChange();
    }

    private void onComponentPopupMenuChange() {
        JPopupMenu popupMenu = getComponentPopupMenu();
        chartPanel.setComponentPopupMenu(popupMenu != null ? popupMenu : buildMenu(chartPanel).getPopupMenu());
    }
    //

    //
    public double getLBound() {
        return lBound;
    }

    public void setLBound(double lBound) {
        double old = this.lBound;
        this.lBound = lBound;
        firePropertyChange(L_BOUND_PROPERTY, old, this.lBound);
    }

    public double getRBound() {
        return rBound;
    }

    public void setRBound(double rBound) {
        double old = this.rBound;
        this.rBound = rBound;
        firePropertyChange(R_BOUND_PROPERTY, old, this.rBound);
    }

    public ContinuousDistribution getDistribution() {
        return distribution;
    }

    public void setDistribution(ContinuousDistribution distribution) {
        ContinuousDistribution old = this.distribution;
        this.distribution = distribution != null ? distribution : DEFAULT_DISTRIBUTION;
        firePropertyChange(DISTRIBUTION_PROPERTY, old, this.distribution);
    }

    public double[] getData() {
        return data;
    }

    public void setData(double[] data) {
        double[] old = this.data;
        this.data = data != null ? data : DEFAULT_DATA;
        firePropertyChange(DATA_PROPERTY, old, this.data);
    }

    public boolean isAdjustDistribution() {
        return adjustDistribution;
    }

    public void setAdjustDistribution(boolean adjustDistribution) {
        boolean old = this.adjustDistribution;
        this.adjustDistribution = adjustDistribution;
        firePropertyChange(ADJUST_DISTRIBUTION_PROPERTY, old, this.adjustDistribution);
    }

    public int getHCount() {
        return hCount;
    }

    public void setHCount(int hCount) {
        int old = this.hCount;
        this.hCount = hCount >= 0 ? hCount : DEFAULT_H_COUNT;
        firePropertyChange(H_COUNT_PROPERTY, old, this.hCount);
    }
    //

    @Override
    public void set(DoubleSeq data) {
        setData(data.toArray());
    }

    @Override
    public void reset() {
        setData(null);
        setDistribution(null);
    }

    private static JFreeChart createDistributionViewChart() {
        XYPlot plot = new XYPlot();

        XYLineAndShapeRenderer dRenderer = new XYSplineRenderer();
        dRenderer.setBaseShapesVisible(false);
        dRenderer.setAutoPopulateSeriesPaint(false);
        dRenderer.setAutoPopulateSeriesStroke(false);
        dRenderer.setBaseStroke(TsCharts.getStrongStroke(LinesThickness.Thin));
        dRenderer.setDrawSeriesLineAsPath(true); // not sure if useful
        plot.setDataset(DISTRIBUTION_INDEX, Charts.emptyXYDataset());
        plot.setRenderer(DISTRIBUTION_INDEX, dRenderer);

        XYBarRenderer hRenderer = new XYBarRenderer();
        hRenderer.setShadowVisible(false);
        hRenderer.setDrawBarOutline(true);
        hRenderer.setAutoPopulateSeriesPaint(false);
        hRenderer.setAutoPopulateSeriesOutlinePaint(false);
        hRenderer.setBaseSeriesVisibleInLegend(false);
        plot.setDataset(HISTOGRAM_INDEX, Charts.emptyXYDataset());
        plot.setRenderer(HISTOGRAM_INDEX, hRenderer);

        NumberAxis domainAxis = new NumberAxis();
        domainAxis.setTickLabelPaint(TsCharts.CHART_TICK_LABEL_COLOR);
        plot.setDomainAxis(domainAxis);
        plot.setDomainGridlinesVisible(false);

        NumberAxis rangeAxis = new NumberAxis();
        rangeAxis.setTickLabelPaint(TsCharts.CHART_TICK_LABEL_COLOR);
        rangeAxis.setTickUnit(new NumberTickUnit(0.05));
        rangeAxis.setNumberFormatOverride(new DecimalFormat("0.###", DecimalFormatSymbols.getInstance(Locale.getDefault(Locale.Category.FORMAT))));
        plot.setRangeAxis(rangeAxis);

        plot.mapDatasetToDomainAxis(0, 0);
        plot.mapDatasetToRangeAxis(0, 0);
        plot.mapDatasetToDomainAxis(1, 0);
        plot.mapDatasetToRangeAxis(1, 0);

        JFreeChart result = new JFreeChart("", JFreeChart.DEFAULT_TITLE_FONT, plot, true);
        result.setPadding(TsCharts.CHART_PADDING);
        result.getTitle().setFont(TsCharts.CHART_TITLE_FONT);
        result.getLegend().setFrame(BlockBorder.NONE);
        result.getLegend().setBackgroundPaint(null);
        return result;
    }

    protected static XYSeriesLabelGenerator getTooltipGenerator(final ContinuousDistribution distribution) {
        return (XYDataset dataset, int series) -> distribution.getDescription();
    }

    private static JMenu buildMenu(ChartPanel chartPanel) {
        JMenu result = new JMenu();

        result.add(MatrixChartCommand.copySeries(DISTRIBUTION_INDEX, 0).toAction(chartPanel)).setText("Copy distribution");
        result.add(MatrixChartCommand.copySeries(HISTOGRAM_INDEX, 0).toAction(chartPanel)).setText("Copy histogram");

        JMenu export = new JMenu("Export image to");
        export.add(ChartCommand.printImage().toAction(chartPanel)).setText("Printer...");
        export.add(ChartCommand.copyImage().toAction(chartPanel)).setText("Clipboard");
        export.add(ChartCommand.saveImage().toAction(chartPanel)).setText("File...");
        result.add(export);

        return result;
    }

    @Override
    protected void firePropertyChange(String propertyName, Object oldValue, Object newValue) {
        if (!Arrays2.arrayEquals(oldValue, newValue)) {
            super.firePropertyChange(propertyName, oldValue, newValue);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy