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

JFreeChart 圖表生成服務:從數據到可訪問圖片 URL 的標準化實現

     編輯:Java 相關知識

本文提供一個可直接落地專案的 JFreeChart 圖表生成服務類,封裝了「數據集構建 → 圖表客製化 → PNG 暫存 → URL 返回」的完整流程。對比同類教程,本實現強化了參數校驗、異常處理、字體跨平台相容、資源釋放四大工程化細節,適合作為專案內部共用的 ChartService 基礎組件。

核心代碼:ChartService.java

package com.example.common.chart;

import org.jfree.chart.*;
import org.jfree.chart.axis.*;
import org.jfree.chart.labels.StandardCategoryItemLabelGenerator;
import org.jfree.chart.plot.CategoryPlot;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.renderer.category.LineAndShapeRenderer;
import org.jfree.chart.servlet.ServletUtilities;
import org.jfree.chart.title.TextTitle;
import org.jfree.data.category.DefaultCategoryDataset;
import org.jfree.ui.RectangleEdge;
import org.jfree.ui.RectangleInsets;

import java.awt.*;
import java.io.IOException;
import java.util.Objects;

public class ChartService {

    /** 預設輸出尺寸 */
    private static final int DEFAULT_WIDTH = 700;
    private static final int DEFAULT_HEIGHT = 350;

    /** 跨平台中文字體策略:Windows 用微軟正黑體,Linux/Docker 用 Noto Sans CJK */
    private static final Font CHINESE_FONT_BOLD = resolveChineseFont(Font.BOLD);
    private static final Font CHINESE_FONT_PLAIN = resolveChineseFont(Font.PLAIN);

    private static Font resolveChineseFont(int style) {
        String os = System.getProperty("os.name").toLowerCase();
        String family = os.contains("win") ? "Microsoft JhengHei" : "Noto Sans CJK TC";
        return new Font(family, style, 12);
    }

    /**
     * 生成折線圖並返回 DisplayChart Servlet 可訪問的 URL
     *
     * @param seriesNames 系列名稱 (圖例),長度 = scores 第 1 維
     * @param scores      數據矩陣 [seriesCount][categoryCount]
     * @param categories  類別標籤 (X 軸),長度 = scores 第 2 維
     * @param title       圖表標題
     * @return 相對路徑 URL,如 "/ctx/DisplayChart?filename=chart_abc123.png"
     * @throws IllegalArgumentException 參數維度不匹配
     * @throws ChartGenerationException   圖表生成/暫存失敗
     */
    public String generateLineChartUrl(String[] seriesNames, double[][] scores,
                                       String[] categories, String title) {
        // 1. 參數防禦性檢查
        Objects.requireNonNull(seriesNames, "seriesNames 不可為 null");
        Objects.requireNonNull(scores, "scores 不可為 null");
        Objects.requireNonNull(categories, "categories 不可為 null");
        if (seriesNames.length != scores.length) {
            throw new IllegalArgumentException("seriesNames 長度(" + seriesNames.length +
                    ") 必須等於 scores 第 1 維長度(" + scores.length + ")");
        }
        for (int i = 0; i < scores.length; i++) {
            if (scores[i].length != categories.length) {
                throw new IllegalArgumentException("系列[" + i + "] 數據長度(" + scores[i].length +
                        ") 必須等於 categories 長度(" + categories.length + ")");
            }
        }

        // 2. 構建數據集
        DefaultCategoryDataset dataset = new DefaultCategoryDataset();
        for (int s = 0; s < seriesNames.length; s++) {
            for (int c = 0; c < categories.length; c++) {
                String cat = truncate(categories[c], 10); // 避免標籤過長
                dataset.addValue(scores[s][c], seriesNames[s], cat);
            }
        }

        // 3. 建立基礎圖表
        JFreeChart chart = ChartFactory.createLineChart(
                title, null, null, dataset,
                PlotOrientation.VERTICAL, true, true, false);

        // 4. 應用統一樣式模板
        applyCommonStyle(chart);

        // 5. 針對系列客製化渲染器 (線型、顏色、資料標籤)
        customizeRenderer(chart.getCategoryPlot(), seriesNames.length);

        // 6. 輸出暫存 PNG 並取得檔名
        String fileName;
        try {
            fileName = ServletUtilities.saveChartAsPNG(chart, DEFAULT_WIDTH, DEFAULT_HEIGHT, null, null);
        } catch (IOException e) {
            throw new ChartGenerationException("圖表 PNG 生成失敗", e);
        }

        // 7. 組裝 URL (需根據實際部署的 context-path 調整前綴)
        String contextPath = getContextPath(); // 可從 ThreadLocal/RequestContextHolder 獲取
        return contextPath + "/DisplayChart?filename=" + fileName;
    }

