<commit_before>#!/usr/bin/env python
# -*- coding: UTF-8 -*-
from __future__ import print_function
import sys
try:
    from exceptions import EOFError
except ImportError:
    pass

import collections as cx

typedef_tag, term_tag = "[Typedef]", "[Term]"
GraphEngines = ("pygraphviz", "pydot")

def after_colon(line):
    # macro for getting anything after the :
    return line.split(":", 1)[1].strip()

def read_until(handle, start):
    # read each line until it has a certain start, and then puts
    # the start tag back
    while 1:
        pos = handle.tell()
        line = handle.readline()
        if not line:
            break
        if line.startswith(start):
            handle.seek(pos)
            return
    raise EOFError("%s tag cannot be found" % start)

class OBOReader:
    """
    parse obo file, usually the most updated can be downloaded from
    http://purl.obolibrary.org/obo/go/go-basic.obo

    >>> reader = OBOReader()
    >>> for rec in reader:
            print rec

    """

    def __init__(self, obo_file="go-basic.obo"):

        try:
            self._handle = open(obo_file)
        except:
            print(("download obo file first\n "
                                 "[http://purl.obolibrary.org/obo/"
                                 "go/go-basic.obo]"), file=sys.stderr)
            sys.exit(1)

    def __iter__(self):

        line = self._handle.readline()
        if not line.startswith(term_tag):
            read_until(self._handle, term_tag)
        while 1:
            yield self.__next__()

    def __next__(self):

        lines = []
        line = self._handle.readline()
        if not line or line.startswith(typedef_tag):
            raise StopIteration

        # read until the next tag and save everything in between
        while 1:
            pos = self._handle.tell()   # save current postion for roll-back
            line = self._handle.readline()
            if not line or (line.startswith(typedef_tag)
                            or line.startswith(term_tag)):
                self._handle.seek(pos)  # roll-back
                break
            lines.append(line)

        rec = GOTerm()
        for line in lines:
            if line.startswith("id:"):
                rec.id = after_colon(line)
                num = rec.id.replace("GO:", "", 1)
                if num.isdigit():
                  rec.id_num = int(num)
            if line.startswith("alt_id:"):
                rec.alt_ids.append(after_colon(line))
            elif line.startswith("name:"):
                rec.name = after_colon(line)
            elif line.startswith("namespace:"):
                rec.namespace = after_colon(line)
            elif line.startswith("is_a:"):
                rec._parents.append(after_colon(line).split()[0])
            elif (line.startswith("is_obsolete:") and
                  after_colon(line) == "true"):
                rec.is_obsolete = True

        return rec

class GOTerm:
    """
    GO term, actually contain a lot more properties than interfaced here
    """

    def __init__(self):
        self.id = ""                # GO:xxxxxx
        self.id_num = None          # GO term integer
        self.name = ""              # description
        self.namespace = ""         # BP, CC, MF
        self._parents = []          # is_a basestring of parents
        self.parents = []           # parent records
        self.children = []          # children records
        self.level = None           # shortest distance from root node
        self.depth = None           # longest distance from root node
        self.is_obsolete = False    # is_obsolete
        self.alt_ids = []           # alternative identifiers

    def __str__(self):
        obsolete = "obsolete" if self.is_obsolete else ""
        return "%s\tlevel-%02d\tdepth-%02d\t%s [%s] %s" % (self.id, self.level, self.depth,
                                               self.name, self.namespace, obsolete)

    def __repr__(self):
        return "GOTerm('%s')" % (self.id)

    def has_parent(self, term):
        for p in self.parents:
            if p.id == term or p.has_parent(term):
                return True
        return False

    def has_child(self, term):
        for p in self.children:
            if p.id == term or p.has_child(term):
                return True
        return False

    def get_all_parents(self):
        all_parents = set()
        for p in self.parents:
            all_parents.add(p.id)
            all_parents |= p.get_all_parents()
        return all_parents

    def get_all_children(self):
        all_children = set()
        for p in self.children:
            all_children.add(p.id)
            all_children |= p.get_all_children()
        return all_children

    def get_all_parent_edges(self):
        all_parent_edges = set()
        for p in self.parents:
            all_parent_edges.add((self.id, p.id))
            all_parent_edges |= p.get_all_parent_edges()
        return all_parent_edges

    def get_all_child_edges(self):
        all_child_edges = set()
        for p in self.children:
            all_child_edges.add((p.id, self.id))
            all_child_edges |= p.get_all_child_edges()
        return all_child_edges

class GODag(dict):

    def __init__(self, obo_file="go-basic.obo"):

        self.load_obo_file(obo_file)

    def load_obo_file(self, obo_file):

        print("load obo file %s" % obo_file, file=sys.stderr)
        obo_reader = OBOReader(obo_file)
        for rec in obo_reader:
            self[rec.id] = rec
            for alt in rec.alt_ids:
                self[alt] = rec

        self.populate_terms()
        print(len(self), "nodes imported", file=sys.stderr)

    def populate_terms(self):

        def _init_level(rec):
            if rec.level is None:
                if not rec.parents:
                    rec.level = 0
                else:
                    rec.level = min(_init_level(rec) for rec in rec.parents) + 1
            return rec.level

        def _init_depth(rec):
            if rec.depth is None:
                if not rec.parents:
                    rec.depth = 0
                else:
                    rec.depth = max(_init_depth(rec) for rec in rec.parents) + 1
            return rec.depth

        # make the parents references to the GO terms
        for rec in self.values():
            rec.parents = [self[x] for x in rec._parents]

        # populate children and levels
        for rec in self.values():
            for p in rec.parents:
                p.children.append(rec)

            if rec.level is None:
                _init_level(rec)

            if rec.depth is None:
                _init_depth(rec)

    def write_dag(self, out=sys.stdout):
        """Write info for all GO Term in obo file, sorted numerically."""
        for rec_id, rec in sorted(self.items()):
            print(rec, file=out)

    def write_summary_cnts(self, out=sys.stdout):
        """Write summary of level and depth counts for all active GO Terms."""
        # Count level and depth values for all unique GO Terms.
        cnts = self.get_cnts_levels_depths(set(self.values()))
        max_val = max(max(dep for dep in cnts['depth']), max(lev for lev in cnts['level']))
        nss = ['biological_process', 'molecular_function', 'cellular_component']
        out.write('Lev <-Depth Counts->  <-Level Counts->\n')
        out.write('Dep   BP    MF    CC    BP    MF    CC\n')
        out.write('--- ----  ----  ----  ----  ----  ----\n')
        for i in range(max_val+1):
            vals = ['{:>5}'.format(cnts[desc][i][ns]) for desc in cnts for ns in nss]
            out.write('{:>02} {}\n'.format(i, ' '.join(vals)))

    def get_cnts_levels_depths(self, recs):
        """Collect counts of levels and depths in a Group of GO Terms."""
        cnts = cx.defaultdict(lambda: cx.defaultdict(cx.Counter))
        for rec in recs:
            if not rec.is_obsolete:
                cnts['level'][rec.level][rec.namespace] += 1
                cnts['depth'][rec.depth][rec.namespace] += 1
        return cnts

    def query_term(self, term, verbose=False):
        if term not in self:
            print("Term %s not found!" % term, file=sys.stderr)
            return

        rec = self[term]
        print(rec, file=sys.stderr)
        if verbose:
            print("all parents:", rec.get_all_parents(), file=sys.stderr)
            print("all children:", rec.get_all_children(), file=sys.stderr)
        return rec

    def paths_to_top(self, term, verbose=False):
        """ Returns all possible paths to the root node

            Each path includes the term given. The order of the path is
            top -> bottom, i.e. it starts with the root and ends with the
            given term (inclusively).

            Parameters:
            -----------
            - term:
                the id of the GO term, where the paths begin (i.e. the
                accession 'GO:0003682')

            Returns:
            --------
            - a list of lists of GO Terms
        """
        # error handling consistent with original authors
        if term not in self:
            print("Term %s not found!" % term, file=sys.stderr)
            return

        def _paths_to_top_recursive(rec):
            if rec.level == 0:
                return [[rec]]
            paths = []
            for parent in rec.parents:
                top_paths = _paths_to_top_recursive(parent)
                for top_path in top_paths:
                    top_path.append(rec)
                    paths.append(top_path)
            return paths

        go_term = self[term]
        return _paths_to_top_recursive(go_term)

    def _label_wrap(self, label):
        wrapped_label = r"%s\n%s" % (label,
                                     self[label].name.replace(",", r"\n"))
        return wrapped_label

    def make_graph_pydot(self, recs, nodecolor,
                     edgecolor, dpi,
                     draw_parents=True, draw_children=True):
        """draw AMIGO style network, lineage containing one query record."""
        import pydot
        G = pydot.Dot(graph_type='digraph', dpi="{}".format(dpi)) # Directed Graph
        edgeset = set()
        usr_ids = [rec.id for rec in recs]
        for rec in recs:
            if draw_parents:
                edgeset.update(rec.get_all_parent_edges())
            if draw_children:
                edgeset.update(rec.get_all_child_edges())

        lw = self._label_wrap
        rec_id_set = set([rec_id for endpts in edgeset for rec_id in endpts])
        nodes = {str(ID):pydot.Node(
              lw(ID).replace("GO:",""),  # Node name
              shape="box",
              style="rounded, filled",
              # Highlight query terms in plum:
              fillcolor="beige" if ID not in usr_ids else "plum",
              color=nodecolor)
                for ID in rec_id_set}

        # add nodes explicitly via add_node
        for rec_id, node in nodes.items():
            G.add_node(node)

        for src, target in edgeset:
            # default layout in graphviz is top->bottom, so we invert
            # the direction and plot using dir="back"
            G.add_edge(pydot.Edge(nodes[target], nodes[src],
              shape="normal",
              color=edgecolor,
              label="is_a",
              dir="back"))

        return G

    def make_graph_pygraphviz(self, recs, nodecolor,
                     edgecolor, dpi,
                     draw_parents=True, draw_children=True):
        # draw AMIGO style network, lineage containing one query record
        import pygraphviz as pgv

        G = pgv.AGraph(name="GO tree")
        edgeset = set()
        for rec in recs:
            if draw_parents:
                edgeset.update(rec.get_all_parent_edges())
            if draw_children:
                edgeset.update(rec.get_all_child_edges())

        edgeset = [(self._label_wrap(a), self._label_wrap(b))
                   for (a, b) in edgeset]

        # add nodes explicitly via add_node
        # adding nodes implicitly via add_edge misses nodes
        # without at least one edge
        for rec in recs:
            G.add_node(self._label_wrap(rec.id))

        for src, target in edgeset:
            # default layout in graphviz is top->bottom, so we invert
            # the direction and plot using dir="back"
            G.add_edge(target, src)

        G.graph_attr.update(dpi="%d" % dpi)
        G.node_attr.update(shape="box", style="rounded,filled",
                           fillcolor="beige", color=nodecolor)
        G.edge_attr.update(shape="normal", color=edgecolor,
                           dir="back", label="is_a")
        # highlight the query terms
        for rec in recs:
            try:
                q = G.get_node(self._label_wrap(rec.id))
                q.attr.update(fillcolor="plum")
            except:
                continue

        return G

    def draw_lineage(self, recs, nodecolor="mediumseagreen",
                     edgecolor="lightslateblue", dpi=96,
                     lineage_img="GO_lineage.png", engine="pygraphviz",
                     gml=False, draw_parents=True, draw_children=True):
        assert engine in GraphEngines
        if engine == "pygraphviz":
            G = self.make_graph_pygraphviz(recs, nodecolor, edgecolor, dpi,
                              draw_parents=draw_parents, draw_children=draw_children)
        else:
            G = self.make_graph_pydot(recs, nodecolor, edgecolor, dpi,
                              draw_parents=draw_parents, draw_children=draw_children)

        if gml:
            import networkx as nx  # use networkx to do the conversion
            pf = lineage_img.rsplit(".", 1)[0]
            NG = nx.from_agraph(G) if engine == "pygraphviz" else nx.from_pydot(G)

            del NG.graph['node']
            del NG.graph['edge']
            gmlfile = pf + ".gml"
            nx.write_gml(NG, gmlfile)
            print("GML graph written to {0}".format(gmlfile), file=sys.stderr)

        print(("lineage info for terms %s written to %s" %
                             ([rec.id for rec in recs], lineage_img)), file=sys.stderr)

        if engine == "pygraphviz":
            G.draw(lineage_img, prog="dot")
        else:
            G.write_png(lineage_img)

    def update_association(self, association):
        bad_terms = set()
        for key, terms in list(association.items()):
            parents = set()
            for term in terms:
                try:
                    parents.update(self[term].get_all_parents())
                except:
                    bad_terms.add(term.strip())
            terms.update(parents)
        if bad_terms:
            print("terms not found: %s" % (bad_terms,), file=sys.stderr)
