Task
I'd like to create a Gantt chart with JavaFX from scratch.
Example
Let's say I have 2 machines Machine A and Machine B and they have 2 states Online (green) and Offline (red). I'd like to show their states at given time intervals in horizontal colored Bars, X-axis is the Date axis, Y-axis is the machine axis.
Question
Where do I start? Which classes do I need to use? It would be great if anyone has a minimal example and could share it.
Thank you very much.
It turned out that the BubbleChart source was a good example to start from.
Basically you can use a modified version of the XYChart and its data. What you need to do is to add extradata like how long the value is valid and some style for the coloring.
What's left is to use a date axis and hence date values instead of numeric ones.
Here's what I've come up with in case anyone wants to toy around with it:
Another example:
The source:
GanttChart.java:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javafx.beans.NamedArg;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.chart.Axis;
import javafx.scene.chart.CategoryAxis;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.ValueAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Rectangle;
public class GanttChart<X,Y> extends XYChart<X,Y> {
public static class ExtraData {
public long length;
public String styleClass;
public ExtraData(long lengthMs, String styleClass) {
super();
this.length = lengthMs;
this.styleClass = styleClass;
}
public long getLength() {
return length;
}
public void setLength(long length) {
this.length = length;
}
public String getStyleClass() {
return styleClass;
}
public void setStyleClass(String styleClass) {
this.styleClass = styleClass;
}
}
private double blockHeight = 10;
public GanttChart(#NamedArg("xAxis") Axis<X> xAxis, #NamedArg("yAxis") Axis<Y> yAxis) {
this(xAxis, yAxis, FXCollections.<Series<X, Y>>observableArrayList());
}
public GanttChart(#NamedArg("xAxis") Axis<X> xAxis, #NamedArg("yAxis") Axis<Y> yAxis, #NamedArg("data") ObservableList<Series<X,Y>> data) {
super(xAxis, yAxis);
if (!(xAxis instanceof ValueAxis && yAxis instanceof CategoryAxis)) {
throw new IllegalArgumentException("Axis type incorrect, X and Y should both be NumberAxis");
}
setData(data);
}
private static String getStyleClass( Object obj) {
return ((ExtraData) obj).getStyleClass();
}
private static double getLength( Object obj) {
return ((ExtraData) obj).getLength();
}
#Override protected void layoutPlotChildren() {
for (int seriesIndex=0; seriesIndex < getData().size(); seriesIndex++) {
Series<X,Y> series = getData().get(seriesIndex);
Iterator<Data<X,Y>> iter = getDisplayedDataIterator(series);
while(iter.hasNext()) {
Data<X,Y> item = iter.next();
double x = getXAxis().getDisplayPosition(item.getXValue());
double y = getYAxis().getDisplayPosition(item.getYValue());
if (Double.isNaN(x) || Double.isNaN(y)) {
continue;
}
Node block = item.getNode();
Rectangle ellipse;
if (block != null) {
if (block instanceof StackPane) {
StackPane region = (StackPane)item.getNode();
if (region.getShape() == null) {
ellipse = new Rectangle( getLength( item.getExtraValue()), getBlockHeight());
} else if (region.getShape() instanceof Rectangle) {
ellipse = (Rectangle)region.getShape();
} else {
return;
}
ellipse.setWidth( getLength( item.getExtraValue()) * ((getXAxis() instanceof NumberAxis) ? Math.abs(((NumberAxis)getXAxis()).getScale()) : 1));
ellipse.setHeight(getBlockHeight() * ((getYAxis() instanceof NumberAxis) ? Math.abs(((NumberAxis)getYAxis()).getScale()) : 1));
y -= getBlockHeight() / 2.0;
// Note: workaround for RT-7689 - saw this in ProgressControlSkin
// The region doesn't update itself when the shape is mutated in place, so we
// null out and then restore the shape in order to force invalidation.
region.setShape(null);
region.setShape(ellipse);
region.setScaleShape(false);
region.setCenterShape(false);
region.setCacheShape(false);
block.setLayoutX(x);
block.setLayoutY(y);
}
}
}
}
}
public double getBlockHeight() {
return blockHeight;
}
public void setBlockHeight( double blockHeight) {
this.blockHeight = blockHeight;
}
#Override protected void dataItemAdded(Series<X,Y> series, int itemIndex, Data<X,Y> item) {
Node block = createContainer(series, getData().indexOf(series), item, itemIndex);
getPlotChildren().add(block);
}
#Override protected void dataItemRemoved(final Data<X,Y> item, final Series<X,Y> series) {
final Node block = item.getNode();
getPlotChildren().remove(block);
removeDataItemFromDisplay(series, item);
}
#Override protected void dataItemChanged(Data<X, Y> item) {
}
#Override protected void seriesAdded(Series<X,Y> series, int seriesIndex) {
for (int j=0; j<series.getData().size(); j++) {
Data<X,Y> item = series.getData().get(j);
Node container = createContainer(series, seriesIndex, item, j);
getPlotChildren().add(container);
}
}
#Override protected void seriesRemoved(final Series<X,Y> series) {
for (XYChart.Data<X,Y> d : series.getData()) {
final Node container = d.getNode();
getPlotChildren().remove(container);
}
removeSeriesFromDisplay(series);
}
private Node createContainer(Series<X, Y> series, int seriesIndex, final Data<X,Y> item, int itemIndex) {
Node container = item.getNode();
if (container == null) {
container = new StackPane();
item.setNode(container);
}
container.getStyleClass().add( getStyleClass( item.getExtraValue()));
return container;
}
#Override protected void updateAxisRange() {
final Axis<X> xa = getXAxis();
final Axis<Y> ya = getYAxis();
List<X> xData = null;
List<Y> yData = null;
if(xa.isAutoRanging()) xData = new ArrayList<X>();
if(ya.isAutoRanging()) yData = new ArrayList<Y>();
if(xData != null || yData != null) {
for(Series<X,Y> series : getData()) {
for(Data<X,Y> data: series.getData()) {
if(xData != null) {
xData.add(data.getXValue());
xData.add(xa.toRealValue(xa.toNumericValue(data.getXValue()) + getLength(data.getExtraValue())));
}
if(yData != null){
yData.add(data.getYValue());
}
}
}
if(xData != null) xa.invalidateRange(xData);
if(yData != null) ya.invalidateRange(yData);
}
}
}
GanttChartSample.java:
import java.util.Arrays;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.scene.Scene;
import javafx.scene.chart.CategoryAxis;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import chart.gantt_04.GanttChart.ExtraData;
// TODO: use date for x-axis
public class GanttChartSample extends Application {
#Override public void start(Stage stage) {
stage.setTitle("Gantt Chart Sample");
String[] machines = new String[] { "Machine 1", "Machine 2", "Machine 3" };
final NumberAxis xAxis = new NumberAxis();
final CategoryAxis yAxis = new CategoryAxis();
final GanttChart<Number,String> chart = new GanttChart<Number,String>(xAxis,yAxis);
xAxis.setLabel("");
xAxis.setTickLabelFill(Color.CHOCOLATE);
xAxis.setMinorTickCount(4);
yAxis.setLabel("");
yAxis.setTickLabelFill(Color.CHOCOLATE);
yAxis.setTickLabelGap(10);
yAxis.setCategories(FXCollections.<String>observableArrayList(Arrays.asList(machines)));
chart.setTitle("Machine Monitoring");
chart.setLegendVisible(false);
chart.setBlockHeight( 50);
String machine;
machine = machines[0];
XYChart.Series series1 = new XYChart.Series();
series1.getData().add(new XYChart.Data(0, machine, new ExtraData( 1, "status-red")));
series1.getData().add(new XYChart.Data(1, machine, new ExtraData( 1, "status-green")));
series1.getData().add(new XYChart.Data(2, machine, new ExtraData( 1, "status-red")));
series1.getData().add(new XYChart.Data(3, machine, new ExtraData( 1, "status-green")));
machine = machines[1];
XYChart.Series series2 = new XYChart.Series();
series2.getData().add(new XYChart.Data(0, machine, new ExtraData( 1, "status-green")));
series2.getData().add(new XYChart.Data(1, machine, new ExtraData( 1, "status-green")));
series2.getData().add(new XYChart.Data(2, machine, new ExtraData( 2, "status-red")));
machine = machines[2];
XYChart.Series series3 = new XYChart.Series();
series3.getData().add(new XYChart.Data(0, machine, new ExtraData( 1, "status-blue")));
series3.getData().add(new XYChart.Data(1, machine, new ExtraData( 2, "status-red")));
series3.getData().add(new XYChart.Data(3, machine, new ExtraData( 1, "status-green")));
chart.getData().addAll(series1, series2, series3);
chart.getStylesheets().add(getClass().getResource("ganttchart.css").toExternalForm());
Scene scene = new Scene(chart,620,350);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
ganttchart.css
.status-red {
-fx-background-color:rgba(128,0,0,0.7);
}
.status-green {
-fx-background-color:rgba(0,128,0,0.7);
}
.status-blue {
-fx-background-color:rgba(0,0,128,0.7);
}
Related
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);
}
});
});
}
}
}
ISymbol interface
package main;
import javafx.scene.image.Image;
public interface ISymbol {
void setImage(String location,String name);
Image getImage();
void setValue(int value);
int getValue();
}
Symbol class
package main;
import javafx.scene.image.Image;
import java.io.File;
public class Symbol implements ISymbol {
Image image;
int value;
#Override
public void setImage(String location,String name) {
File file = new File(location);
image = new Image(file.toURI().toString(),100,100,true,true);
}
#Override
public Image getImage() {
return image;
}
#Override
public void setValue(int value) {
this.value = value;
}
#Override
public int getValue() {
return value;
}
}
In here i'm trying to add images randomly to a array and i'm using that array in my main class to add those images to my reels
Reel class
package main;
import java.util.Random;
public class Reel {
public Symbol[] spin(){
Symbol cherry = new Symbol();
Symbol redSeven = new Symbol();
Symbol watermelon = new Symbol();
Symbol bell = new Symbol();
Symbol lemon = new Symbol();
Symbol plum = new Symbol();
Random random = new Random();
Symbol[] symbolArray = new Symbol[6];
for (int i = 0; i < symbolArray.length; i++) {
int randomNumber = random.nextInt(6);
System.out.println(randomNumber);
switch (randomNumber) {
case 0:
cherry.setValue(2);
cherry.setImage("/images/cherry.png","cherry");
symbolArray[i] = cherry;
break;
case 1:
lemon.setValue(3);
lemon.setImage("/images/lemon.png","lemon");
symbolArray[i] = lemon;
break;
case 2:
plum.setValue(4);
plum.setImage("/images/plum.png","plum");
symbolArray[i] = plum;
break;
case 3:
watermelon.setValue(5);
watermelon.setImage("/images/watermelon.png", "watermelon");
symbolArray[i] = watermelon;
break;
case 4:
bell.setValue(6);
bell.setImage("/images/bell.png", "bell");
symbolArray[i] = bell;
break;
case 5:
redSeven.setValue(7);
redSeven.setImage("images/redseven.png","seven");
symbolArray[i] = redSeven;
break;
default:
break;
}
}
return symbolArray;
}
}
This is my main class that include all methods. In the btnSpin method i'm calling my thread and for setting images for the reels i have used a reel method
I have debug my program and checked whether the image is coming the image was on there but when i set my image to the image view it wont work while i'm running my thread those imageviews are disappeared can someone give me a solution waiting for a reply thank you :)
SlotMachine class
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.effect.Reflection;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import main.Reel;
import main.Symbol;
public class SlotMachine extends Application implements Runnable {
//creating a thread
Thread thread1 = new Thread(){
#Override public void run(){
reel1();
}
};
//default image for reel
private Image image = new Image("/images/icon.png");
//UI variables
private Text title;
private Label lblStatus,lblInformationArea, lblBetAmount, lblCreditArea;
private ImageView image1, image2, image3;
private Button btnSpin, btnAddCoin, btnBetOne, btnBetMax, btnReset, btnStatistics;
//calculation variables
private int remainingCoins = 10;
private int betAmount, wins, lost, reel1value, reel2value, reel3value;
#Override
public void start(Stage primaryStage) {
GridPane grid = new GridPane();
grid.setPadding(new Insets(10, 0, 10, 0));
grid.setHgap(20);
grid.setVgap(20);
grid.setGridLinesVisible(true);
// Title in row 0 column 3 with styling
title = new Text();
title.setCache(true);
title.setText("REEL RUSH");
title.setFill(Color.YELLOW);
title.setFont(Font.font("Arial", FontWeight.BOLD, 60));
Reflection r = new Reflection();
title.setEffect(r);
GridPane.setConstraints(title, 3, 1);
GridPane.setHalignment(title, HPos.CENTER);
// Reel1 in row 4 column 2
image1 = new ImageView(image);
GridPane.setConstraints(image1, 2, 4);
GridPane.setHalignment(image1, HPos.CENTER);
// Reel2 in row 4 column 3
image2 = new ImageView(image);
GridPane.setConstraints(image2, 3, 4);
GridPane.setHalignment(image2, HPos.CENTER);
// Reel3 in row 4 column 4
image3 = new ImageView(image);
GridPane.setConstraints(image3, 4, 4);
GridPane.setHalignment(image3, HPos.CENTER);
// adding mouse click event for image views
image1.setOnMouseClicked(new EventHandler<javafx.scene.input.MouseEvent>() {
#Override
public void handle(javafx.scene.input.MouseEvent event) {
symbolClicked(event);
System.out.println("REEL 1 IS CLICKED");
}
});
image2.setOnMouseClicked(new EventHandler<javafx.scene.input.MouseEvent>() {
#Override
public void handle(javafx.scene.input.MouseEvent event) {
symbolClicked(event);
System.out.println("REEL 2 IS CLICKED");
}
});
image3.setOnMouseClicked(new EventHandler<javafx.scene.input.MouseEvent>() {
#Override
public void handle(javafx.scene.input.MouseEvent event) {
symbolClicked(event);
System.out.println("REEL 3 IS CLICKED");
}
});
// Status label row 8 column 4
lblStatus = new Label("YOU LOOSE");
lblStatus.setId("label-lblStatus");
GridPane.setConstraints(lblStatus, 3, 8);
GridPane.setHalignment(lblStatus, HPos.CENTER);
//information area label row 9 column 3
lblInformationArea = new Label("INFORMATION AREA ");
lblInformationArea.setId("label-lbl");
GridPane.setConstraints(lblInformationArea, 3, 9);
GridPane.setHalignment(lblInformationArea, HPos.CENTER);
// Credit area label row 5 column 2
lblCreditArea = new Label("CREDIT AREA: " + remainingCoins);
lblCreditArea.setId("label-lbl");
GridPane.setConstraints(lblCreditArea, 2, 9);
GridPane.setHalignment(lblCreditArea, HPos.CENTER);
// Bet amount label row 5 column 4
lblBetAmount = new Label("BET AMOUNT: " +betAmount);
lblBetAmount.setId("label-lbl");
GridPane.setConstraints(lblBetAmount, 4, 9);
GridPane.setHalignment(lblBetAmount, HPos.CENTER);
// Add coin button row 6 column 3
btnSpin = new Button("SPIN");
btnSpin.setId("button-btnSpin");
GridPane.setConstraints(btnSpin, 3, 10);
GridPane.setHalignment(btnSpin, HPos.CENTER);
// Add coin button row 8 column 1
btnAddCoin = new Button("ADD COIN");
GridPane.setConstraints(btnAddCoin, 2, 12);
GridPane.setHalignment(btnAddCoin, HPos.CENTER);
// Add coin button row 8 column 2
btnBetOne = new Button("BET ONE");
btnBetOne.setFont(Font.font("Arial", 20));
GridPane.setConstraints(btnBetOne, 1, 12);
GridPane.setHalignment(btnBetOne, HPos.CENTER);
// Add coin button row 8 column 3
btnBetMax = new Button("BET MAX");
GridPane.setConstraints(btnBetMax, 4, 12);
GridPane.setHalignment(btnBetMax, HPos.CENTER);
// Add coin button row 8 column 4
btnReset = new Button("RESET");
GridPane.setConstraints(btnReset, 6, 12);
GridPane.setHalignment(btnReset, HPos.CENTER);
// Add coin button row 8 column 5
btnStatistics = new Button("STATISTICS");
GridPane.setConstraints(btnStatistics, 3, 12);
GridPane.setHalignment(btnStatistics, HPos.CENTER);
// ------------------- Adding mouse events for each button ---------------------------
btnAddCoin.setOnAction(new EventHandler<javafx.event.ActionEvent>() {
#Override
public void handle(javafx.event.ActionEvent event) {
remainingCoins++;
lblCreditArea.setText("CREDIT AREA: "+remainingCoins);
}
});
btnBetOne.setOnAction(new EventHandler<javafx.event.ActionEvent>() {
#Override
public void handle(javafx.event.ActionEvent event) {
if (remainingCoins > 0) {
remainingCoins--;
betAmount++;
lblBetAmount.setText("BET AMOUNT: " + betAmount);
lblCreditArea.setText("CREDIT AREA: " + remainingCoins);
} else {
lblInformationArea.setText("No Credits Left!!!! Please Insert A Coin");
}
}
});
btnSpin.setOnAction(new EventHandler<javafx.event.ActionEvent>() {
#Override
public void handle(javafx.event.ActionEvent event) {
if (betAmount > 0) {
System.out.println("SPIN BUTTON CLICKED");
thread1.start();
} else {
lblInformationArea.setText("You did not bet!!!! Please Bet");
}
}
});
btnReset.setOnAction(new EventHandler<javafx.event.ActionEvent>() {
#Override
public void handle(javafx.event.ActionEvent event) {
remainingCoins = 10;
betAmount = 0;
lblBetAmount.setText("BET AMOUNT: " + betAmount);
lblCreditArea.setText("CREDIT AREA: " + remainingCoins);
lblInformationArea.setText("Status");
image1.setImage(image);
image2.setImage(image);
image3.setImage(image);
}
});
btnBetMax.setOnAction(new EventHandler<javafx.event.ActionEvent>() {
#Override
public void handle(javafx.event.ActionEvent event) {
if (remainingCoins >= 3) {
remainingCoins = remainingCoins - 3;
betAmount = betAmount + 3;
lblBetAmount.setText("BET AMOUNT: " + betAmount);
lblCreditArea.setText("CREDIT AREA: " + remainingCoins);
} else {
lblInformationArea.setText("No Credits Left!!!! Please Insert A Coin");
}
}
});
btnStatistics.setOnAction(new EventHandler<javafx.event.ActionEvent>() {
#Override
public void handle(javafx.event.ActionEvent event) {
//statistic();
lblInformationArea.setText("Spin the Reel First");
}
});
// adding all to the scene
grid.getChildren().addAll(title, lblStatus, lblInformationArea, lblCreditArea, lblBetAmount, btnAddCoin, btnBetMax, btnBetOne, btnReset, btnSpin, btnStatistics, image1, image3 , image2);
grid.setAlignment(Pos.TOP_CENTER);
Scene scene = new Scene(grid, 1450, 920);
scene.getStylesheets().add("/css/main.css");
primaryStage.setTitle("REEL RUSH");
primaryStage.setScene(scene);
primaryStage.show();
}
public void reel1() {
while (true) {
//creating reel objects for each reel
Reel firstReel = new Reel();
Reel secondReel = new Reel();
Reel thirdReel = new Reel();
Symbol[] firstReelSymbols = firstReel.spin();
Symbol[] secondReelSymbols = secondReel.spin();
Symbol[] thirdReelSymbols = thirdReel.spin();
for (Symbol item : firstReelSymbols) {
Image img1 = item.getImage();
image1.setImage(img1);
reel1value = item.getValue();
}
for (Symbol item : secondReelSymbols) {
Image img1 = item.getImage();
image2.setImage(img1);
reel1value = item.getValue();
}
for (Symbol item : thirdReelSymbols) {
Image img1 = item.getImage();
image3.setImage(img1);
reel1value = item.getValue();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void symbolClicked(javafx.scene.input.MouseEvent event) {
//TODO stop thread when image clicked
if((reel1value==reel3value)&&(reel2value==reel3value)){
//check if all 3 numbers are same
lblInformationArea.setText("You Win");
remainingCoins+=(betAmount*reel1value);
lblCreditArea.setText("Credits Area: "+remainingCoins);
wins++;
}else{
lblInformationArea.setText("You Loose");
lost++;
}
betAmount=0;
lblBetAmount.setText("Bet Amount: "+betAmount);
}
#Override
public void run() {
}
public static void main(String[] args){
launch(args);
}
}
Technically, while you can "run" an Application, we would never implement Runnable in an Application. JavaFX will do its own application management, and there is no way you can "run" an application in another thread.
Now, back to your question. Your reel1() is badly written. If you know a particular method is going to run in non-UI thread (i.e. JavaFX Application thread), you must take note not to directly set any kind of value that changes the UI within it.
So, this:
for (Symbol item : firstReelSymbols) {
Image img1 = item.getImage();
image1.setImage(img1);
reel1value = item.getValue();
}
should becomes something like:
for (Symbol item : firstReelSymbols) {
Image img1 = item.getImage();
Platform.runLater(() -> image1.setImage(img1)); // Run this run on UI thread
reel1value = item.getValue(); // Not sure what this value is for, may need to be wrapped inside Platform.runLater() if it affects UI
}
Other than this, it is weird that you are looping through a list of Symbol objects, and inside the loop you are setting the same image1 field.
Is there an easy way to put a scroll bar in an Alert box and have it actually scroll? I'm adding a grid pane to the scroll pane. While the Alert box is active, I'm adding and removing content as the user requests. Unfortunately, there is a limit to ho much I can add because the Alert box grows beyond the screen height. Hence the reason for a scroll pane. The problem is, it doesn't seem to think it needs to scroll even though the content is below the screen
I've tried a bunch of things, but each time the scroll bars grow with the scroll pane. I even tried a custom ScrollPane as suggested by James_D. Still not luck.
Any help would be awesome!
Here is the code for the custom Alert box
import java.util.ArrayList;
import java.util.List;
import com.sun.xml.internal.bind.v2.runtime.unmarshaller.XmlVisitor.TextPredictor;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.DialogPane;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.ScrollPane.ScrollBarPolicy;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
public class SettingsChangeWindow extends Alert {
public enum SETTING_TYPE {SINGLE, MULTIPLE};
private SETTING_TYPE type = null;
private IndexedGridPane parentGrid;
private SettingBean bean;
private DialogPane parentPane;
public SettingsChangeWindow(SettingBean bean) {
super(AlertType.CONFIRMATION);
this.bean = bean;
this.type = bean.getType();
SizeableScrollPane scroll = new SizeableScrollPane();
scroll.setHbarPolicy(ScrollBarPolicy.ALWAYS);
scroll.setVbarPolicy(ScrollBarPolicy.ALWAYS);
scroll.setFitToHeight(true);
scroll.setFitToWidth(true);
parentGrid = new IndexedGridPane();
scroll.setContent(parentGrid);
this.parentPane = getDialogPane();
setResizable(true);
if(type == SETTING_TYPE.SINGLE){
FriendlyVBox vbox = new FriendlyVBox();
setTitle("Change " + bean.getName());
setHeaderText("Change the " + bean.getName() + " value by changing the value in the box");
parentGrid.setPadding(new Insets(20, 150, 0, 10));
vbox.getChildren().addAll(new Label(bean.getName()), new AutoTextBox(bean.getValue()));
parentGrid.add(vbox, 0, 0);
}else{
setTitle("Change " + bean.getName());
String header = "Change the " + bean.getName() + " value by changing the value in the box\n";
header += "You may add and delete value sets (may require resizing)";
setHeaderText(header);
parentGrid.add(new AddButton(), 1, 0);
parentGrid.add(new RemoveButton(), 2, 0);
addMultipleValues();
}
this.parentPane.setContent(scroll);
// parentPane.setMinHeight(GridPane.USE_PREF_SIZE);
// parentPane.setMinWidth(GridPane.USE_PREF_SIZE);
// getDialogPane().getChildren().stream().forEach(node -> ((Label)node).setMinHeight(Region.USE_PREF_SIZE));
}
public SettingBean getValue(){
return bean;
}
private void addMultipleValues(){
List<Object> values = bean.getChildren();
if(bean.getName().equals("TSPAddressPostal") || bean.getName().equals("SchemeOperatorAddressPostal")){
for(Object addr : values){
PhysicalAddressBean address = (PhysicalAddressBean)addr;
addPhysicalAddress(address);
}
}else{
for(Object uri : values){
addURI((String)uri);
}
}
}
public void saveValue(){
if(type == SETTING_TYPE.SINGLE){
List<Node> children = parentGrid.getChildren();
for(Node child : children){
if(child instanceof FriendlyVBox){
// cast to FriendlyVBox
String value = ((FriendlyVBox)child).getTextField().getText();
this.bean.setValue(value);
}
}
}else{
saveMultipleValues();
}
}
/**
* For values in XML that can have multiple child nodes
*/
private void saveMultipleValues(){
switch(bean.getName()){
case "TSPAddressPostal" :
savePostalAddress();
break;
case "SchemeOperatorAddressPostal":
savePostalAddress();
break;
default:
saveURI();
break;
}
}
/**
* If the setting bean is encapsulating a list of physical address
* (when the name is: PostalAddress) populate via predefined structure
*/
private void savePostalAddress(){
List<Object> addresses = new ArrayList<>();
List<Node> children = parentGrid.getChildren();
for(Node child : children){
if(child instanceof IndexedGridPane){
IndexedGridPane pane = (IndexedGridPane) child;
PhysicalAddressBean add = new PhysicalAddressBean();
// each address attribute in the order listed in Trust List XML
add.setStreetAddress(((FriendlyVBox)pane.get(0, 1)).getTextField().getText());
add.setLocality(((FriendlyVBox)pane.get(0, 2)).getTextField().getText());
add.setPostalCode(((FriendlyVBox)pane.get(0, 3)).getTextField().getText());
add.setCountryName(((FriendlyVBox)pane.get(0, 4)).getTextField().getText());
// add address bean to list
addresses.add(add);
}
}
bean.setChildren(addresses);
}
/**
* used to store any values in the XML that can have multiple child URI values
*/
private void saveURI(){
List<Object> uris = new ArrayList<>();
List<Node> children = parentGrid.getChildren();
for(Node child : children){
if(child instanceof FriendlyVBox){
FriendlyVBox vBox = (FriendlyVBox) child;
uris.add(vBox.getTextField().getText());
}
}
bean.setChildren(uris);
}
private void addURI(String uri){
int newSlot = parentGrid.getRowCount();
FriendlyVBox vBox = new FriendlyVBox();
vBox.getChildren().addAll(new Label("\n" + bean.getName()), new AutoTextBox(""));
parentGrid.add(vBox, 0, newSlot);
}
private void addPhysicalAddress(){
int newSlot = parentGrid.getRowCount();
IndexedGridPane pane = new IndexedGridPane();
Label label = new Label("\nPostal Address");
pane.add(label, 0, 0);
label.setFont(Font.font("system", FontWeight.BOLD, 12));
FriendlyVBox postal = new FriendlyVBox();
postal.getChildren().addAll(new Label("Street Address"), new AutoTextBox(""));
FriendlyVBox local = new FriendlyVBox();
local.getChildren().addAll(new Label("Locale"), new AutoTextBox(""));
FriendlyVBox postalCode = new FriendlyVBox();
postalCode.getChildren().addAll(new Label("Postal Code"), new AutoTextBox(""));
FriendlyVBox country = new FriendlyVBox();
country.getChildren().addAll(new Label("Country Name"), new AutoTextBox(""));
pane.add(postal, 0, 1);
pane.add(local, 0, 2);
pane.add(postalCode, 0, 3);
pane.add(country, 0, 4);
parentGrid.add(pane, 0, newSlot);
}
private void addPhysicalAddress(PhysicalAddressBean address){
int newSlot = parentGrid.getRowCount();
IndexedGridPane pane = new IndexedGridPane();
Label label = new Label("\nPostal Address");
pane.add(label, 0, 0);
label.setFont(Font.font("system", FontWeight.BOLD, 12));
FriendlyVBox street = new FriendlyVBox();
street.getChildren().addAll(new Label("Street Address"), new AutoTextBox(address.getStreetAddress()));
FriendlyVBox local = new FriendlyVBox();
local.getChildren().addAll(new Label("Locale"), new AutoTextBox(address.getLocality()));
FriendlyVBox postalCode = new FriendlyVBox();
postalCode.getChildren().addAll(new Label("Postal Code"), new AutoTextBox(address.getPostalCode()));
FriendlyVBox country = new FriendlyVBox();
country.getChildren().addAll(new Label("Country Name"), new AutoTextBox(address.getCountryName()));
pane.add(street, 0, 1);
pane.add(local, 0, 2);
pane.add(postalCode, 0, 3);
pane.add(country, 0, 4);
parentGrid.add(pane, 0, newSlot);
}
private class IndexedGridPane extends GridPane{
public Node get(final int row, final int column) {
Node result = null;
ObservableList<Node> childrens = super.getChildren();
for (Node node : childrens) {
if(super.getRowIndex(node) == row && super.getColumnIndex(node) == column) {
result = node;
break;
}
}
return result;
}
public int getRowCount() {
int numRows = getRowConstraints().size();
for (int i = 0; i < getChildren().size(); i++) {
Node child = getChildren().get(i);
if (child.isManaged()) {
Integer rowIndex = GridPane.getRowIndex(child);
if(rowIndex != null){
numRows = Math.max(numRows,rowIndex+1);
}
}
}
return numRows;
}
}
private class AutoTextBox extends TextField{
public AutoTextBox(String contents){
setMinWidth(Region.USE_PREF_SIZE);
setMaxWidth(Region.USE_PREF_SIZE);
textProperty().addListener(new AutoAdjustText());
setText(contents);
}
private class AutoAdjustText implements ChangeListener<String>{
#Override
public void changed(ObservableValue<? extends String> ov,
String prevText, String currText) {
Platform.runLater(() -> {
Text text = new Text(currText);
text.setFont(getFont()); // Set the same font, so the size is the same
double width = text.getLayoutBounds().getWidth() // This big is the Text in the TextField
+ getPadding().getLeft() + getPadding().getRight() // Add the padding of the TextField
+ 2d; // Add some spacing
setPrefWidth(width); // Set the width
positionCaret(getCaretPosition()); // If you remove this line, it flashes a little bit
});
}
}
}
private class FriendlyVBox extends VBox{
public TextField getTextField(){
List<Node> children = getChildren();
for(Node child : children){
if(child instanceof TextField){
return (TextField)child;
}
}
return null;
}
}
private class AddButton extends Button{
public AddButton(){
setText("Add+");
onActionProperty().set(new AddValue());
}
private class AddValue implements EventHandler<ActionEvent>{
#Override
public void handle(ActionEvent event) {
if(bean.getName().equals("TSPAddressPostal") || bean.getName().equals("SchemeOperatorAddressPostal")){
addPhysicalAddress();
}else{
addURI("Add URI here");
// parentPane.setContent(parentGrid);
}
parentPane.getScene().getWindow().sizeToScene();
}
}
}
private class RemoveButton extends Button{
public RemoveButton(){
setText("Remove");
onActionProperty().set(new RemoveValue());
}
private class RemoveValue implements EventHandler<ActionEvent>{
#Override
public void handle(ActionEvent event) {
int rowCount = parentGrid.getRowCount();
parentGrid.getChildren().remove(rowCount);
parentPane.getScene().getWindow().sizeToScene();
}
}
}
private class SizeableScrollPane extends ScrollPane{
public SizeableScrollPane() {
viewportBoundsProperty().addListener(new Resizer());
hvalueProperty().addListener(new Resizer());
vvalueProperty().addListener(new Resizer());
}
private class Resizer implements ChangeListener<Object> {
#Override
public void changed(ObservableValue<? extends Object> observable, Object oldValue, Object newValue) {
double hmin = getHmin();
double hmax = getHmax();
double hvalue = getHvalue();
double contentWidth = getContent().getLayoutBounds().getWidth();
double viewportWidth = getViewportBounds().getWidth();
double hoffset =
Math.max(0, contentWidth - viewportWidth) * (hvalue - hmin) / (hmax - hmin);
double vmin = getVmin();
double vmax = getVmax();
double vvalue = getVvalue();
double contentHeight = getContent().getLayoutBounds().getHeight();
double viewportHeight = getViewportBounds().getHeight();
double voffset =
Math.max(0, contentHeight - viewportHeight) * (vvalue - vmin) / (vmax - vmin);
System.out.printf("Offset: [%.1f, %.1f] width: %.1f height: %.1f %n",
hoffset, voffset, viewportWidth, viewportHeight);
}
}
}
}
I feel stupid.
if you comment out the lines in the constructor:
scroll.setHbarPolicy(ScrollBarPolicy.ALWAYS);
scroll.setVbarPolicy(ScrollBarPolicy.ALWAYS);
scroll.setFitToHeight(true);
scroll.setFitToWidth(true);
And you comment out the lines in the button listeners:
parentPane.getScene().getWindow().sizeToScene();
All is good in the world. You don't even need a custom ScrollPane, it works as expected. I hope this helps someone else
My desktop application has a timer for starting and stopping a test. On the graph, I want to create two vertical lines to indicate the start and stop time. "Adding vertical lines to StackPane with JavaFX" won't work for my case because I don't want the lines to stay at the same position and those lines should be drawn within the plot not the layout. When the user zooms on the chart, those vertical lines should move corresponding to where the user zooms. Thanks for any tip.
Here are my codes for creating the chart:
LineChart<Number, Number> chart = new LineChart<Number, Number>(xAxis, yAxis, dataset);
xAxis.setLabel("time(s)");
yAxis.setLabel("deg/s");
You need to extend the LineChart class and override the layoutPlotChildren method in order to show your markers.
Kleopatra did a very good example for a Scatter chart. The code below is a modified version for a line chart and has both vertical and horizontal markers:
public class LineChartSample extends Application {
#Override public void start(Stage stage) {
final NumberAxis xAxis = new NumberAxis();
final NumberAxis yAxis = new NumberAxis();
xAxis.setLabel("Number of Month");
final LineChartWithMarkers<Number,Number> lineChart = new LineChartWithMarkers<Number,Number>(xAxis,yAxis);
XYChart.Series series = new XYChart.Series();
series.setName("My portfolio");
series.getData().add(new XYChart.Data(1, 23));
series.getData().add(new XYChart.Data(2, 14));
series.getData().add(new XYChart.Data(3, 15));
series.getData().add(new XYChart.Data(4, 24));
series.getData().add(new XYChart.Data(5, 34));
series.getData().add(new XYChart.Data(6, 36));
series.getData().add(new XYChart.Data(7, 22));
series.getData().add(new XYChart.Data(8, 45));
series.getData().add(new XYChart.Data(9, 43));
series.getData().add(new XYChart.Data(10, 17));
series.getData().add(new XYChart.Data(11, 29));
series.getData().add(new XYChart.Data(12, 25));
lineChart.getData().add(series);
Data<Number, Number> horizontalMarker = new Data<>(0, 25);
lineChart.addHorizontalValueMarker(horizontalMarker);
Data<Number, Number> verticalMarker = new Data<>(10, 0);
lineChart.addVerticalValueMarker(verticalMarker);
Slider horizontalMarkerSlider = new Slider(yAxis.getLowerBound(), yAxis.getUpperBound(), 0);
horizontalMarkerSlider.setOrientation(Orientation.VERTICAL);
horizontalMarkerSlider.setShowTickLabels(true);
horizontalMarkerSlider.valueProperty().bindBidirectional(horizontalMarker.YValueProperty());
horizontalMarkerSlider.minProperty().bind(yAxis.lowerBoundProperty());
horizontalMarkerSlider.maxProperty().bind(yAxis.upperBoundProperty());
Slider verticalMarkerSlider = new Slider(xAxis.getLowerBound(), xAxis.getUpperBound(), 0);
verticalMarkerSlider.setOrientation(Orientation.HORIZONTAL);
verticalMarkerSlider.setShowTickLabels(true);
verticalMarkerSlider.valueProperty().bindBidirectional(verticalMarker.XValueProperty());
verticalMarkerSlider.minProperty().bind(xAxis.lowerBoundProperty());
verticalMarkerSlider.maxProperty().bind(xAxis.upperBoundProperty());
BorderPane borderPane = new BorderPane();
borderPane.setCenter( lineChart);
borderPane.setTop(verticalMarkerSlider);
borderPane.setRight(horizontalMarkerSlider);
Scene scene = new Scene(borderPane,800,600);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
private class LineChartWithMarkers<X,Y> extends LineChart {
private ObservableList<Data<X, Y>> horizontalMarkers;
private ObservableList<Data<X, Y>> verticalMarkers;
public LineChartWithMarkers(Axis<X> xAxis, Axis<Y> yAxis) {
super(xAxis, yAxis);
horizontalMarkers = FXCollections.observableArrayList(data -> new Observable[] {data.YValueProperty()});
horizontalMarkers.addListener((InvalidationListener)observable -> layoutPlotChildren());
verticalMarkers = FXCollections.observableArrayList(data -> new Observable[] {data.XValueProperty()});
verticalMarkers.addListener((InvalidationListener)observable -> layoutPlotChildren());
}
public void addHorizontalValueMarker(Data<X, Y> marker) {
Objects.requireNonNull(marker, "the marker must not be null");
if (horizontalMarkers.contains(marker)) return;
Line line = new Line();
marker.setNode(line );
getPlotChildren().add(line);
horizontalMarkers.add(marker);
}
public void removeHorizontalValueMarker(Data<X, Y> marker) {
Objects.requireNonNull(marker, "the marker must not be null");
if (marker.getNode() != null) {
getPlotChildren().remove(marker.getNode());
marker.setNode(null);
}
horizontalMarkers.remove(marker);
}
public void addVerticalValueMarker(Data<X, Y> marker) {
Objects.requireNonNull(marker, "the marker must not be null");
if (verticalMarkers.contains(marker)) return;
Line line = new Line();
marker.setNode(line );
getPlotChildren().add(line);
verticalMarkers.add(marker);
}
public void removeVerticalValueMarker(Data<X, Y> marker) {
Objects.requireNonNull(marker, "the marker must not be null");
if (marker.getNode() != null) {
getPlotChildren().remove(marker.getNode());
marker.setNode(null);
}
verticalMarkers.remove(marker);
}
#Override
protected void layoutPlotChildren() {
super.layoutPlotChildren();
for (Data<X, Y> horizontalMarker : horizontalMarkers) {
Line line = (Line) horizontalMarker.getNode();
line.setStartX(0);
line.setEndX(getBoundsInLocal().getWidth());
line.setStartY(getYAxis().getDisplayPosition(horizontalMarker.getYValue()) + 0.5); // 0.5 for crispness
line.setEndY(line.getStartY());
line.toFront();
}
for (Data<X, Y> verticalMarker : verticalMarkers) {
Line line = (Line) verticalMarker.getNode();
line.setStartX(getXAxis().getDisplayPosition(verticalMarker.getXValue()) + 0.5); // 0.5 for crispness
line.setEndX(line.getStartX());
line.setStartY(0d);
line.setEndY(getBoundsInLocal().getHeight());
line.toFront();
}
}
}
}
In order to add more marker lines, just use this:
Data<Number, Number> verticalMarker = new Data<>(10, 0);
lineChart.addVerticalValueMarker(verticalMarker);
Of course you could as well use a rectangle instead of a line like this:
private ObservableList<Data<X, X>> verticalRangeMarkers;
public LineChartWithMarkers(Axis<X> xAxis, Axis<Y> yAxis) {
...
verticalRangeMarkers = FXCollections.observableArrayList(data -> new Observable[] {data.XValueProperty()});
verticalRangeMarkers = FXCollections.observableArrayList(data -> new Observable[] {data.YValueProperty()}); // 2nd type of the range is X type as well
verticalRangeMarkers.addListener((InvalidationListener)observable -> layoutPlotChildren());
}
public void addVerticalRangeMarker(Data<X, X> marker) {
Objects.requireNonNull(marker, "the marker must not be null");
if (verticalRangeMarkers.contains(marker)) return;
Rectangle rectangle = new Rectangle(0,0,0,0);
rectangle.setStroke(Color.TRANSPARENT);
rectangle.setFill(Color.BLUE.deriveColor(1, 1, 1, 0.2));
marker.setNode( rectangle);
getPlotChildren().add(rectangle);
verticalRangeMarkers.add(marker);
}
public void removeVerticalRangeMarker(Data<X, X> marker) {
Objects.requireNonNull(marker, "the marker must not be null");
if (marker.getNode() != null) {
getPlotChildren().remove(marker.getNode());
marker.setNode(null);
}
verticalRangeMarkers.remove(marker);
}
protected void layoutPlotChildren() {
...
for (Data<X, X> verticalRangeMarker : verticalRangeMarkers) {
Rectangle rectangle = (Rectangle) verticalRangeMarker.getNode();
rectangle.setX( getXAxis().getDisplayPosition(verticalRangeMarker.getXValue()) + 0.5); // 0.5 for crispness
rectangle.setWidth( getXAxis().getDisplayPosition(verticalRangeMarker.getYValue()) - getXAxis().getDisplayPosition(verticalRangeMarker.getXValue()));
rectangle.setY(0d);
rectangle.setHeight(getBoundsInLocal().getHeight());
rectangle.toBack();
}
}
used like this:
Data<Number, Number> verticalRangeMarker = new Data<>(4, 10);
lineChart.addVerticalRangeMarker(verticalRangeMarker);
To make it look like a range:
I'm not sure which question you are referring to. You can basically do all this with some binding magic: the trick is to map the x value of the line to coordinates relative to the xAxis using xAxis.getDisplayPosition(...). Then you need to transform that coordinate to the coordinate relative to the container holding the chart and the line: the easiest way to do this is to first transform to Scene coordinates using xAxis.localToScene(...) and then to the coordinates of the container, using container.sceneToLocal(...).
Then you just need to let the binding observe everything that it needs to watch for changes: these will be the (numerical) bounds of the axes, the (graphical) bounds of the chart, and, if the line is going to move, a property representing its x-value.
Here is an SSCCE. In this example, I use a Slider to move the line around. I also make the line visible only if it's in range, and bind the y-coordinates so it spans the yAxis.
import java.util.Random;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.value.ObservableDoubleValue;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.control.Slider;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.shape.Line;
import javafx.stage.Stage;
public class LineChartWithVerticalLine extends Application {
#Override
public void start(Stage primaryStage) {
NumberAxis xAxis = new NumberAxis();
NumberAxis yAxis = new NumberAxis();
LineChart<Number, Number> chart = new LineChart<>(xAxis, yAxis);
chart.getData().add(createSeries());
Pane chartHolder = new Pane();
chartHolder.getChildren().add(chart);
DoubleProperty lineX = new SimpleDoubleProperty();
Slider slider = new Slider();
slider.minProperty().bind(xAxis.lowerBoundProperty());
slider.maxProperty().bind(xAxis.upperBoundProperty());
slider.setPadding(new Insets(20));
lineX.bind(slider.valueProperty());
chartHolder.getChildren().add(createVerticalLine(chart, xAxis, yAxis, chartHolder, lineX));
BorderPane root = new BorderPane(chartHolder, null, null, slider, null);
Scene scene = new Scene(root, 800, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
private Line createVerticalLine(XYChart<Number, Number> chart, NumberAxis xAxis, NumberAxis yAxis, Pane container, ObservableDoubleValue x) {
Line line = new Line();
line.startXProperty().bind(Bindings.createDoubleBinding(() -> {
double xInAxis = xAxis.getDisplayPosition(x.get());
Point2D pointInScene = xAxis.localToScene(xInAxis, 0);
double xInContainer = container.sceneToLocal(pointInScene).getX();
return xInContainer ;
},
x,
chart.boundsInParentProperty(),
xAxis.lowerBoundProperty(),
xAxis.upperBoundProperty()));
line.endXProperty().bind(line.startXProperty());
line.startYProperty().bind(Bindings.createDoubleBinding(() -> {
double lowerY = yAxis.getDisplayPosition(yAxis.getLowerBound());
Point2D pointInScene = yAxis.localToScene(0, lowerY);
double yInContainer = container.sceneToLocal(pointInScene).getY();
return yInContainer ;
},
chart.boundsInParentProperty(),
yAxis.lowerBoundProperty()));
line.endYProperty().bind(Bindings.createDoubleBinding(() -> {
double upperY = yAxis.getDisplayPosition(yAxis.getUpperBound());
Point2D pointInScene = yAxis.localToScene(0, upperY);
double yInContainer = container.sceneToLocal(pointInScene).getY();
return yInContainer ;
},
chart.boundsInParentProperty(),
yAxis.lowerBoundProperty()));
line.visibleProperty().bind(
Bindings.lessThan(x, xAxis.lowerBoundProperty())
.and(Bindings.greaterThan(x, xAxis.upperBoundProperty())).not());
return line ;
}
private Series<Number, Number> createSeries() {
Series<Number, Number> series = new Series<>();
series.setName("Data");
Random rng = new Random();
for (int i=0; i<=20; i++) {
series.getData().add(new Data<>(i, rng.nextInt(101)));
}
return series ;
}
public static void main(String[] args) {
launch(args);
}
}
I was able to create a drag and zoom in feature using the Line Chart Example mentioned here. The code listens to the mouse events and adds to the vertical ranges, which makes it appear to be dragging. JavaFX Drag and Zoom Line Chart Example
/**
* The ChartView.
*/
public class ChartController {
private ChartViewModel chartViewModel;
private CustomLineChart<Number, Number> lineChart;
private NumberAxis xAxis;
private NumberAxis yAxis;
private XYChart.Series<Number, Number> series;
private List<Integer> data;
private boolean mouseDragged;
private double initialNumberStart;
private double initialNumberEnd;
#FXML
private VBox mainContainer;
#FXML
private HBox chartContainer;
/**
* The constructor.
*/
public ChartController() {
chartViewModel = new ChartViewModel();
mouseDragged = false;
}
/**
* The initialize method.
*/
public void initialize() {
createChart();
handleEvents();
}
/**
* Handles the events.
*/
private void handleEvents() {
lineChart.setOnMousePressed(pressed -> {
int minSize = 1;
// Get coordinate from the scene and transform to coordinates from the chart axis
Point2D firstSceneCoordinate = new Point2D(pressed.getSceneX(), pressed.getSceneY());
double firstX = xAxis.sceneToLocal(firstSceneCoordinate).getX();
lineChart.setOnMouseDragged(dragged -> {
mouseDragged = true;
Point2D draggedSceneCoordinate = new Point2D(dragged.getSceneX(), dragged.getSceneY());
double draggedX = xAxis.sceneToLocal(draggedSceneCoordinate).getX();
List<Double> numbers = filterSeries(firstX, draggedX);
int size = numbers.size();
double numberStart = size > minSize ? numbers.get(0) : initialNumberStart;
double numberEnd = numbers.size() > minSize ? numbers.get(size - 1) : initialNumberEnd;
if (size > minSize) {
lineChart.addVerticalRangeLines(new Data<>(numberStart, numberEnd));
}
lineChart.setOnMouseReleased(released -> {
if (mouseDragged) {
initialNumberStart = numberStart;
initialNumberEnd = numberEnd;
mouseDragged = false;
redrawChart();
}
});
});
});
}
/**
* Creates the charts.
*/
private void createChart() {
xAxis = new NumberAxis();
yAxis = new NumberAxis();
lineChart = new CustomLineChart<>(xAxis, yAxis);
data = chartViewModel.getData();
createSeries(data);
lineChart.getData().add(series);
initialNumberStart = 1;
initialNumberEnd = data.size() - 1;
chartContainer.getChildren().add(lineChart);
HBox.setHgrow(lineChart, Priority.ALWAYS);
}
/**
* Creates the series for the line chart.
*
* #param numbers The list of numbers for the series
*/
private void createSeries(List<Integer> numbers) {
int size = numbers.size();
series = new XYChart.Series<>();
series.setName("Example");
for (int i = 0; i < size; i++) {
series.getData().add(new XYChart.Data<Number, Number>(i, numbers.get(i)));
}
}
/**
* Filters the nodes and returns the node x positions within the firstX and lastX positions.
*
* #param firstX The first x position
* #param lastX The last x position
* #return The x positions for the nodes within the firstX and lastX
*/
private List<Double> filterSeries(double firstX, double lastX) {
List<Double> nodeXPositions = new ArrayList<>();
lineChart.getData().get(0).getData().forEach(node -> {
double nodeXPosition = lineChart.getXAxis().getDisplayPosition(node.getXValue());
if (nodeXPosition >= firstX && nodeXPosition <= lastX) {
nodeXPositions.add(Double.parseDouble(node.getXValue().toString()));
}
});
return nodeXPositions;
}
/**
* Updates the series for the chart.
*/
private void updateSeries() {
lineChart.getData().remove(0);
lineChart.getData().add(series);
}
/**
* Redraws the chart.
*/
private void redrawChart() {
List<Integer> filteredSeries = new ArrayList<>();
data.forEach(number -> {
if (number >= initialNumberStart && number <= initialNumberEnd) {
filteredSeries.add(number);
}
});
if (!filteredSeries.isEmpty()) {
createSeries(filteredSeries);
updateSeries();
lineChart.removeVerticalRangeLines();
}
}
/**
* Resets the series for the chart.
*
* #param event The event
*/
#FXML
void resetChart(ActionEvent event) {
createSeries(data);
updateSeries();
}
}
I'm using UndoFX & ReactFX for implementing the Undo/Redo function for my 2D shape application.
The problem is when i move my shape the EventStream records every X/Y pixel of movement. I just want to record the last position (when the user releases the drag).
What i have tried so far:
Instead of using changesOf(rect.xProperty()).map(c -> new xChange(c)); and
changesOf(rect.yProperty()).map(c -> new yChange(c));
I created a DoubleProperty x,y, and saved the shape x,y Property to these variables when the user mouse is released.
Lastly i change the changesOf to: changesOf(this.x).map(c -> new xChange(c)); and changesOf(this.y).map(c -> new yChange(c));
But that did not work, it behaved just like before.
....
private class xChange extends RectangleChange<Double> {
public xChange(Double oldValue, Double newValue) {
super(oldValue, newValue);
}
public xChange(Change<Number> c) {
super(c.getOldValue().doubleValue(), c.getNewValue().doubleValue());
}
#Override void redo() { rect.setX(newValue); }
#Override xChange invert() { return new xChange(newValue, oldValue); }
#Override Optional<RectangleChange<?>> mergeWith(RectangleChange<?> other) {
if(other instanceof xChange) {
return Optional.of(new xChange(oldValue, ((xChange) other).newValue));
} else {
return Optional.empty();
}
}
#Override
public boolean equals(Object other) {
if(other instanceof xChange) {
xChange that = (xChange) other;
return Objects.equals(this.oldValue, that.oldValue)
&& Objects.equals(this.newValue, that.newValue);
} else {
return false;
}
}
}
...
EventStream<xChange> xChanges = changesOf(rect.xProperty()).map(c -> new xChange(c));
EventStream<yChange> yChanges = changesOf(rect.yProperty()).map(c -> new yChange(c));
changes = merge(widthChanges, heightChanges, xChanges, yChanges);
undoManager = UndoManagerFactory.unlimitedHistoryUndoManager(
changes, // stream of changes to observe
c -> c.invert(), // function to invert a change
c -> c.redo(), // function to undo a change
(c1, c2) -> c1.mergeWith(c2)); // function to merge two changes
You need to merge the changes in x with the changes in y. At present, a change in x followed by a change in y cannot be merged, so if you move the shape so that it alternates x and y changes (e.g. moving it diagonally), then each individual change will not merge with the previous one.
One way to do this is to generate changes whose old and new values are the locations, e.g. represented by Point2D objects. Here's a quick example:
import java.util.Objects;
import java.util.Optional;
import org.fxmisc.undo.UndoManager;
import org.fxmisc.undo.UndoManagerFactory;
import org.reactfx.EventStream;
import org.reactfx.EventStreams;
import org.reactfx.SuspendableEventStream;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class UndoRectangle extends Application {
#Override
public void start(Stage primaryStage) {
Rectangle rect = new Rectangle(50, 50, 150, 100);
rect.setFill(Color.CORNFLOWERBLUE);
EventStream<PositionChange> xChanges = EventStreams.changesOf(rect.xProperty()).map(c -> {
double oldX = c.getOldValue().doubleValue();
double newX = c.getNewValue().doubleValue();
double y = rect.getY();
return new PositionChange(new Point2D(oldX, y), new Point2D(newX, y));
});
EventStream<PositionChange> yChanges = EventStreams.changesOf(rect.yProperty()).map(c -> {
double oldY = c.getOldValue().doubleValue();
double newY = c.getNewValue().doubleValue();
double x = rect.getX();
return new PositionChange(new Point2D(x, oldY), new Point2D(x, newY));
});
SuspendableEventStream<PositionChange> posChanges = EventStreams.merge(xChanges, yChanges)
.reducible(PositionChange::merge);
UndoManager undoManager = UndoManagerFactory.unlimitedHistoryUndoManager(posChanges,
PositionChange::invert,
c -> posChanges.suspendWhile(() -> {
rect.setX(c.getNewPosition().getX());
rect.setY(c.getNewPosition().getY());
}),
(c1, c2) -> Optional.of(c1.merge(c2))
);
class MouseLoc { double x, y ; }
MouseLoc mouseLoc = new MouseLoc();
rect.setOnMousePressed(e -> {
mouseLoc.x = e.getSceneX();
mouseLoc.y = e.getSceneY();
});
rect.setOnMouseDragged(e -> {
rect.setX(rect.getX() + e.getSceneX() - mouseLoc.x);
rect.setY(rect.getY() + e.getSceneY() - mouseLoc.y);
mouseLoc.x = e.getSceneX();
mouseLoc.y = e.getSceneY();
});
rect.setOnMouseReleased(e -> undoManager.preventMerge());
Pane pane = new Pane(rect);
Button undo = new Button("Undo");
undo.disableProperty().bind(Bindings.not(undoManager.undoAvailableProperty()));
undo.setOnAction(e -> undoManager.undo());
Button redo = new Button("Redo");
redo.disableProperty().bind(Bindings.not(undoManager.redoAvailableProperty()));
redo.setOnAction(e -> undoManager.redo());
HBox buttons = new HBox(5, undo, redo);
buttons.setAlignment(Pos.CENTER);
BorderPane.setMargin(buttons, new Insets(5));
BorderPane root = new BorderPane(pane, null, null, buttons, null);
Scene scene = new Scene(root, 600, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
public static class PositionChange {
private final Point2D oldPosition ;
private final Point2D newPosition ;
public PositionChange(Point2D oldPos, Point2D newPos) {
this.oldPosition = oldPos ;
this.newPosition = newPos ;
}
public Point2D getOldPosition() {
return oldPosition;
}
public Point2D getNewPosition() {
return newPosition;
}
public PositionChange merge(PositionChange other) {
return new PositionChange(oldPosition, other.newPosition);
}
public PositionChange invert() {
return new PositionChange(newPosition, oldPosition);
}
#Override
public boolean equals(Object o) {
if (o instanceof PositionChange) {
PositionChange other = (PositionChange) o ;
return Objects.equals(oldPosition, other.oldPosition)
&& Objects.equals(newPosition, other.newPosition);
} else return false ;
}
#Override
public int hashCode() {
return Objects.hash(oldPosition, newPosition);
}
}
public static void main(String[] args) {
launch(args);
}
}
Note that it's important the "undo" is implemented as an "atomic" change, so the undo manager sees (and ignores) a single change when you implement the undo. This can be achieved by suspending the event stream during the undo.