跳到主要内容

数据加载与处理

真实的数据可视化项目很少会直接把数据硬编码在代码里。通常,数据存储在外部文件中,需要在运行时异步加载。D3 提供了 d3-fetch 模块来处理各种数据格式的加载,同时 d3-array 模块提供了强大的数据处理能力。

为什么需要外部数据加载?

在前面的例子中,我们直接在代码里定义了数据数组。这种方式有几个明显的局限:数据量较小时还可以,一旦数据量变大,代码会变得难以维护;数据更新时需要修改源代码并重新部署;无法多人协作处理数据。

外部数据加载很好地解决了这些问题。数据可以存储在独立的文件中,由专门的团队或系统负责维护,前端代码只需要关注如何展示。

d3-fetch 模块概述

d3-fetch 模块提供了五个主要的加载函数:d3.json() 用于加载 JSON 格式数据;d3.csv() 用于加载 CSV(逗号分隔值)格式数据;d3.tsv() 用于加载 TSV(制表符分隔值)格式数据;d3.xml() 用于加载 XML 格式数据;d3.html() 用于加载 HTML 片段。

这些函数都返回一个 Promise,这意味着你可以使用 async/await 语法或传统的 .then() 链式调用来处理加载结果。

JSON 数据加载

JSON 是 JavaScript 环境中最常用的数据格式。它的数据结构与 JavaScript 对象完全一致,解析效率很高,适合存储结构化数据。

简单的 JSON 文件

假设我们有一个名为 fruits.json 的文件,内容如下:

[
{"name": "苹果", "price": 5, "category": "水果"},
{"name": "香蕉", "price": 3, "category": "水果"},
{"name": "橙子", "price": 4, "category": "水果"},
{"name": "白菜", "price": 2, "category": "蔬菜"},
{"name": "胡萝卜", "price": 3, "category": "蔬菜"}
]

使用 d3.json() 加载这个文件:

async function loadData() {
const data = await d3.json('fruits.json');
console.log(data);
// 输出: [{name: "苹果", price: 5, category: "水果"}, ...]
}

loadData();

带错误处理的加载

在实际项目中,网络请求可能因为各种原因失败。添加适当的错误处理可以让应用更加健壮:

d3.json('fruits.json')
.then(data => {
console.log('数据加载成功:', data);
})
.catch(error => {
console.error('数据加载失败:', error.message);
});

配置请求参数

d3.json() 接受一个可选的配置对象,用于设置请求头、超时时间等:

const config = {
method: 'POST', // 请求方法
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer your-token'
},
body: JSON.stringify({query: 'some data'})
};

d3.json('api/data', config)
.then(data => {
console.log('数据:', data);
});

有时你可能不想使用 await 语法,或者需要在已有的 Promise 链中添加数据加载。使用 .then() 的方式同样可以完成相同的工作:

d3.json('data/fruits.json')
.then(data => {
// 数据加载完成后的处理逻辑
renderChart(data);
})
.catch(error => {
console.error('加载失败:', error);
});

CSV 数据加载

CSV(Comma-Simited Values,逗号分隔值)是最常用的表格数据格式之一。与 JSON 相比,CSV 文件体积更小,更适合处理大规模数据。但 CSV 也有缺点:只能表示扁平结构,没有列标题行的情况下需要手动指定字段名。

基础 CSV 加载

假设有一个 sales.csv 文件,内容如下:

date,sales,region
2024-01-01,1000,华北
2024-01-02,1500,华北
2024-01-03,1200,华东
2024-01-04,1800,华东
2024-01-05,2000,华南

加载 CSV 数据:

d3.csv('sales.csv')
.then(data => {
console.log(data);
// 注意:CSV 加载后,所有值都是字符串类型
// 输出: [{date: "2024-01-01", sales: "1000", region: "华北"}, ...]
});

类型自动转换

默认情况下,CSV 的所有值都被解析为字符串。如果数据包含数字或日期,需要手动进行类型转换:

d3.csv('sales.csv', d3.autoType)
.then(data => {
console.log(data);
// 现在 sales 是数字,date 是 Date 对象
// 输出: [{date: Date, sales: 1000, region: "华北"}, ...]
});

d3.autoType 会智能判断每个字段的类型:纯数字字符串转换为数字;符合日期格式的字符串转换为 Date 对象;其他字符串保持不变。

手动指定列类型

