介绍

做过数据可视化的同学对于HIGHCHARTS可能并不陌生,甚至是家族的其他两个成员 HIGHSTOCKHIGHMAPS 也使用过

它对新老浏览器的兼容性和响应式特性是我们偏爱使用它的主要原因,当然文档齐全和demo丰富也是它的特点

下面是它的兼容性表格:

Brand Versions supported
Internet Explorer 6.0 +
Firefox 2.0 +
Chrome 1.0 +
Safari 4.0 +
Opera 9.0 +
iOS (Safari) 3.0 +
Android Browser 2.0 +

基本覆盖了主要的桌面和移动浏览器,即使在天朝也可以放心使用,在IE9以下版本的浏览器会采用VML的降级方式渲染图形,在Android 2.X的原生浏览器会使用Canvas渲染图形

使用非商用证书可以免费使用 Highchart 的产品,如果商用的话,就要花费一笔购买许可证书之后再使用

使用API绘制图表、地图等等再这里就不细说了,绘制自定义图形是重点

当然,使用 d3, raphael, sylvester, ocanvas, two.js, svg.js, snap.svg 等图形库也可以帮助你绘制自定义图形,但是鉴于 HIGHCHART 的诸多优点,我习惯使用它

进入正题

Renderder

Renderer 这个构造器是绘制的核心,创建这个构造器的实例可以采用下面的方法

1
var renderer = new Highcharts.Renderer(parentNode, width, height);

当调用 highcharts 方法时内部的 this.renderer 就是一个 Renderer 构造器的实例

1
2
3
4
5
6
7
8
9
10
11
$('#container').highcharts({
chart: {
backgroundColor: 'white',
events: {
load: function () {
// Draw the flow chart
var ren = this.renderer,
}
}
}
});

在 highcharts-core 源码里我们可以看到,在Renderder的原型方法中,我们可以独立于 Chart 单独绘图的方法包括:

  • path : 绘制路径
  • circle : 绘制圆形
  • arc : 绘制拱形
  • rect : 绘制矩形
  • g : 创建用于把相关元素进行组合的容器元素
  • image : 显示图片
  • text : 显示文字
  • label : 绘制一个具有阴影、边框、(渐变)背景色的 Label
  • button: 绘制一个具有相应状态(hover, active .etc)的按钮,并可以绑定点击事件的回调函数

element

Element 构造器是 highchart 里 svg 元素 的 constructor,它也提供了一些实用的原型方法供实例调用:

  • add : 像渲染画布中添加元素
  • animate : 对 svg/vml 元素使用动画效果,方法同 jQuery animate() 方法
  • attr : 为 svg/vml 元素添加属性,方法同 jQuery attr 方法
  • css : 不罗嗦。。同 jQuery
  • destroy : 删除元素并释放内存
  • on : 为元素绑定事件处理函数
  • toFront : 前置元素

一言不合, 开始编码

以NCZ大师那篇划时代的前后端分离文章的配图为目标,用 Renderer 实例来绘制相同的原理图

svg demo

为了节省时间只画了上半部分

绘制原理:

添加前端部分的分组

1
2
//declare Front-End part
var frontEnd = render.g().add();

rect() 方法添加前端部分的底层画布,其实就是先声明一个矩形,由于 SVG 没有 z-index 的概念,因此绘制的顺序决定了元素是否显示(即覆盖与否),使用 highcharts 元素内置的 toFront() 方法可以让元素前置显示,但是在编写时保证清晰的顺序是最好的

1
2
3
4
//make canvas for front-end
var FECanvas = render.rect(0, 0, 600, 360).attr({
'fill': '#FDEADA'
}).add(frontEnd);

生成左侧文字,添加到分组,注意 text() 方法的后两个参数为 x 和 y 坐标

1
2
3
4
render.text('Front-End', 45, 164).attr({
'font-size': '19px',
'font-weight': 'bold'
}).add(frontEnd);

