Added World Map to the Geo Location panel on the HTML report.

Area and bar charts are still available for this panel.
Closes #524
This commit is contained in:
Gerardo O 2024-04-11 19:26:38 -05:00
parent d7c7dacae2
commit 5ee83fd63c
11 changed files with 377 additions and 22 deletions

4
.gitignore vendored
View File

@ -97,6 +97,8 @@ Makefile.in
/src/facss.h
/src/hoganjs.h
/src/tpls.h
/src/countries110m.h
/src/topojsonjs.h
# intermediate resources
/resources/css/app.css.tmp
@ -107,3 +109,5 @@ Makefile.in
/resources/js/d3.v?.min.js.tmp
/resources/js/hogan.min.js.tmp
/resources/tpls.html.tmp
/resources/countries-110m.json.tmp
/resources/js/topojson.v3.min.js.tmp

View File

@ -9,7 +9,9 @@ dist_noinst_DATA = \
resources/css/fa.min.css \
resources/js/app.js \
resources/js/charts.js \
resources/countries-110m.json \
resources/js/d3.v7.min.js \
resources/js/topojson.v3.min.js \
resources/js/hogan.min.js
noinst_PROGRAMS = bin2c
@ -21,7 +23,9 @@ BUILT_SOURCES = \
src/facss.h \
src/appcss.h \
src/d3js.h \
src/topojsonjs.h \
src/hoganjs.h \
src/countries110m.h \
src/chartsjs.h \
src/appjs.h
@ -31,14 +35,18 @@ CLEANFILES = \
src/facss.h \
src/appcss.h \
src/d3js.h \
src/topojsonjs.h \
src/hoganjs.h \
src/countries110m.h \
src/chartsjs.h \
src/appjs.h \
resources/tpls.html.tmp \
resources/countries-110m.json.tmp \
resources/css/bootstrap.min.css.tmp \
resources/css/fa.min.css.tmp \
resources/css/app.css.tmp \
resources/js/d3.v7.min.js.tmp \
resources/js/topojson.v3.min.js.tmp \
resources/js/hogan.min.js.tmp \
resources/js/charts.js.tmp \
resources/js/app.js.tmp
@ -51,6 +59,14 @@ if HAS_SEDTR
else
./bin2c $(srcdir)/resources/tpls.html src/tpls.h tpls
endif
# countries.json
src/countries110m.h: bin2c$(EXEEXT) $(srcdir)/resources/countries-110m.json
if HAS_SEDTR
cat $(srcdir)/resources/countries-110m.json | sed "s/^[[:space:]]*//" | sed "/^$$/d" | tr -d "\r\n" > $(srcdir)/resources/countries-110m.json.tmp
./bin2c $(srcdir)/resources/countries-110m.json.tmp src/countries110m.h countries_json
else
./bin2c $(srcdir)/resources/countries-110m.json src/countries110m.h countries_json
endif
# Bootstrap
src/bootstrapcss.h: bin2c$(EXEEXT) $(srcdir)/resources/css/bootstrap.min.css
if HAS_SEDTR
@ -83,6 +99,14 @@ if HAS_SEDTR
else
./bin2c $(srcdir)/resources/js/d3.v7.min.js src/d3js.h d3_js
endif
# topojson.js
src/topojsonjs.h: bin2c$(EXEEXT) $(srcdir)/resources/js/topojson.v3.min.js
if HAS_SEDTR
cat $(srcdir)/resources/js/topojson.v3.min.js | sed "s/^[[:space:]]*//" | sed "/^$$/d" | tr -d "\r\n" > $(srcdir)/resources/js/topojson.v3.min.js.tmp
./bin2c $(srcdir)/resources/js/topojson.v3.min.js.tmp src/topojsonjs.h topojson_js
else
./bin2c $(srcdir)/resources/js/topojson.v3.min.js src/topojsonjs.h topojson_js
endif
# Hogan.js
src/hoganjs.h: bin2c$(EXEEXT) $(srcdir)/resources/js/hogan.min.js
if HAS_SEDTR

File diff suppressed because one or more lines are too long

View File

