JavaFX PiChart, my hover values blink

1.7k views Asked by At

Context :

Hi ! I'm trying to create a little popup which display the value of slice when mouse hover, on my PieChart (with JavaFX).

I successed on my LineChart, AreaChart etc.. Thanks this post : JavaFX LineChart Hover Values (thank you so much Jewelsea for your help).

Problem (1/2) :

But with the PieChart, I have a problem : The popup is blinking oO

enter image description here

My code :

With syntactic color : https://bpaste.net/show/12838ad6b2e2

import java.util.ArrayList;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.event.EventHandler;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.chart.PieChart;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import com.alpha.client.view.nodes.stats.statsEngine.beans.ListRepere;
import com.alpha.client.view.nodes.stats.statsEngine.beans.OptionsChart;
import com.alpha.client.view.nodes.stats.statsEngine.beans.ValueStat;

/**
 *
 * @author Zombkey
 */
public class PieChartNode implements ChartNode {

//My personnal attributes
private ListRepere categories;
private ArrayList<ValueStat> values;
//The PieChart
private PieChart chart;

//The data of Chart, will be fill by a thread
private ObservableList<PieChart.Data> pieChartData;
//The node which contain chart and label
private Group group;
//The Label 
private final Label caption;

public PieChartNode(ListRepere categories, ArrayList<ValueStat> values, OptionsChart optionsChart) {
    this.categories = categories;
    this.values = values;

    //New Group
    group = new Group();
    //I must use a StackPane to place Label hover Chart
    StackPane pane = new StackPane();
    group.getChildren().add(pane);

    //Init' PieChart
    pieChartData = FXCollections.observableArrayList();
    chart = new PieChart(pieChartData);
    chart.setStartAngle(180.0);
    //Add chart to StackPane
    pane.getChildren().add(chart);

    //Init Popup(Label)
    caption = new Label("");
    caption.setVisible(false);
    caption.getStyleClass().addAll("chart-line-symbol", "chart-series-line");
    caption.setStyle("-fx-font-size: 12; -fx-font-weight: bold;");
    caption.setMinSize(Label.USE_PREF_SIZE, Label.USE_PREF_SIZE);
    //Add Label to StackPane
    pane.getChildren().add(caption);
}

@Override
public Node getNodeGraph() {
    return (Node) group;
}

@Override
public Task initTaskFormat() {
    Task<Void> task = new Task<Void>() {
        @Override
        protected Void call() throws Exception {
            //i and sizeOfallElements are just use for ProgressBar 
            int i = 0;
            int sizeOfallElements = values.size();
            updateProgress(i, sizeOfallElements);

            //For Each ValueStat (a Personnal pojo Class), I must create a slice 
            for (ValueStat v : values) {
                //Create the PieChart.Data and add it to ObservableList
                PieChart.Data dataTemp = new PieChart.Data(v.getCategorie().getStringName(), v.getDoubleValue());
                pieChartData.add(dataTemp);

                //HERE, the interessante code !
                //At the same way that the LineChart, I add Event when mouse entered and mouse exited.
                //When mouse entered (on the slice of PieChart)
                dataTemp.getNode().addEventHandler(MouseEvent.MOUSE_ENTERED,
                        new EventHandler<MouseEvent>() {
                            @Override
                            public void handle(MouseEvent e) {
                                System.out.println("MOUSE_ENTERED : "+dataTemp.getName());
                                //I display Label
                                caption.setVisible(true);
                                //I move Label near the mouse cursor
                                caption.setTranslateX(e.getX());
                                caption.setTranslateY(e.getY());
                                //I hide the mouse cursor
                                dataTemp.getNode().setCursor(Cursor.NONE);
                                //I change text of Label
                                caption.setText(String.valueOf(dataTemp.getPieValue()) + "\n" + dataTemp.getName());
                                //I try to change the frame color of Label
                                caption.getStyleClass().add(dataTemp.getNode().getStyleClass().get(2));
                            }
                        });

                //When mouse exited (the slice of PieChart)
                dataTemp.getNode().addEventHandler(MouseEvent.MOUSE_EXITED,
                        new EventHandler<MouseEvent>() {
                            @Override
                            public void handle(MouseEvent e) {
                                System.out.println("MOUSE_EXITED : "+dataTemp.getName());
                                //I Hide Label
                                caption.setVisible(false);
                                //I show the mouse cursor
                                dataTemp.getNode().setCursor(Cursor.DEFAULT);
                            }
                        });
                //Update progress
                updateProgress(i++, sizeOfallElements);
            }
            return null;
        }
    };
    return task;
}
}