对于更精细的控制,可以使用 row 转换函数手动指定每个字段的类型:

d3.csv('sales.csv', function(d) {
return {
date: new Date(d.date), // 解析为日期对象
sales: +d.sales, // 转换为数字(+ 是简写)
region: d.region // 字符串保持不变
};
})
.then(data => {
console.log(data);
});

TSV 数据加载

TSV(Tab-Separated Values,制表符分隔值)使用制表符而非逗号作为分隔符。当数据本身包含逗号时(如描述字段),TSV 是更好的选择:

// 使用 d3.tsv() 加载制表符分隔的文件
d3.tsv('data.tsv')
.then(data => {
console.log(data);
});

CSV 和 TSV 的 API 完全一致,区别只是默认的分隔符不同。

并行加载多个数据源

当一个可视化需要展示来自多个数据源的数据时,可以并行加载来提高效率:

async function loadAllData() {
const [users, orders, products] = await Promise.all([
d3.json('data/users.json'),
d3.json('data/orders.json'),
d3.json('data/products.json')
]);

// 三个请求会并行执行,全部完成后才继续
console.log('用户:', users.length);
console.log('订单:', orders.length);
console.log('产品:', products.length);
}

Promise.all() 会等待所有 Promise 完成后再执行回调,非常适合这种需要同时使用多个数据源的场景。

d3-array 数据处理模块

加载数据后,通常还需要对数据进行预处理,比如排序、分组、筛选、聚合等。d3-array 模块提供了丰富的数组操作函数。

数据排序

有时数据需要按照特定字段进行排序才能用于可视化:

// 假设有销售数据
const salesData = [
{product: '苹果', sales: 150},
{product: '香蕉', sales: 80},
{product: '橙子', sales: 200}
];

// 按销售额升序排序
const sortedAsc = [...salesData].sort((a, b) => a.sales - b.sales);
// 结果: [{product: "香蕉", sales: 80}, {product: "苹果", sales: 150}, ...]

// 按销售额降序排序
const sortedDesc = [...salesData].sort((a, b) => b.sales - a.sales);
// 结果: [{product: "橙子", sales: 200}, {product: "苹果", sales: 150}, ...]

// 使用 d3.ascending 进行安全比较
const sorted = [...salesData].sort((a, b) => d3.ascending(a.sales, b.sales));

数据筛选

根据条件筛选出符合条件的数据子集:

const salesData = [
{product: '苹果', sales: 150, region: '华北'},
{product: '香蕉', sales: 80, region: '华东'},
{product: '橙子', sales: 200, region: '华北'},
{product: '葡萄', sales: 120, region: '华南'}
];

// 筛选华北地区的销售记录
const northData = salesData.filter(d => d.region === '华北');
// 结果: [{product: "苹果", sales: 150, region: "华北"},
// {product: "橙子", sales: 200, region: "华北"}]

// 筛选销售额大于 100 的记录
const highSales = salesData.filter(d => d.sales > 100);

数据分组与聚合

数据分组是可视化中非常常见的操作。d3.group() 和 d3.rollup() 是进行分组的两个主要函数:

const salesData = [
{product: '苹果', sales: 150, region: '华北'},
{product: '香蕉', sales: 80, region: '华东'},
{product: '橙子', sales: 200, region: '华北'},
{product: '葡萄', sales: 120, region: '华南'},
{product: '西瓜', sales: 300, region: '华南'}
];

// 按地区分组
const groupedByRegion = d3.group(salesData, d => d.region);
// Map(3) { "华北" => [...], "华东" => [...], "华南" => [...] }

// 转换为数组便于使用
const groupedArray = Array.from(groupedByRegion, ([region, data]) => ({
region,
data
}));
// 结果: [{region: "华北", data: [...]}, ...]

// 按地区分组并计算每个地区的总销售额
const salesByRegion = d3.rollup(
salesData,
v => d3.sum(v, d => d.sales), // 聚合函数:求和
d => d.region // 分组键
);
// Map(3) { "华北" => 350, "华东" => 80, "华南" => 420 }

// 转换为对象格式
const salesByRegionObj = Object.fromEntries(salesByRegion);
// 结果: { "华北": 350, "华东": 80, "华南": 420 }

统计计算

d3-array 提供了常用的统计函数:

const values = [10, 20, 30, 40, 50];

// 求和
d3.sum(values); // 150

// 求平均值
d3.mean(values); // 30
d3.average(values); // 30(别名)

