diff --git a/CHANGES.rst b/CHANGES.rst index 6114225f1..c2cbeb354 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -12,6 +12,7 @@ New features and enhancements * `xclim` now has a dedicated console command for prefetching testing data from `xclim-testdata` with branch options (e.g.: `$ xclim prefetch_testing_data --branch some_development_branch`). This command can be used to download the testing data to a local cache, which can then be used to run the testing suite without internet access or in "offline" mode. For more information, see the contributing documentation section for `Updating Testing Data`. (:issue:`1468`, :pull:`1473`). * The testing suite now offers a means of running tests in "offline" mode (using `pytest-socket `_ to block external connections). This requires a local copy of `xclim-testdata` to be present in the user's home cache directory and for certain `pytest` options and markers to be set when invoked. For more information, see the contributing documentation section for `Running Tests in Offline Mode`. (:issue:`1468`, :pull:`1473`). * The `SKIP_NOTEBOOKS` flag to speed up docs builds is now documented. See the contributing documentation section `Get Started!` for details. (:issue:`1470`, :pull:`1476`). +* Refactored the indicators page with the addition of a search bar. Bug fixes ^^^^^^^^^ diff --git a/Makefile b/Makefile index a691fd060..066a85af5 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ define BROWSER_PYSCRIPT import os, webbrowser, sys from urllib.request import pathname2url -webbrowser.open(f"file://{pathname2url(os.path.abspath(sys.argv[1]))}") +webbrowser.open(sys.argv[1]) endef export BROWSER_PYSCRIPT @@ -93,7 +93,11 @@ linkcheck: autodoc-custom-index ## run checks over all external links found thro docs: autodoc-custom-index ## generate Sphinx HTML documentation, including API docs, but without indexes for for indices and indicators $(MAKE) -C docs html ifndef READTHEDOCS - $(BROWSER) docs/_build/html/index.html + ## Start http server and show in browser. + ## We want to have the cli command run in the foreground, so it's easy to kill. + ## And we wait 2 sec for the server to start before opening the browser. + \{ sleep 2; $(BROWSER) http://localhost:54345 \} & + python -m http.server 54345 --directory docs/_build/html/ endif servedocs: docs ## compile the docs watching for changes diff --git a/docs/_static/indsearch.js b/docs/_static/indsearch.js new file mode 100644 index 000000000..cbaacbac5 --- /dev/null +++ b/docs/_static/indsearch.js @@ -0,0 +1,95 @@ +/* Array of indicator objects */ +let indicators = []; + +/* MiniSearch object defining search mechanism */ +let miniSearch = new MiniSearch({ + fields: ['title', 'abstract', 'variables', 'keywords'], // fields to index for full-text search + storeFields: ['title', 'abstract', 'vars', 'realm', 'module', 'name', 'keywords'], // fields to return with search results + searchOptions: { + boost: {'title': 3, 'variables': 2}, + fuzzy: 0.1, + prefix: true, + }, + extractField: (doc, field) => { + if (field === 'variables') { + return Object.keys(doc['vars']).join(' '); + } + return MiniSearch.getDefault('extractField')(doc, field); + } +}); + +// Populate search object with complete list of indicators +fetch('indicators.json') + .then(data => data.json()) + .then(data => { + indicators = Object.entries(data).map(([k, v]) => { + return {id: k.toLowerCase(), ...v} + }); + miniSearch.addAll(indicators); + indFilter(); + }); + + +// Populate list of variables +//function getVariables() { +// fetch('variables.json') +// .then((res) => { +// return res.json(); +// }) +//} +//const variables = getVariables(); + + +function makeKeywordLabel(ind) { + /* Print list of keywords only if there is at least one. */ + if (ind.keywords[0].length > 0) { + const keywords = ind.keywords.map(v => `${v.trim()}`).join(''); + return `
Keywords: ${keywords}
`; + } + else { + return ""; + } +} + + +function makeVariableList(ind) { + /* Print list of variables and include mouse-hover tooltip with variable description. */ + return Object.entries(ind.vars).map((kv) => { + const tooltip = ``; + return tooltip + }).join(''); +} + +function indTemplate(ind) { + // const varlist = Object.entries(ind.vars).map((kv) => `${kv[0]}`).join(''); + const varlist = makeVariableList(ind); + return ` +
+ +
Uses: ${varlist}
+

${ind.abstract}

