Skip to content

Commit

Permalink
Merge pull request #1278 from nextstrain/frequencies-default
Browse files Browse the repository at this point in the history
Enforce unnormalized frequencies when data is lacking
  • Loading branch information
trvrb authored Mar 12, 2021
2 parents fe167e3 + 658f8dc commit 058d23f
Show file tree
Hide file tree
Showing 7 changed files with 68 additions and 11 deletions.
14 changes: 10 additions & 4 deletions src/actions/frequencies.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { debounce } from 'lodash';
import * as types from "./types";
import { timerStart, timerEnd } from "../util/perf";
import { computeMatrixFromRawData, processFrequenciesJSON } from "../util/processFrequencies";
import { computeMatrixFromRawData, checkIfNormalizableFromRawData, processFrequenciesJSON } from "../util/processFrequencies";

export const loadFrequencies = (json) => (dispatch, getState) => {
const { tree, controls } = getState();
const { data, pivots, matrix, projection_pivot, normalizeFrequencies } = processFrequenciesJSON(json, tree, controls);
dispatch({
type: types.LOAD_FREQUENCIES,
frequencies: {loaded: true, version: 1, ...processFrequenciesJSON(json, tree, controls)}
frequencies: {loaded: true, version: 1, data, pivots, matrix, projection_pivot},
normalizeFrequencies
});
};

Expand All @@ -22,17 +24,21 @@ const updateFrequencyData = (dispatch, getState) => {
console.error("Race condition in updateFrequencyData. Frequencies data not in state. Matrix can't be calculated.");
return;
}

const normalizeFrequencies = controls.normalizeFrequencies &&
checkIfNormalizableFromRawData(frequencies.data, frequencies.pivots, tree.nodes, tree.visibility);

const matrix = computeMatrixFromRawData(
frequencies.data,
frequencies.pivots,
tree.nodes,
tree.visibility,
controls.colorScale,
controls.colorBy,
controls.normalizeFrequencies
normalizeFrequencies
);
timerEnd("updateFrequencyData");
dispatch({type: types.FREQUENCY_MATRIX, matrix});
dispatch({type: types.FREQUENCY_MATRIX, matrix, normalizeFrequencies});
};

