Drawing only in Spy magnifying glass

spytikz-pgf

I would like to be able to draw something into the spy magnifying glass without drawing it in the main picture (e.g. to add specific annotations into only the zoomed version of the picture).

Why this is not a duplicate?

Related topics:

What I want to achieve

Ideally I would like to indicate something on a zoomed-in portion of a plot like this:

enter image description here

I manage to produce this plot with the following MWE (inspired by this answer):

\documentclass{standalone}
\usepackage{pgfplots}
\pgfplotsset{width=10cm, height=5cm, compat=1.18}
\usepackage{tikz}
\usetikzlibrary{arrows.meta}
\usetikzlibrary{spy}

% Store magnification and lens size in macros
\newcommand\magn{2}
\newcommand\msize{3cm}

\pgfdeclarelayer{fg} % declare foreground layer
\pgfsetlayers{main, fg}
\def\pival{3.14159265359}

\begin{document}
    \begin{tikzpicture}[%
        spy using outlines={circle, magnification=\magn, size=\msize, connect spies}
        ]
        \begin{axis}[domain=0:2*\pival, ymin=0, ymax=1.2, xmin = 0, xmax=2*\pival, samples=17]
            \addplot {sin(x*180/\pival)};
            \coordinate (spyonto) at (axis cs:\pival/2,0.9);
            \coordinate (magloc) at (axis cs:5,0.6);
            \spy on (spyonto) in node at (magloc) [fill=white];
            \coordinate (arrowstart) at (axis cs:4.3,0.5);
            \coordinate (arrowend) at (axis cs:5.7, 0.5);
        \end{axis}
        \begin{pgfonlayer}{fg}
            \draw[Stealth-Stealth, red, thick] (arrowstart) -- (arrowend);
        \end{pgfonlayer}
    \end{tikzpicture}
\end{document}

However, this is not satisfying for the following reasons:

  • The arrow has to be drawn outside the axis environment, which means that if we want to use axis coordinates, we have to declare them beforehand as arrowstart and arrowend.
  • The arrow has to be drawn inside a dedicated layer, which is not practical.
  • The arrow coordinates have to be guessed / estimated given loads of parameters (magnification ratio and lens size, original plot coordinates, magnifying glass location)

Also, keep in mind that if these issues may look like a minor burden in this precise case, it is because it is an oversimplified example of a much complex figure which involves several such annotations.

The other solutions that I explored

