trackplot 0.6.3__tar.gz → 0.6.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. {trackplot-0.6.3/trackplot.egg-info → trackplot-0.6.5}/PKG-INFO +1 -1
  2. {trackplot-0.6.3 → trackplot-0.6.5}/pyproject.toml +4 -1
  3. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot/cli.py +28 -0
  4. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot/plot/coord.py +40 -23
  5. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot/plot/core.py +49 -17
  6. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot/plot/limits.py +20 -16
  7. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot/plot/render.py +110 -20
  8. {trackplot-0.6.3 → trackplot-0.6.5/trackplot.egg-info}/PKG-INFO +1 -1
  9. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot.egg-info/SOURCES.txt +1 -0
  10. trackplot-0.6.5/trackplot.egg-info/entry_points.txt +2 -0
  11. {trackplot-0.6.3 → trackplot-0.6.5}/LICENSE +0 -0
  12. {trackplot-0.6.3 → trackplot-0.6.5}/README.md +0 -0
  13. {trackplot-0.6.3 → trackplot-0.6.5}/setup.cfg +0 -0
  14. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot/__init__.py +0 -0
  15. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot/anno/AxLabel.py +0 -0
  16. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot/anno/__init__.py +0 -0
  17. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot/anno/theme.py +0 -0
  18. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot/base/CoordinateMap.py +0 -0
  19. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot/base/GenomicLoci.py +0 -0
  20. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot/base/Junction.py +0 -0
  21. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot/base/Protein.py +0 -0
  22. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot/base/ReadDepth.py +0 -0
  23. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot/base/Readder.py +0 -0
  24. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot/base/Stroke.py +0 -0
  25. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot/base/Transcript.py +0 -0
  26. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot/base/__init__.py +0 -0
  27. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot/base/pyUniprot.py +0 -0
  28. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot/conf/DomainSetting.py +0 -0
  29. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot/conf/__init__.py +0 -0
  30. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot/conf/config.py +0 -0
  31. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot/conf/drawing.py +0 -0
  32. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot/conf/ui.py +0 -0
  33. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot/file/ATAC.py +0 -0
  34. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot/file/Annotation.py +0 -0
  35. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot/file/Bam.py +0 -0
  36. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot/file/BedGraph.py +0 -0
  37. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot/file/Bigwig.py +0 -0
  38. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot/file/Depth.py +0 -0
  39. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot/file/Fasta.py +0 -0
  40. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot/file/File.py +0 -0
  41. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot/file/HiCMatrixTrack.py +0 -0
  42. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot/file/Junction.py +0 -0
  43. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot/file/Motif.py +0 -0
  44. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot/file/ReadSegments.py +0 -0
  45. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot/file/__init__.py +0 -0
  46. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot/plot/__init__.py +0 -0
  47. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot/plot/info.py +0 -0
  48. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot/plot/utils.py +0 -0
  49. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot/plot_func.py +0 -0
  50. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot/server.py +0 -0
  51. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot.egg-info/dependency_links.txt +0 -0
  52. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot.egg-info/requires.txt +0 -0
  53. {trackplot-0.6.3 → trackplot-0.6.5}/trackplot.egg-info/top_level.txt +0 -0
  54. {trackplot-0.6.3 → trackplot-0.6.5}/ui/assets/Home-B1EPE6Lq.js +0 -0
  55. {trackplot-0.6.3 → trackplot-0.6.5}/ui/assets/Home-DuHqFHEh.css +0 -0
  56. {trackplot-0.6.3 → trackplot-0.6.5}/ui/assets/Plot-CkylK0fA.css +0 -0
  57. {trackplot-0.6.3 → trackplot-0.6.5}/ui/assets/Plot-jZAsK7Tq.js +0 -0
  58. {trackplot-0.6.3 → trackplot-0.6.5}/ui/assets/_plugin-vue_export-helper-DLf8tVwz.css +0 -0
  59. {trackplot-0.6.3 → trackplot-0.6.5}/ui/assets/_plugin-vue_export-helper-IgiCOkTJ.js +0 -0
  60. {trackplot-0.6.3 → trackplot-0.6.5}/ui/assets/css-BDwM7dEv.css +0 -0
  61. {trackplot-0.6.3 → trackplot-0.6.5}/ui/assets/css-CKLNJEB6.js +0 -0
  62. {trackplot-0.6.3 → trackplot-0.6.5}/ui/assets/index-De-rfTdP.js +0 -0
  63. {trackplot-0.6.3 → trackplot-0.6.5}/ui/assets/index-Dlq_kUQH.css +0 -0
  64. {trackplot-0.6.3 → trackplot-0.6.5}/ui/index.html +0 -0
  65. {trackplot-0.6.3 → trackplot-0.6.5}/ui/vite.svg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trackplot
