My d3 axis labels are using natural numbers, and I want them to be in a prettier form, and so I am trying to use MathML via MathJax.
When I try to use MathML tags as an argument to tickFormat
, it treats the tags as text, and it doesn't actually display it in MathML form.
In my JavaScript, I can get the desired fraction form to show up correctly in a paragraph...
d3.select('.main').append('p').html('<math><mrow><mfrac><mn>$</mn> <mn>day</mn></mfrac></mrow></math>');
... but when I try to apply the same text to my axis, like so...
graph.selectAll('#axisY')
.data([null])
.join('g')
.attr('id', 'axisY')
.call(d3.axisLeft(y)
.tickFormat((d) => '<math><mrow><mfrac><mn>$</mn> <mn>day</mn></mfrac></mrow></math>')
)
.attr('transform', `translate(${margin.left}, 0)`);
... It spells out all those tags like text, instead of actually displaying the fraction.
So, how can I get tickFormat to parse the tags instead of treating them just as text? ... or is there an easier way to have pretty fractions on d3 axes?
tickFormat
expects that the axis tick labels are text
elements whereas you want to use html
instead (per your working example).
Consider a few things to get this working:
Use of tickValues
in order to control the number of values shown on the axis - if you leave this automatic the fractions can overlap as the rendering of the fraction is simply larger than the decimal from a screen real estate point of view.
Use of a foreignObject
for each tick on the axis instead of a text
element. Once you use a foreignObject
you can add html and therefore can use mathjax
Conversion of a decimal to smallest fraction - there is an implementation suggested here.
Working example below - but note the hard-coding of sizes and placement adjustments which you will need to adjust for your circumstances.
// prep
const a1 = [0, 0.33, 0.66, 1];
const a2 = [0, 0.25, 0.5, 0.75, 1];
const width = 500;
const height = 180;
const x = d3.scaleLinear().range([0, 200]).domain([0, 1])
const y = d3.scaleLinear().range([150, 10]).domain([0, 1])
const svg = d3.select("body").append("svg").attr("width", width).attr("height", height);
// set up axes - note setting label as ""
const axis1 = d3.axisLeft(y).tickValues(a1).tickFormat(d => "");
const axis2 = d3.axisBottom(x).tickValues(a2).tickFormat(d => "");
// render axis and mathml conversion for fraction label
const gAxis1 = svg
.append("g")
.attr("id", "axis1")
.attr("transform", "translate(80, 10)")
.style("font-size", 20)
.call(axis1)
.call(mathmlAxis, true);
const gAxis2 = svg
.append("g")
.attr("id", "axis2")
.attr("transform", "translate(150, 90)")
.style("font-size", 20)
.call(axis2)
.call(mathmlAxis, false);
function mathmlAxis(ax, vertical) {
ax.selectAll("g")
.append("svg:foreignObject")
.attr("width", 40)
.attr("height", 40)
.attr("x", vertical ? -45 : -10)
.attr("y", vertical ? -16 : 16)
.append("xhtml:div")
.html((d, i) => mathmlFractionFromDec(d));
}
// https://stackoverflow.com/questions/14002113/how-to-simplify-a-decimal-into-the-smallest-possible-fraction
function mathmlFractionFromDec(x0) {
var eps = 1.0E-15;
var h, h1, h2, k, k1, k2, a, x;
x = x0;
a = Math.floor(x);
h1 = 1;
k1 = 0;
h = a;
k = 1;
while (x-a > eps*k*k) {
x = 1/(x-a);
a = Math.floor(x);
h2 = h1; h1 = h;
k2 = k1; k1 = k;
h = h2 + a*h1;
k = k2 + a*k1;
}
return `<math><mrow><mfrac><mn>${h}</mn><mn>${k}</mn></mfrac></mrow></math>`;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.6.0/d3.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>