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