Skip to content

plots

lab.connectome.plots

Figures drawn from tables.

Each function returns a figure and writes no file; plot_neurons and network_figure return HTML, the others a figure object. Colors come from lab.connectome.look.

jaccard_heatmaps(panels, types)

Draw four titled Jaccard matrices, shaded from 0 to 1.

Parameters:

Name Type Description Default
panels dict of str to numpy.ndarray

Four square matrices by title, as overlap.jaccard_matrix returns them, in reading order.

required
types sequence of str

The names of the rows and columns of every matrix.

required

Returns:

Type Description
Figure

A 2 x 2 grid with the value written in each cell.

See Also

lab.connectome.overlap.jaccard_matrix : Builds each panel.

Examples:

>>> import numpy as np
>>> rng = np.random.default_rng(0)
>>> def panel():
...     m = rng.uniform(0, 1, (4, 4))
...     m = (m + m.T) / 2
...     np.fill_diagonal(m, 1)
...     return m
>>> panels = {
...     f"{side}, by {level}": panel()
...     for level in ("type", "cell")
...     for side in ("output", "input")
... }
>>> figure = jaccard_heatmaps(panels, ["T1", "T2", "T3", "T4"])

jaccard_heatmaps

Source code in lab/connectome/plots.py
def jaccard_heatmaps(
    panels: dict[str, np.ndarray], types: Sequence[str]
) -> Figure:
    """Draw four titled Jaccard matrices, shaded from 0 to 1.

    Parameters
    ----------
    panels : dict of str to numpy.ndarray
        Four square matrices by title, as
        `overlap.jaccard_matrix` returns them, in reading order.
    types : sequence of str
        The names of the rows and columns of every matrix.

    Returns
    -------
    matplotlib.figure.Figure
        A 2 x 2 grid with the value written in each cell.

    See Also
    --------
    lab.connectome.overlap.jaccard_matrix : Builds each panel.

    Examples
    --------
    >>> import numpy as np
    >>> rng = np.random.default_rng(0)
    >>> def panel():
    ...     m = rng.uniform(0, 1, (4, 4))
    ...     m = (m + m.T) / 2
    ...     np.fill_diagonal(m, 1)
    ...     return m
    >>> panels = {
    ...     f"{side}, by {level}": panel()
    ...     for level in ("type", "cell")
    ...     for side in ("output", "input")
    ... }
    >>> figure = jaccard_heatmaps(panels, ["T1", "T2", "T3", "T4"])
    """
    fig = Figure(figsize=(9, 8))
    axes = fig.subplots(2, 2)
    for ax, (title, matrix) in zip(axes.ravel(), panels.items(), strict=True):
        image = ax.imshow(matrix, vmin=0, vmax=1, cmap="Greys")
        ax.set_xticks(range(len(types)), types, rotation=45, ha="right")
        ax.set_yticks(range(len(types)), types)
        ax.set_title(title, fontsize=11)
        for i in range(len(types)):
            for j in range(len(types)):
                # Dark text on light cells and white on dark ones;
                # 0.55 is where the shading crosses mid-gray.
                ink = "white" if matrix[i, j] >= 0.55 else "black"
                ax.text(
                    j,
                    i,
                    f"{matrix[i, j]:.2f}",
                    ha="center",
                    va="center",
                    color=ink,
                    fontsize=9,
                )
        fig.colorbar(image, ax=ax, fraction=0.046)
    return fig

network_figure(edges, types)

Draw a network of types and their partner types.

Parameters:

Name Type Description Default
edges DataFrame

Columns src, dst, synapses, and kind, which is "out", "in", or "lateral", as between_types.network_edges returns them.

required
types sequence of str

The types to draw large, each in its color; every other node is a partner type, in gray.

required

Returns:

Type Description
str

A page of HTML that loads vis-network, so it needs a network connection. An arrow runs from each source to its target, as wide as its synapse count in proportion to the heaviest edge, and in the color of its kind. A partner is larger the more edges it has. Drag a node to move it; hover over an edge for its count. The layout starts from a fixed seed, so the same edges draw the same figure.

See Also

lab.connectome.between_types.network_edges : Picks the edges.

Examples:

