How to add a value marker to JavaFX chart? - java

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

Related

Rectangle size binding to Scene size - JavaFX

I'm trying to write a simple JavaFX application that contains a graphical representation of an NxM matrix (default 100 x 100). I would like to bind the matrix size to the Scene size, so when I resize the app window, the matrix follows it and keeps the aspect ratio. The binding works seamlessly for small matrices (for example 10x10), but when the matrix gets bigger (for example 50x50) and rectangles get smaller so they can fit my screen, the binding process becomes discontinuous. The matrix sometimes even gets bigger than window size (can't see all cells/rectangles) and matrix size seems like it's switching between integer values.
What I did is created a StackPane as a root Node inside the Scene and a GridPane as a child Node to the StackPane. I filled the GridPane with an NxM Rectangle matrix.
The code follows:
public class GridPaneExample extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
int n = 100, m = 100; // matrix size
double rectw = 5, recth = 5; // size of each rectangle
StackPane root = new StackPane();
Scene scene = new Scene(root, (m + 2) * rectw, (n + 2) * recth, Color.DARKOLIVEGREEN); // creating a scene with
// a frame around matrix
primaryStage.setScene(scene);
primaryStage.sizeToScene();
GridPane gp = new GridPane();
root.getChildren().add(gp);
Rectangle[][] rects = new Rectangle[n][m];
for (int i = 0; i < n; ++i)
for (int j = 0; j < m; ++j) {
rects[i][j] = new Rectangle();
rects[i][j].setWidth(rectw);
rects[i][j].setHeight(recth);
rects[i][j].setFill(Color.ANTIQUEWHITE);
rects[i][j].setStrokeType(StrokeType.INSIDE);
rects[i][j].setStrokeWidth(0.2);
rects[i][j].setStroke(Color.GREY);
rects[i][j].widthProperty().bind(gp.widthProperty().divide(m));
rects[i][j].heightProperty().bind(gp.heightProperty().divide(n));
GridPane.setRowIndex(rects[i][j], i);
GridPane.setColumnIndex(rects[i][j], j);
gp.getChildren().add(rects[i][j]);
}
rects[0][0].setFill(Color.RED);
rects[0][m - 1].setFill(Color.RED);
rects[n - 1][0].setFill(Color.RED);
rects[n - 1][m - 1].setFill(Color.RED);
StackPane.setAlignment(gp, Pos.TOP_CENTER);
gp.minHeightProperty().bind(scene.heightProperty().subtract(2 * recth));
gp.minWidthProperty().bind(scene.widthProperty().subtract(2 * rectw));
gp.maxHeightProperty().bind(scene.heightProperty().subtract(2 * recth));
gp.maxWidthProperty().bind(scene.widthProperty().subtract(2 * rectw));
root.setLayoutY(recth);
primaryStage.show();
primaryStage.setMinHeight(primaryStage.getHeight());
primaryStage.setMinWidth(primaryStage.getWidth());
}
}
Here are some screenshots of the program showing different sized matrices.
A 10 x 10 matrix app when started (rectangle size is 20x20):
https://i.imgur.com/I2vMLSD.png
A 10 x 10 matrix app when stretched:
https://i.imgur.com/9rR3e0g.png
A 100 x 100 matrix app when started (rectangle size is 5x5):
https://i.imgur.com/1KCKg6W.png
A 100 x 100 matrix app when stretched a bit, see how the matrix gets out of bounds:
https://i.imgur.com/pG8DxXA.png
So, is there a way to make this binding experience smooth and consistent, maybe by using something else other than Rectangles?
Edit: A weird thing happens. When I remove the rectangle binding lines from code and launch the app with 100x100 matrix, the matrix looks as it should. The program prints out the size of the GridPane and red rectangles. They are 500x500 and 5x5 respectively, as expected. However, when I include rectangle binding (as shown in the code), the matrix gets out of bounds, as in third photo attached, but the program still prints out the same dimensions of GridPane and rectangles.
As commented earlier, this problem is fixable by calling Node#setSnapToPixel(false) on the GridPane.
Explanation
This problem occurs when Nodes become so small that a single pixel becomes too big for accurate display.
JavaFX uses a 'pixel snapping' feature by default to ensure a 'clear/clean' look. This would be mainly vissible with borders getting 'fussy/blurry' when not used.
In the backend JavaFX has defined the methods snapSpace, snapSize and snapPosition, which correspond respectively to Math.round, Math.ceil and Math.round.
This means that if a Node has a width of 2.5 with a 1 pixel border, it should have the size of (1 + 2.5 + 1) = 4.5, wich ceiled wil result in 5.0. If you have 100 elements getting a 0.5 width bonus it will result in 50 pixels overflow!
Setting 'snapToPixel' to false means it wil ignore the mechanism above, and use mixed colors per pixel. If you would look closely this 'blurr' effect is noticable on borders. But it is worth the tradeoff for the perfect layout IMO.
Here is a sample app I wrote a long time ago which does a similar kind of thing. It uses resizable nodes within a GridPane with a listener on the layoutBoundsProperty to pick the size of the resizable nodes. Not sure if it is really what you are interested in (the approach you have in your question, with some fixes or tweaks, is probably fine for what you wish to accomplish), but it might be worth taking a look at for an alternate approach if needed.
Relevant code is for the swatch node in the ColorChooser constructor from the sample code below (sorry it's a bit long, it was written for a different purpose but seems close enough that it might be worth posting in its entirety):
import javafx.application.Application;
import javafx.beans.property.*;
import javafx.event.*;
import javafx.geometry.*;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
/**
* Sample application for using the color chooser
*/
public class ColorChooserSample extends Application {
public void start(final Stage stage) throws Exception {
// initialize the stage.
stage.setTitle("Color Chooser");
stage.initStyle(StageStyle.UTILITY);
// create a new color chooser sized to the stage.
final String[][] smallPalette = {
{"aliceblue", "#f0f8ff"}, {"antiquewhite", "#faebd7"}, {"aqua", "#00ffff"}, {"aquamarine", "#7fffd4"},
{"azure", "#f0ffff"}, {"beige", "#f5f5dc"}, {"bisque", "#ffe4c4"}, {"black", "#000000"},
{"blanchedalmond", "#ffebcd"}, {"blue", "#0000ff"}, {"blueviolet", "#8a2be2"}, {"brown", "#a52a2a"},
{"burlywood", "#deb887"}, {"cadetblue", "#5f9ea0"}, {"chartreuse", "#7fff00"}, {"chocolate", "#d2691e"},
{"coral", "#ff7f50"}, {"cornflowerblue", "#6495ed"}, {"cornsilk", "#fff8dc"}, {"crimson", "#dc143c"},
{"cyan", "#00ffff"}, {"darkblue", "#00008b"}, {"darkcyan", "#008b8b"}, {"darkgoldenrod", "#b8860b"},
};
final ColorChooser colorChooser = new ColorChooser(smallPalette);
// to use the full web palette, just use the default constructor.
// final ColorChooser colorChooser = new ColorChooser();
final Scene scene = new Scene(colorChooser, 600, 500);
// show the stage.
stage.setScene(scene);
stage.show();
// monitor the color chooser's chosen color and respond to it.
colorChooser.chosenColorProperty().addListener((observableValue, oldColor, newColor) ->
System.out.println("Chose: " + colorChooser.getChosenColorName() + " " + colorChooser.getChosenColor())
);
}
public static void main(String[] args) {
launch(args);
}
}
/**
* A Color Chooser Component - allows the user to select a color from a palette.
*/
class ColorChooser extends VBox {
private final double GOLDEN_RATIO = 1.618;
private final double MIN_TILE_SIZE = 5;
private final double nColumns;
private final double nRows;
/**
* The color the user has selected or the default initial color (the first color in the palette)
*/
private final ReadOnlyObjectWrapper<Color> chosenColor = new ReadOnlyObjectWrapper<Color>();
public Color getChosenColor() {
return chosenColor.get();
}
public ReadOnlyObjectProperty<Color> chosenColorProperty() {
return chosenColor.getReadOnlyProperty();
}
/**
* Friendly name for the chosen color
*/
private final ReadOnlyObjectWrapper<String> chosenColorName = new ReadOnlyObjectWrapper<String>();
public String getChosenColorName() {
return chosenColorName.get();
}
/**
* Preferred size for a web palette tile
*/
private DoubleProperty prefTileSize = new SimpleDoubleProperty(MIN_TILE_SIZE);
/**
* A palette of colors from http://docs.oracle.com/javafx/2.0/api/javafx/scene/doc-files/cssref.html#typecolor
*/
private static final String[][] webPalette = {
{"aliceblue", "#f0f8ff"}, {"antiquewhite", "#faebd7"}, {"aqua", "#00ffff"}, {"aquamarine", "#7fffd4"},
{"azure", "#f0ffff"}, {"beige", "#f5f5dc"}, {"bisque", "#ffe4c4"}, {"black", "#000000"},
{"blanchedalmond", "#ffebcd"}, {"blue", "#0000ff"}, {"blueviolet", "#8a2be2"}, {"brown", "#a52a2a"},
{"burlywood", "#deb887"}, {"cadetblue", "#5f9ea0"}, {"chartreuse", "#7fff00"}, {"chocolate", "#d2691e"},
{"coral", "#ff7f50"}, {"cornflowerblue", "#6495ed"}, {"cornsilk", "#fff8dc"}, {"crimson", "#dc143c"},
{"cyan", "#00ffff"}, {"darkblue", "#00008b"}, {"darkcyan", "#008b8b"}, {"darkgoldenrod", "#b8860b"},
{"darkgray", "#a9a9a9"}, {"darkgreen", "#006400"}, {"darkgrey", "#a9a9a9"}, {"darkkhaki", "#bdb76b"},
{"darkmagenta", "#8b008b"}, {"darkolivegreen", "#556b2f"}, {"darkorange", "#ff8c00"}, {"darkorchid", "#9932cc"},
{"darkred", "#8b0000"}, {"darksalmon", "#e9967a"}, {"darkseagreen", "#8fbc8f"}, {"darkslateblue", "#483d8b"},
{"darkslategray", "#2f4f4f"}, {"darkslategrey", "#2f4f4f"}, {"darkturquoise", "#00ced1"}, {"darkviolet", "#9400d3"},
{"deeppink", "#ff1493"}, {"deepskyblue", "#00bfff"}, {"dimgray", "#696969"}, {"dimgrey", "#696969"},
{"dodgerblue", "#1e90ff"}, {"firebrick", "#b22222"}, {"floralwhite", "#fffaf0"}, {"forestgreen", "#228b22"},
{"fuchsia", "#ff00ff"}, {"gainsboro", "#dcdcdc"}, {"ghostwhite", "#f8f8ff"}, {"gold", "#ffd700"},
{"goldenrod", "#daa520"}, {"gray", "#808080"}, {"green", "#008000"}, {"greenyellow", "#adff2f"},
{"grey", "#808080"}, {"honeydew", "#f0fff0"}, {"hotpink", "#ff69b4"}, {"indianred", "#cd5c5c"},
{"indigo", "#4b0082"}, {"ivory", "#fffff0"}, {"khaki", "#f0e68c"}, {"lavender", "#e6e6fa"},
{"lavenderblush", "#fff0f5"}, {"lawngreen", "#7cfc00"}, {"lemonchiffon", "#fffacd"}, {"lightblue", "#add8e6"},
{"lightcoral", "#f08080"}, {"lightcyan", "#e0ffff"}, {"lightgoldenrodyellow", "#fafad2"}, {"lightgray", "#d3d3d3"},
{"lightgreen", "#90ee90"}, {"lightgrey", "#d3d3d3"}, {"lightpink", "#ffb6c1"}, {"lightsalmon", "#ffa07a"},
{"lightseagreen", "#20b2aa"}, {"lightskyblue", "#87cefa"}, {"lightslategray", "#778899"}, {"lightslategrey", "#778899"},
{"lightsteelblue", "#b0c4de"}, {"lightyellow", "#ffffe0"}, {"lime", "#00ff00"}, {"limegreen", "#32cd32"},
{"linen", "#faf0e6"}, {"magenta", "#ff00ff"}, {"maroon", "#800000"}, {"mediumaquamarine", "#66cdaa"},
{"mediumblue", "#0000cd"}, {"mediumorchid", "#ba55d3"}, {"mediumpurple", "#9370db"}, {"mediumseagreen", "#3cb371"},
{"mediumslateblue", "#7b68ee"}, {"mediumspringgreen", "#00fa9a"}, {"mediumturquoise", "#48d1cc"}, {"mediumvioletred", "#c71585"},
{"midnightblue", "#191970"}, {"mintcream", "#f5fffa"}, {"mistyrose", "#ffe4e1"}, {"moccasin", "#ffe4b5"},
{"navajowhite", "#ffdead"}, {"navy", "#000080"}, {"oldlace", "#fdf5e6"}, {"olive", "#808000"},
{"olivedrab", "#6b8e23"}, {"orange", "#ffa500"}, {"orangered", "#ff4500"}, {"orchid", "#da70d6"},
{"palegoldenrod", "#eee8aa"}, {"palegreen", "#98fb98"}, {"paleturquoise", "#afeeee"}, {"palevioletred", "#db7093"},
{"papayawhip", "#ffefd5"}, {"peachpuff", "#ffdab9"}, {"peru", "#cd853f"}, {"pink", "#ffc0cb"},
{"plum", "#dda0dd"}, {"powderblue", "#b0e0e6"}, {"purple", "#800080"}, {"red", "#ff0000"},
{"rosybrown", "#bc8f8f"}, {"royalblue", "#4169e1"}, {"saddlebrown", "#8b4513"}, {"salmon", "#fa8072"},
{"sandybrown", "#f4a460"}, {"seagreen", "#2e8b57"}, {"seashell", "#fff5ee"}, {"sienna", "#a0522d"},
{"silver", "#c0c0c0"}, {"skyblue", "#87ceeb"}, {"slateblue", "#6a5acd"}, {"slategray", "#708090"},
{"slategrey", "#708090"}, {"snow", "#fffafa"}, {"springgreen", "#00ff7f"}, {"steelblue", "#4682b4"},
{"tan", "#d2b48c"}, {"teal", "#008080"}, {"thistle", "#d8bfd8"}, {"tomato", "#ff6347"},
{"turquoise", "#40e0d0"}, {"violet", "#ee82ee"}, {"wheat", "#f5deb3"}, {"white", "#ffffff"},
{"whitesmoke", "#f5f5f5"}, {"yellow", "#ffff00"}, {"yellowgreen", "#9acd32"}
};
public ColorChooser() {
this(webPalette);
}
public ColorChooser(String[][] colors) {
super();
// create a pane for showing info on the chosen color.
final HBox colorInfo = new HBox();
final Label selectedColorName = new Label();
HBox.setMargin(selectedColorName, new Insets(2, 0, 2, 10));
colorInfo.getChildren().addAll(selectedColorName);
chosenColorName.addListener((observableValue, oldName, newName) -> {
if (newName != null) {
colorInfo.setStyle("-fx-background-color: " + newName + ";");
selectedColorName.setText(newName);
chosenColor.set(Color.web(newName));
}
});
// create a color swatch.
final GridPane swatch = new GridPane();
swatch.setSnapToPixel(false);
// calculate the number of columns and rows based on the number of colors and a golden ratio for layout.
nColumns = Math.floor(Math.sqrt(colors.length) * 2 / GOLDEN_RATIO);
nRows = Math.ceil(colors.length / nColumns);
// create a bunch of button controls for color selection.
int i = 0;
for (String[] namedColor : colors) {
final String colorName = namedColor[0];
final String colorHex = namedColor[1];
// create a button for choosing a color.
final Button colorChoice = new Button();
colorChoice.setUserData(colorName);
// position the button in the grid.
GridPane.setRowIndex(colorChoice, i / (int) nColumns);
GridPane.setColumnIndex(colorChoice, i % (int) nColumns);
colorChoice.setMinSize(MIN_TILE_SIZE, MIN_TILE_SIZE);
colorChoice.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
// add a mouseover tooltip to display more info on the colour being examined.
// todo it would be nice to be able to have the tooltip appear immediately on mouseover, but there is no easy way to do this, (file jira feature request?)
final Tooltip tooltip = new Tooltip(colorName);
tooltip.setStyle("-fx-font-size: 14");
tooltip.setContentDisplay(ContentDisplay.BOTTOM);
final Rectangle graphic = new Rectangle(30, 30, Color.web(colorHex));
graphic.widthProperty().bind(prefTileSize.multiply(1.5));
graphic.heightProperty().bind(prefTileSize.multiply(1.5));
tooltip.setGraphic(graphic);
colorChoice.setTooltip(tooltip);
// color the button appropriately and change it's hover functionality (doing some of this in a css sheet would be better).
final String backgroundStyle = "-fx-background-color: " + colorHex + "; -fx-background-insets: 0; -fx-background-radius: 0;";
colorChoice.setStyle(backgroundStyle);
colorChoice.setOnMouseEntered(mouseEvent -> {
final String borderStyle = "-fx-border-color: ladder(" + colorHex + ", whitesmoke 49%, darkslategrey 50%); -fx-border-width: 2;";
colorChoice.setStyle(backgroundStyle + borderStyle);
});
colorChoice.setOnMouseExited(mouseEvent -> {
final String borderStyle = "-fx-border-width: 0; -fx-border-insets: 2;";
colorChoice.setStyle(backgroundStyle + borderStyle);
});
// choose the color when the button is clicked.
colorChoice.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent actionEvent) {
chosenColorName.set((String) colorChoice.getUserData());
}
});
// add the color choice to the swatch selection.
swatch.getChildren().add(colorChoice);
i++;
}
// select the first color in the chooser.
((Button) swatch.getChildren().get(0)).fire();
// layout the color picker.
getChildren().addAll(swatch, colorInfo);
VBox.setVgrow(swatch, Priority.ALWAYS);
setStyle("-fx-background-color: black; -fx-font-size: 16;");
swatch.layoutBoundsProperty().addListener((observableValue, oldBounds, newBounds) -> {
prefTileSize.set(Math.max(MIN_TILE_SIZE, Math.min(newBounds.getWidth() / nColumns, newBounds.getHeight() / nRows)));
for (Node child : swatch.getChildrenUnmodifiable()) {
Control tile = (Control) child;
final double margin = prefTileSize.get() / 10;
tile.setPrefSize(prefTileSize.get() - 2 * margin, prefTileSize.get() - 2 * margin);
GridPane.setMargin(child, new Insets(margin));
}
});
}
}

