很早就学习了SVG相关知识,但是一直没有在项目中用到。这次开发阿拉丁卡片中的一个折线图组件,终于让这部分知识有了用武之地,果然还是实践出真知,在开发的过程中踩了很多坑。

刚这接到折线图这个需求时,脑海中的第一反应是可以通过canvas或SVG实现,最后调研一番决定使用SVG。我们先来看看canvas和SVG各自的使用场景。

SVG

SVG 是一种使用 XML 描述 2D 图形的语言。
SVG 基于 XML,这意味着 SVG DOM 中的每个元素都是可用的。您可以为某个元素附加 JavaScript 事件处理器。
在 SVG 中,每个被绘制的图形均被视为对象。如果 SVG 对象的属性发生变化,那么浏览器能够自动重现图形。

Canvas

Canvas 通过 JavaScript 来绘制 2D 图形。
Canvas 是逐像素进行渲染的。
在 canvas 中,一旦图形被绘制完成,它就不会继续得到浏览器的关注。如果其位置发生变化,那么整个场景也需要重新绘制,包括任何或许已被图形覆盖的对象。

Canvas 与 SVG 的比较

下表列出了 canvas 与 SVG 之间的一些不同之处。

Canvas

  • 依赖分辨率
  • 不支持事件处理器
  • 弱的文本渲染能力
  • 能够以 .png 或 .jpg 格式保存结果图像
  • 最适合图像密集型的游戏,其中的许多对象会被频繁重绘

SVG

  • 不依赖分辨率
  • 支持事件处理器
  • 最适合带有大型渲染区域的应用程序(比如谷歌地图)
  • 复杂度高会减慢渲染速度(任何过度使用 DOM 的应用都不快)
  • 不适合游戏应用

回过头来再看我们的需要实现的折线图组件,下面是UE给的设计图。

UE设计图

通过分析需求及UE设计图,我们可以得到下面这些信息。

  • 在wap展现,需要兼容多种分辨率
  • 需要添加简单的事件交互
  • 没有复杂的动画

显然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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<div class="sfc-wk-major-salary-svgBox">
<div class="sfc-wk-major-salary-svg">
<svg class="sfc-wk-major-salary-svgself" version="1.1" xmlns="http://www.w3.org/2000/svg">
<!-- 该专业 -->
{%foreach $xyAxis as $item%}
{%if $item@first%}
<path class="sfc-wk-major-salary-svg-yItem sfc-wk-major-salary-svg-yItem1" d="M 0 100 L 0 100" style="stroke:#ff6400; stroke-width:2"/>
<circle class="sfc-wk-major-salary-svg-yPos sfc-wk-major-salary-svg-yPos1" cx="0" cy="100" r="4" fill="#ff6400" />
{%else%}
<path class="sfc-wk-major-salary-svg-yItem sfc-wk-major-salary-svg-yItem1" d="M 0 100 L 0 100 " style="stroke:#ff6400; stroke-width:2"/>
<circle class="sfc-wk-major-salary-svg-yPos sfc-wk-major-salary-svg-yPos1" cx="0" cy="100" r="4" fill="#ff6400" />
{%/if%}
{%/foreach%}
<!-- <path class="sfc-wk-major-salary-svg-yItem sfc-wk-major-salary-svg-yItem1" d="M 0 100 L 0 100 " style="stroke:#ff6400; stroke-width:2"/> -->
<!-- 所有专业 -->
{%foreach $xyAxis2 as $item%}
{%if $item@first%}
<path class="sfc-wk-major-salary-svg-yItem sfc-wk-major-salary-svg-yItem2" d="M 0 100 L 0 100" style="stroke:#2d8eff; stroke-width:2"/>
<circle class="sfc-wk-major-salary-svg-yPos sfc-wk-major-salary-svg-yPos2" cx="0" cy="100" r="4" fill="#2d8eff" />
{%else%}
<path class="sfc-wk-major-salary-svg-yItem sfc-wk-major-salary-svg-yItem2" d="M 0 100 L 0 100 " style="stroke:#2d8eff; stroke-width:2"/>
<circle class="sfc-wk-major-salary-svg-yPos sfc-wk-major-salary-svg-yPos2" cx="0" cy="100" r="4" fill="#2d8eff" />
{%/if%}
{%/foreach%}
<!-- <path class="sfc-wk-major-salary-svg-yItem sfc-wk-major-salary-svg-yItem2" d="M 0 100 L 0 100 " style="stroke:#2d8eff; stroke-width:2"/> -->
</svg>
</div>
<!-- 虚线 -->
<div class="sfc-wk-major-salary-svg-line">
</div>
<!-- y轴坐标 -->
<div class="sfc-wk-major-salary-svg-yAxis">
{%foreach $xyAxis as $item%}
<span class="sfc-wk-major-salary-svg-yAxis-item sfc-wk-major-salary-svg-yAxis-item1 c-line-clamp1 c-gray">{%$item.y|escape%}</span>
{%/foreach%}
</div>
<!-- y轴坐标2 -->
<div class="sfc-wk-major-salary-svg-yAxis">
{%foreach $xyAxis2 as $item%}
<span class="sfc-wk-major-salary-svg-yAxis-item sfc-wk-major-salary-svg-yAxis-item2 c-line-clamp1 c-gray">{%$item.y|escape%}</span>
{%/foreach%}
</div>
<!-- x轴坐标 -->
<div class="sfc-wk-major-salary-svg-xAxis c-gap-bottom c-gap-top">
{%foreach $xyAxis as $item%}
<span class="sfc-wk-major-salary-svg-xAxis-item c-gray" style="width:{%100/count($xyAxis)|cat:'%'%}">{%$item.x|escape%}</span>
{%/foreach%}
</div>
<!-- 颜色注释 -->
<div class="c-gap-bottom">
<span class="sfc-wk-major-salary-the-major c-gap-right-small"></span><span class="c-gap-right">本专业</span><span class="sfc-wk-major-salary-all-major c-gap-right-small"></span>所有专业
</div>
</div>
<!-- 折线注释 -->
<div class="sfc-wk-major-salary-svg-remark c-line-top">
{%foreach $tplData.remark as $item%}
<p class="c-gray">{%$item|escape%}</p>
{%/foreach%}
</div>
</div>
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
.sfc-wk-major-salary-svgBox {
position: relative;
}

