java jfreechart 折線圖數據量大,X 軸刻度密密麻麻顯示不下,或者省略號的解決辦法


1,背景

1,原因

項目開發中,一般情況下,統計圖什么的都是前端來實現的,后端只需要返回數據就好,但是在一些特殊情況下,比如發送郵件報表什么的,這時候不經過前端,不能讓前端渲染之后把圖片傳回來,這時候我們就需要一個比較好的報表繪制工具了

2,依賴

<!-- 報表繪制 -->
<dependency>
    <groupId>org.jfree</groupId>
    <artifactId>jfreechart</artifactId>
    <version>1.5.2</version>
</dependency>

2,折線圖

1,實現折線圖代碼片段

下面就來繪制一個折線圖

package com.hwq.admin.back.service;

import com.hwq.admin.back.config.jfree.IntervalCategoryAxis;
import com.hwq.common.component.service.MokeService;
import com.hwq.common.model.vo.ChartVO;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartUtils;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.CategoryAxis;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.NumberTickUnit;
import org.jfree.chart.labels.StandardCategorySeriesLabelGenerator;
import org.jfree.chart.plot.CategoryPlot;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.renderer.category.LineAndShapeRenderer;
import org.jfree.chart.title.LegendTitle;
import org.jfree.data.category.DefaultCategoryDataset;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.awt.*;
import java.io.ByteArrayOutputStream;
import java.text.DecimalFormat;
import java.util.List;

import static java.awt.BasicStroke.CAP_ROUND;
import static java.awt.BasicStroke.JOIN_ROUND;

@Service
public class LineChartService {

    @Autowired
    private MokeService mokeService; // 用於讀取 自己造的假數據

    public byte[] getLine() {
        List<ChartVO> chartsL = mokeService.mokes("/mokes/走勢圖左.json", ChartVO.class);  // 讀取假數據
        List<ChartVO> chartsR = mokeService.mokes("/mokes/走勢圖右.json", ChartVO.class);  // 讀取假數據

        // 實例化數據集
        DefaultCategoryDataset dataset1 = new DefaultCategoryDataset();
        DefaultCategoryDataset dataset2 = new DefaultCategoryDataset();
        double minValue = chartsL.get(0).getValue(), maxValue = minValue;
        for (ChartVO vo : chartsL) {
            dataset1.addValue(vo.getValue(), "組合", vo.getDate());
            minValue = Math.min(minValue, vo.getValue());
            maxValue = Math.max(maxValue, vo.getValue());
        }
        for (ChartVO vo : chartsR) {
            dataset2.addValue(vo.getValue() - 1, vo.getTitle(), vo.getDate());
            minValue = Math.min(minValue, vo.getValue());
            maxValue = Math.max(maxValue, vo.getValue());
        }

        // 實例化圖表
        JFreeChart chart = ChartFactory.createLineChart("", "", "", null, PlotOrientation.VERTICAL, true, false, false);
        CategoryPlot plot = (CategoryPlot) chart.getPlot();

        setLine(chart, plot);
        setRenderer(plot, dataset1, dataset2);
        setX(plot);
        setY(plot, minValue, maxValue);
        try (
                ByteArrayOutputStream bto = new ByteArrayOutputStream();
        ) {
            ChartUtils.writeChartAsJPEG( bto, chart, 900, 500);
            return bto.toByteArray();
        } catch (Exception ex) {
            ex.printStackTrace();
            throw new RuntimeException(ex.getMessage());
        }
    }

    /**
     * 設置折線圖樣式
     */
    public void setLine(JFreeChart chart, CategoryPlot plot) {
        LegendTitle legend = chart.getLegend();
        legend.setItemFont(new Font("SimHei", Font.PLAIN, 16));       // 設置標示圖的字體
        chart.getTitle().setFont(new Font("SimHei", Font.BOLD, 20));  // 設置標題字體
        // plot.setRangeAxisLocation(AxisLocation.BOTTOM_OR_RIGHT);      // 設置坐標軸在下方或者右側
        plot.setBackgroundPaint(new Color(255, 255, 255));            // 設置繪圖區的顏色
        plot.setRangeGridlinePaint(new Color(200, 200, 200));         // 設置水平方向背景線顏色
        plot.setRangeGridlineStroke(new BasicStroke(1.0f));           // 設置水平方向背景線粗細
        plot.setRangeGridlinesVisible(true);                          // 設置是否顯示水平方向背景線
    }

