matplot patchcollection(s) moving and rotating, all together and each seprately, what is best?
05:06 01 Oct 2026

what is best practice, professional software program to user can move rotate all patchcollection together/ each seprately? to fit patch(s) to ROI get array data from an image? any tiny advice is highly appreciated. below code has 4 frame like patchcollection and 1 center patchcollection. can be moved and rotated

import matplotlib.pyplot as plt
from matplotlib.patches import Circle
from matplotlib.collections import PatchCollection
from matplotlib import transforms
import numpy as np
import time  # For calculating draw and execution speed
import os    # For reading process memory usage

# ---- Global toggle variable ----
toggle_mode = 0  # 0 = single, 1 = all


# ---- Function to create collections ----
def create_circle_collection(center_x, center_y, radius, rows, cols, spacing,
                             color, gid):
    patches = []
    for i in range(rows):
        for j in range(cols):
            x = center_x + j * spacing
            y = center_y + i * spacing
            circle = Circle((x, y), radius)
            patches.append(circle)

            center_dot = Circle((x, y), radius=0.03)
            patches.append(center_dot)

    collection = PatchCollection(patches,
                                 facecolor=color,
                                 edgecolor='black',
                                 alpha=0.6)
    collection.set_gid(gid)
    return collection, patches


# ---- Setup Figure ----
fig, ax = plt.subplots(figsize=(8, 6))
ax.set_xlim(0, 20)
ax.set_ylim(0, 20)
ax.grid(True, linestyle='--', color='gray', alpha=0.7)

collections = []
patches_list = []
rotation_points = {}  # collection -> fixed rotation point
all_rotation_point = None  # for ALL mode
collection_transforms = {}

params = [(7, 7, 0.5, 1, 7, 1.0, 'skyblue', "Group1"),
          (7, 8, 0.5, 5, 1, 1.0, 'lightcoral', "Group2"),
          (8, 8, 0.5, 5, 5, 1.0, 'green', "Group3"),
          (13, 8, 0.5, 5, 1, 1.0, 'lightcoral', "Group4"),
          (7, 13, 0.5, 1, 7, 1.0, 'skyblue', "Group5")]

for cx, cy, r, rows, cols, spacing, color, gid in params:
    coll, patches = create_circle_collection(cx, cy, r, rows, cols, spacing,
                                             color, gid)
    ax.add_collection(coll)
    collections.append(coll)
    patches_list.append(patches)
    collection_transforms[coll] = transforms.Affine2D()
    rotation_points[coll] = [patches[0].center[0], patches[0].center[1]]

first_collection = collections[0]
all_rotation_point = list(rotation_points[first_collection])

# Rotation tracking
collection_angles = {coll: 0 for coll in collections}

# Dragging state
dragging = {'collection': None, 'start_pos': (0, 0)}


def rotate_collection(collection, angle_deg, pivot):
    cx, cy = pivot

    # Apply rotation incrementally to the existing Affine2D transform
    collection_transforms[collection].rotate_deg_around(cx, cy, angle_deg)

    # Update the collection's transform for drawing
    collection.set_transform(collection_transforms[collection] + ax.transData)

    # Track total rotation angle for this collection
    collection_angles[collection] = (collection_angles.get(collection, 0) +
                                     angle_deg) % 360

    print(
        f"Transform Layer Log: Rotated {collection.get_gid()} around pivot ({cx:.2f}, {cy:.2f}) by {collection_angles[collection]:.2f}°"
    )


# ---- Event Handlers ----


def on_press(event):
    if event.inaxes != ax:
        return

    global all_rotation_point

    for coll in collections:
        contains, _ = coll.contains(event)
        if contains:
            if toggle_mode == 1:
                dragging['collection'] = 'ALL'
                dragging['start_pos'] = (event.xdata, event.ydata)
                print(
                    f"---All pressed center: {all_rotation_point[0] + params[0][2]:.1f}, {all_rotation_point[1] - params[0][2]:.1f}"
                )
                return

            dragging['collection'] = coll
            dragging['start_pos'] = (event.xdata, event.ydata)

            idx = collections.index(coll)
            patch = patches_list[idx][0]
            x_new, y_new = collection_transforms[coll].transform(patch.center)
            print(
                f"{coll.get_gid()} Pressed center: {x_new:.1f}, {y_new:.1f} ")


def on_release(event):
    coll = dragging['collection']

    if coll is None:
        return  # nothing being dragged

    if coll == 'ALL':
        print(
            f"---All released center: {all_rotation_point[0]:.1f}, {all_rotation_point[1]:.1f}"
        )
    else:
        idx = collections.index(coll)
        patch = patches_list[idx][0]
        x_new, y_new = collection_transforms[coll].transform(patch.center)
        print(
            f"---Released center of {coll.get_gid()}: {x_new:.1f}, {y_new:.1f}"
        )

    dragging['collection'] = None