>>> import polars as pl
>>> rows = [
...     ("LC16", "P1", 2400, "out"),
...     ("LC16", "P2", 1500, "out"),
...     ("LC16", "P3", 800, "out"),
...     ("LC16", "P4", 300, "out"),
...     ("P5", "LC16", 900, "in"),
...     ("P6", "LC16", 450, "in"),
...     ("P7", "LC16", 120, "in"),
...     ("P1", "P3", 600, "lateral"),
...     ("P2", "P3", 90, "lateral"),
... ]
>>> names = ["src", "dst", "synapses", "kind"]
>>> edges = pl.DataFrame(rows, schema=names, orient="row")
>>> figure = network_figure(edges, ["LC16"])
Source code in lab/connectome/plots.py
def network_figure(edges: pl.DataFrame, types: Sequence[str]) -> str:
    """Draw a network of types and their partner types.

    Parameters
    ----------
    edges : polars.DataFrame
        Columns ``src``, ``dst``, ``synapses``, and ``kind``, which is
        ``"out"``, ``"in"``, or ``"lateral"``, as
        `between_types.network_edges` returns them.
    types : sequence of str
        The types to draw large, each in its color; every other node is
        a partner type, in gray.

    Returns
    -------
    str
        A page of HTML that loads vis-network, so it needs a network
        connection.
        An arrow runs from each source to its target, as wide as its
        synapse count in proportion to the heaviest edge, and in the
        color of its kind.
        A partner is larger the more edges it has.
        Drag a node to move it; hover over an edge for its count.
        The layout starts from a fixed seed, so the same edges draw the
        same figure.

    See Also
    --------
    lab.connectome.between_types.network_edges : Picks the edges.

    Examples
    --------
    >>> import polars as pl
    >>> rows = [
    ...     ("LC16", "P1", 2400, "out"),
    ...     ("LC16", "P2", 1500, "out"),
    ...     ("LC16", "P3", 800, "out"),
    ...     ("LC16", "P4", 300, "out"),
    ...     ("P5", "LC16", 900, "in"),
    ...     ("P6", "LC16", 450, "in"),
    ...     ("P7", "LC16", 120, "in"),
    ...     ("P1", "P3", 600, "lateral"),
    ...     ("P2", "P3", 90, "lateral"),
    ... ]
    >>> names = ["src", "dst", "synapses", "kind"]
    >>> edges = pl.DataFrame(rows, schema=names, orient="row")
    >>> figure = network_figure(edges, ["LC16"])
    """
    degree: dict[str, int] = {}
    for name in [*edges["src"], *edges["dst"]]:
        degree[name] = degree.get(name, 0) + 1
    kind_colors = {
        "out": DIRECTION_COLORS["out"],
        "in": DIRECTION_COLORS["in"],
        "lateral": CONTEXT_COLOR,
    }
    nodes = []
    for name, count in degree.items():
        is_type = name in types
        fill = TYPE_COLORS.get(name, CONTEXT_COLOR)
        nodes.append(
            {
                "id": name,
                "label": name,
                "shape": "ellipse" if is_type else "dot",
                # A partner grows with its edges, so the ones that tie
                # several types together stand out.
                "size": 24 if is_type else 8 + 2 * min(count, 10),
                "color": fill,
                "font": {
                    "size": 18 if is_type else 14,
                    "color": _ink_on(fill) if is_type else "black",
                },
            }
        )
    heaviest = max(edges["synapses"].to_list())
    links = []
    for row in edges.iter_rows(named=True):
        count = int(row["synapses"])
        links.append(
            {
                "from": row["src"],
                "to": row["dst"],
                "arrows": "to",
                # Proportional to the heaviest edge, with a floor so
                # that a weak edge is still a line.
                "width": max(1.5, 8 * count / heaviest),
                "label": f"{count:,}",
                "title": (
                    f"{row['src']} to {row['dst']}: {count:,} synapses "
                    f"({row['kind']})"
                ),
                "color": {"color": kind_colors[row["kind"]]},
            }
        )
    options = {
        "layout": {"randomSeed": 0},
        "edges": {
            "smooth": {"type": "dynamic"},
            "font": {
                "size": 13,
                "color": "#333",
                "strokeWidth": 2,
                "strokeColor": "white",
            },
        },
        "interaction": {"hover": True},
        "physics": {
            "solver": "forceAtlas2Based",
            "forceAtlas2Based": {
                "gravitationalConstant": -100,
                "springLength": 130,
                "centralGravity": 0.02,
                "avoidOverlap": 0.5,
            },
            "stabilization": {"iterations": 600, "fit": True},
        },
    }
    legend = [
        (kind_colors["out"], "leaves a type"),
        (kind_colors["in"], "enters a type"),
        (kind_colors["lateral"], "lateral"),
    ]
    key = "".join(
        f'<span><i style="background:{color}"></i>{label}</span>'
        for color, label in legend
    )
    # A "</" would end the script early.
    data = json.dumps(
        {"nodes": nodes, "edges": links, "options": options}
    ).replace("</", "<\\/")
    return f"""<!doctype html>
<meta charset="utf-8">
<script src="{VIS_NETWORK}"></script>
<style>
html, body {{ margin: 0; height: 100%; font: 13px sans-serif; }}
#network {{ height: 100%; background: white; }}
#key {{ position: absolute; top: 8px; left: 8px; display: flex;
        gap: 12px; }}
#key i {{ display: inline-block; width: 18px; height: 4px;
          margin-right: 5px; vertical-align: middle; }}
</style>
<div id="network"></div>
<div id="key">{key}</div>
<script>
const spec = {data};
const network = new vis.Network(
    document.getElementById("network"),
    {{
        nodes: new vis.DataSet(spec.nodes),
        edges: new vis.DataSet(spec.edges),
    }},
    spec.options
);
// Once the layout has settled, a dragged node stays where it is put.
network.once("stabilizationIterationsDone", () => {{
    network.setOptions({{ physics: false }});
}});
</script>
"""

