Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tree-related types #1864

Merged
merged 31 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
c4bcae9
Compile for ES2015
victorlin Oct 25, 2024
d06d095
Fix imports from d3-scale
victorlin Oct 28, 2024
1b4c441
Remove non-existent parameter from docstring
victorlin Oct 25, 2024
cb7e657
render(): Convert from positional to named parameters
victorlin Oct 30, 2024
fd26d53
Use context-specific dispatch type
victorlin Nov 5, 2024
7b3a0e1
Add tree-related types
victorlin Nov 5, 2024
402a5e6
Add other tree-related types
victorlin Nov 5, 2024
b1b2488
tsconfig: Disable strictNullChecks
victorlin Oct 30, 2024
2e96210
Add override keyword for React lifecycle methods
victorlin Nov 5, 2024
7d22da0
Add _ prefix for unused variables
victorlin Oct 25, 2024
75fd6ad
Use exported Layout type
victorlin Nov 5, 2024
e02e0ce
Use Math.trunc instead of parseInt
victorlin Oct 25, 2024
a79226e
Use Math.min/max instead of min/max
victorlin Nov 5, 2024
5f4fe7e
Use separate variables for array and set of legend values
victorlin Oct 25, 2024
aa30581
Use null type for domRefs
victorlin Nov 5, 2024
6083f73
Properly handle unknown errors when creating color scales
victorlin Oct 25, 2024
83326a2
Properly check for stroke-width
victorlin Oct 25, 2024
25294e8
Properly check window.event.shiftKey
victorlin Oct 25, 2024
aedce7e
Properly set tipSelectedIdx
victorlin Oct 25, 2024
02b7206
Properly set timeSliceHasPotentiallyChanged
victorlin Oct 25, 2024
5b81e88
Properly set isSubtreeRoot
victorlin Oct 28, 2024
79608a5
Properly set PhyloNode.branch to [string, string]
victorlin Oct 25, 2024
9d57be8
Remove unused parameter to numDate
victorlin Oct 25, 2024
5803970
Remove unused mapLegendOpen, treeLegendOpen from controls state
victorlin Oct 29, 2024
4d2e633
Remove unused selectedNode from state
victorlin Nov 5, 2024
8c6a301
Remove unused default value for performanceFlags
victorlin Nov 7, 2024
3bd4b55
Remove broken/unused searchNodes parameter
victorlin Oct 29, 2024
56b791b
Convert RGBColor to hex string
victorlin Oct 25, 2024
5a681ff
Set default colorings in getDefaultControlsState
victorlin Oct 29, 2024
931f1e6
Set default value of focus to false
victorlin Nov 7, 2024
9c7644b
Allow setState to take a partial state
victorlin Nov 6, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading