手把手教你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线这块,认为好难,我能搞定不.可是一段时间之后,发现之前做的那是一片稀烂,可是这货是主功能啊.迟早的自己操刀,痛下决心,开搞, ...
随机推荐
- Area
http://poj.org/problem?id=1265 #include<cstdio> #include<istream> #include<algorithm& ...
- 23个经典JDK设计模式(转)
下面是JDK中有关23个经典设计模式的示例: Structural(结构模式) Adapter: 把一个接口或是类变成另外一种. o ● java.util.Arrays#asList() o ...
- 【HDOJ】2531 Catch him
简单BFS.就是要把所有的D点当成一个整体考虑(整体移动). /* 2531 */ #include <iostream> #include <queue> #include ...
- Linux&shell 之Linux文件权限
写在前面:案例.常用.归类.解释说明.(By Jim) Linux文件权限用户useradd test (添加用户test)userdel test (删除用户test)passwd test(修改用 ...
- HDU 1269 迷宫城堡 【强联通分量(模版题)】
知识讲解: 在代码里我们是围绕 low 和 dfn 来进行DFS,所以我们务必明白 low 和 dfn 是干什么的? 有什么用,这样才能掌握他. 1. dfn[] 遍历到这个点的时间 2. ...
- HDOJ(HDU) 2123 An easy problem(简单题...)
Problem Description In this problem you need to make a multiply table of N * N ,just like the sample ...
- cf702A Maximum Increase
A. Maximum Increase time limit per test 1 second memory limit per test 256 megabytes input standard ...
- UVAlive3415 Guardian of Decency(最大独立集)
题目链接:http://acm.hust.edu.cn/vjudge/problem/viewProblem.action?id=34831 [思路] 二分图的最大独立集. 即在二分图中选取最多的点, ...
- 怎么用copy关键字
出题者简介: 孙源(sunnyxx),目前就职于百度 整理者简介:陈奕龙,目前就职于滴滴出行. 转载者:豆电雨(starain)微信:doudianyu 用途: NSString.NSArray.NS ...
- maven clean 报错
eclipse在使用maven的tomcat控件编译java程序时,报错 Failed to execute goal org.apache.maven.plugins:maven-clean-plu ...