diff --git a/CHANGELOG.md b/CHANGELOG.md index 40f207d0..ba354227 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/examples/basic_example.yaml b/examples/basic_example.yaml index ad59776f..49c64b0a 100644 --- a/examples/basic_example.yaml +++ b/examples/basic_example.yaml @@ -6,6 +6,10 @@ title: Reek Webviz Demonstration options: menu: initially_pinned: True + plotly_theme: + yaxis: + showgrid: True + gridcolor: lightgrey layout: diff --git a/webviz_config/_config_parser.py b/webviz_config/_config_parser.py index c8fd5887..d4e61815 100644 --- a/webviz_config/_config_parser.py +++ b/webviz_config/_config_parser.py @@ -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", @@ -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", {}) diff --git a/webviz_config/_docs/_create_schema.py b/webviz_config/_docs/_create_schema.py index 32571456..be032a5f 100644 --- a/webviz_config/_docs/_create_schema.py +++ b/webviz_config/_docs/_create_schema.py @@ -43,7 +43,7 @@ }, "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", @@ -51,6 +51,17 @@ }, "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)" diff --git a/webviz_config/_theme_class.py b/webviz_config/_theme_class.py index 2bb6d236..307d9bc0 100644 --- a/webviz_config/_theme_class.py +++ b/webviz_config/_theme_class.py @@ -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 diff --git a/webviz_config/templates/copy_data_template.py.jinja2 b/webviz_config/templates/copy_data_template.py.jinja2 index 78a172d4..1aa1f2ca 100644 --- a/webviz_config/templates/copy_data_template.py.jinja2 +++ b/webviz_config/templates/copy_data_template.py.jinja2 @@ -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 @@ -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, ) @@ -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 %} diff --git a/webviz_config/templates/webviz_template.py.jinja2 b/webviz_config/templates/webviz_template.py.jinja2 index 6bc10e8d..bc0267c2 100644 --- a/webviz_config/templates/webviz_template.py.jinja2 +++ b/webviz_config/templates/webviz_template.py.jinja2 @@ -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__, @@ -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, ) @@ -153,7 +154,7 @@ else: {% endfor %} {% endfor %} app.layout = html.Div( - className="layoutWrapper", + className="layoutWrapper", children=[ dcc.Location(id='location', refresh=True), wcc.WebvizContentManager( @@ -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): @@ -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 ""}} @@ -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