Timeline slider

https://github.com/albertkun/leaflet_hex_timeslider

This is a hex bin implementation of leaflet + d3 + nouislider, using vanilla JavaScript with ECM 2016 web standards, so Internet Explorer is not supported. You can use babel or other similar tools to make it IE compatible though.

Example

See the example here.

Technology used

Leaflet + d3 plugin from: https://github.com/Asymmetrik/leaflet-d3

Timeslider from: https://refreshless.com/nouislider/

Description

Takes in spatial-temporal point data from a geojson and converts it to a hex representation with start and end dates being scrollable.

Installation

Include the following in the header of the HTML file:

		<!-- load css styles -->
		<link rel="stylesheet" href="./static/leaflet.css" />
		<link href="./static/nouislider.css" rel="stylesheet">
		<link href="./static/styles.css" rel="stylesheet">

		<!-- load libraries -->
		<script src="./static/d3.js" charset="utf-8"></script>
		<script src="./static/d3-hexbin.js" charset="utf-8"></script>
		<script src="./static/leaflet-src.js"></script>
		<script src="./static/nouislider.js"></script>
		<script src="./static/wNumb.min.js"></script>
		<script src="./static/leaflet-d3.js" charset="utf-8"></script>

		<!-- load data -->
		<script src="./SAMPLE_DATA.js" charset="utf-8"></script>

Running example Code:

Clone the repository: git clone https://github.com/albertkun/leaflet_hex_timeslider.git

Point data

geojson is used for the data, and it should be formatted with a features.geometry.coordinates[] array, as per GeoJSON standards: https://en.wikipedia.org/wiki/GeoJSON

However, in the example the geojson is brought in as a variable as follows:

json = features.properties.Date

geoJSON time data

Time data should be stored under the properties object, as per GeoJSON standards above. The field used is Date (case sentitive), with date format of 1/1/2020 or any other ISO standard.

CODE

index.html

<!DOCTYPE html>
<html>
	<head>
		<title>Unhoused Deaths in Los Angeles 2020</title>

		<!-- load css styles -->
		<link rel="stylesheet" href="./static/leaflet.css" />
		<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootswatch/4.5.2/solar/bootstrap.min.css" />
		<link href="./static/nouislider.css" rel="stylesheet">
		<link href="./static/styles.css" rel="stylesheet">

		<!-- load libraries -->
		<script src="./static/d3.js" charset="utf-8"></script>
		<script src="./static/d3-hexbin.js" charset="utf-8"></script>
		<script src="./static/leaflet-src.js"></script>
		<script src="./static/nouislider.js"></script>
		<script src="./static/wNumb.min.js"></script>
		<script src="./static/leaflet-d3.js" charset="utf-8"></script>

		<!-- load data -->
		<script src="./SAMPLE_DATA.js" charset="utf-8"></script>

		<!-- this data is not used -->
		<!-- <script src="../UNHOUSED_DEATHS_LA.js" charset="utf-8"></script> -->

	</head>
	<body>

		<div id="header">
			<div>
			<!-- empty spacer div, can be used for home page icons -->
			</div>
			<div id="title">
				<h2>
				Houseless Deaths in Los Angeles 2020 - DEMO
				</h2>
			</div>
			<div>
			<!-- empty spacer div, can be used for social media icons -->
			</div>
		</div>
		<div class="wrapper">
			<!-- The map element -->
			<div id="map" style="border: 1px solid #ccc"></div>
			<div id="map-slider"></div>
				<div id="hovered">
					<h2>
						<h4>
							<div>
								<span id="event-start"></span> - <span id="event-end">Dec, 2020</span>
							</div>
						</h4>
						<h3 id="total">Total Houseless Deaths</h3><h2 id="event-total"></h2>
						<h3>
							<label> </label>
						</h3>
						<h3>
							<span class="count-label"></span>
							<span class="count"></span>
						</h3>
					</h2>
				</div>
				<!-- unused div for clicked on hex, will add more function later -->
				<!-- <div id="clicked">
					<h4>
						<label>Clicked: </label>
						<span class="count"></span>
					</h4>
				</div> -->
			</div>
		</div>
		<div id="footer"></div>
	</body>
	<script src="./static/time_arrays.js" charset="utf-8"></script>
	<script src="./static/init.js" charset="utf-8"></script>
	<script src="./static/sliderImplmentation.js" charset="utf-8"></script>
</html>



sample_data.js

