在 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 )这个表包含系统硬件组件的说明,也包含如序