Zoom Bar Chart with mouse wheel - javafx

I found many examples how to zoom in charts but I'm looking for basic example in which the user can scroll with the mouse wheel.
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.chart.BarChart;
import javafx.scene.chart.CategoryAxis;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.stage.Stage;
public class BarChartSample extends Application {
final static String austria = "Austria";
final static String brazil = "Brazil";
final static String france = "France";
final static String italy = "Italy";
final static String usa = "USA";
#Override public void start(Stage stage) {
stage.setTitle("Bar Chart Sample");
final CategoryAxis xAxis = new CategoryAxis();
final NumberAxis yAxis = new NumberAxis();
final BarChart<String,Number> bc =
new BarChart<String,Number>(xAxis,yAxis);
bc.setTitle("Country Summary");
xAxis.setLabel("Country");
yAxis.setLabel("Value");
XYChart.Series series1 = new XYChart.Series();
series1.setName("2003");
series1.getData().add(new XYChart.Data(austria, 25601.34));
series1.getData().add(new XYChart.Data(brazil, 20148.82));
series1.getData().add(new XYChart.Data(france, 10000));
series1.getData().add(new XYChart.Data(italy, 35407.15));
series1.getData().add(new XYChart.Data(usa, 12000));
XYChart.Series series2 = new XYChart.Series();
series2.setName("2004");
series2.getData().add(new XYChart.Data(austria, 57401.85));
series2.getData().add(new XYChart.Data(brazil, 41941.19));
series2.getData().add(new XYChart.Data(france, 45263.37));
series2.getData().add(new XYChart.Data(italy, 117320.16));
series2.getData().add(new XYChart.Data(usa, 14845.27));
XYChart.Series series3 = new XYChart.Series();
series3.setName("2005");
series3.getData().add(new XYChart.Data(austria, 45000.65));
series3.getData().add(new XYChart.Data(brazil, 44835.76));
series3.getData().add(new XYChart.Data(france, 18722.18));
series3.getData().add(new XYChart.Data(italy, 17557.31));
series3.getData().add(new XYChart.Data(usa, 92633.68));
Scene scene = new Scene(bc,800,600);
bc.getData().addAll(series1, series2, series3);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Any help is welcome.
P.S I tested to insert the code into ScrollPane but I don't see any effect of using it.
ScrollPane s1 = new ScrollPane();
//s1.setPrefSize(620, 620);
s1.setHbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); // Horizontal scroll bar
s1.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); // Vertical scroll bar
s1.setContent(bc);

Take a look at this answer.
I've got it working with this code:
final double SCALE_DELTA = 1.1;
bc.setOnScroll(new EventHandler<ScrollEvent>() {
public void handle(ScrollEvent event) {
event.consume();
if (event.getDeltaY() == 0) {
return;
}
double scaleFactor = (event.getDeltaY() > 0) ? SCALE_DELTA : 1 / SCALE_DELTA;
bc.setScaleX(bc.getScaleX() * scaleFactor);
bc.setScaleY(bc.getScaleY() * scaleFactor);
}
});
bc.setOnMousePressed(new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
if (event.getClickCount() == 2) {
bc.setScaleX(1.0);
bc.setScaleY(1.0);
}
}
});

You should check out Jason Winnebeck's JFXUtils. He has developed a really nice library for chart zooming as well as panning (dragging the zoomed chart area like Google Maps).
You can also easily reset the chart. I wired it up to double click like you suggested.
You can specify the format of the axis labels on his custom StableTicksAxis (I display dates and times on my x axis).
Zooming works from a click-and-drag on the chart area or on the axis for zooming in to the entire span of either the x or y axis.
Mouse wheel zooming is baked right in.
He has a convenience method for super simple default setup, which wraps the chart in the components required to implement zooming.

Related

Displaying gaps on charts in JavaFX

