原文为我同事发表于我我的网站,今天转发于sf
贴个原文连接css
如何在 纯node环境下(即不使用浏览器或无头浏览器、phantomjs)使用highcharts 生成html文件
因为公司项目须要导出页面成pdf,按照老的导出代码须要通过浏览器生成考虑到有可能会损耗,因此尝试在无浏览器的状况下生成html再导出。由于须要导出的页面须要用到highcharts图表。
所以主要难度在于,不使用浏览器意味着取不到dom,问题变成在获取不到dom的状况下生成highcharts图表。
首先,highcharts的使用是须要传入window对象的html
const Highcharts = require(“highcharts”)(window)
因此在bing上搜索highcharts server side (在服务端渲染highcharts)第一篇就是官网的文章Render charts on the server,主要内容为要在服务器上渲染图表 官方推荐使用 PhantomJS, 无头浏览器,可是除了PhantomJS也可使用Batik and Rhino + env.js 或者 jsdom。node
由于咱们的目标就是不使用浏览器因此变成了Batik and Rhino + env.js 或者 jsdom 2选1,介于第一种貌似很麻烦就选择了使用jsdom来解决没有dom的问题,可是官方还提到若是使用jsdom的话他并无的getBBox方法。git
因而开始查找资料,在参考了node-highcharts.js,以下图(主要解决getBBox的问题)github
在有了jsdom的状况下尝试用highcharts生成svg图表再生成html页面,代码以下:npm
const jsdom = require("jsdom"); const { JSDOM } = jsdom; const { window } = (new JSDOM(``)).window; const { document } = window; const Highcharts = require("highcharts")(window); // Convince Highcharts that our window supports SVG's window.SVGAngle = true; // jsdom doesn't yet support createElementNS, so just fake it up window.document.createElementNS = function (ns, tagName) { var elem = window.document.createElement(tagName); elem.getBBox = function () { return { x: elem.offsetLeft, y: elem.offsetTop, width: elem.offsetWidth, height: elem.offsetHeight }; }; return elem; }; require('highcharts/modules/exporting')(Highcharts); function getChart(option) { const div = document.createElement("div"); div.style.width="1000px"; div.style.height="1000px"; const chart = Highcharts.chart(div, option); return div.outerHTML; } const mock = { chart: { renderer: "SVG", // animation: false, }, title: { text: '123' }, yAxis: { title: { text: '就业人数' } }, series: [{ name: '安装实施人员', data: [43934, 52503, 57177, 69658, 97031, 119931, 137133, 154175] }, { name: '工人', data: [24916, 24064, 29742, 29851, 32490, 30282, 38121, 40434] }], } // 调用 // let chart = getChart(mock).replace(/\"\;/g, `'`); let chart = getChart(mock); let tpl = `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <div style="width:100%;height:100%" id="root"> ${chart} </div> </body> </html>`; console.log(tpl);
至此大概使用jsdom生成highcharts图表再生成html,如上就完成了。
可是运行起来后碰到了一系列的问题,以下图:segmentfault
首先,折线图出不来,再者是上面代码我自定义了div的高宽各位1000 生成的html中div的高宽无变化,最后为生成的legend位置重叠。浏览器
因此如今的主要问题就是highcharts图表的问题,咱们先看看highcharts的配置服务器
发现highcharts图表存在动画效果,而且默认为true,可能就是由于动画效果致使折线图还没出来就被我返回出来了app
所以在图表数据列中都加入animation: false
果真折线图成功出现。
接下去是legend位置错误的问题 以及为何div大小不是我设置的值。
对于legend位置错位的问题,其实最简单的解决方法为使用legend的属性itemDistance 去设置一个图标之间的距离,可是这样的话每一个图表都要单独去设置一个itemDistance 十分麻烦,因此仍是须要找出它什么会错位的问题,本着没有难度也要制造难度原则,读highcharts源码;
由上图大概能够看出highcharts生成图表的步骤大体为生成容器、而后根据属性设置容器大小 内外边距,间距,根据属性获取排列折线图数据,建立坐标轴属性列表,linkSeries主要是跟linkedTo属性有关,最后是开始渲染图表。
在这个过程当中我发现生成的图表大小不受咱们控制的问题大几率会出如今这几步中
通过debugger发现chart.getContainer()即获取容器这步中会使用getChartSize()方法去设置容器的宽高
问题在获取offsetWidth,offsetHeight,scrollWidth,,scrollHeight所有为0
因此解决方法为
Object.defineProperty(div, "offsetWidth", { configurable: true, writable: true, }); Object.defineProperty(div, "offsetHeight", { configurable: true, writable: true, }); Object.defineProperty(div, "scrollWidth", { configurable: true, writable: true, }); Object.defineProperty(div, "scrollHeight", { configurable: true, writable: true, }); div.offsetWidth = 1000; div.offsetHeight = 1000; div.scrollWidth = 1000; div.scrollHeight = 1000; div.style.paddingLeft = 0; div.style.paddingRight = 0; div.style["padding-top"] = 0; div.style["padding-bottom"] = 0;
由于offsetHeight这些属性为只读属性,没法直接赋值因此经过defineProperty改成能够写入
成功使生成的图表大小变为咱们自定义的大小
最后就只剩legend错位的问题
咱们接着看
在render()中找到了生成legend的操做
继续debugger在 legend中找到了生成legend中每一项的 renderItem方法
在其中发现生成每一个图例时他是会提早去计算跟下一个图例之间的距离,以下图:
在没有设置itemWidth 以及并无legendItemWidth,的状况下每一个图例的宽度为,生成的文字element的宽度加上设置的额外每一个图例项之间的宽度。
问题在于bBox的没项值全是0
因此致使图例在计算时没加上字体的宽度
根本缘由是下图 获取element的off各属性均返回0
因此解决方法为
固然咱们并不建议修改源码,所以你能够整个重写 Highcharts.Legend.prototype.renderItem方法将内容所有抄过来 加上我上面那段代码,legend错位问题解决。重写代码以下:
//hack-highcharts.js module.exports = function hackHighcharts(Highcharts) { // 修复legend的itemDistance不能自动计算的问题 Highcharts.Legend.prototype.renderItem = function (item) { /***修改源码开始***/ //自定义须要用到的参数名 var H = Highcharts, merge = H.merge, pick = H.pick; /***修改源码结束***/ var legend = this, chart = legend.chart, renderer = chart.renderer, options = legend.options, horizontal = options.layout === 'horizontal', symbolWidth = legend.symbolWidth, symbolPadding = options.symbolPadding, itemStyle = legend.itemStyle, itemHiddenStyle = legend.itemHiddenStyle, itemDistance = horizontal ? pick(options.itemDistance, 20) : 0, ltr = !options.rtl, bBox, li = item.legendItem, isSeries = !item.series, series = !isSeries && item.series.drawLegendSymbol ? item.series : item, seriesOptions = series.options, showCheckbox = legend.createCheckboxForItem && seriesOptions && seriesOptions.showCheckbox, // full width minus text width itemExtraWidth = symbolWidth + symbolPadding + itemDistance + (showCheckbox ? 20 : 0), useHTML = options.useHTML, fontSize = 12, itemClassName = item.options.className; if (!li) { // generate it once, later move it // Generate the group box, a group to hold the symbol and text. Text // is to be appended in Legend class. item.legendGroup = renderer.g('legend-item') .addClass( 'highcharts-' + series.type + '-series ' + 'highcharts-color-' + item.colorIndex + (itemClassName ? ' ' + itemClassName : '') + (isSeries ? ' highcharts-series-' + item.index : '') ) .attr({ zIndex: 1 }) .add(legend.scrollGroup); // Generate the list item text and add it to the group item.legendItem = li = renderer.text( '', ltr ? symbolWidth + symbolPadding : -symbolPadding, legend.baseline || 0, useHTML ) // merge to prevent modifying original (#1021) .css(merge(item.visible ? itemStyle : itemHiddenStyle)) .attr({ align: ltr ? 'left' : 'right', zIndex: 2 }) .add(item.legendGroup); // Get the baseline for the first item - the font size is equal for // all if (!legend.baseline) { fontSize = itemStyle.fontSize; legend.fontMetrics = renderer.fontMetrics( fontSize, li ); legend.baseline = legend.fontMetrics.f + 3 + legend.itemMarginTop; li.attr('y', legend.baseline); } // Draw the legend symbol inside the group box legend.symbolHeight = options.symbolHeight || legend.fontMetrics.f; series.drawLegendSymbol(legend, item); if (legend.setItemEvents) { legend.setItemEvents(item, li, useHTML); } // add the HTML checkbox on top if (showCheckbox) { legend.createCheckboxForItem(item); } } // Colorize the items legend.colorizeItem(item, item.visible); // Take care of max width and text overflow (#6659) if (!itemStyle.width) { li.css({ width: ( options.itemWidth || options.width || chart.spacingBox.width ) - itemExtraWidth }); } // Always update the text legend.setText(item); // calculate the positions for the next line bBox = li.getBBox(); /***修改源码开始***/ //由于存在可能 text的长度没法取到 现加上判断若是text有内容 可是计算出的宽度为0 //则本身根据字数以及字体大小计算宽度确保 排版正常 if (li.textStr.length > 0 && bBox.width === 0) { const len = li.textStr.length; const fontSize = li.styles.fontSize ? parseInt(li.styles.fontSize.replace("px", "")) : 12; bBox.width = len * fontSize; } /***修改源码结束***/ item.itemWidth = item.checkboxOffset = options.itemWidth || item.legendItemWidth || bBox.width + itemExtraWidth; legend.maxItemWidth = Math.max(legend.maxItemWidth, item.itemWidth); legend.totalItemWidth += item.itemWidth; legend.itemHeight = item.itemHeight = Math.round( item.legendItemHeight || bBox.height || legend.symbolHeight ); } }
在引入highcharts后调用一下hack-highcharts.js
至此全部问题解决,生成图表也是正确的
下面为所有源代码
const jsdom = require("jsdom");const { JSDOM } = jsdom; const { window } = (new JSDOM(``)).window; const { document } = window; const Highcharts = require("highcharts")(window); //将修改renderItem的js引入并传入Highcharts修改其中的renderItem方法 const hackHigcharts = require("./hack-highcharts"); //hack try{ hackHighcharts(Highcharts); }catch(error){ console.error(error); } // Convince Highcharts that our window supports SVG's window.SVGAngle = true; // jsdom doesn't yet support createElementNS, so just fake it up window.document.createElementNS = function (ns, tagName) { var elem = window.document.createElement(tagName); elem.getBBox = function () { return { x: elem.offsetLeft, y: elem.offsetTop, width: elem.offsetWidth, height: elem.offsetHeight }; }; return elem; }; require('highcharts/modules/exporting')(Highcharts); function getChart(option) { const div = document.createElement("div"); Object.defineProperty(div, "offsetWidth", { configurable: true, writable: true, }); Object.defineProperty(div, "offsetHeight", { configurable: true, writable: true, }); Object.defineProperty(div, "scrollWidth", { configurable: true, writable: true, }); Object.defineProperty(div, "scrollHeight", { configurable: true, writable: true, }); div.offsetWidth = 1000; div.offsetHeight = 1000; div.scrollWidth = 1000; div.scrollHeight = 1000; div.style.paddingLeft = 0; div.style.paddingRight = 0; div.style["padding-top"] = 0; div.style["padding-bottom"] = 0; const chart = Highcharts.chart(div, option); return div.outerHTML; } const mock = { chart:{ renderer: "SVG", // animation: false, }, title:{ text: '123' }, yAxis:{ title: { text: '就业人数' } }, series: [{ name: '安装实施人员', animation: false, data: [43934, 52503, 57177, 69658, 97031, 119931, 137133, 154175] }, { name: '工人', animation: false, data: [24916, 24064, 29742, 29851, 32490, 30282, 38121, 40434] }], } // 调用// let chart = getChart(mock).replace(/\"\;/g, `'`); let chart = getChart(mock); let tpl = `<!DOCTYPE html><html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title></head> <body> <div style="width:100%;height:100%" id="root"> ${chart} </div></body> </html>`; console.log(tpl);