.sfc-wk-major-salary-svg {
position: absolute;
width: 100%;
height: 200px;
}

.sfc-wk-major-salary-svgself {
width: 100%;
height: 100%;
}

.sfc-wk-major-salary-svg-yAxis-item, .sfc-wk-major-salary-svg-xAxis-item {
text-align: center;
display: inline-block;
}

.sfc-wk-major-salary-svg-yAxis-item {
position: absolute;
top: -999px;
left: -999px;
}

.sfc-wk-major-salary-the-major, .sfc-wk-major-salary-all-major {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
}

.sfc-wk-major-salary-the-major {
background-color: #ff6400;
}

.sfc-wk-major-salary-all-major {
background-color: #2d8eff;
}

.sfc-wk-major-salary-svg-remark {
padding-top: 0.08rem;
}

.sfc-wk-major-salary-svg-line-item {
position: relative;
width: 100%;
border-bottom: 1px dotted #cecece;
}

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
var _this = this;
var $container = $(this.container);
var $yAxis = _this.data.yAxis;
var $yAxis2 = _this.data.yAxis2;
var $svgBox = $container.find(".sfc-wk-major-salary-chart-wrapper");
var $svg = $container.find(".sfc-wk-major-salary-svg");
var $yPos = $container.find(".sfc-wk-major-salary-svg-yAxis-item1");
var $xAxis = $container.find(".sfc-wk-major-salary-svg-xAxis");
var num = $yAxis.length;
var $svgWidth = Math.ceil($svgBox.width());

/* 起点高度 */
var $svgheight = 180;
var start = $svgheight;
var maxPos = 20;
var minPos = start, yAxis = start, prevY = start;
var xAxis = 0, prevX = 0;
var cx = [], cy = [], maxY = [];

/* 将yAxis和yAxis2存入maxY中 */
var allNum = num + $yAxis2.length;
var allAxis = $yAxis.concat($yAxis2);
for(var j=0;j<allNum; j++){
maxY[j] = parseFloat(allAxis[j].y);
};

/* 找到yAxis和yAxis2中的最大值和最小值 */
var max = maxY[0];
var min = maxY[0];
for(var k=0;k<maxY.length; k++){
if(max < maxY[k]){ max = maxY[k] };
if(min > maxY[k]){ min = maxY[k] };
};

/* 最大值和最小值以1000为单位分别向上向下取整 */
max = Math.ceil(max / 1000) * 1000;
min = Math.floor(min / 1000) * 1000;

/* 计算刻度 */
var posChazhi = minPos-maxPos;
var percentChazhi = max-min;
var bili = posChazhi/percentChazhi;

/* 绘制虚线 每间隔1000绘制一条*/
var lineNum = (max - min) / 1000;
var $lineWrapper = $container.find(".sfc-wk-major-salary-svg-line");
var lineH = posChazhi / (lineNum) - 1;
var html = '<div class="sfc-wk-major-salary-svg-line-item" style="height: ' + maxPos + 'px;"><span style="position:absolute;top: 0px;">' + max + '</span></div>';;
for (var n = 0;n < lineNum;n++) {
html += '<div class="sfc-wk-major-salary-svg-line-item" style="height:' + lineH + 'px;"><span style="position:absolute;top:' + (lineH - 20) + 'px;">' + (max - (n + 1) * 1000) + '</span></div>';
}
$lineWrapper.html(html);

