You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用D3.js实现相同parallelism值的分组柱状图问题

D3.js分组柱状图实现问题

我首次使用D3.js,想要为性能测试结果创建分组柱状图,需求如下:

  • X轴展示parallelism(线程数),Y轴展示duration(耗时,单位毫秒)
  • 相同parallelism值的多条记录需相邻显示,理想状态下按duration排序
  • 参考分组柱状图示例调试了一个多小时,始终无法正确实现分组,问题集中在fx、x比例尺的定义以及.attr("x", d => marginLeft+fx(d.duration))的赋值上,尝试多种变体均无效。

原代码

JavaScript

const runData = [
      {"duration":74950.52171194553,"parallelism":1},
      {"duration":88687.86499893665,"parallelism":0,"exitCode":0},
      {"duration":60000,"parallelism":1,"exitCode":0},
      {"duration":90000,"parallelism":0,"exitCode":0},
      {"duration":90000,"parallelism":0,"exitCode":0}
  ];

  const width = 700;
  const height = 400;
  const marginTop = 10;
  const marginRight = 10;
  const marginBottom = 20;
  const marginLeft = 40;
  // Create the SVG container.
  const svg = d3.select("#ca")
    .append("svg")
    .attr("width", width)
    .attr("height", height);

  const paralelismSet = new Set(runData.map(x=>x.parallelism));
  const paralelismList = runData.map(x=>x.parallelism);
  paralelismList.sort();
  const durationSet = new Set(runData.map(x=>x.duration));

  const minDuration = Math.min(...durationSet);
  const maxDuration = Math.max(...durationSet);

  // I am trying here to sort by duration but group by parallelism
  const fx = d3.scaleBand()
      .domain(durationSet)
      .rangeRound([marginLeft, width - marginRight])
      .paddingInner(0.1);

  const x = d3.scaleBand()
      .domain(paralelismList)
      .rangeRound([0, fx.bandwidth()])
      .padding(0.05);

  const color = d3.scaleLinear([minDuration, maxDuration], ["green", "red"]);

  // Y encodes the height of the bar.
  const y = d3.scaleLinear()
      .domain([0, maxDuration]).nice()
      .rangeRound([height - marginBottom, marginTop]);

  svg.append("g")
    .selectAll()
    .data(d3.group(runData, d => d.parallelism))
    .join("g")
      .attr("transform", ([paralelism]) => `translate(${x(paralelism)},0)`)
    .selectAll()
    .data(([, d]) => d)
    .join("rect")
      // I tried various combinations of subtracting fx from x and vice versa
      .attr("x", d => marginLeft+fx(d.duration))
      .attr("y", d => y(d.duration))
      .attr("width", x.bandwidth())
      .attr("height", d => y(0) - y(d.duration))
      .attr("fill", d => color(d.duration));

  // The horizontal axis does not work well either, it is too short
  svg.append("g")
      .attr("transform", `translate(${marginLeft},${height - marginBottom})`)
      .call(d3.axisBottom(x));

  // This code works fine
  svg.append("g")
      .attr("transform", `translate(${marginLeft},0)`)
      .call(d3.axisLeft(y).ticks(null, "s"))
      .call(g => g.selectAll(".domain").remove());

HTML

<div id="ca">
</div>
<script src="https://d3js.org/d3.v6.min.js"></script>

问题分析与修正方案

核心问题

  1. 比例尺定义逻辑颠倒:分组柱状图需外层比例尺对应X轴分组(parallelism),内层比例尺对应分组内子项位置
  2. 分组数据未按duration排序,不符合需求
  3. X轴定位错误,导致显示异常

修正后的完整代码

JavaScript

const runData = [
  {"duration":74950.52171194553,"parallelism":1},
  {"duration":88687.86499893665,"parallelism":0,"exitCode":0},
  {"duration":60000,"parallelism":1,"exitCode":0},
  {"duration":90000,"parallelism":0,"exitCode":0},
  {"duration":90000,"parallelism":0,"exitCode":0}
];

const width = 700;
const height = 400;
const marginTop = 10;
const marginRight = 10;
const marginBottom = 20;
const marginLeft = 40;

// 创建SVG容器
const svg = d3.select("#ca")
  .append("svg")
  .attr("width", width)
  .attr("height", height);

// 提取并去重排序parallelism值,作为X轴分组
const parallelismValues = [...new Set(runData.map(d => d.parallelism))].sort();
// 获取分组内最大子项数量,用于内层比例尺适配
const maxGroupSize = d3.max([...d3.group(runData, d => d.parallelism)].map(([_, group]) => group.length));

const minDuration = d3.min(runData, d => d.duration);
const maxDuration = d3.max(runData, d => d.duration);

// 外层比例尺:控制X轴分组位置
const xScale = d3.scaleBand()
  .domain(parallelismValues)
  .rangeRound([marginLeft, width - marginRight])
  .paddingInner(0.1);

// 内层比例尺:控制分组内子项横向排列
const xSubScale = d3.scaleBand()
  .domain(d3.range(maxGroupSize))
  .rangeRound([0, xScale.bandwidth()])
  .padding(0.05);

// 颜色比例尺:耗时越短越绿,越长越红
const colorScale = d3.scaleLinear()
  .domain([minDuration, maxDuration])
  .range(["green", "red"]);

// Y轴比例尺:映射耗时到高度
const yScale = d3.scaleLinear()
  .domain([0, maxDuration]).nice()
  .rangeRound([height - marginBottom, marginTop]);

// 绘制分组柱状图
svg.append("g")
  .selectAll("g")
  .data(d3.group(runData, d => d.parallelism))
  .join("g")
    // 外层分组平移到对应parallelism的X轴位置
    .attr("transform", ([parallelism]) => `translate(${xScale(parallelism)}, 0)`)
    .selectAll("rect")
    // 分组内数据按duration升序排序
    .data(([, group]) => group.sort((a, b) => a.duration - b.duration))
    .join("rect")
      // 通过子项索引获取内层X位置
      .attr("x", (d, i) => xSubScale(i))
      .attr("y", d => yScale(d.duration))
      .attr("width", xSubScale.bandwidth())
      .attr("height", d => yScale(0) - yScale(d.duration))
      .attr("fill", d => colorScale(d.duration));

// 绘制X轴
svg.append("g")
  .attr("transform", `translate(0, ${height - marginBottom})`)
  .call(d3.axisBottom(xScale))
  .call(g => g.selectAll(".domain").remove());

// 绘制Y轴
svg.append("g")
  .attr("transform", `translate(${marginLeft}, 0)`)
  .call(d3.axisLeft(yScale).ticks(null, "s"))
  .call(g => g.selectAll(".domain").remove());

HTML

<div id="ca"></div>
<script src="https://d3js.org/d3.v6.min.js"></script>

关键修改说明

  1. 比例尺逻辑调整:外层xScale对应parallelism分组,内层xSubScale用索引控制分组内子项位置
  2. 数据排序:绑定分组数据时添加排序逻辑,实现同分组内按耗时升序排列
  3. 坐标轴修正:X轴移除多余的marginLeft偏移,与分组位置对齐
  4. 子项定位:通过子项索引计算内层X位置,确保同分组内子项相邻显示

内容的提问来源于stack exchange,提问作者Tomáš Zato

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 20:52:04