Saturday, March 2, 2013

JavaFX 2: Update StackedBarChart dynamically, with TableView.

In the former article "Create dynamic StackedBarChart of JavaFX 2" demonstrate how to update StackedBarChart dynamically, with TableView. BUT it's not a good example! Both the TableView and one data series of the StackedBarChart work with the same ObservableList<XYChart.Data>, only one data series of the StackedBarChart can be modified with the TableView. And hard to extend to update more data series.

Last article demonstrate how to implement "Editable multi-column TableView". It's modified to work with StackedBarChart, all the data series in StackedBarChart can be changed with the TableView.


Instead of share a common ObservableList<XYChart.Data>, a class MyList is implemented. It hold a ObservableList<Record> for TableView, and two ObservableList<XYChart.Data> for StackedBarChart, and also keep their content consistency.

Once editing of the table committed, the handler update the ObservableList<Record> for TableView, and also the corresponding ObservableList<XYChart.Data> for StackedBarChart.

package javafxmulticolumnchart;

import java.util.Arrays;
import java.util.List;
import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.chart.CategoryAxis;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.StackedBarChart;
import javafx.scene.chart.XYChart;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import javafx.util.Callback;

/**
 *
 * @web http://java-buddy.blogspot.com/
 */
public class JavaFXMultiColumnChart extends Application {
    
    public class Record{
        private SimpleStringProperty fieldMonth;
        private SimpleDoubleProperty fieldValue1;
        private SimpleDoubleProperty fieldValue2;
        
        Record(String fMonth, double fValue1, double fValue2){
            this.fieldMonth = new SimpleStringProperty(fMonth);
            this.fieldValue1 = new SimpleDoubleProperty(fValue1);
            this.fieldValue2 = new SimpleDoubleProperty(fValue2);
        }
        
        public String getFieldMonth() {
            return fieldMonth.get();
        }
        
        public double getFieldValue1() {
            return fieldValue1.get();
        }
        
        public double getFieldValue2() {
            return fieldValue2.get();
        }
        
        public void setFieldMonth(String fMonth) {
            fieldMonth.set(fMonth);
        }
     
        public void setFieldValue1(Double fValue1) {
            fieldValue1.set(fValue1);
        }
        
        public void setFieldValue2(Double fValue2) {
            fieldValue2.set(fValue2);
        }
    }
    
    class MyList {
        
        ObservableList<Record> dataList;
        ObservableList<XYChart.Data> xyList1;
        ObservableList<XYChart.Data> xyList2;
        
        MyList(){
            dataList = FXCollections.observableArrayList();
            xyList1 = FXCollections.observableArrayList();
            xyList2 = FXCollections.observableArrayList();
        }
        
        public void add(Record r){
            dataList.add(r);
            xyList1.add(new XYChart.Data(r.getFieldMonth(), r.getFieldValue1()));
            xyList2.add(new XYChart.Data(r.getFieldMonth(), r.getFieldValue2()));
        }
        
        public void update1(int pos, Double val){
            xyList1.set(pos, new XYChart.Data(xyList1.get(pos).getXValue(), val));
        }
        
        public void update2(int pos, Double val){
            xyList2.set(pos, new XYChart.Data(xyList2.get(pos).getXValue(), val));
        }
    }
    
    MyList myList;
    
    private TableView<Record> tableView = new TableView<>();
    
    public static void main(String[] args) {
        launch(args);
    }
    
    @Override
    public void start(Stage primaryStage) {
        primaryStage.setTitle("java-buddy.blogspot.com");
        
        //prepare myList
        myList = new MyList();
        myList.add(new Record("January", 100, 120));
        myList.add(new Record("February", 200, 210));
        myList.add(new Record("March", 50, 70));
        myList.add(new Record("April", 75, 50));
        myList.add(new Record("May", 110, 120));
        myList.add(new Record("June", 300, 200));
        myList.add(new Record("July", 111, 100));
        myList.add(new Record("August", 30, 50));
        myList.add(new Record("September", 75, 70));
        myList.add(new Record("October", 55, 50));
        myList.add(new Record("November", 225, 225));
        myList.add(new Record("December", 99, 100));
        
        Group root = new Group();
        
        tableView.setEditable(true);

        Callback<TableColumn, TableCell> cellFactory = 
                new Callback<TableColumn, TableCell>() {
                          
                          @Override
                          public TableCell call(TableColumn p) {
                              return new EditingCell();
                          }
                      };
        
        TableColumn columnMonth = new TableColumn("Month");
        columnMonth.setCellValueFactory(
                new PropertyValueFactory<Record,String>("fieldMonth"));
        columnMonth.setMinWidth(60);
        
        TableColumn columnValue1 = new TableColumn("Value 1");
        columnValue1.setCellValueFactory(
                new PropertyValueFactory<Record,Double>("fieldValue1"));
        columnValue1.setMinWidth(60);
        
        TableColumn columnValue2 = new TableColumn("Value 2");
        columnValue2.setCellValueFactory(
                new PropertyValueFactory<Record,Double>("fieldValue2"));
        columnValue2.setMinWidth(60);
     
        //--- Add for Editable Cell of Value field, in Double
        columnValue1.setCellFactory(cellFactory);
        columnValue1.setOnEditCommit(
                new EventHandler<TableColumn.CellEditEvent<Record, Double>>() {
                    
                    @Override public void handle(TableColumn.CellEditEvent<Record, Double> t) {
                        ((Record)t.getTableView().getItems().get(
                                t.getTablePosition().getRow())).setFieldValue1(t.getNewValue());
                        
                        int pos = t.getTablePosition().getRow();
                        myList.update1(pos, t.getNewValue());
                    }
                });
        
        columnValue2.setCellFactory(cellFactory);
        columnValue2.setOnEditCommit(
                new EventHandler<TableColumn.CellEditEvent<Record, Double>>() {
                    
                    @Override public void handle(TableColumn.CellEditEvent<Record, Double> t) {
                        ((Record)t.getTableView()
                                .getItems()
                                .get(t.getTablePosition().getRow())).setFieldValue2(t.getNewValue());
                        
                        int pos = t.getTablePosition().getRow();
                        myList.update2(pos, t.getNewValue());
                    }
                });
      
        //--- Prepare StackedBarChart
        
        List<String> monthLabels = Arrays.asList(
                "January", 
                "February",
                "March",
                "April",
                "May",
                "June",
                "July",
                "August",
                "September",
                "October",
                "November",
                "December");
                
        final CategoryAxis xAxis1 = new CategoryAxis();
        final NumberAxis yAxis1 = new NumberAxis();
        xAxis1.setLabel("Month");
        xAxis1.setCategories(FXCollections.<String> observableArrayList(monthLabels));
        yAxis1.setLabel("Value 1");
        XYChart.Series XYSeries1 = new XYChart.Series(myList.xyList1);
        XYSeries1.setName("XYChart.Series 1");
        
        final CategoryAxis xAxis2 = new CategoryAxis();
        final NumberAxis yAxis2 = new NumberAxis();
        xAxis2.setLabel("Month");
        xAxis2.setCategories(FXCollections.<String> observableArrayList(monthLabels));
        yAxis2.setLabel("Value 2");
        XYChart.Series XYSeries2 = new XYChart.Series(myList.xyList2);
        XYSeries2.setName("XYChart.Series 2");
        
        final StackedBarChart<String,Number> stackedBarChart = new StackedBarChart<>(xAxis1,yAxis1);
        stackedBarChart.setTitle("StackedBarChart");
        stackedBarChart.getData().addAll(XYSeries1, XYSeries2);
        //---
        tableView.setItems(myList.dataList);
        tableView.getColumns().addAll(columnMonth, columnValue1, columnValue2);
        
        HBox hBox = new HBox();
        hBox.setSpacing(10);
        hBox.getChildren().addAll(tableView, stackedBarChart);
 
        root.getChildren().add(hBox);
     
      primaryStage.setScene(new Scene(root, 750, 400));
      primaryStage.show();
    }

    class EditingCell extends TableCell<Record, Double> {
        private TextField textField;
        
        public EditingCell() {}
        
        @Override
        public void startEdit() {
            super.startEdit();
            
            if (textField == null) {
                createTextField();
            }
            
            setGraphic(textField);
            setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
            textField.selectAll();
        }
        
        @Override
        public void cancelEdit() {
            super.cancelEdit();
            
            setText(String.valueOf(getItem()));
            setContentDisplay(ContentDisplay.TEXT_ONLY);
        }
        
        @Override
        public void updateItem(Double item, boolean empty) {
            super.updateItem(item, empty);
         
            if (empty) {
                setText(null);
                setGraphic(null);
            } else {
                if (isEditing()) {
                    if (textField != null) {
                        textField.setText(getString());
                    }
                    
                    setGraphic(textField);
                    setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
                } else {
                    setText(getString());
                    setContentDisplay(ContentDisplay.TEXT_ONLY);
                }
            }
        }
        
        private void createTextField() {
            textField = new TextField(getString());
            textField.setMinWidth(this.getWidth() - this.getGraphicTextGap()*2);
            textField.setOnKeyPressed(new EventHandler<KeyEvent>() {
             
                @Override
                public void handle(KeyEvent t) {
                    if (t.getCode() == KeyCode.ENTER) {
                        commitEdit(Double.parseDouble(textField.getText()));
                    } else if (t.getCode() == KeyCode.ESCAPE) {
                        cancelEdit();
                    }
                }
            });
        }
     
        private String getString() {
            return getItem() == null ? "" : getItem().toString();
        }
    }
    
}


Another example to make PieChart and LineChart update dynamically with TableView.


5 comments:

  1. n the below example i have 3 series Region 1,2 & 3.. i want these series also to be dyanamic what ive done is used a mysql query using the DISTINCT CLAUSE got all the values of the attribute now depending on how many distinct values i have i ned that many Series. n then the values for these Series again needs an invocation of the Mysql query.

    public class StackedBarChartSmple extends Application {

    private void init(Stage primaryStage) {
    Group root = new Group();
    primaryStage.setScene(new Scene(root));
    String[] years = {"2007", "2008", "2009"};
    CategoryAxis xAxis = CategoryAxisBuilder.create()
    .categories(FXCollections.observableArrayList(years)).build();
    NumberAxis yAxis = NumberAxisBuilder.create()
    .label("Units Sold")
    .lowerBound(0.0d)
    .upperBound(10000.0d)
    .tickUnit(1000.0d).build();
    ObservableList barChartData = FXCollections.observableArrayList(
    new StackedBarChart.Series("Region 1", FXCollections.observableArrayList(
    new StackedBarChart.Data(years[0], 567d),
    new StackedBarChart.Data(years[1], 1292d),
    new StackedBarChart.Data(years[2], 1292d)
    )),
    new StackedBarChart.Series("Region 2", FXCollections.observableArrayList(
    new StackedBarChart.Data(years[0], 956),
    new StackedBarChart.Data(years[1], 1665),
    new StackedBarChart.Data(years[2], 2559)
    )),
    new StackedBarChart.Series("Region 3", FXCollections.observableArrayList(
    new StackedBarChart.Data(years[0], 1154),
    new StackedBarChart.Data(years[1], 1927),
    new StackedBarChart.Data(years[2], 2774)
    ))
    );

    StackedBarChart chart = new StackedBarChart(xAxis, yAxis, barChartData, 25.0d);
    root.getChildren().add(chart);
    }

    @Override public void start(Stage primaryStage) throws Exception {
    init(primaryStage);
    primaryStage.show();
    }
    public static void main(String[] args) { launch(args); }
    }

    ReplyDelete
  2. The main problem is I want the "series" to be Dynamic.For e.g in my proposed Systwm if the user select location then let say 7 series should be generated ,if he selects "sub - practice" then 5 series have to be generated and then i should be able to populate this series via SQL I have done something very similar using pie and bar but i can't figure out this one

    ReplyDelete
  3. This code doesn't work adds bars with a gap in the middle

    ReplyDelete
  4. I am not sure why the stackedbarchart is not updated properly when i try it. I'm using JRE 1.8.0_20

    ReplyDelete
  5. There's way way too much code in this for what you are trying to achieve. First of all, you can use a TextFieldTableCell instead of your home-grown TableCell implementation. If you additionally use the JavaFX Properties pattern correctly (i.e. put the proper xxxProperty() methods in your Record class), then you can completely eliminate all the wiring between the cell editor and the data.

    Additionally, if you use Tomas Mikula's excellent EasyBind framework (https://github.com/TomasMikula/EasyBind) (this is scheduled to be part of JavaFX in Java 9) then you can simply use EasyBind.map(...) to create the XYChart.Data lists from the list used for the table, and you can eliminate the MyList class and all the "updating by hand".

    ReplyDelete