0%

Data Visualization and D3.js 笔记(6)

这一节(Lesson 8)实现动画效果。

Lesson 8 动画与互动

地图

数据类型

  • shapefile 二进制
  • GeoJSON 可读性,数据多,臃肿
  • TopoJSON 拓扑结构

投影

墨卡托投影是一种“等角横轴割圆柱投影”,椭圆柱割地球于南纬80度、北纬84度两条等高圈,投影后两条相割的经线上没有变形,而中央经线上长度比0.9996。
墨卡托投影的过程其实非常简单,就是将地球展开成一个圆柱,再将圆柱展开成平面。

主题地图

  • dot 点,颜色,负空间,eg瘟疫地图
  • choropleth 等值线图,根据地区数据进行补色
  • cartogram 变形地图,根据数据值改变区域、形状和尺寸
  • graduated symbol 符号渐变地图

利用d3绘制地图

static map

GeoJSON有很多常见地理区域的数据文件,可以直接使用。
d3内置函数json()可以载入JSON文件

1
d3.json('world_countries.json', draw);

从SVG路径绘制地图

1
2
3
4
5
6
7
8
9
10
11
12
13
function draw(geo_data) {
// ...
var projection = d3.geo.mercator();//墨卡托投影法
// 类似于用scale将整数或浮点数转换成像素点的映射
// 把经度和维度转换为像素域
var path = d3.geo.path.projection(projection);
// 构建SVG对象来正确呈现这些像素
var map = d3.selectAll('path') //空选择
.data(geo_data.features) // 检查geo_data,了解其数据结构,.features与国家坐标的数组相对应
.enter()
.append('path')
.attr('d', path) // 将d属性设置为上面创建的路径对象;将顶点或值填在d属性中,认为是路径是数据,指定SVG路径用于绘图
}

绘制并更改地图

1
2
3
4
5
6
7
8
9
10
11
12
var projection = d3.geo.mercator()
.sacle(170) // 取得或设置投影的缩放系数
.translate([width / 2, height / 2]); //取得或设置投影的平移位置

var map = d3.selectAll('path')
.data(geo_data.features)
.enter()
.append('path')
.attr('d', path)
.style('fill', 'lightBlue')
.style('stroke', 'black')
.style('stroke-width', 0.5)

bubble map

为地图添加表示主办国的circle.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function draw(geo_data) {
// ...
// ...

function plot_points(data) {

};
var format = d3.time.format('%d-%m-%T (%H:%M h)')
d3.tsv('world_cup_geo.tsv', function(d) {
d.attentance = +d.attentance;
d.date = format.parse(d.date);
return d;
}, plot_points);
}

d3的嵌套Nest

嵌套允许数组中的元素被组织为分层树型结构;类似SQL语句里面的GROUP BY方法,但不能多级别分组,而且输出的结果是树而不是一般的表。树的层级由key方法指定。树的叶节点可以按值来排序,而内部节点可以按键来排序。可选参数汇总(rollup)函数可以使用加法函数瓦解每个叶节点的元素. nest 操作符(d3.nest返回的对象)是可以重复使用的,不保留任何嵌套数据的引用。

1
2
3
4
5
6
7
8
var yields = [{yield: 27.00, variety: "Manchuria", year: 1931, site: "University Farm"},
{yield: 48.87, variety: "Manchuria", year: 1931, site: "Waseca"},
{yield: 27.43, variety: "Manchuria", year: 1931, site: "Morris"},
...]
var nest = d3.nest()
.key(function(d) { return d.years; })
.key(function(d) { return d.variety; })
.entries(yields);

返回的嵌套数组中。每个元素的外部数组是键-值对,列出每个不同键的值:

