Diverging Stacked Bar Chart#

This example shows a diverging stacked bar chart for sentiments towards a set of eight questions, displayed as percentages with neutral responses straddling the 0% mark.

import altair as alt
import polars as pl
import polars.selectors as cs

records = [
    {
        "question": "Question 1",
        "type": "Strongly disagree",
        "value": 24,
    },
    {
        "question": "Question 1",
        "type": "Disagree",
        "value": 294,
    },
    {
        "question": "Question 1",
        "type": "Neither agree nor disagree",
        "value": 594,
    },
    {
        "question": "Question 1",
        "type": "Agree",
        "value": 1927,
    },
    {
        "question": "Question 1",
        "type": "Strongly agree",
        "value": 376,
    },
    {
        "question": "Question 2",
        "type": "Strongly disagree",
        "value": 2,
    },
    {
        "question": "Question 2",
        "type": "Disagree",
        "value": 2,
    },
    {
        "question": "Question 2",
        "type": "Neither agree nor disagree",
        "value": 0,
    },
    {
        "question": "Question 2",
        "type": "Agree",
        "value": 7,
    },
    {
        "question": "Question 2",
        "type": "Strongly agree",
        "value": 11,
    },
    {
        "question": "Question 3",
        "type": "Strongly disagree",
        "value": 2,
    },
    {
        "question": "Question 3",
        "type": "Disagree",
        "value": 0,
    },
    {
        "question": "Question 3",
        "type": "Neither agree nor disagree",
        "value": 2,
    },
    {
        "question": "Question 3",
        "type": "Agree",
        "value": 4,
    },
    {
        "question": "Question 3",
        "type": "Strongly agree",
        "value": 2,
    },
    {
        "question": "Question 4",
        "type": "Strongly disagree",
        "value": 0,
    },
    {
        "question": "Question 4",
        "type": "Disagree",
        "value": 2,
    },
    {
        "question": "Question 4",
        "type": "Neither agree nor disagree",
        "value": 1,
    },
    {
        "question": "Question 4",
        "type": "Agree",
        "value": 7,
    },
    {
        "question": "Question 4",
        "type": "Strongly agree",
        "value": 6,
    },
    {
        "question": "Question 5",
        "type": "Strongly disagree",
        "value": 0,
    },
    {
        "question": "Question 5",
        "type": "Disagree",
        "value": 1,
    },
    {
        "question": "Question 5",
        "type": "Neither agree nor disagree",
        "value": 3,
    },
    {
        "question": "Question 5",
        "type": "Agree",
        "value": 16,
    },
    {
        "question": "Question 5",
        "type": "Strongly agree",
        "value": 4,
    },
    {
        "question": "Question 6",
        "type": "Strongly disagree",
        "value": 1,
    },
    {
        "question": "Question 6",
        "type": "Disagree",
        "value": 1,
    },
    {
        "question": "Question 6",
        "type": "Neither agree nor disagree",
        "value": 2,
    },
    {
        "question": "Question 6",
        "type": "Agree",
        "value": 9,
    },
    {
        "question": "Question 6",
        "type": "Strongly agree",
        "value": 3,
    },
    {
        "question": "Question 7",
        "type": "Strongly disagree",
        "value": 0,
    },
    {
        "question": "Question 7",
        "type": "Disagree",
        "value": 0,
    },
    {
        "question": "Question 7",
        "type": "Neither agree nor disagree",
        "value": 1,
    },
    {
        "question": "Question 7",
        "type": "Agree",
        "value": 4,
    },
    {
        "question": "Question 7",
        "type": "Strongly agree",
        "value": 0,
    },
    {
        "question": "Question 8",
        "type": "Strongly disagree",
        "value": 0,
    },
    {
        "question": "Question 8",
        "type": "Disagree",
        "value": 0,
    },
    {
        "question": "Question 8",
        "type": "Neither agree nor disagree",
        "value": 0,
    },
    {
        "question": "Question 8",
        "type": "Agree",
        "value": 0,
    },
    {
        "question": "Question 8",
        "type": "Strongly agree",
        "value": 2,
    },
]

source = pl.DataFrame(records)
type_codes = {
    "Strongly disagree": -2,
    "Disagree": -1,
    "Neither agree nor disagree": 0,
    "Agree": 1,
    "Strongly agree": 2,
}

value = pl.col("value")
perc = pl.col("percentage")
sum_disagree = perc.filter(pl.col.type.is_in(["Strongly disagree", "Disagree"])).sum()
percentage_end = perc.cum_sum() - (
    sum_disagree + perc.filter(type="Neither agree nor disagree").item() / 2
)

source = (
    pl.DataFrame(records)
    .with_columns(
        type_code=pl.col.type.replace_strict(type_codes),
        percentage=((value / value.sum()) * 100).over("question"),
    )
    .group_by("question")
    .agg(
        pl.all(),
        percentage_end=percentage_end,
        percentage_start=(percentage_end - perc),
    )
    .explode(cs.list())
    .sort("question", "type_code")
)

alt.Chart(source).mark_bar().encode(
    x=alt.X("percentage_start").title("Percentage"),
    x2="percentage_end",
    y=alt.Y("question:N").axis(
        title=None, offset=5, ticks=False, minExtent=60, domain=False
    ),
    color=alt.Color("type:N")
    .title("Response")
    .scale(
        domain=list(type_codes),
        range=["#c30d24", "#f3a583", "#cccccc", "#94c6da", "#1770ab"],
    ),
)
import altair as alt
import polars as pl
import polars.selectors as cs

