Skip to content

Commit

Permalink
Merge pull request #1848 from nextstrain/measurements-url
Browse files Browse the repository at this point in the history
Measurements URL query params
  • Loading branch information
joverlee521 authored Sep 12, 2024
2 parents 30c60db + 7924946 commit de94b72
Show file tree
Hide file tree
Showing 9 changed files with 449 additions and 107 deletions.
22 changes: 22 additions & 0 deletions docs/advanced-functionality/view-settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,25 @@ URL queries are the part of the URL coming after the ``?`` character, and typica
**See this in action:**

For instance, go to `nextstrain.org/flu/seasonal/h3n2/ha/2y?c=num_date&d=tree,map&m=div&r=region <https://nextstrain.org/flu/seasonal/h3n2/ha/2y?c=num_date&d=tree,map&m=div&p=grid&r=region>`__ and you'll see how we've changed the coloring to a temporal scale (``c=num_date``), we're only showing the tree & map panels (``d=tree,map``), the tree x-axis is divergence (``m=div``) and the map resolution is region (``r=region``).

Measurements panel URL query options
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The following query options are specifically for the measurements panel.

+----------------------+-----------------------------------------------------------+--------------------------------------------------------------+
| Key | Description | Example(s) |
+======================+===========================================================+==============================================================+
| ``m_collection`` | Specify which collection to display | ``m_collection=h3n2_ha_cell_hi`` |
+----------------------+-----------------------------------------------------------+--------------------------------------------------------------+
| ``m_display`` | Toggle measurements display between mean and raw | ``m_display=mean`` or ``m_display=raw`` |
+----------------------+-----------------------------------------------------------+--------------------------------------------------------------+
| ``m_groupBy`` | Specify group by field to use | ``m_groupBy=reference_strain`` |
+----------------------+-----------------------------------------------------------+--------------------------------------------------------------+
| ``m_overallMean`` | Show or hide the overall mean display | ``m_overallMean=show`` or ``m_overallMean=hide`` |
+----------------------+-----------------------------------------------------------+--------------------------------------------------------------+
| ``m_threshold`` | Show or hide the threshold(s) | ``m_threshold=show`` or ``m_threshold=hide`` |
+----------------------+-----------------------------------------------------------+--------------------------------------------------------------+
| ``mf_<field_name>`` | | Filters for the measurements data. Multiple values for | | ``mf_reference_strain=A/Alabama/5/2010`` |
| | | the same field are specified by multiple query params | | ``mf_clade_reference=145S.2&mf_clade_reference=158N/189K`` |
+----------------------+-----------------------------------------------------------+--------------------------------------------------------------+
398 changes: 329 additions & 69 deletions src/actions/measurements.js

Large diffs are not rendered by default.

30 changes: 26 additions & 4 deletions src/actions/recomputeReduxState.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { getTraitFromNode, getDivFromNode, collectGenotypeStates } from "../util
import { collectAvailableTipLabelOptions } from "../components/controls/choose-tip-label";
import { hasMultipleGridPanels } from "./panelDisplay";
import { strainSymbolUrlString } from "../middleware/changeURL";
import { createMeasurementsControlsFromQuery, getCollectionDefaultControls, getCollectionToDisplay } from "./measurements";

