Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sample uncertainty #1163

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion src/actions/colors.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export const changeColorBy = (providedColorBy = undefined) => { // eslint-disabl
colorScale,
nodeColors,
nodeColorsToo,
version: colorScale.version
nodeColorsVersion: tree.nodeColorsVersion+1
});

/* step 5 - frequency dispatch */
Expand All @@ -64,3 +64,15 @@ export const changeColorBy = (providedColorBy = undefined) => { // eslint-disabl
return null;
};
};


export const changeToNextColorBy = () => {
return (dispatch, getState) => {
const {controls, metadata} = getState();
const current = controls.colorBy;
const available = Object.keys(metadata.colorings)
.filter((c) => c!=="gt"); // filter out genotypes
const nextColorBy = available[(available.indexOf(current)+1) % available.length];
dispatch(changeColorBy(nextColorBy));
};
};
71 changes: 71 additions & 0 deletions src/actions/sample.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { calcNodeColor } from "../util/colorHelpers";
import {RESAMPLE} from "./types";

/**
* Currently this is changing the values behind the selected color-by.
*
* TODO: generalise to any trait
* TODO: Frequencies need updating?
* TODO: second tree
*/
export const sampleTraitFromUncertainty = ({trait, returnToOriginal=false}) => {
console.log(`sampleTraitFromUncertainty trait=${trait} returnToOriginal=${returnToOriginal}`);
return (dispatch, getState) => {
const { controls, tree } = getState();

tree.nodes.forEach((n) => {
if (n.node_attrs[trait] && n.node_attrs[trait].confidence) {
if (returnToOriginal) {
if (!n.node_attrs[trait].originalValue) {
console.error("Original state not saved...");
return;
}
n.node_attrs[trait].value = n.node_attrs[trait].originalValue;
} else {
if (!n.node_attrs[trait].originalValue) {
n.node_attrs[trait].originalValue = n.node_attrs[trait].value; // allows us to go back to original state
}
if (Array.isArray(n.node_attrs[trait].confidence)) {
n.node_attrs[trait].value = sampleUniformlyBetweenBounds(n.node_attrs[trait].confidence);
} else {
n.node_attrs[trait].value = sampleFromDiscreteDistribution(n.node_attrs[trait].confidence);
}
}
}
});

const colorBy = controls.colorBy;
const dispatchObj = {type: RESAMPLE};

if (trait === colorBy) {
/* if the current color-by is the trait we're resampling, then we need to update the node colors */
dispatchObj.nodeColors = calcNodeColor(tree, controls.colorScale);
dispatchObj.nodeColorsVersion = tree.nodeColorsVersion+1;
}
dispatch(dispatchObj);
};
};


function sampleFromDiscreteDistribution(confidenceObj) {
/* based on the algorithm behind R's `sample` function */
// to-do: if the probabilities don't sum to 1 the output will be biased towards the final value
const values = Object.keys(confidenceObj);
const probabilities = Object.values(confidenceObj);
const n = values.length;
const cumulativeProbs = new Array(n);
cumulativeProbs[0] = probabilities[0];
for (let i = 1; i<n; i++) {
cumulativeProbs[i] = probabilities[i] + cumulativeProbs[i-1];
}
const rU = Math.random();
let j;
for (j=0; j<(n-1); j++) {
if (rU <= cumulativeProbs[j]) break;
}
return values[j];
}

function sampleUniformlyBetweenBounds(bounds) {
return bounds[0] + (bounds[1]-bounds[0])*Math.random();
}
1 change: 1 addition & 0 deletions src/actions/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,4 @@ export const SET_AVAILABLE = "SET_AVAILABLE";
export const TOGGLE_SIDEBAR = "TOGGLE_SIDEBAR";
export const TOGGLE_LEGEND = "TOGGLE_LEGEND";
export const TOGGLE_TRANSMISSION_LINES = "TOGGLE_TRANSMISSION_LINES";
export const RESAMPLE = "RESAMPLE";
63 changes: 63 additions & 0 deletions src/components/framework/keyboard-shortcuts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import Mousetrap from "mousetrap";
import React from "react";
import { connect } from "react-redux";
import {changeToNextColorBy} from "../../actions/colors";
import {sampleTraitFromUncertainty} from "../../actions/sample";

/**
* Here we have a react component which currently renders nothing.
* It acts as a listener for keypresses and triggers the appropriate
* (redux) actions.
*
* NOTE 1: There are already a few places in the codebase where we
* listen for key-presses (search for "mousetrap"). Consider
* centralising them here, if possible and as desired.
*
* NOTE 2: If we want to persue this direction, a overlay-style
* UI could be implemented here to describe what key-presses
* are available. See https://excalidraw.com/ for a nice example
* of this.
*/


@connect((state) => ({
colorBy: state.controls.colorBy,
geoResolution: state.controls.geoResolution
}))
class KeyboardShortcuts extends React.Component {
constructor(props) {
super(props);
this.rafId = 0;
}
componentDidMount() {
Mousetrap.bind(['c'], () => {this.props.dispatch(changeToNextColorBy());});
Mousetrap.bind(['s c', 'S C', 's r', 'S R'], (e, combo) => {
this.props.dispatch(sampleTraitFromUncertainty({
trait: combo[2].toLowerCase() === 'c' ? this.props.colorBy : this.props.geoResolution,
returnToOriginal: combo[0]==="S"
}));
});
Mousetrap.bind(['x', 'e'], (e, combo) => {
if (this.rafId) {
window.cancelAnimationFrame(this.rafId);
this.rafId = 0;
return;
}
const cb = () => {
this.props.dispatch(sampleTraitFromUncertainty({
trait: combo==="x" ? this.props.colorBy : this.props.geoResolution
}));
this.rafId = window.requestAnimationFrame(cb);
};
cb();
});
}
componentWillUnmount() {
Mousetrap.unbind(['c', 's c', 'S C', 's r', 'S R']);
}
render() {
return null;
}
}

