From 19a8bb4d28b3f83517281541819e8a3e1079cc3b Mon Sep 17 00:00:00 2001 From: james hadfield Date: Tue, 22 Oct 2024 09:25:05 +1300 Subject: [PATCH 01/22] WIP - stream trees v1 --- src/actions/recomputeReduxState.js | 6 + src/components/tree/phyloTree/layouts.ts | 53 ++++++++ src/components/tree/phyloTree/phyloTree.ts | 2 + src/components/tree/phyloTree/renderers.ts | 52 +++++++- src/components/tree/phyloTree/types.ts | 8 ++ .../tree/reactD3Interface/initialRender.ts | 1 + src/reducers/tree/index.ts | 3 +- src/reducers/tree/types.ts | 1 + src/util/partitionIntoStreams.js | 122 ++++++++++++++++++ 9 files changed, 243 insertions(+), 5 deletions(-) create mode 100644 src/util/partitionIntoStreams.js diff --git a/src/actions/recomputeReduxState.js b/src/actions/recomputeReduxState.js index 0345f4293..69b223cbe 100644 --- a/src/actions/recomputeReduxState.js +++ b/src/actions/recomputeReduxState.js @@ -23,6 +23,7 @@ import { collectAvailableTipLabelOptions } from "../components/controls/choose-t import { hasMultipleGridPanels } from "./panelDisplay"; import { strainSymbolUrlString } from "../middleware/changeURL"; import { combineMeasurementsControlsAndQuery, loadMeasurements } from "./measurements"; +import { partitionIntoStreams } from "../util/partitionIntoStreams.js"; export const doesColorByHaveConfidence = (controlsState, colorBy) => controlsState.coloringsPresentOnTreeWithConfidence.has(colorBy); @@ -993,6 +994,11 @@ export const createStateFromQueryOrJSONs = ({ visibilityToo: treeToo?.visibility, }); + /* STREAMS */ + tree.streams = partitionIntoStreams(tree.nodes, controls.colorScale) + console.log("tree.streams", tree.streams) + + /* calculate entropy in view */ if (entropy.loaded) { /* The selected CDS + positions are only known if a genotype color-by has been set (display defaults | url) */ diff --git a/src/components/tree/phyloTree/layouts.ts b/src/components/tree/phyloTree/layouts.ts index 3ddeaba2c..f1faceaaf 100644 --- a/src/components/tree/phyloTree/layouts.ts +++ b/src/components/tree/phyloTree/layouts.ts @@ -29,6 +29,8 @@ export const setLayout = function setLayout( this.layout = layout; } + this.streamLayout(); + // remove any regression. This will be recalculated if required. this.regression = undefined; @@ -72,6 +74,44 @@ export const rectangularLayout = function rectangularLayout(this: PhyloTreeType) } }; + +export function streamLayout(this: PhyloTreeType): void { + + // TODO XXX - need to store this internally, but do we need this.streams or should this be an arg? + this.phyloStreams = this.streams.map((stream) => { + // the range of the displayOrder for all nodes in this stream + const displayOrders = stream.nodeIdxs.flat().reduce((acc, nodeIdx) => { + const value = this.nodes[nodeIdx].displayOrder; + if (acc[0] > value) acc[0] = value; + if (acc[1] < value) acc[1] = value; + return acc; + }, [Infinity, -Infinity]) + + + // the extent of that displayOrder scaled by maxNodesInInterval + const displayOrderScalar = (displayOrders[1] - displayOrders[0]) / stream.maxNodesInInterval; + const baseDisplayOrder = displayOrders[0]; + console.log("displayOrders", displayOrders, "displayOrderScalar", displayOrderScalar) + + // convert countsByCategory to displayOrderByColorBy + // P.S. stream.countsByCategory[categoryIdx][pivotIdx] = count + // target: displayOrderByColorBy [categoryIdx][pivotIdx] = [displayOrder, displayOrder] + const displayOrderByCategory = stream.countsByCategory.reduce((acc, countsAcrossPivots, categoryIdx) => { + acc.push(countsAcrossPivots.map((count, pivotIdx) => { + const base = categoryIdx===0 ? baseDisplayOrder : acc[categoryIdx-1][pivotIdx][1]; + return [base, base + count*displayOrderScalar]; + })) + return acc; + }, []); + + // NOTE: for num_date the value is the x value. Easy. + return {displayOrderByCategory} + }); + + console.log("this.phyloStreams", this.phyloStreams) + +} + /** * assign x,y coordinates for nodes based upon user-selected variables * TODO: timeVsRootToTip is a specific instance of this @@ -516,6 +556,19 @@ export const mapToScreen = function mapToScreen(this: PhyloTreeType): void { } }); } + + // PROTOTYPE + for (const [streamIdx, stream] of this.phyloStreams.entries()) { + const reduxStream = this.streams[streamIdx]; // urgh need better names + stream.x = reduxStream.pivots.map((pivot) => this.xScale(pivot)) + stream.y = stream.displayOrderByCategory.map((displayOrderByPivot) => { + return displayOrderByPivot.map(([min,max]) => { + return [this.yScale(min), this.yScale(max)] + }) + }) + } + + timerEnd("mapToScreen"); }; diff --git a/src/components/tree/phyloTree/phyloTree.ts b/src/components/tree/phyloTree/phyloTree.ts index 79c2a0f19..4015dcdec 100644 --- a/src/components/tree/phyloTree/phyloTree.ts +++ b/src/components/tree/phyloTree/phyloTree.ts @@ -66,6 +66,7 @@ PhyloTree.prototype.drawVaccines = renderers.drawVaccines; PhyloTree.prototype.drawRegression = renderers.drawRegression; PhyloTree.prototype.removeRegression = renderers.removeRegression; PhyloTree.prototype.updateColorBy = renderers.updateColorBy; +PhyloTree.prototype.drawStreams = renderers.drawStreams; /* C A L C U L A T E G E O M E T R I E S E T C ( M O D I F I E S N O D E S , N O T S V G ) */ PhyloTree.prototype.setDistance = layouts.setDistance; @@ -77,6 +78,7 @@ PhyloTree.prototype.radialLayout = layouts.radialLayout; PhyloTree.prototype.setScales = layouts.setScales; PhyloTree.prototype.mapToScreen = layouts.mapToScreen; PhyloTree.prototype.calculateRegression = regression.calculateRegression; +PhyloTree.prototype.streamLayout = layouts.streamLayout; /* C O N F I D E N C E I N T E R V A L S */ PhyloTree.prototype.removeConfidence = confidence.removeConfidence; diff --git a/src/components/tree/phyloTree/renderers.ts b/src/components/tree/phyloTree/renderers.ts index 1d8b87bb0..a2f9ca447 100644 --- a/src/components/tree/phyloTree/renderers.ts +++ b/src/components/tree/phyloTree/renderers.ts @@ -5,6 +5,7 @@ import { makeRegressionText } from "./regression"; import { getEmphasizedColor } from "../../../util/colorHelpers"; import { Callbacks, Distance, Params, PhyloNode, PhyloTreeType } from "./types"; import { Selection } from "d3"; +import { area, curveCatmullRom } from "d3-shape"; import { Layout, ScatterVariables } from "../../../reducers/controls"; import { ReduxNode, Visibility } from "../../../reducers/tree/types"; @@ -26,7 +27,8 @@ export const render = function render( tipFill, tipRadii, dateRange, - scatterVariables + scatterVariables, + streams, }: { /** the svg into which the tree is drawn */ svg: Selection @@ -74,6 +76,9 @@ export const render = function render( /** {x, y} properties to map nodes => scatterplot (only used if layout="scatter") */ scatterVariables: ScatterVariables + + /* stream tree information TODO XXX */ + streams: any; // TODO XXX }) { timerStart("phyloTree render()"); this.svg = svg; @@ -84,6 +89,11 @@ export const render = function render( this.callbacks = callbacks; this.vaccines = vaccines ? vaccines.map((d) => d.shell) : undefined; this.dateRange = dateRange; + // As long as we keep redux.tree.streams the same object, and also streams.mask the same array, + // the following should be references and thus always in-sync. + // (of course we'll need to call the appropriate render functions on an update) + this.mask = streams?.mask; + this.streams = streams?.streams; /* set nodes stroke / fill */ this.nodes.forEach((d, i) => { @@ -110,6 +120,7 @@ export const render = function render( this.drawBranches(); this.updateTipLabels(); this.drawTips(); + this.drawStreams(); if (this.params.branchLabelKey) this.drawBranchLabels(this.params.branchLabelKey); if (this.vaccines) this.drawVaccines(); if (this.regression) this.drawRegression(); @@ -158,7 +169,7 @@ export const drawTips = function drawTips(this: PhyloTreeType): void { } this.groups.tips .selectAll(".tip") - .data(this.nodes.filter((d) => !d.n.hasChildren)) + .data(this.nodes.filter((d) => !d.n.hasChildren).filter((d) => this.mask?.[d.n.arrayIdx]===1)) .enter() .append("circle") .attr("class", "tip") @@ -236,7 +247,10 @@ export const drawBranches = function drawBranches(this: PhyloTreeType): void { } else { this.groups.branchTee .selectAll('.branch') - .data(this.nodes.filter((d) => d.n.hasChildren && d.displayOrder !== undefined)) + .data(this.nodes + .filter((d) => d.n.hasChildren && d.displayOrder !== undefined) + .filter((d) => this.mask?.[d.n.arrayIdx]===1) + ) .enter() .append("path") .attr("class", "branch T") @@ -268,7 +282,7 @@ export const drawBranches = function drawBranches(this: PhyloTreeType): void { } this.groups.branchStem .selectAll('.branch') - .data(this.nodes.filter((d) => d.displayOrder !== undefined)) + .data(this.nodes.filter((d) => d.displayOrder !== undefined).filter((d) => this.mask?.[d.n.arrayIdx]===1)) .enter() .append("path") .attr("class", "branch S") @@ -290,6 +304,36 @@ export const drawBranches = function drawBranches(this: PhyloTreeType): void { timerEnd("drawBranches"); }; +export function drawStreams(this: PhyloTreeType): void { + + if (!("streams" in this.groups)) { + this.groups.streams = this.svg.append("g").attr("id", "streams"); // .attr("clip-path", "url(#treeClip)"); + } else { + this.groups.streams.selectAll("*").remove(); + } + + for (const [streamIdx, stream] of this.phyloStreams.entries()) { + + const areaObj = area() + .x((_d, pivotIdx) => { + // console.log("area d, i", d, pivotIdx); + return stream.x[pivotIdx]}) + .y0((d) => d[0]) + .y1((d) => d[1]) + .curve(curveCatmullRom.alpha(0.5)) + + + this.groups.streams.selectAll(`.stream${streamIdx}`) + .data(stream.y) + .enter() + .append("path") + .attr("d", areaObj as any) // TODO XXX + .attr("fill", (_d, i) => this.streams[streamIdx].categoryColors[i]) + } + + +} + /** * draws the regression line in the svg and adds a text with the rate estimate diff --git a/src/components/tree/phyloTree/types.ts b/src/components/tree/phyloTree/types.ts index bbecb581f..71132f1c2 100644 --- a/src/components/tree/phyloTree/types.ts +++ b/src/components/tree/phyloTree/types.ts @@ -246,6 +246,7 @@ export interface PhyloTreeType { regression?: Selection tips?: Selection vaccines?: Selection + streams?: Selection } hideGrid: typeof grid.hideGrid hideTemporalSlice: typeof grid.hideTemporalSlice @@ -294,4 +295,11 @@ export interface PhyloTreeType { yScale: any zoomNode: PhyloNode + + streams: {[key:string]: any} // TODO XXX + mask: (1|0)[] + drawStreams: typeof renderers.drawStreams + phyloStreams: {[key:string]: any} // TODO XXX + streamLayout: typeof layouts.streamLayout + } diff --git a/src/components/tree/reactD3Interface/initialRender.ts b/src/components/tree/reactD3Interface/initialRender.ts index 22cec9e4f..e356d227b 100644 --- a/src/components/tree/reactD3Interface/initialRender.ts +++ b/src/components/tree/reactD3Interface/initialRender.ts @@ -59,5 +59,6 @@ export const renderTree = ( tipRadii: treeState.tipRadii, dateRange: [props.dateMinNumeric, props.dateMaxNumeric], scatterVariables: props.scatterVariables, + streams: treeState.streams, }); }; diff --git a/src/reducers/tree/index.ts b/src/reducers/tree/index.ts index b7d08f2e0..b94db0394 100644 --- a/src/reducers/tree/index.ts +++ b/src/reducers/tree/index.ts @@ -23,7 +23,8 @@ export const getDefaultTreeState = (): TreeState | TreeTooState => { totalStateCounts: {}, observedMutations: {}, availableBranchLabels: [], - selectedClade: undefined + selectedClade: undefined, + streams: null, // TODO XXX }; }; diff --git a/src/reducers/tree/types.ts b/src/reducers/tree/types.ts index 722b06fb2..9e432e2d2 100644 --- a/src/reducers/tree/types.ts +++ b/src/reducers/tree/types.ts @@ -77,6 +77,7 @@ export interface TreeState { version: number visibility: Visibility[] | null visibilityVersion: number + streams: any // TODO XXX } export interface TreeTooState extends TreeState { diff --git a/src/util/partitionIntoStreams.js b/src/util/partitionIntoStreams.js new file mode 100644 index 000000000..038633424 --- /dev/null +++ b/src/util/partitionIntoStreams.js @@ -0,0 +1,122 @@ +import { getTraitFromNode } from "./treeMiscHelpers" + + +// Prototype - hardcode the CA of streams +const FOUNDERS = [ + "NODE_0000731", + "NODE_0000648", + "NODE_0000038", +] + + + + + +/** + * CAVEATS: + * - only works for trees with "FOUNDERS" in it + * - only works for categorical colorScale + * - only works for temporal tree + */ +export function partitionIntoStreams(nodes, colorScale) { + + const streams = { + streams: [], + mask: nodes.map((_) => 1), // 1 = show nodes as normal, 0 = mask out, nodes are part of a stream + } + + for (const founderNodeName of FOUNDERS) { + const stream = {} + + const nodesInStream = [] + + const stack = [getNode(nodes, founderNodeName)] + while (stack.length) { + const node = stack.pop(); + streams.mask[node.arrayIdx] = 0; + nodesInStream.push(node); + for (const child of node.children || []) { + stack.push(child) + } + } + stream.categories = observedCategories(nodesInStream, colorScale); + stream.categoryColors = stream.categories.map((value) => colorScale.scale(value)) + const pivotData = calcPivots(nodesInStream); + stream.pivotIntervals = pivotData.intervals; + stream.pivots = pivotData.pivots; + stream.nodeIdxs = groupNodesIntoIntervals(nodesInStream, pivotData.intervals); // indexed by pivot idx + stream.numNodes = nodesInStream.length; + stream.maxNodesInInterval = Math.max(...stream.nodeIdxs.map((idxs) => idxs.length)); + stream.countsByCategory = groupNodesByCategory(nodes, stream.nodeIdxs, colorScale.colorBy, stream.categories); + streams.streams.push(stream); + } + + return streams; +} + + +function getNode(nodes, name) { + for (const node of nodes) { + if (node.name===name) return node + } + throw new Error("didn't find node!!!") +} + +function observedCategories(nodes, colorScale) { + const colorBy = colorScale.colorBy; + const values = new Set(); + for (const node of nodes) { + values.add(getTraitFromNode(node, colorBy)); + } + // TODO XXX we want to check the values are a subset of colorScale.legendValues. Or domain? + // What to do about undefined values? + + return Array.from(values).sort((a,b) => colorScale.legendValues.indexOf(a) - colorScale.legendValues.indexOf(b)) +} + +function calcPivots(nodes) { + const domain = nodes.reduce((acc, node) => { + const value = getTraitFromNode(node, "num_date"); // TODO XXX + if (acc[0] > value) acc[0] = value; + if (acc[1] < value) acc[1] = value; + return acc; + }, [Infinity, -Infinity]) + const nPivots = 10; // TODO XXX + const size = (domain[1]-domain[0])/(nPivots-1); + const intervals = Array.from(Array(nPivots), undefined); + intervals[0] = [domain[0], domain[0] + size/2]; + intervals[intervals.length-1] = [domain[1]-size/2, domain[1]]; + for (let i=1; i domain[0] + i*size); + // console.log("DOMAIN", domain, "size", size, "intervals", intervals, "pivots", pivots) + return {intervals, pivots}; +} + + +function groupNodesIntoIntervals(nodes, intervals) { + const groups = Array.from(Array(intervals.length), () => []) + // TODO XXX this is very crude + for (const node of nodes) { + const value = getTraitFromNode(node, "num_date"); // TODO XXX + for (let i =0; iintervals[i][0] && value<=intervals[i][1]) { // TODO - which side is open, which is closed? + // TODO XXX - I use arrayIdx not the node itself as adding references to nodes like this + // crashed the app with "RangeError: Maximum call stack size exceeded". I presume it's a redux-related + // issue, as it arises from "at trackProperties (redux-toolkit.esm.js:508:22)" + groups[i].push(node.arrayIdx) + break + } + } + } + return groups; +} + +function groupNodesByCategory(nodes, nodeIdxsByPivot, colorBy, categories) { + return categories.map((category) => { + return nodeIdxsByPivot.map((nodeIdxs) => { + return nodeIdxs.filter((nodeIdx) => getTraitFromNode(nodes[nodeIdx], colorBy)===category).length + }) + }) +} \ No newline at end of file From 9cfd317a4e21483893ea79b6cdc7bbbab80f8cd2 Mon Sep 17 00:00:00 2001 From: james hadfield Date: Wed, 23 Oct 2024 11:27:02 +1300 Subject: [PATCH 02/22] WIP centered streams --- src/components/tree/phyloTree/layouts.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/components/tree/phyloTree/layouts.ts b/src/components/tree/phyloTree/layouts.ts index f1faceaaf..e8ba7598e 100644 --- a/src/components/tree/phyloTree/layouts.ts +++ b/src/components/tree/phyloTree/layouts.ts @@ -79,7 +79,8 @@ export function streamLayout(this: PhyloTreeType): void { // TODO XXX - need to store this internally, but do we need this.streams or should this be an arg? this.phyloStreams = this.streams.map((stream) => { - // the range of the displayOrder for all nodes in this stream + // the range of the displayOrder for all nodes in this stream. NOTE: this won't work for nested + // streams, we'd have to deduct them from what we do here... const displayOrders = stream.nodeIdxs.flat().reduce((acc, nodeIdx) => { const value = this.nodes[nodeIdx].displayOrder; if (acc[0] > value) acc[0] = value; @@ -87,7 +88,6 @@ export function streamLayout(this: PhyloTreeType): void { return acc; }, [Infinity, -Infinity]) - // the extent of that displayOrder scaled by maxNodesInInterval const displayOrderScalar = (displayOrders[1] - displayOrders[0]) / stream.maxNodesInInterval; const baseDisplayOrder = displayOrders[0]; @@ -104,6 +104,17 @@ export function streamLayout(this: PhyloTreeType): void { return acc; }, []); + // center the stream graphs + const displayOrderMidpoint = displayOrders[0] + (displayOrders[1] - displayOrders[0])/2; + const nPivots = displayOrderByCategory[0].length; + for (let pivotIdx=0; pivotIdx y+=shift); + } + } + // NOTE: for num_date the value is the x value. Easy. return {displayOrderByCategory} }); From ae5de3dbb3ebf3aa6e634f68e8f7632a6e87f3e3 Mon Sep 17 00:00:00 2001 From: james hadfield Date: Wed, 23 Oct 2024 14:28:31 +1300 Subject: [PATCH 03/22] WIP hierarchical streams --- src/components/tree/phyloTree/labels.js | 1 + src/components/tree/phyloTree/layouts.ts | 55 +++++++--- src/components/tree/phyloTree/renderers.ts | 4 +- src/util/partitionIntoStreams.js | 111 ++++++++++++++++----- 4 files changed, 131 insertions(+), 40 deletions(-) diff --git a/src/components/tree/phyloTree/labels.js b/src/components/tree/phyloTree/labels.js index 41d0ca35e..0c8af5622 100644 --- a/src/components/tree/phyloTree/labels.js +++ b/src/components/tree/phyloTree/labels.js @@ -142,6 +142,7 @@ export const drawBranchLabels = function drawBranchLabels(key) { this.nodes.filter( (d) => d.n.branch_attrs && d.n.branch_attrs.labels && d.n.branch_attrs.labels[key] ) + .filter((d) => this.mask?.[d.n.arrayIdx]===1) ) .enter() .append("text") diff --git a/src/components/tree/phyloTree/layouts.ts b/src/components/tree/phyloTree/layouts.ts index e8ba7598e..4c81d89e7 100644 --- a/src/components/tree/phyloTree/layouts.ts +++ b/src/components/tree/phyloTree/layouts.ts @@ -77,21 +77,46 @@ export const rectangularLayout = function rectangularLayout(this: PhyloTreeType) export function streamLayout(this: PhyloTreeType): void { + const displayOrderUsed = {} + // TODO XXX - need to store this internally, but do we need this.streams or should this be an arg? - this.phyloStreams = this.streams.map((stream) => { - // the range of the displayOrder for all nodes in this stream. NOTE: this won't work for nested - // streams, we'd have to deduct them from what we do here... - const displayOrders = stream.nodeIdxs.flat().reduce((acc, nodeIdx) => { - const value = this.nodes[nodeIdx].displayOrder; - if (acc[0] > value) acc[0] = value; - if (acc[1] < value) acc[1] = value; - return acc; - }, [Infinity, -Infinity]) + // NOTE: this.streams is postorder (i.e. the founder nodes are postorder w.r.t other founders in the main tree) + this.phyloStreams = this.streams.streams.map((stream) => { + const founderNode = this.nodes[stream.founderIdx]; + + // First get the display order range of the entire subtree of founderNode + // (This includes subclades which may themselves be streams) + function getDisplayOrder(node, top=true) { + if ((node.children || []).length) return getDisplayOrder(node.children.at(top?0:-1), top); + return node.shell.displayOrder; + } - // the extent of that displayOrder scaled by maxNodesInInterval - const displayOrderScalar = (displayOrders[1] - displayOrders[0]) / stream.maxNodesInInterval; + const getDisplayOrderExSubtrees = (node, top=true) => { + const children = (node.children || []).filter((child) => !this.streams.founderIndiciesPostorder.includes(child.arrayIdx)); + if (children.length) return getDisplayOrderExSubtrees(children.at(top?0:-1), top); + return node.shell.displayOrder; + } + + const displayOrders = [getDisplayOrder(founderNode.n, false), getDisplayOrder(founderNode.n)] + const displayOrdersExSubtrees = [getDisplayOrderExSubtrees(founderNode.n, false), getDisplayOrderExSubtrees(founderNode.n)] + + // Store the value for other streams to query (note - this is why we iterate through streams postorder) + // displayOrderRanges[stream.founderIdx] = displayOrders; + + // Get the total display order used up by _this_ stream, taking into account the display orders + // which may be used for descendant streams (note- this is why we iterate through streams postorder) + const displayOrderTotal = (displayOrders[1] - displayOrders[0]) - + this.streams.founderIndiciesToDescendantFounderIndicies[stream.founderIdx].reduce( + (acc, founderIdx) => acc + displayOrderUsed[founderIdx], + 0 + ) + + // Store the value for other streams to query + displayOrderUsed[stream.founderIdx] = displayOrderTotal; + + // scale this display order by maxNodesInInterval so the stream never exceeds the allocated range + const displayOrderScalar = displayOrderTotal / stream.maxNodesInInterval; const baseDisplayOrder = displayOrders[0]; - console.log("displayOrders", displayOrders, "displayOrderScalar", displayOrderScalar) // convert countsByCategory to displayOrderByColorBy // P.S. stream.countsByCategory[categoryIdx][pivotIdx] = count @@ -105,7 +130,8 @@ export function streamLayout(this: PhyloTreeType): void { }, []); // center the stream graphs - const displayOrderMidpoint = displayOrders[0] + (displayOrders[1] - displayOrders[0])/2; + // const displayOrderMidpoint = displayOrders[0] + (displayOrders[1] - displayOrders[0])/2; + const displayOrderMidpoint = displayOrdersExSubtrees[0] + (displayOrdersExSubtrees[1] - displayOrdersExSubtrees[0])/2; const nPivots = displayOrderByCategory[0].length; for (let pivotIdx=0; pivotIdx this.xScale(pivot)) stream.y = stream.displayOrderByCategory.map((displayOrderByPivot) => { return displayOrderByPivot.map(([min,max]) => { diff --git a/src/components/tree/phyloTree/renderers.ts b/src/components/tree/phyloTree/renderers.ts index a2f9ca447..eb7c064ea 100644 --- a/src/components/tree/phyloTree/renderers.ts +++ b/src/components/tree/phyloTree/renderers.ts @@ -93,7 +93,7 @@ export const render = function render( // the following should be references and thus always in-sync. // (of course we'll need to call the appropriate render functions on an update) this.mask = streams?.mask; - this.streams = streams?.streams; + this.streams = streams; // TODO - supply as a arg? Must revisit /* set nodes stroke / fill */ this.nodes.forEach((d, i) => { @@ -328,7 +328,7 @@ export function drawStreams(this: PhyloTreeType): void { .enter() .append("path") .attr("d", areaObj as any) // TODO XXX - .attr("fill", (_d, i) => this.streams[streamIdx].categoryColors[i]) + .attr("fill", (_d, i) => this.streams.streams[streamIdx].categoryColors[i]) } diff --git a/src/util/partitionIntoStreams.js b/src/util/partitionIntoStreams.js index 038633424..c8667f10a 100644 --- a/src/util/partitionIntoStreams.js +++ b/src/util/partitionIntoStreams.js @@ -2,13 +2,18 @@ import { getTraitFromNode } from "./treeMiscHelpers" // Prototype - hardcode the CA of streams -const FOUNDERS = [ - "NODE_0000731", - "NODE_0000648", - "NODE_0000038", -] - - +function _isFounderNode(node) { + const FOUNDERS = [ + "NODE_0000731", + "NODE_0001569", // NOTE - other founder nodes are descendants of this clade + "NODE_0000648", + "NODE_0000038", + "NODE_0001227", + "NODE_0001571", + // "NODE_0001773", // VERY BASAL IN TREE - everything is a descendant of this node + ] + return FOUNDERS.includes(node.name); +} @@ -19,21 +24,37 @@ const FOUNDERS = [ * - only works for temporal tree */ export function partitionIntoStreams(nodes, colorScale) { + + const {founderIndiciesToDescendantFounderIndicies, founderIndiciesPostorder} = + getFounderTree(nodes[0], _isFounderNode); + const streams = { streams: [], mask: nodes.map((_) => 1), // 1 = show nodes as normal, 0 = mask out, nodes are part of a stream + // founderTree, + founderIndiciesToDescendantFounderIndicies, + founderIndiciesPostorder, } - - for (const founderNodeName of FOUNDERS) { - const stream = {} - - const nodesInStream = [] - const stack = [getNode(nodes, founderNodeName)] + streams.streams = founderIndiciesPostorder.map((founderIdx) => { + const stream = {}; + stream.founderIdx = founderIdx; + const nodesInStream = []; + const founderNode = nodes[founderIdx]; + stream.founderName = founderNode.name; + const stack = [founderNode]; while (stack.length) { const node = stack.pop(); + if (founderIndiciesToDescendantFounderIndicies[founderNode.arrayIdx].includes(node.arrayIdx)) { + // console.log("Stream for", founderNode.name, "skipping subtree of", node.name) + continue + } streams.mask[node.arrayIdx] = 0; + // Don't mask the founder node so we can draw a branch to the start of the stream. + // Note - this double counts this node I think + // TODO - extend the stem of the founder node branch to join with the stream start point. + // if (node.arrayIdx===founderNode.arrayIdx) streams.mask[node.arrayIdx] = 1; nodesInStream.push(node); for (const child of node.children || []) { stack.push(child) @@ -48,20 +69,12 @@ export function partitionIntoStreams(nodes, colorScale) { stream.numNodes = nodesInStream.length; stream.maxNodesInInterval = Math.max(...stream.nodeIdxs.map((idxs) => idxs.length)); stream.countsByCategory = groupNodesByCategory(nodes, stream.nodeIdxs, colorScale.colorBy, stream.categories); - streams.streams.push(stream); - } + return stream; + }) return streams; } - -function getNode(nodes, name) { - for (const node of nodes) { - if (node.name===name) return node - } - throw new Error("didn't find node!!!") -} - function observedCategories(nodes, colorScale) { const colorBy = colorScale.colorBy; const values = new Set(); @@ -119,4 +132,56 @@ function groupNodesByCategory(nodes, nodeIdxsByPivot, colorBy, categories) { return nodeIdxs.filter((nodeIdx) => getTraitFromNode(nodes[nodeIdx], colorBy)===category).length }) }) +} + +/** + * + * @param {object} rootNode redux tree node + * @param {function} isFounderNode + */ +function getFounderTree(rootNode, isFounderNode) { + // Tree of nodes (in the main tree) which define stream trees + const founderTree = {children: []}; + const nodesInStreamFounderTree = []; + + function traverse(node, streamParentNode=founderTree) { + let newNode; + if (isFounderNode(node)) { + // add this as a child to the appropriate parent not in streamFounderTree + newNode = {children: [], parent: streamParentNode, arrayIdx:node.arrayIdx, name: node.name} + streamParentNode.children.push(newNode) + nodesInStreamFounderTree.push(newNode) + } + for (const child of node.children || []) { + traverse(child, newNode||streamParentNode) + } + } + traverse(rootNode); + + // Create mapping of founder nodes (indicies) to all their descendant indicies + const founderIndiciesToDescendantFounderIndicies = Object.fromEntries( + nodesInStreamFounderTree.map((node) => { + const descendantIndicies = []; + const stack = [node] + while (stack.length) { + const n = stack.shift(); + if (n.arrayIdx!==node.arrayIdx) descendantIndicies.push(n.arrayIdx); + for (const child of n.children || []) stack.push(child); + } + return [node.arrayIdx, descendantIndicies] + }) + ) + + // Create a list of founder indicies in postorder order, such that we can trivially visit + // the nodes without needing traversals + const founderIndiciesPostorder = []; + function postorder(node) { + for (const child of node.children||[]) { + postorder(child); + } + if (node.arrayIdx!==undefined) founderIndiciesPostorder.push(node.arrayIdx); + } + postorder(founderTree) + + return {founderTree, founderIndiciesToDescendantFounderIndicies, founderIndiciesPostorder}; } \ No newline at end of file From dc9dc06c27f8d1794e0f61716e9fe9e901066b71 Mon Sep 17 00:00:00 2001 From: james hadfield Date: Wed, 23 Oct 2024 14:44:13 +1300 Subject: [PATCH 04/22] WIP variable pivot counts --- src/actions/recomputeReduxState.js | 2 +- src/util/partitionIntoStreams.js | 21 ++++++++++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/actions/recomputeReduxState.js b/src/actions/recomputeReduxState.js index 69b223cbe..6a68babb4 100644 --- a/src/actions/recomputeReduxState.js +++ b/src/actions/recomputeReduxState.js @@ -995,7 +995,7 @@ export const createStateFromQueryOrJSONs = ({ }); /* STREAMS */ - tree.streams = partitionIntoStreams(tree.nodes, controls.colorScale) + tree.streams = partitionIntoStreams(tree.nodes, controls.colorScale, controls.absoluteDateMinNumeric, controls.absoluteDateMaxNumeric) console.log("tree.streams", tree.streams) diff --git a/src/util/partitionIntoStreams.js b/src/util/partitionIntoStreams.js index c8667f10a..f66a33f36 100644 --- a/src/util/partitionIntoStreams.js +++ b/src/util/partitionIntoStreams.js @@ -3,13 +3,17 @@ import { getTraitFromNode } from "./treeMiscHelpers" // Prototype - hardcode the CA of streams function _isFounderNode(node) { + + // if (node?.branch_attrs?.labels?.clade) return true; + // return false; + const FOUNDERS = [ "NODE_0000731", "NODE_0001569", // NOTE - other founder nodes are descendants of this clade "NODE_0000648", - "NODE_0000038", - "NODE_0001227", - "NODE_0001571", + // "NODE_0000038", + // "NODE_0001227", + // "NODE_0001571", // "NODE_0001773", // VERY BASAL IN TREE - everything is a descendant of this node ] return FOUNDERS.includes(node.name); @@ -23,7 +27,7 @@ function _isFounderNode(node) { * - only works for categorical colorScale * - only works for temporal tree */ -export function partitionIntoStreams(nodes, colorScale) { +export function partitionIntoStreams(nodes, colorScale, absoluteDateMinNumeric, absoluteDateMaxNumeric) { const {founderIndiciesToDescendantFounderIndicies, founderIndiciesPostorder} = getFounderTree(nodes[0], _isFounderNode); @@ -62,7 +66,7 @@ export function partitionIntoStreams(nodes, colorScale) { } stream.categories = observedCategories(nodesInStream, colorScale); stream.categoryColors = stream.categories.map((value) => colorScale.scale(value)) - const pivotData = calcPivots(nodesInStream); + const pivotData = calcPivots(nodesInStream, absoluteDateMinNumeric, absoluteDateMaxNumeric); stream.pivotIntervals = pivotData.intervals; stream.pivots = pivotData.pivots; stream.nodeIdxs = groupNodesIntoIntervals(nodesInStream, pivotData.intervals); // indexed by pivot idx @@ -87,14 +91,17 @@ function observedCategories(nodes, colorScale) { return Array.from(values).sort((a,b) => colorScale.legendValues.indexOf(a) - colorScale.legendValues.indexOf(b)) } -function calcPivots(nodes) { +function calcPivots(nodes, absoluteDateMinNumeric, absoluteDateMaxNumeric) { const domain = nodes.reduce((acc, node) => { const value = getTraitFromNode(node, "num_date"); // TODO XXX if (acc[0] > value) acc[0] = value; if (acc[1] < value) acc[1] = value; return acc; }, [Infinity, -Infinity]) - const nPivots = 10; // TODO XXX + + const domainFraction = (domain[1]-domain[0]) / (absoluteDateMaxNumeric - absoluteDateMinNumeric); + const availablePivots = 50; + const nPivots = Math.ceil(domainFraction * availablePivots); const size = (domain[1]-domain[0])/(nPivots-1); const intervals = Array.from(Array(nPivots), undefined); intervals[0] = [domain[0], domain[0] + size/2]; From f959f647cf59a22134b732b7c734cc2ffd541814 Mon Sep 17 00:00:00 2001 From: james hadfield Date: Wed, 23 Oct 2024 16:11:38 +1300 Subject: [PATCH 05/22] WIP toggle streams on/off --- src/actions/recomputeReduxState.js | 3 +-- src/actions/streamTrees.js | 13 +++++++++++++ src/actions/types.js | 1 + src/components/controls/choose-metric.js | 14 +++++++++++++- src/components/tree/index.ts | 1 + src/components/tree/phyloTree/labels.js | 4 +++- src/components/tree/phyloTree/renderers.ts | 7 +++---- src/components/tree/tree.tsx | 15 +++++++++++++++ src/reducers/controls.ts | 3 +++ src/reducers/tree/index.ts | 4 +++- src/util/partitionIntoStreams.js | 17 +++++++++-------- 11 files changed, 65 insertions(+), 17 deletions(-) create mode 100644 src/actions/streamTrees.js diff --git a/src/actions/recomputeReduxState.js b/src/actions/recomputeReduxState.js index 6a68babb4..2624d62c5 100644 --- a/src/actions/recomputeReduxState.js +++ b/src/actions/recomputeReduxState.js @@ -995,10 +995,9 @@ export const createStateFromQueryOrJSONs = ({ }); /* STREAMS */ - tree.streams = partitionIntoStreams(tree.nodes, controls.colorScale, controls.absoluteDateMinNumeric, controls.absoluteDateMaxNumeric) + tree.streams = partitionIntoStreams(controls.showStreamTrees, tree.nodes, controls.colorScale, controls.absoluteDateMinNumeric, controls.absoluteDateMaxNumeric) console.log("tree.streams", tree.streams) - /* calculate entropy in view */ if (entropy.loaded) { /* The selected CDS + positions are only known if a genotype color-by has been set (display defaults | url) */ diff --git a/src/actions/streamTrees.js b/src/actions/streamTrees.js new file mode 100644 index 000000000..c4ee0c81f --- /dev/null +++ b/src/actions/streamTrees.js @@ -0,0 +1,13 @@ + +import { TOGGLE_STREAM_TREE } from "./types"; +import { partitionIntoStreams } from "../util/partitionIntoStreams"; + +export function toggleStreamTree() { + return function(dispatch, getState) { + const {controls, tree} = getState(); + const showStreamTrees = !controls.showStreamTrees; + const streams = partitionIntoStreams(showStreamTrees, tree.nodes, controls.colorScale, controls.absoluteDateMinNumeric, controls.absoluteDateMaxNumeric) + console.log("THUNK::New streams structure:", streams) + dispatch({type: TOGGLE_STREAM_TREE, showStreamTrees, streams}) + } +} \ No newline at end of file diff --git a/src/actions/types.js b/src/actions/types.js index dffa484af..5a3e3bc07 100644 --- a/src/actions/types.js +++ b/src/actions/types.js @@ -62,3 +62,4 @@ export const TOGGLE_SHOW_ALL_BRANCH_LABELS = "TOGGLE_SHOW_ALL_BRANCH_LABELS"; export const TOGGLE_MOBILE_DISPLAY = "TOGGLE_MOBILE_DISPLAY"; export const SELECT_NODE = "SELECT_NODE"; export const DESELECT_NODE = "DESELECT_NODE"; +export const TOGGLE_STREAM_TREE = "TOGGLE_STREAM_TREE"; diff --git a/src/components/controls/choose-metric.js b/src/components/controls/choose-metric.js index e4ff42596..5543e92fb 100644 --- a/src/components/controls/choose-metric.js +++ b/src/components/controls/choose-metric.js @@ -4,6 +4,7 @@ import { withTranslation } from "react-i18next"; import { CHANGE_DISTANCE_MEASURE } from "../../actions/types"; import { analyticsControlsEvent } from "../../util/googleAnalytics"; import { toggleTemporalConfidence } from "../../actions/tree"; +import { toggleStreamTree } from "../../actions/streamTrees"; import { SidebarSubtitle, SidebarButton } from "./styles"; import Toggle from "./toggle"; @@ -14,7 +15,8 @@ import Toggle from "./toggle"; layout: state.controls.layout, showTreeToo: state.controls.showTreeToo, branchLengthsToDisplay: state.controls.branchLengthsToDisplay, - temporalConfidence: state.controls.temporalConfidence + temporalConfidence: state.controls.temporalConfidence, + showStreamTrees: state.controls.showStreamTrees, }; }) class ChooseMetric extends React.Component { @@ -62,6 +64,16 @@ class ChooseMetric extends React.Component { ) } + +
+ this.props.dispatch(toggleStreamTree())} + label={t("sidebar:Show stream trees")} + /> +
); } diff --git a/src/components/tree/index.ts b/src/components/tree/index.ts index 10de934bc..a9b9acc1a 100644 --- a/src/components/tree/index.ts +++ b/src/components/tree/index.ts @@ -21,6 +21,7 @@ const mapStateToProps: MapStateToProps d.n.branch_attrs && d.n.branch_attrs.labels && d.n.branch_attrs.labels[key] ) - .filter((d) => this.mask?.[d.n.arrayIdx]===1) + .filter((d) => this.streams?.mask?.[d.n.arrayIdx]===1) ) .enter() .append("text") diff --git a/src/components/tree/phyloTree/renderers.ts b/src/components/tree/phyloTree/renderers.ts index eb7c064ea..3850bdc88 100644 --- a/src/components/tree/phyloTree/renderers.ts +++ b/src/components/tree/phyloTree/renderers.ts @@ -92,7 +92,6 @@ export const render = function render( // As long as we keep redux.tree.streams the same object, and also streams.mask the same array, // the following should be references and thus always in-sync. // (of course we'll need to call the appropriate render functions on an update) - this.mask = streams?.mask; this.streams = streams; // TODO - supply as a arg? Must revisit /* set nodes stroke / fill */ @@ -169,7 +168,7 @@ export const drawTips = function drawTips(this: PhyloTreeType): void { } this.groups.tips .selectAll(".tip") - .data(this.nodes.filter((d) => !d.n.hasChildren).filter((d) => this.mask?.[d.n.arrayIdx]===1)) + .data(this.nodes.filter((d) => !d.n.hasChildren).filter((d) => this.streams?.mask?.[d.n.arrayIdx]===1)) .enter() .append("circle") .attr("class", "tip") @@ -249,7 +248,7 @@ export const drawBranches = function drawBranches(this: PhyloTreeType): void { .selectAll('.branch') .data(this.nodes .filter((d) => d.n.hasChildren && d.displayOrder !== undefined) - .filter((d) => this.mask?.[d.n.arrayIdx]===1) + .filter((d) => this.streams?.mask?.[d.n.arrayIdx]===1) ) .enter() .append("path") @@ -282,7 +281,7 @@ export const drawBranches = function drawBranches(this: PhyloTreeType): void { } this.groups.branchStem .selectAll('.branch') - .data(this.nodes.filter((d) => d.displayOrder !== undefined).filter((d) => this.mask?.[d.n.arrayIdx]===1)) + .data(this.nodes.filter((d) => d.displayOrder !== undefined).filter((d) => this.streams?.mask?.[d.n.arrayIdx]===1)) .enter() .append("path") .attr("class", "branch S") diff --git a/src/components/tree/tree.tsx b/src/components/tree/tree.tsx index e4398230c..0d4da81eb 100644 --- a/src/components/tree/tree.tsx +++ b/src/components/tree/tree.tsx @@ -91,6 +91,21 @@ export class TreeComponent extends React.Component = {}; let rightTreeUpdated = false; + /** + * Turning on/off streams requires a lot of d3 work (certain dom elements need to be added/removed, not just updated) + * so just re-render everything. Note that this doesn't preserve zooming :( + * TODO XXX - we want to utilise the canonical changePhyloTreeViaPropsComparison approach here, obviously + */ + if (prevProps.showStreamTrees !== this.props.showStreamTrees) { + this.state.tree.clearSVG(); + const newState = {}; + newState.tree = new PhyloTree(this.props.tree.nodes, lhsTreeId, this.props.tree.idxOfInViewRootNode); + renderTree(this, true, newState.tree, this.props); + this.setState(newState); /* this will trigger an unnecessary CDU :( */ + return; + } + + /* potentially change the (main / left hand) tree */ const { newState: potentialNewState, diff --git a/src/reducers/controls.ts b/src/reducers/controls.ts index c04271d0c..0de11bd35 100644 --- a/src/reducers/controls.ts +++ b/src/reducers/controls.ts @@ -234,6 +234,7 @@ export const getDefaultControlsState = (): ControlsState => { tipLabelKey: defaults.tipLabelKey, showTreeToo: false, showTangle: false, + showStreamTrees: true, // TODO XXX. We also need some concept of "canShowStreamTrees" zoomMin: undefined, zoomMax: undefined, branchLengthsToDisplay: "divAndDate", @@ -461,6 +462,8 @@ const Controls = (state: ControlsState = getDefaultControlsState(), action): Con return Object.assign({}, state, { sidebarOpen: action.value }); case types.TOGGLE_LEGEND: return Object.assign({}, state, { legendOpen: action.value }); + case types.TOGGLE_STREAM_TREE: + return {...state, showStreamTrees: action.showStreamTrees}; case types.ADD_EXTRA_METADATA: { for (const colorBy of Object.keys(action.newColorings)) { state.coloringsPresentOnTree.add(colorBy); diff --git a/src/reducers/tree/index.ts b/src/reducers/tree/index.ts index b94db0394..e407e7072 100644 --- a/src/reducers/tree/index.ts +++ b/src/reducers/tree/index.ts @@ -70,8 +70,10 @@ const Tree = ( return { ...state, nodeColors: action.nodeColors, - nodeColorsVersion: action.version, + nodeColorsVersion: action.version }; + case types.TOGGLE_STREAM_TREE: + return {...state, streams: action.streams}; case types.TREE_TOO_DATA: return action.tree; case types.ADD_EXTRA_METADATA: { diff --git a/src/util/partitionIntoStreams.js b/src/util/partitionIntoStreams.js index f66a33f36..fce9bed4f 100644 --- a/src/util/partitionIntoStreams.js +++ b/src/util/partitionIntoStreams.js @@ -27,20 +27,21 @@ function _isFounderNode(node) { * - only works for categorical colorScale * - only works for temporal tree */ -export function partitionIntoStreams(nodes, colorScale, absoluteDateMinNumeric, absoluteDateMaxNumeric) { +export function partitionIntoStreams(enabled, nodes, colorScale, absoluteDateMinNumeric, absoluteDateMaxNumeric) { - const {founderIndiciesToDescendantFounderIndicies, founderIndiciesPostorder} = - getFounderTree(nodes[0], _isFounderNode); - - const streams = { streams: [], mask: nodes.map((_) => 1), // 1 = show nodes as normal, 0 = mask out, nodes are part of a stream - // founderTree, - founderIndiciesToDescendantFounderIndicies, - founderIndiciesPostorder, } + if (!enabled) return streams; + + const {founderIndiciesToDescendantFounderIndicies, founderIndiciesPostorder} = + getFounderTree(nodes[0], _isFounderNode); + + streams.founderIndiciesToDescendantFounderIndicies = founderIndiciesToDescendantFounderIndicies; + streams.founderIndiciesPostorder = founderIndiciesPostorder; + streams.streams = founderIndiciesPostorder.map((founderIdx) => { const stream = {}; stream.founderIdx = founderIdx; From cb5fb60dcb2d4a00d73d1a57cd5ebc09530b4379 Mon Sep 17 00:00:00 2001 From: james hadfield Date: Thu, 24 Oct 2024 13:10:54 +1300 Subject: [PATCH 06/22] WIP - visibility updates via redraw --- src/actions/recomputeReduxState.js | 2 +- src/actions/tree.ts | 4 +++ src/components/tree/phyloTree/change.ts | 21 ++++++++++++--- src/components/tree/phyloTree/layouts.ts | 26 +++++++++++++------ src/components/tree/phyloTree/types.ts | 1 + .../tree/reactD3Interface/change.ts | 1 + src/components/tree/tree.tsx | 3 +-- src/components/tree/types.ts | 1 + src/reducers/controls.ts | 2 ++ src/reducers/tree/index.ts | 15 ++++++++--- src/util/partitionIntoStreams.js | 16 +++++++----- src/util/treeVisibilityHelpers.js | 13 +++++++++- 12 files changed, 80 insertions(+), 25 deletions(-) diff --git a/src/actions/recomputeReduxState.js b/src/actions/recomputeReduxState.js index 2624d62c5..001801558 100644 --- a/src/actions/recomputeReduxState.js +++ b/src/actions/recomputeReduxState.js @@ -995,7 +995,7 @@ export const createStateFromQueryOrJSONs = ({ }); /* STREAMS */ - tree.streams = partitionIntoStreams(controls.showStreamTrees, tree.nodes, controls.colorScale, controls.absoluteDateMinNumeric, controls.absoluteDateMaxNumeric) + tree.streams = partitionIntoStreams(controls.showStreamTrees, tree.nodes, tree.visibility, controls.colorScale, controls.absoluteDateMinNumeric, controls.absoluteDateMaxNumeric) console.log("tree.streams", tree.streams) /* calculate entropy in view */ diff --git a/src/actions/tree.ts b/src/actions/tree.ts index 34967f0e4..7c8e33f9f 100644 --- a/src/actions/tree.ts +++ b/src/actions/tree.ts @@ -118,6 +118,7 @@ export const updateVisibleTipsAndBranchThicknesses = ({ idxOfFilteredRoot: data.idxOfFilteredRoot, cladeName: cladeSelected, selectedClade: cladeSelected, + countsByCategoryPerStream: data.countsByCategoryPerStream, }; if (controls.showTreeToo) { @@ -135,6 +136,7 @@ export const updateVisibleTipsAndBranchThicknesses = ({ dispatchObj.branchThicknessVersionToo = dataToo.branchThicknessVersion; dispatchObj.idxOfInViewRootNodeToo = rootIdxTree2; dispatchObj.idxOfFilteredRootToo = dataToo.idxOfFilteredRoot; + dispatchObj.countsByCategoryPerStream = dataToo.countsByCategoryPerStream; /* tip selected is the same as the first tree - the reducer uses that */ } @@ -194,6 +196,7 @@ export const changeDateFilter = ({ branchThickness: data.branchThickness, branchThicknessVersion: data.branchThicknessVersion, idxOfInViewRootNode: tree.idxOfInViewRootNode, + countsByCategoryPerStream: data.countsByCategoryPerStream, }; if (controls.showTreeToo) { const dataToo = calculateVisiblityAndBranchThickness(treeToo, controls, dates); @@ -202,6 +205,7 @@ export const changeDateFilter = ({ dispatchObj.visibilityVersionToo = dataToo.visibilityVersion; dispatchObj.branchThicknessToo = dataToo.branchThickness; dispatchObj.branchThicknessVersionToo = dataToo.branchThicknessVersion; + dispatchObj.countsByCategoryPerStream = dataToo.countsByCategoryPerStream; } /* Changes in visibility require a recomputation of which legend items we wish to display */ diff --git a/src/components/tree/phyloTree/change.ts b/src/components/tree/phyloTree/change.ts index 0c586f7df..06ca7f73b 100644 --- a/src/components/tree/phyloTree/change.ts +++ b/src/components/tree/phyloTree/change.ts @@ -8,6 +8,7 @@ import { getBranchVisibility, strokeForBranch } from "./renderers"; import { shouldDisplayTemporalConfidence } from "../../../reducers/controls"; import { makeTipLabelFunc } from "./labels"; import { ChangeParams, PhyloNode, PhyloTreeType, PropsForPhyloNodes, SVGProperty, TreeElement } from "./types"; +import { mapStreamsToScreen } from "./layouts"; /* loop through the nodes and update each provided prop with the new value * additionally, set d.update -> whether or not the node props changed @@ -287,6 +288,7 @@ interface Extras { export const change = function change( this: PhyloTreeType, { + /* booleans for what should be changed */ changeColorBy = false, changeVisibility = false, changeTipRadii = false, @@ -297,21 +299,25 @@ export const change = function change( svgHasChangedDimensions = false, animationInProgress = false, changeNodeOrder = false, - focus = false, + /* change these things to provided value (unless undefined) */ newDistance = undefined, newLayout = undefined, - updateLayout = 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, + streams = undefined, // PROTOTYPE scatterVariables = undefined, - performanceFlags = undefined, + performanceFlags = new Map(), }: ChangeParams ): void { // console.log("\n** phylotree.change() (time since last run:", Date.now() - this.timeLastRenderRequested, "ms) **\n\n"); @@ -345,6 +351,15 @@ export const change = function change( elemsToUpdate.add(".tip").add(".tipLabel").add(".branchLabel"); svgPropsToUpdate.add("visibility").add("cursor"); nodePropsToModify.visibility = visibility; + + this.streams = streams; + this.streamLayout(); // recompute displayOrder values across pivots + mapStreamsToScreen(this.streams, this.phyloStreams, this.xScale, this.yScale); // recompute pixels (unneeded for branches/tips) + console.log("Updated phylotree streams data & recomputed layout"); + /* add stream SVG elements */ + this.drawStreams() + + } if (changeTipRadii) { elemsToUpdate.add(".tip"); diff --git a/src/components/tree/phyloTree/layouts.ts b/src/components/tree/phyloTree/layouts.ts index 4c81d89e7..5ce57c5df 100644 --- a/src/components/tree/phyloTree/layouts.ts +++ b/src/components/tree/phyloTree/layouts.ts @@ -115,6 +115,8 @@ export function streamLayout(this: PhyloTreeType): void { displayOrderUsed[stream.founderIdx] = displayOrderTotal; // scale this display order by maxNodesInInterval so the stream never exceeds the allocated range + // note that maxNodesInInterval doesn't take into account visibility settings, i.e. it's max nodes assuming everything's visible + // I think this works well for filtering, but unsure about zooming const displayOrderScalar = displayOrderTotal / stream.maxNodesInInterval; const baseDisplayOrder = displayOrders[0]; @@ -594,19 +596,27 @@ export const mapToScreen = function mapToScreen(this: PhyloTreeType): void { } // PROTOTYPE - for (const [streamIdx, stream] of this.phyloStreams.entries()) { - const reduxStream = this.streams.streams[streamIdx]; // urgh need better names - stream.x = reduxStream.pivots.map((pivot) => this.xScale(pivot)) + mapStreamsToScreen(this.streams, this.phyloStreams, this.xScale, this.yScale) + + + timerEnd("mapToScreen"); +}; + + +/** + * modifies phyloStreams object in place + */ +export function mapStreamsToScreen(streams, phyloStreams, xScale, yScale) { + for (const [streamIdx, stream] of phyloStreams.entries()) { + const reduxStream = streams.streams[streamIdx]; // urgh need better names + stream.x = reduxStream.pivots.map((pivot) => xScale(pivot)) stream.y = stream.displayOrderByCategory.map((displayOrderByPivot) => { return displayOrderByPivot.map(([min,max]) => { - return [this.yScale(min), this.yScale(max)] + return [yScale(min), yScale(max)] }) }) } - - - timerEnd("mapToScreen"); -}; +} const JITTER_MIN_STEP_SIZE = 50; // pixels diff --git a/src/components/tree/phyloTree/types.ts b/src/components/tree/phyloTree/types.ts index 71132f1c2..d2f2d6ea5 100644 --- a/src/components/tree/phyloTree/types.ts +++ b/src/components/tree/phyloTree/types.ts @@ -217,6 +217,7 @@ export interface ChangeParams { // other data // scatterVariables?: ScatterVariables performanceFlags?: PerformanceFlags + streams?: any // TODO XXX } export interface PhyloTreeType { diff --git a/src/components/tree/reactD3Interface/change.ts b/src/components/tree/reactD3Interface/change.ts index 24c309b60..cf7009cbb 100644 --- a/src/components/tree/reactD3Interface/change.ts +++ b/src/components/tree/reactD3Interface/change.ts @@ -43,6 +43,7 @@ export const changePhyloTreeViaPropsComparison = ( if (!!newTreeRedux.visibilityVersion && oldTreeRedux.visibilityVersion !== newTreeRedux.visibilityVersion) { args.changeVisibility = true; args.visibility = newTreeRedux.visibility; + args.streams = newTreeRedux.streams; } /* tip radii */ diff --git a/src/components/tree/tree.tsx b/src/components/tree/tree.tsx index 0d4da81eb..b1eb94206 100644 --- a/src/components/tree/tree.tsx +++ b/src/components/tree/tree.tsx @@ -98,10 +98,9 @@ export class TreeComponent extends React.Component(newState); /* this will trigger an unnecessary CDU :( */ return; } diff --git a/src/components/tree/types.ts b/src/components/tree/types.ts index c3773949b..8e4a0fc1e 100644 --- a/src/components/tree/types.ts +++ b/src/components/tree/types.ts @@ -44,6 +44,7 @@ export interface TreeComponentStateProps { tipLabelKey: string | symbol tree: TreeState treeToo: TreeTooState + showStreamTrees: boolean } export interface TreeComponentState { diff --git a/src/reducers/controls.ts b/src/reducers/controls.ts index 0de11bd35..dee0e1e9a 100644 --- a/src/reducers/controls.ts +++ b/src/reducers/controls.ts @@ -149,6 +149,8 @@ export interface BasicControlsState { tipLabelKey: string | symbol zoomMax?: number zoomMin?: number + + showStreamTrees: boolean } export interface MeasurementsControlState { diff --git a/src/reducers/tree/index.ts b/src/reducers/tree/index.ts index e407e7072..119d2b94e 100644 --- a/src/reducers/tree/index.ts +++ b/src/reducers/tree/index.ts @@ -55,10 +55,17 @@ const Tree = ( cladeName: action.cladeName, selectedClade: action.cladeName, }; - return { - ...state, - ...newStates, - }; + if (action.countsByCategoryPerStream) { + newStates.streams = { + ...state.streams, + streams: state.streams.streams.map((stream, idx) => ({ + ...stream, + countsByCategory: action.countsByCategoryPerStream[idx] + })) + }; + } + + return Object.assign({}, state, newStates); } case types.UPDATE_TIP_RADII: return { diff --git a/src/util/partitionIntoStreams.js b/src/util/partitionIntoStreams.js index fce9bed4f..e1d902303 100644 --- a/src/util/partitionIntoStreams.js +++ b/src/util/partitionIntoStreams.js @@ -1,4 +1,5 @@ import { getTraitFromNode } from "./treeMiscHelpers" +import { NODE_VISIBLE } from "./globals"; // Prototype - hardcode the CA of streams @@ -27,7 +28,7 @@ function _isFounderNode(node) { * - only works for categorical colorScale * - only works for temporal tree */ -export function partitionIntoStreams(enabled, nodes, colorScale, absoluteDateMinNumeric, absoluteDateMaxNumeric) { +export function partitionIntoStreams(enabled, nodes, visibility, colorScale, absoluteDateMinNumeric, absoluteDateMaxNumeric) { const streams = { streams: [], @@ -65,15 +66,17 @@ export function partitionIntoStreams(enabled, nodes, colorScale, absoluteDateMin stack.push(child) } } + // categories may have zero counts associated with them (over all pivots) depending on visibility settings stream.categories = observedCategories(nodesInStream, colorScale); stream.categoryColors = stream.categories.map((value) => colorScale.scale(value)) const pivotData = calcPivots(nodesInStream, absoluteDateMinNumeric, absoluteDateMaxNumeric); stream.pivotIntervals = pivotData.intervals; stream.pivots = pivotData.pivots; + // nodeIdxs are all nodes, visible and not visible stream.nodeIdxs = groupNodesIntoIntervals(nodesInStream, pivotData.intervals); // indexed by pivot idx - stream.numNodes = nodesInStream.length; + // stream.numNodes = nodesInStream.length; stream.maxNodesInInterval = Math.max(...stream.nodeIdxs.map((idxs) => idxs.length)); - stream.countsByCategory = groupNodesByCategory(nodes, stream.nodeIdxs, colorScale.colorBy, stream.categories); + stream.countsByCategory = countsByCategory(nodes, stream.nodeIdxs, visibility, colorScale.colorBy, stream.categories); return stream; }) @@ -134,14 +137,15 @@ function groupNodesIntoIntervals(nodes, intervals) { return groups; } -function groupNodesByCategory(nodes, nodeIdxsByPivot, colorBy, categories) { +export function countsByCategory(nodes, nodeIdxsByPivot, visibility, colorBy, categories) { return categories.map((category) => { return nodeIdxsByPivot.map((nodeIdxs) => { - return nodeIdxs.filter((nodeIdx) => getTraitFromNode(nodes[nodeIdx], colorBy)===category).length + return nodeIdxs.filter( + (nodeIdx) => getTraitFromNode(nodes[nodeIdx], colorBy)===category && visibility[nodeIdx]===NODE_VISIBLE + ).length }) }) } - /** * * @param {object} rootNode redux tree node diff --git a/src/util/treeVisibilityHelpers.js b/src/util/treeVisibilityHelpers.js index adbe272fe..b7496939c 100644 --- a/src/util/treeVisibilityHelpers.js +++ b/src/util/treeVisibilityHelpers.js @@ -2,6 +2,7 @@ import { freqScale, NODE_NOT_VISIBLE, NODE_VISIBLE_TO_MAP_ONLY, NODE_VISIBLE, ge import { calcTipCounts } from "./treeCountingHelpers"; import { getTraitFromNode } from "./treeMiscHelpers"; import { warningNotification } from "../actions/notifications"; +import { countsByCategory } from "./partitionIntoStreams"; export const getVisibleDateRange = (nodes, visibility) => nodes .filter((node, idx) => (visibility[idx] === NODE_VISIBLE && !node.hasChildren)) @@ -239,13 +240,23 @@ export const calculateVisiblityAndBranchThickness = (tree, controls, dates) => { const visibility = calcVisibility(tree, controls, dates, inView, filtered); /* recalculate tipCounts over the tree - modifies redux tree nodes in place (yeah, I know) */ calcTipCounts(tree.nodes[0], visibility); + + let countsByCategoryPerStream; + /* Prototype stream trees */ + if (controls.showStreamTrees && tree.streams) { + countsByCategoryPerStream = tree.streams.streams.map((stream) => { + return countsByCategory(tree.nodes, stream.nodeIdxs, visibility, controls.colorScale.colorBy, stream.categories); + }) + } + /* re-calculate branchThickness (inline) */ return { visibility: visibility, visibilityVersion: tree.visibilityVersion + 1, branchThickness: calcBranchThickness(tree.nodes, visibility), branchThicknessVersion: tree.branchThicknessVersion + 1, - idxOfFilteredRoot: idxOfFilteredRoot + idxOfFilteredRoot: idxOfFilteredRoot, + countsByCategoryPerStream }; }; From a361de785ddc287524ce7cf4a88c7a899f0bf29c Mon Sep 17 00:00:00 2001 From: james hadfield Date: Thu, 24 Oct 2024 15:19:21 +1300 Subject: [PATCH 07/22] WIP visibility uptates via attr changes --- src/components/tree/phyloTree/change.ts | 18 +++++++-- src/components/tree/phyloTree/layouts.ts | 45 ++++++++++++++++++---- src/components/tree/phyloTree/renderers.ts | 36 +++++++++-------- src/components/tree/phyloTree/types.ts | 5 ++- 4 files changed, 75 insertions(+), 29 deletions(-) diff --git a/src/components/tree/phyloTree/change.ts b/src/components/tree/phyloTree/change.ts index 06ca7f73b..6a9fdbf3b 100644 --- a/src/components/tree/phyloTree/change.ts +++ b/src/components/tree/phyloTree/change.ts @@ -189,6 +189,19 @@ export const modifySVG = function modifySVG( if (this.regression) this.drawRegression(); } + if (elemsToUpdate.has('.stream')) { + /** Generating this here rather than `createUpdateCall` as we often don't want to update paths (SVG 'd' properties) for tips/branches + * when we do want to update them for streams. Currently it's all-or-nothing within `createUpdateCall` - all paths are updated or none are. + */ + const updateCall = (selection) => { + selection.attr("d", (d) => { + return d[0].area(d); + }) + } + genericSelectAndModify(this.svg, ".stream", updateCall, transitionTime); + } + + /* confidence intervals */ if (extras.removeConfidences && this.confidencesInSVG) { this.removeConfidence(); /* do not use a transition time - it's too clunky (too many elements?) */ @@ -357,9 +370,8 @@ export const change = function change( mapStreamsToScreen(this.streams, this.phyloStreams, this.xScale, this.yScale); // recompute pixels (unneeded for branches/tips) console.log("Updated phylotree streams data & recomputed layout"); /* add stream SVG elements */ - this.drawStreams() - - + // this.drawStreams(); // this would tear it down and rebuild it (slow...) + elemsToUpdate.add('.stream') } if (changeTipRadii) { elemsToUpdate.add(".tip"); diff --git a/src/components/tree/phyloTree/layouts.ts b/src/components/tree/phyloTree/layouts.ts index 5ce57c5df..f4156b50a 100644 --- a/src/components/tree/phyloTree/layouts.ts +++ b/src/components/tree/phyloTree/layouts.ts @@ -8,6 +8,7 @@ import { numDate } from "../../../util/colorHelpers"; import { Layout, ScatterVariables } from "../../../reducers/controls"; import { ReduxNode } from "../../../reducers/tree/types"; import { Distance, Params, PhyloNode, PhyloTreeType } from "./types"; +import { area, curveCatmullRom } from "d3-shape"; /** * assigns the attribute this.layout and calls the function that @@ -79,9 +80,18 @@ export function streamLayout(this: PhyloTreeType): void { const displayOrderUsed = {} + if (!this.phyloStreams) { + /* it's important we only set this up once, as DOM elements will bind to data within, so we need to mutate it */ + this.phyloStreams = this.streams.streams.map((_) => ({})) + } + // TODO XXX - need to store this internally, but do we need this.streams or should this be an arg? // NOTE: this.streams is postorder (i.e. the founder nodes are postorder w.r.t other founders in the main tree) - this.phyloStreams = this.streams.streams.map((stream) => { + // this.phyloStreams = this.streams.streams.map((stream) => { + this.phyloStreams.forEach((phyloStream, streamIdx) => { + + const stream = this.streams.streams[streamIdx]; + const founderNode = this.nodes[stream.founderIdx]; // First get the display order range of the entire subtree of founderNode @@ -144,7 +154,8 @@ export function streamLayout(this: PhyloTreeType): void { } // NOTE: for num_date the value is the x value. Easy. - return {displayOrderByCategory} + // return {displayOrderByCategory} + phyloStream.displayOrderByCategory = displayOrderByCategory; // this is overwritten each update cycle - is this ok? }); console.log("this.phyloStreams", this.phyloStreams) @@ -604,15 +615,35 @@ export const mapToScreen = function mapToScreen(this: PhyloTreeType): void { /** - * modifies phyloStreams object in place + * modifies phyloStreams object in place (as it's attached to a DOM node I think, right?) */ export function mapStreamsToScreen(streams, phyloStreams, xScale, yScale) { + for (const phyloStream of phyloStreams) { + /* it's important we only set this up once, as DOM elements will bind to data within, so we need to mutate it */ + if (!phyloStream.ripples) { + const _area = area() + .x((d) => d.x) + .y0((d) => d.y0) + .y1((d) => d.y1) + .curve(curveCatmullRom.alpha(0.5)) + + phyloStream.ripples = phyloStream.displayOrderByCategory.map((displayOrderByPivot) => { + return displayOrderByPivot.map(() => { + return {area: _area} + }) + }) + } + } + for (const [streamIdx, stream] of phyloStreams.entries()) { const reduxStream = streams.streams[streamIdx]; // urgh need better names - stream.x = reduxStream.pivots.map((pivot) => xScale(pivot)) - stream.y = stream.displayOrderByCategory.map((displayOrderByPivot) => { - return displayOrderByPivot.map(([min,max]) => { - return [yScale(min), yScale(max)] + stream.displayOrderByCategory.forEach((displayOrderByPivot, categoryIdx) => { + stream.ripples[categoryIdx].update = true; // TODO XXX - needed as we filter on this property before updating the DOM. + // TODO XXX - we could work out whether we should actually update things here, i.e. whether anything's changed + displayOrderByPivot.forEach(([min,max], pivotIdx) => { + stream.ripples[categoryIdx][pivotIdx].x = xScale(reduxStream.pivots[pivotIdx]); + stream.ripples[categoryIdx][pivotIdx].y0 = yScale(min); + stream.ripples[categoryIdx][pivotIdx].y1 = yScale(max); }) }) } diff --git a/src/components/tree/phyloTree/renderers.ts b/src/components/tree/phyloTree/renderers.ts index 3850bdc88..0575391b4 100644 --- a/src/components/tree/phyloTree/renderers.ts +++ b/src/components/tree/phyloTree/renderers.ts @@ -5,7 +5,6 @@ import { makeRegressionText } from "./regression"; import { getEmphasizedColor } from "../../../util/colorHelpers"; import { Callbacks, Distance, Params, PhyloNode, PhyloTreeType } from "./types"; import { Selection } from "d3"; -import { area, curveCatmullRom } from "d3-shape"; import { Layout, ScatterVariables } from "../../../reducers/controls"; import { ReduxNode, Visibility } from "../../../reducers/tree/types"; @@ -307,30 +306,33 @@ export function drawStreams(this: PhyloTreeType): void { if (!("streams" in this.groups)) { this.groups.streams = this.svg.append("g").attr("id", "streams"); // .attr("clip-path", "url(#treeClip)"); + // add a group to encapsulate each stream + this.groups.streams.selectAll('g') + .data(this.phyloStreams) + .enter() + .append("g") + .attr("id", (_d, i) => `stream${i}`); } else { this.groups.streams.selectAll("*").remove(); } for (const [streamIdx, stream] of this.phyloStreams.entries()) { - - const areaObj = area() - .x((_d, pivotIdx) => { - // console.log("area d, i", d, pivotIdx); - return stream.x[pivotIdx]}) - .y0((d) => d[0]) - .y1((d) => d[1]) - .curve(curveCatmullRom.alpha(0.5)) - - - this.groups.streams.selectAll(`.stream${streamIdx}`) - .data(stream.y) + /** + * The element each selector gets ("d") is an element of stream.ripples, so + * d is an array with length=numPivots. + * Each of those elements, i.e. d[i], is an object with x, y0, y1 and a pointer to the + * area generator + */ + this.groups.streams.select(`#stream${streamIdx}`) + .selectAll(`.stream`) + .data(stream.ripples) .enter() .append("path") - .attr("d", areaObj as any) // TODO XXX - .attr("fill", (_d, i) => this.streams.streams[streamIdx].categoryColors[i]) + .attr("class", `stream`) + .attr("d", (d) => d[0].area(d)) + .attr("fill", (_d, i:number) => this.streams.streams[streamIdx].categoryColors[i]) } - - + // P.S. To select an individual stream tree: this.groups.streams.select('#stream0').selectAll(`.stream`) } diff --git a/src/components/tree/phyloTree/types.ts b/src/components/tree/phyloTree/types.ts index d2f2d6ea5..4fa6097e7 100644 --- a/src/components/tree/phyloTree/types.ts +++ b/src/components/tree/phyloTree/types.ts @@ -25,7 +25,8 @@ export type TreeElement = ".tip" | ".tipLabel" | ".vaccineCross" | - ".vaccineDottedLine" + ".vaccineDottedLine" | + ".stream" export interface Regression { intercept?: number @@ -300,7 +301,7 @@ export interface PhyloTreeType { streams: {[key:string]: any} // TODO XXX mask: (1|0)[] drawStreams: typeof renderers.drawStreams - phyloStreams: {[key:string]: any} // TODO XXX + phyloStreams: any[] // TODO XXX streamLayout: typeof layouts.streamLayout } From 96c74bde3b74ff51c85933b17aea91a0c9a3aa2c Mon Sep 17 00:00:00 2001 From: james hadfield Date: Thu, 24 Oct 2024 16:36:39 +1300 Subject: [PATCH 08/22] WIP zoom working --- src/components/tree/phyloTree/change.ts | 7 ++++++- src/components/tree/phyloTree/layouts.ts | 1 - src/components/tree/reactD3Interface/change.ts | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/tree/phyloTree/change.ts b/src/components/tree/phyloTree/change.ts index 6a9fdbf3b..b5a1306d5 100644 --- a/src/components/tree/phyloTree/change.ts +++ b/src/components/tree/phyloTree/change.ts @@ -368,7 +368,6 @@ export const change = function change( this.streams = streams; this.streamLayout(); // recompute displayOrder values across pivots mapStreamsToScreen(this.streams, this.phyloStreams, this.xScale, this.yScale); // recompute pixels (unneeded for branches/tips) - console.log("Updated phylotree streams data & recomputed layout"); /* add stream SVG elements */ // this.drawStreams(); // this would tear it down and rebuild it (slow...) elemsToUpdate.add('.stream') @@ -421,6 +420,12 @@ export const change = function change( zoomIntoClade : zoomIntoClade.n.parent.shell; applyToChildren(this.zoomNode, (d: PhyloNode) => {d.inView = true;}); + + /* PROTOTYPE STREAM TREES */ + this.streams = streams; + this.streamLayout(); // recompute displayOrder values across pivots + mapStreamsToScreen(this.streams, this.phyloStreams, this.xScale, this.yScale); // recompute pixels (unneeded for branches/tips) + elemsToUpdate.add('.stream') } if (svgHasChangedDimensions || changeNodeOrder) { this.nodes.forEach((d) => {d.update = true;}); diff --git a/src/components/tree/phyloTree/layouts.ts b/src/components/tree/phyloTree/layouts.ts index f4156b50a..304f4a487 100644 --- a/src/components/tree/phyloTree/layouts.ts +++ b/src/components/tree/phyloTree/layouts.ts @@ -158,7 +158,6 @@ export function streamLayout(this: PhyloTreeType): void { phyloStream.displayOrderByCategory = displayOrderByCategory; // this is overwritten each update cycle - is this ok? }); - console.log("this.phyloStreams", this.phyloStreams) } /** diff --git a/src/components/tree/reactD3Interface/change.ts b/src/components/tree/reactD3Interface/change.ts index cf7009cbb..6ccacd0c0 100644 --- a/src/components/tree/reactD3Interface/change.ts +++ b/src/components/tree/reactD3Interface/change.ts @@ -40,10 +40,11 @@ export const changePhyloTreeViaPropsComparison = ( } /* visibility */ + // TODO XXX - under what circumstances is this conditional true vs (`dateRangeChange || filterChange`)? if (!!newTreeRedux.visibilityVersion && oldTreeRedux.visibilityVersion !== newTreeRedux.visibilityVersion) { args.changeVisibility = true; args.visibility = newTreeRedux.visibility; - args.streams = newTreeRedux.streams; + args.streams = newTreeRedux.streams; // or just reach in and do phylotree.streams = newTreeRedux.streams? } /* tip radii */ @@ -125,6 +126,7 @@ export const changePhyloTreeViaPropsComparison = ( if (newProps.layout === "unrooted") { args.updateLayout = true; } + args.streams = newTreeRedux.streams; // or just reach in and do phylotree.streams = newTreeRedux.streams? } if (oldProps.width !== newProps.width || oldProps.height !== newProps.height) { From 200b851590f5532f0391e0b76289f5da5b38e431 Mon Sep 17 00:00:00 2001 From: james hadfield Date: Thu, 24 Oct 2024 16:41:33 +1300 Subject: [PATCH 09/22] WIP fixup --- src/actions/streamTrees.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/actions/streamTrees.js b/src/actions/streamTrees.js index c4ee0c81f..1c4b86333 100644 --- a/src/actions/streamTrees.js +++ b/src/actions/streamTrees.js @@ -6,7 +6,7 @@ export function toggleStreamTree() { return function(dispatch, getState) { const {controls, tree} = getState(); const showStreamTrees = !controls.showStreamTrees; - const streams = partitionIntoStreams(showStreamTrees, tree.nodes, controls.colorScale, controls.absoluteDateMinNumeric, controls.absoluteDateMaxNumeric) + const streams = partitionIntoStreams(showStreamTrees, tree.nodes, tree.visibility, controls.colorScale, controls.absoluteDateMinNumeric, controls.absoluteDateMaxNumeric) console.log("THUNK::New streams structure:", streams) dispatch({type: TOGGLE_STREAM_TREE, showStreamTrees, streams}) } From 2d8a9b7262dfcfbb60001f49af7de554eb8900e5 Mon Sep 17 00:00:00 2001 From: james hadfield Date: Thu, 24 Oct 2024 17:02:45 +1300 Subject: [PATCH 10/22] WIP change colorBy --- src/actions/colors.js | 8 +++++++- src/components/tree/phyloTree/change.ts | 8 ++++++++ src/components/tree/phyloTree/renderers.ts | 10 ++++++---- src/components/tree/reactD3Interface/change.ts | 1 + src/reducers/tree/index.ts | 3 ++- 5 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/actions/colors.js b/src/actions/colors.js index 11bca2c06..2eb924c95 100644 --- a/src/actions/colors.js +++ b/src/actions/colors.js @@ -4,6 +4,7 @@ import { calcColorScale } from "../util/colorScale"; import { timerStart, timerEnd } from "../util/perf"; import { changeEntropyCdsSelection } from "./entropy"; import { updateFrequencyDataDebounced } from "./frequencies"; +import { partitionIntoStreams } from "../util/partitionIntoStreams"; import * as types from "./types"; /* providedColorBy: undefined | string */ @@ -27,13 +28,18 @@ export const changeColorBy = (providedColorBy = undefined) => { dispatch(changeEntropyCdsSelection(colorBy)); + // Recompute streams + const streams = partitionIntoStreams(controls.showStreamTrees, tree.nodes, tree.visibility, colorScale, controls.absoluteDateMinNumeric, controls.absoluteDateMaxNumeric) + + dispatch({ type: types.NEW_COLORS, colorBy, colorScale, nodeColors, nodeColorsToo, - version: colorScale.version + version: colorScale.version, + streams }); if (frequencies.loaded) { diff --git a/src/components/tree/phyloTree/change.ts b/src/components/tree/phyloTree/change.ts index b5a1306d5..386e1bfc2 100644 --- a/src/components/tree/phyloTree/change.ts +++ b/src/components/tree/phyloTree/change.ts @@ -402,6 +402,14 @@ export const change = function change( // recalculate gradients here? if (changeColorBy) { this.updateColorBy(); + + /* PROTOTYPE STREAM TREES */ + this.streams = streams; + this.phyloStreams = undefined; + this.streamLayout(); // recompute displayOrder values across pivots + mapStreamsToScreen(this.streams, this.phyloStreams, this.xScale, this.yScale); // recompute pixels (unneeded for branches/tips) + this.drawStreams(); // remove & redraw + } // recalculate existing regression if needed if (changeVisibility && this.regression) { diff --git a/src/components/tree/phyloTree/renderers.ts b/src/components/tree/phyloTree/renderers.ts index 0575391b4..22351c3e8 100644 --- a/src/components/tree/phyloTree/renderers.ts +++ b/src/components/tree/phyloTree/renderers.ts @@ -304,6 +304,7 @@ export const drawBranches = function drawBranches(this: PhyloTreeType): void { export function drawStreams(this: PhyloTreeType): void { + /* initial set up - should only ever run once */ if (!("streams" in this.groups)) { this.groups.streams = this.svg.append("g").attr("id", "streams"); // .attr("clip-path", "url(#treeClip)"); // add a group to encapsulate each stream @@ -311,11 +312,12 @@ export function drawStreams(this: PhyloTreeType): void { .data(this.phyloStreams) .enter() .append("g") - .attr("id", (_d, i) => `stream${i}`); - } else { - this.groups.streams.selectAll("*").remove(); + .attr("id", (_d, i: number) => `stream${i}`); } + /* if we call drawStreams() we're not trying to update, we want to remove all stream paths & redraw everything */ + this.groups.streams.selectAll(".stream").remove(); + for (const [streamIdx, stream] of this.phyloStreams.entries()) { /** * The element each selector gets ("d") is an element of stream.ripples, so @@ -332,7 +334,7 @@ export function drawStreams(this: PhyloTreeType): void { .attr("d", (d) => d[0].area(d)) .attr("fill", (_d, i:number) => this.streams.streams[streamIdx].categoryColors[i]) } - // P.S. To select an individual stream tree: this.groups.streams.select('#stream0').selectAll(`.stream`) + // P.S. To select an individual stream tree: this.groups.streams.select('#stream0').selectAll(`.stream`) } diff --git a/src/components/tree/reactD3Interface/change.ts b/src/components/tree/reactD3Interface/change.ts index 6ccacd0c0..b5d018a0b 100644 --- a/src/components/tree/reactD3Interface/change.ts +++ b/src/components/tree/reactD3Interface/change.ts @@ -37,6 +37,7 @@ export const changePhyloTreeViaPropsComparison = ( args.branchStroke = calculateStrokeColors(newTreeRedux, true, newProps.colorByConfidence, newProps.colorBy); args.tipStroke = calculateStrokeColors(newTreeRedux, false, newProps.colorByConfidence, newProps.colorBy); args.fill = args.tipStroke.map(getBrighterColor); + args.streams = newTreeRedux.streams; } /* visibility */ diff --git a/src/reducers/tree/index.ts b/src/reducers/tree/index.ts index 119d2b94e..0032914a5 100644 --- a/src/reducers/tree/index.ts +++ b/src/reducers/tree/index.ts @@ -77,7 +77,8 @@ const Tree = ( return { ...state, nodeColors: action.nodeColors, - nodeColorsVersion: action.version + nodeColorsVersion: action.version, + streams: action.streams, // replace entire structure }; case types.TOGGLE_STREAM_TREE: return {...state, streams: action.streams}; From 03b1bea623b224d9c49e0eda9a7510752c2063af Mon Sep 17 00:00:00 2001 From: james hadfield Date: Thu, 24 Oct 2024 17:11:39 +1300 Subject: [PATCH 11/22] WIP handle window resizes --- src/components/tree/phyloTree/change.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/tree/phyloTree/change.ts b/src/components/tree/phyloTree/change.ts index 386e1bfc2..94e5f023b 100644 --- a/src/components/tree/phyloTree/change.ts +++ b/src/components/tree/phyloTree/change.ts @@ -389,6 +389,8 @@ export const change = function change( elemsToUpdate.add(".grid").add(".regression"); svgPropsToUpdate.add("cx").add("cy").add("d").add("opacity") .add("visibility"); + mapStreamsToScreen(this.streams, this.phyloStreams, this.xScale, this.yScale); // recompute pixels (unneeded for branches/tips) + elemsToUpdate.add('.stream') } if (changeNodeOrder) { From f55d6a37f4e2fe4580527c6a34b1ce89af1a3845 Mon Sep 17 00:00:00 2001 From: james hadfield Date: Fri, 1 Nov 2024 10:20:24 +1300 Subject: [PATCH 12/22] wip --- src/util/partitionIntoStreams.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/util/partitionIntoStreams.js b/src/util/partitionIntoStreams.js index e1d902303..0a2755abb 100644 --- a/src/util/partitionIntoStreams.js +++ b/src/util/partitionIntoStreams.js @@ -9,10 +9,17 @@ function _isFounderNode(node) { // return false; const FOUNDERS = [ - "NODE_0000731", - "NODE_0001569", // NOTE - other founder nodes are descendants of this clade - "NODE_0000648", + // "NODE_0000731", + // "NODE_0001569", // NOTE - other founder nodes are descendants of this clade + // "NODE_0000648", // "NODE_0000038", + + + "NODE_0019794", + "NODE_0015887", + "NODE_0000777", + + // "NODE_0001227", // "NODE_0001571", // "NODE_0001773", // VERY BASAL IN TREE - everything is a descendant of this node @@ -25,7 +32,7 @@ function _isFounderNode(node) { /** * CAVEATS: * - only works for trees with "FOUNDERS" in it - * - only works for categorical colorScale + * - only works for categorical colorScal`e * - only works for temporal tree */ export function partitionIntoStreams(enabled, nodes, visibility, colorScale, absoluteDateMinNumeric, absoluteDateMaxNumeric) { From f28f7260a4996171873d9d9a89acc661b5860f93 Mon Sep 17 00:00:00 2001 From: james hadfield Date: Mon, 11 Nov 2024 11:24:19 +1300 Subject: [PATCH 13/22] wip - streams only show terminal nodes --- src/util/partitionIntoStreams.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/util/partitionIntoStreams.js b/src/util/partitionIntoStreams.js index 0a2755abb..c232582b3 100644 --- a/src/util/partitionIntoStreams.js +++ b/src/util/partitionIntoStreams.js @@ -52,8 +52,8 @@ export function partitionIntoStreams(enabled, nodes, visibility, colorScale, abs streams.streams = founderIndiciesPostorder.map((founderIdx) => { const stream = {}; - stream.founderIdx = founderIdx; - const nodesInStream = []; + stream.founderIdx = founderIdx; // index of the root node (not part of the stream as it's not a tip) + const nodesInStream = []; // TERMINAL NODES ONLY const founderNode = nodes[founderIdx]; stream.founderName = founderNode.name; const stack = [founderNode]; @@ -68,7 +68,11 @@ export function partitionIntoStreams(enabled, nodes, visibility, colorScale, abs // Note - this double counts this node I think // TODO - extend the stem of the founder node branch to join with the stream start point. // if (node.arrayIdx===founderNode.arrayIdx) streams.mask[node.arrayIdx] = 1; - nodesInStream.push(node); + + // nodesInStream is terminal only + if (!node.hasChildren) { + nodesInStream.push(node); + } for (const child of node.children || []) { stack.push(child) } From 7d0184cde8d8e66e8442f19f35ed007d54262389 Mon Sep 17 00:00:00 2001 From: james hadfield Date: Tue, 12 Nov 2024 08:34:03 +1300 Subject: [PATCH 14/22] wip - branch basics --- src/components/tree/phyloTree/change.ts | 2 +- src/components/tree/phyloTree/layouts.ts | 58 +++++++++++++++++- src/components/tree/phyloTree/phyloTree.ts | 1 + src/components/tree/phyloTree/renderers.ts | 20 +++++++ src/util/partitionIntoStreams.js | 69 ++++++++++++++-------- 5 files changed, 121 insertions(+), 29 deletions(-) diff --git a/src/components/tree/phyloTree/change.ts b/src/components/tree/phyloTree/change.ts index 94e5f023b..d624c9ce7 100644 --- a/src/components/tree/phyloTree/change.ts +++ b/src/components/tree/phyloTree/change.ts @@ -411,7 +411,7 @@ export const change = function change( this.streamLayout(); // recompute displayOrder values across pivots mapStreamsToScreen(this.streams, this.phyloStreams, this.xScale, this.yScale); // recompute pixels (unneeded for branches/tips) this.drawStreams(); // remove & redraw - + this.drawStreamConnectors(); // remove & redraw } // recalculate existing regression if needed if (changeVisibility && this.regression) { diff --git a/src/components/tree/phyloTree/layouts.ts b/src/components/tree/phyloTree/layouts.ts index 304f4a487..862330c65 100644 --- a/src/components/tree/phyloTree/layouts.ts +++ b/src/components/tree/phyloTree/layouts.ts @@ -88,7 +88,7 @@ export function streamLayout(this: PhyloTreeType): void { // TODO XXX - need to store this internally, but do we need this.streams or should this be an arg? // NOTE: this.streams is postorder (i.e. the founder nodes are postorder w.r.t other founders in the main tree) // this.phyloStreams = this.streams.streams.map((stream) => { - this.phyloStreams.forEach((phyloStream, streamIdx) => { + const intermediates = this.phyloStreams.map((phyloStream, streamIdx) => { const stream = this.streams.streams[streamIdx]; @@ -102,7 +102,9 @@ export function streamLayout(this: PhyloTreeType): void { } const getDisplayOrderExSubtrees = (node, top=true) => { - const children = (node.children || []).filter((child) => !this.streams.founderIndiciesPostorder.includes(child.arrayIdx)); + const founderIndicies = this.streams.streams.map((f) => f.founderIdx); + + const children = (node.children || []).filter((child) => !founderIndicies.includes(child.arrayIdx)); if (children.length) return getDisplayOrderExSubtrees(children.at(top?0:-1), top); return node.shell.displayOrder; } @@ -115,6 +117,7 @@ export function streamLayout(this: PhyloTreeType): void { // Get the total display order used up by _this_ stream, taking into account the display orders // which may be used for descendant streams (note- this is why we iterate through streams postorder) + // TODO XXX - only use of founderIndiciesToDescendantFounderIndicies const displayOrderTotal = (displayOrders[1] - displayOrders[0]) - this.streams.founderIndiciesToDescendantFounderIndicies[stream.founderIdx].reduce( (acc, founderIdx) => acc + displayOrderUsed[founderIdx], @@ -156,8 +159,54 @@ export function streamLayout(this: PhyloTreeType): void { // NOTE: for num_date the value is the x value. Easy. // return {displayOrderByCategory} phyloStream.displayOrderByCategory = displayOrderByCategory; // this is overwritten each update cycle - is this ok? + + return {displayOrderMidpoint}; + }); - + + /** + * Second loop (once displayOrderByCategory has been calculated for all streams) to work out the connectors + * a.k.a. branches between streams. + * TODO XXX: branch visibility -- partially done, but bugs + * TODO XXX: zoom / move + * TODO XXX: branch confidence / opacity / color + */ + this.phyloStreams.forEach((phyloStream, streamIdx) => { + const stream = this.streams.streams[streamIdx]; + let endDisplayOrder, endPivot; + // finds the first pivot index where we draw some of the stream + for (let pivotIdx=0; pivotIdx intermediates[streamIdx].displayOrderMidpoint ? 1 : 0; + treePhyloNode.displayOrderRange[teeIdx] = endDisplayOrder; + phyloStream.connectorFn = function(x, y) { // scale functions + return `M ${x(startXVal)} ${y(endDisplayOrder)} H ${x(endPivot)}`; + } + } else { // parent is a stream, so a little bit more complex + // const parentStream = this.streams.streams[stream.originatingStreamIdx]; + // const closestPivotIdx = parentStream.pivots.map((p) => Math.abs(p-xVal)).reduce((res, d, i) => (!res?.[0] || d this.streams.streams[streamIdx].categoryColors[i]) } // P.S. To select an individual stream tree: this.groups.streams.select('#stream0').selectAll(`.stream`) + + for (const [streamIdx, stream] of this.phyloStreams.entries()) { + this.groups.streams.select(`#stream${streamIdx}`) + .append("path") + .attr("class", 'connector') + .attr("d", stream.connectorPath) + .style("stroke-width", this.streams.streams[streamIdx].founderVisibility ? 2 : 1) + .style("stroke-dasharray", "3 2") + .style("stroke", () => this.streams.streams[streamIdx].startingColor) + .style("fill", "none") + .style('visibility', 'visible'); + } +} + +export function drawStreamConnectors() { + console.log("drawStreamConnectors is currently a no-op") } diff --git a/src/util/partitionIntoStreams.js b/src/util/partitionIntoStreams.js index c232582b3..94100080a 100644 --- a/src/util/partitionIntoStreams.js +++ b/src/util/partitionIntoStreams.js @@ -5,24 +5,17 @@ import { NODE_VISIBLE } from "./globals"; // Prototype - hardcode the CA of streams function _isFounderNode(node) { - // if (node?.branch_attrs?.labels?.clade) return true; - // return false; - - const FOUNDERS = [ - // "NODE_0000731", - // "NODE_0001569", // NOTE - other founder nodes are descendants of this clade - // "NODE_0000648", - // "NODE_0000038", + if (node?.branch_attrs?.labels?.clade) return true; + return false; - "NODE_0019794", - "NODE_0015887", - "NODE_0000777", - // "NODE_0001227", - // "NODE_0001571", - // "NODE_0001773", // VERY BASAL IN TREE - everything is a descendant of this node + const FOUNDERS = [ + // ZIKA: + "NODE_0000200", // 133, including below, so 40 of its own + "NODE_0000241", // 93 tips + // "NODE_0000001" ] return FOUNDERS.includes(node.name); } @@ -44,17 +37,22 @@ export function partitionIntoStreams(enabled, nodes, visibility, colorScale, abs if (!enabled) return streams; - const {founderIndiciesToDescendantFounderIndicies, founderIndiciesPostorder} = - getFounderTree(nodes[0], _isFounderNode); + const {founderIndiciesToDescendantFounderIndicies, foundersPostorder} = + getFounderTree(nodes, _isFounderNode); + // TODO XXX only one use of founderIndiciesToDescendantFounderIndicies which can be removed streams.founderIndiciesToDescendantFounderIndicies = founderIndiciesToDescendantFounderIndicies; - streams.founderIndiciesPostorder = founderIndiciesPostorder; + // streams.foundersPostorder = foundersPostorder; - streams.streams = founderIndiciesPostorder.map((founderIdx) => { + streams.streams = foundersPostorder.map((founderInfo) => { // TKTK const stream = {}; - stream.founderIdx = founderIdx; // index of the root node (not part of the stream as it's not a tip) + stream.founderIdx = founderInfo.idx; // index of the root node (not part of the stream as it's not a tip) + stream.founderVisibility = visibility[stream.founderIdx]===NODE_VISIBLE; + + stream.originatingNodeIdx = founderInfo.originatingNodeIdx; + stream.originatingStreamIdx = foundersPostorder.reduce((ret, v, i) => v.idx===founderInfo.originatingStreamFounderIdx ? i : ret, null) const nodesInStream = []; // TERMINAL NODES ONLY - const founderNode = nodes[founderIdx]; + const founderNode = nodes[founderInfo.idx]; stream.founderName = founderNode.name; const stack = [founderNode]; while (stack.length) { @@ -80,6 +78,9 @@ export function partitionIntoStreams(enabled, nodes, visibility, colorScale, abs // categories may have zero counts associated with them (over all pivots) depending on visibility settings stream.categories = observedCategories(nodesInStream, colorScale); stream.categoryColors = stream.categories.map((value) => colorScale.scale(value)) + // TODO XXX - the starting color needs to be modified if it is to match the branches! + // See calculateStrokeColors, but this would need refactoring + stream.startingColor = colorScale.scale(getTraitFromNode(nodes[founderInfo.idx], colorScale.colorBy)) const pivotData = calcPivots(nodesInStream, absoluteDateMinNumeric, absoluteDateMaxNumeric); stream.pivotIntervals = pivotData.intervals; stream.pivots = pivotData.pivots; @@ -149,6 +150,7 @@ function groupNodesIntoIntervals(nodes, intervals) { } export function countsByCategory(nodes, nodeIdxsByPivot, visibility, colorBy, categories) { + console.log("countsByCategory") return categories.map((category) => { return nodeIdxsByPivot.map((nodeIdxs) => { return nodeIdxs.filter( @@ -157,12 +159,18 @@ export function countsByCategory(nodes, nodeIdxsByPivot, visibility, colorBy, ca }) }) } + +export function streamConnectorVisibility() { + // TODO XXX - returns boolean - is the founderIdx visible ? and then render has access to this! +} + + /** * * @param {object} rootNode redux tree node * @param {function} isFounderNode */ -function getFounderTree(rootNode, isFounderNode) { +function getFounderTree(treeNodes, isFounderNode) { // Tree of nodes (in the main tree) which define stream trees const founderTree = {children: []}; const nodesInStreamFounderTree = []; @@ -179,7 +187,7 @@ function getFounderTree(rootNode, isFounderNode) { traverse(child, newNode||streamParentNode) } } - traverse(rootNode); + traverse(treeNodes[0]); // Create mapping of founder nodes (indicies) to all their descendant indicies const founderIndiciesToDescendantFounderIndicies = Object.fromEntries( @@ -197,14 +205,25 @@ function getFounderTree(rootNode, isFounderNode) { // Create a list of founder indicies in postorder order, such that we can trivially visit // the nodes without needing traversals - const founderIndiciesPostorder = []; + const foundersPostorder = [] function postorder(node) { for (const child of node.children||[]) { postorder(child); } - if (node.arrayIdx!==undefined) founderIndiciesPostorder.push(node.arrayIdx); + if (node.arrayIdx===undefined) { + return + } + foundersPostorder.push({ + idx: node.arrayIdx, + rootName: node.name, + originatingNodeIdx: treeNodes[node.arrayIdx].parent.arrayIdx, + originatingStreamFounderIdx: Object.hasOwn(node.parent, "arrayIdx") ? node.parent.arrayIdx : null, + childStreamFounders: node?.children?.map((c) => c.arrayIdx) || [], + // NOTE: no concept of child-non-streams, i.e. can't have a normal tree sprout from a stream + }) } postorder(founderTree) - return {founderTree, founderIndiciesToDescendantFounderIndicies, founderIndiciesPostorder}; + console.log({founderTree, founderIndiciesToDescendantFounderIndicies, foundersPostorder}) + return {founderTree, founderIndiciesToDescendantFounderIndicies, foundersPostorder}; } \ No newline at end of file From a0901f33e2a88254930496cf44fabde34f0e4441 Mon Sep 17 00:00:00 2001 From: james hadfield Date: Tue, 12 Nov 2024 09:57:35 +1300 Subject: [PATCH 15/22] wip - branch updates --- src/components/tree/phyloTree/change.ts | 5 +++++ src/components/tree/phyloTree/layouts.ts | 4 ++-- src/components/tree/phyloTree/renderers.ts | 18 ++++++++++++------ .../tree/reactD3Interface/callbacks.ts | 8 ++++++++ .../tree/reactD3Interface/initialRender.ts | 1 + src/util/partitionIntoStreams.js | 4 ++-- 6 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/components/tree/phyloTree/change.ts b/src/components/tree/phyloTree/change.ts index d624c9ce7..75421beb9 100644 --- a/src/components/tree/phyloTree/change.ts +++ b/src/components/tree/phyloTree/change.ts @@ -199,6 +199,11 @@ export const modifySVG = function modifySVG( }) } genericSelectAndModify(this.svg, ".stream", updateCall, transitionTime); + const connectorUpdateCall = (selection) => { + selection.attr("d", (d) => d.connectorPath); + // TODO -- update visibility? I.e. stroke width? + } + genericSelectAndModify(this.svg, ".connector", connectorUpdateCall, transitionTime); } diff --git a/src/components/tree/phyloTree/layouts.ts b/src/components/tree/phyloTree/layouts.ts index 862330c65..8f6320d2e 100644 --- a/src/components/tree/phyloTree/layouts.ts +++ b/src/components/tree/phyloTree/layouts.ts @@ -82,7 +82,7 @@ export function streamLayout(this: PhyloTreeType): void { if (!this.phyloStreams) { /* it's important we only set this up once, as DOM elements will bind to data within, so we need to mutate it */ - this.phyloStreams = this.streams.streams.map((_) => ({})) + this.phyloStreams = this.streams.streams.map((_, streamIdx) => ({streamIdx})) } // TODO XXX - need to store this internally, but do we need this.streams or should this be an arg? @@ -696,7 +696,7 @@ export function mapStreamsToScreen(streams, phyloStreams, xScale, yScale) { }) stream.connectorPath = stream.connectorFn(xScale, yScale) - console.log("stream.connectorPath", stream.connectorPath) + stream.update = true; // needed for current `change` methods to work } } diff --git a/src/components/tree/phyloTree/renderers.ts b/src/components/tree/phyloTree/renderers.ts index b3b21e245..d12408ded 100644 --- a/src/components/tree/phyloTree/renderers.ts +++ b/src/components/tree/phyloTree/renderers.ts @@ -322,7 +322,6 @@ export function drawStreams(this: PhyloTreeType): void { for (const [streamIdx, stream] of this.phyloStreams.entries()) { - console.log("rendering stream", streamIdx) /** * The element each selector gets ("d") is an element of stream.ripples, so * d is an array with length=numPivots. @@ -340,16 +339,23 @@ export function drawStreams(this: PhyloTreeType): void { } // P.S. To select an individual stream tree: this.groups.streams.select('#stream0').selectAll(`.stream`) - for (const [streamIdx, stream] of this.phyloStreams.entries()) { + + + for (const [streamIdx, phyloStream] of this.phyloStreams.entries()) { this.groups.streams.select(`#stream${streamIdx}`) + .selectAll(`.connector`) + .data([phyloStream]) + .enter() .append("path") - .attr("class", 'connector') - .attr("d", stream.connectorPath) + .attr("class", `connector`) + .attr("d", (d) => d.connectorPath) .style("stroke-width", this.streams.streams[streamIdx].founderVisibility ? 2 : 1) - .style("stroke-dasharray", "3 2") + .style("stroke-dasharray", "5 2") .style("stroke", () => this.streams.streams[streamIdx].startingColor) .style("fill", "none") - .style('visibility', 'visible'); + .style('visibility', 'visible') + .style('cursor', 'pointer') // using a dashed line doesn't play nicely with onhover/onclick behaviour :( + .on("click", this.callbacks.onStreamConnectorClick); } } diff --git a/src/components/tree/reactD3Interface/callbacks.ts b/src/components/tree/reactD3Interface/callbacks.ts index 50968c875..1fca5e7ea 100644 --- a/src/components/tree/reactD3Interface/callbacks.ts +++ b/src/components/tree/reactD3Interface/callbacks.ts @@ -102,6 +102,14 @@ export const onBranchClick = function onBranchClick(this: TreeComponent, d: Phyl this.props.dispatch(updateVisibleTipsAndBranchThicknesses({root, cladeSelected})); }; +export function onStreamConnectorClick(phyloStream) { + console.log("CLICK", phyloStream, this) + const root = [this.state.tree.streams.streams[phyloStream.streamIdx].founderIdx, undefined]; + this.props.dispatch(updateVisibleTipsAndBranchThicknesses({root, cladeSelected: undefined})); + // TODO XXX - second trees + // TODO XXX - zoom out behaviour +} + /* onBranchLeave called when mouse-off, i.e. anti-hover */ export const onBranchLeave = function onBranchLeave(this: TreeComponent, d: PhyloNode): void { diff --git a/src/components/tree/reactD3Interface/initialRender.ts b/src/components/tree/reactD3Interface/initialRender.ts index e356d227b..f0ea89283 100644 --- a/src/components/tree/reactD3Interface/initialRender.ts +++ b/src/components/tree/reactD3Interface/initialRender.ts @@ -45,6 +45,7 @@ export const renderTree = ( onTipClick: callbacks.onTipClick.bind(that), onBranchHover: callbacks.onBranchHover.bind(that), onBranchClick: callbacks.onBranchClick.bind(that), + onStreamConnectorClick: callbacks.onStreamConnectorClick.bind(that), onBranchLeave: callbacks.onBranchLeave.bind(that), onTipLeave: callbacks.onTipLeave.bind(that), tipLabel: makeTipLabelFunc(props.tipLabelKey) diff --git a/src/util/partitionIntoStreams.js b/src/util/partitionIntoStreams.js index 94100080a..4851333fc 100644 --- a/src/util/partitionIntoStreams.js +++ b/src/util/partitionIntoStreams.js @@ -5,8 +5,8 @@ import { NODE_VISIBLE } from "./globals"; // Prototype - hardcode the CA of streams function _isFounderNode(node) { - if (node?.branch_attrs?.labels?.clade) return true; - return false; + // if (node?.branch_attrs?.labels?.clade) return true; + // return false; From 1a3ec34ce58a564fd3f59ca481e36c507ae0b393 Mon Sep 17 00:00:00 2001 From: james hadfield Date: Tue, 12 Nov 2024 12:06:18 +1300 Subject: [PATCH 16/22] wip stream positions (sometimes good, sometimes not) --- src/components/tree/phyloTree/layouts.ts | 146 ++++++++---------- src/components/tree/phyloTree/renderers.ts | 2 + .../tree/reactD3Interface/callbacks.ts | 4 + src/util/partitionIntoStreams.js | 62 ++++++-- 4 files changed, 121 insertions(+), 93 deletions(-) diff --git a/src/components/tree/phyloTree/layouts.ts b/src/components/tree/phyloTree/layouts.ts index 8f6320d2e..51f9e18bf 100644 --- a/src/components/tree/phyloTree/layouts.ts +++ b/src/components/tree/phyloTree/layouts.ts @@ -76,93 +76,78 @@ export const rectangularLayout = function rectangularLayout(this: PhyloTreeType) }; -export function streamLayout(this: PhyloTreeType): void { - - const displayOrderUsed = {} +export function streamLayout(this: PhyloTreeType): void { + if (this.streams.streams.length===0) return; if (!this.phyloStreams) { /* it's important we only set this up once, as DOM elements will bind to data within, so we need to mutate it */ this.phyloStreams = this.streams.streams.map((_, streamIdx) => ({streamIdx})) } - // TODO XXX - need to store this internally, but do we need this.streams or should this be an arg? - // NOTE: this.streams is postorder (i.e. the founder nodes are postorder w.r.t other founders in the main tree) - // this.phyloStreams = this.streams.streams.map((stream) => { - const intermediates = this.phyloStreams.map((phyloStream, streamIdx) => { - - const stream = this.streams.streams[streamIdx]; - - const founderNode = this.nodes[stream.founderIdx]; - - // First get the display order range of the entire subtree of founderNode - // (This includes subclades which may themselves be streams) - function getDisplayOrder(node, top=true) { - if ((node.children || []).length) return getDisplayOrder(node.children.at(top?0:-1), top); - return node.shell.displayOrder; - } - - const getDisplayOrderExSubtrees = (node, top=true) => { - const founderIndicies = this.streams.streams.map((f) => f.founderIdx); - - const children = (node.children || []).filter((child) => !founderIndicies.includes(child.arrayIdx)); - if (children.length) return getDisplayOrderExSubtrees(children.at(top?0:-1), top); - return node.shell.displayOrder; - } - - const displayOrders = [getDisplayOrder(founderNode.n, false), getDisplayOrder(founderNode.n)] - const displayOrdersExSubtrees = [getDisplayOrderExSubtrees(founderNode.n, false), getDisplayOrderExSubtrees(founderNode.n)] - - // Store the value for other streams to query (note - this is why we iterate through streams postorder) - // displayOrderRanges[stream.founderIdx] = displayOrders; - - // Get the total display order used up by _this_ stream, taking into account the display orders - // which may be used for descendant streams (note- this is why we iterate through streams postorder) - // TODO XXX - only use of founderIndiciesToDescendantFounderIndicies - const displayOrderTotal = (displayOrders[1] - displayOrders[0]) - - this.streams.founderIndiciesToDescendantFounderIndicies[stream.founderIdx].reduce( - (acc, founderIdx) => acc + displayOrderUsed[founderIdx], - 0 - ) - - // Store the value for other streams to query - displayOrderUsed[stream.founderIdx] = displayOrderTotal; - - // scale this display order by maxNodesInInterval so the stream never exceeds the allocated range - // note that maxNodesInInterval doesn't take into account visibility settings, i.e. it's max nodes assuming everything's visible - // I think this works well for filtering, but unsure about zooming - const displayOrderScalar = displayOrderTotal / stream.maxNodesInInterval; - const baseDisplayOrder = displayOrders[0]; - - // convert countsByCategory to displayOrderByColorBy - // P.S. stream.countsByCategory[categoryIdx][pivotIdx] = count - // target: displayOrderByColorBy [categoryIdx][pivotIdx] = [displayOrder, displayOrder] - const displayOrderByCategory = stream.countsByCategory.reduce((acc, countsAcrossPivots, categoryIdx) => { - acc.push(countsAcrossPivots.map((count, pivotIdx) => { - const base = categoryIdx===0 ? baseDisplayOrder : acc[categoryIdx-1][pivotIdx][1]; - return [base, base + count*displayOrderScalar]; - })) - return acc; - }, []); - - // center the stream graphs - // const displayOrderMidpoint = displayOrders[0] + (displayOrders[1] - displayOrders[0])/2; - const displayOrderMidpoint = displayOrdersExSubtrees[0] + (displayOrdersExSubtrees[1] - displayOrdersExSubtrees[0])/2; - const nPivots = displayOrderByCategory[0].length; - for (let pivotIdx=0; pivotIdx y+=shift); + const displayOrderMidpoints = {}; + + function _getDisplayOrder(node, top=true) { + if ((node.children || []).length) return _getDisplayOrder(node.children.at(top?0:-1), top); + return node.shell.displayOrder; + } + + const _orderStreamGroup = (streamIndicies) => { // fat-arrow to preserve `this` + return streamIndicies + .map((streamIdx) => [streamIdx, this.nodes[this.streams.streams[streamIdx].founderIdx].displayOrder]) + .sort((a,b) => a[1] streamIdx) + } + + for (const streamGroup of this.streams.streamGroups) { + // streamIndicies which make up streamGroup are preorder + const founderNode = this.nodes[this.streams.streams[streamGroup[0]].founderIdx]; + const streamGroupDisplayOrderRange = [_getDisplayOrder(founderNode.n, false), _getDisplayOrder(founderNode.n)] + let floor = streamGroupDisplayOrderRange[1]; // range is [smallerNumber, biggerNumber] + const tipMultiplier = (streamGroupDisplayOrderRange[1]-streamGroupDisplayOrderRange[0]) / founderNode.n.tipCount; // includes any and all substreams + // console.log("STREAM GROUP", streamGroup, "nTips",founderNode.n.tipCount, "streamGroupDisplayOrderRange", streamGroupDisplayOrderRange, tipMultiplier) + for (const streamIdx of _orderStreamGroup(streamGroup)) { + // start at the bottom of the available range + const stream = this.streams.streams[streamIdx]; + const displayOrderTotal = stream.fullTipCountExSubstreams * tipMultiplier; + const displayOrderMidpoint = floor - displayOrderTotal/2; + // console.log("\tstreamIdx", streamIdx, stream.fullTipCountExSubstreams) + + floor -= displayOrderTotal; + // TODO XXX - this is not correct - we;re making the best use of the space available, but this means each stream is + // using a different scaling. The proper way is... hard? + const displayOrderScalar = displayOrderTotal / stream.maxNodesInInterval; + // convert countsByCategory to displayOrderByColorBy + // P.S. stream.countsByCategory[categoryIdx][pivotIdx] = count + // target: displayOrderByColorBy [categoryIdx][pivotIdx] = [displayOrder, displayOrder] + const displayOrderByCategory = stream.countsByCategory.reduce((acc, countsAcrossPivots, categoryIdx) => { + acc.push(countsAcrossPivots.map((count, pivotIdx) => { + const base = categoryIdx===0 ? 0 : acc[categoryIdx-1][pivotIdx][1]; + return [base, base + count*displayOrderScalar]; + })) + return acc; + }, []); + + // center the stream graphs + // const displayOrderMidpoint = displayOrders[0] + (displayOrders[1] - displayOrders[0])/2; + // const displayOrderMidpoint = displayOrdersExSubtrees[0] + (displayOrdersExSubtrees[1] - displayOrdersExSubtrees[0])/2; + displayOrderMidpoints[streamIdx] = displayOrderMidpoint; + const nPivots = displayOrderByCategory[0].length; + for (let pivotIdx=0; pivotIdx y+=shift); + } } - } + + // NOTE: for num_date the value is the x value. Easy. + // return {displayOrderByCategory} + this.phyloStreams[streamIdx].displayOrderByCategory = displayOrderByCategory; // this is overwritten each update cycle - is this ok? - // NOTE: for num_date the value is the x value. Easy. - // return {displayOrderByCategory} - phyloStream.displayOrderByCategory = displayOrderByCategory; // this is overwritten each update cycle - is this ok? + } + } - return {displayOrderMidpoint}; - }); /** * Second loop (once displayOrderByCategory has been calculated for all streams) to work out the connectors @@ -178,7 +163,7 @@ export function streamLayout(this: PhyloTreeType): void { for (let pivotIdx=0; pivotIdx intermediates[streamIdx].displayOrderMidpoint ? 1 : 0; + const teeIdx = treePhyloNode.displayOrder > displayOrderMidpoints[streamIdx] ? 1 : 0; treePhyloNode.displayOrderRange[teeIdx] = endDisplayOrder; phyloStream.connectorFn = function(x, y) { // scale functions return `M ${x(startXVal)} ${y(endDisplayOrder)} H ${x(endPivot)}`; @@ -199,7 +184,7 @@ export function streamLayout(this: PhyloTreeType): void { // const closestPivotIdx = parentStream.pivots.map((p) => Math.abs(p-xVal)).reduce((res, d, i) => (!res?.[0] || d { // TKTK const stream = {}; @@ -89,12 +93,30 @@ export function partitionIntoStreams(enabled, nodes, visibility, colorScale, abs // stream.numNodes = nodesInStream.length; stream.maxNodesInInterval = Math.max(...stream.nodeIdxs.map((idxs) => idxs.length)); stream.countsByCategory = countsByCategory(nodes, stream.nodeIdxs, visibility, colorScale.colorBy, stream.categories); + const descendantFounderIndicies = founderIndiciesToDescendantFounderIndicies[founderNode.arrayIdx]; + stream.fullTipCountExSubstreams = calcFullTipCountExSubstreams(nodes, stream.founderIdx, descendantFounderIndicies); return stream; }) return streams; } + +function calcFullTipCountExSubstreams(nodes, founderNodeIdx, descendantFounderIndicies) { + let fullTipCount = nodes[founderNodeIdx].fullTipCount; + const stack = [nodes[founderNodeIdx]]; + while (stack.length) { + const node = stack.pop(); + if (descendantFounderIndicies.includes(node.arrayIdx)) { + fullTipCount -= nodes[node.arrayIdx].fullTipCount; + } else { + for (const child of (node.children || [])) stack.push(child); + } + } + return fullTipCount; +} + + function observedCategories(nodes, colorScale) { const colorBy = colorScale.colorBy; const values = new Set(); @@ -223,7 +245,21 @@ function getFounderTree(treeNodes, isFounderNode) { }) } postorder(founderTree) + foundersPostorder.forEach((f, i) => {f.streamIdx = i;}); + + const streamGroups = founderTree.children.map((n) => { // n: founderTreeNode + const indicies = []; + const stack = [n]; + while (stack.length) { + const m = stack.shift(); // preorder + indicies.push(foundersPostorder.filter((d) => d.idx===m.arrayIdx)[0].streamIdx); + // indicies.push(m.arrayIdx); + for (const child of m.children) stack.unshift(child); + } + return indicies; + }) + - console.log({founderTree, founderIndiciesToDescendantFounderIndicies, foundersPostorder}) - return {founderTree, founderIndiciesToDescendantFounderIndicies, foundersPostorder}; + console.log({founderTree, founderIndiciesToDescendantFounderIndicies, foundersPostorder, streamGroups}) + return {founderTree, founderIndiciesToDescendantFounderIndicies, foundersPostorder, streamGroups}; } \ No newline at end of file From 9de41a4d6077cfe749a6b5f2aac2d8de0628b07a Mon Sep 17 00:00:00 2001 From: james hadfield Date: Tue, 12 Nov 2024 13:21:56 +1300 Subject: [PATCH 17/22] wip - improve toggle perf --- src/components/tree/phyloTree/change.ts | 29 +++++++++++++++++++ src/components/tree/phyloTree/types.ts | 5 +++- .../tree/reactD3Interface/change.ts | 8 +++++ src/components/tree/tree.tsx | 14 --------- src/util/partitionIntoStreams.js | 1 - 5 files changed, 41 insertions(+), 16 deletions(-) diff --git a/src/components/tree/phyloTree/change.ts b/src/components/tree/phyloTree/change.ts index 75421beb9..6598fb6d2 100644 --- a/src/components/tree/phyloTree/change.ts +++ b/src/components/tree/phyloTree/change.ts @@ -324,6 +324,7 @@ export const change = function change( newBranchLabellingKey = undefined, showAllBranchLabels = undefined, newTipLabelKey = undefined, + showStreams = undefined, /* arrays of data (the same length as nodes) */ branchStroke = undefined, tipStroke = undefined, @@ -406,6 +407,34 @@ export const change = function change( /* change the requested properties on the nodes */ updateNodesWithNewData(this.nodes, nodePropsToModify); + if (showStreams!==undefined) { + console.log("CHANGE::showStreams", showStreams) + this.streams = streams; + this.phyloStreams = undefined; + + if (showStreams===false) { + // turn them off! - TODO - make this a function + this.groups.streams.selectAll(".stream").remove(); + this.groups.streams.selectAll(".connector").remove(); + } else { + this.streamLayout(); // recompute displayOrder values across pivots + mapStreamsToScreen(this.streams, this.phyloStreams, this.xScale, this.yScale); // recompute pixels (unneeded for branches/tips) + this.drawStreams(); // remove & redraw + this.drawStreamConnectors(); // remove & redraw + } + // don't have good methods to remove tips etc (yet) + for (const name of ['branchLabels', 'branchTee', 'branchStem', 'tips', 'tipLabels', 'vaccines']) { + this.groups[name].selectAll("*").remove(); + } + this.drawBranches(); + this.updateTipLabels(); + this.drawTips(); + if (this.vaccines) this.drawVaccines(); + if (this.regression) this.drawRegression(); + return; + } + + // recalculate gradients here? if (changeColorBy) { this.updateColorBy(); diff --git a/src/components/tree/phyloTree/types.ts b/src/components/tree/phyloTree/types.ts index 4fa6097e7..9cedcb73a 100644 --- a/src/components/tree/phyloTree/types.ts +++ b/src/components/tree/phyloTree/types.ts @@ -26,7 +26,8 @@ export type TreeElement = ".tipLabel" | ".vaccineCross" | ".vaccineDottedLine" | - ".stream" + ".stream" | + ".connector" export interface Regression { intercept?: number @@ -218,6 +219,8 @@ export interface ChangeParams { // other data // scatterVariables?: ScatterVariables performanceFlags?: PerformanceFlags + + showStreams?: boolean streams?: any // TODO XXX } diff --git a/src/components/tree/reactD3Interface/change.ts b/src/components/tree/reactD3Interface/change.ts index b5d018a0b..8b23e1daf 100644 --- a/src/components/tree/reactD3Interface/change.ts +++ b/src/components/tree/reactD3Interface/change.ts @@ -40,6 +40,14 @@ export const changePhyloTreeViaPropsComparison = ( args.streams = newTreeRedux.streams; } + /* toggle on-off stream trees? + NOTE: currently this is all-or-nothing, but one day it'll be per-stream */ + if (oldProps.showStreamTrees !== newProps.showStreamTrees) { + console.log("prop change!", oldProps.showStreamTrees, newProps.showStreamTrees) + args.showStreams = newProps.showStreamTrees; + args.streams = newTreeRedux.streams; + } + /* visibility */ // TODO XXX - under what circumstances is this conditional true vs (`dateRangeChange || filterChange`)? if (!!newTreeRedux.visibilityVersion && oldTreeRedux.visibilityVersion !== newTreeRedux.visibilityVersion) { diff --git a/src/components/tree/tree.tsx b/src/components/tree/tree.tsx index b1eb94206..e4398230c 100644 --- a/src/components/tree/tree.tsx +++ b/src/components/tree/tree.tsx @@ -91,20 +91,6 @@ export class TreeComponent extends React.Component = {}; let rightTreeUpdated = false; - /** - * Turning on/off streams requires a lot of d3 work (certain dom elements need to be added/removed, not just updated) - * so just re-render everything. Note that this doesn't preserve zooming :( - * TODO XXX - we want to utilise the canonical changePhyloTreeViaPropsComparison approach here, obviously - */ - if (prevProps.showStreamTrees !== this.props.showStreamTrees) { - this.state.tree.clearSVG(); - newState.tree = new PhyloTree(this.props.tree.nodes, lhsTreeId, this.props.tree.idxOfInViewRootNode); - renderTree(this, true, newState.tree, this.props); - this.setState(newState); /* this will trigger an unnecessary CDU :( */ - return; - } - - /* potentially change the (main / left hand) tree */ const { newState: potentialNewState, diff --git a/src/util/partitionIntoStreams.js b/src/util/partitionIntoStreams.js index 142ada296..ea4468e68 100644 --- a/src/util/partitionIntoStreams.js +++ b/src/util/partitionIntoStreams.js @@ -172,7 +172,6 @@ function groupNodesIntoIntervals(nodes, intervals) { } export function countsByCategory(nodes, nodeIdxsByPivot, visibility, colorBy, categories) { - console.log("countsByCategory") return categories.map((category) => { return nodeIdxsByPivot.map((nodeIdxs) => { return nodeIdxs.filter( From 8f86cb4e3f2387a71b62ed58761e610a69a087d2 Mon Sep 17 00:00:00 2001 From: james hadfield Date: Wed, 13 Nov 2024 15:01:11 +1300 Subject: [PATCH 18/22] WIP select branch label --- src/actions/colors.js | 2 +- src/actions/recomputeReduxState.js | 12 +++- src/actions/streamTrees.js | 21 ++++-- src/actions/types.js | 1 + src/components/controls/choose-metric.js | 23 +++--- .../choose-stream-tree-branch-label.js | 71 +++++++++++++++++++ src/components/controls/controls.tsx | 2 + src/components/tree/index.ts | 1 + src/components/tree/phyloTree/change.ts | 22 +++--- src/components/tree/phyloTree/types.ts | 1 + .../tree/reactD3Interface/change.ts | 4 +- src/components/tree/types.ts | 2 + src/reducers/controls.ts | 8 ++- src/reducers/tree/index.ts | 3 +- src/util/partitionIntoStreams.js | 37 ++-------- 15 files changed, 151 insertions(+), 59 deletions(-) create mode 100644 src/components/controls/choose-stream-tree-branch-label.js diff --git a/src/actions/colors.js b/src/actions/colors.js index 2eb924c95..3f4b9c511 100644 --- a/src/actions/colors.js +++ b/src/actions/colors.js @@ -29,7 +29,7 @@ export const changeColorBy = (providedColorBy = undefined) => { dispatch(changeEntropyCdsSelection(colorBy)); // Recompute streams - const streams = partitionIntoStreams(controls.showStreamTrees, tree.nodes, tree.visibility, colorScale, controls.absoluteDateMinNumeric, controls.absoluteDateMaxNumeric) + const streams = partitionIntoStreams(controls.showStreamTrees, controls.streamTreeBranchLabel, tree.nodes, tree.visibility, colorScale, controls.absoluteDateMinNumeric, controls.absoluteDateMaxNumeric) dispatch({ diff --git a/src/actions/recomputeReduxState.js b/src/actions/recomputeReduxState.js index 001801558..0fc0b980f 100644 --- a/src/actions/recomputeReduxState.js +++ b/src/actions/recomputeReduxState.js @@ -24,6 +24,8 @@ import { hasMultipleGridPanels } from "./panelDisplay"; import { strainSymbolUrlString } from "../middleware/changeURL"; import { combineMeasurementsControlsAndQuery, loadMeasurements } from "./measurements"; import { partitionIntoStreams } from "../util/partitionIntoStreams.js"; +import { canShowStreamTrees, branchLabelsForStreamTrees } from "../components/controls/choose-stream-tree-branch-label"; + export const doesColorByHaveConfidence = (controlsState, colorBy) => controlsState.coloringsPresentOnTreeWithConfidence.has(colorBy); @@ -995,7 +997,15 @@ export const createStateFromQueryOrJSONs = ({ }); /* STREAMS */ - tree.streams = partitionIntoStreams(controls.showStreamTrees, tree.nodes, tree.visibility, controls.colorScale, controls.absoluteDateMinNumeric, controls.absoluteDateMaxNumeric) + const streamBranchLabels = branchLabelsForStreamTrees(tree.availableBranchLabels); + if (canShowStreamTrees(streamBranchLabels)) { + /* TMP: override default state for testing purposes */ + controls.showStreamTrees = true; // toggle on to start with + controls.streamTreeBranchLabel = streamBranchLabels.includes('stream') ? 'stream' : + streamBranchLabels.includes('clade') ? 'clade' : + 'none'; + } + tree.streams = partitionIntoStreams(controls.showStreamTrees, controls.streamTreeBranchLabel, tree.nodes, tree.visibility, controls.colorScale, controls.absoluteDateMinNumeric, controls.absoluteDateMaxNumeric) console.log("tree.streams", tree.streams) /* calculate entropy in view */ diff --git a/src/actions/streamTrees.js b/src/actions/streamTrees.js index 1c4b86333..f16ef603d 100644 --- a/src/actions/streamTrees.js +++ b/src/actions/streamTrees.js @@ -1,13 +1,26 @@ -import { TOGGLE_STREAM_TREE } from "./types"; +import { TOGGLE_STREAM_TREE, CHANGE_STREAM_TREE_BRANCH_LABEL } from "./types"; import { partitionIntoStreams } from "../util/partitionIntoStreams"; export function toggleStreamTree() { return function(dispatch, getState) { const {controls, tree} = getState(); const showStreamTrees = !controls.showStreamTrees; - const streams = partitionIntoStreams(showStreamTrees, tree.nodes, tree.visibility, controls.colorScale, controls.absoluteDateMinNumeric, controls.absoluteDateMaxNumeric) - console.log("THUNK::New streams structure:", streams) + const streams = partitionIntoStreams(showStreamTrees, controls.streamTreeBranchLabel, tree.nodes, tree.visibility, controls.colorScale, controls.absoluteDateMinNumeric, controls.absoluteDateMaxNumeric) dispatch({type: TOGGLE_STREAM_TREE, showStreamTrees, streams}) } -} \ No newline at end of file +} + +export function changeStreamTreeBranchLabel(newLabel) { + return function(dispatch, getState) { + const {controls, tree} = getState(); + const showStreamTrees = newLabel!=='none'; + const streams = partitionIntoStreams(showStreamTrees, newLabel, tree.nodes, tree.visibility, controls.colorScale, controls.absoluteDateMinNumeric, controls.absoluteDateMaxNumeric) + dispatch({ + type: CHANGE_STREAM_TREE_BRANCH_LABEL, + streams, + showStreamTrees, + streamTreeBranchLabel: newLabel + }) + } +} diff --git a/src/actions/types.js b/src/actions/types.js index 5a3e3bc07..a4dfb5188 100644 --- a/src/actions/types.js +++ b/src/actions/types.js @@ -63,3 +63,4 @@ export const TOGGLE_MOBILE_DISPLAY = "TOGGLE_MOBILE_DISPLAY"; export const SELECT_NODE = "SELECT_NODE"; export const DESELECT_NODE = "DESELECT_NODE"; export const TOGGLE_STREAM_TREE = "TOGGLE_STREAM_TREE"; +export const CHANGE_STREAM_TREE_BRANCH_LABEL = "CHANGE_STREAM_TREE_BRANCH_LABEL"; \ No newline at end of file diff --git a/src/components/controls/choose-metric.js b/src/components/controls/choose-metric.js index 5543e92fb..d51c27e92 100644 --- a/src/components/controls/choose-metric.js +++ b/src/components/controls/choose-metric.js @@ -7,6 +7,7 @@ import { toggleTemporalConfidence } from "../../actions/tree"; import { toggleStreamTree } from "../../actions/streamTrees"; import { SidebarSubtitle, SidebarButton } from "./styles"; import Toggle from "./toggle"; +import { canShowStreamTrees, branchLabelsForStreamTrees } from "./choose-stream-tree-branch-label"; /* implements a pair of buttons that toggle between time & divergence tree layouts */ @connect((state) => { @@ -17,6 +18,8 @@ import Toggle from "./toggle"; branchLengthsToDisplay: state.controls.branchLengthsToDisplay, temporalConfidence: state.controls.temporalConfidence, showStreamTrees: state.controls.showStreamTrees, + availableBranchLabels: state.tree.availableBranchLabels, + streamTreeBranchLabel: state.controls.streamTreeBranchLabel, }; }) class ChooseMetric extends React.Component { @@ -65,15 +68,17 @@ class ChooseMetric extends React.Component { ) } -
- this.props.dispatch(toggleStreamTree())} - label={t("sidebar:Show stream trees")} - /> -
+ { canShowStreamTrees(branchLabelsForStreamTrees(this.props.availableBranchLabels)) && this.props.streamTreeBranchLabel!=='none' && +
+ this.props.dispatch(toggleStreamTree())} + label={t("sidebar:Show stream trees")} + /> +
+ } ); } diff --git a/src/components/controls/choose-stream-tree-branch-label.js b/src/components/controls/choose-stream-tree-branch-label.js new file mode 100644 index 000000000..8c017208d --- /dev/null +++ b/src/components/controls/choose-stream-tree-branch-label.js @@ -0,0 +1,71 @@ +import React from "react"; +import { connect } from "react-redux"; +import { withTranslation } from 'react-i18next'; +import { FaInfoCircle } from "react-icons/fa"; +import { ImLab } from "react-icons/im"; +import { SidebarSubtitleFlex, StyledTooltip, SidebarIconContainer } from "./styles"; +import { controlsWidth } from "../../util/globals"; +import CustomSelect from "./customSelect"; +import { changeStreamTreeBranchLabel } from "../../actions/streamTrees"; + + +export function branchLabelsForStreamTrees(availableBranchLabels) { + // TODO - support 2nd tree + return availableBranchLabels.filter((l) => l!=='aa'); +} +export function canShowStreamTrees(availableBranchLabels) { + return branchLabelsForStreamTrees(availableBranchLabels).filter((x)=>x!=='none').length!==0 +} + +@connect((state) => ({ + selected: state.controls.streamTreeBranchLabel, + available: branchLabelsForStreamTrees(state.tree.availableBranchLabels), +})) +class ChooseBranchLabellingForStreamTrees extends React.Component { + constructor(props) { + super(props); + this.change = (value) => { + this.props.dispatch(changeStreamTreeBranchLabel(value.value)) + }; + } + render() { + if (!canShowStreamTrees(this.props.available)) return null; + const { t } = this.props; + const selectOptions = this.props.available.map((x) => ({value: x, label: x})); + return ( +
+ + + + {t("sidebar:Branch Label for Stream Trees")} + + {!this.props.mobileDisplay && ( + <> + + + + + <> + Very experimental! + + + + )} + +
+ value === this.props.selected)} + options={selectOptions} + isClearable={false} + isSearchable={false} + isMulti={false} + onChange={this.change} + /> +
+
+ ) + } +} + +const WithTranslation = withTranslation()(ChooseBranchLabellingForStreamTrees); +export default WithTranslation; diff --git a/src/components/controls/controls.tsx b/src/components/controls/controls.tsx index 74e05788d..ab1f1c181 100644 --- a/src/components/controls/controls.tsx +++ b/src/components/controls/controls.tsx @@ -29,6 +29,7 @@ import {TreeInfo, MapInfo, AnimationOptionsInfo, PanelLayoutInfo, import { ControlHeader } from "./controlHeader"; import MeasurementsOptions from "./measurementsOptions"; import { RootState } from "../../store"; +import ChooseBranchLabellingForStreamTrees from "./choose-stream-tree-branch-label"; function Controls() { const { t } = useTranslation(); @@ -68,6 +69,7 @@ function Controls() { + diff --git a/src/components/tree/index.ts b/src/components/tree/index.ts index a9b9acc1a..b5daf409f 100644 --- a/src/components/tree/index.ts +++ b/src/components/tree/index.ts @@ -22,6 +22,7 @@ const mapStateToProps: MapStateToProps { tipLabelKey: defaults.tipLabelKey, showTreeToo: false, showTangle: false, - showStreamTrees: true, // TODO XXX. We also need some concept of "canShowStreamTrees" + showStreamTrees: false, + streamTreeBranchLabel: 'none', zoomMin: undefined, zoomMax: undefined, branchLengthsToDisplay: "divAndDate", @@ -465,7 +467,11 @@ const Controls = (state: ControlsState = getDefaultControlsState(), action): Con case types.TOGGLE_LEGEND: return Object.assign({}, state, { legendOpen: action.value }); case types.TOGGLE_STREAM_TREE: + console.log("Controls reducer. Show?", action.showStreamTrees, "Label (unchanged):", state.streamTreeBranchLabel); return {...state, showStreamTrees: action.showStreamTrees}; + case types.CHANGE_STREAM_TREE_BRANCH_LABEL: + console.log("Controls reducer. Show?", action.showStreamTrees, "Label", action.streamTreeBranchLabel); + return {...state, showStreamTrees: action.showStreamTrees, streamTreeBranchLabel: action.streamTreeBranchLabel}; case types.ADD_EXTRA_METADATA: { for (const colorBy of Object.keys(action.newColorings)) { state.coloringsPresentOnTree.add(colorBy); diff --git a/src/reducers/tree/index.ts b/src/reducers/tree/index.ts index 0032914a5..a24735ff9 100644 --- a/src/reducers/tree/index.ts +++ b/src/reducers/tree/index.ts @@ -80,7 +80,8 @@ const Tree = ( nodeColorsVersion: action.version, streams: action.streams, // replace entire structure }; - case types.TOGGLE_STREAM_TREE: + case types.TOGGLE_STREAM_TREE: /* fallthrough */ + case types.CHANGE_STREAM_TREE_BRANCH_LABEL: return {...state, streams: action.streams}; case types.TREE_TOO_DATA: return action.tree; diff --git a/src/util/partitionIntoStreams.js b/src/util/partitionIntoStreams.js index ea4468e68..0942d587b 100644 --- a/src/util/partitionIntoStreams.js +++ b/src/util/partitionIntoStreams.js @@ -1,33 +1,5 @@ import { getTraitFromNode } from "./treeMiscHelpers" import { NODE_VISIBLE } from "./globals"; -import { sum } from "d3-array"; - - -// Prototype - hardcode the CA of streams -function _isFounderNode(node) { - - if (node?.branch_attrs?.labels?.clade) return true; - return false; - - - const FOUNDERS = [ - // ZIKA: - // "NODE_0000200", // 133, including below, so 40 of its own - // "NODE_0000241", // 93 tips - // "NODE_0000001" - - - // FLU: - "NODE_0001834", - "NODE_0001220", // 1220 - "NODE_0001102", // 80 - "NODE_0000729", // 729, less 154 = 575 - "NODE_0001261", // 154 - ] - return FOUNDERS.includes(node.name); -} - - /** * CAVEATS: @@ -35,7 +7,7 @@ function _isFounderNode(node) { * - only works for categorical colorScal`e * - only works for temporal tree */ -export function partitionIntoStreams(enabled, nodes, visibility, colorScale, absoluteDateMinNumeric, absoluteDateMaxNumeric) { +export function partitionIntoStreams(enabled, branchLabel, nodes, visibility, colorScale, absoluteDateMinNumeric, absoluteDateMaxNumeric) { const streams = { streams: [], @@ -45,7 +17,7 @@ export function partitionIntoStreams(enabled, nodes, visibility, colorScale, abs if (!enabled) return streams; const {founderIndiciesToDescendantFounderIndicies, foundersPostorder, streamGroups} = - getFounderTree(nodes, _isFounderNode); + getFounderTree(nodes, branchLabel); streams.streamGroups = streamGroups; streams.streams = foundersPostorder.map((founderInfo) => { // TKTK @@ -172,6 +144,7 @@ function groupNodesIntoIntervals(nodes, intervals) { } export function countsByCategory(nodes, nodeIdxsByPivot, visibility, colorBy, categories) { + console.log("countsByCategory") return categories.map((category) => { return nodeIdxsByPivot.map((nodeIdxs) => { return nodeIdxs.filter( @@ -191,14 +164,14 @@ export function streamConnectorVisibility() { * @param {object} rootNode redux tree node * @param {function} isFounderNode */ -function getFounderTree(treeNodes, isFounderNode) { +function getFounderTree(treeNodes, branchLabel) { // Tree of nodes (in the main tree) which define stream trees const founderTree = {children: []}; const nodesInStreamFounderTree = []; function traverse(node, streamParentNode=founderTree) { let newNode; - if (isFounderNode(node)) { + if (node?.branch_attrs?.labels?.[branchLabel]) { // add this as a child to the appropriate parent not in streamFounderTree newNode = {children: [], parent: streamParentNode, arrayIdx:node.arrayIdx, name: node.name} streamParentNode.children.push(newNode) From e143ca7d3555cd41029e72effadf3287dc0896f2 Mon Sep 17 00:00:00 2001 From: james hadfield Date: Thu, 14 Nov 2024 10:09:19 +1300 Subject: [PATCH 19/22] wip - unhandled internal-nodes-only stream --- src/util/partitionIntoStreams.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/util/partitionIntoStreams.js b/src/util/partitionIntoStreams.js index 0942d587b..1559f6f05 100644 --- a/src/util/partitionIntoStreams.js +++ b/src/util/partitionIntoStreams.js @@ -51,6 +51,9 @@ export function partitionIntoStreams(enabled, branchLabel, nodes, visibility, co stack.push(child) } } + if (nodesInStream.length===0) { + throw new Error("Stream constructed without any terminal tips. This is currently an unhandled error.") + } // categories may have zero counts associated with them (over all pivots) depending on visibility settings stream.categories = observedCategories(nodesInStream, colorScale); stream.categoryColors = stream.categories.map((value) => colorScale.scale(value)) From f24a01ad8ebe19a827f444af53970d0782a34b74 Mon Sep 17 00:00:00 2001 From: james hadfield Date: Thu, 14 Nov 2024 10:47:41 +1300 Subject: [PATCH 20/22] wip fix stream y-positions --- src/components/tree/phyloTree/layouts.ts | 2 +- src/util/partitionIntoStreams.js | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/tree/phyloTree/layouts.ts b/src/components/tree/phyloTree/layouts.ts index 51f9e18bf..7b87bb3ac 100644 --- a/src/components/tree/phyloTree/layouts.ts +++ b/src/components/tree/phyloTree/layouts.ts @@ -103,7 +103,7 @@ export function streamLayout(this: PhyloTreeType): void { const founderNode = this.nodes[this.streams.streams[streamGroup[0]].founderIdx]; const streamGroupDisplayOrderRange = [_getDisplayOrder(founderNode.n, false), _getDisplayOrder(founderNode.n)] let floor = streamGroupDisplayOrderRange[1]; // range is [smallerNumber, biggerNumber] - const tipMultiplier = (streamGroupDisplayOrderRange[1]-streamGroupDisplayOrderRange[0]) / founderNode.n.tipCount; // includes any and all substreams + const tipMultiplier = (streamGroupDisplayOrderRange[1]-streamGroupDisplayOrderRange[0]) / founderNode.n.fullTipCount; // includes any and all substreams // console.log("STREAM GROUP", streamGroup, "nTips",founderNode.n.tipCount, "streamGroupDisplayOrderRange", streamGroupDisplayOrderRange, tipMultiplier) for (const streamIdx of _orderStreamGroup(streamGroup)) { // start at the bottom of the available range diff --git a/src/util/partitionIntoStreams.js b/src/util/partitionIntoStreams.js index 1559f6f05..fc972c589 100644 --- a/src/util/partitionIntoStreams.js +++ b/src/util/partitionIntoStreams.js @@ -24,6 +24,7 @@ export function partitionIntoStreams(enabled, branchLabel, nodes, visibility, co const stream = {}; stream.founderIdx = founderInfo.idx; // index of the root node (not part of the stream as it's not a tip) stream.founderVisibility = visibility[stream.founderIdx]===NODE_VISIBLE; + stream.branchLabel = founderInfo.branchLabel; stream.originatingNodeIdx = founderInfo.originatingNodeIdx; stream.originatingStreamIdx = foundersPostorder.reduce((ret, v, i) => v.idx===founderInfo.originatingStreamFounderIdx ? i : ret, null) @@ -147,7 +148,6 @@ function groupNodesIntoIntervals(nodes, intervals) { } export function countsByCategory(nodes, nodeIdxsByPivot, visibility, colorBy, categories) { - console.log("countsByCategory") return categories.map((category) => { return nodeIdxsByPivot.map((nodeIdxs) => { return nodeIdxs.filter( @@ -176,7 +176,7 @@ function getFounderTree(treeNodes, branchLabel) { let newNode; if (node?.branch_attrs?.labels?.[branchLabel]) { // add this as a child to the appropriate parent not in streamFounderTree - newNode = {children: [], parent: streamParentNode, arrayIdx:node.arrayIdx, name: node.name} + newNode = {children: [], parent: streamParentNode, arrayIdx:node.arrayIdx, name: node.name, branchLabel: node.branch_attrs.labels[branchLabel]} streamParentNode.children.push(newNode) nodesInStreamFounderTree.push(newNode) } @@ -213,6 +213,7 @@ function getFounderTree(treeNodes, branchLabel) { foundersPostorder.push({ idx: node.arrayIdx, rootName: node.name, + branchLabel: node.branchLabel, originatingNodeIdx: treeNodes[node.arrayIdx].parent.arrayIdx, originatingStreamFounderIdx: Object.hasOwn(node.parent, "arrayIdx") ? node.parent.arrayIdx : null, childStreamFounders: node?.children?.map((c) => c.arrayIdx) || [], From 42c111e01eb0f0c907850184ea8c9adb4836172c Mon Sep 17 00:00:00 2001 From: james hadfield Date: Thu, 14 Nov 2024 12:27:43 +1300 Subject: [PATCH 21/22] wip - handle metric changes --- src/actions/colors.js | 2 +- src/actions/recomputeReduxState.js | 2 +- src/actions/streamTrees.js | 14 ++++++-- src/components/controls/choose-metric.js | 6 ++-- src/components/tree/phyloTree/change.ts | 2 ++ src/components/tree/phyloTree/layouts.ts | 4 ++- .../tree/reactD3Interface/change.ts | 1 + src/reducers/tree/index.ts | 5 +++ src/util/partitionIntoStreams.js | 32 +++++++++++++------ 9 files changed, 50 insertions(+), 18 deletions(-) diff --git a/src/actions/colors.js b/src/actions/colors.js index 3f4b9c511..89ad85f5e 100644 --- a/src/actions/colors.js +++ b/src/actions/colors.js @@ -29,7 +29,7 @@ export const changeColorBy = (providedColorBy = undefined) => { dispatch(changeEntropyCdsSelection(colorBy)); // Recompute streams - const streams = partitionIntoStreams(controls.showStreamTrees, controls.streamTreeBranchLabel, tree.nodes, tree.visibility, colorScale, controls.absoluteDateMinNumeric, controls.absoluteDateMaxNumeric) + const streams = partitionIntoStreams(controls.showStreamTrees, controls.streamTreeBranchLabel, tree.nodes, tree.visibility, colorScale, controls.absoluteDateMinNumeric, controls.absoluteDateMaxNumeric, controls.distanceMeasure) dispatch({ diff --git a/src/actions/recomputeReduxState.js b/src/actions/recomputeReduxState.js index 0fc0b980f..c15017f1a 100644 --- a/src/actions/recomputeReduxState.js +++ b/src/actions/recomputeReduxState.js @@ -1005,7 +1005,7 @@ export const createStateFromQueryOrJSONs = ({ streamBranchLabels.includes('clade') ? 'clade' : 'none'; } - tree.streams = partitionIntoStreams(controls.showStreamTrees, controls.streamTreeBranchLabel, tree.nodes, tree.visibility, controls.colorScale, controls.absoluteDateMinNumeric, controls.absoluteDateMaxNumeric) + tree.streams = partitionIntoStreams(controls.showStreamTrees, controls.streamTreeBranchLabel, tree.nodes, tree.visibility, controls.colorScale, controls.absoluteDateMinNumeric, controls.absoluteDateMaxNumeric, controls.distanceMeasure) console.log("tree.streams", tree.streams) /* calculate entropy in view */ diff --git a/src/actions/streamTrees.js b/src/actions/streamTrees.js index f16ef603d..d893164a7 100644 --- a/src/actions/streamTrees.js +++ b/src/actions/streamTrees.js @@ -1,12 +1,12 @@ -import { TOGGLE_STREAM_TREE, CHANGE_STREAM_TREE_BRANCH_LABEL } from "./types"; +import { TOGGLE_STREAM_TREE, CHANGE_STREAM_TREE_BRANCH_LABEL, CHANGE_DISTANCE_MEASURE } from "./types"; import { partitionIntoStreams } from "../util/partitionIntoStreams"; export function toggleStreamTree() { return function(dispatch, getState) { const {controls, tree} = getState(); const showStreamTrees = !controls.showStreamTrees; - const streams = partitionIntoStreams(showStreamTrees, controls.streamTreeBranchLabel, tree.nodes, tree.visibility, controls.colorScale, controls.absoluteDateMinNumeric, controls.absoluteDateMaxNumeric) + const streams = partitionIntoStreams(showStreamTrees, controls.streamTreeBranchLabel, tree.nodes, tree.visibility, controls.colorScale, controls.absoluteDateMinNumeric, controls.absoluteDateMaxNumeric, controls.distanceMeasure) dispatch({type: TOGGLE_STREAM_TREE, showStreamTrees, streams}) } } @@ -15,7 +15,7 @@ export function changeStreamTreeBranchLabel(newLabel) { return function(dispatch, getState) { const {controls, tree} = getState(); const showStreamTrees = newLabel!=='none'; - const streams = partitionIntoStreams(showStreamTrees, newLabel, tree.nodes, tree.visibility, controls.colorScale, controls.absoluteDateMinNumeric, controls.absoluteDateMaxNumeric) + const streams = partitionIntoStreams(showStreamTrees, newLabel, tree.nodes, tree.visibility, controls.colorScale, controls.absoluteDateMinNumeric, controls.absoluteDateMaxNumeric, controls.distanceMeasure) dispatch({ type: CHANGE_STREAM_TREE_BRANCH_LABEL, streams, @@ -24,3 +24,11 @@ export function changeStreamTreeBranchLabel(newLabel) { }) } } + +export function changeDistanceMeasure(metric) { + return function(dispatch, getState) { + const {controls, tree} = getState(); + const streams = partitionIntoStreams(controls.showStreamTrees, controls.streamTreeBranchLabel, tree.nodes, tree.visibility, controls.colorScale, controls.absoluteDateMinNumeric, controls.absoluteDateMaxNumeric, metric) + dispatch({type: CHANGE_DISTANCE_MEASURE, data: metric, streams}) + } +} \ No newline at end of file diff --git a/src/components/controls/choose-metric.js b/src/components/controls/choose-metric.js index d51c27e92..f9939cb5a 100644 --- a/src/components/controls/choose-metric.js +++ b/src/components/controls/choose-metric.js @@ -4,7 +4,7 @@ import { withTranslation } from "react-i18next"; import { CHANGE_DISTANCE_MEASURE } from "../../actions/types"; import { analyticsControlsEvent } from "../../util/googleAnalytics"; import { toggleTemporalConfidence } from "../../actions/tree"; -import { toggleStreamTree } from "../../actions/streamTrees"; +import { toggleStreamTree, changeDistanceMeasure } from "../../actions/streamTrees"; import { SidebarSubtitle, SidebarButton } from "./styles"; import Toggle from "./toggle"; import { canShowStreamTrees, branchLabelsForStreamTrees } from "./choose-stream-tree-branch-label"; @@ -39,7 +39,7 @@ class ChooseMetric extends React.Component { selected={this.props.distanceMeasure === "num_date"} onClick={() => { analyticsControlsEvent("tree-metric-temporal"); - this.props.dispatch({ type: CHANGE_DISTANCE_MEASURE, data: "num_date" }); + this.props.dispatch(changeDistanceMeasure('num_date')) }} > {t("sidebar:time")} @@ -49,7 +49,7 @@ class ChooseMetric extends React.Component { selected={this.props.distanceMeasure === "div"} onClick={() => { analyticsControlsEvent("tree-metric-temporal"); - this.props.dispatch({ type: CHANGE_DISTANCE_MEASURE, data: "div" }); + this.props.dispatch(changeDistanceMeasure('div')) }} > {t("sidebar:divergence")} diff --git a/src/components/tree/phyloTree/change.ts b/src/components/tree/phyloTree/change.ts index f731754a3..20667023d 100644 --- a/src/components/tree/phyloTree/change.ts +++ b/src/components/tree/phyloTree/change.ts @@ -355,6 +355,8 @@ export const change = function change( transitionTime = 0; } + if (streams) this.streams = streams; + /* the logic of converting what react is telling us to change and what SVG elements, node properties, svg props we actually change */ if (changeColorBy) { diff --git a/src/components/tree/phyloTree/layouts.ts b/src/components/tree/phyloTree/layouts.ts index 7b87bb3ac..8265211f1 100644 --- a/src/components/tree/phyloTree/layouts.ts +++ b/src/components/tree/phyloTree/layouts.ts @@ -169,7 +169,9 @@ export function streamLayout(this: PhyloTreeType): void { break } } - const startXVal = getTraitFromNode(this.nodes[stream.originatingNodeIdx].n, "num_date"); + const startXVal = this.distance==='num_date' ? + getTraitFromNode(this.nodes[stream.originatingNodeIdx].n, 'num_date') : + getDivFromNode(this.nodes[stream.originatingNodeIdx].n); // connector start if parent not a stream if (stream.originatingStreamIdx===null) { // Increase the tee length of the parent node (a "normal" branch in the tree) so it matches the y-position of the (branch to the) stream diff --git a/src/components/tree/reactD3Interface/change.ts b/src/components/tree/reactD3Interface/change.ts index 50d7c7b7e..be7c179fc 100644 --- a/src/components/tree/reactD3Interface/change.ts +++ b/src/components/tree/reactD3Interface/change.ts @@ -71,6 +71,7 @@ export const changePhyloTreeViaPropsComparison = ( /* change from timetree to divergence tree */ if (oldProps.distanceMeasure !== newProps.distanceMeasure) { args.newDistance = newProps.distanceMeasure; + args.streams = newTreeRedux.streams; } /* explode! */ diff --git a/src/reducers/tree/index.ts b/src/reducers/tree/index.ts index a24735ff9..6f2fe67c1 100644 --- a/src/reducers/tree/index.ts +++ b/src/reducers/tree/index.ts @@ -67,6 +67,11 @@ const Tree = ( return Object.assign({}, state, newStates); } + case types.CHANGE_DISTANCE_MEASURE: + if (action.streams) { + return {...state, streams: action.streams}; + } + return state; case types.UPDATE_TIP_RADII: return { ...state, diff --git a/src/util/partitionIntoStreams.js b/src/util/partitionIntoStreams.js index fc972c589..579bd2688 100644 --- a/src/util/partitionIntoStreams.js +++ b/src/util/partitionIntoStreams.js @@ -1,4 +1,4 @@ -import { getTraitFromNode } from "./treeMiscHelpers" +import { getTraitFromNode, getDivFromNode } from "./treeMiscHelpers" import { NODE_VISIBLE } from "./globals"; /** @@ -7,7 +7,7 @@ import { NODE_VISIBLE } from "./globals"; * - only works for categorical colorScal`e * - only works for temporal tree */ -export function partitionIntoStreams(enabled, branchLabel, nodes, visibility, colorScale, absoluteDateMinNumeric, absoluteDateMaxNumeric) { +export function partitionIntoStreams(enabled, branchLabel, nodes, visibility, colorScale, absoluteDateMinNumeric, absoluteDateMaxNumeric, metric) { const streams = { streams: [], @@ -61,11 +61,11 @@ export function partitionIntoStreams(enabled, branchLabel, nodes, visibility, co // TODO XXX - the starting color needs to be modified if it is to match the branches! // See calculateStrokeColors, but this would need refactoring stream.startingColor = colorScale.scale(getTraitFromNode(nodes[founderInfo.idx], colorScale.colorBy)) - const pivotData = calcPivots(nodesInStream, absoluteDateMinNumeric, absoluteDateMaxNumeric); + const pivotData = calcPivots(metric, nodesInStream, absoluteDateMinNumeric, absoluteDateMaxNumeric); stream.pivotIntervals = pivotData.intervals; stream.pivots = pivotData.pivots; // nodeIdxs are all nodes, visible and not visible - stream.nodeIdxs = groupNodesIntoIntervals(nodesInStream, pivotData.intervals); // indexed by pivot idx + stream.nodeIdxs = groupNodesIntoIntervals(nodesInStream, pivotData.intervals, metric); // indexed by pivot idx // stream.numNodes = nodesInStream.length; stream.maxNodesInInterval = Math.max(...stream.nodeIdxs.map((idxs) => idxs.length)); stream.countsByCategory = countsByCategory(nodes, stream.nodeIdxs, visibility, colorScale.colorBy, stream.categories); @@ -105,9 +105,13 @@ function observedCategories(nodes, colorScale) { return Array.from(values).sort((a,b) => colorScale.legendValues.indexOf(a) - colorScale.legendValues.indexOf(b)) } -function calcPivots(nodes, absoluteDateMinNumeric, absoluteDateMaxNumeric) { - const domain = nodes.reduce((acc, node) => { - const value = getTraitFromNode(node, "num_date"); // TODO XXX +function calcPivots(metric, nodes, absoluteDateMinNumeric, absoluteDateMaxNumeric) { + /** + * TODO XXX - pivot number always calculated using num_date - this helps ensure the number of pivots + * doesn't change when we change the metric. Obviously needs to be fixed. + */ + let domain = nodes.reduce((acc, node) => { + const value = getTraitFromNode(node, 'num_date'); if (acc[0] > value) acc[0] = value; if (acc[1] < value) acc[1] = value; return acc; @@ -116,6 +120,16 @@ function calcPivots(nodes, absoluteDateMinNumeric, absoluteDateMaxNumeric) { const domainFraction = (domain[1]-domain[0]) / (absoluteDateMaxNumeric - absoluteDateMinNumeric); const availablePivots = 50; const nPivots = Math.ceil(domainFraction * availablePivots); + + if (metric==='div') { + domain = nodes.reduce((acc, node) => { + const value = getDivFromNode(node); + if (acc[0] > value) acc[0] = value; + if (acc[1] < value) acc[1] = value; + return acc; + }, [Infinity, -Infinity]) + } + const size = (domain[1]-domain[0])/(nPivots-1); const intervals = Array.from(Array(nPivots), undefined); intervals[0] = [domain[0], domain[0] + size/2]; @@ -129,11 +143,11 @@ function calcPivots(nodes, absoluteDateMinNumeric, absoluteDateMaxNumeric) { } -function groupNodesIntoIntervals(nodes, intervals) { +function groupNodesIntoIntervals(nodes, intervals, metric) { const groups = Array.from(Array(intervals.length), () => []) // TODO XXX this is very crude for (const node of nodes) { - const value = getTraitFromNode(node, "num_date"); // TODO XXX + const value = metric==='num_date' ? getTraitFromNode(node, "num_date") : getDivFromNode(node); for (let i =0; iintervals[i][0] && value<=intervals[i][1]) { // TODO - which side is open, which is closed? // TODO XXX - I use arrayIdx not the node itself as adding references to nodes like this From acbc70cbf6b2ce4b370ed4e49edbc36734cf2df6 Mon Sep 17 00:00:00 2001 From: james hadfield Date: Thu, 14 Nov 2024 16:47:24 +1300 Subject: [PATCH 22/22] appease tsc / eslint --- src/actions/recomputeReduxState.js | 3 ++- src/components/controls/choose-metric.js | 1 - src/components/tree/phyloTree/change.ts | 2 -- src/components/tree/phyloTree/layouts.ts | 6 +++--- src/components/tree/phyloTree/phyloTree.ts | 1 - src/components/tree/phyloTree/renderers.ts | 8 +------- src/components/tree/phyloTree/types.ts | 2 ++ src/components/tree/reactD3Interface/callbacks.ts | 5 ++--- src/reducers/controls.ts | 2 -- src/util/partitionIntoStreams.js | 2 +- tsconfig.json | 1 + 11 files changed, 12 insertions(+), 21 deletions(-) diff --git a/src/actions/recomputeReduxState.js b/src/actions/recomputeReduxState.js index c15017f1a..3461aa766 100644 --- a/src/actions/recomputeReduxState.js +++ b/src/actions/recomputeReduxState.js @@ -1006,7 +1006,8 @@ export const createStateFromQueryOrJSONs = ({ 'none'; } tree.streams = partitionIntoStreams(controls.showStreamTrees, controls.streamTreeBranchLabel, tree.nodes, tree.visibility, controls.colorScale, controls.absoluteDateMinNumeric, controls.absoluteDateMaxNumeric, controls.distanceMeasure) - console.log("tree.streams", tree.streams) + // eslint-disable-next-line + console.log("tree.streams", tree.streams); // TODO - remove console log /* calculate entropy in view */ if (entropy.loaded) { diff --git a/src/components/controls/choose-metric.js b/src/components/controls/choose-metric.js index f9939cb5a..506d8af2e 100644 --- a/src/components/controls/choose-metric.js +++ b/src/components/controls/choose-metric.js @@ -1,7 +1,6 @@ import React from "react"; import { connect } from "react-redux"; import { withTranslation } from "react-i18next"; -import { CHANGE_DISTANCE_MEASURE } from "../../actions/types"; import { analyticsControlsEvent } from "../../util/googleAnalytics"; import { toggleTemporalConfidence } from "../../actions/tree"; import { toggleStreamTree, changeDistanceMeasure } from "../../actions/streamTrees"; diff --git a/src/components/tree/phyloTree/change.ts b/src/components/tree/phyloTree/change.ts index 20667023d..8a0694b32 100644 --- a/src/components/tree/phyloTree/change.ts +++ b/src/components/tree/phyloTree/change.ts @@ -423,7 +423,6 @@ export const change = function change( this.streamLayout(); // recompute displayOrder values across pivots mapStreamsToScreen(this.streams, this.phyloStreams, this.xScale, this.yScale); // recompute pixels (unneeded for branches/tips) this.drawStreams(); // remove & redraw - this.drawStreamConnectors(); // remove & redraw } // don't have good methods to remove tips etc (yet) for (const name of ['branchLabels', 'branchTee', 'branchStem', 'tips', 'tipLabels', 'vaccines']) { @@ -453,7 +452,6 @@ export const change = function change( this.streamLayout(); // recompute displayOrder values across pivots mapStreamsToScreen(this.streams, this.phyloStreams, this.xScale, this.yScale); // recompute pixels (unneeded for branches/tips) this.drawStreams(); // remove & redraw - this.drawStreamConnectors(); // remove & redraw } // recalculate existing regression if needed if (changeVisibility && this.regression) { diff --git a/src/components/tree/phyloTree/layouts.ts b/src/components/tree/phyloTree/layouts.ts index 8265211f1..d42cd3132 100644 --- a/src/components/tree/phyloTree/layouts.ts +++ b/src/components/tree/phyloTree/layouts.ts @@ -658,9 +658,9 @@ export function mapStreamsToScreen(streams, phyloStreams, xScale, yScale) { /* it's important we only set this up once, as DOM elements will bind to data within, so we need to mutate it */ if (!phyloStream.ripples) { const _area = area() - .x((d) => d.x) - .y0((d) => d.y0) - .y1((d) => d.y1) + .x((d: any) => d.x) // TODO XXX fix types + .y0((d: any) => d.y0) // TODO XXX fix types + .y1((d: any) => d.y1) // TODO XXX fix types .curve(curveCatmullRom.alpha(0.5)) phyloStream.ripples = phyloStream.displayOrderByCategory.map((displayOrderByPivot) => { diff --git a/src/components/tree/phyloTree/phyloTree.ts b/src/components/tree/phyloTree/phyloTree.ts index 0af18fadb..4015dcdec 100644 --- a/src/components/tree/phyloTree/phyloTree.ts +++ b/src/components/tree/phyloTree/phyloTree.ts @@ -67,7 +67,6 @@ PhyloTree.prototype.drawRegression = renderers.drawRegression; PhyloTree.prototype.removeRegression = renderers.removeRegression; PhyloTree.prototype.updateColorBy = renderers.updateColorBy; PhyloTree.prototype.drawStreams = renderers.drawStreams; -PhyloTree.prototype.drawStreamConnectors = renderers.drawStreamConnectors; /* C A L C U L A T E G E O M E T R I E S E T C ( M O D I F I E S N O D E S , N O T S V G ) */ PhyloTree.prototype.setDistance = layouts.setDistance; diff --git a/src/components/tree/phyloTree/renderers.ts b/src/components/tree/phyloTree/renderers.ts index 3d15f4087..e56c2013b 100644 --- a/src/components/tree/phyloTree/renderers.ts +++ b/src/components/tree/phyloTree/renderers.ts @@ -119,7 +119,6 @@ export const render = function render( this.updateTipLabels(); this.drawTips(); this.drawStreams(); - this.drawStreamConnectors(); if (this.params.branchLabelKey) this.drawBranchLabels(this.params.branchLabelKey); if (this.vaccines) this.drawVaccines(); if (this.regression) this.drawRegression(); @@ -357,15 +356,10 @@ export function drawStreams(this: PhyloTreeType): void { .style("fill", "none") .style('visibility', 'visible') .style('cursor', 'pointer') // using a dashed line doesn't play nicely with onhover/onclick behaviour :( - .on("click", this.callbacks.onStreamConnectorClick); + .on("click", this.callbacks.onStreamConnectorClick as any); // TODO - fix type } } -export function drawStreamConnectors() { - console.log("drawStreamConnectors is currently a no-op") -} - - /** * draws the regression line in the svg and adds a text with the rate estimate */ diff --git a/src/components/tree/phyloTree/types.ts b/src/components/tree/phyloTree/types.ts index 9f053110d..5912e3fb9 100644 --- a/src/components/tree/phyloTree/types.ts +++ b/src/components/tree/phyloTree/types.ts @@ -2,6 +2,7 @@ import { Selection } from "d3"; import { Layout, PerformanceFlags, ScatterVariables } from "../../../reducers/controls"; import { ReduxNode, Visibility } from "../../../reducers/tree/types"; import { change, modifySVG, modifySVGInStages } from "./change"; +import { TreeComponent } from "../tree"; import * as confidence from "./confidence"; import * as grid from "./grid"; @@ -47,6 +48,7 @@ export interface Callbacks { onTipHover: NodeCallback onTipLeave: NodeCallback tipLabel: NodeCallback + onStreamConnectorClick: (this: TreeComponent, phyloStream: any) => void } // ---------- PhyloNode ---------- // diff --git a/src/components/tree/reactD3Interface/callbacks.ts b/src/components/tree/reactD3Interface/callbacks.ts index ce626af0a..a84ab79e6 100644 --- a/src/components/tree/reactD3Interface/callbacks.ts +++ b/src/components/tree/reactD3Interface/callbacks.ts @@ -102,9 +102,8 @@ export const onBranchClick = function onBranchClick(this: TreeComponent, d: Phyl this.props.dispatch(updateVisibleTipsAndBranchThicknesses({root, cladeSelected})); }; -export function onStreamConnectorClick(phyloStream) { - console.log("CLICK", phyloStream, this) - const root = [this.state.tree.streams.streams[phyloStream.streamIdx].founderIdx, undefined]; +export function onStreamConnectorClick(this: TreeComponent, phyloStream: any) { + const root = [this.state.tree.streams.streams[phyloStream.streamIdx].founderIdx, undefined] as [number, undefined]; // TODO fix type this.props.dispatch(updateVisibleTipsAndBranchThicknesses({root, cladeSelected: undefined})); // TODO - this zoom is wrong because underneath it all we're zooming to the display Order values of the subtree diff --git a/src/reducers/controls.ts b/src/reducers/controls.ts index 152f4e056..7e0e308dc 100644 --- a/src/reducers/controls.ts +++ b/src/reducers/controls.ts @@ -467,10 +467,8 @@ const Controls = (state: ControlsState = getDefaultControlsState(), action): Con case types.TOGGLE_LEGEND: return Object.assign({}, state, { legendOpen: action.value }); case types.TOGGLE_STREAM_TREE: - console.log("Controls reducer. Show?", action.showStreamTrees, "Label (unchanged):", state.streamTreeBranchLabel); return {...state, showStreamTrees: action.showStreamTrees}; case types.CHANGE_STREAM_TREE_BRANCH_LABEL: - console.log("Controls reducer. Show?", action.showStreamTrees, "Label", action.streamTreeBranchLabel); return {...state, showStreamTrees: action.showStreamTrees, streamTreeBranchLabel: action.streamTreeBranchLabel}; case types.ADD_EXTRA_METADATA: { for (const colorBy of Object.keys(action.newColorings)) { diff --git a/src/util/partitionIntoStreams.js b/src/util/partitionIntoStreams.js index 579bd2688..a8b1e1875 100644 --- a/src/util/partitionIntoStreams.js +++ b/src/util/partitionIntoStreams.js @@ -250,6 +250,6 @@ function getFounderTree(treeNodes, branchLabel) { }) - console.log({founderTree, founderIndiciesToDescendantFounderIndicies, foundersPostorder, streamGroups}) + // console.log({founderTree, founderIndiciesToDescendantFounderIndicies, foundersPostorder, streamGroups}) return {founderTree, founderIndiciesToDescendantFounderIndicies, foundersPostorder, streamGroups}; } \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index c6275dc6e..a169a42cc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,6 +14,7 @@ Visit https://aka.ms/tsconfig.json for a detailed list of options. /* Language and Environment */ "jsx": "react", /* Specify what JSX code is generated. */ "target": "es2015", + "lib": ["esnext", "dom"], /* Modules */ "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */