File Exchange Pick of the Week

Our best user submissions

Spider Plot III – Custom Charts (Authoring)

Sean‘s pick this week is spider_plot by Moses.

Last week, we looked at the custom chart I created.  This week, we’ll look at authoring it.

Contents

Authoring the Custom Chart

Now let’s look at the steps to author the custom SpiderChart.

First we need a class that inherits from matlab.graphics.chartcontainer.ChartContainer.

classdef SpiderChart < matlab.graphics.chartcontainer.ChartContainer & ...
      matlab.graphics.chartcontainer.mixin.Legend

It needs a public property for everything we want our end users to be able to set or get. You’ll notice defaults and validation are done on the property level.

properties (SetObservable)
   P {mustBeNumeric}
   AxesInterval(1,1) double {mustBeInteger} = 3 % Number of axes grid lines
   AxesPrecision = 1 % Tick precision
   AxesLimits = [] % Axes limits
   FillOption matlab.lang.OnOffSwitchState = 'off' % Whether to shade data
   FillTransparency(1,1) double {mustBeGreaterThanOrEqual(FillTransparency,0),mustBeLessThanOrEqual(FillTransparency,1)} % Shading alpha
   Color(:,3) double {mustBeGreaterThanOrEqual(Color,0),mustBeLessThanOrEqual(Color,1)} = get(groot,'defaultAxesColorOrder') % Color order
   LineStyle {mustBeMember(LineStyle,{'-','--',':','-.','none'})} = '-' % Data line style
   LineWidth(1,1) double {mustBePositive} = 2 % Data line width
   Marker {mustBeMember(Marker,{'+','o','*','.','x','square','s','diamond','d','v','^','>','<','pentagram','p','hexagram','h','none'})} = 'o' % Data marker
   MarkerSize(1,1) double {mustBePositive} = 8 % Data marker size
   LabelFontSize(1,1) double {mustBePositive} = 10 % Label font size
   TickFontSize(1,1) double {mustBePositive} = 10 % Tick font size
   AxesLabels = "Label " + (1:100); % Axes labels
   DataLabels = "Data " + (1:100); % Data labels
end

We also need properties for the underlying graphics object that our chart will create, adjust, or destroy as necessary. These can’t be saved or replicated so they’ll be Transient and NonCopyable.

properties (Access = private, Transient, NonCopyable)
    ThetaAxesLines = gobjects(0)
    RhoAxesLines = gobjects(0)
    DataLines = gobjects(0)
    LabelObjects = gobjects(0)
    FillPatches = gobjects(0)
    AxesTextLabels = gobjects(0)
    AxesTickLabels = gobjects(0)
    DoWholeUpdate = true
    AxesValues
end

Then we need a constructor. I want my class to have both normal syntaxes for a chart, i.e.:

SpiderChart(data)
SpiderChart(data, 'Name', value, ...)
SpiderChart(parent, ____)

so I’ll handle this in the constructor.

% Constructor
function obj = SpiderChart(parentOrP, varargin)
    narginchk(1, inf);
    if isa(parentOrP, 'matlab.graphics.Graphics')
    % SpiderPlot(parent, P, 'name', value)
       in = [{parentOrP, 'P'}, varargin];
    else
       % SpiderPlot(P, 'name', value)
       in = [{'P', parentOrP} varargin];
    end
    % Construct
    obj@matlab.graphics.chartcontainer.ChartContainer(in{:});
end

With a custom chart, we need to have a setup and update method. The setup runs once when the chart is constructed and update any time a property is changed and a drawnow called.

There are two properties (P, AxesInterval) that when changed could require adjusting the total number of graphics objects needed (i.e. could require creating or deleting objects in an update). Because of this, the only thing I’ll do in setup is set the axes properties. The update will deal with graphics object creation.

function setup(obj)
% Configure axes
ax = getAxes(obj);
hold(ax, 'on')
axis(ax, 'square')
axis(ax, 'off')
end

The decision of whether to destroy and recreate objects is based on a DoWholeUpdate property that is adjusted in the setters for the aforementioned properties. It also defaults to true so on first run, everything is created.

