《Getting Started with D3》填坑之旅(五):第三章(上)

本文通过一个实例展示了如何使用D3.js绘制公交故障与碰撞受伤率的散点图,强调了d3.extent()函数在确定数据范围、比例尺定义以及坐标轴生成中的应用。在D3.js版本更新后,对代码进行了适配,解决了坐标轴标签显示和样式问题,最终实现了预期的可视化效果。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

cover

Chapter 3. Scales, Axes and Lines(比例尺、坐标轴与线)

本章介绍的是各种尺度、坐标轴的处理以及折线的使用。诚如开篇所言,绘制可视化图表时要考虑的一个基本问题,就是怎样将实际的数据值,以恰当比例的像素大小与颜色呈现在页面上。D3 在这方面做了大量优化工作,一起来看看吧。

示例1:公交故障间距关于碰撞受伤率的散点图

本例的背景,是想探讨纽约市的公交故障频率、公交相撞事件及乘客交通事故之间是否存在关联关系。毕竟在纽约这样一个交通管网错综复杂的的大都会,偶发的故障或交通事故可能会引发一系列连锁反应,如果能从统计角度找出一些关联因素,则可以提前采取应对措施。这里选取的问题切入点,是想看看有人员伤亡的公交碰撞事件发生率,是否会对公交事故的间距产生影响(真是不明觉厉的脑回路。。。好吧,只是学学 D3 散点图的绘制,其他的问题就饶过作者吧)。

言归正传。为了解决映射比例的问题,D3 提供了一个工具函数 d3.extent(),返回一个包含最小值与最大值的数组,省去了手动计算取值范围的问题。

然后将实际值的范围作定义域(domain),绘图区的范围作值域(range),得到两个轴向上的比例尺。

接着再将数据绑定到 SVG 绘图区的 circle 元素上,利用定义好的比例尺绘出各个数据点。

最后是利用 D3 的坐标轴函数生成 x 轴和 y 轴,以及对应的坐标轴名称,设置好 CSS 样式,即大功告成。

完整代码如下:

<body>
    <h2>Ch3 - Example 1 | Bus Breakdown, Accident, and Injury</h2>
    <script src="/demos/js/d3.js"></script>
    <script>
        let json = null;
        function draw(data) {
            "use strict";

            // visualization code goes here
            json = data;

            var margin = 50,
                width = 700,
                height = 300;

            // plot circles            
            var x_extent = d3.extent(data, function(d){ return d.collision_with_injury });
            var y_extent = d3.extent(data, function(d){ return d.dist_between_fail });
            
            var x_scale = d3.scale.linear()
                .domain(x_extent)
                .range([margin, width - margin]);
            var y_scale = d3.scale.linear()
                .domain(y_extent)
                .range([height - margin, margin]);

            d3.select('body')
                .append('svg')
                    .attr('width', width)
                    .attr('height', height)
                .selectAll('circle')
                .data(data)
                .enter()
                .append('circle')
                    .attr('r', 5)
                    .attr('cx', function(d){ return x_scale(d.collision_with_injury) })
                    .attr('cy', function(d){ return y_scale(d.dist_between_fail) });

            // Add axes
            // x
            var x_axis = d3.svg.axis().scale(x_scale);
            d3.select('svg')
                .append('g')
                    .attr('class', 'x axis')
                    .attr('transform', 'translate(0, ' + (height - margin) + ')')
                    .call(x_axis);
            // y
            var y_axis = d3.svg.axis().scale(y_scale).orient('left');
            d3.select('svg')
                .append('g')
                    .attr('class', 'y axis')
                    .attr('transform', 'translate(' + margin + ', 0)')
                    .call(y_axis);
            
            // Axis title
            d3.select('.x.axis')
                .append('text')
                    .text("collisions with injury (per million miles)")
                    .attr('x', (width / 2) - margin)
                    .attr('y', margin / 1.5);

            d3.select('.y.axis')
                .append('text')
                .text('mean distance between failure (miles)')
                .attr('transform', 'rotate(-90, -43, 0) translate(-280)');
        }

        d3.json("/demos/data/bus_perf.json", draw);
    </script>
    <style>
        .axis path{
            fill: none;
            stroke: black;
        }
        .axis {
            font-size: 8pt;
            font-family: sans-serif;
        }
        .tick {
            fill: none;
            stroke: black;
        }
        circle {
            stroke: black;
            stroke-width: 0.5px;
            fill: royalblue;
            opacity: 0.6;
        }
    </style>
