aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTerry Truong <terry06890@gmail.com>2022-04-25 01:33:08 +1000
committerTerry Truong <terry06890@gmail.com>2022-04-25 01:33:08 +1000
commit2ab48497797441164e7f57fca2660097d93398ca (patch)
treea6f22d3edff60d182de454359bc40beda82fb5d8
parent23436a9ad4c2a710c7f0d49a07a720e0153d8225 (diff)
Adapt to handle open-tree-of-life data
Added data_otol/ with script that converts data from 'Open Tree of Life' release 13.4 into a JSON form. Moved old tree-of-life data and images into data_tol_old/. Added TolMap type to tol.ts, changed TolNode, and adapted other code to handle it. Temporarily disabling tile images until image data is added.
-rw-r--r--.gitignore4
-rwxr-xr-xdata_otol/namedTreeToJSON.py181
-rwxr-xr-xdata_tol_old/genTestImgs.sh (renamed from src/genTestImgs.sh)0
-rw-r--r--data_tol_old/tolData.txt (renamed from src/tolData.txt)0
-rwxr-xr-xdata_tol_old/txtTreeToJSON.py (renamed from src/txtTreeToJSON.py)0
-rw-r--r--src/App.vue84
-rw-r--r--src/components/AncestryBar.vue10
-rw-r--r--src/components/SearchModal.vue13
-rw-r--r--src/components/Tile.vue25
-rw-r--r--src/components/TileInfoModal.vue9
-rw-r--r--src/layout.ts48
-rw-r--r--src/tol.ts49
12 files changed, 298 insertions, 125 deletions
diff --git a/.gitignore b/.gitignore
index 53b49ae..7f8c994 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,4 +4,6 @@ node_modules/
dist/
src/tolData.json
public/img/
-public/tolgrid-img/
+data_tol_old/img/
+data_tol_old/tolData.json
+data_otol/
diff --git a/data_otol/namedTreeToJSON.py b/data_otol/namedTreeToJSON.py
new file mode 100755
index 0000000..30b8033
--- /dev/null
+++ b/data_otol/namedTreeToJSON.py
@@ -0,0 +1,181 @@
+#!/usr/bin/python3
+
+import sys, re, json
+
+usageInfo = f"usage: {sys.argv[0]}\n"
+usageInfo += "Reads labelled_supertree_ottnames.tre & annotations.json (from an Open Tree of Life release), \n"
+usageInfo += "and prints a JSON object, which maps node names to objects of the form \n"
+usageInfo += "{\"children\": [name1, ...], \"parent\": name1, \"tips\": int1, \"pSupport\": bool1}, which holds \n"
+usageInfo += "child names, a parent name or null, descendant 'tips', and a phylogeny-support indicator\n"
+usageInfo += "\n"
+usageInfo += "This script was adapted to handle Open Tree of Life version 13.4.\n"
+usageInfo += "Link: https://tree.opentreeoflife.org/about/synthesis-release/v13.4\n"
+usageInfo += "\n"
+usageInfo += "labelled_supertree_ottnames.tre format:\n"
+usageInfo += " Represents a tree-of-life in Newick format, roughly like (n1,n2,(n3,n4)n5)n6,\n"
+usageInfo += " where root node is named n6, and has children n1, n2, and n5.\n"
+usageInfo += " Name forms include Homo_sapiens_ott770315, mrcaott6ott22687, and 'Oxalis san-miguelii ott5748753'\n"
+usageInfo += " Some names can be split up into a 'simple' name (like Homo_sapiens) and an id (like ott770315)\n"
+usageInfo += "annotations.json format:\n"
+usageInfo += " JSON object holding information about the tree-of-life release.\n"
+usageInfo += " The object's 'nodes' field maps node IDs to objects holding information about that node,\n"
+usageInfo += " such as phylogenetic trees that support/conflict with it's placement.\n"
+
+if len(sys.argv) > 1:
+ print(usageInfo, file=sys.stderr)
+ sys.exit(1)
+
+nodeMap = {} # The JSON object to output
+idToName = {} # Maps node IDs to names
+
+# Parse labelled_supertree_ottnames.tre
+data = None
+with open("labelled_supertree_ottnames.tre") as file:
+ data = file.read()
+dataIdx = 0
+def parseNewick():
+ """Parses a node using 'data' and 'dataIdx', updates nodeMap accordingly, and returns the node name or None"""
+ global dataIdx
+ # Check for EOF
+ if dataIdx == len(data):
+ print("ERROR: Unexpected EOF at index " + str(dataIdx), file=sys.stderr)
+ return None
+ # Check for inner-node start
+ if data[dataIdx] == "(":
+ dataIdx += 1
+ childNames = []
+ while True:
+ # Read child
+ childName = parseNewick()
+ if childName == None:
+ return None
+ childNames.append(childName)
+ if (dataIdx == len(data)):
+ print("ERROR: Unexpected EOF", file=sys.stderr)
+ return None
+ # Check for next child
+ if (data[dataIdx] == ","):
+ dataIdx += 1
+ continue
+ else:
+ # Get node name
+ dataIdx += 1 # Consume an expected ')'
+ [name, id] = parseNewickName()
+ idToName[id] = name
+ # Get child num-tips total
+ tips = 0
+ for childName in childNames:
+ tips += nodeMap[childName]["tips"]
+ # Add node to nodeMap
+ nodeMap[name] = {
+ "n": name, "id": id, "children": childNames, "parent": None, "tips": tips, "pSupport": False
+ }
+ # Update childrens' parent reference
+ for childName in childNames:
+ nodeMap[childName]["parent"] = name
+ return name
+ else:
+ [name, id] = parseNewickName()
+ idToName[id] = name
+ nodeMap[name] = {"n": name, "id": id, "children": [], "parent": None, "tips": 1, "pSupport": False}
+ return name
+def parseNewickName():
+ """Helper that parses an input node name, and returns a [name,id] pair"""
+ global data, dataIdx
+ name = None
+ end = dataIdx
+ # Get name
+ if (end < len(data) and data[end] == "'"): # Check for quoted name
+ end += 1
+ inQuote = True
+ while end < len(data):
+ if (data[end] == "'"):
+ if end + 1 < len(data) and data[end+1] == "'": # Account for '' as escaped-quote
+ end += 2
+ continue
+ else:
+ end += 1
+ inQuote = False
+ break
+ end += 1
+ if inQuote:
+ raise Exception("ERROR: Unexpected EOF")
+ name = data[dataIdx:end]
+ dataIdx = end
+ else:
+ while end < len(data) and not re.match(r"[(),]", data[end]):
+ end += 1
+ if (end == dataIdx):
+ raise Exception("ERROR: Unexpected EOF")
+ name = data[dataIdx:end].rstrip()
+ if end == len(data): # Ignore trailing input semicolon
+ name = name[:-1]
+ dataIdx = end
+ # Convert to [name, id]
+ if name.startswith("mrca"):
+ return [name, name]
+ elif name[0] == "'":
+ match = re.fullmatch(r"'([^\\\"]+) (ott\d+)'", name)
+ if match == None:
+ raise Exception("ERROR: invalid name \"{}\"".format(name))
+ name = match.group(1).replace("''", "'")
+ return [name, match.group(2)]
+ else:
+ match = re.fullmatch(r"([^\\\"]+)_(ott\d+)", name)
+ if match == None:
+ raise Exception("ERROR: invalid name \"{}\"".format(name))
+ return [match.group(1).replace("_", " "), match.group(2)]
+rootName = parseNewick()
+
+# Parse annotations.json
+data = None
+with open("annotations.json") as file:
+ data = file.read()
+obj = json.loads(data)
+nodeAnnsMap = obj['nodes']
+
+# Do some more postprocessing on each node
+def convertMrcaName(name):
+ """Given an mrca* name, returns an expanded version with the form [name1 + name2]"""
+ match = re.fullmatch(r"mrca(ott\d+)(ott\d+)", name)
+ if match == None:
+ print("ERROR: Invalid name \"{}\"".format(name), file=sys.stderr)
+ else:
+ subName1 = match.group(1)
+ subName2 = match.group(2)
+ if subName1 not in idToName:
+ print("ERROR: MRCA name \"{}\" sub-name \"{}\" not found".format(subName1), file=sys.stderr)
+ elif subName2 not in idToName:
+ print("ERROR: MRCA name \"{}\" sub-name \"{}\" not found".format(subName2), file=sys.stderr)
+ else:
+ return "[{} + {}]".format(idToName[subName1], idToName[subName2])
+namesToSwap = [] # Will hold [oldName, newName] pairs, for renaming nodes in nodeMap
+for node in nodeMap.values():
+ # Set has-support value using annotations
+ id = node["id"]
+ if id in nodeAnnsMap:
+ nodeAnns = nodeAnnsMap[id]
+ supportQty = len(nodeAnns["supported_by"]) if "supported_by" in nodeAnns else 0
+ conflictQty = len(nodeAnns["conflicts_with"]) if "conflicts_with" in nodeAnns else 0
+ node["pSupport"] = supportQty > 0 and conflictQty == 0
+ # Change mrca* names
+ name = node["n"]
+ if (name.startswith("mrca")):
+ namesToSwap.append([name, convertMrcaName(name)])
+ parentName = node["parent"]
+ if (parentName != None and parentName.startswith("mrca")):
+ node["parent"] = convertMrcaName(parentName)
+ childNames = node["children"]
+ for i in range(len(childNames)):
+ if (childNames[i].startswith("mrca")):
+ childNames[i] = convertMrcaName(childNames[i])
+ # Delete some no-longer-needed fields
+ del node["n"]
+ del node["id"]
+# Finish mrca* renamings
+for [oldName, newName] in namesToSwap:
+ nodeMap[newName] = nodeMap[oldName]
+ del nodeMap[oldName]
+
+# Output JSON
+print(json.dumps(nodeMap))
diff --git a/src/genTestImgs.sh b/data_tol_old/genTestImgs.sh
index 21b001b..21b001b 100755
--- a/src/genTestImgs.sh
+++ b/data_tol_old/genTestImgs.sh
diff --git a/src/tolData.txt b/data_tol_old/tolData.txt
index f73a064..f73a064 100644
--- a/src/tolData.txt
+++ b/data_tol_old/tolData.txt
diff --git a/src/txtTreeToJSON.py b/data_tol_old/txtTreeToJSON.py
index 3b77622..3b77622 100755
--- a/src/txtTreeToJSON.py
+++ b/data_tol_old/txtTreeToJSON.py
diff --git a/src/App.vue b/src/App.vue
index cf25b18..e00072b 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -13,7 +13,8 @@ import SearchIcon from './components/icon/SearchIcon.vue';
import PlayIcon from './components/icon/PlayIcon.vue';
import SettingsIcon from './components/icon/SettingsIcon.vue';
// Other
-import {TolNode, TolNodeRaw, tolFromRaw, getTolMap} from './tol';
+import type {TolMap} from './tol';
+import {TolNode} from './tol';
import {LayoutNode, initLayoutTree, initLayoutMap, tryLayout} from './layout';
import type {LayoutOptions} from './layout';
import {arraySum, randWeightedChoice} from './util';
@@ -38,9 +39,9 @@ function getReverseAction(action: Action): Action | null {
}
// Get tree-of-life data
-import tolRaw from './tolData.json';
-const tol = tolFromRaw(tolRaw);
-const tolMap = getTolMap(tol);
+import data from './tolData.json';
+let tolMap: TolMap = data;
+const rootName = "[Elaeocarpus williamsianus + Brunellia mexicana]";
// Configurable options
const defaultLytOpts: LayoutOptions = {
@@ -89,14 +90,14 @@ const defaultUiOpts = {
export default defineComponent({
data(){
- let layoutTree = initLayoutTree(tol, 0);
+ let layoutTree = initLayoutTree(tolMap, rootName, 0);
return {
+ tolMap: tolMap,
layoutTree: layoutTree,
activeRoot: layoutTree, // Differs from layoutTree root when expand-to-view is used
layoutMap: initLayoutMap(layoutTree), // Maps names to LayoutNode objects
- tolMap: tolMap, // Maps names to TolNode objects
// Modals and settings related
- infoModalNode: null as TolNode | null, // Node to display info for, or null
+ infoModalNode: null as LayoutNode | null, // Node to display info for, or null
helpOpen: false,
searchOpen: false,
settingsOpen: false,
@@ -170,16 +171,22 @@ export default defineComponent({
methods: {
// For tile expand/collapse events
onLeafClick(layoutNode: LayoutNode){
- let success = tryLayout(this.activeRoot, this.tileAreaPos, this.tileAreaDims, this.lytOpts,
- {allowCollapse: false, chg: {type: 'expand', node: layoutNode}, layoutMap: this.layoutMap});
+ let success = tryLayout(this.activeRoot, this.tileAreaPos, this.tileAreaDims, this.lytOpts, {
+ allowCollapse: false,
+ chg: {type: 'expand', node: layoutNode, tolMap: this.tolMap},
+ layoutMap: this.layoutMap
+ });
if (!success){
layoutNode.failFlag = !layoutNode.failFlag; // Triggers failure animation
}
return success;
},
onNonleafClick(layoutNode: LayoutNode){
- let success = tryLayout(this.activeRoot, this.tileAreaPos, this.tileAreaDims, this.lytOpts,
- {allowCollapse: false, chg: {type: 'collapse', node: layoutNode}, layoutMap: this.layoutMap});
+ let success = tryLayout(this.activeRoot, this.tileAreaPos, this.tileAreaDims, this.lytOpts, {
+ allowCollapse: false,
+ chg: {type: 'collapse', node: layoutNode, tolMap: this.tolMap},
+ layoutMap: this.layoutMap
+ });
if (!success){
layoutNode.failFlag = !layoutNode.failFlag; // Triggers failure animation
}
@@ -193,8 +200,11 @@ export default defineComponent({
}
LayoutNode.hideUpward(layoutNode);
this.activeRoot = layoutNode;
- tryLayout(this.activeRoot, this.tileAreaPos, this.tileAreaDims, this.lytOpts,
- {allowCollapse: true, chg: {type: 'expand', node: layoutNode}, layoutMap: this.layoutMap});
+ tryLayout(this.activeRoot, this.tileAreaPos, this.tileAreaDims, this.lytOpts, {
+ allowCollapse: true,
+ chg: {type: 'expand', node: layoutNode, tolMap: this.tolMap},
+ layoutMap: this.layoutMap
+ });
},
onNonleafClickHeld(layoutNode: LayoutNode){
if (layoutNode == this.activeRoot){
@@ -204,7 +214,7 @@ export default defineComponent({
LayoutNode.hideUpward(layoutNode);
this.activeRoot = layoutNode;
tryLayout(this.activeRoot, this.tileAreaPos, this.tileAreaDims, this.lytOpts,
- {allowCollapse: true, layoutMap: this.layoutMap, });
+ {allowCollapse: true, layoutMap: this.layoutMap});
},
onDetachedAncestorClick(layoutNode: LayoutNode){
LayoutNode.showDownward(layoutNode);
@@ -215,7 +225,7 @@ export default defineComponent({
// For tile-info events
onInfoIconClick(node: LayoutNode){
this.resetMode();
- this.infoModalNode = node.tolNode;
+ this.infoModalNode = node;
},
// For help events
onHelpIconClick(){
@@ -227,60 +237,58 @@ export default defineComponent({
this.resetMode();
this.searchOpen = true;
},
- onSearchNode(tolNode: TolNode){
+ onSearchNode(name: string){
this.searchOpen = false;
this.modeRunning = true;
- this.expandToTolNode(tolNode);
+ this.expandToNode(name);
},
- expandToTolNode(tolNode: TolNode){
+ expandToNode(name: string){
if (!this.modeRunning){
return;
}
// Check if searched node is displayed
- let layoutNode = this.layoutMap.get(tolNode.name);
- if (layoutNode != null && !layoutNode.hidden){
- this.setLastFocused(layoutNode);
+ let layoutNodeVal = this.layoutMap.get(name);
+ if (layoutNodeVal != null && !layoutNodeVal.hidden){
+ this.setLastFocused(layoutNodeVal);
this.modeRunning = false;
return;
}
// Get nearest in-layout-tree ancestor
- let ancestor = tolNode;
- while (this.layoutMap.get(ancestor.name) == null){
- ancestor = ancestor.parent!;
+ let ancestorName = name;
+ while (this.layoutMap.get(ancestorName) == null){
+ ancestorName = this.tolMap[ancestorName].parent!;
}
- layoutNode = this.layoutMap.get(ancestor.name)!;
+ let layoutNode = this.layoutMap.get(ancestorName)!;
// If hidden, expand self/ancestor in ancestry-bar
if (layoutNode.hidden){
while (!this.detachedAncestors!.includes(layoutNode)){
- ancestor = ancestor.parent!;
- layoutNode = this.layoutMap.get(ancestor.name)!;
+ layoutNode = layoutNode.parent!;
}
this.onDetachedAncestorClick(layoutNode!);
- setTimeout(() => this.expandToTolNode(tolNode), this.uiOpts.tileChgDuration);
+ setTimeout(() => this.expandToNode(name), this.uiOpts.tileChgDuration);
return;
}
// Attempt tile-expand
let success = this.onLeafClick(layoutNode);
if (success){
- setTimeout(() => this.expandToTolNode(tolNode), this.uiOpts.tileChgDuration);
+ setTimeout(() => this.expandToNode(name), this.uiOpts.tileChgDuration);
return;
}
// Attempt expand-to-view on ancestor just below activeRoot
- if (ancestor.name == this.activeRoot.tolNode.name){
+ if (layoutNode == this.activeRoot){
console.log('Unable to complete search (not enough room to expand active root)');
// Note: Only happens if screen is significantly small or node has significantly many children
this.modeRunning = false;
return;
}
while (true){
- if (ancestor.parent!.name == this.activeRoot.tolNode.name){
+ if (layoutNode.parent! == this.activeRoot){
break;
}
- ancestor = ancestor.parent!;
+ layoutNode = layoutNode.parent!;
}
- layoutNode = this.layoutMap.get(ancestor.name)!;
this.onNonleafClickHeld(layoutNode);
- setTimeout(() => this.expandToTolNode(tolNode), this.uiOpts.tileChgDuration);
+ setTimeout(() => this.expandToNode(name), this.uiOpts.tileChgDuration);
},
// For auto-mode events
onPlayIconClick(){
@@ -317,7 +325,7 @@ export default defineComponent({
if (node == this.activeRoot){
actionWeights['move up'] = 0;
}
- if (node.tolNode.children.length == 0){
+ if (this.tolMap[node.name].children.length == 0){
actionWeights['expand'] = 0;
}
} else {
@@ -463,13 +471,13 @@ export default defineComponent({
<template>
<div class="absolute left-0 top-0 w-screen h-screen overflow-hidden" :style="{backgroundColor: uiOpts.appBgColor}">
<!-- Note: Making the above enclosing div's width/height dynamic seems to cause white flashes when resizing -->
- <tile :layoutNode="layoutTree" :lytOpts="lytOpts" :uiOpts="uiOpts"
+ <tile :layoutNode="layoutTree" :tolMap="tolMap" :lytOpts="lytOpts" :uiOpts="uiOpts"
@leaf-click="onLeafClick" @nonleaf-click="onNonleafClick"
@leaf-click-held="onLeafClickHeld" @nonleaf-click-held="onNonleafClickHeld"
@info-icon-click="onInfoIconClick"/>
<ancestry-bar v-if="detachedAncestors != null"
:pos="[0,0]" :dims="ancestryBarDims" :nodes="detachedAncestors"
- :lytOpts="lytOpts" :uiOpts="uiOpts"
+ :tolMap="tolMap" :lytOpts="lytOpts" :uiOpts="uiOpts"
@detached-ancestor-click="onDetachedAncestorClick" @info-icon-click="onInfoIconClick"/>
<!-- Icons -->
<help-icon @click="onHelpIconClick"
@@ -483,7 +491,7 @@ export default defineComponent({
text-white/40 hover:text-white hover:cursor-pointer"/>
<!-- Modals -->
<transition name="fade">
- <tile-info-modal v-if="infoModalNode != null" :tolNode="infoModalNode" :uiOpts="uiOpts"
+ <tile-info-modal v-if="infoModalNode != null" :node="infoModalNode" :uiOpts="uiOpts"
@info-modal-close="infoModalNode = null"/>
</transition>
<transition name="fade">
diff --git a/src/components/AncestryBar.vue b/src/components/AncestryBar.vue
index 6d6ae3c..a156a96 100644
--- a/src/components/AncestryBar.vue
+++ b/src/components/AncestryBar.vue
@@ -3,6 +3,7 @@ import {defineComponent, PropType} from 'vue';
import Tile from './Tile.vue'
import {LayoutNode} from '../layout';
import type {LayoutOptions} from '../layout';
+import type {TolMap} from '../tol';
// Displays a sequence of nodes, representing ancestors from a tree-of-life root to a currently-active root
export default defineComponent({
@@ -12,7 +13,8 @@ export default defineComponent({
dims: {type: Array as unknown as PropType<[number,number]>, required: true},
// The ancestors to display
nodes: {type: Array as PropType<LayoutNode[]>, required: true},
- // Options
+ // Other
+ tolMap: {type: Object as PropType<TolMap>, required: true},
lytOpts: {type: Object as PropType<LayoutOptions>, required: true},
uiOpts: {type: Object, required: true},
},
@@ -26,7 +28,7 @@ export default defineComponent({
},
usedNodes(){ // Childless versions of 'nodes' used to parameterise <tile>
return this.nodes.map(n => {
- let newNode = new LayoutNode(n.tolNode, []);
+ let newNode = new LayoutNode(n.name, []);
newNode.dims = [this.tileSz, this.tileSz];
return newNode;
});
@@ -80,8 +82,8 @@ export default defineComponent({
<template>
<div :style="styles">
- <tile v-for="(node, idx) in usedNodes" :key="node.tolNode.name" class="shrink-0"
- :layoutNode="node" :nonAbsPos="true" :lytOpts="lytOpts" :uiOpts="uiOpts"
+ <tile v-for="(node, idx) in usedNodes" :key="node.name" class="shrink-0"
+ :layoutNode="node" :tolMap="tolMap" :nonAbsPos="true" :lytOpts="lytOpts" :uiOpts="uiOpts"
@leaf-click="onTileClick(nodes[idx])" @info-icon-click="onInfoIconClick"/>
</div>
</template>
diff --git a/src/components/SearchModal.vue b/src/components/SearchModal.vue
index 91f06ae..22b6896 100644
--- a/src/components/SearchModal.vue
+++ b/src/components/SearchModal.vue
@@ -1,15 +1,13 @@
<script lang="ts">
import {defineComponent, PropType} from 'vue';
import SearchIcon from './icon/SearchIcon.vue';
-import {TolNode} from '../tol';
import {LayoutNode} from '../layout';
+import type {TolMap} from '../tol';
// Displays a search box, and sends search requests
export default defineComponent({
props: {
- // Map from tree-of-life node names to TolNode objects
- tolMap: {type: Object as PropType<Map<string,TolNode>>, required: true},
- // Options
+ tolMap: {type: Object as PropType<TolMap>, required: true},
uiOpts: {type: Object, required: true},
},
methods: {
@@ -20,15 +18,14 @@ export default defineComponent({
},
onSearchEnter(){
let input = this.$refs.searchInput as HTMLInputElement;
- let tolNode = this.tolMap.get(input.value);
- if (tolNode == null){
+ if (this.tolMap.hasOwnProperty(input.value)){
+ this.$emit('search-node', input.value);
+ } else {
input.value = '';
// Trigger failure animation
input.classList.remove('animate-red-then-fade');
input.offsetWidth; // Triggers reflow
input.classList.add('animate-red-then-fade');
- } else {
- this.$emit('search-node', tolNode);
}
},
focusInput(){
diff --git a/src/components/Tile.vue b/src/components/Tile.vue
index 1a506d6..a0c0f0f 100644
--- a/src/components/Tile.vue
+++ b/src/components/Tile.vue
@@ -3,12 +3,14 @@ import {defineComponent, PropType} from 'vue';
import InfoIcon from './icon/InfoIcon.vue';
import {LayoutNode} from '../layout';
import type {LayoutOptions} from '../layout';
+import type {TolMap} from '../tol';
+import {TolNode} from '../tol';
// Displays one, or a hierarchy of, tree-of-life nodes, as a 'tile'
export default defineComponent({
props: {
- // A LayoutNode representing a laid-out tree-of-life node to display
layoutNode: {type: Object as PropType<LayoutNode>, required: true},
+ tolMap: {type: Object as PropType<TolMap>, required: true},
// Options
lytOpts: {type: Object as PropType<LayoutOptions>, required: true},
uiOpts: {type: Object, required: true},
@@ -26,12 +28,15 @@ export default defineComponent({
};
},
computed: {
+ tolNode(): TolNode{
+ return this.tolMap[this.layoutNode.name];
+ },
// Basic abbreviations
isLeaf(): boolean {
return this.layoutNode.children.length == 0;
},
isExpandableLeaf(): boolean {
- return this.isLeaf && this.layoutNode.tolNode.children.length > 0;
+ return this.isLeaf && this.tolNode.children.length > 0;
},
showNonleafHeader(): boolean {
return (this.layoutNode.showHeader && this.layoutNode.sepSweptArea == null) ||
@@ -83,9 +88,11 @@ export default defineComponent({
leafStyles(): Record<string,string> {
return {
// Image (and scrims)
+ //backgroundImage:
+ // 'linear-gradient(to bottom, rgba(0,0,0,0.4), #0000 40%, #0000 60%, rgba(0,0,0,0.4) 100%),' +
+ // 'url(\'/img/' + this.layoutNode.name.replaceAll('\'', '\\\'') + '.png\')',
backgroundImage:
- 'linear-gradient(to bottom, rgba(0,0,0,0.4), #0000 40%, #0000 60%, rgba(0,0,0,0.4) 100%),' +
- 'url(\'/img/' + this.layoutNode.tolNode.name.replaceAll('\'', '\\\'') + '.png\')',
+ 'linear-gradient(to bottom, rgba(0,0,0,0.4), #0000 40%, #0000 60%, rgba(0,0,0,0.4) 100%)',
backgroundSize: 'cover',
// Other
borderRadius: this.uiOpts.borderRadius + 'px',
@@ -311,7 +318,7 @@ export default defineComponent({
<div v-if="isLeaf" :style="leafStyles"
class="w-full h-full flex flex-col overflow-hidden" :class="{'hover:cursor-pointer': isExpandableLeaf}"
@mouseenter="onMouseEnter" @mouseleave="onMouseLeave" @mousedown="onMouseDown" @mouseup="onMouseUp">
- <h1 :style="leafHeaderStyles">{{layoutNode.tolNode.name}}</h1>
+ <h1 :style="leafHeaderStyles">{{layoutNode.name}}</h1>
<info-icon :style="[infoIconStyles, {marginTop: 'auto'}]"
class="self-end text-white/10 hover:text-white hover:cursor-pointer"
@click.stop="onInfoIconClick" @mousedown.stop @mouseup.stop/>
@@ -319,7 +326,7 @@ export default defineComponent({
<div v-else :style="nonleafStyles" class="w-full h-full" ref="nonleaf">
<div v-if="showNonleafHeader" :style="nonleafHeaderStyles" class="flex hover:cursor-pointer"
@mouseenter="onMouseEnter" @mouseleave="onMouseLeave" @mousedown="onMouseDown" @mouseup="onMouseUp">
- <h1 :style="nonleafHeaderTextStyles" class="grow">{{layoutNode.tolNode.name}}</h1>
+ <h1 :style="nonleafHeaderTextStyles" class="grow">{{layoutNode.name}}</h1>
<info-icon :style="infoIconStyles" class="text-white/10 hover:text-white hover:cursor-pointer"
@click.stop="onInfoIconClick" @mousedown.stop @mouseup.stop/>
</div>
@@ -328,13 +335,13 @@ export default defineComponent({
<div v-if="layoutNode?.sepSweptArea?.sweptLeft === false"
:style="nonleafHeaderStyles" class="flex hover:cursor-pointer"
@mouseenter="onMouseEnter" @mouseleave="onMouseLeave" @mousedown="onMouseDown" @mouseup="onMouseUp">
- <h1 :style="nonleafHeaderTextStyles" class="grow">{{layoutNode.tolNode.name}}</h1>
+ <h1 :style="nonleafHeaderTextStyles" class="grow">{{layoutNode.name}}</h1>
<info-icon :style="infoIconStyles" class="text-white/10 hover:text-white hover:cursor-pointer"
@click.stop="onInfoIconClick" @mousedown.stop @mouseup.stop/>
</div>
</div>
- <tile v-for="child in layoutNode.children" :key="child.tolNode.name"
- :layoutNode="child" :lytOpts="lytOpts" :uiOpts="uiOpts"
+ <tile v-for="child in layoutNode.children" :key="child.name"
+ :layoutNode="child" :tolMap="tolMap" :lytOpts="lytOpts" :uiOpts="uiOpts"
@leaf-click="onInnerLeafClick" @nonleaf-click="onInnerNonleafClick"
@leaf-click-held="onInnerLeafClickHeld" @nonleaf-click-held="onInnerNonleafClickHeld"
@info-icon-click="onInnerInfoIconClick"/>
diff --git a/src/components/TileInfoModal.vue b/src/components/TileInfoModal.vue
index 7549375..0e2fc94 100644
--- a/src/components/TileInfoModal.vue
+++ b/src/components/TileInfoModal.vue
@@ -1,18 +1,19 @@
<script lang="ts">
import {defineComponent, PropType} from 'vue';
import CloseIcon from './icon/CloseIcon.vue';
-import {TolNode} from '../tol';
+import {LayoutNode} from '../layout';
// Displays information about a tree-of-life node
export default defineComponent({
props: {
- tolNode: {type: Object as PropType<TolNode>, required: true},
+ node: {type: Object as PropType<LayoutNode>, required: true},
uiOpts: {type: Object, required: true},
},
computed: {
imgStyles(): Record<string,string> {
return {
- backgroundImage: 'url(\'/img/' + this.tolNode.name.replaceAll('\'', '\\\'') + '.png\')',
+ //backgroundImage: 'url(\'/img/' + this.node.name.replaceAll('\'', '\\\'') + '.png\')',
+ background: 'black',
width: this.uiOpts.infoModalImgSz + 'px',
height: this.uiOpts.infoModalImgSz + 'px',
backgroundSize: 'cover',
@@ -38,7 +39,7 @@ export default defineComponent({
bg-stone-50 rounded-md shadow shadow-black">
<close-icon @click.stop="onCloseClick" ref="closeIcon"
class="block absolute top-2 right-2 w-6 h-6 hover:cursor-pointer"/>
- <h1 class="text-center text-xl font-bold mb-2">{{tolNode.name}}</h1>
+ <h1 class="text-center text-xl font-bold mb-2">{{node.name}}</h1>
<hr class="mb-4 border-stone-400"/>
<div :style="imgStyles" class="float-left mr-4" alt="an image"></div>
<div>
diff --git a/src/layout.ts b/src/layout.ts
index d863fa0..69e84db 100644
--- a/src/layout.ts
+++ b/src/layout.ts
@@ -2,16 +2,18 @@
* Contains classes for representing tile-based layouts of tree-of-life data.
*
* Generally, given a TolNode tree T, initLayoutTree() produces a
- * subtree-analagous LayoutNode tree, for which tryLayout() can attempt to
+ * subtree-analagous LayoutNode tree, for which tryLayout() can attempt to
* find a tile-based layout, filling in node fields to represent placement.
*/
import {TolNode} from './tol';
+import type {TolMap} from './tol';
import {range, arraySum, limitVals, updateAscSeq} from './util';
// Represents a node/tree that holds layout data for a TolNode node/tree
export class LayoutNode {
- tolNode: TolNode;
+ // TolNode name
+ name: string;
// Tree-structure related
children: LayoutNode[];
parent: LayoutNode | null;
@@ -28,8 +30,8 @@ export class LayoutNode {
hasFocus: boolean; // Used by search and auto-mode to highlight a tile
failFlag: boolean; // Used to trigger failure animations
// Constructor ('parent' are 'depth' are generally initialised later, 'dCount' is computed)
- constructor(tolNode: TolNode, children: LayoutNode[]){
- this.tolNode = tolNode;
+ constructor(name: string, children: LayoutNode[]){
+ this.name = name;
this.children = children;
this.parent = null;
this.dCount = children.length == 0 ? 1 : arraySum(children.map(n => n.dCount));
@@ -45,27 +47,27 @@ export class LayoutNode {
this.hasFocus = false;
this.failFlag = false;
}
- // Returns a new tree with the same structure and TolNode linkage
+ // Returns a new tree with the same structure and names
// 'chg' is usable to apply a change to the resultant tree
cloneNodeTree(chg?: LayoutTreeChg | null): LayoutNode {
let newNode: LayoutNode;
if (chg != null && this == chg.node){
switch (chg.type){
case 'expand':
- let children = this.tolNode.children.map((n: TolNode) => new LayoutNode(n, []));
- newNode = new LayoutNode(this.tolNode, children);
+ let children = chg.tolMap[this.name].children.map((name: string) => new LayoutNode(name, []));
+ newNode = new LayoutNode(this.name, children);
newNode.children.forEach(n => {
n.parent = newNode;
n.depth = this.depth + 1;
});
break;
case 'collapse':
- newNode = new LayoutNode(this.tolNode, []);
+ newNode = new LayoutNode(this.name, []);
break;
}
} else {
let children = this.children.map(n => n.cloneNodeTree(chg));
- newNode = new LayoutNode(this.tolNode, children);
+ newNode = new LayoutNode(this.name, children);
children.forEach(n => {n.parent = newNode});
}
newNode.depth = this.depth;
@@ -150,6 +152,7 @@ export type LayoutOptions = {
export type LayoutTreeChg = {
type: 'expand' | 'collapse';
node: LayoutNode;
+ tolMap: TolMap;
}
// Used with layout option 'sweepToParent', and represents, for a LayoutNode, a parent area to place leaf nodes in
export class SepSweptArea {
@@ -171,7 +174,7 @@ export type LayoutMap = Map<string, LayoutNode>;
// Creates a LayoutMap for a given tree
export function initLayoutMap(layoutTree: LayoutNode): LayoutMap {
function helper(node: LayoutNode, map: LayoutMap): void {
- map.set(node.tolNode.name, node);
+ map.set(node.name, node);
node.children.forEach(n => helper(n, map));
}
let map = new Map();
@@ -180,30 +183,31 @@ export function initLayoutMap(layoutTree: LayoutNode): LayoutMap {
}
// Adds a node and it's descendants' names to a LayoutMap
function addToLayoutMap(node: LayoutNode, map: LayoutMap): void {
- map.set(node.tolNode.name, node);
+ map.set(node.name, node);
node.children.forEach(n => addToLayoutMap(n, map));
}
// Removes a node and it's descendants' names from a LayoutMap
function removeFromLayoutMap(node: LayoutNode, map: LayoutMap): void {
- map.delete(node.tolNode.name);
+ map.delete(node.name);
node.children.forEach(n => removeFromLayoutMap(n, map));
}
// Creates a LayoutNode representing a TolNode tree, up to a given depth (0 means just the root)
-export function initLayoutTree(tol: TolNode, depth: number): LayoutNode {
- function initHelper(tolNode: TolNode, depthLeft: number, atDepth: number = 0): LayoutNode {
+export function initLayoutTree(tolMap: TolMap, rootName: string, depth: number): LayoutNode {
+ function initHelper(tolMap: TolMap, nodeName: string, depthLeft: number, atDepth: number = 0): LayoutNode {
if (depthLeft == 0){
- let node = new LayoutNode(tolNode, []);
+ let node = new LayoutNode(nodeName, []);
node.depth = atDepth;
return node;
} else {
- let children = tolNode.children.map((n: TolNode) => initHelper(n, depthLeft-1, atDepth+1));
- let node = new LayoutNode(tolNode, children);
+ let children = tolMap[nodeName].children.map(
+ (name: string) => initHelper(tolMap, name, depthLeft-1, atDepth+1));
+ let node = new LayoutNode(nodeName, children);
children.forEach(n => n.parent = node);
return node;
}
}
- return initHelper(tol, depth);
+ return initHelper(tolMap, rootName, depth);
}
// Attempts layout on a LayoutNode's corresponding TolNode tree, for an area with given xy-position and width+height
// 'allowCollapse' allows the layout algorithm to collapse nodes to avoid layout failure
@@ -568,7 +572,7 @@ let sweepLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse
if (parentArea != null){
// Attempt leaves layout
sweptLeft = parentArea.sweptLeft;
- leavesLyt = new LayoutNode(new TolNode('SWEEP_' + node.tolNode.name), leaves);
+ leavesLyt = new LayoutNode('SWEEP_' + node.name, leaves);
// Note: Intentionally neglecting to update child nodes' 'parent' or 'depth' fields here
let leavesSuccess = sqrLayout(leavesLyt, [0,0], parentArea.dims, !sweptLeft, false, opts);
if (leavesSuccess){
@@ -579,7 +583,7 @@ let sweepLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse
});
// Attempt non-leaves layout
let newDims: [number,number] = [dims[0], dims[1] - (sweptLeft ? headerSz : 0)];
- nonLeavesLyt = new LayoutNode(new TolNode('SWEEP_REM_' + node.tolNode.name), nonLeaves);
+ nonLeavesLyt = new LayoutNode('SWEEP_REM_' + node.name, nonLeaves);
let tempTree: LayoutNode = nonLeavesLyt.cloneNodeTree();
let sepAreaLen = 0;
let nonLeavesSuccess: boolean;
@@ -670,7 +674,7 @@ let sweepLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse
// Attempt leaves layout
let newPos = [0, headerSz];
let newDims: [number,number] = [dims[0], dims[1] - headerSz];
- leavesLyt = new LayoutNode(new TolNode('SWEEP_' + node.tolNode.name), leaves);
+ leavesLyt = new LayoutNode('SWEEP_' + node.name, leaves);
let minSz = opts.minTileSz + opts.tileSpacing*2;
let sweptW = Math.max(minSz, newDims[0] * ratio), sweptH = Math.max(minSz, newDims[1] * ratio);
let leavesSuccess: boolean;
@@ -723,7 +727,7 @@ let sweepLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse
newPos[1] += leavesLyt.dims[1] - opts.tileSpacing;
newDims[1] += -leavesLyt.dims[1] + opts.tileSpacing
}
- nonLeavesLyt = new LayoutNode(new TolNode('SWEEP_REM_' + node.tolNode.name), nonLeaves);
+ nonLeavesLyt = new LayoutNode('SWEEP_REM_' + node.name, nonLeaves);
let nonLeavesSuccess: boolean;
if (nonLeaves.length > 1){
nonLeavesSuccess = rectLayout(nonLeavesLyt, [0,0], newDims, false, false, opts, {subLayoutFn:
diff --git a/src/tol.ts b/src/tol.ts
index 42605e5..ba6c6c8 100644
--- a/src/tol.ts
+++ b/src/tol.ts
@@ -2,47 +2,18 @@
* Provides classes for representing and working with tree-of-life data.
*/
-// Represents a tree-of-life node/tree
+// Maps tree-of-life node names to node objects
+export type TolMap = {[key: string]: TolNode};
+// Represents a tree-of-life node
export class TolNode {
- name: string;
- children: TolNode[];
- parent: TolNode | null;
- constructor(name: string, children: TolNode[] = [], parent = null){
- this.name = name;
+ children: string[];
+ parent: string | null;
+ tips: number;
+ pSupport: boolean;
+ constructor(children: string[] = [], parent = null, tips = 0, pSupport = false){
this.children = children;
this.parent = parent;
+ this.tips = tips;
+ this.pSupport = pSupport;
}
}
-// Represents a tree-of-life node obtained from tolData.json
-export class TolNodeRaw {
- name: string;
- children?: TolNodeRaw[];
- constructor(name: string, children: TolNodeRaw[] = []){
- this.name = name;
- this.children = children;
- }
-}
-// Converts a TolNodeRaw tree to a TolNode tree
-export function tolFromRaw(node: TolNodeRaw): TolNode {
- function helper(node: TolNodeRaw, parent: TolNode | null){
- let tolNode = new TolNode(node.name);
- if (node.children == null){
- tolNode.children = [];
- } else {
- tolNode.children = node.children.map(child => helper(child, tolNode));
- }
- tolNode.parent = parent;
- return tolNode;
- }
- return helper(node, null);
-}
-// Returns a map from TolNode names to TolNodes in a given tree
-export function getTolMap(tolTree: TolNode): Map<string, TolNode> {
- function helper(node: TolNode, map: Map<string, TolNode>){
- map.set(node.name, node);
- node.children.forEach(child => helper(child, map));
- }
- let map = new Map();
- helper(tolTree, map);
- return map;
-}