JavaFX 中的图形可视化(如 yFiles)
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/30679025/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Graph Visualisation (like yFiles) in JavaFX
提问by 3legit4quit
Something like Graphviz but more specifically, yFiles.
类似于 Graphviz,但更具体地说,是 yFiles。
I want a node/edge type of graph visualization.
我想要一个节点/边类型的图形可视化。
I was thinking about making the node a Circle
and the edge a Line
. The problem is what to use for the area where the nodes/edges appear. Should I use a ScrollPane
, a regular Pane
, a Canvas
, etc...
我正在考虑制作节点 aCircle
和边缘 a Line
。问题是在节点/边缘出现的区域使用什么。我应该使用 a ScrollPane
, a regular Pane
, aCanvas
等...
I will add scrolling functionality, zooming, selecting nodes & dragging nodes.
我将添加滚动功能、缩放、选择节点和拖动节点。
Thanks for the help.
谢谢您的帮助。
采纳答案by Roland
I had 2 hours to kill, so I thought I'd give it a shot. Turns out that it's easy to come up with a prototype.
我有 2 个小时的时间来杀人,所以我想我会试一试。事实证明,想出一个原型很容易。
Here's what you need:
这是您需要的:
- a main class to use the graph library you create
- a graph with a data model
- easy adding and removing of nodes and edges (turns out that it's better to name the nodes cells in order to avoid confusion with JavaFX nodes during programming)
- a zoomable scrollpane
- a layout algorithm for the graph
- 使用您创建的图形库的主类
- 带有数据模型的图
- 轻松添加和删除节点和边(事实证明,最好命名节点单元格,以避免在编程过程中与 JavaFX 节点混淆)
- 可缩放的滚动窗格
- 图的布局算法
It's really too much to be asked on SO, so I'll just add the code with a few comments.
关于 SO 的要求真的太多了,所以我只添加带有一些注释的代码。
The application instantiates the graph, adds cells and connects them via edges.
应用程序实例化图形,添加单元格并通过边连接它们。
application/Main.java
应用程序/Main.java
package application;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import com.fxgraph.graph.CellType;
import com.fxgraph.graph.Graph;
import com.fxgraph.graph.Model;
import com.fxgraph.layout.base.Layout;
import com.fxgraph.layout.random.RandomLayout;
public class Main extends Application {
Graph graph = new Graph();
@Override
public void start(Stage primaryStage) {
BorderPane root = new BorderPane();
graph = new Graph();
root.setCenter(graph.getScrollPane());
Scene scene = new Scene(root, 1024, 768);
scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();
addGraphComponents();
Layout layout = new RandomLayout(graph);
layout.execute();
}
private void addGraphComponents() {
Model model = graph.getModel();
graph.beginUpdate();
model.addCell("Cell A", CellType.RECTANGLE);
model.addCell("Cell B", CellType.RECTANGLE);
model.addCell("Cell C", CellType.RECTANGLE);
model.addCell("Cell D", CellType.TRIANGLE);
model.addCell("Cell E", CellType.TRIANGLE);
model.addCell("Cell F", CellType.RECTANGLE);
model.addCell("Cell G", CellType.RECTANGLE);
model.addEdge("Cell A", "Cell B");
model.addEdge("Cell A", "Cell C");
model.addEdge("Cell B", "Cell C");
model.addEdge("Cell C", "Cell D");
model.addEdge("Cell B", "Cell E");
model.addEdge("Cell D", "Cell F");
model.addEdge("Cell D", "Cell G");
graph.endUpdate();
}
public static void main(String[] args) {
launch(args);
}
}
The scrollpane should have a white background.
滚动窗格应具有白色背景。
application/application.css
应用程序/应用程序.css
.scroll-pane > .viewport {
-fx-background-color: white;
}
The zoomable scrollpane, I got the code base from pixel duke:
ZoomableScrollPane.java
ZoomableScrollPane.java
package com.fxgraph.graph;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.control.ScrollPane;
import javafx.scene.input.ScrollEvent;
import javafx.scene.transform.Scale;
public class ZoomableScrollPane extends ScrollPane {
Group zoomGroup;
Scale scaleTransform;
Node content;
double scaleValue = 1.0;
double delta = 0.1;
public ZoomableScrollPane(Node content) {
this.content = content;
Group contentGroup = new Group();
zoomGroup = new Group();
contentGroup.getChildren().add(zoomGroup);
zoomGroup.getChildren().add(content);
setContent(contentGroup);
scaleTransform = new Scale(scaleValue, scaleValue, 0, 0);
zoomGroup.getTransforms().add(scaleTransform);
zoomGroup.setOnScroll(new ZoomHandler());
}
public double getScaleValue() {
return scaleValue;
}
public void zoomToActual() {
zoomTo(1.0);
}
public void zoomTo(double scaleValue) {
this.scaleValue = scaleValue;
scaleTransform.setX(scaleValue);
scaleTransform.setY(scaleValue);
}
public void zoomActual() {
scaleValue = 1;
zoomTo(scaleValue);
}
public void zoomOut() {
scaleValue -= delta;
if (Double.compare(scaleValue, 0.1) < 0) {
scaleValue = 0.1;
}
zoomTo(scaleValue);
}
public void zoomIn() {
scaleValue += delta;
if (Double.compare(scaleValue, 10) > 0) {
scaleValue = 10;
}
zoomTo(scaleValue);
}
/**
*
* @param minimizeOnly
* If the content fits already into the viewport, then we don't
* zoom if this parameter is true.
*/
public void zoomToFit(boolean minimizeOnly) {
double scaleX = getViewportBounds().getWidth() / getContent().getBoundsInLocal().getWidth();
double scaleY = getViewportBounds().getHeight() / getContent().getBoundsInLocal().getHeight();
// consider current scale (in content calculation)
scaleX *= scaleValue;
scaleY *= scaleValue;
// distorted zoom: we don't want it => we search the minimum scale
// factor and apply it
double scale = Math.min(scaleX, scaleY);
// check precondition
if (minimizeOnly) {
// check if zoom factor would be an enlargement and if so, just set
// it to 1
if (Double.compare(scale, 1) > 0) {
scale = 1;
}
}
// apply zoom
zoomTo(scale);
}
private class ZoomHandler implements EventHandler<ScrollEvent> {
@Override
public void handle(ScrollEvent scrollEvent) {
// if (scrollEvent.isControlDown())
{
if (scrollEvent.getDeltaY() < 0) {
scaleValue -= delta;
} else {
scaleValue += delta;
}
zoomTo(scaleValue);
scrollEvent.consume();
}
}
}
}
Every cell is represented as Pane into which you can put any Node as view (rectangle, label, imageview, etc)
每个单元格都表示为窗格,您可以将任何节点作为视图(矩形、标签、图像视图等)放入其中
Cell.java
细胞.java
package com.fxgraph.graph;
import java.util.ArrayList;
import java.util.List;
import javafx.scene.Node;
import javafx.scene.layout.Pane;
public class Cell extends Pane {
String cellId;
List<Cell> children = new ArrayList<>();
List<Cell> parents = new ArrayList<>();
Node view;
public Cell(String cellId) {
this.cellId = cellId;
}
public void addCellChild(Cell cell) {
children.add(cell);
}
public List<Cell> getCellChildren() {
return children;
}
public void addCellParent(Cell cell) {
parents.add(cell);
}
public List<Cell> getCellParents() {
return parents;
}
public void removeCellChild(Cell cell) {
children.remove(cell);
}
public void setView(Node view) {
this.view = view;
getChildren().add(view);
}
public Node getView() {
return this.view;
}
public String getCellId() {
return cellId;
}
}
The cells should be created via some kind of factory, so they are classified by type:
单元格应该通过某种工厂创建,因此它们按类型分类:
CellType.java
细胞类型.java
package com.fxgraph.graph;
public enum CellType {
RECTANGLE,
TRIANGLE
;
}
Instantiating them is quite easy:
实例化它们非常简单:
RectangleCell.java
矩形单元.java
package com.fxgraph.cells;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import com.fxgraph.graph.Cell;
public class RectangleCell extends Cell {
public RectangleCell( String id) {
super( id);
Rectangle view = new Rectangle( 50,50);
view.setStroke(Color.DODGERBLUE);
view.setFill(Color.DODGERBLUE);
setView( view);
}
}
TriangleCell.java
三角单元.java
package com.fxgraph.cells;
import javafx.scene.paint.Color;
import javafx.scene.shape.Polygon;
import com.fxgraph.graph.Cell;
public class TriangleCell extends Cell {
public TriangleCell( String id) {
super( id);
double width = 50;
double height = 50;
Polygon view = new Polygon( width / 2, 0, width, height, 0, height);
view.setStroke(Color.RED);
view.setFill(Color.RED);
setView( view);
}
}
Then of course you need the edges. You can use any connection you like, even cubic curves. For sake of simplicity I use a line:
那么当然你需要边缘。您可以使用任何您喜欢的连接,甚至是三次曲线。为简单起见,我使用一行:
Edge.java
边缘.java
package com.fxgraph.graph;
import javafx.scene.Group;
import javafx.scene.shape.Line;
public class Edge extends Group {
protected Cell source;
protected Cell target;
Line line;
public Edge(Cell source, Cell target) {
this.source = source;
this.target = target;
source.addCellChild(target);
target.addCellParent(source);
line = new Line();
line.startXProperty().bind( source.layoutXProperty().add(source.getBoundsInParent().getWidth() / 2.0));
line.startYProperty().bind( source.layoutYProperty().add(source.getBoundsInParent().getHeight() / 2.0));
line.endXProperty().bind( target.layoutXProperty().add( target.getBoundsInParent().getWidth() / 2.0));
line.endYProperty().bind( target.layoutYProperty().add( target.getBoundsInParent().getHeight() / 2.0));
getChildren().add( line);
}
public Cell getSource() {
return source;
}
public Cell getTarget() {
return target;
}
}
An extension to this would be to bind the edge to ports (north/south/east/west) of the cells.
对此的扩展是将边缘绑定到单元的端口(北/南/东/西)。
Then you'd want to drag the nodes, so you'd have to add some mouse gestures. The important part is to consider a zoom factor in case the graph canvas is zoomed
然后你想拖动节点,所以你必须添加一些鼠标手势。重要的部分是在图形画布被缩放的情况下考虑缩放因子
MouseGestures.java
鼠标手势.java
package com.fxgraph.graph;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.input.MouseEvent;
public class MouseGestures {
final DragContext dragContext = new DragContext();
Graph graph;
public MouseGestures( Graph graph) {
this.graph = graph;
}
public void makeDraggable( final Node node) {
node.setOnMousePressed(onMousePressedEventHandler);
node.setOnMouseDragged(onMouseDraggedEventHandler);
node.setOnMouseReleased(onMouseReleasedEventHandler);
}
EventHandler<MouseEvent> onMousePressedEventHandler = new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
Node node = (Node) event.getSource();
double scale = graph.getScale();
dragContext.x = node.getBoundsInParent().getMinX() * scale - event.getScreenX();
dragContext.y = node.getBoundsInParent().getMinY() * scale - event.getScreenY();
}
};
EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
Node node = (Node) event.getSource();
double offsetX = event.getScreenX() + dragContext.x;
double offsetY = event.getScreenY() + dragContext.y;
// adjust the offset in case we are zoomed
double scale = graph.getScale();
offsetX /= scale;
offsetY /= scale;
node.relocate(offsetX, offsetY);
}
};
EventHandler<MouseEvent> onMouseReleasedEventHandler = new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
}
};
class DragContext {
double x;
double y;
}
}
Then you need a model in which you store the cells and the edges. Any time new cells can be added and existing ones can be deleted. You need to process them distinguished from the existing ones (e. g. to add mouse gestures, animate them when you add them, etc). When you implement the layout algorithm you'll be faced with the determination of a root node. So you should make an invisible root node (graphParent) which won't be added to the graph itself, but at which all nodes start that don't have a parent.
然后您需要一个模型来存储单元格和边缘。任何时候都可以添加新单元格和删除现有单元格。您需要将它们与现有的区别进行处理(例如,添加鼠标手势,添加它们时为它们设置动画等)。当您实现布局算法时,您将面临根节点的确定。所以你应该创建一个不可见的根节点(graphParent),它不会被添加到图形本身,但所有节点都开始没有父节点。
Model.java
模型.java
package com.fxgraph.graph;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.fxgraph.cells.TriangleCell;
import com.fxgraph.cells.RectangleCell;
public class Model {
Cell graphParent;
List<Cell> allCells;
List<Cell> addedCells;
List<Cell> removedCells;
List<Edge> allEdges;
List<Edge> addedEdges;
List<Edge> removedEdges;
Map<String,Cell> cellMap; // <id,cell>
public Model() {
graphParent = new Cell( "_ROOT_");
// clear model, create lists
clear();
}
public void clear() {
allCells = new ArrayList<>();
addedCells = new ArrayList<>();
removedCells = new ArrayList<>();
allEdges = new ArrayList<>();
addedEdges = new ArrayList<>();
removedEdges = new ArrayList<>();
cellMap = new HashMap<>(); // <id,cell>
}
public void clearAddedLists() {
addedCells.clear();
addedEdges.clear();
}
public List<Cell> getAddedCells() {
return addedCells;
}
public List<Cell> getRemovedCells() {
return removedCells;
}
public List<Cell> getAllCells() {
return allCells;
}
public List<Edge> getAddedEdges() {
return addedEdges;
}
public List<Edge> getRemovedEdges() {
return removedEdges;
}
public List<Edge> getAllEdges() {
return allEdges;
}
public void addCell(String id, CellType type) {
switch (type) {
case RECTANGLE:
RectangleCell rectangleCell = new RectangleCell(id);
addCell(rectangleCell);
break;
case TRIANGLE:
TriangleCell circleCell = new TriangleCell(id);
addCell(circleCell);
break;
default:
throw new UnsupportedOperationException("Unsupported type: " + type);
}
}
private void addCell( Cell cell) {
addedCells.add(cell);
cellMap.put( cell.getCellId(), cell);
}
public void addEdge( String sourceId, String targetId) {
Cell sourceCell = cellMap.get( sourceId);
Cell targetCell = cellMap.get( targetId);
Edge edge = new Edge( sourceCell, targetCell);
addedEdges.add( edge);
}
/**
* Attach all cells which don't have a parent to graphParent
* @param cellList
*/
public void attachOrphansToGraphParent( List<Cell> cellList) {
for( Cell cell: cellList) {
if( cell.getCellParents().size() == 0) {
graphParent.addCellChild( cell);
}
}
}
/**
* Remove the graphParent reference if it is set
* @param cellList
*/
public void disconnectFromGraphParent( List<Cell> cellList) {
for( Cell cell: cellList) {
graphParent.removeCellChild( cell);
}
}
public void merge() {
// cells
allCells.addAll( addedCells);
allCells.removeAll( removedCells);
addedCells.clear();
removedCells.clear();
// edges
allEdges.addAll( addedEdges);
allEdges.removeAll( removedEdges);
addedEdges.clear();
removedEdges.clear();
}
}
And then there's the graph itself which contains the zoomable scrollpane, the model, etc. In the graph the added and removed nodes are handled (mouse gestures, cells and edges added to the scrollpane, etc).
然后是包含可缩放滚动窗格、模型等的图形本身。在图形中处理添加和删除的节点(鼠标手势、添加到滚动窗格的单元格和边缘等)。
Graph.java
图.java
package com.fxgraph.graph;
import javafx.scene.Group;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.Pane;
public class Graph {
private Model model;
private Group canvas;
private ZoomableScrollPane scrollPane;
MouseGestures mouseGestures;
/**
* the pane wrapper is necessary or else the scrollpane would always align
* the top-most and left-most child to the top and left eg when you drag the
* top child down, the entire scrollpane would move down
*/
CellLayer cellLayer;
public Graph() {
this.model = new Model();
canvas = new Group();
cellLayer = new CellLayer();
canvas.getChildren().add(cellLayer);
mouseGestures = new MouseGestures(this);
scrollPane = new ZoomableScrollPane(canvas);
scrollPane.setFitToWidth(true);
scrollPane.setFitToHeight(true);
}
public ScrollPane getScrollPane() {
return this.scrollPane;
}
public Pane getCellLayer() {
return this.cellLayer;
}
public Model getModel() {
return model;
}
public void beginUpdate() {
}
public void endUpdate() {
// add components to graph pane
getCellLayer().getChildren().addAll(model.getAddedEdges());
getCellLayer().getChildren().addAll(model.getAddedCells());
// remove components from graph pane
getCellLayer().getChildren().removeAll(model.getRemovedCells());
getCellLayer().getChildren().removeAll(model.getRemovedEdges());
// enable dragging of cells
for (Cell cell : model.getAddedCells()) {
mouseGestures.makeDraggable(cell);
}
// every cell must have a parent, if it doesn't, then the graphParent is
// the parent
getModel().attachOrphansToGraphParent(model.getAddedCells());
// remove reference to graphParent
getModel().disconnectFromGraphParent(model.getRemovedCells());
// merge added & removed cells with all cells
getModel().merge();
}
public double getScale() {
return this.scrollPane.getScaleValue();
}
}
A wrapper for the cell layer. You'll probably want to add multiple layers (e. g. a selection layer which highlights selected cells)
细胞层的包装。您可能想要添加多个图层(例如突出显示所选单元格的选择图层)
CellLayer.java
细胞层
package com.fxgraph.graph;
import javafx.scene.layout.Pane;
public class CellLayer extends Pane {
}
Now you need a layout for the cells. I suggest to create a simple abstract class which will get extended as you develop the graph.
现在您需要单元格的布局。我建议创建一个简单的抽象类,它会在您开发图形时得到扩展。
package com.fxgraph.layout.base;
public abstract class Layout {
public abstract void execute();
}
For sake of simplicity here's a simple layout algorithm in which random coordinates are used. Of course you'd have to do more complex stuff like tree layouts, etc.
为了简单起见,这里有一个简单的布局算法,其中使用了随机坐标。当然,你必须做更复杂的事情,比如树布局等。
RandomLayout.java
随机布局.java
package com.fxgraph.layout.random;
import java.util.List;
import java.util.Random;
import com.fxgraph.graph.Cell;
import com.fxgraph.graph.Graph;
import com.fxgraph.layout.base.Layout;
public class RandomLayout extends Layout {
Graph graph;
Random rnd = new Random();
public RandomLayout(Graph graph) {
this.graph = graph;
}
public void execute() {
List<Cell> cells = graph.getModel().getAllCells();
for (Cell cell : cells) {
double x = rnd.nextDouble() * 500;
double y = rnd.nextDouble() * 500;
cell.relocate(x, y);
}
}
}
The example looks like this:
该示例如下所示:
You can drag the cells with the mouse button and zoom in and out with the mouse wheel.
您可以使用鼠标按钮拖动单元格并使用鼠标滚轮放大和缩小。
Adding new cell types is as easy as creating subclasses of Cell:
添加新的单元格类型就像创建单元格的子类一样简单:
package com.fxgraph.cells;
import javafx.scene.control.Button;
import com.fxgraph.graph.Cell;
public class ButtonCell extends Cell {
public ButtonCell(String id) {
super(id);
Button view = new Button(id);
setView(view);
}
}
package com.fxgraph.cells;
import javafx.scene.image.ImageView;
import com.fxgraph.graph.Cell;
public class ImageCell extends Cell {
public ImageCell(String id) {
super(id);
ImageView view = new ImageView("http://upload.wikimedia.org/wikipedia/commons/thumb/4/41/Siberischer_tiger_de_edit02.jpg/800px-Siberischer_tiger_de_edit02.jpg");
view.setFitWidth(100);
view.setFitHeight(80);
setView(view);
}
}
package com.fxgraph.cells;
import javafx.scene.control.Label;
import com.fxgraph.graph.Cell;
public class LabelCell extends Cell {
public LabelCell(String id) {
super(id);
Label view = new Label(id);
setView(view);
}
}
package com.fxgraph.cells;
import javafx.scene.control.TitledPane;
import com.fxgraph.graph.Cell;
public class TitledPaneCell extends Cell {
public TitledPaneCell(String id) {
super(id);
TitledPane view = new TitledPane();
view.setPrefSize(100, 80);
setView(view);
}
}
and creating the types
并创建类型
package com.fxgraph.graph;
public enum CellType {
RECTANGLE,
TRIANGLE,
LABEL,
IMAGE,
BUTTON,
TITLEDPANE
;
}
and creating instances depending on the type:
并根据类型创建实例:
...
public void addCell(String id, CellType type) {
switch (type) {
case RECTANGLE:
RectangleCell rectangleCell = new RectangleCell(id);
addCell(rectangleCell);
break;
case TRIANGLE:
TriangleCell circleCell = new TriangleCell(id);
addCell(circleCell);
break;
case LABEL:
LabelCell labelCell = new LabelCell(id);
addCell(labelCell);
break;
case IMAGE:
ImageCell imageCell = new ImageCell(id);
addCell(imageCell);
break;
case BUTTON:
ButtonCell buttonCell = new ButtonCell(id);
addCell(buttonCell);
break;
case TITLEDPANE:
TitledPaneCell titledPaneCell = new TitledPaneCell(id);
addCell(titledPaneCell);
break;
default:
throw new UnsupportedOperationException("Unsupported type: " + type);
}
}
...
and you'll get this
你会得到这个
回答by Balayesu Chilakalapudi
You can use jfreechart
api for generating graph visualization
您可以使用jfreechart
api 生成图形可视化
It provides, Line , Pie, bars. and it is very eary to use.
它提供 Line 、 Pie 、 bar 。而且使用起来很早。
回答by koppor
I would give Prefuxa try. It is a fork of the Prefuseproject.
The original repository starting with JavaFX porting is https://github.com/effrafax/Prefux, but the most maintained fork seems to be the one above (https://github.com/jchildress/Prefux).
从 JavaFX 移植开始的原始存储库是https://github.com/effrafax/Prefux,但维护最多的分支似乎是上面的那个 ( https://github.com/jchildress/Prefux)。
Another attempt to port to JavaFX was started at https://github.com/gedeffe/Prefuse, but it is not active anymore.
另一个移植到 JavaFX 的尝试是在https://github.com/gedeffe/Prefuse开始的,但它不再处于活动状态。
回答by arocketman
I had the same problem, I managed to use the javascript vis.js library along with JavaFX WebView.
我遇到了同样的问题,我设法将 javascript vis.js 库与 JavaFX WebView 一起使用。
You can check it out on github if that's useful to someone: https://github.com/arocketman/VisFX
如果这对某人有用,您可以在 github 上查看:https: //github.com/arocketman/VisFX