diff --git a/packages/charting/docs/demo/generate.js b/packages/charting/docs/demo/generate.js index 20f7e02988..5efe462fcf 100644 --- a/packages/charting/docs/demo/generate.js +++ b/packages/charting/docs/demo/generate.js @@ -42,9 +42,10 @@ exports.model = (id, element) => ({ editable: false, }, ], - // domain: { - // label: 'Characters', - // }, + domain: { + label: + '
Math in the bottom label: \\(3x^2\\)\u200b
', + }, graph: { width: 480, height: 480, @@ -53,11 +54,13 @@ exports.model = (id, element) => ({ promptEnabled: true, rationale: 'Rationale goes here!', range: { - label: 'Amount', + label: + '
Math in the left label: \\(\\frac{\\pi}{2}\\)\u200b
', max: 3, min: 0, labelStep: 1, }, - // title: 'This is a chart!', + title: + '
Math in the title: \\(\\frac{x}{y}\\)\u200b
', rubricEnabled: false, }); diff --git a/packages/charting/src/index.js b/packages/charting/src/index.js index 1270a1e2bf..a1d78996ff 100644 --- a/packages/charting/src/index.js +++ b/packages/charting/src/index.js @@ -10,6 +10,43 @@ export default class Graphing extends HTMLElement { constructor() { super(); this._root = null; + this._mathObserver = null; + this._mathRenderPending = false; + } + + // The title and axis labels are injected synchronously via dangerouslySetInnerHTML, + // but createRoot().render() commits asynchronously, so a queueMicrotask(renderMath) + // would run before the LaTeX spans are in the DOM and leave raw LaTeX on first render. + // Observing the DOM and typesetting after each commit keeps math in sync regardless of timing. + _scheduleMathRender = () => { + if (this._mathRenderPending) return; + this._mathRenderPending = true; + + requestAnimationFrame(() => { + if (this._mathObserver) { + this._mathObserver.disconnect(); + } + renderMath(this); + this._mathRenderPending = false; + setTimeout(() => { + if (this._mathObserver) { + this._mathObserver.observe(this, { childList: true, subtree: true }); + } + }, 50); + }); + }; + + _initMathObserver() { + if (this._mathObserver) return; + this._mathObserver = new MutationObserver(this._scheduleMathRender); + this._mathObserver.observe(this, { childList: true, subtree: true }); + } + + _disconnectMathObserver() { + if (this._mathObserver) { + this._mathObserver.disconnect(); + this._mathObserver = null; + } } set model(m) { @@ -34,6 +71,7 @@ export default class Graphing extends HTMLElement { } connectedCallback() { + this._initMathObserver(); this._render(); } @@ -52,6 +90,8 @@ export default class Graphing extends HTMLElement { return; } + this._initMathObserver(); + const modelClone = { ...this._model, data: this._model.data ? [...this._model.data] : this._model.data, @@ -67,12 +107,10 @@ export default class Graphing extends HTMLElement { this._root = createRoot(this); } this._root.render(el); - queueMicrotask(() => { - renderMath(this); - }); } disconnectedCallback() { + this._disconnectMathObserver(); if (this._root) { this._root.unmount(); } diff --git a/packages/graphing/docs/demo/generate.js b/packages/graphing/docs/demo/generate.js index 665fff9452..2f3c4ccdd4 100644 --- a/packages/graphing/docs/demo/generate.js +++ b/packages/graphing/docs/demo/generate.js @@ -176,7 +176,12 @@ const oldModel = { height: 480, }, coordinatesOnHover: false, - labels: { top: 'top', left: 'left', bottom: 'bottom', right: 'right' }, + labels: { + "top": "
Math in the top label: \\(x^2\\)
", + "right": "
\\(\\frac{\\pi}{2}\\)
", + "left": "
\\(3\\pi\\)
", + "bottom": "
\\(3x^2\\)
" + }, padding: true, prompt: 'Here goes item stem !!!!!!', promptEnabled: true, @@ -189,7 +194,7 @@ const oldModel = { axisLabel: 'y', }, rationale: 'Rationale goes here', - title: 'Graph title', + title: "
Math in the title: \\(\\frac{x}{y}\\)
", rubricEnabled: false, }; diff --git a/packages/graphing/src/index.js b/packages/graphing/src/index.js index b6b3698eb2..c6a2224c2a 100644 --- a/packages/graphing/src/index.js +++ b/packages/graphing/src/index.js @@ -11,6 +11,43 @@ export default class Graphing extends HTMLElement { constructor() { super(); this._root = null; + this._mathObserver = null; + this._mathRenderPending = false; + } + + // The title and axis labels are injected synchronously via dangerouslySetInnerHTML, + // but createRoot().render() commits asynchronously, so a queueMicrotask(renderMath) + // would run before the LaTeX spans are in the DOM and leave raw LaTeX on first render. + // Observing the DOM and typesetting after each commit keeps math in sync regardless of timing. + _scheduleMathRender = () => { + if (this._mathRenderPending) return; + this._mathRenderPending = true; + + requestAnimationFrame(() => { + if (this._mathObserver) { + this._mathObserver.disconnect(); + } + renderMath(this); + this._mathRenderPending = false; + setTimeout(() => { + if (this._mathObserver) { + this._mathObserver.observe(this, { childList: true, subtree: true }); + } + }, 50); + }); + }; + + _initMathObserver() { + if (this._mathObserver) return; + this._mathObserver = new MutationObserver(this._scheduleMathRender); + this._mathObserver.observe(this, { childList: true, subtree: true }); + } + + _disconnectMathObserver() { + if (this._mathObserver) { + this._mathObserver.disconnect(); + this._mathObserver = null; + } } set model(m) { @@ -28,6 +65,7 @@ export default class Graphing extends HTMLElement { } connectedCallback() { + this._initMathObserver(); this._render(); } @@ -46,6 +84,8 @@ export default class Graphing extends HTMLElement { return; } + this._initMathObserver(); + const el = React.createElement(Main, { model: this._model, session: this._session, @@ -56,12 +96,10 @@ export default class Graphing extends HTMLElement { this._root = createRoot(this); } this._root.render(el); - queueMicrotask(() => { - renderMath(this); - }); } disconnectedCallback() { + this._disconnectMathObserver(); if (this._root) { this._root.unmount(); }