一、準(zhǔn)備環(huán)境(主要以Centos7環(huán)境為準(zhǔn),通過Phantomjs和EChartsConvert工具實(shí)現(xiàn)具體功能)
1、下載Phantomjs
-
官網(wǎng)下載:http://phantomjs.org/download.html
image.png 國內(nèi)鏡像:http://npm.taobao.org/dist/phantomjs
2、部署Phantomjs到服務(wù)器
(1)、將從官網(wǎng)下載的包phantomjs-2.1.1-linux-x86_64.tar.bz2上傳到服務(wù)器/usr/local/phantomjs目錄之下,通過以下命令進(jìn)行解壓
tar -jxvf phantomjs-2.1.1-linux-x86_64.tar.bz2
解壓時可能會出現(xiàn)以下錯誤提示
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
出現(xiàn)該錯誤,使用以下命令安裝bzip2插件,安裝完成后再解壓
yum install -y bzip2
完成解壓后進(jìn)入/usr/local/phantomjs/phantomjs-2.1.1-linux-x86_64/bin目錄,通過./phantomjs -v 命令,查看phantomjs 是否能使用,如果能正常使用,則會輸出對應(yīng)phantomjs版本號,但此處提示以下錯誤信息
./phantomjs: error while loading shared libraries: libfontconfig.so.1: cannot open shared object file: No such file or directory
提示該錯誤信息, 需要安裝安裝fontconfig和freetype依賴,其實(shí)phantomjs界面有對應(yīng)的提示,運(yùn)行phantomjs需要fontconfig依賴,通過以下命令,安裝依賴
yum install fontconfig freetype2
安裝依賴完成之后,再到/usr/local/phantomjs/phantomjs-2.1.1-linux-x86_64/bin目錄下,執(zhí)行./phantomjs -v 命令,可看到如下內(nèi)容,則說明安裝成功

(2)、將/usr/local/phantomjs/phantomjs-2.1.1-linux-x86_64/bin添加到環(huán)境變量,將export PATH=$PATH:/usr/local/phantomjs/phantomjs-2.1.1-linux-x86_64/bin放到profile文件最后一行,最后重新加載環(huán)境變量
vi /etc/profile
export PATH=$PATH:/usr/local/phantomjs/phantomjs-2.1.1-linux-x86_64/bin
source /etc/profile
(3)、安裝字體(一定要進(jìn)行字體安裝,否則會導(dǎo)致導(dǎo)出的圖片中文無法正常顯示)
yum install bitmap-fonts bitmap-fonts-cjk
3、下載EChartsConvert并運(yùn)行項(xiàng)目
下載地址:https://gitee.com/saintlee/echartsconvert
1、將下載好的包上傳到服務(wù)器;
2、在echarts-convert.js同級目錄下,運(yùn)行命令phantomjs echarts-convert.js -s
3、 如果控制臺出現(xiàn)echarts-convert server start success. [pid]=xxxx則表示啟動成功,默認(rèn)端口9090;
4、也可以通過phantomjs echarts-convert.js -s -p 8080的方式,直接指定端口號
二、在項(xiàng)目中添加以下Maven依賴
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.23</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.48</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.12</version>
</dependency>
三、編寫工具類
(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<String, String> params, String charset)
throws ClientProtocolException, IOException {
String responseEntity = "";
// 創(chuàng)建CloseableHttpClient對象
CloseableHttpClient client = HttpClients.createDefault();
// 創(chuàng)建post方式請求對象
HttpPost httpPost = new HttpPost(url);
// 生成請求參數(shù)
List<NameValuePair> nameValuePairs = new ArrayList<>();
if (params != null) {
for (Entry<String, String> entry : params.entrySet()) {
nameValuePairs.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
}
}
// 將參數(shù)添加到post請求中
httpPost.setEntity(new UrlEncodedFormEntity(nameValuePairs, charset));
// 發(fā)送請求,獲取結(jié)果(同步阻塞)
CloseableHttpResponse response = client.execute(httpPost);
// 獲取響應(yīng)實(shí)體
HttpEntity entity = response.getEntity();
if (entity != null) {
// 按指定編碼轉(zhuǎn)換結(jié)果實(shí)體為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<String, Object> datas)
throws IOException, TemplateException {
Configuration configuration = new Configuration(Configuration.VERSION_2_3_0);
// 設(shè)置默認(rèn)編碼
configuration.setDefaultEncoding("UTF-8");
// 設(shè)置模板所在文件夾
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字符串作為參數(shù)發(fā)送給echartsConvert服務(wù)器
Map<String, String> params = new HashMap<>();
params.put("opt", option);
String response = HttpUtil.post(url, params, "utf-8");
// 解析echartsConvert響應(yīng)
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項(xiàng)目,直接將模板放置到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} <br/>: {c} (u0z1t8os%25)'
},
legend: {
orient: 'vertical',
left: 10,
data: ['直接訪問', '郵件營銷', '聯(lián)盟廣告', '視頻廣告', '搜索引擎']
},
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: '聯(lián)盟廣告'},
{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 };
// 模板參數(shù)
HashMap<String, Object> 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);
// 根據(jù)option參數(shù)
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時就卡住的問題,程序一直卡在如下界面,不會往下執(zhí)行

此處并不是餅圖繪制不了,而是只要opt中含有’%‘都會掛,原因是作者在代碼里執(zhí)行了兩次decodeURIComponent(詳情參考echarts-convert.js源碼259行),所以’%‘傳遞時也必需encode兩次,否則會造成%后的json串無法被decode導(dǎo)致卡住的問題。
此處可以將’%‘替換為’%25’解決,或是改源碼將decodeURIComponent改為一次,暫時沒有發(fā)現(xiàn)改為一次decode會出現(xiàn)中文問題
- 餅圖問題模板
{
tooltip: {
trigger: 'item',
formatter: '{a} <br/>: {c} (u0z1t8os%)'
},
legend: {
orient: 'vertical',
left: 10,
data: ['直接訪問', '郵件營銷', '聯(lián)盟廣告', '視頻廣告', '搜索引擎']
},
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: '聯(lián)盟廣告'},
{value: 135, name: '視頻廣告'},
{value: 1548, name: '搜索引擎'}
]
}
]
}
- 修正后模板(將%用%25替換)
{
tooltip: {
trigger: 'item',
formatter: '{a} <br/>: {c} (u0z1t8os%25)'
},
legend: {
orient: 'vertical',
left: 10,
data: ['直接訪問', '郵件營銷', '聯(lián)盟廣告', '視頻廣告', '搜索引擎']
},
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: '聯(lián)盟廣告'},
{value: 135, name: '視頻廣告'},
{value: 1548, name: '搜索引擎'}
]
}
]
}
