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

Tidy/add bs theme #940

Closed
wants to merge 17 commits into from
Closed
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -49,7 +49,7 @@ repos:
hooks:
- id: ruff
args: [--fix]
exclude: "vizro-core/examples/scratch_dev/app.py"
exclude: "vizro-core/examples/scratch_dev/app_themes.py"
- id: ruff-format

- repo: https://github.com/PyCQA/bandit
7 changes: 4 additions & 3 deletions tools/pycafe/create_pycafe_links.py
Original file line number Diff line number Diff line change
@@ -63,7 +63,7 @@ def generate_link(directory: str, extra_requirements: Optional[list[str]] = None
)

# App file - get current commit, and modify to remove if clause
app_content = requests.get(f"{base_url}/app.py", timeout=10).text
app_content = requests.get(f"{base_url}/app_themes.py", timeout=10).text
app_content_split = app_content.split('if __name__ == "__main__":')
if len(app_content_split) > 1:
app_content = app_content_split[0] + textwrap.dedent(app_content_split[1])
@@ -93,8 +93,9 @@ def generate_link(directory: str, extra_requirements: Optional[list[str]] = None
"url": f"{base_url}{file['path'].removeprefix(f'{directory}')}",
}
for file in folder_files
# Filter out app.py and requirements.txt (as already added above)
if file["type"] == "blob" and file["path"] not in {f"{directory}/app.py", f"{directory}/requirements.txt"}
# Filter out app_themes.py and requirements.txt (as already added above)
if file["type"] == "blob"
and file["path"] not in {f"{directory}/app_themes.py", f"{directory}/requirements.txt"}
]
else:
raise Exception(f"Failed to fetch file tree from GitHub API: {response.status_code} {response.text}")
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))

-->
179 changes: 159 additions & 20 deletions vizro-core/examples/scratch_dev/app.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,165 @@
"""Dev app to try things out."""

from vizro import Vizro
import vizro.plotly.express as px
import vizro.models as vm
from vizro.tables import dash_data_table

gapminder = px.data.gapminder()

