Java+PhantomJs实现后台生成Echarts图片(完整源码)
- 需求
- 效果图
- 实现
- 引入依赖
- 引入js文件
- 拼接option (完整代码如下)
- 生成图片
- 附上拼接option时涉及的实体
- 生成的折线图样式
- 遇到的坑
需求
生成折线图定期发送邮件。
效果图
实现
引入依赖
<dependency>
<groupId>com.github.abel533</groupId>
<artifactId>ECharts</artifactId>
<version>3.0.0.6</version>
</dependency>
引入js文件
- echarts.min.js
点击下载 - jquery-3.5.1.min.js
点击下载 - echarts-convert.js 生成echart图片的脚本(完整代码如下)
(function () {
var system = require('system');
var fs = require('fs');
var config = {
// define the location of js files
//这样写要求这三个js在同一个文件夹下
JQUERY: 'jquery-3.5.1.min.js',
//ESL: 'esl.js',
ECHARTS: 'echarts.min.js',
// default container width and height
DEFAULT_WIDTH: '400',
DEFAULT_HEIGHT: '600'
}, parseParams, render, pick, usage;
usage = function () {
console.log("\nUsage: phantomjs echarts-convert.js -options options -outfile filename -width width -height height"
+ "OR"
+ "Usage: phantomjs echarts-convert.js -infile URL -outfile filename -width width -height height\n");
};
pick = function () {
var args = arguments, i, arg, length = args.length;
for (i = 0; i < length; i += 1) {
arg = args[i];
if (arg !== undefined && arg !== null && arg !== 'null' && arg != '0') {
return arg;
}
}
};
parseParams = function () {
var map = {}, i, key;
if (system.args.length < 2) {
usage();
phantom.exit();
}
for (i = 0; i < system.args.length; i += 1) {
if (system.args[i].charAt(0) === '-') {
key = system.args[i].substr(1, i.length);
if (key === 'infile') {
// get string from file
// force translate the key from infile to options.
key = 'options';
try {
map[key] = fs.read(system.args[i + 1]).replace(/^\s+/, '');
} catch (e) {
console.log('Error: cannot find file, ' + system.args[i + 1]);
phantom.exit();
}
} else {
map[key] = system.args[i + 1].replace(/^\s+/, '');
}
}
}
return map;
};
render = function (params) {
var page = require('webpage').create(), createChart;
var bodyMale = config.SVG_MALE;
page.onConsoleMessage = function (msg) {
console.log(msg);
};
page.onAlert = function (msg) {
console.log(msg);
};
createChart = function (inputOption, width, height,config) {
var counter = 0;
function decrementImgCounter() {
counter -= 1;
if (counter < 1) {
console.log(messages.imagesLoaded);
}
}
function loadScript(varStr, codeStr) {
var script = $('<script>').attr('type', 'text/javascript');
script.html('var ' + varStr + ' = ' + codeStr);
document.getElementsByTagName("head")[0].appendChild(script[0]);
if (window[varStr] !== undefined) {
console.log('Echarts.' + varStr + ' has been parsed');
}
}
function loadImages() {
var images = $('image'), i, img;
if (images.length > 0) {
counter = images.length;
for (i = 0; i < images.length; i += 1) {
img = new Image();
img.onload = img.onerror = decrementImgCounter;
img.src = images[i].getAttribute('href');
}
} else {
console.log('The images have been loaded');
}
}
// load opitons
if (inputOption != 'undefined') {
// parse the options
loadScript('options', inputOption);
// disable the animation
options.animation = false;
}
// we render the image, so we need set background to white.
$(document.body).css('backgroundColor', 'white');
var container = $("<div>").appendTo(document.body);
container.attr('id', 'container');
container.css({
width: width,
height: height
});
// render the chart
var myChart = echarts.init(container[0]);
myChart.setOption(options);
// load images
loadImages();
return myChart.getDataURL();
};
// parse the params
page.open("about:blank", function (status) {
// inject the dependency js
page.injectJs(config.ESL);
page.injectJs(config.JQUERY);
page.injectJs(config.ECHARTS);
var width = pick(params.width, config.DEFAULT_WIDTH);
var height = pick(params.height, config.DEFAULT_HEIGHT);
// create the chart
var base64 = page.evaluate(createChart, params.options, width, height,config);
fs.write("base64.txt",base64);
// define the clip-rectangle
page.clipRect = {
top: 0,
left: 0,
width: width,
height: height
};
// render the image
page.render(params.outfile);
console.log('render complete:' + params.outfile);
// exit
phantom.exit();
});
};
// get the args
var params = parseParams();
// validate the params
if (params.options === undefined || params.options.length === 0) {
console.log("ERROR: No options or infile found.");
usage();
phantom.exit();
}
// set the default out file
if (params.outfile === undefined) {
var tmpDir = fs.workingDirectory + '/tmp';
// exists tmpDir and is it writable?
if (!fs.exists(tmpDir)) {
try {
fs.makeDirectory(tmpDir);
} catch (e) {
console.log('ERROR: Cannot make tmp directory');
}
}
params.outfile = tmpDir + "/" + new Date().getTime() + ".png";
}
// render the image
render(params);
}());
拼接option (完整代码如下)
public String getOption(String title, String ytitle, List<String> categorie, List<Serie> series) {
GsonOption option = new GsonOption();
// 大标题、位置
option.title().text(title).x("left");
List<String> types = series.stream().map(Serie::getName).collect(Collectors.toList());
// 设置图例
option.legend().data(types.toArray()).x("center");
// 在轴上触发提示数据
option.tooltip().trigger(Trigger.axis);
// 工具栏 显示,保存为图片 实际是一个下载图标不需要所以注掉了
// option.toolbox().show(true).feature(Tool.saveAsImage);
// x轴
com.github.abel533.echarts.axis.CategoryAxis category =
new com.github.abel533.echarts.axis.CategoryAxis();
category.data(categorie.toArray());
// 起始和结束两端空白策略
category.boundaryGap(false);
// option.grid().x(180);
// y轴
com.github.abel533.echarts.axis.ValueAxis ecsY =
new com.github.abel533.echarts.axis.ValueAxis();
ecsY.name(ytitle).position("left").axisLine().lineStyle().color(Color.BLACK);
//不允许出现小数位,坐标轴最小值1
ecsY.minInterval(1);
TextStyle textStyle = new TextStyle();
textStyle.setFontSize(18);
// 循环数据
for (int i = 0; i < types.size(); i++) {
Line line = new Line();
String type = types.get(i);
line.name(type);
int size = series.get(i).getData().size();
for (int j = 0; j < size; j++) {
line.data(series.get(i).getData().get(j));
}
// 设置平滑线条
// line.smooth(true);
option.series(line);
}
// 横轴为类别、纵轴为值
option.xAxis(category);
option.yAxis(ecsY);
// 处理没有数据的情况
NoDataLoadingOption noDataLoadingOption = new NoDataLoadingOption();
Effect effect = new Effect();
effect.setShow(true);
EffectOption effectOption = new EffectOption();
effectOption.setEffect(0);
noDataLoadingOption.setEffect(effect);
noDataLoadingOption.setText("暂无数据");
option.noDataLoadingOption(noDataLoadingOption);
String optionStr = option.toString().replace(" ", "");
log.info(optionStr);
return optionStr;
}
生成图片
public String generateEChart(String options, String imgName, int width, int height) {
String path = "forum-app/src/main/resources/images/" + imgName;
try {
// 文件路径(路径+文件名)
File file = new File(path);
// 文件不存在则创建文件,先创建目录
if (!file.exists()) {
File dir = new File(file.getParent());
dir.mkdirs();
file.createNewFile();
}
// echarts-convert.js 脚本所在 path
//本地这样写是没问题的
String JSpath = "forum-common/src/main/resources/js/echarts-convert.js";
log.info("start cmd exec generate png");
String cmd =
"phantomjs "
+ JSpath
+ " -options "
+ options
+ " -outfile "
+ path
+ " -width "
+ width
+ " -height "
+ height
+ "";
Process process = Runtime.getRuntime().exec(cmd);
process.waitFor();
log.info("end cmd exec generate png");
BufferedReader input = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line = "";
while ((line = input.readLine()) != null) {
log.info(line);
}
input.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
return path;
}
}
附上拼接option时涉及的实体
public class Serie implements Serializable {
private static final long serialVersionUID = 1L;
private String name;// 名字
private Vector<Object> data;// 数据值ֵ
/**
*
* @param name
* 名称(线条名称)
* @param array
* 数据(线条上的所有数据值)
*/
public Serie(String name, Object[] array) {
this.name = name;
if (array != null) {
data = new Vector<Object>(array.length);
for (int i = 0; i < array.length; i++) {
data.add(array[i]);
}
}
}
}
生成的折线图样式
遇到的坑
本地测试一切很完美,高高兴兴部署到linux上测试,效果如下。
生成的都是0kb的图片,心凉了一半。
赶紧去看看日志
{"title":{"text":"问答浏览量","x":"left"},"toolbox":{"feature":{"saveAsImage":{"show":true,"title":"保存为图片","type":"png","lang":["点击保存"]}},"show":true},"tooltip":{"trigger":"axis"},"legend":{"data":["TRANTOR","GAIA","GAIAAPP"],"x":"center"},"xAxis":[{"type":"category","boundaryGap":false,"data":["2021-09-06","2021-09-07","2021-09-08","2021-09-09","2021-09-10","2021-09-11","2021-09-12"]}],"yAxis":[{"type":"value","position":"left","name":"","axisLine":{"lineStyle":{"color":{"value":-16777216,"falpha":0.0}}}}],"series":[{"name":"TRANTOR","type":"line","data":[0,0,0,2,0,3,2]},{"name":"GAIA","type":"line","data":[0,0,0,0,0,0,0]},{"name":"GAIAAPP","type":"line","data":[0,0,0,0,0,0,0]}],"noDataLoadingOption":{"text":"暂无数据","effect":{"show":true}}}
2021-09-12 19:19:37.340 [1053940:http-nio-8080-exec-10] INFO pub.developers.forum.common.support.GenerateEChartUtil - start cmd exec generate png
2021-09-12 19:19:37.425 [1054025:http-nio-8080-exec-10] INFO pub.developers.forum.common.support.GenerateEChartUtil - end cmd exec generate png
日志看起来一切都很正常,心又凉了一大半。
稳住,不能慌!
排查思路:
- 执行命令写的是相对路径,linux环境上可能找不到我安装的phantomjs执行文件
- echarts-convert.js文件没找到。
解决方案:
配置环境变量,将js文件拷贝至容器目录下。
附上dockerfile添加内容:
WORKDIR /
COPY ./forum-common/src/main/resources/js/echarts-convert.js /app/echarts-convert.js
COPY ./forum-common/src/main/resources/js/echarts.min.js /app/echarts.min.js
COPY ./forum-common/src/main/resources/js/jquery-3.5.1.min.js /app/jquery-3.5.1.min.js
#安装bzip2解压PhantomJS压缩包
RUN yum -y install bzip2
#Echarts支持中文
RUN yum -y install bitmap-fonts bitmap-fonts-cjk
# PhantomJS
ENV PHANTOMJS_VERSION 2.1.1
RUN wget --no-check-certificate -q -O - https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-$PHANTOMJS_VERSION-linux-x86_64.tar.bz2 | tar xjC /opt
RUN ln -s /opt/phantomjs-$PHANTOMJS_VERSION-linux-x86_64/bin/phantomjs /usr/bin/phantomjs
ENV PJS_HOME=/opt/phantomjs-$PHANTOMJS_VERSION-linux-x86_64
ENV PATH=$PATH:$PJS_HOME/bin
修改generateEChart()方法中JSpath
String JSpath = "/app/echarts-convert.js";
修改echarts-convert.js 中其他两个js的路径
发布,完美!