%Update implementation
function update(obj)
    if obj.DoWholeUpdate
        % Only reset and reinitialize if P or AxesInterval changed
        resetStoredGraphicsObjects(obj);
        initializeEverything(obj)
        obj.DoWholeUpdate = false;
    end
    adjustAppearances(obj);
end

These are the setters that toggle updating everything.

function set.P(obj, val)
    obj.P = val;
    obj.DoWholeUpdate = true;
end
function set.AxesInterval(obj, val)
    obj.AxesInterval = val;
    obj.DoWholeUpdate = true;
end

From the update method, you can see there are three main algorithmic parts: resetStoredGraphicsObjects, initializeEverything, and adjustAppearances.

The reset step deletes old graphics objects and reinitializes the properties to empty graphics placeholders.

function resetStoredGraphicsObjects(obj)
% Delete old objects
delete(obj.ThetaAxesLines)
delete(obj.RhoAxesLines)
delete(obj.DataLines)
delete(obj.LabelObjects)
delete(obj.FillPatches)
delete(obj.AxesTextLabels)
delete(obj.AxesTickLabels)

% Preallocate new ones as empties
obj.ThetaAxesLines = gobjects(0);
obj.RhoAxesLines = gobjects(0);
obj.DataLines = gobjects(0);
obj.LabelObjects = gobjects(0);
obj.FillPatches = gobjects(0);
obj.AxesTextLabels = gobjects(0);
obj.AxesTickLabels = gobjects(0);
obj.AxesValues = [];
end

Next, we initialize new objects where the number of objects is based on the size of P and the AxesInterval. All of them are initialized invisible with no data associated to them. This is the biggest change I had to make from the original code – I.e. rather than creating objects and not retaining their handles, I need to create them without setting their customizable properties, store the handles, and then adjust those properties later.

function initializeEverything(obj)
% Initialize data children
ax = getAxes(obj);
for ii = obj.NumDataGroups:-1:1
    obj.FillPatches(ii) = patch(nan, nan, nan, 'EdgeColor', 'none', 'HandleVisibility', 'off', 'Parent', ax);
    obj.DataLines(ii) = line(nan, nan, 'Parent', ax);
end

% Plot colors
grey = [0.5, 0.5, 0.5];

% Polar increments
theta_increment = 2*pi/obj.NumDataPoints;
rho_increment = 1/(obj.AxesInterval+1);

%%% Scale Data %%%
% Pre-allocation
P_scaled = zeros(size(obj.P));
axes_range = zeros(3, obj.NumDataPoints);

% Iterate through number of data points
for ii = 1:obj.NumDataPoints
    % Group of points
    group_points = obj.P(:, ii);
    
    % Automatically the range of each group
    min_value = min(group_points);
    max_value = max(group_points);
    rangeii = max_value - min_value;
    
    % Check if axes_limits is empty
    if isempty(obj.AxesLimits)
        % Scale points to range from [rho_increment, 1]
        P_scaled(:, ii) = ((group_points - min_value) / rangeii) * (1 - rho_increment) + rho_increment;
    else
        % Manually set the range of each group
        min_value = obj.AxesLimits(1, ii);
        max_value = obj.AxesLimits(2, ii);
        rangeii = max_value - min_value;
        
        % Check if the axes limits are within range of points
        if min_value > min(group_points) || max_value < max(group_points)
            error('Error: Please ensure the manually specified axes limits are within range of the data points.');
        end
        
        % Scale points to range from [rho_increment, 1]
        P_scaled(:, ii) = ((group_points - min_value) / rangeii) * (1 - rho_increment) + rho_increment;
    end
    
    % Store to array
    axes_range(:, ii) = [min_value; max_value; rangeii];
end

%%% Polar Axes %%%
% Polar coordinates
rho = 0:rho_increment:1;
theta = 0:theta_increment:2*pi;

