Home > Mobile >  D3.js - Issue creating normalised stacked bar chart using example function on Obsevablehq
D3.js - Issue creating normalised stacked bar chart using example function on Obsevablehq

Time:02-12

I'm having issues using the function provided at:

https://observablehq.com/@d3/stacked-normalized-horizontal-bar

The data i'm passing into the function is in the format that is used as an example

{Airline: 'Virgin America', Sentiment: 'positive', Count: 11},
{Airline: 'Virgin America', Sentiment: 'neutral', Count: 8},
{Airline: 'Virgin America', Sentiment: 'negative', Count: 3},
{Airline: 'Delta', Sentiment: 'neutral', Count: 10}.....

The data was not already in this format so I use the following code to process to this format, here is the original dataset

for (object of data){


    if (processed.length === 0){
        processed.push({Airline: object.airline, Sentiment: object.airline_sentiment, Count: 1})
    } else {
        objIndex = processed.findIndex((obj => obj.Airline === object.airline && obj.Sentiment === object.airline_sentiment))

        if (objIndex === -1){
            processed.push({Airline: object.airline, Sentiment: object.airline_sentiment, Count: 1})
        } else {
            processed[objIndex].Count  = 1
            
        }
    }      
}

I'm also passing in a sentiment array as follows for zDomain values

sentiment = ['positive', 'neutral', 'negative']

Here is the parameters i'm using for my function, basically the same as the example

chart = StackedBarChart(processed, {
    x: d => d.Count,
    y: d => d.Airline,
    z: d => d.Sentiment,
    yDomain: d3.groupSort(
        processed,
        D) => D[0].Count / d3.sum(D, d => d.Count), 
        d => d.Airline 
    ),
    colors: d3.schemeSpectral[sentiment.length],
    zDomain: sentiment
)

In the StackedBarChar function i've noticed that the variable series is becoming undefined. Here is the code that defines this which I don't fully understand.

// Compute a nested array of series where each series is [[x1, x2], [x1, x2],
// [x1, x2], …] representing the x-extent of each stacked rect. In addition,
// each tuple has an i (index) property so that we can refer back to the
// original data point (data[i]). This code assumes that there is only one
// data point for a given unique y- and z-value.
const series = d3.stack()
    .keys(zDomain)
    .value(([, I], z) => X[I.get(z)])
    .order(order)
    .offset(offset)
(d3.rollup(I, ([i]) => i, i => Y[i], i => Z[i]))
    .map(s => s.map(d => Object.assign(d, {i: d.data[1].get(s.key)})));

Also the error message is

   Uncaught TypeError: svg.append(...).selectAll(...).data(...).join is not a 
   function
   at StackedBarChart (chart.js:132:8)

which I believe is caused by series being undefined.

What could be causing this? could the format of my data must be wrong somehow?

CodePudding user response:

I am not able to reproduce your issue. The following code successfully draws the normalized stacked bar chart using the twitter airline dataset that you linked. What version of D3 are you using? Perhaps you are using an older version that does not have the selection.join function.

<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <script src="https://d3js.org/d3.v7.js"></script>
</head>

