From 916cc7d26b36d45be43e0f1b27246251d00c229c Mon Sep 17 00:00:00 2001 From: Hilary Ivy <132296554+hxe00570@users.noreply.github.com> Date: Thu, 17 Oct 2024 11:19:57 -0400 Subject: [PATCH] [Demo] Add diverging stacked bar chart to visual-vocabulary (#795) Co-authored-by: Li Nguyen <90609403+huong-li-nguyen@users.noreply.github.com> --- ...4_90609403+huong-li-nguyen_DivergingBar.md | 48 +++++++ .../examples/visual-vocabulary/README.md | 120 ++++++++--------- .../visual-vocabulary/chart_groups.py | 1 - .../visual-vocabulary/custom_charts.py | 69 ++++++++++ .../visual-vocabulary/pages/_pages_utils.py | 7 +- .../visual-vocabulary/pages/deviation.py | 49 ++++++- .../pages/examples/diverging_stacked_bar.py | 123 ++++++++++++++++++ 7 files changed, 352 insertions(+), 65 deletions(-) create mode 100644 vizro-core/changelog.d/20241016_085324_90609403+huong-li-nguyen_DivergingBar.md create mode 100644 vizro-core/examples/visual-vocabulary/pages/examples/diverging_stacked_bar.py diff --git a/vizro-core/changelog.d/20241016_085324_90609403+huong-li-nguyen_DivergingBar.md b/vizro-core/changelog.d/20241016_085324_90609403+huong-li-nguyen_DivergingBar.md new file mode 100644 index 000000000..7c0d58d4f --- /dev/null +++ b/vizro-core/changelog.d/20241016_085324_90609403+huong-li-nguyen_DivergingBar.md @@ -0,0 +1,48 @@ + + + + + + + + + diff --git a/vizro-core/examples/visual-vocabulary/README.md b/vizro-core/examples/visual-vocabulary/README.md index d170e4be1..66e43209a 100644 --- a/vizro-core/examples/visual-vocabulary/README.md +++ b/vizro-core/examples/visual-vocabulary/README.md @@ -38,66 +38,66 @@ The dashboard is still in development. Below is an overview of the chart types f Sure, here's the table with all columns aligned for better readability: -| Chart Type | Status | Category | Credits & sources | API | -| --------------------- | ------ | ------------------------ | ------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Arc | ❌ | Part-to-whole | | | -| Area | ✅ | Time | [Filled area plot with px](https://plotly.com/python/filled-area-plots/) | [px.area](https://plotly.com/python-api-reference/generated/plotly.express.area) | -| Bar | ✅ | Magnitude | [Bar chart with px](https://plotly.com/python/bar-charts/) | [px.bar](https://plotly.com/python-api-reference/generated/plotly.express.bar) | -| Barcode | ❌ | Distribution | | | -| Beeswarm | ❌ | Distribution | | | -| Boxplot | ✅ | Distribution | [Box plot with px](https://plotly.com/python/box-plots/) | [px.box](https://plotly.github.io/plotly.py-docs/generated/plotly.express.box) | -| Bubble | ✅ | Correlation | [Scatter plot with px](https://plotly.com/python/line-and-scatter/) | [px.scatter](https://plotly.com/python-api-reference/generated/plotly.express.scatter) | -| Bubble map | ✅ | Spatial | [Bubble map in px](https://plotly.com/python/bubble-maps/) | [px.scatter_map](https://plotly.github.io/plotly.py-docs/generated/plotly.express.scatter_map) | -| Bubble timeline | ❌ | Time | | | -| Bullet | ❌ | Magnitude | | | -| Bump | ❌ | Ranking | | | -| Butterfly | ✅ | Deviation, Distribution | [Pyramid charts in Plotly](https://plotly.com/python/v3/population-pyramid-charts/) | [go.Bar](https://plotly.com/python-api-reference/generated/plotly.graph_objects.Bar.html) | -| Chord | ❌ | Flow | | | -| Choropleth | ✅ | Spatial | [Choropleth map with px](https://plotly.com/python/choropleth-maps/) | [px.choropleth](https://plotly.github.io/plotly.py-docs/generated/plotly.express.choropleth.html) | -| Column | ✅ | Magnitude, Time | [Bar chart with px](https://plotly.com/python/bar-charts/) | [px.bar](https://plotly.com/python-api-reference/generated/plotly.express.bar.html) | -| Column and line | ✅ | Correlation, Time | [Multiple chart types in Plotly](https://plotly.com/python/graphing-multiple-chart-types/) | [go.Bar](https://plotly.com/python-api-reference/generated/plotly.graph_objects.Bar.html) and [go.Scatter](https://plotly.com/python-api-reference/generated/plotly.graph_objects.Scatter.html) | -| Connected scatter | ✅ | Correlation, Time | [Line plot with px](https://plotly.com/python/line-charts/) | [px.line](https://plotly.com/python-api-reference/generated/plotly.express.line) | -| Cumulative curve | ❌ | Distribution | | | -| Diverging bar | ✅ | Deviation | [Bar chart with px](https://plotly.com/python/bar-charts/) | [px.bar](https://plotly.com/python-api-reference/generated/plotly.express.bar) | -| Diverging stacked bar | ❌ | Deviation | | | -| Donut | ✅ | Part-to-whole | [Pie chart with px](https://plotly.com/python/pie-charts/) | [px.pie](https://plotly.com/python-api-reference/generated/plotly.express.pie) | -| Dot map | ✅ | Spatial | [Bubble map in px](https://plotly.com/python/bubble-maps/) | [px.scatter_map](https://plotly.github.io/plotly.py-docs/generated/plotly.express.scatter_map) | -| Dumbbell | ✅ | Distribution | [Dumbbell plots in Plotly](https://community.plotly.com/t/how-to-make-dumbbell-plots-in-plotly-python/47762) | [px.scatter](https://plotly.com/python-api-reference/generated/plotly.express.scatter.html) and [add_shape](https://plotly.com/python/shapes/) | -| Fan | ❌ | Time | | | -| Flow map | ❌ | Spatial | | | -| Funnel | ✅ | Part-to-whole | [Funnel plot with px](https://plotly.com/python/funnel-charts/) | [px.funnel](https://plotly.com/python/funnel-charts/) | -| Gantt | ✅ | Time | [Gantt chart with px](https://plotly.com/python/gantt/) | [px.timeline](https://plotly.com/python-api-reference/generated/plotly.express.timeline.html) | -| Gridplot | ❌ | Part-to-whole | | | -| Heatmap | ✅ | Time | [Heatmaps with px](https://plotly.com/python/heatmaps/) | [px.density_heatmap](https://plotly.com/python-api-reference/generated/plotly.express.density_heatmap.html) | -| Correlation matrix | ❌ | Correlation | | | -| Histogram | ✅ | Distribution | [Histograms with px](https://plotly.com/python/histograms/) | [px.histogram](https://plotly.github.io/plotly.py-docs/generated/plotly.express.histogram) | -| Line | ✅ | Time | [Line plot with px](https://plotly.com/python/line-charts/) | [px.line](https://plotly.com/python-api-reference/generated/plotly.express.line) | -| Lollipop | ❌ | Ranking, Magnitude | | | -| Marimekko | ❌ | Magnitude, Part-to-whole | | | -| Network | ❌ | Flow | | | -| Ordered bar | ✅ | Ranking | [Bar chart with px](https://plotly.com/python/bar-charts/) | [px.bar](https://plotly.com/python-api-reference/generated/plotly.express.bar.html) | -| Ordered bubble | ❌ | Ranking | | | -| Ordered column | ✅ | Ranking | [Bar chart with px](https://plotly.com/python/bar-charts/) | [px.bar](https://plotly.com/python-api-reference/generated/plotly.express.bar.html) | -| Paired bar | ✅ | Magnitude | [Histograms with px](https://plotly.com/python/histograms/) | [px.histogram](https://plotly.github.io/plotly.py-docs/generated/plotly.express.histogram) | -| Paired column | ✅ | Magnitude | [Histograms with px](https://plotly.com/python/histograms/) | [px.histogram](https://plotly.github.io/plotly.py-docs/generated/plotly.express.histogram) | -| Parallel coordinates | ✅ | Magnitude | [Parallel coordinates plot with px](https://plotly.com/python/parallel-coordinates-plot/) | [px.parallel_coordinates](https://plotly.com/python-api-reference/generated/plotly.express.parallel_coordinates.html) | -| Pictogram | ❌ | Magnitude | | | -| Pie | ✅ | Part-to-whole | [Pie chart with px](https://plotly.com/python/pie-charts/) | [px.pie](https://plotly.com/python-api-reference/generated/plotly.express.pie) | -| Radar | ✅ | Magnitude | [Radar chart with px](https://plotly.com/python/radar-chart/) | [px.line_polar](https://plotly.com/python-api-reference/generated/plotly.express.line_polar) | -| Radial | ❌ | Magnitude | | | -| Sankey | ✅ | Flow | [Sankey diagram in Plotly](https://plotly.com/python/sankey-diagram/) | [go.Sankey](https://plotly.github.io/plotly.py-docs/generated/plotly.graph_objects.Sankey.html) | -| Scatter | ✅ | Correlation | [Scatter plot with px](https://plotly.com/python/line-and-scatter/) | [px.scatter](https://plotly.com/python-api-reference/generated/plotly.express.scatter) | -| Scatter matrix | ✅ | Correlation | [Scatter matrix with px](https://plotly.com/python/splom/) | [px.scatter_matrix](https://plotly.github.io/plotly.py-docs/generated/plotly.express.scatter_matrix.html) | -| Slope | ❌ | Ranking, Time | | | -| Sparkline | ❌ | Time | | | -| Stacked bar | ✅ | Part-to-whole | [Histograms with px](https://plotly.com/python/histograms/) | [px.histogram](https://plotly.github.io/plotly.py-docs/generated/plotly.express.histogram) | -| Stacked column | ✅ | Part-to-whole | [Histograms with px](https://plotly.com/python/histograms/) | [px.histogram](https://plotly.github.io/plotly.py-docs/generated/plotly.express.histogram) | -| Stepped line | ✅ | Time | [Line plot with px](https://plotly.com/python/line-charts/) | [px.line](https://plotly.com/python-api-reference/generated/plotly.express.line) | -| Surplus deficit line | ❌ | Deviation | | | -| Treemap | ✅ | Part-to-whole | [Treemap with px](https://plotly.com/python/treemaps/) | [px.treemap](https://plotly.com/python-api-reference/generated/plotly.express.treemap.html) | -| Venn | ❌ | Part-to-whole | | | -| Violin | ✅ | Distribution | [Violin plot with px](https://plotly.com/python/violin/) | [px.violin](https://plotly.com/python-api-reference/generated/plotly.express.violin.html) | -| Waterfall | ✅ | Part-to-whole, Flow | [Waterfall charts in Plotly](https://plotly.com/python/waterfall-charts/) | [go.Waterfall](https://plotly.github.io/plotly.py-docs/generated/plotly.graph_objects.Waterfall.html) | +| Chart Type | Status | Category | Credits & sources | API | +| --------------------- | ------ | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Arc | ❌ | Part-to-whole | | | +| Area | ✅ | Time | [Filled area plot with px](https://plotly.com/python/filled-area-plots/) | [px.area](https://plotly.com/python-api-reference/generated/plotly.express.area) | +| Bar | ✅ | Magnitude | [Bar chart with px](https://plotly.com/python/bar-charts/) | [px.bar](https://plotly.com/python-api-reference/generated/plotly.express.bar) | +| Barcode | ❌ | Distribution | | | +| Beeswarm | ❌ | Distribution | | | +| Boxplot | ✅ | Distribution | [Box plot with px](https://plotly.com/python/box-plots/) | [px.box](https://plotly.github.io/plotly.py-docs/generated/plotly.express.box) | +| Bubble | ✅ | Correlation | [Scatter plot with px](https://plotly.com/python/line-and-scatter/) | [px.scatter](https://plotly.com/python-api-reference/generated/plotly.express.scatter) | +| Bubble map | ✅ | Spatial | [Bubble map in px](https://plotly.com/python/bubble-maps/) | [px.scatter_map](https://plotly.github.io/plotly.py-docs/generated/plotly.express.scatter_map) | +| Bubble timeline | ❌ | Time | | | +| Bullet | ❌ | Magnitude | | | +| Bump | ❌ | Ranking | | | +| Butterfly | ✅ | Deviation, Distribution | [Pyramid charts in Plotly](https://plotly.com/python/v3/population-pyramid-charts/) | [go.Bar](https://plotly.com/python-api-reference/generated/plotly.graph_objects.Bar.html) | +| Chord | ❌ | Flow | | | +| Choropleth | ✅ | Spatial | [Choropleth map with px](https://plotly.com/python/choropleth-maps/) | [px.choropleth](https://plotly.github.io/plotly.py-docs/generated/plotly.express.choropleth.html) | +| Column | ✅ | Magnitude, Time | [Bar chart with px](https://plotly.com/python/bar-charts/) | [px.bar](https://plotly.com/python-api-reference/generated/plotly.express.bar.html) | +| Column and line | ✅ | Correlation, Time | [Multiple chart types in Plotly](https://plotly.com/python/graphing-multiple-chart-types/) | [go.Bar](https://plotly.com/python-api-reference/generated/plotly.graph_objects.Bar.html) and [go.Scatter](https://plotly.com/python-api-reference/generated/plotly.graph_objects.Scatter.html) | +| Connected scatter | ✅ | Correlation, Time | [Line plot with px](https://plotly.com/python/line-charts/) | [px.line](https://plotly.com/python-api-reference/generated/plotly.express.line) | +| Cumulative curve | ❌ | Distribution | | | +| Diverging bar | ✅ | Deviation | [Bar chart with px](https://plotly.com/python/bar-charts/) | [px.bar](https://plotly.com/python-api-reference/generated/plotly.express.bar) | +| Diverging stacked bar | ✅ | Deviation | [Plotly forum - diverging stacked bar](https://community.plotly.com/t/need-help-in-making-diverging-stacked-bar-charts/34023/2) | [go.Bar](https://plotly.com/python-api-reference/generated/plotly.graph_objects.Bar.html) | +| Donut | ✅ | Part-to-whole | [Pie chart with px](https://plotly.com/python/pie-charts/) | [px.pie](https://plotly.com/python-api-reference/generated/plotly.express.pie) | +| Dot map | ✅ | Spatial | [Bubble map in px](https://plotly.com/python/bubble-maps/) | [px.scatter_map](https://plotly.github.io/plotly.py-docs/generated/plotly.express.scatter_map) | +| Dumbbell | ✅ | Distribution | [Dumbbell plots in Plotly](https://community.plotly.com/t/how-to-make-dumbbell-plots-in-plotly-python/47762) | [px.scatter](https://plotly.com/python-api-reference/generated/plotly.express.scatter.html) and [add_shape](https://plotly.com/python/shapes/) | +| Fan | ❌ | Time | | | +| Flow map | ❌ | Spatial | | | +| Funnel | ✅ | Part-to-whole | [Funnel plot with px](https://plotly.com/python/funnel-charts/) | [px.funnel](https://plotly.com/python/funnel-charts/) | +| Gantt | ✅ | Time | [Gantt chart with px](https://plotly.com/python/gantt/) | [px.timeline](https://plotly.com/python-api-reference/generated/plotly.express.timeline.html) | +| Gridplot | ❌ | Part-to-whole | | | +| Heatmap | ✅ | Time | [Heatmaps with px](https://plotly.com/python/heatmaps/) | [px.density_heatmap](https://plotly.com/python-api-reference/generated/plotly.express.density_heatmap.html) | +| Correlation matrix | ❌ | Correlation | | | +| Histogram | ✅ | Distribution | [Histograms with px](https://plotly.com/python/histograms/) | [px.histogram](https://plotly.github.io/plotly.py-docs/generated/plotly.express.histogram) | +| Line | ✅ | Time | [Line plot with px](https://plotly.com/python/line-charts/) | [px.line](https://plotly.com/python-api-reference/generated/plotly.express.line) | +| Lollipop | ❌ | Ranking, Magnitude | | | +| Marimekko | ❌ | Magnitude, Part-to-whole | | | +| Network | ❌ | Flow | | | +| Ordered bar | ✅ | Ranking | [Bar chart with px](https://plotly.com/python/bar-charts/) | [px.bar](https://plotly.com/python-api-reference/generated/plotly.express.bar.html) | +| Ordered bubble | ❌ | Ranking | | | +| Ordered column | ✅ | Ranking | [Bar chart with px](https://plotly.com/python/bar-charts/) | [px.bar](https://plotly.com/python-api-reference/generated/plotly.express.bar.html) | +| Paired bar | ✅ | Magnitude | [Histograms with px](https://plotly.com/python/histograms/) | [px.histogram](https://plotly.github.io/plotly.py-docs/generated/plotly.express.histogram) | +| Paired column | ✅ | Magnitude | [Histograms with px](https://plotly.com/python/histograms/) | [px.histogram](https://plotly.github.io/plotly.py-docs/generated/plotly.express.histogram) | +| Parallel coordinates | ✅ | Magnitude | [Parallel coordinates plot with px](https://plotly.com/python/parallel-coordinates-plot/) | [px.parallel_coordinates](https://plotly.com/python-api-reference/generated/plotly.express.parallel_coordinates.html) | +| Pictogram | ❌ | Magnitude | | | +| Pie | ✅ | Part-to-whole | [Pie chart with px](https://plotly.com/python/pie-charts/) | [px.pie](https://plotly.com/python-api-reference/generated/plotly.express.pie) | +| Radar | ✅ | Magnitude | [Radar chart with px](https://plotly.com/python/radar-chart/) | [px.line_polar](https://plotly.com/python-api-reference/generated/plotly.express.line_polar) | +| Radial | ❌ | Magnitude | | | +| Sankey | ✅ | Flow | [Sankey diagram in Plotly](https://plotly.com/python/sankey-diagram/) | [go.Sankey](https://plotly.github.io/plotly.py-docs/generated/plotly.graph_objects.Sankey.html) | +| Scatter | ✅ | Correlation | [Scatter plot with px](https://plotly.com/python/line-and-scatter/) | [px.scatter](https://plotly.com/python-api-reference/generated/plotly.express.scatter) | +| Scatter matrix | ✅ | Correlation | [Scatter matrix with px](https://plotly.com/python/splom/) | [px.scatter_matrix](https://plotly.github.io/plotly.py-docs/generated/plotly.express.scatter_matrix.html) | +| Slope | ❌ | Ranking, Time | | | +| Sparkline | ❌ | Time | | | +| Stacked bar | ✅ | Part-to-whole | [Histograms with px](https://plotly.com/python/histograms/) | [px.histogram](https://plotly.github.io/plotly.py-docs/generated/plotly.express.histogram) | +| Stacked column | ✅ | Part-to-whole | [Histograms with px](https://plotly.com/python/histograms/) | [px.histogram](https://plotly.github.io/plotly.py-docs/generated/plotly.express.histogram) | +| Stepped line | ✅ | Time | [Line plot with px](https://plotly.com/python/line-charts/) | [px.line](https://plotly.com/python-api-reference/generated/plotly.express.line) | +| Surplus deficit line | ❌ | Deviation | | | +| Treemap | ✅ | Part-to-whole | [Treemap with px](https://plotly.com/python/treemaps/) | [px.treemap](https://plotly.com/python-api-reference/generated/plotly.express.treemap.html) | +| Venn | ❌ | Part-to-whole | | | +| Violin | ✅ | Distribution | [Violin plot with px](https://plotly.com/python/violin/) | [px.violin](https://plotly.com/python-api-reference/generated/plotly.express.violin.html) | +| Waterfall | ✅ | Part-to-whole, Flow | [Waterfall charts in Plotly](https://plotly.com/python/waterfall-charts/) | [go.Waterfall](https://plotly.github.io/plotly.py-docs/generated/plotly.graph_objects.Waterfall.html) | ## How to contribute diff --git a/vizro-core/examples/visual-vocabulary/chart_groups.py b/vizro-core/examples/visual-vocabulary/chart_groups.py index 0acc2dab2..2896306f8 100644 --- a/vizro-core/examples/visual-vocabulary/chart_groups.py +++ b/vizro-core/examples/visual-vocabulary/chart_groups.py @@ -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", diff --git a/vizro-core/examples/visual-vocabulary/custom_charts.py b/vizro-core/examples/visual-vocabulary/custom_charts.py index 9ed5e78db..293b13225 100644 --- a/vizro-core/examples/visual-vocabulary/custom_charts.py +++ b/vizro-core/examples/visual-vocabulary/custom_charts.py @@ -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 @@ -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 diff --git a/vizro-core/examples/visual-vocabulary/pages/_pages_utils.py b/vizro-core/examples/visual-vocabulary/pages/_pages_utils.py index 3227cb05c..a198af9d0 100644 --- a/vizro-core/examples/visual-vocabulary/pages/_pages_utils.py +++ b/vizro-core/examples/visual-vocabulary/pages/_pages_utils.py @@ -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"], @@ -76,7 +75,6 @@ def make_code_clipboard_from_py_file(filepath: str): } ) - pastries = pd.DataFrame( { "pastry": [ @@ -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, diff --git a/vizro-core/examples/visual-vocabulary/pages/deviation.py b/vizro-core/examples/visual-vocabulary/pages/deviation.py index 406c8d699..5be2fa027 100644 --- a/vizro-core/examples/visual-vocabulary/pages/deviation.py +++ b/vizro-core/examples/visual-vocabulary/pages/deviation.py @@ -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 @@ -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). + +   + + #### 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] diff --git a/vizro-core/examples/visual-vocabulary/pages/examples/diverging_stacked_bar.py b/vizro-core/examples/visual-vocabulary/pages/examples/diverging_stacked_bar.py new file mode 100644 index 000000000..55ab41aeb --- /dev/null +++ b/vizro-core/examples/visual-vocabulary/pages/examples/diverging_stacked_bar.py @@ -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()