% Iterate through each theta
for ii = (length(theta)-1):-1:1
    % Convert polar to cartesian coordinates
    [x_axes, y_axes] = pol2cart(theta(ii), rho);
    
    % Plot
    obj.ThetaAxesLines(ii) = line(ax, x_axes, y_axes, 'LineWidth', 1.5, ...
        'Color', grey, 'HandleVisibility', 'off');
    
    min_value = axes_range(1, ii);
    rangeii = axes_range(3, ii);
    
    % Iterate through points on isocurve
    for jj = length(rho):-1:2
        % Axes increment value
        axes_value = min_value + (rangeii/obj.AxesInterval) * (jj-2);
        
        % Display axes text
        obj.AxesValues(ii, jj-1) = axes_value;
        obj.AxesTickLabels(ii, jj-1) = text(ax, x_axes(jj), y_axes(jj), '', ...
            'Units', 'Data', ...
            'Color', 'k', ...
            'HorizontalAlignment', 'center', ...
            'VerticalAlignment', 'middle', ...
            'Visible', 'off');
    end
end

% Iterate through each rho
for ii = length(rho):-1:2
    % Convert polar to cartesian coordinates
    [x_axes, y_axes] = pol2cart(theta, rho(ii));
    
    % Plot
    obj.RhoAxesLines(ii-1) = line(ax, x_axes, y_axes, 'Color', grey, 'HandleVisibility', 'off');
end

%%% Plot %%%
% Iterate through number of data groups
for ii = obj.NumDataGroups:-1:1
    % Convert polar to cartesian coordinates
    [x_points, y_points] = pol2cart(theta(1:end-1), P_scaled(ii, :));
    
    % Make points circular
    x_circular = [x_points, x_points(1)];
    y_circular = [y_points, y_points(1)];
    
    % Plot data points
    obj.DataLines(ii).XData = x_circular;
    obj.DataLines(ii).YData = y_circular;
    
    % Check if fill option is toggled on
    obj.FillPatches(ii).XData = x_circular;
    obj.FillPatches(ii).YData = y_circular;
    
end

%%% Labels %%%
% Iterate through number of data points
for ii = 1:obj.NumDataPoints
    % Angle of point in radians
    [horz_align, vert_align, x_pos, y_pos] = getQuadrantPosition(theta(ii));
    
    % Display text label
    obj.AxesTextLabels(ii) = text(ax, x_axes(ii)+x_pos, y_axes(ii)+y_pos, '', ...
        'Units', 'Data', ...
        'HorizontalAlignment', horz_align, ...
        'VerticalAlihggnment', vert_align, ...
        'EdgeColor', 'k', ...
        'BackgroundColor', 'w', ...
        'Visible', 'off');
end

end

And then finally, adjust the appearances based on the properties. This fires any time any of the properties are changed. It updates the properties of the underlying graphics objects to implement the change.

function adjustAppearances(obj)

% Repeat colors as necessary
repeat_colors = fix(obj.NumDataPoints/size(obj.Color, 1))+1;
colors = repmat(obj.Color, repeat_colors, 1);

% Patches
for ii = 1:numel(obj.FillPatches)
    if obj.FillOption
        obj.FillPatches(ii).FaceColor = colors(ii, :);
        obj.FillPatches(ii).FaceAlpha = obj.FillTransparency;
    else
        obj.FillPatches(ii).FaceColor = 'none';
    end
end

% Data appearances
for ii = 1:numel(obj.DataLines)
    obj.DataLines(ii).LineStyle = obj.LineStyle;
    obj.DataLines(ii).Marker =  obj.Marker;
    obj.DataLines(ii).Color = colors(ii, :);
    obj.DataLines(ii).LineWidth = obj.LineWidth;
    obj.DataLines(ii).MarkerSize = obj.MarkerSize;
    obj.DataLines(ii).MarkerFaceColor = colors(ii, :);
    obj.DataLines(ii).DisplayName = obj.DataLabels(ii);
end

if isequal(obj.AxesLabels, 'none')
    set(obj.AxesTextLabels, 'Visible', 'off')
else
    set(obj.AxesTextLabels, 'Visible', 'on')
    % Iterate through number of data points
    for ii = 1:obj.NumDataPoints
        % Display text label
        obj.AxesTextLabels(ii).String = obj.AxesLabels{ii};
        obj.AxesTextLabels(ii).FontSize = obj.LabelFontSize;
    end
