from folium.elements import JSCSSMixin from folium.features import GeoJson from folium.map import Layer from folium.template import Template class TimeSliderChoropleth(JSCSSMixin, Layer): """ Create a choropleth with a timeslider for timestamped data. Visualize timestamped data, allowing users to view the choropleth at different timestamps using a slider. Parameters ---------- data: str geojson string styledict: dict A dictionary where the keys are the geojson feature ids and the values are dicts of `{time: style_options_dict}` date_options: str, default "ddd MMM DD YYYY" A format string to render the currently active time in the control. highlight: bool, default False Whether to show a visual effect on mouse hover and click. name : string, default None The name of the Layer, as it will appear in LayerControls. overlay : bool, default False Adds the layer as an optional overlay (True) or the base layer (False). control : bool, default True Whether the Layer will be included in LayerControls. show: bool, default True Whether the layer will be shown on opening. init_timestamp: int, default 0 Initial time-stamp index on the slider. Must be in the range `[-L, L-1]`, where `L` is the maximum number of time stamps in `styledict`. For example, use `-1` to initialize the slider to the latest timestamp. """ _template = Template( """ {% macro script(this, kwargs) %} { let timestamps = {{ this.timestamps|tojson }}; let styledict = {{ this.styledict|tojson }}; let current_timestamp = timestamps[{{ this.init_timestamp }}]; function formatDate(date) { var newdate = new moment(date); return newdate.format({{this.date_format|tojson}}); } let slider_body = d3.select("body").insert("div", "div.folium-map") .attr("id", "slider_{{ this.get_name() }}"); $("#slider_{{ this.get_name() }}").hide(); // insert time slider label slider_body.append("output") .attr("width", "100") .style('font-size', '18px') .style('text-align', 'center') .style('font-weight', '500%') .style('margin', '5px'); // insert time slider slider_body.append("input") .attr("type", "range") .attr("width", "100px") .attr("min", 0) .attr("max", timestamps.length - 1) .attr("value", {{ this.init_timestamp }}) .attr("step", "1") .style('align', 'center'); let datestring = formatDate(parseInt(current_timestamp)*1000); d3.select("#slider_{{ this.get_name() }} > output").text(datestring); let fill_map = function(){ for (var feature_id in styledict){ let style = styledict[feature_id]//[current_timestamp]; var fillColor = 'white'; var opacity = 0; if (current_timestamp in style){ fillColor = style[current_timestamp]['color']; opacity = style[current_timestamp]['opacity']; d3.selectAll('#{{ this.get_name() }}-feature-'+feature_id ).attr('fill', fillColor) .style('fill-opacity', opacity); } } } d3.select("#slider_{{ this.get_name() }} > input").on("input", function() { current_timestamp = timestamps[this.value]; let datestring = formatDate(parseInt(current_timestamp)*1000); d3.select("#slider_{{ this.get_name() }} > output").text(datestring); fill_map(); }); let onEachFeature; {% if this.highlight %} onEachFeature = function(feature, layer) { layer.on({ mouseout: function(e) { if (current_timestamp in styledict[e.target.feature.id]){ var opacity = styledict[e.target.feature.id][current_timestamp]['opacity']; d3.selectAll('#{{ this.get_name() }}-feature-'+e.target.feature.id).style('fill-opacity', opacity); } }, mouseover: function(e) { if (current_timestamp in styledict[e.target.feature.id]){ d3.selectAll('#{{ this.get_name() }}-feature-'+e.target.feature.id).style('fill-opacity', 1); } }, click: function(e) { {{this._parent.get_name()}}.fitBounds(e.target.getBounds()); } }); }; {% endif %} var {{ this.get_name() }} = L.geoJson( {{ this.data|tojson }}, {onEachFeature: onEachFeature} ); {{ this.get_name() }}.setStyle(function(feature) { if (feature.properties.style !== undefined){ return feature.properties.style; } else{ return ""; } }); let onOverlayAdd = function(e) { {{ this.get_name() }}.eachLayer(function (layer) { layer._path.id = '{{ this.get_name() }}-feature-' + layer.feature.id; }); $("#slider_{{ this.get_name() }}").show(); d3.selectAll('path') .attr('stroke', '{{ this.stroke_color }}') .attr('stroke-width', {{ this.stroke_width }}) .attr('stroke-dasharray', '5,5') .attr('stroke-opacity', {{ this.stroke_opacity }}) .attr('fill-opacity', 0); fill_map(); } {{ this.get_name() }}.on('add', onOverlayAdd); {{ this.get_name() }}.on('remove', function() { $("#slider_{{ this.get_name() }}").hide(); }) {%- if this.show %} {{ this.get_name() }}.addTo({{ this._parent.get_name() }}); $("#slider_{{ this.get_name() }}").show(); {%- endif %} } {% endmacro %} """ ) default_js = [ ("d3v4", "https://d3js.org/d3.v4.min.js"), ( "moment", "https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.1/moment.min.js", ), ] def __init__( self, data, styledict, date_options: str = "ddd MMM DD YYYY", highlight: bool = False, name=None, overlay=True, control=True, show=True, init_timestamp=0, stroke_opacity=1, stroke_width=0.8, stroke_color="#FFFFFF", ): super().__init__(name=name, overlay=overlay, control=control, show=show) self.data = GeoJson.process_data(GeoJson({}), data) self.date_format = date_options self.highlight = highlight self.stroke_opacity = stroke_opacity self.stroke_width = stroke_width self.stroke_color = stroke_color if not isinstance(styledict, dict): raise ValueError(f"styledict must be a dictionary, got {styledict!r}") for val in styledict.values(): if not isinstance(val, dict): raise ValueError( f"Each item in styledict must be a dictionary, got {val!r}" ) # Make set of timestamps. timestamps_set = set() for feature in styledict.values(): timestamps_set.update(set(feature.keys())) try: timestamps = sorted(timestamps_set, key=int) except (TypeError, ValueError): timestamps = sorted(timestamps_set) self.timestamps = timestamps self.styledict = styledict assert ( -len(timestamps) <= init_timestamp < len(timestamps) ), f"init_timestamp must be in the range [-{len(timestamps)}, {len(timestamps)}) but got {init_timestamp}" if init_timestamp < 0: init_timestamp = len(timestamps) + init_timestamp self.init_timestamp = init_timestamp