be.nbb.demetra.dfm.output.ConfidenceGraph.java Source code

Java tutorial

Introduction

Here is the source code for be.nbb.demetra.dfm.output.ConfidenceGraph.java

Source

/*
 * 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 be.nbb.demetra.dfm.output;

import ec.tstoolkit.timeseries.simplets.TsData;
import ec.tstoolkit.timeseries.simplets.TsPeriod;
import ec.ui.ATsControl;
import ec.ui.chart.TsCharts;
import ec.ui.chart.TsXYDatasets;
import ec.ui.interfaces.IColorSchemeAble;
import ec.ui.interfaces.ITsChart.LinesThickness;
import ec.util.chart.ColorScheme;
import ec.util.chart.ColorScheme.KnownColor;
import ec.util.chart.swing.Charts;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.geom.Ellipse2D;
import java.beans.PropertyChangeEvent;
import java.util.Date;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartMouseEvent;
import org.jfree.chart.ChartMouseListener;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.DateAxis;
import org.jfree.chart.axis.DateTickMarkPosition;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.entity.XYItemEntity;
import org.jfree.chart.plot.DatasetRenderingOrder;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.AbstractRenderer;
import org.jfree.chart.renderer.xy.XYDifferenceRenderer;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.data.xy.XYDataset;

/**
 *
 * @author Mats Maggi
 */
public class ConfidenceGraph extends ATsControl implements IColorSchemeAble {

    private static final String DATA_PROPERTY = "data";
    private static final String CONFIDENCE_PROPERTY = "confidenceVisibility";
    public static final String ORIGINAL_VISIBLE_PROPERTY = "originalVisible";

    private static final KnownColor MAIN_COLOR = KnownColor.RED;
    private static final KnownColor ORIGINAL_DATA_COLOR = KnownColor.GRAY;
    private static final KnownColor CONFIDENCE_COLOR = KnownColor.BLUE;

    private static final int MAIN_INDEX = 501;
    private static final int ORIGINAL_DATA_INDEX = 500;
    public boolean originalVisible = true;

    private final int CONFIDENCE99_INDEX = 0;
    private final int CONFIDENCE95_INDEX = 10;
    private final int CONFIDENCE90_INDEX = 20;
    private final int CONFIDENCE80_INDEX = 30;
    private final int CONFIDENCE70_INDEX = 40;
    private final int CONFIDENCE60_INDEX = 50;

    private final int[] indexes = { CONFIDENCE60_INDEX, CONFIDENCE70_INDEX, CONFIDENCE80_INDEX, CONFIDENCE90_INDEX,
            CONFIDENCE95_INDEX, CONFIDENCE99_INDEX };

    private final int intermediateValues = 5;

    private ConfidenceData data;
    private final ChartPanel chartPanel;
    private static XYItemEntity highlight;

    double max = CONFIDENCE60_INDEX;
    double min = CONFIDENCE99_INDEX;
    private Color highValueColour = Color.BLACK;
    private Color lowValueColour = Color.WHITE;

    // How many RGB steps there are between the high and low colours.
    private int colourValueDistance;

    private final RevealObs revealObs;

    public ConfidenceGraph() {
        chartPanel = new ChartPanel(createMarginViewChart());
        chartPanel.setMouseWheelEnabled(true);
        data = new ConfidenceData(null, null, null);
        revealObs = new RevealObs();

        Charts.avoidScaling(chartPanel);
        Charts.enableFocusOnClick(chartPanel);

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

        addPropertyChangeListener((PropertyChangeEvent evt) -> {
            switch (evt.getPropertyName()) {
            case DATA_PROPERTY:
            case CONFIDENCE_PROPERTY:
                onDataChange();
                break;
            case ORIGINAL_VISIBLE_PROPERTY:
                onVisibilityChange();
                break;
            }
        });

        chartPanel.addChartMouseListener(new HighlightChartMouseListener2());
        chartPanel.addKeyListener(revealObs);

        highlight = null;

        onDataFormatChange();
        onColorSchemeChange();
    }

    private void onVisibilityChange() {
        XYPlot plot = chartPanel.getChart().getXYPlot();
        XYLineAndShapeRenderer original = (XYLineAndShapeRenderer) plot.getRenderer(ORIGINAL_DATA_INDEX);
        original.setSeriesVisible(0, originalVisible);
    }

    public void addPopupMenuItem(JMenuItem item) {
        JPopupMenu menu = chartPanel.getPopupMenu();

        menu.addSeparator();
        menu.add(item);
    }

    public boolean isOriginalVisible() {
        return originalVisible;
    }

    public void setOriginalVisible(boolean original) {
        boolean old = this.originalVisible;
        this.originalVisible = original;
        firePropertyChange(ORIGINAL_VISIBLE_PROPERTY, old, this.originalVisible);
    }

