Reputation: 170
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 ?
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>
Upvotes: 2
Views: 2743
Reputation: 3140
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.
Upvotes: 2