@ -1144,3 +1144,19 @@ html.dark.purple,
.dark.purple .line1 {
stroke: #d048b6;
}
.country {
fill: #ccc;
stroke: #fff;
stroke-width: 0.5px;
}
.country:hover {
fill: #b3b3b3;
}
.dark .country {
fill: #ccc;
stroke: #222;
stroke-width: 0.5px;
}
.dark .legend-svg text {
fill: #FFF;
}

View File

@ -1094,6 +1094,16 @@ GoAccess.Charts = {
return arr.join(' ');
},
getWMap: function (panel, plotUI, data) {
var chart = WorldMap(d3.select("#chart-" + panel));
chart.width($("#chart-" + panel).getBoundingClientRect().width);
chart.height(400);
chart.metric(plotUI['d3']['y0']['key']);
chart.opts(plotUI);
return chart;
},
getAreaSpline: function (panel, plotUI, data) {
var dualYaxis = plotUI['d3']['y1'];
@ -1186,6 +1196,9 @@ GoAccess.Charts = {
case 'bar':
chart = this.getVBar(panel, plotUI, data);
break;
case 'wmap':
chart = this.getWMap(panel, plotUI, data);
break;
}
return chart;
@ -1196,7 +1209,7 @@ GoAccess.Charts = {
d3.select('#chart-' + panel + '>.chart-tooltip-wrap')
.remove();
// remove svg
d3.select('#chart-' + panel).select('svg')
d3.select('#chart-' + panel).selectAll('svg')
.remove();
// add chart to the document
d3.select("#chart-" + panel)

View File

@ -37,6 +37,286 @@ function truncate(text, width) {
});
}
function WorldMap() {
const maxLat = 84;
let path = null;
let projection = null;
let tlast = [0, 0];
let slast = null;
let opts = {};
let metric = 'hits';
let margin = {
top: 20,
right: 50,
bottom: 40,
left: 50
};
let width = 760;
let height = 170;
function innerW() {
return width - margin.left - margin.right;
}
function mapData(data) {
return data.reduce((countryData, region) => {
if (!region.items) countryData.push(region);
else region.items.forEach(item => countryData.push({
data: item.data,
hits: item.hits.count,
visitors: item.visitors.count,
bytes: item.bytes.count,
region: region.data
}));
return countryData;
}, []);
}
function formatTooltip(data) {
const d = {...data};
let out = {};
out[0] = GoAccess.Util.fmtValue(d['data'], 'str');
out[1] = metric == 'bytes' ? GoAccess.Util.fmtValue(d['bytes'], 'bytes') : d3.format(',')(d['hits']);
if (metric == 'hits')
out[2] = d3.format(',')(d['visitors']);
const template = d3.select('#tpl-chart-tooltip').html();
return Hogan.compile(template).render({
'data': out
});
}
function mouseover(event, selection, data) {
const tooltip = selection.select('.chart-tooltip-wrap');
tooltip.html(formatTooltip(data))
.style('left', `${d3.pointer(event)[0] + 10}px`)
.style('top', `${d3.pointer(event)[1] + 10}px`)
.style('display', 'block');
}
function mouseout(selection, g) {
const tooltip = selection.select('.chart-tooltip-wrap');
tooltip.style('display', 'none');
}
function drawLegend(selection, colorScale) {
const legendHeight = 10;
const legendPadding = 10;
let svg = selection.select('.legend-svg');
if (svg.empty()) {
svg = selection.append('svg')
.attr('class', 'legend-svg')
.attr('width', width + margin.left + margin.right) // Adjust the width of the SVG
.attr('height', legendHeight + 2 * legendPadding);
}
let legend = svg.select('.legend');
if (legend.empty()) {
legend = svg.append('g')
.attr('class', 'legend')
.attr('transform', `translate(${margin.left}, ${legendPadding})`); // Adjust the position of the legend
}
const legendData = colorScale.quantiles();
const legendRects = legend.selectAll('rect')
.data(legendData);
legendRects.enter().append('rect')
.merge(legendRects)
.attr('x', (d, i) => (i * (innerW())) / legendData.length) // Adjust the x attribute
.attr('y', 0)
.attr('width', (innerW()) / legendData.length) // Adjust the width of the rectangles
.attr('height', legendHeight)
.style('fill', d => colorScale(d));
legendRects.exit().remove();
const legendTexts = legend.selectAll('text')
.data(legendData);
legendTexts.enter().append('text')
.merge(legendTexts)
.attr('x', (d, i) => (i * (innerW())) / legendData.length) // Adjust the x attribute
.attr('y', legendHeight + legendPadding)
.text(d => Math.round(d))
.style('font-size', '10px')
.attr('text-anchor', 'middle')
.text(d => metric === 'bytes' ? GoAccess.Util.fmtValue(d, 'bytes') : d3.format(',')(d));
legendTexts.exit().remove();
}
function updateMap(selection, svg, data, countries, countryNameToGeoJson) {
data = mapData(data);
path = d3.geoPath().projection(projection);
const colorScale = d3.scaleQuantile().domain(data.map(d => d[metric])).range(['#ffffccc9', '#c2e699', '#a1dab4c9', '#41b6c4c9', '#2c7fb8c9']);
if (data.length)
drawLegend(selection, colorScale);
// Create a mapping from country name to data
const dataByName = {};
data.forEach(d => {
const k = d.data.split(' ')[0];
dataByName[k] = d;
});
let country = svg.select('g').selectAll('.country')
.data(countries);
let countryEnter = country.enter().append('path')
.attr('class', 'country')
.attr('d', path)
.attr('opacity', 0); // set initial opacity to 0 for entering elements
country = countryEnter.merge(country)
.on('mouseover', function(event, d) {
const countryData = dataByName[d.id];
if (countryData)
mouseover(event, selection, countryData);
})
.on('mouseout', function(d) {
mouseout(selection);
});
country.transition().duration(500)
.style('fill', function(d) {
const countryData = dataByName[d.id];
return countryData ? colorScale(countryData[metric]) : '#cccccc54';
})
.attr('opacity', 1); // animate opacity to 1
country.exit()
.transition().duration(500)
.attr('opacity', 0) // animate opacity to 0
.remove();
}
function setBounds(projection, maxLat) {
const [yaw] = projection.rotate();
const xymax = projection([-yaw + 180 - 1e-6, -maxLat]); // Top left corner
const xymin = projection([-yaw - 180 + 1e-6, maxLat]); // Bottom right corner
return [xymin, xymax];
}
function zoomed(event, projection, path, scaleExtent, g) {
const newX = event.transform.x % width;
const newY = event.transform.y;
const scale = event.transform.k;
if (scale != slast) {
// Adjust the scale of the projection based on the zoom level
projection.scale(scale * (innerW() / (2 * Math.PI)));
} else {
// Calculate the new longitude based on the x-coordinate
let [longitude] = projection.rotate();
// Use the X translation to rotate, based on the current scale
longitude += 360 * ((newX - tlast[0]) / width) * (scaleExtent[0] / scale);
projection.rotate([longitude, 0, 0]);
// Calculate the new latitude based on the y-coordinate
const b = setBounds(projection, maxLat);
let dy = newY - tlast[1];
if (b[0][1] + dy > 0)
dy = -b[0][1];
else if (b[1][1] + dy < height)
dy = height - b[1][1];
projection.translate([projection.translate()[0], projection.translate()[1] + dy]);
}
// Redraw paths with the updated projection
g.selectAll('path').attr('d', path);
// Save last values
slast = scale;
tlast = [newX, newY];
}
function createSVG(selection) {
const svg = d3.select(selection)
.append('svg')
.attr('class', 'map')
.attr('width', width)
.attr('height', height)
.lower();
const g = svg.append('g')
.attr('transform', `translate(${margin.left}, 0)`)
.attr('transform-origin', '50% 50%');
projection = d3.geoMercator()
.center([0, 15])
.scale([(innerW()) / (2 * Math.PI)])
.translate([(innerW()) / 2, height / 1.5]);
path = d3.geoPath().projection(projection);
// Calculate scale extent and initial scale
const bounds = setBounds(projection, maxLat);
const s = width / (bounds[1][0] - bounds[0][0]);
// The minimum and maximum zoom scales
const scaleExtent = [s, 5 * s];
const zoom = d3.zoom()
.scaleExtent(scaleExtent)
.on('zoom', event => {
zoomed(event, projection, path, scaleExtent, g);
});
svg.call(zoom);
return svg;
}
function chart(selection) {
selection.each(function(data) {
const worldData = window.countries110m;
const countries = topojson.feature(worldData, worldData.objects.countries).features;
const countryNameToGeoJson = {};
countries.forEach(country => {
countryNameToGeoJson[country.properties.name] = country;
});
let svg = d3.select(this).select('svg.map');
// if the SVG element doesn't exist, create it
if (svg.empty())
svg = createSVG(this);
updateMap(selection, svg, data, countries, countryNameToGeoJson);
});
}
chart.metric = function(_) {
if (!arguments.length) return metric;
metric = _;
return chart;
};
chart.opts = function (_) {
if (!arguments.length) return opts;
opts = _;
return chart;
};
chart.width = function(_) {
if (!arguments.length) return width;
width = _;
return chart;
};
chart.height = function(_) {
if (!arguments.length) return height;
height = _;
return chart;
};
return chart;
}
function AreaChart(dualYaxis) {
var opts = {};
var margin = {

2
resources/js/topojson.v3.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -99,6 +99,9 @@
<li class="dropdown-header">{{ labels.type }}</li>
<li><a href="javascript:void(0);" data-panel="{{id}}" data-chart-type="area-spline"><i class="fa fa-circle{{^area-spline}}-o{{/area-spline}}"></i> {{labels.area_spline}}</a></li>
<li><a href="javascript:void(0);" data-panel="{{id}}" data-chart-type="bar"><i class="fa fa-circle{{^bar}}-o{{/bar}}"></i> {{labels.bar}}</a></li>
{{#hasMap}}
<li><a href="javascript:void(0);" data-panel="{{id}}" data-chart-type="wmap"><i class="fa fa-circle{{^wmap}}-o{{/wmap}}"></i> {{labels.wmap}}</a></li>
{{/hasMap}}
<li class="dropdown-header">{{labels.plot_metric}}</li>
{{#plot}}
<li><a href="javascript:void(0);" data-panel="{{id}}" data-plot="{{className}}" class="panel-plot-{{className}}"><i class="fa fa-circle{{^selected}}-o{{/selected}}"></i> {{label}}</a></li>

View File

@ -392,6 +392,8 @@
N_("Area Spline")
#define HTML_REPORT_PANEL_BAR \
N_("Bar")
#define HTML_REPORT_PANEL_WMAP \
N_("World Map")
#define HTML_REPORT_PANEL_PLOT_METRIC \
N_("Plot Metric")
#define HTML_REPORT_PANEL_TABLE_COLS \

View File

@ -53,7 +53,9 @@
#include "facss.h"
#include "appcss.h"
#include "d3js.h"
#include "topojsonjs.h"
#include "hoganjs.h"
#include "countries110m.h"
#include "chartsjs.h"
#include "appjs.h"
@ -67,81 +69,81 @@ static void print_host_metrics (FILE * fp, const GHTML * def, int sp);
/* *INDENT-OFF* */
static GHTML htmldef[] = {
{VISITORS, 1, print_metrics, {
{VISITORS, 1, 0, print_metrics, {
{CHART_AREASPLINE, hits_visitors_plot, 1, 1, NULL, NULL} ,
{CHART_AREASPLINE, hits_bw_plot, 1, 1, NULL, NULL} ,
}},
{REQUESTS, 1, print_metrics, {
{REQUESTS, 1, 0, print_metrics, {
{CHART_VBAR, hits_visitors_req_plot, 0, 0, NULL, NULL},
{CHART_VBAR, hits_bw_req_plot, 0, 0, NULL, NULL},
}},
{REQUESTS_STATIC, 1, print_metrics, {
{REQUESTS_STATIC, 1, 0, print_metrics, {
{CHART_VBAR, hits_visitors_req_plot, 0, 0, NULL, NULL},
{CHART_VBAR, hits_bw_req_plot, 0, 0, NULL, NULL},
}},
{NOT_FOUND, 1, print_metrics, {
{NOT_FOUND, 1, 0, print_metrics, {
{CHART_VBAR, hits_visitors_req_plot, 0, 0, NULL, NULL},
{CHART_VBAR, hits_bw_req_plot, 0, 0, NULL, NULL},
}},
{HOSTS, 1, print_host_metrics, {
{HOSTS, 1, 0, print_host_metrics, {
{CHART_VBAR, hits_visitors_plot, 0, 0, NULL, NULL},
{CHART_VBAR, hits_bw_plot, 0, 0, NULL, NULL},
}},
{OS, 1, print_metrics, {
{OS, 1, 0, print_metrics, {
{CHART_VBAR, hits_visitors_plot, 0, 1, NULL, NULL},
{CHART_VBAR, hits_bw_plot, 0, 1, NULL, NULL},
}},
{BROWSERS, 1, print_metrics, {
{BROWSERS, 1, 0, print_metrics, {
{CHART_VBAR, hits_visitors_plot, 0, 1, NULL, NULL},
{CHART_VBAR, hits_bw_plot, 0, 1, NULL, NULL},
}},
{VISIT_TIMES, 1, print_metrics, {
{VISIT_TIMES, 1, 0, print_metrics, {
{CHART_AREASPLINE, hits_visitors_plot, 0, 1, NULL, NULL},
{CHART_AREASPLINE, hits_bw_plot, 0, 1, NULL, NULL},
}},
{VIRTUAL_HOSTS, 1, print_metrics, {
{VIRTUAL_HOSTS, 1, 0, print_metrics, {
{CHART_VBAR, hits_visitors_plot, 0, 0, NULL, NULL},
{CHART_VBAR, hits_bw_plot, 0, 0, NULL, NULL},
}},
{REFERRERS, 1, print_metrics, {
{REFERRERS, 1, 0, print_metrics, {
{CHART_VBAR, hits_visitors_plot, 0, 0, NULL, NULL},
{CHART_VBAR, hits_bw_plot, 0, 0, NULL, NULL},
}},
{REFERRING_SITES, 1, print_metrics, {
{REFERRING_SITES, 1, 0, print_metrics, {
{CHART_VBAR, hits_visitors_plot, 0, 0, NULL, NULL},
{CHART_VBAR, hits_bw_plot, 0, 0, NULL, NULL},
}},
{KEYPHRASES, 1, print_metrics, {
{KEYPHRASES, 1, 0, print_metrics, {
{CHART_VBAR, hits_visitors_plot, 0, 0, NULL, NULL},
{CHART_VBAR, hits_bw_plot, 0, 0, NULL, NULL},
}},
{STATUS_CODES, 1, print_metrics, {
{STATUS_CODES, 1, 0, print_metrics, {
{CHART_VBAR, hits_visitors_plot, 0, 1, NULL, NULL},
{CHART_VBAR, hits_bw_plot, 0, 1, NULL, NULL},
}},
{REMOTE_USER, 1, print_metrics, {
{REMOTE_USER, 1, 0, print_metrics, {
{CHART_VBAR, hits_visitors_plot, 0, 0, NULL, NULL},
{CHART_VBAR, hits_bw_plot, 0, 0, NULL, NULL},
}},
{CACHE_STATUS, 1, print_metrics, {
{CACHE_STATUS, 1, 0, print_metrics, {
{CHART_VBAR, hits_visitors_plot, 0, 0, NULL, NULL},
{CHART_VBAR, hits_bw_plot, 0, 0, NULL, NULL},
}},
#ifdef HAVE_GEOLOCATION
{GEO_LOCATION, 1, print_metrics, {
{CHART_VBAR, hits_visitors_plot, 0, 1, NULL, NULL},
{GEO_LOCATION, 1, 1, print_metrics, {
{CHART_WMAP, hits_visitors_plot, 0, 1, NULL, NULL},
{CHART_VBAR, hits_bw_plot, 0, 1, NULL, NULL},
}},
{ASN, 1, print_metrics, {
{ASN, 1, 0, print_metrics, {
{CHART_VBAR, hits_visitors_plot, 0, 0, NULL, NULL},
{CHART_VBAR, hits_bw_plot, 0, 0, NULL, NULL},
}},
#endif
{MIME_TYPE, 1, print_metrics, {
{MIME_TYPE, 1, 0, print_metrics, {
{CHART_VBAR, hits_visitors_plot, 0, 1, NULL, NULL},
{CHART_VBAR, hits_bw_plot, 0, 1, NULL, NULL},
}},
{TLS_TYPE, 1, print_metrics, {
{TLS_TYPE, 1, 0, print_metrics, {
{CHART_VBAR, hits_visitors_plot, 0, 1, NULL, NULL},
{CHART_VBAR, hits_bw_plot, 0, 1, NULL, NULL},
}},
@ -158,7 +160,7 @@ static int external_assets = 0;
* On success, the chart type string is returned. */
static const char *
chart2str (GChartType type) {
static const char *strings[] = { "null", "bar", "area-spline" };
static const char *strings[] = { "null", "bar", "area-spline", "wmap" };
return strings[type];
}
@ -332,11 +334,13 @@ print_html_footer (FILE * fp, FILE *fjs)
fprintf (fjs, "%.*s", hogan_js_length, hogan_js);
fprintf (fjs, "%.*s", app_js_length, app_js);
fprintf (fjs, "%.*s", charts_js_length, charts_js);
fprintf (fjs, "%.*s", topojson_js_length, topojson_js);
} else {
fprintf (fp, "<script>%.*s</script>", d3_js_length, d3_js);
fprintf (fp, "<script>%.*s</script>", hogan_js_length, hogan_js);
fprintf (fp, "<script>%.*s</script>", app_js_length, app_js);
fprintf (fp, "<script>%.*s</script>", charts_js_length, charts_js);
fprintf (fp, "<script>%.*s</script>", topojson_js_length, topojson_js);
}
/* load custom JS file, if any */
@ -1088,6 +1092,7 @@ print_panel_def_meta (FILE *fp, const GHTML *def, int sp) {
fpskeysval (fp, "id", id, isp, 0);
fpskeyival (fp, "table", def->table, isp, 0);
fpskeyival (fp, "hasMap", def->has_map, isp, 0);
print_def_sort (fp, def, isp);
print_def_plot (fp, def, isp);
@ -1176,6 +1181,7 @@ print_json_i18n_def (FILE *fp) {
{"type" , HTML_REPORT_PANEL_TYPE} ,
{"area_spline" , HTML_REPORT_PANEL_AREA_SPLINE} ,
{"bar" , HTML_REPORT_PANEL_BAR} ,
{"wmap" , HTML_REPORT_PANEL_WMAP} ,
{"plot_metric" , HTML_REPORT_PANEL_PLOT_METRIC} ,
{"table_columns" , HTML_REPORT_PANEL_TABLE_COLS} ,
{"thead" , T_HEAD} ,
@ -1223,6 +1229,8 @@ print_json_defs (FILE *fp) {
print_json_i18n_def (fp);
fprintf (fp, ";");
fprintf (fp, "var html_prefs=%s;", conf.html_prefs ? conf.html_prefs : "{}");
fprintf (fp, "var countries110m=%.*s;", countries_json_length, countries_json);
fprintf (fp, "var html_prefs=%s;", conf.html_prefs ? conf.html_prefs : "{}");
fprintf (fp, "var user_interface=");
fpopen_obj (fp, 0);

View File

@ -46,6 +46,7 @@ typedef enum GChartType_ {
CHART_NONE,
CHART_VBAR,
CHART_AREASPLINE,
CHART_WMAP,
} GChartType;
/* Chart axis structure */
@ -74,6 +75,7 @@ typedef struct GHTMLPlot_ {
typedef struct GHTML_ {
GModule module;
int8_t table;
int8_t has_map;
void (*metrics) (FILE * fp, const struct GHTML_ * def, int sp);
GHTMLPlot chart[MAX_PLOTS];
} GHTML;