手把手教你DIY一个春运迁徙图(一)
换了新工作,也确定了我未来数据可视化的发展方向。新年第一篇博客,又逢春运,这篇技术文章就来交给大家如何做一个酷炫的迁徙图(支持移动哦)。(求star 代码点这里)
迁徙图的制作思路分为静态的元素和变换的动画。其中动画是围绕着静态的元素变换,所以我们首要的任务就是如何绘制静态的元素。
仔细看一下,静态的元素分为弧线(Arc)、弧线端点的箭头(Marker),动画部分主要是弧线终点向脉冲波一样的圆(Pulse),以及像流星一样的动态小箭头和弧线的光晕,这两个我们放在一起成为Spark。我们可以看到Spark主要在弧线上运动,如果你仔细观察一下会发现终点处点头的指向也是朝着终点处的切线方向,所以我们把主要的任务放在如何根据两个点绘制一段弧线。
我们要绘制这段弧线就要知道圆心和半径,而数学定理告诉我们的是三点定圆,过两个点的圆有无数个,所以我们只能找一个比较合适的圆。
所以现在的问题变成了已知两点pointF和pointT,求一个合适的圆心pointC (xc, yc);
根据pointF和pointT所以我们能够确定一条直线,他的斜率 kt =(yt - yf)/ (xt - xf);
根据PointF和pointT我们能够计算出他们的中点pointH=(m, n); m = (xt - xf) / 2, n = (yt - yf) / 2;
经过两点的圆一定在他们两点的中垂线上,而直线的中垂线斜率kl与直线的斜率kt存在数学关系:kl * kt = -1;
把我们的参数全部套入这个公式可得:
((yc - n)/ (xc - m)) * ((yt - yf)/ (xt - xf)) = -1;
接着变换一下:
(yc - n) / (xc - m) = -(xt - xf) / (yt - yf);
去掉碍事的负号:
(yc - n) / (xc - m) = (xt - xf) / (yf - yt);
再变换一下:
(yc - n)/ (xt - xf) = (xc - m) / (yf - yt) = factor;
到此我们得到:
yc - n = (xt - xf) * factor;
xc - m = (yf - yt) * factor;
这两行公式中都各存在两个位置参数(yc、factor) 和 (xc、factor);所以只要找到一个合适的factor就能够得到合适的圆心进而得到半径起始角和终止角以及半径。有了这些那么Marker的指向、Spark的轨迹都可以确定了。
现在需要做的是把上述过程转换为代码:
var Arc = (function() {
var A = function(options) {
var startX = options.startX,
startY = options.startY,
endX = options.endX,
endY = options.endY; //两点之间的圆有多个,通过两点及半径便可以定出两个圆,根据需要选取其中一个圆
var L = Math.sqrt(Math.pow(startX - endX, 2) + Math.pow(startY - endY, 2));
var m = (startX + endX) / 2; // 横轴中点
var n = (startY + endY) / 2; // 纵轴中点
var factor = 1.5; var centerX = (startY - endY) * factor + m;
var centerY = (endX - startX) * factor + n; var radius = Math.sqrt(Math.pow(L / 2, 2) + Math.pow(L * factor, 2));
var startAngle = Math.atan2(startY - centerY, startX - centerX);
var endAngle = Math.atan2(endY - centerY, endX - centerX); // this.L = L;
this.startX = startX;
this.startY = startY;
this.endX = endX;
this.endY = endY;
this.centerX = centerX;
this.centerY = centerY;
this.startAngle = startAngle;
this.endAngle = endAngle;
this.startLabel = options && options.labels && options.labels[0],
this.endLabel = options && options.labels && options.labels[1],
this.radius = radius;
this.lineWidth = options.width || 1;
this.strokeStyle = options.color || '#000';
this.shadowBlur = options.shadowBlur;
}; A.prototype.draw = function(context) {
context.save();
context.lineWidth = this.lineWidth;
context.strokeStyle = this.strokeStyle;
context.shadowColor = this.strokeStyle;
context.shadowBlur = this.shadowBlur || 2; context.beginPath();
context.arc(this.centerX, this.centerY, this.radius, this.startAngle, this.endAngle, false);
context.stroke();
context.restore(); context.save();
context.fillStyle = this.strokeStyle;
context.font = "15px sans-serif";
if (this.startLabel) {
context.fillText(this.startLabel, x, y);
}
if (this.endLabel) {
context.fillText(this.endLabel, x, y);
}
context.restore();
}; return A;
})();
理解了上述过程,我们就已经成功了一半。下一步的重点就是动画的绘制。关于动画首先要了解requestAnimationFrame,不知道的小伙伴要去恶补一下啦。好啦,言归正传。Spark动画分为两部分,一部拖尾效果,一部分是弧线光晕效果,当你第一次打开时候会发现,弧线光晕会随着小箭头运动,到达终点后光晕停止运动,剩下小箭头自己运动。由于每个圆的大小不一致,我们需要在每次动画过程中控制光晕和小箭头的位置。
由于他们是在圆弧上运动,所以我们只要每次计算出他们新的弧度就可以了,弧度的步长可以这样来制定:每走过20像素所转过的弧度就是各个Spark的步长啦。
所以factor = 20 / radius;
每次绘制时,光晕与小箭头的弧度位置为:
var endAngle = this.endAngle;
// 匀速
var angle = this.trailAngle + this.factor;
弧度确定之后我们就能得到小箭头的位置,但是目前并不能得到小箭头的方向。根据canvas中角度的特点,再由简单的几何知识,可以得到小箭头的旋转方向应该为:rotation = angle + Math.PI / 2;
目前为止我们解决了Spark动画中的两大问题,剩下了最后一个:拖尾效果。看起来由粗到细这段就是拖尾效果。
实际上为了保证在移动端的性能,本次实例中并没有明显的拖尾。但拖尾还是一个比较常见的特效,所以我们需要把它掌握。拖尾效果一般是对一个元素进行多次复制,并线性的渐变这队影元素的大小宽度以及颜色的透明度在达到由粗到细由大到小颜色有深变浅的效果。那么每次绘制时候都需要知道这队影元素每个的位置,每个的线宽以及每个的颜色,根据上面讨论的元素位置需要根据弧度来确定。我们说过他们的位置是渐变的,渐变的步长可以这样指定,假设从头到尾的弧长为80,那么每个影元素的之间的间隔为:
this.deltaAngle = (80 / Math.min(this.radius, 400)) / this.tailPointsCount;
由此便可绘制出拖尾效果:
// 拖尾效果
var count = this.tailPointsCount;
for (var i = 0; i < count; i++) {
var arcColor = utils.calculateColor(this.strokeStyle, 0.3-0.3/count*i);
var tailLineWidth = 5;
if (this.trailAngle - this.deltaAngle * i > this.startAngle) {
this.drawArc(context, arcColor,
tailLineWidth - tailLineWidth / count * i,
this.trailAngle - this.deltaAngle * i,
this.trailAngle
);
}
}
所以整个Spark的代码如下:
var Spark = (function() {
var S = function(options) {
var startX = options.startX,
startY = options.startY,
endX = options.endX,
endY = options.endY; //两点之间的圆有多个,通过两点及半径便可以定出两个圆,根据需要选取其中一个圆
var L = Math.sqrt(Math.pow(startX - endX, 2) + Math.pow(startY - endY, 2));
var m = (startX + endX) / 2; // 横轴中点
var n = (startY + endY) / 2; // 纵轴中点
var factor = 1.5; var centerX = (startY - endY) * factor + m;
var centerY = (endX - startX) * factor + n; var radius = Math.sqrt(Math.pow(L / 2, 2) + Math.pow(L * factor, 2));
var startAngle = Math.atan2(startY - centerY, startX - centerX);
var endAngle = Math.atan2(endY - centerY, endX - centerX); // 保证Spark的弧度不超过Math.PI
if (startAngle * endAngle < 0) {
if (startAngle < 0) {
startAngle += Math.PI * 2;
endAngle += Math.PI * 2;
} else {
endAngle += Math.PI * 2;
}
} this.tailPointsCount = 5; // 拖尾点数
this.centerX = centerX;
this.centerY = centerY;
this.startAngle = startAngle;
this.endAngle = endAngle;
this.radius = radius;
this.lineWidth = options.width || 5;
this.strokeStyle = options.color || '#000';
this.factor = 2 / this.radius;
this.deltaAngle = (80 / Math.min(this.radius, 400)) / this.tailPointsCount;
this.trailAngle = this.startAngle;
this.arcAngle = this.startAngle; this.animateBlur = true; this.marker = new Marker({
x: 50,
y:80,
rotation: 50 * Math.PI / 180,
style: 'arrow',
color: 'rgb(255, 255, 255)',
size: 2,
borderWidth: 0,
borderColor: this.strokeStyle
});
}; S.prototype.drawArc = function(context, strokeColor, lineWidth, startAngle, endAngle) {
context.save();
context.lineWidth = lineWidth;
// context.lineWidth = 5;
context.strokeStyle = strokeColor;
context.shadowColor = this.strokeStyle;
// context.shadowBlur = 5;
context.lineCap = "round";
context.beginPath();
context.arc(this.centerX, this.centerY, this.radius, startAngle, endAngle, false);
context.stroke();
context.restore();
}; S.prototype.draw = function(context) {
var endAngle = this.endAngle;
// 匀速
var angle = this.trailAngle + (endAngle - this.startAngle) * this.factor;
var strokeColor = this.strokeStyle;
if (this.animateBlur) {
this.arcAngle = angle;
}
this.trailAngle = angle;
strokeColor = utils.calculateColor(strokeColor, 0.1); this.drawArc(context, strokeColor, this.lineWidth, this.startAngle, this.arcAngle); // 拖尾效果
var count = this.tailPointsCount;
for (var i = 0; i < count; i++) {
var arcColor = utils.calculateColor(this.strokeStyle, 0.3-0.3/count*i);
var tailLineWidth = 5;
if (this.trailAngle - this.deltaAngle * i > this.startAngle) {
this.drawArc(context, arcColor,
tailLineWidth - tailLineWidth / count * i,
this.trailAngle - this.deltaAngle * i,
this.trailAngle
);
}
} context.save();
context.translate(this.centerX, this.centerY);
this.marker.x = Math.cos(this.trailAngle) * this.radius;
this.marker.y = Math.sin(this.trailAngle) * this.radius;
this.marker.rotation = this.trailAngle + Math.PI / 2;
this.marker.draw(context);
context.restore(); if ((endAngle - this.trailAngle) * 180 / Math.PI < 0.5) {
this.trailAngle = this.startAngle;
this.animateBlur = false;
}
}; return S;
})();
Spark源码
到目前为止,迁徙图中主要的技术难点就已经讲完了。但如何把它放到地图上,这个问题我们将在下篇文章中讨论。
手把手教你DIY一个春运迁徙图(一)的更多相关文章
- iOS回顾笔记(05) -- 手把手教你封装一个广告轮播图框架
html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,bi ...
- R数据分析:跟随top期刊手把手教你做一个临床预测模型
临床预测模型也是大家比较感兴趣的,今天就带着大家看一篇临床预测模型的文章,并且用一个例子给大家过一遍做法. 这篇文章来自护理领域顶级期刊的文章,文章名在下面 Ballesta-Castillejos ...
- 只有20行Javascript代码!手把手教你写一个页面模板引擎
http://www.toobug.net/article/how_to_design_front_end_template_engine.html http://barretlee.com/webs ...
- PWA入门:手把手教你制作一个PWA应用
摘要: PWA图文教程 原文:PWA入门:手把手教你制作一个PWA应用 作者:MudOnTire Fundebug经授权转载,版权归原作者所有. 简介 Web前端的同学是否想过学习app开发,以弥补自 ...
- 用Python手把手教你搭一个Transformer!
来源商业新知网,原标题:百闻不如一码!手把手教你用Python搭一个Transformer 与基于RNN的方法相比,Transformer 不需要循环,主要是由Attention 机制组成,因而可以充 ...
- 手把手教你画一个 逼格满满圆形水波纹loadingview Android
才没有完结呢o( ̄︶ ̄)n .大家好,这里是番外篇. 拜读了爱哥的博客,又学到不少东西.爱哥曾经说过: 要站在巨人的丁丁上. 那么今天,我们就站在爱哥的丁丁上来学习制作一款自定义view(开个玩笑,爱 ...
- 手把手教你DIY尼康ML-L3红外遥控器
项目介绍 ML-L3是用于尼康部分型号相机的无线红外遥控器,可以通过红外方式来控制快门的释放,支持B门拍摄.官方售价100RMB左右,山寨版售价10RMB左右.虽然也能实现基本的遥控功能,但是功能还是 ...
- 手把手教你做一个python+matplotlib的炫酷的数据可视化动图
1.效果图 2.注意: 上述资料是虚拟的,为了学习制作动图,构建的. 仅供学习, 不是真实数据,请别误传. 当自己需要对真实数据进行可视化时,可进行适当修改. 3.代码: #第1步:导出模块,固定 i ...
- 手把手教你画AndroidK线分时图及指标
先废话一下:来到公司之前.项目是由外包公司做的,面试初,没有接触过分时图k线这块,认为好难,我能搞定不.可是一段时间之后,发现之前做的那是一片稀烂,可是这货是主功能啊.迟早的自己操刀,痛下决心,开搞, ...
随机推荐
- cf D. Physical Education and Buns
http://codeforces.com/contest/394/problem/D 题意:给你n个数,然后通过操作使得这n个数变为一个等差数列,操作是可以经过小于等于k次加1或减去1,要使得k尽量 ...
- 理解Java机制最受欢迎的8幅图
原文链接: Top 8 Diagrams for Understanding Java 翻译人员: 铁锚 翻译时间: 2013年10月29日 世间总是一图胜过千万言! 下面的8幅图来自于 Progr ...
- 「Poetize8」Divisible
描述 Description 设F[i]为斐波那契数列的第i项,F[1]=1,F[2]=1,F[i]=F[i-1]+F[i-2](i>=3). 输入格式 InputFormat 输入包含若干行( ...
- 禁用页面及页面所有frame内的右键菜单
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"><HTML> <HEAD& ...
- codeforce-191E-Thwarting Demonstrations(树状数组+二分+离散)
题意: 求第K 大连续区间 分析: 二分答案,再n * log(n)判断有几个区间的区间和大于mid,然后调整上下界,使这个值不断的接近k. 判断符合条件的区间总数:线性扫描sum[n](前n项和) ...
- Oracle用户管理
创建用户 概述:在oracle中要创建一个新的用户使用create user 语句,一般是具有dba(数据库管理员)的权限才能使用. create user 用户名 identified by 密码: ...
- Interesting Punch-Bowl(优先队列)
/** http://acm.nyist.net/JudgeOnline/problem.php?pid=547 题意: 有一个正方形的区域 区域上面有高度不同的1*1的立方体自然有凸有凹 凹的地方可 ...
- Redis sort命令
http://www.cnblogs.com/linjiqin/archive/2013/06/14/3135921.html 1.添加 投票选项到 redis的 List 和HashMap lis ...
- vs2008 多人同时开发项目时的代码注释规范格式 分类: C#小技巧 2014-04-23 14:12 297人阅读 评论(0) 收藏
多人同时开发一个项目,区分项目的那个窗体是谁开发的,例:下面的格式 /************************************************ 模块:服务器设置 ...
- 程序启动原理和UIApplication
iOS开发UI篇—程序启动原理和UIApplication 一.UIApplication 1.简单介绍 (1)UIApplication对象是应用程序的象征,一个UIApplication对象就 ...