<commit_msg>Minor changes for easier usage by downstream scripts.<commit_after>#!/usr/bin/env python
# -*- coding: UTF-8 -*-
from __future__ import print_function
import sys
try:
    from exceptions import EOFError
except ImportError:
    pass

import collections as cx

typedef_tag, term_tag = "[Typedef]", "[Term]"
GraphEngines = ("pygraphviz", "pydot")

def after_colon(line):
    # macro for getting anything after the :
    return line.split(":", 1)[1].strip()

def read_until(handle, start):
    # read each line until it has a certain start, and then puts
    # the start tag back
    while 1:
        pos = handle.tell()
        line = handle.readline()
        if not line:
            break
        if line.startswith(start):
            handle.seek(pos)
            return
    raise EOFError("%s tag cannot be found" % start)

class OBOReader:
    """
    parse obo file, usually the most updated can be downloaded from
    http://purl.obolibrary.org/obo/go/go-basic.obo

    >>> reader = OBOReader()
    >>> for rec in reader:
            print rec

    """

    def __init__(self, obo_file="go-basic.obo"):

        try:
            self._handle = open(obo_file)
        except:
            print(("download obo file first\n "
                                 "[http://purl.obolibrary.org/obo/"
                                 "go/go-basic.obo]"), file=sys.stderr)
            sys.exit(1)

    def __iter__(self):

        line = self._handle.readline()
        if not line.startswith(term_tag):
            read_until(self._handle, term_tag)
        while 1:
            yield self.__next__()

    def __next__(self):

        lines = []
        line = self._handle.readline()
        if not line or line.startswith(typedef_tag):
            raise StopIteration

        # read until the next tag and save everything in between
        while 1:
            pos = self._handle.tell()   # save current postion for roll-back
            line = self._handle.readline()
            if not line or (line.startswith(typedef_tag)
                            or line.startswith(term_tag)):
                self._handle.seek(pos)  # roll-back
                break
            lines.append(line)

        rec = GOTerm()
        for line in lines:
            if line.startswith("id:"):
                rec.id = after_colon(line)
                num = rec.id.replace("GO:", "", 1)
                if num.isdigit():
                  rec.id_num = int(num)
            if line.startswith("alt_id:"):
                rec.alt_ids.append(after_colon(line))
            elif line.startswith("name:"):
                rec.name = after_colon(line)
            elif line.startswith("namespace:"):
                rec.namespace = after_colon(line)
            elif line.startswith("is_a:"):
                rec._parents.append(after_colon(line).split()[0])
            elif (line.startswith("is_obsolete:") and
                  after_colon(line) == "true"):
                rec.is_obsolete = True

        return rec

class GOTerm:
    """
    GO term, actually contain a lot more properties than interfaced here
    """

    def __init__(self):
        self.id = ""                # GO:xxxxxx
        self.id_num = None          # GO term integer
        self.name = ""              # description
        self.namespace = ""         # BP, CC, MF
        self._parents = []          # is_a basestring of parents
        self.parents = []           # parent records
        self.children = []          # children records
        self.level = None           # shortest distance from root node
        self.depth = None           # longest distance from root node
        self.is_obsolete = False    # is_obsolete
        self.alt_ids = []           # alternative identifiers

    def __str__(self):
        obsolete = "obsolete" if self.is_obsolete else ""
        return "%s\tlevel-%02d\tdepth-%02d\t%s [%s] %s" % (self.id, self.level, self.depth,
                                               self.name, self.namespace, obsolete)

    def __repr__(self):
        return "GOTerm('%s')" % (self.id)

    def has_parent(self, term):
        for p in self.parents:
            if p.id == term or p.has_parent(term):
                return True
        return False

    def has_child(self, term):
        for p in self.children:
            if p.id == term or p.has_child(term):
                return True
        return False

    def get_all_parents(self):
        all_parents = set()
        for p in self.parents:
            all_parents.add(p.id)
            all_parents |= p.get_all_parents()
        return all_parents

    def get_all_children(self):
        all_children = set()
        for p in self.children:
            all_children.add(p.id)
            all_children |= p.get_all_children()
        return all_children

    def get_all_parent_edges(self):
        all_parent_edges = set()
        for p in self.parents:
            all_parent_edges.add((self.id, p.id))
            all_parent_edges |= p.get_all_parent_edges()
        return all_parent_edges

    def get_all_child_edges(self):
        all_child_edges = set()
        for p in self.children:
            all_child_edges.add((p.id, self.id))
            all_child_edges |= p.get_all_child_edges()
        return all_child_edges

