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

jdplus.toolkit.desktop.plugin.ui.JMarginView 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.timeseries.TsCollection;
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.parts.HasObsFormat;
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.toolkit.desktop.plugin.components.parts.HasObsFormatResolver;
import jdplus.toolkit.desktop.plugin.datatransfer.DataTransferManager;
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.components.parts.HasObsFormatSupport;
import jdplus.main.desktop.design.SwingComponent;
import jdplus.main.desktop.design.SwingProperty;
import jdplus.toolkit.desktop.plugin.util.DateFormatAdapter;
import jdplus.toolkit.base.api.timeseries.Ts;
import jdplus.toolkit.base.api.timeseries.TsData;
import jdplus.toolkit.base.api.timeseries.TsDomain;
import jdplus.toolkit.base.api.timeseries.TsPeriod;
import jdplus.toolkit.base.api.timeseries.TsUnit;
import jdplus.toolkit.base.api.timeseries.calendars.CalendarUtility;
import jdplus.toolkit.base.api.util.Arrays2;
import java.awt.BasicStroke;
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.Toolkit;
import java.awt.datatransfer.Transferable;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.geom.Ellipse2D;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Collection;
import java.util.Date;
import java.util.stream.Stream;
import javax.swing.AbstractAction;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JComponent;
import javax.swing.JMenu;
import javax.swing.JPopupMenu;
import jdplus.toolkit.base.core.stats.DescriptiveStatistics;
import nbbrd.design.SkipProcessing;
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.IntervalMarker;
import org.jfree.chart.plot.Marker;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.ValueMarker;
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;
import org.jfree.ui.Layer;

/**
 *
 * @author Kristof Bayens
 */
@SwingComponent
public final class JMarginView extends JComponent implements TimeSeriesComponent, HasColorScheme, HasObsFormat {

    // PROPERTIES
    @SkipProcessing(target = SwingProperty.class, reason = "to be refactored")
    @SwingProperty
    private static final String DATA_PROPERTY = "data";

    @SkipProcessing(target = SwingProperty.class, reason = "to be refactored")
    @SwingProperty
    private static final String PRECISION_MARKERS_VISIBLE_PROPERTY = "precisionMarkersVisible";

    // CONSTANTS
    private static final int MAIN_INDEX = 1;
    private static final int DIFFERENCE_INDEX = 0;
    private static final Stroke DATE_MARKER_STROKE = new BasicStroke(1.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 1.0f, new float[]{6.0f, 6.0f}, 0.0f);
    private static final float DATE_MARKER_ALPHA = 0.8f;
    private static final KnownColor MAIN_COLOR = KnownColor.RED;
    private static final KnownColor DIFFERENCE_COLOR = KnownColor.BLUE;
    private static final KnownColor DATE_MARKER_COLOR = KnownColor.ORANGE;

    // OTHER
    private final ChartPanel chartPanel;
    private MarginData data;
    private boolean precisionMarkersVisible;

    private final RevealObs revealObs;
    private static XYItemEntity highlight;

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

    @lombok.experimental.Delegate
    private final HasObsFormat obsFormat = HasObsFormatSupport.of(this::firePropertyChange);

    private final HasObsFormatResolver obsFormatResolver;
    private final HasColorSchemeResolver colorSchemeResolver;

    public JMarginView() {
        this.chartPanel = Charts.newChartPanel(createMarginViewChart());
        this.data = new MarginData(null, null, null, false, null);
        this.precisionMarkersVisible = false;
        this.revealObs = new RevealObs();
        this.obsFormatResolver = new HasObsFormatResolver(obsFormat, this::onDataFormatChange);
        this.colorSchemeResolver = new HasColorSchemeResolver(colorScheme, this::onColorSchemeChange);

        registerActions();

        Charts.enableFocusOnClick(chartPanel);

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

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

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

    private void registerActions() {
        HasObsFormatSupport.registerActions(this, getActionMap());
    }

    private void enableProperties() {
        addPropertyChangeListener(evt -> {
            switch (evt.getPropertyName()) {
                case COLOR_SCHEME_PROPERTY:
                    onColorSchemeChange();
                    break;
                case OBS_FORMAT_PROPERTY:
                    onDataFormatChange();
                    break;
                case DATA_PROPERTY:
                    onDataChange();
                    break;
                case PRECISION_MARKERS_VISIBLE_PROPERTY:
                    onPrecisionMarkersVisible();
                    break;
                case "componentPopupMenu":
                    onComponentPopupMenuChange();
                    break;
            }
        });
    }

    //
    private void onDataFormatChange() {
        DateAxis domainAxis = (DateAxis) chartPanel.getChart().getXYPlot().getDomainAxis();
        domainAxis.setDateFormatOverride(new DateFormatAdapter(obsFormatResolver.resolve()));
    }

    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());

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

        XYDifferenceRenderer difference = ((XYDifferenceRenderer) plot.getRenderer(DIFFERENCE_INDEX));
        Color diffArea = SwingColorSchemeSupport.withAlpha(themeSupport.getAreaColor(DIFFERENCE_COLOR), 150);
        difference.setPositivePaint(diffArea);
        difference.setNegativePaint(diffArea);
        difference.setBasePaint(themeSupport.getLineColor(DIFFERENCE_COLOR));

        Collection markers = (Collection) plot.getDomainMarkers(Layer.FOREGROUND);
        if (markers != null && !markers.isEmpty()) {
            Color markerColor = themeSupport.getLineColor(DATE_MARKER_COLOR);
            for (Marker o : markers) {
                o.setPaint(markerColor);
            }
        }

        Collection intervalMarkers = (Collection) plot.getDomainMarkers(Layer.BACKGROUND);
        if (intervalMarkers != null && !intervalMarkers.isEmpty()) {
            Color markerColor = themeSupport.getLineColor(KnownColor.ORANGE);
            for (Marker o : intervalMarkers) {
                o.setPaint(markerColor);
            }
        }
    }

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

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

        plot.setDataset(MAIN_INDEX, TsXYDatasets.from("series", data.series));
        plot.setDataset(DIFFERENCE_INDEX, TsXYDatasets.builder().add("lower", data.lower).add("upper", data.upper).build());

        onPrecisionMarkersVisible();
        onDataFormatChange();

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

    private void onPrecisionMarkersVisible() {
        XYPlot plot = chartPanel.getChart().getXYPlot();
        plot.clearDomainMarkers();
        addDateMarkers();
        if (precisionMarkersVisible) {
            addPrecisionMarkers();
        }
        onColorSchemeChange();
    }

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

    //
    public void setData(TsData series, TsData lower, TsData upper, LocalDateTime... markers) {
        setData(series, lower, upper, false, markers);
    }

    public void setData(TsData series, TsData lower, TsData upper, boolean multiplicative, LocalDateTime... markers) {
        this.data = new MarginData(series, lower, upper, multiplicative, markers);
        firePropertyChange(DATA_PROPERTY, null, data);
    }

    private boolean isPrecisionMarkersVisible() {
        return precisionMarkersVisible;
    }

    private void setPrecisionMarkersVisible(boolean precisionMarkersVisible) {
        boolean old = this.precisionMarkersVisible;
        this.precisionMarkersVisible = precisionMarkersVisible;
        firePropertyChange(PRECISION_MARKERS_VISIBLE_PROPERTY, old, this.precisionMarkersVisible);
    }
    //

    private void addDateMarkers() {
        XYPlot plot = chartPanel.getChart().getXYPlot();
        if (data.markers != null) {
            for (LocalDateTime o : data.markers) {
                ValueMarker marker = new ValueMarker(1000 * o.toEpochSecond(ZoneOffset.UTC));
//                ValueMarker marker = new ValueMarker(new Day(o.getDayOfMonth(), o.getMonthValue(), o.getYear()).getFirstMillisecond());
                marker.setStroke(DATE_MARKER_STROKE);
                marker.setAlpha(DATE_MARKER_ALPHA);
                plot.addDomainMarker(marker, Layer.FOREGROUND);
            }
        }
    }

    private void addPrecisionMarkers() {
        TsData tmp = TsData.fitToDomain(data.series, data.upper.getDomain());
        TsData values = data.multiplicative ? TsData.divide(tmp, data.upper) : TsData.subtract(tmp, data.upper);
        DescriptiveStatistics stats = DescriptiveStatistics.of(values.getValues());
        double min = stats.getMin();
        double max = stats.getMax();
        if (max - min > 0) {
            XYPlot plot = chartPanel.getChart().getXYPlot();
            TsDomain domain = values.getDomain().extend(0, 1);
            for (int i = 0; i < domain.getLength() - 1; i++) {
                float val = (float) ((values.getValue(i) - min) / (max - min));
                IntervalMarker marker = new IntervalMarker(
                        1000.0 * domain.get(i).start().toEpochSecond(ZoneOffset.UTC),
                        1000.0 * domain.get(i + 1).end().toEpochSecond(ZoneOffset.UTC));
                marker.setOutlineStroke(null);
                marker.setAlpha(1f - val);
                plot.addDomainMarker(marker, Layer.BACKGROUND);
            }
        }
    }

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

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

        LinesThickness linesThickness = LinesThickness.Thin;

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

        XYDifferenceRenderer difference = new XYDifferenceRenderer();
        difference.setAutoPopulateSeriesPaint(false);
        difference.setAutoPopulateSeriesStroke(false);
        difference.setBaseStroke(TsCharts.getNormalStroke(linesThickness));
        plot.setRenderer(DIFFERENCE_INDEX, difference);

        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 JMenu buildMenu() {
        JMenu result = new JMenu();

        result.add(new JCheckBoxMenuItem(new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                setPrecisionMarkersVisible(!isPrecisionMarkersVisible());
            }
        })).setText("Show precision gradient");

        result.add(new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                TsCollection col = Stream.of(
                        Ts.of("series", data.series),
                        Ts.of("lower", data.lower),
                        Ts.of("upper", data.upper)
                ).collect(TsCollection.toTsCollection());
                Transferable t = DataTransferManager.get().fromTsCollection(col);
                Toolkit.getDefaultToolkit().getSystemClipboard().setContents(t, null);
            }
        }).setText("Copy all series");

        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;
    }

    private static final class MarginData {

        final TsData series;
        final TsData lower;
        final TsData upper;
        final LocalDateTime[] markers;
        final boolean multiplicative;

        public MarginData(TsData series, TsData lower, TsData upper, boolean multiplicative, LocalDateTime[] markers) {
            this.series = series;
            this.lower = lower;
            this.upper = upper;
            this.markers = markers;
            this.multiplicative = multiplicative;
        }
    }

    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();
        }
    }

    public 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;
        }
    }

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

    private class LineRenderer extends XYLineAndShapeRenderer {

        public LineRenderer() {
            setBaseItemLabelsVisible(true);
            setAutoPopulateSeriesShape(false);
            setAutoPopulateSeriesFillPaint(false);
            setAutoPopulateSeriesOutlineStroke(false);
            setBaseShape(ITEM_SHAPE);
            setUseFillPaint(true);
        }

        @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(MAIN_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 colorSchemeResolver.resolve().getLineColor(MAIN_COLOR);
        }

        @Override
        public Paint getItemPaint(int series, int item) {
            return colorSchemeResolver.resolve().getLineColor(MAIN_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 = colorSchemeResolver.resolve().getLineColor(MAIN_COLOR);
            Stroke outlineStroke = AbstractRenderer.DEFAULT_STROKE;
            Charts.drawItemLabelAsTooltip(g2, x, y, 3d, label, font, paint, fillPaint, paint, outlineStroke);
        }

        private String generateLabel() {
            Date date = new Date(highlight.getDataset().getX(0, highlight.getItem()).longValue());
            LocalDate ldate = CalendarUtility.toLocalDate(date);
            TsUnit unit = TsUnit.ofAnnualFrequency(data.series.getAnnualFrequency());
            TsPeriod p = TsPeriod.of(unit, ldate);
            String label = "Period : " + p + "\nValue : ";
            label += obsFormatResolver.resolve().numberFormatter().formatAsString(data.series.getDoubleValue(p));
            return label;
        }
    }

    @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