为第一个 label 创建分组,来盛放 chrome 那张 logo 和里面那个有边框的 label ,使用 image() 方法引入图片,再用 recttext 组合的方式生成 Layer UI 的 label

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// handle FEGrah group
var FEGrah = render.g().add(frontEnd);

// 为了做出阴影的效果,先生成的矩形被覆盖,但是它的尺寸更大些,因而会水平输出1px的偏移边框,竖直露出2px的偏移边框
render.rect(240, 25, 341, 88, 2).attr({
fill: '#fff',
stroke: '#A09286',
'stroke-width': '6px',
'stroke-opacity': '0.3'
}).add(FEGrah);

render.rect(240, 25, 340, 86, 2).attr({
'fill': '#4F81BD',
'stroke': '#fff',
'stroke-width': '4px'
}).add(FEGrah);

//draw chorme logo
render.image('http://upload.wikimedia.org/wikipedia/commons/8/87/Google_Chrome_icon_(2011).png', 250, 30, 75, 75)
.add(FEGrah);

render.rect(353, 46, 212, 47, 2).attr({
fill: '#A09286',
stroke: '#A09286',
'stroke-width': '6px',
'stroke-opacity': '0.3'
}).add(FEGrah);

render.rect(353, 46, 210, 46, 2).attr({
'fill': '#9BBB59',
'stroke': '#fff',
'stroke-width': '4px'
}).add(FEGrah);

render.text('UI Layer', 425, 75).attr({
'font-size': '19px'
}).css({
'color': '#fff'
}).add(FEGrah);

render.text('Browser', 360, 140).attr({
'font-size': '19px',
'font-weight': 'bold'
}).add(FEGrah);

下图 nodejs logo 所在的 label 采用了 label() 方法来生成,传入文字和坐标,在 attr() 和 css() 方法里指定一些样式让生成的label更美观

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
render.label('UI Layer', 353, 257).attr({
fill: "#F79646",
stroke: 'white',
'stroke-width': 4,
padding: 12,
// 指定圆角半径
r: 2,
width: 185
}).css({
textAlign: 'center',
color: '#fff',
fontSize: '19px'
// 添加进 FEGrah2 分组
}).add(FEGrah2)
// shadow() 方法传入 true 时,label 自带 drop-shadow
.shadow(true);

箭头部分是难点,箭头采用了 path() 方法绘制,定义坐标系原点,然后通过路径数组来自定义路径轨迹,箭头1便是这么画出来的,传入数组:

1
2
3
4
5
6
7
8
9
10
11
12
13

// 'm' 声明了坐标系原点位于(370, 142)处,小写字母代表绝对值,大写字母代表相对值
// 'l' 声明了接下来要连线到后面数值坐标位置,表示相对距离,如向左移动 `25px`, 向下移动 `36px`
// 向右移动13px,竖直不变
// 水平不变,向下移动 `55px`
// 这样绘制最后可绘制出一个宽体箭头
var arrowPath = ['m', 370, 142, 'l', -25, 36, 13, 0, 0, 55, 24, 0, 0, -55, 13, 0, -25, -36];

var narrow = render.path(arrowPath).attr({
stroke: '#EC9694',
'stroke-width': '2',
fill: 'pink'
}).add(FENarrow);

为了节省精力,右边颠倒的箭头我们采用偷懒的方式,复制左边的箭头,然后把复制的箭头放进一个分组中,然后对这个单独的分组添加属性 transform

1
2
3
4
5
6
7
8
9
10
11
12
var narrow2 = render.path(arrowPath).attr({
stroke: '#EC9694',
'stroke-width': '2',
fill: 'pink'
}).add(FENarrow2);

// FENarrow2是个单独的分组
FENarrow2.attr({
// rotate(deg, x, y),分辨传入旋转角度、旋转中心横坐标和旋转中心竖坐标
'transform': 'rotate(-180 397 189)'
// 这样便实现了对左侧箭头的复制,并进行倒置、平移
});

demo展示

SVG展示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<svg version="1.1" style="font-family:&quot;Lucida Grande&quot;, &quot;Lucida Sans Unicode&quot;, Arial, Helvetica, sans-serif;font-size:12px;" xmlns="http://www.w3.org/2000/svg" width="600" height="660">
<desc>Created with Highcharts 4.0.3</desc>
<defs></defs>
<g>
<rect x="0" y="0" width="600" height="360" fill="#FDEADA"></rect>
<text x="45" y="164" font-size="19px" font-weight="bold">Front-End</text>
<g>
<rect x="240" y="25" width="341" height="88" rx="2" ry="2" fill="#fff" stroke="#A09286" stroke-width="6px" stroke-opacity="0.3"></rect>
<rect x="240" y="25" width="340" height="86" rx="2" ry="2" fill="#4F81BD" stroke="#fff" stroke-width="4px"></rect>
<image preserveAspectRatio="none" x="250" y="30" width="75" height="75" xlink:href="http://upload.wikimedia.org/wikipedia/commons/8/87/Google_Chrome_icon_(2011).png"></image>
<rect x="353" y="46" width="212" height="47" rx="2" ry="2" fill="#A09286" stroke="#A09286" stroke-width="6px" stroke-opacity="0.3"></rect>
<rect x="353" y="46" width="210" height="46" rx="2" ry="2" fill="#9BBB59" stroke="#fff" stroke-width="4px"></rect>
<text x="425" y="75" font-size="19px" style="color:#fff;fill:#fff;">
<tspan>UI Layer</tspan>
</text>
<text x="360" y="140" font-size="19px" font-weight="bold">Browser</text>
</g>
<g>
<rect x="240" y="240" width="341" height="88" rx="2" ry="2" fill="#fff" stroke="#A09286" stroke-width="6px" stroke-opacity="0.3"></rect>
<rect x="240" y="240" width="340" height="86" rx="2" ry="2" fill="#4F81BD" stroke="#fff" stroke-width="4px"></rect>
<image preserveAspectRatio="none" x="245" y="255" width="100" height="55" xlink:href="http://nodejs.org/images/logos/nodejs.png"></image>
<g style="text-align:center;" transform="translate(353,257)">
<rect x="0" y="0" width="209" height="47" strokeWidth="4" fill="none" stroke="black" stroke-width="5" rx="2" ry="2" isShadow="true" stroke-opacity="0.049999999999999996" transform="translate(1, 1)"></rect>
<rect x="0" y="0" width="209" height="47" strokeWidth="4" fill="none" stroke="black" stroke-width="3" rx="2" ry="2" isShadow="true" stroke-opacity="0.09999999999999999" transform="translate(1, 1)"></rect>
<rect x="0" y="0" width="209" height="47" strokeWidth="4" fill="none" stroke="black" stroke-width="1" rx="2" ry="2" isShadow="true" stroke-opacity="0.15" transform="translate(1, 1)"></rect>
<rect x="0" y="0" width="209" height="47" strokeWidth="4" fill="#F79646" stroke="white" stroke-width="4" rx="2" ry="2"></rect>
<text x="67.5" zIndex="1" style="font-size:19px;color:#fff;fill:#fff;" y="30">
<tspan>UI Layer</tspan>
</text>
</g>
<text x="375" y="354" font-size="19" font-weight="bold">Server</text>
</g>
<g>
<path fill="pink" d="m 370 142 l -25 36 13 0 0 55 24 0 0 -55 13 0 -25 -36" stroke="#EC9694" stroke-width="2"></path>
</g>
<g transform="rotate(-180 397 189)">
<path fill="pink" d="m 370 142 l -25 36 13 0 0 55 24 0 0 -55 13 0 -25 -36" stroke="#EC9694" stroke-width="2"></path>
</g>
</g>
<text x="455" y="195" font-size="19" font-weight="bold">HTTP/HTTPS</text>
</svg>

参考链接:


http://www.w3.org/TR/SVG/propidx.html
http://api.highcharts.com/highcharts#Renderer

Best wish!

本文地址: https://mrpeak.github.io/2014/09/06/highchart-svg/