drawRing = (svgOrigin, ringParam) => {
const defaultParam = {
cx: 0,
cy: 0,
r: 100,
innerR: 20,
padAngle: 1,
color: "black",
strokeWidth: 1,
fontSize: 12,
fontName: "Cormorant SC",
letterSpacing: 0,
fade: 0,
fadeLimit: 60,
opacity: 1,
opacityLimit: 0.5,
bold: true,
darkenFactor: 0,
radial: false,
div: 1,
n: 1,
off: 0,
square: false,
tilted: false,
darkMode: false,
flipLetters: false,
bottomAdjust: 0,
topAdjust: 0
};
const halfPI = Math.PI / 2;
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 know 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
};
// Create Ring
const svg = d3.create("svg");
const arc = (d) =>
d3
.arc()
.innerRadius(d.r)
.outerRadius(d.r + d.innerR)
.padAngle((Math.PI / 180) * d.padAngle)
.startAngle((Math.PI / 180) * d.startAngle)
.endAngle((Math.PI / 180) * d.endAngle);
ringParam.data.forEach((dOrigin) => {
// d is our main data element
const d = structuredClone(dOrigin);
const id = DOM.uid("p-" + d.index);
// define local parameter for each data according to priority
for (const p in defaultParam) {
dOrigin[p] != undefined
? (d[p] = dOrigin[p])
: ringParam[p] != undefined
? (d[p] = ringParam[p])
: (d[p] = defaultParam[p]);
}
// calculate angles when division is made
console.log(d.startAngle, d.endAngle);
const thetaDiv = (d.endAngle - d.startAngle - 2 * d.off) / d.div;
const thetaStart = d.square
? d.startAngle
: d.startAngle + d.off + (d.n - 1) * thetaDiv;
const thetaEnd = d.square
? d.endAngle
: d.startAngle + d.off + d.n * thetaDiv;
d.startAngle = thetaStart;
d.endAngle = thetaEnd;
console.log(thetaStart, thetaEnd);
// start drawing here
const g = svg
.append("g")
.attr("class", "arcLabel")
.attr("transform", `translate(${d.cx}, ${d.cy})`);
// Square Calculation
const a = (d) => (Math.PI / 180) * (d.startAngle - 90 + d.padAngle);
const b = (d) => (Math.PI / 180) * (d.endAngle - 90 - d.padAngle);
const OAnx = (d, n) =>
(d.r / d.div) * ((d.div - n) * Math.cos(a(d)) + n * Math.cos(b(d)));
const OAny = (d, n) =>
(d.r / d.div) * ((d.div - n) * Math.sin(a(d)) + n * Math.sin(b(d)));
const OBnx = (d, n) =>
((d.r + d.innerR) / d.div) *
((d.div - n) * Math.cos(a(d)) + n * Math.cos(b(d)));
const OBny = (d, n) =>
((d.r + d.innerR) / d.div) *
((d.div - n) * Math.sin(a(d)) + n * Math.sin(b(d)));
const ABx = (d) => d.innerR * Math.cos(a(d));
const ABy = (d) => d.innerR * Math.sin(a(d));
const DCx = (d) => d.innerR * Math.cos(b(d));
const DCy = (d) => d.innerR * Math.sin(b(d));
const AnBnx = (d) => (ABx(d) + DCx(d)) / 2;
const AnBny = (d) => (ABy(d) + DCy(d)) / 2;
const A0A1x = (d) => (OAnx(d, 1) - OAnx(d, 0)) / 2;
const A0A1y = (d) => (OAny(d, 1) - OAny(d, 0)) / 2;
const px = (d) => (A0A1x(d) * d.padAngle) / 10;
const py = (d) => (A0A1y(d) * d.padAngle) / 10;
// Square Path
g.append("path")
.attr("display", d.square ? "" : "none")
.attr(
"d",
d3.line().curve(d3.curveLinearClosed)([
// first point
[OAnx(d, d.n - 1) + px(d), OAny(d, d.n - 1) + py(d)],
// second point
d.tilted || d.n <= 1
? [OBnx(d, d.n - 1) + px(d), OBny(d, d.n - 1) + py(d)]
: [
OAnx(d, d.n - 1) + AnBnx(d) + px(d),
OAny(d, d.n - 1) + AnBny(d) + py(d)
],
// third point
d.tilted || d.n == d.div
? [OBnx(d, d.n), OBny(d, d.n)]
: [OAnx(d, d.n) + AnBnx(d), OAny(d, d.n) + AnBny(d)],
// fourth point
[OAnx(d, d.n), OAny(d, d.n)]
])
)
.style("stroke", d.color)
.style("stroke-width", d.strokeWidth)
.style("fill", fading(d.color, d.fade))
.style(
"fill-opacity",
!d.darkMode ? d.opacity : d.opacity > d.opacityLimit ? d.opacity : 0
);
// Adding path for text
g.append("path")
.attr("id", id)
.style("fill", "none")
.attr("d", () => {
const context = d3.path();
context.arc(
0,
0,
d.r,
(Math.PI / 180) * (atBottom(d) ? d.endAngle : d.startAngle) - halfPI,
(Math.PI / 180) * (atBottom(d) ? d.startAngle : d.endAngle) - halfPI,
atBottom(d)
);
return context.toString();
});
// Adding Arc
g.append("path")
.attr("display", !d.square ? "" : "none")
.style("stroke", d.color)
.style("stroke-width", d.strokeWidth)
.style("fill", fading(d.color, d.fade))
.style(
"fill-opacity",
!d.darkMode ? d.opacity : d.opacity > d.opacityLimit ? d.opacity : 0
)
.attr("d", arc(d));
// Create Text
function labelTransform(d, twoLines = 0) {
// for radial transform
const x = (d.endAngle + d.startAngle) / 2 - 90;
const dx = d.radial ? (d.endAngle - d.startAngle) / 6 : 0;
const angle = twoLines == 0 ? x : twoLines == 1 ? x - dx : x + dx;
// for square transformation
const squareAngle = (d) => (d.endAngle + d.startAngle) / 2;
const cornerX = OAnx(d, d.n - 1);
const cornerY = OAny(d, d.n - 1);
const halfWayX = (OAnx(d, 1) - OAnx(d, 0)) / 2;
const halfWayY = (OAny(d, 1) - OAny(d, 0)) / 2;
return d.square
? `translate(${cornerX + halfWayX + AnBnx(d) / 2},
${cornerY + halfWayY + AnBny(d) / 2}
)
rotate(
${
squareAngle(d) > 270 || squareAngle(d) < 90
? squareAngle(d) + 0
: squareAngle(d) + 180
}
)`
: `rotate(${angle}) translate(${d.r + d.innerR / 2},0) rotate(${
angle < 180 - 90 ? 0 : 180
})`;
}
// Add text
g.append("text")
.style(
"fill",
d.opacity < d.opacityLimit || d.fade > d.fadeLimit
? darken(d.color, d.opacity * d.darkenFactor)
: "white"
)
.style("font-size", d.fontSize)
.style("font-weight", d.bold ? "bold" : "normal")
.attr("font-family", d.fontName)
.attr("dominant-baseline", "middle")
.attr("text-anchor", "middle")
.text(() => {
let rawText = d.text;
if (!rawText.includes("\n")) {
return d.letterSpacing
? rawText.split("").join("\u200A".repeat(d.letterSpacing))
: rawText;
} else {
let arrText = rawText.split("\n");
return d.letterSpacing
? arrText[0].split("").join("\u200A".repeat(d.letterSpacing))
: arrText[0];
}
})
.attr(
"transform",
d.text.includes("\n") ? labelTransform(d, 1) : labelTransform(d, 0)
)
.attr("display", d.radial || d.square ? "" : "none")
.clone()
.text(() => {
let rawText = d.text;
let arrText = rawText.split("\n");
return d.letterSpacing
? arrText[1].split("").join("\u200A".repeat(d.letterSpacing))
: arrText[1];
})
.attr("transform", labelTransform(d, 2));
// If not radial
g.append("text")
.attr("display", d.square ? "none" : "")
.style(
"fill",
d.opacity < d.opacityLimit || d.fade > d.fadeLimit
? darken(d.color, d.opacity * d.darkenFactor)
: "white"
)
.style("font-size", d.fontSize)
.style("font-weight", d.bold ? "bold" : "normal")
.attr("font-family", d.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", "#" + id)
.text(() => {
let rawText = d.text;
if (!rawText.includes("\n")) {
return d.letterSpacing
? rawText.split("").join("\u200A".repeat(d.letterSpacing))
: rawText;
}
})
.select(function () {
return this.parentNode;
})
.attr("display", d.radial || d.square ? "none" : "")
// In case of \n in text (This could be improved with less duplication with the if statement)
.clone()
.attr(
"dy",
(d.innerR / 2) *
(atBottom(d) ? +0.7 + d.bottomAdjust : -1.3 + d.topAdjust)
)
.append("textPath")
.attr("startOffset", "50%")
.attr("xlink:href", "#" + id)
.text(() => {
let rawText = d.text;
if (rawText.includes("\n")) {
let arrText = rawText.split("\n");
return d.letterSpacing
? arrText[0].split("").join("\u200A".repeat(d.letterSpacing))
: arrText[0];
}
})
.select(function () {
return this.parentNode;
})
.attr("display", d.radial || d.square ? "none" : "")
.clone()
.attr(
"dy",
(d.innerR / 2) *
(atBottom(d) ? +1.5 + d.topAdjust : -0.55 + d.bottomAdjust)
)
.append("textPath")
.attr("startOffset", "50%")
.attr("xlink:href", "#" + id)
.text(() => {
let rawText = d.text;
if (rawText.includes("\n")) {
let arrText = rawText.split("\n");
return d.letterSpacing
? arrText[1].split("").join("\u200A".repeat(d.letterSpacing))
: arrText[1];
}
})
.attr("display", d.radial || d.square ? "none" : "");
});
const svgRing = svg.node().innerHTML;
svgOrigin.append("g").html(svgRing);
}