D3可用于创建交互式可视化,允许使用数据操作、转换和呈现,然后将其转换为可在浏览器中显示的SVG、Canvas或HTML。
1 SVG画布
- 使用基于像素的坐标系统,其中浏览器左上角是原点
(0, 0)
,x、y轴正方向分别是右和下。 - SVG标签包含基本构图元素:rect、圆形circle、椭圆ellipse、线line、文本text、路径path等。
- 默认样式未描边并由黑色填充,颜色可被指定、如:16进制值、RGB值、RGB与Alpha透明等。
- 使用fill(填充)、stroke(描边)、stroke-width、opacity(不透明度)、text-anchor(对齐方式)等。
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8' />
<title>SVG画布</title>
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js'>
</script>
</head>
<body style='text-align: center;'><br />
<svg width=500 height=300>
<!-- 矩形 -->
<rect x='200' y='50' width='100' height='30' fill='purple' />
<!-- 圆形,alpha透明度参数,介于0.0到1.0之间 -->
<circle cx='250' cy='100' r='15' fill='rgba(0, 64, 32, 1.0)' />
<!-- 椭圆,cx和cy指定圆心坐标 -->
<ellipse cx='250' cy='150' rx='50' ry='15'
stroke='rgba(0, 0, 255, 0.25)' stroke-width='15' />
<!-- 线 -->
<line x1='0' y1='200' x2='500' y2='200' stroke='red' />
<!-- 带样式的文本 -->
<text x='185' y='250'
font-family='sans-serif' font-size='25' fill='gray'>
Easy-peasy
</text>
</svg>
</body>
</html>
1-1 直线
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8' />
<title>直线</title>
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js'>
</script>
</head>
<body style='text-align: center;'><br />
<script>
// 在body中插入一個svg
var svg = d3.select('body')
.append('svg')
.attr('width', 500)
.attr('height', 400);
// 在svg中插入line
svg.append('line')
.attr('x1', '100')
.attr('y1', '150')
.attr('x2', '400')
.attr('y2', '150')
.style('stroke', 'purple')
.style('stroke-width', 1);
// 在svg中插入line
svg.append('line')
.attr('x1', '100')
.attr('y1', '250')
.attr('x2', '400')
.attr('y2', '250')
.style('stroke', 'purple')
.style('stroke-width', 1);
// 在svg中插入line
svg.append('line')
.attr('x1', '200')
.attr('y1', '50')
.attr('x2', '200')
.attr('y2', '350')
.style('stroke', 'purple')
.style('stroke-width', 1);
// 在svg中插入line
svg.append('line')
.attr('x1', '300')
.attr('y1', '50')
.attr('x2', '300')
.attr('y2', '350')
.style('stroke', 'purple')
.style('stroke-width', 1);
// 在svg中插入circle
svg.append('circle')
.attr('cx', '250')
.attr('cy', '200')
.attr('r', '20')
.style('fill', 'none')
.style('stroke', 'red')
.style('stroke-width', 1);
// 在svg中插入line
svg.append('line')
.attr('x1', '120')
.attr('y1', '80')
.attr('x2', '180')
.attr('y2', '120')
.style('stroke', 'red')
.style('stroke-width', 1);
// 在svg中插入line
svg.append('line')
.attr('x1', '180')
.attr('y1', '80')
.attr('x2', '120')
.attr('y2', '120')
.style('stroke', 'red')
.style('stroke-width', 1);
// 在svg中插入circle
svg.append('circle')
.attr('cx', '350')
.attr('cy', '300')
.attr('r', '20')
.style('fill', 'none')
.style('stroke', 'red')
.style('stroke-width', 1);
</script>
</body>
</html>
1-2 折线
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8' />
<title>折线</title>
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js'>
</script>
</head>
<body style='text-align: center;'><br />
<script>
// 在body中插入一個svg
var svg = d3.select('body')
.append('svg')
.attr('width', 400)
.attr('height', 200);
// 在svg中插入polyline
svg.append('polyline')
.attr('points', '100,10 40,180 190,60 10,60 160,180 100,10')
.style('fill', 'none')
.style('stroke', 'purple')
.style('stroke-width', 1);
// 在svg中插入polyline
svg.append('polyline')
.attr('points', '200,160 240,160 240,120 280,120 \
280,80 320,80 320,40 360,40 360,160 240,160')
.style('fill', 'none')
.style('stroke', 'purple')
.style('stroke-width', 1);
</script>
</body>
</html>
1-3 椭圆
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8' />
<title>椭圆</title>
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js'>
</script>
</head>
<body style='text-align: center;'><br />
<script>
// 在body中插入一個svg
var svg = d3.select('body')
.append('svg')
.attr('width', 300)
.attr('height', 230);
// 在svg中插入ellipse
svg.append('ellipse')
.attr('cx', '100')
.attr('cy', '60')
.attr('rx', '30')
.attr('ry', '50')
.style('fill', 'none')
.style('stroke', 'green')
.style('stroke-width', 10);
// 在svg中插入ellipse
svg.append('ellipse')
.attr('cx', '200')
.attr('cy', '60')
.attr('rx', '30')
.attr('ry', '50')
.style('fill', 'none')
.style('stroke', 'green')
.style('stroke-width', 10);
// 在svg中插入ellipse
svg.append('ellipse')
.attr('cx', '145')
.attr('cy', '180')
.attr('rx', '110')
.attr('ry', '40')
.style('fill', 'none')
.style('stroke', 'green')
.style('stroke-width', 1);
</script>
</body>
</html>
1-4 多边形
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8' />
<title>多边形</title>
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js'>
</script>
</head>
<body style='text-align: center;'><br />
<script>
// 在body中插入一個svg
var svg = d3.select('body')
.append('svg')
.attr('width', 200)
.attr('height', 60);
// 在svg中插入polygon
svg.append('polygon')
.attr('points', '50,10 20,50 80,50')
.style('fill', 'none')
.style('stroke', 'green')
.style('stroke-width', 1);
// 在svg中插入polygon
svg.append('polygon')
.attr('points', '70,10 130,10 100,50')
.style('fill', 'none')
.style('stroke', 'blue')
.style('stroke-width', 1);
// 在svg中插入polygon
svg.append('polygon')
.attr('points', '150,10 120,50 180,50')
.style('fill', 'none')
.style('stroke', 'orange')
.style('stroke-width', 1);
</script>
</body>
</html>
1-5 路径图
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8' />
<title>路径图</title>
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js'>
</script>
</head>
<body style='text-align: center;'><br />
<script>
// 在body中插入一個svg
var svg = d3.select('body')
.append('svg')
.attr('width', 500)
.attr('height', 300);
// 先在svg中插入一個path
svg.append('path')
// M50 150:将路径移动到点(50, 150),这是路径的起点
// Q300 50 300 150:绘制一个二次贝塞尔曲线
// 从当前点(50, 150)开始
// 经过控制点(300, 50),然后到终点(300, 150)
// T450 150:绘制一个平滑的二次贝塞尔曲线
// 从当前点(300, 150)开始
// 经过一个隐式控制点,然后到终点(450, 150)
.attr('d', 'M50 150Q300 50 300 150T450 150')
.style('fill', 'none')
.style('stroke', 'purple')
.style('stroke-width', 1);
</script>
</body>
</html>
1-6 路径文字
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8' />
<title>路径文字</title>
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js'>
</script>
</head>
<body style='text-align: center;'><br />
<script>
// 在body中插入一個svg
var svg = d3.select('body')
.append('svg')
.attr('width', 400)
.attr('height', 320);
// 先在svg中插入一個path
svg.append('path')
.attr('id', 'mypath')
// M50 100:将路径移动到点(50, 100),这是路径的起点
// Q350 50 350 250:绘制一个二次贝塞尔曲线
// 从当前点(50, 100)开始
// 经过控制点(350, 50),然后到终点(350, 250)
// Q250 50 50 250:绘制另一个二次贝塞尔曲线
// 从当前点(350, 250)开始
// 经过控制点(250, 50),然后到终点(50, 250)
.attr('d', 'M50 100Q350 50 350 250Q250 50 50 250')
.style('fill', 'none')
.style('stroke', 'green')
.style('stroke-width', 1);
// 在svg中插入一個text
svg.append('text')
.attr('x', 10)
.attr('y', 20)
.style('fill', 'steelblue')
.style('font-size', '21.5px')
.style('font-weight', 'normal')
.append('textPath')
// 引用路径
.attr('xlink:href', '#mypath')
.text(
'富强、民主、文明、和谐、自由、平等\
、公正、法治、爱国、敬业、诚信、友善'
);
</script>
</body>
</html>
2 散点绘制
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8' />
<title>散点绘制</title>
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js'>
</script>
</head>
<body style='text-align: center;'><br />
<script>
// 高宽
var w = 600;
var h = 100;
var dataset = [
[5, 20], [480, 90], [250, 50], [100, 33], [330, 95],
[410, 12], [475, 44], [25, 67], [85, 21], [220, 88]
];
// 创建SVG
var svg = d3.select('body')
.append('svg')
.attr('width', w)
.attr('height', h);
svg.selectAll('circle')
.data(dataset)
.enter()
.append('circle')
.attr('cx', function (d) {
return d[0] + 50;
})
.attr('cy', function (d) {
return d[1];
})
.attr('r', function (d) {
// 控制半径
return Math.sqrt(h - d[1]);
})
.style('fill', 'green');
svg.selectAll('text')
.data(dataset)
.enter()
.append('text')
.text(function (d) {
return d[0] + ',' + d[1];
})
.attr('x', function (d) {
return d[0] + 60;
})
.attr('y', function (d) {
return d[1];
})
.attr('font-family', 'sans-serif')
.attr('font-size', '11px')
.attr('fill', 'red');
</script>
</body>
</html>
2-1 比例尺
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8' />
<title>比例尺</title>
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js'>
</script>
</head>
<body style='text-align: center;'><br />
<script>
// 高宽
var w = 600;
var h = 100;
var dataset = [
[5, 20], [480, 90], [250, 50], [100, 33], [330, 95],
[410, 12], [475, 44], [25, 67], [85, 21], [220, 88]
];
// 坐标轴的缩放
var xScale = d3.scaleLinear()
.domain([
0,
d3.max(dataset, function (d) {
return d[0];
})
])
.range([0, w]);
var yScale = d3.scaleLinear()
.domain([
0,
d3.max(dataset, function (d) {
return d[1];
})
])
.range([0, h]);
// 创建SVG
var svg = d3.select('body')
.append('svg')
.attr('width', w * 1.1)
.attr('height', h);
svg.selectAll('circle')
.data(dataset)
.enter()
.append('circle')
.attr('cx', function (d) {
return xScale(d[0]) + 5;
})
.attr('cy', function (d) {
return yScale(d[1]) - 3;
})
.attr('r', function (d) {
return Math.sqrt(h - d[1]);
})
.style('fill', 'green');
svg.selectAll('text')
.data(dataset)
.enter()
.append('text')
.text(function (d) {
return d[0] + ',' + d[1];
})
.attr('x', function (d) {
return xScale(d[0]) + 20;
})
.attr('y', function (d) {
return yScale(d[1]);
})
.attr('font-family', 'sans-serif')
.attr('font-size', '11px')
.attr('fill', 'red');
</script>
</body>
</html>
2-2 坐标轴
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8' />
<title>坐标轴</title>
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js'>
</script>
</head>
<body style='text-align: center;'><br />
<script>
// 高宽
var w = 600;
var h = 300;
var padding = 20;
var dataset = [
[5, 20], [480, 90], [250, 50], [100, 33], [330, 95],
[410, 12], [475, 44], [25, 67], [85, 21], [220, 88]
];
// 创建比例尺
var xScale = d3.scaleLinear()
.domain([
d3.min(dataset, function (d) {
return d[0];
}),
d3.max(dataset, function (d) {
return d[0] + 50;
})
])
.range([padding, w - padding * 2]);
var yScale = d3.scaleLinear()
.domain([
d3.min(dataset, function (d) {
return d[1] - 10;
}),
d3.max(dataset, function (d) {
return d[1] + 50;
})
])
.range([h - padding, padding]);
var rScale = d3.scaleLinear()
.domain([
0,
d3.max(dataset, function (d) {
return d[1];
})
])
.range([2, 5]);
// 设置刻度的格式
var formatAsPercentage = d3.format('.0f');
// 创建SVG
var svg = d3.select('body')
.append('svg')
.attr('width', w * 1.1)
.attr('height', h);
svg.selectAll('circle')
.data(dataset)
.enter()
.append('circle')
.attr('cx', function (d) {
// +10,点向右水平平移
return xScale(d[0]) + 10;
})
.attr('cy', function (d) {
return yScale(d[1]);
})
.attr('r', function (d) {
return rScale(d[1]);
})
.style('fill', 'green');
svg.selectAll('text')
.data(dataset)
.enter()
.append('text')
.text(function (d) {
return d[0] + ',' + d[1];
})
.attr('x', function (d) {
return xScale(d[0]) + 35;
})
.attr('y', function (d) {
return yScale(d[1]);
})
.attr('font-family', 'sans-serif')
.attr('font-size', '11px')
.attr('fill', 'red');
// 定义X轴
var xAxis = d3.axisBottom()
.scale(xScale)
.ticks(5)
.tickFormat(formatAsPercentage);
// 定义Y轴
var yAxis = d3.axisLeft()
.scale(yScale)
.ticks(5)
.tickFormat(formatAsPercentage);
// 创建X轴
svg.append('g')
.attr('class', 'axis')
.attr(
'transform',
'translate(10, ' + (h - padding) + ')'
)
.call(xAxis);
// 创建Y轴
svg.append('g')
.attr('class', 'axis')
// translate(x, y)中x水平平移,y垂直平移
.attr(
'transform',
'translate(' + (padding + 10) + ', 0)'
)
.call(yAxis);
</script>
</body>
</html>
3 数据类型
- D3可处理的数据类型
- D3自定义数据类型:集合Set、映射Map、嵌套Nest、颜色空间等。
- 颜色空间对象:红绿蓝RGB、色相饱和度亮度HSB和HSL、Lab*等。
- Js数据类型,例如:数字、时间、字符串、布尔值、数组、对象等。
- D3最常用的数据类型:数组、Json、CSV、GeoJson等。
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8' />
<title>数据类型</title>
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js'>
</script>
<style>
div.bar {
display: inline-block;
width: 20px;
/* 设置层之间的间隔 */
margin-right: 2px;
background-color: green;
}
</style>
</head>
<body style='text-align: center;'><br />
<script>
var dataset = [];
// 使用原生Js的方式
// for (var i = 0; i < 25; i++) {
// // 随机生成0-30的整数
// var newNumber = Math.round(Math.random() * 30);
// dataset.push(newNumber);
// }
// 使用D3方式,生成一个满足期望是15,方差是8的正态分布随机数
var dataset = d3.range(25)
.map(function () {
// Math.round函数对随机数保留一位小数
return Math.round(d3.randomNormal(15, 8)(), 1);
})
d3.select('body')
.selectAll('div')
// 绑定数据
.data(dataset)
.enter()
// 追加元素
.append('div')
// 声明设置属性
.attr('class', 'bar')
// 为每个特定层设置属性
.style('height', function (d) {
return (d * 5) + 'px';
});
</script>
</body>
</html>
3-1 自由矩形
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8' />
<title>自由矩形</title>
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js'>
</script>
</head>
<body style='text-align: center;'><br />
<script>
// SVG宽高
var w = 500;
var h = 300;
var barPadding = 1;
var dataset = [];
// 使用D3方式,生成一个满足期望是15,方差是8的正态分布随机数
var dataset = d3.range(25)
.map(function () {
// Math.round函数对随机数保留一位小数
return Math.round(d3.randomNormal(15, 8)(), 1);
})
// 创建SVG
var svg = d3.select('body')
.append('svg')
.attr('width', w)
.attr('height', h);
svg.selectAll('rect')
.data(dataset)
.enter()
.append('rect')
.attr('x', function (d, i) {
return i * (w / dataset.length);
})
.attr('y', function (d) {
return h - (d * 4);
})
.attr('width', w / dataset.length - barPadding)
.attr('height', function (d) {
return d * 4;
})
.attr('fill', function (d) {
// 随着饱和度的分量而产生不同颜色深度的蓝色
// return 'rgb(0, 0, ' + (d * 10) + ')';
// 绿色
// return 'rgb(0, ' + (d * 10) + ', 0)';
// 青色
return 'rgb(0, ' + (d * 10) + ', ' + (d * 10) + ')';
});
svg.selectAll('text')
.data(dataset)
.enter()
.append('text')
.text(function (d) {
return d;
})
.attr('text-anchor', 'middle')
.attr('x', function (d, i) {
return i * (w / dataset.length) +
(w / dataset.length - barPadding) / 2;
})
.attr('y', function (d) {
return h - (d * 4) + 14;
})
.attr('font-family', 'sans-serif')
.attr('font-size', '11px')
.attr('fill', 'white');
</script>
</body>
</html>
3-2 图形过渡
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8' />
<title>图形过渡</title>
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js'>
</script>
</head>
<body style='text-align: center;'><br />
<button id='update'>更新元素</button>
<button id='remove'>删除元素</button>
<button id='add'>添加元素</button>
<br /><br /><br />
<script>
// 设置SVG宽高
var w = 500;
var h = 300;
var barPadding = 1;
// 准备数据集
// var dataset = [
// 5, 10, 13, 19, 21, 25, 22, 18, 15, 13,
// 11, 12, 15, 20, 18, 17, 16, 18, 23, 25
// ];
var dataset = [];
var dataset = d3.range(25)
.map(function () {
return Math.round(d3.randomNormal(15, 8)(), 1);
});
// 定义比例尺
var xScale = d3.scaleBand()
.domain(d3.range(dataset.length))
.range([0, w])
.padding(0.1);
var yScale = d3.scaleLinear()
.domain([0, d3.max(dataset)])
.range([0, h]);
// 创建SVG元素
var svg = d3.select('body')
.append('svg')
.attr('width', w)
.attr('height', h);
// 为SVG添加条形
svg.selectAll('rect')
.data(dataset)
.enter()
.append('rect')
.attr('x', function (d, i) {
return xScale(i);
})
.attr('y', function (d) {
return h - yScale(d);
})
.attr('width', xScale.bandwidth())
.attr('height', function (d) {
return yScale(d);
})
.attr('fill', function (d) {
return 'rgb(0, ' + (d * 10) + ', ' + (d * 10) + ')';
});
// 为条形加上数值
svg.selectAll('text')
.data(dataset)
.enter()
.append('text')
.text(function (d) {
return d;
})
.attr('text-anchor', 'middle')
.attr('x', function (d, i) {
return xScale(i) + xScale.bandwidth() / 2;
})
.attr('y', function (d) {
return h - yScale(d) + 14;
})
.attr('font-family', 'sans-serif')
.attr('font-size', function (d) {
return xScale.bandwidth() / 2;
})
.attr('fill', 'white');
// 更新元素
d3.select('#update')
.on('click', function () {
// 帮助调试
console.log('Button clicked.');
// 随机生成颜色
const randomColor = '#' +
Math.floor(Math.random() * 16777215).toString(16);
// 存储颜色到localStorage
localStorage.setItem('backgroundColor', randomColor);
// 禁用按钮,防止多次点击
d3.select(this)
.attr('disabled', true);
// 刷新页面
location.reload();
});
// 页面加载时检查是否有颜色并设置背景
window.onload = function () {
const color = localStorage.getItem('backgroundColor');
if (color) {
document.body.style.backgroundColor = color;
}
};
// 删除元素
d3.select('#remove')
.on('click', function () {
// 移除数据集的最后一个元素
dataset.shift();
// 更新X轴比例尺
xScale.domain(d3.range(dataset.length));
// 选择并更新矩形条
var bars = svg.selectAll('rect')
.data(dataset);
// 对于矩形条,退出时执行过渡并移除
bars.exit()
.transition()
.duration(500)
.attr('x', w)
.remove();
// 选择并更新文本
var texts = svg.selectAll('text')
.data(dataset);
// 对于文本,退出时执行过渡并移除
texts.exit()
.transition()
.duration(500)
// 移出画布
.attr('x', w)
.remove();
});
// 添加元素
d3.select('#add')
.on('click', function () {
// 数据集最后添加数值
var maxValue = 75;
// 0-24的整数,容易超出画布高度
// var newNumber = Math.floor(Math.random() * maxValue);
// 添加一个均值为15,标准差为8的正态分布随机数
var newNumber = Math.round(d3.randomNormal(15, 8)(), 1);
dataset.push(newNumber);
// 更新X轴比例尺
xScale.domain(d3.range(dataset.length));
// 选择所有数据,绑定数据到元素集,返回更新的元素集
var bars = svg.selectAll('rect')
.data(dataset);
var texts = svg.selectAll('text')
.data(dataset);
// 添加条形元素到最右边
bars.enter()
.append('rect')
// 在SVG最右边,不可见
.attr('x', w);
texts.enter()
.append('text');
// 更新新矩形到可见范围内
bars.transition()
.duration(500)
.attr('x', function (d, i) {
return xScale(i);
})
// 每个X对应到它相应的档位上
.attr('y', function (d) {
// return h - yScale(d);
// 添加的数据不超过画布高度
return h - Math.min(h, yScale(d));
})
// 这里xScale比例尺已经设置间距了所以直接使用
.attr('width', xScale.bandwidth())
.attr('height', function (d) {
// return yScale(d);
// 确保高度不超过画布
return Math.min(yScale(d), h);
})
// 设置RGB颜色与数值的关系
.attr('fill', function (d) {
return 'rgb(0, ' + (d * 10) + ', 0)';
});
texts.transition()
.duration(500)
.text(function (d) {
return d;
})
.attr('text-anchor', 'middle')
.attr('x', function (d, i) {
return xScale(i) + xScale.bandwidth() / 2;
})
.attr('y', function (d) {
// return h - yScale(d) + 14;
// 确保文本位置正常
return h - Math.min(yScale(d), h) + 14;
})
.attr('font-family', 'sans-serif')
.attr('font-size', '11px')
.attr('fill', 'red');
});
</script>
</body>
</html>
3-3 鼠键交互
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8' />
<title>鼠键交互</title>
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js'>
</script>
<style>
/* 鼠标悬停时变色 */
rect:hover {
fill: orange;
}
/* 过渡效果 */
rect {
-moz-transiton: all 0.3s;
-o-transiton: all 0.3s;
-webkit-transition: all 0.3s;
transition: all 0.3s
}
/* 给提示条加上样式 */
#tooltip {
position: absolute;
width: 200px;
height: auto;
padding: 10px;
background-color: white;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
border-radius: 10px;
-webkit-box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.4);
-moz-box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.4);
box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.4);
/* 避免事件干扰 */
pointer-events: none;
}
#tooltip.hidden {
/* 确保元素不占据空间 */
display: none;
}
#tooltip p {
margin: 0;
font-family: sans-serif;
font-size: 16px;
line-height: 20px;
}
</style>
</head>
<body style='text-align: center;'><br />
<button id='update'>更新元素</button>
<button id='remove' class='click'>删除元素</button>
<button id='add' class='click'>添加元素</button>
<!-- 创建div提示层 -->
<div id='tooltip' class='hidden'>
<p><strong>进度提示</strong></p>
<span id='value'>100</span>
</div>
<br /><br /><br /><br /><br /><br />
<script>
// 键值对数据集
var dataset = [
{ key: 0, value: 5 }, { key: 1, value: 10 },
{ key: 2, value: 13 }, { key: 3, value: 9 },
{ key: 4, value: 21 }, { key: 5, value: 25 },
{ key: 6, value: 22 }, { key: 7, value: 24 },
{ key: 8, value: 14 }, { key: 9, value: 7 },
{ key: 10, value: 11 }, { key: 11, value: 12 },
{ key: 12, value: 15 }, { key: 13, value: 20 },
{ key: 14, value: 19 }, { key: 15, value: 17 },
{ key: 16, value: 16 }, { key: 17, value: 18 },
{ key: 18, value: 23 }, { key: 19, value: 3 }
];
// 设置SVG宽高
var w = 500;
var h = 300;
var barPadding = 1;
// 定义序数比例尺
var xScale = d3.scaleBand()
.domain(d3.range(dataset.length))
.range([0, w])
.padding(0.2);
var yScale = d3.scaleLinear()
.domain([
0,
d3.max(dataset, function (d) { return d.value; })
])
.range([0, h]);
// 定义键函数,以备数据绑定到元素时使用
var key = function (d) { return d.key };
// 值函数
var value = function (d) { return d.value };
// 条形排序函数
var sortOrders = false;
var sortBars = function () {
// 每点击一次排序方向改变
sortOrders = !sortOrders;
svg.selectAll('rect')
.sort(function (a, b) {
if (sortOrders) {
// 对数据集升序排序,键值对要加上值的引用b.value
return d3.ascending(a.value, b.value);
} else {
// 对数据集降序排序
return d3.descending(a.value, b.value);
}
})
.transition()
.duration(1000)
// 对排序之后的横坐标重排
.attr('x', function (d, i) { return xScale(i) });
svg.selectAll('text')
.sort(function (a, b) {
if (sortOrders) {
// 对数据集升序排序,键值对要加上值的引用b.value
return d3.ascending(a.value, b.value);
} else {
// 对数据集降序排序
return d3.descending(a.value, b.value);
}
})
.transition()
.duration(1000)
// 对排序之后的横坐标重排
.attr('x', function (d, i) {
return xScale(i) + xScale.bandwidth() / 2;
});
};
d3.select('#tooltip')
.classed('hidden', true);
// 创建SVG元素
var svg = d3.select('body')
.append('svg')
.attr('width', w)
.attr('height', h);
// 为SVG添加条形
svg.selectAll('rect')
.data(dataset, d => d.key)
.enter()
.append('rect')
.attr('x', function (d, i) { return xScale(d.key) })
.attr('y', function (d) { return h - yScale(d.value) })
.attr('width', xScale.bandwidth())
.attr('height', function (d) { return yScale(d.value) })
.attr('fill', function (d) {
return 'rgb(0, ' + (d.value * 10)
+ ', ' + (d.value * 10) + ')';
})
// 点击排序
.on('click', function () { sortBars() })
// 更新提示条的值和位置
.on('mouseover', function (event, d) {
// 输出d对象的确认值
console.log(d);
// 取得提示显示的位置
var xPosition = parseFloat(d3.select(this)
.attr('x')) + xScale.bandwidth() / 2;
var yPosition = parseFloat(d3.select(this)
.attr('y')) / 2 + h / 2;
d3.select('#tooltip')
.style('left', xPosition + 'px')
.style('top', yPosition + 'px')
.select('#value')
.text(`Value: ${d.value}`);
d3.select('#tooltip')
.classed('hidden', false);
})
// 移除提示条
.on('mouseout', function () {
// 添加隐藏类
d3.select('#tooltip')
.classed('hidden', true);
})
.attr('fill', 'green');
// 为条形加上数值
svg.selectAll('text')
.data(dataset, key)
.enter()
.append('text')
.text(function (d) { return d.value })
.attr('text-anchor', 'middle')
.attr('x', function (d, i) {
return xScale(i) + xScale.bandwidth() / 2;
})
.attr('y', function (d) {
return h - yScale(d.value) + 14;
})
.attr('font-family', 'sans-serif')
.attr('font-size', function (d) {
return xScale.bandwidth() / 2;
})
.attr('fill', 'white');
// 添加、删除功能
d3.selectAll('.click')
.on('click', function () {
// 根据ID确定点击的标签
var paragraphID = d3.select(this).attr('id');
if (paragraphID == 'add') {
// 添加数据
var newNumber = Math.round(d3.randomNormal(15, 8)(), 1);
// 根据最后一个key添加一个值
var lastKeyValue = dataset.length > 0
? dataset[dataset.length - 1].key : 0;
dataset.push({ key: lastKeyValue + 1, value: newNumber });
// 更新X轴比例尺,更新x坐标的domain
xScale.domain(d3.range(dataset.length));
// 选择所有条形,绑定数据到元素集,返回更新的元素集
var bars = svg.selectAll('rect')
.data(dataset, d => d.key);
var texts = svg.selectAll('text')
.data(dataset, d => d.key);
// 处理进入阶段
bars.enter()
.append('rect')
.attr('x', w)
.attr('y', h)
.attr('width', xScale.bandwidth())
.attr('fill', 'purple')
// 更新tooltip内容显示
.on('mouseover', function (event, d) {
// 输出当前条形的值
console.log(d);
var xPosition = parseFloat(d3.select(this)
.attr('x')) + xScale.bandwidth() / 2;
var yPosition = parseFloat(d3.select(this)
.attr('y')) / 2 + h / 2;
// 更新tooltip为当前条形值
d3.select('#tooltip')
.style('left', xPosition + 'px')
.style('top', yPosition + 'px')
.select('#value')
.text(`Value: ${d.value}`);
d3.select('#tooltip')
.classed('hidden', false);
})
.on('mouseout', function () {
d3.select('#tooltip')
.classed('hidden', true);
})
.merge(bars)
.transition()
.duration(500)
.attr('x', (d, i) => xScale(i))
.attr('y', d => h - yScale(d.value))
.attr('height', d => yScale(d.value));
// 处理退出阶段
bars.exit()
.transition()
.duration(500)
.attr('x', -xScale.bandwidth())
.remove();
texts.enter()
.append('text')
.attr('text-anchor', 'middle')
.attr('font-family', 'sans-serif')
.attr('font-size', '12px')
.attr('fill', 'white')
.merge(texts)
.transition()
.duration(500)
.attr('x', (d, i) => xScale(i) + xScale.bandwidth() / 2)
.attr('y', d => h - yScale(d.value) + 14)
.text(d => d.value);
// 更新Tooltip
showTooltip(`Added: ${newNumber}`);
} else {
// 删除的操作
if (dataset.length > 0) {
// 获取删除条目的值
var removedValue = dataset[0].value;
dataset.shift();
}
// 更新X轴比例尺
xScale.domain(d3.range(dataset.length));
var bars = svg.selectAll('rect')
.data(dataset, d => d.key);
var texts = svg.selectAll('text')
.data(dataset, d => d.key);
// 处理退出阶段
bars.exit()
.transition()
.duration(500)
.attr('x', -xScale.bandwidth())
.remove();
texts.exit()
.transition()
.duration(500)
.attr('x', -xScale.bandwidth())
.remove();
// 更新Tooltip
showTooltip(`Removed: ${removedValue}`);
}
// 更新条形图和文本
updateChart();
});
// 点击更新按钮的逻辑
d3.select('#update')
.on('click', function () {
dataset = [];
// 假设设定的新数据长度
var numValues = 19;
for (var i = 0; i < numValues; i++) {
var newNumber = Math.round(d3.randomNormal(15, 8)(), 1);
// 根据i添加一个值
dataset.push({ key: i, value: newNumber });
}
// 更新X轴比例尺
xScale.domain(d3.range(dataset.length));
// 更新条形图和文本
updateChart();
// 更新Tooltip
showTooltip(`Updated chart with ${numValues} values`);
});
// 显示Tooltip的函数
function showTooltip(message) {
d3.select('#tooltip')
.select('#value')
.text(message);
d3.select('#tooltip')
.classed('hidden', false);
setTimeout(() => d3.select('#tooltip')
.classed('hidden', true), 2000);
}
// 更新条形和文本
function updateChart() {
// 更新比例尺,确保纵坐标在范围内
yScale.domain([0, d3.max(dataset, d => d.value) || 0]);
// 更新条形图
var bars = svg.selectAll('rect')
.data(dataset, d => d.key);
// 处理进入阶段
bars.enter()
.append('rect')
.attr('x', (d, i) => w).attr('y', h)
.attr('width', xScale.bandwidth())
.attr('fill', 'green')
// 更新tooltip内容显示
.on('mouseover', function (event, d) {
// 输出当前条形的值
console.log(d);
var xPosition = parseFloat(d3.select(this)
.attr('x')) + xScale.bandwidth() / 2;
var yPosition = parseFloat(d3.select(this)
.attr('y')) / 2 + h / 2;
// 更新tooltip为当前条形值
d3.select('#tooltip')
.style('left', xPosition + 'px')
.style('top', yPosition + 'px')
.select('#value')
.text(`Value: ${d.value}`);
d3.select('#tooltip')
.classed('hidden', false);
})
.on('mouseout', function () {
d3.select('#tooltip')
.classed('hidden', true);
})
.merge(bars)
.transition()
.duration(500)
.attr('x', (d, i) => xScale(i))
.attr('y', d => h - yScale(d.value))
.attr('height', d => yScale(d.value));
// 处理退出阶段
bars.exit()
.transition()
.duration(500)
.attr('height', 0)
.attr('y', h)
.remove();
// 更新条上的数值
var texts = svg.selectAll('text')
.data(dataset, d => d.key);
// 处理进入阶段
texts.enter()
.append('text')
.attr('text-anchor', 'middle')
.attr('font-family', 'sans-serif')
.attr('font-size', '12px')
.attr('fill', 'white')
.merge(texts)
.transition()
.duration(500)
.attr('x', (d, i) => xScale(i) + xScale.bandwidth() / 2)
.attr('y', d => h - yScale(d.value) + 14)
.text(d => d.value);
// 处理退出阶段
texts.exit()
.remove();
}
</script>
</body>
</html>
4 CSV加载
Name,Score
刘一,95
陈二,87
张三,88
李四,98
王五,93
赵六,65
孙七,73
周八,77
吴九,89
郑十,67
- 注意CSV文件的数据格式,确保字段之间无多余的空格或非数字字符。
- 使用VSCode的Live Server插件在本地启动一个HTTP服务器进行查看。
- 在当前路径创建一个file文件夹,并在文件夹中创建一个score.csv文件。
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8' />
<title>CSV加载</title>
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js'>
</script>
<style>
svg {
display: block;
margin: auto;
}
</style>
</head>
<body><br />
<script>
// 数组初始化
var numset = [];
// 全局变量方便加载后使用
var nameset = [];
// 加载csv数据
// 注意D3.js版本5及更高的d3.csv返回一个Promise
// 因此需要使用.then和.catch来处理数据加载和错误
d3.csv('./file/score.csv')
.then(function (data) {
// 取出其中的数字和类别名
for (var i = 0; i < data.length; i++) {
numset.push(parseFloat(data[i].Score));
nameset.push(data[i].Name);
};
// numset转化数据为适合生成饼图的对象数组
var piedata = d3.pie()(numset);
var h = 400;
var w = 400;
// 圆环外半径
var outerRadius = w / 2;
// 圆环内半径
var innerRadius = w / 3;
// 用svg的path绘制弧形的内置方法
var arc = d3.arc()
.outerRadius(outerRadius)
.innerRadius(innerRadius);
var svg = d3.select('body')
.append('svg')
.attr('width', w)
.attr('height', h);
// 颜色函数
var color = d3.scaleOrdinal(d3.schemeCategory10);
// 准备分组,把每个分组移到图表中心
var arcs = svg.selectAll('g.arc')
.data(piedata)
.enter()
.append('g')
.attr('class', 'arc')
// 移到图表中心
.attr(
'transform',
// translate(a, b)中a表示横坐标起点,b表示纵坐标起点
'translate(' + outerRadius + ',' + outerRadius + ')'
);
// 为组中每个元素绘制弧形路路径
arcs.append('path')
.attr('fill', function (d, i) { return color(i) })
// d3使用弧度绘制,将角度转为弧度
.attr('d', arc);
// nameset和numset组合生成文本
arcs.append('text')
.attr('transform', function (d) {
// 计算每个弧形的中心点
return 'translate(' + arc.centroid(d) + ')';
})
.attr('text-anchor', 'middle')
.text(function (d, i) {
return nameset[i] + ':' + d.value;
});
})
.catch(function (error) {
console.error('Error loading the CSV file:', error);
});
</script>
</body>
</html>
5 SVG加载
<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'>
<circle cx='100' cy='100' r='50' fill='none' stroke='red' stroke-width='3' />
<circle cx='100' cy='100' r='60' fill='none' stroke='red' stroke-width='3' />
<circle cx='100' cy='100' r='70' fill='none' stroke='red' stroke-width='3' />
<circle cx='100' cy='100' r='80' fill='none' stroke='red' stroke-width='3' />
<circle cx='100' cy='100' r='90' fill='none' stroke='red' stroke-width='3' />
</svg>
- 使用VSCode的Live Server插件在本地启动一个HTTP服务器进行查看。
- 在当前路径创建一个file文件夹,并在文件夹中创建一个circle.svg文件。
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8' />
<title>SVG加载</title>
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js'>
</script>
<style>
svg {
display: block;
margin: auto;
}
</style>
</head>
<body><br />
<script>
d3.xml('./file/circle.svg')
.then(function (data) {
// 确保加载的是SVG元素
var svgNode = data.documentElement;
// 使用d3.select和append方法将SVG添加到body
d3.select('body')
.node()
.appendChild(svgNode);
})
.catch(function (error) {
console.error('Error loading the SVG file:', error);
});
</script>
</body>
</html>
6 JSON加载
- 径向树
- Radial Tree,以中心节点为起点,向外延伸的方式展示树结构的可视化形式。
- 适合展示较大或对称的层次结构,以中心向外方式显示,强调整体结构的美观。
- 树状图
- Tree Diagram,最常见的层次结构图形,一般自上而下,或左右展示节点关系。
- 直观展示层级关系,简单容易理解,比较适合小到中型数据集,通常用于静态。
- 折叠树
- Collapsible Tree,基于传统树状图的动态可视化形式,允许用户进行交互操作。
- 节省空间,提高数据可读性,适合大型数据集,可通过交互选择需要查看的部分。
- 冰柱图
- Icicle Plot,用于展示分层数据的矩形状态图,每个矩形指一层数据。
- 适合展现各层级的相对贡献或者结构,通常用于表示复杂的分类数据。
- 矩形树
- TreeMap,一种树结构,用于根据层次结构可视化数据,显示数据关系。
- 其中,每个节点包含一个子树和一系列关系,这些关系由线性布局决定。
- 避免跨域问题,使用VSCode的Live Server插件在本地启动一个HTTP服务器进行查看。
- 在当前路径创建一个file文件夹,并在file目录中创建一个tree.json文件,供JSON加载。
{
"name": "福建",
"children": [
{
"name": "福州",
"children": [
{
"name": "闽侯",
"size": 1
},
{
"name": "连江",
"size": 2
},
{
"name": "罗源",
"size": 3
},
{
"name": "闽清",
"size": 4
},
{
"name": "永泰",
"size": 5
},
{
"name": "平潭",
"size": 6
}
]
},
{
"name": "厦门",
"children": [
{
"name": "海沧",
"size": 7
},
{
"name": "集美",
"size": 8
},
{
"name": "同安",
"size": 9
},
{
"name": "翔安",
"size": 10
},
{
"name": "思明",
"size": 11
},
{
"name": "湖里",
"size": 12
}
]
},
{
"name": "莆田",
"children": [
{
"name": "仙游",
"size": 13
},
{
"name": "荔城",
"size": 14
},
{
"name": "城厢",
"size": 15
},
{
"name": "涵江",
"size": 16
},
{
"name": "秀屿",
"size": 17
}
]
},
{
"name": "三明",
"children": [
{
"name": "明溪",
"size": 18
},
{
"name": "清流",
"size": 19
},
{
"name": "宁化",
"size": 20
},
{
"name": "大田",
"size": 21
},
{
"name": "尤溪",
"size": 22
},
{
"name": "将乐",
"size": 23
},
{
"name": "泰宁",
"size": 24
},
{
"name": "建宁",
"size": 25
}
]
},
{
"name": "泉州",
"children": [
{
"name": "惠安",
"size": 26
},
{
"name": "安溪",
"size": 27
},
{
"name": "永春",
"size": 28
},
{
"name": "德化",
"size": 29
},
{
"name": "金门",
"size": 30
}
]
},
{
"name": "漳州",
"children": [
{
"name": "漳浦",
"size": 31
},
{
"name": "云霄",
"size": 32
},
{
"name": "诏安",
"size": 33
},
{
"name": "平和",
"size": 34
},
{
"name": "华安",
"size": 35
},
{
"name": "南靖",
"size": 36
},
{
"name": "东山",
"size": 37
}
]
},
{
"name": "南平",
"children": [
{
"name": "顺昌",
"size": 38
},
{
"name": "浦城",
"size": 39
},
{
"name": "光泽",
"size": 40
},
{
"name": "松溪",
"size": 41
},
{
"name": "政和",
"size": 42
}
]
},
{
"name": "龙岩",
"children": [
{
"name": "长汀",
"size": 43
},
{
"name": "上杭",
"size": 44
},
{
"name": "武平",
"size": 45
},
{
"name": "连城",
"size": 46
}
]
},
{
"name": "宁德",
"children": [
{
"name": "霞浦",
"size": 47
},
{
"name": "古田",
"size": 48
},
{
"name": "屏南",
"size": 49
},
{
"name": "寿宁",
"size": 50
},
{
"name": "周宁",
"size": 51
},
{
"name": "柘荣",
"size": 52
}
]
}
]
}
6-1 径向树
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8' />
<title>径向树</title>
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js'>
</script>
<style>
svg {
display: block;
margin: auto;
}
.node circle {
fill: yellow;
stroke: red;
stroke-width: 1.5px;
}
.node {
font: 10px sans-serif;
}
.link {
fill: green;
stroke: #ccc;
stroke-width: 1.5px;
}
</style>
</head>
<body><br />
<script>
// 设置直径
var diameter = 500;
// 指定径向布局大小和节点邻距
var tree = d3.tree()
// 设置角度360度、半径
.size([360, 200])
.separation(function (a, b) {
// 父节点相同的节点邻距相等,不同的稍宽一点用来区分开
return (a.parent == b.parent ? 1 : 2) / a.depth;
});
// 指定径向布局
var diagonal = d3.linkRadial()
// 将角度转换为弧度
.angle(function (d) { return d.x / 180 * Math.PI; })
.radius(function (d) { return d.y; });
var svg = d3.select('body')
.append('svg')
.attr('width', diameter)
.attr('height', diameter)
.append('g')
.attr(
'transform',
'translate(' + diameter / 2 + ',' + diameter / 2 + ')'
);
// 加载JSON
d3.json('./file/tree.json')
.then(function (root) {
// 返回值是一个数组,每个节点上填充一些计算后的属性
root = d3.hierarchy(root);
tree(root);
// 为连线添加路径
var link = svg.selectAll('.link')
.data(root.links())
.enter()
.append('path')
.attr('class', 'link')
.attr('d', diagonal);
// 节点转换位置
var node = svg.selectAll('.node')
.data(root.descendants())
.enter()
.append('g')
.attr('class', 'node')
.attr('transform', function (d) {
return `rotate(${d.x - 90})translate(${d.y})`;
})
// 节点添加圆圈
node.append('circle')
.attr('r', 4.5);
// 节点添加文字
node.append('text')
.attr('dy', '.31em')
// 小于180度的文字放在前面,否则放在后面
.attr('text-anchor', function (d) {
return d.x < 180 ? 'start' : 'end';
})
.attr('transform', function (d) {
return d.x < 180 ?
'translate(8)' : 'rotate(180)translate(-8)';
})
.text(function (d) { return d.data.name; });
})
.catch(function (error) {
console.error('Error loading the JSON file:', error);
});
</script>
</body>
</html>
6-2 树状图
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8' />
<title>树状图</title>
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js'>
</script>
<style>
svg {
display: block;
margin: auto;
}
.node circle {
fill: #FFF;
stroke: steelblue;
stroke-width: 1.5px;
}
.node {
font: 12px sans-serif;
}
.link {
fill: none;
stroke: #ccc;
stroke-width: 1.5px;
}
</style>
</head>
<body><br />
<svg></svg>
<script>
// 画布
var width = 800;
var height = 1000;
var svg = d3.select('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr(
'transform',
'translate(' + width / 8 + ',' + height / 10 + ')'
);
// color颜色比例尺,能根据传入的索引号获取相应的颜色值
var color = d3.scaleOrdinal(d3.schemeCategory10);
// 加载数据
d3.json('./file/tree.json')
.then(function (data) {
// 使用加载的数据创建层级结构
var root = d3.hierarchy(data);
// 定义一个集群图布局,设定尺寸
var tree = d3.tree()
.size([height - 200, width - 200]);
tree(root);
// 数据转换,使用root.descendants()获取节点
var nodes = root.descendants();
var links = root.links();
console.log(nodes);
console.log(links);
// 绘制连线
var diagonal = d3.linkHorizontal()
.x(function (d) { return d.y; })
.y(function (d) { return d.x; });
var link = svg.selectAll('.link')
.data(root.links())
.enter()
.append('path')
.attr('class', 'link')
.attr('d', diagonal);
// 绘制节点
var node = svg.selectAll('.node')
.data(nodes)
.enter()
.append('g')
.attr('class', 'node')
.attr('transform', function (d) {
return 'translate(' + d.y + ',' + d.x + ')';
});
node.append('circle')
.attr('r', 5)
.style('fill', function (d) {
return color(d.depth);
});
node.append('text')
.attr('dy', '.35em')
.attr('x', function (d) {
return d.children ? -8 : 8;
})
.style('text-anchor', function (d) {
return d.children ? 'end' : 'start';
})
.text(function (d) { return d.data.name; });
})
.catch(function (error) {
console.error('Error loading the JSON file:', error);
});
</script>
</body>
</html>
6-3 折叠树
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8' />
<title>折叠树</title>
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js'>
</script>
<style>
svg {
display: block;
margin: auto;
}
.node circle {
fill: #fff;
stroke: steelblue;
stroke-width: 1.5px;
}
.node {
font: 12px sans-serif;
}
.link {
fill: none;
stroke: #ccc;
stroke-width: 1.5px;
}
</style>
</head>
<body><br />
<svg width='800' height='1000'></svg>
<script>
// 位置参数
var margin = { top: 20, right: 30, bottom: 20, left: 30 },
width = 800 - margin.right - margin.left,
height = 1000 - margin.top - margin.bottom;
var i = 0,
duration = 750,
root, isRootCollapsed = false;
// 声明树布局
var tree = d3.tree()
.size([height, width]);
// 指定为横向布局
var diagonal = d3.linkHorizontal()
.x(function (d) { return d.y; })
.y(function (d) { return d.x; });
var svg = d3.select('svg')
.attr('width', width + margin.right + margin.left)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr(
'transform',
'translate(' + margin.left + ',' + margin.top + ')'
);
d3.json('./file/tree.json')
.then(function (flare) {
// 输出加载的数据以检查格式
// console.log(flare);
// 根节点和位置,将原始数据转换为层次数据结构
root = d3.hierarchy(flare);
root.x0 = height / 2;
root.y0 = 0;
// 初始折叠
root.children.forEach(collapse);
// 折叠后重绘
update(root);
})
.catch(function (error) {
console.error('Error loading the JSON file:', error);
});
// 折叠函数,递归调用
function collapse(d) {
if (d.children) {
// console.log(d);
d._children = d.children;
d._children.forEach(collapse);
d.children = null;
}
}
// 更新布局
function update(source) {
// 计算新树的布局,确保传入树的根节点
var nodes = tree(root).descendants();
var links = tree(root).links();
nodes.forEach(function (d) {
// 树的x,y倒置了,这里y是横向的
d.y = d.depth * 350;
});
// 数据连接,根据id绑定数据
var node = svg.selectAll('g.node')
.data(nodes, function (d) {
// 检查数据是否正确绑定到节点
// console.log('Binding data for node:', d.data.name);
// 最初新点开的节点都没有id,为没有id的节点添加上ID
return d.id || (d.id = ++i);
});
// 点击时增加新的子节点
var nodeEnter = node.enter()
.append('g')
.attr('class', 'node')
.attr('transform', function (d) {
return 'translate(' + d.y + ',' + d.x + ')';
})
.on('click', click);
nodeEnter.append('circle')
.attr('r', 15)
.style('fill', function (d) {
return d._children ? 'lightsteelblue' : '#fff';
});
nodeEnter.append('text')
.attr('x', function (d) {
return d.children || d._children ? 12 : -12;
})
.attr('dy', '.35em')
.attr('text-anchor', function (d) {
return d.children || d._children ? 'end' : 'start';
})
.text(function (d) { return d.data.name; })
.style('fill-opacity', 1);
// 更新已有节点
var nodeUpdate = node.transition()
.duration(duration)
.attr('transform', function (d) {
return 'translate(' + d.y + ',' + d.x + ')';
});
nodeUpdate.select('circle')
.attr('r', 15)
.style('fill', function (d) {
return d._children ? 'lightsteelblue' : '#fff';
});
nodeUpdate.select('text')
.style('fill-opacity', 1);
// 折叠节点的子节点收缩回来
var nodeExit = node.exit()
.transition()
.duration(duration)
.attr('transform', function (d) {
return 'translate(' + d.y + ',' + d.x + ')';
})
.remove();
// 数据连接,根据目标节点的id绑定数据
var link = svg.selectAll('path.link')
.data(links, function (d) { return d.target.id; });
// 增加新连接
link.enter()
.insert('path', 'g')
.attr('class', 'link')
.attr('d', function (d) {
var o = { x: source.x0, y: source.y0 };
return diagonal({ source: o, target: o });
});
// 原有连接更新位置
link.transition()
.duration(duration)
.attr('d', diagonal);
// 折叠的链接,收缩到源节点处
link.exit()
.transition()
.duration(duration)
// .attr('d', function (d) {
// var o = { x: d.source.x, y: d.source.y };
// return diagonal({ source: o, target: o });
// })
.remove();
// 把旧位置存下来,用以过渡
nodes.forEach(function (d) {
d.x0 = d.x;
d.y0 = d.y;
});
}
// 切换折叠与否
function click(event, d) {
// 如果点击的是根节点
if (d === root) {
if (isRootCollapsed) {
// 展开根节点之前的状态
root.children = root._children;
root._children = null;
} else {
// 收缩根节点时,收缩所有子节点
root.children.forEach(collapse);
// 存储当前子节点
root._children = root.children;
// 收缩所有子节点
root.children = null;
}
isRootCollapsed = !isRootCollapsed;
} else {
// 切换当前点击节点的折叠状态
if (d.children) {
// 收缩当前节点
d._children = d.children;
d.children = null;
} else {
// 展开当前节点
d.children = d._children;
d._children = null;
}
// 除了当前节点,收缩其他节点
if (d.parent && d.parent !== root) {
d.parent.children.forEach(function (child) {
if (child !== d) collapse(child);
});
}
}
// 重新渲染
update(d);
}
</script>
</body>
</html>
6-4 冰柱图
- 冰柱图使用了D3的分区布局来实现,主要用来展现数据的层次和包含关系。
- 实际使用笛卡尔排列的分区布局数据绘制即冰柱图,径向排列则是旭日图。
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8' />
<title>冰柱图</title>
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js'>
</script>
<style>
svg {
display: block;
margin: auto;
}
.node {
fill: #ddd;
stroke: #fff;
}
.label {
font: 10px sans-serif;
text-anchor: middle;
}
</style>
</head>
<body><br />
<svg></svg>
<script>
// 画布
var width = 800;
var height = 600;
var svg = d3.select('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr(
'transform',
'translate(' + width / 8 + ',' + height / 8 + ')'
);
// color颜色比例尺,能根据传入的索引号获取相应的颜色值
var color = d3.scaleOrdinal(d3.schemeCategory10);
// 冰柱布局,递归分割节点树到一个旭日或冰柱
var partition = d3.partition()
// x和y指定的布局大小
.size([width, height]);
d3.json('./file/tree.json')
.then(function (data) {
// 使用sum来指定节点的值
var root = d3.hierarchy(data)
.sum(d => d.size)
// 计算分区布局
partition(root);
// 设置文字和节点
svg.selectAll('.node')
.data(root.descendants())
.enter()
.append('rect')
.attr('class', 'node')
.attr('x', d => d.y0)
.attr('y', d => d.x0)
.attr('width', d => d.y1 - d.y0)
.attr('height', d => d.x1 - d.x0)
.style('fill', function (d) {
// 有孩子则返回自己的颜色,无孩子则返回爸爸的颜色
return color((d.children ? d : d.parent).name);
});
svg.selectAll('.label')
.data(
root.descendants()
.filter(d => d.y1 - d.y0 > 6)
)
.enter()
.append('text')
.attr('class', 'label')
.attr('dy', '.35em')
.attr('transform', d => 'translate(' + ((d.y0 + d.y1)
/ 2) + ',' + ((d.x0 + d.x1) / 2) + ')rotate(0)')
.text(function (d) { return d.data.name; });
})
.catch(function (error) {
console.error('Error loading the JSON file:', error);
});
</script>
</body>
</html>
6-5 矩形树
- TreeMap,Ben Shneiderman于1991年推出,递归细分面积成矩形。
- 矩形树使用颜色进行区分类别,用嵌套的方形表示层次关系的布局。
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8' />
<title>矩形树</title>
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js'>
</script>
<style>
svg {
display: block;
margin: auto;
}
</style>
</head>
<body><br />
<svg></svg>
<script>
// 画布
var width = 800;
var height = 600;
var svg = d3.select('svg')
.attr('width', width)
.attr('height', height);
// color颜色比例尺,能根据传入的索引号获取相应的颜色值
var color = d3.scaleOrdinal(d3.schemeCategory10);
// 填充树,使用递归的空间分割来显示节点的树
var treemap = d3.treemap()
// 指定x和y的布局大小
.size([width, height])
// 指定一个父及其子之间的填充
.padding(4);
d3.json('./file/tree.json')
.then(function (data) {
// 将数据转换为d3层次结构
var root = d3.hierarchy(data)
// 计算子节点的大小
.sum(function (d) { return d.size; });
// 创建布局
treemap(root);
// 设置文字和节点
svg.selectAll('.node')
// 使用叶子节点来绘制
.data(root.leaves())
.enter()
.append('rect')
.attr('class', 'node')
.attr('x', function (d) { return d.x0; })
.attr('y', function (d) { return d.y0; })
.attr('width', function (d) { return d.x1 - d.x0; })
.attr('height', function (d) { return d.y1 - d.y0; })
.style('fill', function (d) { return color(d.data.name); });
// 绘制标签
svg.selectAll('.label')
.data(root.leaves())
.enter()
.append('text')
.attr('class', 'label')
.attr('x', function (d) { return d.x0 + 5; })
// 调整标签位置
.attr('y', function (d) { return d.y0 + 20; })
.text(function (d) { return d.data.name; });
})
.catch(function (error) {
console.error('Error loading the JSON file:', error);
});
</script>
</body>
</html>
7 弦图的绘制
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8' />
<title>弦图的绘制</title>
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js'>
</script>
<style>
svg {
display: block;
margin: auto;
}
body {
font: 10px sans-serif;
}
.chord path {
fill-opacity: .67;
stroke: #000;
stroke-width: .5px;
}
</style>
</head>
<body><br />
<script>
// 指定圆环和弦的数值
var matrix = [
// 每组是一个部分圆环,数组的每个值是其中的弦大小
[0, 5871, 8916, 2868], [1951, 10048, 2060, 6171],
[8010, 16145, 0, 8045], [1013, 990, 940, 6907]
];
var width = 960,
height = 500,
innerRadius = Math.min(width, height) * .41,
outerRadius = innerRadius * 1.1;
// 颜色数组
var fill = ['#000000', '#FFDD89', '#957244', '#F26223'];
var svg = d3.select('body')
.append('svg')
.attr('width', width)
.attr('height', height)
.append('g')
// 圆形绘制时以原点为中心,而图形原点在左上角
// 会出现左上图表部分看不到的情况,因此使用translate属性进行位移
.attr(
'transform',
'translate(' + width / 2 + ',' + height / 2 + ')'
);
// 定义弦布局
var chord = d3.chord()
// 设置圆环之间的距离
.padAngle(0.05)
// function规定每行元素的排列顺序
.sortSubgroups(d3.descending);
// 传入数据矩阵
var chords = chord(matrix);
// 绘制圆环并添加鼠标事件
svg.append('g')
.selectAll('path')
// 将圆环按照数据分组,即一个圆环一组
.data(chords.groups)
.enter()
.append('path')
.style('fill', function (d) { return fill[d.index] })
.style('stroke', function (d) { return fill[d.index]; })
// 画圆环
.attr(
'd',
d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius)
)
// 为路径添加鼠标事件
.on('mouseover', fade(.1))
.on('mouseout', fade(1));
// 返回一个设置透明度的函数
function fade(opacity) {
return function (g, i) {
svg.selectAll('.chord path')
// 过滤器,过滤掉没选中的
.filter(function (d) {
return d.source.index !== i && d.target.index !== i;
})
.transition()
// 设置不透明度
.style('opacity', opacity);
};
}
// 用弦生成器绘制弦
svg.append('g')
.attr('class', 'chord')
.selectAll('path')
.data(chords)
.enter()
.append('path')
.attr('d', d3.ribbon().radius(innerRadius))
.style('fill', function (d) {
return fill[d.target.index];
})
.style('opacity', 1);
// 绘制刻度
var ticks = svg.append('g')
.selectAll('g')
// 第一次分组,依据是圆环分组
.data(chords.groups)
.enter()
.append('g')
.selectAll('g')
// 第二次分组,依据是刻度对象
.data(groupTicks)
.enter()
.append('g')
.attr('transform', function (d) {
// 对刻度进行旋转和平移变换
return 'rotate(' + (d.angle * 180 / Math.PI - 90) + ')'
+ 'translate(' + outerRadius + ',0)';
});
// 加刻度线
ticks.append('line')
.attr('x1', 1)
.attr('y1', 0)
.attr('x2', 5)
.attr('y2', 0)
.style('stroke', '#000');
// 加刻度标签
ticks.append('text')
.attr('x', 8)
.attr('dy', '.35em')
.attr('transform', function (d) {
return d.angle > Math.PI ?
'rotate(180)translate(-16)' : null;
})
.style('text-anchor', function (d) {
return d.angle > Math.PI ? 'end' : null;
})
.text(function (d) { return d.label; });
// 生成刻度数据
function groupTicks(d) {
// 把圆环平均化,计算每单位所占角度
var k = (d.endAngle - d.startAngle) / d.value;
// 生成[0, d.value]之间1000个平均分隔的数字
return d3.range(0, d.value, 1000).
// 首先创建一个刻度的数组,map遍历并映射
// 返回一个1000个值的数组,每个值带有角度和标签属性
map(function (v, i) {
return {
// 一刻度的角度,加偏移量得到最终坐标
angle: v * k + d.startAngle,
// 每五个刻度给一个刻度标签
label: i % 5 ? null : v / 1000 + 'k'
};
});
}
</script>
</body>
</html>