项目原因,需要使用web绘制图表,比较靠谱的有echart和d3。
echart:应该是百度的项目,首页做的很漂亮,文档也比较全。个人感觉是一个入手较易,主要在学习怎么配参数,地图是echart的一个亮点。缺点就明显了,入手容易的肯定高度封装过,很难根据自己的需求定制化。而且使用了cavas绘图,在移动端性能不如svg。个人感觉echart适合一些时间比较紧迫,设计没那么明确的场合,直接套上echart,就已经很漂亮了。
d3:大名鼎鼎,受欢迎程度超过jquery,首页的绚丽特效让人眼花,教程的话强烈推荐http://www.ourd3js.com/wordpress/,这个教程好像出了书,叫精通D3.js,基本上把教程撸一遍就没啥大问题了。
d3的优点:用过一段d3后,感觉d3就是svg的jquery,把svg的操作封装为更易用的接口,并提供各种数据可视化的接口。所以d3很灵活,因为你可以用d3在svg中添加一个点、线、饼等等,svg性能不错。链式写法。
缺点:入手稍微慢点,不过撸一遍教程也就一下午的时间。svg的接口网上文档比较少,有些用法stackoverflow都找不到。
如果是学习d3的,推荐前面的教程网站,这里只是个人备忘录,想到哪里说到哪里的流水账,记录那些让人眼前一亮或者眼前一黑的点,以及几个开发中反复查询的用法。
好了,出发
1.
var svg = d3.select("body")
.append("svg")
.attr("width", svgWidth)
.attr("height", svgHeight);
首先,你要在一个dom中添加svg,需要两个参数,width和height,一般我是这么给的
this.svgContent = document.getElementById("id");
this.svgWidth = this.svgContent.clientWidth;
this.svgHeight = this.svgContent.clientHeight;
踩坑:需求在指定操作后才绘制该图标,dom display:none的时候,clientWidth和clientHeight是0,所以svg等dom display正常后再添加。
接下来最好设置一下svg的padding,用来应付后面出现的各种文字绘制超出不显示,UI修改等灵细操作。
this.padding = {left: Ruler.size_1920(200), right: Ruler.size_1920(220), top: Ruler.size_1920(66), bottom: Ruler.size_1920(28)};
因为svg直接传的都是px,所以需要根绝屏幕做一下自适应,size_1920函数如下:
let deviceWidth = document.documentElement.clientWidth;
return (px * deviceWidth) / 1920;
坐标轴:
图标中用到最多的是坐标轴,使用坐标轴首先要设定比例尺,目前共用到了4种比例尺
线性比例尺
this.yScale = d3.scale.linear()
.domain([0, this.threshold])
.range([this.svgHeight - this.padding.top - this.padding.bottom, 0]);
domain中设置数据的min/max,range中设置svg实际位置。
备注:svg是以左上角为原点的,而常规视角中原点是左下角,所以这里的range起止位置是 svgHeight-0,即起始位置是下边缘y=svgHeight,终止位置是上边缘y=0。这一点需要谨记,后面有许多计算x、y位置的,y的位置总要反着想。其他比例尺也是一样的。
离散比例尺
this.xScale = d3.scale.ordinal()
.domain(d3.range(data.length))
.rangeRoundBands([0, this.svgWidth - this.padding.left - this.padding.right]);
or
this.xScale = d3.scale.ordinal()
.domain(["1","2","3","4"])
.rangeRoundBands([0, this.svgWidth - this.padding.left - this.padding.right]);
时间比例尺
时间比例尺适用于一些以时间为坐标轴的情况
this.now = new Date();
let nextDay = new Date(this.now.getTime() + 1000 * 3600 * 24);
// 这里要搞成UTC时间
this.nowUTC = this.now.getUTCFullYear() + "-" + (this.now.getUTCMonth() + 1) + "-" + (this.now.getUTCDate() < 10 ? "0" : "") + this.now.getUTCDate() + "T00:00";
this.nextUTC = nextDay.getUTCFullYear() + "-" + (nextDay.getUTCMonth() + 1) + "-" +
(nextDay.getUTCDate() < 10 ? "0" : "") + nextDay.getUTCDate() + "T00:00";
this.xScale = d3.time.scale()
.domain([new Date(this.nowUTC), new Date(this.nextUTC)])
.range([0, this.svgWidth - this.padding.left - this.padding.right]);
备注:比如这里是今天到明天,第二天nextDay的算法,就用秒,其他都不行。UTC时间一定要注意,所有时间都改成UTC时间,所有get一律要加UTC。
颜色比例尺
// 颜色比例尺
var color = d3.scale.category10() // 20 30 都有
这是个比较特殊的比例尺,不是用来画坐标轴的,而是把颜色均匀分割,category10,category20等等都有,填一个index,输出一个色值。在有些d3的版本中,颜色比例尺没用放在scale里面。
坐标轴
可能还有更多种类的比例尺,目前还没有接触,下面就开始生成坐标轴数据。
let gridXAxis = d3.svg.axis()
.scale(this.xScale)
.orient("top")
.innerTickSize(this.svgHeight - this.padding.top - this.padding.bottom)
.outerTickSize(0)
.ticks(d3.time.hour, 1)
.tickFormat(function (d, i) {
return "";
})
.tickPadding(10);
几个常用参数
scale,必要参数,把比例尺填进去
orient,非必填,刻度相对坐标轴的位置 top bottom left right
ticks,非必填,刻度个数 注:ticks只是参考,最终生成刻度个数以数据为准,比如数据需要25个刻度,但ticks填了17,那d3就没办法了,会生成一个d3生成的刻度个数中选跟17最接近的。
innerTickSize,非必填,内刻度高,默认是6。 注:这里设置了innerTickSize是整个svg的高度,和y轴innerTickSize设置整个svg的宽配合行程网格效果,正常的坐标轴就再画一个,注意高度计算时错开
let xAxis = d3.svg.axis()
.scale(this.xScale)
.orient("bottom")
.ticks(d3.time.hour, 1)
.outerTickSize(0)
.tickFormat(function (d, i) {
});
outerTickSize,非必填,外刻度高
tickFormat,非必填,刻度值,默认为空 注:这是一个数据绑定参数,即根据数据生成不同的值
tickPadding,非必填,刻度文字与坐标轴的间距
真正的绘制
this.svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(" + this.padding.left + "," + (this.svgHeight - this.padding.top + this.xPadding + this.doubleLinePadding) + ")")
.call(xAxis);
this.svg.append("g")
.attr("class", "x axis grid")
.attr("transform", "translate(" + this.padding.left + "," + (this.svgHeight - this.padding.top + this.xPadding) + ")")
.call(gridXAxis);
给svg中添加g元素,call绑定坐标轴数据,attr就是给g元素添加各种属性,class(是不是很熟悉),transform。 注:svg中没有top left margin等属性,大的位移使用transform。
曲线
坐标轴绘制完后,就是曲线的绘制,先生成曲线绘制函数
let _line = d3.svg.line()
.x(function (d) {
let x = self.xScale(d.time);
if (x < 0) {
return 0;
}
return self.xScale(d.time);
})
.y(function (d) {
let x = self.xScale(d.time);
if (x < 0) {
return 0;
}
return self.yScale(d.uv);
})
// .interpolate("cardinal");
.interpolate("monotone");
曲线绘制函数参数
x、y就是根据数据来计算坐标,这里是比例尺大显身手的地方。
interpolate,线段怎么弯曲,讲的最好的是这个地址
http://www.oxxostudio.tw/articles/201411/svg-d3-02-line.html
注:basis曲线保证了优美,没保证曲线与数据的一致,像我这种人是完全无法接受的。
绘制曲线
let paths = this.svg.selectAll(".data-line").data(data);
这一行代码解读一下就是选择所有class是data-line的元素,然后给它绑定数据。data-line哪里来的?这里涉及到d3中一个概念,update enter exit
selectAll得到的元素跟数据相比,有三种可能,元素个数 大于/小于/等于数据个数。
数据超出元素个数的部分叫enter,比如第一次绘制时,所有元素都属于paths.enter
paths.enter()
.append("path")
.attr("class", "data-line gaussian-shadow")
.attr("d", function (d) {
return _line(d.data);
})
.attr("transform", "translate(" + this.padding.left + "," + this.padding.top + ")")
.style("stroke", function (d, i) {
return d3.scale.category10(i);
})
.attr("fill", "none");
paths元素比较简单直接,基本上只需要给一个属性d就能画出来了,d可以用前面的曲线函数直接生成。
stroke,线条颜色,这里可以用颜色比例尺 d3.scale.category10(i)
fill就是填充颜色,paths填充的是一个面。
不超出的部分叫update,即需要更新的部分,paths.update可以看到,也可以直接更新数据,这里是数据驱动的最直观表现
paths.attr("d", function (d) {
if (d && d.data) {
return _line(d.data);
}
});
数据少于元素个数的部分叫exit,一般exit只有一个用法
// 去掉多余的曲线
paths.exit().remove();
到这里,一个坐标轴差不多出来了,但,是不是少了点啥,对,UED最喜欢的灵魂一击,动画。
曲线进入/更新的时候要动画怎么办
d3的动画很好写
paths.transition()
.duration(2000)
.ease("sin")
.attr("d", function (d) {
if (d && d.data) {
return _line(d.data);
}
});
增加 transition ease 和 delay即可,transition前后是动画的起始和结束状态。
绘制一个圆
let circles = this.svg.selectAll("circle")
.data(this.axisData);
circles.attr("cx", this.xScale(this.now))
.attr("cy", function (d) {
if (d && d.data) {
return this.yScale(d.data)
}
})
}
circles.exit().remove();
circles.enter()
.append("circle")
.attr("fill", function (d) {
return d.color;
})
.attr("transform", "translate(" + this.padding.left + "," + this.padding.top + ")")
// .attr("fill-opacity", "0.4")
.attr("cx", this.xScale(this.now))
.attr("cy", function (d) {
if (d && d.data) {
return this.yScale(d.data)
}
})
.attr("r", 10);
矩形
let rectContainers = this.svg.selectAll("item-title")
.data(data)
.enter()
.append("g")
.attr("class", "item-title")
.attr("transform", "translate(" + this.padding.left + "," + this.padding.top + ")");
rectContainers.append("rect")
.attr("fill", function (d) {
return d.color;
})
.attr("x", this.xScale(this.now) + 30)
.attr("y", function (d) {
if (d && d.data) {
return yScale(d.data);
}
})
.attr("width", function (d) {
return 20;
})
.attr("height", rectHeight)
.attr("rx", rectRadius)
.attr("ry", rectRadius)
.attr("fill-opacity", 0.8);
半圆角矩形是用path生成的,顶部圆角矩形
var topRoundedRect = function(x, y, width, height, radius) {
return "M" + x + "," + y
+ "v" + ( radius - height)
+ "a" + radius + "," + radius + " 0 0 1 " + radius + "," + -radius
+ "h" + (width - 2 * radius)
+ "a" + radius + "," + radius + " 0 0 1 " + radius + "," + radius
+ "v" + (height - radius)
+ "z";
};
var rightRoundedRect = function(x, y, width, height, radius) {
return "M" + x + "," + y
+ "h" + (width - radius)
+ "a" + radius + "," + radius + " 0 0 1 " + radius + "," + radius
+ "v" + (height - 2 * radius)
+ "a" + radius + "," + radius + " 0 0 1 " + -radius + "," + radius
+ "h" + (radius - width)
+ "z";
};
this.svg.selectAll(".data-rect")
.data(data)
.enter()
.append("path")
.attr("class", "data-rect")
.attr("transform", "translate(" + this.padding.left + "," + this.padding.top + ")")
.attr("fill", function(d, i) {
return this.colors[i];
}.bind(this))
.attr("d", function (d, i) {
let x = this.xScale(i) + this.rectPadding/2;
let y = this.yScale(this.yScale.domain()[0]);
let radius = 0;
let width = this.xScale.rangeBand() - this.rectPadding;
if (d.albm_cnt != 0) {
radius = this.rectRadius;
}
let height = this.yScale(this.yScale.domain()[0]) - (this.yScale(d.albm_cnt)) + this.extraHeight;
return topRoundedRect(x, y, width, height, radius);
}.bind(this));
topRoundRect和rightRoundRect乍一看完全搞不懂,这里讲的比较详细https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths,仔细观察下,就是
M 起始x y坐标
h 纵移
v 横移
a 比较复杂 前两个参数是圆角的x y radius
中间三个参数 001是三个flag,只能是0/1 圆角矩形都是001
第一个代表弧线角度是否要大于180
第二个代表正角度还是负角度
第三个代表正方向还是反方向
z是合拢
最后两个参数是终止的x y位置
当前了,还有更多的L C S等等,这里不研究了
文字
rectContainers.append("text")
.attr("class", "item-text")
.attr("fill", "white")
.attr("x", this.xScale(this.now) + 30 + 14)
.attr("y", function (d) {
if (d && d.dataSet) {
return self.yScale(AxisView.getPvByTime(d.dataSet, self.now)) + 6;
}
})
.text(function (d) {
return d.type;
});
到这里,d3绘制的套路差不多都熟悉了,append data enter transform attr,拿来画个饼图吧!
生成饼图数据
let pie = d3.layout.pie().sort(null);
this.pieData = pie(percentArray);
查看一下this.pieData,你会发现数据增加了 startEngle 和 endEngle
注:饼图默认会按大小排序,如果不需要排序,增加一个sort(null)即可
弧线函数
this.arc = d3.svg.arc()
.innerRadius(0)
.outerRadius(self.innerRadius);
let arcs = this.svg.selectAll(".time-pie")
.data(this.pieData)
.enter()
.append("g")
.attr("class", "time-pie")
.attr("transform", "translate(" + this.centerX + "," + this.centerY + ")");
arcs.append("path")
.attr("fill", function (d, i) {
return this.colors[i];
}.bind(this))
.attr('d', function(d) {
return this.arc(d);
}.bind(this));
饼图动画不能使用d了,要使用attrTween
简单的饼图动画
arcs.append("path")
.attr("fill", "#fff")
.attr("fill-opacity", 0.2)
.transition()
.duration(this.fadeInTime)
.attrTween('d', function(d) {
var i = d3.interpolate(d.startAngle+0.1, d.endAngle);
return function(t) {
d.endAngle = i(t);
return arc(d);
};
}.bind(this));
高级饼图动画,自己计算delay的时间,让饼一点一点出来
arcs.append("path")
.attr("fill", function (d, i) {
return this.colors[i];
}.bind(this))
.transition()
.delay(function (d, i) {
if (data) {
let _percent = 0;
for (let j = 0; j < i; j++) {
try {
_percent += parseInt(data[j].percent)
} catch (e) {
_percent += 100 / data.length;
}
}
return this.pieAnimTime * _percent / 100;
}
}.bind(this))
.duration(function (d) {
let _time = this.pieAnimTime / data.length;
if (d && d.data) {
let percent = parseInt(d.data);
_time = this.pieAnimTime * percent / 100;
}
this.lastAnimTime+=_time;
return _time;
}.bind(this))
.ease("linear")
.attrTween('d', function(d) {
var i = d3.interpolate(d.startAngle+0.1, d.endAngle);
return function(t) {
d.endAngle = i(t);
return this.arc(d);
}.bind(this);
}.bind(this));
往饼图中间添加一个圆点
this.centriodData = Array.apply([0,0], this.pieData).map(function (val, index) {
// return this.arc.centroid(val);
return {
right: (val.startAngle+val.endAngle)/2<Math.PI,
centroidPos: this.arc.centroid(val)
};
return this.arc.centroid(val);
}.bind(this));
注:centriodData和Angle是钝/锐角的计算提出来,是因为在某些低配机子上,偶现绑定计算错误。
this.svg.selectAll(".time-circle")
.data(this.pieData)
.enter().append("circle")
.attr("class", "time-circle")
.attr("transform", "translate(" + this.centerX + "," + this.centerY + ")")
.attr("cx", function(d, i) {
// var pos= this.arc.centroid(d);
var pos= this.centriodData[i] && this.centriodData[i].centroidPos;
return pos && pos[0] * 1.65;
}.bind(this))
.attr("cy", function(d, i) {
// var pos= this.arc.centroid(d);
var pos= this.centriodData[i] && this.centriodData[i].centroidPos;
return pos && pos[1] * 1.65;
}.bind(this))
.attr("fill", "#fff")
// .attr("fill-opacity", 0.1)
// .transition()
// .duration(this.fadeInTime)
.attr("fill-opacity", 1)
.attr("r", Ruler.size_1920(6));
画一个折线
let line = this.svg.selectAll(".line") //添加文字和弧之间的连线
.data(this.pieData) //返回是pie(data0)
.enter().append("g")
.attr("class", "line")
.attr("transform", "translate(" + this.centerX + "," + this.centerY + ")")
.append("polyline")
.attr('points', function(d, i) {
// var pos1= this.arc.centroid(d),pos2= this.arc.centroid(d),pos3= this.arc.centroid(d);
var pos1=[this.centriodData[i].centroidPos[0], this.centriodData[i].centroidPos[1]] ,pos2= [this.centriodData[i].centroidPos[0], this.centriodData[i].centroidPos[1]],pos3= [this.centriodData[i].centroidPos[0], this.centriodData[i].centroidPos[1]];
pos1[0]*=1.65,pos1[1]*=1.65;
pos2[0]*=2,pos2[1]= pos2[1] *2;
pos3[0]=(this.centriodData[i].right?this.lineLen:-this.lineLen);
pos3[1]= pos3[1]*2;
//pos1表示圆弧的中心往上,pos2是圆弧边,pos3就是将pos2平移后得到的位置
//三点链接在一起就成了线段。
return [pos1,pos2,pos3];
}.bind(this))
.style('fill', 'none')
.style('stroke', "#fff")
.style('stroke-opacity', 0.6)
.style('stroke-width', Ruler.size_1920(3) + "px");
// 这里是动画,直线慢慢生成的动画
// .attr("stroke-dasharray", totalLen + " " + totalLen)
// .attr("stroke-dashoffset", totalLen)
// .transition()
// .duration(this.lineAnimTime)
// .ease("linear")
// .attr("stroke-dashoffset", 0);
到这里,一般的坐标轴、饼图已经难不倒了,而且自己想添加什么就添加什么,无非就是计算x、坐标,d3就是一堆图形函数,帮你计算坐标而已,那还有什么新东西吗?假如UED说想要阴影,光效,这里介绍一下svg里面一个很牛叉的东西,filter
let defs = svg.append("defs");
let filter = defs.append("filter")
.attr("id", "gaussianShadowFilter");
// append gaussian blur to filter
// filter.append("feMorphology")
// .attr("operator", "dilate")
// .attr("radius", 3);
filter.append( 'feGaussianBlur' )
.attr( 'in', 'SourceAlpha' )
.attr( 'stdDeviation', 2 ) // !!! important parameter - blur
.attr( 'result', 'blur' );
// append offset filter to result of gaussion blur filter
filter.append( 'feOffset' )
.attr( 'in', 'blur' )
.attr( 'dx', 10 ) // !!! important parameter - x-offset
.attr( 'dy', 1 ) // !!! important parameter - y-offset
.attr( 'result', 'offsetBlur' );
// merge result with original image
let feMerge = filter.append( 'feMerge' );
// first layer result of blur and offset
feMerge.append( 'feMergeNode' )
.attr( 'in", "offsetBlur' );
// original image on top
feMerge.append( 'feMergeNode' )
.attr( 'in', 'SourceGraphic' );
实话说,这个东西我也只是到抄过来调参的地步,比如dx dy。理解上只知道特效输出为result,下一个特效的in是这个result,其他也是一问三不知,而且看了之后着实没有深入了解下去的想法,简直就是手写Photoshop。
下面是几个介绍filter的地址,留下以备不时之需
1.https://www.w3.org/TR/SVG/filters.html
2.https://www.smashingmagazine.com/2015/05/why-the-svg-filter-is-awesome/
3.https://jorgeatgu.github.io/svg-filters/