Skip to content

Example 2 for Butterfly chart (version2) #4984

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: doc-prod
Choose a base branch
from
108 changes: 106 additions & 2 deletions doc/python/horizontal-bar-charts.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,110 @@ for yd, xd in zip(y_data, x_data):

fig.update_layout(annotations=annotations)

fig.show()
```
### Diverging Bar (or Butterfly) Chart with Neutral Column

The previous diverging bar chart example excluded neutral responses. This variation includes them in a separate column. Jonathan Schwabish discusses tradeoffs between these options on page 92-97 of _Better Data Visualizations_.

```python
import pandas as pd
import plotly.graph_objects as go


df = pd.read_csv('https://raw.githubusercontent.com/plotly/datasets/refs/heads/master/gss_2002_5_pt_likert.csv')
df.rename(columns={'Unnamed: 0':"Category"}, inplace=True)
Comment on lines +228 to +229
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to rename the column here rather in the dataset? Is that just how the dataset was?

Copy link
Contributor

@rl-utility-man rl-utility-man Apr 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a shortcoming of the data set. I just proposed a PR to label the column properly in the data set plotly/datasets#64 A search of github shows no uses of that data set other than in this PR and #4994, so it appears safe to accept that PR. (I uploaded this data set recently in plotly/datasets#62 ) As soon as you merge plotly/datasets#64 , we can remove the rename commands from this and from #4994



#achieve the diverging effect by putting a negative sign on the "disagree" answers
for v in ["Disagree","Strongly Disagree"]:
df[v]=df[v]*-1

fig = go.Figure(layout=go.Layout(
title="Reactions to statements from the 2002 General Social Survey:",
plot_bgcolor="white",
barmode='relative', # Allows bars to diverge from the center
# Put the legend at the bottom center of the figure
legend=dict(
orientation="h", # a horizontal legend matches the horizontal bars
yref="container",
yanchor="bottom",
y=0.02,
xanchor="center",
x=0.5),
# use an unlabeled Y axis, since we're going to list specific questions on the y-axis.
yaxis=dict(
title=""
),
)
)


# this color palette conveys meaning: blues for agreement, reds and oranges for disagreement, gray for Neither Agree nor Disagree
color_by_category={
"Strongly Agree":'darkblue',
"Agree":'lightblue',
"Disagree":'orange',
"Strongly Disagree":'red',
"Neither Agree nor Disagree":'gray',
}


# We want the legend to be ordered in the same order that the categories appear, left to right --
# which is different from the order in which we have to add the traces to the figure.
# since we need to create the "somewhat" traces before the "strongly" traces to display
# the segments in the desired order

legend_rank_by_category={
"Strongly Disagree":1,
"Disagree":2,
"Agree":3,
"Strongly Agree":4,
"Neither Agree nor Disagree":5
}

# Add bars
for col in ["Disagree","Strongly Disagree","Agree","Strongly Agree","Neither Agree nor Disagree"]:
fig.add_trace(go.Bar(
y=df["Category"],
x=df[col],
name=col,
orientation='h',
marker=dict(color=color_by_category[col]),
legendrank=legend_rank_by_category[col],
xaxis=f"x{1+(col=='Neither Agree nor Disagree')}", # in this context, putting "Neither Agree nor Disagree" on a secondary x-axis on a different domain
# yields results equivalent to subplots with far less code
)
)

# make calculations to split the plot into two columns with a shared x axis scale
# by setting the domain and range of the x axes appropriately

# Find the maximum width of the bars to the left and right sides of the origin; remember that the width of
# the plot is the sum of the longest negative bar and the longest positive bar even if they are on separate rows
max_left = min(df[["Disagree","Strongly Disagree"]].sum(axis=1))
max_right = max(df[["Agree","Strongly Agree"]].sum(axis=1))

# we are working in percent, but coded the negative reactions as negative numbers; so we need to take the absolute value
max_width_signed = abs(max_left)+max_right
max_width_neither = max(df["Neither Agree nor Disagree"])

fig.update_xaxes(
zeroline=True, #the zero line distinguishes between positive and negative segments
zerolinecolor="black",
#starting here, we set domain and range to create a shared x-axis scale
# multiply by .98 to add space between the two columns
range=[max_left, max_right],
domain=[0, 0.98*(max_width_signed/(max_width_signed+max_width_neither))]
)

fig.update_layout(
xaxis2=dict(
range=[0, max_width_neither],
domain=[(1-.98*(1-max_width_signed/(max_width_signed+max_width_neither))), 1.0],
)
)

fig.show()
```

Expand Down Expand Up @@ -260,7 +364,7 @@ fig.append_trace(go.Scatter(
), 1, 2)

fig.update_layout(
title='Household savings & net worth for eight OECD countries',
title=dict(text='Household savings & net worth for eight OECD countries'),
yaxis=dict(
showgrid=False,
showline=False,
Expand Down Expand Up @@ -335,4 +439,4 @@ fig.show()

### Reference

See more examples of bar charts and styling options [here](https://plotly.com/python/bar-charts/).<br> See https://plotly.com/python/reference/bar/ for more information and chart attribute options!
See more examples of bar charts and styling options [here](https://plotly.com/python/bar-charts/).<br> See https://plotly.com/python/reference/bar/ for more information and chart attribute options!