Source code for gwsumm.tabs.builtin

# -*- coding: utf-8 -*-
# Copyright (C) Duncan Macleod (2013)
#
# This file is part of GWSumm
#
# GWSumm is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# GWSumm is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with GWSumm.  If not, see <http://www.gnu.org/licenses/>

"""This module defines a number of `Tab` subclasses.

The `builtin` classes provide interfaces for simple operations including

- `ExternalTab`: embedding an existing webpage
- `PlotTab`: scaffolding a collection of existing images
- `StateTab`: scaffolding plots split into a number of states, with
  a button to switch between states in the HTML

"""

import os.path
import warnings
from configparser import NoOptionError

from MarkupPy import markup
from markdown import markdown

from .registry import (get_tab, register_tab)
from ..plot import get_plot
from ..utils import (re_quote, re_cchar)
from ..config import GWSummConfigParser
from ..state import (ALLSTATE, SummaryState, get_state)
from ..html.static import (CSS, JS)
from .. import html

__author__ = 'Duncan Macleod <duncan.macleod@ligo.org>'
__all__ = ['ExternalTab', 'PlotTab', 'StateTab']

Tab = get_tab('basic')
SummaryPlot = get_plot(None)
DataPlot = get_plot('data')


[docs] class ExternalTab(Tab): """A simple tab to link HTML from an external source Parameters ---------- name : `str` name of this tab (required) url : `str` URL of the external content to be linked into this tab. index : `str` HTML file in which to write. By default each tab is written to an index.html file in its own directory. Use :attr:`~Tab.index` to find out the default index, if not given. shortname : `str` shorter name for this tab to use in the navigation bar. By default the regular name is used parent : :class:`~gwsumm.tabs.Tab` parent of this tab. This is used to position this tab in the navigation bar. children : `list` list of child :class:`Tabs <~gwsumm.tabs.Tab>` of this one. This is used to position this tab in the navigation bar. group : `str` name of containing group for this tab in the navigation bar dropdown menu. This is only relevant if this tab has a parent. path : `str` base output directory for this tab (should be the same directory for all tabs in this run). Configuration ------------- """ type = 'external' def __init__(self, name, url, error=True, success=None, **kwargs): """Initialise a new `ExternalTab`. """ super(ExternalTab, self).__init__(name, **kwargs) self.url = url self.error = error self.success = success @property def url(self): return self._url @url.setter def url(self, link): self._url = link
[docs] @classmethod def from_ini(cls, cp, section, *args, **kwargs): """Configure a new `ExternalTab` from a `ConfigParser` section Parameters ---------- cp : :class:`~gwsumm.config.ConfigParser` configuration to parse. section : `str` name of section to read See Also -------- Tab.from_ini : for documentation of the standard configuration options Notes ----- On top of the standard configuration options, the `ExternalTab` can be configured with the ``url`` option, specifying the URL of the external content to be included: .. code-block:: ini [tab-external] name = External data type = external url = https://www.example.org/index.html """ url = cp.get(section, 'url') if cp.has_option(section, 'error'): kwargs.setdefault( 'error', re_quote.sub('', cp.get(section, 'error'))) if cp.has_option(section, 'success'): kwargs.setdefault( 'success', re_quote.sub('', cp.get(section, 'success'))) return super(ExternalTab, cls).from_ini(cp, section, url, *args, **kwargs)
[docs] def html_content(self, content): wrappedcontent = html.load(self.url, id_='content', error=self.error, success=self.success) return super(ExternalTab, self).html_content(wrappedcontent)
[docs] def write_html(self, **kwargs): """Write the HTML page for this tab. See Also -------- gwsumm.tabs.Tab.write_html : for details of all valid keyword arguments """ if not kwargs.pop('writehtml', True): return kwargs.setdefault('footer', self.url.split()[0]) return super(ExternalTab, self).write_html('', **kwargs)
register_tab(ExternalTab)
[docs] class PlotTab(Tab): """A simple tab to layout some figures in the #main div. Parameters ---------- name : `str` name of this tab (required) plots : `list`, optional list of plots to display on this tab. More plots can be added at any time via :meth:`PlotTab.add_plot` layout : `int`, `list`, optional the number of plots to display in each row, or a list of numbers to define each row individually. If the number of plots defined by the layout is less than the total number of plots, the layout for the final row will be repeated as necessary. For example ``layout=[1, 2, 3]`` will display a single plot on the top row, two plots on the second, and 3 plots on each row thereafter. foreword : `~MarkupPy.markup.page`, `str`, optional content to include in the #main HTML before the plots afterword : `~MarkupPy.markup.page`, `str`, optional content to include in the #main HTML after the plots index : `str`, optional HTML file in which to write. By default each tab is written to an index.html file in its own directory. Use :attr:`~Tab.index` to find out the default index, if not given. shortname : `str`, optional shorter name for this tab to use in the navigation bar. By default the regular name is used parent : :class:`~gwsumm.tabs.Tab`, optional parent of this tab. This is used to position this tab in the navigation bar. children : `list`, optional list of child :class:`Tabs <~gwsumm.tabs.Tab>` of this one. This is used to position this tab in the navigation bar. group : `str`, optional name of containing group for this tab in the navigation bar dropdown menu. This is only relevant if this tab has a parent. path : `str`, optional, base output directory for this tab (should be the same directory for all tabs in this run). """ type = 'plots' def __init__(self, name, plots=list(), layout=None, foreword=None, afterword=None, **kwargs): """Initialise a new :class:`PlotTab`. """ super(PlotTab, self).__init__(name, **kwargs) self.plots = [] for p in plots: self.add_plot(p) self.set_layout(layout) self.foreword = foreword self.afterword = afterword @property def layout(self): """List of how many plots to display on each row in the output. By default this is ``1`` if the tab contains only 1 or 3 plots, or ``2`` if otherwise. The final number given in the list will be repeated as necessary. :type: `list` of `ints <int>` """ return self._layout @layout.setter def layout(self, layout_): warnings.warn("Use of `Tab.layout = ...` has been deprecated, " "please switch to using `Tab.set_layout(...)`", DeprecationWarning) self.set_layout(layout_)
[docs] def set_layout(self, layout): """Set the plot scaffolding layout for this tab Parameters ---------- l : `int`, `list` of `int` the desired scaffold layout, one of - an `int`, indicating the number of plots on every row, or - a `list`, indicating the number of plots on each row, with the final `int` repeated for any remaining rows; each entry should be an `int` or a pair of `int` indicating the number of plots on this row AND the desired the scaling of this row, see the examples... Examples -------- To layout 2 plots on each row >>> tab.set_layout(2) or >>> tab.set_layout([2]) To layout 2 plots on the first row, and 3 on all other rows >>> tab.set_layout((2, 3)) To layout 2 plots on the first row, and 1 on the second row BUT have it the same size as plots on a 2-plot row >>> tab.set_layout((2, (1, 2)) """ # shortcut None if layout is None: self._layout = None return # parse a single int if isinstance(layout, int) or ( isinstance(layout, str) and layout.isdigit() ): self._layout = [int(layout)] return # otherwise parse as a list of ints or pairs of ints if isinstance(layout, str): layout = layout.split(',') self._layout = [] for item in layout: if isinstance(item, int): self._layout.append(item) continue if isinstance(item, str): item = item.strip('([').rstrip(')]').split(',') if not isinstance(item, (list, tuple)) or not len(item) == 2: raise ValueError("Cannot parse layout element %r (%s)" % (item, type(item))) self._layout.append(tuple(map(int, item)))
@property def foreword(self): """HTML content to be included before the plots """ return self._pre @foreword.setter def foreword(self, content): if isinstance(content, markup.page) or content is None: self._pre = content else: self._pre = markup.page() self._pre.add(markdown(str(content))) @property def afterword(self): """HTML content to be included after the plots """ return self._post @afterword.setter def afterword(self, content): if isinstance(content, markup.page) or content is None: self._post = content else: self._post = markup.page() self._post.add(markdown(str(content)))
[docs] @classmethod def from_ini(cls, cp, section, *args, **kwargs): """Define a new tab from a :class:`~gwsumm.config.GWConfigParser` Parameters ---------- cp : :class:`~ConfigParser.GWConfigParser` customised configuration parser containing given section section : `str` name of section to parse Returns ------- tab : `PlotTab` a new tab defined from the configuration """ cp = GWSummConfigParser.from_configparser(cp) kwargs.setdefault('path', '') # get layout if cp.has_option(section, 'layout'): try: layout = eval(cp.get(section, 'layout')) except NameError: raise ValueError("Cannot parse 'layout' for '%s' tab. Layout " "should be given as a comma-separated list " "of integers") if isinstance(layout, int): layout = [layout] for item in layout: if isinstance(item, (tuple, list)): item = item[0] if item > 12: raise ValueError("Cannot print more than 12 plots in a " "single row. The chosen layout value for " "each row must be a divisor of 12 to fit " "the Bootstrap scaffolding. For details " "see http://getbootstrap.com/2.3.2/" "scaffolding.html") else: layout = None kwargs.setdefault('layout', layout) # get plots if 'plots' not in kwargs: kwargs['plots'] = [] for idx, url in sorted([(int(opt), url) for (opt, url) in cp.nditems(section) if opt.isdigit()], key=lambda x: x[0]): plot = SummaryPlot(href=url) plot.new = False # this plot does not need to be generated # get caption try: plot.caption = re_quote.sub( '', cp.get(section, '%d-caption' % idx)) except NoOptionError: pass kwargs['plots'].append(plot) # get content try: kwargs.setdefault('foreword', cp.get(section, 'foreword')) except NoOptionError: pass try: kwargs.setdefault('afterword', cp.get(section, 'afterword')) except NoOptionError: pass # build and return tab return super(PlotTab, cls).from_ini(cp, section, *args, **kwargs)
[docs] def add_plot(self, plot): """Add a plot to this tab. Parameters ---------- plot : `str`, :class:`~gwsumm.plot.SummaryPlot` either the URL of a plot to embed, or a formatted `SummaryPlot` object. """ if isinstance(plot, str): plot = SummaryPlot(href=plot) plot.new = False if not isinstance(plot, SummaryPlot): raise TypeError("Cannot append plot of type %r" % type(plot)) self.plots.append(plot)
[docs] def scaffold_plots(self, plots=None, state=None, layout=None, aclass='fancybox', **fbkw): """Build a grid of plots using bootstrap's scaffolding. Returns ------- page : :class:`~MarkupPy.markup.page` formatted markup with grid of plots """ page = markup.page() page.div(class_='card border-light card-body scaffold shadow-sm') if plots is None: if state: plots = [p for p in self.plots if not isinstance(p, DataPlot) or p.state in [state, None]] else: plots = self.plots # get layout if layout is None: if self.layout: layout = list(self.layout) else: layout = len(plots) == 1 and [1] or [2] for i, item in enumerate(layout): if isinstance(item, (list, tuple)): layout[i] = item elif isinstance(item, int): layout[i] = (item, None) else: raise ValueError("Cannot parse layout element '%s'." % item) while sum(list(zip(*layout))[0]) < len(plots): layout.append(layout[-1]) k = i = 0 fbkw.setdefault('rel', 'fancybox') fbkw.setdefault('target', '_blank') fbkw.setdefault('data-fancybox', 'gallery') fbkw.setdefault('data-fancybox-group', 'images') for j, plot in enumerate(plots): # start new row if i == 0: page.div(class_='row') # determine relative size if layout[k][1]: colwidth = 12 // int(layout[k][1]) remainder = 12 - colwidth * layout[k][0] if remainder % 2: raise ValueError("Cannot center column of width %d in a " "12-column format" % colwidth) else: offset = remainder / 2 page.div(class_='col-md-%d offset-md-%d' % (colwidth, offset)) else: colwidth = 12 // int(layout[k][0]) page.div(class_='col-md-%d' % colwidth) if plot.src.endswith('svg'): fbkw['data-fancybox-type'] = 'iframe' page.a(href='%s?iframe' % plot.href.replace('.svg', '.html'), class_=aclass, **fbkw) else: fbkw['title'] = plot.caption fbkw['data-caption'] = plot.caption page.a(href=plot.href, class_=aclass, **fbkw) page.img( class_='img-fluid w-100', src=plot.src.replace('.pdf', '.png') if ( plot.src.endswith('.pdf')) else plot.src, ) page.a.close() page.div.close() # detect end of row if (i + 1) == layout[k][0]: i = 0 k += 1 page.div.close() # detect last plot elif j == (len(plots) - 1): page.div.close() break # or move to next column else: i += 1 page.div.close() return page
[docs] def html_content(self, content): page = markup.page() if self.foreword: page.add(str(self.foreword)) if content: page.add(str(content)) page.add(str(self.scaffold_plots())) if self.afterword: page.add(str(self.afterword)) return Tab.html_content(str(page))
[docs] def write_html(self, foreword=None, afterword=None, **kwargs): """Write the HTML page for this tab. Parameters ---------- foreword : `str`, :class:`~MarkupPy.markup.page`, optional content to place above the plot grid, defaults to :attr:`PlotTab.foreword` afterword : `str`, :class:`~MarkupPy.markup.page`, optional content to place below the plot grid, defaults to :attr:`PlotTab.afterword` **kwargs other keyword arguments to be passed through :meth:`~Tab.write_html` See Also -------- gwsumm.tabs.Tab.write_html : for details of all valid unnamed keyword arguments """ if not kwargs.pop('writehtml', True): return if foreword is not None: self.foreword = foreword if afterword is not None: self.afterword = self.afterword return super(PlotTab, self).write_html(None, **kwargs)
register_tab(PlotTab)
[docs] class StateTab(PlotTab): """Tab with multiple content pages defined via 'states' Each state is printed to its own HTML file which is loaded via javascript upon request into the #main div of the index for the tab. Parameters ---------- name : `str` name of this tab (required) states : `list` a list of states for this tab. Each state can take any form, but must be castable to a `str` in order to be printed. plots : `list` list of plots to display on this tab. More plots can be added at any time via :meth:`PlotTab.add_plot` layout : `int`, `list` the number of plots to display in each row, or a list of numbers to define each row individually. If the number of plots defined by the layout is less than the total number of plots, the layout for the final row will be repeated as necessary. For example ``layout=[1, 2, 3]`` will display a single plot on the top row, two plots on the second, and 3 plots on each row thereafter. index : `str` HTML file in which to write. By default each tab is written to an index.html file in its own directory. Use :attr:`~Tab.index` to find out the default index, if not given. shortname : `str` shorter name for this tab to use in the navigation bar. By default the regular name is used parent : :class:`~gwsumm.tabs.Tab` parent of this tab. This is used to position this tab in the navigation bar. children : `list` list of child :class:`Tabs <~gwsumm.tabs.Tab>` of this one. This is used to position this tab in the navigation bar. group : `str` name of containing group for this tab in the navigation bar dropdown menu. This is only relevant if this tab has a parent. path : `str` base output directory for this tab (should be the same directory for all tabs in this run). """ type = 'state' def __init__(self, name, states=list(), **kwargs): """Initialise a new `Tab`. """ if kwargs.get('mode', None) is None: raise ValueError("%s() needs keyword argument 'mode'" % type(self).__name__) super(StateTab, self).__init__(name, **kwargs) # process states if not isinstance(states, (tuple, list)): states = [states] self.states = states # ------------------------------------------- # StateTab properties @property def states(self): """The set of :class:`states <gwsumm.state.SummaryState>` over whos times this tab's data will be processed. The set of states will be linked in the given order with a switch on the far-right of the HTML navigation bar. """ return self._states @states.setter def states(self, statelist, default=None): self._states = [] for state in statelist: # allow default indication by trailing asterisk if state == default: default_ = True elif (default is None and isinstance(state, str) and state.endswith('*')): state = state[:-1] default_ = True else: default_ = False self.add_state(state, default=default_)
[docs] def add_state(self, state, default=False): """Add a `SummaryState` to this tab Parameters ---------- state : `str`, :class:`~gwsumm.state.SummaryState` either the name of a state, or a `SummaryState`. register : `bool`, default: `False` automatically register all new states """ if not isinstance(state, SummaryState): state = get_state(state) self._states.append(state) if default: self.defaultstate = state
@property def defaultstate(self): try: return self._defaultstate except AttributeError: self._defaultstate = self.states[0] return self._defaultstate @defaultstate.setter def defaultstate(self, state): self._defaultstate = state @property def frames(self): # write page for each state statelinks = [] outdir = os.path.split(self.index)[0] for i, state in enumerate(self.states): statelinks.append(os.path.join( outdir, '%s.html' % re_cchar.sub('_', str(state).lower()))) return statelinks # ------------------------------------------- # StateTab methods
[docs] @classmethod def from_ini(cls, cp, section, *args, **kwargs): # parse states and retrieve their definitions if cp.has_option(section, 'states'): # states listed individually kwargs.setdefault( 'states', [re_quote.sub('', s).strip() for s in cp.get(section, 'states').split(',')]) else: # otherwise use 'all' state - full span with no gaps kwargs.setdefault('states', ['All']) # parse core Tab information return super(StateTab, cls).from_ini(cp, section, *args, **kwargs)
# ------------------------------------------------------------------------ # HTML methods
[docs] def html_navbar(self, help_=None, **kwargs): """Build the navigation bar for this `Tab`. The navigation bar will consist of a switch for this page linked to other interferometer servers, followed by the navbar brand, then the full dropdown-based navigation menus configured for the given ``tabs`` and their descendents. Parameters ---------- help_ : `str`, :class:`~MarkupPy.markup.page` content for upper-right of navbar ifo : `str`, optional prefix for this IFO. ifomap : `dict`, optional `dict` of (ifo, {base url}) pairs to map to summary pages for other IFOs. tabs : `list`, optional list of parent tabs (each with a list of children) to include in the navigation bar. Returns ------- page : `~MarkupPy.markup.page` a markup page containing the navigation bar. """ if len(self.states) > 1 or str(self.states[0]) != ALLSTATE: default = self.states.index(self.defaultstate) help_ = str(html.state_switcher( list(zip(self.states, self.frames)), default)) return super(StateTab, self).html_navbar(help_=help_, **kwargs)
[docs] @staticmethod def html_content(frame): r"""Build the #main div for this tab. In this construction, the <div id="id\_"> is empty, with a javascript hook to load the given frame into the div when ready. """ wrappedcontent = html.load(frame, id_='content') return Tab.html_content(str(wrappedcontent))
[docs] def write_state_html(self, state, pre=None, post=None, plots=True): """Write the frame HTML for the specific state of this tab Parameters ---------- state : `~gwsumm.state.SummaryState` `SummaryState` over which to generate inner HTML """ # build page page = markup.page() if pre: page.add(str(pre)) if plots: page.add(str(self.scaffold_plots(state=state))) if post: page.add(str(post)) # write to file idx = self.states.index(state) with open(self.frames[idx], 'w') as fobj: fobj.write(str(page)) return self.frames[idx]
[docs] def write_html(self, title=None, subtitle=None, tabs=list(), ifo=None, ifomap=dict(), help_=None, css=CSS, js=JS, about=None, footer=None, **inargs): """Write the HTML page for this state Tab. Parameters ---------- maincontent : `str`, :class:`~MarkupPy.markup.page` simple string content, or a structured `page` of markup to embed as the content of the #main div. title : `str`, optional, default: {parent.name} level 1 heading for this `Tab`. subtitle : `str`, optional, default: {self.name} level 2 heading for this `Tab`. tabs: `list`, optional list of top-level tabs (with children) to populate navbar ifo : `str`, optional prefix for this IFO. ifomap : `dict`, optional `dict` of (ifo, {base url}) pairs to map to summary pages for other IFOs. help_ : `str`, :class:`~MarkupPy.markup.page`, optional non-menu content for navigation bar, defaults to calendar css : `list`, optional list of resolvable URLs for CSS files. See `gwsumm.html.static.CSS` for the default list. js : `list`, optional list of resolvable URLs for javascript files. See `gwumm.html.JS` for the default list. about : `str`, optional href for the 'About' page footer : `str`, `~MarkupPy.markup.page` user-defined content for the footer (placed below everything else) **inargs other keyword arguments to pass to the :meth:`~Tab.build_inner_html` method """ default = self.states.index(self.defaultstate) return super(PlotTab, self).write_html( self.frames[default], title=title, subtitle=subtitle, tabs=tabs, ifo=ifo, ifomap=ifomap, help_=help_, css=css, js=js, about=about, footer=footer, **inargs)
register_tab(StateTab) class UrlTab(Tab): type = 'link' def __init__(self, name, url, **kwargs): super(UrlTab, self).__init__(name, **kwargs) self.href = url @property def href(self): return self._href @href.setter def href(self, url): self._href = url @classmethod def from_ini(cls, cp, section, *args, **kwargs): kwargs.setdefault('url', cp.get(section, 'url')) return super(UrlTab, cls).from_ini(cp, section, *args, **kwargs) def write_html(self, **kwargs): return register_tab(UrlTab)