OSCR

Morphometric analysis reveals that the chick cranial neural tube expands as an active shell.

Code ↔ Paper

16 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 16 matches · 1 of them tie a paragraph to a whole file, not to given lines: a weak match, whose lines are not tinted
  1. [1] § Materials and methods › Finite element modeling ↔ finite_element/fem_plane_stress.py, lines 1–40 · score 1.00 · plane stress finite, shear modulus, rational quadratic, membrane stress, nodal forces, hoop stress
  2. [2] § Materials and methods › Neural tube segmentation ↔ src/spatchcocking/sectioning_utils.py, the whole file · a weak match · score 0.83 · marching cubes, Volumetric segmentation, post processed, napari, Fiji, Slicer
  3. [3] § Materials and methods › Neural tube segmentation ↔ notebooks/01_mesh_generation.ipynb, lines 1–38 · score 0.82 · gamma correction, marching cubes, manual refinement, local thresholding, inner lumen, Fiji
  4. [4] § Materials and methods › Proliferation analysis ↔ notebooks/04_pHH3_density_mapping.ipynb, lines 61–115 · score 0.76 · density field, point cloud, mesh vertex, basal mesh, spots, stacks
  5. [5] § Results › Spatial patterns of mitotic activity are preserved across global expansion ↔ notebooks/06_figure_plots.ipynb, lines 965–1098 · score 0.75 · phospho histone H3, inverse distance weighting, mitotic cells, pHH3, local density, radius
  6. [6] § Materials and methods › Proliferation analysis ↔ notebooks/04_pHH3_density_mapping.py, lines 68–109 · score 0.72 · density field, point cloud, mesh vertex, spots, stacks, radius
  7. [7] § Materials and methods › Tissue shape and morphometric analysis ↔ src/spatchcocking/spatchcocking_utils.py, lines 389–455 · score 0.70 · adjacent vertices, quadratic surface, local neighborhood, fitted, Gaussian, curvature
  8. [8] § Materials and methods › Spatchcocking transformation ↔ notebooks/05_spatchcocking.ipynb, lines 124–176 · score 0.63 · thin plate spline, medial axis, deformation, cross, dorsal, Spatchcocking
  9. [9] § Materials and methods › Spatchcocking transformation ↔ src/spatchcocking/spatchcocking_utils.py, lines 525–606 · score 0.58 · MLS smoothing, medial axis, iterative, spatchcocking, Vedo, mesh
  10. [10] § Materials and methods › Tissue shape and morphometric analysis ↔ notebooks/06_figure_plots.ipynb, lines 726–859 · score 0.56 · Euclidean distance, lumen surface, surface mesh, Tissue thickness, nearest, scalar
  11. [11] § Results › Cranial neural tube enlargement is accompanied by significant lumen rounding ↔ notebooks/01_mesh_generation.ipynb, lines 1–38 · score 0.55 · manual refinement, local thresholding, inner lumen, triangular, neural tube, segmented
  12. [12] § Materials and methods › Chick embryo collection ↔ notebooks/06_figure_plots.ipynb, lines 246–291 · score 0.54 · Fold changes, Surface area, inter, SA, ratio, FB
  13. [13] § Materials and methods › Chick embryo collection ↔ notebooks/06_figure_plots.py, lines 245–290 · score 0.54 · Fold changes, Surface area, inter, SA, ratio, FB
  14. [14] § Results › Surface curvature mapping reveals that brain compartmentalization is defined by localized constrictions amid global inflation ↔ notebooks/06_figure_plots.ipynb, lines 293–428 · score 0.54 · dorso ventral, rostral caudal, Gaussian curvature, DV profiles, Spatchcocked, RC
  15. [15] § Results › Standardized 2D mapping enables direct comparison of local neuroepithelium properties across developmental stages ↔ src/spatchcocking/spatchcocking_utils.py, lines 1–37 · score 0.54 · medial axis extracted, manifold, biological, neural tube, flattening, spatchcocking
  16. [16] § Materials and methods › Statistical analysis ↔ notebooks/06_figure_plots.ipynb, lines 1213–1257 · score 0.51 · quantile bins, Pearson correlation, Spearman, width, compartments, DV