records = [
    {
        "question": "Question 1",
        "type": "Strongly disagree",
        "value": 24,
    },
    {
        "question": "Question 1",
        "type": "Disagree",
        "value": 294,
    },
    {
        "question": "Question 1",
        "type": "Neither agree nor disagree",
        "value": 594,
    },
    {
        "question": "Question 1",
        "type": "Agree",
        "value": 1927,
    },
    {
        "question": "Question 1",
        "type": "Strongly agree",
        "value": 376,
    },
    {
        "question": "Question 2",
        "type": "Strongly disagree",
        "value": 2,
    },
    {
        "question": "Question 2",
        "type": "Disagree",
        "value": 2,
    },
    {
        "question": "Question 2",
        "type": "Neither agree nor disagree",
        "value": 0,
    },
    {
        "question": "Question 2",
        "type": "Agree",
        "value": 7,
    },
    {
        "question": "Question 2",
        "type": "Strongly agree",
        "value": 11,
    },
    {
        "question": "Question 3",
        "type": "Strongly disagree",
        "value": 2,
    },
    {
        "question": "Question 3",
        "type": "Disagree",
        "value": 0,
    },
    {
        "question": "Question 3",
        "type": "Neither agree nor disagree",
        "value": 2,
    },
    {
        "question": "Question 3",
        "type": "Agree",
        "value": 4,
    },
    {
        "question": "Question 3",
        "type": "Strongly agree",
        "value": 2,
    },
    {
        "question": "Question 4",
        "type": "Strongly disagree",
        "value": 0,
    },
    {
        "question": "Question 4",
        "type": "Disagree",
        "value": 2,
    },
    {
        "question": "Question 4",
        "type": "Neither agree nor disagree",
        "value": 1,
    },
    {
        "question": "Question 4",
        "type": "Agree",
        "value": 7,
    },
    {
        "question": "Question 4",
        "type": "Strongly agree",
        "value": 6,
    },
    {
        "question": "Question 5",
        "type": "Strongly disagree",
        "value": 0,
    },
    {
        "question": "Question 5",
        "type": "Disagree",
        "value": 1,
    },
    {
        "question": "Question 5",
        "type": "Neither agree nor disagree",
        "value": 3,
    },
    {
        "question": "Question 5",
        "type": "Agree",
        "value": 16,
    },
    {
        "question": "Question 5",
        "type": "Strongly agree",
        "value": 4,
    },
    {
        "question": "Question 6",
        "type": "Strongly disagree",
        "value": 1,
    },
    {
        "question": "Question 6",
        "type": "Disagree",
        "value": 1,
    },
    {
        "question": "Question 6",
        "type": "Neither agree nor disagree",
        "value": 2,
    },
    {
        "question": "Question 6",
        "type": "Agree",
        "value": 9,
    },
    {
        "question": "Question 6",
        "type": "Strongly agree",
        "value": 3,
    },
    {
        "question": "Question 7",
        "type": "Strongly disagree",
        "value": 0,
    },
    {
        "question": "Question 7",
        "type": "Disagree",
        "value": 0,
    },
    {
        "question": "Question 7",
        "type": "Neither agree nor disagree",
        "value": 1,
    },
    {
        "question": "Question 7",
        "type": "Agree",
        "value": 4,
    },
    {
        "question": "Question 7",
        "type": "Strongly agree",
        "value": 0,
    },
    {
        "question": "Question 8",
        "type": "Strongly disagree",
        "value": 0,
    },
    {
        "question": "Question 8",
        "type": "Disagree",
        "value": 0,
    },
    {
        "question": "Question 8",
        "type": "Neither agree nor disagree",
        "value": 0,
    },
    {
        "question": "Question 8",
        "type": "Agree",
        "value": 0,
    },
    {
        "question": "Question 8",
        "type": "Strongly agree",
        "value": 2,
    },
]

source = pl.DataFrame(records)
type_codes = {
    "Strongly disagree": -2,
    "Disagree": -1,
    "Neither agree nor disagree": 0,
    "Agree": 1,
    "Strongly agree": 2,
}

value = pl.col("value")
perc = pl.col("percentage")
sum_disagree = perc.filter(pl.col.type.is_in(["Strongly disagree", "Disagree"])).sum()
percentage_end = perc.cum_sum() - (
    sum_disagree + perc.filter(type="Neither agree nor disagree").item() / 2
)

source = (
    pl.DataFrame(records)
    .with_columns(
        type_code=pl.col.type.replace_strict(type_codes),
        percentage=((value / value.sum()) * 100).over("question"),
    )
    .group_by("question")
    .agg(
        pl.all(),
        percentage_end=percentage_end,
        percentage_start=(percentage_end - perc),
    )
    .explode(cs.list())
    .sort("question", "type_code")
)


color_scale = alt.Scale(
    domain=[
        "Strongly disagree",
        "Disagree",
        "Neither agree nor disagree",
        "Agree",
        "Strongly agree",
    ],
    range=["#c30d24", "#f3a583", "#cccccc", "#94c6da", "#1770ab"],
)

y_axis = alt.Axis(title=None, offset=5, ticks=False, minExtent=60, domain=False)

alt.Chart(source).mark_bar().encode(
    x=alt.X("percentage_start:Q", title='Percentage'),
    x2="percentage_end:Q",
    y=alt.Y("question:N", axis=y_axis),
    color=alt.Color("type:N", title="Response", scale=color_scale),
)