1 /*
   2  * Copyright (c) 2012, 2014, Oracle and/or its affiliates.
   3  * All rights reserved. Use is subject to license terms.
   4  *
   5  * This file is available and licensed under the following license:
   6  *
   7  * Redistribution and use in source and binary forms, with or without
   8  * modification, are permitted provided that the following conditions
   9  * are met:
  10  *
  11  *  - Redistributions of source code must retain the above copyright
  12  *    notice, this list of conditions and the following disclaimer.
  13  *  - Redistributions in binary form must reproduce the above copyright
  14  *    notice, this list of conditions and the following disclaimer in
  15  *    the documentation and/or other materials provided with the distribution.
  16  *  - Neither the name of Oracle Corporation nor the names of its
  17  *    contributors may be used to endorse or promote products derived
  18  *    from this software without specific prior written permission.
  19  *
  20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  21  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  22  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  23  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  24  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  25  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  26  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  27  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  28  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  29  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  30  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  31  */
  32 package ensemble.samplepage;
  33 
  34 
  35 import javafx.beans.property.ObjectProperty;
  36 import javafx.beans.property.SimpleObjectProperty;
  37 import javafx.scene.chart.XYChart;
  38 import javafx.scene.control.TreeItem;
  39 import javafx.scene.control.TreeTableView;
  40 import ensemble.samplepage.XYDataVisualizer.XYChartItem;
  41 import java.lang.ref.WeakReference;
  42 import java.util.ArrayList;
  43 import java.util.Collection;
  44 import java.util.List;
  45 import java.util.logging.Level;
  46 import java.util.logging.Logger;
  47 import javafx.beans.WeakListener;
  48 import javafx.beans.property.SimpleStringProperty;
  49 import javafx.beans.property.StringProperty;
  50 import javafx.beans.value.ChangeListener;
  51 import javafx.beans.value.ObservableValue;
  52 import javafx.collections.FXCollections;
  53 import javafx.collections.ListChangeListener;
  54 import javafx.collections.ObservableList;
  55 import javafx.event.ActionEvent;
  56 import javafx.event.EventHandler;
  57 import javafx.scene.Node;
  58 import javafx.scene.chart.CategoryAxis;
  59 import javafx.scene.chart.NumberAxis;
  60 import javafx.scene.chart.XYChart.Data;
  61 import javafx.scene.chart.XYChart.Series;
  62 import javafx.scene.control.ContextMenu;
  63 import javafx.scene.control.MenuItem;
  64 import javafx.scene.control.TreeTableCell;
  65 import javafx.scene.control.TreeTableColumn;
  66 import javafx.scene.control.TreeTableColumn.CellDataFeatures;
  67 import javafx.scene.control.TreeTableRow;
  68 import javafx.scene.control.cell.TextFieldTreeTableCell;
  69 import javafx.scene.input.ContextMenuEvent;
  70 import javafx.util.Callback;
  71 import javafx.util.StringConverter;
  72 
  73 
  74 public class XYDataVisualizer<X, Y> extends TreeTableView<XYChartItem<X, Y>> {
  75 
  76     XYChart<X, Y> chart;
  77     private Class<?> clzX;
  78     double minY, maxY;
  79 
  80     public XYDataVisualizer(final XYChart<X, Y> chart) {
  81         this.chart = chart;
  82         setShowRoot(false);
  83         XYChartItem<X, Y> root = new XYChartItem<>(chart.getData());
  84         setRoot(new MyTreeItem(root));
  85         setMinHeight(100);
  86         setMinWidth(100);
  87 
  88         parseData();
  89 
  90         if (!getRoot().getChildren().isEmpty()) {
  91             getRoot().getChildren().get(0).setExpanded(true);
  92         }
  93 
  94         chart.dataProperty().addListener((ObservableValue<? extends ObservableList<Series<X, Y>>> ov, ObservableList<Series<X, Y>> t, ObservableList<Series<X, Y>> t1) -> {
  95             setRoot(new MyTreeItem(new XYChartItem<X, Y>(t1)));
  96         });
  97 
  98         TreeTableColumn<XYChartItem<X, Y>, String> nameColumn = new TreeTableColumn<>("Name");
  99         nameColumn.setCellValueFactory((CellDataFeatures<XYChartItem<X, Y>, String> p) -> {
 100             if (p.getValue() != null) {
 101                 return p.getValue().getValue().nameProperty();
 102             } else {
 103                 return null;
 104             }
 105         });
 106         nameColumn.setEditable(true);
 107         nameColumn.setSortable(false);
 108         nameColumn.setMinWidth(70);
 109 
 110         TreeTableColumn<XYChartItem<X, Y>, X> xValueColumn = new TreeTableColumn<>("XValue");
 111         xValueColumn.setCellValueFactory((CellDataFeatures<XYChartItem<X, Y>, X> p) -> {
 112             if (p.getValue() != null) {
 113                 return p.getValue().getValue().xValueProperty();
 114             } else {
 115                 return null;
 116             }
 117         });
 118         xValueColumn.setCellFactory((TreeTableColumn<XYChartItem<X, Y>, X> p) -> new TextFieldTreeTableCell<XYChartItem<X, Y>, X>() {
 119             {
 120                 setConverter(new StringConverter<X>() {
 121                     @Override
 122                     public String toString(X t) {
 123                         return t == null ? null : t.toString();
 124                     }
 125 
 126                     @Override
 127                     public X fromString(String string) {
 128                         if (string == null) {
 129                             return null;
 130                         }
 131                         try {
 132                             if (clzX.isAssignableFrom(String.class)) {
 133                                 return (X) string;
 134                             } else if (clzX.isAssignableFrom(Double.class)) {
 135                                 return (X) new Double(string);
 136                             } else if (clzX.isAssignableFrom(Integer.class)) {
 137                                 return (X) new Integer(string);
 138                             }
 139                         } catch (NumberFormatException ex) {
 140                             Logger.getLogger(XYDataVisualizer.class.getName()).log(Level.FINE,
 141                                     "Failed to parse {0} to type {1}", new Object[]{string, clzX});
 142                             return getItem();
 143                         }
 144                         Logger.getLogger(XYDataVisualizer.class.getName()).log(Level.FINE,
 145                                 "This valueX type is not supported: {0}", clzX);
 146                         return getItem();
 147                     }
 148                 });
 149             }
 150         });
 151         xValueColumn.setEditable(true);
 152         xValueColumn.setSortable(false);
 153         xValueColumn.setMinWidth(50);
 154 
 155         TreeTableColumn<XYChartItem<X, Y>, Y> yValueColumn = new TreeTableColumn<>("YValue");
 156         yValueColumn.setCellValueFactory((CellDataFeatures<XYChartItem<X, Y>, Y> p) -> {
 157             if (p.getValue() != null) {
 158                 return p.getValue().getValue().yValueProperty();
 159             } else {
 160                 return null;
 161             }
 162         });
 163         yValueColumn.setCellFactory((TreeTableColumn<XYChartItem<X, Y>, Y> p) -> new TextFieldTreeTableCell<>(new StringConverter<Y>() {
 164 
 165             @Override
 166             public String toString(Y t) {
 167                 return t == null ? null : t.toString();
 168             }
 169 
 170             @Override
 171             public Y fromString(String string) {
 172                 if (string == null) {
 173                     return null;
 174                 }
 175                 Y y = (Y) new Double(string);
 176                 return y;
 177             }
 178         }));
 179         yValueColumn.setEditable(true);
 180         yValueColumn.setSortable(false);
 181         yValueColumn.setMinWidth(50);
 182 
 183         TreeTableColumn<XYChartItem<X, Y>, Object> extraValueColumn = new TreeTableColumn<>("Extra Value");
 184         extraValueColumn.setCellValueFactory((CellDataFeatures<XYChartItem<X, Y>, Object> p) -> {
 185             if (p.getValue() != null) {
 186                 return p.getValue().getValue().extraValueProperty();
 187             } else {
 188                 return null;
 189             }
 190         });
 191         extraValueColumn.setMinWidth(100);
 192         extraValueColumn.setSortable(false);
 193 
 194         getColumns().setAll(nameColumn, xValueColumn, yValueColumn, extraValueColumn);
 195 
 196         setOnContextMenuRequested((ContextMenuEvent t) -> {
 197             Node node = t.getPickResult().getIntersectedNode();
 198             while (node != null && !(node instanceof TreeTableRow) && !(node instanceof TreeTableCell)) {
 199                 node = node.getParent();
 200             }
 201             if (node == null) {
 202                 getSelectionModel().clearSelection();
 203             } else if (node instanceof TreeTableCell) {
 204                 TreeTableCell tc = (TreeTableCell) node;
 205                 if (tc.getItem() == null) {
 206                     getSelectionModel().clearSelection();
 207                 } else {
 208                     getSelectionModel().select(tc.getIndex());
 209                 }
 210             } else if (node instanceof TreeTableRow) {
 211                 TreeTableRow tr = (TreeTableRow) node;
 212                 if (tr.getItem() == null) {
 213                     getSelectionModel().clearSelection();
 214                 } else {
 215                     getSelectionModel().select(tr.getIndex());
 216                 }
 217             }
 218         });
 219 
 220         MenuItem insertDataItemMenuItem = new MenuItem("Insert data item");
 221         insertDataItemMenuItem.setDisable(!isEditable());
 222         insertDataItemMenuItem.setOnAction((ActionEvent t) -> {
 223             TreeItem<XYChartItem<X, Y>> selectedItem = getSelectionModel().getSelectedItem();
 224             if (selectedItem == null || selectedItem.getParent() == null) {
 225                 return;
 226             }
 227             Object value = selectedItem.getValue().getValue();
 228             Object parentValue = selectedItem.getParent().getValue().getValue();
 229             if (value instanceof Series) {
 230                 Series series = (Series) value;
 231                 insertItem(series.getData());
 232             } else if (parentValue instanceof Series) {
 233                 Series series = (Series) parentValue;
 234                 insertItem(series.getData().indexOf(value), series.getData());
 235             }
 236         });
 237 
 238         MenuItem insertSeriesMenuitem = new MenuItem("Insert Series");
 239         insertSeriesMenuitem.setDisable(!isEditable());
 240         insertSeriesMenuitem.setOnAction((ActionEvent t) -> {
 241             TreeItem<XYChartItem<X, Y>> selectedItem = getSelectionModel().getSelectedItem();
 242             Object value = selectedItem == null ? chart.getData()
 243                     : selectedItem.getValue().getValue();
 244             if (value instanceof ObservableList) {
 245                 ObservableList parentList = (ObservableList) value;
 246                 insertSeries(parentList.size(), parentList);
 247             } else {
 248                 Object parentValue = selectedItem.getParent().getValue().getValue();
 249                 if (parentValue instanceof ObservableList) {
 250                     ObservableList parentList = (ObservableList) parentValue;
 251 
 252                     insertSeries(parentList.indexOf(value), parentList);
 253                 }
 254             }
 255         });
 256 
 257         MenuItem deleteItemMenuItem = new MenuItem("Delete item");
 258         deleteItemMenuItem.setDisable(!isEditable());
 259         deleteItemMenuItem.setOnAction((ActionEvent t) -> {
 260             TreeItem<XYChartItem<X, Y>> selectedItem = getSelectionModel().getSelectedItem();
 261             if (selectedItem == null) {
 262                 return;
 263             }
 264             Object value = selectedItem.getValue().getValue();
 265             Object parentValue = selectedItem.getParent().getValue().getValue();
 266             if (parentValue instanceof ObservableList) {
 267                 ((ObservableList) parentValue).remove(value);
 268             } else if (parentValue instanceof Series) {
 269                 ((Series) parentValue).getData().remove(value);
 270             }
 271         });
 272 
 273         MenuItem removeAllDataMenuItem = new MenuItem("Remove all data");
 274         removeAllDataMenuItem.setDisable(!isEditable());
 275         removeAllDataMenuItem.setOnAction((ActionEvent t) -> {
 276             chart.getData().clear();
 277 //            chart.setData(null);
 278         });
 279 
 280         MenuItem setNewDataMenuItem = new MenuItem("Set new data");
 281         setNewDataMenuItem.setDisable(!isEditable());
 282         setNewDataMenuItem.setOnAction((ActionEvent t) -> {
 283             chart.setData(generateData());
 284         });
 285 
 286         ContextMenu contextMenu = new ContextMenu(
 287                 insertDataItemMenuItem,
 288                 insertSeriesMenuitem,
 289                 deleteItemMenuItem,
 290                 removeAllDataMenuItem,
 291                 setNewDataMenuItem);
 292 
 293         setContextMenu(contextMenu);
 294     }
 295 
 296     private ObservableList generateData() {
 297         seriesIndex = 1;
 298         categoryIndex = 1;
 299         ObservableList newData = FXCollections.observableArrayList();
 300         for (int i = 0; i < 3; i++) {
 301             insertSeries(newData);
 302         }
 303         return newData;
 304     }
 305 
 306     private int seriesIndex = 4;
 307 
 308     private void insertSeries(ObservableList parentList) {
 309         insertSeries(parentList.size(), parentList);
 310     }
 311 
 312     private void insertSeries(int index, ObservableList parentList) {
 313         ObservableList observableArrayList = FXCollections.observableArrayList();
 314 
 315         if (chart.getXAxis() instanceof CategoryAxis) {
 316             CategoryAxis xAxis = (CategoryAxis) chart.getXAxis();
 317             if (xAxis.getCategories().isEmpty()) {
 318                 xAxis.getCategories().addAll("New category A", "New category B", "New category C");
 319             }
 320             for (String category : xAxis.getCategories()) {
 321                 observableArrayList.add(new XYChart.Data(category, Math.random() * (maxY - minY) + minY));
 322             }
 323         } else if (chart.getXAxis() instanceof NumberAxis) {
 324             NumberAxis xAxis = (NumberAxis) chart.getXAxis();
 325             double lower = xAxis.getLowerBound();
 326             double upper = xAxis.getUpperBound();
 327             double x = lower;
 328             while (x < upper - xAxis.getTickUnit()) {
 329                 x += Math.random() * xAxis.getTickUnit() * 2;
 330                 observableArrayList.add(new XYChart.Data(x, Math.random() * (maxY - minY) + minY));
 331             }
 332         }
 333 
 334         parentList.add(index < 0 ? parentList.size() : index,
 335                 new XYChart.Series<>("Series " + (seriesIndex++),
 336                 observableArrayList));
 337     }
 338 
 339     private int categoryIndex = 1;
 340 
 341     public Data<Integer, Integer> insertItem(int index, ObservableList<Data> list) {
 342         Data prev = null, next = null;
 343         if (index >= 0 && index < list.size()) {
 344             next = list.get(index);
 345         }
 346         if (index > 0) {
 347             prev = list.get(index - 1);
 348         }
 349         if (index == -1) {
 350             index = list.size();
 351         }
 352         if (chart.getXAxis() instanceof NumberAxis) {
 353             NumberAxis xAxis = (NumberAxis) chart.getXAxis();
 354             double lower = prev == null
 355                     ? xAxis.getLowerBound() - 2 * xAxis.getTickUnit()
 356                     : ((Number) prev.getXValue()).doubleValue();
 357             double upper = next == null
 358                     ? xAxis.getUpperBound() + 2 * xAxis.getTickUnit()
 359                     : ((Number) next.getXValue()).doubleValue();
 360             Data item = new XYChart.Data<>(
 361                     Math.random() * (upper - lower) + lower,
 362                     Math.random() * (maxY - minY) + minY);
 363             list.add(index, item);
 364             return item;
 365         } else if (chart.getXAxis() instanceof CategoryAxis) {
 366             CategoryAxis xAxis = (CategoryAxis) chart.getXAxis();
 367             int lower = prev == null
 368                     ? -1
 369                     : xAxis.getCategories().indexOf(prev.getXValue());
 370             int upper = next == null
 371                     ? xAxis.getCategories().size()
 372                     : xAxis.getCategories().indexOf(next.getXValue());
 373             String category;
 374             if (upper - lower <= 1) {
 375                 category = "New category " + (categoryIndex++);
 376                 xAxis.getCategories().add(upper < 0 ? 0 : upper, category);
 377             } else {
 378                 category = xAxis.getCategories().get(
 379                         (int) (Math.random() * (upper - lower - 1) + lower + 1));
 380             }
 381             Data item = new XYChart.Data<>(category,
 382                     Math.random() * (maxY - minY) + minY);
 383             list.add(index, item);
 384             return item;
 385         }
 386         return null;
 387     }
 388 
 389     public Data<Integer, Integer> insertItem(ObservableList<Data> list) {
 390         return insertItem(list.size(), list);
 391     }
 392 
 393     private void parseData() {
 394         boolean editable = true;
 395         for (Series<X, Y> series : chart.getData()) {
 396             for (XYChart.Data<X, Y> data : series.getData()) {
 397                 X x = data.getXValue();
 398                 if (x != null) {
 399                     clzX = x.getClass();
 400                 }
 401                 Y y = data.getYValue();
 402                 if (y != null) {
 403                     if (chart.getYAxis() instanceof NumberAxis) {
 404                         minY = Math.min(minY, ((Number) y).doubleValue());
 405                         maxY = Math.max(maxY, ((Number) y).doubleValue());
 406                     }
 407                 }
 408                 if (data.getExtraValue() != null) {
 409                     editable = false;
 410                 }
 411             }
 412         }
 413         if (chart.getYAxis() instanceof CategoryAxis) {
 414             editable = false;
 415         }
 416         setEditable(editable);
 417     }
 418 
 419     private static class MyTreeItem<X, Y> extends TreeItem<XYChartItem<X, Y>> {
 420 
 421         {
 422             expandedProperty().addListener((ObservableValue<? extends Boolean> ov, Boolean t, Boolean expanded) -> {
 423                 if (expanded) {
 424                     ObservableList children = getValue().getChildren();
 425                     if (children != null) {
 426                         ListContentBinding.bind(getChildren(), children, (Object p) -> new MyTreeItem(new XYDataVisualizer.XYChartItem(p), false));
 427                         if (getChildren().size() == 1) {
 428                             getChildren().get(0).setExpanded(true);
 429                         }
 430                     }
 431                 }
 432             });
 433         }
 434 
 435         @Override
 436         public boolean isLeaf() {
 437             return getValue().isLeaf();
 438         }
 439 
 440         public MyTreeItem(XYChartItem t) {
 441             this(t, true);
 442         }
 443 
 444         public MyTreeItem(XYChartItem t, boolean expand) {
 445             super(t);
 446             setExpanded(expand);
 447         }
 448     }
 449 
 450     public static class XYChartItem<X, Y> {
 451 
 452         private boolean leaf = true;
 453         private ObservableList children;
 454         private Object value;
 455 
 456         public XYChartItem(Object value) {
 457             this.value = value;
 458             if (value == null) {
 459                 return;
 460             }
 461             name.set(value.toString());
 462             if (value instanceof ObservableList) {
 463                 children = (ObservableList) value;
 464                 leaf = false;
 465             } else if (value instanceof XYChart.Series) {
 466                 XYChart.Series<X, Y> series = (XYChart.Series<X, Y>) value;
 467                 name = series.nameProperty();
 468                 children = series.getData();
 469                 leaf = false;
 470             } else if (value instanceof XYChart.Data) {
 471                 XYChart.Data<X, Y> data = (XYChart.Data<X, Y>) value;
 472                 name.set("Data");
 473                 xValue = data.XValueProperty();
 474                 yValue = data.YValueProperty();
 475                 extraValue.bindBidirectional(data.extraValueProperty());
 476             }
 477         }
 478 
 479         public ObservableList getChildren() {
 480             return children;
 481         }
 482 
 483         public boolean isLeaf() {
 484             return leaf;
 485         }
 486 
 487         private ObjectProperty<X> xValue = new SimpleObjectProperty<>();
 488 
 489         public ObjectProperty<X> xValueProperty() {
 490             return xValue;
 491         }
 492 
 493         private ObjectProperty<Y> yValue = new SimpleObjectProperty<>();
 494 
 495         public ObjectProperty<Y> yValueProperty() {
 496             return yValue;
 497         }
 498 
 499         private ObjectProperty<Object> extraValue = new SimpleObjectProperty<>();
 500 
 501         public ObjectProperty<Object> extraValueProperty() {
 502             return extraValue;
 503         }
 504 
 505         private StringProperty name = new SimpleStringProperty();
 506 
 507         public StringProperty nameProperty() {
 508             return name;
 509         }
 510 
 511         public Object getValue() {
 512             return value;
 513         }
 514     }
 515 
 516     private static class ListContentBinding<EF, ET> implements ListChangeListener<EF>, WeakListener {
 517 
 518         public static <EF, ET> Object bind(List<ET> list1, ObservableList<? extends EF> list2, Callback<EF, ET> converter) {
 519 //            checkParameters(list1, list2);
 520             final ListContentBinding<EF, ET> contentBinding = new ListContentBinding<>(list1, converter);
 521             if (list1 instanceof ObservableList) {
 522                 ((ObservableList) list1).setAll(contentBinding.convert(list2));
 523             } else {
 524                 list1.clear();
 525                 list1.addAll(contentBinding.convert(list2));
 526             }
 527             list2.addListener(contentBinding);
 528             return contentBinding;
 529         }
 530 
 531         private final WeakReference<List<ET>> listRef;
 532         private final Callback<EF, ET> converter;
 533 
 534         public ListContentBinding(List<ET> list, Callback<EF, ET> converter) {
 535             this.listRef = new WeakReference<>(list);
 536             this.converter = converter;
 537         }
 538 
 539         @Override
 540         public void onChanged(ListChangeListener.Change<? extends EF> change) {
 541             final List<ET> list = listRef.get();
 542             if (list == null) {
 543                 change.getList().removeListener(this);
 544             } else {
 545                 while (change.next()) {
 546                     if (change.wasPermutated()) {
 547                         list.subList(change.getFrom(), change.getTo()).clear();
 548                         list.addAll(change.getFrom(), convert(change.getList().subList(change.getFrom(), change.getTo())));
 549                     } else {
 550                         if (change.wasRemoved()) {
 551                             list.subList(change.getFrom(), change.getFrom() + change.getRemovedSize()).clear();
 552                         }
 553                         if (change.wasAdded()) {
 554                             list.addAll(change.getFrom(), convert(change.getAddedSubList()));
 555                         }
 556                     }
 557                 }
 558             }
 559         }
 560 
 561         @Override
 562         public boolean wasGarbageCollected() {
 563             return listRef.get() == null;
 564         }
 565 
 566         @Override
 567         public int hashCode() {
 568             final List<ET> list = listRef.get();
 569             return (list == null)? 0 : list.hashCode();
 570         }
 571 
 572         @Override
 573         public boolean equals(Object obj) {
 574             if (this == obj) {
 575                 return true;
 576             }
 577 
 578             final List<ET> list1 = listRef.get();
 579             if (list1 == null) {
 580                 return false;
 581             }
 582 
 583             if (obj instanceof ListContentBinding) {
 584                 final ListContentBinding<?, ?> other = (ListContentBinding<?, ?>) obj;
 585                 final List<?> list2 = other.listRef.get();
 586                 return list1 == list2;
 587             }
 588             return false;
 589         }
 590 
 591         private Collection<? extends ET> convert(List<? extends EF> addedSubList) {
 592             List<ET> res = new ArrayList<>(addedSubList.size());
 593 
 594             for (EF elem : addedSubList) {
 595                 res.add(converter.call(elem));
 596             }
 597 
 598             return res;
 599         }
 600     }
 601 }