javajavafxcontrolsfx

Barchart as TableRow in JavaFX


I would like to add such kind of bar chart to my application using JavaFX:

enter image description here

Essentially: A (potentially large, i.e. up to 50 entries) table. For each row there are several columns with information. One piece of information are percentages about win/draw/loss ratio, i.e. say three numbers 10%, 50%, 40%. I would like to display these three percentages graphically as a vertical bar, with three different colors. So that a user can get a visual impression of each of these percentages.

I have not found a simple or straight-forward method of doing that with JavaFX. There seems at least no control for that right now. I also could not find a control from ControlsFX that seemd suitable. What I am curently doing is having the information itself, and three columns for the percentages like this:

Option    Win   Draw    Loss
============================
option1   10%   50%     40%
option2   20%   70%     10%
option3   ...

But that's just not so nice. How can I achieve the above mentioned graphical kind of display?

(added an image for better understanding; it's from the lichess.org where they do exactly that in html)


Solution

  • This uses a combination of trashgod's and James_D's ideas:

    a TableView with a custom cell factory and graphic,

    The graphic could just be three appropriately-styled labels in a single-row grid pane with column constraints set.

    Other than that, it is a standard table view implementation.

    Numbers in my example don't always add up to 100% due to rounding, so you may wish to do something about that, if so, I leave that up to you.

    mapped

    import javafx.application.Application;
    import javafx.beans.property.*;
    import javafx.collections.*;
    import javafx.geometry.Pos;
    import javafx.scene.Scene;
    import javafx.scene.control.*;
    import javafx.scene.layout.*;
    import javafx.stage.Stage;
    
    import java.text.NumberFormat;
    import java.util.Arrays;
    
    public class ChartTableApp extends Application {
    
        private final ObservableList<Outcomes> outcomes = FXCollections.observableList(
                Arrays.asList(
                        new Outcomes("Qxd5", 5722, 5722, 3646),
                        new Outcomes("Kf6", 2727, 2262, 1597),
                        new Outcomes("c6", 11, 1, 5),
                        new Outcomes("e6", 0, 1, 1)
                )
        );
    
        public static void main(String[] args) {
            launch(args);
        }
    
        @Override
        public void start(Stage stage) {
            stage.setScene(new Scene(createOutcomesTableView()));
            stage.show();
        }
    
        private TableView<Outcomes> createOutcomesTableView() {
            final TableView<Outcomes> outcomesTable = new TableView<>(outcomes);
    
            TableColumn<Outcomes, String> moveCol = new TableColumn<>("Move");
            moveCol.setCellValueFactory(o ->
                    new SimpleStringProperty(o.getValue().move())
            );
    
            TableColumn<Outcomes, Integer> totalCol = new TableColumn<>("Total");
            totalCol.setCellValueFactory(o ->
                    new SimpleIntegerProperty(o.getValue().total()).asObject()
            );
            totalCol.setCellFactory(p ->
                    new IntegerCell()
            );
            totalCol.setStyle("-fx-alignment: BASELINE_RIGHT;");
    
            TableColumn<Outcomes, Outcomes> outcomesCol = new TableColumn<>("Outcomes");
            outcomesCol.setCellValueFactory(o ->
                    new SimpleObjectProperty<>(o.getValue())
            );
            outcomesCol.setCellFactory(p ->
                    new OutcomesCell()
            );
    
            //noinspection unchecked
            outcomesTable.getColumns().addAll(
                    moveCol,
                    totalCol,
                    outcomesCol
            );
    
            outcomesTable.setPrefSize(450, 150);
    
            return outcomesTable;
        }
    
        public record Outcomes(String move, int wins, int draws, int losses) {
            public int total() { return wins + draws + losses; }
            public double winPercent() { return percent(wins); }
            public double drawPercent() { return percent(draws); }
            public double lossPercent() { return percent(losses); }
    
            private double percent(int value) { return value * 100.0 / total(); }
        }
    
        private static class OutcomesCell extends TableCell<Outcomes, Outcomes> {
            OutcomesBar bar = new OutcomesBar();
    
            @Override
            protected void updateItem(Outcomes item, boolean empty) {
                super.updateItem(item, empty);
    
                if (empty || item == null) {
                    setText(null);
                    setGraphic(null);
                } else {
                    bar.setOutcomes(item);
                    setGraphic(bar);
                }
            }
        }
    
        private static class OutcomesBar extends GridPane {
    
            private final Label winsLabel = new Label();
            private final Label drawsLabel = new Label();
            private final Label lossesLabel = new Label();
            private final ColumnConstraints winsColConstraints = new ColumnConstraints();
            private final ColumnConstraints drawsColConstraints = new ColumnConstraints();
            private final ColumnConstraints lossesColConstraints = new ColumnConstraints();
    
            public OutcomesBar() {
                winsLabel.setStyle("-fx-background-color : lightgray");
                drawsLabel.setStyle("-fx-background-color : darkgray");
                lossesLabel.setStyle("-fx-background-color : gray");
    
                winsLabel.setAlignment(Pos.CENTER);
                drawsLabel.setAlignment(Pos.CENTER);
                lossesLabel.setAlignment(Pos.CENTER);
    
                winsLabel.setMaxWidth(Double.MAX_VALUE);
                drawsLabel.setMaxWidth(Double.MAX_VALUE);
                lossesLabel.setMaxWidth(Double.MAX_VALUE);
    
                addRow(0, winsLabel, drawsLabel, lossesLabel);
    
                getColumnConstraints().addAll(
                        winsColConstraints,
                        drawsColConstraints,
                        lossesColConstraints
                );
            }
    
            public void setOutcomes(Outcomes outcomes) {
                winsLabel.setText((int) outcomes.winPercent() + "%");
                drawsLabel.setText((int) outcomes.drawPercent() + "%");
                lossesLabel.setText((int) outcomes.lossPercent() + "%");
    
                winsColConstraints.setPercentWidth(outcomes.winPercent());
                drawsColConstraints.setPercentWidth(outcomes.drawPercent());
                lossesColConstraints.setPercentWidth(outcomes.lossPercent());
            }
        }
    
        private static class IntegerCell extends TableCell<Outcomes, Integer> {
            @Override
            protected void updateItem(Integer item, boolean empty) {
                super.updateItem(item, empty);
    
                if (empty || item == null) {
                    setText(null);
                } else {
                    setText(
                            NumberFormat.getNumberInstance().format(
                                    item
                            )
                    );
                }
            }
        }
    }