Skip to content
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

[Demo] Add diverging stacked bar chart to visual-vocabulary #795

Merged
merged 19 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -51,7 +51,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: 68 additions & 1 deletion vizro-core/examples/visual-vocabulary/custom_charts.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Contains custom charts used inside the dashboard."""

from typing import List
from typing import Dict, List, Optional

import pandas as pd
import vizro.plotly.express as px
Expand Down Expand Up @@ -233,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,
huong-li-nguyen marked this conversation as resolved.
Show resolved Hide resolved
) -> 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.
huong-li-nguyen marked this conversation as resolved.
Show resolved Hide resolved

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.
petar-qb marked this conversation as resolved.
Show resolved Hide resolved
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].values,
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}))
huong-li-nguyen marked this conversation as resolved.
Show resolved Hide resolved

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
48 changes: 47 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,49 @@
],
)

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, Neutral, Agree, Strongly Agree).

huong-li-nguyen marked this conversation as resolved.
Show resolved Hide resolved
&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/).
huong-li-nguyen marked this conversation as resolved.
Show resolved Hide resolved
"""
),
vm.Graph(
figure=diverging_stacked_bar(
huong-li-nguyen marked this conversation as resolved.
Show resolved Hide resolved
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 Dict, List, 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.
"""
huong-li-nguyen marked this conversation as resolved.
Show resolved Hide resolved
fig = go.Figure()

# Add traces for negative categories
for column in category_neg:
fig.add_trace(
go.Bar(
x=-data_frame[column].values,
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(
huong-li-nguyen marked this conversation as resolved.
Show resolved Hide resolved
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()
Loading