const json = {"type":"FeatureCollection","features":[{"type":"Feature","properties":{"Date":"1/1/2020"},"geometry":{"type":"Point","coordinates":[-118.39279174804688,34.06517433677496]}},{"type":"Feature","properties":{"Date":"6/1/2020"},"geometry":{"type":"Point","coordinates":[-118.33099365234375,34.064036693555465]}},{"type":"Feature","properties":{"Date":"1/1/2020"},"geometry":{"type":"Point","coordinates":[-118.25340270996094,34.00884266850514]}},{"type":"Feature","properties":{"Date":"1/1/2020"},"geometry":{"type":"Point","coordinates":[-118.33305358886719,33.96215580011896]}},{"type":"Feature","properties":{"Date":"2/2/2020"},"geometry":{"type":"Point","coordinates":[-118.3282470703125,33.988349152677955]}},{"type":"Feature","properties":{"Date":"3/3/2020"},"geometry":{"type":"Point","coordinates":[-118.25408935546875,34.0839432446153]}},{"type":"Feature","properties":{"Date":"2/2/2020"},"geometry":{"type":"Point","coordinates":[-118.28086853027344,34.13908837343849]}},{"type":"Feature","properties":{"Date":"2/2/2020"},"geometry":{"type":"Point","coordinates":[-118.3941650390625,34.186245860011574]}},{"type":"Feature","properties":{"Date":"2/2/2020"},"geometry":{"type":"Point","coordinates":[-118.12774658203125,33.994611584814606]}},{"type":"Feature","properties":{"Date":"7/2/2020"},"geometry":{"type":"Point","coordinates":[-118.14971923828124,34.008273470938335]}},{"type":"Feature","properties":{"Date":"3/3/2020"},"geometry":{"type":"Point","coordinates":[-118.22937011718749,34.04014265821754]}},{"type":"Feature","properties":{"Date":"3/3/2020"},"geometry":{"type":"Point","coordinates":[-118.29940795898438,34.03103839734782]}},{"type":"Feature","properties":{"Date":"7/2/2020"},"geometry":{"type":"Point","coordinates":[-118.30764770507811,34.07427493266743]}},{"type":"Feature","properties":{"Date":"9/3/2020"},"geometry":{"type":"Point","coordinates":[-118.31039428710939,34.0606236722589]}},{"type":"Feature","properties":{"Date":"3/3/2020"},"geometry":{"type":"Point","coordinates":[-118.4271240234375,34.03786668460356]}},{"type":"Feature","properties":{"Date":"9/3/2020"},"geometry":{"type":"Point","coordinates":[-118.20190429687501,33.865854454071865]}},{"type":"Feature","properties":{"Date":"9/3/2020"},"geometry":{"type":"Point","coordinates":[-118.23760986328125,33.946777683283706]}}]}


sliderimplementation.js

// constants for the slider
const slider = document.getElementById('map-slider');
const default_start_date = 'Jan 1,2020'
const default_end_date = 'Dec 1,2020'

// TO-DO: need to change the months to use the start and end dates...
monthVals = months_short.map(month=>Date.parse(month+" 1, 2020"))

// implement the noUiSlider
noUiSlider.create(slider, {
    // step: ,
    behaviour: 'tap-drag',
    connect: true,
    range: {
        min: timestamp(default_start_date),
        max: timestamp(default_end_date)
    },
    direction: 'ltr',
    step: 24 * 60 * 60 * 1000,
    start: [timestamp(default_start_date), timestamp(default_end_date)],
    format: wNumb({
    decimals: 0
    }),
    pips:{
        mode:'values',
        values:monthVals,
        format: {
            to: function(month){
                // custom function to format the months.
                let target_month = new Date(month)
                if (window.innerWidth > 740){
                    month_label = months_short[target_month.getMonth()]
                    console.log(target_month.getMonth())
                    console.log(month_label)
                    return month_label
                }
                else {
                    return []
                }
            },
            from: function(value){
                    return value
            }
        }
    }
});


// Create a string representation of the date.
function formatDate(date) {
    return months_short[date.getMonth()] + ", " +
        date.getFullYear();
}

let dateValues = [
    document.getElementById('event-start'),
    document.getElementById('event-end'),
    document.getElementById('event-total')
];


// add slider
let Slider = L.Control.extend({
    options: {
      position: 'topleft',
    },
    onAdd: function (map) {
      let controlSlider = L.DomUtil.create('div', 'map-slider', L.DomUtil.get('map'));
      // here we can fill the slider with colors, strings and whatever
      controlSlider.innerHTML = '<form><input id="command" type="checkbox"/>command</form>'; 
      return controlSlider;
    },
  });
  map.addControl(new Slider());


slider.noUiSlider.on('update', function (values, handle) {
    dateValues[handle].innerHTML = formatDate(new Date(+values[handle]));
    let new_times = slider.noUiSlider.get();
    // addDataToHexMap(geo_features,new_times[0],new_times[1])
    console.log(dateValues)
    dateValues[2].innerHTML = addDataToHexMap(geo_features,new_times[0],new_times[1])
    // console.log(values[handle])
});


init.js

// options for the map
const map_options = {
    // center on LA
    center: [33.79487002, -118.23543],
    ZoomLevel: 7
}

// options for the Hexbin
const hex_options = {
    radius: 24,
    opacity: .7,
    colorRange: [ '#ffc961', '#1a110c' ],
    radiusRange: [ 4, 24 ]
};

// flatten the original geojson into a single array
const geo_features = json['features'].flat()
//console.log(geo_features)


////////// setup the leaflet map //////////
// use ArcGIS gray base map
let layer = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer/tile/{z}/{y}/{x}', {
    opacity: 1,
    attribution: 'Tiles &copy; Esri &mdash; Esri, DeLorme, NAVTEQ',
    minZoom: 9,
    maxZoom: 14,
    minNativeZoom: 0,
    maxNativeZoom: 19
});

// init the map
let map = L.map('map', {
    layers: [ layer ],
    center: L.latLng(map_options.center[0],map_options.center[1]), zoom: map_options.ZoomLevel
});

// use the stamen labels
let Stamen_TonerLabels = L.tileLayer('https://stamen-tiles-{s}.a.ssl.fastly.net/toner-labels/{z}/{x}/{y}{r}.{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="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
    subdomains: 'abcd',
    opacity: .2,
    minZoom: 0,
    maxZoom: 11,
    ext: 'png'
  });        
  map.addLayer(Stamen_TonerLabels);

