PHD Project - Driver energy prediction
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

1294 lines
46 KiB

function cleanfigure(varargin)
% CLEANFIGURE() removes the unnecessary objects from your MATLAB plot
% to give you a better experience with matlab2tikz.
% CLEANFIGURE comes with several options that can be combined at will.
%
% CLEANFIGURE('handle',HANDLE,...) explicitly specifies the
% handle of the figure that is to be stored. (default: gcf)
%
% CLEANFIGURE('pruneText',BOOL,...) explicitly specifies whether text
% should be pruned. (default: true)
%
% CLEANFIGURE('targetResolution',PPI,...)
% CLEANFIGURE('targetResolution',[W,H],...)
% Reduce the number of data points in line objects by applying
% unperceivable changes at the target resolution.
% The target resolution can be specificed as the number of Pixels Per
% Inch (PPI), e.g. 300, or as the Width and Heigth of the figure in
% pixels, e.g. [9000, 5400].
% Use targetResolution = Inf or 0 to disable line simplification.
% (default: 600)
%
% CLEANFIGURE('scalePrecision',alpha,...)
% Scale the precision the data is represented with. Setting it to 0
% or negative values disable this feature.
% (default: 1)
%
% CLEANFIGURE('normalizeAxis','xyz',...)
% EXPERIMENTAL: Normalize the data of the dimensions specified by
% 'normalizeAxis' to the interval [0, 1]. This might have side effects
% with hgtransform and friends. One can directly pass the axis handle to
% cleanfigure to ensure that only one axis gets normalized.
% Usage: Input 'xz' normalizes only x- and zData but not yData
% (default: '')
%
% Example
% x = -pi:pi/1000:pi;
% y = tan(sin(x)) - sin(tan(x));
% plot(x,y,'--rs');
% cleanfigure();
%
% See also: matlab2tikz
% Treat hidden handles, too.
shh = get(0, 'ShowHiddenHandles');
set(0, 'ShowHiddenHandles', 'on');
% Keep track of the current axes.
meta.gca = [];
% Set up command line options.
m2t.cmdOpts = m2tInputParser;
m2t.cmdOpts = m2t.cmdOpts.addParamValue(m2t.cmdOpts, 'handle', gcf, @ishandle);
m2t.cmdOpts = m2t.cmdOpts.addParamValue(m2t.cmdOpts, 'targetResolution', 600, @isValidTargetResolution);
m2t.cmdOpts = m2t.cmdOpts.addParamValue(m2t.cmdOpts, 'pruneText', true, @islogical);
m2t.cmdOpts = m2t.cmdOpts.addParamValue(m2t.cmdOpts, 'minimumPointsDistance', 1.0e-10, @isnumeric);
m2t.cmdOpts = m2t.cmdOpts.addParamValue(m2t.cmdOpts, 'scalePrecision', 1, @isnumeric);
m2t.cmdOpts = m2t.cmdOpts.addParamValue(m2t.cmdOpts, 'normalizeAxis', '', @isValidAxis);
% Deprecated parameters
m2t.cmdOpts = m2t.cmdOpts.deprecateParam(m2t.cmdOpts, 'minimumPointsDistance', 'targetResolution');
% Finally parse all the elements.
m2t.cmdOpts = m2t.cmdOpts.parse(m2t.cmdOpts, varargin{:});
% Recurse down the tree of plot objects and clean up the leaves.
for h = m2t.cmdOpts.Results.handle(:)'
recursiveCleanup(meta, h, m2t.cmdOpts.Results);
end
% Reset to initial state.
set(0, 'ShowHiddenHandles', shh);
return;
end
% =========================================================================
function recursiveCleanup(meta, h, cmdOpts)
% Recursive function, that cleans up the individual childs of a figure
% Get the type of the current figure handle
type = get(h, 'Type');
%display(sprintf([repmat(' ',1,indent), type, '->']))
% Don't try to be smart about quiver groups.
% NOTE:
% A better way to write `strcmp(get(h,...))` would be to use
% isa(handle(h), 'specgraph.quivergroup').
% The handle() function isn't supported by Octave, though, so let's stick
% with strcmp().
if strcmp(type, 'specgraph.quivergroup')
%if strcmp(class(handle(h)), 'specgraph.quivergroup')
return;
end
% Update the current axes.
if strcmp(type, 'axes')
meta.gca = h;
if ~isempty(cmdOpts.normalizeAxis)
% If chosen transform the date axis
normalizeAxis(h, cmdOpts);
end
end
children = get(h, 'Children');
if ~isempty(children)
for child = children(:)'
recursiveCleanup(meta, child, cmdOpts);
end
else
if strcmp(type, 'line')
% Remove data points outside of the axes
% NOTE: Always remove invisible points before simplifying the
% line. Otherwise it will generate additional line segments
pruneOutsideBox(meta, h);
% Move some points closer to the box to avoid TeX:DimensionTooLarge
% errors. This may involve inserting extra points.
movePointsCloser(meta, h);
% Simplify the lines by removing superflous points
simplifyLine(meta, h, cmdOpts.targetResolution);
% Limit the precision of the output
limitPrecision(meta, h, cmdOpts.scalePrecision);
elseif strcmpi(type, 'stair')
% Remove data points outside of the visible axes
pruneOutsideBox(meta, h);
% Remove superfluous data points
simplifyStairs(meta, h);
% Limit the precision of the output
limitPrecision(meta, h, cmdOpts.scalePrecision);
elseif strcmp(type, 'text') && cmdOpts.pruneText
% Prune text that is outside of the axes
pruneOutsideText(meta, h);
end
end
return;
end
% =========================================================================
function pruneOutsideBox(meta, handle)
% Some sections of the line may sit outside of the visible box.
% Cut those off.
% Extract the visual data from the current line handle.
[xData, yData] = getVisualData(meta, handle);
% Merge the data into one matrix
data = [xData, yData];
% Dont do anything if the data is empty
if isempty(data)
return;
end
% Check if the plot has lines
hasLines = ~strcmp(get(handle, 'LineStyle'),'none')...
&& get(handle, 'LineWidth') > 0.0;
% Extract the visual limits from the current line handle.
[xLim, yLim]= getVisualLimits(meta);
tol = 1.0e-10;
relaxedXLim = xLim + [-tol, tol];
relaxedYLim = yLim + [-tol, tol];
% Get which points are inside a (slightly larger) box.
dataIsInBox = isInBox(data, relaxedXLim, relaxedYLim);
% Plot all the points inside the box
shouldPlot = dataIsInBox;
if hasLines
% Check if the connecting line between two data points is visible.
segvis = segmentVisible(data, dataIsInBox, xLim, yLim);
% Plot points which part of an visible line segment.
shouldPlot = shouldPlot | [false; segvis] | [segvis; false];
end
% Remove or replace points outside the box
id_replace = [];
id_remove = [];
if ~all(shouldPlot)
% For line plots, simply removing the data has the disadvantage
% that the line between two 'loose' ends may now appear in the figure.
% To avoid this, add a row of NaNs wherever a block of actual data is
% removed.
% Get the indices of points that should be removed
id_remove = find(~shouldPlot);
% If there are consecutive data points to be removed, only replace
% the first one by a NaN. Consecutive data points have
% diff(id_remove)==1, so replace diff(id_remove)>1 by NaN and remove
% the rest
idx = [true; diff(id_remove) >1];
id_replace = id_remove(idx);
id_remove = id_remove(~idx);
end
% Replace the data points
replaceDataWithNaN(meta, handle, id_replace);
% Remove the data outside the box
removeData(meta, handle, id_remove);
% Remove possible NaN duplications
removeNaNs(meta, handle);
return;
end
% =========================================================================
function movePointsCloser(meta, handle)
% Move all points outside a box much larger than the visible one
% to the boundary of that box and make sure that lines in the visible
% box are preserved. This typically involves replacing one point by
% two new ones and a NaN.
% TODO: 3D simplification of frontal 2D projection. This requires the
% full transformation rather than the projection, as we have to calculate
% the inverse transformation to project back into 3D
if isAxis3D(meta.gca)
return;
end
% Extract the visual data from the current line handle.
[xData, yData] = getVisualData(meta, handle);
% Extract the visual limits from the current line handle.
[xLim, yLim] = getVisualLimits(meta);
% Calculate the extension of the extended box
xWidth = xLim(2) - xLim(1);
yWidth = yLim(2) - yLim(1);
% Don't choose the larger box too large to make sure that the values inside
% it can still be treated by TeX.
extendFactor = 0.1;
largeXLim = xLim + extendFactor * [-xWidth, xWidth];
largeYLim = yLim + extendFactor * [-yWidth, yWidth];
% Put the data into one matrix
data = [xData, yData];
% Get which points are in an extended box (the limits of which
% don't exceed TeX's memory).
dataIsInLargeBox = isInBox(data, largeXLim, largeYLim);
% Count the NaNs as being inside the box.
dataIsInLargeBox = dataIsInLargeBox | any(isnan(data), 2);
% Find all points which are to be included in the plot yet do not fit
% into the extended box
id_replace = find(~dataIsInLargeBox);
% Only try to replace points if there are some to replace
dataInsert = {};
if ~isempty(id_replace)
% Get the indices of those points, that are the first point in a
% segment. The last data point at size(data, 1) cannot be the first
% point in a segment.
id_first = id_replace(id_replace < size(data, 1));
% Get the indices of those points, that are the second point in a
% segment. Similarly the first data point cannot be the second data
% point in a segment.
id_second = id_replace(id_replace > 1);
% Define the vectors of data points for the segments X1--X2
X1_first = data(id_first, :);
X2_first = data(id_first+1, :);
X1_second = data(id_second, :);
X2_second = data(id_second-1, :);
% Move the points closer to the large box along the segment
newData_first = moveToBox(X1_first, X2_first, largeXLim, largeYLim);
newData_second= moveToBox(X1_second, X2_second, largeXLim, largeYLim);
% Respect logarithmic scaling for the new points
isXlog = strcmp(get(meta.gca, 'XScale'), 'log');
if isXlog
newData_first (:, 1) = 10.^newData_first (:, 1);
newData_second(:, 1) = 10.^newData_second(:, 1);
end
isYlog = strcmp(get(meta.gca, 'YScale'), 'log');
if isYlog
newData_first (:, 2) = 10.^newData_first (:, 2);
newData_second(:, 2) = 10.^newData_second(:, 2);
end
% If newData_* is infinite, the segment was not visible. However, as we
% move the point closer, it would become visible. So insert a NaN.
isInfinite_first = any(~isfinite(newData_first), 2);
isInfinite_second = any(~isfinite(newData_second), 2);
newData_first (isInfinite_first, :) = NaN(sum(isInfinite_first), 2);
newData_second(isInfinite_second, :) = NaN(sum(isInfinite_second), 2);
% If a point is part of two segments, that cross the border, we need to
% insert a NaN to prevent an additional line segment
[trash, trash, id_conflict] = intersect(id_first (~isInfinite_first), ...
id_second(~isInfinite_second));
% Cut the data into length(id_replace)+1 segments.
% Calculate the length of the segments
length_segments = [id_replace(1);
diff(id_replace);
size(data, 1)-id_replace(end)];
% Create an empty cell array for inserting NaNs and fill it at the
% conflict sites
dataInsert_NaN = cell(length(length_segments),1);
dataInsert_NaN(id_conflict) = mat2cell(NaN(length(id_conflict), 2),...
ones(size(id_conflict)), 2);
% Create a cell array for the moved points
dataInsert_first = mat2cell(newData_first, ones(size(id_first)), 2);
dataInsert_second = mat2cell(newData_second, ones(size(id_second)), 2);
% Add an empty cell at the end of the last segment, as we do not
% insert something *after* the data
dataInsert_first = [dataInsert_first; cell(1)];
dataInsert_second = [dataInsert_second; cell(1)];
% If the first or the last point would have been replaced add an empty
% cell at the beginning/end. This is because the last data point
% cannot be the first data point of a line segment and vice versa.
if(id_replace(end) == size(data, 1))
dataInsert_first = [dataInsert_first; cell(1)];
end
if(id_replace(1) == 1)
dataInsert_second = [cell(1); dataInsert_second];
end
% Put the cells together, right points first, then the possible NaN
% and then the left points
dataInsert = cellfun(@(a,b,c) [a; b; c],...
dataInsert_second,...
dataInsert_NaN,...
dataInsert_first,...
'UniformOutput',false);
end
% Insert the data
insertData(meta, handle, id_replace, dataInsert);
% Get the indices of the to be removed points accounting for the now inserted
% data points
numPointsInserted = cellfun(@(x) size(x,1), [cell(1);dataInsert(1:end-2)]);
id_remove = id_replace + cumsum(numPointsInserted);
% Remove the data point that should be replaced.
removeData(meta, handle, id_remove);
% Remove possible NaN duplications
removeNaNs(meta, handle);
end
% =========================================================================
function simplifyLine(meta, handle, targetResolution)
% Reduce the number of data points in the line 'handle'.
%
% Aplies a path-simplification algorithm if there are no markers or
% pixelization otherwise. Changes are visually negligible at the target
% resolution.
%
% The target resolution is either specificed as the number of PPI or as
% the [Width, Heigth] of the figure in pixels.
% A scalar value of INF or 0 disables path simplification.
% (default = 600)
% Do not simpify
if any(isinf(targetResolution) | targetResolution == 0)
return
end
% Retrieve target figure size in pixels
[W, H] = getWidthHeightInPixels(targetResolution);
% Extract the visual data from the current line handle.
[xData, yData] = getVisualData(meta, handle);
% Only simplify if there are more than 2 points
if numel(xData) <= 2 || numel(yData) <= 2
return;
end
% Extract the visual limits from the current line handle.
[xLim, yLim] = getVisualLimits(meta);
% Automatically guess a tol based on the area of the figure and
% the area and resolution of the output
xRange = xLim(2)-xLim(1);
yRange = yLim(2)-yLim(1);
% Conversion factors of data units into pixels
xToPix = W/xRange;
yToPix = H/yRange;
% Mask for removing data points
id_remove = [];
% If the path has markers, perform pixelation instead of simplification
hasMarkers = ~strcmpi(get(handle,'Marker'),'none');
hasLines = ~strcmpi(get(handle,'LineStyle'),'none');
if hasMarkers && ~hasLines
% Pixelate data at the zoom multiplier
mask = pixelate(xData, yData, xToPix, yToPix);
id_remove = find(mask==0);
elseif hasLines && ~hasMarkers
% Get the width of a pixel
xPixelWidth = 1/xToPix;
yPixelWidth = 1/yToPix;
tol = min(xPixelWidth,yPixelWidth);
% Split up lines which are seperated by NaNs
id_nan = isnan(xData) | isnan(yData);
% If lines were separated by a NaN, diff(~id_nan) would give 1 for
% the start of a line and -1 for the index after the end of
% a line.
id_diff = diff([false; ~id_nan; false]);
lineStart = find(id_diff == 1);
lineEnd = find(id_diff == -1)-1;
numLines = numel(lineStart);
id_remove = cell(numLines, 1);
% Simplify the line segments
for ii = 1:numLines
% Actual data that inherits the simplifications
x = xData(lineStart(ii):lineEnd(ii));
y = yData(lineStart(ii):lineEnd(ii));
% Line simplification
if numel(x) > 2
mask = opheimSimplify(x, y, tol);
% Remove all those with mask==0 respecting the number of
% data points in the previous segments
id_remove{ii} = find(mask==0) + lineStart(ii) - 1;
end
end
% Merge the indices of the line segments
id_remove = cat(1, id_remove{:});
end
% Remove the data points
removeData(meta, handle, id_remove);
end
% =========================================================================
function simplifyStairs(meta, handle)
% This function simplifies stair plots by removeing superflous data
% points
% Some data might not lead to a new step in the stair. This is the case
% if the difference in one dimension is zero, e.g
% [(x_1, y_1), (x_2, y_1), ... (x_k, y_1), (x_{k+1} y_2)].
% However, there is one exeption. If the monotonicity of the other
% dimension changes, e.g. the sequence [0, 1], [0, -1], [0, 2]. This
% sequence cannot be simplified. Therefore, we check for monoticity too.
% As an example, we can remove the data points marked with x in the
% following stair
% o--x--o
% | |
% x o --x--o
% |
% o--x--o
% |
% o
% Extract the data
xData = get(handle, 'XData');
yData = get(handle, 'YData');
% Do not do anything if the data is empty
if isempty(xData) || isempty(yData)
return;
end
% Check for nonchanging data points
xNoDiff = [false, (diff(xData) == 0)];
yNoDiff = [false, (diff(yData) == 0)];
% Never remove the last data point
xNoDiff(end) = false;
yNoDiff(end) = false;
% Check for monotonicity (it changes if diff(sign)~=0)
xIsMonotone = [true, diff(sign(diff(xData)))==0, true];
yIsMonotone = [true, diff(sign(diff(yData)))==0, true];
% Only remove points when there is no difference in one dimension and no
% change in monotonicity in the other
xRemove = xNoDiff & yIsMonotone;
yRemove = yNoDiff & xIsMonotone;
% Plot only points, that generate a new step
id_remove = find(xRemove | yRemove);
% Remove the superfluous data
removeData(meta, handle, id_remove);
end
% =========================================================================
function limitPrecision(meta, handle, alpha)
% Limit the precision of the given data
% If alpha is 0 or negative do nothing
if alpha<=0
return
end
% Extract the data from the current line handle.
xData = get(handle, 'XData');
yData = get(handle, 'YData');
if isAxis3D(meta.gca)
zData = get(handle, 'ZData');
end
% Check for log scaling
isXlog = strcmp(get(meta.gca, 'XScale'), 'log');
isYlog = strcmp(get(meta.gca, 'YScale'), 'log');
isZlog = strcmp(get(meta.gca, 'ZScale'), 'log');
% Put the data into a matrix and log bits into vector
if isAxis3D(meta.gca)
data = [xData(:), yData(:), zData(:)];
isLog = [isXlog, isYlog, isZlog];
else
data = [xData(:), yData(:)];
isLog = [isXlog, isYlog];
end
% Only do something if the data is not empty
if isempty(data) || all(isinf(data(:)))
return
end
% Scale to visual coordinates
data(:, isLog) = log10(data(:, isLog));
% Get the maximal value of the data, only considering finite values
maxValue = max(abs(data(isfinite(data))));
% The least significant bit is proportional to the numerical precision
% of the largest number. Scale it with a user defined value alpha
leastSignificantBit = eps(maxValue) * alpha;
% Round to precision and scale back
data = round(data / leastSignificantBit) * leastSignificantBit;
% Scale back in case of log scaling
data(:, isLog) = 10.^data(:, isLog);
% Set the new data.
set(handle, 'XData', data(:, 1));
set(handle, 'YData', data(:, 2));
if isAxis3D(meta.gca)
set(handle, 'zData', data(:, 3));
end
end
% =========================================================================
function pruneOutsideText(meta, handle)
% Function to prune text outside of axis handles.
% Ensure units of type 'data' (default) and restore the setting later
units_original = get(handle, 'Units');
set(handle, 'Units', 'data');
% Check if text is inside bounds by checking if the position is inside
% the x, y and z limits. This works for both 2D and 3D plots.
xLim = get(meta.gca, 'XLim');
yLim = get(meta.gca, 'YLim');
zLim = get(meta.gca, 'ZLim');
axLim = [xLim; yLim; zLim];
pos = get(handle, 'Position');
% If the axis is 2D, ignore the z component and consider the extend of
% the textbox
if ~isAxis3D(meta.gca)
pos(3) = 0;
% In 2D plots the 'extent' of the textbox is available and also
% considered to keep the textbox, if it is partially inside the axis
% limits.
extent = get(handle, 'Extent');
% Extend the actual axis limits by the extent of the textbox so that
% the textbox is not discarded, if it overlaps the axis.
axLim(1, 1) = axLim(1, 1) - extent(3); % x-limit is extended by width
axLim(2, 1) = axLim(2, 1) - extent(4); % y-limit is extended by height
end
% Check if the (extended) textbox is inside the axis limits
bPosInsideLim = ( pos' >= axLim(:,1) ) & ( pos' <= axLim(:,2) );
% Restore original units (after reading all dimensions)
set(handle, 'Units', units_original);
% Check if it is the title
isTitle = (handle == get(meta.gca, 'title'));
% Disable visibility if it is outside the limits and it is not
% the title
if ~all(bPosInsideLim) && ~isTitle
% Warn about to be deprecated text removal
warning('cleanfigure:textRemoval', ...
'Text removal by cleanfigure is planned to be deprecated');
% Artificially disable visibility. m2t will check and skip.
set(handle, 'Visible', 'off');
end
end
% =========================================================================
function mask = isInBox(data, xLim, yLim)
% Returns a mask that indicates, whether a data point is within the
% limits
mask = data(:, 1) > xLim(1) & data(:, 1) < xLim(2) ...
& data(:, 2) > yLim(1) & data(:, 2) < yLim(2);
end
% =========================================================================
function mask = segmentVisible(data, dataIsInBox, xLim, yLim)
% Given a bounding box {x,y}Lim, determine whether the line between all
% pairs of subsequent data points [data(idx,:)<-->data(idx+1,:)] is
% visible. There are two possible cases:
% 1: One of the data points is within the limits
% 2: The line segments between the datapoints crosses the bounding box
n = size(data, 1);
mask = false(n-1, 1);
% Only check if there is more than 1 point
if n>1
% Define the vectors of data points for the segments X1--X2
idx= 1:n-1;
X1 = data(idx, :);
X2 = data(idx+1, :);
% One of the neighbors is inside the box and the other is finite
thisVisible = (dataIsInBox(idx) & all(isfinite(X2), 2));
nextVisible = (dataIsInBox(idx+1) & all(isfinite(X1), 2));
% Get the corner coordinates
[bottomLeft, topLeft, bottomRight, topRight] = corners2D(xLim, yLim);
% Check if data points intersect with the borders of the plot
left = segmentsIntersect(X1, X2, bottomLeft , topLeft);
right = segmentsIntersect(X1, X2, bottomRight, topRight);
bottom = segmentsIntersect(X1, X2, bottomLeft , bottomRight);
top = segmentsIntersect(X1, X2, topLeft , topRight);
% Check the result
mask = thisVisible | nextVisible | left | right | top | bottom;
end
end
% =========================================================================
function mask = segmentsIntersect(X1, X2, X3, X4)
% Checks whether the segments X1--X2 and X3--X4 intersect.
lambda = crossLines(X1, X2, X3, X4);
% Check whether lambda is in bound
mask = 0.0 < lambda(:, 1) & lambda(:, 1) < 1.0 &...
0.0 < lambda(:, 2) & lambda(:, 2) < 1.0;
end
% =========================================================================
function mask = pixelate(x, y, xToPix, yToPix)
% Rough reduction of data points at a multiple of the target resolution
% The resolution is lost only beyond the multiplier magnification
mult = 2;
% Convert data to pixel units and magnify
dataPixel = round([x * xToPix * mult, ...
y * yToPix * mult]);
% Sort the pixels
[dataPixelSorted, id_orig] = sortrows(dataPixel);
% Find the duplicate pixels
mask_sorted = [true; diff(dataPixelSorted(:,1))~=0 | ...
diff(dataPixelSorted(:,2))~=0];
% Unwind the sorting
mask = false(size(x));
mask(id_orig) = mask_sorted;
% Set the first, last, as well as unique pixels to true
mask(1) = true;
mask(end) = true;
% Set NaNs to true
inan = isnan(x) | isnan(y);
mask(inan) = true;
end
% =========================================================================
function mask = opheimSimplify(x,y,tol)
% Opheim path simplification algorithm
%
% Given a path of vertices V and a tolerance TOL, the algorithm:
% 1. selects the first vertex as the KEY;
% 2. finds the first vertex farther than TOL from the KEY and links
% the two vertices with a LINE;
% 3. finds the last vertex from KEY which stays within TOL from the
% LINE and sets it to be the LAST vertex. Removes all points in
% between the KEY and the LAST vertex;
% 4. sets the KEY to the LAST vertex and restarts from step 2.
%
% The Opheim algorithm can produce unexpected results if the path
% returns back on itself while remaining within TOL from the LINE.
% This behaviour can be seen in the following example:
%
% x = [1,2,2,2,3];
% y = [1,1,2,1,1];
% tol < 1
%
% The algorithm undesirably removes the second last point. See
% https://github.com/matlab2tikz/matlab2tikz/pull/585#issuecomment-89397577
% for additional details.
%
% To rectify this issues, step 3 is modified to find the LAST vertex as
% follows:
% 3*. finds the last vertex from KEY which stays within TOL from the
% LINE, or the vertex that connected to its previous point forms
% a segment which spans an angle with LINE larger than 90
% degrees.
mask = false(size(x));
mask(1) = true;
mask(end) = true;
N = numel(x);
i = 1;
while i <= N-2
% Find first vertex farther than TOL from the KEY
j = i+1;
v = [x(j)-x(i); y(j)-y(i)];
while j < N && norm(v) <= tol
j = j+1;
v = [x(j)-x(i); y(j)-y(i)];
end
v = v/norm(v);
% Unit normal to the line between point i and point j
normal = [v(2);-v(1)];
% Find the last point which stays within TOL from the line
% connecting i to j, or the last point within a direction change
% of pi/2.
% Starts from the j+1 points, since all previous points are within
% TOL by construction.
while j < N
% Calculate the perpendicular distance from the i->j line
v1 = [x(j+1)-x(i); y(j+1)-y(i)];
d = abs(normal.'*v1);
if d > tol
break
end
% Calculate the angle between the line from the i->j and the
% line from j -> j+1. If
v2 = [x(j+1)-x(j); y(j+1)-y(j)];
anglecosine = v.'*v2;
if anglecosine <= 0;
break
end
j = j + 1;
end
i = j;
mask(i) = true;
end
end
% =========================================================================
function lambda = crossLines(X1, X2, X3, X4)
% Checks whether the segments X1--X2 and X3--X4 intersect.
% See https://en.wikipedia.org/wiki/Line-line_intersection for reference.
% Given four points X_k=(x_k,y_k), k\in{1,2,3,4}, and the two lines
% defined by those,
%
% L1(lambda) = X1 + lambda (X2 - X1)
% L2(lambda) = X3 + lambda (X4 - X3)
%
% returns the lambda for which they intersect (and Inf if they are parallel).
% Technically, one needs to solve the 2x2 equation system
%
% x1 + lambda1 (x2-x1) = x3 + lambda2 (x4-x3)
% y1 + lambda1 (y2-y1) = y3 + lambda2 (y4-y3)
%
% for lambda1 and lambda2.
% Now X1 is a vector of all data points X1 and X2 is a vector of all
% consecutive data points X2
% n is the number of segments (not points in the plot!)
n = size(X2, 1);
lambda = zeros(n, 2);
% Calculate the determinant of A = [X2-X1, -(X4-X3)];
% detA = -(X2(1)-X1(1))*(X4(2)-X3(2)) + (X2(2)-X1(2))*(X4(1)-X3(1))
% NOTE: Vectorized this is equivalent to the matrix multiplication
% [nx2] * [2x2] * [2x1] = [nx1]
detA = -(X2(:, 1)-X1(:, 1)) .* (X4(2)-X3(2)) + (X2(:, 2)-X1(:, 2)) .* (X4(1)-X3(1));
% Get the indices for nonzero elements
id_detA = detA~=0;
if any(id_detA)
% rhs = X3(:) - X1(:)
% NOTE: Originaly this was a [2x1] vector. However as we vectorize the
% calculation it is beneficial to treat it as an [nx2] matrix rather than a [2xn]
rhs = bsxfun(@minus, X3, X1);
% Calculate the inverse of A and lambda
% invA=[-(X4(2)-X3(2)), X4(1)-X3(1);...
% -(X2(2)-X1(2)), X2(1)-X1(1)] / detA
% lambda = invA * rhs
% Rotational matrix with sign flip. It transforms a given vector [a,b] by
% Rotate * [a,b] = [-b,a] as required for calculation of invA
Rotate = [0, -1; 1, 0];
% Rather than calculating invA first and then multiply with rhs to obtain
% lambda, directly calculate the respective terms
% The upper half of the 2x2 matrix is always the same and is given by:
% [-(X4(2)-X3(2)), X4(1)-X3(1)] / detA * rhs
% This is a matrix multiplication of the form [1x2] * [2x1] = [1x1]
% As we have transposed rhs we can write this as:
% rhs * Rotate * (X4-X3) => [nx2] * [2x2] * [2x1] = [nx1]
lambda(id_detA, 1) = (rhs(id_detA, :) * Rotate * (X4-X3)')./detA(id_detA);
% The lower half is dependent on (X2-X1) which is a matrix of size [nx2]
% [-(X2(2)-X1(2)), X2(1)-X1(1)] / detA * rhs
% As both (X2-X1) and rhs are matrices of size [nx2] there is no simple
% matrix multiplication leading to a [nx1] vector. Therefore, use the
% elementwise multiplication and sum over it
% sum( [nx2] * [2x2] .* [nx2], 2) = sum([nx2],2) = [nx1]
lambda(id_detA, 2) = sum(-(X2(id_detA, :)-X1(id_detA, :)) * Rotate .* rhs(id_detA, :), 2)./detA(id_detA);
end
end
% =========================================================================
function minAlpha = updateAlpha(X1, X2, X3, X4, minAlpha)
% Checks whether the segments X1--X2 and X3--X4 intersect.
lambda = crossLines(X1, X2, X3, X4);
% Check if lambda is in bounds and lambda1 large enough
id_Alpha = 0.0 < lambda(:,2) & lambda(:,2) < 1.0 ...
& abs(minAlpha) > abs(lambda(:,1));
% Update alpha when applicable
minAlpha(id_Alpha) = lambda(id_Alpha,1);
end
% =========================================================================
function xNew = moveToBox(x, xRef, xLim, yLim)
% Takes a box defined by xlim, ylim, a vector of points x and a vector of
% reference points xRef.
% Returns the vector of points xNew that sits on the line segment between
% x and xRef *and* on the box. If several such points exist, take the
% closest one to x.
n = size(x, 1);
% Find out with which border the line x---xRef intersects, and determine
% the smallest parameter alpha such that x + alpha*(xRef-x)
% sits on the boundary. Otherwise set Alpha to inf.
minAlpha = inf(n, 1);
% Get the corner points
[bottomLeft, topLeft, bottomRight, topRight] = corners2D(xLim, yLim);
% left boundary:
minAlpha = updateAlpha(x, xRef, bottomLeft, topLeft, minAlpha);
% bottom boundary:
minAlpha = updateAlpha(x, xRef, bottomLeft, bottomRight, minAlpha);
% right boundary:
minAlpha = updateAlpha(x, xRef, bottomRight, topRight, minAlpha);
% top boundary:
minAlpha = updateAlpha(x, xRef, topLeft, topRight, minAlpha);
% Create the new point
xNew = x + bsxfun(@times ,minAlpha, (xRef-x));
end
% =========================================================================
function [xData, yData] = getVisualData(meta, handle)
% Returns the visual representation of the data (Respecting possible
% log_scaling and projection into the image plane)
% Check whether this is a 3D plot
is3D = isAxis3D(meta.gca);
% Extract the data from the current line handle.
xData = get(handle, 'XData');
yData = get(handle, 'YData');
if is3D
zData = get(handle, 'ZData');
end
% Get info about log scaling
isXlog = strcmp(get(meta.gca, 'XScale'), 'log');
if isXlog
xData = log10(xData);
end
isYlog = strcmp(get(meta.gca, 'YScale'), 'log');
if isYlog
yData = log10(yData);
end
isZlog = strcmp(get(meta.gca, 'ZScale'), 'log');
if isZlog
zData = log10(zData);
end
% In case of 3D plots, project the data into the image plane.
if is3D
% Get the projection matrix
P = getProjectionMatrix(meta);
% Put the data into one matrix accounting for the canonical 4th
% dimension
data = [xData(:), yData(:), zData(:), ones(size(xData(:)))];
% Project the data into the image plane
dataProjected = P * data';
% Only consider the x and y coordinates and scale them correctly
xData = dataProjected(1, :) ./ dataProjected(4, :);
yData = dataProjected(2, :) ./ dataProjected(4, :);
end
% Turn the data into a row vector
xData = xData(:);
yData = yData(:);
end
% =========================================================================
function [xLim, yLim] = getVisualLimits(meta)
% Returns the visual representation of the axis limits (Respecting
% possible log_scaling and projection into the image plane)
% Check whether this is a 3D plot
is3D = isAxis3D(meta.gca);
% Get the axis limits
xLim = get(meta.gca, 'XLim');
yLim = get(meta.gca, 'YLim');
zLim = get(meta.gca, 'ZLim');
% Check for logarithmic scales
isXlog = strcmp(get(meta.gca, 'XScale'), 'log');
if isXlog
xLim = log10(xLim);
end
isYlog = strcmp(get(meta.gca, 'YScale'), 'log');
if isYlog
yLim = log10(yLim);
end
isZlog = strcmp(get(meta.gca, 'ZScale'), 'log');
if isZlog
zLim = log10(zLim);
end
% In case of 3D plots, project the limits into the image plane. Depending
% on the angles, any of the 8 corners of the 3D cube mit be relevant so
% check for all
if is3D
% Get the projection matrix
P = getProjectionMatrix(meta);
% Get the coordinates of the 8 corners
corners = corners3D(xLim, yLim, zLim);
% Add the canonical 4th dimension
corners = [corners, ones(8,1)];
% Project the corner points to 2D coordinates
cornersProjected = P * corners';
% Pick the x and y values of the projected corners and scale them
% correctly
xCorners = cornersProjected(1, :) ./ cornersProjected(4, :);
yCorners = cornersProjected(2, :) ./ cornersProjected(4, :);
% Get the maximal and minimal values of the x and y coordinates as
% limits
xLim = [min(xCorners), max(xCorners)];
yLim = [min(yCorners), max(yCorners)];
end
end
% =========================================================================
function replaceDataWithNaN(meta, handle, id_replace)
% Replaces data at id_replace with NaNs
% Only do something if id_replace is not empty
if isempty(id_replace)
return
end
% Check whether this is a 3D plot
is3D = isAxis3D(meta.gca);
% Extract the data from the current line handle.
xData = get(handle, 'XData');
yData = get(handle, 'YData');
if is3D
zData = get(handle, 'ZData');
end
% Update the data indicated by id_update
xData(id_replace) = NaN(size(id_replace));
yData(id_replace) = NaN(size(id_replace));
if is3D
zData(id_replace) = NaN(size(id_replace));
end
% Set the new (masked) data.
set(handle, 'XData', xData);
set(handle, 'YData', yData);
if is3D
set(handle, 'ZData', zData);
end
end
% =========================================================================
function insertData(meta, handle, id_insert, dataInsert)
% Inserts the elements of the cell array dataInsert at position id_insert
% Only do something if id_insert is not empty
if isempty(id_insert)
return
end
% Check whether this is a 3D plot
is3D = isAxis3D(meta.gca);
% Extract the data from the current line handle.
xData = get(handle, 'XData');
yData = get(handle, 'YData');
if is3D
zData = get(handle, 'ZData');
end
length_segments = [id_insert(1);
diff(id_insert);
length(xData)-id_insert(end)];
% Put the data into one matrix
if is3D
data = [xData(:), yData(:), zData(:)];
else
data = [xData(:), yData(:)];
end
% Cut the data into segments
dataCell = mat2cell(data, length_segments, size(data, 2));
% Merge the cell arrays
dataCell = [dataCell';
dataInsert'];
% Merge the cells back together
data = cat(1, dataCell{:});
% Set the new (masked) data.
set(handle, 'XData', data(:, 1));
set(handle, 'YData', data(:, 2));
if is3D
set(handle, 'ZData', data(:, 3));
end
end
% =========================================================================
function removeData(meta, handle, id_remove)
% Removes the data at position id_remove
% Only do something if id_remove is not empty
if isempty(id_remove)
return
end
% Check whether this is a 3D plot
is3D = isAxis3D(meta.gca);
% Extract the data from the current line handle.
xData = get(handle, 'XData');
yData = get(handle, 'YData');
if is3D
zData = get(handle, 'ZData');
end
% Remove the data indicated by id_remove
xData(id_remove) = [];
yData(id_remove) = [];
if is3D
zData(id_remove) = [];
end
% Set the new data.
set(handle, 'XData', xData);
set(handle, 'YData', yData);
if is3D
set(handle, 'ZData', zData);
end
end
% =========================================================================
function removeNaNs(meta, handle)
% Removes superflous NaNs in the data, i.e. those at the end/beginning of
% the data and consequtive ones.
% Check whether this is a 3D plot
is3D = isAxis3D(meta.gca);
% Extract the data from the current line handle.
xData = get(handle, 'XData');
yData = get(handle, 'YData');
if is3D
zData = get(handle, 'ZData');
end
% Put the data into one matrix
if is3D
data = [xData(:), yData(:), zData(:)];
else
data = [xData(:), yData(:)];
end
% Remove consecutive NaNs
id_nan = any(isnan(data), 2);
id_remove = find(id_nan);
% If a NaN is preceeded by another NaN, then diff(id_remove)==1
id_remove = id_remove(diff(id_remove) == 1);
% Make sure that there are no NaNs at the beginning of the data since
% this would be interpreted as column names by Pgfplots.
% Also drop all NaNs at the end of the data
id_first = find(~id_nan, 1, 'first');
id_last = find(~id_nan, 1, 'last');
% If there are only NaN data points, remove the whole data
if isempty(id_first)
id_remove = 1:length(xData);
else
id_remove = [1:id_first-1, id_remove', id_last+1:length(xData)]';
end
% Remove the NaNs
data(id_remove,:) = [];
% Set the new data.
set(handle, 'XData', data(:, 1));
set(handle, 'YData', data(:, 2));
if is3D
set(handle, 'ZData', data(:, 3));
end
end
% ==========================================================================
function [bottomLeft, topLeft, bottomRight, topRight] = corners2D(xLim, yLim)
% Determine the corners of the axes as defined by xLim and yLim
bottomLeft = [xLim(1), yLim(1)];
topLeft = [xLim(1), yLim(2)];
bottomRight = [xLim(2), yLim(1)];
topRight = [xLim(2), yLim(2)];
end
% ==========================================================================
function corners = corners3D(xLim, yLim, zLim)
% Determine the corners of the 3D axes as defined by xLim, yLim, and
% zLim
% Lower square of the cube
lowerBottomLeft = [xLim(1), yLim(1), zLim(1)];
lowerTopLeft = [xLim(1), yLim(2), zLim(1)];
lowerBottomRight = [xLim(2), yLim(1), zLim(1)];
lowerTopRight = [xLim(2), yLim(2), zLim(1)];
% Upper square of the cube
upperBottomLeft = [xLim(1), yLim(1), zLim(2)];
upperTopLeft = [xLim(1), yLim(2), zLim(2)];
upperBottomRight = [xLim(2), yLim(1), zLim(2)];
upperTopRight = [xLim(2), yLim(2), zLim(2)];
% Put the into one matrix
corners = [lowerBottomLeft;
lowerTopLeft;
lowerBottomRight;
lowerTopRight;
upperBottomLeft;
upperTopLeft;
upperBottomRight;
upperTopRight];
end
% ==========================================================================
function P = getProjectionMatrix(meta)
% Calculate the projection matrix from a 3D plot into the image plane
% Get the projection angle
[az, el] = view(meta.gca);
% Convert from degrees to radians.
az = az*pi/180;
el = el*pi/180;
% The transformation into the image plane is done in a two-step process.
% First: rotate around the z-axis by -az (in radians)
rotationZ = [ cos(-az) -sin(-az) 0 0
sin(-az) cos(-az) 0 0
0 0 1 0
0 0 0 1];
% Second: rotate around the x-axis by (el - pi/2) radians.
% NOTE: There are some trigonometric simplifications, as we use
% (el-pi/2)
% cos(x - pi/2) = sin(x)
% sin(x - pi/2) = -cos(x)
rotationX = [ 1 0 0 0
0 sin(el) cos(el) 0
0 -cos(el) sin(el) 0
0 0 0 1];
% Get the data aspect ratio. This is necessary, as the axes usually do
% not have the same scale (xRange~=yRange)
aspectRatio = get(meta.gca, 'DataAspectRatio');
scaleMatrix = diag([1./aspectRatio, 1]);
% Calculate the projection matrix
P = rotationX * rotationZ * scaleMatrix;
end
% =========================================================================
function [W, H] = getWidthHeightInPixels(targetResolution)
% Retrieves target figure width and height in pixels
% TODO: If targetResolution is a scalar, W and H are determined
% differently on different environments (octave, local vs. Travis).
% It is unclear why, as this even happens, if `Units` and `Position`
% are matching. Could it be that the `set(gcf,'Units','Inches')` is not
% taken into consideration for `Position`, directly after setting it?
% targetResolution is PPI
if isscalar(targetResolution)
% Query figure size in inches and convert W and H to target pixels
oldunits = get(gcf,'Units');
set(gcf,'Units','Inches');
figSizeIn = get(gcf,'Position');
W = figSizeIn(3) * targetResolution;
H = figSizeIn(4) * targetResolution;
set(gcf,'Units', oldunits) % restore original unit
% It is already in the format we want
else
W = targetResolution(1);
H = targetResolution(2);
end
end
% =========================================================================
function bool = isValidTargetResolution(val)
bool = isnumeric(val) && ~any(isnan(val)) && (isscalar(val) || numel(val) == 2);
end
% =========================================================================
function bool = isValidAxis(val)
bool = length(val) <= 3;
for i=1:length(val)
bool = bool && ...
(strcmpi(val(i), 'x') || ...
strcmpi(val(i), 'y') || ...
strcmpi(val(i), 'z'));
end
end
% ========================================================================
function normalizeAxis(handle, cmdOpts)
% Normalizes data from a given axis into the interval [0, 1]
% Warn about normalizeAxis being experimental
warning('cleanfigure:normalizeAxis', ...
'Normalization of axis data is experimental!');
for axis = cmdOpts.normalizeAxis(:)'
% Get the scale needed to set xyz-lim to [0, 1]
dateLimits = get(handle, [upper(axis), 'Lim']);
dateScale = 1/diff(dateLimits);
% Set the TickLabelMode to manual to preserve the labels
set(handle, [upper(axis), 'TickLabelMode'], 'manual');
% Project the ticks
ticks = get(handle, [upper(axis), 'Tick']);
ticks = (ticks - dateLimits(1))*dateScale;
% Set the data
set(handle, [upper(axis), 'Tick'], ticks);
set(handle, [upper(axis), 'Lim'], [0, 1]);
% Traverse the children
children = get(handle, 'Children');
for child = children(:)'
if isprop(child, [upper(axis), 'Data'])
% Get the data and transform it
data = get(child, [upper(axis), 'Data']);
data = (data - dateLimits(1))*dateScale;
% Set the data again
set(child, [upper(axis), 'Data'], data);
end
end
end
end
% =========================================================================