From f386c0baeaa1cf40bed3bad4158254b69de41bbf Mon Sep 17 00:00:00 2001 From: Rob Letzler <22990670+rl-utility-man@users.noreply.github.com> Date: Thu, 6 Feb 2025 00:11:12 -0500 Subject: [PATCH] butterfly with neutral column --- doc/python/horizontal-bar-charts.md | 105 +++++++++++++++++++++++++++- 1 file changed, 103 insertions(+), 2 deletions(-) diff --git a/doc/python/horizontal-bar-charts.md b/doc/python/horizontal-bar-charts.md index 25aa9d4f7d1..f68a34989d9 100644 --- a/doc/python/horizontal-bar-charts.md +++ b/doc/python/horizontal-bar-charts.md @@ -214,6 +214,107 @@ for yd, xd in zip(y_data, x_data): fig.update_layout(annotations=annotations) +fig.show() +``` +### Diverging Bar (or Butterfly) Chart with Neutral Column + +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_. + +``` +import pandas as pd +import plotly.graph_objects as go + +data = { + "Category": ["Content Quality", "Value for Money", "Ease of Use", "Customer Support", "Scale Fidelity"], + "Neutral": [10, 15, 18, 15,20], + "Somewhat Agree": [25, 25, 22, 20, 20], + "Strongly Agree": [35, 35, 25, 40, 20], + "Somewhat Disagree": [-20, -15, -20, -10, -20], + "Strongly Disagree": [-10, -10, -15, -15,-20] +} +df = pd.DataFrame(data) + +fig = go.Figure() +# this color palette conveys meaning: blues for negative, reds for positive, gray for neutral +color_by_category={ + "Strongly Agree":'darkblue', + "Somewhat Agree":'lightblue', + "Somewhat Disagree":'orange', + "Strongly Disagree":'red', + "Neutral":'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, + "Somewhat Disagree":2, + "Somewhat Agree":3, + "Strongly Agree":4, + "Neutral":5 +} + +# Add bars +for col in df[["Somewhat Disagree","Strongly Disagree","Somewhat Agree","Strongly Agree","Neutral"]]: + 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=="Neutral")}", # in this context, putting neutral 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[["Somewhat Disagree","Strongly Disagree"]].sum(axis=1)) +max_right = max(df[["Somewhat 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_neutral = max(df["Neutral"]) + +fig.update_layout( + title="Reactions to the statement, 'The service met your expectations for':", + plot_bgcolor="white", + barmode='relative', # Allows bars to diverge from the center + ) +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_neutral))] +) +fig.update_layout( + xaxis2=dict( + range=[0, max_width_neutral], + domain=[(1-.98*(1-max_width_signed/(max_width_signed+max_width_neutral))), 1.0], + ) +) +fig.update_legends( + orientation="h", # a horizontal legend matches the horizontal bars + yref="container", + yanchor="bottom", + y=0.02, + xanchor="center", + x=0.5 +) + +fig.update_yaxes(title="") + fig.show() ``` @@ -260,7 +361,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, @@ -335,4 +436,4 @@ fig.show() ### Reference -See more examples of bar charts and styling options [here](https://plotly.com/python/bar-charts/).
See https://plotly.com/python/reference/bar/ for more information and chart attribute options! \ No newline at end of file +See more examples of bar charts and styling options [here](https://plotly.com/python/bar-charts/).
See https://plotly.com/python/reference/bar/ for more information and chart attribute options!