class GODag(dict):

    def __init__(self, obo_file="go-basic.obo"):

        self.load_obo_file(obo_file)

    def load_obo_file(self, obo_file):

        print("load obo file %s" % obo_file, file=sys.stderr)
        obo_reader = OBOReader(obo_file)
        for rec in obo_reader:
            self[rec.id] = rec
            for alt in rec.alt_ids:
                self[alt] = rec

        self.populate_terms()
        print(len(self), "nodes imported", file=sys.stderr)

    def populate_terms(self):

        def _init_level(rec):
            if rec.level is None:
                if not rec.parents:
                    rec.level = 0
                else:
                    rec.level = min(_init_level(rec) for rec in rec.parents) + 1
            return rec.level

        def _init_depth(rec):
            if rec.depth is None:
                if not rec.parents:
                    rec.depth = 0
                else:
                    rec.depth = max(_init_depth(rec) for rec in rec.parents) + 1
            return rec.depth

        # make the parents references to the GO terms
        for rec in self.values():
            rec.parents = [self[x] for x in rec._parents]

        # populate children and levels
        for rec in self.values():
            for p in rec.parents:
                p.children.append(rec)

            if rec.level is None:
                _init_level(rec)

            if rec.depth is None:
                _init_depth(rec)

    def write_dag(self, out=sys.stdout):
        """Write info for all GO Term in obo file, sorted numerically."""
        for rec_id, rec in sorted(self.items()):
            print(rec, file=out)

    def write_summary_cnts(self, out=sys.stdout):
        """Write summary of level and depth counts for all active GO Terms."""
        # Count level(shortest path to root) and depth(longest path to root)
        # values for all unique GO Terms.
        cnts = self.get_cnts_levels_depths_all()
        max_val = max(max(dep for dep in cnts['depth']), max(lev for lev in cnts['level']))
        nss = ['biological_process', 'molecular_function', 'cellular_component']
        out.write('Dep <-Depth Counts->  <-Level Counts->\n')
        out.write('Lev   BP    MF    CC    BP    MF    CC\n')
        out.write('--- ----  ----  ----  ----  ----  ----\n')
        for i in range(max_val+1):
            vals = ['{:>5}'.format(cnts[desc][i][ns]) for desc in cnts for ns in nss]
            out.write('{:>02} {}\n'.format(i, ' '.join(vals)))

    def get_cnts_levels_depths_all(self):
        """Collect counts of levels and depths in all GO Terms."""
        return GODag.get_cnts_levels_depths(set(self.values()))

    @staticmethod
    def get_cnts_levels_depths(recs):
        """Collect counts of levels and depths in a Group of GO Terms."""
        cnts = cx.defaultdict(lambda: cx.defaultdict(cx.Counter))
        for rec in recs:
            if not rec.is_obsolete:
                cnts['level'][rec.level][rec.namespace] += 1
                cnts['depth'][rec.depth][rec.namespace] += 1
        return cnts

    def query_term(self, term, verbose=False):
        if term not in self:
            print("Term %s not found!" % term, file=sys.stderr)
            return

        rec = self[term]
        print(rec, file=sys.stderr)
        if verbose:
            print("all parents:", rec.get_all_parents(), file=sys.stderr)
            print("all children:", rec.get_all_children(), file=sys.stderr)
        return rec

    def paths_to_top(self, term, verbose=False):
        """ Returns all possible paths to the root node

            Each path includes the term given. The order of the path is
            top -> bottom, i.e. it starts with the root and ends with the
            given term (inclusively).

            Parameters:
            -----------
            - term:
                the id of the GO term, where the paths begin (i.e. the
                accession 'GO:0003682')

            Returns:
            --------
            - a list of lists of GO Terms
        """
        # error handling consistent with original authors
        if term not in self:
            print("Term %s not found!" % term, file=sys.stderr)
            return

        def _paths_to_top_recursive(rec):
            if rec.level == 0:
                return [[rec]]
            paths = []
            for parent in rec.parents:
                top_paths = _paths_to_top_recursive(parent)
                for top_path in top_paths:
                    top_path.append(rec)
                    paths.append(top_path)
            return paths

        go_term = self[term]
        return _paths_to_top_recursive(go_term)

    def _label_wrap(self, label):
        wrapped_label = r"%s\n%s" % (label,
                                     self[label].name.replace(",", r"\n"))
        return wrapped_label

    def make_graph_pydot(self, recs, nodecolor,
                     edgecolor, dpi,
                     draw_parents=True, draw_children=True):
        """draw AMIGO style network, lineage containing one query record."""
        import pydot
        G = pydot.Dot(graph_type='digraph', dpi="{}".format(dpi)) # Directed Graph
        edgeset = set()
        usr_ids = [rec.id for rec in recs]
        for rec in recs:
            if draw_parents:
                edgeset.update(rec.get_all_parent_edges())
            if draw_children:
                edgeset.update(rec.get_all_child_edges())

        lw = self._label_wrap
        rec_id_set = set([rec_id for endpts in edgeset for rec_id in endpts])
        nodes = {str(ID):pydot.Node(
              lw(ID).replace("GO:",""),  # Node name
              shape="box",
              style="rounded, filled",
              # Highlight query terms in plum:
              fillcolor="beige" if ID not in usr_ids else "plum",
              color=nodecolor)
                for ID in rec_id_set}

        # add nodes explicitly via add_node
        for rec_id, node in nodes.items():
            G.add_node(node)

        for src, target in edgeset:
            # default layout in graphviz is top->bottom, so we invert
            # the direction and plot using dir="back"
            G.add_edge(pydot.Edge(nodes[target], nodes[src],
              shape="normal",
              color=edgecolor,
              label="is_a",
              dir="back"))

        return G

    def make_graph_pygraphviz(self, recs, nodecolor,
                     edgecolor, dpi,
                     draw_parents=True, draw_children=True):
        # draw AMIGO style network, lineage containing one query record
        import pygraphviz as pgv

        G = pgv.AGraph(name="GO tree")
        edgeset = set()
        for rec in recs:
            if draw_parents:
                edgeset.update(rec.get_all_parent_edges())
            if draw_children:
                edgeset.update(rec.get_all_child_edges())

        edgeset = [(self._label_wrap(a), self._label_wrap(b))
                   for (a, b) in edgeset]

        # add nodes explicitly via add_node
        # adding nodes implicitly via add_edge misses nodes
        # without at least one edge
        for rec in recs:
            G.add_node(self._label_wrap(rec.id))

        for src, target in edgeset:
            # default layout in graphviz is top->bottom, so we invert
            # the direction and plot using dir="back"
            G.add_edge(target, src)

        G.graph_attr.update(dpi="%d" % dpi)
        G.node_attr.update(shape="box", style="rounded,filled",
                           fillcolor="beige", color=nodecolor)
        G.edge_attr.update(shape="normal", color=edgecolor,
                           dir="back", label="is_a")
        # highlight the query terms
        for rec in recs:
            try:
                q = G.get_node(self._label_wrap(rec.id))
                q.attr.update(fillcolor="plum")
            except:
                continue

        return G

    def draw_lineage(self, recs, nodecolor="mediumseagreen",
                     edgecolor="lightslateblue", dpi=96,
                     lineage_img="GO_lineage.png", engine="pygraphviz",
                     gml=False, draw_parents=True, draw_children=True):
        assert engine in GraphEngines
        if engine == "pygraphviz":
            G = self.make_graph_pygraphviz(recs, nodecolor, edgecolor, dpi,
                              draw_parents=draw_parents, draw_children=draw_children)
        else:
            G = self.make_graph_pydot(recs, nodecolor, edgecolor, dpi,
                              draw_parents=draw_parents, draw_children=draw_children)

        if gml:
            import networkx as nx  # use networkx to do the conversion
            pf = lineage_img.rsplit(".", 1)[0]
            NG = nx.from_agraph(G) if engine == "pygraphviz" else nx.from_pydot(G)

            del NG.graph['node']
            del NG.graph['edge']
            gmlfile = pf + ".gml"
            nx.write_gml(NG, gmlfile)
            print("GML graph written to {0}".format(gmlfile), file=sys.stderr)

        print(("lineage info for terms %s written to %s" %
                             ([rec.id for rec in recs], lineage_img)), file=sys.stderr)

        if engine == "pygraphviz":
            G.draw(lineage_img, prog="dot")
        else:
            G.write_png(lineage_img)

    def update_association(self, association):
        bad_terms = set()
        for key, terms in list(association.items()):
            parents = set()
            for term in terms:
                try:
                    parents.update(self[term].get_all_parents())
                except:
                    bad_terms.add(term.strip())
            terms.update(parents)
        if bad_terms:
            print("terms not found: %s" % (bad_terms,), file=sys.stderr)

<|endoftext|>


<commit_before>import os.path
import gdb
import glib_gdb
import sys

if sys.version_info[0] >= 3:
    long = int
else:
    import itertools
    map = itertools.imap

# FrameDecorator is new in gdb 7.7, so we adapt to its absence.
try:
    import gdb.FrameDecorator
    HAVE_GDB_FRAMEDECORATOR = True
    FrameDecorator = gdb.FrameDecorator.FrameDecorator
except ImportError:
    HAVE_GDB_FRAMEDECORATOR = False

# This is not quite right, as local vars may override symname
def read_global_var (symname):
    return gdb.selected_frame().read_var(symname)

def g_type_to_name (gtype):
    def lookup_fundamental_type (typenode):
        if typenode == 0:
            return None
        val = read_global_var ("static_fundamental_type_nodes")
        if val == None:
            return None
        return val[typenode >> 2].address()

    gtype = long(gtype)
    typenode = gtype - gtype % 4
    if typenode > (255 << 2):
        typenode = gdb.Value(typenode).cast (gdb.lookup_type("TypeNode").pointer())
    else:
        typenode = lookup_fundamental_type (typenode)
    if typenode != None:
        return glib_gdb.g_quark_to_string (typenode["qname"])
    return None

def is_g_type_instance (val):
    def is_g_type_instance_helper (type):
        if str(type) == "GTypeInstance":
            return True

        while type.code == gdb.TYPE_CODE_TYPEDEF:
            type = type.target()

        if type.code != gdb.TYPE_CODE_STRUCT:
            return False

        fields = type.fields()
        if len (fields) < 1:
            return False

        first_field = fields[0]
        return is_g_type_instance_helper(first_field.type)

    type = val.type
    if type.code != gdb.TYPE_CODE_PTR:
        return False
    type = type.target()
    return is_g_type_instance_helper (type)

def g_type_name_from_instance (instance):
    if long(instance) != 0:
        try:
            inst = instance.cast (gdb.lookup_type("GTypeInstance").pointer())
            klass = inst["g_class"]
            gtype = klass["g_type"]
            name = g_type_to_name (gtype)
            return name
        except RuntimeError:
            pass
    return None

class GTypePrettyPrinter:
    "Prints a GType instance pointer"

    def __init__ (self, val):
        self.val = val

    def to_string (self):
        name = g_type_name_from_instance (self.val)
        if name:
            return ("0x%x [%s]")% (long(self.val), name)
        return  ("0x%x") % (long(self.val))

def pretty_printer_lookup (val):
    if is_g_type_instance (val):
        return GTypePrettyPrinter (val)

    return None

def get_signal_name (id):
    if id == None:
        return None
    id = long(id)
    if id == 0:
        return None
    val = read_global_var ("g_signal_nodes")
    max_s = read_global_var ("g_n_signal_nodes")
    max_s = long(max_s)
    if id < max_s:
        return val[id]["name"].string()
    return None

def frame_name(frame):
    return str(frame.function())

def frame_var(frame, var):
    return frame.inferior_frame().read_var(var)