// add logo
L.Control.logo = L.Control.extend({
    onAdd: function(map) {
        let img = L.DomUtil.create('img');
        img.src = './imgs/garcettiville.png';
        img.style.width = '200px';
        return img;
    },

    onRemove: function(map) {
        // Nothing to do here
    }
});

L.control.logo = function(opts) {
    return new L.Control.logo(opts);
}
L.control.logo({ position: 'bottomright' }).addTo(map);



////////// add the hex bin here //////////
// Create the hexlayer
let hexLayer = L.hexbinLayer(hex_options);

// Set up hover handler ALBERT: disabled for now
//hexLayer.hoverHandler(L.HexbinHoverHandler.tooltip());


// Set up events, note that "on click" is not used.
hexLayer.dispatch()
    .on('mouseover', function(d, i) {

        //console.log({ type: 'mouseover', event: d, index: i, context: this });
        setHovered(d);
    })
    .on('mouseout', function(d, i) {
        //console.log({ type: 'mouseout', event: d, index: i, context: this });
        setHovered();
    })
    .on('click', function(d, i) {
        //console.log({ type: 'click', event: d, index: i, context: this });
        setClicked(d);
    });




////////// begin the helper functions //////////
function timestamp(str) {
    return new Date(str).getTime();
}

function addDataToHexMap(obj,start_date,end_date){
    let map_data
    if (start_date !== undefined){
        let filtered_obj = obj.filter(data => timestamp(data.properties['Date']) >= start_date && timestamp(data.properties['Date']) <= end_date)
        console.log('date provided')
        // console.log(end_date)
        map_data = filtered_obj
        
    }
    else{
        console.log('date not provided, so adding all data')
        map_data = obj
    }
    // get only the lat/long
    let geo_points = map_data.map(feature => ([feature.geometry.coordinates[0],feature.geometry.coordinates[1]]));
    //console.log(geo_points)
    hexLayer.data(geo_points)
    let total = geo_points.length
    console.log('total')
    //console.log(total)
    return total
}

function setHovered(d) {
    d3.select('#hovered .count').text((null != d) ? "Deaths here: "+ d.length : '');
}

function setClicked(d) {
    d3.select('#clicked .count').text((null != d) ? d.length : '');
}

// Add it to the map now that it's all set up
hexLayer.addTo(map);

//initial call for all the data
addDataToHexMap(geo_features)



leaflet-d3.js