    private void onDataChange() {
        chartPanel.getChart().setNotify(false);

        XYPlot plot = chartPanel.getChart().getXYPlot();

        plot.setDataset(ORIGINAL_DATA_INDEX,
                (data.original == null ? null : TsXYDatasets.from("data", data.original)));

        if (data.series != null && data.stdev != null) {
            plot.setDataset(MAIN_INDEX, TsXYDatasets.from("series", data.series));

            TsData stdev60 = data.stdev.times(0.84); // Confidence of 60%
            TsData stdev70 = data.stdev.times(1.035); // Confidence of 70%
            TsData stdev80 = data.stdev.times(1.28); // Confidence of 80%
            TsData stdev90 = data.stdev.times(1.645); // Confidence of 90%
            TsData stdev95 = data.stdev.times(1.96); // Confidence of 95%
            TsData stdev99 = data.stdev.times(2.575); // Confidence of 99%
            TsData diff6070 = stdev70.minus(stdev60).div(intermediateValues);
            TsData diff7080 = stdev80.minus(stdev70).div(intermediateValues);
            TsData diff8090 = stdev90.minus(stdev80).div(intermediateValues);
            TsData diff9095 = stdev95.minus(stdev90).div(intermediateValues);
            TsData diff9599 = stdev99.minus(stdev95).div(intermediateValues);

            TsData[] stdevs = { stdev60, stdev70, stdev80, stdev90, stdev95 };
            TsData[] diffs = { diff6070, diff7080, diff8090, diff9095, diff9599 };

            for (int i = 0; i < stdevs.length; i++) {
                plot.setDataset(indexes[i], TsXYDatasets.builder().add("lower", data.series.minus(stdevs[i]))
                        .add("upper", data.series.plus(stdevs[i])).build());
                for (int j = 1; j < intermediateValues; j++) {
                    TsData l = data.series.minus(stdevs[i]).minus(diffs[i].times(j));
                    TsData u = data.series.plus(stdevs[i]).plus(diffs[i].times(j));
                    plot.setDataset(indexes[i] - j,
                            TsXYDatasets.builder().add("lower" + j, l).add("upper" + j, u).build());
                }
            }

            plot.setDataset(CONFIDENCE99_INDEX, TsXYDatasets.builder().add("lower99", data.series.minus(stdev99))
                    .add("upper99", data.series.plus(stdev99)).build());

        } else {
            plot.setDataset(MAIN_INDEX, null);

            for (int i = 0; i < indexes.length; i++) {
                plot.setDataset(indexes[i], null);
                if (i != indexes.length - 1) {
                    for (int j = 1; j < intermediateValues; j++) {
                        plot.setDataset(indexes[i] - j, null);
                    }
                }
            }
        }
        onDataFormatChange();

        chartPanel.getChart().setNotify(true);
    }

    private JFreeChart createMarginViewChart() {
        final JFreeChart result = ChartFactory.createXYLineChart("", "", "", Charts.emptyXYDataset(),
                PlotOrientation.VERTICAL, false, false, false);
        result.setPadding(TsCharts.CHART_PADDING);

        XYPlot plot = result.getXYPlot();
        plot.setDatasetRenderingOrder(DatasetRenderingOrder.FORWARD);

        XYLineAndShapeRenderer main = new LineRenderer(MAIN_INDEX);
        plot.setRenderer(MAIN_INDEX, main);

        XYLineAndShapeRenderer original = new LineRenderer(ORIGINAL_DATA_INDEX);
        plot.setRenderer(ORIGINAL_DATA_INDEX, original);

        for (int i = 0; i < indexes.length - 1; i++) {
            plot.setRenderer(indexes[i], getDifferenceRenderer());
            for (int j = 1; j < intermediateValues; j++) {
                plot.setRenderer(indexes[i] - j, getDifferenceRenderer());
            }
        }

        plot.setRenderer(CONFIDENCE99_INDEX, getDifferenceRenderer());

        DateAxis domainAxis = new DateAxis();
        domainAxis.setTickMarkPosition(DateTickMarkPosition.MIDDLE);
        domainAxis.setTickLabelPaint(TsCharts.CHART_TICK_LABEL_COLOR);
        plot.setDomainAxis(domainAxis);

        NumberAxis rangeAxis = new NumberAxis();
        rangeAxis.setAutoRangeIncludesZero(false);
        rangeAxis.setTickLabelPaint(TsCharts.CHART_TICK_LABEL_COLOR);
        plot.setRangeAxis(rangeAxis);

        return result;
    }

    private XYDifferenceRenderer getDifferenceRenderer() {
        XYDifferenceRenderer confidence = new XYDifferenceRenderer();
        confidence.setAutoPopulateSeriesPaint(false);
        confidence.setAutoPopulateSeriesStroke(false);
        confidence.setBaseStroke(TsCharts.getNormalStroke(LinesThickness.Thin));
        return confidence;
    }