function SVG1(){
var jianju = Math.round($svgWidth/num);
var first = Math.round(jianju/2);
$xAxis.attr("d","M 0 "+ $svgheight +" L "+ $svgWidth +" "+$svgheight);
for(var i=0; i<num; i++){
var yVal = parseFloat($yAxis[i].y);
var round = Math.round((yVal-min)*bili);
yAxis = start-round;
if(i==0){
xAxis = first;
/* $svgBox.find(".sfc-wk-major-salary-svg-yItem1").eq(i).attr("d","M 0 "+start+" L "+xAxis+" "+yAxis); */
$svgBox.find(".sfc-wk-major-salary-svg-yPos1").eq(i).attr({"cx":xAxis,"cy":yAxis});
$yPos.eq(i).css({"top":(yAxis-20)+"px","left":(xAxis-15)+"px"});
cx[i] = xAxis;
cy[i] = yAxis;
}else{
var yVal1 = parseFloat($yAxis[i-1].y);
var round1 = Math.round((yVal1-min)*bili);
prevY = start-round1;
xAxis = first + jianju*[i];
prevX = first + jianju*([i]-1);
$svgBox.find(".sfc-wk-major-salary-svg-yItem1").eq(i).attr("d","M "+prevX+" "+prevY+" L "+xAxis+" "+yAxis);
$svgBox.find(".sfc-wk-major-salary-svg-yPos1").eq(i).attr({"cx":xAxis,"cy":yAxis});
$yPos.eq(i).css({"top":(yAxis-20)+"px","left":(xAxis-16)+"px"});
cx[i] = xAxis;
cy[i] = yAxis;
}
};
var xlast = $svgBox.find(".sfc-wk-major-salary-svg-yPos1").eq(num-1).attr('cx');
var ylast = $svgBox.find(".sfc-wk-major-salary-svg-yPos1").eq(num-1).attr('cy');

$svgBox.find(".sfc-wk-major-salary-svg-yItem1").eq(num).attr("d","M "+xlast+" "+ylast+" L "+$svgWidth+" "+start);
var points = '';
for(var j=0; j<num; j++){
points += cx[j]+' '+cy[j]+' ';
};
};

function SVG2(){
var $yAxis = _this.data.yAxis2;
var $yPos = $container.find(".sfc-wk-major-salary-svg-yAxis-item2");

var jianju = Math.round($svgWidth/num);
var first = Math.round(jianju/2);
$xAxis.attr("d","M 0 "+ $svgheight +" L "+ $svgWidth +" "+$svgheight);
for(var i=0; i<num; i++){
var yVal = parseFloat($yAxis[i].y);
var round = Math.round((yVal-min)*bili);
yAxis = start-round;
if(i==0){
xAxis = first;
/* $svgBox.find(".sfc-wk-major-salary-svg-yItem2").eq(i).attr("d","M 0 "+start+" L "+xAxis+" "+yAxis); */
$svgBox.find(".sfc-wk-major-salary-svg-yPos2").eq(i).attr({"cx":xAxis,"cy":yAxis});
$yPos.eq(i).css({"top":(yAxis-20)+"px","left":(xAxis-15)+"px"});
cx[i] = xAxis;
cy[i] = yAxis;
}else{
var yVal1 = parseFloat($yAxis[i-1].y);
var round1 = Math.round((yVal1-min)*bili);
prevY = start-round1;
xAxis = first + jianju*[i];
prevX = first + jianju*([i]-1);
$svgBox.find(".sfc-wk-major-salary-svg-yItem2").eq(i).attr("d","M "+prevX+" "+prevY+" L "+xAxis+" "+yAxis);
$svgBox.find(".sfc-wk-major-salary-svg-yPos2").eq(i).attr({"cx":xAxis,"cy":yAxis});
$yPos.eq(i).css({"top":(yAxis-20)+"px","left":(xAxis-16)+"px"});
cx[i] = xAxis;
cy[i] = yAxis;
}
};
var xlast = $svgBox.find(".sfc-wk-major-salary-svg-yPos2").eq(num-1).attr('cx');
var ylast = $svgBox.find(".sfc-wk-major-salary-svg-yPos2").eq(num-1).attr('cy');

$svgBox.find(".sfc-wk-major-salary-svg-yItem2").eq(num).attr("d","M "+xlast+" "+ylast+" L "+$svgWidth+" "+start);
var points = '';
for(var j=0; j<num; j++){
points += cx[j]+' '+cy[j]+' ';
};
};

SVG1();
SVG2();

$(window).resize(function () {
var $resizeWidth = Math.ceil($svgBox.width());
$svgWidth = $resizeWidth;
SVG1();
SVG2();
});

对应的数据结构为

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
<treat>
<coordinate1>
<xy>
<x>毕业三年</x>
<y>7911</y>
</xy>
<xy>
<x>毕业五年</x>
<y>9911</y>
</xy>
<xy>
<x>毕业七年</x>
<y>9000</y>
</xy>
</coordinate1>
<coordinate2>
<xy>
<x>毕业三年</x>
<y>8911</y>
</xy>
<xy>
<x>毕业五年</x>
<y>11341</y>
</xy>
<xy>
<x>毕业七年</x>
<y>8000</y>
</xy>
</coordinate2>
</treat>

最终效果如下

展示效果截图