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
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 |
|
% =========================================================================
|
|
|