drawRing = (svgOrigin, ringParam) => {
const defaultParam = {
cx: 0,
cy: 0,
r: 100,
innerR: 20,
padAngle: 1,
startPadAngle: null,
endPadAngle: null,
padRadius: null,
padLinear: false,
cornerRadius: 0,
radial: false,
strokeWidth: 0.6,
strokeColor: false,
strokeFade: false,
fontSize: 12,
lineHeight: 0,
fontName: "sans-serif",
letterSpacing: 0,
flipLetters: false,
flipLines: false,
bottomAdjust: 0,
topAdjust: 0,
color: "black",
fade: 0,
fadeLimit: 30,
opacity: 1,
opacityLimit: 0.8,
darkenFactor: 0,
opaque: true,
gradientID: false,
gradientR: 50,
gradientI: 0,
gradientX: 50,
gradientY: 50,
gradientFX: 50,
gradientFY: 50,
invertGradient: false,
div: 1,
n: 1,
gPadAngle: 0,
gPadRadius: null,
startGPadAngle: null,
endGPadAngle: null,
endsPadAngle: 0,
endsPadLinear: false,
startRatio: 0,
endRatio: 1,
padRatio: 0,
startPadRatio: false,
endPadRatio: false,
square: false,
squareEnds: false,
centerRadius: true,
centerInnerR: true,
innerSide: false,
padSquare: 0,
tilted: false,
arrowEnd: false,
arrowStart: false,
arrowFactor: 1,
polyId: false,
polyPad: 0,
polyOpacity: 1,
polyFade: 0,
polyOpaque: true,
centerText: "",
up: 0
};
const pi = Math.PI;
const dTR = (deg) => (pi / 180) * deg;
const rTD = (rad) => (180 / pi) * rad;
const modPi = (angle) => angle % (2 * pi);
const sin = (x) => Math.sin(x);
const cos = (x) => Math.cos(x);
const atan = (x) => Math.atan(x);
const sqrt = (x) => Math.sqrt(x);
const fading = (color, f) => {
const { l, c, h } = d3.lch(color);
return d3.lch(l + f, c, h);
};
const darken = (color, f) => fading(color, -f);
const svg = d3.create("svg");
let arrPoints = [];
let d2 = {};
const background = svg.append("g").attr("id", "background");
ringParam.data.forEach((dOrigin, index) => {
const structuredClone = (val) => JSON.parse(JSON.stringify(val));
const d = structuredClone(dOrigin);
const id = DOM.uid("p-" + d.index);
for (const p in defaultParam) {
dOrigin[p] != undefined
? (d[p] = dOrigin[p])
: ringParam[p] != undefined
? (d[p] = ringParam[p])
: (d[p] = defaultParam[p]);
}
d.startAngle = dTR(d.startAngle);
d.endAngle = dTR(d.endAngle);
d.padAngle = dTR(d.padAngle);
d.gPadAngle = dTR(d.gPadAngle);
d.startPadAngle = dTR(d.startPadAngle);
d.endPadAngle = dTR(d.endPadAngle);
d.endsPadAngle = dTR(d.endsPadAngle);
let a = d.startAngle,
b = d.endAngle,
c = (b - a) / 2;
if (d.endsPadLinear)
d.endsPadAngle = Math.abs(
Math.atan(Math.tan(c) - d.endsPadLinear / d.r) - c
);
a = d.startAngle + d.endsPadAngle;
b = d.endAngle - d.endsPadAngle;
c = (b - a) / 2;
let e = (a + b) / 2,
R = d.r,
Rs = R * cos(c),
r = d.innerR,
rs = r * cos(c);
const div = d.div,
n = d.n,
polyPad = d.polyPad;
d.startPadRatio = d.startPadRatio !== false ? d.startPadRatio : d.padRatio;
d.endPadRatio = d.endPadRatio !== false ? d.endPadRatio : d.padRatio;
if (d.centerText != "") {
d.text = "";
d.startAngle = 0;
d.endAngle = 0;
d.strokeWidth = 0;
d.opacity = 0;
d.opaque = false;
}
let sgp = d.startGPadAngle === null ? d.gPadAngle : dTR(d.startGPadAngle),
egp = d.endGPadAngle === null ? d.gPadAngle : dTR(d.endGPadAngle),
rp = d.gPadRadius ? d.gPadRadius : sqrt(r * r + R * R);
sgp = rp * sin(sgp);
egp = rp * sin(egp);
const ca = sgp == 0 ? 0 : atan(sin(b - a) / (egp / sgp + cos(b - a))),
gr = sgp == 0 ? egp / sin(b - a) : sgp / sin(ca);
if (sgp != 0 || egp != 0)
(d.cx += gr * sin(a + ca)),
(d.cy -= gr * cos(a + ca)),
(d.gcx = -d.cx),
(d.gcy = -d.cy),
(R -= gr);
if (d.square) {
if (d.centerRadius) R = R / cos(c);
if (d.innerSide) {
Rs = R * cos(c);
R = Math.sqrt((d.innerSide * d.innerSide) / 4 + Rs * Rs);
a = e - Math.atan(d.innerSide / 2 / Rs);
b = e + Math.atan(d.innerSide / 2 / Rs);
c = (b - a) / 2;
}
if (d.centerInnerR) r = r / cos(c);
Rs = R * cos(c);
rs = r * cos(c);
} else {
const thetaDiv = (b - a) / div,
thetaStart = a + (n - 1) * thetaDiv,
thetaEnd = a + n * thetaDiv;
a = thetaStart;
b = thetaEnd;
c = (b - a) / 2;
e = (a + b) / 2;
}
e = modPi(e);
const atBottom = e > pi / 2 && e < (3 * pi) / 2,
atLeft = e > pi && e < 2 * pi;
const g = svg
.append("g")
.attr("class", "arcLabel")
.attr("transform", `translate(${d.cx}, ${d.cy})`);
const defs = g.append("defs");
const OA00x = R * sin(a),
OA00y = R * -cos(a),
OAddx = R * sin(b),
OAddy = R * -cos(b),
OB00x = (R + r) * sin(a),
OB00y = (R + r) * -cos(a),
OBddx = (R + r) * sin(b),
OBddy = (R + r) * -cos(b);
const OA0x =
OA00x + (d.startRatio + d.padAngle + d.startPadRatio) * (OAddx - OA00x),
OA0y =
OA00y + (d.startRatio + d.padAngle + d.startPadRatio) * (OAddy - OA00y),
OAdx =
OA00x + (d.endRatio - d.padAngle - d.endPadRatio) * (OAddx - OA00x),
OAdy =
OA00y + (d.endRatio - d.padAngle - d.endPadRatio) * (OAddy - OA00y);
const OA0xP = (R - polyPad) * sin(a),
OA0yP = (R - polyPad) * cos(a),
OAdxP = (R - polyPad) * sin(b),
OAdyP = (R - polyPad) * cos(b);
const OAix = (i) => (1 - i / div) * OA0x + (i / div) * OAdx,
OAiy = (i) => (1 - i / div) * OA0y + (i / div) * OAdy,
OAixP = (i) => (1 - i / div) * OA0xP + (i / div) * OAdxP,
OAiyP = (i) => (1 - i / div) * OA0yP + (i / div) * OAdyP;
const OB0tx = OA0x + OB00x - OA00x,
OB0ty = OA0y + OB00y - OA00y,
OB0txP = OA0xP + OB00x - OA00x,
OB0tyP = OA0yP + OB00y - OA00y,
OBdtx = OAdx + OBddx - OAddx,
OBdty = OAdy + OBddy - OAddy;
const OBix = (i) => (1 - i / div) * OB0tx + (i / div) * OBdtx,
OBiy = (i) => (1 - i / div) * OB0ty + (i / div) * OBdty,
A0B0x = (1 / 2) * (OB0tx - OA0x + OBdtx - OAdx),
A0B0y = (1 / 2) * (OB0ty - OA0y + OBdty - OAdy),
A0A1x = OAix(1) - OA0x,
A0A1y = OAiy(1) - OA0y,
px = (A0A1x * d.padSquare) / 100,
py = (A0A1y * d.padSquare) / 100;
if (d.square) {
g.append("path")
.attr("display", d.square ? "" : "none")
.attr(
"d",
d3.line().curve(d3.curveLinearClosed)([
n <= 1
? [OAix(n - 1), OAiy(n - 1)]
: [OAix(n - 1) + px, OAiy(n - 1) + py],
d.squareEnds || d.arrowStart
? [
OAix(n - 1) +
(1 / 2) * A0B0x -
(1 / 2) * A0B0y * (d.arrowStart ? 1 : 0) * d.arrowFactor +
px * (n <= 1 ? 0 : 1),
OAiy(n - 1) +
(1 / 2) * A0B0y +
(1 / 2) * A0B0x * (d.arrowStart ? 1 : 0) * d.arrowFactor +
py * (n <= 1 ? 0 : 1)
]
: n <= 1
? [OAix(n - 1), OAiy(n - 1)]
: [OAix(n - 1) + px, OAiy(n - 1) + py],
n <= 1
? d.squareEnds || d.arrowStart
? [OAix(n - 1) + A0B0x, OAiy(n - 1) + A0B0y]
: [OB0tx, OB0ty]
: [OAix(n - 1) + A0B0x + px, OAiy(n - 1) + A0B0y + py],
n == div
? d.squareEnds || d.arrowEnd
? [OAix(n) + A0B0x, OAiy(n) + A0B0y]
: [OBdtx, OBdty]
: [OAix(n) + A0B0x - px, OAiy(n) + A0B0y - py],
d.squareEnds || d.arrowEnd
? [
OAix(n) +
(1 / 2) * A0B0x -
(1 / 2) * A0B0y * (d.arrowEnd ? 1 : 0) * d.arrowFactor -
px * (n == div ? 0 : 1),
OAiy(n) +
(1 / 2) * A0B0y +
(1 / 2) * A0B0x * (d.arrowEnd ? 1 : 0) * d.arrowFactor -
py * (n == div ? 0 : 1)
]
: n == div
? [OAix(n), OAiy(n)]
: [OAix(n) - px, OAiy(n) - py],
n == div ? [OAix(n), OAiy(n)] : [OAix(n) - px, OAiy(n) - py]
])
)
.style("stroke", d.stokeColor ? d.strokeColor : d.color)
.style("stroke-width", d.strokeWidth)
.style("fill", fading(d.color, d.fade))
.style("fill-opacity", d.opacity)
.clone()
.lower()
.style("fill", d.opaque ? "white" : "")
.style("fill-opacity", d.opaque ? 1 : 0);
}
if (
d.polyId != d2.polyId ||
(index == ringParam.data.length - 1 && d.polyId)
) {
if (index == ringParam.data.length - 1 && d.polyId)
arrPoints.push([OAixP(1), OAiyP(1)]);
arrPoints = [];
}
arrPoints.push([OAixP(1), OAiyP(1)]);
for (const p in d) {
d2[p] = d[p];
}
const centerText = g
.append("text")
.style("font-weight", d.bold)
.style("fill", darken(d.color, d.darkenFactor))
.attr("font-family", d.fontName)
.attr("font-size", d.fontSize);
const titleLines = d.centerText.split("//");
if (titleLines.length == 1)
centerText
.text(d.centerText)
.attr("text-anchor", "middle")
.attr("dy", -d.up + "em");
if (titleLines.length == 2)
centerText
.text(titleLines[0])
.attr("text-anchor", "middle")
.attr("dy", "-0.8" - d.up + "em")
.clone(true)
.text(titleLines[1])
.attr("dy", "0.6" - d.up + "em");
if (titleLines.length == 3)
centerText
.text(titleLines[0])
.attr("text-anchor", "middle")
.attr("dy", -1.2 - d.up + "em")
.clone(true)
.text(titleLines[1])
.attr("dy", 0.1 - d.up + "em")
.clone(true)
.text(titleLines[2])
.attr("dy", 1.4 - d.up + "em");
const lines = d.text?.split("//");
const numberOfLines = lines?.length;
const line = (d, n) =>
numberOfLines > 1
? lines[n - 1].split("").join("\u200A".repeat(d.letterSpacing))
: d.text?.split("").join("\u200A".repeat(d.letterSpacing));
if (!d.square) {
g.append("path")
.style(
"stroke",
d.strokeColor
? d.strokeFade
? fading(d.strokeColor, d.strokeFade)
: d.strokeColor
: d.strokeFade
? fading(d.color, d.strokeFade)
: d.color
)
.style("stroke-width", d.strokeWidth)
.style(
"fill",
d.gradientID
? "url(#radial-gradient-" + d.gradientID + ")"
: fading(d.color, d.fade)
)
.style("fill-opacity", d.opacity)
.attr(
"d",
d3
.arc()
.innerRadius(R)
.outerRadius(R + r)
.padAngle(d.padAngle)
.startAngle(a)
.endAngle(b)
.cornerRadius(d.cornerRadius)
.padRadius(d.padRadius)
)
.clone()
.lower()
.style("fill", d.opaque ? "white" : "")
.style("fill-opacity", d.opaque ? 1 : 0);
if (!d.radial) {
g.append("path")
.attr("id", id)
.style("fill", "none")
.attr("d", () => {
const context = d3.path();
context.arc(
0,
0,
R,
(atBottom ? b : a) - pi / 2,
(atBottom ? a : b) - pi / 2,
atBottom
);
return context.toString();
});
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 ? d.bold : "normal")
.attr("font-family", d.fontName)
.attr("dy", (d.innerR / 2) * (atBottom ? +1 : -0.9))
.attr("dominant-baseline", "middle")
.attr("text-anchor", "middle")
.append("textPath")
.attr("startOffset", "50%")
.attr("xlink:href", "#" + id)
.text(numberOfLines == 1 ? line(d, 1) : "")
.select(function () {
return this.parentNode;
})
.clone()
.attr(
"dy",
(d.innerR / 2) *
(atBottom ? +0.7 + d.bottomAdjust : -1.3 + d.topAdjust)
)
.append("textPath")
.attr("startOffset", "50%")
.attr("xlink:href", "#" + id)
.text(numberOfLines > 1 ? line(d, 1) : "")
.select(function () {
return this.parentNode;
})
.clone()
.attr(
"dy",
(d.innerR / 2) *
(atBottom ? +1.5 + d.topAdjust : -0.55 + d.bottomAdjust)
)
.append("textPath")
.attr("startOffset", "50%")
.attr("xlink:href", "#" + id)
.text(numberOfLines > 1 ? line(d, 2) : "");
}
}
if (d.radial || d.square) {
function labelTransform(d, twoLines = 0) {
const f = modPi(e - pi / 2);
const df = c / 3;
const f2 =
twoLines == 0
? f
: twoLines == 1
? d.flipLines ^ !atLeft ^ atBottom ^ (atBottom && !atLeft)
? f + df
: f - df
: d.flipLines ^ !atLeft ^ atBottom ^ (atBottom && !atLeft)
? f - df
: f + df;
const cornerX = OAix(n - 1);
const cornerY = OAiy(n - 1);
const halfWayX = d.radial
? twoLines == 0
? (OAix(1) - OAix(0)) / 2
: twoLines == 1
? ((OAix(1) - OAix(0)) * 4) / 6
: ((OAix(1) - OAix(0)) * 2) / 6
: (OAix(1) - OAix(0)) / 2;
const halfWayY = d.radial
? twoLines == 0
? (OAiy(1) - OAiy(0)) / 2
: twoLines == 1
? ((OAiy(1) - OAiy(0)) * 4) / 6
: ((OAiy(1) - OAiy(0)) * 2) / 6
: (OAiy(1) - OAiy(0)) / 2;
const heightLineX = d.radial
? A0B0x / 2
: twoLines == 0
? A0B0x / (atBottom ? 1.8 : 2.2)
: twoLines == 1
? (A0B0x * 4) / 6
: (A0B0x * 2) / 6;
const heightLineY = d.radial
? A0B0y / 2
: twoLines == 0
? A0B0y / (atBottom ? 1.8 : 2.2)
: twoLines == 1
? (A0B0y * 4) / 6
: (A0B0y * 2) / 6;
return d.square
? `translate(${cornerX + halfWayX + heightLineX},
${cornerY + halfWayY + heightLineY}
)
rotate(
${
rTD(f) +
(d.flipLetters ? 180 : 0) +
(d.radial ? 0 : 90) +
(atBottom ? 180 : 0) +
(atLeft && d.radial ? 180 : 0)
}
)`
: `rotate(${rTD(f2)}) translate(${R + r / 2},0) rotate(${
(d.flipLetters ? 180 : 0) + atLeft ? 180 : 0
})`;
}
const topLine =
!d.flipLines ^ (!d.radial && atBottom) ^ (d.radial && !atLeft);
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 ? d.bold : "normal")
.attr("font-family", d.fontName)
.attr("dominant-baseline", "middle")
.attr("text-anchor", "middle")
.text(line(d, topLine ? 1 : 2))
.attr(
"transform",
numberOfLines > 1 ? labelTransform(d, 1) : labelTransform(d, 0)
)
.clone()
.text(numberOfLines > 1 ? line(d, topLine ? 2 : 1) : "")
.attr("transform", labelTransform(d, 2));
}
});
const svgRing = svg.node().innerHTML;
svgOrigin.append("g").html(svgRing);
}