Search code examples
javafxchartsjavafx-2z-index

JavaFX How make a Node to be always in front and fully visible?


This program generates a chart and displays the coordinate values when the mouse enters the plotted dots by replacing the dot for a label.

But the problem is that the label does not appear completely if the dot is in the border of the chart.

I couldn't solve this issue using the toFront() and toBack() functions.

My code was adapted from the answer to this question JavaFX LineChart Hover Values, which has the same bug.

Chart.java

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.stage.Stage;

public class Chart extends Application {

@Override
public void start(Stage stage) {

    // Random chart
    // Defining the Axis
    final NumberAxis xAxis = new NumberAxis();
    final NumberAxis yAxis = new NumberAxis();
    // Creating the chart
    LineChart<Number, Number> lineChart = new LineChart(xAxis, yAxis);

    // This didn't solve the bug
    xAxis.toBack();
    yAxis.toBack();

    // Preparing the series
    XYChart.Series series = new XYChart.Series();
    series.setName("Chart");

    for (double x = 0; x <= 10; x++) {
        double y = Math.random() * 100;
        XYChart.Data chartData;
        chartData = new XYChart.Data(x, y);
        chartData.setNode(new ShowCoordinatesNode(x, y));
        series.getData().add(chartData);
    }

    // Adding series to chart
    lineChart.getData().add(series);

    Scene scene = new Scene(lineChart, 800, 600);
    stage.setScene(scene);
    stage.show();
}

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

ShowCoordinatesNode.java

import java.text.DecimalFormat;
import javafx.event.EventHandler;
import javafx.scene.Cursor;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;

public class ShowCoordinatesNode extends StackPane {

public ShowCoordinatesNode(double x, double y) {

    final Label label = createDataThresholdLabel(x, y);

    setOnMouseEntered(new EventHandler<MouseEvent>() {
        @Override
        public void handle(MouseEvent mouseEvent) {
                setScaleX(1);
                setScaleY(1);
                getChildren().setAll(label);
                setCursor(Cursor.NONE);
                toFront(); // This didn't solve the bug
        }
    });
    setOnMouseExited(new EventHandler<MouseEvent>() {
        @Override
        public void handle(MouseEvent mouseEvent) {
                getChildren().clear();
                setCursor(Cursor.CROSSHAIR);
        }
    });
}

private Label createDataThresholdLabel(double x, double y) {
    DecimalFormat df = new DecimalFormat("0.##");
    final Label label = new Label("(" + df.format(x) + "; " + df.format(y) + ")");
    label.getStyleClass().addAll("default-color0", "chart-line-symbol", "chart-series-line");
    label.setStyle("-fx-font-size: 10; -fx-font-weight: bold;");
    label.setMinSize(Label.USE_PREF_SIZE, Label.USE_PREF_SIZE);
    label.setId("show-coord-label");
    return label;
}
}

Solution

  • I have done a some searching trying to make this work and the reason toFront doesn't work as the Javadoc states

    Moves this Node to the front of its sibling nodes in terms of z-order. This is accomplished by moving this Node to the last position in its parent's content ObservableList. This function has no effect if this Node is not part of a group.

    So I could not get that to work in any sense. This is the only solution I could come up with it involves figuring out the width/height of the label/2 and shifting the label if it on the X or Y axis so you could see it

    I made no changes in the Main other than hardcoding a testing value and removing the toFront Calls

    public class Main extends Application {
    
        @Override
        public void start(Stage stage) throws Exception{
            // Random chart
            // Defining the Axis
            final NumberAxis xAxis = new NumberAxis();
            final NumberAxis yAxis = new NumberAxis();
            // Creating the chart
            LineChart<Number, Number> lineChart = new LineChart<>(xAxis, yAxis);
    
            // Preparing the series
            XYChart.Series series = new XYChart.Series();
            series.setName("Chart");
    
            boolean firstRun = true;//First point at 0,0 to test
            for (double x = 0; x <= 10; x++) {
                double y;
                if(firstRun) {
                    y = 0.0;
                    firstRun = false;
                }else
                    y = Math.random() * 100;
                XYChart.Data chartData;
                chartData = new XYChart.Data<>(x, y);
                chartData.setNode(new ShowCoordinatesNode(x, y));
                series.getData().add(chartData);
            }
    
            // Adding series to chart
            lineChart.getData().add(series);
    
            Scene scene = new Scene(lineChart, 800, 600);
            stage.setScene(scene);
            stage.show();
        }
    
        public static void main(String[] args) { launch(args); }
    
    }
    

    Here is where I added 2 If statements to translate the label if on one of the axises

    public class ShowCoordinatesNode extends StackPane {
    
    
        public ShowCoordinatesNode(double x, double y) {
    
            final Label label = createDataThresholdLabel(x, y);
    
            setOnMouseEntered(mouseEvent -> {
                setScaleX(1);
                setScaleY(1);
                getChildren().setAll(label);
    
                if(x == 0.0) {
                    applyCss();
                    layout();
                    label.setTranslateX(label.getWidth()/2);
                }
                if(y == 0.0){
                    applyCss();
                    layout();
                    label.setTranslateY(-label.getHeight()/2);
                }
    
                setCursor(Cursor.NONE);
            });
            setOnMouseExited(mouseEvent -> {
                getChildren().clear();
                setCursor(Cursor.CROSSHAIR);
            });
        }
    
        private Label createDataThresholdLabel(double x, double y) {
            DecimalFormat df = new DecimalFormat("0.##");
            final Label label = new Label("(" + df.format(x) + "; " + df.format(y) + ")");
            label.getStyleClass().addAll("default-color0", "chart-line-symbol", "chart-series-line");
            label.setStyle("-fx-font-size: 10; -fx-font-weight: bold;");
            label.setMinSize(Label.USE_PREF_SIZE, Label.USE_PREF_SIZE);
            label.setId("show-coord-label");
            return label;
        }
    }
    

    I use the applyCSS() to calculate the size of the label before it is added to the window The JavaDoc states:

    If required, apply styles to this Node and its children, if any. This method does not normally need to be invoked directly but may be used in conjunction with Parent.layout() to size a Node before the next pulse, or if the Scene is not in a Stage.