Skip to content

Commit f5c1d9e

Browse files
committed
Add comprehensive ROI analysis visualization functions for two-photon data, including individual and averaged response plots, with support for multiple channels.
1 parent 9b99050 commit f5c1d9e

File tree

1 file changed

+346
-0
lines changed

1 file changed

+346
-0
lines changed
Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
# using GLMakie
2+
# using Statistics
3+
# using ElectroPhysiology, PhysiologyAnalysis
4+
5+
"""
6+
plot_roi_analysis(data::Experiment{TWO_PHOTON};
7+
stim_idx::Int=1, channel_idx::Union{Int,Nothing}=nothing)
8+
9+
Create a comprehensive visualization of ROI analysis results for a single stimulus event including:
10+
1. ROI map showing significant ROIs weighted by their amplitude, overlaid on max projection
11+
2. Individual dF/F traces for significant ROIs
12+
3. Mean trace with standard deviation ribbon
13+
14+
If channel_idx is provided, only that channel will be shown. Otherwise, all channels will be displayed.
15+
16+
Returns a Figure object containing all plots.
17+
"""
18+
function plot_roi_analysis(data::Experiment{TWO_PHOTON, T};
19+
stim_idx::Int=1, channel_idx::Union{Int,Nothing}=nothing) where {T <: Real}
20+
21+
@assert haskey(data.HeaderDict, "ROI_Analysis") "Data must contain ROI analysis results in HeaderDict"
22+
23+
analysis = data.HeaderDict["ROI_Analysis"]
24+
25+
# Plot max projection as background
26+
xlims = data.HeaderDict["xrng"]
27+
ylims = data.HeaderDict["yrng"]
28+
29+
# Determine which channels to process
30+
channels_to_process = isnothing(channel_idx) ? analysis.channels : [channel_idx]
31+
n_channels = length(channels_to_process)
32+
33+
# Create figure with layout
34+
fig = Figure(size=(1200 * n_channels, 800))
35+
36+
# Process each channel
37+
for (ch_idx, channel) in enumerate(channels_to_process)
38+
# Get significant ROIs for this channel and stimulus
39+
sig_rois = get_significant_rois(analysis, stim_idx, channel)
40+
41+
# Create a 2x2 grid for this channel
42+
gl_channel = fig[1, ch_idx] = GridLayout()
43+
44+
# 1. ROI amplitude map with max projection (top left)
45+
ax1 = Axis(gl_channel[1,1], title="Channel $channel Response Map (Stimulus $stim_idx)",
46+
xlabel="X Position (μm)", ylabel="Y Position (μm)",
47+
aspect=DataAspect())
48+
49+
# Get max projection of original data
50+
max_proj = project(data, dims = (3))[:,:,1,channel]
51+
max_proj = rotr90(max_proj)
52+
norm_max_proj = (max_proj .- minimum(max_proj)) ./ (maximum(max_proj) - minimum(max_proj))
53+
hm1 = heatmap!(ax1, xlims, ylims, norm_max_proj)
54+
Colorbar(gl_channel[1,2], hm1, label="dF/F", width=15, vertical=true)
55+
56+
# Create a sub-grid for the traces
57+
gl_traces = gl_channel[1,3] = GridLayout()
58+
59+
# 2. Mean trace with std ribbon (top right, left half)
60+
ax2 = Axis(gl_traces[1,1], title="Mean Response ± STD (Stimulus $stim_idx)",
61+
xlabel="Time (s)", ylabel="ΔF/F")
62+
63+
if !isempty(sig_rois)
64+
# Get all significant traces and calculate statistics
65+
sig_traces = [filter(t -> t.channel == channel && t.stimulus_index == stim_idx, analysis.rois[roi])[1].dfof for roi in sig_rois]
66+
t_series = filter(t -> t.channel == channel && t.stimulus_index == stim_idx, analysis.rois[first(sig_rois)])[1].t_series
67+
68+
mean_trace = mean(sig_traces)
69+
std_trace = std(sig_traces)
70+
71+
# Plot mean and standard deviation
72+
band!(ax2, t_series,
73+
mean_trace .- std_trace,
74+
mean_trace .+ std_trace,
75+
color=(:blue, 0.3))
76+
lines!(ax2, t_series, mean_trace,
77+
color=:blue, linewidth=2,
78+
label="Mean (n=$(length(sig_rois)))")
79+
end
80+
81+
# 3. Individual traces (top right, right half)
82+
ax3 = Axis(gl_traces[2,1], title="Individual ROI Traces (Stimulus $stim_idx)",
83+
xlabel="Time (s)", ylabel="ΔF/F")
84+
85+
if !isempty(sig_rois)
86+
# Plot each significant ROI trace
87+
colors = cgrad(:viridis, length(sig_rois), categorical=true)
88+
for (i, roi_id) in enumerate(sig_rois)
89+
traces = filter(t -> t.channel == channel && t.stimulus_index == stim_idx, analysis.rois[roi_id])
90+
trace = traces[1]
91+
lines!(ax3, trace.t_series, trace.dfof,
92+
color=colors[i], alpha=0.5)
93+
end
94+
end
95+
96+
# Link y-axes of the traces
97+
linkyaxes!(ax2, ax3)
98+
99+
# Get ROI mask and create weighted map
100+
roi_mask = getROImask(data)
101+
roi_mask = rotr90(roi_mask)
102+
weighted_mask = zeros(size(roi_mask))
103+
tau_off_mask = zeros(size(roi_mask))
104+
105+
for roi_id in keys(analysis.rois)
106+
if roi_id in sig_rois
107+
traces = filter(t -> t.channel == channel && t.stimulus_index == stim_idx, analysis.rois[roi_id])
108+
trace = traces[1]
109+
max_response = maximum(trace.dfof)
110+
weighted_mask[roi_mask .== roi_id] .= max_response
111+
if !isnothing(trace.fit_parameters) && length(trace.fit_parameters) >= 3
112+
tau_off_mask[roi_mask .== roi_id] .= trace.fit_parameters[3]
113+
end
114+
end
115+
end
116+
117+
# 4. Weighted response map (bottom left)
118+
ax_weighted = Axis(gl_channel[2,1], title="Channel $channel Weighted Response (Stimulus $stim_idx)",
119+
xlabel="X Position (μm)", ylabel="Y Position (μm)",
120+
aspect=DataAspect())
121+
122+
if !isempty(sig_rois)
123+
hm_weighted = heatmap!(ax_weighted, xlims, ylims, weighted_mask,
124+
colormap=:viridis,
125+
colorrange=(0, maximum(weighted_mask)))
126+
Colorbar(gl_channel[2,2], hm_weighted, label="dF/F", width=15, vertical=true)
127+
else
128+
text!(ax_weighted, "No significant ROIs found",
129+
position=(mean(xlims), mean(ylims)),
130+
align=(:center, :center),
131+
color=:red)
132+
end
133+
134+
# 5. Tau Off map (bottom right)
135+
ax_tau = Axis(gl_channel[2,3], title="Channel $channel Tau Off (Stimulus $stim_idx)",
136+
xlabel="X Position (μm)", ylabel="Y Position (μm)",
137+
aspect=DataAspect())
138+
139+
if !isempty(sig_rois)
140+
hm_tau = heatmap!(ax_tau, xlims, ylims, tau_off_mask,
141+
colormap=:viridis,
142+
colorrange=(0, maximum(filter(!iszero, tau_off_mask))))
143+
Colorbar(gl_channel[2,4], hm_tau, label="Tau Off (s)", width=15, vertical=true)
144+
else
145+
text!(ax_tau, "No significant ROIs found",
146+
position=(mean(xlims), mean(ylims)),
147+
align=(:center, :center),
148+
color=:red)
149+
end
150+
151+
# Add stimulus time indicator if available
152+
if haskey(analysis.analysis_parameters, :delay_time)
153+
delay_time = analysis.analysis_parameters[:delay_time]
154+
vlines!(ax2, [delay_time], color=:red, linestyle=:dash, label="Stimulus")
155+
vlines!(ax3, [delay_time], color=:red, linestyle=:dash, label="Stimulus")
156+
end
157+
158+
# Add legend only for mean response
159+
axislegend(ax2, position=:rt)
160+
end
161+
162+
return fig
163+
end
164+
165+
"""
166+
plot_roi_analysis_averaged(data::Experiment{TWO_PHOTON};
167+
channel_idx::Union{Int,Nothing}=nothing)
168+
169+
Create a visualization showing the average responses across all stimuli for each ROI, including:
170+
1. ROI map showing significant ROIs weighted by their average amplitude across stimuli
171+
2. Average dF/F traces across all stimuli for each significant ROI
172+
3. Overall mean trace with standard deviation ribbon
173+
174+
If channel_idx is provided, only that channel will be shown. Otherwise, all channels will be displayed.
175+
176+
Returns a Figure object containing all plots.
177+
"""
178+
function plot_roi_analysis_averaged(data::Experiment{TWO_PHOTON, T};
179+
channel_idx::Union{Int,Nothing}=nothing) where {T <: Real}
180+
181+
@assert haskey(data.HeaderDict, "ROI_Analysis") "Data must contain ROI analysis results in HeaderDict"
182+
183+
analysis = data.HeaderDict["ROI_Analysis"]
184+
# Plot max projection as background
185+
xlims = data.HeaderDict["xrng"]
186+
ylims = data.HeaderDict["yrng"]
187+
188+
# Determine which channels to process
189+
channels_to_process = isnothing(channel_idx) ? analysis.channels : [channel_idx]
190+
n_channels = length(channels_to_process)
191+
192+
# Create figure with layout
193+
fig = Figure(size=(1200 * n_channels, 800))
194+
195+
# Process each channel
196+
for (ch_idx, channel) in enumerate(channels_to_process)
197+
# Get significant ROIs for this channel (significant in any stimulus)
198+
sig_rois = unique([id for (id, traces) in analysis.rois
199+
if any(t -> t.channel == channel && t.is_significant, traces)])
200+
201+
# Create a 2x2 grid for this channel
202+
gl_channel = fig[1, ch_idx] = GridLayout()
203+
204+
# 1. ROI amplitude map with max projection (top left)
205+
ax1 = Axis(gl_channel[1,1], title="Channel $channel Response Map (Averaged)",
206+
xlabel="X Position (μm)", ylabel="Y Position (μm)",
207+
aspect=DataAspect())
208+
209+
# Get max projection of original data
210+
max_proj = project(data, dims = (3))[:,:,1,channel]
211+
max_proj = rotr90(max_proj)
212+
norm_max_proj = (max_proj .- minimum(max_proj)) ./ (maximum(max_proj) - minimum(max_proj))
213+
hm1 = heatmap!(ax1, xlims, ylims, norm_max_proj)
214+
Colorbar(gl_channel[1,2], hm1, label="dF/F", width=15, vertical=true)
215+
216+
# Create a sub-grid for the traces
217+
gl_traces = gl_channel[1,3] = GridLayout()
218+
219+
# 2. Mean trace with std ribbon (top right, left half)
220+
ax2 = Axis(gl_traces[1,1], title="Mean Response ± STD (Averaged)",
221+
xlabel="Time (s)", ylabel="ΔF/F")
222+
223+
if !isempty(sig_rois)
224+
# Calculate average traces across all stimuli for each ROI
225+
avg_traces = []
226+
t_series = nothing
227+
228+
for roi_id in sig_rois
229+
traces = filter(t -> t.channel == channel, analysis.rois[roi_id])
230+
if !isempty(traces)
231+
# Average the traces for this ROI across all stimuli
232+
roi_avg = mean(t.dfof for t in traces)
233+
push!(avg_traces, roi_avg)
234+
if isnothing(t_series)
235+
t_series = traces[1].t_series
236+
end
237+
end
238+
end
239+
240+
if !isempty(avg_traces)
241+
mean_trace = mean(avg_traces)
242+
std_trace = std(avg_traces)
243+
244+
# Plot mean and standard deviation
245+
band!(ax2, t_series,
246+
mean_trace .- std_trace,
247+
mean_trace .+ std_trace,
248+
color=(:blue, 0.3))
249+
lines!(ax2, t_series, mean_trace,
250+
color=:blue, linewidth=2,
251+
label="Mean (n=$(length(sig_rois)))")
252+
end
253+
end
254+
255+
# 3. Individual averaged traces (top right, right half)
256+
ax3 = Axis(gl_traces[2,1], title="Individual ROI Traces (Averaged)",
257+
xlabel="Time (s)", ylabel="ΔF/F")
258+
259+
if !isempty(sig_rois)
260+
# Plot each ROI's average trace
261+
colors = cgrad(:viridis, length(sig_rois), categorical=true)
262+
for (i, roi_id) in enumerate(sig_rois)
263+
traces = filter(t -> t.channel == channel, analysis.rois[roi_id])
264+
if !isempty(traces)
265+
roi_avg = mean(t.dfof for t in traces)
266+
lines!(ax3, traces[1].t_series, roi_avg,
267+
color=colors[i], alpha=0.5)
268+
end
269+
end
270+
end
271+
272+
# Link y-axes of the traces
273+
linkyaxes!(ax2, ax3)
274+
275+
# Get ROI mask and create weighted map
276+
roi_mask = getROImask(data)
277+
roi_mask = rotr90(roi_mask)
278+
weighted_mask = zeros(size(roi_mask))
279+
tau_off_mask = zeros(size(roi_mask))
280+
281+
for roi_id in keys(analysis.rois)
282+
if roi_id in sig_rois
283+
traces = filter(t -> t.channel == channel, analysis.rois[roi_id])
284+
if !isempty(traces)
285+
# Average the maximum response across all stimuli
286+
max_responses = [maximum(t.dfof) for t in traces]
287+
avg_max_response = mean(max_responses)
288+
weighted_mask[roi_mask .== roi_id] .= avg_max_response
289+
290+
# Average tau_off across all stimuli
291+
tau_offs = [t.fit_parameters[3] for t in traces
292+
if !isnothing(t.fit_parameters) && length(t.fit_parameters) >= 3]
293+
if !isempty(tau_offs)
294+
tau_off_mask[roi_mask .== roi_id] .= mean(tau_offs)
295+
end
296+
end
297+
end
298+
end
299+
300+
# 4. Weighted response map (bottom left)
301+
ax_weighted = Axis(gl_channel[2,1], title="Channel $channel Weighted Response (Averaged)",
302+
xlabel="X Position (μm)", ylabel="Y Position (μm)",
303+
aspect=DataAspect())
304+
305+
if !isempty(sig_rois)
306+
hm_weighted = heatmap!(ax_weighted, xlims, ylims, weighted_mask,
307+
colormap=:viridis,
308+
colorrange=(0, maximum(weighted_mask)))
309+
Colorbar(gl_channel[2,2], hm_weighted, label="dF/F", width=15, vertical=true)
310+
else
311+
text!(ax_weighted, "No significant ROIs found",
312+
position=(mean(xlims), mean(ylims)),
313+
align=(:center, :center),
314+
color=:red)
315+
end
316+
317+
# 5. Tau Off map (bottom right)
318+
ax_tau = Axis(gl_channel[2,3], title="Channel $channel Tau Off (Averaged)",
319+
xlabel="X Position (μm)", ylabel="Y Position (μm)",
320+
aspect=DataAspect())
321+
322+
if !isempty(sig_rois)
323+
hm_tau = heatmap!(ax_tau, xlims, ylims, tau_off_mask,
324+
colormap=:viridis,
325+
colorrange=(0, maximum(filter(!iszero, tau_off_mask))))
326+
Colorbar(gl_channel[2,4], hm_tau, label="Tau Off (s)", width=15, vertical=true)
327+
else
328+
text!(ax_tau, "No significant ROIs found",
329+
position=(mean(xlims), mean(ylims)),
330+
align=(:center, :center),
331+
color=:red)
332+
end
333+
334+
# Add stimulus time indicator if available
335+
if haskey(analysis.analysis_parameters, :delay_time)
336+
delay_time = analysis.analysis_parameters[:delay_time]
337+
vlines!(ax2, [delay_time], color=:red, linestyle=:dash, label="Stimulus")
338+
vlines!(ax3, [delay_time], color=:red, linestyle=:dash, label="Stimulus")
339+
end
340+
341+
# Add legend only for mean response
342+
axislegend(ax2, position=:rt)
343+
end
344+
345+
return fig
346+
end

0 commit comments

Comments
 (0)