Published
Edited
Oct 11, 2022
1 fork
Insert cell
Insert cell
chartSati = {
// Create SVG

const svg = d3
.create("svg")
.attr("viewBox", [-width / 2, -width / 2, width, width]);

// Google Font API

const fontName = "Cormorant SC";

GFontToDataURI(
"https://fonts.googleapis.com/css2?family=Cormorant SC&display=swap"
)
.then((cssRules) => {
let fontRules = cssRules.join("\n");
d3.select("svg")
.append("defs")
.append("style")
.attr("type", "text/css")
.text(fontRules);
console.log("Added Font");
})
.catch((reason) => console.log(reason));

// Text at the Center

const text = svg
.append("text")
.style("font-weight", "bold")
.style("fill", "black")
.attr("font-family", "Cormorant SC")
.attr("font-size", 22)
.attr("text-anchor", "middle")
.text("Center Text")
.attr("dy", "0.35em");

// Ring One

const ringOne = {
cx: 0,
cy: 0,
r: 90,
innerR: 40,
padAngle: 2.3,
fontSize: 15,

// Optional global parameter.
color: "blue",
strokeWidth: 1.8,

data: [
{
text: "Label One",
startAngle: (360 / 8) * 0,
endAngle: (360 / 8) * 2,
letterSpace: 0,
fillFade: 20,
fillOpacity: 0.6
},
{
text: "Label Two",
startAngle: (360 / 8) * 2,
endAngle: (360 / 8) * 3,
fontSize: 10,
letterSpace: 0,
fillOpacity: 1,
fillFade: 70,
strokeWidth: 0.2
},
{
text: "Long Label \n Three",
startAngle: 30 * 5,
endAngle: 30 * 8,
fontSize: 14,
letterSpace: 1,
fillOpacity: 0,
color: "red"
},
{
text: "Long Label \n Four",
startAngle: 30 * 9,
endAngle: 30 * 12,
fontSize: 13,
letterSpace: 4,
fillFade: 18,
fillOpacity: 0.3
}
]
};

drawRing(svg, ringOne);

// Ring Two

const ringTwo = {
cx: 0,
cy: 0,
r: 137,
innerR: 40,
padAngle: 1.5,
fontSize: 15,
color: "red",
data: [
{
text: "Label One - Ring II",
startAngle: (360 / 8) * 0,
endAngle: (360 / 8) * 2,
letterSpace: 0,
fillFade: 20,
fillOpacity: 0.6
},
{
text: "Label Two - II",
startAngle: (360 / 8) * 2,
endAngle: (360 / 8) * 3,
fontSize: 10,
letterSpace: 0,
fillOpacity: 1,
fillFade: 70,
strokeWidth: 3
},
{
text: "Long Label \n Three in Ring II",
startAngle: 30 * 5,
endAngle: 30 * 8,
fontSize: 14,
letterSpace: 1,
fillOpacity: 0.2,
color: "blue"
},
{
text: "Long Label \n Four Ring II",
startAngle: 30 * 9,
endAngle: 30 * 12,
fontSize: 13,
letterSpace: 4,
fillFade: 18,
fillOpacity: 0.3
}
]
};

drawRing(svg, ringTwo);

return svg.node();
}
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 = 2,
fontSize = 12,
fontName = "Cormorant SC",
letterSpace = 0,
fillFade = 0,
fillFadeLimit = 60,
fillOpacity = 1,
fillOpacityLimit = 0.5,
bold = true,
darkenFactor = 0
} = ring;

const halfPI = Math.PI / 2;

// Color functions

const fade = (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 dStrokeWidth = (d) =>
d.strokeWidth != undefined ? d.strokeWidth : strokeWidth;
const dFillFade = (d) => (d.fillFade != undefined ? d.fillFade : fillFade);
const dFillOpacity = (d) =>
d.fillOpacity != undefined ? d.fillOpacity : fillOpacity;
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);

// Create ring

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

const arc = d3
.arc()
.innerRadius(r)
.outerRadius(r + innerR)
.padAngle((Math.PI / 180) * padAngle)
.startAngle((d) => (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) => fade(dColor(d), dFillFade(d)))
.style("fill-opacity", (d) => dFillOpacity(d))
.attr("d", (d) => arc(d));

// Create Text
g.append("text")
.style("fill", (d) =>
dFillOpacity(d) < fillOpacityLimit || dFillFade(d) > fillFadeLimit
? darken(dColor(d), dFillOpacity(d) * dDarkenFactor(d))
: "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 d.letterSpace
? rawText.split("").join("\u200A".repeat(d.letterSpace))
: rawText;
}
})
.select(function () {
return this.parentNode;
})

// 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 d.letterSpace
? arrText[0].split("").join("\u200A".repeat(d.letterSpace))
: arrText[0];
}
})
.select(function () {
return this.parentNode;
})
.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 d.letterSpace
? arrText[1].split("").join("\u200A".repeat(d.letterSpace))
: arrText[1];
}
});
};
data.forEach((d) => (d.uid = DOM.uid("p-" + d.index)));

svg.selectAll("g.arcLabel").data(data).join(enter);
const svgRing = svg.node().innerHTML;
svgOrigin.append("g").html(svgRing);
}
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