/* * Copyright (C) 2014 TESIS DYNAware GmbH. * All rights reserved. Use is subject to license terms. * * This file is licensed under the Eclipse Public License v1.0, which accompanies this * distribution and is available at http://www.eclipse.org/legal/epl-v10.html. */ package de.tesis.dynaware.javafx.fancychart; import java.util.ArrayList; import java.util.List; import java.util.Set; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.scene.Node; import javafx.scene.chart.LineChart; import javafx.scene.chart.NumberAxis; import javafx.scene.chart.XYChart; import javafx.scene.chart.XYChart.Data; import javafx.scene.chart.XYChart.Series; import javafx.scene.control.ColorPicker; import javafx.scene.control.Label; import javafx.scene.input.MouseEvent; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.scene.paint.Color; import javafx.stage.Popup; import de.tesis.dynaware.javafx.fancychart.data.DataItem; import de.tesis.dynaware.javafx.fancychart.data.DefaultDataSet1; import de.tesis.dynaware.javafx.fancychart.data.DefaultDataSet2; import de.tesis.dynaware.javafx.fancychart.data.DefaultDataSet3; import de.tesis.dynaware.javafx.fancychart.events.DataItemImportEvent; import de.tesis.dynaware.javafx.fancychart.events.DataItemSelectionEvent; import de.tesis.dynaware.javafx.fancychart.zoom.Zoom; /** * */ public class FancyChartController { private static final int CHART_AXIS_TICK_UNIT = 5; private static final int NUMBER_OF_DATA_SETS = 3; /** * use these default colours for the line chart when you're running JavaFX 2 (Java 7) */ private static final String CHART_SERIES_DEFAULT_COLOR_0_FX2 = "#f9d900"; private static final String CHART_SERIES_DEFAULT_COLOR_1_FX2 = "#a9e200"; private static final String CHART_SERIES_DEFAULT_COLOR_2_FX2 = "#22bad9"; /** * use these default colours for the line chart when you're running JavaFX 8 (Java 8) */ private static final String CHART_SERIES_DEFAULT_COLOR_0_FX8 = "#f3622d"; private static final String CHART_SERIES_DEFAULT_COLOR_1_FX8 = "#fba71b"; private static final String CHART_SERIES_DEFAULT_COLOR_2_FX8 = "#57b757"; private static final int DATA_POINT_POPUP_WIDTH = 30; private static final int DATA_POINT_POPUP_HEIGHT = 15; private static final int RGB_MAX = 255; private static final double REGULAR_SCALE = 0.5; private static final double SELECTED_SCALE = 1.2; private static String toRGBCode(final Color color) { return String.format("#%02X%02X%02X", (int) (color.getRed() * RGB_MAX), (int) (color.getGreen() * RGB_MAX), (int) (color.getBlue() * RGB_MAX)); } private final List<ObservableList<DataItem>> ALL_DATA_SETS = new ArrayList<>(); @FXML private StackPane rootPane; @FXML private HBox chartPage; @FXML private StackPane tabPaneContainer; @FXML private VBox chartBox; @FXML private HBox colorPickerBox; @FXML private ColorPicker colorPicker0; @FXML private ColorPicker colorPicker1; @FXML private ColorPicker colorPicker2; @FXML private TabbedTablesController tabPaneContainerController; private final ObservableList<Color> seriesColors = FXCollections.observableArrayList(); private final List<ColorPicker> colorPickers = new ArrayList<>(); private LineChart<Number, Number> chart; private StackPane chartPane; private NumberAxis xAxis; private NumberAxis yAxis; public void initialize() { initAllDataSet(); initTables(); setupColors(); setupColorPickers(); loadDefaultDataSets(); createChart(); setDataPointPopup(); initTabPane(); addZoom(); } /** * Adds zoom functionality for the line series in the chart. */ private void addZoom() { Zoom zoom = new Zoom(chart, chartPane); } /** * Initialises the tables in the tab pane. */ private void initTables() { for (int i = 0; i < ALL_DATA_SETS.size(); i++) { tabPaneContainerController.initTable(i, ALL_DATA_SETS.get(i)); } } private void setupColorPickers() { colorPickers.add(colorPicker0); colorPickers.add(colorPicker1); colorPickers.add(colorPicker2); colorPicker0.setValue(seriesColors.get(0)); colorPicker1.setValue(seriesColors.get(1)); colorPicker2.setValue(seriesColors.get(2)); } /** * Initialises the data sets with empty lists. */ private void initAllDataSet() { for (int i = 0; i < NUMBER_OF_DATA_SETS; i++) { final ObservableList<DataItem> dataItems = FXCollections.observableArrayList(); ALL_DATA_SETS.add(dataItems); } } /** * Adds a listener that updates the chart items when the a data set changes */ private void addDataItemChangeListener() { for (int i = 0; i < ALL_DATA_SETS.size(); i++) { ObservableList<DataItem> dataItems = ALL_DATA_SETS.get(i); final int index = i; dataItems.addListener(new ListChangeListener<DataItem>() { @Override public void onChanged(Change<? extends DataItem> change) { if (change.next()) { setDataItems(dataItems, index); } } }); // set initial values setDataItems(dataItems, index); } } private void setupColors() { seriesColors.add(Color.web(CHART_SERIES_DEFAULT_COLOR_0_FX8)); seriesColors.add(Color.web(CHART_SERIES_DEFAULT_COLOR_1_FX8)); seriesColors.add(Color.web(CHART_SERIES_DEFAULT_COLOR_2_FX8)); seriesColors.addListener(new ListChangeListener<Color>() { @Override public void onChanged(final Change<? extends Color> change) { change.next(); if (change.wasAdded()) { int index = change.getFrom(); final List<? extends Color> addedSubList = change.getAddedSubList(); for (final Color color : addedSubList) { final Series<Number, Number> series = chart.getData().get(index); final String newWebColor = toRGBCode(color); final String strokeStyle = "-fx-stroke: " + newWebColor + ";"; final String backgroundColorStyle = "-fx-background-color: " + newWebColor + ", white;"; // set line color series.getNode().setStyle(strokeStyle); // set data point color for (final Data<Number, Number> data : series.getData()) { data.getNode().setStyle(backgroundColorStyle); } // set legend item color final Set<Node> nodes = chart.lookupAll(".chart-legend-item-symbol.default-color" + index); for (final Node n : nodes) { n.setStyle(backgroundColorStyle); } index++; } } } }); } @FXML private void setColor(final ActionEvent event) { final Object source = event.getSource(); if (source instanceof ColorPicker) { final ColorPicker picker = (ColorPicker) source; final Color newColor = picker.getValue(); if (newColor == null) { return; } seriesColors.set(colorPickers.indexOf(picker), newColor); } } private void loadDefaultDataSets() { ALL_DATA_SETS.get(0).setAll(DefaultDataSet1.getDataItems()); ALL_DATA_SETS.get(1).setAll(DefaultDataSet2.getDataItems()); ALL_DATA_SETS.get(2).setAll(DefaultDataSet3.getDataItems()); } private void setDataItems(List<DataItem> items, int index) { Series<Number, Number> series = chart.getData().get(index); clearSeries(series); for (DataItem dataItem : items) { final Data<Number, Number> data = new XYChart.Data<Number, Number>(); data.YValueProperty().bind(dataItem.yProperty()); data.XValueProperty().bind(dataItem.xProperty()); series.getData().add(data); } } private void clearSeries(Series<Number, Number> series) { for (Data<Number, Number> data : series.getData()) { data.XValueProperty().unbind(); data.YValueProperty().unbind(); } series.getData().clear(); } private void createChart() { xAxis = new NumberAxis();// lowerBoundX, upperBoundX, CHART_AXIS_TICK_UNIT); yAxis = new NumberAxis();// lowerBoundY, upperBoundY, CHART_AXIS_TICK_UNIT); chart = new LineChart<>(xAxis, yAxis); xAxis.setLabel("X"); yAxis.setLabel("Y"); final List<XYChart.Series<Number, Number>> seriesList = createChartSeries(); chart.getData().addAll(seriesList); chartPane = new StackPane(chart); VBox.setVgrow(chartPane, Priority.ALWAYS); chartBox.getChildren().add(0, chartPane); addDataItemChangeListener(); addSelectionListener(); addDataImportListener(); } private void addDataImportListener() { rootPane.addEventHandler(DataItemImportEvent.TYPE, new EventHandler<DataItemImportEvent>() { @Override public void handle(DataItemImportEvent event) { int index = event.getDataSeriesIndex(); List<DataItem> importedDataItems = event.getImportedDataItems(); ALL_DATA_SETS.get(index).setAll(importedDataItems); setDataItems(importedDataItems, index); } }); } private void initTabPane() { tabPaneContainer.maxHeightProperty().bind(chart.heightProperty().subtract(60.0)); } private void addSelectionListener() { rootPane.addEventHandler(DataItemSelectionEvent.TYPE, new EventHandler<DataItemSelectionEvent>() { @Override public void handle(final DataItemSelectionEvent event) { final int dataSeriesIndex = event.getDataSeriesIndex(); final List<Integer> selectedIndices = event.getSelectedIndices(); setScale(dataSeriesIndex, selectedIndices); } }); } private void setScale(final int dataSeriesIndex, final List<? extends Integer> indices) { clearChartSelections(); final ObservableList<Data<Number, Number>> data = chart.getData().get(dataSeriesIndex).getData(); for (final int i : indices) { if (i < data.size()) { final Node newNode = data.get(i).getNode(); newNode.setScaleX(SELECTED_SCALE); newNode.setScaleY(SELECTED_SCALE); } } } /** * Sets all scales to regular for all data item in the chart. */ private void clearChartSelections() { for (final Series<Number, Number> series : chart.getData()) { for (final Data<Number, Number> dataItem : series.getData()) { final Node newNode = dataItem.getNode(); newNode.setScaleX(REGULAR_SCALE); newNode.setScaleY(REGULAR_SCALE); } } } private void setDataPointPopup() { final Popup popup = new Popup(); popup.setHeight(DATA_POINT_POPUP_HEIGHT); popup.setWidth(DATA_POINT_POPUP_WIDTH); for (int i = 0; i < chart.getData().size(); i++) { final int dataSeriesIndex = i; final XYChart.Series<Number, Number> series = chart.getData().get(i); for (final Data<Number, Number> data : series.getData()) { final Node node = data.getNode(); node.addEventHandler(MouseEvent.MOUSE_ENTERED_TARGET, new EventHandler<MouseEvent>() { private static final int X_OFFSET = 15; private static final int Y_OFFSET = -5; final Label label = new Label(); @Override public void handle(final MouseEvent event) { final String colorString = toRGBCode(seriesColors.get(dataSeriesIndex)); popup.getContent().setAll(label); label.setStyle("-fx-background-color: " + colorString + "; -fx-border-color: " + colorString + ";"); label.setText("x=" + data.getXValue() + ", y=" + data.getYValue()); popup.show(data.getNode().getScene().getWindow(), event.getScreenX() + X_OFFSET, event.getScreenY() + Y_OFFSET); event.consume(); } public EventHandler<MouseEvent> init() { label.getStyleClass().add("chart-popup-label"); return this; } }.init()); node.addEventHandler(MouseEvent.MOUSE_EXITED_TARGET, new EventHandler<MouseEvent>() { @Override public void handle(final MouseEvent event) { popup.hide(); event.consume(); } }); // this handler selects the corresponding table item when a data // item in the chart was clicked. node.addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() { @Override public void handle(final MouseEvent event) { final int dataItemIndex = series.getData().indexOf(data); tabPaneContainerController.selectDataItem(dataSeriesIndex, dataItemIndex); event.consume(); } }); } } } private List<XYChart.Series<Number, Number>> createChartSeries() { final List<XYChart.Series<Number, Number>> seriesList = new ArrayList<>(); for (int i = 0; i < ALL_DATA_SETS.size(); i++) { final XYChart.Series<Number, Number> series = new XYChart.Series<>(); series.setName("Data Set " + i); seriesList.add(series); } return seriesList; } }