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, // Add white background behind ring
gradientID: false,
gradientR: 50,
gradientI: 0,
gradientX: 50,
gradientY: 50,
gradientFX: 50,
gradientFY: 50,
invertGradient: false,
// Divisions //
div: 1, // |-endsPadAngle-|-1-|-2-|-n-|-4-|-div-|-endsPadAngle-|
n: 1, // Segment num
gPadAngle: 0, // parallel end-padding for the group (this changes the center of the arc... -_-) needs custom d3
gPadRadius: null, // arcline = padRadius x padAngle (default sqrt(innerRadius * innerRadius + outerRadius * outerRadius))
startGPadAngle: null,
endGPadAngle: null,
//gR: 0,
// carefull padAngle at ends of group still needs padAngle
endsPadAngle: 0, // Angle End Padding
endsPadLinear: false, // Linear End Padding
startRatio: 0, // |sR*L|-1-|-2-|-n-|-4-|-div-|L-eR*L|
endRatio: 1, // L : arcLength/square side depends on R and angles
padRatio: 0, // |sR*L|-startPadRatio-|-text-|-endPadRatio-|L-eR*L|
startPadRatio: false, // padRatio by default
endPadRatio: false, // padRatio by default
// Square Style //
square: false,
squareEnds: false,
centerRadius: true, // Radius is mesured from origin to center of cells.
centerInnerR: true, // Inner Radius is mesured from center of cells.
innerSide: false, // length of inner side, overwrite angles and padAngle
padSquare: 0, // Padding between div cells
tilted: false, // Angle of cells wall (not used)
arrowEnd: false, // end in an arrow
arrowStart: false,
arrowFactor: 1,
// Polygone Coloring //
polyId: false, // Identify the poly
polyPad: 0, // inner Padding
polyOpacity: 1,
polyFade: 0,
polyOpaque: true,
centerText: "",
up: 0
};
//const d3 = customD3; // customD3old;
// Global Constants & function
const pi = Math.PI;
const dTR = (deg) => (pi / 180) * deg; // deg to rad
const rTD = (rad) => (180 / pi) * rad; // rad to deg
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);
// Color Functions
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);
// Create Ring
const svg = d3.create("svg");
// For Filling Polygone later
let arrPoints = [];
let d2 = {}; // alternative data point
const background = svg.append("g").attr("id", "background");
ringParam.data.forEach((dOrigin, index) => {
// d will be our main data element
const structuredClone = (val) => JSON.parse(JSON.stringify(val));
const d = structuredClone(dOrigin);
// Index for adding text to arc
const id = DOM.uid("p-" + d.index);
// define local parameter for each data according to priority
// d.origin (data) > ring Param > Default Param
for (const p in defaultParam) {
dOrigin[p] != undefined
? (d[p] = dOrigin[p])
: ringParam[p] != undefined
? (d[p] = ringParam[p])
: (d[p] = defaultParam[p]);
}
//if (check.includes("outlineOnly")) (d.opacity = 0), (d.polyOpacity = 0);
// All to Rad !
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);
// Definition
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;
// by default d.padRatio
d.startPadRatio = d.startPadRatio !== false ? d.startPadRatio : d.padRatio;
d.endPadRatio = d.endPadRatio !== false ? d.endPadRatio : d.padRatio;
// CenterText
if (d.centerText != "") {
d.text = "";
d.startAngle = 0;
d.endAngle = 0;
d.strokeWidth = 0;
d.opacity = 0;
d.opaque = false;
}
// with group padding NEEDS CUSTOM D3
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);
// console.log(d.text, a, ca, R, gr, r, rp, sgp, egp);
// OuterSquare
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 {
// Calculate angles when division is made
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;
}
// At the bottom ?
e = modPi(e);
const atBottom = e > pi / 2 && e < (3 * pi) / 2,
atLeft = e > pi && e < 2 * pi;
// Start drawing here
const g = svg
.append("g")
.attr("class", "arcLabel")
.attr("transform", `translate(${d.cx}, ${d.cy})`);
// Gradient //
//Append a defs (for definition) element to your SVG
const defs = g.append("defs");
// //Append a radialGradient element to the defs and give it a unique id
// const radialGradient = defs
// .append("radialGradient")
// .attr("id", "radial-gradient-" + d.gradientID)
// .attr("cx", d.gradientX + "%") //The x-center of the gradient
// .attr("cy", d.gradientY + "%") //The y-center of the gradient
// .attr("r", d.gradientR + "%"); //The radius of the gradient
// if (d.gradientFX) radialGradient.attr("fx", d.gradientFX + "%"); //The x-center of the gradient
// if (d.gradientFY) radialGradient.attr("fy", d.gradientFY + "%"); //The x-center of the gradient
// //Add colors to make the gradient
// radialGradient
// .append("stop")
// .attr("offset", d.gradientI + "%")
// .attr(
// "stop-color",
// d.invertGradient ? fading(d.color, d.fade) : "#FFFFFF"
// );
// radialGradient
// .append("stop")
// .attr("offset", "100%")
// .attr(
// "stop-color",
// d.invertGradient ? "#FFFFFF" : fading(d.color, d.fade)
// );
//radialGradient.attr("circle at 10%");
// SQUARE SHAPE //
// Square Calculation (y axis is inverted & angle start at the top !)
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) {
// Square Path
g.append("path")
.attr("display", d.square ? "" : "none")
.attr(
"d",
//d3.line().curve(d3.curveCardinalClosed.tension(0.8))([
d3.line().curve(d3.curveLinearClosed)([
// first point
n <= 1
? [OAix(n - 1), OAiy(n - 1)]
: [OAix(n - 1) + px, OAiy(n - 1) + py],
// second start arrow point
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 // copy of first point
? [OAix(n - 1), OAiy(n - 1)]
: [OAix(n - 1) + px, OAiy(n - 1) + py],
// third point
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],
// fourth point
n == div
? d.squareEnds || d.arrowEnd
? [OAix(n) + A0B0x, OAiy(n) + A0B0y]
: [OBdtx, OBdty]
: [OAix(n) + A0B0x - px, OAiy(n) + A0B0y - py],
// fifth end arrow point
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 // copy of fourth point
? [OAix(n), OAiy(n)]
: [OAix(n) - px, OAiy(n) - py],
// sixth point
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);
}
// fill polygone
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)]);
// Gradient //
//Append a radialGradient element to the defs and give it a unique id
// const radialGradient = defs
// .append("radialGradient")
// .attr("id", "radial-gradient-" + d2.gradientID)
// .attr("cx", d2.gradientX + "%") //The x-center of the gradient
// .attr("cy", d2.gradientY + "%") //The y-center of the gradient
// .attr("r", d2.gradientR + "%"); //The radius of the gradient
// //Add colors to make the gradient
// radialGradient
// .append("stop")
// .attr("offset", d2.gradientI + "%")
// .attr("stop-color", d2.invertGradient ? d2.color : "#FFFFFF");
// radialGradient
// .append("stop")
// .attr("offset", "100%")
// .attr("stop-color", d2.invertGradient ? "#FFFFFF" : d2.color);
// background
// .append("g")
// .append("path")
// .attr("transform", `translate(${d2.cx}, ${d2.cy}) rotate(180)`)
// .attr("d", d3.line().curve(d3.curveLinearClosed)(arrPoints))
// .style("stroke", d2.color)
// .style("stroke-width", d2.strokeWidth)
// .style(
// "fill",
// d2.gradientID
// ? "url(#radial-gradient-" + d2.gradientID + ")"
// : fading(d2.color, d2.polyFade)
// )
// .style("fill-opacity", d2.polyOpacity)
// .clone()
// .lower()
// .style("fill", d2.polyOpaque ? "white" : "")
// .style("fill-opacity", d2.polyOpaque ? 1 : 0);
arrPoints = [];
}
// arrPoints.push([OB0txP, OB0tyP]); // outside of polygone
arrPoints.push([OAixP(1), OAiyP(1)]);
for (const p in d) {
d2[p] = d[p];
}
// Adding Center Text for polygone
const centerText = g
.append("text")
.style("font-weight", d.bold)
.style("fill", darken(d.color, d.darkenFactor))
//.style("fill", check.includes("outlineOnly") ? black : "white")
.attr("font-family", d.fontName)
.attr("font-size", d.fontSize);
const titleLines = d.centerText.split("//");
if (titleLines.length == 1)
// one line break
centerText
.text(d.centerText)
.attr("text-anchor", "middle")
.attr("dy", -d.up + "em");
if (titleLines.length == 2)
// one line break
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)
// two line breaks
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");
// Function that split lines and add spacing
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));
// Arc //
// Arc (!square) cases where lines needs:
// - flipping: radial && !atLeft
// - reverse path: !radial && atBottom
// - lower dy: !radial && atBottom
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", fading(d.color, d.fade))
.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)
//.altCX(d.gcx)
//.altCY(d.gcy)
//.gR(gr)
.padAngle(d.padAngle)
// .startPadAngle(d.startPadAngle) // old custom
// .endPadAngle(d.endPadAngle)
.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) {
// If Arc not radial
// Adding Arc path for text
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) : "");
}
}
// Square Style or Radial
if (d.radial || d.square) {
function labelTransform(d, twoLines = 0) {
// 0 middle, 1 up, 2 down
// For arc radial transform
const f = modPi(e - pi / 2); // clockwise from base
const df = c / 3;
const f2 =
twoLines == 0 // Middle Line
? f
: twoLines == 1 // Top Line
? d.flipLines ^ !atLeft ^ atBottom ^ (atBottom && !atLeft) // sorry for that but I have no time to think about it, it works
? f + df
: f - df
: d.flipLines ^ !atLeft ^ atBottom ^ (atBottom && !atLeft) // Bottom Line
? f - df
: f + df;
// For square transformations
const cornerX = OAix(n - 1);
const cornerY = OAiy(n - 1);
const halfWayX = d.radial
? twoLines == 0 // Middle line
? (OAix(1) - OAix(0)) / 2
: twoLines == 1 // Top Line
? ((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 // Middle line
? A0B0x / (atBottom ? 1.8 : 2.2)
: twoLines == 1 // Top Line
? (A0B0x * 4) / 6
: (A0B0x * 2) / 6; // Bottom Line
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 // arc radial
})`;
}
const topLine =
!d.flipLines ^ (!d.radial && atBottom) ^ (d.radial && !atLeft);
// square cases where needs flipping
// square && !radial && atBottom
// square && 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);
}