From 2b656bf17660890e2be108bf53ff2b104eba4290 Mon Sep 17 00:00:00 2001 From: Li Nguyen <90609403+huong-li-nguyen@users.noreply.github.com> Date: Tue, 19 Nov 2024 11:39:37 +0100 Subject: [PATCH] [Demo] Add lollipop chart to ViViVo (#874) Co-authored-by: Petar Pejovic <108530920+petar-qb@users.noreply.github.com> --- vizro-core/docs/pages/explanation/authors.md | 1 + vizro-core/examples/scratch_dev/app.py | 78 +++++++++++-------- .../examples/visual-vocabulary/README.md | 2 +- .../visual-vocabulary/chart_groups.py | 2 - .../visual-vocabulary/custom_charts.py | 48 ++++++++++++ .../visual-vocabulary/pages/_factories.py | 47 ++++++++++- .../pages/examples/lollipop.py | 42 ++++++++++ .../visual-vocabulary/pages/magnitude.py | 12 ++- .../visual-vocabulary/pages/ranking.py | 5 +- 9 files changed, 199 insertions(+), 38 deletions(-) create mode 100644 vizro-core/examples/visual-vocabulary/pages/examples/lollipop.py diff --git a/vizro-core/docs/pages/explanation/authors.md b/vizro-core/docs/pages/explanation/authors.md index 34194b24e..8438eebf0 100644 --- a/vizro-core/docs/pages/explanation/authors.md +++ b/vizro-core/docs/pages/explanation/authors.md @@ -47,6 +47,7 @@ Natalia Kurakina, [Hilary Ivy](https://github.com/hxe00570), [Jasmine Wu](https://github.com/jazwu), [njmcgrat](https://github.com/njmcgrat), +[Jenelle Yonkman](https://github.com/yonkmanjl), [ataraexia](https://github.com/ataraexia) with thanks to Sam Bourton and Kevin Staight for sponsorship, inspiration and guidance, diff --git a/vizro-core/examples/scratch_dev/app.py b/vizro-core/examples/scratch_dev/app.py index 84e70e6aa..b99d81da0 100644 --- a/vizro-core/examples/scratch_dev/app.py +++ b/vizro-core/examples/scratch_dev/app.py @@ -1,45 +1,59 @@ """Dev app to try things out.""" import pandas as pd +import plotly.graph_objects as go import vizro.models as vm import vizro.plotly.express as px from vizro import Vizro -from vizro._themes._color_values import COLORS - -pastry = 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], - } -) +from vizro.models.types import capture + + +@capture("graph") +def lollipop(data_frame: pd.DataFrame, x: str, y: str): + """Creates a lollipop chart using Plotly. + + This function generates a scatter chart and then draws lines extending from each point to the x-axis. + + Args: + data_frame (pd.DataFrame): The data source for the chart. + x (str): The column name to be used for the x-axis. + y (str): The column name to be used for the y-axis. + + Returns: + go.Figure: : A Plotly Figure object representing the lollipop chart. + """ + fig = go.Figure() + + # Draw points + fig.add_trace( + go.Scatter( + x=data_frame[x], + y=data_frame[y], + mode="markers", + marker=dict(color="#00b4ff", size=12), + ) + ) + + for i in range(len(data_frame)): + fig.add_trace( + go.Scatter( + x=[0, data_frame[x].iloc[i]], + y=[data_frame[y].iloc[i], data_frame[y].iloc[i]], + mode="lines", + line=dict(color="#00b4ff", width=3), + ) + ) + fig.update_layout(showlegend=False) + return fig + + +gapminder = px.data.gapminder() page = vm.Page( - title="Charts UI", + title="Lollipop", components=[ - vm.Graph( - figure=px.bar( - pastry.sort_values("Profit Ratio"), - orientation="h", - x="Profit Ratio", - y="pastry", - color="Profit Ratio", - color_continuous_scale=COLORS["DIVERGING_RED_CYAN"], - ), - ), + vm.Graph(figure=lollipop(gapminder.query("year == 2007 and gdpPercap > 36000"), y="country", x="gdpPercap")) ], ) diff --git a/vizro-core/examples/visual-vocabulary/README.md b/vizro-core/examples/visual-vocabulary/README.md index 604fe8c8e..3b298520e 100644 --- a/vizro-core/examples/visual-vocabulary/README.md +++ b/vizro-core/examples/visual-vocabulary/README.md @@ -70,7 +70,7 @@ The dashboard is still in development. Below is an overview of the chart types f | 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 | | | +| Lollipop | ✅ | Ranking, Magnitude | [Lollipop & Dumbbell Charts with Plotly](https://towardsdatascience.com/lollipop-dumbbell-charts-with-plotly-696039d5f85) | [px.scatter](https://plotly.com/python-api-reference/generated/plotly.express.scatter) | | 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) | diff --git a/vizro-core/examples/visual-vocabulary/chart_groups.py b/vizro-core/examples/visual-vocabulary/chart_groups.py index 612322ce6..527ae2d8b 100644 --- a/vizro-core/examples/visual-vocabulary/chart_groups.py +++ b/vizro-core/examples/visual-vocabulary/chart_groups.py @@ -81,7 +81,6 @@ class ChartGroup: incomplete_pages=[ IncompletePage("Ordered bubble"), IncompletePage("Slope"), - IncompletePage("Lollipop"), IncompletePage("Bump"), ], icon="Stacked Bar Chart", @@ -117,7 +116,6 @@ class ChartGroup: pages=pages.magnitude.pages, incomplete_pages=[ IncompletePage("Marimekko"), - IncompletePage("Lollipop"), IncompletePage("Pictogram"), IncompletePage("Bullet"), IncompletePage("Radial"), diff --git a/vizro-core/examples/visual-vocabulary/custom_charts.py b/vizro-core/examples/visual-vocabulary/custom_charts.py index ea7b41d63..6a7aae68b 100644 --- a/vizro-core/examples/visual-vocabulary/custom_charts.py +++ b/vizro-core/examples/visual-vocabulary/custom_charts.py @@ -310,3 +310,51 @@ def diverging_stacked_bar(data_frame: pd.DataFrame, **kwargs) -> go.Figure: fig.add_hline(y=0, line_width=2, line_color="grey") return fig + + +@capture("graph") +def lollipop(data_frame: pd.DataFrame, **kwargs): + """Creates a lollipop based on px.scatter. + + A lollipop chart is a variation of a bar chart where each data point is represented by a line and a dot at the end + to mark the value. + + Inspired by: https://towardsdatascience.com/lollipop-dumbbell-charts-with-plotly-696039d5f85 + + Args: + data_frame: DataFrame for the chart. Can be long form or wide form. + See https://plotly.com/python/wide-form/. + **kwargs: Keyword arguments to pass into px.scatter (e.g. x, y, labels). + See https://plotly.com/python-api-reference/generated/plotly.scatter.html. + + Returns: + go.Figure: Lollipop chart. + """ + # Plots the dots of the lollipop chart + fig = px.scatter(data_frame, **kwargs) + + # Enables the orientation of the chart to be either horizontal or vertical + orientation = fig.data[0].orientation + x_or_y = "x" if orientation == "h" else "y" + y_or_x = "y" if orientation == "h" else "x" + + # Plots the lines of the lollipop chart + for x_or_y_value, y_or_x_value in zip(fig.data[0][x_or_y], fig.data[0][y_or_x]): + fig.add_trace(go.Scatter({x_or_y: [0, x_or_y_value], y_or_x: [y_or_x_value, y_or_x_value], "mode": "lines"})) + + # Styles the lollipop chart and makes it uni-colored + fig.update_traces( + marker_size=12, + line_width=3, + line_color=fig.layout.template.layout.colorway[0], + ) + + fig.update_layout( + { + "showlegend": False, + f"{x_or_y}axis_showgrid": True, + f"{y_or_x}axis_showgrid": False, + f"{x_or_y}axis_rangemode": "tozero", + }, + ) + return fig diff --git a/vizro-core/examples/visual-vocabulary/pages/_factories.py b/vizro-core/examples/visual-vocabulary/pages/_factories.py index 97b4615b5..9fa0deb77 100644 --- a/vizro-core/examples/visual-vocabulary/pages/_factories.py +++ b/vizro-core/examples/visual-vocabulary/pages/_factories.py @@ -7,7 +7,7 @@ import vizro.models as vm from pages._pages_utils import PAGE_GRID, make_code_clipboard_from_py_file -from pages.examples import butterfly, column_and_line, connected_scatter, waterfall +from pages.examples import butterfly, column_and_line, connected_scatter, lollipop, waterfall def butterfly_factory(group: str): @@ -179,3 +179,48 @@ def waterfall_factory(group: str): ), ], ) + + +def lollipop_factory(group: str): + """Reusable function to create the page content for the lollipop chart with a unique ID.""" + return vm.Page( + id=f"{group}-lollipop", + path=f"{group}/lollipop", + title="Lollipop", + layout=vm.Layout(grid=PAGE_GRID), + components=[ + vm.Card( + text=""" + + #### What is a lollipop chart? + + A lollipop chart is a variation of a bar chart where each data point is represented by a line and a + dot at the end to mark the value. It functions like a bar chart but offers a cleaner visual, + especially useful when dealing with a large number of high values, to avoid the clutter of tall columns. + However, it can be less precise due to the difficulty in judging the exact center of the circle. + +   + + #### When should I use it? + + Use a lollipop chart to compare values across categories, especially when dealing with many high values. + It highlights differences and trends clearly without the visual bulk of a bar chart. Ensure clarity by + limiting categories, using consistent scales, and clearly labeling axes. Consider alternatives if + precise value representation is crucial. + """ + ), + vm.Graph(figure=lollipop.fig), + vm.Tabs( + tabs=[ + vm.Container( + title="Vizro dashboard", + components=[make_code_clipboard_from_py_file("lollipop.py", mode="vizro")], + ), + vm.Container( + title="Plotly figure", + components=[make_code_clipboard_from_py_file("lollipop.py", mode="plotly")], + ), + ] + ), + ], + ) diff --git a/vizro-core/examples/visual-vocabulary/pages/examples/lollipop.py b/vizro-core/examples/visual-vocabulary/pages/examples/lollipop.py new file mode 100644 index 000000000..9c483706a --- /dev/null +++ b/vizro-core/examples/visual-vocabulary/pages/examples/lollipop.py @@ -0,0 +1,42 @@ +import pandas as pd +import plotly.express as px +import plotly.graph_objects as go +from vizro.models.types import capture + + +@capture("graph") +def lollipop(data_frame: pd.DataFrame, **kwargs): + """Creates a lollipop chart using Plotly.""" + fig = px.scatter(data_frame, **kwargs) + + orientation = fig.data[0].orientation + x_or_y = "x" if orientation == "h" else "y" + y_or_x = "y" if orientation == "h" else "x" + + for x_or_y_value, y_or_x_value in zip(fig.data[0][x_or_y], fig.data[0][y_or_x]): + fig.add_trace(go.Scatter({x_or_y: [0, x_or_y_value], y_or_x: [y_or_x_value, y_or_x_value], "mode": "lines"})) + + fig.update_traces( + marker_size=12, + line_width=3, + line_color=fig.layout.template.layout.colorway[0], + ) + + fig.update_layout( + { + "showlegend": False, + f"{x_or_y}axis_showgrid": True, + f"{y_or_x}axis_showgrid": False, + f"{x_or_y}axis_rangemode": "tozero", + }, + ) + return fig + + +gapminder = ( + px.data.gapminder() + .query("year == 2007 and country.isin(['United States', 'Pakistan', 'India', 'China', 'Indonesia'])") + .sort_values("pop") +) + +fig = lollipop(gapminder, y="country", x="pop") diff --git a/vizro-core/examples/visual-vocabulary/pages/magnitude.py b/vizro-core/examples/visual-vocabulary/pages/magnitude.py index fc3b23337..983e190e8 100644 --- a/vizro-core/examples/visual-vocabulary/pages/magnitude.py +++ b/vizro-core/examples/visual-vocabulary/pages/magnitude.py @@ -2,6 +2,7 @@ import vizro.models as vm +from pages._factories import lollipop_factory from pages._pages_utils import PAGE_GRID, make_code_clipboard_from_py_file from pages.examples import bar, magnitude_column, paired_bar, paired_column, parallel_coordinates, radar @@ -238,4 +239,13 @@ ], ) -pages = [bar_page, column_page, paired_bar_page, paired_column_page, parallel_coordinates_page, radar_page] +lollipop_page = lollipop_factory("magnitude") +pages = [ + bar_page, + column_page, + paired_bar_page, + paired_column_page, + parallel_coordinates_page, + radar_page, + lollipop_page, +] diff --git a/vizro-core/examples/visual-vocabulary/pages/ranking.py b/vizro-core/examples/visual-vocabulary/pages/ranking.py index a788223d7..3ea7bdbe1 100644 --- a/vizro-core/examples/visual-vocabulary/pages/ranking.py +++ b/vizro-core/examples/visual-vocabulary/pages/ranking.py @@ -2,6 +2,7 @@ import vizro.models as vm +from pages._factories import lollipop_factory from pages._pages_utils import PAGE_GRID, make_code_clipboard_from_py_file from pages.examples import ordered_bar, ordered_column @@ -85,4 +86,6 @@ ) -pages = [ordered_bar_page, ordered_column_page] +lollipop_page = lollipop_factory("deviation") + +pages = [ordered_bar_page, ordered_column_page, lollipop_page]