Public
Edited
Feb 14
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
chart = {
// add <= at the bottom to change the top label orentation
const sd = 0;
const sw = 0.4;
const so = 1;

// parametres du diagramme
const off = 80; // offset des leaves
const offDNMN = 32; // offset des leaves
let scale = 2.5; // echelle globale
const sizeDepth0 = 28; // font size
const sizeDepth1 = 21;
const sizeDepth2 = 16;
const sizeHeight0 = 4;
const sizeHeight1 = 12;
const size = 12;
const bulletDepth0 = 2.5; // bullet size
const bulletDepth1 = 2;
const bullet = 2;
const bulletHeight1 = 0.8;
const bulletHeight0 = 0.5;
const translation = 0;
const squircle2 = (t) => 1 + (1 / 8) * Math.sin(2 * t) * Math.sin(2 * t);
// const a = 1.1; //1.6;
// const b = 1.1;
const squircle1 = (t) =>
a / Math.sqrt(Math.sqrt(1 - (1 / 2) * Math.sin(2 * t) * Math.sin(2 * t)));
const n = nf * times; //4;
const den = (t, n) =>
Math.pow(Math.cos(t) / a, 2 * n) + Math.pow(Math.sin(t) / b, 2 * n);
const rectellipse = (t) => 1 / Math.pow(den(t, n), 1 / (2 * n));
const squircle = rectellipse;
const speed = (deg) => deg;
const speedRad = (rad) => speed(rad); // bug here.

// parametres angulaires
// let divisor = 1.035 + 0.008 - 0.004 + 0.005; // division of the full circle // 1.8
// let angleDeg = -177.3 + 1.4 - 0.6 + 0.8; // angle offset from midday position // -10 is interesting
// let angleRad = (Math.PI / 180) * angleDeg;
// let sepDeg = 2;
// let sepRad = (Math.PI / 180) * sepDeg;
let divisor = 1; //1.035 + 0.008 - 0.004 - 0.007; // division of the full circle // 1.8
let angleDeg = 0; //+ 3.3; // angle offset from midday position // -10 is interesting
let angleRad = (Math.PI / 180) * angleDeg;
let sepDeg = 0; // 1; //.2;
let sepRad = (Math.PI / 180) * sepDeg;

// ajustements fins du diagramme

data.each(function (d) {
if (
d
.ancestors()
.find(
(x) =>
x.data.id != "sutta" &&
x.data.id != "kn" &&
x.data.id != "kp" &&
x.data.id != "dhp" &&
x.data.id != "bv" &&
x.data.id != "mnd" &&
x.data.id.slice(0, 3) != "cnd" &&
x.data.id.slice(0, 2) != "cp" &&
x.data.id.slice(0, 2) != "ps" &&
x.data.id.slice(0, 2) != "ne" &&
x.data.id.slice(0, 2) != "dn" &&
x.data.id.slice(0, 2) != "mn" &&
x.data.id.slice(0, 2) != "an" &&
x.data.id.slice(0, 2) != "sn" &&
x.data.id != "pe"
) &&
d.height == 1
)
d.children = null;
d.height = 0;
});

const root = tree(data);
// const str = "vaggasaṁyutta";
let heightSN = 800;

root.each(function (d) {
d.y = d.y * scale;
if (d.data.name == "Devatāsaṁyutta") heightSN = d.y;
if (
d
.ancestors()
.find(
(x) =>
x.data.name == "Nidānasaṁyutta" ||
x.data.name == "Khandhasaṁyutta" ||
x.data.name == "Saḷāyatanasaṁyutta"
) &&
d.height == 1
) {
d.y = 890;
d.data.spec = "fontSN";
}
if (d.height == 2) d.y = heightSN;
if (d.depth == 1) d.y = 160;
if (d.depth == 2 && !d.data.acro?.includes("DN")) d.y = 400 + 100 + 30;
if (d.depth == 0) d.x = (-Math.PI * 1.1 - angleRad) * divisor + 2 * Math.PI;
if (d.depth == 0) d.y = -70;
if (d.ancestors().find((x) => x.data.name == "Majjhimanikāya"))
d.x = d.x + sepRad;
if (d.ancestors().find((x) => x.data.name == "Saṁyuttanikāya"))
d.x = d.x + 2 * sepRad;
if (d.ancestors().find((x) => x.data.name == "Aṅguttaranikāya"))
d.x = d.x + 3 * sepRad;

// if (d.height == 1 || d.height == 0) d.y = 600 + 230 + 100 + 40;
// if (d.height == 2) d.y = 490 + 230 + 30 + 30;
if (d.depth == 1) d.y = 260;
if (d.depth == 0) d.x = Math.PI;
if (d.data.name == "test") d.y = 800;
if (d.data.id.slice(0, 3) == "cnd" && d.height == 1)
d.y = 490 + 230 + 30 + 30;
if (d.data.id.slice(0, 3) == "cnd" && d.height == 1) d.data.acro = "";
if (d.data.id.slice(0, 2) == "cp" && d.height == 1)
d.y = 490 + 230 + 30 + 30;
if (d.data.id.slice(0, 2) == "cp" && d.height == 2) d.y = 490 + 100;
if (d.data.id.slice(0, 2) == "cp" && d.height == 2) d.data.acro = "";
if (d.data.id.slice(0, 2) == "ps" && d.height == 1)
d.y = 490 + 230 + 30 + 30;
if (d.data.id.slice(0, 2) == "ps" && d.height == 1) d.data.acro = "";
if (d.data.id.slice(0, 2) == "ne" && d.height == 1)
d.y = 490 + 230 + 30 + 30;
if (d.data.id.slice(0, 2) == "ne" && d.height == 2) d.y = 490 + 100;
if (d.data.id.slice(0, 2) == "ne" && d.height == 1) d.data.acro = "";
});

// fonction d'apartenance à DN ou MN (utile pour les acros)

const isDNMN = (d) =>
d.data.acro?.includes("DN") || d.data.acro?.includes("MN");

// fonction pour la taille de font

let fontSize = (d) => {
if (d.height == 0) {
return sizeHeight0;
} else if (d.data.id.slice(0, 2) == "ne" && d.height == 1) {
return sizeHeight1;
} else if (d.data.id.slice(0, 2) == "ps" && d.height == 1) {
return sizeHeight1;
} else if (d.data.id.slice(0, 3) == "cnd" && d.height == 1) {
return sizeHeight1;
} else if (d.data.id.slice(0, 2) == "cp" && d.height == 1) {
return sizeHeight1;
} else if (d.data.id.slice(0, 4) == "thag" && d.height == 2) {
return sizeHeight1 - 2;
} else if (d.data.id.slice(0, 2) == "ja" && d.height == 2) {
return sizeHeight1 - 1;
} else if (d.depth == 1) {
return sizeDepth2;
} else if (d.height == 1) {
return sizeHeight0;
} else if (d.depth == 2) {
return sizeHeight1;
} else if (d.depth == 0) {
return sizeDepth1;
} else if (d.height == 0) {
return sizeHeight0;
} else if (d.data.spec == "fontSN") {
return sizeHeight0;
} else if (d.data.id.slice(0, 2) == "sn" && d.height == 1) {
return sizeHeight1 - 1;
} else if (d.depth == 1) {
return sizeDepth1;
} else if (d.height == 1 && !d.data.acro.includes("AN")) {
return sizeHeight1;
} else if (d.depth == 2) {
return sizeDepth2;
} else if (d.depth == 0) {
return sizeDepth0;
} else return size;
};

// fonction pour la taille des bullets

let bulletSize = (d) => {
if (d.depth == 0) {
return bulletDepth0;
} else if (
d.data.acro == "SN 12" ||
d.data.acro == "SN 22" ||
d.data.acro == "SN 35"
) {
return bulletHeight1;
} else if (d.data.id == "test" || d.data.id == "test2") {
return 0;
} else if (d.depth == 1) {
return bulletDepth1;
} else if (d.height == 0) {
return bulletHeight0;
} else if (d.height == 1 && !d.data.acro.includes("AN")) {
return bulletHeight1;
} else return bullet;
};

// Fonction qui retourne le nom de l'ancestre de depth 1
// utile pour la colorisation

let oneDepth = (d, source = false) => {
let ancestors = {};
source == true
? (ancestors = d.target.ancestors()) // source <=> target for a different result
: (ancestors = d.ancestors());

const oneDepth = ancestors.find((d) => d.depth == 1);
const result = oneDepth == null ? d.data.name : oneDepth.data.name;
// for some reason it dosen't start with sutta pitaka anymore
return result;
// return oneDepth?.data.name;
};
// let firstElement = domain.shift();
// domain.push(firstElement);

let color = d3.scaleOrdinal(domain);
// let colorFaded = d3.scaleOrdinal(domainFaded);

// Original domain array

// Function to deep clone the array
function deepCloneArray(arr) {
return JSON.parse(JSON.stringify(arr));
}

// Function to shift the array
function shiftArray(arr, shiftCount) {
let clonedArray = deepCloneArray(arr);
for (let i = 0; i < shiftCount; i++) {
clonedArray.unshift(clonedArray.pop());
}
return clonedArray;
}

// Example usage
let color2 = d3.scaleOrdinal(shiftArray(domain, sd));

let isLeaf = (d) => {
// let bool = false;
// if (d.height == 0) bool = true;
// return bool;
return d.height === 0;
};

const fade = (color) => {
const c = d3.color(color);
c.l += 0.25;
return c + "";
};

///////////////////////////////////////////////////////////////////////////////////////
///////////////////////////// Parallels /////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////
// const leavesJSON = [];
root.leaves().forEach((leaf, index) => {
leaf.data.facro = leaf.data.acro.toLowerCase().replace(/ /g, "");
leaf.data.parallels = [
...leaveswithfixparallels[index].parallelsInSuttaPtiaka
]; // refeed from the JSON
// leavesJSON.push({ ...leaf.data }); // was used to create a JSON to be processed
});

root.leaves().forEach((leaf) => {
leaf.outgoing = [];
leaf.data.parallels.forEach((parallel) => {
// console.log("---");
// console.log(leaf);
// console.log(parallel);
const target = root.find((d) => d.data.facro == parallel);
if (target) leaf.outgoing.push([leaf, target]);
});
});

// console.log(root.leaves());

// see https://cittadhammo.github.io/parallels/ for how to create leaveswithparallels

// let links = [];
// root.leaves().forEach((leaf) => {
// if (leaf.data.parallels) {
// // exculde index page for the moment?
// leaf.data.parallels.forEach((parallel) =>
// links.push({
// source: leaf.data.facro,
// target: parallel
// })
// );
// }
// });

// console.log(links);

//////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////// création du diagramme ///////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////

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

const g = svg.append("g").attr("cursor", "grab");

// // // // // // // // append arc category // // // // // // // // //
let myColor = (d) =>
color(d.ancestors().find((ancestor) => ancestor.depth == 1).data.name);

const drawRingAround = (
level = 2,
radiusCat = 1000,
catRadius = 50,
fontSize = 10,
offAngle = 0
) => {
let leaves = root.leaves();

let currentCategory = leaves[0]
.ancestors()
.find((ancestor) => ancestor.depth == level).data.name;
console.log(currentCategory);
let currentColor = myColor(
leaves[0].ancestors().find((ancestor) => ancestor.depth == 1)
);
let currentFirstX = leaves[0].x;
let currentLastX = leaves[0].x;
let catArray = [];

for (let i = 1; i < leaves.length; i++) {
if (
leaves[i].ancestors().find((ancestor) => ancestor.depth == level).data
.name !== currentCategory
) {
catArray.push({
color: currentColor,
name: currentCategory,
start: currentFirstX,
end: currentLastX
});
currentColor = myColor(
leaves[i].ancestors().find((ancestor) => ancestor.depth == 1)
);
currentCategory = leaves[i]
.ancestors()
.find((ancestor) => ancestor.depth == level).data.name;
currentFirstX = leaves[i].x;
}
currentLastX = leaves[i].x;
}

catArray.push({
color: currentColor,
name: currentCategory,
start: currentFirstX,
end: currentLastX
});

const catData = catArray.map((d) => {
return {
color: d.color,
text: d.name,
startAngle: (d.start / Math.PI) * 180,
endAngle: (d.end / Math.PI) * 180
};
});

drawRing(g, {
fontSize: fontSize,
r: radiusCat + 8,
r: radiusCat + 8 + catRadius / 2,
innerR: catRadius / 1 - 7,
opacity: 0,
padAngle: 0,
endsPadAngle: -offAngle,
opaque: false,
//radial: true,
cornerRadius: 5,
data: catData
});
};

// level = 2,
// radiusCat = 1000,
// catRadius = 50,
// fontSize = 10
// offAngle = 0,

//drawRingAround(3, 1000 + 50, 20, 5);
//drawRingAround(2, 1000, 20, 1);
drawRingAround(2, 1030, 20, 8);
drawRingAround(1, 1044, 40, 20);
//drawRingAround(4, 1165);

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

const AH = 2150 * frameRatio,
AW = 2150 * frameRatio;

g.append("rect")
.attr("x", translation / 2 - AW / 2)
.attr("y", -AH / 2)
.attr("width", AW)
.attr("height", AH)
.attr("stroke", frame ? "black" : "white")
.attr("fill", "transparent");

// paths

if (hierarchy) {
g.append("g")
.attr("fill", "none")
.attr("stroke", "#555")
.attr("stroke-opacity", 0.4)
.attr("stroke-width", 0.5)
.selectAll("path")
.data(root.links())
.join("path")
.attr(
"d",
d3
.linkRadial()
.angle((d) => speedRad(d.x / divisor + angleRad))
.radius((d) => d.y * squircle(d.x))
)
.attr("stroke", (d) => color(oneDepth(d, true))); // colorisation
}
// bullet

g.append("g")
.selectAll("circle")
.data(root.descendants())
.join("circle")
.attr(
"transform",
(d) => `
rotate(${speed((d.x * 180) / divisor / Math.PI - 90 + angleDeg)})
translate(${d.y * squircle(d.x)},0)
`
)
.attr("r", (d) => bulletSize(d))
.attr("fill", (d) =>
isLeaf(d) ? fade(color(oneDepth(d))) : color(oneDepth(d))
)
.attr("fill-opacity", (d) => (d.height == 0 ? 1 : 0));

// text

g.append("g")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.attr("stroke-linejoin", "round")
.attr("stroke-width", 3)
.selectAll("text")
.data(root.descendants())
.join("text")
.attr("font-size", (d) => fontSize(d))
.style("fill", (d) => color(oneDepth(d))) // colorisation
.attr("transform", (d) => {
// console.log(d.data.name, d.x, Math.PI - angleRad);
return `
rotate(${speed((d.x * 180) / divisor / Math.PI - 90 + angleDeg)})
translate(${d.y * squircle(d.x)},0)
rotate(${
d.x > (Math.PI - angleRad) * divisor &&
d.x < (2 * Math.PI - angleRad) * divisor
? 180
: 0
// 0
})
`;
})
.attr("dy", "0.31em")
.attr("x", (d) =>
(d.x > (Math.PI - angleRad) * divisor &&
d.x < (2 * Math.PI - angleRad) * divisor) === !d.children
? -6
: 6
)
.attr("text-anchor", (d) =>
(d.x > (Math.PI - angleRad) * divisor &&
d.x < (2 * Math.PI - angleRad) * divisor) === !d.children
? "end"
: "start"
)
.text((d) => {
if (d.depth == 0 || d.depth == 1) return d.data.name;
if (d.height != 0 && isDNMN(d)) return d.data.name;
if (d.data.acro == "") return d.data.name; // not usefull for KN
if (
d.x > (Math.PI - angleRad) * divisor &&
d.x < (2 * Math.PI - angleRad) * divisor
) {
return !d.children
? d.data.name + " : " + d.data.acro
: d.data.acro + " : " + d.data.name;
} else {
return !d.children
? d.data.acro + " : " + d.data.name
: d.data.name + " : " + d.data.acro;
}
})
.style("cursor", "pointer")
.on("click", function (d, i) {
// const link = "https://suttacentral.net/" + i.data.id + "/pli/ms";
let link = "https://suttacentral.net/" + i.data.id;
if (i.data.id == "suttaPitaka")
link = "https://suttacentral.net/pitaka/sutta";
window.open(link);
})
.attr("fill-opacity", (d) => (isLeaf(d) ? 1 : 0))
.clone(true)
.lower()
.attr("stroke", "white");

////////////////////////////////////// bundling //////////////////////

if (bundling) {
// const line = d3
// .lineRadial()
// .curve(d3.curveBundle.beta(0.85))
// .radius((d) => d.y)
// .angle((d) => d.x);

const link = svg
.append("g")
.attr("stroke", "black")
.attr("fill", "none")
.attr("stroke-width", sw)
.attr("stroke-opacity", so)
.selectAll("path")
.data(root.leaves().flatMap((leaf) => leaf.outgoing))
.join("path")
.style("mix-blend-mode", "multiply")
.style("opacity", so)
.attr("d", ([i, o]) => line(i.path(o)))
.attr("stroke", (d) => {
//console.log(d);
//const nodeInRoot = root.find((node) => node.id == d[0].id);
return color(oneDepth(d[0]));
// color(
// d[1].ancestors().find((ancestor) => ancestor.depth == 1).data.name
// )
//myColor(root.find((node) => d.depth == 1))
}) // colorisation
.each(function (d) {
d.path = this;
});
}

////////////////////////////////////////////////////// Start of License ///////////////////////////////////////////////

const fontName = "Poppins";

GFontToDataURI(
"https://fonts.googleapis.com/css2?family=Poppins&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));

if (license) {
// svg
// .append("svg:image")
// .attr("x", xLicense - 88)
// .attr("y", yLicense - 65)
// // .attr("width", 88)
// // .attr("height", 31)
// .attr(
// "xlink:href",
// // "https://mirrors.creativecommons.org/presskit/buttons/88x31/svg/cc-zero.svg"
// await FileAttachment("cc-zero.svg").url()
// );
svg
.append("g")
.attr("transform", `translate(${xLicense - 88}, ${yLicense - 65})`)
.html(logo);

const text = svg
.append("text")
.attr("x", xLicense)
.attr("y", yLicense)
.style("font-weight", "bold")
// .style("fill", "#828282")
.attr("font-family", fontName)
.attr("font-size", fLicense)
.text("DhammaCharts.org")
.attr("text-anchor", "end")
.attr("dy", "-1.1em")
.clone(true)
.text("SuttaCentral.net")
.attr("dy", "0.25em");
// .clone(true)
// .text("No Right reserved")
// .attr("dy", "1.6em");
}

if (licenseNoLogo) {
// svg
// .append("svg:image")
// .attr("x", xLicense - 88)
// .attr("y", yLicense - 65)
// // .attr("width", 88)
// // .attr("height", 31)
// .attr(
// "xlink:href",
// // "https://mirrors.creativecommons.org/presskit/buttons/88x31/svg/cc-zero.svg"
// await FileAttachment("cc0gris@3.svg").url()
// );

const text = svg
.append("text")
.attr("x", xLicense)
.attr("y", yLicense)
.style("font-weight", "bold")
.style("fill", "#828282")
.attr("font-family", fontName)
.attr("font-size", fLicense)
.text("CC0 - Public Domain")
.attr("text-anchor", "end")
.attr("dy", "-2.45em")
.clone(true)
.text("DhammaCharts.org")
.attr("dy", "-1.1em")
.clone(true)
.text("SuttaCentral.net")
.attr("dy", "0.25em");
}

////////////////////////////////////////////////////// End of License ///////////////////////////////////////////////

// zoom

if (zoompan) {
g.call(
d3
.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended)
);

// zooming + pan;

svg.call(
d3
.zoom()
.extent([
[0, 0],
[width, height]
])
.scaleExtent([1, 8])
.on("zoom", zoomed)
);

function dragstarted() {
d3.select(this).raise();
g.attr("cursor", "grabbing");
}

function dragged(event, d) {
d3.select(this)
.attr("cx", (d.x = event.x))
.attr("cy", (d.y = event.y));
}

function dragended() {
g.attr("cursor", "grab");
}

function zoomed({ transform }) {
g.attr("transform", transform);
}
}
//return leavesJSON;
return svg.attr("viewBox", autoBox).node();
}
Insert cell
Insert cell
Insert cell
Insert cell
parallelsFiltered = parallels.map((d) =>
d.parallels?.map((p) => p.split("#")[0].replace(/~/g, ""))
)
Insert cell
leaveswithparallels = FileAttachment("leavesWithParallels.json").json()
Insert cell
leaveswithfixparallels = FileAttachment("leavesWithFixParallels.json").json()
Insert cell
parallels = fetch(
"https://raw.githubusercontent.com/suttacentral/sc-data/main/relationship/parallels.json"
).then((response) => response.json())
Insert cell
Insert cell
FileAttachment("cc0gris@3.svg")
Insert cell
FileAttachment("cc0gris@3.svg").html().innerHTML
Insert cell
// {
// var wrapper = document.createElement("div");
// wrapper.innerHTML = logoHTML;
// return wrapper.firstChild;
// }
Insert cell
// logoHTML = fetch(
// "https://mirrors.creativecommons.org/presskit/buttons/88x31/svg/cc-zero.svg"
// )
// .then((response) => response.text())
// // .then((svg) => document.body.insertAdjacentHTML("afterbegin", svg))
Insert cell
// chart2.attr("viewBox", autoBox).node()
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
domain2 = [col5, col2, col3, col4, col1, col6]
Insert cell
d3.scaleOrdinal(domain)("test")
Insert cell
domain = colors2
Insert cell
domainFaded = colors2Faded
Insert cell
Insert cell
// the old color funciton


