Permalink
Browse files

Merge pull request #476 from nextstrain/narrative

Narrative machinery
  • Loading branch information...
jameshadfield committed Dec 6, 2017
2 parents 07d6ee5 + 3f9e2ff commit 9661f6df4900df0417062eb82a82a736fae6942c
View
@@ -37,6 +37,8 @@ data_files=( "manifest_guest.json" "manifest_mumps.json"\
)
static_files=(
"narratives/zika.md"\
"narratives/ebola.md"\
"figures_feb-2016_h1n1pdm_6b2_tree.png"\
"figures_feb-2016_h1n1pdm_clades.png"\
"figures_feb-2016_h1n1pdm_counts.png"\
@@ -138,6 +140,7 @@ done
rm -rf static/
mkdir -p static/
mkdir -p static/narratives
for i in "${static_files[@]}"
do
curl https://raw.githubusercontent.com/nextstrain/themis/master/files/${i} --compressed -o static/${i}
View
@@ -4,6 +4,7 @@ const express = require("express");
const expressStaticGzip = require("express-static-gzip");
const getFiles = require('./src/server/util/getFiles');
const serverReact = require('./src/server/util/sendReactComponents');
const serverNarratives = require('./src/server/util/narratives');
const queryString = require("query-string");
/*
@@ -87,6 +88,9 @@ app.get('/charon*', (req, res) => {
case "manifest": {
getFiles.getManifest(query, res);
break;
} case "narrative": {
serverNarratives.serveNarrative(query, res);
break;
} case "posts_manifest": {
getFiles.getPostsManifest(query, res);
break;
View
@@ -1,9 +1,12 @@
import { parseGenotype } from "../util/getGenotype";
import getColorScale from "../util/getColorScale";
import { calcNodeColor } from "../components/tree/treeHelpers";
import { modifyURLquery } from "../util/urlHelpers";
import * as types from "./types";
export const updateColors = (providedColorBy = undefined) => {
/* providedColorBy: undefined | string
updateURL: undefined | router (this.context.router) */
export const changeColorBy = (providedColorBy = undefined, router = undefined) => {
return (dispatch, getState) => {
const { controls, tree, sequences, metadata } = getState();
/* step 0: bail if all required params aren't (yet) available! */
@@ -39,13 +42,15 @@ export const updateColors = (providedColorBy = undefined) => {
nodeColors,
version
});
/* step 4 (optional): update the URL query field */
if (router) {
modifyURLquery(router, {c: colorBy}, true);
}
return null;
};
};
/* changeColorBy is just a wrapper for updateColors */
export const changeColorBy = (colorBy) => {
return (dispatch) => {
dispatch(updateColors(colorBy));
};
};
/* updateColors calls changeColorBy with no args, i.e. it updates the colorScale & nodeColors */
export const updateColors = () => (dispatch) => {dispatch(changeColorBy());};
View
@@ -3,9 +3,10 @@ import * as types from "./types";
import { updateColors } from "./colors";
import { updateVisibleTipsAndBranchThicknesses } from "./treeProperties";
import { turnURLtoDataPath } from "../util/urlHelpers";
import { charonAPIAddress } from "../util/globals";
import { charonAPIAddress, enableNarratives } from "../util/globals";
import { errorNotification } from "./notifications";
import { getManifest } from "../util/clientAPIInterface";
import { getNarrative } from "../util/getNarrative";
// /* if the metadata specifies an analysis slider, this is where we process it */
// const addAnalysisSlider = (dispatch, tree, controls) => {
@@ -137,6 +138,9 @@ export const loadJSONs = (router, s3override = undefined) => { // eslint-disable
if (values[0].panels.indexOf("entropy") !== -1) {
dispatch(populateEntropyStore(paths));
}
if (enableNarratives) {
getNarrative(dispatch, router.history.location.pathname);
}
})
.catch((err) => {
@@ -153,6 +157,20 @@ export const loadJSONs = (router, s3override = undefined) => { // eslint-disable
};
};
export const urlQueryChange = (query) => {
return (dispatch, getState) => {
const { metadata } = getState();
dispatch({
type: types.URL_QUERY_CHANGE,
query,
metadata
});
/* perhaps check if the following two are actually necessary?!?!?! */
dispatch(updateVisibleTipsAndBranchThicknesses());
dispatch(updateColors());
};
};
export const changeS3Bucket = (router) => {
return (dispatch, getState) => {
View
@@ -43,3 +43,5 @@ export const ADD_COLOR_BYS = "ADD_COLOR_BYS";
export const MANIFEST_RECEIVED = "MANIFEST_RECEIVED";
export const POSTS_MANIFEST_RECEIVED = "POSTS_MANIFEST_RECEIVED";
export const CHANGE_TREE_ROOT_IDX = "CHANGE_TREE_ROOT_IDX";
export const URL_QUERY_CHANGE = "URL_QUERY_CHANGE";
export const NEW_NARRATIVE = "NEW_NARRATIVE";
View
@@ -20,6 +20,7 @@ import Footer from "./framework/footer";
import DownloadModal from "./download/downloadModal";
import { analyticsNewPage } from "../util/googleAnalytics";
import filesDropped from "../actions/filesDropped";
import Narrative from "./narrative";
const nextstrainLogo = require("../images/nextstrain-logo-small.png");
@@ -36,6 +37,7 @@ const nextstrainLogo = require("../images/nextstrain-logo-small.png");
readyToLoad: state.datasets.ready,
metadata: state.metadata,
treeLoaded: state.tree.loaded,
narrativeLoaded: state.narrative.loaded,
browserDimensions: state.browserDimensions.browserDimensions
}))
class App extends React.Component {
@@ -49,11 +51,15 @@ class App extends React.Component {
to be connected to here, triggering an app render anyways
*/
const mql = window.matchMedia(`(min-width: ${controlsHiddenWidth}px)`);
mql.addListener(() => this.setState({sidebarDocked: this.state.mql.matches}));
mql.addListener(() => this.setState({
sidebarDocked: this.state.mql.matches
}));
this.state = {
mql,
sidebarDocked: mql.matches,
sidebarOpen: false
sidebarOpen: false,
rightSidebarDocked: false,
rightSidebarOpen: false
};
analyticsNewPage();
}
@@ -88,15 +94,59 @@ class App extends React.Component {
this.props.dispatch(loadJSONs(this.context.router));
}
}
componentWillReceiveProps(nextProps) {
if (nextProps.narrativeLoaded !== this.props.narrativeLoaded) {
this.setState({rightSidebarDocked: nextProps.narrativeLoaded});
}
}
renderPanels() {
if (!this.props.treeLoaded || !this.props.metadata.loaded) {
return (
<img className={"spinner"} src={nextstrainLogo} alt="loading" style={{marginTop: `${this.props.browserDimensions.height / 2 - 100}px`}}/>
);
}
const sidebar = this.state.sidebarOpen || this.state.sidebarDocked;
const sidebarRight = this.state.rightSidebarOpen || this.state.rightSidebarDocked;
return (
<Background>
<Info sidebar={sidebar} sidebarRight={sidebarRight} />
{this.props.metadata.panels.indexOf("tree") === -1 ? null : (
<TreeView
query={queryString.parse(this.context.router.history.location.search)}
sidebar={sidebar}
sidebarRight={sidebarRight}
/>
)}
{this.props.metadata.panels.indexOf("map") === -1 ? null : (
<Map
sidebar={sidebar}
sidebarRight={sidebarRight}
justGotNewDatasetRenderNewMap={false}
/>
)}
{this.props.metadata.panels.indexOf("entropy") === -1 ? null : (
<Entropy sidebar={sidebar} sidebarRight={sidebarRight} />
)}
<Footer sidebar={sidebar} sidebarRight={sidebarRight} />
</Background>
);
}
render() {
return (
<g>
<DownloadModal/>
<ToggleSidebarTab
open={this.state.sidebarDocked}
handler={() => {this.setState({sidebarDocked: !this.state.sidebarDocked});}}
side={"left"}
/>
{this.props.narrativeLoaded ?
(<ToggleSidebarTab
open={this.state.rightSidebarDocked}
handler={() => {this.setState({rightSidebarDocked: !this.state.rightSidebarDocked});}}
side={"right"}
/>) :
null}
<Sidebar
sidebar={
<div>
@@ -114,36 +164,24 @@ class App extends React.Component {
}
}}
>
{
(!this.props.treeLoaded || !this.props.metadata.loaded) ? (
<img className={"spinner"} src={nextstrainLogo} alt="loading" style={{marginTop: `${this.props.browserDimensions.height / 2 - 100}px`}}/>
) : (
<Background>
<Info
sidebar={this.state.sidebarOpen || this.state.sidebarDocked}
/>
{this.props.metadata.panels.indexOf("tree") === -1 ? null : (
<TreeView
query={queryString.parse(this.context.router.history.location.search)}
sidebar={this.state.sidebarOpen || this.state.sidebarDocked}
/>
)}
{this.props.metadata.panels.indexOf("map") === -1 ? null : (
<Map
sidebar={this.state.sidebarOpen || this.state.sidebarDocked}
justGotNewDatasetRenderNewMap={false}
/>
)}
{this.props.metadata.panels.indexOf("entropy") === -1 ? null : (
<Entropy
sidebar={this.state.sidebarOpen || this.state.sidebarDocked}
/>
)}
<Footer
sidebar={this.state.sidebarOpen || this.state.sidebarDocked}
/>
</Background>
)
{this.props.narrativeLoaded ?
(<Sidebar
sidebar={<Narrative/>}
pullRight
open={this.state.rightSidebarOpen}
docked={this.state.rightSidebarDocked}
onSetOpen={(a) => {this.setState({rightSidebarOpen: a});}}
sidebarClassName={"sidebar"}
styles={{
sidebar: {
backgroundColor: sidebarColor,
width: "300px"
}
}}
>
{this.renderPanels()}
</Sidebar>) :
this.renderPanels()
}
</Sidebar>
</g>
@@ -11,7 +11,6 @@ import EntropyChart from "./entropyD3";
import InfoPanel from "./entropyInfoPanel";
import { TOGGLE_MUT_TYPE } from "../../actions/types";
import { analyticsControlsEvent } from "../../util/googleAnalytics";
import { modifyURLquery } from "../../util/urlHelpers";
import "../../css/entropy.css";
const calcEntropy = (entropy) => {
@@ -111,6 +110,7 @@ class Entropy extends React.Component {
vertical: 0.3,
browserDimensions: p.browserDimensions,
sidebar: p.sidebar,
sidebarRight: p.sidebarRight,
minHeight: 150
});
return {
@@ -148,19 +148,16 @@ class Entropy extends React.Component {
onClick(d) {
const colorBy = constructEncodedGenotype(this.props.mutType === "aa", d);
analyticsControlsEvent("color-by-genotype");
this.props.dispatch(changeColorBy(colorBy));
modifyURLquery(this.context.router, {c: colorBy}, true);
this.props.dispatch(changeColorBy(colorBy, this.context.router));
this.setState({hovered: false});
}
changeMutTypeCallback(newMutType) {
if (newMutType !== this.props.mutType) {
/* 1. switch the redux colorBy back to the default */
this.props.dispatch(changeColorBy(this.props.defaultColorBy));
/* 1. switch the redux colorBy back to the default & update the URL */
this.props.dispatch(changeColorBy(this.props.defaultColorBy, this.context.router));
/* 2. update the mut type in redux */
this.props.dispatch({type: TOGGLE_MUT_TYPE, data: newMutType});
/* 3. modify the URL */
modifyURLquery(this.context.router, {c: this.props.defaultColorBy}, true);
}
}
@@ -216,7 +213,7 @@ class Entropy extends React.Component {
}
if (this.state.chart) {
if ((this.props.browserDimensions !== nextProps.browserDimensions) ||
(this.props.sidebar !== nextProps.sidebar)) {
(this.props.sidebar !== nextProps.sidebar || this.props.sidebarRight !== nextProps.sidebarRight)) {
if (nextProps.colorBy.startsWith("gt")) {
this.state.chart.render(this.getChartGeom(nextProps), nextProps.mutType === "aa", parseEncodedGenotype(nextProps.colorBy));
} else {
@@ -5,7 +5,6 @@ import Select from "react-select";
import { sidebarField } from "../../globalStyles";
import { defaultColorBy, controlsWidth } from "../../util/globals";
import { changeColorBy } from "../../actions/colors";
import { modifyURLquery } from "../../util/urlHelpers";
import { analyticsControlsEvent } from "../../util/googleAnalytics";
/* Why does this have colorBy set as state (here) and in redux?
@@ -51,8 +50,7 @@ class ColorBy extends React.Component {
setColorBy(colorBy) {
if (colorBy.slice(0, 2) !== "gt") {
analyticsControlsEvent(`color-by-${colorBy}`);
this.props.dispatch(changeColorBy(colorBy));
modifyURLquery(this.context.router, {c: colorBy}, true);
this.props.dispatch(changeColorBy(colorBy, this.context.router));
this.setState({colorBySelected: colorBy});
} else {
// don't update colorBy yet, genotype still needs to be specified
@@ -140,8 +138,7 @@ class ColorBy extends React.Component {
}
const colorBy = "gt-" + gene + "_" + position;
analyticsControlsEvent("color-by-genotype");
this.props.dispatch(changeColorBy(colorBy));
modifyURLquery(this.context.router, {c: colorBy}, true);
this.props.dispatch(changeColorBy(colorBy, this.context.router));
}
getStyles() {
@@ -15,6 +15,7 @@ import MapAnimationControls from "./map-animation";
import { controlsWidth, enableAnimationDisplay } from "../../util/globals";
import { titleStyles } from "../../globalStyles";
import DataSource from "./data-source";
import Toggle from "./toggle";
const header = (text) => (
<span style={titleStyles.small}>
@@ -48,6 +49,7 @@ class Controls extends React.Component {
// restore <ToggleBranchLabels/> below when perf is improved
render() {
const mapAndTree = this.props.panels !== undefined && this.props.panels.indexOf("map") !== -1 && this.props.panels.indexOf("tree") !== -1;
return (
<Flex
direction="column"
@@ -260,7 +260,8 @@ class Footer extends React.Component {
horizontal: 1,
vertical: 0.3333333,
browserDimensions: this.props.browserDimensions,
sidebar: this.props.sidebar
sidebar: this.props.sidebar,
sidebarRight: this.props.sidebarRight
});
const width = responsive.width - 30; // need to subtract margin when calculating div width
return (
Oops, something went wrong.

0 comments on commit 9661f6d

Please sign in to comment.