【问题标题】:D3 How to add rounded top border to a stacked bar chartD3 如何为堆积条形图添加圆角顶部边框
【发布时间】:2021-03-30 19:42:42
【问题描述】:

我有一个项目需要创建堆叠条形图。此条形图需要在顶部有一个圆形边框。我找到了一些关于如何圆条形图顶部边框的文档。我遵循的示例可以在这里找到:Rounded top corners for bar chart.

它说要添加具有以下内容的属性:

`
 M${x(item.name)},${y(item.value) + ry}
 a${rx},${ry} 0 0 1 ${rx},${-ry}
 h${x.bandwidth() - 2 * rx}
 a${rx},${ry} 0 0 1 ${rx},${ry}
 v${height - y(item.value) - ry}
 h${-x.bandwidth()}Z
`

您还需要声明两个变量rxry 来定义角的锐度。

我面临的问题是我无法让它与我的堆积条形图一起使用。顶部堆栈需要在顶部四舍五入。此外,当顶部堆栈为零时,下一个堆栈需要四舍五入。这样无论包含什么数据,栏的顶部都始终是圆形的。

我添加了一个精简的 sn-p。它包括一个用于转换顶部堆栈的按钮(设置为零)。这个sn-p当然也包括圆角所需的属性。但它不会四舍五入。

this.width = 400;
this.height = 200;
var margin = {
  top: 20,
  right: 20,
  bottom: 30,
  left: 40
}

this.index = 0;

this.svg = d3
  .select(".canvas")
  .classed("svg-container", true)
  .append("svg")
  .attr("class", "chart")
  .attr(
    "viewBox",
    `0 0 ${this.width} ${this.height}`
  )
  .attr("preserveAspectRatio", "xMinYMin meet")
  .classed("svg-content-responsive", true)
  .append("g");

const scale = [0, 1200];

// set the scales
this.xScale = d3
  .scaleBand()
  .range([0, width])
  .padding(0.3);

this.yScale = d3.scaleLinear().range([this.height, 0]);

var bars = this.svg.append("g").attr("class", "bars");

const update = data => {
  const scale = [0, 1200];

  // Update scales.
  this.xScale.domain(data.map(d => d.key));
  this.yScale.domain([scale[0], scale[1]]);

  const subgroups = ["home", "work", "public"];

  var color = d3
    .scaleOrdinal()
    .domain(subgroups)
    .range(["#206BF3", "#171D2C", "#8B0000"]);

  var stackData = d3.stack().keys(subgroups)(data);

  const rx = 12;
  const ry = 12;

  // Set up transition.
  const dur = 1000;
  const t = d3.transition().duration(dur);

  bars
    .selectAll("g")
    .data(stackData)
    .join(
      enter => enter
      .append("g")
      .attr("fill", d => color(d.key)),

      null, // no update function

      exit => {
        exit
          .transition()
          .duration(dur / 2)
          .style("fill-opacity", 0)
          .remove();
      }
    ).selectAll("rect")
    .data(d => d, d => d.data.key)
    .join(
      enter => enter
      .append("rect")
      .attr("class", "bar")
      .attr("x", d => {
        return this.xScale(d.data.key);
      })
      .attr("y", () => {
        return this.yScale(0);
      })
      .attr("height", () => {
        return this.height - this.yScale(0);
      })
      .attr("width", this.xScale.bandwidth())

      .attr(
        'd',
        item =>
        `M${this.xScale(item.name)},${this.yScale(item.value) + ry}
        a${rx},${ry} 0 0 1 ${rx},${-ry}
        h${this.xScale.bandwidth() - 2 * rx}
        a${rx},${ry} 0 0 1 ${rx},${ry}
        v${this.height - this.yScale(item.value) - ry}
        h${-this.xScale.bandwidth()}Z
        `
      ),
      null,
      exit => {
        exit
          .transition()
          .duration(dur / 2)
          .style("fill-opacity", 0)
          .remove();
      }
    )
    .transition(t)
    .delay((d, i) => i * 20)
    .attr("x", d => this.xScale(d.data.key))
    .attr("y", d => {
      return this.yScale(d[1]);
    })
    .attr("width", this.xScale.bandwidth())
    .attr("height", d => {
      return this.yScale(d[0]) - this.yScale(d[1]);
    });
};

const data = [
  [{
      key: "1",
      home: 282,
      work: 363,
      public: 379
    },
    {
      key: "2",
      home: 232,
      work: 432,
      public: 0
    }
  ],
  [{
      key: "1",
      home: 282,
      work: 363,
      public: 379
    },
    {
      key: "2",
      home: 232,
      work: 0,
      public: 0
    }
  ]
];

update(data[this.index]);

const swap = document.querySelector(".swap");
swap.addEventListener("click", () => {
  if (this.index < 1) this.index += 1;
  else this.index = 0;
  update(data[this.index]);
});
<button class="swap">swap</button>
<div class="canvas"></div>
<script src="https://d3js.org/d3.v6.js"></script>

【问题讨论】:

    标签: javascript d3.js rounded-corners


    【解决方案1】:

    rect的绘图改为path。所以.append("rect") 变成了.append("path"),你就不再需要xyheightwidth 属性了。

    要仅舍入顶部堆栈,您可以在 d3 绘制最后一个子组 (d[1] - d[0] == d.data[subgroups[subgroups.length-1]]) 之前将 rxry 设置为 12,在本例中为“公共”,否则将它们设置为 0 .

    最后,关于你的最后一个问题:

    当顶部堆栈为零时,下一个堆栈需要四舍五入

    堆栈是按顺序绘制的。在每个栈被绘制之前,找出下一个栈/子组是否为零,如果是,则设置rxry = 12。要找出这个,你需要得到当前正在绘制的子组,这样你就可以确定下一个子组将是什么,并获取该子组的值。

    const current_subgroup = Object.keys(d.data).find(key => d.data[key] === d[1] - d[0]);
    const next_subgroup_index = Math.min(subgroups.length - 1, subgroups.indexOf(current_subgroup)+1);
    const next_subgroup_data = stackData[next_subgroup_index][i];
                if (next_subgroup_data[1] - next_subgroup_data[0] == 0) { rx = 12; ry = 12; }
    

    this.width = 400;
    this.height = 200;
    var margin = {
      top: 20,
      right: 20,
      bottom: 30,
      left: 40
    }
    
    this.index = 0;
    
    this.svg = d3
      .select(".canvas")
      .classed("svg-container", true)
      .append("svg")
      .attr("class", "chart")
      .attr(
        "viewBox",
        `0 0 ${this.width} ${this.height}`
      )
      .attr("preserveAspectRatio", "xMinYMin meet")
      .classed("svg-content-responsive", true)
      .append("g");
    
    const scale = [0, 1200];
    
    // set the scales
    this.xScale = d3
      .scaleBand()
      .range([0, width])
      .padding(0.3);
    
    this.yScale = d3.scaleLinear().range([this.height, 0]);
    
    var bars = this.svg.append("g").attr("class", "bars");
    
    const update = data => {
      const scale = [0, 1200];
    
      // Update scales.
      this.xScale.domain(data.map(d => d.key));
      this.yScale.domain([scale[0], scale[1]]);
    
      const subgroups = ["home", "work", "public"];
    
      var color = d3
        .scaleOrdinal()
        .domain(subgroups)
        .range(["#206BF3", "#171D2C", "#8B0000"]);
    
      var stackData = d3.stack().keys(subgroups)(data);
    
      let rx = 12;
      let ry = 12;
    
      // Set up transition.
      const dur = 1000;
      const t = d3.transition().duration(dur);
    
      bars
        .selectAll("g")
        .data(stackData)
        .join(
          enter => enter
          .append("g")
          .attr("fill", d => color(d.key)),
    
          null, // no update function
    
          exit => {
            exit
              .transition()
              .duration(dur / 2)
              .style("fill-opacity", 0)
              .remove();
          }
        ).selectAll("path")
        .data(d => d, d => d.data.key)
        .join(
          enter => enter
          .append("path")
          .attr("class", "bar")
          .attr(
            'd',
            d =>
            `M${this.xScale(d.data.key)},${this.yScale(0)}
            a0,0 0 0 1 0,0
            h${this.xScale.bandwidth()}
            a0,0 0 0 1 0,0
            v${this.height - this.yScale(0)}
            h${-this.xScale.bandwidth()}Z
            `
          ),
          null,
          exit => {
            exit
              .transition()
              .duration(dur / 2)
              .style("fill-opacity", 0)
              .remove();
          }
        )
        .transition(t)
        .delay((d, i) => i * 20)
        .attr(
          'd',
          (d, i) => {
            //if last subgroup, round the corners of the stack
            if (d[1] - d[0] == d.data[subgroups[subgroups.length-1]]) { rx = 12; ry = 12; }
            else { rx = 0; ry = 0; }
            
            //if next subgroup is zero, round the corners of the stack
            const current_subgroup = Object.keys(d.data).find(key => d.data[key] === d[1] - d[0]);
            const next_subgroup_index = Math.min(subgroups.length - 1, subgroups.indexOf(current_subgroup)+1);
            const next_subgroup_data = stackData[next_subgroup_index][i];
            if (next_subgroup_data[1] - next_subgroup_data[0] == 0) { rx = 12; ry = 12; }
            
            //draw the stack
            if (d[1] - d[0] > 0) {
              return `M${this.xScale(d.data.key)},${this.yScale(d[1]) + ry}
              a${rx},${ry} 0 0 1 ${rx},${-ry}
              h${this.xScale.bandwidth() - 2 * rx}
              a${rx},${ry} 0 0 1 ${rx},${ry}
              v${this.yScale(d[0]) - this.yScale(d[1]) - ry}
              h${-this.xScale.bandwidth()}Z
              `
            } else {
              return `M${this.xScale(d.data.key)},${this.yScale(d[1])}
              a0,0 0 0 1 0,0
              h${this.xScale.bandwidth()}
              a0,0 0 0 1 0,0
              v${this.yScale(d[0]) - this.yScale(d[1]) }
              h${-this.xScale.bandwidth()}Z
              `
            }
          }
        );
    };
    const data = [
      [{
          key: "1",
          home: 282,
          work: 363,
          public: 379
        },
        {
          key: "2",
          home: 232,
          work: 432,
          public: 0
        }
      ],
      [{
          key: "1",
          home: 282,
          work: 363,
          public: 379
        },
        {
          key: "2",
          home: 232,
          work: 0,
          public: 0
        }
      ]
    ];
    
    update(data[this.index]);
    
    const swap = document.querySelector(".swap");
    swap.addEventListener("click", () => {
      if (this.index < 1) this.index += 1;
      else this.index = 0;
      update(data[this.index]);
    });
    <button class="swap">swap</button>
    <div class="canvas"></div>
    <script src="https://d3js.org/d3.v6.js"></script>

    【讨论】:

    • 感谢您的回答。但是当我尝试您的解决方案时,两种状态之间的转换就会丢失。它仍应从一种状态转换到另一种状态。
    • @Stephen 这是因为我的解决方案在子组为零时不绘制堆栈,即if (d[1] - d[0] &gt; 0)。我编辑了答案以绘制一个平面/空堆栈,以便保留过渡。
    • 嗨@斯蒂芬!这是否回答你的问题?如果您可以标记为答案,那就太好了。谢谢!
    • 遇到了类似的问题,这是一个巨大的帮助 - 感谢@SharonChoong!
    • 很高兴它帮助了你! @prestonsmith
    猜你喜欢
    • 2018-02-03
    • 1970-01-01
    • 2020-09-14
    • 1970-01-01
    • 1970-01-01
    • 2018-05-04
    • 1970-01-01
    • 2016-10-13
    • 1970-01-01
    相关资源
    最近更新 更多