def on_motion(event):
    if dragging['collection'] is None or event.inaxes != ax:
        return

    # START PERFORMANCE SPEED TRACKING
    start_time = time.perf_counter()

    dx = event.xdata - dragging['start_pos'][0]
    dy = event.ydata - dragging['start_pos'][1]

    # --- RULE 1: Dragging ALL patch collections ---
    # Update global pivot and all local pivots according to dx, dy
    if dragging['collection'] == 'ALL':
        target_collections = collections
        if all_rotation_point is not None:
            all_rotation_point[0] += dx
            all_rotation_point[1] += dy
        for coll in collections:
            rotation_points[coll][0] += dx
            rotation_points[coll][1] += dy
    else:
        # --- RULE 2: Dragging SINGLE patch collection ---
        # Update only that collection's local pivot.
        # If it is the global pivot owner (first_collection), also update global pivot.
        target_collections = [dragging['collection']]
        active_coll = dragging['collection']
        rotation_points[active_coll][0] += dx
        rotation_points[active_coll][1] += dy
            
        if active_coll == first_collection:
            all_rotation_point[0] += dx
            all_rotation_point[1] += dy


    new_rotation = ""
    for coll in target_collections:
        # Mutate transform matrix layers directly
        collection_transforms[coll].translate(dx, dy)
        coll.set_transform(collection_transforms[coll] + ax.transData)


        x, y = rotation_points[coll]
        new_rotation = f"{new_rotation}{coll.get_gid()} => ({x:.2f}, {y:.2f}). "
    print(new_rotation)

    dragging['start_pos'] = (event.xdata, event.ydata)
    fig.canvas.draw_idle()

    # LOG PERFORMANCE SPEED & MEMORY
    execution_time_ms = (time.perf_counter() - start_time) * 1000
    try:
        import psutil
        process_memory_mb = psutil.Process(os.getpid()).memory_info().rss / (1024 * 1024)
    except ImportError:
        # Fallback if psutil library isn't installed
        process_memory_mb = 0.0

    print(f"   [PERF LOG] Motion Event Process Speed: {execution_time_ms:.2f} ms | App RAM Usage: {process_memory_mb:.1f} MB")


def on_key_press(event):
    global toggle_mode, all_rotation_point, rotation_points
    coll = dragging['collection']

    if event.key in ['ctrl+a', 'ctrl+A']:
        print("on_key_press")
        toggle_mode = 1 - toggle_mode
        print(
            f"Toggle mode: {'ALL GROUPS' if toggle_mode else 'SINGLE GROUP'}")
        return

    if toggle_mode == 1:
        target_collections = collections
    elif coll is not None and coll != 'ALL':
        target_collections = [coll]
    else:
        return

    if event.key in ['ctrl+r', 'alt+r']:
        # START KEY ROTATION PERFORMANCE SPEED TRACKING
        start_time = time.perf_counter()
        
        angle = 10 if event.key == 'ctrl+r' else -10

        if toggle_mode == 1:
            # --- RULE 3: Rotation ALL patch collections ---
            # Update only local pivots (to track first-circle visually), do not touch global pivot.
            for coll in target_collections:
                rotate_collection(coll, angle, all_rotation_point)
                
                # Recompute local pivot post-rotation from the first circle's live center
                first_circle = patches_list[collections.index(coll)][0]
                rotation_points[coll] = list(collection_transforms[coll].transform(first_circle.center))
        else:
            # --- RULE 4: Rotation SINGLE patch collection ---
            # Do not update any pivot. Pivot remains fixed.
            for coll in target_collections:
                rotate_collection(coll, angle, rotation_points[coll])

        fig.canvas.draw_idle()
        
        # LOG ROTATION RENDER SPEED
        execution_time_ms = (time.perf_counter() - start_time) * 1000
        print(f"   [PERF LOG] Rotation Event Render Speed: {execution_time_ms:.2f} ms")


# ---- Performance Monitor for Core Matplotlib Renderer Engine ----
def on_draw_perf_log(event):
    """Prints exactly how long Matplotlib takes to redraw the pixel canvas from scratch."""
    print(f"   [MATPLOTLIB CANVAS DRAW] Full Screen Render Pipeline complete.")

fig.canvas.mpl_connect('draw_event', on_draw_perf_log)

# ---- Connect Handlers & Render Loop ----
fig.canvas.mpl_connect('button_press_event', on_press)
fig.canvas.mpl_connect('button_release_event', on_release)
fig.canvas.mpl_connect('motion_notify_event', on_motion)
fig.canvas.mpl_connect('key_press_event', on_key_press)

plt.show()
best-practices