plot_neurons(cells, colors=TYPE_COLORS, height=560)

Show cells in 3D in a Neuroglancer frame, with buttons.

The frame draws the public FlyWire meshes of the cells, each type in its color. The buttons below it open the same scene in a new tab and list the same cells in the Codex search.

Parameters:

Name Type Description Default
cells mapping of str to sequence of int

The root ids of the cells to show, by cell type.

required
colors mapping of str to str

Color of each type; look.TYPE_COLORS by default.

TYPE_COLORS
height int

Height of the frame in pixels.

560

Returns:

Type Description
str

HTML for a page or a notebook cell. The frame needs a browser that has WebGL, and the Codex button needs a Codex login.

See Also

lab.connectome.links.neuroglancer_url : Builds the scene. lab.connectome.links.codex_search_url : Builds the search.

Examples:

>>> cells = {"LC16": [720575940606173630]}
>>> "Open in Codex" in plot_neurons(cells)
True
>>> figure = plot_neurons(cells)
Source code in lab/connectome/plots.py
def plot_neurons(
    cells: Mapping[str, Sequence[int]],
    colors: Mapping[str, str] = TYPE_COLORS,
    height: int = 560,
) -> str:
    """Show cells in 3D in a Neuroglancer frame, with buttons.

    The frame draws the public FlyWire meshes of the cells, each type
    in its color.
    The buttons below it open the same scene in a new tab and list
    the same cells in the Codex search.

    Parameters
    ----------
    cells : mapping of str to sequence of int
        The root ids of the cells to show, by cell type.
    colors : mapping of str to str, optional
        Color of each type; `look.TYPE_COLORS` by default.
    height : int, default 560
        Height of the frame in pixels.

    Returns
    -------
    str
        HTML for a page or a notebook cell.
        The frame needs a browser that has WebGL,
        and the Codex button needs a Codex login.

    See Also
    --------
    lab.connectome.links.neuroglancer_url : Builds the scene.
    lab.connectome.links.codex_search_url : Builds the search.

    Examples
    --------
    >>> cells = {"LC16": [720575940606173630]}
    >>> "Open in Codex" in plot_neurons(cells)
    True
    >>> figure = plot_neurons(cells)
    """
    fill = {i: colors[t] for t, ids in cells.items() for i in ids}
    scene = neuroglancer_url(list(fill), colors=fill)
    search = codex_search_url(", ".join(str(i) for i in fill))
    frame = (
        f'<iframe src="{html.escape(scene)}" width="100%" '
        f'height="{height}" style="border: 1px solid #ccc" '
        'allow="fullscreen" loading="lazy"></iframe>'
    )
    buttons = (
        '<p style="display: flex; gap: 0.5rem; margin: 0.5rem 0 0">'
        f"{_button(scene, 'Open in new tab')}"
        f"{_button(search, 'Open in Codex')}"
        "</p>"
    )
    return f"<div>{frame}{buttons}</div>"

