Permalink
Browse files

Merge pull request #477 from nextstrain/url

add state to URL
  • Loading branch information...
trvrb committed Jan 3, 2018
2 parents 1d54628 + 28c2e49 commit d4ed4bfaaa3a975a7e93d6633169f23cf25111ad
View
@@ -159,15 +159,18 @@ export const loadJSONs = (router, s3override = undefined) => { // eslint-disable
export const urlQueryChange = (query) => {
return (dispatch, getState) => {
const { metadata } = getState();
const { controls, metadata } = getState();
dispatch({
type: types.URL_QUERY_CHANGE,
query,
metadata
});
/* perhaps check if the following two are actually necessary?!?!?! */
const newState = getState();
/* working out whether visibility / thickness needs updating is tricky */
dispatch(updateVisibleTipsAndBranchThicknesses());
dispatch(updateColors());
if (controls.colorBy !== newState.controls.colorBy) {
dispatch(updateColors());
}
};
};
@@ -4,6 +4,7 @@ import { calcVisibility,
identifyPathToTip,
calcBranchThickness } from "../components/tree/treeHelpers";
import * as types from "./types";
import { modifyURLquery } from "../util/urlHelpers";
const calculateVisiblityAndBranchThickness = (tree, controls, dates, {idxOfInViewRootNode = 0, tipSelectedIdx = 0} = {}) => {
const visibility = tipSelectedIdx ? identifyPathToTip(tree.nodes, tipSelectedIdx) : calcVisibility(tree, controls, dates);
@@ -115,7 +116,7 @@ export const legendMouseEnterExit = (label = null) => {
};
};
export const applyFilterQuery = (fields, values, mode = "set") => {
export const applyFilterQuery = (router, fields, values, mode = "set") => {
/* fields: e.g. region || country || authors
values: list of selected values, e.g [brazil, usa, ...]
mode: set | add | remove
@@ -153,6 +154,9 @@ export const applyFilterQuery = (fields, values, mode = "set") => {
}
}
dispatch({type: types.APPLY_FILTER_QUERY, fields, values: newValues});
const q = {};
q[`f_${fields}`] = newValues.join(',');
modifyURLquery(router, q, true);
dispatch(updateVisibleTipsAndBranchThicknesses());
};
};
@@ -92,7 +92,7 @@ const parseEncodedGenotype = (colorBy) => {
browserDimensions: state.browserDimensions.browserDimensions,
loaded: state.entropy.loaded,
colorBy: state.controls.colorBy,
defaultColorBy: state.controls.defaultColorBy,
defaultColorBy: state.controls.defaults.colorBy,
shouldReRender: false,
panelLayout: state.controls.panelLayout
};
@@ -196,7 +196,11 @@ class Entropy extends React.Component {
chart,
chartGeom: this.getChartGeom(props)
});
chart.update({aa: props.mutType === "aa"}); // why is this necessary straight after an initial render?!
/* unsure why this cannot be incorporated into the initial render... */
chart.update({
selected: parseEncodedGenotype(props.colorBy),
aa: props.mutType === "aa"
});
}
componentDidMount() {
if (this.props.loaded) {
@@ -3,14 +3,13 @@ import PropTypes from 'prop-types';
import { connect } from "react-redux";
import Select from "react-select";
import { sidebarField } from "../../globalStyles";
import { defaultColorBy, controlsWidth } from "../../util/globals";
import { controlsWidth } from "../../util/globals";
import { changeColorBy } from "../../actions/colors";
import { analyticsControlsEvent } from "../../util/googleAnalytics";
/* Why does this have colorBy set as state (here) and in redux?
it's for the case where we select genotype, then wait for the
base to be selected, so we modify state but not yet dispatch
*/
/* the reason why we have colorBy as state (here) and in redux
is for the case where we select genotype, then wait for the
base to be selected, so we modify state but not yet dispatch */
@connect((state) => {
return {
@@ -24,7 +23,7 @@ class ColorBy extends React.Component {
constructor(props) {
super(props);
this.state = {
colorBySelected: defaultColorBy,
colorBySelected: props.colorBy,
geneSelected: "nuc",
positionSelected: ""
};
@@ -40,10 +39,21 @@ class ColorBy extends React.Component {
}
componentWillReceiveProps(nextProps) {
const colorBy = nextProps.colorBy;
if (colorBy) {
const selected = (colorBy.slice(0, 2) !== "gt") ? colorBy : "gt";
this.setState({colorBySelected: selected});
if (this.props.colorBy !== nextProps.colorBy) {
if (nextProps.colorBy.startsWith("gt")) {
const matches = nextProps.colorBy.match(/gt-(.+)_(.+)$/);
this.setState({
colorBySelected: "gt",
geneSelected: matches[1],
positionSelected: matches[2]
});
} else {
this.setState({
colorBySelected: nextProps.colorBy,
geneSelected: "nuc",
positionSelected: ""
});
}
}
}
@@ -183,7 +193,7 @@ class ColorBy extends React.Component {
{this.gtPositionInput()}
</div>
:
<div/>
null
}
</div>
);
@@ -25,6 +25,7 @@ const header = (text) => (
@connect((state) => ({
analysisSlider: state.controls.analysisSlider,
canTogglePanelLayout: state.controls.canTogglePanelLayout,
panels: state.metadata.panels
}))
class Controls extends React.Component {
@@ -72,8 +73,8 @@ class Controls extends React.Component {
{header("Color By")}
<ColorBy/>
{mapAndTree ? (header("Panel Layout")) : null}
{mapAndTree ? (<PanelLayout/>) : null}
{mapAndTree && this.props.canTogglePanelLayout ? (header("Panel Layout")) : null}
{mapAndTree && this.props.canTogglePanelLayout ? (<PanelLayout/>) : null}
{header("Tree Options")}
@@ -1,16 +1,21 @@
import React from "react";
import PropTypes from 'prop-types';
import { connect } from "react-redux";
import { materialButton, materialButtonSelected, brandColor, lightGrey, darkGrey } from "../../globalStyles";
import * as icons from "../framework/svg-icons";
import { CHANGE_PANEL_LAYOUT } from "../../actions/types";
import { analyticsControlsEvent } from "../../util/googleAnalytics";
import { modifyURLquery } from "../../util/urlHelpers";
@connect((state) => {
return {
panelLayout: state.controls.panelLayout
};
})
class PanelLayouts extends React.Component {
static contextTypes = {
router: PropTypes.object.isRequired
}
getStyles() {
return {
container: {
@@ -37,6 +42,7 @@ class PanelLayouts extends React.Component {
style={this.props.panelLayout === "full" ? materialButtonSelected : materialButton}
onClick={() => {
analyticsControlsEvent("change-layout-full");
modifyURLquery(this.context.router, {p: "full"}, true);
this.props.dispatch({ type: CHANGE_PANEL_LAYOUT, data: "full" });
}}
>
@@ -48,6 +54,7 @@ class PanelLayouts extends React.Component {
style={this.props.panelLayout === "grid" ? materialButtonSelected : materialButton}
onClick={() => {
analyticsControlsEvent("change-layout-grid");
modifyURLquery(this.context.router, {p: "grid"}, true);
this.props.dispatch({ type: CHANGE_PANEL_LAYOUT, data: "grid" });
}}
>
@@ -60,19 +60,19 @@ export const getAcknowledgments = (router, style) => {
return null;
}
const dispatchFilter = (dispatch, activeFilters, key, value) => {
const dispatchFilter = (router, dispatch, activeFilters, key, value) => {
const mode = activeFilters[key].indexOf(value) === -1 ? "add" : "remove";
dispatch(applyFilterQuery(key, [value], mode));
dispatch(applyFilterQuery(router, key, [value], mode));
};
export const displayFilterValueAsButton = (dispatch, activeFilters, filterName, itemName, display, showX) => {
export const displayFilterValueAsButton = (router, dispatch, activeFilters, filterName, itemName, display, showX) => {
const active = activeFilters[filterName].indexOf(itemName) !== -1;
if (active && showX) {
return (
<div key={itemName} style={{display: "inline-block"}}>
<div
className={'boxed-item-icon'}
onClick={() => {dispatchFilter(dispatch, activeFilters, filterName, itemName);}}
onClick={() => {dispatchFilter(router, dispatch, activeFilters, filterName, itemName);}}
role="button"
tabIndex={0}
>
@@ -89,7 +89,7 @@ export const displayFilterValueAsButton = (dispatch, activeFilters, filterName,
<div
className={"boxed-item active-clickable"}
key={itemName}
onClick={() => {dispatchFilter(dispatch, activeFilters, filterName, itemName);}}
onClick={() => {dispatchFilter(router, dispatch, activeFilters, filterName, itemName);}}
role="button"
tabIndex={0}
>
@@ -101,7 +101,7 @@ export const displayFilterValueAsButton = (dispatch, activeFilters, filterName,
<div
className={"boxed-item inactive"}
key={itemName}
onClick={() => {dispatchFilter(dispatch, activeFilters, filterName, itemName);}}
onClick={() => {dispatchFilter(router, dispatch, activeFilters, filterName, itemName);}}
role="button"
tabIndex={0}
>
@@ -110,13 +110,13 @@ export const displayFilterValueAsButton = (dispatch, activeFilters, filterName,
);
};
const removeFiltersButton = (dispatch, filterNames, outerClassName, label) => {
const removeFiltersButton = (router, dispatch, filterNames, outerClassName, label) => {
return (
<div
className={`${outerClassName} boxed-item active-clickable`}
style={{paddingLeft: '5px', paddingRight: '5px', display: "inline-block"}}
onClick={() => {
filterNames.forEach((n) => dispatch(applyFilterQuery(n, [], 'set')))
filterNames.forEach((n) => dispatch(applyFilterQuery(router, n, [], 'set')))
}}
>
{label}
@@ -195,7 +195,7 @@ class Footer extends React.Component {
return (
<div>
{`Filter by ${prettyString(filterName)}`}
{this.props.activeFilters[filterName].length ? removeFiltersButton(this.props.dispatch, [filterName], "inlineRight", `Clear ${filterName} filter`) : null}
{this.props.activeFilters[filterName].length ? removeFiltersButton(this.context.router, this.props.dispatch, [filterName], "inlineRight", `Clear ${filterName} filter`) : null}
<Flex wrap="wrap" justifyContent="flex-start" alignItems="center" style={styles.citationList}>
{Object.keys(totalStateCount).sort().map((itemName) => {
let display;
@@ -214,7 +214,7 @@ class Footer extends React.Component {
</g>
);
}
return displayFilterValueAsButton(this.props.dispatch, this.props.activeFilters, filterName, itemName, display, false);
return displayFilterValueAsButton(this.context.router, this.props.dispatch, this.props.activeFilters, filterName, itemName, display, false);
})}
</Flex>
</div>
@@ -5,9 +5,11 @@ import _throttle from "lodash/throttle";
import { BROWSER_DIMENSIONS, CHANGE_PANEL_LAYOUT } from "../../actions/types";
import { getManifest, getPostsManifest } from "../../util/clientAPIInterface";
import { twoColumnBreakpoint } from "../../util/globals";
import { modifyURLquery } from "../../util/urlHelpers";
@connect((state) => ({
panels: state.metadata.panels
panels: state.metadata.panels,
canTogglePanelLayout: state.controls.canTogglePanelLayout
}))
class Monitor extends React.Component {
constructor(props) {
@@ -50,10 +52,12 @@ class Monitor extends React.Component {
};
dispatch({type: BROWSER_DIMENSIONS, data: newBrowserDimensions});
/* only switch between grid / full if there is a map and a tree */
if (this.props.panels !== undefined && this.props.panels.indexOf("map") !== -1 && this.props.panels.indexOf("tree") !== -1) {
if (this.props.panels !== undefined && this.props.canTogglePanelLayout) {
if (oldBrowserDimensions.width < twoColumnBreakpoint && newBrowserDimensions.width >= twoColumnBreakpoint) {
modifyURLquery(this.context.router, {p: ""}, true);
dispatch({type: CHANGE_PANEL_LAYOUT, data: "grid"});
} else if (oldBrowserDimensions.width > twoColumnBreakpoint && newBrowserDimensions.width <= twoColumnBreakpoint) {
modifyURLquery(this.context.router, {p: ""}, true);
dispatch({type: CHANGE_PANEL_LAYOUT, data: "full"});
}
}
@@ -7,6 +7,7 @@ import { applyFilterQuery, changeDateFilter } from "../../actions/treeProperties
import { prettyString } from "../../util/stringHelpers";
import { displayFilterValueAsButton } from "../framework/footer";
import { CHANGE_TREE_ROOT_IDX } from "../../actions/types";
import { modifyURLquery } from "../../util/urlHelpers";
const resetTreeButton = (dispatch) => {
return (
@@ -109,6 +110,9 @@ class Info extends React.Component {
dispatch: React.PropTypes.func.isRequired,
idxOfInViewRootNode: React.PropTypes.number
}
static contextTypes = {
router: React.PropTypes.object.isRequired
}
getStyles(responsive) {
let fontSize = 32;
if (this.props.browserDimensions.width < 1000) {
@@ -163,7 +167,10 @@ class Info extends React.Component {
<div key={"timefilter"} style={{display: "inline-block"}}>
<div
className={'boxed-item-icon'}
onClick={() => this.props.dispatch(changeDateFilter({newMin: this.props.absoluteDateMin, newMax: this.props.absoluteDateMax}))}
onClick={() => {
modifyURLquery(this.context.router, {dmin: '', dmax: ''}, true);
this.props.dispatch(changeDateFilter({newMin: this.props.absoluteDateMin, newMax: this.props.absoluteDateMax}));
}}
role="button"
tabIndex={0}
>
@@ -183,15 +190,15 @@ class Info extends React.Component {
{" (" + this.props.totalStateCounts[filterName][itemName] + ")"}
</g>
);
buttons.push(displayFilterValueAsButton(this.props.dispatch, this.props.filters, filterName, itemName, display, true));
buttons.push(displayFilterValueAsButton(this.context.router, this.props.dispatch, this.props.filters, filterName, itemName, display, true));
});
}
clearFilterButton(field) {
return (
<span
style={{cursor: "pointer", color: '#5097BA'}}
key={field}
onClick={() => this.props.dispatch(applyFilterQuery(field, []))}
onClick={() => this.props.dispatch(applyFilterQuery(this.context.router, field, []))}
role="button"
tabIndex={0}
>
@@ -231,15 +238,15 @@ class Info extends React.Component {
if (nSelectedAuthors > 0 && nSelectedAuthors < 3) {
authorInfo.forEach((d) => (
buttons.push(
displayFilterValueAsButton(this.props.dispatch, this.props.filters, "authors", d.name, d.longlabel, true)
displayFilterValueAsButton(this.context.router, this.props.dispatch, this.props.filters, "authors", d.name, d.longlabel, true)
)
));
return;
}
/* case 3: more than 2 authors selected. */
authorInfo.forEach((d) => (
buttons.push(
displayFilterValueAsButton(this.props.dispatch, this.props.filters, "authors", d.name, d.label, true)
displayFilterValueAsButton(this.context.router, this.props.dispatch, this.props.filters, "authors", d.name, d.label, true)
)
));
}
@@ -1,5 +1,4 @@
import { calcFullTipCounts, calcBranchLength, calcDates } from "./treeHelpers";
import { defaultDistanceMeasures } from "../../util/globals";
export const processNodes = (nodes) => {
const rootNode = nodes[0];
@@ -47,12 +46,9 @@ const radialLayout = (node, distanceMeasure, nTips, rootVal) => {
* nodes: array of nodes for which x/y coordinates are to be calculated
* nTips: total number of tips (optional)
* distanceMeasures: the different types of distances used to measure
distances on the tree (date, mutations, etc) (optional)
distances on the tree (date, mutations, etc)
*/
export const calcLayouts = (nodes, distanceMeasures, nTips) => {
if (typeof distanceMeasures==='undefined'){
distanceMeasures = defaultDistanceMeasures;
}
if (typeof nTips==='undefined'){
nTips = nodes.filter((d) => {return !d.hasChildren;} ).length;
}
Oops, something went wrong.

0 comments on commit d4ed4bf

Please sign in to comment.