Hyperlink to markers on map


#1

I’m having difficulties inserting a hyperlink to the map markers.

This is the map I created of NYC schools: map_of_schools

my code:

schools=[]

for i in interest_areas:
    df_sub=df4[df4.interest1==i]
    school = dict(
        type = 'scattermapbox',
        locationmode = 'USA-states',
        lon = df_sub['Longitude'],
        lat = df_sub['Latitude'],
        sizemode = 'diameter',
        mode='markers',
        marker = dict(
            size = (df_sub['Performance']*10)**1.8,
            line = dict(width = 2,color = 'black')
        ),
        # legend name
        name = df_sub['interest1'].values[0], #legend interest name
        hovertext=df_sub['text'],
        hoverlabel=dict(namelength=-1),
        hoverinfo='name+text'
    )
    schools.append(school)

layout = dict(
    autosize=True,
    hovermode='closest',
    hoverdistance=2,
    title='NYC High Schools<br>(bubble sized according to school performance relative to city average)',
    showlegend = True,
    mapbox=dict(
        accesstoken=mapbox_access_token,
        bearing=25,
        style='light',
        center=dict(
            lat=40.705005,
            lon=-73.93879
        ),
        pitch=20,
        zoom=11.5,
    ),

)

fig = dict( data=schools, layout=layout)
py.iplot( fig, validate=False, filename='bubble-map-nyc-schools')

I tried adding “<a href” text to the marker itself, but it won’t translate to a clickable hyperlink.

The Plotly community appears to suggest Click Events as a solution, but I’m not sure how to combine the code in the example with my code above. I’m also not sure how to make a Click Event a hyperlink, so that clicking on a marker (school) will result in opening a new website or new tab.

Does anyone know a workaround for having clickable markers on a scattermapbox?


#2

Hi @adamschroeder,

