diff --git a/src/actions/frequencies.js b/src/actions/frequencies.js index 405e46c90..b49b2bf86 100644 --- a/src/actions/frequencies.js +++ b/src/actions/frequencies.js @@ -28,7 +28,8 @@ const updateFrequencyData = (dispatch, getState) => { tree.nodes, tree.visibility, controls.colorScale, - controls.colorBy + controls.colorBy, + controls.normalizeFrequencies ); timerEnd("updateFrequencyData"); dispatch({type: types.FREQUENCY_MATRIX, matrix}); diff --git a/src/actions/recomputeReduxState.js b/src/actions/recomputeReduxState.js index 274c88152..5fc361774 100644 --- a/src/actions/recomputeReduxState.js +++ b/src/actions/recomputeReduxState.js @@ -192,8 +192,8 @@ const modifyStateViaMetadata = (state, metadata) => { console.warn("JSON did not include any filters"); } if (metadata.displayDefaults) { - const keysToCheckFor = ["geoResolution", "colorBy", "distanceMeasure", "layout", "mapTriplicate", "selectedBranchLabel", 'sidebar', "showTransmissionLines"]; - const expectedTypes = ["string", "string", "string", "string", "boolean", "string", 'string', "boolean" ]; // eslint-disable-line + 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 for (let i = 0; i < keysToCheckFor.length; i += 1) { if (Object.hasOwnProperty.call(metadata.displayDefaults, keysToCheckFor[i])) { @@ -787,7 +787,8 @@ export const createStateFromQueryOrJSONs = ({ tree.nodes, tree.visibility, controls.colorScale, - controls.colorBy + controls.colorBy, + controls.normalizeFrequencies ); } diff --git a/src/components/controls/controls.js b/src/components/controls/controls.js index 01af38816..79a43e662 100644 --- a/src/components/controls/controls.js +++ b/src/components/controls/controls.js @@ -11,6 +11,7 @@ import ChooseMetric from "./choose-metric"; import PanelLayout from "./panel-layout"; import GeoResolution from "./geo-resolution"; import TransmissionLines from './transmission-lines'; +import NormalizeFrequencies from "./frequency-normalization"; import MapAnimationControls from "./map-animation"; import PanelToggles from "./panel-toggles"; import SearchStrains from "./search"; @@ -19,43 +20,48 @@ import Language from "./language"; import { SidebarHeader, ControlsContainer } from "./styles"; -function Controls({mapOn}) { +function Controls({mapOn, frequenciesOn}) { const { t } = useTranslation(); return ( - + {t("sidebar:Date Range")} - - + {t("sidebar:Color By")} - - + {t("sidebar:Tree Options")} - - - - - - - - { mapOn ? ( - + + + + + + + + {mapOn ? ( + {t("sidebar:Map Options")} - - - + + + + + ) : null} + + {frequenciesOn ? ( + + {t("sidebar:Frequency Options")} + ) : null} - + {t("sidebar:Panel Options")} - - - + + + ); } diff --git a/src/components/controls/frequency-normalization.js b/src/components/controls/frequency-normalization.js new file mode 100644 index 000000000..bdaa22193 --- /dev/null +++ b/src/components/controls/frequency-normalization.js @@ -0,0 +1,45 @@ +import React from "react"; +import { connect } from "react-redux"; +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"; + +@connect((state) => { + return { + controls: state.controls, + frequencies: state.frequencies, + tree: state.tree + }; +}) +class NormalizeFrequencies extends React.Component { + render() { + const { t } = this.props; + return ( +
+ { + const normalizeFrequencies = !this.props.controls.normalizeFrequencies; + const matrix = computeMatrixFromRawData( + this.props.frequencies.data, + this.props.frequencies.pivots, + this.props.tree.nodes, + this.props.tree.visibility, + this.props.controls.colorScale, + this.props.controls.colorBy, + normalizeFrequencies + ); + this.props.dispatch({ type: FREQUENCY_MATRIX, matrix, normalizeFrequencies }); + }} + label={t("sidebar:Normalize frequencies")} + /> +
+ ); + } +} + +export default withTranslation()(NormalizeFrequencies); diff --git a/src/components/frequencies/functions.js b/src/components/frequencies/functions.js index 62d3061a4..c4f9ca716 100644 --- a/src/components/frequencies/functions.js +++ b/src/components/frequencies/functions.js @@ -32,6 +32,16 @@ export const parseColorBy = (colorBy, colorOptions) => { return colorBy; }; +export const normString = (normalized, tipCount, fullTipCount) => { + if (tipCount { /* get the colorBy's in the same order as in the tree legend */ const orderedCategories = colorScale.legendValues diff --git a/src/components/frequencies/index.js b/src/components/frequencies/index.js index 170e972d2..b6001fc18 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 } from "./functions"; + areListsEqual, drawStream, processMatrix, parseColorBy, normString } from "./functions"; import "../../css/entropy.css"; @connect((state) => { @@ -13,14 +13,17 @@ import "../../css/entropy.css"; data: state.frequencies.data, pivots: state.frequencies.pivots, matrix: state.frequencies.matrix, + nodes: state.tree.nodes, projection_pivot: state.frequencies.projection_pivot, version: state.frequencies.version, browserDimensions: state.browserDimensions.browserDimensions, colorBy: state.controls.colorBy, colorScale: state.controls.colorScale, - colorOptions: state.metadata.colorings + colorOptions: state.metadata.colorings, + normalizeFrequencies: state.controls.normalizeFrequencies }; }) + class Frequencies extends React.Component { constructor(props) { super(props); @@ -99,8 +102,9 @@ class Frequencies extends React.Component { } render() { const { t } = this.props; + const {tipCount, fullTipCount} = this.props.nodes[0]; return ( - +
- {displayNarrative ? - : + {displayNarrative ? ( + + ) : ( - } + )} ); diff --git a/src/reducers/controls.js b/src/reducers/controls.js index 1e2c756c6..b6f30df37 100644 --- a/src/reducers/controls.js +++ b/src/reducers/controls.js @@ -45,7 +45,7 @@ export const getDefaultControlsState = () => { strain: null, geneLength: {}, mutType: defaultMutType, - temporalConfidence: {exists: false, display: false, on: false}, + temporalConfidence: { exists: false, display: false, on: false }, layout: defaults.layout, distanceMeasure: defaults.distanceMeasure, dateMin, @@ -57,7 +57,7 @@ export const getDefaultControlsState = () => { absoluteDateMax: dateMax, absoluteDateMaxNumeric: dateMaxNumeric, colorBy: defaults.colorBy, - colorByConfidence: {display: false, on: false}, + colorByConfidence: { display: false, on: false }, colorScale: undefined, selectedBranchLabel: "none", analysisSlider: false, @@ -82,7 +82,8 @@ export const getDefaultControlsState = () => { treeLegendOpen: undefined, mapLegendOpen: undefined, showOnlyPanels: false, - showTransmissionLines: true + showTransmissionLines: true, + normalizeFrequencies: true }; }; @@ -95,7 +96,7 @@ const Controls = (state = getDefaultControlsState(), action) => { case types.CLEAN_START: return action.controls; case types.SET_AVAILABLE: - return Object.assign({}, state, {available: action.data}); + return Object.assign({}, state, { available: action.data }); case types.BRANCH_MOUSEENTER: return Object.assign({}, state, { selectedBranch: action.data @@ -113,28 +114,40 @@ const Controls = (state = getDefaultControlsState(), action) => { selectedNode: null }); case types.CHANGE_BRANCH_LABEL: - return Object.assign({}, state, {selectedBranchLabel: action.value}); + return Object.assign({}, state, { selectedBranchLabel: action.value }); case types.CHANGE_LAYOUT: return Object.assign({}, state, { layout: action.data, /* temporal confidence can only be displayed for rectangular trees */ temporalConfidence: Object.assign({}, state.temporalConfidence, { - display: shouldDisplayTemporalConfidence(state.temporalConfidence.exists, state.distanceMeasure, action.data), - on: false}) + display: shouldDisplayTemporalConfidence( + state.temporalConfidence.exists, + state.distanceMeasure, + action.data + ), + on: false + }) }); case types.CHANGE_DISTANCE_MEASURE: const updatesToState = { distanceMeasure: action.data, branchLengthsToDisplay: state.branchLengthsToDisplay }; - if (shouldDisplayTemporalConfidence(state.temporalConfidence.exists, action.data, state.layout)) { - updatesToState.temporalConfidence = Object.assign({}, state.temporalConfidence, {display: true}); + if ( + shouldDisplayTemporalConfidence(state.temporalConfidence.exists, action.data, state.layout) + ) { + updatesToState.temporalConfidence = Object.assign({}, state.temporalConfidence, { + display: true + }); } else { - updatesToState.temporalConfidence = Object.assign({}, state.temporalConfidence, {display: false, on: false}); + updatesToState.temporalConfidence = Object.assign({}, state.temporalConfidence, { + display: false, + on: false + }); } return Object.assign({}, state, updatesToState); case types.CHANGE_DATES_VISIBILITY_THICKNESS: { - const newDates = {quickdraw: action.quickdraw}; + const newDates = { quickdraw: action.quickdraw }; if (action.dateMin) { newDates.dateMin = action.dateMin; newDates.dateMinNumeric = action.dateMinNumeric; @@ -186,7 +199,9 @@ const Controls = (state = getDefaultControlsState(), action) => { return Object.assign({}, state, { panelsToDisplay: action.panelsToDisplay, panelLayout: action.panelLayout, - canTogglePanelLayout: action.panelsToDisplay.indexOf("tree") !== -1 && action.panelsToDisplay.indexOf("map") !== -1 + canTogglePanelLayout: + action.panelsToDisplay.indexOf("tree") !== -1 && + action.panelsToDisplay.indexOf("map") !== -1 }); case types.NEW_COLORS: { const newState = Object.assign({}, state, { @@ -235,20 +250,27 @@ const Controls = (state = getDefaultControlsState(), action) => { }); case types.TOGGLE_TANGLE: if (state.showTreeToo) { - return Object.assign({}, state, {showTangle: !state.showTangle}); + return Object.assign({}, state, { showTangle: !state.showTangle }); } return state; case types.TOGGLE_SIDEBAR: - return Object.assign({}, state, {sidebarOpen: action.value}); + return Object.assign({}, state, { sidebarOpen: action.value }); case types.TOGGLE_LEGEND: - return Object.assign({}, state, {legendOpen: action.value}); + return Object.assign({}, state, { legendOpen: action.value }); case types.ADD_COLOR_BYS: for (const colorBy of Object.keys(action.newColorings)) { state.coloringsPresentOnTree.add(colorBy); } - return Object.assign({}, state, {coloringsPresentOnTree: state.coloringsPresentOnTree}); + return Object.assign({}, state, { coloringsPresentOnTree: state.coloringsPresentOnTree }); case types.TOGGLE_TRANSMISSION_LINES: - return Object.assign({}, state, {showTransmissionLines: action.data}); + return Object.assign({}, state, { showTransmissionLines: action.data }); + + case types.FREQUENCY_MATRIX: { + if (Object.hasOwnProperty.call(action, "normalizeFrequencies")) { + return Object.assign({}, state, { normalizeFrequencies: action.normalizeFrequencies }); + } + return state; + } default: return state; } diff --git a/src/util/processFrequencies.js b/src/util/processFrequencies.js index 0cd55057f..3df05e8d1 100644 --- a/src/util/processFrequencies.js +++ b/src/util/processFrequencies.js @@ -34,7 +34,7 @@ const assignCategory = (colorScale, categories, node, colorBy, isGenotype) => { return unassigned_label; }; -export const computeMatrixFromRawData = (data, pivots, nodes, visibility, colorScale, colorBy) => { +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); categories.push(unassigned_label); /* for tips without a colorBy */ @@ -59,6 +59,16 @@ export const computeMatrixFromRawData = (data, pivots, nodes, visibility, colorS } }); + if (normalizeFrequencies) { + const nCategories = Object.keys(matrix).length; + const minVal = 1e-10; + Object.keys(matrix).forEach((cat) => { + debugPivotTotals.forEach((norm, i) => { + matrix[cat][i] = (matrix[cat][i] + minVal) / (nCategories * minVal + norm); + }); + }); + } + if (matrix[unassigned_label].reduce((a, b) => a + b, 0) === 0) { delete matrix[unassigned_label]; } @@ -94,7 +104,8 @@ export const processFrequenciesJSON = (rawJSON, tree, controls) => { tree.nodes, tree.visibility, controls.colorScale, - controls.colorBy + controls.colorBy, + controls.normalizeFrequencies ); return { data,