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();
}