Chart JavaFX, my hover Label are hidden by the edges of chart

1.5k views Asked by At

I have a problem again with the JavaFX Chart : D

Context :

I had Popup/Label on my chart to display the value on hover : JavaFX LineChart Hover Values (Jewelsea answer)

Problem :

But when the point are near the edges of chart, the popup is hidden by them. The chart with problem, I highlighted the edges of chart.

The chart with problem, I highlighted the edges of chart.

This is a problem, because my popup is bigger and display more informations (x value, y value and the data serie)

Possible solutions :

  • May I can check where the edges are, and if the popup is hide. In this case, I should shift the popup. But when I look doc, I didn't found the right method :

XYChart

XYChart.Data#nodeProperty

  • May I can put the popup above the chart. Like z-index in CSS.

The code :

import javafx.application.Application;
import javafx.collections.*;
import javafx.event.EventHandler;
import javafx.scene.*;
import javafx.scene.chart.*;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

/** 
 * Displays a LineChart which displays the value of a plotted Node when you hover over the Node. 
 * @author original, jewelsea https://gist.github.com/jewelsea
 */
public class LineChartWithHover extends Application {
  @SuppressWarnings("unchecked")
  @Override public void start(Stage stage) {
    final LineChart lineChart = new LineChart(
        new NumberAxis(), new NumberAxis(),
        FXCollections.observableArrayList(
            new XYChart.Series(
                "My portfolio",
                FXCollections.observableArrayList(
                    plot(0, 14, 15, 24, 34, 36, 22, 55, 43, 17, 29, 25)
                )
            )
        )
    );
    lineChart.setCursor(Cursor.CROSSHAIR);

    lineChart.setTitle("Stock Monitoring, 2013");

    stage.setScene(new Scene(lineChart, 500, 400));
    stage.show();
      System.out.println("test 1 = "+lineChart.getProperties());
  }

  /** @return plotted y values for monotonically increasing integer x values, starting from x=1 */
  public ObservableList<XYChart.Data<Integer, Integer>> plot(int... y) {
    final ObservableList<XYChart.Data<Integer, Integer>> dataset = FXCollections.observableArrayList();
    int i = 0;
    while (i < y.length) {
      final XYChart.Data<Integer, Integer> data = new XYChart.Data<>(i + 1, y[i]);
      data.setNode(
          new HoveredThresholdNode(
              (i == 0) ? 0 : y[i-1],
              y[i]
          )
      );

      dataset.add(data);
      i++;
    }

    return dataset;
  }

  /** a node which displays a value on hover, but is otherwise empty */
  class HoveredThresholdNode extends StackPane {
    HoveredThresholdNode(int priorValue, int value) {
      setPrefSize(15, 15);

      final Label label = createDataThresholdLabel(priorValue, value);

      setOnMouseEntered(new EventHandler<MouseEvent>() {
        @Override public void handle(MouseEvent mouseEvent) {
          getChildren().setAll(label);
          setCursor(Cursor.NONE);
          toFront();
        }
      });
      setOnMouseExited(new EventHandler<MouseEvent>() {
        @Override public void handle(MouseEvent mouseEvent) {
          getChildren().clear();
          setCursor(Cursor.CROSSHAIR);
        }
      });
    }

    private Label createDataThresholdLabel(int priorValue, int value) {
      final Label label = new Label(value + "");
      label.getStyleClass().addAll("default-color0", "chart-line-symbol", "chart-series-line");
      label.setStyle("-fx-font-size: 20; -fx-font-weight: bold;");

      if (priorValue == 0) {
        label.setTextFill(Color.DARKGRAY);
      } else if (value > priorValue) {
        label.setTextFill(Color.FORESTGREEN);
      } else {
        label.setTextFill(Color.FIREBRICK);
      }

      label.setMinSize(Label.USE_PREF_SIZE, Label.USE_PREF_SIZE);
      return label;
    }
  }

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

Thank you in advance ! And my apologies about my english, still learning !

2

There are 2 answers

1
pwillemet On BEST ANSWER

I have been looking on the JavaFX CSS reference guide and I could'nt find anything to simply solve your problem.

A possible solution is to translate your symbol depending on how near it is to the max or min value.

I wrote something like this, based on your code :

/**
 * Displays a LineChart which displays the value of a plotted Node when you hover over the Node.
 * @author original, jewelsea https://gist.github.com/jewelsea
 */
public class LineChartWithHover extends Application {


