Skip to content

Instantly share code, notes, and snippets.

@michaschwab
Last active February 21, 2019 22:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save michaschwab/1c5c2f5862b9faba9e19f64f236690ed to your computer and use it in GitHub Desktop.
Save michaschwab/1c5c2f5862b9faba9e19f64f236690ed to your computer and use it in GitHub Desktop.
SSVG Windmap

Animated Wind Map with SSVG

This example shows the ease of creating complicated visualizations with SVG, and having them rendered smoothly as multi-process canvas applications using Scalable Scalable Vector Graphics.

You can see the version without SSVG here (warning: slow). The only difference is inclusion of this script tag:

<script src="https://intervis-projects.ccs.neu.edu/ssvg/ssvg-auto.js"></script>

All design credit goes to by Martin Viégas and Martin Wattenberg, as the design is heavily inspired by their Wind Map.

The technique to animate a fading path in SVG is more succinctly shown on this block.

function getParsedData(response) {
const data = {};
// [0] is temperature. [1] is U component of wind, [2] is V component of wind.
for (let i = 1; i < response.length; i++) {
const dataPart = response[i];
const startLatitude = Math.min(dataPart.header.la1, dataPart.header.la2);
const endLatitude = Math.max(dataPart.header.la1, dataPart.header.la2);
const latitudePoints = endLatitude - startLatitude + 1;
const startLongitude = Math.min(dataPart.header.lo1, dataPart.header.lo2);
const endLongitude = Math.max(dataPart.header.lo1, dataPart.header.lo2);
const longitudePoints = endLongitude - startLongitude + 1;
const windComponent = i === 1 ? 'u' : 'v';
// "outer loop" is latitude, "inner loop" is longitude.
let dataIndex = 0;
let outerLoopIndex = 0;
let innerLoopIndex = 0;
for (let value of dataPart.data) {
outerLoopIndex = Math.floor(dataIndex / longitudePoints);
innerLoopIndex = dataIndex - outerLoopIndex * longitudePoints;
let latitude = startLatitude + outerLoopIndex;
let longitude = startLongitude + innerLoopIndex;
if (!data[latitude]) {
data[latitude] = {};
}
if (!data[latitude][longitude]) {
data[latitude][longitude] = {u: 0, v: 0};
}
data[latitude][longitude][windComponent] = value;
dataIndex++;
}
}
return data;
}
function getForcePerLatLon(data, lat, lon) {
const lowerLat = Math.floor(lat);
const upperLat = lowerLat + 1;
const upperLatRatio = lat - lowerLat;
const lowerLatRatio = 1 - upperLatRatio;
const lowerLon = Math.floor(lon);
const upperLon = lowerLon + 1;
const upperLonRatio = lon - lowerLon;
const lowerLonRatio = 1 - upperLonRatio;
const lowLow = data[lowerLat][lowerLon];
const lowHigh = data[lowerLat][upperLon];
const highLow = data[upperLat][lowerLon];
const highHigh = data[upperLat][upperLon];
let v = 0;
v += lowLow.v * lowerLatRatio * lowerLonRatio;
v += lowHigh.v * lowerLatRatio * upperLonRatio;
v += highLow.v * upperLatRatio * lowerLonRatio;
v += highHigh.v * upperLatRatio * upperLonRatio;
let u = 0;
u += lowLow.u * lowerLatRatio * lowerLonRatio;
u += lowHigh.u * lowerLatRatio * upperLonRatio;
u += highLow.u * upperLatRatio * lowerLonRatio;
u += highHigh.u * upperLatRatio * upperLonRatio;
return {u: u, v: v};
}
function getRandomPoint() {
return {
lat: 25 + Math.random() * (60 - 25),
lon: 230 + Math.random() * (300 - 230)
};
}
function getPointAtLength(path, length) {
let lengthPercentage = (length / path.length) % 1.0000001;
if (lengthPercentage < 0) {
lengthPercentage = 1 + lengthPercentage;
}
const index = Math.floor((path.points.length - 1) * lengthPercentage);
return path.points[index];
}
function createPath(projection, data) {
const points = [getRandomPoint()];
const xy = projection([points[0].lon, points[0].lat]);
points[0].x = xy ? xy[0] : undefined;
points[0].y = xy ? xy[1] : undefined;
let length = 0;
const dataProgressionRate = 0.005;
const pathLength = 2;
const numberOfDataPoints = pathLength / dataProgressionRate;
for (let progress = 0; progress < numberOfDataPoints; progress++) {
const lastPoint = points[points.length - 1];
const wind = getForcePerLatLon(data, lastPoint.lat, lastPoint.lon);
const nextPoint = {
lat: lastPoint.lat + wind.v * dataProgressionRate,
lon: lastPoint.lon + wind.u * dataProgressionRate,
x: 0,
y: 0
};
const xy = projection([nextPoint.lon, nextPoint.lat]);
if (xy) {
nextPoint.x = xy[0];
nextPoint.y = xy[1];
length += Math.sqrt(Math.pow(nextPoint.x - lastPoint.x, 2) + Math.pow(nextPoint.y - lastPoint.y, 2));
points.push(nextPoint);
}
}
return {
length: length,
points: points,
progressPercent: 0
};
}
<html>
<head>
<style>
#map path {
fill: #333;
}
line {
stroke: rgb(255, 255, 255);
}
</style>
</head>
<body>
<svg width="960" height="600">
<g id="map"></g>
<path id="alaska-blocker" fill="#fff" d="M0,300 L500,500 L0,500"></path>
<g id="traces"></g>
</svg>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://d3js.org/topojson.v2.min.js"></script>
<script src="https://intervis-projects.ccs.neu.edu/ssvg/ssvg-auto.js"></script>
<script src="data-processing.js"></script>
<script>
const svg = d3.select("svg");
const mapG = svg.select('#map');
const tracesG = svg.select('#traces');
const projection = d3.geoAlbersUsa();
const path = d3.geoPath()
.projection(projection);
d3.json("us.json").then(us => {
mapG.append("path")
.attr("class", "states")
.datum(topojson.feature(us, us.objects.states))
.attr("d", path);
$.get({url: "http://michailschwab.com/data/wind-data-feb-21-2019.json"}).then(response => {
const data = getParsedData(response);
let pathDs = [];
const numberOfSegments = 4;
let segmentLength = 5;
const raf = () => {
for (let i = 0; i < Math.random() * 100; i++) {
pathDs.push(createPath(projection, data));
}
pathDs = pathDs.filter(p => p.progressPercent < 150);
const segmentData = [];
for(let d of pathDs) {
const guideLength = d.length;
if (!guideLength) {
continue;
}
d.progressPercent += 0.4; // Move 0.4% of the guide length per frame.
const progressLength = d.progressPercent * guideLength / 100;
for (let i = 0; i < numberOfSegments; i++) {
let startDistance = progressLength + segmentLength * (i - numberOfSegments);
let endDistance = progressLength + segmentLength * (i - numberOfSegments + 1);
startDistance = startDistance < 0 ? 0 : startDistance;
endDistance = endDistance < 0 ? 0 : endDistance;
startDistance = startDistance > guideLength ? guideLength : startDistance;
endDistance = endDistance > guideLength ? guideLength : endDistance;
segmentData.push({
start: getPointAtLength(d, startDistance),
end: getPointAtLength(d, endDistance),
index: i
});
}
}
const segmentLines = tracesG
.selectAll('line')
.data(segmentData);
const pathsEnter = segmentLines.enter()
.append('line')
.attr('opacity', (d) => (d.index + 1) / (numberOfSegments + 1));
pathsEnter.merge(segmentLines)
.attr('x1', d => d.start.x)
.attr('y1', d => d.start.y)
.attr('x2', d => d.end.x)
.attr('y2', d => d.end.y);
requestAnimationFrame(raf);
};
raf();
});
}, err => {
console.error(err);
});
</script>
</body>
</html>
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment