2

I am drawing curved arrows in tikz-cd, using the method devised by @AndréC in this answer. There are situations where I want to shorten these arrows, proportional to their arc length. However, the naïve solution (using shorten) is not suitable, as it creates distorted curves (as discussed, for instance, in this question). As an example (Example 1), the following two curves have the same input path, but the red curve has been shortened. What should happen is the red curve should overlay the black curve.

bad shortened curve

Note that it is not just the position of the shortened curve that is incorrect: the shape is incorrect too: in the example (Example 2) below, I've positioned the curves so they have the same apex, but the shortened curve does not overlay the original (the amount of distortion depends on the curve width and height).

overlaid bad curve

The other issue with shorten is that it requires an absolute length to shorten by, whereas I want to specify it proportionally with respect to the length of the curve itself.

I attempted to manually draw an arrow head with decorations.markings, but this does not properly calculate the right size for the arrowhead (or take into account the existing style of the head or tail). In the example below (Example 3), the arrow head further up the curve should be the same size as the one at the tip.

bad manual head

My idea was to shorten this arrow using a custom dash pattern, but this seems like a hack that is likely in practice to fail in edge cases. Ideally, the curve path itself should be changed, which would handle the existing style/arrowheads/tails/etc. The option pgfpathcurvebetweentime seemed like a possible solution, but I could not figure out how to integrate it with the custom curve style in the two examples.

How might one add an option to the custom curve style to allow the curve to be shortened (e.g. curve={height=-40pt, shorten=0.2})? In practice, shortening symmetrically from both ends is usually sufficient, but having options to shorten the start and end differently would be even better if it doesn't add much extra complexity.

For a demonstration of what I intend by "shortening a curved arrow", see @Thruston's example.

Example 1

\documentclass{article}
\usepackage{tikz-cd}
\usetikzlibrary{calc}

\begin{document}