I'm using XYChart in JavaFX 8 and I would like to display gaps for empty cells for the specified series. When I passed null value then I got NullPointerException:
series.get(index).getData().add(new XYChart.Data<>(Key, null));
I also found the bug https://bugs.openjdk.java.net/browse/JDK-8092134 describing this problem, but I don't know is it still actual.
Does anyone know how to resolve this problem?
Best regards,
Michael
As it is quite evident that, this feature is not included.. and if you are very desperate to get this behavior, you can try the below logic.
Having said that, there can be many better ways, but this answer is to give you some initial idea about how you can tweak the current implementation using the protected methods of the chart.
The idea is.. once the plot children layout is done, we recompute the logic of rendering the line path.. and remove the unwanted data points. And as mentioned, this is just for idea purpose only, if you have more data series, then you may need to work accordingly.
[UPDATE] : If you want the paths/data points for each series, append the ".series" to the ".chart-series-line" and ".data" style classes.
Please check the below demo:
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.chart.*;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.layout.VBox;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.shape.PathElement;
import javafx.stage.Stage;
import java.util.*;
public class XYChartDemo extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
VBox root = new VBox();
CategoryAxis xAxis = new CategoryAxis();
xAxis.setLabel("days");
NumberAxis yAxis = new NumberAxis();
yAxis.setLabel("USD");
// AS OF NOW THIS IS THE CORE POINT I AM RELYING ON !! YOU CAN THINK OF A BETTER APPROACH TO IDENTIFY THE GAP POINTS.
List<Integer> s0GapIndexes = Arrays.asList(3, 7);
List<Integer> s1GapIndexes = Arrays.asList(4, 8);
Map<Integer, List<Integer>> seriesGap = new HashMap<>();
seriesGap.put(0, s0GapIndexes);
seriesGap.put(1, s1GapIndexes);
XYChart.Series<String, Double> series0 = new XYChart.Series<>();
series0.getData().add(new Data<>("2000-01-01", 13.2));
series0.getData().add(new Data<>("2000-01-02", 10.1));
series0.getData().add(new Data<>("2000-01-03", 14.1));
series0.getData().add(new Data<>("2000-01-04", 0.0)); // gap (INDEX 3)
series0.getData().add(new Data<>("2000-01-05", 6.3));
series0.getData().add(new Data<>("2000-01-06", 9.82));
series0.getData().add(new Data<>("2000-01-07", 12.82));
series0.getData().add(new Data<>("2000-01-08", 0.0)); // gap (INDEX 7)
series0.getData().add(new Data<>("2000-01-09", 4.82));
series0.getData().add(new Data<>("2000-01-10", 8.82));
series0.getData().add(new Data<>("2000-01-11", 8.82));
XYChart.Series<String, Double> series1 = new XYChart.Series<>();
series1.getData().add(new Data<>("2000-01-01", 20.2));
series1.getData().add(new Data<>("2000-01-02", 14.1));
series1.getData().add(new Data<>("2000-01-03", 7.1));
series1.getData().add(new Data<>("2000-01-04", 9.0));
series1.getData().add(new Data<>("2000-01-05", 0.0)); // gap (INDEX 4)
series1.getData().add(new Data<>("2000-01-06", 5.32));
series1.getData().add(new Data<>("2000-01-07", 11.0));
series1.getData().add(new Data<>("2000-01-08", 15.3));
series1.getData().add(new Data<>("2000-01-09", 0.0)); // gap (INDEX 8)
series1.getData().add(new Data<>("2000-01-10", 4.82));
series1.getData().add(new Data<>("2000-01-11", 6.82));
CustomLineChart lineChart = new CustomLineChart(xAxis, yAxis, seriesGap);
lineChart.getData().addAll(series0, series1);
root.getChildren().addAll(lineChart);
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}
class CustomLineChart<X, Y> extends LineChart<X, Y> {
Map<Integer, List<Integer>> seriesGap;
public CustomLineChart(Axis<X> xAxis, Axis<Y> yAxis, Map<Integer, List<Integer>> seriesGap) {
super(xAxis, yAxis);
this.seriesGap = seriesGap;
}
#Override
protected void layoutPlotChildren() {
super.layoutPlotChildren();
updatePath();
updateDataPoints();
}
private void updatePath() {
seriesGap.forEach((seriesNo, gapIndexes) -> {
Path path = (Path) lookup(".chart-series-line.series" + seriesNo);
System.out.println(path);
if (!path.getElements().isEmpty()) {
int dataSize = getData().get(seriesNo).getData().size();
int pathEleSize = path.getElements().size();
// Just ensuring we are dealing with right path
if (pathEleSize == dataSize + 1) {
// Build a new path, by jumping the gap points
List<PathElement> newPath = new ArrayList<>();
newPath.add(path.getElements().get(0));
for (int i = 1; i < path.getElements().size(); i++) {
if (gapIndexes.contains(i - 1)) {
LineTo lt = (LineTo) path.getElements().get(i + 1);
newPath.add(new MoveTo(lt.getX(), lt.getY()));
} else {
newPath.add(path.getElements().get(i));
}
}
// Update the new path to the current path.
path.getElements().clear();
path.getElements().addAll(newPath);
}
}
});
}
private void updateDataPoints() {
Group plotContent = (Group) lookup(".plot-content");
seriesGap.forEach((seriesNo, gapIndexes) -> {
// Remove all data points at the gap indexes
gapIndexes.forEach(i -> {
Node n = lookup(".series" + seriesNo + ".data" + i);
if (n != null) {
plotContent.getChildren().remove(n);
}
});
});
}
}
}