3
- Version: 0.6.3
3
+ Version: 0.6.5
4
4
  Summary: The trackplot is a tool for visualizing various next-generation sequencing (NGS) data, including DNA-seq, RNA-seq, single-cell RNA-seq and full-length sequencing datasets. https://sashimi.readthedocs.io/
5
5
  Author-email: ygidtu <ygidtu@gmail.com>
6
6
  License-Expression: BSD-3-Clause
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "trackplot"
3
- version = "0.6.3"
3
+ version = "0.6.5"
4
4
  description = "The trackplot is a tool for visualizing various next-generation sequencing (NGS) data, including DNA-seq, RNA-seq, single-cell RNA-seq and full-length sequencing datasets. https://sashimi.readthedocs.io/"
5
5
  authors = [
6
6
  { name = "ygidtu", email = "ygidtu@gmail.com" }
@@ -29,6 +29,9 @@ dependencies = [
29
29
  ]
30
30
 
31
31
 
32
+ [project.scripts]
33
+ trackplot = "trackplot.cli:main" # 假设入口在 trackplot/cli.py 的 main 函数
34
+
32
35
  [build-system]
33
36
  requires = ["setuptools>=65.0", "wheel"]
34
37
  build-backend = "setuptools.build_meta"
@@ -1214,6 +1214,30 @@ def _add_interval(p, f):
1214
1214
  @optgroup.option(
1215
1215
  "--title", type=click.STRING, default=None, help="Title", show_default=True
1216
1216
  )
1217
+ @optgroup.option(
1218
+ "--no-title",
1219
+ type=click.BOOL,
1220
+ is_flag=True,
1221
+ default=False,
1222
+ help="Disable the plot title entirely",
1223
+ show_default=True,
1224
+ )
1225
+ @optgroup.option(
1226
+ "--no-region-text",
1227
+ type=click.BOOL,
1228
+ is_flag=True,
1229
+ default=False,
1230
+ help="Disable the --event region text on the x-axis ruler",
1231
+ show_default=True,
1232
+ )
1233
+ @optgroup.option(
1234
+ "--junctions-on-top",
1235
+ type=click.BOOL,
1236
+ is_flag=True,
1237
+ default=False,
1238
+ help="Force all junction arcs to be drawn above the density plot (only effective when --density-by-strand is not set)",
1239
+ show_default=True,
1240
+ )
1217
1241
  @optgroup.option(
1218
1242
  "--font", type=click.STRING, default=None, help="Fonts", show_default=True
1219
1243
  )
@@ -1362,6 +1386,10 @@ def main(**kwargs):
1362
1386
  normalize_format=kwargs.get("normalize_format"),
1363
1387
  fill_step=kwargs.get("fill_step", "post"),
1364
1388
  smooth_bin=kwargs["smooth_bin"],
1389
+ title=kwargs["title"],
1390
+ no_title=kwargs["no_title"],
1391
+ no_region_text=kwargs["no_region_text"],
1392
+ junctions_on_top=kwargs["junctions_on_top"],
1365
1393
  )
1366
1394
 
1367
1395
 
