Developing advanced MATLAB® visualizations often involves managing multiple low-level graphics objects. This is especially the case for applications containing graphics that update dynamically. Such applications may require time-consuming programming.
A chart object provides a high-level application programming interface (API) for creating custom visualizations. A chart not only provides a convenient visualization API for your end users; it also removes the need for the user to implement low-level graphics programming.
Using a scatter plot containing the best-fit line as the main example, this article provides a step-by-step guide to creating and implementing a custom chart using MATLAB object-oriented programming. Topics include:
- Writing a standard chart template
- Writing a constructor method
- Encapsulating chart data and graphics using
private
properties - Creating a high-level visualization API using
Dependent
properties - Managing the chart life cycle
- Using inheritance to simplify the development of additional charts
Update: Starting in R2019b, MATLAB provides an object-oriented framework for developing custom chart classes. This framework complements the approach described in this article and simplifies some of the low-level programming details. See Chart Development Overview to learn more.
Chart Examples
Several charts are available in MATLAB, including the heatmap
chart, which visualizes matrix values overlaid on colored grid squares, and the geobubble
chart, which provides a quick way to plot discrete data points on a map (Figure 1).
In addition, we've created several application-specific charts (Figure 2). You can download these charts, together with the MATLAB code used in this article, from File Exchange.
Creating a 2D Scatter Plot: Function or Chart?
Let's say we want to create a 2D scatter plot containing the corresponding line of best fit (Figure 3). We could use the scatter
function to visualize the discrete (x,y) data points and the fitlm
function from Statistics and Machine Learning Toolbox™ to compute the best-fit line.
rng default x = randn(1000,1); y = 2*x+1+randn(size(x)); s = scatter(x,y,6,'filled','MarkerFaceAlpha',0.5); m = fitlm(x,y); hold on plot(x,m.Fitted,'LineWidth',2)
The code above is sufficient for static visualizations. However, if the application requires the data to be dynamically modifiable, then we encounter several challenges:
- If we replace the
XData
orYData
with a new array of the same length as the currentXData
, the best-fit line is not dynamically updated (Figure 4).s.XData = s.XData+4;
- The
Scatter
objects
issues a warning, and performs no graphics update, if either of its data properties (XData
orYData
) is set to an array that is longer or shorter than the current array.s.XData = s.XData(1:500);
We can resolve these challenges by designing a chart, ScatterFit
.
Structuring the Chart Code: Function or Class?
A function encapsulates the code as a reusable unit and lets you create multiple charts without duplicating the code.
function scatterfit(varargin) % Ensure 2 or 3 inputs. narginchk(2,3) % We support the calling syntax scatterfit(x,y) or % scatterfit(f,x,y), where f is the parent graphics. switch nargin case 2 f = gcf; x = varargin{1}; y = varargin{2}; otherwise % case 3 f = varargin{1}; x = varargin{2}; y = varargin{3}; end % switch/case % Create the chart axes and scatter plot. ax = axes('Parent',f); scatter(ax,x,y,6,'filled') % Compute and create the best-fit line. m = fitlm(x,y); hold(ax,'on') plot(ax,x,m.Fitted,'LineWidth',2) hold(ax,'off') end % scatterfit function
Note that this function requires the two data inputs (x and y). You can specify the graphics parent f
(for example, a figure) as the first input argument.
scatterfit(x,y)
specifies the two data inputs.scatterfit(f,x,y)
specifies the graphics parent and the data.
In the first case, the function exhibits autoparenting behavior—that is, a figure for the chart will be created automatically.
Using a function to create a chart has some drawbacks:
- You cannot modify the data after the chart has been created.
- To change the chart data, you need to call the function again to recreate the chart, specifying different data inputs.
- It will be difficult for the end user to locate configurable chart parameters (such as annotations and scatter/line properties).
Implementing the chart as a class has all the benefits of code encapsulation and reusability that the function provides while also letting you modify the chart.
Defining a Chart
We'll implement the chart as a handle
class, for consistency with MATLAB graphics objects and so that the chart can be modified in place. We support both the dot notation and the get/set
syntax for chart properties. To achieve this, we derive the ScatterFit
chart from the predefined matlab.mixin.SetGet
class, which is itself a handle
class.
classdef ScatterFit < matlab.mixin.SetGet
As a result, for any property, the syntax shown in Table 1 is automatically supported.
Syntax Type
Dot notation
get/set
Access
x = SF.XData;
x = get(SF,'XData');
Modification
SF.XData = x;
set(SF,'XData',x)
Table 1. Access and modification syntax for chart properties.
Writing the Chart’s Constructor Method
The constructor method is the function within the class definition where the chart object is created. A good place to start is to copy the code from the scatterfit
function to our chart constructor. We then make the following modifications to support the required chart behavior:
- Input arguments. We support the use of name-value pairs for all chart input arguments using
varargin
. This means that no input arguments need to be specified, and all inputs are optional.
function obj = ScatterFit(varargin)
- Parent graphics. Unlike the functional approach, if no
Parent
input on chart construction has been specified, then we do not automatically create one for the chart. Instead, we create anaxes
object with an emptyParent
.obj.Axes = axes('Parent',[]);
Note that this behavior is different from that of convenience functions such as
plot
andscatter
, which exhibit autoparenting. If the user has specified theParent
as an input argument, then this will be set later in the constructor. - Chart graphics. We create and store any graphics objects required by the chart. Most charts will require an axes object together with some axes contents such as line or patch objects. In the
ScatterFit
chart, we need aScatter
object and aLine
object.obj.ScatterSeries = scatter(obj.Axes,NaN,NaN); obj.BestFitLine = line(obj.Axes,NaN,NaN);
- Graphics configuration. We configure the chart graphics by setting any required properties. For example, we may create annotations such as labels or titles, set a specific view of an axes, add a grid, or adjust the color, style, or width of a line.
- User-specified inputs. We set any name-value pair arguments supplied by the end user. Since the chart is derived from
matlab.mixin.SetGet
, this is particularly easy to do:if ~isempty(varargin) set(obj,varargin{:}); end
This is where the data properties (
XData
andYData
) are set, provided that the user has supplied these as name-value pair input arguments. We also note that this coding practice ensures that any errors caused by the user when specifying the name-value pairs will be caught and handled by the chart’s propertyset
methods (discussed later).
Whenever practical, we use primitive objects to create the chart graphics, because high-level convenience functions reset many existing axes properties when called (Table 2). However, there are exceptions to this principle: within ScatterFit
, we use the scatter
function to create the Scatter
graphics object because it supports subsequent changes to individual marker sizes and colors.
Primitive Graphics Function
line
surface
patch
High-Level Graphics Function
plot
surf
fill
Table 2. Examples of primitive and high-level graphics functions.
Encapsulating Chart Data and Graphics
In most charts, the underlying graphics comprise at least one axes object together with its contents (for example, line or surface objects) or axes peer objects (for example, legends or colorbars). The chart also retains the internal data properties to ensure that the public properties are consistent with each other. We store the underlying graphics and internal data as private chart properties. For example, the ScatterFit
chart maintains the following private properties:
properties (Access = private) XData_ YData_ Axes ScatterSeries BestFitLine Legend end
We use the naming convention XData_
to indicate that this is the private, internal version of the chart data. The corresponding public data property visible to the user will be named XData
.
Using private
properties serves three main purposes:
- Visibility of the low-level graphics is restricted, hiding implementation details and reducing visual clutter in the API.
- Access to the low-level graphics is restricted, reducing the chance of bypassing the API.
- Chart data can be easily synchronized (for example, we require the
XData
andYData
properties ofScatterFit
to be related).
Providing a Visualization API
One of the main reasons for designing a chart is to provide a convenient and intuitive API. We equip the ScatterFit
chart with easily recognizable properties, using names consistent with existing graphics object properties (Figure 5).
Users can access or modify these properties using the syntax shown in Table 1. The associated chart graphics update dynamically in response to property modifications. For example, changing the LineWidth
property of the chart updates the LineWidth
of the best-fit line.
We implement the chart API using Dependent
class properties. A Dependent
property is one whose value is not stored explicitly but is derived from other properties in the class. In a chart, the Dependent
properties depend on private properties such as the low-level graphics or internal data properties.
To define a Dependent
property, we first declare its name in a properties
block with the attribute Dependent
. This indicates that the property’s value depends on other properties within the class.
properties (Dependent) XData YData end
We also need to specify how the property depends on the other class properties by writing the corresponding get
method. This method returns a single output argument—namely, the value of the Dependent
property. In the ScatterFit
chart, the XData
property (part of the chart’s public interface) is simply the underlying XData_
property, which is stored internally as a private
property of the chart.
function x = get.XData(obj) x = obj.XData_; end
We write a set
method for each configurable chart property. This method assigns the user-specified value to the correct internal chart property, triggering graphics updates if necessary.
For the ScatterFit
chart, we support dynamic modifications (including length changes) to the data properties (XData
and YData
). When the user sets the (public) XData
of the chart, we either pad or truncate the opposite (private) data property YData_
, depending on whether the new data vector is longer or shorter than the existing data. Recall that this set
method will be invoked by the constructor if the user has specified XData
when creating the chart.
function set.XData( obj, x ) % Perform basic validation. validateattributes(x,{'double'},... {'real', 'vector'},'ScatterFit/set.XData','the x-data') % Decide how to modify the chart data. nX = numel(x); nY = numel(obj.YData_); if nX < nY % If the new x-data is too short ... % ... then truncate the chart y-data. obj.YData_ = obj.YData_(1:nX); else % Otherwise, if nX >= nY, then pad the y-data. obj.YData_(end+1:nX) = NaN; end % if % Set the internal x-data. obj.XData_ = x(:); % Update the chart graphics. update(obj); end % set.XData
We refresh the chart graphics by calling a separate update
method. This method contains the code necessary to set the new data in the Scatter
object, recompute the best-fit line, and set the new data in the corresponding Line
object.
function update(obj) % Update the scatter series with the new data. set(obj.ScatterSeries,'XData',obj.XData_,... 'YData',obj.YData_); % Obtain the new best-fit line. m = fitlm(obj.XData_,obj.YData_); % Update the best-fit line graphics. [~, posMin] = min(obj.XData_); [~, posMax] = max(obj.XData_); set(obj.BestFitLine,'XData',obj.XData_([posMin, posMax]), ... 'YData',m.Fitted([posMin, posMax])); end % update
We implement the set
method for YData
in the same way, switching the roles of the X/YData
properties. The update
method is also called from the set
method for YData
.
To create a rich API appropriate for end users, we implement a broad set of Dependent
properties. As a minimum, for each chart we recommend including the properties shown in Table 3.
Property
Parent
Purpose
Move the chart between figures, panels, or other container graphics
Position
Units
OuterPosition
ActivePositionProperty
Resize the chart
Visible
Toggle onscreen chart visibility
Table 3. Recommended Dependent
properties.
Note that in most cases, these properties map directly to the underlying chart axes. For example, the get
and set
methods for the Parent
property map the chart object’s Parent
to the axes’ Parent
.
function value = get.Parent(obj) value = obj.Axes.Parent; end function set.Parent(obj,value) obj.Axes.Parent = value; end
We enable control of visual settings by defining additional public interface properties, with each property mapped to a specific low-level graphics object maintained by the chart. In this category, the ScatterFit
chart supports various line-related properties such as LineStyle
, LineWidth
, and LineColor
relating to the best-fit line. For example, the chart object’s LineColor
property is mapped to the line object’s Color
property.
function value = get.LineColor(obj) value = obj.BestFitLine.Color; end function set.LineColor(obj,value) obj.BestFitLine.Color = value; end
Typical chart properties in this category include:
- View-related properties—for example, the axes’
View
,XLim
, andYLim
- Annotations—for example, the axes’
XLabel
,YLabel
, andTitle
- Cosmetic properties—for example, colors, line widths, and style; grids; transparencies; and lighting
Managing the Chart Life Cycle
The ScatterFit
chart is closely associated with its underlying axes object, which is stored as one of the chart’s private
properties. To manage the chart’s life cycle correctly, we need to guarantee two behaviors:
- Deleting the axes (for example, by closing the main figure window) deletes the chart. If we do not guarantee this, then modifying the chart’s data properties will cause MATLAB to attempt an update on a deleted graphics object, leading to an error.
- Deleting the chart (for example, when it goes out of scope or when its handle is deleted explicitly) deletes the axes. If we do not guarantee this, then on chart deletion we are left with its static graphical remnants.
Every graphics object in MATLAB has a property named DeleteFcn
, a callback function invoked automatically when the graphics object goes out of scope. Therefore, we can guarantee the first requirement by setting the axes’ DeleteFcn
in the chart constructor.
obj.Axes.DeleteFcn = @obj.onAxesDeleted;
Here, onAxesDeleted
is a private
class method and is simply a wrapper around the chart destructor method. We recall that every handle
class comes equipped with a customizable destructor method. The destructor method is invoked when the object goes out of scope.
function onAxesDeleted(obj,~,~) delete(obj); end
We ensure the second requirement by writing a custom chart destructor. On chart destruction, we delete the chart’s axes.
function delete(obj) delete(obj.Axes); end
Implementing both requirements equips the chart object with the same life cycle as its underlying axes (Figure 6).
Simplifying the Development of Additional Charts
Once we have written several charts, it is easy to identify similarities and duplicated sections of code. We can speed up the process of writing additional charts by centralizing common code in a superclass. Each new chart can be derived from this superclass, enabling us to focus on the implementation details of that particular chart and reducing the need for repetitive coding.
Our superclass (named Chart
) is structured as follows:
Chart
is derived frommatlab.mixin.SetGet
.Chart
implements the six coreDependent
propertiesParent
,Position
,Units
,OuterPosition
,ActivePositionProperty
, andVisible
.Chart
has aprotected
propertyAxes
(the underlying graphics peer).- The
Chart
constructor creates the peer axes object and sets the axes’DeleteFcn
as theprotected
methodonAxesDeleted
. In turn, this method deletes the chart object. - The
Chart
destructor deletes the axes object.
Note that using the superclass Chart
may not be appropriate for all charts. For example, charts maintaining multiple axes require some changes to the architecture described above. We could implement such charts using a uipanel
instead of an axes object as the chart’s underlying graphics peer, and creating the multiple axes inside the panel.
Summary
In this article, we described a design pattern for implementing custom charts, using the ScatterFit
chart as an example. Many common visualization tasks, especially those that require dynamic graphics, can be performed using an appropriate chart. Designing and creating a chart requires up-front development time and effort, but charts can substantially simplify many visualization workflows.