Skip to content

Commit

Permalink
Merge pull request #1864: Add tree-related types
Browse files Browse the repository at this point in the history
  • Loading branch information
victorlin authored Nov 12, 2024
2 parents e8337a8 + 9c7644b commit 1cf70f8
Show file tree
Hide file tree
Showing 31 changed files with 2,135 additions and 593 deletions.
538 changes: 538 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@
},
"devDependencies": {
"@playwright/test": "^1.48.2",
"@types/d3": "^7.4.3",
"@types/leaflet": "^1.9.3",
"@types/node": "^18.15.11",
"@types/webpack-env": "^1.18.2",
Expand Down
2 changes: 0 additions & 2 deletions src/actions/recomputeReduxState.js
Original file line number Diff line number Diff line change
Expand Up @@ -400,8 +400,6 @@ const modifyControlsStateViaTree = (state, tree, treeToo, colorings) => {
* only when a file is dropped. (I've gone down too many rabbit holes in this PR to
* do this now, unfortunately.) james, 2023
*/
state.coloringsPresentOnTree = new Set();
state.coloringsPresentOnTreeWithConfidence = new Set(); // subset of above

let coloringsToCheck = [];
if (colorings) {
Expand Down
228 changes: 142 additions & 86 deletions src/actions/tree.js → src/actions/tree.ts

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions src/components/controls/toggle-focus.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import React from "react";
import { connect } from "react-redux";
import { FaInfoCircle } from "react-icons/fa";
import { Dispatch } from "@reduxjs/toolkit";
import Toggle from "./toggle";
import { SidebarIconContainer, StyledTooltip } from "./styles";
import { TOGGLE_FOCUS } from "../../actions/types";
import { RootState } from "../../store";
import { Layout } from "../../reducers/controls";
import { AppDispatch, RootState } from "../../store";


function ToggleFocus({ tooltip, focus, layout, dispatch, mobileDisplay }: {
tooltip: React.ReactElement;
focus: boolean;
layout: "rect" | "radial" | "unrooted" | "clock" | "scatter";
dispatch: Dispatch;
layout: Layout;
dispatch: AppDispatch;
mobileDisplay: boolean;
}) {
// Focus functionality is only available to layouts that have the concept of a unitless y-axis
Expand Down
11 changes: 8 additions & 3 deletions src/components/tree/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { connect } from "react-redux";
import { connect, MapStateToProps } from "react-redux";
import UnconnectedTree from "./tree";
import { RootState } from "../../store";
import { TreeComponentOwnProps, TreeComponentStateProps } from "./types";

const Tree = connect((state: RootState) => ({
const mapStateToProps: MapStateToProps<TreeComponentStateProps, TreeComponentOwnProps, RootState> = (
state: RootState,
): TreeComponentStateProps => ({
tree: state.tree,
treeToo: state.treeToo,
selectedNode: state.controls.selectedNode,
Expand Down Expand Up @@ -32,6 +35,8 @@ const Tree = connect((state: RootState) => ({
animationPlayPauseButton: state.controls.animationPlayPauseButton,
showOnlyPanels: state.controls.showOnlyPanels,
performanceFlags: state.controls.performanceFlags,
}))(UnconnectedTree);
});

const Tree = connect(mapStateToProps)(UnconnectedTree);

export default Tree;
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Selection, Transition } from "d3";
import { timerFlush } from "d3-timer";
import { calcConfidenceWidth } from "./confidence";
import { applyToChildren, setDisplayOrder } from "./helpers";
Expand All @@ -6,11 +7,15 @@ import { NODE_VISIBLE } from "../../../util/globals";
import { getBranchVisibility, strokeForBranch } from "./renderers";
import { shouldDisplayTemporalConfidence } from "../../../reducers/controls";
import { makeTipLabelFunc } from "./labels";
import { ChangeParams, PhyloNode, PhyloTreeType, PropsForPhyloNodes, SVGProperty, TreeElement } from "./types";

/* loop through the nodes and update each provided prop with the new value
* additionally, set d.update -> whether or not the node props changed
*/
const updateNodesWithNewData = (nodes, newNodeProps) => {
const updateNodesWithNewData = (
nodes: PhyloNode[],
newNodeProps: PropsForPhyloNodes,
): void => {
// console.log("update nodes with data for these keys:", Object.keys(newNodeProps));
// let tmp = 0;
nodes.forEach((d, i) => {
Expand All @@ -35,72 +40,88 @@ const updateNodesWithNewData = (nodes, newNodeProps) => {
const svgSetters = {
attrs: {
".tip": {
r: (d) => d.r,
cx: (d) => d.xTip,
cy: (d) => d.yTip
r: (d: PhyloNode) => d.r,
cx: (d: PhyloNode) => d.xTip,
cy: (d: PhyloNode) => d.yTip
},
".branch": {
},
".vaccineCross": {
d: (d) => d.vaccineCross
d: (d: PhyloNode) => d.vaccineCross
},
".conf": {
d: (d) => d.confLine
d: (d: PhyloNode) => d.confLine
}
},
styles: {
".tip": {
fill: (d) => d.fill,
stroke: (d) => d.tipStroke,
visibility: (d) => d.visibility === NODE_VISIBLE ? "visible" : "hidden"
fill: (d: PhyloNode) => d.fill,
stroke: (d: PhyloNode) => d.tipStroke,
visibility: (d: PhyloNode) => d.visibility === NODE_VISIBLE ? "visible" : "hidden"
},
".conf": {
stroke: (d) => d.branchStroke,
stroke: (d: PhyloNode) => d.branchStroke,
"stroke-width": calcConfidenceWidth
},
// only allow stroke to be set on individual branches
".branch": {
"stroke-width": (d) => d["stroke-width"] + "px", // style - as per drawBranches()
stroke: (d) => strokeForBranch(d), // TODO: revisit if we bring back SVG gradients
cursor: (d) => d.visibility === NODE_VISIBLE ? "pointer" : "default",
"stroke-width": (d: PhyloNode) => d["stroke-width"] + "px", // style - as per drawBranches()
stroke: (d: PhyloNode) => strokeForBranch(d), // TODO: revisit if we bring back SVG gradients
cursor: (d: PhyloNode) => d.visibility === NODE_VISIBLE ? "pointer" : "default",
visibility: getBranchVisibility
}
}
};


type SelectionOrTransition =
Selection<SVGGElement, PhyloNode, SVGSVGElement | null, unknown> |
Transition<SVGGElement, PhyloNode, SVGSVGElement | null, unknown>

type UpdateCall = (selectionOrTransition: SelectionOrTransition) => void;


/** createUpdateCall
* returns a function which can be called as part of a D3 chain in order to modify
* the SVG elements.
* svgSetters (see above) are used to actually modify the property on the element,
* so the given property must also be present there!
* @param {string} treeElem (e.g. ".tip" or ".branch")
* @param {list} properties (e.g. ["visibiliy", "stroke-width"])
* @return {function} used in a d3 selection, i.e. d3.selection().methods().call(X)
*/
const createUpdateCall = (treeElem, properties) => (selection) => {
// First: the properties to update via d3Selection.attr call
if (svgSetters.attrs[treeElem]) {
[...properties].filter((x) => svgSetters.attrs[treeElem][x])
.forEach((attrName) => {
// console.log(`applying attr ${attrName} to ${treeElem}`)
selection.attr(attrName, svgSetters.attrs[treeElem][attrName]);
});
}
// Second: the properties to update via d3Selection.style call
if (svgSetters.styles[treeElem]) {
[...properties].filter((x) => svgSetters.styles[treeElem][x])
.forEach((styleName) => {
// console.log(`applying style ${styleName} to ${treeElem}`)
selection.style(styleName, svgSetters.styles[treeElem][styleName]);
});
}
};
function createUpdateCall(
treeElem: TreeElement,

const genericSelectAndModify = (svg, treeElem, updateCall, transitionTime) => {
/** e.g. ["visibility", "stroke-width"] */
properties: Set<SVGProperty>,
): UpdateCall {
return (selection) => {
// First: the properties to update via d3Selection.attr call
if (svgSetters.attrs[treeElem]) {
[...properties].filter((x) => svgSetters.attrs[treeElem][x])
.forEach((attrName) => {
// console.log(`applying attr ${attrName} to ${treeElem}`)
selection.attr(attrName, svgSetters.attrs[treeElem][attrName]);
});
}
// Second: the properties to update via d3Selection.style call
if (svgSetters.styles[treeElem]) {
[...properties].filter((x) => svgSetters.styles[treeElem][x])
.forEach((styleName) => {
// console.log(`applying style ${styleName} to ${treeElem}`)
selection.style(styleName, svgSetters.styles[treeElem][styleName]);
});
}
};
}

const genericSelectAndModify = (
svg: Selection<SVGSVGElement | null, unknown, null, unknown>,
treeElem: TreeElement,
updateCall: UpdateCall,
transitionTime: number,
): void => {
// console.log("general svg update for", treeElem);
let selection = svg.selectAll(treeElem)
.filter((d) => d.update);
let selection: SelectionOrTransition = svg.selectAll<SVGGElement, PhyloNode>(treeElem)
.filter((d: PhyloNode) => d.update);
if (transitionTime) {
selection = selection.transition().duration(transitionTime);
}
Expand All @@ -113,14 +134,20 @@ const genericSelectAndModify = (svg, treeElem, updateCall, transitionTime) => {
* @transitionTime {INT} - in ms. if 0 then no transition (timerFlush is used)
* @extras {dict} - extra keywords to tell this function to call certain phyloTree update methods. In flux.
*/
export const modifySVG = function modifySVG(elemsToUpdate, svgPropsToUpdate, transitionTime, extras) {
let updateCall;
const classesToPotentiallyUpdate = [".tip", ".vaccineDottedLine", ".vaccineCross", ".branch"]; /* order is respected */
export const modifySVG = function modifySVG(
this: PhyloTreeType,
elemsToUpdate: Set<TreeElement>,
svgPropsToUpdate: Set<SVGProperty>,
transitionTime: number,
extras: Extras,
): void {
let updateCall: UpdateCall;
const classesToPotentiallyUpdate: TreeElement[] = [".tip", ".vaccineDottedLine", ".vaccineCross", ".branch"]; /* order is respected */
/* treat stem / branch specially, but use these to replace a normal .branch call if that's also to be applied */
if (elemsToUpdate.has(".branch.S") || elemsToUpdate.has(".branch.T")) {
const applyBranchPropsAlso = elemsToUpdate.has(".branch");
if (applyBranchPropsAlso) classesToPotentiallyUpdate.splice(classesToPotentiallyUpdate.indexOf(".branch"), 1);
const ST = [".S", ".T"];
const ST: Array<".S" | ".T"> = [".S", ".T"];
ST.forEach((x, STidx) => {
if (elemsToUpdate.has(`.branch${x}`)) {
if (applyBranchPropsAlso) {
Expand Down Expand Up @@ -196,7 +223,14 @@ export const modifySVG = function modifySVG(elemsToUpdate, svgPropsToUpdate, tra
* step 2: when step 1 has finished, move tips across the screen.
* step 3: when step 2 has finished, redraw everything. No transition here.
*/
export const modifySVGInStages = function modifySVGInStages(elemsToUpdate, svgPropsToUpdate, transitionTimeFadeOut, transitionTimeMoveTips, extras) {
export const modifySVGInStages = function modifySVGInStages(
this: PhyloTreeType,
elemsToUpdate: Set<TreeElement>,
svgPropsToUpdate: Set<SVGProperty>,
transitionTimeFadeOut: number,
transitionTimeMoveTips: number,
extras: Extras,
): void {
elemsToUpdate.delete(".tip");
this.hideGrid();
let inProgress = 0; /* counter of transitions currently in progress */
Expand Down Expand Up @@ -236,46 +270,55 @@ export const modifySVGInStages = function modifySVGInStages(elemsToUpdate, svgPr
};


interface Extras {
removeConfidences: boolean
showConfidences: boolean
newBranchLabellingKey?: string

timeSliceHasPotentiallyChanged?: boolean
hideTipLabels?: boolean
}


/* the main interface to changing a currently rendered tree.
* simply call change and tell it what should be changed.
* try to do a single change() call with as many things as possible in it
*/
export const change = function change({
/* booleans for what should be changed */
changeColorBy = false,
changeVisibility = false,
changeTipRadii = false,
changeBranchThickness = false,
showConfidences = false,
removeConfidences = false,
zoomIntoClade = false,
svgHasChangedDimensions = false,
animationInProgress = false,
changeNodeOrder = false,
/* change these things to provided value (unless undefined) */
newDistance = undefined,
newLayout = undefined,
updateLayout = undefined, // todo - this seems identical to `newLayout`
newBranchLabellingKey = undefined,
showAllBranchLabels = undefined,
newTipLabelKey = undefined,
/* arrays of data (the same length as nodes) */
branchStroke = undefined,
tipStroke = undefined,
fill = undefined,
visibility = undefined,
tipRadii = undefined,
branchThickness = undefined,
/* other data */
focus = undefined,
scatterVariables = undefined,
performanceFlags = {},
}) {
export const change = function change(
this: PhyloTreeType,
{
changeColorBy = false,
changeVisibility = false,
changeTipRadii = false,
changeBranchThickness = false,
showConfidences = false,
removeConfidences = false,
zoomIntoClade = false,
svgHasChangedDimensions = false,
animationInProgress = false,
changeNodeOrder = false,
focus = false,
newDistance = undefined,
newLayout = undefined,
updateLayout = undefined,
newBranchLabellingKey = undefined,
showAllBranchLabels = undefined,
newTipLabelKey = undefined,
branchStroke = undefined,
tipStroke = undefined,
fill = undefined,
visibility = undefined,
tipRadii = undefined,
branchThickness = undefined,
scatterVariables = undefined,
performanceFlags = undefined,
}: ChangeParams
): void {
// console.log("\n** phylotree.change() (time since last run:", Date.now() - this.timeLastRenderRequested, "ms) **\n\n");
timerStart("phylotree.change()");
const elemsToUpdate = new Set(); /* what needs updating? E.g. ".branch", ".tip" etc */
const nodePropsToModify = {}; /* which properties (keys) on the nodes should be updated (before the SVG) */
const svgPropsToUpdate = new Set(); /* which SVG properties shall be changed. E.g. "fill", "stroke" */
const elemsToUpdate = new Set<TreeElement>(); /* what needs updating? E.g. ".branch", ".tip" etc */
const nodePropsToModify: PropsForPhyloNodes = {}; /* which properties (keys) on the nodes should be updated (before the SVG) */
const svgPropsToUpdate = new Set<SVGProperty>(); /* which SVG properties shall be changed. E.g. "fill", "stroke" */
const useModifySVGInStages = newLayout; /* use modifySVGInStages rather than modifySVG. Not used often. */


Expand Down Expand Up @@ -350,7 +393,7 @@ export const change = function change({
this.zoomNode = zoomIntoClade.n.hasChildren ?
zoomIntoClade :
zoomIntoClade.n.parent.shell;
applyToChildren(this.zoomNode, (d) => {d.inView = true;});
applyToChildren(this.zoomNode, (d: PhyloNode) => {d.inView = true;});
}
if (svgHasChangedDimensions || changeNodeOrder) {
this.nodes.forEach((d) => {d.update = true;});
Expand All @@ -375,7 +418,7 @@ export const change = function change({
}
/* mapToScreen */
if (
svgPropsToUpdate.has(["stroke-width"]) ||
svgPropsToUpdate.has("stroke-width") ||
newDistance ||
newLayout ||
changeNodeOrder ||
Expand All @@ -392,8 +435,8 @@ export const change = function change({
elemsToUpdate.add('.tipLabel'); /* will trigger d3 commands as required */
}

const extras = { removeConfidences, showConfidences, newBranchLabellingKey };
extras.timeSliceHasPotentiallyChanged = changeVisibility || newDistance;
const extras: Extras = { removeConfidences, showConfidences, newBranchLabellingKey };
extras.timeSliceHasPotentiallyChanged = changeVisibility || newDistance !== undefined;
extras.hideTipLabels = animationInProgress || newTipLabelKey === 'none';
if (useModifySVGInStages) {
this.modifySVGInStages(elemsToUpdate, svgPropsToUpdate, transitionTime, 1000, extras);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { dataFont, darkGrey } from "../../../globalStyles";
import { Params } from "./types";

export const createDefaultParams = () => ({
export const createDefaultParams = (): Params => ({
regressionStroke: darkGrey,
regressionWidth: 6,
majorGridStroke: "#DDD",
Expand Down
Loading

0 comments on commit 1cf70f8

Please sign in to comment.