From 2832b7a8c9998c5d8bf4e6cce42a629bcb801b71 Mon Sep 17 00:00:00 2001 From: Rob Campbell Date: Sat, 11 Mar 2017 09:44:52 +0100 Subject: [PATCH 1/9] Tidy code, improve comments, fix a typo. --- README.md | 2 +- code/notBoxPlot.m | 192 ++++++++++++++++++++++++---------------------- 2 files changed, 101 insertions(+), 93 deletions(-) diff --git a/README.md b/README.md index 6a8bbc2..84e7809 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Although it's worked well for situations I've needed it, I will be happy to modi ## Features - Easily mix a variety of plot styles on one figure -- Easy post-hod modification of plot parameters via returned function handles +- Easy post-hoc modification of plot parameters via returned function handles - Statistics (mean, SD, etc) optionally returned - Optional plotting of median in addition to mean - Option to plot either a 95% confidence interval for the mean or a 95% t-interval diff --git a/code/notBoxPlot.m b/code/notBoxPlot.m index bcf940c..c37c3d1 100644 --- a/code/notBoxPlot.m +++ b/code/notBoxPlot.m @@ -129,7 +129,7 @@ % notBoxPlot(y,[],'interval','tInterval'), title('95% t-interval (n=8)') % % -% Example 8 - Add the median (dotted line) to plots +% Example 9 - Add the median (dotted line) to plots % clf % n=[5,10,20,40]; % for ii=1:4, rng(555), notBoxPlot(rand(1,n(ii)),ii,'markMedian',true), end @@ -142,7 +142,7 @@ - + % Check input arguments if nargin==0 help(mfilename) @@ -150,14 +150,19 @@ end -if isvector(y), y=y(:); end +if isvector(y) + y=y(:); +end + +%Generate an monotonically increasing X variable if the user didn't supply anything if nargin<2 || isempty(x) x=1:size(y,2); end if ~isempty(varargin) + %Check if these look like legacy input arguments if isnumeric(varargin{1}) jitter=varargin{1}; legacyCall=true; @@ -179,6 +184,8 @@ error('notBoxPlot:legacyError','You have mixed legacy with new-style calls. See function help') end + +%If the user fed in legacy arguments, we generate new-style arguments that can be fed to the inputParser if legacyCall if nargin<4 style='patch'; %Can also be 'line' or 'sdline' @@ -196,9 +203,9 @@ - + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -% Parse input arguments if new-style args were provided +% Parse input arguments (including converted legacy arguments) if ~legacyCall params = inputParser; @@ -211,9 +218,9 @@ params.parse(varargin{:}); %Extract values from the inputParser - jitter = params.Results.jitter; - style = params.Results.style; - interval = params.Results.interval; + jitter = params.Results.jitter; + style = params.Results.style; + interval = params.Results.interval; markMedian = params.Results.markMedian; %Set interval function @@ -225,7 +232,6 @@ otherwise error('Interval %s is unknown',interval) end - end @@ -246,7 +252,7 @@ u=unique(x); for ii=1:length(u) - f=x==u(ii); + f = x==u(ii); h(ii)=notBoxPlot(y(f),u(ii),varargin{:}); %recursive call end @@ -265,8 +271,6 @@ end - - if length(x) ~= size(y,2) error('length of x doesn''t match the number of columns in y') @@ -274,9 +278,6 @@ - - - %We're going to render points with the same x value in different %colors so we loop through all unique x values and do the plotting @@ -304,6 +305,7 @@ end +%handle the output arguments if nargout>0 varargout{1}=H; end @@ -324,100 +326,106 @@ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% function [h,statsOut]=myPlotter(X,Y) - SEM=intervalFun(Y); %Supplied external function - SD=std(Y,'omitnan'); %Requires the stats toolbox - mu=mean(Y,'omitnan'); %Requires the stats toolbox - if markMedian - med = median(Y,'omitnan'); - end + SEM=intervalFun(Y); %Supplied external function + SD=std(Y,'omitnan'); %Requires the stats toolbox + mu=mean(Y,'omitnan'); %Requires the stats toolbox + if markMedian + med = median(Y,'omitnan'); + end - %The plot colors to use for multiple sets of points on the same x - %location - cols=hsv(length(X)+1)*0.5; - cols(1,:)=0; - jitScale=jitter*0.55; %To scale the patch by the width of the jitter + %The plot colors to use for multiple sets of points on the same x + %location + cols=hsv(length(X)+1)*0.5; + cols(1,:)=0; + jitScale=jitter*0.55; %To scale the patch by the width of the jitter - for k=1:length(X) - thisY=Y(:,k); - thisY=thisY(~isnan(thisY)); - thisX=repmat(X(k),1,length(thisY)); + for k=1:length(X) + thisY=Y(:,k); + thisY=thisY(~isnan(thisY)); + thisX=repmat(X(k),1,length(thisY)); - if strcmp(style,'patch') - h(k).sdPtch=patchMaker(SD(k),[0.6,0.6,1]); - end + %Assemble stats for optional command line output + statsOut(k).mu = mu(k); + statsOut(k).interval = SEM(k); + statsOut(k).sd = SD(k); - %For optional command line output - statsOut(k).mu = mu(k); - statsOut(k).interval = SEM(k); - statsOut(k).sd = SD(k); + %Add the SD as a patch if the user asked for this + if strcmp(style,'patch') + h(k).sdPtch=patchMaker(SD(k),[0.6,0.6,1]); + end - if strcmp(style,'patch') || strcmp(style,'sdline') - %Plot mean and SEM (and optionally the median) - h(k).semPtch=patchMaker(SEM(k),[1,0.6,0.6]); - h(k).mu=plot([X(k)-jitScale,X(k)+jitScale],[mu(k),mu(k)],'-r',... - 'linewidth',2); - if markMedian - statsOut(k).median = med(k); - h(k).med=plot([X(k)-jitScale,X(k)+jitScale],[med(k),med(k)],':r',... + %Build patch surfaces for SEM, the means, and optionally the medians + if strcmp(style,'patch') || strcmp(style,'sdline') + h(k).semPtch=patchMaker(SEM(k),[1,0.6,0.6]); + h(k).mu=plot([X(k)-jitScale,X(k)+jitScale],[mu(k),mu(k)],'-r',... 'linewidth',2); - end - end - - %Plot jittered raw data - C=cols(k,:); - J=(rand(size(thisX))-0.5)*jitter; - - h(k).data=plot(thisX+J, thisY, 'o', 'color', C,... - 'markerfacecolor', C+(1-C)*0.65); - end - - if strcmp(style,'line') || strcmp(style,'sdline') - for k=1:length(X) - %Plot SD - h(k).sd=plot([X(k),X(k)],[mu(k)-SD(k),mu(k)+SD(k)],... - '-','color',[0.2,0.2,1],'linewidth',2); - set(h(k).sd,'ZData',[1,1]*-1) - end - end - - if strcmp(style,'line') - for k=1:length(X) - %Plot mean and SEM (and optionally the median) - h(k).mu=plot(X(k),mu(k),'o','color','r',... - 'markerfacecolor','r',... - 'markersize',10); + if markMedian + statsOut(k).median = med(k); + h(k).med=plot([X(k)-jitScale,X(k)+jitScale],[med(k),med(k)],':r',... + 'linewidth',2); + end + end - h(k).sem=plot([X(k),X(k)],[mu(k)-SEM(k),mu(k)+SEM(k)],'-r',... - 'linewidth',2); - if markMedian - h(k).med=plot(X(k),med(k),'s','color',[0.8,0,0],... - 'markerfacecolor','none',... - 'lineWidth',2,... - 'markersize',12); + %Overlay the jittered raw data + C=cols(k,:); + J=(rand(size(thisX))-0.5)*jitter; + + h(k).data=plot(thisX+J, thisY, 'o', 'color', C,... + 'markerfacecolor', C+(1-C)*0.65); + end %for k=1:length(X) + + + %Plot SD as a line + if strcmp(style,'line') || strcmp(style,'sdline') + for k=1:length(X) + h(k).sd=plot([X(k),X(k)],[mu(k)-SD(k),mu(k)+SD(k)],... + '-','color',[0.2,0.2,1],'linewidth',2); + set(h(k).sd,'ZData',[1,1]*-1) end + end - h(k).xAxisLocation=x(k); - end - end - for ii=1:length(h) - h(ii).interval=interval; - end + %Plot mean and SEM as a line, the means, and optionally the medians + if strcmp(style,'line') + for k=1:length(X) + + h(k).mu=plot(X(k),mu(k),'o','color','r',... + 'markerfacecolor','r',... + 'markersize',10); + + h(k).sem=plot([X(k),X(k)],[mu(k)-SEM(k),mu(k)+SEM(k)],'-r',... + 'linewidth',2); + if markMedian + h(k).med=plot(X(k),med(k),'s','color',[0.8,0,0],... + 'markerfacecolor','none',... + 'lineWidth',2,... + 'markersize',12); + end + + h(k).xAxisLocation=x(k); + end + end % if strcmp(style,'line') + for ii=1:length(h) + h(ii).interval=interval; + end - function ptch=patchMaker(thisInterval,color) - l=mu(k)-thisInterval; - u=mu(k)+thisInterval; - ptch=patch([X(k)-jitScale, X(k)+jitScale, X(k)+jitScale, X(k)-jitScale],... + + + function ptch=patchMaker(thisInterval,color) + %This nested function builds a patch for the SD or SEM + l=mu(k)-thisInterval; + u=mu(k)+thisInterval; + ptch=patch([X(k)-jitScale, X(k)+jitScale, X(k)+jitScale, X(k)-jitScale],... [l,l,u,u], 0); - set(ptch,'edgecolor',color*0.8,'facecolor',color) - end %function patchMaker + set(ptch,'edgecolor',color*0.8,'facecolor',color) + end %function patchMaker + + - - end %function myPlotter From 26483288678b2c10ced7426172a08f69ec66d7b9 Mon Sep 17 00:00:00 2001 From: Rob Campbell Date: Sat, 11 Mar 2017 10:02:03 +0100 Subject: [PATCH 2/9] MAJOR: remove handling of legacy notBoxPlot call --- code/notBoxPlot.m | 110 ++++++++++++++-------------------------------- 1 file changed, 32 insertions(+), 78 deletions(-) diff --git a/code/notBoxPlot.m b/code/notBoxPlot.m index c37c3d1..0cd6f08 100644 --- a/code/notBoxPlot.m +++ b/code/notBoxPlot.m @@ -155,88 +155,45 @@ end -%Generate an monotonically increasing X variable if the user didn't supply anything +% Generate an monotonically increasing X variable if the user didn't supply anything +% for the grouping variable if nargin<2 || isempty(x) x=1:size(y,2); end - -if ~isempty(varargin) - %Check if these look like legacy input arguments - if isnumeric(varargin{1}) - jitter=varargin{1}; - legacyCall=true; - fprintf(['\n%s called with legacy input arguments. See function help for new call format.\n',... - 'Legacy input arguments will be removed in the next release\n\n'],mfilename) - else - legacyCall=false; - end -else - legacyCall=false; -end - -if legacyCall && isempty(jitter) - jitter=0.3; %larger value means greater amplitude jitter -end - -%Check for innapropriate mixed usage of new and old call types -if (legacyCall && length(varargin)>2) || (~legacyCall && mod(length(varargin),2)>0) - error('notBoxPlot:legacyError','You have mixed legacy with new-style calls. See function help') -end - - -%If the user fed in legacy arguments, we generate new-style arguments that can be fed to the inputParser -if legacyCall - if nargin<4 - style='patch'; %Can also be 'line' or 'sdline' - else - style=lower(varargin{2}); - end - - %TODO: the following will be removed, along with other references to legacy calling, in the next release - varargin={'jitter',jitter,'style',style,'interval','SEM'}; %define varargin so we feed the new-form inputs to the recursive call, below. - intervalFun = @NBP.SEM_calc; - interval= 'SEM'; - markMedian = false; -end - - - +%If x is logical then the function fails. So let's make sure it's a double +x=double(x); %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -% Parse input arguments (including converted legacy arguments) -if ~legacyCall - - params = inputParser; - params.CaseSensitive = false; - params.addParameter('jitter', 0.3, @(x) isnumeric(x) & isscalar(x)); - params.addParameter('style','patch', @(x) ischar(x) && any(strncmp(x,{'patch','line','sdline'},inf)) ); - params.addParameter('interval','SEM', @(x) ischar(x) && any(strncmp(x,{'SEM','tInterval'},inf)) ); - params.addParameter('markMedian', false, @(x) islogical(x)); - - params.parse(varargin{:}); - - %Extract values from the inputParser - jitter = params.Results.jitter; - style = params.Results.style; - interval = params.Results.interval; - markMedian = params.Results.markMedian; - - %Set interval function - switch interval - case 'SEM' - intervalFun = @NBP.SEM_calc; - case 'tInterval' - intervalFun = @NBP.tInterval_calc; - otherwise - error('Interval %s is unknown',interval) - end +% Parse input arguments +params = inputParser; +params.CaseSensitive = false; +params.addParameter('jitter', 0.3, @(x) isnumeric(x) & isscalar(x)); +params.addParameter('style','patch', @(x) ischar(x) && any(strncmp(x,{'patch','line','sdline'},inf)) ); +params.addParameter('interval','SEM', @(x) ischar(x) && any(strncmp(x,{'SEM','tInterval'},inf)) ); +params.addParameter('markMedian', false, @(x) islogical(x)); + +params.parse(varargin{:}); + +%Extract values from the inputParser +jitter = params.Results.jitter; +style = params.Results.style; +interval = params.Results.interval; +markMedian = params.Results.markMedian; + +%Set interval function +switch interval + case 'SEM' + intervalFun = @NBP.SEM_calc; + case 'tInterval' + intervalFun = @NBP.tInterval_calc; + otherwise + error('Interval %s is unknown',interval) end -%If x is logical then the function fails. So let's make sure it's a double -x=double(x); + if jitter==0 && strcmp(style,'patch') warning('A zero value for jitter means no patch object visible') @@ -249,7 +206,7 @@ if length(x)~=length(y) error('length(x) should equal length(y)') end - + u=unique(x); for ii=1:length(u) f = x==u(ii); @@ -262,13 +219,13 @@ xlim([min(u)-1,max(u)+1]) set(gca,'XTick',u) end - + if nargout==1 varargout{1}=h; end return - + end @@ -314,9 +271,6 @@ varargout{2}=stats; end -if nargout>2 - varargout{3}=legacyCall; -end From d0cbaeddf95afeacb9a4e45e93a6907c1237b7f7 Mon Sep 17 00:00:00 2001 From: Rob Campbell Date: Sat, 11 Mar 2017 10:17:34 +0100 Subject: [PATCH 3/9] Add a simple option to parse data in table format. --- code/notBoxPlot.m | 80 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 63 insertions(+), 17 deletions(-) diff --git a/code/notBoxPlot.m b/code/notBoxPlot.m index 0cd6f08..1625e8d 100644 --- a/code/notBoxPlot.m +++ b/code/notBoxPlot.m @@ -17,13 +17,17 @@ % % % Inputs -% y - each column of y is one variable/group. If x is missing or empty -% then each column is plotted in a different x position. +% y - A vector, matrix, or table of the data to plot. +% * vector and no x is provided: all data are grouped at one x position. +% * matrix and no x is provided: each column is plotted in a different x position. +% * vector with x grouping variable provided: data grouped accordig to x +% * a table is treated such that the first column is y and the second x. % % x - [optional], the x axis points at which y columns should be % plotted. This allows more than one set of y values to appear % at one x location. Such instances are coloured differently. % Contrast the first two panels in Example 1 to see how this input behaves. +% x need not be provided if y is a table. % % Note that if x and y are both vectors of the same length this function % behaves like boxplot (see Example 5). @@ -68,9 +72,9 @@ % h=notBoxPlot(randn(10,40)); % d=[h.data]; % set(d(1:4:end),'markerfacecolor',[0.4,1,0.4],'color',[0,0.4,0]) -% +% % Example 2 - overlaying with areas -% clf +% clf % x=[1,2,3,4,5,5]; % y=randn(20,length(x)); % y(:,end)=y(:,end)+3; @@ -101,7 +105,7 @@ % Note: an alternative to the style used in Example 5 is to call % notBoxPlot from a loop in an external function. In this case, the % user will have to take care of the x-ticks and axis limits. -% +% % Example 6 - replacing the SD with bars % clf % y=randn(50,1); @@ -135,13 +139,27 @@ % for ii=1:4, rng(555), notBoxPlot(rand(1,n(ii)),ii,'markMedian',true), end % % +% Example 10 - Table call format +% clf +% albert=[1,1,1,3,2,1,3,3,3,2,2,3,3]'; +% victoria=[7,8,6,1,5,7,2,1,3,4,5,2,4]'; +% M = table(victoria,albert); %place data in first column and groups in the second +% notBoxPlot(M) +% +% Example 11 - Table call format with optional arguments +% clf +% albert=[1,1,1,3,2,1,3,3,3,2,2,3,3]'; +% victoria=[7,8,6,1,5,7,2,1,3,4,5,2,4]'; +% M = table(victoria,albert); %place data in first column and groups in the second +% notBoxPlot(M,'jitter',0.75) +% % % Rob Campbell - August 2016 % -% Also see: boxplot, NBP.example +% Also see: NBP.example, boxplot + - % Check input arguments if nargin==0 @@ -150,21 +168,43 @@ end -if isvector(y) - y=y(:); -end +%Handle table call +if istable(y) + + tableCall=true; + if nargin>1 %so user doesn't need to specify a blank variable for x + if ~isempty(x) + varargin = [x,varargin]; + end + end + thisTable=y; + varNames=thisTable.Properties.VariableNames; + if length(varNames) ~= 2 + fprintf('% s can only handle tables with two variables\n',mfilename) + return + end + y = thisTable.(varNames{1}); + x = thisTable.(varNames{2}); +else + tableCall=false; -% Generate an monotonically increasing X variable if the user didn't supply anything -% for the grouping variable -if nargin<2 || isempty(x) - x=1:size(y,2); + if isvector(y) + y=y(:); + end + + % Generate an monotonically increasing X variable if the user didn't supply anything + % for the grouping variable + if nargin<2 || isempty(x) + x=1:size(y,2); + end end %If x is logical then the function fails. So let's make sure it's a double x=double(x); + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Parse input arguments params = inputParser; @@ -224,6 +264,12 @@ varargout{1}=h; end + %If we had a table we can label the axes + if tableCall + ylabel(varNames{1}) + xlabel(varNames{2}) + end + return end @@ -334,7 +380,7 @@ %Plot SD as a line if strcmp(style,'line') || strcmp(style,'sdline') - for k=1:length(X) + for k=1:length(X) h(k).sd=plot([X(k),X(k)],[mu(k)-SD(k),mu(k)+SD(k)],... '-','color',[0.2,0.2,1],'linewidth',2); set(h(k).sd,'ZData',[1,1]*-1) @@ -344,8 +390,8 @@ %Plot mean and SEM as a line, the means, and optionally the medians if strcmp(style,'line') - for k=1:length(X) - + for k=1:length(X) + h(k).mu=plot(X(k),mu(k),'o','color','r',... 'markerfacecolor','r',... 'markersize',10); From 9db5c8322023dec906d71b56dcb40299ac9b57ea Mon Sep 17 00:00:00 2001 From: Rob Campbell Date: Sat, 11 Mar 2017 11:40:28 +0100 Subject: [PATCH 4/9] Allow user to supply param/val pairs without needing to set x to empty if not supplying this argument. --- code/notBoxPlot.m | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/code/notBoxPlot.m b/code/notBoxPlot.m index 1625e8d..1e82a02 100644 --- a/code/notBoxPlot.m +++ b/code/notBoxPlot.m @@ -193,6 +193,13 @@ y=y(:); end + % Handle case where user doesn't supply X, but have user param/val pairs. e.g. + % notBoxPlot(rand(20,5),'jitter',0.5) + if nargin>2 && ischar(x) + varargin = [x,varargin]; + x=[]; + end + % Generate an monotonically increasing X variable if the user didn't supply anything % for the grouping variable if nargin<2 || isempty(x) From 7e52362e019f15b85d1f6b0b66cde16f8682fe00 Mon Sep 17 00:00:00 2001 From: Rob Campbell Date: Sat, 11 Mar 2017 12:21:36 +0100 Subject: [PATCH 5/9] Organize the more detailed examples into separate functions under NBP --- code/+NBP/SEM_calc.m | 7 +- code/+NBP/jitterExamples.m | 30 ++++++++ code/+NBP/lineExamples.m | 36 +++++++++ code/+NBP/{example.m => showCase.m} | 63 +++++++++------ code/+NBP/simpleExamples.m | 44 +++++++++++ code/+NBP/statsOptionsExample.m | 45 +++++++++++ code/+NBP/tInterval_calc.m | 9 ++- code/+NBP/tableExamples.m | 32 ++++++++ code/notBoxPlot.m | 115 +++++++--------------------- 9 files changed, 263 insertions(+), 118 deletions(-) create mode 100644 code/+NBP/jitterExamples.m create mode 100644 code/+NBP/lineExamples.m rename code/+NBP/{example.m => showCase.m} (63%) create mode 100644 code/+NBP/simpleExamples.m create mode 100644 code/+NBP/statsOptionsExample.m create mode 100644 code/+NBP/tableExamples.m diff --git a/code/+NBP/SEM_calc.m b/code/+NBP/SEM_calc.m index a5fdfa5..ce3e213 100644 --- a/code/+NBP/SEM_calc.m +++ b/code/+NBP/SEM_calc.m @@ -1,7 +1,7 @@ function sem=SEM_calc(vect, CI) % SEM_calc - standard error of the mean, confidence interval % -% function sem=SEM_calc(vect, CI) +% function sem = NBP.SEM_calc(vect, CI) % % Purpose % Calculate the standard error the mean to a given confidence @@ -31,8 +31,10 @@ % plot([mean(r)-S,mean(r)+S], [mean(ylim),mean(ylim)],'r-') % hold off % +% % Rob Campbell % +% % Also see - tInterval_Calc, norminv narginchk(1,2) @@ -51,6 +53,3 @@ sem = ( (nanstd(vect)) ./ sqrt(sum(~isnan(vect))) ) * stdCI ; - - - diff --git a/code/+NBP/jitterExamples.m b/code/+NBP/jitterExamples.m new file mode 100644 index 0000000..9c8efc6 --- /dev/null +++ b/code/+NBP/jitterExamples.m @@ -0,0 +1,30 @@ +function jitterExamples + +% +% % Jitter examples +% % default jitter is 0.3 +% +% clf +% +% R = randn(20,5); +% +% subplot(2,1,1) +% notBoxPlot(R,'jitter',0.15) +% +% subplot(2,1,2) +% notBoxPlot(R,'jitter',0.75); +% + +help(['NBP.',mfilename]) + +clf + +R = randn(20,5); + +subplot(2,1,1) +notBoxPlot(R,'jitter',0.15) + +subplot(2,1,2) +notBoxPlot(R,'jitter',0.75); + + diff --git a/code/+NBP/lineExamples.m b/code/+NBP/lineExamples.m new file mode 100644 index 0000000..4a1b8c5 --- /dev/null +++ b/code/+NBP/lineExamples.m @@ -0,0 +1,36 @@ +function lineExamples + +% % Mixing lines and areas [note that the way notBoxPlot +% % sets the x axis limits can cause problems when combining plots +% % this way] +% +% clf +% +% subplot(1,2,1) +% h=notBoxPlot(randn(10,1)+4,5,'style','line'); +% set(h.data,'color','m') +% h=notBoxPlot(randn(50,10)); +% set(h(5).data,'color','m') +% +% subplot(1,2,2) +% y=randn(50,1); +% clf +% notBoxPlot(y,1,'style','sdline') +% notBoxPlot(y,2) +% xlim([0,3]) + +help(['NBP.',mfilename]) + +clf + +subplot(1,2,1) +h=notBoxPlot(randn(10,1)+4,5,'style','line'); +set(h.data,'color','m') +h=notBoxPlot(randn(50,10)); +set(h(5).data,'color','m') + +subplot(1,2,2) +y=randn(50,1); +notBoxPlot(y,1,'style','sdline') +notBoxPlot(y,2) +xlim([0,3]) \ No newline at end of file diff --git a/code/+NBP/example.m b/code/+NBP/showCase.m similarity index 63% rename from code/+NBP/example.m rename to code/+NBP/showCase.m index dd4a292..31b4236 100644 --- a/code/+NBP/example.m +++ b/code/+NBP/showCase.m @@ -1,13 +1,28 @@ -function example -% Pretty example use of notBoxPlot - -disp('Running example') +function showCase +% Example showing a variety of effects possible with notBoxPlot +% +% function NBP.showCase +% +% +% Purpose +% Showcase notBoxPlot +% +% +% No inputs or outputs +% +% +% Rob Campbell + + + +W=which(['NBP.',mfilename]); +fprintf('Running example located at %s\n',W) hFig=figure(1984); set(hFig,... - 'Name','notBoxPlot example',... - 'PaperPosition',[0,0,12,9]) %Just to make save to disk consistent) + 'Name','notBoxPlot example',... + 'PaperPosition',[0,0,12,9]) %Just to make save to disk consistent) clf W=0.45; %image width @@ -38,16 +53,16 @@ %higher means as green set([H(find(IND)).data],'MarkerSize',4,... - 'markerFaceColor',[1,1,1]*0.25,... - 'markerEdgeColor', 'none') + 'markerFaceColor',[1,1,1]*0.25,... + 'markerEdgeColor', 'none') set([H(find(IND)).semPtch],... - 'FaceColor',[0,0.75,0],... - 'EdgeColor','none') + 'FaceColor',[0,0.75,0],... + 'EdgeColor','none') set([H(find(IND)).sdPtch],... - 'FaceColor',[0.6,1,0.6],... - 'EdgeColor','none') + 'FaceColor',[0.6,1,0.6],... + 'EdgeColor','none') set([H(find(IND)).mu],... - 'Color',[0,0.4,0]) + 'Color',[0,0.4,0]) set(gca,'XTick',[]) @@ -55,16 +70,16 @@ %lower means as gray set([H(find(~IND)).data],'MarkerSize',4,... - 'markerFaceColor',[1,1,1]*0.5,... - 'markerEdgeColor', 'none') + 'markerFaceColor',[1,1,1]*0.5,... + 'markerEdgeColor', 'none') set([H(find(~IND)).semPtch],... - 'FaceColor',[1,1,1]*0.25,... - 'EdgeColor','none') + 'FaceColor',[1,1,1]*0.25,... + 'EdgeColor','none') set([H(find(~IND)).sdPtch],... - 'FaceColor',[1,1,1]*0.75,... - 'EdgeColor','none') + 'FaceColor',[1,1,1]*0.75,... + 'EdgeColor','none') set([H(find(~IND)).mu],... - 'Color','b') + 'Color','b') box on @@ -79,7 +94,7 @@ H=notBoxPlot(y,x,'jitter',0.6,'style','sdline'); set(H(end).data,'Marker','^',... - 'MarkerSize',5) + 'MarkerSize',5) set([H.sd],'LineWidth',4) box on grid on @@ -97,9 +112,9 @@ H=notBoxPlot(r,[],'jitter',0.5); set([H.data],... - 'MarkerFaceColor',[1,1,1]*0.35,.... - 'markerEdgeColor',[1,1,1]*0.35,... - 'MarkerSize',3) + 'MarkerFaceColor',[1,1,1]*0.35,... + 'markerEdgeColor',[1,1,1]*0.35,... + 'MarkerSize',3) set([H.mu],'color','w') J=jet(length(H)); diff --git a/code/+NBP/simpleExamples.m b/code/+NBP/simpleExamples.m new file mode 100644 index 0000000..8b0485c --- /dev/null +++ b/code/+NBP/simpleExamples.m @@ -0,0 +1,44 @@ +function simpleExamples +% +% clf +% +% subplot(2,2,1) +% notBoxPlot(randn(20,5)); +% +% subplot(2,2,2) +% notBoxPlot(randn(20,5),[1:4,7]); +% +% subplot(2,2,3) +% h=notBoxPlot(randn(10,20)); +% d=[h.data]; +% set(d(1:4:end),'markerfacecolor',[0.4,1,0.4],'color',[0,0.4,0]) +% +% subplot(2,2,4) +% x=[1,2,3,4,5,5]; +% y=randn(20,length(x)); +% y(:,end)=y(:,end)+3; +% y(:,end-1)=y(:,end-1)-1; +% notBoxPlot(y,x); + +help(['NBP.',mfilename]) + +clf + +subplot(2,2,1) +notBoxPlot(randn(20,5)); + +subplot(2,2,2) +notBoxPlot(randn(20,5),[1:4,7]); + +subplot(2,2,3) +h=notBoxPlot(randn(10,20)); +d=[h.data]; +set(d(1:4:end),'markerfacecolor',[0.4,1,0.4],'color',[0,0.4,0]) + + +subplot(2,2,4) +x=[1,2,3,4,5,5]; +y=randn(20,length(x)); +y(:,end)=y(:,end)+3; +y(:,end-1)=y(:,end-1)-1; +notBoxPlot(y,x); diff --git a/code/+NBP/statsOptionsExample.m b/code/+NBP/statsOptionsExample.m new file mode 100644 index 0000000..26b8f96 --- /dev/null +++ b/code/+NBP/statsOptionsExample.m @@ -0,0 +1,45 @@ +function statsOptionsExamples + +% +% % Examples of different statistics options +% +% % The 95% SEM vs the 95% t-interval +% clf +% y=randn(8,3); +% subplot(2,2,1) +% notBoxPlot(y) +% title('95% SEM (n=8)') +% +% subplot(2,2,2) +% notBoxPlot(y,'interval','tInterval') +% title('95% t-interval (n=8)') +% +% % Adding medians +% subplot(2,2,:3:4) +% n=[5,10,20,40]; +% for ii=1:4 +% rng(555), notBoxPlot(rand(1,n(ii)),ii,'markMedian',true) +% end +% title('median vs mean') + + +help(['NBP.',mfilename]) + +% The 95% SEM vs the 95% t-interval +clf +y=randn(8,3); +subplot(2,2,1) +notBoxPlot(y) +title('95% SEM (n=8)') + +subplot(2,2,2) +notBoxPlot(y,'interval','tInterval') +title('95% t-interval (n=8)') + +% Adding medians +subplot(2,2,3:4) +n=[5,10,20,40]; +for ii=1:4 + rng(555), notBoxPlot(rand(1,n(ii)),ii,'markMedian',true) +end +title('median vs mean') \ No newline at end of file diff --git a/code/+NBP/tInterval_calc.m b/code/+NBP/tInterval_calc.m index e827ad6..5731e15 100644 --- a/code/+NBP/tInterval_calc.m +++ b/code/+NBP/tInterval_calc.m @@ -1,7 +1,8 @@ function tint=tInterval_Calc(vect, CI) % tInterval_Calc - confidence interval based on the t-distribution % -% function tint=tInterval_Calc(vect, CI) +% function tint = NBP.tInterval_Calc(vect, CI) +% % % Purpose % Calculate the t-interval about the mean to a given confidence @@ -10,6 +11,7 @@ % of this function has been checked against known working code % written in R. % +% % Inputs % - vect: Calculates the two-tailed 95% t confidence limits for the mean. % @@ -24,8 +26,11 @@ % plot([mean(r)-T,mean(r)+T], [mean(ylim),mean(ylim)],'r-') % hold off % +% +% % Rob Campbell - 12/03/08 % +% % Also see - SEM_calc, tinv narginchk(1,2) @@ -48,5 +53,5 @@ end -tint = ( (nanstd(vect)) ./ sqrt(sum(~isnan(vect))) ) * stdCI ; +tint = ( (nanstd(vect)) ./ sqrt(sum(~isnan(vect))) ) * stdCI ; diff --git a/code/+NBP/tableExamples.m b/code/+NBP/tableExamples.m new file mode 100644 index 0000000..725c180 --- /dev/null +++ b/code/+NBP/tableExamples.m @@ -0,0 +1,32 @@ +function tableExamples + +% % Table call format +% +% clf +% +% albert=[1,1,1,3,2,1,3,3,3,2,2,3,3]'; +% victoria=[7,8,6,1,5,7,2,1,3,4,5,2,4]'; +% M = table(victoria,albert); %place data in first column and groups in the second +% +% subplot(1,2,1) +% notBoxPlot(M) +% +% subplot(1,2,2) +% notBoxPlot(M,'jitter',0.5) + + +help(['NBP.',mfilename]) + + +clf + +albert=[1,1,1,3,2,1,3,3,3,2,2,3,3]'; +victoria=[7,8,6,1,5,7,2,1,3,4,5,2,4]'; + +M = table(victoria,albert); %place data in first column and groups in the second + +subplot(1,2,1) +notBoxPlot(M) + +subplot(1,2,2) +notBoxPlot(M,'jitter',0.75) \ No newline at end of file diff --git a/code/notBoxPlot.m b/code/notBoxPlot.m index 1e82a02..d09e6dc 100644 --- a/code/notBoxPlot.m +++ b/code/notBoxPlot.m @@ -61,102 +61,41 @@ % % % -% -% Example 1 - simple example -% clf -% subplot(2,2,1) -% notBoxPlot(randn(20,5)); -% subplot(2,2,2) -% notBoxPlot(randn(20,5),[1:4,7]); -% subplot(2,2,3:4) -% h=notBoxPlot(randn(10,40)); -% d=[h.data]; -% set(d(1:4:end),'markerfacecolor',[0.4,1,0.4],'color',[0,0.4,0]) -% -% Example 2 - overlaying with areas -% clf -% x=[1,2,3,4,5,5]; -% y=randn(20,length(x)); -% y(:,end)=y(:,end)+3; -% y(:,end-1)=y(:,end-1)-1; -% notBoxPlot(y,x); -% -% Example 3 - lines -% clf -% H=notBoxPlot(randn(20,5),[],'style','line'); -% set([H.data],'markersize',10) -% -% Example 4 - mix lines and areas [note that the way this function -% sets the x axis limits can cause problems when combining plots -% this way] -% -% clf -% h=notBoxPlot(randn(10,1)+4,5,'style','line'); -% set(h.data,'color','m') -% h=notBoxPlot(randn(50,10)); -% set(h(5).data,'color','m') -% -% Example 5 - x and y are vectors -% clf -% x=[1,1,1,3,2,1,3,3,3,2,2,3,3]; -% y=[7,8,6,1,5,7,2,1,3,4,5,2,4]; -% notBoxPlot(y,x); +% - - - - - - - - - - - - - - - - - - - - +% Examples (run clf between examples): % -% Note: an alternative to the style used in Example 5 is to call -% notBoxPlot from a loop in an external function. In this case, the -% user will have to take care of the x-ticks and axis limits. -% -% Example 6 - replacing the SD with bars -% clf -% y=randn(50,1); -% clf -% notBoxPlot(y,1,'style','sdline') -% notBoxPlot(y,2) -% xlim([0,3]) -% -% -% Example 7 - the effect of jitter (default jitter is 0.3) -% clf -% subplot(2,1,1) -% notBoxPlot(randn(20,5),[],'jitter',0.15) -% subplot(2,1,2) -% notBoxPlot(randn(20,5),[],'jitter',0.75); -% +% 1 - Basic usage: +% >> notBoxPlot([7,8,6,1,5,7,2,1,3,4,5,2,4]) +% >> notBoxPlot([7,8,6,1,5,7,2,1,3,4,5,2,4], [1,1,1,3,2,1,3,3,3,2,2,3,3]) +% >> notBoxPlot(rand(1,100)) +% >> notBoxPlot(randn(20,5)) +% >> notBoxPlot(randn(20,5),[1:4,7]); +% >> notBoxPlot(MY_TABLE) +% +% For more run: +% NBP.simpleExamples +% NBP.tableExample % -% Example 8 - The 95% SEM vs the 95% t-interval -% clf -% y=randn(8,3); -% subplot(1,2,1) -% notBoxPlot(y), title('95% SEM (n=8)') -% -% subplot(1,2,2) -% notBoxPlot(y,[],'interval','tInterval'), title('95% t-interval (n=8)') -% -% -% Example 9 - Add the median (dotted line) to plots -% clf -% n=[5,10,20,40]; -% for ii=1:4, rng(555), notBoxPlot(rand(1,n(ii)),ii,'markMedian',true), end -% +% 2 - Changing plot style +% >> notBoxPlot(randn(20,5),[],'interval','tinterval'); +% >> notBoxPlot(randn(20,5),'style','line'); %also valid: no need for x +% >> notBoxPlot(MY_TABLE,'jitter',0.5) % -% Example 10 - Table call format -% clf -% albert=[1,1,1,3,2,1,3,3,3,2,2,3,3]'; -% victoria=[7,8,6,1,5,7,2,1,3,4,5,2,4]'; -% M = table(victoria,albert); %place data in first column and groups in the second -% notBoxPlot(M) +% For more run: +% NBP.lineExamples +% NBP.jitterExamples +% NBP.showCase % -% Example 11 - Table call format with optional arguments -% clf -% albert=[1,1,1,3,2,1,3,3,3,2,2,3,3]'; -% victoria=[7,8,6,1,5,7,2,1,3,4,5,2,4]'; -% M = table(victoria,albert); %place data in first column and groups in the second -% notBoxPlot(M,'jitter',0.75) +% 3 - Showing different statistics +% >> notBoxPlot(randn(8,3),'interval','tInterval') +% >> notBoxPlot(randn(8,3),'markMedian',true) % +% For more run: +% NBP.statsOptionsExamples % % Rob Campbell - August 2016 % -% Also see: NBP.example, boxplot +% Also see: boxplot From ea0ad26c5926b37d59f99de5d7d79b6aa3326acc Mon Sep 17 00:00:00 2001 From: Rob Campbell Date: Sat, 11 Mar 2017 13:38:51 +0100 Subject: [PATCH 6/9] Accepts a LinearModel and uses the confidence intervals derived from it. --- code/+NBP/linearModelExamples.m | 105 ++++++++++++++++++++++++++++++ code/notBoxPlot.m | 112 +++++++++++++++++++++++++------- 2 files changed, 193 insertions(+), 24 deletions(-) create mode 100644 code/+NBP/linearModelExamples.m diff --git a/code/+NBP/linearModelExamples.m b/code/+NBP/linearModelExamples.m new file mode 100644 index 0000000..7846291 --- /dev/null +++ b/code/+NBP/linearModelExamples.m @@ -0,0 +1,105 @@ +function linearModelExamples + +% % Linear model call format +% +% +% % Build data +% rng(555), +% n=10; +% R=rand(n,5); +% R(:,3)=R(:,3)+1; +% +% X=repmat(1:5,size(R,1),1); +% lemmings=R(:); +% group=X(:); +% +% clf +% +% % We can call notBoxPlot with just X and Y +% subplot(2,2,1) +% notBoxPlot(lemmings,group,'jitter',0.75) +% grid on, box on +% ylim([-0.5,2.2]) +% title('two vectors') +% +% % We can create a table and get the same plot plus the variable names on the axes +% subplot(2,2,2) +% T = table(lemmings,group); +% notBoxPlot(T,'jitter',0.75) +% grid on, box on +% ylim([-0.5,2.2]) +% title('table') +% +% % We can fit a linear model do the data and plot this +% subplot(2,2,3) +% group = categorical(group); +% T = table(lemmings,group); +% M = fitlm(T,'lemmings ~ group'); +% notBoxPlot(M,'jitter',0.75) +% grid on, box on +% ylim([-0.5,2.2]) +% title('model') +% +% % Increase variance of one group +% subplot(2,2,4) +% lemmings(end-n+1:end) = lemmings(end-n+1:end)*1.75; +% T = table(lemmings,group); +% M = fitlm(T,'lemmings ~ group'); +% notBoxPlot(M,'jitter',0.75) +% grid on, box on +% ylim([-0.5,2.2]) +% titl + +help(['NBP.',mfilename]) + + + + +% Build data +rng(555), +n=10; +R=rand(n,5); +R(:,3)=R(:,3)+1; + +X=repmat(1:5,size(R,1),1); +lemmings=R(:); +group=X(:); + + +clf + +% We can call notBoxPlot with just X and Y +subplot(2,2,1) +notBoxPlot(lemmings,group,'jitter',0.75) +grid on, box on +ylim([-0.5,2.2]) +title('two vectors') + +% We can create a table and get the same plot plus the variable names on the axes +subplot(2,2,2) +T = table(lemmings,group); +notBoxPlot(T,'jitter',0.75) +grid on, box on +ylim([-0.5,2.2]) +title('table') + +% We can fit a linear model do the data and plot this +subplot(2,2,3) +group = categorical(group); +T = table(lemmings,group); +M = fitlm(T,'lemmings ~ group'); +notBoxPlot(M,'jitter',0.75) +grid on, box on +ylim([-0.5,2.2]) +title('model') + + +% Increase variance of one group +subplot(2,2,4) +lemmings(end-n+1:end) = lemmings(end-n+1:end)*1.75; +T = table(lemmings,group); +M = fitlm(T,'lemmings ~ group'); +notBoxPlot(M,'jitter',0.75) +grid on, box on +ylim([-0.5,2.2]) +title('increased variance in group 5') diff --git a/code/notBoxPlot.m b/code/notBoxPlot.m index d09e6dc..32586fa 100644 --- a/code/notBoxPlot.m +++ b/code/notBoxPlot.m @@ -21,7 +21,8 @@ % * vector and no x is provided: all data are grouped at one x position. % * matrix and no x is provided: each column is plotted in a different x position. % * vector with x grouping variable provided: data grouped accordig to x -% * a table is treated such that the first column is y and the second x. +% * a Table is treated such that the first column is y and the second x. +% * a LinearModel produced by fitlm % % x - [optional], the x axis points at which y columns should be % plotted. This allows more than one set of y values to appear @@ -49,6 +50,8 @@ % % 'interval' - 'SEM' [default] Plots a 95% confidence interval for the mean % - 'tInterval' Plots a 95% t-interval for the mean +% - If a LinearModel from fitlm is provided, interval is always +% the tInterval and the confidence interval comes from the model. % % 'markMedian' - false [default] if true the median value is highlighted % The median is highlighted as a dotted line or an open square @@ -106,14 +109,24 @@ return end +% Check if Y is of a suitable class +if ~isnumeric(y) && ~istable(y) && ~isa(y,'LinearModel') + fprintf('Variable y is a %s. This is not an allowed input type. see help %s\n',... + class(y), mfilename) + return +end + +% Parse the different call types +modelCIs=[]; +tableOrModelCall=false; -%Handle table call -if istable(y) +switch lower(class(y)) - tableCall=true; +case 'table' + tableOrModelCall=true; if nargin>1 %so user doesn't need to specify a blank variable for x if ~isempty(x) - varargin = [x,varargin]; + varargin=[x,varargin]; end end thisTable=y; @@ -125,8 +138,31 @@ y = thisTable.(varNames{1}); x = thisTable.(varNames{2}); -else - tableCall=false; +case 'linearmodel' + tableOrModelCall=true; + if nargin>1 %so user doesn't need to specify a blank variable for x + if ~isempty(x) + varargin=[x,varargin]; + end + end + + thisModel=y; + + if length(thisModel.PredictorNames) >1 + fprintf('% s can only handle linear models with one predictor\n',mfilename) + return + end + y = thisModel.Variables.(thisModel.ResponseName); + x = thisModel.Variables.(thisModel.PredictorNames{1}); + varNames = {thisModel.ResponseName,thisModel.PredictorNames{1}}; %for the axis labels + + % Set the SD bar to have 1.96 standard deviations + varargin = [varargin,'numSDs',1.96]; + + % Get the the confidence intervals from the model + modelCIs = coefCI(thisModel,0.05); + +otherwise %Otherwise Y is a vector or a matrix if isvector(y) y=y(:); @@ -135,7 +171,7 @@ % Handle case where user doesn't supply X, but have user param/val pairs. e.g. % notBoxPlot(rand(20,5),'jitter',0.5) if nargin>2 && ischar(x) - varargin = [x,varargin]; + varargin=[x,varargin]; x=[]; end @@ -144,7 +180,9 @@ if nargin<2 || isempty(x) x=1:size(y,2); end -end + +end %switch class(y) + %If x is logical then the function fails. So let's make sure it's a double x=double(x); @@ -155,11 +193,17 @@ % Parse input arguments params = inputParser; params.CaseSensitive = false; -params.addParameter('jitter', 0.3, @(x) isnumeric(x) & isscalar(x)); + +%User-visible options +params.addParameter('jitter', 0.3, @(x) isnumeric(x) && isscalar(x)); params.addParameter('style','patch', @(x) ischar(x) && any(strncmp(x,{'patch','line','sdline'},inf)) ); params.addParameter('interval','SEM', @(x) ischar(x) && any(strncmp(x,{'SEM','tInterval'},inf)) ); params.addParameter('markMedian', false, @(x) islogical(x)); +%Options hidden from the user +params.addParameter('numSDs',1, @(x) isnumeric(x) && isscalar(x) && x>=0) +params.addParameter('manualCI',[], @(x) (isnumeric(x) && isscalar(x)) || isempty(x) ) + params.parse(varargin{:}); %Extract values from the inputParser @@ -168,6 +212,10 @@ interval = params.Results.interval; markMedian = params.Results.markMedian; +%The multiplier for the SD patch. e.g. for 1.96 SDs this value should be 1.96 +numSDs = params.Results.numSDs; +manualCI = params.Results.manualCI; %Is used by the recursive call to over-ride the CI when y is a LinearModel + %Set interval function switch interval case 'SEM' @@ -178,14 +226,15 @@ error('Interval %s is unknown',interval) end - - - if jitter==0 && strcmp(style,'patch') warning('A zero value for jitter means no patch object visible') end + +% - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +% We now loop through the unique x values, plotting each notBox in turn +% using recursive calls to notBoxPlot. if isvector(y) && isvector(x) && length(x)>1 x=x(:); @@ -195,8 +244,16 @@ u=unique(x); for ii=1:length(u) - f = x==u(ii); - h(ii)=notBoxPlot(y(f),u(ii),varargin{:}); %recursive call + f = find(x==u(ii)); + + %If a model was used, we use the 95% t-intervals it produces + if ~isempty(modelCIs) + thisCI = range(modelCIs(ii,:))/2; %the interval is symmetric and we need just this. + else + thisCI =[]; + end + + h(ii)=notBoxPlot(y(f),u(ii),varargin{:},'manualCI',thisCI); %recursive call end @@ -211,16 +268,17 @@ end %If we had a table we can label the axes - if tableCall + if tableOrModelCall ylabel(varNames{1}) xlabel(varNames{2}) end - return - + return % User's call to notBoxPlot never goes beyond here end +% - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - if length(x) ~= size(y,2) error('length of x doesn''t match the number of columns in y') end @@ -271,10 +329,16 @@ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% function [h,statsOut]=myPlotter(X,Y) + %This is a nested function that shares the caller's namespace - SEM=intervalFun(Y); %Supplied external function - SD=std(Y,'omitnan'); %Requires the stats toolbox - mu=mean(Y,'omitnan'); %Requires the stats toolbox + if isempty(manualCI) + SEM=intervalFun(Y); %A function handle to a supplied external function + else + SEM=manualCI; + end + + SD=std(Y,'omitnan')*numSDs; + mu=mean(Y,'omitnan'); if markMedian med = median(Y,'omitnan'); end @@ -314,11 +378,11 @@ 'linewidth',2); end end - + %Overlay the jittered raw data C=cols(k,:); J=(rand(size(thisX))-0.5)*jitter; - + h(k).data=plot(thisX+J, thisY, 'o', 'color', C,... 'markerfacecolor', C+(1-C)*0.65); end %for k=1:length(X) From 0876812413c3b5fb7bf88d9db0d216d559b6f5c5 Mon Sep 17 00:00:00 2001 From: Rob Campbell Date: Sat, 11 Mar 2017 15:51:36 +0100 Subject: [PATCH 7/9] Update readme and unit tests --- README.md | 20 ++++ code/+NBP/linearModelExamples.m | 2 +- tests/core_tests.m | 162 +++++++++----------------------- tests/readme.txt | 10 ++ tests/run_tests.m | 21 ----- 5 files changed, 78 insertions(+), 137 deletions(-) create mode 100644 tests/readme.txt delete mode 100644 tests/run_tests.m diff --git a/README.md b/README.md index 84e7809..e48dbf5 100644 --- a/README.md +++ b/README.md @@ -34,3 +34,23 @@ A: Most modifications can be done using the function handles. See the function h ## Installation Add the ``code`` directory to your MATLAB path. Does not rely on any toolboxes. + + +## Changelog + +**v1.2 (28-08-16)** + +* Add median to plots +* Select SEM or t-interval from command line +* Return stats as second output +* Move to parameter/value pairs by default and warn user if they aren't doing this. +* Add unit tests. + +**v1.3 (11-09-16)** + +* Remove legacy calls +* Allow passing of a table, which automatically labels the axes +* Pass a LinearModel, which automatically labels the axes and uses the model errors +* Examples are now in separate files and doc text is neater +* User can now optionally do `notBoxPlot(y,'jitter',0.5)` instead of `notBoxPlot(y,[],'jitter',0.5)` + diff --git a/code/+NBP/linearModelExamples.m b/code/+NBP/linearModelExamples.m index 7846291..96268d7 100644 --- a/code/+NBP/linearModelExamples.m +++ b/code/+NBP/linearModelExamples.m @@ -48,7 +48,7 @@ % notBoxPlot(M,'jitter',0.75) % grid on, box on % ylim([-0.5,2.2]) -% titl +% title('increased variance in group 5') help(['NBP.',mfilename]) diff --git a/tests/core_tests.m b/tests/core_tests.m index a73052c..ec5b81f 100644 --- a/tests/core_tests.m +++ b/tests/core_tests.m @@ -1,138 +1,70 @@ classdef core_tests < matlab.unittest.TestCase - % Unit tests for notBoxPlot + % Unit tests for notBoxPlot - properties + properties - end %properties + end %properties - methods (Test) + methods (Test) - function checkForNonLegacyCall(testCase) - % Legacy function calls take the form: notBoxPlot(y,x,jitter,style) - % From release v1.2 the form is notBoxPlot(y,x,'param','val',...) - % Here we test that NBP is correctly able to find non-legacy usage - y=rand(10,2); - x=1:2; - clf - [~,~,legacyCall] = notBoxPlot(y); - testCase.verifyTrue(legacyCall == false); + function checkInputParameters(testCase) + %Check that no gross errors are produced for all param/val pairs not already tested above + y=rand(10,3); + x=[1,2,2]; %To place to boxes on the same x location and one on its own - clf - [~,~,legacyCall] = notBoxPlot(y,x); - testCase.verifyTrue(legacyCall == false); + clf + H=notBoxPlot(y,x,'jitter',0.6,'style','sdline'); - clf - [~,~,legacyCall] = notBoxPlot(y,x,'jitter',0.3); - testCase.verifyTrue(legacyCall == false); + clf + notBoxPlot(y,[],'style','patch','interval','SEM'); - clf - [~,~,legacyCall] = notBoxPlot(y,x,'style','patch'); - testCase.verifyTrue(legacyCall == false); + clf + notBoxPlot(y,x,'interval','tInterval'); - clf - [~,~,legacyCall] = notBoxPlot(y,x,'style','line','jitter',0.2); - testCase.verifyTrue(legacyCall == false); + clf + h=notBoxPlot(y,x,'interval','tInterval','markMedian',false); + testCase.verifyFalse(isfield(h,'med')) - close(gcf) - end + clf + h=notBoxPlot(y,x,'interval','tInterval','markMedian',true); + testCase.verifyTrue(isfield(h,'med')) - function checkForLegacyCall(testCase) - % Legacy function calls take the form: notBoxPlot(y,x,jitter,style) - % From release v1.2 the form is notBoxPlot(y,x,'param','val',...) - % Here we test that NBP is correctly able to find legacy usage - y=rand(10,2); - x=1:2; + clf + h=notBoxPlot(y,x,'style','line','markMedian',false); + testCase.verifyFalse(isfield(h,'med')) - clf - [~,~,legacyCall] = notBoxPlot(y,[],0.3); - testCase.verifyTrue(legacyCall == true); + clf + h=notBoxPlot(y,x,'style','line','markMedian',true); + testCase.verifyTrue(isfield(h,'med')) - clf - [~,~,legacyCall] = notBoxPlot(y,[],[],'line'); - testCase.verifyTrue(legacyCall == true); + %Check that the showCase example runs + NBP.showCase + + close(gcf) + end - clf - [~,~,legacyCall] = notBoxPlot(y,x,0.2,'line'); - testCase.verifyTrue(legacyCall == true); - close(gcf) - end + function checkOutputs(testCase) + %Check output args + y=rand(10,3); + x=[1,2,2]; %To place to boxes on the same x location and one on its own - function checkForMixedCalls(testCase) - % Legacy function calls take the form: notBoxPlot(y,x,jitter,style) - % From release v1.2 the form is notBoxPlot(y,x,'param','val',...) - % Here we test that NBP is correctly able to catch erroneous mixed usage - y=rand(10,2); - x=1:2; + clf + [H,stats]=notBoxPlot(y,x,'jitter',0.6,'style','sdline'); + testCase.verifyTrue(isstruct(H)) + testCase.verifyTrue(isstruct(stats)) + testCase.verifyTrue(length(H)==length(stats)) - clf - testCase.verifyError(@() notBoxPlot(y,x,0.3,'style','line'),'notBoxPlot:legacyError') + clf + [H,stats]=notBoxPlot(y,x,'jitter',0.6,'markMedian',true); - clf - testCase.verifyError(@() notBoxPlot(y,x,'style','line',0.3),'notBoxPlot:legacyError') + testCase.verifyTrue(isfield(stats,'median')) - close(gcf) - end - - - function checkInputParameters(testCase) - %Check that no gross errors are produced for all param/val pairs not already tested above - y=rand(10,3); - x=[1,2,2]; %To place to boxes on the same x location and one on its own - - clf - H=notBoxPlot(y,x,'jitter',0.6,'style','sdline'); - - clf - notBoxPlot(y,[],'style','patch','interval','SEM'); - - clf - notBoxPlot(y,x,'interval','tInterval'); - - clf - h=notBoxPlot(y,x,'interval','tInterval','markMedian',false); - testCase.verifyFalse(isfield(h,'med')) - - clf - h=notBoxPlot(y,x,'interval','tInterval','markMedian',true); - testCase.verifyTrue(isfield(h,'med')) - - clf - h=notBoxPlot(y,x,'style','line','markMedian',false); - testCase.verifyFalse(isfield(h,'med')) - - clf - h=notBoxPlot(y,x,'style','line','markMedian',true); - testCase.verifyTrue(isfield(h,'med')) - - %Check that the example runs - NBP.example - - close(gcf) - end - - - function checkOutputs(testCase) - %Check output args - y=rand(10,3); - x=[1,2,2]; %To place to boxes on the same x location and one on its own - - clf - [H,stats]=notBoxPlot(y,x,'jitter',0.6,'style','sdline'); - testCase.verifyTrue(isstruct(H)) - testCase.verifyTrue(isstruct(stats)) - testCase.verifyTrue(length(H)==length(stats)) - - clf - [H,stats]=notBoxPlot(y,x,'jitter',0.6,'markMedian',true); - - testCase.verifyTrue(isfield(stats,'median')) - - - close(gcf) - end - end %methods (Test) + + close(gcf) + end + end %methods (Test) end %classdef core_tests < matlab.unittest.TestCase \ No newline at end of file diff --git a/tests/readme.txt b/tests/readme.txt new file mode 100644 index 0000000..9809e7c --- /dev/null +++ b/tests/readme.txt @@ -0,0 +1,10 @@ +This directory contains code for unit testing settings_handler. +You do not need to add this directory to your path. + +To run the unit tests: + +>> runtests + +or + +>> table(runtests) \ No newline at end of file diff --git a/tests/run_tests.m b/tests/run_tests.m deleted file mode 100644 index 56f0c25..0000000 --- a/tests/run_tests.m +++ /dev/null @@ -1,21 +0,0 @@ -function varargout = run_tests -% run all unit tests for settings handler -% -% function results = run_tests -% -% No input arguments. Results printed to screen and optionally -% returned to the workspace. - - -testCase = core_tests; -results = run(testCase); -out{1}=results; - - -fprintf('\n------------------------ RESULTS ------------------------\n') -for ii=1:length(out) - disp(out{ii}); -end -if nargout>0 - varargout{1}=out; -end From d10661fdfe9239493bfbcc2b15fdac6a2a340c47 Mon Sep 17 00:00:00 2001 From: Rob Campbell Date: Tue, 14 Mar 2017 08:56:05 +0100 Subject: [PATCH 8/9] Do not attempt to plot model if the predictor is numeric. Coerce cell arrays to categorical. --- code/notBoxPlot.m | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/code/notBoxPlot.m b/code/notBoxPlot.m index 32586fa..96f067f 100644 --- a/code/notBoxPlot.m +++ b/code/notBoxPlot.m @@ -154,6 +154,17 @@ end y = thisModel.Variables.(thisModel.ResponseName); x = thisModel.Variables.(thisModel.PredictorNames{1}); + + %Check that x is of a suitable type + if isnumeric(x) + fprintf('The model predictor variable should not be continuous\n') + return + end + if iscell(x) + fprintf('Coercing predictor variable from a cell array to a categorical variable\n') + x=categorical(x); + end + varNames = {thisModel.ResponseName,thisModel.PredictorNames{1}}; %for the axis labels % Set the SD bar to have 1.96 standard deviations From 35b01d65bfadacdc1d001403eb4ea7630ec793ca Mon Sep 17 00:00:00 2001 From: Rob Campbell Date: Tue, 14 Mar 2017 09:01:17 +0100 Subject: [PATCH 9/9] update readme to reflect new feature --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e48dbf5..b8ff212 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ The function has several examples and there are various visualization possibilit Although it's worked well for situations I've needed it, I will be happy to modify the function if users come up against problems. ## Features +- Directly plot LinearModel objects from `fitlm` [NEW] - Easily mix a variety of plot styles on one figure - Easy post-hoc modification of plot parameters via returned function handles - Statistics (mean, SD, etc) optionally returned