Insert cell
function autoBox() {
document.body.appendChild(this);
const {x, y, width, height} = this.getBBox();
document.body.removeChild(this);
return [x, y, width, height];
}
Insert cell
Insert cell
data = {
return d3.stratify()(suttapitakatreefull);
}
Insert cell
data2 = {
return d3.stratify()(kn3);
}
Insert cell
// data.each((d) => // console.log(d.data.name))
Insert cell
// import { SuttaPitakaTree } from "eb4984f02c934460"
import { suttapitakatreefull } from "eb4984f02c934460"
Insert cell
import { kn3 } from "eb4984f02c934460"
Insert cell
width = 975
Insert cell
height = 600
Insert cell
radius = width / 2
Insert cell
tree = d3.cluster().size([2 * Math.PI, radius - 100])
Insert cell
Insert cell
d3 = require("d3@6")
Insert cell
import { colorPicker } from "@shaunlebron/color-picker"
Insert cell
colors2 = subdivs(72, 378, divs).map((x) => color2(x + colorShift, s, l))
Insert cell
colors2Faded = subdivs(72, 378, divs).map((x) => color2(x + colorShift, s, l))
Insert cell
color2 = (h, s = 50, l = 50) => "hsl(" + h + "," + s + "%," + l + "%)"
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// from https://stackoverflow.com/questions/63288390/font-style-of-text-in-svg-not-propagating-to-png-on-download

