JavaFX LineChart or ScatterChart: Connect alternating points - javafx

I'm trying to use either a ScatterChart or LineChart to make a Gantt chart of sorts. I have a series of events, and I'd like to make a line connect the start and end point of each event. The x axis will be time and the y axis will be an incrementing index. So in other words, if event 1 is from t=1 to t=5, and event 2 is from t=3 to t=10, I would like to connect the points (1,1) and (5,1), and (3,2) and (10,2), and no others.
There's an implementation of a Gantt chart elsewhere on StackOverflow, which I started out with, but it is unusably slow when trying to plot ten thousand events.
I know that one solution would be to have a XYChart.Series for each event, so that for 1000 events, the data input for the chart would consist of 1000 XYChart.Series, each containing 2 XYChart.Data objects. But style-wise this would be impossible, as I want there to be multiple series here, with different colors.
Is there a simple solution here? I know that in the worst case I can do this with Paths, although I'm not sure how so I'll have to research this, but I'm not sure if it's the best solution.
EDIT: Below are the examples of what I'm trying to do. I want to make something look like this, but without the vertical lines; I want them disconnected.
EDIT EDIT: I modified the datapoints so that they could overlap each other, for another aspect of the requirements. I added a policy to not sort the input points for the line chart. This is not needed or a valid option in the case where it's a scatter chart, so if you're going back and forth, you'd have to comment out the line chart.setAxisSortingPolicy(LineChart.SortingPolicy.NONE);
Here's the source code. Going from one to the other is as simple as swapping LineChart with ScatterChart (and commenting out the line mentioned above):
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.ScatterChart;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class DummyGanttLineChartApp extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
ObservableList<Data<Number, Number>> dataList1 = FXCollections.observableArrayList();
ObservableList<Data<Number, Number>> dataList2 = FXCollections.observableArrayList();
dataList1.add(new Data<Number, Number>(1, 1));
dataList1.add(new Data<Number, Number>(2, 1));
dataList1.add(new Data<Number, Number>(1.5, 2));
dataList1.add(new Data<Number, Number>(2.5, 2));
dataList1.add(new Data<Number, Number>(2, 3));
dataList1.add(new Data<Number, Number>(3, 3));
dataList2.add(new Data<Number, Number>(1.5, 4));
dataList2.add(new Data<Number, Number>(2.5, 4));
dataList2.add(new Data<Number, Number>(2, 5));
dataList2.add(new Data<Number, Number>(3, 5));
dataList2.add(new Data<Number, Number>(2.5, 6));
dataList2.add(new Data<Number, Number>(3.5, 6));
Series<Number, Number> series1 = new Series<Number, Number>("List 1", dataList1);
Series<Number, Number> series2 = new Series<Number, Number>("List 2", dataList2);
ObservableList<Series<Number, Number>> data = FXCollections.observableArrayList();
data.add(series1);
data.add(series2);
NumberAxis xAxis = new NumberAxis();
NumberAxis yAxis = new NumberAxis();
xAxis.autoRangingProperty().set(false);
xAxis.setLowerBound(0);
xAxis.setUpperBound(5);
LineChart<Number, Number> chart = new LineChart<Number, Number>(xAxis, yAxis);
chart.setAxisSortingPolicy(LineChart.SortingPolicy.NONE);
chart.getData().addAll(data);
VBox box = new VBox(10);
box.getChildren().add(chart);
primaryStage.setScene(new Scene(box));
primaryStage.show();
}
}
This is a separate class that actually launches this (a workaround for some weirdness with my setup. You should be able to use a main method inside DummyGanttLineChartApp, but I get an error). Don't worry about that.
import javafx.application.Application;
public class DummyGanttLineChart {
public static void main(String [] args) {
Application.launch(DummyGanttLineChartApp.class, args);
}
}

