├── .gitattributes ├── BaseZoom.m ├── LICENSE ├── README.md ├── manual.pdf └── parameters.json /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /BaseZoom.m: -------------------------------------------------------------------------------- 1 | classdef BaseZoom < handle 2 | %{ 3 | 4 | Interactive Magnification of Customized Regions. 5 | 6 | Email: iqiukp@outlook.com 7 | 8 | ------------------------------------------------------------- 9 | 10 | Version 1.5.1, 5-FEB-2024 11 | -- Support for zoom of a specified Axes object. 12 | -- Support setting the number of connection lines. 13 | -- Support for manual mode. 14 | -- Fixed minor bugs. 15 | 16 | Version 1.4, 30-MAY-2023 17 | -- Added support for charts with two y-axes. 18 | -- Customize parameters using json files. 19 | 20 | Version 1.3.1, 24-JAN-2022 21 | -- Fixed bugs when applied to logarithmic-scale coordinates. 22 | 23 | Version 1.3, 17-JAN-2022 24 | -- Fixed minor bugs. 25 | -- Added support for image class. 26 | 27 | Version 1.2, 4-OCT-2021 28 | -- Added support for interaction. 29 | 30 | Version 1.1, 1-SEP-2021 31 | -- Fixed minor bugs. 32 | -- Added description of parameters. 33 | 34 | Version 1.0, 10-JUN-2021 35 | -- Magnification of Customized Regions. 36 | 37 | ------------------------------------------------------------- 38 | 39 | BSD 3-Clause License 40 | Copyright (c) 2024, Kepeng Qiu 41 | All rights reserved. 42 | 43 | %} 44 | 45 | % main properties 46 | properties 47 | axesObject 48 | subAxesPosition = []; 49 | zoomAreaPosition = []; 50 | zoomMode = 'interaction'; 51 | subFigure 52 | mainAxes 53 | subAxes 54 | roi 55 | zoomedArea 56 | parameters 57 | XAxis 58 | YAxis 59 | direction 60 | % image-related properties 61 | uPixels 62 | vPixels 63 | vuRatio 64 | CData_ 65 | Colormap_ 66 | imageDim 67 | imagePosition = [0.1, 0.1, 0.8, 0.6] 68 | imageRectangleEdgePosition 69 | imageArrow 70 | % figure-related properties 71 | mappingParams 72 | figureRectangleEdgePosition 73 | lineDirection 74 | axesPosition 75 | figureArrow 76 | % others 77 | drawFunc 78 | axesClassName 79 | isAxesDrawn = 'off' 80 | isRectangleDrawn = 'off' 81 | pauseTime = 0.2 82 | textDisplay = 'on' 83 | end 84 | 85 | % dynamic properties 86 | properties(Dependent) 87 | XLimNew 88 | YLimNew 89 | affinePosition 90 | dynamicPosition 91 | newCData_ 92 | newCData 93 | newCMap 94 | end 95 | 96 | methods 97 | 98 | function this = BaseZoom(varargin) 99 | % Check MATLAB version compatibility 100 | if verLessThan('matlab', '9.5') % MATLAB 2018b version is 9.5 101 | error('BaseZoom:IncompatibleVersion', 'Please use BaseZoom version 1.4 or below for your MATLAB version.'); 102 | end 103 | 104 | switch nargin 105 | case 0 % No input arguments 106 | this.axesObject = gca; 107 | this.zoomMode = 'interaction'; 108 | this.textDisplay = 'on'; 109 | 110 | case 1 % One input argument 111 | this.axesObject = varargin{1}; 112 | if ~isa(this.axesObject, 'matlab.graphics.axis.Axes') 113 | error('Input must be an Axes object.'); 114 | end 115 | this.zoomMode = 'interaction'; 116 | this.textDisplay = 'on'; 117 | % Check if the object is an image or figure 118 | if ~isempty(imhandles(this.axesObject)) 119 | this.axesClassName = 'image'; 120 | else 121 | this.axesClassName = 'figure'; 122 | end 123 | case 2 % Two input arguments 124 | % Check if the first input argument is an Axes object or a position vector 125 | if isa(varargin{1}, 'matlab.graphics.axis.Axes') 126 | this.axesObject = varargin{1}; 127 | if ~isempty(imhandles(this.axesObject)) 128 | % The object is an image 129 | if ~all(isnumeric(varargin{2}) & numel(varargin{2}) == 4) 130 | error('The second input must be a numeric 4-element vector representing zoomAreaPosition.'); 131 | end 132 | this.zoomAreaPosition = varargin{2}; 133 | this.zoomMode = 'manual'; 134 | this.textDisplay = 'off'; 135 | else 136 | error('For two inputs, if the first input is an Axes object, it must be associated with an image.'); 137 | end 138 | elseif all(isnumeric(varargin{1}) & numel(varargin{1}) == 4) 139 | this.subAxesPosition = varargin{1}; 140 | this.zoomAreaPosition = varargin{2}; 141 | this.axesObject = gca; 142 | if ~isempty(imhandles(this.axesObject)) 143 | error('For two numeric inputs, the current Axes must not be associated with an image.'); 144 | end 145 | this.zoomMode = 'manual'; 146 | this.textDisplay = 'off'; 147 | else 148 | error('For two inputs, the first input must be either an Axes object associated with an image or a numeric 4-element vector representing subAxesPosition.'); 149 | end 150 | 151 | case 3 % Three input arguments 152 | this.axesObject = varargin{1}; 153 | this.subAxesPosition = varargin{2}; 154 | this.zoomAreaPosition = varargin{3}; 155 | if ~isa(this.axesObject, 'matlab.graphics.axis.Axes') 156 | error('The first input must be an Axes object.'); 157 | end 158 | if ~isempty(imhandles(this.axesObject)) 159 | error('For three inputs, the Axes object must not be associated with an image.'); 160 | end 161 | if ~(all(isnumeric(this.subAxesPosition) & numel(this.subAxesPosition) == 4) && ... 162 | all(isnumeric(this.zoomAreaPosition) & numel(this.zoomAreaPosition) == 4)) 163 | error('The second and third inputs must be numeric 4-element vectors representing subAxesPosition and zoomAreaPosition, respectively.'); 164 | end 165 | this.zoomMode = 'manual'; 166 | this.textDisplay = 'off'; 167 | 168 | otherwise 169 | error(['Invalid number of input arguments. ',... 170 | 'For two inputs, provide either subAxesPosition and zoomAreaPosition, or an Axes object and zoomAreaPosition. ',... 171 | 'For three inputs, provide an Axes object, subAxesPosition, and zoomAreaPosition.']); 172 | end 173 | this.initialize; 174 | this.loadParameters; 175 | end 176 | 177 | function run(this) 178 | % main steps 179 | switch this.axesClassName 180 | case 'image' 181 | this.addSubAxes; 182 | this.isAxesDrawn = 'off'; 183 | this.displayZoomInstructions(); 184 | this.addZoomedArea; 185 | this.isRectangleDrawn = 'off'; 186 | case 'figure' 187 | this.displaySubAxesInstructions(); 188 | this.addSubAxes; 189 | this.isAxesDrawn = 'off'; 190 | this.displayZoomInstructions(); 191 | this.addZoomedArea; 192 | this.isRectangleDrawn = 'off'; 193 | end 194 | end 195 | 196 | function loadParameters(this) 197 | fileName = 'parameters.json'; 198 | fid = fopen(fileName); 199 | raw = fread(fid,inf); 200 | str = char(raw'); 201 | fclose(fid); 202 | this.parameters = jsondecode(str); 203 | names_ = fieldnames(this.parameters); 204 | for i = 1:length(names_) 205 | if isfield(this.parameters.(names_{i}), 'Comments') 206 | this.parameters.(names_{i}) = rmfield(this.parameters.(names_{i}), 'Comments'); 207 | end 208 | end 209 | end 210 | 211 | function initialize(this) 212 | this.mainAxes = this.axesObject; 213 | if size(imhandles(this.mainAxes),1) ~= 0 214 | this.axesClassName = 'image'; 215 | this.CData_ = get(this.mainAxes.Children, 'CData'); 216 | this.Colormap_ = colormap(gca); 217 | if size(this.Colormap_, 1) == 64 218 | this.Colormap_ = colormap(gcf); 219 | end 220 | [this.vPixels, this.uPixels, ~] = size(this.CData_); 221 | this.vuRatio = this.vPixels/this.uPixels; 222 | this.imageDim = length(size(this.CData_)); 223 | else 224 | this.axesClassName = 'figure'; 225 | end 226 | 227 | if strcmp(this.axesClassName, 'figure') 228 | this.YAxis.direction = {'left', 'right'}; 229 | this.YAxis.number = length(this.mainAxes.YAxis); 230 | this.XAxis.number = length(this.mainAxes.XAxis); 231 | this.XAxis.scale = this.mainAxes.XScale; 232 | this.direction = this.mainAxes.YAxisLocation; 233 | switch this.YAxis.number 234 | case 1 235 | this.YAxis.(this.direction).scale = this.mainAxes.YScale; 236 | case 2 237 | for i = 1:2 238 | yyaxis(this.mainAxes, this.YAxis.direction{1, i}); 239 | this.YAxis.(this.YAxis.direction{1, i}).scale = this.mainAxes.YScale; 240 | this.YAxis.scale{i} = this.mainAxes.YScale; 241 | end 242 | this.YAxis.scale = cell2mat(this.YAxis.scale); 243 | yyaxis(this.mainAxes, this.direction); 244 | end 245 | end 246 | end 247 | 248 | function addSubAxes(this) 249 | switch this.axesClassName 250 | case 'image' 251 | this.subFigure = figure; 252 | this.imagePosition(4) = this.imagePosition(3)*this.vuRatio; 253 | set(this.subFigure, 'Units', 'Normalized', 'OuterPosition', this.imagePosition); 254 | subplot(1, 2, 1, 'Parent', this.subFigure); 255 | image(this.CData_); 256 | this.mainAxes = gca; 257 | if this.imageDim == 2 258 | colormap(this.mainAxes, this.Colormap_); 259 | end 260 | axis off 261 | subplot(1, 2, 2, 'Parent', this.subFigure); 262 | image((ones(this.vPixels, this.uPixels))); 263 | this.subAxes = gca; 264 | colormap(this.subAxes, [240, 240, 240]/255); 265 | axis off 266 | case 'figure' % 267 | if strcmp(this.zoomMode, 'interaction') 268 | this.roi = drawrectangle(this.mainAxes, 'Label', 'SubAxes'); 269 | this.setTheme; 270 | this.creatSubAxes; 271 | set(gcf, 'WindowButtonDownFcn', {@this.clickEvents, 'subAxes'}); 272 | addlistener(this.roi, 'MovingROI', @(source, event) ... 273 | this.allEvents(source, event, 'subAxes')); 274 | addlistener(this.roi, 'ROIMoved', @(source, event) ... 275 | this.allEvents(source, event, 'subAxes')); 276 | while strcmp(this.isAxesDrawn, 'off') 277 | pause(this.pauseTime); 278 | end 279 | else 280 | this.roi = drawrectangle(this.mainAxes,... 281 | 'Position', this.subAxesPosition, 'Label', 'SubAxes'); 282 | this.creatSubAxes; 283 | delete(this.roi); 284 | set(this.subAxes, 'Visible', 'on') 285 | this.isAxesDrawn = 'on'; 286 | end 287 | % this.subAxes.Color = this.mainAxes.Color; 288 | end 289 | end 290 | 291 | function addZoomedArea(this) 292 | switch this.axesClassName 293 | case 'image' 294 | if strcmp(this.zoomMode, 'interaction') 295 | this.roi = drawrectangle(this.mainAxes, 'Label', 'ZoomArea'); 296 | this.setTheme; 297 | this.creatSubAxes; 298 | if strcmp(this.parameters.subAxes.Box, 'on') 299 | this.connectAxesAndBox; 300 | end 301 | set(gcf, 'WindowButtonDownFcn', {@this.clickEvents, 'zoomedArea'}); 302 | addlistener(this.roi, 'MovingROI', @(source, event) ... 303 | this.allEvents(source, event, 'zoomedArea')); 304 | addlistener(this.roi, 'ROIMoved', @(source, event) ... 305 | this.allEvents(source, event, 'zoomedArea')); 306 | while strcmp(this.isRectangleDrawn, 'off') 307 | pause(this.pauseTime); 308 | end 309 | else 310 | 311 | this.roi = drawrectangle(this.mainAxes,... 312 | 'Position', this.zoomAreaPosition, 'Label', 'zoomArea'); 313 | this.setTheme; 314 | if strcmp(this.parameters.subAxes.Box, 'on') 315 | this.connectAxesAndBox; 316 | end 317 | this.creatSubAxes; 318 | this.isRectangleDrawn = 'on'; 319 | this.createRectangle; 320 | delete(this.roi); 321 | end 322 | for iArrow = 1:length(this.imageArrow) 323 | this.imageArrow{iArrow}.Tag = 'ZoomPlot'; 324 | end 325 | 326 | case 'figure' % 327 | if strcmp(this.zoomMode, 'interaction') 328 | this.roi = drawrectangle(this.mainAxes, 'Label', 'zoomArea'); 329 | this.setTheme; 330 | if strcmp(this.parameters.subAxes.Box, 'on') 331 | this.connectAxesAndBox; 332 | end 333 | this.setSubAxesLim; 334 | set(gcf, 'WindowButtonDownFcn', {@this.clickEvents, 'zoomedArea'}); 335 | addlistener(this.roi, 'MovingROI', @(source, event) ... 336 | this.allEvents(source, event, 'zoomedArea')); 337 | addlistener(this.roi, 'ROIMoved', @(source, event) ... 338 | this.allEvents(source, event, 'zoomedArea')); 339 | while strcmp(this.isRectangleDrawn, 'off') 340 | pause(this.pauseTime); 341 | end 342 | else 343 | this.roi = drawrectangle(this.mainAxes,... 344 | 'Position', this.zoomAreaPosition, 'Label', 'zoomArea'); 345 | this.setTheme; 346 | if strcmp(this.parameters.subAxes.Box, 'on') 347 | this.connectAxesAndBox; 348 | end 349 | this.setSubAxesLim; 350 | this.isRectangleDrawn = 'on'; 351 | this.createRectangle; 352 | delete(this.roi); 353 | end 354 | for iArrow = 1:length(this.figureArrow) 355 | this.figureArrow{iArrow}.Tag = 'ZoomPlot'; 356 | end 357 | end 358 | end 359 | 360 | function allEvents(this, ~, ~, mode) 361 | switch mode 362 | case 'subAxes' 363 | if strcmp(this.textDisplay, 'on') 364 | fprintf('adjust the sub axes...\n'); 365 | end 366 | delete(this.subAxes); 367 | this.creatSubAxes; 368 | this.subAxes.Color = this.parameters.subAxes.Color; 369 | case 'zoomedArea' 370 | if strcmp(this.textDisplay, 'on') 371 | fprintf('adjust the zoomed area...\n') 372 | end 373 | delete(findall(gcf, 'Tag', 'ZoomPlot_')) 374 | if strcmp(this.parameters.subAxes.Box, 'on') 375 | this.connectAxesAndBox; 376 | end 377 | switch this.axesClassName 378 | case 'image' % 379 | this.creatSubAxes; 380 | case 'figure' % 381 | this.setSubAxesLim; 382 | end 383 | end 384 | end 385 | 386 | function clickEvents(this, ~, ~, mode) 387 | switch mode 388 | case 'subAxes' 389 | switch get(gcf, 'SelectionType') 390 | case 'alt' 391 | this.isAxesDrawn = 'on'; 392 | set(this.subAxes, 'Visible', 'on'); 393 | set(gcf, 'WindowButtonDownFcn', []); 394 | if strcmp(this.textDisplay, 'on') 395 | fprintf('Complete the adjustment of the sub axes.\n\n'); 396 | end 397 | delete(this.roi); 398 | this.subAxes.Color = this.parameters.subAxes.Color; 399 | 400 | case 'normal' 401 | this.isAxesDrawn = 'off'; 402 | if strcmp(this.textDisplay, 'on') 403 | fprintf('Right-click to stop adjusting.\n'); 404 | end 405 | this.subAxes.Color = this.parameters.subAxes.Color; 406 | 407 | otherwise 408 | this.isAxesDrawn = 'off'; 409 | if strcmp(this.textDisplay, 'on') 410 | fprintf('Right-click to stop adjusting.\n'); 411 | end 412 | this.subAxes.Color = this.parameters.subAxes.Color; 413 | end 414 | 415 | case 'zoomedArea' 416 | switch get(gcf, 'SelectionType') 417 | case 'alt' 418 | this.isRectangleDrawn = 'on'; 419 | this.createRectangle; 420 | set(gcf, 'WindowButtonDownFcn', []); 421 | delete(this.roi); 422 | if strcmp(this.textDisplay, 'on') 423 | fprintf('Complete the adjustment of the zoomed area.\n\n'); 424 | end 425 | case 'normal' 426 | this.isRectangleDrawn = 'off'; 427 | if strcmp(this.textDisplay, 'on') 428 | fprintf('Right-click to stop adjusting.\n'); 429 | end 430 | otherwise 431 | this.isRectangleDrawn = 'off'; 432 | if strcmp(this.textDisplay, 'on') 433 | fprintf('Right-click to stop adjusting.\n'); 434 | end 435 | end 436 | end 437 | end 438 | 439 | function creatSubAxes(this) 440 | switch this.axesClassName 441 | case 'image' 442 | set(this.subAxes.Children, 'CData', this.newCData); 443 | if this.imageDim == 2 444 | colormap(this.subAxes, this.newCMap); 445 | end 446 | case 'figure' 447 | if this.YAxis.number == 1 448 | this.subAxes = axes('Position', this.affinePosition,... 449 | 'XScale', this.XAxis.scale,... 450 | 'YScale', this.YAxis.(this.direction).scale,... 451 | 'parent', get(this.mainAxes, 'Parent')); 452 | mainChildren = this.getMainChildren; 453 | copyobj(mainChildren, this.subAxes); 454 | this.subAxes.XLim = this.mainAxes.XLim; 455 | hold(this.subAxes, 'on'); 456 | set(this.subAxes, this.parameters.subAxes); 457 | set(this.subAxes, 'Visible', 'off'); 458 | end 459 | if this.YAxis.number == 2 460 | diret_ = this.YAxis.direction; 461 | this.subAxes = axes('Position', this.affinePosition, 'parent', get(this.mainAxes, 'Parent')); 462 | for i = 1:2 463 | yyaxis(this.subAxes, diret_{i}); 464 | yyaxis(this.mainAxes, diret_{i}); 465 | set(this.subAxes, 'XScale', this.mainAxes.XScale,... 466 | 'YScale', this.mainAxes.YScale) 467 | mainChildren = this.getMainChildren; 468 | copyobj(mainChildren, this.subAxes); 469 | this.subAxes.XLim = this.mainAxes.XLim; 470 | YLim.(diret_{i}) = this.subAxes.YLim; 471 | end 472 | yyaxis(this.mainAxes, this.direction); 473 | switch this.YAxis.scale 474 | case 'linearlinear' 475 | Y_from = YLim.(this.direction); 476 | Y_to = YLim.(cell2mat(setdiff(diret_, this.direction))); 477 | case 'linearlog' 478 | Y_from = YLim.(this.direction); 479 | Y_to = log10(YLim.(cell2mat(setdiff(diret_, this.direction)))); 480 | case 'loglinear' 481 | Y_from = log10(YLim.(this.direction)); 482 | Y_to = YLim.(cell2mat(setdiff(diret_, this.direction))); 483 | case 'loglog' 484 | Y_from = log10(YLim.(this.direction)); 485 | Y_to = log10(YLim.(cell2mat(setdiff(diret_, this.direction)))); 486 | end 487 | this.YAxis.K = (Y_to(2)-Y_to(1))/(Y_from(2)-Y_from(1)); 488 | this.YAxis.b = Y_to(1)-Y_from(1)*this.YAxis.K; 489 | hold(this.subAxes, 'on'); 490 | set(this.subAxes, this.parameters.subAxes); 491 | set(this.subAxes, 'Visible', 'off'); 492 | end 493 | end 494 | end 495 | 496 | function createRectangle(this) 497 | % Determine rectangle position based on axes class 498 | switch this.axesClassName 499 | case 'image' 500 | position = this.imageRectangleEdgePosition; 501 | case 'figure' 502 | position = this.affinePosition; 503 | end 504 | 505 | % Create the rectangle annotation with common properties 506 | this.zoomedArea = annotation('rectangle', position, ... 507 | 'Color', this.parameters.zoomedArea.Color, ... 508 | 'FaceColor', this.parameters.zoomedArea.FaceColor, ... 509 | 'FaceAlpha', this.parameters.zoomedArea.FaceAlpha, ... 510 | 'LineStyle', this.parameters.zoomedArea.LineStyle, ... 511 | 'LineWidth', this.parameters.zoomedArea.LineWidth); 512 | end 513 | 514 | function mappingParams = computeMappingParams(this) 515 | % Compute the mapping parameters for both axes 516 | [map_k_x, map_b_x] = this.computeAxisMappingParams(this.XAxis.scale, ... 517 | this.mainAxes.XLim, ... 518 | this.mainAxes.Position(1), ... 519 | this.mainAxes.Position(3)); 520 | [map_k_y, map_b_y] = this.computeAxisMappingParams(this.YAxis.(this.direction).scale, ... 521 | this.mainAxes.YLim, ... 522 | this.mainAxes.Position(2), ... 523 | this.mainAxes.Position(4)); 524 | % Construct the mapping parameters matrix 525 | mappingParams = [map_k_x, map_b_x; map_k_y, map_b_y]; 526 | end 527 | 528 | function [map_k, map_b] = computeAxisMappingParams(~, scale, axesLim, pos, size) 529 | % Compute mapping parameters based on the scale (linear or log) 530 | switch scale 531 | case 'linear' 532 | rangeLim = axesLim(2) - axesLim(1); 533 | case 'log' 534 | rangeLim = log10(axesLim(2)) - log10(axesLim(1)); 535 | otherwise 536 | error('BaseZoom:InvalidScale', 'Unsupported axis scale.'); 537 | end 538 | % Compute the scale factor and offset for mapping 539 | map_k = rangeLim / size; 540 | switch scale 541 | case 'linear' 542 | map_b = axesLim(1) - pos * map_k; 543 | case 'log' 544 | map_b = log10(axesLim(1)) - pos * map_k; 545 | end 546 | end 547 | 548 | function connectAxesAndBox(this) 549 | % insert lines between the inserted axes and rectangle 550 | 551 | % Rectangle subAxes 552 | % 2----1 2----1 553 | % 3----4 3----4 554 | switch this.axesClassName 555 | case 'image' % 556 | uPixelsAll = this.uPixels/this.mainAxes.Position(3); 557 | vPixelsAll = this.vPixels/this.mainAxes.Position(4); 558 | Position_ = this.roi.Position; 559 | this.imageRectangleEdgePosition(1) = Position_(1)/uPixelsAll+this.mainAxes.Position(1); 560 | this.imageRectangleEdgePosition(2) = (this.vPixels-Position_(2)-Position_(4))/... 561 | vPixelsAll+this.subAxes.Position(2); 562 | this.imageRectangleEdgePosition(3) = Position_(3)/uPixelsAll; 563 | this.imageRectangleEdgePosition(4) = Position_(4)/vPixelsAll; 564 | % annotation position 1 565 | annotationPosX_1(1) = this.imageRectangleEdgePosition(1)+this.imageRectangleEdgePosition(3); 566 | annotationPosX_1(2) = this.subAxes.Position(1); 567 | annotationPosY_1(1) = this.imageRectangleEdgePosition(2); 568 | annotationPosY_1(2) = this.subAxes.Position(2); 569 | this.imageArrow{1} = annotation(gcf, 'doublearrow',... 570 | annotationPosX_1, annotationPosY_1,... 571 | 'Color', this.parameters.connection.LineColor,... 572 | 'LineWidth', this.parameters.connection.LineWidth,... 573 | 'LineStyle', this.parameters.connection.LineStyle,... 574 | 'Head1Style', this.parameters.connection.StartHeadStyle,... 575 | 'Head1Length', this.parameters.connection.StartHeadLength,... 576 | 'Head1Width', this.parameters.connection.StartHeadWidth,... 577 | 'Head2Style', this.parameters.connection.EndHeadStyle,... 578 | 'Head2Length', this.parameters.connection.EndHeadLength,... 579 | 'Head2Width', this.parameters.connection.EndHeadWidth,... 580 | 'Tag', 'ZoomPlot_'); 581 | % annotation position 2 582 | annotationPosX_2(1) = this.imageRectangleEdgePosition(1)+this.imageRectangleEdgePosition(3); 583 | annotationPosX_2(2) = this.subAxes.Position(1); 584 | annotationPosY_2(1) = this.imageRectangleEdgePosition(2)+this.imageRectangleEdgePosition(4); 585 | annotationPosY_2(2) = this.subAxes.Position(2)+this.subAxes.Position(4); 586 | this.imageArrow{2} = annotation(gcf, 'doublearrow',... 587 | annotationPosX_2, annotationPosY_2,... 588 | 'Color', this.parameters.connection.LineColor,... 589 | 'LineWidth', this.parameters.connection.LineWidth,... 590 | 'LineStyle', this.parameters.connection.LineStyle,... 591 | 'Head1Style', this.parameters.connection.StartHeadStyle,... 592 | 'Head1Length', this.parameters.connection.StartHeadLength,... 593 | 'Head1Width', this.parameters.connection.StartHeadWidth,... 594 | 'Head2Style', this.parameters.connection.EndHeadStyle,... 595 | 'Head2Length', this.parameters.connection.EndHeadLength,... 596 | 'Head2Width', this.parameters.connection.EndHeadWidth,... 597 | 'Tag', 'ZoomPlot_'); 598 | case 'figure' 599 | % real coordinates of the inserted rectangle and axes 600 | this.getAxesAndBoxPosition; 601 | % get the line direction 602 | this.getLineDirection; 603 | % insert lines 604 | % numLine = size(this.lineDirection, 1); 605 | switch this.parameters.connection.LineNumber 606 | case 0 607 | 608 | case 1 609 | lineDirection_ = this.lineDirection(end, :); 610 | case 2 611 | lineDirection_ = this.lineDirection(1:2, :); 612 | otherwise 613 | error('The LineNumber must be 0 or 1 or 2.') 614 | end 615 | 616 | for i = 1:this.parameters.connection.LineNumber 617 | tmp1 = [this.figureRectangleEdgePosition(lineDirection_(i, 1), 1),... 618 | this.figureRectangleEdgePosition(lineDirection_(i, 1), 2)]; 619 | tmp2 = [this.axesPosition(lineDirection_(i, 2), 1),... 620 | this.axesPosition(lineDirection_(i, 2), 2)]; 621 | pos1 = this.transformCoordinate(tmp1, 'a2n'); 622 | pos2 = this.transformCoordinate(tmp2, 'a2n'); 623 | this.figureArrow{i} = annotation(gcf, 'doublearrow',... 624 | [pos1(1, 1), pos2(1, 1)], [pos1(1, 2), pos2(1, 2)],... 625 | 'Color', this.parameters.connection.LineColor,... 626 | 'LineWidth', this.parameters.connection.LineWidth,... 627 | 'LineStyle', this.parameters.connection.LineStyle,... 628 | 'Head1Style', this.parameters.connection.StartHeadStyle,... 629 | 'Head1Length', this.parameters.connection.StartHeadLength,... 630 | 'Head1Width', this.parameters.connection.StartHeadWidth,... 631 | 'Head2Style', this.parameters.connection.EndHeadStyle,... 632 | 'Head2Length', this.parameters.connection.EndHeadLength,... 633 | 'Head2Width', this.parameters.connection.EndHeadWidth,... 634 | 'Tag', 'ZoomPlot_'); 635 | end 636 | end 637 | end 638 | 639 | function getAxesAndBoxPosition(this) 640 | % real coordinates of the inserted rectangle 641 | box1_1 = [this.XLimNew(1, 2), this.YLimNew(1, 2)]; 642 | box1_2 = [this.XLimNew(1, 1), this.YLimNew(1, 2)]; 643 | box1_3 = [this.XLimNew(1, 1), this.YLimNew(1, 1)]; 644 | box1_4 = [this.XLimNew(1, 2), this.YLimNew(1, 1)]; 645 | box1 = [box1_1; box1_2; box1_3; box1_4]; 646 | % real coordinates of the inserted axes 647 | tmp1 = [this.subAxes.Position(1)+this.subAxes.Position(3),... 648 | this.subAxes.Position(2)+this.subAxes.Position(4)]; 649 | box2_1 = this.transformCoordinate(tmp1, 'n2a'); 650 | tmp2 = [this.subAxes.Position(1),... 651 | this.subAxes.Position(2)+this.subAxes.Position(4)]; 652 | box2_2 = this.transformCoordinate(tmp2, 'n2a'); 653 | tmp3 = [this.subAxes.Position(1), this.subAxes.Position(2)]; 654 | box2_3 = this.transformCoordinate(tmp3, 'n2a'); 655 | tmp4 = [this.subAxes.Position(1)+this.subAxes.Position(3),... 656 | this.subAxes.Position(2)]; 657 | box2_4 = this.transformCoordinate(tmp4, 'n2a'); 658 | box2 = [box2_1; box2_2; box2_3; box2_4]; 659 | this.figureRectangleEdgePosition = box1; 660 | this.axesPosition = box2; 661 | end 662 | 663 | function getLineDirection(this) 664 | % get the line direction 665 | % left-upper 666 | if (this.figureRectangleEdgePosition(4, 1) < this.axesPosition(1, 1) &&... 667 | this.figureRectangleEdgePosition(4, 2) > this.axesPosition(2, 2)) 668 | this.lineDirection = [3, 3; 1, 1; 4, 2]; 669 | end 670 | % middle-upper 671 | if (this.figureRectangleEdgePosition(4, 1) > this.axesPosition(2, 1) &&... 672 | this.figureRectangleEdgePosition(4, 2) > this.axesPosition(2, 2)) &&... 673 | this.figureRectangleEdgePosition(3, 1) < this.axesPosition(1, 1) 674 | this.lineDirection = [4, 1; 3, 2; 4 ,1]; 675 | end 676 | % right-upper 677 | if (this.figureRectangleEdgePosition(3, 1) > this.axesPosition(1, 1) &&... 678 | this.figureRectangleEdgePosition(3, 2) > this.axesPosition(1, 2)) 679 | this.lineDirection = [2, 2; 4, 4; 3, 1]; 680 | end 681 | % right-middle 682 | if (this.figureRectangleEdgePosition(3, 1) > this.axesPosition(1, 1) &&... 683 | this.figureRectangleEdgePosition(3, 2) < this.axesPosition(1, 2)) &&... 684 | this.figureRectangleEdgePosition(2, 2) > this.axesPosition(4, 2) 685 | this.lineDirection = [2, 1; 3, 4; 3, 1]; 686 | end 687 | % right-down 688 | if (this.figureRectangleEdgePosition(2, 1) > this.axesPosition(4, 1) &&... 689 | this.figureRectangleEdgePosition(2, 2) < this.axesPosition(4, 2)) 690 | this.lineDirection = [1, 1; 3, 3; 4, 2]; 691 | end 692 | % down-middle 693 | if (this.figureRectangleEdgePosition(1, 1) > this.axesPosition(3, 1) &&... 694 | this.figureRectangleEdgePosition(1, 2) < this.axesPosition(3, 2) &&... 695 | this.figureRectangleEdgePosition(2, 1) < this.axesPosition(4, 1)) 696 | this.lineDirection = [2, 3; 1, 4; 2, 4]; 697 | end 698 | % left-down 699 | if (this.figureRectangleEdgePosition(1, 1) < this.axesPosition(3, 1) &&... 700 | this.figureRectangleEdgePosition(1, 2) < this.axesPosition(3, 2)) 701 | this.lineDirection = [2, 2; 4, 4; 3, 1]; 702 | end 703 | % left-middle 704 | if (this.figureRectangleEdgePosition(4, 1) this.axesPosition(3, 2) 707 | this.lineDirection = [1, 2; 4, 3; 1, 3]; 708 | end 709 | end 710 | 711 | function setSubAxesLim(this) 712 | switch this.YAxis.number 713 | case 1 714 | set(this.subAxes, 'XLim', this.XLimNew, 'YLim', this.YLimNew); 715 | case 2 716 | yyaxis(this.subAxes, this.direction); 717 | set(this.subAxes, 'XLim', this.XLimNew, 'YLim', this.YLimNew); 718 | yyaxis(this.subAxes, 'left'); 719 | switch this.YAxis.scale 720 | case 'linearlinear' 721 | Y_from = this.YLimNew; 722 | Y_to(1) = Y_from(1)*this.YAxis.K+this.YAxis.b; 723 | Y_to(2) = Y_from(2)*this.YAxis.K+this.YAxis.b; 724 | case 'linearlog' 725 | Y_from = this.YLimNew; 726 | Y_to(1) = 10.^(Y_from(1)*this.YAxis.K+this.YAxis.b); 727 | Y_to(2) = 10.^(Y_from(2)*this.YAxis.K+this.YAxis.b); 728 | case 'loglinear' 729 | Y_from = log10(this.YLimNew); 730 | Y_to(1) = Y_from(1)*this.YAxis.K+this.YAxis.b; 731 | Y_to(2) = Y_from(2)*this.YAxis.K+this.YAxis.b; 732 | case 'loglog' 733 | Y_from = log10(this.YLimNew); 734 | Y_to(1) = 10.^(Y_from(1)*this.YAxis.K+this.YAxis.b); 735 | Y_to(2) = 10.^(Y_from(2)*this.YAxis.K+this.YAxis.b); 736 | end 737 | set(this.subAxes, 'XLim', this.XLimNew,'YLim', Y_to); 738 | end 739 | end 740 | 741 | function mainChildren = getMainChildren(this) 742 | children_ = get(this.mainAxes, 'children'); 743 | numChildren_ = 1:length(children_); 744 | for ii = 1:length(children_) 745 | if strcmp(children_(ii, 1).Type, 'images.roi.rectangle') ||... 746 | strcmp(children_(ii, 1).Type, 'hggroup') 747 | numChildren_(ii) = []; 748 | end 749 | end 750 | mainChildren = children_(numChildren_); 751 | end 752 | 753 | function setTheme(this) 754 | % set the theme of the dynamic rectangle 755 | try 756 | this.roi.MarkerSize = this.parameters.dynamicRect.MarkerSize; 757 | catch 758 | end 759 | this.roi.Color = this.parameters.dynamicRect.FaceColor; 760 | this.roi.FaceAlpha = this.parameters.dynamicRect.FaceAspect; 761 | this.roi.LineWidth = this.parameters.dynamicRect.LineWidth; 762 | end 763 | 764 | function coordinate = transformCoordinate(this, coordinate, type) 765 | % coordinate transformation 766 | switch type 767 | % absolute coordinates to normalized coordinates 768 | case 'a2n' 769 | switch this.XAxis.scale 770 | case 'linear' 771 | coordinate(1, 1) = (coordinate(1, 1)-this.mappingParams(1, 2))... 772 | /this.mappingParams(1, 1); 773 | case 'log' 774 | coordinate(1, 1) = (log10(coordinate(1, 1))-this.mappingParams(1, 2))... 775 | /this.mappingParams(1, 1); 776 | end 777 | 778 | switch this.YAxis.(this.direction).scale 779 | case 'linear' 780 | coordinate(1, 2) = (coordinate(1, 2)-this.mappingParams(2, 2))... 781 | /this.mappingParams(2, 1); 782 | case 'log' 783 | coordinate(1, 2) = (log10(coordinate(1, 2))-this.mappingParams(2, 2))... 784 | /this.mappingParams(2, 1); 785 | end 786 | % normalized coordinates to absolute coordinates 787 | case 'n2a' 788 | switch this.XAxis.scale 789 | case 'linear' 790 | coordinate(1, 1) = coordinate(1, 1)*this.mappingParams(1, 1)... 791 | +this.mappingParams(1, 2); 792 | case 'log' 793 | coordinate(1, 1) = 10^(coordinate(1, 1)*this.mappingParams(1, 1)... 794 | +this.mappingParams(1, 2)); 795 | end 796 | switch this.YAxis.(this.direction).scale 797 | case 'linear' 798 | coordinate(1, 2) = coordinate(1, 2)*this.mappingParams(2, 1)... 799 | +this.mappingParams(2, 2); 800 | case 'log' 801 | coordinate(1, 2) = 10^(coordinate(1, 2)*this.mappingParams(2, 1)... 802 | +this.mappingParams(2, 2)); 803 | end 804 | end 805 | end 806 | 807 | function throwError(~, message) 808 | error('BaseZoom:InvalidInput', message); 809 | end 810 | 811 | function displaySubAxesInstructions(this) 812 | if strcmp(this.textDisplay, 'on') 813 | fprintf('Use the left mouse button to draw a rectangle.\n'); 814 | fprintf('for the sub axes...\n'); 815 | end 816 | end 817 | 818 | function displayZoomInstructions(this) 819 | if strcmp(this.textDisplay, 'on') 820 | fprintf('Use the left mouse button to draw a rectangle.\n'); 821 | fprintf('for the zoomed area...\n'); 822 | end 823 | end 824 | 825 | % dependent properties 826 | function dynamicPosition = get.dynamicPosition(this) 827 | dynamicPosition = this.roi.Position; 828 | end 829 | 830 | % dependent properties 831 | function XLimNew = get.XLimNew(this) 832 | XLimNew = [this.dynamicPosition(1), this.dynamicPosition(1)+this.dynamicPosition(3)]; 833 | end 834 | 835 | % dependent properties 836 | function YLimNew = get.YLimNew(this) 837 | YLimNew = [this.dynamicPosition(2), this.dynamicPosition(2)+this.dynamicPosition(4)]; 838 | end 839 | 840 | % dependent properties 841 | function affinePosition = get.affinePosition(this) 842 | this.mappingParams = this.computeMappingParams; 843 | tmp1 = this.transformCoordinate([this.XLimNew(1, 1), this.YLimNew(1, 1)], 'a2n'); 844 | tmp2 = this.transformCoordinate([this.XLimNew(1, 2), this.YLimNew(1, 2)], 'a2n'); 845 | affinePosition(1, 1) = tmp1(1, 1); 846 | affinePosition(1, 2) = tmp1(1, 2); 847 | affinePosition(1, 3) = tmp2(1, 1)-tmp1(1, 1); 848 | affinePosition(1, 4) = tmp2(1, 2)-tmp1(1, 2); 849 | end 850 | 851 | % dependent properties 852 | function newCData_ = get.newCData_(this) 853 | newCData_ = imcrop(this.CData_,this.Colormap_, this.roi.Position); 854 | end 855 | 856 | % dependent properties 857 | function newCData = get.newCData(this) 858 | switch this.imageDim 859 | case 2 860 | [newCData, ~] = imresize(this.newCData_, this.Colormap_, [this.vPixels, this.uPixels]); 861 | % [~, newCMap] = imresize(this.newCData_, this.newCMap_, [this.vPixels, this.uPixels]); 862 | case 3 863 | newCData = imresize(this.newCData_, [this.vPixels, this.uPixels]); 864 | end 865 | end 866 | 867 | % dependent properties 868 | function newCMap = get.newCMap(this) 869 | switch this.imageDim 870 | case 2 871 | [~, newCMap] = imresize(this.newCData_, this.Colormap_, [this.vPixels, this.uPixels]); 872 | case 3 873 | newCMap=[]; 874 | end 875 | end 876 | end 877 | end -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, Kepeng Qiu 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

ZoomPlot

6 | 7 |

MATLAB Code for Interactive Magnification of Customized Regions.

8 |

Version 1.5.1, 5-FEB-2024

9 |

Email: iqiukp@outlook.com

10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 |
22 | 23 | ## ✨ Main features 24 | 25 | - Easy application with just two lines of code 26 | - Interactive plotting 27 | - Support for image and figure classes 28 | - Support for multiple zoomed zones 29 | - Custom settings of parameters and themes 30 | 31 | ## ⚠️ Requirements 32 | 33 | - R201Bb and later releases 34 | - Image Processing Toolbox 35 | 36 | ## 👉 How to use 37 | 38 | 1. Add `BaseZoom.m` and `parameters.json` to MATLAB search path or current working directory 39 | 2. After completing the basic drawing, enter the following two lines of code in the command line window or your m-file: 40 | ```MATLAB 41 | % add a zoomed zone 42 | zp = BaseZoom(); 43 | zp.run; 44 | ``` 45 | 46 | *if multiple zoomed zones are required, for example, 3 zoomed zones, the code is as follows:* 47 | ```MATLAB 48 | % add 3 zoomed zones 49 | zp = BaseZoom(); 50 | zp.run; 51 | zp.run; 52 | zp.run; 53 | ``` 54 | ⚠️⚠️⚠️ For More details please see the `manual.pdf`. 55 | 56 | ## ✨ About `manual.pdf`: 57 | The `manual.pdf` file is the official user manual for the ZoomPlot MATLAB code. It provides users with detailed instructions on using the code, including the syntax, descriptions, example code, and requirements to run the ZoomPlot for interactive magnification of plots and images within MATLAB. 58 | 59 | - Introduction 60 | - Syntax 61 | - Description 62 | - Files 63 | - Requirements 64 | - Preparations 65 | - Examples 66 | - Interactive Local Magnification for Figure Class 67 | - Implement Multiple Local Magnifications for Figure Class 68 | - Specify Axes for Local Magnification for Figure Class 69 | - Manually Set SubAxes and Zoom Area for Figure Class 70 | - Manually ZoomPlot in Sub Plots for Figure Class 71 | - Interactive Local Magnification for Image Class 72 | - Manually Set Zoom Area for Image Class 73 | - Parameter Configuration 74 | - Sub Axes Theme 75 | - Zoomed Area Theme 76 | - Dynamic Rectangle Theme 77 | - Connection Lines Theme 78 | - More Parameter Configuration 79 | 80 | ## 👉 Examples for image class 81 | 82 | Multiple types of image are supported for interactive magnification of customized regions in the `ZoomPlot`. 83 |

84 | 85 |

86 | 87 | ## 👉 Examples for figure class 88 | 89 | Multiple zoomed zones are supported for figure class. 90 |

91 | 92 |

93 | 94 | ## Star History 95 | 96 | [![Star History Chart](https://api.star-history.com/svg?repos=iqiukp/ZoomPlot-MATLAB)](https://star-history.com/#iqiukp/ZoomPlot-MATLAB) 97 | -------------------------------------------------------------------------------- /manual.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iqiukp/ZoomPlot-MATLAB/26e66cc960a0d024083b82ce2066978a95d4b701/manual.pdf -------------------------------------------------------------------------------- /parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "subAxes": 3 | { 4 | "Color": "none", 5 | "LineWidth": 1.2, 6 | "XGrid": "off", 7 | "YGrid": "off", 8 | "ZGrid": "off", 9 | "GridAlpha": 0.15, 10 | "GridColor": [0.15, 0.15, 0.15], 11 | "GridLineStyle": "-", 12 | "Box": "on", 13 | "TickDir": "in", 14 | "Comments": "theme of the sub axes" 15 | }, 16 | 17 | "zoomedArea": 18 | { 19 | "Color": "k", 20 | "FaceColor": "none", 21 | "FaceAlpha": 0, 22 | "LineStyle": "-", 23 | "LineWidth": 1.5, 24 | "Comments": "theme of the zoomed area" 25 | }, 26 | 27 | "dynamicRect": 28 | { 29 | "LineColor": [0, 0.4471, 0.7412], 30 | "LineWidth": 2, 31 | "MarkerSize": 9, 32 | "FaceColor": [0, 0.4471, 0.7412], 33 | "FaceAspect": 0.3, 34 | "EdgeColor": "k", 35 | "Comments": "theme of the dynamic rectangle" 36 | }, 37 | 38 | "connection": 39 | { 40 | "LineNumber": 2, 41 | "LineColor": "k", 42 | "LineWidth": 1.5, 43 | "LineStyle": ":", 44 | "StartHeadStyle": "ellipse", 45 | "StartHeadLength": 3, 46 | "StartHeadWidth": 3, 47 | "EndHeadStyle": "cback2", 48 | "EndHeadLength": 7, 49 | "EndHeadWidth": 7, 50 | "Comments": "theme of the connected lines" 51 | } 52 | 53 | } 54 | 55 | --------------------------------------------------------------------------------