Browse Source

major refactor to remove histogram and io in favor of using instead matplottery and uproot in applications

Caleb Fangmeier 6 years ago
parent
commit
cdba873a1d

+ 55 - 0
README.md

@@ -0,0 +1,55 @@
+# Matplotboard
+
+A utility to generate html dashboards using matplotlib. Matplotboard makes it easy to
+wrap your plotting functions and embed them into a Markdown document. This is best
+demonstrated with an example.
+
+
+``` python
+import numpy as np
+import matplotlib.pyplot as plt
+
+from matplotboard import (decl_fig, render, generate_report)
+
+
+@decl_fig
+def cool_fig():
+    xs = np.linspace(-10,10, 100)
+    ys = xs**2
+    plt.plot(xs, ys)
+
+
+@decl_fig
+def fig_with_args(amp, freq):
+    '''
+    A plot of a sine wave with configurable amplitude and frequency.
+    '''
+    xs = np.linspace(-np.pi, np.pi, 100)
+    ys = amp*np.sin(xs*freq)
+    plt.plot(xs, ys)
+
+
+if __name__ == '__main__':
+    figures = {'my_cool_fig': cool_fig,
+               'slow': (fig_with_args, (1, np.pi)),
+               'fast': (fig_with_args, (0.75, 3*np.pi)),
+               }
+
+    render(figures)
+    generate_report(figures, 'Report',
+                    source=__file__,
+                    body="""
+# Making **Awesome Dashboards**
+
+Sometimes you just want to push out a static html page with plots and relevant
+commentary. For example, what does a parabola look like?
+
+fig::my_cool_fig
+
+There we go, `matplotboard` also supports plotting functions that take arguments.
+
+fig::slow
+fig::fast
+
+    """)
+```

+ 0 - 0
filval/__init__.py


+ 0 - 103
filval/graph_vals.py

@@ -1,103 +0,0 @@
-import pydotplus.graphviz as pdp
-
-
-def parse(str_in, alias=None):
-    """ Creates a call-tree for the supplied value name
-    """
-    str_in = "("+str_in+")"
-
-    functions = []
-    ends = {}
-    nests = {}
-    names = {}
-    styles = {}
-    parens = []
-    name = ""
-    name_start = 0
-    name_end = 0
-    for i, char in enumerate(str_in):
-        if char == "(":
-            nests[name_start] = []
-            if parens:
-                nests[parens[-1]].append(name_start)
-            names[name_start] = name  # save function name
-            styles[name_start] = {"shape": "ellipse"}
-            name = ""
-            parens.append(name_start)
-        elif char == ")":
-            if name:
-                ends[name_start] = name_end
-                names[name_start] = name
-                styles[name_start] = {"shape": "rectangle"}
-                nests[parens[-1]].append(name_start)
-                name = ""
-            ends[parens.pop()] = i
-        elif char in ",:":
-            if name:
-                ends[name_start] = name_end
-                names[name_start] = name
-                if char == ",":
-                    styles[name_start] = {"shape": "rectangle"}
-                else:
-                    styles[name_start] = {"shape": "invhouse"}
-                    functions.append(name)
-                nests[parens[-1]].append(name_start)
-                name = ""
-        else:
-            if not name:
-                name_start = i
-            name += char
-            name_end = i
-
-    # clean up duplicate sub-trees
-    text = {}
-    for start, end in ends.items():
-        s = str_in[start:end+1]
-        if s in text:
-            dup_id = text[s]
-            names.pop(start)
-            if start in nests:
-                nests.pop(start)
-            for l in nests.values():
-                for i in range(len(l)):
-                    if l[i] == start:
-                        l[i] = dup_id
-        else:
-            text[s] = start
-
-    names.pop(0)
-    nests.pop(0)
-
-    g = pdp.Dot()
-
-    for id_, name in names.items():
-        g.add_node(pdp.Node(str(id_), label=name, **styles[id_]))
-    for group_id, children in nests.items():
-        for child_id in children:
-            g.add_edge(pdp.Edge(str(group_id), str(child_id)))
-    if alias:
-        g.add_node(pdp.Node(alias, shape="plain", pos="0,0!"))
-    return g, functions
-
-
-if __name__ == '__main__':
-    import re
-    import sys
-    aliases = {}
-    ali_re = re.compile(r"ALIAS::\"([^\"]*)\" referring to \"([^\"]*)\"")
-
-    with open(sys.argv[1]) as f:
-        for line in f.readlines():
-            res = ali_re.findall(line)
-            if res:
-                aliases[res[0][1]] = res[0][0]
-                continue
-    for name, alias in aliases.items():
-        graph, _ = parse(name, alias)
-        fname = "val_graph_{}.gif".format(alias)
-        with open(fname, "wb") as f:
-            try:
-                f.write(graph.create_gif())
-            except Exception as e:
-                print(e)
-                print(graph.to_string())

+ 0 - 262
filval/histogram.py

