Skip to content

Commit

Permalink
wip: prototype node states graph
Browse files Browse the repository at this point in the history
  • Loading branch information
jameshadfield committed Jul 16, 2020
1 parent 631fa8d commit 762dff7
Show file tree
Hide file tree
Showing 5 changed files with 269 additions and 2 deletions.
8 changes: 8 additions & 0 deletions scripts/get-data.sh
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,12 @@ do
curl http://data.nextstrain.org/"${i}" --compressed -o data/"${i}"
done

staging_files=(
"testing_states.json" \
)
for i in "${staging_files[@]}"
do
curl http://staging.nextstrain.org/"${i}" --compressed -o data/"${i}"
done

echo "The local data directory ./data now contains up-to-date datasets from http://data.nextstrain.org"
2 changes: 2 additions & 0 deletions src/components/main/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import SidebarToggle from "../framework/sidebar-toggle";
import Info from "../info/info";
import Tree from "../tree";
import Map from "../map/map";
import States from "../states/states";
import { controlsHiddenWidth } from "../../util/globals";
import Footer from "../framework/footer";
import DownloadModal from "../download/downloadModal";
Expand Down Expand Up @@ -141,6 +142,7 @@ class Main extends React.Component {
}
{this.props.displayNarrative || this.props.showOnlyPanels ? null : <Info width={calcUsableWidth(availableWidth, 1)} />}
{this.props.panelsToDisplay.includes("tree") ? <Tree width={big.width} height={big.height} /> : null}
<States legend width={big.width} height={big.height} />
{this.props.panelsToDisplay.includes("map") ? <Map width={big.width} height={big.height} justGotNewDatasetRenderNewMap={false} legend={this.shouldShowMapLegend()} /> : null}
{this.props.panelsToDisplay.includes("entropy") ?
(<Suspense fallback={null}>
Expand Down
7 changes: 7 additions & 0 deletions src/components/map/map.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,9 @@ class Map extends React.Component {
// const initialVisibilityVersion = this.props.visibilityVersion === 1; /* see tree reducer, we set this to 1 after tree comes back */
// const newVisibilityVersion = this.props.visibilityVersion !== prevProps.visibilityVersion;

// following is temporary and only for prototyping <States>
if (!this.props.metadata.geoResolutions.filter((x) => x.key === this.props.geoResolution)[0].demes) return;

if (mapIsDrawn && allDataPresent && demesTransmissionsNotComputed) {
timerStart("drawDemesAndTransmissions");
/* data structures to feed to d3 latLongs = { tips: [{}, {}], transmissions: [{}, {}] } */
Expand Down Expand Up @@ -365,6 +368,10 @@ class Map extends React.Component {
*/
maybeUpdateDemesAndTransmissions(nextProps) {
if (!this.state.map || !this.props.treeLoaded || !this.state.d3elems) { return; }

// following is temporary and only for prototyping <States>
if (!nextProps.metadata.geoResolutions.filter((x) => x.key === nextProps.geoResolution)[0].demes) return;

const visibilityChange = nextProps.visibilityVersion !== this.props.visibilityVersion;
const haveData = nextProps.nodes && nextProps.visibility && nextProps.geoResolution && nextProps.nodeColors;

Expand Down
4 changes: 2 additions & 2 deletions src/components/map/mapHelpersLatLong.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const maybeGetTransmissionPair = (latOrig, longOrig, latDest, longDest, map) =>
* Traverses the tips of the tree to create a dict of
* location(deme) -> list of visible tips at that location
*/
const getVisibleNodesPerLocation = (nodes, visibility, geoResolution) => {
export const getVisibleNodesPerLocation = (nodes, visibility, geoResolution) => {
const locationToVisibleNodes = {};
nodes.forEach((n, i) => {
if (n.children) return; /* only consider terminal nodes */
Expand All @@ -73,7 +73,7 @@ const getVisibleNodesPerLocation = (nodes, visibility, geoResolution) => {
* @param {array} currentArcs only used if updating. Array of current arcs.
* @returns {array} arcs for display
*/
const createOrUpdateArcs = (visibleNodes, legendValues, colorBy, nodeColors, currentArcs=undefined) => {
export const createOrUpdateArcs = (visibleNodes, legendValues, colorBy, nodeColors, currentArcs=undefined) => {
const colorByIsGenotype = isColorByGenotype(colorBy);
const legendValueToArcIdx = {};
const undefinedArcIdx = legendValues.length; /* the arc which is grey to represent undefined values on tips */
Expand Down
250 changes: 250 additions & 0 deletions src/components/states/states.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
import React from "react";
import { connect } from "react-redux";
import { withTranslation } from "react-i18next";
import { select } from "d3-selection";
import { interpolateNumber } from "d3-interpolate";
import ErrorBoundary from "../../util/errorBoundry";
import Legend from "../tree/legend/legend";
import Card from "../framework/card";
import { getVisibleNodesPerLocation, createOrUpdateArcs } from "../map/mapHelpersLatLong";
import { getAverageColorFromNodes } from "../../util/colorHelpers";
import { drawDemesAndTransmissions } from "../map/mapHelpers";
import { getTraitFromNode } from "../../util/treeMiscHelpers";
import { bezier } from "../map/transmissionBezier";
import { NODE_NOT_VISIBLE } from "../../util/globals";

/**
* This is a prototype.
* There are numerous calls into functions and use of data structures designed
* for the <Map> component. These are unnecessarily complex for this use case,
* but are employed to simplify the creation of a prototype without needing
* to refactor shared functions or duplicate code.
*/

@connect((state) => {
return {
branchLengthsToDisplay: state.controls.branchLengthsToDisplay,
absoluteDateMin: state.controls.absoluteDateMin,
absoluteDateMax: state.controls.absoluteDateMax,
nodes: state.tree.nodes,
nodeColors: state.tree.nodeColors,
visibility: state.tree.visibility,
metadata: state.metadata,
geoResolution: state.controls.geoResolution,
dateMinNumeric: state.controls.dateMinNumeric,
dateMaxNumeric: state.controls.dateMaxNumeric,
colorBy: state.controls.colorScale.colorBy,
pieChart: (
!state.controls.colorScale.continuous && // continuous color scale = no pie chart
state.controls.geoResolution !== state.controls.colorScale.colorBy // geo circles match colorby == no pie chart
),
legendValues: state.controls.colorScale.legendValues,
showTransmissionLines: state.controls.showTransmissionLines
};
})
class States extends React.Component {
constructor(props) {
super(props);
this.svgDomRef = null;
}
redraw(props) {
// prototype. Recreate data every update & redraw.
const {demeData, demeIndices, transmissionData, transmissionIndices} = setUpDataStructures(props); // eslint-disable-line
// console.log("redraw()");
// console.log("demeData", demeData);
// console.log("demeIndices", demeIndices);
// console.log("transmissionData", transmissionData);
// console.log("transmissionIndices", transmissionIndices);
renderDemes({svgDomRef: this.svgDomRef, demeData, transmissionData, ...props});
}
componentDidMount() {
// console.log("\n\n----------CDM-------------");
this.redraw(this.props);
}
componentWillReceiveProps(nextProps) {
// console.log("\n\n----------CWRP-------------");
this.redraw(nextProps);
}

render() {
const { t } = this.props;
return (
<Card center title={t("Node States")}>
{this.props.legend && (
<ErrorBoundary>
<Legend right width={this.props.width} />
</ErrorBoundary>
)}
<svg
id="NodeStatesGraph"
style={{pointerEvents: "auto", cursor: "default", userSelect: "none"}}
width={this.props.width}
height={this.props.height}
ref={(c) => {this.svgDomRef = c;}}
/>
</Card>
);
}

}

function setUpDataStructures(props) {
const locationToVisibleNodes = getVisibleNodesPerLocation(props.nodes, props.visibility, props.geoResolution);
const demeData = [];
const demeIndices = []; // not useful since we never use triplicate for <States>

const getCoord = coordFactory(props.width, props.height, Object.keys(locationToVisibleNodes).length)

Object.entries(locationToVisibleNodes).forEach(([location, visibleNodes], index) => {
const deme = {
name: location,
count: visibleNodes.length,
coords: getCoord(index)
};
if (props.pieChart) {
/* create the arcs for the pie chart. NB `demeDataIdx` is the index of the deme in `demeData` where this will be inserted */
deme.arcs = createOrUpdateArcs(visibleNodes, props.legendValues, props.colorBy, props.nodeColors);
/* create back links between the arcs & which index of `demeData` they (will be) stored at */
deme.arcs.forEach((a) => {a.demeDataIdx = index;});
} else {
/* average out the constituent colours for a blended-colour circle */
deme.color = getAverageColorFromNodes(visibleNodes, props.nodeColors);
}
demeData.push(deme);
demeIndices[location] = [index];
});

const {transmissionData, transmissionIndices} = setUpTransmissions(
props.showTransmissionLines,
props.nodes,
props.visibility,
props.geoResolution,
demeData,
demeIndices,
props.nodeColors
);

return {demeData, demeIndices, transmissionData, transmissionIndices};
}


function renderDemes({svgDomRef, demeData, transmissionData, nodes, dateMinNumeric, dateMaxNumeric, pieChart, geoResolution, dispatch}) {
const svg = select(svgDomRef);
svg.selectAll("*").remove();
const g = svg.append("g").attr("id", "StateDemes");

drawDemesAndTransmissions(
demeData,
transmissionData,
g,
null, // not used in fn!
nodes,
dateMinNumeric,
dateMaxNumeric,
pieChart,
geoResolution,
dispatch
);

// draw text labels over each deme
g.selectAll("demeLabels")
.data(demeData)
.enter()
.append("text")
.attr("x", (d) => d.coords.x + 10)
.attr("y", (d) => d.coords.y)
.text((d) => d.name)
.attr("class", "tipLabel")
.style("font-size", "12px");
}


function setUpTransmissions(showTransmissionLines, nodes, visibility, geoResolution, demeData, demeIndices, nodeColors) {
/* similar to the <Map>'s setupTransmissionData */
const transmissionData = []; /* edges, animation paths */
const transmissionIndices = {}; /* map of transmission id to array of indices. Only used for updating? */
const demeToDemeCounts = {}; /* Used to ompute the "extend" so that curves don't sit on top of each other */

if (!showTransmissionLines) return {transmissionData, transmissionIndices};

/* loop through nodes and compare each with its own children to get A->B transmissions */
nodes.forEach((n) => {
const nodeDeme = getTraitFromNode(n, geoResolution);
if (n.children) {
n.children.forEach((child) => {
const childDeme = getTraitFromNode(child, geoResolution);
if (nodeDeme && childDeme && nodeDeme !== childDeme) {

// Keep track of how many we've seen from A->B in order to get a curve's "extend"
if ([nodeDeme, childDeme] in demeToDemeCounts) {
demeToDemeCounts[[nodeDeme, childDeme]] += 1;
} else {
demeToDemeCounts[[nodeDeme, childDeme]] = 1;
}
const extend = demeToDemeCounts[[nodeDeme, childDeme]];

// compute a bezier curve
// logic following the <Map>'s maybeConstructTransmissionEvent
// console.log(`TRANSMISSION! ${nodeDeme} -> ${childDeme}, ${extend}`);
const nodeCoords = demeData[demeIndices[nodeDeme]].coords;
const childCoords = demeData[demeIndices[childDeme]].coords;
const bezierCurve = bezier(nodeCoords, childCoords, extend);
/* set up interpolator with origin and destination numdates */
const nodeDate = getTraitFromNode(n, "num_date");
const childDate = getTraitFromNode(child, "num_date");
const interpolator = interpolateNumber(nodeDate, childDate);
/* make a bezierDates array as long as bezierCurve */
const bezierDates = bezierCurve.map((d, i) => {
return interpolator(i / (bezierCurve.length - 1));
});

// following data structure same as in <Map>
const transmission = {
id: n.arrayIdx.toString() + "-" + child.arrayIdx.toString(),
originNode: n,
destinationNode: child,
bezierCurve,
bezierDates,
originName: nodeDeme,
destinationName: childDeme,
originCoords: nodeCoords, // after interchange
destinationCoords: childCoords, // after interchange
originNumDate: nodeDate,
destinationNumDate: childDate,
color: nodeColors[n.arrayIdx], // colour given by *origin* node
visible: visibility[child.arrayIdx] !== NODE_NOT_VISIBLE ? "visible" : "hidden", // transmission visible if child is visible
extend: extend
};
transmissionData.push(transmission);
}
});
}
});

transmissionData.forEach((transmission, index) => {
if (!transmissionIndices[transmission.id]) {
transmissionIndices[transmission.id] = [index];
} else {
transmissionIndices[transmission.id].push(index);
}
});

return {transmissionData, transmissionIndices};
}

function coordFactory(width, height, n) {
const x0 = width/2;
const y0 = height/2;
const t = 2 * Math.PI / (n+1);
const r = Math.min(width, height) * 0.40;
return (index) => {
return {
x: x0+r*Math.cos(t*index),
y: y0+r*Math.sin(t*index)
};
};
}


const WithTranslation = withTranslation()(States);
export default WithTranslation;

0 comments on commit 762dff7

Please sign in to comment.