end

if isequal(obj.AxesPrecision, 'none')
    set(obj.AxesTickLabels, 'Visible', 'off')
else
    set(obj.AxesTickLabels, 'Visible', 'on')
    % Traverse to update precision
    for ii = 1:numel(obj.AxesValues)
        text_str = sprintf(sprintf('%%.%if', obj.AxesPrecision), obj.AxesValues(ii));
        obj.AxesTickLabels(ii).String = text_str;
        obj.AxesTickLabels(ii).FontSize = obj.TickFontSize;
    end
end

end

Full Custom Chart

Obviously, it took some effort, design, and a lot of refactoring to turn this into a custom chart and there are still further improvements that could be made (e.g. not updating everything, but one property at a time). If you want to play with it yourself, here is the full SpiderChart.m class definition file. I took the liberty to make a couple enhancements to Moses’ original, e.g. separating label fontsize and tick fontsize and making things work with string arrays.

classdef SpiderChart < matlab.graphics.chartcontainer.ChartContainer & ...
        matlab.graphics.chartcontainer.mixin.Legend
%spider_plot Create a spider or radar plot with individual axes.
%
% Syntax:
%   SpiderChart(PData)
%   SpiderChart(PData, Name, Value, ...)
%   SpiderChart(parent, ___)
%
% Input Arguments:
%   (Required)
%   PData            - The data points used to plot the spider chart. The
%                      rows are the groups of data and the columns are the
%                      data points. The axes labels and axes limits are
%                      automatically generated if not specified.
%                      [vector | matrix]
%
% Name-Value Pair Arguments:
%   (Optional)
%   AxesLabels       - Used to specify the label each of the axes.
%                      [auto-generated (default) | cell of strings | 'none']
%
%   AxesInterval     - Used to change the number of intervals displayed
%                      between the webs.
%                      [3 (default) | integer]
%
%   AxesPrecision    - Used to change the precision level on the value
%                      displayed on the axes. Enter in 'none' to remove
%                      axes text.
%                      [1 (default) | integer | 'none']
%
%   AxesLimits       - Used to manually set the axes limits. A matrix of
%                      2 x size(P, 2). The top row is the minimum axes
%                      limits and the bottow row is the maximum axes limits.
%                      [auto-scaled (default) | matrix]
%
%   DataLabels       - Labels for data to be used in legend.  String vector
%                      with number of elements size(PData, 1).
%
%   FillOption       - Used to toggle color fill option.
%                      ['off' (default) | 'on']
%
%   FillTransparency - Used to set color fill transparency.
%                      [0.1 (default) | scalar in range (0, 1)]
%
%   Color            - Used to specify the line color, specified as an RGB
%                      triplet. The intensities must be in the range (0, 1).
%                      [MATLAB colors (default) | RGB triplet]
%
%   LineStyle        - Used to change the line style of the plots.
%                      ['-' (default) | '--' | ':' | '-.' | 'none']
%
%   LineWidth        - Used to change the line width, where 1 point is 
%                      1/72 of an inch.
%                      [0.5 (default) | positive value]
%
%   Marker           - Used to change the marker symbol of the plots.
%                      ['o' (default) | 'none' | '*' | 's' | 'd' | ...]
%
%   MarkerSize       - Used to change the marker size, where 1 point is
%                      1/72 of an inch.
%                      [8 (default) | positive value]
%
%   LabelFontSize    - Used to change the font size of the labels and
%                      values displayed on the axes.
%                      [10 (default) | scalar value greater than zero]
%
%   TickFontSize     - Used to change the font size of the values displayed 
%                      on the axes. [10 (default) | scalar value greater than zero]
%
%% Examples:
%%   
% Example 1: Minimal number of arguments. All non-specified, optional
% arguments are set to their default values. Axes labels and limits are
% automatically generated and set.
% 
% D1 = [5 3 9 1 2];   % Initialize data points
% D2 = [5 8 7 2 9];
% D3 = [8 2 1 4 6];
% P = [D1; D2; D3];
% SpiderChart(P, 'DataLabels', "D" + (1:3));
% legend show
%%
% Example 2: Manually setting the axes limits. All non-specified, optional
% arguments are set to their default values.
% 
% axes_limits = [1, 2, 1, 1, 1; 10, 8, 9, 5, 10]; % Axes limits [min axes limits; max axes limits]
% SpiderChart(P, 'AxesLimits', axes_limits);
% 
%%  
% Example 3: Set fill option on. The fill transparency can be adjusted.
% 
% axes_labels = {'S1', 'S2', 'S3', 'S4', 'S5'}; % Axes properties
% axes_interval = 2;
% fill_option = 'on';
% fill_transparency = 0.1;
% SpiderChart(P,...
%     'AxesLabels', axes_labels,...
%     'AxesInterval', axes_interval,...
%     'FillOption', fill_option,...
%     'FillTransparency', fill_transparency);
% 
%%
% Example 4: Maximum number of arguments.
% 
% axes_labels = {'S1', 'S2', 'S3', 'S4', 'S5'}; % Axes properties
% axes_interval = 4;
% axes_precision = 'none';
% axes_limits = [1, 2, 1, 1, 1; 10, 8, 9, 5, 10];
% fill_option = 'on';
% fill_transparency = 0.2;
% colors = [1, 0, 0; 0, 1, 0; 0, 0, 1];
% line_style = '--';
% line_width = 3;
% marker_type = 'd';
% marker_size = 10;
% label_font_size = 12;
% tick_font_size = 8;
% data_labels = ["Hello" "World" "Happy 2020"];
% SpiderChart(P,...
%     'AxesLabels', axes_labels,...
%     'AxesInterval', axes_interval,...
%     'AxesPrecision', axes_precision,...
%     'AxesLimits', axes_limits,...
%     'FillOption', fill_option,...
%     'FillTransparency', fill_transparency,...
%     'Color', colors,...
%     'LineStyle', line_style,...
%     'LineWidth', line_width,...
%     'Marker', marker_type,...
%     'MarkerSize', marker_size,...
%     'LabelFontSize', label_font_size, ...
%     'TickFontSize', tick_font_size, ...
%     'DataLabels', data_labels)
% legend show

    properties (SetObservable)
        P {mustBeNumeric}
        AxesInterval(1,1) double {mustBeInteger} = 3 % Number of axes grid lines
        AxesPrecision = 1 % Tick precision
        AxesLimits = [] % Axes limits
        FillOption matlab.lang.OnOffSwitchState = 'off' % Whether to shade data
        FillTransparency(1,1) double {mustBeGreaterThanOrEqual(FillTransparency,0),mustBeLessThanOrEqual(FillTransparency,1)} % Shading alpha
        Color(:,3) double {mustBeGreaterThanOrEqual(Color,0),mustBeLessThanOrEqual(Color,1)} = get(groot,'defaultAxesColorOrder') % Color order
        LineStyle {mustBeMember(LineStyle,{'-','--',':','-.','none'})} = '-' % Data line style
        LineWidth(1,1) double {mustBePositive} = 2 % Data line width
        Marker {mustBeMember(Marker,{'+','o','*','.','x','square','s','diamond','d','v','^','>','<','pentagram','p','hexagram','h','none'})} = 'o' % Data marker
        MarkerSize(1,1) double {mustBePositive} = 8 % Data marker size
        LabelFontSize(1,1) double {mustBePositive} = 10 % Label font size
        TickFontSize(1,1) double {mustBePositive} = 10 % Tick font size
        AxesLabels = "Label " + (1:100); % Axes labels
        DataLabels = "Data " + (1:100); % Data labels
    end
    
    properties (Access = private, Transient, NonCopyable)
        ThetaAxesLines = gobjects(0)
        RhoAxesLines = gobjects(0)
        DataLines = gobjects(0)
        LabelObjects = gobjects(0)
        FillPatches = gobjects(0)
        AxesTextLabels = gobjects(0)
        AxesTickLabels = gobjects(0)
        DoWholeUpdate = true
        AxesValues
    end
    
    properties (Dependent, Access = protected, Hidden)
        NumDataGroups
        NumDataPoints
    end    
    
    methods
        % Constructor
        function obj = SpiderChart(parentOrP, varargin)
            narginchk(1, inf);
            if isa(parentOrP, 'matlab.graphics.Graphics')
                % SpiderPlot(parent, P, 'name', value)
                in = [{parentOrP, 'P'}, varargin];
            else
                % SpiderPlot(P, 'name', value)                
                in = [{'P', parentOrP} varargin];
            end
            
            % Construct
            obj@matlab.graphics.chartcontainer.ChartContainer(in{:});

        end
        
        %% Getters
        function numpts = get.NumDataPoints(obj)
            numpts = size(obj.P, 2);
            
        end
        
        function numpts = get.NumDataGroups(obj)
            numpts = size(obj.P, 1);
            
        end
        
        %% Setters
        % Okay to set DoWholeUpdate property in setters.
        %#ok<*MCSUP>
        
        function set.P(obj, val)
            obj.P = val;
            obj.DoWholeUpdate = true;
            
        end
        
        function set.AxesInterval(obj, val)
            obj.AxesInterval = val;
            obj.DoWholeUpdate = true;
            
        end
        
    end
    
    methods (Access = protected)
        % Setup implementation
        function setup(obj)
            % Configure axes
            ax = getAxes(obj);
            hold(ax, 'on')
            axis(ax, 'square')
            axis(ax, 'off')
            
        end
        
        % Update implementation
        function update(obj)            
            if obj.DoWholeUpdate                
                % Only reset and reinitialize if P or AxesInterval changed                
                resetStoredGraphicsObjects(obj);                                           
                initializeEverything(obj)            
                obj.DoWholeUpdate = false;
            end
            adjustAppearances(obj);      
            
        end
        
        % Adjust existing graphics objects
        function adjustAppearances(obj)
            
            % Repeat colors as necessary
            repeat_colors = fix(obj.NumDataPoints/size(obj.Color, 1))+1;
            colors = repmat(obj.Color, repeat_colors, 1);
            
            % Patches
            for ii = 1:numel(obj.FillPatches)
                if obj.FillOption
                    obj.FillPatches(ii).FaceColor = colors(ii, :);
                    obj.FillPatches(ii).FaceAlpha = obj.FillTransparency;
                else 
                    obj.FillPatches(ii).FaceColor = 'none';
                end
            end
            
            % Data appearances
            for ii = 1:numel(obj.DataLines)
                obj.DataLines(ii).LineStyle = obj.LineStyle;
                obj.DataLines(ii).Marker =  obj.Marker;
                obj.DataLines(ii).Color = colors(ii, :);
                obj.DataLines(ii).LineWidth = obj.LineWidth;
                obj.DataLines(ii).MarkerSize = obj.MarkerSize;
                obj.DataLines(ii).MarkerFaceColor = colors(ii, :);
                obj.DataLines(ii).DisplayName = obj.DataLabels(ii);
            end
            
            if isequal(obj.AxesLabels, 'none')
                set(obj.AxesTextLabels, 'Visible', 'off')
            else
                set(obj.AxesTextLabels, 'Visible', 'on')
                % Iterate through number of data points
                for ii = 1:obj.NumDataPoints
                    % Display text label
                    obj.AxesTextLabels(ii).String = obj.AxesLabels{ii};
                    obj.AxesTextLabels(ii).FontSize = obj.LabelFontSize;
                end                
            end
            
            if isequal(obj.AxesPrecision, 'none')
                set(obj.AxesTickLabels, 'Visible', 'off')
            else
                set(obj.AxesTickLabels, 'Visible', 'on')                
                % Traverse to update precision
                for ii = 1:numel(obj.AxesValues)
                    text_str = sprintf(sprintf('%%.%if', obj.AxesPrecision), obj.AxesValues(ii));
                    obj.AxesTickLabels(ii).String = text_str;
                    obj.AxesTickLabels(ii).FontSize = obj.TickFontSize;
                end
            end            
            
        end
        
        % Initialize and preconfigure graphics objects
        function initializeEverything(obj)
            % Initialize data children
            ax = getAxes(obj);
            for ii = obj.NumDataGroups:-1:1
                obj.FillPatches(ii) = patch(nan, nan, nan, 'EdgeColor', 'none', 'HandleVisibility', 'off', 'Parent', ax);
                obj.DataLines(ii) = line(nan, nan, 'Parent', ax);
            end
            
            % Plot colors
            grey = [0.5, 0.5, 0.5];
            
            % Polar increments
            theta_increment = 2*pi/obj.NumDataPoints;
            rho_increment = 1/(obj.AxesInterval+1);
            
            %%% Scale Data %%%
            % Pre-allocation
            P_scaled = zeros(size(obj.P));
            axes_range = zeros(3, obj.NumDataPoints);
            
            % Iterate through number of data points
            for ii = 1:obj.NumDataPoints
                % Group of points
                group_points = obj.P(:, ii);
                
                % Automatically the range of each group
                min_value = min(group_points);
                max_value = max(group_points);
                rangeii = max_value - min_value;
                
                % Check if axes_limits is empty
                if isempty(obj.AxesLimits)
                    % Scale points to range from [rho_increment, 1]
                    P_scaled(:, ii) = ((group_points - min_value) / rangeii) * (1 - rho_increment) + rho_increment;
                else
                    % Manually set the range of each group
                    min_value = obj.AxesLimits(1, ii);
                    max_value = obj.AxesLimits(2, ii);
                    rangeii = max_value - min_value;
                    
                    % Check if the axes limits are within range of points
                    if min_value > min(group_points) || max_value < max(group_points)
                        error('Error: Please ensure the manually specified axes limits are within range of the data points.');
                    end
                    
                    % Scale points to range from [rho_increment, 1]
                    P_scaled(:, ii) = ((group_points - min_value) / rangeii) * (1 - rho_increment) + rho_increment;
                end
                
                % Store to array
                axes_range(:, ii) = [min_value; max_value; rangeii];
            end
            
            %%% Polar Axes %%%
            % Polar coordinates
            rho = 0:rho_increment:1;
            theta = 0:theta_increment:2*pi;
            
            % Iterate through each theta
            for ii = (length(theta)-1):-1:1
                % Convert polar to cartesian coordinates
                [x_axes, y_axes] = pol2cart(theta(ii), rho);
                
                % Plot
                obj.ThetaAxesLines(ii) = line(ax, x_axes, y_axes, 'LineWidth', 1.5, ...
                    'Color', grey, 'HandleVisibility', 'off');
                
                min_value = axes_range(1, ii);
                rangeii = axes_range(3, ii);
                
                % Iterate through points on isocurve
                for jj = length(rho):-1:2
                    % Axes increment value                    
                    axes_value = min_value + (rangeii/obj.AxesInterval) * (jj-2);
                    
                    % Display axes text
                    obj.AxesValues(ii, jj-1) = axes_value;
                    obj.AxesTickLabels(ii, jj-1) = text(ax, x_axes(jj), y_axes(jj), '', ...
                        'Units', 'Data', ...
                        'Color', 'k', ...
                        'HorizontalAlignment', 'center', ...
                        'VerticalAlignment', 'middle', ...
                        'Visible', 'off');
                end
            end
            
            % Iterate through each rho
            for ii = length(rho):-1:2
                % Convert polar to cartesian coordinates
                [x_axes, y_axes] = pol2cart(theta, rho(ii));
                
                % Plot
                obj.RhoAxesLines(ii-1) = line(ax, x_axes, y_axes, 'Color', grey, 'HandleVisibility', 'off');
            end
            
            %%% Plot %%%
            % Iterate through number of data groups
            for ii = obj.NumDataGroups:-1:1
                % Convert polar to cartesian coordinates
                [x_points, y_points] = pol2cart(theta(1:end-1), P_scaled(ii, :));
                
                % Make points circular
                x_circular = [x_points, x_points(1)];
                y_circular = [y_points, y_points(1)];
                
                % Plot data points
                obj.DataLines(ii).XData = x_circular;
                obj.DataLines(ii).YData = y_circular;
                
                % Check if fill option is toggled on
                obj.FillPatches(ii).XData = x_circular;
                obj.FillPatches(ii).YData = y_circular;

            end
            
            %%% Labels %%%
            % Iterate through number of data points
            for ii = 1:obj.NumDataPoints
                % Angle of point in radians
                [horz_align, vert_align, x_pos, y_pos] = getQuadrantPosition(theta(ii));
                
                % Display text label
                obj.AxesTextLabels(ii) = text(ax, x_axes(ii)+x_pos, y_axes(ii)+y_pos, '', ...
                    'Units', 'Data', ...
                    'HorizontalAlignment', horz_align, ...
                    'VerticalAlignment', vert_align, ...
                    'EdgeColor', 'k', ...
                    'BackgroundColor', 'w', ...
                    'Visible', 'off');
            end
            
        end
        
        % Delete graphics objects and reset properties before reinitializing
        function resetStoredGraphicsObjects(obj)
            % Delete old objects
            delete(obj.ThetaAxesLines)
            delete(obj.RhoAxesLines)
            delete(obj.DataLines)
            delete(obj.LabelObjects)
            delete(obj.FillPatches)
            delete(obj.AxesTextLabels)
            delete(obj.AxesTickLabels)                
            
            % Preallocate new ones as empties
            obj.ThetaAxesLines = gobjects(0);
            obj.RhoAxesLines = gobjects(0);
            obj.DataLines = gobjects(0);
            obj.LabelObjects = gobjects(0);
            obj.FillPatches = gobjects(0);
            obj.AxesTextLabels = gobjects(0);
            obj.AxesTickLabels = gobjects(0);
            obj.AxesValues = [];
        end
        
    end
    
