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

Measurements URL query params #1848

Merged
merged 14 commits into from
Sep 12, 2024
Merged
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
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