// 求最小值
d3.min(values); // 10
d3.minIndex(values); // 0(最小值索引)

// 求最大值
d3.max(values); // 50
d3.maxIndex(values); // 4

// 同时获取 min 和 max
d3.extent(values); // [10, 50]

// 中位数
d3.median(values); // 30

// 方差和标准差
d3.variance(values); // 200
d3.deviation(values); // 14.14

数据集求交、并、补

处理多个数据集时,常用的集合操作:

const setA = [1, 2, 3, 4];
const setB = [3, 4, 5, 6];

// 并集:合并两个数组并去重
const union = d3.union(setA, setB); // [1, 2, 3, 4, 5, 6]

// 交集:两个数组都有的元素
const intersection = d3.intersection(setA, setB); // [3, 4]

// 差集:在 A 中但不在 B 中的元素
const difference = d3.difference(setA, setB); // [1, 2]

// 补集:在 A 或 B 中但不在两者中都有的元素
const disjoint = d3.disjoint(setA, setB); // false

完整示例:从 CSV 到可视化

让我们用一个完整的例子来演示数据加载和处理的完整流程。

步骤 1:准备数据文件

创建一个 sales.csv 文件:

date,product,category,sales,region
2024-01-01, iPhone15, 电子, 8000, 北京
2024-01-01, MacBook, 电子, 12000, 北京
2024-01-01, T恤, 服装, 200, 上海
2024-01-02, iPhone15, 电子, 7500, 北京
2024-01-02, AirPods, 电子, 1500, 上海
2024-01-02, 牛仔裤, 服装, 350, 上海

步骤 2:编写加载和处理代码

async function createSalesChart() {
// 1. 加载 CSV 数据
const rawData = await d3.csv('sales.csv', d3.autoType);

console.log('原始数据:', rawData);
// [
// {date: Date, product: "iPhone15", category: "电子", sales: 8000, region: "北京"},
// ...
// ]

// 2. 按产品类别分组并计算销售额
const categorySales = d3.rollup(
rawData,
v => d3.sum(v, d => d.sales), // 聚合:求和
d => d.category // 分组:按类别
);

// 3. 转换为数组格式便于 D3 处理
const chartData = Array.from(categorySales, ([category, sales]) => ({
category,
sales
}));

// 4. 按销售额降序排序
chartData.sort((a, b) => b.sales - a.sales);

console.log('处理后的数据:', chartData);
// [{category: "电子", sales: 21000}, {category: "服装", sales: 550}]

// 5. 创建柱状图...
renderBarChart(chartData);
}

function renderBarChart(data) {
const margin = {top: 30, right: 30, bottom: 50, left: 60};
const width = 600 - margin.left - margin.right;
const height = 400 - margin.top - margin.bottom;

const svg = d3.select('#chart')
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);

// X 轴:产品类别
const x = d3.scaleBand()
.domain(data.map(d => d.category))
.range([0, width])
.padding(0.2);

// Y 轴:销售额
const y = d3.scaleLinear()
.domain([0, d3.max(data, d => d.sales)])
.nice()
.range([height, 0]);

// 绘制柱形
svg.selectAll('.bar')
.data(data)
.join('rect')
.attr('class', 'bar')
.attr('x', d => x(d.category))
.attr('y', d => y(d.sales))
.attr('width', x.bandwidth())
.attr('height', d => height - y(d.sales))
.attr('fill', 'steelblue');

// 添加 X 轴
svg.append('g')
.attr('transform', `translate(0,${height})`)
.call(d3.axisBottom(x));

// 添加 Y 轴
svg.append('g')
.call(d3.axisLeft(y).tickFormat(d => `¥${d}`));
}

createSalesChart();

这段代码涵盖了数据加载的完整流程:从外部文件加载数据、对数据进行预处理(分组、排序)、将数据绑定到可视化元素。理解了这个流程,你就可以应对大多数数据可视化的场景。

本章小结

本章介绍了 D3 中数据加载和处理的核心概念。外部数据加载让我们能够从独立的数据文件中获取数据,使代码更加模块化。d3-fetch 模块提供了加载 JSON、CSV、TSV 等格式的方法,而 d3-array 模块则提供了丰富的数据处理函数。

在实际项目中,数据加载通常需要考虑网络错误处理、数据类型转换、多数据源并行加载等场景。掌握这些技能对于构建生产级别的数据可视化应用至关重要。