Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,17 +95,23 @@ While non-minified source files may contain characters outside UTF-8, it is reco
> Please note that as of v2 the "plotly-latest" outputs (e.g. https://cdn.plot.ly/plotly-latest.min.js) will no longer be updated on the CDN, and will stay at the last v1 patch v1.58.5. Therefore, to use the CDN with plotly.js v2 and higher, you must specify an exact plotly.js version.

### MathJax
You can load either version two or version three of MathJax files. For example:
plotly.js supports using MathJax v3 or v4 to render mathematical expressions within plots.

MathJax is not included in the plotly.js bundle, so it must be loaded separately as a script on the webpage.

For MathJax v4:
```html
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-AMS-MML_SVG.js"></script>
<script src="https://cdn.jsdelivr.net/npm/mathjax@4.1.2/tex-svg.js"></script>
```

For MathJax v3:
```html
<script src="https://cdn.jsdelivr.net/npm/mathjax@3.2.2/es5/tex-svg.js"></script>
```

> When using MathJax version 3, it is also possible to use `chtml` output on the other parts of the page in addition to `svg` output for the plotly graph.
Please refer to `devtools/test_dashboard/index-mathjax3chtml.html` to see an example.
When MathJax is available, any string contained within `$..$` signs will be rendered using the MathJax engine. This applies to plot titles, axis labels, tick label text, and annotations.

plotly.js's use of the MathJax `svg` renderer should not prevent using other MathJax renderers elsewhere on the page, nor will it affect the global MathJax config. See `devtools/test_dashboard/index-mathjax3chtml.html` for an example of a webpage which uses the MathJax `chtml` renderer in the main page, and also displays Plotly charts containing MathJax equations rendered as `svg`.

### Need to have several WebGL graphs on a page?
You may simply load the [virtual-webgl](https://github.com/greggman/virtual-webgl) script for WebGL 1 (not WebGL 2) before loading other scripts.
Expand Down
2 changes: 1 addition & 1 deletion devtools/test_dashboard/index-mathjax3.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ <h4>no MathJax: Apple: $2, Orange: $3</h4>
};
</script>
<script src="../../node_modules/@plotly/mathjax-v3/es5/tex-svg.js"></script>
<script charset="utf-8" id="source" src="../../build/plotly.js" type="module"></script>
<script charset="utf-8" id="source" src="../../build/plotly.js"></script>
<script charset="utf-8" src="../../build/test_dashboard-bundle.js"></script>
</body>
</html>
9 changes: 2 additions & 7 deletions devtools/test_dashboard/index-mathjax3chtml.html
Original file line number Diff line number Diff line change
Expand Up @@ -49,20 +49,15 @@
options.format = svgDocument.inputJax[0].name;
return svgDocument.convert(math, options);
};
/*
MathJax.tex2svgPromise = (math, options = {}) => {
options.format = svgDocument.inputJax[0].name;
return mathjax.handleRetriesFor(() => svgDocument.convert(math, options));
};
*/

MathJax.svgStylesheet = () => svgOutput.styleSheet(svgDocument);
}
}
};
</script>
<script src="../../node_modules/@plotly/mathjax-v3/es5/tex-chtml.js"></script>

<script charset="utf-8" id="source" src="../../build/plotly.js" type="module"></script>
<script charset="utf-8" id="source" src="../../build/plotly.js"></script>
<script charset="utf-8" src="../../build/test_dashboard-bundle.js"></script>
</body>
</html>
2 changes: 1 addition & 1 deletion devtools/test_dashboard/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
<!-- Update UI elements if strict mode is enabled -->
<script src="./strict.js"></script>