table = vm.Page(
title="Table",
components=[
vm.Table(
figure=dash_data_table(data_frame=gapminder, page_size=5),
title="Gapminder Data Insights",
header="""#### An Interactive Exploration of Global Health, Wealth, and Population""",
footer="""SOURCE: **Plotly gapminder data set, 2024**""",
)
"""****** Important! *******
If you run this app locally, un-comment line 127 to add the theme change components to the layout
"""

import dash_ag_grid as dag
import dash_bootstrap_components as dbc
import plotly.express as px
from dash import Dash, Input, Output, callback, clientside_callback, dcc, html

df = px.data.gapminder()
years = df.year.unique()
continents = df.continent.unique()

# Test out local vizro-bootstrap file. Note: It takes a while after a commit for the updated file to be available
base = "https://cdn.jsdelivr.net/gh/mckinsey/vizro@tidy/add-bs-theme/vizro-core/src/vizro/static/css/"
vizro_bootstrap = base + "vizro-bootstrap.min.css"

app = Dash(
__name__,
external_stylesheets=[
# dbc.themes.BOOTSTRAP,
vizro_bootstrap,
dbc.icons.FONT_AWESOME,
],
)

header = html.H3("Theme Explorer Sample App", className="bg-primary p-2 mt-4")

grid = dag.AgGrid(
id="grid",
columnDefs=[{"field": i} for i in df.columns],
rowData=df.to_dict("records"),
defaultColDef={"flex": 1, "minWidth": 120, "sortable": True, "resizable": True, "filter": True},
dashGridOptions={"rowSelection": "multiple"},
)

dropdown = html.Div(
[
dbc.Label("Select indicator (y-axis)"),
dcc.Dropdown(
["gdpPercap", "lifeExp", "pop"],
"pop",
id="indicator",
clearable=False,
),
],
className="mb-4",
)

checklist = html.Div(
[
dbc.Label("Select Continents"),
dbc.Checklist(
id="continents",
options=continents,
value=continents,
inline=True,
),
],
className="mb-4",
)

slider = html.Div(
[
dbc.Label("Select Years"),
dcc.RangeSlider(
years[0],
years[-1],
5,
id="years",
marks=None,
tooltip={"placement": "bottom", "always_visible": True},
value=[years[2], years[-2]],
className="p-0",
),
],
className="mb-5",
)

toggle = dbc.Switch(id="switch", value=False, persistence=True, persistence_type="session")

theme_colors = [
"primary",
"secondary",
"success",
"warning",
"danger",
"info",
"light",
"dark",
"link",
]

colors = html.Div([dbc.Button(f"{color}", color=f"{color}", size="sm") for color in theme_colors])
colors = html.Div(["Theme Colors:", colors], className="mt-2")

controls = dbc.Card([dropdown, checklist, slider, toggle])
tab1 = dbc.Tab([dcc.Graph(id="line-chart", figure=px.line())], label="Line Chart", className="p-4")
tab2 = dbc.Tab([dcc.Graph(id="scatter-chart", figure=px.scatter())], label="Scatter Chart", className="p-4")
tab3 = dbc.Tab([grid], label="Grid", className="p-4")
tabs = dbc.Card(dbc.Tabs([tab1, tab2, tab3]))

app.layout = html.Div(
children=[header, dbc.Row([dbc.Col(controls, width=4), dbc.Col([tabs, colors], width=8)])],
)


@callback(
Output("line-chart", "figure"),
Output("scatter-chart", "figure"),
Output("grid", "dashGridOptions"),
Input("indicator", "value"),
Input("continents", "value"),
Input("years", "value"),
)
def update(indicator, continent, yrs):
if continent == [] or indicator is None:
return {}, {}, {}

dff = df[df.year.between(yrs[0], yrs[1])]
dff = dff[dff.continent.isin(continent)]

fig = px.line(
dff,
x="year",
y=indicator,
color="continent",
line_group="country",
)

fig_scatter = px.scatter(
dff[dff.year == yrs[0]],
x="gdpPercap",
y="lifeExp",
size="pop",
color="continent",
log_x=True,
size_max=60,
)

grid_filter = (
f"{continent}.includes(params.data.continent) && params.data.year >= {yrs[0]} && params.data.year <= {yrs[1]}"
)
dashGridOptions = {
"isExternalFilterPresent": {"function": "true"},
"doesExternalFilterPass": {"function": grid_filter},
}
return fig, fig_scatter, dashGridOptions


# updates the Bootstrap global light/dark color mode
clientside_callback(
"""
switchOn => {
document.documentElement.setAttribute('data-bs-theme', switchOn ? 'light' : 'dark');
return window.dash_clientside.no_update
}
""",
Output("switch", "id"),
Input("switch", "value"),
)

dashboard = vm.Dashboard(pages=[table])

if __name__ == "__main__":
Vizro().build(dashboard).run()
app.run_server(debug=True)
213 changes: 213 additions & 0 deletions vizro-core/examples/scratch_dev/app_themes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
"""
****** Important! *******
If you run this app locally, un-comment line 127 to add the theme change components to the layout
"""

from dash import Dash, dcc, html, Input, Output, State, callback, Patch, clientside_callback
import plotly.express as px
import plotly.io as pio
import dash_bootstrap_components as dbc
from dash_bootstrap_templates import ThemeChangerAIO, template_from_url
import dash_ag_grid as dag

df = px.data.gapminder()
years = df.year.unique()
continents = df.continent.unique()

# stylesheet with the .dbc class to style dcc, DataTable and AG Grid components with a Bootstrap theme
dbc_css = "https://cdn.jsdelivr.net/gh/AnnMarieW/dash-bootstrap-templates/dbc.min.css"

app = Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP, dbc.icons.FONT_AWESOME, dbc_css])


color_mode_switch = html.Span(
[
dbc.Label(className="fa fa-moon", html_for="switch"),
dbc.Switch(id="switch", value=True, className="d-inline-block ms-1", persistence=True),
dbc.Label(className="fa fa-sun", html_for="switch"),
]
)

# The ThemeChangerAIO loads all 52 Bootstrap themed figure templates to plotly.io
theme_controls = html.Div([ThemeChangerAIO(aio_id="theme"), color_mode_switch], className="hstack gap-3 mt-2")

header = html.H4("Theme Explorer Sample App", className="bg-primary text-white p-2 mb-2 text-center")

grid = dag.AgGrid(
id="grid",
columnDefs=[{"field": i} for i in df.columns],
rowData=df.to_dict("records"),
defaultColDef={"flex": 1, "minWidth": 120, "sortable": True, "resizable": True, "filter": True},
dashGridOptions={"rowSelection": "multiple"},
)

dropdown = html.Div(
[
dbc.Label("Select indicator (y-axis)"),
dcc.Dropdown(
["gdpPercap", "lifeExp", "pop"],
"pop",
id="indicator",
clearable=False,
),
],
className="mb-4",
)

checklist = html.Div(
[
dbc.Label("Select Continents"),
dbc.Checklist(
id="continents",
options=continents,
value=continents,
inline=True,
),
],
className="mb-4",
)

slider = html.Div(
[
dbc.Label("Select Years"),
dcc.RangeSlider(
years[0],
years[-1],
5,
id="years",
marks=None,
tooltip={"placement": "bottom", "always_visible": True},
value=[years[2], years[-2]],
className="p-0",
),
],
className="mb-4",
)
theme_colors = [
"primary",
"secondary",
"success",
"warning",
"danger",
"info",
"light",
"dark",
"link",
]
colors = html.Div([dbc.Button(f"{color}", color=f"{color}", size="sm") for color in theme_colors])
colors = html.Div(["Theme Colors:", colors], className="mt-2")