Thanks for providing the screenshot and data. Considering your requirement, I think customizing ScatterChart can help you to get the desired behavior.
The idea is, we figure out the min and max data points for each unique Y value and draw a line between them (I am using Path). I am picking the series color from the data points itself and applying it on the Path. This works well good when resizing the chart as well.
Note: I have not tested the performance part as you mentioned (ten thousand events), may be you can give a try and update me about the performance :)
Below is the quick sample demo of what I have tried:
import javafx.application.Application;
import javafx.beans.NamedArg;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Bounds;
import javafx.scene.Scene;
import javafx.scene.chart.Axis;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.ScatterChart;
import javafx.scene.chart.XYChart;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.stage.Stage;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
public class DummyGanttLineChartApp extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
ObservableList<Data<Number, Number>> dataList1 = FXCollections.observableArrayList();
ObservableList<Data<Number, Number>> dataList2 = FXCollections.observableArrayList();
dataList1.add(new Data<Number, Number>(1, 1));
dataList1.add(new Data<Number, Number>(2, 1));
dataList1.add(new Data<Number, Number>(1.5, 2));
dataList1.add(new Data<Number, Number>(2.5, 2));
dataList1.add(new Data<Number, Number>(3.5, 2)); // Including this to test whether it is picking min and max points ;)
dataList1.add(new Data<Number, Number>(2, 3));
dataList1.add(new Data<Number, Number>(3, 3));
dataList2.add(new Data<Number, Number>(1.5, 4));
dataList2.add(new Data<Number, Number>(2.5, 4));
dataList2.add(new Data<Number, Number>(2, 5));
dataList2.add(new Data<Number, Number>(3, 5));
dataList2.add(new Data<Number, Number>(2.5, 6));
dataList2.add(new Data<Number, Number>(3.5, 6));
Series<Number, Number> series1 = new Series<Number, Number>("List 1", dataList1);
Series<Number, Number> series2 = new Series<Number, Number>("List 2", dataList2);
ObservableList<Series<Number, Number>> data = FXCollections.observableArrayList();
data.add(series1);
data.add(series2);
NumberAxis xAxis = new NumberAxis();
NumberAxis yAxis = new NumberAxis();
xAxis.autoRangingProperty().set(false);
xAxis.setLowerBound(0);
xAxis.setUpperBound(5);
GanttLineChart chart = new GanttLineChart(xAxis, yAxis);
chart.getData().addAll(data);
VBox box = new VBox(10);
box.getChildren().add(chart);
primaryStage.setScene(new Scene(box));
primaryStage.show();
}
/**
* Custom GanttLine chart.
*/
class GanttLineChart extends ScatterChart<Number, Number> {
/**
* References to all paths. Usually one path per series.
*/
final List<Path> paths = new ArrayList<>();
public GanttLineChart(#NamedArg("xAxis") Axis<Number> xAxis, #NamedArg("yAxis") Axis<Number> yAxis) {
super(xAxis, yAxis, FXCollections.observableArrayList());
}
#Override
protected void layoutPlotChildren() {
super.layoutPlotChildren();
// Ensure to remove previous paths.
getPlotChildren().removeAll(paths);
for (XYChart.Series<Number, Number> series : getData()) {
// Map to keep references of min and max items for each unique Y value.
final Map<Number, MinMax<XYChart.Data<Number, Number>, XYChart.Data<Number, Number>>> yMap = new HashMap<>();
for (XYChart.Data<Number, Number> item : series.getData()) {
final Number y = item.getYValue();
MinMax<XYChart.Data<Number, Number>, XYChart.Data<Number, Number>> bounds = yMap.get(y);
if (bounds == null) {
bounds = new MinMax<>(item, item);
yMap.put(y, bounds);
}
if (bounds.getMin() != item) {
if (compareTo(item.getXValue(), bounds.getMin().getXValue()) < 0) {
bounds.setMin(item);
}
}
if (bounds.getMax() != item) {
if (compareTo(item.getXValue(), bounds.getMin().getXValue()) > 0) {
bounds.setMax(item);
}
}
}
final AtomicBoolean filled = new AtomicBoolean();
// Construct a path per series.
final Path path = new Path();
path.setStrokeWidth(2);
yMap.forEach((k, v) -> {
if (!filled.get()) {
Region node = (Region) v.getMin().getNode();
node.applyCss();
path.setFill(node.getBackground().getFills().get(0).getFill());
path.setStroke(path.getFill());
filled.set(true);
}
Bounds minPointBounds = v.getMin().getNode().getBoundsInParent();
Bounds maxPointBounds = v.getMax().getNode().getBoundsInParent();
double minX = minPointBounds.getMinX() + minPointBounds.getWidth() / 2;
double minY = minPointBounds.getMinY() + minPointBounds.getHeight() / 2;
double maxX = maxPointBounds.getMinX() + maxPointBounds.getWidth() / 2;
double maxY = maxPointBounds.getMinY() + maxPointBounds.getHeight() / 2;
// Draw a line in the path from min to max points for each unique Y value.
path.getElements().addAll(new MoveTo(minX, minY), new LineTo(maxX, maxY));
});
// Add the path to plot children and keep a reference for later use.
getPlotChildren().add(path);
paths.add(path);
}
}
private int compareTo(Number n1, Number n2) {
BigDecimal b1 = new BigDecimal(n1.doubleValue());
BigDecimal b2 = new BigDecimal(n2.doubleValue());
return b1.compareTo(b2);
}
/**
* Data object to hold min and max items.
*/
class MinMax<K, V> {
private K min;
private V max;
public MinMax(#NamedArg("key") K min, #NamedArg("value") V max) {
this.min = min;
this.max = max;
}
public K getMin() {
return min;
}
public V getMax() {
return max;
}
public void setMin(K min) {
this.min = min;
}
public void setMax(V max) {
this.max = max;
}
}
}
}

