JFreeChart 作為老牌 Java 開源圖表庫,其 SpiderWebPlot(蜘蛛網圖/雷達圖)默認不支持軸向刻度標籤顯示,導致圖表可讀性差,無法直觀展示各維度的數值刻度。本文提供一個通過繼承 SpiderWebPlot 重寫繪製邏輯的完整解決方案,實現自定義刻度線與數值標籤繪製。
原版效果(無刻度標籤)
優化後效果(含刻度線與數值標籤)
通過繼承 SpiderWebPlot 並重寫 drawLabel 方法,在繪製軸標籤的同時繪製刻度線與數值:
public class MySpiderWebPlot extends SpiderWebPlot {
private int ticks = DEFAULT_TICKS;
private static final int DEFAULT_TICKS = 5;
private NumberFormat format = NumberFormat.getInstance();
private static final double PERPENDICULAR = 90;
private static final double TICK_SCALE = 0.015;
private int valueLabelGap = DEFAULT_GAP;
private static final int DEFAULT_GAP = 10;
private static final double THRESHOLD = 15;
public MySpiderWebPlot(CategoryDataset dataset) {
super(dataset);
}
@Override
protected void drawLabel(Graphics2D g2, Rectangle2D plotArea, double value,
int cat, double startAngle, double extent) {
// 1. 先調用父類繪製原有軸標籤
super.drawLabel(g2, plotArea, value, cat, startAngle, extent);
FontRenderContext frc = g2.getFontRenderContext();
double[] transformed = new double[2];
double[] transformer = new double[2];
Arc2D arc1 = new Arc2D.Double(plotArea, startAngle, 0, Arc2D.OPEN);
// 2. 繪製每個刻度
for (int i = 1; i <= ticks; i++) {
Point2D point1 = arc1.getEndPoint();
double deltaX = plotArea.getCenterX();
double deltaY = plotArea.getCenterY();
double labelX = point1.getX() - deltaX;
double labelY = point1.getY() - deltaY;
// 計算刻度縮放比例
double scale = (double) i / (double) ticks;
AffineTransform tx = AffineTransform.getScaleInstance(scale, scale);
AffineTransform pointTrans = AffineTransform.getScaleInstance(scale + TICK_SCALE, scale + TICK_SCALE);
transformer[0] = labelX;
transformer[1] = labelY;
pointTrans.transform(transformer, 0, transformed, 0, 1);
double pointX = transformed[0] + deltaX;
double pointY = transformed[1] + deltaY;
tx.transform(transformer, 0, transformed, 0, 1);
labelX = transformed[0] + deltaX;
labelY = transformed[1] + deltaY;
// 繪製刻度線(垂直於軸線)
double rotated = PERPENDICULAR;
AffineTransform rotateTrans = AffineTransform.getRotateInstance(Math.toRadians(rotated), labelX, labelY);
transformer[0] = pointX;
transformer[1] = pointY;
rotateTrans.transform(transformer, 0, transformed, 0, 1);
double x1 = transformed[0];
double y1 = transformed[1];
rotated = -PERPENDICULAR;
rotateTrans = AffineTransform.getRotateInstance(Math.toRadians(rotated), labelX, labelY);
rotateTrans.transform(transformer, 0, transformed, 0, 1);
Composite saveComposite = g2.getComposite();
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f));
g2.draw(new Line2D.Double(transformed[0], transformed[1], x1, y1));
// 3. 僅在第一個軸繪製數值標籤避免重複
if (startAngle == this.getStartAngle()) {
String label = format.format(((double) i / (double) ticks) * this.getMaxValue());
Rectangle2D labelBounds = getLabelFont().getStringBounds(label, frc);
LineMetrics lm = getLabelFont().getLineMetrics(label, frc);
double ascent = lm.getAscent();
// 智能定位標籤位置避免重疊
if (Math.abs(labelX - plotArea.getCenterX()) < THRESHOLD) {
labelX += valueLabelGap;
labelY += ascent / 2f;
} else if (Math.abs(labelY - plotArea.getCenterY()) < THRESHOLD) {
labelY += valueLabelGap;
} else if (labelX >= plotArea.getCenterX()) {
if (labelY < plotArea.getCenterY()) {
labelX += valueLabelGap;
labelY += valueLabelGap;
} else {
labelX -= valueLabelGap;
labelY += valueLabelGap;
}
} else {
if (labelY > plotArea.getCenterY()) {
labelX -= valueLabelGap;
labelY -= valueLabelGap;
} else {
labelX += valueLabelGap;
labelY -= valueLabelGap;
}
}
g2.setPaint(getLabelPaint());
g2.setFont(getLabelFont());
g2.drawString(label, (float) labelX, (float) labelY);
}
g2.setComposite(saveComposite);
}
}
}
利用 AffineTransform 進行幾何變換:
i/ticks 比例計算刻度在軸線上的位置根據刻度點相對圖表中心的位置(四象限/軸線上),動態調整標籤偏移量,防止標籤與軸線、其他標籤重疊。
通過 startAngle == this.getStartAngle() 判斷僅在第一個軸繪製數值標籤,避免多軸重複顯示相同刻度值。
// 1. 準備數據集
DefaultCategoryDataset dataset = new DefaultCategoryDataset();
dataset.addValue(80, "指標", "性能");
dataset.addValue(60, "指標", "穩定性");
dataset.addValue(90, "指標", "易用性");
// ... 添加更多維度
// 2. 創建自定義繪圖器
MySpiderWebPlot plot = new MySpiderWebPlot(dataset);
plot.setTicks(5); // 設置刻度數(默認5)
plot.setMaxValue(100); // 設置最大值
plot.setLabelFont(new Font("微軟雅黑", Font.PLAIN, 12));
// 3. 創建圖表
JFreeChart chart = new JFreeChart("雷達圖示例", JFreeChart.DEFAULT_TITLE_FONT, plot, true);
// 4. 輸出圖片
ChartUtilities.saveChartAsPNG(new File("radar.png"), chart, 600, 500);
setNumberFormat(NumberFormat) 方法支持百分比、科學計數法等drawLabel 或單獨重寫 drawBackground 繪製同心多邊形網格ChartRenderingInfo 實現鼠標懸停顯示數值通過繼承 SpiderWebPlot 重寫 drawLabel 方法,利用 Java 2D 幾何變換 API 精確計算刻度位置,成功解決了 JFreeChart 雷達圖缺少刻度標籤的缺陷。該方案保持了與原有 API 的兼容性,僅需替換 Plot 實例即可無縫升級現有項目。開發者可根據業務需求進一步擴展網格線、多系列支持等高級功能。