Skip to content

Commit

Permalink
Expose plotly theme layout in config options (#658)
Browse files Browse the repository at this point in the history
  • Loading branch information
asnyv authored Dec 11, 2022
1 parent bef5f2f commit 4975c04
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 114 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added
- [#644](https://github.com/equinor/webviz-config/pull/644) - Added option to download tables in `DataTable` and `PivotTable`.
- [#658](https://github.com/equinor/webviz-config/pull/658) - Added `plotly_theme` to `options` in configuration file, allowing user to modify theming of plotly plots without creating a completely new theme. Examples are formatting of axes like gridlines, ticks, color palettes and number formatting.

## [0.5.0] - 2022-10-10

Expand Down
4 changes: 4 additions & 0 deletions examples/basic_example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ title: Reek Webviz Demonstration
options:
menu:
initially_pinned: True
plotly_theme:
yaxis:
showgrid: True
gridcolor: lightgrey

layout:

Expand Down
200 changes: 98 additions & 102 deletions webviz_config/_config_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -470,114 +470,106 @@ def _parse_navigation(self) -> None:
"navigation_items"
] = self._recursively_parse_navigation_item(self.configuration["pages"], 0)

options_found = False
if "options" in self.configuration:
if "menu" in self.configuration["options"]:
options_found = True

if "bar_position" not in self.configuration["options"]["menu"]:
self.configuration["options"]["menu"]["bar_position"] = "left"
elif self.configuration["options"]["menu"]["bar_position"] not in [
"left",
"top",
"right",
"bottom",
]:
raise ParserError(
f"{terminal_colors.RED}{terminal_colors.BOLD}"
"Invalid option for options > menu > bar_position: "
f"{self.configuration['options']['menu']['bar_position']}. "
"Please select one of the following options: left, top, right, bottom."
f"{terminal_colors.END}"
)

if "drawer_position" not in self.configuration["options"]["menu"]:
self.configuration["options"]["menu"]["drawer_position"] = "left"
elif self.configuration["options"]["menu"]["drawer_position"] not in [
"left",
"right",
]:
raise ParserError(
f"{terminal_colors.RED}{terminal_colors.BOLD}"
"Invalid option for options > menu > drawer_position: "
f"{self.configuration['options']['menu']['drawer_position']}. "
"Please select one of the following options: left, right."
f"{terminal_colors.END}"
)

if "initially_pinned" not in self.configuration["options"]["menu"]:
self.configuration["options"]["menu"]["initially_pinned"] = False
elif not isinstance(
self.configuration["options"]["menu"]["initially_pinned"], bool
):
raise ParserError(
f"{terminal_colors.RED}{terminal_colors.BOLD}"
"Invalid option for options > menu > initially_pinned: "
f"{self.configuration['options']['menu']['initially_pinned']}. "
"Please select a boolean value: True, False"
f"{terminal_colors.END}"
)
self.configuration["options"] = self.configuration.get("options", {})
if "menu" in self.configuration["options"]:

if "bar_position" not in self.configuration["options"]["menu"]:
self.configuration["options"]["menu"]["bar_position"] = "left"
elif self.configuration["options"]["menu"]["bar_position"] not in [
"left",
"top",
"right",
"bottom",
]:
raise ParserError(
f"{terminal_colors.RED}{terminal_colors.BOLD}"
"Invalid option for options > menu > bar_position: "
f"{self.configuration['options']['menu']['bar_position']}. "
"Please select one of the following options: left, top, right, bottom."
f"{terminal_colors.END}"
)

if "homepage" not in self.configuration["options"]["menu"]:
self.configuration["options"]["menu"]["homepage"] = None
elif not isinstance(
self.configuration["options"]["menu"]["homepage"], str
):
raise ParserError(
f"{terminal_colors.RED}{terminal_colors.BOLD}"
"Invalid option for options > menu > homepage: "
f"{self.configuration['options']['menu']['homepage']}. "
"Please select a valid string value"
f"{terminal_colors.END}"
)
elif (
self.configuration["options"]["menu"]["homepage"]
not in self._page_titles
):
raise ParserError(
f"{terminal_colors.RED}{terminal_colors.BOLD}"
"Invalid option for options > menu > homepage: "
f"{self.configuration['options']['menu']['homepage']}. "
f"Please check your config file and use the name of an existing page."
f"{terminal_colors.END}"
)
else:
self.configuration["options"]["menu"]["homepage"] = self._page_ids[
self._page_titles.index(
self.configuration["options"]["menu"]["homepage"]
)
]
if "drawer_position" not in self.configuration["options"]["menu"]:
self.configuration["options"]["menu"]["drawer_position"] = "left"
elif self.configuration["options"]["menu"]["drawer_position"] not in [
"left",
"right",
]:
raise ParserError(
f"{terminal_colors.RED}{terminal_colors.BOLD}"
"Invalid option for options > menu > drawer_position: "
f"{self.configuration['options']['menu']['drawer_position']}. "
"Please select one of the following options: left, right."
f"{terminal_colors.END}"
)

if "initially_collapsed" not in self.configuration["options"]["menu"]:
self.configuration["options"]["menu"]["initially_collapsed"] = False
elif not isinstance(
self.configuration["options"]["menu"]["initially_collapsed"], bool
):
raise ParserError(
f"{terminal_colors.RED}{terminal_colors.BOLD}"
"Invalid option for options > menu > initially_collapsed: "
f"{self.configuration['options']['menu']['initially_collapsed']}. "
"Please select a boolean value: True, False"
f"{terminal_colors.END}"
)
if "initially_pinned" not in self.configuration["options"]["menu"]:
self.configuration["options"]["menu"]["initially_pinned"] = False
elif not isinstance(
self.configuration["options"]["menu"]["initially_pinned"], bool
):
raise ParserError(
f"{terminal_colors.RED}{terminal_colors.BOLD}"
"Invalid option for options > menu > initially_pinned: "
f"{self.configuration['options']['menu']['initially_pinned']}. "
"Please select a boolean value: True, False"
f"{terminal_colors.END}"
)

if "show_logo" not in self.configuration["options"]["menu"]:
self.configuration["options"]["menu"]["show_logo"] = True
elif not isinstance(
self.configuration["options"]["menu"]["show_logo"], bool
):
raise ParserError(
f"{terminal_colors.RED}{terminal_colors.BOLD}"
"Invalid option for options > menu > show_logo: "
f"{self.configuration['options']['menu']['show_logo']}. "
"Please select a boolean value: True, False"
f"{terminal_colors.END}"
if "homepage" not in self.configuration["options"]["menu"]:
self.configuration["options"]["menu"]["homepage"] = None
elif not isinstance(self.configuration["options"]["menu"]["homepage"], str):
raise ParserError(
f"{terminal_colors.RED}{terminal_colors.BOLD}"
"Invalid option for options > menu > homepage: "
f"{self.configuration['options']['menu']['homepage']}. "
"Please select a valid string value"
f"{terminal_colors.END}"
)
elif (
self.configuration["options"]["menu"]["homepage"]
not in self._page_titles
):
raise ParserError(
f"{terminal_colors.RED}{terminal_colors.BOLD}"
"Invalid option for options > menu > homepage: "
f"{self.configuration['options']['menu']['homepage']}. "
f"Please check your config file and use the name of an existing page."
f"{terminal_colors.END}"
)
else:
self.configuration["options"]["menu"]["homepage"] = self._page_ids[
self._page_titles.index(
self.configuration["options"]["menu"]["homepage"]
)
]

if not options_found:
if "options" not in self.configuration:
self.configuration["options"] = {}
if "initially_collapsed" not in self.configuration["options"]["menu"]:
self.configuration["options"]["menu"]["initially_collapsed"] = False
elif not isinstance(
self.configuration["options"]["menu"]["initially_collapsed"], bool
):
raise ParserError(
f"{terminal_colors.RED}{terminal_colors.BOLD}"
"Invalid option for options > menu > initially_collapsed: "
f"{self.configuration['options']['menu']['initially_collapsed']}. "
"Please select a boolean value: True, False"
f"{terminal_colors.END}"
)

if "show_logo" not in self.configuration["options"]["menu"]:
self.configuration["options"]["menu"]["show_logo"] = True
elif not isinstance(
self.configuration["options"]["menu"]["show_logo"], bool
):
raise ParserError(
f"{terminal_colors.RED}{terminal_colors.BOLD}"
"Invalid option for options > menu > show_logo: "
f"{self.configuration['options']['menu']['show_logo']}. "
"Please select a boolean value: True, False"
f"{terminal_colors.END}"
)
else:
self.configuration["options"]["menu"] = {
"bar_position": "left",
"drawer_position": "left",
Expand All @@ -586,3 +578,7 @@ def _parse_navigation(self) -> None:
"show_logo": True,
"homepage": None,
}

self.configuration["options"]["plotly_theme"] = self.configuration[
"options"
].get("plotly_theme", {})
13 changes: 12 additions & 1 deletion webviz_config/_docs/_create_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,25 @@
},
"homepage": {
"description": """
Set a custom page as homepage to which the user returns when clicking on the logo.
Set a custom page as homepage to which the user returns when clicking on the logo.
Use the page's title.
""",
"type": "string",
},
},
"additionalProperties": False,
},
"plotly_theme": {
"type": "object",
"description": """
Option to define modifications to the theme's Plotly figure layout.
Examples are e.g. axis formatting and color palettes. Will be merged
with existing theme layout, with options defined here prioritized if
conflicting with the existing theme. The layout is defined as a
dictionary, for details see:
https://plotly.com/python/reference/layout/
""",
},
},
"layout": {
"description": "Define the pages (and potential sections and groups)"
Expand Down
4 changes: 4 additions & 0 deletions webviz_config/_theme_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ def plotly_theme(self, plotly_theme: dict) -> None:
"""Layout object of Plotly graph objects."""
self._plotly_theme = plotly_theme

def plotly_theme_layout_update(self, plotly_theme_layout: dict) -> None:
"""Updates layout object of Plotly graph objects based on input dict"""
self._plotly_theme["layout"] = self.create_themed_layout(plotly_theme_layout)

@property
def external_stylesheets(self) -> list:
return self._external_stylesheets
Expand Down
5 changes: 3 additions & 2 deletions webviz_config/templates/copy_data_template.py.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ logging.basicConfig(level=logging.{{ loglevel }})

theme = webviz_config.WebvizConfigTheme("{{ theme_name }}")
theme.from_json((Path(__file__).resolve().parent / "theme_settings.json").read_text())
theme.plotly_theme_layout_update({{ options.plotly_theme }})

app = dash.Dash()
app.config.suppress_callback_exceptions = True
Expand All @@ -40,7 +41,7 @@ webviz_config.plugins.{{ content._call_signature[0].split('(')[0]}}
# argument to all plugins that request it.
webviz_settings: webviz_config.WebvizSettings = webviz_config.WebvizSettings(
shared_settings=webviz_config.SHARED_SETTINGS_SUBSCRIPTIONS.transformed_settings(
{{ shared_settings }}, {{ config_folder }}, {{ False }}
{{ shared_settings }}, {{ config_folder }}, {{ False }}
),
theme=theme,
)
Expand All @@ -55,7 +56,7 @@ app._deprecated_webviz_settings = {
}

{% if logging_config_dict is defined %}
# Apply a logging config dict as specified via the --logconfig command line argument.
# Apply a logging config dict as specified via the --logconfig command line argument.
logging.config.dictConfig({{ logging_config_dict }})
{% endif %}

Expand Down
19 changes: 10 additions & 9 deletions webviz_config/templates/webviz_template.py.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,15 @@ from webviz_config.utils import deprecate_webviz_settings_attribute_in_dash_app
import webviz_core_components as wcc


# Start out by setting a sensible configuration for the root logger and setting a global
# loglevel. The (global) loglevel defaults to WARNING, but can be set by the user via
# the --loglevel argument. The call to basicConfig() should happen before any other logging
# Start out by setting a sensible configuration for the root logger and setting a global
# loglevel. The (global) loglevel defaults to WARNING, but can be set by the user via
# the --loglevel argument. The call to basicConfig() should happen before any other logging
# related calls, see https://docs.python.org/3/library/logging.html#logging.basicConfig
logging.basicConfig(level=logging.{{ loglevel }})

theme = webviz_config.WebvizConfigTheme("{{ theme_name }}")
theme.from_json((Path(__file__).resolve().parent / "theme_settings.json").read_text())
theme.plotly_theme_layout_update({{ options.plotly_theme }})

app = Dash(
name=__name__,
Expand Down Expand Up @@ -88,7 +89,7 @@ webviz_config.plugins.{{ content._call_signature[0].split('(')[0]}}
# argument to all plugins that request it.
webviz_settings: webviz_config.WebvizSettings = webviz_config.WebvizSettings(
shared_settings=webviz_config.SHARED_SETTINGS_SUBSCRIPTIONS.transformed_settings(
{{ shared_settings }}, {{ config_folder }}, {{ portable }}
{{ shared_settings }}, {{ config_folder }}, {{ portable }}
),
theme=theme,
)
Expand Down Expand Up @@ -153,7 +154,7 @@ else:
{% endfor %}
{% endfor %}
app.layout = html.Div(
className="layoutWrapper",
className="layoutWrapper",
children=[
dcc.Location(id='location', refresh=True),
wcc.WebvizContentManager(
Expand Down Expand Up @@ -192,8 +193,8 @@ Talisman(server, content_security_policy=theme.csp, feature_policy=theme.feature
oauth2 = webviz_config.Oauth2(app.server) if use_oauth2 else None

@callback(
Output("plugins-wrapper", "children"),
Output("settings-drawer", "children"),
Output("plugins-wrapper", "children"),
Output("settings-drawer", "children"),
Input("location", "pathname")
)
def update_page(pathname):
Expand All @@ -204,7 +205,7 @@ def update_page(pathname):
else:
pathname = ""
if not pathname:
pathname = next(iter(page_plugins))
pathname = next(iter(page_plugins))
return page_plugins.get(pathname, ["Oooppss... Page not found."]), page_settings.get(pathname, [])

{{ "WEBVIZ_ASSETS.directly_host_assets(app)" if not portable else ""}}
Expand Down Expand Up @@ -238,7 +239,7 @@ if __name__ == "__main__":
dev_tools_ui=True,
dev_tools_props_check=True,
dev_tools_serve_dev_bundles=True,
{% endif %}
{% endif %}
)
else:
# This will be applied if not running on localhost
Expand Down

0 comments on commit 4975c04

Please sign in to comment.