Skip to content

Instantly share code, notes, and snippets.

@james-d
Created October 31, 2013 16:29
Show Gist options
  • Star 14 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save james-d/7252698 to your computer and use it in GitHub Desktop.
Save james-d/7252698 to your computer and use it in GitHub Desktop.
Example of a LineChart that can be zoomed via mouse.
import java.util.Collections;
import java.util.Random;
import javafx.application.Application;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.chart.Axis;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.control.Button;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class ZoomableLineChart extends Application {
private static final int NUM_DATA_POINTS = 1000 ;
@Override
public void start(Stage primaryStage) {
final LineChart<Number, Number> chart = createChart();
final StackPane chartContainer = new StackPane();
chartContainer.getChildren().add(chart);
final Rectangle zoomRect = new Rectangle();
zoomRect.setManaged(false);
zoomRect.setFill(Color.LIGHTSEAGREEN.deriveColor(0, 1, 1, 0.5));
chartContainer.getChildren().add(zoomRect);
setUpZooming(zoomRect, chart);
final HBox controls = new HBox(10);
controls.setPadding(new Insets(10));
controls.setAlignment(Pos.CENTER);
final Button zoomButton = new Button("Zoom");
final Button resetButton = new Button("Reset");
zoomButton.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
doZoom(zoomRect, chart);
}
});
resetButton.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
final NumberAxis xAxis = (NumberAxis)chart.getXAxis();
xAxis.setLowerBound(0);
xAxis.setUpperBound(1000);
final NumberAxis yAxis = (NumberAxis)chart.getYAxis();
yAxis.setLowerBound(0);
yAxis.setUpperBound(1000);
zoomRect.setWidth(0);
zoomRect.setHeight(0);
}
});
final BooleanBinding disableControls =
zoomRect.widthProperty().lessThan(5)
.or(zoomRect.heightProperty().lessThan(5));
zoomButton.disableProperty().bind(disableControls);
controls.getChildren().addAll(zoomButton, resetButton);
final BorderPane root = new BorderPane();
root.setCenter(chartContainer);
root.setBottom(controls);
final Scene scene = new Scene(root, 600, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
private LineChart<Number, Number> createChart() {
final NumberAxis xAxis = createAxis();
final NumberAxis yAxis = createAxis();
final LineChart<Number, Number> chart = new LineChart<>(xAxis, yAxis);
chart.setAnimated(false);
chart.setCreateSymbols(false);
chart.setData(generateChartData());
return chart ;
}
private NumberAxis createAxis() {
final NumberAxis xAxis = new NumberAxis();
xAxis.setAutoRanging(false);
xAxis.setLowerBound(0);
xAxis.setUpperBound(1000);
return xAxis;
}
private ObservableList<Series<Number, Number>> generateChartData() {
final Series<Number, Number> series = new Series<>();
series.setName("Data");
final Random rng = new Random();
for (int i=0; i<NUM_DATA_POINTS; i++) {
Data<Number, Number> dataPoint = new Data<Number, Number>(i, rng.nextInt(1000));
series.getData().add(dataPoint);
}
return FXCollections.observableArrayList(Collections.singleton(series));
}
private void setUpZooming(final Rectangle rect, final Node zoomingNode) {
final ObjectProperty<Point2D> mouseAnchor = new SimpleObjectProperty<>();
zoomingNode.setOnMousePressed(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
mouseAnchor.set(new Point2D(event.getX(), event.getY()));
rect.setWidth(0);
rect.setHeight(0);
}
});
zoomingNode.setOnMouseDragged(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
double x = event.getX();
double y = event.getY();
rect.setX(Math.min(x, mouseAnchor.get().getX()));
rect.setY(Math.min(y, mouseAnchor.get().getY()));
rect.setWidth(Math.abs(x - mouseAnchor.get().getX()));
rect.setHeight(Math.abs(y - mouseAnchor.get().getY()));
}
});
}
private void doZoom(Rectangle zoomRect, LineChart<Number, Number> chart) {
Point2D zoomTopLeft = new Point2D(zoomRect.getX(), zoomRect.getY());
Point2D zoomBottomRight = new Point2D(zoomRect.getX() + zoomRect.getWidth(), zoomRect.getY() + zoomRect.getHeight());
final NumberAxis yAxis = (NumberAxis) chart.getYAxis();
Point2D yAxisInScene = yAxis.localToScene(0, 0);
final NumberAxis xAxis = (NumberAxis) chart.getXAxis();
Point2D xAxisInScene = xAxis.localToScene(0, 0);
double xOffset = zoomTopLeft.getX() - yAxisInScene.getX() ;
double yOffset = zoomBottomRight.getY() - xAxisInScene.getY();
double xAxisScale = xAxis.getScale();
double yAxisScale = yAxis.getScale();
xAxis.setLowerBound(xAxis.getLowerBound() + xOffset / xAxisScale);
xAxis.setUpperBound(xAxis.getLowerBound() + zoomRect.getWidth() / xAxisScale);
yAxis.setLowerBound(yAxis.getLowerBound() + yOffset / yAxisScale);
yAxis.setUpperBound(yAxis.getLowerBound() - zoomRect.getHeight() / yAxisScale);
System.out.println(yAxis.getLowerBound() + " " + yAxis.getUpperBound());
zoomRect.setWidth(0);
zoomRect.setHeight(0);
}
public static void main(String[] args) {
launch(args);
}
}
@liguoqinjim
Copy link

