// Update button onclick handler function updateFontFamily(event) { event.preventDefault() d3.select("svg").style("font-family", d3.select("#input").node().value) calibrate() } // This evaluates and prints the updated function function calibrate() { var widths = calculate() // this is weird and broken-up cuz "normal" function declarations don't seem to play well with eval var fnName = 'measureText' var fnParams = '(string, fontSize = 10)' var fn = `{ const widths = ${JSON.stringify(widths)} const avg = ${d3.mean(widths.filter(d => d !== 0))} return string .split('') .map(c => c.charCodeAt(0) < widths.length ? widths[c.charCodeAt(0)] : avg) .reduce((cur, acc) => acc + cur) * fontSize }` eval(`${fnName} = function${fnParams} ${fn}`) d3.select("#output").text(`function ${fnName}${fnParams} ${fn}`) // fill in example textbox var ex = `${fnName}('Hello world', 12)` d3.select("#ex").text(`${ex} // ${eval(ex)}`) } // This does the actual calculation function calculate() { var chars = [] var widths = [] // For "!" through "~"... for (var i = 33; i < 127; i++) { chars[i] = String.fromCharCode(i) } // Create element, measure bounding client rect, put in array var letter = d3.select("#calibrate").selectAll("text.calibrate") .data(chars) .enter() .append("text") .classed("calibrate", true) .text(d => d) .each(function(d) { var bb = this.getBoundingClientRect() widths.push(bb.width) }) // A naked space (charCode 32) doesn't take up any space, so, special case... var space = d3.select("#calibrate").append("text") .classed("calibrate", true) .text("t t") .each(function(d) { var bb = this.getBoundingClientRect() widths[32] = bb.width - 2 * widths["t".charCodeAt(0)] }) // Clean up after self d3.select("svg").selectAll("text.calibrate").remove() // These are from 10px font; normalize to 1px, and return return widths.map(d => d/10) }