<script src="../../node_modules/@plotly/mathjax-v2/MathJax.js?config=TeX-AMS-MML_SVG"></script>
<script src="../../node_modules/@plotly/mathjax-v4/tex-svg.js"></script>
<script charset="utf-8" id="source" src="../../build/plotly.js"></script>
<script charset="utf-8" src="../../build/test_dashboard-bundle.js"></script>
</body>
Expand Down
1 change: 1 addition & 0 deletions draftlogs/7898_change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Add support for MathJax v4, and **drop support for v2** [[#7898](https://github.com/plotly/plotly.js/pull/7898)]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
- Add support for MathJax v4, and **drop support for v2** [[#7898](https://github.com/plotly/plotly.js/pull/7898)]
- **Breaking:** Add support for MathJax v4, and **drop support for v2** [[#7898](https://github.com/plotly/plotly.js/pull/7898)]

29 changes: 20 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@
},
"devDependencies": {
"@biomejs/biome": "2.2.0",
"@plotly/mathjax-v2": "npm:mathjax@2.7.5",
"@plotly/mathjax-v3": "npm:mathjax@^3.2.2",
"@plotly/mathjax-v3": "npm:mathjax@3.2.2",
"@plotly/mathjax-v4": "npm:mathjax@^4.1.2",
"@types/d3": "3.5.34",
"@types/node": "^24.10.0",
"amdefine": "^1.0.1",
Expand Down
178 changes: 59 additions & 119 deletions src/lib/svg_text_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ exports.convertToTspans = function(_context, gd, _callback) {
_context.style('display', 'none');
var fontSize = parseInt(_context.node().style.fontSize, 10);
var config = {fontSize: fontSize};
// Capture the text-anchor value now, since the the callback function below
// will run async, and in the meantime the text-anchor may change
// which would result in a wrongly-positioned SVG
var textAnchor = _context.attr('text-anchor');

texToSVG(tex[2], config, function(_svgEl, _glyphDefs, _svgBBox) {
parent.selectAll('svg.' + svgClass).remove();
Expand All @@ -90,6 +94,11 @@ exports.convertToTspans = function(_context, gd, _callback) {
return;
}

// Now that the MathJax render has finished, re-hide the source text.
// We hid it earlier, too, but since this callback runs async,
// another function may have made it visible again

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

add a TODO to investigate this further

_context.style('display', 'none');

var mathjaxGroup = parent.append('g')
.classed(svgClass + '-group', true)
.attr({
Expand Down Expand Up @@ -151,11 +160,9 @@ exports.convertToTspans = function(_context, gd, _callback) {
x = 0;
y = dy;
} else {
var anchor = _context.attr('text-anchor');

x = x - w * (
anchor === 'middle' ? 0.5 :
anchor === 'end' ? 1 : 0
textAnchor === 'middle' ? 0.5 :
textAnchor === 'end' ? 1 : 0
);
y = y + dy - h / 2;
}
Expand Down Expand Up @@ -185,154 +192,87 @@ function cleanEscapesForTex(s) {
.replace(GT_MATCH, '\\gt ');
}

var inlineMath = [['$', '$'], ['\\(', '\\)']];
// Create a dedicated MathDocument just for Plotly.js,
// to avoid interfering with any other MathJax on the page.
// mathjaxSVGDocument is initialized on the first call to texToSVG(),
// and reused for subsequent calls.
var mathjaxSVGDocument = null;

function texToSVG(_texString, _config, _callback) {
var MathJaxVersion = parseInt(
const MathJaxVersion = parseInt(
(MathJax.version || '').split('.')[0]
);

if(
MathJaxVersion !== 2 &&
MathJaxVersion !== 3
MathJaxVersion !== 3 &&
MathJaxVersion !== 4
) {
Lib.warn('No MathJax version:', MathJax.version);
Lib.warn('Unsupported MathJax version:', MathJax.version);
return;
}

var originalRenderer,
originalConfig,
originalProcessSectionDelay,
tmpDiv;

var setConfig2 = function() {
originalConfig = Lib.extendDeepAll({}, MathJax.Hub.config);

originalProcessSectionDelay = MathJax.Hub.processSectionDelay;
if(MathJax.Hub.processSectionDelay !== undefined) {
// MathJax 2.5+ but not 3+
MathJax.Hub.processSectionDelay = 0;
}

return MathJax.Hub.Config({
messageStyle: 'none',
tex2jax: {
inlineMath: inlineMath
},
displayAlign: 'left',
});
};

var setConfig3 = function() {
originalConfig = Lib.extendDeepAll({}, MathJax.config);

if(!MathJax.config.tex) {
MathJax.config.tex = {};
}

MathJax.config.tex.inlineMath = inlineMath;
};

var setRenderer2 = function() {
originalRenderer = MathJax.Hub.config.menuSettings.renderer;
if(originalRenderer !== 'SVG') {
return MathJax.Hub.setRenderer('SVG');
}
};

var setRenderer3 = function() {
originalRenderer = MathJax.config.startup.output;
if(originalRenderer !== 'svg') {
MathJax.config.startup.output = 'svg';
var tmpDiv;

const initiateMathJax = function() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should this be an async function?

if(!mathjaxSVGDocument) {
const SVG = MathJax._.output.svg_ts.SVG;
// fontCache 'local' keeps each rendered svg self-contained
const svgConfig = Lib.extendFlat({}, MathJax.config.svg, {fontCache: 'local'});
// MathJax v4 enables automatic inline linebreaking by default, which
// messes up our layout assumptions. Disabling it gives behavior consistent with v3.
if(MathJaxVersion === 4) {
svgConfig.linebreaks = Lib.extendFlat({}, svgConfig.linebreaks, {inline: false});
}
mathjaxSVGDocument = MathJax._.mathjax.mathjax.document(document, Lib.extendFlat({}, MathJax.config.options, {
InputJax: MathJax.startup.input,
OutputJax: new SVG(svgConfig)
}));
}
};

var initiateMathJax = function() {
var randomID = 'math-output-' + Lib.randstr({}, 64);
const randomID = 'math-output-' + Lib.randstr({}, 64);
tmpDiv = d3.select('body').append('div')
.attr({id: randomID})
.style({
visibility: 'hidden',
position: 'absolute',
'font-size': _config.fontSize + 'px'
})
.text(cleanEscapesForTex(_texString));

var tmpNode = tmpDiv.node();
});

return MathJaxVersion === 2 ?
MathJax.Hub.Typeset(tmpNode) :
MathJax.typeset([tmpNode]);
// Strip $…$ delimiters and clean up escape charaters,
// then pass the result to MathDocument.convert() to get an SVG element back
const texMath = cleanEscapesForTex(_texString).replace(/^\$+|\$+$/g, '');
return MathJax._.mathjax.mathjax.handleRetriesFor(function() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why is this call to handleRetriesFor required?

return mathjaxSVGDocument.convert(texMath, {display: false});
}).then(function(node) {
tmpDiv.node().appendChild(node);
});
};

var finalizeMathJax = function() {
var sel = tmpDiv.select(
MathJaxVersion === 2 ? '.MathJax_SVG' : '.MathJax'
);
const finalizeMathJax = function() {
const sel = tmpDiv.select('.MathJax');

var node = !sel.empty() && tmpDiv.select('svg').node();
const node = !sel.empty() && tmpDiv.select('svg').node();
if(!node) {
Lib.log('There was an error in the tex syntax.', _texString);
_callback();
} else {
var nodeBBox = node.getBoundingClientRect();
var glyphDefs;
if(MathJaxVersion === 2) {
glyphDefs = d3.select('body').select('#MathJax_SVG_glyphs');
} else {
glyphDefs = sel.select('defs');
}
const nodeBBox = node.getBoundingClientRect();
const glyphDefs = sel.select('defs');
_callback(sel, glyphDefs, nodeBBox);
}

tmpDiv.remove();
};

var resetRenderer2 = function() {
if(originalRenderer !== 'SVG') {
return MathJax.Hub.setRenderer(originalRenderer);
}
};

var resetRenderer3 = function() {
if(originalRenderer !== 'svg') {
MathJax.config.startup.output = originalRenderer;
}
};

var resetConfig2 = function() {
if(originalProcessSectionDelay !== undefined) {
MathJax.Hub.processSectionDelay = originalProcessSectionDelay;
}
return MathJax.Hub.Config(originalConfig);
};

var resetConfig3 = function() {
MathJax.config = originalConfig;
};

if(MathJaxVersion === 2) {
MathJax.Hub.Queue(
setConfig2,
setRenderer2,
initiateMathJax,
finalizeMathJax,
resetRenderer2,
resetConfig2
);
} else if(MathJaxVersion === 3) {
setConfig3();
setRenderer3();
MathJax.startup.defaultReady();

MathJax.startup.promise.then(function() {
initiateMathJax();
finalizeMathJax();

resetRenderer3();
resetConfig3();
// Initialize, render MathJax, then call _callback()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
// Initialize, render MathJax, then call _callback()

Promise.resolve()
.then(initiateMathJax)
.then(finalizeMathJax)
.catch((err) => {
Lib.log('MathJax typesetting failed.', _texString, err);
if(tmpDiv) tmpDiv.remove();
_callback();
});
}
}

var TAG_STYLES = {
Expand Down
2 changes: 1 addition & 1 deletion src/plot_api/plot_config.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ var configAttributes = {
dflt: true,
description: [
'Determines whether math should be typeset or not,',
'when MathJax (either v2 or v3) is present on the page.'
'when MathJax (either v3 or v4) is present on the page.'
].join(' ')
},

Expand Down
Loading
Loading