Problem (2/2) :

The problem is that the events (MOUSE_ENTERED and MOUSE_EXITED) are emitted, too often instead of once.

Ex : I just put in, then put off, my mouse hover a slice. Here the result on console :

MOUSE_ENTERED : BC
MOUSE_EXITED : BC
MOUSE_ENTERED : BC
MOUSE_EXITED : BC
MOUSE_ENTERED : BC
MOUSE_EXITED : BC
MOUSE_ENTERED : BC
MOUSE_EXITED : BC

Anyone know why the event bug ?

Thanks : )

3

There are 3 answers

2
maskacovnik On BEST ANSWER

It not the blinking effect caused by label?
When you shows the label, it means that you exited the node which is listened. This causes hiding the label. When label disappears, it fires the mouse entered event on the node, it shows the label etc.
Not tested, just an idea.


EDIT:
If I am right, try to avoid putting label under the mouse pointer:

caption.setTranslateX(e.getX()+10);
caption.setTranslateY(e.getY()+10);

For example (10 is a magic number, depends on insets etc.)

1
Zombkey On

Thanks all for your help.

@maskacovnik to find the problem, @James_D to find a cool solution, and @ItachiUchiha to put my image on my post : D

Now, my new code.

import java.util.ArrayList;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.event.EventHandler;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.chart.PieChart;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import com.alpha.client.view.nodes.stats.statsEngine.beans.ListRepere;
import com.alpha.client.view.nodes.stats.statsEngine.beans.OptionsChart;
import com.alpha.client.view.nodes.stats.statsEngine.beans.ValueStat;

public class PieChartNode implements ChartNode {

    //My personnal attributes
    private ListRepere categories;
    private ArrayList<ValueStat> values;
    //The PieChart
    private PieChart chart;

    //The data of Chart, will be fill by a thread
    private ObservableList<PieChart.Data> pieChartData;
    //The node which contain chart and label
    private Group group;
    //The Label 
    private final Label caption;

    public PieChartNode(ListRepere categories, ArrayList<ValueStat> values, OptionsChart optionsChart) {
        this.categories = categories;
        this.values = values;

        //New Group
        group = new Group();
        //I must use a StackPane to place Label hover Chart
        StackPane pane = new StackPane();
        group.getChildren().add(pane);

        //Init' PieChart
        pieChartData = FXCollections.observableArrayList();
        chart = new PieChart(pieChartData);
        chart.setStartAngle(180.0);
        //Add chart to StackPane
        pane.getChildren().add(chart);

        //Init Popup(Label)
        caption = new Label("");
        caption.setVisible(false);
        caption.getStyleClass().addAll("chart-line-symbol", "chart-series-line");
        caption.setStyle("-fx-font-size: 12; -fx-font-weight: bold;");
        caption.setMinSize(Label.USE_PREF_SIZE, Label.USE_PREF_SIZE);
        //Add Label to StackPane
        pane.getChildren().add(caption);
    }

    @Override
    public Node getNodeGraph() {
        return (Node) group;
    }

