From 0e55b3022db5ac8c5ffa52deddec61b7182f8103 Mon Sep 17 00:00:00 2001 From: james hadfield Date: Fri, 18 Dec 2020 16:34:25 +1300 Subject: [PATCH] Allow tip labels to be user selected Adds an additional dropdown box to allow users to change the attribute used as tip labels. This is similar in design to the branch label drop down. Currently attributes are limited to the colorings. The function which collects these, `collectAvailableTipLabelOptions` could be easily extended to produce further valid attributes. This should pave the way for smarter and more customisable logic around _when_ labels are displayed, and the dropdown should not be shown in the situation that labels aren't shown. For a full description, see #1201 Closes #1201 --- src/actions/recomputeReduxState.js | 12 +++- src/actions/types.js | 1 + src/components/controls/choose-tip-label.js | 66 +++++++++++++++++++ src/components/controls/controls.js | 2 + src/components/tree/index.js | 1 + src/components/tree/phyloTree/change.js | 9 ++- src/components/tree/phyloTree/labels.js | 17 +++++ .../tree/reactD3Interface/change.js | 6 +- .../tree/reactD3Interface/initialRender.js | 3 +- src/middleware/changeURL.js | 4 ++ src/reducers/controls.js | 4 ++ 11 files changed, 120 insertions(+), 5 deletions(-) create mode 100644 src/components/controls/choose-tip-label.js diff --git a/src/actions/recomputeReduxState.js b/src/actions/recomputeReduxState.js index 4cf121d96..da06f6a62 100644 --- a/src/actions/recomputeReduxState.js +++ b/src/actions/recomputeReduxState.js @@ -16,7 +16,7 @@ import { computeMatrixFromRawData } from "../util/processFrequencies"; import { applyInViewNodesToTree } from "../actions/tree"; import { isColorByGenotype, decodeColorByGenotype } from "../util/getGenotype"; import { getTraitFromNode, getDivFromNode } from "../util/treeMiscHelpers"; - +import { collectAvailableTipLabelOptions } from "../components/controls/choose-tip-label"; export const doesColorByHaveConfidence = (controlsState, colorBy) => controlsState.coloringsPresentOnTreeWithConfidence.has(colorBy); @@ -71,6 +71,9 @@ const modifyStateViaURLQuery = (state, query) => { if (query.p && state.canTogglePanelLayout && (query.p === "full" || query.p === "grid")) { state["panelLayout"] = query.p; } + if (query.tl) { + state["tipLabelKey"] = query.tl; + } if (query.d) { const proposed = query.d.split(","); state.panelsToDisplay = state.panelsAvailable.filter((n) => proposed.indexOf(n) !== -1); @@ -169,6 +172,7 @@ const restoreQueryableStateToDefaults = (state) => { state["panelLayout"] = calcBrowserDimensionsInitialState().width > twoColumnBreakpoint ? "grid" : "full"; state.panelsToDisplay = state.panelsAvailable.slice(); + state.tipLabelKey = strainSymbol; // console.log("state now", state); return state; }; @@ -478,6 +482,12 @@ const checkAndCorrectErrorsInState = (state, metadata, query, tree, viewingNarra state.defaults.selectedBranchLabel = "none"; } + /* check tip label is valid. We use the function which generates the options for the dropdown here */ + if (!collectAvailableTipLabelOptions(metadata.colorings).map((o) => o.value).includes(state.tipLabelKey)) { + console.error("Can't set selected tip label to ", state.tipLabelKey); + state.tipLabelKey = strainSymbol; + } + /* temporalConfidence */ if (shouldDisplayTemporalConfidence(state.temporalConfidence.exists, state.distanceMeasure, state.layout)) { state.temporalConfidence.display = true; diff --git a/src/actions/types.js b/src/actions/types.js index 5bef4fb19..2b1775149 100644 --- a/src/actions/types.js +++ b/src/actions/types.js @@ -53,3 +53,4 @@ export const TOGGLE_LEGEND = "TOGGLE_LEGEND"; export const TOGGLE_TRANSMISSION_LINES = "TOGGLE_TRANSMISSION_LINES"; export const CACHE_JSONS = "CACHE_JSONS"; export const SET_ROOT_SEQUENCE = "SET_ROOT_SEQUENCE"; +export const CHANGE_TIP_LABEL_KEY = "CHANGE_TIP_LABEL_KEY"; diff --git a/src/components/controls/choose-tip-label.js b/src/components/controls/choose-tip-label.js new file mode 100644 index 000000000..e37e77388 --- /dev/null +++ b/src/components/controls/choose-tip-label.js @@ -0,0 +1,66 @@ +import React from "react"; +import { connect } from "react-redux"; +import Select from "react-select/lib/Select"; +import { withTranslation } from 'react-i18next'; +import { CHANGE_TIP_LABEL_KEY } from "../../actions/types"; +import { SidebarSubtitle } from "./styles"; +import { controlsWidth, strainSymbol } from "../../util/globals"; + +@connect((state) => ({ + selected: state.controls.tipLabelKey, + options: collectAvailableTipLabelOptions(state.metadata.colorings) +})) +class ChooseTipLabel extends React.Component { + constructor(props) { + super(props); + this.change = (value) => {this.props.dispatch({type: CHANGE_TIP_LABEL_KEY, key: value.value});}; + } + render() { + const { t } = this.props; + return ( +
+ + {t("sidebar:Tip Labels")} + +
+ can't handle Symbols so we need to write our own algorithm. + */ +function findSelectedValue(selected, options) { + return options.filter((o) => o.value===selected)[0]; +} diff --git a/src/components/controls/controls.js b/src/components/controls/controls.js index c9fa34b69..94735825d 100644 --- a/src/components/controls/controls.js +++ b/src/components/controls/controls.js @@ -7,6 +7,7 @@ import ChooseBranchLabelling from "./choose-branch-labelling"; import ChooseLayout from "./choose-layout"; import ChooseDataset from "./choose-dataset"; import ChooseSecondTree from "./choose-second-tree"; +import ChooseTipLabel from "./choose-tip-label"; import ChooseMetric from "./choose-metric"; import PanelLayout from "./panel-layout"; import GeoResolution from "./geo-resolution"; @@ -42,6 +43,7 @@ function Controls({mapOn, frequenciesOn, mobileDisplay}) { + diff --git a/src/components/tree/index.js b/src/components/tree/index.js index 229d056aa..e8adac72e 100644 --- a/src/components/tree/index.js +++ b/src/components/tree/index.js @@ -19,6 +19,7 @@ const Tree = connect((state) => ({ showTangle: state.controls.showTangle, panelsToDisplay: state.controls.panelsToDisplay, selectedBranchLabel: state.controls.selectedBranchLabel, + tipLabelKey: state.controls.tipLabelKey, narrativeMode: state.narrative.display, animationPlayPauseButton: state.controls.animationPlayPauseButton }))(UnconnectedTree); diff --git a/src/components/tree/phyloTree/change.js b/src/components/tree/phyloTree/change.js index e6b0edc79..a4091b50f 100644 --- a/src/components/tree/phyloTree/change.js +++ b/src/components/tree/phyloTree/change.js @@ -5,6 +5,7 @@ import { timerStart, timerEnd } from "../../../util/perf"; import { NODE_VISIBLE } from "../../../util/globals"; import { getBranchVisibility, strokeForBranch } from "./renderers"; import { shouldDisplayTemporalConfidence } from "../../../reducers/controls"; +import { makeTipLabelFunc } from "./labels"; /* loop through the nodes and update each provided prop with the new value * additionally, set d.update -> whether or not the node props changed @@ -253,11 +254,12 @@ export const change = function change({ zoomIntoClade = false, svgHasChangedDimensions = false, animationInProgress = false, - /* change these things to provided value */ + /* change these things to provided value (unless undefined) */ newDistance = undefined, newLayout = undefined, updateLayout = undefined, newBranchLabellingKey = undefined, + newTipLabelKey = undefined, /* arrays of data (the same length as nodes) */ branchStroke = undefined, tipStroke = undefined, @@ -358,6 +360,11 @@ export const change = function change({ ) { this.mapToScreen(); } + /* tip label key change -> update callback used */ + if (newTipLabelKey) { + this.callbacks.tipLabel = makeTipLabelFunc(newTipLabelKey); + elemsToUpdate.add('.tipLabel'); /* will trigger d3 commands as required */ + } /* Finally, actually change the SVG elements themselves */ const extras = { removeConfidences, showConfidences, newBranchLabellingKey }; diff --git a/src/components/tree/phyloTree/labels.js b/src/components/tree/phyloTree/labels.js index 29348c509..f0c4a3bec 100644 --- a/src/components/tree/phyloTree/labels.js +++ b/src/components/tree/phyloTree/labels.js @@ -1,5 +1,7 @@ import { timerFlush } from "d3-timer"; import { NODE_VISIBLE } from "../../../util/globals"; +import { numericToDateObject, prettifyDate } from "../../../util/dateHelpers"; +import { getTraitFromNode } from "../../../util/treeMiscHelpers"; export const updateTipLabels = function updateTipLabels(dt) { if ("tipLabels" in this.groups) { @@ -148,3 +150,18 @@ export const drawBranchLabels = function drawBranchLabels(key) { .style("font-size", labelSize) .text((d) => d.n.branch_attrs.labels[key]); }; + +/** + * A helper factory to create the tip label function. + * This (returned function) is typically set elsewhere + * and stored on `this.callbacks.tipLabel` which is used + * in the `updateTipLabels` function. + */ +export const makeTipLabelFunc = (tipLabelKey) => { + /* special-case `num_date`. In the future we may wish to examine + `metadata.colorings` and special case other scale types */ + if (tipLabelKey === "num_date") { + return (d) => prettifyDate("DAY", numericToDateObject(getTraitFromNode(d.n, "num_date"))); + } + return (d) => getTraitFromNode(d.n, tipLabelKey); +}; diff --git a/src/components/tree/reactD3Interface/change.js b/src/components/tree/reactD3Interface/change.js index 635c2f1bb..68d94a9b8 100644 --- a/src/components/tree/reactD3Interface/change.js +++ b/src/components/tree/reactD3Interface/change.js @@ -51,11 +51,13 @@ export const changePhyloTreeViaPropsComparison = (mainTree, phylotree, oldProps, args.newDistance = newProps.distanceMeasure; } - /* change in key used to define branch labels (e.g. aa, clade...) */ + /* change in key used to define branch labels, tip labels */ if (oldProps.selectedBranchLabel !== newProps.selectedBranchLabel) { args.newBranchLabellingKey = newProps.selectedBranchLabel; } - + if (oldProps.tipLabelKey !== newProps.tipLabelKey) { + args.newTipLabelKey = newProps.tipLabelKey; + } /* show / remove confidence intervals across the tree */ if ( diff --git a/src/components/tree/reactD3Interface/initialRender.js b/src/components/tree/reactD3Interface/initialRender.js index 765719312..023fded44 100644 --- a/src/components/tree/reactD3Interface/initialRender.js +++ b/src/components/tree/reactD3Interface/initialRender.js @@ -3,6 +3,7 @@ import 'd3-transition'; import { rgb } from "d3-color"; import { calcBranchStrokeCols } from "../../../util/colorHelpers"; import * as callbacks from "./callbacks"; +import { makeTipLabelFunc } from "../phyloTree/labels"; export const renderTree = (that, main, phylotree, props) => { const ref = main ? that.domRefs.mainTree : that.domRefs.secondTree; @@ -31,7 +32,7 @@ export const renderTree = (that, main, phylotree, props) => { onBranchClick: callbacks.onBranchClick.bind(that), onBranchLeave: callbacks.onBranchLeave.bind(that), onTipLeave: callbacks.onTipLeave.bind(that), - tipLabel: (d) => d.n.name + tipLabel: makeTipLabelFunc(props.tipLabelKey) }, treeState.branchThickness, /* guarenteed to be in redux by now */ treeState.visibility, diff --git a/src/middleware/changeURL.js b/src/middleware/changeURL.js index cbbc370d8..6a7f8f6e6 100644 --- a/src/middleware/changeURL.js +++ b/src/middleware/changeURL.js @@ -129,6 +129,10 @@ export const changeURLMiddleware = (store) => (next) => (action) => { query.p = action.panelLayout; break; } + case types.CHANGE_TIP_LABEL_KEY: { + query.tl = action.key===strainSymbol ? undefined : action.key; + break; + } case types.CHANGE_DATES_VISIBILITY_THICKNESS: { if (state.controls.animationPlayPauseButton === "Pause") { // animation in progress - no dates in URL query.dmin = undefined; diff --git a/src/reducers/controls.js b/src/reducers/controls.js index 9b1cbb9be..d846507d8 100644 --- a/src/reducers/controls.js +++ b/src/reducers/controls.js @@ -6,6 +6,7 @@ import { defaultGeoResolution, defaultLayout, defaultMutType, controlsHiddenWidth, + strainSymbol, twoColumnBreakpoint } from "../util/globals"; import * as types from "../actions/types"; import { calcBrowserDimensionsInitialState } from "./browserDimensions"; @@ -74,6 +75,7 @@ export const getDefaultControlsState = () => { panelsAvailable: [], panelsToDisplay: [], panelLayout: calcBrowserDimensionsInitialState().width > twoColumnBreakpoint ? "grid" : "full", + tipLabelKey: strainSymbol, showTreeToo: undefined, showTangle: false, zoomMin: undefined, @@ -194,6 +196,8 @@ const Controls = (state = getDefaultControlsState(), action) => { return Object.assign({}, state, { panelLayout: action.data }); + case types.CHANGE_TIP_LABEL_KEY: + return {...state, tipLabelKey: action.key}; case types.TREE_TOO_DATA: return action.controls; case types.TOGGLE_PANEL_DISPLAY: