TableCell: как использовать StackedBarChart (или это невозможно)?

Триггером для моего эксперимента был недавний вопрос - одна ячейка в строке должна визуализировать относительную пропорцию значений в нескольких значениях ячеек. в том же ряду. В fx такая визуализация поддерживается в StackedBarChart (вырождается в одну категорию, а yAxis является осью категории).

К сожалению, использование такой диаграммы в качестве графики ячейки имеет странные эффекты при обновлении элемента, в зависимости от того, как мы делаем обновление:

  • сценарий A: инициализируйте диаграмму рядом и обновите данные в ряду. Полосы отображаются хорошо только при первом показе, прокрутка вперед и назад оставляет случайные «пробелы» внутри
  • сценарий B: создавать и устанавливать новые серии в каждом раунде. Полосы имеют правильную ширину, но их цвета случайным образом меняются при прокрутке.

Кроме того, есть небольшие визуальные причуды, например. не могу найти способ ограничить высоту ячейки по мере необходимости.

Вопросы:

  • как заставить его работать правильно или
  • что не так, какая часть механизма рендеринга мешает?

Пример:

import javafx.application.Application;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
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.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.stage.Stage;

/**
 * from SO, how to show relative bars with colors of 
 * a related chart
 * 
 * https://stackoverflow.com/a/28141421/203657
 * 
 * That's a solution with manually calculating and
 * filling a rectangle with base chart colors
 * 
 * Here trying to use StackedBarChart .. problems as noted in cell doc.
 * Extracted TableStackedBarChart for SO question.
 */
public class TableStackedBar extends Application {
    public static void main(String[] args) { launch(args); }

    @Override
    public void start(Stage stage) {
        ObservableList<Data> data = FXCollections.observableArrayList();
        for (int i = 0; i<10; i++) data.add(new Data());

        TableView<Data> tv = new TableView<>(data);
        TableColumn<Data, Number> col1 = new TableColumn<>("num1");
        TableColumn<Data, Number> col2 = new TableColumn<>("num2");
        col1.setCellValueFactory((p)->{return p.getValue().num1;});
        col2.setCellValueFactory((p)->{return p.getValue().num2;});

        //make this column hold the entire Data object so we can access all fields
        TableColumn<Data, Data> col3 = new TableColumn<>("bar");
        col3.setPrefWidth(500);
        col3.setCellValueFactory((p)->{return new ReadOnlyObjectWrapper<>(p.getValue());});

        col3.setCellFactory(p -> new StackedBarChartCell(2000.));
        tv.getColumns().addAll(col1,col2,col3);
        tv.setFixedCellSize(50.);

        Scene scene = new Scene(tv);

        stage.setScene(scene);
        stage.show();
    }

    /**
     * TableCell that uses a StackedBarChart to visualize relation of 
     * data.
     * 
     * Problems with updating items:
     * - scenario A: updating the series leaves empty patches horizontally
     * - scenario B: re-setting the series changes colors randomly
     * 
     * Other problems
     * - runs amok without fixedCellSize on tableView
     * - can't max the height of the chart (so it's cut-off in the middle
     */
    public static class StackedBarChartCell extends TableCell<Data, Data> {

        NumberAxis xAxisHoriz = new NumberAxis();
        CategoryAxis yAxisHoriz = new CategoryAxis();
        StackedBarChart<Number, String> sbcHoriz = new StackedBarChart<>(xAxisHoriz, yAxisHoriz);
        XYChart.Series<Number, String> series1Horiz = new XYChart.Series<>();
        XYChart.Series<Number, String> series2Horiz = new XYChart.Series<>();


        public StackedBarChartCell(double upperBound) {
            yAxisHoriz.setTickLabelsVisible(false);
            yAxisHoriz.setTickMarkVisible(false);
            yAxisHoriz.setStyle("-fx-border-color: transparent transparent transparent transparent;");

            xAxisHoriz.setTickLabelsVisible(false);
            xAxisHoriz.setTickMarkVisible(false);
            xAxisHoriz.setMinorTickVisible(false);
            xAxisHoriz.setStyle("-fx-border-color: transparent transparent transparent transparent;");
            xAxisHoriz.setAutoRanging(false);
            xAxisHoriz.setUpperBound(upperBound);
            xAxisHoriz.setLowerBound(0.);

            sbcHoriz.setHorizontalGridLinesVisible(false);
            sbcHoriz.setVerticalGridLinesVisible(false);
            sbcHoriz.setLegendVisible(false);
            sbcHoriz.setAnimated(false);

            // scenario A: set series initially
            sbcHoriz.getData().setAll(series1Horiz, series2Horiz);
            sbcHoriz.setCategoryGap(0);
            // no effect
            sbcHoriz.setMaxHeight(20);
        }
        @Override
        protected void updateItem(Data item, boolean empty) {
            super.updateItem(item, empty);
            if (empty) {
                setGraphic(null);
            } else {
                setGraphic(sbcHoriz);
                // scenario B: set new series
                // uncomment for scenario A
//                XYChart.Series<Number, String> series1Horiz = new XYChart.Series<>();
//                XYChart.Series<Number, String> series2Horiz = new XYChart.Series<>();
//                sbcHoriz.getData().setAll(series1Horiz, series2Horiz);
                //---- end of scenario B
                series1Horiz.getData().setAll(new XYChart.Data(item.num1.get(), "none"));
                series2Horiz.getData().setAll(new XYChart.Data(item.num2.get(), "none"));
            }
        }

    }