Controlling the size of two XYCharts

I have two XY charts and I want them to be placed one on the top and the other at the bottom of a JavaFX application.
Both graph widths should fill their "container" and the bottom graph should be x times smaller than the top graph.
For the sake of reproducibility, here is an example built from Oracle's tutorial:
import javafx.application.Application;
import javafx.geometry.Side;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.chart.CategoryAxis;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class Test extends Application {
#Override
public void start(Stage stage) {
stage.setTitle("Line Chart Sample");
final CategoryAxis xAxis1 = new CategoryAxis();
final NumberAxis yAxis1 = new NumberAxis();
xAxis1.setLabel("Month");
final LineChart<String, Number> lineChart1 =
new LineChart<>(xAxis1, yAxis1);
final CategoryAxis xAxis2 = new CategoryAxis();
xAxis2.setLabel("Month");
final NumberAxis yAxis2 = new NumberAxis();
final LineChart<String, Number> lineChart2 =
new LineChart<>(xAxis2, yAxis2);
lineChart1.setLegendVisible(false);
lineChart2.setLegendVisible(false);
XYChart.Series series1 = new XYChart.Series();
series1.setName("Portfolio 1");
series1.getData().add(new XYChart.Data("Jan", 23));
series1.getData().add(new XYChart.Data("Feb", 14));
series1.getData().add(new XYChart.Data("Mar", 15));
series1.getData().add(new XYChart.Data("Apr", 24));
series1.getData().add(new XYChart.Data("May", 34));
series1.getData().add(new XYChart.Data("Jun", 36));
series1.getData().add(new XYChart.Data("Jul", 22));
series1.getData().add(new XYChart.Data("Aug", 45));
series1.getData().add(new XYChart.Data("Sep", 43));
series1.getData().add(new XYChart.Data("Oct", 17));
series1.getData().add(new XYChart.Data("Nov", 29));
series1.getData().add(new XYChart.Data("Dec", 25));
XYChart.Series series2 = new XYChart.Series();
series2.setName("Portfolio 2");
series2.getData().add(new XYChart.Data("Jan", 33));
series2.getData().add(new XYChart.Data("Feb", 34));
series2.getData().add(new XYChart.Data("Mar", 25));
series2.getData().add(new XYChart.Data("Apr", 44));
series2.getData().add(new XYChart.Data("May", 39));
series2.getData().add(new XYChart.Data("Jun", 16));
series2.getData().add(new XYChart.Data("Jul", 55));
series2.getData().add(new XYChart.Data("Aug", 54));
series2.getData().add(new XYChart.Data("Sep", 48));
series2.getData().add(new XYChart.Data("Oct", 27));
series2.getData().add(new XYChart.Data("Nov", 37));
series2.getData().add(new XYChart.Data("Dec", 29));
lineChart1.getData().addAll(series1);
lineChart2.getData().addAll(series2);
Scene scene = new Scene(createVBoxLayout(lineChart1, lineChart2), 800, 600);
stage.setScene(scene);
stage.show();
}
private Parent createVBoxLayout(XYChart chart1, XYChart chart2) {
VBox vBox = new VBox(chart1, chart2);
chart1.prefHeight(200);
chart1.minHeight(200);
chart1.maxHeight(200);
chart2.prefHeight(400);
chart2.minHeight(400);
chart2.maxHeight(400);
return vBox;
}
private Parent createGridLayout(XYChart chart1, XYChart chart2) {
GridPane gridPane = new GridPane();
gridPane.add(chart1, 0, 0, 1, 1);
gridPane.add(chart2, 0, 1, 1, 2);
return gridPane;
}
public static void main(String[] args) {
launch(args);
}
}
The graph on the bottom should be twice as big as the one on the top.
I have tried two approaches:
Method createVBoxLayout creates a VBox and uses the prefered/min/max sizes to modify the chart size.
Here is the result:
Clearly, the bottom graph is not twice as big as the graph at the top. Moreover, the size ratio would not be kept when the window is resized.
The method createGridLayout creates a GridPane, here is the result:
Now, it's the top graph that is twice as big as the bottom one, which is the opposite of what is coded in the method! Moreover, the graphs do not fill the width of the window. Finally, when resizing the window, the charts overlap and don't keep their relative sizes (should be 1/3 - 2/3).
The methods prefHeight(...), minHeight(...), etc do not set the preferred height, etc. They return the computed values for those, with the argument being the width for the chart (i.e. chart1.prefHeight(200) means "tell me the preferred height if the width is 200"). See the documentation.
You need
chart1.setPrefHeight(200);
chart1.setMinHeight(200);
chart1.setMaxHeight(200);
etc.
For the grid pane version, use RowConstraints and ColumnConstraints to control the layout of the grid. The rows are simply resized to contain the nodes they contain, so making a chart span two rows will probably just give one of the rows zero height. (Again, see the documentation.)
private Parent createGridLayout(XYChart chart1, XYChart chart2) {
GridPane gridPane = new GridPane();
gridPane.add(chart1, 0, 0, 1, 1);
gridPane.add(chart2, 0, 1, 1, 2);
RowConstraints top = new RowConstraints(200);
RowConstraints bottom = new RowConstraints(400);
gridPane.getRowConstraints().addAll(top, bottom);
ColumnConstraints cc = new ColumnConstraints();
cc.setHgrow(Priority.ALWAYS);
gridPane.getColumnConstraints().add(cc);
return gridPane;
}
or
private Parent createGridLayout(XYChart chart1, XYChart chart2) {
GridPane gridPane = new GridPane();
gridPane.add(chart1, 0, 0);
gridPane.add(chart2, 0, 1);
RowConstraints top = new RowConstraints();
top.setPercentHeight(100.0 / 3.0);
RowConstraints bottom = new RowConstraints();
bottom.setPercentHeight(200.0 / 3.0);
gridPane.getRowConstraints().addAll(top, bottom);
ColumnConstraints cc = new ColumnConstraints();
cc.setHgrow(Priority.ALWAYS);
gridPane.getColumnConstraints().add(cc);
return gridPane;
}

JavaFX : Barchart with multiple (2) y axis

I would like to create a JavaFX barchart with takes 2 y-axis, since the series-values are too wide apart, resulting in second series not showing off properly in the chart.
I have dummed-down the code to its very basics, for the purpose of this question:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.chart.BarChart;
import javafx.scene.chart.CategoryAxis;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.stage.Stage;
public class MultiAxisTryout extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
stage.setTitle("Bar Chart Multi Example");
final CategoryAxis xAxis = new CategoryAxis();
final NumberAxis yAxis = new NumberAxis();
final BarChart bc =
new BarChart(xAxis,yAxis);
bc.setTitle("Cost & Delta by JV");
xAxis.setLabel("JV");
XYChart.Series series1 = new XYChart.Series();
series1.setName("Cost");
XYChart.Series series2 = new XYChart.Series();
series2.setName("Delta");
series1.getData().add(new XYChart.Data("1000000", 25000));
series1.getData().add(new XYChart.Data("1200000", 30000));
series2.getData().add(new XYChart.Data("1000000", 22));
series2.getData().add(new XYChart.Data("1200000", 86));
Scene scene = new Scene(bc ,1800,1000);
scene.setFill(null);
bc.setAnimated(false);
bc.getData().addAll(series1,series2);
stage.setScene(scene);
stage.show();
}
}
The result looks like this :
As you can see, the second series is not showing off very well in the barchart, so it would be nice to have a second y-axis with its own scale, showing off better in the barchart.
Is this possible in JavaFx ?

How do I change the size of a chart symbol in a javafx scatter chart?