Related

JavaFX LineChart Mouse Hover Values [duplicate]

I am in the process of creating a line chart in JavaFX. All is good currently and it successfully creates the chart with the data I need from a database stored procedure. Anyway what I require if possible is for every data point on the LineChart to have a mouse hover event on it which states the value behind the specific point, for example £150,000. I have seen examples of this been done on PieCharts where it shows the % value on hover but I cannot find examples anywhere for LineCharts, can this even be done?
Can anyone point me in the right direction if possible?
Code so far:
private static final String MINIMIZED = "MINIMIZED";
private static final String MAXIMIZED = "MAXIMIZED";
private static String chartState = MINIMIZED;
// 12 Month Sales Chart
XYChart.Series<String, Number> series = new XYChart.Series<>();
XYChart.Series<String, Number> series2 = new XYChart.Series<>();
public void getDeltaData() {
try {
Connection con = DriverManager.getConnection(connectionUrl);
//Get all records from table
String SQL = "";
Statement stmt = con.createStatement();
//Create the result set from query execution.
ResultSet rs = stmt.executeQuery(SQL);
while (rs.next()) {
series.getData().add(new XYChart.Data<String, Number>(rs.getString(1),
Double.parseDouble(rs.getString(7))));
series2.getData().add(new XYChart.Data<String, Number>(rs.getString(1),
Double.parseDouble(rs.getString(8))));
}
rs.close();
stmt.close();
} catch (Exception e) {
}
yearChart = createChart();
}
protected LineChart<String, Number> createChart() {
final CategoryAxis xAxis = new CategoryAxis();
final NumberAxis yAxis = new NumberAxis();
// setup chart
series.setName("Target");
series2.setName("Actual");
xAxis.setLabel("Period");
yAxis.setLabel("£");
yearChart.getData().add(series);
yearChart.getData().add(series2);
yearChart.setCreateSymbols(false);
return yearChart;
}
Answer provided by jewelsea is a perfect solution to this problem.
Thank you, jewelsea.
Use XYChart.Data.setNode(hoverPane) to display a custom node for each data point. Make the hoverNode a container like a StackPane. Add mouse event listeners so that you know when the mouse enters and leaves the node. On enter, place a Label for the value inside the hoverPane. On exit, remove the label from the hoverPane.
There is some example code to demonstrate this technique.
Output of the sample code is shown with the cursor hovered over the 22 node.
Using Tooltip:
import java.util.Iterator;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.TreeMap;
import javafx.application.Application;
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.control.Tooltip;
import javafx.stage.Stage;
/**
*
* #author blj0011
*/
public class JavaFXApplication250 extends Application
{
#Override
public void start(Stage stage)
{
stage.setTitle("Line Chart Sample");
//defining the axes
final NumberAxis xAxis = new NumberAxis();
final NumberAxis yAxis = new NumberAxis();
xAxis.setLabel("Number of Month");
//creating the chart
final LineChart<Number, Number> lineChart = new LineChart<>(xAxis, yAxis);
lineChart.setTitle("Stock Monitoring, 2010");
//defining a series
XYChart.Series<Number, Number> series = new XYChart.Series();
series.setName("My portfolio");
//populating the series with data
Random rand = new Random();
TreeMap<Integer, Integer> data = new TreeMap();
//Create Chart data
for (int i = 0; i < 3; i++) {
data.put(rand.nextInt(51), rand.nextInt(51));
}
Set set = data.entrySet();
Iterator i = set.iterator();
while (i.hasNext()) {
Map.Entry me = (Map.Entry) i.next();
System.out.println(me.getKey() + " - " + me.getValue());
series.getData().add(new XYChart.Data(me.getKey(), me.getValue()));//Add data to series
}
lineChart.getData().add(series);
//loop through data and add tooltip
//THIS MUST BE DONE AFTER ADDING THE DATA TO THE CHART!
for (Data<Number, Number> entry : series.getData()) {
System.out.println("Entered!");
Tooltip t = new Tooltip(entry.getYValue().toString());
Tooltip.install(entry.getNode(), t);
}
Scene scene = new Scene(lineChart, 800, 600);
stage.setScene(scene);
stage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args)
{
launch(args);
}
}

