Source code for qumin.lattice.stylers

# !usr/bin/python3
# -*- coding: utf-8 -*-

import re

import matplotlib as mpl


[docs] class NodeStyler(): """ Instances of this class can be passed to drawing functions of the `qumin.clustering.node.Node` class. There are two levels of customization: - Use one of the default NodeStyler's: DefaultStyler. - Using this class as is, many options are available to change the default colormap, fontsize, etc. You simply have to instanciate an object and pass it to further scripts. - You can inherit methods from this class and rewrite some of them. Just be sure that you implement an interface for each property, as in the default LatticeStyler (markers, labels, colors). Then, you instanciate an object and pass it to further scripts. Attributes: colors (List): the colorscheme to use. edge_kws (dict): options that will be passed to draw edges. marker_kws (dict): options that will be passed to draw nodes. label_kws (dict): options that will be passed to draw labels. """ colors = ['#004f00', 'indigo'] markers = ['o', 's'] edge_kws = { "linewidth": 0.5 } marker_kws = {} label_kws = { 'family': 'DejaVu Sans', 'weight': 'normal', 'size': 9 }
[docs] def __init__(self, colors=None, min_size=0, max_size=0, min_markersize=20, max_markersize=50, square=False, markers=None, textoffset=0.08, edge_kws=None, marker_kws=None, label_kws=None, draw_nodes=True, layout_y_factor=1, layout_x_factor=15, topdown=True, horizontal=False, ): """ Arguments: draw_nodes (dict): Whether to draw the nodes or not. colors (list[str]): Color scheme to use. Two items. textoffset (int): Offset for text labels. min_size (int): Minimum size of a node. max_size (int): Maximum size of a node. min_markersize (int): Minimum size of a marker. max_markersize (int): Maximum size of a marker. marker (str): The marker shape. Defaults to 'o'. edge_kws (dict): Additional default keyword arguments for edges. marker_kws (dict): Additional default keyword arguments for markers. label_kws (dict): Additional default keyword arguments for labels. layout_y_factor (float): Scaling on the y-axis. layout_x_factor (float): Scaling on the x-axis. horizontal (bool): Orientation of the lattice (vertical or horizontal). topdown (bool): Direction of the lattice (top->down or right->left when `horizontal=True`). square (bool): Controls whether the lattice is drawn with straight lines or squared. Example:: square=True square=False │ ┌──┴──┐ │ ╱╲ horizontal=False │ │ ┌─┴─┐ │ ╱ ╲ topdown=True │ │ │ │ │ ╱ ╱╲ │ │ │ │ │ ╱ ╱ ╲ │__│___│___│ │╱___╱____╲ │─────┐ │⟍ │───┐ ├ │ ⟍ horizontal=True │ ├─┘ │⟍ ⟋ topdown=True │───┘ │⟋ │____________ │____________ """ self.min_size = min_size self.max_size = max_size self.min_markersize = min_markersize self.max_markersize = max_markersize self.square = square self.textoffset = textoffset self.draw_nodes = draw_nodes self.layout_y_factor = layout_y_factor self.layout_x_factor = layout_x_factor self.topdown = topdown self.horizontal = horizontal if colors: self.colors = colors if markers: self.markers = markers self.attribute_label_kws = {} if edge_kws is not None: self.edge_kws.update(edge_kws) if marker_kws is not None: self.marker_kws.update(marker_kws) if label_kws is not None: self.label_kws.update(label_kws)
[docs] def set_size_scale(self, root): """ Initializes the size norming function. This cannot be done when the instance is created (as we don't know yet the size scale to use), so this function is called by the lattice drawing method. Arguments: root (qumin.clustering.node.Node): Top level lattice node. """ size_list = [x.attributes.get('size', 0) for x in root] self.max_size = max(size_list) self.min_size = min(size_list)
[docs] def get_marker_size(self, node): """ Normalizes the size of the node for the visualization. Arguments: node (qumin.clustering.node.Node): A lattice Node to draw. Returns: float: the size of the node in the visualization. """ if self.min_size == self.max_size: return self.min_size s = self.min_markersize + ( ((node.attributes.get("size", 0) - self.min_size) / (self.max_size - self.min_size)) * (self.max_markersize - self.min_markersize)) return s
[docs] def get_marker_properties(self, node): """ Returns the marker properties for a given node. This method is able to handle up to two colors, using the marker edge color. The output of this function should be a dictionary of keyword arguments for :func:`matplotlib.pyplot.scatter`. Arguments: node (qumin.clustering.node.Node): A lattice Node to draw. Returns: dict: A list of keyword options for:func:`matplotlib.pyplot.scatter`. """ node_colors = self.get_node_color(node) options = self.marker_kws.copy() marker = mpl.markers.MarkerStyle(marker=self.get_marker_shape(node)) options.update({ "color": node_colors[0], "marker": marker, "zorder": 3, "edgecolors": node_colors[2] if len(node_colors) > 2 else None, 'linewidths': self.min_markersize / 7 if len(node_colors) > 2 else 0, }) options["s"] = self.get_marker_size(node) return options
[docs] def get_label_position(self, node): """ Returns keywords for the positioning of the labels in matplotlib. """ if self.horizontal: lva = "center" lha = "left" if self.topdown else "right" r = 0 else: lva = "top" if self.topdown else "bottom" lha = "center" r = 80 return dict(va=lva, ha=lha, rotation=r)
[docs] def get_label_properties(self, node): """ Returns the text label and properties for a given node. Arguments: node (qumin.clustering.node.Node): A Node to draw. Returns: List[Tuple[str, Dict]]: A list of tuples containing 2 items each: a label (str) and a dictionary of matplotlib options (position & style). """ return []
[docs] def get_node_color(self, node): """ Arguments: node (qumin.clustering.node.Node): A lattice Node to draw. Returns: iterable: The colors to use for a given node. """ if node.is_supremum() or node.introduces_objects(): return (self.colors[1],) else: return (self.colors[0],)
[docs] def get_edge_color(self, node, child): """ """ return self.get_node_color(node)
[docs] def get_edge_properties(self, node, child): """ Returns the edge properties for a pair of parent - child nodes. This method is able to handle up to two colors, using multicolor dashes. Arguments: node (qumin.clustering.node.Node): A lattice Node to draw. child (qumin.clustering.node.Node): The child connected by this edge. Returns: dict: A list of matplotlib options for the marker.. """ colors = self.get_edge_color(node, child) options = self.edge_kws.copy() options.update({ "color": colors[0], "gapcolor": colors[1] if len(colors) > 1 else None, "dashes": (4, 4) if len(colors) > 1 else [], }) return options
[docs] def get_marker_shape(self, node): """ Arguments: node (qumin.clustering.node.Node): A lattice Node to draw. Returns: str or matplotlib.pyplot.Marker: the marker to use for a given node. """ return self.markers[0] if self.draw_nodes else ""
[docs] class LatticeStyler(NodeStyler): """ Instances of this class can be passed to drawing functions of the `qumin.lattice.lattice.ICLattice` class. All properties from the :class:`qumin.lattice.stylers.NodeStyler` class are available. """
[docs] def __init__(self, *args, label_attributes=False, max_label_number=3, attribute_label_kws=None, show_size=True, **kwargs): """ Arguments: max_label_number (int): Maximum number of object names shown. label_attributes (bool): Whether to label attribute nodes too. show_size (bool): Whether to show the size of inflection classes in node labels. attribute_label_kws (dict): Additional default keyword arguments for attribute labels. kwargs (dict): Additional keyword arguments are passed to :func:`qumin.lattice.stylers.NodeStyler.__init__`. """ kwargs['square'] = False self.edge_kws = { "linewidth": 0.2 } super().__init__(*args, **kwargs) self.max_label_number = max_label_number self.label_attributes = label_attributes self.show_size = show_size if attribute_label_kws is not None: self.attribute_label_kws.update(attribute_label_kws)
[docs] def get_label_properties(self, node): """ Returns the text label and properties for a given lattice node. Arguments: node (qumin.clustering.node.Node): A lattice Node to draw. Returns: List[Tuple[str, Dict]]: A list of tuples containing 2 items each: a label (str) and a dictionary of matplotlib options (position & style). """ labels = [] positioning_options = self.get_label_position(node) # Object labels if node.introduces_objects(): options = self.label_kws.copy() options.update(positioning_options) if self.show_size: n = f" ({node.attributes.get('size', 1)})" else: n = " " max_number = min(len(node.attributes['objects']), self.max_label_number) labels.append(( ", ".join([o.split('_')[0] for o in node.attributes['objects']][0:max_number]) + n, options )) # Attribute labels if self.label_attributes and node.introduces_attributes(): options = self.label_kws.copy() options.update(positioning_options) matchs = [re.match(r'(.*)=<(.*)>', x) for x in node.attributes['common']] options.update({'va': 'bottom', 'ha': 'left', 'rotation': 0}) if self.attributes_kws is not None: options.update(self.attributes_kws) labels.append(( ' ' + '\n'.join( [f'{m[1]}={m[2] if m[2] else "Ø"} ' for m in matchs if m]), options )) return labels
[docs] def get_marker_properties(self, node): """ Returns the marker properties for a given lattice node. This method is able to handle up to three colors, using matplotlib's utility for two color markers + the marker edge color. The output of this function should be a dictionary of keyword arguments for :func:`matplotlib.pyplot.scatter`. Arguments: node (qumin.clustering.node.Node): A lattice Node to draw. Returns: dict: A list of keyword options for:func:`matplotlib.pyplot.scatter`. """ node_colors = self.get_node_color(node) options = { } options = self.marker_kws.copy() marker = mpl.markers.MarkerStyle(marker=self.get_marker_shape(node)) options.update({ "color": node_colors[0], "marker": marker, "zorder": 3, "edgecolors": node_colors[2] if len(node_colors) > 2 else None, 'linewidths': self.min_markersize / 7 if len(node_colors) > 2 else 0, }) options["s"] = self.get_marker_size(node) # If the node does not introduce any new concept, we shade it. if ( not node.attributes.get('common', False) and not node.attributes.get('objects', False)): options['alpha'] = 0.2 # options['s'] = self.min_markersize return options
[docs] def get_html_tooltip(self, node, patterns_map, comp=None): """ Render node tooltips on HTML visualizations (with mpld3). Arguments: patterns_map (dict[str, List]): a dictionary that maps a pattern to the list of patterns that have the same distribution. node (qumin.clustering.node.Node): the node whose label we render. Returns: str: HTML tooltip that will render for each node on hover. """ objs = node.attributes.get("objects", node.labels) if not objs: objs = node.labels count = node.attributes.get("size", "unknown nb of") size = f'{count} {"lexemes" if not isinstance(count, int) or count > 1 else "lexeme"}' header = f"<table><thead><th colspan=2> Ex: {objs[0]}, {size} </th></thead>" if node.introduces_attributes() & len(node.attributes['common']) > 0: line = "<tr><th>{}</th><td>{}</td></tr>" line2 = "<tr class='alternate'><th>{}</th><td>{}</td></tr>" line_no_head = "<tr><td colspan=2>{}</td></tr>" common = "" for pattern in patterns_map[node.attributes["common"][0]]: if "=" in pattern: attr, val = pattern.split("=") val = val.strip("<>") alt = val.split("/")[0].strip() if alt == "⇌": val = "syncretic" if comp and attr.startswith(comp): common += line2.format(attr[len(comp):], val) else: common += line.format(attr, val) else: common += line_no_head.format(pattern) if not common: common = "<tr><td colspan=2>Empty</td></tr>" return header + common + "</table>" return header + "</table>"
[docs] def get_marker_shape(self, node): """ Arguments: node (qumin.clustering.node.Node): A lattice Node to draw. Returns: str or matplotlib.pyplot.Marker: the marker to use for a given node. """ if not self.draw_nodes: return "" elif node.is_supremum() or node.introduces_objects(): return self.markers[1] return self.markers[0]
[docs] def get_edge_color(self, node, child): """ Arguments: node (qumin.clustering.node.Node): Starting point. child (qumin.clustering.node.Node): Endpoint. Returns: tuple[str]: the colors to use. """ return (self.colors[0],)
[docs] class TreeStyler(NodeStyler): """ Instances of this class can be passed to drawing functions for macroclass trees. All properties from the :class:`qumin.lattice.stylers.NodeStyler` class are available. """
[docs] def __init__(self, *args, onlymacroclasses=False, min_markersize=15, max_markersize=50, **kwargs): """ Arguments: """ self.colors = ['#d94d4d', '#aaaaaa'] super().__init__(*args, **kwargs) self.onlymacroclasses = onlymacroclasses
[docs] def get_label_properties(self, node): """ Returns the text label and properties for a given node. Arguments: node (qumin.clustering.node.Node): A Node to draw. Returns: List[Tuple[str, Dict]]: A list of tuples containing 2 items each: a label (str) and a dictionary of matplotlib options (position & style). """ should_annotate = (not self.onlymacroclasses) or node.attributes.get("macroclass_root", False) label = "" if should_annotate: if not node.children: # leaf label = node.labels[0] + " (" + str(node.attributes["size"]) + ")" else: # node label = "{0:.3f}".format(node.attributes["DL"]) if should_annotate else "" options = self.label_kws.copy() options.update(self.get_label_position(node)) return [(label, options)]
[docs] def get_node_color(self, node): c = node.attributes.get('color', self.colors[0]) return [c]
# This default stylers are used by Qumin's algorithms to draw lattices. DefaultNodeStyler = NodeStyler() DefaultLatticeStyler = LatticeStyler() DefaultTreeStyler = TreeStyler()