Skip to content

Commit

Permalink
Add combineMeasurementsControlsAndQuery
Browse files Browse the repository at this point in the history
Consolidate measurements controls and query param handling in
`combineMeasurementsControlsAndQuery`. Now that we load measurements
JSON upfront, we can use the collection's data to validate the query
params on initial load of the page. Handling all measurements
controls and query params in one place to make it easier to ensure
they are kept in sync.
  • Loading branch information
joverlee521 committed Oct 30, 2024
1 parent a2431ae commit 947eda6
Show file tree
Hide file tree
Showing 2 changed files with 89 additions and 54 deletions.
109 changes: 80 additions & 29 deletions src/actions/measurements.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
* @param {string} defaultKey
* @returns {Object}
*/
export const getCollectionToDisplay = (collections, collectionKey, defaultKey) => {
const getCollectionToDisplay = (collections, collectionKey, defaultKey) => {
const defaultCollection = collections.filter((collection) => collection.key === defaultKey)[0];
if (!collectionKey) return defaultCollection;
const potentialCollections = collections.filter((collection) => collection.key === collectionKey);
Expand Down Expand Up @@ -109,7 +109,7 @@ function getCollectionDefaultControl(controlKey, collection) {
* @param {Object} collection
* @returns {MeasurementsControlState}
*/
export function getCollectionDefaultControls(collection) {
function getCollectionDefaultControls(collection) {
const defaultControls = {...defaultMeasurementsControlState};
if (Object.keys(collection).length) {
for (const [key, value] of Object.entries(defaultControls)) {
Expand Down Expand Up @@ -447,7 +447,7 @@ export function removeInvalidMeasurementsFilterQuery(query, newQueryParams) {
return newQuery
}

export function createMeasurementsQueryFromControls(measurementControls, collection, defaultCollectionKey) {
function createMeasurementsQueryFromControls(measurementControls, collection, defaultCollectionKey) {
const newQuery = {
m_collection: collection.key === defaultCollectionKey ? "" : collection.key
};
Expand Down Expand Up @@ -497,49 +497,100 @@ export function createMeasurementsQueryFromControls(measurementControls, collect
return newQuery;
}

export function createMeasurementsControlsFromQuery(query){
const newState = {};
/**
* Parses the current collection's controls from measurements and updates them
* with valid query parameters.
*
* In cases where the query param is invalid, the query param is removed from the
* returned query object.
* @param {Object} measurements
* @param {Object} query
* @returns {Object}
*/
export const combineMeasurementsControlsAndQuery = (measurements, query) => {
const updatedQuery = cloneDeep(query);
const collectionKeys = measurements.collections.map((collection) => collection.key);
// Remove m_collection query if it's invalid or the default collection key
if (!collectionKeys.includes(updatedQuery.m_collection) ||
updatedQuery.m_collection === measurements.defaultCollectionKey) {
delete updatedQuery.m_collection;
}
// Parse collection's default controls
const collectionKey = updatedQuery.m_collection || measurements.defaultCollectionKey;
const collectionToDisplay = getCollectionToDisplay(measurements.collections, collectionKey, measurements.defaultCollectionKey)
const collectionControls = getCollectionDefaultControls(collectionToDisplay);
const collectionGroupings = Array.from(collectionToDisplay.groupings.keys());
// Modify controls via query
for (const [controlKey, queryKey] of Object.entries(controlToQueryParamMap)) {
const queryValue = query[queryKey];
const queryValue = updatedQuery[queryKey];
if (queryValue === undefined) continue;
let expectedValues = [];
let conversionFn = () => null;
let newControlState = undefined;
switch(queryKey) {
case "m_display":
expectedValues = ["mean", "raw"];
conversionFn = () => queryValue;
if (queryValue === "mean" || queryValue === "raw") {
newControlState = queryValue;
}
break;
case "m_collection": // fallthrough
case "m_groupBy":
// Accept any value here because we cannot validate the query before
// the measurements JSON is loaded
expectedValues = [queryValue];
conversionFn = () => queryValue;
// Verify value is a valid grouping of collection
if (collectionGroupings.includes(queryValue)) {
newControlState = queryValue;
}
break;
case "m_overallMean":
if (queryValue === "show" || queryValue === "hide") {
newControlState = queryValue === "show";
}
break;
case "m_overallMean": // fallthrough
case "m_threshold":
expectedValues = ["show", "hide"];
conversionFn = () => queryValue === "show";
if (collectionToDisplay.thresholds &&
(queryValue === "show" || queryValue === "hide")) {
newControlState = queryValue === "show";
}
break;
default:
console.error(`Ignoring unsupported query ${queryKey}`);
}

if(expectedValues.includes(queryValue)) {
newState[controlKey] = conversionFn();
} else {
console.error(`Ignoring invalid query param ${queryKey}=${queryValue}, value should be one of ${expectedValues}`);
// Remove query if it's invalid or the same as the collection's default controls
if (newControlState === undefined || newControlState === collectionControls[controlKey]) {
delete updatedQuery[queryKey];
continue;
}
collectionControls[controlKey] = newControlState
}

// Accept any value here because we cannot validate the query before the measurements JSON is loaded
for (const filterKey of Object.keys(query).filter((c) => c.startsWith(filterQueryPrefix))) {
// Special handling of the filter query since these can be arbitrary query keys `mf_*`
for (const filterKey of Object.keys(updatedQuery).filter((c) => c.startsWith(filterQueryPrefix))) {
// Remove and ignore query for invalid fields
const field = filterKey.replace(filterQueryPrefix, '');
const filterValues = Array.isArray(query[filterKey]) ? query[filterKey] : [query[filterKey]];
const measurementsFilters = {...newState.measurementsFilters};
if (!collectionToDisplay.filters.has(field)) {
delete updatedQuery[filterKey];
continue;
}

// Remove and ignore query for invalid field values
const collectionFieldValues = collectionToDisplay.filters.get(field).values;
const filterValues = Array.isArray(updatedQuery[filterKey]) ? updatedQuery[filterKey] : [updatedQuery[filterKey]];
const validFilterValues = filterValues.filter((value) => collectionFieldValues.has(value));
if (!validFilterValues.length) {
delete updatedQuery[filterKey];
continue;
}

// Set field filter controls and query to the valid filter values
updatedQuery[filterKey] = validFilterValues;
const measurementsFilters = {...collectionControls.measurementsFilters};
measurementsFilters[field] = new Map(measurementsFilters[field]);
for (const value of filterValues) {
for (const value of validFilterValues) {
measurementsFilters[field].set(value, {active: true});
}
newState.measurementsFilters = measurementsFilters;
collectionControls.measurementsFilters = measurementsFilters;

}
return {
collectionToDisplay,
collectionControls,
updatedQuery
}
return newState;
}
34 changes: 9 additions & 25 deletions src/actions/recomputeReduxState.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,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, loadMeasurements } from "./measurements";
import { combineMeasurementsControlsAndQuery, loadMeasurements } from "./measurements";

export const doesColorByHaveConfidence = (controlsState, colorBy) =>
controlsState.coloringsPresentOnTreeWithConfidence.has(colorBy);
Expand Down Expand Up @@ -207,9 +207,6 @@ const modifyStateViaURLQuery = (state, query) => {
if (query.scatterX) state.scatterVariables.x = query.scatterX;
if (query.scatterY) state.scatterVariables.y = query.scatterY;

/* 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 @@ -891,9 +888,6 @@ export const createStateFromQueryOrJSONs = ({
controls = getDefaultControlsState();
controls = modifyControlsStateViaTree(controls, tree, treeToo, metadata.colorings);
controls = modifyStateViaMetadata(controls, metadata, entropy.genomeMap);
if (measurements.loaded) {
controls = {...controls, ...getCollectionDefaultControls(measurements.collectionToDisplay)};
}
} else if (oldState) {
/* creating deep copies avoids references to (nested) objects remaining the same which
can affect props comparisons. Due to the size of some of the state, we only do this selectively */
Expand All @@ -906,12 +900,6 @@ 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 @@ -925,22 +913,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);

/* Special handling of measurements controls and query params */
if (measurements.loaded) {
const { collectionToDisplay, collectionControls, updatedQuery} = combineMeasurementsControlsAndQuery(measurements, query);
measurements.collectionToDisplay = collectionToDisplay;
controls = {...controls, ...collectionControls};
query = updatedQuery;
}

/* certain narrative slides prescribe the main panel to simply render narrative-provided markdown content */
if (narrativeBlocks && narrative[narrativeSlideIdx].mainDisplayMarkdown) {
controls.panelsToDisplay = ["MainDisplayMarkdown"];
Expand Down

0 comments on commit 947eda6

Please sign in to comment.