I don’t think it’s possible to add a true hyperlink to a marker trace. If you’re working in the Jupyter Notebook you could simulate the effect by using the on_click callback mechanism of a FigureWidget as you mentioned. To actually open the browser tab you could have your callback use the Python webbrowser module (https://docs.python.org/3/library/webbrowser.html).

Here’s an example of that approach:

import webbrowser
import pandas as pd
import plotly.graph_objs as go
df = pd.DataFrame({'x': [1, 2, 3],
                   'y': [1, 3, 2],
                   'link': ['https://google.com', 'https://bing.com', 'https://duckduckgo.com']})

fig = go.FigureWidget(layout={'hovermode': 'closest'})
scatter = fig.add_scatter(x=df.x, y=df.y, mode='markers', marker={'size': 20})

def do_click(trace, points, state):
    if points.point_inds:
        ind = points.point_inds[0]
        url = df.link.iloc[ind]
        webbrowser.open_new_tab(url)
        
scatter.on_click(do_click)
fig

Hope that helps!
-Jon


#3

Thank you Jon, @jmmease . I was able to make it work thanks to your example (full code below). The only challenge I have left to overcome is to make it work on plotly. The on_click callback doesn’t work for me when I try to run code through plotly:

py.iplot(fig)

The map appears fine, but the the markers can’t be clicked anymore.

If I leave the FigureWidget (fig) outside py.plotly as such:

school.on_click(do_click)
fig

the clickable function works beautifully.

Do you know how I can implement this in plotly offline or online?

fig = go.FigureWidget(layout = dict(
    autosize=True,
    hovermode='closest',
    hoverdistance=2,
    title='NYC High Schools<br>(bubble sized according to school performance relative to city average)',
    showlegend = True,
    mapbox=dict(
        accesstoken=mapbox_access_token,
        bearing=25,
        style='light',
        center=dict(
            lat=40.705005,
            lon=-73.93879
        ),
        pitch=20,
        zoom=11.5,
    ),
))

def do_click(trace, points, state):
    if points.point_inds:
        ind = points.point_inds[0]
        url = df.link.iloc[ind]
        webbrowser.open_new_tab(url)

school=[]
for i in interest_areas:
    df_sub=df[df.interest1==i]
    school = fig.add_scattermapbox(
        lon = df_sub['Longitude'],
        lat = df_sub['Latitude'],
        mode='markers',
        marker = dict(
            size = (df_sub['Performance']*10)**1.8
                     ),
        hovertext=df_sub['text'],
        hoverlabel=dict(namelength=-1),
        hoverinfo='name+text',
        name = df_sub['interest1'].values[0], #legend interest name

    )
    school.on_click(do_click)
fig

If it’s not possible in plotly, I’ll try to find a way to share my jupyter notebook with the public in a way that the callback click_on still works. (Ideally, NYC parents would use this map to find a good high school for their children)

Thank you,


#4

Hi @adamschroeder,

Yes, unfortunately this FigureWidget approach will only work in the Jupyter Notebook with a live running python kernel. I don’t think there is a way to host a figure with custom callbacks on the online Chart Studio.

I don’t know if this would help with your particular use case, but it should be possible to output a standalone HTML file that contains the figure and the callback specified as a JavaScript function. If approach would be helpful, I can create a new version of the example above (probably tomorrow).

-Jon


#5

Thank you @jmmease,
that would be very helpful if you could create it. In the meantime, I’m trying other things like using kaggle or brython, but nothing yet.

My goal is to send the html link to parents in my organization and around the city, so they could use it in their search for high-performing high schools. Ideally, it would look like this (high_school_map), but with the clickable function you helped me create. And I would update the map and its data on a bi-annual basis, as the Department of Education releases new stats.

Thank you Jon,


#6

Hi @adamschroeder,

Here’s an example of the approach I mentioned

from plotly.offline import plot
import pandas as pd
import plotly.graph_objs as go

# mapbox_access_token = '...'

# Build scattermapbox trace and store URL's in the customdata
# property. The values of this list will be easy to get to in the
# JavaScript callback below
data = [
    go.Scattermapbox(
        lat=['45.5017', '45.7', '45.3'],
        lon=['-73.5673', '-73.7', '-73.3'],
        mode='markers',
        marker=dict(
            size=14
        ),
        name='mapbox 1',
        text=['Montreal'],
        customdata=['https://google.com', 'https://bing.com', 'https://duckduckgo.com']
    )
]

# Build layout
layout = go.Layout(
    hovermode='closest',
    mapbox=dict(
    accesstoken=mapbox_access_token,
    bearing=0,
    center=dict(
        lat=45,
        lon=-73
    ),
    pitch=0,
    zoom=6,
    )
)

# Build Figure
fig = go.Figure(
    data=data,
    layout=layout,
)

# Get HTML representation of plotly.js and this figure
plot_div = plot(fig, output_type='div', include_plotlyjs=True)

# Get id of html div element that looks like
# <div id="301d22ab-bfba-4621-8f5d-dc4fd855bb33" ... >
res = re.search('<div id="([^"]*)"', plot_div)
div_id = res.groups()[0]

# Build JavaScript callback for handling clicks
# and opening the URL in the trace's customdata 
js_callback = """
<script>
var plot_element = document.getElementById("{div_id}");
plot_element.on('plotly_click', function(data){{
    console.log(data);
    var point = data.points[0];
    if (point) {{
        console.log(point.customdata);
        window.open(point.customdata);
    }}
}})
</script>
""".format(div_id=div_id)

# Build HTML string
html_str = """
<html>
<body>
{plot_div}
{js_callback}
</body>
</html>
""".format(plot_div=plot_div, js_callback=js_callback)

# Write out HTML file
with open('hyperlink_fig.html', 'w') as f:
    f.write(html_str)

This will produce an html file in the current directory named hyperlink_fig.html. This file is standalone and doesn’t require Python and it’s something you could email out or host on your own website.

Hope that helps or at least gives you some more ideas :slight_smile:

-Jon


#7

Thank you very much @jmmease, this works perfect.

If I’d like to do similar maps and interactive visualizations with callbacks in the future, would you recommend I learn Dash or try to learn basic JavaScript so I can do what you did?


#8

Great!

Dash is definitely the more mature solution and you get pretty much unlimited control since you can write all of the callbacks/backend login in Python, and you can use a really nice suite of widget controls. But this does require the webapp to be hosted on a webserver somewhere (The dash docs have instructions for hosting on Heroku). The only real advantage of the approach I used above is that you can create standalone HTML files that don’t require a server.

-Jon