Skip to content

Commit c09fb77

Browse files
committed
io: prevent RequireJS define() mismatch when using CDN plotly.js
nbviewer injects RequireJS; when plotly.js is loaded via a plain CDN <script>, it can be treated as an anonymous AMD/CommonJS module, triggering "Mismatched anonymous define()" and leaving `window.Plotly` undefined. - Wrap the CDN script tag with a guard that temporarily disables AMD/CommonJS detection and then restores it. - Drop the redundant connected renderer global-init module import. - Update renderer tests to cover the RequireJS guard and new init output.
1 parent 55597ab commit c09fb77

3 files changed

Lines changed: 117 additions & 14 deletions

File tree

plotly/io/_base_renderers.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -277,11 +277,9 @@ def activate(self):
277277
{win_config}
278278
{mathjax_config}
279279
</script>
280-
<script type="module">import \"{plotly_cdn}\"</script>
281280
""".format(
282281
win_config=_window_plotly_config,
283282
mathjax_config=_mathjax_config,
284-
plotly_cdn=plotly_cdn_url().rstrip(".js"),
285283
)
286284

287285
else:

plotly/io/_html.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,62 @@ def to_html(
260260

261261
load_plotlyjs = """\
262262
{win_config}
263+
<script type="text/javascript">\
264+
window.__PLOTLY_PY_REQUIREJS_BACKUP__ = window.__PLOTLY_PY_REQUIREJS_BACKUP__ || [];\
265+
window.__PLOTLY_PY_REQUIREJS_BACKUP__.push({{\
266+
has_define: typeof window.define === "function",\
267+
has_define_amd: typeof window.define === "function" && Object.prototype.hasOwnProperty.call(window.define, "amd"),\
268+
define_amd: typeof window.define === "function" ? window.define.amd : undefined,\
269+
has_module: Object.prototype.hasOwnProperty.call(window, "module"),\
270+
module: window.module,\
271+
has_exports: Object.prototype.hasOwnProperty.call(window, "exports"),\
272+
exports: window.exports\
273+
}});\
274+
/*\
275+
nbviewer loads RequireJS; plotly.js may register as an anonymous AMD module, triggering\
276+
\"Mismatched anonymous define()\" and leaving `Plotly` undefined. Temporarily disable\
277+
AMD/CommonJS detection while loading plotly.js from the CDN.\
278+
*/\
279+
if (typeof window.define === \"function\" && window.define.amd) {{\
280+
window.define.amd = undefined;\
281+
}}\
282+
if (typeof window.module === \"object\" && window.module && window.module.exports) {{\
283+
window.module = undefined;\
284+
}}\
285+
if (typeof window.exports === \"object\") {{\
286+
window.exports = undefined;\
287+
}}\
288+
</script>
263289
<script charset="utf-8" src="{cdn_url}" integrity="{integrity}" crossorigin="anonymous"></script>\
290+
<script type="text/javascript">\
291+
(function() {{\
292+
var backups = window.__PLOTLY_PY_REQUIREJS_BACKUP__;\
293+
if (!backups || !backups.length) {{\
294+
return;\
295+
}}\
296+
var b = backups.pop();\
297+
if (b.has_define) {{\
298+
if (b.has_define_amd) {{\
299+
window.define.amd = b.define_amd;\
300+
}} else {{\
301+
try {{ delete window.define.amd; }} catch (e) {{ window.define.amd = undefined; }}\
302+
}}\
303+
}}\
304+
if (b.has_module) {{\
305+
window.module = b.module;\
306+
}} else {{\
307+
try {{ delete window.module; }} catch (e) {{ window.module = undefined; }}\
308+
}}\
309+
if (b.has_exports) {{\
310+
window.exports = b.exports;\
311+
}} else {{\
312+
try {{ delete window.exports; }} catch (e) {{ window.exports = undefined; }}\
313+
}}\
314+
if (!backups.length) {{\
315+
try {{ delete window.__PLOTLY_PY_REQUIREJS_BACKUP__; }} catch (e) {{ window.__PLOTLY_PY_REQUIREJS_BACKUP__ = undefined; }}\
316+
}}\
317+
}})();\
318+
</script>\
264319
""".format(
265320
win_config=_window_plotly_config,
266321
cdn_url=plotly_cdn_url(),

tests/test_io/test_renderers.py

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,13 @@ def assert_offline(html):
147147
assert get_plotlyjs() in html
148148

149149

150+
def assert_requirejs_workaround(html):
151+
# nbviewer runs RequireJS; ensure we include the guard that prevents plotly.js
152+
# from registering as an anonymous AMD module (which would leave `Plotly`
153+
# undefined in the output).
154+
assert "__PLOTLY_PY_REQUIREJS_BACKUP__" in html
155+
156+
150157
def test_colab_renderer_show(fig1):
151158
pio.renderers.default = "colab"
152159

@@ -164,6 +171,7 @@ def test_colab_renderer_show(fig1):
164171
html = mock_arg1["text/html"]
165172
assert_full_html(html)
166173
assert_html_renderer_connected(html)
174+
assert_requirejs_workaround(html)
167175

168176
# check kwargs
169177
mock_kwargs = mock_call_args[1]
@@ -191,7 +199,8 @@ def test_notebook_connected_show(fig1, name, connected):
191199
# Check init display contents
192200
bundle_display_html = mock_arg1_html
193201
if connected:
194-
assert_html_renderer_connected(bundle_display_html)
202+
assert "window.PlotlyConfig" in bundle_display_html
203+
assert get_plotlyjs() not in bundle_display_html
195204
else:
196205
assert_offline(bundle_display_html)
197206

@@ -206,6 +215,9 @@ def test_notebook_connected_show(fig1, name, connected):
206215
# Check html display contents
207216
bundle_html = mock_arg1["text/html"]
208217
assert_not_full_html(bundle_html)
218+
if connected:
219+
assert_html_renderer_connected(bundle_html)
220+
assert_requirejs_workaround(bundle_html)
209221

210222
# check kwargs
211223
mock_kwargs = mock_call_args[1]
@@ -305,22 +317,60 @@ def test_repr_html(renderer):
305317
plotlyjs_content = get_plotlyjs()
306318
sri_hash = _generate_sri_hash(plotlyjs_content)
307319

320+
requirejs_workaround_pre = (
321+
'<script type="text/javascript">window.__PLOTLY_PY_REQUIREJS_BACKUP__ = '
322+
"window.__PLOTLY_PY_REQUIREJS_BACKUP__ || [];"
323+
"window.__PLOTLY_PY_REQUIREJS_BACKUP__.push({ has_define: typeof "
324+
'window.define === "function", has_define_amd: typeof window.define === '
325+
'"function" && Object.prototype.hasOwnProperty.call(window.define, "amd"), '
326+
'define_amd: typeof window.define === "function" ? window.define.amd : '
327+
"undefined, has_module: Object.prototype.hasOwnProperty.call(window, "
328+
'"module"), module: window.module, has_exports: '
329+
'Object.prototype.hasOwnProperty.call(window, "exports"), exports: '
330+
'window.exports});/*nbviewer loads RequireJS; plotly.js may register as an '
331+
'anonymous AMD module, triggering"Mismatched anonymous define()" and leaving '
332+
"`Plotly` undefined. Temporarily disableAMD/CommonJS detection while loading "
333+
'plotly.js from the CDN.*/if (typeof window.define === "function" && '
334+
"window.define.amd) { window.define.amd = undefined;}if (typeof "
335+
'window.module === "object" && window.module && window.module.exports) { '
336+
"window.module = undefined;}if (typeof window.exports === \"object\") { "
337+
"window.exports = undefined;} </script>\n "
338+
)
339+
340+
requirejs_workaround_post = (
341+
'<script type="text/javascript">(function() { var backups = '
342+
"window.__PLOTLY_PY_REQUIREJS_BACKUP__; if (!backups || !backups.length) "
343+
"{ return; } var b = backups.pop(); if (b.has_define) { "
344+
"if (b.has_define_amd) { window.define.amd = b.define_amd; } "
345+
"else { try { delete window.define.amd; } catch (e) { "
346+
"window.define.amd = undefined; } } } if (b.has_module) { "
347+
"window.module = b.module; } else { try { delete window.module; } "
348+
"catch (e) { window.module = undefined; } } if (b.has_exports) { "
349+
"window.exports = b.exports; } else { try { delete window.exports; } "
350+
"catch (e) { window.exports = undefined; } } if (!backups.length) { "
351+
"try { delete window.__PLOTLY_PY_REQUIREJS_BACKUP__; } catch (e) { "
352+
"window.__PLOTLY_PY_REQUIREJS_BACKUP__ = undefined; } }})(); </script>"
353+
)
354+
308355
template = (
309356
'<div> <script type="text/javascript">'
310-
"window.PlotlyConfig = {MathJaxConfig: 'local'};</script>\n "
311-
'<script charset="utf-8" src="'
357+
+ "window.PlotlyConfig = {MathJaxConfig: 'local'};</script>\n "
358+
+ requirejs_workaround_pre
359+
+ '<script charset="utf-8" src="'
312360
+ plotly_cdn_url()
313361
+ '" integrity="'
314362
+ sri_hash
315-
+ '" crossorigin="anonymous"></script> '
316-
'<div id="cd462b94-79ce-42a2-887f-2650a761a144" class="plotly-graph-div" '
317-
'style="height:100%; width:100%;"></div> <script type="text/javascript">'
318-
" window.PLOTLYENV=window.PLOTLYENV || {};"
319-
' if (document.getElementById("cd462b94-79ce-42a2-887f-2650a761a144"))'
320-
' { Plotly.newPlot( "cd462b94-79ce-42a2-887f-2650a761a144",'
321-
' [], {"template":{}},'
322-
' {"responsive": true} ) };'
323-
" </script> </div>"
363+
+ '" crossorigin="anonymous"></script> '
364+
+ requirejs_workaround_post
365+
+ " "
366+
+ '<div id="cd462b94-79ce-42a2-887f-2650a761a144" class="plotly-graph-div" '
367+
+ 'style="height:100%; width:100%;"></div> <script type="text/javascript">'
368+
+ " window.PLOTLYENV=window.PLOTLYENV || {};"
369+
+ ' if (document.getElementById("cd462b94-79ce-42a2-887f-2650a761a144"))'
370+
+ ' { Plotly.newPlot( "cd462b94-79ce-42a2-887f-2650a761a144",'
371+
+ ' [], {"template":{}},'
372+
+ ' {"responsive": true} ) };'
373+
+ " </script> </div>"
324374
)
325375
if "text/html" in bundle:
326376
str_bundle = bundle["text/html"]

0 commit comments

Comments
 (0)