@@ -1,262 +0,0 @@
-"""
-    histogram.py
-    The functions in this module use a representation of a histogram that is a
-    tuple containing an arr of N bin values, an array of N bin errors(symmetric)
-    and an array of N+1 bin edges(N lower edges + 1 upper edge).
-
-    For 2d histograms, It is similar, but the arrays are two dimensional and
-    there are separate arrays for x-edges and y-edges.
-"""
-
-import numpy as np
-from scipy.optimize import curve_fit
-
-
-def hist(th1, rescale_x=1.0, rescale_y=1.0):
-    nbins = th1.GetNbinsX()
-
-    edges = np.zeros(nbins+1, np.float32)
-    values = np.zeros(nbins, np.float32)
-    errors = np.zeros(nbins, np.float32)
-
-    for i in range(nbins):
-        edges[i] = th1.GetXaxis().GetBinLowEdge(i+1)
-        values[i] = th1.GetBinContent(i+1)
-        errors[i] = th1.GetBinError(i+1)
-
-    edges[nbins] = th1.GetXaxis().GetBinUpEdge(nbins)
-    edges *= rescale_x
-    values *= rescale_y
-    errors *= rescale_y
-    return values, errors, edges
-
-
-def hist_bin_centers(h):
-    _, _, edges = h
-    return (edges[:-1] + edges[1:])/2.0
-
-
-def hist_slice(h, range_):
-    values, errors, edges = h
-    lim_low, lim_high = range_
-    slice_ = np.logical_and(edges[:-1] > lim_low, edges[1:] < lim_high)
-    last = len(slice_) - np.argmax(slice_[::-1])
-    return (values[slice_],
-            errors[slice_],
-            np.concatenate([edges[:-1][slice_], [edges[last]]]))
-
-
-def hist_add(*hs):
-    if len(hs) == 0:
-        return np.zeros(0)
-    vals, errs, edges = zip(*hs)
-    return np.sum(vals, axis=0), np.sqrt(np.sum([err*err for err in errs], axis=0)), edges[0]
-
-
-def hist_sub(*hs):
-    if len(hs) == 0:
-        return np.zeros(0)
-    h0, hs = hs
-    hs = hist_add(hs)
-    hs = -hs[0], *hs[1:]
-    return hist_add(h0, hs)
-
-
-def hist_mul(h1, h2, cov=None):
-    h1_vals, h1_errs, num_edges = h1
-    h2_vals, h2_errs, _ = h2
-    prod_vals = h1_vals * h2_vals
-    prod_errs2 = (h1_errs/h1_vals)**2 + (h2_errs/h2_vals)**2
-    if cov:
-        prod_errs2 += 2*cov/(h1_vals*h2_vals)
-    prod_errs = abs(h1_vals*h2_vals)*np.sqrt(prod_errs2)
-    return prod_vals, prod_errs, num_edges
-
-
-def hist_div(num, den, cov=None):
-    num_vals, num_errs, num_edges = num
-    den_vals, den_errs, _ = den
-    rat_vals = num_vals / den_vals
-    rat_errs2 = (num_errs/num_vals)**2 + (den_errs/den_vals)**2
-    if cov:
-        rat_errs2 -= 2*cov/(num_vals*den_vals)
-    rat_errs = abs(num_vals/den_vals)*np.sqrt(rat_errs2)
-    return rat_vals, rat_errs, num_edges
-
-
-def hist_integral(h, times_bin_width=True):
-    values, errors, edges = h
-    if times_bin_width:
-        bin_widths = [abs(x2 - x1) for x1, x2 in zip(edges[:-1], edges[1:])]
-        return sum(val*width for val, width in zip(values, bin_widths))
-    else:
-        return sum(values)
-
-
-def hist_scale(h, scale):
-    values, errors, edges = h
-    return values*scale, errors*scale, edges
-
-
-def hist_norm(h, norm=1):
-    scale = norm/np.sum(h[0])
-    return hist_scale(h, scale)
-
-
-def hist_mean(h):
-    xs = hist_bin_centers(h)
-    ys, _, _ = h
-    return sum(x*y for x, y in zip(xs, ys)) / sum(ys)
-
-
-def hist_var(h):
-    xs = hist_bin_centers(h)
-    ys, _, _ = h
-    mean = sum(x*y for x, y in zip(xs, ys)) / sum(ys)
-    mean2 = sum((x**2)*y for x, y in zip(xs, ys)) / sum(ys)
-    return mean2 - mean**2
-
-
-def hist_std(h):
-    return np.sqrt(hist_var(h))
-
-
-def hist_stats(h):
-    return {'int': hist_integral(h),
-            'sum': hist_integral(h, False),
-            'mean': hist_mean(h),
-            'var': hist_var(h),
-            'std': hist_std(h)}
-
-
-# def hist_slice2d(h, range_):
-#     values, errors, xs, ys = h
-
-#     last = len(slice_) - np.argmax(slice_[::-1])
-
-#     (xlim_low, xlim_high), (ylim_low, ylim_high) = range_
-#     slice_ = np.logical_and(xs[:-1, :-1] > xlim_low, xs[1:, 1:] < xlim_high,
-#                             ys[:-1, :-1] > ylim_low, ys[1:, 1:] < ylim_high)
-#     last = len(slice_) - np.argmax(slice_[::-1])
-#     return (values[slice_],
-#             errors[slice_],
-#             np.concatenate([edges[:-1][slice_], [edges[last]]]))
-
-
-def hist_fit(h, f, p0=None):
-    values, errors, edges = h
-    xs = hist_bin_centers(h)
-    # popt, pcov = curve_fit(f, xs, values, p0=p0, sigma=errors)
-    popt, pcov = curve_fit(f, xs, values, p0=p0)
-    return popt, pcov
-
-
-def hist_rebin(h, nbins, min_, max_):
-    vals, errs, edges = h
-    low_edges = edges[:-1]
-    high_edges = edges[1:]
-    widths = edges[1:] - edges[:-1]
-
-    vals_new = np.zeros(nbins, dtype=vals.dtype)
-    errs_new = np.zeros(nbins, dtype=vals.dtype)  # TODO: properly propagate errors
-    edges_new = np.linspace(min_, max_, nbins+1, dtype=vals.dtype)
-
-    for i, (low, high) in enumerate(zip(edges_new[:-1], edges_new[1:])):
-        # wholly contained bins
-        b_idx = np.logical_and((low_edges >= low), (high_edges <= high)).nonzero()[0]
-        bin_sum = np.sum(vals[b_idx])
-        # internally contained
-        b_idx = np.logical_and((low_edges < low), (high_edges > high)).nonzero()[0]
-        bin_sum += np.sum(vals[b_idx])
-        # left edge
-        b_idx = np.logical_and((low_edges < high), (low_edges >= low), (high_edges > high)).nonzero()[0]
-        if len(b_idx) != 0:
-            idx = b_idx[0]
-            bin_sum += vals[idx]*(high - low_edges[idx])/widths[idx]
-        # right edge
-        b_idx = np.logical_and((high_edges > low), (low_edges < low), (high_edges <= high)).nonzero()[0]
-        if len(b_idx) != 0:
-            idx = b_idx[0]
-            bin_sum += vals[idx]*(high_edges[idx] - low)/widths[idx]
-
-        vals_new[i] = bin_sum
-
-    return vals_new, errs_new, edges_new
-
-
-def hist2d(th2, rescale_x=1.0, rescale_y=1.0, rescale_z=1.0):
-    """ Converts TH2 object to something amenable to
-        plotting w/ matplotlab's pcolormesh.
-    """
-    nbins_x = th2.GetNbinsX()
-    nbins_y = th2.GetNbinsY()
-    xs = np.zeros((nbins_y+1, nbins_x+1), dtype=np.float32)
-    ys = np.zeros((nbins_y+1, nbins_x+1), dtype=np.float32)
-    values = np.zeros((nbins_y, nbins_x), dtype=np.float32)
-    errors = np.zeros((nbins_y, nbins_x), dtype=np.float32)
-    for i in range(nbins_x):
-        for j in range(nbins_y):
-            xs[j][i] = th2.GetXaxis().GetBinLowEdge(i+1)
-            ys[j][i] = th2.GetYaxis().GetBinLowEdge(j+1)
-            values[j][i] = th2.GetBinContent(i+1, j+1)
-            errors[j][i] = th2.GetBinError(i+1, j+1)
-        xs[nbins_y][i] = th2.GetXaxis().GetBinUpEdge(i)
-        ys[nbins_y][i] = th2.GetYaxis().GetBinUpEdge(nbins_y)
-    for j in range(nbins_y+1):
-        xs[j][nbins_x] = th2.GetXaxis().GetBinUpEdge(nbins_x)
-        ys[j][nbins_x] = th2.GetYaxis().GetBinUpEdge(j)
-
-    xs *= rescale_x
-    ys *= rescale_y
-    values *= rescale_z
-    errors *= rescale_z
-
-    return values, errors, xs, ys
-
-
-def hist2d_norm(h, norm=1, axis=None):
-    """
-
-    :param h:
-    :param norm: value to normalize the sum of axis to
-    :param axis: which axis to normalize None is the sum over all bins, 0 is columns, 1 is rows.
-    :return: The normalized histogram
-    """
-    values, errors, xs, ys = h
-    with np.warnings.catch_warnings():
-        np.warnings.filterwarnings('ignore', 'invalid value encountered in true_divide')
-        scale_values = norm / np.sum(values, axis=axis)
-        scale_values[scale_values == np.inf] = 1
-        scale_values[scale_values == -np.inf] = 1
-    if axis == 1:
-        scale_values.shape = (scale_values.shape[0], 1)
-    values = values * scale_values
-    errors = errors * scale_values
-    return values, errors, xs.copy(), ys.copy()
-
-
-def hist2d_percent_contour(h, percent: float, axis: str):
-    values, _, xs, ys = h
-
-    try:
-        axis = axis.lower()
-        axis_idx = {'x': 1, 'y': 0}[axis]
-    except KeyError:
-        raise ValueError('axis must be \'x\' or \'y\'')
-    if percent < 0 or percent > 1:
-        raise ValueError('percent must be in [0,1]')
-
-    with np.warnings.catch_warnings():
-        np.warnings.filterwarnings('ignore', 'invalid value encountered in true_divide')
-        values = values / np.sum(values, axis=axis_idx, keepdims=True)
-        np.nan_to_num(values, copy=False)
-    values = np.cumsum(values, axis=axis_idx)
-    idxs = np.argmax(values > percent, axis=axis_idx)
-
-    bins_y = (ys[:-1, 0] + ys[1:, 0])/2
-    bins_x = (xs[0, :-1] + xs[0, 1:])/2
-
-    if axis == 'x':
-        return bins_x[idxs], bins_y
-    else:
-        return bins_x, bins_y[idxs]

+ 0 - 422
filval/plotting.py

