2

I add some text at the top of bars (the value of each bar). It's working but the problem is I want to remove this text each time I update the chart. In fact, text stays after updating data.

For the first chart, since there is no previous data it's displaying correctly. first chart displaying correctly without previous data

But after I update chart's data, here is what I get (it's working but previous added text remains...):

I highlighted in red the wrong part second chart displaying wrong with previous data

I add the text on top of each bar with this method:

private void displayLabelForData(XYChart.Data<String, Number> data) {
    final Node node = data.getNode();
    final Text dataText = new Text(data.getYValue() + "");
    node.parentProperty().addListener(new ChangeListener<Parent>() {
      @Override public void changed(ObservableValue<? extends Parent> ov, Parent oldParent, Parent parent) {
        Group parentGroup = (Group) parent;
        parentGroup.getChildren().add(dataText);
      }
    });

    node.boundsInParentProperty().addListener(new ChangeListener<Bounds>() {
      @Override public void changed(ObservableValue<? extends Bounds> ov, Bounds oldBounds, Bounds bounds) {
        dataText.setLayoutX(
          Math.round(
            bounds.getMinX() + bounds.getWidth() / 2 - dataText.prefWidth(-1) / 2
          )
        );
        dataText.setLayoutY(
          Math.round(
            bounds.getMinY() - dataText.prefHeight(-1) * 0.5
          )
        );
      }
    });
}

My full code is available on Gist

Still no answer to this...

Thanks !

