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,最后的效果
终于实现了,间隔踩点的功能
心得:看来绘图渲染什么的,还是得前端来,后端渲染什么的框架真的是不是很靠谱,当然也可能我用的不恰当,如果大家有更好的办法,欢迎指正