https://github.com/JRihel/Sleep-Analysis
Tip revision: 44fe2250c5b18c52c73d9df1bb6b96acb3b39421 authored by JRihel on 15 February 2023, 16:53:01 UTC
Delete Perl Batch Folder2_Folder Structure.zip
Delete Perl Batch Folder2_Folder Structure.zip
Tip revision: 44fe225
plotSpread.m
function handles = plotSpread(varargin)
%PLOTSPREAD plots distributions of points by spreading them around the y-axis
%
% SYNOPSIS: handles = plotSpread(data,binWidth,spreadFcn,xNames,showMM,xValues)
% handles = plotSpread(ah,...)
%
% INPUT data: cell array of distributions or nDatapoints-by-mDistributions
% array, or array with data that is indexed by either
% distributionIdx or categoryIdx, or both.
% distributionIdx: grouping variable that determines to which
% distribution a data point belongs. Grouping is
% resolved by calling grp2idx, and unless xNames have
% been supplied, group names determine the x-labels.
% If the grouping variable is numeric, group labels also
% determine x-values, unless the parameter xValues has
% been specified.
% distributionColors : color identifier (string, cell array of
% strings), or colormap, with a single color, or one color per
% distribution (or per entry in distributionIdx). Colors the
% distributions. Default: 'b'
% distributionMarkers : string, or cell array of strings, with either
% a single marker or one marker per distribution (or per entry in
% distributionIdx). See linespec for admissible markers.
% Default: '.'
% categoryIdx: grouping variable that determines group membership for data
% points across distributions. Grouping is resolved by calling
% grp2idx.
% categoryColors : color identifier (cell array of
% strings), or colormap, with one color per category.
% Colors the categories, and will override distributionColors.
% Default is generated using distinguishable_colors by Timothy E.
% Holy.
% categoryMarkers : cell array of strings, with one marker per
% category. See linespec for admissible markers. Will override
% distributionMarkers. Default: ''
% binWidth : width of bins (along y) that control which data
% points are considered close enough to be spread. Default: 0.1
% spreadFcn : cell array of length 2 with {name,param}
% if name is 'lin', the spread goes linear with the number of
% points inside the bin, until it reaches the maximum of 0.9 at
% n==param.
% if name is 'xp', the spread increases as 1-exp(log(0.9)*x).
% param is empty
% Default {'xp',[]}
% spreadWidth : width, along the x-axis (y-axis if flipped) that can
% at most be covered by the points. Default:
% median(diff(sort(xValues))); 1 if no xValues have been supplied
% showMM : if 1, mean and median are shown as red crosses and
% green squares, respectively. Default: 0
% 2: only mean
% 3: only median
% 4: mean +/- standard error of the mean (no median)
% 5: mean +/- standard deviation (no median)
% xNames : cell array of length nDistributions containing x-tick names
% (instead of the default '1,2,3')
% xValues : list of x-values at which the data should
% be plotted. Default: 1,2,3...
% xMode : if 'auto', x-ticks are spaced automatically. If 'manual',
% there is a tick for each distribution. If xNames is
% provided as input, xMode is forced to 'manual'. Default:
% 'manual'.
% xyOri : orientation of axes. Either 'normal' (=default), or
% 'flipped'. If 'flipped', the x-and y-axes are switched, so
% that violin plots are horizontal. Consequently,
% axes-specific properties, such as 'yLabel' are applied to
% the other axis.
% yLabel : string with label for y-axis. Default : ''
% ah : handles of axes into which to plot
%
% OUTPUT handles: 3-by-1 cell array with handles to distributions,
% mean/median etc, and the axes, respectively
%
% REMARKS: plotSpread is useful for distributions with a small number of
% data points. For larger amounts of data, distributionPlot is
% more suited.
%
% EXAMPLES: data = {randn(25,1),randn(100,1),randn(300,1)};
% figure,plotSpread(data,[],[],{'25 pts','100 pts','300 pts'})
%
% data = [randn(50,1);randn(50,1)+3.5]*[1 1];
% catIdx = [ones(50,1);zeros(50,1);randi([0,1],[100,1])];
% figure
% plotSpread(data,'categoryIdx',catIdx,...
% 'categoryMarkers',{'o','+'},'categoryColors',{'r','b'})
%
% END
%
% created with MATLAB ver.: 7.9.0.3470 (R2009b) on Mac OS X Version: 10.5.7 Build: 9J61
%
% created by: jonas
% DATE: 11-Jul-2009
%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
def.binWidth = 0.1;
def.spreadFcn = {'xp',[]};
def.xNames = [];
def.showMM = false;
def.xValues = [];
def.distributionIdx = [];
def.distributionColors = 'b';
def.distributionMarkers = '.';
def.xMode = 'manual';
def.xyOri = 'normal';
def.categoryIdx = [];
def.categoryColors = [];
def.categoryMarkers = '';
def.yLabel = '';
def.spreadWidth = [];
%% CHECK INPUT
% check for axes handle
if ~iscell(varargin{1}) && length(varargin{1}) == 1 && ...
ishandle(varargin{1}) && strcmp(get(varargin{1},'Type'),'axes')
ah = varargin{1};
data = varargin{2};
varargin(1:2) = [];
newAx = false;
else
ah = gca;
data = varargin{1};
varargin(1) = [];
% if the axes have children, it's not new (important for adjusting
% limits below)
newAx = isempty(get(ah,'Children'));
end
% optional arguments
parserObj = inputParser;
parserObj.FunctionName = 'plotSpread';
distributionIdx = [];distributionLabels = '';
if ~isempty(varargin) && ~ischar(varargin{1}) && ~isstruct(varargin{1})
% old syntax
parserObj.addOptional('binWidth',def.binWidth);
parserObj.addOptional('spreadFcn',def.spreadFcn);
parserObj.addOptional('xNames',def.xNames);
parserObj.addOptional('showMM',def.showMM);
parserObj.addOptional('xValues',def.xValues);
parserObj.parse(varargin{:});
opt = parserObj.Results;
opt.distributionIdx = [];
opt.distributionColors = def.distributionColors;
opt.distributionMarkers = def.distributionMarkers;
opt.xMode = def.xMode;
opt.xyOri = def.xyOri;
opt.categoryIdx = [];
opt.categoryColors = def.distributionColors;
opt.categoryMarkers = def.distributionMarkers;
opt.yLabel = '';
opt.spreadWidth = def.spreadWidth;
for fn = fieldnames(def)'
if ~isfield(opt,fn{1})
% Manually adding the new defaults means a lot fewer bugs
error('please add option %s to old syntax',fn{1});
end
if isempty(opt.(fn{1}))
opt.(fn{1}) = def.(fn{1});
end
end
else
% new syntax
defNames = fieldnames(def);
for dn = defNames(:)'
parserObj.addParamValue(dn{1},def.(dn{1}));
end
parserObj.parse(varargin{:});
opt = parserObj.Results;
end
% We want data to be a vector, so that indexing with both groupIdx and
% distributionIdx becomes straightforward, and so that we can conveniently
% eliminate NaNs that otherwise could mess up grouping.
% Consequently, if data is a cell array, we convert it, and build a
% corresponding distributionIdx (allowing a user-supplied distributionIdx
% to override, though), and then we go and take care of groupIdx. Once all
% three indices have been built, NaN can be removed.
if iscell(data)
% make sure data is all n-by-1
data = cellfun(@(x)x(:),data,'UniformOutput',false);
nData = length(data);
nn = cellfun(@numel,data);
% make vector
data = cat(1,data{:});
distributionIdx = repeatEntries((1:nData)',nn);
else
% distributions in columns
nData = size(data,2);
distributionIdx = repeatEntries((1:nData)',size(data,1));
data = data(:);
end
% distribution groups
if ~isempty(opt.distributionIdx)
[distributionIdx,distributionLabels,vals] = grp2idx(opt.distributionIdx);
% convert data to cell array
nData = length(distributionLabels);
% if not otherwise provided, use group labels for xnames
if isempty(opt.xNames)
opt.xNames = distributionLabels;
if ~iscell(opt.xNames)
opt.xNames = num2cell(opt.xNames);
end
end
if isnumeric(vals) && isempty(opt.xValues)
opt.xValues = vals;
end
end
if ~isempty(opt.xNames)
opt.xMode = 'manual';
end
% distribution colors&markers
if ischar(opt.distributionColors)
opt.distributionColors = {opt.distributionColors};
end
if iscell(opt.distributionColors)
if length(opt.distributionColors) == 1
% expand
opt.distributionColors = repmat(opt.distributionColors,nData,1);
elseif length(opt.distributionColors) ~= nData
error('please submit one color per distribution (%i dist, %i colors)',nData,length(opt.distributionColors));
end
else
if size(opt.distributionColors,2) ~= 3
error('please specify colormap with three columns')
end
if size(opt.distributionColors,1) == 1
opt.distributionColors = repmat(opt.distributionColors,nData,1);
elseif size(opt.distributionColors,1) ~= nData
error('please submit one color per distribution (%i dist, %i colors)',nData,size(opt.distributionColors,1));
end
% create a cell array
opt.distributionColors = mat2cell(opt.distributionColors,ones(nData,1),3);
end
if ischar(opt.distributionMarkers)
opt.distributionMarkers = {opt.distributionMarkers};
end
if length(opt.distributionMarkers) == 1
% expand
opt.distributionMarkers = repmat(opt.distributionMarkers,nData,1);
elseif length(opt.distributionMarkers) ~= nData
error('please submit one color per distribution (%i dist, %i colors)',nData,length(opt.distributionMarkers));
end
stdWidth = 1;
if isempty(opt.xValues)
opt.xValues = 1:nData;
end
if isempty(opt.spreadWidth)
% scale width
tmp = median(diff(sort(opt.xValues)));
if ~isnan(tmp)
stdWidth = tmp;
end
else
stdWidth = opt.spreadWidth;
end
if ~ischar(opt.xyOri) || ~any(ismember(opt.xyOri,{'normal','flipped'}))
error('option xyOri must be either ''normal'' or ''flipped'' (is ''%s'')',opt.xyOri);
end
% check for categoryIdx/colors/markers
% If there are categories, check colors/markers individually first,
% then check whether any of them at all have been supplied, and
% if not, override distributionColors with default categoryColors
if isempty(opt.categoryIdx)
categoryIdx = ones(size(distributionIdx));
nCategories = 1;
categoryLabels = '';
else
[categoryIdx,categoryLabels] = grp2idx(opt.categoryIdx(:));
nCategories = max(categoryIdx);
end
% plotColors, plotMarkers, plotLabels: nDist-by-nCat arrays
plotColors = repmat(opt.distributionColors(:),1,nCategories);
plotMarkers= repmat(opt.distributionMarkers(:),1,nCategories);
if isempty(distributionLabels)
distributionLabels = opt.xNames;
if isempty(distributionLabels)
distributionLabels = cellstr(num2str(opt.xValues(:)));
end
end
if nCategories == 1
plotLabels = distributionLabels(:);
else
plotLabels = cell(nData,nCategories);
for iData = 1:nData
for iCategory = 1:nCategories
plotLabels{iData,iCategory} = ...
sprintf('%s-%s',num2str(distributionLabels{iData}),...
num2str(categoryLabels{iCategory}));
end
end
end
categoryIsLabeled = false;
if nCategories > 1
% if not using defaults for categoryColors: apply them
if ~any(strcmp('categoryColors',parserObj.UsingDefaults))
if iscell(opt.categoryColors)
if length(opt.categoryColors) ~= nCategories
error('please supply one category color per category')
end
plotColors = repmat(opt.categoryColors(:)',nData,1);
categoryIsLabeled = true;
else
if all(size(opt.categoryColors) ~= [nCategories,3])
error('please supply a #-of-categories-by-3 color array')
end
plotColors = repmat( mat2cell(opt.categoryColors,ones(nCategories,1),3)', nData,1);
categoryIsLabeled = true;
end
end
if ~any(strcmp('categoryMarkers',parserObj.UsingDefaults))
if length(opt.categoryMarkers) ~= nCategories
error('please supply one category marker per category')
end
if ~iscell(opt.categoryMarkers)
error('please supply a list of markers as cell array')
end
plotMarkers = repmat(opt.categoryMarkers(:)',nData,1);
categoryIsLabeled = true;
end
if ~categoryIsLabeled
% use distinguishable_colors to mark categories
plotColors = repmat( mat2cell(...
distinguishable_colors(nCategories),...
ones(nCategories,1),3)', nData,1);
end
end
% remove NaNs from data
badData = ~isfinite(data) | ~isfinite(distributionIdx) | ~isfinite(categoryIdx);
data(badData) = [];
distributionIdx(badData) = [];
categoryIdx(badData) = [];
%% TRANSFORM DATA
% Here, I try to estimate what the aspect ratio of the data is going to be
fh = figure('Visible','off');
if ~isempty(data)
minMax = [min(data);max(data)];
else
minMax = [0 1];
end
switch opt.xyOri
case 'normal'
plot([0.5;nData+0.5],minMax,'o');
case 'flipped'
plot(minMax,[0.5;nData+0.5],'o');
end
aspectRatio = get(gca,'DataAspectRatio');
close(fh);
tFact = aspectRatio(2)/aspectRatio(1);
if strcmp(opt.xyOri,'flipped')
tFact = 1/tFact;
end
%% SPREAD POINTS
% assign either nData, or xValues number of values, in case we're working
% with group-indices
[m,md,sem,sd] = deal(nan(max(nData,length(opt.xValues)),1));
% augment data to make n-by-2
data(:,2) = 0;
for iData = 1:nData
currentDataIdx = distributionIdx==iData;
currentData = data(currentDataIdx,1);
if ~isempty(currentData)
% transform and sort
currentData = currentData / tFact;
%currentData = sort(currentData);
% add x
currentData = [ones(size(currentData))*opt.xValues(iData),currentData]; %#ok<AGROW>
% step through the data in 0.1 increments. If there are multiple
% entries, spread along x
for y = min(currentData(:,2)):opt.binWidth:max(currentData(:,2))
% find values
valIdx = find(currentData(:,2) >= y & currentData(:,2) < y+opt.binWidth);
nVal = length(valIdx);
if nVal > 1
% spread
switch opt.spreadFcn{1}
case 'xp'
spreadWidth = stdWidth*0.9*(1-exp(log(0.9)*(nVal-1)));
case 'lin'
spreadWidth = stdWidth*0.9*min(nVal-1,opt.spreadFcn{2})/opt.spreadFcn{2};
end
spreadDist = spreadWidth / (nVal - 1);
if isEven(nVal)
offset = spreadDist / 2;
else
offset = eps;
end
for v = 1:nVal
currentData(valIdx(v),1) = opt.xValues(iData) + offset;
% update offset
offset = offset - sign(offset) * spreadDist * v;
end
end
end
% update data
currentData(:,2) = data(currentDataIdx,1);
data(currentDataIdx,:) = currentData;
if opt.showMM > 0
m(iData) = nanmean(currentData(:,2));
md(iData) = nanmedian(currentData(:,2));
sd(iData) = nanstd(currentData(:,2));
sem(iData) = sd(iData)/sqrt(sum(isfinite(currentData(:,2))));
end
end % test isempty
end
%% plot
set(ah,'NextPlot','add')
ph = NaN(nData,nCategories);
for iData = 1:nData
for iCategory = 1:nCategories
currentIdx = distributionIdx == iData & categoryIdx == iCategory;
if any(currentIdx)
switch opt.xyOri
case 'normal'
ph(iData,iCategory) = plot(ah,data(currentIdx,1),...
data(currentIdx,2),...
'marker',plotMarkers{iData,iCategory},...
'color',plotColors{iData,iCategory},...
'lineStyle','none',...
'DisplayName',plotLabels{iData,iCategory});
case 'flipped'
ph(iData,iCategory) = plot(ah,data(currentIdx,2),...
data(currentIdx,1),...
'marker',plotMarkers{iData,iCategory},...
'color',plotColors{iData,iCategory},...
'lineStyle','none',...
'DisplayName',plotLabels{iData,iCategory});
end
end
end
end
% if ~empty, use xNames
switch opt.xyOri
case 'normal'
switch opt.xMode
case 'manual'
set(ah,'XTick',opt.xValues);
if ~isempty(opt.xNames)
set(ah,'XTickLabel',opt.xNames)
end
case 'auto'
% no need to do anything
end
% have plot start/end properly
minX = min(opt.xValues)-stdWidth;
maxX = max(opt.xValues)+stdWidth;
if ~newAx
oldLim = xlim;
minX = min(minX,oldLim(1));
maxX = max(maxX,oldLim(2));
end
xlim([minX,maxX])
ylabel(ah,opt.yLabel)
case 'flipped'
switch opt.xMode
case 'manual'
set(ah,'YTick',opt.xValues);
if ~isempty(opt.xNames)
set(ah,'YTickLabel',opt.xNames)
end
case 'auto'
% no need to do anything
end
% have plot start/end properly (for ease of copying, only switch
% xlim to ylim
minX = min(opt.xValues)-stdWidth;
maxX = max(opt.xValues)+stdWidth;
if ~newAx
oldLim = ylim;
minX = min(minX,oldLim(1));
maxX = max(maxX,oldLim(2));
end
ylim([minX,maxX])
xlabel(ah,opt.yLabel);
end
% add mean/median
mh = [];mdh=[];
if opt.showMM
% plot mean, median. Mean is filled red circle, median is green square
% I don't know of a very clever way to flip xy and keep everything
% readable, thus it'll be copy-paste
switch opt.xyOri
case 'normal'
if any(opt.showMM==[1,2])
mh = plot(ah,opt.xValues,m,'+r','Color','r','MarkerSize',12);
end
if any(opt.showMM==[1,3])
mdh = plot(ah,opt.xValues,md,'sg','MarkerSize',12);
end
if opt.showMM == 4
% mh = plot(ah,opt.xValues,m,'+r','Color','black','MarkerSize',14);
mh = plot(ah,opt.xValues,m,'+r','Color',[.5 .5 .5],'MarkerSize',14);
mdh = myErrorbar(ah,opt.xValues,m,sem);
end
if opt.showMM == 5
mh = plot(ah,opt.xValues,m,'+r','Color','r','MarkerSize',12);
mdh = myErrorbar(ah,opt.xValues,m,sd);
end
case 'flipped'
if any(opt.showMM==[1,2])
mh = plot(ah,m,opt.xValues,'+r','Color','r','MarkerSize',12);
end
if any(opt.showMM==[1,3])
mdh = plot(ah,md,opt.xValues,'sg','MarkerSize',12);
end
if opt.showMM == 4
mh = plot(ah,m,opt.xValues,'+r','Color','black','MarkerSize',18);
mdh = myErrorbar(ah,m,opt.xValues,[sem,NaN(size(sem))]);
end
if opt.showMM == 5
mh = plot(ah,m,opt.xValues,'+r','Color','r','MarkerSize',12);
mdh = myErrorbar(ah,m,opt.xValues,[sd,NaN(size(sd))]);
end
end
end
%==========================
%% CLEANUP & ASSIGN OUTPUT
%==========================
if nargout > 0
handles{1} = ph;
handles{2} = [mh;mdh];
handles{3} = ah;
end