chart-fx icon indicating copy to clipboard operation
chart-fx copied to clipboard

Plugin node rendering is broken when a data set is dynamically added

Open dedeibel opened this issue 1 year ago • 0 comments

Describe the bug:

When using DataPointTooltip and you add a new data set dynamically (after the chart has been fully set up and rendered once), the tooltip content is not updated correctly and showing the previous content when hovering over a point. This might mean that it is empty or containing data from a different point.

I think this one is not so easy to describe but I hope the video should make it clear.

https://github.com/user-attachments/assets/1e103d1e-f6f2-4ba7-b027-d9fe3becf9b9 (I resized the window at 00:46)

(Make sure to apply the fixes of #677 or add the axes to the renderers or the sample will not work as expected in the first place.)

The problem is described here using the example of the plugin DataPointTooltip but I think it is might no be limited to it. Other plugins could be affected as well.

To Reproduce:

Open FxmlSample and hover over a data point. The tooltip appears. Add a second data set using the chart's menu, like "gauss". Now hover over any point on any of the lines, the old data point tooltip text will be shown.

Workaround: Resize the window. After that the situation is resolved until a new data set is added.

Environment:

  • OS: Debian 12.7
  • Java version: openjdk version "17.0.13" 2024-10-15
  • JavaFx version: 16
  • ChartFx version: main, 05980c41

Analysis:

I tried debugging the issue but found it quite hard. Without knowing the actual cause: it seems like it has something to do with the nature of the plugin nodes being managed = false and possibly with the ChartPanes implementation and nesting. The Legend also plays a role in the concrete example mentioned above.

I looked at the nodes hierarchy and their layoutFlags. The Label of the DataPointTooltip requests a layout because of its content and position change. But the tooltip node is located in the nested ChartPanes together with the Legend somewhere upstream. When the data set is added the legend is changed and causes it and the parent .chart-legend-pane and .chart-measurement-pane to require layout. I think the tooltip label's request to layout stops at the .chart-legend-pane above since the pane is already "needs layout". Somehow the surrounding .chart-measurement-pane will become "clean" (instead of "needs layout" or being "dirty branch") but the .chart-legend-pane underneath remains "needs layout", so does the tooltip label down below without being rendered. The label which is then stuck in a dirty state. The legend seems to be displayed fine. Layouting does not stop at .chart-measurement-pane since it is clean.

I saw this when adding log output for the layoutFlag of the parent hierarchy of the tooltip node. See output¹ and debug code for the output².

Here is a list of different workarounds I found, some more weird than others :-):

  • Resizing the window resp. chart.
  • Calling .layout() on the tooltip label directly after updating it.
  • The problem is mitigated if you replace the legend implementation with an empty one (getNode() only returns an empty Region for example).
  • Removing the -fx-alignment: center; line from .chart-legend { CSS.
  • The problem is also mitigated if you manually call requestLayout on the .chart-legend-pane if any of the children need layout³
  • The problem is mitigated when you change all Chart.pluginsArea Groups to be managed
    • Not a good solution... the nodes will be relocated at some point by the parent

Even if this could be Java FX problem I wanted to document it here since it is has real consequences when using Chart FX. Maybe there are better ways to mitigate the issue or you can find the actual fix.


1

Directly after adding data set tooltip node. Before adding the second data set.

                                                                     - [root]
................................................................ - class javafx.scene.layout.GridPane; css: root; layoutFlags: DIRTY_BRANCH
                                                                   - class javafx.scene.control.TextField; css: text-input, text-field, search-box; layoutFlags: CLEAN
                                                                   - class javafx.scene.control.TreeView; css: tree-view, samples-tree; layoutFlags: CLEAN
                                                                   - class javafx.scene.control.TabPane; css: tab-pane, floating; layoutFlags: DIRTY_BRANCH
............................................................ - class javafx.scene.control.TabPane; css: tab-pane, floating; layoutFlags: DIRTY_BRANCH
                                                               - class javafx.scene.control.skin.TabPaneSkin$TabContentRegion; managed: false; css: tab-content-area; layoutFlags: CLEAN
                                                               - class javafx.scene.control.skin.TabPaneSkin$TabContentRegion; managed: false; css: tab-content-area; layoutFlags: CLEAN
                                                               - class javafx.scene.control.skin.TabPaneSkin$TabContentRegion; managed: false; css: tab-content-area; layoutFlags: CLEAN
                                                               - class javafx.scene.control.skin.TabPaneSkin$TabContentRegion; managed: false; css: tab-content-area; layoutFlags: DIRTY_BRANCH
                                                               - class javafx.scene.control.skin.TabPaneSkin$TabHeaderArea; managed: false; css: tab-header-area; layoutFlags: CLEAN
........................................................ - class javafx.scene.control.skin.TabPaneSkin$TabContentRegion; managed: false; css: tab-content-area; layoutFlags: DIRTY_BRANCH
                                                           - class javafx.scene.control.SplitPane; css: split-pane; layoutFlags: DIRTY_BRANCH
.................................................... - class javafx.scene.control.SplitPane; css: split-pane; layoutFlags: DIRTY_BRANCH
                                                       - class javafx.scene.control.skin.SplitPaneSkin$Content; layoutFlags: DIRTY_BRANCH
................................................ - class javafx.scene.control.skin.SplitPaneSkin$Content; layoutFlags: DIRTY_BRANCH
                                                   - class javafx.scene.layout.BorderPane; layoutFlags: DIRTY_BRANCH
............................................ - class javafx.scene.layout.BorderPane; layoutFlags: DIRTY_BRANCH
                                               - class javafx.scene.control.MenuBar; css: menu-bar; layoutFlags: CLEAN
                                               - class io.fair_acc.chartfx.XYChart; css: chart; layoutFlags: DIRTY_BRANCH
........................................ - class io.fair_acc.chartfx.XYChart; css: chart; layoutFlags: DIRTY_BRANCH
                                           - class io.fair_acc.chartfx.ui.css.StyleGroup; managed: false; layoutFlags: CLEAN
                                           - class io.fair_acc.chartfx.ui.HiddenSidesPane; layoutFlags: DIRTY_BRANCH
.................................... - class io.fair_acc.chartfx.ui.HiddenSidesPane; layoutFlags: DIRTY_BRANCH
                                       - class javafx.scene.layout.StackPane; layoutFlags: DIRTY_BRANCH
................................ - class javafx.scene.layout.StackPane; layoutFlags: DIRTY_BRANCH
                                   - class io.fair_acc.chartfx.ui.layout.ChartPane; css: chart-measurement-pane; layoutFlags: DIRTY_BRANCH
                                   - class io.fair_acc.chartfx.ui.ToolBarFlowPane; managed: false; layoutFlags: CLEAN
............................ - class io.fair_acc.chartfx.ui.layout.ChartPane; css: chart-measurement-pane; layoutFlags: DIRTY_BRANCH
                               - class io.fair_acc.chartfx.ui.layout.ChartPane; css: chart-title-pane, chart-legend-pane; layoutFlags: DIRTY_BRANCH
                               - class io.fair_acc.chartfx.viewer.DataView; layoutFlags: CLEAN
........................ - class io.fair_acc.chartfx.ui.layout.ChartPane; css: chart-title-pane, chart-legend-pane; layoutFlags: DIRTY_BRANCH
                           - class io.fair_acc.chartfx.legend.spi.DefaultLegend; css: chart-legend; layoutFlags: CLEAN
                           - class io.fair_acc.chartfx.ui.layout.TitleLabel; css: label, chart-title; layoutFlags: CLEAN
                           - class io.fair_acc.chartfx.ui.layout.ChartPane; css: chart-content; layoutFlags: DIRTY_BRANCH
.................... - class io.fair_acc.chartfx.ui.layout.ChartPane; css: chart-content; layoutFlags: DIRTY_BRANCH
                       - class io.fair_acc.chartfx.ui.layout.FullSizePane; css: chart-plot-background; layoutFlags: CLEAN
                       - class io.fair_acc.chartfx.ui.HiddenSidesPane; css: chart-plot-area; layoutFlags: DIRTY_BRANCH
                       - class io.fair_acc.chartfx.ui.layout.FullSizePane; css: chart-plot-foreground; layoutFlags: CLEAN
                       - class io.fair_acc.chartfx.axes.spi.DefaultNumericAxis; css: axis; layoutFlags: CLEAN
                       - class io.fair_acc.chartfx.axes.spi.DefaultNumericAxis; css: axis; layoutFlags: CLEAN
................ - class io.fair_acc.chartfx.ui.HiddenSidesPane; css: chart-plot-area; layoutFlags: DIRTY_BRANCH
                   - class javafx.scene.layout.StackPane; layoutFlags: DIRTY_BRANCH
............ - class javafx.scene.layout.StackPane; layoutFlags: DIRTY_BRANCH
               - class io.fair_acc.chartfx.ui.layout.FullSizePane; css: chart-canvas-area; layoutFlags: DIRTY_BRANCH
               - class io.fair_acc.chartfx.plugins.Zoomer$ZoomRangeSlider; managed: false; css: range-slider; layoutFlags: CLEAN
........ - class io.fair_acc.chartfx.ui.layout.FullSizePane; css: chart-canvas-area; layoutFlags: DIRTY_BRANCH
           - class io.fair_acc.chartfx.ui.ResizableCanvas; css: chart-canvas
           - class io.fair_acc.chartfx.ui.layout.FullSizePane; managed: false; css: chart-canvas-foreground; layoutFlags: CLEAN
           - class javafx.scene.Group; managed: false; layoutFlags: DIRTY_BRANCH
.... - class javafx.scene.Group; managed: false; layoutFlags: DIRTY_BRANCH
       - class javafx.scene.Group; managed: false; layoutFlags: CLEAN
       - class javafx.scene.Group; managed: false; layoutFlags: NEEDS_LAYOUT
       - class javafx.scene.Group; managed: false; layoutFlags: CLEAN
       - class javafx.scene.Group; managed: false; layoutFlags: CLEAN
 - class javafx.scene.Group; managed: false; layoutFlags: NEEDS_LAYOUT
   - class javafx.scene.control.Label; managed: false; css: label, chart-datapoint-tooltip-label; layoutFlags: NEEDS_LAYOUT

Directly after adding data set tooltip node. After adding the second data set.

                                                                     - [root]
................................................................ - class javafx.scene.layout.GridPane; css: root; layoutFlags: CLEAN
                                                                   - class javafx.scene.control.TextField; css: text-input, text-field, search-box; layoutFlags: CLEAN
                                                                   - class javafx.scene.control.TreeView; css: tree-view, samples-tree; layoutFlags: CLEAN
                                                                   - class javafx.scene.control.TabPane; css: tab-pane, floating; layoutFlags: CLEAN
............................................................ - class javafx.scene.control.TabPane; css: tab-pane, floating; layoutFlags: CLEAN
                                                               - class javafx.scene.control.skin.TabPaneSkin$TabContentRegion; managed: false; css: tab-content-area; layoutFlags: CLEAN
                                                               - class javafx.scene.control.skin.TabPaneSkin$TabContentRegion; managed: false; css: tab-content-area; layoutFlags: CLEAN
                                                               - class javafx.scene.control.skin.TabPaneSkin$TabContentRegion; managed: false; css: tab-content-area; layoutFlags: CLEAN
                                                               - class javafx.scene.control.skin.TabPaneSkin$TabContentRegion; managed: false; css: tab-content-area; layoutFlags: CLEAN
                                                               - class javafx.scene.control.skin.TabPaneSkin$TabHeaderArea; managed: false; css: tab-header-area; layoutFlags: CLEAN
........................................................ - class javafx.scene.control.skin.TabPaneSkin$TabContentRegion; managed: false; css: tab-content-area; layoutFlags: CLEAN
                                                           - class javafx.scene.control.SplitPane; css: split-pane; layoutFlags: CLEAN
.................................................... - class javafx.scene.control.SplitPane; css: split-pane; layoutFlags: CLEAN
                                                       - class javafx.scene.control.skin.SplitPaneSkin$Content; layoutFlags: CLEAN
................................................ - class javafx.scene.control.skin.SplitPaneSkin$Content; layoutFlags: CLEAN
                                                   - class javafx.scene.layout.BorderPane; layoutFlags: CLEAN
............................................ - class javafx.scene.layout.BorderPane; layoutFlags: CLEAN
                                               - class javafx.scene.control.MenuBar; css: menu-bar; layoutFlags: CLEAN
                                               - class io.fair_acc.chartfx.XYChart; css: chart; layoutFlags: CLEAN
........................................ - class io.fair_acc.chartfx.XYChart; css: chart; layoutFlags: CLEAN
                                           - class io.fair_acc.chartfx.ui.css.StyleGroup; managed: false; layoutFlags: CLEAN
                                           - class io.fair_acc.chartfx.ui.HiddenSidesPane; layoutFlags: CLEAN
.................................... - class io.fair_acc.chartfx.ui.HiddenSidesPane; layoutFlags: CLEAN
                                       - class javafx.scene.layout.StackPane; layoutFlags: CLEAN
................................ - class javafx.scene.layout.StackPane; layoutFlags: CLEAN
                                   - class io.fair_acc.chartfx.ui.layout.ChartPane; css: chart-measurement-pane; layoutFlags: CLEAN
                                   - class io.fair_acc.chartfx.ui.ToolBarFlowPane; managed: false; layoutFlags: CLEAN
............................ - class io.fair_acc.chartfx.ui.layout.ChartPane; css: chart-measurement-pane; layoutFlags: CLEAN
                               - class io.fair_acc.chartfx.ui.layout.ChartPane; css: chart-title-pane, chart-legend-pane; layoutFlags: NEEDS_LAYOUT
                               - class io.fair_acc.chartfx.viewer.DataView; layoutFlags: CLEAN
........................ - class io.fair_acc.chartfx.ui.layout.ChartPane; css: chart-title-pane, chart-legend-pane; layoutFlags: NEEDS_LAYOUT
                           - class io.fair_acc.chartfx.legend.spi.DefaultLegend; css: chart-legend; layoutFlags: NEEDS_LAYOUT
                           - class io.fair_acc.chartfx.ui.layout.TitleLabel; css: label, chart-title; layoutFlags: CLEAN
                           - class io.fair_acc.chartfx.ui.layout.ChartPane; css: chart-content; layoutFlags: DIRTY_BRANCH
.................... - class io.fair_acc.chartfx.ui.layout.ChartPane; css: chart-content; layoutFlags: DIRTY_BRANCH
                       - class io.fair_acc.chartfx.ui.layout.FullSizePane; css: chart-plot-background; layoutFlags: CLEAN
                       - class io.fair_acc.chartfx.ui.HiddenSidesPane; css: chart-plot-area; layoutFlags: DIRTY_BRANCH
                       - class io.fair_acc.chartfx.ui.layout.FullSizePane; css: chart-plot-foreground; layoutFlags: CLEAN
                       - class io.fair_acc.chartfx.axes.spi.DefaultNumericAxis; css: axis; layoutFlags: CLEAN
                       - class io.fair_acc.chartfx.axes.spi.DefaultNumericAxis; css: axis; layoutFlags: CLEAN
................ - class io.fair_acc.chartfx.ui.HiddenSidesPane; css: chart-plot-area; layoutFlags: DIRTY_BRANCH
                   - class javafx.scene.layout.StackPane; layoutFlags: DIRTY_BRANCH
............ - class javafx.scene.layout.StackPane; layoutFlags: DIRTY_BRANCH
               - class io.fair_acc.chartfx.ui.layout.FullSizePane; css: chart-canvas-area; layoutFlags: DIRTY_BRANCH
               - class io.fair_acc.chartfx.plugins.Zoomer$ZoomRangeSlider; managed: false; css: range-slider; layoutFlags: CLEAN
........ - class io.fair_acc.chartfx.ui.layout.FullSizePane; css: chart-canvas-area; layoutFlags: DIRTY_BRANCH
           - class io.fair_acc.chartfx.ui.ResizableCanvas; css: chart-canvas
           - class io.fair_acc.chartfx.ui.layout.FullSizePane; managed: false; css: chart-canvas-foreground; layoutFlags: CLEAN
           - class javafx.scene.Group; managed: false; layoutFlags: DIRTY_BRANCH
.... - class javafx.scene.Group; managed: false; layoutFlags: DIRTY_BRANCH
       - class javafx.scene.Group; managed: false; layoutFlags: CLEAN
       - class javafx.scene.Group; managed: false; layoutFlags: NEEDS_LAYOUT
       - class javafx.scene.Group; managed: false; layoutFlags: CLEAN
       - class javafx.scene.Group; managed: false; layoutFlags: CLEAN
 - class javafx.scene.Group; managed: false; layoutFlags: NEEDS_LAYOUT
   - class javafx.scene.control.Label; managed: false; css: label, chart-datapoint-tooltip-label; layoutFlags: NEEDS_LAYOUT

²

diff --git a/chartfx-chart/src/main/java/io/fair_acc/chartfx/plugins/DataPointTooltip.java b/chartfx-chart/src/main/java/io/fair_acc/chartfx/plugins/DataPointTooltip.java
index 5b2306e0..79175f1a 100644
--- a/chartfx-chart/src/main/java/io/fair_acc/chartfx/plugins/DataPointTooltip.java
+++ b/chartfx-chart/src/main/java/io/fair_acc/chartfx/plugins/DataPointTooltip.java
@@ -3,20 +3,14 @@
  */
 package io.fair_acc.chartfx.plugins;
 
+import java.lang.reflect.InvocationTargetException;
 import java.util.List;
 import java.util.Optional;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
 import java.util.stream.Stream;
 
-import javafx.beans.property.DoubleProperty;
-import javafx.beans.property.SimpleDoubleProperty;
-import javafx.collections.ObservableList;
-import javafx.event.EventHandler;
-import javafx.geometry.Bounds;
-import javafx.geometry.Point2D;
-import javafx.scene.control.Label;
-import javafx.scene.input.MouseEvent;
+import org.apache.commons.lang3.StringUtils;
 
 import io.fair_acc.chartfx.Chart;
 import io.fair_acc.chartfx.XYChart;
@@ -26,6 +20,18 @@ import io.fair_acc.chartfx.renderer.spi.ErrorDataSetRenderer;
 import io.fair_acc.dataset.DataSet;
 import io.fair_acc.dataset.GridDataSet;
 import io.fair_acc.dataset.spi.utils.Tuple;
+import javafx.beans.property.DoubleProperty;
+import javafx.beans.property.SimpleDoubleProperty;
+import javafx.collections.ObservableList;
+import javafx.event.EventHandler;
+import javafx.geometry.Bounds;
+import javafx.geometry.Point2D;
+import javafx.scene.Node;
+import javafx.scene.Parent;
+import javafx.scene.Scene;
+import javafx.scene.control.Label;
+import javafx.scene.control.Labeled;
+import javafx.scene.input.MouseEvent;
 
 /**
  * A tool tip label appearing next to the mouse cursor when placed over a data point's symbol. If symbols are not
@@ -282,6 +288,7 @@ public class DataPointTooltip extends AbstractDataFormattingPlugin {
         if (!getChartChildren().contains(label)) {
             getChartChildren().add(label);
             label.requestLayout();
+            printDpTLayoutState(label.getScene());
         }
     }
 
@@ -310,4 +317,80 @@ public class DataPointTooltip extends AbstractDataFormattingPlugin {
             return new DataPoint(renderer, x, y, formattedLabel, distanceFromMouse, formattedLabel);
         }
     }
+    
+    public static void printDpTLayoutState(Scene scene) {
+        Node tt = scene.lookup("." + DataPointTooltip.STYLE_CLASS_LABEL);
+        if (tt == null) {
+            System.err.println("DT: null");
+            return;
+        }
+
+        StringBuilder sb = new StringBuilder();
+        print(tt.getParent(), sb, 0);
+        System.err.println(sb.toString());
+    }
+
+    private static void print(Node node, StringBuilder sb, int lvl) {
+        final String pad;
+        if (node instanceof Parent) {
+            pad = StringUtils.repeat("..", lvl) + " - ";
+        } else {
+            pad = StringUtils.repeat("  ", lvl) + " - ";
+        }
+
+        if (node == null) {
+            sb.append(pad);
+            sb.append("[root]").append("\n");
+            return;
+        }
+
+        print(node.getParent(), sb, lvl + 2);
+
+        sb.append(pad);
+        printSelf(node, sb);
+        sb.append("\n");
+
+        if (node instanceof Parent parent) {
+            for (var c : parent.getChildrenUnmodifiable()) {
+                String padc = StringUtils.repeat("  ", lvl + 1) + " - ";
+                sb.append(padc);
+                printSelf(c, sb);
+                sb.append("\n");
+            }
+        }
+    }
+
+    private static void printSelf(Node node, StringBuilder sb) {
+        sb.append(node.getClass());
+
+        if (!node.isManaged()) {
+            sb.append("; managed: ").append(node.isManaged());
+        }
+
+        if (!node.getStyleClass().isEmpty()) {
+            sb.append("; css: ").append(node.getStyleClass().stream().collect(Collectors.joining(", ")));
+        }
+
+        try {
+            java.lang.reflect.Method m = Labeled.class.getDeclaredMethod("getText");
+            Object textObj = m.invoke(m);
+            if (textObj != null && !textObj.toString().equals("")) {
+                sb.append("; text: ").append(textObj);
+            }
+        } catch (IllegalArgumentException | IllegalAccessException | SecurityException | NoSuchMethodException
+                | InvocationTargetException e) {
+            // ignore
+        }
+
+        if (node instanceof Parent parent) {
+            try {
+                java.lang.reflect.Field protectedfield = Parent.class.getDeclaredField("layoutFlag");
+                protectedfield.setAccessible(true);
+                Object layoutFlagValueObj = protectedfield.get(parent);
+                sb.append("; layoutFlags: ").append(layoutFlagValueObj);
+            } catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException | SecurityException e) {
+                sb.append("; layoutFlags: ").append(e);
+            }
+        }
+    }
 }

³

… extends XYChart {
…

    @Override
    protected void runPreLayout() {
        if (pluginsArea.getChildren().stream().anyMatch(n -> n instanceof final Parent p && p.isNeedsLayout())) {
            LOGGER.debug("Requesting title-legend pane layout because of plugin UI updates");
            getTitleLegendPane().requestLayout();
        }

        super.runPreLayout();
    }

dedeibel avatar Nov 20 '24 14:11 dedeibel