Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions docs/examples/plot_types/11_topic_ribbon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"""
Top-aligned ribbon flow
=======================

Fixed-row ribbon flows for category transitions across adjacent periods.

Why UltraPlot here?
-------------------
This is a distinct flow layout from Sankey: topic rows are fixed globally and
flows are stacked from each row top, so vertical position is semantically stable.

Key function: :py:meth:`ultraplot.axes.PlotAxes.ribbon`.

See also
--------
* :doc:`2D plot types </2dplots>`
* :doc:`Layered Sankey diagram <07_sankey>`
"""

import numpy as np
import pandas as pd

import ultraplot as uplt

GROUP_COLORS = {
"Group A": "#2E7D32",
"Group B": "#6A1B9A",
"Group C": "#5D4037",
"Group D": "#0277BD",
"Group E": "#F57C00",
"Group F": "#C62828",
"Group G": "#D84315",
}

TOPIC_TO_GROUP = {
"Topic 01": "Group A",
"Topic 02": "Group A",
"Topic 03": "Group B",
"Topic 04": "Group B",
"Topic 05": "Group C",
"Topic 06": "Group C",
"Topic 07": "Group D",
"Topic 08": "Group D",
"Topic 09": "Group E",
"Topic 10": "Group E",
"Topic 11": "Group F",
"Topic 12": "Group F",
"Topic 13": "Group G",
"Topic 14": "Group G",
}


def build_assignments():
"""Synthetic entity-category assignments by period."""
state = np.random.RandomState(51423)
countries = [f"Entity {i:02d}" for i in range(1, 41)]
periods = ["1990-1999", "2000-2009", "2010-2019", "2020-2029"]
topics = list(TOPIC_TO_GROUP.keys())

rows = []
for country in countries:
topic = state.choice(topics)
rows.append((country, periods[0], topic))
for period in periods[1:]:
if state.rand() < 0.68:
next_topic = topic
else:
group = TOPIC_TO_GROUP[topic]
same_group = [
t for t in topics if TOPIC_TO_GROUP[t] == group and t != topic
]
next_topic = state.choice(
same_group if same_group and state.rand() < 0.6 else topics
)
topic = next_topic
rows.append((country, period, topic))
return pd.DataFrame(rows, columns=["country", "period", "topic"]), periods


df, periods = build_assignments()

group_order = list(GROUP_COLORS)
topic_order = []
for group in group_order:
topic_order.extend(sorted([t for t, g in TOPIC_TO_GROUP.items() if g == group]))

fig, axs = uplt.subplots(nrows=2, hratios=(3.0, 0.8), refwidth=6.3, share=False)
axs[0].ribbon(
df,
id_col="country",
period_col="period",
topic_col="topic",
period_order=periods,
topic_order=topic_order,
group_map=TOPIC_TO_GROUP,
group_order=group_order,
group_colors=GROUP_COLORS,
composition=True,
composition_ax=axs[1],
composition_ylabel="Assigned topics",
)

axs[0].format(title="Category transitions with fixed top-aligned rows")
fig.format(suptitle="Top-aligned ribbon flow by period")
fig.show()
110 changes: 110 additions & 0 deletions ultraplot/axes/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -2305,6 +2305,116 @@ def _looks_like_links(values):
diagrams = sankey.finish()
return diagrams[0] if len(diagrams) == 1 else diagrams

@docstring._snippet_manager
def ribbon(
self,
data: Any,
*,
id_col: str = "id",
period_col: str = "period",
topic_col: str = "topic",
value_col: str | None = None,
period_order: Sequence[Any] | None = None,
topic_order: Sequence[Any] | None = None,
group_map: Mapping[Any, Any] | None = None,
group_order: Sequence[Any] | None = None,
group_colors: Mapping[Any, Any] | None = None,
xmargin: float = 0.12,
ymargin: float = 0.08,
row_height_ratio: float = 2.2,
node_width: float = 0.018,
flow_curvature: float = 0.45,
flow_alpha: float = 0.58,
show_topic_labels: bool = True,
topic_label_offset: float = 0.028,
topic_label_size: float = 7.4,
topic_label_box: bool = True,
composition_ax: Any | None = None,
composition: bool = False,
composition_alpha: float = 0.86,
composition_ylabel: str = "Assigned topics",
) -> dict[str, Any]:
"""
Draw a fixed-row, top-aligned ribbon flow diagram from long-form records.

Parameters
----------
data : pandas.DataFrame or mapping-like
Long-form records with entity id, period, and topic columns.
id_col, period_col, topic_col : str, optional
Column names for entity id, period, and topic.
value_col : str, optional
Optional weight column. If omitted, each record is weighted as 1.
period_order, topic_order : sequence, optional
Explicit ordering for periods and topic rows.
group_map : mapping, optional
Topic-to-group mapping used for grouped ordering and colors.
group_order : sequence, optional
Group ordering for row arrangement and composition stacking.
group_colors : mapping, optional
Group-to-color mapping. Missing groups use the patch color cycle.
xmargin, ymargin : float, optional
Plot-space margins in normalized axes coordinates.
row_height_ratio : float, optional
Scale factor controlling row occupancy by nodes/flows.
node_width : float, optional
Node column width in normalized axes coordinates.
flow_curvature : float, optional
Bezier curvature for ribbons.
flow_alpha : float, optional
Ribbon alpha.
show_topic_labels : bool, optional
Whether to draw topic labels on the right.
topic_label_offset : float, optional
Offset for right-side topic labels.
topic_label_size : float, optional
Topic label font size.
topic_label_box : bool, optional
Whether to draw white backing boxes behind topic labels.
composition_ax : `~ultraplot.axes.Axes`, optional
Optional secondary axes for a stacked group composition panel.
composition : bool, optional
Whether to draw composition stackplot on `composition_ax`.
composition_alpha : float, optional
Alpha for composition stack areas.
composition_ylabel : str, optional
Y label for composition panel.

Returns
-------
dict
Mapping of created artists and resolved orders.
"""
from .plot_types.ribbon import ribbon_diagram

return ribbon_diagram(
self,
data,
id_col=id_col,
period_col=period_col,
topic_col=topic_col,
value_col=value_col,
period_order=period_order,
topic_order=topic_order,
group_map=group_map,
group_order=group_order,
group_colors=group_colors,
xmargin=xmargin,
ymargin=ymargin,
row_height_ratio=row_height_ratio,
node_width=node_width,
flow_curvature=flow_curvature,
flow_alpha=flow_alpha,
show_topic_labels=show_topic_labels,
topic_label_offset=topic_label_offset,
topic_label_size=topic_label_size,
topic_label_box=topic_label_box,
composition_ax=composition_ax,
composition=composition,
composition_alpha=composition_alpha,
composition_ylabel=composition_ylabel,
)

def circos(
self,
sectors: Mapping[str, Any],
Expand Down
Loading
Loading