-
-
Save james-d/7252698 to your computer and use it in GitHub Desktop.
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); | |
} | |
} |
Absolutely great! Perfection would be constraining the rectangle to the chart area. Also, copious comments describing the logic would really help for modification.
thanks
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;
}
}
@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
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());
Line 150 should be as follows:
double xOffset = zoomTopLeft.getX() - (yAxisInScene.getX() - yAxis.getWidth());
wonderful!