上一篇: 基础折线图 https://blog.csdn.net/zjw_python/article/details/98210977
下一篇: 曲线图 https://blog.csdn.net/zjw_python/article/details/98478578
代码结构和初始化画布的Chart对象介绍,请先看 https://blog.csdn.net/zjw_python/article/details/98182540
本图完整的源码地址: https://github.com/zjw666/D3_demo/tree/master/src/lineChart/stackedAreaChart
1 图表效果
2 数据
date,total,food,transportation,education
Mon,120,30,40,50
Tue,200,20,80,100
Wed,150,20,50,80
Thu,80,10,30,40
Fri,70,15,20,35
Sat,110,10,30,70
Sun,130,20,50,60
3 关键代码
导入数据
d3.csv('./data.csv', function(d){
return {
date: d.date,
total: +d.total,
food: +d.food,
transportation: +d.transportation,
education: +d.education
};
}).then(function(data){
.....
一些样式参数配置,与基础折线图类似
const config = {
margins: {top: 80, left: 80, bottom: 50, right: 80},
textColor: 'black',
gridColor: 'gray',
ShowGridX: [],
ShowGridY: [50, 100, 150, 200, 250, 300, 350, 400],
title: '堆叠面积图',
pointSize: 5,
pointColor: 'white',
hoverColor: 'red',
animateDuration: 1000
}
尺度转换和布局函数定义,堆叠面积图和堆叠柱状图类似,都使用了d3.stack
计算布局
/* ----------------------------尺度转换------------------------ */
chart.scaleX = d3.scalePoint()
.domain(data.map((d) => d.date))
.range([0, chart.getBodyWidth()])
chart.scaleY = d3.scaleLinear()
.domain([0, (Math.floor((d3.max(data, (d) => d.total) + d3.max(data, (d) => d.food) + d3.max(data, (d) => d.education) + d3.max(data, (d) => d.transportation))/10) + 1)*10])
.range([chart.getBodyHeight(), 0])
chart.stack = d3.stack()
.keys(['total', 'food', 'transportation', 'education'])
.order(d3.stackOrderAscending)
.offset(d3.stackOffsetNone);
渲染线条,这里与基础折线图类似,使用插值和中间帧实现动画过渡效果
/* ----------------------------渲染线条------------------------ */
chart.renderLines = function(){
let lines = chart.body().selectAll('.line')
.data(chart.stack(data));
lines.enter()
.append('path')
.attr('class', (d) => 'line line-' + d.key)
.merge(lines)
.attr('fill', 'none')
.attr('stroke', (d,i) => chart._colors(i))
.transition().duration(config.animateDuration)
.attrTween('d', lineTween);
lines.exit()
.remove();
//中间帧函数
function lineTween(_d){
if (!_d) return;
const generateLine = d3.line()
.x((d) => d[0])
.y((d) => d[1]);
const pointX = data.map((d) => chart.scaleX(d.date));
const pointY = _d.map((d) => chart.scaleY(d[1]));
const interpolate = getInterpolate(pointX, pointY);
const ponits = [];
const interval = 1/(pointX.length-1);
let index = 0;
return function(t){
if (t - interval > 0 && t % interval < Math.pow(10, -1.4)){ //保证线条一定经过数据点
index = Math.floor(t / interval);
ponits.push([pointX[index], pointY[index]]);
}else{
ponits.push([interpolate.x(t), interpolate.y(t)]);
}
return generateLine(ponits);
}
}
//点插值
function getInterpolate(pointX, pointY){
const domain = d3.range(0, 1, 1/(pointX.length-1));
domain.push(1);
const interpolateX = d3.scaleLinear()
.domain(domain)
.range(pointX);
const interpolateY = d3.scaleLinear()
.domain(domain)
.range(pointY);
return {
x: interpolateX,
y: interpolateY
};
}
}
渲染圆点,代码很简单
/* ----------------------------渲染点------------------------ */
chart.renderPonits = function(){
chart.stack(data).forEach((pointData, i) => {
let ponits = chart.body().selectAll('.point-' + pointData.key)
.data(pointData);
ponits.enter()
.append('circle')
.attr('class', 'point point-' + pointData.key)
.merge(ponits)
.attr('cx', (d) => chart.scaleX(d.data.date))
.attr('cy', (d) => chart.scaleY(d[1]))
.attr('r', 0)
.attr('fill', config.pointColor)
.attr('stroke', chart._colors(i))
.transition().duration(config.animateDuration)
.attr('r', config.pointSize)
.attr('value', (d) => pointData.key + ':' + d.data[pointData.key]);
});
};
渲染面,这里使用d3.area
函数,方便地生成填充面,填充面地坐标使用stack
函数计算地结果,为了和线条保持一致,仍然使用插值和中间帧实现动画过渡效果
chart.renderArea = function(){
const areas = chart.body().insert('g',':first-child')
.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]);
const pointX = data.map((d) => chart.scaleX(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
};
}
}
接着渲染坐标轴,文本标签和网格线这些基础组件
/* ----------------------------渲染坐标轴------------------------ */
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));
}
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('.yAxis .tick')
.each(function(d, i){
if (config.ShowGridY.indexOf(d) > -1){
d3.select(this).append('line')
.attr('class','grid')
.attr('stroke', config.gridColor)
.attr('x1', 0)
.attr('y1', 0)
.attr('x2', chart.getBodyWidth())
.attr('y2', 0);
}
});
d3.selectAll('.xAxis .tick')
.each(function(d, i){
if (config.ShowGridX.indexOf(d) > -1){
d3.select(this).append('line')
.attr('class','grid')
.attr('stroke', config.gridColor)
.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('.point')
.on('mouseover', function(d){
const e = d3.event;
const position = d3.mouse(chart.svg().node());
e.target.style.cursor = 'hand'
d3.select(e.target)
.attr('fill', config.hoverColor);
chart.svg()
.append('text')
.classed('tip', true)
.attr('x', position[0]+5)
.attr('y', position[1])
.attr('fill', config.textColor)
.text(() => {
return d3.select(this).attr('value');
});
})
.on('mouseleave', function(){
const e = d3.event;
d3.select(e.target)
.attr('fill', config.pointColor);
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)
);
}