Skip to content

Commit

Permalink
Allow strains to be searched as normal filters
Browse files Browse the repository at this point in the history
With the introduction of a "filter" search box in the sidebar, we wanted to allow strain-search here which would allow us to get rid of the "strain search" box.

There are some slight differences with this new UI:

Positives: One can now select multiple strains, which is really cool. They are stored in the URL (comma separated list) and thus can be used in narratives.

Todo: The previous behavior of expanding the tip radii on partial search completion isn't part of this new UI, but it should be.
  • Loading branch information
jameshadfield committed Nov 18, 2020
1 parent a430ef6 commit b2a6980
Show file tree
Hide file tree
Showing 9 changed files with 63 additions and 131 deletions.
46 changes: 20 additions & 26 deletions src/actions/recomputeReduxState.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import queryString from "query-string";
import { cloneDeep } from 'lodash';
import { numericToCalendar, calendarToNumeric } from "../util/dateHelpers";
import { reallySmallNumber, twoColumnBreakpoint, defaultColorBy, defaultGeoResolution, defaultDateRange, nucleotide_gene } from "../util/globals";
import { reallySmallNumber, twoColumnBreakpoint, defaultColorBy, defaultGeoResolution, defaultDateRange, nucleotide_gene, strainSymbol } from "../util/globals";
import { calcBrowserDimensionsInitialState } from "../reducers/browserDimensions";
import { strainNameToIdx, getIdxMatchingLabel, calculateVisiblityAndBranchThickness } from "../util/treeVisibilityHelpers";
import { getIdxMatchingLabel, calculateVisiblityAndBranchThickness } from "../util/treeVisibilityHelpers";
import { constructVisibleTipLookupBetweenTrees } from "../util/treeTangleHelpers";
import { calcTipRadii } from "../util/tipRadiusHelpers";
import { getDefaultControlsState, shouldDisplayTemporalConfidence } from "../reducers/controls";
import { countTraitsAcrossTree, calcTotalTipsInTree } from "../util/treeCountingHelpers";
import { calcEntropyInView } from "../util/entropy";
Expand Down Expand Up @@ -91,6 +90,9 @@ const modifyStateViaURLQuery = (state, query) => {
state.filters[filterKey.replace('f_', '')] = query[filterKey].split(',')
.map((value) => ({value, active: true})); /* all filters in the URL are "active" */
}
if (query.s) { // selected strains are a filter too
state.filters[strainSymbol] = query.s.split(',').map((value) => ({value, active: true}));
}
if (query.animate) {
const params = query.animate.split(',');
// console.log("start animation!", params);
Expand Down Expand Up @@ -137,7 +139,6 @@ const modifyStateViaURLQuery = (state, query) => {
state.showTransmissionLines = false;
}
}

return state;
};

Expand Down Expand Up @@ -204,6 +205,7 @@ const modifyStateViaMetadata = (state, metadata) => {
} else {
console.warn("JSON did not include any filters");
}
state.filters[strainSymbol] = [];
if (metadata.displayDefaults) {
const keysToCheckFor = ["geoResolution", "colorBy", "distanceMeasure", "layout", "mapTriplicate", "selectedBranchLabel", 'sidebar', "showTransmissionLines", "normalizeFrequencies"];
const expectedTypes = ["string", "string", "string", "string", "boolean", "string", 'string', "boolean" , "boolean"]; // eslint-disable-line
Expand Down Expand Up @@ -506,6 +508,13 @@ const checkAndCorrectErrorsInState = (state, metadata, query, tree, viewingNarra
query[`f_${filterName}`] = validItems.map((x) => x.value).join(",");
}
});
if (state.filters[strainSymbol]) {
const validNames = tree.nodes.map((n) => n.name);
state.filters[strainSymbol] = state.filters[strainSymbol]
.filter((strainFilter) => validNames.includes(strainFilter.value));
query.s = state.filters[strainSymbol].map((f) => f.value).join(",");
if (!query.s) delete query.s;
}

