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 }