</body>

效果如下:

旧版示意图

本例的一个小坑,出现在 D3.js 的版本替换上,已知的变更包括:

  1. 线性比例尺的创建,由 d3.scale.linear() 改为了 d3.scaleLinear()
  2. 坐标轴的创建方式:
    1. 旧版: d3.svg.axis().scale(x_scale)d3.svg.axis().scale(y_scale).orient('left')
    2. 新版: d3.axisBottom(x_scale)d3.axisLeft(y_scale)(当然还有 d3.axisTopd3.axisRight,更贴近声明式风格)

但第一次改动后的最终效果却不甚理想:

首次修复示意图

可以看到坐标轴上的数字加粗了,且坐标轴标签没有显示出来。前者可以在 CSS 样式中禁用 .tich{ stroke: black; } 来修复,后者则要按 F12 查看 text 标签是否生成了(确实生成了):

F12 检查 text 元素是否存在

然后在 JS 中或 CSS 中加入填充色:

  1. JavaScript 中修复:
d3.select('.x.axis')
  .append('text')
    .attr('fill', 'black')
// ...
d3.select('.x.axis')
  .append('text')
    .attr('fill', 'black')
// ...
  1. CSS 中修复:
text {
    fill: black;
}

效果如下:

首次修复示意图

注意到 y 轴名称显示不全,需再次手动调整 y 标签旋转后的纵向平移量,实测发现改为 translate(-100) 比较合适:

最终效果

附:新版写法完整代码:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
    <title>Ch3 - Example 1 | Getting Started with D3</title>
</head>

<body>
    <h2>Ch3 - Example 1 | Bus Breakdown, Accident, and Injury</h2>
    <script src="/demos/js/d3.v6.js"></script>
    <script>
        let json = null;
        const draw = data => {
            "use strict";

            // badass visualization code goes here
            json = data;

            const margin = 50,
                width = 700,
                height = 300;

            // plot circles            
            const x_extent = d3.extent(data, d => d.collision_with_injury);
            const y_extent = d3.extent(data, d => d.dist_between_fail);
            
            const x_scale = d3.scaleLinear()  // v6.7.0
                .domain(x_extent)
                .range([margin, width - margin]);
            const y_scale = d3.scaleLinear()  // v6.7.0
                .domain(y_extent)
                .range([height - margin, margin]);

            d3.select('body')
                .append('svg')
                    .attr('width', width)
                    .attr('height', height)
                .selectAll('circle')
                .data(data)
                .join(enter => enter.append('circle')
                    .attr('r', 5)
                    .attr('cx', d => x_scale(d.collision_with_injury))
                    .attr('cy', d => y_scale(d.dist_between_fail))
                );

            // Add axes
            // x
            const x_axis = d3.axisBottom(x_scale);  // v6.7.0
            d3.select('svg')
                .append('g')
                    .attr('class', 'x axis')
                    .attr('transform', `translate(0, ${(height - margin)})`)
                    .call(x_axis);
            // y
            const y_axis = d3.axisLeft(y_scale);    // v6.7.0
            d3.select('svg')
                .append('g')
                    .attr('class', 'y axis')
                    .attr('transform', `translate(${margin}, 0)`)
                    .call(y_axis);
            
            // Axis title
            // x
            d3.select('.x.axis')
                .append('text')
                    .attr('fill', 'black')  // v6.7.0 
                    .text("collisions with injury (per million miles)")
                    .attr('x', (width / 2) - margin)
                    .attr('y', margin / 1.5);
            // y
            d3.select('.y.axis')
                .append('text')
                    .attr('fill', 'black')  // v6.7.0
                    .text('mean distance between failure (miles)')
                    .attr('transform', 'rotate(-90, -43, 0) translate(-100)');
        }

        d3.json("/demos/data/bus_perf.json").then(draw)
            .catch(console.error);
    </script>
    <style>
        .axis path{
            fill: none;
            stroke: black;
        }
        .axis {
            font-size: 8pt;
            font-family: sans-serif;
        }
        .tick {
            fill: none;
            /* stroke: black; */
        }
        circle {
            stroke: black;
            stroke-width: 0.5px;
            fill: royalblue;
            opacity: 0.6;
        }
    </style>
</body>

</html>

示例2:绘制两个地点的转门日均流量对比图

(未完待续)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

安冬的码畜日常

您的鼓励是我持续优质内容的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值