+ ${makeKeywordLabel(ind)} +
Yaml ID: ${ind.id}
+
+ `; +} + +function indFilter() { + const input = document.getElementById("queryInput").value; + let inds = []; + if (input === "") { + inds = indicators; + } else { + inds = miniSearch.search(input); + } + + const newTable = inds.map(indTemplate).join(''); + const tableElem = document.getElementById("indTable"); + tableElem.innerHTML = newTable; + return newTable; +} diff --git a/docs/_static/style.css b/docs/_static/style.css index 0f2390dae..5babe9e93 100644 --- a/docs/_static/style.css +++ b/docs/_static/style.css @@ -65,3 +65,36 @@ ul.simple li { #contents .toctree-wrapper:first-of-type ul { margin-bottom: 0; } + +#queryInput { + width: 100%; + padding: 10px; + margin: 5px; +} + +.indElem { + margin: 10px; + padding: 10px; + background-color: #ddd; + border-radius: 10px; +} + +.indName { + float: right; +} + +code > .indName { + background-color: #ccc; +} + +.indVarname { + border-radius: 10px; +} + +/* Rounded corners for keyword labels: */ +.keywordlabel { + border-radius: 10px; + padding: 5px; + margin: 5px; + background-color: #ddd; +} diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html index 4c57ba830..f00ef0ad7 100644 --- a/docs/_templates/layout.html +++ b/docs/_templates/layout.html @@ -1,2 +1,16 @@ {% extends "!layout.html" %} {% set css_files = css_files + ["_static/style.css"] %} + + + +{% block scripts %} +{% if "indicators" in sourcename %} + + +{% endif %} +{{ super() }} +{% endblock %} diff --git a/docs/conf.py b/docs/conf.py index 368f44cc2..22ef76a68 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -14,12 +14,13 @@ from __future__ import annotations import datetime +import json import os import sys import warnings -from collections import OrderedDict import xarray +import yaml from pybtex.plugin import register_plugin # noqa from pybtex.style.formatting.alpha import Style as AlphaStyle # noqa from pybtex.style.labels import BaseLabelStyle # noqa @@ -29,6 +30,7 @@ xarray.CFTimeIndex.__module__ = "xarray" import xclim # noqa +from xclim.core.indicator import registry # noqa # If extensions (or modules to document with autodoc) are in another # directory, add these directories to sys.path here. If the directory is @@ -38,51 +40,40 @@ sys.path.insert(0, os.path.abspath("..")) sys.path.insert(0, os.path.abspath(".")) - -def _get_indicators(module): - """For all modules or classes listed, return the children that are instances of registered Indicator classes. - - module : A xclim module. - """ - from xclim.core.indicator import registry - - out = {} - for key, val in module.__dict__.items(): - if hasattr(val, "_registry_id") and val._registry_id in registry: # noqa - out[key] = val - - return OrderedDict(sorted(out.items())) - - -def _indicator_table(module): - """Return a sequence of dicts storing metadata about all available indices in xclim.""" - inds = _get_indicators(getattr(xclim.indicators, module)) - table = {} - for ind_name, ind in inds.items(): - # Apply default values - # args = { - # name: p.default if p.default != inspect._empty else f"<{name}>" - # for (name, p) in ind._sig.parameters.items() - # } - try: - table[ind_name] = ind.json() # args? - except KeyError as err: - warnings.warn( - f"{ind.identifier} could not be documented.({err})", UserWarning - ) - else: - table[ind_name]["doc"] = ind.__doc__ - if ind.compute.__module__.endswith("generic"): - table[ind_name][ - "function" - ] = f"xclim.indices.generic.{ind.compute.__name__}" - else: - table[ind_name]["function"] = f"xclim.indices.{ind.compute.__name__}" - return table - - -modules = ("atmos", "generic", "land", "seaIce", "cf", "icclim", "anuclim") -indicators = {module: _indicator_table(module) for module in modules} +# Indicator data for populating the searchable indicators page +# Get all indicators and some information about them +indicators = {} +# FIXME: Include cf module when its indicators documentation is improved. +for module in ("atmos", "generic", "land", "seaIce", "icclim", "anuclim"): + for key, ind in getattr(xclim.indicators, module).__dict__.items(): + if hasattr(ind, "_registry_id") and ind._registry_id in registry: # noqa + indicators[ind._registry_id] = { + "realm": ind.realm, + "title": ind.title, + "name": key, + "module": module, + "abstract": ind.abstract, + "vars": { + param_name: f"{param.description}" + for param_name, param in ind._all_parameters.items() + if param.kind < 2 and not param.injected + }, + "keywords": ind.keywords.split(","), + } +# Sort by title +indicators = dict(sorted(indicators.items(), key=lambda kv: kv[1]["title"])) + +# Dump indicators to json. The json is added to the html output (html_extra_path) +# It is read by _static/indsearch.js to populate the table in indicators.rst +with open("indicators.json", "w") as f: + json.dump(indicators, f) + + +# Dump variables information +with open("variables.json", "w") as fout: + with open("../xclim/data/variables.yml") as fin: + data = yaml.safe_load(fin) + json.dump(data, fout) # -- General configuration --------------------------------------------- @@ -233,6 +224,7 @@ class XCStyle(AlphaStyle): """ nbsphinx_timeout = 300 nbsphinx_allow_errors = False +# nbsphinx_requirejs_path = "" # To make MiniSearch work in the indicators page # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] @@ -294,7 +286,7 @@ class XCStyle(AlphaStyle): html_theme = "sphinx_rtd_theme" -html_context = {"indicators": indicators} +html_extra_path = ["indicators.json", "variables.json"] # Theme options are theme-specific and customize the look and feel of a # theme further. For a list of options available for each theme, see the diff --git a/docs/indicators.rst b/docs/indicators.rst index e6481f79f..be46e0007 100644 --- a/docs/indicators.rst +++ b/docs/indicators.rst @@ -12,135 +12,17 @@ that conform as much as possible with the `CF-Convention`_. Indicators are split into realms (atmos, land, seaIce), according to the variables they operate on. See :ref:`notebooks/extendxclim:Defining new indicators` for instruction on how to create your own indicators. This page -lists all indicators with a summary description, click on the names to get to the complete docstring of each indicator. - -atmos: Atmosphere -================= - -.. raw:: html - -
- {% for indname, ind in indicators['atmos'].items() %} -
atmos.{{ indname | safe}} : {{ ind.title }}
-
- {% if ind.identifier != indname %}Id: {{ ind.identifier }}
{% endif %} - Description: {{ ind.abstract }}
- Based on {{ ind.function }} 
- Produces: {% for var in ind['outputs'] %} {{ var['var_name'] }}: {{ var['long_name'] }} [{{ var['units'] }}] {% endfor %} -
- {% endfor %} -
- - -land: Land surface -================== - -.. raw:: html - -
- {% for indname, ind in indicators['land'].items() %} -
land.{{ indname | safe}} : {{ ind.title }}
-
- {% if ind.identifier != indname %}Id: {{ ind.identifier }}
{% endif %} - Description: {{ ind.abstract }}
- Based on {{ ind.function }} 
- Produces: {% for var in ind['outputs'] %} {{ var['var_name'] }}: {{ var['long_name'] }} [{{ var['units'] }}] {% endfor %} -
- {% endfor %} -
- - -seaIce: Sea ice -=============== +allows a simple free text search of all indicators. Click on the python names to get to the complete docstring of each indicator. .. raw:: html -
- {% for indname, ind in indicators['seaIce'].items() %} -
seaIce.{{ indname | safe}} : {{ ind.title }}
-
- {% if ind.identifier != indname %}Id: {{ ind.identifier }}
{% endif %} - Description: {{ ind.abstract }}
- Based on {{ ind.function }} 
- Produces: {% for var in ind['outputs'] %} {{ var['var_name'] }}: {{ var['long_name'] }} [{{ var['units'] }}] {% endfor %} -
- {% endfor %} -
- - -generic: Generic indicators -=========================== - -Indicators in this "realm" do not have units assigned to their inputs. They provide useful functions that might apply to many different types of data. They are most useful for building custom "indicator modules", see :ref:`indicators:Virtual submodules`. - -.. raw:: html + -
- {% for indname, ind in indicators['generic'].items() %} -
generic.{{ indname | safe}} : {{ ind.title }}
-
- {% if ind.identifier != indname %}Id: {{ ind.identifier }}
{% endif %} - Description: {{ ind.abstract }}
- Based on {{ ind.function }} 
- Produces: {% for var in ind['outputs'] %} {{ var['var_name'] }}: {{ var['long_name'] }} [{{ var['units'] }}] {% endfor %} -
- {% endfor %} -
+
+
+.. + Filling of the table and search is done by scripts in _static/indsearch.js which are added through _templates/layout.html + the data comes from indicators.json which is created by conf.py. .. _CF-Convention: http://cfconventions.org/ - - -Virtual submodules -================== - -.. automodule:: xclim.indicators.cf - :noindex: - -.. raw:: html - -
- {% for indname, ind in indicators['cf'].items() %} -
cf.{{ indname | safe}} : {{ ind.title }}
-
- {% if ind.identifier != indname %}Id: {{ ind.identifier }}
{% endif %} - Description: {{ ind.abstract }}
- Based on {{ ind.function }} 
- Produces: {% for var in ind['outputs'] %} {{ var['var_name'] }}: {{ var['long_name'] }} [{{ var['units'] }}] {% endfor %} -
- {% endfor %} -
- -.. automodule:: xclim.indicators.icclim - :noindex: - -.. raw:: html - -
- {% for indname, ind in indicators['icclim'].items() %} -
icclim.{{ indname | safe}} : {{ ind.title }}
-
- {% if ind.identifier != indname %}Id: {{ ind.identifier }}
{% endif %} - Description: {{ ind.abstract }}
- Based on {{ ind.function }} 
- Produces: {% for var in ind['outputs'] %} {{ var['var_name'] }}: {{ var['long_name'] }} [{{ var['units'] }}] {% endfor %} -
- {% endfor %} -
- -.. automodule:: xclim.indicators.anuclim - :noindex: - -.. raw:: html - -
- {% for indname, ind in indicators['anuclim'].items() %} -
anuclim.{{ indname | safe}} : {{ ind.title }}
-
- {% if ind.identifier != indname %}Id: {{ ind.identifier }}
{% endif %} - Description: {{ ind.abstract }}
- Based on {{ ind.function }} 
- Produces: {% for var in ind['outputs'] %} {{ var['var_name'] }}: {{ var['long_name'] }} [{{ var['units'] }}] {% endfor %} -
- {% endfor %} -
diff --git a/xclim/core/indicator.py b/xclim/core/indicator.py index eba93350f..4aeb7bb2a 100644 --- a/xclim/core/indicator.py +++ b/xclim/core/indicator.py @@ -1140,6 +1140,7 @@ def _translate(cf_attrs, names, var_id=None): ) return attrs + @classmethod def json(self, args=None): """Return a serializable dictionary representation of the class. diff --git a/xclim/indicators/atmos/_conversion.py b/xclim/indicators/atmos/_conversion.py index 6876755f2..f1ccab51d 100644 --- a/xclim/indicators/atmos/_conversion.py +++ b/xclim/indicators/atmos/_conversion.py @@ -56,6 +56,7 @@ def cfcheck(self, **das): long_name="Humidex index", description="Humidex index describing the temperature felt by the average person in response to relative humidity.", cell_methods="", + keywords="heatwave", abstract="The humidex describes the temperature felt by a person when relative humidity is taken into account. " "It can be interpreted as the equivalent temperature felt when the air is dry.", compute=indices.humidex, @@ -370,7 +371,7 @@ def cfcheck(self, **das): corn_heat_units = Converter( - title=" Corn heat units", + title="Corn heat units", identifier="corn_heat_units", units="", long_name="Corn heat units (Tmin > {thresh_tasmin} and Tmax > {thresh_tasmax})", diff --git a/xclim/indicators/atmos/_precip.py b/xclim/indicators/atmos/_precip.py index 1d0e98ac1..988a09476 100644 --- a/xclim/indicators/atmos/_precip.py +++ b/xclim/indicators/atmos/_precip.py @@ -563,7 +563,7 @@ class HrPrecip(Hourly): # FIXME: Are fraction_over_precip_thresh and fraction_over_precip_doy_thresh the same thing? # FIXME: Clarity needed in both French and English metadata fields fraction_over_precip_doy_thresh = PrecipWithIndexing( - title="", + title="Fraction of precipitation due to wet days with daily precipitation over a given daily percentile.", identifier="fraction_over_precip_doy_thresh", long_name="Fraction of precipitation due to days with daily precipitation above {pr_per_thresh}th daily percentile", description="{freq} fraction of total precipitation due to days with precipitation above {pr_per_thresh}th daily "