Skip to content

Commit f386c0b

Browse files
butterfly with neutral column
1 parent fbc94a2 commit f386c0b

File tree

1 file changed

+103
-2
lines changed

1 file changed

+103
-2
lines changed

doc/python/horizontal-bar-charts.md

+103-2
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,107 @@ for yd, xd in zip(y_data, x_data):
214214

215215
fig.update_layout(annotations=annotations)
216216

217+
fig.show()
218+
```
219+
### Diverging Bar (or Butterfly) Chart with Neutral Column
220+
221+
Diverging bar charts offer two imperfect options for responses that are neither positive nor negative: omit them, leaving them implicit when the categories add to 100%, as we did above or put them in a separate column, as we do in this example. Jonathan Schwabish discusses this on page 92-97 of _Better Data Visualizations_.
222+
223+
```
224+
import pandas as pd
225+
import plotly.graph_objects as go
226+
227+
data = {
228+
"Category": ["Content Quality", "Value for Money", "Ease of Use", "Customer Support", "Scale Fidelity"],
229+
"Neutral": [10, 15, 18, 15,20],
230+
"Somewhat Agree": [25, 25, 22, 20, 20],
231+
"Strongly Agree": [35, 35, 25, 40, 20],
232+
"Somewhat Disagree": [-20, -15, -20, -10, -20],
233+
"Strongly Disagree": [-10, -10, -15, -15,-20]
234+
}
235+
df = pd.DataFrame(data)
236+
237+
fig = go.Figure()
238+
# this color palette conveys meaning: blues for negative, reds for positive, gray for neutral
239+
color_by_category={
240+
"Strongly Agree":'darkblue',
241+
"Somewhat Agree":'lightblue',
242+
"Somewhat Disagree":'orange',
243+
"Strongly Disagree":'red',
244+
"Neutral":'gray',
245+
}
246+
247+
# We want the legend to be ordered in the same order that the categories appear, left to right --
248+
# which is different from the order in which we have to add the traces to the figure.
249+
# since we need to create the "somewhat" traces before the "strongly" traces to display
250+
# the segments in the desired order
251+
252+
legend_rank_by_category={
253+
"Strongly Disagree":1,
254+
"Somewhat Disagree":2,
255+
"Somewhat Agree":3,
256+
"Strongly Agree":4,
257+
"Neutral":5
258+
}
259+
260+
# Add bars
261+
for col in df[["Somewhat Disagree","Strongly Disagree","Somewhat Agree","Strongly Agree","Neutral"]]:
262+
fig.add_trace(go.Bar(
263+
y=df["Category"],
264+
x=df[col],
265+
name=col,
266+
orientation='h',
267+
marker=dict(color=color_by_category[col]),
268+
legendrank=legend_rank_by_category[col],
269+
xaxis=f"x{1+(col=="Neutral")}", # in this context, putting neutral on a secondary x-axis on a different domain
270+
# yields results equivalent to subplots with far less code
271+
272+
273+
)
274+
)
275+
276+
# make calculations to split the plot into two columns with a shared x axis scale
277+
# by setting the domain and range of the x axes appropriately
278+
279+
# Find the maximum width of the bars to the left and right sides of the origin; remember that the width of
280+
# the plot is the sum of the longest negative bar and the longest positive bar even if they are on separate rows
281+
max_left = min(df[["Somewhat Disagree","Strongly Disagree"]].sum(axis=1))
282+
max_right = max(df[["Somewhat Agree","Strongly Agree"]].sum(axis=1))
283+
284+
# we are working in percent, but coded the negative reactions as negative numbers; so we need to take the absolute value
285+
max_width_signed = abs(max_left)+max_right
286+
max_width_neutral = max(df["Neutral"])
287+
288+
fig.update_layout(
289+
title="Reactions to the statement, 'The service met your expectations for':",
290+
plot_bgcolor="white",
291+
barmode='relative', # Allows bars to diverge from the center
292+
)
293+
fig.update_xaxes(
294+
zeroline=True, #the zero line distinguishes between positive and negative segments
295+
zerolinecolor="black",
296+
#starting here, we set domain and range to create a shared x-axis scale
297+
# multiply by .98 to add space between the two columns
298+
range=[max_left, max_right],
299+
domain=[0, 0.98*(max_width_signed/(max_width_signed+max_width_neutral))]
300+
)
301+
fig.update_layout(
302+
xaxis2=dict(
303+
range=[0, max_width_neutral],
304+
domain=[(1-.98*(1-max_width_signed/(max_width_signed+max_width_neutral))), 1.0],
305+
)
306+
)
307+
fig.update_legends(
308+
orientation="h", # a horizontal legend matches the horizontal bars
309+
yref="container",
310+
yanchor="bottom",
311+
y=0.02,
312+
xanchor="center",
313+
x=0.5
314+
)
315+
316+
fig.update_yaxes(title="")
317+
217318
fig.show()
218319
```
219320

@@ -260,7 +361,7 @@ fig.append_trace(go.Scatter(
260361
), 1, 2)
261362

262363
fig.update_layout(
263-
title='Household savings & net worth for eight OECD countries',
364+
title=dict(text='Household savings & net worth for eight OECD countries'),
264365
yaxis=dict(
265366
showgrid=False,
266367
showline=False,
@@ -335,4 +436,4 @@ fig.show()
335436

336437
### Reference
337438

338-
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!
439+
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!

0 commit comments

Comments
 (0)