Source code for plotnine_extra.positions.position_disjoint_ranges
"""
``position_disjoint_ranges``: stack overlapping intervals into
disjoint rows. Port of ``ggh4x::position_disjoint_ranges``.
Each layer with ``xmin`` / ``xmax`` aesthetics gets reassigned a
``y`` value so that intervals that overlap on x are placed on
distinct rows. Useful for gene-track / Gantt-style plots.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import numpy as np
from plotnine.positions.position import position
if TYPE_CHECKING:
import pandas as pd
def _assign_rows(
starts: np.ndarray, ends: np.ndarray, extend: float = 0.0
) -> np.ndarray:
"""Greedy interval-graph colouring; returns 0-indexed row ids."""
n = starts.size
order = np.argsort(starts)
rows_end: list[float] = []
out = np.empty(n, dtype=int)
for idx in order:
s = starts[idx] - extend
e = ends[idx] + extend
placed = False
for r, current_end in enumerate(rows_end):
if current_end < s:
rows_end[r] = e
out[idx] = r
placed = True
break
if not placed:
rows_end.append(e)
out[idx] = len(rows_end) - 1
return out
[docs]
class position_disjoint_ranges(position):
"""
Vertically stack overlapping x intervals into disjoint rows.
Parameters
----------
extend : float, default 0
Padding added to each interval before deciding overlap.
Positive values force more vertical separation.
stepsize : float, default 1
Vertical spacing between rows.
"""
REQUIRED_AES = {"xmin", "xmax"}
def __init__(self, extend: float = 0.0, stepsize: float = 1.0):
super().__init__()
self.extend = extend
self.stepsize = stepsize
[docs]
def setup_params(self, data):
return {"extend": self.extend, "stepsize": self.stepsize}
[docs]
@classmethod
def compute_layer(
cls,
data: pd.DataFrame,
params,
layout,
) -> pd.DataFrame:
if "xmin" not in data.columns or "xmax" not in data.columns:
return data
data = data.copy()
starts = data["xmin"].to_numpy(dtype=float)
ends = data["xmax"].to_numpy(dtype=float)
rows = _assign_rows(starts, ends, extend=params["extend"])
ys = (rows + 1) * params["stepsize"]
data["y"] = ys
if "ymin" in data.columns:
data["ymin"] = ys - params["stepsize"] / 2
if "ymax" in data.columns:
data["ymax"] = ys + params["stepsize"] / 2
return data