Skip to content

Instantly share code, notes, and snippets.

@neocsr
Created October 9, 2012 06:12
Show Gist options
  • Save neocsr/3856939 to your computer and use it in GitHub Desktop.
Save neocsr/3856939 to your computer and use it in GitHub Desktop.
XKCDIFY! Redraw existing Matlab axes in an XKCD style
function xkcdify(axHandle)
%XKCDIFY redraw an existing axes in an XKCD style
%
% XKCDIFY( AXES ) re-renders all childen of AXES to have a hand drawn
% XKCD style, http://xkcd.com, AXES can be a single axes or a vector of axes
%
% NOTE: Only plots of type LINE and PATCH are re-rendered. This should
% be sufficient for the majority of 2d plots such as:
% - plot
% - bar
% - boxplot
% - etc...
%
% NOTE: This function does not alter the actual style of the axes
% themselves, that functionality will be added in the next version. I
% still have to figure out the best way to do this, if you have a
% suggestion please email me!
%
% Finally the most up to date version of this code can be found at:
% https://github.com/slayton/matlab-xkcdify
%
% Copyright(c) 2012, Stuart P. Layton <stuart.layton@gmail.com> MIT
% http://stuartlayton.com
% Revision History
% 2012/10/04 - Initial Release
if nargin==0
error('axHandle must be specified');
end
for axN = 1:numel(axHandle)
ax = axHandle(axN);
pixPerX = [];
pixPerY = [];
axChildren = get(ax, 'Children');
operate_on_children(axChildren, ax);
end
function operate_on_children(C, ax)
% iterate on the individual children but in reverse order
% also ensure that C is treated as a row vector
for c = fliplr( C(:)' )
%for i = 1:nCh
% we want to
% c = C(nCh - i + 1);
cType = get(c,'Type');
switch cType
case 'line'
cartoonify_line(c, ax);
uistack(c,'top');
case 'patch'
cartoonify_patch(c, ax);
uistack(c,'top');
case 'hggroup'
% if not a line or patch operate on the children of the
% hggroup child, plot-ception!
operate_on_children( get(c,'Children'), ax);
uistack(c,'top');
otherwise
warning('Received unsupportd child of type %s', cType);
end
end
end
function cartoonify_line(l, ax)
xpts = get(l, 'XData')';
ypts = get(l, 'YData')';
%only jitter lines with more than 1 point
if numel(xpts)>1
[pixPerX, pixPerY] = getPixelsPerUnit();
% I should figure out a better way to calculate this
xJitter = 6 / pixPerX;
yJitter = 6 / pixPerY;
if all( diff( ypts) == 0)
% if the line is horizontal don't jitter in X
xJitter = 0;
elseif all( diff( xpts) == 0)
% if the line is veritcal don't jitter in y
yJitter = 0;
end
[xpts, ypts] = up_sample_and_jitter(xpts, ypts, xJitter, yJitter);
end
set(l, 'XData', xpts , 'YData', ypts, 'linestyle', '-');
add_background_mask(xpts, ypts, get(l, 'LineWidth') * 3, ax);
end
function [x, y] = up_sample_and_jitter(x, y, jx, jy, n)
% we want to upsample the line to have a number of that is proportional
% to the number of pixels the line occupies on the screen. Long lines
% will get a lot of samples, short points will get a few
if nargin == 4 || n == 0
n = getLineLength(x,y);
ptsPerPix = 1/4;
n = ceil( n * ptsPerPix);
end
x = interp1( linspace(0, 1, numel(x)) , x, linspace(0, 1, n) );
y = interp1( linspace(0, 1, numel(y)) , y, linspace(0, 1, n) );
x = x + smooth( generateNoise(n) .* rand(n,1) .* jx )';
y = y + smooth( generateNoise(n) .* rand(n,1) .* jy )';
end
function noise = generateNoise(n)
noise = zeros(n,1);
iStart = ceil(n/50);
iEnd = n - iStart;
i = iStart;
while i < iEnd
if randi(10,1,1) < 2
upDown = randsample([-1 1], 1);
maxDur = max( min(iEnd - i, 100), 1);
duration = randi( maxDur , 1, 1);
noise(i:i+duration) = upDown;
i = i + duration;
end
i = i +1;
end
noise = noise(:);
end
function add_background_mask(xpts, ypts, w, ax)
bg = get(ax, 'color');
line(xpts, ypts, 'linewidth', w, 'color', bg, 'Parent', ax);
end
function [ppX ppY] = getPixelsPerUnit()
if ~isempty(pixPerX) && ~ isempty(pixPerY)
ppX = pixPerX;
ppY = pixPerY;
return;
end
%get the size of the current axes in pixels
%get the lims of the current axes in plotting units
%calculate the number of pixels per plotting unit
% if the current axes contains a box plot then we need to create a
% temporary axes as changing the units on a boxplot causes the
% pos(4) to be set to 0
axUserData = get(ax,'UserData');
if ~isempty(axUserData) && iscell(axUserData) && strcmp(axUserData{1}, 'boxplot')
axTemp = axes('Units','normalized','Position', get(ax,'Position'));
set(axTemp,'Units', 'pixels');
pos = get(axTemp,'position');
delete(axTemp);
else
units = get(ax,'Units');
set(ax,'Units', 'pixels');
pos = get(ax,'Position');
set(ax,'Units', units);
end
xLim = get(ax, 'XLim');
yLim = get(ax, 'YLim');
ppX = pos(3) ./ diff(xLim);
ppY = pos(4) ./ diff(yLim);
end
function [ len ] = getLineLength(x, y)
% convert x and y to pixels from units
[pixPerX, pixPerY] = getPixelsPerUnit();
x = x(:) * pixPerX;
y = y(:) * pixPerY;
%compute the length of the line
len = sum( sqrt( diff( x ).^2 + diff( y ).^2 ) );
end
function v = smooth(v)
% these values are pretty arbitrary, i should probably come up with a
% better way to calculate them from the data
a = 1/2;
nPad = 10;
% filter the yValues to smooth the jitter
v = filtfilt(a, [1 a-1], [ ones(nPad ,1) * v(1); v; ones(nPad,1) * v(end) ]);
v = filtfilt(a, [1 a-1], v);
v = v(nPad+1:end-nPad);
v = v(:);
end
% This method is by far the buggiest part of the script. It appears to work,
% however it fails to retain the original color of the patch, and sets it to
% blue. This doesn't prevent the user from reseting the color after the
% fact using set(barHandle, 'FaceColor', color) which IMHO is an acceptable
% workaround
function cartoonify_patch(p, ax)
xPts = get(p, 'XData');
yPts = get(p, 'YData');
cData = get(p, 'CData');
nOld = size(xPts,1);
xNew = [];
yNew = [];
cNew = [];
oldVtx = get(p, 'Vertices');
oldVtxNorm = get(p, 'VertexNormals');
nPatch = size(xPts, 2);
nVtx = size(oldVtx,1);
newVtx = [];
newVtxNorm = [];
[pixPerX, pixPerY] = getPixelsPerUnit();
xJitter = 6 / pixPerX;
yJitter = 6 / pixPerY;
nNew = 0;
for i = 1:nPatch
%newVtx( end+1,:) = oldVtx( 1 + (i-1)*nOld , : );
[x, y] = up_sample_and_jitter(xPts(:,i), yPts(:,i), xJitter, yJitter, nNew);
xNew(:,i) = x(:);
yNew(:,i) = y(:);
nNew = numel(x);
cNew(:,i) = interp1( linspace( 0 , 1, nOld), cData(:,i), linspace(0, 1, nNew));
newVtx(end+1,1:2) = oldVtx( 1 + (i-1)*(nOld+1), 1:2);
newVtxNorm( end+1, 1:3) = nan;
% set the first and last vertex for each bar back in its original
% position so everything lines up
yNew([1, end], i) = yPts([1,end],i);
xNew([1, end], i) = xPts([1,end],i);
newVtx(end + (1:nNew), :) = [xNew(:,i), yNew(:,i)] ;
t = repmat( oldVtxNorm( 1+1 + (i-1)*(nOld+1) , : ), nNew, 1);
newVtxNorm( end+ (1 : nNew) , : ) = t;
add_background_mask(xNew(:,i), yNew(:,i), 6, ax);
end
newVtx(end+1, :) = oldVtx(end,:);
newVtxNorm(end+1, : ) = nan;
% construct the new vertex data
newFaces = true(size(newVtx,1),1);
newFaces(1:nNew+1:end) = false;
newFaces = find(newFaces);
newFaces = reshape(newFaces, nNew, nPatch)';
% I can't seem to get this working correct, so I'll set the color to
% the default matlab blue not the same as 'color', 'blue'!
newFaceVtxCData = [ 0 0 .5608 ];
set(p, 'CData', cNew, 'FaceVertexCData', newFaceVtxCData, 'Faces', newFaces, ...
'Vertices', newVtx, 'XData', xNew, 'YData', yNew, 'VertexNormals', newVtxNorm);
set(p, 'EdgeColor', 'none');
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment