From ce33cbd65975e3891bb418b7fe6f3496db77e5cc Mon Sep 17 00:00:00 2001 From: james hadfield Date: Mon, 17 Jun 2024 14:44:58 +1200 Subject: [PATCH] wip --- ...teReduxState.js => recomputeReduxState.ts} | 45 +++- src/reducers/{metadata.js => metadata.ts} | 40 +++- src/store.ts | 2 +- src/types/datasetJson.ts | 216 ++++++++++++++++++ src/util/entropyCreateStateFromJsons.ts | 4 +- 5 files changed, 289 insertions(+), 18 deletions(-) rename src/actions/{recomputeReduxState.js => recomputeReduxState.ts} (96%) rename src/reducers/{metadata.js => metadata.ts} (62%) create mode 100644 src/types/datasetJson.ts diff --git a/src/actions/recomputeReduxState.js b/src/actions/recomputeReduxState.ts similarity index 96% rename from src/actions/recomputeReduxState.js rename to src/actions/recomputeReduxState.ts index 9a1f45d45..34cd0cdb3 100644 --- a/src/actions/recomputeReduxState.js +++ b/src/actions/recomputeReduxState.ts @@ -16,7 +16,7 @@ import { entropyCreateState, genomeMap as createGenomeMap } from "../util/entrop import { calcNodeColor } from "../util/colorHelpers"; import { calcColorScale, createVisibleLegendValues } from "../util/colorScale"; import { computeMatrixFromRawData, checkIfNormalizableFromRawData } from "../util/processFrequencies"; -import { applyInViewNodesToTree } from "../actions/tree"; +import { applyInViewNodesToTree } from "./tree"; import { validateScatterVariables } from "../util/scatterplotHelpers"; import { isColorByGenotype, decodeColorByGenotype, encodeColorByGenotype, decodeGenotypeFilters, encodeGenotypeFilters, getCdsFromGenotype } from "../util/getGenotype"; import { getTraitFromNode, getDivFromNode, collectGenotypeStates } from "../util/treeMiscHelpers"; @@ -24,6 +24,10 @@ import { collectAvailableTipLabelOptions } from "../components/controls/choose-t import { hasMultipleGridPanels } from "./panelDisplay"; import { strainSymbolUrlString } from "../middleware/changeURL"; +import { DatasetJson } from "../types/datasetJson"; +import { MetadataState } from "../reducers/metadata"; +import { ReduxRootState, AppDispatch } from "../store"; + export const doesColorByHaveConfidence = (controlsState, colorBy) => controlsState.coloringsPresentOnTreeWithConfidence.has(colorBy); @@ -118,8 +122,11 @@ const modifyStateViaURLQuery = (state, query) => { if (query.animate) { const params = query.animate.split(','); // console.log("start animation!", params); - window.NEXTSTRAIN.animationStartPoint = calendarToNumeric(params[0]); - window.NEXTSTRAIN.animationEndPoint = calendarToNumeric(params[1]); + // TS TODO FIX + if ('NEXTSTRAIN' in window) { + (window.NEXTSTRAIN as any).animationStartPoint = calendarToNumeric(params[0]); + (window.NEXTSTRAIN as any).animationEndPoint = calendarToNumeric(params[1]); + } state.dateMin = params[0]; state.dateMax = params[1]; state.dateMinNumeric = calendarToNumeric(params[0]); @@ -380,7 +387,7 @@ const modifyControlsStateViaTree = (state, tree, treeToo, colorings) => { state.coloringsPresentOnTree = new Set(); state.coloringsPresentOnTreeWithConfidence = new Set(); // subset of above - let coloringsToCheck = []; + let coloringsToCheck: string[] = []; if (colorings) { coloringsToCheck = Object.keys(colorings); } @@ -444,7 +451,7 @@ const modifyControlsStateViaTree = (state, tree, treeToo, colorings) => { return state; }; -const checkAndCorrectErrorsInState = (state, metadata, genomeMap, query, tree, viewingNarrative) => { +const checkAndCorrectErrorsInState = (state, metadata: MetadataState, genomeMap, query, tree, viewingNarrative) => { /* want to check that the (currently set) colorBy (state.colorBy) is valid, * and fall-back to an available colorBy if not */ @@ -597,7 +604,8 @@ const checkAndCorrectErrorsInState = (state, metadata, genomeMap, query, tree, v continue } /* delete filter names (e.g. country, region) which aren't observed on the tree */ - if (!Object.keys(tree.totalStateCounts).includes(traitName) && traitName!==strainSymbol && traitName!==genotypeSymbol) { + // TS TODO - remove the following cast (and check code _was_ working as I thought it was...) + if (!Object.keys(tree.totalStateCounts).includes(traitName as string) && traitName!==strainSymbol && traitName!==genotypeSymbol) { delete state.filters[traitName]; delete query[_queryKey(traitName)]; continue @@ -734,8 +742,8 @@ const convertColoringsListToDict = (coloringsList) => { * * A lot of this is simply changing augur's snake_case to auspice's camelCase */ -const createMetadataStateFromJSON = (json) => { - const metadata = {}; +const createMetadataStateFromJSON = (json: DatasetJson): MetadataState => { + const metadata: MetadataState = {}; if (json.meta.colorings) { metadata.colorings = convertColoringsListToDict(json.meta.colorings); } @@ -851,6 +859,18 @@ export const getNarrativePageFromQuery = (query, narrative) => { return n; }; +/* TS TODO */ +interface CreateStateArgs { + json: DatasetJson | false; + secondTreeDataset: any; + oldState: ReduxRootState | false; + narrativeBlocks: any; /* if in a narrative this argument is set */ + mainTreeName: string | false; + secondTreeName: string | false; + query?: any; + dispatch: any; /* redux should define this for us... */ +} + export const createStateFromQueryOrJSONs = ({ json = false, /* raw json data - completely nuke existing redux state */ secondTreeDataset = false, @@ -860,8 +880,9 @@ export const createStateFromQueryOrJSONs = ({ secondTreeName = false, query, dispatch -}) => { - let tree, treeToo, entropy, controls, metadata, narrative, frequencies, measurements; +}: CreateStateArgs) => { + let tree, treeToo, entropy, controls, narrative, frequencies, measurements; + let metadata: MetadataState; /* first task is to create metadata, entropy, controls & tree partial state */ if (json) { /* create metadata state */ @@ -905,6 +926,8 @@ export const createStateFromQueryOrJSONs = ({ measurements = {...oldState.measurements}; controls = restoreQueryableStateToDefaults(controls); controls = modifyStateViaMetadata(controls, metadata, entropy.genomeMap); + } else { + // can this _ever_ occur? If it does then `metadata` is undefined... } /* For the creation of state, we want to parse out URL query parameters @@ -936,7 +959,7 @@ export const createStateFromQueryOrJSONs = ({ /* calculate colours if loading from JSONs or if the query demands change */ - if (json || controls.colorBy !== oldState.controls.colorBy) { + if (json || controls.colorBy !== oldState?.controls.colorBy) { // TODO - why elvis operator not working here? const colorScale = calcColorScale(controls.colorBy, controls, tree, treeToo, metadata); const nodeColors = calcNodeColor(tree, colorScale); controls.colorScale = colorScale; diff --git a/src/reducers/metadata.js b/src/reducers/metadata.ts similarity index 62% rename from src/reducers/metadata.js rename to src/reducers/metadata.ts index 818159496..5f77498ab 100644 --- a/src/reducers/metadata.js +++ b/src/reducers/metadata.ts @@ -1,19 +1,49 @@ import { colorOptions } from "../util/globals"; import * as types from "../actions/types"; +import { DatasetJsonRootSequence, DatasetJson, DatasetJsonMeta } from "../types/datasetJson"; + +/** + * A lot of to-dos here... + * Does it make sense to have two state types, a non-loaded one and + * a loaded one (discriminated type based on loaded: boolean) to give + * more assurances? Otherwise most properties are potential undefineds. + * (TS still helpful here tho) + */ +export interface MetadataState { + loaded: boolean; + rootSequence?: DatasetJsonRootSequence; + identicalGenomeMapAcrossBothTrees: boolean; + rootSequenceSecondTree?: DatasetJsonRootSequence; + colorOptions: any; // TODO XXX + colorings?: any; // TODO XXX + geoResolutions?: DatasetJsonMeta['geo_resolutions']; + buildUrl?: DatasetJsonMeta['build_url'] | false; + displayDefaults?: Record; // TODO XXX + panels?: DatasetJsonMeta['panels']; + mainTreeNumTips?: number; + title?: DatasetJsonMeta['title']; + version?: DatasetJson['version']; + filters?: DatasetJsonMeta['filters']; + dataProvenance?: DatasetJsonMeta['data_provenance']; + maintainers?: DatasetJsonMeta['maintainers']; + description?: DatasetJsonMeta['description']; + updated?: DatasetJsonMeta['updated']; +} + /* The metadata reducer holds data that is * (a) mostly derived from the dataset JSON * (b) rarely changes */ -const Metadata = (state = { +const Metadata = (state:MetadataState = { loaded: false, /* see comment in the sequences reducer for explanation */ - metadata: null, + // metadata: null, rootSequence: undefined, identicalGenomeMapAcrossBothTrees: false, rootSequenceSecondTree: undefined, colorOptions // this can't be removed as the colorScale currently runs before it should -}, action) => { +}, action): MetadataState => { switch (action.type) { case types.DATA_INVALID: return Object.assign({}, state, { @@ -22,6 +52,7 @@ const Metadata = (state = { case types.URL_QUERY_CHANGE_WITH_COMPUTED_STATE: case types.TREE_TOO_DATA: case types.CLEAN_START: + console.log("incoming!", action.metadata) return action.metadata; case types.ADD_EXTRA_METADATA: { const colorings = Object.assign({}, state.colorings, action.newColorings); @@ -54,7 +85,8 @@ const Metadata = (state = { } }; -function getBuildUrlFromGetAvailableJson(availableData) { +// TODO - can we replace the returned 'false' with 'undefined'? +function getBuildUrlFromGetAvailableJson(availableData): (false | undefined | string) { if (!availableData) return undefined; /* check if the current dataset is present in the getAvailable data We currently parse the URL (pathname) for the current dataset but this diff --git a/src/store.ts b/src/store.ts index 135128400..20a451a0c 100644 --- a/src/store.ts +++ b/src/store.ts @@ -47,7 +47,7 @@ if (process.env.NODE_ENV !== 'production' && module.hot) { } // Infer types from the store. -export type RootState = ReturnType +export type ReduxRootState = ReturnType export type AppDispatch = typeof store.dispatch export default store; diff --git a/src/types/datasetJson.ts b/src/types/datasetJson.ts new file mode 100644 index 000000000..d0c5488ea --- /dev/null +++ b/src/types/datasetJson.ts @@ -0,0 +1,216 @@ +import { JsonAnnotations as DatasetJsonAnnotations } from "../util/entropyCreateStateFromJsons"; + +export interface DatasetJson { + meta: DatasetJsonMeta; + tree: any; + root_sequence?: DatasetJsonRootSequence; + version: string; +} + +export interface DatasetJsonRootSequence { + /** + * Nucleotide sequence of whole genome (from the output of `augur ancestral`) + */ + nuc: string; + /** + * Amino acid sequence of genome annotation (e.g. gene) identified by this key (from the output of `augur translate`) + */ + [k: string]: string; +} + +// type MetadataPanel = "tree" | "map" | "frequencies" | "entropy" | "measurements"; + +export interface DatasetJsonMeta { + /** + * Auspice displays this at the top of the page + */ + title?: string; + /** + * Auspice displays this (currently only in the footer) + */ + updated: string; + /** + * URL with instructions to reproduce build, usually expected to be a GitHub repo URL + */ + build_url?: string; + /** + * Auspice displays this currently in the footer. + */ + description?: string; + maintainers?: [ + { + name: string; + url?: string; + [k: string]: unknown; + }, + ...{ + name: string; + url?: string; + [k: string]: unknown; + }[] + ]; + genome_annotations?: DatasetJsonAnnotations; + /** + * These appear as filters in the footer of Auspice (which populates the displayed values based upon the tree) + */ + filters?: string[]; + panels: [ + "tree" | "map" | "frequencies" | "entropy" | "measurements", + ...("tree" | "map" | "frequencies" | "entropy" | "measurements")[] + ]; + /** + * Data to be passed through to the the resulting dataset JSON + */ + extensions?: { + [k: string]: unknown; + }; + /** + * The available options for the geographic resolution dropdown, and their lat/long information + * + * @minItems 1 + */ + geo_resolutions?: [ + { + /** + * Trait key - must be specified on nodes (e.g. 'country') + */ + key: string; + /** + * The title to display in the geo resolution dropdown. Optional -- if not provided then `key` will be used. + */ + title?: string; + /** + * Mapping from deme (trait values) to lat/long + */ + demes: { + /** + * This interface was referenced by `undefined`'s JSON-Schema definition + * via the `patternProperty` "^[a-z_]+$". + */ + [k: string]: { + latitude?: number; + longitude?: number; + }; + }; + }, + ...{ + /** + * Trait key - must be specified on nodes (e.g. 'country') + */ + key: string; + /** + * The title to display in the geo resolution dropdown. Optional -- if not provided then `key` will be used. + */ + title?: string; + /** + * Mapping from deme (trait values) to lat/long + */ + demes: { + /** + * This interface was referenced by `undefined`'s JSON-Schema definition + * via the `patternProperty` "^[a-z_]+$". + */ + [k: string]: { + latitude?: number; + longitude?: number; + }; + }; + }[] + ]; + colorings?: ColorDefinition[]; + display_defaults?: DisplayDefaults; + data_provenance?: [ + { + /** + * Name of the data source + */ + name: string; + /** + * URL to use in link to data source + */ + url?: string; + }, + ...{ + /** + * Name of the data source + */ + name: string; + /** + * URL to use in link to data source + */ + url?: string; + }[] + ]; +} + + +/** + * Each object here is an individual coloring, which will populate the sidebar dropdown in auspice + */ +interface ColorDefinition { + /** + * They key used to access the value of this coloring on each node + */ + key: string; + /** + * Text to be displayed in the "color by" dropdown and legends + */ + title?: string; + /** + * Defines how the color scale should be constructed + */ + type?: "continuous" | "temporal" | "ordinal" | "categorical" | "boolean"; + /** + * Provided mapping between trait values & hex values. For continuous scales at least 2 items must be specified + */ + scale?: [] | [string | number] | [string | number, string][]; + /** + * Specify the entries displayed in the legend. This can be used to restrict the entries in the legend for display without otherwise affecting the data viz + */ + legend?: { + /** + * value to associate with this legend entry. Used to determine colour. For non-continuous scales this also determines the matching between legend items and data. + */ + value: string | number; + /** + * Label to display in the legend. Optional - `value` will be used if this is not provided. + */ + display?: string | number; + /** + * (for continuous scales only) provide the lower & upper bounds to match data to this legend entry. Bounds from different legend entries must not overlap. Matching is (a, b] - exclusive of the lower bound, inclusive of the upper. + */ + bounds?: [] | [number] | [number, number]; + [k: string]: unknown; + }[]; +} + +/** + * Set the defaults for certain display options in Auspice. All are optional. + */ +export interface DisplayDefaults { + map_triplicate?: boolean; + geo_resolution?: string; + color_by?: string; + distance_measure?: "num_date" | "div"; + layout?: "rect" | "radial" | "unrooted" | "clock"; + /** + * What branch label should be displayed by default, or 'none' to hide labels by default. + */ + branch_label?: string; + /** + * What tip label should be displayed by default, or 'none' to hide labels by default. + */ + tip_label?: string; + transmission_lines?: boolean; + /** + * A BCP 47 language tag specifying the default language in which to display Auspice's interface (if supported) + */ + language?: string; + sidebar?: "open" | "closed"; + /** + * Panels which start toggled on (default is for all available to be shown) + * + * @minItems 1 + */ + panels?: ["tree" | "map" | "frequencies" | "entropy", ...("tree" | "map" | "frequencies" | "entropy")[]]; +} diff --git a/src/util/entropyCreateStateFromJsons.ts b/src/util/entropyCreateStateFromJsons.ts index 628f5f34d..30d2d300b 100644 --- a/src/util/entropyCreateStateFromJsons.ts +++ b/src/util/entropyCreateStateFromJsons.ts @@ -1,7 +1,7 @@ import { genotypeColors } from "./globals"; import { defaultEntropyState } from "../reducers/entropy"; -type JsonAnnotations = Record +export type JsonAnnotations = Record type Strand = "+" | "-"; // note that other GFF-valid options are '.' and '?' but not used in Auspice datasets type JsonSegmentRange = {start: number, end: number}; // Start is 1-based, End is 1-based closed (GFF) interface JsonAnnotation { @@ -140,7 +140,7 @@ export const genomeMap = (annotations: JsonAnnotations): GenomeAnnotation => { return [chromosome]; } -export const entropyCreateState = (genomeAnnotations: JsonAnnotations) => { +export const entropyCreateState = (genomeAnnotations: JsonAnnotations|undefined) => { if (genomeAnnotations) { try { return {