@@ -1,422 +0,0 @@
-"""
-    plotting.py
-    The functions in this module are meant for plotting the histogram objects created via
-    filval.histogram
-"""
-
-from collections import defaultdict
-from itertools import zip_longest
-from io import BytesIO
-from base64 import b64encode
-import numpy as np
-import matplotlib.pyplot as plt
-from markdown import Markdown
-import latexipy as lp
-
-from filval.histogram import (hist, hist2d, hist_bin_centers, hist_fit,
-                              hist_norm, hist_stats)
-
-__all__ = ['Plot',
-           'decl_plot',
-           'grid_plot',
-           'render_plots',
-           'generate_dashboard',
-           'hist_plot',
-           'hist_plot_stack',
-           'hist2d_plot',
-           'hists_to_table',
-           'simple_plot']
-
-
-class Plot:
-    def __init__(self, subplots, name, title=None, docs="N/A", arg_dicts=None):
-        if type(subplots) is not list:
-            subplots = [[subplots]]
-        elif len(subplots) > 0 and type(subplots[0]) is not list:
-            subplots = [subplots]
-        self.subplots = subplots
-        self.name = name
-        self.title = title
-        self.docs = docs
-        self.arg_dicts = arg_dicts if arg_dicts is not None else {}
-
-
-MD = Markdown(extensions=['mdx_math', 'tables'],
-              extension_configs={'mdx_math': {'enable_dollar_delimiter': True}})
-
-lp.latexify(params={'pgf.texsystem': 'pdflatex',
-                    'text.usetex': True,
-                    'font.family': 'serif',
-                    'pgf.preamble': [],
-                    'font.size': 15,
-                    'axes.labelsize': 15,
-                    'axes.titlesize': 13,
-                    'legend.fontsize': 13,
-                    'xtick.labelsize': 11,
-                    'ytick.labelsize': 11,
-                    'figure.dpi': 150,
-                    'savefig.transparent': False,
-                    },
-            new_backend='TkAgg')
-
-
-def _fn_call_to_dict(fn, *args, **kwargs):
-    from inspect import signature
-    from html import escape
-    pnames = list(signature(fn).parameters)
-    pvals = list(args) + list(kwargs.values())
-    return {escape(str(k)): escape(str(v)) for k, v in zip(pnames, pvals)}
-
-
-def _process_docs(fn):
-    from inspect import getdoc
-    raw = getdoc(fn)
-    if raw:
-        return MD.convert(raw)
-    else:
-        return None
-
-
-def decl_plot(fn):
-    from functools import wraps
-
-    @wraps(fn)
-    def f(*args, **kwargs):
-        txt = fn(*args, **kwargs)
-        argdict = _fn_call_to_dict(fn, *args, **kwargs)
-        docs = _process_docs(fn)
-        if not txt:
-            txt = ''
-        txt = MD.convert(txt)
-
-        return argdict, docs, txt
-
-    return f
-
-
-def simple_plot(thx, *args, log=None, **kwargs):
-    import ROOT
-
-    if isinstance(thx, ROOT.TH2):
-        def f(h):
-            hist2d_plot(hist2d(h), *args, **kwargs)
-            plt.xlabel(h.GetXaxis().GetTitle())
-            plt.ylabel(h.GetYaxis().GetTitle())
-            if log == 'x':
-                plt.semilogx()
-            elif log == 'y':
-                plt.semilogy()
-            elif log == 'xy':
-                plt.loglog()
-            return dict(), "", ""
-
-        return Plot([[(f, (thx,), {})]], thx.GetName())
-    elif isinstance(thx, ROOT.TH1):
-        def f(h):
-            hist_plot(hist(h), *args, **kwargs)
-            plt.xlabel(h.GetXaxis().GetTitle())
-            plt.ylabel(h.GetYaxis().GetTitle())
-            if log == 'x':
-                plt.semilogx()
-            elif log == 'y':
-                plt.semilogy()
-            elif log == 'xy':
-                plt.loglog()
-            return dict(), "", ""
-
-        return Plot([[(f, (thx,), {})]], thx.GetName())
-    else:
-        raise ValueError("must call simple_plot with a ROOT TH1 or TH2 object")
-
-
-def generate_dashboard(plots, title, output='dashboard.html', template='dashboard.j2',
-                       source=None, ana_source=None, config=None, header=None):
-    from jinja2 import Environment, PackageLoader, select_autoescape
-    from os.path import join, isdir
-    from os import mkdir
-    from urllib.parse import quote
-
-    env = Environment(
-        loader=PackageLoader('filval', 'templates'),
-        autoescape=select_autoescape(['htm', 'html', 'xml']),
-    )
-    env.globals.update({'quote': quote,
-                        'enumerate': enumerate,
-                        'zip': zip,
-                        })
-
-    def get_by_n(objects, n=2):
-        objects = list(objects)
-        while objects:
-            yield objects[:n]
-            objects = objects[n:]
-
-    if source is not None:
-        with open(source, 'r') as f:
-            source = f.read()
-
-    if not isdir('output'):
-        mkdir('output')
-
-    if header is not None:
-        header = MD.convert(header)
-
-    dashboard_path = join('output', output)
-    with open(dashboard_path, 'w') as tempout:
-        templ = env.get_template(template)
-        tempout.write(templ.render(
-            plots=get_by_n(plots, 3),
-            title=title,
-            source=source,
-            ana_source=ana_source,
-            config=config,
-            header=header,
-        ))
-    return dashboard_path
-
-
-def _add_stats(hist, title=''):
-    fmt = r'''\begin{{eqnarray*}}
-\sum{{x_i}} &=& {sum:5.3f}                  \\
-\sum{{\Delta x_i \cdot x_i}} &=& {int:5.3G} \\
-\mu &=& {mean:5.3G}                         \\
-\sigma^2 &=& {var:5.3G}                     \\
-\sigma &=& {std:5.3G}
-\end{{eqnarray*}}'''
-
-    txt = fmt.format(**hist_stats(hist), title=title)
-    txt = txt.replace('\n', ' ')
-
-    plt.text(0.7, 0.9, txt,
-             bbox={'facecolor': 'white',
-                   'alpha': 0.7,
-                   'boxstyle': 'square,pad=0.8'},
-             transform=plt.gca().transAxes,
-             verticalalignment='top',
-             horizontalalignment='left',
-             size='small')
-    if title:
-        plt.text(0.72, 0.97, title,
-                 bbox={'facecolor': 'white',
-                       'alpha': 0.8},
-                 transform=plt.gca().transAxes,
-                 verticalalignment='top',
-                 horizontalalignment='left')
-
-
-def grid_plot(subplots):
-    if any(len(row) != len(subplots[0]) for row in subplots):
-        raise ValueError('make_plot requires a rectangular list-of-lists as '
-                         'input. Fill empty slots with None')
-
-    def calc_row_span(fig, row, col):
-        span = 1
-        for r in range(row + 1, len(fig)):
-            if fig[r][col] == 'FU':
-                span += 1
-            else:
-                break
-        return span
-
-    def calc_column_span(fig, row, col):
-        span = 1
-        for c in range(col + 1, len(fig[row])):
-            if fig[row][c] == 'FL':
-                span += 1
-            else:
-                break
-        return span
-
-    rows = len(subplots)
-    cols = len(subplots[0])
-
-    argdicts = defaultdict(list)
-    docs = defaultdict(list)
-    txts = defaultdict(list)
-    for i in range(rows):
-        for j in range(cols):
-            cell = subplots[i][j]
-            if cell in ('FL', 'FU', None):
-                continue
-            if not isinstance(cell, list):
-                cell = [cell]
-            column_span = calc_column_span(subplots, i, j)
-            row_span = calc_row_span(subplots, i, j)
-            plt.subplot2grid((rows, cols), (i, j),
-                             colspan=column_span, rowspan=row_span)
-            for plot in cell:
-                if not isinstance(plot, tuple):
-                    plot_fn, args, kwargs = plot, (), {}
-                elif len(plot) == 1:
-                    plot_fn, args, kwargs = plot[0], (), {}
-                elif len(plot) == 2:
-                    plot_fn, args, kwargs = plot[0], plot[1], {}
-                elif len(plot) == 3:
-                    plot_fn, args, kwargs = plot[0], plot[1], plot[2]
-                else:
-                    raise ValueError('Plot tuple must be of format (func), '
-                                     f'or (func, tuple), or (func, tuple, dict). Got {plot}')
-                this_args, this_docs, txt = plot_fn(*args, **kwargs)
-                argdicts[(i, j)].append(this_args)
-                docs[(i, j)].append(this_docs)
-                txts[(i, j)].append(txt)
-    return argdicts, docs, txts
-
-
-def render_plots(plots, exts=('png',), directory='output/figures/', scale=1.0, to_disk=True):
-    for plot in plots:
-        print(f'Building plot {plot.name}')
-        plot.data = None
-        if to_disk:
-            with lp.figure(plot.name.replace(' ', '_'), directory=directory,
-                           exts=exts,
-                           size=(scale * 10, scale * 10)):
-                argdicts, docs, txts = grid_plot(plot.subplots)
-        else:
-            out = BytesIO()
-            with lp.mem_figure(out,
-                               ext=exts[0],
-                               size=(scale * 10, scale * 10)):
-                argdicts, docs, txts = grid_plot(plot.subplots)
-            out.seek(0)
-            plot.data = b64encode(out.read()).decode()
-        plot.argdicts = argdicts
-        plot.docs = docs
-        plot.txts = txts
-
-
-def add_decorations(axes, luminosity, energy):
-    cms_prelim = r'{\raggedright{}\textsf{\textbf{CMS}}\\ \emph{Preliminary}}'
-    axes.text(0.01, 0.98, cms_prelim,
-              horizontalalignment='left',
-              verticalalignment='top',
-              transform=axes.transAxes)
-
-    lumi = ""
-    energy_str = ""
-    if luminosity is not None:
-        lumi = r'${} \mathrm{{fb}}^{{-1}}$'.format(luminosity)
-    if energy is not None:
-        energy_str = r'({} TeV)'.format(energy)
-
-    axes.text(1, 1, ' '.join([lumi, energy_str]),
-              horizontalalignment='right',
-              verticalalignment='bottom',
-              transform=axes.transAxes)
-
-
-def hist_plot(h, *args, include_errors=False, fit=None, stats=False, **kwargs):
-    """ Plots a 1D ROOT histogram object using matplotlib """
-    from inspect import signature
-    values, errors, edges = h
-
-    left, right = np.array(edges[:-1]), np.array(edges[1:])
-    x = np.array([left, right]).T.flatten()
-    y = np.array([values, values]).T.flatten()
-
-    title = kwargs.pop('title', '')
-
-    plt.plot(x, y, *args, linewidth=1, **kwargs)
-    if include_errors:
-        plt.errorbar(hist_bin_centers(h), values, yerr=errors,
-                     color='k', marker=None, linestyle='None',
-                     barsabove=True, elinewidth=.7, capsize=1)
-    if fit:
-        f, p0 = fit
-        popt, pcov = hist_fit(h, f, p0)
-        fit_xs = np.linspace(x[0], x[-1], 100)
-        fit_ys = f(fit_xs, *popt)
-        plt.plot(fit_xs, fit_ys, '--g')
-        arglabels = list(signature(f).parameters)[1:]
-        label_txt = "\n".join('{:7s}={: 0.2G}'.format(label, value)
-                              for label, value in zip(arglabels, popt))
-        plt.text(0.60, 0.95, label_txt, va='top', transform=plt.gca().transAxes,
-                 fontsize='medium', family='monospace', usetex=False)
-    if stats:
-        _add_stats(h, title)
-
-
-def hist2d_plot(h, txt_format=None, colorbar=False, **kwargs):
-    """ Plots a 2D ROOT histogram object using matplotlib """
-    try:
-        values, errors, xs, ys = h
-    except (TypeError, ValueError):
-        values, errors, xs, ys = hist2d(h)
-
-    plt.xlabel(kwargs.pop('xlabel', ''))
-    plt.ylabel(kwargs.pop('ylabel', ''))
-    plt.title(kwargs.pop('title', ''))
-    plt.pcolormesh(xs, ys, values, **kwargs)
-    if txt_format is not None:
-        cmap = plt.get_cmap()
-        min_, max_ = float(np.min(values)), float(np.max(values))
-
-        def get_intensity(val):
-            cmap_idx = int((cmap.N-1) * (val - min_) / (max_-min_))
-            color = cmap.colors[cmap_idx]
-            return color[0]*0.25 + color[1]*0.5 + color[2]*0.25
-
-        for idx_row in range(values.shape[0]):
-            for idx_col in range(values.shape[1]):
-                x_mid = (xs[idx_row, idx_col] + xs[idx_row, idx_col+1]) / 2
-                y_mid = (ys[idx_row, idx_col] + ys[idx_row+1, idx_col]) / 2
-                val = txt_format.format(values[idx_row, idx_col])
-                txt_color = 'w' if get_intensity(values[idx_row, idx_col]) < 0.5 else 'k'
-                plt.text(x_mid, y_mid, val, verticalalignment='center', horizontalalignment='center',
-                         color=txt_color, fontsize=12)
-    if colorbar:
-        plt.colorbar()
-
-
-def hist_plot_stack(hists: list, labels: list = None):
-    """
-    Creates a stacked histogram in the current axes.
-
-    :param hists: list of histogram
-    :param labels:
-    :return:
-    """
-    if len(hists) == 0:
-        return
-
-    if len(set([len(hist[0]) for hist in hists])) != 1:
-        raise ValueError("all histograms must have the same number of bins")
-    if labels is None:
-        labels = [None for _ in hists]
-    if len(labels) != len(hists):
-        raise ValueError("Label mismatch")
-
-    bottoms = [0 for _ in hists[0][0]]
-
-    for hist, label in zip(hists, labels):
-        centers = []
-        widths = []
-        heights = []
-        for left, right, content in zip(hist[2][:-1], hist[2][1:], hist[0]):
-            centers.append((right + left) / 2)
-            widths.append(right - left)
-            heights.append(content)
-
-        plt.bar(centers, heights, widths, bottoms, label=label)
-        for i, content in enumerate(hist[0]):
-            bottoms[i] += content
-
-
-def hists_to_table(hists, row_labels=(), column_labels=(), format="{:.2f}"):
-    table = ['<table class="table table-condensed">']
-    if column_labels:
-        table.append('<thead><tr>')
-        if row_labels:
-            table.append('<th></th>')
-        table.extend(f'<th>{label}</th>' for label in column_labels)
-        table.append('</tr></thead>')
-    table.append('<tbody>\n')
-    for row_label, (vals, *_) in zip_longest(row_labels, hists):
-        table.append('<tr>')
-        if row_label:
-            table.append(f'<td><strong>{row_label}</strong></td>')
-        table.extend(('<td>'+format.format(val)+'</td>') for val in vals)
-        table.append('</tr>\n')
-    table.append('</tbody></table>')
-    return ''.join(table)
-