1
2
3
4
5
6
7
8
9
[{key: 1931, values: [
{key: "Manchuria", values: [
{yield: 27.00, variety: "Manchuria", year: 1931, site: "University Farm"},
{yield: 48.87, variety: "Manchuria", year: 1931, site: "Waseca"},
{yield: 27.43, variety: "Manchuria", year: 1931, site: "Morris"}, ...]},
{key: "Glabron", values: [
{yield: 43.07, variety: "Glabron", year: 1931, site: "University Farm"},
{yield: 55.20, variety: "Glabron", year: 1931, site: "Waseca"}, ...]}, ...]},
{key: 1932, values: ...}]

d3.nest()创建一个新的操作符,keys集合初始为空。如果map或entries操作符在任何键函数key被注册之前被调用,这个嵌套操作符通常返回输入数组。

nest.key(function)注册一个新的键函数function,键函数将被输入数组中的每个函数调用,并且必须返回一个用于分配元素到它的组内的字符串标识符。通常,这个函数被实现为一个简单的访问器,如上面例子中year和variety的访问器。 输入的数组的引导(index)并没有传递给function。每当一个key 被注册,它被放到一个内部键数组的末端,和由此产生的map或实体将有一个额外的层级。当前没有一个方法可以删除或查询注册的键。最近注册的键在后续的方法中被当作当前键。

nest.rollup(function)为每组中的叶子元素指定汇总函数(rollup)function。汇总函数的返回值会覆盖叶子值数组。不论是由map操作符返回的关联数组,还是实体操作符返回的每个实体的值属性。

nest.map(array[, mapType])对指定的数组使用nest操作符,返回一个关联数组。返回的关联数组array中每个实体对应由第一个key函数返回的不同的key值。实体值决定于注册的key函数的数量:如果有一个额外的key,值就是另外一个嵌套的关联数组;否则,值就是过滤自含有指定key值得输入数组array的元素数组。

nest.entries(array)为指定的array参数应用nest操作符,返回一个键值对数组。

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
function plot_points(data) {
var nested = d3.nest()
.key(function(d) {
return d.date.getUTCFullYear();
})
.rollup(function(leaves) {
var total = d3.sum(leaves, function(d) {
return d.attendance;
});
var coords = leaves.map(function(d) {
return projection([+d.long, +d.lat]);
});
var center_X = d3.mean(coords, function(d) {
return d[0];
});
var center_Y = d3.mean(coords, function(d) {
return d[1];
});
return {
'attendance': total,
'x': center_X,
'y': center_Y
}
})
.entries(data)
}

nested返回的是包含20个元素的数组,这20个元素对应世界杯的举办次数。每个元素的key值为年份,values值是attendance和x与y坐标。

添加圆圈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
svg.append('g')
.attr('class', 'bubble')
.selectAll('circle')
.data(nested)
.enter()
.append('circle')
.attr('cx', function(d) {
return d.values.x;
})
.attr('cy', function(d) {
return d.values.y;
})
.attr('r', function(d) {
return radius(d.values['attendance']);
})
.style('stroke', 'black')
.style('stroke-width', 0.7)
.style('fill', 'rgb(247,148,32)')
.style('opacity', 0.7);

使用圆圈表达数据时,需要注意,圆圈表示的数据是半径的平方
要避免这个问题,数据值应匹配圆圈的面积。可以将数据值开平方,以确定每个圆圈的半径。

1
2
3
4
5
6
var attendance_max = d3.max(nested, function(d) {
return d.values.attendance;
})
var radius = d3.scale.sqrt()
.domain([0, attendance_max])
.range([0, 15]);

就绘图顺序对数据进行排序

不干预数据的绘制顺序,可能会出现大圆遮挡小圆的情况。通过数据分类,我们能避免这种情况。

1
2
3
4
5
svg.append('g')
//...
.data(nested.sort(function(a, b) {
return b.values.attendance > a.values.attendance;
}))

author driven narrative (animation)

当前的设计为了保留空间信息而丢失了时间信息。

动画可以传达变化的时间数据。

更新函数

