gwsumm/tabs/builtin.py
# -*- 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')
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
@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)
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)
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)
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_)
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)))
@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)
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)
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
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))
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)
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_)
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
@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
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)
@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))
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]
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)