diff --git a/src/actions/frequencies.js b/src/actions/frequencies.js index b49b2bf86..8987bc1fb 100644 --- a/src/actions/frequencies.js +++ b/src/actions/frequencies.js @@ -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 }); }; @@ -22,6 +24,10 @@ 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, @@ -29,10 +35,10 @@ const updateFrequencyData = (dispatch, getState) => { 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) */ diff --git a/src/actions/recomputeReduxState.js b/src/actions/recomputeReduxState.js index 6fc0d02bd..d479ff0b4 100644 --- a/src/actions/recomputeReduxState.js +++ b/src/actions/recomputeReduxState.js @@ -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"; @@ -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, diff --git a/src/components/controls/frequency-normalization.js b/src/components/controls/frequency-normalization.js index bdaa22193..e9bcbfb54 100644 --- a/src/components/controls/frequency-normalization.js +++ b/src/components/controls/frequency-normalization.js @@ -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 { @@ -17,6 +18,20 @@ 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 ( + (Frequencies cannot be normalized) + ); + } + return (
{ const normalizeFrequencies = !this.props.controls.normalizeFrequencies; + const matrix = computeMatrixFromRawData( this.props.frequencies.data, this.props.frequencies.pivots, diff --git a/src/components/controls/miscInfoText.js b/src/components/controls/miscInfoText.js index 517199042..2ed4bac57 100644 --- a/src/components/controls/miscInfoText.js +++ b/src/components/controls/miscInfoText.js @@ -38,5 +38,6 @@ export const PanelOptionsInfo = ( export const FrequencyInfo = ( <> Normalize frequencies 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. ); diff --git a/src/components/frequencies/index.js b/src/components/frequencies/index.js index b6001fc18..3aa4833fe 100644 --- a/src/components/frequencies/index.js +++ b/src/components/frequencies/index.js @@ -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) => { @@ -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; diff --git a/src/reducers/controls.js b/src/reducers/controls.js index e5942e415..e027578bc 100644 --- a/src/reducers/controls.js +++ b/src/reducers/controls.js @@ -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 }); diff --git a/src/util/processFrequencies.js b/src/util/processFrequencies.js index 3df05e8d1..8a06cfc36 100644 --- a/src/util/processFrequencies.js +++ b/src/util/processFrequencies.js @@ -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); @@ -98,6 +115,10 @@ 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, @@ -105,12 +126,13 @@ export const processFrequenciesJSON = (rawJSON, tree, controls) => { tree.visibility, controls.colorScale, controls.colorBy, - controls.normalizeFrequencies + normalizeFrequencies ); return { data, pivots, matrix, - projection_pivot + projection_pivot, + normalizeFrequencies }; };