/*! @asymmetrik/leaflet-d3 - 4.4.0 - Copyright (c) 2007-2019 Asymmetrik Ltd, a Maryland Corporation + */
(function (global, factory) {
	typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('d3'), require('d3-hexbin'), require('leaflet')) :
	typeof define === 'function' && define.amd ? define(['d3', 'd3-hexbin', 'leaflet'], factory) :
	(global = global || self, factory(global.d3, global.d3.hexbin));
}(this, function (d3, d3Hexbin) { 'use strict';

	/**
	 * This is a convoluted way of getting ahold of the hexbin function.
	 * - When imported globally, d3 is exposed in the global namespace as 'd3'
	 * - When imported using a module system, it's a named import (and can't collide with d3)
	 * - When someone isn't importing d3-hexbin, the named import will be undefined
	 *
	 * As a result, we have to figure out how it's being imported and get the function reference
	 * (which is why we have this convoluted nested ternary statement
	 */
	var d3_hexbin = (null != d3.hexbin)? d3.hexbin : (null != d3Hexbin)? d3Hexbin.hexbin : null;

	/**
	 * L is defined by the Leaflet library, see git://github.com/Leaflet/Leaflet.git for documentation
	 * We extend L.SVG to take advantage of built-in zoom animations.
	 */
	L.HexbinLayer = L.SVG.extend({
		includes: L.Evented || L.Mixin.Events,

		/**
		 * Default options
		 */
		options : {
			radius : 12,
			opacity: 0.6,
			duration: 200,

			colorScaleExtent: [ 1, undefined ],
			radiusScaleExtent: [ 1, undefined ],
			colorDomain: null,
			radiusDomain: null,
			colorRange: [ '#f7fbff', '#08306b' ],
			radiusRange: [ 4, 12 ],

			pointerEvents: 'all'
		},


		/**
		 * Standard Leaflet initialize function, accepting an options argument provided by the
		 * user when they create the layer
		 * @param options Options object where the options override the defaults
		 */
		initialize : function(options) {
			L.setOptions(this, options);

			// Set up the various overrideable functions
			this._fn = {
				lng: function(d) { return d[0]; },
				lat: function(d) { return d[1]; },
				colorValue: function(d) { return d.length; },
				radiusValue: function(d) { return Number.MAX_VALUE; },

				fill: function(d) {
					var val = this._fn.colorValue(d);
					return (null != val) ? this._scale.color(val) : 'none';
				}
			};

			// Set up the customizable scale
			this._scale = {
				color: d3.scaleLinear(),
				radius: d3.scaleLinear()
			};

			// Set up the Dispatcher for managing events and callbacks
			this._dispatch = d3.dispatch('mouseover', 'mouseout', 'click');

			// Set up the default hover handler
			this._hoverHandler = L.HexbinHoverHandler.none();

			// Create the hex layout
			this._hexLayout = d3_hexbin()
				.radius(this.options.radius)
				.x(function(d) { return d.point[0]; })
				.y(function(d) { return d.point[1]; });

			// Initialize the data array to be empty
			this._data = [];

			this._scale.color
				.range(this.options.colorRange)
				.clamp(true);

			this._scale.radius
				.range(this.options.radiusRange)
				.clamp(true);

		},

		/**
		 * Callback made by Leaflet when the layer is added to the map
		 * @param map Reference to the map to which this layer has been added
		 */
		onAdd : function(map) {

			L.SVG.prototype.onAdd.call(this);

			// Store a reference to the map for later use
			this._map = map;

			// Redraw on moveend
			map.on({ 'moveend': this.redraw }, this);

			// Initial draw
			this.redraw();

		},

		/**
		 * Callback made by Leaflet when the layer is removed from the map
		 * @param map Reference to the map from which this layer is being removed
		 */
		onRemove : function(map) {

			L.SVG.prototype.onRemove.call(this);

			// Destroy the svg container
			this._destroyContainer();

			// Remove events
			map.off({ 'moveend': this.redraw }, this);

			this._map = null;

			// Explicitly will leave the data array alone in case the layer will be shown again
			//this._data = [];

		},

		/**
		 * Create the SVG container for the hexbins
		 * @private
		 */
		_initContainer : function() {

			L.SVG.prototype._initContainer.call(this);
			this._d3Container = d3.select(this._container).select('g');
		},

		/**
		 * Clean up the svg container
		 * @private
		 */
		_destroyContainer: function() {

			// Don't do anything

		},

		/**
		 * (Re)draws the hexbins data on the container
		 * @private
		 */
		redraw : function() {
			var that = this;

			if (!that._map) {
				return;
			}

			// Generate the mapped version of the data
			var data = that._data.map(function(d) {
				var lng = that._fn.lng(d);
				var lat = that._fn.lat(d);

				var point = that._project([ lng, lat ]);
				return { o: d, point: point };
			});

			// Select the hex group for the current zoom level. This has
			// the effect of recreating the group if the zoom level has changed
			var join = this._d3Container.selectAll('g.hexbin')
				.data([ this._map.getZoom() ], function(d) { return d; });

			// enter
			var enter = join.enter().append('g')
				.attr('class', function(d) { return 'hexbin zoom-' + d; });

			// enter + update
			var enterUpdate = enter.merge(join);

			// exit
			join.exit().remove();

			// add the hexagons to the select
			this._createHexagons(enterUpdate, data);

		},

		_createHexagons : function(g, data) {
			var that = this;

			// Create the bins using the hexbin layout

			// Generate the map bounds (to be used to filter the hexes to what is visible)
			var bounds = that._map.getBounds();
			var size = that._map.getSize();
			bounds = bounds.pad(that.options.radius * 2 / Math.max(size.x, size.y));

			var bins = that._hexLayout(data);

			// Derive the extents of the data values for each dimension
			var colorExtent = that._getExtent(bins, that._fn.colorValue, that.options.colorScaleExtent);
			var radiusExtent = that._getExtent(bins, that._fn.radiusValue, that.options.radiusScaleExtent);

			// Match the domain cardinality to that of the color range, to allow for a polylinear scale
			var colorDomain = this.options.colorDomain;
			if (null == colorDomain) {
				colorDomain = that._linearlySpace(colorExtent[0], colorExtent[1], that._scale.color.range().length);
			}
			var radiusDomain = this.options.radiusDomain || radiusExtent;

			// Set the scale domains
			that._scale.color.domain(colorDomain);
			that._scale.radius.domain(radiusDomain);


			/*
			 * Join
			 *    Join the Hexagons to the data
			 *    Use a deterministic id for tracking bins based on position
			 */
			bins = bins.filter(function(d) {
				return bounds.contains(that._map.layerPointToLatLng(L.point(d.x, d.y)));
			});
			var join = g.selectAll('g.hexbin-container')
				.data(bins, function(d) {
					return d.x + ':' + d.y;
				});


			/*
			 * Update
			 *    Set the fill and opacity on a transition
			 *    opacity is re-applied in case the enter transition was cancelled
			 *    the path is applied as well to resize the bins
			 */
			join.select('path.hexbin-hexagon').transition().duration(that.options.duration)
				.attr('fill', that._fn.fill.bind(that))
				.attr('fill-opacity', that.options.opacity)
				.attr('stroke-opacity', that.options.opacity)
				.attr('d', function(d) {
					return that._hexLayout.hexagon(that._scale.radius(that._fn.radiusValue.call(that, d)));
				});


			/*
			 * Enter
			 *    Establish the path, size, fill, and the initial opacity
			 *    Transition to the final opacity and size
			 */
			var enter = join.enter().append('g').attr('class', 'hexbin-container');

			enter.append('path').attr('class', 'hexbin-hexagon')
				.attr('transform', function(d) {
					return 'translate(' + d.x + ',' + d.y + ')';
				})
				.attr('d', function(d) {
					return that._hexLayout.hexagon(that._scale.radius.range()[0]);
				})
				.attr('fill', that._fn.fill.bind(that))
				.attr('fill-opacity', 0.01)
				.attr('stroke-opacity', 0.01)
				.transition().duration(that.options.duration)
					.attr('fill-opacity', that.options.opacity)
					.attr('stroke-opacity', that.options.opacity)
					.attr('d', function(d) {
						return that._hexLayout.hexagon(that._scale.radius(that._fn.radiusValue.call(that, d)));
					});

			// Grid
			var gridEnter = enter.append('path').attr('class', 'hexbin-grid')
				.attr('transform', function(d) {
					return 'translate(' + d.x + ',' + d.y + ')';
				})
				.attr('d', function(d) {
					return that._hexLayout.hexagon(that.options.radius);
				})
				.attr('fill', 'none')
				.attr('stroke', 'none')
				.style('pointer-events', that.options.pointerEvents);

			// Grid enter-update
			gridEnter.merge(join.select('path.hexbin-grid'))
				.on('mouseover', function(d, i) {
					that._hoverHandler.mouseover.call(this, that, d, i);
					that._dispatch.call('mouseover', this, d, i);
				})
				.on('mouseout', function(d, i) {
					that._dispatch.call('mouseout', this, d, i);
					that._hoverHandler.mouseout.call(this, that, d, i);
				})
				.on('click', function(d, i) {
					that._dispatch.call('click', this, d, i);
				});


			// Exit
			var exit = join.exit();

			exit.select('path.hexbin-hexagon')
				.transition().duration(that.options.duration)
				.attr('fill-opacity', 0)
				.attr('stroke-opacity', 0)
				.attr('d', function(d) {
					return that._hexLayout.hexagon(0);
				});

			exit.transition().duration(that.options.duration)
				.remove();

		},

		_getExtent: function(bins, valueFn, scaleExtent) {

			// Determine the extent of the values
			var extent = d3.extent(bins, valueFn.bind(this));

			// If either's null, initialize them to 0
			if (null == extent[0]) extent[0] = 0;
			if (null == extent[1]) extent[1] = 0;

			// Now apply the optional clipping of the extent
			if (null != scaleExtent[0]) extent[0] = scaleExtent[0];
			if (null != scaleExtent[1]) extent[1] = scaleExtent[1];

			return extent;

		},

		_project : function(coord) {
			var point = this._map.latLngToLayerPoint([ coord[1], coord[0] ]);
			return [ point.x, point.y ];
		},

		_getBounds: function(data) {
			if(null == data || data.length < 1) {
				return { min: [ 0, 0 ], max: [ 0, 0 ]};
			}

			// bounds is [[min long, min lat], [max long, max lat]]
			var bounds = [ [ 999, 999 ], [ -999, -999 ] ];

			data.forEach(function(element) {
				var x = element.point[0];
				var y = element.point[1];

				bounds[0][0] = Math.min(bounds[0][0], x);
				bounds[0][1] = Math.min(bounds[0][1], y);
				bounds[1][0] = Math.max(bounds[1][0], x);
				bounds[1][1] = Math.max(bounds[1][1], y);
			});

			return { min: bounds[0], max: bounds[1] };
		},

		_linearlySpace: function(from, to, length) {
			var arr = new Array(length);
			var step = (to - from) / Math.max(length - 1, 1);

			for (var i = 0; i < length; ++i) {
				arr[i] = from + (i * step);
			}

			return arr;
		},


		// ------------------------------------
		// Public API
		// ------------------------------------

		radius: function(v) {
			if (!arguments.length) { return this.options.radius; }

			this.options.radius = v;
			this._hexLayout.radius(v);

			return this;
		},

		opacity: function(v) {
			if (!arguments.length) { return this.options.opacity; }
			this.options.opacity = v;

			return this;
		},

		duration: function(v) {
			if (!arguments.length) { return this.options.duration; }
			this.options.duration = v;

			return this;
		},

		colorScaleExtent: function(v) {
			if (!arguments.length) { return this.options.colorScaleExtent; }
			this.options.colorScaleExtent = v;

			return this;
		},

		radiusScaleExtent: function(v) {
			if (!arguments.length) { return this.options.radiusScaleExtent; }
			this.options.radiusScaleExtent = v;

			return this;
		},

		colorRange: function(v) {
			if (!arguments.length) { return this.options.colorRange; }
			this.options.colorRange = v;
			this._scale.color.range(v);

			return this;
		},

		radiusRange: function(v) {
			if (!arguments.length) { return this.options.radiusRange; }
			this.options.radiusRange = v;
			this._scale.radius.range(v);

			return this;
		},

		colorScale: function(v) {
			if (!arguments.length) { return this._scale.color; }
			this._scale.color = v;

			return this;
		},

		radiusScale: function(v) {
			if (!arguments.length) { return this._scale.radius; }
			this._scale.radius = v;

			return this;
		},

		lng: function(v) {
			if (!arguments.length) { return this._fn.lng; }
			this._fn.lng = v;

			return this;
		},

		lat: function(v) {
			if (!arguments.length) { return this._fn.lat; }
			this._fn.lat = v;

			return this;
		},

		colorValue: function(v) {
			if (!arguments.length) { return this._fn.colorValue; }
			this._fn.colorValue = v;

			return this;
		},

		radiusValue: function(v) {
			if (!arguments.length) { return this._fn.radiusValue; }
			this._fn.radiusValue = v;

			return this;
		},

		fill: function(v) {
			if (!arguments.length) { return this._fn.fill; }
			this._fn.fill = v;

			return this;
		},

		data: function(v) {
			if (!arguments.length) { return this._data; }
			this._data = (null != v) ? v : [];

			this.redraw();

			return this;
		},

		/*
		 * Getter for the event dispatcher
		 */
		dispatch: function() {
			return this._dispatch;
		},

		hoverHandler: function(v) {
			if (!arguments.length) { return this._hoverHandler; }
			this._hoverHandler = (null != v) ? v : L.HexbinHoverHandler.none();

			this.redraw();

			return this;
		},

		/*
		 * Returns an array of the points in the path, or nested arrays of points in case of multi-polyline.
		 */
		getLatLngs: function () {
			var that = this;

			// Map the data into an array of latLngs using the configured lat/lng accessors
			return this._data.map(function(d) {
				return L.latLng(that.options.lat(d), that.options.lng(d));
			});
		},

		/*
		 * Get path geometry as GeoJSON
		 */
		toGeoJSON: function () {
			return L.GeoJSON.getFeature(this, {
				type: 'LineString',
				coordinates: L.GeoJSON.latLngsToCoords(this.getLatLngs(), 0)
			});
		}

	});

	// Hover Handlers modify the hexagon and can be combined
	L.HexbinHoverHandler = {

		tooltip: function(options) {

			// merge options with defaults
			options = options || {};
			if (null == options.tooltipContent) { options.tooltipContent = function(d) { return 'Count: ' + d.length; }; }

			// Generate the tooltip
			var tooltip = d3.select('body').append('div')
				.attr('class', 'hexbin-tooltip')
				.style('z-index', 9999)
				.style('pointer-events', 'none')
				.style('visibility', 'hidden')
				.style('position', 'fixed');

			tooltip.append('div').attr('class', 'tooltip-content');

			// return the handler instance
			return {
				mouseover: function (hexLayer, data) {
					var event = d3.event;
					var gCoords = d3.mouse(this);

					tooltip
						.style('visibility', 'visible')
						.html(options.tooltipContent(data, hexLayer));

					var div = null;
					if (null != tooltip._groups && tooltip._groups.length > 0 && tooltip._groups[0].length > 0) {
						div = tooltip._groups[0][0];
					}
					var h = div.clientHeight, w = div.clientWidth;

					tooltip
						.style('top', '' + event.clientY - gCoords[1] - h - 16 + 'px')
						.style('left', '' + event.clientX - gCoords[0] - w/2 + 'px');

				},
				mouseout: function (hexLayer, data) {
					tooltip
						.style('visibility', 'hidden')
						.html();
				}
			};

		},

		resizeFill: function() {

			// return the handler instance
			return {
				mouseover: function (hexLayer, data) {
					var o = d3.select(this.parentNode);
					o.select('path.hexbin-hexagon')
						.attr('d', function (d) {
							return hexLayer._hexLayout.hexagon(hexLayer.options.radius);
						});
				},
				mouseout: function (hexLayer, data) {
					var o = d3.select(this.parentNode);
					o.select('path.hexbin-hexagon')
						.attr('d', function (d) {
							return hexLayer._hexLayout.hexagon(hexLayer._scale.radius(hexLayer._fn.radiusValue.call(hexLayer, d)));
						});
				}
			};

		},

		resizeScale: function(options) {

			// merge options with defaults
			options = options || {};
			if (null == options.radiusScale) options.radiusScale = 0.5;

			// return the handler instance
			return {
				mouseover: function (hexLayer, data) {
					var o = d3.select(this.parentNode);
					o.select('path.hexbin-hexagon')
						.attr('d', function (d) {
							return hexLayer._hexLayout.hexagon(hexLayer._scale.radius.range()[1] * (1 + options.radiusScale));
						});
				},
				mouseout: function (hexLayer, data) {
					var o = d3.select(this.parentNode);
					o.select('path.hexbin-hexagon')
						.attr('d', function (d) {
							return hexLayer._hexLayout.hexagon(hexLayer._scale.radius(hexLayer._fn.radiusValue.call(hexLayer, d)));
						});
				}
			};

		},

		compound: function(options) {

			options = options || {};
			if (null == options.handlers) options.handlers = [ L.HexbinHoverHandler.none() ];

			return {
				mouseover: function (hexLayer, data) {
					var that = this;
					options.handlers.forEach(function(h) { h.mouseover.call(that, hexLayer, data); });
				},
				mouseout: function (hexLayer, data) {
					var that = this;
					options.handlers.forEach(function(h) { h.mouseout.call(that, hexLayer, data); });
				}
			};

		},

		none: function() {
			return {
				mouseover: function () {},
				mouseout: function () {}
			};
		}
	};

	L.hexbinLayer = function(options) {
		return new L.HexbinLayer(options);
	};

	/**
	 * L is defined by the Leaflet library, see git://github.com/Leaflet/Leaflet.git for documentation
	 * We extend L.SVG to take advantage of built-in zoom animations.
	 */
	L.PingLayer = L.SVG.extend({
		includes: L.Evented || L.Mixin.Events,

		/*
		 * Default options
		 */
		options : {
			duration: 800,
			fps: 32,
			opacityRange: [ 1, 0 ],
			radiusRange: [ 3, 15 ]
		},


		// Initialization of the plugin
		initialize : function(options) {
			L.setOptions(this, options);

			this._fn = {
				lng: function(d) { return d[0]; },
				lat: function(d) { return d[1]; },
				radiusScaleFactor: function(d) { return 1; }
			};

			this._scale = {
				radius: d3.scalePow().exponent(0.35),
				opacity: d3.scaleLinear()
			};

			this._lastUpdate = Date.now();
			this._fps = 0;

			this._scale.radius
				.domain([ 0, this.options.duration ])
				.range(this.options.radiusRange)
				.clamp(true);
			this._scale.opacity
				.domain([ 0, this.options.duration ])
				.range(this.options.opacityRange)
				.clamp(true);
		},

		// Called when the plugin layer is added to the map
		onAdd : function(map) {

			L.SVG.prototype.onAdd.call(this);

			// Store a reference to the map for later use
			this._map = map;

			// Init the state of the simulation
			this._running = false;

			// Set up events
			map.on({'move': this._updateContainer}, this);

		},

		// Called when the plugin layer is removed from the map
		onRemove : function(map) {

			L.SVG.prototype.onRemove.call(this);

			// Destroy the svg container
			this._destroyContainer();

			// Remove events
			map.off({'move': this._updateContainer}, this);

			this._map = null;
			this._data = null;

		},


		/*
		 * Private Methods
		 */

		// Initialize the Container - creates the svg pane
		_initContainer : function() {

			L.SVG.prototype._initContainer.call(this);
			this._d3Container = d3.select(this._container).select('g');

		},

		// Update the container - Updates the dimensions of the svg pane
		_updateContainer : function() {

			this._updatePings(true);

		},

		// Cleanup the svg pane
		_destroyContainer: function() {

			// Don't do anything

		},


		// Calculate the circle coordinates for the provided data
		_getCircleCoords: function(geo) {
			var point = this._map.latLngToLayerPoint(geo);
			return { x: point.x, y: point.y };
		},


		// Add a ping to the map
		_addPing : function(data, cssClass) {
			// Lazy init the data array
			if (null == this._data) this._data = [];

			// Derive the spatial data
			var geo = [ this._fn.lat(data), this._fn.lng(data) ];
			var coords = this._getCircleCoords(geo);

			// Add the data to the list of pings
			var circle = {
				data: data,
				geo: geo,
				ts: Date.now(),
				nts: 0
			};
			circle.c = this._d3Container.append('circle')
				.attr('class', (null != cssClass)? 'ping ' + cssClass : 'ping')
				.attr('cx', coords.x)
				.attr('cy', coords.y)
				.attr('r', this._fn.radiusScaleFactor.call(this, data) * this._scale.radius.range()[0]);

			// Push new circles
			this._data.push(circle);
		},

		// Main update loop
		_updatePings : function(immediate) {
			var nowTs = Date.now();
			if (null == this._data) this._data = [];

			var maxIndex = -1;

			// Update everything
			for (var i=0; i < this._data.length; i++) {

				var d = this._data[i];
				var age = nowTs - d.ts;

				if (this.options.duration < age) {

					// If the blip is beyond it's life, remove it from the dom and track the lowest index to remove
					d.c.remove();
					maxIndex = i;

				}
				else {

					// If the blip is still alive, process it
					if (immediate || d.nts < nowTs) {

						var coords = this._getCircleCoords(d.geo);

						d.c.attr('cx', coords.x)
						   .attr('cy', coords.y)
						   .attr('r', this._fn.radiusScaleFactor.call(this, d.data) * this._scale.radius(age))
						   .attr('fill-opacity', this._scale.opacity(age))
						   .attr('stroke-opacity', this._scale.opacity(age));
						d.nts = Math.round(nowTs + 1000/this.options.fps);

					}
				}
			}

			// Delete all the aged off data at once
			if (maxIndex > -1) {
				this._data.splice(0, maxIndex + 1);
			}

			// The return function dictates whether the timer loop will continue
			this._running = (this._data.length > 0);

			if (this._running) {
				this._fps = 1000/(nowTs - this._lastUpdate);
				this._lastUpdate = nowTs;
			}

			return !this._running;
		},

		// Expire old pings
		_expirePings : function() {
			var maxIndex = -1;
			var nowTs = Date.now();

			// Search from the front of the array
			for (var i=0; i < this._data.length; i++) {
				var d = this._data[i];
				var age = nowTs - d.ts;

				if(this.options.duration < age) {
					// If the blip is beyond it's life, remove it from the dom and track the lowest index to remove
					d.c.remove();
					maxIndex = i;
				}
				else {
					break;
				}
			}

			// Delete all the aged off data at once
			if (maxIndex > -1) {
				this._data.splice(0, maxIndex + 1);
			}
		},

		/*
		 * Public Methods
		 */

		duration: function(v) {
			if (!arguments.length) { return this.options.duration; }
			this.options.duration = v;

			return this;
		},

		fps: function(v) {
			if (!arguments.length) { return this.options.fps; }
			this.options.fps = v;

			return this;
		},

		lng: function(v) {
			if (!arguments.length) { return this._fn.lng; }
			this._fn.lng = v;

			return this;
		},

		lat: function(v) {
			if (!arguments.length) { return this._fn.lat; }
			this._fn.lat = v;

			return this;
		},

		radiusRange: function(v) {
			if (!arguments.length) { return this.options.radiusRange; }
			this.options.radiusRange = v;
			this._scale.radius().range(v);

			return this;
		},

		opacityRange: function(v) {
			if (!arguments.length) { return this.options.opacityRange; }
			this.options.opacityRange = v;
			this._scale.opacity().range(v);

			return this;
		},

		radiusScale: function(v) {
			if (!arguments.length) { return this._scale.radius; }
			this._scale.radius = v;

			return this;
		},

		opacityScale: function(v) {
			if (!arguments.length) { return this._scale.opacity; }
			this._scale.opacity = v;

			return this;
		},

		radiusScaleFactor: function(v) {
			if (!arguments.length) { return this._fn.radiusScaleFactor; }
			this._fn.radiusScaleFactor = v;

			return this;
		},

		/*
		 * Method by which to "add" pings
		 */
		ping : function(data, cssClass) {
			this._addPing(data, cssClass);
			this._expirePings();

			// Start timer if not active
			if (!this._running && this._data.length > 0) {
				this._running = true;
				this._lastUpdate = Date.now();

				var that = this;
				d3.timer(function() { that._updatePings.call(that, false); });
			}

			return this;
		},

		getActualFps : function() {
			return this._fps;
		},

		data : function() {
			return this._data;
		},

	});

	L.pingLayer = function(options) {
		return new L.PingLayer(options);
	};

}));
//# sourceMappingURL=leaflet-d3.js.map


time_arrays.js

const weekdays = [
    "Sunday", "Monday", "Tuesday",
    "Wednesday", "Thursday", "Friday",
    "Saturday"
];

const months_short = [
    "Jan", "Feb", "Mar","Apr", "May", "Jun", "Jul","Aug", "Sep", "Oct","Nov", "Dec"
];

// const months = [
//     "January", "February", "March",
//     "April", "May", "June", "July",
//     "August", "September", "October",
//     "November", "December"
// ];

// correct months here
// const months_short = [
//     "Jan", "Feb", "Mar",
//     "Apr", "May", "Jun", "Jul",
//     "Aug", "Sep", "Oct",
//     "Nov", "Dec"
// ];


wNumb.min.js

!function(e){"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?module.exports=e():window.wNumb=e()}(function(){"use strict";var o=["decimals","thousand","mark","prefix","suffix","encoder","decoder","negativeBefore","negative","edit","undo"];function w(e){return e.split("").reverse().join("")}function h(e,t){return e.substring(0,t.length)===t}function f(e,t,n){if((e[t]||e[n])&&e[t]===e[n])throw new Error(t)}function x(e){return"number"==typeof e&&isFinite(e)}function n(e,t,n,r,i,o,f,u,s,c,a,p){var d,l,h,g=p,v="",m="";return o&&(p=o(p)),!!x(p)&&(!1!==e&&0===parseFloat(p.toFixed(e))&&(p=0),p<0&&(d=!0,p=Math.abs(p)),!1!==e&&(p=function(e,t){return e=e.toString().split("e"),(+((e=(e=Math.round(+(e[0]+"e"+(e[1]?+e[1]+t:t)))).toString().split("e"))[0]+"e"+(e[1]?e[1]-t:-t))).toFixed(t)}(p,e)),-1!==(p=p.toString()).indexOf(".")?(h=(l=p.split("."))[0],n&&(v=n+l[1])):h=p,t&&(h=w((h=w(h).match(/.{1,3}/g)).join(w(t)))),d&&u&&(m+=u),r&&(m+=r),d&&s&&(m+=s),m+=h,m+=v,i&&(m+=i),c&&(m=c(m,g)),m)}function r(e,t,n,r,i,o,f,u,s,c,a,p){var d,l="";return a&&(p=a(p)),!(!p||"string"!=typeof p)&&(u&&h(p,u)&&(p=p.replace(u,""),d=!0),r&&h(p,r)&&(p=p.replace(r,"")),s&&h(p,s)&&(p=p.replace(s,""),d=!0),i&&function(e,t){return e.slice(-1*t.length)===t}(p,i)&&(p=p.slice(0,-1*i.length)),t&&(p=p.split(t).join("")),n&&(p=p.replace(n,".")),d&&(l+="-"),""!==(l=(l+=p).replace(/[^0-9\.\-.]/g,""))&&(l=Number(l),f&&(l=f(l)),!!x(l)&&l))}function i(e,t,n){var r,i=[];for(r=0;r<o.length;r+=1)i.push(e[o[r]]);return i.push(n),t.apply("",i)}return function e(t){if(!(this instanceof e))return new e(t);"object"==typeof t&&(t=function(e){var t,n,r,i={};for(void 0===e.suffix&&(e.suffix=e.postfix),t=0;t<o.length;t+=1)if(void 0===(r=e[n=o[t]]))"negative"!==n||i.negativeBefore?"mark"===n&&"."!==i.thousand?i[n]=".":i[n]=!1:i[n]="-";else if("decimals"===n){if(!(0<=r&&r<8))throw new Error(n);i[n]=r}else if("encoder"===n||"decoder"===n||"edit"===n||"undo"===n){if("function"!=typeof r)throw new Error(n);i[n]=r}else{if("string"!=typeof r)throw new Error(n);i[n]=r}return f(i,"mark","thousand"),f(i,"prefix","negative"),f(i,"prefix","negativeBefore"),i}(t),this.to=function(e){return i(t,n,e)},this.from=function(e){return i(t,r,e)})}});


styles.css

html, body {
    padding: 5px;
 }
 body{
     display: grid;
     grid-template-rows: .1fr .90fr .05fr;
     grid-template-columns: 1fr;
     grid-template-areas: "header" "main" "footer";
 }
 #header{
    grid-area: header;
    display: grid;
    grid-template-columns: .2fr .6fr .2fr;
    justify-content: center;
 }


svg path.hexbin-hexagon {
    stroke: #000;
    stroke-width: 0px;
}
.wrapper {
    display: grid;
    grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
    grid-template-rows: .05fr .85fr .1fr;
    gap: 10px;
    grid-gap: .5em;
    height: 80vh;
    grid-area: "main"
  }
  #footer{
      grid-area: "footer";
  }
  #map {
    grid-column: 1 / 5;
    grid-row: 1 / 4;
  }
  #map-slider { 
    grid-column: 2/4 ;
    grid-row: 2;
    z-index: 1000;
  }
  #hovered {
    grid-column: 5;
    grid-row: 1 / 3;
  }

  .noUi-value{
      font-size: 14px;
      -webkit-text-fill-color: rgb(153, 153, 153);/* Will override color (regardless of order) */
      -webkit-text-stroke-width: .5px;
      -webkit-text-stroke-color: rgb(49, 49, 49);
  }



Posted

in

by

Tags: