D3组合条形图和面积图

时间:2018-05-07 08:36:28

标签: javascript d3.js

我想知道是否可以按照下面的屏幕截图显示的方式实现combination of areabar chart

同时使区域之间可点击以进行其他操作。 如果您可以引导我参考一些示例以了解如何实现相同目标,那将非常有用。

Aimed Display

2 个答案:

答案 0 :(得分:1)

在下面的示例中,我将一个简单的条形图(如this famous bl.lock)与一些polygons组合在一起。我想也可以用path来实现。

Chart

const data = [
  { letter: "a", value: 9 },
  { letter: "b", value: 6 },
  { letter: "c", value: 3 },
  { letter: "d", value: 8 }
];
const svg = d3.select("#chart");
const margin = { top: 20, right: 20, bottom: 30, left: 40 };
const width = +svg.attr("width") - margin.left - margin.right;
const height = +svg.attr("height") - margin.top - margin.bottom;

const xScale = d3.scaleBand()
  .rangeRound([0, width]).padding(0.5)
  .domain(data.map(d => d.letter));
  
const yScale = d3.scaleLinear()
  .rangeRound([height, 0])
  .domain([0, 10]);

const g = svg.append("g")
  .attr("transform", `translate(${margin.left},${margin.top})`);

g.append("g")
  .attr("class", "axis axis--x")
  .attr("transform", `translate(0,${height})`)
  .call(d3.axisBottom(xScale));
  
g.append("g")
  .attr("class", "axis axis--y")
  .call(d3.axisLeft(yScale));
  
g.selectAll(".bar")
  .data(data)
  .enter().append("rect")
    .attr("class", "bar")
    .attr("x", d => xScale(d.letter))
    .attr("y", d => yScale(d.value))
    .attr("width", xScale.bandwidth())
    .attr("height", d => height - yScale(d.value));

// Add polygons
g.selectAll(".area")
  .data(data)
  .enter().append("polygon")
    .attr("class", "area")
    .attr("points", (d,i,nodes) => {
      if (i < nodes.length - 1) {
        const dNext = d3.select(nodes[i + 1]).datum();
        
        const x1 = xScale(d.letter) + xScale.bandwidth();
        const y1 = height;
        
        const x2 = x1;
        const y2 = yScale(d.value);
        
        const x3 = xScale(dNext.letter);
        const y3 = yScale(dNext.value);
        
        const x4 = x3;
        const y4 = height;
        
        return `${x1},${y1} ${x2},${y2} ${x3},${y3} ${x4},${y4} ${x1},${y1}`;        
      }
    })
    .on("click", (d,i,nodes) => {
      const dNext = d3.select(nodes[i + 1]).datum();
      const pc = Math.round((dNext.value - d.value) / d.value * 100.0);
      alert(`${d.letter} to ${dNext.letter}: ${pc > 0 ? '+' : ''}${pc} %`);
    });
.bar {
  fill: steelblue;
}

.area {
  fill: lightblue;
}

