Chris
Chris

Reputation: 2465

JavaFx Charts highlight entire curve on hover

I am using a logarithmic scale for a graph and I want to be able to make the individual curves a little wider when hovered over and on top of all the other curves as well as the color key's value too. Below is a picture better illustrating what I want to do (with sensitive data redacted)

enter image description here

Is something like this even possible? And if so what direction should I move in to achieve this?

Upvotes: 0

Views: 673

Answers (1)

Peter
Peter

Reputation: 1612

The first of your cases, which is applying the visual changes when the mouse hovers over the curve, is possible by modifying the Node that represents the Series on the chart, which is a Path. You can apply the changes to the stroke of the Path making it darker, wider and bringing in to the front when the mouse enters and reverting them when the mouse leaves


The second, which is applying the visual changes when hovering over the legend items is still possible but it's not as clean a solution, at least it's not in my implementation below. With a Node lookup you can get the items and cast them to either a Label or a LegendItem which expose the graphic and text. I chose Label to avoid using the internal API

See more here: It is a bad practice to use Sun's proprietary Java classes?

With String comparisons between the legend text and series names you can associate the two assuming each series has a name and that it is unique. If it isn't unique you could compare the stroke fills as well as the names

Important: These assumptions limit this approach and so if possible I'd avoid it


SSCCE:

public class DarkerSeriesOnHoverExample extends Application {

    @SuppressWarnings("unchecked")
    @Override
    public void start(Stage primaryStage) throws Exception {
        //LogarithmicNumberAxis source: https://stackoverflow.com/a/22424519/5556314
        LineChart lineChart = new LineChart(new LogarithmicNumberAxis(1, 1000000), new NumberAxis(0, 2.25, 0.25));
        lineChart.setCreateSymbols(false);

        //Values guessed from the screen shot
        ObservableList<XYChart.Data> seriesData = FXCollections.observableArrayList(new XYChart.Data(1, 2),
                new XYChart.Data(10, 2), new XYChart.Data(100, 2), new XYChart.Data(1000, 1.85),
                new XYChart.Data(10000, 1.50), new XYChart.Data(100000, 1.20), new XYChart.Data(1000000, 0.9));

        ObservableList<XYChart.Data> series2Data = FXCollections.observableArrayList(new XYChart.Data(1, 2),
                new XYChart.Data(10, 2), new XYChart.Data(100, 2), new XYChart.Data(1000, 1.60),
                new XYChart.Data(10000, 1.25), new XYChart.Data(100000, 0.95), new XYChart.Data(1000000, 0.65));

        ObservableList<XYChart.Data> series3Data = FXCollections.observableArrayList(new XYChart.Data(1, 2),
                new XYChart.Data(10, 1.85), new XYChart.Data(100, 1.55), new XYChart.Data(1000, 1),
                new XYChart.Data(10000, 0.65), new XYChart.Data(100000, 0.5), new XYChart.Data(1000000, 0.45));

        ObservableList<XYChart.Series> displayedSeries = FXCollections.observableArrayList(
                new XYChart.Series("Series 1", seriesData), new XYChart.Series("Series 2", series2Data),
                new XYChart.Series("Series 3", series3Data));

        lineChart.getData().addAll(displayedSeries);
        Scene scene = new Scene(lineChart, 300, 300);
        primaryStage.setScene(scene);
        primaryStage.show();

        darkenSeriesOnHover(displayedSeries); //Setup for hovering on series (cleaner)
        darkenSeriesOnLegendHover(lineChart); //Setup both hovering on series and legend (messier)
    }

    private void darkenSeriesOnHover(List<XYChart.Series> seriesList){
        for(XYChart.Series series : seriesList){
            Node seriesNode = series.getNode();
            //seriesNode will be null if this method is called before the scene CSS has been applied
            if(seriesNode != null && seriesNode instanceof Path){
                Path seriesPath = (Path) seriesNode;
                Color initialStrokeColor = (Color)seriesPath.getStroke();
                double initialStrokeWidth = seriesPath.getStrokeWidth();

                seriesPath.setOnMouseEntered(event -> {
                    updatePath(seriesPath, initialStrokeColor.darker(), initialStrokeWidth*2, true);
                });
                seriesPath.setOnMouseExited(event -> {
                    //Reset
                    updatePath(seriesPath, initialStrokeColor, initialStrokeWidth, false);
                });
            }
        }
    }

    private void darkenSeriesOnLegendHover(LineChart lineChart){
        Set<Node> legendItems = lineChart.lookupAll("Label.chart-legend-item");
        List<XYChart.Series> seriesList = lineChart.getData();

        //Will be empty if this method is called before the scene CSS has been applied
        if(legendItems.isEmpty()){ return; }

        for(Node legendItem : legendItems){
            Label legend = (Label) legendItem;
            XYChart.Series matchingSeries = getMatchingSeriesByName(seriesList, legend.getText());
            if(matchingSeries == null){ return; }

            Node seriesNode = matchingSeries.getNode();
            //seriesNode will be null if this method is called before the scene CSS has been applied
            if(seriesNode != null && seriesNode instanceof Path){
                Path seriesPath = (Path) seriesNode;
                Color initialStrokeColor = (Color)seriesPath.getStroke();
                double initialStrokeWidth = seriesPath.getStrokeWidth();

                legendItem.setOnMouseEntered(event -> {
                    updatePath(seriesPath, initialStrokeColor.darker(), initialStrokeWidth*2, true);
                });
                legendItem.setOnMouseExited(event -> {
                    //Reset
                    updatePath(seriesPath, initialStrokeColor, initialStrokeWidth, false);
                });

                seriesPath.setOnMouseEntered(event -> {
                    updatePath(seriesPath, initialStrokeColor.darker(), initialStrokeWidth*2, true);
                });
                seriesPath.setOnMouseExited(event -> {
                    //Reset
                    updatePath(seriesPath, initialStrokeColor, initialStrokeWidth, false);
                });
            }
        }
    }

    private void updatePath(Path seriesPath, Paint strokeColor, double strokeWidth, boolean toFront){
        seriesPath.setStroke(strokeColor);
        seriesPath.setStrokeWidth(strokeWidth);
        if(!toFront){ return; }
        seriesPath.toFront();
    }

    private XYChart.Series getMatchingSeriesByName(List<XYChart.Series> seriesList, String searchParam){
        for (XYChart.Series series : seriesList){
            if(series.getName().equals(searchParam)){
                return series;
            }
        }
        return null;
    }
}

Output:

Before vs Hover

before hover hover

Upvotes: 3

Related Questions