Dashboard Link: Bylaw Infractions
For information about the dashboard Click Me!!
Last active
December 17, 2018 22:33
-
-
Save mikelotis/0f6d9d656249b54ebc1fd28728722683 to your computer and use it in GitHub Desktop.
Bylaw Infractions Dashboard
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
border: yes |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
html, body { | |
height: 100%; | |
width: 100%; | |
overflow-x: hidden; | |
} | |
/* width for map and charts */ | |
#map-plot, | |
.svg-container { | |
width: 100%; | |
} | |
/* map height */ | |
#map-plot { | |
padding-bottom: 129.6%; | |
} | |
/* to make map nice for 1920px width Screens and up */ | |
@media (min-width: 1920px) { | |
#map-plot { | |
padding-bottom: 125.3%; | |
} | |
} | |
/* top padding for all charts */ | |
#pie-plot > svg, | |
#bar-chart > svg, | |
#row-chart > svg, | |
#bubble-plot > svg { | |
padding-top: 0.30em; | |
} | |
/* placed chart title as desired */ | |
#pie-plot > div, | |
#bubble-plot > div, | |
#bar-chart > div, | |
#row-chart > div, | |
#board > div > div:nth-child(1) > div > div > div > div > div.chart-title { | |
padding-left: 0; | |
padding-right: 0; | |
padding-bottom: 0.5em; | |
padding-top: 0.5em; | |
} | |
/* neighbourhood info on leaflet map */ | |
.myinfo { | |
padding: 0.5em; | |
font: 1.1em Arial, Helvetica; | |
background: rgba(255,255,255,0.8); | |
border-radius: 0.5em; | |
} | |
/* path style */ | |
svg.leaflet-zoom-animated g path { | |
stroke: rgba(31, 30, 30, 0.612); | |
stroke-width: 0.1em; | |
fill-opacity: 0.45; | |
} | |
/* legend rec style */ | |
.legend i { | |
opacity: 0.45; | |
} | |
/* path style on hover */ | |
svg.leaflet-zoom-animated g path:hover { | |
stroke: rgb(0, 0, 0); | |
fill: brown; | |
stroke-width: 0.1em; | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
document.addEventListener("DOMContentLoaded", function dashboard() { | |
//Set up loading spinner | |
var opts = { | |
lines: 13, | |
length: 22, | |
width: 9, | |
radius: 30, | |
corners: 1, | |
rotate: 2, | |
direction: 1, | |
speed: 1, | |
trail: 60, | |
shadow: false, | |
hwaccel: false, | |
className: 'spinner', | |
zIndex: 2e9, | |
top:'17%', | |
left: '50%' | |
}; | |
var target = document.getElementById("spinner"); | |
var spinner = new Spinner(opts).spin(target); | |
queue() | |
.defer(d3.json, "https://raw.githubusercontent.com/Edmonton-Open-Data/Edmonton-Bylaw-Infractions-II/master/data/Edmonton2.json") //neighbourhoodsGejson | |
.defer(d3.json, "https://raw.githubusercontent.com/Edmonton-Open-Data/Edmonton-Bylaw-Infractions-II/master/data/data.json") //datajson | |
.await(renderCharts); | |
function renderCharts(error, neighbourhoodsGejson, dataJson) { | |
if(error) throw error; | |
//cleaned data | |
var data = dataJson.data; | |
//Crossfilter instance | |
var ndx = crossfilter(data); | |
//Define values to be used by chart(s) | |
var chartHeightScale = 0.58, | |
pieXscale = 1.41, | |
pieRscale = chartHeightScale * 0.5, | |
pieInnerRscale = pieRscale * 0.52, | |
monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], | |
countSum = data.map(function(d) { return d.COUNT; }) | |
.reduce(function(sum, value) { return sum + value; }, 0 ), | |
map, info, mapReset, title, texts, chartTexts, slideMenu; | |
//Colors and color scales | |
//Got the colors from http://colorbrewer2.org | |
var pieColors = ["#66c2a5", "#fc8d62", "#8da0cb", "#e78ac3", "#a6d854"], | |
mapColors = ["#a6cee3","#1f78b4","#b2df8a","#33a02c","#fb9a99","#e31a1c","#fdbf6f","#ff7f00"], | |
bubbleColors = ["#a6cee3", "#1f78b4", "#b2df8a", "#33a02c"], | |
pieScaleColors = d3.scale.quantize().domain([0, pieColors.length - 1]).range(pieColors), | |
bubbleScaleColors = d3.scale.quantize().domain([0, bubbleColors.length - 1]).range(bubbleColors); | |
//Define Dimensions | |
var neighbourhoodsDim = ndx.dimension(function(d) { return d.NEIGHBOURHOOD; }), | |
complaintsDim = ndx.dimension(function(d) { return d.COMPLAINT; }), | |
monthDim = ndx.dimension(function(d) { return monthNames[d["MONTH_NUMBER"] - 1]; }), | |
yearDim = ndx.dimension(function(d) { return d.YEAR; }), | |
initiatorDim = ndx.dimension(function(d) { return [d["INITIATED_BY"], d.STATUS]; }); | |
//Define groups | |
var groupByCount = function(d) { return d.COUNT; }, | |
neighbourhoodsGroup = neighbourhoodsDim.group().reduceSum(groupByCount), | |
complaintsGroup = complaintsDim.group().reduceSum(groupByCount), | |
monthGroup = monthDim.group().reduceSum(groupByCount), | |
yearGroup = yearDim.group().reduceSum(groupByCount), | |
statusGroup = initiatorDim.group().reduceSum(groupByCount), | |
sumofAllInfractions = ndx.groupAll().reduceSum(groupByCount); | |
//Charts, selections, and filterCount(no var to be detected by reset link) | |
//find better solution to make code secure("use strict wont allow this") | |
dcMap = dc.leafletChoroplethChart("#map-plot"), | |
pie = dc.pieChart("#pie-plot"), | |
barChart = dc.barChart("#bar-chart"), | |
rowChart = dc.rowChart("#row-chart"), | |
bubbleChart = dc.bubbleCloud("#bubble-plot"); | |
var recordCounter = dc.dataCount("#records-count"), | |
charts, neighbourSelections; | |
recordCounter.dimension(ndx) | |
.group(ndx.groupAll()) | |
.html({some:'<strong>%filter-count</strong> selected out of <strong>%total-count</strong> records. | '+ | |
'<a href= "javascript:dc.filterAll(); dc.redrawAll();">Reset All</a>', | |
all: 'All records selected. Please click on the chart(s) to apply filters.' | |
}); | |
recordCounter.render(); | |
charts = [dcMap, pie, barChart, rowChart, bubbleChart]; | |
//add filter listerner to update sum and percentage text | |
// for all the charts | |
charts.forEach(statsUpdate); | |
dcMap | |
.dimension(neighbourhoodsDim) | |
.group(neighbourhoodsGroup) | |
.mapOptions({ | |
center: [53.5250, -113.4448], | |
zoom: 10, | |
scrollWheelZoom: false, | |
minZoom: 10, | |
maxZoom: 16, | |
touchZoom: false | |
}) | |
.geojson(neighbourhoodsGejson) | |
.valueAccessor(function(d) { return d.value; }) | |
.colors(mapColors) | |
.colorAccessor(function(d) { return d.value; }) | |
.featureKeyAccessor(function(feature) { return feature.properties.name; }) | |
.legend(dc.leafletLegend().position("bottomright")) | |
.on("renderlet.dcMap", function infractionMapInfoUpdate(chart, filter) { | |
eventTrigger(function updater() { | |
//get all the feature layers | |
var eArray = Object.values(chart.map()._layers) | |
.filter(function(e) { if( e.hasOwnProperty("feature") ) return e; } ); | |
//get path(layer) popupContent and update the map info | |
eArray.forEach(function(layer) { | |
chart.map()._layers[layer._leaflet_id].on("mouseover", function() { | |
info.update(layer); | |
}); | |
}); | |
}); | |
}) | |
.on("preRender.dcMap", colorUpdate) | |
.on("preRedraw.dcMap", colorUpdate); | |
rowChart.dimension(yearDim) | |
.group(yearGroup) | |
.height(setHeight(rowChart)) | |
.margins(chartMargin(rowChart, {top: 0.02, right: 0.02, bottom: 0.10, left:0.02})) | |
.useViewBoxResizing(true) | |
.label(function(d) { return d.key; }) | |
.title(function(d) { return d.value.toLocaleString(); }) | |
.elasticX(true) | |
.on("pretransition.xAxis", fontSizeUpdate("row-chart")); | |
rowChart.xAxis().ticks(6); | |
barChart.dimension(monthDim) | |
.group(monthGroup) | |
.height(setHeight(barChart)) | |
.margins(chartMargin(barChart, {top: 0.02, right: 0.02, bottom: 0.10, left:0.12})) | |
.useViewBoxResizing(true) | |
.title(function(d) { return d.value.toLocaleString(); }) | |
.x(d3.scale.ordinal().domain(monthNames)) | |
.xUnits(dc.units.ordinal) | |
.elasticY(true) | |
.on("pretransition.Axis", fontSizeUpdate("bar-chart")); | |
barChart.xAxis().ticks(6); | |
pie | |
.dimension(complaintsDim) | |
.group(complaintsGroup) | |
.height(setHeight(pie)) | |
.cx(pie.width() / pieXscale) | |
.radius(pie.width() * pieRscale) | |
.innerRadius(pie.width() * pieInnerRscale) | |
.useViewBoxResizing(true) | |
.label(function(d) { return ((d.value / countSum) * 100).toFixed(3) + '%'; }) | |
.title(function(d) { return d.key + ': ' + ((d.value / countSum) * 100).toFixed(3) + '%'; }) | |
.colorAccessor(function(d, i) {return i; }) | |
.colors(pieScaleColors) | |
.legend(dc.legend() | |
.y(Math.round(pie.height() * 0.02 , 1)) | |
.gap(Math.round(pie.height() * 0.02 , 1)) | |
) | |
.on("pretransition.legend", function legendDynamicText(chart) { | |
eventTrigger(function textUpdater() { | |
//https://github.com/dc-js/dc.js/blob/master/web/examples/pie-external-labels.html | |
//solution for adding dynamic text to legend | |
chart.selectAll(".dc-legend-item text") | |
.text('') | |
.append("tspan") | |
.text(function(d) { return d.name; }) | |
.style("font-size", Math.round(chart.height() * 0.04, 1)) | |
.append("tspan") | |
.attr('x', Math.round(pie.width() * 0.41, 1)) | |
.attr('text-anchor', 'end') | |
.text(function(d) { return d.data.toLocaleString(); }) | |
.style("font-size", Math.round(chart.height() * 0.04, 1)); | |
}); | |
}); | |
bubbleChart | |
.dimension(initiatorDim) | |
.group(statusGroup) | |
.height(setHeight(bubbleChart)) | |
.useViewBoxResizing(true) | |
.margins(chartMargin(barChart, {top: 0.15, right: 0.1428, bottom: 0.15, left: 0.1428})) | |
.clipPadding(55) | |
.radiusValueAccessor(function(d) { return d.value; }) | |
.maxBubbleRelativeSize(0.24) | |
.r(d3.scale.linear() | |
.domain( d3.extent( bubbleChart.group().all(), bubbleChart.valueAccessor() ) ) | |
) | |
.elasticRadius(true) | |
.x(d3.scale.ordinal()) | |
.label(function(d) { return d.key[0]+": "+d.key[1]; }) | |
.title(function(d) { return '('+d.key[0]+')'+d.key[1] + ': ' + d.value.toLocaleString(); }) | |
.colorAccessor(function(d, i) { return i; }) | |
.colors(bubbleScaleColors); | |
dc.renderAll(); | |
//Choropleth map | |
map = dcMap.map(); | |
//reset map location when window is resized | |
map.on("resize", function(e) { map.setZoom(10).getBounds().getCenter(); }); | |
//----------------------------Additions to leaflet map---------------------------- | |
//SlideMenu, dc reset, and map info | |
//https://github.com/unbam/Leaflet.SlideMenu | |
title = '<h3>Neighbourhood Selection</h3>', | |
contents = '<div id="selection" class="svg-container"></div>', | |
slideMenu = L.control.slideMenu('', {position: 'topright', menuposition: 'topright', width: '70%', height: '45%', delay: '50'}).addTo(map); | |
slideMenu.setContents(title + contents); | |
//http://leafletjs.com/examples/choropleth.html | |
info = L.control({position: "bottomleft"}); | |
info.onAdd = function(map) { | |
this._div = L.DomUtil.create("div", "myinfo"); | |
this.update(); | |
return this._div; | |
}; | |
info.update = function(e) { | |
this._div.innerHTML = "<span><strong>Neighbourhood Infractions</strong></span><br>" + (e ? | |
"<span>"+e._popupContent+"</span>": "Hover over a map region"); | |
}; | |
info.addTo(map); | |
neighbourSelections = dc.selectMenu("#selection"); | |
//add filter listerner to update sum and percentage text | |
statsUpdate(neighbourSelections); | |
neighbourSelections.dimension(neighbourhoodsDim) | |
.group(neighbourhoodsGroup) | |
.multiple(true) | |
.numberVisible(11) | |
.controlsUseVisibility(true) | |
.order(function (a,b) { return a.value > b.value ? 1 : b.value > a.value ? -1 : 0; }); | |
neighbourSelections.render(); | |
//----------------------------Additions to leaflet map---------------------------- | |
//---------------------------Addition to Pie Chart Sum and Percentage Stats--------------------------- | |
texts = [ | |
{ | |
class: "stats-title", | |
x: 0, | |
y: pie.height() * 0.97, | |
content: "Sum:", | |
"text-anchor": "start" | |
}, | |
{ | |
id: "sum", | |
x: pie.width() * 0.13, | |
y: pie.height() * 0.97, | |
content: sumofAllInfractions.value().toLocaleString(), | |
"text-anchor": "start", | |
"font-size": Math.round(pie.height() * 0.14, 1) | |
}, | |
{ | |
class: "stats-title", | |
x: pie.cx(), | |
y: pie.height() * 0.46, | |
content: "Percentage:", | |
"text-anchor": "middle", | |
}, | |
{ | |
id: "percent", | |
x: pie.cx(), | |
y: pie.height() * 0.58, | |
content: ((sumofAllInfractions.value()/countSum) * 100).toFixed(3), | |
"text-anchor": "middle", | |
"font-size": Math.round(pie.height() * 0.12, 1) | |
} | |
]; | |
d3.select("#pie-plot svg") | |
.selectAll(".stats text") | |
.data(texts).enter() | |
.append("text") | |
.classed("stats", true) | |
.style( "font-size", function(d) { return d["font-size"] ? d["font-size"] : Math.round(pie.height() * 0.08, 1); } ) | |
.text(function(d) { return d.content; }) | |
.attr({ | |
id: function(d) { return d.id ? d.id : ""; }, | |
class: function(d) { return d.class ? d.class : ""; }, | |
x: function(d) { return d.x; }, | |
y: function(d) { return d.y; }, | |
"text-anchor": function(d) { return d["text-anchor"]; } | |
}); | |
//---------------------------Addition to Pie Chart Sum and Percentage Stats--------------------------- | |
//------------Font Size for legend and label texts------------ | |
chartTexts = [ | |
{selector: "text.pie-slice" , scale: 0.05}, | |
{selector: "text.row" , scale: 0.05}, | |
{selector: "g.node text" , scale: 0.04} | |
]; | |
chartTexts.forEach(function(text){textFontSize(text.selector, text.scale); }); | |
//------------Font Size for axis and label texts------------ | |
d3.select("#spinner").remove(); //remove spinner after files and charts are loaded | |
function setHeight(chart) { return chart.width() * chartHeightScale; }; | |
function textFontSize(selector, scale) { | |
d3.selectAll(selector) | |
.style("font-size", Math.round(pie.height() * scale, 1)); | |
}; | |
function statsUpdate(chart) { | |
chart.on("filtered." + chart.chartID(), function() { | |
eventTrigger(function htmlUpdater() { | |
//update the sum text | |
d3.select("#sum").html( sumofAllInfractions.value().toLocaleString() ); | |
//update the percent text | |
d3.select("#percent").html( ((sumofAllInfractions.value()/countSum) * 100).toFixed(3) ); | |
}); | |
}); | |
}; | |
function colorUpdate(chart, filter) { | |
eventTrigger(function() { | |
//update color domain to correspond with user filters | |
chart.calculateColorDomain( d3.extent(chart.group().all(), chart.valueAccessor()) ); | |
}); | |
}; | |
function fontSizeUpdate(chartId) { | |
return function(chart, filter) { | |
eventTrigger(function sizeUpdater() { | |
chart.selectAll("#"+chartId+" g.tick text") | |
.style("font-size", Math.round(chart.height() * 0.05, 1)); | |
}); | |
}; | |
}; | |
function chartMargin(chart, margin) { | |
return { | |
top: Math.round(chart.height() * margin.top , 1), | |
right: Math.round(chart.width() * margin.right , 1), | |
bottom: Math.round(chart.height() * margin.bottom , 1), | |
left: Math.round(chart.width() * margin.left , 1) | |
}; | |
}; | |
function eventTrigger(func){ | |
return dc.events.trigger(func); | |
}; | |
}; //renderCharts | |
})//document.addEventListener | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!doctype html> | |
<html> | |
<head> | |
<title>Bylaw Infractions</title> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=0.9"> | |
<meta http-equiv="X-UA-Compatible" content="ie=edge"> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.2.0/css/bootstrap.min.css"> | |
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/Edmonton-Open-Data/Edmonton-Bylaw-Infractions-II@master/libs/keen_io/keen-dashboards.css"> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/dc/2.1.8/dc.min.css"> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/leaflet.css"> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"> | |
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/leaflet-map/dc-leaflet-legend.min.css"> | |
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/Edmonton-Open-Data/Edmonton-Bylaw-Infractions-II@master/libs/slideMenu/L.Control.SlideMenu.css"> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.2.0/js/bootstrap.min.js"></script> | |
<link rel="stylesheet" href="bylaw_infractions.css"> | |
</head> | |
<body class="application"> | |
<div id="spinner"></div> | |
<!-- Navbar --> | |
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation"> | |
<div class="container-fluid"> | |
<div class="navbar-header"> | |
<a class="navbar-brand" href="https://github.com/Edmonton-Open-Data/Edmonton-Bylaw-Infractions-II"> | |
Edmonton's Bylaw Infractions | |
</a> | |
</div> | |
</div> | |
</div> | |
<!-- Makes Row Responsive --> | |
<div id="board" class="container-fluid"> | |
<div class="row"> | |
<!-- column 1 for row --> | |
<div class="col-lg-4 col-md-4"> | |
<!-- row in column 1 --> | |
<div class="row"> | |
<!-- Map --> | |
<div class="col-lg-12 col-md-12"> | |
<div class="chart-wrapper"> | |
<div class="chart-stage"> | |
<div class="chart-title"> | |
<span> | |
<strong>Neighbourhoods Map</strong> | |
</span> | |
</div> | |
<span id="records-count"></span> | |
<div id="map-plot"></div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- column 2 for row --> | |
<div class="col-lg-4 col-md-4"> | |
<!-- row in column 2 --> | |
<div class="row"> | |
<!-- Row Chart --> | |
<div class="col-lg-12 col-md-12"> | |
<div class="chart-wrapper"> | |
<div class="chart-stage"> | |
<div id="row-chart" class="svg-container"> | |
<div class="chart-title"> | |
<span> | |
<strong>Yearly Trend</strong> | |
</span> | |
<span> | |
<a class="reset" style="display:none" href="javascript:rowChart.filterAll(); dc.redrawAll();"> | |
reset | |
</a> | |
</span> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Bar Chart --> | |
<div class="col-lg-12 col-md-12"> | |
<div class="chart-wrapper"> | |
<div class="chart-stage"> | |
<div id="bar-chart" class="svg-container"> | |
<div class="chart-title"> | |
<span> | |
<strong>Monthly Trends</strong> | |
</span> | |
<span> | |
<a class="reset" style="display:none" href="javascript:barChart.filterAll(); dc.redrawAll();"> | |
reset | |
</a> | |
</span> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- column 3 for row --> | |
<div class="col-lg-4 col-md-4"> | |
<!-- row in column 3 --> | |
<div class="row"> | |
<!-- Pie Chart --> | |
<div class="col-lg-12 col-md-12"> | |
<div class="chart-wrapper"> | |
<div class="chart-stage"> | |
<div id="pie-plot" class="svg-container"> | |
<div class="chart-title"> | |
<span> | |
<strong> | |
Types of Complaint | |
</strong> | |
</span> | |
<span> | |
<a class="reset" style="display:none" href="javascript:pie.filterAll(); dc.redrawAll();"> | |
reset | |
</a> | |
</span> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Bubble Cloud --> | |
<div class="col-lg-12 col-md-12"> | |
<div class="chart-wrapper"> | |
<div class="chart-stage"> | |
<div id="bubble-plot" class="svg-container"> | |
<div class="chart-title"> | |
<span> | |
<strong> | |
Initiators and Status | |
</strong> | |
</span> | |
<span> | |
<a class="reset" style="display:none" href="javascript:bubbleChart.filterAll(); dc.redrawAll();"> | |
reset | |
</a> | |
</span> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3-queue/1.2.3/queue.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/crossfilter/1.3.12/crossfilter.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/dc/2.1.8/dc.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/leaflet.js"></script> | |
<script src="https://cdn.jsdelivr.net/gh/Edmonton-Open-Data/Edmonton-Bylaw-Infractions-II@master/libs/slideMenu/L.Control.SlideMenu.js"></script> | |
<script src="https://cdn.jsdelivr.net/gh/Edmonton-Open-Data/Edmonton-Bylaw-Infractions-II@master/libs/dc_addons/base-map-chart.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/src/scripts/base-leaflet-chart.min.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/src/scripts/leaflet-choropleth-chart.min.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/src/scripts/bubble-cloud.min.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/src/scripts/leaflet-legend.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/spin.js/2.3.2/spin.min.js"></script> | |
<script src="bylaw_infractions.js"></script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment