Unlisted
Edited
Jun 29, 2023
1 fork
Insert cell
Insert cell
cloneData = await FileAttachment("y2.json").json()
Insert cell
Insert cell
Insert cell
tree = createBellPlotTree(cloneData.models.XXXX_pOme1_DNA[2])
Insert cell
Insert cell
anotherTree = {
const t = createBellPlotTree(cloneData.models.XXXX_pOme1_DNA[2]);
t.initialSize = rootInitialSize;
//t.children[1].initialSize = 1;
return t;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
treeToSvgDom(anotherTree)
Insert cell
treeToSvgDom = function (tree) {
const barWidth = 20;
const spacing = 3;

const domNode = DOM.svg(
barWidth + spacing + bellWidth + spacing + barWidth,
bellHeight
);
const svg = SVG.SVG(domNode);

const bellGroup = svg.group().transform({
scaleX: bellWidth,
scaleY: bellHeight,
translateX: barWidth + spacing
});

// Make shaper functions for the nested logistic geometries of the subclones
const shapers = treeToShapers(tree);
// Render the subclones recursively
addTreeToSvgGroup(tree, shapers, bellGroup);

const leftBarGroup = svg.group().transform({
scaleX: barWidth,
scaleY: bellHeight,
translateX: 0
});

// Stacked bar chart. Displays the proportions. Also provides attachment points for tentacles.
addStackedBarToSvgGroup(tree, stackTree(tree, shapers, 0), leftBarGroup);

const rightBarGroup = svg.group().transform({
scaleX: barWidth,
scaleY: bellHeight,
translateX: barWidth + spacing + bellWidth + spacing
});

// Stacked bar chart. Displays the proportions. Also provides attachment points for tentacles.
addStackedBarToSvgGroup(tree, stackTree(tree, shapers, 1), rightBarGroup);

return domNode;
}
Insert cell
/**
* Adds the nested subclones into an SVG group. The dimensions of tree are 1x1 pixels.
*/
addTreeToSvgGroup = function (tree, shapers, g) {
/**
* Draw an 1x1 "rectangle" that is shaped using the shaper function.
*/
function drawNode(node) {
const shaper = shapers.get(node.id);

// 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));
}

g.path(p.toString())
.fill(node.color)
.stroke("black")
.attr("stroke-opacity", 0.7)
.attr("vector-effect", "non-scaling-stroke")
.addClass("clone")
.data("clone", node.data);

for (const child of node.children) {
drawNode(child);
}
}

drawNode(tree);

return g;
}
Insert cell
/**
* Creates shaper functions for each subclone
*/
treeToShapers = function (tree) {
const totalDepth = getDepth(tree);

/** @type {Map<string, function>} */
const shapers = new Map();

function process(node, shaper, depth = 0) {
if (!shaper) {
shaper = (x, y) => y; // Make an initial shaper. Just a rectangle, no bell shape
}

shapers.set(node.id, shaper);

// 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 interpolateSpreadStacked = (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;

const initialSize = childNode.initialSize ?? 0;

// Never use spread positions when there's an initial size.
// It looks ugly and serves no purpose.
const doInterpolateSpreadStacked =
initialSize > 0
? (childIdx, x) => stackedPositions[childIdx]
: interpolateSpreadStacked;

// Create a new shaper for each children. Also chain with parent's shaper.
const childShaper = (x, y) => {
// The fractionalChildDepth defines when the bell starts to appear.
// If there's an initial size, it has to appear right away.
const transformedY =
lerp(
fancystep(initialSize > 0 ? 0 : fractionalChildDepth, 1, x),
1,
initialSize
) *
childFraction *
(y - 0.5) +
0.5 +
doInterpolateSpreadStacked(i, x);
return shaper(x, transformedY);
};

process(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 = {
id: "pseudoRoot",
fraction: 1,
children: [tree]
};

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

return shapers;
}
Insert cell
stackChildren = function (node, spread = false) {
// Fractions wrt. the parent
const fractions = node.children.map((n) => n.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
/**
* Take the tree stucture and shapers. Render a stacked bar.
*/
addStackedBarToSvgGroup = function (tree, stackedNodes, g) {
function process(node) {
const [top, bottom] = stackedNodes.get(node.id);
g.rect(1, bottom - top)
.move(0, top)
.fill(node.color)
.stroke("black")
.attr("stroke-opacity", 0.7)
.attr("vector-effect", "non-scaling-stroke");

for (const child of node.children) {
process(child);
}
}

process(tree);

return g;
}
Insert cell
/**
* Takes the shapers and builds stacked extents of the subclone
* proportions visible at the left or right edge of the bells.
* Returns a Map that maps node ids to an extents.
*
* This has two use cases:
* 1. Render a pretty stacked bar chart
* 2. Get attachment areas for the tentacles
*
* edge: 0 = left, 1 = right
*/
stackTree = function (tree, shapers, edge = 1) {
const stackedNodes = new Map();

function process(node) {
const nodeShaper = shapers.get(node.id);
const top = nodeShaper(edge, 0);

let bottom = nodeShaper(edge, 1);
for (const child of node.children) {
bottom = Math.min(bottom, process(child));
}
stackedNodes.set(node.id, [top, bottom]);

return bottom - top > 0 ? top : 1;
}

process(tree);
return stackedNodes;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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