export const doesColorByHaveConfidence = (controlsState, colorBy) =>
controlsState.coloringsPresentOnTreeWithConfidence.has(colorBy);
Expand Down Expand Up @@ -146,8 +147,8 @@ const modifyStateViaURLQuery = (state, query) => {
const [_dmin, _dminNum] = [params[0], calendarToNumeric(params[0])];
const [_dmax, _dmaxNum] = [params[1], calendarToNumeric(params[1])];
if (
!_validDate(_dminNum, state.absoluteDateMinNumeric, state.absoluteDateMaxNumeric) ||
!_validDate(_dmaxNum, state.absoluteDateMinNumeric, state.absoluteDateMaxNumeric) ||
!_validDate(_dminNum, state.absoluteDateMinNumeric, state.absoluteDateMaxNumeric) ||
!_validDate(_dmaxNum, state.absoluteDateMinNumeric, state.absoluteDateMaxNumeric) ||
_dminNum >= _dmaxNum
) {
console.error("Invalid 'animate' URL query (invalid date range)")
Expand Down Expand Up @@ -206,8 +207,11 @@ const modifyStateViaURLQuery = (state, query) => {
if (query.regression==="hide") state.scatterVariables.showRegression = false;
if (query.scatterX) state.scatterVariables.x = query.scatterX;
if (query.scatterY) state.scatterVariables.y = query.scatterY;
return state;

/* Process query params for measurements panel. These all start with `m_` or `mf_` prefix to avoid conflicts */
state = {...state, ...createMeasurementsControlsFromQuery(query)}

return state;
function _validDate(dateNum, absoluteDateMinNumeric, absoluteDateMaxNumeric) {
return !(dateNum===undefined || dateNum > absoluteDateMaxNumeric || dateNum < absoluteDateMinNumeric);
}
Expand Down Expand Up @@ -899,6 +903,12 @@ export const createStateFromQueryOrJSONs = ({
measurements = {...oldState.measurements};
controls = restoreQueryableStateToDefaults(controls);
controls = modifyStateViaMetadata(controls, metadata, entropy.genomeMap);
/* If available, reset to the default collection and the collection's default controls
so that narrative queries are respected between slides */
if (measurements.loaded) {
measurements.collectionToDisplay = getCollectionToDisplay(measurements.collections, "", measurements.defaultCollectionKey)
controls = {...controls, ...getCollectionDefaultControls(measurements.collectionToDisplay)};
}
}

/* For the creation of state, we want to parse out URL query parameters
Expand All @@ -912,6 +922,18 @@ export const createStateFromQueryOrJSONs = ({
narrativeSlideIdx = getNarrativePageFromQuery(query, narrative);
/* replace the query with the information which can guide the view */
query = queryString.parse(narrative[narrativeSlideIdx].query);
/**
* Special case where narrative includes query param for new measurements collection `m_collection`
* We need to reset the measurements and controls to the new collection's defaults before
* processing the remaining query params
*/
if (query.m_collection && measurements.loaded) {
const newCollectionToDisplay = getCollectionToDisplay(measurements.collections, query.m_collection, measurements.defaultCollectionKey);
measurements.collectionToDisplay = newCollectionToDisplay;
controls = {...controls, ...getCollectionDefaultControls(measurements.collectionToDisplay)};
// Delete `m_collection` so there's no chance of things getting mixed up when processing remaining query params
delete query.m_collection;
}
}

controls = modifyStateViaURLQuery(controls, query);
Expand Down Expand Up @@ -1096,4 +1118,4 @@ function updateSecondTree(tree, treeToo, controls, dispatch) {
controls.panelLayout = "full";

return treeToo;
}
}
3 changes: 2 additions & 1 deletion src/components/controls/filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ class FilterData extends React.Component {
...(this.props.totalStateCounts[traitName]?.keys() || []),
...(this.props.totalStateCountsSecondTree?.[traitName]?.keys() || []),
]);

this.props.totalStateCounts[traitName];
const traitTitle = this.getFilterTitle(traitName);
const filterValuesCurrentlyActive = new Set((this.props.activeFilters[traitName] || []).filter((x) => x.active).map((x) => x.value));
Expand Down Expand Up @@ -175,6 +175,7 @@ class FilterData extends React.Component {
});
}
summariseMeasurementsFilters = () => {
if (this.props.measurementsFieldsMap === undefined) return [];
return Object.entries(this.props.measurementsFilters).map(([field, valuesMap]) => {
const activeFiltersCount = Array.from(valuesMap.values()).reduce((prevCount, currentValue) => {
return currentValue.active ? prevCount + 1 : prevCount;
Expand Down
27 changes: 11 additions & 16 deletions src/components/controls/measurementsOptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import React from "react";
import { useSelector } from "react-redux";
import { useAppDispatch } from "../../hooks";
import { isEqual } from "lodash";
import { changeMeasurementsCollection } from "../../actions/measurements";
import {
CHANGE_MEASUREMENTS_DISPLAY,
CHANGE_MEASUREMENTS_GROUP_BY,
TOGGLE_MEASUREMENTS_OVERALL_MEAN,
TOGGLE_MEASUREMENTS_THRESHOLD
} from "../../actions/types";
changeMeasurementsCollection,
changeMeasurementsDisplay,
changeMeasurementsGroupBy,
toggleOverallMean,
toggleThreshold
} from "../../actions/measurements";
import { controlsWidth } from "../../util/globals";
import { SidebarSubtitle, SidebarButton } from "./styles";
import Toggle from "./toggle";
Expand Down Expand Up @@ -81,26 +81,21 @@ const MeasurementsOptions = () => {
isClearable={false}
isSearchable={false}
isMulti={false}
onChange={(opt) => {
dispatch({
type: CHANGE_MEASUREMENTS_GROUP_BY,
data: opt.value
});
}}
onChange={(opt) => {dispatch(changeMeasurementsGroupBy(opt.value));}}
/>
</div>
<SidebarSubtitle>
{"Measurements Display"}
</SidebarSubtitle>
<SidebarButton
selected={display === "mean"}
onClick={() => {dispatch({ type: CHANGE_MEASUREMENTS_DISPLAY, data: "mean" });}}
onClick={() => {dispatch(changeMeasurementsDisplay("mean"));}}
>
{"Mean ± SD"}
</SidebarButton>
<SidebarButton
selected={display === "raw"}
onClick={() => {dispatch({ type: CHANGE_MEASUREMENTS_DISPLAY, data: "raw" });}}
onClick={() => {dispatch(changeMeasurementsDisplay("raw"));}}
>
{"Raw"}
</SidebarButton>
Expand All @@ -109,7 +104,7 @@ const MeasurementsOptions = () => {
display
on={showOverallMean}
label="Show overall mean ± SD"
callback={() => dispatch({type: TOGGLE_MEASUREMENTS_OVERALL_MEAN, data: !showOverallMean})}
callback={() => dispatch(toggleOverallMean())}
/>
<Toggle
// Only display threshold toggle if the collection has a valid threshold
Expand All @@ -119,7 +114,7 @@ const MeasurementsOptions = () => {
}
on={showThreshold}
label="Show measurement threshold(s)"
callback={() => dispatch({type: TOGGLE_MEASUREMENTS_THRESHOLD, data: !showThreshold})}
callback={() => dispatch(toggleThreshold())}
/>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/info/filtersSummary.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ class FiltersSummary extends React.Component {
{". "}
</>
}
{Object.keys(this.props.measurementsFilters).length > 0 &&
{(Object.keys(this.props.measurementsFilters).length > 0 && this.props.measurementsFields !== undefined) &&
<>
<br/>
{t("Measurements filtered to") + " "}
Expand Down
13 changes: 13 additions & 0 deletions src/middleware/changeURL.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { numericToCalendar } from "../util/dateHelpers";
import { shouldDisplayTemporalConfidence } from "../reducers/controls";
import { genotypeSymbol, nucleotide_gene, strainSymbol } from "../util/globals";
import { encodeGenotypeFilters, decodeColorByGenotype, isColorByGenotype } from "../util/getGenotype";
import { removeInvalidMeasurementsFilterQuery } from "../actions/measurements";

export const strainSymbolUrlString = "__strain__";

Expand Down Expand Up @@ -222,6 +223,18 @@ export const changeURLMiddleware = (store) => (next) => (action) => {
}
break;
}
case types.LOAD_MEASUREMENTS: // fallthrough
case types.CHANGE_MEASUREMENTS_COLLECTION: // fallthrough
case types.APPLY_MEASUREMENTS_FILTER:
query = removeInvalidMeasurementsFilterQuery(query, action.queryParams)
query = {...query, ...action.queryParams};
break;
case types.CHANGE_MEASUREMENTS_DISPLAY: // fallthrough
case types.CHANGE_MEASUREMENTS_GROUP_BY: // fallthrough
case types.TOGGLE_MEASUREMENTS_OVERALL_MEAN: // fallthrough
case types.TOGGLE_MEASUREMENTS_THRESHOLD: // fallthrough
query = {...query, ...action.queryParams};
break;
default:
break;
}
Expand Down
59 changes: 43 additions & 16 deletions src/reducers/controls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { calcBrowserDimensionsInitialState } from "./browserDimensions";
import { doesColorByHaveConfidence } from "../actions/recomputeReduxState";
import { hasMultipleGridPanels } from "../actions/panelDisplay";

export interface ControlsState {
export interface BasicControlsState {
panelsAvailable: string[]
panelsToDisplay: string[]
showTreeToo: boolean
Expand All @@ -23,6 +23,19 @@ export interface ControlsState {
[propName: string]: any;
}

export interface MeasurementsControlState {
measurementsCollectionKey: string | undefined,
measurementsGroupBy: string | undefined,
measurementsDisplay: string | undefined,
measurementsShowOverallMean: boolean | undefined,
measurementsShowThreshold: boolean | undefined,
measurementsFilters: {
[key: string]: Map<string, {active: boolean}>
}
}

export interface ControlsState extends BasicControlsState, MeasurementsControlState {}

/* defaultState is a fn so that we can re-create it
at any time, e.g. if we want to revert things (e.g. on dataset change)
*/
Expand Down Expand Up @@ -99,14 +112,33 @@ export const getDefaultControlsState = () => {
showOnlyPanels: false,
showTransmissionLines: true,
normalizeFrequencies: true,
measurementsCollectionKey: undefined,
measurementsGroupBy: undefined,
measurementsDisplay: "mean",
measurementsShowOverallMean: true,
measurementsShowThreshold: true,
measurementsDisplay: undefined,
measurementsShowOverallMean: undefined,
measurementsShowThreshold: undefined,
measurementsFilters: {}
};
};

/**
* Keeping measurements control state separate from getDefaultControlsState
* in order to be able to differentiate when the page is loaded with and without
* URL params for the measurements panel.
*
* The initial control state is constructed then the URL params update the state.
* However, the measurements JSON is loaded after this, so it needs a way to
* differentiate the clean slate vs the added URL params.
*/
export const defaultMeasurementsControlState: MeasurementsControlState = {
measurementsCollectionKey: undefined,
measurementsGroupBy: undefined,
measurementsDisplay: "mean",
measurementsShowOverallMean: true,
measurementsShowThreshold: true,
measurementsFilters: {}
};

/* while this may change, div currently doesn't have CIs, so they shouldn't be displayed. */
export const shouldDisplayTemporalConfidence = (exists, distMeasure, layout) => exists && distMeasure === "num_date" && layout === "rect";

Expand Down Expand Up @@ -329,19 +361,14 @@ const Controls = (state: ControlsState = getDefaultControlsState(), action): Con
}
return state;
}
case types.LOAD_MEASUREMENTS: /* fallthrough */
case types.CHANGE_MEASUREMENTS_COLLECTION:
return {...state, ...action.controls};
case types.CHANGE_MEASUREMENTS_GROUP_BY:
return {...state, measurementsGroupBy: action.data};
case types.TOGGLE_MEASUREMENTS_THRESHOLD:
return {...state, measurementsShowThreshold: action.data};
case types.TOGGLE_MEASUREMENTS_OVERALL_MEAN:
return {...state, measurementsShowOverallMean: action.data};
case types.CHANGE_MEASUREMENTS_DISPLAY:
return {...state, measurementsDisplay: action.data};
case types.LOAD_MEASUREMENTS: // fallthrough
case types.CHANGE_MEASUREMENTS_COLLECTION: // fallthrough
case types.CHANGE_MEASUREMENTS_DISPLAY: // fallthrough
case types.CHANGE_MEASUREMENTS_GROUP_BY: // fallthrough
case types.TOGGLE_MEASUREMENTS_OVERALL_MEAN: // fallthrough
case types.TOGGLE_MEASUREMENTS_THRESHOLD: // fallthrough
case types.APPLY_MEASUREMENTS_FILTER:
return {...state, measurementsFilters: action.data};
return {...state, ...action.controls};
/**
* Currently the CHANGE_ZOOM action (entropy panel zoom changed) does not
* update the zoomMin/zoomMax, and as such they only represent the initially
Expand Down
2 changes: 2 additions & 0 deletions src/reducers/measurements.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
export const getDefaultMeasurementsState = () => ({
error: undefined,
loaded: false,
defaultCollectionKey: "",
collections: [],
collectionToDisplay: {}
});
Expand All @@ -20,6 +21,7 @@ const measurements = (state = getDefaultMeasurementsState(), action) => {
return {
...state,
loaded: true,
defaultCollectionKey: action.defaultCollectionKey,
collections: action.collections,
collectionToDisplay: action.collectionToDisplay
};
Expand Down

0 comments on commit de94b72

Please sign in to comment.