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,效果
恩!看着很完美,但是隨着數據量一大,坑爹的問題就來了,看下圖
MD,橫坐標不見了,全部變成了省略號,作為一個成熟的報表繪制工具,居然沒有自動踩點的功能,簡直失敗,然后只能去查文檔,查百度,看看有沒有設置步數什么的,結果居然發現沒有,只有一個 時序圖什么的才能設置步數。
筆者也試了一下,首先那個需要橫坐標是日期對象,到是能自動踩點了,但是可能自動采到的點並不是你存在的數據,這時我們只能通過設置步數獲取自己想要的坐標,但是設置了步數更坑,第一個坐標居然不見了,此時筆者的內心是崩潰的!!!
3,改源碼
百度了一圈,實在沒辦法,只能從源碼下手了
筆者 debug 了一下,發現 CategoryAxis 對象獲取 X軸 橫坐標點方法是以下這個方法
那就好辦了,繼承,重寫(拷貝原方法,修改)
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;
}
}
主要思路大致就是傳入一個步數,讓列表下標正好是這個步數的整數倍的時候,才像返回值列表里添加坐標,並且添加的時候,把默認的省略號重新賦予正常的數值,最后在調用的地方,使用這個繼承類
4,最后的效果
終於實現了,間隔踩點的功能
心得:看來繪圖渲染什么的,還是得前端來,后端渲染什么的框架真的是不是很靠譜,當然也可能我用的不恰當,如果大家有更好的辦法,歡迎指正