<body>
  <div id="chart"></div>

  <script>
    d3.csv('Tweets.csv').then(drawChart);

    function drawChart(data){
      const processed = [];

      for (object of data) {
        if (processed.length === 0){
          processed.push({Airline: object.airline, Sentiment: object.airline_sentiment, Count: 1})
        } else {
          objIndex = processed.findIndex((obj => obj.Airline === object.airline && obj.Sentiment === object.airline_sentiment))

          if (objIndex === -1){
            processed.push({Airline: object.airline, Sentiment: object.airline_sentiment, Count: 1})
          } else {
            processed[objIndex].Count  = 1
          }
        }
      }

      const sentiment = ['positive', 'neutral', 'negative'];

      const yDomain = d3.groupSort(
        processed,
        D => D[0].Count / d3.sum(D, d => d.Count),
        d => d.Airline
      );

      const sbc = StackedBarChart(processed, {
        x: d => d.Count,
        y: d => d.Airline,
        z: d => d.Sentiment,
        yDomain: yDomain,
        colors: d3.schemeSpectral[sentiment.length],
        zDomain: sentiment
      });

      const div = document.getElementById('chart');
      div.append(sbc);
    }

    // Copyright 2021 Observable, Inc.
    // Released under the ISC license.
    // https://observablehq.com/@d3/stacked-normalized-horizontal-bar
    function StackedBarChart(data, {
      x = d => d, // given d in data, returns the (quantitative) x-value
      y = (d, i) => i, // given d in data, returns the (ordinal) y-value
      z = () => true, // given d in data, returns the (categorical) z-value
      title, // given d in data, returns the title text
      marginTop = 30, // top margin, in pixels
      marginRight = 20, // right margin, in pixels
      marginBottom = 0, // bottom margin, in pixels
      marginLeft = 40, // left margin, in pixels
      width = 640, // outer width, in pixels
      height, // outer height, in pixels
      xType = d3.scaleLinear, // type of x-scale
      xDomain, // [xmin, xmax]
      xRange = [marginLeft, width - marginRight], // [left, right]
      yDomain, // array of y-values
      yRange, // [bottom, top]
      yPadding = 0.1, // amount of y-range to reserve to separate bars
      zDomain, // array of z-values
      offset = d3.stackOffsetExpand, // stack offset method
      order = d3.stackOrderNone, // stack order method
      xFormat = "%", // a format specifier string for the x-axis
      xLabel, // a label for the x-axis
      colors = d3.schemeTableau10, // array of colors
    } = {}) {
      // Compute values.
      const X = d3.map(data, x);
      const Y = d3.map(data, y);
      const Z = d3.map(data, z);

      // Compute default y- and z-domains, and unique them.
      if (yDomain === undefined) yDomain = Y;
      if (zDomain === undefined) zDomain = Z;
      yDomain = new d3.InternSet(yDomain);
      zDomain = new d3.InternSet(zDomain);

      // Omit any data not present in the y- and z-domains.
      const I = d3.range(X.length).filter(i => yDomain.has(Y[i]) && zDomain.has(Z[i]));

      // If the height is not specified, derive it from the y-domain.
      if (height === undefined) height = yDomain.size * 25   marginTop   marginBottom;
      if (yRange === undefined) yRange = [height - marginBottom, marginTop];

      // Compute a nested array of series where each series is [[x1, x2], [x1, x2],
      // [x1, x2], …] representing the x-extent of each stacked rect. In addition,
      // each tuple has an i (index) property so that we can refer back to the
      // original data point (data[i]). This code assumes that there is only one
      // data point for a given unique y- and z-value.
      const series = d3.stack()
          .keys(zDomain)
          .value(([, I], z) => X[I.get(z)])
          .order(order)
          .offset(offset)
        (d3.rollup(I, ([i]) => i, i => Y[i], i => Z[i]))
        .map(s => s.map(d => Object.assign(d, {i: d.data[1].get(s.key)})));

      // Compute the default y-domain. Note: diverging stacks can be negative.
      if (xDomain === undefined) xDomain = d3.extent(series.flat(2));

      // Construct scales, axes, and formats.
      const xScale = xType(xDomain, xRange);
      const yScale = d3.scaleBand(yDomain, yRange).paddingInner(yPadding);
      const color = d3.scaleOrdinal(zDomain, colors);
      const xAxis = d3.axisTop(xScale).ticks(width / 80, xFormat);
      const yAxis = d3.axisLeft(yScale).tickSizeOuter(0);

      // Compute titles.
      if (title === undefined) {
        title = i => `${Y[i]}\n${Z[i]}\n${X[i].toLocaleString()}`;
      } else {
        const O = d3.map(data, d => d);
        const T = title;
        title = i => T(O[i], i, data);
      }

      const svg = d3.create("svg")
          .attr("width", width)
          .attr("height", height)
          .attr("viewBox", [0, 0, width, height])
          .attr("style", "max-width: 100%; height: auto; height: intrinsic;");

      const bar = svg.append("g")
        .selectAll("g")
        .data(series)
        .join("g")
          .attr("fill", ([{i}]) => color(Z[i]))
        .selectAll("rect")
        .data(d => d)
        .join("rect")
          .attr("x", ([x1, x2]) => Math.min(xScale(x1), xScale(x2)))
          .attr("y", ({i}) => yScale(Y[i]))
          .attr("width", ([x1, x2]) => Math.abs(xScale(x1) - xScale(x2)))
          .attr("height", yScale.bandwidth());

      if (title) bar.append("title")
          .text(({i}) => title(i));

      svg.append("g")
          .attr("transform", `translate(0,${marginTop})`)
          .call(xAxis)
          .call(g => g.select(".domain").remove())
          .call(g => g.append("text")
              .attr("x", width - marginRight)
              .attr("y", -22)
              .attr("fill", "currentColor")
              .attr("text-anchor", "end")
              .text(xLabel));

      svg.append("g")
          .attr("transform", `translate(${xScale(0)},0)`)
          .call(yAxis);

      return Object.assign(svg.node(), {scales: {color}});
    }
  </script>
</body>

</html>
  • Related