Skip to content
Permalink
Browse files

Merge pull request #712 from nextstrain/map-improvements

Map improvements including transmission ribbons
  • Loading branch information...
trvrb committed Mar 11, 2019
2 parents 2bfe1dc + c45803a commit e21bffa177a77aa1d2ce7fcf562dcc7dbab14fb9
@@ -5,7 +5,7 @@ import leaflet from "leaflet";
import _min from "lodash/min";
import _max from "lodash/max";
import { select } from "d3-selection";
import 'd3-transition'
import 'd3-transition';
import leafletImage from "leaflet-image";
import Card from "../framework/card";
import { drawDemesAndTransmissions, updateOnMoveEnd, updateVisibility } from "./mapHelpers";
@@ -189,11 +189,9 @@ class Map extends React.Component {
if (!this.state.boundsSet) { // we are doing the initial render -> set map to the range of the data
const SWNE = this.getGeoRange();
// L. available because leaflet() was called in componentWillMount
this.state.map.fitBounds(L.latLngBounds(SWNE[0], SWNE[1])); // eslint-disable-line no-undef
this.state.map.fitBounds(L.latLngBounds(SWNE[0], SWNE[1]));
}

this.state.map.setMaxBounds(this.getBounds());

const {
demeData,
transmissionData,
@@ -209,10 +207,15 @@ class Map extends React.Component {
this.props.metadata,
this.state.map
);
if (demesMissingLatLongs.size) {

const filteredDemesMissingLatLongs = [...demesMissingLatLongs].filter((value) => {
return value !== "Unknown" || value !== "unknown";
});

if (filteredDemesMissingLatLongs.size) {
this.props.dispatch(errorNotification({
message: "The following demes are missing lat/long information",
details: [...demesMissingLatLongs].join(", ")
details: [...filteredDemesMissingLatLongs].join(", ")
}));
}

@@ -263,10 +266,6 @@ class Map extends React.Component {
const geoResolutionChanged = this.props.geoResolution !== nextProps.geoResolution;
const dataChanged = (!nextProps.treeLoaded || this.props.treeVersion !== nextProps.treeVersion);

// (this.props.colorBy !== nextProps.colorBy ||
// this.props.visibilityVersion !== nextProps.visibilityVersion ||
// this.props.colorScale.version !== nextProps.colorScale.version);

if (mapIsDrawn && (geoResolutionChanged || dataChanged)) {
this.state.d3DOMNode.selectAll("*").remove();

@@ -319,10 +318,11 @@ class Map extends React.Component {
const minLng = _min(longitudes);
const lngRange = (maxLng - minLng) % 360;
const latRange = (maxLat - minLat);
const south = _max([-80, minLat - (0.2 * latRange)]);
const north = _min([80, maxLat + (0.2 * latRange)]);
const east = _max([-180, minLng - (0.2 * lngRange)]);
const west = _min([180, maxLng + (0.2 * lngRange)]);
const south = _max([-55, minLat - (0.1 * latRange)]);
const north = _min([70, maxLat + (0.1 * latRange)]);
const east = _max([-180, minLng - (0.1 * lngRange)]);
const west = _min([180, maxLng + (0.1 * lngRange)]);

return [L.latLng(south, west), L.latLng(north, east)];
}
/**
@@ -339,7 +339,7 @@ class Map extends React.Component {
colorOrVisibilityChange &&
haveData
) {
timerStart("updateDemesAndTransmissions")
timerStart("updateDemesAndTransmissions");
const { newDemes, newTransmissions } = updateDemeAndTransmissionDataColAndVis(
this.state.demeData,
this.state.transmissionData,
@@ -367,11 +367,11 @@ class Map extends React.Component {
demeData: newDemes,
transmissionData: newTransmissions
});
timerEnd("updateDemesAndTransmissions")
timerEnd("updateDemesAndTransmissions");
}
}

getBounds() {
getInitialBounds() {
let southWest;
let northEast;

@@ -388,6 +388,7 @@ class Map extends React.Component {

return bounds;
}

createMap() {

const zoom = 2;
@@ -401,9 +402,10 @@ class Map extends React.Component {
center: center,
zoom: zoom,
scrollWheelZoom: false,
maxBounds: this.getBounds(),
maxBounds: this.getInitialBounds(),
minZoom: 2,
maxZoom: 10,
zoomSnap: 0.5,
zoomControl: false,
/* leaflet sleep see https://cliffcloud.github.io/Leaflet.Sleep/#summary */
// true by default, false if you want a wild map
@@ -457,14 +459,17 @@ class Map extends React.Component {
>
{this.props.animationPlayPauseButton}
</button>
<button style={{...buttonBaseStyle, top: 20, left: 88, width: 60, backgroundColor: lightGrey}} onClick={this.resetButtonClicked}>
<button
style={{...buttonBaseStyle, top: 20, left: 88, width: 60, backgroundColor: lightGrey}}
onClick={this.resetButtonClicked}
>
Reset
</button>
</div>
);
}
/* else - divOnly */
return (<div></div>);
return (<div/>);
}

maybeCreateMapDiv() {
@@ -1,7 +1,9 @@
import _findIndex from "lodash/findIndex";
import _findLastIndex from "lodash/findLastIndex";
import _max from "lodash/max";
import { line, curveBasis } from "d3-shape";
import { easeLinear } from "d3-ease";
import { demeCountMultiplier, demeCountMinimum } from "../../util/globals";

/* util */

@@ -151,13 +153,16 @@ export const drawDemesAndTransmissions = (
.attr("stroke-width", 1);

// add demes
const demeMultiplier = demeCountMultiplier / Math.sqrt(_max([nodes.length, demeCountMinimum]));
const demes = g.selectAll("demes")
.data(demeData)
.enter().append("circle")
.style("stroke", "none")
.style("fill-opacity", 0.65)
.style("fill", (d) => { return d.color; })
.attr("r", (d) => { return 4 * Math.sqrt(d.count); })
.style("stroke-opacity", 0.85)
.style("stroke", (d) => { return d.color; })
.attr("r", (d) => { return demeMultiplier * Math.sqrt(d.count); })
.attr("transform", (d) => {
return "translate(" + d.coords.x + "," + d.coords.y + ")";
});
@@ -209,13 +214,14 @@ export const updateVisibility = (
numDateMax
) => {

const demeMultiplier = demeCountMultiplier / Math.sqrt(_max([nodes.length, demeCountMinimum]));
d3elems.demes
.data(demeData)
.transition()
.duration(200)
.ease(easeLinear)
.style("fill", (d) => { return d.count > 0 ? d.color : "white"; })
.attr("r", (d) => { return 4 * Math.sqrt(d.count); });
.attr("r", (d) => { return demeMultiplier * Math.sqrt(d.count); });

d3elems.transmissions
.data(transmissionData)
@@ -246,39 +252,6 @@ export const updateFoo = (d3elems, latLongs) => {
.data(latLongs.transmissions);
};

// const missiles = transmissionPaths.map((transmissionPath) => {
//
// // console.log(transmissionPath)
//
// const missile = g.append("circle")
// .attr("r", 0)
// .attr("fill", (d) => { return colorScale(transmissionPath.transmission.to) })
// .attr("transform", `translate(
// ${transmissionPath.partialTransmission[0].x},
// ${transmissionPath.partialTransmission[0].y}
// )`) /* begin the missile on the start of the line */
// // .transition()
// // .duration(5000)
// // .attrTween("transform", translateAlong(transmissionPath.elem.node()));
//
// return missile;
// })

// const setTipCoords = () => {
// demes.attr("transform", (d) => {
// return "translate(" + d.coords.x + "," + d.coords.y + ")";
// });
// };

// const animateTransmissions = () => {
// /* point along path interpolation https://bl.ocks.org/mbostock/1705868 */
//
// }

// setTipCoords();
// map.on("viewreset", setTipCoords); /* search: -Note (A) for an idea as to why this might not be working properly */
// animateTransmissions();

/*
http://gis.stackexchange.com/questions/49114/d3-geo-path-to-draw-a-path-from-gis-coordinates
https://bl.ocks.org/mbostock/3916621 // Compute point-interpolators at each distance.
@@ -291,6 +264,3 @@ export const updateFoo = (d3elems, latLongs) => {
https://github.com/d3/d3-shape/blob/master/README.md#curves
https://github.com/d3/d3-shape/blob/master/README.md#lines
*/

// since we're now using d3 we'll probably do something like http://stackoverflow.com/questions/11808860/arrow-triangles-on-my-svg-line/11809868#11809868
// decorator docs: https://github.com/bbecquet/Leaflet.PolylineDecorator
@@ -3,7 +3,7 @@ import _map from "lodash/map";
import _minBy from "lodash/minBy";
import { interpolateNumber } from "d3-interpolate";
import { averageColors } from "../../util/colorHelpers";
import { computeMidpoint, Bezier } from "./transmissionBezier";
import { bezier } from "./transmissionBezier";
import { NODE_NOT_VISIBLE } from "../../util/globals";


@@ -119,17 +119,10 @@ const setupDemeData = (nodes, visibility, geoResolution, nodeColors, triplicate,

const constructBcurve = (
originLatLongPair,
destinationLatLongPair
destinationLatLongPair,
extend
) => {
const midpoint = computeMidpoint(originLatLongPair, destinationLatLongPair);
const Bcurve = Bezier(
[
originLatLongPair,
midpoint,
destinationLatLongPair
]
);
return Bcurve;
return bezier(originLatLongPair, destinationLatLongPair, extend);
};

const maybeConstructTransmissionEvent = (
@@ -142,7 +135,8 @@ const maybeConstructTransmissionEvent = (
map,
offsetOrig,
offsetDest,
demesMissingLatLongs
demesMissingLatLongs,
extend
) => {
let latOrig, longOrig, latDest, longDest;
let transmission;
@@ -174,7 +168,7 @@ const maybeConstructTransmissionEvent = (

if (validLatLongPair) {

const Bcurve = constructBcurve(validLatLongPair[0], validLatLongPair[1]);
const Bcurve = constructBcurve(validLatLongPair[0], validLatLongPair[1], extend);

/* set up interpolator with origin and destination numdates */
const interpolator = interpolateNumber(node.attr.num_date, child.attr.num_date);
@@ -206,7 +200,8 @@ const maybeConstructTransmissionEvent = (
originNumDate: node.attr["num_date"],
destinationNumDate: child.attr["num_date"],
color: nodeColors[node.arrayIdx],
visible: visibility[child.arrayIdx] !== NODE_NOT_VISIBLE ? "visible" : "hidden" // transmission visible if child is visible
visible: visibility[child.arrayIdx] !== NODE_NOT_VISIBLE ? "visible" : "hidden", // transmission visible if child is visible
extend: extend
};
}
return transmission;
@@ -221,7 +216,8 @@ const maybeGetClosestTransmissionEvent = (
visibility,
map,
offsetOrig,
demesMissingLatLongs
demesMissingLatLongs,
extend
) => {
const possibleEvents = [];
// iterate over offsets applied to transmission destination
@@ -237,7 +233,8 @@ const maybeGetClosestTransmissionEvent = (
map,
offsetOrig,
offsetDest,
demesMissingLatLongs
demesMissingLatLongs,
extend
);
if (t) { possibleEvents.push(t); }
});
@@ -270,14 +267,20 @@ const setupTransmissionData = (
const transmissionData = []; /* edges, animation paths */
const transmissionIndices = {}; /* map of transmission id to array of indices */
const demesMissingLatLongs = new Set();
const demeToDemeCounts = {};
nodes.forEach((n) => {
const nodeDeme = n.attr[geoResolution];
if (n.children) {
n.children.forEach((child) => {
if (
n.attr[geoResolution] &&
child.attr[geoResolution] &&
n.attr[geoResolution] !== child.attr[geoResolution]
) {
const childDeme = child.attr[geoResolution];
if (nodeDeme && childDeme && nodeDeme !== childDeme) {
// record transmission event
if ([nodeDeme, childDeme] in demeToDemeCounts) {
demeToDemeCounts[[nodeDeme, childDeme]] += 1;
} else {
demeToDemeCounts[[nodeDeme, childDeme]] = 1;
}
const extend = demeToDemeCounts[[nodeDeme, childDeme]];
// offset is applied to transmission origin
offsets.forEach((offsetOrig) => {
const t = maybeGetClosestTransmissionEvent(
@@ -289,7 +292,8 @@ const setupTransmissionData = (
visibility,
map,
offsetOrig,
demesMissingLatLongs
demesMissingLatLongs,
extend
);
if (t) { transmissionData.push(t); }
});
@@ -473,7 +477,7 @@ const updateTransmissionDataLatLong = (transmissionData, map) => {
transmission.bezierCurve = constructBcurve(
transmission.originCoords,
transmission.destinationCoords,
transmission.destinationNumDate
transmission.extend
);
});

Oops, something went wrong.

0 comments on commit e21bffa

Please sign in to comment.
You can’t perform that action at this time.