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")