Public
Edited
Nov 11, 2022
1 fork
Insert cell
Insert cell
Insert cell
Insert cell
import { ringFourNobleTruths } from "3915c29fed00300c"
Insert cell
Insert cell
ringTwo = ({
opacity: 0.4,
darkenFactor: 20,
data: dataRingTwo
})
Insert cell
ringThree = ({
opacity: 0.4,
darkenFactor: 20,
square: true,
padAngle: 0.2,
innerR: 30,
data: dataRingThree
})
Insert cell
Insert cell
dataRingThree = {
const arr = [];
for (let i = 0; i < 12; i++) {
const d = { text: "cell " + i };
d.r = 260;
d.startAngle = (360 / 12) * i;
d.endAngle = (360 / 12) * (i + 1);
const colors = ["#a6cee3"];
d.color = colors[0];
arr.push(d);
}
return arr;
}
Insert cell
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
};

// Global Constants

const halfPI = Math.PI / 2;

// Color Functions

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);
}
Insert cell
import { slider } from "@jashkenas/inputs"
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