Get Viewport of translated and scaled node

The ask: How do I get the viewing rectangle in the coordinates of a transformed and scaled node?
The code is attached below, it is based upon the code from this answer: JavaFX 8 Dynamic Node scaling
The details:
I have a simple pane, BigGridPane that contains a collection of squares, all 50x50.
I have it within this PanAndZoomPane construct that was lifted from the answer referenced above. I can not honestly say I fully understand the PanAndZoomPane implementation. For example, it's not clear to me why it needs a ScrollPane at all, but I have not delved in to trying without it.
The PanAndZoomPane lets me pan and zoom my BigGridPane. This works just dandy.
There are 4 Panes involved in this total construct, in this heirarchy: ScrollPane contains PanAndZoomPane which contains Group which contains BigGridPane.
ScrollPane
PanAndZoomPane
Group
BigGridPane
I have put listeners on the boundsInLocalProperty and boundsInParentProperty of all of these, and the only one of these that changes while panning and zooming, is the boundsInParentProperty of the PanAndZoomPane. (For some reason I've seen it trigger on the scroll pane, but all of the values are the same, so I don't include that here).
Along with the boundsInParentProperty changes, the translateX, translateY, and myScale properties of the PanAndZoomPane change as things move around. This is expected, of course. myScale is bound to the scaleX and scaleY properties of the PanAndZoomPane.
This is what it looks like at startup.
If I pan the grid as shown, putting 2-2 in the upper left:
We can see the properties of the PanAndZoomPane.
panAndZoom in parent: BoundingBox [minX:-99.5, minY:-99.5, minZ:0.0,
width:501.5, height:501.5, depth:0.0,
maxX:402.0, maxY:402.0, maxZ:0.0]
paz scale = 1.0 - tx: -99.0 - ty: -99.0
Scale is 1 (no zoom), and we've translated ~100x100. That is, the origin of the BigGridPane is at -100,-100. This all makes complete sense. Similarly, the bounding box shows the same thing. The origin is at -100,-100.
In this scenario, I would like to derive a rectangle that shows me what I'm seeing in the window, in the coordinates of the BigGridPane. That would mean a rectangle of
x:100 y:100 width:250 height:250
Normally, I think, this would be the viewport of the ScrollPane, but since this code isn't actually using the ScrollPane for scrolling (again, I'm not quite exactly what it's role is here), the ScrollPane viewport never changes.
I should note that there are shenanigans happening right now because of the retina display on my mac. If you look at the rectangles, showing 5x5, they're 50x50 rectangles, so we should be seeing 10x10, but because of the retina display on my iMac, everything is doubled. What we're seeing in BigGridPane coordinates is a 250x250 block of 5 squares, offset by 100x100. The fact that this is being showing in a window of 500x500 is a detail (but unlikely one we can ignore).
But to reiterate what my question is, that's what I'm trying to get: that 250x250 square at 100x100.
It's odd that it's offset by 100x100 even though the frame is twice as big (500 vs 250). If I pan to where 1-1 is the upper left, the offset is -50,-50, like it should be.
Now, let's add zooming, and pan again to 2-2.
1 click of the scroll wheel and the scale jumps to 1.5.
panAndZoom in parent: BoundingBox [minX:-149.375, minY:-150.375, minZ:0.0,
width:752.25, height:752.25, depth:0.0,
maxX:602.875, maxY:601.875, maxZ:0.0]
paz scale = 1.5 - tx: -23.375 - ty: -24.375
What I want, again, in this case, is a rectangle in BigGridPane coordinates. Roughly:
x:100 y:100 w:150 h:150
We see we're offset by 2x2 boxes (100x100) and we see 3+ boxes (150x150).
So. Back to the bounding box. MinX and minY = -150,-150. This is good. 100 x 1.5 = 150. Similarly the width and height are 750. 500 x 1.5 = 750. So, that is good.
The translates are where we go off the rails. -23.375, -24.375. I have no idea where these numbers come from. I can't seem to correlate them to anything in regards to 100, 150, 1.5 zoom, etc.
Worse, if we pan (while still at 1.5 scale) to "0,0", before, at scale=1, tx and ty were both 0. That's good.
panAndZoom in parent: BoundingBox [minX:0.625, minY:0.625, minZ:0.0,
width:752.25, height:752.25, depth:0.0,
maxX:752.875, maxY:752.875, maxZ:0.0]
paz scale = 1.5 - tx: 126.625 - ty: 126.625
Now, they're 126.625 (probably should be rounded to 125). I have no idea where those numbers come from.
I've tried all sorts of runs on the numbers to see where these numbers come from.
JavaFX knows what the numbers are! (even if the whole retina thing is kind of messing with my head, I'm going to ignore it for the moment).
And I don't see anything in the transforms of any of the panes.
So, my coordinate systems are all over the map, and I'd like to know what part of my BigGridPane is being shown in my panned and scaled view.
Code:
package pkg;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.util.Duration;
public class PanZoomTest extends Application {
private ScrollPane scrollPane = new ScrollPane();
private final DoubleProperty zoomProperty = new SimpleDoubleProperty(1.0d);
private final DoubleProperty deltaY = new SimpleDoubleProperty(0.0d);
private final Group group = new Group();
PanAndZoomPane panAndZoomPane = null;
BigGridPane1 bigGridPane = new BigGridPane1(10, 10, 50);
#Override
public void start(Stage primaryStage) throws Exception {
scrollPane.setPannable(true);
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
group.getChildren().add(bigGridPane);
panAndZoomPane = new PanAndZoomPane();
zoomProperty.bind(panAndZoomPane.myScale);
deltaY.bind(panAndZoomPane.deltaY);
panAndZoomPane.getChildren().add(group);
SceneGestures sceneGestures = new SceneGestures(panAndZoomPane);
scrollPane.setContent(panAndZoomPane);
panAndZoomPane.toBack();
addListeners("panAndZoom", panAndZoomPane);
scrollPane.addEventFilter(MouseEvent.MOUSE_PRESSED, sceneGestures.getOnMousePressedEventHandler());
scrollPane.addEventFilter(MouseEvent.MOUSE_DRAGGED, sceneGestures.getOnMouseDraggedEventHandler());
scrollPane.addEventFilter(ScrollEvent.ANY, sceneGestures.getOnScrollEventHandler());
AnchorPane anchorPane = new AnchorPane();
anchorPane.getChildren().add(scrollPane);
anchorPane.setTopAnchor(scrollPane, 1.0d);
anchorPane.setRightAnchor(scrollPane, 1.0d);
anchorPane.setBottomAnchor(scrollPane, 1.0d);
anchorPane.setLeftAnchor(scrollPane, 1.0d);
BorderPane root = new BorderPane(anchorPane);
Label label = new Label("Pan and Zoom Test");
root.setTop(label);
Scene scene = new Scene(root, 250, 250);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
private void addListeners(String label, Node node) {
node.boundsInLocalProperty().addListener((o) -> {
System.out.println(label + " in local: " + node.getBoundsInLocal());
});
node.boundsInParentProperty().addListener((o) -> {
System.out.println(label + " in parent: " + node.getBoundsInParent());
System.out.println("paz scale = " + panAndZoomPane.getScale() + " - "
+ panAndZoomPane.getTranslateX() + " - "
+ panAndZoomPane.getTranslateY());
System.out.println(group.getTransforms());
});
}
class BigGridPane extends Region {
int rows;
int cols;
int size;
Font numFont = Font.font("sans-serif", 8);
FontMetrics numMetrics = new FontMetrics(numFont);
public BigGridPane(int cols, int rows, int size) {
this.rows = rows;
this.cols = cols;
this.size = size;
int sizeX = cols * size;
int sizeY = rows * size;
setMinSize(sizeX, sizeY);
setMaxSize(sizeX, sizeY);
setPrefSize(sizeX, sizeY);
populate();
}
#Override
protected void layoutChildren() {
System.out.println("grid layout");
super.layoutChildren();
}
private void populate() {
ObservableList<Node> children = getChildren();
children.clear();
for (int i = 0; i < cols; i++) {
for (int j = 0; j < rows; j++) {
Rectangle r = new Rectangle(i * size, j * size, size, size);
r.setFill(null);
r.setStroke(Color.BLACK);
String label = i + "-" + j;
Point2D p = new Point2D(r.getBoundsInLocal().getCenterX(), r.getBoundsInLocal().getCenterY());
Text t = new Text(label);
t.setX(p.getX() - numMetrics.computeStringWidth(label) / 2);
t.setY(p.getY() + numMetrics.getLineHeight() / 2);
t.setFont(numFont);
children.add(r);
children.add(t);
}
}
}
}
class PanAndZoomPane extends Pane {
public static final double DEFAULT_DELTA = 1.5d; //1.3d
DoubleProperty myScale = new SimpleDoubleProperty(1.0);
public DoubleProperty deltaY = new SimpleDoubleProperty(0.0);
private Timeline timeline;
public PanAndZoomPane() {
this.timeline = new Timeline(30);//60
// add scale transform
scaleXProperty().bind(myScale);
scaleYProperty().bind(myScale);
}
public double getScale() {
return myScale.get();
}
public void setScale(double scale) {
myScale.set(scale);
}
public void setPivot(double x, double y, double scale) {
// note: pivot value must be untransformed, i. e. without scaling
// timeline that scales and moves the node
timeline.getKeyFrames().clear();
timeline.getKeyFrames().addAll(
new KeyFrame(Duration.millis(200), new KeyValue(translateXProperty(), getTranslateX() - x)), //200
new KeyFrame(Duration.millis(200), new KeyValue(translateYProperty(), getTranslateY() - y)), //200
new KeyFrame(Duration.millis(200), new KeyValue(myScale, scale)) //200
);
timeline.play();
}
public double getDeltaY() {
return deltaY.get();
}
public void setDeltaY(double dY) {
deltaY.set(dY);
}
}
/**
* Mouse drag context used for scene and nodes.
*/
class DragContext {
double mouseAnchorX;
double mouseAnchorY;
double translateAnchorX;
double translateAnchorY;
}
/**
* Listeners for making the scene's canvas draggable and zoomable
*/
public class SceneGestures {
private DragContext sceneDragContext = new DragContext();
PanAndZoomPane panAndZoomPane;
public SceneGestures(PanAndZoomPane canvas) {
this.panAndZoomPane = canvas;
}
public EventHandler<MouseEvent> getOnMousePressedEventHandler() {
return onMousePressedEventHandler;
}
public EventHandler<MouseEvent> getOnMouseDraggedEventHandler() {
return onMouseDraggedEventHandler;
}
public EventHandler<ScrollEvent> getOnScrollEventHandler() {
return onScrollEventHandler;
}
private EventHandler<MouseEvent> onMousePressedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
sceneDragContext.mouseAnchorX = event.getX();
sceneDragContext.mouseAnchorY = event.getY();
sceneDragContext.translateAnchorX = panAndZoomPane.getTranslateX();
sceneDragContext.translateAnchorY = panAndZoomPane.getTranslateY();
}
};
private EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
public void handle(MouseEvent event) {
panAndZoomPane.setTranslateX(sceneDragContext.translateAnchorX + event.getX() - sceneDragContext.mouseAnchorX);
panAndZoomPane.setTranslateY(sceneDragContext.translateAnchorY + event.getY() - sceneDragContext.mouseAnchorY);
event.consume();
}
};
/**
* Mouse wheel handler: zoom to pivot point
*/
private EventHandler<ScrollEvent> onScrollEventHandler = new EventHandler<ScrollEvent>() {
#Override
public void handle(ScrollEvent event) {
double delta = PanAndZoomPane.DEFAULT_DELTA;
double scale = panAndZoomPane.getScale(); // currently we only use Y, same value is used for X
double oldScale = scale;
panAndZoomPane.setDeltaY(event.getDeltaY());
if (panAndZoomPane.deltaY.get() < 0) {
scale /= delta;
} else {
scale *= delta;
}
double f = (scale / oldScale) - 1;
double dx = (event.getX() - (panAndZoomPane.getBoundsInParent().getWidth() / 2 + panAndZoomPane.getBoundsInParent().getMinX()));
double dy = (event.getY() - (panAndZoomPane.getBoundsInParent().getHeight() / 2 + panAndZoomPane.getBoundsInParent().getMinY()));
panAndZoomPane.setPivot(f * dx, f * dy, scale);
event.consume();
}
};
}
class FontMetrics {
final private Text internal;
public float lineHeight;
public FontMetrics(Font fnt) {
internal = new Text();
internal.setFont(fnt);
Bounds b = internal.getLayoutBounds();
lineHeight = (float) b.getHeight();
}
public float computeStringWidth(String txt) {
internal.setText(txt);
return (float) internal.getLayoutBounds().getWidth();
}
public float getLineHeight() {
return lineHeight;
}
}
}
Generally, you can get the bounds of node1 in the coordinate system of node2 if both are in the same scene using
node2.sceneToLocal(node1.localToScene(node1.getBoundsInLocal()));
I don't understand all the code you posted; I don't really know why you are using a scroll pane when you seem to be implementing all the panning and zooming yourself. Here is a simpler version of a PanZoomPane and then a test which shows how to use the idea above to get the bounds of the viewport in the coordinate system of the panning/zooming content. The "viewport" is just the bounds of the panning/zooming pane in the coordinate system of the content.
If you need the additional functionality in your version of panning and zooming, you should be able to adapt this idea to that; but it would take me too long to understand everything you are doing there.
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.layout.Region;
import javafx.scene.shape.Rectangle;
import javafx.scene.transform.Affine;
import javafx.scene.transform.Transform;
public class PanZoomPane extends Region {
private final Node content ;
private final Rectangle clip ;
private Affine transform ;
private Point2D mouseDown ;
private static final double SCALE = 1.01 ; // zoom factor per pixel scrolled
public PanZoomPane(Node content) {
this.content = content ;
getChildren().add(content);
clip = new Rectangle();
setClip(clip);
transform = Affine.affine(1, 0, 0, 1, 0, 0);
content.getTransforms().setAll(transform);
content.setOnMousePressed(event -> mouseDown = new Point2D(event.getX(), event.getY()));
content.setOnMouseDragged(event -> {
double deltaX = event.getX() - mouseDown.getX();
double deltaY = event.getY() - mouseDown.getY();
translate(deltaX, deltaY);
});
content.setOnScroll(event -> {
double pivotX = event.getX();
double pivotY = event.getY();
double scale = Math.pow(SCALE, event.getDeltaY());
scale(pivotX, pivotY, scale);
});
}
public Node getContent() {
return content ;
}
#Override
protected void layoutChildren() {
clip.setWidth(getWidth());
clip.setHeight(getHeight());
}
public void scale(double pivotX, double pivotY, double scale) {
transform.append(Transform.scale(scale, scale, pivotX, pivotY));
}
public void translate(double x, double y) {
transform.append(Transform.translate(x, y));
}
public void reset() {
transform.setToIdentity();
}
}
import javafx.application.Application;
import javafx.beans.binding.Binding;
import javafx.beans.binding.ObjectBinding;
import javafx.geometry.Bounds;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.RowConstraints;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class PanZoomTest extends Application {
private Binding<Bounds> viewport ;
#Override
public void start(Stage stage) {
Node content = createContent(50, 50, 50) ;
PanZoomPane pane = new PanZoomPane(content);
viewport = new ObjectBinding<>() {
{
bind(
pane.localToSceneTransformProperty(),
pane.boundsInLocalProperty(),
content.localToSceneTransformProperty()
);
}
#Override
protected Bounds computeValue() {
return content.sceneToLocal(pane.localToScene(pane.getBoundsInLocal()));
}
};
viewport.addListener((obs, oldViewport, newViewport) -> System.out.println(newViewport));
BorderPane root = new BorderPane(pane);
Button reset = new Button("Reset");
reset.setOnAction(event -> pane.reset());
HBox buttons = new HBox(reset);
buttons.setAlignment(Pos.CENTER);
buttons.setPadding(new Insets(10));
root.setTop(buttons);
Scene scene = new Scene(root, 800, 800);
stage.setScene(scene);
stage.show();
}
private Node createContent(int columns, int rows, double cellSize) {
GridPane grid = new GridPane() ;
ColumnConstraints cc = new ColumnConstraints();
cc.setMinWidth(cellSize);
cc.setPrefWidth(cellSize);
cc.setMaxWidth(cellSize);
cc.setFillWidth(true);
cc.setHalignment(HPos.CENTER);
for (int column = 0 ; column < columns ; column++) {
grid.getColumnConstraints().add(cc);
}
RowConstraints rc = new RowConstraints();
rc.setMinHeight(cellSize);
rc.setPrefHeight(cellSize);
rc.setMaxHeight(cellSize);
rc.setFillHeight(true);
rc.setValignment(VPos.CENTER);
for (int row = 0 ; row < rows ; row++) {
grid.getRowConstraints().add(rc);
}
for (int x = 0 ; x < columns ; x++) {
for (int y = 0 ; y < rows ; y++) {
Label label = new Label(String.format("[%d, %d]", x, y));
label.setBackground(new Background(
new BackgroundFill(Color.BLACK, CornerRadii.EMPTY, Insets.EMPTY),
new BackgroundFill(Color.WHITE, CornerRadii.EMPTY, new Insets(1,1,0,0))
));
label.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
grid.add(label, x, y);
}
}
return grid ;
}
public static void main(String[] args) {
launch();
}
}