Using the approach presented here (pay attention to Xiao Hu's comment), I managed to get this result:

enter image description here

Using the following code:

\documentclass{standalone}
\usepackage{pgfplots}
\pgfplotsset{width=10cm, height=5cm, compat=1.18}
\usepackage{tikz}
\usetikzlibrary{arrows.meta}
\usetikzlibrary{spy, calc}

% Store magnification and lens size in macros
\newcommand\magn{2}
\newcommand\msize{3cm}

\pgfdeclarelayer{fg} % declare foreground layer
\pgfsetlayers{main, fg}
\def\pival{3.14159265359}

\tikzstyle{only in spy node}=[%
transform canvas={%
    shift={($-\magn*(spyonto)+(magloc)$)},
    scale=\magn,
}
]

\begin{document}
    \begin{tikzpicture}[%
        spy using outlines={circle, magnification=\magn, size=\msize, connect spies}
        ]
        \begin{axis}[domain=0:2*\pival, ymin=0, ymax=1.2, xmin = 0, xmax=2*\pival, samples=17]
            \begin{scope}
                \addplot {sin(x*180/\pival)};
                \coordinate (spyonto) at (axis cs:\pival/2,0.9);
                \coordinate (magloc) at (axis cs:5,0.6);
                \spy on (spyonto) in node at (magloc);% [fill=white];
                \coordinate (arrowstart) at (axis cs:4.3,0.5);
                \coordinate (arrowend) at (axis cs:5.7, 0.5);
            \end{scope}
            \begin{scope}[only in spy node]
                % clip this scope to keep it inside the magnifier
                \clip (spyonto) circle ({(\msize/2-0.4pt)/\magn});
                \fill [green] (spyonto) circle (12pt);
                \fill [red] (spyonto) circle (2pt);
                \draw[Stealth-Stealth, red, thick] (1.2, 0.95) -- (2, 0.95);
            \end{scope}
        \end{axis}
    \end{tikzpicture}
\end{document}

This is somewhat better since:

  • We can now use the native axis coordinate to plot our annotations (which is convenient if we have exact data points that we want to indicate accurately)
  • We no longer use separate layers

However, this is not fully working since the annotations are now behind the main picture. This is the reason why [fill=white]; is commented out, all annotations disappears behind it if you uncomment it. I tried to put them in a foreground layer by using:

\begin{pgfonlayer}{fg}
    \clip (spyonto) circle ({(\msize/2-0.4pt)/\magn});
    \fill [green] (spyonto) circle (12pt);
    \fill [red] (spyonto) circle (2pt);
    \draw[Stealth-Stealth, red, thick] (1.2, 0.95) -- (2, 0.95);
\end{pgfonlayer}

But if I do so they also appear in the miniature picture:

enter image description here

Which is not the desired behaviour.

I also had a look there but since we want to draw something inside the magnifying glass, we do not want to exit its spy scope.

If you have an approach which solves this issue, it would be more than welcome! ๐Ÿ™‚

Best Answer

The goal of this solution is

  • to not have to deal with the spy lens options,
  • to not use a canvas transformation for the annotations,
  • to allow using the PGFPlots coordinate system and
  • to be able to draw behind and before the magnified picture.

For this,

  • \tikz@lib@spy@do (which is the main macro that does all the things) is patched to find the location of the spy-in node before it is actually put on the page,
  • the macro \tikz@lib@spy@transformation is added before the annotations that sets up the needed transformation.

The before background and the behind background path of the spy-in node are used to draw things before and behind the magnified picture (which is actually a path picture [with a node with a PGFpicture] and thus part of the background path โ€“ for most nodes this is the only path defined).

The needed changes for your TikZ picture are:

  • move the spy scope to be the axis environment (the spy-in node is typeset at the end of the spy scope, this is the only chance we can access PGFPlots' coordinate systems),
  • always use the explicit coordinate system (i.e. axis cs: etc),
  • and put everything that has to do with the spy command into \pgfplotsextra.

The \spy macro itself doesn't do much but collect its arguments and puts them into a list to be executed at the end of the scope but at that point it's to late for some of the PGFPlots magic. We need to โ€œexecuteโ€ the spy code directly with \pgfplotsextra.


Should the annotations be clipped? Maybe with a rewrite of \tikz@lib@spy@do but that's a transformation festival with nested things. I don't know and use much PGFPlots to dive into this.

Code

\documentclass[tikz]{standalone}
\usepackage{pgfplots}
\pgfplotsset{width=10cm, height=5cm, compat=1.18}
\usetikzlibrary{arrows.meta, spy, calc}
\makeatletter
\let\tikz@lib@spy@do@orig\tikz@lib@spy@do % sigh
\def\tikz@lib@spy@do#1#2#3{% Have to find the location of spyinnode w/o drawing
  \setbox\pgfutil@tempboxa\hbox{%
    \node[overlay,spy annotations fg/.code=,spy annotations bg/.code=,
      alias=tikzspyinnode]#3{};}%
  \tikz@lib@spy@do@orig{#1}{#2}{#3}}
\tikzset{
  spy annotations fg/.code={%
    \tikz@addoption{%
      \expandafter\edef\csname pgf@sh@fbg@\tikz@shape\endcsname{%
        \noexpand\tikz@lib@spy@transformation{\tikz@shape}%
        \pgfutil@unexpanded{#1}}}},
  spy annotations bg/.code={%
    \tikz@addoption{%
      \expandafter\edef\csname pgf@sh@bbg@\tikz@shape\endcsname{%
        \noexpand\tikz@lib@spy@transformation{\tikz@shape}%
        \pgfutil@unexpanded{#1}}}}}
\def\tikz@lib@spy@transformation#1{% what shape really uses these paths?
  \expandafter\let\csname pgf@sh@fbg@#1\endcsname\pgfutil@undefined % nesting
  \expandafter\let\csname pgf@sh@bbg@#1\endcsname\pgfutil@undefined % nesting
  \pgftransformreset
  \let\tikz@transform\relax
  \pgftransformshift{%
    \expandafter\tikzset\expandafter{\tikz@lib@spy@lens}%
    \pgftransforminvert
    \pgfpointdiff                     {\pgfpointanchor{tikzspyonnode}{center}}
                 {\pgfpointtransformed{\pgfpointanchor{tikzspyinnode}{center}}}}%
  \expandafter\tikzset\expandafter{\tikz@lib@spy@lens}}
\makeatother
\begin{document}
\begin{tikzpicture}[declare function={sind(\x)=sin(deg(\x));}]
\begin{axis}[
  domain=0:2*pi, ymin=0, ymax=1.2, xmin = 0, xmax=2*pi, samples=17,
  spy using outlines={circle, magnification=2, size=3cm, connect spies}]% โ†
\addplot {sind(x)};
\pgfplotsextra{% โ† !
  \coordinate (magloc) at (axis cs:5, 0.6); % needs axis cs:
  \spy on (axis cs:pi/2,0.9) in node at (magloc) [% needs axis cs:
    spy annotations fg={% these all need explicit cs:
      \draw[Stealth-Stealth, red, thick]
        (axis cs: pi/2-pi/8, {sind(pi/2-pi/8)}) -- +(axis direction cs: pi/4, 0);
    },
    spy annotations bg={% yeah, these too
      \fill [green] (axis cs: pi/2, .9) circle[radius=12pt];
      \fill [red]   (axis cs: pi/2, .9) circle[radius= 2pt];
      \node[below left, inner sep=+0pt, draw, dashed, overlay, align=center]
        at (tikzspyinnode) {I'm not clipped\\and also a circle.};
       %    tikzspyinnode only works because of the patch
    }
  ];
}
\end{axis}
\end{tikzpicture}
\end{document}

Output

enter image description here