Database tüddelei DBHanlder läuft gut für DIESE Datanbank struktur... App begonnen aber weit entfernt von gutem Stand
1295 lines
46 KiB
Matlab
1295 lines
46 KiB
Matlab
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
|
|
% =========================================================================
|