It is not possible to plot arrows as annotation on a scattermapbox and maintain the 3D locations of the arrows.
What I did was to generate 2 traces for each arrow. The first trace is composed of 2 points, start and end, while the second trace has 4 points, [end, tipA, tipB, end) and filled "onself".
windField.windvectorList.forEach((item) =>{
this.TrackMapPlot_data.push({
type: "scattermapbox",
mode: "lines",
lat: [item.startlat, item.stoplat],
lon: [item.startlon, item.stoplon],
line: { color: "rgb(48,151,255)", width: 3}
});
this.TrackMapPlot_data.push({
type: "scattermapbox",
mode: "lines",
fill:"toself",
fillcolor: "rgb(48,151,255)",
lat: [item.stoplat, item.tipalat, item.tipblat, item.stoplat ],
lon: [item.stoplon, item.tipalon, item.tipblon, item.stoplon],
line: { color: "rgb(48,151,255)", width: 3}
})
});
To come up with tipA and tipB and have the arrow tip also scale with the arrow length I define:
- arrow opening: alpha
- arrow length as % of the total arrow length: d
- use the geopy library in the python backend to generate the 3 additional points. Arrow end and the 2 arrow tips.

def AddArrowTipsToWindVectors(wind_table: pd.DataFrame(), scale: float) -> pd.DataFrame():
#wind_table = wind_table.loc[:100, :]
# Make wind vectors
arrow_angle = 20
arrow_length = 0.2
wind_vectors = list()
wind_table['WindArrowTipALat'] = 0
wind_table['WindArrowTipALon'] = 0
wind_table['WindArrowTipBLat'] = 0
wind_table['WindArrowTipBLon'] = 0
for index, wind_row in wind_table.iterrows():
arrow_delta = math.asin(arrow_length*math.sin(math.radians(arrow_angle/2))/(1-arrow_length))
arrow_dist = wind_row['WindSpeed']*scale*(1-arrow_length)/math.cos(arrow_delta)
arrow_tip_A = GetTerminalLocFromDistAndHeading(wind_row['StationLat'], wind_row['StationLon'], arrow_dist, wind_row['WindDirection'] + math.degrees(arrow_delta))
wind_table.loc[index, 'WindArrowTipALat'] = arrow_tip_A[0]
wind_table.loc[index, 'WindArrowTipALon'] = arrow_tip_A[1]
arrow_tip_B = GetTerminalLocFromDistAndHeading(wind_row['StationLat'], wind_row['StationLon'], arrow_dist, wind_row['WindDirection'] - math.degrees(arrow_delta))
wind_table.loc[index, 'WindArrowTipBLat'] = arrow_tip_B[0]
wind_table.loc[index, 'WindArrowTipBLon'] = arrow_tip_B[1]
This is not a great solution but it works.
HOWEVER, there is a big issue with plotly and mapbox. Unfortunately, when adding several traces everything becomes extremely slow as pointed out in the following github links:
https://github.com/plotly/plotly.js/issues/3227
https://github.com/plotly/plotly.js/issues/1535
The problem is not the number of points in a single trace but the number of traces.
This makes plotly unusable for my application.
AGM solution
Angular Google Maps seems to work very well.
https://angular-maps.com/guides/getting-started/
Here is a snippet that shows how I use it:
<agm-map
[clickableIcons]="false"
[disableDefaultUI]="true"
[latitude]="trackDetails.mapboxlat"
[longitude]="trackDetails.mapboxlon"
[mapTypeId]="'satellite'"
[zoom]="trackDetails.mapboxzoomsmall"
(mapClick)="onChoseLocation($event)"
>
<agm-polyline *ngFor="let windvector of windField.windvectorList"
[visible]="true" [strokeWeight]="2" [strokeColor]="'rgb(43,42,255)'">
<agm-polyline-point
[latitude]="windvector.startlat"
[longitude]="windvector.startlon">
</agm-polyline-point>
<agm-polyline-point
[latitude]="windvector.stoplat"
[longitude]="windvector.stoplon">
</agm-polyline-point>
<agm-icon-sequence
[fixedRotation]="false"
[scale]="1.5"
[path]="'FORWARD_OPEN_ARROW'"
[fillColor]="'rgb(43,42,255)'">
</agm-icon-sequence>
</agm-polyline>
</agm-map>