GeoIP Map (#142)
* First working version of world map chart * Cleanup code, fix aspect ratio, add GeoIP Map header * Remove limited-height on session list with already limited content * Update package lock * Integrate map into service page * Adjust map colors * Adjust colors further Co-authored-by: R. Miles McCain <github@sendmiles.email>
This commit is contained in:
parent
83b20643d2
commit
9832de0c19
1020
package-lock.json
generated
1020
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -20,6 +20,7 @@
|
||||
"@fortawesome/fontawesome-free": "^5.15.1",
|
||||
"a17t": "^0.5.1",
|
||||
"apexcharts": "^3.24.0",
|
||||
"datamaps": "^0.5.9",
|
||||
"flag-icon-css": "^3.5.0",
|
||||
"inter-ui": "^3.15.0",
|
||||
"litepicker": "^2.0.11"
|
||||
|
@ -11,6 +11,11 @@
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
.force-limited-height {
|
||||
max-height: 400px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.rf {
|
||||
text-align: right !important;
|
||||
}
|
||||
|
@ -12,11 +12,13 @@
|
||||
<script src="{% static 'apexcharts/dist/apexcharts.min.js'%}"></script>
|
||||
<script src="{% static 'litepicker/dist/nocss/litepicker.js' %}"></script>
|
||||
<script src="{% static 'litepicker/dist/plugins/ranges.js' %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'litepicker/dist/css/litepicker.css' %}">
|
||||
|
||||
<script src="{% static 'd3/d3.min.js' %}"></script>
|
||||
<script src="{% static 'topojson/build/topojson.min.js' %}"></script>
|
||||
<script src="{% static 'datamaps/dist/datamaps.world.min.js' %}"></script>
|
||||
<script src="{% static 'dashboard/js/base.js' %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'dashboard/css/global.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'flag-icon-css/css/flag-icon.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'litepicker/dist/css/litepicker.css' %}">
|
||||
{% block extra_head %}
|
||||
{% endblock %}
|
||||
</head>
|
||||
|
59
shynet/dashboard/templates/dashboard/includes/map_chart.html
Normal file
59
shynet/dashboard/templates/dashboard/includes/map_chart.html
Normal file
@ -0,0 +1,59 @@
|
||||
{% load helpers %}
|
||||
|
||||
<div id="map-chart" class="relative"></div>
|
||||
<script>
|
||||
// Colors
|
||||
const lightBlue = "#C4B5FD";
|
||||
const highlightBlue = "#8B5CF6";
|
||||
const white = "#ffffff";
|
||||
|
||||
// Data maps
|
||||
const countryMapData = {};
|
||||
const countryMapColors = {};
|
||||
const countryMap = {
|
||||
{% for country in countries %}"{{country.country|safe|datamap_id}}": {{country.count}},
|
||||
{% endfor %}
|
||||
};
|
||||
|
||||
// Max session count will be full opacity
|
||||
const maxSessionCount = Math.max(...Object.values(countryMap));
|
||||
|
||||
// Color scale starts from opacity 0.1 - 1.0, 0 sessions gets opacity 0
|
||||
const minPercentage = 0.1
|
||||
|
||||
// Loop over country map and transform data for Datamaps use
|
||||
const keys = Object.keys(countryMap);
|
||||
const length = keys.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
countryMapData[keys[i]] = {
|
||||
sessionCount: countryMap[keys[i]],
|
||||
color: `rgba(124, 58, 237, ${countryMap[keys[i]] === 0 ? 0 : minPercentage + (countryMap[keys[i]] / maxSessionCount * (1 - minPercentage))})`
|
||||
};
|
||||
countryMapColors[keys[i]] = countryMapData[keys[i]].color;
|
||||
}
|
||||
|
||||
// Create datamap
|
||||
const map = new Datamap({
|
||||
element: document.getElementById('map-chart'),
|
||||
projection: 'mercator',
|
||||
responsive: true,
|
||||
geographyConfig: {
|
||||
borderColor: lightBlue,
|
||||
highlightBorderColor: highlightBlue,
|
||||
highlightBorderWidth: 1.5,
|
||||
highlightFillColor: (geography) => geography.color || white,
|
||||
highlightFillOpacity: 0.9,
|
||||
popupTemplate: (geography, data) => '<div class="hoverinfo"><strong>' + geography.properties.name + '</strong>: ' + data.sessionCount + ' sessions</div>'
|
||||
|
||||
},
|
||||
fills: {
|
||||
defaultFill: white
|
||||
},
|
||||
data: countryMapData,
|
||||
aspectRatio: 0.68
|
||||
});
|
||||
map.updateChoropleth(countryMapColors);
|
||||
|
||||
// Handle resize. TODO: debounce?
|
||||
window.onresize = () => map.resize();
|
||||
</script>
|
@ -132,6 +132,10 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card ~neutral !low force-limited-height py-2 overflow-y-hidden">
|
||||
<p class="text-sm font-semibold mx-2 p-2 border-b mb-2">Sessions by Geography</p>
|
||||
{% include 'dashboard/includes/map_chart.html' with countries=stats.countries %}
|
||||
</div>
|
||||
<div class="card ~neutral !low limited-height py-2">
|
||||
<table class="table">
|
||||
<thead class="text-sm">
|
||||
@ -166,40 +170,6 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card ~neutral !low limited-height py-2">
|
||||
<table class="table">
|
||||
<thead class="text-sm">
|
||||
<tr>
|
||||
<th>Country</th>
|
||||
<th class="rf">Sessions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for country in stats.countries %}
|
||||
<tr>
|
||||
<td class="truncate w-full max-w-0 relative" title="{{country.country|country_name}}">
|
||||
{% include 'dashboard/includes/bar.html' with count=country.count max=stats.countries.0.count total=stats.session_count %}
|
||||
<div class="relative flex items-center">
|
||||
<span class="flex-none {{country.country|flag_class}}"></span> {{country.country|country_name}}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex justify-end items-center">
|
||||
{{country.count|intcomma}}
|
||||
<span class="text-xs rf" style="min-width: 48px">
|
||||
({{country.count|percent:stats.session_count}})
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td><span class="text-gray-600">No data yet...</span></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card ~neutral !low limited-height py-2">
|
||||
<table class="table">
|
||||
<thead class="text-sm">
|
||||
@ -304,7 +274,7 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card ~neutral !low limited-height py-2">
|
||||
<div class="card ~neutral !low py-2">
|
||||
{% include 'dashboard/includes/session_list.html' %}
|
||||
<hr class="sep h-8 md:h-12">
|
||||
<a href="{% contextual_url 'dashboard:service_session_list' service.uuid %}" class="button ~neutral w-auto mb-2">View more
|
||||
|
@ -43,6 +43,14 @@ def country_name(isocode):
|
||||
return "Unknown"
|
||||
|
||||
|
||||
@register.filter
|
||||
def datamap_id(isocode):
|
||||
try:
|
||||
return pycountry.countries.get(alpha_2=isocode).alpha_3
|
||||
except:
|
||||
return "UNKNOWN"
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def relative_stat_tone(
|
||||
start,
|
||||
|
@ -310,6 +310,9 @@ NPM_FILE_PATTERNS = {
|
||||
"stimulus": [os.path.join("dist", "stimulus.umd.js")],
|
||||
"inter-ui": [os.path.join("Inter (web)", "*")],
|
||||
"@fortawesome": [os.path.join("fontawesome-free", "js", "all.min.js")],
|
||||
"datamaps": [os.path.join("dist", "datamaps.world.min.js")],
|
||||
"d3": ["d3.min.js"],
|
||||
"topojson": [os.path.join("build", "topojson.min.js")],
|
||||
"flag-icon-css": [
|
||||
os.path.join("css", "flag-icon.min.css"),
|
||||
os.path.join("flags", "*"),
|
||||
|
Loading…
Reference in New Issue
Block a user