    private static class Data{
        private SimpleIntegerProperty num1 = new SimpleIntegerProperty((int)(Math.random()*1000));
        private SimpleIntegerProperty num2 = new SimpleIntegerProperty((int)(Math.random()*1000));

        public SimpleIntegerProperty num1Property(){return num1;}
        public SimpleIntegerProperty num2Property(){return num2;}
    }
}

Обновление: кажется, регресс в 8u40 - работает для 8u20/25, а не для 8u40b20. Сообщается как RT-39884


person kleopatra    schedule 26.01.2015    source источник
comment
Мои знания об этих внутренних классах всегда путаются, но, возможно, статический класс делится своими переменными между собой. Если я делаю все в updateItem, то все работает нормально. Все равно выглядит ужасно. Вы должны пройти через код диаграммы, чтобы увидеть, где установлены все размеры. Это некоторая комбинация оси и диаграммы. Диаграммы также имеют большой отступ по умолчанию.   -  person brian    schedule 26.01.2015
comment
@brian Спасибо за ваш вклад! Хотя... статические или внутренние не должны иметь значения, но тогда кажется, что это ошибка/регрессия в 8u40, так что может случиться что угодно ;-) В 8u20 все нормально - какая у вас версия/ОС?   -  person kleopatra    schedule 26.01.2015
comment
jvm - это 1.8.0_25-b18 от Oracle, но я могу использовать более раннюю версию jdk, так как я использую XP, мне нужно установить ее вручную.   -  person brian    schedule 26.01.2015
comment
@brian спасибо за информацию - укрепляет мое предположение о регрессии :)   -  person kleopatra    schedule 26.01.2015


Ответы (1)


Вот где я скопировал материал в свою CellFactory

    col3.setCellFactory((TableColumn<Data, Data> param) -> {
        return new TableCell<Data, Data>() {

            @Override
            protected void updateItem(Data item, boolean empty) {
                super.updateItem(item, empty);
                if (empty) setGraphic(null);
                else {
                    super.updateItem(item, empty);
                    if (item == null || empty) {
                        setGraphic(null);
                    } else {
                        NumberAxis xAxisHoriz = new NumberAxis(0, 2000, 1000);
                        CategoryAxis yAxisHoriz = new CategoryAxis(FXCollections.observableArrayList(""));
                        XYChart.Series<Number, String> series1Horiz = new XYChart.Series<>();
                        XYChart.Series<Number, String> series2Horiz = new XYChart.Series<>();
                        StackedBarChart<Number, String> sbcHoriz = new StackedBarChart<>(xAxisHoriz, yAxisHoriz);
                        sbcHoriz.getData().setAll(series1Horiz, series2Horiz);

                        yAxisHoriz.setStyle("-fx-border-color: transparent transparent transparent transparent;"
                                + "-fx-tick-labels-visible: false;"
                                + "-fx-tick-mark-visible: false;"
                                + "-fx-minor-tick-visible: false;"
                                + "-fx-padding: 0 0 0 0;");

                        xAxisHoriz.setStyle("-fx-border-color: transparent transparent transparent transparent;"
                                + "-fx-tick-labels-visible: false;"
                                + "-fx-tick-mark-visible: false;"
                                + "-fx-minor-tick-visible: false;"
                                + "-fx-padding: 0 0 0 0;");

                        sbcHoriz.setHorizontalGridLinesVisible(false);
                        sbcHoriz.setVerticalGridLinesVisible(false);
                        sbcHoriz.setLegendVisible(false);
                        sbcHoriz.setAnimated(false);

                        xAxisHoriz.setMaxWidth(100);
                        sbcHoriz.setMaxWidth(100);
                        sbcHoriz.setPadding(Insets.EMPTY);

                        sbcHoriz.setCategoryGap(0);
                        setGraphic(sbcHoriz);
                        series1Horiz.getData().setAll(new XYChart.Data(item.num1.get(), ""));
                        series2Horiz.getData().setAll(new XYChart.Data(item.num2.get(), ""));
                    }
                }
            }
        };
    });

а также после того, как я установил это tv.setFixedCellSize(30);

Мне также пришлось изменить ширину столбца на 200, я не могу сделать диаграмму меньше.

пример изображения

person brian    schedule 26.01.2015
comment
+1 проверено: воссоздание диаграммы каждый раз в updateItem действительно работает в 8u40b20 (хотя и неприятно, но в этом нет необходимости) - может помочь отследить причину регрессии. - person kleopatra; 26.01.2015