    @SuppressWarnings("unchecked")
    @Override public void start(Stage stage) {
        final LineChart lineChart = new LineChart(
                new NumberAxis(), new NumberAxis(),
                FXCollections.observableArrayList(
                        new XYChart.Series(
                                "My portfolio",
                                FXCollections.observableArrayList(
                                        plot(0, 14, 15, 24, 34, 36, 22, 55, 43, 17, 29, 25)
                                )
                        )
                )
        );
        lineChart.setCursor(Cursor.CROSSHAIR);

        lineChart.setTitle("Stock Monitoring, 2013");

        stage.setScene(new Scene(lineChart, 500, 400));
        stage.show();

        System.out.println("test 1 = "+lineChart.getProperties());
    }

    /** @return plotted y values for monotonically increasing integer x values, starting from x=1 */
    public ObservableList<XYChart.Data<Integer, Integer>> plot(Integer... y) {
        final ObservableList<XYChart.Data<Integer, Integer>> dataset = FXCollections.observableArrayList();
        int i = 0;
        List<Integer> list = Arrays.asList(y);
        int min = Collections.min(list);
        int max = Collections.max(list);
        int minThreshold = 5;
        int maxThreshold = 5;

        while (i < y.length) {
            final XYChart.Data<Integer, Integer> data = new XYChart.Data<>(i + 1, y[i]);
            int topMargin = 0;
            if(y[i] <= min + minThreshold) {
                topMargin = -50;
            } else if (y[i] >= max - maxThreshold) {
                topMargin = 50;
            }
            StackPane stackPane = new HoveredThresholdNode(
                    (i == 0) ? 0 : y[i-1],
                    y[i],
                    topMargin
            );

            data.setNode(stackPane);

            dataset.add(data);
            i++;
        }

        return dataset;
    }

    /** a node which displays a value on hover, but is otherwise empty */
    class HoveredThresholdNode extends StackPane {
        HoveredThresholdNode(int priorValue, int value, int topMargin) {
            setPrefSize(15, 15);

            final Label label = createDataThresholdLabel(priorValue, value);

            setOnMouseEntered(new EventHandler<MouseEvent>() {
                @Override public void handle(MouseEvent mouseEvent) {
                    getChildren().setAll(label);
                    setCursor(Cursor.NONE);
                    toFront();
                    setMargin(label, new Insets(topMargin,0,0,0));
                }
            });
            setOnMouseExited(new EventHandler<MouseEvent>() {
                @Override public void handle(MouseEvent mouseEvent) {
                    getChildren().clear();
                    setCursor(Cursor.CROSSHAIR);
                }
            });

        }

        private Label createDataThresholdLabel(int priorValue, int value) {
            final Label label = new Label(value + "");
            label.getStyleClass().addAll("default-color0", "chart-line-symbol", "chart-series-line");
            label.setStyle("-fx-font-size: 20; -fx-font-weight: bold;");

            if (priorValue == 0) {
                label.setTextFill(Color.DARKGRAY);
            } else if (value > priorValue) {
                label.setTextFill(Color.FORESTGREEN);
            } else {
                label.setTextFill(Color.FIREBRICK);
            }

            label.setMinSize(Label.USE_PREF_SIZE, Label.USE_PREF_SIZE);
            return label;
        }
    }

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

Basically, I am just saying that all values <= min+5 and >= max-5 must be translated.

The +/- 5 is arbitrary and should be calculated from the ticks gap and plot scale to have a perfect repositioning. Anyway, without performing any maths, it is still quite satisfying.

0
KenobiBastila On

Based on Mr Kwoinkwoin solution, I wrote my own. Im not sure if its possible to optimize it or improve it. But seems to be working for me so far.

import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Month;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import javafx.application.Application;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Cursor;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.NumberAxis.DefaultFormatter;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import static javafx.scene.layout.StackPane.setMargin;
import javafx.stage.Stage;
import javafx.util.StringConverter;

public class StockLineChartApp extends Application {

