diff options
| -rw-r--r-- | .gitignore | 4 | ||||
| -rwxr-xr-x | data_otol/namedTreeToJSON.py | 181 | ||||
| -rwxr-xr-x | data_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-x | data_tol_old/txtTreeToJSON.py (renamed from src/txtTreeToJSON.py) | 0 | ||||
| -rw-r--r-- | src/App.vue | 84 | ||||
| -rw-r--r-- | src/components/AncestryBar.vue | 10 | ||||
| -rw-r--r-- | src/components/SearchModal.vue | 13 | ||||
| -rw-r--r-- | src/components/Tile.vue | 25 | ||||
| -rw-r--r-- | src/components/TileInfoModal.vue | 9 | ||||
| -rw-r--r-- | src/layout.ts | 48 | ||||
| -rw-r--r-- | src/tol.ts | 49 |
12 files changed, 298 insertions, 125 deletions
@@ -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: @@ -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; -} |
