Source code for modelly.viz.render.drawers.network

"""Drawer for network spaces."""

import altair as alt
import matplotlib.pyplot as plt
import networkx as nx
import pandas as pd

from modelly.viz.render.drawers.base import (
    BaseSpaceDrawer,
    Network,
    chart_properties,
)


[docs] class NetworkSpaceDrawer(BaseSpaceDrawer): """Drawer for network-based spaces.""" def __init__( self, space: Network, layout_alg=nx.spring_layout, layout_kwargs=None, ): """Initialize the network space drawer. Args: space: The network space to draw layout_alg: NetworkX layout algorithm to use layout_kwargs: Keyword arguments for the layout algorithm """ super().__init__(space) self.layout_alg = layout_alg self.layout_kwargs = layout_kwargs if layout_kwargs is not None else {"seed": 0} # gather locations for nodes in network self.graph = self.space.G self.pos = {} for node_id, cell in self.space._cells.items(): if getattr(cell, "_position", None) is not None: self.pos[node_id] = cell.position x, y = list(zip(*self.pos.values())) if self.pos else ([0], [0]) self._set_padded_limits((min(x), max(x)), (min(y), max(y)))
[docs] def draw_matplotlib(self, ax=None, **draw_space_kwargs): """Draw the network using matplotlib. Args: ax: Matplotlib axes object to draw on. **draw_space_kwargs: Dictionaries of keyword arguments for styling. Can also handle zorder for both nodes and edges if passed. * ``node_kwargs``: A dict passed to nx.draw_networkx_nodes. * ``edge_kwargs``: A dict passed to nx.draw_networkx_edges. Returns: The modified axes object. """ if ax is None: _, ax = plt.subplots() ax.set_axis_off() ax.set_xlim(self.viz_xmin, self.viz_xmax) ax.set_ylim(self.viz_ymin, self.viz_ymax) node_kwargs = {"alpha": 0.5} edge_kwargs = {"alpha": 0.5, "style": "--"} node_kwargs.update(draw_space_kwargs.get("node_kwargs", {})) edge_kwargs.update(draw_space_kwargs.get("edge_kwargs", {})) node_zorder = node_kwargs.pop("zorder", 1) edge_zorder = edge_kwargs.pop("zorder", 0) nodes = nx.draw_networkx_nodes(self.graph, self.pos, ax=ax, **node_kwargs) edges = nx.draw_networkx_edges(self.graph, self.pos, ax=ax, **edge_kwargs) if nodes: nodes.set_zorder(node_zorder) _set_edge_zorder(edges, edge_zorder) return ax
[docs] def draw_altair(self, chart_width=450, chart_height=350, **draw_chart_kwargs): """Draw the network using Altair. Args: chart_width: Width for the shown chart chart_height: Height for the shown chart **draw_chart_kwargs: Dictionaries for styling the chart. * ``node_kwargs``: A dict of properties for the node's mark_point. * ``edge_kwargs``: A dict of properties for the edge's mark_rule. * Other kwargs (e.g., title, width) are passed to chart.properties(). Returns: Altair chart object representing the network. """ nodes_df, edge_positions = self._node_and_edge_frames() node_mark_kwargs = {"filled": True, "opacity": 0.5, "size": 500} edge_mark_kwargs = {"opacity": 0.5, "strokeDash": [5, 3]} node_mark_kwargs.update(draw_chart_kwargs.pop("node_kwargs", {})) edge_mark_kwargs.update(draw_chart_kwargs.pop("edge_kwargs", {})) chart_props = chart_properties(chart_width, chart_height, draw_chart_kwargs) edge_plot = self._edge_chart(edge_positions, edge_mark_kwargs) node_plot = ( alt.Chart(nodes_df) .mark_point(**node_mark_kwargs) .encode(x="x", y="y", tooltip=["node"]) ) chart = edge_plot + node_plot if chart_props: chart = chart.properties(**chart_props) return chart
def _node_and_edge_frames(self): """Return the node positions and the edges joined to both end positions.""" nodes_df = pd.DataFrame(self.pos).T.reset_index() nodes_df.columns = ["node", "x", "y"] edges_df = pd.DataFrame(self.graph.edges(), columns=["source", "target"]) edge_positions = edges_df.merge( nodes_df, how="left", left_on="source", right_on="node" ).merge( nodes_df, how="left", left_on="target", right_on="node", suffixes=("_source", "_target"), ) return nodes_df, edge_positions def _edge_chart(self, edge_positions, edge_mark_kwargs): """Return the Altair rule chart drawing every edge.""" return ( alt.Chart(edge_positions) .mark_rule(**edge_mark_kwargs) .encode( x=alt.X( "x_source", scale=alt.Scale(domain=[self.viz_xmin, self.viz_xmax]), axis=None, ), y=alt.Y( "y_source", scale=alt.Scale(domain=[self.viz_ymin, self.viz_ymax]), axis=None, ), x2="x_target", y2="y_target", ) )
def _set_edge_zorder(edges, edge_zorder): """Set the zorder of the drawn edges, a collection or a list of them.""" # In some matplotlib versions, edges can be a list of collections if isinstance(edges, list): for edge_collection in edges: edge_collection.set_zorder(edge_zorder) elif edges: edges.set_zorder(edge_zorder)