From e472245ae8c67b3c13ab828ca79312beae06799b Mon Sep 17 00:00:00 2001 From: Mats Maggi Date: Wed, 18 Nov 2015 10:59:17 +0100 Subject: [PATCH] - Bug fix in the simulation when model can't be calculated for a certain date of the provided evaluation sample - Improvements in Correlation Ball : - Zoom feature - Wheel can be rotated to ease label reading - Selection sync between the list of variables and the wheel - By default, the highest correlations are shown (representing 25% of total data) --- README.md | 16 + .../main/java/ec/tss/dfm/DfmSimulation.java | 3 +- .../dfm/DfmSimulationTopComponent.java | 35 +- .../dfm/output/CorrelationBallView.java | 118 ++++-- .../dfm/output/ResidualsMatrixView.java | 2 +- .../correlationball/ClickableCircle.java | 132 ------- .../correlationball/ClickableShape.java | 111 ++++++ .../correlationball/CorrelationBall.java | 356 +++++++++++++----- .../CorrelationBallCommand.java | 44 +++ .../correlationball/CorrelationJList.java | 92 +++++ .../nbb/demetra/dfm/CorrelationBallDemo.java | 94 +++++ 11 files changed, 745 insertions(+), 258 deletions(-) create mode 100644 README.md delete mode 100644 nbdemetra-dfm/src/main/java/be/nbb/demetra/dfm/output/correlationball/ClickableCircle.java create mode 100644 nbdemetra-dfm/src/main/java/be/nbb/demetra/dfm/output/correlationball/ClickableShape.java create mode 100644 nbdemetra-dfm/src/main/java/be/nbb/demetra/dfm/output/correlationball/CorrelationJList.java create mode 100644 nbdemetra-dfm/src/test/java/be/nbb/demetra/dfm/CorrelationBallDemo.java diff --git a/README.md b/README.md new file mode 100644 index 0000000..ba0a37d --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +#JDemetra+ Nowcasting plugin + +_Current version of the plugin needs the latest development version of JDemetra+ (develop branch, not the 2.0.0 release)_ + +##Features +- Operationalize the nowcasting process +- Visual analysis of news and updates +- Real-time simulations + +##Installation +In order to install the plugin, 2 nbm files have to be added to the JDemetra+ plugins : + +- nbdemetra-core2-X.X.X.nbm +- nbdemetra-dfm-X.X.X.nbm + +where X.X.X is the version of the compiled plugin \ No newline at end of file diff --git a/jtss2/src/main/java/ec/tss/dfm/DfmSimulation.java b/jtss2/src/main/java/ec/tss/dfm/DfmSimulation.java index 69b219a..144950e 100644 --- a/jtss2/src/main/java/ec/tss/dfm/DfmSimulation.java +++ b/jtss2/src/main/java/ec/tss/dfm/DfmSimulation.java @@ -96,7 +96,6 @@ public boolean process(DfmDocument refdoc, Day[] ed, List estimationDays) { Ts[] input = refdoc.getInput(); TsInformationSet info = new TsInformationSet(refdoc.getData()); - descriptions.addAll(Arrays.asList(refdoc.getDfmResults().getDescriptions())); for (MeasurementSpec ms : refdoc.getSpecification().getModelSpec().getMeasurements()) { watched.add(ms.isWatched()); @@ -131,7 +130,7 @@ public boolean process(DfmDocument refdoc, Day[] ed, List estimationDays) { if (doc.getResults() != null) { Node n = doc.getResults().getNode(DfmProcessingFactory.FINALC); - if (n != null) { + if (n != null && n.results != null) { SimulationResultsDocument rslts = new SimulationResultsDocument(n.results); rslts.setSmoothedSeriesStdev(doc.getDfmResults() == null ? null : doc.getDfmResults().getSmoothedSeriesStdev()); rslts_.put(ed[i], rslts); diff --git a/nbdemetra-dfm/src/main/java/be/nbb/demetra/dfm/DfmSimulationTopComponent.java b/nbdemetra-dfm/src/main/java/be/nbb/demetra/dfm/DfmSimulationTopComponent.java index 20d8c81..42b4226 100644 --- a/nbdemetra-dfm/src/main/java/be/nbb/demetra/dfm/DfmSimulationTopComponent.java +++ b/nbdemetra-dfm/src/main/java/be/nbb/demetra/dfm/DfmSimulationTopComponent.java @@ -218,17 +218,17 @@ protected Object doInBackground() throws Exception { last.move(last.getFrequency().intValue()); Day horizon = last.lastday(); simulation = new DfmSimulation(horizon); - + simulation.addPropertyChangeListener(new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { if (evt.getPropertyName().equals(DfmSimulation.CALENDAR_RESULTS)) { - Day value = (Day)evt.getNewValue(); + Day value = (Day) evt.getNewValue(); publish("Generating data of publication date : " + value.toString() + "..."); } } }); - + publish("Processing simulation of DFM..."); simulation.process(vdoc.getCurrent(), new ArrayList<>(Arrays.asList(vdoc.getCurrent().getSpecification().getSimulationSpec().getEstimationDays()))); Map results = simulation.getResults(); @@ -248,7 +248,19 @@ public void propertyChange(PropertyChangeEvent evt) { publish("Generating results of series #" + s); TsDataTable tble = new TsDataTable(); for (int i = 0; i < cal.length; ++i) { - tble.insert(-1, results.get(cal[i]).getSimulationResults().getData("var" + (s + 1), TsData.class)); + if (results.get(cal[i]) != null) { + if (results.get(cal[i]).getSimulationResults() != null) { + if (results.get(cal[i]).getSimulationResults().contains("var" + (s + 1))) { + tble.insert(-1, results.get(cal[i]).getSimulationResults().getData("var" + (s + 1), TsData.class)); + } else { + System.out.println("Simulation results for calendar " + cal[i].toString() + " don't contain variable " + (s + 1)); + } + } else { + System.out.println("No simulation results for calendar " + cal[i].toString() + " - Variable " + (s + 1)); + } + } else { + System.out.println("No results for calendar " + cal[i].toString() + " - Variable " + (s + 1)); + } } tble.insert(-1, info.series(s)); @@ -378,6 +390,13 @@ private DfmSimulationResults createFHTable(StringWriter writer, TsDataTable tabl mapQoQ.put(diff, new Double[nbHeaders]); } +// TsDataTableInfo dataInfo; +// try { +// dataInfo = table.getDataInfo(i, j); +// } catch (TsException ex) { +// dataInfo = null; +// } +// if (dataInfo != null && dataInfo == TsDataTableInfo.Valid) { TsDataTableInfo dataInfo = table.getDataInfo(i, j); if (dataInfo == TsDataTableInfo.Valid) { Double current = table.getData(i, j); @@ -572,7 +591,7 @@ private DfmSimulationResults createFHTable(StringWriter writer, TsDataTable tabl r.setTrueValues(trueValues); r.setTrueValuesYoY(trueValuesYoY); r.setTrueValuesQoQ(trueValuesQoQ); - + return r; } @@ -709,7 +728,7 @@ public void execute(DfmSimulationTopComponent c) throws Exception { JOptionPane.showMessageDialog(null, "DFM processing must be done before the simulation !", "Error", JOptionPane.ERROR_MESSAGE); return; } - + VersionedDfmDocument vd = c.getDocument().getElement(); List m = vd.getSpecification().getModelSpec().getMeasurements(); int count = 0; @@ -720,12 +739,12 @@ public void execute(DfmSimulationTopComponent c) throws Exception { } i++; } - + if (count == 0) { JOptionPane.showMessageDialog(null, "You must select at least one series for data generation !\nRight click on a series to enable/disable data generation.", "Error", JOptionPane.ERROR_MESSAGE); return; } - + chooser.setDialogTitle("Select the output folder"); chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); chooser.setAcceptAllFileFilterUsed(false); diff --git a/nbdemetra-dfm/src/main/java/be/nbb/demetra/dfm/output/CorrelationBallView.java b/nbdemetra-dfm/src/main/java/be/nbb/demetra/dfm/output/CorrelationBallView.java index 62c2238..2aaeb08 100644 --- a/nbdemetra-dfm/src/main/java/be/nbb/demetra/dfm/output/CorrelationBallView.java +++ b/nbdemetra-dfm/src/main/java/be/nbb/demetra/dfm/output/CorrelationBallView.java @@ -18,18 +18,29 @@ import be.nbb.demetra.dfm.output.correlationball.CorrelationBall; import static be.nbb.demetra.dfm.output.correlationball.CorrelationBall.COLOR_SCALE_PROPERTY; +import be.nbb.demetra.dfm.output.correlationball.CorrelationJList; import com.google.common.base.Optional; -import ec.nbdemetra.ui.awt.PopupListener; +import ec.nbdemetra.ui.NbComponents; import ec.tss.dfm.DfmResults; import ec.tstoolkit.dfm.DynamicFactorModel; import ec.tstoolkit.maths.matrices.Matrix; import java.awt.BorderLayout; import java.awt.Color; +import java.awt.Dimension; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.ArrayList; +import java.util.Dictionary; +import java.util.Hashtable; import java.util.List; +import javax.swing.JComponent; +import javax.swing.JLabel; import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSlider; +import javax.swing.JSplitPane; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; /** * @@ -39,16 +50,18 @@ public class CorrelationBallView extends JPanel { // Properties public static final String DFM_RESULTS_PROPERTY = "dfmResults"; - + private Optional results; private CorrelationBall ball; - + private List titles; + private final CorrelationJList list; + public CorrelationBallView(Optional r) { setLayout(new BorderLayout()); - + this.results = r; createBall(); - + addPropertyChangeListener(new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { @@ -59,7 +72,27 @@ public void propertyChange(PropertyChangeEvent evt) { } } }); - + + final JSlider slider = new JSlider(0, 360, 0); + slider.setBackground(Color.WHITE); + Dictionary dic = new Hashtable<>(); + for (int i = 0; i <= 360; i = i + 30) { + dic.put(i, new JLabel(String.valueOf(i) + "°")); + } + slider.setLabelTable(dic); + slider.setPaintLabels(true); + slider.setPaintTicks(true); + slider.setMajorTickSpacing(30); + slider.setMinorTickSpacing(5); + slider.setFocusable(false); + slider.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + JSlider source = (JSlider) e.getSource(); + ball.spin(source.getValue()); + } + }); + ball.addPropertyChangeListener(new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { @@ -67,25 +100,65 @@ public void propertyChange(PropertyChangeEvent evt) { case COLOR_SCALE_PROPERTY: updateBall(); break; + case CorrelationBall.SPIN_ON_SELECTION_PROPERTY : + slider.setValue((int)((double)evt.getNewValue())); + break; } } }); - ball.addMouseListener(new PopupListener.PopupAdapter(ball.buildGridMenu().getPopupMenu())); - - add(ball, BorderLayout.CENTER); - + ball.setComponentPopupMenu(ball.buildGridMenu().getPopupMenu()); + list = new CorrelationJList(); + + JScrollPane scrollPane = new JScrollPane(list, + JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, + JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + scrollPane.setPreferredSize(new Dimension(150, 500)); + JSplitPane splitPane = NbComponents.newJSplitPane(JSplitPane.HORIZONTAL_SPLIT, ball, scrollPane); + splitPane.setResizeWeight(0.9); + + enableSync(); + + add(slider, BorderLayout.SOUTH); + add(splitPane, BorderLayout.CENTER); + updateBall(); } - + + private void enableSync() { + PropertyChangeListener listener = new PropertyChangeListener() { + boolean updating = false; + + @Override + public void propertyChange(PropertyChangeEvent evt) { + if (updating) { + return; + } + + updating = true; + switch (evt.getPropertyName()) { + case CorrelationBall.INDEX_SELECTED_PROPERTY: + int index = (int) evt.getNewValue(); + list.setSelectedItem(index); + break; + case CorrelationJList.SELECTED_ITEM_PROPERTY: + ball.setIndexSelected((int) evt.getNewValue()); + break; + } + updating = false; + } + }; + + ball.addPropertyChangeListener(listener); + list.addPropertyChangeListener(listener); + } + private void createBall() { ball = new CorrelationBall(); ball.setBackground(Color.WHITE); ball.setForeground(Color.BLACK); } - - private List titles; - + private double[][] filterMatrix() { List indexes = new ArrayList<>(); titles = new ArrayList<>(); @@ -98,7 +171,7 @@ private double[][] filterMatrix() { titles.add(rslts.getDescription(i).description); } } - + double[][] result = new double[indexes.size()][indexes.size()]; for (int i = 0; i < indexes.size(); i++) { for (int j = 0; j < indexes.size(); j++) { @@ -107,24 +180,27 @@ private double[][] filterMatrix() { } return result; } - + public Optional getDfmResults() { return results; } - + public void setDfmResults(Optional dfmResults) { Optional old = this.results; this.results = dfmResults != null ? dfmResults : Optional.absent(); firePropertyChange(DFM_RESULTS_PROPERTY, old, this.results); } - + private void updateBall() { - removeAll(); + ball.removeAll(); if (results != null && results.isPresent()) { double[][] matrix = filterMatrix(); ball.setMatrix(titles, matrix); - add(ball, BorderLayout.CENTER); + + list.setListData(titles.toArray(new String[titles.size()])); } } - + + + } diff --git a/nbdemetra-dfm/src/main/java/be/nbb/demetra/dfm/output/ResidualsMatrixView.java b/nbdemetra-dfm/src/main/java/be/nbb/demetra/dfm/output/ResidualsMatrixView.java index 386c13e..c9c3d92 100644 --- a/nbdemetra-dfm/src/main/java/be/nbb/demetra/dfm/output/ResidualsMatrixView.java +++ b/nbdemetra-dfm/src/main/java/be/nbb/demetra/dfm/output/ResidualsMatrixView.java @@ -382,7 +382,7 @@ public Component getTableCellRendererComponent(JTable table, Object value, boole l.setForeground(table.getSelectionForeground()); } } else if (value instanceof Double) { - DemetraUI demetraUI = DemetraUI.getInstance(); + DemetraUI demetraUI = DemetraUI.getDefault(); Formatter format = demetraUI.getDataFormat().numberFormatter(); Number number = (Double) value; diff --git a/nbdemetra-dfm/src/main/java/be/nbb/demetra/dfm/output/correlationball/ClickableCircle.java b/nbdemetra-dfm/src/main/java/be/nbb/demetra/dfm/output/correlationball/ClickableCircle.java deleted file mode 100644 index f4a123d..0000000 --- a/nbdemetra-dfm/src/main/java/be/nbb/demetra/dfm/output/correlationball/ClickableCircle.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * 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.correlationball; - -import java.awt.BasicStroke; -import java.awt.Color; -import java.awt.Container; -import java.awt.Dimension; -import java.awt.Graphics; -import java.awt.Graphics2D; -import java.awt.RenderingHints; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.awt.geom.Path2D; -import java.util.ArrayList; -import java.util.List; -import javax.swing.JComponent; - -/** - * - * @author Mats Maggi - */ -public class ClickableCircle extends JComponent { - - private int index; - private String title; - private final List paths; - public final static String HIGHLIGHT = "HIGHLIGHT"; - - public ClickableCircle(final int index) { - setPreferredSize(new Dimension(8, 8)); - this.index = index; - paths = new ArrayList<>(); - - addMouseListener(new MouseAdapter() { - @Override - public void mouseEntered(MouseEvent e) { - foreground = Color.RED; - repaint(); - } - - @Override - public void mouseExited(MouseEvent e) { - foreground = getForeground(); - repaint(); - } - - @Override - public void mouseClicked(MouseEvent e) { - fire(); - } - }); - } - - public void fire() { - Container c = getParent(); - if (c instanceof CorrelationBall) { - ((CorrelationBall) c).changeHighlight(index); - } - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - setToolTipText(this.title); - } - - public List getPaths() { - return paths; - } - - public void addPath(Path2D.Double p, float w, int from, int to) { - for (Path path : paths) { - if ((path.from == from && path.to == to) - || (path.to == from && path.from == to)) { - return; - } - } - paths.add(new Path(p, w, from, to)); - } - - public int getIndex() { - return index; - } - - public void setIndex(int index) { - this.index = index; - } - - private Color foreground = getForeground(); - - @Override - protected void paintComponent(Graphics g) { - Graphics2D g2d = (Graphics2D) g; - g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, - RenderingHints.VALUE_ANTIALIAS_ON); - g2d.setColor(foreground); - g2d.setStroke(new BasicStroke(1f)); - g2d.fillOval(0, 0, getWidth(), getHeight()); - } - - public class Path { - public Path2D.Double path; - public float weight; - public int from; - public int to; - - public Path(Path2D.Double path, float weight, int from, int to) { - this.path = path; - this.weight = weight; - this.from = from; - this.to = to; - } - } -} diff --git a/nbdemetra-dfm/src/main/java/be/nbb/demetra/dfm/output/correlationball/ClickableShape.java b/nbdemetra-dfm/src/main/java/be/nbb/demetra/dfm/output/correlationball/ClickableShape.java new file mode 100644 index 0000000..1ddc414 --- /dev/null +++ b/nbdemetra-dfm/src/main/java/be/nbb/demetra/dfm/output/correlationball/ClickableShape.java @@ -0,0 +1,111 @@ +/* + * Copyright 2014 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.correlationball; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Path2D; +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import javax.swing.JComponent; + +/** + * + * @author Mats Maggi + */ +public class ClickableShape extends JComponent { + + private final Ellipse2D.Double ellipse; + private Color foreground; + private Point2D position; + private final int index; + private final List paths; + + public ClickableShape(int index) { + setPreferredSize(new Dimension(10, 10)); + this.ellipse = new Ellipse2D.Double(0, 0, 10, 10); + this.index = index; + this.paths = new ArrayList<>(); + } + + public void setPosition(Point2D position) { + this.position = position; + } + + public void setCoords(double x, double y) { + this.ellipse.x = x; + this.ellipse.y = y; + repaint(); + } + + public List getPaths() { + return paths; + } + + public Set getConnectedShapes() { + Set connected = new HashSet<>(); + for (Path p : paths) { + connected.add(p.to); + } + + return connected; + } + + public Ellipse2D.Double getEllipse() { + return ellipse; + } + + public void addPath(Path2D.Double p, float w, int from, int to) { + for (Path path : paths) { + if ((path.from == from && path.to == to) + || (path.to == from && path.from == to)) { + return; + } + } + paths.add(new Path(p, w, from, to)); + } + + public class Path implements Comparable { + + public Path2D.Double path; + public float weight; + public int from; + public int to; + + public Path(Path2D.Double path, float weight, int from, int to) { + this.path = path; + this.weight = weight; + this.from = from; + this.to = to; + } + + @Override + public int compareTo(Path o) { + if (this.weight < o.weight) { + return -1; + } else if (this.weight > o.weight) { + return 1; + } else { + return 0; + } + } + } +} diff --git a/nbdemetra-dfm/src/main/java/be/nbb/demetra/dfm/output/correlationball/CorrelationBall.java b/nbdemetra-dfm/src/main/java/be/nbb/demetra/dfm/output/correlationball/CorrelationBall.java index 27bb097..40c24ba 100644 --- a/nbdemetra-dfm/src/main/java/be/nbb/demetra/dfm/output/correlationball/CorrelationBall.java +++ b/nbdemetra-dfm/src/main/java/be/nbb/demetra/dfm/output/correlationball/CorrelationBall.java @@ -16,7 +16,7 @@ */ package be.nbb.demetra.dfm.output.correlationball; -import be.nbb.demetra.dfm.output.correlationball.ClickableCircle.Path; +import be.nbb.demetra.dfm.output.correlationball.ClickableShape.Path; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Dimension; @@ -26,12 +26,20 @@ import java.awt.RenderingHints; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; +import java.awt.event.MouseMotionAdapter; +import java.awt.event.MouseWheelEvent; import java.awt.geom.AffineTransform; +import java.awt.geom.NoninvertibleTransformException; import java.awt.geom.Path2D; +import java.awt.geom.Point2D; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.swing.JCheckBoxMenuItem; import javax.swing.JMenu; import javax.swing.JPanel; @@ -44,14 +52,22 @@ * @author Mats Maggi */ public class CorrelationBall extends JPanel { - + public static final String COLOR_SCALE_PROPERTY = "scale"; + public static final String SHOW_IMPORTANT_PROPERTY = "showImportantOnly"; + public static final String INDEX_SELECTED_PROPERTY = "indexSelected"; + public static final String SPIN_ON_SELECTION_PROPERTY = "spinOnSelection"; private double min, max; - private final float maxStroke = 4f; + private double zoom = 1.0; + private final float maxStroke = 3f; private final float minStroke = .1f; - private double colorScale = 1.0; - + private int spinAngle = 0; + + private double colorScale = 0.5; + private boolean showImportantOnly = true; + private boolean spinOnSelection = false; + // Heat map colour settings. private final Color highValueColour = new Color(0, 82, 163); private final Color lowValueColour = Color.WHITE; @@ -59,52 +75,126 @@ public class CorrelationBall extends JPanel { // How many RGB steps there are between the high and low colours. private int colourValueDistance; - private int indexToHighlight = -1; - private final List circles = new ArrayList<>(); + private int indexSelected = -1; + private int indexHovered = -1; + + private List titles; + private double[][] matrix; + + private AffineTransform original; + + private final List shapes = new ArrayList<>(); public CorrelationBall() { super(); - setPreferredSize(new Dimension(500, 500)); + setDoubleBuffered(true); + setPreferredSize(new Dimension(300, 300)); setLayout(null); + addMouseWheelListener(new MouseAdapter() { + @Override + public void mouseWheelMoved(MouseWheelEvent e) { + int rotations = e.getWheelRotation(); + if (rotations > 0 || (rotations < 0 && zoom > 0.2)) { + zoom += rotations * 0.1; + } + + zoom = Math.max(0.00001, zoom); + repaint(); + } + }); + addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { - super.mouseClicked(e); - indexToHighlight = -1; - repaint(); + try { + setIndexSelected(-1); + Point2D pt = new Point2D.Double(); + original.inverseTransform((Point2D) e.getPoint(), pt); + int i = 0; + boolean found = false; + while (!found && i < shapes.size()) { + if (shapes.get(i).getEllipse().contains(pt)) { + setIndexSelected(i); + found = true; + } + i++; + } + super.mouseClicked(e); + } catch (NoninvertibleTransformException ex) { + Logger.getLogger(CorrelationBall.class.getName()).log(Level.SEVERE, null, ex); + } } + }); + addMouseMotionListener(new MouseMotionAdapter() { + + @Override + public void mouseMoved(MouseEvent e) { + super.mouseMoved(e); + boolean found = false; + int i = 0; + try { + Point2D pt = new Point2D.Double(); + original.inverseTransform((Point2D) e.getPoint(), pt); + if (indexHovered != -1 && !shapes.get(indexHovered).getEllipse().contains(pt)) { + indexHovered = -1; + repaint(); + } else { + while (!found && i < shapes.size()) { + if (shapes.get(i).getEllipse().contains(pt)) { + if (indexHovered != i) { + indexHovered = i; + found = true; + repaint(); + } + + break; + } + i++; + } + } + + } catch (NoninvertibleTransformException ex) { + Logger.getLogger(CorrelationBall.class.getName()).log(Level.SEVERE, null, ex); + } + } }); } - + public JMenu buildGridMenu() { JMenu result = new JMenu(); - + JMenu scale = new JMenu("Color Scale"); final JSlider slider = new JSlider(1, 100, 1); - { - slider.setPreferredSize(new Dimension(50, slider.getPreferredSize().height)); - slider.addChangeListener(new ChangeListener() { - @Override - public void stateChanged(ChangeEvent e) { - setColorScale((double)slider.getValue()/10.0); - } - }); - addPropertyChangeListener(COLOR_SCALE_PROPERTY, new PropertyChangeListener() { - @Override - public void propertyChange(PropertyChangeEvent evt) { - slider.setValue((int)(getColorScale()*10.0)); - } - }); - } + slider.setPreferredSize(new Dimension(50, slider.getPreferredSize().height)); + slider.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + setColorScale((double) slider.getValue() / 10.0); + } + }); + addPropertyChangeListener(COLOR_SCALE_PROPERTY, new PropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent evt) { + slider.setValue((int) (getColorScale() * 10.0)); + } + }); scale.add(slider); for (final double o : new double[]{0.1, 0.5, 1.0, 5.0, 10.0}) { scale.add(new JCheckBoxMenuItem(CorrelationBallCommand.applyColorScale(o).toAction(this))).setText(String.valueOf(o)); } result.add(scale); + final JCheckBoxMenuItem show = new JCheckBoxMenuItem(CorrelationBallCommand.showAllCommand().toAction(this)); + show.setText("Show important relations only"); + result.add(show); + + final JCheckBoxMenuItem spin = new JCheckBoxMenuItem(CorrelationBallCommand.spinOnSelectionCommand().toAction(this)); + spin.setText("Spin on selection"); + result.add(spin); + return result; } @@ -118,14 +208,33 @@ public void setColorScale(double scale) { firePropertyChange(COLOR_SCALE_PROPERTY, old, this.colorScale); } - private double[][] matrix; - private List titles; + public boolean isImportantOnlyShown() { + return this.showImportantOnly; + } + + public void setImportantOnlyShown(boolean showAll) { + this.showImportantOnly = showAll; + repaint(); + } + + public boolean isSpinOnSelection() { + return spinOnSelection; + } + + public void setSpinOnSelection(boolean spinOnSelection) { + this.spinOnSelection = spinOnSelection; + } + + public void spin(int value) { + spinAngle = value; + repaint(); + } public final void setMatrix(List titles, double[][] matrix) { - circles.clear(); + shapes.clear(); removeAll(); revalidate(); - indexToHighlight = -1; + indexSelected = -1; this.matrix = matrix; this.titles = titles; @@ -134,10 +243,8 @@ public final void setMatrix(List titles, double[][] matrix) { max = Double.MIN_VALUE; for (int i = 0; i < matrix.length; i++) { - ClickableCircle c = new ClickableCircle(i); - c.setTitle(titles.get(i)); - circles.add(c); - add(c); + ClickableShape cc = new ClickableShape(i); + shapes.add(cc); for (int j = 0; j < matrix[i].length; j++) { double abs = Math.abs(matrix[i][j]); if (abs != 0) { @@ -147,22 +254,26 @@ public final void setMatrix(List titles, double[][] matrix) { max = abs; } } - } } - - updateColourDistance(); - repaint(); + updateColourDistance(); + updateUI(); } - public void changeHighlight(int index) { - if (indexToHighlight == index) { - indexToHighlight = -1; - } else { - indexToHighlight = index; + public void setIndexSelected(int index) { + int old = this.indexSelected; + if (indexSelected == index) { + index = -1; } + this.indexSelected = index; repaint(); + firePropertyChange(INDEX_SELECTED_PROPERTY, old, this.indexSelected); + + if (spinOnSelection) { + double step = 360.0 / shapes.size(); + firePropertyChange(SPIN_ON_SELECTION_PROPERTY, null, 360.0-(step*index)); + } } @Override @@ -173,55 +284,58 @@ protected void paintComponent(Graphics g) { } int space = getWidth() / 5; int radius = (getWidth() - space * 2) / 2; - + Graphics2D g2d = (Graphics2D) g; + g2d.translate(getWidth() / 2, getHeight() / 2); + g2d.scale(zoom, zoom); + g2d.rotate(Math.toRadians(spinAngle)); + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); - - g2d.setColor(Color.GRAY); - g2d.setStroke(new BasicStroke(11f)); - g2d.drawOval(space, space, getWidth()-space*2, getWidth()-space*2); + g2d.setRenderingHint(RenderingHints.KEY_RENDERING, + RenderingHints.VALUE_RENDER_SPEED); + g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, + RenderingHints.VALUE_ALPHA_INTERPOLATION_SPEED); + g2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, + RenderingHints.VALUE_COLOR_RENDER_SPEED); g2d.setColor(getForeground()); g2d.setStroke(new BasicStroke(1f)); - AffineTransform original = g2d.getTransform(); + original = g2d.getTransform(); int length = matrix.length; double step = 360.0 / length; List points = new ArrayList<>(); - Color offLedColor = new Color(86, 0, 0); for (int i = 0; i < length; i++) { double angle = Math.toRadians(step * i); - double newX = (radius * Math.cos(angle)) + radius; - double newY = (radius * Math.sin(angle)) + radius; - points.add(new Point.Double(newX + space, newY + space)); + double newX = (radius * Math.cos(angle)) - 5; + double newY = (radius * Math.sin(angle)) - 5; - AffineTransform orig = (AffineTransform) g2d.getTransform().clone(); - orig.rotate(angle, newX + space, newY + space); - g2d.setTransform(orig); + points.add(new Point.Double(newX + 5, newY + 5)); - ClickableCircle c = circles.get(i); - c.setForeground(offLedColor); - c.getPaths().clear(); - c.setLocation((int) newX + space - 5, (int) newY + space - 5); - c.setBounds((int) newX + space - 5, (int) newY + space - 5, 10, 10); - - newX = ((radius) * Math.cos(angle)) + radius + 10; - newY = ((radius) * Math.sin(angle)) + radius; + Point2D pt = new Point2D.Double(); + try { + original.inverseTransform(shapes.get(i).getEllipse().getBounds().getLocation(), pt); + } catch (NoninvertibleTransformException ex) { + Logger.getLogger(CorrelationBall.class.getName()).log(Level.SEVERE, null, ex); + } - g2d.drawString(titles.get(i), (int) newX + space + 5, (int) newY + space + 5); - g2d.setTransform(original); + shapes.get(i).setBounds((int) pt.getX(), (int) pt.getY(), 10, 10); + shapes.get(i).setLocation((int) pt.getX(), (int) pt.getY()); + shapes.get(i).setCoords(newX, newY); + shapes.get(i).getPaths().clear(); } - + + allPaths = new ArrayList<>(); for (int i = 0; i < length; i++) { - for (int j = 0; j <= i; j++) { + for (int j = 0; j < i; j++) { if (!Double.isNaN(matrix[i][j]) && matrix[i][j] != 0) { Path2D.Double path = new Path2D.Double(); @@ -240,42 +354,96 @@ protected void paintComponent(Graphics g) { double angleRad = Math.toRadians(angleFinal); - double newX = ((radius / 3) * Math.cos(angleRad)) + radius; - double newY = ((radius / 3) * Math.sin(angleRad)) + radius; + double newX = ((radius / 3) * Math.cos(angleRad)); + double newY = ((radius / 3) * Math.sin(angleRad)); path.moveTo(points.get(i).x, points.get(i).y); - path.curveTo(newX + space, newY + space, newX + space, newY + space, points.get(j).x, points.get(j).y); + path.curveTo(newX, newY + 5, newX, newY, points.get(j).x, points.get(j).y); double normValue = ((double) Math.abs(matrix[i][j]) - min) / (max - min); - float strokeValue = ((float)normValue * (maxStroke - minStroke)) + minStroke; + float strokeValue = ((float) normValue * (maxStroke - minStroke)) + minStroke; - circles.get(i).addPath(path, strokeValue, i, j); - circles.get(j).addPath(path, strokeValue, j, i); + shapes.get(i).addPath(path, strokeValue, i, j); + shapes.get(j).addPath(path, strokeValue, j, i); } } } + + for (ClickableShape shape : shapes) { + allPaths.addAll(shape.getPaths()); + } + Collections.sort(allPaths); + int size = allPaths.size(); + allPaths = allPaths.subList((int)(size*0.75), size-1); - paintLines(g2d); + paintLines(g2d, radius, step); } - - private void paintLines(Graphics2D g2d) { - - for (int i = 0; i < circles.size(); i++) { - for (Path p : circles.get(i).getPaths()) { - g2d.setColor(getCellColour(normalize(p.weight))); - if (indexToHighlight == -1 || i == indexToHighlight) { - g2d.setStroke(new BasicStroke(p.weight)); + + private List allPaths; + + private void paintLines(Graphics2D g2d, int radius, double step) { + for (int i = 0; i < shapes.size(); i++) { + for (Path p : shapes.get(i).getPaths()) { + double normalized = normalize(p.weight); + g2d.setColor(getCellColour(normalized)); + g2d.setStroke(new BasicStroke(p.weight)); + if (indexSelected == -1) { + if (showImportantOnly) { + if (allPaths.contains(p)) { + g2d.draw(p.path); + } + } else { + g2d.draw(p.path); + } + } else if (indexSelected == i) { g2d.draw(p.path); } } } + + Color offLedColor = new Color(86, 0, 0); + for (int i = 0; i < shapes.size(); i++) { + double angle = Math.toRadians(step * i); + if (i == indexSelected || i == indexHovered) { + g2d.setColor(new Color(250, 70, 71)); + } else { + if (indexSelected != -1) { + Set connected = shapes.get(indexSelected).getConnectedShapes(); + if (connected.contains(i)) { + g2d.setColor(new Color(33, 150, 243)); + } else { + g2d.setColor(new Color(211, 234, 253)); + } + drawString(g2d, titles.get(i), radius, angle); + } else { + g2d.setColor(offLedColor); + } + } + drawString(g2d, titles.get(i), radius, angle); + + add(shapes.get(i)); + g2d.fill(shapes.get(i).getEllipse()); + } + } + + private void drawString(Graphics2D g2d, String label, int radius, double angle) { + // Draw labels + int h = (int) Math.floor(g2d.getFontMetrics().getStringBounds(label, g2d).getHeight() / 2); + double X = (radius * Math.cos(0)) - 5; + double Y = (radius * Math.sin(0)) - 5; + AffineTransform rotated = (AffineTransform) g2d.getTransform().clone(); + rotated.rotate(angle); + g2d.setTransform(rotated); + + g2d.drawString(label, (int) X + 15, (int) Y + h + 3); + + g2d.setTransform(original); } private double normalize(double value) { return (value - minStroke) / (maxStroke - minStroke); } - - // + private Color getCellColour(double data) { double range = max - min; double position = data - min; @@ -285,7 +453,7 @@ private Color getCellColour(double data) { // Which colour group does that put us in. int colourPosition = getColourPosition(percentPosition); - + int r = lowValueColour.getRed(); int g = lowValueColour.getGreen(); int b = lowValueColour.getBlue(); @@ -295,7 +463,7 @@ private Color getCellColour(double data) { 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. @@ -308,14 +476,14 @@ private Color getCellColour(double data) { b = changeColourValue(b, bDistance); } } - + return new Color(r, g, b); } - + private int getColourPosition(double percentPosition) { return (int) Math.round(colourValueDistance * Math.pow(percentPosition, getColorScale())); } - + private int changeColourValue(int colourValue, int colourDistance) { if (colourDistance < 0) { return colourValue + 1; @@ -326,7 +494,7 @@ private int changeColourValue(int colourValue, int colourDistance) { return colourValue; } } - + private void updateColourDistance() { int r1 = lowValueColour.getRed(); int g1 = lowValueColour.getGreen(); @@ -334,7 +502,7 @@ private void updateColourDistance() { 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); diff --git a/nbdemetra-dfm/src/main/java/be/nbb/demetra/dfm/output/correlationball/CorrelationBallCommand.java b/nbdemetra-dfm/src/main/java/be/nbb/demetra/dfm/output/correlationball/CorrelationBallCommand.java index ee390fa..d3b79a2 100644 --- a/nbdemetra-dfm/src/main/java/be/nbb/demetra/dfm/output/correlationball/CorrelationBallCommand.java +++ b/nbdemetra-dfm/src/main/java/be/nbb/demetra/dfm/output/correlationball/CorrelationBallCommand.java @@ -33,6 +33,16 @@ public static JCommand applyColorScale(double scale) { return new ColorScaleCommand(scale); } + @Nonnull + public static JCommand showAllCommand() { + return new ShowImportantOnlyCommand(); + } + + @Nonnull + public static JCommand spinOnSelectionCommand() { + return new SpinOnSelectionCommand(); + } + public static final class ColorScaleCommand extends ComponentCommand { private final double colorScale; @@ -52,4 +62,38 @@ public void execute(CorrelationBall component) throws Exception { component.setColorScale(colorScale); } } + + public static final class ShowImportantOnlyCommand extends ComponentCommand { + + public ShowImportantOnlyCommand() { + super(CorrelationBall.SHOW_IMPORTANT_PROPERTY); + } + + @Override + public boolean isSelected(CorrelationBall component) { + return component.isImportantOnlyShown(); + } + + @Override + public void execute(CorrelationBall component) throws Exception { + component.setImportantOnlyShown(!component.isImportantOnlyShown()); + } + } + + public static final class SpinOnSelectionCommand extends ComponentCommand { + + public SpinOnSelectionCommand() { + super(CorrelationBall.SPIN_ON_SELECTION_PROPERTY); + } + + @Override + public boolean isSelected(CorrelationBall component) { + return component.isSpinOnSelection(); + } + + @Override + public void execute(CorrelationBall component) throws Exception { + component.setSpinOnSelection(!component.isSpinOnSelection()); + } + } } diff --git a/nbdemetra-dfm/src/main/java/be/nbb/demetra/dfm/output/correlationball/CorrelationJList.java b/nbdemetra-dfm/src/main/java/be/nbb/demetra/dfm/output/correlationball/CorrelationJList.java new file mode 100644 index 0000000..c0a8a6f --- /dev/null +++ b/nbdemetra-dfm/src/main/java/be/nbb/demetra/dfm/output/correlationball/CorrelationJList.java @@ -0,0 +1,92 @@ +/* + * Copyright 2014 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.correlationball; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import javax.swing.JList; +import javax.swing.ListSelectionModel; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; + +/** + * + * @author Mats Maggi + */ +public class CorrelationJList extends JList { + + public static final String SELECTED_ITEM_PROPERTY = "itemSelected"; + + private int selectedItem = -1; + private CellSelectionListener selectionListener; + + public CorrelationJList() { + super(); + setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + selectionListener = new CellSelectionListener(); + getSelectionModel().addListSelectionListener(selectionListener); + + addPropertyChangeListener(new PropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent evt) { + switch (evt.getPropertyName()) { + case SELECTED_ITEM_PROPERTY : + onSelectedItemChange(); + break; + } + } + }); + } + + public void setSelectedItem(int index) { + int old = this.selectedItem; + this.selectedItem = index; + firePropertyChange(SELECTED_ITEM_PROPERTY, old, this.selectedItem); + } + + private void onSelectedItemChange() { + if (selectionListener.enabled) { + selectionListener.enabled = false; + if (selectedItem < 0) { + getSelectionModel().clearSelection(); + } else { + getSelectionModel().addSelectionInterval(selectedItem, selectedItem); + } + selectionListener.enabled = true; + } + repaint(); + } + + private final class CellSelectionListener implements ListSelectionListener { + + boolean enabled = true; + + @Override + public void valueChanged(ListSelectionEvent e) { + if (enabled && !e.getValueIsAdjusting()) { + enabled = false; + ListSelectionModel model = getSelectionModel(); + if (model.isSelectionEmpty()) { + setSelectedItem(-1); + } else { + setSelectedItem(getSelectedIndex()); + } + enabled = true; + } + } + } +} diff --git a/nbdemetra-dfm/src/test/java/be/nbb/demetra/dfm/CorrelationBallDemo.java b/nbdemetra-dfm/src/test/java/be/nbb/demetra/dfm/CorrelationBallDemo.java new file mode 100644 index 0000000..cb8d9c8 --- /dev/null +++ b/nbdemetra-dfm/src/test/java/be/nbb/demetra/dfm/CorrelationBallDemo.java @@ -0,0 +1,94 @@ +/* + * 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; + +import be.nbb.demetra.dfm.output.correlationball.CorrelationBall; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.event.ActionEvent; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import javax.swing.AbstractAction; +import javax.swing.JButton; +import javax.swing.JFrame; + +/** + * + * @author Mats Maggi + */ +public class CorrelationBallDemo { + + public static void main(String[] args) { + JFrame f = new JFrame("Correlation Ball"); + f.setLayout(new BorderLayout()); + f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + final CorrelationBall correlationBall = new CorrelationBall(); + double[][] matrix = createMatrix(); + List titles = createTitles(matrix.length); + correlationBall.setMatrix(titles, matrix); + correlationBall.setBackground(Color.WHITE); + correlationBall.setForeground(Color.BLACK); + + f.add(correlationBall, BorderLayout.CENTER); + JButton b = new JButton(new AbstractAction("Random data") { + + @Override + public void actionPerformed(ActionEvent e) { + double[][] matrix = createMatrix(); + List titles = createTitles(matrix.length); + correlationBall.setMatrix(titles, matrix); + } + }); + + f.add(b, BorderLayout.SOUTH); + + f.pack(); + f.setVisible(true); + } + + private static double[][] createMatrix() { + Random r = new Random(); + int size = r.nextInt(50) + 6; + + double [][] matrix = new double[size][size]; + + for (int i = 0; i < matrix.length; i++) { + for (int j = i; j < matrix[i].length; j++) { + if (i==j) { + matrix[i][j] = 1; + } else { + double nb = (r.nextDouble()+1)*100; + double weight = (r.nextDouble()+1)*100; + matrix[i][j] = (nb > 97) ? weight : 0; + } + + } + } + + return matrix; + } + + private static List createTitles(int nb) { + List titles = new ArrayList<>(); + for (int i = 0; i < nb; i++) { + titles.add("Variable number " + i); + } + + return titles; + } +}