2

In a plotly dash app, I am adding a text annotation with a clickable link that has a hash in it.

topic = "Australia"  # might contain spaces
hashtag = "#" + topic

annotation_text=f"<a href=\"https://twitter.com/search?q={urllib.parse.quote_plus(hashtag)}&src=typed_query&f=live\">{topic}</a>"

I need the output html to contain "https://twitter.com/search?q=%23Australia&src=typed_query&f=live" but I can't get the "#" character to encode properly. It gets double encoded to %2523.


Minimal Working Example:

import dash
from dash.dependencies import Input, Output
import plotly.express as px
import urllib.parse

df = px.data.gapminder()
all_continents = df.continent.unique()

app = dash.Dash(__name__)

app.layout = dash.html.Div([
    dash.dcc.Checklist(
        id="checklist",
        options=[{"label": x, "value": x}
                 for x in all_continents],
        value=all_continents[4:],
        labelStyle={'display': 'inline-block'}
    ),
    dash.dcc.Graph(id="line-chart"),
])


@app.callback(
    Output("line-chart", "figure"),
    [Input("checklist", "value")])
def update_line_chart(continents):
    mask = df.continent.isin(continents)
    fig = px.line(df[mask],
                  x="year", y="lifeExp", color='country')
    annotations = []
    df_last_value = df[mask].sort_values(['country', 'year', ]).drop_duplicates('country', keep='last')
    for topic, year, last_lifeExp_value in zip(df_last_value.country, df_last_value.year, df_last_value.lifeExp):
        hashtag = "#" + topic
        annotations.append(dict(xref='paper', x=0.95, y=last_lifeExp_value,
                                xanchor='left', yanchor='middle',
                                text=f"<a href=\"https://twitter.com/search?q={urllib.parse.quote_plus(hashtag)}&src=typed_query&f=live\">{topic}</a>",
                                # text=f"<a href=\"https://twitter.com/search?q=#{urllib.parse.quote_plus(topic)}&src=typed_query&f=live\">{topic}</a>",

                                font=dict(family='Arial',
                                          size=16),
                                showarrow=False))

    fig.update_layout(annotations=annotations)
    return fig


app.run_server(debug=True)

When you run this and click on the text "Australia" at the end of the line graph, it should open up a twitter search page for #Australia.


What I've tried:

  1. just using a bare "#" character: text=f"<a href=\"https://twitter.com/search?q=#{urllib.parse.quote_plus(topic)}&src=typed_query&f=live\">{topic}</a>"

Here, the # character is not encoded as %23 in the output, which results in a broken link for twitter.

https://twitter.com/search?q=#mytopic&amp;src=typed_query&amp;f=live link

  1. using quote_plus on the hashtag text=f"<a href=\"https://twitter.com/search?q=#{urllib.parse.quote_plus(hashtag)}&src=typed_query&f=live\">{topic}</a>"

Here, the %23 (the encoded # character) gets encoded again, resulting in %2523 in the output.

https://twitter.com/search?q=%2523mytopic&amp;src=typed_query&amp;f=livelink


How do I get it to correctly encode the # (to %23) so I get

href="https://twitter.com/search?q=%23mytopic&amp;src=typed_query&amp;f=live

Pranab
  • 2,207
  • 5
  • 30
  • 50
  • 1
    I tested your code and got the same result. It seems like the query part of the uri (after the `?`) is not escaped as it should be, and the whole is encoded as if there were no `?`. This is probably a bug in the the annotation text rendering. A workaround would be to override it via javascript. – EricLavault Nov 24 '21 at 14:49
  • 1
    It's a known bug: [plotly/plotly.js#4084](https://github.com/plotly/plotly.js/issues/4084) – aaron Dec 03 '21 at 06:04

1 Answers1

1

It's a known bug: plotly/plotly.js#4084

Offending line in plotly.js:

nodeSpec.href = encodeURI(decodeURI(href));
  • decodeURI doesn't decode %23 (decodeURIComponent does).
  • encodeURI doesn't encode # but encodes % (encodeURIComponent does both).

More on that: What is the difference between decodeURIComponent and decodeURI?

Workaround

You can override the built-in encodeURI to revert the encoding of % in %23:

app._inline_scripts.append('''
_encodeURI = encodeURI;
encodeURI = uri => _encodeURI(uri).replace('%2523', '%23');
''')
aaron
  • 39,695
  • 6
  • 46
  • 102