+ 0 - 92
filval/result_set.py

@@ -1,92 +0,0 @@
-import ROOT
-
-from filval.plotting import hist_plot, hist2d_plot
-from numpy import ceil
-
-
-class ResultSet:
-
-    def __init__(self, sample_name, input_filename):
-        self.sample_name = sample_name
-        self.input_filename = input_filename
-        self.values = {}
-        self.map = {}
-        self.config = None
-        self.load_objects()
-
-        ResultSet.add_collection(self)
-
-    def load_objects(self):
-        file = ROOT.TFile.Open(self.input_filename)
-        try:
-            self.values = dict(file.Get("_value_lookup"))
-        except TypeError:
-            pass
-        try:
-            self.config = str(file.Get("_config").GetString())
-        except (TypeError, AttributeError):
-            pass
-        list_of_keys = file.GetListOfKeys()
-        for i in range(list_of_keys.GetSize()):
-            name = list_of_keys.At(i).GetName()
-            new_name = ":".join((self.sample_name, name))
-            obj = file.Get(name)
-            try:
-                obj.SetName(new_name)
-                obj.SetDirectory(0)  # disconnects Object from file
-            except AttributeError:
-                pass
-            if 'ROOT.vector<int>' in str(type(obj)) and '_count' in name:
-                obj = obj[0]
-            self.map[name] = obj
-            setattr(self, name, obj)
-        file.Close()
-
-        # Now add these histograms into the current ROOT directory (in memory)
-        # and remove old versions if needed
-        for obj in self.map.values():
-            try:
-                old_obj = ROOT.gDirectory.Get(obj.GetName())
-                ROOT.gDirectory.Remove(old_obj)
-                ROOT.gDirectory.Add(obj)
-            except AttributeError:
-                pass
-
-    @classmethod
-    def calc_shape(cls, n_plots):
-        if n_plots > 3:
-            return ceil(n_plots / 3), 3
-        else:
-            return 1, n_plots
-
-    def draw(self, figure=None, shape=None):
-        objs = [(name, obj) for name, obj in self.map.items() if isinstance(obj, ROOT.TH1)]
-        shape = self.calc_shape(len(objs))
-        if figure is None:
-            import matplotlib.pyplot as plt
-            figure = plt.gcf() if plt.gcf() is not None else plt.figure()
-        figure.clear()
-        for i, (name, obj) in enumerate(objs):
-            axes = figure.add_subplot(*shape, i+1)
-            if isinstance(obj, ROOT.TH2):
-                hist2d_plot(obj, title=obj.GetTitle(), axes=axes)
-            else:
-                hist_plot(obj, title=obj.GetTitle(), axes=axes)
-        figure.tight_layout()
-
-    @classmethod
-    def get_hist_set(cls, attrname):
-        return [(sample_name, getattr(h, attrname))
-                for sample_name, h in cls.collections.items()]
-
-    @classmethod
-    def add_collection(cls, hc):
-        if not hasattr(cls, "collections"):
-            cls.collections = {}
-        cls.collections[hc.sample_name] = hc
-
-    def __str__(self):
-        return self.sample_name+"@"+self.input_filename
-
-    def __repr__(self):
-        return f"<ResultSet: input_filename: {self.input_filename}>"

+ 0 - 51
filval/utils.py

@@ -1,51 +0,0 @@
-import ROOT
-
-__all__ = ["pdg", "show_function", "show_value"]
-
-db = ROOT.TDatabasePDG()
-
-
-class PDGParticle:
-
-    def __init__(self, tPart):
-        self.pdgId = tPart.PdgCode()
-        self.name = tPart.GetName()
-        self.charge = tPart.Charge() / 3.0
-        self.mass = tPart.Mass()
-        self.spin = tPart.Spin()
-
-    def __repr__(self):
-        return (f"<PDGParticle {self.name}:"
-                f"pdgId={self.pdgId}, charge={self.charge}, mass={self.mass:5.4e} GeV, spin={self.spin}>")
-
-
-def pdg(pdg_id):
-    try:
-        return PDGParticle(db.GetParticle(pdg_id))
-    except ReferenceError:
-        raise ValueError(f"unknown pdgId: {pdg_id}")
-
-
-def show_function(dataset, fname):
-    from IPython.display import Markdown
-
-    def md_single(fname_):
-        impl = dataset._function_impl_lookup[fname_]
-        return '*{}*\n-----\n```cpp\n{}\n```\n\n---'.format(fname_, impl)
-    try:
-        return Markdown('\n'.join(md_single(fname_) for fname_ in iter(fname)))
-    except TypeError:
-        return Markdown(md_single(fname))
-
-
-def show_value(dataset, container):
-    from IPython.display import Image
-    from graph_vals import parse
-    if type(container) != str:
-        container = container.GetName().split(':')[1]
-    g, functions = parse(dataset.values[container], container)
-    try:
-        return Image(g.create_gif()), show_function(dataset, functions)
-    except Exception as e:
-        print(e)
-        print(g.to_string())

