55import pltstyle
66import tkinter as tk
77import re
8+ import numpy as np
9+ from matplotlib .transforms import Bbox
810
911from pathlib import Path
1012from tkinter import filedialog , messagebox
1315from bmg_to_txt import read_bmg_xlsx , extract_concentration_vector
1416
1517
18+ def place_annotation_safely (ax , text , data_x , data_y , line2d = None , marker_size = 12 , margin = 8 , ** annotate_kwargs ):
19+ """
20+ Place annotation box in a location that avoids overlap with legend, data points, and optionally a line.
21+ - data_x, data_y: arrays of data points (for scatter/markers)
22+ - line2d: optional, a matplotlib Line2D object to check for overlap with the line itself
23+ - marker_size: pixel size of marker to avoid (default 12)
24+ - margin: extra pixels to add around annotation box (default 8)
25+ """
26+ renderer = ax .figure .canvas .get_renderer ()
27+ corners = [
28+ (0.01 , 0.99 , {'xycoords' : 'axes fraction' , 'va' : 'top' , 'ha' : 'left' }),
29+ (0.99 , 0.99 , {'xycoords' : 'axes fraction' , 'va' : 'top' , 'ha' : 'right' }),
30+ (0.01 , 0.01 , {'xycoords' : 'axes fraction' , 'va' : 'bottom' , 'ha' : 'left' }),
31+ (0.99 , 0.01 , {'xycoords' : 'axes fraction' , 'va' : 'bottom' , 'ha' : 'right' }),
32+ ]
33+ legend = ax .get_legend ()
34+ legend_bbox = legend .get_window_extent (renderer ) if legend else None
35+ # Data points as bboxes (expanded for marker size)
36+ data_disp = ax .transData .transform (np .column_stack ([data_x , data_y ]))
37+ data_bboxes = [Bbox .from_bounds (x - marker_size , y - marker_size , 2 * marker_size , 2 * marker_size ) for x , y in data_disp ]
38+ # If a line is provided, rasterize it to points and add bboxes
39+ line_bboxes = []
40+ if line2d is not None :
41+ line_x , line_y = line2d .get_data ()
42+ line_disp = ax .transData .transform (np .column_stack ([line_x , line_y ]))
43+ for x , y in line_disp :
44+ line_bboxes .append (Bbox .from_bounds (x - 2 , y - 2 , 4 , 4 ))
45+ best = None
46+ min_overlap = float ('inf' )
47+ for x , y , opts in corners :
48+ ann = ax .annotate (text , (x , y ), bbox = dict (boxstyle = 'round' , fc = 'w' , ec = 'k' , alpha = 0.8 ), annotation_clip = False , ** opts , ** annotate_kwargs )
49+ ax .figure .canvas .draw ()
50+ ann_bbox = ann .get_window_extent (renderer ).expanded (1.05 , 1.1 ).padded (margin )
51+ overlap = 0
52+ if legend_bbox and ann_bbox .overlaps (legend_bbox ):
53+ overlap += 1e6
54+ for db in data_bboxes :
55+ if ann_bbox .overlaps (db ):
56+ overlap += 1
57+ for lb in line_bboxes :
58+ if ann_bbox .overlaps (lb ):
59+ overlap += 1
60+ if overlap == 0 :
61+ return ann
62+ if overlap < min_overlap :
63+ min_overlap = overlap
64+ best = ann
65+ ann .remove ()
66+ return best # fallback: least overlap
67+
68+
69+ def place_annotation_opposite_legend (ax , text , offset_frac = 0.03 , ** annotate_kwargs ):
70+ """
71+ Place annotation box in the corner diagonally opposite to the legend, with a margin from axes.
72+ offset_frac: fraction of axes width/height to offset from the edge (default 0.03 = 3%)
73+ """
74+ legend = ax .get_legend ()
75+ loc = getattr (legend , '_loc' , 'upper right' ) if legend else 'upper right'
76+ loc_map = {
77+ 'upper right' : (offset_frac , offset_frac , {'xycoords' : 'axes fraction' , 'va' : 'bottom' , 'ha' : 'left' }),
78+ 'upper left' : (1 - offset_frac , offset_frac , {'xycoords' : 'axes fraction' , 'va' : 'bottom' , 'ha' : 'right' }),
79+ 'lower left' : (1 - offset_frac , 1 - offset_frac , {'xycoords' : 'axes fraction' , 'va' : 'top' , 'ha' : 'right' }),
80+ 'lower right' : (offset_frac , 1 - offset_frac , {'xycoords' : 'axes fraction' , 'va' : 'top' , 'ha' : 'left' }),
81+ 'best' : (offset_frac , offset_frac , {'xycoords' : 'axes fraction' , 'va' : 'bottom' , 'ha' : 'left' }),
82+ }
83+ x , y , opts = loc_map .get (loc , loc_map ['upper right' ])
84+ return ax .annotate (
85+ text , (x , y ),
86+ bbox = dict (boxstyle = 'round' , fc = 'w' , ec = 'k' , alpha = 0.8 ),
87+ annotation_clip = False ,
88+ ** opts , ** annotate_kwargs
89+ )
90+
91+
1692def plot_all_replica (raw_data_path : str , robot_file_path : str , save_dir : str ):
1793 """this is for ONE excel file"""
1894 raw_data_path , robot_file_path , save_dir = map (Path , (raw_data_path , robot_file_path , save_dir ))
@@ -25,7 +101,7 @@ def plot_all_replica(raw_data_path : str, robot_file_path : str, save_dir : str)
25101 fig , ax = pltstyle .create_plots (plot_title = plot_title )
26102 # ax.boxplot(data, tick_labels=concentration_vector, patch_artist=True)
27103 ax .plot (concentration_vector , data .values .T , label = [f'Replica { i + 1 } ' for i in range (data .shape [0 ])])#
28- ax .legend ()
104+ ax .legend (loc = 'best' )
29105
30106 # save plot of different analytes to separate folders
31107 save_dir_analyte = (save_dir / ' ' .join (raw_data_path .stem .split ('_' )[1 :4 ]))
0 commit comments