    /**
     * 設置兩個體系繪制
     */
    public void setRenderer(CategoryPlot plot, DefaultCategoryDataset dataset1, DefaultCategoryDataset dataset2) {
        LineAndShapeRenderer renderer1 = new LineAndShapeRenderer();
        renderer1.setLegendItemLabelGenerator(new StandardCategorySeriesLabelGenerator());
        renderer1.setSeriesStroke(0, new BasicStroke(3.0f, CAP_ROUND, JOIN_ROUND, 2000));
        renderer1.setSeriesPaint(0, new Color(191, 0 ,0));
        renderer1.setSeriesShapesVisible(0, false);

        LineAndShapeRenderer renderer2 = new LineAndShapeRenderer();
        renderer2.setLegendItemLabelGenerator(new StandardCategorySeriesLabelGenerator());
        renderer2.setSeriesStroke(0, new BasicStroke(3.0f, CAP_ROUND, JOIN_ROUND, 2000));
        renderer2.setSeriesPaint(0, new Color(54, 75, 151));
        renderer2.setSeriesShapesVisible(0, false);
        renderer2.setSeriesStroke(1, new BasicStroke(3.0f, CAP_ROUND, JOIN_ROUND, 2000));
        renderer2.setSeriesPaint(1, new Color(181, 125, 69));
        renderer2.setSeriesShapesVisible(1, false);

        plot.setDataset(0, dataset1);
        plot.setRenderer(0, renderer1);

        plot.setDataset(1, dataset2);
        plot.setRenderer(1, renderer2);
    }

    /**
     * 配置 X 軸
     */
    public void setX(CategoryPlot plot) {
        CategoryAxis axis = plot.getDomainAxis();
        axis.setTickLabelFont(new Font("SimHei", Font.BOLD, 16)); // 設置 X 軸每個刻度的字體
        axis.setLabelFont(new Font("SimHei", Font.BOLD, 20));     // 設置 X 軸測的字體
        axis.setAxisLineVisible(false);                           // 設置 X 軸軸線不顯示
        axis.setTickMarksVisible(false);                          // 設置 X 軸刻度不顯示

        // axis.setCategoryLabelPositions(CategoryLabelPositions.DOWN_45);  // 橫坐標旋轉一個角度
        axis.setUpperMargin(0);
        axis.setLowerMargin(0);
    }

    /**
     * 設置 Y 軸
     */
    public void setY(CategoryPlot plot, double minValue, double maxValue) {
        double tickUnit = (maxValue - minValue) / 10;
        // NumberAxis axis = (NumberAxis) plot.getRangeAxis();
        NumberAxis axis1 = new NumberAxis();
        axis1.setNumberFormatOverride(new DecimalFormat("0.0000"));     // 設置數據的展示模式
        axis1.setAutoTickUnitSelection(false);                     // 取消自動分配間距
        axis1.setTickUnit(new NumberTickUnit(tickUnit));           // 設置間隔距離
        axis1.setRange(minValue, maxValue);             // 設置顯示范圍
        // axis1.setUpperMargin(tickUnit1);                            // 設置最大值到頂端的距離
        axis1.setTickLabelFont(new Font("SimHei", Font.BOLD, 16)); // 設置 Y 軸每個刻度的字體
        axis1.setLabelFont(new Font("SimHei", Font.BOLD, 20));     // 設置 Y 軸側的字體
        axis1.setAxisLineVisible(false);                           // 設置 Y 軸軸線不顯示
        axis1.setTickMarksVisible(false);

        NumberAxis axis2 = new NumberAxis();
        axis2.setNumberFormatOverride(new DecimalFormat("0.00%"));    // 設置數據的展示模式
        axis2.setAutoTickUnitSelection(false);                     // 取消自動分配間距
        axis2.setTickUnit(new NumberTickUnit(tickUnit));           // 設置間隔距離
        axis2.setRange(minValue - 1, maxValue - 1);             // 設置顯示范圍
        // axis2.setUpperMargin(tickUnit2);                            // 設置最大值到頂端的距離
        axis2.setTickLabelFont(new Font("SimHei", Font.BOLD, 16)); // 設置 Y 軸每個刻度的字體
        axis2.setLabelFont(new Font("SimHei", Font.BOLD, 20));     // 設置 Y 軸側的字體
        axis2.setAxisLineVisible(false);                           // 設置 Y 軸軸線不顯示
        axis2.setTickMarksVisible(false);

        plot.setRangeAxis(0, axis1);       // 新增一個 Y 軸
        plot.setRangeAxis(1, axis2);       // 新增一個 Y 軸
        plot.mapDatasetToRangeAxis(1, 1);  // 設置為分別渲染,左右坐標軸 不互相影響
    }
}

