I have 4 series that I want to plot in one linechart. Each series has around 100.000 datapoints. Now, using javafx linechart, it takes several seconds to build the Chart. Not only that it is slow, it also consumes very much memory.
Does anybody has an idea how to build the desired chart faster. If necessary I would use other java libraries too.
Here is some working code. For numDataPoints = 10000 it runs fine, but for numDataPoints = 70000 it's very slow or even crashes.
Thank you in advance.
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.ScatterChart;
import javafx.scene.chart.XYChart;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class Chart extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) throws Exception {
prepareStage(stage);
}
private void prepareStage(Stage stage) {
stage.setTitle("JavaFX Chart Demo");
StackPane pane = new StackPane();
NumberAxis xAxis = new NumberAxis();
NumberAxis yAxis = new NumberAxis();
ScatterChart ac = new ScatterChart(xAxis, yAxis);
ac.setTitle("Segregation");
ac.setAnimated(false);
ac.setHorizontalGridLinesVisible(false);
ac.setVerticalGridLinesVisible(false);
int numDataPoints = 70000;
for (int k=0; k<4; k++) {
ObservableList<XYChart.Data<Number, Number>> data = FXCollections.<XYChart.Data<Number, Number>>observableArrayList();
for (int i = 0; i < numDataPoints; i++)
data.add(new XYChart.Data<>(i,Math.random() ));
XYChart.Series series = new XYChart.Series(data);
ac.getData().add(series);
}
pane.getChildren().add(ac);
Scene scene = new Scene(pane, 400, 200);
scene.getStylesheets().add("chart.css");
stage.setScene(scene);
stage.show();
}
}
This question has been asked here alread quite often. It is not so much a question of performance but a question of a reasonable tool usage. Do you know how many pixels in a row your monitor device has? If yes, then you can ask yourself the question what you think will remain from your 100.000 data points.
The solution to your problem is to just reduce the amount of data to something reasonable. You can also try to virtualize your display by externally managing the amount and portion of the data that is displayed at a certain point in time, e.g. for scrolling, in a similar way as a TableView would do it.
https://github.com/GSI-CS-CO/chart-fx
was specifically designed having this in mind. Have a look at -- or much better -- try out the examples.
N.B. We use this internally for our accelerator-related data acquisition/display purposes with 10k to 1M data points updated in real-time had a fair Swing-based solution (JDataViewer) but couldn't find anything equivalent compatible with JavaFX ... give it a try... it's free ... and open for contributions and comments.
Related
I have made programmatic changes to a Java FX Line Chart and I need a programmatic way to force a re-layout of the JavaFX Chart to occur. This question has been asked/answered before but not in my context.
I have tried the typical methods that have been presented as answers to this question (see complete, minimal example code below with in-line attempts at solving the problem). None of the typical solutions to this problem work.
Specifically (sp is a StackPane):
sp.requestLayout(); // does not work
and
sp.applyCss();
sp.layout(); // does not work
placing the above code in a .runLater() does not work.
I know that my changes are present in the chart because
(1) When I resize the chart by hand my changes suddenly appear
(2) When I use the "resize" method programmatically my changes appear BUT there is a different error (plus only parent nodes are supposed to use the "resize" method - not us programmers).
Below is a minimal complete set of code which reproduces the problem. When you run the code I programmatically change one of the data points to be larger when the chart is displayed. This resize works correctly. When you right-click on the chart a context menu appears with one choice ("Resize ALL the points"). When you select that single option my code resizes all the points - BUT - none of the data points are resized visually. If I resize the chart manually by dragging a side, the chart does a re-layout and all the data node sizes immediately visually change to the correct size (The size I programmatically set them to).
How can I force the re-layout to occur programmatically that I can force to occur manually? I would NOT like to do a hack (like programmatically set the stage size to be 1 pixel smaller and then set it one pixel larger).
Note: I have read that attempts to do a requestLayout() while a layout is in progress will be ignored so perhaps something like that is going on. I think a requestLayout() inside of a runLater() would fix the issue of an ongoing Layout() but that has not worked either.
Update: Scaling was suggested as an alternative to changing the StackPane size. This solution may be helpful to some but not to me. The Look and Feel of scaling a symbol is different than the look and feel of changing the regions size and allowing the "symbol" to grow into that size.
As a complete aside this is my first stackoverflow post. So thanks for all the previous examples a I have used from this forum in the past & thanks in advance for the answer to this problem.
import java.util.Random;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.LineChart.SortingPolicy;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class dummy extends Application {
#Override
public void start(Stage primaryStage) {
Random random = new Random();
final NumberAxis xAxis = new NumberAxis();
final NumberAxis yAxis = new NumberAxis();
xAxis.setLabel("X");
yAxis.setLabel("Y");
final LineChart<Number,Number> lineChart = new LineChart<Number,Number>(xAxis,yAxis);
Series<Number,Number> series = new Series<Number,Number>();
series.setName("Dummy Data");
// Generate data
double x = 0.0;
double y = 0.0;
for (int i = 0; i < 10; i++) {
Data<Number,Number> data = new Data(x += random.nextDouble(), y+=random.nextDouble());
series.getData().add(data);
}
lineChart.getData().add(series);
lineChart.setTitle("Random Data");
lineChart.setAxisSortingPolicy(SortingPolicy.NONE);
Scene scene = new Scene(lineChart,1200,600);
Stage stage = new Stage();
stage.setScene(scene);
stage.show();
// This resizes the first data point directly (this resize is displayed correctly when program is run)
Node node = series.getData().get(0).getNode();
setSize((StackPane)node,20);
// The context menu is invoked by a right click on the line Chart. It will resize the data point based on a context menu pick
// this resize does not work....unless I resize the window manually which causes a refresh/re-layout of the chart).
lineChart.setOnMouseClicked(new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent mouseEvent) {
if (MouseButton.SECONDARY.equals(mouseEvent.getButton())) {
Scene scene = ((Node)mouseEvent.getSource()).getScene();
ContextMenu menu = createMenu(lineChart);
menu.show(scene.getWindow(), mouseEvent.getScreenX(), mouseEvent.getScreenY());
}
}
});
}
private void setSize(StackPane sp, int size) {
sp.setMinSize(size, size);
sp.setMaxSize(size, size);
sp.setPrefSize(size, size);
}
// this creates a context menu that will allow you to resize all the data point nodes
private ContextMenu createMenu(LineChart<Number,Number> lineChart) {
final ContextMenu contextMenu = new ContextMenu();
final MenuItem resize = new MenuItem("Resize ALL the points");
resize.setOnAction(new EventHandler<ActionEvent>() {
#Override public void handle(ActionEvent event) {
for (Series<Number, Number> series : lineChart.getData()) {
for (Data<Number, Number> data : series.getData()) {
StackPane sp = (StackPane)data.getNode();
setSize (sp, 20);
// The above resizes do not take effect unless/until I manually resize the chart.
// the following two calls do not do anything;
sp.applyCss();
sp.layout();
// The request to layout the node does nothing
sp.requestLayout();
// Doing both of the above as runLaters does nothing
Platform.runLater(()->{sp.applyCss();sp.layout();});
Platform.runLater(()->{sp.requestLayout();});
// Going after the parent does nothing either
Group group = (Group)sp.getParent();
group.applyCss();
group.layout();
group.requestLayout();
// Going after the parent in a run later does nothing
Platform.runLater(()->{
group.applyCss();
group.layout();
group.requestLayout();
});
// note... doing a resize [commented out below] will work-ish.
// The documentation says NOT to use it thought that as it is for internal use only.
// By work-ish, the data points are enlarged BUT they are no longer centered on the data point
// When I resize the stage they get centered again - so this "solves" my original problem but causes a different problem
////////////////////////////////////
// sp.resize(20, 20); // Uncomment this line to see how it mostly works but introduces a new issue
////////////////////////////////////
}
}
}
});
contextMenu.getItems().add(resize);
return contextMenu;
}
public static void main(String[] args) {
Application.launch(args);
}
}
You can force a relayout by using e.g. an inner class
class LineChartX<X, Y> extends LineChart<X, Y>
{
public LineChartX(#NamedArg("xAxis") Axis<X> xAxis, #NamedArg("yAxis") Axis<Y> yAxis)
{
super(xAxis, yAxis);
}
#Override
public void layoutPlotChildren()
{
super.layoutPlotChildren();
}
}
and calling
lineChart.layoutPlotChildren();
in your menu action.
Simple one-line Solution:
nodes in LineChart scene graph have these parent-child relationships:
Pane chartContent - Group plotArea - Group plotContent - Path seriesLine;
layout requests for Group plotArea, defined in class XYChart, are suppressed:
private final Group plotArea = new Group(){
#Override public void requestLayout() {} // suppress layout requests
};
but Pane chartContent accepts layout requests:
Node node = series.getNode();
if (node instanceof Path) {
Path seriesLine = (Path) node;
Parent parent = seriesLine.getParent();
if (parent instanceof Group) {
Group plotContent = (Group) parent;
parent = plotContent.getParent();
if (parent instanceof Group) {
Group plotArea = (Group) parent;
parent = plotArea.getParent();
if (parent instanceof Pane) {
Pane chartContent = (Pane) parent;
chartContent.requestLayout();
}
}
}
}
so relayout of your chart can be forced by addding this single line
series.getNode().getParent().getParent().getParent().requestLayout();
to the end of your menu action handler.
You don't need to cast that node into a StackPane and set sizes. You need to use the setScaleX() and setScaleY() Methods
Node node = series.getData().get(0).getNode();
node.setScaleX(20);
node.setScaleY(20);
#Override
public void handle(ActionEvent event) {
for (Series<Number, Number> series : lineChart.getData()) {
for (Data<Number, Number> data : series.getData()) {
Node node = data.getNode();
node.setScaleY(20);
node.setScaleX(20);
}
}
}
#c0der posted a solution in comment form to my original post which worked but produced a runtime warning (in Eclipse). His solution was to add a dummy style sheet at the lineChart level with lineChart.getStylesheets().add(""); after the for loops ended. This code produced the warning "Apr 28, 2020 9:01:12 AM com.sun.javafx.css.StyleManager loadStylesheetUnPrivileged WARNING: Resource "" not found."
What did work without causing a run-time warning was to load an empty .css file and add it as a StyleSheet:
lineChart.getStylesheets().add(CSS.class.getResource("Empty.css").toExternalForm());
// note: I keep my .css resource files at the same location as my CSS class
// which is why I have the code "CSS.class" above
This one line solution worked once but I doubted it would work multiple times. I tested it to by increasing the size of the StackPane by 5 for each successive time "Resize ALL the points" was selected (in my dummy code above). Sure enough, it only worked the first time.
However, I added the no-op code lineChart.getStylesheets().replaceAll((s)->s+" "); before that one line solution and now it works multiple times in a row.
No matter how many times I executed the two lines of code
lineChart.getStylesheets().replaceAll((s)->s+" ");
lineChart.getStylesheets().add(CSS.class.getResource("Empty.css").toExternalForm()); `
it (1) worked and (2) The size of the list of lineChart StyleSheets did not grow beyond a size of 1. So a solution with a mystery.
Note: if you have an existing style sheet (I did not in my dummy example above) lineChart.getStylesheets().replaceAll((s)->s+" "); by itself may work as well. For some reason lineChart.getStylesheets().replaceAll((s)->s); without adding the " " on the end did not work.
Note: I originally thought I would have to code up a toggle solution to add Empty.css and the remove Empty.css with alternate calls but that proved unnecessary.
Bottom Line: if you have an existing StyleSheet lineChart.getStylesheets().replaceAll((s)->s+" "); works. If you do not have an existing StyleSheet adding an empty .css file as a StyleSheet in conjunction with the above replaceAll works.
Thanks again to #c0der for his original approach.
a mock-up of an issue I'm having:
import javafx.application.Application;
import javafx.stage.*;
import javafx.scene.*;
import javafx.scene.layout.VBox;
import javafx.scene.control.*;
import javafx.geometry.*;
public class tester extends Application {
#Override
public void start(Stage stage) {
String ipsum = "Lorem ipsum dolor"
+ "\nInteger nec odio."
+ "\nSed nisi. Nulla "
+ "\nPraesent mauris "
+ "\nPraesent mauris"
+ "\nPraesent mauris"
+ "\nPraesent mauris.";
VBox root = new VBox(25);
Label lblHeader = new Label("Place Holder");
TextArea txtOutputBox = new TextArea(ipsum);
Button btnSubmit = new Button("Submit");
VBox.setMargin(txtOutputBox, new Insets(30));
VBox.setMargin(lblHeader, new Insets(10,0,0,10));
txtOutputBox.setFocusTraversable(false);
txtOutputBox.setEditable(false);
txtOutputBox.setFocused(false); // <---- The method setFocused(boolean) from the type Node is not visible
root.setAlignment(Pos.CENTER);
root.getChildren().addAll(lblHeader, txtOutputBox, btnSubmit);
Scene scene = new Scene(root, 350, 250);
stage.setScene(scene);
stage.setTitle("Unexplained");
stage.show();
}
public static void main(String[] args) {
// TODO Auto-generated method stub
launch(args);
}
}
Basically, I am using a TextArea control to display some text which is intended to be read only. For aesthetic purposes and so as not to miguide the user, I want to prevent the control from receiving focus. I can easily set to false the FocusTraversable property but it's still possible to give the control focus by clicking within it (albeit, no editing can be done).
Reading through the documentation, TextArea says that it inherits the setFocused() method from Node, its distant relative. When I look at this method the docs say that it's Protected. Bingo I thought - but, my understanding of Protected was that it became available to all of the classes subclasses. Therefore, I don't understand why it's not available to the subclass TextArea.
So, I have two questions. In order of priority, the first would be great for my own education and understanding. I've wasted half a day of trying to figure it out :-( The second question would also be helpful if anyone has a quick suggestion.
Could someone explain why I can't access the Protected method of Node from the subclass TextArea (perhaps, one is not a subclass - but it seems to be).
Any suggestions as to how I could achieve what I'm trying to do here ?
Thanks anyone and all,
I am just trying to extend a SimpleStringProperty in OpenJFX 11.0.1 to add some extra functionality. But ist seems not so easy, I experienced strange behavior of my extended Property and I don't know why. I think it should work.
My in this sample code simplified SimpleStringProperty extension contains another readonly string property which should be updated every time the the user types into a bound TextField. In this case remove all not allowed characters and convert the prefix. (I know this is not perfect but short enough to show)
After starting the sample code you will get a window with a rows of Controls. Typing in a String like "001 (242) 555666" the label should show the normalized phone number like "+1242555666".
The initial conversion works correcty.
I never get any exceptions.
The conversion is called when I type in new digits.
But if you play around with typing and deleting after a few seconds the set() method of my property isn't longer triggered by the bidirectional binding to the TextField.
To simplify the example I didn't use a TextFormatter. If I use one the problem doesn't change.
Can anyone help me figure out the problem?
Windows and OS X show the same behavior with OpenJFX 11 and OpenJFX 11.0.1
I tried the same code with JDK 1.8 and there it works fine.
package testproperty;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
public class TestProperty extends Application {
// attempt to create an own property
public class myPhoneNumberProperty extends SimpleStringProperty {
private final ReadOnlyStringWrapper normalizedNumber = new ReadOnlyStringWrapper("");
public ReadOnlyStringProperty normalizedNumberProperty() { return normalizedNumber.getReadOnlyProperty(); }
public String getNormalizedNumber() { return normalizedNumber.get(); }
public myPhoneNumberProperty() {
super();
}
public myPhoneNumberProperty(String s) {
super(s);
calculate();
}
#Override
public void set(String s) {
super.set(s);
calculate();
}
private void calculate() {
// some calculations (only for test purposes)
String original = this.get();
String result = original.replaceAll("[^0123456789]","");
if (result.startsWith("00")) result = result.replaceFirst("00", "+");
if (original.startsWith("+")) result = "+".concat(result);
normalizedNumber.set(result);
}
}
#Override
public void start(Stage primaryStage) {
// create my property
myPhoneNumberProperty phoneNumberA = new myPhoneNumberProperty("+34 952 111 222");
// set up grid pane
GridPane grid = new GridPane();
grid.setPadding(new Insets(5,5,5,5));
grid.setVgap(20);
grid.setHgap(20);
// set up the row
Label labelA = new Label("Enter phone number");
TextField textFieldA = new TextField();
textFieldA.textProperty().bindBidirectional(phoneNumberA);
Label labelB = new Label("Normalized number");
Label labelN = new Label();
labelN.textProperty().bind(phoneNumberA.normalizedNumberProperty());
grid.addRow(0, labelA, textFieldA, labelB, labelN);
// complete scene
Scene scene = new Scene(grid, 1000, 100);
primaryStage.setTitle("PhoneNumberProperty TestProg");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Your phoneNumberA property object is being garbage collected. To fix this you must keep a strong reference to the object. One option is to make it an instance field.
JavaFX implements bindings using weak listeners/references. Bidirectional bindings have no strong references to the other property. This is different from unidirectional bindings where a reference to the observable value must be kept in order to unbind from it later.
I'm new to the forums, so I hope I'm not asking a question that has been answered in the past. I've tried to be thorough looking for answer before posting.
I am currently working on a pie chart that will eventually be used for tracking financial expenses. Right now I have several categories that make up each slice. I am trying to hide the legend for the zero value slices.
I am doing this in javaFX. I'm still very green when it comes to programming and don't have experience outside of Java. Any help as explained to dummies would be appreciated. Thanks.
Added a picture and complete code to illustrate the problem at hand. Restaurants & Dining, and Shopping & entertainment both have zero values. I want to hide the legend for those items in this example.
package Example;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.chart.PieChart;
public class PieExampleController implements Initializable {
#FXML
private PieChart pieChart;
#Override
public void initialize(URL arg0, ResourceBundle arg1) {
// TODO Auto-generated method stub
ObservableList<PieChart.Data> pieChartData = FXCollections.observableArrayList(
new PieChart.Data("Groceries", 1),
new PieChart.Data("Transportation", 1),
new PieChart.Data("Restaurants & Dining", 0),
new PieChart.Data("Shopping & Entertainment", 0));
pieChart.setData(pieChartData);
}
}
Thats how i do it:
List<PieChart.Data> dataArrayList = new LinkedList<Data>();
if (value1>0) {
Data data = new PieChart.Data("my label", value1);
dataArrayList.add(data);
}
...
ObservableList<PieChart.Data> pieChartData =
FXCollections.observableArrayList(dataArrayList);
Adding only not empty data entries (or removing empty entries) manually once at startup is just fine if the data is immutable and unmodifiable. On the other hand, if it can change during the lifetime of the chart, we need a mechanism that handles the add/remove automagically: FilteredList to the rescue.
Below is an example that
configures a source list with an extractor (on the pieValueProperty): doing so will notify any listChangeListener on change of that value with a change of type update
wraps a FilteredList around the source list
configures the pieChart with the filteredList
With that in place, we can install a predicate on the filteredList that hides items as needed: the example uses a Slider to update the lower threshhold of which data values should be included in the chart.
Unfortunately, PieChart has a couple of bugs (sigh... whatever I touch in FX, they always boil up ...) that interfere with such a simple setup
due to a freaky mixture of node/value plus "optimized" internal data structure plus incorrect implementation of syncing the internal (linked) data structure with changes to the list the chart can't be animated
the sync can't handle changes of type replaced at all (which is what FilteredList fires on resetting the predicate)
In an example both issues can be avoided by disabling animation and clearing out the list (set a predicate that blocks all) before setting the real condition. In producation code such tweaking may or may not be possible.
The example:
public class FilteredPieChartExample extends Application {
#Override
public void start(Stage primaryStage) {
FilteredList<Data> filtered = getChartData();
//ListChangeReport report = new ListChangeReport(filtered);
PieChart pieChart = new PieChart(filtered);
// bug in pieChart: can't handle data modification with animation on
pieChart.setAnimated(false);
// use slider to set lower threshhold for value of data to show in pie
Slider slider = new Slider(-1., 100., -1.);
slider.valueProperty().addListener((src, ov, nv) -> {
// actually, cannot handle data modification at all ... need to clear out first ...
// bug in pieChart.dataChangeListener: doesn't handle replaced correctly
filtered.setPredicate(data -> false);
filtered.setPredicate(data -> data.getPieValue() > nv.doubleValue());
//report.prettyPrint();
});
primaryStage.setTitle("PieChart");
Pane root = new VBox(pieChart, slider);
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}
private FilteredList<Data> getChartData() {
// use ObservableList with extractor on pieValueProperty
ObservableList<Data> answer = FXCollections.observableArrayList(
e -> new Observable[] {e.pieValueProperty()}
);
answer.addAll(
new Data("java", 17.56),
new Data("C", 17.06),
new Data("C++", 8.25),
new Data("C#", 8.20),
new Data("ObjectiveC", 6.8),
new Data("PHP", 6.0),
new Data("(Visual)Basic", 4.76),
new Data("Other", 31.37),
new Data("empty", 0)
);
return new FilteredList<>(answer);
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
#SuppressWarnings("unused")
private static final Logger LOG = Logger.getLogger(FilteredPieChartExample.class
.getName());
}
I am currently developing a tool for visualization of metagenomics data using graphs and so the Java JUNG graph visualization library.
I encounter a delay when there are around 1000 nodes being shown, either by moving the camera around or dragging some of the nodes.
Is there any hack can that be used to improve this situation?
I read something about dividing the window in chunks, and to only work with chunks of the panel that are being shown, but I cannot understand this.
Thank you.
The question might be considered as too broad, because there are simply too many degrees of freedom for the optimization. And there are questions that are at least related (Improve the rendering of a JUNG graph , JUNG cannot display large graphs? or others), if not duplicates.
However, I'll try to answer it here:
In general, with JUNG, you can create a nice graph, with impressive default functionality (interaction), and many features, easily and with a few lines of code. In this regard, JUNG does not primarily aim at painting graphs with 1000's of vertices. Instead, it aims at painting a graph with dozens (or maybe few 100's) vertices and edges nicely.
(In fact, painting a graph with >1000 vertices rarely makes sense at all, from a theoretical, information visualization standpoint. You won't be able to visually extract any information from such a graph - at least, not without excesssive zooming and panning)
When you want to render a graph with many vertices and many edges, there are options to increase the performance. (You did not say anything about the number of edges. In many cases, these are the most expensive thing!).
From my experience, the single most important thing for improving the rendering performance is to....
disable anti-aliasing!
Seriously, this is really expensive. In JUNG, this can be done with
visualizationViewer.getRenderingHints().remove(
RenderingHints.KEY_ANTIALIASING)
Beyond that, there are many options to increase the performance, but of course, they all depend on which visual feature you want to sacrifice. Below is an example that shows a graph with 2500 vertices and 5000 edges. By default, it's horribly slow. The improvePerformance method contains several options of how to make the visualization faster. Even when only disabling anti-aliasing, the performance is acceptable on my (rather slow) machine.
Edited/extended in response to the comments:
import java.awt.Dimension;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.geom.Point2D;
import java.util.Random;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
import org.apache.commons.collections15.Predicate;
import edu.uci.ics.jung.algorithms.layout.FRLayout;
import edu.uci.ics.jung.algorithms.layout.Layout;
import edu.uci.ics.jung.graph.DirectedSparseGraph;
import edu.uci.ics.jung.graph.Graph;
import edu.uci.ics.jung.graph.util.Context;
import edu.uci.ics.jung.graph.util.Pair;
import edu.uci.ics.jung.visualization.Layer;
import edu.uci.ics.jung.visualization.RenderContext;
import edu.uci.ics.jung.visualization.VisualizationViewer;
import edu.uci.ics.jung.visualization.control.DefaultModalGraphMouse;
import edu.uci.ics.jung.visualization.decorators.EdgeShape;
import edu.uci.ics.jung.visualization.renderers.BasicEdgeRenderer;
import edu.uci.ics.jung.visualization.transform.shape.GraphicsDecorator;
public class JungPerformance
{
public static void main(String[] args)
{
SwingUtilities.invokeLater(new Runnable()
{
#Override
public void run()
{
createAndShowGUI();
}
});
}
private static void createAndShowGUI()
{
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Graph<String, String> g = createGraph();
Dimension size = new Dimension(800,800);
VisualizationViewer<String, String> vv =
new VisualizationViewer<String, String>(
new FRLayout<String, String>(g, size));
DefaultModalGraphMouse<String, Double> graphMouse =
new DefaultModalGraphMouse<String, Double>();
vv.setGraphMouse(graphMouse);
improvePerformance(vv);
f.getContentPane().add(vv);
f.setSize(size);
f.setLocationRelativeTo(null);
f.setVisible(true);
}
// This method summarizes several options for improving the painting
// performance. Enable or disable them depending on which visual features
// you want to sacrifice for the higher performance.
private static <V, E> void improvePerformance(
VisualizationViewer<V, E> vv)
{
// Probably the most important step for the pure rendering performance:
// Disable anti-aliasing
vv.getRenderingHints().remove(RenderingHints.KEY_ANTIALIASING);
// Skip vertices that are not inside the visible area.
doNotPaintInvisibleVertices(vv);
// May be helpful for performance in general, but not appropriate
// when there are multiple edges between a pair of nodes: Draw
// the edges not as curves, but as straight lines:
vv.getRenderContext().setEdgeShapeTransformer(new EdgeShape.Line<V,E>());
// May be helpful for painting performance: Omit the arrow heads
// of directed edges
Predicate<Context<Graph<V, E>, E>> edgeArrowPredicate =
new Predicate<Context<Graph<V,E>,E>>()
{
#Override
public boolean evaluate(Context<Graph<V, E>, E> arg0)
{
return false;
}
};
vv.getRenderContext().setEdgeArrowPredicate(edgeArrowPredicate);
}
// Skip all vertices that are not in the visible area.
// NOTE: See notes at the end of this method!
private static <V, E> void doNotPaintInvisibleVertices(
VisualizationViewer<V, E> vv)
{
Predicate<Context<Graph<V, E>, V>> vertexIncludePredicate =
new Predicate<Context<Graph<V,E>,V>>()
{
Dimension size = new Dimension();
#Override
public boolean evaluate(Context<Graph<V, E>, V> c)
{
vv.getSize(size);
Point2D point = vv.getGraphLayout().transform(c.element);
Point2D transformed =
vv.getRenderContext().getMultiLayerTransformer()
.transform(point);
if (transformed.getX() < 0 || transformed.getX() > size.width)
{
return false;
}
if (transformed.getY() < 0 || transformed.getY() > size.height)
{
return false;
}
return true;
}
};
vv.getRenderContext().setVertexIncludePredicate(vertexIncludePredicate);
// NOTE: By default, edges will NOT be included in the visualization
// when ONE of their vertices is NOT included in the visualization.
// This may look a bit odd when zooming and panning over the graph.
// Calling the following method will cause the edges to be skipped
// ONLY when BOTH their vertices are NOT included in the visualization,
// which may look nicer and more intuitive
doPaintEdgesAtLeastOneVertexIsVisible(vv);
}
// See note at end of "doNotPaintInvisibleVertices"
private static <V, E> void doPaintEdgesAtLeastOneVertexIsVisible(
VisualizationViewer<V, E> vv)
{
vv.getRenderer().setEdgeRenderer(new BasicEdgeRenderer<V, E>()
{
#Override
public void paintEdge(RenderContext<V,E> rc, Layout<V, E> layout, E e)
{
GraphicsDecorator g2d = rc.getGraphicsContext();
Graph<V,E> graph = layout.getGraph();
if (!rc.getEdgeIncludePredicate().evaluate(
Context.<Graph<V,E>,E>getInstance(graph,e)))
return;
Pair<V> endpoints = graph.getEndpoints(e);
V v1 = endpoints.getFirst();
V v2 = endpoints.getSecond();
if (!rc.getVertexIncludePredicate().evaluate(
Context.<Graph<V,E>,V>getInstance(graph,v1)) &&
!rc.getVertexIncludePredicate().evaluate(
Context.<Graph<V,E>,V>getInstance(graph,v2)))
return;
Stroke new_stroke = rc.getEdgeStrokeTransformer().transform(e);
Stroke old_stroke = g2d.getStroke();
if (new_stroke != null)
g2d.setStroke(new_stroke);
drawSimpleEdge(rc, layout, e);
// restore paint and stroke
if (new_stroke != null)
g2d.setStroke(old_stroke);
}
});
}
public static Graph<String, String> createGraph()
{
Random random = new Random(0);
int numVertices = 2500;
int numEdges = 5000;
Graph<String, String> g = new DirectedSparseGraph<String, String>();
for (int i=0; i<numVertices; i++)
{
g.addVertex("v"+i);
}
for (int i=0; i<numEdges; i++)
{
int v0 = random.nextInt(numVertices);
int v1 = random.nextInt(numVertices);
g.addEdge("e"+i, "v"+v0, "v"+v1);
}
return g;
}
}
#Marco13's answer is a good one. I will add (as one of JUNG's authors) that JUNG's current major flaw in terms of visualization scaling is a lack of good spatial data structures. As a result, both force-directed layouts and interactive visualization for larger graphs can be pretty slow.
At some point we'll get around to addressing that (patches welcome :) ).