class SignalFrame(FrameDecorator):
    def __init__ (self, frames):
        FrameDecorator.__init__(self, frames[-1])
        self.frame = frames[-1]
        self.frames = frames

    def name (self):
        return "signal-emission"

    def read_var (self, frame, name, array = None):
        try:
            v = frame_var (frame, name)
            if v == None or v.is_optimized_out:
                return None
            if array != None:
                array.append (v)
            return v
        except ValueError:
            return None

    def read_object (self, frame, name, array = None):
        try:
            v = frame_var (frame, name)
            if v == None or v.is_optimized_out:
                return None
            v = v.cast (gdb.lookup_type("GObject").pointer())
            # Ensure this is a somewhat correct object pointer
            if v != None and g_type_name_from_instance (v):
                if array != None:
                    array.append (v)
                return v
            return None
        except ValueError:
            return None

    def append (self, array, obj):
        if obj != None:
            array.append (obj)

    def or_join_array (self, array):
        if len(array) == 0:
            return "???"
        else:
            return ' or '.join(set(map(str, array)))

    def get_detailed_signal_from_frame(self, frame, signal):
        detail = self.read_var (frame, "detail")
        detail = glib_gdb.g_quark_to_string (detail)
        if detail is not None:
            return signal + ":" + detail
        else:
            return detail

    def function (self):
        instances = []
        signals = []

        for frame in self.frames:
            name = frame_name(frame)
            if name == "signal_emit_unlocked_R":
                self.read_object (frame, "instance", instances)
                node = self.read_var (frame, "node")
                if node:
                    signal = node["name"].string()
                    signal = self.get_detailed_signal_from_frame(frame, signal)
                    self.append(signals, signal)

            if name == "g_signal_emitv":
                instance_and_params = self.read_var (frame, "instance_and_params")
                if instance_and_params:
                    instance = instance_and_params[0]["v_pointer"].cast (gdb.Type("GObject").pointer())
                    self.append (instances, instance)
                id = self.read_var (frame, "signal_id")
                signal = get_signal_name (id)
                if signal:
                    signal = self.get_detailed_signal_from_frame(frame, signal)
                    self.append (signals, signal)

            if name == "g_signal_emit_valist" or name == "g_signal_emit":
                self.read_object (frame, "instance", instances)
                id = self.read_var (frame, "signal_id")
                signal = get_signal_name (id)
                if signal:
                    signal = self.get_detailed_signal_from_frame(frame, signal)
                    self.append (signals, signal)

            if name == "g_signal_emit_by_name":
                self.read_object (frame, "instance", instances)
                self.read_var (frame, "detailed_signal", signals)
                break

        instance = self.or_join_array (instances)
        signal = self.or_join_array (signals)

        return "<emit signal %s on instance %s>" %  (signal, instance)

    def elided (self):
        return self.frames[0:-1]

    def describe (self, stream, full):
        stream.write (" " + self.function () + "\n")

class GFrameDecorator:
    def __init__ (self, iter):
        self.queue = []
        self.iter = iter

    def __iter__ (self):
        return self

    def fill (self):
        while len(self.queue) <= 8:
            try:
                f = next(self.iter)
                self.queue.append (f)
            except StopIteration:
                return

    def find_signal_emission (self):
        for i in range (min (len(self.queue), 3)):
            if frame_name(self.queue[i]) == "signal_emit_unlocked_R":
                return i
        return -1

    def next (self):
        # Ensure we have enough frames for a full signal emission
        self.fill()

        # Are we at the end?
        if len(self.queue) == 0:
            raise StopIteration

        emission = self.find_signal_emission ()
        if emission > 0:
            start = emission
            while True:
                if start == 0:
                    break
                prev_name = frame_name(self.queue[start-1])
                if prev_name.find("_marshal_") >= 0 or prev_name == "g_closure_invoke":
                    start = start - 1
                else:
                    break
            end = emission + 1
            while end < len(self.queue):
                if frame_name(self.queue[end]) in ["g_signal_emitv",
                                                   "g_signal_emit_valist",
                                                   "g_signal_emit",
                                                   "g_signal_emit_by_name",
                                                   "_g_closure_invoke_va"]:
                    end = end + 1
                else:
                    break

            signal_frames = self.queue[start:end]
            new_frames = [SignalFrame(signal_frames)]
            self.queue[start:end] = new_frames

        return self.queue.pop(0)

    def __next__ (self):
        return self.next()

class GFrameFilter(object):
    name = 'glib'
    enabled = True
    priority = 100

    def filter(self, iterator):
        return GFrameDecorator(iterator)

def register (obj):
    if obj == None:
        obj = gdb

    if HAVE_GDB_FRAMEDECORATOR:
        filter = GFrameFilter()
        obj.frame_filters[filter.name] = filter
    obj.pretty_printers.append(pretty_printer_lookup)
<commit_msg>gobject_gdb.py: 'address' is a property of gdb.Value not a function<commit_after>import os.path
import gdb
import glib_gdb
import sys

if sys.version_info[0] >= 3:
    long = int
else:
    import itertools
    map = itertools.imap

# FrameDecorator is new in gdb 7.7, so we adapt to its absence.
try:
    import gdb.FrameDecorator
    HAVE_GDB_FRAMEDECORATOR = True
    FrameDecorator = gdb.FrameDecorator.FrameDecorator
except ImportError:
    HAVE_GDB_FRAMEDECORATOR = False

# This is not quite right, as local vars may override symname
def read_global_var (symname):
    return gdb.selected_frame().read_var(symname)

def g_type_to_name (gtype):
    def lookup_fundamental_type (typenode):
        if typenode == 0:
            return None
        val = read_global_var ("static_fundamental_type_nodes")
        if val == None:
            return None
        return val[typenode >> 2].address

    gtype = long(gtype)
    typenode = gtype - gtype % 4
    if typenode > (255 << 2):
        typenode = gdb.Value(typenode).cast (gdb.lookup_type("TypeNode").pointer())
    else:
        typenode = lookup_fundamental_type (typenode)
    if typenode != None:
        return glib_gdb.g_quark_to_string (typenode["qname"])
    return None

def is_g_type_instance (val):
    def is_g_type_instance_helper (type):
        if str(type) == "GTypeInstance":
            return True

        while type.code == gdb.TYPE_CODE_TYPEDEF:
            type = type.target()

        if type.code != gdb.TYPE_CODE_STRUCT:
            return False

        fields = type.fields()
        if len (fields) < 1:
            return False

        first_field = fields[0]
        return is_g_type_instance_helper(first_field.type)

    type = val.type
    if type.code != gdb.TYPE_CODE_PTR:
        return False
    type = type.target()
    return is_g_type_instance_helper (type)

def g_type_name_from_instance (instance):
    if long(instance) != 0:
        try:
            inst = instance.cast (gdb.lookup_type("GTypeInstance").pointer())
            klass = inst["g_class"]
            gtype = klass["g_type"]
            name = g_type_to_name (gtype)
            return name
        except RuntimeError:
            pass
    return None

class GTypePrettyPrinter:
    "Prints a GType instance pointer"

    def __init__ (self, val):
        self.val = val

    def to_string (self):
        name = g_type_name_from_instance (self.val)
        if name:
            return ("0x%x [%s]")% (long(self.val), name)
        return  ("0x%x") % (long(self.val))

def pretty_printer_lookup (val):
    if is_g_type_instance (val):
        return GTypePrettyPrinter (val)

    return None

def get_signal_name (id):
    if id == None:
        return None
    id = long(id)
    if id == 0:
        return None
    val = read_global_var ("g_signal_nodes")
    max_s = read_global_var ("g_n_signal_nodes")
    max_s = long(max_s)
    if id < max_s:
        return val[id]["name"].string()
    return None

def frame_name(frame):
    return str(frame.function())

def frame_var(frame, var):
    return frame.inferior_frame().read_var(var)

class SignalFrame(FrameDecorator):
    def __init__ (self, frames):
        FrameDecorator.__init__(self, frames[-1])
        self.frame = frames[-1]
        self.frames = frames

    def name (self):
        return "signal-emission"

    def read_var (self, frame, name, array = None):
        try:
            v = frame_var (frame, name)
            if v == None or v.is_optimized_out:
                return None
            if array != None:
                array.append (v)
            return v
        except ValueError:
            return None

    def read_object (self, frame, name, array = None):
        try:
            v = frame_var (frame, name)
            if v == None or v.is_optimized_out:
                return None
            v = v.cast (gdb.lookup_type("GObject").pointer())
            # Ensure this is a somewhat correct object pointer
            if v != None and g_type_name_from_instance (v):
                if array != None:
                    array.append (v)
                return v
            return None
        except ValueError:
            return None

    def append (self, array, obj):
        if obj != None:
            array.append (obj)

    def or_join_array (self, array):
        if len(array) == 0:
            return "???"
        else:
            return ' or '.join(set(map(str, array)))

    def get_detailed_signal_from_frame(self, frame, signal):
        detail = self.read_var (frame, "detail")
        detail = glib_gdb.g_quark_to_string (detail)
        if detail is not None:
            return signal + ":" + detail
        else:
            return detail

    def function (self):
        instances = []
        signals = []

        for frame in self.frames:
            name = frame_name(frame)
            if name == "signal_emit_unlocked_R":
                self.read_object (frame, "instance", instances)
                node = self.read_var (frame, "node")
                if node:
                    signal = node["name"].string()
                    signal = self.get_detailed_signal_from_frame(frame, signal)
                    self.append(signals, signal)

            if name == "g_signal_emitv":
                instance_and_params = self.read_var (frame, "instance_and_params")
                if instance_and_params:
                    instance = instance_and_params[0]["v_pointer"].cast (gdb.Type("GObject").pointer())
                    self.append (instances, instance)
                id = self.read_var (frame, "signal_id")
                signal = get_signal_name (id)
                if signal:
                    signal = self.get_detailed_signal_from_frame(frame, signal)
                    self.append (signals, signal)

            if name == "g_signal_emit_valist" or name == "g_signal_emit":
                self.read_object (frame, "instance", instances)
                id = self.read_var (frame, "signal_id")
                signal = get_signal_name (id)
                if signal:
                    signal = self.get_detailed_signal_from_frame(frame, signal)
                    self.append (signals, signal)

            if name == "g_signal_emit_by_name":
                self.read_object (frame, "instance", instances)
                self.read_var (frame, "detailed_signal", signals)
                break

        instance = self.or_join_array (instances)
        signal = self.or_join_array (signals)

        return "<emit signal %s on instance %s>" %  (signal, instance)

    def elided (self):
        return self.frames[0:-1]

    def describe (self, stream, full):
        stream.write (" " + self.function () + "\n")

