-
Notifications
You must be signed in to change notification settings - Fork 166
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #478 from nextstrain/entropy
Reactive entropy data & URLs handled via middleware
- Loading branch information
Showing
60 changed files
with
1,395 additions
and
1,159 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,56 +1,60 @@ | ||
import { parseGenotype } from "../util/getGenotype"; | ||
import getColorScale from "../util/getColorScale"; | ||
import { setGenotype } from "../util/setGenotype"; | ||
import { calcNodeColor } from "../components/tree/treeHelpers"; | ||
import { modifyURLquery } from "../util/urlHelpers"; | ||
import { determineColorByGenotypeType } from "../util/colorHelpers"; | ||
import { updateEntropyVisibility } from "./entropy"; | ||
import * as types from "./types"; | ||
|
||
/* providedColorBy: undefined | string | ||
updateURL: undefined | router (this.context.router) */ | ||
export const changeColorBy = (providedColorBy = undefined, router = undefined) => { | ||
/* providedColorBy: undefined | string */ | ||
export const changeColorBy = (providedColorBy = undefined) => { // eslint-disable-line import/prefer-default-export | ||
return (dispatch, getState) => { | ||
const { controls, tree, sequences, metadata } = getState(); | ||
const { controls, tree, metadata } = getState(); | ||
/* step 0: bail if all required params aren't (yet) available! */ | ||
/* note this *can* run before the tree is loaded - we only need the nodes */ | ||
if (!(tree.nodes !== null && sequences.loaded && metadata.loaded)) { | ||
if (!(tree.nodes !== null && metadata.loaded)) { | ||
// console.log( | ||
// "updateColorScale not running due to load statuses of ", | ||
// "tree nodes are null?", tree.nodes === null, | ||
// "sequences", sequences.loaded, | ||
// "metadata", metadata.loaded | ||
// ); | ||
return null; | ||
} | ||
const colorBy = providedColorBy ? providedColorBy : controls.colorBy; | ||
|
||
if (colorBy.slice(0, 3) === "gt-") { | ||
const x = parseGenotype(colorBy, controls.geneLength); | ||
setGenotype(tree.nodes, x[0][0], x[0][1] + 1); | ||
} | ||
|
||
/* step 1: calculate the required colour scale */ | ||
const version = controls.colorScale === undefined ? 1 : controls.colorScale.version + 1; | ||
// console.log("updateColorScale setting colorScale to ", version); | ||
const colorScale = getColorScale(colorBy, tree, sequences, metadata.colorOptions, version); | ||
const colorScale = getColorScale(colorBy, tree, controls.geneLength, metadata.colorOptions, version); | ||
/* */ | ||
if (colorBy.slice(0, 3) === "gt-" && sequences.geneLength) { | ||
colorScale.genotype = parseGenotype(colorBy, sequences.geneLength); | ||
if (colorBy.slice(0, 3) === "gt-" && controls.geneLength) { | ||
colorScale.genotype = parseGenotype(colorBy, controls.geneLength); | ||
} | ||
|
||
/* step 2: calculate the node colours */ | ||
const nodeColors = calcNodeColor(tree, colorScale, sequences); | ||
const nodeColors = calcNodeColor(tree, colorScale); | ||
|
||
/* step 3: change in mutType? */ | ||
const newMutType = determineColorByGenotypeType(colorBy) !== controls.mutType ? determineColorByGenotypeType(colorBy) : false; | ||
if (newMutType) { | ||
updateEntropyVisibility(dispatch, getState); | ||
} | ||
|
||
/* step 3: dispatch */ | ||
/* step 4: dispatch */ | ||
dispatch({ | ||
type: types.NEW_COLORS, | ||
colorBy, | ||
colorScale, | ||
nodeColors, | ||
version | ||
version, | ||
newMutType | ||
}); | ||
|
||
/* step 4 (optional): update the URL query field */ | ||
if (router) { | ||
modifyURLquery(router, {c: colorBy}, true); | ||
} | ||
|
||
return null; | ||
}; | ||
}; | ||
|
||
/* updateColors calls changeColorBy with no args, i.e. it updates the colorScale & nodeColors */ | ||
export const updateColors = () => (dispatch) => {dispatch(changeColorBy());}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { debounce } from 'lodash'; | ||
import { calcEntropyInView } from "../util/treeTraversals"; | ||
import * as types from "./types"; | ||
|
||
/* debounce works better than throttle, as it _won't_ update while events are still coming in (e.g. dragging the date slider) */ | ||
export const updateEntropyVisibility = debounce((dispatch, getState) => { | ||
const { entropy, controls, tree } = getState(); | ||
if (!tree.nodes || | ||
!tree.visibility || | ||
!entropy.geneMap || | ||
controls.mapAnimationPlayPauseButton !== "Play" | ||
) {return;} | ||
const [data, maxYVal] = calcEntropyInView(tree.nodes, tree.visibility, controls.mutType, entropy.geneMap, entropy.showCounts); | ||
dispatch({type: types.ENTROPY_DATA, data, maxYVal}); | ||
}, 500, { leading: true, trailing: true }); | ||
|
||
export const changeMutType = (newMutType) => (dispatch, getState) => { | ||
dispatch({type: types.TOGGLE_MUT_TYPE, data: newMutType}); | ||
updateEntropyVisibility(dispatch, getState); | ||
}; | ||
|
||
export const showCountsNotEntropy = (showCounts) => (dispatch, getState) => { | ||
dispatch({type: types.ENTROPY_COUNTS_TOGGLE, showCounts}); | ||
updateEntropyVisibility(dispatch, getState); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import parseParams from "../util/parseParams"; | ||
import queryString from "query-string"; | ||
import { getPost } from "../util/getMarkdown"; | ||
import { PAGE_CHANGE, URL_QUERY_CHANGE } from "./types"; | ||
import { updateVisibleTipsAndBranchThicknesses } from "./treeProperties"; | ||
import { changeColorBy } from "./colors"; | ||
|
||
// make prefix for data files with fields joined by _ instead of / as in URL | ||
const makeDataPathFromParsedParams = (parsedParams) => { | ||
const tmp_levels = Object.keys(parsedParams.dataset).map((d) => parsedParams.dataset[d]); | ||
tmp_levels.sort((x, y) => x[0] > y[0]); | ||
return tmp_levels.map((d) => d[1]).join("_"); | ||
}; | ||
|
||
/* match URL pathname to datasets (from manifest) */ | ||
const getDatapath = (pathname, availableDatasets) => { | ||
if (!availableDatasets) {return undefined;} | ||
const parsedParams = parseParams(pathname, availableDatasets); | ||
if (parsedParams.valid) { | ||
return makeDataPathFromParsedParams(parsedParams); | ||
} | ||
return pathname.replace(/^\//, '').replace(/\/$/, '').replace('/', '_'); | ||
}; | ||
|
||
export const getPageFromPathname = (pathname) => { | ||
if (pathname === "/") { | ||
return "splash"; | ||
} else if (pathname.startsWith("/methods")) { | ||
return "methods"; | ||
} else if (pathname.startsWith("/posts")) { | ||
return "posts"; | ||
} else if (pathname.startsWith("/about")) { | ||
return "about"; | ||
} | ||
return "app"; // fallthrough | ||
}; | ||
|
||
/* changes the state of the page and (perhaps) the dataset displayed. | ||
required argument path is the destination path - e.g. "zika" or "flu/..." | ||
optional argument query additionally changes the URL query in the middleware (has no effect on the reducers) | ||
(if this is left out, then the query is left unchanged by the middleware) (TYPE: object) | ||
optional argument push signals that pushState should be used (has no effect on the reducers) | ||
this is an action, rather than the reducer, as it is not pure (it may change the URL) */ | ||
export const changePage = ({path, query = undefined, push = true}) => (dispatch, getState) => { | ||
if (!path) {console.error("changePage called without a path"); return;} | ||
const { datasets } = getState(); | ||
const d = { | ||
type: PAGE_CHANGE, | ||
page: getPageFromPathname(path) | ||
}; | ||
d.datapath = d.page === "app" ? getDatapath(path, datasets.availableDatasets) : undefined; | ||
if (query !== undefined) { d.query = query; } | ||
if (push) { d.pushState = true; } | ||
/* check if this is "valid" - we can change it here before it is dispatched */ | ||
dispatch(d); | ||
/* if a specific post is specified in the URL, fetch it */ | ||
if (d.page === "posts" && path !== "/posts") { | ||
dispatch(getPost(`post_${path.replace("/posts/", "")}.md`)); | ||
} | ||
}; | ||
|
||
/* quite different to changePage - obviously only the query is changing, but this is sent to the reducers (it's not in changePage) | ||
required argument query is sent to the reducers and additionally changes the URL query in the middleware (TYPE: object) | ||
optional argument push signals that pushState should be used (has no effect on the reducers) */ | ||
export const changePageQuery = ({query, push = true}) => (dispatch, getState) => { | ||
const { controls, metadata } = getState(); | ||
dispatch({ | ||
type: URL_QUERY_CHANGE, | ||
query, | ||
metadata, | ||
pushState: push | ||
}); | ||
const newState = getState(); | ||
/* working out whether visibility / thickness needs updating is tricky */ | ||
dispatch(updateVisibleTipsAndBranchThicknesses()); | ||
if (controls.colorBy !== newState.controls.colorBy) { | ||
dispatch(changeColorBy()); | ||
} | ||
}; | ||
|
||
export const browserBackForward = () => (dispatch, getState) => { | ||
const { datasets } = getState(); | ||
/* if the pathname has changed, trigger the changePage action (will trigger new post to load, new dataset to load, etc) */ | ||
console.log("broswer back/forward detected. From: ", datasets.urlPath, datasets.urlSearch, "to:", window.location.pathname, window.location.search) | ||
if (datasets.urlPath !== window.location.pathname) { | ||
dispatch(changePage({path: window.location.pathname})); | ||
} else { | ||
dispatch(changePageQuery({query: queryString.parse(window.location.search)})); | ||
} | ||
}; |
Oops, something went wrong.