Skip to content

Diverging Stacked Bar Charts

This example demonstrates how to create stacked bar charts for Likert-scale survey data using vertical bars. The original Observable Plot example (https://observablehq.com/@observablehq/plot-diverging-stacked-bar) uses horizontal bars with custom offset functions.

Note: These examples use vertical stacked bars (type: bar with stack: y) because Observable Plot's horizontal bar stacking (barX) requires different data structures that don't work well with pre-aggregated count data. For survey visualizations, vertical stacking provides equivalent functionality and follows the proven pattern from DataGlass's working examples.

Limitations and Workarounds

Features from the original Observable Plot example NOT currently supported in DataGlass:

  1. Custom offset functions - The original uses a custom "Likert" offset that centers neutral responses around the midpoint. DataGlass only supports built-in offsets: expand, center, normalize, wiggle, silhouette.

  2. groupZ transform with custom reducers - The original uses Plot.groupZ({x: "count"}, {...}) to automatically aggregate and count responses. DataGlass requires pre-aggregated data with explicit counts.

  3. Custom tick formatters - The original uses x: {tickFormat: Math.abs} to show absolute values on both sides of zero. DataGlass uses standard tick formatting.

Workarounds available in DataGlass:

  • Pre-aggregate your data to count responses per category
  • Use offset: "center" or offset: "normalize" for symmetric/proportion effects
  • Use offset: "expand" for 100% normalized stacks (same as normalize)
  • Use faceting (fy:) to show multiple survey questions as small multiples
  • Apply diverging color scales with manual color ranges to emphasize positive vs negative sentiment

Important Note: Observable Plot's barX automatically stacks bars horizontally when you provide a fill channel that creates groups. The offset parameter controls how the stacks are positioned.

Basic Survey Data Structure

For Likert-scale surveys, data should have:

  • A question identifier (e.g., "Q1", "Q2")
  • A response category ("Strongly Disagree", "Disagree", "Neutral", "Agree", "Strongly Agree")
  • A count of responses in each category

Example 1: Simple Vertical Stacked Bar (Pre-aggregated Data)

This shows a single survey question with response counts stacked vertically.

View Source
data:
  source: [
    {"question": "Q1", "response": "Strongly Disagree", "count": 12},
    {"question": "Q1", "response": "Disagree", "count": 18},
    {"question": "Q1", "response": "Neutral", "count": 15},
    {"question": "Q1", "response": "Agree", "count": 32},
    {"question": "Q1", "response": "Strongly Agree", "count": 23}
  ]
engine: plot
title: Employee Satisfaction Survey (Q1)
width: 800
height: 500
marks:
  - type: bar
    configuration:
      x: question
      y: count
      fill: response
      stack: y
      tip: true
scales:
  color:
    type: ordinal
    domain: ["Strongly Disagree", "Disagree", "Neutral", "Agree", "Strongly Agree"]
    range: ["#d73027", "#fc8d59", "#fee090", "#91bfdb", "#4575b4"]
marginLeft: 80
marginRight: 50

Example 2: Normalized Vertical Stacked Bar (100%)

Shows proportions to compare response distributions.

View Source
data:
  source: [
    {"question": "Q1: Job Satisfaction", "response": "Strongly Disagree", "count": 12},
    {"question": "Q1: Job Satisfaction", "response": "Disagree", "count": 18},
    {"question": "Q1: Job Satisfaction", "response": "Neutral", "count": 15},
    {"question": "Q1: Job Satisfaction", "response": "Agree", "count": 32},
    {"question": "Q1: Job Satisfaction", "response": "Strongly Agree", "count": 23},
    {"question": "Q2: Work-Life Balance", "response": "Strongly Disagree", "count": 8},
    {"question": "Q2: Work-Life Balance", "response": "Disagree", "count": 22},
    {"question": "Q2: Work-Life Balance", "response": "Neutral", "count": 20},
    {"question": "Q2: Work-Life Balance", "response": "Agree", "count": 28},
    {"question": "Q2: Work-Life Balance", "response": "Strongly Agree", "count": 22},
    {"question": "Q3: Career Growth", "response": "Strongly Disagree", "count": 15},
    {"question": "Q3: Career Growth", "response": "Disagree", "count": 25},
    {"question": "Q3: Career Growth", "response": "Neutral", "count": 18},
    {"question": "Q3: Career Growth", "response": "Agree", "count": 24},
    {"question": "Q3: Career Growth", "response": "Strongly Agree", "count": 18}
  ]

engine: plot
title: Employee Survey Responses (Normalized)
width: 800
height: 500
marks:
  - type: bar
    configuration:
      x: question
      y: count
      fill: response
      stack: y
      offset: normalize
      tip: true
scales:
  color:
    type: ordinal
    domain: ["Strongly Disagree", "Disagree", "Neutral", "Agree", "Strongly Agree"]
    range: ["#d73027", "#fc8d59", "#fee090", "#91bfdb", "#4575b4"]
  y:
    label: Proportion of Responses
marginLeft: 180
marginRight: 50
marginBottom: 80

Example 3: Faceted Survey Questions

Shows multiple questions using small multiples with chart-level faceting.

View Source
engine: plot
title: Employee Engagement Survey Results
width: 800
height: 750
facet:
  y: question
scales:
  color:
    type: ordinal
    domain: ["Strongly Disagree", "Disagree", "Neutral", "Agree", "Strongly Agree"]
    range: ["#d73027", "#fc8d59", "#fee090", "#91bfdb", "#4575b4"]
    legend: true
  x:
    label: Number of Responses
marks:
  - type: barX
    configuration:
      x: count
      y: response
      fill: response
      tip: true
      offset: normalize
marginLeft: 50
marginRight: 50
data:
  source: [
    {"question": "Job Satisfaction", "response": "Strongly Disagree", "count": 12},
    {"question": "Job Satisfaction", "response": "Disagree", "count": 18},
    {"question": "Job Satisfaction", "response": "Neutral", "count": 15},
    {"question": "Job Satisfaction", "response": "Agree", "count": 32},
    {"question": "Job Satisfaction", "response": "Strongly Agree", "count": 23},
    {"question": "Work-Life Balance", "response": "Strongly Disagree", "count": 8},
    {"question": "Work-Life Balance", "response": "Disagree", "count": 22},
    {"question": "Work-Life Balance", "response": "Neutral", "count": 20},
    {"question": "Work-Life Balance", "response": "Agree", "count": 28},
    {"question": "Work-Life Balance", "response": "Strongly Agree", "count": 22},
    {"question": "Management Quality", "response": "Strongly Disagree", "count": 10},
    {"question": "Management Quality", "response": "Disagree", "count": 15},
    {"question": "Management Quality", "response": "Neutral", "count": 22},
    {"question": "Management Quality", "response": "Agree", "count": 30},
    {"question": "Management Quality", "response": "Strongly Agree", "count": 23},
    {"question": "Career Growth", "response": "Strongly Disagree", "count": 15},
    {"question": "Career Growth", "response": "Disagree", "count": 25},
    {"question": "Career Growth", "response": "Neutral", "count": 18},
    {"question": "Career Growth", "response": "Agree", "count": 24},
    {"question": "Career Growth", "response": "Strongly Agree", "count": 18},
    {"question": "Company Culture", "response": "Strongly Disagree", "count": 6},
    {"question": "Company Culture", "response": "Disagree", "count": 12},
    {"question": "Company Culture", "response": "Neutral", "count": 16},
    {"question": "Company Culture", "response": "Agree", "count": 35},
    {"question": "Company Culture", "response": "Strongly Agree", "count": 31}
  ]

Example 4: Customer Satisfaction Vertical Bars

This example uses vertical stacked bars with diverging colors to emphasize positive vs negative sentiment.

View Source
data:
  source: [
    {"category": "Product Quality", "response": "Very Dissatisfied", "count": 8},
    {"category": "Product Quality", "response": "Dissatisfied", "count": 12},
    {"category": "Product Quality", "response": "Neutral", "count": 15},
    {"category": "Product Quality", "response": "Satisfied", "count": 42},
    {"category": "Product Quality", "response": "Very Satisfied", "count": 23},
    {"category": "Customer Service", "response": "Very Dissatisfied", "count": 15},
    {"category": "Customer Service", "response": "Dissatisfied", "count": 20},
    {"category": "Customer Service", "response": "Neutral", "count": 18},
    {"category": "Customer Service", "response": "Satisfied", "count": 28},
    {"category": "Customer Service", "response": "Very Satisfied", "count": 19},
    {"category": "Value for Money", "response": "Very Dissatisfied", "count": 12},
    {"category": "Value for Money", "response": "Dissatisfied", "count": 18},
    {"category": "Value for Money", "response": "Neutral", "count": 22},
    {"category": "Value for Money", "response": "Satisfied", "count": 32},
    {"category": "Value for Money", "response": "Very Satisfied", "count": 16},
    {"category": "Delivery Speed", "response": "Very Dissatisfied", "count": 5},
    {"category": "Delivery Speed", "response": "Dissatisfied", "count": 10},
    {"category": "Delivery Speed", "response": "Neutral", "count": 12},
    {"category": "Delivery Speed", "response": "Satisfied", "count": 38},
    {"category": "Delivery Speed", "response": "Very Satisfied", "count": 35}
  ]
engine: plot
title: Customer Satisfaction Survey
width: 800
height: 500
marks:
  - type: bar
    configuration:
      x: category
      y: count
      fill: response
      stack: y
      offset: normalize
      tip: true
scales:
  color:
    type: ordinal
    domain: ["Very Dissatisfied", "Dissatisfied", "Neutral", "Satisfied", "Very Satisfied"]
    range: ["#ca0020", "#f4a582", "#f7f7f7", "#92c5de", "#0571b0"]
    legend: true
  y:
    label: Response Distribution
marginLeft: 150
marginRight: 50
marginBottom: 80

Example 5: Product Feedback Vertical Bars with Centered Offset

Using vertical stacked bars with the center offset to create a more symmetric visualization.

View Source
data:
  source: [
    {"feature": "Ease of Use", "rating": "Poor", "count": 5},
    {"feature": "Ease of Use", "rating": "Below Average", "count": 8},
    {"feature": "Ease of Use", "rating": "Average", "count": 12},
    {"feature": "Ease of Use", "rating": "Good", "count": 35},
    {"feature": "Ease of Use", "rating": "Excellent", "count": 40},
    {"feature": "Performance", "rating": "Poor", "count": 12},
    {"feature": "Performance", "rating": "Below Average", "count": 15},
    {"feature": "Performance", "rating": "Average", "count": 18},
    {"feature": "Performance", "rating": "Good", "count": 30},
    {"feature": "Performance", "rating": "Excellent", "count": 25},
    {"feature": "Design", "rating": "Poor", "count": 3},
    {"feature": "Design", "rating": "Below Average", "count": 7},
    {"feature": "Design", "rating": "Average", "count": 15},
    {"feature": "Design", "rating": "Good", "count": 38},
    {"feature": "Design", "rating": "Excellent", "count": 37},
    {"feature": "Documentation", "rating": "Poor", "count": 18},
    {"feature": "Documentation", "rating": "Below Average", "count": 22},
    {"feature": "Documentation", "rating": "Average", "count": 20},
    {"feature": "Documentation", "rating": "Good", "count": 25},
    {"feature": "Documentation", "rating": "Excellent", "count": 15}
  ]
engine: plot
title: Product Feature Ratings (Centered)
width: 800
height: 500
marks:
  - type: bar
    configuration:
      x: feature
      y: count
      fill: rating
      stack: y
      offset: center
      tip: true
scales:
  color:
    type: ordinal
    domain: ["Poor", "Below Average", "Average", "Good", "Excellent"]
    range: ["#b2182b", "#ef8a62", "#fddbc7", "#67a9cf", "#2166ac"]
    legend: true
  y:
    label: Number of Responses
marginLeft: 150
marginRight: 50
marginBottom: 100

Tips for Effective Diverging Bar Charts

  1. Pre-aggregate your data - Count responses for each category before visualization
  2. Use consistent ordering - Always maintain the same order for Likert scales (negative to positive)
  3. Choose appropriate color schemes:
    • Red-Blue for temperature/sentiment (red = negative, blue = positive)
    • Red-Green for agree/disagree (avoid if color-blind accessibility is important)
    • Neutral colors in the middle (gray, yellow, white)
  4. Consider normalization - Use offset: "normalize" when comparing questions with different response counts
  5. Use faceting for multiple questions - The fy: parameter creates small multiples for easier comparison
  6. Add reference lines - Include a rule mark at the neutral point if needed
  7. Provide context - Include the number of total responses in titles or annotations
  • See stacked-areas.md for examples of stack transforms with areas
  • See diverging-color-examples.md for diverging color scale patterns
  • See complete-mark-showcase.md for more bar chart variations

Future Enhancements

When DataGlass adds support for custom offset functions, we'll be able to implement the exact Likert-style centering from the original Observable Plot example. For now, these workarounds provide effective alternatives for visualizing survey data.

Released under the MIT License. Built by Boundary Lab.