文章目录
数据绑定
DataJoin
本质: 数据与图元进行绑定,即绑定后,每个图元就有了自己的语义。
如demo2中,将数据中地区字段与图元绑定,那么图元就代表了地区
Eg:
- 每个国家的人数绑定到矩形的长度
- 疫情感染的人数比例绑定到圆的半径
目的
- 使用Data-Join可以省区大量根据数据设置图元属性的代码量
- 为数据动态更新提供统一接口
问题
//错误的绑定方式
d3.selectAll('rect').data(data).attr('width', d=>xScale(d.value))
但是上述绑定会出现一些问题,因为如果data() 不指定key的话,会默认按照索引进行绑定,因此若数据集改变了索引,如 [ {name: A , value: 20} ,{name : B , value : 60} ] 改成了 [ {name : B , value : 60} , {name: A , value: 20}] ,绑定的时候不指定key就会出问题
解决方式 -> 实现数据绑定的绑定函数为data(data, keyFunction),使用keyFuntion 为数据绑定key。
//正确绑定方式
d3.selectAll('rect').data(data, d=> d.name).attr('width', d=> d.value)
通过keyFunciont绑定的数据,重复数据也只会绑定为一个图元
注意!
但是这里的keyFunction有一个注意事项,如果是第一次绑定,必须是根据索引进行绑定的。因此如果第一次绑定就指定keyFunction,就会报错。
D3.js绑定数据的三个状态:
-
Update
图元和数据条目相同,即上文中介绍的均为单纯的update
var update = d3.select().data(dataset) //该语句获得update 状态的 selection
-
Enter
数据的条目多于图元,甚至没有图元,常用于第一次数据绑定
var enter = update.enter() //获得enter状态的selection var enterSel = enter.append('circle') // 绑定图元
-
Exit
数据的条目少于图元,常用于结束可视化
var exit = update.exit() //获取exit 状态的图元 exit.remove() //删除exit状态的图元
Enter
const p = maingroup.selectAll('.class').data(data).enter().append('').attr('')
enter() 方法:
- 给不存在的数据绑定的图元提供一个占位符
相当于在selectAll()方法选择的时候,没有选择到图元。那么设置一个占位符,设置完毕后,.append()方法可以为数据添加一个图元,如append(‘rect’)。
append() 方法:
- 事实上,调用了append之后,data-join 就会从Enter模式切换为Update模式
总结
-
enter() 方法本质上是生成了一个指向父节点的指针,而append()操作相当于在父节点后面添加指针数量的图元并将其与多出的数据绑定
-
⚠️ 一般在Enter时,data() 是不需要指定keyFunction的。因为这是第一次绑定,必须是根据索引进行绑定的。
Update
- Update是可视化任务中最常用的状态
- 用D3实现动画时,是基于Update 状态的
Exit
- 有图元没有数据了,就需要Exit()
const p = maingroup.selectAll('.class').data(data).exit().remove() //{..后面的和前两种状态是一样的,可以设置属性,动画等}
data.join
【老方法】
因为一般的数据绑定流程为
1. 先绑定,并获取update部分:var ele = svg.selectAll().data()。
2. 然后再删除exit()部分: ele.exit().remove()
3. 最后再添加enter()部分 ele = ele.enter().append()
注意: 调用enter后,会生成一个新的图元集合 others。这里可以用merge把 enter()部分和update部分合并,即:
ele = ele.enter().append().merge(ele)
以达到一次性更新所有存在画布上的图元**(enter部分,update 部分)**的属性
【新方法】
还有一种比较简洁的方法,d3.js Version4+ 提供了join()方法,直接省去了merge那步
d3.selectAll().data(dataset).join()
等价于
d3.selectAll().data(dataset).enter().append('图元').merge(update)
非常方便
D3 实现动画
示例代码:
d3.selectAll('rect').data(data,d=>d.name).transition().duration(1000).attr('width',d=>xScale(d.value))
解释:
- transition() 表示后面设置属性 attr() 均采用动画的形式进行
- duration() 表示动画持续时间,单位为ms
D3 数据读取
d3.csv('host/dictionary/fileName').then(data => {})
- d3.csv() 函数的返回值是一个JS的Promise对象,该对象用于执行异步操作
- .then()的参数为一个函数,其参数为.csv() 的返回值
- 也就是说,d3.csv() 会正常向服务器请求数据,在请求并处理好之后,将结果扔给then()中的回调函数。
但这也会引发跨域问题: CORS
- 如果d3.csv读取的是主机本地的路径,那么会报跨域错误,这就是为什么d3编程需要一台服务器
基本编程思路
- 将默认加载的元素封装为一个函数renderInit()
- 将需要动态加载的元素封装成一个函数
本节课示例代码 (demo2-动态散点图.html)
<!DOCTYPE html>
<html>
<head>
<title>Scatter-Simple</title>
<script src="./js/d3.min.js"></script>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
</head>
<body style="text-align: center">
<svg width="1500" height="600" id="mainsvg" class="svgs" style="background-color: #ffffff;"></svg>
<script>
// get main SVG and its attributes & setting hyper-parameters;
const svg = d3.select('#mainsvg');
const width = +svg.attr('width');
const height = +svg.attr('height');
const margin = {top: 100, right: 120, bottom: 100, left: 90};
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
let xScale, yScale;
const xAxisLabel = '累计确诊人数(对数)';
const yAxisLabel = '新增人数(对数)';
let alldates;
let sequantial;
//提升代码的可拓展性
//因为是要把d和图元进行绑定,如果每次想要取得d中每个特定属性值的时候
//可以将取值函数定义在最前面,之后如果相对某个属性进行取值得时候,可以把xValue和yValue作为回调函数丢进去就可以了。
//且如果改横纵坐标值,改这个地方就可以了,就不用大段更改代码
//实际上是先将数据转换为对数值,再去映射
let xValue = d => Math.log(d['确诊人数']+1);
let yValue = d => Math.log(d['新增确诊']+1);
let rValue = d => Math.sqrt(d['扩散指数']*500)*0.8;
var color = {
"武汉":"#ff1c12",
"黄石": "#de5991",
"十堰": "#759AA0",
"荆州": "#E69D87",
"宜昌": "#be3259",
"襄阳": "#EA7E53",
"鄂州": "#EEDD78",
"荆门": "#9359b1",
"孝感": "#47c0d4",
"黄冈": "#F49F42",
"咸宁": "#AA312C",
"恩施州": "#B35E45",
"随州": "#4B8E6F",
"仙桃": "#ff8603",
"天门": "#ffde1d",
"潜江": "#1e9d95",
"神农架": "#7289AB"
}
/**
* 初始化函数
* 定义比例尺
* 添加坐标轴
*/
const renderinit = function(data){
// Linear Scale: Data Space -> Screen Space;
xScale = d3.scaleLinear()
.domain([d3.min(data, xValue), d3.max(data, xValue)]) // "extent" is equivalent to [d3.min(data, xValue), d3.max(data, xValue)];
.range([0, innerWidth])
.nice(); //不加nice()会使得坐标轴多出一截
// Introducing y-Scale;
yScale = d3.scaleLinear()
.domain(d3.extent(data, yValue).reverse()) // remember to use reverse() to make y-axis start from the bottom;
//d3 的纵坐标轴,默认是从左上角开始的,因此应该将小一点的数据值映射到大一点的坐标值,可以对domain进行reverse(), 也可以对range() 进行reverse()
.range([0, innerHeight])
.nice();
// The reason of using group is that nothing is rendered outside svg, so margin of svg is always blank while margin of group is rendered inside svg;
const g = svg.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`)
.attr('id', 'maingroup');
// Adding axes;
const yAxis = d3.axisLeft(yScale)
.tickSize(-innerWidth)
//.tickFormat(d3.format('.2s'))
.tickPadding(10); // .tickPadding is used to prevend intersection of ticks;
const xAxis = d3.axisBottom(xScale)
//.tickFormat(d3.format('.2s'))
.tickSize(-innerHeight)
.tickPadding(10);
let yAxisGroup = g.append('g').call(yAxis)
.attr('id', 'yaxis');
yAxisGroup.append('text')
.attr('font-size', '2em')
.attr('transform', `rotate(-90)`)
.attr('x', -innerHeight / 2)
.attr('y', -60)
.attr('fill', '#333333')
.text(yAxisLabel)
.attr('text-anchor', 'middle') // Make label at the middle of axis.
yAxisGroup.selectAll('.domain').remove(); // we can select multiple tags using comma to seperate them and we can use space to signify nesting;
let xAxisGroup = g.append('g').call(xAxis)
.attr('transform', `translate(${0}, ${innerHeight})`)
.attr('id', 'xaxis');
xAxisGroup.append('text')
.attr('font-size', '2em')
.attr('y', 60)
.attr('x', innerWidth / 2)
.attr('fill', '#333333')
.text(xAxisLabel);
xAxisGroup.selectAll('.domain').remove();
//图例
var legend_color = [
"#ff1c12",
"#de5991",
"#759AA0",
"#E69D87",
"#be3259",
"#EA7E53",
"#EEDD78",
"#9359b1",
"#47c0d4",
"#F49F42",
"#AA312C",
"#B35E45",
"#4B8E6F",
"#ff8603",
"#ffde1d",
"#1e9d95",
"#7289AB"
]
var legend_name = ["武汉市",
"黄石市",
"十堰市",
"荆州市",
"宜昌市",
"襄阳市",
"鄂州市",
"荆门市",
"孝感市",
"黄冈市",
"咸宁市",
"恩施州",
"随州市",
"仙桃市",
"天门市",
"潜江市",
"神农架",
];
//draw
//每一行图例设置为一个group
let legend = d3.select('#maingroup').selectAll('.legend')
.data(legend_name)
.enter()
.append('g')
.attr('class','legend' )
.attr('transform', function(d,i){
return `translate(${innerWidth+10},${i*25+10})`
})
//图例矩形
legend.append('rect')
.data(legend_name)
.attr('x',0 )
.attr('y',0 )
.attr('width', 30)
.attr('height', 10)
.attr('fill', function(d,i){
return legend_color[i]
})
//图例文字
legend.append("text")
.data(legend_name)
.attr('class', 'legend_text')
.attr("x", 40)
.attr("y", 9)
.attr("dy", ".06em")
.style("text-anchor", "start")
.text(function (d,i) {return legend_name[i];});
//title
d3.select('#maingroup')
.append('text')
.text("Demo2-疫情动态散点图")
.attr('text-anchor', 'middle')
.attr('font-size', '2em')
.attr('transform', `translate(${innerWidth / 2},-10)`)
};
const renderUpdate = function(seq){
const g = d3.select('#maingroup');
time = seq[0]['日期'];
g.selectAll('.date_text').remove();
//添加日期
g.append("text")
.data(['seq'])
.attr('class', 'date_text')
.attr('x', innerWidth/4+30)
.attr('y', innerHeight/10-10)
.attr('dy', '.5em')
.attr('text-anchor','end' )
.attr('fill', '#504f4f')
.attr('font-size', '4em')
.attr('font-weight', 'bold')
.text(time)
let circleUpdate = g.selectAll('circle').data(seq,d => d['地区']);
let circleEnter = circleUpdate.enter().append('circle')
.attr('cx', d => xScale(xValue(d)) )
.attr('cy', d => yScale(yValue(d)) )
.attr('r',d => rValue(d))
.attr('fill', d => color[d['地区']] )
.attr('opacity',0.8)
circleUpdate.merge(circleEnter).transition().ease(d3.easeLinear).duration(1000)
.attr('cx', d => xScale(xValue(d)) )
.attr('cy', d => yScale(yValue(d)) )
.attr('r',d => rValue(d));
//ease:更新模式(幅度)
}
d3.csv('./data/hubeinxt.csv').then( data => {
data = data.filter( d => d['地区'] !== '总计');//数据清洗
data.forEach(d => {
d['确诊人数'] = +(d['确诊人数']);
d['新增确诊'] = +(d['新增确诊']);
d['扩散指数'] = +(d['扩散指数']);
if(d['新增确诊']<0){
d['新增确诊'] = 0;
}
console.log(data)
});
allDates = data.map(d => d['日期']);
//console.log(allDates); 发现有很多日期是重复的
allDates = Array.from(new Set(allDates));//用集合去掉重复值
//对日期进行排序
allDates = allDates.sort((a,b)=>{
return new Date(a)- new Date(b);
});
//设置一个空array,用于存放每一天的数据,每一天的数据也是array
sequantial = [];
allDates.forEach(d =>{
sequantial.push([])
});
//根据日期的索引,将对应日期的数据push进 sequentail数组中。
data.forEach(d => {
sequantial[ allDates.indexOf(d['日期'])].push(d);
});
renderinit(data);
let c = 0; //sequential的索引
let intervalId = setInterval(()=> {
if (c>allDates.length ) {
clearInterval(intervalID);
}else{
renderUpdate(sequantial[c]);
c = c+1 ;
}
}, 1000);
});//end of then()
</script>
</body>
</html>
可视化效果
本节课示例代码 (demo3-折线图.html)
<!DOCTYPE html>
<html>
<head>
<title>Line</title>
<script src="./js/d3.min.js"></script>
<meta charset="utf-8">
</head>
<body>
<svg width="1600" height="800" id="mainsvg" class="svgs"></svg>
<script>
const svg = d3.select('#mainsvg');
const width = +svg.attr('width');
const height = +svg.attr('height');
const margin = {top: 120, right: 50, bottom: 50, left: 120};
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
const g = svg.append('g').attr('id', 'maingroup')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
const xValue = (datum) => {return datum['日期']};
const yValue = (datum) => {return datum['现有确诊']};
let xSacle, yScale;
let alldates;
let allkeys;
const render_init = function(data){
xScale = d3.scaleTime()
.domain(d3.extent(data, xValue))
.range([0, innerWidth])
.nice();
yScale = d3.scaleLinear()
.domain([d3.max(data, yValue), d3.min(data, yValue)])
.range([0, innerHeight])
.nice();
// Adding axes
const xAxis = d3.axisBottom(xScale)
.ticks(Math.floor(alldates.length) / 4)
.tickFormat(d3.timeFormat('%b-%d'))
.tickSize(-innerHeight)
//ticks: 表示设置几个刻度
//tickSize:表示刻度线的长短
const xAxisGroup = g.append('g').call(xAxis)
.attr('transform', `translate(0, ${innerHeight})`);
const yAxis = d3.axisLeft(yScale).tickSize(-innerWidth);
const yAxisGroup = g.append('g').call(yAxis);
g.selectAll('.tick text').attr('font-size', '2em');
g.append('path').attr('id', 'alterPath');
};
const render_update = function(data){
const line = d3.line()
.x(d => {return xScale(xValue(d))})
.y(d => {return yScale(yValue(d))})
.curve(d3.curveCardinal.tension(0.5))
// lineEmpty is typically used for the first animation that raise the line up;
const lineEmpty = d3.line()
.x(d => {return xScale(xValue(d))})
.y(d => {return yScale(0)})
.curve(d3.curveCardinal.tension(0.5))
const maingroup = d3.select('#maingroup');
const pathupdate = maingroup.selectAll('.datacurve').data([data])
const pathenter = pathupdate.enter().append('path')
.attr('class', 'datacurve')
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 2.5)
.attr("d", lineEmpty)
pathupdate.merge(pathenter)
.transition().duration(2000).ease(d3.easeLinear)
.attr("d", line)
};
const render_update_alter = function(data){
const line = d3.line()
.x( d => xScale(xValue(d)) )
.y( d => yScale(yValue(d)) )
.curve(d3.curveCardinal.tension(0.5) );
//line中,设置属性 x,y 时,d会遍历投进来的数组
//curve: 拟合策略
// tension: 平滑程度
d3.select('#alterPath').datum(data)
.attr('stroke', 'green')
.attr('stroke-width', '2.5')
.attr('fill','none' )
.transition().duration(2000)
.attr('d', line)
//datum: 不遍历传进来的数据,而是将data作为一个主体传进来
//使用datum传进来后,.attr( '' , d => {...}) 这里的d就是整个数据,而不是遍历后的单条数据了
//Notable: fill是有一个默认的黑色填充的,应该把fill设置成None
}
d3.csv('./data/province.csv').then(function(data){
data = data.filter(d => d['省份']!== '总计');
data = data.filter(d => d['省份']!== '湖北');
//因为湖北的数据量太大了,这里应该另外想办法处理
alldates = Array.from(new Set(data.map(d => d['日期'])));
//这句话放在日期格式化前!放在日期格式化后将不起作用
//猜测: 转化为Date()类后,实体不一样
//pre-processing
data.forEach(d => {
d['现有确诊'] = +(d['现有确诊']);
d['日期'] = new Date(d['日期']);
});
//按照省份划分数据
let provinces = {};
allkeys = Array.from(new Set( data.map(d => d['省份']) ));
allkeys.forEach(key => {
provinces[key] = [];
});
data.forEach(d => {
provinces[ d['省份'] ].push(d);
});
allkeys.forEach( key => {
provinces[key] = provinces[key].sort( (b,a) => {
return b['日期'] - a['日期'];
});
});
console.log(provinces)
render_init(data);
let c = 0;
let intervalId = setInterval(() => {
if(c >= allkeys.length){
clearInterval(intervalId);
}else{
let key = allkeys[c];
render_update_alter(provinces[key]);
c = c + 1;
}
}, 2000);
});
</script>
</body>
</html>