Skip to content

Commit 6800b94

Browse files
authored
Merge pull request CDCgov#42 from d-callan/color-branches
Color branches
2 parents efa3570 + fd95f02 commit 6800b94

File tree

3 files changed

+268
-88
lines changed

3 files changed

+268
-88
lines changed

app/index.html

Lines changed: 88 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -386,19 +386,32 @@ <h5 class="mb-0">
386386
data-parent="#accordion"
387387
>
388388
<div class="card-body">
389-
<!-- <div class="form-group">
390-
<label for="colorMode">Color Mode</label>
391-
<select id="colorMode" class="form-control form-control-sm skip"></select>
392-
</div> -->
393389
<div class="form-group">
394-
<label>Default Color</label>
390+
<label for="highlightMode">Highlighting Example</label>
391+
<select id="highlightMode" class="form-control form-control-sm skip">
392+
<option value="none">No highlighting</option>
393+
<option value="leaves">Highlight leaf nodes</option>
394+
<option value="monophyletic">Highlight E. coli and Shigella branches+nodes</option>
395+
</select>
396+
</div>
397+
<div class="form-group">
398+
<label>Default Node Color</label>
395399
<input
396400
type="color"
397-
id="defaultColor"
401+
id="defaultNodeColor"
398402
class="skip"
399403
value="#4682B4"
400404
>
401405
</div>
406+
<div class="form-group">
407+
<label>Default Branch Color</label>
408+
<input
409+
type="color"
410+
id="defaultBranchColor"
411+
class="skip"
412+
value="#cccccc"
413+
>
414+
</div>
402415
<div class="form-group">
403416
<label>Highlight Color</label>
404417
<input
@@ -408,13 +421,6 @@ <h5 class="mb-0">
408421
value="#feb640"
409422
>
410423
</div>
411-
<div class="form-group">
412-
<div class="switch">
413-
<label>
414-
<input id="highlightLeaves" type="checkbox" class="skip"> Highlight Leaves
415-
</label>
416-
</div>
417-
</div>
418424
</div>
419425
</div>
420426
</div>
@@ -657,7 +663,7 @@ <h5 class="modal-title" id="exportModalLabel">Export</h5>
657663
fetch("life.nwk").then(response => response.text().then(buildTree));
658664
});
659665

660-
["layout", "mode", "type", "colorMode"].forEach(thing => {
666+
["layout", "mode", "type"].forEach(thing => {
661667
var title = "valid" + thing[0].toUpperCase() + thing.slice(1) + "s";
662668
d3.select("#" + thing)
663669
.selectAll("option")
@@ -683,8 +689,10 @@ <h5 class="modal-title" id="exportModalLabel">Export</h5>
683689
type: d3.select("#type").node().value,
684690
colorOptions:
685691
{
686-
colorMode: "list",
687-
defaultColor: d3.select("#defaultColor").node().value,
692+
nodeColorMode: "list",
693+
branchColorMode: "monophyletic",
694+
defaultNodeColor: d3.select("#defaultNodeColor").node().value,
695+
defaultBranchColor: d3.select("#defaultBranchColor").node().value,
688696
highlightColor: d3.select("#highlightColor").node().value,
689697
},
690698
leafNodes: d3.select("#leafNodes").node().checked,
@@ -732,62 +740,86 @@ <h5 class="modal-title" id="exportModalLabel">Export</h5>
732740
tree.setAnimation(cached);
733741
});
734742

735-
// might want this back when we have more color mode options
736-
// for now, ill leave it out in favor of the highlight toggle
737-
/* d3.select("#colorMode").on("input", function() {
738-
// this could prove annoying, but ill leave it for now
739-
if (this.value === "list") {
740-
d3.select("#defaultColor").node().value = "#243127";
743+
// Checks if the given node, or all of its leaves, is an E. Coli or Shigella node.
744+
//
745+
// Parameters:
746+
// - node: The node to check.
747+
//
748+
// Returns:
749+
// - true if the node, or all of its leaves, is an E. Coli or Shigella node, false otherwise.
750+
isEColiOrShigellaNode = function(node) {
751+
// this bc its recursive and the children are organized differently
752+
let nodeData = node.__data__ ? node.__data__.data : node;
753+
754+
if (nodeData.children.length === 0) {
755+
return nodeData.id.includes("Escherichia_coli") || nodeData.id.includes("Shigella");
741756
} else {
742-
d3.select("#defaultColor").node().value = "#4682B4";
757+
return nodeData.children.every(isEColiOrShigellaNode);
743758
}
759+
}
744760

761+
d3.select("#highlightMode").on("input", function() {
762+
if (this.value === "none") {
763+
tree.setColorOptions({
764+
nodeColorMode: "none",
765+
branchColorMode: "none",
766+
defaultNodeColor: d3.select("#defaultNodeColor").node().value,
767+
defaultBranchColor: d3.select("#defaultBranchColor").node().value,
768+
highlightColor: d3.select("#highlightColor").node().value
769+
});
770+
} else if (this.value === "leaves") {
771+
tree.setColorOptions({
772+
nodeColorMode: "list",
773+
branchColorMode: "none",
774+
defaultNodeColor: d3.select("#defaultNodeColor").node().value,
775+
defaultBranchColor: d3.select("#defaultBranchColor").node().value,
776+
highlightColor: d3.select("#highlightColor").node().value,
777+
nodeList: tree.getNodeGUIDs(true)
778+
});
779+
} else if (this.value === "monophyletic") {
780+
tree.setColorOptions({
781+
nodeColorMode: "list",
782+
branchColorMode: "monophyletic",
783+
defaultNodeColor: d3.select("#defaultNodeColor").node().value,
784+
defaultBranchColor: d3.select("#defaultBranchColor").node().value,
785+
highlightColor: d3.select("#highlightColor").node().value,
786+
nodeList: tree.getNodeGUIDs(false, isEColiOrShigellaNode)
787+
})
788+
}
789+
});
790+
791+
d3.select("#defaultNodeColor").on("input", function() {
745792
tree.setColorOptions({
746-
colorMode: this.value,
747-
defaultColor: d3.select("#defaultColor").node().value,
748-
highlightColor: d3.select("#highlightColor").node().value
793+
nodeColorMode: d3.select("#highlightMode").property("value") === "none" ? "none" : "list",
794+
branchColorMode: d3.select("#highlightMode").property("value") === "monophyletic" ? "monophyletic" : "none",
795+
defaultNodeColor: this.value,
796+
defaultBranchColor: d3.select("#defaultBranchColor").node().value,
797+
highlightColor: d3.select("#highlightColor").node().value,
798+
nodeList: d3.select("#highlightMode").property("value") === "monophyletic" ? tree.getNodeGUIDs(false, isEColiOrShigellaNode) : tree.getNodeGUIDs(true)
749799
});
750800
});
751-
*/
752-
d3.select("#defaultColor").on("input", function() {
801+
802+
d3.select("#defaultBranchColor").on("input", function() {
753803
tree.setColorOptions({
754-
colorMode: d3.select("#highlightLeaves").node().value ? "list" : "none",
755-
defaultColor: this.value,
804+
nodeColorMode: d3.select("#highlightMode").property("value") === "none" ? "none" : "list",
805+
branchColorMode: d3.select("#highlightMode").property("value") === "monophyletic" ? "monophyletic" : "none",
806+
defaultNodeColor: d3.select("#defaultNodeColor").node().value,
807+
defaultBranchColor: this.value,
756808
highlightColor: d3.select("#highlightColor").node().value,
757-
nodeList: tree.getNodeGUIDs(true)
809+
nodeList: d3.select("#highlightMode").property("value") === "monophyletic" ? tree.getNodeGUIDs(false, isEColiOrShigellaNode) : tree.getNodeGUIDs(true)
758810
});
759811
});
760812

761813
d3.select("#highlightColor").on("input", function() {
762-
if (!d3.select("#highlightLeaves").property("checked")) {
763-
d3.select("#highlightLeaves").property("checked",true);
764-
}
765814
tree.setColorOptions({
766-
colorMode: d3.select("#highlightLeaves").node().value ? "list" : "none",
767-
defaultColor: d3.select("#defaultColor").node().value,
815+
nodeColorMode: d3.select("#highlightMode").property("value") === "none" ? "none" : "list",
816+
branchColorMode: d3.select("#highlightMode").property("value") === "monophyletic" ? "monophyletic" : "none",
817+
defaultNodeColor: d3.select("#defaultNodeColor").node().value,
818+
defaultBranchColor: d3.select("#defaultBranchColor").node().value,
768819
highlightColor: this.value,
769-
nodeList: tree.getNodeGUIDs(true)
820+
nodeList: d3.select("#highlightMode").property("value") === "monophyletic" ? tree.getNodeGUIDs(false, isEColiOrShigellaNode) : tree.getNodeGUIDs(true)
770821
});
771822
});
772-
773-
d3.select("#highlightLeaves").on("input", function() {
774-
// console.log("tree", tree.getNodeGUIDs(this.value));
775-
if (d3.select("#highlightLeaves").property("checked")) {
776-
tree.setColorOptions({
777-
colorMode: "list",
778-
defaultColor: d3.select("#defaultColor").node().value,
779-
highlightColor: d3.select("#highlightColor").node().value,
780-
nodeList: tree.getNodeGUIDs(true)
781-
})
782-
} else {
783-
tree.setColorOptions({
784-
colorMode: "none",
785-
defaultColor: d3.select("#defaultColor").node().value,
786-
highlightColor: d3.select("#highlightColor").node().value,
787-
nodeList: tree.getNodeGUIDs(false)
788-
})
789-
}
790-
});
791823

792824
d3.select("#branchNodeSize").on("input", function() {
793825
tree.eachBranchNode((node, data) => {

dist/tidytree.js

Lines changed: 90 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1258,7 +1258,7 @@ var TidyTree = (function () {
12581258
layout: "vertical",
12591259
type: "tree",
12601260
mode: "smooth",
1261-
colorOptions: { colorMode: "none" },
1261+
colorOptions: { nodeColorMode: "none" },
12621262
leafNodes: true,
12631263
leafLabels: false,
12641264
equidistantLeaves: false,
@@ -1357,7 +1357,13 @@ var TidyTree = (function () {
13571357
* The available color modes for rendering nodes.
13581358
* @type {Array}
13591359
*/
1360-
TidyTree.validColorModes = ["none", "list"]; // later, highlight on hover, or maybe color by annotation on a node/ search
1360+
TidyTree.validNodeColorModes = ["none", "list"]; // later, highlight on hover, or maybe color by annotation on a node/ search
1361+
1362+
/**
1363+
* The available color modes for rendering branches.
1364+
* @type {Array}
1365+
*/
1366+
TidyTree.validBranchColorModes = ["none", "monophyletic"]; // later, toRoot?
13611367

13621368
/**
13631369
* Draws a Phylogenetic on the element referred to by selector
@@ -1621,20 +1627,70 @@ var TidyTree = (function () {
16211627
* @return {string} The color of the node.
16221628
*/
16231629
function findNodeColor(node, colorOptions) {
1624-
if (colorOptions.colorMode === "none") {
1630+
if (colorOptions.nodeColorMode === "none") {
16251631
// steelblue
1626-
return colorOptions.defaultColor ?? "#4682B4";
1632+
return colorOptions.defaultNodeColor ?? "#4682B4";
16271633
}
16281634

16291635
let nodeList = colorOptions.nodeList;
16301636

16311637
if (nodeList && nodeList.includes(node.data._guid)) {
1632-
// charcoal
1638+
// yellowish
16331639
return colorOptions.highlightColor ?? "#feb640";
16341640
} else {
1641+
// charcoal
1642+
return colorOptions.defaultNodeColor ?? "#243127";
1643+
}
1644+
}
1645+
1646+
/**
1647+
* Find the color of a given link based on the provided color options.
1648+
*
1649+
* @param {string} link - The link for which to find the color.
1650+
* @param {object} colorOptions - The options for different link colors.
1651+
* @return {string} The color of the link.
1652+
*/
1653+
function findBranchColor(link, colorOptions) {
1654+
if (colorOptions.branchColorMode === "none") {
1655+
// light gray
1656+
return colorOptions.defaultBranchColor ?? "#cccccc";
1657+
}
1658+
1659+
let source = link.source;
1660+
let childLeafNodes = getAllLeaves(source);
1661+
1662+
let allChildLeafNodesInNodeList = childLeafNodes.every(child =>
1663+
colorOptions.nodeList?.includes(child.data._guid)
1664+
);
1665+
1666+
if (allChildLeafNodesInNodeList) {
16351667
// yellowish
1636-
return colorOptions.defaultColor ?? "#243127";
1668+
return colorOptions.highlightColor ?? "#feb640";
1669+
}
1670+
1671+
return colorOptions.defaultBranchColor ?? "#cccccc";
1672+
}
1673+
1674+
/**
1675+
* Returns an array of all the child leaf nodes of the given node in a tree.
1676+
*
1677+
* @param {Object} node - A node of the tree.
1678+
* @param {boolean} includeSelf - Whether to include the given node itself as a leaf node. Defaults to false.
1679+
* @return {Array} An array of leaf nodes.
1680+
*/
1681+
function getAllLeaves(node, includeSelf) {
1682+
includeSelf = includeSelf ?? false;
1683+
let leaves = [];
1684+
1685+
if (includeSelf && node.height === 0) {
1686+
leaves.push(node);
1687+
} else {
1688+
node.children.forEach(child => {
1689+
leaves.push(...getAllLeaves(child, true));
1690+
});
16371691
}
1692+
1693+
return leaves;
16381694
}
16391695

16401696
const radToDeg = 180 / Math.PI;
@@ -1775,7 +1831,7 @@ var TidyTree = (function () {
17751831
newLinks
17761832
.append("path")
17771833
.attr("fill", "none")
1778-
.attr("stroke", "#ccc")
1834+
.attr("stroke", d => findBranchColor(d, this.colorOptions))
17791835
.attr("d", linkTransformer)
17801836
.transition()
17811837
.duration(this.animation)
@@ -1797,11 +1853,14 @@ var TidyTree = (function () {
17971853
let linkTransformer = linkTransformers[this.type][this.mode][this.layout];
17981854
let paths = update.select("path");
17991855
if (!this.animation > 0) {
1800-
paths.attr("d", linkTransformer);
1856+
paths
1857+
.attr("d", linkTransformer)
1858+
.attr("stroke", d => findBranchColor(d, this.colorOptions));
18011859
} else {
18021860
paths
18031861
.transition()
18041862
.duration(this.animation / 2)
1863+
.attr("stroke", d => findBranchColor(d, this.colorOptions))
18051864
.attr("opacity", 0)
18061865
.end()
18071866
.then(() => {
@@ -2054,17 +2113,30 @@ var TidyTree = (function () {
20542113
* @return {TidyTree} The TidyTree Object
20552114
*/
20562115
TidyTree.prototype.setColorOptions = function (newColorOptions) {
2057-
if (!TidyTree.validColorModes.includes(newColorOptions.colorMode)) {
2116+
if (!TidyTree.validNodeColorModes.includes(newColorOptions.nodeColorMode)) {
2117+
throw Error(`
2118+
Cannot set TidyTree colorOptions: ${newColorOptions.nodeColorMode}\n
2119+
Valid nodeColorModes are: ${TidyTree.validNodeColorModes.join(', ')}
2120+
`);
2121+
}
2122+
if (!TidyTree.validBranchColorModes.includes(newColorOptions.branchColorMode)) {
20582123
throw Error(`
2059-
Cannot set TidyTree to colorOptions: ${newColorOptions.colorMode}\n
2060-
Valid colorModes are: ${TidyTree.validColorModes.join(', ')}
2124+
Cannot set TidyTree colorOptions: ${newColorOptions.branchColorMode}\n
2125+
Valid branchColorModes are: ${TidyTree.validBranchColorModes.join(', ')}
20612126
`);
20622127
}
2063-
if (newColorOptions.colorMode === 'list') {
2128+
2129+
if (newColorOptions.nodeColorMode === 'list') {
20642130
if (!Array.isArray(newColorOptions.nodeList)) {
2065-
throw Error('nodeList must be an array for colorMode "list"');
2131+
throw Error('nodeList must be an array for nodeColorMode "list"');
2132+
}
2133+
} else {
2134+
// nodeColorMode === 'none'
2135+
if (newColorOptions.branchColorMode !== 'none') {
2136+
throw Error('branchColorMode must be "none" for nodeColorMode "none"');
20662137
}
20672138
}
2139+
20682140
this.colorOptions = newColorOptions;
20692141
if (this.parent) return this.redraw();
20702142
return this;
@@ -2400,10 +2472,10 @@ var TidyTree = (function () {
24002472
* Retrieves the GUIDs of the nodes in the TidyTree instance.
24012473
*
24022474
* @param {boolean} leavesOnly - Whether to retrieve GUIDs only for leaf nodes.
2475+
* @param {function} predicate - A function that returns true if the node should be included
24032476
* @return {Array} An array of GUIDs of the nodes.
24042477
*/
2405-
TidyTree.prototype.getNodeGUIDs = function (leavesOnly) {
2406-
// todo: make sure these are returned in order
2478+
TidyTree.prototype.getNodeGUIDs = function (leavesOnly, predicate) {
24072479
let nodeList = this.parent
24082480
.select("svg")
24092481
.selectAll("g.tidytree-node-leaf circle")
@@ -2418,7 +2490,9 @@ var TidyTree = (function () {
24182490

24192491
let nodeGUIDs = [];
24202492
for (const node of nodeList.values()) {
2421-
nodeGUIDs.push(node.__data__.data._guid);
2493+
if (!predicate || predicate(node)) {
2494+
nodeGUIDs.push(node.__data__.data._guid);
2495+
}
24222496
}
24232497

24242498
return nodeGUIDs;

0 commit comments

Comments
 (0)