    private LineChart<Number, Number> chart;
    private Series<Number, Number> series;
    private NumberAxis xAxis;

    private ZonedDateTime time;

    public StockLineChartApp() {
        time = ZonedDateTime.of(LocalDateTime.of(LocalDate.of(2000, Month.JANUARY, 1), LocalTime.NOON), ZoneId.systemDefault());

    }

    public Parent createContent() {
        xAxis = new NumberAxis();
        xAxis.setLabel("Date/Time");
        xAxis.setForceZeroInRange(false);
        xAxis.setAutoRanging(true);
        xAxis.setTickLabelFormatter(new StringConverter<Number>() {
            @Override
            public String toString(Number t) {

                long longValue = t.longValue();

                ZonedDateTime zd = convertLongToZonedDateTime(longValue);
                String formatDate = formatDate(zd, "dd/MM/yyyy");

                return formatDate;
            }

            @Override
            public Number fromString(String string) {

                ZonedDateTime dl = ZonedDateTime.parse(string, DateTimeFormatter.ofPattern("dd/MM/yyyy"));
                long toEpochMilli = dl.toEpochSecond();

                //DateTimeFormatter.ofPattern(string).p
                return toEpochMilli;
            }
        });

        final NumberAxis yAxis = new NumberAxis();
        yAxis.setAutoRanging(true);
        chart = new LineChart<>(xAxis, yAxis);
        chart.setCursor(Cursor.CROSSHAIR);
        chart.setAlternativeRowFillVisible(true);
        chart.setAlternativeColumnFillVisible(true);

        // setup chart
        //final String stockLineChartCss= getClass().getResource("StockLineChart.css").toExternalForm();
        //chart.getStylesheets().add(stockLineChartCss);
        chart.setCreateSymbols(true);
        chart.setAnimated(true);
        chart.setLegendVisible(true);
        chart.setTitle("ACME Company Stock");

        yAxis.setLabel("Share Price");
        yAxis.setTickLabelFormatter(new DefaultFormatter(yAxis, "$", null));
        // add starting data
        series = new Series<>();
        series.setName("Data por Peça");

        for (double m = 0; m < (10); m++) {
            long data = nextTime();
            addData(data, (long) (Math.random() * 10));
            System.out.println(data);
        }

        //chart.
        chart.getData().add(series);
        //chart.getData().add(hourDataSeries);

        return chart;
    }

    private void addData(long x, long y) {

        Data<Number, Number> data = new Data<Number, Number>(x, y);
        series.getData().add(data);

        ZonedDateTime zd = convertLongToZonedDateTime(x);
        String formatDate = formatDate(zd, "dd/MM/yyyy");
        //String text = "(" + formatDate + ";" + y + ")";
        String text = y + "";
        if (text.length() > 4) {
            text = text.substring(0, 4);
        }

        String t = formatDate + "\nValor: " + text;
        data.setNode(new HoveredThresholdNode(t, data));
    }

    public static long convertZonedDateTimeToLong(ZonedDateTime zonedDateTime) {

        long e = zonedDateTime.toInstant().toEpochMilli();

        return e;

    }

