function h = schemaball_JS(r, lbls, ccolor, ncolor)
% SCHEMABALL Plots correlation matrix as a schemaball
%
% SCHEMABALL(R) R is a square numeric matrix with values in [-1,1].
%
% NOTE: only the off-diagonal lower triangular section of R is
% considered, i.e. tril(r,-1).
%
% SCHEMABALL(..., LBLS, CCOLOR, NCOLOR) Plot schemaball with optional
% arguments (accepts empty args).
%
% - LBLS Plot a schemaball with custom labels at each node.
% LBLS is either a cellstring of length M, where
% M = size(r,1), or a M by N char array, where each
% row is a label.
%
% - CCOLOR Supply an RGB triplet that specifies the color of
% the curves. CURVECOLOR can also be a 2 by 3 matrix
% with the color in the first row for negative
% correlations and the color in the second row for
% positive correlations.
%
% - NCOLOR Change color of the nodes with an RGB triplet.
%
%
% H = SCHEMABALL(...) Returns a structure with handles to the graphic objects
%
% h.l handles to the curves (line objects), one per color shade.
% If no curves fall into a color shade that handle will be NaN.
% h.s handle to the nodes (scattergroup object)
% h.t handles to the node text labels (text objects)
%
%
% Examples
%
% % Base demo
% schemaball
%
% % Supply your own correlation matrix (only lower off-diagonal triangular part is considered)
% x = rand(10).^3;
% x(:,3) = 1.3*mean(x,2);
% schemaball(x)
%
% % Supply custom labels as ['aa'; 'bb'; 'cc'; ...] or {'Hi','how','are',...}
% schemaball(x, repmat(('a':'j')',1,2))
% schemaball(x, {'Hi','how','is','your','day?', 'Do','you','like','schemaballs?','NO!!'})
%
% % Customize curve colors
% schemaball([],[],[1,0,1;1 1 0])
%
% % Customize node color
% schemaball([],[],[],[0,1,0])
%
% % Customize manually other aspects
% h = schemaball;
% set(h.l(~isnan(h.l)), 'LineWidth',1.2)
% set(h.s, 'MarkerEdgeColor','red','LineWidth',2,'SizeData',100)
%
%
% Additional features:
% - FEX schemaball page
% - Origin: question on Stackoverflow.com
% - Schemaball by Gunther Struyf
%
% See also: CORR, CORRPLOT
% Author: Oleg Komarov (oleg.komarov@hotmail.it)
% Tested on R2013a Win7 64 and Vista 32
% 15 jun 2013 - Created
%% Parameters
% Tweak these only
% Number of color shades/buckets (large N simply creates many perceptually indifferent color shades)
N = 100;
% Points in [0, 1] for bezier curves: leave space at the extremes to detach a bit the nodes.
% Smaller step will use more points to plot the curves.
t = (0.025: 0.05 :1)';
% Nodes edge color
% ecolor = [.25 .103922 .012745];
ecolor = [0 0 0];
% Text color
tcolor = [.3 .3 .3];
%% Checks
% Ninput
narginchk(0,4)
% Some defaults
if nargin < 1 || isempty(r); r = (rand(50)*2-1).^29; end
sz = size(r);
if nargin < 2 || isempty(lbls); lbls = cellstr(reshape(sprintf('%-4d',1:sz(1)),4,sz(1))'); end
if nargin < 4 || isempty(ncolor); ncolor = [0 0 1]; end
% R
if ~isnumeric(r) || any(abs(r(:)) > 1) || sz(1) ~= sz(2) || numel(sz) > 2 || sz(1) < 3
error('schemaball:validR','R should be a square numeric matrix with values in [-1, 1].')
end
% Lbls
if (~ischar(lbls) || size(lbls,1) ~= sz(1)) && (~iscellstr(lbls) || ~isvector(lbls) || length(lbls) ~= sz(1))
error('schemaball:validLbls','LBLS should either be an M by N char array or a cellstring of length M, where M is size(R,1).')
end
if ischar(lbls)
lbls = cellstr(lbls);
end
% Ccolor
if nargin < 3 || isempty(ccolor)
ccolor = hsv2rgb([[linspace(.8333, .95, N); ones(1, N); linspace(1,0,N)],...
[linspace(.03, .1666, N); ones(1, N); linspace(0,1,N)]]');
else
szC = size(ccolor);
if ~isnumeric(ccolor) || szC(2) ~= 3
error('schemaball:validCcolor','CCOLOR should be a 1 by 3 or 2 by 3 numeric matrix with RGB colors.')
elseif szC(1) == 1
ccolor = [ccolor; ccolor];
ccolor = rgb2hsv(ccolor);
ccolor = hsv2rgb([repmat(ccolor(1,1:2),N,1), linspace(ccolor(1,end),0,N)';
repmat(ccolor(2,1:2),N,1), linspace(0,ccolor(2,end),N)']);
elseif szC(1) == 2
ccolor = rgb2hsv(ccolor);
ccolor = hsv2rgb([repmat(ccolor(1,1:2),N,1), linspace(ccolor(1,end),0,N)';
repmat(ccolor(2,1:2),N,1), linspace(0,ccolor(2,end),N)']);
else
N = floor(size(ccolor,1)/2);
end
end
% Ncolor
szN = size(ncolor);
if ~isnumeric(ncolor) || szN(2) ~= 3 || szN(1) > 1
error('schemaball:validNcolor','NCOLOR should be a single RGB color, i.e. a numeric row triplet.')
end
ncolor = rgb2hsv(ncolor);
%% Engine
% Create figure
figure('renderer','zbuffer','visible','off')
axes('NextPlot','add')
% Index only low triangular matrix without main diag
tf = tril(true(sz),-1);
% Index correlations into bucketed colormap to determine plotting order (darkest to brightest)
N2 = 2*N;
%[n, isrt] = histc(r(tf), [-1 0 1]);
[n, isrt] = histc(r(tf), linspace(min(r(tf)),max(r(tf)) + eps(100),N2 + 1));
plotorder = reshape([N:-1:1; N+1:N2],N2,1);
% Retrieve pairings of nodes
[row,col] = find(tf);
% Use tau http://tauday.com/tau-manifesto
tau = 2*pi;
% Positions of nodes on the circle starting from (0,-1), useful later for label orientation
step = tau/sz(1);
theta = -.25*tau : step : .75*tau - step;
% Get cartesian x-y coordinates of the nodes
x = cos(theta);
y = sin(theta);
% PLOT BEZIER CURVES
% Calculate Bx and By positions of quadratic Bezier curves with P1 at (0,0)
% B(t) = (1-t)^2*P0 + t^2*P2 where t is a vector of points in [0, 1] and determines, i.e.
% how many points are used for each curve, and P0-P2 is the node pair with (x,y) coordinates.
t2 = [1-t, t].^2;
s.l = NaN(N2,1);
% LOOP per color bucket
for c = 1:N2
pos = plotorder(c);
idx = isrt == pos;
if nnz(idx)
Bx = [t2*[x(col(idx)); x(row(idx))]; NaN(1,n(pos))];
By = [t2*[y(col(idx)); y(row(idx))]; NaN(1,n(pos))];
s.l(c) = plot(Bx(:),By(:),'Color',ccolor(pos,:),'LineWidth',1);
end
end
% PLOT NODES
% Do not rely that r is symmetric and base the mean on lower triangular part only
[row,col] = find(tf(end:-1:1,end:-1:1) | tf);
subs = col;
iswap = row < col;
tmp = row(iswap);
row(iswap) = col(iswap);
col(iswap) = tmp;
% Plot in brighter color those nodes with larger within connections
[Z,isrt] = sort(diag(r));
Z = (Z-min(Z)+0.01)/(max(Z)-min(Z)+0.01);
ncolor = hsv2rgb([repmat(ncolor(1:2), sz(1),1) Z*ncolor(3)]);
s.s = scatter(x(isrt),y(isrt),round(Z*48), ncolor,'fill');
% PLACE TEXT LABELS such that you always read 'left to right'
ipos = x > 0;
s.t = zeros(sz(1),1);
s.t( ipos) = text(x( ipos), y( ipos), lbls( ipos),'FontSize',8,'Color',tcolor);
set(s.t( ipos),{'Rotation'}, num2cell(theta(ipos)'/tau*360))
s.t(~ipos) = text(x(~ipos), y(~ipos), lbls(~ipos),'FontSize',8,'Color',tcolor);
set(s.t(~ipos),{'Rotation'}, num2cell(theta(~ipos)'/tau*360-180),'Horiz','right')
% ADJUST FIGURE height width to fit text labels
xtn = cell2mat(get(s.t,'extent'));
post = cell2mat(get(s.t,'pos'));
sg = sign(post(:,2));
posfa = cell2mat(get([gcf gca],'pos'));
% Calculate xlim and ylim in data units as x (y) position + extension along x (y)
ylims = post(:,2) + xtn(:,4).*sg;
ylims = [min(ylims), max(ylims)];
xlims = post(:,1) + xtn(:,3).*sg;
xlims = [min(xlims), max(xlims)];
% Stretch figure
posfa(1,3) = (( diff(xlims)/2 - 1)*posfa(2,3) + 1) * posfa(1,3);
posfa(1,4) = (( diff(ylims)/2 - 1)*posfa(2,4) + 1) * posfa(1,4);
% Position it a bit lower (movegui slow)
posfa(1,2) = 100;
% Axis settings
set(gca, 'Xlim',xlims,'Ylim',ylims, 'XColor','none','YColor','none',...
'clim',[min(r(tf)),max(r(tf))])
set(gcf, 'pos' ,posfa(1,:),'Visible','on')
axis equal
% Set colormap
colormap(gca,ccolor);
if nargout == 1
h = s;
end
end