Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add MathJax / Latex support for offline plot/iplot/FigureWidget #1243

Merged
merged 8 commits into from
Oct 30, 2018
8 changes: 8 additions & 0 deletions js/src/Figure.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
var widgets = require("@jupyter-widgets/base");
var _ = require("lodash");

window.PlotlyConfig = {MathJaxConfig: 'local'};
var Plotly = require("plotly.js/dist/plotly");
var PlotlyIndex = require("plotly.js/src/lib/index");
var semver_range = "^" + require("../package.json").version;
Expand Down Expand Up @@ -720,6 +722,12 @@ var FigureView = widgets.DOMWidgetView.extend({
this.model.on("change:_py2js_animate",
this.do_animate, this);

// MathJax configuration
// ---------------------
if (window.MathJax) {
MathJax.Hub.Config({SVG: {font: "STIX-Web"}});
}

// Get message ids
// ---------------------
var layout_edit_id = this.model.get("_last_layout_edit_id");
Expand Down
114 changes: 90 additions & 24 deletions plotly/offline/offline.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,29 @@ def get_plotlyjs():
return plotlyjs


def _build_resize_script(plotdivid):
resize_script = (
'<script type="text/javascript">'
'window.addEventListener("resize", function(){{'
'Plotly.Plots.resize(document.getElementById("{id}"));}});'
'</script>'
).format(id=plotdivid)
return resize_script


# Build script to set global PlotlyConfig object. This must execute before
# plotly.js is loaded.
_window_plotly_config = """\
<script type="text/javascript">\
window.PlotlyConfig = {MathJaxConfig: 'local'};\
</script>"""

_mathjax_config = """\
<script type="text/javascript">\
if (window.MathJax) {MathJax.Hub.Config({SVG: {font: "STIX-Web"}});}\
</script>"""


def get_image_download_script(caller):
"""
This function will return a script that will download an image of a Plotly
Expand Down Expand Up @@ -168,23 +191,26 @@ def init_notebook_mode(connected=False):
if connected:
# Inject plotly.js into the output cell
script_inject = (
''
'{win_config}'
'{mathjax_config}'
'<script>'
'requirejs.config({'
'paths: { '
'requirejs.config({{'
'paths: {{ '
# Note we omit the extension .js because require will include it.
'\'plotly\': [\'https://cdn.plot.ly/plotly-latest.min\']},'
'});'
'\'plotly\': [\'https://cdn.plot.ly/plotly-latest.min\']}},'
'}});'
'if(!window.Plotly) {{'
'require([\'plotly\'],'
'function(plotly) {window.Plotly=plotly;});'
'function(plotly) {{window.Plotly=plotly;}});'
'}}'
'</script>'
)
).format(win_config=_window_plotly_config,
mathjax_config=_mathjax_config)
else:
# Inject plotly.js into the output cell
script_inject = (
''
'{win_config}'
'{mathjax_config}'
'<script type=\'text/javascript\'>'
'if(!window.Plotly){{'
'define(\'plotly\', function(require, exports, module) {{'
Expand All @@ -195,7 +221,9 @@ def init_notebook_mode(connected=False):
'}});'
'}}'
'</script>'
'').format(script=get_plotlyjs())
'').format(script=get_plotlyjs(),
win_config=_window_plotly_config,
mathjax_config=_mathjax_config)

display_bundle = {
'text/html': script_inject,
Expand Down Expand Up @@ -450,21 +478,11 @@ def iplot(figure_or_data, show_link=True, link_text='Export to plot.ly',
ipython_display.display(ipython_display.HTML(script))


def _build_resize_script(plotdivid):
resize_script = (
'<script type="text/javascript">'
'window.addEventListener("resize", function(){{'
'Plotly.Plots.resize(document.getElementById("{id}"));}});'
'</script>'
).format(id=plotdivid)
return resize_script


def plot(figure_or_data, show_link=True, link_text='Export to plot.ly',
validate=True, output_type='file', include_plotlyjs=True,
filename='temp-plot.html', auto_open=True, image=None,
image_filename='plot_image', image_width=800, image_height=600,
config=None):
config=None, include_mathjax=False):
""" Create a plotly graph locally as an HTML document or string.

Example:
Expand Down Expand Up @@ -558,6 +576,22 @@ def plot(figure_or_data, show_link=True, link_text='Export to plot.ly',
config (default=None) -- Plot view options dictionary. Keyword arguments
`show_link` and `link_text` set the associated options in this
dictionary if it doesn't contain them already.
include_mathjax (False | 'cdn' | path - default=False) --
Specifies how the MathJax.js library is included in the output html
file or div string. MathJax is required in order to display labels
with LaTeX typesetting.

If False, no script tag referencing MathJax.js will be included in the
output. HTML files generated with this option will not be able to
display LaTeX typesetting.

If 'cdn', a script tag that references a MathJax CDN location will be
included in the output. HTML files generated with this option will be
able to display LaTeX typesetting as long as they have internet access.

If a string that ends in '.js', a script tag is included that
references the specified path. This approach can be used to point the
resulting HTML file to an alternative CDN.
"""
if output_type not in ['div', 'file']:
raise ValueError(
Expand All @@ -577,31 +611,61 @@ def plot(figure_or_data, show_link=True, link_text='Export to plot.ly',
figure_or_data, config, validate,
'100%', '100%', global_requirejs=False)

# Build resize_script
resize_script = ''
if width == '100%' or height == '100%':
resize_script = _build_resize_script(plotdivid)

# Process include_plotlyjs and build plotly_js_script
include_plotlyjs_orig = include_plotlyjs
if isinstance(include_plotlyjs, six.string_types):
include_plotlyjs = include_plotlyjs.lower()

if include_plotlyjs == 'cdn':
plotly_js_script = """\
plotly_js_script = _window_plotly_config + """\
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>"""
elif include_plotlyjs == 'directory':
plotly_js_script = '<script src="plotly.min.js"></script>'
plotly_js_script = (_window_plotly_config +
'<script src="plotly.min.js"></script>')
elif (isinstance(include_plotlyjs, six.string_types) and
include_plotlyjs.endswith('.js')):
plotly_js_script = '<script src="{url}"></script>'.format(
url=include_plotlyjs)
plotly_js_script = (_window_plotly_config +
'<script src="{url}"></script>'.format(
url=include_plotlyjs_orig))
elif include_plotlyjs:
plotly_js_script = ''.join([
_window_plotly_config,
'<script type="text/javascript">',
get_plotlyjs(),
'</script>',
])
else:
plotly_js_script = ''

# Process include_mathjax and build mathjax_script
include_mathjax_orig = include_mathjax
if isinstance(include_mathjax, six.string_types):
include_mathjax = include_mathjax.lower()

if include_mathjax == 'cdn':
mathjax_script = (
'<script src="{url}?config=TeX-AMS-MML_SVG"></script>'.format(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add STIX_Web font config from mathjax_config. This way appearance of exported html will match iplot and FigureWidget

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there an example on how to use include_mathjax='cdn' in a Jupyter notebook? For instance where would I enable LaTeX in plotly in the following block?

import plotly.offline as py
py.init_notebook_mode()
import plotly.graph_objs as go
from ipywidgets import interact
fig = go.FigureWidget()

I don't seem to get LaTeX working in the resulting interactive plots.
Thank you.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @dealmeidavf ,

include_mathajx is only needed for plotly.offline.plot. For FigureWidget and plotly.offline.iplot Jupyter's internal MathJax should be used automatically.

import plotly.graph_objs as go
go.FigureWidget(layout={'title': '$\LaTeX$'})

newplot 16

Could you open a new issue with your browser info, and whether you're using the classic notebook of JupyterLab? There are some issues with FireFox still, so try it out with Chrome if you haven't. Also, in JupyterLab you sometimes need to run a markdown cell containing LaTeX first so that the MathJax is initialized.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you. I am using Jupyter notebook in the anaconda navigator on a Mac. LaTeX does not work on a Mac Firefox. It works on Safari. I am new to plotly but a user of matplotlib. The interactive plots in plotly using ipywidgets are amazing!

url=('https://cdnjs.cloudflare.com'
'/ajax/libs/mathjax/2.7.5/MathJax.js')))
elif (isinstance(include_mathjax, six.string_types) and
include_mathjax.endswith('.js')):
mathjax_script = '<script src="{url}"></script>'.format(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs ?config=TeX-AMS-MML_SVG as well

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, add STIX_Web font config from mathjax_config

url=include_mathjax_orig)
elif not include_mathjax:
mathjax_script = ''
else:
raise ValueError("""\
Invalid value of type {typ} received as the include_mathjax argument
Received value: {val}

include_mathjax may be specified as False, 'cdn', or a string ending with '.js'
""".format(typ=type(include_mathjax), val=include_mathjax))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use val=repr(include_mathjax) to be a bit more clear


if output_type == 'file':
with open(filename, 'w') as f:
if image:
Expand All @@ -624,6 +688,7 @@ def plot(figure_or_data, show_link=True, link_text='Export to plot.ly',
'<html>',
'<head><meta charset="utf-8" /></head>',
'<body>',
mathjax_script,
plotly_js_script,
plot_html,
resize_script,
Expand All @@ -650,6 +715,7 @@ def plot(figure_or_data, show_link=True, link_text='Export to plot.ly',

return ''.join([
'<div>',
mathjax_script,
plotly_js_script,
plot_html,
resize_script,
Expand Down
90 changes: 90 additions & 0 deletions plotly/tests/test_core/test_offline/test_offline.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,23 @@

PLOTLYJS = plotly.offline.get_plotlyjs()

plotly_config_script = """\
<script type="text/javascript">\
window.PlotlyConfig = {MathJaxConfig: 'local'};</script>"""

cdn_script = ('<script src="https://cdn.plot.ly/plotly-latest.min.js">'
'</script>')

directory_script = '<script src="plotly.min.js"></script>'


mathjax_cdn = ('https://cdnjs.cloudflare.com'
'/ajax/libs/mathjax/2.7.5/MathJax.js')

mathjax_cdn_script = ('<script src="%s?config=TeX-AMS-MML_SVG"></script>' %
mathjax_cdn)


class PlotlyOfflineBaseTestCase(TestCase):
def tearDown(self):
# Some offline tests produce an html file. Make sure we clean up :)
Expand Down Expand Up @@ -76,6 +87,7 @@ def test_default_plot_generates_expected_html(self):

self.assertTrue(x_data in html and y_data in html) # data in there
self.assertIn(layout_json, html) # so is layout
self.assertIn(plotly_config_script, html) # so is config
self.assertIn(PLOTLYJS, html) # and the source code
# and it's an <html> doc
self.assertTrue(html.startswith('<html>') and html.endswith('</html>'))
Expand All @@ -89,6 +101,8 @@ def test_including_plotlyjs_truthy_html(self):
include_plotlyjs=include_plotlyjs,
output_type='file',
auto_open=False))

self.assertIn(plotly_config_script, html)
self.assertIn(PLOTLYJS, html)
self.assertNotIn(cdn_script, html)
self.assertNotIn(directory_script, html)
Expand All @@ -101,6 +115,8 @@ def test_including_plotlyjs_truthy_div(self):
fig,
include_plotlyjs=include_plotlyjs,
output_type='div')

self.assertIn(plotly_config_script, html)
self.assertIn(PLOTLYJS, html)
self.assertNotIn(cdn_script, html)
self.assertNotIn(directory_script, html)
Expand All @@ -114,6 +130,8 @@ def test_including_plotlyjs_false_html(self):
include_plotlyjs=include_plotlyjs,
output_type='file',
auto_open=False))

self.assertNotIn(plotly_config_script, html)
self.assertNotIn(PLOTLYJS, html)
self.assertNotIn(cdn_script, html)
self.assertNotIn(directory_script, html)
Expand All @@ -124,6 +142,7 @@ def test_including_plotlyjs_false_div(self):
fig,
include_plotlyjs=include_plotlyjs,
output_type='div')
self.assertNotIn(plotly_config_script, html)
self.assertNotIn(PLOTLYJS, html)
self.assertNotIn(cdn_script, html)
self.assertNotIn(directory_script, html)
Expand All @@ -135,6 +154,7 @@ def test_including_plotlyjs_cdn_html(self):
include_plotlyjs=include_plotlyjs,
output_type='file',
auto_open=False))
self.assertIn(plotly_config_script, html)
self.assertNotIn(PLOTLYJS, html)
self.assertIn(cdn_script, html)
self.assertNotIn(directory_script, html)
Expand All @@ -145,6 +165,7 @@ def test_including_plotlyjs_cdn_div(self):
fig,
include_plotlyjs=include_plotlyjs,
output_type='div')
self.assertIn(plotly_config_script, html)
self.assertNotIn(PLOTLYJS, html)
self.assertIn(cdn_script, html)
self.assertNotIn(directory_script, html)
Expand All @@ -157,6 +178,7 @@ def test_including_plotlyjs_directory_html(self):
fig,
include_plotlyjs=include_plotlyjs,
auto_open=False))
self.assertIn(plotly_config_script, html)
self.assertNotIn(PLOTLYJS, html)
self.assertNotIn(cdn_script, html)
self.assertIn(directory_script, html)
Expand All @@ -176,6 +198,7 @@ def test_including_plotlyjs_directory_div(self):
output_type='div',
auto_open=False)

self.assertIn(plotly_config_script, html)
self.assertNotIn(PLOTLYJS, html)
self.assertNotIn(cdn_script, html)
self.assertIn(directory_script, html)
Expand Down Expand Up @@ -279,3 +302,70 @@ def test_plotlyjs_version(self):

self.assertEqual(expected_version,
plotly.offline.get_plotlyjs_version())

def test_include_mathjax_false_html(self):
html = self._read_html(plotly.offline.plot(
fig,
include_mathjax=False,
output_type='file',
auto_open=False))

self.assertIn(plotly_config_script, html)
self.assertIn(PLOTLYJS, html)
self.assertNotIn(mathjax_cdn_script, html)

def test_include_mathjax_false_div(self):
html = plotly.offline.plot(
fig,
include_mathjax=False,
output_type='div')

self.assertIn(plotly_config_script, html)
self.assertIn(PLOTLYJS, html)
self.assertNotIn(mathjax_cdn_script, html)

def test_include_mathjax_cdn_html(self):
html = self._read_html(plotly.offline.plot(
fig,
include_mathjax='cdn',
output_type='file',
auto_open=False))

self.assertIn(plotly_config_script, html)
self.assertIn(PLOTLYJS, html)
self.assertIn(mathjax_cdn_script, html)

def test_include_mathjax_cdn_div(self):
html = plotly.offline.plot(
fig,
include_mathjax='cdn',
output_type='div')

self.assertIn(plotly_config_script, html)
self.assertIn(PLOTLYJS, html)
self.assertIn(mathjax_cdn_script, html)

def test_include_mathjax_path_html(self):
other_cdn = 'http://another/cdn/MathJax.js'
html = self._read_html(plotly.offline.plot(
fig,
include_mathjax=other_cdn,
output_type='file',
auto_open=False))

self.assertIn(plotly_config_script, html)
self.assertIn(PLOTLYJS, html)
self.assertNotIn(mathjax_cdn_script, html)
self.assertIn(other_cdn, html)

def test_include_mathjax_path_div(self):
other_cdn = 'http://another/cdn/MathJax.js'
html = plotly.offline.plot(
fig,
include_mathjax=other_cdn,
output_type='div')

self.assertIn(plotly_config_script, html)
self.assertIn(PLOTLYJS, html)
self.assertNotIn(mathjax_cdn_script, html)
self.assertIn(other_cdn, html)