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/0531eb1383365ac6afd5f115775e3b08 to your computer and use it in GitHub Desktop.
Save michaschwab/0531eb1383365ac6afd5f115775e3b08 to your computer and use it in GitHub Desktop.
Windmap without SSVG

Animated Wind Map without 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.

This version here is not using SSVG, and renders up to 8X slower than the version with SSVG. You can see the version with SSVG here. 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://ssvg.surge.sh/ssvg-auto.js"></script>--> <!-- This would enable the speed improvements! -->
<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