From 01e57a9e81cb6d0a35da6923680fa906d14611bf Mon Sep 17 00:00:00 2001 From: scivision Date: Sun, 29 Mar 2026 10:51:05 -0400 Subject: [PATCH 1/8] fix syntax error in modern matlab, improve efficiency of dir check --- hand.m | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/hand.m b/hand.m index 98b6a86..c0dbe29 100644 --- a/hand.m +++ b/hand.m @@ -42,7 +42,7 @@ function hand(colors_in) % >> hand; % - global COLORS + global COLORS %#ok<*GVMIS> global CANVAS_RGB global FONT_NAME global FONT_SIZE @@ -83,7 +83,7 @@ function hand(colors_in) try isfont = install_font(); catch err - warning(err.identifier, err.message); + warning(err.identifier, '%s', err.message); isfont = false; end if isfont @@ -92,7 +92,7 @@ function hand(colors_in) estr = sprintf('%s %s\n', estr,repmat('*',1,56)); estr = sprintf('%s Restart MATLAB to refresh font list.\n', estr); estr = sprintf('%s %s\n\n', estr,repmat('*',1,56)); - warning(estr); + warning('%s', estr); else if strcmp(graphics_toolkit(),'gnuplot') estr = sprintf('\n\n'); @@ -103,7 +103,7 @@ function hand(colors_in) estr = sprintf('%s toolkit with graphics_toolkit(new_toolkit_name).\n', estr); estr = sprintf('%s See help graphics_toolkit for more information.\n', estr); estr = sprintf('%s %s\n\n', estr,repmat('*',1,56)); - warning(estr); + warning('%s', estr); end end else @@ -125,21 +125,20 @@ function hand(colors_in) ok = false; return end - if ~exist('~/.local') + if ~exist('~/.local', 'dir') mkdir('~','.local') end - if ~exist('~/.local/share') + if ~exist('~/.local/share', 'dir') mkdir('~/.local','share') end - if ~exist('~/.local/share/fonts') + if ~exist('~/.local/share/fonts', 'dir') mkdir('~/.local/share','fonts') end - if ~exist('~/.local/share/fonts/xkcd-script.ttf') + if ~exist('~/.local/share/fonts/xkcd-script.ttf', 'file') P = mfilename('fullpath'); fp = fileparts([P '.m']); ff = fullfile(fp,'xkcd-script.ttf'); - cmd1 = sprintf('cp %s ~/.local/share/fonts/',ff); - ok = ~system(cmd1); + ok = copyfile(ff, '~/.local/share/fonts/'); if ok system('fc-cache -f -y -v ~/.local/share/fonts/ 2> /dev/null'); ok = ~system("fc-list -q 'xkcd Script'"); From ac8303f69dab0809348683bb3fe6e440b12e1d0c Mon Sep 17 00:00:00 2001 From: scivision Date: Sun, 29 Mar 2026 11:00:41 -0400 Subject: [PATCH 2/8] use switch for clarity & to avoid duplicated function calls --- dark.m | 22 ++++++++++++---------- hand.m | 12 +++++++----- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/dark.m b/dark.m index d6599b1..7eeb1ff 100644 --- a/dark.m +++ b/dark.m @@ -50,7 +50,7 @@ function dark(colors_in) % >> dark('bp'); % - global colors + global colors %#ok<*GVMIS> if nargin == 0 colors = fetch_colortab(); @@ -100,24 +100,25 @@ function ml_graph_data() for kk = numel(h):-1:1 cc = cc + 1; index = rem(cc-1,size(colors,1)) + 1; - if isa(h(kk), 'matlab.graphics.chart.primitive.Line') + switch class(h(kk)) + case 'matlab.graphics.chart.primitive.Line' set(h(kk),'Color',colors(index,:)); set(h(kk),'MarkerFaceColor',colors(index,:)); set(h(kk),'MarkerEdgeColor',colors(index,:)); - elseif isa(h(kk), 'matlab.graphics.chart.primitive.Bar') + case 'matlab.graphics.chart.primitive.Bar' set(h(kk),'FaceColor',colors(index,:)); set(h(kk),'EdgeColor','white'); set(h(kk),'EdgeAlpha',0.5); - elseif isa(h(kk), 'matlab.graphics.chart.primitive.Stem') + case 'matlab.graphics.chart.primitive.Stem' set(h(kk),'Color',colors(index,:)); set(h(kk),'MarkerFaceColor',colors(index,:)); set(h(kk),'MarkerEdgeColor',colors(index,:)); - elseif isa(h(kk), 'matlab.graphics.primitive.Patch') + case 'matlab.graphics.primitive.Patch' celight = get(h(kk),'EdgeColor'); cflight = get(h(kk),'FaceColor'); set(h(kk),'EdgeColor',[1 1 1] - celight); set(h(kk),'FaceColor',[1 1 1] - cflight); - elseif isa(h(kk), 'matlab.graphics.primitive.Text') + case 'matlab.graphics.primitive.Text' set(h(kk),'Color','w'); end end @@ -136,9 +137,10 @@ function go_graph_data() index = rem(cc-1,size(colors,1)) + 1; if isa(h, 'double') % octave - if strcmpi(get(h(kk),'type'),'line') + switch lower(get(h(kk),'type')) + case 'line' set(h(kk),'Color',colors(index,:)); - elseif strcmpi(get(h(kk),'type'),'hggroup') + case 'hggroup' if isfield(get(h(kk)),'bargroup') % bar plot set(h(kk),'FaceColor',colors(index,:)); @@ -151,12 +153,12 @@ function go_graph_data() set(h(kk),'MarkerFaceColor',colors(index,:)); end end - elseif strcmpi(get(h(kk),'type'),'patch') + case 'patch' celight = get(h(kk),'EdgeColor'); cflight = get(h(kk),'FaceColor'); set(h(kk),'EdgeColor',[1 1 1] - celight); set(h(kk),'FaceColor',[1 1 1] - cflight); - elseif strcmpi(get(h(kk),'type'),'text') + case 'text' set(h(kk),'Color','w'); end % hggroup end % double diff --git a/hand.m b/hand.m index c0dbe29..593fb09 100644 --- a/hand.m +++ b/hand.m @@ -180,16 +180,17 @@ function ml_graph_data() for kk = numel(h):-1:1 cc = cc + 1; index = rem(cc-1,size(COLORS,1)) + 1; - if isa(h(kk), 'matlab.graphics.chart.primitive.Line') + switch class(h(kk)) + case 'matlab.graphics.chart.primitive.Line' set(h(kk),'Color',COLORS(index,:)); set(h(kk),'MarkerFaceColor',COLORS(index,:)); set(h(kk),'MarkerEdgeColor',COLORS(index,:)); set(h(kk),'LineWidth',3); - elseif isa(h(kk), 'matlab.graphics.chart.primitive.Bar') + case 'matlab.graphics.chart.primitive.Bar' set(h(kk),'FaceColor',COLORS(index,:)); set(h(kk),'EdgeColor','black'); set(h(kk),'EdgeAlpha',0.5); - elseif isa(h(kk), 'matlab.graphics.chart.primitive.Stem') + case 'matlab.graphics.chart.primitive.Stem' set(h(kk),'Color',COLORS(index,:)); set(h(kk),'MarkerFaceColor',COLORS(index,:)); set(h(kk),'MarkerEdgeColor',COLORS(index,:)); @@ -210,10 +211,11 @@ function go_graph_data() index = rem(cc-1,size(COLORS,1)) + 1; if isa(h, 'double') % octave - if strcmpi(get(h(kk),'type'),'line') + switch lower(get(h(kk),'type')) + case 'line' set(h(kk),'Color',COLORS(index,:)); set(h(kk),'LineWidth',3); - elseif strcmpi(get(h(kk),'type'),'hggroup') + case 'hggroup' if isfield(get(h(kk)),'bargroup') % bar plot set(h(kk),'FaceColor',COLORS(index,:)); From 9946cd2774e54a0cc473d2ff81f1642d8f356335 Mon Sep 17 00:00:00 2001 From: scivision Date: Sun, 29 Mar 2026 11:25:48 -0400 Subject: [PATCH 3/8] rationalize install_font logic --- hand.m | 50 ++++++++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/hand.m b/hand.m index 593fb09..a274c33 100644 --- a/hand.m +++ b/hand.m @@ -78,7 +78,11 @@ function hand(colors_in) font_name = 'Segoe Print'; elseif isunix() font_name = 'xkcd Script'; - isfont = ~system("fc-list -q 'xkcd Script'"); + if isoctave() + isfont = ~system("fc-list -q 'xkcd Script'"); + else + isfont = any(ismember(listfonts(), 'xkcd Script')); + end if ~isfont try isfont = install_font(); @@ -120,30 +124,32 @@ function hand(colors_in) end % function function ok = install_font() - % Linux only - if ~isunix() - ok = false; - return - end - if ~exist('~/.local', 'dir') - mkdir('~','.local') - end - if ~exist('~/.local/share', 'dir') - mkdir('~/.local','share') + ok = false; + if ~isunix() + return + end + + xkcd_ttf = fullfile(fileparts([mfilename('fullpath') '.m']), 'xkcd-script.ttf'); + + if ismac + disp('Install XKCD Script with Font Book, then restart Matlab') + ok = ~system(sprintf('open -a "Font Book" %s', xkcd_ttf)); + else + fdir = '~/.local/share/fonts/'; + if ~exist(fdir, 'dir') + mkdir(fdir); end - if ~exist('~/.local/share/fonts', 'dir') - mkdir('~/.local/share','fonts') + fname = [fdir, 'xkcd-script.ttf']; + + if ~exist(fname, 'file') + copyfile(xkcd_ttf, fname); end - if ~exist('~/.local/share/fonts/xkcd-script.ttf', 'file') - P = mfilename('fullpath'); - fp = fileparts([P '.m']); - ff = fullfile(fp,'xkcd-script.ttf'); - ok = copyfile(ff, '~/.local/share/fonts/'); - if ok - system('fc-cache -f -y -v ~/.local/share/fonts/ 2> /dev/null'); - ok = ~system("fc-list -q 'xkcd Script'"); - end + + if exist(fname, 'file') + system('fc-cache -f -y -v ~/.local/share/fonts/ 2> /dev/null'); + ok = ~system("fc-list -q 'xkcd Script'"); end + end end % function function draw_canvas() From cdd5ddac986a8cb11d9b979f8d62fd3cbb50d886 Mon Sep 17 00:00:00 2001 From: scivision Date: Sun, 29 Mar 2026 12:05:01 -0400 Subject: [PATCH 4/8] doc --- README.md | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 72b87de..1d45afd 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,23 @@ DARK converts a standard light-themed plot to a dark color scheme. DARK is easy to use, simply run the command "dark" on your current plot to render it in a dark theme. Once converted you may continue to manipulate the plot as desired including zooming, panning, and modifying properties. -DARK uses only native functions common to MATLAB and GNU Octave without any dependencies on toolboxes or packages. Because of this it will likely run on most any version. It's been tested with MATLAB versions R2019b, R2020b, R2022b, and R2023b as well as GNU Octave versions 3.8.2, 4.4.0, 5.2.0, 6.4.0, 8.3.0, and 9.1.0. DARK has been tested on Windows 10 and Linux distros running Centos7, RHEL 8, and Ubuntu 22.04. - -UNDARK restores a dark-themed plot back to its standard light theme. Running the command "undark" will restore the current plot to its standard light theme. In short UNDARK undoes DARK. - -HAND renders a plot in a theme that emulates a hand-drawn plot in an engineering notebook. It works the same way as DARK, simply run the command "hand" on your current plot to render it in a hand-drawn thme. Once converted you may continue to modify properties, zoom, and pan as desired. - -HAND has been tested with MATLAB R2023b and GNU Octave versions 5.2.0, 6.4.0, 8.3.0, and 9.1.0 running on Windows 10 as well as Linux RHEL 8 and Ubuntu 22.04. For running on Linux, HAND bundles the excellent "xkcd Script" font from [xkcd-font](https://github.com/ipython/xkcd-font). When running on Windows, HAND uses the "Segoe Print" font included with all modern Windows distributions. Similar to DARK, HAND is coded using funtions native to MATLAB and GNU Octave without any dependencies on toolboxes or packages, making it highly portable. +DARK uses only native functions common to MATLAB and GNU Octave without any dependencies on toolboxes or packages. +Because of this it will likely run on most any version. +It's been tested with MATLAB versions R2019b, R2020b, R2022b, R2023b, and R2026a +as well as GNU Octave versions 3.8.2, 4.4.0, 5.2.0, 6.4.0, 8.3.0, 9.1.0, and 11.1.0. +DARK has been tested on Windows 10 and Linux distros running Centos7, RHEL 8, and Ubuntu 22.04. + +UNDARK restores a dark-themed plot back to its standard light theme. +Running the command "undark" will restore the current plot to its standard light theme. +In short UNDARK undoes DARK. + +HAND renders a plot in a theme that emulates a hand-drawn plot in an engineering notebook. +It works the same way as DARK, simply run the command "hand" on your current plot to render it in a hand-drawn thme. +Once converted you may continue to modify properties, zoom, and pan as desired. + +HAND has been tested with MATLAB R2023b and GNU Octave versions 5.2.0, 6.4.0, 8.3.0, and 9.1.0 running on Windows 10 as well as Linux RHEL 8 and Ubuntu 22.04. +For running on Linux, HAND bundles the excellent "xkcd Script" font from [xkcd-font](https://github.com/ipython/xkcd-font). +When running on Windows, HAND uses the "Segoe Print" font included with all modern Windows distributions. Similar to DARK, HAND is coded using funtions native to MATLAB and GNU Octave without any dependencies on toolboxes or packages, making it highly portable. # Files * hand.m - NEW - Convert plot to a hand-drawn theme From 2943f0a0d50e1b4231f9d98039d28b9792c238f1 Mon Sep 17 00:00:00 2001 From: scivision Date: Sun, 29 Mar 2026 12:14:55 -0400 Subject: [PATCH 5/8] lint: unneeded ; --- dark.m | 88 ++++++++++++++++++++++++++-------------------------- hand.m | 98 +++++++++++++++++++++++++++++----------------------------- 2 files changed, 93 insertions(+), 93 deletions(-) diff --git a/dark.m b/dark.m index 7eeb1ff..f596d7b 100644 --- a/dark.m +++ b/dark.m @@ -70,12 +70,12 @@ function dark(colors_in) function draw_canvas() % draw background - set(gca,'Color','black'); - set(gcf,'Color','black'); - set(gca,'zcolor','white'); + set(gca,'Color','black') + set(gcf,'Color','black') + set(gca,'zcolor','white') if isoctave() - set(gca,'xcolor','white'); - set(gca,'ycolor','white'); + set(gca,'xcolor','white') + set(gca,'ycolor','white') end end % function @@ -102,24 +102,24 @@ function ml_graph_data() index = rem(cc-1,size(colors,1)) + 1; switch class(h(kk)) case 'matlab.graphics.chart.primitive.Line' - set(h(kk),'Color',colors(index,:)); - set(h(kk),'MarkerFaceColor',colors(index,:)); - set(h(kk),'MarkerEdgeColor',colors(index,:)); + set(h(kk),'Color',colors(index,:)) + set(h(kk),'MarkerFaceColor',colors(index,:)) + set(h(kk),'MarkerEdgeColor',colors(index,:)) case 'matlab.graphics.chart.primitive.Bar' - set(h(kk),'FaceColor',colors(index,:)); - set(h(kk),'EdgeColor','white'); - set(h(kk),'EdgeAlpha',0.5); + set(h(kk),'FaceColor',colors(index,:)) + set(h(kk),'EdgeColor','white') + set(h(kk),'EdgeAlpha',0.5) case 'matlab.graphics.chart.primitive.Stem' - set(h(kk),'Color',colors(index,:)); - set(h(kk),'MarkerFaceColor',colors(index,:)); - set(h(kk),'MarkerEdgeColor',colors(index,:)); + set(h(kk),'Color',colors(index,:)) + set(h(kk),'MarkerFaceColor',colors(index,:)) + set(h(kk),'MarkerEdgeColor',colors(index,:)) case 'matlab.graphics.primitive.Patch' celight = get(h(kk),'EdgeColor'); cflight = get(h(kk),'FaceColor'); - set(h(kk),'EdgeColor',[1 1 1] - celight); - set(h(kk),'FaceColor',[1 1 1] - cflight); + set(h(kk),'EdgeColor',[1 1 1] - celight) + set(h(kk),'FaceColor',[1 1 1] - cflight) case 'matlab.graphics.primitive.Text' - set(h(kk),'Color','w'); + set(h(kk),'Color','w') end end @@ -139,27 +139,27 @@ function go_graph_data() % octave switch lower(get(h(kk),'type')) case 'line' - set(h(kk),'Color',colors(index,:)); + set(h(kk),'Color',colors(index,:)) case 'hggroup' if isfield(get(h(kk)),'bargroup') % bar plot - set(h(kk),'FaceColor',colors(index,:)); - set(h(kk),'EdgeColor','white'); + set(h(kk),'FaceColor',colors(index,:)) + set(h(kk),'EdgeColor','white') else % stem plot - set(h(kk),'Color',colors(index,:)); - set(h(kk),'MarkerEdgeColor',colors(index,:)); + set(h(kk),'Color',colors(index,:)) + set(h(kk),'MarkerEdgeColor',colors(index,:)) if isnumeric(get(h(kk)).markerfacecolor) - set(h(kk),'MarkerFaceColor',colors(index,:)); + set(h(kk),'MarkerFaceColor',colors(index,:)) end end case 'patch' celight = get(h(kk),'EdgeColor'); cflight = get(h(kk),'FaceColor'); - set(h(kk),'EdgeColor',[1 1 1] - celight); - set(h(kk),'FaceColor',[1 1 1] - cflight); + set(h(kk),'EdgeColor',[1 1 1] - celight) + set(h(kk),'FaceColor',[1 1 1] - cflight) case 'text' - set(h(kk),'Color','w'); + set(h(kk),'Color','w') end % hggroup end % double end % kk @@ -181,9 +181,9 @@ function ml_label_axes() tt = get(gca,'title'); xx = get(gca,'xaxis'); yy = get(gca,'yaxis'); - set(tt,'Color','white'); - set(xx,'Color','white'); - set(yy,'Color','white'); + set(tt,'Color','white') + set(xx,'Color','white') + set(yy,'Color','white') end % function @@ -194,10 +194,10 @@ function go_label_axes() xx = get(gca,'xlabel'); yy = get(gca,'ylabel'); zz = get(gca,'zlabel'); - set(tt,'Color','white'); - set(xx,'Color','white'); - set(yy,'Color','white'); - set(zz,'Color','white'); + set(tt,'Color','white') + set(xx,'Color','white') + set(yy,'Color','white') + set(zz,'Color','white') end % function @@ -243,9 +243,9 @@ function go_handle_legend() props = get(h(end)); if isfield(props,'displayname') && ~isempty(props.displayname) lgd = legend; - set(lgd,'Color','black'); - set(lgd,'EdgeColor','white'); - set(lgd,'TextColor','white'); + set(lgd,'Color','black') + set(lgd,'EdgeColor','white') + set(lgd,'TextColor','white') end end % function @@ -264,7 +264,7 @@ function ml_handle_colorbar() cb = get(gca,'Colorbar'); if ~isempty(cb) - set(cb,'Color','white'); + set(cb,'Color','white') end end % function @@ -275,8 +275,8 @@ function go_handle_colorbar() if isfield(props,'__colorbar_handle__') cb = get(gca,'__colorbar_handle__'); if ~isempty(cb) - set(cb,'ycolor','white'); - set(cb,'fontsize',12); + set(cb,'ycolor','white') + set(cb,'fontsize',12) end end @@ -294,8 +294,8 @@ function finish_up() function ml_finish_up() - set(gca,'GridColor','white'); - set(gca,'GridAlpha',0.3); + set(gca,'GridColor','white') + set(gca,'GridAlpha',0.3) end % function @@ -303,13 +303,13 @@ function go_finish_up() props = get(gca); if isfield(props,'gridcolor') - set(gca,'GridColor','white'); + set(gca,'GridColor','white') end if isfield(props,'gridalpha') - set(gca,'GridAlpha',0.3); + set(gca,'GridAlpha',0.3) end if isfield(props,'fontsize') - set(gca,'FontSize',14); + set(gca,'FontSize',14) end end % function diff --git a/hand.m b/hand.m index a274c33..845b3f3 100644 --- a/hand.m +++ b/hand.m @@ -97,7 +97,7 @@ function hand(colors_in) estr = sprintf('%s Restart MATLAB to refresh font list.\n', estr); estr = sprintf('%s %s\n\n', estr,repmat('*',1,56)); warning('%s', estr); - else + else if strcmp(graphics_toolkit(),'gnuplot') estr = sprintf('\n\n'); estr = sprintf('%s %s\n', estr,repmat('*',1,56)); @@ -108,8 +108,8 @@ function hand(colors_in) estr = sprintf('%s See help graphics_toolkit for more information.\n', estr); estr = sprintf('%s %s\n\n', estr,repmat('*',1,56)); warning('%s', estr); - end - end + end + end else % font did not install, fallback to a standard font font_name = 'Times'; % @@ -117,7 +117,7 @@ function hand(colors_in) end % isfont end % ~isfont else - warning('Only Windows and Linux are currently supported.'); + warning('Only Windows, macOS, and Linux are currently supported.'); font_name = 'Pacifico'; end % ispc() @@ -128,7 +128,7 @@ function hand(colors_in) if ~isunix() return end - + xkcd_ttf = fullfile(fileparts([mfilename('fullpath') '.m']), 'xkcd-script.ttf'); if ismac @@ -156,12 +156,12 @@ function draw_canvas() global CANVAS_RGB % emulate engineering graph paper background - set(gca,'Color',CANVAS_RGB); - set(gcf,'Color',CANVAS_RGB); - set(gca,'zcolor','black'); + set(gca,'Color',CANVAS_RGB) + set(gcf,'Color',CANVAS_RGB) + set(gca,'zcolor','black') if isoctave() - set(gca,'xcolor','black'); - set(gca,'ycolor','black'); + set(gca,'xcolor','black') + set(gca,'ycolor','black') end end % function @@ -188,18 +188,18 @@ function ml_graph_data() index = rem(cc-1,size(COLORS,1)) + 1; switch class(h(kk)) case 'matlab.graphics.chart.primitive.Line' - set(h(kk),'Color',COLORS(index,:)); - set(h(kk),'MarkerFaceColor',COLORS(index,:)); - set(h(kk),'MarkerEdgeColor',COLORS(index,:)); - set(h(kk),'LineWidth',3); + set(h(kk),'Color',COLORS(index,:)) + set(h(kk),'MarkerFaceColor',COLORS(index,:)) + set(h(kk),'MarkerEdgeColor',COLORS(index,:)) + set(h(kk),'LineWidth',3) case 'matlab.graphics.chart.primitive.Bar' - set(h(kk),'FaceColor',COLORS(index,:)); - set(h(kk),'EdgeColor','black'); - set(h(kk),'EdgeAlpha',0.5); + set(h(kk),'FaceColor',COLORS(index,:)) + set(h(kk),'EdgeColor','black') + set(h(kk),'EdgeAlpha',0.5) case 'matlab.graphics.chart.primitive.Stem' - set(h(kk),'Color',COLORS(index,:)); - set(h(kk),'MarkerFaceColor',COLORS(index,:)); - set(h(kk),'MarkerEdgeColor',COLORS(index,:)); + set(h(kk),'Color',COLORS(index,:)) + set(h(kk),'MarkerFaceColor',COLORS(index,:)) + set(h(kk),'MarkerEdgeColor',COLORS(index,:)) end end @@ -219,19 +219,19 @@ function go_graph_data() % octave switch lower(get(h(kk),'type')) case 'line' - set(h(kk),'Color',COLORS(index,:)); - set(h(kk),'LineWidth',3); + set(h(kk),'Color',COLORS(index,:)) + set(h(kk),'LineWidth',3) case 'hggroup' if isfield(get(h(kk)),'bargroup') % bar plot - set(h(kk),'FaceColor',COLORS(index,:)); - set(h(kk),'EdgeColor','black'); + set(h(kk),'FaceColor',COLORS(index,:)) + set(h(kk),'EdgeColor','black') else % stem plot - set(h(kk),'Color',COLORS(index,:)); - set(h(kk),'MarkerEdgeColor',COLORS(index,:)); + set(h(kk),'Color',COLORS(index,:)) + set(h(kk),'MarkerEdgeColor',COLORS(index,:)) if isnumeric(get(h(kk)).markerfacecolor) - set(h(kk),'MarkerFaceColor',COLORS(index,:)); + set(h(kk),'MarkerFaceColor',COLORS(index,:)) end end end % hggroup @@ -259,13 +259,13 @@ function ml_label_axes() tt = get(gca,'title'); xx = get(gca,'xaxis'); yy = get(gca,'yaxis'); - set(tt,'Color','black'); - set(xx,'Color','black'); - set(yy,'Color','black'); + set(tt,'Color','black') + set(xx,'Color','black') + set(yy,'Color','black') if ~strcmpi(FONT_NAME,'default') - set(gca,'FontName',FONT_NAME,'FontSize',FONT_SIZE,'FontAngle',FONT_ANGLE); + set(gca,'FontName',FONT_NAME,'FontSize',FONT_SIZE,'FontAngle',FONT_ANGLE) else - set(gca,'FontAngle',FONT_ANGLE); + set(gca,'FontAngle',FONT_ANGLE) end end % function @@ -281,14 +281,14 @@ function go_label_axes() xx = get(gca,'xlabel'); yy = get(gca,'ylabel'); zz = get(gca,'zlabel'); - set(tt,'Color','black'); - set(xx,'Color','black'); - set(yy,'Color','black'); - set(zz,'Color','black'); + set(tt,'Color','black') + set(xx,'Color','black') + set(yy,'Color','black') + set(zz,'Color','black') if ~strcmpi(FONT_NAME,'default') - set(gca,'fontname',FONT_NAME,'fontSize',FONT_SIZE,'fontangle',FONT_ANGLE); + set(gca,'fontname',FONT_NAME,'fontSize',FONT_SIZE,'fontangle',FONT_ANGLE) else - set(gca,'fontangle',FONT_ANGLE); + set(gca,'fontangle',FONT_ANGLE) end @@ -338,9 +338,9 @@ function go_handle_legend() props = get(h(end)); if isfield(props,'displayname') && ~isempty(props.displayname) lgd = legend; - set(lgd,'Color',CANVAS_RGB); - set(lgd,'EdgeColor','black'); - set(lgd,'TextColor','black'); + set(lgd,'Color',CANVAS_RGB) + set(lgd,'EdgeColor','black') + set(lgd,'TextColor','black') end end % function @@ -359,7 +359,7 @@ function ml_handle_colorbar() cb = get(gca,'Colorbar'); if ~isempty(cb) - set(cb,'Color','black'); + set(cb,'Color','black') end end % function @@ -370,8 +370,8 @@ function go_handle_colorbar() if isfield(props,'__colorbar_handle__') cb = get(gca,'__colorbar_handle__'); if ~isempty(cb) - set(cb,'ycolor','black'); - set(cb,'fontsize',12); + set(cb,'ycolor','black') + set(cb,'fontsize',12) end end @@ -391,8 +391,8 @@ function finish_up() function ml_finish_up() cb = colortab_base(); - set(gca,'GridColor',cb.g); - set(gca,'GridAlpha',0.5); + set(gca,'GridColor',cb.g) + set(gca,'GridAlpha',0.5) end % function @@ -401,13 +401,13 @@ function go_finish_up() props = get(gca); if isfield(props,'gridcolor') cb = colortab_base(); - set(gca,'GridColor',cb.g); + set(gca,'GridColor',cb.g) end if isfield(props,'gridalpha') - set(gca,'GridAlpha',0.5); + set(gca,'GridAlpha',0.5) end if isfield(props,'fontsize') - set(gca,'FontSize',20); + set(gca,'FontSize',20) end end % function From 636802a1db8e4ccf34ff30693333967343b9da69 Mon Sep 17 00:00:00 2001 From: scivision Date: Sun, 29 Mar 2026 12:44:26 -0400 Subject: [PATCH 6/8] add Matlab unit test --- README.md | 55 +++++++----- test/TestDark.m | 164 +++++++++++++++++++++++++++++++++ test/TestHand.m | 225 ++++++++++++++++++++++++++++++++++++++++++++++ test/TestUndark.m | 132 +++++++++++++++++++++++++++ 4 files changed, 556 insertions(+), 20 deletions(-) create mode 100644 test/TestDark.m create mode 100644 test/TestHand.m create mode 100644 test/TestUndark.m diff --git a/README.md b/README.md index 1d45afd..d391c97 100644 --- a/README.md +++ b/README.md @@ -2,32 +2,47 @@ DARK converts a standard light-themed plot to a dark color scheme. DARK is easy to use, simply run the command "dark" on your current plot to render it in a dark theme. Once converted you may continue to manipulate the plot as desired including zooming, panning, and modifying properties. -DARK uses only native functions common to MATLAB and GNU Octave without any dependencies on toolboxes or packages. -Because of this it will likely run on most any version. -It's been tested with MATLAB versions R2019b, R2020b, R2022b, R2023b, and R2026a -as well as GNU Octave versions 3.8.2, 4.4.0, 5.2.0, 6.4.0, 8.3.0, 9.1.0, and 11.1.0. -DARK has been tested on Windows 10 and Linux distros running Centos7, RHEL 8, and Ubuntu 22.04. - -UNDARK restores a dark-themed plot back to its standard light theme. -Running the command "undark" will restore the current plot to its standard light theme. +DARK uses only native functions common to MATLAB and GNU Octave without any dependencies on toolboxes or packages. +Because of this it will likely run on most any version. +It's been tested with MATLAB versions R2019b, R2020b, R2022b, R2023b, and R2026a +as well as GNU Octave versions 3.8.2, 4.4.0, 5.2.0, 6.4.0, 8.3.0, 9.1.0, and 11.1.0. +DARK has been tested on Windows 10, macOS,and Linux distros running Centos7, RHEL 8, and Ubuntu 22.04. + +UNDARK restores a dark-themed plot back to its standard light theme. +Running the command "undark" will restore the current plot to its standard light theme. In short UNDARK undoes DARK. -HAND renders a plot in a theme that emulates a hand-drawn plot in an engineering notebook. -It works the same way as DARK, simply run the command "hand" on your current plot to render it in a hand-drawn thme. +HAND renders a plot in a theme that emulates a hand-drawn plot in an engineering notebook. +It works the same way as DARK, simply run the command "hand" on your current plot to render it in a hand-drawn thme. Once converted you may continue to modify properties, zoom, and pan as desired. -HAND has been tested with MATLAB R2023b and GNU Octave versions 5.2.0, 6.4.0, 8.3.0, and 9.1.0 running on Windows 10 as well as Linux RHEL 8 and Ubuntu 22.04. -For running on Linux, HAND bundles the excellent "xkcd Script" font from [xkcd-font](https://github.com/ipython/xkcd-font). -When running on Windows, HAND uses the "Segoe Print" font included with all modern Windows distributions. Similar to DARK, HAND is coded using funtions native to MATLAB and GNU Octave without any dependencies on toolboxes or packages, making it highly portable. +HAND has been tested with MATLAB R2023b and R2026a and +GNU Octave versions 5.2.0, 6.4.0, 8.3.0, and 9.1.0 running on Windows 10, macOS, Linux RHEL 8 and Ubuntu 22.04. +For running on Linux, HAND bundles the excellent "xkcd Script" font from [xkcd-font](https://github.com/ipython/xkcd-font). +When running on Windows, HAND uses the "Segoe Print" font included with all modern Windows distributions. +Similar to DARK, HAND is coded using funtions native to MATLAB and GNU Octave without any dependencies on toolboxes or packages, making it highly portable. -# Files -* hand.m - NEW - Convert plot to a hand-drawn theme -* test_hand.m - NEW - Test for hand.m +To run self-tests on Matlab: + +```matlab +runtests +``` + +## Files + +* hand.m - Convert plot to a hand-drawn theme * dark.m - Convert plot to a dark color theme * undark.m - Convert dark theme plot back to the standard light theme -* test_dark.m - Test dark.m with several plot types -* test_undark.m - Test undark.m with several plot types -* xkcd-script.ttf - NEW - xkcd Script font from [xkcd-font](https://github.com/ipython/xkcd-font) +* xkcd-script.ttf - xkcd Script font from [xkcd-font](https://github.com/ipython/xkcd-font) + +Test files: + +* test_hand.m - Script test for hand.m (Octave or Matlab) +* TestHand.m - Matlab unit tests for hand.m +* test_dark.m - Script test for dark.m with several plot types (Octave or Matlab) +* TestDark.m - Matlab unit tests for dark.m +* test_undark.m - Script test for undark.m with several plot types (Octave or Matlab) +* TestUndark.m - Matlab unit tests for undark.m ![Sample plot 1](./images/example1.PNG "Sample plot 1") @@ -71,6 +86,6 @@ hand ~~~~ # Citation -1. **[xkcd-font](https://github.com/ipython/xkcd-font)** +1. **[xkcd-font](https://github.com/ipython/xkcd-font)** > Written with [StackEdit](https://stackedit.io/). diff --git a/test/TestDark.m b/test/TestDark.m new file mode 100644 index 0000000..35e0e29 --- /dev/null +++ b/test/TestDark.m @@ -0,0 +1,164 @@ +classdef (SharedTestFixtures={ matlab.unittest.fixtures.PathFixture(fileparts(fileparts(mfilename('fullpath'))))}) ... + TestDark < matlab.unittest.TestCase + +properties (Constant) +Tol = 1e-12; +Black = [0 0 0]; +White = [1 1 1]; +Peach = [0.9290 0.6940 0.1250]; +Blue = [0 0 1]; +interactive = false +end + + +methods (Test) +function lineLegendAndTextStyling(tc) +fig = figure(Visible=tc.interactive); +tc.addTeardown(@close, fig) + + +plot([(1:10)' (10:-1:1)'],'o-'); +grid on +legend('this','that') +xlabel('blivit') +ylabel('barvid') +title('This and That') +th = text(4.5, 8, 'hello world!'); +dark() + +ax = gca; +lgd = legend; + +% Canvas and axes decoration colors should switch to dark mode. +tc.verifyEqual(ax.Color, tc.Black); +tc.verifyEqual(fig.Color, tc.Black); +tc.verifyEqual(ax.XColor, tc.White); +tc.verifyEqual(ax.YColor, tc.White); +tc.verifyEqual(ax.Title.Color, tc.White); +tc.verifyEqual(ax.XLabel.Color, tc.White); +tc.verifyEqual(ax.YLabel.Color, tc.White); + +% User annotations and legend should also use dark-mode text colors. +tc.verifyEqual(th.Color, tc.White); +tc.verifyEqual(lgd.Color, tc.Black); +tc.verifyEqual(lgd.TextColor, tc.White); +tc.verifyEqual(lgd.EdgeColor, tc.White); + +% Dark mode adjusts grid appearance as well. +tc.verifyEqual(ax.GridColor, tc.White); +tc.verifyEqual(ax.GridAlpha, 0.3, AbsTol=tc.Tol); +end + +function barhStyling(tc) +fig = figure(Visible=tc.interactive); +tc.addTeardown(@close, fig) + +barh([(1:10)' (10:-1:1)']); +grid on +legend('this','that') +dark() + +ax = gca; +ch = ax.Children; +isBar = arrayfun(@(h) isa(h, 'matlab.graphics.chart.primitive.Bar'), ch); +barHandles = ch(isBar); + +tc.verifyEqual(numel(barHandles), 2); +for k = 1:numel(barHandles) + tc.verifyEqual(barHandles(k).FaceColorMode, 'manual'); + tc.verifyEqual(barHandles(k).EdgeColor, tc.White); + tc.verifyEqual(barHandles(k).EdgeAlpha, 0.5, AbsTol=tc.Tol); +end +end + +function customBarColors(tc) +fig = figure(Visible=tc.interactive); +tc.addTeardown(@close, fig) + +bar([(1:10)' (10:-1:1)']); +dark('bp') + +ax = gca; +ch = ax.Children; +isBar = arrayfun(@(h) isa(h, 'matlab.graphics.chart.primitive.Bar'), ch); +barHandles = ch(isBar); + +tc.verifyEqual(numel(barHandles), 2); +barColors = zeros(numel(barHandles), 3); +for k = 1:numel(barHandles) + barColors(k, :) = barHandles(k).FaceColor; +end + +expected = [tc.Blue; tc.Peach]; +for i = 1:size(expected, 1) + deltas = max(abs(barColors - expected(i, :)), [], 2); + tc.verifyTrue(any(deltas <= tc.Tol), 'Expected dark("bp") bar color missing.'); +end +end + +function plot3Styling(tc) +fig = figure(Visible=tc.interactive); +tc.addTeardown(@close, fig) + +t = 0:pi/50:10*pi; +st = sin(t); +ct = cos(t); +plot3(st, ct, t) +grid on +xlabel('X') +ylabel('Y') +zlabel('Z') +title('3-d plot') +th = text(-0.75,-0.75,1,'hello world!'); +dark() + +ax = gca; +tc.verifyEqual(ax.Color, tc.Black); +tc.verifyEqual(ax.XColor, tc.White); +tc.verifyEqual(ax.YColor, tc.White); +tc.verifyEqual(ax.ZColor, tc.White); +tc.verifyEqual(ax.Title.Color, tc.White); +tc.verifyEqual(ax.XLabel.Color, tc.White); +tc.verifyEqual(ax.YLabel.Color, tc.White); +tc.verifyEqual(ax.ZLabel.Color, tc.White); +tc.verifyEqual(th.Color, tc.White); +end + +function surfOverImagescStyling(tc) +fig = figure(Visible=tc.interactive); +tc.addTeardown(@close, fig) + +Z = 10 + peaks; +surf(Z) +hold on +imagesc(Z) +grid off +hold off +xlabel('X') +ylabel('Y') +zlabel('Z') +title('3-d plot on top of imagesc') +text(10,20,2,'hello world!') +colorbar +dark() + +ax = gca; +tc.verifyEqual(ax.Color, tc.Black); +tc.verifyEqual(ax.XColor, tc.White); +tc.verifyEqual(ax.YColor, tc.White); +tc.verifyEqual(ax.ZColor, tc.White); +tc.verifyEqual(ax.Title.Color, tc.White); +tc.verifyEqual(ax.XLabel.Color, tc.White); +tc.verifyEqual(ax.YLabel.Color, tc.White); +tc.verifyEqual(ax.ZLabel.Color, tc.White); + +cb = ax.Colorbar; +tc.verifyNotEmpty(cb); +tc.verifyEqual(cb.Color, tc.White); + +ch = ax.Children; +tc.verifyTrue(any(arrayfun(@(h) isa(h, 'matlab.graphics.chart.primitive.Surface'), ch))); +tc.verifyTrue(any(arrayfun(@(h) isa(h, 'matlab.graphics.primitive.Image'), ch))); +end +end +end diff --git a/test/TestHand.m b/test/TestHand.m new file mode 100644 index 0000000..5db1b05 --- /dev/null +++ b/test/TestHand.m @@ -0,0 +1,225 @@ +classdef (SharedTestFixtures={ matlab.unittest.fixtures.PathFixture(fileparts(fileparts(mfilename('fullpath'))))}) ... + TestHand < matlab.unittest.TestCase + +properties (Constant) +Tol = 1e-12; +CanvasRGB = [220 243 182] / 256; +ExpectedRG = [ +0.6350 0.0780 0.1840; % r +0.3023 0.6203 0.1812 % g +]; +interactive = false +end + +properties +allowedFontName +end + +methods (TestClassSetup) +function setupAllowedFontNames(tc) +if ispc + tc.allowedFontName = 'Segoe Print'; +else + tc.allowedFontName = 'xkcd Script'; +end +end +end + + +methods (Test) +function axesAndLabelColors(tc) +fig = figure(Visible=tc.interactive); +tc.addTeardown(@close, fig) + +plot(1:10, 'LineWidth', 1.5); +grid on +xlabel('X') +ylabel('Y') +title('Y vs X') +hand() + +ax = gca; +xt = ax.XLabel; +yt = ax.YLabel; +tt = ax.Title; + +tc.verifyEqual(ax.Color, tc.CanvasRGB, AbsTol=tc.Tol); +tc.verifyEqual(ax.XColor, [0 0 0]); +tc.verifyEqual(ax.YColor, [0 0 0]); +tc.verifyEqual(tt.Color, [0 0 0]); +tc.verifyEqual(xt.Color, [0 0 0]); +tc.verifyEqual(yt.Color, [0 0 0]); +end + +function fontProperties(tc) +fig = figure(Visible=tc.interactive); +tc.addTeardown(@close, fig) + +plot(1:10); +xlabel('X') +ylabel('Y') +title('Font Check') +hand() + +ax = gca; +tc.verifyEqual(ax.FontName, tc.allowedFontName, ... + sprintf('Unexpected font name: %s', ax.FontName)); +tc.verifyEqual(ax.FontSize, 20); +tc.verifyTrue(any(strcmpi(ax.FontAngle, {'normal', 'italic'})), ... + sprintf('Unexpected font angle: %s', ax.FontAngle)); +end + +function lineColorsAndWidth(tc) +fig = figure(Visible=tc.interactive); +tc.addTeardown(@close, fig) + +plot([(1:10)' (10:-1:1)']); +hand('rg') + +ax = gca; +ch = ax.Children; +isLine = arrayfun(@(h) isa(h, 'matlab.graphics.chart.primitive.Line'), ch); +lineHandles = ch(isLine); + +tc.verifyEqual(numel(lineHandles), 2); +for k = 1:numel(lineHandles) + tc.verifyEqual(lineHandles(k).LineWidth, 3); +end + +lineColors = zeros(numel(lineHandles), 3); +for k = 1:numel(lineHandles) + lineColors(k, :) = lineHandles(k).Color; +end + +for i = 1:size(tc.ExpectedRG, 1) + deltas = max(abs(lineColors - tc.ExpectedRG(i, :)), [], 2); + tc.verifyTrue(any(deltas <= tc.Tol), ... + 'Expected line color missing after hand("rg").'); +end +end + +function legendStyling(tc) +fig = figure(Visible=tc.interactive); +tc.addTeardown(@close, fig) + +plot([(1:10)' (10:-1:1)']); +legend('this', 'that'); +hand() + +lgd = legend; +tc.verifyEqual(lgd.Color, tc.CanvasRGB, 'AbsTol', tc.Tol); +tc.verifyEqual(lgd.TextColor, [0 0 0]); +tc.verifyEqual(lgd.EdgeColor, [0 0 0]); +end + +function barhStyling(tc) +fig = figure(Visible=tc.interactive); +tc.addTeardown(@close, fig) + +barh([(1:10)' (10:-1:1)']); +grid on +legend('this', 'that') +xlabel('blivit') +ylabel('barvid') +title('This and That') +hand() + +ax = gca; +ch = ax.Children; +isBar = arrayfun(@(h) isa(h, 'matlab.graphics.chart.primitive.Bar'), ch); +barHandles = ch(isBar); + +tc.verifyEqual(numel(barHandles), 2); +for k = 1:numel(barHandles) + tc.verifyEqual(barHandles(k).FaceColorMode, 'manual'); + tc.verifyEqual(barHandles(k).EdgeColor, [0 0 0]); + tc.verifyEqual(barHandles(k).EdgeAlpha, 0.5); +end + +lgd = legend; +tc.verifyEqual(lgd.Color, tc.CanvasRGB, AbsTol=tc.Tol); +tc.verifyEqual(lgd.TextColor, [0 0 0]); +tc.verifyEqual(lgd.EdgeColor, [0 0 0]); +end + +function plot3Styling(tc) +fig = figure(Visible=tc.interactive); +tc.addTeardown(@close, fig) + +t = 0:pi/50:10*pi; +st = sin(t); +ct = cos(t); +plot3(st, ct, t) +grid on +xlabel('X') +ylabel('Y') +zlabel('Z') +title('3-d plot') +hand() + +ax = gca; +tt = ax.Title; +xt = ax.XLabel; +yt = ax.YLabel; +zt = ax.ZLabel; + +tc.verifyEqual(ax.Color, tc.CanvasRGB, AbsTol=tc.Tol); +tc.verifyEqual(ax.XColor, [0 0 0]); +tc.verifyEqual(ax.YColor, [0 0 0]); +tc.verifyEqual(ax.ZColor, [0 0 0]); +tc.verifyEqual(tt.Color, [0 0 0]); +tc.verifyEqual(xt.Color, [0 0 0]); +tc.verifyEqual(yt.Color, [0 0 0]); +tc.verifyEqual(zt.Color, [0 0 0]); + +ch = ax.Children; +isLine = arrayfun(@(h) isa(h, 'matlab.graphics.chart.primitive.Line'), ch); +lineHandles = ch(isLine); +tc.verifyGreaterThanOrEqual(numel(lineHandles), 1); +for k = 1:numel(lineHandles) + tc.verifyEqual(lineHandles(k).LineWidth, 3); +end +end + +function surfOverImagescStyling(tc) +fig = figure(Visible=tc.interactive); +tc.addTeardown(@close, fig) + +Z = 10 + peaks; +surf(Z) +hold on +imagesc(Z) +grid off +hold off +xlabel('X') +ylabel('Y') +zlabel('Z') +title('3-d plot on top of imagesc') +colorbar +hand() + +ax = gca; +tt = ax.Title; +xt = ax.XLabel; +yt = ax.YLabel; +zt = ax.ZLabel; + +tc.verifyEqual(ax.Color, tc.CanvasRGB, AbsTol=tc.Tol); +tc.verifyEqual(ax.XColor, [0 0 0]); +tc.verifyEqual(ax.YColor, [0 0 0]); +tc.verifyEqual(ax.ZColor, [0 0 0]); +tc.verifyEqual(tt.Color, [0 0 0]); +tc.verifyEqual(xt.Color, [0 0 0]); +tc.verifyEqual(yt.Color, [0 0 0]); +tc.verifyEqual(zt.Color, [0 0 0]); + +cb = ax.Colorbar; +tc.verifyNotEmpty(cb); +tc.verifyEqual(cb.Color, [0 0 0]); + +ch = ax.Children; +tc.verifyTrue(any(arrayfun(@(h) isa(h, 'matlab.graphics.chart.primitive.Surface'), ch))); +tc.verifyTrue(any(arrayfun(@(h) isa(h, 'matlab.graphics.primitive.Image'), ch))); +end +end +end diff --git a/test/TestUndark.m b/test/TestUndark.m new file mode 100644 index 0000000..b2fafbc --- /dev/null +++ b/test/TestUndark.m @@ -0,0 +1,132 @@ +classdef (SharedTestFixtures={ matlab.unittest.fixtures.PathFixture(fileparts(fileparts(mfilename('fullpath'))))}) ... + TestUndark < matlab.unittest.TestCase + +properties (Constant) +Tol = 1e-12; +Black = [0 0 0]; +White = [1 1 1]; +FigureLightGray = 0.94 * [1 1 1]; +GridGray = 0.15 * [1 1 1]; +interactive = false +end + +methods (Test) +function restoresLineLegendAndGrid(tc) +fig = figure(Visible=tc.interactive); +tc.addTeardown(@close, fig) + +plot([(1:10)' (10:-1:1)'],'o-'); +grid on +legend('this','that') +xlabel('blivit') +ylabel('barvid') +title('This and That') +text(4.5,8,'hello world!') +dark() +undark() + +ax = gca; +lgd = legend; + +tc.verifyEqual(ax.Color, tc.White); +tc.verifyEqual(fig.Color, tc.FigureLightGray, AbsTol=tc.Tol); +tc.verifyEqual(ax.XColor, tc.Black); +tc.verifyEqual(ax.YColor, tc.Black); +tc.verifyEqual(ax.Title.Color, tc.Black); +tc.verifyEqual(ax.XLabel.Color, tc.Black); +tc.verifyEqual(ax.YLabel.Color, tc.Black); + +tc.verifyEqual(lgd.Color, tc.White); +tc.verifyEqual(lgd.TextColor, tc.Black); +tc.verifyEqual(lgd.EdgeColor, tc.Black); + +tc.verifyEqual(ax.GridColor, tc.GridGray, AbsTol=tc.Tol); +tc.verifyEqual(ax.GridAlpha, 0.2, AbsTol=tc.Tol); +end + +function restoresBarhStyling(tc) +fig = figure(Visible=tc.interactive); +tc.addTeardown(@close, fig) + +barh([(1:10)' (10:-1:1)']); +grid on +legend('this','that') +dark() +undark() + +ax = gca; +ch = ax.Children; +isBar = arrayfun(@(h) isa(h, 'matlab.graphics.chart.primitive.Bar'), ch); +barHandles = ch(isBar); + +tc.verifyEqual(numel(barHandles), 2); +for k = 1:numel(barHandles) + tc.verifyEqual(barHandles(k).FaceColorMode, 'manual'); + tc.verifyEqual(barHandles(k).EdgeColor, tc.Black); + tc.verifyEqual(barHandles(k).EdgeAlpha, 1.0, AbsTol=tc.Tol); +end +end + +function restoresPlot3Styling(tc) +fig = figure(Visible=tc.interactive); +tc.addTeardown(@close, fig) + +t = 0:pi/50:10*pi; +st = sin(t); +ct = cos(t); +plot3(st, ct, t) +grid on +xlabel('X') +ylabel('Y') +zlabel('Z') +title('3-d plot') +text(-0.75,-0.75,1,'hello world!') +dark() +undark() + +ax = gca; +tc.verifyEqual(ax.Color, tc.White); +tc.verifyEqual(ax.XColor, tc.Black); +tc.verifyEqual(ax.YColor, tc.Black); +tc.verifyEqual(ax.ZColor, tc.Black); +tc.verifyEqual(ax.Title.Color, tc.Black); +tc.verifyEqual(ax.XLabel.Color, tc.Black); +tc.verifyEqual(ax.YLabel.Color, tc.Black); +tc.verifyEqual(ax.ZLabel.Color, tc.Black); +end + +function restoresSurfOverImagescAndColorbar(tc) +fig = figure(Visible=tc.interactive); +tc.addTeardown(@close, fig) + +Z = 10 + peaks; +surf(Z) +hold on +imagesc(Z) +grid off +hold off +xlabel('X') +ylabel('Y') +zlabel('Z') +title('3-d plot on top of imagesc') +text(10,20,2,'hello world!') +colorbar +dark() +undark() + +ax = gca; +tc.verifyEqual(ax.Color, tc.White); +tc.verifyEqual(ax.XColor, tc.Black); +tc.verifyEqual(ax.YColor, tc.Black); +tc.verifyEqual(ax.ZColor, tc.Black); + +cb = ax.Colorbar; +tc.verifyNotEmpty(cb); +tc.verifyEqual(cb.Color, tc.GridGray, AbsTol=tc.Tol); + +ch = ax.Children; +tc.verifyTrue(any(arrayfun(@(h) isa(h, 'matlab.graphics.chart.primitive.Surface'), ch))); +tc.verifyTrue(any(arrayfun(@(h) isa(h, 'matlab.graphics.primitive.Image'), ch))); +end +end +end From 490c55009c44ff97d726164fa51017cc9074c27b Mon Sep 17 00:00:00 2001 From: scivision Date: Sun, 29 Mar 2026 13:39:42 -0400 Subject: [PATCH 7/8] install_file own script for clarity --- .gitignore | 1 + README.md | 10 +++-- hand.m | 102 ++++++++++++------------------------------------- install_font.m | 67 ++++++++++++++++++++++++++++++++ 4 files changed, 98 insertions(+), 82 deletions(-) create mode 100644 install_font.m diff --git a/.gitignore b/.gitignore index 1377554..64b9fbf 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ *.swp +*.asv diff --git a/README.md b/README.md index d391c97..d932dd1 100644 --- a/README.md +++ b/README.md @@ -15,17 +15,18 @@ In short UNDARK undoes DARK. HAND renders a plot in a theme that emulates a hand-drawn plot in an engineering notebook. It works the same way as DARK, simply run the command "hand" on your current plot to render it in a hand-drawn thme. Once converted you may continue to modify properties, zoom, and pan as desired. +If the "xkcd Script" font is not yet installed, hand() will prompt to run `install_font()` to install the font. +This only needs to be done once per system. HAND has been tested with MATLAB R2023b and R2026a and -GNU Octave versions 5.2.0, 6.4.0, 8.3.0, and 9.1.0 running on Windows 10, macOS, Linux RHEL 8 and Ubuntu 22.04. -For running on Linux, HAND bundles the excellent "xkcd Script" font from [xkcd-font](https://github.com/ipython/xkcd-font). -When running on Windows, HAND uses the "Segoe Print" font included with all modern Windows distributions. +GNU Octave versions 7.1.0, 8.3.0, 9.1.0, and 11.1.0 running on Windows 10, macOS, Linux RHEL 8 and Ubuntu 22.04. +HAND bundles the excellent "xkcd Script" font from [xkcd-font](https://github.com/ipython/xkcd-font). Similar to DARK, HAND is coded using funtions native to MATLAB and GNU Octave without any dependencies on toolboxes or packages, making it highly portable. To run self-tests on Matlab: ```matlab -runtests +runtests test/ ``` ## Files @@ -33,6 +34,7 @@ runtests * hand.m - Convert plot to a hand-drawn theme * dark.m - Convert plot to a dark color theme * undark.m - Convert dark theme plot back to the standard light theme +* install_font.m - Install the "xkcd Script" font for use with hand.m. This only needs to be run once per system. * xkcd-script.ttf - xkcd Script font from [xkcd-font](https://github.com/ipython/xkcd-font) Test files: diff --git a/hand.m b/hand.m index 845b3f3..1553abe 100644 --- a/hand.m +++ b/hand.m @@ -1,4 +1,4 @@ -function hand(colors_in) +function hand(colors_in, fallback) % Usage: hand(colors_in) % % Hand-drawn-theme plots. @@ -48,16 +48,20 @@ function hand(colors_in) global FONT_SIZE global FONT_ANGLE - if nargin == 0 + if nargin < 1 COLORS = fetch_colortab(); else COLORS = fetch_colortab(lower(colors_in)); end + if nargin < 2 + fallback = true; + end + % Emulate engineering graph paper background CANVAS_RGB = [220 243 182]/256; - [font_name, font_size, font_angle] = setup_font(); + [font_name, font_size, font_angle] = check_font(fallback); FONT_NAME = font_name; FONT_SIZE = font_size; FONT_ANGLE = font_angle; @@ -70,86 +74,28 @@ function hand(colors_in) end % main function -function [font_name, font_size, font_angle] = setup_font() - font_angle = 'normal'; - font_size = 20; - if ispc() - %font_name = 'Comic Sans MS'; - font_name = 'Segoe Print'; - elseif isunix() - font_name = 'xkcd Script'; - if isoctave() - isfont = ~system("fc-list -q 'xkcd Script'"); - else - isfont = any(ismember(listfonts(), 'xkcd Script')); - end - if ~isfont - try - isfont = install_font(); - catch err - warning(err.identifier, '%s', err.message); - isfont = false; - end - if isfont - if ~isoctave() - estr = sprintf('\n\n'); - estr = sprintf('%s %s\n', estr,repmat('*',1,56)); - estr = sprintf('%s Restart MATLAB to refresh font list.\n', estr); - estr = sprintf('%s %s\n\n', estr,repmat('*',1,56)); - warning('%s', estr); - else - if strcmp(graphics_toolkit(),'gnuplot') - estr = sprintf('\n\n'); - estr = sprintf('%s %s\n', estr,repmat('*',1,56)); - estr = sprintf('%s GNU Octave is configured to use gnuplot graphics which\n', estr); - estr = sprintf('%s is not compatible with HAND. Use the GNU Octave command\n', estr); - estr = sprintf('%s available_graphics_toolkits() and change to a different\n', estr); - estr = sprintf('%s toolkit with graphics_toolkit(new_toolkit_name).\n', estr); - estr = sprintf('%s See help graphics_toolkit for more information.\n', estr); - estr = sprintf('%s %s\n\n', estr,repmat('*',1,56)); - warning('%s', estr); - end - end - else - % font did not install, fallback to a standard font - font_name = 'Times'; % - font_angle = 'italic'; - end % isfont - end % ~isfont - else - warning('Only Windows, macOS, and Linux are currently supported.'); - font_name = 'Pacifico'; - end % ispc() - -end % function - -function ok = install_font() - ok = false; - if ~isunix() - return - end - - xkcd_ttf = fullfile(fileparts([mfilename('fullpath') '.m']), 'xkcd-script.ttf'); - - if ismac - disp('Install XKCD Script with Font Book, then restart Matlab') - ok = ~system(sprintf('open -a "Font Book" %s', xkcd_ttf)); - else - fdir = '~/.local/share/fonts/'; - if ~exist(fdir, 'dir') - mkdir(fdir); +function [font_name, font_size, font_angle] = check_font(fallback) + font_angle = 'normal'; + font_size = 20; + font_name = 'xkcd Script'; + fonts_avail = listfonts(); + isfont = any(ismember(fonts_avail, 'xkcd Script')); + if ~isfont + if ~fallback + error(isfont, "run install_font() function to setup 'xkcd Script' font") end - fname = [fdir, 'xkcd-script.ttf']; - if ~exist(fname, 'file') - copyfile(xkcd_ttf, fname); + fonts = {'Segoe Print', 'Pacifico', 'Times', 'default'}; + for i = 1:length(fonts) + if any(ismember(fonts_avail, fonts{i})) + font_name = fonts{i}; + break + end end - if exist(fname, 'file') - system('fc-cache -f -y -v ~/.local/share/fonts/ 2> /dev/null'); - ok = ~system("fc-list -q 'xkcd Script'"); - end + warning('font "xkcd Script" not registered; trying font %s.\nUse function install_font() to install xkcd Script', font_name) end + end % function function draw_canvas() diff --git a/install_font.m b/install_font.m new file mode 100644 index 0000000..534ae23 --- /dev/null +++ b/install_font.m @@ -0,0 +1,67 @@ +function ok = install_font() + +ok = any(ismember(listfonts(), 'xkcd Script')); +if ok + disp('xkcd Script is already registered') + return +end + +xkcd_ttf = fullfile(fileparts([mfilename('fullpath') '.m']), 'xkcd-script.ttf'); + +if ispc() + cmd = sprintf('(New-Object -ComObject Shell.Application).Namespace(0x14).CopyHere(%s)', xkcd_ttf); + ok = ~system(cmd); +elseif ismac() + disp('Install XKCD Script with Font Book') + ok = ~system(sprintf('open -a "Font Book" %s', xkcd_ttf)); +else + fdir = '~/.local/share/fonts/'; + if ~exist(fdir, 'dir') + mkdir(fdir); + end + fname = [fdir, 'xkcd-script.ttf']; + + if ~exist(fname, 'file') + copyfile(xkcd_ttf, fname); + end + + if exist(fname, 'file') + system('fc-cache -f -y -v ~/.local/share/fonts/ 2> /dev/null'); + ok = ~system("fc-list -q 'xkcd Script'"); + end +end + +inform_restart() + +end % function + +function y = isoctave() +y = exist('OCTAVE_VERSION', 'builtin') ~= 0; +end % function + + +function inform_restart() + + estr = sprintf('\n\n'); + estr = sprintf('%s %s\n', estr,repmat('*',1,56)); + + if isoctave() + if strcmp(graphics_toolkit(),'gnuplot') + estr = sprintf('%s GNU Octave is configured to use gnuplot graphics which\n', estr); + estr = sprintf('%s is not compatible with HAND. Use the GNU Octave command\n', estr); + estr = sprintf('%s available_graphics_toolkits() and change to a different\n', estr); + estr = sprintf('%s toolkit with graphics_toolkit(new_toolkit_name).\n', estr); + estr = sprintf('%s See help graphics_toolkit for more information.\n', estr); + error('%s', estr) + else + estr = sprintf('%s Restart GNU OCtave to refresh font list.\n', estr); + end + else + estr = sprintf('%s Restart MATLAB to refresh font list.\n', estr); + end + + estr = sprintf('%s %s\n\n', estr,repmat('*',1,56)); + + fprintf(estr); + +end % function From b5a1f099eee2a6a6654640964c5574214a71c89b Mon Sep 17 00:00:00 2001 From: scivision Date: Sun, 29 Mar 2026 12:44:26 -0400 Subject: [PATCH 8/8] add Matlab unit test add continuous integration self-test --- .github/workflows/ci.yml | 56 ++++++++++++++++++++++ README.md | 10 +++- buildfile.m | 24 ++++++++++ dark.m | 2 +- hand.m | 2 +- install_font.m | 2 +- test/TestHand.m | 101 +++++++++++++++++---------------------- test_hand.m | 2 +- undark.m | 2 +- 9 files changed, 138 insertions(+), 63 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 buildfile.m diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d09c540 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,56 @@ +name: matlab + +on: + push: + paths: + - "**.m" + - ".github/workflows/ci.yml" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + + +jobs: + + tests: + strategy: + fail-fast: true + matrix: + os: [ubuntu-latest] + matlab: [R2023b, R2025b] + include: + - os: macos-latest + matlab: R2025b + - os: windows-latest + matlab: R2025b + + runs-on: ${{ matrix.os }} + + timeout-minutes: 15 + + steps: + + - name: Install MATLAB + timeout-minutes: 10 + uses: matlab-actions/setup-matlab@v2 + with: + release: ${{ matrix.matlab }} + cache: true + + - uses: actions/checkout@v6 + + - name: Check Matlab code + uses: matlab-actions/run-build@v2 + with: + tasks: check + + - name: Install font + uses: matlab-actions/run-build@v2 + with: + tasks: install + + - name: Run Matlab tests + uses: matlab-actions/run-build@v2 + with: + tasks: test diff --git a/README.md b/README.md index d932dd1..7aa5ac0 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,14 @@ Similar to DARK, HAND is coded using funtions native to MATLAB and GNU Octave wi To run self-tests on Matlab: ```matlab -runtests test/ +buildtool check +buildtool test +``` + +To install fonts with Matlab buildtool + +```matlab +buildtool install ``` ## Files @@ -39,6 +46,7 @@ runtests test/ Test files: +* buildfile.m - Matlab build system * test_hand.m - Script test for hand.m (Octave or Matlab) * TestHand.m - Matlab unit tests for hand.m * test_dark.m - Script test for dark.m with several plot types (Octave or Matlab) diff --git a/buildfile.m b/buildfile.m new file mode 100644 index 0000000..1933ac3 --- /dev/null +++ b/buildfile.m @@ -0,0 +1,24 @@ +function plan = buildfile + +plan = buildplan(localfunctions); + +plan("test") = matlab.buildtool.tasks.TestTask("test/"); +end + +function checkTask(context) +root = context.Plan.RootFolder; + +c = codeIssues(root, IncludeSubfolders=true); + +if isempty(c.Issues) + fprintf('%d files checked OK with %s under %s\n', numel(c.Files), c.Release, root) +else + disp(c.Issues) + error("Errors found in " + join(c.Issues.Location, newline)) +end + +end + +function installTask(~) +assert(install_font(), "install font failed") +end diff --git a/dark.m b/dark.m index f596d7b..f68595b 100644 --- a/dark.m +++ b/dark.m @@ -320,7 +320,7 @@ function go_finish_up() fc = fieldnames(colortab_base()); c = ''; for kk = 1:numel(fc) - c = [c fc{kk}]; + c = [c fc{kk}]; %#ok<*AGROW> end else c = colors_in; diff --git a/hand.m b/hand.m index 1553abe..41224b5 100644 --- a/hand.m +++ b/hand.m @@ -374,7 +374,7 @@ function go_finish_up() colorbase = colortab_base(); for kk = 1:numel(c) if isfield(colorbase,c(kk)) - colortab = [colortab; colorbase.(c(kk))]; + colortab = [colortab; colorbase.(c(kk))]; %#ok<*AGROW> elseif c(kk) == 'k' colortab = [colortab; [0 0 0]]; % reluctantly support black else diff --git a/install_font.m b/install_font.m index 534ae23..c5c2545 100644 --- a/install_font.m +++ b/install_font.m @@ -9,7 +9,7 @@ xkcd_ttf = fullfile(fileparts([mfilename('fullpath') '.m']), 'xkcd-script.ttf'); if ispc() - cmd = sprintf('(New-Object -ComObject Shell.Application).Namespace(0x14).CopyHere(%s)', xkcd_ttf); + cmd = sprintf('pwsh -c "(New-Object -ComObject Shell.Application).Namespace(0x14).CopyHere(''%s'')"', xkcd_ttf); ok = ~system(cmd); elseif ismac() disp('Install XKCD Script with Font Book') diff --git a/test/TestHand.m b/test/TestHand.m index 5db1b05..9f99462 100644 --- a/test/TestHand.m +++ b/test/TestHand.m @@ -9,20 +9,8 @@ 0.3023 0.6203 0.1812 % g ]; interactive = false -end - -properties -allowedFontName -end - -methods (TestClassSetup) -function setupAllowedFontNames(tc) -if ispc - tc.allowedFontName = 'Segoe Print'; -else - tc.allowedFontName = 'xkcd Script'; -end -end +expectedFontName = 'xkcd Script' +fallbackFontName = {'xkcd Script', 'Segoe Print', 'Pacifico', 'Times'} end @@ -31,8 +19,8 @@ function axesAndLabelColors(tc) fig = figure(Visible=tc.interactive); tc.addTeardown(@close, fig) -plot(1:10, 'LineWidth', 1.5); -grid on +plot(1:10, 'LineWidth', 1.5) +grid('on') xlabel('X') ylabel('Y') title('Y vs X') @@ -43,15 +31,17 @@ function axesAndLabelColors(tc) yt = ax.YLabel; tt = ax.Title; -tc.verifyEqual(ax.Color, tc.CanvasRGB, AbsTol=tc.Tol); -tc.verifyEqual(ax.XColor, [0 0 0]); -tc.verifyEqual(ax.YColor, [0 0 0]); -tc.verifyEqual(tt.Color, [0 0 0]); -tc.verifyEqual(xt.Color, [0 0 0]); -tc.verifyEqual(yt.Color, [0 0 0]); +tc.verifyEqual(ax.Color, tc.CanvasRGB, AbsTol=tc.Tol) +tc.verifyEqual(ax.XColor, [0 0 0]) +tc.verifyEqual(ax.YColor, [0 0 0]) +tc.verifyEqual(tt.Color, [0 0 0]) +tc.verifyEqual(xt.Color, [0 0 0]) +tc.verifyEqual(yt.Color, [0 0 0]) end function fontProperties(tc) +import matlab.unittest.constraints.IsEqualTo +import matlab.unittest.constraints.IsSubsetOf fig = figure(Visible=tc.interactive); tc.addTeardown(@close, fig) @@ -62,18 +52,20 @@ function fontProperties(tc) hand() ax = gca; -tc.verifyEqual(ax.FontName, tc.allowedFontName, ... - sprintf('Unexpected font name: %s', ax.FontName)); -tc.verifyEqual(ax.FontSize, 20); -tc.verifyTrue(any(strcmpi(ax.FontAngle, {'normal', 'italic'})), ... - sprintf('Unexpected font angle: %s', ax.FontAngle)); +if getenv('CI') == "true" + tc.verifyThat({ax.FontName}, IsSubsetOf(tc.fallbackFontName)) +else + tc.verifyThat(ax.FontName, IsEqualTo(tc.expectedFontName)) +end +tc.verifyEqual(ax.FontSize, 20) +tc.verifyThat(ax.FontAngle, IsEqualTo('normal') | IsEqualTo('italic')) end function lineColorsAndWidth(tc) fig = figure(Visible=tc.interactive); tc.addTeardown(@close, fig) -plot([(1:10)' (10:-1:1)']); +plot([(1:10)' (10:-1:1)']) hand('rg') ax = gca; @@ -81,9 +73,9 @@ function lineColorsAndWidth(tc) isLine = arrayfun(@(h) isa(h, 'matlab.graphics.chart.primitive.Line'), ch); lineHandles = ch(isLine); -tc.verifyEqual(numel(lineHandles), 2); +tc.verifyEqual(numel(lineHandles), 2) for k = 1:numel(lineHandles) - tc.verifyEqual(lineHandles(k).LineWidth, 3); + tc.verifyEqual(lineHandles(k).LineWidth, 3) end lineColors = zeros(numel(lineHandles), 3); @@ -93,8 +85,7 @@ function lineColorsAndWidth(tc) for i = 1:size(tc.ExpectedRG, 1) deltas = max(abs(lineColors - tc.ExpectedRG(i, :)), [], 2); - tc.verifyTrue(any(deltas <= tc.Tol), ... - 'Expected line color missing after hand("rg").'); + tc.verifyTrue(any(deltas <= tc.Tol), 'Expected line color missing after hand("rg").') end end @@ -102,22 +93,22 @@ function legendStyling(tc) fig = figure(Visible=tc.interactive); tc.addTeardown(@close, fig) -plot([(1:10)' (10:-1:1)']); -legend('this', 'that'); +plot([(1:10)' (10:-1:1)']) +legend('this', 'that') hand() lgd = legend; -tc.verifyEqual(lgd.Color, tc.CanvasRGB, 'AbsTol', tc.Tol); -tc.verifyEqual(lgd.TextColor, [0 0 0]); -tc.verifyEqual(lgd.EdgeColor, [0 0 0]); +tc.verifyEqual(lgd.Color, tc.CanvasRGB, 'AbsTol', tc.Tol) +tc.verifyEqual(lgd.TextColor, [0 0 0]) +tc.verifyEqual(lgd.EdgeColor, [0 0 0]) end function barhStyling(tc) fig = figure(Visible=tc.interactive); tc.addTeardown(@close, fig) -barh([(1:10)' (10:-1:1)']); -grid on +barh([(1:10)' (10:-1:1)']) +grid('on') legend('this', 'that') xlabel('blivit') ylabel('barvid') @@ -131,15 +122,15 @@ function barhStyling(tc) tc.verifyEqual(numel(barHandles), 2); for k = 1:numel(barHandles) - tc.verifyEqual(barHandles(k).FaceColorMode, 'manual'); - tc.verifyEqual(barHandles(k).EdgeColor, [0 0 0]); - tc.verifyEqual(barHandles(k).EdgeAlpha, 0.5); + tc.verifyEqual(barHandles(k).FaceColorMode, 'manual') + tc.verifyEqual(barHandles(k).EdgeColor, [0 0 0]) + tc.verifyEqual(barHandles(k).EdgeAlpha, 0.5) end lgd = legend; -tc.verifyEqual(lgd.Color, tc.CanvasRGB, AbsTol=tc.Tol); -tc.verifyEqual(lgd.TextColor, [0 0 0]); -tc.verifyEqual(lgd.EdgeColor, [0 0 0]); +tc.verifyEqual(lgd.Color, tc.CanvasRGB, AbsTol=tc.Tol) +tc.verifyEqual(lgd.TextColor, [0 0 0]) +tc.verifyEqual(lgd.EdgeColor, [0 0 0]) end function plot3Styling(tc) @@ -158,19 +149,15 @@ function plot3Styling(tc) hand() ax = gca; -tt = ax.Title; -xt = ax.XLabel; -yt = ax.YLabel; -zt = ax.ZLabel; -tc.verifyEqual(ax.Color, tc.CanvasRGB, AbsTol=tc.Tol); -tc.verifyEqual(ax.XColor, [0 0 0]); -tc.verifyEqual(ax.YColor, [0 0 0]); -tc.verifyEqual(ax.ZColor, [0 0 0]); -tc.verifyEqual(tt.Color, [0 0 0]); -tc.verifyEqual(xt.Color, [0 0 0]); -tc.verifyEqual(yt.Color, [0 0 0]); -tc.verifyEqual(zt.Color, [0 0 0]); +tc.verifyEqual(ax.Color, tc.CanvasRGB, AbsTol=tc.Tol) +tc.verifyEqual(ax.XColor, [0 0 0]) +tc.verifyEqual(ax.YColor, [0 0 0]) +tc.verifyEqual(ax.ZColor, [0 0 0]) +tc.verifyEqual(ax.Title.Color, [0 0 0]) +tc.verifyEqual(ax.XLabel.Color, [0 0 0]) +tc.verifyEqual(ax.YLabel.Color, [0 0 0]) +tc.verifyEqual(ax.ZLabel.Color, [0 0 0]) ch = ax.Children; isLine = arrayfun(@(h) isa(h, 'matlab.graphics.chart.primitive.Line'), ch); diff --git a/test_hand.m b/test_hand.m index fef573b..ae49228 100644 --- a/test_hand.m +++ b/test_hand.m @@ -15,7 +15,7 @@ pause(2) close all -plot([real(exp(i*2*pi/1024*(0:1024)));imag(exp(i*2*pi/1024*(0:1024)))].'); +plot([real(exp(1j*2*pi/1024*(0:1024)));imag(exp(1j*2*pi/1024*(0:1024)))].'); grid on xlabel('Time (s)') ylabel('Amplitude (V)') diff --git a/undark.m b/undark.m index db5da80..728a5ca 100644 --- a/undark.m +++ b/undark.m @@ -31,7 +31,7 @@ function undark() % >> undark('bp'); % - global colors + global colors %#ok<*GVMIS> colors = fetch_colortab();