Public
Edited
Mar 25
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
DOM.download(serialize(chart), undefined, "Download as SVG")
Insert cell
chart = {
const sw = 0.4;
const so = 1;
const cols = 20;
const offSet = 0;
const off = 38;
const strokeWidth = 2;
// const ray = 55;
const ray = 55 + 22;
// const ray2 = 100;
const ray2 = 100 + 22;
// const ray3 = 22;
const ray3 = 22 + 22 - 10;
const offnumber = 22;
const offAngle = (Math.PI / 180) * 1.3;
const fontname = "sans-serif";
// const fontname = "IM Fell English SC";

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

// const magic = (d) => (d.data.subcat != "" ? d.data.subcat : d.data.cat);
const magic = (d) => (d.data.cat != "" ? d.data.cat : d.data.cat);

// const myColor = d3.scaleOrdinal(d3.schemeSpectral[7]);
// const myColor = d3.scaleOrdinal(d3.schemeSet1);
// const myColor = d3.scaleOrdinal(
// d3.quantize(d3.interpolateHcl("#60c96e", "#4d4193"), 10)
// );
// const myColor = d3.scaleOrdinal(d3.quantize(d3.interpolateRainbow, cols));
const colorFunc = myColor(5, 1, 9, 0.7);

function darken(color, k = 1) {
const { l, c, h } = d3.lch(color);
return d3.lch(l - 30 * k, c, h);
}

function lighten(color, k = 1) {
const { l, c, h } = d3.lch(color);
return d3.lch(l + 30 * k, c, h);
}

const root = tree(
rootWithIngoing.sort((a, b) => d3.ascending(a.data.subcat, b.data.subcat))
);

const svg = d3.create("svg");
// .attr("viewBox", [-width / 2, -width / 2, width, width]);

const ringCat = {
cx: 0,
cy: 0,
r: 275 - 5,
innerR: 20 - 5,
padAngle: 0,
color: colorFunc,
strokeWidth: 0.5,
bold: false,
data: dataSubCat2Level2,
// data: myData((Math.PI / 360) * 3).slice(2),
fontSize: 8,
fontName: fontname
};
if (categories) svg.append("g").html(ringObjHTML(ringCat));

const ringCat2Level = {
cx: 0,
cy: 0,
r: 250,
innerR: 45 - 5 - 5,
padAngle: 0,
color: colorFunc,
strokeWidth: 0.5,
bold: false,
data: dataSubCat2Level,
fontSize: 10,
fontName: fontname
};
if (categories) svg.append("g").html(ringObjHTML(ringCat2Level));

const ringSubCat = {
cx: 0,
cy: 0,
r: 250,
innerR: 20 - 5,
padAngle: 0,
color: colorFunc,
strokeWidth: 0.5,
bold: false,
data: dataSubCat,
fontSize: 7,

fontName: fontname
};
if (categories) svg.append("g").html(ringObjHTML(ringSubCat));

const node = svg
.append("g")
.attr("font-family", "sans-serif")
// .attr("font-size", 9)
.attr("stroke-linejoin", "round")
.attr("stroke-width", 3)
.attr("font-size", 8)
.selectAll("g")
.data(root.leaves())
.join("g")
.attr(
"transform",
(d) => `rotate(${(d.x * 180) / Math.PI - 90}) translate(${d.y},0)`
)
.append("text")
.attr("dy", "0.31em")
.attr("x", (d) => (d.x < Math.PI ? 6 : -6 - offnumber))
.attr("text-anchor", (d) => (d.x < Math.PI ? "start" : "start"))
.attr("transform", (d) => (d.x >= Math.PI ? "rotate(180)" : null))
.style("fill", (d) => colorFunc(magic(d)))

.text((d) => {
if (d.x < Math.PI - offSet) {
if (typeof d.data.num !== "undefined") {
if (d.data.num.length <= 4) {
var str = d.data.num;
var nameOnly = str.substring(0, str.length - 1);
var numOnly = str.substring(str.length - 1, str.length);
d.data.num = nameOnly + "\xa0\xa0" + numOnly;
}
return d.data.num;
} else return d.data.content;
} else {
if (typeof d.data.num !== "undefined") {
if (d.data.num.length <= 4) {
var str = d.data.num;
var nameOnly = str.substring(0, str.length - 1);
var numOnly = str.substring(str.length - 1, str.length);
d.data.num = nameOnly + "\xa0\xa0" + numOnly;
}
return d.data.num;
} else return d.data.content;
}
});
// .clone(true)
// .lower()
// .attr("stroke", "white")
// .clone(true)
// // .attr("stroke-width", 0)
// // .attr("transform", (d) => `translate(${ray3},0)`) // .attr("transform", `translate(${ray3},0)`)
// // .attr("dy", "0.31em")
// .attr("text-anchor", (d) => (d.x < Math.PI ? "start" : "end"))
// .attr("transform", (d) => (d.x >= Math.PI ? "rotate(180)" : null))
// .attr("x", (d) =>
// d.x < Math.PI - offSet === !d.children ? off + ray3 : -off - ray3
// )
// .style("fill", (d) => colorFunc(magic(d)))
// .text((d) => {
// // return d.data.content + " (" + magic(d).slice(1) + ")";
// return d.data.content;
// });

// CONNECTIONS //

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

console.log(root.leaves());

const link = svg
.append("g")
// .attr("stroke", "red")
.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) => lighten(colorFunc(magic(d[1]))))
.each(function (d) {
d.path = this;
});

