Public
Edited
Jul 23, 2023
2 forks
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
subheader = svg`
<svg class="subheader">
<style>
<!-- Make sure to add a namespace class or the style will bleed to other elements -->

.subheader, .subheader tspan {
font-family: Helvetica;
font-size: 18px;
}

.subheader tspan {
font-weight: bold;
}
</style>

<text class="subheader" x="0" y="0" dy="1em" fill="#535353">
and this is <tspan fill="red">the</tspan> subheader
</text>
</svg>
`
Insert cell
Insert cell
histogram = Plot.rectY(
{ length: 10000 },
Plot.binX({ y: "count" }, { x: d3.randomNormal() })
).plot({ width: 523, height: 413 })
Insert cell
Insert cell
Insert cell
Insert cell
{
// make sure to add all the chart elements you want in the order you want them
const elements = [
//{ element: header, offsest: 100, transform: (el) => textwrap(el, 100) }, // not working properly
{ element: header, offsest: 100 },
{ element: subheader, offset: 16 },
{ element: histogram, offset: 16 },
{ element: footer, offset: 0 }
];

// create new svg container
const chart = d3.create("svg");

// wrap all elements in groups for cleaner rendering and add the offset to the datum of the group
elements.forEach((el, i) => {
const g = chart
.append("g")
.datum({ offset: el.offset, transform: el.transform })
.append(() => el.element);
});

chart.call(joinElement);

return chart.node();
}
Insert cell
Insert cell
/**
This function select all the direct children of the svg,
calculates the height of each child and
translates it its correct position based on the height.

The function also sets the height and width of the svg based on its content.
**/

function joinElement(svg) {
const postRender = (svg) => {
const size = { height: 0, width: 0 };

svg
.selectChildren()
.nodes()
.forEach((el) => {
const datum = d3.select(el).datum();

if (datum.transform) {
datum.transform(el);
}

const box = el.getBBox();

// always translate the element with the previous elements height
d3.select(el).attr("transform", `translate(0, ${size.height})`);

// get the height of the element and add an offset if there is one specified in the datum
size.height += box.height + (datum.offset ? datum.offset : 0);

// use the width of the widest element
size.width = box.width > size.width ? box.width : size.width;
});

svg.attr("width", size.width);
svg.attr("height", size.height);
};

// do this once the svg has been rendered to screen, ie. it has a height and width
if (svg.isConnected) {
Promise.resolve().then(() => postRender(svg));
} else if (typeof requestAnimationFrame !== "undefined") {
requestAnimationFrame(() => postRender(svg));
}
}
Insert cell
/**

Copied (and adapted) from https://observablehq.com/@jtrim-ons/svg-text-wrapping

**/

function textwrap(
chart,
width = 500,
selector = "text",
lineHeightEms = 1.05,
lineHeightSquishFactor = 1,
splitOnHyphen = true,
centreVertically = true
) {
const text = d3.select(chart).selectAll("text");

text.each(function () {
const text = d3.select(this);
const x = text.attr("x");
const y = text.attr("y");

// get the styles of the text element, need to re-apply on each new text && tspan element
const textStyle = text.attr("style");

console.log(text.text());
const words = [];
text
.text()
.split(/\s+/)
.forEach(function (w) {
if (splitOnHyphen) {
const subWords = w.split("-");
for (var i = 0; i < subWords.length - 1; i++)
words.push(subWords[i] + "-");
words.push(subWords[subWords.length - 1] + " ");
} else {
words.push(w + " ");
}
});

text.text(null); // Empty the text element
text.attr("style", textStyle);
text.attr("dy", "1em");

// `tspan` is the tspan element that is currently being added to
let tspan = text.append("tspan").attr("style", textStyle);

let line = ""; // The current value of the line
let prevLine = ""; // The value of the line before the last word (or sub-word) was added
let nWordsInLine = 0; // Number of words in the line
for (var i = 0; i < words.length; i++) {
let word = words[i];
prevLine = line;
line = line + word;
++nWordsInLine;
tspan.text(line.trim());

if (tspan.node().getComputedTextLength() > width && nWordsInLine > 1) {
// The tspan is too long, and it contains more than one word.
// Remove the last word and add it to a new tspan.
tspan.text(prevLine.trim());
prevLine = "";
line = word;
nWordsInLine = 1;
tspan = text.append("tspan");
tspan.attr("style", textStyle);
tspan.text(word.trim());
}
}

const tspans = text.selectAll("tspan");

let h = lineHeightEms;
// Reduce the line height a bit if there are more than 2 lines.
if (tspans.size() > 2)
for (var i = 0; i < tspans.size(); i++) h *= lineHeightSquishFactor;

tspans.each(function (d, i) {
// Calculate the y offset (dy) for each tspan so that the vertical centre
// of the tspans roughly aligns with the text element's y position.
let dy = i * h;

d3.select(this)
.attr("y", y)
.attr("x", x)
.attr("dy", dy + "em");
});
});
}
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