From 9e6000e8c717b73a65c45f8cce4fc058d00e91df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20H=C3=A4fele?= Date: Wed, 3 Jul 2024 15:36:32 +0200 Subject: [PATCH 1/7] Added orientation parameter which allows to plots to generate a vertical or horizontal layout. --- Violin.m | 1440 +++++++++++++++++++++++++------------------------- violinplot.m | 32 +- 2 files changed, 753 insertions(+), 719 deletions(-) diff --git a/Violin.m b/Violin.m index d2d41bd..cb20906 100644 --- a/Violin.m +++ b/Violin.m @@ -1,714 +1,726 @@ -classdef Violin < handle - % Violin creates violin plots for some data - % 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, the - % X-axis position of the violin plot, and the categories. - % - % Use violinplot for a - % boxplot-like wrapper for - % interactive plotting. - % - % See for more information on Violin Plots: - % J. L. Hintze and R. D. Nelson, "Violin plots: a box - % plot-density trace synergism," The American Statistician, vol. - % 52, no. 2, pp. 181-184, 1998. - % - % Violin Properties: - % 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 - % HalfViolin - Whether to do a half violin(left, right side) or - % full. Defaults to full. - % QuartileStyle - Option on how to display quartiles, with a - % boxplot, shadow or none. Defaults to boxplot. - % DataStyle - Defines the style to show the data points. Opts: - % 'scatter', 'histogram' or 'none'. Default is 'scatter'. - % - % - % Violin Children: - % 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 - - 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) - HistogramPlot % histogram of the data - ViolinPlotQ % fill plot of the Quartiles as shadow - end - - properties (Dependent=true) - ViolinColor % fill color of the violin area and data points - ViolinAlpha % transparency of the violin area and data points - MarkerSize % marker size for the data dots - MedianMarkerSize % 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 - HalfViolin % whether to do a half violin(left, right side) or full - end - - methods - function obj = Violin(data, pos, varargin) - %Violin plots a violin plot of some data at pos - % VIOLIN(DATA, POS) plots a violin at x-position POS for - % a vector of DATA points. - % - % VIOLIN(..., 'PARAM1', val1, 'PARAM2', val2, ...) - % specifies optional name/value pairs: - % 'Width' Width of the violin in axis space. - % Defaults to 0.3 - % '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.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. - % 'MarkerSize' Size of the data points, if shown. - % Defaults to 24 - % 'MedianMarkerSize' Size of the median indicator, if shown. - % Defaults to 36 - % '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 line - % Defaults to true - % 'ShowWhiskers' Whether to show the whiskers - % Defaults to true - % 'HalfViolin' Whether to do a half violin(left, right side) or - % full. Defaults to full. - % 'QuartileStyle' Option on how to display quartiles, with a - % boxplot or as a shadow. Defaults to boxplot. - % 'DataStyle' Defines the style to show the data points. Opts: - % 'scatter', 'histogram' or 'none'. Default is 'Scatter'. - - st = dbstack; % get the calling function for reporting errors - namefun = st.name; - args = obj.checkInputs(data, pos, varargin{:}); - - if length(data)==1 - data2 = []; - data = data{1}; - - else - data2 = data{2}; - data = data{1}; - end - - if isempty(args.ViolinColor) - Release= strsplit(version('-release'), {'a','b'}); %Check release - if str2num(Release{1})> 2019 || strcmp(version('-release'), '2019b') - C = colororder; - else - C = lines; - end - - if pos > length(C) - C = lines; - end - 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 - 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 - [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 - - %% Plot the data points within the violin area - if length(density) > 1 - [~, unique_idx] = unique(value); - jitterstrength = interp1(value(unique_idx), density(unique_idx)*width, data, 'linear','extrap'); - else % all data is identical: - jitterstrength = density*width; - end - 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 - switch args.HalfViolin % this is more modular - case 'left' - jitter = -1*(rand(size(data))); %left - case 'right' - jitter = 1*(rand(size(data))); %right - case 'full' - jitter = 2*(rand(size(data))-0.5); - end - % Make scatter plot - switch args.DataStyle - case 'scatter' - if ~isempty(data2) - jitter = 1*(rand(size(data))); %right - obj.ScatterPlot = ... - scatter(pos + jitter.*jitterstrength, data, args.MarkerSize, 'filled'); - % 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 = -1*rand(size(data2));% left - obj.ScatterPlot2 = ... - scatter(pos + jitter.*jitterstrength, data2, args.MarkerSize, 'filled'); - else - obj.ScatterPlot = ... - scatter(pos + jitter.*jitterstrength, data, args.MarkerSize, 'filled'); - - end - case 'histogram' - [counts,edges] = histcounts(data, size(unique(data),1)); - switch args.HalfViolin - case 'right' - obj.HistogramPlot= plot([pos-((counts')/max(counts))*max(jitterstrength)*2, pos*ones(size(counts,2),1)]',... - [edges(1:end-1)+max(diff(edges))/2; edges(1:end-1)+max(diff(edges))/2],'-','LineWidth',1, 'Color', 'k'); - case 'left' - obj.HistogramPlot= plot([pos*ones(size(counts,2),1), pos+((counts')/max(counts))*max(jitterstrength)*2]',... - [edges(1:end-1)+max(diff(edges))/2; edges(1:end-1)+max(diff(edges))/2],'-','LineWidth',1, 'Color', 'k'); - otherwise - fprintf([namefun, ' No histogram/bar plot option available for full violins, as it would look overcrowded.\n']) - end - case 'none' - end - - %% Plot the violin - halfViol= ones(1, size(density,2)); - if isempty(data2) % if no comparison data - switch args.HalfViolin - case 'right' - obj.ViolinPlot = ... % plot color will be overwritten later - fill([pos+density*width halfViol*pos], ... - [value value(end:-1:1)], [1 1 1],'LineStyle','-'); - case 'left' - obj.ViolinPlot = ... % plot color will be overwritten later - fill([halfViol*pos pos-density(end:-1:1)*width], ... - [value value(end:-1:1)], [1 1 1],'LineStyle','-'); - case 'full' - 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],'LineStyle','-'); - end - else - % plot right half of the violin - obj.ViolinPlot = ... - fill([pos+density*width pos-density(1)*width], ... - [value value(1)], [1 1 1],'LineStyle','-'); - % 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],'LineStyle','-'); - end - - %% Plot the quartiles within the violin - quartiles = quantile(data, [0.25, 0.5, 0.75]); - flat= [halfViol*pos halfViol*pos]; - switch args.QuartileStyle - case 'shadow' - switch args.HalfViolin - case 'right' - w = [pos+density*width halfViol*pos]; - h= [value value(end:-1:1)]; - case 'left' - w = [halfViol*pos pos-density(end:-1:1)*width]; - h= [value value(end:-1:1)]; - case 'full' - w = [pos+density*width pos-density(end:-1:1)*width]; - h= [value value(end:-1:1)]; - end - indices = h >= quartiles(1) & h <= quartiles(3); - obj.ViolinPlotQ = ... % plot color will be overwritten later - fill(w(indices), ... - h(indices),'Marker','none', [1 1 1],'LineStyle','-'); - case 'boxplot' - 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],'Marker','none','LineStyle','-'); - case 'none' - end - - %% Plot the data mean - meanValue = mean(data); - if length(density) > 1 - [~, unique_idx] = unique(value); - meanDensityWidth = interp1(value(unique_idx), density(unique_idx), meanValue, 'linear','extrap')*width; - else % all data is identical: - meanDensityWidth = density*width; - end - if meanDensityWidth lowhisker))); - hiwhisker = quartiles(3) + 1.5*IQR; - hiwhisker = min(hiwhisker, max(data(data < hiwhisker))); - if ~isempty(lowhisker) && ~isempty(hiwhisker) - obj.WhiskerPlot = plot([pos pos], [lowhisker hiwhisker],... - 'Marker','none','LineStyle','-'); - end - - % Median - obj.MedianPlot = scatter(pos, quartiles(2), args.MedianMarkerSize, [1 1 1], 'filled'); - - % Notches - obj.NotchPlots = ... - scatter(pos, quartiles(2)-1.57*IQR/sqrt(length(data)), ... - [], [1 1 1], 'filled', '^'); - 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.ShowWhiskers = args.ShowWhiskers; - - if not(isempty(args.ViolinColor)) - 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 - ViolinColor{2} = ViolinColor{1}; - end - else - % defaults - if args.scpltBool - ViolinColor{1} = obj.ScatterPlot.CData; - else - ViolinColor{1} = [0 0 0]; - end - ViolinColor{2} = [0 0 0]; - 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}/2; % default unless specified - end - else - % default - ViolinAlpha = {1,1}; - end - obj.ViolinAlpha = ViolinAlpha; - - set(obj.ViolinPlot, "Marker", "none", "LineStyle","-"); - set(obj.ViolinPlot2, "Marker", "none", "LineStyle","-"); - end - - %% SET METHODS - function set.EdgeColor(obj, color) - if ~isempty(obj.ViolinPlot) - obj.ViolinPlot.EdgeColor = color; - obj.ViolinPlotQ.EdgeColor = color; - if ~isempty(obj.ViolinPlot2) - obj.ViolinPlot2.EdgeColor = color; - end - end - end - - function color = get.EdgeColor(obj) - if ~isempty(obj.ViolinPlot) - color = obj.ViolinPlot.EdgeColor; - end - end - - - function set.MedianColor(obj, color) - obj.MedianPlot.MarkerFaceColor = color; - if ~isempty(obj.NotchPlots) - obj.NotchPlots(1).MarkerFaceColor = color; - obj.NotchPlots(2).MarkerFaceColor = color; - end - end - - function color = get.MedianColor(obj) - color = obj.MedianPlot.MarkerFaceColor; - end - - - function set.BoxColor(obj, color) - if ~isempty(obj.BoxPlot) - obj.BoxPlot.FaceColor = color; - obj.BoxPlot.EdgeColor = color; - obj.WhiskerPlot.Color = color; - obj.MedianPlot.MarkerEdgeColor = color; - obj.NotchPlots(1).MarkerFaceColor = color; - obj.NotchPlots(2).MarkerFaceColor = color; - elseif ~isempty(obj.ViolinPlotQ) - obj.WhiskerPlot.Color = color; - obj.MedianPlot.MarkerEdgeColor = color; - obj.NotchPlots(1).MarkerFaceColor = color; - obj.NotchPlots(2).MarkerFaceColor = color; - end - end - - function color = get.BoxColor(obj) - if ~isempty(obj.BoxPlot) - color = obj.BoxPlot.FaceColor; - end - end - - - function set.BoxWidth(obj,width) - if ~isempty(obj.BoxPlot) - pos=mean(obj.BoxPlot.XData); - obj.BoxPlot.XData=pos+[-1,1,1,-1]*width; - end - end - - function width = get.BoxWidth(obj) - width=max(obj.BoxPlot.XData)-min(obj.BoxPlot.XData); - end - - - function set.ViolinColor(obj, 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 - if ~isempty(obj.ViolinPlotQ) - obj.ViolinPlotQ.FaceColor = color{1}; - end - for idx = 1: size(obj.HistogramPlot,1) - obj.HistogramPlot(idx).Color = color{1}; - end - end - - function color = get.ViolinColor(obj) - color{1} = obj.ViolinPlot.FaceColor; - if ~isempty(obj.ViolinPlot2) - color{2} = obj.ViolinPlot2.FaceColor; - end - end - - - function set.ViolinAlpha(obj, alpha) - obj.ViolinPlotQ.FaceAlpha = .65; - obj.ViolinPlot.FaceAlpha = alpha{1}; - obj.ScatterPlot.MarkerFaceAlpha = 1; - if ~isempty(obj.ViolinPlot2) - obj.ViolinPlot2.FaceAlpha = alpha{2}; - obj.ScatterPlot2.MarkerFaceAlpha = 1; - end - end - - function alpha = get.ViolinAlpha(obj) - alpha{1} = obj.ViolinPlot.FaceAlpha; - if ~isempty(obj.ViolinPlot2) - alpha{2} = obj.ViolinPlot2.FaceAlpha; - end - end - - - function set.ShowData(obj, yesno) - if yesno - obj.ScatterPlot.Visible = 'on'; - for idx = 1: size(obj.HistogramPlot,1) - obj.HistogramPlot(idx).Visible = 'on'; - end - else - obj.ScatterPlot.Visible = 'off'; - for idx = 1: size(obj.HistogramPlot,1) - obj.HistogramPlot(idx).Visible = 'off'; - end - end - if ~isempty(obj.ScatterPlot2) - obj.ScatterPlot2.Visible = obj.ScatterPlot.Visible; - end - - end - - function yesno = get.ShowData(obj) - if ~isempty(obj.ScatterPlot) - yesno = strcmp(obj.ScatterPlot.Visible, 'on'); - end - end - - - function set.ShowNotches(obj, yesno) - if ~isempty(obj.NotchPlots) - if yesno - obj.NotchPlots(1).Visible = 'on'; - obj.NotchPlots(2).Visible = 'on'; - else - obj.NotchPlots(1).Visible = 'off'; - obj.NotchPlots(2).Visible = 'off'; - end - end - end - - function yesno = get.ShowNotches(obj) - if ~isempty(obj.NotchPlots) - yesno = strcmp(obj.NotchPlots(1).Visible, 'on'); - end - end - - - function set.ShowMean(obj, yesno) - if ~isempty(obj.MeanPlot) - if yesno - obj.MeanPlot.Visible = 'on'; - else - obj.MeanPlot.Visible = 'off'; - end - end - end - - function yesno = get.ShowMean(obj) - 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.ShowWhiskers(obj, yesno) - if ~isempty(obj.WhiskerPlot) - if yesno - obj.WhiskerPlot.Visible = 'on'; - else - obj.WhiskerPlot.Visible = 'off'; - end - end - end - - function yesno = get.ShowWhiskers(obj) - if ~isempty(obj.WhiskerPlot) - yesno = strcmp(obj.WhiskerPlot.Visible, 'on'); - end - end - - end - - methods (Access=private) - function results = checkInputs(~, data, pos, varargin) - isscalarnumber = @(x) (isnumeric(x) & isscalar(x)); - p = inputParser(); - 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', [], @(x)iscolor(vertcat(x{:}))); - p.addParameter('MarkerSize', 24, @isnumeric); - p.addParameter('MedianMarkerSize', 36, @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,0.15}, @(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('ShowWhiskers', true, isscalarlogical); - validSides={'full', 'right', 'left'}; - checkSide = @(x) any(validatestring(x, validSides)); - p.addParameter('HalfViolin', 'full', checkSide); - validQuartileStyles={'boxplot', 'shadow', 'none'}; - checkQuartile = @(x)any(validatestring(x, validQuartileStyles)); - p.addParameter('QuartileStyle', 'boxplot', checkQuartile); - validDataStyles = {'scatter', 'histogram', 'none'}; - checkStyle = @(x)any(validatestring(x, validDataStyles)); - p.addParameter('DataStyle', 'scatter', checkStyle); - - 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; - value= mean(value); - end - - width = width/max(density); - end - end -end - +classdef Violin < handle + % Violin creates violin plots for some data + % 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, the + % X-axis position of the violin plot, and the categories. + % + % Use violinplot for a + % boxplot-like wrapper for + % interactive plotting. + % + % See for more information on Violin Plots: + % J. L. Hintze and R. D. Nelson, "Violin plots: a box + % plot-density trace synergism," The American Statistician, vol. + % 52, no. 2, pp. 181-184, 1998. + % + % Violin Properties: + % 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 + % HalfViolin - Whether to do a half violin(left, right side) or + % full. Defaults to full. + % QuartileStyle - Option on how to display quartiles, with a + % boxplot, shadow or none. Defaults to boxplot. + % DataStyle - Defines the style to show the data points. Opts: + % 'scatter', 'histogram' or 'none'. Default is 'scatter'. + % Orientation - Defines the orientation of the violin plot. Opts: + % 'vertical', 'horizontal'. Default is 'vertical'. + % + % + % Violin Children: + % 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 + + 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) + HistogramPlot % histogram of the data + ViolinPlotQ % fill plot of the Quartiles as shadow + end + + properties (Hidden = true) + Orientation % 'horizontal' or 'vertical' + end + + properties (Dependent=true) + ViolinColor % fill color of the violin area and data points + ViolinAlpha % transparency of the violin area and data points + MarkerSize % marker size for the data dots + MedianMarkerSize % 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 + HalfViolin % whether to do a half violin(left, right side) or full + end + + methods + function obj = Violin(data, pos, varargin) + %Violin plots a violin plot of some data at pos + % VIOLIN(DATA, POS) plots a violin at x-position POS for + % a vector of DATA points. + % + % VIOLIN(..., 'PARAM1', val1, 'PARAM2', val2, ...) + % specifies optional name/value pairs: + % 'Width' Width of the violin in axis space. + % Defaults to 0.3 + % '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.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. + % 'MarkerSize' Size of the data points, if shown. + % Defaults to 24 + % 'MedianMarkerSize' Size of the median indicator, if shown. + % Defaults to 36 + % '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 line + % Defaults to true + % 'ShowWhiskers' Whether to show the whiskers + % Defaults to true + % 'HalfViolin' Whether to do a half violin(left, right side) or + % full. Defaults to full. + % 'QuartileStyle' Option on how to display quartiles, with a + % boxplot or as a shadow. Defaults to boxplot. + % 'DataStyle' Defines the style to show the data points. Opts: + % 'scatter', 'histogram' or 'none'. Default is 'Scatter'. + + st = dbstack; % get the calling function for reporting errors + namefun = st.name; + args = obj.checkInputs(data, pos, varargin{:}); + obj.Orientation = args.Orientation; + + if length(data)==1 + data2 = []; + data = data{1}; + + else + data2 = data{2}; + data = data{1}; + end + + if isempty(args.ViolinColor) + Release= strsplit(version('-release'), {'a','b'}); %Check release + if str2num(Release{1})> 2019 || strcmp(version('-release'), '2019b') + C = colororder; + else + C = lines; + end + + if pos > length(C) + C = lines; + end + 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 + [x,y] = obj.plotOrientation(pos, data); + obj.MedianPlot = scatter(x, y, 'filled'); + obj.MedianColor = args.MedianColor; + obj.MedianPlot.MarkerEdgeColor = args.EdgeColor; + return + end + + hold('on'); + + + %% 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 + + %% Plot the data points within the violin area + if length(density) > 1 + [~, unique_idx] = unique(value); + jitterstrength = interp1(value(unique_idx), density(unique_idx)*width, data, 'linear','extrap'); + else % all data is identical: + jitterstrength = density*width; + end + 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 + switch args.HalfViolin % this is more modular + case 'left' + jitter = -1*(rand(size(data))); %left + case 'right' + jitter = 1*(rand(size(data))); %right + case 'full' + jitter = 2*(rand(size(data))-0.5); + end + % Make scatter plot + switch args.DataStyle + case 'scatter' + if ~isempty(data2) + jitter = 1*(rand(size(data))); %right + [x,y] = obj.plotOrientation(pos + jitter.*jitterstrength, data); + obj.ScatterPlot = ... + scatter(x, y, args.MarkerSize, 'filled'); + % 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 = -1*rand(size(data2));% left + [x,y] = obj.plotOrientation(pos + jitter.*jitterstrength,data2); + obj.ScatterPlot2 = ... + scatter(x, y, args.MarkerSize, 'filled'); + else + [x,y] = obj.plotOrientation(pos + jitter.*jitterstrength,data); + obj.ScatterPlot = ... + scatter(x, y, args.MarkerSize, 'filled'); + + end + case 'histogram' + [counts,edges] = histcounts(data, size(unique(data),1)); + switch args.HalfViolin + case 'right' + [x,y] = obj.plotOrientation([pos-((counts')/max(counts))*max(jitterstrength)*2, pos*ones(size(counts,2),1)]',... + [edges(1:end-1)+max(diff(edges))/2; edges(1:end-1)+max(diff(edges))/2]); + case 'left' + [x,y] = obj.plotOrientation([pos*ones(size(counts,2),1), pos+((counts')/max(counts))*max(jitterstrength)*2]', ... + [edges(1:end-1)+max(diff(edges))/2; edges(1:end-1)+max(diff(edges))/2]); + otherwise + fprintf([namefun, ' No histogram/bar plot option available for full violins, as it would look overcrowded.\n']) + end + obj.HistogramPlot = plot(x,y,'-','LineWidth',1, 'Color', 'k'); + case 'none' + end + + %% Plot the violin + halfViol= ones(1, size(density,2)); + if isempty(data2) % if no comparison data + switch args.HalfViolin + case 'right' + [x,y] = obj.plotOrientation([pos+density*width halfViol*pos],... + [value value(end:-1:1)]); + case 'left' + [x,y] = obj.plotOrientation([halfViol*pos pos-density(end:-1:1)*width], ... + [value value(end:-1:1)]); + case 'full' + [x,y] = obj.plotOrientation([pos+density*width pos-density(end:-1:1)*width],... + [value value(end:-1:1)]); + end + obj.ViolinPlot = fill(x,y, [1 1 1]);% plot color will be overwritten later + else + % plot right half of the violin + [x,y] = obj.plotOrientation([pos+density*width pos-density(1)*width],[value value(1)]); + obj.ViolinPlot = fill(x,y,[1 1 1]); + % plot left half of the violin + [x,y] = obj.plotOrientation([pos-densityC(end)*widthC pos-densityC(end:-1:1)*widthC],... + [valueC(end) valueC(end:-1:1)]); + obj.ViolinPlot2 = fill(x,y, [1 1 1]); + end + + %% Plot the quartiles within the violin + quartiles = quantile(data, [0.25, 0.5, 0.75]); + flat= [halfViol*pos halfViol*pos]; + switch args.QuartileStyle + case 'shadow' + switch args.HalfViolin + case 'right' + w = [pos+density*width halfViol*pos]; + h= [value value(end:-1:1)]; + case 'left' + w = [halfViol*pos pos-density(end:-1:1)*width]; + h= [value value(end:-1:1)]; + case 'full' + w = [pos+density*width pos-density(end:-1:1)*width]; + h= [value value(end:-1:1)]; + end + indices = h >= quartiles(1) & h <= quartiles(3); + [x,y] = obj.plotOrientation(w(indices),h(indices)); + obj.ViolinPlotQ = fill(x,y, [1 1 1]); % plot color will be overwritten later + case 'boxplot' + [x,y] = obj.plotOrientation(pos+[-1,1,1,-1]*args.BoxWidth,... + [quartiles(1) quartiles(1) quartiles(3) quartiles(3)]); + obj.BoxPlot = fill(x,y, [1 1 1]); % plot color will be overwritten later + case 'none' + end + + %% Plot the data mean + meanValue = mean(data); + if length(density) > 1 + [~, unique_idx] = unique(value); + meanDensityWidth = interp1(value(unique_idx), density(unique_idx), meanValue, 'linear','extrap')*width; + else % all data is identical: + meanDensityWidth = density*width; + end + if meanDensityWidth lowhisker))); + hiwhisker = quartiles(3) + 1.5*IQR; + hiwhisker = min(hiwhisker, max(data(data < hiwhisker))); + if ~isempty(lowhisker) && ~isempty(hiwhisker) + [x,y] = obj.plotOrientation([pos pos],[lowhisker hiwhisker]); + obj.WhiskerPlot = plot(x,y); + end + + % Median + [x,y] = obj.plotOrientation(pos, quartiles(2)); + obj.MedianPlot = scatter(x,y, args.MedianMarkerSize, [1 1 1], 'filled'); + + % Notches + [x,y] = obj.plotOrientation(pos, quartiles(2)-1.57*IQR/sqrt(length(data))); + obj.NotchPlots = scatter(x,y, [], [1 1 1], 'filled', '^'); + [x,y] = obj.plotOrientation(pos, quartiles(2)+1.57*IQR/sqrt(length(data))); + obj.NotchPlots(2) = scatter(x,y, [], [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.ShowWhiskers = args.ShowWhiskers; + + if not(isempty(args.ViolinColor)) + 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 + ViolinColor{2} = ViolinColor{1}; + end + else + % defaults + if args.scpltBool + ViolinColor{1} = obj.ScatterPlot.CData; + else + ViolinColor{1} = [0 0 0]; + end + ViolinColor{2} = [0 0 0]; + 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}/2; % default unless specified + 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; + obj.ViolinPlotQ.EdgeColor = color; + if ~isempty(obj.ViolinPlot2) + obj.ViolinPlot2.EdgeColor = color; + end + end + end + + function color = get.EdgeColor(obj) + if ~isempty(obj.ViolinPlot) + color = obj.ViolinPlot.EdgeColor; + end + end + + + function set.MedianColor(obj, color) + obj.MedianPlot.MarkerFaceColor = color; + if ~isempty(obj.NotchPlots) + obj.NotchPlots(1).MarkerFaceColor = color; + obj.NotchPlots(2).MarkerFaceColor = color; + end + end + + function color = get.MedianColor(obj) + color = obj.MedianPlot.MarkerFaceColor; + end + + + function set.BoxColor(obj, color) + if ~isempty(obj.BoxPlot) + obj.BoxPlot.FaceColor = color; + obj.BoxPlot.EdgeColor = color; + obj.WhiskerPlot.Color = color; + obj.MedianPlot.MarkerEdgeColor = color; + obj.NotchPlots(1).MarkerFaceColor = color; + obj.NotchPlots(2).MarkerFaceColor = color; + elseif ~isempty(obj.ViolinPlotQ) + obj.WhiskerPlot.Color = color; + obj.MedianPlot.MarkerEdgeColor = color; + obj.NotchPlots(1).MarkerFaceColor = color; + obj.NotchPlots(2).MarkerFaceColor = color; + end + end + + function color = get.BoxColor(obj) + if ~isempty(obj.BoxPlot) + color = obj.BoxPlot.FaceColor; + end + end + + + function set.BoxWidth(obj,width) + if ~isempty(obj.BoxPlot) + pos=mean(obj.BoxPlot.XData); + obj.BoxPlot.XData=pos+[-1,1,1,-1]*width; + end + end + + function width = get.BoxWidth(obj) + width=max(obj.BoxPlot.XData)-min(obj.BoxPlot.XData); + end + + + function set.ViolinColor(obj, 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 + if ~isempty(obj.ViolinPlotQ) + obj.ViolinPlotQ.FaceColor = color{1}; + end + for idx = 1: size(obj.HistogramPlot,1) + obj.HistogramPlot(idx).Color = color{1}; + end + end + + function color = get.ViolinColor(obj) + color{1} = obj.ViolinPlot.FaceColor; + if ~isempty(obj.ViolinPlot2) + color{2} = obj.ViolinPlot2.FaceColor; + end + end + + + function set.ViolinAlpha(obj, alpha) + obj.ViolinPlotQ.FaceAlpha = .65; + obj.ViolinPlot.FaceAlpha = alpha{1}; + obj.ScatterPlot.MarkerFaceAlpha = 1; + if ~isempty(obj.ViolinPlot2) + obj.ViolinPlot2.FaceAlpha = alpha{2}; + obj.ScatterPlot2.MarkerFaceAlpha = 1; + end + end + + function alpha = get.ViolinAlpha(obj) + alpha{1} = obj.ViolinPlot.FaceAlpha; + if ~isempty(obj.ViolinPlot2) + alpha{2} = obj.ViolinPlot2.FaceAlpha; + end + end + + + function set.ShowData(obj, yesno) + if yesno + obj.ScatterPlot.Visible = 'on'; + for idx = 1: size(obj.HistogramPlot,1) + obj.HistogramPlot(idx).Visible = 'on'; + end + else + obj.ScatterPlot.Visible = 'off'; + for idx = 1: size(obj.HistogramPlot,1) + obj.HistogramPlot(idx).Visible = 'off'; + end + end + if ~isempty(obj.ScatterPlot2) + obj.ScatterPlot2.Visible = obj.ScatterPlot.Visible; + end + + end + + function yesno = get.ShowData(obj) + if ~isempty(obj.ScatterPlot) + yesno = strcmp(obj.ScatterPlot.Visible, 'on'); + end + end + + + function set.ShowNotches(obj, yesno) + if ~isempty(obj.NotchPlots) + if yesno + obj.NotchPlots(1).Visible = 'on'; + obj.NotchPlots(2).Visible = 'on'; + else + obj.NotchPlots(1).Visible = 'off'; + obj.NotchPlots(2).Visible = 'off'; + end + end + end + + function yesno = get.ShowNotches(obj) + if ~isempty(obj.NotchPlots) + yesno = strcmp(obj.NotchPlots(1).Visible, 'on'); + end + end + + + function set.ShowMean(obj, yesno) + if ~isempty(obj.MeanPlot) + if yesno + obj.MeanPlot.Visible = 'on'; + else + obj.MeanPlot.Visible = 'off'; + end + end + end + + function yesno = get.ShowMean(obj) + 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.ShowWhiskers(obj, yesno) + if ~isempty(obj.WhiskerPlot) + if yesno + obj.WhiskerPlot.Visible = 'on'; + else + obj.WhiskerPlot.Visible = 'off'; + end + end + end + + function yesno = get.ShowWhiskers(obj) + if ~isempty(obj.WhiskerPlot) + yesno = strcmp(obj.WhiskerPlot.Visible, 'on'); + end + end + + end + + methods (Access=private) + function results = checkInputs(~, data, pos, varargin) + isscalarnumber = @(x) (isnumeric(x) & isscalar(x)); + p = inputParser(); + 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', [], @(x)iscolor(vertcat(x{:}))); + p.addParameter('MarkerSize', 24, @isnumeric); + p.addParameter('MedianMarkerSize', 36, @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,0.15}, @(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('ShowWhiskers', true, isscalarlogical); + validSides={'full', 'right', 'left'}; + checkSide = @(x) any(validatestring(x, validSides)); + p.addParameter('HalfViolin', 'full', checkSide); + validQuartileStyles={'boxplot', 'shadow', 'none'}; + checkQuartile = @(x)any(validatestring(x, validQuartileStyles)); + p.addParameter('QuartileStyle', 'boxplot', checkQuartile); + validDataStyles = {'scatter', 'histogram', 'none'}; + checkStyle = @(x)any(validatestring(x, validDataStyles)); + p.addParameter('DataStyle', 'scatter', checkStyle); + p.addParameter('Orientation', 'vertical', @(x) ismember(x, {'vertical', 'horizontal'})); + + p.parse(data, pos, varargin{:}); + results = p.Results; + end + + function [x,y] = plotOrientation(obj, x, y) + if strcmp(obj.Orientation,'horizontal') + tmp = x; + x = y; + y = tmp; + end + 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; + value= mean(value); + end + + width = width/max(density); + end + end +end + diff --git a/violinplot.m b/violinplot.m index 63af839..9525cab 100644 --- a/violinplot.m +++ b/violinplot.m @@ -69,6 +69,8 @@ % Defaults to true % 'GroupOrder' Cell of category names in order to be plotted. % Defaults to alphabetical ordering +% 'Orientation' Orientation of the violin plot. +% Defaults to 'vertical'. % Copyright (c) 2016, Bastian Bechtold % This code is released under the terms of the BSD 3-clause license @@ -138,7 +140,11 @@ thisData = data.(catnames{n}); violins(n) = Violin({thisData}, n, varargin{:}); end - set(gca, 'XTick', 1:length(catnames), 'XTickLabels', catnames); + if strcmp(violins(1).Orientation,'vertical') + set(gca, 'XTick', 1:length(catnames), 'XTickLabels', catnames); + else + set(gca, 'YTick', 1:length(catnames), 'YTickLabels', catnames); + end set(gca,'Box','on'); return elseif iscell(data) && length(data(:))==2 % cell input @@ -164,7 +170,11 @@ thisData = data(cats == thisCat); violins(n) = Violin({thisData}, n, varargin{:}); end - set(gca, 'XTick', 1:length(catnames), 'XTickLabels', catnames_labels); + if strcmp(violins(1).Orientation,'vertical') + set(gca, 'XTick', 1:length(catnames), 'XTickLabels', catnames); + else + set(gca, 'YTick', 1:length(catnames), 'YTickLabels', catnames); + end set(gca,'Box','on'); return else @@ -175,16 +185,28 @@ % 1D data, no categories if not(hascategories) && isvector(data{1}) violins = Violin(data, 1, varargin{:}); - set(gca, 'XTick', 1); + if strcmp(violins(1).Orientation,'vertical') + set(gca, 'XTick', 1); + else + set(gca, 'yTick', 1); + end % 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{1}, 2)); + if strcmp(violins(1).Orientation,'vertical') + set(gca, 'XTick', 1:size(data{1}, 2)); + else + set(gca, 'YTick', 1:size(data{1}, 2)); + end if hascategories && length(cats) == size(data{1}, 2) - set(gca, 'XTickLabels', cats); + if strcmp(violins(1).Orientation,'vertical') + set(gca, 'XTickLabels', cats); + else + set(gca, 'YTickLabels', cats); + end end end From 38de4dd1a16c956445879c3e033cb944da19aea5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20H=C3=A4fele?= Date: Thu, 4 Jul 2024 14:17:05 +0200 Subject: [PATCH 2/7] Orientation property is set to read only. Adjustments to code readability and maintainability --- Violin.m | 57 ++++++++++++++++++++++++++++++-------------------------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/Violin.m b/Violin.m index cb20906..95878d8 100644 --- a/Violin.m +++ b/Violin.m @@ -87,7 +87,7 @@ ViolinPlotQ % fill plot of the Quartiles as shadow end - properties (Hidden = true) + properties (SetAccess=protected, GetAccess=public) Orientation % 'horizontal' or 'vertical' end @@ -193,7 +193,7 @@ data = data(not(isnan(data))); data2 = data2(not(isnan(data2))); if numel(data) == 1 - [x,y] = obj.plotOrientation(pos, data); + [x,y] = obj.setAxisOrientation(pos, data); obj.MedianPlot = scatter(x, y, 'filled'); obj.MedianColor = args.MedianColor; obj.MedianPlot.MarkerEdgeColor = args.EdgeColor; @@ -237,7 +237,7 @@ case 'scatter' if ~isempty(data2) jitter = 1*(rand(size(data))); %right - [x,y] = obj.plotOrientation(pos + jitter.*jitterstrength, data); + [x,y] = obj.setAxisOrientation(pos + jitter.*jitterstrength, data); obj.ScatterPlot = ... scatter(x, y, args.MarkerSize, 'filled'); % plot the data points within the violin area @@ -247,11 +247,11 @@ jitterstrength = densityC*widthC; end jitter = -1*rand(size(data2));% left - [x,y] = obj.plotOrientation(pos + jitter.*jitterstrength,data2); + [x,y] = obj.setAxisOrientation(pos + jitter.*jitterstrength,data2); obj.ScatterPlot2 = ... scatter(x, y, args.MarkerSize, 'filled'); else - [x,y] = obj.plotOrientation(pos + jitter.*jitterstrength,data); + [x,y] = obj.setAxisOrientation(pos + jitter.*jitterstrength,data); obj.ScatterPlot = ... scatter(x, y, args.MarkerSize, 'filled'); @@ -260,15 +260,16 @@ [counts,edges] = histcounts(data, size(unique(data),1)); switch args.HalfViolin case 'right' - [x,y] = obj.plotOrientation([pos-((counts')/max(counts))*max(jitterstrength)*2, pos*ones(size(counts,2),1)]',... - [edges(1:end-1)+max(diff(edges))/2; edges(1:end-1)+max(diff(edges))/2]); + [x,y] = obj.setAxisOrientation([pos-((counts')/max(counts))*max(jitterstrength)*2, pos*ones(size(counts,2),1)]',... + [edges(1:end-1)+max(diff(edges))/2; edges(1:end-1)+max(diff(edges))/2]); + obj.HistogramPlot = plot(x,y,'-','LineWidth',1, 'Color', 'k'); case 'left' - [x,y] = obj.plotOrientation([pos*ones(size(counts,2),1), pos+((counts')/max(counts))*max(jitterstrength)*2]', ... + [x,y] = obj.setAxisOrientation([pos*ones(size(counts,2),1), pos+((counts')/max(counts))*max(jitterstrength)*2]', ... [edges(1:end-1)+max(diff(edges))/2; edges(1:end-1)+max(diff(edges))/2]); + obj.HistogramPlot = plot(x,y,'-','LineWidth',1, 'Color', 'k'); otherwise fprintf([namefun, ' No histogram/bar plot option available for full violins, as it would look overcrowded.\n']) end - obj.HistogramPlot = plot(x,y,'-','LineWidth',1, 'Color', 'k'); case 'none' end @@ -277,22 +278,24 @@ if isempty(data2) % if no comparison data switch args.HalfViolin case 'right' - [x,y] = obj.plotOrientation([pos+density*width halfViol*pos],... + [x,y] = obj.setAxisOrientation([pos+density*width halfViol*pos],... [value value(end:-1:1)]); + obj.ViolinPlot = fill(x,y, [1 1 1]);% plot color will be overwritten later case 'left' - [x,y] = obj.plotOrientation([halfViol*pos pos-density(end:-1:1)*width], ... + [x,y] = obj.setAxisOrientation([halfViol*pos pos-density(end:-1:1)*width], ... [value value(end:-1:1)]); + obj.ViolinPlot = fill(x,y, [1 1 1]);% plot color will be overwritten later case 'full' - [x,y] = obj.plotOrientation([pos+density*width pos-density(end:-1:1)*width],... + [x,y] = obj.setAxisOrientation([pos+density*width pos-density(end:-1:1)*width],... [value value(end:-1:1)]); + obj.ViolinPlot = fill(x,y, [1 1 1]);% plot color will be overwritten later end - obj.ViolinPlot = fill(x,y, [1 1 1]);% plot color will be overwritten later else % plot right half of the violin - [x,y] = obj.plotOrientation([pos+density*width pos-density(1)*width],[value value(1)]); + [x,y] = obj.setAxisOrientation([pos+density*width pos-density(1)*width],[value value(1)]); obj.ViolinPlot = fill(x,y,[1 1 1]); % plot left half of the violin - [x,y] = obj.plotOrientation([pos-densityC(end)*widthC pos-densityC(end:-1:1)*widthC],... + [x,y] = obj.setAxisOrientation([pos-densityC(end)*widthC pos-densityC(end:-1:1)*widthC],... [valueC(end) valueC(end:-1:1)]); obj.ViolinPlot2 = fill(x,y, [1 1 1]); end @@ -314,10 +317,10 @@ h= [value value(end:-1:1)]; end indices = h >= quartiles(1) & h <= quartiles(3); - [x,y] = obj.plotOrientation(w(indices),h(indices)); + [x,y] = obj.setAxisOrientation(w(indices),h(indices)); obj.ViolinPlotQ = fill(x,y, [1 1 1]); % plot color will be overwritten later case 'boxplot' - [x,y] = obj.plotOrientation(pos+[-1,1,1,-1]*args.BoxWidth,... + [x,y] = obj.setAxisOrientation(pos+[-1,1,1,-1]*args.BoxWidth,... [quartiles(1) quartiles(1) quartiles(3) quartiles(3)]); obj.BoxPlot = fill(x,y, [1 1 1]); % plot color will be overwritten later case 'none' @@ -336,13 +339,15 @@ end switch args.HalfViolin case 'right' - [x,y] = obj.plotOrientation(pos+[0,1].*meanDensityWidth,[meanValue, meanValue]); + [x,y] = obj.setAxisOrientation(pos+[0,1].*meanDensityWidth,[meanValue, meanValue]); + obj.MeanPlot = plot(x,y); case 'left' - [x,y] = obj.plotOrientation(pos+[-1,0].*meanDensityWidth,[meanValue, meanValue]); + [x,y] = obj.setAxisOrientation(pos+[-1,0].*meanDensityWidth,[meanValue, meanValue]); + obj.MeanPlot = plot(x,y); case 'full' - [x,y] = obj.plotOrientation(pos+[-1,1].*meanDensityWidth,[meanValue, meanValue]); + [x,y] = obj.setAxisOrientation(pos+[-1,1].*meanDensityWidth,[meanValue, meanValue]); + obj.MeanPlot = plot(x,y); end - obj.MeanPlot = plot(x,y); obj.MeanPlot.LineWidth = 1; %% Plot the median, notch, and whiskers @@ -352,18 +357,18 @@ hiwhisker = quartiles(3) + 1.5*IQR; hiwhisker = min(hiwhisker, max(data(data < hiwhisker))); if ~isempty(lowhisker) && ~isempty(hiwhisker) - [x,y] = obj.plotOrientation([pos pos],[lowhisker hiwhisker]); + [x,y] = obj.setAxisOrientation([pos pos],[lowhisker hiwhisker]); obj.WhiskerPlot = plot(x,y); end % Median - [x,y] = obj.plotOrientation(pos, quartiles(2)); + [x,y] = obj.setAxisOrientation(pos, quartiles(2)); obj.MedianPlot = scatter(x,y, args.MedianMarkerSize, [1 1 1], 'filled'); % Notches - [x,y] = obj.plotOrientation(pos, quartiles(2)-1.57*IQR/sqrt(length(data))); + [x,y] = obj.setAxisOrientation(pos, quartiles(2)-1.57*IQR/sqrt(length(data))); obj.NotchPlots = scatter(x,y, [], [1 1 1], 'filled', '^'); - [x,y] = obj.plotOrientation(pos, quartiles(2)+1.57*IQR/sqrt(length(data))); + [x,y] = obj.setAxisOrientation(pos, quartiles(2)+1.57*IQR/sqrt(length(data))); obj.NotchPlots(2) = scatter(x,y, [], [1 1 1], 'filled', 'v'); %% Set graphical preferences @@ -691,7 +696,7 @@ results = p.Results; end - function [x,y] = plotOrientation(obj, x, y) + function [x,y] = setAxisOrientation(obj, x, y) if strcmp(obj.Orientation,'horizontal') tmp = x; x = y; From 210e14c15c361f830baa916416a624e95858fc9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20H=C3=A4fele?= Date: Thu, 4 Jul 2024 15:02:05 +0200 Subject: [PATCH 3/7] Orientation property is set to read only. Adjustments to code readability and maintainability --- Violin.m | 761 ++----------------------------------------------------- 1 file changed, 23 insertions(+), 738 deletions(-) diff --git a/Violin.m b/Violin.m index 71e1f79..f0ee3f0 100644 --- a/Violin.m +++ b/Violin.m @@ -1,4 +1,3 @@ -<<<<<<< HEAD classdef Violin < handle % Violin creates violin plots for some data % A violin plot is an easy to read substitute for a box plot @@ -194,7 +193,7 @@ data = data(not(isnan(data))); data2 = data2(not(isnan(data2))); if numel(data) == 1 - [x,y] = obj.setAxisOrientation(pos, data); + [x,y] = obj.swapOrientationMaybe(pos, data); obj.MedianPlot = scatter(x, y, 'filled'); obj.MedianColor = args.MedianColor; obj.MedianPlot.MarkerEdgeColor = args.EdgeColor; @@ -238,7 +237,7 @@ case 'scatter' if ~isempty(data2) jitter = 1*(rand(size(data))); %right - [x,y] = obj.setAxisOrientation(pos + jitter.*jitterstrength, data); + [x,y] = obj.swapOrientationMaybe(pos + jitter.*jitterstrength, data); obj.ScatterPlot = ... scatter(x, y, args.MarkerSize, 'filled'); % plot the data points within the violin area @@ -248,11 +247,11 @@ jitterstrength = densityC*widthC; end jitter = -1*rand(size(data2));% left - [x,y] = obj.setAxisOrientation(pos + jitter.*jitterstrength,data2); + [x,y] = obj.swapOrientationMaybe(pos + jitter.*jitterstrength,data2); obj.ScatterPlot2 = ... scatter(x, y, args.MarkerSize, 'filled'); else - [x,y] = obj.setAxisOrientation(pos + jitter.*jitterstrength,data); + [x,y] = obj.swapOrientationMaybe(pos + jitter.*jitterstrength,data); obj.ScatterPlot = ... scatter(x, y, args.MarkerSize, 'filled'); @@ -261,11 +260,11 @@ [counts,edges] = histcounts(data, size(unique(data),1)); switch args.HalfViolin case 'right' - [x,y] = obj.setAxisOrientation([pos-((counts')/max(counts))*max(jitterstrength)*2, pos*ones(size(counts,2),1)]',... + [x,y] = obj.swapOrientationMaybe([pos-((counts')/max(counts))*max(jitterstrength)*2, pos*ones(size(counts,2),1)]',... [edges(1:end-1)+max(diff(edges))/2; edges(1:end-1)+max(diff(edges))/2]); obj.HistogramPlot = plot(x,y,'-','LineWidth',1, 'Color', 'k'); case 'left' - [x,y] = obj.setAxisOrientation([pos*ones(size(counts,2),1), pos+((counts')/max(counts))*max(jitterstrength)*2]', ... + [x,y] = obj.swapOrientationMaybe([pos*ones(size(counts,2),1), pos+((counts')/max(counts))*max(jitterstrength)*2]', ... [edges(1:end-1)+max(diff(edges))/2; edges(1:end-1)+max(diff(edges))/2]); obj.HistogramPlot = plot(x,y,'-','LineWidth',1, 'Color', 'k'); otherwise @@ -279,24 +278,24 @@ if isempty(data2) % if no comparison data switch args.HalfViolin case 'right' - [x,y] = obj.setAxisOrientation([pos+density*width halfViol*pos],... + [x,y] = obj.swapOrientationMaybe([pos+density*width halfViol*pos],... [value value(end:-1:1)]); obj.ViolinPlot = fill(x,y, [1 1 1]);% plot color will be overwritten later case 'left' - [x,y] = obj.setAxisOrientation([halfViol*pos pos-density(end:-1:1)*width], ... + [x,y] = obj.swapOrientationMaybe([halfViol*pos pos-density(end:-1:1)*width], ... [value value(end:-1:1)]); obj.ViolinPlot = fill(x,y, [1 1 1]);% plot color will be overwritten later case 'full' - [x,y] = obj.setAxisOrientation([pos+density*width pos-density(end:-1:1)*width],... + [x,y] = obj.swapOrientationMaybe([pos+density*width pos-density(end:-1:1)*width],... [value value(end:-1:1)]); obj.ViolinPlot = fill(x,y, [1 1 1]);% plot color will be overwritten later end else % plot right half of the violin - [x,y] = obj.setAxisOrientation([pos+density*width pos-density(1)*width],[value value(1)]); + [x,y] = obj.swapOrientationMaybe([pos+density*width pos-density(1)*width],[value value(1)]); obj.ViolinPlot = fill(x,y,[1 1 1]); % plot left half of the violin - [x,y] = obj.setAxisOrientation([pos-densityC(end)*widthC pos-densityC(end:-1:1)*widthC],... + [x,y] = obj.swapOrientationMaybe([pos-densityC(end)*widthC pos-densityC(end:-1:1)*widthC],... [valueC(end) valueC(end:-1:1)]); obj.ViolinPlot2 = fill(x,y, [1 1 1]); end @@ -318,10 +317,10 @@ h= [value value(end:-1:1)]; end indices = h >= quartiles(1) & h <= quartiles(3); - [x,y] = obj.setAxisOrientation(w(indices),h(indices)); + [x,y] = obj.swapOrientationMaybe(w(indices),h(indices)); obj.ViolinPlotQ = fill(x,y, [1 1 1]); % plot color will be overwritten later case 'boxplot' - [x,y] = obj.setAxisOrientation(pos+[-1,1,1,-1]*args.BoxWidth,... + [x,y] = obj.swapOrientationMaybe(pos+[-1,1,1,-1]*args.BoxWidth,... [quartiles(1) quartiles(1) quartiles(3) quartiles(3)]); obj.BoxPlot = fill(x,y, [1 1 1]); % plot color will be overwritten later case 'none' @@ -340,13 +339,13 @@ end switch args.HalfViolin case 'right' - [x,y] = obj.setAxisOrientation(pos+[0,1].*meanDensityWidth,[meanValue, meanValue]); + [x,y] = obj.swapOrientationMaybe(pos+[0,1].*meanDensityWidth,[meanValue, meanValue]); obj.MeanPlot = plot(x,y); case 'left' - [x,y] = obj.setAxisOrientation(pos+[-1,0].*meanDensityWidth,[meanValue, meanValue]); + [x,y] = obj.swapOrientationMaybe(pos+[-1,0].*meanDensityWidth,[meanValue, meanValue]); obj.MeanPlot = plot(x,y); case 'full' - [x,y] = obj.setAxisOrientation(pos+[-1,1].*meanDensityWidth,[meanValue, meanValue]); + [x,y] = obj.swapOrientationMaybe(pos+[-1,1].*meanDensityWidth,[meanValue, meanValue]); obj.MeanPlot = plot(x,y); end obj.MeanPlot.LineWidth = 1; @@ -358,18 +357,18 @@ hiwhisker = quartiles(3) + 1.5*IQR; hiwhisker = min(hiwhisker, max(data(data < hiwhisker))); if ~isempty(lowhisker) && ~isempty(hiwhisker) - [x,y] = obj.setAxisOrientation([pos pos],[lowhisker hiwhisker]); + [x,y] = obj.swapOrientationMaybe([pos pos],[lowhisker hiwhisker]); obj.WhiskerPlot = plot(x,y); end % Median - [x,y] = obj.setAxisOrientation(pos, quartiles(2)); + [x,y] = obj.swapOrientationMaybe(pos, quartiles(2)); obj.MedianPlot = scatter(x,y, args.MedianMarkerSize, [1 1 1], 'filled'); % Notches - [x,y] = obj.setAxisOrientation(pos, quartiles(2)-1.57*IQR/sqrt(length(data))); + [x,y] = obj.swapOrientationMaybe(pos, quartiles(2)-1.57*IQR/sqrt(length(data))); obj.NotchPlots = scatter(x,y, [], [1 1 1], 'filled', '^'); - [x,y] = obj.setAxisOrientation(pos, quartiles(2)+1.57*IQR/sqrt(length(data))); + [x,y] = obj.swapOrientationMaybe(pos, quartiles(2)+1.57*IQR/sqrt(length(data))); obj.NotchPlots(2) = scatter(x,y, [], [1 1 1], 'filled', 'v'); %% Set graphical preferences @@ -434,6 +433,8 @@ obj.ViolinAlpha = ViolinAlpha; + set(obj.ViolinPlot, 'Marker', 'none', 'LineStyle', '-'); + set(obj.ViolinPlot2, 'Marker', 'none', 'LineStyle', '-'); end %% SET METHODS @@ -697,7 +698,7 @@ results = p.Results; end - function [x,y] = setAxisOrientation(obj, x, y) + function [x,y] = swapOrientationMaybe(obj, x, y) if strcmp(obj.Orientation,'horizontal') tmp = x; x = y; @@ -730,719 +731,3 @@ end end -======= -classdef Violin < handle - % Violin creates violin plots for some data - % 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, the - % X-axis position of the violin plot, and the categories. - % - % Use violinplot for a - % boxplot-like wrapper for - % interactive plotting. - % - % See for more information on Violin Plots: - % J. L. Hintze and R. D. Nelson, "Violin plots: a box - % plot-density trace synergism," The American Statistician, vol. - % 52, no. 2, pp. 181-184, 1998. - % - % Violin Properties: - % 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 - % HalfViolin - Whether to do a half violin(left, right side) or - % full. Defaults to full. - % QuartileStyle - Option on how to display quartiles, with a - % boxplot, shadow or none. Defaults to boxplot. - % DataStyle - Defines the style to show the data points. Opts: - % 'scatter', 'histogram' or 'none'. Default is 'scatter'. - % - % - % Violin Children: - % 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 - - 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) - HistogramPlot % histogram of the data - ViolinPlotQ % fill plot of the Quartiles as shadow - end - - properties (Dependent=true) - ViolinColor % fill color of the violin area and data points - ViolinAlpha % transparency of the violin area and data points - MarkerSize % marker size for the data dots - MedianMarkerSize % 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 - HalfViolin % whether to do a half violin(left, right side) or full - end - - methods - function obj = Violin(data, pos, varargin) - %Violin plots a violin plot of some data at pos - % VIOLIN(DATA, POS) plots a violin at x-position POS for - % a vector of DATA points. - % - % VIOLIN(..., 'PARAM1', val1, 'PARAM2', val2, ...) - % specifies optional name/value pairs: - % 'Width' Width of the violin in axis space. - % Defaults to 0.3 - % '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.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. - % 'MarkerSize' Size of the data points, if shown. - % Defaults to 24 - % 'MedianMarkerSize' Size of the median indicator, if shown. - % Defaults to 36 - % '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 line - % Defaults to true - % 'ShowWhiskers' Whether to show the whiskers - % Defaults to true - % 'HalfViolin' Whether to do a half violin(left, right side) or - % full. Defaults to full. - % 'QuartileStyle' Option on how to display quartiles, with a - % boxplot or as a shadow. Defaults to boxplot. - % 'DataStyle' Defines the style to show the data points. Opts: - % 'scatter', 'histogram' or 'none'. Default is 'Scatter'. - - st = dbstack; % get the calling function for reporting errors - namefun = st.name; - args = obj.checkInputs(data, pos, varargin{:}); - - if length(data)==1 - data2 = []; - data = data{1}; - - else - data2 = data{2}; - data = data{1}; - end - - if isempty(args.ViolinColor) - Release= strsplit(version('-release'), {'a','b'}); %Check release - if str2num(Release{1})> 2019 || strcmp(version('-release'), '2019b') - C = colororder; - else - C = lines; - end - - if pos > length(C) - C = lines; - end - 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 - 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 - [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 - - %% Plot the data points within the violin area - if length(density) > 1 - [~, unique_idx] = unique(value); - jitterstrength = interp1(value(unique_idx), density(unique_idx)*width, data, 'linear','extrap'); - else % all data is identical: - jitterstrength = density*width; - end - 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 - switch args.HalfViolin % this is more modular - case 'left' - jitter = -1*(rand(size(data))); %left - case 'right' - jitter = 1*(rand(size(data))); %right - case 'full' - jitter = 2*(rand(size(data))-0.5); - end - % Make scatter plot - switch args.DataStyle - case 'scatter' - if ~isempty(data2) - jitter = 1*(rand(size(data))); %right - obj.ScatterPlot = ... - scatter(pos + jitter.*jitterstrength, data, args.MarkerSize, 'filled'); - % 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 = -1*rand(size(data2));% left - obj.ScatterPlot2 = ... - scatter(pos + jitter.*jitterstrength, data2, args.MarkerSize, 'filled'); - else - obj.ScatterPlot = ... - scatter(pos + jitter.*jitterstrength, data, args.MarkerSize, 'filled'); - - end - case 'histogram' - [counts,edges] = histcounts(data, size(unique(data),1)); - switch args.HalfViolin - case 'right' - obj.HistogramPlot= plot([pos-((counts')/max(counts))*max(jitterstrength)*2, pos*ones(size(counts,2),1)]',... - [edges(1:end-1)+max(diff(edges))/2; edges(1:end-1)+max(diff(edges))/2],'-','LineWidth',1, 'Color', 'k'); - case 'left' - obj.HistogramPlot= plot([pos*ones(size(counts,2),1), pos+((counts')/max(counts))*max(jitterstrength)*2]',... - [edges(1:end-1)+max(diff(edges))/2; edges(1:end-1)+max(diff(edges))/2],'-','LineWidth',1, 'Color', 'k'); - otherwise - fprintf([namefun, ' No histogram/bar plot option available for full violins, as it would look overcrowded.\n']) - end - case 'none' - end - - %% Plot the violin - halfViol= ones(1, size(density,2)); - if isempty(data2) % if no comparison data - switch args.HalfViolin - case 'right' - obj.ViolinPlot = ... % plot color will be overwritten later - fill([pos+density*width halfViol*pos], ... - [value value(end:-1:1)], [1 1 1],'LineStyle','-'); - case 'left' - obj.ViolinPlot = ... % plot color will be overwritten later - fill([halfViol*pos pos-density(end:-1:1)*width], ... - [value value(end:-1:1)], [1 1 1],'LineStyle','-'); - case 'full' - 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],'LineStyle','-'); - end - else - % plot right half of the violin - obj.ViolinPlot = ... - fill([pos+density*width pos-density(1)*width], ... - [value value(1)], [1 1 1],'LineStyle','-'); - % 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],'LineStyle','-'); - end - - %% Plot the quartiles within the violin - quartiles = quantile(data, [0.25, 0.5, 0.75]); - flat= [halfViol*pos halfViol*pos]; - switch args.QuartileStyle - case 'shadow' - switch args.HalfViolin - case 'right' - w = [pos+density*width halfViol*pos]; - h= [value value(end:-1:1)]; - case 'left' - w = [halfViol*pos pos-density(end:-1:1)*width]; - h= [value value(end:-1:1)]; - case 'full' - w = [pos+density*width pos-density(end:-1:1)*width]; - h= [value value(end:-1:1)]; - end - indices = h >= quartiles(1) & h <= quartiles(3); - obj.ViolinPlotQ = ... % plot color will be overwritten later - fill(w(indices), ... - h(indices),'Marker','none', [1 1 1],'LineStyle','-'); - case 'boxplot' - 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],'Marker','none','LineStyle','-'); - case 'none' - end - - %% Plot the data mean - meanValue = mean(data); - if length(density) > 1 - [~, unique_idx] = unique(value); - meanDensityWidth = interp1(value(unique_idx), density(unique_idx), meanValue, 'linear','extrap')*width; - else % all data is identical: - meanDensityWidth = density*width; - end - if meanDensityWidth lowhisker))); - hiwhisker = quartiles(3) + 1.5*IQR; - hiwhisker = min(hiwhisker, max(data(data < hiwhisker))); - if ~isempty(lowhisker) && ~isempty(hiwhisker) - obj.WhiskerPlot = plot([pos pos], [lowhisker hiwhisker],... - 'Marker','none','LineStyle','-'); - end - - % Median - obj.MedianPlot = scatter(pos, quartiles(2), args.MedianMarkerSize, [1 1 1], 'filled'); - - % Notches - obj.NotchPlots = ... - scatter(pos, quartiles(2)-1.57*IQR/sqrt(length(data)), ... - [], [1 1 1], 'filled', '^'); - 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.ShowWhiskers = args.ShowWhiskers; - - if not(isempty(args.ViolinColor)) - 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 - ViolinColor{2} = ViolinColor{1}; - end - else - % defaults - if args.scpltBool - ViolinColor{1} = obj.ScatterPlot.CData; - else - ViolinColor{1} = [0 0 0]; - end - ViolinColor{2} = [0 0 0]; - 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}/2; % default unless specified - end - else - % default - ViolinAlpha = {1,1}; - end - obj.ViolinAlpha = ViolinAlpha; - - set(obj.ViolinPlot, 'Marker', 'none', 'LineStyle', '-'); - set(obj.ViolinPlot2, 'Marker', 'none', 'LineStyle', '-'); - end - - %% SET METHODS - function set.EdgeColor(obj, color) - if ~isempty(obj.ViolinPlot) - obj.ViolinPlot.EdgeColor = color; - obj.ViolinPlotQ.EdgeColor = color; - if ~isempty(obj.ViolinPlot2) - obj.ViolinPlot2.EdgeColor = color; - end - end - end - - function color = get.EdgeColor(obj) - if ~isempty(obj.ViolinPlot) - color = obj.ViolinPlot.EdgeColor; - end - end - - - function set.MedianColor(obj, color) - obj.MedianPlot.MarkerFaceColor = color; - if ~isempty(obj.NotchPlots) - obj.NotchPlots(1).MarkerFaceColor = color; - obj.NotchPlots(2).MarkerFaceColor = color; - end - end - - function color = get.MedianColor(obj) - color = obj.MedianPlot.MarkerFaceColor; - end - - - function set.BoxColor(obj, color) - if ~isempty(obj.BoxPlot) - obj.BoxPlot.FaceColor = color; - obj.BoxPlot.EdgeColor = color; - obj.WhiskerPlot.Color = color; - obj.MedianPlot.MarkerEdgeColor = color; - obj.NotchPlots(1).MarkerFaceColor = color; - obj.NotchPlots(2).MarkerFaceColor = color; - elseif ~isempty(obj.ViolinPlotQ) - obj.WhiskerPlot.Color = color; - obj.MedianPlot.MarkerEdgeColor = color; - obj.NotchPlots(1).MarkerFaceColor = color; - obj.NotchPlots(2).MarkerFaceColor = color; - end - end - - function color = get.BoxColor(obj) - if ~isempty(obj.BoxPlot) - color = obj.BoxPlot.FaceColor; - end - end - - - function set.BoxWidth(obj,width) - if ~isempty(obj.BoxPlot) - pos=mean(obj.BoxPlot.XData); - obj.BoxPlot.XData=pos+[-1,1,1,-1]*width; - end - end - - function width = get.BoxWidth(obj) - width=max(obj.BoxPlot.XData)-min(obj.BoxPlot.XData); - end - - - function set.ViolinColor(obj, 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 - if ~isempty(obj.ViolinPlotQ) - obj.ViolinPlotQ.FaceColor = color{1}; - end - for idx = 1: size(obj.HistogramPlot,1) - obj.HistogramPlot(idx).Color = color{1}; - end - end - - function color = get.ViolinColor(obj) - color{1} = obj.ViolinPlot.FaceColor; - if ~isempty(obj.ViolinPlot2) - color{2} = obj.ViolinPlot2.FaceColor; - end - end - - - function set.ViolinAlpha(obj, alpha) - obj.ViolinPlotQ.FaceAlpha = .65; - obj.ViolinPlot.FaceAlpha = alpha{1}; - obj.ScatterPlot.MarkerFaceAlpha = 1; - if ~isempty(obj.ViolinPlot2) - obj.ViolinPlot2.FaceAlpha = alpha{2}; - obj.ScatterPlot2.MarkerFaceAlpha = 1; - end - end - - function alpha = get.ViolinAlpha(obj) - alpha{1} = obj.ViolinPlot.FaceAlpha; - if ~isempty(obj.ViolinPlot2) - alpha{2} = obj.ViolinPlot2.FaceAlpha; - end - end - - - function set.ShowData(obj, yesno) - if yesno - obj.ScatterPlot.Visible = 'on'; - for idx = 1: size(obj.HistogramPlot,1) - obj.HistogramPlot(idx).Visible = 'on'; - end - else - obj.ScatterPlot.Visible = 'off'; - for idx = 1: size(obj.HistogramPlot,1) - obj.HistogramPlot(idx).Visible = 'off'; - end - end - if ~isempty(obj.ScatterPlot2) - obj.ScatterPlot2.Visible = obj.ScatterPlot.Visible; - end - - end - - function yesno = get.ShowData(obj) - if ~isempty(obj.ScatterPlot) - yesno = strcmp(obj.ScatterPlot.Visible, 'on'); - end - end - - - function set.ShowNotches(obj, yesno) - if ~isempty(obj.NotchPlots) - if yesno - obj.NotchPlots(1).Visible = 'on'; - obj.NotchPlots(2).Visible = 'on'; - else - obj.NotchPlots(1).Visible = 'off'; - obj.NotchPlots(2).Visible = 'off'; - end - end - end - - function yesno = get.ShowNotches(obj) - if ~isempty(obj.NotchPlots) - yesno = strcmp(obj.NotchPlots(1).Visible, 'on'); - end - end - - - function set.ShowMean(obj, yesno) - if ~isempty(obj.MeanPlot) - if yesno - obj.MeanPlot.Visible = 'on'; - else - obj.MeanPlot.Visible = 'off'; - end - end - end - - function yesno = get.ShowMean(obj) - 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.ShowWhiskers(obj, yesno) - if ~isempty(obj.WhiskerPlot) - if yesno - obj.WhiskerPlot.Visible = 'on'; - else - obj.WhiskerPlot.Visible = 'off'; - end - end - end - - function yesno = get.ShowWhiskers(obj) - if ~isempty(obj.WhiskerPlot) - yesno = strcmp(obj.WhiskerPlot.Visible, 'on'); - end - end - - end - - methods (Access=private) - function results = checkInputs(~, data, pos, varargin) - isscalarnumber = @(x) (isnumeric(x) & isscalar(x)); - p = inputParser(); - 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', [], @(x)iscolor(vertcat(x{:}))); - p.addParameter('MarkerSize', 24, @isnumeric); - p.addParameter('MedianMarkerSize', 36, @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,0.15}, @(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('ShowWhiskers', true, isscalarlogical); - validSides={'full', 'right', 'left'}; - checkSide = @(x) any(validatestring(x, validSides)); - p.addParameter('HalfViolin', 'full', checkSide); - validQuartileStyles={'boxplot', 'shadow', 'none'}; - checkQuartile = @(x)any(validatestring(x, validQuartileStyles)); - p.addParameter('QuartileStyle', 'boxplot', checkQuartile); - validDataStyles = {'scatter', 'histogram', 'none'}; - checkStyle = @(x)any(validatestring(x, validDataStyles)); - p.addParameter('DataStyle', 'scatter', checkStyle); - - 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; - value= mean(value); - end - - width = width/max(density); - end - end -end - ->>>>>>> master From 3984742b78c5991a943bbb9eab535b688f1dc727 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20H=C3=A4fele?= Date: Sat, 6 Jul 2024 21:26:45 +0200 Subject: [PATCH 4/7] Added documentation to swapOrientationMaybe function and code formatting. --- Violin.m | 83 ++++++++++++++++++++++++++++++-------------------------- 1 file changed, 44 insertions(+), 39 deletions(-) diff --git a/Violin.m b/Violin.m index f0ee3f0..69be995 100644 --- a/Violin.m +++ b/Violin.m @@ -160,7 +160,9 @@ % 'QuartileStyle' Option on how to display quartiles, with a % boxplot or as a shadow. Defaults to boxplot. % 'DataStyle' Defines the style to show the data points. Opts: - % 'scatter', 'histogram' or 'none'. Default is 'Scatter'. + % 'scatter', 'histogram' or 'none'. Default is 'Scatter'. + % 'Orientation' Defines the orientation of the violin plot. Opts: + % 'vertical', 'horizontal'. Default is 'vertical'. st = dbstack; % get the calling function for reporting errors namefun = st.name; @@ -193,7 +195,7 @@ data = data(not(isnan(data))); data2 = data2(not(isnan(data2))); if numel(data) == 1 - [x,y] = obj.swapOrientationMaybe(pos, data); + [x, y] = obj.swapOrientationMaybe(pos, data); obj.MedianPlot = scatter(x, y, 'filled'); obj.MedianColor = args.MedianColor; obj.MedianPlot.MarkerEdgeColor = args.EdgeColor; @@ -237,7 +239,7 @@ case 'scatter' if ~isempty(data2) jitter = 1*(rand(size(data))); %right - [x,y] = obj.swapOrientationMaybe(pos + jitter.*jitterstrength, data); + [x, y] = obj.swapOrientationMaybe(pos + jitter.*jitterstrength, data); obj.ScatterPlot = ... scatter(x, y, args.MarkerSize, 'filled'); % plot the data points within the violin area @@ -247,11 +249,11 @@ jitterstrength = densityC*widthC; end jitter = -1*rand(size(data2));% left - [x,y] = obj.swapOrientationMaybe(pos + jitter.*jitterstrength,data2); + [x, y] = obj.swapOrientationMaybe(pos + jitter.*jitterstrength, data2); obj.ScatterPlot2 = ... scatter(x, y, args.MarkerSize, 'filled'); else - [x,y] = obj.swapOrientationMaybe(pos + jitter.*jitterstrength,data); + [x, y] = obj.swapOrientationMaybe(pos + jitter.*jitterstrength, data); obj.ScatterPlot = ... scatter(x, y, args.MarkerSize, 'filled'); @@ -260,11 +262,11 @@ [counts,edges] = histcounts(data, size(unique(data),1)); switch args.HalfViolin case 'right' - [x,y] = obj.swapOrientationMaybe([pos-((counts')/max(counts))*max(jitterstrength)*2, pos*ones(size(counts,2),1)]',... + [x, y] = obj.swapOrientationMaybe([pos-((counts')/max(counts))*max(jitterstrength)*2, pos*ones(size(counts,2),1)]', ... [edges(1:end-1)+max(diff(edges))/2; edges(1:end-1)+max(diff(edges))/2]); obj.HistogramPlot = plot(x,y,'-','LineWidth',1, 'Color', 'k'); case 'left' - [x,y] = obj.swapOrientationMaybe([pos*ones(size(counts,2),1), pos+((counts')/max(counts))*max(jitterstrength)*2]', ... + [x, y] = obj.swapOrientationMaybe([pos*ones(size(counts,2),1), pos+((counts')/max(counts))*max(jitterstrength)*2]', ... [edges(1:end-1)+max(diff(edges))/2; edges(1:end-1)+max(diff(edges))/2]); obj.HistogramPlot = plot(x,y,'-','LineWidth',1, 'Color', 'k'); otherwise @@ -278,26 +280,26 @@ if isempty(data2) % if no comparison data switch args.HalfViolin case 'right' - [x,y] = obj.swapOrientationMaybe([pos+density*width halfViol*pos],... + [x,y ] = obj.swapOrientationMaybe([pos+density*width halfViol*pos], ... [value value(end:-1:1)]); - obj.ViolinPlot = fill(x,y, [1 1 1]);% plot color will be overwritten later + obj.ViolinPlot = fill(x,y, [1 1 1]); % plot color will be overwritten later case 'left' [x,y] = obj.swapOrientationMaybe([halfViol*pos pos-density(end:-1:1)*width], ... [value value(end:-1:1)]); - obj.ViolinPlot = fill(x,y, [1 1 1]);% plot color will be overwritten later + obj.ViolinPlot = fill(x,y, [1 1 1]); % plot color will be overwritten later case 'full' - [x,y] = obj.swapOrientationMaybe([pos+density*width pos-density(end:-1:1)*width],... + [x, y] = obj.swapOrientationMaybe([pos+density*width pos-density(end:-1:1)*width], ... [value value(end:-1:1)]); - obj.ViolinPlot = fill(x,y, [1 1 1]);% plot color will be overwritten later + obj.ViolinPlot = fill(x,y, [1 1 1]); % plot color will be overwritten later end else % plot right half of the violin - [x,y] = obj.swapOrientationMaybe([pos+density*width pos-density(1)*width],[value value(1)]); - obj.ViolinPlot = fill(x,y,[1 1 1]); + [x, y] = obj.swapOrientationMaybe([pos+density*width pos-density(1)*width], [value value(1)]); + obj.ViolinPlot = fill(x ,y, [1 1 1]); % plot left half of the violin - [x,y] = obj.swapOrientationMaybe([pos-densityC(end)*widthC pos-densityC(end:-1:1)*widthC],... + [x, y] = obj.swapOrientationMaybe([pos-densityC(end)*widthC pos-densityC(end:-1:1)*widthC], ... [valueC(end) valueC(end:-1:1)]); - obj.ViolinPlot2 = fill(x,y, [1 1 1]); + obj.ViolinPlot2 = fill(x, y, [1 1 1]); end %% Plot the quartiles within the violin @@ -308,21 +310,21 @@ switch args.HalfViolin case 'right' w = [pos+density*width halfViol*pos]; - h= [value value(end:-1:1)]; + h = [value value(end:-1:1)]; case 'left' w = [halfViol*pos pos-density(end:-1:1)*width]; - h= [value value(end:-1:1)]; + h = [value value(end:-1:1)]; case 'full' w = [pos+density*width pos-density(end:-1:1)*width]; - h= [value value(end:-1:1)]; + h = [value value(end:-1:1)]; end indices = h >= quartiles(1) & h <= quartiles(3); - [x,y] = obj.swapOrientationMaybe(w(indices),h(indices)); - obj.ViolinPlotQ = fill(x,y, [1 1 1]); % plot color will be overwritten later + [x, y] = obj.swapOrientationMaybe(w(indices), h(indices)); + obj.ViolinPlotQ = fill(x, y, [1 1 1]); % plot color will be overwritten later case 'boxplot' - [x,y] = obj.swapOrientationMaybe(pos+[-1,1,1,-1]*args.BoxWidth,... + [x, y] = obj.swapOrientationMaybe(pos+[-1,1,1,-1]*args.BoxWidth, ... [quartiles(1) quartiles(1) quartiles(3) quartiles(3)]); - obj.BoxPlot = fill(x,y, [1 1 1]); % plot color will be overwritten later + obj.BoxPlot = fill(x, y, [1 1 1]); % plot color will be overwritten later case 'none' end @@ -339,14 +341,14 @@ end switch args.HalfViolin case 'right' - [x,y] = obj.swapOrientationMaybe(pos+[0,1].*meanDensityWidth,[meanValue, meanValue]); - obj.MeanPlot = plot(x,y); + [x, y] = obj.swapOrientationMaybe(pos+[0,1].*meanDensityWidth, [meanValue, meanValue]); + obj.MeanPlot = plot(x, y); case 'left' - [x,y] = obj.swapOrientationMaybe(pos+[-1,0].*meanDensityWidth,[meanValue, meanValue]); - obj.MeanPlot = plot(x,y); + [x, y] = obj.swapOrientationMaybe(pos+[-1,0].*meanDensityWidth, [meanValue, meanValue]); + obj.MeanPlot = plot(x, y); case 'full' - [x,y] = obj.swapOrientationMaybe(pos+[-1,1].*meanDensityWidth,[meanValue, meanValue]); - obj.MeanPlot = plot(x,y); + [x, y] = obj.swapOrientationMaybe(pos+[-1,1].*meanDensityWidth, [meanValue, meanValue]); + obj.MeanPlot = plot(x, y); end obj.MeanPlot.LineWidth = 1; @@ -357,19 +359,19 @@ hiwhisker = quartiles(3) + 1.5*IQR; hiwhisker = min(hiwhisker, max(data(data < hiwhisker))); if ~isempty(lowhisker) && ~isempty(hiwhisker) - [x,y] = obj.swapOrientationMaybe([pos pos],[lowhisker hiwhisker]); - obj.WhiskerPlot = plot(x,y); + [x, y] = obj.swapOrientationMaybe([pos pos], [lowhisker hiwhisker]); + obj.WhiskerPlot = plot(x, y); end % Median - [x,y] = obj.swapOrientationMaybe(pos, quartiles(2)); - obj.MedianPlot = scatter(x,y, args.MedianMarkerSize, [1 1 1], 'filled'); + [x, y] = obj.swapOrientationMaybe(pos, quartiles(2)); + obj.MedianPlot = scatter(x, y, args.MedianMarkerSize, [1 1 1], 'filled'); % Notches - [x,y] = obj.swapOrientationMaybe(pos, quartiles(2)-1.57*IQR/sqrt(length(data))); - obj.NotchPlots = scatter(x,y, [], [1 1 1], 'filled', '^'); - [x,y] = obj.swapOrientationMaybe(pos, quartiles(2)+1.57*IQR/sqrt(length(data))); - obj.NotchPlots(2) = scatter(x,y, [], [1 1 1], 'filled', 'v'); + [x, y] = obj.swapOrientationMaybe(pos, quartiles(2)-1.57*IQR/sqrt(length(data))); + obj.NotchPlots = scatter(x, y, [], [1 1 1], 'filled', '^'); + [x, y] = obj.swapOrientationMaybe(pos, quartiles(2)+1.57*IQR/sqrt(length(data))); + obj.NotchPlots(2) = scatter(x, y, [], [1 1 1], 'filled', 'v'); %% Set graphical preferences obj.EdgeColor = args.EdgeColor; @@ -698,8 +700,11 @@ results = p.Results; end - function [x,y] = swapOrientationMaybe(obj, x, y) - if strcmp(obj.Orientation,'horizontal') + function [x, y] = swapOrientationMaybe(obj, x, y) + %swapOrientationMaybe swaps the two variables x and y + % if Violin.Orientation property set to horizontal. + % If orientation is vertical, it returns x and y as is. + if strcmp(obj.Orientation, 'horizontal') tmp = x; x = y; y = tmp; From d3d13d0fe979bc35ce9611442a8ef656667eb291 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20H=C3=A4fele?= Date: Mon, 8 Jul 2024 10:52:51 +0200 Subject: [PATCH 5/7] Added 'Parent' property to plot violins on specific axes. --- Violin.m | 48 +++++++++++++++++++++++++++--------------------- violinplot.m | 27 ++++++++++++++------------- 2 files changed, 41 insertions(+), 34 deletions(-) diff --git a/Violin.m b/Violin.m index 69be995..0a069a8 100644 --- a/Violin.m +++ b/Violin.m @@ -56,6 +56,7 @@ % 'scatter', 'histogram' or 'none'. Default is 'scatter'. % Orientation - Defines the orientation of the violin plot. Opts: % 'vertical', 'horizontal'. Default is 'vertical'. + % Parent - The parent axis of the violin plot. % % % Violin Children: @@ -85,6 +86,7 @@ MeanPlot % line plot of the mean (horizontal line) HistogramPlot % histogram of the data ViolinPlotQ % fill plot of the Quartiles as shadow + Parent % parent axis end properties (SetAccess=protected, GetAccess=public) @@ -163,11 +165,13 @@ % 'scatter', 'histogram' or 'none'. Default is 'Scatter'. % 'Orientation' Defines the orientation of the violin plot. Opts: % 'vertical', 'horizontal'. Default is 'vertical'. + % 'Parent' The parent axis of the violin plot. st = dbstack; % get the calling function for reporting errors namefun = st.name; args = obj.checkInputs(data, pos, varargin{:}); obj.Orientation = args.Orientation; + obj.Parent = args.Parent; if length(data)==1 data2 = []; @@ -196,13 +200,13 @@ data2 = data2(not(isnan(data2))); if numel(data) == 1 [x, y] = obj.swapOrientationMaybe(pos, data); - obj.MedianPlot = scatter(x, y, 'filled'); + obj.MedianPlot = scatter(x, y, 'filled', 'Parent', obj.Parent); obj.MedianColor = args.MedianColor; obj.MedianPlot.MarkerEdgeColor = args.EdgeColor; return end - hold('on'); + hold(obj.Parent, 'on'); %% Calculate kernel density estimation for the violin @@ -241,7 +245,7 @@ jitter = 1*(rand(size(data))); %right [x, y] = obj.swapOrientationMaybe(pos + jitter.*jitterstrength, data); obj.ScatterPlot = ... - scatter(x, y, args.MarkerSize, 'filled'); + scatter(x, y, args.MarkerSize, 'filled', 'Parent', obj.Parent); % plot the data points within the violin area if length(densityC) > 1 jitterstrength = interp1(valueC, densityC*widthC, data2); @@ -251,11 +255,11 @@ jitter = -1*rand(size(data2));% left [x, y] = obj.swapOrientationMaybe(pos + jitter.*jitterstrength, data2); obj.ScatterPlot2 = ... - scatter(x, y, args.MarkerSize, 'filled'); + scatter(x, y, args.MarkerSize, 'filled', 'Parent', obj.Parent); else [x, y] = obj.swapOrientationMaybe(pos + jitter.*jitterstrength, data); obj.ScatterPlot = ... - scatter(x, y, args.MarkerSize, 'filled'); + scatter(x, y, args.MarkerSize, 'filled', 'Parent', obj.Parent); end case 'histogram' @@ -264,11 +268,11 @@ case 'right' [x, y] = obj.swapOrientationMaybe([pos-((counts')/max(counts))*max(jitterstrength)*2, pos*ones(size(counts,2),1)]', ... [edges(1:end-1)+max(diff(edges))/2; edges(1:end-1)+max(diff(edges))/2]); - obj.HistogramPlot = plot(x,y,'-','LineWidth',1, 'Color', 'k'); + obj.HistogramPlot = plot(x,y,'-','LineWidth',1, 'Color', 'k', 'Parent', obj.Parent); case 'left' [x, y] = obj.swapOrientationMaybe([pos*ones(size(counts,2),1), pos+((counts')/max(counts))*max(jitterstrength)*2]', ... [edges(1:end-1)+max(diff(edges))/2; edges(1:end-1)+max(diff(edges))/2]); - obj.HistogramPlot = plot(x,y,'-','LineWidth',1, 'Color', 'k'); + obj.HistogramPlot = plot(x,y,'-','LineWidth',1, 'Color', 'k', 'Parent', obj.Parent); otherwise fprintf([namefun, ' No histogram/bar plot option available for full violins, as it would look overcrowded.\n']) end @@ -282,24 +286,24 @@ case 'right' [x,y ] = obj.swapOrientationMaybe([pos+density*width halfViol*pos], ... [value value(end:-1:1)]); - obj.ViolinPlot = fill(x,y, [1 1 1]); % plot color will be overwritten later + obj.ViolinPlot = fill(x,y, [1 1 1], 'Parent', obj.Parent); % plot color will be overwritten later case 'left' [x,y] = obj.swapOrientationMaybe([halfViol*pos pos-density(end:-1:1)*width], ... [value value(end:-1:1)]); - obj.ViolinPlot = fill(x,y, [1 1 1]); % plot color will be overwritten later + obj.ViolinPlot = fill(x,y, [1 1 1], 'Parent', obj.Parent); % plot color will be overwritten later case 'full' [x, y] = obj.swapOrientationMaybe([pos+density*width pos-density(end:-1:1)*width], ... [value value(end:-1:1)]); - obj.ViolinPlot = fill(x,y, [1 1 1]); % plot color will be overwritten later + obj.ViolinPlot = fill(x,y, [1 1 1], 'Parent', obj.Parent); % plot color will be overwritten later end else % plot right half of the violin [x, y] = obj.swapOrientationMaybe([pos+density*width pos-density(1)*width], [value value(1)]); - obj.ViolinPlot = fill(x ,y, [1 1 1]); + obj.ViolinPlot = fill(x ,y, [1 1 1], 'Parent', obj.Parent); % plot left half of the violin [x, y] = obj.swapOrientationMaybe([pos-densityC(end)*widthC pos-densityC(end:-1:1)*widthC], ... [valueC(end) valueC(end:-1:1)]); - obj.ViolinPlot2 = fill(x, y, [1 1 1]); + obj.ViolinPlot2 = fill(x, y, [1 1 1], 'Parent', obj.Parent); end %% Plot the quartiles within the violin @@ -320,11 +324,11 @@ end indices = h >= quartiles(1) & h <= quartiles(3); [x, y] = obj.swapOrientationMaybe(w(indices), h(indices)); - obj.ViolinPlotQ = fill(x, y, [1 1 1]); % plot color will be overwritten later + obj.ViolinPlotQ = fill(x, y, [1 1 1], 'Parent', obj.Parent); % plot color will be overwritten later case 'boxplot' [x, y] = obj.swapOrientationMaybe(pos+[-1,1,1,-1]*args.BoxWidth, ... [quartiles(1) quartiles(1) quartiles(3) quartiles(3)]); - obj.BoxPlot = fill(x, y, [1 1 1]); % plot color will be overwritten later + obj.BoxPlot = fill(x, y, [1 1 1], 'Parent', obj.Parent); % plot color will be overwritten later case 'none' end @@ -342,13 +346,13 @@ switch args.HalfViolin case 'right' [x, y] = obj.swapOrientationMaybe(pos+[0,1].*meanDensityWidth, [meanValue, meanValue]); - obj.MeanPlot = plot(x, y); + obj.MeanPlot = plot(x, y, 'Parent', obj.Parent); case 'left' [x, y] = obj.swapOrientationMaybe(pos+[-1,0].*meanDensityWidth, [meanValue, meanValue]); - obj.MeanPlot = plot(x, y); + obj.MeanPlot = plot(x, y, 'Parent', obj.Parent); case 'full' [x, y] = obj.swapOrientationMaybe(pos+[-1,1].*meanDensityWidth, [meanValue, meanValue]); - obj.MeanPlot = plot(x, y); + obj.MeanPlot = plot(x, y, 'Parent', obj.Parent); end obj.MeanPlot.LineWidth = 1; @@ -360,18 +364,19 @@ hiwhisker = min(hiwhisker, max(data(data < hiwhisker))); if ~isempty(lowhisker) && ~isempty(hiwhisker) [x, y] = obj.swapOrientationMaybe([pos pos], [lowhisker hiwhisker]); - obj.WhiskerPlot = plot(x, y); + obj.WhiskerPlot = plot(x, y, 'Parent', obj.Parent); end % Median [x, y] = obj.swapOrientationMaybe(pos, quartiles(2)); - obj.MedianPlot = scatter(x, y, args.MedianMarkerSize, [1 1 1], 'filled'); + obj.MedianPlot = scatter(x, y, ... + args.MedianMarkerSize, [1 1 1], 'filled', 'Parent', obj.Parent); % Notches [x, y] = obj.swapOrientationMaybe(pos, quartiles(2)-1.57*IQR/sqrt(length(data))); - obj.NotchPlots = scatter(x, y, [], [1 1 1], 'filled', '^'); + obj.NotchPlots = scatter(x, y, [], [1 1 1], 'filled', '^', 'Parent', obj.Parent); [x, y] = obj.swapOrientationMaybe(pos, quartiles(2)+1.57*IQR/sqrt(length(data))); - obj.NotchPlots(2) = scatter(x, y, [], [1 1 1], 'filled', 'v'); + obj.NotchPlots(2) = scatter(x, y, [], [1 1 1], 'filled', 'v', 'Parent', obj.Parent); %% Set graphical preferences obj.EdgeColor = args.EdgeColor; @@ -695,6 +700,7 @@ checkStyle = @(x)any(validatestring(x, validDataStyles)); p.addParameter('DataStyle', 'scatter', checkStyle); p.addParameter('Orientation', 'vertical', @(x) ismember(x, {'vertical', 'horizontal'})); + p.addParameter('Parent', gca, @(x) isa(x,'matlab.graphics.axis.Axes')); p.parse(data, pos, varargin{:}); results = p.Results; diff --git a/violinplot.m b/violinplot.m index 9525cab..6cbd860 100644 --- a/violinplot.m +++ b/violinplot.m @@ -71,6 +71,7 @@ % Defaults to alphabetical ordering % 'Orientation' Orientation of the violin plot. % Defaults to 'vertical'. +% 'Parent' The parent axis of the violin plot. % Copyright (c) 2016, Bastian Bechtold % This code is released under the terms of the BSD 3-clause license @@ -141,11 +142,11 @@ violins(n) = Violin({thisData}, n, varargin{:}); end if strcmp(violins(1).Orientation,'vertical') - set(gca, 'XTick', 1:length(catnames), 'XTickLabels', catnames); + set(violins(1).Parent, 'XTick', 1:length(catnames), 'XTickLabels', catnames); else - set(gca, 'YTick', 1:length(catnames), 'YTickLabels', catnames); + set(violins(1).Parent, 'YTick', 1:length(catnames), 'YTickLabels', catnames); end - set(gca,'Box','on'); + set(violins(1).Parent,'Box','on'); return elseif iscell(data) && length(data(:))==2 % cell input if not(size(data{1},2)==size(data{2},2)) @@ -171,11 +172,11 @@ violins(n) = Violin({thisData}, n, varargin{:}); end if strcmp(violins(1).Orientation,'vertical') - set(gca, 'XTick', 1:length(catnames), 'XTickLabels', catnames); + set(violins(1).Parent, 'XTick', 1:length(catnames), 'XTickLabels', catnames); else - set(gca, 'YTick', 1:length(catnames), 'YTickLabels', catnames); + set(violins(1).Parent, 'YTick', 1:length(catnames), 'YTickLabels', catnames); end - set(gca,'Box','on'); + set(violins(1).Parent,'Box','on'); return else data = {data}; @@ -186,9 +187,9 @@ if not(hascategories) && isvector(data{1}) violins = Violin(data, 1, varargin{:}); if strcmp(violins(1).Orientation,'vertical') - set(gca, 'XTick', 1); + set(violins(1).Parent, 'XTick', 1); else - set(gca, 'yTick', 1); + set(violins(1).Parent, 'yTick', 1); end % 2D data with or without categories elseif ismatrix(data{1}) @@ -197,19 +198,19 @@ violins(n) = Violin(thisData, n, varargin{:}); end if strcmp(violins(1).Orientation,'vertical') - set(gca, 'XTick', 1:size(data{1}, 2)); + set(violins(1).Parent, 'XTick', 1:size(data{1}, 2)); else - set(gca, 'YTick', 1:size(data{1}, 2)); + set(violins(1).Parent, 'YTick', 1:size(data{1}, 2)); end if hascategories && length(cats) == size(data{1}, 2) if strcmp(violins(1).Orientation,'vertical') - set(gca, 'XTickLabels', cats); + set(violins(1).Parent, 'XTickLabels', cats); else - set(gca, 'YTickLabels', cats); + set(violins(1).Parent, 'YTickLabels', cats); end end end -set(gca,'Box','on'); +set(violins(1).Parent,'Box','on'); end From ae4bc11e06ff5b1c8d07394ee6c7cf6f7ff52444 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20H=C3=A4fele?= Date: Fri, 12 Jul 2024 09:35:58 +0200 Subject: [PATCH 6/7] adjusted README and readme_figures. Small code snippet works now on its own. --- README.md | 10 +++++++-- example2.png | Bin 50917 -> 24640 bytes readme_figures.m | 52 +++++++++++++++++++++++++++-------------------- 3 files changed, 38 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 4153322..96644c0 100644 --- a/README.md +++ b/README.md @@ -38,15 +38,21 @@ You can also play around with the different options, and tune your violin plots ```matlab grouporder={'England','Sweden','Japan','Italy','Germany','France','USA'}; +color = jet(length(grouporder)); +pos = 3; vs = Violin({MPG(strcmp(Origin, grouporder{pos}))},... - position,... + pos,... 'HalfViolin','right',...% left, full 'QuartileStyle','shadow',... % boxplot, none 'DataStyle', 'histogram',... % scatter, none 'ShowNotches', false,... 'ShowMean', false,... 'ShowMedian', true,... - 'ViolinColor', color); + 'ViolinColor', {color(pos,:)},... + 'Orientation', 'horizontal'); +ylim(pos + [-.5, 0.5]) +yticks(pos) +yticklabels(grouporder{pos}) ``` ![example image2](example2.png) diff --git a/example2.png b/example2.png index 392203a007984b99c77c5c64979e15b0259d0f00..3adfb73a7aa1a03cf397df9a49c5861b1587a799 100644 GIT binary patch literal 24640 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 diff --git a/readme_figures.m b/readme_figures.m index ef5a933..87ab7dd 100644 --- a/readme_figures.m +++ b/readme_figures.m @@ -11,48 +11,53 @@ ylabel('Fuel Economy in MPG'); xlim([0.5, 7.5]); -set(gcf,"Units","pixels","Position",[100 100 560 420]) +set(gcf,'Units','pixels','Position',[100 100 560 420]) % save results -exportgraphics(gcf,"example.png") +exportgraphics(gcf,'example.png') %% figure 2 close all clear vs -C = colormap("jet"); +C = colormap('jet'); grouporder={'England','Sweden','Japan','Italy','Germany','France','USA'}; % England (just a dot) -vs = Violin({MPG(strcmp(Origin,'England'))},1); +vs = Violin({MPG(strcmp(Origin,'England'))},1,'Orientation','horizontal'); % Sweden (different Bandwidths) vs = Violin({MPG(strcmp(Origin,'Sweden'))},2,... - "HalfViolin","left",... - "Bandwidth",3); + 'HalfViolin','left',... + 'Bandwidth',3,... + 'Orientation','horizontal'); vs = Violin({MPG(strcmp(Origin,'Sweden'))},2,... - "HalfViolin","right",... - "Bandwidth",12); + 'HalfViolin','right',... + 'Bandwidth',12,... + 'Orientation','horizontal'); % Japan (show quartiles) vs = Violin({MPG(strcmp(Origin,'Japan'))},3,... 'QuartileStyle','shadow',... % boxplot, none - 'ShowBox',true); + 'ShowBox',true,... + 'Orientation','horizontal'); % Italy left: standard vs = Violin({MPG(strcmp(Origin,'Italy'))},4,... 'QuartileStyle','shadow',... % boxplot, none 'ShowBox',true,... - "HalfViolin","left"); + 'HalfViolin','left',... + 'Orientation','horizontal'); % Italy right: change color, offset vs = Violin({MPG(strcmp(Origin,'Italy'))-15},4,... 'QuartileStyle','shadow',... % boxplot, none 'ShowBox',true,... - "HalfViolin","right",... - "ViolinColor",{C(7,:)}); + 'HalfViolin','right',... + 'ViolinColor',{C(7,:)},... + 'Orientation','horizontal'); % Germany left: standard @@ -60,37 +65,40 @@ 'QuartileStyle','none',... % boxplot, none 'DataStyle', 'none',... % scatter, histogram 'ShowMean',true,... - "HalfViolin","left"); + 'HalfViolin','left',... + 'Orientation','horizontal'); % Germany histogram vs = Violin({MPG(strcmp(Origin,'Germany'))},5,... 'QuartileStyle','none',... % boxplot, none 'DataStyle', 'histogram',... % scatter, histogram 'ShowMean',true,... - "HalfViolin","left"); + 'HalfViolin','left',... + 'Orientation','horizontal'); % France histogram vs = Violin({MPG(strcmp(Origin,'France'))},6,... 'QuartileStyle','none',... % boxplot, none 'DataStyle', 'histogram',... % scatter, histogram 'ShowMean',false,... - "HalfViolin","right"); + 'HalfViolin','right',... + 'Orientation','horizontal'); % USA histogram and quartiles vs = Violin({MPG(strcmp(Origin,'USA'))},7,... 'QuartileStyle','shadow',... % boxplot, none 'DataStyle', 'histogram',... % scatter, histogram 'ShowMean',false,... - "HalfViolin","right"); + 'HalfViolin','right',... + 'Orientation','horizontal'); % set correct labels -set(gca,xticklabels=grouporder) +set(gca,yticklabels=grouporder) -ylabel('Fuel Economy in MPG'); -xlim([0.5, 7.5]); -set(gca,"XTickLabelRotation",30) +xlabel('Fuel Economy in MPG'); +ylim([0.5, 7.5]); -set(gcf,"Units","pixels","Position",[200 200 560 420]) +set(gcf,'Units','pixels','Position',[200 200 560 420]) % save results -exportgraphics(gcf,"example2.png") +exportgraphics(gcf,'example2.png') \ No newline at end of file From f52960770558d71bd677431751a9a3cdec3c23fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20H=C3=A4fele?= Date: Fri, 12 Jul 2024 09:54:54 +0200 Subject: [PATCH 7/7] test cases 9 and 10 added. --- test_cases/testviolinplot.m | 38 +++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/test_cases/testviolinplot.m b/test_cases/testviolinplot.m index 339632e..851d94c 100644 --- a/test_cases/testviolinplot.m +++ b/test_cases/testviolinplot.m @@ -50,14 +50,14 @@ function testviolinplot() % TEST CASE 6 disp('Test 6: Test plotting only right side & histogram plot, with quartiles as boxplot.'); subplot(2,4,6); -vs5 = violinplot(MPG, Origin, 'QuartileStyle','boxplot', 'HalfViolin','right',... +vs6 = violinplot(MPG, Origin, 'QuartileStyle','boxplot', 'HalfViolin','right',... 'DataStyle', 'histogram'); plotdetails(6); % TEST CASE 7 disp('Test 7: Test plotting only left side & histogram plot, and quartiles as shadow.'); subplot(2,4,7); -vs5 = violinplot(MPG, Origin, 'QuartileStyle','shadow', 'HalfViolin','left',... +vs7 = violinplot(MPG, Origin, 'QuartileStyle','shadow', 'HalfViolin','left',... 'DataStyle', 'histogram', 'ShowMean', true); plotdetails(7); @@ -65,15 +65,41 @@ function testviolinplot() % TEST CASE 8 disp('Test 8: Same as previous one, just removing the data of half of the violins afterwards.'); subplot(2,4,8); -vs5 = violinplot([MPG; 5;5;5;5;5], [Origin; 'test';'test';'test';'test';'test'], 'QuartileStyle','shadow', 'HalfViolin','full',... +vs8 = violinplot([MPG; 5;5;5;5;5], [Origin; 'test';'test';'test';'test';'test'], 'QuartileStyle','shadow', 'HalfViolin','full',... 'DataStyle', 'scatter', 'ShowMean', false); plotdetails(8); -for n= 1:round(length(vs5)/2) - vs5(1,n).ShowData = 0; +for n= 1:round(length(vs8)/2) + vs8(1,n).ShowData = 0; end xlim([0, 9]); -%other test cases could be added here +% TEST CASE 9 +disp('Test 9: Test parent property.'); +figure +for i = 1:4 + ax(i) = subplot(2,2,i); +end +vs9 = violinplot(MPG, Origin,'parent',ax(3)); +title(ax(3),sprintf('Test %02.0f \n',9)); +ylabel(ax(3),'Fuel Economy in MPG '); +xlim(ax(3),[0, 8]); +grid(ax(3),'minor'); +set(ax(3), 'color', 'none'); +xtickangle(ax(3),-30); +fprintf('Test %02.0f passed ok! \n ',9); + +% TEST CASE 10 +disp('Test 10: Test of axis orientation.'); +vs10 = violinplot(MPG, Origin,'parent',ax(2),'Orientation','horizontal'); +title(ax(2),sprintf('Test %02.0f \n',10)); +xlabel(ax(2),'Fuel Economy in MPG '); +ylim(ax(2),[0, 8]); +grid(ax(2),'minor'); +set(ax(2), 'color', 'none'); +ytickangle(ax(2),0); +fprintf('Test %02.0f passed ok! \n ',10); + +%other test cases could be added here end function plotdetails(n)