Well, apparently, it is possible - but boy, it was difficult to get all this info in place, so as to get a working example ...
First, these are the versions I have installed:
$ python3 --version
Python 3.8.10
$ pip3 list | grep 'jupyter \|nbextensions\|plotly'
jupyter 1.0.0
jupyter-contrib-nbextensions 0.5.1
jupyter-nbextensions-configurator 0.4.1
plotly 5.2.2
And some key points:
- While, in principle, it might be possible to use a link to javascript inside a Markdown link - in practice, it is not: Jupyter Markdown cell links are sanitized, and especially if they use colons, no
href
is propagated to them either (tried also urlencoding there, and it didn't work).
- That means, that the only solution to this, is to define your Jupyter cell as Code (IPython kernel in this case), then use the "magic" command
%%html
, to be able to output verbatim HTML and JavaScript
- The main trick here is to use
Jupyter.notebook.kernel.execute
JavaScript function, to call kernel (here IPython) code from JavaScript; however, note that Document the IPython.notebook.kernel.execute function · Issue #2219 · jupyter/notebook is kinda lacking;
- With this approach, whenever you use
figure.show()
or pio.show(figure)
methods, something in the JavaScript/Python connection will break, and the figure will not be able to update - even if otherwise, the code seems to run (as evidenced by console.log
traces); the only function that seems to work in this context is display(figure)
Note that in this example:
- There is a Plotly/IPython interactive dropdown, which independently changes the range of the plot
- There is a Python function that adjusts the range, and here it is being called both from Python and from JavaScript (links)
That being said, here is what should be a working example; in the first cell, with the Python code that produces the graphs (so, defined as a code cell):
import plotly.graph_objs as go
import pandas as pd
from plotly.subplots import make_subplots
# import plotly.io as pio # https://plotly.com/python/getting-started-with-chart-studio/
from IPython.display import display, HTML
df = pd.read_csv("https://raw.githubusercontent.com/plotly/datasets/master/volcano.csv")
figSubs = go.FigureWidget(
make_subplots(rows=2, cols=1, specs = [[{}], [{}]], vertical_spacing = 0.05)
)
figSubs.add_trace(
go.Scatter(mode='lines+markers', x=df["V1"], y=df["V55"], name='Test1', marker={'color': 'red'}, xaxis="x1"),
row=1, col=1
)
figSubs.add_trace(
go.Scatter(mode='lines+markers', x=[0, 1, 2, 3, 4], y=[0, 990, 980, 970, 960], name='Test21', marker={'color': 'blue'}, xaxis="x1"),
row=2, col=1
)
figSubs.add_trace(
go.Scatter(mode='lines+markers', x=[0, 1, 2, 3, 4], y=[0, 980, 970, 960, 950], name='Test22', marker={'color': 'violet'}, xaxis="x1"),
row=2, col=1
)
figSubs.update_layout(margin=go.layout.Margin(l=20,t=10,b=10,pad=4))
figSubs.update_yaxes(zeroline=True,showline=True,zerolinewidth=1,zerolinecolor="#000", row=1, col=1)
figSubs.update_yaxes(zeroline=True,showline=True,zerolinewidth=1,zerolinecolor="#000", row=2, col=1)
# Add dropdown
figSubs.update_layout(
updatemenus=[
dict(
buttons=list([
dict(
args=[{"yaxis.range": [0, 1000], "yaxis.autorange": False, "row": 1, "col": 1}],
label="[0, 1000]",
method="relayout"
),
dict(
args=[{"yaxis.range": [100, 200], "yaxis.autorange": False}],
label="[100, 200]",
method="relayout"
)
]),
direction="down",
pad={"r": 10, "t": 10},
showactive=True,
x=0.1,
xanchor="left",
y=1.12,
yanchor="top"
),
]
)
# the Python function to adjust the Y range of the first plot - which is also called from JavaScript
def PsetMyYRange(ymin, ymax, dodraw=True):
figSubs['layout']['yaxis'].update(range=[ymin, ymax], autorange=False)
#figSubs.update_yaxes(range=[ymin, ymax]) # changes both!
#figSubs.update_layout(margin=go.layout.Margin(l=200,t=100,b=100,pad=40))
#figSubs.show() # do NOT call this, else cannot manupulate the plot via JavaScript calls of this function later on!
if dodraw:
display(figSubs) #MUST have this to update the plot from JavaScript->Python; note with Plotly in a Jupyter extension, there is no `Plotly` javascript object accessible on the page!
return "{}; {}; {}".format(figSubs, ymin, ymax) # just for the console.log printout
PsetMyYRange(110,120,dodraw=False) # call once to make sure it also works from here; but don't "draw", else we get two plots
#figSubs.show() # NOTE: .show() at end, will prevent the PsetMyYRange being able to redraw!
#pio.show(figSubs) # NOTE: also pio.show() at end, will prevent the PsetMyYRange being able to redraw!
display(figSubs) # ... display() works fine however
And the second cell, which would have been the "Markdown links" cell as desired in the OP, again has to be a Code cell, with the %%html
magic command:
%%html
<script type='text/javascript'>
window.executePython = function(python) {
return new Promise((resolve, reject) => {
var callbacks = {
iopub: {
output: (data) => resolve(data.content.text.trim())
}
};
Jupyter.notebook.kernel.execute(`print(${python})`, callbacks);
});
}
function setMyYRange(ymin, ymax){
// NONE of the below quite works - we must call via Promise:
//objstring = IPython.notebook.kernel.execute("global figSubs; print(figSubs)");
//prevstring = IPython.notebook.kernel.execute("print(Jupyter.notebook.get_prev_cell())");
//runstring = "global figSubs; figSubs['layout']['yaxis'].update(range=["+ymin+", "+ymax+"], autorange=False)";
//console.log("setMyYRange " + ymin + " " + ymax + " ... " + objstring + " ... " + prevstring + " ... " + runstring);
//IPython.notebook.kernel.execute(runstring);
// the only thing needed for the code to work:
window.executePython("PsetMyYRange("+ymin+","+ymax+")")
.then(result => console.log(result));
}
</script>
<a onclick="javascript:setMyYRange(0,1000);" href="javascript:void(0);">here (0,1000)</a>
<a onclick="javascript:setMyYRange(100,200);" href="javascript:void(0);">here (100,200)</a>