javascriptsvgd3.jsdata-analysisobservablehq

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


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?


Solution

  • 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>