    private long nextTime() {

        time = time.plusYears(10);
        return convertZonedDateTimeToLong(time);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {

        Parent createContent = createContent();

        //
        final StackPane pane = new StackPane();
        pane.getChildren().add(createContent);
        final Scene scene = new Scene(pane, 500, 400);
        //new ZoomManager(pane, chart, series);

        //
        primaryStage.setScene(scene);
        primaryStage.show();

    }

    public static String formatDate(ZonedDateTime ts, String format) {
        try {

            if (ts == null) {
                return "";
            }

            String format1 = ts.format(DateTimeFormatter.ofPattern(format));
            return format1;

        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return "";

    }

    public static ZonedDateTime convertLongToZonedDateTime(long e) {

        Instant i = Instant.ofEpochMilli(e);
        ZonedDateTime ofInstant = ZonedDateTime.ofInstant(i, ZoneId.systemDefault());

        return ofInstant;
    }

    /**
     * Java main for when running without JavaFX launcher
     */
    public static void main(String[] args) {
        launch(args);
    }

    public class HoveredThresholdNode extends StackPane {

        //Reference
        private Data<Number, Number> data;

        private Label label;
        private String value;

        public HoveredThresholdNode(String value, Data<Number, Number> data) {
            this.data = data;
            this.value = value;
            this.label = new Label(value);
            this.label.getStyleClass().clear();
            this.getStyleClass().clear();

            this.label.setStyle("-fx-font-size: 12; fx-text-fill: black;");
            this.label.getStyleClass().addAll("default-color0", "chart-line-symbol", "chart-series-line");

            this.label.setWrapText(true);
            this.label.setMinSize(Label.USE_PREF_SIZE, Label.USE_PREF_SIZE);

            setOnMouseEntered(new EventHandler<MouseEvent>() {
                @Override
                public void handle(MouseEvent mouseEvent) {

                    getChildren().setAll(label);

                    toFront();

                    boolean close_top = false, close_right = false, close_bottom = false, close_left = false;

                    long min_x = Long.MAX_VALUE;
                    long max_x = Long.MIN_VALUE;

                    long min_y = Long.MAX_VALUE;
                    long max_y = Long.MIN_VALUE;

                    ObservableList<Series<Number, Number>> chartSeries = chart.getData();
                    for (Series<Number, Number> s : chartSeries) {
                        ObservableList<Data<Number, Number>> chartData = s.getData();
                        for (Data<Number, Number> d : chartData) {
                            Number xValue = d.getXValue();
                            Number yValue = d.getYValue();

                            long kx = xValue.longValue();
                            long ky = yValue.longValue();

                            if (kx < min_x) {
                                min_x = kx;
                            }

                            if (kx > max_x) {
                                max_x = kx;
                            }

                            if (ky < min_y) {
                                min_y = ky;
                            }

                            if (ky > max_y) {
                                max_y = ky;
                            }

                        }

                    }

                    if (data.getXValue().longValue() - max_x == 0) {
                        close_right = true;
                    }

                    if (data.getXValue().longValue() - min_x == 0) {
                        close_left = true;
                    }

                    if (data.getYValue().longValue() - min_y == 0) {
                        close_bottom = true;
                    }

                    if (data.getYValue().longValue() - max_y == 0) {
                        close_top = true;
                    }

//                    System.out.println("\n");
//                    System.out.println("  close_right " + close_right);
//                    System.out.println("  close_left " + close_left);
//                    System.out.println("  close_bottom " + close_bottom);
//                    System.out.println("  close_top " + close_top);
                    double top = 0;
                    double right = 0;
                    double bottom = 0;
                    double left = 0;

                    if (close_top) {
                        top = 50;
                    }

                    if (close_bottom) {
                        bottom = 50;
                    }

                    if (close_right) {
                        right = 50;
                    }

                    if (close_left) {
                        left = 50;
                    }

                    setMargin(label, new Insets(top, right, bottom, left));
                }
            });

            setOnMouseExited(new EventHandler<MouseEvent>() {
                @Override
                public void handle(MouseEvent mouseEvent) {

                    getChildren().clear();
                }
            });

        }

        public HoveredThresholdNode copy() {
            HoveredThresholdNode copy = new HoveredThresholdNode(value, data);
            return copy;
        }

    }
}