diff --git a/barphase.py b/barphase.py index 2b48f22..7abe9ec 100644 --- a/barphase.py +++ b/barphase.py @@ -1,5 +1,6 @@ -import matplotlib.pyplot as plt -import numpy as np +import matplotlib.pyplot as plt +import numpy as np +from phase_breaks import draw_phase_breaks # Data for Escape-Maintained Off-Task Behavior (Frequency per 10-minute interval) # Lower frequency is the desired outcome here. @@ -68,9 +69,9 @@ ax.set_ylabel("Frequency of Escape-Maintained Off-Task Behavior") ax.set_title("Impact of Negative Reinforcement on Off-Task Behavior", fontsize=14, pad=20) -# Add vertical dotted lines for phase changes -for boundary in phase_boundaries: - ax.axvline(x=boundary + 0.5, color='black', linestyle=':', linewidth=1.5) +# Add APA/JABA-style zig-zag phase breaks. +# Change styles to "solid" if the publication target requires solid separators. +draw_phase_breaks(ax, phase_boundaries, styles="dotted", zigzag=True) # Add phase labels above the graph current_x_pos = 0.5 # Start position for the first label (center of first bar) diff --git a/linephase.py b/linephase.py index e107db9..7e3b50c 100644 --- a/linephase.py +++ b/linephase.py @@ -1,5 +1,6 @@ -import matplotlib.pyplot as plt -import numpy as np +import matplotlib.pyplot as plt +import numpy as np +from phase_breaks import draw_phase_breaks # Data for Escape-Maintained Off-Task Behavior (Frequency per 10-minute interval) # Lower frequency is the desired outcome here. @@ -72,10 +73,10 @@ ax.set_ylabel("Frequency of Escape-Maintained Off-Task Behavior") ax.set_title("Impact of Negative Reinforcement on Off-Task Behavior", fontsize=14, pad=20) -# Add vertical dotted lines for phase changes -# These lines should be placed at the boundary *between* the last point of one phase and the first point of the next -for boundary in phase_boundaries: - ax.axvline(x=boundary + 0.5, color='black', linestyle=':', linewidth=1.5) +# Add APA/JABA-style zig-zag phase breaks. +# Boundaries are placed between the last point of one phase and the first point +# of the next. Use styles="solid" when a manuscript requires solid breaks. +draw_phase_breaks(ax, phase_boundaries, styles="dotted", zigzag=True) # Add phase labels above the graph # Calculate x positions for labels within each phase diff --git a/loglin.py b/loglin.py index 2804ae1..2f3993f 100644 --- a/loglin.py +++ b/loglin.py @@ -1,5 +1,6 @@ -import matplotlib.pyplot as plt -import numpy as np +import matplotlib.pyplot as plt +import numpy as np +from phase_breaks import draw_phase_breaks # Data for Escape-Maintained Off-Task Behavior (Frequency per 10-minute interval) # Lower frequency is the desired outcome here. @@ -77,9 +78,9 @@ ax.set_ylabel("Frequency of Escape-Maintained Off-Task Behavior (Log Scale)") ax.set_title("Impact of Negative Reinforcement on Off-Task Behavior Across Phases (Log-Linear)", fontsize=14, pad=20) -# Add vertical dotted lines for phase changes -for boundary in phase_boundaries: - ax.axvline(x=boundary + 0.5, color='black', linestyle=':', linewidth=1.5) +# Add APA/JABA-style zig-zag phase breaks. +# Change styles to "solid" if the publication target requires solid separators. +draw_phase_breaks(ax, phase_boundaries, styles="dotted", zigzag=True) # Add phase labels above the graph current_x_pos_start = 1 # Starting x for label calculation based on 1-indexed sessions diff --git a/phase_breaks.py b/phase_breaks.py new file mode 100644 index 0000000..78bd3af --- /dev/null +++ b/phase_breaks.py @@ -0,0 +1,107 @@ +"""Helpers for APA/JABA-style phase-change separators. + +The plotting scripts in this repo already draw phase boundaries with +``ax.axvline``. This module keeps that simple path, but adds a reusable +zig-zag separator for papers that require phase breaks to be shown as +asymptote-like vertical breaks instead of plain straight lines. +""" + +from __future__ import annotations + +from collections.abc import Iterable, Sequence + +import numpy as np +from matplotlib.axes import Axes +from matplotlib.transforms import blended_transform_factory + + +def draw_phase_breaks( + ax: Axes, + phase_boundaries: Iterable[float], + *, + styles: Sequence[str] | str = "dotted", + zigzag: bool = True, + color: str = "black", + linewidth: float = 1.5, + zigzag_width: float = 0.18, + zigzag_steps: int = 18, +) -> None: + """Draw publication-friendly phase-change separators. + + Parameters + ---------- + ax: + Matplotlib axes to draw on. + phase_boundaries: + Session indices after which a new phase starts. A boundary of ``24`` + is drawn at ``24.5``, between sessions 24 and 25. + styles: + Either one style for every boundary or one style per boundary. + Accepted values are ``"dotted"``, ``"solid"``, ``"dashed"``, and + Matplotlib linestyles such as ``":"`` or ``"--"``. + zigzag: + When true, draw a vertical zig-zag phase separator. When false, draw + a straight line with the requested style. + color: + Separator color. + linewidth: + Separator stroke width. + zigzag_width: + Horizontal zig-zag amplitude in x-axis data units. + zigzag_steps: + Number of line segments used from the bottom to the top of the plot. + """ + + boundaries = list(phase_boundaries) + if isinstance(styles, str): + resolved_styles = [styles] * len(boundaries) + else: + resolved_styles = list(styles) + if len(resolved_styles) != len(boundaries): + raise ValueError("styles must be one value or match phase_boundaries length") + + transform = blended_transform_factory(ax.transData, ax.transAxes) + + for boundary, style in zip(boundaries, resolved_styles): + x_position = boundary + 0.5 + linestyle = _resolve_linestyle(style) + + if not zigzag: + ax.axvline( + x=x_position, + color=color, + linestyle=linestyle, + linewidth=linewidth, + ) + continue + + y_values = np.linspace(0, 1, zigzag_steps + 1) + x_values = np.array( + [ + x_position + (zigzag_width if index % 2 else -zigzag_width) + for index in range(len(y_values)) + ] + ) + + ax.plot( + x_values, + y_values, + color=color, + linestyle=linestyle, + linewidth=linewidth, + transform=transform, + clip_on=False, + solid_capstyle="butt", + ) + + +def _resolve_linestyle(style: str) -> str: + normalized = style.strip().lower() + aliases = { + "dotted": ":", + "dot": ":", + "solid": "-", + "dashed": "--", + "dash": "--", + } + return aliases.get(normalized, style)