@@ -40,21 +40,20 @@ class Circos:
40
40
41
41
def __init__ (
42
42
self ,
43
- sectors : Mapping [str , int | float ],
43
+ sectors : Mapping [str , int | float | tuple [ float , float ] ],
44
44
start : float = 0 ,
45
45
end : float = 360 ,
46
46
* ,
47
47
space : float | list [float ] = 0 ,
48
48
endspace : bool = True ,
49
- sector2start_pos : Mapping [str , int | float ] | None = None ,
50
49
sector2clockwise : dict [str , bool ] | None = None ,
51
50
show_axis_for_debug : bool = False ,
52
51
):
53
52
"""
54
53
Parameters
55
54
----------
56
- sectors : Mapping[str, int | float]
57
- Sector name & size dict
55
+ sectors : Mapping[str, int | float | tuple[float, float] ]
56
+ Sector name & size (or range) dict
58
57
start : float, optional
59
58
Plot start degree (`-360 <= start < end <= 360`)
60
59
end : float, optional
@@ -63,14 +62,11 @@ def __init__(
63
62
Space degree(s) between sector
64
63
endspace : bool, optional
65
64
If True, insert space after the end sector
66
- sector2start_pos : Mapping[str, int | float] | None, optional
67
- Sector name & start position dict. By default, `start_pos=0`.
68
65
sector2clockwise : dict[str, bool] | None, optional
69
66
Sector name & clockwise bool dict. By default, `clockwise=True`.
70
67
show_axis_for_debug : bool, optional
71
68
Show axis for position check debugging (Developer option)
72
69
"""
73
- sector2start_pos = {} if sector2start_pos is None else sector2start_pos
74
70
sector2clockwise = {} if sector2clockwise is None else sector2clockwise
75
71
76
72
# Check start-end degree range
@@ -100,19 +96,21 @@ def __init__(
100
96
"""
101
97
)[1 :- 1 ]
102
98
raise ValueError (err_msg )
103
- sector_total_size = sum (sectors .values ())
99
+
100
+ sector2range = self ._to_sector2range (sectors )
101
+ sector_total_size = sum ([max (r ) - min (r ) for r in sector2range .values ()])
104
102
105
103
rad_pos = math .radians (start )
106
104
self ._sectors : list [Sector ] = []
107
- for idx , (sector_name , sector_size ) in enumerate (sectors .items ()):
105
+ for idx , (sector_name , sector_range ) in enumerate (sector2range .items ()):
106
+ sector_size = max (sector_range ) - min (sector_range )
108
107
sector_size_ratio = sector_size / sector_total_size
109
108
deg_size = whole_deg_size_without_space * sector_size_ratio
110
109
rad_size = math .radians (deg_size )
111
110
rad_lim = (rad_pos , rad_pos + rad_size )
112
111
rad_pos += rad_size + math .radians (space_list [idx ])
113
- start_pos = sector2start_pos .get (sector_name , 0 )
114
112
clockwise = sector2clockwise .get (sector_name , True )
115
- sector = Sector (sector_name , sector_size , rad_lim , start_pos , clockwise )
113
+ sector = Sector (sector_name , sector_range , rad_lim , clockwise )
116
114
self ._sectors .append (sector )
117
115
118
116
self ._deg_lim = (start , end )
@@ -180,6 +178,7 @@ def radar_chart(
180
178
table : str | Path | pd .DataFrame | RadarTable ,
181
179
* ,
182
180
r_lim : tuple [float , float ] = (0 , 100 ),
181
+ vmin : float = 0 ,
183
182
vmax : float = 100 ,
184
183
fill : bool = True ,
185
184
marker_size : int = 0 ,
@@ -203,6 +202,8 @@ def radar_chart(
203
202
Table file or Table dataframe or RadarTable instance
204
203
r_lim : tuple[float, float], optional
205
204
Radar chart radius limit region (0 - 100)
205
+ vmin : float, optional
206
+ Min value
206
207
vmax : float, optional
207
208
Max value
208
209
fill : bool, optional
@@ -244,6 +245,10 @@ def radar_chart(
244
245
circos : Circos
245
246
Circos instance initialized for radar chart
246
247
"""
248
+ if not vmin < vmax :
249
+ raise ValueError (f"vmax must be larger than vmin ({ vmin = } , { vmax = } )" )
250
+ size = vmax - vmin
251
+
247
252
# Setup default properties
248
253
grid_line_kws = {} if grid_line_kws is None else deepcopy (grid_line_kws )
249
254
for k , v in dict (color = "grey" , ls = "dashed" , lw = 0.5 ).items ():
@@ -269,11 +274,12 @@ def radar_chart(
269
274
if not 0 < grid_interval_ratio <= 1.0 :
270
275
raise ValueError (f"{ grid_interval_ratio = } is invalid." )
271
276
# Plot horizontal grid line & label
272
- stop , step = vmax + (vmax / 1000 ), vmax * grid_interval_ratio
273
- for v in np .arange (0 , stop , step ):
274
- track .line (x , [v ] * len (x ), vmax = vmax , arc = circular , ** grid_line_kws )
277
+ stop , step = vmax + (size / 1000 ), size * grid_interval_ratio
278
+ for v in np .arange (vmin , stop , step ):
279
+ y = [v ] * len (x )
280
+ track .line (x , y , vmin = vmin , vmax = vmax , arc = circular , ** grid_line_kws )
275
281
if show_grid_label :
276
- r = track ._y_to_r (v , 0 , vmax )
282
+ r = track ._y_to_r (v , vmin , vmax )
277
283
# Format grid label
278
284
if grid_label_formatter :
279
285
text = grid_label_formatter (v )
@@ -283,7 +289,7 @@ def radar_chart(
283
289
track .text (text , 0 , r , ** grid_label_kws )
284
290
# Plot vertical grid line
285
291
for p in x [:- 1 ]:
286
- track .line ([p , p ], [0 , vmax ], vmax = vmax , ** grid_line_kws )
292
+ track .line ([p , p ], [vmin , vmax ], vmin = vmin , vmax = vmax , ** grid_line_kws )
287
293
288
294
# Plot radar charts
289
295
if isinstance (cmap , str ):
@@ -296,15 +302,16 @@ def radar_chart(
296
302
line_kws = line_kws_handler (row_name ) if line_kws_handler else {}
297
303
line_kws .setdefault ("lw" , 1.0 )
298
304
line_kws .setdefault ("label" , row_name )
299
- track .line (x , y , vmax = vmax , arc = False , color = color , ** line_kws )
305
+ track .line (x , y , vmin = vmin , vmax = vmax , arc = False , color = color , ** line_kws )
300
306
if marker_size > 0 :
301
307
marker_kws = marker_kws_handler (row_name ) if marker_kws_handler else {}
302
308
marker_kws .setdefault ("marker" , "o" )
303
309
marker_kws .setdefault ("zorder" , 2 )
304
310
marker_kws .update (s = marker_size ** 2 )
305
- track .scatter (x , y , vmax = vmax , color = color , ** marker_kws )
311
+ track .scatter (x , y , vmin = vmin , vmax = vmax , color = color , ** marker_kws )
306
312
if fill :
307
- track .fill_between (x , y , vmax = vmax , arc = False , color = color , alpha = 0.5 )
313
+ fill_kws = dict (arc = False , color = color , alpha = 0.5 )
314
+ track .fill_between (x , y , y2 = vmin , vmin = vmin , vmax = vmax , ** fill_kws ) # type:ignore
308
315
309
316
# Plot column names
310
317
for idx , col_name in enumerate (radar_table .col_names ):
@@ -577,15 +584,13 @@ def initialize_from_bed(
577
584
Circos instance initialized from BED file
578
585
"""
579
586
records = Bed (bed_file ).records
580
- sectors = {rec .chr : rec .size for rec in records }
581
- sector2start_pos = {rec .chr : rec .start for rec in records }
587
+ sectors = {rec .chr : (rec .start , rec .end ) for rec in records }
582
588
return Circos (
583
589
sectors ,
584
590
start ,
585
591
end ,
586
592
space = space ,
587
593
endspace = endspace ,
588
- sector2start_pos = sector2start_pos ,
589
594
sector2clockwise = sector2clockwise ,
590
595
)
591
596
@@ -1098,6 +1103,23 @@ def _check_degree_range(self, start: float, end: float) -> None:
1098
1103
err_msg = f"'end - start' must be less than { max_deg } ({ start = } , { end = } )"
1099
1104
raise ValueError (err_msg )
1100
1105
1106
+ def _to_sector2range (
1107
+ self ,
1108
+ sectors : Mapping [str , int | float | tuple [float , float ]],
1109
+ ) -> dict [str , tuple [float , float ]]:
1110
+ """Convert sectors to sector2range"""
1111
+ sector2range : dict [str , tuple [float , float ]] = {}
1112
+ for name , value in sectors .items ():
1113
+ if isinstance (value , (tuple , list )):
1114
+ sector_start , sector_end = value
1115
+ if not sector_start < sector_end :
1116
+ err_msg = f"{ sector_end = } must be larger than { sector_start = } ."
1117
+ raise ValueError (err_msg )
1118
+ sector2range [name ] = (sector_start , sector_end )
1119
+ else :
1120
+ sector2range [name ] = (0 , value )
1121
+ return sector2range
1122
+
1101
1123
def _initialize_figure (
1102
1124
self ,
1103
1125
figsize : tuple [float , float ] = (8 , 8 ),
0 commit comments