diff --git a/scripts/get-data.sh b/scripts/get-data.sh index 73dbb84fc..38d757cfa 100755 --- a/scripts/get-data.sh +++ b/scripts/get-data.sh @@ -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" diff --git a/src/components/main/index.js b/src/components/main/index.js index 8aa27ff41..bcf1e93e6 100644 --- a/src/components/main/index.js +++ b/src/components/main/index.js @@ -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"; @@ -141,6 +142,7 @@ class Main extends React.Component { } {this.props.displayNarrative || this.props.showOnlyPanels ? null : } {this.props.panelsToDisplay.includes("tree") ? : null} + {this.props.panelsToDisplay.includes("map") ? : null} {this.props.panelsToDisplay.includes("entropy") ? ( diff --git a/src/components/map/map.js b/src/components/map/map.js index 3ec2b2c5b..30753399a 100644 --- a/src/components/map/map.js +++ b/src/components/map/map.js @@ -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 + 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: [{}, {}] } */ @@ -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 + 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; diff --git a/src/components/map/mapHelpersLatLong.js b/src/components/map/mapHelpersLatLong.js index 8a213e18b..9119bd9e1 100644 --- a/src/components/map/mapHelpersLatLong.js +++ b/src/components/map/mapHelpersLatLong.js @@ -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 */ @@ -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 */ diff --git a/src/components/states/states.js b/src/components/states/states.js new file mode 100644 index 000000000..497495fa6 --- /dev/null +++ b/src/components/states/states.js @@ -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 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 ( + + {this.props.legend && ( + + + + )} + {this.svgDomRef = c;}} + /> + + ); + } + +} + +function setUpDataStructures(props) { + const locationToVisibleNodes = getVisibleNodesPerLocation(props.nodes, props.visibility, props.geoResolution); + const demeData = []; + const demeIndices = []; // not useful since we never use triplicate for + + 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 '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 '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 + 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;