Public
Edited
Oct 4, 2024
Fork of Nibbana III
1 fork
Importers
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// Function that create a HTML layer arc of multile labels with options.
// data contains the angles, text, font-size & letter-spacing...

drawRing = (svgOrigin, ring) => {
// Deconstruct object param

const {
cx = 0,
cy = 0,
r = 100,
innerR = 20,
padAngle = 1,
color = "black",
data,
strokeWidth = 1,
fontSize = 12,
fontName = "Cormorant SC",
letterSpace = 0,
fade = 0,
fadeLimit = 60,
opacity = 1,
opacityLimit = 0.5,
bold = true,
darkenFactor = 0,
radial = false,
div = 1,
n = 1,
off = 0
} = ring;

const halfPI = Math.PI / 2;

// Color functions

const fading = (color, f) => {
const { l, c, h } = d3.lch(color);
return d3.lch(l + f, c, h);
};
const darken = (color, f) => {
const { l, c, h } = d3.lch(color);
return d3.lch(l - f, c, h);
};

// Function to konw if we are at the bottom

const atBottom = (d) => {
const middle =
((Math.PI / 180) * d.startAngle -
halfPI +
(Math.PI / 180) * d.endAngle -
halfPI) /
2;
const bottom = middle >= 0 && middle < Math.PI;
return bottom ^ d.flipLetters; // XOR
};

// Define parameters if global or local in data

const dFlipLetters = (d) =>
d.flipLetters != undefined ? d.flipLetters : false;
const dColor = (d) => (d.color != undefined ? d.color : color);
const dLetterSpace = (d) =>
d.letterSpace != undefined ? d.letterSpace : letterSpace;
const dStrokeWidth = (d) =>
d.strokeWidth != undefined ? d.strokeWidth : strokeWidth;
const dFade = (d) => (d.fade != undefined ? d.fade : fade);
const dOpacity = (d) => (d.opacity != undefined ? d.opacity : opacity);
const dRadial = (d) => (d.radial != undefined ? d.radial : radial);
const dDarkenFactor = (d) =>
d.darkenFactor != undefined ? d.darkenFactor : darkenFactor;
const dBottomAjust = (d) => (d.bottomAjust != undefined ? d.bottomAjust : 0);
const dTopAjust = (d) => (d.topAjust != undefined ? d.topAjust : 0);
const dDiv = (d) => (d.div != undefined ? d.div : div);
const dN = (d) => (d.n != undefined ? d.n : n);
const dOff = (d) => (d.off != undefined ? d.off : off);

// calculate angles when division is made

const thetaDiv = (d) => (d.endAngle - d.startAngle - 2 * dOff(d)) / dDiv(d);

const newData = data.map((d) => {
const thetaStart = d.startAngle + dOff(d) + (dN(d) - 1) * thetaDiv(d);
const thetaEnd = d.startAngle + dOff(d) + dN(d) * thetaDiv(d);
return { ...d, startAngle: thetaStart, endAngle: thetaEnd };
});

// Create ring

const svg = d3.create("svg");

const arc = d3
.arc()
.innerRadius(r)
.outerRadius(r + innerR)
.padAngle((Math.PI / 180) * padAngle)
.startAngle((d) => {
return (Math.PI / 180) * d.startAngle;
})
.endAngle((d) => (Math.PI / 180) * d.endAngle);

const enter = (enter) => {
// Move to center
const g = enter
.append("g")
.attr("class", "arcLabel")
.attr("transform", `translate(${cx}, ${cy})`);

// Create Arc and fill
g.append("path")
.attr("id", (d) => d.uid.id)
.style("fill", "none")
.attr("d", (d) => {
const context = d3.path();
if (atBottom(d)) {
context.arc(
0,
0,
r,
(Math.PI / 180) * d.endAngle - halfPI,
(Math.PI / 180) * d.startAngle - halfPI,
true
);
} else {
context.arc(
0,
0,
r,
(Math.PI / 180) * d.startAngle - halfPI,
(Math.PI / 180) * d.endAngle - halfPI,
false
);
}
return context.toString();
});
g.append("path")
.style("stroke", (d) => dColor(d))
.style("stroke-width", (d) => dStrokeWidth(d))
.style("fill", (d) => fading(dColor(d), dFade(d)))
.style("fill-opacity", (d) =>
!darkMode ? dOpacity(d) : dOpacity(d) > opacityLimit ? dOpacity(d) : 0
)
.attr("d", (d) => arc(d));
// .clone() // for dark mode
// .lower()
// .style("fill", "white")
// .style("fill-opacity", 1);

// Create Text

// if radial

function labelTransform(d) {
const x = (d.endAngle + d.startAngle) / 2 - 90;
return `rotate(${x}) translate(${r + innerR / 2},0) rotate(${
x < 180 - 90 ? 0 : 180
})`;
}

g.append("text")
.style("fill", (d) =>
dOpacity(d) < opacityLimit || dFade(d) > fadeLimit
? darken(dColor(d), dOpacity(d) * dDarkenFactor(d))
: "white"
)
.style("font-size", (d) => (d.fontSize ? d.fontSize : fontSize))
.style("font-weight", bold ? "bold" : "normal")
.attr("font-family", fontName)
.attr("dominant-baseline", "middle")
.attr("text-anchor", "middle")
.text((d) => d.text)
.attr("transform", (d) => labelTransform(d))
.attr("display", (d) => (dRadial(d) ? "true" : "none"));

// if not radial

g.append("text")
.style("fill", (d) =>
dOpacity(d) < opacityLimit || dFade(d) > fadeLimit
? darken(dColor(d), dOpacity(d) * dDarkenFactor(d))
: darkMode
? "black"
: "white"
)
.style("font-size", (d) => (d.fontSize ? d.fontSize : fontSize))
.style("font-weight", bold ? "bold" : "normal")
.attr("font-family", fontName)
.attr("dy", (d) => (innerR / 2) * (atBottom(d) ? +1 : -0.9))
.attr("dominant-baseline", "middle")
.attr("text-anchor", "middle")
.append("textPath")
.attr("startOffset", "50%")
.attr("xlink:href", (d) => d.uid.href)
.text((d) => {
let rawText = d.text;
if (!rawText.includes("\n")) {
return dLetterSpace(d)
? rawText.split("").join("\u200A".repeat(dLetterSpace(d)))
: rawText;
}
})
.select(function () {
return this.parentNode;
})
.attr("display", (d) => (dRadial(d) ? "none" : "true"))

// In case of \n in text
.clone()
.attr(
"dy",
(d) =>
(innerR / 2) *
(atBottom(d) ? +0.7 + dBottomAjust(d) : -1.3 + dTopAjust(d))
)
.append("textPath")
.attr("startOffset", "50%")
.attr("xlink:href", (d) => d.uid.href)
.text((d) => {
let rawText = d.text;
if (rawText.includes("\n")) {
let arrText = rawText.split("\n");
return dLetterSpace(d)
? arrText[0].split("").join("\u200A".repeat(dLetterSpace(d)))
: arrText[0];
}
})
.select(function () {
return this.parentNode;
})
.attr("display", (d) => (dRadial(d) ? "none" : "true"))

.clone()
.attr(
"dy",
(d) =>
(innerR / 2) *
(atBottom(d) ? +1.5 + dTopAjust(d) : -0.55 + dBottomAjust(d))
)
.append("textPath")
.attr("startOffset", "50%")
.attr("xlink:href", (d) => d.uid.href)
.text((d) => {
let rawText = d.text;
if (rawText.includes("\n")) {
let arrText = rawText.split("\n");
return dLetterSpace(d)
? arrText[1].split("").join("\u200A".repeat(dLetterSpace(d)))
: arrText[1];
}
})
.attr("display", (d) => (dRadial(d) ? "none" : "true"));
};
newData.forEach((d) => (d.uid = DOM.uid("p-" + d.index)));

svg.selectAll("g.arcLabel").data(newData).join(enter);
const svgRing = svg.node().innerHTML;
svgOrigin.append("g").html(svgRing);
}
Insert cell
Insert cell
import { serialize } from "@mbostock/saving-svg"
Insert cell
viewof PNGprecision = slider({
title: "PNG Resolution Factor",
min: 0,
max: 12,
step: 1,
value: 3
})
Insert cell
import { slider, select, color, checkbox, button } from "@jashkenas/inputs"
Insert cell
function rasterizeWhite(svg, PNGprecision) {
let resolve, reject;
const promise = new Promise((y, n) => ((resolve = y), (reject = n)));
const image = new Image();
image.onerror = reject;
image.onload = () => {
const rect = svg.getBoundingClientRect();
const context = DOM.context2d(
rect.width * PNGprecision,
rect.height * PNGprecision
);
context.fillStyle = "white";
context.fillRect(
0,
0,
rect.width * PNGprecision,
rect.height * PNGprecision
);
context.drawImage(
image,
0,
0,
rect.width * PNGprecision,
rect.height * PNGprecision
);
context.canvas.toBlob(resolve, "image/png", 1);
};
image.src = URL.createObjectURL(serialize(svg));
return promise;
}
Insert cell
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