diff --git a/doc/python/horizontal-bar-charts.md b/doc/python/horizontal-bar-charts.md index 25aa9d4f7d..3181f5285a 100644 --- a/doc/python/horizontal-bar-charts.md +++ b/doc/python/horizontal-bar-charts.md @@ -5,10 +5,10 @@ jupyter: text_representation: extension: .md format_name: markdown - format_version: '1.1' - jupytext_version: 1.1.1 + format_version: '1.3' + jupytext_version: 1.16.4 kernelspec: - display_name: Python 3 + display_name: Python 3 (ipykernel) language: python name: python3 language_info: @@ -20,7 +20,7 @@ jupyter: name: python nbconvert_exporter: python pygments_lexer: ipython3 - version: 3.6.7 + version: 3.11.10 plotly: description: How to make horizontal bar charts in Python with Plotly. display_as: basic @@ -217,6 +217,182 @@ fig.update_layout(annotations=annotations) fig.show() ``` +### Diverging Bar (or Butterfly) Chart + +Diverging bar charts show counts of positive outcomes or sentiments to the right of zero and counts of negative outcomes to the left of zero, allowing the reader to easily spot areas of excellence and concern. This example allows the reader of the graph to infer the number of people offering a neutral response because the neutral category, which is left implicit, would make the responses add to 100%. + +```python +import plotly.graph_objects as go +import pandas as pd + + +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) + +#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() +# this color palette conveys meaning: blues for positive, red and orange for negative +color_by_category={ + "Strongly Agree":'darkblue', + "Agree":'lightblue', + "Disagree":'orange', + "Strongly Disagree":'red', +} + + +# 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, +} +# Add bars for each category +for col in ["Disagree","Strongly Disagree","Agree","Strongly Agree"]: + 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] + )) + +fig.update_layout( + title="Reactions to statements from the 2002 General Social Survey:", + yaxis_title = "", + barmode='relative', # Allows bars to diverge from the center + plot_bgcolor="white", +) + +fig.update_xaxes( + title="Percent of Responses", + zeroline=True, # Ensure there's a zero line for divergence + zerolinecolor="black", + # use array tick mode to show that the counts to the left of zero are still positive. + # this is hard coded; generalize this if you plan to create a function that takes unknown or widely varying data + tickmode = 'array', + tickvals = [-50, 0, 50, 100], + ticktext = [50, 0, 50, 100] +) + +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) + + +#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() +``` + ### Bar Chart with Line Plot ```python @@ -260,7 +436,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,