Source code for pythiaplotter.printers.web_printer

"""Print webpage with interactive graph.

This uses vis.js to do all the hard work: http://visjs.org/,
but we still use graphviz to do the layout for (a) parity with the PDF version,
(b) because it is faster, whereas web pages (so far) crash.

Of course if I could find a graphviz-as-as-JS service, that would be cooler...
"""


from __future__ import absolute_import, print_function
import json
from string import Template
from subprocess import PIPE, Popen
from pkg_resources import resource_filename
from pythiaplotter.utils.logging_config import get_logger
from pythiaplotter.utils.common import generate_repr_str
from pythiaplotter.utils.pdgid_converter import pdgid_to_string


log = get_logger(__name__)


[docs]class VisPrinter(object): def __init__(self, opts): """ Parameters ---------- opts : Argparse.Namespace Set of options from the arg parser. Attributes ---------- output_filename : str Final web page output filename renderer : str Graphviz program to use for rendering layout, default is dot since dealing with DAGs graph_opts : dict Dict of Graphviz attributes for the whole graph (e.g. direction, nodesep) """ self.output_filename = opts.output self.renderer = opts.layout self.graph_opts = opts.GRAPH_OPTS # self.pythia_statusfile = 'particledata/pythia6status.json' if opts.inputFormat == "CMSSW" else def __repr__(self): return generate_repr_str(self)
[docs] def print_event(self, event): """Calculate layout, add to graph nodes, and make website file for this event. Parameters ---------- event : Event """ gv_str = construct_gv_only_edges(event.graph, self.graph_opts) raw_json = get_dot_json(gv_str, self.renderer) add_node_positions(event.graph, raw_json) vis_node_dicts, vis_edge_dicts = create_vis_dicts(event.graph) pythia8status_file = resource_filename('pythiaplotter', 'particledata/pythia6status.json') with open(pythia8status_file) as f: pythia8status = f.read() dkwargs = dict(indent=None, sort_keys=True) field_data = dict( title=event.title, inputfile=event.source, eventnum=event.event_num, nodedata=json.dumps(vis_node_dicts, **dkwargs), edgedata=json.dumps(vis_edge_dicts, **dkwargs), pythia8status=pythia8status ) # create new webpage write_webpage(field_data, self.output_filename)
[docs]def construct_gv_only_edges(graph, graph_attr=None): """Create a graph in DOT language with just edges specified. This is a minimal graph, just used to determine the node positioning. Parameters ---------- graph : NetworkX.MultiDiGraph graph_attr : dict, optional Graph attributes such as rankdir, nodesep Returns ------- str The graph in DOT language """ gv_str = ["digraph g{"] if graph_attr: for k, v in graph_attr.items(): gv_str.append("{}={};".format(k, v)) for out_node, in_node in graph.edges_iter(data=False): gv_str.append("{0} -> {1};".format(out_node, in_node)) initial = ' '.join([str(node) for node, node_data in graph.nodes_iter(data=True) if len(graph.predecessors(node)) == 0]) gv_str.append("{{rank=same; {0} }};".format(initial)) gv_str.append("}") return "".join(gv_str)
[docs]def get_dot_json(graphviz_str, renderer="dot"): """Get the JSON output (with co-ords) from running a layout renderer. Parameters ---------- graphviz_str : str Graph in DOT language. renderer : str, optional Renderer to use. Default is dot. Returns ------- str JSON string """ dot_args = [renderer, "-Tjson0"] p = Popen(dot_args, stdin=PIPE, stdout=PIPE, stderr=PIPE) out, err = p.communicate(input=graphviz_str.encode()) if p.returncode != 0: raise RuntimeError(err) return out.decode()
[docs]def add_node_positions(graph, raw_json): """Update graph nodes with their positions, using info in `raw_json`. Parameters ---------- graph : NetworkX.MultiDiGraph Graph to be updated raw_json : str JSON with nodes & their positions """ gv_dict = json.loads(raw_json) # add node positions. for obj in gv_dict['objects']: # skip not proper nodes if 'nodes' in obj: continue barcode = int(obj['name']) x, y = obj['pos'].split(',') x, y = float(x), float(y) graph.node[barcode]['pos'] = (x, y)
[docs]def create_vis_dicts(graph): """Create list of dicts for nodes & edges suitable for input to vis.js This includes node position, label, hover info, etc Parameters ---------- graph : NetworkX.MultiDiGraph Returns ------- list[dict], list[dict] Lists of dicts corresponding to (nodes, edges) """ def _generate_particle_opts(particle): pd = { 'label': pdgid_to_string(particle.pdgid), 'name': pdgid_to_string(particle.pdgid), 'title': "", # does tooltip, control in webpage itself 'group': "default" } attr = particle.__dict__ for k, v in attr.items(): if isinstance(v, float): attr[k] = "%.3g" % v pd.update(**attr) if particle.initial_state: pd['group'] = 'initial' if particle.final_state: pd['group'] = 'final' pd['originalGroup'] = pd['group'] return pd node_dicts = [] for node, node_data in graph.nodes_iter(data=True): nd = { "id": node, "label": "", "x": node_data['pos'][0], "y": node_data['pos'][1] } if 'particle' in node_data: nd.update(_generate_particle_opts(node_data['particle'])) node_dicts.append(nd) edge_dicts = [] for out_vtx, in_vtx, edge_data in graph.edges_iter(data=True): ed = {"from": out_vtx, "to": in_vtx} if 'particle' in edge_data: ed.update(_generate_particle_opts(edge_data['particle'])) edge_dicts.append(ed) return node_dicts, edge_dicts
[docs]def write_webpage(field_data, output_filename): """Write webpage using template file and filling with user data. Parameters ---------- field_data: dict Dict of template {field name: value str} to be replaced output_filename : str Output HTML filename """ template_file = resource_filename('pythiaplotter', 'printers/templates/vis_template.html') with open(template_file, "r") as f: template = f.read() template = Template(template).safe_substitute(field_data) with open(output_filename, 'w') as f: f.write(template) log.info("Webpage written to %s", output_filename)