Mercurial > hg > camir-aes2014
diff 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 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/toolboxes/graph_visualisation/graphViz4Matlab/graphViz4Matlab.m Tue Feb 10 15:05:51 2015 +0000 @@ -0,0 +1,1028 @@ +classdef graphViz4Matlab < handle + % Visualize a graph in a Matlab figure window by specifying an + % adjacency matrix and optionally node labels, descriptions, colors and the + % the layout algorithm. The best layout algorithms require that graphViz be + % installed, available free at <http://www.graphviz.org>. + % + % Matthew Dunham + % University of British Columbia + % Last Updated: April 24, 2010 + % Requires Matlab version 2008a or newer. + % + % Syntax (see more examples below): + % graphViz4Matlab('-adjMat',adj,'-nodeLabels',labels,'-nodeColors',colors); + % + % Self loops are removed and not represented on the graph. + % + % Once the graph is displayed, there are several operations you can perform + % with the mouse. + % (1) Move a single node around. + % (2) Draw a mouse box around several nodes and move them all together. + % (3) Enter a description for a node by double clicking it. + % (4) Shade the node by right clicking it + % (5) Display a node's properties on the console by shift clicking it. + % (6) Increase or decrease the font size + % (7) Increase or decrease the node size + % (8) Tighten the axes and relax the square aspect ratio. + % (9) Ask the current layout algorithm to layout the nodes as though the + % arrows were pointing the other way. This only affects some of the + % layouts. + % (10) Change the layout algorithm and refresh the graph + % + % Additionally, any operation you could do with a regular Matlab figure can + % be done here, e.g. annotating or saving as a pdf. + % + % Options are specified via name value pairs in any order. + % [] denote defaults. + % + % '-adjMat' [example matrix] The adjacency matrix + % + % '-layout' [Gvizlayout if graphViz installed, else Gridlayout] + % A layout object, i.e. Gvizlayout | Gridlayout | Circlelayout + % (See knownLayouts Property) + % + % '-nodeLabels' ['1':'n'] A cell array of labels for the nodes + % + % '-nodeDescriptions' [{}] Longer descriptions for the nodes, displayed when + % double clicking on a node. + % + % '-nodeColors' ['c'] A cell array or n-by-3 matrix specifying colors + % for the nodes. If fewer colors than nodes are specified, + % the specified colors are reused and cycled through. + % + % '-undirected' [false] If true, no arrows are displayed. + % + % '-edgeColors' [] An n-by-3 cell array listing + % {fromName,toName,color} for each row. You can + % list only the n < numel(edges) edges you want to + % color. If you do not label the nodes, graphViz4Matlab + % uses '1','2','3', etc, in which case use these. + % You can specify the text 'all' in place of toName, + % to mean all nodes, i.e. {fromName,'all',color} + % + % + % '-splitLabels' [true] If true, long node labels are split into + % several rows + % + % '-doubleClickFn' (by default, double clicking a node brings up + % an edit box for the node's description, but you + % can pass in a custom function handle. The function + % gets passed the node's label. + % Examples: + % + % adj = rand(5,5) > 0.8; + % labels = {'First','Second','Third','Fourth','Fifth'}; + % colors = {'g','b'}; % will cycle through + % s = graphViz4Matlab('-adjMat',adj,'-nodeLabels',labels,'-nodeColors',colors); + % freeze(s); % convert to an image + % + % If you are only specifying an adjacency matrix, you can omit the + % '-adjMat' name as in graphViz4Matlab(adj). + % + % Calling graphViz4Matlab without any parameters displays an example graph. + % + + + properties(GetAccess = 'public', SetAccess = 'private') + % read only + path = addpath(genpath(fileparts(which(mfilename)))); % automatically adds subdirectories to path + graphVizPath = setupPath(); + nnodes = 0; % The number of nodes + nedges = 0; % The number of edges + currentLayout= []; % The current layout object + layouts = []; % List currently added layout objects + adjMatrix = []; % The adjacency matrix + isvisible = false; % True iff the graph is being displayed + nodeArray = []; % The nodes + edgeArray = []; % The edges + fig = []; % The main window + ax = []; % The main axes + doubleClickFn = []; % function to execute when a user double clicks on a node, (must be a function handle that takes in the node name + selectedNode = []; % The selected node, if any + minNodeSize = []; % A minimum size for the nodes + maxNodeSize = []; % A maximum size for the nodes + undirected = false; % If undirected, arrows are not displayed + flipped = false; % If true, layout is done as though edge directions were reversed. + % (does not affect the logical layout). + knownLayouts = {Gvizlayout ,... % add your own layout here or use + Treelayout ,... % the addLayout() method + Radiallayout,... + Circularlayout,... + Springlayout,... + Circlelayout,... + Gridlayout ,... + Randlayout }; + defaultEdgeColor = [0,0,0];%[20,43,140]/255; + edgeColors; + square = true; % amounts to a the call "axis square" + splitLabels = true; + end + + properties(GetAccess = 'private', SetAccess = 'private') + % These values store the initial values not the current ones. + nodeLabels = {}; + nodeDescriptions = {}; + nodeColors = {}; + end + + properties(GetAccess = 'protected',SetAccess = 'protected') + toolbar; % The button toolbar + layoutButtons; % The layout buttons + fontSize; % last calculated optimal font size + selectedFontSize; % + + previousMouseLocation; % last mouse location relative to the axes + groupSelectionMode = 0; % current stage in a group selection task + groupSelectedNodes; % selected nodes in a group selection + groupSelectedDims; % size of enclosing rectangle of selected nodes + groupSelectedRect; % a bounding rectangle for the selected nodes + end + + methods + + function obj = graphViz4Matlab(varargin) + % graphViz4Matlab constructor + if(~exist('processArgs','file')), error('Requires processArgs() function'); end + obj.addKnownLayouts(); + obj.processInputs(varargin{:}) + obj.addNodes(); + obj.addEdges(); + obj.draw(); + end + + function draw(obj) + % Draw the graph + if(obj.isvisible) + obj.erase() + end + obj.createWindow(); + obj.calculateMinMaxNodeSize(); + obj.layoutNodes(); + obj.displayGraph(); + obj.isvisible = true; + obj.paperCrop(); + end + + function fig = freeze(obj) + % Freeze the current image into a regular Matlab figure + figure(obj.fig); + print tmp.png -dpng -r300 + fig = figure; + image(imread('tmp.png')); + axis off; + delete tmp.png; + close(obj.fig); + end + + function redraw(obj) + % Redraw the graph. (You could also call draw() again but then the + % window is recreated as well and it doesn't look as nice). + if(~obj.isvisible) + obj.draw(); + return; + end + cla(obj.ax); + obj.clearGroupSelection(); + obj.calculateMinMaxNodeSize(); + obj.layoutNodes(); + obj.displayGraph(); + end + + function flip(obj,varargin) + % Have the layout algorithms layout the graph as though the arrows + % were pointing in the opposite direction. The node connectivity + % remains the same and if node one pointed to node 2 before, it + % still does after. This is useful for tree layout, for example to + % turn the tree on its head. Calling it twice flips it back. + obj.flipped = ~obj.flipped; + if(obj.isvisible) + obj.redraw(); + end + end + + function erase(obj) + % Erase the graph but maintain the state so that it can be redrawn. + if(obj.isvisible) + obj.clearGroupSelection(); + delete(obj.fig); + obj.isvisible = false; + + end + end + + function nodeSelected(obj,node) + % This function is called by nodes when they are selected by the + % mouse. It should not be called manually. + if(obj.groupSelectionMode == 1) + obj.groupSelectionStage1(); + return; + end + if(~isempty(obj.selectedNode)) + node.deselect(); + obj.selectedNode = []; + return; + end + switch get(obj.fig,'SelectionType') + case 'normal' + obj.singleClick(node); + case 'open' + obj.doubleClick(node); + case 'alt' + obj.rightClick(node); + otherwise + obj.shiftClick(node); + end + end + + function addLayout(obj,layout) + % Let the graph know about a new layout you have created so that it + % will be available via a toolbar button. The layout object must be + % a descendant of the Abstractlayout class. This method does not have + % to be called for existing layouts, nor does it need to be called + % if you passed the new layout to the constructor or to the + % setLayout() method. It will not add two layouts with the same + % name property. + if(~ismember(layout.name,fieldnames(obj.layouts))) + if(layout.isavailable()) + obj.layouts.(layout.name) = layout; + if(obj.isvisible) + obj.addButtons(); + end + else + warning('graphViz4Matlab:layout','This layout is not available'); + end + end + end + + function setLayout(obj,layout) + % Set a new layout algorithm and refresh the graph. + if(layout.isavailable()) + obj.addLayout(layout); + obj.currentLayout = obj.layouts.(layout.name); + obj.redraw(); + else + warning('graphViz4Matlab:layout','Sorry, this layout is not available'); + end + end + + function squareAxes(obj,varargin) + % Toggle the axes from square to normal and vice versa. + obj.clearGroupSelection(); + if(obj.square) + axis(obj.ax,'normal'); + obj.square = false; + else + axis(obj.ax,'square'); + obj.square = true; + end + + end + + function tightenAxes(obj,varargin) + % Tighten the axes as much as possible. + obj.clearGroupSelection(); + xpos = vertcat(obj.nodeArray.xpos); + ypos = vertcat(obj.nodeArray.ypos); + r = obj.nodeArray(1).width/2; + axis(obj.ax,[min(xpos)-r,max(xpos)+r,min(ypos)-r,max(ypos)+r]); + axis normal; + end + + + + + end % end of public methods + + + methods(Access = 'protected') + + function addKnownLayouts(obj) + % Add all of the known layouts + obj.layouts = struct; + for i=1:numel(obj.knownLayouts) + layout = obj.knownLayouts{i}; + if(layout.isavailable()) + obj.layouts.(layout.name) = layout; + end + end + end + + function processInputs(obj,varargin) + % Process the inputs and perform error checking + labels = {'adj', 'adjMatrix', 'adjMat', 'layout', 'nodeLabels', 'nodeDescriptions', 'nodeColors', 'undirected', 'edgeColors', 'splitLabels', 'doubleClickFn'}; + for i=1:numel(varargin) + arg = varargin{i}; + if ~ischar(arg), continue; end + for j = 1:numel(labels) + if strcmpi(arg, labels{i}); + varargin{i} = ['-', arg]; + end + if strcmpi(arg, '-adj') || strcmpi(arg, '-adjMatrix') + varargin{i} = '-adjMat'; + end + end + end + + [adjMatrix, currentLayout, nodeLabels, nodeDescriptions, nodeColors,obj.undirected,obj.edgeColors,obj.splitLabels,obj.doubleClickFn] = processArgs(varargin,... + '-adjMat' , [] ,... + '-layout' , [] ,... + '-nodeLabels' , {} ,... + '-nodeDescriptions' , {} ,... + '-nodeColors' , {} ,... + '-undirected' , false ,... + '-edgeColors' , [] ,... + '-splitLabels' , true ,... + '-doubleClickFn' , [] ); + + + if(~isempty(currentLayout) && ~isavailable(currentLayout)) + currentLayout = []; + end + if(isempty(adjMatrix)) + adjMatrix = [0 0 0 0; 1 0 0 0; 1 1 0 0; 1 1 1 0]; % example graph + end + + if(isempty(currentLayout)) + fields = fieldnames(obj.layouts); + currentLayout = obj.layouts.(fields{1}); + else + obj.addLayout(currentLayout); + end + obj.nnodes = size(adjMatrix,1); + obj.adjMatrix = adjMatrix; + if(isempty(nodeDescriptions)) + nodeDescriptions = repmat({'Enter a description here...'},size(adjMatrix,1),1); + end + obj.nodeDescriptions = nodeDescriptions; + obj.nodeColors = nodeColors; + + if(isempty(nodeLabels)) + nodeLabels = cellfun(@(x)num2str(x),mat2cell(1:obj.nnodes,1,ones(1,obj.nnodes)),'UniformOutput',false); + end + + obj.nodeLabels = nodeLabels; + if(~isequal(numel(obj.nodeLabels),size(adjMatrix,1),size(adjMatrix,2))) + error('graphViz4Matlab:dimMismatch','The number of labels must match the dimensions of adjmatrix.'); + end + obj.currentLayout = currentLayout; + end + + function createWindow(obj) + % Create the main window + obj.fig = figure(floor(1000*rand) + 1000); + set(obj.fig,'Name','graphViz4Matlab',... + 'NumberTitle' ,'off',... + 'Color','w' ,'Toolbar','none'); + obj.createAxes(); + ssize = get(0,'ScreenSize'); + pos = [ssize(3)/2,50,-20+ssize(3)/2,ssize(4)-200]; + set(obj.fig,'Position',pos); + obj.setCallbacks(); + obj.addButtons(); + + end + + function createAxes(obj) + % Create the axes upon which the graph will be displayed. + obj.ax = axes('Parent',obj.fig,'box','on','UserData','main'); + outerpos = get(obj.ax,'OuterPosition'); + axpos = outerpos; + axpos(4) = 0.90; + axpos(2) = 0.03; + axis manual + if(obj.square) + axis square + end + set(obj.ax,'Position',axpos,'XTick',[],'YTick',[],'LineWidth',0.5); + set(obj.ax,'ButtonDownFcn',@obj.axPressed); + end + + + + function setCallbacks(obj) + % Set the callback functions for the figure, i.e. functions that + % will be called when the user performs various actions. + set(obj.fig,'ResizeFcn' ,@obj.windowResized); + set(obj.fig,'WindowButtonMotionFcn' ,@obj.mouseMoved); + set(obj.fig,'WindowButtonUpFcn' ,@obj.buttonUp); + set(obj.fig,'DeleteFcn' ,@obj.deleted); + end + + function addNodes(obj) + % Add all of the nodes to the graph structure, but don't display + % them yet. + obj.nodeArray = []; + for i=1:obj.nnodes + newnode = graphViz4MatlabNode(obj.nodeLabels{i}); + newnode.containingGraph = obj; + newnode.showFullLabel = ~obj.splitLabels; + obj.nodeArray = [obj.nodeArray newnode]; + end + obj.addNodeDescriptions(obj.nodeDescriptions); + obj.addNodeColors(obj.nodeColors); + end + + function addNodeDescriptions(obj,nodeDescriptions) + % Add any descriptions to the newly created nodes. + if(~isempty(nodeDescriptions)) + if(numel(nodeDescriptions) == 1) + nodeDescriptions = repmat(nodeDescriptions,obj.nnodes,1); + end + for i=1:obj.nnodes + obj.nodeArray(i).description = nodeDescriptions{i}; + end + end + end + + function addNodeColors(obj,nodeColors) + % Shade the nodes according to the specified colors. If too few + % colors are specified, they are cycled through. + if(~isempty(nodeColors)) + if(~iscell(nodeColors)) + nodeColors = mat2cell(nodeColors,ones(1,size(nodeColors,1)),size(nodeColors,2)); + end + if(size(nodeColors,2) > size(nodeColors,1)) + nodeColors = nodeColors'; + end + if(numel(nodeColors) < obj.nnodes) + nodeColors = repmat(nodeColors,ceil(obj.nnodes/numel(nodeColors)),1); + nodeColors = nodeColors(1:obj.nnodes); + end + for i=1:obj.nnodes + obj.nodeArray(i).shade(nodeColors{i}); + end + obj.nodeColors = nodeColors; + end + end + + function addEdges(obj) + % Add all of the edges to the graph structure, but don't display + % them yet. + if(any(diag(obj.adjMatrix))) + fprintf('\nRemoving Self Loops\n'); + obj.adjMatrix = obj.adjMatrix - diag(diag(obj.adjMatrix)); + end + obj.edgeArray = struct('from',[],'to',[],'arrow',[]); + counter = 1; + for i=1:obj.nnodes + for j=1:obj.nnodes + if(obj.adjMatrix(i,j)) + obj.edgeArray(counter) = struct('from',obj.nodeArray(i),'to',obj.nodeArray(j),'arrow',-1); + obj.nodeArray(i).outedges = [obj.nodeArray(i).outedges,counter]; + obj.nodeArray(j).inedges = [obj.nodeArray(j).inedges,counter]; + counter = counter + 1; + end + end + end + obj.nedges = counter -1; + end + + function calculateMinMaxNodeSize(obj) + % calculates the maximum and minimum node sizes in data units + SCREEN_PROPORTION_MAX = 1/10; + SCREEN_PROPORTION_MIN = 1/35; + units = get(0,'Units'); + set(0,'Units','pixels'); + screensize = get(0,'ScreenSize'); + set(0,'Units',units); + axunits = get(obj.ax,'Units'); + set(obj.ax,'Units','pixels'); + axsize = get(obj.ax,'Position'); + set(obj.ax,'Units',axunits); + if(screensize(3) < screensize(4)) + dataUnitsPerPixel = abs(diff(xlim))/axsize(3); + obj.minNodeSize = (SCREEN_PROPORTION_MIN*screensize(3))*dataUnitsPerPixel; + obj.maxNodeSize = (SCREEN_PROPORTION_MAX*screensize(3))*dataUnitsPerPixel; + else + dataUnitsPerPixel = abs(diff(ylim))/axsize(4); + obj.minNodeSize = (SCREEN_PROPORTION_MIN*screensize(4))*dataUnitsPerPixel; + obj.maxNodeSize = (SCREEN_PROPORTION_MAX*screensize(4))*dataUnitsPerPixel; + end + end + + function layoutNodes(obj) + % Layout the nodes and edges according to the current layout + % algorithm. + if(obj.flipped) + adj = obj.adjMatrix'; + else + adj = obj.adjMatrix; + end + obj.currentLayout.dolayout(adj,obj.ax,obj.maxNodeSize); + nodesize = obj.currentLayout.nodeSize(); + locs = obj.currentLayout.centers(); + for i=1:obj.nnodes + node = obj.nodeArray(i); + node.resize(nodesize); + node.move(locs(i,1),locs(i,2)); + end + end + + function displayGraph(obj) + % Display all of the nodes and edges. + cla(obj.ax); + obj.setFontSize(); + for i=1:obj.nnodes + node = obj.nodeArray(i); + node.fontSize = obj.fontSize; + node.draw(obj.ax); + end + displayEdges(obj); + end + + function displayEdges(obj,indices) + % Display or refresh the specified edges. If none specified, all + % are refreshed. Currently only works with round nodes. + figure(obj.fig); + if(nargin < 2) + indices = 1:obj.nedges; + else + indices = unique(indices); + end + for i=1:numel(indices) + edge = obj.edgeArray(indices(i)); + [X,Y,Xarrow,Yarrow] = obj.calcPositions(edge); + if(ishandle(edge.arrow)) + delete(edge.arrow) + end + hold on; + edgeColor = obj.defaultEdgeColor; + if ~isempty(obj.edgeColors) + candidates = obj.edgeColors(findString(edge.from.label,obj.edgeColors(:,1)),:); + if size(candidates,1)==1 && strcmpi(candidates(1,2),'all') + edgeColor = candidates{1,3}; + else + edgeCol = candidates(findString(edge.to.label,candidates(:,2)),3); + if ~isempty(edgeCol); edgeColor = edgeCol{1}; end + end + end + edge.arrow = plot(X,Y,'LineWidth',2,'HitTest','off','Color',edgeColor); + if(~obj.undirected) + arrowHead = obj.displayArrowHead(X,Y,Xarrow,Yarrow,edgeColor); + edge.arrow = [edge.arrow arrowHead]; + end + hold off; + obj.edgeArray(indices(i)) = edge; + end + end + + function arrowHead = displayArrowHead(obj,X,Y,Xarrow,Yarrow,arrowColor) %#ok + % Displays the arrow head given the appropriate coordinates + % calculated via the calcPositions() function. + + arrowHead = patch('Faces' ,[1,2,3] ,... + 'Vertices' ,[Xarrow(1) Yarrow(1); Xarrow(2) Yarrow(2) ;X(2) Y(2)],... + 'FaceColor' ,arrowColor); + end + + function [X,Y,Xarrow,Yarrow] = calcPositions(obj,edge) + % Helper function for displayEdges() - calculates edge and arrow + % start and end positions in data units. + X = [edge.from.xpos edge.to.xpos]; + Y = [edge.from.ypos edge.to.ypos]; + ratio = (Y(2) - Y(1))/(X(2)-X(1)); + if(isinf(ratio)) + ratio = realmax; + end + % dx: x-distance from node1 center to perimeter in direction of node2 + % dy: y-distance from node1 center to perimeter in direction of node2 + % ddx: x-distance from node1 perimeter to base of arrow head + % ddy: y-distance from node1 perimeter to base of arrow head + % dpx: x-offset away from edge in perpendicular direction, for arrow head + % dpy: y-offset away from edge in perpendicular direction, for arrow head + + arrowSize = obj.maxNodeSize/10; + [dx,dy] = pol2cart(atan(ratio),edge.from.width/2); + [ddx,ddy] = pol2cart(atan(ratio),arrowSize); + ratio = 1/ratio; % now work out perpendicular directions. + if(isinf(ratio)) + ratio = realmax; + end + [dpx dpy] = pol2cart(atan(ratio),arrowSize/2); + ddx = abs(ddx); ddy = abs(ddy); dpx = abs(dpx); dpy = abs(dpy); + dx = abs(dx); dy = abs(dy); + if(X(1) < X(2)) + X(1) = X(1) + dx; X(2) = X(2) - dx; + else + X(1) = X(1) - dx; X(2) = X(2) + dx; + end + if(Y(1) < Y(2)) + Y(1) = Y(1) + dy; Y(2) = Y(2) - dy; + else + Y(1) = Y(1) - dy; Y(2) = Y(2) + dy; + end + if(X(1) <= X(2) && Y(1) <= Y(2)) + Xarrow(1) = X(2) - ddx - dpx; Xarrow(2) = X(2) - ddx + dpx; + Yarrow(1) = Y(2) - ddy + dpy; Yarrow(2) = Y(2) - ddy - dpy; + elseif(X(1) <= X(2) && Y(1) >= Y(2)) + Xarrow(1) = X(2) - ddx - dpx; Xarrow(2) = X(2) - ddx + dpx; + Yarrow(1) = Y(2) + ddy - dpy; Yarrow(2) = Y(2) + ddy + dpy; + elseif(X(1) >= X(2) && Y(1) <= Y(2)) + Xarrow(1) = X(2) + ddx - dpx; Xarrow(2) = X(2) + ddx + dpx; + Yarrow(1) = Y(2) - ddy - dpy; Yarrow(2) = Y(2) - ddy + dpy; + else % (X(1) >= (X(2) && Y(1) >= Y(2)) + Xarrow(1) = X(2) + ddx - dpx; Xarrow(2) = X(2) + ddx + dpx; + Yarrow(1) = Y(2) + ddy + dpy; Yarrow(2) = Y(2) + ddy - dpy; + end + end + + function addButtons(obj) + % Add user interface buttons. + if(~isempty(obj.toolbar)) + if(ishandle(obj.toolbar)) + delete(obj.toolbar); + obj.toolbar = []; + end + end + obj.toolbar = uitoolbar(obj.fig); + + % button icons + load glicons; + + uipushtool(obj.toolbar,... + 'ClickedCallback' ,@obj.decreaseFontSize,... + 'TooltipString' ,'Decrease Font Size',... + 'CData' ,icons.downblue); + + uipushtool(obj.toolbar,... + 'ClickedCallback' ,@obj.increaseFontSize,... + 'TooltipString' ,'Increase Font Size',... + 'CData' ,icons.upblue); + + uipushtool(obj.toolbar,... + 'ClickedCallback' ,@obj.tightenAxes,... + 'TooltipString' ,'Tighten Axes',... + 'CData' ,icons.expand); + + uipushtool(obj.toolbar,... + 'ClickedCallback' ,@obj.flip,... + 'TooltipString' ,'Flip/Reset Layout',... + 'CData' , icons.flip); + + uipushtool(obj.toolbar,... + 'ClickedCallback' ,@obj.shrinkNodes,... + 'TooltipString' ,'Decrease Node Size',... + 'CData' , icons.downdarkblue); + + uipushtool(obj.toolbar,... + 'ClickedCallback' ,@obj.growNodes,... + 'TooltipString' ,'Increase Node Size',... + 'CData' , icons.updarkblue); + + if(~isempty(obj.layoutButtons)) + for i=1:numel(obj.layoutButtons) + if(ishandle(obj.layoutButtons(i))) + delete(obj.layoutButtons(i)); + end + end + obj.layoutButtons = []; + end + + layoutNames = fieldnames(obj.layouts); + for i=1:numel(layoutNames) + layout = obj.layouts.(layoutNames{i}); + layoutButton = uipushtool(obj.toolbar,... + 'ClickedCallback', @obj.layoutButtonPushed,... + 'TooltipString', layout.shortDescription,... + 'UserData' , layoutNames{i},... + 'Separator' , 'on',... + 'CData' , layout.image); + obj.layoutButtons = [obj.layoutButtons,layoutButton]; + end + end + + function setFontSize(obj) + % fontsize = obj.maxFontSize; + fontSize = 20; + maxchars = size(char(obj.nodeLabels),2); + width = obj.nodeArray(1).width; + height = obj.nodeArray(1).height; + xpos = -10; ypos = -10; + t = text(xpos,ypos,repmat('g',1,maxchars),... + 'FontUnits' , 'points' ,... + 'Units' , 'data' ,... + 'HorizontalAlignment' , 'center' ,... + 'VerticalAlignment' , 'middle' ,... + 'FontWeight' , 'demi' ,... + 'LineStyle' , 'none' ,... + 'Margin' , 0.01 ,... + 'FontSize' , fontSize ,... + 'Color' , 'w' ); + + extent = get(t,'Extent'); + + while(extent(3) > width || extent(4) > height) + fontSize = fontSize - 1; + if(fontSize < 2), break,end + set(t,'FontSize',fontSize); + extent = get(t,'Extent'); + end + obj.fontSize = fontSize; + + end + + function asp = aspectRatio(obj) + % Return the current aspect ratio of the figure, width/height + + units = get(obj.ax,'Units'); + set(obj.ax,'Units','pixels'); + pos = get(obj.ax,'Position'); + set(obj.ax,'Units',units); + asp = (pos(3)/pos(4)); + + end + + function paperCrop(obj) + % Make the papersize the same as the the figure size. This is + % useful when saving as pdf. + units = get(obj.fig,'Units'); + set(obj.fig,'Units','inches'); + pos = get(obj.fig,'Position'); + set(obj.fig,'Units',units); + set(obj.fig,'PaperPositionMode','auto','PaperSize',pos(3:4)); + end + %% + % Callbacks + + function layoutButtonPushed(obj,buttonPushed,varargin) + % Called when a layout button is pushed. + name = get(buttonPushed,'UserData'); + obj.currentLayout = obj.layouts.(name); + axis square; + obj.redraw; + end + + function windowResized(obj,varargin) + % This function is called whenever the window is resized. It + % redraws the whole graph. + if(obj.isvisible) + obj.redraw; + obj.paperCrop(); + end + + end + + function mouseMoved(obj,varargin) + % This function is called whenever the mouse moves within the + % figure. + if(obj.groupSelectionMode == 2) + % Move all of the nodes & rectangle + currentPoint = get(obj.ax,'CurrentPoint'); + xlimits = get(obj.ax,'XLim'); + ylimits = get(obj.ax,'YLim'); + sdims = obj.groupSelectedDims; + xdiff = currentPoint(1,1) - obj.previousMouseLocation(1,1); + ydiff = currentPoint(1,2) - obj.previousMouseLocation(1,2); + + if(xdiff <=0) + xdiff = max(xdiff,(xlimits(1)-sdims(1))); + else + xdiff = min(xdiff,xlimits(2)-sdims(2)); + end + if(ydiff <=0) + ydiff = max(ydiff,(ylimits(1)-sdims(3))); + else + ydiff = min(ydiff,ylimits(2)-sdims(4)); + end + xnodepos = vertcat(obj.groupSelectedNodes.xpos) + xdiff; + ynodepos = vertcat(obj.groupSelectedNodes.ypos) + ydiff; + for i=1:numel(obj.groupSelectedNodes) + obj.groupSelectedNodes(i).move(xnodepos(i),ynodepos(i)); + end + recpos = get(obj.groupSelectedRect,'Position'); + recpos(1) = recpos(1) + xdiff; + recpos(2) = recpos(2) + ydiff; + obj.groupSelectedDims = [recpos(1),recpos(1)+recpos(3),recpos(2),recpos(2)+recpos(4)]; + set(obj.groupSelectedRect,'Position',recpos); + edges = [obj.groupSelectedNodes.inedges,obj.groupSelectedNodes.outedges]; + obj.displayEdges(edges); + obj.previousMouseLocation = currentPoint; + else + if(isempty(obj.selectedNode)), return,end + currentPoint = get(obj.ax,'CurrentPoint'); + x = currentPoint(1,1); y = currentPoint(1,2); + xl = xlim + [obj.selectedNode.width,-obj.selectedNode.width]/2; + yl = ylim + [obj.selectedNode.height,-obj.selectedNode.height]/2; + x = min(max(xl(1),x),xl(2)); + y = min(max(yl(1),y),yl(2)); + obj.selectedNode.move(x,y); + obj.displayEdges([obj.selectedNode.inedges,obj.selectedNode.outedges]); + end + end + + function buttonUp(obj,varargin) + % This function executes when the mouse button is released. + if(obj.groupSelectionMode == 2) + obj.clearGroupSelection(); + return; + end + if(isempty(obj.selectedNode)),return,end + obj.selectedNode.deselect(); + + obj.selectedNode.useFullLabel = false; + obj.selectedNode.fontSize = obj.selectedFontSize; + obj.selectedNode.redraw(); + obj.selectedNode = []; + set(gcf,'Pointer','arrow'); + end + + function axPressed(obj,varargin) + % Called when the user selects the axes but not a node + switch obj.groupSelectionMode + case 0 % hasn't been selected yet + xpos = vertcat(obj.nodeArray.xpos); + ypos = vertcat(obj.nodeArray.ypos); + p1 = get(obj.ax,'CurrentPoint'); + rbbox; % returns after box drawn + p2 = get(obj.ax,'CurrentPoint'); + xleft = min(p1(1,1),p2(1,1)); + xright = max(p1(1,1),p2(1,1)); + ylower = min(p1(1,2),p2(1,2)); + yupper = max(p1(1,2),p2(1,2)); + selectedX = (xpos <= xright) & (xpos >= xleft); + selectedY = (ypos <= yupper) & (ypos >= ylower); + selected = selectedX & selectedY; + if(~any(selected)),return,end + obj.groupSelectionMode = 1; + obj.groupSelectedNodes = obj.nodeArray(selected); + for i=1:numel(obj.groupSelectedNodes) + node = obj.groupSelectedNodes(i); + node.select(); + node.redraw(); + end + + w = obj.groupSelectedNodes(1).width/2; + h = obj.groupSelectedNodes(1).height/2; + x = vertcat(obj.groupSelectedNodes.xpos); + y = vertcat(obj.groupSelectedNodes.ypos); + minx = min(x)-w; maxx = max(x)+w; miny = min(y)-h; maxy = max(y)+h; + obj.groupSelectedDims = [minx,maxx,miny,maxy]; + obj.groupSelectedRect = rectangle('Position',[minx,miny,maxx-minx,maxy-miny],'LineStyle','--','EdgeColor','r'); + case 1 % nodes selected + obj.groupSelectionStage1(); + case 2 %not ever reached in this function + obj.clearGroupSelection(); + end + end + + function groupSelectionStage1(obj) + % Called after a group of nodes has been selected and the mouse + % button has been pressed somewhere on the axes, (or on a node). + p = get(obj.ax,'CurrentPoint'); + obj.previousMouseLocation = p; + dims = obj.groupSelectedDims; + if(p(1,1) >= dims(1) && p(1,1) <= dims(2) && p(1,2) >= dims(3) && p(1,2) <=dims(4)) + set(gcf,'Pointer','hand'); + obj.groupSelectionMode = 2; + else + obj.clearGroupSelection(); + end + end + + function clearGroupSelection(obj) + % Clear a group selection + if(ishandle(obj.groupSelectedRect)) + delete(obj.groupSelectedRect); + end + obj.groupSelectedRect = []; + for i=1:numel(obj.groupSelectedNodes) + obj.groupSelectedNodes(i).deselect(); + end + obj.groupSelectedNodes = []; + obj.groupSelectedDims = []; + obj.groupSelectionMode = 0; + set(gcf,'Pointer','arrow'); + end + + function deleted(obj,varargin) + % Called when the figure is deleted by the user. + obj.isvisible = false; + obj.clearGroupSelection(); + end + + function singleClick(obj,node) + % Called when a user single clicks on a node. + obj.selectedNode = node; + node.select(); + set(gcf,'Pointer','hand'); + obj.selectedFontSize = node.fontSize; + node.useFullLabel = true; + node.fontSize = max(15,node.fontSize*1.5); + node.redraw(); + end + + function doubleClick(obj,node) + % Called when a user double clicks on a node + if isempty(obj.doubleClickFn) + description = node.description; + if(~iscell(description)) + description = {description}; + end + answer = inputdlg('',node.label,4,description); + if(~isempty(answer)) + node.description = answer; + end + else + obj.doubleClickFn(node.label); + end + end + + function rightClick(obj,node) %#ok + % Called when a user right clicks on a node + if(node.isshaded) + node.unshade(); + else + node.shade(); + end + end + + function shiftClick(obj,node) %#ok + % Called when a user shift clicks on a node + display(node); + end + + + end + + methods + % Callbacks that can be called by the user programmatically. + function shrinkNodes(obj,varargin) + % Shrink the nodes to 95% of their original size, (but not smaller + % than a calculated minimum. + obj.clearGroupSelection(); + s = max(0.8*obj.nodeArray(1).width,obj.minNodeSize); + obj.nodeArray(1).resize(s); + obj.setFontSize(); + for i=1:obj.nnodes + node = obj.nodeArray(i); + node.fontSize = obj.fontSize; + node.resize(s); + end + obj.displayEdges(); + end + + function growNodes(obj,varargin) + % Grow the nodes to 1/0.95 times their original size, (but not + % larger than a calculated maximum. + obj.clearGroupSelection(); + s = min(obj.nodeArray(1).width/0.8,1.5*obj.maxNodeSize); + obj.nodeArray(1).resize(s); + obj.setFontSize(); + for i=1:obj.nnodes + node = obj.nodeArray(i); + node.fontSize = obj.fontSize; + node.resize(s); + end + obj.displayEdges(); + end + + function increaseFontSize(obj,varargin) + % Increase the fontsize of all the nodes by 0.5 points. + current = get(obj.nodeArray(1).labelhandle,'FontSize'); + newsize = current + 1; + for i=1:numel(obj.nodeArray) + node = obj.nodeArray(i); + node.fontSize = newsize; + node.redraw(); + end + end + + function decreaseFontSize(obj,varargin) + % Decrease the fontsize of all the nodes by 0.5 points. + current = get(obj.nodeArray(1).labelhandle,'FontSize'); + newsize = max(current - 1,1); + for i=1:numel(obj.nodeArray) + node = obj.nodeArray(i); + node.fontSize = newsize; + node.redraw(); + end + end + + function XY = getNodePositions(obj) + % Return the current positions of the nodes. The bottom left + % corner is [0 0] and the top right is [1 1]. Node positions + % refer to the centre of a node. + XY = zeros(obj.nnodes, 2); + for i=1:obj.nnodes + XY(i, 1) = obj.nodeArray(i).xpos; + XY(i, 2) = obj.nodeArray(i).ypos; + end + end + + function setNodePositions(obj, XY) + % Programmatically set the node positions + % XY(i, 1) is the xposition of node i, XY(i, 2) is the yposition. + for i=1:obj.nnodes + obj.nodeArray(i).move(XY(i, 1), XY(i, 2)); + end + obj.displayGraph(); + end + + function moveNode(obj, nodeIndex, xpos, ypos) + % Programmatically set a node position. + obj.nodeArray(nodeIndex).move(xpos, ypos); + obj.displayGraph(); + end + + end +end