// svg
// .append("g")
// .attr("fill", "none")
// .selectAll("path")
// .data(
// d3.transpose(
// root
// .leaves()
// .filter((rule) => rule.outgoing)
// .flatMap((leaf) => leaf.outgoing.map(path))
// .map((path) => Array.from(path.split(k)))
// )
// )
// .join("path")
// .style("mix-blend-mode", "darken")
// .attr("stroke", (d, i) => color(d3.easeQuad(i / ((1 << k) - 1))))
// // .attr("stroke", (d, i) => {
// // // const magic = d.data.subcat != "" ? d.data.subcat : d.data.cat;
// // color(d3.easeQuad(i / (1 << k)));
// // })
// // .attr("stroke", (d, i) => color(i - 1))
// .attr("d", (d) => d.join(""));

return svg.attr("viewBox", autoBox).node();
}
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
myData((Math.PI / 360) * 3)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
myDataArcsubcat = (offAngle) => {
const myDataArc = [];
let i = 0;
rootUsed.each((d) => {
const push = {};
if (d.height == 1) {
push.data = d.data[0].slice(1);
push.index = i;
i++;
push.value = 1;
push.startAngle = d.children[0].x - offAngle;
push.endAngle = d.children[d.children.length - 1].x + offAngle;
myDataArc.push(push);
}
});
return myDataArc;
}
Insert cell
myDataArc = (offAngle) => {
const myDataArc = [];
let i = 0;
rootUsed.each((d) => {
const push = {};
if (d.height == 1) {
push.data = d.data[0].slice(1);
push.index = i;
i++;
push.value = 1;
push.startAngle = d.children[0].x - offAngle;
push.endAngle = d.children[d.children.length - 1].x + offAngle;
myDataArc.push(push);
}
});
return myDataArc;
}
Insert cell
rootUsed3 = tree(
rootWithIngoing.sort((a, b) => d3.ascending(a.data.subcat, b.data.subcat))
)
Insert cell
dataArc = d3
.pie()
.sort(null)
.value((d) => 1)([
"Samana Etiquette",
"Communal Harmony",
"Right Livelyhood",
"Right Action",
"Right Speech"
])
Insert cell
url = "https://docs.google.com/spreadsheets/d/e/2PACX-1vRdNo7gB5mA7rJUYOxIyvljDYeb5t5pgW60eGGBjzQ9f0JlTrCIo_e3QmHly9kgQ4A_TiIqRHECGfDN/pub?output=csv"
Insert cell
rootSub = d3.hierarchy(data3)
Insert cell
data3 = d3.group(
csvData,
(d) => d.cat
// (d) => d.subcat
)
Insert cell
Insert cell
root2 = d3.hierarchy(data2)
Insert cell
array = [["test", "test2"]]
Insert cell
rootSorted = rootWithIngoing.sort((a, b) =>
d3.ascending(a.data.subcat, b.data.subcat)
)
Insert cell
rootWithIngoing = {
rootNew.each((node) => {
let foundLeaf = 0;
// tree() necessary ?
if (node.height == 0) {
// on leaves ?
rootNew.each((leaf) => {
if (leaf.outgoing) {
// check if leaf has outgoing
for (var i = 0; i < leaf.outgoing.length; i++) {
// check if leaf is outgoing to node
if (leaf?.outgoing[i][1] == node) {
foundLeaf = 1;
if (!node.incoming) {
node.incoming = [[leaf, node]];
} else {
node.incoming = [...node.incoming, [leaf, node]];
}
}
}
}
});
}
if (node.height == 0 && foundLeaf == 0) node.incoming = [];
});
return rootNew;
}
Insert cell
rootNew = tree(rootWithOutgoing)
Insert cell
rootWithOutgoing = {
rootSub.each((d) => {
if (d.data.related) {
const ruleNums = d3.csvParse(d.data.related).columns; // an array

const ruleNames = ruleNums.map((ruleNum) => {
ruleNum = ruleNum.trim();
const name = rootSub.find((x) => x.data.num == ruleNum);
return [d, name];
});
d.outgoing = ruleNames;
} else if (d.height == 0) {
d.outgoing = [];
}
});
return rootSub;
}
Insert cell
Insert cell
data = hierarchy(await FileAttachment("flare.json").json())
Insert cell
d3.hierarchy(data)
Insert cell
function hierarchy(data, delimiter = ".") {
let root;
const map = new Map;
data.forEach(function find(data) {
const {name} = data;
if (map.has(name)) return map.get(name);
const i = name.lastIndexOf(delimiter);
map.set(name, data);
if (i >= 0) {
find({name: name.substring(0, i), children: []}).children.push(data);
data.name = name.substring(i + 1);
} else {
root = data;
}
return data;
});
return root;
}
Insert cell
Insert cell
function id(node) {
return `${node.parent ? id(node.parent) + "." : ""}${node.data.name}`;
}
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
line = d3.lineRadial()
.curve(d3.curveBundle)
.radius(d => d.y)
.angle(d => d.x)
Insert cell
tree = d3.cluster()
.size([2 * Math.PI, radius - 100])
Insert cell
color = t => d3.interpolateRdBu(1 - t)
Insert cell
k = 6 // 2^k colors segments per curve
Insert cell
width = 954
Insert cell
radius = width / 3
Insert cell
d3 = require("d3@6")
Insert cell
// from https://stackoverflow.com/questions/196972/convert-string-to-title-case-with-javascript?noredirect=1&lq=1
function toTitleCase(str) {
const articles = ["a", "an", "the"];
const conjunctions = ["for", "and", "nor", "but", "or", "yet", "so"];
const prepositions = [
"with",
"at",
"from",
"into",
"upon",
"of",
"to",
"in",
"for",
"on",
"by",
"like",
"over",
"plus",
"but",
"up",
"down",
"off",
"near"
];

// The list of spacial characters can be tweaked here
const replaceCharsWithSpace = (str) =>
str.replace(/[^0-9a-z&/\\]/gi, " ").replace(/(\s\s+)/gi, " ");
const capitalizeFirstLetter = (str) =>
str.charAt(0).toUpperCase() + str.substr(1);
const normalizeStr = (str) => str.toLowerCase().trim();
const shouldCapitalize = (word, fullWordList, posWithinStr) => {
if (posWithinStr == 0 || posWithinStr == fullWordList.length - 1) {
return true;
}

return !(
articles.includes(word) ||
conjunctions.includes(word) ||
prepositions.includes(word)
);
};

//str = replaceCharsWithSpace(str);
str = normalizeStr(str);

let words = str.split(" ");
if (words.length <= 2) {
// Strings less than 3 words long should always have first words capitalized
words = words.map((w) => capitalizeFirstLetter(w));
} else {
for (let i = 0; i < words.length; i++) {
words[i] = shouldCapitalize(words[i], words, i)
? capitalizeFirstLetter(words[i], words, i)
: words[i];
}
}

const str2 = words.join(" ");

words = str2.split("-");
words = words.map((w) => capitalizeFirstLetter(w));
return words.join("-");
}
Insert cell
// Function that create a HTML layer arc of multile labels with options.
// data contains the angles, text, font-size & letter-spacing...