Jérémy Halin
  • 553
  • 4
  • 29
  • Related: [how to display bar value on top of bar javafx](http://stackoverflow.com/questions/15237192/how-to-display-bar-value-on-top-of-bar-javafx) – jewelsea Dec 16 '15 at 18:11
  • 1
    @jewelsea I know it's related, I used your code to do it :) But you don't explain how to update/clean these labels, and I tried for hours to do it without success... – Jérémy Halin Dec 17 '15 at 09:40

3 Answers3

4

Personally I'd prefer subclassing BarChart and overriding the proper methods.

This is just a demo for your needs, there's refinement to be done, i. e. overriding all related methods, formatter for the text, centering the text, resize chart area to fit the added text, etc. But I guess you'll get the drift:

import java.util.HashMap;
import java.util.Map;

import javafx.application.Application;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.chart.Axis;
import javafx.scene.chart.BarChart;
import javafx.scene.chart.CategoryAxis;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.stage.Stage;

public class BarChartSample extends Application {

    final static String austria = "Austria";
    final static String brazil = "Brazil";
    final static String france = "France";
    final static String italy = "Italy";
    final static String usa = "USA";

    /**
     * Barchart with a clear button
     */
    @Override
    public void start(Stage stage) {

        final CategoryAxis xAxis = new CategoryAxis();
        final NumberAxis yAxis = new NumberAxis();
        final BarChartExt<String, Number> bc = new BarChartExt<String, Number>(xAxis, yAxis);

        bc.setTitle("Country Summary");
        xAxis.setLabel("Country");
        yAxis.setLabel("Value");

        XYChart.Series series1 = new XYChart.Series();
        series1.setName("2003");
        series1.getData().add(new XYChart.Data(austria, 25601.34));
        series1.getData().add(new XYChart.Data(brazil, 20148.82));
        series1.getData().add(new XYChart.Data(france, 10000));
        series1.getData().add(new XYChart.Data(italy, 35407.15));
        series1.getData().add(new XYChart.Data(usa, 12000));

        XYChart.Series series2 = new XYChart.Series();
        series2.setName("2004");
        series2.getData().add(new XYChart.Data(austria, 57401.85));
        series2.getData().add(new XYChart.Data(brazil, 41941.19));
        series2.getData().add(new XYChart.Data(france, 45263.37));
        series2.getData().add(new XYChart.Data(italy, 117320.16));
        series2.getData().add(new XYChart.Data(usa, 14845.27));

        XYChart.Series series3 = new XYChart.Series();
        series3.setName("2005");
        series3.getData().add(new XYChart.Data(austria, 45000.65));
        series3.getData().add(new XYChart.Data(brazil, 44835.76));
        series3.getData().add(new XYChart.Data(france, 18722.18));
        series3.getData().add(new XYChart.Data(italy, 17557.31));
        series3.getData().add(new XYChart.Data(usa, 92633.68));

        bc.getData().addAll(series1, series2, series3);

        Button clearButton = new Button("Clear");
        clearButton.setOnAction(e -> {

            bc.getData().clear();

        });

        HBox toolbar = new HBox();
        toolbar.getChildren().addAll(clearButton);

        VBox root = new VBox();
        VBox.setVgrow(bc, Priority.ALWAYS);
        root.getChildren().addAll(toolbar, bc);

        Scene scene = new Scene(root, 800, 600);

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

    /**
     * Custom barchart with text on top of bars
     *
     * @param <X>
     * @param <Y>
     */
    private static class BarChartExt<X, Y> extends BarChart<X, Y> {

        /**
         * Registry for text nodes of the bars
         */
        Map<Node, Node> nodeMap = new HashMap<>();

        public BarChartExt(Axis xAxis, Axis yAxis) {
            super(xAxis, yAxis);
        }

        /**
         * Add text for bars
         */
        @Override
        protected void seriesAdded(Series<X, Y> series, int seriesIndex) {

            super.seriesAdded(series, seriesIndex);

            for (int j = 0; j < series.getData().size(); j++) {

                Data<X, Y> item = series.getData().get(j);

                Node text = new Text(String.valueOf(item.getYValue()));
                nodeMap.put(item.getNode(), text);
                getPlotChildren().add(text);

            }

        }

        /**
         * Remove text of bars
         */
        @Override
        protected void seriesRemoved(final Series<X, Y> series) {

            for (Node bar : nodeMap.keySet()) {

                Node text = nodeMap.get(bar);
                getPlotChildren().remove(text);

            }

            nodeMap.clear();

            super.seriesRemoved(series);
        }

        /**
         * Adjust text of bars, position them on top
         */
        @Override
        protected void layoutPlotChildren() {

            super.layoutPlotChildren();

            for (Node bar : nodeMap.keySet()) {

                Node text = nodeMap.get(bar);

                text.relocate(bar.getBoundsInParent().getMinX(), bar.getBoundsInParent().getMinY() - 30);

            }

        }
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Screenshot:

enter image description here

Roland
  • 18,114
  • 12
  • 62
  • 93
  • There is not text on top of bars... In `seriesAdded(Series series, int seriesIndex)`, `series.getData().size() = 0` – Jérémy Halin Jan 06 '16 at 11:03
  • What do you mean? I added a screenshot, I see the text. This is just a draft by the way, implementing it properly requires more code and effort. It's a suggestion, I don't like the eventing solution since it can become complicated real quick as you've seen. And usually you require more additional stuff like markers and what not, so subclassing the bar chart class seems the proper way to me. But it's your decision. I can delete the answer again if it isn't what you're searching for. – Roland Jan 06 '16 at 11:23
  • Your solution is great, I'm trying to implement it. But for now I don't see any values on top of bars, surely by my fault. I'm trying to find why. – Jérémy Halin Jan 06 '16 at 12:01
  • If you simply copy/paste the code I posted and execute it, you should see them. I used Java 8u65 on windows by the way. Oh, and taking an (unrelated) peek at [my post here](http://stackoverflow.com/questions/27975898/gantt-chart-from-scratch) may also give you an idea. – Roland Jan 06 '16 at 12:06
  • I can't just copy/paste because I'm in a Swing application. – Jérémy Halin Jan 06 '16 at 12:41
  • That shouldn't matter if you embed the code in a swing application via jfxpanel. If you have problems with that, let me know. – Roland Jan 06 '16 at 23:23
3

Thanks to Roland I was able to create the following chart with fixed alignments. This might be helpful for someone due the lack of proposed solutions for this problem.

enter image description here

public class CustomBarChart<X, Y> extends BarChart<X, Y> {

    Map<Node, TextFlow> nodeMap = new HashMap<>();

    public CustomBarChart(Axis xAxis, Axis yAxis) {
        super(xAxis, yAxis);
        this.setBarGap(0.0);
    }

    @Override
    protected void seriesAdded(Series<X, Y> series, int seriesIndex) {

        super.seriesAdded(series, seriesIndex);

        for (int j = 0; j < series.getData().size(); j++) {

            Data<X, Y> item = series.getData().get(j);

            Text text = new Text(item.getYValue().toString());
            text.setStyle("-fx-font-size: 10pt;");

            TextFlow textFlow = new TextFlow(text);
            textFlow.setTextAlignment(TextAlignment.CENTER);

            nodeMap.put(item.getNode(), textFlow);
            this.getPlotChildren().add(textFlow);

        }

    }

    @Override
    protected void seriesRemoved(final Series<X, Y> series) {

        for (Node bar : nodeMap.keySet()) {

            Node text = nodeMap.get(bar);
            this.getPlotChildren().remove(text);

        }

        nodeMap.clear();

        super.seriesRemoved(series);
    }

    @Override
    protected void layoutPlotChildren() {

        super.layoutPlotChildren();

        for (Node bar : nodeMap.keySet()) {

            TextFlow textFlow = nodeMap.get(bar);

            if (bar.getBoundsInParent().getHeight() > 30) {
                ((Text) textFlow.getChildren().get(0)).setFill(Color.WHITE);
                textFlow.resize(bar.getBoundsInParent().getWidth(), 200);
                textFlow.relocate(bar.getBoundsInParent().getMinX(), bar.getBoundsInParent().getMinY() + 10);
            } else {
                ((Text) textFlow.getChildren().get(0)).setFill(Color.GRAY);
                textFlow.resize(bar.getBoundsInParent().getWidth(), 200);
                textFlow.relocate(bar.getBoundsInParent().getMinX(), bar.getBoundsInParent().getMinY() - 20);
            }
        }
    }
}
cmtjk
  • 359
  • 2
  • 12
0

I have found only one solution, which is to clear every parentNode of each Data when you receive an update. You'll reconstruct the whole series then. As follow, it works but I didn't check the performance. On my environment it's pretty fast :

ObservableList<Series<String, Number>> allSeries = yourBarChart.getData();
    if (updateReceived) {
        for (XYChart.Series<String, Number> series : allSeries) {
            for (XYChart.Data<String, Number> data : series.getData()) {
                Node node = data.getNode();
                Parent parent = node.parentProperty().get();
                if (parent != null && parent instanceof Group) {
                    Group group = (Group) parent;
                    group.getChildren().clear();
                }
            }
        }
        allSeries.clear();
    }
A.LnG
  • 23
  • 1
  • 6
  • If anyone has an idea on how to position the Text for a negative Y bar, I cannot figure it out ... even when I modify the layoutY for dataText when my Yvalue is negative – A.LnG Jan 05 '16 at 17:43
  • Found it, I just modified the boundsInParentProperty as follow : double yBound = 0D; if(data.getYValue().doubleValue() < 0D) { yBound = Math.round(bounds.getMaxY() + dataText.prefHeight(-1) * 1.5); } else { yBound = Math.round(bounds.getMinY() - dataText.prefHeight(-1) * 0.5); } dataText.setLayoutY(yBound); – A.LnG Jan 05 '16 at 17:54