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",
|
"@fortawesome/fontawesome-free": "^5.15.1",
|
||||||
"a17t": "^0.5.1",
|
"a17t": "^0.5.1",
|
||||||
"apexcharts": "^3.24.0",
|
"apexcharts": "^3.24.0",
|
||||||
|
"datamaps": "^0.5.9",
|
||||||
"flag-icon-css": "^3.5.0",
|
"flag-icon-css": "^3.5.0",
|
||||||
"inter-ui": "^3.15.0",
|
"inter-ui": "^3.15.0",
|
||||||
"litepicker": "^2.0.11"
|
"litepicker": "^2.0.11"
|
||||||
|
@ -11,6 +11,11 @@
|
|||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.force-limited-height {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.rf {
|
.rf {
|
||||||
text-align: right !important;
|
text-align: right !important;
|
||||||
}
|
}
|
||||||
|
@ -12,11 +12,13 @@
|
|||||||
<script src="{% static 'apexcharts/dist/apexcharts.min.js'%}"></script>
|
<script src="{% static 'apexcharts/dist/apexcharts.min.js'%}"></script>
|
||||||
<script src="{% static 'litepicker/dist/nocss/litepicker.js' %}"></script>
|
<script src="{% static 'litepicker/dist/nocss/litepicker.js' %}"></script>
|
||||||
<script src="{% static 'litepicker/dist/plugins/ranges.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>
|
<script src="{% static 'dashboard/js/base.js' %}"></script>
|
||||||
<link rel="stylesheet" href="{% static 'dashboard/css/global.css' %}">
|
<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 'flag-icon-css/css/flag-icon.min.css' %}">
|
||||||
|
<link rel="stylesheet" href="{% static 'litepicker/dist/css/litepicker.css' %}">
|
||||||
{% block extra_head %}
|
{% block extra_head %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</head>
|
</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>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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">
|
<div class="card ~neutral !low limited-height py-2">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead class="text-sm">
|
<thead class="text-sm">
|
||||||
@ -166,40 +170,6 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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">
|
<div class="card ~neutral !low limited-height py-2">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead class="text-sm">
|
<thead class="text-sm">
|
||||||
@ -304,7 +274,7 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card ~neutral !low limited-height py-2">
|
<div class="card ~neutral !low py-2">
|
||||||
{% include 'dashboard/includes/session_list.html' %}
|
{% include 'dashboard/includes/session_list.html' %}
|
||||||
<hr class="sep h-8 md:h-12">
|
<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
|
<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"
|
return "Unknown"
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def datamap_id(isocode):
|
||||||
|
try:
|
||||||
|
return pycountry.countries.get(alpha_2=isocode).alpha_3
|
||||||
|
except:
|
||||||
|
return "UNKNOWN"
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag
|
@register.simple_tag
|
||||||
def relative_stat_tone(
|
def relative_stat_tone(
|
||||||
start,
|
start,
|
||||||
|
@ -310,6 +310,9 @@ NPM_FILE_PATTERNS = {
|
|||||||
"stimulus": [os.path.join("dist", "stimulus.umd.js")],
|
"stimulus": [os.path.join("dist", "stimulus.umd.js")],
|
||||||
"inter-ui": [os.path.join("Inter (web)", "*")],
|
"inter-ui": [os.path.join("Inter (web)", "*")],
|
||||||
"@fortawesome": [os.path.join("fontawesome-free", "js", "all.min.js")],
|
"@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": [
|
"flag-icon-css": [
|
||||||
os.path.join("css", "flag-icon.min.css"),
|
os.path.join("css", "flag-icon.min.css"),
|
||||||
os.path.join("flags", "*"),
|
os.path.join("flags", "*"),
|
||||||
|
Loading…
Reference in New Issue
Block a user