export default KeyboardShortcuts;
6 changes: 4 additions & 2 deletions src/components/map/map.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ import "../../css/mapbox.css";
state.controls.geoResolution !== state.controls.colorScale.colorBy // geo circles match colorby == no pie chart
),
legendValues: state.controls.colorScale.legendValues,
showTransmissionLines: state.controls.showTransmissionLines
showTransmissionLines: state.controls.showTransmissionLines,
resamplingCounter: state.controls.resamplingCounter
};
})

Expand Down Expand Up @@ -276,7 +277,8 @@ class Map extends React.Component {
const transmissionLinesToggleChanged = this.props.showTransmissionLines !== nextProps.showTransmissionLines;
const dataChanged = (!nextProps.treeLoaded || this.props.treeVersion !== nextProps.treeVersion);
const colorByChanged = (nextProps.colorScaleVersion !== this.props.colorScaleVersion);
if (mapIsDrawn && (geoResolutionChanged || dataChanged || colorByChanged || transmissionLinesToggleChanged)) {
const traitResampling = this.props.resamplingCounter !== nextProps.resamplingCounter;
if (mapIsDrawn && (geoResolutionChanged || dataChanged || colorByChanged || transmissionLinesToggleChanged || traitResampling)) {
this.state.d3DOMNode.selectAll("*").remove();
this.setState({
d3elems: null,
Expand Down
3 changes: 2 additions & 1 deletion src/components/tree/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ const Tree = connect((state) => ({
panelsToDisplay: state.controls.panelsToDisplay,
selectedBranchLabel: state.controls.selectedBranchLabel,
narrativeMode: state.narrative.display,
animationPlayPauseButton: state.controls.animationPlayPauseButton
animationPlayPauseButton: state.controls.animationPlayPauseButton,
resamplingCounter: state.controls.resamplingCounter
}))(UnconnectedTree);

export default Tree;
5 changes: 5 additions & 0 deletions src/components/tree/reactD3Interface/change.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ export const changePhyloTreeViaPropsComparison = (mainTree, phylotree, oldProps,
}
}

/* todo - this is too hacky */
if (newProps.distanceMeasure === "num_date" && newProps.colorBy === "num_date" && oldProps.resamplingCounter !== newProps.resamplingCounter) {
args.newDistance = true;
}

if (oldProps.width !== newProps.width || oldProps.height !== newProps.height) {
args.svgHasChangedDimensions = true;
}
Expand Down
5 changes: 4 additions & 1 deletion src/reducers/controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ export const getDefaultControlsState = () => {
mapLegendOpen: undefined,
showOnlyPanels: false,
showTransmissionLines: true,
normalizeFrequencies: true
normalizeFrequencies: true,
resamplingCounter: 0,
};
};

Expand Down Expand Up @@ -113,6 +114,8 @@ const Controls = (state = getDefaultControlsState(), action) => {
return Object.assign({}, state, {
selectedNode: null
});
case types.RESAMPLE:
return {...state, resamplingCounter: state.resamplingCounter+1};
case types.CHANGE_BRANCH_LABEL:
return Object.assign({}, state, { selectedBranchLabel: action.value });
case types.CHANGE_LAYOUT:
Expand Down
7 changes: 6 additions & 1 deletion src/reducers/tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,13 @@ const Tree = (state = getDefaultTreeState(), action) => {
case types.NEW_COLORS:
return Object.assign({}, state, {
nodeColors: action.nodeColors,
nodeColorsVersion: action.version
nodeColorsVersion: action.nodeColorsVersion
});
case types.RESAMPLE:
if (action.nodeColors) {
return {...state, nodeColors: action.nodeColors, nodeColorsVersion: action.nodeColorsVersion};
}
return state;
case types.TREE_TOO_DATA:
return action.tree;
case types.ADD_COLOR_BYS:
Expand Down
2 changes: 1 addition & 1 deletion src/reducers/treeToo.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const treeToo = (state = getDefaultTreeState(), action) => {
if (action.nodeColorsToo) {
return Object.assign({}, state, {
nodeColors: action.nodeColorsToo,
nodeColorsVersion: action.version
nodeColorsVersion: action.nodeColorsVersion
});
}
return state;
Expand Down
2 changes: 2 additions & 0 deletions src/root.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { lazy, Suspense } from 'react';
import { connect } from "react-redux";
import { hot } from 'react-hot-loader/root';
import Monitor from "./components/framework/monitor";
import KeyboardShortcuts from "./components/framework/keyboard-shortcuts";
import DatasetLoader from "./components/datasetLoader";
import Spinner from "./components/framework/spinner";
import Head from "./components/framework/head";
Expand Down Expand Up @@ -56,6 +57,7 @@ const Root = () => {
<div>
<Head/>
<Monitor/>
<KeyboardShortcuts/>
<Notifications/>
<MainComponentSwitch/>
</div>
Expand Down