end

function [horz_align, vert_align, x_pos, y_pos] = getQuadrantPosition(theta_point)

% Find out which quadrant the point is in
if theta_point == 0
    quadrant = 0;
elseif theta_point == pi/2
    quadrant = 1.5;
elseif theta_point == pi
    quadrant = 2.5;
elseif theta_point == 3*pi/2
    quadrant = 3.5;
elseif theta_point == 2*pi
    quadrant = 0;
elseif theta_point > 0 && theta_point < pi/2
    quadrant = 1;
elseif theta_point > pi/2 && theta_point < pi
    quadrant = 2;
elseif theta_point > pi && theta_point < 3*pi/2
    quadrant = 3;
elseif theta_point > 3*pi/2 && theta_point < 2*pi
    quadrant = 4;
end

% Adjust label alignment depending on quadrant
% Shift axis label
shift_pos = 0.13;
switch quadrant
    case 0
        horz_align = 'left';
        vert_align = 'middle';
        x_pos = shift_pos;
        y_pos = 0;
    case 1
        horz_align = 'left';
        vert_align = 'bottom';
        x_pos = shift_pos;
        y_pos = shift_pos;
    case 1.5
        horz_align = 'center';
        vert_align = 'bottom';
        x_pos = 0;
        y_pos = shift_pos;
    case 2
        horz_align = 'right';
        vert_align = 'bottom';
        x_pos = -shift_pos;
        y_pos = shift_pos;
    case 2.5
        horz_align = 'right';
        vert_align = 'middle';
        x_pos = -shift_pos;
        y_pos = 0;
    case 3
        horz_align = 'right';
        vert_align = 'top';
        x_pos = -shift_pos;
        y_pos = -shift_pos;
    case 3.5
        horz_align = 'center';
        vert_align = 'top';
        x_pos = 0;
        y_pos = -shift_pos;
    case 4
        horz_align = 'left';
        vert_align = 'top';
        x_pos = shift_pos;
        y_pos = -shift_pos;
end

end

Comments

Do you have a use for custom charts or a chart you would like MathWorks to make?

Give it a try and let us know what you think here or leave a comment for Moses.

Published with MATLAB® R2020a

|
  • print

Comments

To leave a comment, please click here to sign in to your MathWorks Account or create a new one.