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

Hi

I am trying to create a go.Scatter() plot with marker symbols (for example tadpoles) that are oriented.
In the doc, I didn’t find any property to modify the orientation of marker symbols.

Is there perhaps a workaround?

I found that in Matplotlib, there is such a functionality (link).

Thanks a lot in advance!
Martin

Hi @vont,
This is the Plotly version for oriented markers:

import plotly.graph_objects as go
import numpy as np
tris= ["triangle-up", "triangle-down", "triangle-left", "triangle-right"]
fig=go.Figure(go.Scatter(x=[1,2,3,4,5,6], y=np.random.randint(1, 13, 6), 
                         mode="markers", marker_size=15,
                         marker_symbol= [tris[k] for k in np.random.randint(0, 4, 6)], 
                         marker_color=["red", "blue", "orange", "green", "yellow", "black"])
                         #or marker=dict(size=15, symbol=[tris[k] for k in np.random.randint(0, 4, 6)], color=["red"])
             )

fig.update_layout(width=500, height=450)

oriented-multicolor-triangles
or single color markers, defined as a dict in the commented line:
oriented-triangles

1 Like

Hello empet

Thanks a lot for your reply. It’s not super important but my intention was to add azimuth information to the markers, so that I could have the markers be oriented in any direction, depending of what azimuth is provided. Such plots are used in geology to illustrate the orientation of borehole fracture data.

If the azimuth is indicated at the marker with a small tail sticking out from let’s say a round marker, it looks a bit like a tadpole which explains the name of these plots.

Would it be difficult to develop such a functionality inside Plotly?

Best wishes and again many thanks for your reply,
Martin

Hello @vont,
It is possible to draw a shape as a filled closed Bezier curve, that looks like a tadpole. At the beginning of the next week I’ll post the code, that generates such a Bezier curve. Just now I am in a sort of vacation.

Hi Empet

Looking good there with all the sun you have. Enjoy your stay and I am looking forward to seeing how this works with the Bezier curves.

Best wishes,
Martin

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

Hi empet

Many thanks for your reply! This time I am en route - not vacation - but just not at the desk.
This looks brilliant - I’ll have a more detailed look at your code next time I’m at home.

Many thanks again and have a good weekend!
Martin

Hello @empet,
It is very close to what I want below:

image

It will be great if more marks were implemented in plotly.

Hello Laotu,

With more recent versions of Plotly you can style your markers pretty much as you like.
I think the styling of markers wasn’t possible in Plotly about 9 months ago when I asked the above question.

I need the same marker style as you to create a tadpole plot and came up with the following solution:

import numpy as np
import plotly.graph_objs as go
import plotly.express as px
from ipywidgets import interactive, HBox, VBox, widgets, interact, fixed,
interact_manual, Layout, Button, Box, FloatText, Textarea, Dropdown,
Label, IntSlider
from IPython.display import display, HTML
display(HTML(“.container { width:100% !important; }”))

“”" FUNCTION TO UPDATE FIGURE-WIDGET. “”"

def update1(XAxis, xAxisTuple):

# The following two function arguments are not used on the subsequent lines.
print('type(XAxis.range) =', type(XAxis.range))
print('type(xAxisTuple) =', type(xAxisTuple))

with f1.batch_update():
    xAxMin = f1.layout.xaxis['range'][0]
    print(xAxMin)
    xAxMax = f1.layout.xaxis['range'][1]
    print(xAxMax)
    yAxMin = f1.layout.yaxis['range'][0]
    print(yAxMin)
    yAxMax = f1.layout.yaxis['range'][1]
    print(yAxMax)
    
    # Corrective term accounting for ratio of axes ranges.
    fac1 = (xAxMax-xAxMin) / (yAxMax-yAxMin)

    # Corrective term accounting for window size and figure margins.
    fac2 = (heig-bordT-bordB)/(wid-bordL-bordR)
    fac3 = (1000-bordT-bordB)/(heig-bordT-bordB)
    print(fac2)
    
    # Adjustment term for chosen value for variable l = 0.0042
    fac4 = (yAxMax-yAxMin)/3
    
    f1.data[1].x = df['sepal_width']
    f1.data[1].y = df['sepal_length']
    
    f1.data[1].x = df['sepal_width']  + l * np.cos(np.deg2rad(-ang))*fac1*fac2*fac3*fac4
    f1.data[1].y = df['sepal_length'] + l * np.sin(np.deg2rad(-ang))*fac3*fac4

“”" LOADING DATA AND SETTING SOME FIGURE PARAMETERS. “”"

df = px.data.iris()
ang = 120
XaxRange = [2, 5]
YaxRange = [4, 9]
wid = 1000
heig = 800
bordL = 10
bordR = 10
bordT = 10
bordB = 10
l = 0.042

“”" TRACE 1, CIRCLE MARKERS. “”"

fig1 = px.scatter(df, x=‘sepal_width’, y=‘sepal_length’)
fig1.update_traces(marker=dict(size=8,
symbol=“circle”,
line=dict(width=2, color=“DarkSlateGrey”)),
selector=dict(mode=“markers”),
showlegend=False,
marker_color=‘LightSteelBlue’)

“”" TRACE 2, LINE MARKERS “”"

DEFINITION OF POSITIONS OF LINE MARKERS.

Corrective term accounting for ratio of axes ranges.

fac1 = (XaxRange[1]-XaxRange[0]) / (YaxRange[1]-YaxRange[0])

Corrective term accounting for window size and figure margins.

fac2 = (heig-bordT-bordB)/(wid-bordL-bordR)
fac3 = (1000-bordT-bordB)/(heig-bordT-bordB)

Adjustment term for chosen value for variable l = 0.042

fac4 = (YaxRange[1]-YaxRange[0])/3

df[‘x’] = df[‘sepal_width’] + l * np.cos(np.deg2rad(-ang)) * fac1 * fac2 * fac3 * fac4
df[‘y’] = df[‘sepal_length’] + l * np.sin(np.deg2rad(-ang)) * fac3 * fac4

fig2 = px.scatter(df, x=‘x’, y=‘y’)
fig2.update_traces(marker=dict(size=12,
symbol=“line-ew”,
angle=ang,
line=dict(width=2, color=“DarkSlateGrey”)),
selector=dict(mode=“markers”),
showlegend=False)

“”" FIGURE-WIDGET COMBINING TRACES 1 AND 2. “”"

fig3 = go.Figure(data=fig1.data + fig2.data,
layout_xaxis_range=XaxRange,
layout_yaxis_range=YaxRange)

fig3.update_layout(margin=dict(l=bordL, r=bordR, t=bordT, b=bordB),
paper_bgcolor=“LightSteelBlue”,
width=wid, height=heig)

f1 = go.FigureWidget(fig3)

display(f1)

f1.layout.xaxis.on_change(update1, ‘range’)