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: