diff --git a/vizro-core/docs/pages/explanation/authors.md b/vizro-core/docs/pages/explanation/authors.md index c59700a4a..4919c893a 100644 --- a/vizro-core/docs/pages/explanation/authors.md +++ b/vizro-core/docs/pages/explanation/authors.md @@ -46,7 +46,8 @@ Natalia Kurakina, [Rosheen C.](https://github.com/rc678), [Hilary Ivy](https://github.com/hxe00570), [Jasmine Wu](https://github.com/jazwu), -[njmcgrat](https://github.com/njmcgrat) +[njmcgrat](https://github.com/njmcgrat), +[Jenelle Yonkman](https://github.com/yonkmanjl) 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 667742e8c..cf60959ab 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 2896306f8..2ecf566b6 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..05bffad81 100644 --- a/vizro-core/examples/visual-vocabulary/custom_charts.py +++ b/vizro-core/examples/visual-vocabulary/custom_charts.py @@ -310,3 +310,48 @@ 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. + """ + # Unlike the column_and_line chart, where all traces hold equal significance, here the traces differ in importance. + # The primary scatter plot is the main trace, while the additional traces merely serve as connecting lines. + # Therefore, should we apply the kwargs solely to the main scatter plot, as illustrated below? + fig = px.scatter(data_frame, **kwargs) + + # Enable for both orientations + is_horizontal = fig.data[0].orientation == "h" + x_coords = [[0, x] if is_horizontal else [x, x] for x in fig.data[0]["x"]] + y_coords = [[y, y] if is_horizontal else [0, y] for y in fig.data[0]["y"]] + for x, y in zip(x_coords, y_coords): + fig.add_trace(go.Scatter(x=x, y=y, mode="lines")) + + xaxis_showgrid = is_horizontal + yaxis_showgrid = not is_horizontal + + fig.update_traces( + marker_size=12, + line_width=3, + line_color=fig.layout.template.layout.colorway[0], + ) + + fig.update_layout( + showlegend=False, yaxis_showgrid=yaxis_showgrid, xaxis_showgrid=xaxis_showgrid, yaxis_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..52f07de09 --- /dev/null +++ b/vizro-core/examples/visual-vocabulary/pages/examples/lollipop.py @@ -0,0 +1,40 @@ +import pandas as pd +import plotly.express as px +import plotly.graph_objects as go +from vizro.models.types import capture + +gapminder = px.data.gapminder() + + +@capture("graph") +def lollipop(data_frame: pd.DataFrame, **kwargs): + """Creates a lollipop chart using Plotly.""" + fig = px.scatter(data_frame, **kwargs) + + # Enable for both orientations + is_horizontal = fig.data[0].orientation == "h" + x_coords = [[0, x] if is_horizontal else [x, x] for x in fig.data[0]["x"]] + y_coords = [[y, y] if is_horizontal else [0, y] for y in fig.data[0]["y"]] + for x, y in zip(x_coords, y_coords): + fig.add_trace(go.Scatter(x=x, y=y, mode="lines")) + + xaxis_showgrid = is_horizontal + yaxis_showgrid = not is_horizontal + + fig.update_traces( + marker_size=12, + line_width=3, + line_color=fig.layout.template.layout.colorway[0], + ) + + fig.update_layout( + showlegend=False, yaxis_showgrid=yaxis_showgrid, xaxis_showgrid=xaxis_showgrid, yaxis_rangemode="tozero" + ) + return fig + + +fig = lollipop( + data_frame=gapminder.query("year == 2007 and gdpPercap > 36000").sort_values("gdpPercap"), + y="country", + x="gdpPercap", +) 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]