diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7eb5315a..1199f7a8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,9 +1,9 @@ # Contributing to Webviz configuration utility -## Creating a new composite object +## Creating a new container Most of the development work is towards creating standard containers. -An container usually does three things: +A container usually does three things: * It has a `layout` property, consisting of multiple [Dash components](https://dash.plot.ly/getting-started). @@ -21,9 +21,10 @@ mandatory to provide. A minimal container could look like: ```python import dash_html_components as html +from webviz_config.containers import WebvizContainer -class ExampleContainer: +class ExampleContainer(WebvizContainer): def __init__(self): pass @@ -74,9 +75,10 @@ backend, you can add callbacks. A simple example of this is given below. from uuid import uuid4 import dash_html_components as html from dash.dependencies import Input, Output +from webviz_config.containers import WebvizContainer -class ExampleContainer: +class ExampleContainer(WebvizContainer): def __init__(self, app): self.button_id = f'submit-button-{uuid4()}' @@ -128,9 +130,10 @@ user provided arguments. A minimal example could look like: ```python import dash_html_components as html +from webviz_config.containers import WebvizContainer -class ExampleContainer: +class ExampleContainer(WebvizContainer): def __init__(self, title: str, number: int=42): self.title = title @@ -265,11 +268,12 @@ A full example could look like e.g.: ```python import pandas as pd -from ..webviz_store import webvizstore -from ..common_cache import cache +from webviz_config.webviz_store import webvizstore +from webviz_config.common_cache import cache +from webviz_config.containers import WebvizContainer -class ExamplePortable: +class ExamplePortable(WebvizContainer): def __init__(self, some_number: int): self.some_number = some_number @@ -336,9 +340,10 @@ the configuration file. As an example, assume someone on your project has made ```python import dash_html_components as html +from webviz_config.containers import WebvizContainer -class OurCustomContainer: +class OurCustomContainer(WebvizContainer): def __init__(self, title: str): self.title = title diff --git a/README.md b/README.md index c4040d23..85d79912 100644 --- a/README.md +++ b/README.md @@ -16,14 +16,20 @@ This utility reduces the skills necessary to configure a Webviz application. *The workflow can be summarized as this:* 1) The user provides a configuration file following the [yaml](https://en.wikipedia.org/wiki/YAML) standard. 2) This utility reads the configuration file and automatically writes the corresponding Webviz Dash code. -3) *Optional:* The user can customize the automatically generated Python code if non-standard content is requested. +3) The created application can either be viewed locally, or deployed to a cloud provider. Both out of the box. + +Creating custom, specialized elements is possible. See the [contribution guide](./CONTRIBUTING.md) for +more details. The [yaml](https://en.wikipedia.org/wiki/YAML) configuration file can either be manually created, or it could be auto-generated by some other tool. +Example configuration file and information about the standard containers +can be seen in [the documentation](https://equinor.github.io/webviz-config/). + ### Installation -As Dash is using Python3-only functionality, you should use a Python3 virtual +As `dash` is using Python3-only functionality, you should use a Python3 (virtual) environment before installation. One way of doing this is ```bash PATH_TO_VENV='./my_new_venv' @@ -71,6 +77,11 @@ cd ./docs python3 build_docs.py ``` +Officially updated built end-user documentation (i.e. information to the +person setting up the configuration file) is +[hosted here on github](https://equinor.github.io/webviz-config/). + + ### Usage After installation, there is a console script named `webviz` available @@ -114,9 +125,9 @@ webviz certificate ``` Certificate installation guidelines will be given when running the command. -### Creating new elements +### Creating new containers -If you are interested in creating new elements which can be configured through +If you are interested in creating new containers which can be configured through the configuration file, take a look at the [contribution guide](./CONTRIBUTING.md). ### Disclaimer diff --git a/docs/build_docs.py b/docs/build_docs.py index 2291696e..e5d440ab 100644 --- a/docs/build_docs.py +++ b/docs/build_docs.py @@ -23,8 +23,7 @@ import jinja2 from markdown import markdown import webviz_config.containers - -SPECIAL_ARGS = ['self', 'app', 'container_settings'] +from webviz_config._config_parser import SPECIAL_ARGS SCRIPT_DIR = pathlib.Path(__file__).resolve().parent BUILD_DIR = SCRIPT_DIR / '_build' diff --git a/webviz_config/_build_webviz.py b/webviz_config/_build_webviz.py index e5965fa9..2fee8ce4 100644 --- a/webviz_config/_build_webviz.py +++ b/webviz_config/_build_webviz.py @@ -58,8 +58,13 @@ def build_webviz(args): 'Finished data extraction. All output saved.' '\033[0m') - write_script(args, build_directory, - 'webviz_template.py.jinja2', BUILD_FILENAME) + non_default_assets = write_script(args, + build_directory, + 'webviz_template.py.jinja2', + BUILD_FILENAME) + + for asset in non_default_assets: + shutil.copy(asset, os.path.join(build_directory, 'assets')) if not args.portable: run_webviz(args, build_directory) diff --git a/webviz_config/_config_parser.py b/webviz_config/_config_parser.py index e84f39b5..04badd39 100644 --- a/webviz_config/_config_parser.py +++ b/webviz_config/_config_parser.py @@ -5,21 +5,26 @@ import importlib import yaml from . import containers as standard_containers +from .containers import WebvizContainer SPECIAL_ARGS = ['self', 'app', 'container_settings', '_call_signature', '_imports'] -def get_class_members(module): - '''Returns a list of all class names defined +def _get_webviz_containers(module): + '''Returns a list of all Webviz Containers in the module given as input. ''' + + def _is_webviz_container(obj): + return inspect.isclass(obj) and issubclass(obj, WebvizContainer) + return [member[0] for member in - inspect.getmembers(module, inspect.isclass)] + inspect.getmembers(module, _is_webviz_container)] -def call_signature(module, module_name, container_name, - container_settings, kwargs, config_folder): +def _call_signature(module, module_name, container_name, + container_settings, kwargs, config_folder): '''Takes as input the name of a container, the module it is located in, together with user given arguments (originating from the configuration file). Returns the equivalent Python code wrt. initiating an instance of @@ -93,7 +98,7 @@ class ParserError(Exception): class ConfigParser: - STANDARD_CONTAINERS = get_class_members(standard_containers) + STANDARD_CONTAINERS = _get_webviz_containers(standard_containers) def __init__(self, yaml_file): try: @@ -111,6 +116,7 @@ def __init__(self, yaml_file): self._config_folder = pathlib.Path(yaml_file).parent self._page_ids = [] + self._assets = set() self.clean_configuration() def _generate_page_id(self, title): @@ -215,14 +221,18 @@ def clean_configuration(self): .add(('webviz_config.containers', 'standard_containers')) - container['_call_signature'] = call_signature( - standard_containers, - 'standard_containers', - container_name, - container_settings, - kwargs, - self._config_folder - ) + container['_call_signature'] = _call_signature( + standard_containers, + 'standard_containers', + container_name, + container_settings, + kwargs, + self._config_folder + ) + + self.assets.update(getattr(standard_containers, + container_name).ASSETS) + else: parts = container_name.split('.') @@ -230,14 +240,14 @@ def clean_configuration(self): module_name = ".".join(parts[:-1]) module = importlib.import_module(module_name) - if container_name not in get_class_members(module): + if container_name not in _get_webviz_containers(module): raise ParserError('\033[91m' f'Module `{module}` does not have a ' f'container named `{container_name}`' '\033[0m') else: self.configuration['_imports'].add(module_name) - container['_call_signature'] = call_signature( + container['_call_signature'] = _call_signature( module, module_name, container_name, @@ -246,6 +256,13 @@ def clean_configuration(self): self._config_folder ) + self.assets.update(getattr(standard_containers, + container_name).ASSETS) + @property def configuration(self): return self._configuration + + @property + def assets(self): + return self._assets diff --git a/webviz_config/_write_script.py b/webviz_config/_write_script.py index df3c1ace..359c5c4b 100644 --- a/webviz_config/_write_script.py +++ b/webviz_config/_write_script.py @@ -8,7 +8,9 @@ def write_script(args, build_directory, template_filename, output_filename): - configuration = ConfigParser(args.yaml_file).configuration + config_parser = ConfigParser(args.yaml_file) + configuration = config_parser.configuration + configuration['port'] = args.port configuration['portable'] = True if args.portable else False configuration['debug'] = args.debug @@ -18,6 +20,7 @@ def write_script(args, build_directory, template_filename, output_filename): configuration['csp'] = theme.csp configuration['feature_policy'] = theme.feature_policy configuration['external_stylesheets'] = theme.external_stylesheets + configuration['plotly_layout'] = theme.plotly_layout configuration['ssl_context'] = '({!r}, {!r})'\ .format(SERVER_CRT_FILENAME, @@ -34,3 +37,5 @@ def write_script(args, build_directory, template_filename, output_filename): with open(os.path.join(build_directory, output_filename), 'w') as fh: fh.write(template.render(configuration)) + + return config_parser.assets diff --git a/webviz_config/containers/__init__.py b/webviz_config/containers/__init__.py index 4075c3eb..c77a28bc 100644 --- a/webviz_config/containers/__init__.py +++ b/webviz_config/containers/__init__.py @@ -6,6 +6,8 @@ import pkg_resources +from ._container_class import WebvizContainer + from ._example_container import ExampleContainer from ._example_assets import ExampleAssets from ._example_portable import ExamplePortable @@ -16,7 +18,8 @@ from ._embed_pdf import EmbedPdf from ._markdown import Markdown -__all__ = ['ExampleContainer', +__all__ = ['WebvizContainer', + 'ExampleContainer', 'ExampleAssets', 'ExamplePortable', 'BannerImage', diff --git a/webviz_config/containers/_banner_image.py b/webviz_config/containers/_banner_image.py index d197a84b..d7a72e0f 100644 --- a/webviz_config/containers/_banner_image.py +++ b/webviz_config/containers/_banner_image.py @@ -1,10 +1,11 @@ import os from pathlib import Path import dash_html_components as html +from . import WebvizContainer from ..webviz_assets import webviz_assets -class BannerImage: +class BannerImage(WebvizContainer): '''### Banner image This container adds a full width _banner image_, with an optional overlayed diff --git a/webviz_config/containers/_container_class.py b/webviz_config/containers/_container_class.py new file mode 100644 index 00000000..5232fd6d --- /dev/null +++ b/webviz_config/containers/_container_class.py @@ -0,0 +1,31 @@ +import abc + + +class WebvizContainer(abc.ABC): + '''All webviz containers need to subclass this abstract base class, + e.g. + + ```python + class MyContainer(WebvizContainer): + + def __init__(self): + ... + + def layout(self): + ... + ``` + ''' + + # List of container specific assets which should be copied + # over to the ./assets folder in the generated webviz app. + # This is typically custom JavaScript and/or CSS files. + # All paths in the returned ASSETS list should be absolute. + ASSETS = [] + + @abc.abstractmethod + def layout(self): + '''This is the only required function of a Webviz Container. + It returns a Dash layout which by webviz-config is added to + the main Webviz application. + ''' + pass diff --git a/webviz_config/containers/_data_table.py b/webviz_config/containers/_data_table.py index e8b12b41..98487009 100644 --- a/webviz_config/containers/_data_table.py +++ b/webviz_config/containers/_data_table.py @@ -2,11 +2,12 @@ from pathlib import Path import pandas as pd import dash_table +from . import WebvizContainer from ..webviz_store import webvizstore from ..common_cache import cache -class DataTable: +class DataTable(WebvizContainer): '''### Data table This container adds a table to the webviz instance, using tabular data from diff --git a/webviz_config/containers/_embed_pdf.py b/webviz_config/containers/_embed_pdf.py index 537c7e75..6e974a07 100644 --- a/webviz_config/containers/_embed_pdf.py +++ b/webviz_config/containers/_embed_pdf.py @@ -1,9 +1,10 @@ from pathlib import Path import dash_html_components as html +from . import WebvizContainer from ..webviz_assets import webviz_assets -class EmbedPdf: +class EmbedPdf(WebvizContainer): '''### Embed PDF file This container embeds a given PDF file into the page. diff --git a/webviz_config/containers/_example_assets.py b/webviz_config/containers/_example_assets.py index 7b95bda3..08e3726a 100644 --- a/webviz_config/containers/_example_assets.py +++ b/webviz_config/containers/_example_assets.py @@ -1,9 +1,10 @@ import dash_html_components as html from pathlib import Path +from . import WebvizContainer from ..webviz_assets import webviz_assets -class ExampleAssets: +class ExampleAssets(WebvizContainer): def __init__(self, app, picture_path: Path): self.asset_url = webviz_assets.add(picture_path) diff --git a/webviz_config/containers/_example_container.py b/webviz_config/containers/_example_container.py index 3b1e6302..925a5ff7 100644 --- a/webviz_config/containers/_example_container.py +++ b/webviz_config/containers/_example_container.py @@ -1,9 +1,10 @@ from uuid import uuid4 import dash_html_components as html from dash.dependencies import Input, Output +from . import WebvizContainer -class ExampleContainer: +class ExampleContainer(WebvizContainer): def __init__(self, app, title: str): self.title = title diff --git a/webviz_config/containers/_example_portable.py b/webviz_config/containers/_example_portable.py index fa21ba85..9264921c 100644 --- a/webviz_config/containers/_example_portable.py +++ b/webviz_config/containers/_example_portable.py @@ -1,9 +1,10 @@ import pandas as pd +from . import WebvizContainer from ..webviz_store import webvizstore from ..common_cache import cache -class ExamplePortable: +class ExamplePortable(WebvizContainer): def __init__(self, some_number: int): self.some_number = some_number diff --git a/webviz_config/containers/_markdown.py b/webviz_config/containers/_markdown.py index b9925dcc..51e720dc 100644 --- a/webviz_config/containers/_markdown.py +++ b/webviz_config/containers/_markdown.py @@ -5,6 +5,7 @@ from markdown.extensions import Extension from markdown.inlinepatterns import ImageInlineProcessor, IMAGE_LINK_RE import dash_core_components as html +from . import WebvizContainer from ..webviz_assets import webviz_assets @@ -58,7 +59,7 @@ def handleMatch(self, match, data): return container, start, index -class Markdown: +class Markdown(WebvizContainer): '''### Include Markdown This container renders and includes the content from a Markdown file. Images diff --git a/webviz_config/containers/_syntax_highlighter.py b/webviz_config/containers/_syntax_highlighter.py index 49fd5318..e0ed8690 100644 --- a/webviz_config/containers/_syntax_highlighter.py +++ b/webviz_config/containers/_syntax_highlighter.py @@ -1,10 +1,11 @@ from uuid import uuid4 from pathlib import Path import dash_core_components as dcc +from . import WebvizContainer from ..webviz_store import webvizstore -class SyntaxHighlighter: +class SyntaxHighlighter(WebvizContainer): '''### Syntax highlighter This container adds support for syntax highlighting of code. Language is diff --git a/webviz_config/containers/_table_plotter.py b/webviz_config/containers/_table_plotter.py index 1e0a0be2..b82b12a2 100644 --- a/webviz_config/containers/_table_plotter.py +++ b/webviz_config/containers/_table_plotter.py @@ -6,12 +6,13 @@ import dash_html_components as html import dash_core_components as dcc from dash.dependencies import Input, Output +import plotly_express as px +from . import WebvizContainer from ..webviz_store import webvizstore from ..common_cache import cache -import plotly_express as px -class TablePlotter: +class TablePlotter(WebvizContainer): '''### TablePlotter This container adds a plotter to the webviz instance, using tabular data from diff --git a/webviz_config/templates/webviz_template.py.jinja2 b/webviz_config/templates/webviz_template.py.jinja2 index 49f15b6a..e748102c 100644 --- a/webviz_config/templates/webviz_template.py.jinja2 +++ b/webviz_config/templates/webviz_template.py.jinja2 @@ -29,6 +29,11 @@ app.css.config.serve_locally = True app.scripts.config.serve_locally = True app.config.suppress_callback_exceptions = True +app.webviz_settings = { + 'portable': {{ portable }}, + 'plotly_layout': {{ plotly_layout }} + } + cache.init_app(server) CSP = {{ csp }} diff --git a/webviz_config/themes/_default_theme.py b/webviz_config/themes/_default_theme.py index 2b6709e5..d5725b5a 100644 --- a/webviz_config/themes/_default_theme.py +++ b/webviz_config/themes/_default_theme.py @@ -9,3 +9,16 @@ 'default_assets', '*') ) + +default_theme.plotly_layout = { + 'paper_bgcolor': 'rgba(90, 90, 90)', + 'plot_bgcolor': 'rgba(90, 90, 90)', + 'colorway': ['#14213d', + '#3a2d58', + '#69356a', + '#9a3a6f', + '#c84367', + '#ea5954', + '#fe7c37', + '#ffa600'] + } diff --git a/webviz_config/themes/_theme_class.py b/webviz_config/themes/_theme_class.py index 28d937f2..f44a3b74 100644 --- a/webviz_config/themes/_theme_class.py +++ b/webviz_config/themes/_theme_class.py @@ -31,9 +31,9 @@ class WebvizConfigTheme: - """Webviz config themes are all instances of this class. The only mandatory + '''Webviz config themes are all instances of this class. The only mandatory property is the theme name set at initialization. - """ + ''' def __init__(self, theme_name): self.theme_name = theme_name @@ -42,15 +42,17 @@ def __init__(self, theme_name): self._feature_policy = FEATURE_POLICY self._external_stylesheets = [] self._assets = [] + self._plotly_layout = {} def adjust_csp(self, dictionary, append=True): - """If the default CSP settings needs to be changed, this function can + '''If the default CSP settings needs to be changed, this function can be called by giving in a dictionary with key-value pairs which should be changed. If `append=True`, the CSP sources given in the dictionary are appended to the whitelisted sources in the default configuration. If not the input value source list is instead replacing the default whitelisted sources. - """ + ''' + for key, value in dictionary.items(): if append and key in self._csp: self._csp[key] += value @@ -59,23 +61,32 @@ def adjust_csp(self, dictionary, append=True): @property def csp(self): - """Returns the content security policy settings for the theme""" + '''Returns the content security policy settings for the theme.''' return self._csp @property def feature_policy(self): - """Returns the feature policy settings for the theme""" + '''Returns the feature policy settings for the theme.''' return self._feature_policy + @property + def plotly_layout(self): + return self._plotly_layout + + @plotly_layout.setter + def plotly_layout(self, plotly_layout): + '''Layout object of Plotly graph objects.''' + self._plotly_layout = plotly_layout + @property def external_stylesheets(self): return self._external_stylesheets @external_stylesheets.setter def external_stylesheets(self, external_stylesheets): - """Set optional external stylesheets to be used in the Dash + '''Set optional external stylesheets to be used in the Dash application. The input variable `external_stylesheets` should be - a list.""" + a list.''' self._external_stylesheets = external_stylesheets @property @@ -84,9 +95,9 @@ def assets(self): @assets.setter def assets(self, assets): - """Set optional theme assets to be copied over to the `./assets` folder + '''Set optional theme assets to be copied over to the `./assets` folder when the webviz dash application is created. The input variable `assets` should be a list of absolute file paths to the different assets. - """ + ''' self._assets = assets