go.Scatter() plot with oriented markers (for example tadpole markers)

Hello @vont,
Here are the pseudo-tadpoles. You can customize their shape changing the parameters and constants in the function
control_polygon. Initially I suggested to define tadpoles as plotly shapes, but after a few experiments I realised that drawing them as filled closed curves is much faster than defining the curve as a svg path in a shape definition. Since so far I haven’t seen a plot with tadpoles I didn’t know whether the tadpole tail is pointing to a an element or its head. I chose the tail.

On the other hand, if a tadpole could be defined by only 4 points with coincident first and last points, then there was no need to use the de Casteljau algorithm because plotly offers the possibility to define a cubic Bezier curve (i.e. with 4 control points) as a plotly shape. But with only 3 distinct points a tadpole would look like a pin for geographical location: https://chart-studio.plotly.com/~empet/15362

import plotly.graph_objects as go
import numpy as np
from numpy import pi, sin, cos, sqrt

def rot(theta):
    theta*=pi/180
    return np.array([[cos(theta), -sin(theta)], [sin(theta), cos(theta)]])

def control_polygon(x=0, y=0,  width=0.1, height=0.18):
    # defines the bezier control points for a standard tadpole with tail at (0,0), and axis Oy as a symmetry line
    # width is the largest width measured in the horizontal direction 
    # height is the height of curve between the first control point and intersection point with the vertical through it
    # The parameters and constants in the ctrl elements definition have been set by trial and error
    
    ctrl=  np.array([[x, y],
                     [x - 0.1*width, y + 1.65*height],       
                     [x - sqrt(3)*width, y + 4*height/3], 
                     [x + sqrt(3)*width, y + 4*height/3], 
                     [x + 0.1*width, y + 1.65*height],                
                     [x, y]])
    return ctrl


def deCasteljau(ctrl, t): #de Casteljau algorithm to evaluste a point on a Bezier curve
    #ctrl an array of shape (6, 2) 
    #t a number in [0,1]
    #returns the point on the Bezier curve, corresponding to t
    N = ctrl.shape[0] 
    if N != 6:
        raise ValueError("A tadpole must be defined by a closed Bezier curve of 6 ctrl points") 
    a = np.copy(ctrl) 
    for r in range(1, N): 
        a[:N-r, :] = (1-t) * a[:N-r, :] + t * a[1:N-r+1, :]# convex combinations in the r^th step                                
    return a[0, :]

def cBezier(ctrl, nr=30):# evaluates nr points on the Bezier curve of control, points ctrl
    t = np.linspace(0, 1, nr)
    return np.asarray([deCasteljau(ctrl, t[k]) for k in range(nr)]) 


#Draw tadpoles pointing to markers from a prescribed direction (angle) 

X = [1, 2, 3]
Y = [1, 1.3, 2.4]
Theta = [-30, 45, 60]
fig = go.Figure(go.Scatter(x=X, y=Y, mode="markers", marker_color="red", 
                           marker_size=4.5, name="my markers"))
tadpole_x = []
tadpole_y = []
for x, y, t in zip(X, Y, Theta):
    c = control_polygon() #standard ctrl polygon 
    c = (rot(t) @ c.T).T + np.array([x,y]) #rotate the control polygon with t degrees and translate it in the direction (x,y)
    b = cBezier(c) # the Bezier curve as the boundary of a tadpole
    tadpole_x.extend(list(b[:, 0])+[None]) #insert a None after each tadpole
    tadpole_y.extend(list(b[:, 1])+[None])
fig.add_scatter(x=tadpole_x, y=tadpole_y, fill="toself", 
                fillcolor="RoyalBlue", line_color="RoyalBlue", showlegend=False)
fig.update_layout(width=600, height=450, yaxis_scaleratio=1, yaxis_scaleanchor="x")

tadpoles

1 Like