Diverging Stacked Bar Chart in Plot

I am interested in creating a diverging stacked bar chart to visualize some survey data. I have attached a sample notebook with where I am at so far.

What I would like to do is transform each bar to be aligned such that the “Neutral” category is centered on the x-axis about zero. I am using the Robbins & Heiberger paper as a reference.

I don’t know if this is an operation that would be done in the Plot specification, or if I should transform the data first, and then plot afterward.

Each bar would need to be moved to the left by the sum of “Strongly Disagree”, “Disagree” and half of “Neutral”.

Anyways, just looking for some advice and wondering if anyone here has done something similar before.

Thanks for your help,
-Eitan

I can’t think of an easy way to center the neutral category… but you can create diverging stacked bar charts by returning a negative value (e.g., a negative count) from the group reducer. For example:

Plot.plot({
  facet: {
    data: data,
    y: "Question"
  },
  color: {
    legend: true,
    domain: scale,
    scheme: "RdBu"
  },
  marks: [
    Plot.barX(
      data,
      Plot.groupZ(
        {
          x(D) {
            switch (D[0]) {
              case "Strongly Disagree":
              case "Disagree":
                return -D.length;
              case "Agree":
              case "Strongly Agree":
              default:
                return D.length;
            }
          }
        },
        {
          x: "Response",
          fill: "Response",
          order: [
            "Disagree",
            "Strongly Disagree",
            "Neutral",
            "Agree",
            "Strongly Agree"
          ]
        }
      )
    )
  ]
})

Note that the stack order is reversed with the negative values, hence the reordered order option.

1 Like

This is a common requirement, unfortunately quite difficult to address with the current stacking options. Here’s a suggestion: Diverging Stacked Bar Chart Help / Fil / Observable ; but it’s probably worth opening an issue to make this feasible as a stacking option?

2 Likes

Nice technique, @Fil.

I’ve completely rewritten the notebook, with details for a better answer to this issue


EDIT: now working towards an even better resolution.

I love your solution to this! - I had one question/difficulty with the solution - which is that if the questions are long, they are truncated in the plot so as to be unreadable. I’d love to be to able to show the question text, and ensure the order of the plot matches the order of the questions.
I’ve spent a long time trying to make it work, so if you have any suggestion it would be really appreciated. You can see an example here: Likert Plot / 913763a397e5fecf / Observable - thank you!

Hello,

I’d suggest:

  • a larger left margin
  • a larger width for the Plot
  • a tickFormat that truncates labels that are too long:
  marginLeft: 300,
  width: 800,
  y: { tickSize: 0, tickFormat: d => d.length > 52 ? d.slice(0,48).trim().concat("…") : d },

“marginLeft” - OMG I spent so long trying to find out what that setting might be. I didn’t put it at the top level, though, I was putting margins, padding, width etc in the “y” section.
Legend, thank you!

1 Like

I wasn’t able to find a way to keep the order of the questions in the chart, but did a bodge, putting the question number at the start of the question text so that when sorted alphabetically it did the right thing. e.g. “01. blah”, “02. example”.

You might want to pass the list of questions (as an ordered array) as the domain of the y scale. (The default domain is computed from the values encountered, then sorted.)

1 Like

I feel a bit bad asking one final thing - the labels are aligned right, but is it possible to have the y tick labels aligned neatly on the left? I tried “labelAnchor”, but that didn’t seem to work - it said “invalid labelAnchor: left” I was looking at the code here: plot/axis.js at 0df32cd992322619bab5344a60afbeb1ab655dd4 · observablehq/plot · GitHub - nothing else seemed to stand out. Thank you!

A bit of a hack, but you can do this with a “right” y-axis and some negative tickPadding.

Plot.plot({
  marginLeft: 300,
  width: 900,
  x: { tickFormat: Math.abs, label: "# of answers" },
  y: {
    axis: "right",
    tickSize: 0,
    tickPadding: -860,
    tickFormat: (d) => (d.length > 52 ? d.slice(0, 48).trim().concat("…") : d)
  },
  color: { domain: likert.order, scheme: "RdBu", legend: true },
  marks: [
    Plot.barX(
      survey,
      Plot.groupY(
        { x: "count" },
        { y: "Question", fill: "Response", ...likert }
      )
    ),
    Plot.ruleX([0])
  ]
})

Or, you could use a text mark instead of an axis.

Plot.plot({
  width: 900,
  marginLeft: 0,
  x: { insetLeft: 240, tickFormat: Math.abs, label: "# of answers" },
  y: { axis: null },
  color: { domain: likert.order, scheme: "RdBu", legend: true },
  marks: [
    Plot.textY(new Set(Array.from(survey, (d) => d.Question)), {
      text: (d) => (d.length > 52 ? d.slice(0, 48).trim().concat("…") : d),
      frameAnchor: "left"
    }),
    Plot.barX(
      survey,
      Plot.groupY(
        { x: "count" },
        { y: "Question", fill: "Response", ...likert }
      )
    ),
    Plot.ruleX([0])
  ]
}) 

Both of these look great. I think I’ll probably pick the “hack” version as its the least bit of code.

Well, I can only lastly say how impressed I am with the community support. Thank you so much for your assistance!

1 Like