2

I'm a new JavaFX user, I have some issues into my internship project. I have to make a GUI which draw one spectra and add labels (x value) at the top of each peaks. The user can zoom on chart, so labels must be updated.

I use a first method to add labels based on this post :write text on a chart but i have some trouble, only half of labels are visible (see image) and the update of the labels position are not good after a zoom. I checked each labels value and position, they are good, if i apply an action on my chart, the second half of labels appear, so i don't know the reason of this misunderstanding. My update (labels position) function is a listener of the upperbound yaxis for the zoom, but to have the good positions i need to use my update button manually, i don't find the solution, if someone have ideas ? half labels

I currently use anchorpane, is it better to use stackpane?

I put a part of my code :

public class PaneController implements Initializable{
 @FXML
 private LineChart<Number, Number> lineChart;
 private ObservableList<Annotation> annotationNodes = FXCollections.observableArrayList();

 @Override
 public void initialize(URL location, ResourceBundle resources) {
  // TODO Auto-generated method stub
  lineChart.setLegendVisible(false);
  lineChart.setCreateSymbols(false);
  lineChart.setData(createData());
  //create labels
  createLabels(lineChart.getData());
  //frame listener
  ((AnchorPane)lineChart.getParent()).heightProperty().addListener((num)-> update());
     //axis listener
  ((NumberAxis)lineChart.getYAxis()).upperBoundProperty().addListener((num)-> update());
     //update listener on middle button
     lineChart.getParent().addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
         @Override
         public void handle(MouseEvent event) {
            if( event.getButton() == MouseButton.MIDDLE){
             update();
             }
         }
     });
     MyJFXChartUtil.setupZooming( lineChart );
 }
 
 /** create data for the line chart */
 public ObservableList<Series<Number,Number>> createData(){
   ObservableList<Series<Number,Number>> data = FXCollections.observableArrayList();
   for(int i=1;i<=10;i++){
    XYChart.Series<Number, Number> peakSelect = new XYChart.Series<>();
    peakSelect.getData().add(new XYChart.Data<Number, Number>(i*2,0));
    peakSelect.getData().add(new XYChart.Data<Number, Number>(i*2,i));
    data.add(peakSelect);
   }
   return data;
 }
 
   /** place a x-value label on each peak */
   private void createLabels(ObservableList<Series<Number,Number>> data){
      Pane pane = (Pane) lineChart.getParent();
      //clear old annotations
      pane.getChildren().clear();
      pane.getChildren().add(lineChart);
   for(int i=0;i<lineChart.getData().size();i++){
    for(XYChart.Data<Number, Number> value:lineChart.getData().get(i).getData()){
     if(value.getYValue().intValue()>0){
         //massAnnot
         Annotation massAnnot = new Annotation(new Label(""+value.getXValue()), value.getXValue().doubleValue(), value.getYValue().doubleValue());
            annotationNodes.add(massAnnot);
            //labelAnnot
         Annotation labelAnnot = new Annotation(new Label(""+(i+1)), value.getXValue().doubleValue(), 11);
            annotationNodes.add(labelAnnot);
              }
    }
   }
      //add node to parent
      for (Annotation annot : annotationNodes) { 
        pane.getChildren().add(annot.getLabel());
      }
   }
   
   /** update position of labels */
   private void update(){
    //clear nodes and add the linechart as children
    NumberAxis xAxis = (NumberAxis) lineChart.getXAxis();
    NumberAxis yAxis = (NumberAxis) lineChart.getYAxis();
    System.out.println(xAxis.getUpperBound()+" update "+yAxis.getUpperBound());
    Pane pane = (Pane) lineChart.getParent();
    
    pane.getChildren().clear();
    pane.getChildren().add(lineChart);
    int i = 0;
    for (Annotation annot : annotationNodes) {
     Node node = annot.getLabel();
     if(i%2==0){//massAnnot
      double x = lineChart.getXAxis().localToParent(xAxis.getDisplayPosition(annot.getX()), 0).getX() + lineChart.getPadding().getLeft();
      double y = yAxis.localToParent(0,yAxis.getDisplayPosition(annot.getY())).getY() + lineChart.getPadding().getTop();
      node.setLayoutX(x);
      node.setLayoutY(y - node.prefHeight(Integer.MAX_VALUE));
     }
     else{//labelAnnot
      double x = lineChart.getXAxis().localToParent(xAxis.getDisplayPosition(annot.getX()), 0).getX() + lineChart.getPadding().getLeft();
      double y = yAxis.localToParent(0,yAxis.getDisplayPosition(annot.getY())).getY() + lineChart.getPadding().getTop();
      node.setLayoutX(x);
      node.setLayoutY(y - node.prefHeight(Integer.MAX_VALUE));
     }
     node.autosize();
     pane.getChildren().add(node);
    }
   }
   
  class Annotation{
   private Label label;
   private double x;
   private double y;

   public Annotation(Label label, double x, double y) {
    this.label = label;
    this.x = x;
    this.y = y;
   }
   public Label getLabel(){
    return this.label;
   }
   
   public double getX(){
    return this.x;
   }
   
   public double getY(){
    return this.y;
   }
  } 
}
public class Main extends Application {
 @Override
 public void start(Stage primaryStage) {
  try {
   FXMLLoader loader = new FXMLLoader();
   loader.setLocation(Main.class.getResource("Pane.fxml"));
   AnchorPane rootPane = (AnchorPane) loader.load();
   Scene scene = new Scene(rootPane);
   primaryStage.setScene(scene);
   primaryStage.setTitle("Test");

   PaneController controller = loader.getController();
   
   primaryStage.show();
            
  } catch(Exception e) {
   e.printStackTrace();
  }
 }
 
