Skip to content

Commit

Permalink
Merge pull request #49 from geoneric/gh41
Browse files Browse the repository at this point in the history
Add support for spreading of vertical lines
  • Loading branch information
kordejong authored Dec 6, 2024
2 parents 5c8e94e + b2755a6 commit a8e410f
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 24 deletions.
8 changes: 7 additions & 1 deletion source/package/adaptation_pathways/cli/plot_pathway_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def main() -> int:
Usage:
{command} [--title=<title>] [--x_label=<label>] [--show_legend]
[--overshoot] <basename> <plot>
[--overshoot] [--spread=<spread>] <basename> <plot>
Arguments:
basename Either, the name without postfix and extension of text
Expand All @@ -74,6 +74,10 @@ def main() -> int:
--overshoot Show tipping points as overshoots, extending a little
bit beyond the actual point
--show_legend Show legend
--spread=<spread> Separate overlapping lines by a percentage [0, 1] of
the range passed in. A value of 0.01 means 1% of the
range of x-coordinates. Passing in a value > 0.02 is
likely not useful.
--title=<title> Title
--x_label=<label> Label of x-axis
Expand All @@ -93,11 +97,13 @@ def main() -> int:
x_label = arguments["--x_label"] if arguments["--x_label"] is not None else ""
show_legend = arguments["--show_legend"]
overshoot = arguments["--overshoot"]
overlapping_lines_spread = float(arguments["--spread"])

plot_arguments: dict[str, typing.Any] = {
"title": title,
"x_label": x_label,
"show_legend": show_legend,
"overlapping_lines_spread": overlapping_lines_spread,
}

if overshoot:
Expand Down
69 changes: 62 additions & 7 deletions source/package/adaptation_pathways/plot/pathway_map/classic.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from ...graph import PathwayMap
from ...graph.node import ActionBegin, ActionEnd
from .. import alias
from ..util import add_position
from ..util import add_position, distribute


