9

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! :)

5
  • 1
    Excellent (and very detailed) question. I'm looking forward to seeing some good answers.
    – SebGlav
    Sep 22 at 14:42
  • PGFPlots uses its own layer system which makes this interesting. Do I understand it correctly that the green should be behind the plot while the red should be in front of it? Maybe the approach should be spy scope inside axis with the plots and afterwards drawing the annotation with the same spy transformation active (and then deal with the layers). Sep 22 at 14:49
  • Ugh, spy and pgfplots don't play nicely. The spy node is created when PGFPlots isn't active anymore (so axis cs: isn't available) and the content of the spy node is a node with a path picture with a node with a PGFPicture. Sep 22 at 15:29
  • @Qrrbrbirlbel spy is a prickly character and doesn't play nicely with lots of things .....
    – cfr
    Sep 22 at 15:43
  • @Qrrbrbirlbel, Actually I only wanted the red arrow to be on top, but your (mis-)understanding - not really a misunderstanding btw, I just wasn't specific enough in my question - came to produce an even more satisfying and complete answer! Kudos! And yes, spy's compatibility is quite nightmarish: with fill between for instance (see tex.stackexchange.com/a/695895/222633), but also for the possibility to not zoom into the marks, I'll ask a question about that also in the upcoming days...
    – mranvick
    Sep 25 at 8:41

2 Answers 2

4

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

3
  • I'd like to put a scope inside the axis but that opens another can of worms. Sep 22 at 18:46
  • Thank you for this complete, well explained, and most-importantly working answer! Out of curiosity, why do you have to specify axis cs: everywhere, given that you already are in the axis environment? Please pardon my lack of knowledge if this is evident, I am but a novice to TikZ / PGFPlots underlying mechanics.
    – mranvick
    Sep 25 at 8:46
  • 1
    @mranvick The spy library's action don't happen until the \end{axis} (which is a scope as well). I guess this happens at some fringe place where PGFPlots still has control but the implicit form of it doesn't work anymore. (Though, I'm wrong in my coment, the definition of the magloc coordinate doesn't need it.) One might to fix this by putting another scope environment inside the axis environment but then other things break. I believe, this is easier even though it makes it a bit more verbose. Sep 28 at 14:53
1

Voilà :

\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={}]
      \begin{axis}[domain=0:2*\pival, ymin=0, ymax=1.2, xmin = 0, xmax=2*\pival, samples=17]
        \coordinate (spyonto) at (axis cs:\pival/2,0.9);
        \coordinate (magloc) at (axis cs:5,0.6);
        \coordinate (arrowstart) at (axis cs:4.3,0.5);
        \coordinate (arrowend) at (axis cs:5.7, 0.5);
        \begin{scope}[spy using outlines={circle, magnification=\magn, size=\msize, connect spies}]
          \addplot {sin(x*180/\pival)};
          \spy on (spyonto) in node at (magloc)[name=spynode];
        \end{scope}
        % Remplace pink par white, j'ai mis pink pour que tu te rendes mieux compte sur la figure.
        \fill [pink]  (magloc) circle (\msize/2);
        \begin{scope}[anchor=spyonto, transform canvas={%
            shift={($-\magn*(spyonto)+(magloc)$)},
            scale=\magn }]
          % 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}

Compiled source

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .