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 © Esri — 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> — Map data © <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); }