D3.js 高级主题 #
本章将介绍 D3.js 的高级主题,帮助你成为真正的 D3.js 专家,能够处理复杂的数据可视化项目。
性能优化 #
减少 DOM 操作 #
javascript
const circles = svg.selectAll('circle')
.data(data)
.enter()
.append('circle')
.attr('cx', d => d.x)
.attr('cy', d => d.y)
.attr('r', 5)
.attr('fill', 'steelblue')
.attr('stroke', '#333')
.attr('stroke-width', 1);
circles.attr('fill', d => colorScale(d.value));
使用 Canvas 渲染大数据 #
javascript
const canvas = d3.select('#chart')
.append('canvas')
.attr('width', width)
.attr('height', height);
const context = canvas.node().getContext('2d');
data.forEach(d => {
context.beginPath();
context.arc(xScale(d.x), yScale(d.y), 3, 0, 2 * Math.PI);
context.fillStyle = 'steelblue';
context.fill();
});
虚拟 DOM 技术 #
javascript
function render(data) {
const virtualDom = d3.select(document.createElement('svg'));
virtualDom.selectAll('circle')
.data(data)
.join('circle')
.attr('cx', d => d.x)
.attr('cy', d => d.y)
.attr('r', 5);
return virtualDom;
}
使用 Web Workers #
javascript
const worker = new Worker('worker.js');
worker.postMessage({ type: 'process', data: largeData });
worker.onmessage = function(event) {
const processedData = event.data;
updateChart(processedData);
};
function updateChart(data) {
svg.selectAll('circle')
.data(data)
.join('circle')
.attr('cx', d => d.x)
.attr('cy', d => d.y)
.attr('r', 3);
}
优化过渡动画 #
javascript
d3.selectAll('circle')
.transition()
.duration(300)
.attr('r', 10);
d3.selectAll('circle')
.transition()
.duration(1000)
.attr('r', 10);
使用 requestAnimationFrame #
javascript
function animate() {
d3.selectAll('circle')
.attr('cx', d => d.x + Math.random() * 2 - 1);
requestAnimationFrame(animate);
}
animate();
模块化开发 #
可复用图表组件 #
javascript
function barChart() {
let width = 400;
let height = 300;
let margin = { top: 20, right: 20, bottom: 30, left: 40 };
let x = d => d.category;
let y = d => d.value;
let color = 'steelblue';
function chart(selection) {
selection.each(function(data) {
const svg = d3.select(this)
.selectAll('svg')
.data([data]);
const svgEnter = svg.enter().append('svg');
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
const xScale = d3.scaleBand()
.domain(data.map(x))
.range([0, innerWidth])
.padding(0.2);
const yScale = d3.scaleLinear()
.domain([0, d3.max(data, y)])
.nice()
.range([innerHeight, 0]);
const g = svgEnter.merge(svg)
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
g.selectAll('.bar')
.data(data)
.join('rect')
.attr('class', 'bar')
.attr('x', d => xScale(x(d)))
.attr('y', d => yScale(y(d)))
.attr('width', xScale.bandwidth())
.attr('height', d => innerHeight - yScale(y(d)))
.attr('fill', color);
});
}
chart.width = function(_) {
if (!arguments.length) return width;
width = _;
return chart;
};
chart.height = function(_) {
if (!arguments.length) return height;
height = _;
return chart;
};
chart.margin = function(_) {
if (!arguments.length) return margin;
margin = _;
return chart;
};
chart.x = function(_) {
if (!arguments.length) return x;
x = _;
return chart;
};
chart.y = function(_) {
if (!arguments.length) return y;
y = _;
return chart;
};
chart.color = function(_) {
if (!arguments.length) return color;
color = _;
return chart;
};
return chart;
}
const chart = barChart()
.width(600)
.height(400)
.x(d => d.category)
.y(d => d.value)
.color('steelblue');
d3.select('#chart')
.datum(data)
.call(chart);
使用 ES6 类 #
javascript
class BarChart {
constructor(options) {
this.width = options.width || 400;
this.height = options.height || 300;
this.margin = options.margin || { top: 20, right: 20, bottom: 30, left: 40 };
this.data = [];
}
render(selection) {
this.selection = selection;
this.update();
return this;
}
update() {
const innerWidth = this.width - this.margin.left - this.margin.right;
const innerHeight = this.height - this.margin.top - this.margin.bottom;
const xScale = d3.scaleBand()
.domain(this.data.map(d => d.category))
.range([0, innerWidth])
.padding(0.2);
const yScale = d3.scaleLinear()
.domain([0, d3.max(this.data, d => d.value)])
.nice()
.range([innerHeight, 0]);
this.selection.selectAll('.bar')
.data(this.data)
.join('rect')
.attr('class', 'bar')
.attr('x', d => xScale(d.category))
.attr('y', d => yScale(d.value))
.attr('width', xScale.bandwidth())
.attr('height', d => innerHeight - yScale(d.value))
.attr('fill', 'steelblue');
return this;
}
setData(data) {
this.data = data;
this.update();
return this;
}
resize(width, height) {
this.width = width;
this.height = height;
this.update();
return this;
}
}
const chart = new BarChart({ width: 600, height: 400 });
chart.render(d3.select('#chart')).setData(data);
数据处理 #
数据加载 #
javascript
d3.csv('data.csv').then(data => {
data.forEach(d => {
d.value = +d.value;
});
render(data);
});
d3.json('data.json').then(data => {
render(data);
});
d3.tsv('data.tsv').then(data => {
render(data);
});
Promise.all([
d3.csv('data1.csv'),
d3.json('data2.json')
]).then(([csvData, jsonData]) => {
render(csvData, jsonData);
});
数据转换 #
javascript
const grouped = d3.group(data, d => d.category);
const rolled = d3.rollup(
data,
v => d3.mean(v, d => d.value),
d => d.category
);
const nested = d3.nest()
.key(d => d.category)
.rollup(v => d3.mean(v, d => d.value))
.entries(data);
const flat = d3.flatRollup(
data,
v => d3.mean(v, d => d.value),
d => d.category
);
数据统计 #
javascript
d3.min(data, d => d.value);
d3.max(data, d => d.value);
d3.extent(data, d => d.value);
d3.mean(data, d => d.value);
d3.median(data, d => d.value);
d3.sum(data, d => d.value);
d3.deviation(data, d => d.value);
d3.variance(data, d => d.value);
d3.quantile(data.map(d => d.value).sort(d3.ascending), 0.5);
const bisect = d3.bisector(d => d.x).left;
const index = bisect(data, xValue);
数据分箱 #
javascript
const bin = d3.bin()
.value(d => d.value)
.domain([0, 100])
.thresholds(10);
const bins = bin(data);
格式化 #
数字格式化 #
javascript
const format = d3.format('.2f');
format(3.14159);
const formatPercent = d3.format('.0%');
formatPercent(0.123);
const formatCurrency = d3.format('$,.2f');
formatCurrency(1234.5);
const formatSI = d3.format('.2s');
formatSI(1234567);
const formatThousands = d3.format(',.0f');
formatThousands(1234567);
时间格式化 #
javascript
const formatTime = d3.timeFormat('%Y-%m-%d');
formatTime(new Date());
const formatMonth = d3.timeFormat('%B %Y');
formatMonth(new Date());
const formatHour = d3.timeFormat('%H:%M');
formatHour(new Date());
const parseTime = d3.timeParse('%Y-%m-%d');
parseTime('2020-01-01');
const isoFormat = d3.isoFormat;
const isoParse = d3.isoParse;
本地化格式化 #
javascript
const zhCN = d3.formatLocale({
decimal: '.',
thousands: ',',
grouping: [3],
currency: ['¥', '']
});
const format = zhCN.format('$,.2f');
format(1234.5);
地理可视化 #
基本地图 #
javascript
const projection = d3.geoMercator()
.scale(150)
.translate([width / 2, height / 2]);
const path = d3.geoPath().projection(projection);
d3.json('world.json').then(world => {
svg.selectAll('path')
.data(topojson.feature(world, world.objects.countries).features)
.enter()
.append('path')
.attr('d', path)
.attr('fill', 'lightgray')
.attr('stroke', 'white');
});
投影类型 #
javascript
d3.geoMercator()
d3.geoAlbers()
d3.geoAlbersUsa()
d3.geoEquirectangular()
d3.geoOrthographic()
d3.geoConicEqualArea()
d3.geoTransverseMercator()
地理缩放 #
javascript
const zoom = d3.zoom()
.scaleExtent([1, 8])
.on('zoom', zoomed);
svg.call(zoom);
function zoomed(event) {
svg.selectAll('path')
.attr('transform', event.transform);
projection
.scale(150 * event.transform.k)
.translate([
event.transform.x + width / 2,
event.transform.y + height / 2
]);
}
最佳实践 #
1. 代码组织 #
javascript
const chart = {
margin: { top: 20, right: 20, bottom: 30, left: 40 },
width: 600,
height: 400,
init() {
this.innerWidth = this.width - this.margin.left - this.margin.right;
this.innerHeight = this.height - this.margin.top - this.margin.bottom;
this.createScales();
this.createSVG();
return this;
},
createScales() {
this.xScale = d3.scaleBand()
.range([0, this.innerWidth])
.padding(0.2);
this.yScale = d3.scaleLinear()
.range([this.innerHeight, 0]);
},
createSVG() {
this.svg = d3.select('#chart')
.append('svg')
.attr('width', this.width)
.attr('height', this.height);
this.g = this.svg.append('g')
.attr('transform', `translate(${this.margin.left}, ${this.margin.top})`);
},
render(data) {
this.xScale.domain(data.map(d => d.category));
this.yScale.domain([0, d3.max(data, d => d.value)]);
this.drawBars(data);
this.drawAxes();
return this;
},
drawBars(data) {
this.g.selectAll('.bar')
.data(data)
.join('rect')
.attr('class', 'bar')
.attr('x', d => this.xScale(d.category))
.attr('y', d => this.yScale(d.value))
.attr('width', this.xScale.bandwidth())
.attr('height', d => this.innerHeight - this.yScale(d.value))
.attr('fill', 'steelblue');
},
drawAxes() {
this.g.append('g')
.attr('transform', `translate(0, ${this.innerHeight})`)
.call(d3.axisBottom(this.xScale));
this.g.append('g')
.call(d3.axisLeft(this.yScale));
}
};
chart.init().render(data);
2. 响应式设计 #
javascript
function responsiveChart() {
const container = d3.select('#chart');
const width = container.node().clientWidth;
const height = width * 0.6;
svg.attr('width', width).attr('height', height);
xScale.range([0, width - margin.left - margin.right]);
yScale.range([height - margin.top - margin.bottom, 0]);
updateChart();
}
d3.select(window).on('resize', responsiveChart);
3. 无障碍设计 #
javascript
svg.attr('role', 'img')
.attr('aria-label', 'Bar chart showing sales data');
svg.selectAll('.bar')
.attr('role', 'graphics-symbol')
.attr('aria-label', d => `${d.category}: ${d.value}`);
svg.append('title')
.text('Sales data by category');
svg.append('desc')
.text('A bar chart showing sales data for five categories');
4. 错误处理 #
javascript
d3.csv('data.csv')
.then(data => {
if (!data || data.length === 0) {
throw new Error('No data loaded');
}
render(data);
})
.catch(error => {
console.error('Error loading data:', error);
showError('Failed to load data');
});
function showError(message) {
d3.select('#chart')
.append('div')
.attr('class', 'error')
.text(message);
}
5. 测试 #
javascript
function testScale() {
const scale = d3.scaleLinear()
.domain([0, 100])
.range([0, 500]);
console.assert(scale(0) === 0, 'scale(0) should be 0');
console.assert(scale(50) === 250, 'scale(50) should be 250');
console.assert(scale(100) === 500, 'scale(100) should be 500');
}
function testChart() {
const data = [
{ category: 'A', value: 10 },
{ category: 'B', value: 20 }
];
const chart = barChart().width(400).height(300);
const selection = d3.select(document.createElement('div'));
selection.datum(data).call(chart);
console.assert(selection.selectAll('rect').size() === 2, 'Should have 2 bars');
}
调试技巧 #
使用 console #
javascript
console.log(d3.select('#chart').node());
d3.selectAll('circle').each(function(d, i) {
console.log(i, d, this);
});
console.log(xScale.domain(), xScale.range());
使用 debugger #
javascript
d3.selectAll('circle')
.attr('cx', function(d) {
debugger;
return xScale(d.x);
});
检查选择 #
javascript
const selection = d3.selectAll('circle');
console.log(selection.size());
console.log(selection.empty());
console.log(selection.node());
console.log(selection.nodes());
总结 #
恭喜你完成了 D3.js 的学习之旅!从基础的选择器和数据绑定,到高级的性能优化和模块化开发,你已经掌握了成为数据可视化专家所需的所有技能。
继续实践,不断探索,你将能够创建出令人惊叹的数据可视化作品!
资源推荐 #
最后更新:2026-03-28