forked from Achuan-2/calcium_deltaF_caculate
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathCalciumDeltaFCaculator.m
More file actions
1363 lines (1222 loc) · 68.9 KB
/
CalciumDeltaFCaculator.m
File metadata and controls
1363 lines (1222 loc) · 68.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
classdef CalciumDeltaFCaculator < matlab.apps.AppBase
% Properties that correspond to app components
properties (Access = public)
UIFigure matlab.ui.Figure
FileOperationsPanel matlab.ui.container.Panel
FramerateHzLabel matlab.ui.control.Label
FramerateEditField matlab.ui.control.NumericEditField
LoadDataButton matlab.ui.control.Button
LoadedFileLabel matlab.ui.control.Label
DeltaFOverFCalculatePanel matlab.ui.container.Panel
CalculateZScoreCheckBox matlab.ui.control.CheckBox
BaselineMethodDropDownLabel matlab.ui.control.Label
BaselineMethodDropDown matlab.ui.control.DropDown
PercentileEditFieldLabel matlab.ui.control.Label
PercentileEditField matlab.ui.control.EditField
BaselineTimeLabel matlab.ui.control.Label
BaselineTimeEditField matlab.ui.control.EditField
PolynomialOrderLabel matlab.ui.control.Label
PolynomialOrderEditField matlab.ui.control.NumericEditField
MovingWindowLabel matlab.ui.control.Label
MovingWindowEditField matlab.ui.control.NumericEditField
MovingPercentileLabel matlab.ui.control.Label
MovingPercentileEditField matlab.ui.control.NumericEditField
RunAnalysisButton matlab.ui.control.Button
NeuronDisplayPanel matlab.ui.container.Panel
SelectNeuronLabel matlab.ui.control.Label
NeuronDropDown matlab.ui.control.DropDown
PreviousNeuronButton matlab.ui.control.Button
NextNeuronButton matlab.ui.control.Button
SaveResultsButton matlab.ui.control.Button
AllNeuronsDisplayPanel matlab.ui.container.Panel
ScalebarSignalLabel matlab.ui.control.Label
ScalebarSignalEditField matlab.ui.control.NumericEditField
PlotScaleBarTimeCheckBox matlab.ui.control.CheckBox
ScalebarTimeLabel matlab.ui.control.Label
ScalebarTimeEditField matlab.ui.control.NumericEditField
SelectedROILabel matlab.ui.control.Label
SelectedROIEditField matlab.ui.control.EditField
ROIIntervalLabel matlab.ui.control.Label
ROIIntervalEditField matlab.ui.control.NumericEditField
ColorMapLabel matlab.ui.control.Label
ColorMapEditField matlab.ui.control.EditField
DisplayAllNeuronsButton matlab.ui.control.Button
UIAxes matlab.ui.control.UIAxes
SignalTypeDropDown matlab.ui.control.DropDown
ExportPlotButton matlab.ui.control.Button
SetWidthButton matlab.ui.control.Button
SetHeightButton matlab.ui.control.Button
ExportAllNeuronsButton matlab.ui.control.Button
SetPlotColorButton matlab.ui.control.Button % NEW: Set Plot Color Button
end
% Properties that store app data
properties (Access = public)
selectedFolder = '' % Selected folder for data
fluo_data % Loaded fluorescence data (rows=neurons, cols=frames)
dff_data % Calculated ΔF/F data
zscore_dff_data % Calculated z-score ΔF/F data
time_vector % Time vector for plotting
framerate = 30 % Default framerate (Hz)
calculate_zscore = false % Flag to calculate z-score ΔF/F
baseline_method = 'Percentile' % Default baseline method
percentile_value = '10:20' % Default percentile
baseline_time = 'all' % Default baseline time range
polynomial_order = 3 % Default polynomial order
moving_window_sec = 20 % Default moving window size in seconds
moving_percentile = 20 % Default moving percentile value
current_neuron_id = 1 % ID of the currently displayed neuron
display_all = false % Flag to display all neurons
results % Structure to store analysis results
display_figure_handles % Handle for the all neurons figure
scalebar_signal = 1 % Default scalebar signal value
plot_scale_bar_time = true % Default plot scale bar time
scalebar_time = 10 % Default scalebar time length
selected_roi_str = '' % Default selected ROI string
roi_interval = 1 % Default ROI interval
color_map = 'turbo' % Default color map
signal_type = 'Raw Signal' % Current signal type to display
loaded_file_name = '' % Name of the loaded file
current_plot_color = [0 0.4470 0.7410] % NEW: Property to store current plot color (default MATLAB blue)
end
methods (Access = private)
% Update plot display
function UpdatePlot(app)
cla(app.UIAxes);
hold(app.UIAxes, 'on');
if isempty(app.fluo_data)
title(app.UIAxes, 'No data to plot.');
xlabel(app.UIAxes, 'Time (s)');
ylabel(app.UIAxes, 'Signal');
return;
end
% Ensure time_vector is up-to-date if framerate changed
if ~isempty(app.fluo_data) && (length(app.time_vector) ~= size(app.fluo_data,2) || abs(app.time_vector(2) - 1/app.framerate) > 1e-9 ) % Check if recalc needed
[~, num_frames] = size(app.fluo_data);
app.time_vector = (0:num_frames-1) / app.framerate;
end
ylabel_str = 'Raw Signal';
current_data_to_plot = [];
switch app.signal_type
case 'Raw Signal'
ylabel_str = 'Raw Signal';
current_data_to_plot = app.fluo_data;
case 'ΔF/F'
ylabel_str = 'ΔF/F';
if isempty(app.dff_data)
title(app.UIAxes, 'No ΔF/F data. Please run analysis.');
xlabel(app.UIAxes, 'Time (s)');
ylabel(app.UIAxes, ylabel_str);
return;
end
current_data_to_plot = app.dff_data;
case 'z-score ΔF/F'
ylabel_str = 'z-score ΔF/F';
if isempty(app.zscore_dff_data)
title(app.UIAxes, 'No z-score ΔF/F data. Please run analysis or enable z-score calculation.');
xlabel(app.UIAxes, 'Time (s)');
ylabel(app.UIAxes, ylabel_str);
return;
end
current_data_to_plot = app.zscore_dff_data;
otherwise
title(app.UIAxes, 'Unknown signal type selected.');
return;
end
if isempty(current_data_to_plot) && ~strcmp(app.signal_type, 'Raw Signal') % For dF/F or Z-score if data is missing
title(app.UIAxes, sprintf('%s data not available. Run analysis.', app.signal_type));
xlabel(app.UIAxes, 'Time (s)');
ylabel(app.UIAxes, ylabel_str);
return;
end
if app.display_all
num_neurons = size(current_data_to_plot, 1);
for n = 1:num_neurons
plot(app.UIAxes, app.time_vector, current_data_to_plot(n, :), ...
'DisplayName', sprintf('Neuron %d', n)); % Uses default color cycling
end
title(app.UIAxes, sprintf('%s for All %d Neurons', app.signal_type, num_neurons));
else
neuron_id = app.current_neuron_id;
if neuron_id == 0 || neuron_id > size(current_data_to_plot, 1)
title(app.UIAxes, 'Invalid or no neuron selected.');
xlabel(app.UIAxes, 'Time (s)');
ylabel(app.UIAxes, ylabel_str);
return;
end
trace = current_data_to_plot(neuron_id, :);
% MODIFIED: Use app.current_plot_color for single neuron trace
plot(app.UIAxes, app.time_vector, trace, 'Color', app.current_plot_color, 'LineWidth', 1.5, ...
'DisplayName', sprintf('Neuron %d', neuron_id));
title(app.UIAxes, sprintf('%s for Neuron %d', app.signal_type, neuron_id));
end
xlabel(app.UIAxes, 'Time (s)');
ylabel(app.UIAxes, ylabel_str);
grid(app.UIAxes, 'on');
if app.display_all
legend(app.UIAxes, 'show', 'Location', 'eastoutside');
end
hold(app.UIAxes, 'off');
xlim(app.UIAxes, [app.time_vector(1), app.time_vector(end)]); % Ensure x-axis limits are correct
end
% Export plot to external figure
function ExportPlotButtonPushed(app, event)
if isempty(app.fluo_data) || app.current_neuron_id == 0
uialert(app.UIFigure, 'No data or neuron selected to export.', 'Export Error');
return;
end
fig = figure('Name', sprintf('%s - Neuron %d', app.signal_type, app.current_neuron_id));
ax = axes(fig);
trace = [];
ylabel_str = app.signal_type;
switch app.signal_type
case 'Raw Signal'
trace = app.fluo_data(app.current_neuron_id, :);
case 'ΔF/F'
if isempty(app.dff_data)
uialert(app.UIFigure, 'ΔF/F data not available for export.', 'Export Error');
close(fig); return;
end
trace = app.dff_data(app.current_neuron_id, :);
case 'z-score ΔF/F'
if isempty(app.zscore_dff_data)
uialert(app.UIFigure, 'z-score ΔF/F data not available for export.', 'Export Error');
close(fig); return;
end
trace = app.zscore_dff_data(app.current_neuron_id, :);
otherwise
uialert(app.UIFigure, 'Unknown signal type selected for export.', 'Export Error');
close(fig); return;
end
if isempty(trace)
uialert(app.UIFigure, 'Selected trace data is empty.', 'Export Error');
close(fig); return;
end
% MODIFIED: Use app.current_plot_color for exported single neuron plot
plot(ax, app.time_vector, trace, 'Color', app.current_plot_color, 'LineWidth', 1.5);
title(ax, sprintf('%s for Neuron %d', app.signal_type, app.current_neuron_id));
xlabel(ax, 'Time (s)');
ylabel(ax, ylabel_str);
grid(ax, 'on');
xlim(ax, [app.time_vector(1), app.time_vector(end)]);
end
% Set axes width
function SetWidthButtonPushed(app, event)
prompt = {'Enter new width for UIAxes (pixels):'};
dlgtitle = 'Set Axes Width';
dims = [1 50];
definput = {num2str(app.UIAxes.Position(3))};
answer = inputdlg(prompt, dlgtitle, dims, definput);
if ~isempty(answer)
new_width = str2double(answer{1});
if isnan(new_width) || new_width < 100
uialert(app.UIFigure, 'Invalid width. Must be a number >= 100.', 'Input Error');
return;
end
app.UIAxes.Position(3) = new_width;
uialert(app.UIFigure, sprintf('UIAxes width set to %d pixels.', new_width), ...
'Width Updated', 'Icon', 'success');
end
end
% Set axes height
function SetHeightButtonPushed(app, event)
prompt = {'Enter new height for UIAxes (pixels):'};
dlgtitle = 'Set Axes Height';
dims = [1 50];
definput = {num2str(app.UIAxes.Position(4))};
answer = inputdlg(prompt, dlgtitle, dims, definput);
if ~isempty(answer)
new_height = str2double(answer{1});
if isnan(new_height) || new_height < 100
uialert(app.UIFigure, 'Invalid height. Must be a number >= 100.', 'Input Error');
return;
end
app.UIAxes.Position(4) = new_height;
uialert(app.UIFigure, sprintf('UIAxes height set to %d pixels.', new_height), ...
'Height Updated', 'Icon', 'success');
end
end
% NEW: Callback for Set Plot Color button
function SetPlotColorButtonPushed(app, event)
% Use the current plot color as the default in the color picker
selected_color = uisetcolor(app.current_plot_color, 'Select Plot Color');
% uisetcolor returns the input color if the user cancels,
% or the new color if the user clicks OK.
% It returns 0 if the figure is closed without clicking OK/Cancel (e.g., window X button).
if isequal(size(selected_color), [1 3]) % Check if a valid color was returned (not 0 from closing window)
if ~isequal(selected_color, app.current_plot_color)
figure(app.UIFigure); % Bring the app figure to front
app.current_plot_color = selected_color;
UpdatePlot(app); % Redraw the plot with the new color
uialert(app.UIFigure, 'Plot color updated.', 'Color Updated', 'Icon', 'success');
% else: color is the same as before (could be cancel, or picked same color)
% No need for an alert if the color didn't change.
end
% else: uisetcolor dialog was closed abruptly, selected_color might be 0. Do nothing.
end
end
% Calculate baseline F0
function F0 = CalculateBaseline(app, fluo_trace)
if ~strcmpi(app.baseline_time, 'all')
try
time_range_str = app.baseline_time;
if contains(time_range_str, ':') % Format like "1:30" or "10.5:20.0"
parts = strsplit(time_range_str, ':');
if length(parts) ~= 2
error('Invalid time range format. Use "all" or range like "1:30" or "10.5:20.0".');
end
time_range = [str2double(parts{1}), str2double(parts{2})];
if any(isnan(time_range)) || time_range(1) < 0 || time_range(2) < time_range(1)
error('Invalid numeric values in time range or end time < start time.');
end
start_frame = max(1, round(time_range(1) * app.framerate) + 1); % +1 for 1-based indexing
end_frame = min(length(fluo_trace), round(time_range(2) * app.framerate));
else % Format like "30" (interpreted as 0 to 30s)
single_time = str2double(time_range_str);
if isnan(single_time) || single_time < 0
error('Invalid single time value for baseline. Must be non-negative.');
end
start_frame = 1;
end_frame = min(length(fluo_trace), round(single_time * app.framerate));
end
if start_frame > end_frame % Handle cases where range is too small or outside data
warning('Baseline time range resulted in empty or invalid frame selection. Using full trace for this neuron.');
baseline_trace = fluo_trace;
else
baseline_trace = fluo_trace(start_frame:end_frame);
end
catch ME
warning('Invalid time range specified (%s): %s. Using all data for this neuron.', app.baseline_time, ME.message);
baseline_trace = fluo_trace;
end
else
baseline_trace = fluo_trace;
end
if isempty(baseline_trace) % Fallback if selection somehow results in empty
warning('Baseline trace is empty after time selection. Using full trace for this neuron.');
baseline_trace = fluo_trace;
end
switch app.baseline_method
case 'Percentile'
perc_str = app.percentile_value;
if contains(perc_str, ':')
range = str2num(perc_str);
if length(range) < 2 || any(isnan(range)) || any(range < 0) || any(range > 100)
error('Invalid percentile range. Use format "10:20" or single value like "20".');
end
F0 = mean(prctile(baseline_trace, range));
else
perc = str2double(perc_str);
if isnan(perc) || perc < 0 || perc > 100
error('Invalid percentile value. Must be between 0 and 100.');
end
F0 = prctile(baseline_trace, perc);
end
case 'Polynomial'
t = (1:length(fluo_trace))';
p = polyfit(t, fluo_trace', app.polynomial_order);
F0 = polyval(p, t)';
case 'Moving Percentile'
w_size = round(app.moving_window_sec * app.framerate);
if w_size < 1, w_size = 1; end % Ensure window size is at least 1
q_value = app.moving_percentile;
x_w = floor(w_size/2);
F0 = zeros(1, length(fluo_trace)); % Preallocate
for i = 1:length(fluo_trace)
idx_start = max(1, i - x_w);
idx_end = min(length(fluo_trace), i + x_w);
F0(i) = prctile(fluo_trace(idx_start:idx_end), q_value);
end
otherwise
error('Unknown baseline method.');
end
if any(F0 <= 0) % Prevent division by zero or negative F0
warning('Baseline (F0) calculation resulted in non-positive values. Clamping to a small positive value to avoid errors.');
F0(F0 <= 0) = 1e-6; % A small positive number
end
end
% Update all neurons plot
function UpdateAllNeuronsPlot(app)
if isempty(app.fluo_data) % Check raw data first
uialert(app.UIFigure, 'No data loaded to display.', 'Display Error');
return;
end
plot_signal_data = [];
current_signal_type_for_plot = app.signal_type; % Use the app's current signal type
switch current_signal_type_for_plot
case 'Raw Signal'
plot_signal_data = app.fluo_data;
case 'ΔF/F'
if isempty(app.dff_data)
uialert(app.UIFigure, 'ΔF/F data not available. Please run analysis.', 'Display Error');
return;
end
plot_signal_data = app.dff_data;
case 'z-score ΔF/F'
if isempty(app.zscore_dff_data)
uialert(app.UIFigure, 'Z-score ΔF/F data not available. Please run analysis with z-score enabled.', 'Display Error');
return;
end
plot_signal_data = app.zscore_dff_data;
otherwise
uialert(app.UIFigure, 'Invalid signal type for "All Neurons Plot".', 'Display Error');
return;
end
if isempty(plot_signal_data)
uialert(app.UIFigure, ['No data available for signal type: ' current_signal_type_for_plot], 'Display Error');
return;
end
if ~isfield(app, 'display_figure_handles') || isempty(app.display_figure_handles) || ~isvalid(app.display_figure_handles)
app.display_figure_handles = figure('Name', ['All Neurons - ' current_signal_type_for_plot]);
else
figure(app.display_figure_handles); % Bring to front
clf(app.display_figure_handles); % Clear previous content
app.display_figure_handles.Name = ['All Neurons - ' current_signal_type_for_plot];
end
% Call external plot_signal function (assuming it exists and is on path)
% Ensure plot.plot_signal can handle the 'fig' argument correctly
% and that it uses app.time_vector or recalculates based on framerate.
try
plot.plot_signal(plot_signal_data, ...
'frame_rate', app.framerate, ...
'time_vector', app.time_vector, ... % Pass the time vector
'color_map', app.color_map, ...
'signal_type', current_signal_type_for_plot, ...
'fig', app.display_figure_handles, ...
'scalebar_signal', app.scalebar_signal, ...
'plot_scale_bar_time', app.plot_scale_bar_time, ...
'scalebar_time', app.scalebar_time, ...
'selected_roi_str', app.selected_roi_str, ...
'roi_interval', app.roi_interval);
catch ME_plot_signal
uialert(app.UIFigure, ['Error calling plot_signal: ' ME_plot_signal.message], 'Plotting Error');
disp(ME_plot_signal.getReport);
end
end
% Callback for FramerateEditField value changed
function FramerateEditFieldValueChanged(app, event)
new_framerate = app.FramerateEditField.Value;
if isnan(new_framerate) || new_framerate <= 0
uialert(app.UIFigure, 'Framerate must be a positive number.', 'Input Error');
app.FramerateEditField.Value = app.framerate; % Revert to old value
return;
end
app.framerate = new_framerate; % Update the stored framerate
if ~isempty(app.fluo_data)
% Recalculate time vector
[~, num_frames] = size(app.fluo_data);
app.time_vector = (0:num_frames-1) / app.framerate;
% Update the plot
UpdatePlot(app);
% Update all neurons plot if it's open and valid
if isfield(app, 'display_figure_handles') && ~isempty(app.display_figure_handles) && isvalid(app.display_figure_handles)
if app.display_all % Only if it was meant to be displaying all neurons
UpdateAllNeuronsPlot(app); % This will replot with new framerate/time_vector
end
end
else
end
end
% Callback for Export All Neurons button
function ExportAllNeuronsButtonPushed(app, event)
if isempty(app.fluo_data)
uialert(app.UIFigure, 'No data loaded to export.', 'Export Error');
return;
end
selected_signal_type = app.SignalTypeDropDown.Value;
data_to_export = [];
file_suffix_type = '';
switch selected_signal_type
case 'Raw Signal'
data_to_export = app.fluo_data;
file_suffix_type = 'Raw_Signal';
case 'ΔF/F'
if isempty(app.dff_data)
uialert(app.UIFigure, 'ΔF/F data not available. Please run analysis first.', 'Export Error');
return;
end
data_to_export = app.dff_data;
file_suffix_type = 'DeltaF_F';
case 'z-score ΔF/F'
if isempty(app.zscore_dff_data)
uialert(app.UIFigure, 'z-score ΔF/F data not available. Please run analysis with z-score enabled.', 'Export Error');
return;
end
data_to_export = app.zscore_dff_data;
file_suffix_type = 'ZScore_DeltaF_F';
otherwise
uialert(app.UIFigure, 'Invalid signal type selected for export.', 'Export Error');
return;
end
if isempty(data_to_export)
uialert(app.UIFigure, ['No data found for the selected signal type: ' selected_signal_type], 'Export Error');
return;
end
[~, baseNameWithoutExt, ~] = fileparts(app.loaded_file_name);
if isempty(baseNameWithoutExt)
baseNameWithoutExt = 'ExportedData'; % Fallback if loaded_file_name is weird
end
exportDir = fullfile(app.selectedFolder, [baseNameWithoutExt '_ExportedPlots']);
if ~exist(exportDir, 'dir')
try
mkdir(exportDir);
catch ME_mkdir
uialert(app.UIFigure, ['Error creating export directory: ' ME_mkdir.message], 'Directory Error');
return;
end
end
num_neurons = size(data_to_export, 1);
progDlg = uiprogressdlg(app.UIFigure, 'Title', 'Exporting All Neuron Plots', ...
'Message', sprintf('Starting export for %d neurons...', num_neurons), ...
'Cancelable', 'on', 'Indeterminate', 'off');
cleanupProgDlg = onCleanup(@() delete(progDlg)); % Ensure dialog closes
% Get UIAxes dimensions for exported figures
% Ensure UIAxes has valid position data
if isempty(app.UIAxes.Position) || any(app.UIAxes.Position(3:4) <=0)
uialert(app.UIFigure, 'UIAxes dimensions are invalid. Cannot set export figure size.', 'Export Error');
return;
end
export_fig_width = app.UIAxes.Position(3);
export_fig_height = app.UIAxes.Position(4);
if export_fig_width < 50 || export_fig_height < 50 % Minimum sensible size
warning('UIAxes dimensions are very small. Exported figures might be tiny. Using default 600x400.');
export_fig_width = 600;
export_fig_height = 400;
end
for n = 1:num_neurons
if progDlg.CancelRequested
uialert(app.UIFigure, 'Export cancelled by user.', 'Export Cancelled');
return;
end
progDlg.Message = sprintf('Exporting Neuron %d/%d (%s)...', n, num_neurons, selected_signal_type);
progDlg.Value = n / num_neurons;
drawnow; % Update dialog
trace_data = data_to_export(n, :);
fig_export = []; % Initialize for catch block
try
fig_export = figure('Visible', 'off', ...
'Units', 'pixels', ...
'Position', [100, 100, export_fig_width, export_fig_height]); % Use UIAxes dimensions
ax_export = axes(fig_export);
% MODIFIED: Use app.current_plot_color for all individually exported neuron plots
plot(ax_export, app.time_vector, trace_data, 'Color', app.current_plot_color, 'LineWidth', 1);
title(ax_export, sprintf('%s - Neuron %d', selected_signal_type, n), 'Interpreter', 'none');
xlabel(ax_export, 'Time (s)');
ylabel(ax_export, strrep(selected_signal_type, 'ΔF/F', 'dF/F'));
grid(ax_export, 'on');
xlim(ax_export, [app.time_vector(1), app.time_vector(end)]);
baseFigName = sprintf('%s_Neuron%d_%s', baseNameWithoutExt, n, file_suffix_type);
figFilePath = fullfile(exportDir, [baseFigName '.fig']);
pngFilePath = fullfile(exportDir, [baseFigName '.png']);
savefig(fig_export, figFilePath);
exportgraphics(ax_export, pngFilePath, 'Resolution', 150);
close(fig_export);
catch ME_export_neuron
if isvalid(fig_export)
close(fig_export);
end
warning('Error exporting neuron %d: %s', n, ME_export_neuron.message);
choice = uiconfirm(app.UIFigure, ...
sprintf('Error exporting neuron %d: %s\n\nDo you want to continue with other neurons?', n, ME_export_neuron.message), ...
'Export Error', 'Options', {'Continue', 'Cancel All'}, 'DefaultOption', 'Continue');
if strcmp(choice, 'Cancel All')
uialert(app.UIFigure, 'Export cancelled due to error.', 'Export Cancelled');
return;
end
end
end
progDlg.Value = 1; progDlg.Message = 'Export complete!';
pause(0.5); % Allow dialog to show complete message
uialert(app.UIFigure, sprintf('All neuron plots exported successfully to:\n%s', exportDir), ...
'Export Complete', 'Icon', 'success');
end
end
% Callbacks that handle component events
methods (Access = private)
% Startup function
function startupFcn(app)
assignin('base', 'app', app);
app.RunAnalysisButton.Enable = 'off';
app.NeuronDropDown.Enable = 'off';
app.PreviousNeuronButton.Enable = 'off';
app.NextNeuronButton.Enable = 'off';
app.SaveResultsButton.Enable = 'off';
app.DisplayAllNeuronsButton.Enable = 'off';
app.NeuronDropDown.Items = {'N/A'};
app.NeuronDropDown.Value = 'N/A';
app.SignalTypeDropDown.Items = {'Raw Signal'};
app.SignalTypeDropDown.Enable = 'off';
app.ExportAllNeuronsButton.Enable = 'off';
title(app.UIAxes, 'Load Data to Begin');
app.FramerateEditField.Value = app.framerate;
app.CalculateZScoreCheckBox.Value = app.calculate_zscore;
app.PercentileEditField.Value = app.percentile_value;
app.BaselineTimeEditField.Value = app.baseline_time;
app.PolynomialOrderEditField.Value = app.polynomial_order;
app.MovingWindowEditField.Value = app.moving_window_sec;
app.MovingPercentileEditField.Value = app.moving_percentile;
app.BaselineMethodDropDown.Value = app.baseline_method;
app.ScalebarSignalEditField.Value = app.scalebar_signal;
app.PlotScaleBarTimeCheckBox.Value = app.plot_scale_bar_time;
app.ScalebarTimeEditField.Value = app.scalebar_time;
app.SelectedROIEditField.Value = app.selected_roi_str;
app.ROIIntervalEditField.Value = app.roi_interval;
app.ColorMapEditField.Value = app.color_map;
app.LoadedFileLabel.Text = 'No file loaded';
% Initialize visibility of baseline parameter fields
BaselineMethodDropDownValueChanged(app, []); % Call to set initial visibility
end
% Load data button pushed
function LoadDataButtonPushed(app, event)
f_dummy = figure('Position', [-100 -100 0 0],'CloseRequestFcn','','Visible','off');
[fileName, filePath] = uigetfile({'*.mat';'*.xlsx;*.xls'}, 'Select Data File');
delete(f_dummy);
if isequal(fileName, 0) || isequal(filePath, 0)
uialert(app.UIFigure, 'No file selected.', 'File Load');
return;
end
app.selectedFolder = filePath;
app.loaded_file_name = fileName;
app.LoadedFileLabel.Text = sprintf('%s', fileName);
fullPath = fullfile(filePath, fileName);
[~, ~, ext] = fileparts(fileName);
% Use the current value from the FramerateEditField when loading data
app.framerate = app.FramerateEditField.Value;
if isnan(app.framerate) || app.framerate <= 0
uialert(app.UIFigure, 'Invalid framerate. Please set a positive value.', 'Framerate Error');
app.FramerateEditField.Value = 30; % Reset to a default
app.framerate = 30;
return;
end
try
if strcmpi(ext, '.mat')
dataLoaded = load(fullPath);
varNames = fieldnames(dataLoaded);
if isempty(varNames)
uialert(app.UIFigure, 'MAT file is empty.', 'Load Error'); return;
end
if length(varNames) == 1
app.fluo_data = dataLoaded.(varNames{1});
else
f_dummy_list = figure('Position', [-100 -100 0 0],'CloseRequestFcn','','Visible','off');
[indx, tf] = listdlg('PromptString', {'Select fluorescence variable: (rows=neurons, cols=frames)'}, ...
'SelectionMode', 'single', 'ListString', varNames, ...
'Name', 'Select Variable', 'OKString', 'Select');
delete(f_dummy_list);
if tf
app.fluo_data = dataLoaded.(varNames{indx});
else
uialert(app.UIFigure, 'No variable selected.', 'Load Error'); return;
end
end
elseif any(strcmpi(ext, {'.xlsx', '.xls'}))
sheetNames = sheetnames(fullPath);
if isempty(sheetNames)
uialert(app.UIFigure, 'Excel file has no sheets.', 'Load Error'); return;
end
if length(sheetNames) == 1
app.fluo_data = readmatrix(fullPath, 'Sheet', sheetNames{1});
else
f_dummy_list = figure('Position', [-100 -100 0 0],'CloseRequestFcn','','Visible','off');
[indx, tf] = listdlg('PromptString', {'Select sheet: (rows=neurons, cols=frames)'}, ...
'SelectionMode', 'single', 'ListString', sheetNames, ...
'Name', 'Select Sheet', 'OKString', 'Select');
delete(f_dummy_list);
if tf
app.fluo_data = readmatrix(fullPath, 'Sheet', sheetNames{indx});
else
uialert(app.UIFigure, 'No sheet selected.', 'Load Error'); return;
end
end
else
uialert(app.UIFigure, 'Unsupported file type.', 'Load Error'); return;
end
if (~ismatrix(app.fluo_data) || isempty(app.fluo_data) || ~isnumeric(app.fluo_data))
uialert(app.UIFigure, 'Selected data is not a valid numeric matrix or is empty.', 'Data Error');
app.fluo_data = []; return;
end
[num_neurons, num_frames] = size(app.fluo_data);
app.time_vector = (0:num_frames-1) / app.framerate;
app.dff_data = [];
app.zscore_dff_data = [];
app.results = []; % Clear previous results
app.RunAnalysisButton.Enable = 'on';
app.NeuronDropDown.Enable = 'on';
app.PreviousNeuronButton.Enable = 'on';
app.NextNeuronButton.Enable = 'on';
app.DisplayAllNeuronsButton.Enable = 'on';
app.ExportAllNeuronsButton.Enable = 'on';
neuronItems = arrayfun(@(x) sprintf('Neuron %d', x), 1:num_neurons, 'UniformOutput', false);
if isempty(neuronItems)
app.NeuronDropDown.Items = {'N/A'};
app.NeuronDropDown.Value = 'N/A';
app.current_neuron_id = 0;
else
app.NeuronDropDown.Items = neuronItems;
app.NeuronDropDown.Value = neuronItems{1}; % Select first neuron
app.current_neuron_id = 1;
end
app.signal_type = 'Raw Signal';
app.SignalTypeDropDown.Items = {'Raw Signal'}; % Reset available types
app.SignalTypeDropDown.Value = 'Raw Signal';
app.SignalTypeDropDown.Enable = 'on';
app.SaveResultsButton.Enable = 'off'; % Only enable after analysis
UpdatePlot(app);
uialert(app.UIFigure, sprintf('Data loaded: %d neurons, %d frames at %.2f Hz.', num_neurons, num_frames, app.framerate), ...
'Load Success', 'Icon', 'success');
catch ME
uialert(app.UIFigure, ['Error loading data: ' ME.message], 'Load Error');
disp(ME.getReport); % For debugging
app.fluo_data = [];
app.dff_data = [];
app.zscore_dff_data = [];
app.RunAnalysisButton.Enable = 'off';
app.SignalTypeDropDown.Enable = 'off';
app.NeuronDropDown.Enable = 'off';
app.PreviousNeuronButton.Enable = 'off';
app.NextNeuronButton.Enable = 'off';
app.DisplayAllNeuronsButton.Enable = 'off';
app.ExportAllNeuronsButton.Enable = 'off';
app.SaveResultsButton.Enable = 'off';
app.LoadedFileLabel.Text = 'No file loaded';
app.NeuronDropDown.Items = {'N/A'};
app.NeuronDropDown.Value = 'N/A';
app.current_neuron_id = 0;
UpdatePlot(app); % Clear plot
end
end
% Run analysis button pushed
function RunAnalysisButtonPushed(app, event)
if isempty(app.fluo_data)
uialert(app.UIFigure, 'No data loaded to analyze.', 'Analysis Error');
return;
end
app.calculate_zscore = app.CalculateZScoreCheckBox.Value;
app.baseline_method = app.BaselineMethodDropDown.Value;
app.percentile_value = app.PercentileEditField.Value;
app.baseline_time = app.BaselineTimeEditField.Value;
app.polynomial_order = app.PolynomialOrderEditField.Value;
app.moving_window_sec = app.MovingWindowEditField.Value;
app.moving_percentile = app.MovingPercentileEditField.Value;
try
if app.framerate <= 0
error('Framerate must be positive.');
end
if ~strcmpi(app.baseline_time, 'all')
% Basic validation for baseline_time format (more in CalculateBaseline)
if ~matches(app.baseline_time, digitsPattern | (digitsPattern + ":" + digitsPattern))
if ~matches(app.baseline_time, textBoundary + (("0"|"."|[digitsPattern])+".*") + textBoundary) % allow decimals
% error('Invalid baseline time format. Use "all", "1:30", "10.5:20", or "30".');
end
end
end
if strcmp(app.baseline_method, 'Percentile')
perc_str = app.percentile_value;
if contains(perc_str, ':')
range = str2num(perc_str); %#ok<ST2NM>
if length(range) < 2 || any(isnan(range)) || any(range < 0) || any(range > 100)
error('Invalid percentile range. Use format "10:20" or single value like "20".');
end
else
perc = str2double(perc_str);
if isnan(perc) || perc < 0 || perc > 100
error('Invalid percentile value. Must be between 0 and 100.');
end
end
elseif strcmp(app.baseline_method, 'Polynomial')
if app.polynomial_order < 1 || mod(app.polynomial_order, 1) ~= 0
error('Polynomial order must be a positive integer.');
end
elseif strcmp(app.baseline_method, 'Moving Percentile')
if app.moving_window_sec <= 0
error('Moving window size must be positive.');
end
if app.moving_percentile < 0 || app.moving_percentile > 100
error('Moving percentile must be between 0 and 100.');
end
end
catch ME
uialert(app.UIFigure, ['Parameter error: ' ME.message], 'Parameter Error');
return;
end
progDlg = uiprogressdlg(app.UIFigure, 'Title', 'Computing Signals', ...
'Message', 'Initializing...', 'Cancelable', 'on', 'Indeterminate', 'off');
[num_neurons, num_frames] = size(app.fluo_data);
app.dff_data = zeros(num_neurons, num_frames);
if app.calculate_zscore
app.zscore_dff_data = zeros(num_neurons, num_frames);
else
app.zscore_dff_data = []; % Clear it if not calculated
end
app.results = struct('neuron_id', {}, 'dff_trace', {}, 'zscore_dff_trace', {});
cleanupObj = onCleanup(@() delete(progDlg)); % Ensure dialog closes
calculationErrorOccurred = false;
for n = 1:num_neurons
if progDlg.CancelRequested
uialert(app.UIFigure, 'Analysis cancelled by user.', 'Analysis Cancelled');
app.dff_data = []; % Clear partial results
app.zscore_dff_data = [];
app.results = [];
% Reset signal type dropdown if analysis was for dF/F
app.SignalTypeDropDown.Items = {'Raw Signal'};
app.SignalTypeDropDown.Value = 'Raw Signal';
app.signal_type = 'Raw Signal';
app.SaveResultsButton.Enable = 'off';
UpdatePlot(app);
return;
end
progDlg.Message = sprintf('Processing Neuron %d/%d', n, num_neurons);
progDlg.Value = n / num_neurons;
drawnow; % Update dialog
fluo_trace = app.fluo_data(n, :);
app.results(n).neuron_id = n;
try
F0 = CalculateBaseline(app, fluo_trace);
dff_trace = (fluo_trace - F0) ./ F0;
app.dff_data(n, :) = dff_trace;
app.results(n).dff_trace = dff_trace;
if app.calculate_zscore
if std(dff_trace) == 0 % Avoid division by zero for constant traces
app.zscore_dff_data(n, :) = zeros(1, num_frames);
else
app.zscore_dff_data(n, :) = (dff_trace - mean(dff_trace)) / std(dff_trace);
end
app.results(n).zscore_dff_trace = app.zscore_dff_data(n, :);
else
app.results(n).zscore_dff_trace = [];
end
catch ME_neuron
warning('Error computing signals for neuron %d: %s', n, ME_neuron.message);
% Mark error and continue, or stop? For now, continue but warn.
% Fill with NaNs or zeros for this neuron to indicate failure
app.dff_data(n, :) = NaN;
if app.calculate_zscore, app.zscore_dff_data(n, :) = NaN; end
app.results(n).dff_trace = NaN(1,num_frames);
if app.calculate_zscore, app.results(n).zscore_dff_trace = NaN(1,num_frames); end
calculationErrorOccurred = true;
end
end
progDlg.Value = 1; progDlg.Message = 'Analysis complete!';
pause(0.5); % Allow dialog to show complete message
if calculationErrorOccurred
uialert(app.UIFigure, 'Analysis completed, but errors occurred for one or more neurons. Check command window for warnings.', ...
'Analysis Warning', 'Icon', 'warning');
else
uialert(app.UIFigure, 'Analysis complete.', 'Success', 'Icon', 'success');
end
% Update UI elements related to results
app.SaveResultsButton.Enable = 'on';
new_signal_types = {'Raw Signal', 'ΔF/F'};
if app.calculate_zscore && ~isempty(app.zscore_dff_data)
new_signal_types = [new_signal_types, 'z-score ΔF/F'];
end
app.SignalTypeDropDown.Items = new_signal_types;
% Set plot to ΔF/F by default after analysis
if ismember('ΔF/F', new_signal_types)
app.SignalTypeDropDown.Value = 'ΔF/F';
app.signal_type = 'ΔF/F';
else % Should not happen if dff_data is populated
app.SignalTypeDropDown.Value = 'Raw Signal';
app.signal_type = 'Raw Signal';
end
% Ensure current_neuron_id is valid before updating plot
if app.current_neuron_id == 0 && num_neurons > 0
app.current_neuron_id = 1;
app.NeuronDropDown.Value = sprintf('Neuron %d', app.current_neuron_id);
elseif app.current_neuron_id > num_neurons && num_neurons > 0
app.current_neuron_id = num_neurons;
app.NeuronDropDown.Value = sprintf('Neuron %d', app.current_neuron_id);
elseif num_neurons == 0 % No neurons, should not happen here
app.current_neuron_id = 0;
app.NeuronDropDown.Value = 'N/A';
end
UpdatePlot(app);
end
% Signal type dropdown value changed
function SignalTypeDropDownValueChanged(app, event)
app.signal_type = app.SignalTypeDropDown.Value;
app.display_all = false; % Reset to single neuron view when type changes
UpdatePlot(app);
end
% Neuron dropdown value changed
function NeuronDropDownValueChanged(app, event)
if strcmp(app.NeuronDropDown.Value, 'N/A')
app.current_neuron_id = 0; % Or some other indicator for no neuron
UpdatePlot(app); % Update plot to show "no neuron selected" or clear
return;
end
selectedNeuronStr = app.NeuronDropDown.Value;
app.current_neuron_id = str2double(regexp(selectedNeuronStr, '\d+', 'match', 'once'));
app.display_all = false; % Ensure single neuron display
UpdatePlot(app);
end
% Previous neuron button pushed
function PreviousNeuronButtonPushed(app, event)
if isempty(app.fluo_data) || app.current_neuron_id <= 1
return;
end
app.current_neuron_id = app.current_neuron_id - 1;
app.NeuronDropDown.Value = sprintf('Neuron %d', app.current_neuron_id);
app.display_all = false;
UpdatePlot(app);
end
% Next neuron button pushed
function NextNeuronButtonPushed(app, event)
if isempty(app.fluo_data) || app.current_neuron_id >= size(app.fluo_data, 1)
return;
end
app.current_neuron_id = app.current_neuron_id + 1;
app.NeuronDropDown.Value = sprintf('Neuron %d', app.current_neuron_id);
app.display_all = false;
UpdatePlot(app);
end
% Update all neurons plot button pushed
function DisplayAllNeuronsButtonPushed(app, event)
app.scalebar_signal = app.ScalebarSignalEditField.Value;
app.plot_scale_bar_time = app.PlotScaleBarTimeCheckBox.Value;
app.scalebar_time = app.ScalebarTimeEditField.Value;
app.selected_roi_str = app.SelectedROIEditField.Value;
app.roi_interval = app.ROIIntervalEditField.Value;
app.color_map = app.ColorMapEditField.Value;
UpdateAllNeuronsPlot(app); % This updates the separate figure window
end
% Save results button pushed
function SaveResultsButtonPushed(app, event)
if isempty(app.results) && isempty(app.dff_data) % Check if there are any results to save
uialert(app.UIFigure, 'No analysis results to save.', 'Save Error');
return;
end
defaultFileName = 'analysis_results.mat';
if ~isempty(app.loaded_file_name)
[~, name, ~] = fileparts(app.loaded_file_name);
defaultFileName = [name '_analysis_results.mat'];
end
[fileName, filePath] = uiputfile({'*.mat', 'MAT-file (*.mat)'}, 'Save Results', fullfile(app.selectedFolder, defaultFileName));
if isequal(fileName, 0) || isequal(filePath, 0)
uialert(app.UIFigure, 'Save cancelled.', 'Save Operation');
return;
end
progressDlg = uiprogressdlg(app.UIFigure,'Title','Saving Data','Message', 'Preparing data for saving...','Indeterminate','on');
drawnow
[~, name, ~] = fileparts(fileName); % Use the name part from uiputfile
matPath = fullfile(filePath, [name '.mat']);
xlsxPath = fullfile(filePath, [name '.xlsx']); % Excel file with the same base name
% Prepare data for saving (ensure all relevant app properties are captured)
saveData.raw_fluorescence = app.fluo_data;
saveData.deltaF_over_F = app.dff_data;