/* can we display branch length by div or num_date? */
if (query.m && state.branchLengthsToDisplay !== "divAndDate") {
Expand All @@ -525,30 +534,23 @@ const checkAndCorrectErrorsInState = (state, metadata, query, tree, viewingNarra
return state;
};

const modifyTreeStateVisAndBranchThickness = (oldState, tipSelected, zoomSelected, controlsState, dispatch) => {
const modifyTreeStateVisAndBranchThickness = (oldState, zoomSelected, controlsState, dispatch) => {
/* calculate new branch thicknesses & visibility */
let tipSelectedIdx = 0;
/* check if the query defines a strain to be selected */
let newIdxRoot = oldState.idxOfInViewRootNode;
if (tipSelected) {
tipSelectedIdx = strainNameToIdx(oldState.nodes, tipSelected);
oldState.selectedStrain = tipSelected;
}
if (zoomSelected) {
// Check and fix old format 'clade=B' - in this case selectionClade is just 'B'
const [labelName, labelValue] = zoomSelected.split(":");
const cladeSelectedIdx = getIdxMatchingLabel(oldState.nodes, labelName, labelValue, dispatch);
oldState.selectedClade = zoomSelected;
newIdxRoot = applyInViewNodesToTree(cladeSelectedIdx, oldState); // tipSelectedIdx, oldState);
newIdxRoot = applyInViewNodesToTree(cladeSelectedIdx, oldState);
} else {
oldState.selectedClade = undefined;
newIdxRoot = applyInViewNodesToTree(0, oldState); // tipSelectedIdx, oldState);
newIdxRoot = applyInViewNodesToTree(0, oldState);
}
const visAndThicknessData = calculateVisiblityAndBranchThickness(
oldState,
controlsState,
{dateMinNumeric: controlsState.dateMinNumeric, dateMaxNumeric: controlsState.dateMaxNumeric},
{tipSelectedIdx}
{dateMinNumeric: controlsState.dateMinNumeric, dateMaxNumeric: controlsState.dateMaxNumeric}
);

const newState = Object.assign({}, oldState, visAndThicknessData);
Expand All @@ -557,10 +559,6 @@ const modifyTreeStateVisAndBranchThickness = (oldState, tipSelected, zoomSelecte
newState.visibleStateCounts = countTraitsAcrossTree(newState.nodes, newState.stateCountAttrs, newState.visibility, true);
newState.totalStateCounts = countTraitsAcrossTree(newState.nodes, newState.stateCountAttrs, false, true); // eslint-disable-line

if (tipSelectedIdx) { /* i.e. query.s was set */
newState.tipRadii = calcTipRadii({tipSelectedIdx, colorScale: controlsState.colorScale, tree: newState});
newState.tipRadiiVersion = 1;
}
return newState;
};

Expand Down Expand Up @@ -776,12 +774,12 @@ export const createStateFromQueryOrJSONs = ({
}

/* if query.label is undefined then we intend to zoom to the root */
tree = modifyTreeStateVisAndBranchThickness(tree, query.s, query.label, controls, dispatch);
tree = modifyTreeStateVisAndBranchThickness(tree, query.label, controls, dispatch);

if (treeToo && treeToo.loaded) {
treeToo.nodeColorsVersion = tree.nodeColorsVersion;
treeToo.nodeColors = calcNodeColor(treeToo, controls.colorScale);
treeToo = modifyTreeStateVisAndBranchThickness(treeToo, query.s, undefined, controls, dispatch);
treeToo = modifyTreeStateVisAndBranchThickness(treeToo, undefined, controls, dispatch);
controls = modifyControlsViaTreeToo(controls, treeToo.name);
treeToo.tangleTipLookup = constructVisibleTipLookupBetweenTrees(tree.nodes, treeToo.nodes, tree.visibility, treeToo.visibility);
}
Expand Down Expand Up @@ -833,7 +831,7 @@ export const createTreeTooState = ({
treeToo.debug = "RIGHT";
controls = modifyControlsStateViaTree(controls, tree, treeToo, oldState.metadata.colorings);
controls = modifyControlsViaTreeToo(controls, secondTreeUrl);
treeToo = modifyTreeStateVisAndBranchThickness(treeToo, tree.selectedStrain, undefined, controls, dispatch);
treeToo = modifyTreeStateVisAndBranchThickness(treeToo, undefined, controls, dispatch);

/* calculate colours if loading from JSONs or if the query demands change */
const colorScale = calcColorScale(controls.colorBy, controls, tree, treeToo, oldState.metadata);
Expand All @@ -850,9 +848,5 @@ export const createTreeTooState = ({
tree.nodes, treeToo.nodes, tree.visibility, treeToo.visibility
);

// if (tipSelectedIdx) { /* i.e. query.s was set */
// tree.tipRadii = calcTipRadii({tipSelectedIdx, colorScale: controls.colorScale, tree});
// tree.tipRadiiVersion = 1;
// }
return {tree, treeToo, controls};
};
53 changes: 5 additions & 48 deletions src/actions/tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,31 +35,6 @@ export const applyInViewNodesToTree = (idx, tree) => {
return validIdxRoot;
};

const processSelectedTip = (d, tree, treeToo) => {
if (!d) {
return [undefined, undefined, undefined];
}
if (d.clear) {
return [undefined, undefined, undefined];
}
if (d.treeIdx) {
const name = tree.nodes[d.treeIdx].name;
const idx2 = treeToo ? strainNameToIdx(treeToo.nodes, name) : undefined;
return [d.treeIdx, idx2, name];
}
if (d.treeTooIdx) {
const name = treeToo.nodes[d.treeTooIdx].name;
const idx1 = strainNameToIdx(tree.nodes, name);
return [idx1, d.treeTooIdx, name];
}
if (tree.selectedStrain) {
const idx1 = strainNameToIdx(tree.nodes, tree.selectedStrain);
const idx2 = treeToo ? strainNameToIdx(treeToo.nodes, tree.selectedStrain) : undefined;
return [idx1, idx2, tree.selectedStrain];
}
return [undefined, undefined, undefined];
};

/**
* define the visible branches and their thicknesses. This could be a path to a single tip or a selected clade.
* filtering etc will "turn off" branches, etc etc
Expand All @@ -73,7 +48,7 @@ const processSelectedTip = (d, tree, treeToo) => {
* @return {function} a function to be handled by redux (thunk)
*/
export const updateVisibleTipsAndBranchThicknesses = (
{root = [undefined, undefined], tipSelected = undefined, cladeSelected = undefined} = {}
{root = [undefined, undefined], cladeSelected = undefined} = {}
) => {
return (dispatch, getState) => {
const { tree, treeToo, controls, frequencies } = getState();
Expand All @@ -86,13 +61,11 @@ export const updateVisibleTipsAndBranchThicknesses = (
// console.log("ROOT SETTING TO", root)
/* mark nodes as "in view" as applicable */
const rootIdxTree1 = applyInViewNodesToTree(root[0], tree);
const [tipIdx1, tipIdx2, tipName] = processSelectedTip(tipSelected, tree, controls.showTreeToo ? treeToo : undefined);

const data = calculateVisiblityAndBranchThickness(
tree,
controls,
{dateMinNumeric: controls.dateMinNumeric, dateMaxNumeric: controls.dateMaxNumeric},
{tipSelectedIdx: tipIdx1}
{dateMinNumeric: controls.dateMinNumeric, dateMaxNumeric: controls.dateMaxNumeric}
);
const dispatchObj = {
type: types.UPDATE_VISIBILITY_AND_BRANCH_THICKNESS,
Expand All @@ -103,8 +76,7 @@ export const updateVisibleTipsAndBranchThicknesses = (
idxOfInViewRootNode: rootIdxTree1,
cladeName: cladeSelected,
selectedClade: cladeSelected,
stateCountAttrs: Object.keys(controls.filters),
selectedStrain: tipName
stateCountAttrs: Object.keys(controls.filters)
};

if (controls.showTreeToo) {
Expand All @@ -113,7 +85,7 @@ export const updateVisibleTipsAndBranchThicknesses = (
treeToo,
controls,
{dateMinNumeric: controls.dateMinNumeric, dateMaxNumeric: controls.dateMaxNumeric},
{tipSelectedIdx: tipIdx2}
// {tipSelectedIdx: tipIdx2}
);
dispatchObj.tangleTipLookup = constructVisibleTipLookupBetweenTrees(tree.nodes, treeToo.nodes, data.visibility, dataToo.visibility);
dispatchObj.visibilityToo = dataToo.visibility;
Expand All @@ -124,21 +96,6 @@ export const updateVisibleTipsAndBranchThicknesses = (
/* tip selected is the same as the first tree - the reducer uses that */
}

/* I think this fixes bug of not resizing tipradii if in URL then deselected */
if (tipSelected) {
const newTipRadii = calcTipRadii({tipSelected, colorScale: controls.colorScale, tree: tree});
const newTipRadiiVersion = tree.tipRadiiVersion + 1;
const dispatchRadii = {
type: types.UPDATE_TIP_RADII,
data: newTipRadii,
version: newTipRadiiVersion
};
if (controls.showTreeToo) {
dispatchRadii.dataToo = calcTipRadii({tipSelected, colorScale: controls.colorScale, tree: treeToo});
}
dispatch(dispatchRadii);
}

/* D I S P A T C H */
dispatch(dispatchObj);
updateEntropyVisibility(dispatch, getState);
Expand Down Expand Up @@ -242,7 +199,7 @@ export const updateTipRadii = (
export const applyFilter = (mode, trait, values) => {
return (dispatch, getState) => {
const { controls } = getState();
const currentlyFilteredTraits = Object.keys(controls.filters);
const currentlyFilteredTraits = Reflect.ownKeys(controls.filters);
let newValues;
switch (mode) {
case "set":
Expand Down
16 changes: 14 additions & 2 deletions src/components/controls/filter.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from "react";
import { connect } from "react-redux";
import Select from "react-select/lib/Select";
import { controlsWidth, isValueValid } from "../../util/globals";
import { controlsWidth, isValueValid, strainSymbol} from "../../util/globals";
import { applyFilter } from "../../actions/tree";

/**
Expand All @@ -13,7 +13,8 @@ import { applyFilter } from "../../actions/tree";
@connect((state) => {
return {
activeFilters: state.controls.filters,
totalStateCounts: state.tree.totalStateCounts
totalStateCounts: state.tree.totalStateCounts,
nodes: state.tree.nodes
};
})
class FilterData extends React.Component {
Expand Down Expand Up @@ -51,6 +52,16 @@ class FilterData extends React.Component {
});
});
});
if (strainSymbol in this.props.activeFilters) {
this.props.nodes
.filter((n) => !n.hasChildren)
.forEach((n) => {
options.push({
label: `sample → ${n.name}`,
value: [strainSymbol, n.name]
});
});
}
return options;
}
selectionMade = (sel) => {
Expand All @@ -69,6 +80,7 @@ class FilterData extends React.Component {
clearable={false}
searchable
multi={false}
valueKey="label"
onChange={this.selectionMade}
/>
</div>
Expand Down
24 changes: 4 additions & 20 deletions src/components/info/info.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import { withTranslation } from 'react-i18next';

import Card from "../framework/card";
import { titleFont, headerFont, medGrey, darkGrey } from "../../globalStyles";
import { applyFilter, changeDateFilter, updateVisibleTipsAndBranchThicknesses } from "../../actions/tree";
import { applyFilter, changeDateFilter } from "../../actions/tree";
import { getVisibleDateRange } from "../../util/treeVisibilityHelpers";
import { numericToCalendar } from "../../util/dateHelpers";
import { months, NODE_VISIBLE } from "../../util/globals";
import { months, NODE_VISIBLE, strainSymbol } from "../../util/globals";
import Byline from "./byline";
import { FilterBadge } from "./filterBadge";

Expand Down Expand Up @@ -118,7 +118,6 @@ export const createSummary = (mainTreeNumTips, nodes, filters, visibility, visib
visibleStateCounts: state.tree.visibleStateCounts,
totalStateCounts: state.tree.totalStateCounts,
visibility: state.tree.visibility,
selectedStrain: state.tree.selectedStrain,
selectedClade: state.tree.selectedClade,
dateMin: state.controls.dateMin,
dateMax: state.controls.dateMax,
Expand Down Expand Up @@ -200,25 +199,11 @@ class Info extends React.Component {
>
<span>
{item.value}
{` (${this.props.totalStateCounts[filterName].get(item.value)})`}
{filterName!==strainSymbol && ` (${this.props.totalStateCounts[filterName].get(item.value)})`}
</span>
</FilterBadge>
));
}
selectedStrainButton(strain) {
return (
<span>
{"Showing a single strain "}
<FilterBadge
remove={() => this.props.dispatch(
updateVisibleTipsAndBranchThicknesses({tipSelected: {clear: true}, cladeSelected: this.props.selectedClade})
)}
>
{strain}
</FilterBadge>
</span>
);
}
clearFilterButton(field) {
return (
<span
Expand Down Expand Up @@ -271,7 +256,7 @@ class Info extends React.Component {

/* part II - the filters in play (both active and inactive) */
const filters = [];
Object.keys(this.props.filters)
Reflect.ownKeys(this.props.filters)
.filter((filterName) => this.props.filters[filterName].length > 0)
.forEach((filterName) => {
filters.push(...this.createFilterBadges(filterName));
Expand All @@ -285,7 +270,6 @@ class Info extends React.Component {
<Byline styles={styles} width={this.props.width} metadata={this.props.metadata}/>
<div width={this.props.width} style={styles.n}>
{animating ? t("Animation in progress") + ". " : null}
{this.props.selectedStrain ? this.selectedStrainButton(this.props.selectedStrain) : null}
{/* part 1 - the summary */}
{showExtended ? summary : null}
{/* part 2 - the filters */}
Expand Down
30 changes: 4 additions & 26 deletions src/components/tree/reactD3Interface/callbacks.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { updateVisibleTipsAndBranchThicknesses} from "../../../actions/tree";
import { NODE_VISIBLE } from "../../../util/globals";
import { updateVisibleTipsAndBranchThicknesses, applyFilter } from "../../../actions/tree";
import { NODE_VISIBLE, strainSymbol } from "../../../util/globals";
import { getDomId, getParentBeyondPolytomy, getIdxOfInViewRootNode } from "../phyloTree/helpers";
import { branchStrokeForHover, branchStrokeForLeave } from "../phyloTree/renderers";

Expand All @@ -20,16 +20,11 @@ export const onTipHover = function onTipHover(d) {
export const onTipClick = function onTipClick(d) {
if (d.visibility !== NODE_VISIBLE) return;
if (this.props.narrativeMode) return;
// console.log("tip click", d)
this.setState({
hovered: null,
selectedTip: d
});
/* are we clicking from tree1 or tree2? */
const tipSelected = d.that.params.orientation[0] === 1 ?
{treeIdx: d.n.arrayIdx} :
{treeTooIdx: d.n.arrayIdx};
this.props.dispatch(updateVisibleTipsAndBranchThicknesses({tipSelected, cladeSelected: this.props.tree.selectedClade}));
this.props.dispatch(applyFilter("add", strainSymbol, [d.n.name]));
};


Expand Down Expand Up @@ -130,22 +125,5 @@ export const clearSelectedTip = function clearSelectedTip(d) {
.attr("r", (dd) => dd["r"]);
this.setState({selectedTip: null, hovered: null});
/* restore the tip visibility! */
this.props.dispatch(updateVisibleTipsAndBranchThicknesses(
{tipSelected: {clear: true}, cladeSelected: this.props.tree.selectedClade}
));
};

/**
* @param {node} d tree node object
* @param {int} n total number of nodes in current view
* @return {int} font size of the tip label
*/
export const tipLabelSize = (d, n) => {
if (n > 70) {
return 0;
} else if (n < 20) {
return 14;
}
const fs = 6 + 8 * (70 - n) / (70 - 20);
return fs;
this.props.dispatch(applyFilter("remove", strainSymbol, [d.n.name]));
};
Loading

0 comments on commit b2a6980

Please sign in to comment.