figureDisk20190624gin.m 55 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499
  1. clear all
  2. close all
  3. grating = displayPattern(360/22);
  4. velocity = displayCosine(12.5);
  5. [leftdisk,leftparam] = displayDiskStimuli('left');
  6. [rightdisk,rightparam] = displayDiskStimuli('right');
  7. singledisk = displayDiskStimuli('single');
  8. [diskhandle,diskdata,fitdata] = gainAnalysisDisk('Resolution',50);
  9. variant = 3; % 1 = regular, 2 = rotated, 3 = combined/corrected %%%%% !!!! %%%%%
  10. hd.collage.fg = figure();
  11. hd.collage.fg.Name = 'Sphere manuscript (Fig 6); OKR gain dependence on stimulus location';
  12. hd.collage.fg.Color = [1 1 1];
  13. hd.collage.fg.Position = [0 40 1070 960];
  14. % Populate the combined figure from previously generated partial figures...
  15. hd.collage.ax(1,1) = copyobj(grating.pattern.ax(1), hd.collage.fg);
  16. hd.collage.ax(1,2) = copyobj(velocity.cosine.ax(1), hd.collage.fg);
  17. hd.collage.ax(1,3) = copyobj(leftdisk.default.ax(1), hd.collage.fg);
  18. hd.collage.ax(1,4) = copyobj(rightdisk.default.ax(1), hd.collage.fg);
  19. hd.collage.ax(1,5) = copyobj(singledisk.default.ax(1), hd.collage.fg);
  20. % hd.collage.ax(2,1) = copyobj(diskdata.gain.ax(7), hd.collage.fg);
  21. hd.collage.ax(2,1) = axes;
  22. for subpanel = 2:4
  23. hd.collage.ax(2,subpanel) = copyobj(diskhandle.gain(variant).ax(subpanel-1), hd.collage.fg);
  24. end
  25. for subpanel = 5:7
  26. hd.collage.ax(2,subpanel) = copyobj(hd.collage.ax(2,subpanel-3), hd.collage.fg);
  27. end
  28. % ...get rid of the partial figures once pillaged...
  29. % close(1:7)
  30. % ...then adjust the size, position and other properties of those objects.
  31. [hd.collage.ax(1,1:5).Units, hd.collage.ax(2,1:7).Units] = deal('pixels');
  32. hd.collage.ax(1,1).Position = [100 850 180 70];
  33. hd.collage.ax(1,2).Position = [130 730 140 55];
  34. hd.collage.ax(1,3).Position = [350 730 200 200];
  35. hd.collage.ax(1,4).Position = hd.collage.ax(1,3).Position + [250 0 0 0];
  36. hd.collage.ax(1,5).Position = hd.collage.ax(1,3).Position + [520 0 0 0];
  37. hd.collage.ax(2,1).Position = [ 50 240 50 160];
  38. hd.collage.ax(2,2).Position = [ 90 280 350 350];
  39. for subpanel = 3:4
  40. hd.collage.ax(2,subpanel).Position = hd.collage.ax(2,subpanel-1).Position + [320 0 0 0];
  41. end
  42. for subpanel = 5:7
  43. hd.collage.ax(2,subpanel).Position = hd.collage.ax(2,subpanel-3).Position - [0 280 0 0];
  44. end
  45. darkgreyscale = (.8:.01:.99)' * [1 1 1];
  46. lightgreyscale = (.9:.01:.99)' * [1 1 1];
  47. [hd.collage.ax(1,3:4).Colormap] = deal(lightgreyscale);
  48. [hd.collage.ax(1,5).Colormap] = deal(darkgreyscale);
  49. % % Shared properties of all axis objects anywhere:
  50. [hd.collage.ax(1,1:5).FontSize, ...
  51. hd.collage.ax(2,1:7).FontSize] = deal(15);
  52. [hd.collage.ax(1,1:5).LabelFontSizeMultiplier, ...
  53. hd.collage.ax(1,1:5).TitleFontSizeMultiplier, ...
  54. hd.collage.ax(2,1:7).LabelFontSizeMultiplier, ...
  55. hd.collage.ax(2,1:7).TitleFontSizeMultiplier] = deal(1);
  56. % Create content of first subpanel of panel b (colorbar):
  57. hd.collage.ax(2,1).CLim = hd.collage.ax(2,2).CLim;
  58. hd.collage.ob(2,1,1) = colorbar;
  59. hd.collage.ob(2,1,1).Label.String = 'OKR gain';
  60. hd.collage.ob(2,1,1).Label.Units = 'normalized';
  61. hd.collage.ob(2,1,1).AxisLocation = 'in';
  62. hd.collage.ax(2,1).Visible = 'off';
  63. hd.collage.ob(2,1,1).Units = 'pixels';
  64. hd.collage.ob(2,1,1).Position(3) = 9;
  65. hd.collage.ob(2,1,1).FontSize = hd.collage.ax(2,1).FontSize;
  66. hd.collage.ob(2,1,1).Label.Position = [-.8 1 1] .* hd.collage.ob(2,1,1).Label.Position;
  67. % Add and format titles to panel a:
  68. for panel = 1
  69. hd.collage.ax(panel,1).Title.String = 'stimulus pattern';
  70. hd.collage.ax(panel,3).Title.String = '----------------------- stimulus crop -----------------------';
  71. hd.collage.ax(panel,3).Title.Interpreter = 'Latex';
  72. % hd.collage.ax(panel,4).Title.String = {'stimulus crop','(right hemisphere)'};
  73. hd.collage.ax(panel,2).XLabel.String = 'time (sec)';
  74. hd.collage.ax(panel,2).YLabel.String = {'velocity','(deg/sec)'};
  75. hd.collage.ax(panel,3).XLabel.String = 'left hemisphere';
  76. hd.collage.ax(panel,4).XLabel.String = 'right hemisphere';
  77. [hd.collage.ax(panel,3).XLabel.Visible, ...
  78. hd.collage.ax(panel,4).XLabel.Visible] = deal('on');
  79. hd.collage.ax(panel,5).Title.String = 'stimulus';
  80. for subpanel = 1:5
  81. hd.collage.ax(panel,subpanel).Title.FontWeight = 'normal';
  82. [hd.collage.ax(panel,subpanel).Title.Units, ...
  83. hd.collage.ax(panel,subpanel).XLabel.Units] = deal('pixels');
  84. end
  85. % hd.collage.ax(panel,1).Title.Position(2) = hd.collage.ax(panel,1).Position(4) - 14;
  86. hd.collage.ax(panel,1).XLabel.Position(2) = hd.collage.ax(panel,1).XLabel.Position(2) + 25;
  87. % hd.collage.ax(panel,2).XLabel.Position(2) = hd.collage.ax(panel,2).XLabel.Position(2) - 65;
  88. hd.collage.ax(panel,2).XLabel.Position(2) = hd.collage.ax(panel,2).XLabel.Position(2) + 25;
  89. hd.collage.ax(panel,3).Title.Position(1) = hd.collage.ax(1,3).Title.Position(1) + 126;
  90. for subpanel = 3:4
  91. hd.collage.ax(panel,subpanel).Title.Position(2) = hd.collage.ax(panel,subpanel).Position(4) - 2;
  92. hd.collage.ax(panel,subpanel).XLabel.Position(2) = hd.collage.ax(panel,subpanel).XLabel.Position(2) + 23;
  93. end
  94. % hd.collage.ax(panel,5).Title.Position(2) = hd.collage.ax(panel,4).Position(4) + 2;
  95. end
  96. % axes(hd.collage.ax(1,3))
  97. % leftline = text(hd.collage.ax(1,3).Title.Position(1) - 50, ...
  98. % hd.collage.ax(1,3).Title.Position(2) + 20, ...
  99. % '-----------------------','Interpreter','Latex','HorizontalAlignment','right','Units','pixels')
  100. % Force visibility of axis ticks on bottom left (sine) plot of panel a:
  101. % hd.collage.ax(1,2).XLim = [-13 13];
  102. hd.collage.ax(1,2).YLim = [-13 13];
  103. % Format titles on top row of panels b to d:
  104. for subpanel = 2:4
  105. hd.collage.ax(2,subpanel).Title.Units = 'pixels';
  106. hd.collage.ax(2,subpanel).Title.Position(2) = hd.collage.ax(2,subpanel).Title.Position(2) - 25;
  107. end
  108. % Correct for odd treatment of indices (or their absence):
  109. hd.collage.ax(2,3).Title.Position(2) = hd.collage.ax(2,3).Title.Position(2) + 9;
  110. % Remove titles from bottom row of panels b to d:
  111. for subpanel = 5:7
  112. hd.collage.ax(2,subpanel).Title.Visible = 'off';
  113. end
  114. % % Make fish on panels b to d more visible by adjusting sphere alpha:
  115. % [hd.collage.ax(
  116. % Adjust camera perspective on the lower row of panels b to d:
  117. [hd.collage.ax(2,5:7).CameraPosition] = deal([19.5 0 0]);
  118. % Display panel lettering:
  119. hd.collage.ax(4,1) = axes;
  120. hd.collage.ax(4,1).Color = 'none';
  121. hd.collage.ax(4,1).Position = [0 0 1 1];
  122. hd.collage.ax(4,1).Units = 'pixels';
  123. hd.collage.ax(4,1).Visible = 'off';
  124. hold on
  125. hd.collage.lt(1) = text(hd.collage.ax(1,1).Position(1) - 85 , ...
  126. hd.collage.ax(1,1).Position(2) + hd.collage.ax(1,1).Position(4) + 20, ...
  127. 'a', 'Units', 'pixels');
  128. hd.collage.lt(2) = text(hd.collage.ax(2,2).Position(1) - 50, ...
  129. hd.collage.ax(2,2).Position(2) + hd.collage.ax(2,2).Position(4) - 10, ...
  130. 'b', 'Units', 'pixels');
  131. hd.collage.lt(3) = text(hd.collage.ax(2,3).Position(1) + 35, ...
  132. hd.collage.ax(2,3).Position(2) + hd.collage.ax(2,3).Position(4) - 10, ...
  133. 'c', 'Units', 'pixels');
  134. hd.collage.lt(4) = text(hd.collage.ax(2,4).Position(1) + 35, ...
  135. hd.collage.ax(2,4).Position(2) + hd.collage.ax(2,4).Position(4) - 10, ...
  136. 'd', 'Units', 'pixels');
  137. hold off
  138. [hd.collage.lt(:).FontSize] = deal(15);
  139. [hd.collage.lt(:).FontWeight] = deal('bold');
  140. % plotAsymmetry(diskdata(variant))
  141. mercatorPanel20190520
  142. delete(hd.collage.ax(1,1:2)) % remove panels about basic stimulus pattern
  143. close(1:8)
  144. for subpanel = 3:4
  145. hd.collage.ax(1,subpanel).Position = hd.collage.ax(1,subpanel).Position - [200 0 0 0];
  146. end
  147. hd.collage.ax(1,5).Position = hd.collage.ax(1,5).Position - [100 0 0 0];
  148. % [hd.collage.ax(1:2,1:2).Units,hd.collage.ax(3:5,:).Units] = deal('pixels');
  149. % [hd.collage.ax(1:2,1:2).FontSize,hd.collage.ax(3:5,:).FontSize] = deal(15);
  150. % [hd.collage.ax(1:2,1:2).LabelFontSizeMultiplier, ...
  151. % hd.collage.ax(1:2,1:2).TitleFontSizeMultiplier, ...
  152. % hd.collage.ax(3:5,:).LabelFontSizeMultiplier, ...
  153. % hd.collage.ax(3:5,:).TitleFontSizeMultiplier] = deal(1);
  154. % add dashed outline around or instead of white patches
  155. figure(hd.collage.fg)
  156. hd.collage.ax(5,2) = axes();
  157. hd.collage.ax(5,2).Units = 'pixels';
  158. % hd.collage.ax(5,2).Position = hd.collage.ax(2,2).Position + [0 -2 +2 +4] + [40 45 -80 -90];
  159. hd.collage.ax(5,2).Position = hd.collage.ax(2,2).Position + [0 -2 +2 +4] + [40 45 -80 -90] + 280*[0 -1 0 1];
  160. plot([.5,1,1,.5,.5]-.5,[1 1 0 0 1],'--k','LineWidth',1)
  161. hd.collage.ax(5,2).Color = 'none';
  162. hd.collage.ax(5,2).XAxis.Visible = 'off';
  163. hd.collage.ax(5,2).YAxis.Visible = 'off';
  164. hd.collage.ax(5,2).XLim = [0 1];
  165. hd.collage.ax(5,2).YLim = [0 1];
  166. hd.collage.ax(3,4) = axes();
  167. hd.collage.ax(3,4).Units = 'pixels';
  168. % hd.collage.ax(3,4).Position = hd.collage.ax(2,4).Position + [-2 -2 +4 +4] + [40 45 -80 -90];
  169. hd.collage.ax(3,4).Position = hd.collage.ax(2,4).Position + [-2 -2 +4 +4] + [40 45 -80 -90] + 280*[0 -1 0 1];
  170. plot([.5,1,1,.5,.5],[1 1 0 0 1],'--k','LineWidth',1)
  171. hd.collage.ax(3,4).Color = 'none';
  172. hd.collage.ax(3,4).XAxis.Visible = 'off';
  173. hd.collage.ax(3,4).YAxis.Visible = 'off';
  174. hd.collage.ax(3,4).XLim = [0 1];
  175. hd.collage.ax(3,4).YLim = [0 1];
  176. function plotAsymmetry(diskdata)
  177. col.barface = [.2 .8 .4];
  178. col.neutralbarface = .6*[1 1 1];
  179. col.baredge = 'none';
  180. col.errorbar = .3*[1 1 1];
  181. hd.asymindex.fg = figure();
  182. hd.asymindex.fg.Color = [1 1 1];
  183. hd.asymindex.fg.Position = [0 100 1900 390];
  184. hd.asymindex.ax(1,1) = axes();
  185. % hd.asymindex.ax(1,2) = axes();
  186. [hd.asymindex.ax(:,:).Units] = deal('pixels');
  187. hd.asymindex.ax(1,1).Position = [90 50 1780 300];
  188. y5a = diskdata.asymmetry.A1;
  189. y5b = diskdata.asymmetry.A2;
  190. % e5a = mscdisk.botheyes.stdgain;
  191. % e5b = upside.botheyes.stdgain;
  192. e5a = NaN(size(y5a));
  193. e5b = NaN(size(y5b));
  194. pvalue.asymmetry = signrank(y5a,y5b);
  195. x5a = (1:numel(y5a))' - .2;
  196. x5b = (1:numel(y5b))' + .2;
  197. axes(hd.asymindex.ax(1,1))
  198. hold on
  199. hd.asymindex.barregular = bar(x5a,y5a);
  200. hd.asymindex.barcontrol = bar(x5b,y5b);
  201. hd.asymindex.errorregular = errorbar(x5a,y5a,e5a,'LineStyle','none');
  202. hd.asymindex.errorcontrol = errorbar(x5b,y5b,e5b,'LineStyle','none');
  203. hd.asymindex.explanation = text(24,.351,{'biological asymmetry shown in green', ...
  204. '(error bars show s.e.m.)'},'HorizontalAlignment','center');
  205. hd.asymindex.pvalue = text(36,.351,{['p = ',num2str(pvalue.asymmetry)], ...
  206. '(Wilcoxon signed-rank test)'},'HorizontalAlignment','center');
  207. hold off
  208. [hd.asymindex.ax(1,:).XTick] = deal([]);
  209. hd.asymindex.barregular.FaceColor = col.neutralbarface;
  210. hd.asymindex.barcontrol.FaceColor = col.barface;
  211. [hd.asymindex.barregular.BarWidth, ...
  212. hd.asymindex.barcontrol.BarWidth] = deal(.35);
  213. [hd.asymindex.errorregular.Color, ...
  214. hd.asymindex.errorcontrol.Color] = deal(col.errorbar);
  215. [hd.asymindex.errorregular.LineWidth, ...
  216. hd.asymindex.errorcontrol.LineWidth] = deal(1);
  217. hd.asymindex.ax(1,1).XLim = [7 46];
  218. % hd.asymindex.ax(1,1).YLim = hd.asymindex.ax(1,2).YLim;
  219. hd.asymindex.ax(1,1).YLabel.String = 'mean OKR gain (pooled across eyes)';
  220. % hd.asymindex.ax(1,2).YLabel.String = 'mean OKR gain (right eye)';
  221. for panel = 1:1
  222. hd.asymindex.ax(1,panel).YLabel.Units = 'pixels';
  223. hd.asymindex.ax(1,panel).YLabel.Position(1) = hd.asymindex.ax(1,panel).YLabel.Position(1) - 20;
  224. end
  225. hd.asymindex.ax(1,1).XLabel.String = 'stimulus types';
  226. % hd.asymindex.ax(1,2).XLabel.String = 'stimulus types';
  227. [hd.asymindex.explanation.FontSize, ...
  228. hd.asymindex.pvalue.FontSize] = deal(13);
  229. for panel = 1:1
  230. hd.asymindex.ax(1,panel).XLabel.FontSize = 13;
  231. hd.asymindex.ax(1,panel).FontSize = 13;
  232. hd.asymindex.ax(1,panel).LabelFontSizeMultiplier = 1;
  233. hd.asymindex.ax(1,panel).YGrid = 'on';
  234. hd.asymindex.ax(1,panel).TickLength = [0 0];
  235. end
  236. end
  237. function hd = displayPattern(barwidth)
  238. spatialfreq = 180/barwidth;
  239. grid.colour = .3*[1 1 1];
  240. grid.width = 360;
  241. grid.height = 180;
  242. grid.x0 = -180;
  243. grid.y0 = -90;
  244. hd.pattern.fg = figure();
  245. hd.pattern.ax = axes();
  246. hold on
  247. dy = grid.height;
  248. numbar = ceil(grid.width*spatialfreq);
  249. dx = grid.width / spatialfreq / 2;
  250. for barindex = 1:numbar
  251. x0 = grid.x0 + (barindex-1) * dx * 2;
  252. xmax = grid.x0 + grid.width;
  253. y0 = grid.y0;
  254. darkbar(barindex) = patch([x0*[1 1],min(x0+dx,xmax)*[1 1]], ...
  255. [y0,y0+dy,y0+dy,y0], grid.colour);
  256. end
  257. [darkbar(:).EdgeColor] = deal('none');
  258. outline = plot(grid.x0*[1 1 1 1 1] + grid.width*[0 0 1 1 0], ...
  259. grid.y0*[1 1 1 1 1] + grid.height*[0 1 1 0 0], ...
  260. 'Color', grid.colour);
  261. hold off
  262. hd.pattern.ax.PlotBoxAspectRatio = [grid.width, grid.height, grid.height];
  263. % % [hd.pattern.ax.XAxis.Visible, hd.pattern.ax.YAxis.Visible] = deal('off');
  264. hd.pattern.ax.XLim = [-180 180];
  265. hd.pattern.ax.YLim = [-90 90];
  266. hd.pattern.ax.XTick = [-180 180];
  267. hd.pattern.ax.YTick = [-90 0 90];
  268. hd.pattern.ax.XLabel.String = 'azimuth';
  269. hd.pattern.ax.YLabel.String = 'elevation';
  270. end
  271. function hd = displayCosine(maxvelocity)
  272. time = 0:.2:25;
  273. velocity = sin(time*2*pi/10) * maxvelocity;
  274. hd.cosine.fg = figure();
  275. hd.cosine.ax = axes();
  276. hold on
  277. hd.cosine.ob(1) = plot(time([1 end]),[0 0]);
  278. hd.cosine.ob(2) = plot(time,velocity);
  279. hold off
  280. hd.cosine.ob(1).LineStyle = ':';
  281. hd.cosine.ob(1).Color = .0*[1 1 1];
  282. hd.cosine.ob(2).Color = .2*[1 1 1];
  283. hd.cosine.ax.Box = 'off';
  284. hd.cosine.ax.XGrid = 'on';
  285. % hd.cosine.ax.XMinorGrid = 'on';
  286. % hd.cosine.ax.YGrid = 'on';
  287. % hd.cosine.ax.XAxisLocation = 'origin';
  288. hd.cosine.ax.XTick = [0 25];
  289. % hd.cosine.ax.XTick = 25;
  290. % hd.cosine.ax.XTickLabel = hd.cosine.ax.XTick;
  291. hd.cosine.ax.YTick = [-1 1] * maxvelocity;
  292. end
  293. function [handle,data,datafit] = gainAnalysisDisk(varargin) %#ok<FNDEF>
  294. % GAINANALYSIS and all subfunctions were written by Florian Alexander
  295. % Dehmelt in 2018. This is version 20180911, a.k.a. gainAnalysis20180911.m.
  296. % Apologies for the lack of polish (and comments) at this point.
  297. %
  298. % All input arguments are optional:
  299. %
  300. % Resolution - scalar, indicating your choice of resolution for the
  301. % rendering of the spherical surface as part % of the
  302. % figures. Actual data fitting is completely unaffected.
  303. % CamAzimuth - scalar, horizontal angle at which sphere is shown
  304. % CamElevation - scalar, vertical angle at which sphere is shown
  305. % Normalisation - options: nothing, or 'mean gain per hemisphere',
  306. % or 'max gain per hemisphere',
  307. % or 'ratio of mean gains per hemisphere'
  308. %
  309. % Output arguments are:
  310. %
  311. % handle - structure containing handles to all graphics objects
  312. % (figures, axes, plots, decorations)
  313. % data - structure containing pre-processed data, with those fields:
  314. % .left - numerical array of means of OKR gain for the left eye
  315. % .right - numerical array of means of OKR gain for the right eye
  316. % .both - numerical array of mean of OKR gain means across both eyes
  317. % datafit - structure containing the fits to data, with following fields:
  318. % .centre - 2x2 numerical array of azimuths (1st row) and elevation
  319. % angles (2nd row) of the two individial von Mises-Fisher
  320. % functions fit to data (1st column = 1st function, etc.).
  321. % These are NOT the peaks of the actual fit (= sum of both
  322. % functions)! I still have to implement that feature.
  323. % .value - 36x1 numerical array containing the value of the actual fit
  324. % (= sum of both functions) at the locations sampled by data
  325. % .param - 1x9 numerical array containing all best-fit parameters
  326. % (offset, amplitude1, azimuth1, elevation1, concentration1,
  327. % amplitude2, azimuth2, elevation2, concentration2)
  328. %
  329. % The recommended default is calling "hd = gainAnalysis20180911(200);".
  330. %
  331. % To specify camera position before analysis, call the function as follows:
  332. % hd = gainAnalysis20180911(200,'CamAzimuth',-30,'CamElevation',15);
  333. %
  334. % To edit camera position post-hoc, adjust and execute the following lines:
  335. % az = 0; el = 0; % substitute your desired camera coordinates here
  336. % [hd.gain(end).ax(1:6).CameraPosition] = ...
  337. % deal([cosd(az)*cosd(el),sind(az)*cosd(el),sind(el)]/19.0526);
  338. % figure(21)
  339. % MANAGE VARIABLE INPUT
  340. p = inputParser;
  341. valid.resolution = @(x) isnumeric(x) && numel(x)==1 && x>0;
  342. valid.camazimuth = @(x) isnumeric(x) && numel(x)==1;
  343. valid.camelevation = @(x) isnumeric(x) && numel(x)==1;
  344. valid.normalisation = @(x) ischar(x);
  345. % valid.rotation = @(x) ischar(x);
  346. default.resolution = 200;
  347. default.camazimuth = 0;
  348. default.camelevation = 45;
  349. default.normalisation = '';
  350. % default.rotation = 'none';
  351. addOptional(p, 'Resolution', default.resolution, valid.resolution);
  352. addOptional(p, 'CamAzimuth', default.camazimuth, valid.camazimuth);
  353. addOptional(p, 'CamElevation', default.camelevation, valid.camelevation);
  354. addOptional(p, 'Normalisation', default.normalisation, valid.normalisation);
  355. % addOptional(p, 'Rotation', default.rotation, valid.rotation);
  356. parse(p,varargin{:});
  357. resolution = p.Results.Resolution;
  358. cam.azimuth = p.Results.CamAzimuth;
  359. cam.elevation = p.Results.CamElevation;
  360. normalisation = p.Results.Normalisation;
  361. % rotation = p.Results.Rotation;
  362. % GENERAL SETTINGS
  363. warning('off','MATLAB:interp1:UsePCHIP')
  364. % The line above deactivates the following cbrewer warning:
  365. % Warning: INTERP1(...,'CUBIC') will change in a future release. Use INTERP1(...,'PCHIP') instead.
  366. % > In interp1>sanitycheckmethod (line 265)
  367. % In interp1>parseinputs (line 401)
  368. % In interp1 (line 80)
  369. % In interpolate_cbrewer (line 31)
  370. % In cbrewer (line 101)
  371. % In heatmap20180823 (line 21)
  372. % Identify which stimulus entries correspond to the disk-mask stimuli.
  373. datarange = 9:46;
  374. % datarange = 9:44;
  375. % !!! IMPORTANT !!! These are the Excel row numbers, not stimulus types.
  376. % READ DATA
  377. % Query user to manually select the main data regularset. Other files have
  378. % standard names and are expected to be in the same folder.
  379. [mfilefolder,~,~] = fileparts(mfilename('fullpath'));
  380. % [regularset.result,regularset.folder] = uigetfile([mfilefolder,'\*.mat']);
  381. regularset.file = 'result_20180110_125537.mat';
  382. regularset.folder = ['..\..\data\Fig3 default disk data\Data D Series (neue Experimente)\'];
  383. rotatedset.file = 'result_20181001_114504.mat';
  384. rotatedset.folder = ['..\..\data\Fig3 default disk data\Control Data Rotation (arena flip)\'];
  385. regularset.structure = load([regularset.folder,regularset.file],'result');
  386. rotatedset.structure = load([rotatedset.folder,rotatedset.file],'result');
  387. regularset.result = regularset.structure.result;
  388. rotatedset.result = rotatedset.structure.result;
  389. % Discard unwanted trials, based on .xlsx documenting manual assessment.
  390. regularset.result = discardTrial(regularset.result,regularset.folder);
  391. rotatedset.result = discardTrial(rotatedset.result,rotatedset.folder);
  392. % Pool gain data in various ways (this is a significant subfunction!).
  393. regularset.gain = poolGainData(regularset.result);
  394. rotatedset.gain = poolGainData(rotatedset.result);
  395. % READ STIMULUS INFORMATION
  396. regularset.stimcentre = readStimulus(regularset.folder,datarange);
  397. rotatedset.stimcentre = readStimulus(rotatedset.folder,datarange);
  398. % reordering = [1;2;3;5;4;7;6]; % hemisphere stimuli, U vs. D and L vs. R flipped; now add disks:
  399. % for regularindex = 1:size(regularset.stimcentre,1)
  400. % % Find rotated-arena stimulus appearing at same position relative to fish:
  401. % rotatedindex = find(sum(regularset.stimcentre(regularindex,:)==rotatedset.stimcentre,2)==2);
  402. % reordering = [reordering; rotatedindex]; %#ok<AGROW>
  403. % end
  404. %
  405. % Correction above is unnecessary if stimulus coordinates were already in
  406. % fish-centred coordinates. In this case, do not reorder data:
  407. reordering = 1:45;
  408. % CHOOSE SUBSET OF DATA TO ANALYSE, AND PROCEED TO ANALYSE IT
  409. variantlist = {'regular','rotated','corrected'};
  410. for variant = 1:numel(variantlist)
  411. switch variantlist{variant}
  412. case 'regular'
  413. data(variant).left = regularset.gain.mean.FR{1};
  414. data(variant).right = regularset.gain.mean.FR{2};
  415. case 'rotated'
  416. data(variant).left = rotatedset.gain.mean.FR{1}(reordering);
  417. data(variant).right = rotatedset.gain.mean.FR{1}(reordering);
  418. case 'corrected'
  419. % data(variant).left = (regularset.gain.mean.FR{1} + rotatedset.gain.mean.FR{1}) / 2;
  420. % data(variant).right = (regularset.gain.mean.FR{2} + rotatedset.gain.mean.FR{1}) / 2;
  421. data(variant).left = (regularset.gain.mean.FR{1} + rotatedset.gain.mean.FR{1}(reordering)) / 2;
  422. data(variant).right = (regularset.gain.mean.FR{2} + rotatedset.gain.mean.FR{1}(reordering)) / 2;
  423. for selection = datarange-1
  424. gLreg = regularset.gain.mean.FR{1}(selection);
  425. gRreg = regularset.gain.mean.FR{2}(selection);
  426. gLrot = rotatedset.gain.mean.FR{1}(reordering(selection));
  427. gRrot = rotatedset.gain.mean.FR{2}(reordering(selection));
  428. data(variant).asymmetry.A1(selection) = ((gLreg-gRreg)/(gLreg+gRreg) - (gLrot-gRrot)/(gLrot+gRrot)) / 2;
  429. data(variant).asymmetry.A2(selection) = ((gLreg-gRreg)/(gLreg+gRreg) + (gLrot-gRrot)/(gLrot+gRrot)) / 2;
  430. end
  431. end
  432. data(variant).both = [data(variant).left(datarange(1:end/2)-1);data(variant).right(datarange(end/2+1:end)-1)];
  433. % data(variant).both = mean([data(variant).left,data(variant).right],2); % important change! no more averaging
  434. % Create figure(s).
  435. hd.gain(variant).fg = figure();
  436. hd.gain(variant).fg.Name = 'OKR gain';
  437. hd.gain(variant).fg.Color = [1 1 1];
  438. hd.gain(variant).fg.Position = [50 50 1000 650];
  439. hd.gain(variant).fg.Colormap = flipud(cbrewer('div','RdBu',100));
  440. % hd.gain(variant).fg.Colormap = cbrewer('div','PRGn',100);
  441. choicelist = {'left','both','right'};
  442. for choice = 1:numel(choicelist)
  443. switch choicelist{choice}
  444. case 'right'
  445. response = data(variant).right(datarange-1); % -1 is offset by Excel title row
  446. case 'left'
  447. response = data(variant).left(datarange-1);
  448. case 'both'
  449. response = data(variant).both;
  450. otherwise
  451. error(['Input argument must be one of the following strings: ' ...
  452. '''left'', ''right'', or ''both''.'])
  453. end
  454. [x,y,z] = sphere(resolution);
  455. [datafit(choice).matrix,datafit(choice).param] = ...
  456. vonMisesFisherFit(regularset.stimcentre,response,x,y,z);
  457. % set 3D fish parameters
  458. scale = .1;
  459. yaw = 180;
  460. pitch = 0;
  461. roll = 0;
  462. % set decorative arc parameters; compute their cartesian coordinates
  463. equator.base = -1:.01:1;
  464. equator.x = [equator.base, fliplr(equator.base)];
  465. equator.y = [sqrt(1-equator.base.^2), -sqrt(1-equator.base.^2)];
  466. equator.z = 0*ones(1,numel(equator.x));
  467. meridian.x = equator.x;
  468. meridian.y = equator.z;
  469. meridian.z = equator.y;
  470. aura.x = equator.z;
  471. aura.y = equator.y;
  472. aura.z = equator.x;
  473. [sample.x,sample.y,sample.z] = ...
  474. geo2car(regularset.stimcentre(:,1), regularset.stimcentre(:,2), ...
  475. ones(size(regularset.stimcentre,1),1),'CCW');
  476. % plot all of the above onto the correct panel of the existing figure
  477. panel = choice;
  478. figure(hd.gain(variant).fg.Number)
  479. hd.gain(variant).ax(panel) = axes;
  480. hd.gain(variant).ax(panel).Position = [.13 .45 .35 .45] + (panel-1)*[.25 0 0 0];
  481. hold on
  482. hd.gain(variant).ob(panel,1) = surf(x,y,z,datafit(panel).matrix);
  483. shading interp
  484. colour.eye = 0*[1 1 1];
  485. colour.body = 1*[1 1 1];
  486. hd.gain(variant).ob(panel,2:4) = plot3dFish(scale,yaw,pitch,roll,colour);
  487. hd.gain(variant).ob(panel,5) = plot3(equator.x,equator.y,equator.z,'k','LineWidth',2);
  488. hd.gain(variant).ob(panel,6) = plot3(meridian.x,meridian.y,meridian.z,'--k');
  489. hd.gain(variant).ob(panel,7) = plot3(aura.x,aura.y,aura.z,':k');
  490. hd.gain(variant).ob(panel,8) = scatter3(1.03*sample.x,1.03*sample.y,1.03*sample.z, ...
  491. 180*ones(size(sample.x)),response,'Marker','o');
  492. hold off
  493. end
  494. % Apply full formatting to the figure(s). Subfunction included below.
  495. hd = fishbowlFigure(hd,cam,choicelist);
  496. % % Compute yoking/lateralisation index per individual stimulus location.
  497. % convention = 'CCW';
  498. % switch convention
  499. % case 'CCW'
  500. % stim.isleft = stim.azimuth > 0;
  501. % stim.isright = stim.azimuth < 0;
  502. % case 'CW'
  503. % stim.isleft = stim.azimuth < 0;
  504. % stim.isright = stim.azimuth > 0;
  505. % otherwise
  506. % error('Sign convention must either be ''CCW'' or ''CW''.')
  507. % end
  508. end
  509. % Collect output arguments, unless already done (e.g., "datafit").
  510. handle = hd;
  511. end
  512. function stimcentre = readStimulus(datafolder,datarange)
  513. stimulus = 'Stimulus.xlsx';
  514. [~,tableconvention,~] = xlsread([datafolder,stimulus],'C1:C1');
  515. azimuth = xlsread([datafolder,stimulus],['C',num2str(datarange(1)),':C',num2str(datarange(end))]);
  516. elevation = xlsread([datafolder,stimulus],['D',num2str(datarange(1)),':D',num2str(datarange(end))]);
  517. % Stimulus tables may use CW convention; default code uses CCW.
  518. switch tableconvention{:}
  519. case 'azimuth (CCW)'
  520. case 'azimuth (CW)'
  521. azimuth = -azimuth;
  522. otherwise
  523. error(['Field C1 of Stimulus.xlsx should contain either ' ...
  524. 'string ''azimuth (CCW)'' or string ''azimuth (CW)''.'])
  525. end
  526. stimcentre = [azimuth,elevation];
  527. end
  528. function gain = poolGainData(result)
  529. %POOLGAINDATA converts a raw result structure into plottable data by
  530. %pooling OKR gain data in various ways, and storing those results in a new
  531. %structure called "gain".
  532. % Pool data in various ways (e.g., .FR = pooled across all [F]ish and
  533. % all [R]epetitions, but with a separate array for each [E]ye).
  534. for stimtype = 1:size(result,3)
  535. gainpool(stimtype).FER = [result(:,:,stimtype,:).gain];
  536. for eye = 1:size(result,2)
  537. gainpool(stimtype).FR{eye} = [result(:,eye,stimtype,:).gain];
  538. end
  539. for fish = 1:size(result,1)
  540. gainpool(stimtype).ER{fish} = [result(fish,:,stimtype,:).gain];
  541. end
  542. for repetition = 1:size(result,4)
  543. gainpool(stimtype).FE{repetition} = [result(:,:,stimtype,repetition).gain];
  544. end
  545. for fish = 1:size(result,1)
  546. for eye = 1:size(result,2)
  547. gainpool(stimtype).R{fish,eye} = [result(fish,eye,stimtype,:).gain];
  548. end
  549. end
  550. end
  551. % Make sure that in following section, if a stimulus type is not present,
  552. % the array contain a NaN at that position, instead of a zero.
  553. nantemplate = NaN(numel(gainpool),1);
  554. gain.mean .FER = nantemplate;
  555. gain.median.FER = nantemplate;
  556. gain.std .FER = nantemplate;
  557. gain.sem .FER = nantemplate;
  558. for eye = 1:size(result,2)
  559. gain.mean .FR{eye} = nantemplate;
  560. gain.median.FR{eye} = nantemplate;
  561. gain.std .FR{eye} = nantemplate;
  562. gain.sem .FR{eye} = nantemplate;
  563. end
  564. for fish = 1:size(result,1)
  565. gain.mean .ER{fish} = nantemplate;
  566. gain.median.ER{fish} = nantemplate;
  567. gain.std .ER{fish} = nantemplate;
  568. gain.sem .ER{fish} = nantemplate;
  569. end
  570. for fish = 1:size(result,1)
  571. for eye = 1:size(result,2)
  572. gain.mean .R{fish,eye} = nantemplate;
  573. gain.median.R{fish,eye} = nantemplate;
  574. gain.std .R{fish,eye} = nantemplate;
  575. gain.sem .R{fish,eye} = nantemplate;
  576. end
  577. end
  578. % mean, median, stdev, sem across all pooled data, one per stimulus type
  579. for stimtype = 1:numel(gainpool)
  580. selection = gainpool(stimtype).FER;
  581. gain.mean .FER(stimtype) = mean(selection);
  582. gain.median.FER(stimtype) = median(selection);
  583. gain.std .FER(stimtype) = std(selection);
  584. gain.sem .FER(stimtype) = std(selection)/sqrt(numel(selection));
  585. for eye = 1:size(result,2)
  586. selection = gainpool(stimtype).FR{eye};
  587. gain.mean .FR{eye}(stimtype) = mean(selection);
  588. gain.median.FR{eye}(stimtype) = median(selection);
  589. gain.std .FR{eye}(stimtype) = std(selection);
  590. gain.sem .FR{eye}(stimtype) = std(selection)/sqrt(numel(selection));
  591. end
  592. for fish = 1:size(result,1)
  593. selection = gainpool(stimtype).ER{fish};
  594. gain.mean .ER{fish}(stimtype) = mean(selection);
  595. gain.median.ER{fish}(stimtype) = median(selection);
  596. gain.std .ER{fish}(stimtype) = std(selection);
  597. gain.sem .ER{fish}(stimtype) = std(selection)/sqrt(numel(selection));
  598. end
  599. for fish = 1:size(result,1)
  600. for eye = 1:size(result,2)
  601. selection = gainpool(stimtype).R{fish,eye};
  602. gain.mean .R{fish,eye}(stimtype) = mean(selection);
  603. gain.median.R{fish,eye}(stimtype) = median(selection);
  604. gain.std .R{fish,eye}(stimtype) = std(selection);
  605. gain.sem .R{fish,eye}(stimtype) = std(selection)/sqrt(numel(selection));
  606. end
  607. end
  608. end
  609. end
  610. function [fitresult,fitparam] = vonMisesFisherFit(stimcentre,response,x,y,z)
  611. predictor = stimcentre;
  612. % Initialise fit parameters.
  613. offset = 0;
  614. amplitude(1) = 1;
  615. amplitude(2) = 1;
  616. meanazimuth(1) = 70;
  617. meanelevation(1) = 20; % azimuth, elevation
  618. meanazimuth(2) = -70;
  619. meanelevation(2) = 20; % azimuth, elevation
  620. concentration(1) = 2.5;
  621. concentration(2) = 2.5;
  622. initialparam = [offset, ...
  623. amplitude(1), ...
  624. meanazimuth(1), ...
  625. meanelevation(1), ...
  626. concentration(1), ...
  627. amplitude(2), ...
  628. meanazimuth(2), ...
  629. meanelevation(2), ...
  630. concentration(2)];
  631. % Fit.
  632. option.MaxIter = 1e4;
  633. datafit.param = nlinfit(predictor,response,@vonmisesfisher9param,initialparam,option);
  634. datafit.value = vonmisesfisher9param(datafit.param,predictor);
  635. % Identify centres.
  636. azimuth(1) = datafit.param(3);
  637. elevation(1) = datafit.param(4);
  638. azimuth(2) = datafit.param(7);
  639. elevation(2) = datafit.param(8);
  640. for k = 1:2
  641. % azimuth(k) = datafit.param(3); % This was incorrect - same value read twice.
  642. % elevation(k) = datafit.param(4);
  643. uniqueazimuth(k) = mod(azimuth(k)+180,360) - 180;
  644. uniqueelevation(k) = mod(elevation(k)+90,180) - 90;
  645. datafit.centre(:,k) = [uniqueazimuth(k), uniqueelevation(k)];
  646. end
  647. % compute geographic coordinates of sphere, and its colour data values
  648. [azimuth,elevation,radius] = car2geo(x,y,z,'CCW');
  649. predictor = [azimuth(:),elevation(:),radius(:)];
  650. fitresult = reshape(vonmisesfisher9param(datafit.param,predictor),size(radius));
  651. fitparam = datafit.param;
  652. end
  653. function hd = fishbowlFigure(hd,cam,choicelist)
  654. %FISHBOWLFIGURE configures an existing figure of fishbowl plots, adds decorations
  655. %
  656. % HD = fishbowlFigure sets the parameters of an existing fishbowl figure,
  657. % including axis limits, labels, colours, axes object sizes, etc., and
  658. % adds additional decorations such as colorbars.
  659. cmin = min([min(min(hd.gain(end).ob(1,1).CData)), ...
  660. min(min(hd.gain(end).ob(2,1).CData)), ...
  661. min(min(hd.gain(end).ob(3,1).CData)), ...
  662. min(min(hd.gain(end).ob(1,8).CData)), ...
  663. min(min(hd.gain(end).ob(2,8).CData)), ...
  664. min(min(hd.gain(end).ob(3,8).CData))]);
  665. cmax = max([max(max(hd.gain(end).ob(1,1).CData)), ...
  666. max(max(hd.gain(end).ob(2,1).CData)), ...
  667. max(max(hd.gain(end).ob(3,1).CData)), ...
  668. max(max(hd.gain(end).ob(1,8).CData)), ...
  669. max(max(hd.gain(end).ob(2,8).CData)), ...
  670. max(max(hd.gain(end).ob(3,8).CData))]);
  671. clim = max(abs([cmin cmax]))*[-1 1];
  672. for panel = 1:3
  673. switch choicelist{panel}
  674. case 'left'
  675. string.title = 'left eye gain g_L';
  676. case 'right'
  677. string.title = 'right eye gain g_R';
  678. case 'both'
  679. string.title = 'merge stimulated';
  680. % string.title = 'mean = (g_L+ g_R)/2';
  681. end
  682. hd.gain(end).ax(panel).Title.String = string.title;
  683. % title(string.title,'Interpreter','LaTeX')
  684. hd.gain(end).ob(panel,8).MarkerFaceColor = hd.gain(end).ob(panel,8).MarkerEdgeColor;
  685. hd.gain(end).ob(panel,8).MarkerEdgeColor = [0 0 0];
  686. hd.gain(end).ax(panel).CLim = [0 cmax];
  687. % hd.gain(end).ax(panel).CLim = [0 max(max(datafit(choice).matrix))];
  688. % hd.gain(end).ob(panel,1).FaceAlpha = .85;
  689. hd.gain(end).ob(panel,1).FaceAlpha = .75;
  690. hd.gain(end).ob(panel,1).EdgeColor = 'none';
  691. % hd.gain(end).ob(panel,8).Shading = 'flat';
  692. shading interp
  693. axis square
  694. xlabel('x')
  695. ylabel('y')
  696. zlabel('z')
  697. hd.gain(end).ax(panel).XLim = [-1.1 1.1];
  698. hd.gain(end).ax(panel).YLim = [-1.1 1.1];
  699. hd.gain(end).ax(panel).ZLim = [-1.1 1.1];
  700. % hd.gain(end).ax(panel).CameraPosition = [12.2934, -9.0967,8.1315];
  701. cam.radius = 19.0526; % MATLAB automatically reverts to this values while rotating, anyway. Might as well choose it as default.
  702. [cam.x,cam.y,cam.z] = geo2car(cam.azimuth,cam.elevation,cam.radius,'CCW');
  703. hd.gain(end).ax(panel).CameraPosition = [cam.x cam.y cam.z]; % FD20190507: this hides 3rd fish!!!
  704. hd.gain(end).ax(panel).CameraTarget = [0 0 0];
  705. hd.gain(end).ax(panel).CameraUpVector = [0 0 1];
  706. % hd.gain(end).ax(panel).CameraViewAngle = 10.134;
  707. hd.gain(end).ax(panel).CameraViewAngle = 9;
  708. end
  709. hd.gain(end).ax(4) = axes;
  710. hd.gain(end).ax(4).Position = hd.gain(end).ax(1).Position - [.13 -.1 .2 .2];
  711. hd.gain(end).ax(4).CLim = hd.gain(end).ax(1).CLim;
  712. hd.gain(end).ob(4,1) = colorbar;
  713. hd.gain(end).ob(4,1).Label.String = 'OKR gain';
  714. hd.gain(end).ob(4,1).Label.Units = 'normalized';
  715. hd.gain(end).ob(4,1).Label.Position = [-.8 1 1] .* hd.gain(end).ob(4,1).Label.Position;
  716. [hd.gain(end).ob(4,1).Label.FontSize, hd.gain(end).ob(4,1).FontSize, ...
  717. hd.gain(end).ax.FontSize] = deal(14);
  718. [hd.gain(end).ax(1).Title.FontSize, ...
  719. hd.gain(end).ax(2).Title.FontSize, ...
  720. hd.gain(end).ax(3).Title.FontSize] = deal(14);
  721. [hd.gain(end).ax(1).Title.FontWeight, ...
  722. hd.gain(end).ax(2).Title.FontWeight, ...
  723. hd.gain(end).ax(3).Title.FontWeight] = deal('normal');
  724. [hd.gain(end).ax.Visible] = deal('off');
  725. % [hd.gain(end).ax.Title.Visible] = deal('on');
  726. set(([hd.gain(end).ax.Title]),'Visible','on')
  727. set(([hd.gain(end).ax.Title]),'Position',[1 1 .7] .* hd.gain(end).ax(1).Title.Position)
  728. end
  729. function [retained,original] = discardTrial(original,folderstring)
  730. % DISCARDTRIAL removes data obtained from trials consider "poorly fit" etc.
  731. %
  732. % This function takes a "result" structure as its only input argument, and
  733. % requires an XLSX file containing the indices of trials to be discarded.
  734. % It then removes those bad trials, and returns a reduced structure as its
  735. % primary output argument, "retained". For easy access, it also returns its
  736. % original input.
  737. %
  738. % This function can safely be executed multiple times, as "discarded"
  739. % trials are in fact just set to empty, so their array indices are not
  740. % reassigned to other trials.
  741. %
  742. % Written by Florian Alexander Dehmelt, Tuebingen University, in 2018.
  743. % Identify and load an Excel file containing the indices of all unwanted
  744. % trials to be discarded. This file must contain four columns of numbers
  745. % (from left to right: fish no., eye no., stimulus type, and repetition.
  746. % It may contain as many text comments as desired; these will be ignored.
  747. folder.badtrial = folderstring;
  748. % folder.badtrial = 'C:\Users\fdehmelt\Dropbox\ThinkPad Dropbox\MATLAB\201808 SphereAnalysis\Sample Data\D series\';
  749. regularset.badtrial = 'DiscardTrial.xlsx';
  750. badtrial = xlsread([folder.badtrial,regularset.badtrial]);
  751. % The "retained" structure is the same as the "original" one...
  752. retained = original;
  753. % ...except that unwanted trials are overwritten by empty values.
  754. % They thus appear the same way as non-existent trials already did.
  755. for trial = 1:size(badtrial,1)
  756. % Find the position of the bad trial within the structure.
  757. fish = badtrial(trial,1);
  758. eye = badtrial(trial,2);
  759. stimtype = badtrial(trial,3) + 1; % The +1 converts index >=0 to >=1.
  760. repetition = badtrial(trial,4);
  761. % Overwrite all field entries with "[]".
  762. fieldname = fieldnames(retained);
  763. for field = 1:numel(fieldname)
  764. retained(fish,eye,stimtype,repetition).(fieldname{field}) = [];
  765. end
  766. end
  767. end
  768. function [altered,original] = zeroTrial(original,folderstring)
  769. % ZEROTRIAL sets gains to zero based on visual inspection of trials.
  770. %
  771. % This function takes a "result" structure as its only input argument, and
  772. % requires an XLSX file containing the indices of trials to be set to zero.
  773. % It then adjusts those gains, and returns an altered structure as its
  774. % primary output argument, "retained". For easy access, it also returns its
  775. % original input.
  776. %
  777. % This function can safely be executed multiple times, as "altered"
  778. % trials are in fact just set to zero, so their array indices are not
  779. % reassigned to other trials.
  780. %
  781. % Written by Florian Alexander Dehmelt, Tuebingen University, in 2018.
  782. % Identify and load an Excel file containing the indices of all unwanted
  783. % trials to be discarded. This file must contain four columns of numbers
  784. % (from left to right: fish no., eye no., stimulus type, and repetition.
  785. % It may contain as many text comments as desired; these will be ignored.
  786. folder.badtrial = folderstring;
  787. % folder.badtrial = 'C:\Users\fdehmelt\Dropbox\ThinkPad Dropbox\MATLAB\201808 SphereAnalysis\Sample Data\D series\';
  788. regularset.badtrial = 'ZeroTrial.xlsx';
  789. badtrial = xlsread([folder.badtrial,regularset.badtrial]);
  790. % The "altered" structure is the same as the "original" one...
  791. altered = original;
  792. % ...except that gains of visually identified trials are set to zero.
  793. % They thus appear the same way as non-existent trials already did.
  794. for trial = 1:size(badtrial,1)
  795. % Find the position of the bad trial within the structure.
  796. fish = badtrial(trial,1);
  797. eye = badtrial(trial,2);
  798. stimtype = badtrial(trial,3) + 1; % The +1 converts index >=0 to >=1.
  799. repetition = badtrial(trial,4);
  800. % Overwrite the gain field entries with "0", retain all other values.
  801. altered(fish,eye,stimtype,repetition).gain = 0;
  802. end
  803. end
  804. function combinedvalue = vonmisesfisher9param(param,predictor)
  805. %VONMISESFISHER9PARAM Evaluate two overlapping von Mises-Fisher distributions.
  806. %
  807. % VALUE = VONMISESFISHER9PARAM computes the value of two(!) overlapping,
  808. % spherical (p=3) von Mises-Fisher distributions at the chosen location.
  809. %
  810. % It is formatted to match the input/output structure required by the
  811. % built-in non-linear regression function nlinfit.
  812. convention = 'CCW';
  813. dimension = 3;
  814. radius = 1;
  815. if size(predictor,2) == 2
  816. predictor(:,3) = radius*ones(size(predictor(:,1)));
  817. end
  818. [x,y,z] = geo2car(predictor(:,1),predictor(:,2),predictor(:,3),convention);
  819. direction = [x,y,z];
  820. numberofdistributions = 2;
  821. value = NaN(size(predictor,1),numberofdistributions);
  822. for k = 1:numberofdistributions
  823. offset = param(1);
  824. amplitude = param(4*k-2);
  825. meanazimuth = param(4*k-1);
  826. meanelevation = param(4*k);
  827. concentration = param(4*k+1);
  828. [meanx,meany,meanz] = geo2car(meanazimuth,meanelevation,radius,convention);
  829. meandirection = [meanx,meany,meanz];
  830. bessel = concentration / ...
  831. (2*pi * (exp(concentration) - exp(-concentration)));
  832. normalisation = concentration^(dimension/2 - 1) / ...
  833. ((2*pi)^(dimension/2) * bessel);
  834. value(:,k) = normalisation * exp(concentration*meandirection*direction')';
  835. value(:,k) = amplitude * value(:,k) + offset;
  836. % HACK to constrain values
  837. if abs(meanazimuth) > 180 || ...
  838. abs(meanelevation) > 90 || ...
  839. amplitude < 0 || ...
  840. concentration < 0
  841. value(:,k) = 1e10*ones(size(value(:,k)));
  842. end
  843. end
  844. combinedvalue = sum(value,2);
  845. end
  846. function [x,y,z] = geo2car(azimuth,elevation,radius,convention)
  847. switch convention
  848. case 'CCW'
  849. x = radius .* cosd(elevation).*cosd(azimuth);
  850. y = radius .* cosd(elevation).*sind(azimuth);
  851. z = radius .* sind(elevation);
  852. case 'CW'
  853. x = radius .* cosd(elevation).*cosd(azimuth);
  854. y = radius .* cosd(elevation).*-sind(azimuth);
  855. z = radius .* sind(elevation);
  856. otherwise
  857. error('Convention not supported.')
  858. end
  859. end
  860. function [azimuth,elevation,radius] = car2geo(x,y,z,convention)
  861. azimuth = atand(y./x) + 180*((x<0).*(y>=0) - (x<0).*(y<0));
  862. switch convention
  863. case 'CCW'
  864. case 'CW'
  865. azimuth = -azimuth;
  866. otherwise
  867. error('Convention not supported.')
  868. end
  869. radius = sqrt(x.^2+y.^2+z.^2);
  870. elevation = asind(z./radius);
  871. end
  872. function [lefteyehandle,righteyehandle,bodyhandle] = ...
  873. plot3dFish(scale,yaw,pitch,roll,colour)
  874. % This corresponds to version 20170505a, a.k.a. plot3dFish20170505a.m.
  875. set(gcf,'Color',[1 1 1])
  876. resolution = 10;
  877. % colour.eye = .1*[1 1 1];
  878. % colour.body = .7*[1 1 1];
  879. eye.x = scale * (cos(-pi:pi/resolution:3*pi-pi/resolution)');
  880. eye.y = scale * (0.7*sin(0:pi/resolution:4*pi-pi/resolution)');
  881. eye.z = scale * ([.7*ones(2*resolution,1); ...
  882. 1*ones(2*resolution,1)]);
  883. N = 2*resolution;
  884. lefteye.vertices = [eye.x, eye.y, eye.z];
  885. righteye.vertices = [eye.x, -eye.y, eye.z];
  886. lefteye.faces = [[1:2*resolution 1]; ...
  887. [1+2*resolution:4*resolution 1+2*resolution]];
  888. for k = 1:N
  889. lefteye.faces = [lefteye.faces; ...
  890. [mod([k-1 k],N)+1, ...
  891. mod([k k-1],N)+1+N, ...
  892. mod(k-1,N)+1, ...
  893. NaN(1,N-4)]];
  894. end
  895. lefteye.facevertexcdata = repmat(colour.eye,[size(lefteye.faces,1) 1]);
  896. righteye.faces = lefteye.faces;
  897. righteye.facevertexcdata = lefteye.facevertexcdata;
  898. lefteyehandle = patch(lefteye);
  899. righteyehandle = patch(righteye);
  900. set([lefteyehandle,righteyehandle],'FaceColor','flat','EdgeColor','none')
  901. mainbody.x = scale * (-.1 + [1.0*cos(-pi:pi/resolution:pi-pi/resolution), ...
  902. 0.4*cos(-pi:pi/resolution:pi-pi/resolution)]');
  903. mainbody.y = scale * ([0.7*sin(0:pi/resolution:2*pi-pi/resolution), ...
  904. 0.2*sin(0:pi/resolution:2*pi-pi/resolution)]');
  905. mainbody.z = scale * ([1.1*ones(2*resolution,1); ...
  906. -7*ones(2*resolution,1)]);
  907. N = 2*resolution;
  908. body.vertices = [mainbody.x, mainbody.y, mainbody.z];
  909. body.faces = [[1:2*resolution 1]; ...
  910. [1+2*resolution:4*resolution 1+2*resolution]];
  911. for k = 1:N
  912. body.faces = [body.faces; ...
  913. [mod([k-1 k],N)+1, ...
  914. mod([k k-1],N)+1+N, ...
  915. mod(k-1,N)+1, ...
  916. NaN(1,N-4)]];
  917. end
  918. body.facevertexcdata = repmat(colour.body,[size(body.faces,1) 1]);
  919. bodyhandle = patch(body);
  920. set(bodyhandle,'FaceColor','flat','EdgeColor','none')
  921. rotate(lefteyehandle,[1 0 0],80)
  922. rotate(lefteyehandle,[0 0 1],-10)
  923. rotate(righteyehandle,[1 0 0],-80)
  924. rotate(righteyehandle,[0 0 1],10)
  925. rotate(bodyhandle,[0 1 0],-90)
  926. rotate(lefteyehandle,[0 0 1],yaw)
  927. rotate(righteyehandle,[0 0 1],yaw)
  928. rotate(bodyhandle,[0 0 1],yaw)
  929. rotate(lefteyehandle,[0 1 0],pitch)
  930. rotate(righteyehandle,[0 1 0],pitch)
  931. rotate(bodyhandle,[0 1 0],pitch)
  932. rotate(lefteyehandle,[1 0 0],roll)
  933. rotate(righteyehandle,[1 0 0],roll)
  934. rotate(bodyhandle,[1 0 0],roll)
  935. end
  936. function [hd,stim] = displayDiskStimuli(choice)
  937. warning('off','MATLAB:interp1:UsePCHIP')
  938. % The line above deactivates the following cbrewer warning:
  939. % Warning: INTERP1(...,'CUBIC') will change in a future release. Use INTERP1(...,'PCHIP') instead.
  940. % > In interp1>sanitycheckmethod (line 265)
  941. % In interp1>parseinputs (line 401)
  942. % In interp1 (line 80)
  943. % In interpolate_cbrewer (line 31)
  944. % In cbrewer (line 101)
  945. % In heatmap20180823 (line 21)
  946. % close all
  947. % clf
  948. regularset.folder = '..\..\data\Fig3 default disk data\Data D Series (neue Experimente)\';
  949. datarange = 9:46;
  950. % regularset.folder = '..\..\data\Fig5 size data\Data A Series (A-Series)\';
  951. % datarange = 2:43;
  952. % datarange = 2:8;
  953. % regularset.folder = '..\..\data\Fig5 frequency data\Data V Series (V-Serie)\';
  954. % datarange = 2:43;
  955. regularset.stimulus = 'Stimulus.xlsx';
  956. [~,stim.tableconvention,~] = xlsread([regularset.folder,regularset.stimulus],'C1:C1');
  957. stim.azimuth = xlsread([regularset.folder,regularset.stimulus],['C',num2str(datarange(1)),':C',num2str(datarange(end))]);
  958. stim.elevation = xlsread([regularset.folder,regularset.stimulus],['D',num2str(datarange(1)),':D',num2str(datarange(end))]);
  959. % stim.radius = xlsread([regularset.folder,regularset.stimulus],['E',num2str(datarange(1)),':E',num2str(datarange(end))]);
  960. % stim.azimuth = stim.azimuth(1:2:end);
  961. % stim.elevation = stim.elevation(1:2:end);
  962. stim.radius = 40*ones(size(stim.elevation));
  963. % % [surface.]backgroundSphere
  964. % % compute background sphere with shading
  965. % [surface.x,surface.y,surface.z] = sphere(100);
  966. % reference.geo = [30;20;1]; % desired azimuth, elevation, radius of maximum colour value
  967. % reference.azim = reference.geo(1);
  968. % reference.elev = reference.geo(2);
  969. % reference.rads = ones(size(reference.geo(1)));
  970. % [reference.x,reference.y,reference.z] = geo2car(reference.azim,reference.elev,reference.rads,'CW');
  971. % scalarproduct = [surface.x(:),surface.y(:),surface.z(:)] * [reference.x;reference.y;reference.z];
  972. % surface.c = reshape(scalarproduct,size(surface.x));
  973. % surface.c = 10.^(surface.c+1);
  974. % % surface.c = 2.^(surface.c+1);
  975. % compute disk boundaries
  976. diskcentre.geo = [stim.azimuth, stim.elevation];
  977. diskcentre.angle = stim.radius;
  978. % diskcentre.geo = [ 45 20; ...
  979. % 30 5; ...
  980. % 60 -45];
  981. % diskcentre.angle = 40 * [ones(size(diskcentre.geo,1),1)];
  982. diskcentre.azim = diskcentre.geo(:,1);
  983. diskcentre.elev = diskcentre.geo(:,2);
  984. diskcentre.rads = ones(size(diskcentre.geo(:,1)));
  985. resolution = .2;
  986. numdisk = size(diskcentre.geo,1);
  987. for disk = 1:numdisk
  988. [boundary.azim{disk}, ...
  989. boundary.elev{disk}] = diskBoundary(diskcentre.azim(disk), ...
  990. diskcentre.elev(disk), diskcentre.angle(disk), resolution);
  991. [boundary.x{disk}, ...
  992. boundary.y{disk}, ...
  993. boundary.z{disk}] = geo2car(boundary.azim{disk}, boundary.elev{disk}, ...
  994. ones(size(boundary.azim{disk})),'CW');
  995. end
  996. [diskcentre.x,diskcentre.y,diskcentre.z] = ...
  997. geo2car(diskcentre.azim,diskcentre.elev,diskcentre.rads,'CW');
  998. [textcentre.x,textcentre.y,textcentre.z] = ...
  999. geo2car(diskcentre.azim,.91*diskcentre.elev,1.10*diskcentre.rads,'CW');
  1000. % create figure
  1001. hd.default.fg = figure();
  1002. hd.default.fg.Name = 'This is a sphere.';
  1003. hd.default.fg.Position = [50 50 500 800];
  1004. hd.default.fg.Color = [1 1 1];
  1005. hd.default.ax(1) = axes;
  1006. hd.default.ax(1).Position = [0 0 1 1];
  1007. hold on
  1008. % plot background sphere with shading
  1009. hd.default.globe = displaySphere;
  1010. % hd.default.globe = surf(surface.x,surface.y,surface.z,surface.c);
  1011. % shading flat
  1012. % axis square
  1013. % greyscale = (.8:.01:.99)' * [1 1 1];
  1014. % colormap(greyscale)
  1015. fish.scale = .05;
  1016. fish.yaw = 180;
  1017. fish.pitch = 0;
  1018. fish.roll = 0;
  1019. colour.eye = 0*[1 1 1];
  1020. colour.body = .5*[1 1 1];
  1021. plot3dFish(fish.scale,fish.yaw,fish.pitch,fish.roll,colour);
  1022. hd.default.globe.FaceAlpha = .5;
  1023. cropdisk = 8;
  1024. switch choice
  1025. case 'left'
  1026. disklist = 1 : numdisk/2;
  1027. case 'right'
  1028. disklist = numdisk/2+1 : numdisk;
  1029. case 'single'
  1030. disklist = cropdisk;
  1031. end
  1032. % plot disk boundaries
  1033. % diskcolour = cbrewer('seq','Greens',numdisk*2);
  1034. cbrewergreen = cbrewer('seq','Greens',numdisk);
  1035. diskcolour = repmat(cbrewergreen(round(numdisk/1.4),:),[numdisk,1]);
  1036. % diskcolour = flipud(cbrewer('seq','Greys',numdisk*2));
  1037. % diskcolour = diskcolour(randperm(numdisk),:);
  1038. for disk = disklist
  1039. if sum(disk==cropdisk)
  1040. plot3(boundary.x{disk},boundary.y{disk},boundary.z{disk},'-','LineWidth',2,'Color',0*[1 1 1])
  1041. disktext(disk) = text(textcentre.x(disk),textcentre.y(disk),textcentre.z(disk), ...
  1042. ['',num2str(disk)], ...
  1043. 'FontSize', 13, 'HorizontalAlignment','center','VerticalAlignment','middle');
  1044. else
  1045. plot3(boundary.x{disk},boundary.y{disk},boundary.z{disk},'-','LineWidth',1,'Color',.6*[1 1 1])
  1046. if sum(disk==disklist)
  1047. disktext(disk) = text(textcentre.x(disk),textcentre.y(disk),textcentre.z(disk), ...
  1048. ['',num2str(disk)], ...
  1049. 'FontSize', 13, 'HorizontalAlignment','center','VerticalAlignment','middle');
  1050. end
  1051. % plot3(boundary.x{disk},boundary.y{disk},boundary.z{disk},'-','LineWidth',2,'Color',1*[1 1 1])
  1052. % plot3(boundary.x{disk},boundary.y{disk},boundary.z{disk},'-','LineWidth',1,'Color',diskcolour(disk,:))
  1053. % plot3(boundary.x{disk},boundary.y{disk},boundary.z{disk},'-','LineWidth',1,'Color',.2*[1 1 1])
  1054. end
  1055. end
  1056. hold off
  1057. switch choice
  1058. case 'single'
  1059. disktext(cropdisk).Visible = 'off';
  1060. barwidth = 360/22/2;
  1061. % gratingazimuth = -180:barwidth:(180-barwidth);
  1062. gratingazimuth = -180:barwidth:-80;
  1063. for bar = 1:2:numel(gratingazimuth)-1
  1064. % if bar < numel(gratingazimuth)
  1065. azimlimit = gratingazimuth([bar bar+1]);
  1066. % else
  1067. % azimlimit = gratingazimuth([bar 1]);
  1068. % end
  1069. displaySphereBar(azimlimit(1),azimlimit(2),boundary.azim{cropdisk},boundary.elev{cropdisk})
  1070. end
  1071. case {'left','right'}
  1072. disktext(disklist(ceil(end/2))).Position = ...
  1073. disktext(disklist(ceil(end/2))).Position - [0 0 .16];
  1074. end
  1075. % hd.default.ax(1).CameraPosition = [2.3 17 2.2];
  1076. switch choice
  1077. case {'left','single'}
  1078. hd.default.ax(1).CameraPosition = [0 20 0];
  1079. case 'right'
  1080. hd.default.ax(1).CameraPosition = [0 -20 0];
  1081. end
  1082. hd.default.ax(1).CameraViewAngle = 6;
  1083. [hd.default.ax(1).XAxis.Visible, ...
  1084. hd.default.ax(1).YAxis.Visible, ...
  1085. hd.default.ax(1).ZAxis.Visible] = deal('off');
  1086. end
  1087. function globe = displaySphere
  1088. [surface.x,surface.y,surface.z] = sphere(300);
  1089. reference.geo = [30;20;1]; % desired azimuth, elevation, radius of maximum colour value
  1090. reference.azim = reference.geo(1);
  1091. reference.elev = reference.geo(2);
  1092. reference.rads = ones(size(reference.geo(1)));
  1093. [reference.x,reference.y,reference.z] = geo2car(reference.azim,reference.elev,reference.rads,'CW');
  1094. scalarproduct = [surface.x(:),surface.y(:),surface.z(:)] * [reference.x;reference.y;reference.z];
  1095. surface.c = reshape(scalarproduct,size(surface.x));
  1096. surface.c = 10.^(surface.c);
  1097. globe = surf(surface.x,surface.y,surface.z,surface.c);
  1098. shading interp
  1099. axis square
  1100. greyscale = (.8:.01:.99)' * [1 1 1];
  1101. % greyscale = (.9:.01:.99)' * [1 1 1];
  1102. colormap(greyscale)
  1103. end
  1104. function [azimuthOut, elevationOut] = diskBoundary(azimuthIn, elevationIn, angleIn, resolutionIn)
  1105. % The following equation describes orthodromic (or "great circle") distance as an angle:
  1106. % arccos(sin(centre.elevation)*sin(border.elevation) + ...
  1107. % cos(centre.elevation)*cos(border.elevation)*cos(border.azimuth-centre.azimuth)) = angle;
  1108. % This equation is used to compute border.azimuth below.
  1109. centre.azimuth = azimuthIn;
  1110. centre.elevation = elevationIn;
  1111. border.angle = angleIn / 2; % convert solid angle ("diameter") to "radius"
  1112. border.resolution = resolutionIn;
  1113. intended = -90 : border.resolution : 90;
  1114. % limit = cosd([centre.elevation-border.angle centre.elevation+border.angle]);
  1115. % intended = acosd(min(limit) : border.resolution : max(limit));
  1116. % "auxiliary" elevations are explicitly added to guarantee coverage near top/bottom of circle:
  1117. auxiliary = centre.elevation + border.angle*[-1+1e-9 1-1e-9];
  1118. border.elevation = unique([intended,auxiliary]);
  1119. border.azimuth = centre.azimuth + ...
  1120. acosd((cosd(border.angle) - sind(centre.elevation)*sind(border.elevation)) ./ ...
  1121. (cosd(centre.elevation)*cosd(border.elevation)));
  1122. % Eliminate complex elements...
  1123. border.azimuth(imag(border.azimuth)~=0) = NaN; % This statement sometimes fails horribly.
  1124. % border.azimuth(imag(border.azimuth)>1e-5) = NaN; % This statement is silly, but more stable.
  1125. % ...and compute corresponding values for the second half of the boundary.
  1126. border.azimuth = [border.azimuth, 2*centre.azimuth-border.azimuth];
  1127. border.elevation = [border.elevation, border.elevation];
  1128. border.azimuth = mod(border.azimuth+180,360)-168;
  1129. azimuthOut = border.azimuth';
  1130. elevationOut = border.elevation';
  1131. end
  1132. function hd = displaySphereBar(azimlimit1,azimlimit2,boundaryazim,boundaryelev)
  1133. resolution = .01;
  1134. if azimlimit2 < azimlimit1
  1135. error('sdasdsa')
  1136. end
  1137. crop.index = logical((azimlimit1 <= boundaryazim) .* (boundaryazim <= azimlimit2));
  1138. if sum(crop.index) > 0
  1139. crop.azim = boundaryazim(crop.index)';
  1140. crop.elev = boundaryelev(crop.index)';
  1141. crop.azim1 = crop.azim(diff(crop.azim) > 0);
  1142. crop.azim2 = crop.azim(diff(crop.azim) < 0);
  1143. crop.elev1 = crop.elev(diff(crop.azim) > 0);
  1144. crop.elev2 = crop.elev(diff(crop.azim) < 0);
  1145. sequence = @(a,b) a:(sign(b-a)*resolution):b;
  1146. edge.elev1 = sequence(crop.elev1(1), crop.elev2(end));
  1147. edge.elev2 = sequence(crop.elev2(1), crop.elev1(end));
  1148. % edge.elev1 = sequence(crop.elev1(1), crop.elev2(end));
  1149. % edge.elev2 = sequence(crop.elev2(1), crop.elev1(end));
  1150. edge.azim1 = azimlimit1*ones(size(edge.elev1));
  1151. edge.azim2 = azimlimit2*ones(size(edge.elev2));
  1152. azim = [edge.azim1,fliplr(crop.azim2),edge.azim2,fliplr(crop.azim1)];
  1153. elev = [edge.elev1,fliplr(crop.elev2),edge.elev2,fliplr(crop.elev1)];
  1154. rads = 1.009*ones(size(elev));
  1155. [bar.x,bar.y,bar.z] = geo2car(azim,elev,rads,'CW');
  1156. hold on, p = patch(bar.x,bar.y,bar.z,'k'); hold off
  1157. % p.FaceColor = [.2 .8 .4];
  1158. p.FaceColor = .2*[1 1 1];
  1159. p.EdgeColor = 'none';
  1160. % hold on, plot3(bar.x,bar.y,bar.z,'r'); hold off
  1161. end
  1162. end