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/.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 72b87de..7aa5ac0 100644 --- a/README.md +++ b/README.md @@ -2,22 +2,57 @@ 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. +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. +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 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 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 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. -# 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 +buildtool check +buildtool test +``` + +To install fonts with Matlab buildtool + +```matlab +buildtool install +``` + +## 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) +* 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: + +* 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) +* 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") @@ -61,6 +96,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/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 d6599b1..f68595b 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(); @@ -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 @@ -100,25 +100,26 @@ 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') - 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') - 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') - 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') + 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,:)) + case 'matlab.graphics.chart.primitive.Bar' + 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,:)) + 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') - set(h(kk),'Color','w'); + 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') end end @@ -136,28 +137,29 @@ function go_graph_data() index = rem(cc-1,size(colors,1)) + 1; if isa(h, 'double') % octave - if strcmpi(get(h(kk),'type'),'line') - set(h(kk),'Color',colors(index,:)); - elseif strcmpi(get(h(kk),'type'),'hggroup') + switch lower(get(h(kk),'type')) + case 'line' + 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 - 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') - set(h(kk),'Color','w'); + set(h(kk),'EdgeColor',[1 1 1] - celight) + set(h(kk),'FaceColor',[1 1 1] - cflight) + case 'text' + set(h(kk),'Color','w') end % hggroup end % double end % kk @@ -179,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 @@ -192,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 @@ -241,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 @@ -262,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 @@ -273,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 @@ -292,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 @@ -301,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 @@ -318,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 98b6a86..41224b5 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. @@ -42,22 +42,26 @@ function hand(colors_in) % >> hand; % - global COLORS + global COLORS %#ok<*GVMIS> global CANVAS_RGB global FONT_NAME 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,93 +74,40 @@ 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'; - isfont = ~system("fc-list -q 'xkcd Script'"); - if ~isfont - try - isfont = install_font(); - catch err - warning(err.identifier, 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(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(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 and Linux are currently supported.'); - font_name = 'Pacifico'; - end % ispc() - -end % function - -function ok = install_font() - % Linux only - if ~isunix() - ok = false; - return +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 - if ~exist('~/.local') - mkdir('~','.local') - end - if ~exist('~/.local/share') - mkdir('~/.local','share') - end - if ~exist('~/.local/share/fonts') - mkdir('~/.local/share','fonts') - end - if ~exist('~/.local/share/fonts/xkcd-script.ttf') - P = mfilename('fullpath'); - fp = fileparts([P '.m']); - ff = fullfile(fp,'xkcd-script.ttf'); - cmd1 = sprintf('cp %s ~/.local/share/fonts/',ff); - ok = ~system(cmd1); - if ok - system('fc-cache -f -y -v ~/.local/share/fonts/ 2> /dev/null'); - ok = ~system("fc-list -q 'xkcd Script'"); - end + + 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 + + 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() 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 @@ -181,19 +132,20 @@ 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') - 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') - 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') - set(h(kk),'Color',COLORS(index,:)); - set(h(kk),'MarkerFaceColor',COLORS(index,:)); - set(h(kk),'MarkerEdgeColor',COLORS(index,:)); + 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) + case 'matlab.graphics.chart.primitive.Bar' + 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,:)) end end @@ -211,20 +163,21 @@ function go_graph_data() index = rem(cc-1,size(COLORS,1)) + 1; if isa(h, 'double') % octave - if strcmpi(get(h(kk),'type'),'line') - set(h(kk),'Color',COLORS(index,:)); - set(h(kk),'LineWidth',3); - elseif strcmpi(get(h(kk),'type'),'hggroup') + switch lower(get(h(kk),'type')) + case 'line' + 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 @@ -252,13 +205,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 @@ -274,14 +227,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 @@ -331,9 +284,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 @@ -352,7 +305,7 @@ function ml_handle_colorbar() cb = get(gca,'Colorbar'); if ~isempty(cb) - set(cb,'Color','black'); + set(cb,'Color','black') end end % function @@ -363,8 +316,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 @@ -384,8 +337,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 @@ -394,13 +347,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 @@ -421,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 new file mode 100644 index 0000000..c5c2545 --- /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('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') + 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 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..9f99462 --- /dev/null +++ b/test/TestHand.m @@ -0,0 +1,212 @@ +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 +expectedFontName = 'xkcd Script' +fallbackFontName = {'xkcd Script', 'Segoe Print', 'Pacifico', 'Times'} +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) +import matlab.unittest.constraints.IsEqualTo +import matlab.unittest.constraints.IsSubsetOf +fig = figure(Visible=tc.interactive); +tc.addTeardown(@close, fig) + +plot(1:10); +xlabel('X') +ylabel('Y') +title('Font Check') +hand() + +ax = gca; +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)']) +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; + +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); +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 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();