I am going to add some shapes on LineChart. I put LineChart and AnchorPane into the StackPane. I added shapes to AnchorPane by getting x and y coordinates from the chart series. Here is example.
LineChartApp.java
package shapes;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class LineChartApp extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setScene(new Scene(new ChartContent()));
primaryStage.setMaximized(true);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
ChartContent.java
package shapes;
import java.util.ArrayList;
import java.util.List;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Side;
import javafx.scene.Node;
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.layout.AnchorPane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Shape;
import javafx.util.Duration;
public class ChartContent extends StackPane {
private AnchorPane objectsLayer;
private LineChart<Number, Number> chart;
private NumberAxis xAxis;
private NumberAxis yAxis;
private Series<Number, Number> series = new Series<Number, Number>();
private int level = 0;
private int datas[][] = { { 15, 8, 12, 11, 16, 21, 13 },
{ 10, 24, 20, 16, 31, 25, 44 }, { 88, 60, 105, 75, 151, 121, 137 },
{ 1000, 1341, 1211, 1562, 1400, 1600, 1550 }
};
private List<Shape> shapes = new ArrayList<Shape>();
public ChartContent() {
xAxis = new NumberAxis();
yAxis = new NumberAxis();
yAxis.setSide(Side.RIGHT);
yAxis.setForceZeroInRange(false);
xAxis.setForceZeroInRange(false);
chart = new LineChart<Number, Number>(xAxis, yAxis);
chart.setCreateSymbols(false);
chart.setLegendVisible(false);
chart.setAnimated(false);
chart.setVerticalZeroLineVisible(false);
Timeline timer = new Timeline(new KeyFrame(Duration.seconds(5),
new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
chartRefresh();
}
}));
timer.setCycleCount(datas.length - 1);
timer.play();
objectsLayer = new AnchorPane();
objectsLayer.prefHeightProperty().bind(heightProperty());
objectsLayer.prefWidthProperty().bind(widthProperty());
getChildren().addAll(chart, objectsLayer);
chartRefresh();
}
private void chartRefresh() {
series.getData().clear();
if (level < datas.length) {
for (int i = 0; i < datas[level].length; i++) {
series.getData().add(
new Data<Number, Number>(i, datas[level][i]));
}
}
level++;
chart.getData().clear();
chart.getData().add(series);
series.getNode().setStyle("-fx-stroke:blue;-fx-stroke-width:1");
reDrawShapes(series);
}
private void reDrawShapes(Series<Number, Number> series) {
Node chartPlotBackground = chart.lookup(".chart-plot-background");
chartPlotBackground.setStyle("-fx-background-color:white");
Circle circle;
objectsLayer.getChildren().removeAll(shapes);
shapes.clear();
double top = chart.getPadding().getTop(), left = chart.getPadding()
.getLeft();
double minX = chartPlotBackground.getBoundsInParent().getMinX();
double minY = chartPlotBackground.getBoundsInParent().getMinY();
for (Data<Number, Number> data : series.getData()) {
circle = new Circle(minX
+ chart.getXAxis().getDisplayPosition(data.getXValue())
+ left, minY
+ chart.getYAxis().getDisplayPosition(data.getYValue())
+ top, 3, Color.RED);
shapes.add(circle);
}
objectsLayer.getChildren().addAll(shapes);
}
}
I am refreshing chart series every five seconds and redrawing its shapes as well. But after the shapes added to the AnchorPane, they are not there where I expect them to be.
Expected Result
Actual Result
First, note that for the exact functionality you're trying to achieve, this can be done simply by setting a node on the data.
(Aside: it could be argued, and I would argue, that making a node a property of the data displayed in the chart violates pretty much every good practice on the separation of view from data in UI development. The Chart API has a number of bad design flaws, imho, and this is one of them. There probably should be something like a Function<Data<X,Y>, Node> nodeFactory property of the Chart itself for this. However, it is what it is.)
private void chartRefresh() {
series.getData().clear();
if (level < datas.length) {
for (int i = 0; i < datas[level].length; i++) {
Data<Number, Number> data = new Data<Number, Number>(i, datas[level][i]);
data.setNode(new Circle(3, Color.RED));
series.getData().add(data);
}
}
level++;
chart.getData().clear();
chart.getData().add(series);
series.getNode().setStyle("-fx-stroke:blue;-fx-stroke-width:1");
// reDrawShapes(series);
}
This works if your node is simple enough that centering it on the point is what you need.
If you want something more complex, for which this doesn't work, the supported mechanism is to subclass the chart class and override the layoutPlotChildren() method. Here's the complete class using this approach:
import java.util.ArrayList;
import java.util.List;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Side;
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.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Shape;
import javafx.util.Duration;
public class ChartContent extends StackPane {
private LineChart<Number, Number> chart;
private NumberAxis xAxis;
private NumberAxis yAxis;
private Series<Number, Number> series = new Series<Number, Number>();
private int level = 0;
private int datas[][] = { { 15, 8, 12, 11, 16, 21, 13 },
{ 10, 24, 20, 16, 31, 25, 44 }, { 88, 60, 105, 75, 151, 121, 137 },
{ 1000, 1341, 1211, 1562, 1400, 1600, 1550 }
};
public ChartContent() {
xAxis = new NumberAxis();
yAxis = new NumberAxis();
yAxis.setSide(Side.RIGHT);
yAxis.setForceZeroInRange(false);
xAxis.setForceZeroInRange(false);
chart = new LineChart<Number, Number>(xAxis, yAxis) {
private List<Shape> shapes = new ArrayList<>();
#Override
public void layoutPlotChildren() {
super.layoutPlotChildren();
getPlotChildren().removeAll(shapes);
shapes.clear();
for (Data<Number, Number> d : series.getData()) {
double x = xAxis.getDisplayPosition(d.getXValue());
double y = yAxis.getDisplayPosition(d.getYValue());
shapes.add(new Circle(x, y, 3, Color.RED));
}
getPlotChildren().addAll(shapes);
}
};
chart.setCreateSymbols(false);
chart.setLegendVisible(false);
chart.setAnimated(false);
chart.setVerticalZeroLineVisible(false);
Timeline timer = new Timeline(new KeyFrame(Duration.seconds(5),
new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
chartRefresh();
}
}));
timer.setCycleCount(datas.length - 1);
timer.play();
getChildren().addAll(chart);
chartRefresh();
}
private void chartRefresh() {
series.getData().clear();
if (level < datas.length) {
for (int i = 0; i < datas[level].length; i++) {
Data<Number, Number> data = new Data<Number, Number>(i, datas[level][i]);
data.setNode(new Circle(3, Color.RED));
series.getData().add(data);
}
}
level++;
chart.getData().clear();
chart.getData().add(series);
series.getNode().setStyle("-fx-stroke:blue;-fx-stroke-width:1");
}
}
This results in
You can use this technique to, for example, add best fit lines to scatter plots or trend lines to line charts, etc.
I can't tell exactly why the code you used doesn't work, but it makes several assumptions about how the layout is managed (i.e. the location of chart-plot-background in relation to the overall chart itself) and also about when measurements are taken in order to do things like compute the scale in the axes for the mapping from "chart coordinates" to "pixel coordinates". It's not too hard to imagine these becoming invalid when the data changes and only being recalculated at the beginning of the layout process, for example. Logging the "data values" (data.getXValue() and data.getYValue()) alongside the values you get from Axis.getDisplayValue(...) for those values suggests that something akin to the latter explanation may be the case, as those definitely do not seem to produce the correct transformations.
Hooking into the layoutPlotChildren() method is more reliable.
Related
This chart shows the problem:
I have JavaFX program that calculates data and draws a chart, but why points are not connected properly? I have tried many things, even creating two separate series, but it doesn't work.
public void createScatterChart(){
final NumberAxis xAxis = new NumberAxis();
final NumberAxis yAxis = new NumberAxis();
final SmoothedChart<Number,Number> smoothedChart = new SmoothedChart<>(xAxis, yAxis);
XYChart.Series series1 = new XYChart.Series();
XYChart.Series series2 = new XYChart.Series();
XYChart.Series series3 = new XYChart.Series();
for(int i = 0 ; i < this.r.size() ; i ++)
{
series1.getData().add(new XYChart.Data(this.r.get(i) * Math.cos(Math.toRadians(this.nodes.get(i))),this.r.get(i) * Math.sin(Math.toRadians(this.nodes.get(i)))));
//series2.getData().add(new XYChart.Data(this.r.get(i) * Math.cos(Math.toRadians(this.nodes.get(i) * this.xArray[i][0])),this.r.get(i) * Math.sin(Math.toRadians(this.nodes.get(i) * this.xArray[i][0]))));
}
smoothedChart.getData().add(series1);
smoothedChart.getData().add(series2);
Stage stage = new Stage();
Scene scene = new Scene(smoothedChart,800,600);
stage.setScene(scene);
stage.show();
}
A similar problem is examined here, in which the solution hinges on the data sort order. Looking at LineChart, SortingPolicy.NONE specifies "The data should be left in the order defined by the list in XYChart.dataProperty()."
I had to change chart from my SmoothChart to standard LineChart.
Depending on your approach to smoothing, you may encounter the kind of cubic spline artifacts examined here, which also occurs in jfreechart-fx. An approach using Bézier curves is adduced here.
As tested using synthetic data:
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
/**
* #see https://stackoverflow.com/a/72607616/230513
* #see https://stackoverflow.com/a/2510048/230513
*/
public class ChartTest extends Application {
private static final int N = 32;
#Override
public void start(Stage stage) {
var xAxis = new NumberAxis();
var yAxis = new NumberAxis();
var series = new XYChart.Series();
series.setName("Data");
for (int i = 0; i <= N; i++) {
var t = 2 * Math.PI * i / N;
var x = Math.cos(t);
var y = Math.sin(t);
series.getData().add(new XYChart.Data(x, y));
}
var chart = new LineChart<Number, Number>(xAxis, yAxis);
chart.getData().add(series);
ObservableList<LineChart.SortingPolicy> policies
= FXCollections.observableArrayList(LineChart.SortingPolicy.values());
var policy = new ChoiceBox<LineChart.SortingPolicy>(policies);
policy.setTooltip(new Tooltip("Choose a data sorting policy."));
policy.getSelectionModel().select(chart.getAxisSortingPolicy());
chart.axisSortingPolicyProperty().bind(policy.valueProperty());
Pane root = new StackPane(chart, policy);
StackPane.setAlignment(policy, Pos.TOP_RIGHT);
stage.setScene(new Scene(root));
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
I'd like to implement the reveal highlight effect for JavaFX that can be seen in various parts of Windows 10, particularly the Settings and Calculator apps.
The effect seems to be composed for two parts, a border highlight (seen here) and a background highlight (seen here, though admittedly looks better in person due to compression).
My first instinct was to see if this could be done in some sort of pixel shader but after googling around for that it seems JavaFX does provide a public API for anything like that?
Would it be possible to create this effect without resorting to a canvas and drawing the whole UI by hand?
First I'd like to say I have no idea how Windows implements that style. But one idea I had is to have multiple layers:
A black background.
A circle with a radial gradient going from white to transparent that moves with the mouse.
A region with a black background and a shape that has holes wherever the option nodes are.
The option nodes with a layered background.
When the mouse is not hovering:
Transparent background with no insets.
Black background with a slight inset.
When the mouse is hovering:
Low opacity white background with no insets.
Black background with a slight inset.
White-to-transparent radial gradient background that's centered on the mouse.
Unfortunately that means a lot of the styling has to be done in the code even though I'd prefer to put most of it in CSS. Here's a proof-of-concept I quickly mocked up. It's not fully functional but shows the look you want is possible.
OptionsPane.java
import javafx.beans.InvalidationListener;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Bounds;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.TilePane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.paint.RadialGradient;
import javafx.scene.paint.Stop;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Shape;
import javafx.scene.text.Font;
public class OptionsPane extends Region {
public static class Option {
private final String title;
private final String subtitle;
private final Node graphic;
public Option(String title, String subtitle, Node graphic) {
this.title = title;
this.subtitle = subtitle;
this.graphic = graphic;
}
public String getTitle() {
return title;
}
public String getSubtitle() {
return subtitle;
}
public Node getGraphic() {
return graphic;
}
}
private final ObservableList<Option> options = FXCollections.observableArrayList();
private final TilePane topTiles = new TilePane();
private final Region midCover = new Region();
private final Circle underGlow = new Circle();
public OptionsPane() {
setBackground(new Background(new BackgroundFill(Color.BLACK, null, null)));
underGlow.setManaged(false);
underGlow.setRadius(75);
underGlow.visibleProperty().bind(hoverProperty());
underGlow.setFill(
new RadialGradient(
0, 0,
0.5, 0.5,
1.0,
true,
null,
new Stop(0.0, Color.WHITE),
new Stop(0.35, Color.TRANSPARENT)));
addEventFilter(
MouseEvent.MOUSE_MOVED,
e -> {
underGlow.setCenterX(e.getX());
underGlow.setCenterY(e.getY());
});
midCover.setBackground(new Background(new BackgroundFill(Color.BLACK, null, null)));
topTiles.setMinSize(0, 0);
topTiles.setVgap(20);
topTiles.setHgap(20);
topTiles.setPadding(new Insets(20));
topTiles.setPrefTileWidth(250);
topTiles.setPrefTileHeight(100);
topTiles.setPrefColumns(3);
options.addListener(
(InvalidationListener)
obs -> {
topTiles.getChildren().clear();
options.forEach(opt -> topTiles.getChildren().add(createOptionRegion(opt)));
});
getChildren().addAll(underGlow, midCover, topTiles);
}
public final ObservableList<Option> getOptions() {
return options;
}
#Override
protected void layoutChildren() {
double x = getInsets().getLeft();
double y = getInsets().getTop();
double w = getWidth() - getInsets().getRight() - x;
double h = getHeight() - getInsets().getBottom() - y;
layoutInArea(midCover, x, y, w, h, -1, HPos.CENTER, VPos.CENTER);
layoutInArea(topTiles, x, y, w, h, -1, HPos.CENTER, VPos.CENTER);
Shape coverShape = new Rectangle(x, y, w, h);
for (Node optionNode : topTiles.getChildren()) {
Bounds b = optionNode.getBoundsInParent();
Rectangle rect = new Rectangle(b.getMinX(), b.getMinY(), b.getWidth(), b.getHeight());
coverShape = Shape.subtract(coverShape, rect);
}
midCover.setShape(coverShape);
}
private Region createOptionRegion(Option option) {
Label titleLabel = new Label(option.getTitle());
titleLabel.setTextFill(Color.WHITE);
titleLabel.setFont(Font.font("System", 13));
Label subtitleLabel = new Label(option.getSubtitle());
subtitleLabel.setTextFill(Color.DARKGRAY);
subtitleLabel.setFont(Font.font("System", 10));
VBox textBox = new VBox(5, titleLabel, subtitleLabel);
HBox.setHgrow(textBox, Priority.ALWAYS);
HBox container = new HBox(10, textBox);
container.setPadding(new Insets(10));
if (option.getGraphic() != null) {
container.getChildren().add(0, option.getGraphic());
}
setNonHoverBackground(container);
container
.hoverProperty()
.addListener(
(obs, ov, nv) -> {
if (!nv) {
setNonHoverBackground(container);
}
});
container.setOnMouseMoved(e -> setHoverBackground(container, e.getX(), e.getY()));
return container;
}
private void setNonHoverBackground(Region region) {
BackgroundFill fill1 = new BackgroundFill(Color.TRANSPARENT, null, null);
BackgroundFill fill2 = new BackgroundFill(Color.BLACK, null, new Insets(2));
region.setBackground(new Background(fill1, fill2));
}
private void setHoverBackground(Region region, double x, double y) {
RadialGradient gradient =
new RadialGradient(
0, 0,
x, y,
400,
false,
null,
new Stop(0.0, new Color(1, 1, 1, 0.2)),
new Stop(0.35, Color.TRANSPARENT));
BackgroundFill fill1 = new BackgroundFill(new Color(1, 1, 1, 0.3), null, null);
BackgroundFill fill2 = new BackgroundFill(Color.BLACK, null, new Insets(2));
BackgroundFill fill3 = new BackgroundFill(gradient, null, null);
region.setBackground(new Background(fill1, fill2, fill3));
}
}
Main.java
import java.util.ArrayList;
import java.util.List;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
OptionsPane pane = new OptionsPane();
List<OptionsPane.Option> options = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Rectangle graphic = new Rectangle(20, 20, Color.DARKGRAY);
options.add(
new OptionsPane.Option("Option Title #" + (i + 1), "Description #" + (i + 1), graphic));
}
pane.getOptions().addAll(options);
primaryStage.setScene(new Scene(pane));
primaryStage.show();
}
}
And this is what it looks like:
It's not exactly the same but you can experiment yourself and change things as you want.
The JavaFX coordinate system draws Y coords from the top of screen, and is positive downwards. I would like it to be positive upwards, and start from the bottom of screen.
There needs to be a translate, and the text nodes need to be flipped.
And with that, hopefully the drawn rectangle will be positioned the "natural" way we do it in math class. With its bottom-left at the origin, expanding to the top-right.
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.chart.*;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class FlippedExampleChart extends Application {
private LineChart<Number, Number> chart;
#Override
public void start(Stage primaryStage) throws Exception {
final NumberAxis xAxis = new NumberAxis();
final NumberAxis yAxis = new NumberAxis();
// Flip the axis
yAxis.setScaleY(-1);
// TODO How to translate to bottom of screen.
// TODO How to flip the text nodes.
this.chart = new LineChart<Number, Number>(xAxis, yAxis) {
#Override
public void layoutPlotChildren() {
super.layoutPlotChildren();
double height = yAxis.getDisplayPosition(100);
Rectangle r = new Rectangle(0, 0, 50, height);
r.setFill(Color.GREEN);
getPlotChildren().addAll(r);
}
};
this.chart.setAnimated(false);
VBox vbox = new VBox(this.chart);
Scene scene = new Scene(vbox, 400, 200);
primaryStage.setScene(scene);
primaryStage.setHeight(600);
primaryStage.setWidth(400);
primaryStage.show();
}
public static void main(String[] args) {
Application.launch(args);
}
}
I'm assuming here the aim is to draw a shape using the coordinate system defined by the chart axes.
The easiest way is probably to transform the shape instead of the axis. You can create a utility method for this:
private Transform chartDisplayTransform(NumberAxis xAxis, NumberAxis yAxis) {
return new Affine(
xAxis.getScale(), 0, xAxis.getDisplayPosition(0),
0, yAxis.getScale(), yAxis.getDisplayPosition(0)
);
}
One other note about your code: the layoutPlotChildren() method doesn't necessarily remove nodes, so you may end up adding more rectangles than you expect with the code you posted.
Here's a version of your code that uses this method (and ensures the rectangle is only added once).
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.transform.Affine;
import javafx.scene.transform.Transform;
import javafx.stage.Stage;
public class FlippedExampleChart extends Application {
private LineChart<Number, Number> chart;
#Override
public void start(Stage primaryStage) throws Exception {
final NumberAxis xAxis = new NumberAxis();
final NumberAxis yAxis = new NumberAxis();
// Flip the axis
// yAxis.setScaleY(-1);
Rectangle r = new Rectangle(0, 0, 50, 100);
r.setFill(Color.GREEN);
this.chart = new LineChart<Number, Number>(xAxis, yAxis) {
#Override
public void layoutPlotChildren() {
super.layoutPlotChildren();
r.getTransforms().setAll(chartDisplayTransform(xAxis, yAxis));
// note nodes don't get removed from the plot children, and this method may be
// called often:
if (!getPlotChildren().contains(r)) {
getPlotChildren().add(r);
}
}
};
this.chart.setAnimated(false);
VBox vbox = new VBox(this.chart);
Scene scene = new Scene(vbox, 400, 200);
primaryStage.setScene(scene);
primaryStage.setHeight(600);
primaryStage.setWidth(400);
primaryStage.show();
}
private Transform chartDisplayTransform(NumberAxis xAxis, NumberAxis yAxis) {
return new Affine(xAxis.getScale(), 0, xAxis.getDisplayPosition(0), 0, yAxis.getScale(),
yAxis.getDisplayPosition(0));
}
public static void main(String[] args) {
Application.launch(args);
}
}
And the result:
If you have multiple nodes to treat this way, the strategy is to add them to a Group, and apply the transform to the Group:
#Override
public void start(Stage primaryStage) throws Exception {
final NumberAxis xAxis = new NumberAxis();
final NumberAxis yAxis = new NumberAxis();
Group extraNodes = new Group();
this.chart = new LineChart<Number, Number>(xAxis, yAxis) {
#Override
public void layoutPlotChildren() {
super.layoutPlotChildren();
Rectangle r1 = new Rectangle(0, 0, 50, 100);
r1.setFill(Color.GREEN);
Rectangle r2 = new Rectangle(70, 0, 30, 20);
r2.setFill(Color.AQUAMARINE);
extraNodes.getChildren().setAll(r1, r2);
extraNodes.getTransforms().setAll(chartDisplayTransform(xAxis, yAxis));
// note nodes don't get removed from the plot children, and this method may be
// called often:
if (!getPlotChildren().contains(extraNodes)) {
getPlotChildren().add(extraNodes);
}
}
};
this.chart.setAnimated(false);
VBox vbox = new VBox(this.chart);
Scene scene = new Scene(vbox, 400, 200);
primaryStage.setScene(scene);
primaryStage.setHeight(600);
primaryStage.setWidth(400);
primaryStage.show();
}
Also see this related question
I'm trying to force the update of a custom XYChart in a timer method, but the only thing that seems to work is resizing the window.
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Shape;
import javafx.scene.transform.Affine;
import javafx.scene.transform.Transform;
import javafx.stage.Stage;
import java.util.*;
import java.util.stream.Collectors;
public class TestCustomLayoutUpdate extends Application {
private LineChart<Number, Number> chart;
private NumberAxis xAxis;
private NumberAxis yAxis;
private ShopItem currentShopItem;
class ShopItem {
private double price;
public ShopItem(double price) {
this.price = price;
}
}
#Override
public void start(Stage primaryStage) {
createChart();
Scene scene = new Scene(chart, 600, 400);
primaryStage.setScene(scene);
primaryStage.setHeight(600);
primaryStage.setWidth(400);
primaryStage.show();
Random rng = new Random();
// Note since this is a regular timer not javafx timer that we should use platform run later.
TimerTask repeatedTask = new TimerTask() {
public void run() {
currentShopItem = new ShopItem(rng.nextDouble() * 100);
Platform.runLater(() -> {
chart.layout();
chart.requestLayout();
xAxis.layout();
});
}
};
Timer timer = new Timer("Timer");
long delay = 1000L;
long period = 1000L;
timer.scheduleAtFixedRate(repeatedTask, delay, period);
}
public void createChart() {
xAxis = new NumberAxis();
yAxis = new NumberAxis();
xAxis.setAutoRanging(false);
xAxis.setUpperBound(100);
chart = new LineChart<Number, Number>(xAxis, yAxis) {
private List<Shape> shapes = new ArrayList<>();
#Override
public void layoutPlotChildren() {
super.layoutPlotChildren();
getPlotChildren().removeAll(shapes);
shapes.clear();
if (currentShopItem != null) {
Rectangle rect = new Rectangle(0, 0, 10, currentShopItem.price);
rect.getTransforms().setAll(chartDisplayTransform(xAxis, yAxis));
rect.setFill(Color.RED);
shapes.add(rect);
getPlotChildren().addAll(shapes);
}
}
};
}
private Transform chartDisplayTransform(NumberAxis xAxis, NumberAxis yAxis) {
return new Affine(xAxis.getScale(), 0, xAxis.getDisplayPosition(0), 0, yAxis.getScale(),
yAxis.getDisplayPosition(0));
}
public static void main(String[] args) {
Application.launch(args);
}
}
JavaFX will automatically layout and redraw any parts of the scene graph if properties of any of the nodes that are part of the graph change. The problem with
the way you have structured the code is that you only change the scene graph (change the dimensions of the rectangle and/or change the plot children of the chart) in the layoutPlotChildren() method, which (I believe) is called as part of the layout process. So when you request a layout, JavaFX checks to see if anything in the scene graph has changed, sees that it hasn't, and so doesn't perform a layout. Thus layoutPlotChildren() isn't called, and so the scene graph isn't changed...
So to fix this, you just need to make sure the existing rectangle is updated, or that the list of plot children change, when the underlying data change. You can accomplish this by using JavaFX properties, and observing them from your chart subclass. (There are other ways too, I suppose, such as defining a method in the chart subclass that updates the rectangle, and invoking it from the animation loop. But observing a JavaFX property is the API-preferred way to do this.)
As an aside, if you want to change anything periodically that updates graphics, the preferred way to do this in JavaFX is with a Timeline, which operates entirely on the JavaFX thread and avoids the need to think about synchronization of variables, etc.
Here's a version of your example with these changes, which works as desired:
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Shape;
import javafx.scene.transform.Affine;
import javafx.scene.transform.Transform;
import javafx.stage.Stage;
import javafx.util.Duration;
public class TestCustomLayoutUpdate extends Application {
private LineChart<Number, Number> chart;
private NumberAxis xAxis;
private NumberAxis yAxis;
private ObjectProperty<ShopItem> currentShopItem;
class ShopItem {
private double price;
public ShopItem(double price) {
this.price = price;
}
}
#Override
public void start(Stage primaryStage) {
currentShopItem = new SimpleObjectProperty<>();
createChart();
Scene scene = new Scene(chart, 600, 400);
primaryStage.setScene(scene);
primaryStage.setHeight(600);
primaryStage.setWidth(400);
primaryStage.show();
Random rng = new Random();
Timeline timeline = new Timeline(new KeyFrame(Duration.seconds(1),
evt -> currentShopItem.set(new ShopItem(rng.nextDouble() * 100))
));
timeline.setCycleCount(Animation.INDEFINITE);
timeline.play();
}
public void createChart() {
xAxis = new NumberAxis();
yAxis = new NumberAxis();
xAxis.setAutoRanging(false);
xAxis.setUpperBound(100);
chart = new LineChart<Number, Number>(xAxis, yAxis) {
private List<Shape> shapes = new ArrayList<>();
private Rectangle rect ;
// anonymous class constructor:
{
rect = new Rectangle(0,0, Color.RED);
currentShopItem.addListener((obs, oldItem, newItem) -> {
if (newItem == null) {
rect.setWidth(0);
rect.setHeight(0);
} else {
rect.setWidth(10);
rect.setHeight(newItem.price);
}
});
}
#Override
public void layoutPlotChildren() {
super.layoutPlotChildren();
getPlotChildren().removeAll(shapes);
shapes.clear();
if (currentShopItem != null) {
rect.getTransforms().setAll(chartDisplayTransform(xAxis, yAxis));
shapes.add(rect);
getPlotChildren().addAll(shapes);
}
}
};
}
private Transform chartDisplayTransform(NumberAxis xAxis, NumberAxis yAxis) {
return new Affine(xAxis.getScale(), 0, xAxis.getDisplayPosition(0), 0, yAxis.getScale(),
yAxis.getDisplayPosition(0));
}
public static void main(String[] args) {
Application.launch(args);
}
}
I'm writing a test application that displays an A* search in action, and it's running really slow. I profiled it using VisualVM, and got the following results:
Note that the third entry is a lambda containing some long-running code.
The problem is, I can't find a lambda with such a signature anywhere.
Is there any way for me to find out what lambda it's referring to?
Here is pacmanTest.FrontierVisual.java:
package pacmanTest;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import utils.Duple;
import utils.MainLoop;
public class FrontierVisual
extends Application {
private Stage stage;
private Scene scene;
private final int areaWidth = 800;
private final int areaHeight = 800;
private final double drawScale = 15;
private Canvas canvas = new Canvas(800, 800);
private GraphicsContext gc = canvas.getGraphicsContext2D();
private final MapArea<Wall> area = new MapArea<>(areaWidth, areaHeight);
private static List<Duple<Integer>> cellsAround(Duple<Integer> pos, List<Duple<Integer>> offsets) {
List<Duple<Integer>> surroundingCells = new ArrayList<>();
offsets.stream()
.map( offset -> pos.map(offset, (x1, x2) -> x1 + x2) )
.forEach(surroundingCells::add);
return surroundingCells;
}
private void drawSquare(Duple<Integer> position, Color color) {
gc.setFill(color);
gc.fillRect(position.getX() * drawScale, position.getY() * drawScale, drawScale, drawScale);
}
#Override
public void start(Stage stage) throws Exception {
this.stage = stage;
BorderPane rootNode = new BorderPane();
scene = new Scene(rootNode);
stage.setScene(scene);
rootNode.setCenter(canvas);
Duple<Integer> startPos = new Duple<Integer>(20, 20);
Duple<Integer> goalPos = new Duple<Integer>(30, 30);
Wall wall = new SolidWall();
/*for (int d = 29; d <= 41; d++) {
int e = d / 2;
area.setCellAt(d, e, wall);
}*/
area.setCellAt(19, 32, wall);
area.setCellAt(20, 31, wall);
area.setCellAt(21, 30, wall);
area.setCellAt(22, 29, wall);
area.setCellAt(23, 28, wall);
area.setCellAt(24, 27, wall);
area.setCellAt(25, 26, wall);
area.setCellAt(26, 25, wall);
area.setCellAt(27, 24, wall);
area.setCellAt(28, 23, wall);
area.setCellAt(29, 22, wall);
area.setCellAt(30, 21, wall);
area.setCellAt(31, 20, wall);
area.setCellAt(32, 19, wall);
Deque<Duple<Integer>> frontier = new ArrayDeque<>();
Map<Duple<Integer>, Duple<Integer>> cameFrom = new HashMap<>();
cameFrom.put(startPos, startPos);
frontier.push(startPos);
final List<Duple<Integer>> fourDirectionOffsets = Collections.unmodifiableList(Arrays.asList(
new Duple<Integer>(1,0), new Duple<Integer>(-1,0), new Duple<Integer>(0,1), new Duple<Integer>(0,-1) ));
MainLoop mainLoop = new MainLoop(10000, t -> {
utils.Utils.clearCanvas(gc);
gc.setFill(Color.STEELBLUE);
Duple<Integer> poppedLocation = frontier.pop();
drawSquare(startPos, Color.BLACK);
drawSquare(goalPos, Color.GREEN);
List<Duple<Integer>> neighbors = cellsAround(poppedLocation, fourDirectionOffsets);
neighbors.stream()
.filter(location -> !cameFrom.containsKey(location) && area.cellIsInBounds(location) && area.getCellAt(location) == null)
.forEach( neighbor -> {
frontier.add(neighbor);
cameFrom.put(neighbor, poppedLocation);
drawSquare(neighbor, Color.CORAL);
});
frontier.stream()
.forEach( frontierPos -> {
drawSquare(frontierPos, Color.BLUE);
});
reconstructPath(cameFrom, startPos, goalPos).stream()
.forEach( pathPos -> {
if (pathPos != startPos && area.getCellAt(pathPos) == null) {
drawSquare(pathPos, Color.ORANGE);
}
});
area.forallNonEmptyCells( (pos, contents) -> {
drawSquare(pos, Color.CHOCOLATE);
});
});
mainLoop.start();
stage.show();
}
private static List<Duple<Integer>> reconstructPath(Map<Duple<Integer>, Duple<Integer>> cameFrom, Duple<Integer> start, Duple<Integer> goal) {
List<Duple<Integer>> path = new ArrayList<>();
path.add(goal);
Duple<Integer> current = goal;
do {
path.add(current);
current = cameFrom.get(current);
} while (current != null && !current.equals(start));
Collections.reverse(path);
return path;
}
public static void main(String[] args) {
launch(args);
}
}
Any ideas here would be appreciated.
Side note:
If anyone knows how to reduce the drawing time of JavaFX's canvas, that would be awesome given it's the biggest CPU hog.
Well this is your lambda$1 expression because of the "parameter list". This part is the only one which uses a Deque, 2 x Duple, List, Map, Long:
new MainLoop(10000, /* THIS -> */ t -> { ... });
BTW don't do any filtering actions in a Stream in the forEach method. Therefore use the Stream::filter.
Here:
reconstructPath(cameFrom, startPos, goalPos).stream()
.filter(pathPos != startPos && area.getCellAt(pathPos) == null)
.forEach(pathPos -> drawSquare(pathPos, Color.ORANGE));