+ 175 - 0
matplotboard/__init__.py

@@ -0,0 +1,175 @@
+"""
+    __init__.py
+    The functions in this module are meant for plotting the histogram objects created via
+    matplotboard.histogram
+"""
+
+import re
+from io import BytesIO
+from base64 import b64encode
+from markdown import Markdown
+import latexipy as lp
+
+
+__all__ = ['decl_fig',
+           'render',
+           'generate_report']
+
+
+MD = Markdown(extensions=['mdx_math', 'tables'],
+              extension_configs={'mdx_math': {'enable_dollar_delimiter': True}})
+
+lp.latexify(params={'pgf.texsystem': 'pdflatex',
+                    'text.usetex': True,
+                    'font.family': 'serif',
+                    'pgf.preamble': [],
+                    'font.size': 15,
+                    'axes.labelsize': 15,
+                    'axes.titlesize': 13,
+                    'legend.fontsize': 13,
+                    'xtick.labelsize': 11,
+                    'ytick.labelsize': 11,
+                    'figure.dpi': 150,
+                    'savefig.transparent': False,
+                    },
+            new_backend='TkAgg')
+
+
+def decl_fig(fn):
+    from functools import wraps
+
+    def _fn_call_to_dict(fn, *args, **kwargs):
+        from inspect import signature
+        from html import escape
+        pnames = list(signature(fn).parameters)
+        pvals = list(args) + list(kwargs.values())
+        return {escape(str(k)): escape(str(v)) for k, v in zip(pnames, pvals)}
+
+    def _process_docs(fn):
+        from inspect import getdoc
+        raw = getdoc(fn)
+        if raw:
+            return MD.convert(raw)
+        else:
+            return None
+
+    @wraps(fn)
+    def f(*args, **kwargs):
+        global _decl_counter
+        txt = fn(*args, **kwargs)
+        argdict = _fn_call_to_dict(fn, *args, **kwargs)
+        docs = _process_docs(fn)
+        if not txt:
+            txt = ''
+        html = MD.convert(txt)
+
+        return argdict, docs, html
+
+    return f
+
+
+def render(figures, scale=1.0):
+    from namedlist import namedlist
+
+    Figure = namedlist('Figure', 'name data argdict docs html idx')
+
+    def exec_fig(fig):
+        if not isinstance(fig, tuple):
+            fn, args, kwargs = fig, (), {}
+        elif len(fig) == 1:
+            fn, args, kwargs = fig[0], (), {}
+        elif len(fig) == 2:
+            fn, args, kwargs = fig[0], fig[1], {}
+        elif len(fig) == 3:
+            fn, args, kwargs = fig[0], fig[1], fig[2]
+        else:
+            raise ValueError('Plot tuple must be of format (func), '
+                             f'or (func, tuple), or (func, tuple, dict). Got {fig}')
+        return fn(*args, **kwargs)
+
+    for idx, (name, figure) in enumerate(figures.items()):
+        print(f'Building plot #{idx}: {name}')
+        out = BytesIO()
+        with lp.mem_figure(out,
+                           ext='png',
+                           size=(scale * 10, scale * 10)):
+            argdict, docs, html = exec_fig(figure)
+        out.seek(0)
+        figures[name] = Figure(name, out, argdict, docs, html, idx)
+
+
+def generate_report(figures, title, outputdir='report',
+                    source=None, ana_source=None, config=None, body=None):
+    from os.path import join, dirname, abspath
+    from os import mkdir
+    from shutil import rmtree, copytree
+    from jinja2 import Environment, PackageLoader, select_autoescape, Template
+    from urllib.parse import quote
+
+    if body is None:
+        raise ValueError("You must supply the body of the report!")
+
+    env = Environment(
+        loader=PackageLoader('matplotboard', 'templates'),
+        autoescape=select_autoescape(['htm', 'html', 'xml']),
+    )
+    env.globals.update({'quote': quote,
+                        'enumerate': enumerate,
+                        'zip': zip,
+                        })
+
+    if source is not None:
+        with open(source, 'r') as f:
+            source = f.read()
+
+    rmtree(outputdir)
+    mkdir(outputdir)
+    pkgdir = dirname(abspath(__file__))
+    copytree(join(pkgdir, 'static', 'js'), join(outputdir, 'js'))
+    copytree(join(pkgdir, 'static', 'css'), join(outputdir, 'css'))
+    figure_dir = join(outputdir, 'figures')
+    mkdir(figure_dir)
+
+    for name, figure in figures.items():
+        fname = join(figure_dir, f'{figure.name}.png')
+        with open(fname, 'wb') as f:
+            f.write(figure.data.read())
+
+    body = re.sub(r'fig::(\w+)', r'{{ fig(figures["\1"]) }}', body)
+    body = MD.convert(body)
+
+    report_template = env.from_string(f'''
+{{% extends("report.j2")%}}
+{{% from 'macros.j2' import fig %}}
+{{% block body %}}
+{body}
+{{% endblock %}}''')
+
+    with open(join(outputdir, 'report.html'), 'w') as f:
+        f.write(report_template.render(
+            title=title,
+            figures=figures,
+            source=source,
+            ana_source=ana_source,
+            config=config,
+            ))
+
+
+# def hists_to_table(hists, row_labels=(), column_labels=(), format="{:.2f}"):
+#     table = ['<table class="table table-condensed">']
+#     if column_labels:
+#         table.append('<thead><tr>')
+#         if row_labels:
+#             table.append('<th></th>')
+#         table.extend(f'<th>{label}</th>' for label in column_labels)
+#         table.append('</tr></thead>')
+#     table.append('<tbody>\n')
+#     for row_label, (vals, *_) in zip_longest(row_labels, hists):
+#         table.append('<tr>')
+#         if row_label:
+#             table.append(f'<td><strong>{row_label}</strong></td>')
+#         table.extend(('<td>'+format.format(val)+'</td>') for val in vals)
+#         table.append('</tr>\n')
+#     table.append('</tbody></table>')
+#     return ''.join(table)
+

+ 226 - 0
matplotboard/static/css/shCore.css