@@ -40,31 +40,45 @@ def init_graph_coords(
40
40
 
41
41
  if exons:
42
42
  if intron_scale <= 1:
43
+ # 填充首个外显子前的内含子区域
43
44
  for i in range(0, exons[0][0] - region.start):
44
45
  graph_coords[i] = (i - 0) * intron_scale
45
46
  exons = __merge_exons__(exons)
46
47
  for i in range(0, len(exons)):
47
48
  exon = exons[i]
49
+
50
+ # 外显子间内含子(从上一个外显子结束的下一碱基开始,避免重叠)
48
51
  if i > 0:
49
- intron = [exons[i - 1][1], exons[i][0]]
50
- for j in range(intron[0], intron[1]):
52
+ intron_start = exons[i - 1][1] + 1
53
+ intron_end = exons[i][0]
54
+ for j in range(intron_start, intron_end):
51
55
  if j >= region.start:
52
56
  graph_coords[j - region.start] = (
53
- graph_coords[intron[0] - region.start - 1]
54
- + (j - intron[0] + 1) * intron_scale
57
+ graph_coords[intron_start - region.start - 1]
58
+ + (j - intron_start + 1) * intron_scale
55
59
  )
60
+
61
+ # 填充外显子区域
56
62
  for j in range(exon[0], exon[1] + 1):
57
63
  if j >= region.start:
58
- graph_coords[j - region.start] = (
59
- graph_coords[exon[0] - region.start - 1]
60
- + (j - exon[0] + 1) * exon_scale
61
- )
62
- intron = [exons[-1][-1], region.end]
63
- for i in range(intron[0], intron[1]):
64
+ base_idx = exon[0] - region.start - 1
65
+ if base_idx < 0:
66
+ # 外显子起始位置在区域起点之前或等于起点,从 0 开始
67
+ graph_coords[j - region.start] = (j - region.start) * exon_scale
68
+ else:
69
+ graph_coords[j - region.start] = (
70
+ graph_coords[base_idx]
71
+ + (j - exon[0] + 1) * exon_scale
72
+ )
73
+
74
+ # 填充最后一个外显子之后的内含子(从下一碱基开始,到 region.end 结束)
75
+ intron_start = exons[-1][-1] + 1
76
+ intron_end = region.end + 1 # +1 确保包含 region.end
77
+ for i in range(intron_start, intron_end):
64
78
  if i >= region.start:
65
79
  graph_coords[i - region.start] = (
66
- graph_coords[intron[0] - region.start - 1]
67
- + (i - intron[0] + 1) * intron_scale
80
+ graph_coords[intron_start - region.start - 1]
81
+ + (i - intron_start + 1) * intron_scale
68
82
  )
69
83
  else:
70
84
  exons = __merge_exons__(exons)
@@ -85,14 +99,14 @@ def init_graph_coords(
85
99
  for i in range(exons[0][0]):
86
100
  steps[i] = step
87
101
  for e in range(1, len(exons)):
88
- interval = exons[e][0] - exons[e - 1][1] - 2
102
+ interval = exons[e][0] - exons[e - 1][1] - 1
89
103
  step = intron_scale / interval
90
- for i in range(exons[e - 1][1], exons[e][0]):
104
+ for i in range(exons[e - 1][1] + 1, exons[e][0]):
91
105
  steps[i] = step
92
106
 
93
107
  if last_interval := len(region) - exons[-1][1] - 1:
94
108
  step = intron_scale / last_interval
95
- for i in range(exons[1][1] + 1, len(region)):
109
+ for i in range(exons[-1][1] + 1, len(region)):
96
110
  steps[i] = step
97
111
  graph_coords = list(map(int, itertools.accumulate(steps)))
98
112
  else:
@@ -134,14 +148,17 @@ def set_x_ticks(
134
148
  x_label = f"{x_label}, y axis is {log_trans} transformed"
135
149
 
136
150
  ax.hlines(y=0, xmin=0, xmax=max(graph_coords), color="black", lw=1)
137
- ax.text(
138
- x=graph_coords[len(graph_coords) // 2],
139
- y=-2.8,
140
- s=x_label,
141
- fontsize=font_size,
142
- ha="center",
143
- va="top",
144
- )
151
+
152
+ no_region_text = kwargs.pop("no_region_text", False)
153
+ if not no_region_text:
154
+ ax.text(
155
+ x=graph_coords[len(graph_coords) // 2],
156
+ y=-2.8,
157
+ s=x_label,
158
+ fontsize=font_size,
159
+ ha="center",
160
+ va="top",
161
+ )
145
162
 
146
163
  bk = 1
147
164
  if not sequence and nx_ticks > 1:
@@ -866,20 +866,25 @@ class Plot(object):
866
866
  return default_y
867
867
 
868
868
  def _precompute_plot_y_limits(
869
- self, p, max_used_y_dict, min_used_y_dict, same_y_by_groups, default_y, **kwargs
869
+ self, p, max_used_y_dict, min_used_y_dict, base_max_dict, same_y_by_groups, default_y, **kwargs
870
870
  ):
871
- """Compute and store y-limits for all objects in a plot."""
871
+ """Compute and store y-limits for all objects in a plot.
872
+
873
+ Also collects base_max (raw data maximum) per object path for use as
874
+ a global arc-height reference in --same-y mode.
875
+ """
872
876
  if p.type not in ("density", "site-plot", "line"):
873
877
  return
874
878
 
875
879
  for obj in p.obj:
876
- _max, _min = precompute_y_limits(
880
+ _max, _min, _base = precompute_y_limits(
877
881
  obj,
878
882
  data=obj.data,
879
883
  region=self.region,
880
884
  graph_coords=self.graph_coords,
881
885
  **kwargs,
882
886
  )
887
+ base_max_dict[obj.path] = max(_base, base_max_dict.get(obj.path, 0))
883
888
 
884
889
  if obj.label in same_y_by_groups:
885
890
  key = same_y_by_groups[obj.label]
@@ -890,10 +895,11 @@ class Plot(object):
890
895
  if obj.data.minus is None
891
896
  else min_used_y_dict.get(obj.path, 0),
892
897
  )
898
+ base_max_dict[key] = max(_base, base_max_dict.get(key, 0))
893
899
 
894
900
  if isinstance(obj.data, dict):
895
901
  for key, readDepth in obj.data.items():
896
- _max, _min = precompute_y_limits(
902
+ _max, _min, _base = precompute_y_limits(
897
903
  obj,
898
904
  data=readDepth,
899
905
  graph_coords=self.graph_coords,
@@ -907,6 +913,7 @@ class Plot(object):
907
913
  if readDepth.minus is None
908
914
  else min_used_y_dict.get(obj.path, 0),
909
915
  )
916
+ base_max_dict[key] = max(_base, base_max_dict.get(key, 0))
910
917
 
911
918
  continue
912
919
 
@@ -928,21 +935,29 @@ class Plot(object):
928
935
  min_used_y_dict[key] = min_used_y_dict[obj.path]
929
936
 
930
937
  def _resolve_plot_y_limits(
931
- self, p, max_used_y_val, min_used_y_val, same_y_by_groups, default_y, **kwargs
938
+ self, p, max_used_y_val, min_used_y_val, base_max_val, same_y_by_groups, default_y, **kwargs
932
939
  ):
933
- """Determine y-limits for a single plot based on same-y / default-y settings."""
940
+ """Determine y-limits for a single plot based on same-y / default-y settings.
941
+
942
+ Returns (max_y, min_y, global_base_max).
943
+ global_base_max is the max of all base_max values (for --same-y mode),
944
+ used as the arc height reference so all panels share consistent arc heights.
945
+ """
934
946
  same_y_sc = kwargs.get("same_y_sc")
935
947
  same_y = kwargs.get("same_y")
936
948
 
937
949
  if same_y_sc and p.obj[0].is_single_cell:
938
- return max_used_y_val.get(p.obj[0].path), min_used_y_val.get(p.obj[0].path)
950
+ _bm = base_max_val.get(p.obj[0].path) if base_max_val else None
951
+ return max_used_y_val.get(p.obj[0].path), min_used_y_val.get(p.obj[0].path), _bm
939
952
 
940
953
  if same_y_by_groups and p.obj[0].label in same_y_by_groups:
941
954
  key = same_y_by_groups[p.obj[0].label]
942
- return max_used_y_val.get(key), min_used_y_val.get(key)
955
+ _bm = base_max_val.get(key) if base_max_val else None
956
+ return max_used_y_val.get(key), min_used_y_val.get(key), _bm
943
957
 
944
958
  if same_y and max_used_y_val:
945
- return max(max_used_y_val.values()), min(min_used_y_val.values())
959
+ _bm = max(base_max_val.values()) if base_max_val else None
960
+ return max(max_used_y_val.values()), min(min_used_y_val.values()), _bm
946
961
 
947
962
  if default_y and p.type in ("density", "site-plot", "line"):
948
963
  for obj in p.obj:
@@ -956,8 +971,8 @@ class Plot(object):
956
971
  if same_y_by_groups and obj.label in same_y_by_groups:
957
972
  max_y = max_used_y_val.get(obj.path, max_y)
958
973
  min_y = min_used_y_val.get(obj.path, min_y)
959
- return max_y, min_y
960
- return None, None
974
+ return max_y, min_y, None
975
+ return None, None, None
961
976
 
962
977
  # ------------------------------------------------------------------
963
978
  # Main plot method
@@ -986,6 +1001,11 @@ class Plot(object):
986
1001
 
987
1002
  igv_height_scale = kwargs.get("igv_height_scale")
988
1003
 
1004
+ # Extract / pop custom params before they flow into downstream **kwargs
1005
+ title = kwargs.pop("title", None)
1006
+ no_title = kwargs.pop("no_title", False)
1007
+ junctions_on_top = kwargs.pop("junctions_on_top", False)
1008
+
989
1009
  # ====== Phase 1: Load data ======
990
1010
  self._load_plot_data(n_jobs, *args, **kwargs)
991
1011
 
@@ -1010,7 +1030,7 @@ class Plot(object):
1010
1030
  )
1011
1031
 
1012
1032
  # ====== Phase 4: Precompute global y-limits ======
1013
- max_used_y_val, min_used_y_val = {}, {}
1033
+ max_used_y_val, min_used_y_val, base_max_val = {}, {}, {}
1014
1034
  same_y_by_groups = {}
1015
1035
  default_y = self._load_default_y(kwargs.get("y_limit"))
1016
1036
 
@@ -1034,6 +1054,7 @@ class Plot(object):
1034
1054
  p,
1035
1055
  max_used_y_val,
1036
1056
  min_used_y_val,
1057
+ base_max_val,
1037
1058
  same_y_by_groups,
1038
1059
  default_y,
1039
1060
  **kwargs,
@@ -1048,25 +1069,30 @@ class Plot(object):
1048
1069
  else:
1049
1070
  ax_var = plt.subplot(gs[curr_idx, 0])
1050
1071
 
1051
- if curr_idx == 0:
1052
- ax_var.set_title(str(self.region), loc="left")
1072
+ if curr_idx == 0 and not no_title:
1073
+ ax_var.set_title(title or str(self.region), loc="left")
1053
1074
 
1054
- max_y_val_, min_y_val_ = self._resolve_plot_y_limits(
1075
+ max_y_val_, min_y_val_, base_max_ = self._resolve_plot_y_limits(
1055
1076
  p,
1056
1077
  max_used_y_val,
1057
1078
  min_used_y_val,
1079
+ base_max_val,
1058
1080
  same_y_by_groups,
1059
1081
  default_y,
1060
1082
  same_y_sc=kwargs.get("same_y_sc"),
1061
1083
  same_y=kwargs.get("same_y"),
1062
1084
  )
1063
-
1085
+
1064
1086
  if max_y_val_ is not None:
1065
1087
  max_y_val_ *= 1.1
1066
-
1088
+
1067
1089
  if min_y_val_ is not None:
1068
1090
  min_y_val_ *= 1.1
1069
1091
 
1092
+ # For --same-y mode: use global base_max as arc height reference
1093
+ # so all panels draw junctions at a consistent scale.
1094
+ global_arc_ref = base_max_
1095
+
1070
1096
  logger.info(
1071
1097
  f"plotting {p.type} at idx: {curr_idx} with height_ratio: {height_ratio[curr_idx]}"
1072
1098
  )
@@ -1088,6 +1114,8 @@ class Plot(object):
1088
1114
  distance_between_label_axis=distance_between_label_axis,
1089
1115
  raster=raster,
1090
1116
  fill_step=fill_step,
1117
+ junctions_on_top=junctions_on_top,
1118
+ global_arc_ref=global_arc_ref,
1091
1119
  **temp_params,
1092
1120
  )
1093
1121
  curr_idx += 1
@@ -1103,6 +1131,8 @@ class Plot(object):
1103
1131
  distance_between_label_axis=distance_between_label_axis,
1104
1132
  raster=raster,
1105
1133
  fill_step=fill_step,
1134
+ junctions_on_top=junctions_on_top,
1135
+ global_arc_ref=global_arc_ref,
1106
1136
  **self.params.get(p, {}),
1107
1137
  )
1108
1138
  elif p.type == "hic":
@@ -1123,6 +1153,8 @@ class Plot(object):
1123
1153
  min_used_y_val=min_y_val_,
1124
1154
  distance_between_label_axis=distance_between_label_axis,
1125
1155
  raster=raster,
1156
+ junctions_on_top=junctions_on_top,
1157
+ global_arc_ref=global_arc_ref,
1126
1158
  **self.params.get(p, {}),
1127
1159
  )
1128
1160
  curr_idx += 1
@@ -26,14 +26,17 @@ def _compute_y_limits(
26
26
  density_by_strand: bool = False,
27
27
  fill_step: str = "post",
28
28
  show_mean_jxn_number: bool = False,
29
- ) -> Tuple[float, float]:
29
+ junctions_on_top: bool = False,
30
+ ) -> Tuple[float, float, float]:
30
31
  """
31
32
  Compute y-axis limits from ReadDepth data, including junction arc extents.
32
33
 
33
34
  This is the shared core used by both precompute_y_limits() and plot_density()
34
35
  to avoid code duplication.
35
36
 
36
- Returns (max_used_y_val, min_used_y_val).
37
+ Returns (max_used_y_val, min_used_y_val, base_max).
38
+ base_max is the raw data maximum before junction arc expansion, used as
39
+ arc height reference for --same-y cross-panel consistency.
37
40
  """
38
41
  # --- Determine data-driven baseline values for arc height ---
39
42
  # These baselines capture the "true" data range before junction expansion.
@@ -69,19 +72,15 @@ def _compute_y_limits(
69
72
  if min_used_y_val is None:
70
73
  min_used_y_val = base_min
71
74
 
72
- # --- Arc heights: use the LARGER of (data baseline, incoming y-limit) ---
73
- # In --same-y mode, max_used_y_val may be the global limit (much larger than
74
- # this panel's own baseline). We want the arcs to look proportional to the
75
- # visible axis range, so we take max(baseline, incoming_limit).
75
+ # --- Arc heights: always based on the data baseline ---
76
+ # Previously this used max(base_max, max_used_y_val) which caused
77
+ # arc height inflation when max_used_y_val was already expanded by
78
+ # the precompute step. The inflated arc peak then exceeded the
79
+ # y-axis limit and overflowed the panel.
80
+ # Use base_max / base_min exclusively — the limit expansion logic
81
+ # below still ensures the axis range grows to accommodate the arcs.
76
82
  _arc_ref_max = base_max
77
- if max_used_y_val is not None:
78
- _arc_ref_max = max(base_max, max_used_y_val)
79
-
80
83
  _arc_ref_min = base_min
81
- if min_used_y_val is not None:
82
- _arc_ref_min = min(
83
- base_min, min_used_y_val
84
- ) # both negative → pick the more negative
85
84
 
86
85
  top_arc_height = abs(3 * _arc_ref_max / 4)
87
86
  bot_arc_height = abs(3 * _arc_ref_min / 4) if _arc_ref_min != 0 else top_arc_height
@@ -119,6 +118,8 @@ def _compute_y_limits(
119
118
 
120
119
  if density_by_strand:
121
120
  jxn_on_top = jxn.strand == "+"
121
+ elif junctions_on_top:
122
+ jxn_on_top = True
122
123
  else:
123
124
  jxn_on_top = jxn_idx % 2 == 0
124
125
  if abs(min_used_y_val) < max_used_y_val:
@@ -163,7 +164,7 @@ def _compute_y_limits(
163
164
  if density_by_strand:
164
165
  max_used_y_val = max(abs(min_used_y_val), max_used_y_val)
165
166
  min_used_y_val = -max_used_y_val
166
- elif not density_by_strand and not jxns:
167
+ elif not density_by_strand and (junctions_on_top or not jxns):
167
168
  min_used_y_val = 0
168
169
 
169
170
  # Small expansion so the top / bottom arcs have visual breathing room
@@ -171,7 +172,7 @@ def _compute_y_limits(
171
172
  max_used_y_val *= 1.1
172
173
  min_used_y_val *= 1.1
173
174
 
174
- return max_used_y_val, min_used_y_val
175
+ return max_used_y_val, min_used_y_val, base_max
175
176
 
176
177
 
177
178
  def precompute_y_limits(
@@ -198,7 +199,7 @@ def precompute_y_limits(
198
199
  if data is None:
199
200
  data = obj.data
200
201
 
201
- return _compute_y_limits(
202
+ _max, _min, _base = _compute_y_limits(
202
203
  data=data,
203
204
  region=region,
204
205
  graph_coords=graph_coords,
@@ -207,4 +208,7 @@ def precompute_y_limits(
207
208
  density_by_strand=kwargs.get("density_by_strand", False),
208
209
  fill_step=fill_step,
209
210
  show_mean_jxn_number=False,
211
+ junctions_on_top=kwargs.get("junctions_on_top", False),
210
212
  )
213
+
214
+ return _max, _min, _base
@@ -406,6 +406,87 @@ def _plot_local_domain(
406
406
  # ============================================================================
407
407
 
408
408
 
409
+ def _auto_adjust_horizontal(
410
+ text_objs: List,
411
+ ax,
412
+ padding_px: float = 4.0,
413
+ max_iter: int = 20,
414
+ ):
415
+ """
416
+ 水平碰撞检测与自动调整 —— 替代 adjust_text 的 2D 随意移动。
417
+ 将所有文本沿弧线水平排列,通过贪婪算法确保相邻标签互不重叠。
418
+
419
+ 原理:
420
+ 1. 获取每个文本在显示坐标中的包围盒,计算其数据坐标宽度。
421
+ 2. 按 x 坐标排序,从左到右逐对检查。
422
+ 3. 如果前一个的右边缘 + 间距 > 后一个的左边缘,则把后一个向右推。
423
+ 4. 重复迭代直到没有标签需要移动为止。
424
+
425
+ :param text_objs: matplotlib.text.Text 对象列表
426
+ :param ax: matplotlib Axes
427
+ :param padding_px: 标签之间的最小间距(像素)
428
+ :param max_iter: 最大迭代次数
429
+ """
430
+ if len(text_objs) < 2:
431
+ return
432
+
433
+ try:
434
+ fig = ax.figure
435
+ fig.canvas.draw()
436
+ except Exception:
437
+ return # 无法渲染时直接跳过调整
438
+
439
+ inv_trans = ax.transData.inverted()
440
+
441
+ # ---- 收集每个标签的数据坐标宽度 ----
442
+ items = []
443
+ for t in text_objs:
444
+ x_data, y_data = t.get_position()
445
+ bbox = t.get_window_extent()
446
+
447
+ # 像素 → 数据坐标
448
+ left_data = inv_trans.transform((bbox.x0, bbox.y0))[0]
449
+ right_data = inv_trans.transform((bbox.x1, bbox.y0))[0]
450
+ half_w = (right_data - left_data) / 2.0
451
+
452
+ # 间距(像素 → 数据坐标)
453
+ p0 = inv_trans.transform((0, 0))[0]
454
+ p1 = inv_trans.transform((padding_px, 0))[0]
455
+ pad = abs(p1 - p0)
456
+
457
+ items.append({
458
+ "obj": t,
459
+ "x": x_data,
460
+ "y": y_data,
461
+ "half": half_w,
462
+ "pad": pad,
463
+ })
464
+
465
+ # 按 x 排序
466
+ items.sort(key=lambda d: d["x"])
467
+
468
+ # ---- 贪婪水平调整 ----
469
+ for _ in range(max_iter):
470
+ any_adjusted = False
471
+
472
+ for i in range(len(items) - 1):
473
+ cur = items[i]
474
+ nxt = items[i + 1]
475
+
476
+ cur_right = cur["x"] + cur["half"]
477
+ nxt_left = nxt["x"] - nxt["half"]
478
+ min_gap = max(cur["pad"], nxt["pad"])
479
+
480
+ need = cur_right + min_gap - nxt_left
481
+ if need > 0:
482
+ nxt["x"] += need
483
+ nxt["obj"].set_position((nxt["x"], nxt["y"]))
484
+ any_adjusted = True
485
+
486
+ if not any_adjusted:
487
+ break
488
+
489
+
409
490
  def plot_density(
410
491
  ax,
411
492
  obj: Optional[File] = None,
@@ -460,6 +541,11 @@ def plot_density(
460
541
  # drawn junction arcs have exactly the same height that was used to expand
461
542
  # the y-axis limits. Using the already-expanded max_used_y_val for arc
462
543
  # height causes non-convergent growth and truncated junction arcs.
544
+
545
+ # Determine whether y-limits were externally provided (e.g. from --same-y).
546
+ # Must be computed here BEFORE the arc-height block below uses it.
547
+ fixed_max_used_y = max_used_y_val is not None
548
+
463
549
  if isinstance(data, dict):
464
550
  _base_max = max(max(v.plus) if v.plus is not None else 0 for v in data.values())
465
551
  _minus_maxes = [
@@ -473,17 +559,20 @@ def plot_density(
473
559
  if _base_max % 2 == 1:
474
560
  _base_max += 1
475
561
 
476
- # Arc height reference: use the LARGER of (data baseline, incoming y-limit)
477
- # so arcs look proportional to the visible axis range (especially in
478
- # --same-y / --same-y-sc mode). If we only used the data baseline, panels
479
- # with small data would have tiny arcs hugging the x-axis when a large
480
- # global y-limit is shared across panels.
481
- _arc_ref_max = (
482
- max(_base_max, max_used_y_val) if max_used_y_val is not None else _base_max
483
- )
484
- _arc_ref_min = (
485
- min(_base_min, min_used_y_val) if min_used_y_val is not None else _base_min
486
- )
562
+ # Arc height reference: use global_arc_ref (from --same-y) when available
563
+ # and when y-limits were externally provided. This ensures panels with
564
+ # small data ranges don't get tiny arcs flattened against the x-axis when
565
+ # the panel's y-range is scaled up to match a larger panel.
566
+ #
567
+ # NOTE: we only use global_arc_ref when fixed_max_used_y is True so that
568
+ # standalone panels (no --same-y) preserve their original behavior.
569
+ global_arc_ref = kwargs.get("global_arc_ref")
570
+ if fixed_max_used_y and global_arc_ref is not None:
571
+ _arc_ref_max = max(_base_max, global_arc_ref)
572
+ _arc_ref_min = max(abs(_base_min), global_arc_ref) if _base_min != 0 else _arc_ref_max
573
+ else:
574
+ _arc_ref_max = _base_max
575
+ _arc_ref_min = _base_min
487
576
 
488
577
  _top_arc_height = abs(3 * _arc_ref_max / 4)
489
578
  _bot_arc_height = (
@@ -491,9 +580,9 @@ def plot_density(
491
580
  )
492
581
 
493
582
  # Compute y limits using shared logic
494
- fixed_max_used_y = max_used_y_val is not None
583
+ junctions_on_top = kwargs.get("junctions_on_top", False)
495
584
 
496
- max_used_y_val, min_used_y_val = _compute_y_limits(
585
+ max_used_y_val, min_used_y_val, _base = _compute_y_limits(
497
586
  data=data,
498
587
  region=region,
499
588
  graph_coords=graph_coords,
@@ -502,6 +591,7 @@ def plot_density(
502
591
  density_by_strand=kwargs.get("density_by_strand", False),
503
592
  fill_step=fill_step,
504
593
  show_mean_jxn_number=show_mean_jxn_number,
594
+ junctions_on_top=junctions_on_top,
505
595
  )
506
596
 
507
597
  # Draw fill
@@ -557,6 +647,8 @@ def plot_density(
557
647
 
558
648
  if kwargs.get("density_by_strand"):
559
649
  jxn_on_top = jxn.strand == "+"
650
+ elif junctions_on_top:
651
+ jxn_on_top = True
560
652
  else:
561
653
  jxn_on_top = jxn_idx % 2 == 0
562
654
  if abs(min_used_y_val) < max_used_y_val:
@@ -623,17 +715,15 @@ def plot_density(
623
715
  va="center",
624
716
  backgroundcolor="w",
625
717
  )
626
- t.set_bbox(dict(alpha=0))
718
+ t.set_bbox(dict(alpha=0, pad=0.5))
627
719
  text_objs.append(t)
628
720
 
629
721
  if show_junction_number and len(text_objs) > 1:
630
- adjust_text(
722
+ _auto_adjust_horizontal(
631
723
  text_objs,
632
724
  ax=ax,
633
- force_text=(0.5, 0.5),
634
- expand=(1.2, 1.2),
635
- arrowprops=None,
636
- lim=100,
725
+ padding_px=4.0,
726
+ max_iter=20,
637
727
  )
638
728
 
639
729
  if obj and obj.title:
@@ -659,7 +749,7 @@ def plot_density(
659
749
  ):
660
750
  max_used_y_val = max(abs(min_used_y_val), max_used_y_val)
661
751
  min_used_y_val = -max_used_y_val
662
- elif not kwargs.get("density_by_strand") and not jxns:
752
+ elif not kwargs.get("density_by_strand") and (junctions_on_top or not jxns):
663
753
  min_used_y_val = 0
664
754
 
665
755
  set_y_ticks(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trackplot
3
- Version: 0.6.3
3
+ Version: 0.6.5
4
4
  Summary: The trackplot is a tool for visualizing various next-generation sequencing (NGS) data, including DNA-seq, RNA-seq, single-cell RNA-seq and full-length sequencing datasets. https://sashimi.readthedocs.io/
5
5
  Author-email: ygidtu <ygidtu@gmail.com>
6
6
  License-Expression: BSD-3-Clause
@@ -8,6 +8,7 @@ trackplot/server.py
8
8
  trackplot.egg-info/PKG-INFO
9
9
  trackplot.egg-info/SOURCES.txt
10
10
  trackplot.egg-info/dependency_links.txt
11
+ trackplot.egg-info/entry_points.txt
11
12
  trackplot.egg-info/requires.txt
12
13
  trackplot.egg-info/top_level.txt
13
14
  trackplot/anno/AxLabel.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ trackplot = trackplot.cli:main
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes