Tadpole style marker for figures (automatically update FigureWidget when zooming)

Hello

I recently saw that it’s now possible to style markers in Plotly:
https://plotly.com/python/marker-style/

Would it be possible to add a “tadpole” marker style to the markers listed in the above link?

@empet already suggested a workaround a few months ago here:

With the markers above I also found another workaround which now makes the tails of the markers appear as lines as wanted. I simply combine two traces with the marker symbols “circle” and “line-ew” (these are the tadpole tails) to get the desired tadpole plot.

In the workaround code below, the “line-ew” markers are placed with a small offset with respect to the “circle” markers because I want the lines to stick out only on one side of the “circle” markers. The corrective terms account for different figure sizes, x-/y-scale ranges and figure margins.
Even when zooming into the figure, the “line-ew” marks are always positioned at the right spot. It took me a while to figure out the corrective terms but it now works. :slightly_smiling_face:
I used the function f1.layout.xaxis.on_change(update1, ‘range’) to update the figure automatically each time one zooms into the figure or out again.

If anyone has a more straighforward solution, please let me know!

code:

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)
    
    # Adjustment term for chosen value for variable l = 0.0042
    fac4 = (yAxMax-yAxMin)/3
    
    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’)

And with differently oriented markers:

LOADING LIBRARIES

import numpy as np
import pandas as pd
import plotly.express as px
import plotly.graph_objs as go

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)
    
    ### Adjustment term for chosen value for variable l = 0.0042 ###
    fac4 = (yAxMax-yAxMin)/3
    
    ### Updating of figure 2 traces ###
    for i in range(0, len(dataF['sepal_length']), 1):
        f1.data[i+1].x = [dataF.loc[i,'sepal_width']           + l * np.cos(np.deg2rad(-(dataF.loc[i,'sepal_length']*500))) * fac1 * fac2 * fac3 * fac4]
        f1.data[i+1].y = [dataF.loc[i,'sepal_length']  + l * np.sin(np.deg2rad(-(dataF.loc[i,'sepal_length']*500))) * fac3 * fac4]

LOADING DATA AND SETTING SOaME FIGURE PARAMETERS

dataF = px.data.iris()
ang = 120
XaxRange = [1, 5]
YaxRange = [4, 8]
wid = 1000
heig = 800
bordL = 40
bordR = 40
bordT = 40
bordB = 40
l = 0.042

FIGURE 1, CIRCLE MARKERS

fig1 = px.scatter(dataF, 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’)

FIGURE 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

Positions of line markers

x = dataF.loc[0,‘sepal_width’] + l * np.cos(np.deg2rad(-(dataF.loc[0,‘sepal_length’]*500))) * fac1 * fac2 * fac3 * fac4
y = dataF.loc[0,‘sepal_length’] + l * np.sin(np.deg2rad(-(dataF.loc[0,‘sepal_length’]*500))) * fac3 * fac4

Addition of first trace to figure 2

df = pd.DataFrame({‘x’:,
‘y’:[y]})
fig2 = px.scatter(df, x=‘x’, y=‘y’)
fig2.update_traces(marker=dict(size=12,
symbol=“line-ew”,
angle=(dataF.loc[0,‘sepal_length’]-90),
line=dict(width=1, color=“DarkSlateGrey”)),
selector=dict(mode=“markers”),
showlegend=False)

Addition of remaining traces to figure 2

for i in range(1, len(dataF[‘sepal_length’]), 1):
x = dataF.loc[i,‘sepal_width’] + l * np.cos(np.deg2rad(-(dataF.loc[i,‘sepal_length’]*500))) * fac1 * fac2 * fac3 * fac4
y = dataF.loc[i,‘sepal_length’] + l * np.sin(np.deg2rad(-(dataF.loc[i,‘sepal_length’]*500))) * fac3 * fac4
df = pd.DataFrame({
‘x’:,
‘y’:[y]})

fig2_2 = px.scatter(df, x='x', y='y')
fig2_2.update_traces(marker=dict(size=12,
                               symbol="line-ew",
                               angle=(dataF.loc[i,'sepal_length']*500),
                               line=dict(width=1, color="DarkSlateGrey")),
                   selector=dict(mode="markers"),
                   showlegend=False)

fig2.add_trace(fig2_2.data[0])

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