/* * Copyright (c) 2012, 2014, Oracle and/or its affiliates. * All rights reserved. Use is subject to license terms. * * This file is available and licensed under the following license: * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * - Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * - Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the distribution. * - Neither the name of Oracle Corporation nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package ensemble.samplepage; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.scene.chart.XYChart; import javafx.scene.control.TreeItem; import javafx.scene.control.TreeTableView; import ensemble.samplepage.XYDataVisualizer.XYChartItem; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import javafx.beans.WeakListener; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.scene.Node; import javafx.scene.chart.CategoryAxis; import javafx.scene.chart.NumberAxis; import javafx.scene.chart.XYChart.Data; import javafx.scene.chart.XYChart.Series; import javafx.scene.control.ContextMenu; import javafx.scene.control.MenuItem; import javafx.scene.control.TreeTableCell; import javafx.scene.control.TreeTableColumn; import javafx.scene.control.TreeTableColumn.CellDataFeatures; import javafx.scene.control.TreeTableRow; import javafx.scene.control.cell.TextFieldTreeTableCell; import javafx.scene.input.ContextMenuEvent; import javafx.util.Callback; import javafx.util.StringConverter; public class XYDataVisualizer extends TreeTableView> { XYChart chart; private Class clzX; double minY, maxY; public XYDataVisualizer(final XYChart chart) { this.chart = chart; setShowRoot(false); XYChartItem root = new XYChartItem<>(chart.getData()); setRoot(new MyTreeItem(root)); setMinHeight(100); setMinWidth(100); parseData(); if (!getRoot().getChildren().isEmpty()) { getRoot().getChildren().get(0).setExpanded(true); } chart.dataProperty().addListener((ObservableValue>> ov, ObservableList> t, ObservableList> t1) -> { setRoot(new MyTreeItem(new XYChartItem(t1))); }); TreeTableColumn, String> nameColumn = new TreeTableColumn<>("Name"); nameColumn.setCellValueFactory((CellDataFeatures, String> p) -> { if (p.getValue() != null) { return p.getValue().getValue().nameProperty(); } else { return null; } }); nameColumn.setEditable(true); nameColumn.setSortable(false); nameColumn.setMinWidth(70); TreeTableColumn, X> xValueColumn = new TreeTableColumn<>("XValue"); xValueColumn.setCellValueFactory((CellDataFeatures, X> p) -> { if (p.getValue() != null) { return p.getValue().getValue().xValueProperty(); } else { return null; } }); xValueColumn.setCellFactory((TreeTableColumn, X> p) -> new TextFieldTreeTableCell, X>() { { setConverter(new StringConverter() { @Override public String toString(X t) { return t == null ? null : t.toString(); } @Override public X fromString(String string) { if (string == null) { return null; } try { if (clzX.isAssignableFrom(String.class)) { return (X) string; } else if (clzX.isAssignableFrom(Double.class)) { return (X) new Double(string); } else if (clzX.isAssignableFrom(Integer.class)) { return (X) new Integer(string); } } catch (NumberFormatException ex) { Logger.getLogger(XYDataVisualizer.class.getName()).log(Level.FINE, "Failed to parse {0} to type {1}", new Object[]{string, clzX}); return getItem(); } Logger.getLogger(XYDataVisualizer.class.getName()).log(Level.FINE, "This valueX type is not supported: {0}", clzX); return getItem(); } }); } }); xValueColumn.setEditable(true); xValueColumn.setSortable(false); xValueColumn.setMinWidth(50); TreeTableColumn, Y> yValueColumn = new TreeTableColumn<>("YValue"); yValueColumn.setCellValueFactory((CellDataFeatures, Y> p) -> { if (p.getValue() != null) { return p.getValue().getValue().yValueProperty(); } else { return null; } }); yValueColumn.setCellFactory((TreeTableColumn, Y> p) -> new TextFieldTreeTableCell<>(new StringConverter() { @Override public String toString(Y t) { return t == null ? null : t.toString(); } @Override public Y fromString(String string) { if (string == null) { return null; } Y y = (Y) new Double(string); return y; } })); yValueColumn.setEditable(true); yValueColumn.setSortable(false); yValueColumn.setMinWidth(50); TreeTableColumn, Object> extraValueColumn = new TreeTableColumn<>("Extra Value"); extraValueColumn.setCellValueFactory((CellDataFeatures, Object> p) -> { if (p.getValue() != null) { return p.getValue().getValue().extraValueProperty(); } else { return null; } }); extraValueColumn.setMinWidth(100); extraValueColumn.setSortable(false); getColumns().setAll(nameColumn, xValueColumn, yValueColumn, extraValueColumn); setOnContextMenuRequested((ContextMenuEvent t) -> { Node node = t.getPickResult().getIntersectedNode(); while (node != null && !(node instanceof TreeTableRow) && !(node instanceof TreeTableCell)) { node = node.getParent(); } if (node == null) { getSelectionModel().clearSelection(); } else if (node instanceof TreeTableCell) { TreeTableCell tc = (TreeTableCell) node; if (tc.getItem() == null) { getSelectionModel().clearSelection(); } else { getSelectionModel().select(tc.getIndex()); } } else if (node instanceof TreeTableRow) { TreeTableRow tr = (TreeTableRow) node; if (tr.getItem() == null) { getSelectionModel().clearSelection(); } else { getSelectionModel().select(tr.getIndex()); } } }); MenuItem insertDataItemMenuItem = new MenuItem("Insert data item"); insertDataItemMenuItem.setDisable(!isEditable()); insertDataItemMenuItem.setOnAction((ActionEvent t) -> { TreeItem> selectedItem = getSelectionModel().getSelectedItem(); if (selectedItem == null || selectedItem.getParent() == null) { return; } Object value = selectedItem.getValue().getValue(); Object parentValue = selectedItem.getParent().getValue().getValue(); if (value instanceof Series) { Series series = (Series) value; insertItem(series.getData()); } else if (parentValue instanceof Series) { Series series = (Series) parentValue; insertItem(series.getData().indexOf(value), series.getData()); } }); MenuItem insertSeriesMenuitem = new MenuItem("Insert Series"); insertSeriesMenuitem.setDisable(!isEditable()); insertSeriesMenuitem.setOnAction((ActionEvent t) -> { TreeItem> selectedItem = getSelectionModel().getSelectedItem(); Object value = selectedItem == null ? chart.getData() : selectedItem.getValue().getValue(); if (value instanceof ObservableList) { ObservableList parentList = (ObservableList) value; insertSeries(parentList.size(), parentList); } else { Object parentValue = selectedItem.getParent().getValue().getValue(); if (parentValue instanceof ObservableList) { ObservableList parentList = (ObservableList) parentValue; insertSeries(parentList.indexOf(value), parentList); } } }); MenuItem deleteItemMenuItem = new MenuItem("Delete item"); deleteItemMenuItem.setDisable(!isEditable()); deleteItemMenuItem.setOnAction((ActionEvent t) -> { TreeItem> selectedItem = getSelectionModel().getSelectedItem(); if (selectedItem == null) { return; } Object value = selectedItem.getValue().getValue(); Object parentValue = selectedItem.getParent().getValue().getValue(); if (parentValue instanceof ObservableList) { ((ObservableList) parentValue).remove(value); } else if (parentValue instanceof Series) { ((Series) parentValue).getData().remove(value); } }); MenuItem removeAllDataMenuItem = new MenuItem("Remove all data"); removeAllDataMenuItem.setDisable(!isEditable()); removeAllDataMenuItem.setOnAction((ActionEvent t) -> { chart.getData().clear(); // chart.setData(null); }); MenuItem setNewDataMenuItem = new MenuItem("Set new data"); setNewDataMenuItem.setDisable(!isEditable()); setNewDataMenuItem.setOnAction((ActionEvent t) -> { chart.setData(generateData()); }); ContextMenu contextMenu = new ContextMenu( insertDataItemMenuItem, insertSeriesMenuitem, deleteItemMenuItem, removeAllDataMenuItem, setNewDataMenuItem); setContextMenu(contextMenu); } private ObservableList generateData() { seriesIndex = 1; categoryIndex = 1; ObservableList newData = FXCollections.observableArrayList(); for (int i = 0; i < 3; i++) { insertSeries(newData); } return newData; } private int seriesIndex = 4; private void insertSeries(ObservableList parentList) { insertSeries(parentList.size(), parentList); } private void insertSeries(int index, ObservableList parentList) { ObservableList observableArrayList = FXCollections.observableArrayList(); if (chart.getXAxis() instanceof CategoryAxis) { CategoryAxis xAxis = (CategoryAxis) chart.getXAxis(); if (xAxis.getCategories().isEmpty()) { xAxis.getCategories().addAll("New category A", "New category B", "New category C"); } for (String category : xAxis.getCategories()) { observableArrayList.add(new XYChart.Data(category, Math.random() * (maxY - minY) + minY)); } } else if (chart.getXAxis() instanceof NumberAxis) { NumberAxis xAxis = (NumberAxis) chart.getXAxis(); double lower = xAxis.getLowerBound(); double upper = xAxis.getUpperBound(); double x = lower; while (x < upper - xAxis.getTickUnit()) { x += Math.random() * xAxis.getTickUnit() * 2; observableArrayList.add(new XYChart.Data(x, Math.random() * (maxY - minY) + minY)); } } parentList.add(index < 0 ? parentList.size() : index, new XYChart.Series<>("Series " + (seriesIndex++), observableArrayList)); } private int categoryIndex = 1; public Data insertItem(int index, ObservableList list) { Data prev = null, next = null; if (index >= 0 && index < list.size()) { next = list.get(index); } if (index > 0) { prev = list.get(index - 1); } if (index == -1) { index = list.size(); } if (chart.getXAxis() instanceof NumberAxis) { NumberAxis xAxis = (NumberAxis) chart.getXAxis(); double lower = prev == null ? xAxis.getLowerBound() - 2 * xAxis.getTickUnit() : ((Number) prev.getXValue()).doubleValue(); double upper = next == null ? xAxis.getUpperBound() + 2 * xAxis.getTickUnit() : ((Number) next.getXValue()).doubleValue(); Data item = new XYChart.Data<>( Math.random() * (upper - lower) + lower, Math.random() * (maxY - minY) + minY); list.add(index, item); return item; } else if (chart.getXAxis() instanceof CategoryAxis) { CategoryAxis xAxis = (CategoryAxis) chart.getXAxis(); int lower = prev == null ? -1 : xAxis.getCategories().indexOf(prev.getXValue()); int upper = next == null ? xAxis.getCategories().size() : xAxis.getCategories().indexOf(next.getXValue()); String category; if (upper - lower <= 1) { category = "New category " + (categoryIndex++); xAxis.getCategories().add(upper < 0 ? 0 : upper, category); } else { category = xAxis.getCategories().get( (int) (Math.random() * (upper - lower - 1) + lower + 1)); } Data item = new XYChart.Data<>(category, Math.random() * (maxY - minY) + minY); list.add(index, item); return item; } return null; } public Data insertItem(ObservableList list) { return insertItem(list.size(), list); } private void parseData() { boolean editable = true; for (Series series : chart.getData()) { for (XYChart.Data data : series.getData()) { X x = data.getXValue(); if (x != null) { clzX = x.getClass(); } Y y = data.getYValue(); if (y != null) { if (chart.getYAxis() instanceof NumberAxis) { minY = Math.min(minY, ((Number) y).doubleValue()); maxY = Math.max(maxY, ((Number) y).doubleValue()); } } if (data.getExtraValue() != null) { editable = false; } } } if (chart.getYAxis() instanceof CategoryAxis) { editable = false; } setEditable(editable); } private static class MyTreeItem extends TreeItem> { { expandedProperty().addListener((ObservableValue ov, Boolean t, Boolean expanded) -> { if (expanded) { ObservableList children = getValue().getChildren(); if (children != null) { ListContentBinding.bind(getChildren(), children, (Object p) -> new MyTreeItem(new XYDataVisualizer.XYChartItem(p), false)); if (getChildren().size() == 1) { getChildren().get(0).setExpanded(true); } } } }); } @Override public boolean isLeaf() { return getValue().isLeaf(); } public MyTreeItem(XYChartItem t) { this(t, true); } public MyTreeItem(XYChartItem t, boolean expand) { super(t); setExpanded(expand); } } public static class XYChartItem { private boolean leaf = true; private ObservableList children; private Object value; public XYChartItem(Object value) { this.value = value; if (value == null) { return; } name.set(value.toString()); if (value instanceof ObservableList) { children = (ObservableList) value; leaf = false; } else if (value instanceof XYChart.Series) { XYChart.Series series = (XYChart.Series) value; name = series.nameProperty(); children = series.getData(); leaf = false; } else if (value instanceof XYChart.Data) { XYChart.Data data = (XYChart.Data) value; name.set("Data"); xValue = data.XValueProperty(); yValue = data.YValueProperty(); extraValue.bindBidirectional(data.extraValueProperty()); } } public ObservableList getChildren() { return children; } public boolean isLeaf() { return leaf; } private ObjectProperty xValue = new SimpleObjectProperty<>(); public ObjectProperty xValueProperty() { return xValue; } private ObjectProperty yValue = new SimpleObjectProperty<>(); public ObjectProperty yValueProperty() { return yValue; } private ObjectProperty extraValue = new SimpleObjectProperty<>(); public ObjectProperty extraValueProperty() { return extraValue; } private StringProperty name = new SimpleStringProperty(); public StringProperty nameProperty() { return name; } public Object getValue() { return value; } } private static class ListContentBinding implements ListChangeListener, WeakListener { public static Object bind(List list1, ObservableList list2, Callback converter) { // checkParameters(list1, list2); final ListContentBinding contentBinding = new ListContentBinding<>(list1, converter); if (list1 instanceof ObservableList) { ((ObservableList) list1).setAll(contentBinding.convert(list2)); } else { list1.clear(); list1.addAll(contentBinding.convert(list2)); } list2.addListener(contentBinding); return contentBinding; } private final WeakReference> listRef; private final Callback converter; public ListContentBinding(List list, Callback converter) { this.listRef = new WeakReference<>(list); this.converter = converter; } @Override public void onChanged(ListChangeListener.Change change) { final List list = listRef.get(); if (list == null) { change.getList().removeListener(this); } else { while (change.next()) { if (change.wasPermutated()) { list.subList(change.getFrom(), change.getTo()).clear(); list.addAll(change.getFrom(), convert(change.getList().subList(change.getFrom(), change.getTo()))); } else { if (change.wasRemoved()) { list.subList(change.getFrom(), change.getFrom() + change.getRemovedSize()).clear(); } if (change.wasAdded()) { list.addAll(change.getFrom(), convert(change.getAddedSubList())); } } } } } @Override public boolean wasGarbageCollected() { return listRef.get() == null; } @Override public int hashCode() { final List list = listRef.get(); return (list == null)? 0 : list.hashCode(); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } final List list1 = listRef.get(); if (list1 == null) { return false; } if (obj instanceof ListContentBinding) { final ListContentBinding other = (ListContentBinding) obj; final List list2 = other.listRef.get(); return list1 == list2; } return false; } private Collection convert(List addedSubList) { List res = new ArrayList<>(addedSubList.size()); for (EF elem : addedSubList) { res.add(converter.call(elem)); } return res; } } }