How to change the colors of specific bars using Javafx BarChart?

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.

How to connect a line to circle (not the center) using java fx

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.

Creating Text inside Bubble of JavaFX BubbleChart

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.

Randomizing Vertex Locations JUNG

I have created a java program of which starts with 1 vertex and from there it adds one vertice and 2 edges per cycle. It uses the Static Layout
Layout<Number, Number> staticLayout = new StaticLayout<Number, Number>(g, layout);
vv = new VisualizationViewer<Number, Number>(staticLayout, new Dimension(550, 550));
This is going to sound very un-technical, but the graph just doesn't look random enough, basically what i mean by this, is that every time it gets run they always seems to cluster a lot all the way around the edges of the graph, while very few get anywhere near the center. My program typically uses 100 generated verties and i will end up with half a dozen in the center and the others all round the edges.
Below is a random example that i just created just now.
Perhaps if someone could confirm that this is actually random, or if not if there is a way to get around this problem or if I've set something up wrong. As i wish to have the nodes as random as possible.
Any help would be appreciated.
Thanks
Below is the relevant code to the applet. involving its set up.
public class AnimatingAddNodeDemo extends JApplet {
//create a graph
Graph<Number, Number> ig = Graphs.synchronizedUndirectedGraph(new UndirectedSparseMultigraph<Number, Number>());
ObservableGraph<Number, Number> og = new ObservableGraph<Number, Number>(ig);
og.addGraphEventListener(new GraphEventListener<Number, Number>() {
public void handleGraphEvent(GraphEvent<Number, Number> evt) {
//System.err.println("got " + evt);
}
});
this.g = og;
//create a graphdraw
layout = new FRLayout<Number, Number>(g);
layout.setSize(new Dimension(600, 600));
setSize(700, 700);
Relaxer relaxer = new VisRunner((IterativeContext) layout);
relaxer.stop();
relaxer.prerelax();
Layout<Number, Number> staticLayout = new StaticLayout<Number, Number>(g, layout);
vv = new VisualizationViewer<Number, Number>(staticLayout, new Dimension(550, 550));
JRootPane rp = this.getRootPane();
rp.putClientProperty("defeatSystemEventQueueCheck", Boolean.TRUE);
getContentPane().setLayout(new BorderLayout());
}
Integer v_prev = null;
public void process() {
vv.getRenderContext().getPickedVertexState().clear();
vv.getRenderContext().getPickedEdgeState().clear();
try {
if (g.getVertexCount() < 100) {
//add a vertex
Integer v1 = nodeCount;
g.addVertex(v1);
nodeCount++;
System.out.println("adding vertex " + v1);
vv.getRenderContext().getPickedVertexState().pick(v1, true);
j.setText(myText);
// wire it to some edges
if (v_prev != null) {
Integer edge = edgeCount;
//vv.getRenderContext().getPickedEdgeState().pick(edge, true);
// let's connect to a random vertex, too!
int rand = (int) (Math.random() * (edgeCount-1)); // because there is a 0 node
while (v1.equals(rand)) {
System.out.println("avoided connecting to myself");
rand = (int) (Math.random() * (edgeCount-1)); // because there is a 0 node
}
edgeCount++;
g.addEdge(edge, rand, v1); //add an edge called var1, between the nodes var2 and var3
vv.getRenderContext().getPickedEdgeState().pick(edge, true);
System.out.println("Adding edge " + edge + " between " + rand + " & " + v1 + "()");
}
v_prev = v1;
layout.initialize();
Relaxer relaxer = new VisRunner((IterativeContext) layout);
relaxer.stop();
relaxer.prerelax();
vv.getRenderContext().getMultiLayerTransformer().setToIdentity();
vv.repaint();
} else {
done = true;
}
} catch (Exception e) {
System.out.println(e);
}
}
public static void main(String[] args) {
AnimatingAddNodeDemo and = new AnimatingAddNodeDemo();
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(and);
and.init();
and.start();
frame.pack();
//frame.setVisible(true);
}
}
The reason your graph isn't random likely stems from the fact that you are passing a FRLayout to the constructor.
layout = new FRLayout<Number, Number>(g);
// ...
Layout<Number, Number> staticLayout = new StaticLayout<Number, Number>(g, layout);
You could make your own random layout class by extending AbstractLayout. But, according to the JavaDoc, StaticLayout will randomly layout nodes if you exclude the second constructor argument.
Layout<Number, Number> staticLayout = new StaticLayout(Number, Number>(g);
I Didn't get to a conclusion on whether or not it is random. So instead when i create each vertex i decided to set the particular co-ordinate of the vertex using layout.setLocation(v1, x, y)
With making x and y using math.random() and multiplying it by the width and height of my applet.
Therefore i now know that it is random.
EDIT
This actually seemed to work, however it actually did not, I had to remove the FRLayout.
It turns out FRLayout will not let you set your own locations because of what the algorithm does.
FRLayout is a force directed Layout that will reposition the vertices
according to the topology of the graph.
So i therefore changed the FRLayout to StaticLayout, removed a few things that worked only with FRLayout and it works correctly now.

Categories