diff --git a/utils/buildDependencyGraph.m b/utils/buildDependencyGraph.m new file mode 100644 index 0000000..ccf193f --- /dev/null +++ b/utils/buildDependencyGraph.m @@ -0,0 +1,217 @@ +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 \ No newline at end of file diff --git a/utils/nappSaveDataTable.m b/utils/nappSaveDataTable.m index 4fb4478..b5b95e7 100644 --- a/utils/nappSaveDataTable.m +++ b/utils/nappSaveDataTable.m @@ -1,70 +1,48 @@ function saveFilename = nappSaveDataTable(DataTable, options) -% NAPPSAVEDATATABLE Automatically saves the data table with a dynamic filename. -% -% NAPPSAVEDATATABLE(DataTable) saves the provided table into a .mat file. -% The filename is automatically generated based on the script or Live -% Script that called this function (e.g., "myScript_FileList.mat"). -% -% The variable inside the .mat file will keep the exact same name it had -% in your workspace (e.g., if you pass 'FileList', it saves as 'FileList'). -% -% Name-Value arguments: -% suffix - Text to append to the script name (default: nothing) - arguments DataTable table - - % Name-Value arguments options.suffix char = "" + options.varName char = "" end currentScriptName = ''; - % 1. Determine the name of the script that CALLED this function + % 1. Determine script name (Your existing logic is solid here) stackInfo = dbstack; if length(stackInfo) > 1 - % stackInfo(1) is nappSaveDataTable, stackInfo(2) is the caller [~, currentScriptName, ~] = fileparts(stackInfo(2).file); - - % INTERCEPT LIVE EDITOR: If it's a hidden temporary evaluation file, - % clear the name so we force it to use the fallback below. if contains(currentScriptName, 'LiveEditorEvaluationHelper') currentScriptName = ''; end end - % 2. Fallback if run from the command window or intercepted from the Live Editor if isempty(currentScriptName) try - % Ask the MATLAB editor for the actual name of the currently open tab [~, currentScriptName, ~] = fileparts(matlab.desktop.editor.getActiveFilename); catch currentScriptName = 'ManualRun'; end end - % Ensure we have a valid string (just in case the editor is closed) - if isempty(currentScriptName) - currentScriptName = 'WorkspaceData'; - end - - % 3. Generate the final filename + % 2. Generate the final filename saveFilename = sprintf('%s%s.mat', currentScriptName, options.suffix); - % 4. Determine the original variable name passed by the user (e.g., "FileList") - originalVarName = inputname(1); - - % If it has no workspace name, default to 'DataTable' - if isempty(originalVarName) - originalVarName = 'DataTable'; + % 3. Determine the variable name + % Priority: 1. Manual override, 2. inputname, 3. Default 'DataTable' + if options.varName ~= "" + finalVarName = options.varName; + else + finalVarName = inputname(1); + if isempty(finalVarName) + finalVarName = 'DataTable'; + end end - % 5. Create a local variable with that exact name so the .mat file stores it correctly - eval(sprintf('%s = DataTable;', originalVarName)); - - % 6. Save the specific variable to the file - save(saveFilename, originalVarName); + % 4. SAVE + % We create a struct where the field name is our dynamic string. + % Then we save the struct with the '-struct' flag. + saveData.(finalVarName) = DataTable; + save(saveFilename, '-struct', 'saveData'); - % 7. Print confirmation - fprintf('✅ Successfully saved %s to: %s\n', originalVarName, saveFilename); + fprintf('✅ Successfully saved %s to: %s\n', finalVarName, saveFilename); end \ No newline at end of file diff --git a/utils/plotNodeHistory.m b/utils/plotNodeHistory.m new file mode 100644 index 0000000..7248025 --- /dev/null +++ b/utils/plotNodeHistory.m @@ -0,0 +1,152 @@ +function historyG = plotNodeHistory(G, targetFile) +% PLOTNODEHISTORY Extracts and plots the entire upstream history for a single file. +% +% USAGE: +% G = buildDependencyGraph(); +% plotNodeHistory(G, 'Results/FinalPlot.fig'); +% +% ========================================================================= +% 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). +% +% ========================================================================= +% ========================================================================= +% 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. +% ========================================================================= + + % 1. Check if the file actually exists in the graph + if findnode(G, targetFile) == 0 + error('File "%s" was not found in the dependency graph.', targetFile); + end + + % 2. Reverse the graph's arrows to walk backwards up the chain + revG = flipedge(G); + + % 3. Use Breadth-First Search to find all "ancestors" (upstream nodes) + % This returns the target file itself plus everything that leads to it. + ancestors = bfsearch(revG, targetFile); + + % 4. Extract the historical subgraph from the ORIGINAL graph + historyG = subgraph(G, ancestors); + + % --------------------------------------------------------- + % 5. Apply Colors + % --------------------------------------------------------- + numNodes = numnodes(historyG); + nodeColors = zeros(numNodes, 3); + + colorScript = [0 0.4470 0.7410]; % Blue for .m scripts + colorData = [0.8500 0.3250 0.0980]; % Orange for .mat data + colorFig = [0.4940 0.1840 0.5560]; % Purple for figures (.fig, .png, etc.) + colorOther = [0.4660 0.6740 0.1880]; % Green for anything else + colorTarget = [0.9290 0.6940 0.1250]; % Gold/Yellow for the TARGET file + + for n = 1:numNodes + nodeName = historyG.Nodes.Name{n}; + + if strcmp(nodeName, targetFile) + nodeColors(n, :) = colorTarget; + elseif endsWith(nodeName, '.m', 'IgnoreCase', true) + nodeColors(n, :) = colorScript; + elseif endsWith(nodeName, '.mat', 'IgnoreCase', true) + nodeColors(n, :) = colorData; + elseif endsWith(nodeName, {'.fig', '.png', '.svg', '.pdf'}, 'IgnoreCase', true) + nodeColors(n, :) = colorFig; + else + nodeColors(n, :) = colorOther; + end + end + + % --------------------------------------------------------- + % 6. Plot the History Graph + % --------------------------------------------------------- + fig = figure('Name', ['History for ' targetFile], 'Color', 'w'); + p = plot(historyG, 'Layout', 'layered', ... + 'NodeLabel', historyG.Nodes.Name, ... + 'Interpreter', 'none', ... + 'NodeColor', nodeColors, ... + 'MarkerSize', 8, ... + 'EdgeColor', [0.7 0.7 0.7], ... + 'LineWidth', 1.5, ... + 'ArrowSize', 12); + + % Highlight the target file + highlight(p, targetFile, 'MarkerSize', 14); + + p.NodeFontSize = 10; + p.NodeFontWeight = 'bold'; + title(['Full Provenance Chain for: ' targetFile], 'Interpreter', 'none'); + + % --------------------------------------------------------- + % 7. Export Data + % --------------------------------------------------------- + safeName = strrep(targetFile, '/', '_'); + safeName = strrep(safeName, '\', '_'); + outputFile = sprintf('Dependency_History_%s.png', safeName); + + % --- Export text for yEd Graph Editor (TGF Format) --- + if numedges(historyG) > 0 + % Extract the exact edges from the history subgraph + histSources = historyG.Edges.EndNodes(:,1); + histTargets = historyG.Edges.EndNodes(:,2); + + % 1. Create a unique list of all nodes in this history graph + allNodes = unique([histSources; histTargets; targetFile]); + + safeName = strrep(targetFile, '/', '_'); + safeName = strrep(safeName, '\', '_'); + tgfFile = sprintf('Dependency_History_%s.tgf', safeName); + 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(histSources) + srcIdx = find(strcmp(allNodes, histSources{i})); + tgtIdx = find(strcmp(allNodes, histTargets{i})); + fprintf(fid, '%d %d\n', srcIdx, tgtIdx); + end + + fclose(fid); + fprintf('Generated %s for yEd Graph Editor.\n', tgfFile); + + end + % --- Export high-res PNG --- + exportgraphics(fig, outputFile, 'Resolution', 300, 'BackgroundColor', 'w'); + fprintf('Saved full history graph to %s\n', outputFile); +end \ No newline at end of file diff --git a/utils/plotSingleFileDependencies.m b/utils/plotSingleFileDependencies.m new file mode 100644 index 0000000..e4f1be2 --- /dev/null +++ b/utils/plotSingleFileDependencies.m @@ -0,0 +1,151 @@ +function subG = plotSingleFileDependencies(G, targetFile) +% PLOTSINGLEFILEDEPENDENCIES Extracts and plots the dependencies for a single file. +% +% USAGE: +% G = buildDependencyGraph(); % First, get the master graph +% plotSingleFileDependencies(G, 'FitList.mat'); +% +% ========================================================================= +% 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). +% +% ========================================================================= +% ========================================================================= +% 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. +% ========================================================================= + + % 1. Check if the file actually exists in the graph + if findnode(G, targetFile) == 0 + error('File "%s" was not found in the dependency graph. Check spelling and paths.', targetFile); + end + + % 2. Find scripts that create it (incoming edges) + creators = predecessors(G, targetFile); + + % 3. Find scripts that use it (outgoing edges) + consumers = successors(G, targetFile); + + % 4. Combine them into a single list of nodes to keep + neighborhood = unique([targetFile; creators; consumers]); + + % 5. Extract the smaller subgraph + subG = subgraph(G, neighborhood); + + % --------------------------------------------------------- + % 6. Apply Colors (Highlighting the Target File) + % --------------------------------------------------------- + numNodes = numnodes(subG); + nodeColors = zeros(numNodes, 3); + + 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 + colorTarget = [0.9290 0.6940 0.1250]; % Gold/Yellow for the TARGET file + + for n = 1:numNodes + nodeName = subG.Nodes.Name{n}; + if strcmp(nodeName, targetFile) + nodeColors(n, :) = colorTarget; % Apply Gold to target + elseif endsWith(nodeName, '.m', 'IgnoreCase', true) + nodeColors(n, :) = colorScript; + elseif endsWith(nodeName, '.mat', 'IgnoreCase', true) + nodeColors(n, :) = colorData; + else + nodeColors(n, :) = colorOther; + end + end + + % --------------------------------------------------------- + % 7. Plot the Subgraph + % --------------------------------------------------------- + fig = figure('Name', ['Dependencies for ' targetFile], 'Color', 'w'); + p = plot(subG, 'Layout', 'layered', ... + 'NodeLabel', subG.Nodes.Name, ... + 'Interpreter', 'none', ... + 'NodeColor', nodeColors, ... + 'MarkerSize', 8, ... + 'EdgeColor', [0.7 0.7 0.7], ... + 'LineWidth', 1.5, ... + 'ArrowSize', 12); + + % Make the target file visually stand out by making its marker larger + highlight(p, targetFile, 'MarkerSize', 14); + + p.NodeFontSize = 10; + p.NodeFontWeight = 'bold'; + title(['Data Flow for: ' targetFile], 'Interpreter', 'none'); + + % --------------------------------------------------------- + % 8. Export Data + % --------------------------------------------------------- + safeName = strrep(targetFile, '/', '_'); + safeName = strrep(safeName, '\', '_'); + % Use the new consistent prefix + outputFile = sprintf('Dependency_Node_%s.png', safeName); + + + % --- Export text for yEd Graph Editor (TGF Format) --- + if numedges(subG) > 0 + % Extract the exact edges from the subgraph + subSources = subG.Edges.EndNodes(:,1); + subTargets = subG.Edges.EndNodes(:,2); + + % 1. Create a unique list of all nodes in this subgraph + allNodes = unique([subSources; subTargets; targetFile]); + + safeName = strrep(targetFile, '/', '_'); + safeName = strrep(safeName, '\', '_'); + tgfFile = sprintf('Dependency_Node_%s.tgf', safeName); + 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(subSources) + srcIdx = find(strcmp(allNodes, subSources{i})); + tgtIdx = find(strcmp(allNodes, subTargets{i})); + fprintf(fid, '%d %d\n', srcIdx, tgtIdx); + end + + fclose(fid); + fprintf('Generated %s for yEd Graph Editor.\n', tgfFile); + end + % --- Export high-res PNG --- + exportgraphics(fig, outputFile, 'Resolution', 300, 'BackgroundColor', 'w'); + fprintf('Saved isolated dependency graph to %s\n', outputFile); +end \ No newline at end of file