From 42c111e01eb0f0c907850184ea8c9adb4836172c Mon Sep 17 00:00:00 2001 From: james hadfield Date: Thu, 14 Nov 2024 12:27:43 +1300 Subject: [PATCH] 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