FredH
FredH

Reputation: 53

Zoom selection rectangle in JavaFX Chart not showing, though Zoom working

I have written a sample program that implements Zooming function on a JavaFX Chart, I found the Zoom class from the GitHub project and am just reusing it. My challenge is that when I drag the mouse to select some region to Zoom, the selected area rectangle doesn't show in Windows 7, Linux, Mac OS X, but it works fine in Windows 10. What am I missing, how can I make the selectedRectangle to show so the user can know what area they are zooming into?

Below are all the files that are needed to compile and run this program:

/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package testlinechartgraphs;

import java.util.ArrayList;
import java.util.Random;
import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.geometry.Side;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.control.CheckBox;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class TestLineChartGraphs extends Application {

    final static ObservableList<XYChart.Series<Number, Number>> lineChartData = FXCollections.observableArrayList();

    @Override
    public void start(Stage stage) {
        stage.setTitle("Line Chart Sample");
        //defining the axes
        final NumberAxis xAxis = new NumberAxis();
        final NumberAxis yAxis = new NumberAxis();
        xAxis.setLabel("Number of Month");
        Random randomNumbers = new Random();
        ArrayList<Integer> arrayList = new ArrayList<>();

        //creating the chart
        final LineChart<Number, Number> lineChart
                = new LineChart<Number, Number>(xAxis, yAxis);

        lineChart.setTitle("Stock Monitoring, 2010");
        lineChart.setLegendSide(Side.RIGHT);

        int randomCount = randomNumbers.nextInt(14)+1;
        //System.out.println("randomCount = " + randomCount);
        for (int i = 0; i < randomCount; i++) {
            XYChart.Series series = new XYChart.Series();
            series.setName("series_" + i);
            for (int k = 0; k < 20; k++) {
                int x = randomNumbers.nextInt(50);

                series.getData().add(new XYChart.Data(k, x));
            }
            //seriesList.add(series);
            lineChartData.add(series);
        }


        lineChart.setData(lineChartData);

        final StackPane chartContainer = new StackPane();

        Zoom zoom = new Zoom(lineChart, chartContainer);

        chartContainer.getChildren()
                .add(lineChart);

        BorderPane borderPane = new BorderPane();

        borderPane.setCenter(chartContainer);
        //borderPane.setCenter(lineChart);

        borderPane.setBottom(getLegend());
////        
        //Scene scene = new Scene(lineChart, 800, 600);
        Scene scene = new Scene(borderPane, 800, 600);
        //lineChart.getData().addAll(series, series1);

        stage.setScene(scene);
        scene.getStylesheets().addAll("file:///C:/Users/siphoh/Documents/NetBeansProjects/WiresharkSeqNum/src/fancychart.css");
        //scene.getStylesheets().addAll(getClass().getResource("fancychart.css").toExternalForm());

        stage.show();
    }

    public static Node getLegend() {
        HBox hBox = new HBox();

        for (final XYChart.Series<Number, Number> series : lineChartData) {
            CheckBox checkBox = new CheckBox(series.getName());


            checkBox.setSelected(true);
            checkBox.setOnAction(event -> {
                if (lineChartData.contains(series)) {

                    lineChartData.remove(series);
                } else {
                    lineChartData.add(series);
                }
            });

            hBox.getChildren().add(checkBox);
        }

        hBox.setAlignment(Pos.CENTER);
        hBox.setSpacing(20);
        hBox.setStyle("-fx-padding: 0 10 20 10");

        return hBox;
    }

    public static void main(String[] args) {
        launch(args);
    }
}

//Zoom.java class:

package testlinechartgraphs;

/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */

/**
 *
 * @author *************
 */
import javafx.event.EventHandler;
import javafx.geometry.Point2D;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.control.Label;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;

/**
 * This class adds a zoom functionality to a given XY chart. Zoom means that a
 user can select a region in the chart that should be displayed at a larger
 scale.
 *
 */
public class Zoom {

    private static final String INFO_LABEL_ID = "zoomInfoLabel";
    private final Pane pane;
    private final XYChart<Number, Number> chart;
    private final NumberAxis xAxis;
    private final NumberAxis yAxis;
    private final SelectionRectangle selectionRectangle;
    private Label infoLabel;

    private Point2D selectionRectangleStart;
    private Point2D selectionRectangleEnd;

    /**
     * Create a new instance of this class with the given chart and pane
     * instances. The {@link Pane} instance is needed as a parent for the
     * rectangle that represents the user selection.
     *
     * @param chart the xy chart to which the zoom support should be added
     * @param pane the pane on which the selection rectangle will be drawn.
     */
    public Zoom(XYChart<Number, Number> chart, Pane pane) {
        this.pane = pane;
        this.chart = chart;
        this.xAxis = (NumberAxis) chart.getXAxis();
        this.yAxis = (NumberAxis) chart.getYAxis();
        selectionRectangle = new SelectionRectangle();

        pane.getChildren().add(selectionRectangle);
        addDragSelectionMechanism();
        addInfoLabel();
    }

    /**
     * The info label shows a short info text that tells the user how to unreset
     * the zoom level.
     */
    private void addInfoLabel() {
        infoLabel = new Label("Click ESC to reset the zoom level.");
        infoLabel.setId(INFO_LABEL_ID);
        pane.getChildren().add(infoLabel);
        StackPane.setAlignment(infoLabel, Pos.TOP_RIGHT);
        infoLabel.setVisible(false);
    }

    /**
     * Adds a mechanism to select an area in the chart that should be displayed
     * at larged scale.
     */
    private void addDragSelectionMechanism() {
        pane.addEventHandler(MouseEvent.MOUSE_PRESSED, new MousePressedHandler());
        pane.addEventHandler(MouseEvent.MOUSE_DRAGGED, new MouseDraggedHandler());
        pane.addEventHandler(MouseEvent.MOUSE_RELEASED, new MouseReleasedHandler());
        pane.addEventHandler(KeyEvent.KEY_RELEASED, new EscapeKeyHandler());
    }

    private Point2D computeRectanglePoint(double eventX, double eventY) {
        double lowerBoundX = computeOffsetInChart(xAxis, false);
        double upperBoundX = lowerBoundX + xAxis.getWidth();
        double lowerBoundY = computeOffsetInChart(yAxis, true);
        double upperBoundY = lowerBoundY + yAxis.getHeight();
        // make sure the rectangle's end point is in the interval defined by the lower and upper bounds for each
        // dimension
        double x = Math.max(lowerBoundX, Math.min(eventX, upperBoundX));
        double y = Math.max(lowerBoundY, Math.min(eventY, upperBoundY));
        return new Point2D(x, y);
    }

    /**
     * Computes the pixel offset of the given node inside the chart node.
     *
     * @param node the node for which to compute the pixel offset
     * @param vertical flag that indicates whether the horizontal or the
     * vertical dimension should be taken into account
     * @return the offset inside the chart node
     */
    private double computeOffsetInChart(Node node, boolean vertical) {
        double offset = 0;
        do {
            if (vertical) {
                offset += node.getLayoutY();
            } else {
                offset += node.getLayoutX();
            }
            node = node.getParent();
        } while (node != chart);
        return offset;
    }

    /**
     *
     */
    private final class MousePressedHandler implements EventHandler<MouseEvent> {

        @Override
        public void handle(final MouseEvent event) {

            // do nothing for a right-click
            if (event.isSecondaryButtonDown()) {
                return;
            }

            // store position of initial click
            selectionRectangleStart = computeRectanglePoint(event.getX(), event.getY());
            event.consume();
        }
    }

    /**
     *
     */
    private final class MouseDraggedHandler implements EventHandler<MouseEvent> {

        @Override
        public void handle(final MouseEvent event) {

            // do nothing for a right-click
            if (event.isSecondaryButtonDown()) {
                return;
            }

            // store current cursor position
            selectionRectangleEnd = computeRectanglePoint(event.getX(), event.getY());

            double x = Math.min(selectionRectangleStart.getX(), selectionRectangleEnd.getX());
            double y = Math.min(selectionRectangleStart.getY(), selectionRectangleEnd.getY());
            double width = Math.abs(selectionRectangleStart.getX() - selectionRectangleEnd.getX());
            double height = Math.abs(selectionRectangleStart.getY() - selectionRectangleEnd.getY());

            drawSelectionRectangle(x, y, width, height);
            event.consume();
        }

        /**
         * Draws a selection box in the view.
         *
         * @param x the x position of the selection box
         * @param y the y position of the selection box
         * @param width the width of the selection box
         * @param height the height of the selection box
         */
        private void drawSelectionRectangle(final double x, final double y, final double width, final double height) {
            selectionRectangle.setVisible(true);
            selectionRectangle.setX(x);
            selectionRectangle.setY(y);
            selectionRectangle.setWidth(width);
            selectionRectangle.setHeight(height);
            //selectionRectangle.setFill(Color.LIGHTSEAGREEN.deriveColor(0, 1, 1, 0.5));
            //System.out.println("Draw the rectangle ...");
        }
    }

