| 類別 | 角色 |
|---|---|
| FigureCanvas | SWT Canvas 擴充,承載 Draw2D 圖形樹 |
| Panel | 通用容器圖形,採用 XYLayout 絕對定位 |
| DirectedGraph | 有向圖模型:Node(點)+ Edge(線) |
| Node / Edge | 圖論模型元素,儲存座標與連線關係 |
| Dot (extends Ellipse) | 資料點視覺表現 |
| PolylineConnection | 折線連線,支援錨點自動對齊 |
private List<double[]> generateData() {
List<double[]> series = new ArrayList<>();
// 系列 1:線性遞增
double[] s1 = new double[5];
for (int i = 0; i < 5; i++) s1[i] = i * 10 + 10; // 10,20,30,40,50
// 系列 2:隨機分布
double[] s2 = {20, 150, 5, 90, 35, 20, 150, 5, 45};
// 系列 3:線性遞增
double[] s3 = new double[7];
for (int i = 0; i < 7; i++) s3[i] = i * 20 + 15;
series.add(s1); series.add(s2); series.add(s3);
return series;
}
X 軸:等距分段
private int[] calculateXCoordinates(List<double[]> series) {
int xSpan = 800; // 繪圖區寬度
int maxPoints = series.stream().mapToInt(arr -> arr.length).max().orElse(0);
int segmentWidth = xSpan / maxPoints;
int[] xCoords = new int[maxPoints];
for (int i = 0; i < maxPoints; i++) {
xCoords[i] = (i + 1) * segmentWidth; // 避開 Y 軸區域
}
return xCoords;
}
Y 軸:三值線性插值
原始範圍:[minValue, maxValue] → 目標範圍:[0, ySpan]
公式:y = ySpan - (value - minValue) * ySpan / (maxValue - minValue)
(減去是因為螢幕座標 Y 軸向下)
private int[] calculateYCoordinates(double[] data, int ySpan) {
double min = Arrays.stream(data).min().orElse(0);
double max = Arrays.stream(data).max().orElse(1);
double range = max - min;
int[] yCoords = new int[data.length];
for (int i = 0; i < data.length; i++) {
double ratio = (data[i] - min) / range;
yCoords[i] = (int) (ySpan - ratio * ySpan); // 翻轉 Y 軸
}
return yCoords;
}
public void plot(Composite parent, List<double[]> seriesData) {
// 1. SWT 容器
Composite composite = new Composite(parent, SWT.BORDER);
composite.setLayout(new FillLayout());
// 2. FigureCanvas + Panel
FigureCanvas canvas = new FigureCanvas(composite);
Panel contents = new Panel();
contents.setLayoutManager(new XYLayout());
// 3. 計算繪圖區範圍
Rectangle clientArea = contents.getClientArea();
int xSpan = clientArea.width - 80; // 保留左邊 Y 軸空間
int ySpan = clientArea.height - 60; // 保留底部 X 軸空間
// 4. 繪製座標軸
drawAxes(contents, xSpan, ySpan);
// 5. 建立圖模型並繪製各系列
for (int i = 0; i < seriesData.size(); i++) {
DirectedGraph graph = buildGraph(seriesData.get(i), i, xSpan, ySpan);
drawGraph(contents, graph, getSeriesColor(i));
}
canvas.setContents(contents);
}
private DirectedGraph buildGraph(double[] data, int seriesIndex, int xSpan, int ySpan) {
int[] xCoords = calculateXCoordinates(Collections.singletonList(data));
int[] yCoords = calculateYCoordinates(data, ySpan);
NodeList nodes = new NodeList();
EdgeList edges = new EdgeList();
// 建立節點
for (int i = 0; i < data.length; i++) {
Node node = new Node(new Double(data[i]));
node.x = xCoords[i];
node.y = yCoords[i];
nodes.add(node);
}
// 建立邊(連線)
for (int i = 0; i < nodes.size() - 1; i++) {
Node left = nodes.getNode(i);
Node right = nodes.getNode(i + 1);
Edge edge = new Edge(left, right);
edge.start = new Point(left.x, left.y);
edge.end = new Point(right.x, right.y);
edges.add(edge);
}
DirectedGraph graph = new DirectedGraph();
graph.nodes = nodes;
graph.edges = edges;
new BreakCycles().visit(graph); // 佈局演算法
return graph;
}
private void drawGraph(Panel contents, DirectedGraph graph, Color color) {
// 繪製資料點
for (int i = 0; i < graph.nodes.size(); i++) {
Node node = graph.nodes.getNode(i);
Dot dot = new Dot();
dot.setForegroundColor(color);
dot.setBackgroundColor(color);
node.data = dot; // 儲存視覺物件供連線使用
contents.add(dot);
contents.setConstraint(dot, new Rectangle(node.x - 3, node.y - 3, 6, 6));
}
// 繪製連線
for (int i = 0; i < graph.edges.size(); i++) {
Edge edge = graph.edges.getEdge(i);
PolylineConnection line = new PolylineConnection();
line.setForegroundColor(color);
line.setLineWidth(2);
// 使用橢圓錨點讓連線自動貼合圓點邊緣
line.setSourceAnchor(new EllipseAnchor((Dot) edge.source.data));
line.setTargetAnchor(new EllipseAnchor((Dot) edge.target.data));
contents.add(line);
}
}
XRulerBar:X 軸刻度、標籤、網格線YRulerBar:Y 軸刻度、標籤、網格線Figure,實作 paintFigure(Graphics)掌握「資料→座標→模型→視覺」流程後,易於擴充:
canvas.getUpdateManager().performUpdate()CtClass.detach() 釋放 Javassist 資源(若用於動態類別)