D3:创建响应范围条形图

D3: Creating responsive range bar chart

我正在尝试在 D3 中创建范围条形图(请参阅 a similar example)。

我找到了 ,我能够将它定制成我所需要的,除了它使用旧版本的 D3,数据是单独数组的愚蠢格式,而且它不是响应式(为什么有那么多 D3 东西没有响应式?)。

我放弃了那个尝试,从头开始。我现在主要使用最新的 D3 和上面 SO 问题的一些想法来工作,但数据显示不正确。当我调整大小时,条形图无法预测地增长和收缩,最终在我变宽时得到负宽度值。

如果您能帮助我理解我在条形尺寸方面做错了什么,我将不胜感激。请注意,要观察不正确的行为,您必须展开代码段并调整 window.

的大小

let data = [
  { field: "Field Name 1", salary: [42, 65], bonusRange: [null, null] },
  { field: "Field Name 2", salary: [28, 58], bonusRange: [null, null] },
  { field: "Field Name 3", salary: [33, 57], bonusRange: [null, null] },
  { field: "Field Name 4", salary: [33, 50], bonusRange: [null, null] },
  { field: "Field Name 5", salary: [32, 44], bonusRange: [38, 52] },
  { field: "Field Name 6", salary: [27, 40], bonusRange: [null, null] }
];

const chartDiv = document.getElementById("chart");
const svg = d3.select(chartDiv).append("svg");

function redraw() {
  svg.selectAll("g").remove();

  const margin = 20;
  const width = chartDiv.clientWidth - margin * 2;
  const height = chartDiv.clientHeight - margin * 2;

  svg
    .attr("width", width)
    .attr("height", height)
    .attr("transform", `translate(${margin}, ${margin})`);

  const xScale = d3
    .scaleLinear()
    .range([120, width - 10])
    .domain([20, 70]);

  const xAxis = d3.axisBottom(xScale);

  const yScale = d3
    .scaleLinear()
    .range([0, height - margin])
    .domain([-0.5, (data.length - 1) * 1.1]);

  const yAxis = d3
    .axisLeft(yScale)
    .ticks(data.length)
    .tickFormat(function(d, i) {
      return data[i].field;
    });

  svg
    .append("g")
    .attr("class", "axis")
    .call(xAxis)
    .attr("transform", `translate(0, ${height - margin})`);

  svg
    .append("g")
    .attr("class", "axis")
    .call(yAxis)
    .attr("transform", "translate(120, 0)");

  svg
    .append("g")
    .attr("id", "bars")
    .selectAll(".bar")
    .data(data)
    .enter()
    .append("rect")
    .style("fill", "#f61166")
    .attr("class", "bar")
    .attr("x", function(d) {
      return xScale(d.salary[0]);
    })
    .attr("height", 25)
    .attr("width", function(d) {
      return xScale(d.salary[1] - d.salary[0]);
    })
    .attr("y", function(d, i) {
      return yScale(i) - margin * 0.5;
    });
}

redraw();
window.addEventListener("resize", redraw);
* {
  box-sizing: border-box;
}

body {
  padding: 10px;
}

#chart {
  outline: 1px solid fuchsia;
  height: 0;
  overflow: hidden;
  padding-top: 59.75%;
  background: white;
  position: relative;
}

#chart svg {
  position: absolute;
  top: 0;
  left: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="chart" class=""></div>

你的代码需要大量的重构,你通常应该遵循 d3 update pattern 只要你想改变数据(或者在这种情况下 window 的大小,ps。我不是 100% 确定这是个好主意,因为我从来没有像这样制作图表 "responsive",但它似乎工作得很好)

换句话说,当你想更新时不要使用 svg.selectAll("g").remove();,你显然可以,但这不是 d3 的惯用语。

我从重绘函数中删除了所有附加内容,并将它们放在包装函数中,这样我们就可以通过 class 名称或变量来引用它们。

我还把你的 yScale 变量变成了一个比例尺,这样可以更容易地根据数据调整图表的大小。

const yScale = d3.scaleBand()
    .domain(data.map(d => d.field))
    .padding(0.1)
    .paddingOuter(0.1)
    .paddingInner(0.1);

我在下面添加了一个片段,希望它应该清楚如何完成这样的事情。

let data = [
 { field: "Field Name 1", salary: [42, 65], bonusRange: [null, null] },
 { field: "Field Name 2", salary: [28, 58], bonusRange: [null, null] },
 { field: "Field Name 3", salary: [33, 57], bonusRange: [null, null] },
 { field: "Field Name 4", salary: [33, 50], bonusRange: [null, null] },
 { field: "Field Name 5", salary: [32, 44], bonusRange: [38, 52] },
 { field: "Field Name 6", salary: [27, 40], bonusRange: [null, null] }
];

chart(data);

function chart(data) {

 var svg = d3.select("#chart"),
  margin = {top: 55, bottom: 0, left: 85, right: 0},
  width  = parseInt(svg.style("width")) - margin.left - margin.right,
  height = parseInt(svg.style("height")) - margin.top - margin.bottom;
 
 const xScale = d3.scaleLinear()
  .domain([20, 70]);

 const yScale = d3.scaleBand()
  .domain(data.map(d => d.field))
  .padding(0.1)
  .paddingOuter(0.1)
  .paddingInner(0.1);

 const xAxis = svg.append("g")
  .attr("class", "x-axis")

 const yAxis = svg.append("g")
  .attr("class", "y-axis")

 redraw(width, height);

 function redraw(width, height) {

  yScale.range([margin.top, height - margin.bottom])
 
  svg.selectAll(".y-axis")
   .attr("transform", `translate(${margin.left},0)`)
   .call(d3.axisLeft(yScale)
    .ticks(data.length)
    .tickFormat(function(d, i) {
     return data[i].field;
       }));
 
  xScale.range([margin.left, width - margin.right]);

  svg.selectAll(".x-axis").transition().duration(0)
   .attr("transform", `translate(0,${height})`)
   .call(d3.axisBottom(xScale));
 
  var bar = svg.selectAll(".bar")
   .data(data)

  bar.exit().remove();

  bar.enter().append("rect")
   .attr("class", "bar")
   .style("fill", "#f61166")
   .merge(bar)
  .transition().duration(0)
   .attr("width", function(d) {
    return xScale(d.salary[1] - d.salary[0]) - xScale(0);
   })
   .attr("height", yScale.bandwidth())
   .attr("y", d => yScale(d.field))
   .attr("x", function(d) {
    return xScale(d.salary[0]);
   });
 }

 d3.select(window).on('resize', function() {
  width = parseInt(svg.style("width")) - margin.left - margin.right,
  height = parseInt(svg.style("height")) - margin.top - margin.bottom;
  redraw(width, height);
 });
}
body {
 padding: 25px 25px 25px 25px;
 font: 18px arial;
}
#chart {
 outline: 1px solid fuchsia;
 position: absolute;
 width: 95%;
 height: 95%;
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg id="chart"></svg>