Unlisted
Edited
Jun 27, 2023
2 forks
Insert cell
Insert cell
cloneData = await FileAttachment("y2.json").json()
Insert cell
Insert cell
tree = createBellPlotTree(cloneData.models.XXXX_pOme1_DNA[2])
Insert cell
getDepth(tree)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
treeToSvgDom(tree)
Insert cell
treeToSvgDom = function (tree) {
const domNode = DOM.svg(bellWidth, bellHeight);
const svg = SVG.SVG(domNode);

const g = svg.group().transform({
scaleX: bellWidth,
scaleY: bellHeight
});

addTreeToSvgGroup(tree, g);

return domNode;
}
Insert cell
addTreeToSvgGroup = function (tree, g) {
const totalDepth = getDepth(tree);

function drawNode(node, shaper, depth = 0) {
if (shaper) {
// Segment count. Higher number produces smoother curves.
const sc = 100;

// Find the first segment where the subclone starts to emerge
let firstSegment = 0;
for (let i = 0; i <= sc; i++) {
const x = i / sc;

if (shaper(x, 0) - shaper(x, 1) != 0) {
// The one where upper and lower edges collide
firstSegment = Math.max(0, i - 1);
break;
}
}

const p = d3.path();
// Start the path
p.moveTo(firstSegment / sc, shaper(firstSegment / sc, 1));

// Upper part of the bell path
for (let i = firstSegment + 1; i <= sc; i++) {
const x = i / sc;
p.lineTo(x, shaper(x, 1));
}
// Lower part of the bell path
for (let i = sc; i >= firstSegment; i--) {
const x = i / sc;
p.lineTo(x, shaper(x, 0));
}

const clonePath = g.path(p.toString());
clonePath.fill(node.color);
clonePath.stroke("black");
clonePath.attr("stroke-opacity", 0.3);
clonePath.attr("vector-effect", "non-scaling-stroke");
clonePath.addClass("clone");
clonePath.data("clone", node.data);
} else {
shaper = (x, y) => y; // Make an initial shaper. Just a rectangle, no bell shape
}

// Children emerge as spread to better emphasize what their parent is
const spreadPositions = stackChildren(node, true);
// They end up as stacked to make the perception of the proportions easier
const stackedPositions = stackChildren(node, false);

const childDepth = depth + 1;
const fractionalChildDepth = childDepth / totalDepth;

// Make an interpolator that smoothly interpolates between the spread and stacked positions
const interpolatePositions = (childIdx, x) => {
let a = smoothstep(fractionalChildDepth, 1, x);
const s = 1 - spreadStrength;
a = a * (1 - s) + s;
return lerp(spreadPositions[childIdx], stackedPositions[childIdx], a);
};

for (let i = 0; i < node.children.length; i++) {
const childNode = node.children[i];
// Fractions indicate the proportion of the subclone in the whole sample.
// However, we need the fraction within its parent.
const childFraction = childNode.fraction / node.fraction;

// Create a new shaper for each children. Also apply parent's shaper.
const childShaper = (x, y) => {
// The fractionalChildDepth defines when the bell starts to appear
const v = fancystep(fractionalChildDepth, 1, x) * childFraction;
y = v * (y - 0.5) + 0.5 + interpolatePositions(i, x);
return shaper(x, y);
};

drawNode(childNode, childShaper, childDepth);
}
}

// Make a pseudo root that contains the actual root.
// Rationale: the drawNode function provides shapers for node's children.
// We need an imaginary node so that we get a shaper for the root node.
const pseudoRoot = {
fraction: 1,
children: [tree]
};

drawNode(
pseudoRoot, // root node
null, // no initial shaper. One is created for the true root node.
-1 // initial depth
);

return g;
}
Insert cell
stackChildren = function (node, spread = false) {
// Fractions wrt. the parent
const fractions = node.children.map((n) => n.fraction / node.fraction);

const remainingSpace = 1 - fractions.reduce((a, c) => a + c, 0);

// Stack or spread?
const spacing = spread ? remainingSpace / (fractions.length + 1) : 0;
let cumSum = spread ? spacing : remainingSpace;

const positions = [];
for (const x of fractions) {
positions.push(cumSum + (x - 1) / 2);
cumSum += x + spacing;
}
return positions;
}
Insert cell
Insert cell
Insert cell
Insert cell
lerp = (a, b, x) => (1 - x) * a + x * b
Insert cell
clamp = (lower, upper, x) => Math.max(lower, Math.min(upper, x))
Insert cell
Insert cell
SVG = import("@svgdotjs/svg.js")
Insert cell
d3scaleChromatic = require('d3-scale-chromatic')
Insert cell
import {slider} from '@jashkenas/inputs'
Insert cell
Insert cell
d3 = require("d3")
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