
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