I can change the symbol shape, colour, padding, radius but I can't seem to find a CSS item that will change the size of the symbol.
I had thought that if I recreated the symbol with smaller SVG path, that might be a way to do it, but it appears that my smaller shape just gets scaled up anyway.
I need smaller symbols but so far have not found a way to scale them.
Here is the code, and the loop where you can find all symbols and change Width and Height. I know only this way. If you try this in CSS you will change also legend under chart.
public class Chart extends Application {
#Override
public void start(Stage stage) {
//set Axis
final NumberAxis xAxis = new NumberAxis();
final NumberAxis yAxis = new NumberAxis();
//chart
final ScatterChart<Number, Number> lineChart = new ScatterChart<Number, Number>(xAxis, yAxis);
//prepare series
XYChart.Series series1 = new XYChart.Series();
series1.getData().add(new XYChart.Data(1, 2));
series1.getData().add(new XYChart.Data(2, 2));
series1.getData().add(new XYChart.Data(3, 1));
series1.getData().add(new XYChart.Data(3, 3));
series1.getData().add(new XYChart.Data(5, 2));
HBox hbox = new HBox();
//add series to chart
lineChart.getData().addAll(series1);
//take all series
for (XYChart.Series<Number, Number> series : lineChart.getData()) {
//for all series, take date, each data has Node (symbol) for representing point
for (XYChart.Data<Number, Number> data : series.getData()) {
// this node is StackPane
StackPane stackPane = (StackPane) data.getNode();
stackPane.setPrefWidth(50);
stackPane.setPrefHeight(50);
}
}
hbox.getChildren().addAll(lineChart);
Scene scene = new Scene(hbox, 800, 600);
scene.getStylesheets().add(getClass().getResource("/resources/chart.css").toExternalForm());
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Here is CSS wher you can set for this series radius
.default-color0.chart-symbol {
-fx-background-radius: 30px;
}
In this example symbols are bigger
This is how I created a rectangle in SVG path then changed the size in css.
.chart-symbol{
-fx-shape: "M 20.0 20.0 v24.0 h 10.0 v-24 Z";
-fx-padding: 7px 7px 7px 7px;
}

JavaFX Class LineChart zoom operation doesn't work

I have a problem I can not figure out. Please let me put the code first for better explanation:
public class TestXYChart extends Application {
NumberAxis xAxis = new NumberAxis(); // 1
NumberAxis yAxis = new NumberAxis(); // 2
XYChart.Series series1 = new XYChart.Series();
#Override
public void start(Stage primaryStage) {
StackPane root = new StackPane();
Scene scene = new Scene(root, 300, 250);
LineChart lineChart = new LineChart<Number, Number>(xAxis, yAxis);
root.setOnScroll(scrollHandler);
root.getChildren().addAll(lineChart);
series1.getData().add(new XYChart.Data(1, 20));
series1.getData().add(new XYChart.Data(2, 40));
series1.getData().add(new XYChart.Data(3, 100));
series1.getData().add(new XYChart.Data(4, 50));
series1.getData().add(new XYChart.Data(5, 170));
series1.getData().add(new XYChart.Data(6, 190));
series1.getData().add(new XYChart.Data(7, 30));
series1.getData().add(new XYChart.Data(8, 39));
series1.getData().add(new XYChart.Data(9, 44));
series1.getData().add(new XYChart.Data(10, 78));
series1.getData().add(new XYChart.Data(11, 93));
series1.getData().add(new XYChart.Data(12, 75));
// this.loadIntoDataBaseFromFile();
lineChart.getData().addAll(series1);
lineChart.setAnimated(true);
lineChart.setAlternativeColumnFillVisible(false);
lineChart.setAlternativeRowFillVisible(false);
primaryStage.setTitle("Hello World!");
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
EventHandler scrollHandler = new EventHandler<ScrollEvent>(){
#Override
public void handle(ScrollEvent event) {
System.out.println("Handling Scroll Event");
xAxis.setLowerBound(xAxis.getLowerBound() + 1);
xAxis.setUpperBound(xAxis.getUpperBound() - 1);
}
};
}
I use the code above to test whether I can operate the LineChart, especially when I scroll mouse, the LineChart will zoom in/out.
I found the fact that exactly as above code, it doesn't work. But if we just change the first two line codes, change from:
NumberAxis xAxis = new NumberAxis(); // 1
NumberAxis yAxis = new NumberAxis(); // 2
to:
NumberAxis xAxis = new NumberAxis(0,12,0.5); // 1
NumberAxis yAxis = new NumberAxis(0,300,10); // 2
Everything works well. So, why it happens like this?Thank you.
And, if I seperate the new NumberAxis(...) instance from the declaration NumberAxis xAxis/yAxis, it doesn't work too.
The difference between using an empty constructor or with arguments is clear if you have a look at NumberAxis JavaDoc:
public NumberAxis()
Create a auto-ranging NumberAxis
and
public NumberAxis(double lowerBound, double upperBound, double tickUnit)
Create a non-auto-ranging NumberAxis with the given upper bound, lower bound and tick unit
Basically, the first one has autoRanging set to true, what means it will try to show the data, no matter how you modify the bounds of the axis.
On the contrary, the second one has autoRanging set to false, so any change in the axis bounds will be reflected on the chart.
Note that you can set this in the first case to change the default behaviour:
xAxis.setAutoRanging(false);

Resources