From 4e01aaf6ad594fa6a8202ff4c128cec1840ee047 Mon Sep 17 00:00:00 2001 From: Iahn Cajigas Date: Thu, 5 Mar 2026 14:30:50 -0500 Subject: [PATCH] PR1: add global plot style API and modern style hooks Parity: PASS (numeric) Plots: PASS (structure) --- FitResSummary.m | 18 ++++ FitResult.m | 36 ++++++++ README.md | 10 +++ SignalObj.m | 10 ++- docs/DEVPLAN.md | 8 ++ tools/+nstat/applyPlotStyle.m | 158 ++++++++++++++++++++++++++++++++++ tools/+nstat/getPlotStyle.m | 39 +++++++++ tools/+nstat/setPlotStyle.m | 31 +++++++ 8 files changed, 308 insertions(+), 2 deletions(-) create mode 100644 tools/+nstat/applyPlotStyle.m create mode 100644 tools/+nstat/getPlotStyle.m create mode 100644 tools/+nstat/setPlotStyle.m diff --git a/FitResSummary.m b/FitResSummary.m index ab3445c..ba8d21a 100644 --- a/FitResSummary.m +++ b/FitResSummary.m @@ -407,6 +407,12 @@ function setCoeffRange(frsObj,minVal,maxVal) xticklabel_rotate([],90,[],'Fontsize',10);%rotateticklabel(gca,-90); end + if exist('nstat.applyPlotStyle', 'file') == 2 + try + nstat.applyPlotStyle(h); + catch + end + end end function handle = plot3dCoeffSummary(frsObj,h) % handle = plot3dCoeffSummary(frsObj,h) @@ -605,6 +611,12 @@ function setCoeffRange(frsObj,minVal,maxVal) h= legend; set(h,'Location','BestOutside'); ylabel(''); xlabel(''); title(''); end end + if exist('nstat.applyPlotStyle', 'file') == 2 + try + nstat.applyPlotStyle(handle); + catch + end + end end function handle = plotSummary(frsObj) @@ -636,6 +648,12 @@ function setCoeffRange(frsObj,minVal,maxVal) set([hx hy],'FontName', 'Arial','FontSize',11,'FontWeight','bold'); title('Change in BIC Across Neurons','FontWeight','bold','FontSize',11,'FontName','Arial'); set(gca,'XTickLabelRotation',90); + if exist('nstat.applyPlotStyle', 'file') == 2 + try + nstat.applyPlotStyle(handle); + catch + end + end end function handle = boxPlot(frsObj,X,diffIndex,h,dataLabels,varargin) if(nargin<3) diff --git a/FitResult.m b/FitResult.m index b0a0498..bb7bb23 100644 --- a/FitResult.m +++ b/FitResult.m @@ -1117,6 +1117,12 @@ function computePlotParams(fitObj,fitNum) 'LineWidth' , 1 ); hx=get(gca,'XLabel'); hy=get(gca,'YLabel'); set([hx hy],'FontName', 'Arial','FontSize',12,'FontWeight','bold'); + if exist('nstat.applyPlotStyle', 'file') == 2 + try + nstat.applyPlotStyle(gca); + catch + end + end end @@ -1138,6 +1144,12 @@ function computePlotParams(fitObj,fitNum) subplot(2,4,4); fitObj.plotSeqCorr; subplot(2,4,[7 8]); fitObj.plotResidual; subplot(2,4,[5 6]); fitObj.plotCoeffs; + if exist('nstat.applyPlotStyle', 'file') == 2 + try + nstat.applyPlotStyle(h); + catch + end + end end function handle = KSPlot(fitObj,fitNum) % handle = KSPlot(fitObj) @@ -1181,6 +1193,12 @@ function computePlotParams(fitObj,fitNum) 'YTick' , 0:.2:1, ... 'XTick' , 0:.2:1, ... 'LineWidth' , 1 ); + if exist('nstat.applyPlotStyle', 'file') == 2 + try + nstat.applyPlotStyle(gca); + catch + end + end end function structure = toStructure(fitObj) @@ -1279,6 +1297,12 @@ function computePlotParams(fitObj,fitNum) 'YTick' , 0:.25:1, ... 'XTick' , 0:.25:1, ... 'LineWidth' , 1 ); + if exist('nstat.applyPlotStyle', 'file') == 2 + try + nstat.applyPlotStyle(gca); + catch + end + end @@ -1336,6 +1360,12 @@ function computePlotParams(fitObj,fitNum) v=axis; maxY = max(abs(v(3:4)))*(1.1); %add 10% axis([v(1:2) -maxY maxY]); + if exist('nstat.applyPlotStyle', 'file') == 2 + try + nstat.applyPlotStyle(gca); + catch + end + end end function handle = plotResidual(fitObj) % handle = plotResidual(fitObj) @@ -1356,6 +1386,12 @@ function computePlotParams(fitObj,fitNum) v=axis; maxY = max(abs(v(3:4)))*(1.1); %add 10% axis([v(1:2) -maxY maxY]); + if exist('nstat.applyPlotStyle', 'file') == 2 + try + nstat.applyPlotStyle(gca); + catch + end + end end diff --git a/README.md b/README.md index 9f813ed..ecf1364 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,16 @@ nSTAT_Install('RebuildDocSearch', true, 'CleanUserPathPrefs', false) - `RebuildDocSearch` rebuilds the help search database in `helpfiles/`. - `CleanUserPathPrefs` removes stale user MATLAB path entries. +Plot style policy: + +```matlab +% Modern readability-focused plots (default) +nstat.setPlotStyle('modern'); + +% Legacy visual style for strict reproduction +nstat.setPlotStyle('legacy'); +``` + Rendered help documentation (GitHub Pages): - https://cajigaslab.github.io/nSTAT/ diff --git a/SignalObj.m b/SignalObj.m index e3eb294..bcd33c1 100644 --- a/SignalObj.m +++ b/SignalObj.m @@ -2043,8 +2043,14 @@ function clearPlotProps(sObj,index) h=plot(handle,sObj.time,sObj.data(:,sArray)); end - sObj.setupPlots(handle,sArray); - end + sObj.setupPlots(handle,sArray); + if exist('nstat.applyPlotStyle', 'file') == 2 + try + nstat.applyPlotStyle(handle); + catch + end + end + end function setupPlots(sObj,handle,sArray) %setupPlots(sObj,sArray) %Sets the labels for the x-axis, y-axis, and legend. diff --git a/docs/DEVPLAN.md b/docs/DEVPLAN.md index 3ee2178..bc641dd 100644 --- a/docs/DEVPLAN.md +++ b/docs/DEVPLAN.md @@ -76,6 +76,14 @@ Policy: - Always compare **structure** (figure/axes/traces/labels/legend metadata). - Optional pixel parity for `legacy` style only (determinism-dependent). +## Plot Style Toggle +- Global preference API (PR1): + - `nstat.setPlotStyle('modern')` (default) + - `nstat.setPlotStyle('legacy')` + - `nstat.getPlotStyle` +- Plotting methods call `nstat.applyPlotStyle` and preserve legacy behavior + when style is set to `legacy`. + ## Fixture Update Policy - Fixtures are versioned and treated as protected golden artifacts. - Update fixtures only when: diff --git a/tools/+nstat/applyPlotStyle.m b/tools/+nstat/applyPlotStyle.m new file mode 100644 index 0000000..ce17005 --- /dev/null +++ b/tools/+nstat/applyPlotStyle.m @@ -0,0 +1,158 @@ +function applyPlotStyle(target, varargin) +%APPLYPLOTSTYLE Apply nSTAT plot style to an axes/figure/graphics handle. +% +% Syntax: +% nstat.applyPlotStyle +% nstat.applyPlotStyle(gca) +% nstat.applyPlotStyle(gcf,'Style','legacy') +% +% Name-Value: +% 'Style' - 'legacy' or 'modern'. When omitted, uses nstat.getPlotStyle. + +if nargin < 1 || isempty(target) + if isempty(get(groot, 'CurrentFigure')) + return; + end + target = gca; +end + +parser = inputParser; +parser.FunctionName = 'nstat.applyPlotStyle'; +addParameter(parser, 'Style', '', @(x)ischar(x) || (isstring(x) && isscalar(x))); +parse(parser, varargin{:}); + +style = char(string(parser.Results.Style)); +if isempty(style) + style = nstat.getPlotStyle; +else + style = validateStyle(style); +end + +if strcmp(style, 'legacy') + return; +end + +[axList, figList] = resolveTargets(target); +if isempty(axList) && isempty(figList) + return; +end + +for iFig = 1:numel(figList) + try + set(figList(iFig), 'Color', 'w'); + catch + end +end + +for iAx = 1:numel(axList) + ax = axList(iAx); + if ~isgraphics(ax, 'axes') + continue; + end + + try + set(ax, ... + 'FontName', 'Helvetica', ... + 'FontSize', 10, ... + 'LineWidth', 1, ... + 'TickDir', 'out', ... + 'Layer', 'top'); + catch + end + + try + ln = findall(ax, 'Type', 'Line'); + for iLine = 1:numel(ln) + lw = get(ln(iLine), 'LineWidth'); + if isempty(lw) || ~isnumeric(lw) + continue; + end + if lw < 1.25 + set(ln(iLine), 'LineWidth', 1.25); + end + if strcmp(get(ln(iLine), 'Marker'), '.') + set(ln(iLine), 'MarkerSize', max(get(ln(iLine), 'MarkerSize'), 9)); + end + end + catch + end + + try + sc = findall(ax, 'Type', 'Scatter'); + for iSc = 1:numel(sc) + sz = get(sc(iSc), 'SizeData'); + if isempty(sz) + continue; + end + set(sc(iSc), 'SizeData', max(sz, 30)); + end + catch + end + +end + +if ~isempty(figList) + try + lgd = findall(figList, 'Type', 'Legend'); + for iL = 1:numel(lgd) + if isgraphics(lgd(iL), 'legend') + set(lgd(iL), 'FontSize', 10, 'Box', 'off'); + end + end + catch + end +end +end + +function [axList, figList] = resolveTargets(target) +axList = gobjects(0); +figList = gobjects(0); + +if isgraphics(target) + target = target(:); +else + return; +end + +for i = 1:numel(target) + t = target(i); + if isgraphics(t, 'figure') + figList(end+1,1) = t; %#ok + ax = findall(t, 'Type', 'axes'); + if ~isempty(ax) + axList = [axList; ax(:)]; %#ok + end + elseif isgraphics(t, 'axes') + axList(end+1,1) = t; %#ok + fig = ancestor(t, 'figure'); + if ~isempty(fig) + figList(end+1,1) = fig; %#ok + end + else + ax = ancestor(t, 'axes'); + if ~isempty(ax) + axList(end+1,1) = ax; %#ok + fig = ancestor(ax, 'figure'); + if ~isempty(fig) + figList(end+1,1) = fig; %#ok + end + end + end +end + +if ~isempty(axList) + axList = unique(axList(isgraphics(axList, 'axes'))); +end +if ~isempty(figList) + figList = unique(figList(isgraphics(figList, 'figure'))); +end +end + +function style = validateStyle(style) +style = lower(char(string(style))); +valid = {'legacy', 'modern'}; +if ~any(strcmp(style, valid)) + error('nstat:plot:InvalidStyle', ... + 'Invalid plot style "%s". Valid styles: legacy, modern.', style); +end +end diff --git a/tools/+nstat/getPlotStyle.m b/tools/+nstat/getPlotStyle.m new file mode 100644 index 0000000..76e5855 --- /dev/null +++ b/tools/+nstat/getPlotStyle.m @@ -0,0 +1,39 @@ +function style = getPlotStyle(varargin) +%GETPLOTSTYLE Get global nSTAT plotting style preference. +% +% Syntax: +% style = nstat.getPlotStyle +% style = nstat.getPlotStyle('Default','legacy') +% +% Name-Value: +% 'Default' - fallback style when no preference is set (default 'modern'). +% +% Output: +% style - 'legacy' or 'modern' + +parser = inputParser; +parser.FunctionName = 'nstat.getPlotStyle'; +addParameter(parser, 'Default', 'modern', @(x)ischar(x) || (isstring(x) && isscalar(x))); +parse(parser, varargin{:}); + +defaultStyle = validateStyle(parser.Results.Default); +style = defaultStyle; + +if ispref('nstat', 'PlotStyle') + try + style = validateStyle(getpref('nstat', 'PlotStyle')); + catch + style = defaultStyle; + end +end +end + +function style = validateStyle(style) +style = lower(char(string(style))); +valid = {'legacy', 'modern'}; +if ~any(strcmp(style, valid)) + error('nstat:plot:InvalidStyle', ... + 'Invalid plot style "%s". Valid styles: legacy, modern.', style); +end +end + diff --git a/tools/+nstat/setPlotStyle.m b/tools/+nstat/setPlotStyle.m new file mode 100644 index 0000000..425f3a1 --- /dev/null +++ b/tools/+nstat/setPlotStyle.m @@ -0,0 +1,31 @@ +function style = setPlotStyle(style) +%SETPLOTSTYLE Set global nSTAT plotting style preference. +% +% Syntax: +% nstat.setPlotStyle +% nstat.setPlotStyle('legacy') +% style = nstat.setPlotStyle('modern') +% +% Inputs: +% style - 'legacy' or 'modern'. Defaults to 'modern'. +% +% Output: +% style - normalized style value that was saved. + +if nargin < 1 || isempty(style) + style = 'modern'; +end + +style = validateStyle(style); +setpref('nstat', 'PlotStyle', style); +end + +function style = validateStyle(style) +style = lower(char(string(style))); +valid = {'legacy', 'modern'}; +if ~any(strcmp(style, valid)) + error('nstat:plot:InvalidStyle', ... + 'Invalid plot style "%s". Valid styles: legacy, modern.', style); +end +end +