/*
Only tested on a really limited set of fonts, can very well not work
This should be taken as an proof of concept rather than a solid script.
@Params : an url pointing to an embed Google Font stylesheet
@Returns : a Promise, fulfiled with all the cssRules converted to dataURI as an Array
*/
function GFontToDataURI(url) {
return fetch(url) // first fecth the embed stylesheet page
.then((resp) => resp.text()) // we only need the text of it
.then((text) => {
// now we need to parse the CSSruleSets contained
// but chrome doesn't support styleSheets in DOMParsed docs...
let s = document.createElement("style");
s.innerHTML = text;
document.head.appendChild(s);
let styleSheet = s.sheet;

// this will help us to keep track of the rules and the original urls
let FontRule = (rule) => {
let src =
rule.style.getPropertyValue("src") ||
rule.style.cssText.match(/url\(.*?\)/g)[0];
if (!src) return null;
let url = src.split("url(")[1].split(")")[0];
return {
rule: rule,
src: src,
url: url.replace(/\"/g, "")
};
};
let fontRules = [],
fontProms = [];

// iterate through all the cssRules of the embedded doc
// Edge doesn't make CSSRuleList enumerable...
for (let i = 0; i < styleSheet.cssRules.length; i++) {
let r = styleSheet.cssRules[i];
let fR = FontRule(r);
if (!fR) {
continue;
}
fontRules.push(fR);
fontProms.push(
fetch(fR.url) // fetch the actual font-file (.woff)
.then((resp) => resp.blob())
.then((blob) => {
return new Promise((resolve) => {
// we have to return it as a dataURI
// because for whatever reason,
// browser are afraid of blobURI in <img> too...
let f = new FileReader();
f.onload = (e) => resolve(f.result);
f.readAsDataURL(blob);
});
})
.then((dataURL) => {
// now that we have our dataURI version,
// we can replace the original URI with it
// and we return the full rule's cssText
return fR.rule.cssText.replace(fR.url, dataURL);
})
);
}
document.head.removeChild(s); // clean up
return Promise.all(fontProms); // wait for all this has been done
});
}
Insert cell
line = d3
.lineRadial()
.curve(d3.curveBundle)
.radius((d) => d.y)
.angle((d) => d.x)
Insert cell
function path([source, target]) {
const p = new Path();
line.context(p)(source.path(target));
return p;
}
Insert cell
class Path {
constructor(_) {
this._ = _;
this._m = undefined;
}
moveTo(x, y) {
this._ = [];
this._m = [x, y];
}
lineTo(x, y) {
this._.push(new Line(this._m, (this._m = [x, y])));
}
bezierCurveTo(ax, ay, bx, by, x, y) {
this._.push(
new BezierCurve(this._m, [ax, ay], [bx, by], (this._m = [x, y]))
);
}
*split(k = 0) {
const n = this._.length;
const i = Math.floor(n / 2);
const j = Math.ceil(n / 2);
const a = new Path(this._.slice(0, i));
const b = new Path(this._.slice(j));
if (i !== j) {
const [ab, ba] = this._[i].split();
a._.push(ab);
b._.unshift(ba);
}
if (k > 1) {
yield* a.split(k - 1);
yield* b.split(k - 1);
} else {
yield a;
yield b;
}
}
toString() {
return this._.join("");
}
}

Insert cell
class Line {
constructor(a, b) {
this.a = a;
this.b = b;
}
split() {
const { a, b } = this;
const m = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
return [new Line(a, m), new Line(m, b)];
}
toString() {
return `M${this.a}L${this.b}`;
}
}
Insert cell
BezierCurve = {
const l1 = [4 / 8, 4 / 8, 0 / 8, 0 / 8];
const l2 = [2 / 8, 4 / 8, 2 / 8, 0 / 8];
const l3 = [1 / 8, 3 / 8, 3 / 8, 1 / 8];
const r1 = [0 / 8, 2 / 8, 4 / 8, 2 / 8];
const r2 = [0 / 8, 0 / 8, 4 / 8, 4 / 8];

function dot([ka, kb, kc, kd], { a, b, c, d }) {
return [
ka * a[0] + kb * b[0] + kc * c[0] + kd * d[0],
ka * a[1] + kb * b[1] + kc * c[1] + kd * d[1]
];
}

return class BezierCurve {
constructor(a, b, c, d) {
this.a = a;
this.b = b;
this.c = c;
this.d = d;
}
split() {
const m = dot(l3, this);
return [
new BezierCurve(this.a, dot(l1, this), dot(l2, this), m),
new BezierCurve(m, dot(r1, this), dot(r2, this), this.d)
];
}
toString() {
return `M${this.a}C${this.b},${this.c},${this.d}`;
}
};
}
Insert cell
drawRing = (svgOrigin, ringParam) => {
const defaultParam = {
// Geometry //
cx: 0, // Angles are mesured from top like a clock in deg
cy: 0,
r: 100,
innerR: 20,
padAngle: 1, // Angle Padding (actually work linear)
startPadAngle: null, // used with customD3 old one not used here...
endPadAngle: null, // need uncomment at the arc() function
padRadius: null, // arcline = padRadius x padAngle (default sqrt(innerRadius * innerRadius + outerRadius * outerRadius))
padLinear: false, // Linear Padding (not used)
cornerRadius: 0,

// Aspect //
radial: false, // Direction of text
strokeWidth: 0.6,
strokeColor: false,
strokeFade: false,
fontSize: 12,
lineHeight: 0,
fontName: "sans-serif",
letterSpacing: 0,
flipLetters: false, // Upside Down
flipLines: false, // Top & Bottom
//bold: selectedFont != "Cormorant SC" ? 400 : 700,
bottomAdjust: 0, // Line Height
topAdjust: 0, // Line height

// Coloring //
color: "black",
fade: 0,
fadeLimit: 30, // Turn the background dark and letters white
opacity: 1,
opacityLimit: 0.8, // same (could have a variable switchColor)
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);
}
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