Paper

Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Jupyter notebook · 1,989 lines · 67 KB · MIT · 5 matches

  1. # %% [markdown]
  2. # Connected to ni-plots (Python 3.14.2)
  3. # %% [markdown]
  4. # # Figure reproduction from CSV data
  5. #
  6. # This script reproduces the statistical figures from the paper using the
  7. # pre-computed CSV files in `../data/csv/`. No mesh processing is required —
  8. # all vertex-level measurements have already been extracted via the spatchcocking
  9. # pipeline (notebooks 02–05) and saved as CSV.
  10. #
  11. # | Figure | Content | CSV file |
  12. # |---|---|---|
  13. # | Fig 1j | Cross-section area profile along RC axis | `cross_section_area.csv` |
  14. # | Fig 1e–i | Fold changes: length, SA, volume, SA/V ratio | `compartment_lengths.csv`, `lumen_geometry.csv` |
  15. # | Fig 3g | Gaussian curvature DV profiles per vesicle | `spatchcocked_measurements.csv` |
  16. # | Fig 4c,f,g | Mean curvature: global comparison, DV profiles | `spatchcocked_measurements.csv` |
  17. # | Fig 5c,f,g | Tissue thickness: global comparison, DV profiles | `spatchcocked_measurements.csv` |
  18. # | Fig 6c,f,g | pHH3 mitotic density: global comparison, DV profiles | `spatchcocked_measurements.csv` |
  19. # | Supp Fig 1 | Absolute geometry, fold changes, correlations | all CSVs |
  20. #
  21. # **Dependencies** (in addition to the main spatchcocking environment):
  22. # `statannotations` — install with `pip install statannotations`
  23. #
  24. # **Run in VS Code**: open this file and use "Run Cell" (Shift+Enter) per block,
  25. # or "Run All Cells" from the Jupyter toolbar. Each `# %%` block is one cell.
  26. # Outputs are saved as SVG files alongside this script.
  27. # %% [markdown]
  28. # ## Figure 1 — Lumen geometry
  29. #
  30. # **Fig 1j** — Cross-sectional area of the lumen plotted against normalised
  31. # rostral-caudal position. Each point is the mean area at one z-position;
  32. # shaded band = ±1 SD across embryos. HH17 (blue) and HH20 (red).
  33. #
  34. # **CSV**: `cross_section_area.csv`
  35. # Columns: `z_grid` (µm along RC axis), `area` (µm²), `stage`, `type`
  36. # (`individual` = per-embryo datapoints used for statistics).
  37. # %%
  38. import pandas as pd
  39. import seaborn as sns
  40. import matplotlib.pyplot as plt
  41. import matplotlib.ticker as ticker
  42. sns.set_style("ticks")
  43. fontsize = 10
  44. plt.rcParams.update({
  45. 'font.size': 10,
  46. 'axes.labelsize': 10,
  47. 'axes.linewidth': 1.5,
  48. 'xtick.major.width': 1.5,
  49. 'ytick.major.width': 1.5,
  50. 'xtick.major.size': 6,
  51. 'ytick.major.size': 6,
  52. 'xtick.direction': 'out',
  53. 'ytick.direction': 'out',
  54. 'xtick.labelsize': 10,
  55. 'ytick.labelsize': 10,
  56. 'svg.fonttype': 'none',
  57. 'font.family': 'sans-serif',
  58. 'font.sans-serif': ['Arial']
  59. })
  60. dpival = 100
  61. csv_filename = '../data/csv/cross_section_area.csv'
  62. df = pd.read_csv(csv_filename)
  63. labels = ['HH17', 'HH20']
  64. palette = {'HH17': '#2c7bb6', 'HH20': '#d7191c'}
  65. fig, ax = plt.subplots(figsize=(3, 5), dpi=dpival)
  66. for label in labels:
  67. stage_df = df[(df['stage'] == label) & (df['type'] == 'individual')]
  68. if stage_df.empty:
  69. continue
  70. stats = stage_df.groupby('z_grid')['area'].agg(['mean', 'std']).reset_index()
  71. stats['mean_smooth'] = stats['mean'].rolling(window=10, center=True).mean()
  72. stats['std_smooth'] = stats['std'].rolling(window=10, center=True).mean()
  73. stats = stats.dropna()
  74. ax.fill_betweenx(stats['z_grid'],
  75. stats['mean_smooth'] - stats['std_smooth'],
  76. stats['mean_smooth'] + stats['std_smooth'],
  77. color=palette[label], alpha=0.15, linewidth=0)
  78. ax.plot(stats['mean_smooth'], stats['z_grid'],
  79. color=palette[label], lw=3.0, label=label)
  80. x_step = 1e5
  81. ax.xaxis.set_major_locator(ticker.MultipleLocator(x_step))
  82. x_fmt = ticker.ScalarFormatter(useMathText=True)
  83. x_fmt.set_scientific(True)
  84. x_fmt.set_powerlimits((5, 5))
  85. ax.xaxis.set_major_formatter(x_fmt)
  86. y_fmt = ticker.ScalarFormatter(useMathText=True)
  87. y_fmt.set_scientific(True)
  88. y_fmt.set_powerlimits((3, 3))
  89. ax.yaxis.set_major_formatter(y_fmt)
  90. ax.set_ylabel(r'Rostral-Caudal Distance [$\mu$m]')
  91. ax.set_xlabel(r'Cross-section Area [$\mu$m$^2$]')
  92. ax.set_xlim(left=0)
  93. ax.set_ylim(bottom=0)
  94. sns.despine()
  95. ax.legend(title=" ", frameon=False, fontsize=fontsize, title_fontsize=fontsize,
  96. bbox_to_anchor=(1.02, 1), loc='upper left')
  97. plt.tight_layout()
  98. plt.savefig('__csarea-over-rclength.svg', format='svg', bbox_inches='tight')
  99. plt.show()
  100. # %% [markdown]
  101. # **Fig 1e–g** — Fold changes in total length, surface area, and volume
  102. # (each embryo normalised to the HH17 group mean = 1.0).
  103. # Welch t-test between stages; bracket = significance level.
  104. #
  105. # **Fig 1h–i** — Fold change in SA per compartment, and raw SA/V ratio per
  106. # compartment (FB = forebrain, MB = midbrain, HB = hindbrain).
  107. # Within-compartment (HH17 vs HH20) and inter-compartment (HH20 vs HH20)
  108. # comparisons are shown.
  109. #
  110. # **CSVs**:
  111. # - `compartment_lengths.csv` — columns: `stage`, `end` (total length µm),
  112. # `mhb` (midbrain–hindbrain boundary µm), `fmb` (fore–midbrain boundary µm)
  113. # - `lumen_geometry.csv` — columns: `Stage`, `Region`, `Area` (µm²), `Volume` (µm³)
  114. # %%
  115. import pandas as pd
  116. import matplotlib.pyplot as plt
  117. import matplotlib.ticker as ticker
  118. import seaborn as sns
  119. from scipy import stats
  120. sns.set_style("ticks")
  121. plt.rcParams.update({
  122. 'font.size': 10,
  123. 'axes.labelsize': 10,
  124. 'axes.titlesize': 10,
  125. 'axes.linewidth': 1.5,
  126. 'xtick.major.width': 1.5,
  127. 'ytick.major.width': 1.5,
  128. 'xtick.major.size': 6,
  129. 'ytick.major.size': 6,
  130. 'xtick.direction': 'out',
  131. 'ytick.direction': 'out',
  132. 'xtick.labelsize': 10,
  133. 'ytick.labelsize': 10,
  134. 'svg.fonttype': 'none',
  135. 'font.family': 'sans-serif',
  136. 'font.sans-serif': ['Arial']
  137. })
  138. dpival = 100
  139. PALETTE = {'HH17': '#3182bd', 'HH20': '#de2d26'}
  140. COMPARTMENTS = ['Forebrain', 'Midbrain', 'Hindbrain']
  141. STAGES = ['HH17', 'HH20']
  142. def set_common_style(ax, xlabel, ylabel):
  143. """Applies consistent L-frame (linewidths handled by rcParams)."""
  144. ax.spines['top'].set_visible(False)
  145. ax.spines['right'].set_visible(False)
  146. ax.set_xlabel(xlabel)
  147. ax.set_ylabel(ylabel)
  148. def add_external_stat(ax, x1, x2, level, p_val):
  149. """Adds brackets OUTSIDE the axis."""
  150. base_y = 1.05
  151. step = 0.08
  152. y = base_y + (level * step)
  153. ax.plot([x1, x1, x2, x2], [y, y+0.02, y+0.02, y],
  154. color='black', clip_on=False, transform=ax.get_xaxis_transform())
  155. if p_val < 0.001: text = '***'
  156. elif p_val < 0.01: text = '**'
  157. elif p_val < 0.05: text = '*'
  158. else: text = 'ns'
  159. ax.text((x1+x2)*0.5, y+0.02, text, ha='center', va='bottom',
  160. transform=ax.get_xaxis_transform())
  161. # Length Data
  162. df_len = pd.read_csv('../data/csv/compartment_lengths.csv')
  163. df_len['stage'] = df_len['stage'].astype(str).str.upper()
  164. df_len['Total Length'] = df_len['end']
  165. hh17_mean_len = df_len[df_len['stage'] == 'HH17']['Total Length'].mean()
  166. df_len_fc = df_len.copy()
  167. df_len_fc['Total Length FC'] = df_len['Total Length'] / hh17_mean_len
  168. # Area & Volume Data
  169. df_av = pd.read_csv("../data/csv/lumen_geometry.csv")
  170. df_av['Stage'] = df_av['Stage'].astype(str).str.upper()
  171. df_av['SA_Vol_Ratio'] = df_av['Area'] / df_av['Volume']
  172. df_av_fc = df_av.copy()
  173. for metric in ['Area', 'Volume']:
  174. for reg in COMPARTMENTS:
  175. hh17_mean = df_av[(df_av['Stage'] == 'HH17') & (df_av['Region'] == reg)][metric].mean()
  176. mask = df_av['Region'] == reg
  177. df_av_fc.loc[mask, metric] = df_av.loc[mask, metric] / hh17_mean
  178. # FIGURE 1: Total Fold Changes (Length, Area, Volume)
  179. fig1, axes1 = plt.subplots(1, 3, figsize=(4.3, 2), dpi=dpival)
  180. fc_stage_configs = [
  181. (df_len_fc, 'stage', 'Total Length FC', 'Fold Change (Length)', 0),
  182. (df_av_fc, 'Stage', 'Area', 'Fold Change (Surface Area)', 1),
  183. (df_av_fc, 'Stage', 'Volume', 'Fold Change (Volume)', 2)
  184. ]
  185. for df_plot, x_col, y_col, ylabel, i in fc_stage_configs:
  186. ax = axes1[i]
  187. sns.boxplot(data=df_plot, x=x_col, y=y_col, hue=x_col, palette=PALETTE,
  188. width=0.5, order=STAGES, legend=False, ax=ax)
  189. ax.axhline(1.0, color='black', linestyle='--', alpha=0.7)
  190. g1 = df_plot[df_plot[x_col] == 'HH17'][y_col]
  191. g2 = df_plot[df_plot[x_col] == 'HH20'][y_col]
  192. _, p = stats.ttest_ind(g1, g2)
  193. add_external_stat(ax, 0, 1, 0, p)
  194. set_common_style(ax, ' ', ylabel)
  195. axes1[2].set_ylim(top=20)
  196. fig1.subplots_adjust(top=0.95, bottom=0.05, left=0.10, right=0.95, wspace=0.7)
  197. fig1.savefig("__fig1_total_fold_changes.svg", format='svg', bbox_inches='tight')
  198. plt.show()
  199. # FIGURE 2: Fold Change SA & SA/Vol Ratio by Compartment
  200. fig2, axes2 = plt.subplots(1, 2, figsize=(5.5, 2), dpi=dpival)
  201. ax_fc_area = axes2[0]
  202. sns.boxplot(data=df_av_fc, x='Region', y='Area', hue='Stage',
  203. order=COMPARTMENTS, palette=PALETTE, width=0.6, ax=ax_fc_area)
  204. ax_fc_area.axhline(1.0, color='black', linestyle='--', alpha=0.7)
  205. set_common_style(ax_fc_area, ' ', 'Fold Change (Surface Area)')
  206. ax_fc_area.legend_.remove()
  207. ax_ratio = axes2[1]
  208. sns.boxplot(data=df_av, x='Region', y='SA_Vol_Ratio', hue='Stage',
  209. order=COMPARTMENTS, palette=PALETTE, width=0.6, ax=ax_ratio)
  210. set_common_style(ax_ratio, ' ', r'Surface Area / Volume Ratio [$\mu$m$^{-1}$]')
  211. ax_ratio.set_ylim(top=2.5e-2)
  212. fmt = ticker.ScalarFormatter(useMathText=True)
  213. fmt.set_scientific(True)
  214. fmt.set_powerlimits((-2, -2))
  215. ax_ratio.yaxis.set_major_formatter(fmt)
  216. ax_ratio.legend(frameon=False, loc='upper left', bbox_to_anchor=(1, 1))
  217. inter_comps = [(0, 1, 1), (1, 2, 1), (0, 2, 2)]
  218. for ax, df_plot, metric in [(ax_fc_area, df_av_fc, 'Area'), (ax_ratio, df_av, 'SA_Vol_Ratio')]:
  219. for i, comp in enumerate(COMPARTMENTS):
  220. d1 = df_plot[(df_plot['Region'] == comp) & (df_plot['Stage'] == 'HH17')][metric]
  221. d2 = df_plot[(df_plot['Region'] == comp) & (df_plot['Stage'] == 'HH20')][metric]
  222. _, p = stats.ttest_ind(d1, d2)
  223. add_external_stat(ax, i - 0.15, i + 0.15, 0, p)
  224. for (idx1, idx2, level) in inter_comps:
  225. c1, c2 = COMPARTMENTS[idx1], COMPARTMENTS[idx2]
  226. d1 = df_plot[(df_plot['Region'] == c1) & (df_plot['Stage'] == 'HH20')][metric]
  227. d2 = df_plot[(df_plot['Region'] == c2) & (df_plot['Stage'] == 'HH20')][metric]
  228. _, p = stats.ttest_ind(d1, d2)
  229. add_external_stat(ax, idx1 + 0.2, idx2 + 0.2, level, p)
  230. for ax in axes2:
  231. ax.set_xticks([0, 1, 2])
  232. ax.set_xticklabels(['FB', 'MB', 'HB'])
  233. fig2.subplots_adjust(top=0.95, bottom=0.05, left=0.08, right=0.80, wspace=0.5)
  234. fig2.savefig("__fig2_compartment_sa_and_ratio.svg", format='svg', bbox_inches='tight')
  235. plt.show()
  236. # %% [markdown]
  237. # ## Figure 3 — Gaussian curvature DV profiles
  238. #
  239. # **Fig 3g** — Dorso-ventral profiles of Gaussian curvature *K* [µm⁻²] for each
  240. # brain vesicle (forebrain, midbrain, hindbrain). Line = mean across embryos;
  241. # shaded band = ±1 SD. Angle θ = 0° is dorsal, ±180° is ventral.
  242. #
  243. # Compartments are defined by normalised rostral-caudal position (`norm_height`):
  244. # - HH17: FB > 0.80, MB 0.60–0.80, HB < 0.60
  245. # - HH20: FB > 0.75, MB 0.55–0.75, HB < 0.55
  246. #
  247. # **CSV**: `spatchcocked_measurements.csv`
  248. # Each row is one mesh vertex from one embryo, with columns:
  249. # `stage`, `timepoint` (embryo ID), `norm_height`, `angle_degrees`,
  250. # `Gauss_Curvature`, `Mean_Curvature`, `K1`, `K2`, `thickness`, `phh3`
  251. # %%
  252. import pandas as pd
  253. import numpy as np
  254. import seaborn as sns
  255. import matplotlib.pyplot as plt
  256. import matplotlib.ticker as ticker
  257. sns.set_theme(style="ticks")
  258. plt.rcParams.update({
  259. 'font.size': 10,
  260. 'axes.labelsize': 10,
  261. 'axes.titlesize': 10,
  262. 'axes.titleweight': 'normal',
  263. 'axes.labelweight': 'normal',
  264. 'axes.linewidth': 1.5,
  265. 'xtick.major.width': 1.5,
  266. 'ytick.major.width': 1.5,
  267. 'xtick.major.size': 6,
  268. 'ytick.major.size': 6,
  269. 'xtick.direction': 'out',
  270. 'ytick.direction': 'out',
  271. 'xtick.labelsize': 10,
  272. 'ytick.labelsize': 10,
  273. 'svg.fonttype': 'none',
  274. 'font.family': 'sans-serif',
  275. 'font.sans-serif': ['Arial']
  276. })
  277. TARGET_METRIC = 'Gauss_Curvature'
  278. property_map = {
  279. 'Gauss_Curvature': r'Gaussian Curvature [$\mu m^{-2}$]',
  280. 'Mean_Curvature': r'Mean Curvature [$\mu m^{-1}$]',
  281. 'K1': r'K1 [$\mu m^{-1}$]',
  282. 'K2': r'K2 [$\mu m^{-1}$]',
  283. 'thickness': r'Thickness [$\mu m$]',
  284. 'phh3': 'Local pH3+ cell count'
  285. }
  286. master_csv = "../data/csv/spatchcocked_measurements.csv"
  287. df = pd.read_csv(master_csv)
  288. df['stage'] = df['stage'].astype(str).str.upper()
  289. df['Compartment'] = None
  290. mask_hh17 = df['stage'] == 'HH17'
  291. df.loc[mask_hh17 & (df['norm_height'] < 0.6), 'Compartment'] = 'Hindbrain'
  292. df.loc[mask_hh17 & (df['norm_height'] >= 0.6) & (df['norm_height'] <= 0.8), 'Compartment'] = 'Midbrain'
  293. df.loc[mask_hh17 & (df['norm_height'] > 0.8), 'Compartment'] = 'Forebrain'
  294. mask_hh20 = df['stage'] == 'HH20'
  295. df.loc[mask_hh20 & (df['norm_height'] < 0.55), 'Compartment'] = 'Hindbrain'
  296. df.loc[mask_hh20 & (df['norm_height'] >= 0.55) & (df['norm_height'] <= 0.75), 'Compartment'] = 'Midbrain'
  297. df.loc[mask_hh20 & (df['norm_height'] > 0.75), 'Compartment'] = 'Forebrain'
  298. df['angle_bin'] = pd.cut(df['angle_degrees'], bins=np.arange(-180, 190, 10))
  299. df['angle_bin_center'] = df['angle_bin'].apply(lambda x: x.mid).astype(float)
  300. stage_palette = {"HH17": "#3182bd", "HH20": "#de2d26"}
  301. compartments = ['Forebrain', 'Midbrain', 'Hindbrain']
  302. ylabel = property_map[TARGET_METRIC]
  303. fig, axes = plt.subplots(1, 3, figsize=(6.7, 2.6), sharey=True, dpi=100)
  304. for i, comp in enumerate(compartments):
  305. comp_data = df[df['Compartment'] == comp]
  306. ax = axes[i]
  307. sns.lineplot(
  308. data=comp_data,
  309. x='angle_bin_center',
  310. y=TARGET_METRIC,
  311. hue='stage',
  312. palette=stage_palette,
  313. linewidth=2.5,
  314. errorbar='sd',
  315. ax=ax
  316. )
  317. ax.set_title(comp, pad=10)
  318. ax.set_xlabel('Dorso-ventral axis')
  319. ax.set_xticks([-180, -90, 0, 90, 180])
  320. ax.set_xlim(-180, 180)
  321. if i == 0:
  322. ax.set_ylabel(ylabel)
  323. else:
  324. ax.set_ylabel('')
  325. if i == 2:
  326. ax.legend(title=' ', loc='best', frameon=False)
  327. else:
  328. ax.get_legend().remove()
  329. if 'Curvature' in TARGET_METRIC or TARGET_METRIC in ['K1', 'K2']:
  330. ax.axhline(0, color='black', linestyle='--', linewidth=1, alpha=0.5)
  331. for spine in ax.spines.values():
  332. spine.set_visible(True)
  333. if 'Curvature' in TARGET_METRIC or TARGET_METRIC in ['K1', 'K2']:
  334. ymin, ymax = axes[0].get_ylim()
  335. abs_max = max(abs(ymin), abs(ymax))
  336. axes[0].set_ylim(bottom=-abs_max, top=abs_max)
  337. if TARGET_METRIC in ['thickness', 'phh3']:
  338. axes[0].set_ylim(bottom=0)
  339. formatter = ticker.ScalarFormatter(useMathText=True)
  340. formatter.set_scientific(True)
  341. formatter.set_powerlimits((-2, 3))
  342. axes[0].yaxis.set_major_formatter(formatter)
  343. plt.tight_layout()
  344. save_path = f"__compartment_plot_{TARGET_METRIC}.svg"
  345. plt.savefig(save_path, format='svg', bbox_inches='tight')
  346. print(f"Plot saved: {save_path}")
  347. plt.show()
  348. # %% [markdown]
  349. # ## Figure 4 — Mean curvature
  350. #
  351. # **Fig 4c** — Global mean curvature *H* [µm⁻¹] comparison between HH17 and
  352. # HH20. Each data point is the per-embryo mean; Welch t-test annotated with
  353. # stars (statannotations library).
  354. #
  355. # **Fig 4f** — DV profiles of *H* per vesicle (same layout as Fig 3g).
  356. #
  357. # **Fig 4g** — Overall DV profile of *H* pooling all three vesicles.
  358. # %%
  359. import pandas as pd
  360. import numpy as np
  361. import seaborn as sns
  362. import matplotlib.pyplot as plt
  363. import matplotlib.ticker as ticker
  364. sns.set_theme(style="ticks")
  365. plt.rcParams.update({
  366. 'font.size': 10,
  367. 'axes.labelsize': 10,
  368. 'axes.titlesize': 10,
  369. 'axes.titleweight': 'normal',
  370. 'axes.labelweight': 'normal',
  371. 'axes.linewidth': 1.5,
  372. 'xtick.major.width': 1.5,
  373. 'ytick.major.width': 1.5,
  374. 'xtick.major.size': 6,
  375. 'ytick.major.size': 6,
  376. 'xtick.direction': 'out',
  377. 'ytick.direction': 'out',
  378. 'xtick.labelsize': 10,
  379. 'ytick.labelsize': 10,
  380. 'svg.fonttype': 'none',
  381. 'font.family': 'sans-serif',
  382. 'font.sans-serif': ['Arial']
  383. })
  384. TARGET_METRIC = 'Mean_Curvature'
  385. property_map = {
  386. 'Gauss_Curvature': r'Gaussian Curvature [$\mu m^{-2}$]',
  387. 'Mean_Curvature': r'Mean Curvature [$\mu m^{-1}$]',
  388. 'K1': r'K1 [$\mu m^{-1}$]',
  389. 'K2': r'K2 [$\mu m^{-1}$]',
  390. 'thickness': r'Thickness [$\mu m$]',
  391. 'phh3': 'Local pH3+ cell count'
  392. }
  393. master_csv = "../data/csv/spatchcocked_measurements.csv"
  394. df = pd.read_csv(master_csv)
  395. df['stage'] = df['stage'].astype(str).str.upper()
  396. df['Compartment'] = None
  397. mask_hh17 = df['stage'] == 'HH17'
  398. df.loc[mask_hh17 & (df['norm_height'] < 0.6), 'Compartment'] = 'Hindbrain'
  399. df.loc[mask_hh17 & (df['norm_height'] >= 0.6) & (df['norm_height'] <= 0.8), 'Compartment'] = 'Midbrain'
  400. df.loc[mask_hh17 & (df['norm_height'] > 0.8), 'Compartment'] = 'Forebrain'
  401. mask_hh20 = df['stage'] == 'HH20'
  402. df.loc[mask_hh20 & (df['norm_height'] < 0.55), 'Compartment'] = 'Hindbrain'
  403. df.loc[mask_hh20 & (df['norm_height'] >= 0.55) & (df['norm_height'] <= 0.75), 'Compartment'] = 'Midbrain'
  404. df.loc[mask_hh20 & (df['norm_height'] > 0.75), 'Compartment'] = 'Forebrain'
  405. df['angle_bin'] = pd.cut(df['angle_degrees'], bins=np.arange(-180, 190, 10))
  406. df['angle_bin_center'] = df['angle_bin'].apply(lambda x: x.mid).astype(float)
  407. stage_palette = {"HH17": "#3182bd", "HH20": "#de2d26"}
  408. compartments = ['Forebrain', 'Midbrain', 'Hindbrain']
  409. ylabel = property_map[TARGET_METRIC]
  410. fig, axes = plt.subplots(1, 3, figsize=(6.7, 2.6), sharey=True, dpi=100)
  411. for i, comp in enumerate(compartments):
  412. comp_data = df[df['Compartment'] == comp]
  413. ax = axes[i]
  414. sns.lineplot(
  415. data=comp_data,
  416. x='angle_bin_center',
  417. y=TARGET_METRIC,
  418. hue='stage',
  419. palette=stage_palette,
  420. linewidth=2.5,
  421. errorbar='sd',
  422. ax=ax
  423. )
  424. ax.set_title(comp, pad=10)
  425. ax.set_xlabel('Dorso-ventral axis')
  426. ax.set_xticks([-180, -90, 0, 90, 180])
  427. ax.set_xlim(-180, 180)
  428. if i == 0:
  429. ax.set_ylabel(ylabel)
  430. else:
  431. ax.set_ylabel('')
  432. if i == 2:
  433. ax.legend(title=' ', loc='best', frameon=False)
  434. else:
  435. ax.get_legend().remove()
  436. if 'Curvature' in TARGET_METRIC or TARGET_METRIC in ['K1', 'K2']:
  437. ax.axhline(0, color='black', linestyle='--', linewidth=1, alpha=0.5)
  438. for spine in ax.spines.values():
  439. spine.set_visible(True)
  440. if 'Curvature' in TARGET_METRIC or TARGET_METRIC in ['K1', 'K2']:
  441. ymin, ymax = axes[0].get_ylim()
  442. abs_max = max(abs(ymin), abs(ymax))
  443. axes[0].set_ylim(top=abs_max)
  444. if TARGET_METRIC in ['thickness', 'phh3']:
  445. axes[0].set_ylim(bottom=0)
  446. formatter = ticker.ScalarFormatter(useMathText=True)
  447. formatter.set_scientific(True)
  448. formatter.set_powerlimits((-2, 3))
  449. axes[0].yaxis.set_major_formatter(formatter)
  450. plt.tight_layout()
  451. save_path = f"__compartment_plot_{TARGET_METRIC}.svg"
  452. plt.savefig(save_path, format='svg', bbox_inches='tight')
  453. print(f"Plot saved: {save_path}")
  454. plt.show()
  455. # %%
  456. import pandas as pd
  457. import numpy as np
  458. import seaborn as sns
  459. import matplotlib.pyplot as plt
  460. import matplotlib.ticker as ticker
  461. sns.set_theme(style="ticks")
  462. plt.rcParams.update({
  463. 'font.size': 10,
  464. 'axes.labelsize': 10,
  465. 'axes.titlesize': 10,
  466. 'axes.titleweight': 'normal',
  467. 'axes.labelweight': 'normal',
  468. 'axes.linewidth': 1.5,
  469. 'xtick.major.width': 1.5,
  470. 'ytick.major.width': 1.5,
  471. 'xtick.major.size': 6,
  472. 'ytick.major.size': 6,
  473. 'xtick.direction': 'out',
  474. 'ytick.direction': 'out',
  475. 'xtick.labelsize': 10,
  476. 'ytick.labelsize': 10,
  477. 'svg.fonttype': 'none',
  478. 'font.family': 'sans-serif',
  479. 'font.sans-serif': ['Arial']
  480. })
  481. TARGET_METRIC = 'Mean_Curvature'
  482. property_map = {
  483. 'Gauss_Curvature': r'Gaussian Curvature [$\mu m^{-2}$]',
  484. 'Mean_Curvature': r'Mean Curvature [$\mu m^{-1}$]',
  485. 'K1': r'K1 [$\mu m^{-1}$]',
  486. 'K2': r'K2 [$\mu m^{-1}$]',
  487. 'thickness': r'Thickness [$\mu m$]',
  488. 'phh3': 'Local pH3+ cell count'
  489. }
  490. master_csv = "../data/csv/spatchcocked_measurements.csv"
  491. df = pd.read_csv(master_csv)
  492. df['stage'] = df['stage'].astype(str).str.upper()
  493. df['angle_bin'] = pd.cut(df['angle_degrees'], bins=np.arange(-180, 190, 10))
  494. df['angle_bin_center'] = df['angle_bin'].apply(lambda x: x.mid).astype(float)
  495. stage_palette = {"HH17": "#3182bd", "HH20": "#de2d26"}
  496. ylabel = property_map.get(TARGET_METRIC, TARGET_METRIC)
  497. fig, ax = plt.subplots(figsize=(2.4, 2.8), dpi=100)
  498. sns.lineplot(
  499. data=df,
  500. x='angle_bin_center',
  501. y=TARGET_METRIC,
  502. hue='stage',
  503. palette=stage_palette,
  504. linewidth=3,
  505. errorbar='sd',
  506. ax=ax
  507. )
  508. ax.set_xlabel('Dorso-ventral axis')
  509. ax.set_ylabel(ylabel)
  510. ax.set_xticks([-180, -90, 0, 90, 180])
  511. ax.set_xlim(-180, 180)
  512. if 'Curvature' in TARGET_METRIC or TARGET_METRIC in ['K1', 'K2']:
  513. ax.axhline(0, color='black', linestyle='--', linewidth=1.5, alpha=0.6, zorder=1)
  514. ax.spines['top'].set_visible(False)
  515. ax.spines['right'].set_visible(False)
  516. ax.legend(title=' ', loc='best', frameon=False)
  517. if 'Curvature' in TARGET_METRIC or TARGET_METRIC in ['K1', 'K2']:
  518. ymin, ymax = ax.get_ylim()
  519. abs_max = max(abs(ymin), abs(ymax))
  520. ax.set_ylim(top=abs_max)
  521. if TARGET_METRIC in ['thickness', 'phh3']:
  522. ax.set_ylim(bottom=0)
  523. formatter = ticker.ScalarFormatter(useMathText=True)
  524. formatter.set_scientific(True)
  525. formatter.set_powerlimits((-2, 3))
  526. ax.yaxis.set_major_formatter(formatter)
  527. plt.tight_layout()
  528. save_path = f"__overall_dv_plot_{TARGET_METRIC}.svg"
  529. plt.savefig(save_path, format='svg', bbox_inches='tight')
  530. print(f"Overall plot saved: {save_path}")
  531. plt.show()
  532. # %%
  533. import pandas as pd
  534. import matplotlib.pyplot as plt
  535. import matplotlib.ticker as ticker
  536. import seaborn as sns
  537. from statannotations.Annotator import Annotator
  538. TARGET_METRIC = 'Mean_Curvature'
  539. Y_LABEL = r'Mean Curvature [$\mu m^{-1}$]'
  540. FONT_SIZE = 10
  541. plt.rcParams.update({
  542. 'svg.fonttype': 'none',
  543. 'font.size': FONT_SIZE,
  544. 'axes.labelsize': FONT_SIZE,
  545. 'xtick.labelsize': FONT_SIZE,
  546. 'ytick.labelsize': FONT_SIZE,
  547. 'legend.fontsize': FONT_SIZE,
  548. 'axes.titlesize': FONT_SIZE,
  549. 'axes.linewidth': 1.5,
  550. 'xtick.major.width': 1.5,
  551. 'ytick.major.width': 1.5,
  552. 'font.family': 'sans-serif',
  553. 'font.sans-serif': ['Arial']
  554. })
  555. palette = {'HH17': '#2c7bb6', 'HH20': '#d7191c'}
  556. df = pd.read_csv("../data/csv/spatchcocked_measurements.csv")
  557. df['stage'] = df['stage'].astype(str).str.upper()
  558. median_df = df.groupby(['timepoint', 'stage'])[TARGET_METRIC].mean().reset_index()
  559. fig, ax = plt.subplots(figsize=(2.4, 2.8), dpi=100)
  560. order = ['HH17', 'HH20']
  561. sns.boxplot(data=median_df, x='stage', y=TARGET_METRIC, palette=palette, order=order, width=0.5, ax=ax, fliersize=0)
  562. annotator = Annotator(ax, [("HH17", "HH20")], data=median_df, x='stage', y=TARGET_METRIC, order=order)
  563. annotator.configure(test='t-test_ind', text_format='star', loc='outside')
  564. annotator.apply_and_annotate()
  565. ax.spines['top'].set_visible(False)
  566. ax.spines['right'].set_visible(False)
  567. ax.spines['left'].set_linewidth(1.5)
  568. ax.spines['bottom'].set_linewidth(1.5)
  569. ax.set_ylabel(Y_LABEL)
  570. ax.set_xlabel(" ")
  571. ymin, ymax = ax.get_ylim()
  572. abs_max = max(abs(ymin), abs(ymax))
  573. ax.set_ylim(bottom=0, top=abs_max)
  574. ax.axhline(0, color='black', linestyle='--', linewidth=1.5, alpha=0.5, zorder=0)
  575. fmt = ticker.ScalarFormatter(useMathText=True)
  576. fmt.set_scientific(True)
  577. fmt.set_powerlimits((-3, 3))
  578. ax.yaxis.set_major_formatter(fmt)
  579. ax.yaxis.get_offset_text().set_fontsize(FONT_SIZE)
  580. plt.tight_layout()
  581. plt.savefig(f"__global_stat_{TARGET_METRIC}.svg", format='svg')
  582. plt.show()
  583. # %% [markdown]
  584. # ## Figure 5 — Tissue thickness
  585. #
  586. # **Fig 5c** — Global tissue thickness [µm] comparison HH17 vs HH20 (per-embryo
  587. # means, boxplot + Welch t-test).
  588. #
  589. # **Fig 5f** — DV profiles of thickness per vesicle (same format as Fig 3g).
  590. #
  591. # **Fig 5g** — Compartment-wise boxplots of thickness, with within-compartment
  592. # (HH17 vs HH20) and inter-compartment (HH20 only) significance brackets.
  593. #
  594. # Thickness is measured as the Euclidean distance from each lumen surface vertex
  595. # to the nearest point on the basal (outer) surface mesh (notebook 03).
  596. # %%
  597. import pandas as pd
  598. import numpy as np
  599. import seaborn as sns
  600. import matplotlib.pyplot as plt
  601. import matplotlib.ticker as ticker
  602. sns.set_theme(style="ticks")
  603. plt.rcParams.update({
  604. 'font.size': 10,
  605. 'axes.labelsize': 10,
  606. 'axes.titlesize': 10,
  607. 'axes.titleweight': 'normal',
  608. 'axes.labelweight': 'normal',
  609. 'axes.linewidth': 1.5,
  610. 'xtick.major.width': 1.5,
  611. 'ytick.major.width': 1.5,
  612. 'xtick.major.size': 6,
  613. 'ytick.major.size': 6,
  614. 'xtick.direction': 'out',
  615. 'ytick.direction': 'out',
  616. 'xtick.labelsize': 10,
  617. 'ytick.labelsize': 10,
  618. 'svg.fonttype': 'none',
  619. 'font.family': 'sans-serif',
  620. 'font.sans-serif': ['Arial']
  621. })
  622. TARGET_METRIC = 'thickness'
  623. property_map = {
  624. 'Gauss_Curvature': r'Gaussian Curvature [$\mu m^{-2}$]',
  625. 'Mean_Curvature': r'Mean Curvature [$\mu m^{-1}$]',
  626. 'K1': r'K1 [$\mu m^{-1}$]',
  627. 'K2': r'K2 [$\mu m^{-1}$]',
  628. 'thickness': r'Thickness [$\mu m$]',
  629. 'phh3': 'Local pH3+ cell count'
  630. }
  631. master_csv = "../data/csv/spatchcocked_measurements.csv"
  632. df = pd.read_csv(master_csv)
  633. df['stage'] = df['stage'].astype(str).str.upper()
  634. df['Compartment'] = None
  635. mask_hh17 = df['stage'] == 'HH17'
  636. df.loc[mask_hh17 & (df['norm_height'] < 0.6), 'Compartment'] = 'Hindbrain'
  637. df.loc[mask_hh17 & (df['norm_height'] >= 0.6) & (df['norm_height'] <= 0.8), 'Compartment'] = 'Midbrain'
  638. df.loc[mask_hh17 & (df['norm_height'] > 0.8), 'Compartment'] = 'Forebrain'
  639. mask_hh20 = df['stage'] == 'HH20'
  640. df.loc[mask_hh20 & (df['norm_height'] < 0.55), 'Compartment'] = 'Hindbrain'
  641. df.loc[mask_hh20 & (df['norm_height'] >= 0.55) & (df['norm_height'] <= 0.75), 'Compartment'] = 'Midbrain'
  642. df.loc[mask_hh20 & (df['norm_height'] > 0.75), 'Compartment'] = 'Forebrain'
  643. df['angle_bin'] = pd.cut(df['angle_degrees'], bins=np.arange(-180, 190, 10))
  644. df['angle_bin_center'] = df['angle_bin'].apply(lambda x: x.mid).astype(float)
  645. stage_palette = {"HH17": "#3182bd", "HH20": "#de2d26"}
  646. compartments = ['Forebrain', 'Midbrain', 'Hindbrain']
  647. ylabel = property_map[TARGET_METRIC]
  648. fig, axes = plt.subplots(1, 3, figsize=(6.7, 2.6), sharey=True, dpi=100)
  649. for i, comp in enumerate(compartments):
  650. comp_data = df[df['Compartment'] == comp]
  651. ax = axes[i]
  652. sns.lineplot(
  653. data=comp_data,
  654. x='angle_bin_center',
  655. y=TARGET_METRIC,
  656. hue='stage',
  657. palette=stage_palette,
  658. linewidth=2.5,
  659. errorbar='sd',
  660. ax=ax
  661. )
  662. ax.set_title(comp, pad=10)
  663. ax.set_xlabel('Dorso-ventral axis')
  664. ax.set_xticks([-180, -90, 0, 90, 180])
  665. ax.set_xlim(-180, 180)
  666. if i == 0:
  667. ax.set_ylabel(ylabel)
  668. else:
  669. ax.set_ylabel('')
  670. if i == 2:
  671. ax.legend(title=' ', loc='best', frameon=False)
  672. else:
  673. ax.get_legend().remove()
  674. if 'Curvature' in TARGET_METRIC or TARGET_METRIC in ['K1', 'K2']:
  675. ax.axhline(0, color='black', linestyle='--', linewidth=1, alpha=0.5)
  676. for spine in ax.spines.values():
  677. spine.set_visible(True)
  678. if 'Curvature' in TARGET_METRIC or TARGET_METRIC in ['K1', 'K2']:
  679. ymin, ymax = axes[0].get_ylim()
  680. abs_max = max(abs(ymin), abs(ymax))
  681. axes[0].set_ylim(top=abs_max)
  682. if TARGET_METRIC in ['thickness', 'phh3']:
  683. axes[0].set_ylim(bottom=0)
  684. formatter = ticker.ScalarFormatter(useMathText=True)
  685. formatter.set_scientific(True)
  686. formatter.set_powerlimits((-2, 3))
  687. axes[0].yaxis.set_major_formatter(formatter)
  688. plt.tight_layout()
  689. save_path = f"__compartment_plot_{TARGET_METRIC}.svg"
  690. plt.savefig(save_path, format='svg', bbox_inches='tight')
  691. print(f"Plot saved: {save_path}")
  692. plt.show()
  693. # %%
  694. import pandas as pd
  695. import numpy as np
  696. import matplotlib.pyplot as plt
  697. import seaborn as sns
  698. from scipy import stats
  699. dpival = 100
  700. FONT_SIZE = 10
  701. plt.rcParams.update({
  702. 'svg.fonttype': 'none',
  703. 'font.size': FONT_SIZE,
  704. 'axes.labelsize': FONT_SIZE,
  705. 'xtick.labelsize': FONT_SIZE,
  706. 'ytick.labelsize': FONT_SIZE,
  707. 'legend.fontsize': FONT_SIZE,
  708. 'axes.titlesize': FONT_SIZE,
  709. 'axes.linewidth': 1.5,
  710. 'xtick.major.width': 1.5,
  711. 'ytick.major.width': 1.5,
  712. 'font.family': 'sans-serif',
  713. 'font.sans-serif': ['Arial']
  714. })
  715. PALETTE = {'HH17': '#3182bd', 'HH20': '#de2d26'}
  716. COMPARTMENTS = ['Forebrain', 'Midbrain', 'Hindbrain']
  717. STAGES = ['HH17', 'HH20']
  718. def set_common_style(ax, ylabel):
  719. ax.spines['top'].set_visible(False)
  720. ax.spines['right'].set_visible(False)
  721. for side in ['left', 'bottom']:
  722. ax.spines[side].set_linewidth(1.5)
  723. ax.set_xlabel("")
  724. ax.set_ylabel(ylabel)
  725. def add_external_stat(ax, x1, x2, level, p_val):
  726. base_y = 1.05
  727. step = 0.08
  728. y = base_y + (level * step)
  729. ax.plot([x1, x1, x2, x2], [y, y+0.02, y+0.02, y],
  730. lw=1.5, color='black', clip_on=False,
  731. transform=ax.get_xaxis_transform())
  732. if p_val < 0.001: text = '***'
  733. elif p_val < 0.01: text = '**'
  734. elif p_val < 0.05: text = '*'
  735. else: text = 'ns'
  736. ax.text((x1+x2)*0.5, y+0.02, text, ha='center', va='bottom',
  737. color='black', transform=ax.get_xaxis_transform())
  738. master_csv = "../data/csv/spatchcocked_measurements.csv"
  739. df_raw = pd.read_csv(master_csv)
  740. df_raw['stage'] = df_raw['stage'].astype(str).str.upper()
  741. df_raw['Compartment'] = None
  742. for stage, bounds in {'HH17': (0.6, 0.8), 'HH20': (0.55, 0.75)}.items():
  743. mask = df_raw['stage'] == stage
  744. df_raw.loc[mask & (df_raw['norm_height'] < bounds[0]), 'Compartment'] = 'Hindbrain'
  745. df_raw.loc[mask & (df_raw['norm_height'] >= bounds[0]) & (df_raw['norm_height'] <= bounds[1]), 'Compartment'] = 'Midbrain'
  746. df_raw.loc[mask & (df_raw['norm_height'] > bounds[1]), 'Compartment'] = 'Forebrain'
  747. df_total = df_raw.groupby(['timepoint', 'stage'])['thickness'].mean().reset_index()
  748. df_comp = df_raw.groupby(['timepoint', 'stage', 'Compartment'])['thickness'].mean().reset_index()
  749. def plot_total_thickness(data, y_col, ylabel, filename):
  750. plt.figure(figsize=(2, 3.2), dpi=dpival)
  751. ax = sns.boxplot(data=data, x='stage', y=y_col, hue='stage', palette=PALETTE,
  752. width=0.5, linewidth=1.5, order=STAGES, legend=False)
  753. g1 = data[data['stage'] == 'HH17'][y_col]
  754. g2 = data[data['stage'] == 'HH20'][y_col]
  755. _, p = stats.ttest_ind(g1, g2)
  756. add_external_stat(ax, 0, 1, 0, p)
  757. set_common_style(ax, ylabel)
  758. plt.subplots_adjust(top=0.8)
  759. plt.savefig(f"{filename}.svg", format='svg', bbox_inches='tight')
  760. def plot_compartment_thickness(data, y_col, ylabel, filename):
  761. plt.figure(figsize=(2.5, 3.0), dpi=dpival)
  762. ax = sns.boxplot(data=data, x='Compartment', y=y_col, hue='stage',
  763. order=COMPARTMENTS, palette=PALETTE, width=0.6, linewidth=1.5)
  764. for i, comp in enumerate(COMPARTMENTS):
  765. d1 = data[(data['Compartment'] == comp) & (data['stage'] == 'HH17')][y_col]
  766. d2 = data[(data['Compartment'] == comp) & (data['stage'] == 'HH20')][y_col]
  767. _, p = stats.ttest_ind(d1, d2)
  768. add_external_stat(ax, i - 0.2, i + 0.2, 0, p)
  769. inter_comps = [(0, 1, 1), (1, 2, 1), (0, 2, 2)]
  770. for (idx1, idx2, level) in inter_comps:
  771. comp1, comp2 = COMPARTMENTS[idx1], COMPARTMENTS[idx2]
  772. d1 = data[(data['Compartment'] == comp1) & (data['stage'] == 'HH20')][y_col]
  773. d2 = data[(data['Compartment'] == comp2) & (data['stage'] == 'HH20')][y_col]
  774. _, p = stats.ttest_ind(d1, d2)
  775. add_external_stat(ax, idx1 + 0.2, idx2 + 0.2, level, p)
  776. set_common_style(ax, ylabel)
  777. plt.legend(frameon=False, loc='best', bbox_to_anchor=(1, 1), title=' ')
  778. ax.set_xticks(range(len(COMPARTMENTS)))
  779. ax.set_xticklabels(['FB', 'MB', 'HB'])
  780. plt.subplots_adjust(top=0.75, right=0.75)
  781. plt.savefig(f"{filename}.svg", format='svg', bbox_inches='tight')
  782. plot_total_thickness(df_total, 'thickness', r'Thickness [$\mu$m]', 'thickness_total_raw')
  783. plot_compartment_thickness(df_comp, 'thickness', r'Thickness [$\mu$m]', 'thickness_comp_raw')
  784. plt.show()
  785. # %% [markdown]
  786. # ## Figure 6 — Mitotic cell density (pHH3)
  787. #
  788. # **Fig 6c** — Global pHH3+ cell density comparison HH17 vs HH20.
  789. #
  790. # **Fig 6f** — DV profiles of pHH3+ density per vesicle.
  791. #
  792. # **Fig 6g** — Compartment-wise boxplots of pHH3+ density.
  793. #
  794. # pHH3 (phospho-Histone H3) marks cells in mitosis. The local density at
  795. # each lumen surface vertex is the number of pHH3+ spots (detected in Imaris)
  796. # within a sphere of radius R = 100 µm, interpolated onto the mesh via inverse
  797. # distance weighting (notebook 04).
  798. # %%
  799. import pandas as pd
  800. import numpy as np
  801. import seaborn as sns
  802. import matplotlib.pyplot as plt
  803. import matplotlib.ticker as ticker
  804. sns.set_theme(style="ticks")
  805. plt.rcParams.update({
  806. 'font.size': 10,
  807. 'axes.labelsize': 10,
  808. 'axes.titlesize': 10,
  809. 'axes.titleweight': 'normal',
  810. 'axes.labelweight': 'normal',
  811. 'axes.linewidth': 1.5,
  812. 'xtick.major.width': 1.5,
  813. 'ytick.major.width': 1.5,
  814. 'xtick.major.size': 6,
  815. 'ytick.major.size': 6,
  816. 'xtick.direction': 'out',
  817. 'ytick.direction': 'out',
  818. 'xtick.labelsize': 10,
  819. 'ytick.labelsize': 10,
  820. 'svg.fonttype': 'none',
  821. 'font.family': 'sans-serif',
  822. 'font.sans-serif': ['Arial']
  823. })
  824. TARGET_METRIC = 'phh3'
  825. property_map = {
  826. 'Gauss_Curvature': r'Gaussian Curvature [$\mu m^{-2}$]',
  827. 'Mean_Curvature': r'Mean Curvature [$\mu m^{-1}$]',
  828. 'K1': r'K1 [$\mu m^{-1}$]',
  829. 'K2': r'K2 [$\mu m^{-1}$]',
  830. 'thickness': r'Thickness [$\mu m$]',
  831. 'phh3': 'Local pHH3+ cell count'
  832. }
  833. master_csv = "../data/csv/spatchcocked_measurements.csv"
  834. df = pd.read_csv(master_csv)
  835. df['stage'] = df['stage'].astype(str).str.upper()
  836. df['Compartment'] = None
  837. mask_hh17 = df['stage'] == 'HH17'
  838. df.loc[mask_hh17 & (df['norm_height'] < 0.6), 'Compartment'] = 'Hindbrain'
  839. df.loc[mask_hh17 & (df['norm_height'] >= 0.6) & (df['norm_height'] <= 0.8), 'Compartment'] = 'Midbrain'
  840. df.loc[mask_hh17 & (df['norm_height'] > 0.8), 'Compartment'] = 'Forebrain'
  841. mask_hh20 = df['stage'] == 'HH20'
  842. df.loc[mask_hh20 & (df['norm_height'] < 0.55), 'Compartment'] = 'Hindbrain'
  843. df.loc[mask_hh20 & (df['norm_height'] >= 0.55) & (df['norm_height'] <= 0.75), 'Compartment'] = 'Midbrain'
  844. df.loc[mask_hh20 & (df['norm_height'] > 0.75), 'Compartment'] = 'Forebrain'
  845. df['angle_bin'] = pd.cut(df['angle_degrees'], bins=np.arange(-180, 190, 10))
  846. df['angle_bin_center'] = df['angle_bin'].apply(lambda x: x.mid).astype(float)
  847. stage_palette = {"HH17": "#3182bd", "HH20": "#de2d26"}
  848. compartments = ['Forebrain', 'Midbrain', 'Hindbrain']
  849. ylabel = property_map[TARGET_METRIC]
  850. fig, axes = plt.subplots(1, 3, figsize=(6.7, 2.6), sharey=True, dpi=100)
  851. for i, comp in enumerate(compartments):
  852. comp_data = df[df['Compartment'] == comp]
  853. ax = axes[i]
  854. sns.lineplot(
  855. data=comp_data,
  856. x='angle_bin_center',
  857. y=TARGET_METRIC,
  858. hue='stage',
  859. palette=stage_palette,
  860. linewidth=2.5,
  861. errorbar='sd',
  862. ax=ax
  863. )
  864. ax.set_title(comp, pad=10)
  865. ax.set_xlabel('Dorso-ventral axis')
  866. ax.set_xticks([-180, -90, 0, 90, 180])
  867. ax.set_xlim(-180, 180)
  868. if i == 0:
  869. ax.set_ylabel(ylabel)
  870. else:
  871. ax.set_ylabel('')
  872. if i == 2:
  873. ax.legend(title=' ', loc='best', frameon=False)
  874. else:
  875. ax.get_legend().remove()
  876. if 'Curvature' in TARGET_METRIC or TARGET_METRIC in ['K1', 'K2']:
  877. ax.axhline(0, color='black', linestyle='--', linewidth=1, alpha=0.5)
  878. for spine in ax.spines.values():
  879. spine.set_visible(True)
  880. if 'Curvature' in TARGET_METRIC or TARGET_METRIC in ['K1', 'K2']:
  881. ymin, ymax = axes[0].get_ylim()
  882. abs_max = max(abs(ymin), abs(ymax))
  883. axes[0].set_ylim(top=abs_max)
  884. if TARGET_METRIC in ['thickness', 'phh3']:
  885. axes[0].set_ylim(bottom=0)
  886. formatter = ticker.ScalarFormatter(useMathText=True)
  887. formatter.set_scientific(True)
  888. formatter.set_powerlimits((-2, 3))
  889. axes[0].yaxis.set_major_formatter(formatter)
  890. plt.tight_layout()
  891. save_path = f"__compartment_plot_{TARGET_METRIC}.svg"
  892. plt.savefig(save_path, format='svg', bbox_inches='tight')
  893. print(f"Plot saved: {save_path}")
  894. plt.show()
  895. # %%
  896. import pandas as pd
  897. import numpy as np
  898. import matplotlib.pyplot as plt
  899. import seaborn as sns
  900. from scipy import stats
  901. dpival = 100
  902. FONT_SIZE = 10
  903. plt.rcParams.update({
  904. 'svg.fonttype': 'none',
  905. 'font.size': FONT_SIZE,
  906. 'axes.labelsize': FONT_SIZE,
  907. 'xtick.labelsize': FONT_SIZE,
  908. 'ytick.labelsize': FONT_SIZE,
  909. 'legend.fontsize': FONT_SIZE,
  910. 'axes.titlesize': FONT_SIZE,
  911. 'axes.linewidth': 1.5,
  912. 'xtick.major.width': 1.5,
  913. 'ytick.major.width': 1.5,
  914. 'font.family': 'sans-serif',
  915. 'font.sans-serif': ['Arial']
  916. })
  917. PALETTE = {'HH17': '#3182bd', 'HH20': '#de2d26'}
  918. COMPARTMENTS = ['Forebrain', 'Midbrain', 'Hindbrain']
  919. STAGES = ['HH17', 'HH20']
  920. TARGET_METRIC = 'phh3'
  921. def set_common_style(ax, ylabel):
  922. ax.spines['top'].set_visible(False)
  923. ax.spines['right'].set_visible(False)
  924. for side in ['left', 'bottom']:
  925. ax.spines[side].set_linewidth(1.5)
  926. ax.set_xlabel("")
  927. ax.set_ylabel(ylabel)
  928. def add_external_stat(ax, x1, x2, level, p_val):
  929. base_y = 1.05
  930. step = 0.08
  931. y = base_y + (level * step)
  932. ax.plot([x1, x1, x2, x2], [y, y+0.02, y+0.02, y],
  933. lw=1.5, color='black', clip_on=False,
  934. transform=ax.get_xaxis_transform())
  935. if p_val < 0.001: text = '***'
  936. elif p_val < 0.01: text = '**'
  937. elif p_val < 0.05: text = '*'
  938. else: text = 'ns'
  939. ax.text((x1+x2)*0.5, y+0.02, text, ha='center', va='bottom',
  940. color='black', transform=ax.get_xaxis_transform())
  941. master_csv = "../data/csv/spatchcocked_measurements.csv"
  942. df_raw = pd.read_csv(master_csv)
  943. df_raw['stage'] = df_raw['stage'].astype(str).str.upper()
  944. df_raw['Compartment'] = None
  945. for stage, bounds in {'HH17': (0.6, 0.8), 'HH20': (0.55, 0.75)}.items():
  946. mask = df_raw['stage'] == stage
  947. df_raw.loc[mask & (df_raw['norm_height'] < bounds[0]), 'Compartment'] = 'Hindbrain'
  948. df_raw.loc[mask & (df_raw['norm_height'] >= bounds[0]) & (df_raw['norm_height'] <= bounds[1]), 'Compartment'] = 'Midbrain'
  949. df_raw.loc[mask & (df_raw['norm_height'] > bounds[1]), 'Compartment'] = 'Forebrain'
  950. df_total = df_raw.groupby(['timepoint', 'stage'])[TARGET_METRIC].mean().reset_index()
  951. df_comp = df_raw.groupby(['timepoint', 'stage', 'Compartment'])[TARGET_METRIC].mean().reset_index()
  952. def plot_total_phh3(data, y_col, ylabel, filename, L=False):
  953. plt.figure(figsize=(2, 3.2), dpi=dpival)
  954. ax = sns.boxplot(data=data, x='stage', y=y_col, hue='stage', palette=PALETTE,
  955. width=0.5, linewidth=1.5, order=STAGES, legend=False)
  956. if L:
  957. ax.set_ylim(bottom=0)
  958. if 'Fold' in ylabel:
  959. plt.axhline(1.0, color='black', linestyle='--', linewidth=1, alpha=0.7)
  960. g1 = data[data['stage'] == 'HH17'][y_col]
  961. g2 = data[data['stage'] == 'HH20'][y_col]
  962. _, p = stats.ttest_ind(g1, g2)
  963. add_external_stat(ax, 0, 1, 0, p)
  964. set_common_style(ax, ylabel)
  965. plt.subplots_adjust(top=0.8)
  966. plt.savefig(f"{filename}.svg", format='svg', bbox_inches='tight')
  967. def plot_compartments_phh3(data, y_col, ylabel, filename, L=False):
  968. plt.figure(figsize=(2.5, 3), dpi=dpival)
  969. ax = sns.boxplot(data=data, x='Compartment', y=y_col, hue='stage',
  970. order=COMPARTMENTS, palette=PALETTE, width=0.6, linewidth=1.5)
  971. if L:
  972. ax.set_ylim(bottom=0)
  973. if 'Fold' in ylabel:
  974. plt.axhline(1.0, color='black', linestyle='--', linewidth=1, alpha=0.7)
  975. for i, comp in enumerate(COMPARTMENTS):
  976. d1 = data[(data['Compartment'] == comp) & (data['stage'] == 'HH17')][y_col]
  977. d2 = data[(data['Compartment'] == comp) & (data['stage'] == 'HH20')][y_col]
  978. _, p = stats.ttest_ind(d1, d2)
  979. add_external_stat(ax, i - 0.2, i + 0.2, 0, p)
  980. inter_comps = [(0, 1, 1), (1, 2, 1), (0, 2, 2)]
  981. for (idx1, idx2, level) in inter_comps:
  982. comp1, comp2 = COMPARTMENTS[idx1], COMPARTMENTS[idx2]
  983. d1 = data[(data['Compartment'] == comp1) & (data['stage'] == 'HH20')][y_col]
  984. d2 = data[(data['Compartment'] == comp2) & (data['stage'] == 'HH20')][y_col]
  985. _, p = stats.ttest_ind(d1, d2)
  986. add_external_stat(ax, idx1 + 0.2, idx2 + 0.2, level, p)
  987. set_common_style(ax, ylabel)
  988. plt.legend(frameon=False, loc='upper left', bbox_to_anchor=(1, 1), title='Stage')
  989. ax.set_xticks(range(len(COMPARTMENTS)))
  990. ax.set_xticklabels(['FB', 'MB', 'HB'])
  991. plt.subplots_adjust(top=0.75, right=0.75)
  992. plt.savefig(f"{filename}.svg", format='svg', bbox_inches='tight')
  993. plot_total_phh3(df_total, TARGET_METRIC, 'Local pHH3+ cell count', 'phh3_total_raw', L=True)
  994. plot_compartments_phh3(df_comp, TARGET_METRIC, 'Local pHH3+ cell count', 'phh3_comp_raw', L=True)
  995. plt.show()
  996. # %% [markdown]
  997. # ## Supplementary Figure 1 — Absolute geometry and correlations
  998. #
  999. # **Supp 1a–c** — Absolute lumen length, surface area, and volume at HH17 and
  1000. # HH20 (total and per compartment).
  1001. #
  1002. # **Supp 1d–f** — Fold changes in length and volume per compartment.
  1003. #
  1004. # **Supp 1g–h** — Pearson correlation between tissue thickness and curvature
  1005. # (Gaussian and Mean), computed on binned data (10 quantile bins per stage).
  1006. # Each point = mean of one bin; regression line + r and p shown in legend.
  1007. #
  1008. # **Supp 1i–l** — Spearman rank correlation of the spatial pattern (HH17 vs
  1009. # HH20) for all four metrics. Each point = one 10° DV bin; colour encodes DV
  1010. # position (dorsal = centre, ventral = ends of the cyclic_plasma colormap).
  1011. # %%
  1012. import pandas as pd
  1013. import numpy as np
  1014. import matplotlib.pyplot as plt
  1015. import matplotlib.ticker as ticker
  1016. import seaborn as sns
  1017. from scipy import stats
  1018. dpival = 100
  1019. FONT_SIZE = 10
  1020. plt.rcParams.update({
  1021. 'svg.fonttype': 'none',
  1022. 'font.size': FONT_SIZE,
  1023. 'axes.labelsize': FONT_SIZE,
  1024. 'xtick.labelsize': FONT_SIZE,
  1025. 'ytick.labelsize': FONT_SIZE,
  1026. 'legend.fontsize': FONT_SIZE,
  1027. 'axes.titlesize': FONT_SIZE,
  1028. 'axes.linewidth': 1.5,
  1029. 'xtick.major.width': 1.5,
  1030. 'ytick.major.width': 1.5,
  1031. 'font.family': 'sans-serif',
  1032. 'font.sans-serif': ['Arial']
  1033. })
  1034. PALETTE = {'HH17': '#3182bd', 'HH20': '#de2d26'}
  1035. STAGES = ['HH17', 'HH20']
  1036. COMPARTMENTS = ['Forebrain', 'Midbrain', 'Hindbrain']
  1037. def set_common_style(ax, ylabel):
  1038. ax.spines['top'].set_visible(False)
  1039. ax.spines['right'].set_visible(False)
  1040. for side in ['left', 'bottom']:
  1041. ax.spines[side].set_linewidth(1.5)
  1042. ax.set_xlabel("")
  1043. ax.set_ylabel(ylabel)
  1044. def add_external_stat(ax, x1, x2, level, p_val):
  1045. base_y = 1.05
  1046. step = 0.08
  1047. y = base_y + (level * step)
  1048. ax.plot([x1, x1, x2, x2], [y, y+0.02, y+0.02, y],
  1049. lw=1.5, color='black', clip_on=False,
  1050. transform=ax.get_xaxis_transform())
  1051. if p_val < 0.001: text = '***'
  1052. elif p_val < 0.01: text = '**'
  1053. elif p_val < 0.05: text = '*'
  1054. else: text = 'ns'
  1055. ax.text((x1+x2)*0.5, y+0.02, text, ha='center', va='bottom',
  1056. color='black', transform=ax.get_xaxis_transform())
  1057. # Length Data
  1058. df_len = pd.read_csv('../data/csv/compartment_lengths.csv')
  1059. df_len['stage'] = df_len['stage'].astype(str).str.upper()
  1060. df_len['Hindbrain'] = df_len['mhb']
  1061. df_len['Midbrain'] = df_len['fmb'] - df_len['mhb']
  1062. df_len['Forebrain'] = df_len['end'] - df_len['fmb']
  1063. df_len['Total Length'] = df_len['end']
  1064. hh17_means_len = df_len[df_len['stage'] == 'HH17'][COMPARTMENTS + ['Total Length']].mean()
  1065. df_len_fc = df_len.copy()
  1066. for col in COMPARTMENTS + ['Total Length']:
  1067. df_len_fc[col] = df_len[col] / hh17_means_len[col]
  1068. melted_len_raw = df_len.melt(id_vars=['stage'], value_vars=COMPARTMENTS, var_name='Compartment', value_name='Length')
  1069. melted_len_fc = df_len_fc.melt(id_vars=['stage'], value_vars=COMPARTMENTS, var_name='Compartment', value_name='Fold Change')
  1070. # Area & Volume Data
  1071. df_av = pd.read_csv("../data/csv/lumen_geometry.csv")
  1072. df_av['Stage'] = df_av['Stage'].astype(str).str.upper()
  1073. df_av_fc = df_av.copy()
  1074. for metric in ['Area', 'Volume']:
  1075. for reg in COMPARTMENTS:
  1076. hh17_mean = df_av[(df_av['Stage'] == 'HH17') & (df_av['Region'] == reg)][metric].mean()
  1077. mask = df_av['Region'] == reg
  1078. df_av_fc.loc[mask, metric] = df_av.loc[mask, metric] / hh17_mean
  1079. def plot_total(ax, data, x_col, y_col, ylabel, power=None, is_fc=False):
  1080. stage_col = 'stage' if 'stage' in data.columns else 'Stage'
  1081. sns.boxplot(data=data, x=stage_col, y=y_col, hue=stage_col, palette=PALETTE,
  1082. width=0.5, linewidth=1.5, order=STAGES, legend=False, ax=ax)
  1083. if is_fc:
  1084. ax.axhline(1.0, color='black', linestyle='--', linewidth=1, alpha=0.7)
  1085. else:
  1086. ax.set_ylim(bottom=0)
  1087. g1 = data[data[stage_col] == 'HH17'][y_col]
  1088. g2 = data[data[stage_col] == 'HH20'][y_col]
  1089. _, p = stats.ttest_ind(g1.dropna(), g2.dropna())
  1090. add_external_stat(ax, 0, 1, 0, p)
  1091. set_common_style(ax, ylabel)
  1092. if not is_fc and power is not None:
  1093. fmt = ticker.ScalarFormatter(useMathText=True)
  1094. fmt.set_scientific(True)
  1095. fmt.set_powerlimits((power, power))
  1096. ax.yaxis.set_major_formatter(fmt)
  1097. def plot_compartments(ax, data, comp_col, y_col, ylabel, power=None, is_fc=False, show_legend=False):
  1098. stage_col = 'stage' if 'stage' in data.columns else 'Stage'
  1099. sns.boxplot(data=data, x=comp_col, y=y_col, hue=stage_col,
  1100. order=COMPARTMENTS, palette=PALETTE, width=0.6, linewidth=1.5, ax=ax)
  1101. if is_fc:
  1102. ax.axhline(1.0, color='black', linestyle='--', linewidth=1, alpha=0.7)
  1103. else:
  1104. ax.set_ylim(bottom=0)
  1105. for i, comp in enumerate(COMPARTMENTS):
  1106. d1 = data[(data[comp_col] == comp) & (data[stage_col] == 'HH17')][y_col]
  1107. d2 = data[(data[comp_col] == comp) & (data[stage_col] == 'HH20')][y_col]
  1108. if not d1.empty and not d2.empty:
  1109. _, p = stats.ttest_ind(d1.dropna(), d2.dropna())
  1110. add_external_stat(ax, i - 0.15, i + 0.15, 0, p)
  1111. inter_comps = [(0, 1, 1), (1, 2, 1), (0, 2, 2)]
  1112. for (idx1, idx2, level) in inter_comps:
  1113. comp1, comp2 = COMPARTMENTS[idx1], COMPARTMENTS[idx2]
  1114. d1 = data[(data[comp_col] == comp1) & (data[stage_col] == 'HH20')][y_col]
  1115. d2 = data[(data[comp_col] == comp2) & (data[stage_col] == 'HH20')][y_col]
  1116. if not d1.empty and not d2.empty:
  1117. _, p = stats.ttest_ind(d1.dropna(), d2.dropna())
  1118. add_external_stat(ax, idx1 + 0.2, idx2 + 0.2, level, p)
  1119. set_common_style(ax, ylabel)
  1120. ax.set_xticks(range(len(COMPARTMENTS)))
  1121. ax.set_xticklabels(['FB', 'MB', 'HB'])
  1122. if not is_fc and power is not None:
  1123. fmt = ticker.ScalarFormatter(useMathText=True)
  1124. fmt.set_scientific(True)
  1125. fmt.set_powerlimits((power, power))
  1126. ax.yaxis.set_major_formatter(fmt)
  1127. if show_legend:
  1128. ax.legend(frameon=False, loc='upper left', bbox_to_anchor=(1, 1), title='Stage')
  1129. else:
  1130. ax.legend().remove()
  1131. # Figure 1: Absolute Values (Total)
  1132. fig1, axes1 = plt.subplots(1, 3, figsize=(6, 2.5), dpi=dpival)
  1133. plot_total(axes1[0], df_len, 'stage', 'Total Length', r'Length [$\mu$m]')
  1134. plot_total(axes1[1], df_av, 'Stage', 'Area', r'Surface Area [$\mu$m$^2$]', power=6)
  1135. plot_total(axes1[2], df_av, 'Stage', 'Volume', r'Volume [$\mu$m$^3$]', power=8)
  1136. fig1.tight_layout()
  1137. fig1.subplots_adjust(top=0.8)
  1138. fig1.savefig("fig1_total_absolute.svg", format='svg')
  1139. # Figure 2: Absolute Values (Compartment-wise)
  1140. fig2, axes2 = plt.subplots(1, 3, figsize=(7, 2.5), dpi=dpival)
  1141. plot_compartments(axes2[0], melted_len_raw, 'Compartment', 'Length', r'Length [$\mu$m]')
  1142. plot_compartments(axes2[1], df_av, 'Region', 'Area', r'Surface Area [$\mu$m$^2$]', power=6)
  1143. plot_compartments(axes2[2], df_av, 'Region', 'Volume', r'Volume [$\mu$m$^3$]', power=8, show_legend=True)
  1144. fig2.tight_layout()
  1145. fig2.subplots_adjust(top=0.75, right=0.9)
  1146. fig2.savefig("fig2_compartment_absolute.svg", format='svg')
  1147. # Figure 3: Fold Change (Compartment-wise Length & Volume)
  1148. fig3, axes3 = plt.subplots(1, 2, figsize=(5, 2.5), dpi=dpival)
  1149. plot_compartments(axes3[0], melted_len_fc, 'Compartment', 'Fold Change', 'Fold Change (Length)', is_fc=True)
  1150. plot_compartments(axes3[1], df_av_fc, 'Region', 'Volume', 'Fold Change (Volume)', is_fc=True, show_legend=True)
  1151. fig3.tight_layout()
  1152. fig3.subplots_adjust(top=0.75, right=0.85)
  1153. fig3.savefig("fig3_fold_change_length_volume.svg", format='svg')
  1154. plt.show()
  1155. # %%
  1156. import pandas as pd
  1157. import numpy as np
  1158. import matplotlib.pyplot as plt
  1159. import matplotlib.ticker as ticker
  1160. import seaborn as sns
  1161. from scipy.stats import pearsonr
  1162. dpival = 100
  1163. FONT_SIZE = 10
  1164. plt.rcParams.update({
  1165. 'svg.fonttype': 'none',
  1166. 'font.size': FONT_SIZE,
  1167. 'axes.labelsize': FONT_SIZE+1,
  1168. 'xtick.labelsize': FONT_SIZE,
  1169. 'ytick.labelsize': FONT_SIZE,
  1170. 'legend.fontsize': FONT_SIZE,
  1171. 'axes.titlesize': FONT_SIZE,
  1172. 'axes.linewidth': 1.5,
  1173. 'xtick.major.width': 1.5,
  1174. 'ytick.major.width': 1.5,
  1175. 'font.family': 'sans-serif',
  1176. 'font.sans-serif': ['Arial']
  1177. })
  1178. PALETTE = {'HH17': '#3182bd', 'HH20': '#de2d26'}
  1179. master_csv = "../data/csv/spatchcocked_measurements.csv"
  1180. df = pd.read_csv(master_csv)
  1181. df['stage'] = df['stage'].astype(str).str.upper()
  1182. def plot_binned_correlation(ax, data, x_col, y_col, xlabel, ylabel, n_bins=10):
  1183. """Calculates binned means, Pearson correlation, and plots to a specific axis."""
  1184. if x_col not in data.columns or y_col not in data.columns:
  1185. print(f"Skipping {y_col} vs {x_col}: columns missing from data.")
  1186. return
  1187. df_clean = data.dropna(subset=[x_col, y_col])
  1188. binned_data = []
  1189. for stage in ['HH17', 'HH20']:
  1190. stage_df = df_clean[df_clean['stage'] == stage].copy()
  1191. if len(stage_df) < n_bins:
  1192. continue
  1193. stage_df['x_bin'] = pd.qcut(stage_df[x_col], q=n_bins, duplicates='drop')
  1194. bin_means = stage_df.groupby('x_bin', observed=False)[[x_col, y_col]].mean().reset_index()
  1195. bin_means['stage'] = stage
  1196. bin_means = bin_means.dropna()
  1197. binned_data.append(bin_means)
  1198. if not binned_data:
  1199. return
  1200. binned_df = pd.concat(binned_data, ignore_index=True)
  1201. for stage in ['HH17', 'HH20']:
  1202. stage_binned = binned_df[binned_df['stage'] == stage]
  1203. if len(stage_binned) < 2:
  1204. continue
  1205. corr, pval = pearsonr(stage_binned[x_col], stage_binned[y_col])
  1206. p_text = "p < 0.001" if pval < 0.001 else f"p = {pval:.3f}"
  1207. legend_label = f"{stage} ($r$={corr:.2f}, {p_text})"
  1208. sns.regplot(
  1209. data=stage_binned, x=x_col, y=y_col, ax=ax,
  1210. color=PALETTE[stage],
  1211. scatter_kws={'s': 50, 'edgecolors': 'white', 'linewidths': 0.8},
  1212. line_kws={'linewidth': 2},
  1213. label=legend_label
  1214. )
  1215. ax.spines['top'].set_visible(False)
  1216. ax.spines['right'].set_visible(False)
  1217. for side in ['left', 'bottom']:
  1218. ax.spines[side].set_linewidth(1.5)
  1219. ax.set_xlabel(xlabel)
  1220. ax.set_ylabel(ylabel)
  1221. fmt = ticker.ScalarFormatter(useMathText=True)
  1222. fmt.set_scientific(True)
  1223. fmt.set_powerlimits((-3, 3))
  1224. ax.yaxis.set_major_formatter(fmt)
  1225. ax.legend(frameon=False, loc='lower center', bbox_to_anchor=(0.5, 1.05), ncol=1)
  1226. fig, axes = plt.subplots(1, 2, figsize=(6, 4), dpi=dpival)
  1227. plot_binned_correlation(
  1228. ax=axes[0], data=df,
  1229. x_col='thickness', y_col='Gauss_Curvature',
  1230. xlabel=r'Tissue Thickness [$\mu m$]',
  1231. ylabel='Gaussian Curvature'
  1232. )
  1233. plot_binned_correlation(
  1234. ax=axes[1], data=df,
  1235. x_col='thickness', y_col='Mean_Curvature',
  1236. xlabel=r'Tissue Thickness [$\mu m$]',
  1237. ylabel='Mean Curvature'
  1238. )
  1239. plt.tight_layout()
  1240. plt.savefig("correlation_1x2_thickness_curvatures.svg", format='svg', bbox_inches='tight')
  1241. plt.show()
  1242. # %%
  1243. import pandas as pd
  1244. import numpy as np
  1245. import matplotlib.pyplot as plt
  1246. import matplotlib.ticker as ticker
  1247. import matplotlib.colors as mcolors
  1248. import seaborn as sns
  1249. from scipy.stats import spearmanr
  1250. # Custom cyclic colormap: mirrors 'plasma' so ventral (±180°) is dark at both ends
  1251. # and dorsal (0°) is bright in the centre — matches the DV coordinate convention.
  1252. base_cmap = plt.cm.get_cmap('plasma', 128)
  1253. colors_half = base_cmap(np.linspace(0, 1, 128))
  1254. cyclic_colors = np.vstack((colors_half, colors_half[::-1]))
  1255. cyclic_plasma = mcolors.LinearSegmentedColormap.from_list('cyclic_plasma', cyclic_colors)
  1256. MASTER_CSV = "../data/csv/spatchcocked_measurements.csv"
  1257. N_ANGLE_BINS = 40
  1258. plt.rcParams.update({
  1259. 'font.size': 13,
  1260. 'axes.labelsize': 13,
  1261. 'axes.linewidth': 1.5,
  1262. 'xtick.major.width': 1.5,
  1263. 'ytick.major.width': 1.5,
  1264. 'svg.fonttype': 'none',
  1265. 'font.family': 'sans-serif',
  1266. 'font.sans-serif': ['Arial']
  1267. })
  1268. PROPS = [
  1269. ('Gauss_Curvature', r'$K$ [$\mu m^{-2}$]', -6, 0),
  1270. ('Mean_Curvature', r'$H$ [$\mu m^{-1}$]', -3, 1),
  1271. ('thickness', r'Thickness [$\mu m$]', 0, 2),
  1272. ('phh3', 'Local pH3+ cell count', 0, 3)
  1273. ]
  1274. df = pd.read_csv(MASTER_CSV)
  1275. df['stage'] = df['stage'].astype(str).str.upper()
  1276. fig, axes = plt.subplots(2, 2, figsize=(9, 8), dpi=100)
  1277. axes_flat = axes.flatten()
  1278. for col, base_label, exp, idx in PROPS:
  1279. ax = axes_flat[idx]
  1280. df_clean = df.dropna(subset=['angle_degrees', col])
  1281. bins = np.linspace(-180, 180, N_ANGLE_BINS + 1)
  1282. df_clean['angle_bin'] = pd.cut(df_clean['angle_degrees'], bins=bins)
  1283. grouped = df_clean.groupby('angle_bin', observed=False).agg({
  1284. col: lambda x: x[df_clean.loc[x.index, 'stage'] == 'HH17'].mean(),
  1285. 'angle_degrees': 'mean'
  1286. }).rename(columns={col: 'HH17', 'angle_degrees': 'bin_center'})
  1287. grouped['HH20'] = df_clean[df_clean['stage'] == 'HH20'].groupby('angle_bin', observed=False)[col].mean()
  1288. comp_df = grouped.dropna(subset=['HH17', 'HH20'])
  1289. corr, pval = spearmanr(comp_df['HH17'], comp_df['HH20'])
  1290. p_text = "p < 0.001" if pval < 0.001 else f"p = {pval:.3f}"
  1291. sns.regplot(
  1292. data=comp_df, x='HH17', y='HH20', ax=ax,
  1293. scatter=False, color='gray', line_kws={'linewidth': 1.5, 'linestyle': '--', 'alpha': 0.5}
  1294. )
  1295. scatter = ax.scatter(
  1296. comp_df['HH17'], comp_df['HH20'],
  1297. c=comp_df['bin_center'],
  1298. cmap=cyclic_plasma,
  1299. s=60, edgecolors='black', linewidths=0.5, alpha=0.9
  1300. )
  1301. ax.spines['top'].set_visible(False)
  1302. ax.spines['right'].set_visible(False)
  1303. label_suffix = f" ($\\times 10^{{{exp}}}$)" if exp != 0 else ""
  1304. ax.set_xlabel(f"HH17 {base_label}{label_suffix}")
  1305. ax.set_ylabel(f"HH20 {base_label}{label_suffix}")
  1306. ax.set_title(f"$\\rho$ = {corr:.2f} ({p_text})", fontsize=11, pad=10)
  1307. if exp != 0:
  1308. func = ticker.FuncFormatter(lambda x, pos, e=exp: f'{x/(10**e):g}')
  1309. ax.xaxis.set_major_formatter(func)
  1310. ax.yaxis.set_major_formatter(func)
  1311. if col == 'phh3':
  1312. ax.set_yticks([15, 20, 25])
  1313. cbar_ax = fig.add_axes([0.93, 0.75, 0.015, 0.18])
  1314. cbar = fig.colorbar(scatter, cax=cbar_ax)
  1315. cbar.set_label(' ', rotation=270, labelpad=15)
  1316. cbar.set_ticks([-175, 0, 175])
  1317. cbar.set_ticklabels(['V', 'D', 'V'])
  1318. plt.tight_layout(rect=[0, 0, 0.9, 1])
  1319. plt.savefig("Pattern_Correlation_Spearman.svg", format='svg', bbox_inches='tight')
  1320. plt.show()
  1321. # %% [markdown]
  1322. # ## Supplementary — pHH3 spot density validation
  1323. #
  1324. # Independent validation of pHH3+ spot density comparing HH17 (66 hr incubation)
  1325. # and HH20 (90 hr incubation) tissue sections. Data comes from raw Imaris spot
  1326. # detection (search radius R = 100 µm) on optical sections, prior to mesh
  1327. # projection. This confirms that the difference in pHH3+ density seen in the
  1328. # spatchcocked data (Fig 6) reflects a genuine biological signal rather than an
  1329. # artefact of the projection method.
  1330. #
  1331. # The dataset is small (n ≈ 20 sections per stage) and is embedded inline — it
  1332. # does not require a separate CSV file.
  1333. # %%
  1334. import pandas as pd
  1335. import io
  1336. import seaborn as sns
  1337. import matplotlib.pyplot as plt
  1338. from scipy import stats
  1339. csv_data = """Filename,Total_Spots,Search_Radius_r,Mean_Density,Median_Density,Std_Density,Max_Density
  1340. 66hr-FBMB-bottom1,81,100,4.02,3.0,2.77,16.0
  1341. 66hr-FBMB-bottom10,125,100,5.65,6.0,2.81,14.0
  1342. 66hr-FBMB-bottom11,71,100,3.69,4.0,1.98,9.0
  1343. 66hr-FBMB-bottom12,129,100,5.49,6.0,2.91,13.0
  1344. 66hr-FBMB-bottom13,82,100,4.02,4.0,2.25,10.0
  1345. 66hr-FBMB-bottom2,87,100,3.57,3.0,2.38,11.0
  1346. 66hr-FBMB-bottom3,74,100,5.58,6.0,2.43,11.0
  1347. 66hr-FBMB-bottom4,60,100,5.27,5.0,2.72,12.0
  1348. 66hr-FBMB-bottom8,126,100,4.34,4.0,2.38,11.0
  1349. 66hr-FBMB-bottom9,64,100,5.08,5.0,3.08,12.0
  1350. 66hr-FBMB-top2,69,100,4.56,4.0,2.75,12.0
  1351. 66hr-FBMB-top3,71,100,3.01,3.0,1.6,10.0
  1352. 66hr-FBMB-top4,81,100,4.87,5.0,2.66,14.0
  1353. 66hr-FBMB-top5,121,100,5.22,5.0,2.86,13.0
  1354. 66hr-FBMB-top6,128,100,6.86,7.0,3.85,16.0
  1355. 66hr-FBMB-top7,103,100,3.26,3.0,2.02,11.0
  1356. 66hr-FBMB-top8,65,100,4.6,4.0,2.38,12.0
  1357. 66hr-HB-bottom1,72,100,6.69,7.0,2.79,14.0
  1358. 66hr-HB-bottom2,45,100,9.56,10.0,4.08,20.0
  1359. 66hr-HB-bottom4,55,100,5.81,6.0,2.72,12.0
  1360. 66hr-HB-bottom6,81,100,6.84,7.0,3.64,18.0
  1361. FB-MB-90--Snap-1035,79,100,3.01,3.0,1.75,10.0
  1362. FB-MB-90-Snap-1030,75,100,2.83,3.0,1.53,8.0
  1363. FB-MB-90-Snap-1031,99,100,2.7,3.0,1.3,6.0
  1364. FB-MB-90-Snap-1032,78,100,2.93,3.0,1.28,6.0
  1365. FB-MB-90-Snap-1033,72,100,2.29,2.0,1.21,6.0
  1366. FB-MB-90-Snap-1034,59,100,2.76,3.0,1.33,6.0
  1367. FB-MB-90-Snap-1036,64,100,2.97,3.0,1.3,7.0
  1368. FB-MB-90-Snap-1038,73,100,2.64,3.0,1.31,7.0
  1369. FB-MB-90-Snap-1039,60,100,2.33,2.0,1.26,6.0
  1370. fb-mb-90-Snap-1054,66,100,3.34,3.0,1.42,7.0
  1371. fb-mb-90-Snap-1055,72,100,2.88,3.0,1.34,6.0
  1372. HB-90-Snap-1022,70,100,2.54,2.0,1.45,7.0
  1373. HB-90-Snap-1027,76,100,2.18,2.0,1.16,6.0
  1374. """
  1375. df = pd.read_csv(io.StringIO(csv_data))
  1376. def get_stage(filename):
  1377. filename_lower = filename.lower()
  1378. if '66' in filename_lower: return 'HH17'
  1379. elif '90' in filename_lower: return 'HH20'
  1380. return 'Unknown'
  1381. df['Stage'] = df['Filename'].apply(get_stage)
  1382. dpival = 100
  1383. PALETTE = {'HH17': '#3182bd', 'HH20': '#de2d26'}
  1384. STAGES = ['HH17', 'HH20']
  1385. plt.rcParams.update({
  1386. 'font.family': 'sans-serif',
  1387. 'font.sans-serif': ['Arial']
  1388. })
  1389. def set_common_style(ax, ylabel):
  1390. ax.spines['top'].set_visible(False)
  1391. ax.spines['right'].set_visible(False)
  1392. for side in ['left', 'bottom']:
  1393. ax.spines[side].set_linewidth(1.5)
  1394. ax.grid(False)
  1395. ax.set_xlabel('Stage', fontsize=13, fontweight='normal')
  1396. ax.set_ylabel(ylabel, fontsize=13, fontweight='normal')
  1397. ax.tick_params(labelsize=12, width=1.5)
  1398. def add_external_stat(ax, x1, x2, level, p_val):
  1399. base_y = 1.05
  1400. step = 0.08
  1401. y = base_y + (level * step)
  1402. ax.plot([x1, x1, x2, x2], [y, y+0.02, y+0.02, y],
  1403. lw=1.5, color='black', clip_on=False,
  1404. transform=ax.get_xaxis_transform())
  1405. if p_val < 0.001: text = '***'
  1406. elif p_val < 0.01: text = '**'
  1407. elif p_val < 0.05: text = '*'
  1408. else: text = 'ns'
  1409. ax.text((x1+x2)*0.5, y+0.02, text, ha='center', va='bottom',
  1410. color='black', fontsize=13, fontweight='normal',
  1411. transform=ax.get_xaxis_transform())
  1412. def plot_density(data, y_col, ylabel, filename, L=False):
  1413. plt.figure(figsize=(2, 4), dpi=dpival)
  1414. ax = sns.boxplot(data=data, x='Stage', y=y_col, hue='Stage', palette=PALETTE,
  1415. width=0.5, linewidth=1.5, order=STAGES, legend=False, showfliers=False)
  1416. if L:
  1417. ax.set_ylim(bottom=0)
  1418. g1 = data[data['Stage'] == 'HH17'][y_col].dropna()
  1419. g2 = data[data['Stage'] == 'HH20'][y_col].dropna()
  1420. _, p = stats.ttest_ind(g1, g2, equal_var=False)
  1421. add_external_stat(ax, 0, 1, 0, p)
  1422. set_common_style(ax, ylabel)
  1423. plt.subplots_adjust(top=0.8)
  1424. plt.rcParams['svg.fonttype'] = 'none'
  1425. plt.savefig(f"{filename}.svg", format='svg', bbox_inches='tight')
  1426. plot_density(
  1427. data=df,
  1428. y_col='Mean_Density',
  1429. ylabel='Spots density',
  1430. filename='HH17vsHH20_median_density_template',
  1431. L=True
  1432. )
  1433. plt.show()
  1434. # %% [markdown]
  1435. # ## Single-embryo heatmap
  1436. #
  1437. # Interpolates one scalar field from a single embryo onto a regular 500×500
  1438. # grid and displays it as a dorsoventral × rostrocaudal heatmap.
  1439. #
  1440. # **CSV**: `spatchcocked_measurements.csv` (or any CSV with the same columns)
  1441. # Columns required: `timepoint`, `norm_height`, `angle_degrees`, + the scalar
  1442. #
  1443. # **Parameters to adjust:**
  1444. # - `TARGET_TIMEPOINT` — date-stamp string identifying the embryo
  1445. # - `TARGET_PROPERTY` — column name to plot (see `property_map` below)
  1446. # - `master_csv` — path to the spatchcocked measurements CSV
  1447. # %%
  1448. import pandas as pd
  1449. import numpy as np
  1450. import matplotlib.pyplot as plt
  1451. import matplotlib.ticker as ticker
  1452. from scipy.interpolate import griddata
  1453. # --- Helper: symmetric colour range clipped at median ± sigma × std ---
  1454. def getTightercmap(values, sigma=3):
  1455. """Return (vmin, vmax) clipped at median ± sigma × std."""
  1456. return (np.median(values) - sigma * np.std(values),
  1457. np.median(values) + sigma * np.std(values))
  1458. # --- Property metadata: (axis label, colormap, symmetric range?, exponent) ---
  1459. property_map = {
  1460. "Gauss_Curvature": (r"Gaussian Curvature [$\mu m^{-2}$]", "PiYG", True, -6),
  1461. "Mean_Curvature": (r"Mean Curvature [$\mu m^{-1}$]", "PiYG", True, -3),
  1462. "K1": (r"Max principal k1 [$\mu m^{-1}$]", "Spectral_r", True, -3),
  1463. "K2": (r"Min principal k2 [$\mu m^{-1}$]", "Spectral_r", True, -3),
  1464. "thickness": (r"Thickness [$\mu m$]", "GnBu", False, 0),
  1465. "phh3": (r"Local pHH3+ cell count", "viridis", False, 0),
  1466. }
  1467. # --- Parameters ---
  1468. TARGET_TIMEPOINT = "2025-09-18-13-02" # embryo to plot
  1469. TARGET_PROPERTY = "Gauss_Curvature" # column to visualise (see property_map keys)
  1470. master_csv = "../data/csv/spatchcocked_measurements.csv"
  1471. # --- Load and filter ---
  1472. df_full = pd.read_csv(master_csv)
  1473. df = df_full[df_full["timepoint"] == TARGET_TIMEPOINT].copy()
  1474. print(f"Embryo {TARGET_TIMEPOINT}: {len(df)} vertices")
  1475. label, cmap_name, is_curv, exp = property_map[TARGET_PROPERTY]
  1476. # Rostrocaudal: flip so rostral (norm_height ≈ 1) appears at top of plot
  1477. y_coord = 1 - df["norm_height"] # 0 = rostral, 1 = caudal
  1478. x_coord = df["angle_degrees"] # −180 to +180
  1479. z = df[TARGET_PROPERTY]
  1480. # --- Interpolate onto a regular 500×500 grid ---
  1481. # Normalise both axes to [0,1] for griddata, then map back for display
  1482. y_norm = (y_coord - y_coord.min()) / (y_coord.max() - y_coord.min())
  1483. x_norm = (x_coord - x_coord.min()) / (x_coord.max() - x_coord.min())
  1484. grid_size = 500
  1485. xi, yi = np.meshgrid(np.linspace(0, 1, grid_size),
  1486. np.linspace(0, 1, grid_size))
  1487. zi = griddata((x_norm, y_norm), z, (xi, yi), method="linear")
  1488. # --- Colour limits ---
  1489. vmin, vmax = getTightercmap(z, sigma=3)
  1490. if is_curv:
  1491. vlim = max(abs(vmin), abs(vmax))
  1492. vmin, vmax = -vlim, vlim
  1493. else:
  1494. vmin = 0 # non-curvature fields start at zero
  1495. # --- Plot ---
  1496. line_width = 1.5
  1497. plt.rcParams.update({
  1498. "font.size": 12,
  1499. "axes.labelsize": 14,
  1500. "axes.titlesize": 14,
  1501. "xtick.major.width": line_width,
  1502. "ytick.major.width": line_width,
  1503. "axes.linewidth": line_width,
  1504. })
  1505. fig = plt.figure(figsize=(4, 4), dpi=100)
  1506. ax = fig.add_axes([0.15, 0.1, 0.65, 0.8])
  1507. im = ax.imshow(zi,
  1508. extent=[x_coord.min(), x_coord.max(),
  1509. y_coord.min(), y_coord.max()],
  1510. origin="lower", aspect="auto",
  1511. cmap=cmap_name, vmin=vmin, vmax=vmax)
  1512. ax.invert_yaxis() # rostral at top
  1513. ax.spines["top"].set_visible(False)
  1514. ax.spines["right"].set_visible(False)
  1515. ax.set_xticks([-180, -90, 0, 90, 180])
  1516. ax.set_xlabel("Dorsoventral axis")
  1517. ax.set_ylabel("Rostrocaudal axis")
  1518. cbar = plt.colorbar(im, ax=ax, orientation="vertical", pad=0.05)
  1519. # Scientific notation for curvature values or very small numbers
  1520. if is_curv or z.abs().max() < 0.1:
  1521. mult = 10 ** exp
  1522. cbar.ax.yaxis.set_major_formatter(
  1523. ticker.FuncFormatter(lambda val, pos, m=mult: f"{val/m:g}")
  1524. )
  1525. cbar.ax.text(0.5, 1.02, f"$\\times 10^{{{exp}}}$",
  1526. transform=cbar.ax.transAxes, ha="center", va="bottom")
  1527. cbar.set_label(label, labelpad=10)
  1528. plt.show()
  1529. # %% [markdown]
  1530. # ## Stage-averaged binned heatmap
  1531. #
  1532. # Bins all embryos of one stage into a 50-row RC grid × 10°-wide DV columns,
  1533. # takes the mean of each bin, and displays as a seaborn heatmap.
  1534. # This is the multi-embryo average map used in the paper figures.
  1535. #
  1536. # **Parameters to adjust:**
  1537. # - `TARGET_PROPERTY` — column to visualise (see `property_map` keys)
  1538. # - `TARGET_STAGE` — `"hh17"` or `"hh20"` (lowercase)
  1539. # - `master_csv` — path to the spatchcocked measurements CSV
  1540. #
  1541. # **Output:** SVG saved as `average_{TARGET_PROPERTY}_{TARGET_STAGE}.svg`
  1542. # %%
  1543. # %%
  1544. import pandas as pd
  1545. import numpy as np
  1546. import matplotlib.pyplot as plt
  1547. import matplotlib.ticker as ticker
  1548. import seaborn as sns
  1549. # %%
  1550. # ==========================================
  1551. # CONFIGURATION & TARGETS
  1552. # ==========================================
  1553. # TARGET_PROPERTY = "Gauss_Curvature"
  1554. # TARGET_STAGE = "hh17"
  1555. # TARGET_PROPERTY = "Mean_Curvature"
  1556. # TARGET_STAGE = "hh20"
  1557. # TARGET_PROPERTY = "thickness"
  1558. # TARGET_STAGE = "hh17"
  1559. TARGET_PROPERTY = "Gauss_Curvature"
  1560. TARGET_STAGE = "hh20"
  1561. master_csv = "../data/csv/spatchcocked_measurements.csv"
  1562. dpival = 100
  1563. # Styling
  1564. line_width = 1.5
  1565. font_size = 13
  1566. plt.rcParams.update({
  1567. 'font.size': font_size,
  1568. 'axes.labelsize': font_size + 2,
  1569. 'xtick.major.width': line_width,
  1570. 'ytick.major.width': line_width,
  1571. 'xtick.major.size': 6,
  1572. 'ytick.major.size': 6,
  1573. 'axes.linewidth': line_width,
  1574. 'svg.fonttype': 'none'
  1575. })
  1576. property_map = {
  1577. "Gauss_Curvature": (r"Gaussian Curvature [$\mu m^{-2}$]", "PiYG", True, -5),
  1578. "Mean_Curvature": (r"Mean Curvature [$\mu m^{-1}$]", "PiYG", True, -3),
  1579. "thickness": (r"Thickness [$\mu m$]", "GnBu", False, 0),
  1580. "phh3": (r"Local pH3+ cell count", "viridis", False, 0),
  1581. }
  1582. label, cmap_name, is_curv, exponent = property_map[TARGET_PROPERTY]
  1583. multiplier = 10 ** exponent
  1584. # ==========================================
  1585. # DATA PROCESSING
  1586. # ==========================================
  1587. df = pd.read_csv(master_csv)
  1588. # Bin vertices into RC (50 bins, rostral at top) × DV (10° bins)
  1589. df['rc_bin'] = pd.cut(1 - df['norm_height'], bins=50)
  1590. df['bin_center'] = df['rc_bin'].apply(lambda x: x.mid).astype(float)
  1591. df['angle_bin'] = pd.cut(df['angle_degrees'], bins=np.arange(-180, 190, 10))
  1592. df['angle_bin_center'] = df['angle_bin'].apply(lambda x: x.mid).astype(float)
  1593. df_stage = df[df['stage'] == TARGET_STAGE].copy()
  1594. print(f"Stage {TARGET_STAGE}: {len(df_stage)} vertices")
  1595. # ==========================================
  1596. # PLOTTING
  1597. # ==========================================
  1598. fig = plt.figure(figsize=(5, 5), dpi=dpival)
  1599. ax = fig.add_axes([0.15, 0.15, 0.7, 0.75])
  1600. # Mean per bin, pivot to 2D; interpolate gaps and fill remaining NaN
  1601. grouped = (df_stage
  1602. .groupby(['bin_center', 'angle_bin_center'], observed=False)[TARGET_PROPERTY]
  1603. .mean()
  1604. .reset_index())
  1605. pivot = grouped.pivot(index='bin_center', columns='angle_bin_center', values=TARGET_PROPERTY)
  1606. pivot = pivot.interpolate(axis=1).fillna(0).sort_index(ascending=True)
  1607. # Colour limits: 95th percentile of absolute values
  1608. raw_data = df_stage[TARGET_PROPERTY].dropna()
  1609. v_max_avg = np.percentile(np.abs(raw_data), 95)
  1610. if exponent != 0:
  1611. v_max_avg = np.ceil(v_max_avg / (multiplier * 0.5)) * (multiplier * 0.5)
  1612. v_min_avg = -v_max_avg if is_curv else 0
  1613. sns.heatmap(
  1614. pivot,
  1615. cmap=cmap_name,
  1616. vmin=v_min_avg, vmax=v_max_avg,
  1617. ax=ax,
  1618. linewidths=0.5,
  1619. linecolor='white',
  1620. xticklabels=False,
  1621. yticklabels=False,
  1622. cbar=False
  1623. )
  1624. # ==========================================
  1625. # STYLING & COLORBAR
  1626. # ==========================================
  1627. for _, spine in ax.spines.items():
  1628. spine.set_visible(True)
  1629. spine.set_linewidth(line_width)
  1630. ax.spines['top'].set_visible(False)
  1631. ax.spines['right'].set_visible(False)
  1632. ax.set_xlabel('Dorsoventral axis', labelpad=10)
  1633. ax.set_ylabel('Rostrocaudal axis', labelpad=10)
  1634. # Index-based tick positions — find nearest bin centre to each target value
  1635. target_y = [0.0, 0.2, 0.4, 0.6, 0.8, 1.0]
  1636. target_x = [-180, -90, 0, 90, 180]
  1637. y_pos = [np.abs(pivot.index.values - t).argmin() + 0.5 for t in target_y]
  1638. x_pos = [np.abs(pivot.columns.values - t).argmin() + 0.5 for t in target_x]
  1639. ax.set_yticks(y_pos)
  1640. ax.set_yticklabels([f"{t:.1f}" for t in target_y], rotation=0)
  1641. ax.set_xticks(x_pos)
  1642. ax.set_xticklabels([str(t) for t in target_x])
  1643. # Colorbar
  1644. cbar = fig.colorbar(ax.collections[0], ax=ax, orientation='vertical', pad=0.05)
  1645. cbar.outline.set_linewidth(line_width)
  1646. cbar.ax.tick_params(width=line_width, size=6)
  1647. cbar.set_label(label, labelpad=10)
  1648. if exponent != 0:
  1649. cbar.ax.yaxis.set_major_formatter(
  1650. ticker.FuncFormatter(lambda x, pos: f'{int(round(x / multiplier))}')
  1651. )
  1652. cbar.ax.text(0.5, 1.03, f'$\\times 10^{{{exponent}}}$',
  1653. transform=cbar.ax.transAxes, ha='center', va='bottom')
  1654. # ax.set_title(f"Stage Average: {TARGET_STAGE}", pad=20)
  1655. plt.savefig(f"average_{TARGET_PROPERTY}_{TARGET_STAGE}.svg", format='svg', bbox_inches='tight')
  1656. plt.show()

