Skip to content

Commit

Permalink
[Demo] Add diverging stacked bar chart to visual-vocabulary (#795)
Browse files Browse the repository at this point in the history
Co-authored-by: Li Nguyen <[email protected]>
  • Loading branch information
hxe00570 and huong-li-nguyen authored Oct 17, 2024
1 parent f2a52c5 commit 916cc7d
Show file tree
Hide file tree
Showing 7 changed files with 352 additions and 65 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<!--
A new scriv changelog fragment.
Uncomment the section that is right (remove the HTML comment wrapper).
-->

<!--
### Highlights ✨
- A bullet item for the Highlights ✨ category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX. ([#1](https://github.com/mckinsey/vizro/pull/1))
-->
<!--
### Removed
- A bullet item for the Removed category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX. ([#1](https://github.com/mckinsey/vizro/pull/1))
-->
<!--
### Added
- A bullet item for the Added category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX. ([#1](https://github.com/mckinsey/vizro/pull/1))
-->
<!--
### Changed
- A bullet item for the Changed category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX. ([#1](https://github.com/mckinsey/vizro/pull/1))
-->
<!--
### Deprecated
- A bullet item for the Deprecated category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX. ([#1](https://github.com/mckinsey/vizro/pull/1))
-->
<!--
### Fixed
- A bullet item for the Fixed category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX. ([#1](https://github.com/mckinsey/vizro/pull/1))
-->
<!--
### Security
- A bullet item for the Security category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX. ([#1](https://github.com/mckinsey/vizro/pull/1))
-->
120 changes: 60 additions & 60 deletions vizro-core/examples/visual-vocabulary/README.md

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion vizro-core/examples/visual-vocabulary/chart_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ class ChartGroup:
name="Deviation",
pages=pages.deviation.pages,
incomplete_pages=[
IncompletePage("Diverging stacked bar"),
IncompletePage(title="Surplus deficit filled line"),
],
icon="Contrast Square",
Expand Down
69 changes: 69 additions & 0 deletions vizro-core/examples/visual-vocabulary/custom_charts.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Contains custom charts used inside the dashboard."""

from typing import Optional

import pandas as pd
import vizro.plotly.express as px
from plotly import graph_objects as go
Expand Down Expand Up @@ -231,3 +233,70 @@ def dumbbell(data_frame: pd.DataFrame, x: str, y: str, color: str) -> go.Figure:
# Increase size of dots
fig.update_traces(marker_size=12)
return fig


@capture("graph")
def diverging_stacked_bar(
data_frame,
y: str,
category_pos: list[str],
category_neg: list[str],
color_discrete_map: Optional[dict[str, str]] = None,
) -> go.Figure:
"""Creates a horizontal diverging stacked bar chart (with positive and negative values only) using Plotly's go.Bar.
This type of chart is a variant of the standard stacked bar chart, with bars aligned on a central baseline to
show both positive and negative values. Each bar is segmented to represent different categories.
This function is not suitable for diverging stacked bar charts that include a neutral category.
Inspired by: https://community.plotly.com/t/need-help-in-making-diverging-stacked-bar-charts/34023
Args:
data_frame (pd.DataFrame): The data frame for the chart.
y (str): The name of the categorical column in the data frame to be used for the y-axis (categories)
category_pos (list[str]): List of column names in the data frame representing positive values. Columns should be
ordered from least to most positive.
category_neg (list[str]): List of column names in the DataFrame representing negative values. Columns should be
ordered from least to most negative.
color_discrete_map: Optional[dict[str, str]]: A dictionary mapping category names to color strings.
Returns:
go.Figure: A Plotly Figure object representing the horizontal diverging stacked bar chart.
"""
fig = go.Figure()

# Add traces for negative categories
for column in category_neg:
fig.add_trace(
go.Bar(
x=-data_frame[column].to_numpy(),
y=data_frame[y],
orientation="h",
name=column,
marker_color=color_discrete_map.get(column, None) if color_discrete_map else None,
)
)

# Add traces for positive categories
for column in category_pos:
fig.add_trace(
go.Bar(
x=data_frame[column],
y=data_frame[y],
orientation="h",
name=column,
marker_color=color_discrete_map.get(column, None) if color_discrete_map else None,
)
)

# Update layout and add central baseline
fig.update_layout(barmode="relative")
fig.add_vline(x=0, line_width=2, line_color="grey")

# Update legend order to go from most negative to most positive
category_order = category_neg[::-1] + category_pos
for i, category in enumerate(category_order):
fig.update_traces(legendrank=i, selector=({"name": category}))

return fig
7 changes: 4 additions & 3 deletions vizro-core/examples/visual-vocabulary/pages/_pages_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ def make_code_clipboard_from_py_file(filepath: str):
stocks = px.data.stocks()
tips = px.data.tips()
wind = px.data.wind()

ages = pd.DataFrame(
{
"Age": ["0-19", "20-29", "30-39", "40-49", "50-59", ">=60"],
Expand Down Expand Up @@ -76,7 +75,6 @@ def make_code_clipboard_from_py_file(filepath: str):
}
)


pastries = pd.DataFrame(
{
"pastry": [
Expand All @@ -94,10 +92,13 @@ def make_code_clipboard_from_py_file(filepath: str):
"Pies",
],
"Profit Ratio": [-0.10, -0.15, -0.05, 0.10, 0.05, 0.20, 0.15, -0.08, 0.08, -0.12, 0.02, -0.07],
"Strongly Disagree": [20, 30, 10, 5, 15, 5, 10, 25, 8, 20, 5, 10],
"Disagree": [30, 25, 20, 10, 20, 10, 15, 30, 12, 30, 10, 15],
"Agree": [30, 25, 40, 40, 45, 40, 40, 25, 40, 30, 45, 35],
"Strongly Agree": [20, 20, 30, 45, 20, 45, 35, 20, 40, 20, 40, 40],
}
)


salaries = pd.DataFrame(
{
"Job": ["Developer", "Analyst", "Manager", "Specialist"] * 2,
Expand Down
49 changes: 48 additions & 1 deletion vizro-core/examples/visual-vocabulary/pages/deviation.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import plotly.io as pio
import vizro.models as vm
import vizro.plotly.express as px
from custom_charts import diverging_stacked_bar

from pages._factories import butterfly_factory
from pages._pages_utils import PAGE_GRID, make_code_clipboard_from_py_file, pastries
Expand Down Expand Up @@ -51,4 +52,50 @@
],
)

pages = [butterfly, diverging_bar]
diverging_stacked_bar = vm.Page(
title="Diverging stacked bar",
path="deviation/diverging-stacked-bar",
layout=vm.Layout(grid=PAGE_GRID),
components=[
vm.Card(
text="""
#### What is a diverging stacked bar?
A diverging stacked bar chart is like a stacked bar chart but aligns bars on a central baseline instead of
the left or right. It displays positive and negative values, with each bar divided into segments for
different categories. This type of chart is commonly used for percentage shares, especially in survey
results using Likert scales (e.g., Strongly Disagree, Disagree, Agree, Strongly Agree).
&nbsp;
#### When should I use it?
A diverging stacked bar chart is useful for comparing positive and negative values and showing the
composition of each bar. However, use this chart with caution: since none of the segments share a
common baseline, direct comparisons can be more challenging. For clearer comparisons, consider using a
100% stacked bar chart with a baseline starting from the left or right. For more insights on the potential
pitfalls, we recommend reading the article from
[Datawrapper on diverging stacked bar charts](https://blog.datawrapper.de/divergingbars/).
"""
),
vm.Graph(
title="Would you recommend the pastry to your friends?",
figure=diverging_stacked_bar(
data_frame=pastries,
y="pastry",
category_pos=["Agree", "Strongly Agree"],
category_neg=["Disagree", "Strongly Disagree"],
color_discrete_map={
"Strongly Agree": "#1a85ff",
"Agree": "#70a1ff",
"Disagree": "#ff5584",
"Strongly Disagree": "#d41159",
},
),
),
make_code_clipboard_from_py_file("diverging_stacked_bar.py"),
],
)

pages = [butterfly, diverging_bar, diverging_stacked_bar]
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
from typing import Optional

import pandas as pd
import plotly.graph_objects as go
import vizro.models as vm
from vizro import Vizro
from vizro.models.types import capture

pastries = pd.DataFrame(
{
"pastry": [
"Scones",
"Bagels",
"Muffins",
"Cakes",
"Donuts",
"Cookies",
"Croissants",
"Eclairs",
"Brownies",
"Tarts",
"Macarons",
"Pies",
],
"Profit Ratio": [-0.10, -0.15, -0.05, 0.10, 0.05, 0.20, 0.15, -0.08, 0.08, -0.12, 0.02, -0.07],
"Strongly Disagree": [20, 30, 10, 5, 15, 5, 10, 25, 8, 20, 5, 10],
"Disagree": [30, 25, 20, 10, 20, 10, 15, 30, 12, 30, 10, 15],
"Agree": [30, 25, 40, 40, 45, 40, 40, 25, 40, 30, 45, 35],
"Strongly Agree": [20, 20, 30, 45, 20, 45, 35, 20, 40, 20, 40, 40],
}
)


@capture("graph")
def diverging_stacked_bar(
data_frame,
y: str,
category_pos: list[str],
category_neg: list[str],
color_discrete_map: Optional[dict[str, str]] = None,
) -> go.Figure:
"""Creates a horizontal diverging stacked bar chart (with positive and negative values only) using Plotly's go.Bar.
This type of chart is a variant of the standard stacked bar chart, with bars aligned on a central baseline to
show both positive and negative values. Each bar is segmented to represent different categories.
This function is not suitable for diverging stacked bar charts that include a neutral category.
Inspired by: https://community.plotly.com/t/need-help-in-making-diverging-stacked-bar-charts/34023
Args:
data_frame (pd.DataFrame): The data frame for the chart.
y (str): The name of the categorical column in the data frame to be used for the y-axis (categories)
category_pos (list[str]): List of column names in the data frame representing positive values. Columns should be
ordered from least to most positive.
category_neg (list[str]): List of column names in the DataFrame representing negative values. Columns should be
ordered from least to most negative.
color_discrete_map: Optional[dict[str, str]]: A dictionary mapping category names to color strings.
Returns:
go.Figure: A Plotly Figure object representing the horizontal diverging stacked bar chart.
"""
fig = go.Figure()

# Add traces for negative categories
for column in category_neg:
fig.add_trace(
go.Bar(
x=-data_frame[column].to_numpy(),
y=data_frame[y],
orientation="h",
name=column,
marker_color=color_discrete_map.get(column, None) if color_discrete_map else None,
)
)

# Add traces for positive categories
for column in category_pos:
fig.add_trace(
go.Bar(
x=data_frame[column],
y=data_frame[y],
orientation="h",
name=column,
marker_color=color_discrete_map.get(column, None) if color_discrete_map else None,
)
)

# Update layout and add central baseline
fig.update_layout(barmode="relative")
fig.add_vline(x=0, line_width=2, line_color="grey")

# Update legend order to go from most negative to most positive
category_order = category_neg[::-1] + category_pos
for i, category in enumerate(category_order):
fig.update_traces(legendrank=i, selector=({"name": category}))

return fig


page = vm.Page(
title="Diverging stacked bar",
components=[
vm.Graph(
title="Would you recommend the pastry to your friends?",
figure=diverging_stacked_bar(
data_frame=pastries,
y="pastry",
category_pos=["Agree", "Strongly Agree"],
category_neg=["Disagree", "Strongly Disagree"],
color_discrete_map={
"Strongly Agree": "#1a85ff",
"Agree": "#70a1ff",
"Disagree": "#ff5584",
"Strongly Disagree": "#d41159",
},
),
),
],
)

dashboard = vm.Dashboard(pages=[page])
Vizro().build(dashboard).run()

0 comments on commit 916cc7d

Please sign in to comment.