{\tikzset{curve/.style={settings={#1},to path={(\tikztostart)
    .. controls ($(\tikztostart)!\pv{pos}!(\tikztotarget)!\pv{height}!270:(\tikztotarget)$)
    and ($(\tikztostart)!1-\pv{pos}!(\tikztotarget)!\pv{height}!270:(\tikztotarget)$)
    .. (\tikztotarget)\tikztonodes}},
    settings/.code={\tikzset{quiver/.cd,#1}
        \def\pv##1{\pgfkeysvalueof{/tikz/quiver/##1}}},
    quiver/.cd,pos/.initial=0.35,height/.initial=0}

\[\begin{tikzcd}
    {\bullet} & {\bullet}
    \arrow[from=1-1, to=1-2, curve={height=-40pt}, shorten <= 30pt, shorten >= 30pt, color=red]
    \arrow[from=1-1, to=1-2, curve={height=-40pt}]
\end{tikzcd}\]}

\end{document}

Example 2

\documentclass{article}
\usepackage{tikz-cd}
\usetikzlibrary{calc}

\begin{document}

{\tikzset{curve/.style={settings={#1},to path={(\tikztostart)
    .. controls ($(\tikztostart)!\pv{pos}!(\tikztotarget)!\pv{height}!270:(\tikztotarget)$)
    and ($(\tikztostart)!1-\pv{pos}!(\tikztotarget)!\pv{height}!270:(\tikztotarget)$)
    .. (\tikztotarget)\tikztonodes}},
    settings/.code={\tikzset{quiver/.cd,#1}
        \def\pv##1{\pgfkeysvalueof{/tikz/quiver/##1}}},
    quiver/.cd,pos/.initial=0.35,height/.initial=0}

\[\begin{tikzcd}
    {\bullet} &&& {\bullet}
    \arrow[from=1-1, to=1-4, curve={height=-60pt}, shorten <= 30pt, shorten >= 30pt, color=red, yshift=-11pt]
    \arrow[from=1-1, to=1-4, curve={height=-60pt}]
\end{tikzcd}\]}

\end{document}

Example 3

\documentclass{article}
\usepackage{tikz-cd}
\usetikzlibrary{calc}
\usetikzlibrary{decorations.markings}

\begin{document}

{\tikzset{curve/.style={settings={#1},to path={(\tikztostart)
    .. controls ($(\tikztostart)!\pv{pos}!(\tikztotarget)!\pv{height}!270:(\tikztotarget)$)
    and ($(\tikztostart)!1-\pv{pos}!(\tikztotarget)!\pv{height}!270:(\tikztotarget)$)
    .. (\tikztotarget)\tikztonodes},
    % Arrow head and tail
    decoration={
      markings,
      mark=at position 0.8 with {\arrow{>}}}
    , postaction={decorate}},
    %
    settings/.code={\tikzset{quiver/.cd,#1}
        \def\pv##1{\pgfkeysvalueof{/tikz/quiver/##1}}},
    quiver/.cd,pos/.initial=0.35,height/.initial=0}

\[\begin{tikzcd}
    {\bullet} & {\bullet}
    \arrow[Rightarrow, from=1-1, to=1-2, curve={height=-40pt}]
\end{tikzcd}\]}

\end{document}
7
  • Isn't it enough to move this arrow with e.g. yshift=5pt?
    – AndréC
    Oct 28, 2020 at 12:02
  • @AndréC: unfortunately not: the actual curve shape is incorrect. I've added an example in the question. In some cases, it's close enough not to be noticeable, but that's not always the case. I also don't know of a way to deterministically calculate how much to shift by to align the apices, even if this was sufficient.
    – varkor
    Oct 28, 2020 at 12:25
  • What are the cases that prevent you from using such a move?
    – AndréC
    Oct 28, 2020 at 12:44
  • I'm outputting TikZ code from a diagram editor, so the TikZ diagram needs to match the original diagram as closely as possible, for arbitrary diagrams. This means I can't hard-code offsets, and need to be confident that the output will be reasonable regardless of the shape of the curve.
    – varkor
    Oct 28, 2020 at 13:22
  • I can add a parameter that shifts the curve perpendicular to the path, would that be okay with you?
    – AndréC
    Oct 28, 2020 at 14:32

6 Answers 6

5

Update: 2023-04-8: This is now in the version of spath3 available on CTAN

Now that this is implemented in the version of spath3 on CTAN, the code below can be considerably simplified.

\documentclass{article}
%\url{https://tex.stackexchange.com/q/568648/86}
\usepackage{tikz-cd}
\usetikzlibrary{
  calc,
  spath3
}



\def\pv#1{%
  \pgfkeysvalueof{/tikz/quiver/#1}%
}


\tikzset{
  curve/.style={
    quiver/.cd,
    #1,
    /tikz/.cd,
    to path={
      (\tikztostart) .. controls ($(\tikztostart)!\pv{pos}!(\tikztotarget)!\pv{height}!270:(\tikztotarget)$)
    and ($(\tikztostart)!1-\pv{pos}!(\tikztotarget)!\pv{height}!270:(\tikztotarget)$)
    .. (\tikztotarget)\tikztonodes}
  },
  quiver/.cd,
  pos/.initial=0.35,
  height/.initial=0
}

\begin{document}

\begin{tikzpicture}
\draw[spath/save=curve, ultra thick] (0,0) .. controls +(1,2) and +(-1,2) .. (3,0);
\tikzset{
%  spath/split at keep start={curve}{.9},
%  spath/split at keep end={curve}{.1/.9},
  spath/split at keep middle={curve}{.1}{.9},
}
\draw[spath/use={curve},red];
\end{tikzpicture}

\begin{tikzcd}
{\bullet} & {\bullet} & {\bullet}
\arrow[
  from=1-1,
  to=1-2,
  curve={
    height=-40pt
  }
]
\arrow[
  from=1-2,
  to=1-3,
  curve={
    height=-40pt
  },
  /tikz/spath/at end path construction={
    \tikzset{spath/split at keep middle={current}{.1}{.9}}
  },
  color=red,
]
\end{tikzcd}

\end{document}

This produces:

Shortened curves along their paths


The capability to do this is essentially in my spath3 package. The existing user-level code shortens paths by a dimension so I needed to do a bit of coding to add the required functionality to the user interface, this will probably end up in the library in some form or other as it feels quite useful (I just need to think about consistency between the various commands that I have). So don't be alarmed by the stuff in the preamble - that'll end up in the package eventually.

\documentclass{article}
%\url{https://tex.stackexchange.com/q/568648/86}
\usepackage{tikz-cd}
\usetikzlibrary{
  calc,
  spath3
}

\makeatletter
\ExplSyntaxOn

\cs_new_protected_nopar:Npn \spath_discard_after:Nn #1#2
{
  \spath_split_at:NNVn \l_tmpa_tl \l_tmpb_tl #1 {#2}
  \tl_set_eq:NN #1 \l_tmpa_tl
}

\cs_new_protected_nopar:Npn \spath_discard_before:Nn #1#2
{
  \spath_split_at:NNVn \l_tmpa_tl \l_tmpb_tl #1 {#2}
  \tl_set_eq:NN #1 \l_tmpb_tl
}

\cs_new_protected_nopar:Npn \spath_discard_outside:Nnn #1#2#3
{
  \spath_split_at:NNVn \l_tmpa_tl \l_tmpb_tl #1 {#3}
  \spath_split_at:NNVn \l_tmpa_tl \l_tmpb_tl \l_tmpa_tl {(#2)/(#3)}
  \tl_set_eq:NN #1 \l_tmpb_tl
}

\cs_generate_variant:Nn \spath_discard_after:Nn {cn}
\cs_generate_variant:Nn \spath_discard_before:Nn {cn}
\cs_generate_variant:Nn \spath_discard_outside:Nnn {cnn}

\tl_new:N \l__tikzspath_tikzpath_finish_tl

\cs_new_protected_nopar:Npn \spath_at_end_of_path_construction:
{
  \tl_use:N \l__tikzspath_tikzpath_finish_tl
}
\tl_put_left:Nn \tikz@finish {\spath_at_end_of_path_construction:}

\tikzset{
  spath/.cd,
  at~ end~ path~ construction/.code={%
    \tl_put_right:Nn \l__tikzspath_tikzpath_finish_tl {#1}
  },
  discard~ after~ point/.code~ 2~ args={
    \__tikzspath_maybe_current_path_reuse:nnn
    {
      \__tikzspath_check_path:nnn
      {
        \spath_discard_after:cn
      }
    } {#1} { {} {#2} }
  },
  discard~ before~ point/.code~ 2~ args={
    \__tikzspath_maybe_current_path_reuse:nnn
    {
      \__tikzspath_check_path:nnn
      {
        \spath_discard_after:cn
      }
    } {#1} { {} {#2} }
  },
  discard~ outside~ points/.code~ n~ args={3}{
    \__tikzspath_maybe_current_path_reuse:nnn
    {
      \__tikzspath_check_path:nnn
      {
        \spath_discard_outside:cnn
      }
    } {#1} { {} {#2} {#3} }
  },
}

\ExplSyntaxOff
\makeatother

\def\pv#1{%
  \pgfkeysvalueof{/tikz/quiver/#1}%
}


\tikzset{
  curve/.style={
    quiver/.cd,
    #1,
    /tikz/.cd,
    to path={
      (\tikztostart) .. controls ($(\tikztostart)!\pv{pos}!(\tikztotarget)!\pv{height}!270:(\tikztotarget)$)
    and ($(\tikztostart)!1-\pv{pos}!(\tikztotarget)!\pv{height}!270:(\tikztotarget)$)
    .. (\tikztotarget)\tikztonodes}
  },
  quiver/.cd,
  pos/.initial=0.35,
  height/.initial=0
}

\begin{document}

\begin{tikzcd}
{\bullet} & {\bullet} & {\bullet}
\arrow[
  from=1-1,
  to=1-2,
  curve={
    height=-40pt
  }
]
\arrow[
  from=1-2,
  to=1-3,
  curve={
    height=-40pt
  },
  /tikz/spath/at end path construction={
    \tikzset{spath/discard outside points={current}{.1}{.9}}
  },
  color=red,
]
\end{tikzcd}

\end{document}

Path shortened by parameter

NB The link above is to the github repository as the development version is a little ahead of the ctan at the moment.

6
  • This looks great: from a little testing, it seems to work well with existing arrow styles (e.g. Rightarrow). If it can be made more user-friendly with TikZ keys (once you've figured out the best API), it will be perfect. It seems like it could be a stand-in for the shorten keys, as it essentially corrects their behaviour.
    – varkor
    Nov 20, 2021 at 18:08
  • @varkor Thanks. If you encounter any bizarre behaviour in testing please do let me know (via github, or my contact details are in the documentation). There's already keys for shortening correctly via a dimension. The difference here is that it is using a parameter which I I didn't originally have. Nov 20, 2021 at 18:36
  • I'll accept this answer, as it achieves what I want, though please let me know when you add a convenient API for this feature!
    – varkor
    Nov 21, 2021 at 15:05
  • @varkor Will do. I have a few updates nearly ready to go on this library so it shouldn't be too long. Just a few things to test first to make sure it's stable. Nov 21, 2021 at 15:19
  • 1
    @varkor almost certainly, I just need a few minutes at my computer to revisit it with the latest version. Thanks for the reminder. Apr 8 at 12:58
4

Is this what you mean?

enter image description here

This is done in Metapost, purely for comparison, and in the hope that some one else here can show us how to do the equivalent in TikZ.

\documentclass[border=5mm]{standalone}
\usepackage{luamplib}
\begin{document}
\mplibtextextlabel{enable}
\begin{mplibcode}
input colorbrewer-rgb
beginfig(1);
    z0 = -z1 = 34 left;
    path a; a = z0 .. controls (-13, 55) and (13, 55) .. z1;
    ahangle := 30;
    ahlength := 2;
    for s=1 upto 8:
        drawarrow a 
            cutbefore fullcircle scaled 10s shifted z0
            cutafter  fullcircle scaled 10s shifted z1
            withcolor Blues[9][s];
    endfor
    drawdot z0 withpen pencircle scaled dotlabeldiam;
    drawdot z1 withpen pencircle scaled dotlabeldiam;
endfig;
\end{mplibcode}
\end{document}

(Compile with lualatex).

What I have done each is draw the same curve each time, but with more of it "cut off" at each end.

3
  • 1
    + 1 very nice !
    – AndréC
    Oct 29, 2020 at 12:23
  • Yes, thank you, this is precisely what I mean. I will add a link to your answer in the original question for clarity.
    – varkor
    Oct 29, 2020 at 13:12
  • Great answer...Thruston and don't forget that I'm still waiting for your answer on the feymmp package arrows. Otherwise I won't vote for you any more ahahah.
    – Sebastiano
    Oct 29, 2020 at 22:13
4

This is a topic I also struggle with for some time. I found something that could be useful on this topic: Tikz Graphics: Curved arrow drawn parallel to curved line

I adapted it to the current question and am almost happy with it:

\documentclass{article}
\usepackage{tikz}
\begin{document}
\thispagestyle{empty}

\begin{tikzpicture}

    % Nodes for start and end
    \node at (0,0) (A) {};  
    \node at (5,0) (B) {};  

  
    % Path from start to end, using Bezier definition (no to[bend] allowed)
    % The path is cut into 20 parts, using 'pos'
    \path  (A) .. controls +(2,4) and +(-2,4).. (B)
        {\foreach \t [count=\i] in {0.05,0.10,0.15,...,0.85,0.90,0.95,1}
            {
            coordinate[pos=\t] (p\i)
            }
        };
  
    % naming p0 and p20 to allow the loop to draw the complete first curve
    \coordinate (p0) at (A);
    \coordinate (p20) at (B);
    
    % Drawing the 9 other curves
    \foreach \k in {0,...,9}
        {
        \pgfmathtruncatemacro\l{20-\k}      % p\k is the start point, p\l is the end point
        \pgfmathtruncatemacro\clr{10*\k}    % fading the colour of the curves
        
        % Now draw (segments) between all p\i points from p\k to p\l
        \draw[cap=round,line width=1.5pt,-stealth,blue!\clr] (p\k)
            {\foreach \i in {\k,...,\l} {-- (p\i) }};
        }
    
\end{tikzpicture}
\end{document}

First attempt

Now, the main issue of this solution is that it doesn't draw curves but broken lines. This could easily be fixed by computing more points on the original path. You may want to play with it, while finding a better solution.

3
  • Thanks for sharing this method! This achieves the effect I was going for, but I'm concerned that it may not be compatible with existing TikZ/tikz-cd arrow styles, like double arrows and squiggly arrows, because it relies upon manually drawing the edges. Perhaps I'm wrong, though?
    – varkor
    Jan 2, 2021 at 1:18
  • I don't understand why it wouldn't be compatible with any TikZ decorations. You may use the decorations.markings TikZ library with postaction to place any arrow tip at the end of the partial path. I'm working on another better solution, maybe you could be more precise about what you exactly want to draw.
    – SebGlav
    Jan 2, 2021 at 11:28
  • I'm just not familiar enough with tikzpicture to know how to apply the Rightarrow and squiggly styles, for instance, to arrows drawn with this method.
    – varkor
    Jan 2, 2021 at 12:18
3
\documentclass[tikz, border=1 cm]{standalone}
\usetikzlibrary{decorations.markings}
\begin{document}
\begin{tikzpicture}
\newcommand{\mypath}{(0,0) to[out=80, in=100, min distance=1cm] (1,0)}
\newcommand{\mystart}{0.2} %start path at 20%
\newcommand{\myend}{0.8}   %end path at 80%
\draw[ultra thick] \mypath;
\draw[
draw=none,
preaction={decoration={markings, mark=at position \myend with {\arrow{>[green]}}}, decorate},
postaction={draw, green, decoration={curveto, 
pre=moveto, pre length=\mystart*\pgfmetadecoratedpathlength,
post=moveto, post length=(1-\myend)*\pgfmetadecoratedpathlength, 
}, decorate}
] \mypath;
\end{tikzpicture}
\end{document}

Proportional shortened arrow


Edit: Example 1 with the shortened arrow:

\documentclass[tikz, border=1 cm]{standalone}
\usepackage{tikz-cd}
\usetikzlibrary{calc, decorations.markings}
\begin{document}
\tikzset{curve/.style={settings={#1},to path={(\tikztostart)
    .. controls ($(\tikztostart)!\pv{pos}!(\tikztotarget)!\pv{height}!270:(\tikztotarget)$)
    and ($(\tikztostart)!1-\pv{pos}!(\tikztotarget)!\pv{height}!270:(\tikztotarget)$)
    .. (\tikztotarget)\tikztonodes}},
    settings/.code={\tikzset{quiver/.cd,#1}
        \def\pv##1{\pgfkeysvalueof{/tikz/quiver/##1}}},
    quiver/.cd,pos/.initial=0.35,height/.initial=0}

\begin{tikzcd}
    {\bullet} & {\bullet}
    \arrow[from=1-1, to=1-2, curve={height=-40pt}]
    \arrow[from=1-1, to=1-2, curve={height=-40pt},
    -_,
    draw=none,
    preaction={decoration={markings, mark=at position 0.8 with {\arrow{>[green]}}}, decorate},
    postaction={draw, green, decoration={curveto, 
    pre=moveto, pre length=0.2*\pgfmetadecoratedpathlength,
    post=moveto, post length=(1-0.8)*\pgfmetadecoratedpathlength, 
    }, decorate}]
\end{tikzcd}
\end{document}

Two dots, arrow and shortened green arrow

5
  • Thanks for your answer: could you that this is compatible with the existing method for drawing curved arrows in my examples? When I tried using decoration.markings, it wasn't compatible with the arrowhead/tail styles declared in tikz-cd (e.g. see Example 3 in the question).
    – varkor
    Nov 20, 2021 at 13:36
  • @varkor: I do have not tried to understand your style, but it works fine with this shortening methode. Visually, your style does the same as someting like ->, shorten >=10pt, shorten <=10pt Nov 20, 2021 at 18:33
  • @varkor: I should have used Example 3, but the style is even more confusing for me, with extra arrowhead. I do not know how to make a double line arrow head (Implies[])with \arrow{} Nov 20, 2021 at 19:06
  • Thanks, this looks very close to what I'm after. Unfortunately, for my use-case, I really do need to be able to use double with the correct arrowhead; I agree this seems tricky, and it was a problem I encountered too.
    – varkor
    Nov 20, 2021 at 19:27
  • @varkor: Questions should focus on one topic at a time, if you need \arrow to draw a double line arrow, you could ask a new question about that. You have six answers please accept one. Nov 21, 2021 at 14:42
2

Here is my proposal:

On the previous style, I added the shift parameter which allows me to shift the curve perpendicular to the path. To do this, I applied the idea I developed in my answer to the question Annotating a table with arrows.

The new style is called curve 1, I put as default height=-40 pt as in your examples.

screenshot

\documentclass{article}
\usepackage{tikz-cd}
\usetikzlibrary{calc}

\begin{document}

\tikzset{curve 1/.style={settings={#1},
                        to path={($(\tikztostart)!\pv{shift}!90:(\tikztotarget)$) .. controls ($(\tikztostart)!\pv{pos}!(\tikztotarget)!\pv{height}!270:(\tikztotarget)$)
    and ($(\tikztostart)!1-\pv{pos}!(\tikztotarget)!\pv{height}!270:(\tikztotarget)$)
    .. ($(\tikztotarget)!\pv{shift}!270:(\tikztostart)$)\tikztonodes}},
    settings/.code={\tikzset{quiver/.cd,#1}
        \def\pv##1{\pgfkeysvalueof{/tikz/quiver/##1}}},
    quiver/.cd,pos/.initial=0.35,height/.initial=-40pt,shift/.initial=0pt}
    
\[\begin{tikzcd}
    {\bullet} & {\bullet}
    \arrow[from=1-1, to=1-2, curve 1={shift=2mm},color=blue,thick]
    \arrow[from=1-1, to=1-2, curve 1={shift=4mm},color=cyan,thick]
    \arrow[from=1-1, to=1-2, curve 1={shift=6mm},color=green,thick]
    \arrow[from=1-1, to=1-2, curve 1={shift=8mm},color=yellow,thick]
    \arrow[from=1-1, to=1-2, curve 1={shift=10mm},color=orange,thick]
    \arrow[from=1-1, to=1-2, curve 1={shift=12mm},color=red,thick]
\end{tikzcd}\]

\end{document}
1
  • Thanks for your answer. Unfortunately, this doesn't solve the problem mentioned above, that using shorten, combined with shift will have distorted shape compared to the original curve. Also, even if the shape were correct, there's no well to tell how much to shift by to align the two curves perfectly.
    – varkor
    Oct 28, 2020 at 18:17
2

Since I don't really understand what still refrains you to use this method, I made some adjustments, in order to make it easier to use. In this new version, you can decide the position of both starting and ending points of your arrow, and the number of points to be computed, in order to avoid the uggly broken line effect on strongly curved lines.

\documentclass{standalone}
\usepackage{tikz}
\begin{document}
\thispagestyle{empty}

\begin{tikzpicture}

    % First, the starting and ending nodes
    \draw (0,0) node[circle, inner sep=0.8pt, fill=black, label={below:{$A$}}] (A) {};  
    \draw (5,1) node[circle, inner sep=0.8pt, fill=black, label={above:{$B$}}] (B) {};
    
    % Define the path between the two nodes (Bezier curve)
    \def\route{(A) .. controls +(2,5) and +(-2,-3).. (B)}
    
    % Drawing the complete path
    \draw[orange] \route;
    
    %%%%%%%%%%%%%%
    % Blue arrow %
    %%%%%%%%%%%%%%
    
    \def\start{0.5}     % Starting point position
    \def\len{0.45}      % Arrow length (percentage of total line length)
    \def\numpoints{30}  % Number of points to draw segments between them
    
    % Computing the 30 points from the arrow start and the arrow end
    \path  \route 
        {\foreach \i in {1,...,\numpoints} { coordinate[pos=\start+\len*\i/\numpoints] (p\i) } };
        
    % Drawing a broken line linking all 30 points, with the arrow decoration at the end
    \draw[blue, cap=round, opacity=0.5, line width=1.5pt,->>] (p1)
        { \foreach \j in {2,...,\numpoints} {-- (p\j) } };
    
    %%%%%%%%%%%%%
    % Red arrow %
    %%%%%%%%%%%%%
    \def\start{0.4} \def\len{-0.3} \def\numpoints{30}
    \path  \route 
        {\foreach \i in {1,...,\numpoints} { coordinate[pos=\start+\len*\i/\numpoints] (p\i) } };
    \draw[red, cap=round, opacity=0.5, line width=1.5pt,->>] (p1)
        { \foreach \j in {2,...,\numpoints} {-- (p\j) } };
    
\end{tikzpicture}


\end{document}

Hope this will give you a better insight on this.Arrows on Curve

edit: I realize now that you would want to do this, using @AndréC 's method of creating curves between points. If you don't find how to implement this, let us know, sure someone will find a way.

You must log in to answer this question.

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