Skip to content

Instantly share code, notes, and snippets.

@weedkiller
Created July 21, 2018 05:52
Show Gist options
  • Save weedkiller/1bd987beafaa688f17e3c715375efa57 to your computer and use it in GitHub Desktop.
Save weedkiller/1bd987beafaa688f17e3c715375efa57 to your computer and use it in GitHub Desktop.
Parcel location tracker
.container
.map
.map__inner#map
.sidebar
h1 Location History
.timeline#timeline
function Location(title, locName, date, lat, lng) {
this.title = title;
this.locName = locName;
this.date = date;
this.lat = lat;
this.lng = lng;
}
var data = [
new Location('Order processed', 'Phoenix, AZ, USA', new Date(2015, 3, 10), 33.43, -112.15),
new Location('Picked up by courier', 'Phoenix, AZ, USA', new Date(2015, 3, 11), 33.43, -112.15),
new Location('Departed depot', 'Phoenix, AZ, USA', new Date(2015, 3, 12), 33.67, -112.11),
new Location('Cleared customs', 'LAX, CA, USA', new Date(2015, 3, 15), 34.05, -117.65),
new Location('Departed country', 'LAX, CA, USA', new Date(2015, 3, 15), 34.05, -117.65),
new Location('Arrived in country', 'Auckland, NZ', new Date(2015, 3, 17), -37.00, 174.78),
new Location('Cleared customs', 'Auckland, NZ', new Date(2015, 3, 18), -37.00, 174.78),
new Location('Picked up by courier', 'Auckland, NZ', new Date(2015, 3, 18), -37.00, 174.78),
new Location('ETA', 'Hamilton, NZ', new Date(2015, 3, 20), -37.80, 175.28)
];
var colTop = "#27a4e5",
colBottom = "#27cfe5";
var map = L.map('map', {zoomControl: false, worldCopyJump: true}).setView([data[data.length - 1].lat, data[data.length - 1].lng], 4);
L.tileLayer('http://stamen-tiles-{s}.a.ssl.fastly.net/toner/{z}/{x}/{y}.{ext}', {
attribution: 'Map tiles by <a href="http://stamen.com">Stamen Design</a>, <a href="http://creativecommons.org/licenses/by/3.0">CC BY 3.0</a> &mdash; Map data &copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
subdomains: 'abcd',
minZoom: 0,
maxZoom: 20,
ext: 'png'
}).addTo(map);
var markerIcon = L.divIcon({className: 'map__marker'});
// Add a marker and popup for each data point
data.forEach(function(d) {
var marker = L.marker([d.lat, d.lng], { icon: markerIcon }).addTo(map);
marker.bindPopup(d.title + '<br>' + d.locName);
d.marker = marker;
});
// Timeline creation
var margin = {top: 10, right: 0, bottom: 50, left: 70},
width = 272 - margin.left - margin.right,
height = (data.length * 80) - margin.top - margin.bottom;
var vis = d3.select('#timeline')
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
var labelsGroup = vis.append('g')
.classed('timeline__labels', true);
var linksGroup = vis.append('g')
.classed('timeline__lines', true);
var timeScale = d3.time.scale()
.domain([data[0].date, data[data.length - 1].date])
.range([0, height]).nice(d3.time.day);
var colorScale = d3.time.scale()
.domain([0, height])
.range([colTop, colBottom]);
var axis = d3.svg.axis()
.scale(timeScale)
.orient('left')
.tickValues(data.map(function(d) { return d.date }))
.tickSize(0, 0)
.tickPadding(20);
var lineGradient = vis.append('linearGradient')
.attr('id', 'lineGradient')
.attr('x1', 0).attr('x2', margin.top)
.attr('y1', 0).attr('y2', height)
.attr("gradientUnits", "userSpaceOnUse");
lineGradient.append("stop")
.attr("offset", "0")
.attr("stop-color", colTop);
lineGradient.append("stop")
.attr("offset", "1")
.attr("stop-color", colBottom);
var axisEl = vis.append('g')
.classed('timeline__line', true)
.call(axis);
// Add a circle to each date on the timeline
axisEl.selectAll('.tick')
.append('circle')
.classed('timeline__point', true)
.attr('r', 6)
.attr('stroke', function(d) { return colorScale(timeScale(d)) });
// Moving the timeline line behind the points
var line = axisEl.select('.domain').remove();
axisEl.node().insertBefore(line.node(), axisEl.node().children[0]);
var labelPadding = 10; // Top/bottom padding around each label
var placeholder = labelsGroup.append('text');
// Getting the height of each label by adding each to the DOM then grabbing its bounds
var nodes = data.map(function(d) {
var bounds = placeholder.text(d.title)[0][0].getBBox();
return new labella.Node(timeScale(d.date), bounds.height + (labelPadding * 2), d);
});
placeholder.remove();
var renderer = new labella.Renderer({
layerGap: 20,
nodeHeight: 150,
direction: 'right'
});
function drawLabels(nodes) {
renderer.layout(nodes);
var labels = labelsGroup.selectAll('.timeline__label')
.data(nodes)
.enter()
.append('g')
.classed('timeline__label', true)
.attr('transform', function(d){ return 'translate(' + d.x + ',' + (d.y - d.dy/2) + ')'; });
labels.append('text')
.text(function(d) { return d.data.title })
.attr('dominant-baseline', 'central')
.attr('y', labelPadding)
.attr('dy', '0.55em');
labels.on('click', function(d) {
map.panTo(d.data.marker.getLatLng(), {animate: true})
d.data.marker.openPopup();
});
var links = linksGroup.selectAll('.timeline__links')
.data(nodes)
.enter()
.append('path')
.classed('timeline__link', true)
.attr('d', function(d){ return renderer.generatePath(d); });
linksGroup.attr('transform', 'translate(15, 0)');
labelsGroup.attr('transform', 'translate(20, 0)');
}
var force = new labella.Force()
.nodes(nodes)
.on('end', function() {
drawLabels(force.nodes());
}).start(100);
<script src="http://cdn.leafletjs.com/leaflet/v0.7.7/leaflet.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js"></script>
<script src="https://cdn.rawgit.com/twitter/labella.js/bdfb26ef4e6f34cf6cd73f7016f8efdc7ffde6e4/dist/labella.min.js"></script>
$col-bg: #0d264e;
$col-line: #27b4e5;
$font-stack: Roboto, Helvetica, Arial, sans-serif;
$break-medium-min: 560px;
html,
body {
height: 100%;
}
body {
font-family: $font-stack;
font-size: 16px;
font-weight: 400;
}
.container {
display: flex;
flex-direction: column;
height: 100%;
@media (min-width: $break-medium-min) {
flex-direction: row;
}
}
.sidebar {
box-sizing: border-box;
width: 100%;
flex-direction: row;
flex-shrink: 1;
flex-grow: 1;
padding: 24px;
color: white;
background: $col-bg;
box-shadow: 0 0 10px rgba(0,0,0,0.5);
z-index: 1;
overflow-y: auto;
@media (min-width: $break-medium-min) {
order: -1;
width: 320px;
flex-grow: 0;
flex-shrink: 0;
}
}
.map {
flex-shrink: 0;
width: 100%;
height: 200px;
background: black; // in case (somehow) the gradient doesn't work but blend mode does
background: radial-gradient(circle farthest-corner at 100% 10%, rgb(86, 172, 203), rgb(195, 190, 255));
@media (min-width: $break-medium-min) {
flex-grow: 1;
flex-shrink: 1;
height: 100%;
}
&__inner {
width: 100%;
height: 100%;
mix-blend-mode: screen;
}
&__marker {
display: block;
width: 10px;
height: 10px;
left: -5px;
border-radius: 50%;
border: 4px solid #029DFF;
background: white;
box-shadow: 0 2px 2px 0px black;
}
.leaflet-container .leaflet-control-attribution {
color: black;
background: white;
}
.leaflet-popup-content-wrapper {
font-family: $font-stack;
font-size: 16px;
color: black;
border-radius: 2px;
box-shadow: 0 2px 8px black;
}
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
background: white;
}
}
h1 {
font-weight: 300;
font-size: 1.8em;
margin-bottom: 1em;
}
.timeline {
width: 100%;
svg {
display: block;
margin: 0 auto;
}
&__line {
.domain {
stroke: url('#lineGradient');
stroke-width: 4px;
fill: none;
shape-rendering: crispEdges;
}
text {
text-transform: uppercase;
font-size: 0.8em;
font-weight: 300;
fill: white;
opacity: 0.6;
}
}
&__point {
fill: $col-bg;
stroke-width: 4px;
}
&__labels {
text {
font-size: 1em;
fill: white;
cursor: pointer;
}
}
&__link {
fill: none;
stroke: white;
stroke-width: 1px;
stroke-dasharray: 2,5;
}
}
<link href="https://fonts.googleapis.com/css?family=Roboto:400,300,700,400italic" rel="stylesheet" />
<link href="http://cdn.leafletjs.com/leaflet/v0.7.7/leaflet.css" rel="stylesheet" />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment