Files
bartels-rausch e5226802a3 Add project dependency tracking system with yEd and PNG export
- Implement @DEP_IN/OUT/RUN tag standard for MATLAB script metadata
- Add buildDependencyGraph.m for full project mapping and yEd (.tgf) export
- Add plotSingleFileDependencies.m and plotNodeHistory.m for targeted analysis
- Standardize output naming convention to 'Dependency_*' for Finder organization
- Configure high-resolution PNG export as default static fallback
2026-04-02 10:56:28 +02:00

217 lines
8.0 KiB
Matlab

function G = buildDependencyGraph(scanPath, outputFile)
% BUILDDEPENDENCYGRAPH Scans MATLAB files for custom dependency tags and builds a directed graph.
%
% USAGE:
% G = buildDependencyGraph();
% G = buildDependencyGraph(scanPath);
% G = buildDependencyGraph(scanPath, outputFile);
%
% DEPENDENCY TAGS:
% % @DEP_IN: filename.mat
% % @DEP_OUT: filename.mat
% % @DEP_RUN: scriptname.m
%
%
% =========================================================================
% SCRIPT OUTPUTS & VISUALIZATION OPTIONS
% =========================================================================
% 1. MATLAB Figure: A standard MATLAB figure window will automatically
% open displaying the dependency graph.
%
% 2. Static Image (PNG): A high-resolution (300 DPI) PNG file is saved to
% your current directory (e.g., 'ProjectDependencies.png'). This is
% ideal for quick viewing, sharing, or inserting into reports.
%
% 3. Interactive Graph (yEd Graph Editor): For complex networks where you
% can drag, auto-layout, and isolate nodes:
% a) Find the generated text file (e.g., 'Dependency_Graph.tgf').
% b) Open the free yEd Graph Editor app (macOS/Windows/Linux).
% c) Go to File -> Open and select the .tgf file.
% d) To arrange the messy graph, go to the top menu:
% Layout -> Hierarchical (or press Cmd+Option+H).
%
% =========================================================================
% COMPANION FUNCTIONS
% =========================================================================
% Once the main graph (G) is built, you can pass it to these additional
% functions to inspect specific files in detail:
%
% plotSingleFileDependencies(G, 'FileName.mat');
% -> Extracts and plots only the immediate "neighborhood" (the scripts
% that directly create it, and the scripts that directly read it).
%
% plotNodeHistory(G, 'FileName.fig');
% -> Traces the data provenance. It plots the entire upstream chain
% of all scripts and data files that ultimately led to this file.
% =========================================================================
% =========================================================================
% HOW TO UNTANGLE THE GRAPH IN yEd (First-time Open)
% =========================================================================
% When you first open the .tgf file, the nodes will be in a messy pile.
% Follow these 3 steps to instantly format the dependency tree:
%
% 1. UNTANGLE: Go to Layout -> Hierarchical (or press Cmd+Option+H).
% This arranges the scripts and data files in a logical flow.
%
% 2. FIX BOX SIZES: Press Cmd+A to select all, then go to:
% Tools -> Fit Node to Label... -> Click OK.
% This resizes the boxes to fit your file names perfectly.
%
% 3. VIEW ALL: Click the 'Fit Content' icon (magnifying glass with square)
% in the top toolbar to center the graph on your screen.
%
% You can now click and drag any node; the arrows will follow automatically.
% =========================================================================
% Handle optional input arguments
if nargin < 1 || isempty(scanPath)
scanPath = pwd;
end
if nargin < 2 || isempty(outputFile)
outputFile = 'Dependency_MasterGraph.png';
end
% 1. Find scripts in the main directory
scriptFiles = dir(fullfile(scanPath, '*.m'));
for i = 1:length(scriptFiles)
scriptFiles(i).relPath = scriptFiles(i).name;
end
% 2. Find subdirectories (excluding '.' and '..')
allDirs = dir(scanPath);
subDirs = allDirs([allDirs.isdir] & ~startsWith({allDirs.name}, '.'));
% 3. Scan 1 level deep into subdirectories
for i = 1:length(subDirs)
subDirName = subDirs(i).name;
subFolderPath = fullfile(scanPath, subDirName);
subFiles = dir(fullfile(subFolderPath, '*.m'));
for j = 1:length(subFiles)
subFiles(j).relPath = [subDirName '/' subFiles(j).name];
end
if ~isempty(subFiles)
if isempty(scriptFiles)
scriptFiles = subFiles;
else
scriptFiles = [scriptFiles; subFiles];
end
end
end
% 4. Initialize Dependency Lists
sources = {};
targets = {};
% 5. Extract Tags
for i = 1:length(scriptFiles)
scriptName = scriptFiles(i).relPath;
if contains(scriptName, 'buildDependencyGraph.m'), continue; end
actualPath = fullfile(scriptFiles(i).folder, scriptFiles(i).name);
code = fileread(actualPath);
inputs = regexp(code, '@DEP_IN:\s*([^\n\r]+)', 'tokens');
outputs = regexp(code, '@DEP_OUT:\s*([^\n\r]+)', 'tokens');
runs = regexp(code, '@DEP_RUN:\s*([^\n\r]+)', 'tokens');
for k = 1:length(inputs)
depName = strrep(strtrim(inputs{k}{1}), '\', '/');
sources{end+1} = depName;
targets{end+1} = scriptName;
end
for k = 1:length(outputs)
depName = strrep(strtrim(outputs{k}{1}), '\', '/');
sources{end+1} = scriptName;
targets{end+1} = depName;
end
for k = 1:length(runs)
depName = strrep(strtrim(runs{k}{1}), '\', '/');
sources{end+1} = scriptName;
targets{end+1} = depName;
end
end
if isempty(sources)
disp('No @DEP_ tags found in the scanned files.');
G = digraph();
return;
end
G = digraph(sources, targets);
% ---------------------------------------------------------
% 6. Construct Node Colors Based on File Extension
% ---------------------------------------------------------
numNodes = numnodes(G);
nodeColors = zeros(numNodes, 3);
% Define standard colorblind-friendly colors
colorScript = [0 0.4470 0.7410]; % Blue for .m scripts
colorData = [0.8500 0.3250 0.0980]; % Orange for .mat data
colorOther = [0.4660 0.6740 0.1880]; % Green for anything else (e.g. .csv)
for n = 1:numNodes
nodeName = G.Nodes.Name{n};
if endsWith(nodeName, '.m', 'IgnoreCase', true)
nodeColors(n, :) = colorScript;
elseif endsWith(nodeName, '.mat', 'IgnoreCase', true)
nodeColors(n, :) = colorData;
else
nodeColors(n, :) = colorOther;
end
end
% Plot the Graph
fig = figure('Name', 'Project Dependencies', 'Color', 'w');
p = plot(G, 'Layout', 'layered', ...
'NodeLabel', G.Nodes.Name, ...
'Interpreter', 'none', ...
'NodeColor', nodeColors, ... % <-- Using the new color array here
'MarkerSize', 8, ...
'EdgeColor', [0.7 0.7 0.7], ... % Lightened edge color for visual clarity
'LineWidth', 1.5, ...
'ArrowSize', 12);
p.NodeFontSize = 10;
p.NodeFontWeight = 'bold';
title('Data and Script Dependency Flow','Interpreter', 'none');
% ---------------------------------------------------------
% 7. Export
% ---------------------------------------------------------
% Change the output file extension
outputFile = strrep(outputFile, '.svg', '.png');
% --- Export text for yEd Graph Editor (TGF Format) ---
% 1. Create a unique list of all nodes
allNodes = unique([sources, targets]);
tgfFile = 'Dependency_MasterGraph.tgf';
fid = fopen(tgfFile, 'w');
% 2. Write the Node List (Format: ID Label)
for i = 1:length(allNodes)
fprintf(fid, '%d %s\n', i, allNodes{i});
end
% 3. Write the Separator
fprintf(fid, '#\n');
% 4. Write the Edge List (Format: SourceID TargetID)
for i = 1:length(sources)
srcIdx = find(strcmp(allNodes, sources{i}));
tgtIdx = find(strcmp(allNodes, targets{i}));
fprintf(fid, '%d %d\n', srcIdx, tgtIdx);
end
fclose(fid);
disp('Generated Dependency_Graph.tgf for yEd Graph Editor.');
% Export as a 300 DPI high-resolution PNG
exportgraphics(fig, outputFile, 'Resolution', 300, 'BackgroundColor', 'w');
fprintf('Dependency graph saved as %s\n', outputFile);
end