Public
Edited
Jan 3, 2023
Importers
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
demo = {
const demo = new SVGMultilineTextElement(
textString,
textWidth,
textFontSize,
textLeading,
{
debug: toggleGuides
}
);
const height = demo.getHeight();
return html`
<svg width="${width}" height="${height}">
${demo.node()}
</svg>`;
}
Insert cell
demoImperative = {
const svg = d3.create("svg").attr("width", width).attr("height", 300);
svg.call(multiLineText, textString, {
width: textWidth,
fontSize: textFontSize,
leading: textLeading,
options: { debug: toggleGuides }
});
const text = svg.select("text");
return svg.node();
}
Insert cell
demoDefaults = {
const svg = d3.create("svg").attr("width", width).attr("height", 300);
svg.call(multiLineText, textString);
return svg.node();
}
Insert cell
Insert cell
multiLineText = (selection, text, mlOptions = {}) => {
const { width, fontSize, leading, options } = mlOptions;
const textElement = new SVGMultilineTextElement(
text,
width,
fontSize,
leading,
options
);
selection.append(() => textElement.node());
return { node: textElement.node(), height: textElement.getHeight() };
}
Insert cell
class SVGMultilineTextElement {
constructor(
text,
width = 200,
fontSize = 12,
leading = 1.2,
options = { debug: false }
) {
this.text = text;
this.width = width;
this.fontSize = fontSize;
this.leading = leading;
this.debug = options.debug;
this.textElement = d3.create("text");
this.lines = this.getLines();
}

getWords() {
return this.text.split(" ");
}

getLineDimensions(lineText) {
const container = d3.select("body").append("svg");
const textElement = container.append("text");
textElement.text(lineText).attr("font-size", this.fontSize);
const width = textElement.node().getComputedTextLength();
const height = textElement.node().getExtentOfChar(0).height;
container.remove();
return { width, height };
}

getHeight() {
const lineHeight = this.getLineDimensions(this.lines[0]).height;
return lineHeight * this.leading * (this.lines.length - 1);
}

getDebugGuide() {
return svg`<line x1="${this.width}" x2="${
this.width
}" y2="${this.getHeight()}" stroke="red" />`;
}

hasTextOverflow() {
const lastWord = this.getWords().slice(-1)[0];
const lastRenderedWord = this.lines.flat().slice(-1)[0];
return lastWord != lastRenderedWord;
}

// TODO: add when fixed height can be set
// getTextOverflowIndicator() {
// if (!this.hasTextOverflow()) return;
// return svg`
// <g transform="translate(${this.width} ${
// this.getHeight() - this.fontSize
// })">
// <circle r="10" fill="white" stroke="red" stroke-width="2" />
// <text text-anchor="middle" y="5" font-size="20" font-family="monospace" font-weight="bold" fill="red">+</text>
// </g>`;
// }

getLines() {
const line = [];
const lines = [];
const words = this.getWords();

words.forEach((word, idx) => {
line.push(word);
const lineText = line.join(" ");
if (this.getLineDimensions(lineText).width > this.width) {
const overflowingWord = line.pop();
lines.push(line.slice());
line.length = 0;
line.push(overflowingWord);

if (idx + 1 == words.length) lines.push(line.slice());
}
if (idx + 1 == words.length && line.length > 1) lines.push(line.slice());
// TODO: refactor – less if conditions, more intiuitiv approach 🤷‍♂️
});
return lines;
}

node() {
const tspans = this.lines.map(
(line, i) =>
svg`<tspan x="0" dy="${
i > 0 ? this.fontSize * this.leading : 0
}">${line.join(" ")}</tspan>`
);
return svg`
<g>
${this.debug ? this.getDebugGuide() : ""}
<text class="multi-line-text" font-size="${this.fontSize}" y="${
this.fontSize
}">${tspans}</text>
</g>
`;
}
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
import { todoList } from "@jakoblistabarth/todo-list"
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more