@@ -0,0 +1,226 @@
+/**
+ * SyntaxHighlighter
+ * http://alexgorbatchev.com/SyntaxHighlighter
+ *
+ * SyntaxHighlighter is donationware. If you are using it, please donate.
+ * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
+ *
+ * @version
+ * 3.0.83 (July 02 2010)
+ * 
+ * @copyright
+ * Copyright (C) 2004-2010 Alex Gorbatchev.
+ *
+ * @license
+ * Dual licensed under the MIT and GPL licenses.
+ */
+.syntaxhighlighter a,
+.syntaxhighlighter div,
+.syntaxhighlighter code,
+.syntaxhighlighter table,
+.syntaxhighlighter table td,
+.syntaxhighlighter table tr,
+.syntaxhighlighter table tbody,
+.syntaxhighlighter table thead,
+.syntaxhighlighter table caption,
+.syntaxhighlighter textarea {
+  -moz-border-radius: 0 0 0 0 !important;
+  -webkit-border-radius: 0 0 0 0 !important;
+  background: none !important;
+  border: 0 !important;
+  bottom: auto !important;
+  float: none !important;
+  height: auto !important;
+  left: auto !important;
+  line-height: 1.1em !important;
+  margin: 0 !important;
+  outline: 0 !important;
+  overflow: visible !important;
+  padding: 0 !important;
+  position: static !important;
+  right: auto !important;
+  text-align: left !important;
+  top: auto !important;
+  vertical-align: baseline !important;
+  width: auto !important;
+  box-sizing: content-box !important;
+  font-family: "Consolas", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace !important;
+  font-weight: normal !important;
+  font-style: normal !important;
+  font-size: 1em !important;
+  min-height: inherit !important;
+  min-height: auto !important;
+}
+
+.syntaxhighlighter {
+  width: 100% !important;
+  margin: 1em 0 1em 0 !important;
+  position: relative !important;
+  overflow: auto !important;
+  font-size: 1em !important;
+}
+.syntaxhighlighter.source {
+  overflow: hidden !important;
+}
+.syntaxhighlighter .bold {
+  font-weight: bold !important;
+}
+.syntaxhighlighter .italic {
+  font-style: italic !important;
+}
+.syntaxhighlighter .line {
+  white-space: pre !important;
+}
+.syntaxhighlighter table {
+  width: 100% !important;
+}
+.syntaxhighlighter table caption {
+  text-align: left !important;
+  padding: .5em 0 0.5em 1em !important;
+}
+.syntaxhighlighter table td.code {
+  width: 100% !important;
+}
+.syntaxhighlighter table td.code .container {
+  position: relative !important;
+}
+.syntaxhighlighter table td.code .container textarea {
+  box-sizing: border-box !important;
+  position: absolute !important;
+  left: 0 !important;
+  top: 0 !important;
+  width: 100% !important;
+  height: 100% !important;
+  border: none !important;
+  background: white !important;
+  padding-left: 1em !important;
+  overflow: hidden !important;
+  white-space: pre !important;
+}
+.syntaxhighlighter table td.gutter .line {
+  text-align: right !important;
+  padding: 0 0.5em 0 1em !important;
+}
+.syntaxhighlighter table td.code .line {
+  padding: 0 1em !important;
+}
+.syntaxhighlighter.nogutter td.code .container textarea, .syntaxhighlighter.nogutter td.code .line {
+  padding-left: 0em !important;
+}
+.syntaxhighlighter.show {
+  display: block !important;
+}
+.syntaxhighlighter.collapsed table {
+  display: none !important;
+}
+.syntaxhighlighter.collapsed .toolbar {
+  padding: 0.1em 0.8em 0em 0.8em !important;
+  font-size: 1em !important;
+  position: static !important;
+  width: auto !important;
+  height: auto !important;
+}
+.syntaxhighlighter.collapsed .toolbar span {
+  display: inline !important;
+  margin-right: 1em !important;
+}
+.syntaxhighlighter.collapsed .toolbar span a {
+  padding: 0 !important;
+  display: none !important;
+}
+.syntaxhighlighter.collapsed .toolbar span a.expandSource {
+  display: inline !important;
+}
+.syntaxhighlighter .toolbar {
+  position: absolute !important;
+  right: 1px !important;
+  top: 1px !important;
+  width: 11px !important;
+  height: 11px !important;
+  font-size: 10px !important;
+  z-index: 10 !important;
+}
+.syntaxhighlighter .toolbar span.title {
+  display: inline !important;
+}
+.syntaxhighlighter .toolbar a {
+  display: block !important;
+  text-align: center !important;
+  text-decoration: none !important;
+  padding-top: 1px !important;
+}
+.syntaxhighlighter .toolbar a.expandSource {
+  display: none !important;
+}
+.syntaxhighlighter.ie {
+  font-size: .9em !important;
+  padding: 1px 0 1px 0 !important;
+}
+.syntaxhighlighter.ie .toolbar {
+  line-height: 8px !important;
+}
+.syntaxhighlighter.ie .toolbar a {
+  padding-top: 0px !important;
+}
+.syntaxhighlighter.printing .line.alt1 .content,
+.syntaxhighlighter.printing .line.alt2 .content,
+.syntaxhighlighter.printing .line.highlighted .number,
+.syntaxhighlighter.printing .line.highlighted.alt1 .content,
+.syntaxhighlighter.printing .line.highlighted.alt2 .content {
+  background: none !important;
+}
+.syntaxhighlighter.printing .line .number {
+  color: #bbbbbb !important;
+}
+.syntaxhighlighter.printing .line .content {
+  color: black !important;
+}
+.syntaxhighlighter.printing .toolbar {
+  display: none !important;
+}
+.syntaxhighlighter.printing a {
+  text-decoration: none !important;
+}
+.syntaxhighlighter.printing .plain, .syntaxhighlighter.printing .plain a {
+  color: black !important;
+}
+.syntaxhighlighter.printing .comments, .syntaxhighlighter.printing .comments a {
+  color: #008200 !important;
+}
+.syntaxhighlighter.printing .string, .syntaxhighlighter.printing .string a {
+  color: blue !important;
+}
+.syntaxhighlighter.printing .keyword {
+  color: #006699 !important;
+  font-weight: bold !important;
+}
+.syntaxhighlighter.printing .preprocessor {
+  color: gray !important;
+}
+.syntaxhighlighter.printing .variable {
+  color: #aa7700 !important;
+}
+.syntaxhighlighter.printing .value {
+  color: #009900 !important;
+}
+.syntaxhighlighter.printing .functions {
+  color: #ff1493 !important;
+}
+.syntaxhighlighter.printing .constants {
+  color: #0066cc !important;
+}
+.syntaxhighlighter.printing .script {
+  font-weight: bold !important;
+}
+.syntaxhighlighter.printing .color1, .syntaxhighlighter.printing .color1 a {
+  color: gray !important;
+}
+.syntaxhighlighter.printing .color2, .syntaxhighlighter.printing .color2 a {
+  color: #ff1493 !important;
+}
+.syntaxhighlighter.printing .color3, .syntaxhighlighter.printing .color3 a {
+  color: red !important;
+}
+.syntaxhighlighter.printing .break, .syntaxhighlighter.printing .break a {
+  color: black !important;
+}

+ 117 - 0
matplotboard/static/css/shThemeDefault.css

@@ -0,0 +1,117 @@
+/**
+ * SyntaxHighlighter
+ * http://alexgorbatchev.com/SyntaxHighlighter
+ *
+ * SyntaxHighlighter is donationware. If you are using it, please donate.
+ * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
+ *
+ * @version
+ * 3.0.83 (July 02 2010)
+ * 
+ * @copyright
+ * Copyright (C) 2004-2010 Alex Gorbatchev.
+ *
+ * @license
+ * Dual licensed under the MIT and GPL licenses.
+ */
+.syntaxhighlighter {
+  background-color: white !important;
+}
+.syntaxhighlighter .line.alt1 {
+  background-color: white !important;
+}
+.syntaxhighlighter .line.alt2 {
+  background-color: white !important;
+}
+.syntaxhighlighter .line.highlighted.alt1, .syntaxhighlighter .line.highlighted.alt2 {
+  background-color: #e0e0e0 !important;
+}
+.syntaxhighlighter .line.highlighted.number {
+  color: black !important;
+}
+.syntaxhighlighter table caption {
+  color: black !important;
+}
+.syntaxhighlighter .gutter {
+  color: #afafaf !important;
+}
+.syntaxhighlighter .gutter .line {
+  border-right: 3px solid #6ce26c !important;
+}
+.syntaxhighlighter .gutter .line.highlighted {
+  background-color: #6ce26c !important;
+  color: white !important;
+}
+.syntaxhighlighter.printing .line .content {
+  border: none !important;
+}
+.syntaxhighlighter.collapsed {
+  overflow: visible !important;
+}
+.syntaxhighlighter.collapsed .toolbar {
+  color: blue !important;
+  background: white !important;
+  border: 1px solid #6ce26c !important;
+}
+.syntaxhighlighter.collapsed .toolbar a {
+  color: blue !important;
+}
+.syntaxhighlighter.collapsed .toolbar a:hover {
+  color: red !important;
+}
+.syntaxhighlighter .toolbar {
+  color: white !important;
+  background: #6ce26c !important;
+  border: none !important;
+}
+.syntaxhighlighter .toolbar a {
+  color: white !important;
+}
+.syntaxhighlighter .toolbar a:hover {
+  color: black !important;
+}
+.syntaxhighlighter .plain, .syntaxhighlighter .plain a {
+  color: black !important;
+}
+.syntaxhighlighter .comments, .syntaxhighlighter .comments a {
+  color: #008200 !important;
+}
+.syntaxhighlighter .string, .syntaxhighlighter .string a {
+  color: blue !important;
+}
+.syntaxhighlighter .keyword {
+  color: #006699 !important;
+}
+.syntaxhighlighter .preprocessor {
+  color: gray !important;
+}
+.syntaxhighlighter .variable {
+  color: #aa7700 !important;
+}
+.syntaxhighlighter .value {
+  color: #009900 !important;
+}
+.syntaxhighlighter .functions {
+  color: #ff1493 !important;
+}
+.syntaxhighlighter .constants {
+  color: #0066cc !important;
+}
+.syntaxhighlighter .script {
+  font-weight: bold !important;
+  color: #006699 !important;
+  background-color: none !important;
+}
+.syntaxhighlighter .color1, .syntaxhighlighter .color1 a {
+  color: gray !important;
+}
+.syntaxhighlighter .color2, .syntaxhighlighter .color2 a {
+  color: #ff1493 !important;
+}
+.syntaxhighlighter .color3, .syntaxhighlighter .color3 a {
+  color: red !important;
+}
+
+.syntaxhighlighter .keyword {
+  font-weight: bold !important;
+}

File diff suppressed because it is too large
+ 17 - 0
matplotboard/static/js/shAutoloader.js


+ 33 - 0
matplotboard/static/js/shBrushPlain.js

@@ -0,0 +1,33 @@
+/**
+ * SyntaxHighlighter
+ * http://alexgorbatchev.com/SyntaxHighlighter
+ *
+ * SyntaxHighlighter is donationware. If you are using it, please donate.
+ * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
+ *
+ * @version
+ * 3.0.83 (July 02 2010)
+ * 
+ * @copyright
+ * Copyright (C) 2004-2010 Alex Gorbatchev.
+ *
+ * @license
+ * Dual licensed under the MIT and GPL licenses.
+ */
+;(function()
+{
+	// CommonJS
+	typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null;
+
+	function Brush()
+	{
+	};
+
+	Brush.prototype	= new SyntaxHighlighter.Highlighter();
+	Brush.aliases	= ['text', 'plain'];
+
+	SyntaxHighlighter.brushes.Plain = Brush;
+
+	// CommonJS
+	typeof(exports) != 'undefined' ? exports.Brush = Brush : null;
+})();

