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