.area:hover {
  fill: sandybrown;
  cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<svg width="400" height="300" id="chart"></svg>

答案 1 :(得分:1)

我发布了一个codepen here。这会创建一个条形图,然后在每个条形图之间分隔区域图。

&#13;
&#13;
const BarChart = () => {
  // set data
  const data = [
    {
      value: 48,
      label: 'One Rect'
    },
    {
      value: 32,
      label: 'Two Rect'
    },
    {
      value: 40,
      label: 'Three Rect'
    }
  ];
  // set selector of container div
  const selector = '#bar-chart';
  // set margin
  const margin = {top: 60, right: 0, bottom: 90, left: 30};
  // width and height of chart
  let width;
  let height;
  // skeleton of the chart
  let svg;
  // scales
  let xScale;
  let yScale;
  // axes
  let xAxis;
  let yAxis;
  // bars
  let rect;
  // area
  let areas = [];

  function init() {

    // get size of container
    width = parseInt(d3.select(selector).style('width')) - margin.left - margin.right;
    height = parseInt(d3.select(selector).style('height')) - margin.top - margin.bottom;
    // create the skeleton of the chart
    svg = d3.select(selector)
      .append('svg')
        .attr('width', '100%')
        .attr('height', height + margin.top + margin.bottom)
      .append('g')
        .attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')');

    xScale = d3.scaleBand().padding(0.15);
    xAxis = d3.axisBottom(xScale);
    yScale = d3.scaleLinear();
    yAxis = d3.axisLeft(yScale);

    svg.append('g')
      .attr('class', 'x axis')
      .attr('transform', `translate(0, ${height})`);

    svg.append('g')
      .attr('class', 'y axis');

    svg.append('g')
      .attr('class', 'x label')
      .attr('transform', `translate(10, 20)`)
      .append('text')
      .text('Value');

    xScale
      .domain(data.map(d => d.label))
      .range([0, width])
      .padding(0.3);

    yScale
      .domain([0, 75])
      .range([height, 0]);

    xAxis
      .scale(xScale);

    yAxis
      .scale(yScale);

    rect = svg.selectAll('rect')
      .data(data);

    rect
      .enter()
      .append('rect')
      .style('fill', d => '#00BCD4')
      .attr('y', d => yScale(d.value))
      .attr('height', d => height - yScale(d.value))
      .attr('x', d => xScale(d.label))
      .attr('width', xScale.bandwidth());

    // call the axes
    svg.select('.x.axis')
      .call(xAxis);

    svg.select('.y.axis')
      .call(yAxis);

    // rotate axis text
    svg.select('.x.axis')
      .selectAll('text')
        .attr('transform', 'rotate(45)')
        .style('text-anchor', 'start');

    if (parseInt(width) >= 600) {
      // level axis text
      svg.select('.x.axis')
        .selectAll('text')
          .attr('transform', 'rotate(0)')
          .style('text-anchor', 'middle');
    }
    
    data.forEach(
      (d, i) => {
        if (data[i + 1]) {
          areas.push([
            {
              x: d.label,
              y: d.value
            },
            {
              x: data[i + 1].label,
              y: data[i + 1].value
            }
          ]);
        }
      }
    );
    
    areas = areas.filter(
      d => Object.keys(d).length !== 0
    );
    
    areas.forEach(
      a => {
        const area = d3.area()
          .x((d, i) => {
            return i === 0 ?
              xScale(d.x) + xScale.bandwidth() :
              xScale(d.x);
          })
          .y0(height)
          .y1(d => yScale(d.y));
        
        svg.append('path')
          .datum(a)
          .attr('class', 'area')
          .style('fill', d => '#B2EBF2')
          .attr('d', area)
          .on('click', d => {
            console.log('hello click!');
        });
      }
    )
  }

  return { init };
};

const myChart = BarChart();
myChart.init();
&#13;
#bar-chart {
  height: 500px;
  width: 100%;
}
&#13;
<script src="https://unpkg.com/d3@5.2.0/dist/d3.min.js"></script>
<div id="bar-chart"></div>
&#13;
&#13;
&#13;

创建条形图后,我重新打包数据,使其有助于创建区域图表。我创建了一个areas数组,其中每个项目将成为一个单独的区域图表。我基本上取第一个柱和下一个柱的值,并将它们打包在一起。

data.forEach(
  (d, i) => {
    if (data[i + 1]) {
      areas.push([
        {
          x: d.label,
          y: d.value
        },
        {
          x: data[i + 1].label,
          y: data[i + 1].value
        }
      ]);
    }
  }
);

areas = areas.filter(
  d => Object.keys(d).length !== 0
);

然后我遍历areas上的每个元素并创建面积图。

我认为,这里唯一棘手的问题是让区域图从第一个柱的末端延伸到第二个柱的开始,而不是从第一个柱的末端到第二个柱的末端。酒吧。为了实现这一点,我在处理第一个数据点时,从我的x-scale添加了一个矩形宽度到区域图的预期x值,而不是第二个。

我认为这是在一条线上制作两个点:一个用于第一个柱,一个用于下一个柱。 D3的区域功能可以遮蔽一条线下的所有区域。所以,我的第一个点应该是第一个栏的右上角。第二个点应该是下一个栏的左上角。

最后附加一个点击事件非常简单。

areas.forEach(
  a => {
    const area = d3.area()
      .x((d, i) => {
        return i === 0 ?
          xScale(d.x) + xScale.bandwidth() :
          xScale(d.x);
      })
      .y0(height)
      .y1(d => yScale(d.y));

    svg.append('path')
      .datum(a)
      .attr('class', 'area')
      .style('fill', d => '#B2EBF2')
      .attr('d', area)
      .on('click', d => {
        console.log('hello click!');
    });
  }
)