06_figure_plots.ipynb at commit 3306f17, under MIT · at the source

Overview

Authors: Nimesh Chahare1, Chieko Imamura1, Nandan L. Nerurkar1
  1. Department of Biomedical Engineering, Columbia University, New York, NY, USA
Institutions: Columbia University (United States)
Journal: Biophysical reports, volume 6, issue 3, article 100280
Dates: received 20 May 2026; accepted 3 August 2026; published online 5 August 2026; in print September 2026
Type: Research article · Language: English
License: CC BY-NC-ND
Identifiers: DOI 10.1016/j.bpr.2026.100280 · PMID 42556618 · PMCID PMC13521015 · OpenAlex W7196991408
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: structural MRI / diffusion (modality), other (organism), developmental (subfield)
Methods: Connectivity, Spectral & time-frequency
MeSH: Neural Tube*, Skull*, Animals, Cell Proliferation, Chick Embryo (* major topic)
Topic: Neurogenesis and neuroplasticity mechanisms (Developmental Neuroscience, Neuroscience), according to OpenAlex
Funding: NIH (R35 GM142995)
Citations: not cited yet (Europe PMC); 67 references in the paper

Abstract

The abstract is not reproduced here: the paper's license (CC BY-NC-ND) does not allow it. Read it in the paper, at the publisher or on Europe PMC.