top_partners_figure(out_mass, in_mass, types, *, shown=15)

Draw the heaviest partner types of each type as bars.

Parameters:

Name Type Description Default
out_mass DataFrame

Columns type, partner_type, and synapses, as strength.type_mass returns them for outputs and for inputs.

required
in_mass DataFrame

Columns type, partner_type, and synapses, as strength.type_mass returns them for outputs and for inputs.

required
types sequence of str

The types, one column of panels each, in their colors.

required
shown int

Bars per panel.

15

Returns:

Type Description
Figure

Outputs in the top row, inputs in the bottom row.

Examples:

>>> import polars as pl
>>> def mass(counts):
...     return pl.DataFrame(
...         {
...             "type": ["LC16"] * 6 + ["LC6"] * 6,
...             "partner_type": [f"P{i}" for i in range(1, 7)] * 2,
...             "synapses": counts,
...         }
...     )
>>> out_mass = mass(
...     [820, 460, 300, 120, 60, 25, 90, 640, 40, 410, 30, 220]
... )
>>> in_mass = mass(
...     [300, 700, 150, 520, 80, 40, 610, 200, 330, 90, 260, 20]
... )
>>> figure = top_partners_figure(
...     out_mass, in_mass, ["LC16", "LC6"], shown=5
... )

top_partners_figure

Source code in lab/connectome/plots.py
def top_partners_figure(
    out_mass: pl.DataFrame,
    in_mass: pl.DataFrame,
    types: Sequence[str],
    *,
    shown: int = 15,
) -> Figure:
    """Draw the heaviest partner types of each type as bars.

    Parameters
    ----------
    out_mass, in_mass : polars.DataFrame
        Columns ``type``, ``partner_type``, and ``synapses``, as
        `strength.type_mass` returns them for outputs and for inputs.
    types : sequence of str
        The types, one column of panels each, in their colors.
    shown : int, default 15
        Bars per panel.

    Returns
    -------
    matplotlib.figure.Figure
        Outputs in the top row, inputs in the bottom row.

    Examples
    --------
    >>> import polars as pl
    >>> def mass(counts):
    ...     return pl.DataFrame(
    ...         {
    ...             "type": ["LC16"] * 6 + ["LC6"] * 6,
    ...             "partner_type": [f"P{i}" for i in range(1, 7)] * 2,
    ...             "synapses": counts,
    ...         }
    ...     )
    >>> out_mass = mass(
    ...     [820, 460, 300, 120, 60, 25, 90, 640, 40, 410, 30, 220]
    ... )
    >>> in_mass = mass(
    ...     [300, 700, 150, 520, 80, 40, 610, 200, 330, 90, 260, 20]
    ... )
    >>> figure = top_partners_figure(
    ...     out_mass, in_mass, ["LC16", "LC6"], shown=5
    ... )
    """
    fig = Figure(figsize=(14, 8))
    axes = fig.subplots(2, len(types), squeeze=False)
    for col, t in enumerate(types):
        for row, (mass, direction) in enumerate(
            [(out_mass, "out"), (in_mass, "in")]
        ):
            ax = axes[row][col]
            top = (
                mass.filter(pl.col("type") == t)
                .sort("synapses", descending=True)
                .head(shown)
                .sort("synapses")
            )
            ax.barh(top["partner_type"], top["synapses"], color=TYPE_COLORS[t])
            ax.set_title(f"{t} {direction}", color=TYPE_COLORS[t])
            ax.tick_params(labelsize=8)
            ax.set_xlabel("synapses")
    return fig