Adding flair to boring Matlab Axes one plot at a time
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

426 lines
11 KiB

  1. function xkcdify(axesList, renderAxesLines)
  2. %XKCDIFY redraw an existing axes in an XKCD style
  3. %
  4. % XKCDIFY( AXES ) re-renders all childen of AXES to have a hand drawn
  5. % XKCD style, http://xkcd.com, AXES can be a single axes or a vector of axes
  6. %
  7. % NOTE: Only plots of type LINE and PATCH are re-rendered. This should
  8. % be sufficient for the majority of 2d plots such as:
  9. % - plot
  10. % - bar
  11. % - boxplot
  12. % - etc...
  13. %
  14. % NOTE: This function does not alter the actual style of the axes
  15. % themselves, that functionality will be added in the next version. I
  16. % still have to figure out the best way to do this, if you have a
  17. % suggestion please email me!
  18. %
  19. % Finally the most up to date version of this code can be found at:
  20. % https://github.com/slayton/matlab-xkcdify
  21. %
  22. % Copyright(c) 2012, Stuart P. Layton <stuart.layton@gmail.com> MIT
  23. % http://stuartlayton.com
  24. % Revision History
  25. % 2012/10/04 - Initial Release
  26. if nargin==0
  27. error('axHandle must be specified');
  28. elseif ~all( ishandle(axesList) )
  29. error('axHandle must be a valid axes handle');
  30. elseif ~all( strcmp( get(axesList, 'type'), 'axes') )
  31. error('axHandle must be a valid axes handle');
  32. end
  33. if nargin==1
  34. renderAxesLines = 0;
  35. end
  36. for axN = 1:numel(axesList)
  37. axHandle = axesList(axN);
  38. pixPerX = [];
  39. pixPerY = [];
  40. axChildren = get(axHandle, 'Children');
  41. operateOnChildren(axChildren, axHandle);
  42. if renderAxesLines == 1
  43. renderNewAxesLine(axHandle)
  44. end
  45. end
  46. function renderNewAxesLine(ax)
  47. nPixOffset = 15;
  48. isBoxOn = strcmp( get(ax,'Box'), 'on' );
  49. set(ax,'Box', 'off');
  50. % Get the correct location for the next axes
  51. pos = getAxesPositionInUnits(ax,'Pixels');
  52. pos(1:2) = pos(1:2) - nPixOffset;
  53. if isBoxOn
  54. pos(3:4) = pos(3:4) + nPixOffset*2;
  55. else
  56. pos(3:4) = pos(3:4) + nPixOffset;
  57. end
  58. newAxes = axes('Units', 'pixels', 'Position', pos, 'Color', 'none');
  59. set(newAxes,'Units', get(ax,'Units'), 'XTick', [], 'YTick', []);
  60. [px, py] = getPixelsPerUnitForAxes(newAxes);
  61. dx = nPixOffset / px;
  62. dy = nPixOffset / py;
  63. xlim = get(newAxes,'XLim');
  64. ylim = get(newAxes, 'YLim');
  65. axArgs = {'Parent', newAxes, 'Color', 'k', 'LineWidth', 4};
  66. axLine(1) = line([dx dx], ylim + [dy -dy], axArgs{:});
  67. axLine(2) = line(xlim + [dx -dx], [dy dy], axArgs{:});
  68. %if 'Box' is on then draw the top and right edges of thea axes
  69. if isBoxOn
  70. axLine(3) = line(xlim(2) - [dx dx] + .00001, ylim + [dy -dy], axArgs{:});
  71. axLine(4) = line(xlim + [dx -dx], ylim(2) - [dy dy] + .00001, axArgs{:});
  72. end
  73. axis(newAxes, 'off');
  74. for i = 1:numel(axLine)
  75. cartoonifyAxesEdge(axLine(i), newAxes);
  76. end
  77. if any(strcmp(listfonts,'xkcd Script'))
  78. % prefer 'xkcd Script'
  79. % https://github.com/ipython/xkcd-font/blob/master/xkcd-script/font/xkcd-script.ttf
  80. set(ax, 'FontName', 'xkcd Script', 'FontSize', 14);
  81. elseif any(strcmp(listfonts,'Humor Sans'))
  82. % fall back on Humor Sans font
  83. % https://github.com/shreyankg/xkcd-desktop/blob/master/Humor-Sans.ttf
  84. set(ax, 'FontName', 'Humor Sans', 'FontSize', 14);
  85. else
  86. set(ax, 'FontName', 'Comic Sans MS', 'FontSize', 14);
  87. end
  88. end
  89. function operateOnChildren(C, ax)
  90. % iterate on the individual children but in reverse order
  91. % also ensure that C is treated as a row vector
  92. for c = fliplr( C(:)' )
  93. %for i = 1:nCh
  94. % we want to
  95. % c = C(nCh - i + 1);
  96. cType = get(c,'Type');
  97. switch cType
  98. case 'line'
  99. cartoonifyLine(c, ax);
  100. uistack(c,'top');
  101. case 'patch'
  102. cartoonifyPatch(c, ax);
  103. uistack(c,'top');
  104. case 'hggroup'
  105. % if not a line or patch operate on the children of the
  106. % hggroup child, plot-ception!
  107. operateOnChildren( get(c,'Children'), ax);
  108. uistack(c,'top');
  109. otherwise
  110. warning('Received unsupportd child of type %s', cType);
  111. end
  112. end
  113. end
  114. function cartoonifyLine(l, ax)
  115. if nargin==2
  116. addMask = 1;
  117. end
  118. xpts = get(l, 'XData')';
  119. ypts = get(l, 'YData')';
  120. %only jitter lines with more than 1 point
  121. if numel(xpts)>1
  122. [pixPerX, pixPerY] = getPixelsPerUnitForAxes(ax);
  123. % I should figure out a better way to calculate this
  124. xJitter = 6 / pixPerX;
  125. yJitter = 6 / pixPerY;
  126. if all( diff( ypts) == 0)
  127. % if the line is horizontal don't jitter in X
  128. xJitter = 0;
  129. elseif all( diff( xpts) == 0)
  130. % if the line is veritcal don't jitter in y
  131. yJitter = 0;
  132. end
  133. [xpts, ypts] = upSampleAndJitter(xpts, ypts, xJitter, yJitter);
  134. end
  135. set(l, 'XData', xpts , 'YData', ypts, 'linestyle', '-');
  136. addBackgroundMask(xpts, ypts, get(l, 'LineWidth') * 3, ax);
  137. end
  138. function cartoonifyAxesEdge(l, ax)
  139. xpts = get(l, 'XData')';
  140. ypts = get(l, 'YData')';
  141. %only jitter lines with more than 1 point
  142. if numel(xpts)>1
  143. [pixPerX, pixPerY] = getPixelsPerUnitForAxes(ax);
  144. % I should figure out a better way to calculate this
  145. xJitter = 3 / pixPerX;
  146. yJitter = 3 / pixPerY;
  147. if all(diff(ypts) == 0)
  148. % if the line is horizontal don't jitter in X
  149. xJitter = 0;
  150. elseif all(diff(xpts) == 0)
  151. % if the line is veritcal don't jitter in y
  152. yJitter = 0;
  153. end
  154. [xpts, ypts] = upSampleAndJitter(xpts, ypts, xJitter, yJitter);
  155. end
  156. set(l, 'XData', xpts , 'YData', ypts, 'linestyle', '-');
  157. end
  158. function [x, y] = upSampleAndJitter(x, y, jx, jy, n)
  159. % we want to upsample the line to have a number of that is proportional
  160. % to the number of pixels the line occupies on the screen. Long lines
  161. % will get a lot of samples, short points will get a few
  162. if nargin == 4 || n == 0
  163. n = getLineLength(x,y);
  164. ptsPerPix = 1/4;
  165. n = ceil(n * ptsPerPix);
  166. end
  167. x = interp1(linspace(0, 1, numel(x)), x, linspace(0, 1, n));
  168. y = interp1(linspace(0, 1, numel(y)), y, linspace(0, 1, n));
  169. x = x + smooth(generateNoise(n) .* rand(n,1) .* jx)';
  170. y = y + smooth(generateNoise(n) .* rand(n,1) .* jy)';
  171. end
  172. function noise = generateNoise(n)
  173. noise = zeros(n,1);
  174. iStart = ceil(n/50);
  175. iEnd = n - iStart;
  176. i = iStart;
  177. while i < iEnd
  178. if randi(10,1,1) < 2
  179. upDown = randsample([-1 1], 1);
  180. maxDur = max( min(iEnd - i, 100), 1);
  181. duration = randi( maxDur , 1, 1);
  182. noise(i:i+duration) = upDown;
  183. i = i + duration;
  184. end
  185. i = i +1;
  186. end
  187. noise = noise(:);
  188. end
  189. function addBackgroundMask(xpts, ypts, w, ax)
  190. bg = get(ax, 'color');
  191. line(xpts, ypts, 'linewidth', w, 'color', bg, 'Parent', ax);
  192. end
  193. function pos = getAxesPositionInUnits(ax, units)
  194. if strcmp(get(ax,'Units'), units)
  195. pos = get(ax,'Position');
  196. return;
  197. end
  198. % if the current axes contains a box plot then we need to create a
  199. % temporary axes as changing the units on a boxplot causes the
  200. % pos(4) to be set to 0
  201. axUserData = get(ax,'UserData');
  202. if ~isempty(axUserData) && iscell(axUserData) && strcmp(axUserData{1}, 'boxplot')
  203. axTemp = axes('Units','normalized','Position', get(ax,'Position'));
  204. set(axTemp,'Units', units);
  205. pos = get(axTemp,'position');
  206. delete(axTemp);
  207. else
  208. origUnits = get(ax,'Units');
  209. set(ax,'Units', 'pixels');
  210. pos = get(ax,'Position');
  211. set(ax,'Units', origUnits);
  212. end
  213. end
  214. function setAxesPositionInUnits(ax, pos, units)
  215. if strcmp(get(ax,'Units'), units)
  216. set(ax,'Position', pos);
  217. return;
  218. end
  219. % if the current axes contains a box plot then we need to create a
  220. % temporary axes as changing the units on a boxplot causes the
  221. % pos(4) to be set to 0
  222. axUserData = get(ax,'UserData');
  223. if ~isempty(axUserData) && iscell(axUserData) && strcmp(axUserData{1}, 'boxplot')
  224. axTemp = axes('Units', get(ax,'Units'), 'Position', get(ax,'Position'));
  225. origUnit = get(axTemp,'Units');
  226. set(axTemp,'Units', units);
  227. set(axTemp,'position', pos);
  228. set(axTemp, 'Units', origUnit);
  229. set(ax, 'Position', get(axTemp, 'Position'));
  230. delete(axTemp);
  231. else
  232. origUnits = get(ax,'Units');
  233. set(ax,'Units', units);
  234. set(ax,'Potision', pos);
  235. set(ax,'Units', origUnits);
  236. end
  237. end
  238. % Main function for converting units to pixels, refers to the main drawing
  239. % axes
  240. function [ppX, ppY] = getPixelsPerUnit()
  241. if ~isempty(pixPerX) && ~ isempty(pixPerY)
  242. ppX = pixPerX;
  243. ppY = pixPerY;
  244. return;
  245. end
  246. [ppX, ppY] = getPixelsPerUnitForAxes(axHandle);
  247. end
  248. % Worker function for converting units to pixels, can be used with any axes
  249. % allowing it to be used with subsequently created axes that are involved
  250. % in rendering the axes lines
  251. function [px, py] = getPixelsPerUnitForAxes(axH)
  252. %get the size of the current axes in pixels
  253. %get the lims of the current axes in plotting units
  254. %calculate the number of pixels per plotting unit
  255. pos = getAxesPositionInUnits(axH, 'Pixels');
  256. xLim = get(axH, 'XLim');
  257. yLim = get(axH, 'YLim');
  258. px = pos(3) ./ diff(xLim);
  259. py = pos(4) ./ diff(yLim);
  260. end
  261. function [ len ] = getLineLength(x, y)
  262. % convert x and y to pixels from units
  263. [pixPerX, pixPerY] = getPixelsPerUnit();
  264. x = x(:) * pixPerX;
  265. y = y(:) * pixPerY;
  266. %compute the length of the line
  267. len = sum(sqrt(diff(x).^2 + diff(y).^2));
  268. end
  269. function v = smooth(v)
  270. % these values are pretty arbitrary, i should probably come up with a
  271. % better way to calculate them from the data
  272. a = 1/2;
  273. nPad = 10;
  274. % filter the yValues to smooth the jitter
  275. v = filtfilt(a, [1 a-1], [ ones(nPad ,1) * v(1); v; ones(nPad,1) * v(end) ]);
  276. v = filtfilt(a, [1 a-1], v);
  277. v = v(nPad+1:end-nPad);
  278. v = v(:);
  279. end
  280. % This method is by far the buggiest part of the script. It appears to work,
  281. % however it fails to retain the original color of the patch, and sets it to
  282. % blue. This doesn't prevent the user from reseting the color after the
  283. % fact using set(barHandle, 'FaceColor', color) which IMHO is an acceptable
  284. % workaround
  285. function cartoonifyPatch(p, ax)
  286. xPts = get(p, 'XData');
  287. yPts = get(p, 'YData');
  288. cData = get(p, 'CData');
  289. nOld = size(xPts,1);
  290. xNew = [];
  291. yNew = [];
  292. cNew = [];
  293. oldVtx = get(p, 'Vertices');
  294. oldVtxNorm = get(p, 'VertexNormals');
  295. nPatch = size(xPts, 2);
  296. nVtx = size(oldVtx,1);
  297. newVtx = [];
  298. newVtxNorm = [];
  299. [pixPerX, pixPerY] = getPixelsPerUnit();
  300. xJitter = 6 / pixPerX;
  301. yJitter = 6 / pixPerY;
  302. nNew = 0;
  303. cNew = [];
  304. for i = 1:nPatch
  305. %newVtx( end+1,:) = oldVtx( 1 + (i-1)*nOld , : );
  306. [x, y] = upSampleAndJitter(xPts(:,i), yPts(:,i), xJitter, yJitter, nNew);
  307. xNew(:,i) = x(:);
  308. yNew(:,i) = y(:);
  309. nNew = numel(x);
  310. if ~isempty(cData)
  311. cNew(:,i) = interp1(linspace(0, 1, nOld), cData(:,i), linspace(0, 1, nNew));
  312. end
  313. newVtx(end+1,1:2) = oldVtx(1+(i-1)*(nOld+1), 1:2);
  314. newVtxNorm(end+1, 1:3) = nan;
  315. % set the first and last vertex for each bar back in its original
  316. % position so everything lines up
  317. yNew([1, end], i) = yPts([1,end],i);
  318. xNew([1, end], i) = xPts([1,end],i);
  319. newVtx(end+(1:nNew), :) = [xNew(:,i), yNew(:,i)] ;
  320. t = repmat(oldVtxNorm(1+1+(i-1)*(nOld+1), :), nNew, 1);
  321. newVtxNorm(end+(1:nNew), :) = t;
  322. addBackgroundMask(xNew(:,i), yNew(:,i), 6, ax);
  323. end
  324. newVtx(end+1, :) = oldVtx(end,:);
  325. newVtxNorm(end+1, :) = nan;
  326. % construct the new vertex data
  327. newFaces = true(size(newVtx,1),1);
  328. newFaces(1:nNew+1:end) = false;
  329. newFaces = find(newFaces);
  330. newFaces = reshape(newFaces, nNew, nPatch)';
  331. % I can't seem to get this working correct, so I'll set the color to
  332. % the default matlab blue not the same as 'color', 'blue'!
  333. newFaceVtxCData = [ 0 0 .5608 ];
  334. set(p, 'CData', cNew, 'FaceVertexCData', newFaceVtxCData, 'Faces', newFaces, ...
  335. 'Vertices', newVtx, 'XData', xNew, 'YData', yNew, 'VertexNormals', newVtxNorm);
  336. %set(p, 'EdgeColor', 'none');
  337. end
  338. end