/* debounce works better than throttle, as it _won't_ update while events are still coming in (e.g. dragging the date slider) */
Expand Down
14 changes: 13 additions & 1 deletion src/actions/recomputeReduxState.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { treeJsonToState } from "../util/treeJsonProcessing";
import { entropyCreateState } from "../util/entropyCreateStateFromJsons";
import { determineColorByGenotypeMutType, calcNodeColor } from "../util/colorHelpers";
import { calcColorScale, createVisibleLegendValues } from "../util/colorScale";
import { computeMatrixFromRawData } from "../util/processFrequencies";
import { computeMatrixFromRawData, checkIfNormalizableFromRawData } from "../util/processFrequencies";
import { applyInViewNodesToTree } from "../actions/tree";
import { isColorByGenotype, decodeColorByGenotype, decodeGenotypeFilters, encodeGenotypeFilters } from "../util/getGenotype";
import { getTraitFromNode, getDivFromNode, collectGenotypeStates } from "../util/treeMiscHelpers";
Expand Down Expand Up @@ -830,6 +830,18 @@ export const createStateFromQueryOrJSONs = ({
/* update frequencies if they exist (not done for new JSONs) */
if (frequencies && frequencies.loaded) {
frequencies.version++;

const allowNormalization = checkIfNormalizableFromRawData(
frequencies.data,
frequencies.pivots,
tree.nodes,
tree.visibility
);

if (!allowNormalization) {
controls.normalizeFrequencies = false;
}

frequencies.matrix = computeMatrixFromRawData(
frequencies.data,
frequencies.pivots,
Expand Down
18 changes: 17 additions & 1 deletion src/components/controls/frequency-normalization.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { withTranslation } from "react-i18next";
import Toggle from "./toggle";
import { controlsWidth } from "../../util/globals";
import { FREQUENCY_MATRIX } from "../../actions/types";
import { computeMatrixFromRawData } from "../../util/processFrequencies";
import { computeMatrixFromRawData, checkIfNormalizableFromRawData } from "../../util/processFrequencies";
import { SidebarSubtitle } from "./styles";

@connect((state) => {
return {
Expand All @@ -17,13 +18,28 @@ import { computeMatrixFromRawData } from "../../util/processFrequencies";
class NormalizeFrequencies extends React.Component {
render() {
const { t } = this.props;

const allowNormalization = this.props.frequencies.loaded && this.props.tree.loaded &&
checkIfNormalizableFromRawData(
this.props.frequencies.data,
this.props.frequencies.pivots,
this.props.tree.nodes,
this.props.tree.visibility
);
if (!allowNormalization) {
return (
<SidebarSubtitle>(Frequencies cannot be normalized)</SidebarSubtitle>
);
}

return (
<div style={{marginBottom: 10, width: controlsWidth, fontSize: 14}}>
<Toggle
display
on={this.props.controls.normalizeFrequencies}
callback={() => {
const normalizeFrequencies = !this.props.controls.normalizeFrequencies;

const matrix = computeMatrixFromRawData(
this.props.frequencies.data,
this.props.frequencies.pivots,
Expand Down
1 change: 1 addition & 0 deletions src/components/controls/miscInfoText.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,6 @@ export const PanelOptionsInfo = (
export const FrequencyInfo = (
<>
<em>Normalize frequencies</em> controls whether the vertical axis represents the entire dataset or only the samples currently visible (e.g. due to filtering).
This option is not available when data is limited to prevent numerical issues.
</>
);
4 changes: 1 addition & 3 deletions src/components/frequencies/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import 'd3-transition';
import { connect } from "react-redux";
import Card from "../framework/card";
import { calcXScale, calcYScale, drawXAxis, drawYAxis, drawProjectionInfo,
areListsEqual, drawStream, processMatrix, parseColorBy, normString } from "./functions";
drawStream, processMatrix, parseColorBy, normString } from "./functions";
import "../../css/entropy.css";

@connect((state) => {
Expand Down Expand Up @@ -50,8 +50,6 @@ class Frequencies extends React.Component {
/* we don't have to check width / height changes here - that's done in componentDidUpdate */
const data = processMatrix({...newProps});
const maxYChange = oldState.maxY !== data.maxY;
const catChange = !areListsEqual(oldState.categories, data.categories);
if (!maxYChange && !catChange) return false;
const chartGeom = this.calcChartGeom(newProps.width, newProps.height);
/* should the y scale be updated? */
let newScales;
Expand Down
2 changes: 2 additions & 0 deletions src/reducers/controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,8 @@ const Controls = (state = getDefaultControlsState(), action) => {
case types.TOGGLE_TRANSMISSION_LINES:
return Object.assign({}, state, { showTransmissionLines: action.data });

case types.LOAD_FREQUENCIES:
return {...state, normalizeFrequencies: action.normalizeFrequencies};
case types.FREQUENCY_MATRIX: {
if (Object.hasOwnProperty.call(action, "normalizeFrequencies")) {
return Object.assign({}, state, { normalizeFrequencies: action.normalizeFrequencies });
Expand Down
26 changes: 24 additions & 2 deletions src/util/processFrequencies.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,23 @@ const assignCategory = (colorScale, categories, node, colorBy, isGenotype) => {
return unassigned_label;
};

// Returns a boolean specifying if frequencies are allowed to be normalized
// Only normalize if minimum frequency is above 0.1%
export const checkIfNormalizableFromRawData = (data, pivots, nodes, visibility) => {
const pivotsLen = pivots.length;
const pivotTotals = new Array(pivotsLen).fill(0);
data.forEach((d) => {
if (visibility[d.idx] === NODE_VISIBLE) {
for (let i = 0; i < pivotsLen; i++) {
pivotTotals[i] += d.values[i];
}
}
});
const minFrequency = Math.min(...pivotTotals);
const allowNormalization = minFrequency > 0.001;
return allowNormalization;
};

export const computeMatrixFromRawData = (data, pivots, nodes, visibility, colorScale, colorBy, normalizeFrequencies) => {
/* color scale domain forms the categories in the stream graph */
const categories = colorScale.legendValues.filter((d) => d !== undefined);
Expand Down Expand Up @@ -98,19 +115,24 @@ export const processFrequenciesJSON = (rawJSON, tree, controls) => {
weight: rawJSON[n.name].weight
});
});

const normalizeFrequencies = controls.normalizeFrequencies &&
checkIfNormalizableFromRawData(data, pivots, tree.nodes, tree.visibility);

const matrix = computeMatrixFromRawData(
data,
pivots,
tree.nodes,
tree.visibility,
controls.colorScale,
controls.colorBy,
controls.normalizeFrequencies
normalizeFrequencies
);
return {
data,
pivots,
matrix,
projection_pivot
projection_pivot,
normalizeFrequencies
};
};

0 comments on commit 058d23f

Please sign in to comment.