喜迎
春节

可视化D3(二)


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>

文章作者: bsf
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC 4.0 许可协议。转载请注明来源 bsf !
评 论
  目录