Mercurial > hg > camir-aes2014
comparison toolboxes/graph_visualisation/graphViz4Matlab/graphViz4Matlab.m @ 0:e9a9cd732c1e tip
first hg version after svn
author | wolffd |
---|---|
date | Tue, 10 Feb 2015 15:05:51 +0000 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:e9a9cd732c1e |
---|---|
1 classdef graphViz4Matlab < handle | |
2 % Visualize a graph in a Matlab figure window by specifying an | |
3 % adjacency matrix and optionally node labels, descriptions, colors and the | |
4 % the layout algorithm. The best layout algorithms require that graphViz be | |
5 % installed, available free at <http://www.graphviz.org>. | |
6 % | |
7 % Matthew Dunham | |
8 % University of British Columbia | |
9 % Last Updated: April 24, 2010 | |
10 % Requires Matlab version 2008a or newer. | |
11 % | |
12 % Syntax (see more examples below): | |
13 % graphViz4Matlab('-adjMat',adj,'-nodeLabels',labels,'-nodeColors',colors); | |
14 % | |
15 % Self loops are removed and not represented on the graph. | |
16 % | |
17 % Once the graph is displayed, there are several operations you can perform | |
18 % with the mouse. | |
19 % (1) Move a single node around. | |
20 % (2) Draw a mouse box around several nodes and move them all together. | |
21 % (3) Enter a description for a node by double clicking it. | |
22 % (4) Shade the node by right clicking it | |
23 % (5) Display a node's properties on the console by shift clicking it. | |
24 % (6) Increase or decrease the font size | |
25 % (7) Increase or decrease the node size | |
26 % (8) Tighten the axes and relax the square aspect ratio. | |
27 % (9) Ask the current layout algorithm to layout the nodes as though the | |
28 % arrows were pointing the other way. This only affects some of the | |
29 % layouts. | |
30 % (10) Change the layout algorithm and refresh the graph | |
31 % | |
32 % Additionally, any operation you could do with a regular Matlab figure can | |
33 % be done here, e.g. annotating or saving as a pdf. | |
34 % | |
35 % Options are specified via name value pairs in any order. | |
36 % [] denote defaults. | |
37 % | |
38 % '-adjMat' [example matrix] The adjacency matrix | |
39 % | |
40 % '-layout' [Gvizlayout if graphViz installed, else Gridlayout] | |
41 % A layout object, i.e. Gvizlayout | Gridlayout | Circlelayout | |
42 % (See knownLayouts Property) | |
43 % | |
44 % '-nodeLabels' ['1':'n'] A cell array of labels for the nodes | |
45 % | |
46 % '-nodeDescriptions' [{}] Longer descriptions for the nodes, displayed when | |
47 % double clicking on a node. | |
48 % | |
49 % '-nodeColors' ['c'] A cell array or n-by-3 matrix specifying colors | |
50 % for the nodes. If fewer colors than nodes are specified, | |
51 % the specified colors are reused and cycled through. | |
52 % | |
53 % '-undirected' [false] If true, no arrows are displayed. | |
54 % | |
55 % '-edgeColors' [] An n-by-3 cell array listing | |
56 % {fromName,toName,color} for each row. You can | |
57 % list only the n < numel(edges) edges you want to | |
58 % color. If you do not label the nodes, graphViz4Matlab | |
59 % uses '1','2','3', etc, in which case use these. | |
60 % You can specify the text 'all' in place of toName, | |
61 % to mean all nodes, i.e. {fromName,'all',color} | |
62 % | |
63 % | |
64 % '-splitLabels' [true] If true, long node labels are split into | |
65 % several rows | |
66 % | |
67 % '-doubleClickFn' (by default, double clicking a node brings up | |
68 % an edit box for the node's description, but you | |
69 % can pass in a custom function handle. The function | |
70 % gets passed the node's label. | |
71 % Examples: | |
72 % | |
73 % adj = rand(5,5) > 0.8; | |
74 % labels = {'First','Second','Third','Fourth','Fifth'}; | |
75 % colors = {'g','b'}; % will cycle through | |
76 % s = graphViz4Matlab('-adjMat',adj,'-nodeLabels',labels,'-nodeColors',colors); | |
77 % freeze(s); % convert to an image | |
78 % | |
79 % If you are only specifying an adjacency matrix, you can omit the | |
80 % '-adjMat' name as in graphViz4Matlab(adj). | |
81 % | |
82 % Calling graphViz4Matlab without any parameters displays an example graph. | |
83 % | |
84 | |
85 | |
86 properties(GetAccess = 'public', SetAccess = 'private') | |
87 % read only | |
88 path = addpath(genpath(fileparts(which(mfilename)))); % automatically adds subdirectories to path | |
89 graphVizPath = setupPath(); | |
90 nnodes = 0; % The number of nodes | |
91 nedges = 0; % The number of edges | |
92 currentLayout= []; % The current layout object | |
93 layouts = []; % List currently added layout objects | |
94 adjMatrix = []; % The adjacency matrix | |
95 isvisible = false; % True iff the graph is being displayed | |
96 nodeArray = []; % The nodes | |
97 edgeArray = []; % The edges | |
98 fig = []; % The main window | |
99 ax = []; % The main axes | |
100 doubleClickFn = []; % function to execute when a user double clicks on a node, (must be a function handle that takes in the node name | |
101 selectedNode = []; % The selected node, if any | |
102 minNodeSize = []; % A minimum size for the nodes | |
103 maxNodeSize = []; % A maximum size for the nodes | |
104 undirected = false; % If undirected, arrows are not displayed | |
105 flipped = false; % If true, layout is done as though edge directions were reversed. | |
106 % (does not affect the logical layout). | |
107 knownLayouts = {Gvizlayout ,... % add your own layout here or use | |
108 Treelayout ,... % the addLayout() method | |
109 Radiallayout,... | |
110 Circularlayout,... | |
111 Springlayout,... | |
112 Circlelayout,... | |
113 Gridlayout ,... | |
114 Randlayout }; | |
115 defaultEdgeColor = [0,0,0];%[20,43,140]/255; | |
116 edgeColors; | |
117 square = true; % amounts to a the call "axis square" | |
118 splitLabels = true; | |
119 end | |
120 | |
121 properties(GetAccess = 'private', SetAccess = 'private') | |
122 % These values store the initial values not the current ones. | |
123 nodeLabels = {}; | |
124 nodeDescriptions = {}; | |
125 nodeColors = {}; | |
126 end | |
127 | |
128 properties(GetAccess = 'protected',SetAccess = 'protected') | |
129 toolbar; % The button toolbar | |
130 layoutButtons; % The layout buttons | |
131 fontSize; % last calculated optimal font size | |
132 selectedFontSize; % | |
133 | |
134 previousMouseLocation; % last mouse location relative to the axes | |
135 groupSelectionMode = 0; % current stage in a group selection task | |
136 groupSelectedNodes; % selected nodes in a group selection | |
137 groupSelectedDims; % size of enclosing rectangle of selected nodes | |
138 groupSelectedRect; % a bounding rectangle for the selected nodes | |
139 end | |
140 | |
141 methods | |
142 | |
143 function obj = graphViz4Matlab(varargin) | |
144 % graphViz4Matlab constructor | |
145 if(~exist('processArgs','file')), error('Requires processArgs() function'); end | |
146 obj.addKnownLayouts(); | |
147 obj.processInputs(varargin{:}) | |
148 obj.addNodes(); | |
149 obj.addEdges(); | |
150 obj.draw(); | |
151 end | |
152 | |
153 function draw(obj) | |
154 % Draw the graph | |
155 if(obj.isvisible) | |
156 obj.erase() | |
157 end | |
158 obj.createWindow(); | |
159 obj.calculateMinMaxNodeSize(); | |
160 obj.layoutNodes(); | |
161 obj.displayGraph(); | |
162 obj.isvisible = true; | |
163 obj.paperCrop(); | |
164 end | |
165 | |
166 function fig = freeze(obj) | |
167 % Freeze the current image into a regular Matlab figure | |
168 figure(obj.fig); | |
169 print tmp.png -dpng -r300 | |
170 fig = figure; | |
171 image(imread('tmp.png')); | |
172 axis off; | |
173 delete tmp.png; | |
174 close(obj.fig); | |
175 end | |
176 | |
177 function redraw(obj) | |
178 % Redraw the graph. (You could also call draw() again but then the | |
179 % window is recreated as well and it doesn't look as nice). | |
180 if(~obj.isvisible) | |
181 obj.draw(); | |
182 return; | |
183 end | |
184 cla(obj.ax); | |
185 obj.clearGroupSelection(); | |
186 obj.calculateMinMaxNodeSize(); | |
187 obj.layoutNodes(); | |
188 obj.displayGraph(); | |
189 end | |
190 | |
191 function flip(obj,varargin) | |
192 % Have the layout algorithms layout the graph as though the arrows | |
193 % were pointing in the opposite direction. The node connectivity | |
194 % remains the same and if node one pointed to node 2 before, it | |
195 % still does after. This is useful for tree layout, for example to | |
196 % turn the tree on its head. Calling it twice flips it back. | |
197 obj.flipped = ~obj.flipped; | |
198 if(obj.isvisible) | |
199 obj.redraw(); | |
200 end | |
201 end | |
202 | |
203 function erase(obj) | |
204 % Erase the graph but maintain the state so that it can be redrawn. | |
205 if(obj.isvisible) | |
206 obj.clearGroupSelection(); | |
207 delete(obj.fig); | |
208 obj.isvisible = false; | |
209 | |
210 end | |
211 end | |
212 | |
213 function nodeSelected(obj,node) | |
214 % This function is called by nodes when they are selected by the | |
215 % mouse. It should not be called manually. | |
216 if(obj.groupSelectionMode == 1) | |
217 obj.groupSelectionStage1(); | |
218 return; | |
219 end | |
220 if(~isempty(obj.selectedNode)) | |
221 node.deselect(); | |
222 obj.selectedNode = []; | |
223 return; | |
224 end | |
225 switch get(obj.fig,'SelectionType') | |
226 case 'normal' | |
227 obj.singleClick(node); | |
228 case 'open' | |
229 obj.doubleClick(node); | |
230 case 'alt' | |
231 obj.rightClick(node); | |
232 otherwise | |
233 obj.shiftClick(node); | |
234 end | |
235 end | |
236 | |
237 function addLayout(obj,layout) | |
238 % Let the graph know about a new layout you have created so that it | |
239 % will be available via a toolbar button. The layout object must be | |
240 % a descendant of the Abstractlayout class. This method does not have | |
241 % to be called for existing layouts, nor does it need to be called | |
242 % if you passed the new layout to the constructor or to the | |
243 % setLayout() method. It will not add two layouts with the same | |
244 % name property. | |
245 if(~ismember(layout.name,fieldnames(obj.layouts))) | |
246 if(layout.isavailable()) | |
247 obj.layouts.(layout.name) = layout; | |
248 if(obj.isvisible) | |
249 obj.addButtons(); | |
250 end | |
251 else | |
252 warning('graphViz4Matlab:layout','This layout is not available'); | |
253 end | |
254 end | |
255 end | |
256 | |
257 function setLayout(obj,layout) | |
258 % Set a new layout algorithm and refresh the graph. | |
259 if(layout.isavailable()) | |
260 obj.addLayout(layout); | |
261 obj.currentLayout = obj.layouts.(layout.name); | |
262 obj.redraw(); | |
263 else | |
264 warning('graphViz4Matlab:layout','Sorry, this layout is not available'); | |
265 end | |
266 end | |
267 | |
268 function squareAxes(obj,varargin) | |
269 % Toggle the axes from square to normal and vice versa. | |
270 obj.clearGroupSelection(); | |
271 if(obj.square) | |
272 axis(obj.ax,'normal'); | |
273 obj.square = false; | |
274 else | |
275 axis(obj.ax,'square'); | |
276 obj.square = true; | |
277 end | |
278 | |
279 end | |
280 | |
281 function tightenAxes(obj,varargin) | |
282 % Tighten the axes as much as possible. | |
283 obj.clearGroupSelection(); | |
284 xpos = vertcat(obj.nodeArray.xpos); | |
285 ypos = vertcat(obj.nodeArray.ypos); | |
286 r = obj.nodeArray(1).width/2; | |
287 axis(obj.ax,[min(xpos)-r,max(xpos)+r,min(ypos)-r,max(ypos)+r]); | |
288 axis normal; | |
289 end | |
290 | |
291 | |
292 | |
293 | |
294 end % end of public methods | |
295 | |
296 | |
297 methods(Access = 'protected') | |
298 | |
299 function addKnownLayouts(obj) | |
300 % Add all of the known layouts | |
301 obj.layouts = struct; | |
302 for i=1:numel(obj.knownLayouts) | |
303 layout = obj.knownLayouts{i}; | |
304 if(layout.isavailable()) | |
305 obj.layouts.(layout.name) = layout; | |
306 end | |
307 end | |
308 end | |
309 | |
310 function processInputs(obj,varargin) | |
311 % Process the inputs and perform error checking | |
312 labels = {'adj', 'adjMatrix', 'adjMat', 'layout', 'nodeLabels', 'nodeDescriptions', 'nodeColors', 'undirected', 'edgeColors', 'splitLabels', 'doubleClickFn'}; | |
313 for i=1:numel(varargin) | |
314 arg = varargin{i}; | |
315 if ~ischar(arg), continue; end | |
316 for j = 1:numel(labels) | |
317 if strcmpi(arg, labels{i}); | |
318 varargin{i} = ['-', arg]; | |
319 end | |
320 if strcmpi(arg, '-adj') || strcmpi(arg, '-adjMatrix') | |
321 varargin{i} = '-adjMat'; | |
322 end | |
323 end | |
324 end | |
325 | |
326 [adjMatrix, currentLayout, nodeLabels, nodeDescriptions, nodeColors,obj.undirected,obj.edgeColors,obj.splitLabels,obj.doubleClickFn] = processArgs(varargin,... | |
327 '-adjMat' , [] ,... | |
328 '-layout' , [] ,... | |
329 '-nodeLabels' , {} ,... | |
330 '-nodeDescriptions' , {} ,... | |
331 '-nodeColors' , {} ,... | |
332 '-undirected' , false ,... | |
333 '-edgeColors' , [] ,... | |
334 '-splitLabels' , true ,... | |
335 '-doubleClickFn' , [] ); | |
336 | |
337 | |
338 if(~isempty(currentLayout) && ~isavailable(currentLayout)) | |
339 currentLayout = []; | |
340 end | |
341 if(isempty(adjMatrix)) | |
342 adjMatrix = [0 0 0 0; 1 0 0 0; 1 1 0 0; 1 1 1 0]; % example graph | |
343 end | |
344 | |
345 if(isempty(currentLayout)) | |
346 fields = fieldnames(obj.layouts); | |
347 currentLayout = obj.layouts.(fields{1}); | |
348 else | |
349 obj.addLayout(currentLayout); | |
350 end | |
351 obj.nnodes = size(adjMatrix,1); | |
352 obj.adjMatrix = adjMatrix; | |
353 if(isempty(nodeDescriptions)) | |
354 nodeDescriptions = repmat({'Enter a description here...'},size(adjMatrix,1),1); | |
355 end | |
356 obj.nodeDescriptions = nodeDescriptions; | |
357 obj.nodeColors = nodeColors; | |
358 | |
359 if(isempty(nodeLabels)) | |
360 nodeLabels = cellfun(@(x)num2str(x),mat2cell(1:obj.nnodes,1,ones(1,obj.nnodes)),'UniformOutput',false); | |
361 end | |
362 | |
363 obj.nodeLabels = nodeLabels; | |
364 if(~isequal(numel(obj.nodeLabels),size(adjMatrix,1),size(adjMatrix,2))) | |
365 error('graphViz4Matlab:dimMismatch','The number of labels must match the dimensions of adjmatrix.'); | |
366 end | |
367 obj.currentLayout = currentLayout; | |
368 end | |
369 | |
370 function createWindow(obj) | |
371 % Create the main window | |
372 obj.fig = figure(floor(1000*rand) + 1000); | |
373 set(obj.fig,'Name','graphViz4Matlab',... | |
374 'NumberTitle' ,'off',... | |
375 'Color','w' ,'Toolbar','none'); | |
376 obj.createAxes(); | |
377 ssize = get(0,'ScreenSize'); | |
378 pos = [ssize(3)/2,50,-20+ssize(3)/2,ssize(4)-200]; | |
379 set(obj.fig,'Position',pos); | |
380 obj.setCallbacks(); | |
381 obj.addButtons(); | |
382 | |
383 end | |
384 | |
385 function createAxes(obj) | |
386 % Create the axes upon which the graph will be displayed. | |
387 obj.ax = axes('Parent',obj.fig,'box','on','UserData','main'); | |
388 outerpos = get(obj.ax,'OuterPosition'); | |
389 axpos = outerpos; | |
390 axpos(4) = 0.90; | |
391 axpos(2) = 0.03; | |
392 axis manual | |
393 if(obj.square) | |
394 axis square | |
395 end | |
396 set(obj.ax,'Position',axpos,'XTick',[],'YTick',[],'LineWidth',0.5); | |
397 set(obj.ax,'ButtonDownFcn',@obj.axPressed); | |
398 end | |
399 | |
400 | |
401 | |
402 function setCallbacks(obj) | |
403 % Set the callback functions for the figure, i.e. functions that | |
404 % will be called when the user performs various actions. | |
405 set(obj.fig,'ResizeFcn' ,@obj.windowResized); | |
406 set(obj.fig,'WindowButtonMotionFcn' ,@obj.mouseMoved); | |
407 set(obj.fig,'WindowButtonUpFcn' ,@obj.buttonUp); | |
408 set(obj.fig,'DeleteFcn' ,@obj.deleted); | |
409 end | |
410 | |
411 function addNodes(obj) | |
412 % Add all of the nodes to the graph structure, but don't display | |
413 % them yet. | |
414 obj.nodeArray = []; | |
415 for i=1:obj.nnodes | |
416 newnode = graphViz4MatlabNode(obj.nodeLabels{i}); | |
417 newnode.containingGraph = obj; | |
418 newnode.showFullLabel = ~obj.splitLabels; | |
419 obj.nodeArray = [obj.nodeArray newnode]; | |
420 end | |
421 obj.addNodeDescriptions(obj.nodeDescriptions); | |
422 obj.addNodeColors(obj.nodeColors); | |
423 end | |
424 | |
425 function addNodeDescriptions(obj,nodeDescriptions) | |
426 % Add any descriptions to the newly created nodes. | |
427 if(~isempty(nodeDescriptions)) | |
428 if(numel(nodeDescriptions) == 1) | |
429 nodeDescriptions = repmat(nodeDescriptions,obj.nnodes,1); | |
430 end | |
431 for i=1:obj.nnodes | |
432 obj.nodeArray(i).description = nodeDescriptions{i}; | |
433 end | |
434 end | |
435 end | |
436 | |
437 function addNodeColors(obj,nodeColors) | |
438 % Shade the nodes according to the specified colors. If too few | |
439 % colors are specified, they are cycled through. | |
440 if(~isempty(nodeColors)) | |
441 if(~iscell(nodeColors)) | |
442 nodeColors = mat2cell(nodeColors,ones(1,size(nodeColors,1)),size(nodeColors,2)); | |
443 end | |
444 if(size(nodeColors,2) > size(nodeColors,1)) | |
445 nodeColors = nodeColors'; | |
446 end | |
447 if(numel(nodeColors) < obj.nnodes) | |
448 nodeColors = repmat(nodeColors,ceil(obj.nnodes/numel(nodeColors)),1); | |
449 nodeColors = nodeColors(1:obj.nnodes); | |
450 end | |
451 for i=1:obj.nnodes | |
452 obj.nodeArray(i).shade(nodeColors{i}); | |
453 end | |
454 obj.nodeColors = nodeColors; | |
455 end | |
456 end | |
457 | |
458 function addEdges(obj) | |
459 % Add all of the edges to the graph structure, but don't display | |
460 % them yet. | |
461 if(any(diag(obj.adjMatrix))) | |
462 fprintf('\nRemoving Self Loops\n'); | |
463 obj.adjMatrix = obj.adjMatrix - diag(diag(obj.adjMatrix)); | |
464 end | |
465 obj.edgeArray = struct('from',[],'to',[],'arrow',[]); | |
466 counter = 1; | |
467 for i=1:obj.nnodes | |
468 for j=1:obj.nnodes | |
469 if(obj.adjMatrix(i,j)) | |
470 obj.edgeArray(counter) = struct('from',obj.nodeArray(i),'to',obj.nodeArray(j),'arrow',-1); | |
471 obj.nodeArray(i).outedges = [obj.nodeArray(i).outedges,counter]; | |
472 obj.nodeArray(j).inedges = [obj.nodeArray(j).inedges,counter]; | |
473 counter = counter + 1; | |
474 end | |
475 end | |
476 end | |
477 obj.nedges = counter -1; | |
478 end | |
479 | |
480 function calculateMinMaxNodeSize(obj) | |
481 % calculates the maximum and minimum node sizes in data units | |
482 SCREEN_PROPORTION_MAX = 1/10; | |
483 SCREEN_PROPORTION_MIN = 1/35; | |
484 units = get(0,'Units'); | |
485 set(0,'Units','pixels'); | |
486 screensize = get(0,'ScreenSize'); | |
487 set(0,'Units',units); | |
488 axunits = get(obj.ax,'Units'); | |
489 set(obj.ax,'Units','pixels'); | |
490 axsize = get(obj.ax,'Position'); | |
491 set(obj.ax,'Units',axunits); | |
492 if(screensize(3) < screensize(4)) | |
493 dataUnitsPerPixel = abs(diff(xlim))/axsize(3); | |
494 obj.minNodeSize = (SCREEN_PROPORTION_MIN*screensize(3))*dataUnitsPerPixel; | |
495 obj.maxNodeSize = (SCREEN_PROPORTION_MAX*screensize(3))*dataUnitsPerPixel; | |
496 else | |
497 dataUnitsPerPixel = abs(diff(ylim))/axsize(4); | |
498 obj.minNodeSize = (SCREEN_PROPORTION_MIN*screensize(4))*dataUnitsPerPixel; | |
499 obj.maxNodeSize = (SCREEN_PROPORTION_MAX*screensize(4))*dataUnitsPerPixel; | |
500 end | |
501 end | |
502 | |
503 function layoutNodes(obj) | |
504 % Layout the nodes and edges according to the current layout | |
505 % algorithm. | |
506 if(obj.flipped) | |
507 adj = obj.adjMatrix'; | |
508 else | |
509 adj = obj.adjMatrix; | |
510 end | |
511 obj.currentLayout.dolayout(adj,obj.ax,obj.maxNodeSize); | |
512 nodesize = obj.currentLayout.nodeSize(); | |
513 locs = obj.currentLayout.centers(); | |
514 for i=1:obj.nnodes | |
515 node = obj.nodeArray(i); | |
516 node.resize(nodesize); | |
517 node.move(locs(i,1),locs(i,2)); | |
518 end | |
519 end | |
520 | |
521 function displayGraph(obj) | |
522 % Display all of the nodes and edges. | |
523 cla(obj.ax); | |
524 obj.setFontSize(); | |
525 for i=1:obj.nnodes | |
526 node = obj.nodeArray(i); | |
527 node.fontSize = obj.fontSize; | |
528 node.draw(obj.ax); | |
529 end | |
530 displayEdges(obj); | |
531 end | |
532 | |
533 function displayEdges(obj,indices) | |
534 % Display or refresh the specified edges. If none specified, all | |
535 % are refreshed. Currently only works with round nodes. | |
536 figure(obj.fig); | |
537 if(nargin < 2) | |
538 indices = 1:obj.nedges; | |
539 else | |
540 indices = unique(indices); | |
541 end | |
542 for i=1:numel(indices) | |
543 edge = obj.edgeArray(indices(i)); | |
544 [X,Y,Xarrow,Yarrow] = obj.calcPositions(edge); | |
545 if(ishandle(edge.arrow)) | |
546 delete(edge.arrow) | |
547 end | |
548 hold on; | |
549 edgeColor = obj.defaultEdgeColor; | |
550 if ~isempty(obj.edgeColors) | |
551 candidates = obj.edgeColors(findString(edge.from.label,obj.edgeColors(:,1)),:); | |
552 if size(candidates,1)==1 && strcmpi(candidates(1,2),'all') | |
553 edgeColor = candidates{1,3}; | |
554 else | |
555 edgeCol = candidates(findString(edge.to.label,candidates(:,2)),3); | |
556 if ~isempty(edgeCol); edgeColor = edgeCol{1}; end | |
557 end | |
558 end | |
559 edge.arrow = plot(X,Y,'LineWidth',2,'HitTest','off','Color',edgeColor); | |
560 if(~obj.undirected) | |
561 arrowHead = obj.displayArrowHead(X,Y,Xarrow,Yarrow,edgeColor); | |
562 edge.arrow = [edge.arrow arrowHead]; | |
563 end | |
564 hold off; | |
565 obj.edgeArray(indices(i)) = edge; | |
566 end | |
567 end | |
568 | |
569 function arrowHead = displayArrowHead(obj,X,Y,Xarrow,Yarrow,arrowColor) %#ok | |
570 % Displays the arrow head given the appropriate coordinates | |
571 % calculated via the calcPositions() function. | |
572 | |
573 arrowHead = patch('Faces' ,[1,2,3] ,... | |
574 'Vertices' ,[Xarrow(1) Yarrow(1); Xarrow(2) Yarrow(2) ;X(2) Y(2)],... | |
575 'FaceColor' ,arrowColor); | |
576 end | |
577 | |
578 function [X,Y,Xarrow,Yarrow] = calcPositions(obj,edge) | |
579 % Helper function for displayEdges() - calculates edge and arrow | |
580 % start and end positions in data units. | |
581 X = [edge.from.xpos edge.to.xpos]; | |
582 Y = [edge.from.ypos edge.to.ypos]; | |
583 ratio = (Y(2) - Y(1))/(X(2)-X(1)); | |
584 if(isinf(ratio)) | |
585 ratio = realmax; | |
586 end | |
587 % dx: x-distance from node1 center to perimeter in direction of node2 | |
588 % dy: y-distance from node1 center to perimeter in direction of node2 | |
589 % ddx: x-distance from node1 perimeter to base of arrow head | |
590 % ddy: y-distance from node1 perimeter to base of arrow head | |
591 % dpx: x-offset away from edge in perpendicular direction, for arrow head | |
592 % dpy: y-offset away from edge in perpendicular direction, for arrow head | |
593 | |
594 arrowSize = obj.maxNodeSize/10; | |
595 [dx,dy] = pol2cart(atan(ratio),edge.from.width/2); | |
596 [ddx,ddy] = pol2cart(atan(ratio),arrowSize); | |
597 ratio = 1/ratio; % now work out perpendicular directions. | |
598 if(isinf(ratio)) | |
599 ratio = realmax; | |
600 end | |
601 [dpx dpy] = pol2cart(atan(ratio),arrowSize/2); | |
602 ddx = abs(ddx); ddy = abs(ddy); dpx = abs(dpx); dpy = abs(dpy); | |
603 dx = abs(dx); dy = abs(dy); | |
604 if(X(1) < X(2)) | |
605 X(1) = X(1) + dx; X(2) = X(2) - dx; | |
606 else | |
607 X(1) = X(1) - dx; X(2) = X(2) + dx; | |
608 end | |
609 if(Y(1) < Y(2)) | |
610 Y(1) = Y(1) + dy; Y(2) = Y(2) - dy; | |
611 else | |
612 Y(1) = Y(1) - dy; Y(2) = Y(2) + dy; | |
613 end | |
614 if(X(1) <= X(2) && Y(1) <= Y(2)) | |
615 Xarrow(1) = X(2) - ddx - dpx; Xarrow(2) = X(2) - ddx + dpx; | |
616 Yarrow(1) = Y(2) - ddy + dpy; Yarrow(2) = Y(2) - ddy - dpy; | |
617 elseif(X(1) <= X(2) && Y(1) >= Y(2)) | |
618 Xarrow(1) = X(2) - ddx - dpx; Xarrow(2) = X(2) - ddx + dpx; | |
619 Yarrow(1) = Y(2) + ddy - dpy; Yarrow(2) = Y(2) + ddy + dpy; | |
620 elseif(X(1) >= X(2) && Y(1) <= Y(2)) | |
621 Xarrow(1) = X(2) + ddx - dpx; Xarrow(2) = X(2) + ddx + dpx; | |
622 Yarrow(1) = Y(2) - ddy - dpy; Yarrow(2) = Y(2) - ddy + dpy; | |
623 else % (X(1) >= (X(2) && Y(1) >= Y(2)) | |
624 Xarrow(1) = X(2) + ddx - dpx; Xarrow(2) = X(2) + ddx + dpx; | |
625 Yarrow(1) = Y(2) + ddy + dpy; Yarrow(2) = Y(2) + ddy - dpy; | |
626 end | |
627 end | |
628 | |
629 function addButtons(obj) | |
630 % Add user interface buttons. | |
631 if(~isempty(obj.toolbar)) | |
632 if(ishandle(obj.toolbar)) | |
633 delete(obj.toolbar); | |
634 obj.toolbar = []; | |
635 end | |
636 end | |
637 obj.toolbar = uitoolbar(obj.fig); | |
638 | |
639 % button icons | |
640 load glicons; | |
641 | |
642 uipushtool(obj.toolbar,... | |
643 'ClickedCallback' ,@obj.decreaseFontSize,... | |
644 'TooltipString' ,'Decrease Font Size',... | |
645 'CData' ,icons.downblue); | |
646 | |
647 uipushtool(obj.toolbar,... | |
648 'ClickedCallback' ,@obj.increaseFontSize,... | |
649 'TooltipString' ,'Increase Font Size',... | |
650 'CData' ,icons.upblue); | |
651 | |
652 uipushtool(obj.toolbar,... | |
653 'ClickedCallback' ,@obj.tightenAxes,... | |
654 'TooltipString' ,'Tighten Axes',... | |
655 'CData' ,icons.expand); | |
656 | |
657 uipushtool(obj.toolbar,... | |
658 'ClickedCallback' ,@obj.flip,... | |
659 'TooltipString' ,'Flip/Reset Layout',... | |
660 'CData' , icons.flip); | |
661 | |
662 uipushtool(obj.toolbar,... | |
663 'ClickedCallback' ,@obj.shrinkNodes,... | |
664 'TooltipString' ,'Decrease Node Size',... | |
665 'CData' , icons.downdarkblue); | |
666 | |
667 uipushtool(obj.toolbar,... | |
668 'ClickedCallback' ,@obj.growNodes,... | |
669 'TooltipString' ,'Increase Node Size',... | |
670 'CData' , icons.updarkblue); | |
671 | |
672 if(~isempty(obj.layoutButtons)) | |
673 for i=1:numel(obj.layoutButtons) | |
674 if(ishandle(obj.layoutButtons(i))) | |
675 delete(obj.layoutButtons(i)); | |
676 end | |
677 end | |
678 obj.layoutButtons = []; | |
679 end | |
680 | |
681 layoutNames = fieldnames(obj.layouts); | |
682 for i=1:numel(layoutNames) | |
683 layout = obj.layouts.(layoutNames{i}); | |
684 layoutButton = uipushtool(obj.toolbar,... | |
685 'ClickedCallback', @obj.layoutButtonPushed,... | |
686 'TooltipString', layout.shortDescription,... | |
687 'UserData' , layoutNames{i},... | |
688 'Separator' , 'on',... | |
689 'CData' , layout.image); | |
690 obj.layoutButtons = [obj.layoutButtons,layoutButton]; | |
691 end | |
692 end | |
693 | |
694 function setFontSize(obj) | |
695 % fontsize = obj.maxFontSize; | |
696 fontSize = 20; | |
697 maxchars = size(char(obj.nodeLabels),2); | |
698 width = obj.nodeArray(1).width; | |
699 height = obj.nodeArray(1).height; | |
700 xpos = -10; ypos = -10; | |
701 t = text(xpos,ypos,repmat('g',1,maxchars),... | |
702 'FontUnits' , 'points' ,... | |
703 'Units' , 'data' ,... | |
704 'HorizontalAlignment' , 'center' ,... | |
705 'VerticalAlignment' , 'middle' ,... | |
706 'FontWeight' , 'demi' ,... | |
707 'LineStyle' , 'none' ,... | |
708 'Margin' , 0.01 ,... | |
709 'FontSize' , fontSize ,... | |
710 'Color' , 'w' ); | |
711 | |
712 extent = get(t,'Extent'); | |
713 | |
714 while(extent(3) > width || extent(4) > height) | |
715 fontSize = fontSize - 1; | |
716 if(fontSize < 2), break,end | |
717 set(t,'FontSize',fontSize); | |
718 extent = get(t,'Extent'); | |
719 end | |
720 obj.fontSize = fontSize; | |
721 | |
722 end | |
723 | |
724 function asp = aspectRatio(obj) | |
725 % Return the current aspect ratio of the figure, width/height | |
726 | |
727 units = get(obj.ax,'Units'); | |
728 set(obj.ax,'Units','pixels'); | |
729 pos = get(obj.ax,'Position'); | |
730 set(obj.ax,'Units',units); | |
731 asp = (pos(3)/pos(4)); | |
732 | |
733 end | |
734 | |
735 function paperCrop(obj) | |
736 % Make the papersize the same as the the figure size. This is | |
737 % useful when saving as pdf. | |
738 units = get(obj.fig,'Units'); | |
739 set(obj.fig,'Units','inches'); | |
740 pos = get(obj.fig,'Position'); | |
741 set(obj.fig,'Units',units); | |
742 set(obj.fig,'PaperPositionMode','auto','PaperSize',pos(3:4)); | |
743 end | |
744 %% | |
745 % Callbacks | |
746 | |
747 function layoutButtonPushed(obj,buttonPushed,varargin) | |
748 % Called when a layout button is pushed. | |
749 name = get(buttonPushed,'UserData'); | |
750 obj.currentLayout = obj.layouts.(name); | |
751 axis square; | |
752 obj.redraw; | |
753 end | |
754 | |
755 function windowResized(obj,varargin) | |
756 % This function is called whenever the window is resized. It | |
757 % redraws the whole graph. | |
758 if(obj.isvisible) | |
759 obj.redraw; | |
760 obj.paperCrop(); | |
761 end | |
762 | |
763 end | |
764 | |
765 function mouseMoved(obj,varargin) | |
766 % This function is called whenever the mouse moves within the | |
767 % figure. | |
768 if(obj.groupSelectionMode == 2) | |
769 % Move all of the nodes & rectangle | |
770 currentPoint = get(obj.ax,'CurrentPoint'); | |
771 xlimits = get(obj.ax,'XLim'); | |
772 ylimits = get(obj.ax,'YLim'); | |
773 sdims = obj.groupSelectedDims; | |
774 xdiff = currentPoint(1,1) - obj.previousMouseLocation(1,1); | |
775 ydiff = currentPoint(1,2) - obj.previousMouseLocation(1,2); | |
776 | |
777 if(xdiff <=0) | |
778 xdiff = max(xdiff,(xlimits(1)-sdims(1))); | |
779 else | |
780 xdiff = min(xdiff,xlimits(2)-sdims(2)); | |
781 end | |
782 if(ydiff <=0) | |
783 ydiff = max(ydiff,(ylimits(1)-sdims(3))); | |
784 else | |
785 ydiff = min(ydiff,ylimits(2)-sdims(4)); | |
786 end | |
787 xnodepos = vertcat(obj.groupSelectedNodes.xpos) + xdiff; | |
788 ynodepos = vertcat(obj.groupSelectedNodes.ypos) + ydiff; | |
789 for i=1:numel(obj.groupSelectedNodes) | |
790 obj.groupSelectedNodes(i).move(xnodepos(i),ynodepos(i)); | |
791 end | |
792 recpos = get(obj.groupSelectedRect,'Position'); | |
793 recpos(1) = recpos(1) + xdiff; | |
794 recpos(2) = recpos(2) + ydiff; | |
795 obj.groupSelectedDims = [recpos(1),recpos(1)+recpos(3),recpos(2),recpos(2)+recpos(4)]; | |
796 set(obj.groupSelectedRect,'Position',recpos); | |
797 edges = [obj.groupSelectedNodes.inedges,obj.groupSelectedNodes.outedges]; | |
798 obj.displayEdges(edges); | |
799 obj.previousMouseLocation = currentPoint; | |
800 else | |
801 if(isempty(obj.selectedNode)), return,end | |
802 currentPoint = get(obj.ax,'CurrentPoint'); | |
803 x = currentPoint(1,1); y = currentPoint(1,2); | |
804 xl = xlim + [obj.selectedNode.width,-obj.selectedNode.width]/2; | |
805 yl = ylim + [obj.selectedNode.height,-obj.selectedNode.height]/2; | |
806 x = min(max(xl(1),x),xl(2)); | |
807 y = min(max(yl(1),y),yl(2)); | |
808 obj.selectedNode.move(x,y); | |
809 obj.displayEdges([obj.selectedNode.inedges,obj.selectedNode.outedges]); | |
810 end | |
811 end | |
812 | |
813 function buttonUp(obj,varargin) | |
814 % This function executes when the mouse button is released. | |
815 if(obj.groupSelectionMode == 2) | |
816 obj.clearGroupSelection(); | |
817 return; | |
818 end | |
819 if(isempty(obj.selectedNode)),return,end | |
820 obj.selectedNode.deselect(); | |
821 | |
822 obj.selectedNode.useFullLabel = false; | |
823 obj.selectedNode.fontSize = obj.selectedFontSize; | |
824 obj.selectedNode.redraw(); | |
825 obj.selectedNode = []; | |
826 set(gcf,'Pointer','arrow'); | |
827 end | |
828 | |
829 function axPressed(obj,varargin) | |
830 % Called when the user selects the axes but not a node | |
831 switch obj.groupSelectionMode | |
832 case 0 % hasn't been selected yet | |
833 xpos = vertcat(obj.nodeArray.xpos); | |
834 ypos = vertcat(obj.nodeArray.ypos); | |
835 p1 = get(obj.ax,'CurrentPoint'); | |
836 rbbox; % returns after box drawn | |
837 p2 = get(obj.ax,'CurrentPoint'); | |
838 xleft = min(p1(1,1),p2(1,1)); | |
839 xright = max(p1(1,1),p2(1,1)); | |
840 ylower = min(p1(1,2),p2(1,2)); | |
841 yupper = max(p1(1,2),p2(1,2)); | |
842 selectedX = (xpos <= xright) & (xpos >= xleft); | |
843 selectedY = (ypos <= yupper) & (ypos >= ylower); | |
844 selected = selectedX & selectedY; | |
845 if(~any(selected)),return,end | |
846 obj.groupSelectionMode = 1; | |
847 obj.groupSelectedNodes = obj.nodeArray(selected); | |
848 for i=1:numel(obj.groupSelectedNodes) | |
849 node = obj.groupSelectedNodes(i); | |
850 node.select(); | |
851 node.redraw(); | |
852 end | |
853 | |
854 w = obj.groupSelectedNodes(1).width/2; | |
855 h = obj.groupSelectedNodes(1).height/2; | |
856 x = vertcat(obj.groupSelectedNodes.xpos); | |
857 y = vertcat(obj.groupSelectedNodes.ypos); | |
858 minx = min(x)-w; maxx = max(x)+w; miny = min(y)-h; maxy = max(y)+h; | |
859 obj.groupSelectedDims = [minx,maxx,miny,maxy]; | |
860 obj.groupSelectedRect = rectangle('Position',[minx,miny,maxx-minx,maxy-miny],'LineStyle','--','EdgeColor','r'); | |
861 case 1 % nodes selected | |
862 obj.groupSelectionStage1(); | |
863 case 2 %not ever reached in this function | |
864 obj.clearGroupSelection(); | |
865 end | |
866 end | |
867 | |
868 function groupSelectionStage1(obj) | |
869 % Called after a group of nodes has been selected and the mouse | |
870 % button has been pressed somewhere on the axes, (or on a node). | |
871 p = get(obj.ax,'CurrentPoint'); | |
872 obj.previousMouseLocation = p; | |
873 dims = obj.groupSelectedDims; | |
874 if(p(1,1) >= dims(1) && p(1,1) <= dims(2) && p(1,2) >= dims(3) && p(1,2) <=dims(4)) | |
875 set(gcf,'Pointer','hand'); | |
876 obj.groupSelectionMode = 2; | |
877 else | |
878 obj.clearGroupSelection(); | |
879 end | |
880 end | |
881 | |
882 function clearGroupSelection(obj) | |
883 % Clear a group selection | |
884 if(ishandle(obj.groupSelectedRect)) | |
885 delete(obj.groupSelectedRect); | |
886 end | |
887 obj.groupSelectedRect = []; | |
888 for i=1:numel(obj.groupSelectedNodes) | |
889 obj.groupSelectedNodes(i).deselect(); | |
890 end | |
891 obj.groupSelectedNodes = []; | |
892 obj.groupSelectedDims = []; | |
893 obj.groupSelectionMode = 0; | |
894 set(gcf,'Pointer','arrow'); | |
895 end | |
896 | |
897 function deleted(obj,varargin) | |
898 % Called when the figure is deleted by the user. | |
899 obj.isvisible = false; | |
900 obj.clearGroupSelection(); | |
901 end | |
902 | |
903 function singleClick(obj,node) | |
904 % Called when a user single clicks on a node. | |
905 obj.selectedNode = node; | |
906 node.select(); | |
907 set(gcf,'Pointer','hand'); | |
908 obj.selectedFontSize = node.fontSize; | |
909 node.useFullLabel = true; | |
910 node.fontSize = max(15,node.fontSize*1.5); | |
911 node.redraw(); | |
912 end | |
913 | |
914 function doubleClick(obj,node) | |
915 % Called when a user double clicks on a node | |
916 if isempty(obj.doubleClickFn) | |
917 description = node.description; | |
918 if(~iscell(description)) | |
919 description = {description}; | |
920 end | |
921 answer = inputdlg('',node.label,4,description); | |
922 if(~isempty(answer)) | |
923 node.description = answer; | |
924 end | |
925 else | |
926 obj.doubleClickFn(node.label); | |
927 end | |
928 end | |
929 | |
930 function rightClick(obj,node) %#ok | |
931 % Called when a user right clicks on a node | |
932 if(node.isshaded) | |
933 node.unshade(); | |
934 else | |
935 node.shade(); | |
936 end | |
937 end | |
938 | |
939 function shiftClick(obj,node) %#ok | |
940 % Called when a user shift clicks on a node | |
941 display(node); | |
942 end | |
943 | |
944 | |
945 end | |
946 | |
947 methods | |
948 % Callbacks that can be called by the user programmatically. | |
949 function shrinkNodes(obj,varargin) | |
950 % Shrink the nodes to 95% of their original size, (but not smaller | |
951 % than a calculated minimum. | |
952 obj.clearGroupSelection(); | |
953 s = max(0.8*obj.nodeArray(1).width,obj.minNodeSize); | |
954 obj.nodeArray(1).resize(s); | |
955 obj.setFontSize(); | |
956 for i=1:obj.nnodes | |
957 node = obj.nodeArray(i); | |
958 node.fontSize = obj.fontSize; | |
959 node.resize(s); | |
960 end | |
961 obj.displayEdges(); | |
962 end | |
963 | |
964 function growNodes(obj,varargin) | |
965 % Grow the nodes to 1/0.95 times their original size, (but not | |
966 % larger than a calculated maximum. | |
967 obj.clearGroupSelection(); | |
968 s = min(obj.nodeArray(1).width/0.8,1.5*obj.maxNodeSize); | |
969 obj.nodeArray(1).resize(s); | |
970 obj.setFontSize(); | |
971 for i=1:obj.nnodes | |
972 node = obj.nodeArray(i); | |
973 node.fontSize = obj.fontSize; | |
974 node.resize(s); | |
975 end | |
976 obj.displayEdges(); | |
977 end | |
978 | |
979 function increaseFontSize(obj,varargin) | |
980 % Increase the fontsize of all the nodes by 0.5 points. | |
981 current = get(obj.nodeArray(1).labelhandle,'FontSize'); | |
982 newsize = current + 1; | |
983 for i=1:numel(obj.nodeArray) | |
984 node = obj.nodeArray(i); | |
985 node.fontSize = newsize; | |
986 node.redraw(); | |
987 end | |
988 end | |
989 | |
990 function decreaseFontSize(obj,varargin) | |
991 % Decrease the fontsize of all the nodes by 0.5 points. | |
992 current = get(obj.nodeArray(1).labelhandle,'FontSize'); | |
993 newsize = max(current - 1,1); | |
994 for i=1:numel(obj.nodeArray) | |
995 node = obj.nodeArray(i); | |
996 node.fontSize = newsize; | |
997 node.redraw(); | |
998 end | |
999 end | |
1000 | |
1001 function XY = getNodePositions(obj) | |
1002 % Return the current positions of the nodes. The bottom left | |
1003 % corner is [0 0] and the top right is [1 1]. Node positions | |
1004 % refer to the centre of a node. | |
1005 XY = zeros(obj.nnodes, 2); | |
1006 for i=1:obj.nnodes | |
1007 XY(i, 1) = obj.nodeArray(i).xpos; | |
1008 XY(i, 2) = obj.nodeArray(i).ypos; | |
1009 end | |
1010 end | |
1011 | |
1012 function setNodePositions(obj, XY) | |
1013 % Programmatically set the node positions | |
1014 % XY(i, 1) is the xposition of node i, XY(i, 2) is the yposition. | |
1015 for i=1:obj.nnodes | |
1016 obj.nodeArray(i).move(XY(i, 1), XY(i, 2)); | |
1017 end | |
1018 obj.displayGraph(); | |
1019 end | |
1020 | |
1021 function moveNode(obj, nodeIndex, xpos, ypos) | |
1022 % Programmatically set a node position. | |
1023 obj.nodeArray(nodeIndex).move(xpos, ypos); | |
1024 obj.displayGraph(); | |
1025 end | |
1026 | |
1027 end | |
1028 end |