+ 64 - 0
matplotboard/static/js/shBrushPython.js

@@ -0,0 +1,64 @@
+/**
+ * SyntaxHighlighter
+ * http://alexgorbatchev.com/SyntaxHighlighter
+ *
+ * SyntaxHighlighter is donationware. If you are using it, please donate.
+ * http://alexgorbatchev.com/SyntaxHighlighter/donate.html
+ *
+ * @version
+ * 3.0.83 (July 02 2010)
+ * 
+ * @copyright
+ * Copyright (C) 2004-2010 Alex Gorbatchev.
+ *
+ * @license
+ * Dual licensed under the MIT and GPL licenses.
+ */
+;(function()
+{
+	// CommonJS
+	typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null;
+
+	function Brush()
+	{
+		// Contributed by Gheorghe Milas and Ahmad Sherif
+	
+		var keywords =  'and assert break class continue def del elif else ' +
+						'except exec finally for from global if import in is ' +
+						'lambda not or pass print raise return try yield while';
+
+		var funcs = '__import__ abs all any apply basestring bin bool buffer callable ' +
+					'chr classmethod cmp coerce compile complex delattr dict dir ' +
+					'divmod enumerate eval execfile file filter float format frozenset ' +
+					'getattr globals hasattr hash help hex id input int intern ' +
+					'isinstance issubclass iter len list locals long map max min next ' +
+					'object oct open ord pow print property range raw_input reduce ' +
+					'reload repr reversed round set setattr slice sorted staticmethod ' +
+					'str sum super tuple type type unichr unicode vars xrange zip';
+
+		var special =  'None True False self cls class_';
+
+		this.regexList = [
+				{ regex: SyntaxHighlighter.regexLib.singleLinePerlComments, css: 'comments' },
+				{ regex: /^\s*@\w+/gm, 										css: 'decorator' },
+				{ regex: /(['\"]{3})([^\1])*?\1/gm, 						css: 'comments' },
+				{ regex: /"(?!")(?:\.|\\\"|[^\""\n])*"/gm, 					css: 'string' },
+				{ regex: /'(?!')(?:\.|(\\\')|[^\''\n])*'/gm, 				css: 'string' },
+				{ regex: /\+|\-|\*|\/|\%|=|==/gm, 							css: 'keyword' },
+				{ regex: /\b\d+\.?\w*/g, 									css: 'value' },
+				{ regex: new RegExp(this.getKeywords(funcs), 'gmi'),		css: 'functions' },
+				{ regex: new RegExp(this.getKeywords(keywords), 'gm'), 		css: 'keyword' },
+				{ regex: new RegExp(this.getKeywords(special), 'gm'), 		css: 'color1' }
+				];
+			
+		this.forHtmlScript(SyntaxHighlighter.regexLib.aspScriptTags);
+	};
+
+	Brush.prototype	= new SyntaxHighlighter.Highlighter();
+	Brush.aliases	= ['py', 'python'];
+
+	SyntaxHighlighter.brushes.Python = Brush;
+
+	// CommonJS
+	typeof(exports) != 'undefined' ? exports.Brush = Brush : null;
+})();

File diff suppressed because it is too large
+ 17 - 0
matplotboard/static/js/shCore.js


+ 41 - 0
matplotboard/templates/macros.j2

@@ -0,0 +1,41 @@
+{% macro fig(plot, embed_figure=False) %}
+<div class="well center-block figure-container">
+  <div>
+    <a href="#" title="{{ plot.name }}">
+      {% if embed_figure %}
+      <img src="data:img/png;base64,{{ plot.data }}" class="thumbnail img-responsive">
+      {% else %}
+      <img src="./figures/{{ plot.name }}.png" class="thumbnail img-responsive">
+      {% endif %}
+    </a>
+  </div>
+  <div class="caption">
+    <p class="text-center"> {{ plot.name }} </p>
+    <div class="panel-group" id="accordion{{ r }}{{ c }}">
+      <div class="panel-heading">
+        <h4 class="panel-title">
+          <button data-toggle="collapse" data-parent="#accordion{{ plot.idx }}" class="btn btn-info btn-block" href="#collapse{{plot.idx}}">
+            Plot Info</button>
+        </h4>
+      </div>
+      <div id="collapse{{plot.idx}}" class="panel-collapse collapse">
+        <div class="panel-body">
+          <div class="text-left">{{ plot.docs|safe }}</div>
+          <div class="text-left">{{ plot.ret_html|safe }}</div>
+          <hr>
+          <p class="text-left"><strong>Plot Arguments</strong></p>
+          <table class="table table-hover">
+            <tbody>
+            {% for key, val in plot.argdict.items() %}
+              <tr>
+                <td>{{ key }}</td> <td>{{ val }}</td>
+              </tr>
+            {% endfor %}
+            </tbody>
+          </table>
+      </div>
+      </div>
+    </div>
+  </div>
+</div>
+{% endmacro %}

+ 36 - 69
filval/templates/dashboard.j2

@@ -8,12 +8,12 @@
   <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
   <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
 
-  <script src="https://tttt.fangmeier.tech/hl/shCore.js"         type="text/javascript"></script>
-  <script src="https://tttt.fangmeier.tech/hl/shBrushPython.js" type="text/javascript"></script>
-  <script src="https://tttt.fangmeier.tech/hl/shBrushPlain.js" type="text/javascript"></script>
-  <link href="https://tttt.fangmeier.tech/hl/shCore.css"          rel="stylesheet" type="text/css" />
-  <link href="https://tttt.fangmeier.tech/hl/shThemeDefault.css"  rel="stylesheet" type="text/css" />
-  <script src="https://tttt.fangmeier.tech/hl/shAutoloader.js" type="text/javascript"></script>
+  <script src="./js/shCore.js"         type="text/javascript"></script>
+  <script src="./js/shBrushPython.js" type="text/javascript"></script>
+  <script src="./js/shBrushPlain.js" type="text/javascript"></script>
+  <link href="./css/shCore.css"          rel="stylesheet" type="text/css" />
+  <link href="./css/shThemeDefault.css"  rel="stylesheet" type="text/css" />
+  <script src="./js/shAutoloader.js" type="text/javascript"></script>
 
 <script type="text/x-mathjax-config">
 MathJax.Hub.Config({
@@ -23,67 +23,36 @@ MathJax.Hub.Config({
 });
 </script>
 <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS_HTML-full"> </script>
+
+<style>
+    .modal-dialog {width:90%;}
+    .thumbnail {margin-bottom:6px;}
+    .modal-title {text-align:center;}
+    .figure-container {
+        max-width:35%;
+        padding-bottom: 0px;
+    }
+    .main-content {
+        padding-left: 30px;
+        padding-right: 30px;
+    }
+    .panel-group {
+        margin-bottom: 0px;
+    }
+</style>
+
 </head>
 <body>
 <div class="container-fluid">
     <div class="row">
         <a href="./" class="button"> Go Up <span class="glyphicon glyphicon-circle-arrow-left"></span> </a>
     </div>
-    {% if header %}
-    <div class="row">
-        {{ header|safe }}
-    </div>
-    {% endif %}
-{% for r, plot_row in enumerate(plots) %}
-  <div class="row">
-  {% for c, plot in enumerate(plot_row) %}
-    <div class="col-md-4">
-      <div class="well">
-    {% if plot.title %}
-        <h3 class="text-center">{{ plot.title }}</h3>
-    {% endif %}
-        <div>
-          <a href="#" title="{{ plot.name }}">
-            <img src="data:img/png;base64,{{ plot.data }}" style="width:100%" class="thumbnail img-responsive">
-          </a>
-        </div>
-        <div class="caption">
-          <p class="text-center"> {{ plot.name }} </p>
-          <div class="panel-group" id="accordion{{ r }}{{ c }}">
-    {% for id, (i,j) in enumerate(plot.docs.keys()) %}
-            <div class="panel-heading">
-              <h4 class="panel-title">
-                <button data-toggle="collapse" data-parent="#accordion{{ r }}{{ c }}" class="btn btn-info" href="#collapse{{r}}-{{c}}-{{id}}">
-                  Plot at ({{ i+1 }}, {{ j+1 }})</button>
-              </h4>
-            </div>
-            <div id="collapse{{r}}-{{c}}-{{id}}" class="panel-collapse collapse">
-              <div class="panel-body">
-      {% for doc, argdict, txt in zip(plot.docs[(i,j)], plot.argdicts[(i,j)], plot.txts[(i,j)]) %}
-                <div class="text-left">{{ doc|safe }}</div>
-                <div class="text-left">{{ txt|safe }}</div>
-                <hr>
-                <p class="text-left"><strong>Plot Arguments</strong></p>
-                <table class="table table-hover">
-                  <tbody>
-        {% for key, val in argdict.items() %}
-                    <tr>
-                      <td>{{ key }}</td> <td>{{ val }}</td>
-                    </tr>
-        {% endfor %}
-                  </tbody>
-                </table>
-      {% endfor %}
-              </div>
-            </div>
-    {% endfor %}
-          </div>
-        </div>
-      </div>
+    <div class="row main-content">
+    {% block body %}
+        <h3>No body supplied</h3>
+    {% endblock %}
     </div>
-  {% endfor %}
-  </div>
-{% endfor %}
+
   <div class="row">
     <div class="col-12-lg">
       <div class="panel-group" id="accordion">
@@ -93,13 +62,14 @@ MathJax.Hub.Config({
             <button data-toggle="collapse" data-parent="#accordion" class="btn btn-default" href="#collapseConfig">Analysis Configuration</button>
             {% endif %}
             {% if source %}
-            <button data-toggle="collapse" data-parent="#accordion" class="btn btn-default" href="#collapseSrc">Figure Source Code</button>
+            <button data-toggle="collapse" data-parent="#accordion" class="btn btn-default" href="#collapseSrc">Report Source Code</button>
             {% endif %}
             {% if ana_source %}
             <a class="btn btn-default" href="{{ ana_source }}" target="_blank">Analysis Source Code</a>
             {% endif %}
           </h4>
         </div>
+
       {% if config %}
           <div id="collapseConfig" class="panel-collapse collapse">
               <div class="panel-body">
@@ -107,6 +77,7 @@ MathJax.Hub.Config({
               </div>
           </div>
       {% endif %}
+
       {% if source %}
         <div id="collapseSrc" class="panel-collapse collapse">
           <div class="panel-body">
@@ -114,6 +85,7 @@ MathJax.Hub.Config({
           </div>
         </div>
       {% endif %}
+
       </div>
     </div>
   </div>
@@ -125,18 +97,12 @@ MathJax.Hub.Config({
         <button type="button" class="close" data-dismiss="modal">×</button>
         <h3 class="modal-title">Heading</h3>
       </div>
-      <div class="modal-body">
-
-      </div>
+      <div class="modal-body"> </div>
    </div>
   </div>
 </div>
 </body>
-<style>
-.modal-dialog {width:90%;}
-.thumbnail {margin-bottom:6px;}
-.modal-title {text-align:center;}
-</style>
+
 <script>
 $('.thumbnail').click(function(){
     $('.modal-body').empty();
@@ -147,4 +113,5 @@ $('.thumbnail').click(function(){
 });
 SyntaxHighlighter.all()
 </script>
+
 </html>

+ 1 - 5
requirements.txt

@@ -1,10 +1,6 @@
-numpy
 matplotlib
 latexipy
-ipython
-scipy
-jupyter
-pydotplus
 Jinja2
 Markdown
 python-markdown-math
+namedlist

+ 0 - 76
scripts/merge.py

@@ -1,76 +0,0 @@
-#!env/bin/python
-import argparse
-import re
-import os
-import ROOT
-
-
-def merge_stl_obj(obj_key, output_file, input1, input_rest, merge_func=None):
-    """ Merges STL objects and saves the result into the output file, user
-        must supply the merging function.
-    """
-    obj = input1.Get(obj_key)
-    type_name_raw = str(type(obj))
-    try:
-        type_name = re.findall("<class 'ROOT.([^']+)'>", type_name_raw)[0]
-    except IndexError:
-        raise ValueError(f"Couldn't extract stl type name from {type_name_raw}")
-    if merge_func is not None:
-        for input_file in input_rest:
-            obj_ = input_file.Get(obj_key)
-            merge_func(obj, obj_)
-    output_file.WriteObjectAny(obj, type_name, obj_key)
-
-
-def merge_obj(obj_key, output_file, input1, input_rest):
-    obj = input1.Get(obj_key)
-    print('='*80)
-    print(f'Merging object {obj_key} of type {type(obj)}')
-    if isinstance(obj, ROOT.TH1):
-        obj.SetDirectory(output_file)  # detach from input file
-        for input_file in input_rest:
-            obj_ = input_file.Get(obj_key)
-            obj.Add(obj_)
-        obj.Write()
-    else:
-        print(f"I don't know how to merge object of type{type(obj)}, but "
-              "you can add a case in merge_obj to handle it!")
-
-
-def merge_files(input_filenames, output_filename, preserve=False):
-    print(f"Merging files {', '.join(input_filenames)} into {output_filename}")
-
-    input1, *input_rest = [ROOT.TFile.Open(input_file, "READ") for input_file in input_filenames]
-    output_file = ROOT.TFile.Open(output_filename, "RECREATE")
-    output_file.cd()
-
-    obj_keys = [k.GetName() for k in input1.GetListOfKeys()]
-    for obj_key in obj_keys:
-        if obj_key in {"_value_lookup", "_function_impl_lookup"}:
-            merge_stl_obj(obj_key, output_file, input1, [])
-        else:
-            merge_obj(obj_key, output_file, input1, input_rest)
-
-    for file_ in [input1, *input_rest, output_file]:
-        file_.Close()
-    print(f"Merge finished! Results have been saved into {output_filename}")
-    if preserve:
-        print("Preseve specified, leaving input files intact")
-    else:
-        print("Removing input files...", end='', flush=True)
-        for filename in input_filenames:
-            os.remove(filename)
-        print("Done!")
-
-
-if __name__ == '__main__':
-    parser = argparse.ArgumentParser()
-
-    add = parser.add_argument
-
-    add('output_file')
-    add('input_files', nargs='+')
-    add('--preserve', '-p', action='store_true')
-
-    args = parser.parse_args()
-    merge_files(args.input_files, args.output_file, args.preserve)

+ 0 - 63
scripts/process_parallel.py

@@ -1,63 +0,0 @@
-#! /usr/bin/env python3
-from os import listdir
-from os.path import join, isdir
-import argparse
-import subprocess
-
-import multiprocessing
-from multiprocessing.pool import Pool
-
-from merge import merge_files
-
-
-def run_job(job_number, executable, files):
-    file_list = f'file_list_{job_number:02d}.txt'
-    with open(file_list, 'w') as f:
-        f.write("\n".join(files))
-
-    output_filename = f'output_{job_number:02d}.root'
-    ret = subprocess.run([executable, '-s', '-F', file_list,
-                          '-o', output_filename])
-    retcode = ret.returncode
-    if retcode != 0:
-        raise RuntimeError(f'Job {job_number} encountered errors!'
-                           f'(retcode: {retcode}), check log file.')
-    return (job_number, output_filename)
-
-
-if __name__ == '__main__':
-    parser = argparse.ArgumentParser()
-    add = parser.add_argument
-
-    add('executable')
-    add('--jobs', '-j', type=int, default=multiprocessing.cpu_count())
-    add('--dir', '-d', default='./data')
-    add('--mergeinto', '-m', default='output.root')
-    args = parser.parse_args()
-
-    if not isdir(args.dir):
-        raise ValueError(f'Directory {args.dir} does not exist')
-
-    files = sorted([join(args.dir, fname) for fname in listdir(args.dir) if fname[-5:] == '.root'])
-
-    files_per_job = len(files) // args.jobs
-    job_files = [files[i::args.jobs] for i in range(args.jobs)]
-    output_files = []
-
-    def job_callback(args):
-        job_num, job_file = args
-        print(f'job {job_num} finished')
-        output_files.append(job_file)
-
-    with Pool(args.jobs) as pool:
-        print(f'Starting {args.jobs} processes to process {len(files)} files')
-        results = []
-        for i, files in enumerate(job_files):
-            results.append(pool.apply_async(run_job, (i, args.executable, files),
-                           callback=job_callback))
-        for result in results: result.get()
-        pool.close()
-        pool.join()
-        print('Finished processing nTuples.')
-    print('Begin merging job files')
-    merge_files(output_files, args.mergeinto)

+ 7 - 7
setup.py

@@ -4,15 +4,15 @@ with open('requirements.txt') as req:
     install_requires = [l.strip() for l in req.readlines()]
 
 setup(
-    name='filval',
-    version='0.1',
+    name='matplotboard',
+    version='0.2.0',
     install_requires=install_requires,
     dependency_links=[
         "git+ssh://git@github.com/cfangmeier/latexipy.git#egg=latexipy"
     ],
-    packages=['filval'],
-    scripts=['scripts/merge.py',
-             'scripts/process_parallel.py'
-             ],
-    package_data={'filval': ['templates/*.j2']},
+    packages=['matplotboard'],
+    package_data={'matplotboard': ['templates/*.j2',
+                                   'static/css/*.css',
+                                   'static/js/*.js',
+                                   ]},
 )