    /** 統一樣式模板:背景、圖例、標題、坐標軸、網格 */
    private void applyCommonStyle(JFreeChart chart) {
        chart.setBackgroundPaint(Color.WHITE);
        chart.getLegend().setPosition(RectangleEdge.RIGHT);
        chart.getLegend().setItemFont(CHINESE_FONT_PLAIN);
        chart.setTitle(new TextTitle(chart.getTitle().getText(), CHINESE_FONT_BOLD.deriveFont(16f)));

        CategoryPlot plot = chart.getCategoryPlot();
        plot.setBackgroundPaint(Color.WHITE);
        plot.setBackgroundAlpha(0.85f);
        plot.setDomainGridlinesVisible(true);
        plot.setRangeGridlinesVisible(true);
        plot.setDomainGridlinePaint(new Color(0xE0E0E0));
        plot.setRangeGridlinePaint(new Color(0xE0E0E0));
        plot.setAxisOffset(new RectangleInsets(0, 0, 0, 12));
        plot.setNoDataMessage("無資料可顯示");

        // X 軸
        CategoryAxis domainAxis = plot.getDomainAxis();
        domainAxis.setTickLabelFont(CHINESE_FONT_PLAIN);
        domainAxis.setLabelFont(CHINESE_FONT_BOLD);
        domainAxis.setMaximumCategoryLabelLines(2);
        domainAxis.setCategoryMargin(0.15);

        // Y 軸
        NumberAxis rangeAxis = (NumberAxis) plot.getRangeAxis();
        rangeAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
        rangeAxis.setAutoRangeIncludesZero(true);
        rangeAxis.setUpperMargin(0.12);
        rangeAxis.setTickLabelFont(CHINESE_FONT_PLAIN);
        rangeAxis.setLabelFont(CHINESE_FONT_BOLD);
    }

    /** 系列渲染器客製化:形狀、線型、資料標籤 */
    private void customizeRenderer(CategoryPlot plot, int seriesCount) {
        LineAndShapeRenderer renderer = (LineAndShapeRenderer) plot.getRenderer();
        renderer.setShapesVisible(true);
        renderer.setBaseItemLabelsVisible(true);
        renderer.setBaseItemLabelGenerator(new StandardCategoryItemLabelGenerator());
        renderer.setBaseItemLabelFont(CHINESE_FONT_PLAIN.deriveFont(10f));

        // 預設色盤 (可外部化配置)
        Color[] palette = {new Color(0x1F77B4), new Color(0xFF7F0E), new Color(0x2CA02C),
                new Color(0xD62728), new Color(0x9467BD), new Color(0x8C564B)};

        for (int i = 0; i < seriesCount; i++) {
            renderer.setSeriesPaint(i, palette[i % palette.length]);
            renderer.setSeriesShape(i, ShapeUtilities.createRegularCross(3, 1)); // 十字標記

            // 線型區分:0 實線, 1 長虛線, 2 短虛線, 3 點線
            float[] dash = switch (i % 4) {
                case 0 -> null; // 實線
                case 1 -> new float[]{10f, 6f};
                case 2 -> new float[]{6f, 6f};
                default -> new float[]{2f, 4f};
            };
            if (dash != null) {
                renderer.setSeriesStroke(i, new BasicStroke(2.0f, BasicStroke.CAP_ROUND,
                        BasicStroke.JOIN_ROUND, 1.0f, dash, 0.0f));
            } else {
                renderer.setSeriesStroke(i, new BasicStroke(2.0f));
            }
        }
    }

    /** 截斷過長標籤 */
    private String truncate(String s, int maxLen) {
        return (s != null && s.length() > maxLen) ? s.substring(0, maxLen) + "…" : s;
    }

    /** 獲取 Web 應用上下文路徑 (需根據框架實現,此處示意) */
    private String getContextPath() {
        // Spring: ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest().getContextPath()
        // 簡易版假設部署在根路徑或已知 context
        return "/myapp"; // 請替換為動態獲取
    }

    /** 業務異常包裝 */
    public static class ChartGenerationException extends RuntimeException {
        public ChartGenerationException(String msg, Throwable cause) { super(msg, cause); }
    }
}

使用範例

@RestController
@RequestMapping("/api/chart")
public class ChartController {
    private final ChartService chartService = new ChartService();

    @GetMapping("/score-trend")
    public Result<String> scoreTrend() {
        String[] series = {"A班", "B班", "C班"};
        double[][] data = {
                {82, 88, 79, 91, 85},
                {78, 85, 80, 88, 82},
                {85, 90, 84, 92, 88}
        };
        String[] cats = {"第1週", "第2週", "第3週", "第4週", "第5週"};

        String url = chartService.generateLineChartUrl(series, data, cats, "五週成績趨勢對比");
        return Result.success(url); // 前端直接 
    }
}

工程化關鍵點對照表

維度普通教程本實現增強
參數校驗無維度匹配、非空檢查、具體錯誤訊息
異常處理printStackTrace自定義 ChartGenerationException 統一拋出,利於全局異常處理器捕獲
字體跨平台寫死 "微軟正黑體"OS 判斷動態選擇 Microsoft JhengHei / Noto Sans CJK TC
資源管理忽略依賴 ServletUtilities 內部暫存機制;高併發建議改 ByteArrayOutputStream 直輸出
樣式複用硬編碼在方法中拆分 applyCommonStyle / customizeRenderer 便於繼承覆寫
ContextPath 動態化寫死 "/projectname"預留 getContextPath() 接口,配合 Spring RequestContextHolder 實現

部署檢查清單

  1. web.xml 已註冊 org.jfree.chart.servlet.DisplayChart,url-pattern 為 /DisplayChart。
  2. Linux/Docker 環境 安裝中文字體:apt-get install -y fontconfig fonts-noto-cjk 或掛載字體卷。
  3. 暫存目錄權限:Servlet 容器寫入權限 (通常為 java.io.tmpdir 或 Session 目錄)。
  4. 高併發場景:考慮改為 response.getOutputStream() 直接輸出 PNG 流,或引入 Caffeine 緩存熱門圖表。

結語

將 JFreeChart 的繁瑣 API 封裝為 單一職責、可測試、可擴展的 ChartService,不僅能消除 Controller 層的樣板代碼,更能統一專案圖表風格、降低維護成本。本實現作為起點,可按業務擴展支援長條圖、餅圖、散佈圖等類型,構建專屬的圖表組件庫。

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