Displaying gaps on charts in JavaFX

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

JavaFX 8 scale node for printing without changing displayed node

In JavaFX 8, I am printing a node (e.g., ScatterChart) with printerJob.printPage(). Without scaling, the printed node is cropped. If I scale for printing, then the printed node is correctly fit to the page, but the displayed node is scaled. A simple solution would be to make a copy/clone of the node, but it appears that isn't supported. Is there a better solution than scaling the node and then removing the scaling (which causes the displayed node to briefly rescale, which is unsightly)? It would seem that printing a graph would be a basic operation for JavaFX.
You can play with this app. It creates a PNG of the chart. It then prints the chart. I didn't scale the image. The actual image is located in your source folder. You can also open it using Paint and print from there. You can also code the printer settings so that the printer dialog shows up before printing.
import java.awt.Graphics;
import java.awt.print.PageFormat;
import java.awt.print.Printable;
import static java.awt.print.Printable.NO_SUCH_PAGE;
import static java.awt.print.Printable.PAGE_EXISTS;
import java.awt.print.PrinterException;
import java.awt.print.PrinterJob;
import java.io.File;
import java.io.IOException;
import javafx.application.Application;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.Scene;
import javafx.scene.SnapshotParameters;
import javafx.scene.chart.BarChart;
import javafx.scene.chart.CategoryAxis;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.control.Button;
import javafx.scene.image.Image;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javax.imageio.ImageIO;
/**
*
* #author blj0011
*/
public class JavaFXApplication145 extends Application
{
final static String itemA = "A";
final static String itemB = "B";
final static String itemC = "F";
#Override
public void start(Stage stage) {
final NumberAxis xAxis = new NumberAxis();
final CategoryAxis yAxis = new CategoryAxis();
final BarChart<Number, String> bc = new BarChart<Number, String>(xAxis, yAxis);
bc.setTitle("Summary");
xAxis.setLabel("Value");
xAxis.setTickLabelRotation(90);
yAxis.setLabel("Item");
XYChart.Series series1 = new XYChart.Series();
series1.setName("2003");
series1.getData().add(new XYChart.Data(2, itemA));
series1.getData().add(new XYChart.Data(20, itemB));
series1.getData().add(new XYChart.Data(10, itemC));
XYChart.Series series2 = new XYChart.Series();
series2.setName("2004");
series2.getData().add(new XYChart.Data(50, itemA));
series2.getData().add(new XYChart.Data(41, itemB));
series2.getData().add(new XYChart.Data(45, itemC));
XYChart.Series series3 = new XYChart.Series();
series3.setName("2005");
series3.getData().add(new XYChart.Data(45, itemA));
series3.getData().add(new XYChart.Data(44, itemB));
series3.getData().add(new XYChart.Data(18, itemC));
Button button = new Button("Print Chart");
button.setOnAction((event)->{printImage(saveAsPng(bc));});//Create the image and print it.
VBox vbox = new VBox();
vbox.getChildren().add(bc);
StackPane stackPane = new StackPane();
stackPane.getChildren().add(button);
vbox.getChildren().add(stackPane);
Scene scene = new Scene(vbox, 800, 600);
bc.getData().addAll(series1, series2, series3);
stage.setScene(scene);
stage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args)
{
launch(args);
}
public File saveAsPng(BarChart barChart) {
WritableImage image = barChart.snapshot(new SnapshotParameters(), null);
// TODO: probably use a file chooser here
File file = new File("chart.png");
try {
ImageIO.write(SwingFXUtils.fromFXImage(image, null), "png", file);
} catch (IOException e) {
// TODO: handle exception here
}
return file;
}
private void printImage(File file) {
Image image = new Image(file.toURI().toString());
java.awt.image.BufferedImage bufferedImage = SwingFXUtils.fromFXImage(image, null);
PrinterJob printJob = PrinterJob.getPrinterJob();
printJob.setPrintable(new Printable() {
#Override
public int print(Graphics graphics, PageFormat pageFormat, int pageIndex) throws PrinterException {
// Get the upper left corner that it printable
int x = (int) Math.ceil(pageFormat.getImageableX());
int y = (int) Math.ceil(pageFormat.getImageableY());
if (pageIndex != 0) {
return NO_SUCH_PAGE;
}
graphics.drawImage(bufferedImage, x, y, bufferedImage.getWidth(), bufferedImage.getHeight(), null);
return PAGE_EXISTS;
}
});
try {
printJob.print();
} catch (PrinterException e1) {
e1.printStackTrace();
}
}
}

Gantt chart from scratch

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);
}

Resources