在 HTML 页面渲染好的 HighCharts 图表,可以获取其 SVG 信息并发送后台,进一步创建图表文件(JPG、PNG等)
试图不经前端渲染直接后台生成图表文件,只靠 HighCharts 目前是无法实现的
如果可以在后台模拟前端 HTML 的渲染过程,是否就可以解决问题了呢?答案是肯定的。问题关键在于 How ,怎么做。
PhantomJS (幻影)就是用以实现模拟前端渲染的独立程序,下载 地址 ,这在 HighCharts 官网也是被支持的,相关 链接
![]()
这是摘自 HighCharts 官方一段说明,相关 链接
研究官方的说明文档固然是好的,但是未免枯燥无聊且操作复杂, 能不能傻瓜式一键搞定?
![]()
Espen Hovlandsdal 已经帮我们封装好了!
highcharts-png-renderer ,访问 地址 ,从Git上把项目 clone 下来后,结构如下图所示
![]()
将从 PhantomJS 官网下载的 phantomjs.exe 文件放到 highcharts-png-renderer 子文件路径下
并 执行命令: phantomjs run.js ,如图所示
![]()
命令窗口输出 Listening on port 11942
打开 PostMan 模拟HTTP请求,参数和返回值如下, 惊不惊喜!意不意外!
类型
POST
![]()
URL
参数
![]()
中篇:行百里半九十,下面才是正题
如果你可以将 highcharts-png-renderer 做成服务,随机自启、持续运行、时刻待命,这是最好的解决方案!!!
关于 将bat做成服务 的相关知识,参考 地址
但是,如果想智能化处理渲染器的关停,那么就要自己实现了
1、 Demo 的项目结构
依据业务逻辑划分,将
highcharts-png-renderer重命名为 renderer ,将 renderer 、 phantomjs.exe 、 mould.json 置于文件夹 highcharts-renderer 内,其磁盘路径在 RenderUtil.java 中有使用到,这应写入配置文件中项目结构如下图所示:
![]()
2、 mould.json
HighCharts的Option属性包含很多参数,大多数参数对于一个稳定的项目来说是固定不变的,为了减少代码冗余,建一个模板Option,使用时读取,只将需要修改的少量参数替换掉即可
mould.json模板示例:
{ "global": { "useUTC": false "chart": { "renderTo": "container", "type": "spline", "height": 300, "width": 500, "marginTop": 45, "marginBottom": 45 "title": { "text": "", "style": { "color": "rgb(139, 134, 134)", "font": "bold 1.1em 'Trebuchet MS', Verdana, sans-serif" "credits": { "enabled": false "legend": { "enabled": false "xAxis": { "title": { "enabled": true, "text": "", "align": "high", "style": { "color": "rgb(114, 111, 111)" "labels": { "style": { "color": "rgb(114, 111, 111)" "dateTimeLabelFormats": { "day": "%e. %b", "minute": "%H:%M" "type": "datetime", "showLastLabel": true, "minRange": 60000, "tickPixelInterval": 80, "lineWidth": 1, "lineColor": "#A0A0A0", "gridLineWidth": 0, "gridLineColor": "#E8E8E8" "yAxis": { "title": { "enabled": true, "text": "", "style": { "color": "rgb(114, 111, 111)" "labels": { "style": { "color": "rgb(114, 111, 111)" "minRange": 0.0004, "tickPixelInterval": 25, "lineWidth": 1, "lineColor": "#A0A0A0", "gridLineWidth": 0, "gridLineColor": "#E8E8E8" "plotOptions": { "spline": { "lineWidth": 1, "pointInterval": 60000, "marker": { "enabled": false "series": [ "id": "curve_line", "color": "rgba(0, 0, 255, 1.0)", "data": [ [1538830800000, 9.020376], [1538830801000, 9.020376], [1538834400000, 10.574599], [1538838000000, 6.3690405], [1538841600000, 4.102905] }
3、 RenderUtil.java
渲染器工具类,包含 mould.json 载入、
highcharts-png-rendererimport com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpEntity; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; import java.io.File; import java.io.IOException; import java.util.Date; public class RenderUtil { private static String highChartOptionMouldString = null; private static Process highChartsRendererProcess = null; //以下常量可写入配置文件 private static final String localTempFolder = "D:/temp/charts"; private static final String highChartsRendererPath = "D:/highcharts-renderer"; private static final String highChartsRendererUrl = "http://127.0.0.1:11942/"; * 格式化文件路径 * @param path * @return public static String formatPath(String path) { if (StringUtils.isBlank(path)) { return ""; while (path.indexOf("\\") > -1) { path = path.replace("\\", "/"); while (path.indexOf("//") > -1) { path = path.replace("//", "/"); return path; * 载入HighCharts的模板Option private static void loadHighChartOptionMould() throws IOException { String mouldPath = highChartsRendererPath + "/mould.json"; mouldPath = formatPath(mouldPath); String content = FileUtils.readFileToString(new File(mouldPath), "UTF-8"); if (null != content) { JSONObject mouldJson = JSONObject.parseObject(content);//为了验证格式的正确性 highChartOptionMouldString = mouldJson.toJSONString(); * 启动 HighCharts 渲染器 * @return synchronized public static boolean startRenderer() { if (null != highChartsRendererProcess) { endRenderer(); try { String phantomJs = highChartsRendererPath + "/phantomjs"; phantomJs = formatPath(phantomJs); String runJs = highChartsRendererPath + "/renderer/run.js"; runJs = formatPath(runJs); Runtime rt = Runtime.getRuntime(); highChartsRendererProcess = rt.exec(phantomJs + " " + runJs); (new Robot()).delay(10 * 1000);//延时10s,防止服务尚未启动完全即刻发送HTTP请求 return true; } catch (Exception e) { e.printStackTrace(); return false; * 销毁 HighCharts 渲染器 * @return synchronized public static void endRenderer() { if (null != highChartsRendererProcess) { highChartsRendererProcess.destroy(); highChartsRendererProcess = null; try { (new Robot()).delay(10 * 1000);//延时10s,防止服务尚未完全关闭即刻再启服务 } catch (Exception e) { e.printStackTrace(); * 发送给HighCharts渲染器,取得图表的字节流 * @param param * @return synchronized private static byte[] post2Renderer(String param) { CloseableHttpResponse response = null; try { HttpPost post = new HttpPost(highChartsRendererUrl); if (StringUtils.isNotBlank(param)) { StringEntity entity = new StringEntity(param, "utf-8"); entity.setContentEncoding("UTF-8"); entity.setContentType("application/json"); post.setEntity(entity); // TODO 处理请求超时 CloseableHttpClient client = HttpClients.createDefault(); response = client.execute(post); HttpEntity entity = response.getEntity(); byte[] bytes = EntityUtils.toByteArray(entity); EntityUtils.consume(entity);//关闭流 return bytes; } catch (Exception e) { e.printStackTrace(); return null; } finally { if (null != response) { try { response.close(); } catch (Exception e) { e.printStackTrace(); * 存储图表到本地,返回文件路径 * @param chartTitle 图表标题 * @param xAxisTitle x轴的标题 * @param yAxisTitle y轴的标题 * @param data 数据 * @return synchronized public static String storeChart(String chartTitle, String xAxisTitle, String yAxisTitle, JSONArray data) { if (null == highChartOptionMouldString) { try { loadHighChartOptionMould(); } catch (IOException e) { e.printStackTrace(); return null; //变相实现深度拷贝 JSONObject mouldJson = JSONObject.parseObject(highChartOptionMouldString); JSONObject title = mouldJson.getJSONObject("title"); title.put("text", chartTitle); JSONObject xAxis = mouldJson.getJSONObject("xAxis"); JSONObject xTitle = xAxis.getJSONObject("title"); xTitle.put("text", xAxisTitle); JSONObject yAxis = mouldJson.getJSONObject("yAxis"); JSONObject yTitle = yAxis.getJSONObject("title"); yTitle.put("text", yAxisTitle); mouldJson.put("series", data); if (null != data.get(0)) { JSONObject line1 = data.getJSONObject(0); JSONArray data1 = line1.getJSONArray("data"); if (null != data1 && data1.size() < 2) { JSONObject plotOptions = mouldJson.getJSONObject("plotOptions"); JSONObject spline = plotOptions.getJSONObject("spline"); JSONObject marker = spline.getJSONObject("marker"); marker.put("enabled", true);//显示散点 byte[] bytes = post2Renderer(mouldJson.toJSONString()); if (null == bytes) { return null; try { String localPath = localTempFolder + "/" + (new Date()).getTime() + ".png"; localPath = formatPath(localPath); FileUtils.writeByteArrayToFile(new File(localPath), bytes); return localPath; } catch (Exception e) { e.printStackTrace(); return null; }
4、 RunMain.java
这是一个简单的测试用例主函数,如下
import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import java.util.ArrayList; import java.util.List; public class RunMain { public static void main(String[] args) { d0Render(); * 启动服务生成图表文件到本地 synchronized public static void d0Render() { try { boolean success = RenderUtil.startRenderer(); if (success) { String chartPath = loadData2Chart(); if (null != chartPath) { System.out.println("数据载入成功,图表文件生成后的路径:" + chartPath); } catch (Exception e) { e.printStackTrace(); } finally { RenderUtil.endRenderer(); * 载入数据生成图表,并存储本地 * @return private static String loadData2Chart() { double[] l1p1 = {1538830800000.0, 9.020376};//线1点1 double[] l1p2 = {1538830801000.0, 9.020376}; double[] l1p3 = {1538834400000.0, 10.574599}; double[] l1p4 = {1538838000000.0, 6.3690405}; double[] l1p5 = {1538841600000.0, 4.102905}; double[] l2p1 = {1538830800000.0, 5.020376};//线2点1 double[] l2p2 = {1538834400000.0, 5.574599}; double[] l2p3 = {1538841600000.0, 5.102905}; List<double[]> data1 = new ArrayList<double[]>(); data1.add(l1p1); data1.add(l1p2); data1.add(l1p3); data1.add(l1p4); data1.add(l1p5); // TODO 按x值排序 List<double[]> data2 = new ArrayList<double[]>(); data2.add(l2p1); data2.add(l2p2); data2.add(l2p3); // TODO 处理时区错乱 JSONObject line1 = new JSONObject(); line1.put("id", "blue"); line1.put("color", "rgba(0, 0, 255, 1.0)"); line1.put("data", data1); JSONObject line2 = new JSONObject(); line2.put("id", "red"); line2.put("color", "rgba(255, 0, 0, 1.0)"); line2.put("data", data2); JSONArray lines = new JSONArray(); lines.add(line1); lines.add(line2); return RenderUtil.storeChart("演示图表", "Date", "Value", lines); }
5、实测效果图
控制台输出: 数据载入成功,图表文件生成后的路径:D:/temp/charts/*.png
![]()
下篇:还是有坑在等你
1、渲染器服务的端口占用
(1)人工配置服务端口
文件夹
highcharts-png-renderer内的 config.json 可配置服务端口(2)程序实现灵活检查
//TODO
2、 HighCharts 时区错乱问题
蓝线 峰值点对应x坐标值为 14:00 ,但是我们的输入值 1538834400000.0 毫秒是 22:00 ,正好差8个小时!!
double[] l1p3 = {1538834400000.0, 10.574599};错误原因是渲染器使用了国际时间,东八区的我们自然会比国际时间早8个小时
解决方案:在 mould.json 中配置参数
"global": { "useUTC": false }测试结果:渲染器中 毫无卵用 ,经HTML前端渲染后却是有效的,至于原因嘛。。简单推断可能是
highcharts-png-renderer的服务所采用的 HighCharts 版本太低
再次尝试解决:使用最新 HighCharts 包替换
highcharts-png-renderer服务内 libs 文件夹下的 highcharts.js 和 highcharts-more.js测试结果:毫无变化,偶尔产生纯黑图表
赶时间的我即不想研究源码也不想瞎猜,简单暴力点:
在 RunMain.java 的方法 loadData2Chart 内
// TODO 处理时区错乱处直接再加8小时//手动处理时区错乱问题 for (double[] data : data1) { data[0] += 8 * 60 * 60 * 1000; for (double[] data : data2) { data[0] += 8 * 60 * 60 * 1000; }@All
测试结果:符合预期
![]()
3、渲染器的稳定性问题
测试中发现渲染器经常卡死无响应,严重阻塞执行流程
优化方案:调整类 RenderUtil.java 中的方法 post2Renderer ,增加超时判断并停服重连
/** * 发送给HighCharts渲染器,取得图表的字节流 synchronized private static byte[] post2Renderer(String param) { CloseableHttpResponse response = null; try { HttpPost post = new HttpPost(highChartsRendererUrl); if (StringUtils.isNotBlank(param)) { StringEntity entity = new StringEntity(param, "utf-8"); entity.setContentEncoding("UTF-8"); entity.setContentType("application/json"); post.setEntity(entity); RequestConfig config = RequestConfig.custom() .setSocketTimeout(2 * 60 * 1000).setConnectTimeout(30 * 1000).build(); post.setConfig(config); CloseableHttpClient client = HttpClients.createDefault(); for (int i = 0; i < 3; i++) { try { response = client.execute(post); HttpEntity entity = response.getEntity(); byte[] bytes = EntityUtils.toByteArray(entity); EntityUtils.consume(entity);//关闭流 return bytes; } catch (SocketTimeoutException e) { e.printStackTrace(); endRenderer(); startRenderer(); System.out.println("Try and try but fail: " + param); } catch (Exception e) { e.printStackTrace(); } finally { if (null != response) { try { response.close(); } catch (Exception e) { e.printStackTrace(); return null; }
4、 Data 数据应当是已排序的
类 RunMain.java 的方法 loadData2Chart 中的变量 data1 和 data2 put 进 line 之前,应当是按x值已排序好的,升序或降序都可以,否则图表会出现错乱,如下图所示
double[] l1p1 = {1538830800000.0, 9.020376};//线1点1 double[] l1p2 = {1538830801000.0, 9.020376}; double[] l1p3 = {1538834400000.0, 10.574599}; double[] l1p4 = {1538838000000.0, 6.3690405}; double[] l1p5 = {1538841600000.0, 4.102905}; List<double[]> data1 = new ArrayList<double[]>(); data1.add(l1p1);//顺序不对 data1.add(l1p5); data1.add(l1p4); data1.add(l1p3); data1.add(l1p2);
![]()
解决方案:
Collections.sort(data1, new Comparator<double[]>() { public int compare(double[] p1, double[] p2) { if (p1[0] < p2[0]) { return -1; if (p1[0] > p2[0]) { return 1; return 0; });
附:
pom.xml
<dependencies> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.8.1</version> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.6</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.54</version> </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.6</version> </dependency> </dependencies>
模糊的实现有很多方法,例如均值模糊和中值模糊。均值模糊同意使用了卷积操作,它使用的卷积核中的各个元素都相等,且相加等于1.也就是说,卷积后得到的像素值时期邻域内各个像素值的平均值。而中值模糊则是选择邻域内对所有像素排序后的中值替换到原颜色。一个更高级的模糊方法是高斯模糊。C#代码:using System.Collections; using
这可以通过 6 种方法来实现,下面我来演示一下怎么做。方法一:使用 dmidecode 命令dmidecode 是一个读取电脑 DMI(桌面管理接口Desktop Management Interface)表内容并且以人类可读的格式显示系统硬件信息的工具。(也有人说是读取 SMBIOS —— 系统管理 BIOSSystem Management BIOS )这个表包含系统硬件组件的说明,也包含如序