ringObjHTML = (ring) => {
const {
cx,
cy,
r,
innerR,
padAngle = 1,
color = "red",
data, // contains : d.startAngle d.endAngle d.text d.fontSize=12 d.letterSpace=1 d.textOffAngle
// d.uid (d.index?) is created in this function
strokeWidth = 2, // 0 invert colors and fill up
bold = true,
fontName,
fontSize = 12
} = ring;

const haveClass = (d) => (d.class ? d.class : d.uid);
const flipLetters = (d) => (d.flipLetters ? true : false);
const dStrokeWidth = (d) =>
d.strokeWidth != undefined ? d.strokeWidth : strokeWidth;

const halfPI = Math.PI / 2;
const svg = d3.create("svg");
const arc = d3
.arc()
.innerRadius(r)
.outerRadius(r + innerR)
.padAngle((Math.PI / 180) * padAngle)
.startAngle((d) => (Math.PI / 180) * d.startAngle)
.endAngle((d) => (Math.PI / 180) * d.endAngle)
.cornerRadius(10);
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
};
const enter = (enter) => {
const g = enter
.append("g")
.attr("class", "arcLabel")
.attr("transform", `translate(${cx}, ${cy})`);
g.append("path")
.attr("id", (d) => d.uid.id)
.style("fill", "none")
.attr("d", (d) => {
const context = d3.path();
if (atBottom(d)) {
context.arc(
0,
0,
r,
(Math.PI / 180) * d.endAngle - halfPI,
(Math.PI / 180) * d.startAngle - halfPI,
true
);
} else {
context.arc(
0,
0,
r,
(Math.PI / 180) * d.startAngle - halfPI,
(Math.PI / 180) * d.endAngle - halfPI,
false
);
}
return context.toString();
});
g.append("path")
.style(
"stroke",
(d) => color(haveClass(d))
// dStrokeWidth(d) == 0 ? null : color(haveClass(d))
)
.style("stroke-width", strokeWidth)
// .style("stroke-width", (d) => dStrokeWidth(d))
.style(
"fill",
(d) => (dStrokeWidth(d) != 0 ? "transparent" : color(haveClass(d)))
// strokeWidth != 0 ? "transparent" : (d) => color(haveClass(d))
)
.attr("d", (d) => arc(d));
g.append("text")
.style("fill", (d) =>
dStrokeWidth(d) != 0 ? color(haveClass(d)) : "white"
)
// .style("fill", strokeWidth != 0 ? (d) => color(haveClass(d)) : "white")
.style("font-size", (d) => (d.fontSize ? d.fontSize : fontSize))
.style("font-weight", bold ? "bold" : "normal")
// .attr("dy", (d) => (innerR / 2) * (atBottom(d) ? +1 : -0.9))
.attr("font-family", 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("startOffset", (d) =>
// d.textOffAngle ? 50 + d.textOffAngle + "%" : "50%"
// )
.attr("xlink:href", (d) => d.uid.href)
.text((d) => {
let rawText = d.text;
if (!rawText.includes("\n")) {
return d.letterSpace
? rawText.split("").join("\xa0".repeat(d.letterSpace))
: rawText;
}
})
.select(function () {
return this.parentNode;
})
// In case of \n in text
.clone()
.attr("dy", (d) => (innerR / 2) * (atBottom(d) ? +1 + 0.45 : -0.9 - 0.45))
.append("textPath")
.attr("startOffset", "50%")
.attr("xlink:href", (d) => d.uid.href)
.text((d) => {
let rawText = d.text;
if (rawText.includes("\n")) {
let arrText = rawText.split("\n");
return d.letterSpace
? arrText[0].split("").join("\xa0".repeat(d.letterSpace))
: arrText[0];
}
})
.select(function () {
return this.parentNode;
})
.clone()
.attr(
"dy",
(d) => (innerR / 2) * (atBottom(d) ? +1.5 - 0.45 : -0.9 + 0.3)
)
.append("textPath")
.attr("startOffset", "50%")
.attr("xlink:href", (d) => d.uid.href)
.text((d) => {
let rawText = d.text;
if (rawText.includes("\n")) {
let arrText = rawText.split("\n");
return d.letterSpace
? arrText[1].split("").join("\xa0".repeat(d.letterSpace))
: arrText[1];
}
});
};
data.forEach((d) => (d.uid = DOM.uid("p-" + d.index)));

svg.selectAll("g.arcLabel").data(data).join(enter);
return svg.node().innerHTML;
}
Insert cell
import { myColor } from "854486827ec381b5"
Insert cell
shuffleSeed = require("https://bundle.run/shuffle-seed@1.1.6")
Insert cell
Insert cell
import { serialize } from "@cocisuts/saving-svg"
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