How to draw double-headed arrows with hollow and solid heads in pygmt?

Hi all,

I am trying to create a plot with double-headed arrows where one head is filled (solid) and the other is hollow (outlined), similar to the example attached.

I have experimented with velo -Sx, but it only produces arrows with identical style heads pointing vertically, and it does not allow me to control the style of each arrowhead independently.

I am considering using plot multiple times—such as twice (one for outward double-headed arrows, or twice single-headed inward, or even four times to combine styles)—but so far, I haven’t found a clean or efficient approach.

Has anyone successfully created such mixed-style double-headed arrows in pygmt? Any advice or example scripts would be greatly appreciated!

Thank you very much for your help!

Hi @tiannh7,

These are quite interesting questions!

From reading the GMT docs for velo, I think it’s unfortuantely not possible to plot the extensional and compressional arrows of a strain cross in different colors, but users have to call velo twice, please see the PyGMT code below. Maybe users with more experience know more here :slightly_smiling_face: .

import pygmt
import numpy as np


size = 5

# >>> I have littel experience with plotting strain crosses. <<<
# >>> All data / numbers are are rondaom / dummy data. <<<
data = np.array([
    [-2, 0, 0.5, -0.2, 70, -1],
    [ 0, 0, 0.5, -0.2, 70,  0],
    [ 2, 0, 0.5, -0.2, 70,  1],
])
data_ext = data.copy()
data_ext[:,2] = [0] * len(data)
data_com = data.copy()
data_com[:,3] = [0] * len(data)


fig = pygmt.Figure()
fig.basemap(region=[-size, size] * 2, projection=f"X{size * 2}c", frame="g1")

for data, vector, pen in zip(
    [data_ext, data_com],
    ["+a40+h0.2", "+a60+h0"],
    ["1.5p,black", "2p,gray50,2_2"],
):
    fig.velo(data=data, spec="x5c", vector=vector, pen=pen)

fig.show()

Output figure:


Regarding the black outlined white filled arrow (head and stem) in the provided map (for details see velo — GMT 6.6.0 documentation):

Vectors were completely redesigned for GMT5 which separated the vector head (a polygon) from the vector stem (a line). In GMT4, the entire vector was a polygon and it could only be a straight Cartesian vector.


Arrows with different head and tail: I think there are some possibilities, like different symbols for the head and tail, but I think one can not provided different fill colors:

import pygmt


fig = pygmt.Figure()
fig.basemap(region=[-5, 5, -2, 2], projection="X10c/4c", frame=1)

for y, style in zip(
    [-1, 1],
    ["v0.6c+ba+eA+a60+h0", "v0.6c+ba+ec+a60+h0"],
):
    fig.plot(
        x=-3,
        y=y,
        style=style,
        direction=([0], [6]),
        pen="1p",
        fill="red",
    )

fig.show()

Output figure:

But it’s possible to add symbols at the start and end of lines via -W. There, symbols and colors can be adjusted separately for the start and end; for details see 3. General Features — GMT 6.6.0 documentation.

import pygmt


fig = pygmt.Figure()
fig.basemap(region=[-5, 5, -1, 1], projection="X10c/2c", frame=1)

fig.plot(
    x=[-4, 4],
    y=[0, 0],
    pen="1p,gray50+vb0.6c+bc+gpurple+p1.5p,magenta+ve0.8c+gblue+p1p,cyan",
)

fig.show()

Output figure:

Hi @yvonnefroehlich,

Thank you very much for your detailed and insightful reply!

I agree that calling velo twice is a practical approach, and your example code really helps clarify how to separate extensional and compressional components with different styles. I have tried similar methods but still struggled to achieve the hollow style for the extensional arrows consistently.

Also, I noticed that the -W pen options don’t seem to fully apply within velo for customizing outlines and fills, which limits flexibility. Your suggestion about using plot with different symbols at line ends is very interesting — it might be a good workaround.

I really appreciate you sharing these example codes and explanations. This definitely gives me some new ideas to try!

Thanks again for your help!

To plot an entirely hollow arrow (stem and head/tail) directly, one needs to use the GMT4 syntax. There arrows were defined as a polygon including the stem.

import pygmt


fig = pygmt.Figure()
fig.basemap(region=[-5, 5, -1, 1], projection="X10c/2c", frame=1)

fig.plot(
    x=-3,
    y=0,
    # GMT4 synthax tailwidth/headlength/halfheadwidth
    style="v0.08c/0.3c/0.25c",
    direction=([0], [6]),
    pen="0.1p",
)

fig.show()

Output figure:

In the new definition, only the head/tail is a polygon, but the stem is now a line. Thus the stem does not have a fill and an outline anymore; only the head/tail.
But it looks like that velo does not support the GMT4 syntax for arrows for plotting strain crosses. So, maybe plotting a thicker black arrow and a second smaller white arrow on top is a workaround? Something in the direction of:

import pygmt


color_out = "black"
color_in = "white"

fig = pygmt.Figure()
fig.basemap(region=[-5, 5, -1, 1], projection="X10c/2c", frame=1)

for pen in [f"4p,{color_out}", f"2p,{color_in}"]:

    fig.plot(
        x=-3,
        y=0,
        style=f"v0.6c+ba+a60+h0+p1p,{color_out}",
        direction=([0], [6]),
        pen=pen,
        fill=color_in,
    )

fig.show()

Output figure:

Thank you so much for the detailed explanation and example code! This really helps clarify the limitations and possible workarounds.

I will definitely try the approach of layering a thicker black arrow with a smaller white arrow on top. If I manage to find a good solution, I’ll be sure to share it here.

Thanks again for your support and valuable suggestions!