    @Override
    public Task initTaskFormat() {
        Task<Void> task = new Task<Void>() {
            @Override
            protected Void call() throws Exception {
                //i and sizeOfallElements are just use for ProgressBar 
                int i = 0;
                int sizeOfallElements = values.size();
                updateProgress(i, sizeOfallElements);

                //For Each ValueStat (a Personnal pojo Class), I must create a slice 
                for (ValueStat v : values) {
                    //Create the PieChart.Data and add it to ObservableList
                    PieChart.Data dataTemp = new PieChart.Data(v.getCategorie().getStringName(), v.getDoubleValue());
                    pieChartData.add(dataTemp);

                    //At the same way that the LineChart, I add Event when mouse entered and mouse exited.
                    //When mouse entered (on the slice of PieChart)
                    dataTemp.getNode().addEventHandler(MouseEvent.MOUSE_ENTERED,
                            new EventHandler<MouseEvent>() {
                                @Override
                                public void handle(MouseEvent e) {
                                    //Set Label ignores the mouse 
                                    caption.setMouseTransparent(true);
                                    //I move Label near the mouse cursor, with a offset !
                                    caption.setTranslateX(e.getX());
                                    caption.setTranslateY(e.getY()+20);
                                    //I change text of Label
                                    caption.setText(String.valueOf(dataTemp.getPieValue()) + "\n" + dataTemp.getName());
                                    //Change the color of popup, to adapt it to slice
                                    if(caption.getStyleClass().size() == 4){
                                        caption.getStyleClass().remove(3);
                                    }
                                    caption.getStyleClass().add(dataTemp.getNode().getStyleClass().get(2));
                                    //I display Label
                                    caption.setVisible(true);
                                }
                            });
                    //Need to add a event when the mouse move hover the slice
                    //If I don't the popup stay blocked on edges of the slice.
                    dataTemp.getNode().addEventHandler(MouseEvent.MOUSE_MOVED,
                            new EventHandler<MouseEvent>() {
                                @Override
                                public void handle(MouseEvent e) {
                                    //Keep Label near the mouse
                                    caption.setTranslateX(e.getX());
                                    caption.setTranslateY(e.getY()+20);
                                }
                            });

                    //When mouse exited (the slice of PieChart)
                    dataTemp.getNode().addEventHandler(MouseEvent.MOUSE_EXITED,
                            new EventHandler<MouseEvent>() {
                                @Override
                                public void handle(MouseEvent e) {
                                    //I Hide Label
                                    caption.setVisible(false);
                                }
                            });
                    //Update progress
                    updateProgress(i++, sizeOfallElements);
                }
                return null;
            }
        };
        return task;
    }
}

Here the result : PieChart JavaFX with Label/Popup/Value

0
Daan Terra On

I had the same problem but also wanted to make sure that the popup can extend beyond the chart, i.e. that it does not get cut off when the text does not fit in the chart. Here's a solution using a Tooltip instead of a Label:

public class ChartHoverUtil<T> {
   public static void setupPieChartHovering(PieChart chart) {
      new ChartHoverUtil<PieChart.Data>(
            data -> String.format("Value = ", data.getPieValue()),
            data -> data.getNode())
            .setupHovering(chart.getData());
   }

   private final Tooltip tooltip = new Tooltip();
   private final SimpleBooleanProperty adjustingTooltip = new SimpleBooleanProperty(false);
   private final Function<T, String> textProvider;
   private final Function<T, Node> nodeProvider;

   private EventHandler<MouseEvent> moveHandler = new EventHandler<MouseEvent>() {
      @Override
      public void handle(MouseEvent e) {
         if (tooltip.isShowing()) {
            setLabelPosition(e);
         }
      }
   };

   private EventHandler<MouseEvent> enterHandler = new EventHandler<MouseEvent>() {
      @Override
      public void handle(MouseEvent e) {
         adjustingTooltip.set(true);

         Node chartNode = (Node) e.getSource();

         tooltip.show(chartNode, e.getScreenX(), e.getScreenY());
         setLabelPosition(e);

         ObservableBooleanValue stillHovering = chartNode.hoverProperty().or(adjustingTooltip);
         stillHovering.addListener(new ChangeListener<Boolean>() {
            @Override
            public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean nowHovering) {
               if (!nowHovering) {
                  stillHovering.removeListener(this);
                  tooltip.hide();
               }
            }
         });

         T chartData = (T) chartNode.getUserData();
         String txt = textProvider.apply(chartData);
         tooltip.setText(txt);

         adjustingTooltip.set(false);
      }
   };

   public ChartHoverUtil(Function<T, String> textProvider, Function<T, Node> getNode) {
      this.textProvider = textProvider;
      this.nodeProvider = getNode;
      tooltip.addEventFilter(MouseEvent.MOUSE_MOVED, moveHandler);
   }

   public void setupHovering(Collection<T> data) {
      for (T chartData : data) {
         Node node = nodeProvider.apply(chartData);
         node.setUserData(chartData);
         setupNodeHovering(node);
      }
   }

   private void setupNodeHovering(Node node) {
      node.addEventFilter(MouseEvent.MOUSE_MOVED, moveHandler);
      node.addEventHandler(MouseEvent.MOUSE_ENTERED, enterHandler);
      // Do not use MOUSE_EXIT handler because it is triggered immediately when showing the tooltip
   }

   private void setLabelPosition(MouseEvent e) {
      adjustingTooltip.set(true);

      tooltip.setAnchorX(e.getScreenX());
      tooltip.setAnchorY(e.getScreenY() + 20);

      adjustingTooltip.set(false);
   }
}