class GFrameDecorator:
    def __init__ (self, iter):
        self.queue = []
        self.iter = iter

    def __iter__ (self):
        return self

    def fill (self):
        while len(self.queue) <= 8:
            try:
                f = next(self.iter)
                self.queue.append (f)
            except StopIteration:
                return

    def find_signal_emission (self):
        for i in range (min (len(self.queue), 3)):
            if frame_name(self.queue[i]) == "signal_emit_unlocked_R":
                return i
        return -1

    def next (self):
        # Ensure we have enough frames for a full signal emission
        self.fill()

        # Are we at the end?
        if len(self.queue) == 0:
            raise StopIteration

        emission = self.find_signal_emission ()
        if emission > 0:
            start = emission
            while True:
                if start == 0:
                    break
                prev_name = frame_name(self.queue[start-1])
                if prev_name.find("_marshal_") >= 0 or prev_name == "g_closure_invoke":
                    start = start - 1
                else:
                    break
            end = emission + 1
            while end < len(self.queue):
                if frame_name(self.queue[end]) in ["g_signal_emitv",
                                                   "g_signal_emit_valist",
                                                   "g_signal_emit",
                                                   "g_signal_emit_by_name",
                                                   "_g_closure_invoke_va"]:
                    end = end + 1
                else:
                    break

            signal_frames = self.queue[start:end]
            new_frames = [SignalFrame(signal_frames)]
            self.queue[start:end] = new_frames

        return self.queue.pop(0)

    def __next__ (self):
        return self.next()

class GFrameFilter(object):
    name = 'glib'
    enabled = True
    priority = 100

    def filter(self, iterator):
        return GFrameDecorator(iterator)

def register (obj):
    if obj == None:
        obj = gdb

    if HAVE_GDB_FRAMEDECORATOR:
        filter = GFrameFilter()
        obj.frame_filters[filter.name] = filter
    obj.pretty_printers.append(pretty_printer_lookup)
<|endoftext|>


<commit_before>#!/usr/bin/env python

'''Generic event dispatcher framework.

All objects that produce events in pyglet implement `EventDispatcher`,
providing a consistent interface for registering and manipulating event
handlers.  A commonly used event dispatcher is `pyglet.window.Window`.

Event types
===========

For each event dispatcher there is a set of events that it dispatches; these
correspond with the type of event handlers you can attach.  Event types are
identified by their name, for example, ''on_resize''.  If you are creating a
new class which implements `EventDispatcher`, you must call
`EventDispatcher.register_event_type` for each event type.

Attaching event handlers
========================

An event handler is simply a function or method.  You can attach an event
handler by setting the appropriate function on the instance::

    def on_resize(width, height):
        # ...
    dispatcher.on_resize = on_resize

There is also a convenience decorator that reduces typing::

    @dispatcher.event
    def on_resize(width, height):
        # ...

You may prefer to subclass and override the event handlers instead::

    class MyDispatcher(DispatcherClass):
        def on_resize(self, width, height):
            # ...

Event handler stack
===================

When attaching an event handler to a dispatcher using the above methods, it
replaces any existing handler (causing the original handler to no longer be
called).  Each dispatcher maintains a stack of event handlers, allowing you to
insert an event handler "above" the existing one rather than replacing it.

There are two main use cases for "pushing" event handlers:

* Temporarily intercepting the events coming from the dispatcher by pushing a
  custom set of handlers onto the dispatcher, then later "popping" them all
  off at once.
* Creating "chains" of event handlers, where the event propogates from the
  top-most (most recently added) handler to the bottom, until a handler
  takes care of it.

Use `EventDispatcher.push_handlers` to create a new level in the stack and
attach handlers to it.  You can push several handlers at once::

    dispatcher.push_handlers(on_resize, on_key_press)

If your function handlers have different names to the events they handle, use
keyword arguments::

    dispatcher.push_handlers(on_resize=my_resize,
                             on_key_press=my_key_press)

After an event handler has processed an event, it is passed on to the
next-lowest event handler, unless the handler returns `EVENT_HANDLED`, which
prevents further propogation.

To remove all handlers on the top stack level, use
`EventDispatcher.pop_handlers`.

Note that any handlers pushed onto the stack have precedence over the
handlers set directly on the instance (for example, using the methods
described in the previous section), regardless of when they were set.
For example, handler `foo` is called before handler `bar` in the following
example::

    dispatcher.push_handlers(on_resize=foo)
    dispatcher.on_resize = bar

Dispatching events
==================

pyglet uses a single-threaded model for all application code.  Events are
never called except when explicitly calling `EventDispatcher.dispatch_events`.
It is up to the specific event dispatcher to queue relevant events until they
can be dispatched, at which point the handlers are called in the order the
events were originally generated.

This implies that your application runs with a main loop that continously
updates the application state and checks for new events::

    while True:
        dispatcher.dispatch_events()
        # ... additional per-frame processing

Not all event dispatchers require the call to `dispatch_events`; check with
the particular class's documentation.

'''

__docformat__ = 'restructuredtext'
__version__ = '$Id$'

import inspect

EVENT_HANDLED = True
EVENT_UNHANDLED = None

class EventException(Exception):
    pass

class EventDispatcher(object):
    '''Generic event dispatcher interface.

    See the module docstring for usage.
    '''
    # Placeholder empty stack; real stack is created only if needed
    _event_stack = ()

    @classmethod
    def register_event_type(cls, name):
        '''Register an event type with the dispatcher.

        Registering event types allows the dispatcher to validate event
        handler names as they are attached, and to search attached objects for
        suitable handlers.
        '''
        if not hasattr(cls, 'event_types'):
            cls.event_types = []
        cls.event_types.append(name)
        return name

    def push_handlers(self, *args, **kwargs):
        '''Push a level onto the top of the handler stack, then attach zero or
        more event handlers.

        If keyword arguments are given, they name the event type to attach.
        Otherwise, a callable's `__name__` attribute will be used.  Any other
        object may also be specified, in which case it will be searched for
        callables with event names.
        '''
        # Create event stack if necessary
        if type(self._event_stack) is tuple:
            self._event_stack = [{}]

        # Place dict full of new handlers at beginning of stack
        self._event_stack.insert(0, {})
        self.set_handlers(*args, **kwargs)

    def set_handlers(self, *args, **kwargs):
        '''Attach one or more event handlers to the top level of the handler
        stack.

        See `push_handlers` for the accepted argument types.
        '''
        # Create event stack if necessary
        if type(self._event_stack) is tuple:
            self._event_stack = [{}]

        for object in args:
            if inspect.isroutine(object):
                # Single magically named function
                name = object.__name__
                if name not in self.event_types:
                    raise EventException('Unknown event "%s"' % name)
                self.set_handler(name, object)
            else:
                # Single instance with magically named methods
                for name, handler in inspect.getmembers(object):
                    if name in self.event_types:
                        self.set_handler(name, handler)
        for name, handler in kwargs.items():
            # Function for handling given event (no magic)
            if name not in self.event_types:
                raise EventException('Unknown event "%s"' % name)
            self.set_handler(name, handler)

    def set_handler(self, name, handler):
        '''Attach a single event handler.

        :Parameters:
            `name` : str
                Name of the event type to attach to.
            `handler` : callable
                Event handler to attach.

        '''
        # Create event stack if necessary
        if type(self._event_stack) is tuple:
            self._event_stack = [{}]

        self._event_stack[0][name] = handler

    def pop_handlers(self):
        '''Pop the top level of event handlers off the stack.
        '''
        assert self._event_stack and 'No handlers pushed'

        del self._event_stack[0]

    def dispatch_event(self, event_type, *args):
        '''Dispatch a single event to the attached handlers, propogating
        from the top of the stack until one returns `EVENT_UNHANDLED`.
        This method should be used only by `EventDispatcher` implementors;
        applications should call `dispatch_events`.

        :Parameters:
            `event_type` : str
                Name of the event.
            `args` : sequence
                Arguments to pass to the event handler.

        '''
        # Search handler stack for matching event handlers
        for frame in self._event_stack:
            handler = frame.get(event_type, None)
            if handler:
                ret = handler(*args)
                if ret != EVENT_UNHANDLED:
                    return

        # Check instance for an event handler
        if hasattr(self, event_type):
            getattr(self, event_type)(*args)

    def dispatch_events(self):
        '''Call attached event handlers for all queued events.
        '''
        raise NotImplementedError(
            'This class does not implement dispatch_events')

    def event(self, *args):
        '''Function decorator for an event handler.

        Usage::

            win = window.Window()

            @win.event
            def on_resize(self, width, height):
                # ...

        or::

            @win.event('on_resize')
            def foo(self, width, height):
                # ...

        '''
        if len(args) == 0:                      # @window.event()
            def decorator(func):
                name = func.__name__
                setattr(self, name, func)
                return func
            return decorator
        elif inspect.isroutine(args[0]):        # @window.event
            name = func.__name__
            setattr(self, name, func)
            return args[0]
        elif type(args[0]) in (str, unicode):   # @window.event('on_resize')
            name = args[0]
            def decorator(func):
                setattr(self, name, func)
                return func
            return decorator
<commit_msg>Whoops, how did this slip through.<commit_after>#!/usr/bin/env python

'''Generic event dispatcher framework.

All objects that produce events in pyglet implement `EventDispatcher`,
providing a consistent interface for registering and manipulating event
handlers.  A commonly used event dispatcher is `pyglet.window.Window`.

Event types
===========

For each event dispatcher there is a set of events that it dispatches; these
correspond with the type of event handlers you can attach.  Event types are
identified by their name, for example, ''on_resize''.  If you are creating a
new class which implements `EventDispatcher`, you must call
`EventDispatcher.register_event_type` for each event type.

Attaching event handlers
========================

An event handler is simply a function or method.  You can attach an event
handler by setting the appropriate function on the instance::

    def on_resize(width, height):
        # ...
    dispatcher.on_resize = on_resize

There is also a convenience decorator that reduces typing::

    @dispatcher.event
    def on_resize(width, height):
        # ...

You may prefer to subclass and override the event handlers instead::

    class MyDispatcher(DispatcherClass):
        def on_resize(self, width, height):
            # ...

Event handler stack
===================

When attaching an event handler to a dispatcher using the above methods, it
replaces any existing handler (causing the original handler to no longer be
called).  Each dispatcher maintains a stack of event handlers, allowing you to
insert an event handler "above" the existing one rather than replacing it.

There are two main use cases for "pushing" event handlers:

* Temporarily intercepting the events coming from the dispatcher by pushing a
  custom set of handlers onto the dispatcher, then later "popping" them all
  off at once.
* Creating "chains" of event handlers, where the event propogates from the
  top-most (most recently added) handler to the bottom, until a handler
  takes care of it.

Use `EventDispatcher.push_handlers` to create a new level in the stack and
attach handlers to it.  You can push several handlers at once::

    dispatcher.push_handlers(on_resize, on_key_press)

If your function handlers have different names to the events they handle, use
keyword arguments::

    dispatcher.push_handlers(on_resize=my_resize,
                             on_key_press=my_key_press)

After an event handler has processed an event, it is passed on to the
next-lowest event handler, unless the handler returns `EVENT_HANDLED`, which
prevents further propogation.

To remove all handlers on the top stack level, use
`EventDispatcher.pop_handlers`.

Note that any handlers pushed onto the stack have precedence over the
handlers set directly on the instance (for example, using the methods
described in the previous section), regardless of when they were set.
For example, handler `foo` is called before handler `bar` in the following
example::

    dispatcher.push_handlers(on_resize=foo)
    dispatcher.on_resize = bar

Dispatching events
==================

pyglet uses a single-threaded model for all application code.  Events are
never called except when explicitly calling `EventDispatcher.dispatch_events`.
It is up to the specific event dispatcher to queue relevant events until they
can be dispatched, at which point the handlers are called in the order the
events were originally generated.

This implies that your application runs with a main loop that continously
updates the application state and checks for new events::

    while True:
        dispatcher.dispatch_events()
        # ... additional per-frame processing

Not all event dispatchers require the call to `dispatch_events`; check with
the particular class's documentation.

'''

__docformat__ = 'restructuredtext'
__version__ = '$Id$'

import inspect

EVENT_HANDLED = True
EVENT_UNHANDLED = None

class EventException(Exception):
    pass

class EventDispatcher(object):
    '''Generic event dispatcher interface.

    See the module docstring for usage.
    '''
    # Placeholder empty stack; real stack is created only if needed
    _event_stack = ()

    @classmethod
    def register_event_type(cls, name):
        '''Register an event type with the dispatcher.

        Registering event types allows the dispatcher to validate event
        handler names as they are attached, and to search attached objects for
        suitable handlers.
        '''
        if not hasattr(cls, 'event_types'):
            cls.event_types = []
        cls.event_types.append(name)
        return name

    def push_handlers(self, *args, **kwargs):
        '''Push a level onto the top of the handler stack, then attach zero or
        more event handlers.

        If keyword arguments are given, they name the event type to attach.
        Otherwise, a callable's `__name__` attribute will be used.  Any other
        object may also be specified, in which case it will be searched for
        callables with event names.
        '''
        # Create event stack if necessary
        if type(self._event_stack) is tuple:
            self._event_stack = [{}]

        # Place dict full of new handlers at beginning of stack
        self._event_stack.insert(0, {})
        self.set_handlers(*args, **kwargs)

    def set_handlers(self, *args, **kwargs):
        '''Attach one or more event handlers to the top level of the handler
        stack.

        See `push_handlers` for the accepted argument types.
        '''
        # Create event stack if necessary
        if type(self._event_stack) is tuple:
            self._event_stack = [{}]

        for object in args:
            if inspect.isroutine(object):
                # Single magically named function
                name = object.__name__
                if name not in self.event_types:
                    raise EventException('Unknown event "%s"' % name)
                self.set_handler(name, object)
            else:
                # Single instance with magically named methods
                for name, handler in inspect.getmembers(object):
                    if name in self.event_types:
                        self.set_handler(name, handler)
        for name, handler in kwargs.items():
            # Function for handling given event (no magic)
            if name not in self.event_types:
                raise EventException('Unknown event "%s"' % name)
            self.set_handler(name, handler)

    def set_handler(self, name, handler):
        '''Attach a single event handler.

        :Parameters:
            `name` : str
                Name of the event type to attach to.
            `handler` : callable
                Event handler to attach.

        '''
        # Create event stack if necessary
        if type(self._event_stack) is tuple:
            self._event_stack = [{}]

        self._event_stack[0][name] = handler

    def pop_handlers(self):
        '''Pop the top level of event handlers off the stack.
        '''
        assert self._event_stack and 'No handlers pushed'

        del self._event_stack[0]

    def dispatch_event(self, event_type, *args):
        '''Dispatch a single event to the attached handlers, propogating
        from the top of the stack until one returns `EVENT_UNHANDLED`.
        This method should be used only by `EventDispatcher` implementors;
        applications should call `dispatch_events`.

        :Parameters:
            `event_type` : str
                Name of the event.
            `args` : sequence
                Arguments to pass to the event handler.

        '''
        # Search handler stack for matching event handlers
        for frame in self._event_stack:
            handler = frame.get(event_type, None)
            if handler:
                ret = handler(*args)
                if ret != EVENT_UNHANDLED:
                    return

        # Check instance for an event handler
        if hasattr(self, event_type):
            getattr(self, event_type)(*args)

    def dispatch_events(self):
        '''Call attached event handlers for all queued events.
        '''
        raise NotImplementedError(
            'This class does not implement dispatch_events')

    def event(self, *args):
        '''Function decorator for an event handler.

        Usage::

            win = window.Window()

            @win.event
            def on_resize(self, width, height):
                # ...

        or::

            @win.event('on_resize')
            def foo(self, width, height):
                # ...

        '''
        if len(args) == 0:                      # @window.event()
            def decorator(func):
                name = func.__name__
                setattr(self, name, func)
                return func
            return decorator
        elif inspect.isroutine(args[0]):        # @window.event
            func = args[0]
            name = func.__name__
            setattr(self, name, func)
            return args[0]
        elif type(args[0]) in (str, unicode):   # @window.event('on_resize')
            name = args[0]
            def decorator(func):
                setattr(self, name, func)
                return func
            return decorator
<|endoftext|>


<commit_before>import numpy as np

import param

from ..core import util
from ..core import OrderedDict, Dimension, UniformNdMapping, Element, Columns, Element2D, NdElement, HoloMap
from .tabular import ItemTable, Table
from .util import compute_edges

class Chart(Columns, Element2D):
    """
    The data held within Chart is a numpy array of shape (N, D),
    where N is the number of samples and D the number of dimensions.
    Chart Elements are sliceable along up to two key dimensions.
    The data may be supplied in one of three formats:

    1) As a numpy array of shape (N, D).
    2) As a list of length N containing tuples of length D.
    3) As a tuple of length D containing iterables of length N.
    """

    kdims = param.List(default=[Dimension('x')], bounds=(1,2), doc="""
        The key dimensions of the Chart, determining the number of
        indexable dimensions.""")

    group = param.String(default='Chart', constant=True)

    vdims = param.List(default=[Dimension('y')], bounds=(1,None), doc="""
        The value dimensions of the Chart, usually corresponding to a
        number of dependent variables.""")

    _null_value = np.array([[], []]).T # For when data is None

class Scatter(Chart):
    """
    Scatter is a Element2D type which gets displayed as a number of
    disconnected points.
    """

    group = param.String(default='Scatter', constant=True)

class Curve(Chart):
    """
    Curve is a simple Chart Element providing 1D indexing along
    the x-axis.
    """

    group = param.String(default='Curve', constant=True)

class ErrorBars(Chart):
    """
    ErrorBars is a Chart Element type representing any number of
    errorbars situated in a 2D space. The errors must be supplied
    as an Nx3 or Nx4 array representing the x/y-positions and
    either the symmetric error or assymetric errors respectively.
    Internally the data is always held as an Nx4 array with the
    lower and upper bounds.
    """

    group = param.String(default='ErrorBars', constant=True, doc="""
        A string describing the quantitity measured by the ErrorBars
        object.""")

    kdims = param.List(default=[Dimension('x'), Dimension('y')],
                       bounds=(2, 2), constant=True, doc="""
        The Dimensions corresponding to the x- and y-positions of
        the error bars.""")

    vdims = param.List(default=[Dimension('lerror'), Dimension('uerror')],
                       bounds=(2,2), constant=True)

    def __init__(self, data, **params):
        super(ErrorBars, self).__init__(data, **params)
        if self.shape[1] == 3:
            self.data = self.interface.add_dimension(self, self.vdims[1].name,
                                                     3, self.dimension_values(2))

    def range(self, dim, data_range=True):
        drange = super(ErrorBars, self).range(dim, data_range)
        didx = self.get_dimension_index(dim)
        if didx == 1 and data_range:
            mean = self.dimension_values(1)
            neg_error = self.dimension_values(2)
            pos_error = self.dimension_values(3)
            lower = np.nanmin(mean-neg_error)
            upper = np.nanmax(mean+pos_error)
            return util.max_range([(lower, upper), drange])
        else:
            return drange

class Spread(ErrorBars):
    """
    Spread is a Chart Element type respresenting a spread of
    values as given by a mean and standard error or confidence
    intervals. Just like the ErrorBars Element type, mean and
    deviations from the mean should be supplied as either an
    Nx3 or Nx4 array representing the x-values, mean values
    and symmetric or assymetric errors respective. Internally
    the data is always expanded to an Nx4 array.
    """

    group = param.String(default='Spread', constant=True)

class Bars(Columns):
    """
    Bars is an Element type, representing a number of stacked and
    grouped bars, depending the dimensionality of the key and value
    dimensions. Bars is useful for categorical data, which may be
    laid via groups, categories and stacks. Internally Bars is
    a NdElement with up to three key dimensions and a single value
    dimension.
    """

    group = param.String(default='Bars', constant=True)

    kdims = param.List(default=[Dimension('x')], bounds=(1,3))

    vdims = param.List(default=[Dimension('y')], bounds=(1,1))

class Histogram(Element2D):
    """
    Histogram contains a number of bins, which are defined by the
    upper and lower bounds of their edges and the computed bin values.
    """

    kdims = param.List(default=[Dimension('x')], bounds=(1,1), doc="""
        Dimensions on Element2Ds determine the number of indexable
        dimensions.""")

    group = param.String(default='Histogram', constant=True)

    vdims = param.List(default=[Dimension('Frequency')])

    def __init__(self, values, edges=None, extents=None, **params):
        self.values, self.edges, settings = self._process_data(values, edges)
        settings.update(params)
        super(Histogram, self).__init__((self.values, self.edges), **settings)
        self._width = None
        self._extents = (None, None, None, None) if extents is None else extents

    def __getitem__(self, key):
        """
        Implements slicing or indexing of the Histogram
        """
        if key in self.dimensions(): return self.dimension_values(key)
        if key is (): return self # May no longer be necessary
        if isinstance(key, tuple) and len(key) > self.ndims:
            raise Exception("Slice must match number of key dimensions.")

        centers = [(float(l)+r)/2 for (l,r) in zip(self.edges, self.edges[1:])]
        if isinstance(key, slice):
            start, stop = key.start, key.stop
            if [start, stop] == [None,None]: return self
            start_idx, stop_idx = None,None
            if start is not None:
                start_idx = np.digitize([start], centers, right=True)[0]
            if stop is not None:
                stop_idx = np.digitize([stop], centers, right=True)[0]

            slice_end = stop_idx+1 if stop_idx is not None else None
            slice_values = self.values[start_idx:stop_idx]
            slice_edges =  self.edges[start_idx: slice_end]

            extents = (min(slice_edges), self.extents[1],
                       max(slice_edges), self.extents[3])
            return self.clone((slice_values, slice_edges), extents=extents)
        else:
            if not (self.edges.min() <= key < self.edges.max()):
                raise Exception("Key value %s is out of the histogram bounds" % key)
            idx = np.digitize([key], self.edges)[0]
            return self.values[idx-1 if idx>0 else idx]

    def _process_data(self, values, edges):
        """
        Ensure that edges are specified as left and right edges of the
        histogram bins rather than bin centers.
        """
        settings = {}
        (values, edges) = values if isinstance(values, tuple) else (values, edges)
        if isinstance(values, Element2D):
            settings = dict(values.get_param_values(onlychanged=True))
            edges = values.data[:, 0].copy()
            values = values.data[:, 1].copy()
        elif isinstance(values, np.ndarray) and len(values.shape) == 2:
            edges = values[:, 0]
            values = values[:, 1]
        elif all(isinstance(el, tuple) for el in values):
            edges, values = zip(*values)
        else:
            values = np.array(values)
            if edges is None:
                edges = np.arange(len(values), dtype=np.float)
            else:
                edges = np.array(edges, dtype=np.float)

        if len(edges) == len(values):
            edges = compute_edges(edges)
        return values, edges, settings

    @property
    def extents(self):
        if any(lim is not None for lim in self._extents):
            return self._extents
        else:
            return (np.min(self.edges), None, np.max(self.edges), None)

    @extents.setter
    def extents(self, extents):
        self._extents = extents

    def dimension_values(self, dim):
        dim = self.get_dimension(dim).name
        if dim in self.vdims:
            return self.values
        elif dim in self.kdims:
            return np.convolve(self.edges, np.ones((2,))/2, mode='valid')
        else:
            return super(Histogram, self).dimension_values(dim)

    def sample(self, samples=[], **sample_values):
        raise NotImplementedError('Cannot sample a Histogram.')

    def reduce(self, dimensions=None, function=None, **reduce_map):
        raise NotImplementedError('Reduction of Histogram not implemented.')

class Points(Chart):
    """
    Allows sets of points to be positioned over a sheet coordinate
    system. Each points may optionally be associated with a chosen
    numeric value.

    The input data can be a Nx2 or Nx3 Numpy array where the first two
    columns corresponds to the X,Y coordinates in sheet coordinates,
    within the declared bounding region. For Nx3 arrays, the third
    column corresponds to the magnitude values of the points. Any
    additional columns will be ignored (use VectorFields instead).

    The input data may be also be passed as a tuple of elements that
    may be numpy arrays or values that can be cast to arrays. When
    such a tuple is supplied, the elements are joined column-wise into
    a single array, allowing the magnitudes to be easily supplied
    separately.

    Note that if magnitudes are to be rendered correctly by default,
    they should lie in the range [0,1].
    """

    kdims = param.List(default=[Dimension('x'), Dimension('y')],
                       bounds=(2, 2), constant=True, doc="""
        The label of the x- and y-dimension of the Points in form
        of a string or dimension object.""")

    group = param.String(default='Points', constant=True)

    vdims = param.List(default=[])

    _min_dims = 2                      # Minimum number of columns

    def __iter__(self):
        i = 0
        while i < len(self):
            yield tuple(self.data[i, ...])
            i += 1

class VectorField(Points):
    """
    A VectorField contains is a collection of vectors where each
    vector has an associated position in sheet coordinates.

    The constructor of VectorField is similar to the constructor of
    Points: the input data can be an NxM Numpy array where the first
    two columns corresponds to the X,Y coordinates in sheet
    coordinates, within the declared bounding region. As with Points,
    the input can be a tuple of array objects or of objects that can
    be cast to arrays (the tuple elements are joined column-wise).

    The third column maps to the vector angle which must be specified
    in radians. Note that it is possible to supply a collection which
    isn't a numpy array, whereby each element of the collection is
    assumed to be an iterable corresponding to a single column of the
    NxM array.

    The visualization of any additional columns is decided by the
    plotting code. For instance, the fourth and fifth columns could
    correspond to arrow length and colour map value. All that is
    assumed is that these additional dimension are normalized between
    0.0 and 1.0 for the default visualization to work well.

    The only restriction is that the final data array is NxM where
    M>3. In other words, the vector must have a dimensionality of 2 or
    higher.
    """

    group = param.String(default='VectorField', constant=True)

    vdims = param.List(default=[Dimension('Angle', cyclic=True, range=(0,2*np.pi)),
                                Dimension('Magnitude')], bounds=(1, 2))

    _null_value = np.array([[], [], [], []]).T # For when data is None
    _min_dims = 3                              # Minimum number of columns

    def __init__(self, data, **params):
        if isinstance(data, list) and all(isinstance(d, np.ndarray) for d in data):
            data = np.column_stack([d.flat if d.ndim > 1 else d for d in data])
        super(VectorField, self).__init__(data, **params)
<commit_msg>Reimplemented extents setting on Chart types<commit_after>import numpy as np

import param

from ..core import util
from ..core import OrderedDict, Dimension, UniformNdMapping, Element, Columns, Element2D, NdElement, HoloMap
from .tabular import ItemTable, Table
from .util import compute_edges

class Chart(Columns, Element2D):
    """
    The data held within Chart is a numpy array of shape (N, D),
    where N is the number of samples and D the number of dimensions.
    Chart Elements are sliceable along up to two key dimensions.
    The data may be supplied in one of three formats:

    1) As a numpy array of shape (N, D).
    2) As a list of length N containing tuples of length D.
    3) As a tuple of length D containing iterables of length N.
    """

    kdims = param.List(default=[Dimension('x')], bounds=(1,2), doc="""
        The key dimensions of the Chart, determining the number of
        indexable dimensions.""")

    group = param.String(default='Chart', constant=True)

    vdims = param.List(default=[Dimension('y')], bounds=(1,None), doc="""
        The value dimensions of the Chart, usually corresponding to a
        number of dependent variables.""")

    def __getitem__(self, index):
        sliced = super(Chart, self).__getitem__(index)
        if not isinstance(index, tuple): index = (index,)
        ndims = len(self.extents)/2
        lower_bounds, upper_bounds = [None]*ndims, [None]*ndims
        for i, slc in enumerate(index[:ndims]):
            if isinstance(slc, slice):
                lower_bounds[i] = lbound if slc.start is None else slc.start
                upper_bounds[i] = ubound if slc.stop is None else slc.stop
        sliced.extents = tuple(lower_bounds+upper_bounds)
        return sliced

class Scatter(Chart):
    """
    Scatter is a Element2D type which gets displayed as a number of
    disconnected points.
    """

    group = param.String(default='Scatter', constant=True)

class Curve(Chart):
    """
    Curve is a simple Chart Element providing 1D indexing along
    the x-axis.
    """

    group = param.String(default='Curve', constant=True)

class ErrorBars(Chart):
    """
    ErrorBars is a Chart Element type representing any number of
    errorbars situated in a 2D space. The errors must be supplied
    as an Nx3 or Nx4 array representing the x/y-positions and
    either the symmetric error or assymetric errors respectively.
    Internally the data is always held as an Nx4 array with the
    lower and upper bounds.
    """

    group = param.String(default='ErrorBars', constant=True, doc="""
        A string describing the quantitity measured by the ErrorBars
        object.""")

    kdims = param.List(default=[Dimension('x'), Dimension('y')],
                       bounds=(2, 2), constant=True, doc="""
        The Dimensions corresponding to the x- and y-positions of
        the error bars.""")

    vdims = param.List(default=[Dimension('lerror'), Dimension('uerror')],
                       bounds=(2,2), constant=True)

    def __init__(self, data, **params):
        super(ErrorBars, self).__init__(data, **params)
        if self.shape[1] == 3:
            self.data = self.interface.add_dimension(self, self.vdims[1].name,
                                                     3, self.dimension_values(2))

    def range(self, dim, data_range=True):
        drange = super(ErrorBars, self).range(dim, data_range)
        didx = self.get_dimension_index(dim)
        if didx == 1 and data_range:
            mean = self.dimension_values(1)
            neg_error = self.dimension_values(2)
            pos_error = self.dimension_values(3)
            lower = np.nanmin(mean-neg_error)
            upper = np.nanmax(mean+pos_error)
            return util.max_range([(lower, upper), drange])
        else:
            return drange

class Spread(ErrorBars):
    """
    Spread is a Chart Element type respresenting a spread of
    values as given by a mean and standard error or confidence
    intervals. Just like the ErrorBars Element type, mean and
    deviations from the mean should be supplied as either an
    Nx3 or Nx4 array representing the x-values, mean values
    and symmetric or assymetric errors respective. Internally
    the data is always expanded to an Nx4 array.
    """

    group = param.String(default='Spread', constant=True)

class Bars(Columns):
    """
    Bars is an Element type, representing a number of stacked and
    grouped bars, depending the dimensionality of the key and value
    dimensions. Bars is useful for categorical data, which may be
    laid via groups, categories and stacks. Internally Bars is
    a NdElement with up to three key dimensions and a single value
    dimension.
    """

    group = param.String(default='Bars', constant=True)

    kdims = param.List(default=[Dimension('x')], bounds=(1,3))

    vdims = param.List(default=[Dimension('y')], bounds=(1,1))

class Histogram(Element2D):
    """
    Histogram contains a number of bins, which are defined by the
    upper and lower bounds of their edges and the computed bin values.
    """

    kdims = param.List(default=[Dimension('x')], bounds=(1,1), doc="""
        Dimensions on Element2Ds determine the number of indexable
        dimensions.""")

    group = param.String(default='Histogram', constant=True)

    vdims = param.List(default=[Dimension('Frequency')])

    def __init__(self, values, edges=None, extents=None, **params):
        self.values, self.edges, settings = self._process_data(values, edges)
        settings.update(params)
        super(Histogram, self).__init__((self.values, self.edges), **settings)
        self._width = None
        self._extents = (None, None, None, None) if extents is None else extents

    def __getitem__(self, key):
        """
        Implements slicing or indexing of the Histogram
        """
        if key in self.dimensions(): return self.dimension_values(key)
        if key is (): return self # May no longer be necessary
        if isinstance(key, tuple) and len(key) > self.ndims:
            raise Exception("Slice must match number of key dimensions.")

        centers = [(float(l)+r)/2 for (l,r) in zip(self.edges, self.edges[1:])]
        if isinstance(key, slice):
            start, stop = key.start, key.stop
            if [start, stop] == [None,None]: return self
            start_idx, stop_idx = None,None
            if start is not None:
                start_idx = np.digitize([start], centers, right=True)[0]
            if stop is not None:
                stop_idx = np.digitize([stop], centers, right=True)[0]

            slice_end = stop_idx+1 if stop_idx is not None else None
            slice_values = self.values[start_idx:stop_idx]
            slice_edges =  self.edges[start_idx: slice_end]

            extents = (min(slice_edges), self.extents[1],
                       max(slice_edges), self.extents[3])
            return self.clone((slice_values, slice_edges), extents=extents)
        else:
            if not (self.edges.min() <= key < self.edges.max()):
                raise Exception("Key value %s is out of the histogram bounds" % key)
            idx = np.digitize([key], self.edges)[0]
            return self.values[idx-1 if idx>0 else idx]

    def _process_data(self, values, edges):
        """
        Ensure that edges are specified as left and right edges of the
        histogram bins rather than bin centers.
        """
        settings = {}
        (values, edges) = values if isinstance(values, tuple) else (values, edges)
        if isinstance(values, Element2D):
            settings = dict(values.get_param_values(onlychanged=True))
            edges = values.data[:, 0].copy()
            values = values.data[:, 1].copy()
        elif isinstance(values, np.ndarray) and len(values.shape) == 2:
            edges = values[:, 0]
            values = values[:, 1]
        elif all(isinstance(el, tuple) for el in values):
            edges, values = zip(*values)
        else:
            values = np.array(values)
            if edges is None:
                edges = np.arange(len(values), dtype=np.float)
            else:
                edges = np.array(edges, dtype=np.float)

        if len(edges) == len(values):
            edges = compute_edges(edges)
        return values, edges, settings

    @property
    def extents(self):
        if any(lim is not None for lim in self._extents):
            return self._extents
        else:
            return (np.min(self.edges), None, np.max(self.edges), None)

    @extents.setter
    def extents(self, extents):
        self._extents = extents

    def dimension_values(self, dim):
        dim = self.get_dimension(dim).name
        if dim in self.vdims:
            return self.values
        elif dim in self.kdims:
            return np.convolve(self.edges, np.ones((2,))/2, mode='valid')
        else:
            return super(Histogram, self).dimension_values(dim)

    def sample(self, samples=[], **sample_values):
        raise NotImplementedError('Cannot sample a Histogram.')

    def reduce(self, dimensions=None, function=None, **reduce_map):
        raise NotImplementedError('Reduction of Histogram not implemented.')

class Points(Chart):
    """
    Allows sets of points to be positioned over a sheet coordinate
    system. Each points may optionally be associated with a chosen
    numeric value.

    The input data can be a Nx2 or Nx3 Numpy array where the first two
    columns corresponds to the X,Y coordinates in sheet coordinates,
    within the declared bounding region. For Nx3 arrays, the third
    column corresponds to the magnitude values of the points. Any
    additional columns will be ignored (use VectorFields instead).

    The input data may be also be passed as a tuple of elements that
    may be numpy arrays or values that can be cast to arrays. When
    such a tuple is supplied, the elements are joined column-wise into
    a single array, allowing the magnitudes to be easily supplied
    separately.

    Note that if magnitudes are to be rendered correctly by default,
    they should lie in the range [0,1].
    """

    kdims = param.List(default=[Dimension('x'), Dimension('y')],
                       bounds=(2, 2), constant=True, doc="""
        The label of the x- and y-dimension of the Points in form
        of a string or dimension object.""")

    group = param.String(default='Points', constant=True)

    vdims = param.List(default=[])

    _min_dims = 2                      # Minimum number of columns

    def __iter__(self):
        i = 0
        while i < len(self):
            yield tuple(self.data[i, ...])
            i += 1

class VectorField(Points):
    """
    A VectorField contains is a collection of vectors where each
    vector has an associated position in sheet coordinates.

    The constructor of VectorField is similar to the constructor of
    Points: the input data can be an NxM Numpy array where the first
    two columns corresponds to the X,Y coordinates in sheet
    coordinates, within the declared bounding region. As with Points,
    the input can be a tuple of array objects or of objects that can
    be cast to arrays (the tuple elements are joined column-wise).

    The third column maps to the vector angle which must be specified
    in radians. Note that it is possible to supply a collection which
    isn't a numpy array, whereby each element of the collection is
    assumed to be an iterable corresponding to a single column of the
    NxM array.

    The visualization of any additional columns is decided by the
    plotting code. For instance, the fourth and fifth columns could
    correspond to arrow length and colour map value. All that is
    assumed is that these additional dimension are normalized between
    0.0 and 1.0 for the default visualization to work well.

    The only restriction is that the final data array is NxM where
    M>3. In other words, the vector must have a dimensionality of 2 or
    higher.
    """

    group = param.String(default='VectorField', constant=True)

    vdims = param.List(default=[Dimension('Angle', cyclic=True, range=(0,2*np.pi)),
                                Dimension('Magnitude')], bounds=(1, 2))

    _null_value = np.array([[], [], [], []]).T # For when data is None
    _min_dims = 3                              # Minimum number of columns

    def __init__(self, data, **params):
        if isinstance(data, list) and all(isinstance(d, np.ndarray) for d in data):
            data = np.column_stack([d.flat if d.ndim > 1 else d for d in data])
        super(VectorField, self).__init__(data, **params)
<|endoftext|>