2,效果

image-20210326191620912

恩!看着很完美,但是隨着數據量一大,坑爹的問題就來了,看下圖

image-20210326192317926

MD,橫坐標不見了,全部變成了省略號,作為一個成熟的報表繪制工具,居然沒有自動踩點的功能,簡直失敗,然后只能去查文檔,查百度,看看有沒有設置步數什么的,結果居然發現沒有,只有一個 時序圖什么的才能設置步數

筆者也試了一下,首先那個需要橫坐標是日期對象,到是能自動踩點了,但是可能自動采到的點並不是你存在的數據,這時我們只能通過設置步數獲取自己想要的坐標,但是設置了步數更坑,第一個坐標居然不見了,此時筆者的內心是崩潰的!!!

3,改源碼

百度了一圈,實在沒辦法,只能從源碼下手了

筆者 debug 了一下,發現 CategoryAxis 對象獲取 X軸 橫坐標點方法是以下這個方法

image-20210326192956538

那就好辦了,繼承,重寫(拷貝原方法,修改)

package com.hwq.admin.back.config.jfree;

import org.jfree.chart.axis.*;
import org.jfree.chart.plot.CategoryPlot;
import org.jfree.chart.text.TextBlock;
import org.jfree.chart.ui.RectangleEdge;

import java.awt.*;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.List;

public class IntervalCategoryAxis extends CategoryAxis {

    private final int stepNum;  // 步數

    public IntervalCategoryAxis(int stepNum) {
        this.stepNum = stepNum;
    }

    /**
     * 重寫獲取橫坐標的方法,根據步數踩點展示,防止橫坐標密密麻麻
     */
    @Override
    public List<Tick> refreshTicks(Graphics2D g2, AxisState state, Rectangle2D dataArea, RectangleEdge edge) {
        List<Tick> ticks = new ArrayList<>();
        if (dataArea.getHeight() <= 0.0 || dataArea.getWidth() < 0.0) {
            return ticks;
        }
        CategoryPlot plot = (CategoryPlot) getPlot();
        List<?> categories = plot.getCategoriesForAxis(this);
        double max = 0.0;
        if (categories != null) {
            CategoryLabelPosition position = super.getCategoryLabelPositions().getLabelPosition(edge);
            int categoryIndex = 0;
            for (Object o : categories) {
                Comparable<?> category = (Comparable<?>) o;
                g2.setFont(getTickLabelFont(category));
                TextBlock label = new TextBlock();
                label.addLine(category.toString(), getTickLabelFont(category), getTickLabelPaint(category));
                if (edge == RectangleEdge.TOP || edge == RectangleEdge.BOTTOM) {
                    max = Math.max(max, calculateTextBlockHeight(label, position, g2));
                } else if (edge == RectangleEdge.LEFT || edge == RectangleEdge.RIGHT) {
                    max = Math.max(max, calculateTextBlockWidth(label, position, g2));
                }
                if (categoryIndex % stepNum == 0) {
                    Tick tick = new CategoryTick(category, label, position.getLabelAnchor(), position.getRotationAnchor(), position.getAngle());
                    ticks.add(tick);
                }
                categoryIndex = categoryIndex + 1;
            }
        }
        state.setMax(max);
        return ticks;
    }

}

主要思路大致就是傳入一個步數,讓列表下標正好是這個步數的整數倍的時候,才像返回值列表里添加坐標,並且添加的時候,把默認的省略號重新賦予正常的數值,最后在調用的地方,使用這個繼承類

image-20210326193558861

4,最后的效果

image-20210326193704067

終於實現了,間隔踩點的功能

心得:看來繪圖渲染什么的,還是得前端來,后端渲染什么的框架真的是不是很靠譜,當然也可能我用的不恰當,如果大家有更好的辦法,歡迎指正


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM