首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

JFreeChart 蜘蛛網圖(雷達圖)添加刻度標籤完整解決方案

     編輯:Java 相關知識

問題背景

JFreeChart 作為老牌 Java 開源圖表庫,其 SpiderWebPlot(蜘蛛網圖/雷達圖)默認不支持軸向刻度標籤顯示,導致圖表可讀性差,無法直觀展示各維度的數值刻度。本文提供一個通過繼承 SpiderWebPlot 重寫繪製邏輯的完整解決方案,實現自定義刻度線與數值標籤繪製。

效果對比

原版效果(無刻度標籤)

原版蜘蛛網圖

優化後效果(含刻度線與數值標籤)

優化後蜘蛛網圖

核心實現:MySpiderWebPlot 類

通過繼承 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);
        }
    }
}

關鍵算法解析

1. 刻度點座標計算

利用 AffineTransform 進行幾何變換:

  • 縮放變換:根據 i/ticks 比例計算刻度在軸線上的位置
  • 旋轉變換:繞刻度點旋轉 ±90° 獲取刻度線兩端點
  • 平移變換:將相對中心點座標轉換為絕對繪圖座標

2. 智能標籤定位

根據刻度點相對圖表中心的位置(四象限/軸線上),動態調整標籤偏移量,防止標籤與軸線、其他標籤重疊。

3. 單軸繪製數值

通過 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 實例即可無縫升級現有項目。開發者可根據業務需求進一步擴展網格線、多系列支持等高級功能。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved