Robochameleon  v1.0
progressbar.m
1 function progressbar(varargin)
2 % Description:
3 % progressbar() provides an indication of the progress of some task using
4 % graphics and text. Calling progressbar repeatedly will update the figure and
5 % automatically estimate the amount of time remaining.
6 % This implementation of progressbar is intended to be extremely simple to use
7 % while providing a high quality user experience.
8 %
9 % Features:
10 % - Can add progressbar to existing m-files with a single line of code.
11 % - Supports multiple bars in one figure to show progress of nested loops.
12 % - Optional labels on bars.
13 % - Figure closes automatically when task is complete.
14 % - Only one figure can exist so old figures don't clutter the desktop.
15 % - Remaining time estimate is accurate even if the figure gets closed.
16 % - Minimal execution time. Won't slow down code.
17 % - Randomized color. When a programmer gets bored...
18 %
19 % Example Function Calls For Single Bar Usage:
20 % progressbar % Initialize/reset
21 % progressbar(0) % Initialize/reset
22 % progressbar('Label') % Initialize/reset and label the bar
23 % progressbar(0.5) % Update
24 % progressbar(1) % Close
25 %
26 % Example Function Calls For Multi Bar Usage:
27 % progressbar(0, 0) % Initialize/reset two bars
28 % progressbar('A', '') % Initialize/reset two bars with one label
29 % progressbar('', 'B') % Initialize/reset two bars with one label
30 % progressbar('A', 'B') % Initialize/reset two bars with two labels
31 % progressbar(0.3) % Update 1st bar
32 % progressbar(0.3, []) % Update 1st bar
33 % progressbar([], 0.3) % Update 2nd bar
34 % progressbar(0.7, 0.9) % Update both bars
35 % progressbar(1) % Close
36 % progressbar(1, []) % Close
37 % progressbar(1, 0.4) % Close
38 %
39 % Notes:
40 % For best results, call progressbar with all zero (or all string) inputs
41 % before any processing. This sets the proper starting time reference to
42 % calculate time remaining.
43 % Bar color is choosen randomly when the figure is created or reset. Clicking
44 % the bar will cause a random color change.
45 %
46 % Demos:
47 % % Single bar
48 % m = 500;
49 % progressbar % Init single bar
50 % for i = 1:m
51 % pause(0.01) % Do something important
52 % progressbar(i/m) % Update progress bar
53 % end
54 %
55 % % Simple multi bar (update one bar at a time)
56 % m = 4;
57 % n = 3;
58 % p = 100;
59 % progressbar(0,0,0) % Init 3 bars
60 % for i = 1:m
61 % progressbar([],0) % Reset 2nd bar
62 % for j = 1:n
63 % progressbar([],[],0) % Reset 3rd bar
64 % for k = 1:p
65 % pause(0.01) % Do something important
66 % progressbar([],[],k/p) % Update 3rd bar
67 % end
68 % progressbar([],j/n) % Update 2nd bar
69 % end
70 % progressbar(i/m) % Update 1st bar
71 % end
72 %
73 % % Fancy multi bar (use labels and update all bars at once)
74 % m = 4;
75 % n = 3;
76 % p = 100;
77 % progressbar('Monte Carlo Trials','Simulation','Component') % Init 3 bars
78 % for i = 1:m
79 % for j = 1:n
80 % for k = 1:p
81 % pause(0.01) % Do something important
82 % % Update all bars
83 % frac3 = k/p;
84 % frac2 = ((j-1) + frac3) / n;
85 % frac1 = ((i-1) + frac2) / m;
86 % progressbar(frac1, frac2, frac3)
87 % end
88 % end
89 % end
90 %
91 % Author:
92 % Steve Hoelzer
93 %
94 % Revisions:
95 % 2002-Feb-27 Created function
96 % 2002-Mar-19 Updated title text order
97 % 2002-Apr-11 Use floor instead of round for percentdone
98 % 2002-Jun-06 Updated for speed using patch (Thanks to waitbar.m)
99 % 2002-Jun-19 Choose random patch color when a new figure is created
100 % 2002-Jun-24 Click on bar or axes to choose new random color
101 % 2002-Jun-27 Calc time left, reset progress bar when fractiondone == 0
102 % 2002-Jun-28 Remove extraText var, add position var
103 % 2002-Jul-18 fractiondone input is optional
104 % 2002-Jul-19 Allow position to specify screen coordinates
105 % 2002-Jul-22 Clear vars used in color change callback routine
106 % 2002-Jul-29 Position input is always specified in pixels
107 % 2002-Sep-09 Change order of title bar text
108 % 2003-Jun-13 Change 'min' to 'm' because of built in function 'min'
109 % 2003-Sep-08 Use callback for changing color instead of string
110 % 2003-Sep-10 Use persistent vars for speed, modify titlebarstr
111 % 2003-Sep-25 Correct titlebarstr for 0% case
112 % 2003-Nov-25 Clear all persistent vars when percentdone = 100
113 % 2004-Jan-22 Cleaner reset process, don't create figure if percentdone = 100
114 % 2004-Jan-27 Handle incorrect position input
115 % 2004-Feb-16 Minimum time interval between updates
116 % 2004-Apr-01 Cleaner process of enforcing minimum time interval
117 % 2004-Oct-08 Seperate function for timeleftstr, expand to include days
118 % 2004-Oct-20 Efficient if-else structure for sec2timestr
119 % 2006-Sep-11 Width is a multiple of height (don't stretch on widescreens)
120 % 2010-Sep-21 Major overhaul to support multiple bars and add labels
121 %
122 
123 persistent progfig progdata lastupdate
124 
125 % Get inputs
126 if nargin > 0
127  input = varargin;
128  ninput = nargin;
129 else
130  % If no inputs, init with a single bar
131  input = {0};
132  ninput = 1;
133 end
134 
135 % If task completed, close figure and clear vars, then exit
136 if input{1} == 1
137  if ishandle(progfig)
138  delete(progfig) % Close progress bar
139  end
140  clear progfig progdata lastupdate % Clear persistent vars
141  drawnow
142  return
143 end
144 
145 % Init reset flag
146 resetflag = false;
147 
148 % Set reset flag if first input is a string
149 if ischar(input{1})
150  resetflag = true;
151 end
152 
153 % Set reset flag if all inputs are zero
154 if input{1} == 0
155  % If the quick check above passes, need to check all inputs
156  if all([input{:}] == 0) && (length([input{:}]) == ninput)
157  resetflag = true;
158  end
159 end
160 
161 % Set reset flag if more inputs than bars
162 if ninput > length(progdata)
163  resetflag = true;
164 end
165 
166 % If reset needed, close figure and forget old data
167 if resetflag
168  if ishandle(progfig)
169  delete(progfig) % Close progress bar
170  end
171  progfig = [];
172  progdata = []; % Forget obsolete data
173 end
174 
175 % Create new progress bar if needed
176 if ishandle(progfig)
177 else % This strange if-else works when progfig is empty (~ishandle() does not)
178 
179  % Define figure size and axes padding for the single bar case
180  height = 0.03;
181  width = height * 8;
182  hpad = 0.02;
183  vpad = 0.25;
184 
185  % Figure out how many bars to draw
186  nbars = max(ninput, length(progdata));
187 
188  % Adjust figure size and axes padding for number of bars
189  heightfactor = (1 - vpad) * nbars + vpad;
190  height = height * heightfactor;
191  vpad = vpad / heightfactor;
192 
193  % Initialize progress bar figure
194  left = (1 - width) / 2;
195  bottom = (1 - height) / 2;
196  progfig = figure(...
197  'Units', 'normalized',...
198  'Position', [left bottom width height],...
199  'NumberTitle', 'off',...
200  'Resize', 'off',...
201  'MenuBar', 'none' );
202 
203  % Initialize axes, patch, and text for each bar
204  left = hpad;
205  width = 1 - 2*hpad;
206  vpadtotal = vpad * (nbars + 1);
207  height = (1 - vpadtotal) / nbars;
208  for ndx = 1:nbars
209  % Create axes, patch, and text
210  bottom = vpad + (vpad + height) * (nbars - ndx);
211  progdata(ndx).progaxes = axes( ...
212  'Position', [left bottom width height], ...
213  'XLim', [0 1], ...
214  'YLim', [0 1], ...
215  'Box', 'on', ...
216  'ytick', [], ...
217  'xtick', [] );
218  progdata(ndx).progpatch = patch( ...
219  'XData', [0 0 0 0], ...
220  'YData', [0 0 1 1] );
221  progdata(ndx).progtext = text(0.99, 0.5, '', ...
222  'HorizontalAlignment', 'Right', ...
223  'FontUnits', 'Normalized', ...
224  'FontSize', 0.7 );
225  progdata(ndx).proglabel = text(0.01, 0.5, '', ...
226  'HorizontalAlignment', 'Left', ...
227  'FontUnits', 'Normalized', ...
228  'FontSize', 0.7 );
229  if ischar(input{ndx})
230  set(progdata(ndx).proglabel, 'String', input{ndx})
231  input{ndx} = 0;
232  end
233 
234  % Set callbacks to change color on mouse click
235  set(progdata(ndx).progaxes, 'ButtonDownFcn', {@changecolor, progdata(ndx).progpatch})
236  set(progdata(ndx).progpatch, 'ButtonDownFcn', {@changecolor, progdata(ndx).progpatch})
237  set(progdata(ndx).progtext, 'ButtonDownFcn', {@changecolor, progdata(ndx).progpatch})
238  set(progdata(ndx).proglabel, 'ButtonDownFcn', {@changecolor, progdata(ndx).progpatch})
239 
240  % Pick a random color for this patch
241  changecolor([], [], progdata(ndx).progpatch)
242 
243  % Set starting time reference
244  if ~isfield(progdata(ndx), 'starttime') || isempty(progdata(ndx).starttime)
245  progdata(ndx).starttime = clock;
246  end
247  end
248 
249  % Set time of last update to ensure a redraw
250  lastupdate = clock - 1;
251 
252 end
253 
254 % Process inputs and update state of progdata
255 for ndx = 1:ninput
256  if ~isempty(input{ndx})
257  progdata(ndx).fractiondone = input{ndx};
258  progdata(ndx).clock = clock;
259  end
260 end
261 
262 % Enforce a minimum time interval between graphics updates
263 myclock = clock;
264 if abs(myclock(6) - lastupdate(6)) < 0.01 % Could use etime() but this is faster
265  return
266 end
267 
268 % Update progress patch
269 for ndx = 1:length(progdata)
270  set(progdata(ndx).progpatch, 'XData', ...
271  [0, progdata(ndx).fractiondone, progdata(ndx).fractiondone, 0])
272 end
273 
274 % Update progress text if there is more than one bar
275 if length(progdata) > 1
276  for ndx = 1:length(progdata)
277  set(progdata(ndx).progtext, 'String', ...
278  sprintf('%1d%%', floor(100*progdata(ndx).fractiondone)))
279  end
280 end
281 
282 % Update progress figure title bar
283 if progdata(1).fractiondone > 0
284  runtime = etime(progdata(1).clock, progdata(1).starttime);
285  timeleft = runtime / progdata(1).fractiondone - runtime;
286  timeleftstr = sec2timestr(timeleft);
287  titlebarstr = sprintf('%2d%% %s remaining', ...
288  floor(100*progdata(1).fractiondone), timeleftstr);
289 else
290  titlebarstr = ' 0%';
291 end
292 set(progfig, 'Name', titlebarstr)
293 
294 % Force redraw to show changes
295 drawnow
296 
297 % Record time of this update
298 lastupdate = clock;
299 
300 
301 % ------------------------------------------------------------------------------
302 function changecolor(h, e, progpatch) %#ok<INUSL>
303 % Change the color of the progress bar patch
304 
305 % Prevent color from being too dark or too light
306 colormin = 1.5;
307 colormax = 2.8;
308 
309 thiscolor = rand(1, 3);
310 while (sum(thiscolor) < colormin) || (sum(thiscolor) > colormax)
311  thiscolor = rand(1, 3);
312 end
313 
314 set(progpatch, 'FaceColor', thiscolor)
315 
316 
317 % ------------------------------------------------------------------------------
318 function timestr = sec2timestr(sec)
319 % Convert a time measurement from seconds into a human readable string.
320 
321 % Convert seconds to other units
322 w = floor(sec/604800); % Weeks
323 sec = sec - w*604800;
324 d = floor(sec/86400); % Days
325 sec = sec - d*86400;
326 h = floor(sec/3600); % Hours
327 sec = sec - h*3600;
328 m = floor(sec/60); % Minutes
329 sec = sec - m*60;
330 s = floor(sec); % Seconds
331 
332 % Create time string
333 if w > 0
334  if w > 9
335  timestr = sprintf('%d week', w);
336  else
337  timestr = sprintf('%d week, %d day', w, d);
338  end
339 elseif d > 0
340  if d > 9
341  timestr = sprintf('%d day', d);
342  else
343  timestr = sprintf('%d day, %d hr', d, h);
344  end
345 elseif h > 0
346  if h > 9
347  timestr = sprintf('%d hr', h);
348  else
349  timestr = sprintf('%d hr, %d min', h, m);
350  end
351 elseif m > 0
352  if m > 9
353  timestr = sprintf('%d min', m);
354  else
355  timestr = sprintf('%d min, %d sec', m, s);
356  end
357 else
358  timestr = sprintf('%d sec', s);
359 end