 public static void main(String[] args) {
  launch(args);
 }
}
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.chart.*?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>

<AnchorPane xmlns="http://javafx.com/javafx/8.0.40" xmlns:fx="http://javafx.com/fxml/1" fx:controller="application.PaneController">
   <children>
      <LineChart fx:id="lineChart" prefHeight="400.0" prefWidth="500.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
        <xAxis>
          <NumberAxis side="BOTTOM" />
        </xAxis>
        <yAxis>
          <NumberAxis side="LEFT" />
        </yAxis>
      </LineChart>
   </children>
</AnchorPane>
Community
  • 1
  • 1
Thomas
  • 170
  • 1
  • 12
  • Could you please post a short example? http://sscce.org/ – J Atkin Apr 23 '15 at 12:37
  • 1
    I edit the code by a MVCE, i can't reproduce to see only half of labels, but you can test their positions, an update is automatically done at the start according to the upperbound y axis but it's wrong. If you pressed middle button mouse, you call update function and see labels move. However if you resize the frame, there is the same issue. Sorry for my bad english and thanks for your help. – Thomas Apr 23 '15 at 14:05
  • I'm confused, it looks like nothing is wrong with your code. The problem is probably around the zoom code, could you include that as well? – J Atkin Apr 23 '15 at 14:37
  • the zoom code provide from [jfxutils library](https://github.com/gillius/jfxutils), so it will be difficult to implement for an example because it needs to add library in the build path. – Thomas Apr 23 '15 at 14:47
  • i'd like to not use button to update positions of labels, i'd like that will be auto when there is a zoom or a frame resize. – Thomas Apr 23 '15 at 14:49
  • I'll see if I can include it into the code I have. If you want you can include that code and say "depends on [jfxutils](https://github.com/gillius/jfxutils)". – J Atkin Apr 23 '15 at 14:54
  • [MyJFXChartUtil class](http://textuploader.com/xvop). the code to add : MyJFXChartUtil.setupZooming( lineChart ); (at the end of initialize) the library for dependencies of the zoom function [jar file](http://www.megafileupload.com/Yx1/jfxutils-0.4-SNAPSHOT.jar) – Thomas Apr 23 '15 at 15:11
  • Great, you may want to add that to your question in case someone else sees this. – J Atkin Apr 23 '15 at 15:32
  • Ah, now I see your problem, somehow the labels aren't updating their position automatically. However the half label problem is gone. – J Atkin Apr 23 '15 at 15:42
  • yes, it's like there is a shift or a time lag between zoom and update and i don't understand why and how ?! however if you applied manually the update by the mouse,, labels position are good ! I will add zoom info to my post. Anyway thanks for you time. – Thomas Apr 23 '15 at 15:58
  • I think that update is called to soon, before the layout is applied, and then you are basing your update on old values. – J Atkin Apr 23 '15 at 16:05
  • maybe, but I checked the upperbound axis values and there were the good ones. Perhaps put the listerner update on a different property but I don't know which one. – Thomas Apr 23 '15 at 16:14
  • That is true, but the axis hasn't had a chance to update the screen location because you are using a value change listener, which is called right after it is changed, no time to layout. That is how you are getting the old values – J Atkin Apr 23 '15 at 16:21

1 Answers1

2

It seems that update is called to fast and the axis haven't updated, so when you update label positions none of the layout values have been updated. You could change your code to this:

private void update() {
    runIn(50, () -> {
        /*your current code*/
    }
}

private void runIn(int ms, Runnable callback) {
    new Thread(() -> {
        try {
            Thread.sleep(ms);
        } catch (InterruptedException e) { e.printStackTrace(); }
        Platform.runLater(callback);
    }).start();
}

This works because I guess how long the longest Pulse (one screen layout and render) will take. You can view how long pulses take by adding the -Djavafx.pulseLogger=true flag to your JVM args. Every pulse will show on the console. You are looking for this:

...
PULSE: 49 [15ms:29ms]
...

The 29ms is how long it took to layout and render the scene, since 50ms is a good bit longer, when update() ran it got the new values. However, say we have a slow machine, it may take 60ms for a pulse, to long for update() to get the proper values. You would then need to change the 50ms to say, 70ms. It would be better to get a hook on the pulse and see when it ends, then run update(). Unfortunately I have no idea if such a hook exists.

J Atkin
  • 3,080
  • 2
  • 19
  • 33