I have a JavaFX BubbleChart in my visualization and I need to be able to create/display text within each bubble of the chart. In my visualization, I have numerous XYChart.Series with only 1 bubble per series. For each series, I do "series.setName("xxxx");" (where xxxx = unique series name) and I need to be able to display that series name inside the bubble.
I already have implemented a Tooltip (mouse-over event) for the Bubble Chart that displays the series name, but I need to also have the text visible inside the bubble without requiring a mouse-over.
For the sake of having code to work against, here is a basic example with 5 series. How would I go about adding a text inside each Bubble?
Thank you.
public class bubbleChartTest extends Application {
#Override
public void start(Stage stage) {
final NumberAxis xAxis = new NumberAxis(0, 10, 1);
final NumberAxis yAxis = new NumberAxis(0, 10, 1);
final BubbleChart<Number, Number> bc = new BubbleChart<Number, Number>(xAxis, yAxis);
xAxis.setLabel("X Axis");
xAxis.setMinorTickCount(2);
yAxis.setLabel("Y Axis");
yAxis.setTickLabelGap(2);
bc.setTitle("Bubble Chart StackOverflow Example");
XYChart.Series<Number, Number> series1 = new XYChart.Series<Number, Number>();
series1.setName("Series 1");
series1.getData().add(new XYChart.Data<Number, Number>(3, 7, 1.5));
XYChart.Series<Number, Number> series2 = new XYChart.Series<Number, Number>();
series2.setName("Series 2");
series2.getData().add(new XYChart.Data<Number, Number>(8, 3, 1));
XYChart.Series<Number, Number> series3 = new XYChart.Series<Number, Number>();
series3.setName("Series 3");
series3.getData().add(new XYChart.Data<Number, Number>(1, 9, 2));
XYChart.Series<Number, Number> series4 = new XYChart.Series<Number, Number>();
series4.setName("Series 4");
series4.getData().add(new XYChart.Data<Number, Number>(4, 1, 0.5));
XYChart.Series<Number, Number> series5 = new XYChart.Series<Number, Number>();
series5.setName("Series 5");
series5.getData().add(new XYChart.Data<Number, Number>(9, 9, 3));
Scene scene = new Scene(bc);
bc.getData().addAll(series1, series2, series3, series4, series5);
stage.setScene(scene);
stage.show();
for(XYChart.Series<Number, Number> series : bc.getData()) {
for(XYChart.Data<Number, Number> data : series.getData()) {
Tooltip.install(data.getNode(), new Tooltip(series.getName()));
}
}
}
public static void main(String[] args) {
launch(args);
}
}
The node you get from the data is a Stackpane, and the Stackpane is shaped as an Ellipse. You probably need the radius of that Ellipse in x orientation and add a label to the Stackpane. But you need to set the minWidth Property of the Label, otherwise it will only display the three dots. And you need a Property to hold a dynamic font size, because it should look pretty if you want to resize the chart.
You do not need much code to get this work:
for (XYChart.Series<Number, Number> series : bc.getData()) {
for (XYChart.Data<Number, Number> data : series.getData()) {
Node bubble = data.getNode();
if (bubble != null && bubble instanceof StackPane) {
StackPane region = (StackPane) bubble;
if (region.getShape() != null && region.getShape() instanceof Ellipse) {
Ellipse ellipse = (Ellipse) region.getShape();
DoubleProperty fontSize = new SimpleDoubleProperty(10);
Label label = new Label(series.getName());
label.setAlignment(Pos.CENTER);
label.minWidthProperty().bind(ellipse.radiusXProperty());
//fontSize.bind(Bindings.when(ellipse.radiusXProperty().lessThan(40)).then(6).otherwise(10));
fontSize.bind(Bindings.divide(ellipse.radiusXProperty(), 5));
label.styleProperty().bind(Bindings.concat("-fx-font-size:", fontSize.asString(), ";"));
region.getChildren().add(label);
}
}
}
}
Update
James_D mentioned that the loop isn't much robust in case of changing the, for ex., Shape. So I've changed it a bit to ask for the ellipse instance. This is a bit like the original layoutPlotChildren method from BubbleChart.
Related
I am completing a project in which I have implemented a brute force algorithm for solving the Convex Hull problem. I need to also create a visual for this algorithm. I am trying to create a coordinate plane ranging from (-100, 100) on both the x and y axis, plot all the points in the complete set, and dynamically draw lines between the points in order to create the convex hull. For example, assume I have 4 points: A(1, 1), B(3,2), C(2, 4), and D(1,3). I want to first plot all four of these points and then draw a line from A to B and then from B to C and then from C to D and then finally from D to A. So far I have been trying to do this using JavaFX LineChart.
// creating axis
NumberAxis xAxis = new NumberAxis(-100, 100, 1);
NumberAxis yAxis = new NumberAxis(-100, 100, 1);
// creating chart
LineChart<Number, Number> lineChart = new LineChart<>(xAxis, yAxis);
and then drew lines using
public static void populatePoints(List<Double []> points, LineChart lineChart) {
XYChart.Series series = new XYChart.Series();
series.setName("Points of set");
for (Double[] point : points) {
series.getData().add(new XYChart.Data<Double, Double>(point[0], point[1]));
}
lineChart.getData().add(series);
}
However, this just draws the lines in a typical line chart fashion. I need it to be in a convex shape. I have attached the result of my current code with points (-51.6, 72.97), (33.58, 98.97), (-86.68, 9.77), and (-49.41, -46.26).
You can use the approach shown here:
Add the points to the series in the desired order.
Specify SortingPolicy.NONE.
series.getData().add(new XYChart.Data(-86.68, 9.77));
series.getData().add(new XYChart.Data(-51.6, 72.97));
series.getData().add(new XYChart.Data( 33.58, 98.97));
series.getData().add(new XYChart.Data(-49.41, -46.26));
series.getData().add(new XYChart.Data(-86.68, 9.77));
…
policy.getSelectionModel().select(LineChart.SortingPolicy.NONE);
I am trying to force the child of a GridPane to fit the size of the cell it is in. In this case, I am creating a month view calendar and the columns and rows are a set size regardless of what is in it.
On this calendar, each cell contains a normal VBox and each VBox contains a label that displays the day of the month and each of the events for that day. Many of the days have no events; some of them have one event; and a few have more than one event.
The calendar size is dependent on the window size and will grow and shrink in accordance to the window. Right now, if the cell is too small to fit all of the events, then the height of that one VBox for that day in the cell becomes larger than the cell.
The header row has the following constraint:
HEADER_CONSTRAINT = new RowConstraints(10, -1, 500,
Priority.NEVER, VPos.BASELINE, true);
and the other rows have this constraint:
ROW_CONSTRAINT = new RowConstraints(30, 30, Integer.MAX_VALUE,
Priority.ALWAYS, VPos.TOP, true);
What I think I need to do is:
grid.add(cell, c, r);
vbox.maxHeightProperty().bind(grid.getRow(r).heightProperty()); // <-- this line is not right, but something like this.
As #fabian mentioned correctly, the size of a VBox is entirely dependent the sizes of its children. If you wish to have a container that does not resize depending on its children, you can use a Pane instead. Like this:
public void start(Stage primaryStage)
{
GridPane root = new GridPane();
Scene scene = new Scene(root, 300, 300);
int c = 0, r = 0;
Pane c0 = new Pane();
c0.setPrefSize(200, 200);
c0.setBorder(new Border(new BorderStroke(Color.RED, BorderStrokeStyle.SOLID, CornerRadii.EMPTY,
new BorderWidths(1))));
root.add(c0, c, r);
primaryStage.setScene(scene);
primaryStage.show();
}
Add a shape to test:
Pane c0 = new Pane();
c0.setPrefSize(200, 200);
c0.setBorder(new Border(new BorderStroke(Color.RED, BorderStrokeStyle.SOLID, CornerRadii.EMPTY,
new BorderWidths(1))));
{
Circle circle = new Circle(160, Color.BLUE);
circle.setCenterX(160);
circle.setCenterY(160);
c0.getChildren().add(circle);
}
root.add(c0, c, r);
Note that although the shape protrudes outside of the bounds of c0, the borders of c0 stay in place, indicating that its size is unaffected. To prevent the content from protruding out, you need to add a clip:
c0.setClip(new Rectangle(c0.getPrefWidth(), c0.getPrefHeight()));
If you want a more fancy clip instead of a simple trim, such as fade-in, fade-out, and shadows, you can read this very good tutorial here.
Now just replace this Circle with your VBox, so that the VBox is a child of this Pane, and add the Pane to your GridPane and you're done. I will provide my final code here as a reference:
public void start(Stage primaryStage)
{
GridPane root = new GridPane();
Scene scene = new Scene(root, 300, 300);
int c = 0, r = 0;
Pane c0 = new Pane();
c0.setPrefSize(200, 200);
c0.setClip(new Rectangle(c0.getPrefWidth(), c0.getPrefHeight()));
c0.setBorder(new Border(new BorderStroke(Color.RED, BorderStrokeStyle.SOLID, CornerRadii.EMPTY,
new BorderWidths(1))));
{
Circle circle = new Circle(160, Color.BLUE);
circle.setCenterX(160);
circle.setCenterY(160);
c0.getChildren().add(circle);
}
root.add(c0, c, r);
primaryStage.setScene(scene);
primaryStage.show();
}
Update:
#Jai pointed out that the size is static in this implementation. If you wish to dynamically adjust the size of the cells if the size of the GridPane changes, you can add a listener to its widthProperty and heightProperty like this:
int rowCount = 5, columnCount = 7; // You should know these values.
ChangeListener<Number> updater = (o, oV, nV) ->
{
double newWidth = root.getWidth() / columnCount;
double newHeight = root.getHeight() / rowCount;
root.getChildren().forEach(n ->
{
// Assuming every child is a Pane.
Pane p = (Pane) n;
p.setPrefSize(newWidth, newHeight);
p.setClip(new Rectangle(newWidth, newHeight));
});
};
root.widthProperty().addListener(updater);
root.heightProperty().addListener(updater);
Alternatively...
If you wish to use ColumnConstraints and RowConstraints to determine the cell size you can set the Panes to expand, and only update their clips in the listener, which now listens to ColumnConstraints and RowConstraints:
ColumnConstraints cc = new ColumnConstraints(200);
RowConstraints rc = new RowConstraints(200);
c0.setPrefSize(Double.MAX_VALUE, Double.MAX_VALUE);
c0.setClip(new Rectangle(cc.getPrefWidth(), rc.getPrefHeight()));
and...
ChangeListener<Number> updater = (o, oV, nV) ->
{
root.getChildren().forEach(n ->
{
// Assuming every child is a Pane.
Pane p = (Pane) n;
p.setClip(new Rectangle(cc.getPrefWidth(), rc.getPrefHeight()));
});
};
cc.prefWidthProperty().addListener(updater);
rc.prefHeightProperty().addListener(updater);
This is something that I have been searching for for a while while working on a project. The best I had been able to find was how to change all of the bars or independently based on their specific value. Neither felt like a proper solutions. You might want to change individual bars and the amounts of each may change.
The solution I found ended up being fairly simple, but I did not find very good documentation on it and wanted to post it for future use.
NOTE: All of these bars are in the same series. A similar fix could probably be applied to bar charts using multiple series, but that caused a spacing issue for me when each category only needed one bar.
The following code will create a bar chart that with 4 categories to track and adds them to the chart.
public class BarChartExample extends Application {
final static String project = "Project - 20%";
final static String quiz = "Quiz - 10%";
final static String midterm = "Midterm - 30%";
final static String finalexam = "Final - 40%";
#Override
public void start(Stage primaryStage) throws Exception{
primaryStage.setTitle("Change Bar Color Example");
final CategoryAxis xAxis = new CategoryAxis();
final NumberAxis yAxis = new NumberAxis();
final BarChart<String,Number> barChart = new BarChart<String,Number>(xAxis,yAxis);
xAxis.setLabel("Assignment Type");
yAxis.setLabel("Percentage");
XYChart.Series series = new XYChart.Series();
series.getData().add(new XYChart.Data(project, 20));
series.getData().add(new XYChart.Data(quiz, 10));
series.getData().add(new XYChart.Data(midterm, 30));
series.getData().add(new XYChart.Data(finalexam, 40));
barChart.getData().add(series);
This next bit is the part relevant to this post. This is where the color change happens. The bars are considered Nodes, so you can set each bar equal to a node variable and use CSS to change it's style.
Node n = barChart.lookup(".data0.chart-bar");
n.setStyle("-fx-bar-fill: red");
n = barChart.lookup(".data1.chart-bar");
n.setStyle("-fx-bar-fill: blue");
n = barChart.lookup(".data2.chart-bar");
n.setStyle("-fx-bar-fill: green");
n = barChart.lookup(".data3.chart-bar");
n.setStyle("-fx-bar-fill: orange");
The rest is just getting rid of the legend (since in this case it's unnecessary) and filling out the remaining necessary code to make it run.
barChart.setLegendVisible(false);
VBox vbox = new VBox(barChart);
Scene scene = new Scene(vbox);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
Application.launch(args);
}
}
I hope this helps anyone looking into trying to set bars with specific colors.
I am using javafx
I wont to Connect a line to 2 circle the connection must be to the surface of the circle not the center
And when the circle move the line connect to the best point in the circle.
when connecting, the line and the circle must not overlap
Thank you
Edit
The solution i am looking for require that
the line intersect with the circle so the tangent is not the solution
if the line virtually continue it must intersect with the center of the circle
Assuming the position is only modified using the centerX and centerY properties, this can be done by simply adding listeners to those properties.
The ends of the line can be determined by using the center of a circle translated a distance equal to the radius in direction of the other center.
Note that this doesn't take the stroke widths into account:
public static Point2D getDirection(Circle c1, Circle c2) {
return new Point2D(c2.getCenterX() - c1.getCenterX(), c2.getCenterY() - c1.getCenterY()).normalize();
}
public static void connect(Circle c1, Circle c2, Line line) {
InvalidationListener startInvalidated = observable -> {
Point2D dir = getDirection(c1, c2);
Point2D diff = dir.multiply(c1.getRadius());
line.setStartX(c1.getCenterX() + diff.getX());
line.setStartY(c1.getCenterY() + diff.getY());
};
InvalidationListener endInvalidated = observable -> {
Point2D dir = getDirection(c2, c1);
Point2D diff = dir.multiply(c2.getRadius());
line.setEndX(c2.getCenterX() + diff.getX());
line.setEndY(c2.getCenterY() + diff.getY());
};
c1.centerXProperty().addListener(startInvalidated);
c1.centerYProperty().addListener(startInvalidated);
c1.radiusProperty().addListener(startInvalidated);
startInvalidated.invalidated(null);
c2.centerXProperty().addListener(endInvalidated);
c2.centerYProperty().addListener(endInvalidated);
c2.radiusProperty().addListener(endInvalidated);
endInvalidated.invalidated(null);
}
#Override
public void start(Stage primaryStage) {
Circle c1 = new Circle(100, 100, 50, null);
c1.setStroke(Color.BLUE);
Circle c2 = new Circle(200, 200, 50, null);
c2.setStroke(Color.RED);
Line line = new Line();
connect(c1, c2, line);
Pane pane = new Pane(line, c1, c2);
// demonstrate update during movement
Timeline timeline = new Timeline(
new KeyFrame(Duration.ZERO, new KeyValue(c1.centerXProperty(), 100)),
new KeyFrame(Duration.ZERO, new KeyValue(c1.centerYProperty(), 100)),
new KeyFrame(Duration.seconds(1), new KeyValue(c1.centerXProperty(), 300)),
new KeyFrame(Duration.seconds(1), new KeyValue(c1.centerYProperty(), 50)),
new KeyFrame(Duration.ZERO, new KeyValue(c2.centerXProperty(), 200)),
new KeyFrame(Duration.ZERO, new KeyValue(c2.centerYProperty(), 200)),
new KeyFrame(Duration.seconds(1), new KeyValue(c2.centerXProperty(), 100)),
new KeyFrame(Duration.seconds(1), new KeyValue(c2.centerYProperty(), 100))
);
timeline.setCycleCount(Animation.INDEFINITE);
timeline.setAutoReverse(true);
timeline.play();
Scene scene = new Scene(pane, 500, 500);
primaryStage.setScene(scene);
primaryStage.show();
}
If you use larger stroke widths, you may need to take them into account...
Note that the whole problem becomes a lot easier, if the fill of the circles with fully opaque paint and don't modify the blend effects, since the circles after the line will result in the circles being drawn on top of the line in that case, which means connecting the centers would suffice.
I am trying to build a series chart using JavaFX, where data is inserted dynamically.
Each time that a new value is inserted I would like to check if this is the highest value so far, and if so, I want to draw an horizontal line to show that this is the maximum value.
In JFree chart I would have used a ValueMarker, but I am trying to do the same with JavaFX.
I tried using the Line object, but it is definitely not the same, because I cannot provide the Chart values, it takes the relative pixel positions in the windows.
Here is the screenshot of chart I want to achieve:
http://postimg.org/image/s5fkupsuz/
Any suggestions?
Thank you.
To convert chart values to pixels you can use method NumberAxis#getDisplayPosition() which return actual coordinates of the chart nodes.
Although these coordinates are relative to chart area, which you can find out by next code:
Node chartArea = chart.lookup(".chart-plot-background");
Bounds chartAreaBounds = chartArea.localToScene(chartArea.getBoundsInLocal());
Note localToScene() method which allows you to convert any coordinates to Scene ones. Thus you can use them to update your value marker coordinates. Make sure you make localToScene call after your Scene have been shown.
See sample program below which produces next chart:
public class LineChartValueMarker extends Application {
private Line valueMarker = new Line();
private XYChart.Series<Number, Number> series = new XYChart.Series<>();
private NumberAxis yAxis;
private double yShift;
private void updateMarker() {
// find maximal y value
double max = 0;
for (Data<Number, Number> value : series.getData()) {
double y = value.getYValue().doubleValue();
if (y > max) {
max = y;
}
}
// find pixel position of that value
double displayPosition = yAxis.getDisplayPosition(max);
// update marker
valueMarker.setStartY(yShift + displayPosition);
valueMarker.setEndY(yShift + displayPosition);
}
#Override
public void start(Stage stage) {
LineChart<Number, Number> chart = new LineChart<>(new NumberAxis(0, 100, 10), yAxis = new NumberAxis(0, 100, 10));
series.getData().add(new XYChart.Data(0, 0));
series.getData().add(new XYChart.Data(10, 20));
chart.getData().addAll(series);
Pane pane = new Pane();
pane.getChildren().addAll(chart, valueMarker);
Scene scene = new Scene(pane);
// add new value on mouseclick for testing
chart.setOnMouseClicked(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent t) {
series.getData().add(new XYChart.Data(series.getData().size() * 10, 30 + 50 * new Random().nextDouble()));
updateMarker();
}
});
stage.setScene(scene);
stage.show();
// find chart area Node
Node chartArea = chart.lookup(".chart-plot-background");
Bounds chartAreaBounds = chartArea.localToScene(chartArea.getBoundsInLocal());
// remember scene position of chart area
yShift = chartAreaBounds.getMinY();
// set x parameters of the valueMarker to chart area bounds
valueMarker.setStartX(chartAreaBounds.getMinX());
valueMarker.setEndX(chartAreaBounds.getMaxX());
updateMarker();
}
public static void main(String[] args) {
launch();
}
}