Marsilea: Declarative Composable Visualizations

repository·main·Indexed 43 days ago

https://github.com/marsilea-viz/marsilea

A Python library built on top of Matplotlib for the declarative creation of complex, composable visualizations. It allows users to incrementally build figures like annotated heatmaps, oncoprints, UpSet plots, and biological panels using components like WhiteBoard and ClusterBoard. Supports integration with AnnData and provides specialized plotters for genomics, single-cell RNA-seq, and statistical analysis.

Tokens
25.8K
Snippets
91
Records
112
Agent score
55%

What's inside Marsilea

  1. Explore the Marsilea visualization gallery

    main

    Marsilea provides a gallery of composable visualizations built using Python, Marsilea, and Matplotlib. These examples demonstrate how to create complex biological and data-driven figures, including:

    • Heatmaps: Complex annotated heatmaps.
    • Genomics: Oncoprints, sequence logos, sequence alignments, genome tracks, and arc diagrams.
    • Set Theory: UpSet plots.
    • Single-cell: Single-cell specific figures.

    Each example in the gallery is provided as a full script, allowing you to download the code and replace the sample data with your own datasets.

  2. What is Marsilea and how does it work?

    main
    Marsilea is a Python library for the declarative creation of composable visualizations. It provides a high-level API built on top of Matplotlib, allowing users to 'puzzle' different visualizations together. This approach enables the creation of complex figures—like genome tracks, sequence logos, and single-cell panels—using a consistent, high-level interface.
  3. What is Composable Visualization in Marsilea?

    main
    Composable visualization refers to the ability to combine multiple plots to show different aspects of a single dataset. In Marsilea, you create these visualizations declaratively and incrementally. For example, you can start with a heatmap and then add a bar chart on top, labels on the side, or a dendrogram to provide additional context or summary statistics.
  4. Handle overlaid hue levels in `Strip`, `Swarm`, and `Point` plots

    main

    The Strip, Swarm, and Point plotters draw hue levels on top of each other by default. If you attempt to annotate comparisons between these overlaid levels, the comparison will be skipped with a warning.

    To fix this, use dodge=True to separate the hue levels spatially, allowing brackets to point to them correctly.

    >>> strip = mp.Strip(
    ...     {"Control": control, "Treated": treated},
    ...     dodge=True, size=3, label="Expression",
    ... )
    >>> strip.annotate_stats(pairs="hue", text_format="star")
  5. Use `pairs` shorthands for systematic comparisons

    main

    When calling .annotate_stats(), you can use shorthands to avoid manually defining every pair:

    • pairs="hue": Compares the conditions (hue levels) inside every category. Useful when passing a dictionary of DataFrames to a plotter.
    • pairs="all": Compares all categories with each other. Note: In grouped plots, this only compares categories within the same group.
    • pairs="ref": Reduces the comparison to only those against a single reference. When used with pairs="hue", it compares every condition to one reference condition. When used with pairs="all", it compares every category to one reference category.
    >>> # Compare all categories against category '0'
    >>> bar.annotate_stats(pairs="all", ref="0", text_format="star")
    
    >>> # Compare conditions within each category
    >>> box.annotate_stats(pairs="hue", test="Mann-Whitney")
  6. Stack multiple cross-layouts with StackBoard

    main

    Use marsilea.base.StackBoard to arrange multiple cross-layouts (like heatmaps) in a specific direction and alignment.

    Unlike CompositeBoard, a StackBoard can stack itself or stack a CompositeBoard. However, a StackBoard cannot be appended to a CompositeBoard using the + or / operators.

    Key Parameters:

    • direction: Controls the stacking axis. Options include "horizontal" or "vertical".
    • align: Controls how the elements are aligned relative to the main canvas. Options include "top", "center", "bottom", "left", "right" (depending on direction).

    Note on Axis Access: Methods get_ax() and get_main_ax() only access the boards directly stacked within the StackBoard. They do not reach into nested stacks or composite boards.

    import marsilea as ma
    import marsilea.plotter as mp
    import numpy as np
    
    data = np.random.rand(10, 10)
    h1 = ma.Heatmap(data, cmap="Reds")
    h2 = ma.Heatmap(data, cmap="Greens")
    
    # Stack horizontally, aligned to the top
    sb = ma.StackBoard([h1, h2], direction="horizontal", align="top")
    sb.render()
  7. Specify pairs for statistical comparison

    main

    You can define which groups to compare using the pairs argument in the statistical annotation configuration. Supported values for pairs include:

    • A list of tuples: Explicitly name the categories to compare.
      • Without hue: [('GroupA', 'GroupB'), ('GroupB', 'GroupC')]
      • With hue: [(('GroupA', 'Level1'), ('GroupB', 'Level1')), ...]
    • 'hue': Compares all levels of the hue variable within every category.
    • 'all': Compares all possible pairs of categories. If a ref (reference) category is provided, it compares the reference to all other categories.

    Warning: Using pairs='all' on a large number of categories can result in an unreadable number of brackets. It is recommended to list specific pairs of interest.

    # Explicit pairs
    pairs = [('Control', 'Treatment'), ('Control', 'Placebo')]
    
    # Compare all hue levels within each category
    pairs = 'hue'
    
    # Compare everything to a specific reference category
    # (Requires setting ref='Control')
    pairs = 'all'
  8. Handle different plot sides using RenderPlan attributes

    main

    When creating custom RenderPlan objects that are added to different sides of a plot (e.g., using .add_left(), .add_top(), etc.), you often need to adjust the drawing logic (like orientation or axis inversion) based on where the plan is placed.

    To implement side-aware drawing, it is recommended to inherit from StatsBase instead of the base RenderPlan. You can then use the following attributes to query the placement:

    • .side: Returns the current side (e.g., 'left', 'top').
    • .is_body: Boolean indicating if the plan is in the main area, top, or bottom.
    • .is_flank: Boolean indicating if the plan is on the left or right side.

    Example logic for adjusting orientation and axis inversion:

    from marsilea.plotter.base import StatsBase
    
    class Lollipop(StatsBase):
        def __init__(self, data):
            self.set_data(data)
    
        def render_ax(self, spec):
            ax = spec.ax
            data = spec.data
            lim = len(data)
            locs = np.arange(lim) + 0.5
            
            # Adjust orientation based on whether it's the main body or a side flank
            orientation = "vertical" if self.is_body else "horizontal"
            ax.stem(locs, data, basefmt="none", orientation=orientation)
            ax.set_axis_off()
    
            if self.is_body:
                ax.set_xlim(0, lim)
            if self.side == "left":
                ax.invert_xaxis()
            if self.is_flank:
                ax.set_ylim(lim, 0)
  9. Concatenate WhiteBoards using operators

    main

    You can combine multiple WhiteBoard objects using overloaded operators to create complex layouts:

    • + (Plus): Places two canvases side-by-side. You can insert an integer between them to specify the amount of white space added.
    • / (Divide): Places two canvases top and bottom. You can insert an integer between them to specify the amount of white space added.
    # Side-by-side with 1 unit of padding
    (wb1 + 1 + wb2).render()
    
    # Top and bottom with 1 unit of padding
    (wb1 / 1 / wb2).render()
  10. How AnnData references carry axis information

    main

    Marsilea uses the axis information embedded in anndata.acc references to ensure components are placed on the correct side of a board.

    • A.obs[...] spans observations (cells) and belongs on the left or right.
    • A.var[...] spans variables (genes) and belongs on the top or bottom.

    If you attempt to add a reference to the wrong side (e.g., adding an observation reference to the top), Marsilea will raise a MisalignedRef error, which explicitly names the reference and the required fix.

    # Correct placement based on axis
    h = ma.Heatmap(adata, A.X[:, :])
    h.add_left(mp.Colors(A.obs["lineage"]), size=0.2)  # obs goes on left/right
    h.add_bottom(mp.Labels(A.var.index))               # var goes on top/bottom
  11. Quickstart: Create a composable heatmap

    main

    Marsilea allows you to build complex visualizations incrementally by adding building blocks (like bar charts, labels, or dendrograms) to a main plot object (like a Heatmap). This declarative approach is built on top of Matplotlib.

    import numpy as np
    import marsilea as ma
    import marsilea.plotter as mp
    
    data = np.random.default_rng(0).standard_normal((10, 10))
    
    # Initialize the main heatmap
    h = ma.Heatmap(data, linewidth=0.5, height=4, width=4)
    
    # Add components incrementally
    h.add_top(mp.Bar(data.mean(axis=0)), size=0.8, pad=0.1)
    h.add_left(mp.Labels([f"Gene{i}" for i in range(10)]), pad=0.05)
    h.add_dendrogram("right")
    h.add_legends()
    
    # Render the final visualization
    h.render()
  12. Customize plots by accessing main axes

    main

    To customize a Marsilea plot using standard Matplotlib commands, you must first call the .render() method. Once rendered, you can retrieve the primary axes using .get_main_ax().

    If the plot is a single visualization, .get_main_ax() returns a single Matplotlib Axes object. If the plot is split (e.g., via .cut_rows() or .cut_cols()), it returns a list of Axes objects ordered from the upper left to the lower right.

    import marsilea as ma
    import numpy as np
    from matplotlib.patches import Rectangle
    
    data = np.random.rand(20, 20)
    h = ma.Heatmap(data, linewidth=1)
    h.render()  # Must call render() first
    
    # For a single plot:
    hax = h.get_main_ax()
    
    # For split plots (returns a list):
    h.cut_rows(cut=[5])
    h.render()
    hax_list = h.get_main_ax()
    for ax in hax_list:
        # Apply customization to each axis
        pass