def _plot_action_lines(
Expand Down Expand Up @@ -104,11 +104,11 @@ def _plot_action_tipping_points(
path_collection = mpl.collections.PathCollection(None)

if len(nodes) > 0:
# The last "tipping point" is skipped
node_pos = np.asarray([layout[v] for v in nodes][:-1])
# TODO Skip the tipping point at the end of each individual path way
node_pos = np.asarray([layout[v] for v in nodes])
x, y = zip(*node_pos)
x = np.array(x) + tipping_point_overshoot
colours = [colour_by_action_name[node.action.name] for node in nodes][:-1]
colours = [colour_by_action_name[node.action.name] for node in nodes]

if tipping_point_marker.is_filled():
scatter_arguments = {
Expand Down Expand Up @@ -311,11 +311,57 @@ def _distribute_horizontally(
_distribute_horizontally(pathway_map, action_begin_new, position_by_node)


def _spread_vertically(
pathway_map: PathwayMap,
position_by_node: dict[ActionBegin | ActionEnd, np.ndarray],
overlapping_lines_spread: float,
) -> None:

# - If vertical spreading is enabled
# - Assign all action_end / action_begin combinations to bins
# - For those bins that contain more than one elements, tweak the x-coordinates
# - Make the magnitude of the tweak configurable (dependent on line width?)

nodes_by_x: dict[float, list[tuple[ActionEnd, ActionBegin]]] = {}

for action_end in pathway_map.all_action_ends():
action_begins = pathway_map.action_begins(action_end)
x = position_by_node[action_end][0]

if x not in nodes_by_x:
nodes_by_x[x] = []

for action_begin in action_begins:
assert position_by_node[action_begin][0] == x
nodes_by_x[x].append((action_end, action_begin))

min_x = min(nodes_by_x.keys())
max_x = max(nodes_by_x.keys())
range_x = max_x - min_x

# Root action end. This x coordinate needs no tweaking.
del nodes_by_x[min_x]

for x, nodes in nodes_by_x.items():
nr_nodes = len(nodes)

if nr_nodes > 1:
x_coordinates = distribute(
nr_nodes * [x], overlapping_lines_spread * range_x
)

for idx in range(nr_nodes):
action_end, action_begin = nodes[idx]
position_by_node[action_end][0] = x_coordinates[idx]
position_by_node[action_begin][0] = x_coordinates[idx]


# pylint: disable-next=too-many-locals, too-many-branches
def _distribute_vertically(
pathway_map: PathwayMap,
root_action_begin: ActionBegin,
position_by_node: dict[ActionBegin | ActionEnd, np.ndarray],
overlapping_lines_spread: float,
) -> None:

action_end = pathway_map.action_end(root_action_begin)
Expand Down Expand Up @@ -438,9 +484,14 @@ def _distribute_vertically(
assert np.isnan(position_by_node[action_end][1])
position_by_node[action_end][1] = y_coordinate

if overlapping_lines_spread > 0:
_spread_vertically(pathway_map, position_by_node, overlapping_lines_spread)


def _layout(
pathway_map: PathwayMap,
*,
overlapping_lines_spread: float,
) -> dict[ActionBegin | ActionEnd, np.ndarray]:
"""
Layout that replicates the pathway map layout of the original (pre-2024) pathway generator
Expand All @@ -460,7 +511,7 @@ def _layout(
The graph in the pathway map passed in must contain an attribute called "level_by_action",
with a numeric value per action, which corresponds with the position in the above mentioned
stack. Low numbers correspond with a high position in the stack (large y-coordinate). Such
stack. Low numbers correspond with a high position in the stack (large y-coordinate). Such
actions will be positioned at the top of the pathway map.
"""
position_by_node: dict[ActionBegin | ActionEnd, np.ndarray] = {}
Expand All @@ -478,7 +529,9 @@ def _layout(
add_position(position_by_node, root_action_begin, (x_coordinate, 0))

_distribute_horizontally(pathway_map, root_action_begin, position_by_node)
_distribute_vertically(pathway_map, root_action_begin, position_by_node)
_distribute_vertically(
pathway_map, root_action_begin, position_by_node, overlapping_lines_spread
)

return position_by_node

Expand All @@ -497,10 +550,12 @@ def plot(
if legend_arguments is None:
legend_arguments = {}

overlapping_lines_spread: float = arguments.get("overlapping_lines_spread", 0)

classic_pathway_map_plotter(
axes,
pathway_map,
_layout(pathway_map),
_layout(pathway_map, overlapping_lines_spread=overlapping_lines_spread),
arguments=arguments,
legend_arguments=legend_arguments,
)
32 changes: 16 additions & 16 deletions source/test/ap_test/plot/pathway_map/layout_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -668,7 +668,7 @@ def test_empty(self):
sequence_graph = SequenceGraph()
pathway_map = sequence_graph_to_pathway_map(sequence_graph)
pathway_map.assign_tipping_points({})
positions = classic_layout(pathway_map)
positions = classic_layout(pathway_map, overlapping_lines_spread=0)

self.assertEqual(len(positions), 0)

Expand All @@ -693,7 +693,7 @@ def test_single_period(self):
}
)

positions = classic_layout(pathway_map)
positions = classic_layout(pathway_map, overlapping_lines_spread=0)
self.assertEqual(len(positions), 4)

self.assert_equal_positions(
Expand Down Expand Up @@ -736,7 +736,7 @@ def test_serial_pathway(self):
}
)

positions = classic_layout(pathway_map)
positions = classic_layout(pathway_map, overlapping_lines_spread=0)
self.assertEqual(len(positions), 8)

self.assert_equal_positions(
Expand Down Expand Up @@ -783,7 +783,7 @@ def test_diverging_pathways(self):
}
)

positions = classic_layout(pathway_map)
positions = classic_layout(pathway_map, overlapping_lines_spread=0)
self.assertEqual(len(positions), 8)

self.assert_equal_positions(
Expand Down Expand Up @@ -845,7 +845,7 @@ def test_use_case_01(self):
paths = list(pathway_map.all_paths())
self.assertEqual(len(paths), 4)

positions = classic_layout(pathway_map)
positions = classic_layout(pathway_map, overlapping_lines_spread=0)
self.assertEqual(len(positions), 24)

self.assert_equal_positions(
Expand Down Expand Up @@ -937,7 +937,7 @@ def test_use_case_02(self):
paths = list(pathway_map.all_paths())
self.assertEqual(len(paths), 10)

positions = classic_layout(pathway_map)
positions = classic_layout(pathway_map, overlapping_lines_spread=0)
self.assertEqual(len(positions), 32)

self.assert_equal_positions(
Expand Down Expand Up @@ -1103,7 +1103,7 @@ def test_action_combination01(self):
d: 2100,
}
)
positions = classic_layout(pathway_map)
positions = classic_layout(pathway_map, overlapping_lines_spread=0)
self.assertEqual(len(positions), 10)

self.assert_equal_positions(
Expand Down Expand Up @@ -1159,7 +1159,7 @@ def test_action_combination02(self):
paths = list(pathway_map.all_paths())
self.assertEqual(len(paths), 3)

positions = classic_layout(pathway_map)
positions = classic_layout(pathway_map, overlapping_lines_spread=0)
self.assertEqual(len(positions), 12)

self.assert_equal_positions(
Expand Down Expand Up @@ -1234,7 +1234,7 @@ def test_action_combination03(self):
d: 2100,
}
)
positions = classic_layout(pathway_map)
positions = classic_layout(pathway_map, overlapping_lines_spread=0)
self.assertEqual(len(positions), 12)

self.assert_equal_positions(
Expand Down Expand Up @@ -1309,7 +1309,7 @@ def test_action_combination04(self):
d: 2100,
}
)
positions = classic_layout(pathway_map)
positions = classic_layout(pathway_map, overlapping_lines_spread=0)
self.assertEqual(len(positions), 10)

self.assert_equal_positions(
Expand Down Expand Up @@ -1365,7 +1365,7 @@ def test_action_combination05(self):
paths = list(pathway_map.all_paths())
self.assertEqual(len(paths), 3)

positions = classic_layout(pathway_map)
positions = classic_layout(pathway_map, overlapping_lines_spread=0)
self.assertEqual(len(positions), 12)

self.assert_equal_positions(
Expand Down Expand Up @@ -1422,7 +1422,7 @@ def test_action_combination06(self):
paths = list(pathway_map.all_paths())
self.assertEqual(len(paths), 3)

positions = classic_layout(pathway_map)
positions = classic_layout(pathway_map, overlapping_lines_spread=0)
self.assertEqual(len(positions), 10)

self.assert_equal_positions(
Expand Down Expand Up @@ -1490,7 +1490,7 @@ def test_action_edition_01(self):
a2: 2060,
}
)
positions = classic_layout(pathway_map)
positions = classic_layout(pathway_map, overlapping_lines_spread=0)
self.assertEqual(len(positions), 8)

self.assert_equal_positions(
Expand Down Expand Up @@ -1539,7 +1539,7 @@ def test_vertical_action_order_01(self):
b c 2040
"""
pathway_map = configure_pathway_map(actions, sequences)
positions = classic_layout(pathway_map)
positions = classic_layout(pathway_map, overlapping_lines_spread=0)

y_coordinates_we_want = {
"current": 0,
Expand Down Expand Up @@ -1568,7 +1568,7 @@ def test_vertical_action_order_02(self):
current b 2030
"""
pathway_map = configure_pathway_map(actions, sequences)
positions = classic_layout(pathway_map)
positions = classic_layout(pathway_map, overlapping_lines_spread=0)

y_coordinates_we_want = {
"current": 0,
Expand Down Expand Up @@ -1599,7 +1599,7 @@ def test_vertical_action_order_03(self):
current b 2030
"""
pathway_map = configure_pathway_map(actions, sequences)
positions = classic_layout(pathway_map)
positions = classic_layout(pathway_map, overlapping_lines_spread=0)

y_coordinates_we_want = {
"current": 0,
Expand Down

0 comments on commit a8e410f

Please sign in to comment.