From 89797b4211657a947cf63dc15b37baad80df72aa Mon Sep 17 00:00:00 2001 From: binelp Date: Wed, 16 Mar 2022 19:05:26 +0100 Subject: [PATCH 1/4] Add possibility of having two distributions side by side, more arguments available. Headers not corrected yet --- Violin.m | 348 ++++++++++++++++++++++++++++++++++++++++++--------- violinplot.m | 158 +++++++++++++---------- 2 files changed, 381 insertions(+), 125 deletions(-) diff --git a/Violin.m b/Violin.m index 21a8c1b..1da0c65 100644 --- a/Violin.m +++ b/Violin.m @@ -48,19 +48,25 @@ % Copyright (c) 2016, Bastian Bechtold % This code is released under the terms of the BSD 3-clause license - properties - ScatterPlot % scatter plot of the data points - ViolinPlot % fill plot of the kernel density estimate - BoxPlot % fill plot of the box between the quartiles - WhiskerPlot % line plot between the whisker ends - MedianPlot % scatter plot of the median (one point) - NotchPlots % scatter plots for the notch indicators - MeanPlot % line plot of the mean (horizontal line) + properties (Access=public) + ScatterPlot % scatter plot of the data points + ScatterPlot2 % comparison scatter plot of the data points + ViolinPlot % fill plot of the kernel density estimate + ViolinPlot2 % comparison fill plot of the kernel density estimate + BoxPlot % fill plot of the box between the quartiles + WhiskerPlot % line plot between the whisker ends + MedianPlot % scatter plot of the median (one point) + NotchPlots % scatter plots for the notch indicators + MeanPlot % line plot of the mean (horizontal line) + MeanDotPlot + MeanDotPlot2 end properties (Dependent=true) ViolinColor % fill color of the violin area and data points ViolinAlpha % transparency of the violin area and data points + MarkerSize + LineWidth EdgeColor % color of the violin area outline BoxColor % color of box, whiskers, and median/notch edges BoxWidth % width of box between the quartiles in axis space (default 10% of Violin plot width, 0.03) @@ -68,6 +74,10 @@ ShowData % whether to show data points ShowNotches % whether to show notch indicators ShowMean % whether to show mean indicator + ShowBox + ShowMedian + ShowMeanDot + ShowWhiskerPlot end methods @@ -104,56 +114,88 @@ % Defaults to false args = obj.checkInputs(data, pos, varargin{:}); + + if length(data)==1 + data2 = []; + data = data{1}; + + else + data2 = data{2}; + data = data{1}; + end + data = data(not(isnan(data))); + data2 = data2(not(isnan(data2))); if numel(data) == 1 obj.MedianPlot = scatter(pos, data, 'filled'); obj.MedianColor = args.MedianColor; obj.MedianPlot.MarkerEdgeColor = args.EdgeColor; return end - + hold('on'); - - % calculate kernel density estimation for the violin - if isempty(data) - return - end - [density, value] = ksdensity(data, 'bandwidth', args.Bandwidth); - density = density(value >= min(data) & value <= max(data)); - value = value(value >= min(data) & value <= max(data)); - value(1) = min(data); - value(end) = max(data); - - % all data is identical - if min(data) == max(data) - density = 1; + + + %% calculate kernel density estimation for the violin + [density, value, width] = obj.calcKernelDensity(data, args.Bandwidth, args.Width); + + % also calculate the kernel density of the comparison data if + % provided + if ~isempty(data2) + [densityC, valueC, widthC] = obj.calcKernelDensity(data2, args.Bandwidth, args.Width); end - - width = args.Width/max(density); - - % plot the data points within the violin area + + %% plot the data points within the violin area if length(density) > 1 jitterstrength = interp1(value, density*width, data); else % all data is identical: jitterstrength = density*width; end - jitter = 2*(rand(size(data))-0.5); + if isempty(data2) % if no comparison data + jitter = 2*(rand(size(data))-0.5); % both sides + else + jitter = rand(size(data)); % only right side + end obj.ScatterPlot = ... scatter(pos + jitter.*jitterstrength, data, 'filled'); + + if ~isempty(data2) + % plot the data points within the violin area + if length(densityC) > 1 + jitterstrength = interp1(valueC, densityC*widthC, data2); + else % all data is identical: + jitterstrength = densityC*widthC; + end + jitter = rand(size(data2))-1; + obj.ScatterPlot2 = ... + scatter(pos + jitter.*jitterstrength, data2, 'filled'); + end + + %% plot the violins + if isempty(data2) + % plot the violin on boths sides + obj.ViolinPlot = ... % plot color will be overwritten later + fill([pos+density*width pos-density(end:-1:1)*width], ... + [value value(end:-1:1)], [1 1 1]); + else + % plot right half of the violin + obj.ViolinPlot = ... + fill([pos+density*width pos-density(1)*width], ... + [value value(1)], [1 1 1]); + % plot left half of the violin + obj.ViolinPlot2 = ... + fill([pos-densityC(end)*widthC pos-densityC(end:-1:1)*widthC], ... + [valueC(end) valueC(end:-1:1)], [1 1 1]); + end - % plot the violin - obj.ViolinPlot = ... % plot color will be overwritten later - fill([pos+density*width pos-density(end:-1:1)*width], ... - [value value(end:-1:1)], [1 1 1]); - - % plot the mini-boxplot within the violin + %% plot the mini-boxplot within the violin quartiles = quantile(data, [0.25, 0.5, 0.75]); obj.BoxPlot = ... % plot color will be overwritten later fill(pos+[-1,1,1,-1]*args.BoxWidth, ... [quartiles(1) quartiles(1) quartiles(3) quartiles(3)], ... [1 1 1]); - % plot the data mean + %% plot the data mean meanValue = mean(data); if length(density) > 1 meanDensityWidth = interp1(value, density, meanValue)*width; @@ -166,7 +208,15 @@ obj.MeanPlot = plot(pos+[-1,1].*meanDensityWidth, ... [meanValue, meanValue]); obj.MeanPlot.LineWidth = 1; - + + obj.MeanDotPlot = scatter(pos, meanValue, args.MarkerSize, args.BoxColor, 'filled'); + + if ~isempty(data2) + meanValue2 = mean(data2); + obj.MeanDotPlot2 = scatter(pos, meanValue2, args.MarkerSize, [0.2 0.2 0.2], 'filled'); + end + + %% plot the median, notch, and whiskers IQR = quartiles(3) - quartiles(1); lowhisker = quartiles(1) - 1.5*IQR; lowhisker = max(lowhisker, min(data(data > lowhisker))); @@ -175,7 +225,8 @@ if ~isempty(lowhisker) && ~isempty(hiwhisker) obj.WhiskerPlot = plot([pos pos], [lowhisker hiwhisker]); end - obj.MedianPlot = scatter(pos, quartiles(2), [], [1 1 1], 'filled'); + + obj.MedianPlot = scatter(pos, quartiles(2), args.MarkerSize, [1 1 1], 'filled'); obj.NotchPlots = ... scatter(pos, quartiles(2)-1.57*IQR/sqrt(length(data)), ... @@ -183,29 +234,75 @@ obj.NotchPlots(2) = ... scatter(pos, quartiles(2)+1.57*IQR/sqrt(length(data)), ... [], [1 1 1], 'filled', 'v'); - + + %% set graphical preferences obj.EdgeColor = args.EdgeColor; + obj.MedianPlot.LineWidth = args.LineWidth; obj.BoxColor = args.BoxColor; obj.BoxWidth = args.BoxWidth; obj.MedianColor = args.MedianColor; + obj.ShowData = args.ShowData; + obj.ShowNotches = args.ShowNotches; + obj.ShowMean = args.ShowMean; + obj.ShowBox = args.ShowBox; + obj.ShowMedian = args.ShowMedian; + obj.ShowMeanDot = args.ShowMeanDot; + obj.ShowWhiskerPlot = args.ShowWhiskerPlot; + if not(isempty(args.ViolinColor)) - if size(args.ViolinColor,1) > 1 - obj.ViolinColor = args.ViolinColor(pos,:); + if size(args.ViolinColor{1},1) > 1 + ViolinColor{1} = args.ViolinColor{1}(pos,:); + else + ViolinColor{1} = args.ViolinColor{1}; + end + if length(args.ViolinColor)==2 + if size(args.ViolinColor{2},1) > 1 + ViolinColor{2} = args.ViolinColor{2}(pos,:); + else + ViolinColor{2} = args.ViolinColor{2}; + end else - obj.ViolinColor = args.ViolinColor; + ViolinColor{2} = ViolinColor{1}; end else - obj.ViolinColor = obj.ScatterPlot.CData; + % defaults + ViolinColor{1} = obj.ScatterPlot.CData; + ViolinColor{2} = [0 0 0]; end - obj.ViolinAlpha = args.ViolinAlpha; - obj.ShowData = args.ShowData; - obj.ShowNotches = args.ShowNotches; - obj.ShowMean = args.ShowMean; - end + obj.ViolinColor = ViolinColor; + + + if not(isempty(args.ViolinAlpha)) + if length(args.ViolinAlpha{1})>1 + error('Only scalar values are accepted for the alpha color channel'); + else + ViolinAlpha{1} = args.ViolinAlpha{1}; + end + if length(args.ViolinAlpha)==2 + if length(args.ViolinAlpha{2})>1 + error('Only scalar values are accepted for the alpha color channel'); + else + ViolinAlpha{2} = args.ViolinAlpha{2}; + end + else + ViolinAlpha{2} = ViolinAlpha{1}; + end + else + % default + ViolinAlpha = {1,1}; + end + obj.ViolinAlpha = ViolinAlpha; + + end + + %% SET METHODS function set.EdgeColor(obj, color) if ~isempty(obj.ViolinPlot) obj.ViolinPlot.EdgeColor = color; + if ~isempty(obj.ViolinPlot2) + obj.ViolinPlot2.EdgeColor = color; + end end end @@ -215,6 +312,7 @@ end end + function set.MedianColor(obj, color) obj.MedianPlot.MarkerFaceColor = color; if ~isempty(obj.NotchPlots) @@ -226,6 +324,7 @@ function color = get.MedianColor(obj) color = obj.MedianPlot.MarkerFaceColor; end + function set.BoxColor(obj, color) if ~isempty(obj.BoxPlot) @@ -244,6 +343,7 @@ end end + function set.BoxWidth(obj,width) if ~isempty(obj.BoxPlot) pos=mean(obj.BoxPlot.XData); @@ -254,25 +354,42 @@ function width = get.BoxWidth(obj) width=max(obj.BoxPlot.XData)-min(obj.BoxPlot.XData); end + function set.ViolinColor(obj, color) - obj.ViolinPlot.FaceColor = color; - obj.ScatterPlot.MarkerFaceColor = color; - obj.MeanPlot.Color = color; + obj.ViolinPlot.FaceColor = color{1}; + obj.ScatterPlot.MarkerFaceColor = color{1}; + obj.MeanPlot.Color = color{1}; + if ~isempty(obj.ViolinPlot2) + obj.ViolinPlot2.FaceColor = color{2}; + obj.ScatterPlot2.MarkerFaceColor = color{2}; + end end function color = get.ViolinColor(obj) - color = obj.ViolinPlot.FaceColor; + color{1} = obj.ViolinPlot.FaceColor; + if ~isempty(obj.ViolinPlot2) + color{2} = obj.ViolinPlot2.FaceColor; + end end - + + function set.ViolinAlpha(obj, alpha) - obj.ScatterPlot.MarkerFaceAlpha = alpha; - obj.ViolinPlot.FaceAlpha = alpha; + obj.ViolinPlot.FaceAlpha = alpha{1}; + obj.ScatterPlot.MarkerFaceAlpha = alpha{1}; + if ~isempty(obj.ViolinPlot2) + obj.ViolinPlot2.FaceAlpha = alpha{2}; + obj.ScatterPlot2.MarkerFaceAlpha = alpha{2}; + end end function alpha = get.ViolinAlpha(obj) - alpha = obj.ViolinPlot.FaceAlpha; + alpha{1} = obj.ViolinPlot.FaceAlpha; + if ~isempty(obj.ViolinPlot2) + alpha{2} = obj.ViolinPlot2.FaceAlpha; + end end + function set.ShowData(obj, yesno) if yesno @@ -280,6 +397,10 @@ else obj.ScatterPlot.Visible = 'off'; end + if ~isempty(obj.ScatterPlot2) + obj.ScatterPlot2.Visible = obj.ScatterPlot.Visible; + end + end function yesno = get.ShowData(obj) @@ -288,6 +409,7 @@ end end + function set.ShowNotches(obj, yesno) if ~isempty(obj.NotchPlots) if yesno @@ -306,6 +428,7 @@ end end + function set.ShowMean(obj, yesno) if ~isempty(obj.MeanPlot) if yesno @@ -317,34 +440,135 @@ end function yesno = get.ShowMean(obj) - if ~isempty(obj.MeanPlot) - yesno = strcmp(obj.MeanPlot.Visible, 'on'); + if ~isempty(obj.BoxPlot) + yesno = strcmp(obj.BoxPlot.Visible, 'on'); + end + end + + + function set.ShowBox(obj, yesno) + if ~isempty(obj.BoxPlot) + if yesno + obj.BoxPlot.Visible = 'on'; + else + obj.BoxPlot.Visible = 'off'; + end + end + end + + function yesno = get.ShowBox(obj) + if ~isempty(obj.BoxPlot) + yesno = strcmp(obj.BoxPlot.Visible, 'on'); + end + end + + + function set.ShowMedian(obj, yesno) + if ~isempty(obj.MedianPlot) + if yesno + obj.MedianPlot.Visible = 'on'; + else + obj.MedianPlot.Visible = 'off'; + end end end + + function yesno = get.ShowMedian(obj) + if ~isempty(obj.MedianPlot) + yesno = strcmp(obj.MedianPlot.Visible, 'on'); + end + end + + + function set.ShowMeanDot(obj, yesno) + if ~isempty(obj.MeanDotPlot) + if yesno + obj.MeanDotPlot.Visible = 'on'; + else + obj.MeanDotPlot.Visible = 'off'; + end + if ~isempty(obj.MeanDotPlot2) + obj.MeanDotPlot2.Visible = obj.MeanDotPlot.Visible; + end + end + end + + function yesno = get.ShowMeanDot(obj) + if ~isempty(obj.MeanDotPlot) + yesno = strcmp(obj.MeanDotPlot.Visible, 'on'); + end + end + + + function set.ShowWhiskerPlot(obj, yesno) + if ~isempty(obj.WhiskerPlot) + if yesno + obj.WhiskerPlot.Visible = 'on'; + else + obj.WhiskerPlot.Visible = 'off'; + end + end + end + + function yesno = get.ShowWhiskerPlot(obj) + if ~isempty(obj.WhiskerPlot) + yesno = strcmp(obj.WhiskerPlot.Visible, 'on'); + end + end + end methods (Access=private) - function results = checkInputs(obj, data, pos, varargin) + function results = checkInputs(~, data, pos, varargin) isscalarnumber = @(x) (isnumeric(x) & isscalar(x)); p = inputParser(); - p.addRequired('Data', @isnumeric); + p.addRequired('Data', @(x)isnumeric(vertcat(x{:}))); p.addRequired('Pos', isscalarnumber); p.addParameter('Width', 0.3, isscalarnumber); p.addParameter('Bandwidth', [], isscalarnumber); iscolor = @(x) (isnumeric(x) & size(x,2) == 3); - p.addParameter('ViolinColor', [], iscolor); + p.addParameter('ViolinColor', [], @(x)iscolor(vertcat(x{:}))); + p.addParameter('MarkerSize', 4, @isnumeric); + p.addParameter('LineWidth', 0.75, @isnumeric); p.addParameter('BoxColor', [0.5 0.5 0.5], iscolor); p.addParameter('BoxWidth', 0.01, isscalarnumber); p.addParameter('EdgeColor', [0.5 0.5 0.5], iscolor); p.addParameter('MedianColor', [1 1 1], iscolor); - p.addParameter('ViolinAlpha', 0.3, isscalarnumber); + p.addParameter('ViolinAlpha', {0.3,0.3}, @(x)isnumeric(vertcat(x{:}))); isscalarlogical = @(x) (islogical(x) & isscalar(x)); p.addParameter('ShowData', true, isscalarlogical); p.addParameter('ShowNotches', false, isscalarlogical); p.addParameter('ShowMean', false, isscalarlogical); - + p.addParameter('ShowBox', true, isscalarlogical); + p.addParameter('ShowMedian', true, isscalarlogical); + p.addParameter('ShowMeanDot', false, isscalarlogical); + p.addParameter('ShowWhiskerPlot', true, isscalarlogical); p.parse(data, pos, varargin{:}); results = p.Results; end end + + methods (Static) + function [density, value, width] = calcKernelDensity(data, bandwidth, width) + + if isempty(data) + error('Empty input data'); + end + [density, value] = ksdensity(data, 'bandwidth', bandwidth); + density = density(value >= min(data) & value <= max(data)); + value = value(value >= min(data) & value <= max(data)); + value(1) = min(data); + value(end) = max(data); + value = [value(1)*(1-1E-5), value, value(end)*(1+1E-5)]; + density = [0, density, 0]; + + % all data is identical + if min(data) == max(data) + density = 1; + end + + width = width/max(density); + + end + end end diff --git a/violinplot.m b/violinplot.m index 505f1eb..73fbbe1 100644 --- a/violinplot.m +++ b/violinplot.m @@ -52,88 +52,120 @@ % Copyright (c) 2016, Bastian Bechtold % This code is released under the terms of the BSD 3-clause license - hascategories = exist('cats','var') && not(isempty(cats)); - - %parse the optional grouporder argument - %if it exists parse the categories order - % but also delete it from the arguments passed to Violin - grouporder = {}; - idx=find(strcmp(varargin, 'GroupOrder')); - if ~isempty(idx) && numel(varargin)>idx - if iscell(varargin{idx+1}) - grouporder = varargin{idx+1}; - varargin(idx:idx+1)=[]; - else - error('Second argument of ''GroupOrder'' optional arg must be a cell of category names') - end +hascategories = exist('cats','var') && not(isempty(cats)); + +%parse the optional grouporder argument +%if it exists parse the categories order +% but also delete it from the arguments passed to Violin +grouporder = {}; +idx=find(strcmp(varargin, 'GroupOrder')); +if ~isempty(idx) && numel(varargin)>idx + if iscell(varargin{idx+1}) + grouporder = varargin{idx+1}; + varargin(idx:idx+1)=[]; + else + error('Second argument of ''GroupOrder'' optional arg must be a cell of category names') end - - % tabular data - if isa(data, 'dataset') || isstruct(data) || istable(data) - if isa(data, 'dataset') - colnames = data.Properties.VarNames; - elseif istable(data) - colnames = data.Properties.VariableNames; - elseif isstruct(data) - colnames = fieldnames(data); - end - catnames = {}; - if isempty(grouporder) - for n=1:length(colnames) - if isnumeric(data.(colnames{n})) - catnames = [catnames colnames{n}]; - end - end - catnames = sort(catnames); - else - for n=1:length(grouporder) - if isnumeric(data.(grouporder{n})) - catnames = [catnames grouporder{n}]; - end +end + +% check and correct the structure of ViolinColor input +idx=find(strcmp(varargin, 'ViolinColor')); +if ~isempty(idx) && iscell(varargin{idx+1}) + if length(varargin{idx+1}(:))>2 + error('ViolinColor input can be at most a two element cell array'); + end +elseif ~isempty(idx) && isnumeric(varargin{idx+1}) + varargin{idx+1} = varargin(idx+1); +end + +% check and correct the structure of ViolinAlpha input +idx=find(strcmp(varargin, 'ViolinAlpha')); +if ~isempty(idx) && iscell(varargin{idx+1}) + if length(varargin{idx+1}(:))>2 + error('ViolinAlpha input can be at most a two element cell array'); + end +elseif ~isempty(idx) && isnumeric(varargin{idx+1}) + varargin{idx+1} = varargin(idx+1); +end + +% tabular data +if isa(data, 'dataset') || isstruct(data) || istable(data) + if isa(data, 'dataset') + colnames = data.Properties.VarNames; + elseif istable(data) + colnames = data.Properties.VariableNames; + elseif isstruct(data) + colnames = fieldnames(data); + end + catnames = {}; + if isempty(grouporder) + for n=1:length(colnames) + if isnumeric(data.(colnames{n})) + catnames = [catnames colnames{n}]; %#ok<*AGROW> end end - - for n=1:length(catnames) - thisData = data.(catnames{n}); - violins(n) = Violin(thisData, n, varargin{:}); + catnames = sort(catnames); + else + for n=1:length(grouporder) + if isnumeric(data.(grouporder{n})) + catnames = [catnames grouporder{n}]; + end end - set(gca, 'XTick', 1:length(catnames), 'XTickLabels', catnames); - + end + + for n=1:length(catnames) + thisData = data.(catnames{n}); + violins(n) = Violin(thisData, n, varargin{:}); + end + set(gca, 'XTick', 1:length(catnames), 'XTickLabels', catnames); + +elseif iscell(data) && length(data(:))==2 % cell input + if not(size(data{1},2)==size(data{2},2)) + error('The two input data matrices have to have the same number of columns'); + end +elseif iscell(data) && length(data(:))>2 % cell input + error('Up to two datasets can be compared'); +elseif isnumeric(data) % numeric input + % 1D data, one category for each data point - elseif hascategories && numel(data) == numel(cats) - + if hascategories && numel(data) == numel(cats) + if isempty(grouporder) cats = categorical(cats); else cats = categorical(cats, grouporder); end - + catnames = (unique(cats)); % this ignores categories without any data catnames_labels = {}; for n = 1:length(catnames) thisCat = catnames(n); catnames_labels{n} = char(thisCat); thisData = data(cats == thisCat); - violins(n) = Violin(thisData, n, varargin{:}); + violins(n) = Violin({thisData}, n, varargin{:}); end set(gca, 'XTick', 1:length(catnames), 'XTickLabels', catnames_labels); + else + data = {data}; + end +end - % 1D data, no categories - elseif not(hascategories) && isvector(data) - violins = Violin(data, 1, varargin{:}); - set(gca, 'XTick', 1); - - % 2D data with or without categories - elseif ismatrix(data) - for n=1:size(data, 2) - thisData = data(:, n); - violins(n) = Violin(thisData, n, varargin{:}); - end - set(gca, 'XTick', 1:size(data, 2)); - if hascategories && length(cats) == size(data, 2) - set(gca, 'XTickLabels', cats); - end - +% 1D data, no categories +if not(hascategories) && isvector(data{1}) + violins = Violin(data, 1, varargin{:}); + set(gca, 'XTick', 1); + +% 2D data with or without categories +elseif ismatrix(data{1}) + for n=1:size(data{1}, 2) + thisData = cellfun(@(x)x(:,n),data,'UniformOutput',false); + violins(n) = Violin(thisData, n, varargin{:}); + end + set(gca, 'XTick', 1:size(data, 2)); + if hascategories && length(cats) == size(data, 2) + set(gca, 'XTickLabels', cats); end + +end end From 91be868dfcec4342013536fac0e51c717f1a223d Mon Sep 17 00:00:00 2001 From: binelp Date: Thu, 17 Mar 2022 12:00:26 +0100 Subject: [PATCH 2/4] Changes headers --- Violin.m | 160 ++++++++++++++++++++++++--------------------------- violinplot.m | 35 ++++++++--- 2 files changed, 102 insertions(+), 93 deletions(-) diff --git a/Violin.m b/Violin.m index 1da0c65..97c2406 100644 --- a/Violin.m +++ b/Violin.m @@ -3,10 +3,13 @@ % A violin plot is an easy to read substitute for a box plot % that replaces the box shape with a kernel density estimate of % the data, and optionally overlays the data points itself. + % It is also possible to provide two sets of data which are supposed + % to be compared by plotting each column of the two datasets together + % on each side of the violin. % % Additional constructor parameters include the width of the - % plot, the bandwidth of the kernel density estimation, and the - % X-axis position of the violin plot. + % plot, the bandwidth of the kernel density estimation, the + % X-axis position of the violin plot, and the categories. % % Use violinplot for a % boxplot-like wrapper for @@ -18,32 +21,44 @@ % 52, no. 2, pp. 181-184, 1998. % % Violin Properties: - % ViolinColor - Fill color of the violin area and data points. - % Defaults to the next default color cycle. - % ViolinAlpha - Transparency of the ciolin area and data points. - % Defaults to 0.3. - % EdgeColor - Color of the violin area outline. - % Defaults to [0.5 0.5 0.5] - % BoxColor - Color of the box, whiskers, and the outlines of - % the median point and the notch indicators. - % Defaults to [0.5 0.5 0.5] - % MedianColor - Fill color of the median and notch indicators. - % Defaults to [1 1 1] - % ShowData - Whether to show data points. - % Defaults to true - % ShowNotches - Whether to show notch indicators. - % Defaults to false - % ShowMean - Whether to show mean indicator. - % Defaults to false + % ViolinColor - Fill color of the violin area and data points. + % Can be either a matrix nx3 or an array of up to two + % cells containing nx3 matrices. + % Defaults to the next default color cycle. + % ViolinAlpha - Transparency of the violin area and data points. + % Can be either a single scalar value or an array of + % up to two cells containing scalar values. + % Defaults to 0.3. + % EdgeColor - Color of the violin area outline. + % Defaults to [0.5 0.5 0.5] + % BoxColor - Color of the box, whiskers, and the outlines of + % the median point and the notch indicators. + % Defaults to [0.5 0.5 0.5] + % MedianColor - Fill color of the median and notch indicators. + % Defaults to [1 1 1] + % ShowData - Whether to show data points. + % Defaults to true + % ShowNotches - Whether to show notch indicators. + % Defaults to false + % ShowMean - Whether to show mean indicator. + % Defaults to false + % ShowBox - Whether to show the box. + % Defaults to true + % ShowMedian - Whether to show the median indicator. + % Defaults to true + % ShowWhiskers - Whether to show the whiskers + % Defaults to true % % Violin Children: - % ScatterPlot - scatter plot of the data points - % ViolinPlot - fill plot of the kernel density estimate - % BoxPlot - fill plot of the box between the quartiles - % WhiskerPlot - line plot between the whisker ends - % MedianPlot - scatter plot of the median (one point) - % NotchPlots - scatter plots for the notch indicators - % MeanPlot - line plot at mean value + % ScatterPlot - scatter plot of the data points + % ScatterPlot2 - scatter second plot of the data points + % ViolinPlot - fill plot of the kernel density estimate + % ViolinPlot2 - fill second plot of the kernel density estimate + % BoxPlot - fill plot of the box between the quartiles + % WhiskerPlot - line plot between the whisker ends + % MedianPlot - scatter plot of the median (one point) + % NotchPlots - scatter plots for the notch indicators + % MeanPlot - line plot at mean value % Copyright (c) 2016, Bastian Bechtold % This code is released under the terms of the BSD 3-clause license @@ -58,26 +73,23 @@ MedianPlot % scatter plot of the median (one point) NotchPlots % scatter plots for the notch indicators MeanPlot % line plot of the mean (horizontal line) - MeanDotPlot - MeanDotPlot2 end properties (Dependent=true) - ViolinColor % fill color of the violin area and data points - ViolinAlpha % transparency of the violin area and data points - MarkerSize - LineWidth - EdgeColor % color of the violin area outline - BoxColor % color of box, whiskers, and median/notch edges - BoxWidth % width of box between the quartiles in axis space (default 10% of Violin plot width, 0.03) - MedianColor % fill color of median and notches - ShowData % whether to show data points - ShowNotches % whether to show notch indicators - ShowMean % whether to show mean indicator - ShowBox - ShowMedian - ShowMeanDot - ShowWhiskerPlot + ViolinColor % fill color of the violin area and data points + ViolinAlpha % transparency of the violin area and data points + MarkerSize % marker size for the median dot + LineWidth % linewidth of the median plot + EdgeColor % color of the violin area outline + BoxColor % color of box, whiskers, and median/notch edges + BoxWidth % width of box between the quartiles in axis space (default 10% of Violin plot width, 0.03) + MedianColor % fill color of median and notches + ShowData % whether to show data points + ShowNotches % whether to show notch indicators + ShowMean % whether to show mean indicator + ShowBox % whether to show the box + ShowMedian % whether to show the median line + ShowWhiskers % whether to show the whiskers end methods @@ -93,11 +105,14 @@ % 'Bandwidth' Bandwidth of the kernel density % estimate. Should be between 10% and % 40% of the data range. - % 'ViolinColor' Fill color of the violin area and - % data points. Defaults to the next - % default color cycle. - % 'ViolinAlpha' Transparency of the violin area and - % data points. Defaults to 0.3. + % 'ViolinColor' Fill color of the violin area + % and data points.Can be either a matrix + % nx3 or an array of up to two cells + % containing nx3 matrices. + % 'ViolinAlpha' Transparency of the violin area and data + % points. Can be either a single scalar + % value or an array of up to two cells + % containing scalar values. Defaults to 0.3. % 'EdgeColor' Color of the violin area outline. % Defaults to [0.5 0.5 0.5] % 'BoxColor' Color of the box, whiskers, and the @@ -112,6 +127,12 @@ % Defaults to false % 'ShowMean' Whether to show mean indicator. % Defaults to false + % 'ShowBox' Whether to show the box + % Defaults to true + % 'ShowMedian' Whether to show the median line + % Defaults to true + % 'ShowWhiskers' Whether to show the whiskers + % Defaults to true args = obj.checkInputs(data, pos, varargin{:}); @@ -209,13 +230,6 @@ [meanValue, meanValue]); obj.MeanPlot.LineWidth = 1; - obj.MeanDotPlot = scatter(pos, meanValue, args.MarkerSize, args.BoxColor, 'filled'); - - if ~isempty(data2) - meanValue2 = mean(data2); - obj.MeanDotPlot2 = scatter(pos, meanValue2, args.MarkerSize, [0.2 0.2 0.2], 'filled'); - end - %% plot the median, notch, and whiskers IQR = quartiles(3) - quartiles(1); lowhisker = quartiles(1) - 1.5*IQR; @@ -246,8 +260,7 @@ obj.ShowMean = args.ShowMean; obj.ShowBox = args.ShowBox; obj.ShowMedian = args.ShowMedian; - obj.ShowMeanDot = args.ShowMeanDot; - obj.ShowWhiskerPlot = args.ShowWhiskerPlot; + obj.ShowWhiskers = args.ShowWhiskers; if not(isempty(args.ViolinColor)) if size(args.ViolinColor{1},1) > 1 @@ -480,27 +493,7 @@ end - function set.ShowMeanDot(obj, yesno) - if ~isempty(obj.MeanDotPlot) - if yesno - obj.MeanDotPlot.Visible = 'on'; - else - obj.MeanDotPlot.Visible = 'off'; - end - if ~isempty(obj.MeanDotPlot2) - obj.MeanDotPlot2.Visible = obj.MeanDotPlot.Visible; - end - end - end - - function yesno = get.ShowMeanDot(obj) - if ~isempty(obj.MeanDotPlot) - yesno = strcmp(obj.MeanDotPlot.Visible, 'on'); - end - end - - - function set.ShowWhiskerPlot(obj, yesno) + function set.ShowWhiskers(obj, yesno) if ~isempty(obj.WhiskerPlot) if yesno obj.WhiskerPlot.Visible = 'on'; @@ -510,7 +503,7 @@ end end - function yesno = get.ShowWhiskerPlot(obj) + function yesno = get.ShowWhiskers(obj) if ~isempty(obj.WhiskerPlot) yesno = strcmp(obj.WhiskerPlot.Visible, 'on'); end @@ -528,7 +521,7 @@ p.addParameter('Bandwidth', [], isscalarnumber); iscolor = @(x) (isnumeric(x) & size(x,2) == 3); p.addParameter('ViolinColor', [], @(x)iscolor(vertcat(x{:}))); - p.addParameter('MarkerSize', 4, @isnumeric); + p.addParameter('MarkerSize', 36, @isnumeric); p.addParameter('LineWidth', 0.75, @isnumeric); p.addParameter('BoxColor', [0.5 0.5 0.5], iscolor); p.addParameter('BoxWidth', 0.01, isscalarnumber); @@ -541,8 +534,7 @@ p.addParameter('ShowMean', false, isscalarlogical); p.addParameter('ShowBox', true, isscalarlogical); p.addParameter('ShowMedian', true, isscalarlogical); - p.addParameter('ShowMeanDot', false, isscalarlogical); - p.addParameter('ShowWhiskerPlot', true, isscalarlogical); + p.addParameter('ShowWhiskers', true, isscalarlogical); p.parse(data, pos, varargin{:}); results = p.Results; end @@ -550,7 +542,6 @@ methods (Static) function [density, value, width] = calcKernelDensity(data, bandwidth, width) - if isempty(data) error('Empty input data'); end @@ -566,9 +557,8 @@ if min(data) == max(data) density = 1; end - + width = width/max(density); - end end end diff --git a/violinplot.m b/violinplot.m index 73fbbe1..125a73a 100644 --- a/violinplot.m +++ b/violinplot.m @@ -5,19 +5,26 @@ % VIOLINPLOT(DATAMATRIX) plots violins for each column in % DATAMATRIX. % -% VIOLINPLOT(TABLE), VIOLINPLOT(STRUCT), VIOLINPLOT(DATASET) -% plots violins for each column in TABLE, each field in STRUCT, and -% each variable in DATASET. The violins are labeled according to -% the table/dataset variable name or the struct field name. -% % VIOLINPLOT(DATAMATRIX, CATEGORYNAMES) plots violins for each % column in DATAMATRIX and labels them according to the names in the % cell-of-strings CATEGORYNAMES. % +% In the cases above DATA and DATAMATRIX can be a vector or a matrix, +% respectively, either as is or wrapped in a cell. +% To produce violins which have one distribution on one half and another +% one on the other half, DATA and DATAMATRIX have to be cell arrays +% with two elements, each containing a vector or a matrix. The number of +% columns of the two data sets has to be the same. +% % VIOLINPLOT(DATA, CATEGORIES) where double vector DATA and vector % CATEGORIES are of equal length; plots violins for each category in % DATA. % +% VIOLINPLOT(TABLE), VIOLINPLOT(STRUCT), VIOLINPLOT(DATASET) +% plots violins for each column in TABLE, each field in STRUCT, and +% each variable in DATASET. The violins are labeled according to +% the table/dataset variable name or the struct field name. +% % violins = VIOLINPLOT(...) returns an object array of % Violin objects. % @@ -29,9 +36,13 @@ % Should be between 10% and 40% of the data range. % 'ViolinColor' Fill color of the violin area and data points. Accepts % 1x3 color vector or nx3 color vector where n = num -% groups +% groups. In case of two data sets being compared it can +% be an array of up to two cells containing nx3 +% matrices. % Defaults to the next default color cycle. % 'ViolinAlpha' Transparency of the violin area and data points. +% Can be either a single scalar value or an array of +% up to two cells containing scalar values. % Defaults to 0.3. % 'EdgeColor' Color of the violin area outline. % Defaults to [0.5 0.5 0.5] @@ -46,6 +57,12 @@ % Defaults to false % 'ShowMean' Whether to show mean indicator % Defaults to false +% 'ShowBox' Whether to show the box. +% Defaults to true +% 'ShowMedian' Whether to show the median indicator. +% Defaults to true +% 'ShowWhiskers' Whether to show the whiskers +% Defaults to true % 'GroupOrder' Cell of category names in order to be plotted. % Defaults to alphabetical ordering @@ -161,11 +178,13 @@ thisData = cellfun(@(x)x(:,n),data,'UniformOutput',false); violins(n) = Violin(thisData, n, varargin{:}); end - set(gca, 'XTick', 1:size(data, 2)); - if hascategories && length(cats) == size(data, 2) + set(gca, 'XTick', 1:size(data{1}, 2)); + if hascategories && length(cats) == size(data{1}, 2) set(gca, 'XTickLabels', cats); end end +set(gca,'Box','on'); + end From 21f78e2568c355bbd300b0d7a411671a4b83b2d8 Mon Sep 17 00:00:00 2001 From: binelp Date: Mon, 21 Mar 2022 12:00:33 +0100 Subject: [PATCH 3/4] More reliable default color order --- Violin.m | 9 +++++++-- violinplot.m | 13 ++++++------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/Violin.m b/Violin.m index 97c2406..93c0595 100644 --- a/Violin.m +++ b/Violin.m @@ -145,6 +145,11 @@ data = data{1}; end + if isempty(args.ViolinColor) + C = colororder; + args.ViolinColor = {repmat(C,ceil(size(data,2)/length(C)),1)}; + end + data = data(not(isnan(data))); data2 = data2(not(isnan(data2))); if numel(data) == 1 @@ -298,7 +303,7 @@ ViolinAlpha{2} = args.ViolinAlpha{2}; end else - ViolinAlpha{2} = ViolinAlpha{1}; + ViolinAlpha{2} = ViolinAlpha{1}/2; % default unless specified end else % default @@ -527,7 +532,7 @@ p.addParameter('BoxWidth', 0.01, isscalarnumber); p.addParameter('EdgeColor', [0.5 0.5 0.5], iscolor); p.addParameter('MedianColor', [1 1 1], iscolor); - p.addParameter('ViolinAlpha', {0.3,0.3}, @(x)isnumeric(vertcat(x{:}))); + p.addParameter('ViolinAlpha', {0.3,0.15}, @(x)isnumeric(vertcat(x{:}))); isscalarlogical = @(x) (islogical(x) & isscalar(x)); p.addParameter('ShowData', true, isscalarlogical); p.addParameter('ShowNotches', false, isscalarlogical); diff --git a/violinplot.m b/violinplot.m index 125a73a..03d16bc 100644 --- a/violinplot.m +++ b/violinplot.m @@ -135,18 +135,17 @@ violins(n) = Violin(thisData, n, varargin{:}); end set(gca, 'XTick', 1:length(catnames), 'XTickLabels', catnames); - + set(gca,'Box','on'); + return elseif iscell(data) && length(data(:))==2 % cell input if not(size(data{1},2)==size(data{2},2)) error('The two input data matrices have to have the same number of columns'); end elseif iscell(data) && length(data(:))>2 % cell input error('Up to two datasets can be compared'); -elseif isnumeric(data) % numeric input - +elseif isnumeric(data) % numeric input % 1D data, one category for each data point - if hascategories && numel(data) == numel(cats) - + if hascategories && numel(data) == numel(cats) if isempty(grouporder) cats = categorical(cats); else @@ -162,6 +161,8 @@ violins(n) = Violin({thisData}, n, varargin{:}); end set(gca, 'XTick', 1:length(catnames), 'XTickLabels', catnames_labels); + set(gca,'Box','on'); + return else data = {data}; end @@ -171,7 +172,6 @@ if not(hascategories) && isvector(data{1}) violins = Violin(data, 1, varargin{:}); set(gca, 'XTick', 1); - % 2D data with or without categories elseif ismatrix(data{1}) for n=1:size(data{1}, 2) @@ -182,7 +182,6 @@ if hascategories && length(cats) == size(data{1}, 2) set(gca, 'XTickLabels', cats); end - end set(gca,'Box','on'); From cae9f95b1e0c59615d70c2c9e1d17bcb02513c57 Mon Sep 17 00:00:00 2001 From: binelp Date: Mon, 21 Mar 2022 12:32:56 +0100 Subject: [PATCH 4/4] Adds two test cases --- test_cases/testviolinplot.m | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/test_cases/testviolinplot.m b/test_cases/testviolinplot.m index 6047002..51e2a15 100644 --- a/test_cases/testviolinplot.m +++ b/test_cases/testviolinplot.m @@ -1,6 +1,6 @@ % TEST CASE 1 -display('Test 1: Violin plot default options'); +disp('Test 1: Violin plot default options'); load carbig MPG Origin Origin = cellstr(Origin); figure @@ -8,14 +8,40 @@ ylabel('Fuel Economy in MPG'); xlim([0.5, 7.5]); -display('Test 1 passed ok'); +disp('Test 1 passed ok'); % TEST CASE 2 -display('Test 2: Test the plot ordering option'); +disp('Test 2: Test the plot ordering option'); grouporder={'USA','Sweden','Japan','Italy','Germany','France','England'}; figure; vs2 = violinplot(MPG,Origin,'GroupOrder',grouporder); -display('Test 2 passed ok'); +disp('Test 2 passed ok'); +xlim([0.5, 7.5]); + +% TEST CASE 3 +disp('Test 3: Test the numeric input construction mode'); +figure +cats = categorical(Origin); +catnames = (unique(cats)); % this ignores categories without any data +catnames_labels = {}; +thisData = NaN(length(MPG),length(catnames)); +for n = 1:length(catnames) + thisCat = catnames(n); + catnames_labels{n} = char(thisCat); + thisData(1:length(MPG(cats == thisCat)),n) = MPG(cats == thisCat); +end +vs3 = violinplot(thisData,catnames_labels); +xlim([0.5, 7.5]); +disp('Test 3 passed ok'); + +% TEST CASE 4 +disp('Test 4: Test two sided violin plots. Japan is being compared.'); +figure +C = colororder; +vs4 = violinplot({thisData,repmat(thisData(:,5),1,7)},catnames_labels,'ViolinColor',{C,C(5,:)},'ViolinAlpha',{0.3 0.3}); +xlim([0.5, 7.5]); +disp('Test 4 passed ok'); + %other test cases could be added here