update函数应该做到:

  1. filter data filter
  2. remove any elements which no longer belong there .exit()
  3. add any new elements .enter()
    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
    function key_func(d) {
    return d.key;
    }
    function update(year) {
    // 过滤得到指定年份的数据
    var filter = nested.filter(function(d) {
    return new Date(d.key).getUTCFullYear() === year;
    });

    // 更新标题数据
    d3.select('h2')
    .text('World Cup ' + year);

    // 选择页面上所有的circle
    var circles = svg.selectAll('circle')
    .data(filter, key_func); //做了update,执行完只有一个圆圈被选中

    circles.exit() // 包含20个元素的数组(包括一个parentNode);exit就是本页面未包含的内容
    .remove(); // 移除由exit查找出的不属于页面的元素进行移除

    circles.enter() // 对应包含1个元素的数组,含有'update'属性
    .append('circle')
    .transition()
    .duration(500)
    .attr('cx', function(d) {
    return d.values.x;
    })
    .attr('cy', function(d) {
    return d.values.y;
    })
    .attr('r', function(d) {
    return radius(d.values['attendance']);
    });

    // 突出当前年参加的国家
    var countries = filter[0].values['teams'];
    function update_countries(d) {
    if(countries.indexOf(d.properties.name) !== -1){
    return 'lightBlue';
    } else {
    return 'white';
    }
    }
    svg.selectAll('path')
    .transition() // 使用过渡来平滑化动画
    .duration(500)
    .style('fill', update_countries);
    //.style('stroke', update_countries)
    };

data, enter和exit

selection.data([values[, key]])
连接指定的一组数据的和当前选择。指定的values是一组数据值(例如,数字或对象)或一个函数返回一组值。如果没有指定key函数,则values的第一数据被分配到当前选择中第一元素,第二数据分配给当前选择的第二个元素,依此类推。当数据被分配给一个元素,它被存储在属性__data__中,从而使数据“沾粘”,从而使数据可以再选择。
data操作的结果是update选择;这表示选择的DOM元素已成功绑定到指定的数据元素。update选择还包含对enterexit的选择,对应于添加和删除数据节点。
key函数可以被指定为控制数据是如何连接元素。key函数,基于先前结合的数据返回一个用于连接数据和相关的元素的字符串。例如,如果每个数据都有一个唯一的字段name,该连接可以被指定为.data(data, function(d) { return d.name; })

selection.enter()
返回输入(enter)选择:当前选择中存在但是当前DOM元素中还不存在的每个数据元素的占位符节点。此方法只在由data运算符返回的更新选择中定义。此外,输入选择只定义了appendinsertselectcall操作符;必须使用这些操作符在修改任何内容之前实例化输入元素。

selection.exit()
返回退出(exit)选择:找出在当前选择存在的DOM元素中没有新的数据元素。此方法只被定义在data运算符返回的更新选择。exit选择定义了所有的正常操作符,但通常你主要使用的是remove;其他操作符存在的主要目的是让您可以根据需要定义一个退出的过渡。

添加动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var years = [];

for(var i=1930; i < 2015; i += 4) {
if(i !== 1942 && i != 1946) // 这两年因为二战没有举办世界杯
years.push(i);
}

var year_idx = 0;

var year_interval = setInterval(function() {
update(years[year_idx]);

year_idx ++;
if(year_idx >= years.length){ // 停止条件
clearInterval(year_interval);
}
}, 1000);

reader driven narrative (interactive)

在动画结束之后,提供按钮以供读者自行点选年份进行比较。

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
var year_interval = setInterval(function() {
update(years[year_idx]);

year_idx ++;
if(year_idx >= years.length){
clearInterval(year_interval);
var buttons = d3.select('body')
.append('div')
.attr('class', 'years_buttons')
.selectAll('div')
.data(years)
.enter()
.append('div')
.text(function(d) {
return d;
});
buttons.on('click', function(d) {
// 先将目前的特殊样式进行清理
d3.select('.years_buttons')
.selectAll('div')
.transition()
.duration(500)
.style('background', 'rgb(251, 201, 127)')
.style('color', 'black');

d3.select(this)
.transition()
.duration(500)
.style('background', 'lightBlue')
.style('color', 'white');
update(d);
});
}
}, 1000);