Source code for plotnine.positions.position_stack
from __future__ import annotations
from warnings import warn
import numpy as np
import pandas as pd
from ..exceptions import PlotnineWarning
from ..utils import remove_missing
from .position import position
[docs]class position_stack(position):
"""
Stack plotted objects on top of each other
The objects to stack are those that have
an overlapping x range.
"""
fill = False
def __init__(self, vjust=1, reverse=False):
self.params = {"vjust": vjust, "reverse": reverse}
def setup_params(self, data):
"""
Verify, modify & return a copy of the params.
"""
# Variable for which to do the stacking
if "ymax" in data:
if any((data["ymin"] != 0) & (data["ymax"] != 0)):
warn(
"Stacking not well defined when not "
"anchored on the axis.",
PlotnineWarning,
)
var = "ymax"
elif "y" in data:
var = "y"
else:
warn(
"Stacking requires either ymin & ymax or y "
"aesthetics. Maybe you want position = 'identity'?",
PlotnineWarning,
)
var = None
params = self.params.copy()
params["var"] = var
params["fill"] = self.fill
return params
def setup_data(self, data, params):
if not params["var"]:
return data
if params["var"] == "y":
data["ymax"] = data["y"]
elif params["var"] == "ymax":
bool_idx = data["ymax"] == 0
data.loc[bool_idx, "ymax"] = data.loc[bool_idx, "ymin"]
data = remove_missing(
data, vars=("x", "xmin", "xmax", "y"), name="position_stack"
)
return data
@classmethod
def compute_panel(cls, data, scales, params):
if not params["var"]:
return data
# Positioning happens after scale has transformed the data,
# and stacking only works well for linear data.
# If the scale(transformation) is not linear, we undo it,
# do the "stacking" and redo the transformation.
from ..scales.scale_continuous import scale_continuous
if isinstance(scales.y, scale_continuous):
undo_transform = (
not scales.y.is_linear and scales.y.trans.domain_is_numerical
)
else:
undo_transform = False
if undo_transform:
data = cls.transform_position(data, trans_y=scales.y.inverse)
negative = data["ymax"] < 0
neg = data.loc[negative]
pos = data.loc[~negative]
if len(neg):
neg = cls.collide(neg, params=params)
if len(pos):
pos = cls.collide(pos, params=params)
data = pd.concat([neg, pos], axis=0, ignore_index=True, sort=True)
if undo_transform:
data = cls.transform_position(data, trans_y=scales.y.transform)
return data
@staticmethod
def strategy(data, params):
"""
Stack overlapping intervals.
Assumes that each set has the same horizontal position
"""
vjust = params["vjust"]
y = data["y"].copy()
y[np.isnan(y)] = 0
heights = np.append(0, y.cumsum())
if params["fill"]:
heights = heights / np.abs(heights[-1])
data["ymin"] = np.min([heights[:-1], heights[1:]], axis=0)
data["ymax"] = np.max([heights[:-1], heights[1:]], axis=0)
# less intuitive than (ymin + vjust(ymax-ymin)), but
# this way avoids subtracting numbers of potentially
# similar precision
data["y"] = (1 - vjust) * data["ymin"] + vjust * data["ymax"]
return data