wonderful!

@nkolban
Copy link

nkolban commented Apr 15, 2015

Absolutely great! Perfection would be constraining the rectangle to the chart area. Also, copious comments describing the logic would really help for modification.

@EliaRohana
Copy link

thanks

@polisettyManoj
Copy link

polisettyManoj commented Feb 19, 2018

how can i implement the same functionality to a Real time line chart..Can any one help me ?? Thanks in advance :) @james-d #james-d help needed :)

this is my sample code for real-time line chart...

 import java.util.concurrent.ConcurrentLinkedQueue;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.logging.Level;
    import java.util.logging.Logger;
    import javafx.animation.AnimationTimer;
    import javafx.animation.Timeline;
    import javafx.application.Application;
    import static javafx.application.Application.launch;
    import javafx.scene.Scene;
    import javafx.scene.chart.LineChart;
    import javafx.scene.chart.NumberAxis;
    import javafx.scene.chart.XYChart.Data;
    import javafx.scene.chart.XYChart.Series;
    import javafx.stage.Stage;



    public class MainApp extends Application {

    private static final int MAX_DATA_POINTS = 100;

    private Series series;
    private int xSeriesData = 100;
    private ConcurrentLinkedQueue<Number> dataQ = new ConcurrentLinkedQueue<Number>();
    private ExecutorService executor;
    private AddToQueue addToQueue;
    private Timeline timeline2;
    private NumberAxis xAxis;

    private void init(Stage primaryStage) throws Exception {

        xAxis = new NumberAxis(0, MAX_DATA_POINTS, MAX_DATA_POINTS / 100);
        xAxis.setForceZeroInRange(false);
        xAxis.setAutoRanging(false);

        NumberAxis yAxis = new NumberAxis();
        yAxis.setAutoRanging(true);

        //-- Chart
        final LineChart<Number, Number> sc = new LineChart<Number, Number>(xAxis, yAxis) {
            // Override to remove symbols on each data point            
            @Override
            protected void dataItemAdded(Series<Number, Number> series, int itemIndex, Data<Number, Number> item) {

            }
        };
        sc.setAnimated(false);
        sc.setId("liveAreaChart");
        sc.setTitle("RealTime Area Chart");

        //-- Chart Series
        series = new LineChart.Series<Number, Number>();
        series.setName("Area Chart Series");
        sc.getData().add(series);

        primaryStage.setScene(new Scene(sc));
    }

    @Override
    public void start(Stage primaryStage) throws Exception {

        init(primaryStage);
        primaryStage.show();

        executor = Executors.newCachedThreadPool();

        addToQueue = new AddToQueue();

        executor.execute(addToQueue);

        //-- Prepare Timeline
        prepareTimeline();

    }

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

    private class AddToQueue implements Runnable {

        public void run() {
            try {

                dataQ.add(Math.random());

                Thread.sleep(50);

                executor.execute(this);

            } catch (InterruptedException ex) {

                Logger.getLogger(MainApp.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }

    //-- Timeline gets called in the JavaFX Main thread
    private void prepareTimeline() {
        // Every frame to take any data from queue and add to chart

        new AnimationTimer() {

            @Override
            public void handle(long now) {

                addDataToSeries();

            }
        }.start();

    }

    private void addDataToSeries() {

        for (int i = 0; i < 20; i++) {

            if (dataQ.isEmpty()) {

                break;
            }

            series.getData().add(new LineChart.Data(xSeriesData++, dataQ.remove()));

        }
        // remove points to keep us at no more than MAX_DATA_POINTS
        if (series.getData().size() > MAX_DATA_POINTS) {
            series.getData().remove(0, series.getData().size() - MAX_DATA_POINTS);
        }

        // update 
        xAxis.setLowerBound(xSeriesData - MAX_DATA_POINTS);
        int m = xSeriesData - MAX_DATA_POINTS;

        xAxis.setUpperBound(xSeriesData - 1);
        int j = xSeriesData - 1;

    }

}

Copy link

ghost commented Feb 21, 2018

@polisettyManoj
Hey i just saw your post.

Step 1. Create class called Data -> Example Company (AAPL)

Step 2. There are just a few changes you need to make

  • Make a ComboBox Field within MainApp

`private ComboBox companyComboBox;
private String symbol;'

  • From the class company, you are going to make two fields that are called symbol and company -> make getMethods

  • Make a constructor for the Company class
    - Company
    - Parameters: String symbol, String name
    - super
    -this.symbol = symbol

  • Make a method called getCompanies()
    public static List<Company> getCompanies() { List<Company> company =new ArrayList<>(); list.add(new Company("AAPL", "AAPL"); return company }
    Part 3: Head back to MainApp

  • create a field called listCompany

    • ObservableList listCompany = FXCollections.observableArrayList();
  • within the init Method
    - ComboBox<Company> companyComboBox = new ComboBox<>(); companyComboBox.setItems(listCompany);
    - companyComboBox.getSelectionModel().selectedItemProperty().addListener((
    ChangeListener<? super Company>)
    (ObservableValue<? extends Company> observable, Company oldValue, Company newValue) -> {
    if (newValue.getSymbol != null) {
    symbol = newValue.getSymbol();
    // call to method start()
    start();
    } else {
    // do something else
    }
    }

  • next add companyComboBox to Hbox

  • `private static void start() {

      executor = Executors.newCachedThreadPool(r ->  {
      	
      	Thread thread = new Thread(r);
      	thread.setDaemon(true);
      	return thread;
      
      });
      
      AddToQueue add = new AddToQueue();
      executor.execute(add);
      prepareTimeline();
    

    }`

  • and finally to add the real-time data
    - Data is provided by "https://github.com/WojciechZankowski/iextrading4j"
    - private static class AddToQueue implements Runnable {

      private IEXTradingClient iexTradingClient;
      private Quote quote;
    
    
      @Override
      public void run() {
      	try {
              BigDecimal last = getQuotes().getLatestPrice();
      		BigDecimal volume = getQuotes().getLatestVolume();
    
      		queue1.add(last);
      		queue2.add(volume);
    
      		Thread.sleep(1000);
      		executor.execute(this);
      		
      	} catch (InterruptedException e) {
      		e.printStackTrace();
      	}
      }
    
      private Quote getQuotes() {
          new LiveStockQuoteService(symbol);
          iexTradingClient = IEXTradingClient.create();
          this.quote = iexTradingClient.executeRequest(new QuoteRequestBuilder()
                  .withSymbol(symbol)
                  .build());
          return quote;
      }
    

    }

  • The rest should be pretty easy to do yourself

@BonkersTBobcat
Copy link

BonkersTBobcat commented Jun 22, 2020

There's a small bug in this, where the zoom is actually a bit to the right of the zoom band that you see visually. This is caused by the yAxis point (0,0) being at the top-left of the yAxis, and not taking into account the thickness of the axis. When the code gets the xOffset, it should take into acount the right side of the axis, not the left.

Line 150 should be as follows:

double xOffset = zoomTopLeft.getX() - (yAxisInScene.getX() + yAxis.getWidth());

@AndreNL22
Copy link

Line 150 should be as follows:

double xOffset = zoomTopLeft.getX() - (yAxisInScene.getX() - yAxis.getWidth());

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment