一、准备环境(主要以Centos7环境为准,通过Phantomjs和EChartsConvert工具实现具体功能)
1、下载Phantomjs
-
官网下载:http://phantomjs.org/download.html
国内镜像:http://npm.taobao.org/dist/phantomjs
2、部署Phantomjs到服务器
(1)、将从官网下载的包phantomjs-2.1.1-linux-x86_64.tar.bz2上传到服务器/usr/local/phantomjs目录之下,通过以下命令进行解压
tar -jxvf phantomjs-2.1.1-linux-x86_64.tar.bz2
解压时可能会出现以下错误提示
tar (child): bzip2: Cannot exec: No such file or directory
tar (child): Error is not recoverable: exiting now
tar: Child returned status 2
tar: Error is not recoverable: exiting now
出现该错误,使用以下命令安装bzip2插件,安装完成后再解压
yum install -y bzip2
完成解压后进入/usr/local/phantomjs/phantomjs-2.1.1-linux-x86_64/bin目录,通过./phantomjs -v 命令,查看phantomjs 是否能使用,如果能正常使用,则会输出对应phantomjs版本号,但此处提示以下错误信息
./phantomjs: error while loading shared libraries: libfontconfig.so.1: cannot open shared object file: No such file or directory
提示该错误信息, 需要安装安装fontconfig和freetype依赖,其实phantomjs界面有对应的提示,运行phantomjs需要fontconfig依赖,通过以下命令,安装依赖
yum install fontconfig freetype2
安装依赖完成之后,再到/usr/local/phantomjs/phantomjs-2.1.1-linux-x86_64/bin目录下,执行./phantomjs -v 命令,可看到如下内容,则说明安装成功
(2)、将/usr/local/phantomjs/phantomjs-2.1.1-linux-x86_64/bin添加到环境变量,将export PATH=$PATH:/usr/local/phantomjs/phantomjs-2.1.1-linux-x86_64/bin放到profile文件最后一行,最后重新加载环境变量
vi /etc/profile
export PATH=$PATH:/usr/local/phantomjs/phantomjs-2.1.1-linux-x86_64/bin
source /etc/profile
(3)、安装字体(一定要进行字体安装,否则会导致导出的图片中文无法正常显示)
yum install bitmap-fonts bitmap-fonts-cjk
3、下载EChartsConvert并运行项目
下载地址:https://gitee.com/saintlee/echartsconvert
1、将下载好的包上传到服务器;
2、在echarts-convert.js
同级目录下,运行命令phantomjs echarts-convert.js -s
3、 如果控制台出现echarts-convert server start success. [pid]=xxxx
则表示启动成功,默认端口9090;
4、也可以通过phantomjs echarts-convert.js -s -p 8080
的方式,直接指定端口号
二、在项目中添加以下Maven依赖
org.freemarker
freemarker
2.3.23
com.alibaba
fastjson
1.2.48
org.apache.httpcomponents
httpclient
4.5.12
三、编写工具类
(1)、Http工具类
package com.framework.pie.poi.echarts;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
public class HttpUtil {
public static String post(String url, Map params, String charset)
throws ClientProtocolException, IOException {
String responseEntity = "";
// 创建CloseableHttpClient对象
CloseableHttpClient client = HttpClients.createDefault();
// 创建post方式请求对象
HttpPost httpPost = new HttpPost(url);
// 生成请求参数
List nameValuePairs = new ArrayList<>();
if (params != null) {
for (Entry entry : params.entrySet()) {
nameValuePairs.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
}
}
// 将参数添加到post请求中
httpPost.setEntity(new UrlEncodedFormEntity(nameValuePairs, charset));
// 发送请求,获取结果(同步阻塞)
CloseableHttpResponse response = client.execute(httpPost);
// 获取响应实体
HttpEntity entity = response.getEntity();
if (entity != null) {
// 按指定编码转换结果实体为String类型
responseEntity = EntityUtils.toString(entity, charset);
}
// 释放资源
EntityUtils.consume(entity);
response.close();
return responseEntity;
}
}
(2)、Freemarker工具类
package com.framework.pie.poi.echarts;
import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.util.Map;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
public class FreemarkerUtil {
private static final String path = FreemarkerUtil.class.getClassLoader().getResource("").getPath();
public static String generateString(String templateFileName, String templateDirectory, Map datas)
throws IOException, TemplateException {
Configuration configuration = new Configuration(Configuration.VERSION_2_3_0);
// 设置默认编码
configuration.setDefaultEncoding("UTF-8");
// 设置模板所在文件夹
configuration.setDirectoryForTemplateLoading(new File(path + templateDirectory));
// 生成模板对象
Template template = configuration.getTemplate(templateFileName);
// 将datas写入模板并返回
try (StringWriter stringWriter = new StringWriter()) {
template.process(datas, stringWriter);
stringWriter.flush();
return stringWriter.getBuffer().toString();
}
}
}
(3)、Echarts工具类
package com.framework.pie.poi.echarts;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import org.apache.http.client.ClientProtocolException;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
public class EchartsUtil {
private static String url = "http://192.168.199.138:9090";
private static final String SUCCESS_CODE = "1";
public static String generateEchartsBase64(String option) throws ClientProtocolException, IOException {
String base64 = "";
if (option == null) {
return base64;
}
option = option.replaceAll("\\s+", "").replaceAll("\"", "'");
// 将option字符串作为参数发送给echartsConvert服务器
Map params = new HashMap<>();
params.put("opt", option);
String response = HttpUtil.post(url, params, "utf-8");
// 解析echartsConvert响应
JSONObject responseJson = JSON.parseObject(response);
String code = responseJson.getString("code");
// 如果echartsConvert正常返回
if (SUCCESS_CODE.equals(code)) {
base64 = responseJson.getString("data");
}
// 未正常返回
else {
String string = responseJson.getString("msg");
throw new RuntimeException(string);
}
return base64;
}
}
(4)、柱形图option.ftl(用的springboot项目,直接将模板放置到resources中template文件夹中)
{
title: {
text:'${title}',
x:'middle',
textAlign:'center'
},
xAxis: {
type: 'category',
data: ${categories}
},
yAxis: {
type: 'value'
},
series: [{
data: ${values},
type: 'bar'
}]
}
折线图
{
title: {
text: '折线图测试'
},
tooltip : {
trigger: 'axis'
},
legend: {
data: ['国家级', '省级', '州市级']
},
xAxis: [
{
type: 'category',
boundaryGap: false,
data: ['2019上半年', '2019下半年', '2020上半年', '2020下半年', '2021上半年', '2021下半年'],
axisLabel: {
interval:0,
rotate:30,
}
}
],
yAxis: [
{
type: 'value'
}
],
series: [
{
name: '国家级',
type: 'line',
stack: 'Total',
data: [120, 132, 101, 134, 90, 230, 210]
},
{
name: '省级',
type: 'line',
stack: 'Total',
data: [220, 182, 191, 234, 290, 330, 310]
},
{
name: '州市级',
type: 'line',
stack: 'Total',
data: [150, 232, 201, 154, 190, 330, 410]
}
]
}
饼图
{
tooltip: {
trigger: 'item',
formatter: '{a}
{b}: {c} ({d}%25)'
},
legend: {
orient: 'vertical',
left: 10,
data: ['直接访问', '邮件营销', '联盟广告', '视频广告', '搜索引擎']
},
series: [
{
name: '访问来源',
type: 'pie',
radius: ['50%25', '70%25'],
avoidLabelOverlap: false,
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: '30',
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: [
{value: 335, name: '直接访问'},
{value: 310, name: '邮件营销'},
{value: 234, name: '联盟广告'},
{value: 135, name: '视频广告'},
{value: 1548, name: '搜索引擎'}
]
}
]
}
(5)、测试类信息
package com.framework.pie.poi.echarts;
import com.alibaba.fastjson.JSON;
import freemarker.template.TemplateException;
import org.apache.http.client.ClientProtocolException;
import sun.misc.BASE64Decoder;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
public class TestEcharts02 {
public static void main(String[] args) throws ClientProtocolException, IOException, TemplateException {
// 变量
String title = "水果";
String[] categories = new String[] { "苹果", "香蕉", "西瓜" };
int[] values = new int[] { 3, 2, 1 };
// 模板参数
HashMap datas = new HashMap<>();
datas.put("categories", JSON.toJSONString(categories));
datas.put("values", JSON.toJSONString(values));
datas.put("title", title);
// 生成option字符串
String option = FreemarkerUtil.generateString("option.ftl", "template/echarts", datas);
// 根据option参数
String base64 = EchartsUtil.generateEchartsBase64(option);
System.out.println("BASE64:" + base64);
generateImage(base64, "E:/export/echarts001.png");
}
public static void generateImage(String base64, String path) throws IOException {
BASE64Decoder decoder = new BASE64Decoder();
try (OutputStream out = new FileOutputStream(path)){
// 解密
byte[] b = decoder.decodeBuffer(base64);
for (int i = 0; i < b.length; ++i) {
if (b[i] < 0) {
b[i] += 256;
}
}
out.write(b);
out.flush();
}
}
}
四、效果图
五、需要注意的问题
饼图绘制不了,request时就卡住的问题,程序一直卡在如下界面,不会往下执行
此处并不是饼图绘制不了,而是只要opt中含有’%‘都会挂,原因是作者在代码里执行了两次decodeURIComponent(详情参考echarts-convert.js源码259行),所以’%‘传递时也必需encode两次,否则会造成%后的json串无法被decode导致卡住的问题。
此处可以将’%‘替换为’%25’解决,或是改源码将decodeURIComponent改为一次,暂时没有发现改为一次decode会出现中文问题
- 饼图问题模板
{
tooltip: {
trigger: 'item',
formatter: '{a}
{b}: {c} ({d}%)'
},
legend: {
orient: 'vertical',
left: 10,
data: ['直接访问', '邮件营销', '联盟广告', '视频广告', '搜索引擎']
},
series: [
{
name: '访问来源',
type: 'pie',
radius: ['50%', '70%'],
avoidLabelOverlap: false,
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: '30',
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: [
{value: 335, name: '直接访问'},
{value: 310, name: '邮件营销'},
{value: 234, name: '联盟广告'},
{value: 135, name: '视频广告'},
{value: 1548, name: '搜索引擎'}
]
}
]
}
- 修正后模板(将%用%25替换)
{
tooltip: {
trigger: 'item',
formatter: '{a}
{b}: {c} ({d}%25)'
},
legend: {
orient: 'vertical',
left: 10,
data: ['直接访问', '邮件营销', '联盟广告', '视频广告', '搜索引擎']
},
series: [
{
name: '访问来源',
type: 'pie',
radius: ['50%25', '70%25'],
avoidLabelOverlap: false,
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: '30',
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: [
{value: 335, name: '直接访问'},
{value: 310, name: '邮件营销'},
{value: 234, name: '联盟广告'},
{value: 135, name: '视频广告'},
{value: 1548, name: '搜索引擎'}
]
}
]
}