目录
前言
最近开发中遇到需要实现后端导出数据报告文档,文档中附带各种可视化图表(柱状图、饼图等),这些图表在前端非常容易实现,但是java支持绘图的库并不多,于是研究了两天,写成文档帮助后面的小伙伴少走弯路。
JFreeChart
这是博主第一个尝试的库,非常简单,导入依赖直接调用api就可以直接生成图片,如此简便带来的问题也很多,样式难看不说,也无法修改图表结构,无法定制操作。实现后直接舍弃作为备选方案,开始研究其他组件。代码很简单就不贴了,直接上生成好的图:
ECharts-Java
在前端小伙伴的推荐下,博主看了ECharts的官网,发现github上有一个开源的项目ECharts-Java,这正好符合需求,后端可以直接生成的可定制的可视化图表,直接上效果图:
柱状图
饼图
文字重叠可以调整html模板解决
其他
ECharts-Java还支持生成折线图、散点图、雷达图等各种图表,不需要会前端,直接在ECharts官网下载需要的示例模板,剩下的组件会自动生成。
一、使用ECharts-Javc生成HTML文件
1.依赖
<dependency>
<groupId>org.icepear.echarts</groupId>
<artifactId>echarts-java</artifactId>
<version>1.0.7</version>
</dependency>
后面将会介绍每个依赖的作用
2.准备HTML模板
ECharts-Java通过HTML模板生成一个HTML文件,以下使用官方文档提供的HTML模板,模板的文件后缀名必须为hbs,我们在示例中就叫index.hbs。HTML中引用了在线的JS文件,建议下载到本地换为本地的路径。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdnjs.cloudflare.com/ajax/libs/echarts/5.2.2/echarts.min.js"
integrity="sha512-ivdGNkeO+FTZH5ZoVC4gS4ovGSiWc+6v60/hvHkccaMN2BXchfKdvEZtviy5L4xSpF8NPsfS0EVNSGf+EsUdxA=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<title>ECharts-Java Demo</title>
</head>
<body>
<h1>Welcome to ECharts Java</h1>
{{!-- method 2: using Option object--}}
<div id="display-container" style="height: 600px; width: 600px"></div>
</div>
<script>
let chart = echarts.init(document.getElementById("display-container"));
let option = {{{ this }}}
chart.setOption(option);
</script>
</body>
</html>
3.生成HTML图表文件
3.1柱状图
Bar bar = new Bar().addXAxis(
new CategoryAxis()
.setData(new String[] { "中国", "美国", "俄罗斯", "法国", "英国"})
.setBoundaryGap(false))
.setTitle(new Title().setText("五大常任理事国GDP统计").setTextAlign("auto").setLeft("center"))
.addYAxis("万亿美元")
.addSeries(new BarSeries().setName("2023年").setData(new Number[] { 19.37, 27.36, 1.99, 3.02, 3.34}))
.addSeries(new BarSeries().setName("2022年").setData(new Number[] { 17.96, 25.44, 2.24, 2.78, 3.09}))
.setLegend(new Legend().setShow(true).setData(new String[] {"2022年","2023年"}).setAlign("auto").setLeft("certer"));
//定义文件名
String file_name = "pie" + System.currentTimeMillis();
//关闭动画
Option option = pie.getOption();
option.setAnimation(false);
Engine engine = new Engine();
Handlebars handlebars = new Handlebars();
String html = "";
//读模板生成html
try {
//模板路径
Template template = handlebars.compile("template/index.hbs");
html = template.apply(engine.renderJsonOption(pie));
} catch (IOException e) {
System.out.println("模板未找到");
}
//导出html文件
try {
//导出文件路径
FileWriter fileWriter = new FileWriter("html/" + file_name + ".html");
fileWriter.write(html);
fileWriter.close();
} catch (IOException e) {
System.out.println("导出html失败");
e.printStackTrace();
}
3.2饼图
Pie pie = new Pie().setTitle("2023年五常gdp占比")
.setLegend()
.addSeries(new PieDataItem[]{
new PieDataItem().setValue(19.37).setName("中国"),
new PieDataItem().setValue(27.36).setName("美国"),
new PieDataItem().setValue(1.99).setName("俄罗斯"),
new PieDataItem().setValue(3.02).setName("法国"),
new PieDataItem().setValue(3.34).setName("英国"),
});
//后续同柱状图
...
至此,我们成功导出了柱状图文件,但还是没办法用它进行更多的操作,还需要进一步处理。
二、对HTML页面进行截图
1.依赖
<dependency>
<groupId>com.hynnet</groupId>
<artifactId>DJNativeSwing</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.hynnet</groupId>
<artifactId>DJNativeSwing-SWT</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.eclipse.swt.org.eclipse.swt.win32.win32.x86_64.4.3.swt</groupId>
<artifactId>org.eclipse.swt.win32.win32.x86_64</artifactId>
<version>4.3</version>
</dependency>
2.使用Swing对生成的HTML文件进行截图
2.1 SwingUtil工具类
使用Swing可以在JVM中创建一个浏览器,把刚刚生成的HTML文件在浏览器中打开就可以对其进行截图操作并实现输出图片格式的文件。这也是我们在上文中使用option.setAnimation(false)关闭动画的原因。
public class SwingUtil extends JPanel {
private static final long serialVersionUID = 1L;
// 行分隔符
public final static String LS = System.getProperty("line.separator", "\n");
// 文件分割符
public final static String FS = System.getProperty("file.separator", "\\");
// 以javascript脚本获得网页全屏后大小
private final static StringBuffer JS_DIMENSION;
//初始化浏览器视口的尺寸,以便进行页面布局或调整
static {
JS_DIMENSION = new StringBuffer();
JS_DIMENSION.append("var width = 0;").append(LS);
JS_DIMENSION.append("var height = 0;").append(LS);
JS_DIMENSION.append("if(document.documentElement) {").append(LS);
JS_DIMENSION.append(" width = Math.max(width, document.documentElement.scrollWidth);").append(LS);
JS_DIMENSION.append(" height = Math.max(height, document.documentElement.scrollHeight);").append(LS);
JS_DIMENSION.append("}").append(LS);
JS_DIMENSION.append("if(self.innerWidth) {").append(LS);
JS_DIMENSION.append(" width = Math.max(width, self.innerWidth);").append(LS);
JS_DIMENSION.append(" height = Math.max(height, self.innerHeight);").append(LS);
JS_DIMENSION.append("}").append(LS);
JS_DIMENSION.append("if(document.body.scrollWidth) {").append(LS);
JS_DIMENSION.append(" width = Math.max(width, document.body.scrollWidth);").append(LS);
JS_DIMENSION.append(" height = Math.max(height, document.body.scrollHeight);").append(LS);
JS_DIMENSION.append("}").append(LS);
JS_DIMENSION.append("return width + ':' + height;");
}
public SwingUtil(final String url, final String fileName) {
//在jvm中创建一个浏览器用于加载html文件
super(new BorderLayout());
// 创建一个面板用于放置网页浏览器
JPanel webBrowserPanel = new JPanel(new BorderLayout());
// 创建一个网页浏览器对象
final JWebBrowser webBrowser = new JWebBrowser(null);
// 隐藏浏览器工具栏
webBrowser.setBarsVisible(false);
// 导航到指定的URL
webBrowser.navigate(url);
// 将浏览器添加到面板中央
webBrowserPanel.add(webBrowser, BorderLayout.CENTER);
// 将浏览器面板添加到主面板中央
add(webBrowserPanel, BorderLayout.CENTER);
// 创建一个面板用于放置按钮
JPanel panel = new JPanel(new FlowLayout(FlowLayout.CENTER, 4, 4));
// 添加网页浏览器监听器
webBrowser.addWebBrowserListener(new WebBrowserAdapter() {
// 添加网页浏览器监听器
@Override
public void loadingProgressChanged(WebBrowserEvent e) {
// 当加载完毕时,进度100%
if (e.getWebBrowser().getLoadingProgress() == 100) {
// 执行JavaScript获取网页尺寸
String result = (String) webBrowser.executeJavascriptWithResult(JS_DIMENSION.toString());
int index = result == null ? -1 : result.indexOf(":");
// 获取原生组件
NativeComponent nativeComponent = webBrowser.getNativeComponent();
// 获取原始尺寸
Dimension originalSize = nativeComponent.getSize();
// 解析网页尺寸
Dimension imageSize = new Dimension(Integer.parseInt(result.substring(0, index)), Integer.parseInt(result.substring(index + 1)));
// 调整图像尺寸,确保宽度和高度不小于原始尺寸
imageSize.width = Math.max(originalSize.width, imageSize.width + 50);
imageSize.height = Math.max(originalSize.height, imageSize.height + 50);
// 设置原生组件尺寸
nativeComponent.setSize(imageSize);
// 创建一个BufferedImage用于绘制网页截图
BufferedImage image = new BufferedImage(imageSize.width, imageSize.height, BufferedImage.TYPE_INT_RGB);
// 将网页绘制到BufferedImage
nativeComponent.paintComponent(image);
// 调整原生组件尺寸
nativeComponent.setSize(originalSize);
try {
// 输出图像
ImageIO.write(image, "PNG", new File(fileName));
} catch (IOException ex) {
ex.printStackTrace();
}
// 退出操作,这里应该关闭线程
}
}
});
// 将按钮添加到面板底部
add(panel, BorderLayout.SOUTH);
}
}
2.2 在JNI中执行Swing
private static void screenShot(String file_name){
//对html截图
NativeInterface.open();
SwingUtilities.invokeLater(() -> {
// SWT组件转Swing组件,不初始化父窗体将无法启动webBrowser
JFrame frame = new JFrame();
// 实际项目中传入URL参数,根据不同参数截取不同网页快照,保存地址也可以在构造器中多设置一个参数,保存到指定目录
frame.getContentPane().add(new SwingUtil("html/" + file_name + ".html", "png/" + file_name + ".png"), BorderLayout.CENTER);
frame.setSize(1920, 1080);
// 仅初始化,但不显示
frame.invalidate();
frame.pack();
frame.setVisible(false);
});
NativeInterface.runEventPump();
}
2.3 开线程执行截图方法
//对html截图,开线程执行,5秒后结束
Thread thread = new Thread(() -> screenShot(file_name));
thread.start();
try{
Thread.sleep(5000L);
}catch (InterruptedException e){
e.printStackTrace();
}
thread.interrupt();
三、总结
在使用截图前,博主也考虑过直接使用HTML2Image库来直接把HTML转为图片,但是HTML2Image只能将静态的页面,由于ECharts生成的HTML是有JS渲染的页面,所以不可行。Swing虽然耗费资源但是可以实现功能,在不是频繁使用的业务场景可以勉强作为实现方案。
博主没有研究出来怎么在截图完成后自动关闭JNI,欢迎知道怎么实现的小伙伴在评论区留言。