Repository

Its files are read in the Code ↔ Paper reader above, with 16 matches between paragraphs and lines of code.

nchahare/spatchcocking

License: MIT
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: 3306f17988f70473ab3b944a391e26828f64a028, 21 May 2026
Languages: Python (13), Jupyter (8)
Size: 64 files, 21 scripts
Software Heritage: not archived
Found in: “Data and code availability”
Holds: README, license file, CITATION.cff, environment (requirements.txt, setup.py, finite_element/requirements_fem.txt), continuous integration, documentation, 8 notebooks
Not found: tests
Tools: NumPy (15 files), Matplotlib (7 files), SciPy (5 files), pandas (4 files), seaborn (2 files), statannotations (2 files), scikit-image (1 file), tifffile (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
23 files

The paper's code and data availability statement is in the Data section.

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 21 scripts, each with its path and the digest of its content;
  • 16 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

No dataset and no data link were found in the paper.

Code and data availability statement

The paper has a code and data availability statement. Its license (CC BY-NC-ND) does not allow reproducing it here; in short, from what the harvester recognized in it:

Read it in the paper: doi.org/10.1016/j.bpr.2026.100280.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 1, 27 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 3 authors, 5 MeSH terms, 1 funder, 64 references.

Cite

This paper

Chahare, N., Imamura, C., & Nerurkar, N. L. (2026). Morphometric analysis reveals that the chick cranial neural tube expands as an active shell. Biophysical reports, 6(3), 100280. https://doi.org/10.1016/j.bpr.2026.100280

BibTeX

@article{chahare2026morphometric,
author = {Chahare, Nimesh and Imamura, Chieko and Nerurkar, Nandan L.},
title = {{Morphometric analysis reveals that the chick cranial neural tube expands as an active shell}},
journal = {Biophysical reports},
year = {2026},
month = aug,
volume = {6},
number = {3},
pages = {100280},
publisher = {Elsevier},
issn = {2667-0747},
doi = {10.1016/j.bpr.2026.100280},
url = {https://doi.org/10.1016/j.bpr.2026.100280},
pmid = {42556618},
pmcid = {PMC13521015}
}

RIS

TY - JOUR
AU - Chahare, Nimesh
AU - Imamura, Chieko
AU - Nerurkar, Nandan L.
TI - Morphometric analysis reveals that the chick cranial neural tube expands as an active shell
T2 - Biophysical reports
J2 - Biophys Rep (N Y)
PY - 2026
DA - 2026/08/05
VL - 6
IS - 3
SP - 100280
SN - 2667-0747
PB - Elsevier
DO - 10.1016/j.bpr.2026.100280
UR - https://doi.org/10.1016/j.bpr.2026.100280
LA - en
ER -

CSL-JSON

{
"id": "10.1016/j.bpr.2026.100280",
"type": "article-journal",
"title": "Morphometric analysis reveals that the chick cranial neural tube expands as an active shell",
"container-title": "Biophysical reports",
"author": [
{
"family": "Chahare",
"given": "Nimesh"
},
{
"family": "Imamura",
"given": "Chieko"
},
{
"family": "Nerurkar",
"given": "Nandan L."
}
],
"container-title-short": "Biophys Rep (N Y)",
"volume": "6",
"issue": "3",
"page": "100280",
"DOI": "10.1016/j.bpr.2026.100280",
"PMID": "42556618",
"PMCID": "PMC13521015",
"ISSN": "2667-0747",
"publisher": "Elsevier",
"URL": "https://doi.org/10.1016/j.bpr.2026.100280",
"language": "en",
"issued": {
"date-parts": [
[
2026,
8,
5
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi: [code]
Real-time closed-loop feedback system for mouse mesoscale cortical signal and movement control
Journal: eLife
In common: statannotations, tifffile, scikit-image, 5 other tools
[2] doi:10.1126/sciadv.aeb4205 [code]
Learning stochastic dynamics and cell-fate landscapes from single-cell snapshots via optimal transport.
Journal: Science advances
In common: statannotations, tifffile, scikit-image, 5 other tools
[3] doi:10.64898/2026.03.30.715222 [code]
Synthetic lumen rounding directs neural progenitor division mode
Journal: bioRxiv (preprint)
In common: tifffile, scikit-image, seaborn, 4 other tools, developmental, 1 reference
[4] doi:10.1364/boe.605322 [code]
Generalized plaque digitization framework for multi-dimensional mesoscopic images.
Journal: Biomedical optics express
In common: tifffile, scikit-image, seaborn, 4 other tools, 1 reference
[5] doi:10.1186/s12880-026-02335-x [code]
Automatic lateral ventricle and choroid plexus segmentation method in infant brain MR images.
Journal: BMC medical imaging
In common: tifffile, scikit-image, seaborn, 4 other tools, developmental, structural MRI / diffusion
[6] doi:10.1117/1.nph.13.3.035006 [code]
Characterizing developmental changes in infant habituation using functional change point detection.
Journal: Neurophotonics
In common: statannotations, scikit-image, seaborn, 4 other tools, developmental
[7] doi:10.1038/s41467-026-73770-1 [code]
Non-coding structural variants disrupt FOXG1 transcriptional regulation in early neurodevelopment.
Journal: Nature communications
In common: statannotations, scikit-image, seaborn, 4 other tools, developmental
[8] doi:10.1002/alz.71649 [code]
Postmortem brain MRI reveals differential associations of subcortical and limbic volumes with cortical thinning and neurodegenerative pathologies.
Journal: Alzheimer's & dementia : the journal of the Alzheimer's Association
In common: statannotations, scikit-image, seaborn, 4 other tools, structural MRI / diffusion
[9] doi:10.1016/j.stem.2026.05.005 [code]
Generation of human appetite-regulating neurons and tanycytes from pluripotent stem cells.
Journal: Cell stem cell
In common: seaborn, pandas, SciPy, 2 other tools, 3 references
[10] doi:10.1038/s41467-026-76569-2 [code]
Self-organization of vascularized muscle from bovine embryonic stem cells.
Journal: Nature communications
In common: tifffile, scikit-image, seaborn, 4 other tools, other

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.

Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.