    @Override
    protected void onDataFormatChange() {
        DateAxis domainAxis = (DateAxis) chartPanel.getChart().getXYPlot().getDomainAxis();
        try {
            domainAxis.setDateFormatOverride(themeSupport.getDataFormat().newDateFormat());
        } catch (IllegalArgumentException ex) {
            // do nothing?
        }
    }

    @Override
    protected void onColorSchemeChange() {
        highValueColour = themeSupport.getLineColor(CONFIDENCE_COLOR);
        lowValueColour = themeSupport.getPlotColor();
        updateColourDistance();
        XYPlot plot = chartPanel.getChart().getXYPlot();
        plot.setBackgroundPaint(themeSupport.getPlotColor());
        plot.setDomainGridlinePaint(themeSupport.getGridColor());
        plot.setRangeGridlinePaint(themeSupport.getGridColor());
        chartPanel.getChart().setBackgroundPaint(themeSupport.getBackColor());

        XYLineAndShapeRenderer main = (XYLineAndShapeRenderer) plot.getRenderer(MAIN_INDEX);
        main.setBasePaint(themeSupport.getLineColor(MAIN_COLOR));
        XYLineAndShapeRenderer original = (XYLineAndShapeRenderer) plot.getRenderer(ORIGINAL_DATA_INDEX);
        original.setBasePaint(themeSupport.getLineColor(ORIGINAL_DATA_COLOR));

        for (int i = 0; i < indexes.length; i++) {
            XYDifferenceRenderer confidence = ((XYDifferenceRenderer) plot.getRenderer(indexes[i]));
            Color diffArea = getCellColour(indexes[i]);
            confidence.setPositivePaint(diffArea);
            confidence.setNegativePaint(diffArea);
            confidence.setBasePaint(diffArea);
            if (i != indexes.length - 1) {
                for (int j = 1; j < intermediateValues; j++) {
                    confidence = ((XYDifferenceRenderer) plot.getRenderer(indexes[i] - j));
                    diffArea = getCellColour(indexes[i] - ((10 / intermediateValues) * j));
                    confidence.setPositivePaint(diffArea);
                    confidence.setNegativePaint(diffArea);
                    confidence.setBasePaint(diffArea);
                }
            }
        }
    }

    @Override
    public ColorScheme getColorScheme() {
        return themeSupport.getLocalColorScheme();
    }

    @Override
    public void setColorScheme(ColorScheme colorScheme) {
        themeSupport.setLocalColorScheme(colorScheme);
    }

    public void setData(TsData series, TsData stdev, TsData original) {
        this.data = new ConfidenceData(series, stdev, original);
        firePropertyChange(DATA_PROPERTY, null, data);
    }

    private static final class ConfidenceData {

        final TsData series;
        final TsData original;
        final TsData stdev;

        public ConfidenceData(TsData series, TsData stdev, TsData original) {
            this.series = series;
            this.stdev = stdev;
            this.original = original;
        }
    }

    private final class HighlightChartMouseListener2 implements ChartMouseListener {

        @Override
        public void chartMouseClicked(ChartMouseEvent event) {
        }

        @Override
        public void chartMouseMoved(ChartMouseEvent event) {
            if (event.getEntity() instanceof XYItemEntity) {
                XYItemEntity xxx = (XYItemEntity) event.getEntity();
                setHighlightedObs(xxx);
            } else {
                setHighlightedObs(null);
            }
        }
    }

    private void setHighlightedObs(XYItemEntity item) {
        if (item == null || highlight != item) {
            highlight = item;
            chartPanel.getChart().fireChartChanged();
        }
    }

    //==================================================================
    private Color getCellColour(double data) {
        double range = max - min;
        double position = data - min;

        // What proportion of the way through the possible values is that.
        double percentPosition = position / range;

        // Which colour group does that put us in.
        int colourPosition = getColourPosition(percentPosition);

        int r = lowValueColour.getRed();
        int g = lowValueColour.getGreen();
        int b = lowValueColour.getBlue();

        // Make n shifts of the colour, where n is the colourPosition.
        for (int i = 0; i < colourPosition; i++) {
            int rDistance = r - highValueColour.getRed();
            int gDistance = g - highValueColour.getGreen();
            int bDistance = b - highValueColour.getBlue();

            if ((Math.abs(rDistance) >= Math.abs(gDistance)) && (Math.abs(rDistance) >= Math.abs(bDistance))) {
                // Red must be the largest.
                r = changeColourValue(r, rDistance);
            } else if (Math.abs(gDistance) >= Math.abs(bDistance)) {
                // Green must be the largest.
                g = changeColourValue(g, gDistance);
            } else {
                // Blue must be the largest.
                b = changeColourValue(b, bDistance);
            }
        }

        return new Color(r, g, b);
    }

    private int getColourPosition(double percentPosition) {
        return (int) Math.round(colourValueDistance * Math.pow(percentPosition, 1.0));
    }

    private int changeColourValue(int colourValue, int colourDistance) {
        if (colourDistance < 0) {
            return colourValue + 1;
        } else if (colourDistance > 0) {
            return colourValue - 1;
        } else {
            // This shouldn't actually happen here.
            return colourValue;
        }
    }

    private void updateColourDistance() {
        int r1 = lowValueColour.getRed();
        int g1 = lowValueColour.getGreen();
        int b1 = lowValueColour.getBlue();
        int r2 = highValueColour.getRed();
        int g2 = highValueColour.getGreen();
        int b2 = highValueColour.getBlue();

        colourValueDistance = Math.abs(r1 - r2);
        colourValueDistance += Math.abs(g1 - g2);
        colourValueDistance += Math.abs(b1 - b2);
    }

    private static final Shape ITEM_SHAPE = new Ellipse2D.Double(-3, -3, 6, 6);

    private class LineRenderer extends XYLineAndShapeRenderer {

        private final int index;
        private final KnownColor color;

        public LineRenderer(int index) {
            setBaseItemLabelsVisible(true);
            setAutoPopulateSeriesShape(false);
            setAutoPopulateSeriesFillPaint(false);
            setAutoPopulateSeriesOutlineStroke(false);
            setBaseShape(ITEM_SHAPE);
            setUseFillPaint(true);
            this.index = index;
            color = (this.index == MAIN_INDEX ? MAIN_COLOR : ORIGINAL_DATA_COLOR);
        }

        @Override
        public boolean getItemShapeVisible(int series, int item) {
            return revealObs.isEnabled() || isObsHighlighted(series, item);
        }

        private boolean isObsHighlighted(int series, int item) {
            XYPlot plot = (XYPlot) chartPanel.getChart().getPlot();
            if (highlight != null && highlight.getDataset().equals(plot.getDataset(index))) {
                return highlight.getSeriesIndex() == series && highlight.getItem() == item;
            } else {
                return false;
            }
        }

        @Override
        public boolean isItemLabelVisible(int series, int item) {
            return isObsHighlighted(series, item);
        }

        @Override
        public Paint getSeriesPaint(int series) {
            return themeSupport.getLineColor(color);
        }

        @Override
        public Paint getItemPaint(int series, int item) {
            return themeSupport.getLineColor(color);
        }

        @Override
        public Paint getItemFillPaint(int series, int item) {
            return chartPanel.getChart().getPlot().getBackgroundPaint();
        }

        @Override
        public Stroke getSeriesStroke(int series) {
            return TsCharts.getStrongStroke(LinesThickness.Thin);
        }

        @Override
        public Stroke getItemOutlineStroke(int series, int item) {
            return TsCharts.getStrongStroke(LinesThickness.Thin);
        }

        @Override
        protected void drawItemLabel(Graphics2D g2, PlotOrientation orientation, XYDataset dataset, int series,
                int item, double x, double y, boolean negative) {
            String label = generateLabel();
            Font font = chartPanel.getFont();
            Paint paint = chartPanel.getChart().getPlot().getBackgroundPaint();
            Paint fillPaint = themeSupport.getLineColor(color);
            Stroke outlineStroke = AbstractRenderer.DEFAULT_STROKE;
            Charts.drawItemLabelAsTooltip(g2, x, y, 3d, label, font, paint, fillPaint, paint, outlineStroke);
        }

        private String generateLabel() {
            TsPeriod p = new TsPeriod(data.series.getFrequency(),
                    new Date(highlight.getDataset().getX(0, highlight.getItem()).longValue()));
            String label = (index == ORIGINAL_DATA_INDEX ? "Original data\n" : "") + "Period : " + p.toString()
                    + "\nValue : ";
            double value = index == MAIN_INDEX ? data.series.get(p) : data.original.get(p);
            label += themeSupport.getDataFormat().numberFormatter().formatAsString(value);
            return label;
        }
    };

    private final class RevealObs implements KeyListener {

        private boolean enabled = false;

        @Override
        public void keyTyped(KeyEvent e) {
        }

        @Override
        public void keyPressed(KeyEvent e) {
            if (e.getKeyChar() == 'r') {
                setEnabled(true);
            }
        }

        @Override
        public void keyReleased(KeyEvent e) {
            if (e.getKeyChar() == 'r') {
                setEnabled(false);
            }
        }

        private void setEnabled(boolean enabled) {
            if (this.enabled != enabled) {
                this.enabled = enabled;
                firePropertyChange("revealObs", !enabled, enabled);
                chartPanel.getChart().fireChartChanged();
            }
        }

        public boolean isEnabled() {
            return enabled;
        }
    }
}