上一篇: 封闭图 https://blog.csdn.net/zjw_python/article/details/98591118
下一篇: 仪表盘图 https://blog.csdn.net/zjw_python/article/details/98596174
代码结构和初始化画布的Chart对象介绍,请先看 https://blog.csdn.net/zjw_python/article/details/98182540
本图完整的源码地址: https://github.com/zjw666/D3_demo/tree/master/src/riverChart/basicRiver
1 图表效果
2 数据
date,food,transportation,education,house,clothes
2019-06-01,30,40,50,56,30
2019-06-02,20,80,56,42,60
2019-06-03,20,50,80,57,54
2019-06-04,10,30,40,36,62
2019-06-05,15,20,35,50,43
2019-06-06,10,30,70,73,34
2019-06-07,20,56,60,37,20
2019-06-08,15,20,65,46,25
2019-06-09,40,34,43,64,45
2019-06-10,36,46,60,75,62
2019-06-11,25,32,40,60,38
2019-06-12,10,18,32,34,55
2019-06-13,34,23,42,43,32
2019-06-14,30,46,25,25,23
2019-06-15,22,62,52,20,26
2019-06-16,17,45,42,10,21
2019-06-17,13,50,72,21,30
2019-06-18,10,34,65,34,20
2019-06-19,25,34,56,44,15
2019-06-21,15,14,32,56,32
2019-06-22,30,32,42,24,42
2019-06-23,20,25,23,32,23
2019-06-24,5,14,52,42,10
2019-06-25,18,36,25,36,12
2019-06-26,34,40,30,20,22
2019-06-27,12,32,34,54,34
2019-06-28,10,50,24,68,20
2019-06-29,26,55,32,35,36
2019-06-30,21,40,20,53,30
3 关键代码
导入数据
d3.csv('./data.csv', function(d){
return {
date: d.date,
house: +d.house,
food: +d.food,
transportation: +d.transportation,
education: +d.education,
clothes: +d.clothes
};
}).then(function(data){
......
一些样式参数配置
const config = {
margins: {top: 80, left: 80, bottom: 50, right: 80},
textColor: 'black',
gridColor: 'gray',
title: '基础河流图',
animateDuration: 1000
}
尺度转换,河流图的样子与堆叠面积图有几分类似,两者都运用d3.stack
进行布局,区别在于,河流图堆叠的基准线不再是X轴。虽然布局算法不同,但我们只需要稍微更改一下配置选项即可,非常简单
/* ----------------------------尺度转换------------------------ */
chart.scaleX = d3.scaleTime()
.domain([new Date(data[0].date), new Date(data[data.length-1].date)])
.range([0, chart.getBodyWidth()]);
chart.scaleY = d3.scaleLinear()
.domain([0, (Math.floor((
d3.max(data, (d) => d.house) +
d3.max(data, (d) => d.food) +
d3.max(data, (d) => d.education) +
d3.max(data, (d) => d.transportation) +
d3.max(data, (d) => d.clothes)
)/10) + 1)*10])
.range([chart.getBodyHeight(), 0])
chart.stack = d3.stack()
.keys(['house', 'food', 'transportation', 'education', 'clothes'])
.order(d3.stackOrderInsideOut)
.offset(d3.stackOffsetWiggle);
经过stack
函数处理后的数据自带布局信息,但为达到从左向右过渡动画的效果,我们还是对数据点进行线性插个值,然后运用中间帧函数实现动画效果。
/* ----------------------------渲染面------------------------ */
chart.renderArea = function(){
const areas = chart.body().insert('g',':first-child')
.attr('transform', 'translate(0, -' + d3.max(data, (d) => d3.mean(Object.values(d))) + ')') // 使流图的位置处于Y轴中部
.selectAll('.area')
.data(chart.stack(data));
areas.enter()
.append('path')
.attr('class', (d) => 'area area-' + d.key)
.merge(areas)
.style('fill', (d,i) => chart._colors(i))
.transition().duration(config.animateDuration)
.attrTween('d', areaTween);
//中间帧函数
function areaTween(_d){
if (!_d) return;
const generateArea = d3.area()
.x((d) => d[0])
.y0((d) => d[1])
.y1((d) => d[2])
.curve(d3.curveCardinal.tension(0));
const pointX = data.map((d) => chart.scaleX(new Date(d.date)));
const pointY0 = _d.map((d) => chart.scaleY(d[0]));
const pointY1 = _d.map((d) => chart.scaleY(d[1]));
const interpolate = getAreaInterpolate(pointX, pointY0, pointY1);
const ponits = [];
return function(t){
ponits.push([interpolate.x(t), interpolate.y0(t), interpolate.y1(t)]);
return generateArea(ponits);
}
}
//点插值
function getAreaInterpolate(pointX, pointY0, pointY1){
const domain = d3.range(0, 1, 1/(pointX.length-1));
domain.push(1);
const interpolateX = d3.scaleLinear()
.domain(domain)
.range(pointX);
const interpolateY0 = d3.scaleLinear()
.domain(domain)
.range(pointY0);
const interpolateY1 = d3.scaleLinear()
.domain(domain)
.range(pointY1);
return {
x: interpolateX,
y0: interpolateY0,
y1: interpolateY1
};
}
}
接着就是渲染坐标轴、网格线和文本标签等常规操作,值得注意是,这里的X轴是时间尺度,为了美观,我们需要运用tickFormat
将其格式化一下,只显示日期。
/* ----------------------------渲染坐标轴------------------------ */
chart.renderX = function(){
chart.svg().insert('g','.body')
.attr('transform', 'translate(' + chart.bodyX() + ',' + (chart.bodyY() + chart.getBodyHeight()) + ')')
.attr('class', 'xAxis')
.call(d3.axisBottom(chart.scaleX).ticks(d3.timeDay.every(3)).tickFormat((d) => d3.timeFormat("%d")(d)));
}
chart.renderY = function(){
chart.svg().insert('g','.body')
.attr('transform', 'translate(' + chart.bodyX() + ',' + chart.bodyY() + ')')
.attr('class', 'yAxis')
.call(d3.axisLeft(chart.scaleY));
}
chart.renderAxis = function(){
chart.renderX();
chart.renderY();
}
/* ----------------------------渲染文本标签------------------------ */
chart.renderText = function(){
d3.select('.xAxis').append('text')
.attr('class', 'axisText')
.attr('x', chart.getBodyWidth())
.attr('y', 0)
.attr('fill', config.textColor)
.attr('dy', 40)
.text('日期');
d3.select('.yAxis').append('text')
.attr('class', 'axisText')
.attr('x', 0)
.attr('y', 0)
.attr('fill', config.textColor)
.attr('transform', 'rotate(-90)')
.attr('dy', -40)
.attr('text-anchor','end')
.text('每日支出(元)');
}
/* ----------------------------渲染网格线------------------------ */
chart.renderGrid = function(){
d3.selectAll('.xAxis .tick')
.append('line')
.attr('class','grid')
.attr('stroke', config.gridColor)
.attr('stroke-dasharray', '10,10')
.attr('x1', 0)
.attr('y1', 0)
.attr('x2', 0)
.attr('y2', -chart.getBodyHeight());
}
最后绑定鼠标交互事件,当鼠标悬停时,突出显示当前选中块,其余块的透明度减小
/* ----------------------------绑定鼠标交互事件------------------------ */
chart.addMouseOn = function(){
//防抖函数
function debounce(fn, time){
let timeId = null;
return function(){
const context = this;
const event = d3.event;
timeId && clearTimeout(timeId)
timeId = setTimeout(function(){
d3.event = event;
fn.apply(context, arguments);
}, time);
}
}
d3.selectAll('.area')
.on('mouseover', function(d){
const e = d3.event;
const position = d3.mouse(chart.svg().node());
e.target.style.cursor = 'hand'
d3.selectAll('.area')
.attr('fill-opacity', 0.3);
d3.select(e.target)
.attr('fill-opacity', 1);
chart.svg()
.append('text')
.classed('tip', true)
.attr('x', position[0]+5)
.attr('y', position[1])
.attr('fill', config.textColor)
.text(d.key);
})
.on('mouseleave', function(){
const e = d3.event;
d3.selectAll('.area')
.attr('fill-opacity', 1);
d3.select('.tip').remove();
})
.on('mousemove', debounce(function(){
const position = d3.mouse(chart.svg().node());
d3.select('.tip')
.attr('x', position[0]+5)
.attr('y', position[1]-5);
}, 6)
);
}