ASP.NET Core 6.0 - World Heat Map

This article will describe the implementation of IP Geolocation to highlight countries on a world map. I will assume you have downloaded the ASP.NET Core 6.0 - Homegrown Analytics Project.

Homegrown Analytics Project and Article Series

The Homegrown Analytics Project (HAP) is designed and organized to allow easy integration with existing projects. V2 simplifies the process with an Analytics area folder. The analytics schema can be implemented with SQL scripts or EF Core migration. See ASP.NET Core 6.0 - Analytics Schema and Integration. The HAP details page includes the version change log.

ASP.NET Core 6.0

Updated with links as the articles become published.

As I added more features and new pages, a dashboard with an overview of aggregated data became more important. First, I developed a line chart of page hit counts from a query of the selected segments and basic filters. I add a second dataset from the previous span of time for the selected segments. Then I developed pie charts for browser, operating system, and screen sizes for the current span.

I wanted a World Heat Map like Google Analytics - Demographics - Users by Country graphic. I was surprised by my prototype which implemented a svg of a world map with selectable countries and a database to lookup the country code for an ip address. The map is provided by AMCHARTS - Free SVG Maps . The database is provided by IP Geolocation by DB-IP. The MaxMind.Db NuGet package is required to read the mmdb file format.

The prototype decoded and serialized up to 10 countries. This required 2 GroupBy queries. The The first groups the AnonymizedIp from the filtered pageHitsQuery. I initialize the dbReader and pass it to a GetIpCountryCode method which returns the country code for the ip. The prototype counted distinct AnonymizedIps. The UsePageHitCount option and the Count extension provides the number of PageHits per ip. The second groups the country codes and uses a CountryNameDictionary to look up the full country name for the selected countryTakeCount. The Sum extension provides the total number of IPs or PageHits per country. Testing with the AnalyticsSampleData derived from a KenHaggerty.Com db backup indicated I should decode and serialize up to 20 countries.

string path = Path.Combine(_env.ContentRootPath, AnalyticsSettings.IpToCountryDataPath);
using var dbReader = new MaxMind.Db.Reader(path);
var countryList = await pageHitsQuery
    .GroupBy(ph => ph.AnonymizedIp)
    .Select(ic => new ItemCountModel()
    {
        Label = AnalyticsUtilities.GetIpCountryCode(dbReader, ic.Key),
        Count = UsePageHitCount ? ic.Count() : 1
    }).ToListAsync().ConfigureAwait(false);

var countryTakeCount = 20;
CountryCounts = countryList
    .GroupBy(cl => cl.Label)
    .Select(cc => new ItemCountModel()
    {
        Label = cc.Key,
        ItemIdString = AnalyticsUtilities.CountryNameDictionary.ContainsKey(cc.Key) ?
            AnalyticsUtilities.CountryNameDictionary[cc.Key] : "Not Found",
        Count = cc.Sum(s => s.Count)
    })
    .OrderByDescending(cc => cc.Count)
    .ThenBy(cc => cc.ItemIdString)
    .Take(countryTakeCount)
    .ToList();

The prototype highlighted the countries with a list of 10 shades of blue I found online. I could not find a list of 20 shades of blue so I developed a utility to generate 10 and 20 gradients of blue. I added the utility with a selectable hue to the FREE ASP.NET Core 6.0 - Demos And Utilities Project . I published the utility at Hue Gradients - Ken's Demo Site .

Blue Gradients.

The utility provides an array of hex color codes. I added the codes to the BlueGradient10 and BlueGradient20 lists in AnalyticsUtilities.

public static readonly List<string> BlueGradient10 = new() { "#0000CC", "#0000E5", "#0000FF",
    "#1919FE", "#3333FF", "#4C4CFF", "#6666FF", "#7F7FFF", "#9999FE", "#B2B2FF" };

public static readonly List<string> BlueGradient20 = new() { "#000066", "#00007A", "#00008E",
    "#0000A3", "#0000B7", "#0000CB", "#0000E0", "#0000F4", "#0A0AFF", "#1E1EFE", "#3232FE",
    "#4747FF", "#5B5BFF", "#7070FF", "#8484FF", "#9898FF", "#ADADFE", "#C1C1FF",
    "#D6D6FF", "#EAEAFF" };

I use BlueGradient10 and BlueGradient20 lists to populate the CountryCounts.Description and a countryColors Dictionary to pass the codes to the UI. I use BlueGradient10 when the selected country count is less than 11.

Dictionary<string, string> countryColors = new();
var countryCount = CountryCounts.Count;
var colorIndex = 0;
for (var i = 0; i < countryCount; i++)
{
    if (countryCount < 11)
    {
        CountryCounts[i].Description = AnalyticsUtilities.BlueGradient10[colorIndex];
        countryColors.Add(CountryCounts[i].Label, AnalyticsUtilities.BlueGradient10[colorIndex]);
        colorIndex = colorIndex < 10 ? colorIndex + 1 : 0;
    }
    else
    {
        CountryCounts[i].Description = AnalyticsUtilities.BlueGradient20[colorIndex];
        countryColors.Add(CountryCounts[i].Label, AnalyticsUtilities.BlueGradient20[colorIndex]);
        colorIndex = colorIndex < 20 ? colorIndex + 1 : 0;
    }
}
CountryColorsJson = JsonSerializer.Serialize(countryColors);

I use the CountryColorsJson in JavaScript to highlight the country on the area of the world map. I implement an object tag with the data attribute set to the svg path relative to wwwroot.

<object id="WorldObject" data="/images/analytics/worldKashmirLow.svg" type="image/svg+xml" onload="this.ready = true;"></object>

Notice onload="this.ready = true;". Sometimes, not often, the WorldObject loads before the add event listener is executed.

let worldImage = document.getElementById('WorldObject');
let countryColors = @Html.Raw(Model.CountryColorsJson);

function fillWorldImage() {
    let svgDoc = worldImage.contentDocument;
    for (var c in countryColors) {
        if (countryColors.hasOwnProperty(c)) {
            let country = svgDoc.getElementById(c);
            if (country != null) country.style.fill = countryColors[c];
        }
    }
};

if (worldImage.ready)
    fillWorldImage();
else
    worldImage.addEventListener('load', function () { fillWorldImage(); });

The images display the AnalyticsSampleData derived from a KenHaggerty.Com db backup.

Dashboard Desktop. Dashboard Mobile.
Width

Live Demonstration

I publish the Users Without Passwords Project (UWPP) on the FIDO subdomain and the Users With Comments Project (UWCP) on the Preview subdomain. I integrated the HAP with the published projects. The subdomains have their own databases. Registered users can request permission to access the Admin - Analytics Dashboard. The permission will be granted automatically. The UWCP implements the Users With Device 2FA Project (UWD2FAP). Preview requires 2FA enabled to access admin pages.

After you register, browse to Manage Account > Visit Log. Click the More link in the HAP banner. Request permission.

Preview Visit Log.
Preview Request Permission.
Ken Haggerty
Created 09/19/22
Updated 09/19/22 19:23 GMT

Log In or Reset Quota to read more.

Article Tags:

Analytics JavaScript Json Model
Successfully completed. Thank you for contributing.
Contribute to enjoy content without advertisments.
Something went wrong. Please try again.

Comments(0)

Loading...
Loading...

Not accepting new comments.

Submit your comment. Comments are moderated.

User Image.
DisplayedName - Member Since ?