    /**
     *
     */
    private final class MouseReleasedHandler implements EventHandler<MouseEvent> {

        /**
         * Defines a minimum width for the selected area. If the selected
         * rectangle is not wider than this value, no zooming will take place.
         * This helps prevent accidental zooming.
         */
        private static final double MIN_RECTANGE_WIDTH = 10;

        /**
         * Defines a minimum height for the selected area. If the selected
         * rectangle is not wider than this value, no zooming will take place.
         * This helps prevent accidental zooming.
         */
        private static final double MIN_RECTANGLE_HEIGHT = 10;

        @Override
        public void handle(final MouseEvent event) {
            hideSelectionRectangle();

            if (selectionRectangleStart == null || selectionRectangleEnd == null) {
                return;
            }

            if (isRectangleSizeTooSmall()) {
                return;
            }

            setAxisBounds();
            showInfo();
            selectionRectangleStart = null;
            selectionRectangleEnd = null;

            // needed for the key event handler to receive events
            pane.requestFocus();
            event.consume();
        }

        private boolean isRectangleSizeTooSmall() {
            double width = Math.abs(selectionRectangleEnd.getX() - selectionRectangleStart.getX());
            double height = Math.abs(selectionRectangleEnd.getY() - selectionRectangleStart.getY());
            return width < MIN_RECTANGE_WIDTH || height < MIN_RECTANGLE_HEIGHT;
        }

        /**
         * Hides the selection rectangle.
         */
        private void hideSelectionRectangle() {
            selectionRectangle.setVisible(false);
        }

        private void setAxisBounds() {
            disableAutoRanging();

            // compute new bounds for the chart's x and y axes
            double selectionMinX = Math.min(selectionRectangleStart.getX(), selectionRectangleEnd.getX());
            double selectionMaxX = Math.max(selectionRectangleStart.getX(), selectionRectangleEnd.getX());
            double selectionMinY = Math.min(selectionRectangleStart.getY(), selectionRectangleEnd.getY());
            double selectionMaxY = Math.max(selectionRectangleStart.getY(), selectionRectangleEnd.getY());

            setHorizontalBounds(selectionMinX, selectionMaxX);
            setVerticalBounds(selectionMinY, selectionMaxY);
        }

        private void disableAutoRanging() {
            xAxis.setAutoRanging(false);
            yAxis.setAutoRanging(false);
        }

        private void showInfo() {
            infoLabel.setVisible(true);
        }

        /**
         * Sets new bounds for the chart's x axis.
         *
         * @param minPixelPosition the x position of the selection rectangle's
         * left edge (in pixels)
         * @param maxPixelPosition the x position of the selection rectangle's
         * right edge (in pixels)
         */
        private void setHorizontalBounds(double minPixelPosition, double maxPixelPosition) {
            double currentLowerBound = xAxis.getLowerBound();
            double currentUpperBound = xAxis.getUpperBound();
            double offset = computeOffsetInChart(xAxis, false);
            setLowerBoundX(minPixelPosition, currentLowerBound, currentUpperBound, offset);
            setUpperBoundX(maxPixelPosition, currentLowerBound, currentUpperBound, offset);
        }

        /**
         * Sets new bounds for the chart's y axis.
         *
         * @param minPixelPosition the y position of the selection rectangle's
         * upper edge (in pixels)
         * @param maxPixelPosition the y position of the selection rectangle's
         * lower edge (in pixels)
         */
        private void setVerticalBounds(double minPixelPosition, double maxPixelPosition) {
            double currentLowerBound = yAxis.getLowerBound();
            double currentUpperBound = yAxis.getUpperBound();
            double offset = computeOffsetInChart(yAxis, true);
            setLowerBoundY(maxPixelPosition, currentLowerBound, currentUpperBound, offset);
            setUpperBoundY(minPixelPosition, currentLowerBound, currentUpperBound, offset);
        }

        private void setLowerBoundX(double pixelPosition, double currentLowerBound, double currentUpperBound,
                double offset) {
            double newLowerBound = computeBound(pixelPosition, offset, xAxis.getWidth(), currentLowerBound,
                    currentUpperBound, false);
            xAxis.setLowerBound(newLowerBound);
        }

        private void setUpperBoundX(double pixelPosition, double currentLowerBound, double currentUpperBound,
                double offset) {
            double newUpperBound = computeBound(pixelPosition, offset, xAxis.getWidth(), currentLowerBound,
                    currentUpperBound, false);
            xAxis.setUpperBound(newUpperBound);
        }

        private void setLowerBoundY(double pixelPosition, double currentLowerBound, double currentUpperBound,
                double offset) {
            double newLowerBound = computeBound(pixelPosition, offset, yAxis.getHeight(), currentLowerBound,
                    currentUpperBound, true);
            yAxis.setLowerBound(newLowerBound);
        }

        private void setUpperBoundY(double pixelPosition, double currentLowerBound, double currentUpperBound,
                double offset) {
            double newUpperBound = computeBound(pixelPosition, offset, yAxis.getHeight(), currentLowerBound,
                    currentUpperBound, true);
            yAxis.setUpperBound(newUpperBound);
        }

        private double computeBound(double pixelPosition, double pixelOffset, double pixelLength, double lowerBound,
                double upperBound, boolean axisInverted) {
            double pixelPositionWithoutOffset = pixelPosition - pixelOffset;
            double relativePosition = pixelPositionWithoutOffset / pixelLength;
            double axisLength = upperBound - lowerBound;

            // The screen's y axis grows from top to bottom, whereas the chart's y axis goes from bottom to top.
            // That's
            // why we need to have this distinction here.
            double offset = 0;
            int sign = 0;
            if (axisInverted) {
                offset = upperBound;
                sign = -1;
            } else {
                offset = lowerBound;
                sign = 1;
            }

            double newBound = offset + sign * relativePosition * axisLength;
            return newBound;
        }
    }

    /**
     *
     */
    private final class EscapeKeyHandler implements EventHandler<KeyEvent> {

        @Override
        public void handle(KeyEvent event) {

            // the ESCAPE key lets the user reset the zoom level
            if (KeyCode.ESCAPE.equals(event.getCode())) {
                resetAxisBounds();
                hideInfo();
            }
        }

        private void resetAxisBounds() {
            xAxis.setAutoRanging(true);
            yAxis.setAutoRanging(true);
        }

        private void hideInfo() {
            infoLabel.setVisible(false);
        }
    }

}

//SelectionRectangle.java class

package testlinechartgraphs;

/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */

/**
 *
 * @author **************
 */
import javafx.scene.shape.Rectangle;

/**
 * Represents an area on the screen that was selected by a mouse drag operation.
 *
 */
public class SelectionRectangle extends Rectangle {

    private static final String STYLE_CLASS_SELECTION_BOX = "chart-selection-rectangle";

    public SelectionRectangle() {

        getStyleClass().addAll(STYLE_CLASS_SELECTION_BOX);
        setVisible(false);
        setManaged(false);
        setMouseTransparent(true);

    }
}

// fancychart.css file

.chart-line-symbol {
    -fx-scale-x: 0.5;
    -fx-scale-y: 0.5;
}

.chart-popup-label {
    -fx-padding: 1 3 1 3;
    -fx-border-radius: 1;
    -fx-border-width: 1;
    -fx-opacity: 0.7;
    -fx-effect: dropshadow( two-pass-box , rgba(0,0,0,0.3) , 8, 0.0 , 0 , 3 );
}

.chart-legend-item {
    -fx-padding : 1 23 1 23;
}

.chart-legend-item-symbol {
   -fx-scale-x: 0.8;
   -fx-scale-y: 0.8;
}



.chart-selection-rectangle {
    -fx-stroke: rgba(135, 206, 250, 0.8);
    -fx-stroke-type: inside;
    -fx-fill: rgba(135, 206, 250, 0.2);
}







#zoomInfoLabel {
    -fx-background-color: rgba(135, 206, 250, 0.8);
    -fx-font-size: 14;
    -fx-padding: 3;
    -fx-background-radius: 2;
}

Any help will be greatly appreciated to resolve this issue.

thanks,

Upvotes: 3

Views: 1709

Answers (1)

Roland
Roland

Reputation: 18415

Your order is wrong. You have

    final StackPane chartContainer = new StackPane();
    Zoom zoom = new Zoom(lineChart, chartContainer);
    chartContainer.getChildren().add(lineChart);

Which means you first create the container, then add the zoom rectangle, then add the chart. So the zoom rectangle is on the back of the chart.

You need to have it this way:

    final StackPane chartContainer = new StackPane();
    chartContainer.getChildren().add(lineChart);
    Zoom zoom = new Zoom(lineChart, chartContainer);

Upvotes: 2

Related Questions