controls = dbc.Card(
[dropdown, checklist, slider],
body=True,
)

tab1 = dbc.Tab([dcc.Graph(id="line-chart", figure=px.line(template="bootstrap"))], label="Line Chart")
tab2 = dbc.Tab([dcc.Graph(id="scatter-chart", figure=px.scatter(template="bootstrap"))], label="Scatter Chart")
tab3 = dbc.Tab([grid], label="Grid", className="p-4")
tabs = dbc.Card(dbc.Tabs([tab1, tab2, tab3]))

app.layout = dbc.Container(
[
header,
dbc.Row(
[
dbc.Col(
[
controls,
# ************************************
# Uncomment line below when running locally!
# ************************************
theme_controls,
],
width=4,
),
dbc.Col([tabs, colors], width=8),
]
),
],
fluid=True,
className="dbc dbc-ag-grid",
)


@callback(
Output("line-chart", "figure"),
Output("scatter-chart", "figure"),
Output("grid", "dashGridOptions"),
Input("indicator", "value"),
Input("continents", "value"),
Input("years", "value"),
State(ThemeChangerAIO.ids.radio("theme"), "value"),
State("switch", "value"),
)
def update(indicator, continent, yrs, theme, color_mode_switch_on):
if continent == [] or indicator is None:
return {}, {}, {}

theme_name = template_from_url(theme)
template_name = theme_name if color_mode_switch_on else theme_name + "_dark"

dff = df[df.year.between(yrs[0], yrs[1])]
dff = dff[dff.continent.isin(continent)]

fig = px.line(dff, x="year", y=indicator, color="continent", line_group="country", template=template_name)

fig_scatter = px.scatter(
dff[dff.year == yrs[0]],
x="gdpPercap",
y="lifeExp",
size="pop",
color="continent",
log_x=True,
size_max=60,
template=template_name,
title="Gapminder %s: %s theme" % (yrs[1], template_name),
)

grid_filter = (
f"{continent}.includes(params.data.continent) && params.data.year >= {yrs[0]} && params.data.year <= {yrs[1]}"
)
dashGridOptions = {
"isExternalFilterPresent": {"function": "true"},
"doesExternalFilterPass": {"function": grid_filter},
}

return fig, fig_scatter, dashGridOptions


# updates the Bootstrap global light/dark color mode
clientside_callback(
"""
switchOn => {
document.documentElement.setAttribute('data-bs-theme', switchOn ? 'light' : 'dark');
return window.dash_clientside.no_update
}
""",
Output("switch", "id"),
Input("switch", "value"),
)


# This callback makes updating figures with the new theme much faster
@callback(
Output("line-chart", "figure", allow_duplicate=True),
Output("scatter-chart", "figure", allow_duplicate=True),
Input(ThemeChangerAIO.ids.radio("theme"), "value"),
Input("switch", "value"),
prevent_initial_call=True,
)
def update_template(theme, color_mode_switch_on):
theme_name = template_from_url(theme)
template_name = theme_name if color_mode_switch_on else theme_name + "_dark"

patched_figure = Patch()
# When using Patch() to update the figure template, you must use the figure template dict
# from plotly.io and not just the template name
patched_figure["layout"]["template"] = pio.templates[template_name]
return patched_figure, patched_figure


if __name__ == "__main__":
app.run_server(debug=True)
2 changes: 2 additions & 0 deletions vizro-core/src/vizro/static/css/table.css
Original file line number Diff line number Diff line change
@@ -4,11 +4,13 @@

#dashboard-container .dash-spreadsheet-inner th {
background-color: inherit;
color: var(--text-primary);
padding: 10px 0;
}

#dashboard-container .dash-spreadsheet-inner td {
background-color: inherit;
color: var(--text-primary);
}

#dashboard-container .dash-spreadsheet-inner table {
2 changes: 1 addition & 1 deletion vizro-core/src/vizro/static/css/vizro-bootstrap.min.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion vizro-core/tests/integration/test_examples.py
Original file line number Diff line number Diff line change
@@ -29,7 +29,7 @@ def dashboard(request, monkeypatch):
example_directory = request.getfixturevalue("example_path") / request.getfixturevalue("version")
monkeypatch.chdir(example_directory)
monkeypatch.syspath_prepend(example_directory)
return runpy.run_path("app.py")["dashboard"]
return runpy.run_path("app_themes.py")["dashboard"]
# Both run_path and run_module contaminate sys.modules, so we need to undo this in order to avoid interference
# between tests. However, if you do this then importlib.import_module seems to cause the problem due to mysterious
# reasons. The current system should work well so long as there's no sub-packages with clashing names in the